讨论 2022-09-27
=
###### tags: `tutorials` `Fudan` `2022`
# Transformer简述
## 基本构成

- Multi-head attention
- Self Attention
- Cross Attention
- FFN
- Layer norm
- Residual
## Attention的几种角度
\begin{gather}
\alpha = \mathrm{softmax}(\frac{QK^T}{\sqrt{D}}) \\
h = \sum_i \alpha_i V_i
\end{gather}
attention最常见的解释是Q,K,V,即query, key, value, 可以理解为从KV store结构中取出信息的过程。也就是从一个字典里面拿东西,这种解释非常形象,但不完全严谨,首先attention是连续的,其次这个取出过程是有连锁反应的,比如我取了A,那么根据Attention的形式,我一定会取和A很接近的B(度量由参数定义的空间决定),同样就无法取出和A相反的C。所以其实attention获取信息不是独立的,是有一定关联效应的。
\begin{gather}
\alpha = \mathrm{softmax}(\frac{H_x W_Q (H_y W_K)^T}{\sqrt{D}}) \\
= \mathrm{softmax}(\frac{H_x W_{Z}H_y }{\sqrt{D}}) \\
h = \sum_i \alpha_i V_i
\end{gather}
第二种理解是双线性函数即 $f(ax+b,y) = af(x, y) + f(b,y)$,attention map就是一个典型的双线性函数,同样的,也可以理解为把$W_Z$作为度量的内积空间。双线性的精髓在于可拆分,很多在attention阶段融合特征,比如relative position embedding等工作中就有广泛应用。
第三种理解是把query投影到value所在的凸区域里面。首先我们要知道归一化系数所张成的空间是凸区域,即$\sum_i \alpha_i=1,\alpha_i>=0$的情况下,$\sum_i \alpha_i V_i$构成的集合是凸区域,即任意区域内两点间的连线上的点还在区域内。设想在一个attention模块中,query有多个,但value是固定的一组,那么对应的凸区域是固定的,所以attention也可以理解为把query投影到这个凸区域里面。这样做有多方面的好处,第一是区域小且紧凑,第二是更容易泛化和拟合(凸区域往往形况更简单)。
## Multi-head的意义
实际上multi-head的两个极端,single-head和head数量与维度一致都有人进行过尝试,multi-head目前的解释通常为增加表达能力。因为正如上述提到的,把多个query放到value的凸区域里面容易混淆,因为value的凸区域并不大,也相对简单,难以描述query之间复杂的特点。通过多组特征描述是一种简单有效的应对方法。而head数量不需要过多是因为表达能力溢出也没有好处,通过观察预训练后的head,经常可以发现雷同或功能接近的head,甚至替换其中一部分,下游任务也不会有很大的影响。说明更多的head也只是冗余。
## FFN
Transformer的一个特点是纺锤状的FFN,即先升维再降维。为什么要这样做,好处是什么?这个问题目前还没有非常明确的答案,但人们确实发现了他在效果上有优势,我个人比较认同的观点有两个,一是信息瓶颈理论,升维可以看作是把一个物体打散成组件,降维则是选取一部分组件重新组合,那么我们可以这个过程更容易去保留主要特征而忽略次要特征。二是高维空间的运算符更强力,高维空间中的加减乘除比低维空间中的更复杂,表达能力更强。
## 预训练的意义
一般来说,没有预训练的transformer是很可能不如RNN和CNN的。参照以下的简单例子,可以看到transformer并不好训练。
```python=
import torch
import torch.nn as nn
import tqdm
import numpy as np
BATCH_SIZE = 16
D_MODEL = 256
N_HEAD = 4
N_LAYER = 6
IN_LEN = 10
IN_DIM = 50
class Trans(nn.Module):
def __init__(self):
super(Trans, self).__init__()
self.trans = nn.TransformerEncoder(nn.TransformerEncoderLayer(D_MODEL, N_HEAD), N_LAYER)
self.in_fc = nn.Linear(IN_DIM*2, D_MODEL)
self.out_fc = nn.Linear(D_MODEL, IN_DIM)
def forward(self, x):
h = self.in_fc(x)
h = self.trans(h)
y = self.out_fc(h)
return y
def run(model, test=False):
model.train()
losses = []
lr = 1e-3
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
with tqdm.trange(1000) as tq:
for i,_ in enumerate(tq):
a = torch.randn((BATCH_SIZE, IN_LEN, IN_DIM)).cuda()
b = torch.randn((BATCH_SIZE, IN_LEN, IN_DIM)).cuda()
tar = (a+1)**2 + b
y = model(torch.cat([a,b], -1))
loss = (0.5*(y-tar)**2).mean()
model.zero_grad()
if not test:
loss.backward()
optimizer.step()
tq.set_postfix({'loss':loss.item()})
losses.append(loss.item())
if test:
print('AVG Loss', np.mean(losses))
if __name__ == '__main__':
model = Trans()
model.cuda()
run(model)
run(model, True)
```
|Setting|MSE|
|-|-|
|lr=1e-3, no warm-up| 3.37|
|lr=1e-4, no warm-up| 0.56|
|lr=1e-5, no warm-up| 2.31|
|lr=1e-3, 1% warm-up| 0.10|
|Setting|MSE|
|-|-|
|default| 0.097|
|xavier_uniform| 0.191|
|xavier_normal| 0.189|
|normal 0.02| 0.66|
|normal 0.002| 2.95|
|normal 0.1| 3.50|
|uniform +-0.05| 3.50|
|uniform +-0.01| 0.424|
|uniform +-0.001| 3.44|
|kaiming_uniform| 3.50|
|kaiming_normal| 3.50|
|Setting|MSE|
|-|-|
|kaiming_normal 2 layer| 0.524|
|kaiming_uniform 2 layer| 0.531|
|xavier_uniform 2 layer| 0.124|
|default 2 layer| 0.092|
|uniform +-0.01 2 layer| 0.117|
|Setting|MSE|
|-|-|
|lr=1e-3, no warm-up 2 layer| 0.086|
|lr=1e-4, no warm-up 2 layer | 0.614|
|Setting|MSE|
|-|-|
|lr=1e-3, no warm-up batch_size 64| 0.037|