RNN
Transformer
参考
https://www.ylkz.life/deeplearning/p12158901
https://zhuanlan.zhihu.com/p/396221959
https://zhuanlan.zhihu.com/p/376286459
https://zhuanlan.zhihu.com/p/265108616
https://zhuanlan.zhihu.com/p/338817680
模型结构
Input Embedding
将文本中词汇的数字表示转变为向量表示, 希望得到其在高维空间中的特征表示向量。
# 导入必备的工具包
import torch
import torch.nn as nn
import math
from torch.autograd import Variable
# 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层, 他们共享参数.
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
"""类的初始化函数,有两个参数,
d_model: 指词嵌入的维度,
vocab: 指词表的大小
"""
super(Embeddings, self).__init__()
# 调用nn中的预定义层Embedding, 获得一个词嵌入对象self.lut
self.lut = nn.Embedding(vocab, d_model)
# 将d_model传入类中
self.d_model = d_model
def forward(self, x):
# 将x传给self.lut并与根号下self.d_model相乘作为结果返回
return self.lut(x) * math.sqrt(self.d_model)
Positional Encoding
在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.
# 定义位置编码器类
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
"""位置编码器类的初始化函数, 共有三个参数
分别是d_model: 词嵌入维度
dropout: 置0比率
max_len: 每个句子的最大长度
"""
super(PositionalEncoding, self).__init__()
# 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropout
self.dropout = nn.Dropout(p=dropout)
# 初始化一个位置编码矩阵, 它是一个0阵,矩阵的大小是max_len x d_model.
pe = torch.zeros(max_len, d_model)
# 初始化一个绝对位置矩阵, 在这里,词汇的绝对位置用它的索引表示.
# 首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵,
position = torch.arange(0, max_len).unsqueeze(1)
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
# 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
# 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
# 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 使用unsqueeze拓展维度.
pe = pe.unsqueeze(0)
# 最后把pe位置编码矩阵注册成模型的buffer
self.register_buffer('pe', pe)
def forward(self, x):
"""forward函数的参数是x, 表示文本序列的词嵌入表示"""
# 在相加之前我们对pe做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的那一维将切片到与输入的x的第二维相同即x.size(1),
# 因为我们默认max_len为5000一般来讲实在太大了,很难有一条句子包含5000个词汇,所以要进行与输入张量的适配.
# 最后使用Variable进行封装,使其与x的样式相同,但是它是不需要进行梯度求解的,因此把requires_grad设置成false.
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
# 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
return self.dropout(x)
Attention
你看这是猫是狗?
观察事物时,之所以能够快速判断一种事物,是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断。比如这里的狗头就是我们会重点注意的地方。
从本质上理解,Attention是从大量信息中有筛选出少量重要信息,Value(i)是i部分的信息,i部分越重要权重越大,越不重要的Value(i)权重越小。
Attention机制的具体计算过程:
- 根据Query和Key计算权重系数
1.1 根据Query和Key计算两者的相似性或者相关性(常用三种方法)
- 求两者的向量点积
- 求两者的向量Cosine相似性
- 通过再引入额外的神经网络来求值
1.2 进行归一化处理 - 根据权重系数对Value进行加权求和
第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样。引入类似SoftMax的计算方式对得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:
第一阶段的计算结果 ai 即为 Valuei 对应的权重系数,然后进行加权求和即可得到Attention数值:
Self-Attention
自注意力机制在文本中的应用,主要是通过计算单词间的互相影响,来解决长距离依赖问题。
1、self-attention将embedding向量(512维)与三个随机初始化的WQ、WK、WV嵌入向量对应(64×512,其值在BP的过程中会一直进行更新)相乘得到Query、Key、Value(均为64维)。
2、针对Thinking这个词,计算出其他词对于该词的一个分数值。该分数值决定了当我们在Thinking位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点乘,以下图为例,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2。
3、把点成的结果除以一个常数。这里我们除以8,这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算,得到的结果即是每个词对于当前位置的词的相关性大小。
4、把Value和softmax得到的值进行相乘,并相加,得到的结果即是self-attetion在当前节点的值
在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,然后把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵
这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention。(dk向量维度,根号dk排除向量维度的影响)
下边是attention后两个句子中it与上下文单词的关系热点图,很容易看出来第一个图片中的it与animal关系很强,第二个图it与street关系很强。这个结果说明注意力机制是可以很好地学习到上下文的语言信息。
Multi-Head Attention
transformer中使用多头注意力机制,将QKV拆分做自注意力计算,然后拼接。
Add (残差连接)& Norm(层归一化)
其中 X表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和 FeedForward(X) 表示输出 (输出与输入 X 维度是一样的,所以可以相加)。
Add指 X+MultiHeadAttention(X),是一种残差连接,通常用于解决多层网络训练的问题,可以让网络只关注当前差异的部分,在 ResNet 中经常用到:
Norm指 Layer Normalization,通常用于 RNN 结构,Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的,这样可以加快收敛。
Feed Forward (两层全连接层)
掩码张量
只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量
在transformer中, 掩码张量的主要作用在应用attention,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,需要进行遮掩
原理实现
>>> atten_data=torch.tensor([[4,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16,17,18,19,20]])
>>> data
tensor([[ 4, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20]])
>>> mask=np.triu([[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]],k=1)
>>> mask=1-mask
>>> mask
array([[1, 0, 0, 0, 0],
[1, 1, 0, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 1, 0]])
>>> data=data.masked_fill(mask==0,-1e9)
>>> data
tensor([[ 4, -1000000000, -1000000000, -1000000000, -1000000000],
[ 6, 7, -1000000000, -1000000000, -1000000000],
[ 11, 12, 13, -1000000000, -1000000000],
[ 16, 17, 18, 19, -1000000000]])
transformer中
多层Transformer
在实际使用种常常使用多层transformer结构(原论文6层)
在多层Transformer中,多层编码器先对输入序列进行编码,然后得到最后一个Encoder的输出Memory;解码器先通过Masked Multi-Head Attention对输入序列进行编码,然后将输出结果同Memory通过Encoder-Decoder Attention后得到第1层解码器的输出;接着再将第1层Decoder的输出通过Masked Multi-Head Attention进行编码,接着将编码后的结果同Memory通过Encoder-Decoder Attention后得到第2层解码器的输出,以此类推得到最后一个Decoder的输出。
值得注意的是,在多层Transformer的解码过程中,每一个Decoder在Encoder-Decoder Attention中所使用的Memory均是同一个。