现代循环神经网络

GRU 门控制单元

关注一个序列:不是每个每个观察都是同等重要

想值记住相关的观察需要

  • 能关注的机制(更新门)
  • 能遗忘的机制(重置门)

对于给定的时间步t,H是隐藏状态

$$ \begin{gathered} \mathbf{R}_t =\sigma(\mathbf{X}_t\mathbf{W}_{xr}+\mathbf{H}_{t-1}\mathbf{W}_{hr}+\mathbf{b}_r), \\ \mathbf{Z}_{t} =\sigma(\mathbf{X}_t\mathbf{W}_{xz}+\mathbf{H}_{t-1}\mathbf{W}_{hz}+\mathbf{b}_z), \end{gathered} $$


候选隐藏状态,用来生成我们真正要的隐藏状态。其中R和H的维度是一样的$\odot$代表的是按元素乘法

$$ \tilde{\mathbf{H}}_t=\tanh(\mathbf{X}_t\mathbf{W}_{xh}+(\mathbf{R}_t\odot\mathbf{H}_{t-1})\mathbf{W}_{hh}+\mathbf{b}_h), $$

隐状态

$$ H_t=Z_t\odot H_{t-1}+(1-Z_t)\odot\tilde{H}_t $$

其中当$Z_t$为0时就变成了RNN的情况,这里的Z也是0-1之间的数字

$$ \begin{aligned} &R_{t}&& =\sigma(X_tW_{xr}+H_{t-1}W_{hr}+b_r), \\ &Z_{t}&& =\sigma(X_tW_{xz}+H_{t-1}W_{hz}+b_z) \\ &\tilde{\boldsymbol{H}}_t&& =\tanh(X_t\boldsymbol{W}_{xh}+\left(\boldsymbol{R}_t\odot\boldsymbol{H}_{t-1}\right)\boldsymbol{W}_{hh}+\boldsymbol{b}_h) \\ &\boldsymbol{H}_t&& =Z_t\odot H_{t-1}+(1-Z_t)\odot\tilde{\boldsymbol{H}}_t \end{aligned} $$

更新门控制二者混合时的比重大小。整体思想就是过去状态的重要性可以学习了

遗忘门的作用是看多少上一刻隐藏层的状态(的权重),更新门的作用是直接让输出可以劲量多关注当前的输入不要被前面的状态带动(要不要很具输入来更新上一刻隐藏层)。

import torch
from torch import nn
from d2l import torch as d2l
from language_model import load_data_time_machine
from rnn import train_ch8, predict_ch8, RNNModelScratch
from rnn_2 import RNNModel

batch_size, num_steps = 32, 35
train_iter, vocab = load_data_time_machine(batch_size, num_steps)


# 初始化模型参数
def get_params(vocab_size, num_hiddens, device):
  num_inputs = num_outputs = vocab_size

  # 均值为0方差0.01的随机权重
  def normal(shape):
      return torch.randn(size=shape, device=device) * 0.01

  # 定义三层
  def three():
      return (normal((num_inputs, num_hiddens)),
              normal((num_hiddens, num_hiddens)),
              torch.zeros(num_hiddens, device=device))

  # 一共有9个可学习参数,GRU就是多了更新门参数和重置门参数
  W_xz, W_hz, b_z = three()  # 更新门参数
  W_xr, W_hr, b_r = three()  # 重置门参数
  W_xh, W_hh, b_h = three()  # 候选隐状态参数
  # 输出层参数
  W_hq = normal((num_hiddens, num_outputs))
  b_q = torch.zeros(num_outputs, device=device)
  # 附加梯度
  params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
  for param in params:
      param.requires_grad_(True)
  return params


# 定义初始化状态函数
def init_gru_state(batch_size, num_hiddens, device):
  return (torch.zeros((batch_size, num_hiddens), device=device),)


# 定义门控循环单元模型
def gru(inputs, state, params):
  W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
  H, = state
  outputs = []
  for X in inputs:
      # 这里是gru的核心公式
      Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
      R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
      # 这里的*是按元素乘
      H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
      H = Z * H + (1 - Z) * H_tilda
      Y = H @ W_hq + b_q
      outputs.append(Y)
  return torch.cat(outputs, dim=0), (H,)


vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = RNNModelScratch(len(vocab), num_hiddens, device, get_params,
                      init_gru_state, gru)
train_ch8(model, train_iter, vocab, lr, num_epochs, device)

# pytorch简单实现的版本
num_inputs = vocab_size
# 唯一的变化
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = RNNModel(gru_layer, len(vocab))
model = model.to(device)
train_ch8(model, train_iter, vocab, lr, num_epochs, device)

当时lstm出来的时候还没有relu

LSTM

