当前位置: 首页 > news >正文

跟着李沐老师学习深度学习(十三)

现代循环神经网络

循环神经网络中梯度异常在实践中的意义引发了一些问题:

  • 早期观测值影响重大:早期观测值对预测所有未来观测值极为重要,如序列中第一个观测值包含校验和,需在序列末尾辨别其是否正确,若无特殊机制存储早期重要信息,就需给该观测值指定很大梯度,因为它影响后续所有观测值。
  • 存在无关词元:在对网页内容进行情感分析等任务时,可能存在一些与观测值无关的词元,如辅助 HTML 代码,希望有机制能在隐状态表示中跳过此类词元。
  • 序列存在逻辑中断:序列的各个部分之间可能存在逻辑中断,如书的章节之间、证券的熊市和牛市之间的过渡,最好有一种方法来重置内部状态表示。

解决方案

  • 长短期记忆(LSTM):是学术界提出的用于解决上述问题的最早方法之一。
  • 门控循环单元(GRU):是一个稍微简化的变体,通常能提供与 LSTM 同等的效果,且计算速度明显更快。

门控循环单元(GRU)

  • 关注一个序列:
    • 不是每个观察值都是同等重要
    • 只想记住相关的观察需要
      • 能关注的机制(更新门 update gate):将重要的数据放入该门
      • 能遗忘的机制(重置门 reset gate):到目前为止,前面东西不重要了。

更新门和重置门

在这里插入图片描述
其中:在这里插入图片描述

通过重置门和更新门控制隐状态的更新与重置。重置门决定保留过去状态的程度,更新门控制新状态中旧状态的占比,两者都用 sigmoid 函数将输入转换到 (0, 1) 区间进行凸组合

候选隐状态(备胎)

将重置门 Rt与 常规隐状态更新机制集成,得到在时间步 t 的候选隐状态(candidate hidden state):
在这里插入图片描述

在这里插入图片描述

重置门接近 1 时类似普通循环神经网络,接近 0 时候选隐状态取决于当前输入。

隐状态(真正的)

在这里插入图片描述
更新门接近 1 时保留旧状态,接近 0 时新隐状态接近候选隐状态,有助于处理梯度消失问题和捕获长序列依赖。
在这里插入图片描述

总之,门控循环单元具有以下两个显著特征:

  • 重置门有助于捕获序列中的短期依赖关系;

  • 更新门有助于捕获序列中的长期依赖关系

总结

代码实现 - 从零实现

# 门循环单元GRU
# 从零开始实现import torch
from torch import nn
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 初始化模型参数
def get_params(vocab_size, num_hiddens, device):num_inputs = num_outputs = vocab_sizedef normal(shape):return torch.randn(size=shape, device=device)*0.01def three():return (normal((num_inputs, num_hiddens)),normal((num_hiddens, num_hiddens)),torch.zeros(num_hiddens, device=device))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 = paramsH, = stateoutputs = []for X in inputs: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_tildaY = H @ W_hq + b_qoutputs.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 = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,init_gru_state, gru)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

结果:
在这里插入图片描述

代码实现 - 简洁实现

# 使用高级API,运行速度很快(使用的是编译好的运算符而不是Python来处理之前阐述的许多细节)
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

结果:
在这里插入图片描述

长短期记忆网络(LSTM)

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

效果上和GRU一样,都是:是不是要忘掉过去的状态,看现在的输入,或者不看现在输入的数据而看前一时刻的状态;

忘记门、输入门、输出门

公式:
在这里插入图片描述
在这里插入图片描述

候选记忆单元

公式:
在这里插入图片描述
候选记忆元如下图所示:
在这里插入图片描述

记忆单元

上一个时刻的记忆单元会作为状态加入(与GRU不同之处)
在这里插入图片描述
如果遗忘门始终为1且输入门始终为0, 则过去的记忆元Ct-1 将随时间被保存并传递到当前时间步。 引入这种设计是为了缓解梯度消失问题, 并更好地捕获序列中的长距离依赖关系。

这样就得到了计算记忆元的流程图:
在这里插入图片描述

隐状态

公式:(将隐藏状态约束到(-1,1))
在这里插入图片描述
只要输出门接近1,就能够有效地将所有记忆信息传递给预测部分, 而对于输出门接近0,只保留记忆元内的所有信息,而不需要更新隐状态。如下提供了数据流的图形化演示。
在这里插入图片描述

