1. 位置编码:对于输入的向量x.shape[b,s,d_model] 而言进行位置上的编码
class PositionEncoding(nn.module):
def __init__(self,d_model,max_len):
super.__init__()
pe = torch.zeros(max_len,d_model)
position = torch.arange(0,max_len)
div_term = torch.exp(-math.log(10000)*torch.arange(0,d_model,2)/d_model)
pe[:,0::2] = torch.sin(position*div_term)
pe[:,1::2] = torch.cos(position*div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe",pe)
def forward(self,x):
x = x + self.pe[:,:x.size(1)]
return x
使用位置编码可以将输入中任意某个tensor的位置量化成d_model维度的向量,编码不会重复(单通道下至少需要10000*2pi个位置才有可能重复),并且由于正弦余弦函数是周期函数,在推理时可以插入比训练序列更长的序列。
为什么要使用register_buffer?
——因为pe是常数,是不参与训练的,只需要记录就好。
2. 多头注意力机制:
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.d_k = d_model // num_heads
self.num_heads = num_heads
self.q_linear = nn.Linear(d_model, d_model)
self.k_linear = nn.Linear(d_model, d_model)
self.v_linear = nn.Linear(d_model, d_model)
self.out = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(0.1)
def forward(self, q, k, v, mask=None): # 此处q k v均是x
B, T, D = q.size()
q = self.q_linear(q).view(B, T, self.num_heads, self.d_k).transpose(1, 2)
k = self.k_linear(k).view(B, T, self.num_heads, self.d_k).transpose(1, 2)
v = self.v_linear(v).view(B, T, self.num_heads, self.d_k).transpose(1, 2)
scores = q @ k.transpose(-2, -1) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn = torch.softmax(scores, dim=-1)
attn = self.dropout(attn)
x = attn @ v
x = x.transpose(1, 2).contiguous().view(B, T, -1) # 合并多头
return self.out(x)
scores本质上是query与key的相关度,也就是某两个位置的token相关度有多高,然后attn作用于v,本质上是对某一个token处的embedding加上与之相关度高的token的embedding。
3. 前馈网络:
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff=2048):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.dropout = nn.Dropout(0.1)
self.linear2 = nn.Linear(d_ff, d_model)
def forward(self, x):
return self.linear2(self.dropout(torch.relu(self.linear1(x))))
4. encoder layer:一层attention一层前馈,每一层后都有残差连接和layer norm
以翻译器为例,假如输入是 “I love you”,需要对应翻译 “我爱你”,那么编码器的输入source token应该是“I love you”,此时的attention机制可以感知全局。
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.ff = FeedForward(d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
def forward(self, x, mask):
attn = self.self_attn(x, x, x, mask)
x = self.norm1(x + attn)
ff = self.ff(x)
return self.norm2(x + ff)
为什么在attention层和前馈网络后进行layer norm?
最初的transformer使用的是post norm,也就是在经过网络和残差连接之后进行layer norm,主要是由于残差直接将输入加到子层的输出中,虽然保留了底层信息但是会造成数值不稳定,并且使用norm能够有效抑制梯度爆炸和消失,同时加速训练收敛。BERT、T5 等现代 Transformer都使用pre norm,能够使得训练更加稳定,更适用于大模型。
5. decoder layer:一层self attention和cross attention以及一层前馈,每一层后都有残差连接和layer norm
解码器的输入target token应该是“BOS 我 爱 你”,BOS(begin of sequence)是起始点,decoder需要进行感知。此时的每一个target token的输入都能感知到完整的encoder output的信息。
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.ff = FeedForward(d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
def forward(self, x, enc_output, src_mask, tgt_mask):
x = self.norm1(x + self.self_attn(x, x, x, tgt_mask))
x = self.norm2(x + self.cross_attn(x, enc_output, enc_output, src_mask))
x = self.norm3(x + self.ff(x))
return x
self attention和cross attention有什么不同?
self attention主要是为了让target tokens具备感知上文的能力,同时使用target mask遮盖未来token的信息。而cross attention是为了让当前的每一条target token embedding关注输入source token的全部上下文信息。在cross attention计算过程中,target token和enc_output的序列长度是不需要对齐的。
为什么cross attention不需要进行mask掩码?
回顾QKV计算,对于decoder输入target token矩阵来说,每一条query都可以完整的感知encoder的输出信息,后续参与计算的也是当前上文中的token和原文全部的位置信息。
6. encoder和decoder:
class Encoder(nn.Module):
def __init__(self, vocab_size, d_model, N, num_heads):
super().__init__()
self.embed = nn.Embedding(vocab_size, d_model)
self.pos = PositionalEncoding(d_model)
self.layers = nn.ModuleList([EncoderLayer(d_model, num_heads) for _ in range(N)])
def forward(self, src, mask):
x = self.pos(self.embed(src))
for layer in self.layers:
x = layer(x, mask)
return x
class Decoder(nn.Module):
def __init__(self, vocab_size, d_model, N, num_heads):
super().__init__()
self.embed = nn.Embedding(vocab_size, d_model)
self.pos = PositionalEncoding(d_model)
self.layers = nn.ModuleList([DecoderLayer(d_model, num_heads) for _ in range(N)])
def forward(self, tgt, enc_output, src_mask, tgt_mask):
x = self.pos(self.embed(tgt))
for layer in self.layers:
x = layer(x, enc_output, src_mask, tgt_mask)
return x
7. transformer整体结构
class Transformer(nn.Module):
def __init__(self, src_vocab, tgt_vocab, d_model=512, N=6, heads=8):
super().__init__()
self.encoder = Encoder(src_vocab, d_model, N, heads)
self.decoder = Decoder(tgt_vocab, d_model, N, heads)
self.out = nn.Linear(d_model, tgt_vocab)
def forward(self, src, tgt, src_mask, tgt_mask):
enc_output = self.encoder(src, src_mask)
dec_output = self.decoder(tgt, enc_output, src_mask, tgt_mask)
return self.out(dec_output) # shape: (batch, tgt_len, tgt_vocab)
8. transformer损失函数
transformer的损失函数一般使用多元交叉熵函数,也就是针对一个目标序列中的第t个token,模型预测出目标词汇表V长度的概率分布,其label是真值词的one-hot编码。
9. 面试中常见transformer八股
1. transformer基本架构组成
2. self attention机制的作用是什么?
长距离依赖捕获所有位置的token的信息、实现并行计算、可以处理变长输入、权重可视化。
3. self attention如何计算?为什么要除以根号dk?
在QKV计算中,QK点积的大小会随着dk维度的变化而变化,如果不除以dk,scores经过softmax之后的向量会接近one-hot编码,根据softmax的反函数,其梯度会变得非常小,不利于网络的反向传播。
4. multi-head attention的作用是什么?
多个head其实相当于让模型学习到多个不同的注意力模式,捕捉更多样化的关系和语义;多个head能够实现更加稳定的梯度传播和训练;如果是并行计算,多个head比一个head计算效率更高。