长短期记忆网络:

  • 遗忘门:将值朝0减少
  • 输入门:决定是不是忽略掉输入的数据
  • 输出门:决定是不是使用隐状态

$$ \begin{gathered} I_{t} =\sigma(\mathbf{X}_t\mathbf{W}_{xi}+\mathbf{H}_{t-1}\mathbf{W}_{hi}+\mathbf{b}_i), \\ \mathbf{F}_t =\sigma(\mathbf{X}_t\mathbf{W}_{xf}+\mathbf{H}_{t-1}\mathbf{W}_{hf}+\mathbf{b}_f), \\ \mathbf{0}_t =\sigma(\mathbf{X}_t\mathbf{W}_{xo}+\mathbf{H}_{t-1}\mathbf{W}_{ho}+\mathbf{b}_o), \end{gathered} $$

它们具有sigmoid激活函数的全连接层,所以这三个门的输出值都在(0,1)之间

候选记忆单元

这里是学习到一个潜在的规律

$$ \tilde{\mathbf{C}}_t=\tanh(\mathbf{X}_t\mathbf{W}_{xc}+\mathbf{H}_{t-1}\mathbf{W}_{hc}+\mathbf{b}_c), $$

记忆元

记忆单元的权重乘以遗忘门权重,I越大用的权重就越大

$$ \mathbf{C}_t=\mathbf{F}_t\odot\mathbf{C}_{t-1}+\mathbf{I}_t\odot\tilde{\mathbf{C}}_t. $$

隐藏态

这里的C是长期记忆。

$$ \mathbf{H}_t=\mathbf{O}_t\odot\tanh(\mathbf{C}_t). $$

tanh是为了保证输出在-1和12之间

总结

$$ \begin{aligned} \boldsymbol{I}_t& =\sigma(X_tW_{xi}+H_{t-1}W_{hi}+b_i) \\ \boldsymbol{F}_{t}& =\sigma(X_tW_{xf}+H_{t-1}W_{hf}+b_f)\\ \boldsymbol{O}_{t}& =\sigma(X_tW_{xo}+H_{t-1}W_{ho}+b_o) \\ \tilde{C}_t& =\tanh(X_tW_{xc}+H_{t-1}W_{hc}+b_c) \\ C_t& =F_t\odot C_{t-1}+I_t\odot\tilde{C}_t \\ \boldsymbol{H}_t& =O_t\odot\tanh(C_t) \end{aligned} $$

代码

import torch
from torch import nn
from d2l import torch as d2l
from language_model import load_data_time_machine
from rnn import train_ch8, predict_ch8, RNNModelScratch
from rnn_2 import RNNModel

batch_size, num_steps = 32, 35
train_iter, vocab = load_data_time_machine(batch_size, num_steps)


def get_lstm_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size

    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01

    def three():
        return (normal((num_inputs, num_hiddens)),  # 输入到隐藏状态的权重
                normal((num_hiddens, num_hiddens)),  # 隐藏到隐藏状态的权重
                torch.zeros(num_hiddens, device=device))  # 偏置向量

    W_xi, W_hi, b_i = three()  # 输入门参数
    W_xf, W_hf, b_f = three()  # 遗忘门参数
    W_xo, W_ho, b_o = three()  # 输出门参数
    W_xc, W_hc, b_c = three()  # 候选记忆元参数
    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # 附加梯度
    params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
              b_c, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params


# 长短期要返回一个额外的记忆单元, 单元的值为0,形状为(批量大小,隐藏单元数),也就是C
def init_lstm_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),
            torch.zeros((batch_size, num_hiddens), device=device))


def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
     W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
        F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
        O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
        C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * torch.tanh(C)
        Y = (H @ W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H, C)


vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,
                        init_lstm_state, lstm)
train_ch8(model, train_iter, vocab, lr, num_epochs, device)

# 简洁实现lstm
num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

深度循环神经网络

浅RNN就是一个隐藏层,深RNN就是多个隐藏层

代码

import torch
from torch import nn
from d2l import torch as d2l
from language_model import load_data_time_machine
from rnn import train_ch8, predict_ch8, RNNModelScratch
from rnn_2 import RNNModel



batch_size, num_steps = 32, 35
train_iter, vocab = load_data_time_machine(batch_size, num_steps)

vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
device = d2l.try_gpu()
#pytorch 的LSTM是不带输出的
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = RNNModel(lstm_layer, len(vocab))
model = model.to(device)
num_epochs, lr = 500, 2
train_ch8(model, train_iter, vocab, lr*1.0, num_epochs, device)

双向循环神经网络

双向循环神经网络通过反向更新隐藏层,来一用方向时间信息。

  • 一个前向RNN隐层
  • 一个反向RNN隐层
  • 合并俩个隐状态得到输出