总结

  • 长短期记忆网络有三种类型的门:输入门、遗忘门和输出门。

  • 长短期记忆网络的隐藏层输出包括“隐状态”和“记忆元”。只有隐状态会传递到输出层,而记忆元完全属于内部信息。

  • 长短期记忆网络可以缓解梯度消失和梯度爆炸。

代码实现 - 从零实现

# 长短期记忆网络(LSTM)import torch
from torch import nn
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# [初始化模型参数
# 超参数num_hiddens定义隐藏单元的数量
def get_lstm_params(vocab_size, num_hiddens, device):num_inputs = num_outputs = vocab_sizedef normal(shape):return torch.randn(size=shape, device=device)*0.01def 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,形状为(批量大小,隐藏单元数)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) = stateoutputs = []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_tildaH = O * torch.tanh(C)Y = (H @ W_hq) + b_qoutputs.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 = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

结果:
在这里插入图片描述

代码实现 - 简洁实现

# 简洁实现
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,主要就是如何得到更多的非线性 —— 多加几个隐藏层。

更深的:

在这里插入图片描述

总结

  • 在深度循环神经网络中,隐状态的信息被传递到当前层的下一时间步和下一层的当前时间步。
    有许多不同风格的深度循环神经网络, 如长短期记忆网络、门控循环单元、或经典循环神经网络。 这些模型在深度学习框架的高级API中都有涵盖。
    总体而言,深度循环神经网络需要大量的调参(如学习率和修剪) 来确保合适的收敛,模型的初始化也需要谨慎。

代码实现

# 深度循环神经网络import torch
from torch import nn
from d2l import torch as d2lbatch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)# 通过num_layers的值来设定隐藏层数vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)# 训练和预测
num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr*1.0, num_epochs, device)

结果:
在这里插入图片描述

双向循环神经网络

对于某些序列,可以从前往后看,也可以从后往前看。
首先,给一个例子,我们考虑以下三个在文本序列中填空的任务。
在这里插入图片描述
根据可获得的信息量,我们可以用不同的词填空, 如“很高兴”(“happy”)、“不”(“not”)和“非常”(“very”)。 很明显,每个短语的“下文”传达了重要信息(如果有的话), 而这些信息关乎到选择哪个词来填空, 所以无法利用这一点的序列模型将在相关任务上表现不佳。 例如,如果要做好命名实体识别 (例如,识别“Green”指的是“格林先生”还是绿色), 不同长度的上下文范围重要性是相同的。

  • 取决于过去和未来的上下文,可以填很不一样的词
  • 到目前为止RNN只看过去
  • 在填空的时候,也可以看未来

双向RNN

  • 一个前向RNN隐层
  • 一个方向RNN隐层
  • 合并两个隐状态得到输出
    在这里插入图片描述
    公式:
    在这里插入图片描述
    在这里插入图片描述

总结

  • 特性:使用来自**序列两端(过去和未来)**的观测信息预测当前观测。
  • 问题:在预测下一个词元时,因测试期间只有过去数据,精度较差;计算速度慢,前向传播需在双向层进行前向和后向递归,反向传播依赖前向传播结果,导致梯度求解链长。
  • 应用场景:实际应用较少,用于填充缺失单词、词元注释(如命名实体识别)以及作为序列处理流水线步骤对序列编码(如机器翻译),不能预测未来。

代码实现

首先,双向RNN不能训练语言模型,所以下面这个是一个错误的应用,只是为了演示该模型。与之前不同的是:改为bidirectional=True

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=True就是双向rnn
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)

结果:很差,因为没有未来信息,精度很低,预测信息很不靠谱。
在这里插入图片描述

编码器-解码器架构

机器翻译和数据集

  • 机器翻译指的是将文本序列从一种语言自动翻译成另一种语言。
  • 使用单词级词元化时的词表大小,将明显大于使用字符级词元化时的词表大小。为了缓解这一问题,我们可以将低频词元视为相同的未知词元。
  • 通过截断和填充文本序列,可以保证所有的文本序列都具有相同的长度,以便以小批量的方式加载。

代码实现

