---
# System prepended metadata

title: 讨论 2022-09-27
tags: [Fudan, '2022', tutorials]

---

讨论 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|