声明:本文内容受到李宏毅老师transformer课程教学启发,是在看完老师的课程后再看原文进行总结,有兴趣的同学可以去看一看老师的课程。李宏毅老师Youtube-Transformer课程
概览:
Transformer是一种深度学习的模型,由Vaswani等大神在2017年的论文“attention is all you need”中首次提出,主要用于处理序列数据,尤其是在NLP领域,具有优秀的效果。而其的出世也是将注意力机制真正带入公众视野的一个关键点。
总而言之,在现在,如果你对深度学习感兴趣,那么注意力机制与Transformer你是必须要学的。最近我也在学习相关的知识,因而萌生了写这一篇博客的想法。个人水平有限,若有错误之处,欢迎大家批评指正。Email:yuhan.huang@whu.edu.cn
Attention is all you need带读:
说句题外话,我看的论文不多,但是第一眼觉得这个论文题目真的很霸气啊。没有实力哪敢这么狂 :)
尤其注意:这里我强烈建议大家不要看的太深入,了解其机制,知道各个部分是什么就可以,有的地方你看图是看不明白的,看不懂的部分就直接看我的代码!!!
背景(encoder&decoder&attention mechanism)
在当时的环境中,人们处理序列数据主要还是通过包含encoder(编码器)与decoder(解码器)的循环神经网络或者卷积神经网络实现,在当时最好的模型表现上还添加了attention mechanism(注意力机制)。
所谓编码器解码器模型,以语言翻译为例子,其主要的思想就是:把我们的输入序列 “they are watching.”这个句子通过encoder转换成一个上下文变量,即一个vector,然后再通过上下文变量以及当前的输入决定当前步的输出值。若对于这个步骤都不了解的话,我还是建议去看一看李宏毅老师的教学视频,然后再来看这个,把基础打好总是最重要的。
模型结构
我们先不着急讨论transformer的所谓优势与亮点,先看其模型的结构,可以看出,论文中的模型其实也是一个encoder-decoder的框架,输入输出格式与上面的图相同。只不过在其中,其并没有用到以前常用的循环神经网络或是卷积神经网络,而是通过多头注意力机制进行一个编码与解码的过程。
注意,下面我的计算是一个句子的形式,在实际的训练过程中,肯定是一个批次一个批次的进行计算的,所以我们的形状前面其实还有一个batch_size的维度,不过这里只是告诉大家计算逻辑,没有影响。
提一嘴,建议大家看的过程中如果对于掩码操作有疑惑的话,可以参考这篇csdn文章,原文链接:Transformer中的mask操作详解(训练模型一定要看)
编码器:
encoder中N=6,在每一个大层中包含了两个小层,最首先的就是一个multi-head attension mechanism(多头注意力机制),不懂这个的不着急,我们马上就给大家详细的讲解一下注意力机制与多头注意力机制。第二个就是一个简单的前馈网络。然后在两个子层sublayer的每一个结尾处都进行残差连接并进行LayerNorm(层归一化),这就是我们的前馈网络。为了方便这些残差连接,模型中所有子层以及嵌入层的输出纬度为 .
或许有的同学看到这里以及急了:什么是注意力机制?什么是前馈网络?什么是层归一化?别急,慢慢来。我们只是先看一个整体架构。
解码器
decoder中的N同样等于6,其首先包含一个掩码多头注意力机制(masked multi-head attention mechanism),然后再是一个多头注意力机制,并最后跟随一个前馈网络。与encoder类似,我们在子层后依然跟随残差连接,并跟随层归一化。这个掩码其实也就是为了在decoder的过程中,不可以受到当前位置后面的因素的影响。
注意力机制
attention mechanism(注意力机制),就是将查询和一组键值对映射到输出中,输出被值的加权和(权重由查询q以及相应键k的相关的函数进行计算)进行计算。许多人看了还是懵懵懂懂,qkv是什么玩意? 不着急,我们慢慢的来看。首先,我们从一个头的注意力机制看起。
1.Scaled Dot-Product Attention
这个是论文中使用的注意力机制,对于一个输入序列中,其查询q和键值k的维度是(你们可以先想一想他俩的维度为什么要相同 :),然后值v的纬度是。我们计算q和k的点积,进行缩放变换后进行softmax变换,再乘以值v。这大概就是一个计算流程。计算公式如下:
有的同学估计可能会疑惑,为什么这里要除以一个根号下dk呢?我们可以从softmax的图形来看。
当x过大或者过小的时候,softmax的梯度太小了,这样是不利于我们训练的,所以一个值能够帮助我们进行反向传播。 至于为什么是根号下dk呢?这就是人家选好的比较好的结果了。站在巨人的肩膀上研究 :)
我们可以直接引入李宏毅老师的课程截图。
老师的这个图可以很形象的表示出自注意力机制的计算过程(与论文计算不完全一样),但是我对于矩阵计算还是有一些疑惑,但是先不管,逻辑是没问题的。在我下面的公式中,我默认a是(1, features)的向量,这很重要。所以我总结的计算公式是(这里我也不是很确定,欢迎大家批评指正)。
其中(认为是图片里的a1,a2,a3,a4),A的大小是(4, 512), Wq,Wk的大小是(512, dk), Wv的大小是(512, dv),所以Q,K,V三个矩阵的形状分别是(4, dk), (4, dk), (4, dv).
多提一嘴,这个512是因为embedding和encoder所有子层的输出都是512维的向量。
那么,Q与K的转置相乘得到的形状大小则为(4, 4),除以值和softmax的操作并不会对形状进行改变,再乘以V的形状大小则为(4, dv)。因此我们可以确定,在代码层面上,我这么理解应该是对的,把一个词的特征理解成行向量,李宏毅老师应该是按照列向量理解的。
2. Multi-Head Attention
多头注意力机制其实就是在注意力机制多了一个步骤。依然以我们上面a1,a2,a3,a4为例。
论文里的计算公式如下:
可以知道,在这里,Q,K,V的计算是与前面有所不同的,在这里,我们的Q,K,V都是依然通过线性变换获得的,只不过在这一个步骤中,其形状皆为(4, 512)。然后我们会将其划分成h块,在论文里h=8,则划分成(4, 8, 64)的模块,当然在计算的过程中,我们会涉及到一些转置等操作,这里我们不深究,大家看代码就能明白了。大概就是会把(4, 8, 64)的Q和K还有V都变换成(8, 4, 64),然后再把K转置为(8, 64, 4), Q和转置后的K相乘得到(8, 4, 4)的矩阵,再与(8, 4, 64)的V相乘得到(8, 4, 64)的矩阵,最后再进行转置变化(4, 8, 64)的矩阵,并把1,2维合并得到(4, 512)的输出。
这里几个矩阵都是(512, 64)的形状,Wo的形状是(hdv, dmodel)是(512, 512)。这就是论文中多头注意力机制的计算过程。
可以看见,我们输入的是(4, 512)的矩阵,经过多头注意力机制后,输出的形状没有发生变化。
总结:在论文架构中,我们可以看见注意力机制大概有三个用法,第一个是在encoder中,第二个是在decoder中,第三个是在encoder与decoder之间。这里内部的逻辑咱们先不细究怎么实现,在写代码的时候我们再细讲,别着急,先往后面看。
位置前馈网络(Position-wise Feed-forward Networks)
这个网络在encoder和decoder中都有用到。
其就是两个线性层连接,中间用一个Relu层进行非线性化,计算公式如下:
其中W1的形状为(512,2048), W2的形状为(2048, 512),所以其最后输出的形状没有发生变化。
位置编码(Positional emcoding)
这里的编码其实跟embedding差不多,对于模型的序列设置一个dmodel维的向量,并添加在embedding中,论文中的计算公式则为:
代码实现:
1.嵌入层:
# positionalEncoding is a fixed mat, only add 2 matrix in forward pass
class PositionalEncoding(nn.Module):
def __init__(self, max_len, d_model):
super().__init__()
self.max_len = max_len
self.d_model = d_model
self.encoding = torch.zeros([max_len, d_model], requires_grad=False) # (max_len, d_model)
pos = torch.arange(0, max_len).unsqueeze(1) # (max_len, 1)
_2i = torch.arange(0, d_model, 2) # (d_model/2, )
# PE(pos, 2i) = sin(pos/ 10000 ** (2i/d_model)) ; PE(pos, 2i+1) = cos(pos/ 10000 ** (2i/d_model))
# broadcast in this way:
# 10000 ** (_2i / d_model) -> (d_model/2, ) -> (1, d_model/2) -> (max_len, d_model/2)
# pos -> (max_len, 1) -> (max_len, d_model/2)
self.encoding[:, 0::2] = torch.sin(pos / (10000 ** (_2i / d_model)))
self.encoding[:, 1::2] = torch.cos(pos / (10000 ** (_2i / d_model)))
def forward(self, x): # (batch_size, max_len, d_model)
return x + self.encoding # (batch_size, max_len, d_model)
class TransformerEmbedding(nn.Module):
def __init__(self, vocab_size, d_model, max_len, drop_prob):
super().__init__()
self.tok_emb = nn.Embedding(vocab_size, d_model)
self.pos_emb = PositionalEncoding(max_len, d_model)
self.dropout = nn.Dropout(p=drop_prob)
def forward(self, x): # (batch, max_len)
x = self.tok_emb(x)
x = self.pos_emb(x)
return self.dropout(x) # (batch_ max_len, d_model)
2. 多头注意力机制
# scaled dot-product attention
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super().__init__()
self.softmax = nn.Softmax(dim=-1)
def forward(self, q, k, v, mask=None):
# (batch_size, h, max_len, dk)
# Attention(Q,K,V) = softmax(Q @ K_T / sqrt(dk)) @ V
_, _, _, dk = k.shape
k_T = k.transpose(2, 3) # (batch_size, h, dk, max_len)
scores = torch.matmul(q, k_T) / math.sqrt(dk) # (batch_size, h, max_len, max_len)
if mask is not None:
scores = scores.masked_fill(mask == 0, -10000)
scores = self.softmax(scores) # (batch_size, h, max_len, max_len)
outputs = torch.matmul(scores, v) # (batch_size, h, max_len, dv)
return outputs, scores
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_head):
super().__init__()
assert d_model % n_head == 0, "d_model is not divisible by n_head"
self.n_head = n_head
self.attention = ScaledDotProductAttention()
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 forward(self, q, k, v, mask=None):
# shapes of q,k,v are all (batch_size, max_len, d_model)
q, k, v = self.w_q(q), self.w_k(k), self.w_v(v) # shapes do not change
batch_size, max_len, d_model = k.shape
q = q.view(batch_size, max_len, self.n_head, -1).transpose(1, 2) # (batch_size, n_head, max_len, d_tensor) where d_tensor = d_model / n_head
k = k.view(batch_size, max_len, self.n_head, -1).transpose(1, 2)
v = v.view(batch_size, max_len, self.n_head, -1).transpose(1, 2)
outputs, scores = self.attention(q, k, v, mask=mask) # (batch_size, n_head, max_len, d_tensor), (batch_size, n_head, max_len, max_len)
# concat
outputs = outputs.transpose(1, 2)
outputs = outputs.contiguous().view(batch_size, max_len, d_model) # (batch_size, max_len, d_model)
# Linear
outputs = self.w_o(outputs) # (batch_size, max_len, d_model)
return outputs
3. 层正则化
# layer norm (bn in dim -1)
class LayerNorm(nn.Module):
def __init__(self, d_model, eps=1e-12):
super().__init__()
self.gama = nn.Parameter(torch.ones(d_model))
self.beta = nn.Parameter(torch.zeros(d_model))
self.eps = eps
def forward(self, x): # (batch_size, max_len, d_model)
miu = torch.mean(x, dim=-1, keepdim=True) # (batch_size, max_len, 1)
var = torch.var(x, dim=-1, keepdim=True, unbiased=False) # in paper, used biased var
x_bar = (x - miu) / torch.sqrt(var + self.eps) # (batch_size, max_len, d_model)
return self.gama * x_bar + self.beta # (batch_size, max_len, d_model)
4. 位置前馈网络
# position wise feed forward
class PositionWiseFeedForward(nn.Module):
def __init__(self, d_model, n_hidden, dropout=0.1):
super().__init__()
self.linear_1 = nn.Linear(d_model, n_hidden)
self.relu = nn.ReLU()
self.linear_2 = nn.Linear(n_hidden, d_model)
self.dropout = nn.Dropout(p=dropout)
def forward(self, x): # (batch_size, max_len, d_model)
x = self.linear_1(x) # (batch_size, max_len, n_hidden)
x = self.relu(x)
x = self.linear_2(x) # (batch_size, max_len, n_hidden)
return self.dropout(x)
5.编码层
# encoder layer
class EncoderLayer(nn.Module):
def __init__(self, d_model, n_hidden, n_head, drop_prob):
super().__init__()
# sublayer 1
self.attention = MultiHeadAttention(d_model, n_head)
self.norm1 = LayerNorm(d_model)
self.dropout1 = nn.Dropout(p=drop_prob)
# sublayer 2
self.ffn = PositionWiseFeedForward(d_model, n_hidden, drop_prob)
self.norm2 = LayerNorm(d_model)
self.dropout2 = nn.Dropout(p=drop_prob)
def forward(self, x, src_mask): # (batch_size, max_len, n_hidden) all block will not change the shape of tensor
# sublayer 1
_x = x
x = self.attention(x, x, x, mask=src_mask)
x = self.dropout1(x)
x = self.norm1(x + _x)
# sublayer2
_x = x
x = self.ffn(x)
x = self.dropout2(x)
x = self.norm2(x + _x)
return x
class Encoder(nn.Module):
def __init__(self, enc_vocab_size, max_len, d_model, ffn_hidden, n_head, n_layers, drop_prob):
super().__init__()
self.emb = TransformerEmbedding(enc_vocab_size, d_model, max_len, drop_prob)
self.layers = nn.ModuleList([EncoderLayer(
d_model=d_model,
n_hidden=ffn_hidden,
n_head=n_head,
drop_prob=drop_prob
) for _ in range(n_layers)])
def forward(self, x, src_mask): # (batch_size, max_len)
x = self.emb(x)
for layer in self.layers:
x = layer(x, src_mask)
return x
6.解码层
class DecoderLayer(nn.Module):
def __init__(self, d_model, n_hidden, n_head, drop_prob):
super().__init__()
# sublayer 1
self.attention1 = MultiHeadAttention(d_model, n_head) # self attention
self.norm1 = LayerNorm(d_model)
self.dropout1 = nn.Dropout(p=drop_prob)
# sublayer 2
self.attention2 = MultiHeadAttention(d_model, n_head) # enc_dec_attention
self.norm2 = LayerNorm(d_model)
self.dropout2 = nn.Dropout(p=drop_prob)
# sublayer 3
self.ffn = PositionWiseFeedForward(d_model, n_hidden, drop_prob)
self.norm3 = LayerNorm(d_model)
self.dropout3 = nn.Dropout(p=drop_prob)
def forward(self, dec, enc, trg_mask, src_mask):
# dec is the previous output (batch_size, max_length, d_model)
_x = dec
x = self.attention1(dec, dec, dec, mask=trg_mask) # don't get information of following tokens -> tril mat
x = self.dropout1(x)
x = self.norm1(x + _x)
if enc is not None:
_x = x
x = self.attention2(q=x, k=enc, v=enc, mask=src_mask) # (batch_size, max_length, d_model) -> change <pad> -> negative infinity
x = self.dropout2(x)
x = self.norm2(x + _x)
_x = x
x = self.ffn(x)
x = self.dropout3(x)
x = self.norm3(x + _x)
return x # (batch_size, max_length, d_model)
class Decoder(nn.Module):
def __init__(self, dec_vocab_size, max_len, d_model, ffn_hidden, n_head, n_layers, drop_prob):
super().__init__()
self.emb = TransformerEmbedding(d_model=d_model, drop_prob=drop_prob, max_len=max_len, vocab_size=dec_vocab_size)
self.layers = nn.ModuleList([DecoderLayer(d_model, ffn_hidden, n_head, drop_prob) for _ in range(n_layers)])
self.linear = nn.Linear(d_model, dec_vocab_size)
def forward(self, trg, src, trg_mask, src_mask):
trg = self.emb(trg)
for layer in self.layers:
trg = layer(trg, src, trg_mask, src_mask)
output = self.linear(trg)
return output
嗯,以上就是模型架构全部的代码,当然可能有的同学对于掩码操作比较不熟悉,感到困惑,再等我俩天,找到合适的数据,把训练和预测给大家也肝出来 :)
呃,我发现貌似还有一些问题,没事,我慢慢更新,目前的代码都是没有问题的,后面给大家更新完整 2024.8.19 20:34