双向循环神经网络几乎不能用作推理,它主要用与对于句子进行特征提取。

以下是一个错误的应用,因为不能用双向神经网络训练语言模型

import torch
from torch import nn
from d2l import torch as d2l

# 加载数据
batch_size, num_steps, device = 32, 35, d2l.try_gpu()
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 通过设置“bidirective=True”来定义双向LSTM模型
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
#bidirectional就是设置反向
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, bidirectional=True)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
# 训练模型
num_epochs, lr = 500, 1
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

编解码器

重新考察RNN中的应用

编码器:将文本表示成向量

解码器:向量表示成输出

一个模型被分成俩块

编码器处理输出,解码器生成输出

抽象代码

from torch import nn


# @save
class Encoder(nn.Module):
    """编码器-解码器架构的基本编码器接口"""

    def __init__(self, **kwargs):
        super(Encoder, self).__init__(**kwargs)

    def forward(self, X, *args):
        raise NotImplementedError


# @save
class Decoder(nn.Module):
    """编码器-解码器架构的基本解码器接口"""

    def __init__(self, **kwargs):
        super(Decoder, self).__init__(**kwargs)

    def init_state(self, enc_outputs, *args):
        raise NotImplementedError

    def forward(self, X, state):
        raise NotImplementedError


# @save
class EncoderDecoder(nn.Module):
    """编码器-解码器架构的基类"""

    def __init__(self, encoder, decoder, **kwargs):
        super(EncoderDecoder, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, enc_X, dec_X, *args):
        enc_outputs = self.encoder(enc_X, *args)
        # 用编码器的输出来初始化解码器
        dec_state = self.decoder.init_state(enc_outputs, *args)
        return self.decoder(dec_X, dec_state)

序列到序列的学习

例子,机器翻译。早起的谷歌翻译就是用序列导序列(seqtoseq)

给定一个原语言的句子,自动翻译成目标语言

俩个句子可以有不同的长度

  • 编码器是一个RNN,读取输入句子(可以是双向)
  • 解码器使用另外一个RNN来输出

  • 编码器是没有输出的RNN
  • 编码器最后时间步的隐状态用作解码器的初始状态

训练时,解码器用目标句子作为输入。即便一句话中第一个词就预测错了,但是之后的输入依旧是正确的输入。

衡量生成序列好坏的BLEU

$p_n$是预测中所有n-gram的精度。

标签序列ABCDEF和预测序列ABBCD,有$p_1=4/5,p_2=3/4,p_4=0$(最后展开到$p_n$)n就是每次匹配几个单词

BLEU的定义,

$$ \exp\left(\min\left(0,1-\frac{\mathrm{len}_{\mathrm{label}}}{\mathrm{len}_{\mathrm{pred}}}\right)\right)\prod_{n=1}^kp_n^{1/2^n} $$

从一个句子生成另一个句子,将编码器最后时间状态来初始化解码器隐状态来完成信息传递,用BLEU来衡量生成序列的好坏。

词嵌入

使用嵌入层layer 来获得输入序列中每个词元的特征向量。嵌入的权重是一个矩阵,其行数等于输入此表的大小,其列数等于特征向量的维度。

one_hot是高维稀疏向量。而embedding是低维度的连续向量。词嵌入向量:不仅可以表达语义的相似性。可以通过向量的数学关系,描述词语之间的语义关联。比如man-woman的距离类似于king-queen的距离。

通过特定的词嵌入算法,如word2vec、fasttext、Glove

词嵌入的过程,假设有5000个单词,维度维128每个词,那么嵌入矩阵的大小就是5000*128

先将句子进行分词,使用one-hot进行编码, 句子矩阵记作V。将句子矩阵和嵌入矩阵想乘。就可以得到这个句子的嵌入向量。通过相乘将矩阵中的元素从嵌入矩阵中取出。

one-hot编码不具有通用性,不同预料的编码得到的one-hot表示一般不同。但是嵌入矩阵是通用的,同一份词向量,可以用在不同的NLP任务中。

import torch
from d2l import torch as d2l
from torch import nn
import math
import collections
from ae import EncoderDecoder


# @save
class Seq2SeqEncoder(d2l.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""

    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        # 嵌入层
        # 将离散的输入映射到连续的空间向量,这个embed_size是一个超参数
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # 这里不需要输出层,因为编码器是不需要输出的
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)

    def forward(self, X, *args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)
        # permute改变维度 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)
        # 如果未提及状态,则默认为0 state就是最后一层的输出
        output, state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state的形状:(num_layers,batch_size,num_hiddens)
        return output, state