# 机器翻译与数据集
# 语言模型是自然语言处理的关键, 而机器翻译是语言模型最成功的基准测试
# 机器翻译(machine translation)指的是 将序列从一种语言自动翻译成另一种语言。 import os
import torch
from d2l import torch as d2l# 下载和预处理数据集
# 下载一个由Tatoeba项目的双语句子对(https://www.manythings.org/anki/) 组成的“英-法”数据集
# 
# 英语是源语言(source language), 法语是目标语言(target language)d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip','94646ad1522d915e7b0f9296181140edcf86a4f5')def read_data_nmt():"""载入“英语-法语”数据集"""data_dir = d2l.download_extract('fra-eng')with open(os.path.join(data_dir, 'fra.txt'), 'r',encoding='utf-8') as f:return f.read()raw_text = read_data_nmt()
print(raw_text[:75])# 预处理
# 用空格代替不间断空格(non-breaking space)
# 使用小写字母替换大写字母
# 在单词和标点符号之间插入空格。def preprocess_nmt(text):"""预处理“英语-法语”数据集"""def no_space(char, prev_char):return char in set(',.!?') and prev_char != ' '# 使用空格替换不间断空格# 使用小写字母替换大写字母text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()# 在单词和标点符号之间插入空格out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else charfor i, char in enumerate(text)]return ''.join(out)text = preprocess_nmt(raw_text)
print(text[:80])# 词元化
# tokenize_nmt函数对前num_examples个文本序列对进行词元, 其中每个词元要么是一个词,要么是一个标点符号。
# 此函数返回两个词元列表:source和target: 
# source[i]是源语言(这里是英语)第 𝑖 个文本序列的词元列表
# target[i]是目标语言(这里是法语)第 𝑖个文本序列的词元列表。
def tokenize_nmt(text, num_examples=None):"""词元化“英语-法语”数据数据集"""source, target = [], []for i, line in enumerate(text.split('\n')):if num_examples and i > num_examples:breakparts = line.split('\t')if len(parts) == 2:source.append(parts[0].split(' '))target.append(parts[1].split(' '))return source, targetsource, target = tokenize_nmt(text)
source[:6], target[:6]# 绘制每个文本序列所包含的词元数量的直方图
#@save
def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):"""绘制列表长度对的直方图"""d2l.set_figsize()_, _, patches = d2l.plt.hist([[len(l) for l in xlist], [len(l) for l in ylist]])d2l.plt.xlabel(xlabel)d2l.plt.ylabel(ylabel)for patch in patches[1].patches:patch.set_hatch('/')d2l.plt.legend(legend)show_list_len_pair_hist(['source', 'target'], '# tokens per sequence','count', source, target);# 构建两个词表
# 将出现次数少于2次的低频率词元 视为相同的未知(“<unk>”)词元
# 在小批量时用于将序列填充到相同长度的填充词元(“<pad>”), 以及序列的开始词元(“<bos>”)和结束词元(“<eos>”)
src_vocab = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)# 加载数据集
def truncate_pad(line, num_steps, padding_token):"""截断或填充文本序列"""if len(line) > num_steps:return line[:num_steps]  # 截断return line + [padding_token] * (num_steps - len(line))  # 填充truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])# 定义一个函数: 将文本序列 [转换成小批量数据集用于训练
def build_array_nmt(lines, vocab, num_steps):"""将机器翻译的文本序列转换成小批量"""lines = [vocab[l] for l in lines]lines = [l + [vocab['<eos>']] for l in lines]array = torch.tensor([truncate_pad(l, num_steps, vocab['<pad>']) for l in lines])valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)return array, valid_len# 训练模型
def load_data_nmt(batch_size, num_steps, num_examples=600):"""返回翻译数据集的迭代器和词表"""text = preprocess_nmt(read_data_nmt())source, target = tokenize_nmt(text, num_examples)src_vocab = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])tgt_vocab = d2l.Vocab(target, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)data_iter = d2l.load_array(data_arrays, batch_size)return data_iter, src_vocab, tgt_vocab# 读出“英语-法语”数据集中的第一个小批量数据
train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:print('X:', X.type(torch.int32))print('X的有效长度:', X_valid_len)print('Y:', Y.type(torch.int32))print('Y的有效长度:', Y_valid_len)break

编码器和解码器

回顾:

  • 在CNN中,将“特征提取”看作编码器,softmax回归看作解码器;—> 编码器:将输入编程成中间表达形式(特征);解码器:将中间表示解码成输出。
  • 在RNN中,编码器:将文本表示成向量;解码器:向量表示成输出。

引出:

  • 机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出, 我们可以设计一个包含两个主要组件的架构:
    • 第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。
    • 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。

