Transformer简明教程, 从理论到代码实现到项目实战, NLP进阶必知必会
项目地址: https://github.com/lansinuote/Transformer_Example
这个应该是我见过讲过最透彻的:如何最简单、通俗地理解Transformer? - 木羽Cheney的回答 - 知乎
This post is all you need(上卷)——层层剥开Transformer
如何最简单、通俗地理解Transformer? - 鱼先生的回答 - 知乎
文章目录
总体架构
self-attention:自注意力层
计算注意力
通过Embedding将a和b转化成向量(可以理解为one hot向量,但在实际项目中基本上都是word to vector的连续值的向量)
x1和x2分别与WQ,WK,WV相乘得到Querier,keys,Values
dk跟词向量编码的维度有关
自注意力计算的矩阵形式
计算机运行效率更高
单头注意力对比多头注意力
只有一组qkv的就叫单头注意力
多组qkv的就是多头注意力
多头注意力计算完整过程
8组qkv,得到8组自注意力层的结果
Z0到Z7左右拼接在一起(形成一个两行多列的矩阵),然后与Wo相乘得到Z
词向量编码
前置知识:RNN(循环神经网络)
史上最详细循环神经网络讲解(RNN/LSTM/GRU)
RNN对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,利用了RNN的这种能力,使深度学习模型在解决语音识别、语言模型、机器翻译以及时序分析等NLP领域的问题时有所突破。
在常规的全连接神经网络模型中,无法做到结合上下文去训练模型,于是就有了循环神经网络
X是一个向量,也就是某个字或词的特征向量,作为输入层
U是输入层到隐藏层的参数矩阵
S是隐藏层的向量
V是隐藏层到输出层的参数矩阵
O是输出层的向量
x->u->s->v->o就相当于一个全连接神经网络
句子:I love you
X
t
−
1
X_{t-1}
Xt−1代表I这个单词向量
X
t
X_{t}
Xt代表love这个单词向量
X
t
+
1
X_{t+1}
Xt+1代表you这个单词向量
W其实是每个时间点之间的权重矩阵
RNN之所以可以解决序列问题,是因为它可以记住每一时刻的信息,每一时刻的隐藏层不仅由该时刻的输入层决定,还由上一时刻的隐藏层决定。
因此不同的输入序列顺序会导致不同的结果
注:在整个训练过程中,每一时刻所用的都是同样的W
前置知识:LSTM
回到Transformer
因为Transformer不考虑位置信息,所以要引入位置编码,将不同位置的相同数据区分开
普通编码 + 位置编码 = 最终编码
位置编码计算
pos为第几个词(PE矩阵的行数)
i是词向量编码之后,它的第0个向量,第1个向量(PE矩阵的列数)
如果词向量编码是32维度,i是从0到31
位置编码矩阵光谱
越往后的列 波动速度越来越慢
MASK
transformer网络内attention使用的mask
mask (掩码、掩膜)是深度学习中的常见操作。简单而言,其相当于在原始张量上盖上一层掩膜,从而屏蔽或选择一些特定元素,因此常用于构建张量的过滤器。
PAD MASK
句子:a b PAD
< PAD >无任何意义,对短的句子进行补长,只是为了让句子保持同一长度,方便计算
注意力计算是每个词之间的相互相乘的结果
每个词对PAD的注意力是MASK(因为对PAD的注意力无意义,为了避免无用功,所以直接设为MASK),PAD对其他词的注意力是正常计算
上三角MASK
上三角MASK不计算针对未来词的注意
句子:a b c
b和c是a之后预测的一个结果,要根据a预测b和c,因此在计算a的注意力的时候,不能让他看到b这个词
集合两种MASK
完整计算流程
Encoder计算流
thinking和machines经过词向量编码之后就得到x1和x2,与PE矩阵(position encoding)做相加操作,然后计算一个自注意力层(Self-Attention)得到z1和z2,然后再拿z1和x2短接相加(即图片中Add&Normalize的内容),LayerNorm是数据归一化,帮助它的数据保持稳定的计算流,做完这些操作得到z1和z2。然后进行Feed Forward全连接的运算
可能会有n个Encoder,彼此之间是上下串联的关系,最终的结果输出到Decoder当中去运算
Decoder与Encoder的区别在于Encoder-Decoder Attention这一层运算
Self-Attention在算自注意力层的时候:是拿词向量的编码在计算一个qkv的向量
Encoder-Decoder Attention:也是计算自注意力,但是它的qkv不是根据x1和x2来计算的,(注意看图中画线,输入来自Encoder)将Encoder计算完的结果当过k和v,Self-Attention计算出来的结果当q
Decoder也可能有很多串联在一起
Encoder最终计算的结果是k和v。一开始输入应该是一个起始符,然后每个时间步计算出的结果再加入到输入中作为下一个时间步的输入,预测出am,然后把am当作一个输入,再继续预测下一个字
这是训练过程,Decoder输入虽然直接是翻译后的句子,但mask会把后面还没输出的那些词掩盖掉,因此Decoder看不到后面的输出
下面动态图的详细地址
在有监督学习的场景下,编码器负责处理输入样本,而解码器处理与之对应的标签
实验数据介绍
x语言的所有词汇就7个词,说任何话都是用这7个词
采样概率是模仿自然语言中有些词经常被说到(热门词),有些词很难被说到(生僻词)
以概率在词汇表中进行采样,采一个随机长度
transformer的作用是将x翻译成y,也就说y和x之间要有一定的关联性,y可以根据x推导出来的
x和y的关联规则:y当中的每一词都是x当中的词进行逆序之后的结果,小写字母变大写字母,y当中的数字是用9减去x当中的数字得到的
虚线的箭头表明y当中的第一位取决于x的最后一位(9-x),即y当中第一位和第二位是相同的。这样让y的长度比x多一位,且同时增加映射复杂度
为了让数据(句子)长度一致,SOS表示句子开始,EOS为结尾,PAD为补充
数据样例
代码讲解_定义数据
字典的定义:
生成函数
def get_data():
# 定义词集合
words = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'q', 'w', 'e', 'r',
't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k',
'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm'
]
# 定义每个词被选中的概率
p = np.array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
])
p = p / p.sum()
# 随机选n个词
n = random.randint(30, 48) # 句子的长度
x = np.random.choice(words, size=n, replace=True, p=p) # 对数据进行采样
# 采样的结果就是x
x = x.tolist()
# y是对x的变换得到的
# 字母大写,数字取9以内的互补数
def f(i):
i = i.upper()
if not i.isdigit():
return i
i = 9 - int(i)
return str(i)
y = [f(i) for i in x]
# y的第一位双写(多一位)
y = y + [y[-1]]
# 逆序
y = y[::-1]
# 加上首尾符号
x = ['<SOS>'] + x + ['<EOS>']
y = ['<SOS>'] + y + ['<EOS>']
# 补pad到固定长度
x = x + ['<PAD>'] * 50
y = y + ['<PAD>'] * 51
x = x[:50]
y = y[:51]
# 编码成数据
x = [zidian_x[i] for i in x]
y = [zidian_y[i] for i in y]
# 转tensor
x = torch.LongTensor(x)
y = torch.LongTensor(y)
return x, y
定义数据集
# 定义数据集
class Dataset(torch.utils.data.Dataset):
def __init__(self):
super(Dataset, self).__init__()
def __len__(self):
return 100000
def __getitem__(self, i):
return get_data()
# 数据加载器
# 每次调用得到8组x和y
loader = torch.utils.data.DataLoader(dataset=Dataset(),
batch_size=8,
drop_last=True,
shuffle=True,
collate_fn=None)
代码讲解_注意力_位置编码
注意力计算函数
# 注意力计算函数
def attention(Q, K, V, mask):
# b句话,每句话50个词,每个词编码成32维向量,4个头,每个头分到8维向量
# Q,K,V = [b, 4, 50, 8]
# K.permute(0, 1, 3, 2) = [b, 4, 8, 50]
# [b, 4, 50, 8] * [b, 4, 8, 50] -> [b, 4, 50, 50]
# Q,K矩阵相乘,求每个词相对其他所有词的注意力
score = torch.matmul(Q, K.permute(0, 1, 3, 2))
# 除以每个头维数的平方根,做数值缩放
score /= 8 ** 0.5
# mask遮盖,mask是true的地方都被替换成-inf,这样在计算softmax的时候,-inf会被压缩到0
# mask是bool矩阵
# mask = [b, 1, 50, 50]
score = score.masked_fill_(mask, -float('inf'))
# 负无穷经过soft max之后会被压缩成0,被mask遮住的地方注意力为0
score = torch.softmax(score, dim=-1)
# 以注意力分数乘以V,得到最终的注意力结果
# [b, 4, 50, 50] * [b, 4, 50, 8] -> [b, 4, 50, 8]
score = torch.matmul(score, V)
# 每个头计算的结果合一
# permute 函数用于重新排列张量的维度顺序
# score.permute(0, 2, 1, 3) = [b, 50, 4, 8]
# [b, 4, 50, 8] -> [b, 50, 32]
score = score.permute(0, 2, 1, 3).reshape(-1, 50, 32)
return score
关于BN和LN
Batch normalize:用在图像中
layer normalize:用在自然语言处理中
两个矩阵对应两份数据
# 规范化之后,均值是0,标准差是1
# BN是取不同样本做归一化
# LN是取不同通道做归一化
# affine=True,elementwise_affine=True,指定规范化后,再计算一个线性映射
# norm = torch.nn.BatchNorm1d(num_features=4, affine=True)
# print(norm(torch.arange(32, dtype=torch.float32).reshape(2, 4, 4)))
"""
[[[-1.1761, -1.0523, -0.9285, -0.8047],
[-1.1761, -1.0523, -0.9285, -0.8047],
[-1.1761, -1.0523, -0.9285, -0.8047],
[-1.1761, -1.0523, -0.9285, -0.8047]],
[[ 0.8047, 0.9285, 1.0523, 1.1761],
[ 0.8047, 0.9285, 1.0523, 1.1761],
[ 0.8047, 0.9285, 1.0523, 1.1761],
[ 0.8047, 0.9285, 1.0523, 1.1761]]]"""
# norm = torch.nn.LayerNorm(normalized_shape=4, elementwise_affine=True)
# print(norm(torch.arange(32, dtype=torch.float32).reshape(2, 4, 4)))
"""
[[[-1.3416, -0.4472, 0.4472, 1.3416],
[-1.3416, -0.4472, 0.4472, 1.3416],
[-1.3416, -0.4472, 0.4472, 1.3416],
[-1.3416, -0.4472, 0.4472, 1.3416]],
[[-1.3416, -0.4472, 0.4472, 1.3416],
[-1.3416, -0.4472, 0.4472, 1.3416],
[-1.3416, -0.4472, 0.4472, 1.3416],
[-1.3416, -0.4472, 0.4472, 1.3416]]]"""
多头注意力机制
# 多头注意力计算层
class MultiHead(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc_Q = torch.nn.Linear(32, 32)
self.fc_K = torch.nn.Linear(32, 32)
self.fc_V = torch.nn.Linear(32, 32)
self.out_fc = torch.nn.Linear(32, 32)
self.norm = torch.nn.LayerNorm(normalized_shape=32, elementwise_affine=True)
self.dropout = torch.nn.Dropout(p=0.1)
def forward(self, Q, K, V, mask):
# b句话,每句话50个词,每个词编码成32维向量
# Q,K,V = [b, 50, 32]
b = Q.shape[0]
# 保留下原始的Q,后面要做短接用
clone_Q = Q.clone()
# 在原论文中规范化层放在了后面去运算,但经过广泛的实验论证,规范化层放在前面会更好,能帮助模型更好的收敛
# 规范化
Q = self.norm(Q)
K = self.norm(K)
V = self.norm(V)
# 线性运算,维度不变
# [b, 50, 32] -> [b, 50, 32]
K = self.fc_K(K)
V = self.fc_V(V)
Q = self.fc_Q(Q)
# 单头拆分成多个头
# b句话,每句话50个词,每个词编码成32维向量,4个头,每个头分到8维向量
# [b, 50, 32] -> [b, 4, 50, 8]
Q = Q.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
K = K.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
V = V.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
# 计算注意力
# [b, 4, 50, 8] -> [b, 50, 32]
score = attention(Q, K, V, mask)
# 计算输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
# dropout防止过拟合
score = self.dropout(self.out_fc(score))
# 短接
score = clone_Q + score
return score
位置编码
# 位置编码层
class PositionEmbedding(torch.nn.Module):
def __init__(self):
super().__init__()
# 位置编码矩阵元素计算公式
# pos是第几个词,i是第几个维度,d_model是维度总数
def get_pe(pos, i, d_model):
fenmu = 1e4 ** (i / d_model)
pe = pos / fenmu
if i % 2 == 0:
return math.sin(pe)
return math.cos(pe)
# 初始化位置编码矩阵
# Y在计算时会被剪切到最后一个词,所以是51-1=50个词
# 32是每个词都会被编码成32维的向量
pe = torch.empty(50, 32)
for i in range(50):
for j in range(32):
pe[i, j] = get_pe(i, j, 32)
# 在张量 pe 的第0维度上增加一个维度,将其扩展为一个批次(batch)
pe = pe.unsqueeze(0)
# 定义为不更新的常量
self.register_buffer('pe', pe)
# 词编码层
"""
构造函数的参数是 (39, 32),其中 39 表示词汇表的大小,32 表示每个词的嵌入维度。
因此,这个 Embedding 层将39个词汇映射到32维的向量空间中。
在深度学习任务中,Embedding 层通常用于将离散的输入(例如单词或类别的索引)转换为密集的连续值向量
"""
self.embed = torch.nn.Embedding(39, 32)
# 初始化参数,对权重进行正态分布初始化,其中均值为 0,标准差为 0.1。
self.embed.weight.data.normal_(0, 0.1)
def forward(self, x):
# 8句话,每句话50个词,把每个词编码成32维向量
# [8, 50] -> [8, 50, 32]
embed = self.embed(x)
# 词编码embed矩阵和位置编码PE矩阵相加
# [8, 50, 32] + [1, 50, 32] -> [8, 50, 32]
embed = embed + self.pe
return embed
代码讲解_Mask函数
import torch
from data import zidian_x, zidian_y
# pad mask
def mask_pad(data):
# b句话,每句话50个词,这里是还没embed的
# data = [b, 50]
# 判断每个词是不是<PAD>
mask = (data == zidian_x['<PAD>'])
# [b, 50] -> [b, 1, 1, 50]
mask = mask.reshape(-1, 1, 1, 50)
# 在计算注意力时,是计算50个词和50个词相互之间的注意力,所以是个50*50的矩阵
# 是pad的列是true,意味着任何词对pad的注意力都是0
# 但是pad本身对其他词的注意力并不是0
# 所以是pad的行不是true
# 复制n次
# [b, 1, 1, 50] -> [b, 1, 50, 50]
mask = mask.expand(-1, 1, 50, 50)
return mask
# 上三角mask
def mask_tril(data):
# b句话,每句话50个词,这里是还没embed的
# data = [b, 50]
# 50*50的矩阵表示每个词对其他词是否可见
# 上三角矩阵,不包括对角线,意味着,对每个词而言,他只能看到他自己,和他之前的词,而看不到之后的词
# [1, 50, 50]
"""
[[0, 1, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 1, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 0]]"""
tril = 1 - torch.tril(torch.ones(1, 50, 50, dtype=torch.long))
# 判断y当中每个词是不是pad,如果是pad则不可见
# [b, 50]
mask = data == zidian_y['<PAD>']
# 变形+转型,为了之后的计算
# [b, 1, 50]
mask = mask.unsqueeze(1).long()
# mask和tril求并集
# [b, 1, 50] + [1, 50, 50] -> [b, 50, 50]
mask = mask + tril
# 转布尔型
mask = mask > 0
# 转布尔型,增加一个维度,便于后续的计算
mask = (mask == 1).unsqueeze(dim=1)
return mask
代码讲解_定义模型
编码器层
自注意力就是它的qkv三个矩阵是同一个
# 编码器层
class EncoderLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.mh = MultiHead()
self.fc = FullyConnectedOutput()
def forward(self, x, mask):
# 计算自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
score = self.mh(x, x, x, mask)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
out = self.fc(score)
return out
class Encoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = EncoderLayer()
self.layer_2 = EncoderLayer()
self.layer_3 = EncoderLayer()
def forward(self, x, mask):
x = self.layer_1(x, mask)
x = self.layer_2(x, mask)
x = self.layer_3(x, mask)
return x
解码器层
在解码器中第一层多头注意力的mask用的mask_tril(上三角矩阵叠加一个PAD mask),这样在计算y的注意力的时候不会注意到当前词的下一个词,只会注意到当前词和当前词之前的词
第二层多头注意力用的第一层的输出,y = self.mh2(y, x, x, mask_pad_x)为之前讲的decoder多出来的那一层(encoder decoder attention层)
# 解码器层
class DecoderLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.mh1 = MultiHead()
self.mh2 = MultiHead()
self.fc = FullyConnectedOutput()
def forward(self, x, y, mask_pad_x, mask_tril_y):
# 先计算y的自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
# 次数的mask为tril mask,这样在计算y的注意力的时候不会注意到当前词的下一个词,只会注意到当前词和当前词之前的词
y = self.mh1(y, y, y, mask_tril_y)
# 结合x和y的注意力计算,维度不变
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.mh2(y, x, x, mask_pad_x)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
y = self.fc(y)
return y
class Decoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = DecoderLayer()
self.layer_2 = DecoderLayer()
self.layer_3 = DecoderLayer()
def forward(self, x, y, mask_pad_x, mask_tril_y):
y = self.layer_1(x, y, mask_pad_x, mask_tril_y)
y = self.layer_2(x, y, mask_pad_x, mask_tril_y)
y = self.layer_3(x, y, mask_pad_x, mask_tril_y)
return y
主模型
# 主模型
import torch
from mask import mask_pad, mask_tril
from util import MultiHead, PositionEmbedding, FullyConnectedOutput
# 编码器层
class EncoderLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.mh = MultiHead()
self.fc = FullyConnectedOutput()
def forward(self, x, mask):
# 计算自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
score = self.mh(x, x, x, mask)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
out = self.fc(score)
return out
class Encoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = EncoderLayer()
self.layer_2 = EncoderLayer()
self.layer_3 = EncoderLayer()
def forward(self, x, mask):
x = self.layer_1(x, mask)
x = self.layer_2(x, mask)
x = self.layer_3(x, mask)
return x
# 解码器层
class DecoderLayer(torch.nn.Module):
def __init__(self):
super().__init__()
self.mh1 = MultiHead()
self.mh2 = MultiHead()
self.fc = FullyConnectedOutput()
def forward(self, x, y, mask_pad_x, mask_tril_y):
# 先计算y的自注意力,维度不变
# [b, 50, 32] -> [b, 50, 32]
y = self.mh1(y, y, y, mask_tril_y)
# 结合x和y的注意力计算,维度不变
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.mh2(y, x, x, mask_pad_x)
# 全连接输出,维度不变
# [b, 50, 32] -> [b, 50, 32]
y = self.fc(y)
return y
class Decoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = DecoderLayer()
self.layer_2 = DecoderLayer()
self.layer_3 = DecoderLayer()
def forward(self, x, y, mask_pad_x, mask_tril_y):
y = self.layer_1(x, y, mask_pad_x, mask_tril_y)
y = self.layer_2(x, y, mask_pad_x, mask_tril_y)
y = self.layer_3(x, y, mask_pad_x, mask_tril_y)
return y
# 主模型
class Transformer(torch.nn.Module):
def __init__(self):
super().__init__()
self.embed_x = PositionEmbedding()
self.embed_y = PositionEmbedding()
self.encoder = Encoder()
self.decoder = Decoder()
self.fc_out = torch.nn.Linear(32, 39)
def forward(self, x, y):
# 拿x做编码层计算,拿y做解码层计算
# [b, 1, 50, 50]
mask_pad_x = mask_pad(x)
mask_tril_y = mask_tril(y)
# 编码,添加位置信息
# x = [b, 50] -> [b, 50, 32]
# y = [b, 50] -> [b, 50, 32]
x, y = self.embed_x(x), self.embed_y(y)
# 编码层计算
# [b, 50, 32] -> [b, 50, 32]
x = self.encoder(x, mask_pad_x)
# 解码层计算
# [b, 50, 32],[b, 50, 32] -> [b, 50, 32]
y = self.decoder(x, y, mask_pad_x, mask_tril_y)
# 全连接输出,维度发生改变
# [b, 50, 32] -> [b, 50, 39]
y = self.fc_out(y)
return y
代码讲解
import torch
from data import zidian_y, loader, zidian_xr, zidian_yr
from mask import mask_pad, mask_tril
from model import Transformer
# 预测函数,根据x推算出y
def predict(x):
# x = [1, 50]
model.eval()
# [1, 1, 50, 50]
mask_pad_x = mask_pad(x)
# 初始化输出,这个是固定值
# [1, 50]
# [[0,2,2,2...]],第一位是SOS,后面补充PAD
target = [zidian_y['<SOS>']] + [zidian_y['<PAD>']] * 49
target = torch.LongTensor(target).unsqueeze(0)
# x编码,添加位置信息
# [1, 50] -> [1, 50, 32]
x = model.embed_x(x)
# 编码层计算,维度不变
# [1, 50, 32] -> [1, 50, 32]
x = model.encoder(x, mask_pad_x)
# 遍历生成第1个词到第49个词
# 根据x里面编码的信息,将y当中1~49个词给它预测出来
for i in range(49):
# [1, 50]
y = target
# [1, 1, 50, 50]
mask_tril_y = mask_tril(y)
# y编码,添加位置信息
# [1, 50] -> [1, 50, 32]
y = model.embed_y(y)
# 解码层计算,维度不变
# [1, 50, 32],[1, 50, 32] -> [1, 50, 32]
y = model.decoder(x, y, mask_pad_x, mask_tril_y)
# 全连接输出,39分类
# [1, 50, 32] -> [1, 50, 39]
out = model.fc_out(y)
# 取出当前词的输出
# [1, 50, 39] -> [1, 39]
out = out[:, i, :]
# 取出分类结果
# [1, 39] -> [1]
out = out.argmax(dim=1).detach()
# 以当前词预测下一个词,填到结果中
target[:, i + 1] = out
return target
model = Transformer()
loss_func = torch.nn.CrossEntropyLoss()
optim = torch.optim.Adam(model.parameters(), lr=2e-3) # 优化器
# 每调用3次lr会折半
sched = torch.optim.lr_scheduler.StepLR(optim, step_size=3, gamma=0.5)
for epoch in range(1):
for i, (x, y) in enumerate(loader):
# x = [8, 50]
# y = [8, 51]
# y比x多一位
# 在训练时,是拿y的每一个字符输入,预测下一个字符,所以不需要最后一个字
# [8, 50, 39]
pred = model(x, y[:, :-1])
# [8, 50, 39] -> [400, 39],为了方便计算loss
pred = pred.reshape(-1, 39)
# 模型是拿y的每一个字符去预测它的下一个字符,所以在预测过程中并没有预测y的第0个字符
# [8, 51] -> [400]
y = y[:, 1:].reshape(-1)
# EOS之后都是PAD,对PAD的预测没有意义
# 忽略pad
select = y != zidian_y['<PAD>']
pred = pred[select]
y = y[select]
# 梯度下降
loss = loss_func(pred, y)
optim.zero_grad()
loss.backward()
optim.step()
if i % 200 == 0:
# [select, 39] -> [select]
pred = pred.argmax(1)
correct = (pred == y).sum().item()
accuracy = correct / len(pred)
lr = optim.param_groups[0]['lr']
print(epoch, i, lr, loss.item(), accuracy)
sched.step()
# 测试
for i, (x, y) in enumerate(loader):
break
for i in range(8):
print(i)
print(''.join([zidian_xr[i] for i in x[i].tolist()]))
print(''.join([zidian_yr[i] for i in y[i].tolist()]))
print(''.join([zidian_yr[i] for i in predict(x[i].unsqueeze(0))[0].tolist()]))
Transformer代码(版本:DASOU讲AI)
## from https://github.com/graykode/nlp-tutorial/tree/master/5-1.Transformer
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import math
def make_batch(sentences):
input_batch = [[src_vocab[n] for n in sentences[0].split()]]
output_batch = [[tgt_vocab[n] for n in sentences[1].split()]]
target_batch = [[tgt_vocab[n] for n in sentences[2].split()]]
return torch.LongTensor(input_batch), torch.LongTensor(output_batch), torch.LongTensor(target_batch)
## 10
def get_attn_subsequent_mask(seq):
"""
seq: [batch_size, tgt_len]
"""
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
# attn_shape: [batch_size, tgt_len, tgt_len]
subsequence_mask = np.triu(np.ones(attn_shape), k=1) # 生成一个上三角矩阵
subsequence_mask = torch.from_numpy(subsequence_mask).byte()
return subsequence_mask # [batch_size, tgt_len, tgt_len]
## 7. ScaledDotProductAttention
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super(ScaledDotProductAttention, self).__init__()
def forward(self, Q, K, V, attn_mask):
## 输入进来的维度分别是 [batch_size x n_heads x len_q x d_k] K: [batch_size x n_heads x len_k x d_k] V: [batch_size x n_heads x len_k x d_v]
##首先经过matmul函数得到的scores形状是 : [batch_size x n_heads x len_q x len_k]
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
## 然后关键词地方来了,下面这个就是用到了我们之前重点讲的attn_mask,把被mask的地方置为无限小,softmax之后基本就是0,对q的单词不起作用
scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is one.
attn = nn.Softmax(dim=-1)(scores)
context = torch.matmul(attn, V)
return context, attn
## 6. MultiHeadAttention
class MultiHeadAttention(nn.Module):
def __init__(self):
super(MultiHeadAttention, self).__init__()
## 输入进来的QKV是相等的,我们会使用映射linear做一个映射得到参数矩阵Wq, Wk,Wv
self.W_Q = nn.Linear(d_model, d_k * n_heads)
self.W_K = nn.Linear(d_model, d_k * n_heads)
self.W_V = nn.Linear(d_model, d_v * n_heads)
self.linear = nn.Linear(n_heads * d_v, d_model)
self.layer_norm = nn.LayerNorm(d_model)
def forward(self, Q, K, V, attn_mask):
## 这个多头分为这几个步骤,首先映射分头,然后计算atten_scores,然后计算atten_value;
##输入进来的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model], V: [batch_size x len_k x d_model]
residual, batch_size = Q, Q.size(0)
# (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)
##下面这个就是先映射,后分头;一定要注意的是q和k分头之后维度是一致额,所以一看这里都是dk
q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2) # q_s: [batch_size x n_heads x len_q x d_k]
k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2) # k_s: [batch_size x n_heads x len_k x d_k]
v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2) # v_s: [batch_size x n_heads x len_k x d_v]
## 输入进行的attn_mask形状是 batch_size x len_q x len_k,然后经过下面这个代码得到 新的attn_mask : [batch_size x n_heads x len_q x len_k],就是把pad信息重复了n个头上
attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
##然后我们计算 ScaledDotProductAttention 这个函数,去7.看一下
## 得到的结果有两个:context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q x len_k]
context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v]
output = self.linear(context)
return self.layer_norm(output + residual), attn # output: [batch_size x len_q x d_model]
## 8. PoswiseFeedForwardNet
class PoswiseFeedForwardNet(nn.Module):
def __init__(self):
super(PoswiseFeedForwardNet, self).__init__()
self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)
self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)
self.layer_norm = nn.LayerNorm(d_model)
def forward(self, inputs):
residual = inputs # inputs : [batch_size, len_q, d_model]
output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))
output = self.conv2(output).transpose(1, 2)
return self.layer_norm(output + residual)
## 4. get_attn_pad_mask
## 比如说,我现在的句子长度是5,在后面注意力机制的部分,我们在计算出来QK转置除以根号之后,softmax之前,我们得到的形状
## len_input * len*input 代表每个单词对其余包含自己的单词的影响力
## 所以这里我需要有一个同等大小形状的矩阵,告诉我哪个位置是PAD部分,之后在计算计算softmax之前会把这里置为无穷大;
## 一定需要注意的是这里得到的矩阵形状是batch_size x len_q x len_k,我们是对k中的pad符号进行标识,并没有对k中的做标识,因为没必要
## seq_q 和 seq_k 不一定一致,在交互注意力,q来自解码端,k来自编码端,所以告诉模型编码这边pad符号信息就可以,解码端的pad信息在交互注意力层是没有用到的;
def get_attn_pad_mask(seq_q, seq_k):
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# eq(zero) is PAD token
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # batch_size x 1 x len_k, one is masking
return pad_attn_mask.expand(batch_size, len_q, len_k) # batch_size x len_q x len_k
## 3. PositionalEncoding 代码实现
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
## 位置编码的实现其实很简单,直接对照着公式去敲代码就可以,下面这个代码只是其中一种实现方式;
## 从理解来讲,需要注意的就是偶数和奇数在公式上有一个共同部分,我们使用log函数把次方拿下来,方便计算;
## pos代表的是单词在句子中的索引,这点需要注意;比如max_len是128个,那么索引就是从0,1,2,...,127
##假设我的demodel是512,2i那个符号中i从0取到了255,那么2i对应取值就是0,2,4...510
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)## 这里需要注意的是pe[:, 0::2]这个用法,就是从0开始到最后面,补长为2,其实代表的就是偶数位置
pe[:, 1::2] = torch.cos(position * div_term)##这里需要注意的是pe[:, 1::2]这个用法,就是从1开始到最后面,补长为2,其实代表的就是奇数位置
## 上面代码获取之后得到的pe:[max_len*d_model]
## 下面这个代码之后,我们得到的pe形状是:[max_len*1*d_model]
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe) ## 定一个缓冲区,其实简单理解为这个参数不更新就可以
def forward(self, x):
"""
x: [seq_len, batch_size, d_model]
"""
x = x + self.pe[:x.size(0), :]
return self.dropout(x)
## 5. EncoderLayer :包含两个部分,多头注意力机制和前馈神经网络
class EncoderLayer(nn.Module):
def __init__(self):
super(EncoderLayer, self).__init__()
self.enc_self_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()
def forward(self, enc_inputs, enc_self_attn_mask):
## 下面这个就是做自注意力层,输入是enc_inputs,形状是[batch_size x seq_len_q x d_model] 需要注意的是最初始的QKV矩阵是等同于这个输入的,去看一下enc_self_attn函数 6.
enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask) # enc_inputs to same Q,K,V
enc_outputs = self.pos_ffn(enc_outputs) # enc_outputs: [batch_size x len_q x d_model]
return enc_outputs, attn
## 2. Encoder 部分包含三个部分:词向量embedding,位置编码部分,注意力层及后续的前馈神经网络
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
self.src_emb = nn.Embedding(src_vocab_size, d_model) ## 这个其实就是去定义生成一个矩阵,大小是 src_vocab_size * d_model
self.pos_emb = PositionalEncoding(d_model) ## 位置编码情况,这里是固定的正余弦函数,也可以使用类似词向量的nn.Embedding获得一个可以更新学习的位置编码
self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)]) ## 使用ModuleList对多个encoder进行堆叠,因为后续的encoder并没有使用词向量和位置编码,所以抽离出来;
def forward(self, enc_inputs):
## 这里我们的 enc_inputs 形状是: [batch_size x source_len]
## 下面这个代码通过src_emb,进行索引定位,enc_outputs输出形状是[batch_size, src_len, d_model]
enc_outputs = self.src_emb(enc_inputs)
## 这里就是位置编码,把两者相加放入到了这个函数里面,从这里可以去看一下位置编码函数的实现;3.
enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)
##get_attn_pad_mask是为了得到句子中pad的位置信息,给到模型后面,在计算自注意力和交互注意力的时候去掉pad符号的影响,去看一下这个函数 4.
enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
enc_self_attns = []
for layer in self.layers:
## 去看EncoderLayer 层函数 5.
enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
enc_self_attns.append(enc_self_attn)
return enc_outputs, enc_self_attns
## 10.
class DecoderLayer(nn.Module):
def __init__(self):
super(DecoderLayer, self).__init__()
self.dec_self_attn = MultiHeadAttention()
self.dec_enc_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()
def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):
dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask)
dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask)
dec_outputs = self.pos_ffn(dec_outputs)
return dec_outputs, dec_self_attn, dec_enc_attn
## 9. Decoder
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
self.pos_emb = PositionalEncoding(d_model)
self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])
def forward(self, dec_inputs, enc_inputs, enc_outputs): # dec_inputs : [batch_size x target_len]
dec_outputs = self.tgt_emb(dec_inputs) # [batch_size, tgt_len, d_model]
dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1) # [batch_size, tgt_len, d_model]
## get_attn_pad_mask 自注意力层的时候的pad 部分
dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)
## get_attn_subsequent_mask 这个做的是自注意层的mask部分,就是当前单词之后看不到,使用一个上三角为1的矩阵
dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)
## 两个矩阵相加,大于0的为1,不大于0的为0,为1的在之后就会被fill到无限小
dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequent_mask), 0)
## 这个做的是交互注意力机制中的mask矩阵,enc的输入是k,我去看这个k里面哪些是pad符号,给到后面的模型;注意哦,我q肯定也是有pad符号,但是这里我不在意的,之前说了好多次了哈
dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)
dec_self_attns, dec_enc_attns = [], []
for layer in self.layers:
dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask)
dec_self_attns.append(dec_self_attn)
dec_enc_attns.append(dec_enc_attn)
return dec_outputs, dec_self_attns, dec_enc_attns
## 1. 从整体网路结构来看,分为三个部分:编码层,解码层,输出层
class Transformer(nn.Module):
def __init__(self):
super(Transformer, self).__init__()
self.encoder = Encoder() ## 编码层
self.decoder = Decoder() ## 解码层
self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False) ## 输出层 d_model 是我们解码层每个token输出的维度大小,之后会做一个 tgt_vocab_size 大小的softmax
def forward(self, enc_inputs, dec_inputs):
## 这里有两个数据进行输入,一个是enc_inputs 形状为[batch_size, src_len],主要是作为编码段的输入,一个dec_inputs,形状为[batch_size, tgt_len],主要是作为解码端的输入
## enc_inputs作为输入 形状为[batch_size, src_len],输出由自己的函数内部指定,想要什么指定输出什么,可以是全部tokens的输出,可以是特定每一层的输出;也可以是中间某些参数的输出;
## enc_outputs就是主要的输出,enc_self_attns这里没记错的是QK转置相乘之后softmax之后的矩阵值,代表的是每个单词和其他单词相关性;
enc_outputs, enc_self_attns = self.encoder(enc_inputs)
## dec_outputs 是decoder主要输出,用于后续的linear映射; dec_self_attns类比于enc_self_attns 是查看每个单词对decoder中输入的其余单词的相关性;dec_enc_attns是decoder中每个单词对encoder中每个单词的相关性;
dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)
## dec_outputs做映射到词表大小
dec_logits = self.projection(dec_outputs) # dec_logits : [batch_size x src_vocab_size x tgt_vocab_size]
return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns
if __name__ == '__main__':
## 句子的输入部分,分别是: 编码端输入,解码端输入,解码端真实标签
# S表示开始标志,E表示结束标志,P表示填充字符
sentences = ['ich mochte ein bier P', 'S i want a beer', 'i want a beer E']
# Transformer Parameters
# Padding Should be Zero
## 构建词表
src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4}
src_vocab_size = len(src_vocab)
tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}
tgt_vocab_size = len(tgt_vocab)
src_len = 5 # length of source
tgt_len = 5 # length of target
## 模型参数
d_model = 512 # Embedding Size
d_ff = 2048 # FeedForward dimension
d_k = d_v = 64 # dimension of K(=Q), V
n_layers = 6 # number of Encoder of Decoder Layer
n_heads = 8 # number of heads in Multi-Head Attention
model = Transformer()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
enc_inputs, dec_inputs, target_batch = make_batch(sentences)
for epoch in range(20):
optimizer.zero_grad()
outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
loss = criterion(outputs, target_batch.contiguous().view(-1))
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()
代码讲解_一个更加复杂的翻译任务
把加法看作一个翻译任务
X = 数据1 a 数据2
Y = 数据1+数据2
一些常见问题细节分析
1.对注意力公式的理解:
Q:即query,可以理解为某个单词像其它单词发出询问
K:即Key,可以理解为某个单词回答其它单词的提问
V:即Value,可以理解为某个单词的实际值,表示根据两个词之间的亲密关系,决定提取出多少信息出来融入到自身
Q
K
T
QK^T
QKT表达了词与词之间关系的紧密程度
经过Softmax处理后,第一行变成了某种分数形式,例如:0.4,0.2,0.2,0.1,那么这意味着将用第一个词的40%的信息、第二个词的20%的信息…来构建目标词的新表示
为什么除以$sqrt(d_k),因为q1乘k1的值很大,会导致softmax在反向传播时梯度很小(softmax在输入较大时的函数变化平缓,导数很小),会造成梯度消失,而除以dk是为了保证方差为1
2.训练和推理的区别
训练:
训练时向解码器输入整个目标序列的方法被称为 Teacher Forcing。
如果训练和预测采取一样的方式,一旦中间有个词预测错误,会导致后面的词预测都出现偏差。因此训练时直接向解码器提供目标序列,实际上是给了一个提示,即便这个词预测错误,但因为有标准答案(完整的目标序列),也可以用正确的词来预测下一个词,避免了 错误的持续累加。而且这种机制保证了 Transformer 在训练阶段并行地输出所有的词,而不需要循环(推理时需要不断循环输入),这大大加快了训练速度。
推理:
在第一个时间步,使用一个只有句首符号的空序列来代替训练过程中使用的目标序列。空序列转换为嵌入带有位置编码的嵌入(start_position_embed),并被送入解码器。
总结:在训练时,解码器输入的是目标序列(需要mask处理),而在推理时,一开始输入的是只有句首符号的空序列(代替训练过程中使用的目标序列),将预测的新词附加到解码器序列中。重复这个步骤,直到它预测出一个句末标记
3.在只有单词向量的情况下,如何获取QKV
x1矩阵乘
W
Q
W^Q
WQ得到q1矩阵,而
W
Q
W^Q
WQ矩阵是随机生成(初始化),然后反向传播的时候更新
为什么用Layer Normalization,而不用Batch Normal
BN在nlp的效果差,所以不用
一个batch的样本相同位置往往没有联系,而单个样本中有联系
针对整个batch中的样本同一维度特征处理
每一列是不同样本
每一行是一个特征(例如:第一行代表体重,第二行代表身高)
BN是对整个样本中的同一特征/维度(例如:所有人的体重)做处理
BN的优点:可以解决内部协变量偏移(玄学),缓解了梯度饱和问题(如果使用sigmoid激活函数的话),加快收敛
BN的缺点:1.batch_size较小的时候,效果差(用整个batch中样本的均值和方差来模拟全部数据的均值和方差)
2.BN在RNN中效果比较差,RNN的输入是动态的,所以不能有效得到整个batchsize的均值和方差。例子:前九个句子都有5个单词,第10个句子有20个单词,那么第6到第20个单词的均值和方差只能由第10个句子来决定(回到缺点1)
为什么BN可以用于cv,因为每个图像的格式都是一样的,不会有这种问题
为什么用Layer-norm:针对的同一个样本的所有单词做缩放
以上图为例子:第10个句子有20个单词,LN就是对这20个单词做缩放,求均值和方差
BN是对所有句子的第一个单词做均值和方差,然后对第二个单词做均值和方差。。。
在BN中,因为对“我”和“今”做了均值方差,因此BN认为“我”和“今”有相同的语义信息,但实际上“我”和“今”没有什么语音信息,但实际上应该是“今天天气真不错”这句话中每个单词之间存在语义信息
为什么需要mask
简单说就是:如果没有mask,训练和验证存在gap,训练时按照答案训练,而验证只能拿着之前的得到的结果(而非标准答案),导致模型效果不好(可以理解练习册的答案在学生自己手里,但做练习册的时候不能照抄答案,否则正规考试还是不会)
Encoder和Decoder交互
Encoders输出的值与每个Decoder都做交互
Encoder生成的是k,v矩阵,Decoder生成的q矩阵,可以理解为:decoder中每个q(query)去询问每个encoder输出的量,并与之结合
此处可以叫做Cross Attention:
与RNN相比,为什么Transformer效果更好
总结有两点:1.相距较远单词的相关性 2.并行处理
对于长句中相距较远的单词,其间的长距离依赖关系是一个挑战。
RNNs 每个时间步值处理输入序列的一个词。这意味着在完成时间步 T-1 计算之前,它无法进行时间步骤 T 的计算。(即无法进行并行计算)这降低了训练和推理速度。
Transformer可以并行地处理序列中的所有单词,从而大大加快了计算速度。(只是在训练阶段与推理阶段的 Encoder 中吧。推理过程的 Decoder 也不是并行的)。
输入序列中单词之间的距离并不重要。Transformer 同样擅长计算相邻词和相距较远的词之间的依赖关系。
Transformer相关面试题
史上最全Transformer面试题系列(一):灵魂20问帮你彻底搞定Transformer-干货!
多头注意力实现部分
# 单头拆分成多个头
# b句话,每句话50个词,每个词编码成32维向量,4个头,每个头分到8维向量
# [b, 50, 32] -> [b, 4, 50, 8]
Q = Q.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
K = K.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
V = V.reshape(b, 50, 4, 8).permute(0, 2, 1, 3)
# 计算注意力
# [b, 4, 50, 8] -> [b, 50, 32]
score = attention(Q, K, V, mask)