神经机器翻译技术NMT
主要特点:输出是单词序列而不是单个单词,并且可能输出序列的长度与输入序列的长度不同
机器翻译的实现过程
1. 数据预处理
-
乱码处理
我们通常所用的空格是 \x20 ,是在标准ASCII可见字符 0x20~0x7e 范围内。 而 \xa0 属于 latin1 (ISO/IEC_8859-1)中的扩展字符集字符,代表不间断空白符nbsp(non-breaking space),超出gbk编码范围,是需要去除的特殊字符。 -
大小写转换
分词
把字符串转换成单词组成的列表list
建立词典
把单词组成的列表,转换成单词id组成的列表,还有词频
源语言和目标语言都需要建立词典
载入数据
字符是无法直接输入到模型的,所以我们通常需要把数据转换成vector,转换的方法有很多,这里不做详细描述。
这里有个pad函数,是要限制输入的长度,因为我们需要输入的长度都必须一致,所以写了这个函数:
def pad(line, max_len, padding_token):
if len(line) > max_len:
return line[:max_len]
return line + [padding_token] * (max_len - len(line))
pad(src_vocab[source[0]], 10, src_vocab.pad)
如果超出了max_len,我们就截取到最大值,如果小于max_len,那么在多余的位置补上padding_token,通常设为0
Encoder-Decoder
encoder:输入到隐藏状态
decoder:隐藏状态到输出
还记得上面提到的输入输出长度不一的问题吗? Encoder-Decoder模型框架就可以应对这个问题,Encoder中进行训练,得到隐藏状态,然后再把隐藏状态作为Decoder输入,在Decoder中输出结果.
Seq2Seq模型
Seq2Seq模型是按照 Encoder-Decoder框架设计的,目的就是输入一个序列,输出另一个序列
模型:
训练
预测
具体结构:
上面的三张图片很好的解释了Seq2Seq模型的运作方式,我们把英语按序列顺序,输入到Encoder的RNN中,最后计算出一个hidden state,作为Decoder的隐藏状态输入,然后用法语数据作为输入X,训练出模型。
在预测中,输入英语句子,得到hidden state,用BOS作为Decoder的输入X,hidden state作为隐藏状态输入,一直计算知道遇见EOS,计算的时候,一个神经元的单词输出作为下一个神经元的输入X。Dense层是最后的翻译的输出层
损失函数
def SequenceMask(X, X_len,value=0):
maxlen = X.size(1)
mask = torch.arange(maxlen)[None, :].to(X_len.device) < X_len[:, None]
X[~mask]=value
return X
SequenceMask函数是为了处理之前padding出来的多余的0,因为计算这些0的损失是没有意义的,所以使用函数去除它.
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
# pred shape: (batch_size, seq_len, vocab_size)
# label shape: (batch_size, seq_len)
# valid_length shape: (batch_size, )
def forward(self, pred, label, valid_length):
# the sample weights shape should be (batch_size, seq_len)
weights = torch.ones_like(label)
weights = SequenceMask(weights, valid_length).float()
self.reduction='none'
output=super(MaskedSoftmaxCELoss, self).forward(pred.transpose(1,2), label)
return (output*weights).mean(dim=1)
MaskedSoftmaxCELoss是这里用的损失函数
Beam Search集束搜索
在生成每个时间步的单词,使用的是
贪心搜索算法:每一步都把得分最高的单词作为输出,可是有个缺点,就是只会得到当前的最优解,没有考虑前后语义是否连贯,没有考虑全局最优解。
维特比算法:把所有的单词都试一遍,然后找到整体得分最高的句子,但是搜索空间太大,计算难
集束搜索:
所以提出集束搜索方法,结合了贪心搜索和维特比算法,ABCDE代表单词表里的单词,找到得分最高的两个,对于A和C的下一个词,从10个中又找两个,以此类推,
最后得到最好的两个候选句子。
这个2是因为我们把beam设成2
Attention注意力机制加入到Seq2Seq模型
提出attention机制的缘由:
在Seq2Seq模型中,Decoder从Encoder中接受的唯一信息是最后一个隐藏状态,类似于输入序列总结的向量表示。 当输入较长的序列,一个向量非常难表达全部信息,会导致灾难性遗忘。 所以如果不是给decoder一个向量,而是给Decoder提供Encoder每个时间步的向量表示,是否会得到更好地结果呢?
注意力机制的简述
注意力机制有两种类型:全局注意力、局部注意力
全局:使用所有编码器隐藏状态的注意力类型
局部:仅使用编码器隐藏状态的子集
seq2seq 原理:翻译器从头到尾读取德语文本。读取完成后,开始逐词将文本译成英文。如果句子非常长,它可能已经忘记了前文的内容
seq2seq+attention 原理:翻译器从头到尾读取德语文本并记录关键词,之后将文本译成英文。在翻译每个德语单词时,翻译器会使用记录的关键词。
通过为每个单词分配分值,注意力为不同单词分配不同的注意力。然后利用 softmax 对编码器隐藏状态进行加权求和,得到上下文向量(context vector)。注意力层的实现可以分为 4 个步骤。
原理介绍
注意力是编码器和解码器之间的接口,它为解码器提供每个编码器隐藏状态的信息。这个做法有助于模型有选择性地侧重输入序列的有用部分,从而学习它们之间的 alignment,即原始文本片段匹配与其对应的译文片段,这样可以有效处理输入的长句子。
法语单词「la」的 alignment 分布在输入序列中,但主要分布在这四个词上:『the』、『European』、『Economic』 和 『Area』。深紫色表示注意力得分更高。
实现步骤
-
准备隐藏状态
第一个Decoder单元的隐藏状态(query), 所有可用的Encoder隐藏状态(key) -
获取每个Encoder的隐藏状态分数
有一个评分函数(alignment评分函数),这里使用Encoder和Decoder的隐藏状态之间的点积 -
通过softmax层运行所有分数
将得分放到softmax层,softmax得分和为1,softmax得分分布代表注意力分布 -
把得分与每个编码器隐藏状态(value)相乘,获得alignment向量
-
将 alignment 向量相加(加权求和),生成上下文向量 。上下文向量是前一步 alignment 向量的集合信息
-
将上下文向量输入解码器(结束)
评分函数
点积注意力dot product
计算query和key转置的乘积来计算attention score,通常还会除去 d \sqrt{d} d减少计算出来的score对维度𝑑的依赖性
假设 𝐐∈ℝ^{𝑚×𝑑} 有
m
m
m 个query,𝐊∈ℝ^{𝑛×𝑑}有
n
n
n 个keys. 我们可以通过矩阵运算的方式计算所有
m
n
mn
mn 个score:
α
(
Q
,
K
)
=
Q
K
T
/
d
\alpha(Q,K)=QK^T/\sqrt{d}
α(Q,K)=QKT/d
现在让我们实现这个层,它支持一批查询和键值对。此外,它支持作为正则化随机删除一些注意力权重.
多层感知机注意力multilayer perceptron
在多层感知器中,我们首先将 query and keys 投影到
R
h
ℝ^ℎ
Rh .为了更具体,我们将可以学习的参数做如下映射
W
k
∈
R
h
×
d
k
W_k∈ℝ^{h×d_k}
Wk∈Rh×dk ,
W
q
∈
R
h
×
d
q
W_q∈ℝ^{h×d_q}
Wq∈Rh×dq , and
v
∈
R
h
v∈ℝ^h
v∈Rh . 将score函数定义
α
(
k
,
q
)
=
v
T
t
a
n
h
(
W
k
k
+
W
q
q
)
\alpha(k,q)=v^Ttanh(W_kk+W_qq)
α(k,q)=vTtanh(Wkk+Wqq)
.
然后将key 和 value 在特征的维度上合并concatenate,然后送至一个单隐藏层的感知机,这层中 hidden layer 为 ℎ 并且输出的size为 1 .隐层激活函数为tanh,无偏置.
Transformer
transformer有点难,我觉得自己理解上还不到位,就写一写关键点吧。
Transformer为了解决什么问题?结构?
CNN可以并行化,但无法捕捉长序列里的依赖关系,而RNN可以捕捉这些关系,却无法并行化处理序列,所以Google整合了两个NN的优势,设计了Transformer模型出来,利用Attention机制实现了并行化捕捉序列依赖,并且同时处理序列的每个位置的tokens。
性能优异且训练时间减少
首先,Transformer模型用blocks代替了seq2seq的RNN,Blocks里面包含了多头注意力层和前馈神经网络。Decoder中比encoder多一个多头注意力层,是用来接受encoder的隐藏状态。
其次,有一个add and norm层,该层包含残差结构以及层归一化,是为了让输出出来的值按照正确的方式输入到下一层。
最后,我们留意到一个positional encoding,因为自注意力层并没有区分元素顺序,所以需要一个位置编码层被用于向序列元素里添加位置信息
多头注意力层
多头注意力层是由自注意力结构变化而来的,所以先来了解一下什么是自注意力模型:
自注意力模型就是根据Attention机制设计的,它的特点是序列的每一个元素对应的key、value、query都是完全一致的,并且自注意力对每个元素输出的计算是并行的。
多头注意力层包含
h
h
h个并行的自注意力层,每一个这种层被叫做一个head。对每个头来说,在进行注意力计算之前,我们会将query、key和value用三个现行层进行映射,这
h
h
h个注意力头的输出将会被拼接之后输入最后一个线性层进行整合。
假设query,key和value的维度分别是 d q d_q dq、 d k d_k dk和 d v d_v dv。那么对于每一个头 i = 1 , … , h i=1,\ldots,h i=1,…,h,我们可以训练相应的模型权重 W q ( i ) ∈ R p q × d q W_q^{(i)} \in \mathbb{R}^{p_q\times d_q} Wq(i)∈Rpq×dq、 W k ( i ) ∈ R p k × d k W_k^{(i)} \in \mathbb{R}^{p_k\times d_k} Wk(i)∈Rpk×dk和 W v ( i ) ∈ R p v × d v W_v^{(i)} \in \mathbb{R}^{p_v\times d_v} Wv(i)∈Rpv×dv,以得到每个头的输出:
o ( i ) = a t t e n t i o n ( W q ( i ) q , W k ( i ) k , W v ( i ) v ) o^{(i)} = attention(W_q^{(i)}q, W_k^{(i)}k, W_v^{(i)}v) o(i)=attention(Wq(i)q,Wk(i)k,Wv(i)v)
这里的attention可以是任意的attention function,比如前一节介绍的dot-product attention以及MLP attention。之后我们将所有head对应的输出拼接起来,送入最后一个线性层进行整合,这个层的权重可以表示为 W o ∈ R d 0 × h p v W_o\in \mathbb{R}^{d_0 \times hp_v} Wo∈Rd0×hpv
o = W o [ o ( 1 ) , … , o ( h ) ] o = W_o[o^{(1)}, \ldots, o^{(h)}] o=Wo[o(1),…,o(h)]
基于位置的前馈网络Position-wise FFN
它接受一个形状为(batch_size,seq_length, feature_size)的三维张量。Position-wise FFN由两个全连接层组成,他们作用在最后一维上。因为序列的每个位置的状态都会被单独地更新,所以我们称他为position-wise,这等效于一个1x1的卷积。
Add and Norm
相加归一化层,它可以平滑地整合输入和其他层的输出,因此我们在每个多头注意力层和FFN层后面都添加一个含残差连接的Layer Norm层。层归一化可以防止层内的数值变化过大,从而有利于加快训练速度并且提高泛化性能。
- Layer Norm和Batch Norm的区别
它们很相似,唯一的区别在于Batch Norm是对于batch size这个维度进行计算均值和方差的,而Layer Norm则是对最后一维进行计算。
位置编码positional encoding
多头注意力网络和前馈神经网络都是独立地对每个位置的元素进行更新,会丢失了序列顺序的信息,所以Transformer模型引入了位置编码去保持输入序列元素的位置。
假设输入序列的嵌入表示 X ∈ R l × d X\in \mathbb{R}^{l\times d} X∈Rl×d, 序列长度为 l l l嵌入向量维度为 d d d,则其位置编码为 P ∈ R l × d P \in \mathbb{R}^{l\times d} P∈Rl×d ,输出的向量就是二者相加 X + P X + P X+P。
位置编码是一个二维的矩阵,i对应着序列中的顺序,j对应其embedding vector内部的维度索引。我们可以通过以下等式计算位置编码:
P i , 2 j = s i n ( i / 1000 0 2 j / d ) P_{i,2j} = sin(i/10000^{2j/d}) Pi,2j=sin(i/100002j/d)
P i , 2 j + 1 = c o s ( i / 1000 0 2 j / d ) P_{i,2j+1} = cos(i/10000^{2j/d}) Pi,2j+1=cos(i/100002j/d)
f o r i = 0 , … , l − 1 a n d j = 0 , … , ⌊ ( d − 1 ) / 2 ⌋ for\ i=0,\ldots, l-1\ and\ j=0,\ldots,\lfloor (d-1)/2 \rfloor for i=0,…,l−1 and j=0,…,⌊(d−1)/2⌋