liuyuqi-dellpc 1 year ago
parent
commit
a93d6baf3a
3 changed files with 860 additions and 0 deletions
  1. 491 0
      _c1/attention.md
  2. 156 0
      _c1/nlp.md
  3. 213 0
      _c1/transformer.md

+ 491 - 0
_c1/attention.md

@@ -0,0 +1,491 @@
+---
+title: 第三章:注意力机制
+author: SHENG XU
+date: 2022-05-04
+category: nlp
+layout: post
+---
+
+正如在前两章所说,自从 2017 年 Google 发布[《Attention is All You Need》](https://arxiv.org/abs/1706.03762)之后,各种基于 Transformer 的模型和方法层出不穷。尤其是 2018 年,OpenAI 发布的 [GPT](https://openai.com/blog/language-unsupervised/) 和 Google 发布的 [BERT](https://aclanthology.org/N19-1423/) 模型在几乎所有 NLP 任务上都取得了远超先前最强基准的性能,将 Transformer 模型的热度推上了新的高峰。
+
+<img src="/assets/img/attention/transformers_timeline.png" alt="transformers_timeline" style="display: block; margin: auto; width: 700px">
+
+Transformer 模型之所以如此强大,是因为它抛弃了之前广泛采用的循环网络和卷积网络,而采用了一种特殊的结构——注意力机制 (Attention) 来建模文本。
+
+本章将向大家介绍目前最常见的 Multi-Head Attention,并使用 Pytorch 框架实现一个 Transformer block。
+
+> 如果你不熟悉 Pytorch 可以跳过本章的代码部分,后学借助于 Transformers 库,我们可以非常方便地调用任何 Transformer 模型,而不必像本章一样手工编写。
+{: .block-warning }
+
+## Attention
+
+NLP 神经网络模型的本质就是对输入文本进行编码,常规的做法是首先对句子进行分词,然后将每个词语 (token) 都转化为对应的词向量 (token embeddings),这样文本就转换为一个由词语向量组成的矩阵 $\boldsymbol{X}=(\boldsymbol{x}_1,\boldsymbol{x}_2,\dots,\boldsymbol{x}_n)$,其中 $\boldsymbol{x}_i$ 就表示第 $i$ 个词语的词向量,维度为 $d$,故 $\boldsymbol{X}\in \mathbb{R}^{n\times d}$。
+
+在 Transformer 模型提出之前,对 token 序列 $\boldsymbol{X}$ 的常规编码方式是通过循环网络 (RNNs) 和卷积网络 (CNNs)。
+
+
+- RNN(例如 LSTM)的方案很简单,每一个词语 $\boldsymbol{x}_t$ 对应的编码结果 $\boldsymbol{y}_t$ 通过递归地计算得到:
+  
+  $$
+  \boldsymbol{y}_t =f(\boldsymbol{y}_{t-1},\boldsymbol{x}_t)
+  $$
+  
+  RNN 的序列建模方式虽然与人类阅读类似,但是递归的结构导致其无法并行计算,因此速度较慢。而且 RNN 本质是一个马尔科夫决策过程,难以学习到全局的结构信息;
+
+- CNN 则通过滑动窗口基于局部上下文来编码文本,例如核尺寸为 3 的卷积操作就是使用每一个词自身以及前一个和后一个词来生成嵌入式表示:
+  
+  $$
+  \boldsymbol{y}_t = f(\boldsymbol{x}_{t-1},\boldsymbol{x}_t,\boldsymbol{x}_{t+1})
+  $$
+  
+  CNN 能够并行地计算,因此速度很快,但是由于是通过窗口来进行编码,所以更侧重于捕获局部信息,难以建模长距离的语义依赖。
+
+Google《Attention is All You Need》提供了第三个方案:**直接使用 Attention 机制编码整个文本**。相比 RNN 要逐步递归才能获得全局信息(因此一般使用双向 RNN),而 CNN 实际只能获取局部信息,需要通过层叠来增大感受野,Attention 机制一步到位获取了全局信息:
+
+$$
+\boldsymbol{y}_t = f(\boldsymbol{x}_t,\boldsymbol{A},\boldsymbol{B})
+$$
+
+其中 $\boldsymbol{A},\boldsymbol{B}$ 是另外的词语序列(矩阵),如果取 $\boldsymbol{A}=\boldsymbol{B}=\boldsymbol{X}$ 就称为 Self-Attention,即直接将 $\boldsymbol{x}_t$ 与自身序列中的每个词语进行比较,最后算出 $\boldsymbol{y}_t$。
+
+### Scaled Dot-product Attention
+
+虽然 Attention 有许多种实现方式,但是最常见的还是 Scaled Dot-product Attention。
+
+<img width="250px" src="/assets/img/attention/attention.png" style="display:block;margin:auto;">
+
+Scaled Dot-product Attention 共包含 2 个主要步骤:
+
+1. **计算注意力权重**:使用某种相似度函数度量每一个 query 向量和所有 key 向量之间的关联程度。对于长度为 $m$ 的 Query 序列和长度为 $n$ 的 Key 序列,该步骤会生成一个尺寸为 $m \times n$ 的注意力分数矩阵。
+
+   特别地,Scaled Dot-product Attention 使用点积作为相似度函数,这样相似的 queries 和 keys 会具有较大的点积。
+
+   由于点积可以产生任意大的数字,这会破坏训练过程的稳定性。因此注意力分数还需要乘以一个缩放因子来标准化它们的方差,然后用一个 softmax 标准化。这样就得到了最终的注意力权重 $w_{ij}$,表示第 $i$ 个 query 向量与第 $j$ 个 key 向量之间的关联程度。
+
+2. **更新 token embeddings:**将权重 $w\_{ij}$ 与对应的 value 向量 $\boldsymbol{v}\_1,...,\boldsymbol{v}\_n$ 相乘以获得第 $i$ 个 query 向量更新后的语义表示 $\boldsymbol{x}\_i' = \sum_{j} w\_{ij}\boldsymbol{v}\_j$。
+
+形式化表示为:
+
+$$
+\text{Attention}(\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}) = \text{softmax}\left(\frac{\boldsymbol{Q}\boldsymbol{K}^{\top}}{\sqrt{d_k}}\right)\boldsymbol{V} \tag{4}
+$$
+
+其中 $\boldsymbol{Q}\in\mathbb{R}^{m\times d_k}, \boldsymbol{K}\in\mathbb{R}^{n\times d_k}, \boldsymbol{V}\in\mathbb{R}^{n\times d_v}$ 分别是 query、key、value 向量序列。如果忽略 softmax 激活函数,实际上它就是三个 $m\times d_k,d_k\times n, n\times d_v$ 矩阵相乘,得到一个 $m\times d_v$ 的矩阵,也就是将 $m\times d_k$ 的序列 $\boldsymbol{Q}$ 编码成了一个新的 $m\times d_v$ 的序列。
+
+将上面的公式拆开来看更加清楚:
+
+$$
+\text{Attention}(\boldsymbol{q}_t,\boldsymbol{K},\boldsymbol{V}) = \sum_{s=1}^m \frac{1}{Z}\exp\left(\frac{\langle\boldsymbol{q}_t, \boldsymbol{k}_s\rangle}{\sqrt{d_k}}\right)\boldsymbol{v}_s \tag{5}
+$$
+
+其中 $Z$ 是归一化因子,$\boldsymbol{K},\boldsymbol{V}$ 是一一对应的 key 和 value 向量序列,Scaled Dot-product Attention 就是通过 $\boldsymbol{q}_t$ 这个 query 与各个 $\boldsymbol{k}_s$ 内积并 softmax 的方式来得到 $\boldsymbol{q}_t$ 与各个 $\boldsymbol{v}_s$ 的相似度,然后加权求和,得到一个 $d_v$ 维的向量。其中因子 $\sqrt{d_k}$ 起到调节作用,使得内积不至于太大。
+
+下面我们通过 Pytorch 来手工实现 Scaled Dot-product Attention:
+
+首先需要将文本分词为词语 (token) 序列,然后将每一个词语转换为对应的词向量 (embedding)。Pytorch 提供了 `torch.nn.Embedding` 层来完成该操作,即构建一个从 token ID 到 token embedding 的映射表:
+
+```python
+from torch import nn
+from transformers import AutoConfig
+from transformers import AutoTokenizer
+
+model_ckpt = "bert-base-uncased"
+tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
+
+text = "time flies like an arrow"
+inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
+print(inputs.input_ids)
+
+config = AutoConfig.from_pretrained(model_ckpt)
+token_emb = nn.Embedding(config.vocab_size, config.hidden_size)
+print(token_emb)
+
+inputs_embeds = token_emb(inputs.input_ids)
+print(inputs_embeds.size())
+```
+
+```
+tensor([[ 2051, 10029,  2066,  2019,  8612]])
+Embedding(30522, 768)
+torch.Size([1, 5, 768])
+```
+
+> 为了演示方便,这里我们通过设置 `add_special_tokens=False` 去除了分词结果中的 `[CLS]` 和 `[SEP]`。
+{: .block-warning }
+
+可以看到,BERT-base-uncased 模型对应的词表大小为 30522,每个词语的词向量维度为 768。Embedding 层把输入的词语序列映射到了尺寸为 `[batch_size, seq_len, hidden_dim]` 的张量。
+
+接下来就是创建 query、key、value 向量序列 $\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}$,并且使用点积作为相似度函数来计算注意力分数:
+
+```python
+import torch
+from math import sqrt
+
+Q = K = V = inputs_embeds
+dim_k = K.size(-1)
+scores = torch.bmm(Q, K.transpose(1,2)) / sqrt(dim_k)
+print(scores.size())
+```
+
+```
+torch.Size([1, 5, 5])
+```
+
+这里 $\boldsymbol{Q},\boldsymbol{K}$ 的序列长度都为 5,因此生成了一个 $5\times 5$ 的注意力分数矩阵,接下来就是应用 Softmax 标准化注意力权重:
+
+```python
+import torch.nn.functional as F
+
+weights = F.softmax(scores, dim=-1)
+print(weights.sum(dim=-1))
+```
+
+```
+tensor([[1., 1., 1., 1., 1.]], grad_fn=<SumBackward1>)
+```
+
+最后将注意力权重与 value 序列相乘:
+
+```python
+attn_outputs = torch.bmm(weights, V)
+print(attn_outputs.shape)
+```
+
+```
+torch.Size([1, 5, 768])
+```
+
+至此就实现了一个简化版的 Scaled Dot-product Attention。可以将上面这些操作封装为函数以方便后续调用:
+
+```python
+import torch
+import torch.nn.functional as F
+from math import sqrt
+
+def scaled_dot_product_attention(query, key, value, query_mask=None, key_mask=None, mask=None):
+    dim_k = query.size(-1)
+    scores = torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)
+    if query_mask is not None and key_mask is not None:
+        mask = torch.bmm(query_mask.unsqueeze(-1), key_mask.unsqueeze(1))
+    if mask is not None:
+        scores = scores.masked_fill(mask == 0, -float("inf"))
+    weights = F.softmax(scores, dim=-1)
+    return torch.bmm(weights, value)
+```
+
+上面的代码还考虑了 $\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}$ 序列的 Mask。填充 (padding) 字符不应该参与计算,因此将对应的注意力分数设置为 $-\infty$,这样 softmax 之后其对应的注意力权重就为 0 了($e^{-\infty}=0$)。
+
+注意!上面的做法会带来一个问题:当 $\boldsymbol{Q}$ 和 $\boldsymbol{K}$ 序列相同时,注意力机制会为上下文中的相同单词分配非常大的分数(点积为 1),而在实践中,相关词往往比相同词更重要。例如对于上面的例子,只有关注“time”和“arrow”才能够确认“flies”的含义。
+
+因此,多头注意力 (Multi-head Attention) 出现了!
+
+### Multi-head Attention
+
+Multi-head Attention 首先通过线性映射将 $\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}$ 序列映射到特征空间,每一组线性投影后的向量表示称为一个头 (head),然后在每组映射后的序列上再应用 Scaled Dot-product Attention:
+
+<img width="250px" src="/assets/img/attention/multi_head_attention.png" style="display:block;margin:auto;">
+
+每个注意力头负责关注某一方面的语义相似性,多个头就可以让模型同时关注多个方面。因此与简单的 Scaled Dot-product Attention 相比,Multi-head Attention 可以捕获到更加复杂的特征信息。
+
+形式化表示为:
+
+$$
+\begin{gather}head_i = \text{Attention}(\boldsymbol{Q}\boldsymbol{W}_i^Q,\boldsymbol{K}\boldsymbol{W}_i^K,\boldsymbol{V}\boldsymbol{W}_i^V)\\\text{MultiHead}(\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}) = \text{Concat}(head_1,...,head_h)\end{gather} \tag{6}
+$$
+
+其中 $\boldsymbol{W}_i^Q\in\mathbb{R}^{d_k\times \tilde{d}_k}, \boldsymbol{W}_i^K\in\mathbb{R}^{d_k\times \tilde{d}_k}, \boldsymbol{W}_i^V\in\mathbb{R}^{d_v\times \tilde{d}_v}$ 是映射矩阵,$h$ 是注意力头的数量。最后,将多头的结果拼接起来就得到最终 $m\times h\tilde{d}_v$ 的结果序列。所谓的“多头” (Multi-head),其实就是多做几次 Scaled Dot-product Attention,然后把结果拼接。
+
+下面我们首先实现一个注意力头:
+
+```python
+from torch import nn
+
+class AttentionHead(nn.Module):
+    def __init__(self, embed_dim, head_dim):
+        super().__init__()
+        self.q = nn.Linear(embed_dim, head_dim)
+        self.k = nn.Linear(embed_dim, head_dim)
+        self.v = nn.Linear(embed_dim, head_dim)
+
+    def forward(self, query, key, value, query_mask=None, key_mask=None, mask=None):
+        attn_outputs = scaled_dot_product_attention(
+            self.q(query), self.k(key), self.v(value), query_mask, key_mask, mask)
+        return attn_outputs
+```
+
+每个头都会初始化三个独立的线性层,负责将 $\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}$ 序列映射到尺寸为  `[batch_size, seq_len, head_dim]` 的张量,其中 `head_dim` 是映射到的向量维度。
+
+> 实践中一般将 `head_dim` 设置为 `embed_dim` 的因数,这样 token 嵌入式表示的维度就可以保持不变,例如 BERT 有 12 个注意力头,因此每个头的维度被设置为 $768 / 12 = 64$。
+{: .block-warning }
+
+最后只需要拼接多个注意力头的输出就可以构建出 Multi-head Attention 层了(这里在拼接后还通过一个线性变换来生成最终的输出张量):
+
+```python
+class MultiHeadAttention(nn.Module):
+    def __init__(self, config):
+        super().__init__()
+        embed_dim = config.hidden_size
+        num_heads = config.num_attention_heads
+        head_dim = embed_dim // num_heads
+        self.heads = nn.ModuleList(
+            [AttentionHead(embed_dim, head_dim) for _ in range(num_heads)]
+        )
+        self.output_linear = nn.Linear(embed_dim, embed_dim)
+
+    def forward(self, query, key, value, query_mask=None, key_mask=None, mask=None):
+        x = torch.cat([
+            h(query, key, value, query_mask, key_mask, mask) for h in self.heads
+        ], dim=-1)
+        x = self.output_linear(x)
+        return x
+```
+
+这里使用 BERT-base-uncased 模型的参数初始化 Multi-head Attention 层,并且将之前构建的输入送入模型以验证是否工作正常:
+
+```python
+from transformers import AutoConfig
+from transformers import AutoTokenizer
+
+model_ckpt = "bert-base-uncased"
+tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
+
+text = "time flies like an arrow"
+inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
+config = AutoConfig.from_pretrained(model_ckpt)
+token_emb = nn.Embedding(config.vocab_size, config.hidden_size)
+inputs_embeds = token_emb(inputs.input_ids)
+
+multihead_attn = MultiHeadAttention(config)
+query = key = value = inputs_embeds
+attn_output = multihead_attn(query, key, value)
+print(attn_output.size())
+```
+
+```
+torch.Size([1, 5, 768])
+```
+
+## Transformer Encoder
+
+回忆一下上一章中介绍过的标准 Transformer 结构,Encoder 负责将输入的词语序列转换为词向量序列,Decoder 则基于 Encoder 的隐状态来迭代地生成词语序列作为输出,每次生成一个词语。
+
+<img src="/assets/img/attention/transformer.jpeg" alt="transformer" style="display: block; margin: auto; width: 700px">
+
+其中,Encoder 和 Decoder 都各自包含有多个 building blocks。下图展示了一个翻译任务的例子:
+
+<img src="/assets/img/attention/encoder_decoder_architecture.png" alt="encoder_decoder_architecture" style="display: block; margin: auto; width: 700px">
+
+可以看到:
+
+- 输入的词语首先被转换为词向量。由于注意力机制无法捕获词语之间的位置关系,因此还通过 positional embeddings 向输入中添加位置信息;
+- Encoder 由一堆 encoder layers (blocks) 组成,类似于图像领域中的堆叠卷积层。同样地,在 Decoder 中也包含有堆叠的 decoder layers;
+- Encoder 的输出被送入到 Decoder 层中以预测概率最大的下一个词,然后当前的词语序列又被送回到 Decoder 中以继续生成下一个词,重复直至出现序列结束符 EOS 或者超过最大输出长度。
+
+### The Feed-Forward Layer
+
+Transformer Encoder/Decoder 中的前馈子层实际上就是两层全连接神经网络,它单独地处理序列中的每一个词向量,也被称为 position-wise feed-forward layer。常见做法是让第一层的维度是词向量大小的 4 倍,然后以 GELU 作为激活函数。
+
+下面实现一个简单的 Feed-Forward Layer:
+
+```python
+class FeedForward(nn.Module):
+    def __init__(self, config):
+        super().__init__()
+        self.linear_1 = nn.Linear(config.hidden_size, config.intermediate_size)
+        self.linear_2 = nn.Linear(config.intermediate_size, config.hidden_size)
+        self.gelu = nn.GELU()
+        self.dropout = nn.Dropout(config.hidden_dropout_prob)
+
+    def forward(self, x):
+        x = self.linear_1(x)
+        x = self.gelu(x)
+        x = self.linear_2(x)
+        x = self.dropout(x)
+        return x
+```
+
+将前面注意力层的输出送入到该层中以测试是否符合我们的预期:
+
+```python
+feed_forward = FeedForward(config)
+ff_outputs = feed_forward(attn_output)
+print(ff_outputs.size())
+```
+
+```
+torch.Size([1, 5, 768])
+```
+
+至此创建完整 Transformer Encoder 的所有要素都已齐备,只需要再加上 Skip Connections 和 Layer Normalization 就大功告成了。
+
+### Layer Normalization
+
+Layer Normalization 负责将一批 (batch) 输入中的每一个都标准化为均值为零且具有单位方差;Skip Connections 则是将张量直接传递给模型的下一层而不进行处理,并将其添加到处理后的张量中。
+
+向 Transformer Encoder/Decoder 中添加 Layer Normalization 目前共有两种做法:
+
+<img src="/assets/img/attention/arrangements_of_layer_normalization.png" alt="arrangements_of_layer_normalization" style="display: block; margin: auto; width: 600px">
+
+- **Post layer normalization**:Transformer 论文中使用的方式,将 Layer normalization 放在 Skip Connections 之间。 但是因为梯度可能会发散,这种做法很难训练,还需要结合学习率预热 (learning rate warm-up) 等技巧;
+- **Pre layer normalization**:目前主流的做法,将 Layer Normalization 放置于 Skip Connections 的范围内。这种做法通常训练过程会更加稳定,并且不需要任何学习率预热。
+
+本章采用第二种方式来构建 Transformer Encoder 层:
+
+```python
+class TransformerEncoderLayer(nn.Module):
+    def __init__(self, config):
+        super().__init__()
+        self.layer_norm_1 = nn.LayerNorm(config.hidden_size)
+        self.layer_norm_2 = nn.LayerNorm(config.hidden_size)
+        self.attention = MultiHeadAttention(config)
+        self.feed_forward = FeedForward(config)
+
+    def forward(self, x, mask=None):
+        # Apply layer normalization and then copy input into query, key, value
+        hidden_state = self.layer_norm_1(x)
+        # Apply attention with a skip connection
+        x = x + self.attention(hidden_state, hidden_state, hidden_state, mask=mask)
+        # Apply feed-forward layer with a skip connection
+        x = x + self.feed_forward(self.layer_norm_2(x))
+        return x
+```
+
+同样地,这里将之前构建的输入送入到该层中进行测试:
+
+```python
+encoder_layer = TransformerEncoderLayer(config)
+print(inputs_embeds.shape)
+print(encoder_layer(inputs_embeds).size())
+```
+
+```
+torch.Size([1, 5, 768])
+torch.Size([1, 5, 768])
+```
+
+结果符合预期!至此,本章就构建出了一个几乎完整的 Transformer Encoder 层。
+
+### Positional Embeddings
+
+前面讲过,由于注意力机制无法捕获词语之间的位置信息,因此 Transformer 模型还使用 Positional Embeddings 添加了词语的位置信息。
+
+Positional Embeddings 基于一个简单但有效的想法:**使用与位置相关的值模式来增强词向量。**
+
+如果预训练数据集足够大,那么最简单的方法就是让模型自动学习位置嵌入。下面本章就以这种方式创建一个自定义的 Embeddings 模块,它同时将词语和位置映射到嵌入式表示,最终的输出是两个表示之和:
+
+```python
+class Embeddings(nn.Module):
+    def __init__(self, config):
+        super().__init__()
+        self.token_embeddings = nn.Embedding(config.vocab_size,
+                                             config.hidden_size)
+        self.position_embeddings = nn.Embedding(config.max_position_embeddings,
+                                                config.hidden_size)
+        self.layer_norm = nn.LayerNorm(config.hidden_size, eps=1e-12)
+        self.dropout = nn.Dropout()
+
+    def forward(self, input_ids):
+        # Create position IDs for input sequence
+        seq_length = input_ids.size(1)
+        position_ids = torch.arange(seq_length, dtype=torch.long).unsqueeze(0)
+        # Create token and position embeddings
+        token_embeddings = self.token_embeddings(input_ids)
+        position_embeddings = self.position_embeddings(position_ids)
+        # Combine token and position embeddings
+        embeddings = token_embeddings + position_embeddings
+        embeddings = self.layer_norm(embeddings)
+        embeddings = self.dropout(embeddings)
+        return embeddings
+
+embedding_layer = Embeddings(config)
+print(embedding_layer(inputs.input_ids).size())
+```
+
+```
+torch.Size([1, 5, 768])
+```
+
+除此以外,Positional Embeddings 还有一些替代方案:
+
+**绝对位置表示**:使用由调制的正弦和余弦信号组成的静态模式来编码位置。 当没有大量训练数据可用时,这种方法尤其有效;
+
+**相对位置表示**:在生成某个词语的词向量时,一般距离它近的词语更为重要,因此也有工作采用相对位置编码。因为每个词语的相对嵌入会根据序列的位置而变化,这需要在模型层面对注意力机制进行修改,而不是通过引入嵌入层来完成,例如 DeBERTa 等模型。
+
+下面将所有这些层结合起来构建完整的 Transformer Encoder:
+
+```python
+class TransformerEncoder(nn.Module):
+    def __init__(self, config):
+        super().__init__()
+        self.embeddings = Embeddings(config)
+        self.layers = nn.ModuleList([TransformerEncoderLayer(config)
+                                     for _ in range(config.num_hidden_layers)])
+
+    def forward(self, x, mask=None):
+        x = self.embeddings(x)
+        for layer in self.layers:
+            x = layer(x, mask=mask)
+        return x
+```
+
+同样地,我们对该层进行简单的测试:
+
+```python
+encoder = TransformerEncoder(config)
+print(encoder(inputs.input_ids).size())
+```
+
+```
+torch.Size([1, 5, 768])
+```
+
+## Transformer Decoder
+
+Transformer Decoder 与 Encoder 最大的不同在于 Decoder 有两个注意力子层,如下图所示:
+
+<img src="/assets/img/attention/transformer_decoder.png" alt="transformer_decoder" style="display: block; margin: auto; width: 600px">
+
+**Masked multi-head self-attention layer**:确保在每个时间步生成的词语仅基于过去的输出和当前预测的词,否则 Decoder 相当于作弊了;
+
+**Encoder-decoder attention layer**:以解码器的中间表示作为 queries,对 encoder stack 的输出 key 和 value 向量执行 Multi-head Attention。通过这种方式,Encoder-Decoder Attention Layer 就可以学习到如何关联来自两个不同序列的词语,例如两种不同的语言。 解码器可以访问每个 block 中 Encoder 的 keys 和 values。
+
+与 Encoder 中的 Mask 不同,Decoder 的 Mask 是一个下三角矩阵:
+
+```python
+seq_len = inputs.input_ids.size(-1)
+mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0)
+print(mask[0])
+```
+
+```
+tensor([[1., 0., 0., 0., 0.],
+        [1., 1., 0., 0., 0.],
+        [1., 1., 1., 0., 0.],
+        [1., 1., 1., 1., 0.],
+        [1., 1., 1., 1., 1.]])
+```
+
+这里使用 PyTorch 自带的 `tril()` 函数来创建下三角矩阵,然后同样地,通过 `Tensor.masked_fill()` 将所有零替换为负无穷大来防止注意力头看到未来的词语而造成信息泄露:
+
+```python
+scores.masked_fill(mask == 0, -float("inf"))
+```
+
+```
+tensor([[[26.8082,    -inf,    -inf,    -inf,    -inf],
+         [-0.6981, 26.9043,    -inf,    -inf,    -inf],
+         [-2.3190,  1.2928, 27.8710,    -inf,    -inf],
+         [-0.5897,  0.3497, -0.3807, 27.5488,    -inf],
+         [ 0.5275,  2.0493, -0.4869,  1.6100, 29.0893]]],
+       grad_fn=<MaskedFillBackward0>)
+```
+
+本章对 Decoder 只做简单的介绍,如果你想更深入的了解可以参考 Andrej Karpathy 实现的 [minGPT](https://github.com/karpathy/minGPT)。
+
+本章的所有代码已经整理于 Github:  
+[https://gist.github.com/jsksxs360/3ae3b176352fa78a4fca39fff0ffe648](https://gist.github.com/jsksxs360/3ae3b176352fa78a4fca39fff0ffe648)
+
+## 参考
+
+[[1]](https://kexue.fm/archives/4765) 《Attention is All You Need》浅读(简介+代码)  
+[[2]](https://www.oreilly.com/library/view/natural-language-processing/9781098103231/) 《Natural Language Processing with Transformers》

+ 156 - 0
_c1/nlp.md

@@ -0,0 +1,156 @@
+---
+title: 第一章:自然语言处理
+author: SHENG XU
+date: 2020-01-08
+category: NLP
+layout: post
+mathjax: yes
+---
+
+自然语言处理 (NLP, Natural Language Processing) 又称为计算语言学,是一门借助计算机技术研究人类语言的科学。虽然 NLP 只有六七十年的历史,但是这门学科发展迅速且取得了令人印象深刻的成果。
+
+在上手实践之前,我想先花点时间给大家介绍一下 NLP 的发展历史和 Transformer 模型相关的概念,这对于后期理解模型结构以及自己设计方法会有很大的帮助。
+
+本章将带大家快速穿越 NLP 的发展史,见证从专家系统到 BERT 模型的巨大变化。
+
+## 机器是否要理解语言?
+
+自然语言处理六七十年的发展历程,基本可以分为两个阶段。
+
+### 第一阶段:不懂语法怎么理解语言
+
+上世纪 50 年代到 70 年代,人们对用计算机处理自然语言的认识都局限在人类学习语言的方式上,用了二十多年时间苦苦探寻让计算机理解语言的方法,最终却一无所获。
+
+当时学术界普遍认为,要让机器完成 NLP 任务,首先必须让机器理解语言。因此分析语句和获取语义成为首要任务,而这主要依靠语言学家人工总结文法规则。特别是 60 年代基于乔姆斯基形式语言的编译器得到了很大的发展,更加鼓舞了人们通过概括语法规则来解决 NLP 问题的决心。
+
+但是人类语言既复杂又灵活,仅靠手工编写的文法规则根本无法覆盖,规则之间还可能存在矛盾。毕竟与规范严谨的程序语言不同,自然语言是一种复杂的上下文有关文法,实际很难用计算机进行解析。
+
+因此这一阶段,可以说自然语言处理的研究进入了一个误区。
+
+### 第二阶段:只要看的足够多,就能处理语言
+
+正如人类是通过空气动力学而不是通过模仿鸟类造出了飞机,事实上进行自然语言处理也未必要让机器完全理解语言。70 年代,随着统计语言学的提出,基于数学模型和统计的方法开始兴起,NLP 领域才在接下来的四十多年里取得了一系列突破。
+
+当时,基于统计方法的核心模型是“通信系统加隐马尔可夫模型”,其输入和输出都是一维的符号序列,而且保持原有的次序(例如语音识别、词性分析),但是面对输出为二维树形结构的句法分析以及次序会有很大变化的机器翻译等任务,这种方法就难以解决了。
+
+80 年代以来,随着硬件计算能力的不断提高,以及互联网发展产生的海量数据,越来越多的统计机器学习方法被应用到自然语言处理中。例如随着基于有向图的统计模型的发展,机器已经能够进行复杂的句法分析,2005 年 Google 基于统计方法的翻译系统更是全面超过了基于规则的 SysTran 系统。
+
+**基于神经网络方法的崛起**
+
+2006 年,随着 [Hinton](https://www.mitpressjournals.org/doi/abs/10.1162/neco.2006.18.7.1527) 表明深度信念网络可以通过逐层预训练策略有效地训练,基于神经网络和反向传播算法进行训练的深度学习方法开始兴起,越来越多之前由于缺乏数据、计算能力以及有效优化方法而被忽视的神经网络模型得到了复兴。
+
+> 例如 1997 年就已提出的长短时记忆网络 ([LSTM](https://www.mitpressjournals.org/doi/abs/10.1162/neco.1997.9.8.1735)),直到如今,许多 LSTM 的变体依然在序列建模任务中广泛应用,包括工业界的许多自然语言处理模型。
+{: .block-warning }
+
+随着越来越多 NLP 研究者将注意力转向深度学习方法,各种神经网络模型也被引入到自然语言处理任务中(例如循环网络 LSTM 和卷积网络 CNN)。2017 年,Google 在前人工作的基础上提出了 [Attention](https://papers.nips.cc/paper/7181-attention-is-all-you-need) 注意力模型,为大家提供了另一种编码思路,论文中提出的 Transformer 结构更是引导了后续 NLP 模型的发展。
+
+今天,已经没有人会质疑基于统计的 NLP 方法,但基于规则的传统方法也依然有其存在的价值。近年来许多研究都尝试将传统语言学特征融入到神经网络模型中,将语言学的研究成果与深度学习方法相结合。
+
+## 如何建模语言?
+
+正如第一节所说,人类语言是一种上下文相关的信息表达方式,因此要让机器能够处理自然语言,首先就要为自然语言建立数学模型,这个模型被称为**“统计语言模型”**。
+
+统计语言模型的核心思想就是**判断一个文字序列是否构成人类能理解并且有意义的句子**(也就是判断一个句子是不是人话),这个问题曾经困扰了学术界很多年。
+
+20 世纪 70 年代之前,研究者试图从文字序列是否合乎文法、含义是否正确的角度入手,最终伴随着难以穷尽以及越来越繁琐的规则,这个问题依然无法有效解决。直到 IBM 实验室的 Jelinek 为了研究语音识别问题换了一个思路,用一个非常简单的统计模型就解决了这个问题。
+
+Jelinek 的想法非常简单,一个文字序列 $w_1,w_2,…,w_n$ 是否合理,就看这个句子 $S$ 出现的概率 $P(S)$ 如何,出现概率越大的句子越合理:
+
+$$
+P(S) = P(w_1,w_2,...,w_n)\\
+= P(w_1)P(w_2|w_1)P(w_3|w_1,w_2)...P(w_n|w_1,w_2,...,w_{n-1})
+$$
+
+任意一个词语 $w_n$ 的出现概率都取决于它前面出现的所有词(理论上也可以引入后面的词语共同预测单词的出现概率)。但是,随着文本长度的增加,条件概率 $P(w_n\mid w_1,w_2,…,w_{n-1})$ 会变得越来难计算,因而实际计算时会假设每个词语 $w_i$ 的出现概率仅与它前面的 $N−1$ 个词语有关,即:
+
+$$
+P(w_i|w_1,w_2,...,w_{i-1}) = P(w_i|w_{i-N+1},w_{i-N+2},...,w_{i-1})
+$$
+
+这种假设被称为马尔可夫假设,对应的语言模型被称为 $N$ 元 ($\text{N-Gram}$) 模型。特别地,$N=2$ 时,任意词语 $w_i$ 的出现概率只与它前面的词语 $w_{i−1}$ 有关,被称为二元 (Bigram) 模型;显然,$N=1$ 时的一元模型实际上就是一个上下文无关模型。由于 $N$ 元模型的空间和时间复杂度都几乎是 $N$ 的指数函数,因此实际应用中最常见的是 $N=3$ 的三元模型。
+
+但是,即使是三元、四元甚至是更高阶的语言模型,依然无法覆盖所有的语言现象。在自然语言中,上下文之间的相关性可能跨度非常大,比如从一个段落跨到另一个段落,这是马尔可夫假设解决不了的。此时就需要使用 LSTM、Transformer 等模型来捕获词语之间的长程依赖性 (long distance dependency) 了。
+
+### NNLM
+
+2003 年,Bengio 提出了神经网络语言模型 (NNLM, [Neural Network Language Model](http://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf)),可惜它生不逢时,在之后的十年中都没有引起太大的反响。直到 2013 年,随着越来越多的研究使用深度学习模型进行 NLP 任务,NNLM 才被重新发掘,并成为使用神经网络建模语言的经典范例。
+
+NNLM 的思路非常接近于前面介绍的统计语言模型,它通过输入前面的 $n−1$ 个词来预测当前词。
+
+<img src="/assets/img/nnlm-to-bert/nnlm.png" width="600px" style="display:block; margin:auto;">
+
+首先通过查表 $C$ 得到要预测词语 $w_t$ 前面的 $n−1$ 个词对应的词向量 $C(w_{t-n+1}),…,C(w_{t-2}),C(w_{t-1})$,然后将这些词向量拼接后输入到带有激活函数的隐藏层中,最后通过 $\text{Softmax}$ 函数预测概率。
+
+特别地,包含所有词向量的矩阵 $C$ 也是模型的参数,需要通过学习获得。因此 NNLM 不仅能够能够根据上文预测后接的词语是什么,同时获得了所有词语的**词向量 (Word Embedding)**。
+
+### Word2Vec
+
+真正将神经网络语言模型发扬光大的是 2013 年 Google 提出的 [Word2Vec](https://arxiv.org/pdf/1301.3781.pdf),这个模型提供的词向量在很长一段时间里都是 NLP 模型的标配,即使是后来出现的 Glove 模型也难掩它的光芒。
+
+Word2Vec 的模型结构和 NNLM 基本一致,只是训练方法有所不同,分为 CBOW (Continuous Bag-of-Words) 和 Skip-gram 两种。
+
+<img src="/assets/img/nnlm-to-bert/word2vec.png" width="700px" style="display:block; margin:auto;">
+
+其中 CBOW 使用待预测词语周围的词语 $w(t-2),w(t-1),w(t+1),w(t+2)$ 来进行预测 $w(t)$,Skip-gram 则正好相反,它使用当前词语 $w(t)$ 来预测它的周围词语。
+
+可以看到,与严格参照统计语言模型设计的 NNLM 不同(每个词语的出现概率只与它前面的词语有关),Word2Vec 在结构上更加自由,训练目标也更多地是为获得词向量服务。特别是其提出的同时通过上文和下文来预测当前词语的 CBOW 训练方法,打破了语言模型的固定思维,为后续一系列神经语言模型的发展奠定了基础。
+
+虽然 Word2Vec 取得了巨大的成功,但是有一片乌云一直笼罩在词向量的上空——多义词问题。一词多义正是语言灵活性和高效性的体现,但 Word2Vec 却对此束手无策,无论词语的上下文如何,Word2Vec 对于一个词语只能提供一个词向量,即多义词被编码到了完全相同的参数空间。
+
+而事实上,早在上世纪 90 年代初,Yarowsky 就给出了一个非常简单又有效的解决方案——运用词语之间的**互信息**。具体地,对于一个多义词,分别从大量文本中找出这个词在表示不同语义时,同时出现的互信息最大的一些词。当在判别词语语义时,只需要看看上下文中哪些词语出现的多就可以了,即通过上下文来判断这个词语当前表达的语义。
+
+> **香农与信息论**
+>
+> 1948 年,香农 (Claude Elwood Shannon) 在他著名的论文《通信的数学原理》中提出了“信息熵”的概念,解决了信息的度量问题,并且量化出信息的作用。上面提到的互信息就来自于信息论。
+>
+> 如果你对此感兴趣,可以阅读我的博文[《信息的度量和作用:信息论基本概念》](https://xiaosheng.run/2017/03/09/how-to-measure-information.html)。
+{: .block-tip }
+
+因此,在后来的几年中,标准 NLP 流程都是将 Word2Vec 预训练好的词向量作为模型的输入,然后通过 LSTM、CNN 等模型来重新对句子中的词语进行编码,以便捕获到词语的上下文信息。
+
+### ELMO
+
+2018 年 [ELMO](https://arxiv.org/pdf/1802.05365.pdf) 模型的提出,直接在词向量端给出了一种简洁优雅的解决方案。与 Word2Vec 训练好之后就固定了的静态词向量不同,ELMO 会自动地根据词语的上下文信息去动态调整词语的词向量,因此自然就解决了多义词问题。
+
+具体地,ELMO 首先利用语言模型进行预训练,然后在实际使用时,从预训练网络中提取各层的词向量拼接起来作为新的词向量。
+
+<img src="/assets/img/nnlm-to-bert/elmo.png" width="600px" style="display:block; margin:auto;">
+
+ELMO 采用双层双向 LSTM 作为网络结构,从两个方向编码词语的上下文信息来进行预测,相当于将编码层直接封装到了语言模型中。训练完成后不仅学习到了词语的词向量,还训练好了一个双层双向的 LSTM 网络结构。对于每个词语,可以从第一层 LSTM 中得到包含句法信息的词向量,从第二层 LSTM 中得到包含语义信息的词向量……最终通过加权求和就可以得到每一个词语最终的词向量。
+
+但是 ELMO 也依然存在缺陷,首先它使用 LSTM 作为编码器,而不是特征提取能力更强的 Transformer,其次直接通过拼接来融合双向抽取特征的方法也不够优美。
+
+随后将 ELMO 中的 LSTM 更换为 Transformer 的 [GPT](https://www.cs.ubc.ca/~amuham01/LING530/papers/radford2018improving.pdf) 模型也很快出现了。但是 GPT 又再次追随了 NNLM 的脚步,只通过词语的上文来进行预测,这在很大程度上限制了模型的应用场景。例如阅读理解这种任务,如果预训练时候不把词语的下文嵌入到词向量中,会白白丢掉很多信息。
+
+### BERT
+
+2018 年底随着 [BERT](https://arxiv.org/pdf/1810.04805.pdf) 的提出,这一阶段神经语言模型的发展终于出现了一位集大成者,它在 11 个 NLP 任务上都达到了最好性能。
+
+BERT 在模型大框架上采用和 GPT 完全相同的两阶段模型,首先是语言模型预训练,然后使用微调模式解决下游任务。BERT 不仅像 GPT 模型一样采用 Transformer 作为编码器,而且在预训练阶段采用了类似 ELMO 的双向语言模型。
+
+<img src="/assets/img/nnlm-to-bert/bert.png" width="300px" style="display:block; margin:auto;">
+
+因此 BERT 不仅编码能力强大,而且对各种下游任务,Bert 都可以简单地通过改造输入输出部分来完成。但是 BERT 的优点同样也是它的缺陷,由于 BERT 构建的是双向语言模型,因而无法直接用于文本生成任务。
+
+### 小结
+
+可以看到,从 2003 年 NNLM 模型提出时的无人问津,到 2018 年底 BERT 模型几乎在所有任务上横扫 NLP 领域,神经网络语言模型的发展也经历了漫长的时间。
+
+在这段时间里,许多研究者一直在不断地努力,对前人的工作进行改进,这才迎来了 BERT 的成功。BERT 的出现并非一蹴而就,它不仅借鉴了 ELMO、GPT 等模型的结构与框架,更是延续了 Word2Vec 提出的 CBOW 的思想,是这一阶段神经语言模型发展的集大成者。
+
+## 未来路在何方?
+
+BERT 模型取得成功之后, NLP 研究者们并没有停下脚步,例如 [MASS](https://arxiv.org/pdf/1905.02450.pdf)、[ALBERT](https://arxiv.org/pdf/1909.11942.pdf)、[RoBERTa](https://arxiv.org/pdf/1907.11692.pdf) 等都在 BERT 的基础上进行了某个方向的改良。
+
+其中微软提出的 [UNILM](https://arxiv.org/pdf/1905.03197.pdf) (UNIfied pretrained Language Model) 就是其中一个典型的代表,它把 BERT 的 MASK 机制运用到了一个很高的水平。
+
+<img src="/assets/img/nnlm-to-bert/unilm.png" width="700px" style="display:block; margin:auto;">
+
+UNILM 通过给 Transformer 中 Self-Attention 添加不同的 MASK 矩阵,可以在不改变 BERT 模型结构的基础上,同时实现双向、单向和 Seq2Seq 的语言模型,是一个对 BERT 进行扩展的优雅方案。
+
+在 BERT 取得巨大成功的鼓舞下,正在有越来越多的机构和研究者加入这一领域,目前 [HuggingFace Hub](https://huggingface.co/models) 上托管的各种预训练语言模型已经有八万多个,相信在未来还会有更多高效神经语言模型出现。
+
+## 参考
+
+[[1]](https://book.douban.com/subject/26163454/) 吴军.2014.数学之美 (第二版).人民邮电出版社  
+[[2]](https://book.douban.com/subject/26708119/) 周志华.2016.机器学习.清华大学出版社

+ 213 - 0
_c1/transformer.md

@@ -0,0 +1,213 @@
+---
+title: 第二章:Transformer 模型
+author: SHENG XU
+date: 2022-04-04
+category: nlp
+layout: post
+---
+
+正如上一章所说,自从 BERT 和 GPT 模型取得重大成功之后, Transformer 结构已经替代了循环神经网络 (RNN) 和卷积神经网络 (CNN),成为了当前 NLP 模型的标配。
+
+本章将简单介绍 Transformer 模型的定义及发展,希望它可以成为你探究 Transformer 的地图。
+
+## 起源与发展
+
+2017 年 Google 在[《Attention Is All You Need》](https://arxiv.org/abs/1706.03762)中提出了 Transformer 结构用于序列标注,在翻译任务上超过了之前最优秀的循环神经网络模型;与此同时,Fast AI 在[《Universal Language Model Fine-tuning for Text Classification》](https://arxiv.org/abs/1801.06146)中提出了一种名为 ULMFiT 的迁移学习方法,将在大规模数据上预训练好的 LSTM 模型迁移用于文本分类,只用很少的标注数据就达到了最佳性能。
+
+这些具有开创性的工作促成了两个著名 Transformer 模型的出现:
+
+- **[GPT](https://openai.com/blog/language-unsupervised/)** (the Generative Pretrained Transformer);
+- **[BERT](https://arxiv.org/abs/1810.04805)** (Bidirectional Encoder Representations from Transformers)。
+
+通过将 Transformer 结构与无监督学习相结合,我们不再需要对每一个任务都从头开始训练模型,并且几乎在所有 NLP 任务上都远远超过先前的最强基准。
+
+GPT 和 BERT 被提出之后,NLP 领域出现了越来越多基于 Transformer 结构的模型,其中比较有名有:
+
+<img src="/assets/img/transformers/transformers_chrono.svg" alt="transformers_chrono">
+
+虽然新的 Transformer 模型层出不穷,它们采用不同的预训练目标在不同的数据集上进行训练,但是依然可以按模型结构将它们大致分为三类:
+
+- **纯 Encoder 模型**(例如 BERT),又称自编码 (auto-encoding) Transformer 模型;
+- **纯 Decoder 模型**(例如 GPT),又称自回归 (auto-regressive) Transformer 模型;
+- **Encoder-Decoder 模型**(例如 BART、T5),又称 Seq2Seq (sequence-to-sequence) Transformer 模型。
+
+本章下面会对这三种模型框架进行更详细的介绍。
+
+## 什么是 Transformer
+
+### 语言模型
+
+Transformer 模型本质上都是预训练语言模型,大都采用自监督学习 (Self-supervised learning) 的方式在大量生语料上进行训练,也就是说,训练这些 Transformer 模型完全不需要人工标注数据。
+
+> 自监督学习是一种训练目标可以根据模型的输入自动计算的训练方法。
+{: .block-tip }
+
+例如下面两个常用的预训练任务:
+
+- 基于句子的前 $n$ 个词来预测下一个词,因为输出依赖于过去和当前的输入,因此该任务被称为**因果语言建模** (causal language modeling);
+
+  <img src="/assets/img/transformers/causal_modeling.svg" alt="causal_modeling" style="display: block; margin: auto; width: 700px">
+
+- 基于上下文(周围的词语)来预测句子中被遮盖掉的词语 (masked word),因此该任务被称为**遮盖语言建模** (masked language modeling)。
+
+  <img src="/assets/img/transformers/masked_modeling.svg" alt="masked_modeling" style="display: block; margin: auto; width: 700px">
+
+这些语言模型虽然可以对训练过的语言产生统计意义上的理解,例如可以根据上下文预测被遮盖掉的词语,但是如果直接拿来完成特定任务,效果往往并不好。
+
+> 回忆一下,“因果语言建模”就是上一章中说的统计语言模型,只使用前面的词来预测当前词,由 NNLM 首次运用;而“遮盖语言建模”实际上就是 Word2Vec 模型提出的 CBOW。
+{: .block-warning }
+
+因此,我们通常还会采用迁移学习 (transfer learning) 方法,使用特定任务的标注语料,以有监督学习的方式对预训练模型参数进行微调 (fine-tune),以取得更好的性能。
+
+### 大模型与碳排放
+
+除了 DistilBERT 等少数模型,大部分 Transformer 模型都为了取得更好的性能而不断地增加模型大小(参数量)和增加预训练数据。下图展示了近年来模型大小的变化趋势:
+
+<img src="/assets/img/transformers/nlp_models_size.png" alt="nlp_models_size" style="display: block; margin: auto; width: 700px">
+
+但是,从头训练一个预训练语言模型,尤其是大模型,需要海量的数据,不仅时间和计算成本非常高,对环境的影响也很大:
+
+<img src="/assets/img/transformers/carbon_footprint.svg" alt="carbon_footprint">
+
+可以想象,如果每一次研究者或是公司想要使用语言模型,都需要基于海量数据从头训练,将耗费巨大且不必要的全球成本,因此共享语言模型非常重要。只要在预训练好的模型权重上构建模型,就可以大幅地降低计算成本和碳排放。
+
+> 现在也有一些工作致力于在尽可能保持模型性能的情况下大幅减少参数量,达到用“小模型”获得媲美“大模型”的效果(例如模型蒸馏)。
+{: .block-tip }
+
+### 迁移学习
+
+前面已经讲过,预训练是一种从头开始训练模型的方式:所有的模型权重都被随机初始化,然后在没有任何先验知识的情况下开始训练:
+
+<img src="/assets/img/transformers/pretraining.svg" alt="pretraining" style="display: block; margin: auto; width: 700px">
+
+这个过程不仅需要海量的训练数据,而且时间和经济成本都非常高。
+
+因此,大部分情况下,我们都不会从头训练模型,而是将别人预训练好的模型权重通过迁移学习应用到自己的模型中,即使用自己的任务语料对模型进行“二次训练”,通过微调参数使模型适用于新任务。
+
+这种迁移学习的好处是:
+
+- 预训练时模型很可能已经见过与我们任务类似的数据集,通过微调可以激发出模型在预训练过程中获得的知识,将基于海量数据获得的统计理解能力应用于我们的任务;
+- 由于模型已经在大量数据上进行过预训练,微调时只需要很少的数据量就可以达到不错的性能;
+- 换句话说,在自己任务上获得优秀性能所需的时间和计算成本都可以很小。
+
+例如,我们可以选择一个在大规模英文语料上预训练好的模型,使用 arXiv 语料进行微调,以生成一个面向学术/研究领域的模型。这个微调的过程只需要很少的数据:我们相当于将预训练模型已经获得的知识“迁移”到了新的领域,因此被称为**迁移学习**。
+
+<img src="/assets/img/transformers/finetuning.svg" alt="finetuning" style="display: block; margin: auto; width: 700px">
+
+与从头训练相比,微调模型所需的时间、数据、经济和环境成本都要低得多,并且与完整的预训练相比,微调训练的约束更少,因此迭代尝试不同的微调方案也更快、更容易。实践证明,即使是对于自定义任务,除非你有大量的语料,否则相比训练一个专门的模型,基于预训练模型进行微调会是一个更好的选择。
+
+**在绝大部分情况下,我们都应该尝试找到一个尽可能接近我们任务的预训练模型,然后微调它**,也就是所谓的“站在巨人的肩膀上”。
+
+## Transformer 的结构
+
+标准的 Transformer 模型主要由两个模块构成:
+
+- **Encoder(左边):**负责理解输入文本,为每个输入构造对应的语义表示(语义特征);
+- **Decoder(右边):**负责生成输出,使用 Encoder 输出的语义表示结合其他输入来生成目标序列。
+
+<img src="/assets/img/transformers/transformers_blocks.svg" alt="transformers_blocks" style="display: block; margin: auto; width: 700px">
+
+这两个模块可以根据任务的需求而单独使用:
+
+- **纯 Encoder 模型:**适用于只需要理解输入语义的任务,例如句子分类、命名实体识别;
+- **纯 Decoder 模型:**适用于生成式任务,例如文本生成;
+- **Encoder-Decoder 模型**或 **Seq2Seq 模型:**适用于需要基于输入的生成式任务,例如翻译、摘要。
+
+本章后面会具体地介绍每一种框架。
+
+### 注意力层
+
+Transformer 模型的标志就是采用了**注意力层** (Attention Layers) 的结构,前面也说过,提出 Transformer 结构的论文名字就叫[《Attention Is All You Need》](https://arxiv.org/abs/1706.03762)。顾名思义,注意力层的作用就是让模型在处理文本时,将注意力只放在某些词语上。
+
+例如要将英文“You like this course”翻译为法语,由于法语中“like”的变位方式因主语而异,因此需要同时关注相邻的词语“You”。同样地,在翻译“this”时还需要注意“course”,因为“this”的法语翻译会根据相关名词的极性而变化。对于复杂的句子,要正确翻译某个词语,甚至需要关注离这个词很远的词。
+
+同样的概念也适用于其他 NLP 任务:虽然词语本身就有语义,但是其深受上下文的影响,同一个词语出现在不同上下文中可能会有完全不同的语义(例如“我买了一个苹果”和“我买了一个苹果手机”中的“苹果”)。
+
+> 我们在上一章中已经讨论过多义词的问题,这也是 Word2Vec 这些静态模型所解决不了的。
+{: .block-warning }
+
+### 原始结构
+
+Transformer 模型本来是为了翻译任务而设计的。在训练过程中,Encoder 接受源语言的句子作为输入,而 Decoder 则接受目标语言的翻译作为输入。在 Encoder 中,由于翻译一个词语需要依赖于上下文,因此注意力层可以访问句子中的所有词语;而 Decoder 是顺序地进行解码,在生成每个词语时,注意力层只能访问前面已经生成的单词。
+
+例如,假设翻译模型当前已经预测出了三个词语,我们会把这三个词语作为输入送入 Decoder,然后 Decoder 结合 Encoder 所有的源语言输入来预测第四个词语。
+
+> 实际训练中为了加快速度,会将整个目标序列都送入 Decoder,然后在注意力层中通过 Mask 遮盖掉未来的词语来防止信息泄露。例如我们在预测第三个词语时,应该只能访问到已生成的前两个词语,如果 Decoder 能够访问到序列中的第三个(甚至后面的)词语,就相当于作弊了。
+{: .block-tip }
+
+原始的 Transformer 模型结构如下图所示,Encoder 在左,Decoder 在右:
+
+<img src="/assets/img/transformers/transformers.svg" alt="transformers">
+
+其中,Decoder 中的第一个注意力层关注 Decoder 过去所有的输入,而第二个注意力层则是使用 Encoder 的输出,因此 Decoder 可以基于整个输入句子来预测当前词语。这对于翻译任务非常有用,因为同一句话在不同语言下的词语顺序可能并不一致(不能逐词翻译),所以出现在源语言句子后部的词语反而可能对目标语言句子前部词语的预测非常重要。
+
+> 在 Encoder/Decoder 的注意力层中,我们还会使用 Attention Mask 遮盖掉某些词语来防止模型关注它们,例如为了将数据处理为相同长度而向序列中添加的填充 (padding) 字符。
+{: .block-warning }
+
+## Transformer 家族
+
+虽然新的 Transformer 模型层出不穷,但是它们依然可以被归纳到以下三种结构中:
+
+<img src="/assets/img/transformers/main_transformer_architectures.png" alt="main_transformer_architectures" style="display: block; margin: auto; width: 400px">
+
+### Encoder 分支
+
+纯 Encoder 模型只使用 Transformer 模型中的 Encoder 模块,也被称为自编码 (auto-encoding) 模型。在每个阶段,注意力层都可以访问到原始输入句子中的所有词语,即具有“双向 (Bi-directional)”注意力。
+
+纯 Encoder 模型通常通过破坏给定的句子(例如随机遮盖其中的词语),然后让模型进行重构来进行预训练,最适合处理那些需要理解整个句子语义的任务,例如句子分类、命名实体识别(词语分类)、抽取式问答。
+
+BERT 是第一个基于 Transformer 结构的纯 Encoder 模型,它在提出时横扫了整个 NLP 界,在流行的 [GLUE](https://arxiv.org/abs/1804.07461) 基准上超过了当时所有的最强模型。随后的一系列工作对 BERT 的预训练目标和架构进行调整以进一步提高性能。目前,纯 Encoder 模型依然在 NLP 行业中占据主导地位。
+
+下面简略介绍一下 BERT 模型及它的常见变体:
+
+- **[BERT](https://arxiv.org/abs/1810.04805)**:通过预测文本中被遮盖的词语和判断一个文本是否跟随另一个来进行预训练,前一个任务被称为**遮盖语言建模** (Masked Language Modeling, MLM),后一个任务被称为**下句预测** (Next Sentence Prediction, NSP);
+- **[DistilBERT](https://arxiv.org/abs/1910.01108)**:尽管 BERT 性能优异,但它的模型大小使其难以部署在低延迟需求的环境中。 通过在预训练期间使用知识蒸馏 (knowledge distillation) 技术,DistilBERT 在内存占用减少 40%、计算速度提高 60% 的情况下,依然可以保持 97% 的性能;
+- **[RoBERTa](https://arxiv.org/abs/1907.11692)**:BERT 之后的一项研究表明,通过修改预训练方案可以进一步提高性能。 RoBERTa 在更多的训练数据上,以更大的批次训练了更长的时间,并且放弃了 NSP 任务。与 BERT 模型相比,这些改变显著地提高了模型的性能;
+- **[XLM](https://arxiv.org/abs/1901.07291)**:跨语言语言模型 (XLM) 探索了构建多语言模型的多个预训练目标,包括来自 GPT 的自回归语言建模和来自 BERT 的 MLM,还将 MLM 拓展到多语言输入,提出了翻译语言建模 (Translation Language Modeling, TLM)。XLM 在多个多语言 NLU 基准和翻译任务上都取得了最好的性能;
+- **[XLM-RoBERTa](https://arxiv.org/abs/1911.02116)**:跟随 XLM 和 RoBERTa,XLM-RoBERTa (XLM-R) 通过升级训练数据来改进多语言预训练。其基于 Common Crawl 创建了一个 2.5 TB 的语料,然后运用 MLM 训练编码器,由于没有平行对照文本,因此移除了 XLM 的 TLM 目标。最终,该模型大幅超越了 XLM 和多语言 BERT 变体;
+- **[ALBERT](https://arxiv.org/abs/1909.11942)**:ALBERT 通过三处变化使得 Encoder 架构更高效:首先将词嵌入维度与隐藏维度解耦以减少模型参数;其次所有模型层共享参数;最后将 NSP 任务替换为句子排序预测(判断句子顺序是否被交换)。这些变化使得可以用更少的参数训练更大的模型,并在 NLU 任务上取得了优异的性能;
+- **[ELECTRA](https://arxiv.org/abs/2003.10555)**:MLM 在每个训练步骤中只有被遮盖掉词语的表示会得到更新。ELECTRA 使用了一种双模型方法来解决这个问题:第一个模型继续按标准 MLM 工作;第二个模型(鉴别器)则预测第一个模型的输出中哪些词语是被遮盖的,这使得训练效率提高了 30 倍。下游任务使用时,鉴别器也参与微调;
+- **[DeBERTa](https://arxiv.org/abs/2006.03654)**:DeBERTa 模型引入了两处架构变化。首先将词语的内容与相对位置分离,使得自注意力层 (Self-Attention) 层可以更好地建模邻近词语对的依赖关系;此外在解码头的 softmax 层之前添加了绝对位置嵌入。DeBERTa 是第一个在 [SuperGLUE](https://arxiv.org/abs/1905.00537) 基准上击败人类的模型。
+
+### Decoder 分支
+
+纯 Decoder 模型只使用 Transformer 模型中的 Decoder 模块。在每个阶段,对于给定的词语,注意力层只能访问句子中位于它之前的词语,即只能迭代地基于已经生成的词语来逐个预测后面的词语,因此也被称为自回归 (auto-regressive) 模型。
+
+纯 Decoder 模型的预训练通常围绕着预测句子中下一个单词展开。纯 Decoder 模型适合处理那些只涉及文本生成的任务。
+
+对 Transformer Decoder 模型的探索在在很大程度上是由 [OpenAI](https://openai.com/) 带头进行的,通过使用更大的数据集进行预训练,以及将模型的规模扩大,纯 Decoder 模型的性能也在不断提高。
+
+下面就简要介绍一些常见的生成模型:
+
+- **[GPT](https://openai.com/blog/language-unsupervised)**:结合了 Transformer Decoder 架构和迁移学习,通过根据上文预测下一个单词的预训练任务,在 BookCorpus 数据集上进行了预训练。GPT 模型在分类等下游任务上取得了很好的效果;
+- **[GPT-2](https://openai.com/blog/better-language-models/)**:受简单且可扩展的预训练方法的启发,OpenAI 通过扩大原始模型和训练集创造了 GPT-2,它能够生成篇幅较长且语义连贯的文本;
+- **[CTRL](https://arxiv.org/abs/1909.05858)**:GPT-2 虽然可以根据模板 (prompt) 续写文本,但是几乎无法控制生成序列的风格。条件 Transformer 语言模型 (Conditional Transformer Language, CTRL) 通过在序列开头添加特殊的“控制符”以控制生成文本的风格,这样只需要调整控制符就可以生成多样化的文本;
+- **[GPT-3](https://arxiv.org/abs/2005.14165)**:将 GPT-2 进一步放大 100 倍,GPT-3 具有 1750 亿个参数。除了能生成令人印象深刻的真实篇章之外,还展示了小样本学习 (few-shot learning) 的能力。这个模型目前没有开源;
+- **[GPT-Neo](https://zenodo.org/record/5297715)** / **[GPT-J-6B](https://github.com/kingoflolz/mesh-transformer-jax)**:由于 GPT-3 没有开源,因此一些旨在重新创建和发布 GPT-3 规模模型的研究人员组成了 EleutherAI,训练出了类似 GPT 的 GPT-Neo 和 GPT-J-6B 。当前公布的模型具有 1.3、2.7、60 亿个参数,在性能上可以媲美较小版本的 GPT-3 模型。
+
+### Encoder-Decoder 分支
+
+Encoder-Decoder 模型(又称 Seq2Seq 模型)同时使用 Transformer 架构的两个模块。在每个阶段,Encoder 的注意力层都可以访问初始输入句子中的所有单词,而 Decoder 的注意力层则只能访问输入中给定词语之前的词语(即已经解码生成的词语)。
+
+Encoder-Decoder 模型可以使用 Encoder 或 Decoder 模型的目标来完成预训练,但通常会包含一些更复杂的任务。例如,T5 通过随机遮盖掉输入中的文本片段进行预训练,训练目标则是预测出被遮盖掉的文本。Encoder-Decoder 模型适合处理那些需要根据给定输入来生成新文本的任务,例如自动摘要、翻译、生成式问答。
+
+下面简单介绍一些在自然语言理解 (NLU) 和自然语言生成 (NLG) 领域的 Encoder-Decoder 模型:
+
+- **[T5](https://arxiv.org/abs/1910.10683)**:将所有 NLU 和 NLG 任务都转换为 Seq2Seq 形式统一解决(例如,文本分类就是将文本送入 Encoder,然后 Decoder 生成文本形式的标签)。T5 通过 MLM 及将所有 SuperGLUE 任务转换为 Seq2Seq 任务来进行预训练。最终,具有 110 亿参数的大版本 T5 在多个基准上取得了最优性能。
+- **[BART](https://arxiv.org/abs/1910.13461)**:同时结合了 BERT 和 GPT 的预训练过程。将输入句子通过遮盖词语、打乱句子顺序、删除词语、文档旋转等方式破坏后传给 Encoder 编码,然后要求 Decoder 能够重构出原始的文本。这使得模型可以灵活地用于 NLU 或 NLG 任务,并且在两者上都实现了最优性能。
+- **[M2M-100](https://arxiv.org/abs/2010.11125)**:语言对之间可能存在共享知识可以用来处理小众语言之间的翻译。M2M-100 是第一个可以在 100 种语言之间进行翻译的模型,并且对小众的语言也能生成高质量的翻译。该模型使用特殊的前缀标记来指示源语言和目标语言。
+- **[BigBird](https://arxiv.org/abs/2007.14062)**:由于注意力机制 $\mathcal{O}(n^2)$ 的内存要求,Transformer 模型只能处理一定长度内的文本。 BigBird 通过使用线性扩展的稀疏注意力形式,将可处理的文本长度从大多数模型的 512 扩展到 4096,这对于处理文本摘要等需要捕获长距离依赖的任务特别有用。
+
+## 小结
+
+通过本章,相信你已经对 Transformer 模型的定义和发展有了大概的了解,接下来就可以根据自己的需要对感兴趣的 Transformer 模型进行更深入地探索。
+
+幸运的是,[Hugging Face](https://huggingface.co/) 专门为使用 Transformer 模型编写了一个 [Transformers 库](https://huggingface.co/docs/transformers/index),本章中介绍的所有 Transformer 模型都可以在 [Hugging Face Hub](https://huggingface.co/models) 中找到并且加载使用。
+
+不要着急,在后面的章节中我会手把手地带你编写并训练自己的 Transformer 模型来完成任务。
+
+## 参考
+
+[[1]](https://huggingface.co/course/chapter1/1) HuggingFace 在线教程  
+[[2]](https://transformersbook.com/) Lewis Tunstall 等人. 《Natural Language Processing with Transformers》
+