这被称为编码器-解码器(encoder-decoder)架构。
在这里插入图片描述
一个模块被分为两部分:编码器处理输入;解码器负责输出

代码实现

# 编码器-解码器架构
# 机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 
# 为了处理这种类型的输入和输出, 我们可以设计一个包含两个主要组件的架构:
# 第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。
# 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。
# 这被称为编码器-解码器(encoder-decoder)架构# 编码器from torch import nnclass Encoder(nn.Module):"""编码器-解码器架构的基本编码器接口"""def __init__(self, **kwarge):super(Encoder, self).__init__(**kwarge)def forward(self, X, *args):raise NotImplementedError# 解码器
#  用于将编码器的输出(enc_outputs)转换为编码后的状态class Decoder(nn.Module):def __init__(self, **kwargs):super(Decoder, self).__init__(**kwargs)# 中间状态:编码的输出enc_outputsdef init_state(self, enc_outputs, *args):raise NotImplementedErrordef forward(self, X, state):raise NotImplementedError# 合并编码器和解码器
class EncoderDecoder(nn.Module):"""编码器-解码器架构的基类"""def __init__(self, encoder, decoder, **kwargs):super(EncoderDecoder, self).__init__(**kwargs)self.encoder = encoderself.decoder = decoderdef 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)

序列到序列学习(seq2seq)

在生物中,将DNA序列转为RNA。

机器翻译

  • 机器翻译是最常见的应用
  • 给定一个源语言的句子,自动翻译成目标语言
  • 这两个句子都可以有不同的长度

Seq2Seq

在这里插入图片描述

  • 编码器是一个RNN,读取输入的句子

    • 可以是双向的(经常用在Encoder中)
  • 解码器使用另一个RNN来输入

  • 编码器解码器细节

    • 编码器是没有输出的RNN
    • 编码器最后时间步的隐状态用作解码器的初始隐状态
      在这里插入图片描述
  • 训练:

    • 训练时解码器使用目标句子作为输入
    • 推理的时候就是根据当前的输入和状态来生成输出。

衡量生成序列的好坏的BLEU

  • pn是预测中所有n-gram的精度
    • 表示 n 元语法的精确度,它是两个数量的比值: 第一个是预测序列与标签序列中匹配的n元语法的数量, 第二个是预测序列中 n 元语法的数量的比率;
    • 标签序列A B C D E F 和预测序列A B B C D,有p1 = 4/5,p2 = 3/4,p2 = 1/3,p4 = 0
    • BLEU定义:
      在这里插入图片描述
      前部分min()代表 惩罚过段的预测;后部分有高权重。

总结

  • seq2seq从一个句子生成另一个句子
  • 根据“编码器-解码器”架构的设计, 我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。
  • 在实现编码器和解码器时,我们可以使用多层循环神经网络。
  • 将编码器最后时间隐状态来初始解码器隐状态来完成信息传递
  • 在“编码器-解码器”训练中,强制教学方法将原始输出序列(而非预测结果)输入解码器。
  • BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的 𝑛 元语法的匹配度来评估预测。

代码实现

