一、整体框架
整体框架可以理解为输入、编码、解码、输出
二、自注意力机制
自注意力机制实际上是想让机器注意到整个输入中不同部分之间的相关性
计算输入词汇的自注意力:将输入的词汇转换为向量,通常为连续值向量,再将向量分别乘三个矩阵WQ、WK、WV,分别得到三个向量q、k、v;具体如下图所示。
Z1 = 0.88V1 + 0.12V2
前馈神经网络的输入是self-attention的输出,即Z
其中用矩阵计算效率更高
以上为单头自注意力(只有一组q、k、v向量),多头自注意力为多组q、k、v。
注意力机制代码如下:
def attention(Q, K, V, mask):
# b句话,每句话50个词,每个词编码成32维向量,4个头,每个头分到8维向量
# Q,K,V = [b, 4, 50, 8]
# [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 = [b, 1, 50, 50]
score = score.masked_fill_(mask, -float('inf'))
score = torch.softmax(score, dim=-1)
# 以注意力分数乘以V,得到最终的注意力结果
# [b, 4, 50, 50] * [b, 4, 50, 8] -> [b, 4, 50, 8]
score = torch.matmul(score, V)
# 每个头计算的结果合一
# [b, 4, 50, 8] -> [b, 50, 32]
score = score.permute(0, 2, 1, 3).reshape(-1, 50, 32)
return score
多头注意力计算层:
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)
# 规范化之后,均值是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)))
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]
score = self.dropout(self.out_fc(score))
# 短接
score = clone_Q + score
return score
三、词向量编码和Mask
Transformer会在数据中加入位置信息,以便让不同位置的相同数据有所区分。
输入的词汇编码会有一个基本的编码(embedding)和一个位置编码(positional embedding),最终的编码是两者相加,实现区分。
位置编码矩阵的计算公式如下:
pos:词位置,(0,1,2...)
i:编码位置,(0,1,2...)
位置编码层代码如下:
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)
# 初始化位置编码矩阵
pe = torch.empty(50, 32)
for i in range(50):
for j in range(32):
pe[i, j] = get_pe(i, j, 32)
pe = pe.unsqueeze(0)
# 定义为不更新的常量
self.register_buffer('pe', pe)
# 词编码层
self.embed = torch.nn.Embedding(39, 32)
# 初始化参数
self.embed.weight.data.normal_(0, 0.1)
def forward(self, x):
# [8, 50] -> [8, 50, 32]
embed = self.embed(x)
# 词编码和位置编码相加
# [8, 50, 32] + [1, 50, 32] -> [8, 50, 32]
embed = embed + self.pe
return embed
对短的句子进行补长
1、PAD MASK:不计算针对PAD的注意力(每个词对PAD的注意力为mask,PAD对每个词的注意力不做处理)
2、上三角MASK:不计算针对“未来词”的注意力。
3、整合两种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
def mask_tril(data):
# b句话,每句话50个词,这里是还没embed的
# data = [b, 50]
# 50*50的矩阵表示每个词对其他词是否可见
# 上三角矩阵,不包括对角线,意味着,对每个词而言,他只能看到他自己,和他之前的词,而看不到之后的词
# [1, 50, 50]
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
四、transformer完整架构
# 预测函数
def predict(x):
# x = [1, 50]
model.eval()
# [1, 1, 50, 50]
mask_pad_x = mask_pad(x)
# 初始化输出,这个是固定值
# [1, 50]
# [[0,2,2,2...]]
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个词
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
整体框架看懂了还是好理解。