# Transformer 常见问题

![last modify](https://img.shields.io/static/v1?label=last%20modify\&message=2023-02-02%2016%3A35%3A31\&color=yellowgreen\&style=flat-square)

* [Transformer Encoder 代码](#transformer-encoder-代码)
* [Transformer 与 RNN/CNN 的比较](#transformer-与-rnncnn-的比较)
  * [RNN](#rnn)
  * [CNN](#cnn)
  * [Transformer](#transformer)
  * [Transformer 能完全取代 RNN 吗？](#transformer-能完全取代-rnn-吗)
* [Transformer 中各模块的作用](#transformer-中各模块的作用)
  * [QKV Projection](#qkv-projection)
    * [为什么在 Attention 之前要对 Q/K/V 做一次投影？](#为什么在-attention-之前要对-qkv-做一次投影)
  * [Self-Attention](#self-attention)
    * [为什么要使用多头？](#为什么要使用多头)
    * [为什么 Transformer 中使用的是乘性 Attention（点积），而不是加性 Attention？](#为什么-transformer-中使用的是乘性-attention点积而不是加性-attention)
    * [Attention 计算中 Scaled 操作的目的是什么？](#attention-计算中-scaled-操作的目的是什么)
    * [在 Softmax 之前加上 Mask 的作用是什么？](#在-softmax-之前加上-mask-的作用是什么)
  * [Add & Norm](#add--norm)
    * [加入残差的作用是什么？](#加入残差的作用是什么)
    * [加入 LayerNorm 的作用是什么？](#加入-layernorm-的作用是什么)
    * [Pre-LN 和 Post-LN 的区别](#pre-ln-和-post-ln-的区别)
  * [Feed-Forward Network](#feed-forward-network)
    * [FFN 层的作用是什么？](#ffn-层的作用是什么)
    * [FFN 中激活函数的选择](#ffn-中激活函数的选择)
* [BERT 相关面试题](#bert-相关面试题)
* [参考资料](#参考资料)

## Transformer Encoder 代码

<details>

<summary>Transformer Encoder（点击展开）</summary>

```python
import math

import torch
import torch.nn as nn
import torch.nn.functional as F
import einops


class TransformerEncoder(nn.Module):

    def __init__(self, n_head, d_model, d_ff, act=F.gelu):
        super().__init__()

        self.h = n_head
        self.d = d_model // n_head
        # Attention
        self.Q = nn.Linear(d_model, d_model)
        self.K = nn.Linear(d_model, d_model)
        self.V = nn.Linear(d_model, d_model)
        self.O = nn.Linear(d_model, d_model)
        # LN
        self.LN1 = nn.LayerNorm(d_model)
        self.LN2 = nn.LayerNorm(d_model)
        # FFN
        self.W1 = nn.Linear(d_model, d_ff)
        self.W2 = nn.Linear(d_ff, d_model)
        self.act = act
        #
        self.dropout = nn.Dropout(0.2)

    def attn(self, x, mask):
        q, k, v = self.Q(x), self.K(x), self.V(x)
        q = einops.rearrange(q, 'B L (H d) -> B H L d', H=self.h)
        k = einops.rearrange(k, 'B L (H d) -> B H d L', H=self.h)
        v = einops.rearrange(v, 'B L (H d) -> B H L d', H=self.h)
        a = torch.softmax(q @ k / math.sqrt(self.d) + mask, dim=-1)  # [B H L L]
        o = einops.rearrange(a @ v, 'B H L d -> B L (H d)')
        o = self.O(o)
        return o

    def ffn(self, x):
        x = self.dropout(self.act(self.W1(x)))
        x = self.dropout(self.W2(x))
        return x

    def forward(self, x, mask):
        x = self.LN1(x + self.dropout(self.attn(x, mask)))
        x = self.LN2(x + self.dropout(self.ffn(x)))
        return x


model = TransformerEncoder(2, 4, 8)
x = torch.randn(2, 3, 4)
mask = torch.randn(1, 1, 3, 3)
o = model(x, mask)

model.eval()
traced_model = torch.jit.trace(model, (x, mask))

x = torch.randn(2, 3, 4)
mask = torch.randn(1, 1, 3, 3)

assert torch.allclose(model(x, mask), traced_model(x, mask))
```

</details>

## Transformer 与 RNN/CNN 的比较

> 其他提法：Transformer 为什么比 RNN/CNN 更好用？优势在哪里？\
> 参考资料：
>
> * [自然语言处理三大特征抽取器（CNN/RNN/Transformer）比较 - 知乎](https://zhuanlan.zhihu.com/p/54743941)
>   * [CNN/RNN/Transformer比较 - 简书](https://www.jianshu.com/p/67666ada573b)
>   * [NLP常用特征提取方法对比 - CSDN博客](https://blog.csdn.net/u013124704/article/details/105201349)

### RNN

* 特点/优势（Transformer之前）：
  * 适合解决线性序列问题；天然能够捕获位置信息（相对+绝对）；

    > 绝对位置：每个 token 都是在固定时间步加入编码；相对位置：token 与 token 之间间隔的时间步也是固定的；
  * 支持不定长输入；
  * LSTM/Attention 的引入，加强了长距离语义建模的能力；
* 劣势：
  * 串行结构难以支持并行计算；
  * 依然存在长距离依赖问题；

    > 有论文表明：RNN 最多只能记忆 50 个词左右的距离（How Neural Language Models Use Context）；
  * 单向语义建模（Bi-RNN 是两个单向拼接）

### CNN

* 特点/优势：
  * 捕获 n-gram 片段信息（局部建模）；
  * 滑动窗口捕获相对位置特征（但 Pooling 层会丢失位置特征）；
  * 并行度高（滑动窗口并行、卷积核并行），计算速度快；
* 劣势：
  * 长程建模能力弱：受感受野限制，无法捕获长距离依赖，需要空洞卷积或加深层数等策略来弥补；
  * Pooling 层会丢失位置信息（目前常见的作法会放弃 Pooling）；
  * 相对位置敏感，绝对位置不敏感（平移不变性）

### Transformer

* 特点/优势：
  * 通过位置编码（position embedding）建模相对位置和绝对位置特征；
  * Self-Attention 同时编码双向语义和解决长距离依赖问题；
  * 支持并行计算；
* 缺点/劣势：
  * 不支持不定长输入（通过 padding 填充到定长）；
  * 计算复杂度高；

### Transformer 能完全取代 RNN 吗？

> [有了Transformer框架后是不是RNN完全可以废弃了？ - 知乎](https://www.zhihu.com/question/302392659?sort=created)

* 不行；

## Transformer 中各模块的作用

### QKV Projection

#### 为什么在 Attention 之前要对 Q/K/V 做一次投影？

* 首先在 Transformer-Encoder 中，Q/K/V 是相同的输入；
* 加入这个全连接的目的就是为了将 Q/K/V 投影到不同的空间中，增加多样性；
* 如果没有这个投影，在之后的 Attention 中相当于让相同的 Q 和 K 做点击，那么 attention 矩阵中的分数将集中在对角线上，即每个词的注意力都在自己身上；这与 Attention 的初衷相悖——**让每个词去融合上下文语义**；

### Self-Attention

#### 为什么要使用多头？

> 其他提法：多头的加入既没有增加宽度也没有增加深度，那加入它的意义在哪里？

* 这里的多头和 CNN 中多通道的思想类似，目的是期望不同的注意力头能学到不同的特征；

#### 为什么 Transformer 中使用的是乘性 Attention（点积），而不是加性 Attention？

* 在 GPU 场景下，矩阵乘法的效率更高（原作说法）；
* **在不进行 Scaled 的前提下**，随着 d（每个头的特征维度）的增大，乘性 Attention 的效果减弱，加性 Attention 的效果更好（原因见下一个问题）；

  > [小莲子的回答 - 知乎](https://www.zhihu.com/question/339723385/answer/811341890)

#### Attention 计算中 Scaled 操作的目的是什么？

> 相似提法：为什么在计算 Q 和 K 的点积时要除以根号 d？\
> 参考内容：[Transformer 中的 attention 为什么要 scaled? - 知乎](https://www.zhihu.com/question/339723385)

* **目的**：防止梯度消失；
* **解释**：在 Attention 模块中，注意力权重通过 Softmax 转换为概率分布；但是 Softmax 对输入比较敏感，当输入的方差越大，其计算出的概率分布就越“尖锐”，即大部分概率集中到少数几个分量位置。极端情况下，其概率分布将退化成一个 One-Hot 向量；其结果就是雅可比矩阵（偏导矩阵）中绝大部分位置的值趋于 0，即梯度消失；通过缩放操作可以使注意力权重的方差重新调整为 1，从而缓解梯度消失的问题；
  * 假设 $Q$ 和 $K$ 的各分量 $\vec{q\_i}$ 和 $\vec{k\_i}$ 相互独立，且均值为 $0$，方差为 $1$；

    > 在 Embedding 和每一个 Encoder 后都会过一个 LN 层，所以可以认为这个假设是合理的；
  * 则未经过缩放的注意力权重 $A$ 的各分量 $\vec{a\_i}$ 将服从均值为 $0$，方差为 $d$ 的正态分布；
  * $d$ 越大，意味着 $\vec{a\_i}$ 中各分量的差越大，其结果就是经过 softmax 后，会出现数值非常小的分量；这样在反向传播时，就会导致**梯度消失**的问题；
  * 此时除以 $\sqrt{d}$ 会使 $\vec{a\_i}$ 重新服从标准的正态分布，使 softmax 后的 Attention 矩阵尽量平滑，从而缓解梯度消失的问题；
  * **数学推导**：

    > [Transformer 中的 attention 为什么要 scaled? - TniL的回答（已删除）](https://www.zhihu.com/question/339723385/answer/782509914)

    * 定义 $Q=\[\vec{q\_1}, \vec{q\_2}, .., \vec{q\_n}]^T$, $K=\[\vec{k\_1}, \vec{k\_2}, .., \vec{k\_n}]^T$，其中 $\vec{q\_i}$ 和 $\vec{k\_i}$ 都是 $d$ 维向量；
    * 假设 $\vec{q\_i}$ 和 $\vec{k\_i}$ 的各分量都是服从标准正态分布（均值为 0，方差为 1）的随机变量，且相互独立，记 $q\_i$ 和 $k\_i$，即 $E(q\_i)=E(k\_i)=0$, $D(q\_i)=D(k\_i)=1$；
    * 根据期望与方差的性质，有 $E(q\_ik\_i)=0$ 和 $D(q\_ik\_i)=1$，推导如下：

      $$$
      E(q\_ik\_i) &= E(q\_i)E(k\_i) = 0 \times 0 = 0 \ D(q\_ik\_i) &= E(q\_i^2k\_i^2) - E^2(q\_ik\_i) \ &= E(q\_i^2)E(k\_i^2) - E^2(q\_i)E^2(k\_i) \ &= \left \[E(q\_i^2) - E^2(q\_i) \right ] \left \[E(k\_i^2) - E^2(k\_i) \right ] - 0^2 \times 0^2 \ &= D(q\_i)D(k\_i) - 0 \ &= 1 \end{align\*}$$
      $$$
    * 进一步，有 $E(\vec{q\_i}\vec{k\_i}^T)=0$ 和 $D(\vec{q\_i}\vec{k\_i}^T)=d$，推导如下：

      $$$
      E(\vec{q\_i}\vec{k\_i}^T) &= E(\sum\_{i=1}^d q\_ik\_i) = \sum\_{i=1}^d E(q\_ik\_i) = 0 \ D(\vec{q\_i}\vec{k\_i}^T) &= D(\sum\_{i=1}^d q\_ik\_i) = \sum\_{i=1}^d D(q\_ik\_i) = d \end{align\*}$$
      $$$
    * 根据 attention 的计算公式（softmax 前）, $A'=\frac{QK^T}{\sqrt{d}}=\[\frac{\vec{q\_1}\vec{k\_1}^T}{\sqrt{d}}, \frac{\vec{q\_2}\vec{k\_2}^T}{\sqrt{d}}, .., \frac{\vec{q\_n}\vec{k\_n}^T}{\sqrt{d}}]=\[\vec{a\_1}, \vec{a\_2}, .., \vec{a\_n}]$，可知 $E(\vec{a\_i})=0$, $D(\vec{a\_i})=1$，推导如下：

      $$$
      E(\vec{a\_i}) &= E(\frac{\vec{q\_i}\vec{k\_i}^T}{\sqrt{d}}) = \frac{E(\vec{q\_i}\vec{k\_i}^T)}{\sqrt{d}} = \frac{0}{\sqrt{d}} = 0 \ D(\vec{a\_i}) &= D(\frac{\vec{q\_i}\vec{k\_i}^T}{\sqrt{d}}) = \frac{D(\vec{q\_i}\vec{k\_i}^T)}{(\sqrt{d})^2} = \frac{d}{d} = 1 \end{align\*}$$
      $$$
  * **代码验证**

    ```python
    import torch

    def get_x(shape, eps=1e-9):
        """创建一个 2d 张量，且最后一维服从正态分布"""
        x = torch.randn(shape)
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return (x - mean) / (std + eps)

    d = 400  # 数字设大一些，否则不明显
    q = get_x((2000, d))
    k = get_x((2000, d))

    # 不除以 根号 d
    a = torch.matmul(q, k.transpose(-1, -2))  # / (d ** 0.5)
    print(a.mean(-1, keepdim=True))  # 各分量接近 0
    print(a.var(-1, keepdim=True))  # 各分量接近 d

    # 除以根号 d
    a = torch.matmul(q, k.transpose(-1, -2)) / (d ** 0.5)
    print(a.mean(-1, keepdim=True))  # 各分量接近 0
    print(a.var(-1, keepdim=True))  # 各分量接近 1
    ```

#### 在 Softmax 之前加上 Mask 的作用是什么？

> 相关问题：为什么将被 mask 的位置是加上一个极小值（-1e9），而不是置为 0？

* 回顾 softmax 的公式；
* 其目的就是使无意义的 token 在 softmax 后得到的概率值（注意力）尽量接近于 0；从而使正常 token 位置的概率和接近 1；

### Add & Norm

#### 加入残差的作用是什么？

* 在求导时加入一个恒等项，以减少梯度消失问题；

#### 加入 LayerNorm 的作用是什么？

* 提升网络的泛化性；（TODO：详细解释）
* 加在激活函数之前，避免激活值落入饱和区，减少梯度消失问题；

#### Pre-LN 和 Post-LN 的区别

* Post-LN（BERT 实现）： $$x\_{n+1} = \text{LN}(x\_n + f(x\_n))$$
  * 先做完残差连接，再归一化；
  * 优点：保持主干网络的方程比较稳定，是模型泛化能力更强，性能更好；
  * 缺点：把恒等路径放在 norm 里，使模型收敛更难（反向传播时梯度变小，残差的作用被减弱）
* Pre-LN： $$x\_{n+1} = x\_n + f(\text{LN}(x\_n))$$
  * 先归一化，再做残差连接；
  * 优点：加速收敛
  * 缺点：效果减弱

### Feed-Forward Network

* 前向公式 $$W\_2 \cdot \text{ReLU}(W\_1x + b\_1) + b\_2$$

#### FFN 层的作用是什么？

* 功能与 1\*1 卷积类似：1）跨通道的特征融合/信息交互；2）通过激活函数增加非线性；

  > [1\*1卷积核的作用\_nefetaria的博客-CSDN博客](https://blog.csdn.net/nefetaria/article/details/107977597)
* 之前操作都是线性的：1）Projection 层并没有加入激活函数；2）Attention 层只是线性加权；

#### FFN 中激活函数的选择

> 相关问题：BERT 为什么要把 FFN 中的 ReLU 替换为 GeLU？

* 背景：原始 Transformer 中使用的是 **ReLU**；BERT 中使用的是 **GeLU**；
* GeLU 在激活函数中引入了正则的思想，越小的值越容易被丢弃；相当于综合了 ReLU 和 Dropout 的功能；而 ReLU 缺乏这个随机性；
* 为什么不使用 sigmoid 或 tanh？——这两个函数存在饱和区，会使导数趋向于 0，带来梯度消失的问题；不利于深层网络的训练；

## BERT 相关面试题

* [daily-interview/BERT面试题.md at master · datawhalechina/daily-interview](https://github.com/datawhalechina/daily-interview/blob/master/AI%E7%AE%97%E6%B3%95/NLP/%E7%89%B9%E5%BE%81%E6%8C%96%E6%8E%98/BERT/BERT%E9%9D%A2%E8%AF%95%E9%A2%98.md)

## 参考资料

* [深入剖析PyTorch中的Transformer API源码\_哔哩哔哩\_bilibili](https://www.bilibili.com/video/BV1o44y1Y7cp/?spm_id_from=333.788)
* [超硬核Transformer细节全梳理！\_哔哩哔哩\_bilibili](https://www.bilibili.com/video/BV1AU4y1d7nT)
* [Transformer、RNN 与 CNN 三大特征提取器的比较\_Takoony的博客-CSDN博客](https://blog.csdn.net/ningyanggege/article/details/89707196)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://imhuay.gitbook.io/studies/notes/_archives/2022/05/transformer-chang-jian-wen-ti.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