# 序列到序列学习 import collections
import math
import torch
from torch import nn
from d2l import torch as d2l# 实现循环神经网络编码器
class Seq2SeqEncoder(d2l.Encoder):"""用于序列到序列学习的循环神经网络编码器"""def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout=0, **kwargs):super(Seq2SeqEncoder, self).__init__(**kwargs)# 使用嵌入层(embedding layer) 获得输入序列中每个词元的特征向量。# 嵌入层的权重是一个矩阵# 其行数等于输入词表的大小(vocab_size), 其列数等于特征向量的维度(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)# 在循环神经网络模型中,第一个轴对应于时间步X = X.permute(1, 0, 2)# 如果未提及状态,则默认为0output, state = self.rnn(X) # 得到输出和状态# output的形状:(num_steps,batch_size,num_hiddens)# state的形状:(num_layers,batch_size,num_hiddens)return output, state# 实例化 上述编码器的实现:使用一个两层门控循环单元编码器,其隐藏单元数为 16
# 定一小批量的输入序列X(批量大小为 4 ,时间步为 7 )。
# 完成所有时间步之后,最后一层的隐状态的输出是一个张量,形状:(时间步数,批量大小,隐藏单元数)encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval() # dropout就不会生效了
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
# output形状:(时间步数,批量大小,隐藏单元数)
# state形状:(隐藏层的数量,批量大小,隐藏单元的数量)
output.shape,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)self.embedding = nn.Embedding(vocab_size, embed_size)self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,dropout=dropout)self.dense = nn.Linear(num_hiddens, vocab_size)def init_state(self, enc_outputs, *args):return enc_outputs[1]def forward(self, X, state):# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X).permute(1, 0, 2)# 广播context,使其具有与X相同的num_stepscontext = state[-1].repeat(X.shape[0], 1, 1)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)
output.shape, state.shape# 损失函数
# sequence_mask函数 [通过零值化屏蔽不相关的项]
# 以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。
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] = valuereturn XX = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))# 还可以使用此函数屏蔽最后几个轴上的所有项
X = torch.ones(2, 3, 4)
sequence_mask(X, torch.tensor([1, 2]), value=-1)# 通过扩展softmax交叉熵损失函数来遮蔽不相关的预测
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):weights = torch.ones_like(label)weights = sequence_mask(weights, valid_len)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()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),torch.tensor([4, 2, 0]))# 训练
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 = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],device=device).reshape(-1, 1)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()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 = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)# 预测
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,device, save_attention_weights=False):"""序列到序列模型的预测"""# 在预测时将net设置为评估模式net.eval()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 = [], []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>']:breakoutput_seq.append(pred)return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq# 预测序列的评估
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])] += 1for i in range(len_pred - n + 1):if label_subs[' '.join(pred_tokens[i: i + n])] > 0:num_matches += 1label_subs[' '.join(pred_tokens[i: i + n])] -= 1score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))return score# 利用训练好的循环神经网络“编码器-解码器”模型, [将几个英语句子翻译成法语],并计算BLEU的最终结果。
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}')

束搜索

beam search

贪心搜索(greedy search)

  • 在seq2seq中我们使用贪心搜索来预测序列
    • 将当前时刻预测概率最大的值输出
  • 但贪心很可能不是最优的:
    在这里插入图片描述
    在这里插入图片描述
    效率高但可能不是最优的

穷举搜索

如果目标是获得最优序列, 我们可以考虑使用穷举搜索(exhaustive search): 穷举地列举所有可能的输出序列及其条件概率, 然后计算输出条件概率最高的一个。

  • 最优算法:对所有可能的序列,计算它的概率,然后选取最好的那个。
  • 如果输出字典大小为n,序列最长为T,那么需要考虑 n^T个序列
    • n = 10000, T = 10: n^T = 10^40
    • 优点肯定能找到最优,但是计算不可行

束搜索(折中)

  • 保持最好的k个候选(束宽:k)

  • 在每个时刻,对每个候选新加一项(n种可能),在 kn 个选项中选出最好的k个。
    在这里插入图片描述
    候选输出序列是A、C、AB、CE、ABD和CED

  • 时间复杂度O(knT)

  • 每个候选的最终分数是:
    在这里插入图片描述

    • L是最终候选序列的长度,通常α = 0.75

总结

束搜索的结果介于贪心搜索和穷举搜索之间(k=1是贪心)。通过灵活地选择束宽,束搜索可以在正确率和计算代价之间进行权衡。

相关文章:

跟着李沐老师学习深度学习(十三)

现代循环神经网络 循环神经网络中梯度异常在实践中的意义引发了一些问题&#xff1a; 早期观测值影响重大&#xff1a;早期观测值对预测所有未来观测值极为重要&#xff0c;如序列中第一个观测值包含校验和&#xff0c;需在序列末尾辨别其是否正确&#xff0c;若无特殊机制存…...

鸿蒙与跨端迁移的重要性

鸿蒙操作系统&#xff08;HarmonyOS&#xff09;是由华为公司开发的一款面向未来的全场景分布式操作系统。它旨在提供一个统一的平台&#xff0c;支持各种设备之间的无缝协作和数据共享&#xff0c;从而为用户提供更加连贯和高效的体验。在鸿蒙的生态系统中&#xff0c;跨端迁移…...

成员函数定义后面加const是什么功能:C++中const成员函数的作用

成员函数定义后面加const是什么功能&#xff1a;C中const成员函数的作用 前言C中const成员函数的作用总结 前言 在PX4的代码中的位置控制模块中&#xff0c;有这样一个成员函数 void getAttitudeSetpoint(vehicle_attitude_setpoint_s &attitude_setpoint) const;该函数的…...

QSNCTF-WEB做题记录

