Transformer
- 前言
- 网络架构
- 数据处理
- 词嵌入向量
- 位置编码
- 模型定义
- 多头注意力机制
- 编码器Encoder
- 解码器Decoder
前言
Transformer应用范围非常广泛,涵盖了自然语言处理、时间序列数据预测、图像处理等领域。由于笔者之前都是应用,但是对于原理并没有深刻理解导致想要进行进一步的调试难度比较大,所以学习Transformer的原理以便更加好地运用Transformer。Transformer-vit-tutorial-baseline这一篇文章写的非常好,非常清晰地展示了用代码实现Transformer的具体编程步骤以及原理解释,另外Transformer From Scratch和Build Transformer With Pytorch展示了用Pytorch复现Transformer代码的全过程。
网络架构
数据处理
词嵌入向量
将单词转化成嵌入向量能够更加好地捕捉单词之间的语法和语义关系,比如在自然语言处理中经常设置512这个超参数的数值,表示可以最多提取出512个表征特征用于做模型训练。
class Embedding(nn.Module):
def __init__(self, vocab_size, embed_dim):
"""
Args:
vocab_size: size of vocabulary
embed_dim: dimension of embeddings
"""
super(Embedding, self).__init__()
self.embed = nn.Embedding(vocab_size, embed_dim)
def forward(self, x):
"""
Args:
x: input vector
Returns:
out: embedding vector
"""
out = self.embed(x)
return out
位置编码
由于一次性是将所有的单词输入,模型并不知道单词之间的先后关系,然而对于自然语言来说语义顺序很重要,比如“我是你爸爸”顺序不对就变成了“你是我爸爸”。
class PositionalEmbedding(nn.Module):
def __init__(self,max_seq_len,embed_model_dim):
"""
Args:
seq_len: length of input sequence
embed_model_dim: demension of embedding
"""
super(PositionalEmbedding, self).__init__()
self.embed_dim = embed_model_dim
pe = torch.zeros(max_seq_len,self.embed_dim)
for pos in range(max_seq_len):
for i in range(0,self.embed_dim,2):
pe[pos, i] = math.sin(pos / (10000 ** ((2 * i)/self.embed_dim)))
pe[pos, i + 1] = math.cos(pos / (10000 ** ((2 * (i + 1))/self.embed_dim)))
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: input vector
Returns:
x: output
"""
# make embeddings relatively larger
x = x * math.sqrt(self.embed_dim)
#add constant to embedding
seq_len = x.size(1)
x = x + torch.autograd.Variable(self.pe[:,:seq_len], requires_grad=False)
return x
模型定义
多头注意力机制
多头注意力机制作用是让模型知道一句话单词与单词之间的关系,比如有一句话“Dog is crossing the street because it saw its master”。多头注意力就可以分析出it代指的就是Dog。多头注意力机制的基础是自注意力机制,也就是当前单词和当前词所在的句子中其他单词和当前单词之间的关系。对多个单词进行自注意力计算并拼接起来就实现了多头注意力。
1、从输入的文本中得到Q、K、V三个权值向量,Q代表用户的请求,就是输入的对话,K是与Q对应的键值,V是最后的输出。
2、计算[Q*K.t],先进行乘积,这里K用转置矩阵
3、运行Softmax函数并隐藏当前处理单词后面的单词,得到输出得分
4、将输出与Value进行乘积操作
5、通过线性层输出
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
assert d_model % num_heads == 0
# 初始化维度
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
# 初始化K, Q, V以及O向量空间
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model)
def scaled_dot_product_attention(self, Q, K, V, mask=None):
# 计算注意力系数
attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# 如果有mask层就应用
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
# 将注意力系数转化成概率
attn_probs = torch.softmax(attn_scores, dim=-1)
# 与V矩阵相乘得到结果
output = torch.matmul(attn_probs, V)
return output
def split_heads(self, x):
# 改变输入内容的形状适应多头注意力
batch_size, seq_length, d_model = x.size()
return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
def combine_heads(self, x):
# 将多头变回到原始大小
batch_size, _, seq_length, d_k = x.size()
return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
def forward(self, Q, K, V, mask=None):
# 计算Q,K,V矩阵
Q = self.split_heads(self.W_q(Q))
K = self.split_heads(self.W_k(K))
V = self.split_heads(self.W_v(V))
# 计算注意力系数
attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
# 将Q和K矩阵计算结果进行合并
output = self.W_o(self.combine_heads(attn_output))
return output
编码器Encoder
编码器将输入的词加载到模型中,值得注意的是编码器中没有Mask层,输入中的句子是整体”可见“的。以输入长度为d的句子为例,编码器的输入就是n个长度为d的向量。输出就是当前单词本身跟其他单词的相似度,是一个d*d的矩阵。输出实际上就是Value的加权和。
1、将单词编码成词嵌入向量
2、给单词加上位置编码
3、将输入的n个长为d的向量复制两份并输入到多头注意力层中
4、将结果矩阵输出归一化方便进行统一评价
5、经过Feedforward层(TODO:这里还需要解释)
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionalEncoding(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def froward(self, x, mask):
attn_output = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_output))
return x
解码器Decoder
解码器的输入是每一次生成的文字序列,作用是在编码器的输出中挑选感兴趣的内容。跟编码器中的多头注意力原理相似,输出就是从Value中选取感兴趣的内容。
1.跟编码器一样,讲输入转化成词嵌入向量并进行位置编码
2.经过多头注意力层并产生输出向量,不同的一点是这里使用了Mask层屏蔽了当前处理单词后面的所有单词
3.将结果相加并归一化输出作为Value向量
4.将Encoder输出的Key和Value向量和Decoder输出的Query向量相结合组成多头注意力层
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super(DecoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionalEncoding(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, enc_output, src_mask, tgt_mask):
attn_output = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout(attn_output))
attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
x = self.norm2(x + self.dropout(attn_output))
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout(ff_output))
return x