07-Seq2Seq英译法案例
Seq2Seq英译法案例
1 任务目的:
目的: 给定一段英文,翻译为法文
典型的文本分类(token分类)任务: 每个时间步去预测应该属于哪个法文单词
2 数据格式
-
注意:两列数据,第一列是英文文本,第二列是法文文本,中间用制表符号"\t"隔开
i am from brazil . je viens du bresil .
i am from france . je viens de france .
i am from russia . je viens de russie .
i am frying fish . je fais frire du poisson .
i am not kidding . je ne blague pas .
3 任务实现流程
1. 获取数据:案例中是直接给定的
2. 数据预处理: 脏数据清洗、数据格式转换、数据源Dataset的构造、数据迭代器Dataloader的构造
3. 模型搭建: 编码器和解码器等一系列模型
4. 模型评估(测试)
5. 模型上线---API接口
4 数据预处理
4.1 定义样本清洗函数和构建字典
目的:
样本清洗函数: 将脏数据进行清洗,以免影响模型训练
构建字典:一方面是为了将文本进行数字表示,还有一方面进行解码的时候将预测索引数字映射为真实的文本
样本清洗函数代码实现
# 文本清洗工具函数
def normalizeString(s):"""字符串规范化函数, 参数s代表传入的字符串"""s = s.lower().strip()# 在.!?前加一个空格 这里的\1表示第一个分组 正则中的\nums = re.sub(r"([.!?])", r" \1", s)# s = re.sub(r"([.!?])", r" ", s)# 使用正则表达式将字符串中 不是 大小写字母和正常标点的都替换成空格s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)return s
构建字典代码实现
def my_getdata():# 1.读取数据with open(data_path, 'r' , encoding='utf-8') as fr:sentens_str = fr.read()sentences = sentens_str.strip().split('\n')
# 2.构建数据源pairmy_pairs = [[normalizeString(s) for s in l.split('\t')] for l in sentences]
# 3.1 初始化两个字典english_word2index = {"SOS": 0, "EOS": 1}english_word_n = 2french_word2index = {"SOS": 0, "EOS": 1}french_word_n = 2# 3.2遍历my_pairsfor pair in my_pairs:for word in pair[0].split(' '):if word not in english_word2index:english_word2index[word] = english_word_nenglish_word_n += 1# english_word2index[word] = len(english_word2index)
for word in pair[1].split(' '):if word not in french_word2index:french_word2index[word] = french_word_nfrench_word_n += 1
english_index2word = {v: k for k, v in english_word2index.items()}french_index2word = {v: k for k, v in french_word2index.items()}return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairs
4.2 构建自己的数据源DataSet
目的:
使用Pytorch框架,一般遵从一个规矩:使用DataSet方法构造数据源,来让模型进行使用
构造数据源的过程中:必须继承torch.utils.data.Dataset类,必须构造两个魔法方法:__len__(), __getitem__()
__len__(): 一般返回的是样本的总个数,我们可以直接len(dataset对象)直接就可以获得结果
__getitem__(): 可以根据某个索引取出样本值,我们可以直接用dataset对象[index]来直接获得结果
代码实现:
# 3.构建数据源Dataset
class Seq2SeqDaset(Dataset):def __init__(self, my_pairs):self.my_pairs = my_pairsself.sample_len = len(my_pairs)
def __len__(self):return self.sample_len
def __getitem__(self, index):# 1.index异常值处理[0, self.sample_len-1]index = min(max(index, 0), self.sample_len-1)
# 2. 根据index取出样本数据x = self.my_pairs[index][0]y = self.my_pairs[index][1]
# 3.进行文本数据数字化的转换x1 = [english_word2index[word] for word in x.split(' ')]tensor_x = torch.tensor(x1, dtype=torch.long, device=device)
y1 = [french_word2index[word] for word in y.split(' ')]y1.append(EOS_token)tensor_y = torch.tensor(y1, dtype=torch.long, device=device)
return tensor_x, tensor_y
4.3 构建数据源Dataloader
目的:
为了将Dataset我们上一步构建的数据源,进行再次封装,变成一个迭代器,可以进行for循环,而且,可以自动为我们dataset里面的数据进行增维(bath_size),也可以随机打乱我们的取值顺序
代码实现:
# 4.构建数据迭代器dataloader
def get_dataloader():# 1.实例化datasetmy_dataset = Seq2SeqDaset(my_pairs)# 2.实例化dataloadermy_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
return my_dataloader
5 模型搭建
5.1 搭建编码器GRU模型
-
注意事项
GRU模型在实例化的时候,默认batch_first=False,因此,需要小心输入数据的形状
因为: dataloader返回的结果x---》shape--〉[batch_size, seq_len, input_size], 所以课堂上代码和讲义稍微有点不同,讲义是默认的batch_first=False,而我们的代码是batch_first=True,这样做的目的,可以直接承接x的输入。
-
代码实现
# 5. 构建GRU编码器模型
class EncoderGRU(nn.Module):def __init__(self, vocab_size, hidden_size):super().__init__()# 1.vocab_size代表英文单词的总个数(去重)self.vocab_size = vocab_size# 2. hidden_size词嵌入维度/隐藏层输出维度(我们让他相等)self.hidden_size = hidden_size# 3.定义Embedding层,目的:将每个词汇进行向量表示self.embed = nn.Embedding(self.vocab_size, self.hidden_size)# 4.定义GRU层第一个self.hidden_size实际上是embedding的输出结果词嵌入维度# 4.定义GRU层第二个self.hidden_size实是我们指定的GRU模型的输出维度,只不过这里GRU输入和输出一样self.gru = nn.GRU(self.hidden_size, self.hidden_size, batch_first=True)def forward(self, input, hidden):# 1.input-->[1, 6]需要经过embedding--》[1,6, 256]input_x = self.embed(input)# 2.将input_x和hidden送入GRU模型output, hidden = self.gru(input_x, hidden)return output, hidden
def inithidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)
编码器模型测试
# 测试编码器模型
def test_EncoderGRU():# 获取数据my_dataloader = get_dataloader()# 实例化模型my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)my_encoder.to(device)# 初始化h0h0 = my_encoder.inithidden()
for i, (x, y) in enumerate(my_dataloader):print(f'x---》{x.shape}')output, hn = my_encoder(x, h0)print(f'output--》{output.shape}')print(f'hn--》{hn.shape}')break
5.2 搭建解码器无Attention模型
-
代码实现
#6. 构建没有attention的GRU解码器
class DecoderGRU(nn.Module):def __init__(self, vocab_size, hidden_size):super().__init__()# vocab_size代表解码器中法语单词的词汇总量(去重)self.vocab_size = vocab_size# hidden_size词嵌入维度self.hidden_size = hidden_size# Embedding层self.embed = nn.Embedding(self.vocab_size, self.hidden_size)
# GRU层:这里定义GRU模型的输入和输出形状一样self.gru = nn.GRU(self.hidden_size, self.hidden_size, batch_first=True)
# 定义输出层:判断法语单词属于self.vocab_size里面的哪一个self.out = nn.Linear(self.hidden_size, self.vocab_size)
# 定义softmax层self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden):# input输入一般是一个字,解码的时候,是一个字符一个字符解码的# 1. input-->[1, 1]-->embeding之后--》[1, 1, hidden_size]-->[1, 1, 256]input_x = self.embed(input)# 2. relu激活函数使用input_x = F.relu(input_x)# 3. 将数据送入GRU模型:input_x-->[1,1,256],hidden:[1, 1, 256]# output-->[1, 1, 256]output, hidden = self.gru(input_x, hidden)
# 4. 将output结果取最后一个词隐藏层输出送入linear层# output-->[1, vocab_size]-->[1, 4345]output = self.out(output[0])return self.softmax(output), hidden
def inithidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)
代码测试
# 测试没有attention的解码器
def test_DecoderGRU():# 1.实例化datasetmydataset = Seq2SeqDaset(my_pairs)# 2.实例化dataloadermy_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)
# 3.实例化编码器模型my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)# my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)my_encoder.to(device)# 4. 实例化解码器模型my_decoder = DecoderGRU(vocab_size=french_word_n, hidden_size=256)my_decoder.to(device)
# 5.将数据送入模型for x, y in my_dataloader:print(x)print(f'x--->{x.shape}')print(f'y--->{y.shape}')print(f'y--->{y}')# 5.1将x英文原始输入送入编码器模型得到编码结果;hidden就是Coutput, hidden = my_encoder(input=x, hidden=my_encoder.inithidden())# 5.2基于C开始一个字符一个字符的去解码for i in range(y.shape[1]):# print(f'y[0]-->{y[0]}')# print(f'y[0][i]-->{y[0][i]}')temp = y[0][i].view(1, -1)output, hidden = my_decoder(input=temp, hidden=hidden)print(f'output--》{output.shape}')break
5.3 搭建解码器带Attention模型
-
注意事项
带Attention:需要有三个参数:Q、K、V,在本次案例中Q上一时间步预测的真实结果;K:上一时间步隐藏层输出的结果;V代表编码器的输出结果
-
代码实现
# 7.带attention的解码器
class AttentionDecoderGRU(nn.Module):def __init__(self, vocab_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):super().__init__()# vocab_size:属于解码器端,代表法语的总的单词个数self.vocab_size = vocab_size# hidden_size:代表词嵌入的维度self.hidden_size = hidden_size# 随机失活概率self.dropout_p = dropout_p# 最大句子长度:因为训练语料里面不管英文还是法文最大句子长度都不超过10,我们这里限定最大长度,目的是方便计算注意力self.max_length = max_length
# 定义Embedding层self.embed = nn.Embedding(self.vocab_size, self.hidden_size)# 计算注意力的第一个全连接层:得到权重分数self.attn = nn.Linear(self.hidden_size*2, self.max_length)# 随机失活层self.droupout = nn.Dropout(p=self.dropout_p)# 计算注意力的第二个全连接层:让注意力按照指定维度输出self.attn_combin = nn.Linear(self.hidden_size*2, self.hidden_size)# 定义GRU层self.gru = nn.GRU(self.hidden_size, self.hidden_size, batch_first=True)# 定义输出层self.out = nn.Linear(self.hidden_size, self.vocab_size)
# 定义softmax层self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden, encoder_output):# input-->query--》解码器输入某个词[1, 1]# hidden-->key--》上一时间步隐藏层输出[1, 1, 256]# encoder_output--->value-->编码器的输出结果[10, 256]--[max_length, 256]# 1. 将input送入embedding-->input_x-->[1,1,256]--query(真正)input_x = self.embed(input)# 1.1对input_x进行dropoutinput_x = self.droupout(input_x)# 2. 计算注意力权重分数self.attn_weight-->[1, 10]-->[1, max_length]attn_weight = F.softmax(self.attn(torch.cat((input_x[0], hidden[0]), dim=-1)), dim=-1)# 3. 将注意力权重和Value相乘:[1, 1,10]*[1,10,256]-->self.attn_applied-->[1, 1,256]attn_applied = torch.bmm(attn_weight.unsqueeze(0), encoder_output.unsqueeze(0))# 4.将query和self.attn_applied结果拼接之后,再经过线性的变换self.output1-->[1, 1, 256]output1 = self.attn_combin(torch.cat((input_x[0], attn_applied[0]), dim=-1)).unsqueeze(0)# 5. 经过relu激活函数relu_output = F.relu(output1)# 6. 将self.relu_output,以及hidden送入GRU模型中-->gru_output-->[1, 1,256]gru_output, hidden = self.gru(relu_output , hidden)
# 7.将gru的结果送入输出层,得到最后的预测结果output-->[1, 4345]output = self.out(gru_output[0])return self.softmax(output), hidden, attn_weight
def inithidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)
模型测试
# 测试带attention的解码器
def test_AttenDecoder():# 1.实例化datasetmydataset = Seq2SeqDaset(my_pairs)# 2.实例化dataloadermy_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)
# 3.实例化编码器模型my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)# my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)my_encoder.to(device)
# 4.实例化解码器模型my_attenDecoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256)my_attenDecoder.to(device)
#5.循环数据送入模型for x, y in my_dataloader:print(f'x--》{x.shape}')print(f'y--》{y.shape}')# 1.将x送入编码器模型得到结果h0 = my_encoder.inithidden()encoder_output, hidden = my_encoder(input=x, hidden=h0)# print(f'encoder_output--》{encoder_output.shape}')# 2.将编码的结果进行处理,统一长度,方便计算注意力encoder_output_c = torch.zeros(MAX_LENGTH, my_encoder.hidden_size, device=device)# print(f'encoder_output_c--》{encoder_output_c.shape}')
# 2.1将真实的编码的输出 结果赋值到encoder_output_c中,多余的都是用0来表示for i in range(encoder_output.shape[1]):# print(f'encoder_output[0][i]-->{encoder_output[0][i].shape}')# print(f'encoder_output[0, i]-->{encoder_output[0, i].shape}')encoder_output_c[i] = encoder_output[0][i]# 3.测试:进行解码应用for j in range(y.shape[1]):temp = y[0][j].view(1, -1)output, hidden, attn_weight = my_attenDecoder(temp, hidden, encoder_output_c)print(f'output--》{output.shape}')print(f'hidden--》{hidden.shape}')print(f'attn_weight--》{attn_weight.shape}')print("*"*80)break
6 模型训练
基本过程
1.获取数据
2.构建数据源Dataset
3.构建数据迭代器Dataloader
4.实例化自定义的模型: 编码器模型和解码器模型
5.实例化损失函数对象
6.实例化优化器对象: 编码器优化器和解码器优化器
7.定义打印日志参数
8.开始训练
8.1 实现外层大循环epoch
(可以在这构建数据迭代器Dataloader)
8.2 内部遍历数据迭代球dataloader
8.3 将数据送入模型得到输出结果
8.4 计算损失
8.5 梯度清零: optimizer.zero_grad()
8.6 反向传播: loss.backward()
8.7 参数更新(梯度更新): optimizer.step()
8.8 打印训练日志
9. 保存模型: torch.save(model.state_dict(), "model_path")
6.1 模型训练代码实现
# 8.构建模型的训练函数
def train_seq2seq():# 1.实例化datasetmydataset = Seq2SeqDaset(my_pairs)# 2.实例化dataloadermy_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)
# 3.实例化编码器模型my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)# my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)my_encoder.to(device)
# 4.实例化解码器模型my_attenDecoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256)my_attenDecoder.to(device)
# 5.实例化优化器encoder_optimizer = optim.Adam(my_encoder.parameters(), lr=mylr)decoder_optimizer = optim.Adam(my_attenDecoder.parameters(), lr=mylr)
# 6.实例化损失对象crossentropy = nn.NLLLoss()
# 7.定义一个空列表list--》存储损失值,画图plot_loss_list = []
# 8. 进入外层循环for epoch_idx in range(epochs):# 初始化损失值为0print_loss_total, plot_loss_total = 0.0, 0.0start_time = time.time()# 进入内部循环for i, (x, y) in enumerate(tqdm(my_dataloader), start=1):myloss = Train_Iters(x, y, my_encoder,my_attenDecoder,encoder_optimizer,decoder_optimizer,crossentropy)# print(f'主训练了函数的myloss--》{myloss}')print_loss_total += mylossplot_loss_total += myloss
# 打印日志# 每隔1000步打印损失if i % 10 == 0:print_loss_avg = print_loss_total / 1000
print_loss_total = 0use_time = time.time() - start_timeprint(f'当前的轮次%d,平均损失%.4f,时间%.2f'%(epoch_idx+1, print_loss_avg.item()*100, use_time))
# 每隔100步保留损失,画图if i % 10 == 0:plot_loss_avg = plot_loss_total / 100# 如果画图报错:放到CPU--》plot_loss_avg.cpu().detach().numpy()plot_loss_list.append(plot_loss_avg.cpu().detach().numpy())plot_loss_total = 0
# 保存模型torch.save(my_encoder.state_dict(), './ai19_model/my_encoder_%s.pth'%(epoch_idx+1))torch.save(my_attenDecoder.state_dict(), './ai19_model/my_decoder_%s.pth'%(epoch_idx+1))
# 画图plt.figure()plt.plot(plot_loss_list)plt.savefig("./ai19_seq2se1_loss.png")plt.show()
return plot_loss_list
6.2 模型训练内部迭代函数代码实现
# 定义内部迭代函数
def Train_Iters(x, y, my_encoder, my_attenDecoder, encoder_optimizer, decoder_optimizer, crossentropy):# 1.将x送入编码器得到编码的结果# print(f'x-->{x.shape}')# print(f'y-->{y.shape}')h0 = my_encoder.inithidden()encoder_output, encoder_hidden = my_encoder(x, h0)# print(f'encoder_output--》{encoder_output.shape}')# print(f'encoder_hidden--》{encoder_hidden.shape}')# 2. 定义解码器的参数# 2.1 中间语意张量C:valueencoder_output_c = torch.zeros(MAX_LENGTH, my_encoder.hidden_size, device=device)for i in range(x.shape[1]):encoder_output_c[i] = encoder_output[0][i]# 2.2 解码器的初始化的hidden, keydecoder_hidden = encoder_hidden# 2.3 解码器的初始化输出:queryinput_y = torch.tensor([[SOS_token]], dtype=torch.long, device=device)
# 3.定义一个初始化的损失my_loss = 0.0# 4.选择性的使用teacher_forcing策略teacher_forcing = True if random.random() < teacher_forcing_ratio else False# 5.开始计算损失if teacher_forcing:for i in range(y.shape[1]):# output_y--》[1, 4345]output_y, decoder_hidden, attn_weight =my_attenDecoder(input_y, decoder_hidden, encoder_output_c)# 根据预测结果计算损失target_y = y[0][i].view(1)# print(f'target_y--》{target_y}')
my_loss = my_loss + crossentropy(output_y, target_y)# print(f'my_loss--》{my_loss}')# 将真实的下一个单词当作input_yinput_y = y[0][i].view(1, -1)# print(f'input_y--》{input_y}')else:for i in range(y.shape[1]):# output_y--》[1, 4345]output_y, decoder_hidden, attn_weight = my_attenDecoder(input_y, decoder_hidden, encoder_output_c)# 根据预测结果计算损失target_y = y[0][i].view(1)my_loss = my_loss + crossentropy(output_y, target_y)topv, topi = output_y.topk(1)# 如果output_y预测的最大值对应的索引刚好等EOS,直接终止if topi.squeeze().item() == EOS_token:break# 将预测结果的当作下一个input_yinput_y = topi.detach()
# 6. 梯度清零encoder_optimizer.zero_grad()decoder_optimizer.zero_grad()# 7. 反向传播my_loss.backward()# 8.梯度更新encoder_optimizer.step()decoder_optimizer.step()
return my_loss / y.shape[1]
7 模型预测
基本过程
1.获取数据
2.数据预处理
3.实例化模型: 编码器和解码器
4.加载模型训练好的参数: model.load_state_dict(torch.load("model_path"))
5.with torch.no_grad():
6.将数据送入模型进行预测(注意:张量的形状变换)
主代码实现
# 9.定义模型评估/预测函数
def test_Seq2Seq_Evaluate():# 1.加载训练好的编码器模型my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)my_encoder.load_state_dict(torch.load("./ai19_model/my_encoder_1.pth"))# my_encoder.to(device)print(my_encoder)# 2.加载训练好的解码器模型my_decoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256).to(device)my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth"))# my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth", map_location="cpu"), strict=False)print(my_decoder)print('*'*80)# 3.准备样本
my_samplepairs =[['i m impressed with your french .', 'je suis impressionne par votre francais .'],['i m more than a friend .', 'je suis plus qu une amie .'],['she is beautiful like her mother .', 'elle est belle comme sa mere .']]print('my_samplepairs--->', len(my_samplepairs))
# 4. 将样本输入模型得到结果for index, pair in enumerate(my_samplepairs[2:]):x = pair[0]y = pair[1]# 需要对x英文文本进行处理x_word2id = [english_word2index[word] for word in x.split(' ')]# print(f'x_word2id--》{x_word2id}')tensor_x = torch.tensor([x_word2id], dtype=torch.long, device=device)# print(f'tensor_x-->{tensor_x}')decoder_words, attention_weights = evaluate_seq2seq(tensor_x, my_encoder, my_decoder)decoder_french = ' '.join(decoder_words)print("*"*80)print(f'原始的英文输入文本是---》{x}')print(f'原始的法文标签文本是---》{y}')print(f'模型预测的法文---》{decoder_french}')
内部评估代码实现
def evaluate_seq2seq(x, encoder, decoder):# x代表当前需要翻译的英文文本# encoder代表编码器模型# decoder代表解码器模型# 1. 将x送入编码器得到编码结果# print(f'x---》{x.shape}')h0 = encoder.inithidden()encoder_outputs, encoder_hidden = encoder(x, h0)# 2. 准备解码的参数# 2.1 编码器结果--Valueencoder_outputs_c = torch.zeros(MAX_LENGTH, encoder.hidden_size, device=device)for index in range(x.shape[1]):encoder_outputs_c[index] = encoder_outputs[0][index]# 2.2 解码器的keydecode_hidden = encoder_hidden
# 2.3 解码器的原始输入input_y = torch.tensor([[SOS_token]], dtype=torch.long, device=device)
# 3.准备变量# 3.1 存储模型的预测结果decoder_words = []# 3.2 初始化一个全0的权重矩阵:存储每一步解码出来的注意力权重attention_weights = torch.zeros(MAX_LENGTH, MAX_LENGTH, device=device)# 4.进行模型的预测index = 0
for i in range(MAX_LENGTH):output_y, decode_hidden, atten = decoder(input_y, decode_hidden, encoder_outputs_c)# 获取output_y预测的最大概率值对应索引topv, topi = output_y.topk(1)# print(f'topi--》{topi}')# print(f'atten-->{atten}')# 将真实的注意力权重赋值给attention_weights# print(f' attention_weights[i]-->{ attention_weights[i]}')attention_weights[i] = attenif topi.item() == EOS_token:decoder_words.append("<EOS>")breakelse:decoder_words.append(french_index2word[topi.item()])input_y = topi.detach()index = i
print(f'attention_weights--》{attention_weights}')return decoder_words, attention_weights[:index+1]
注意力图
# 10.注意力图展示
def test_attention_plot():# 1.加载训练好的编码器模型my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)my_encoder.load_state_dict(torch.load("./ai19_model/my_encoder_1.pth"))# my_encoder.to(device)print(my_encoder)# 2.加载训练好的解码器模型my_decoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256).to(device)my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth"))# my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth", map_location="cpu"), strict=False)print(my_decoder)print('*' * 80)sentence = "we re both teachers ."# 需要对x英文文本进行处理x_word2id = [english_word2index[word] for word in sentence.split(' ')]# print(f'x_word2id--》{x_word2id}')tensor_x = torch.tensor([x_word2id], dtype=torch.long, device=device)# print(f'tensor_x-->{tensor_x}')decoder_words, attention_weights = evaluate_seq2seq(tensor_x, my_encoder, my_decoder)
plt.matshow(attention_weights.cpu().detach().numpy())plt.savefig('./ai19_attention.png')plt.show()