【文本摘要(2)】pytorch之Seq2Seq(下)

简介: 【文本摘要(2)】pytorch之Seq2Seq(上)

生成模型的输出序列

这段代码是一个基本的序列到序列模型中的解码部分,用于生成模型的输出序列。具体而言,这个代码通过模型的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>

原因&解决

在文本摘要中,通常用来表示句子的结束,因此,如果输出为,可能的原因有以下几种,为了解决这个问题,尝试以下方法:

  1. 模型生成了连续的<eos>标记,这可能是因为模型在训练数据中经常看到连续的<eos>标记,导致模型过度地学习了这种模式。
    尝试:调整模型的超参数,例如增加dropout或减小模型的层数,以减少模型的过度拟合。
  2. 模型生成了多个<eos>标记,这可能是因为模型对输入的理解存在问题,认为输入中应该存在多个句子,因此在生成摘要时也生成了多个句子的结束标记。
    修改模型的输入数据,例如添加标点符号或其他指示句子结束的符号,以更明确地表示句子的边界。
  3. 数据预处理时出现了错误,例如,在将输入数据转换为模型可接受的格式时,意外地将多个<eos>标记添加到了输入文本的结尾,导致模型在生成摘要时也生成了多个<eos>标记。
    检查数据预处理的过程,确保在将数据转换为模型可接受的格式时没有出现错误。

问题2

对一句话的预测为<sos><sos><sos>

原因

  1. 数据集中存在缺失数据或标注不准确的情况,导致训练模型时出现了错误的标注,从而影响了模型的预测结果。
  2. 训练数据不足或数据质量不高,导致模型没有学习到正确的文本摘要生成方式,从而无法正确地预测摘要。
  3. 模型本身存在缺陷或设计不当,导致无法正确地学习和预测文本摘要。例如,模型可能存在过拟合、欠拟合、梯度消失、梯度爆炸等问题,从而导致预测结果不准确。

解决

可以考虑对训练数据进行清洗和预处理,增加数据的质量和数量,

重新设计模型架构和参数,

以及尝试不同的训练策略和优化算法,以提高模型的准确性和稳定性。

目录
相关文章
|
机器学习/深度学习 自然语言处理 PyTorch
PyTorch应用实战六:利用LSTM实现文本情感分类
PyTorch应用实战六:利用LSTM实现文本情感分类
267 0
|
5月前
|
机器学习/深度学习 自然语言处理 算法
【从零开始学习深度学习】49.Pytorch_NLP项目实战:文本情感分类---使用循环神经网络RNN
【从零开始学习深度学习】49.Pytorch_NLP项目实战:文本情感分类---使用循环神经网络RNN
|
5月前
|
机器学习/深度学习 算法 PyTorch
【从零开始学习深度学习】50.Pytorch_NLP项目实战:卷积神经网络textCNN在文本情感分类的运用
【从零开始学习深度学习】50.Pytorch_NLP项目实战:卷积神经网络textCNN在文本情感分类的运用
|
6月前
|
机器学习/深度学习 数据采集 自然语言处理
PyTorch搭建LSTM神经网络实现文本情感分析实战(附源码和数据集)
PyTorch搭建LSTM神经网络实现文本情感分析实战(附源码和数据集)
603 0
|
机器学习/深度学习 自然语言处理 PyTorch
【文本摘要(3)】Pytorch之Seq2seq: attention
【文本摘要(3)】Pytorch之Seq2seq: attention
77 0
|
机器学习/深度学习 自然语言处理 PyTorch
【文本摘要(2)】pytorch之Seq2Seq(上)
【文本摘要(2)】pytorch之Seq2Seq
178 0
|
数据采集 自然语言处理 PyTorch
全套解决方案:基于pytorch、transformers的中文NLP训练框架,支持大模型训练和文本生成,快速上手,海量训练数据!
全套解决方案:基于pytorch、transformers的中文NLP训练框架,支持大模型训练和文本生成,快速上手,海量训练数据!
全套解决方案:基于pytorch、transformers的中文NLP训练框架,支持大模型训练和文本生成,快速上手,海量训练数据!
|
机器学习/深度学习 自然语言处理 PyTorch
在PyTorch中使用Seq2Seq构建的神经机器翻译模型(三)
在PyTorch中使用Seq2Seq构建的神经机器翻译模型
145 0
在PyTorch中使用Seq2Seq构建的神经机器翻译模型(三)
|
1月前
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
168 2
|
1月前
|
机器学习/深度学习 自然语言处理 监控
利用 PyTorch Lightning 搭建一个文本分类模型
利用 PyTorch Lightning 搭建一个文本分类模型
55 8
利用 PyTorch Lightning 搭建一个文本分类模型

热门文章

最新文章