1.1 seq2seq模型
定义:
一个序列到一个序列(seq2seq)模型,接收的输入是一个(单词、字母、图像特征)序列,输出的是另一个序列;在神经机器翻译中,一个序列是指一连串的单词。
模型结构:由编码器和解码器组成
编码器:用于处理输入序列中的每个元素,把这些信息转换为一个上下文向量,处理完成后,输出到解码器;
解码器:用于逐项生成输出序列中的元素。
原始模型:
编码器和解码器使用循环神经网络(RNN);
上下文向量的长度基于编码器RNN的隐藏层神经元的数量;
RNN在每个时间步接受2个输入:输入序列中的一个元素、一个隐藏层状态(hidden state)。
处理过程:
编解码器处理过程:编码器和解码器在每个时间步处理输入得到输出,RNN根据当前时间步的输入和前一个时间步的隐藏层状态,更新当前时间步的隐藏层状态。
特点:与RNN最后一次输出隐藏层状态不同,编码器和解码器在每个时间步处理输入得到输出。
1.2 Attention
效果:
在机器翻译场景,产生英语翻译之前,提供从输入序列范大相关信号的能力,使得模型产生更好的结果。
Attention与seq2seq模型的区别:
Attention中,编码器把所有时间步的隐藏层状态传递给解码器;而seq2seq中,只将最后一个隐藏层传递给解码器;
Attention的解码器在产生输出序列之前,做了一些处理:
1.查看所有接收到编码器的hidden state
2.给每个hidden state一个分数
3.将每个hidden state乘以经过softmax的对应分数,高分对应的hidden state被放大,低分的则被缩小
Attention处理过程:
解码器RNN的输入:一个embedding向量、一个初始化的解码器hidden state
处理上述输入,产生一个输出和一个新的hidden state(h4向量)
使用编码器的hidden state和上述产生的hidden state(h4向量)计算当前时间步的上下文向量(C4向量)
将h4和C4进行拼接,得到一个向量
将上述向量输入到一个前馈神经网络
前馈神经网络的输出表示这个时间步输出的单词
下一个时间步重复上述步骤(从第1步到第6步)
1.3 Transformer
主要组成部分:
由编码部分和解码部分组成
编码部分由多层编码器组成(6层),解码部分由多层解码器组成(6层)
编码器由Self-Attention Layer 和 Feed Forward Neural Network(前馈神经网络,FFNN)组成
解码器由Self-Attention Layer、Encoder-Decoder Attention、FFNN组成,其中Encoder-Decoder Attention用于帮助解码器聚焦于输入句子的相关部分
1.Transformer输入
使用词嵌入算法,将每个词转换为一个词向量,向量列表的长度为训练集中的句子最大长度。
2.Encoder(编码器)
输入:一个向量列表,上一个编码器的输出
输出:同样大小的向量列表,连接到下一个编码器
3.数据流:
每个单词转换成一个向量后,输入到self-attention层
每个位置的单词得到新向量,输入到FFNN
4.Self-Attention
直观理解,Self-Attention机制使模型不仅能够关注这个位置的词,而且能够关注句子中其他位置的词,作为辅助线索,更好地编码当前位置的词。
5.残差连接:
在编解码器的每个子层,都有一个残差连接、一个层标准化(layer-normalization)
6.Decoder(解码器)
解码阶段的每一个时间步都输出一个翻译后的单词
将解码器的输入向量,加上位置编码向量,用于指示每个词的位置
Self-Attention层:只允许关注到输出序列中早于当前位置之前的单词,在Self-Attention分数经过Softmax层之前,屏蔽当前位置之后的位置
Encoder-Decoder Attention层:和多头注意力机制类似,但是使用前一层的输出构造Query矩阵,而Key矩阵和Value矩阵来自于解码器最终的输出。
7.最后的线性层和Softmax层
线性层:把解码器输出的向量,映射到一个更长的向量(logist向量),和模型的输出词汇表长度一致
Softmax层:把logist向量中的数,转换为概率,取概率最高的那个数字对应的词,就是输出的单词
1.4 Self-Attention
Self-Attention输出的计算步骤:
对输入编码器的每个词向量,通过乘以三个参数矩阵,创建3个向量(Query向量、Key向量、Value向量)
计算Attention Score分数:计算某个词的对句子中其他位置的词放置的注意力程度,通过该词对应的Query向量与每个词的Key向量点积计算得到。
将每个分数除以\sqrt{d_{key}}dkey,d_{key}dkey是Key向量的长度
这些分数经过一个Softmax,进行归一化
将归一化的分数乘以每个Value向量
对上步的值进行相加,得到该词在Self-Attention层的输出
Self-Attention的矩阵形式计算
假设所有词向量组成矩阵XX,权重矩阵W^Q,W^K,W^VWQ,WK,WV
根据第1步,可得Q = X \cdot W^Q \\ K = X \cdot W^K \\ V = X \cdot W^VQ=X⋅WQK=X⋅WKV=X⋅WV
根据第2~6步,可得Self-Attention的输出Z = \text{softmax}\left( \frac{Q \cdot K^T}{\sqrt{d_{K}}} \right) \cdot VZ=softmax(dKQ⋅KT)⋅V
1.5 多头注意力机制(multi-head attention)
作用:增强Attention层
扩展了模型关注不同位置的能力
赋予Attention层多个“子表示空间”
处理过程:
随机初始化8组W^Q,W^K,W^VWQ,WK,WV
根据矩阵计算的第1步,词向量矩阵XX和每组W^Q,W^K,W^VWQ,WK,WV相乘,得到8组Q,K,VQ,K,V矩阵
根据矩阵计算的第2步,得到8个ZZ矩阵
将8个ZZ矩阵横向拼接,并乘以另一个权重矩阵W^OWO,得到一个矩阵ZZ
1.6 使用位置编码表示序列中单词的顺序
目的:通过对每个输入的向量,添加一个遵循特定模式向量,用于确定每个单词的我i之,或者句子中不同单词之间的距离。
特定模式向量:向量的左半部分值由sine函数产生,右半部分的值由cosine函数产生,然后拼接,得到每个位置编码向量。
优点:扩展到需要翻译句子的序列长度
1.7 损失函数
目的:由于模型的参数是随机初始化的,每个词输出的概率分布与正确的输出概率分布比较,使用反向传播调整模型权重
贪婪解码:由于模型每个时间步只产生一个输出,模型是从概率分布中选择概率最大的词,并丢弃其他词
集束搜索:每个时间步保留beam_size个概率最高的输出词,然后在下一个时间步,根据第1个词计算第2个位置的词的概率分布,对于后续位置,重复上述过程。
beam_size:用于在所有时间步保留最高频率的词的个数
top_beams:用于表示最终返回翻译结果的个数
2 实战练习
词嵌入¶
如上图所示,Transformer图里左边的是Encoder,右边是Decoder部分。Encoder输入源语言序列,Decoder里面输入需要被翻译的语言文本(在训练时)。一个文本常有许多序列组成,常见操作为将序列进行一些预处理(如词切分等)变成列表,一个序列的列表的元素通常为词表中不可切分的最小词,整个文本就是一个大列表,元素为一个一个由序列组成的列表。如一个序列经过切分后变为["am", "##ro", "##zi", "meets", "his", "father"],接下来按照它们在词表中对应的索引进行转换,假设结果如[23, 94, 13, 41, 27, 96]。假如整个文本一共100个句子,那么就有100个列表为它的元素,因为每个序列的长度不一,需要设定最大长度,这里不妨设为128,那么将整个文本转换为数组之后,形状即为100 x 128,这就对应着batch_size和seq_length。
输入之后,紧接着进行词嵌入处理,词嵌入就是将每一个词用预先训练好的向量进行映射。
词嵌入在torch里基于torch.nn.Embedding实现,实例化时需要设置的参数为词表的大小和被映射的向量的维度比如embed = nn.Embedding(10,8)。向量的维度通俗来说就是向量里面有多少个数。注意,第一个参数是词表的大小,如果你目前最多有8个词,通常填写10(多一个位置留给unk和pad),你后面万一进入与这8个词不同的词就映射到unk上,序列padding的部分就映射到pad上。
假如我们打算映射到8维(num_features或者embed_dim),那么,整个文本的形状变为100 x 128 x 8。接下来举个小例子解释一下:假设我们词表一共有10个词(算上unk和pad),文本里有2个句子,每个句子有4个词,我们想要把每个词映射到8维的向量。于是2,4,8对应于batch_size, seq_length, embed_dim(如果batch在第一维的话)。
另外,一般深度学习任务只改变num_features,所以讲维度一般是针对最后特征所在的维度。
开始编程:
所有需要的包的导入:
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
from torch.nn.init import xavier_uniform_
from torch.nn.init import constant_
from torch.nn.init import xavier_normal_
import torch.nn.functional as F
from typing import Optional, Tuple, Any
from typing import List, Optional, Tuple
import math
import warnings
X = torch.zeros((2,4),dtype=torch.long)
embed = nn.Embedding(10,8)
print(embed(X).shape)
位置编码
词嵌入之后紧接着就是位置编码,位置编码用以区分不同词以及同词不同特征之间的关系。代码中需要注意:X_只是初始化的矩阵,并不是输入进来的;完成位置编码之后会加一个dropout。另外,位置编码是最后加上去的,因此输入输出形状不变。
Tensor = torch.Tensor
def positional_encoding(X, num_features, dropout_p=0.1, max_len=512) -> Tensor:
r'''
给输入加入位置编码
参数:
- num_features: 输入进来的维度
- dropout_p: dropout的概率,当其为非零时执行dropout
- max_len: 句子的最大长度,默认512
形状:
- 输入: [batch_size, seq_length, num_features]
- 输出: [batch_size, seq_length, num_features]
例子:
>>> X = torch.randn((2,4,10))
>>> X = positional_encoding(X, 10)
>>> print(X.shape)
>>> torch.Size([2, 4, 10])
'''
dropout = nn.Dropout(dropout_p)
P = torch.zeros((1,max_len,num_features))
X_ = torch.arange(max_len,dtype=torch.float32).reshape(-1,1) / torch.pow(
10000,
torch.arange(0,num_features,2,dtype=torch.float32) /num_features)
P[:,:,0::2] = torch.sin(X_)
P[:,:,1::2] = torch.cos(X_)
X = X + P[:,:X.shape[1],:].to(X.device)
return dropout(X)
# 位置编码例子
X = torch.randn((2,4,10))
X = positional_encoding(X, 10)
print(X.shape)
2.1 使用PyTorch的MultiheadAttention来实现Attention的计算
torch.nn.MultiheadAttention(embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False, add_zero_attn=False, kdim=None, vdim=None)
参数说明如下:
embed_dim:最终输出的 K、Q、V 矩阵的维度,这个维度需要和词向量的维度一样
num_heads:设置多头注意力的数量。如果设置为 1,那么只使用一组注意力。如果设置为其他数值,那么 num_heads 的值需要能够被 embed_dim 整除
dropout:这个 dropout 加在 attention score 后面
forward(query, key, value, key_padding_mask=None, need_weights=True, attn_mask=None)
参数说明如下:
query:对应于 Key 矩阵,形状是 (L,N,E) 。其中 L 是输出序列长度,N 是 batch size,E 是词向量的维度
key:对应于 Key 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度
value:对应于 Value 矩阵,形状是 (S,N,E) 。其中 S 是输入序列长度,N 是 batch size,E 是词向量的维度
key_padding_mask:如果提供了这个参数,那么计算 attention score 时,忽略 Key 矩阵中某些 padding 元素,不参与计算 attention。形状是 (N,S)。其中 N 是 batch size,S 是输入序列长度。
如果 key_padding_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
如果 key_padding_mask 是 BoolTensor,那么 True 对应的位置会被忽略
attn_mask:计算输出时,忽略某些位置。形状可以是 2D (L,S),或者 3D (N∗numheads,L,S)。其中 L 是输出序列长度,S 是输入序列长度,N 是 batch size。
如果 attn_mask 是 ByteTensor,那么非 0 元素对应的位置会被忽略
如果 attn_mask 是 BoolTensor,那么 True 对应的位置会被忽略