文章目录
0.声明
本人不做NLP,看这个只是为了看懂vit,故不会很完善,且仅个人的学习笔记,代码大量复制粘贴于哈佛关于transformer的技术解析。
这里的工作在于加入了很多我的理解,方便我日后忘掉时再看。
1.整体结构
2 拆解
整个结构可以拆解成如图所示的四个部分
3.embedding
#%%
# 文本嵌入层
# 输入是由若干个句子经词汇映射得到的一个二维数字张量
# 下面是对 “ 由句子经词汇映射得到的一个一维数字张量”的一个演示,数字表示在一行的第几列为1,其余数字为0
# (乱写的对应关系,看懂意思就行)(每个数字表示该token在英语词表的位置)“I am the forever chosen” --> [0,23, 51, 69, 5666]
# (乱写的对应关系,看懂意思就行)(每个数字表示该token在中文词表的位置)"我 即 永世 神选" --> [62 , 32, 5896, 2663]
# 作用是将每一个句子对应的一维数字张量中的每一个数字转为一个高维张量
import torch
import torch.nn as nn
import math
class Embeddings(nn.Module):
def __init__(self,d_model,vocab):
# vocab:词表大小
# d_model:词嵌入维度
super(Embeddings,self).__init__()
self.d_model=d_model
self.vocab=vocab
self.lut=nn.Embedding(vocab, d_model)
def forward(self,x):
# x:句子经词汇映射得到的数字张量
# x:[batch_size, sequence_length],batch_size是每批样本的数量,sequence_length是序列的长度
# 可以认为batch_size表示句子数量,sequence_len是这个句子中的单词数量
# 输出:[batch_size, sequence_length, d_model]
return self.lut(x)*math.sqrt(self.d_model)
#%%
# 位置编码器
# 单词所在的位置对语义的影响不同
# 所以需要提供位置信息
# 作用:将词汇因位置不同而产生的不同的语义信息加入到词嵌入张量中,来弥补位置信息的缺失
import math
from torch.autograd import Variable
class PositionalEmbedding(nn.Module):
def __init__(self,dropout,d_model,max_len=5000):
# dropout:张量中每一个元素的置零概率
# max_len:一个句子最大含词量
# d_model:要映射到d_model个维度
super(PositionalEmbedding).__init__()
self.dropout=nn.Dropout(p=dropout)
# 位置矩阵初始化
pe=torch.zeros(max_len,d_model)
# position,shape: max_len*1
position=torch.arange(0,max_len).unsqueeze(1)
# 这个常数用来缩小位置编码,便于梯度下降时更好的“进化”
constant=-math.log(10000)/d_model
# div_term,shape 1*d_model/2,中转
div_term=torch.exp(torch.arange(0,d_model,2)* constant)
pe[:,0::2]=torch.sin(position * div_term)
pe[:,1::2]=torch.cos(position * div_term)
pe=pe.unsqueeze(0)
# 将pe注册为一个buffer,部参与更新但是保存和加载的时候都要用上它
self.register_buffer('pe',pe)
def forward(self,x):
# x: [batch_size, sequence_length, d_model]
# batch_size是每批样本的数量,sequence_length是序列的长度,d_model是embedding向量的维度
# 可以认为batch_size表示一个句子,sequence_len是这个句子中的单词数量
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
return self.dropout(x)
4. encoder
4.1 attention机制
# 形象化解释
# Q:一组查询语句
# V:数据库
# K:可以理解为每个数据项对应的特征、主题、主体,所以K、V数量一定一样,而维度不一定相同
# 注意力机制的输出结果是对问题的回答,其逻辑是先看看每个数据项的主题是不是和问题高度相关,相关的看多一些,不相关的看少些
#实现细节:
# Q、K、V三者的形状都是[batch_size, sequence_length, d_model](单头时,多头最后一个为d_model/m)
# 查询(Q):通过一个线性变换,将输入的查询序列转换为一个查询向量。这个查询向量代表了模型想要关注的特定信息或特征。
# 键(K)和值(V):通过两个不同的线性变换,将输入的键序列和值序列转换为键向量和值向量。键向量用于计算注意力权重,而值向量用于表示每个位置的具体特征或信息。
# 注意力权重计算:通过计算查询向量与每个键向量之间的相似度(例如点积或缩放点积),得到每个位置的注意力权重。这些权重描述了查询与各个键之间的关联程度,决定了模型关注不同位置的程度。
# 上下文向量计算:将注意力权重与值向量相乘并求和,得到一个上下文向量。这个上下文向量是由值向量加权组合而成的,它汇集了与查询相关的信息。
#自注意力机制,就是 q=k=v
# 形象地,其计算了每个单词对其他单词的"注意力",最后结果是所有词向量的加权求和,即将其他位置的信息纳入计算,为每个位置提供了上下文表示
4.2 attention机制的实现
import torch
import torch.nn.functional as F
def attention(q,k,v,masked=None,dropout=None):
# Q、K、V三者的形状都是[batch_size, sequence_length, d_model](单头时,多头最后一个为d_model/m)
d_model=q.shape[-1]
# shape:batch_size*sequence_length*sequence_length
score=torch.matmul(q,k.transpose(-2,-1))/math.sqrt(d_model)
if masked is not None:
# 将对应的掩码位置转化为1e-9
score=score.masked_filled(0,1e-9)
# 结果是每个sequence_length的sequence_length列个元素的和为1,
# p_attn表示每个key中的每一个词对一个询问语句中的一个单词的相关程度(sequence_length_of_query * sequence_length_of_key)
# shape:batch_size*sequence_length*sequence_length
p_attn=F.softmax(score,dim=-1)
if dropout is not None:
p_attn=nn.Dropout(p_attn,p=dropout)
# query的注意力表示:torch.matmul(p_attn,v), shape:[batch_size, sequence_length, d_model]
# 注意力张量:p_attn, shape:batch_size*sequence_length*sequence_length
return torch.matmul(p_attn,v),p_attn
4.3 多头注意力机制
#%% 多头注意力
# 就是对词向量进行拆分再使用attention
import copy
def clones(module, num):
return nn.ModuleList([copy.deepcopy(module) for _ in range(num)])
class MultiHeadAttention(nn.Module):
def __init__(self,head,d_model,dropout=0.1):
#embedding_dim:就是class PositionalEmbedding里设置的d_model的值
super(MultiHeadAttention,self).__init__()
self.head=head
assert d_model % head == 0
self.d_k=d_model/ head
self.d_model=d_model
self.linears=copy(nn.Linear(self.d_model, self.d_model),4)
self.dropout=dropout
self.attn=None
def forward(self,q,k,v,masked=None):
if masked is not None:
masked=masked.unsqueeze(1)
batch_size=q.size[0]
# http://nlp.seas.harvard.edu/2018/04/03/attention.html
# 原文的图中意思是将q,k,v从d_model(假设512维)映射到不同的子空间(维度都为d_k,假设为64)中
# 但是实际的做法是直接截取d_k维度(8)的词向量做同维度(8)映射
# 最后转换后形状为: 句子数量 * 头的数量(8) * 每个句子单词数 * 截取的词维度数目(64)
q,k,v=[l(x).view(batch_size,-1,self.head,self.d_k).transpose(1,2) for l,x in zip(self.linears,(q,k,v))]
x,self.attn=attention(q,k,v,masked,self.dropout)
# contiguous()的作用在于使得后面的view能用,不然报bug
# 实际上在完成concat,将8个片段合在一起,重新化为512维
x=x.transpose(1,2).contiguous().view(batch_size,-1,self.head * self.d_k)
return self.linears[-1](x)
4.4 Feed Forward
#%% 前馈全连接层 Feed Forward
# 组成:两层全连接
# 作用:考虑到注意力机制对复杂过程的拟合程度不够,通过增加两层全连接层来增强模型的能力
# 用全连接而不是卷积是因为要理解文本中的全局信息(对不同位置的加权没咋变过,如3*3卷积核就那9个值)和长距离依赖关系
# 长距离依赖关系:文本中可能出现位置较后的文本与较前位置的文本有很强的关联
# eg."文本中可能出现位置较后的文本与较前位置的文本有很强的关联性" 这句
# ”较后的文本“与”关联性“、”文本中“ 都有较强的关系
class FeedForward(nn.Module):
# d_model:词嵌入的维度
# d_hl: 第一个线性层的输出维度
def __init__(self,d_model, d_hl,dropout=0.1):
super(FeedForward,self).__init__()
self.l1=nn.Linear(d_model,d_hl)
self.l2=nn.Linear(d_hl,d_model)
self.dropout=nn.Dropout(p=dropout)
def forward(self,x):
return self.l2(self.dropout(F.relu(self.l1(x))))
4.5 LayerNorm
#%% LayerNorm
# BN是在每个维度上统计所有样本的值,计算均值和方差;
# LN是在每个样本上统计所有维度的值,计算均值和方差(语句里难以区分特征,不如视一句话一个特征)
class LayerNorm(nn.Module):
# d_model:词向量嵌入长度
# eps,一个放在分母的小数,为0
def __init__(self,d_model,eps=1e-6):
super(LayerNorm,self).__init__()
self.eps=eps
self.d_model=d_model
# self.a: 增益参数
# self.b: 偏移项
# 公式见 https://zhuanlan.zhihu.com/p/54530247 中的(4)
self.a=nn.Parameter(torch.ones(self.d_model))
self.b=nn.Parameter(torch.zeros(self.d_model))
def forward(self,x):
mean=torch.mean(x,dim=-1,keepdim=True)
std=torch.std(x,dim=-1,keepdim=True)
result=self.a*(x-mean)/(std+self.eps)+self.b
return result
4.6 残差连接
class sublayerConnection(nn.Module):
def __init__(self, size, dropout):
# size等同于d_model
super(sublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
# 这里的 sublayer 指转换函数,如在encoder里面第一个sublayer 是MultiHeadAttention,第二个是FeedForward
# 这里归一化的位置是有些奇怪,个人熟悉的应该是 self.norm(x + self.dropout(sublayer(x)))
# 关于上一点会在 Encoder 的正式定义里见到,在正式输出到下个encoder之前会再norm一次
return x + self.dropout(sublayer(self.norm(x)))
4.7 encoder layer
就是要搭建一个这个
class EncoderLayer(nn.Module):
def __init__(self,multi_head_attn,feedForward,d_model,dropout):
super(EncoderLayer,self).__init__()
# multi_head_attn:多头注意力机制的一个实现类
# feedForward:前馈全连接层的一个实现类
self.multiHeadAttention=multi_head_attn
self.feedForward=feedForward
self.d_model=d_model
self.subPlayerConnect=clones(sublayerConnection(d_model,dropout),2)
def forward(self,x,mask):
# 解释一下,由于多头注意力机制需要四个参数,而subPlayerConnect只有一个参数传递的地方,所以其他三个位置的参数需要提前指定
# 在x + self.dropout(sublayer(self.norm(x)))里还是等价于 x+self.self_attn(self.norm(x), self.norm(x), self.norm(x), mask)
# 而feedForward只需要一个参数,所以可以直接使用
# 其实下面地self.multiHeadAttention(x, x, x, mask)即是自注意力机制
# 形象地,其计算了每个单词对其他单词的"注意力",最后结果是所有词向量的加权求和,即将其他位置的信息纳入计算,为每个位置提供了上下文表示
x=self.subPlayerConnect[0](x, lambda x: self.multiHeadAttention(x, x, x, mask))
x=self.subPlayerConnect[1](x,self.feedForward)
return x
4.8 encoder
就是要将N个encoderLayer串联起来
#%% encoder 编码器
# 对自然语言进行特征提取,也就是编码
# 实际上就是由N个编码器层串联而成的
class Encoder(nn.Module):
def __init__(self,layer,num):
# layer: 就是encoder layer的一个实现类
# num: 数量
super(Encoder, self).__init__()
self.layers=clones(layer,num)
self.norm=LayerNorm(layer.d_model)
self.num=num
def forward(self,x,mask):
for layer in self.layers:
x=layer(x,mask)
return self.norm(x)
5.Decoder
5.1 整体结构
其实大多基础结构是一致的,比如attention机制,multi head attention 之类的,所以会复用在第四节写过的一些代码
5.2 解码器的工作逻辑
解码器会逐个位置地生成输出序列。 在生成每个位置的输出时,会考虑前面已经生成的部分序列,并根据注意力机制对输入序列和已生成序列进行关联计算。这样可以使模型能够逐步生成输出序列,并且在生成每个位置的输出时利用了前面已生成的内容。 这就是掩码张量存在的意义
5.3 decoderLayer
#%% decoderLayer 解码器层
#
# 解码器会逐个位置地生成输出序列。
# 在生成每个位置的输出时,会考虑前面已经生成的部分序列,并根据注意力机制对输入序列和已生成序列进行关联计算。
# 这样可以使模型能够逐步生成输出序列,并且在生成每个位置的输出时利用了前面已生成的内容.
# 这就是掩码张量的意义
class decoderLayer(nn.Module):
def __init__(self,d_model,self_attn,src_attn,feed_forward,dropout):
# d_model: 词嵌入的维度
# self_attn: 自注意力机制
# src_attn: 普通的多头注意力机制
super(decoderLayer,self).__init__()
self.d_modle=d_model
self.dropout=dropout
self.self_attn=self_attn
self.src_attn=src_attn
self.feed_forward=feed_forward
self.subLayers=clones(sublayerConnection(self.d_modle,self.dropout))
def forward(self,x,memory,src_mask,tgt_mask):
# x: output的再输入(可以认为是一个迭代过程)
# memory:编码器的输出结果
# src_mask:有时候因为句子长度不一要进行填充,这里的mask就是将填充部分的加权置为0
# tgt_mask:就是上面所说的屏蔽未来位置
m=memory
x=self.subLayers[0](x,lambda x: self.self_attn(x,x,x,tgt_mask))
x=self.subLayers[1](x,lambda x: self.src_attn(x,m,m,src_mask))
return self.subLayers[2](x,self.feed_forward)
5.4 decoder
就是将N个decoderLayer串联
#%% decoder 解码器
# 和encoder类似,都是串联输出
class Decoder(nn.Module):
def __init__(self,layer,num):
super(Decoder, self).__init__()
self.layers=clones(layer,num)
self.norm=LayerNorm(layer.d_model)
def forward(self,x,memory,src_mask,tgt_mask):
for layer in self.layers
x=layer(x,memory,src_mask,tgt_mask)
return self.norm(x)
6 mask 掩码张量
6.1 为什么需要它
#具体而言,掩码张量在Transformer中有两种常见的应用:
#填充遮挡(Padding Masking):当输入序列的长度不一致时,通常需要在较短的序列后面进行填充,使其与较长序列具有相同的长度。在进行自注意力计算时,通过填充遮挡掩码将填充部分的注意力权重置为0,以避免模型在注意力计算中使用填充位置的信息。
#未来遮挡(Future Masking):在训练Transformer模型时,为了遵循自回归的特性,即模型只能根据已知的上文预测下一个位置,需要对当前位置之后的信息进行遮挡。未来遮挡掩码将当前位置之后的位置的注意力权重置为0,以确保模型只能看到当前位置及其之前的信息
# 功能口语化:翻译出来的内容中的第n个位置是结合了(不知输出结果)前n-1所有位置(大于等于0)的信息
6.2 怎么work的
1.前置:注意力权重矩阵,shape:batch_size, sequence_length,sequence_length;
mask,shape:batch_size, sequence_length,sequence_length;
2. mask对应这么一个矩阵,黄色是1,表示保留权重,紫色是0,表示舍弃权重(实际上是将原来权重设置为1e-9),
3. 处理后的注意力权重矩阵与V做矩阵乘法,非常容易发现,对[sequence_length, d_model] 而言,第一行的结果是只有V中第一行词向量参与了,第二行的结果有第一、二两行词向量参与,以此类推;(与前面说的翻译到第n个词要参考前n-1个结果 有些出入,没想好怎么编)
6.3 代码
#%% masked
#具体而言,掩码张量在Transformer中有两种常见的应用:
#填充遮挡(Padding Masking):当输入序列的长度不一致时,通常需要在较短的序列后面进行填充,使其与较长序列具有相同的长度。在进行自注意力计算时,通过填充遮挡掩码将填充部分的注意力权重置为0,以避免模型在注意力计算中使用填充位置的信息。
#未来遮挡(Future Masking):在训练Transformer模型时,为了遵循自回归的特性,即模型只能根据已知的上文预测下一个位置,需要对当前位置之后的信息进行遮挡。未来遮挡掩码将当前位置之后的位置的注意力权重置为0,以确保模型只能看到当前位置及其之前的信息
# 功能口语化:翻译出来的内容中的第n个位置是结合了(不知输出结果)前n-1所有位置(大于等于0)的信息
import numpy as np
def subsequentMask(size):
attn_shape=(1,size,size)
subsequent_mask=np.triu(np.ones(attn_shape),k=1).astype('uint8')
return torch.from_numpy(1-subsequent_mask)
7 输出
#%% 输出
import torch.nn.functional as F
class Generator(nn.Module):
def __init__(self,d_model,vocab_size):
# vocab_size:最后对应词表大小,比如任务是中译英的话,这里的vocab_size就是英语词表的大小
super(Generator,self).__init__()
self.l=nn.Linear(d_model,vocab_size)
def forward(self,x):
return F.log_softmax(self.l(x),dim=-1)
8 粗略的框架
从大佬讲解视频那里截了个图,方便大家吹牛B,vit属于下面的 encoder only