前言
transformer网上的资料已经非常多了,这里主要是做笔记,仅对自己可见。博客主要参考李宏毅课程视频:https://www.youtube.com/watch?v=ugWDIIOHtPA&t=538s
self-attention
先说self-attention,总体步骤如下:
1、从词向量中获取三个不同的向量 q、k、v(图片来均来自于李宏毅课程视频):
这里可以将
a
i
a^i
ai 理解为一个词向量,其中,k:query(to match others),k:key(to be matched),v:information to be extracted,它们通过下面式子得到:
q
i
=
W
q
a
i
q^i = W^q a^i
qi=Wqai
k
i
=
W
k
a
i
k^i = W^k a^i
ki=Wkai
v
i
=
W
v
a
i
v^i = W^v a^i
vi=Wvai
其中
W
W
W 是不同的矩阵,代码上可以用pytorch这么实现:
self.q_linear = Linear(inp_dim, d_model)
self.v_linear = Linear(inp_dim, d_model)
self.k_linear = Linear(inp_dim, d_model)
q = self.q_linear(a)
k = self.k_linear(a)
v = self.v_linear(a)
2、然后用每一个 q 对每一个 v 做一个 attention ,也就是一个点乘运算,即:
公式的形式为:
α
1
,
i
=
q
1
∗
k
i
/
d
\alpha _ {1, i} = q^1 * k^i / \sqrt d
α1,i=q1∗ki/d
这里的 d 是 q 和 v 的维度,之所以有这个 d 是因为,向量的维度越长,那么值也就越大,所以加上这个 d 来平衡。然后我们将结果做一个 softmax :
同样,附上代码更容易懂(score 代表的就是 α ^ \hat{\alpha} α^):
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)
scores = F.softmax(scores, dim=-1)
3、得到了
α
^
\hat \alpha
α^ 后,我们再拿每一个
α
^
\hat \alpha
α^ 与
v
v
v 相乘,然后每个位置求和,也即:
b
j
=
∑
i
α
^
j
,
i
v
i
b^j = \sum_{i} \hat{\alpha}_{j, i} v^i
bj=i∑α^j,ivi
直观的理解如下图:
这样,我们就完成的从 a 1 a^1 a1 到 b 1 b^1 b1 的过程了,同理,其他的位置也是相同的,他们可以并行的运算。这里考虑另外一个问题,假如在计算 b 1 b^1 b1 的时候,我不想看到后面的 a 4 a^4 a4 的信息,这个可以通过 MASK 来解决,就是将后面的数值设为 0,代码实现过程如下:
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9) #这里相当于将mask掉的位置参数设为 0
scores = F.softmax(scores, dim=-1)
output = torch.matmul(scores, v)
4、self-attention 平行化,之前的运行可以通过矩阵运算来平行化计算:
q
i
=
W
q
a
i
q^i = W^q a^i
qi=Wqai
当所有的
a
a
a 一起运算时:
Q
=
W
q
A
Q = W^q A
Q=WqA
其他的步骤可以类似的并行运算。
5、如果上面的没看懂,可以简单地理解为输入了一个序列,并行地输出了另一个序列:
Multi-head Self-attention
之前每个 a 都是分出一个 q 、k、v,Multi-head 的意思是每个 a 分出多个 q、k、v,其他运算过程一模一样,如下图所示:
代码实现也很简单:
self.q_linear = Linear(inp_dim, d_model)
self.v_linear = Linear(inp_dim, d_model)
self.k_linear = Linear(inp_dim, d_model)
q = self.q_linear(a).view(-1, heads, d_model//heads)
k = self.k_linear(a).view(-1, heads, d_model//heads)
v = self.v_linear(a).view(-1, heads, d_model//heads)
#再将 heads 转置到前面
q = q.transpose(-2, -3)
。。。
这样经过同样的运算后,将几个 heads 的结果拼起来就可以了,代码上实现很简单,用一个 reshape 就可以了,这里就不写了。
Positional Encoding
之前的运算,我们可以看出,它是并行计算了,所以每个词的位置相当于是等价的。但是实际问题中,我们是需要考虑词的相对位置信息的。所以需要额外加一个位置信息,原始 paper 中用的是将每一个 a i a^i ai 加上一个位置向量 e i e^i ei,这个位置向量它直接给出来了,代码如下:
pe = torch.zeros(max_seq_len, d_model)
for pos in range(max_seq_len):
for i in range(0, d_model, 2):
pe[pos, i] = math.sin(pos / (10000 ** ((2 * i)/d_model)))
pe[pos, i + 1] = math.cos(pos / (10000 ** ((2 * (i + 1))/d_model)))
例如,输入三个词,维度为4,那么起结果为(图片来自https://www.jianshu.com/p/e7d8caa13b21):
Transformer
1、直接上整体的结构图:
这个图里的核心部分上面已经都写过了,除了 【Add & Norm】和【Feed Forward】,接下来来看这两块实现了什么功能。
2、先说 【Add & Norm】, Add 就是常规的相加操作,Norm 是 layer normalization,这里就不展开讲了,简单的代码实现如下:
# 其中 alpha 和 bias 为学习的参数,eps 是一个很小的值
norm = alpha * (x - x.mean()) / (x.std() + eps) + bias
3、再来看【Feed Forward】,这一层看名字就知道怎么做了,但是它在这个模型中非常重要,因为之前讲的都是线性变化,只有这一层提供了激活函数。