讨论 2022-09-27 = ###### tags: `tutorials` `Fudan` `2022` # Transformer简述 ## 基本构成 ![](https://i.imgur.com/E4Gg1eB.png) - 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|