# 编码器实现
encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
encoder.eval()
# 4是batch_size 7是句子的长度
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
# 输出7,4,16 分别是时刻,批量大小,隐藏层
print(output.shape)
# 2,4,16 分别代表了 2层,批量大小,隐藏层大小
print(state.shape)


class Seq2SeqDecoder(d2l.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""

    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        # 解码器有自己的Embedding层
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # 这里是假设encoder的隐藏层和decoder的隐藏层一样的
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
        # 做一个vocab_size的分类
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, *args):
        # 这里的1才是真正的output,【2】是state
        return enc_outputs[1]

    def forward(self, X, state):
        # 第二个维度是时间,把时间部分放到前面
        X = self.embedding(X).permute(1, 0, 2)
        # 这里是最后一个时刻,最后一层的输出,将第一个维度复制X.shape[0]次,将第二个维度复制一次,第三个维度也是复制一次
        # context是上下文,获取encoder最后一个时刻的输出。将它变成时间步一样。将它重复表示为decoder输入的长度
        # 即广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0], 1, 1)
        # decoder的输入是当前Embedding的输出,加上编码器最后一刻的输入拼在一起
        X_and_context = torch.cat((X, context), 2)
        output, state = self.rnn(X_and_context, state)
        # 最后经过一个线性层
        output = self.dense(output).permute(1, 0, 2)
        # output的形状:(batch_size,num_steps,vocab_size)
        # state的形状:(num_layers,batch_size,num_hiddens)
        return output, state


decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
print(output.shape, state.shape)  # 输出4,7,10.和2,4,16


# 通过零值化屏蔽不相关的项,以便后面任何不相关的预测的计算都是与0的乘机
# @save
def sequence_mask(X, valid_len, value=0):
    """在序列中屏蔽不相关的项"""
    maxlen = X.size(1)
    mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:, None]
    X[~mask] = value
    return X


X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))  # 这里是个demo会输出[1,0,0],[4,5,0]


# 使用交叉熵函数来屏蔽不相关的预测。最初,所有预测词元的掩码都设置为1。一旦给定了有效长度,与填充词元对应的掩码将被设置为0。
# 最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。简单来说就是填充的东西不需要计算softmax
# @save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""

    # pred的形状:(batch_size,num_steps,vocab_size)
    # label的形状:(batch_size,num_steps)
    # valid_len的形状:(batch_size,)
    def forward(self, pred, label, valid_len):
        # 创建相同形状值为1的张量
        weights = torch.ones_like(label)
        # 把有效的保留
        weights = sequence_mask(weights, valid_len)
        # reduction为none就不会对loss求和或者求平均
        self.reduction = 'none'
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)
        # 只保留有效的
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss


loss = MaskedSoftmaxCELoss()
# 3是批大小,4是时间长度,10是词长度。标号为3,4。第一个样本4个都是,第二个样本只有钱2个是验证的。第三个样本都不是
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long), torch.tensor([4, 2, 0]))


# @save
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""

    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])

    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    loss = MaskedSoftmaxCELoss()
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # 训练损失总和,词元数量
        for batch in data_iter:
            optimizer.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
            # 把<bos>转成向量,然后乘以Y第一个维度的长度。-1自适应形状。这里的bos
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0], device=device).reshape(-1, 1)
            # 去掉换行符,然后补bos
            dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学
            Y_hat, _ = net(X, dec_input, X_valid_len)
            l = loss(Y_hat, Y, Y_valid_len)
            l.sum().backward()  # 损失函数的标量进行“反向传播”
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
          f'tokens/sec on {str(device)}')


embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()

# 输入和输出的embedding是分开的, src_vocab编码的,tgt_vocab是解码的
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)
net = EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)


# 预测,每个解码器的当前时间的输入都来自于迁移时间步的预测词元。
# 与训练类似,序列开始词元<obs>在初始时间被输入到解码器中。
# 当输出序列的预测到词元就结束了
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为评估模式
    net.eval()
    # eos一般表示句子的结束(end of sentence)
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    # 添加批量轴
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    # 如果句子没有结束最多运行10次
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq


# 使用bleu函数来
def bleu(pred_seq, label_seq, k):  # @save
    """计算BLEU"""
    pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[' '.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score


# 进行翻译
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(
        net, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

束搜索

在seq2seq中我们使用了贪心搜索来预测序列,将当前时刻预测概率最大的词输出。但是贪心可能不是最优的

最优算法:对于所有可能的序列,计算它的概率,然后选取最好的那个

如果输出的字典大小为n,序列长度为T,我们需要考虑$n^T$个序列

束搜索:保存最好的K个序列,在每个时刻,对每个候选新加一项(n种可能),在kn个选项中选择最好的那个

它的时间复杂度是O(knT)

Last modification:August 21, 2024
如果觉得我的文章对你有用,请随意赞赏