第一题&#xff0c;文章管理系统 来自 <天狩CTF竞赛平台> 描述&#xff1a;这是我们的文章管理系统&#xff0c;快来看看有什么漏洞可以拿到FLAG吧&#xff1f;注意&#xff1a;可能有个假FLAG哦 1&#xff0c;首先观察题目网站的结构和特征 这个一个文件管理系统&#x…...

UE引擎游戏加固方案解析

据VGinsights的报告&#xff0c;近年来UE引擎在过去几年中市场占比显著增长&#xff0c;其中亚洲市场增幅达到了30%&#xff0c;随着UE5的推出和技术的不断进步&#xff0c;UE引擎在独立开发者和移动游戏开发中的应用也在逐步增加。 UE引擎的优势在于强大的画面表现与视觉特效…...

统计函数运行时间的python脚本

这是一个统计函数运行时间的实用脚本&#xff0c;其中用到了函数的嵌套、链式传输参数&#xff0c;以及修饰器。 import time# 定义一个装饰器timer&#xff0c;用于计算被装饰函数的运行时间 def timer(func):print("执行了timer")def wrapper(*args, **kwargs):st…...

大模型WebUI:Gradio全解11——使用transformers.agents构建Gradio UI(3)

大模型WebUI&#xff1a;Gradio全解11——使用transformers.agents构建Gradio UI&#xff08;3&#xff09; 前言本篇摘要11. 使用transformers.agents构建Gradio UI11.3 创建和使用工具Tools11.3.1 默认工具箱与load_tool11.3.2 创建新工具11.3.3 管理代理的工具箱toolbox11.3…...

spring boot知识点5

1.如何你有俩套配置环境&#xff0c;运行时如何选择 如果有俩套配置环境&#xff0c;则需要三个yml application.yml 用于配置你用那个配置环境 application-dev.yml 用于开发配置环境 application-prod.yml 用于发布配置环境 spring:profiles:active: prod # 指定当前激…...

【C++】面向对象的三大特性

面向对象编程三大核心特性&#xff1a;封装、继承和多态。 1. 封装 封装指的是将数据和操作这些数据的方法绑定在一起&#xff0c;形成一个对象&#xff0c;并且隐藏对象的内部实现细节&#xff0c;只暴露必要的接口。封装的目的是保护数据&#xff0c;确保外部代码不能直接访…...

Docker构建时,设定默认进入的工作目录的方法

在 Docker 中,你可以通过不同的方式来设定容器默认进入的目录,以下针对不同场景分别介绍具体方法: 1. 使用 Dockerfile 设定工作目录 如果你是通过构建镜像的方式来运行容器,那么可以在 Dockerfile 中使用 WORKDIR 指令来设置容器启动时的默认工作目录。以下是具体步骤:…...

DeepSeek等大模型功能集成到WPS中的详细步骤

记录下将**DeepSeek功能集成到WPS中**的步骤&#xff0c;以备忘。 1. 下载并安装OfficeAI插件 访问OfficeAI插件下载地址&#xff1a;https://www.office-ai.cn/&#xff0c;下载插件&#xff08;目前只支持windows系统&#xff09;。 注意&#xff0c;有两个插件&#xff0…...

教学资料档案管理系统

本系统构建 JAVA 体系的后端系统&#xff0c;围绕以安全&#xff0c;可靠&#xff0c;高速&#xff0c;健壮&#xff0c;易于扩展为目标的方向进行开发&#xff0c;在阿里等开源库的基础上实现提供教学资料档案的管理系统的后端接口的微服务架构系统。 功能包含&#xff1a;系…...

linux core分析---TLS读取异常

文章目录 TLS概念core 线程调用栈查看堆栈: bt查看所有线程堆栈:core分析:锁分析代码修改:thread8 f 4 (第四层堆栈) jcallback.c:186**thread10 f4 SynStack.cpp:1175tl_send_message 加锁修改tls_table1 socket_tab加锁保护2 增加tls_table 中buse的使用3 tls_tl_read_mes…...

SpringBoot 排除一些包的注入

文章目录 需求一、使用 ComponentScan 需求 在系统迭代的过程中&#xff0c;有一些 Controller 大批量的不再使用&#xff0c;或者有一些接口我们不想再提供给外界 一、使用 ComponentScan SpringBootApplication(scanBasePackages "com.zrb.excludeSomePkg") Comp…...

PHP Composer:高效项目依赖管理工具详解

