目录
摘要
本周我读了一篇题为《Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation》的论文。该文首次提出RNN编码器-解码器框架,在机器翻译任务上取得了良好效果。但其最大贡献在于提出了GRU模型。GRU简化了LSTM的设计,计算更快。通过学习,我理解了GRU的门控机制,以及它相对于LSTM在计算速度上的优势。
ABSTRACT
This week I read a paper titled "Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation". This paper first proposed the RNN encoder-decoder framework and achieved good performance on machine translation tasks. But its biggest contribution was proposing the GRU model. GRU simplified the design of LSTM and has faster calculation. Through learning, I understood the gating mechanisms of GRU and its advantage in computation speed over LSTM.
一、文献阅读
1、题目
论文题目:Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation
论文地址:https://arxiv.org/abs/1406.1078
2、ABSTRACT
本篇文章提出了一种新型神经网络模型,名为RNN编码器-解码器,它由两个RNN组成。一个RNN将一系列符号编码为固定长度的向量表示,另一个RNN将该表示解码为另一个符号序列。这篇文章最大的贡献就是提出了GRU。
This paper proposes a novel neural network model called the RNN encoder-decoder, which consists of two RNNs. One RNN encodes a sequence of symbols into a fixed-length vector representation, and the other decodes the representation into another sequence of symbols. The major contribution of this paper is the proposal of the GRU.
3、Decoder-Encoder模型
编码器是一个RNN,它根据输入序列在每一时间步更新隐状态,最后输出编码向量c来表示整个输入序列。
解码器是一个条件RNN,它根据编码向量c以及前面生成的符号,通过计算隐状态和条件概率分布,生成输出序列。
编码器:
编码器部分标注出了隐藏层状态,用表示,其更新公式为:
公式(1)说明当前时刻的隐藏层状态是由上一时刻的状态
和当前时刻的输入
共同决定的。f 是一个函数,可以是一个简单的逻辑函数,比如sigmoid函数,也可以是一个复杂的函数,比如LSTM。这篇论文中f用的是作者提出的一个新的结构,叫做GRU。
C是中间语义表示,可以是各个时刻隐藏输出的汇总:
也可以是最后一层隐层的输出:
本篇论文中作者是将隐层的最后输出作为语义表示 C 。由于编码器用的是RNN的结构,每一个循环中的信息都会流到下一个循环中,因此中间语义表示 C 理论上包含了输入序列的所有信息。
解码器:
图中标注出了解码器的隐藏层状态,用表示,从图中可以看出,隐藏层的状态更新,不仅跟上一时刻的输出
和当前时刻的输入
有关系,还跟上一时刻的输出
有关:
4、文献解读
1、Introduction
在研究神经网络在SMT中的应用的基础上,提出了一种新的神经网络体系结构,可以作为传统基于短语的SMT系统的一部分。提出的神经网络结构,我们将称为一个RNN Encoder–Decode,由两个递归神经网络(RNN)作为编码器和解码器对。实验结果表明,采用RNN Encoder–Decode对短语进行译码,提高了译码性能。定性分析表明,RNN Encoder–Decode能较好地捕捉短语表中的语言规律,间接解释了整体翻译性能的量化提升。
2、创新点
文章最大的共享就是提出了GRU:
先来看一下GRU各个状态是如何更新的:
其中,代表向量的第j个元素,也就是图中的公式都是以单个元素为例进行计算的。
表示将两个形状相同的矩阵对应元素相乘。[.]+[.]代表将两个向量进行拼接,不是对应元素相加。
和
分别代表输入和上一层的隐藏状态。
和
代表权重矩阵,*代表r、z或空。
代表sigmoid函数,图中z和r分别代表更新门和重置门。
如果重置门被设置为0,那么先前的隐藏层状态就会被强制遗忘,然后用当前的输入进行重置。这使得隐藏层状态可以有效的丢 弃跟未来没有任何关系的信息,从而使信息表达更为紧凑。而更新门则是用来控制先前的隐藏层状态会携带多少信息到当前状态。
3、实验过程
一、数据集
使用WMT2014数据集,其包含大量英法平行数据。双语语料库包括Europarl(6100万单词)、news commentary(550万单词)、UN(421万单词)和两个爬行语料库(9000万单词)。最后两个语料库比较吵。为了训练法语语言模型,除了bitext的目标侧外,还有大约7.12亿的爬行报纸材料。所有的单词计数都是经过标记的法语单词。
二、评估指标
提出使用RNN编码器-解码器模型来对短语进行分值,与传统的基于语言模型的方法进行对比BLEU分数。
三、超参数的设定
实验中使用的RNN Encoder–Decoder有1000个隐藏单元,标准差固定为0.01,h = ,ρ = 0.95 。
四、实验数据
当我们同时使用CSLM和RNN Encoder–Decoder的短语评分时,可以获得最佳的性能。这说明CSLM和RNN Encoder–Decoder的贡献不太相关,单独改进这两种方法可以得到更好的结果。此外,我们尝试惩罚神经网络未知的单词数量(即不在候选列表中的单词)。我们通过简单地将未知单词的数量作为一个附加特性添加到式(9)中的对数线性模型中来实现这一点。然而,在这种情况下我们无法在测试集上获得更好的性能,而只能在开发集上获得更好的性能。
图4左边的图显示了使用RNN Encoder–Decoder学习的单词嵌入矩阵对单词进行二维嵌入。该预测是由最近提出的BarnesHut-SNE (van der Maaten, 2013)完成的。我们可以清楚地看到,语义上相似的单词彼此聚集在一起(参见图4中的放大图)。这种情况下的表示(图1中的c)是一个1000维的向量。与单词表示类似,我们使用图5中的bar - hut - sne可视化由四个或更多单词组成的短语的表示。
从可视化结果可以看出,RNN Encoder–Decoder同时捕获了短语的语义结构和句法结构。例如,在左下角的图中,大多数短语是关于持续时间的,而那些在语法上相似的短语则聚集在一起。右下角的图表显示了语义相似(国家或地区)的短语集群。另一方面,右上角的图表显示了语法上相似的短语。
4、结论
采用RNN Encoder–Decoder的译码分数可以提高整体翻译性能。此外,我们发现RNN Encoder–Decoder的贡献与现有的在SMT系统中使用神经网络的方法是相当正交的,因此我们可以通过将RNN Encoder–Decoder和神经网络语言模型结合使用来进一步提高性能。我们对训练后的模型的定性分析表明,它确实在多个层面上捕捉到了语言规律,即单词层面和短语层面。这表明,有可能有更多的自然语言相关的应用程序,可能受益于拟议的RNN Encoder–Decoder。
二、GRU
我们想想对于一个序列而言,有的早期观测值对所有的未来观测值都非常有用,有的观测值对所有的未来预测都没有用,或者说有的序列各个部分之间有逻辑中断。总结起来就是:
- 并不是每个观测值都是同等重要
- 想要只记住相关的观察需要:
- 能关注的机制(更新门)
- 能遗忘的机制(重置门)
早的方法是"长-短期记忆" (long-short-term memory, LSMT):(Hochreiter.Schmidhuber.1997) 。门控循环单元(gated recurrent unit, GRU)(Cho.Van-Merrienboer.Bahdanau.ea.2014) 是一个稍微简化的变体,通常能够提供同等的效果,并且计算 (Chung.Gulcehre.Cho.ea.2014) 的速度明显更快。由于它更简单,就让我们从门控循环单元开始。
一、门控隐藏状态
普通的循环神经网络和门控循环单元之间的关键区别在于后者支持隐藏状态的门控,这意味着有专门的机制来确定应该何时更新隐藏状态,以及应该何时重置隐藏状态。
例如,如果第一个标记非常重要,我们将学会在第一次观测之后不更新隐藏状态。同样,我们也可以学会跳过不相关的临时观测。最后,我们还将学会在需要的时候重置隐藏状态。
1.1 重置门和更新门
重置门(reset gate)和更新门(update gate),我们把它们设计成(0,1)区间中的向量,这样我们就可以进行凸组合。例如,重置门允许我们控制可能还想记住的过去状态的数量。同样,更新门将允许我们控制新状态中有多少个是旧状态的副本。
下图描述了门控循环单元中的重置门和更新门的输入,输入是由当前时间步的输入和前一时间步的隐藏状态给出。两个门的输出是由使用 sigmoid 激活函数的两个全连接层给出。
1.2候选隐藏状态
接下来,让我们将重置门与RNN中的常规隐状态更新机制集成,得到在时间步t的候选隐藏状态
符号 是哈达码乘积(按元素乘积)运算符。在这里,我们使用 tanh 非线性激活函数来确保候选隐藏状态中的值保持在区间 ( − 1 , 1 ) 中。
计算的结果是候选者,因为我们仍然需要结合更新门的操作。与基础的RNN相比候选隐藏状态中的和
的元素相乘可以减少以往状态的影响。每当重置门
中的项接近1时,我们恢复一个如基本RNN中的普通的循环神经网络。对于重置门
中所有接近0的项,候选隐藏状态是以
作为输入的多层感知机的结果。因此,任何预先存在的隐藏状态都会被重置为默认值。
1.3 隐藏状态
最后,我们需要结合更新门,这确定新的的隐藏状态
在多大程度上就是旧的状态
,以及对新的候选状态
的使用量。更新门
仅需要在
和
之间进行按元素的凸组合就可以实现这个目标。门控循环单元的更新公式:
每当更新门 接近1时,我们就只保留旧状态。来自
的信息基本上被忽略,从而有效地跳过了依赖链条中的时间步t。相反,当
接近0时,新的隐藏状态
就会接近候选的隐藏状态
。这些设计可以帮助我们处理循环神经网络中的梯度消失问题,并更好地捕获时间步距离很长的序列的依赖关系。例如,如果整个子序列的所有时间步的更新门都接近于 1,则无论序列的长度如何,在序列起始时间步的旧隐藏状态都将很容易保留并传递到序列结束。
重置门有助于捕获序列中的短期依赖关系。
更新门有助于捕获序列中的长期依赖关系。
二、手动实现GRU
其中d2l是李沐老师自己的一个包,可以从github上自行下载,之后我们就可以通过train_iter以mini-batch方式读取数据;并通过vocab把词转换成数字表示进行模型输入和输出。
import torch
from torch import nn
from d2l import torch as d2l
# 设置每个batch的大小和时间步数
batch_size, num_steps = 32, 35
# 加载时间机器数据集并获取词汇表
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
先设置输入输出参数维度为词汇表大小,然后定义了两个函数normal和three,用于生成服从正态分布的随机权重张量。然后利用three函数初始化了GRU模型中更新门、重置门、候选隐状态等不同组件所需的权重和偏置参数。接着初始化了从隐状态到输出的权重和偏置。最后将所有参数放入列表中,设置requires_grad,并返回参数列表。这样就完成了GRU模型中不同门控机制的参数初始化,为后续建立GRU模型提供了参数准备。
# 初始化模型参数
def get_params(vocab_size, num_hiddens, device):
# 设置输入和输出的维度为词汇表大小
num_inputs = num_outputs = vocab_size
# 定义一个函数,用于生成服从正态分布的随机张量,并乘以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))
# 初始化GRU中的权重和偏置
# 权重和偏置用于控制更新门
W_xz, W_hz, b_z = three() # GRU多了这两行
# 权重和偏置用于控制重置门
W_xr, W_hr, b_r = three() # GRU多了这两行
# 权重和偏置用于计算候选隐藏状态
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:
# 设置参数的`requires_grad`属性为True,以便进行梯度计算
param.requires_grad_(True)
# 返回参数列表中所有参数
return params
这个函数接收batch_size, num_hiddens和device作为参数。它返回一个只包含一个元素的元组,个元素是一个形状为(batch_size, num_hiddens)、设备为device、且值全为0的张量。这个全零的隐藏状态张量将作为GRU模型最初的隐藏状态使用。
# 定义隐藏状态的初始化函数
def init_gru_state(batch_size, num_hiddens, device):
# 返回隐藏状态初始化为全零的元组
return (torch.zeros((batch_size, num_hiddens), device=device),)
以下代码通过输入序列上的逐步计算,实现了GRU的基本前向计算流程,包括不同门控单元的计算以及隐藏状态的更新。
def gru(inputs, state, params):
# 参数 params 解包为多个变量,分别表示模型中的权重和偏置
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
# 传入的隐藏状态 state 解包为单个变量 H。
H, = state
# 创建一个空列表,用于存储每个时间步的输出
outputs = []
# 遍历输入序列中的每个时间步
for X in inputs:
# 更新门控机制 Z
Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
# 重置门控机制 R
R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
# 计算候选隐藏状态 H_tilda
H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
# 更新隐藏状态 H
H = Z * H + (1 - Z) * H_tilda
# 计算输出 Y
Y = H @ W_hq + b_q
# 将输出添加到列表中
outputs.append(Y)
# 将所有输出拼接在一起,并返回拼接后的结果和最终的隐藏状态
return torch.cat(outputs, dim=0), (H,)
训练与预测:
训练和预测的工作方式与RNN中的实现完全相同。训练结束后,我们分别打印输出训练集的困惑度和前缀“time traveler”和“traveler”的预测序列上的困惑度。
# 训练
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
# 设置训练的总轮数和学习率
num_epochs, lr = 500, 1
# 创建 GRU 模型实例
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,
init_gru_state, gru)
# 调用 D2L 库中的训练函数,传入模型实例、训练数据迭代器、词汇表、学习率和训练的总轮数
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
简单实现:
高级API包含了前文介绍地全部配置细节,所以可以直接实例化GRU。其使用编译好的运算符来进行计算,而非python处理其中的许多细节
# 简洁实现
# 设置输入特征的维度为词汇表大小
num_inputs = vocab_size
# 创建一个 GRU 层,指定输入特征的维度和隐藏单元的数量
gru_layer = nn.GRU(num_inputs, num_hiddens)
# 创建一个 RNNModel 实例,传入 GRU 层和词汇表的大小
model = d2l.RNNModel(gru_layer, len(vocab))
# 将模型移动到指定的设备上(可能是 GPU)
model = model.to(device)
# 调用 D2L 库中的 train_ch8 函数进行训练,传入模型实例、训练数据迭代器、词汇表、学习率和训练的总轮数
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
三、张量
一、梯度
梯度是描述标量场变化率的向量。对于三维空间中的标量场f(x,y,z),其梯度是一个向量场,定义为:grad f = (df/dx, df/dy, df/dz)这里,df/dx、df/dy和df/dz分别表示f函数在x、y、z方向上的偏导数。直观上,梯度指向了函数值增加最快的方向。几何意义上,梯度与函数的等高线垂直。物理意义上,梯度表示场的力,其方向表示最大变化率的方向。梯度广泛应用于描述温度场、压力场等标量场的变化情况。
梯度算符作用于一个张量是并积的作用。
梯度算符也叫nabla算子、Hamilton算符。
举例:标量场的梯度
也就是零阶张量的梯度。
一阶张量与零阶张量做并积,生成一个一阶张量。
例:向量场的梯度
当求一阶张量的梯度时,得到的就是一个二阶张量。
由于梯度算子的作用方式是并积,并且梯度算子是一个一阶张量,所以一个张量求梯度后,一定会升高一阶。
二、散度
散度描述向量场的发散和汇聚性质。对三维向量场F=(P,Q,R),其散度为一个标量:div F = dP/dx + dQ/dy + dR/dz散度大于0表示场向外发散,小于0表示场向内汇聚。几何意义上,散度描述向量场的源(汇)的密度。物理意义上,与质量守恒定律相关。散度广泛应用于连续介质力学、电荷密度分布等方面。
散度的作用方式是nabla算子与张量做点积。标量场无散度,故我们用向量场来进行举例。
三、旋度
旋度描述向量场中旋转的程度和方向。对三维空间中的向量场F=(P,Q,R),其旋度是一个向量场,定义为:curl F = (dR/dy - dQ/dz, dP/dz - dR/dx, dQ/dx - dP/dy)几何意义上,旋度描述了向量场绕其定向路径的循环。物理意义上,旋度与流体的皮拉克流和涡量有关。非零旋度通常意味着向量场中存在旋转。旋度广泛应用于电磁学、流体力学等领域。
旋度是给我们一个向量场,我们得到的是一个向量,代表的是这一点的旋转情况。
四、爱因斯坦求和约定
爱因斯坦标记法不仅使用下标(如),同时也会使用上标(如
,i=index),这与我们以前常用的幂次方(
)容易产生混淆。
在张量分析中,很多表达式都是对指标的求和式,例如:
在使用了爱因斯坦求和约定后,就可以进行简化:
规则1:如果在式子中一个指标出现两次(我们称为哑指标),那么这个式子就代表着求和,范围一般是从1到3(因为大部分问题都是在三维空间下进行分析)
如:
规则2:哑指标与自由指标的定义:
上式中j是哑指标,代表需要进行求和,i可以被替换为其他字母,只需满足两个条件:
在原式中未使用过,比如i无法被替换成j;
替换的字母要有相同的求和范围,例如从1到3。
i是自由指标,自由指标只能出现一次i可以表示哑指标j所能表示的任何值,即1,2,3中的任意一个,但一次只能表示一个值,当i表示1时,式子即为
自由指标不能被替换成其他的自由指标,比如说把i换成k,因为k可能代表的2,而i代表的是1。
规则3:任何一个单项式中一个指标最多只能出现两次,出现三次及以上就是错误的表达,比如说可以写成,但不能写成
注意:当我们在统计指标出现次数的时候,上下标是合起来一起数的,不会说下标和上标分开数。比如说
中j是一个哑指标。
规则4:在一个使用求和标记的等式中,等式两边的自由指标要匹配。
如:,在等式左边项的自由指标是i,在等式右边项的自由指标也是i。
如:,等式左边项的自有指标是i,在等式右边第一项的自由指标是i,哑指标是k和j,在等式右边第二项的自由指标是i,哑指标是k。
总结
GRU跟LSTM很相似,不过它只有两个门控单元,其次也对张量的梯度散度旋度进行了初步学习,下周我将阅读seq2seq的论文,并且继续对张量进行学习。