生成模型的输出序列
这段代码是一个基本的序列到序列模型中的解码部分,用于生成模型的输出序列。具体而言,这个代码通过模型的encoder对输入序列进行编码,然后使用模型的decoder对编码后的信息进行解码,从而生成模型的输出序列。下面是这段代码的具体解释:
res = []:创建一个空列表,用于存储模型生成的输出序列(字符串)。
encoder_outputs, hidden = model.encoder(tokens_idx.unsqueeze(0).to(device)):将输入序列(tokens_idx)通过模型的encoder进行编码。tokens_idx.unsqueeze(0)将输入序列的维度从(seq_len,)转换为(1, seq_len),以便在encoder中进行计算。这个编码过程会产生一个编码后的输出(encoder_outputs)和一个encoder的最后隐藏状态(hidden)。
inputs = torch.tensor([SOS_IDX]).to(device):创建一个包含起始标记索引的张量对象(inputs),作为decoder的第一个输入。这个起始标记用于表示序列的开始位置。
for t in range(1, 100)::循环100次,最多生成100个输出符号。
output, hidden = model.decoder(inputs, hidden, encoder_outputs):使用当前的decoder输入、encoder的隐藏状态和编码后的输出,通过decoder生成下一个输出。这个过程会产生一个输出张量(output)和一个decoder的最后隐藏状态(hidden)。
inputs = output.argmax(1):选择输出张量中概率最大的符号,并将其作为下一个decoder的输入。
word = id2vocab[inputs.item()]:将当前选择的符号(即上一步中选择的符号)转换为一个字符串,存储在word变量中。
res.append(word):将当前选择的符号添加到输出序列中。
if word == ‘’::检查当前选择的符号是否为结束符号,如果是,则退出循环。
print(‘’.join(res)):将生成的输出序列打印到屏幕上,使用空格连接所有符号,形成一个字符串。
总之,这段代码用于生成一个基本的序列到序列模型的输出序列,通过循环从decoder中不断生成下一个符号,并将其添加到输出序列中,直到生成了结束符号或达到了输出序列的最大长度。
res = [] encoder_outputs, hidden = model.encoder(tokens_idx.unsqueeze(0).to(device)) inputs = torch.tensor([SOS_IDX]).to(device) for t in range(1, 25): output, hidden = model.decoder(inputs, hidden, encoder_outputs) inputs = output.argmax(1) word = id2vocab[inputs.item()] res.append(word) if word == '<eos>': break print(''.join(res))
model.py模型结构定义
模型结构定义代码
# -*- coding: utf-8 -*- import random import torch.nn as nn import torch import torch.nn.functional as F
对应下面的公式
Encoder函数 编码器
Encoder函数构建一个encoder,内部RNN使用了torch内置的GRU,参数为:
input_dim:输入词表的大小
emb_dim:embedding词向量的维度
hid_dim:隐藏层的维度,即ht,ct的维度
n_layers:LSTM的层数
dropout:dropout的概率,减少过拟合
forward参数:
[batch size, src len, emb dim]src len填充后句子的长度
hidden = [n layers * n directions, batch size, hid dim]
[2,1,512]
512:这个就是隐藏层维度
1:batchsize
2:由于我们采用了两层的LSTM,所以输出就有2个h,然后叠在了一起
z1=(h[0],c[0]), z2=(h[1],c[1])
具体实现时,矩阵维度的变换比较繁琐,为了矩阵的运算经常需要增减维度或者交换维度的顺序,代码中已给出标注,建议自己调试一遍,感受维度变换过程。
encoder的输入为原文,输出为hidden_state,size需要设置
class Encoder(nn.Module): def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout): super().__init__() self.hid_dim = hid_dim self.n_layers = n_layers self.embedding = nn.Embedding(input_dim, emb_dim) self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True) self.dropout = nn.Dropout(dropout) def forward(self, src): # src = [batch size, src len] # 对输入的数据进行embedding操作 embedded = self.dropout(self.embedding(src)) # embedded = [batch size, src len, emb dim] outputs, (hidden, cell) = self.rnn(embedded) # outputs = [batch size, src len, hid dim * n directions] # hidden = [n layers * n directions, batch size, hid dim] # cell = [n layers * n directions, batch size, hid dim] # outputs are always from the top hidden layer return hidden, cell
解码器
其中,
然后,我们将隐藏状态从 RNN 的顶层传递到线性层 f ,以预测目标(输出)序列中的下一个标记应该是什么
1、这里只是decoder的一部分(即一个黄色方块)
因为我们需要对它的每一个输出进行处理和预测,所以它的输入并不像encoder是一个句子,它的输入只是一个单词
2、它的第一个输入只是input.unsqueeze(0)
因为此时我们的输入只是一个单词,那么此时输入的维度就是[batch size]
LSTM的输入是要[句子长度,batch size],所以需要unsqueeze(0),即告诉它我们的句子长度为0
3、这个时候output
只需要用最上一层的RNN的输出即可,所以不需要hidden,cell
class Decoder(nn.Module): def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout): super().__init__() self.output_dim = output_dim self.hid_dim = hid_dim self.n_layers = n_layers self.embedding = nn.Embedding(output_dim, emb_dim) self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True) self.fc_out = nn.Linear(hid_dim, output_dim) self.dropout = nn.Dropout(dropout) def forward(self, inputs, hidden, cell): # inputs = [batch size] # hidden = [n layers * n directions, batch size, hid dim] # cell = [n layers * n directions, batch size, hid dim] # n directions in the decoder will both always be 1, therefore: # hidden = [n layers, batch size, hid dim] # cell = [n layers, batch size, hid dim] inputs = inputs.unsqueeze(1) # inputs = [batch size, 1] embedded = self.dropout(self.embedding(inputs)) # embedded = [batch size, 1, emb dim] output, (hidden, cell) = self.rnn(embedded, (hidden, cell)) # output = [batch size, seq len, hid dim * n directions] # hidden = [n layers * n directions, batch size, hid dim] # cell = [n layers * n directions, batch size, hid dim] # seq len and n directions will always be 1 in the decoder, therefore: # output = [batch size, 1, hid dim] # hidden = [n layers, batch size, hid dim] # cell = [n layers, batch size, hid dim] prediction = self.fc_out(output.squeeze(1)) # prediction = [batch size, output dim] return prediction, hidden, cell
Seq2Seq
1、teacher_forcing_ratio
这里使用的ratio就表示不一定所有输入都是teacher_forcing的,有概率会出现输入由上一个输出确定,当然这不代表上一个输出都是错的。
2、输入和输出对
3、计算loss的时候丢掉第一位的元素:
至此,我们就成功搭建好Seq2seq了
class Seq2Seq(nn.Module): def __init__(self, encoder, decoder, device): super().__init__() self.encoder = encoder self.decoder = decoder self.device = device assert encoder.hid_dim == decoder.hid_dim, \ "Hidden dimensions of encoder and decoder must be equal!" assert encoder.n_layers == decoder.n_layers, \ "Encoder and decoder must have equal number of layers!" def forward(self, src, trg, teacher_forcing_ratio=0.2): # src = [batch size, src len] # trg = [batch size, trg len] # teacher_forcing_ratio is probability to use teacher forcing # e.g. if teacher_forcing_ratio is 0.75 we use ground-truth inputs 75% of the time batch_size = trg.shape[0] trg_len = trg.shape[1] trg_vocab_size = self.decoder.output_dim # tensor to store decoder outputs outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device) # last hidden state of the encoder is used as the initial hidden state of the decoder hidden, cell = self.encoder(src) # first inputs to the decoder is the <sos> tokens inputs = trg[:, 0] for t in range(1, trg_len): # insert inputs token embedding, previous hidden and previous cell states # receive output tensor (predictions) and new hidden and cell states output, hidden, cell = self.decoder(inputs, hidden, cell) # place predictions in a tensor holding predictions for each token outputs[:, t, :] = output # decide if we are going to use teacher forcing or not teacher_force = random.random() < teacher_forcing_ratio # get the highest predicted token from our predictions top1 = output.argmax(1) # if teacher forcing, use actual next token as next inputs # if not, use predicted token inputs = trg[:, t] if teacher_force else top1 return outputs
train_eval.py
模型训练+验证
# -*- coding: utf-8 -*- import torch import torch.nn as nn import torch.optim as optim import matplotlib.pyplot as plt import numpy as np from tqdm import tqdm from load_data import train_iter, val_iter, id2vocab, PAD_IDX from model import Encoder, Decoder, Seq2Seq device = "cuda" if torch.cuda.is_available() else 'cpu' # device = torch.device('cuda:3') INPUT_DIM = len(id2vocab) OUTPUT_DIM = len(id2vocab) ENC_EMB_DIM = 256 DEC_EMB_DIM = 256 HID_DIM = 512 N_LAYERS = 2 ENC_DROPOUT = 0.5 DEC_DROPOUT = 0.5 N_EPOCHS = 10 CLIP = 1 enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT) dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) model = Seq2Seq(enc, dec, device).to(device)
权重初始化
权重初始化对于训练神经网络至关重要,好的初始化权重可以有效的避免梯度消失等问题的发生。
def init_weights(m): for name, param in m.named_parameters(): nn.init.uniform_(param.data, -0.08, 0.08) model.apply(init_weights)
优化算法
参考:https://blog.csdn.net/kgzhang/article/details/77479737
torch.optim是一个实现了多种优化算法的包,大多数通用的方法都已支持,提供了丰富的接口调用
为了使用torch.optim,需先构造一个优化器对象Optimizer,用来保存当前的状态,并能够根据计算得到的梯度来更新参数。
要构建一个优化器optimizer,你必须给它一个可进行迭代优化的包含了所有参数(所有的参数必须是变量s)的列表。 然后,可以指定程序优化特定的选项,例如学习速率,权重衰减等。
Adam的特点有:
1、结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点;
2、对内存需求较小;
3、为不同的参数计算不同的自适应学习率;
4、也适用于大多非凸优化-适用于大数据集和高维空间。
optimizer = optim.Adam(model.parameters(), lr=5e-5) # we ignore the loss whenever the target token is a padding token. criterion = nn.CrossEntropyLoss(ignore_index=PAD_IDX)
模型训练+验证
loss_vals = [] loss_vals_eval = [] for epoch in range(N_EPOCHS): model.train() epoch_loss = [] pbar = tqdm(train_iter) pbar.set_description("[Train Epoch {}]".format(epoch)) for batch in pbar: trg= batch.trg src = batch.src trg, src = trg.to(device), src.to(device) model.zero_grad() output = model(src, trg) # trg = [batch size, trg len] # output = [batch size, trg len, output dim] output_dim = output.shape[-1] output = output[:, 1:, :].reshape(-1, output_dim) trg = trg[:, 1:].reshape(-1) # trg = [(trg len - 1) * batch size] # output = [(trg len - 1) * batch size, output dim] loss = criterion(output, trg) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), CLIP) epoch_loss.append(loss.item()) optimizer.step() pbar.set_postfix(loss=loss.item()) loss_vals.append(np.mean(epoch_loss)) model.eval() epoch_loss_eval = [] pbar = tqdm(val_iter) pbar.set_description("[Eval Epoch {}]".format(epoch)) for batch in pbar: trg= batch.trg src = batch.src trg, src = trg.to(device), src.to(device) model.zero_grad() output = model(src, trg) # trg = [batch size, trg len] # output = [batch size, trg len, output dim] output_dim = output.shape[-1] output = output[:, 1:, :].reshape(-1, output_dim) trg = trg[:, 1:].reshape(-1) # trg = [(trg len - 1) * batch size] # output = [(trg len - 1) * batch size, output dim] loss = criterion(output, trg) epoch_loss_eval.append(loss.item()) pbar.set_postfix(loss=loss.item()) loss_vals_eval.append(np.mean(epoch_loss_eval))
打印loss图像
torch.save(model.state_dict(), 'model.pt') l1, = plt.plot(np.linspace(1, N_EPOCHS, N_EPOCHS).astype(int), loss_vals) l2, = plt.plot(np.linspace(1, N_EPOCHS, N_EPOCHS).astype(int), loss_vals_eval) plt.legend(handles=[l1, l2], labels=['Train loss', 'Eval loss'], loc='best') plt.show()
predict.py
预测代码
# -*- coding: utf-8 -*- import pkuseg import torch from load_data import UNK_IDX, SOS_IDX, EOS_IDX, vocab2id, id2vocab from model import Encoder, Decoder, Seq2Seq import os os.environ['CUDA_LAUNCH_BLOCKING'] = '1' # 下面老是报错 shape 不一致 # device = "cuda" if torch.cuda.is_available() else 'cpu' device = torch.device('cuda:3') INPUT_DIM = len(id2vocab) OUTPUT_DIM = len(id2vocab) ENC_EMB_DIM = 256 DEC_EMB_DIM = 256 HID_DIM = 512 N_LAYERS = 2 ENC_DROPOUT = 0.5 DEC_DROPOUT = 0.5 enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT) dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) model = Seq2Seq(enc, dec, device).to(device) model.load_state_dict(torch.load('model.pt')) model.eval() text = '文本摘要原文' seg = pkuseg.pkuseg(model_name='medicine') tokens = [tok for tok in seg.cut(text)] tokens_idx = [SOS_IDX] + [vocab2id.get(word, UNK_IDX) for word in tokens] + [EOS_IDX] tokens_idx = torch.tensor(tokens_idx) print(tokens_idx) res = [] hidden, cell = model.encoder(tokens_idx.unsqueeze(0).to(device)) inputs = torch.tensor([SOS_IDX]).to(device) for t in range(1, 35): output, hidden, cell = model.decoder(inputs, hidden, cell) inputs = output.argmax(1) word = id2vocab[inputs.item()] res.append(word) if word == '<eos>': break print(''.join(res))
遇到问题
问题1
对一句话的预测为<eos><eos><eos>
原因&解决
在文本摘要中,通常用来表示句子的结束,因此,如果输出为,可能的原因有以下几种,为了解决这个问题,尝试以下方法:
- 模型生成了连续的
<eos>
标记,这可能是因为模型在训练数据中经常看到连续的<eos>
标记,导致模型过度地学习了这种模式。
尝试:调整模型的超参数,例如增加dropout或减小模型的层数,以减少模型的过度拟合。 - 模型生成了多个
<eos>
标记,这可能是因为模型对输入的理解存在问题,认为输入中应该存在多个句子,因此在生成摘要时也生成了多个句子的结束标记。
修改模型的输入数据,例如添加标点符号或其他指示句子结束的符号,以更明确地表示句子的边界。 - 数据预处理时出现了错误,例如,在将输入数据转换为模型可接受的格式时,意外地将多个
<eos>
标记添加到了输入文本的结尾,导致模型在生成摘要时也生成了多个<eos>
标记。
检查数据预处理的过程,确保在将数据转换为模型可接受的格式时没有出现错误。
问题2
对一句话的预测为<sos><sos><sos>
原因
- 数据集中存在缺失数据或标注不准确的情况,导致训练模型时出现了错误的标注,从而影响了模型的预测结果。
- 训练数据不足或数据质量不高,导致模型没有学习到正确的文本摘要生成方式,从而无法正确地预测摘要。
- 模型本身存在缺陷或设计不当,导致无法正确地学习和预测文本摘要。例如,模型可能存在过拟合、欠拟合、梯度消失、梯度爆炸等问题,从而导致预测结果不准确。
解决
可以考虑对训练数据进行清洗和预处理,增加数据的质量和数量,
重新设计模型架构和参数,
以及尝试不同的训练策略和优化算法,以提高模型的准确性和稳定性。