PHP Composer:高效项目依赖管理工具详解 引言 随着Web开发领域的不断扩展,项目的复杂性也在逐渐增加。为了提高开发效率,减少重复劳动,依赖管理工具应运而生。其中,PHP的Composer成为了开发者们的首选。本文将详细介绍PHP Composer的功能、使用方法以及在实际开发中的应…...

第四届图像、信号处理与模式识别国际学术会议(ISPP 2025)

重要信息 大会官网&#xff1a;www.icispp.com 大会时间&#xff1a;2025年3月28日-30日 大会地点&#xff1a;南京 简介 由河海大学和江苏大学联合主办的第四届图像、信号处理与模式识别&#xff08;ISPP 2025) 将于2025年3月28日-30日在中国南京举行。主要围绕图像信号处…...

【设计模式精讲】创建型模式之工厂方法模式(简单工厂、工厂方法)

文章目录 第四章 创建型模式4.2 工厂方法模式4.2.1 需求: 模拟发放奖品业务4.2.2 原始开发方式4.2.3 简单工厂模式4.2.3.1 简单工厂模式介绍4.2.3.2 简单工厂原理4.2.3.3 简单工厂模式重构代码4.2.3.4 简单工厂模式总结 4.2.4 工厂方法模式4.2.4.1 工厂方法模式介绍4.2.4.2 工厂…...

python:多重继承、MRO(方法解析顺序)

在 Python 中&#xff0c;当类存在多重继承时&#xff0c;方法的调用顺序由 方法解析顺序&#xff08;Method Resolution Order, MRO&#xff09; 决定。 Python 使用 C3线性化算法 来确定类的继承顺序&#xff08;MRO&#xff09;&#xff0c;其核心规则是&#xff1a; 子类优…...

Oracle RAC数据库单节点轮流重启

0、sqlplus / as sysdba 备份参数文件 create pfile/home/oracle/pfile.ora from spfile; 备份控制文件 Alter database backup controlfile to trace; 1、关闭两节点的监听&#xff1b; 2、操作系统层面kill掉所有LOCALNO的所有进程&#xff0c;即&#xff1a;连接会话。 p…...

电脑想安装 Windows 11 需要开启 TPM 2.0 怎么办?

尽管 TPM 2.0 已经内置在许多新电脑中&#xff0c;但很多人并不知道如何激活这一功能&#xff0c;甚至完全忽略了它的存在。其实&#xff0c;只需简单的几步操作&#xff0c;你就能开启这项强大的安全特性&#xff0c;为你的数字生活增添一层坚固的防护屏障。无论你是普通用户还…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度​

一、引言&#xff1a;多云环境的技术复杂性本质​​ 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时&#xff0c;​​基础设施的技术债呈现指数级积累​​。网络连接、身份认证、成本管理这三大核心挑战相互嵌套&#xff1a;跨云网络构建数据…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

【配置 YOLOX 用于按目录分类的图片数据集】

现在的图标点选越来越多&#xff0c;如何一步解决&#xff0c;采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集&#xff08;每个目录代表一个类别&#xff0c;目录下是该类别的所有图片&#xff09;&#xff0c;你需要进行以下配置步骤&#x…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序

一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)

要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况&#xff0c;可以通过以下几种方式模拟或触发&#xff1a; 1. 增加CPU负载 运行大量计算密集型任务&#xff0c;例如&#xff1a; 使用多线程循环执行复杂计算&#xff08;如数学运算、加密解密等&#xff09;。运行图…...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

【Go语言基础【12】】指针:声明、取地址、解引用

文章目录 零、概述&#xff1a;指针 vs. 引用&#xff08;类比其他语言&#xff09;一、指针基础概念二、指针声明与初始化三、指针操作符1. &&#xff1a;取地址&#xff08;拿到内存地址&#xff09;2. *&#xff1a;解引用&#xff08;拿到值&#xff09; 四、空指针&am…...

论文阅读笔记——Muffin: Testing Deep Learning Libraries via Neural Architecture Fuzzing

Muffin 论文 现有方法 CRADLE 和 LEMON&#xff0c;依赖模型推理阶段输出进行差分测试&#xff0c;但在训练阶段是不可行的&#xff0c;因为训练阶段直到最后才有固定输出&#xff0c;中间过程是不断变化的。API 库覆盖低&#xff0c;因为各个 API 都是在各种具体场景下使用。…...