Skip to content

传统NLP

Transformer一开始是为了解决NLP问题而提出的,在最初的论文中用的案例也是翻译任务。我们先来看看传统NLP到底在解决什么问题,并使用了哪些方法。理解传统NLP的局限性,是理解Transformer为何革命性的关键前提。

自然语言处理的核心任务

自然语言处理(Natural Language Processing)的目标是让机器"理解"和"生成"人类语言。核心任务包括:

任务 说明 示例
文本分类 给文本打标签 情感分析:这条评论是正面还是负面?
命名实体识别 (NER) 识别文本中的专有名词 "乔布斯在加州创办了苹果" → 人名、地名、公司名
机器翻译 将一种语言翻译为另一种 法语 → 英语
问答系统 根据上下文回答问题 给定一段维基百科,回答"谁发明了电话?"
文本生成 生成连贯的自然语言 自动写摘要、对话系统
序列标注 对每个词做标注 词性标注(名词、动词、形容词...)

所有这些任务的第一个问题都是:如何让计算机表示一个"词"? 计算机只认数字,不认文字。


文本表示的演进

One-Hot Encoding(独热编码)

最朴素的想法:假设词典有 \(V\) 个词,那每个词就用一个 \(V\) 维的向量表示,只有对应位置是1,其余全是0。

\[ \text{"猫"} = [0, 0, 1, 0, \ldots, 0] \quad \text{"狗"} = [0, 0, 0, 1, \ldots, 0] \]

致命问题:

  1. 维度灾难:如果词典有5万个词,每个词就是一个5万维的稀疏向量,计算极其低效
  2. 无法表达语义相似性:任意两个不同词的向量内积都是0——在这个表示下,"猫"和"狗"的相似度 = "猫"和"飞机"的相似度 = 0

词袋模型(Bag of Words)与 TF-IDF

词袋模型把一个句子/文档表示为词频向量:统计每个词出现了几次,完全忽略词序

\[ \text{"我 喜欢 猫 我 喜欢 狗"} \rightarrow \{\text{我}:2,\ \text{喜欢}:2,\ \text{猫}:1,\ \text{狗}:1\} \]

TF-IDF(词频-逆文档频率) 在此基础上加了一层权重:如果一个词在所有文档中都出现(如"的"、"是"),它的权重就低;如果只在少数文档中出现,权重就高。

\[ \text{TF-IDF}(t, d) = \text{TF}(t, d) \times \log\frac{N}{\text{DF}(t)} \]
  • \(\text{TF}(t,d)\):词 \(t\) 在文档 \(d\) 中的出现频率
  • \(\text{DF}(t)\):包含词 \(t\) 的文档数量
  • \(N\):文档总数

局限性: "狗咬人"和"人咬狗"在词袋模型下完全相同——词序信息完全丢失。

词嵌入(Word Embeddings)

核心突破: 将每个词映射到一个低维稠密向量空间,使得语义相近的词在向量空间中距离也近。

什么是词向量?

词向量(Word Vector)就是一个词在低维嵌入空间中的坐标——一组具体的浮点数。例如一个4维(极度简化)的词向量可能长这样:

\[ \vec{\text{猫}} = [0.82,\ -0.31,\ 0.56,\ 0.12] \]
\[ \vec{\text{狗}} = [0.79,\ -0.28,\ 0.61,\ 0.15] \]
\[ \vec{\text{汽车}} = [-0.45,\ 0.72,\ 0.08,\ -0.63] \]

"猫"和"狗"的向量很接近(都是动物),而"汽车"和它们相距甚远。

稠密向量 vs 稀疏向量

One-Hot编码也是一种"向量",但它是高维稀疏的——5万维中只有1维有值。词嵌入是低维稠密的——每个维度都有非零值,都参与表达语义。

One-Hot(5万维):  [0, 0, 0, ..., 1, ..., 0, 0, 0]   ← 只有1维有用
                   ├──────── 49999个0 ────────┤

Word2Vec(300维): [0.82, -0.31, 0.56, ..., 0.12]     ← 每一维都有用
                   ├────── 300个有意义的值 ──────┤

核心区别在于:One-Hot是一个符号标记("这是词典中第3724个词",不携带任何语义信息),而词向量是一个语义描述(每个维度都编码了某种隐含特征)。

每个维度编码了什么?

词向量的每个维度并非人为定义的,而是模型自动学出来的。但通过事后分析可以发现,某些维度确实隐约对应着可解释的语义方向:

维度方向(概念性) 高值的词 低值的词
"动物性" 猫、狗、鸟 桌子、椅子、电脑
"性别" 女王、姐姐、她 国王、兄弟、他
"大小" 大象、建筑、海洋 蚂蚁、原子、细胞
"情感极性" 开心、美好、赞 悲伤、糟糕、差

需要强调:这些"语义方向"通常不与单个维度精确对齐,而是分布在多个维度的组合中。但这种分布式表示正是词向量强大的原因——它可以用有限的维度编码无穷多种语义关系。

常见词向量维度

模型/场景 典型维度 说明
Word2Vec 100 ~ 300 Google预训练版本为300维
GloVe 50 / 100 / 200 /300 提供多种维度的预训练版本
Transformer Base 512 "Attention is All You Need" 原论文
GPT-2 768 ~ 1600 Small到XL
GPT-3 12288 175B参数模型
LLaMA-7B 4096 Meta开源模型

维度越高,表达能力越强,但计算成本也越高。早期的300维已经足够捕捉丰富的语义关系;到了大模型时代,更大的维度是为了支撑更复杂的上下文理解。

词向量间的相似度

既然词向量是空间中的坐标,我们就可以度量词与词之间的距离。最常用的是余弦相似度

\[ \text{cos\_sim}(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| \cdot |\vec{b}|} = \frac{\sum_{i=1}^{d} a_i b_i}{\sqrt{\sum_{i=1}^{d} a_i^2} \cdot \sqrt{\sum_{i=1}^{d} b_i^2}} \]
  • 值域为 \([-1, 1]\),1表示方向完全相同,0表示正交无关,-1表示方向完全相反
  • 只比较方向不比较长度,因此不受向量大小影响

实际的余弦相似度例子(基于预训练Word2Vec):

词对 余弦相似度 说明
(猫, 狗) ~0.76 同为宠物,高度相似
(国王, 女王) ~0.65 同为统治者,有关联
(猫, 汽车) ~0.05 几乎无关
(好, 坏) ~0.40 虽然语义相反,但经常出现在相似上下文中

注意最后一组:"好"和"坏"的相似度并不低!因为Word2Vec基于分布假说——"好"和"坏"常出现在相同的上下文中("这部电影很___"),所以它们的向量反而比较接近。这是词嵌入的一个著名局限:它捕捉的是共现关系而非语义对立

分布假说(Distributional Hypothesis)

"You shall know a word by the company it keeps." — J.R. Firth, 1957

一个词的含义由它的上下文决定。"猫"和"狗"经常出现在相似的上下文中("喂养"、"在沙发上睡觉"),所以它们的向量应该相近。

Word2Vec (Mikolov et al., 2013)

两种训练模式:

CBOW (Continuous Bag of Words):用上下文预测中心词

\[ P(w_t | w_{t-c}, \ldots, w_{t-1}, w_{t+1}, \ldots, w_{t+c}) \]

给定窗口大小 \(c\) 内的上下文词,预测中间的词 \(w_t\)

Skip-gram:用中心词预测上下文(实践中效果更好)

\[ P(w_{t+j} | w_t) \quad \text{for } j \in [-c, c], j \neq 0 \]

给定中心词 \(w_t\),预测周围窗口内每个位置的词。

训练过程本质上是一个浅层神经网络(只有一个隐藏层),训练完成后取隐藏层的权重矩阵作为词向量。

Word2Vec的经典发现——向量算术:

\[ \vec{\text{king}} - \vec{\text{man}} + \vec{\text{woman}} \approx \vec{\text{queen}} \]

这说明词嵌入空间捕捉到了某种"语义方向",例如"性别方向"、"时态方向"等。

GloVe (Pennington et al., 2014)

GloVe (Global Vectors) 结合了全局统计信息和局部上下文。它直接对词-词共现矩阵进行分解:

\[ J = \sum_{i,j=1}^{V} f(X_{ij})\left(\vec{w}_i^T \vec{\tilde{w}}_j + b_i + \tilde{b}_j - \log X_{ij}\right)^2 \]

其中 \(X_{ij}\) 是词 \(i\) 和词 \(j\) 在上下文窗口中的共现次数,\(f\) 是加权函数(防止高频词主导)。

词嵌入的局限性

静态嵌入问题:每个词只有一个固定向量,无法处理一词多义:

  • "我吃了一个苹果" → 水果
  • "我买了一部苹果手机" → 公司

在两个句子中,"苹果"的 Word2Vec/GloVe 向量完全相同。我们需要上下文相关的动态表示——这正是后来 ELMo、BERT、GPT 等模型要解决的问题。


序列模型

自然语言本质上是一个序列——词的顺序至关重要。为了处理这种序列结构,人们提出了循环神经网络系列。

RNN(循环神经网络)

核心思想:模型逐词阅读序列,每一步都维护一个"隐藏状态" \(h_t\),作为对之前所有信息的记忆。

x₁ → [RNN] → h₁
               ↓
x₂ → [RNN] → h₂
               ↓
x₃ → [RNN] → h₃
               ↓
              ...

数学公式:

\[ h_t = \tanh(W_{hh} \cdot h_{t-1} + W_{xh} \cdot x_t + b_h) \]
\[ y_t = W_{hy} \cdot h_t + b_y \]
  • \(x_t\):第 \(t\) 步的输入(词嵌入向量)
  • \(h_t\):第 \(t\) 步的隐藏状态
  • \(W_{hh}, W_{xh}, W_{hy}\):权重矩阵
  • 每一步的 \(h_t\) 都是 \(h_{t-1}\)(历史记忆)和 \(x_t\)(当前输入)的函数

致命问题——梯度消失与梯度爆炸:

反向传播需要沿时间展开(Backpropagation Through Time, BPTT),梯度会经过连乘:

\[ \frac{\partial h_T}{\partial h_1} = \prod_{t=2}^{T} \frac{\partial h_t}{\partial h_{t-1}} = \prod_{t=2}^{T} W_{hh}^T \cdot \text{diag}(\tanh'(\cdot)) \]
  • 如果 \(W_{hh}\) 的最大特征值 < 1:梯度指数衰减 → 梯度消失 → 模型学不到长距离依赖
  • 如果 \(W_{hh}\) 的最大特征值 > 1:梯度指数增长 → 梯度爆炸 → 训练不稳定

实践中,梯度消失更常见。这意味着RNN在处理长句子时,前面的信息几乎无法影响后面的梯度更新

具体示例:用RNN做情感分析

任务:判断电影评论 "这部 电影 太 好看 了" 是正面还是负面。

Step 1:词嵌入

每个词通过嵌入矩阵转为词向量(假设维度为4):

\[ \text{"这部"} \to x_1 = [0.1, 0.3, -0.2, 0.5] \]
\[ \text{"电影"} \to x_2 = [0.8, -0.1, 0.4, 0.2] \]
\[ \text{"太"} \to x_3 = [0.0, 0.6, 0.1, -0.3] \]
\[ \text{"好看"} \to x_4 = [0.9, 0.7, 0.3, 0.8] \]
\[ \text{"了"} \to x_5 = [-0.1, 0.0, 0.2, 0.1] \]

Step 2:RNN逐词处理

x₁"这部" → [RNN] → h₁ (知道了"这部",还不知道在说什么)
                      ↓
x₂"电影" → [RNN] → h₂ (知道了"这部电影",开始有上下文)
                      ↓
x₃"太"   → [RNN] → h₃ (知道了程度词,在期待后面的情感词)
                      ↓
x₄"好看" → [RNN] → h₄ (关键!正面情感信号进入隐藏状态)
                      ↓
x₅"了"   → [RNN] → h₅ (句末助词,h₅ 应该包含整句的信息)

每一步都执行 \(h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b)\),隐藏状态像滚雪球一样逐步积累信息。

Step 3:分类

取最后一个隐藏状态 \(h_5\),通过一个全连接层 + Softmax输出类别概率:

\[ P(\text{正面} | \text{句子}) = \text{softmax}(W_{\text{cls}} \cdot h_5 + b_{\text{cls}}) \]

如果 \(P(\text{正面}) = 0.92\),则模型判定为正面评论。

RNN做情感分析的问题: 如果评论很长——"虽然开头有点无聊,中间节奏也慢,但最后的反转真的太精彩了,这部电影整体来说非常好看"——等处理到"好看"时,"虽然"和"开头"的信息早已被挤出隐藏状态,模型可能无法正确理解"虽然...但..."这个转折结构。这就是梯度消失导致的长距离依赖问题。

LSTM(长短期记忆网络)

核心改进:引入门控机制(Gating Mechanism)细胞状态(Cell State)\(C_t\),让信息可以"高速公路般"地直接传递,绕过非线性变换的挤压。

          ┌──────────────────────────────────────────────┐
          │               Cell State  Cₜ                  │
          │  Cₜ₋₁ ──→ [×forget] ──→ [+input] ──→ Cₜ ──→│
          └──────────────────────────────────────────────┘
                ↑              ↑             ↓
           Forget Gate    Input Gate    Output Gate
               fₜ            iₜ            oₜ

四个关键方程:

遗忘门(Forget Gate)——决定丢弃哪些旧信息:

\[ f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \]

输入门(Input Gate)——决定写入哪些新信息:

\[ i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \]
\[ \tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) \]

细胞状态更新——核心!加法结构使梯度可以直接流过:

\[ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t \]

输出门(Output Gate)——决定输出哪些信息:

\[ o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \]
\[ h_t = o_t \odot \tanh(C_t) \]

其中 \(\sigma\) 是 Sigmoid 函数(输出0到1,控制"门"的开合度),\(\odot\) 是逐元素乘法。

为什么LSTM能缓解梯度消失?

关键在于 \(C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t\) 这个加法结构。梯度沿 \(C\) 传播时:

\[ \frac{\partial C_t}{\partial C_{t-1}} = f_t \]

只要遗忘门 \(f_t\) 接近1(即网络学会"不要忘记"),梯度就能几乎无损地传递很远。相比RNN中梯度要反复乘以 \(W_{hh}\),LSTM的梯度通道要"宽敞"得多。

具体示例:用LSTM做命名实体识别(NER)

任务:在句子 "乔布斯 在 加州 创办 了 苹果 公司" 中识别出所有命名实体(人名、地名、组织名)。

NER是一个序列标注任务——对每个词输出一个标签,而不是对整个句子输出一个标签。常用的标签体系是BIO标注

  • B-PER:人名的开始(Begin-Person)
  • I-PER:人名的内部(Inside-Person)
  • B-LOC:地名的开始
  • B-ORG:组织名的开始
  • I-ORG:组织名的内部
  • O:不属于任何实体(Outside)

Step 1:词嵌入

每个词转为词向量后输入LSTM。

Step 2:LSTM逐词处理,每一步都输出标签

与情感分析不同,NER需要每个位置都有输出,而不只是最后一步:

词:    乔布斯    在     加州     创办     了     苹果     公司
        ↓       ↓       ↓       ↓       ↓       ↓       ↓
       [LSTM]→[LSTM]→[LSTM]→[LSTM]→[LSTM]→[LSTM]→[LSTM]
        ↓       ↓       ↓       ↓       ↓       ↓       ↓
标签:  B-PER    O     B-LOC     O       O     B-ORG   I-ORG

Step 3:每个位置的分类

每一步的隐藏状态 \(h_t\) 都通过一个全连接层预测标签:

\[ P(y_t | x_1, \ldots, x_t) = \text{softmax}(W_{\text{ner}} \cdot h_t + b_{\text{ner}}) \]

LSTM在NER中的关键优势:识别 "苹果 公司" 是组织名时,模型需要记住前面有 "创办" 这个动词——这暗示后面跟的是一个组织。如果是 "吃 了 苹果",同样的 "苹果" 就不是实体。LSTM的门控机制让它能保留这些远距离的上下文线索。

为什么RNN做NER会更差? 在长句中,比如 "据新华社今天上午的报道,著名企业家乔布斯在加利福尼亚州旧金山市创办了苹果公司"——"创办"和"苹果公司"之间隔了很多词,RNN的梯度消失会让模型难以学到"创办"对"苹果"标签的影响,而LSTM可以通过遗忘门保留这个信号。

实践中更好的方案:BiLSTM-CRF

单向LSTM只能看到前文。但NER中后文也很重要——看到"公司"才能确认"苹果"是组织名。因此实践中常用双向LSTM(BiLSTM)

前向: 乔布斯 → 在 → 加州 → 创办 → 了 → 苹果 → 公司
                                                →  h⃗₆
后向: 乔布斯 ← 在 ← 加州 ← 创办 ← 了 ← 苹果 ← 公司
                                          h⃖₆  ←
\[ h_t = [\overrightarrow{h_t};\ \overleftarrow{h_t}] \]

再加上CRF(条件随机场)层做全局标签解码,保证标签序列的合理性(比如 I-ORG 前面必须是 B-ORG 或 I-ORG,不能突然出现)。BiLSTM-CRF在Transformer出现之前是NER任务的标准方案。

GRU(门控循环单元)

GRU (Cho et al., 2014) 是LSTM的简化版本,将遗忘门和输入门合并为一个更新门,参数更少,训练更快:

更新门(Update Gate)——控制保留多少旧信息:

\[ z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z) \]

重置门(Reset Gate)——控制忽略多少旧信息:

\[ r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r) \]

候选隐藏状态

\[ \tilde{h}_t = \tanh(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h) \]

最终隐藏状态——在旧状态和候选状态之间插值:

\[ h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \]

\(z_t \approx 0\) 时,信息直接复制(类似LSTM的遗忘门为1);当 \(z_t \approx 1\) 时,完全替换为新信息。

LSTM vs GRU:性能上通常非常接近,GRU参数更少适合小数据集,LSTM在非常长的序列上稍有优势。实践中两者差异不大。


Seq2Seq 架构与固定向量瓶颈

Seq2Seq (Sutskever et al., 2014)

机器翻译的核心范式:编码器-解码器(Encoder-Decoder) 架构。

  Encoder                          Decoder
 ┌─────────────────────┐       ┌─────────────────────────┐
 │ Bonjour → h₁        │       │        → I              │
 │ le      → h₂        │  c    │ I      → am             │
 │ monde   → h₃ ───────┼──→────│ am     → happy          │
 │                      │       │ happy  → <EOS>          │
 └─────────────────────┘       └─────────────────────────┘
         法语输入            上下文向量        英语输出

工作流程:

  1. 编码阶段:编码器(一个RNN/LSTM)逐词读入源语言句子,将最终的隐藏状态 \(h_T\) 作为上下文向量(Context Vector) \(c\)
  2. 解码阶段:解码器(另一个RNN/LSTM)以 \(c\) 作为初始隐藏状态,逐词生成目标语言句子
\[ c = h_T^{\text{enc}} \]
\[ h_t^{\text{dec}} = f(h_{t-1}^{\text{dec}}, y_{t-1}, c) \]
\[ P(y_t | y_{<t}, x) = \text{softmax}(W_s \cdot h_t^{\text{dec}}) \]

具体示例:用Seq2Seq做中译英(带详细数值)

任务:将 "我 喜欢 猫" 翻译为 "I like cats"。

为了把过程讲清楚,我们假设一个极度简化的设定:

  • 中文词表:{我, 喜欢, 猫, ...} 共1000个词
  • 英文词表:{I, like, cats, dog, the, am, happy, ..., <BOS>, <EOS>} 共3万个词
  • 词嵌入维度:\(d = 4\)(真实场景通常256~512)
  • LSTM隐藏状态维度:\(d_h = 4\)(真实场景通常256~512)

第一阶段:编码(Encoding)——把中文句子压缩成一个向量

Encoder 就是前向传播。 它没有任何特殊的"压缩"或"理解"机制——就是把一个 LSTM 在输入序列上跑一遍前向传播(矩阵乘法 + 激活函数),每读入一个词就更新一次隐藏状态,最后一步的隐藏状态就是上下文向量。如果你理解了全连接网络的前向传播(\(y = \sigma(Wx + b)\)),那 Encoder 只是在此基础上加了"循环"和"门控"。

Step 1:词嵌入——把文字变成数字

每个中文词通过一个嵌入矩阵 \(E_{\text{zh}} \in \mathbb{R}^{1000 \times 4}\)(1000个词,每个词4维)查表得到词向量:

\[ \text{"我"} \xrightarrow{查表} x_1 = [0.21,\ -0.45,\ 0.73,\ 0.12] \]
\[ \text{"喜欢"} \xrightarrow{查表} x_2 = [0.65,\ 0.33,\ -0.18,\ 0.51] \]
\[ \text{"猫"} \xrightarrow{查表} x_3 = [0.82,\ -0.31,\ 0.56,\ 0.08] \]

这些数值不是手动指定的,而是模型训练过程中自动学出来的。

Step 2:LSTM逐词处理——Encoder的核心就在这里

LSTM从左到右读入每个词向量,每一步更新隐藏状态 \(h_t\) 和细胞状态 \(C_t\)。下面我们完整展开 LSTM 内部的计算过程——每一步就是四次矩阵乘法加一些逐元素运算,没有任何更复杂的操作。

LSTM内部有四组可学习的参数(权重矩阵),分别对应四个"门":

  • \(W_f \in \mathbb{R}^{4 \times 8}\)\(b_f \in \mathbb{R}^{4}\):遗忘门参数
  • \(W_i \in \mathbb{R}^{4 \times 8}\)\(b_i \in \mathbb{R}^{4}\):输入门参数
  • \(W_C \in \mathbb{R}^{4 \times 8}\)\(b_C \in \mathbb{R}^{4}\):候选记忆参数
  • \(W_o \in \mathbb{R}^{4 \times 8}\)\(b_o \in \mathbb{R}^{4}\):输出门参数

为什么是 \(4 \times 8\)?因为每一步的输入是 \([h_{t-1};\ x_t]\)(把上一步的隐藏状态和当前词向量拼接起来),\(h\) 是4维 + \(x\) 是4维 = 8维。输出是4维(隐藏状态的维度)。


处理第1个词:"我"

初始状态:\(h_0 = [0, 0, 0, 0]\)\(C_0 = [0, 0, 0, 0]\)(全零)

(a) 拼接输入

\[ [h_0;\ x_1] = [0,\ 0,\ 0,\ 0,\ 0.21,\ -0.45,\ 0.73,\ 0.12] \in \mathbb{R}^8 \]

就是把两个4维向量首尾相接,变成一个8维向量。

(b) 四次矩阵乘法——四个门各算一次

每个门做的事情完全一样:8维输入 × 权重矩阵 + 偏置 → 激活函数 → 4维输出。和普通全连接层 \(y = \sigma(Wx + b)\) 一模一样。

\[ f_1 = \sigma(W_f \cdot [h_0; x_1] + b_f) = \sigma(\text{8维→4维的矩阵乘法}) = [0.82,\ 0.15,\ 0.91,\ 0.53] \]
\[ i_1 = \sigma(W_i \cdot [h_0; x_1] + b_i) = [0.31,\ 0.72,\ 0.08,\ 0.64] \]
\[ \tilde{C}_1 = \tanh(W_C \cdot [h_0; x_1] + b_C) = [0.45,\ -0.38,\ 0.79,\ 0.11] \]
\[ o_1 = \sigma(W_o \cdot [h_0; x_1] + b_o) = [0.62,\ 0.41,\ 0.73,\ 0.28] \]

其中 \(\sigma\) 是 Sigmoid 函数(输出0~1,控制门的开合),\(\tanh\) 输出-1~1。

(c) 更新细胞状态——逐元素乘法和加法

\[ C_1 = f_1 \odot C_0 + i_1 \odot \tilde{C}_1 \]

展开计算(\(\odot\) 就是对应位置相乘):

\[ C_1 = [0.82, 0.15, 0.91, 0.53] \odot [0, 0, 0, 0] + [0.31, 0.72, 0.08, 0.64] \odot [0.45, -0.38, 0.79, 0.11] \]
\[ = [0, 0, 0, 0] + [0.14,\ -0.27,\ 0.06,\ 0.07] \]
\[ = [0.14,\ -0.27,\ 0.06,\ 0.07] \]

因为 \(C_0\) 全是0(没有旧记忆可遗忘),所以遗忘门 \(f_1\) 在这一步实际上没有作用。细胞状态完全由输入门 \(i_1\) 和候选记忆 \(\tilde{C}_1\) 决定。

(d) 输出隐藏状态

\[ h_1 = o_1 \odot \tanh(C_1) = [0.62, 0.41, 0.73, 0.28] \odot \tanh([0.14, -0.27, 0.06, 0.07]) \]
\[ = [0.62, 0.41, 0.73, 0.28] \odot [0.14,\ -0.26,\ 0.06,\ 0.07] \]
\[ = [0.09,\ -0.11,\ 0.04,\ 0.02] \]

\(h_1 = [0.09,\ -0.11,\ 0.04,\ 0.02]\) ← 这就是读完"我"之后的隐藏状态。


处理第2个词:"喜欢"

(a) 拼接\([h_1;\ x_2] = [0.09,\ -0.11,\ 0.04,\ 0.02,\ 0.65,\ 0.33,\ -0.18,\ 0.51]\)

(b) 四次矩阵乘法——用同一组权重矩阵(\(W_f, W_i, W_C, W_o\) 在所有时间步共享):

\[ f_2 = \sigma(W_f \cdot [h_1; x_2] + b_f) = [0.75,\ 0.88,\ 0.42,\ 0.61] \]
\[ i_2 = \sigma(W_i \cdot [h_1; x_2] + b_i) = [0.55,\ 0.36,\ 0.81,\ 0.47] \]
\[ \tilde{C}_2 = \tanh(W_C \cdot [h_1; x_2] + b_C) = [0.62,\ 0.19,\ -0.53,\ 0.71] \]
\[ o_2 = \sigma(W_o \cdot [h_1; x_2] + b_o) = [0.48,\ 0.67,\ 0.55,\ 0.39] \]

(c) 更新细胞状态——现在遗忘门开始起作用了:

\[ C_2 = f_2 \odot C_1 + i_2 \odot \tilde{C}_2 \]
\[ = [0.75, 0.88, 0.42, 0.61] \odot [0.14, -0.27, 0.06, 0.07] + [0.55, 0.36, 0.81, 0.47] \odot [0.62, 0.19, -0.53, 0.71] \]
\[ = [0.11,\ -0.24,\ 0.03,\ 0.04] + [0.34,\ 0.07,\ -0.43,\ 0.33] \]
\[ = [0.45,\ -0.17,\ -0.40,\ 0.37] \]

注意第三个维度:\(f_2[3] = 0.42\) 意味着遗忘门只保留了42%的旧信息(0.06 × 0.42 = 0.03),同时写入了新信息(-0.43)。这就是LSTM"选择性记忆"的过程。

(d) 输出隐藏状态

\[ h_2 = o_2 \odot \tanh(C_2) = [0.48, 0.67, 0.55, 0.39] \odot [0.42, -0.17, -0.38, 0.35] = [0.20,\ -0.11,\ -0.21,\ 0.14] \]

处理第3个词:"猫"

完全相同的过程:拼接 \([h_2; x_3]\) → 四次矩阵乘法 → 更新 \(C_3\) → 输出 \(h_3\)

\[ h_3 = [0.58,\ -0.07,\ 0.39,\ 0.44] \]

Step 3:提取上下文向量

\[ c = h_3 = [0.58,\ -0.07,\ 0.39,\ 0.44] \]

这就是上下文向量。整个句子"我喜欢猫"的全部信息都压缩在这4个数字里(真实场景是256~512个数字)。

回顾整个 Encoder 做了什么:

"我"   → 查嵌入表 → x₁ ─┐
                         ├→ [h₀; x₁] → Wf×+b→sigmoid → f₁ ─┐
                         │             Wi×+b→sigmoid → i₁  ├→ C₁, h₁
                         │             Wc×+b→tanh   → C̃₁  │
                         │             Wo×+b→sigmoid → o₁ ─┘
                         │                                ↓
"喜欢" → 查嵌入表 → x₂ ─┤→ [h₁; x₂] → 同样四次矩阵乘法  → C₂, h₂
                         │                                ↓
"猫"   → 查嵌入表 → x₃ ─┘→ [h₂; x₃] → 同样四次矩阵乘法  → C₃, h₃ = c
                                                              ↑
                                                         上下文向量

就是这样。 三个词,每个词做四次矩阵乘法(\(W \times \text{input} + b\),然后过激活函数),一共12次矩阵乘法。所有时间步共享同一组权重。Encoder 没有任何超出前向传播的操作。

需要理解的关键点:\(c\) 不是"中文句子的某种编码",也不是"可以在英文中查找匹配的索引"。它是一个抽象的语义表示——一组数字,在训练过程中被调整到"能够让解码器从中提取出足够的信息来生成正确翻译"。它的每个维度没有人类可读的含义,但包含了生成所需的全部信号。

那"理解"从何而来? 不是来自 Encoder 的结构,而是来自训练。在反向传播中,梯度会告诉 \(W_f, W_i, W_C, W_o\) 这些权重矩阵应该怎么调整,使得 \(h_3\) 刚好包含解码器生成正确翻译所需的信息。训练几十万个句子对后,这些权重就被调到了"恰好能让前向传播产生有用的语义表示"的状态。


第二阶段:解码(Decoding)——从向量中逐词"生"出英文

解码器是另一个独立的LSTM(参数与编码器不共享)。它的工作方式不是"检索"或"匹配",而是逐步生成——每一步从3万个英文词中选出概率最高的那个。

解码器有两个关键组件:

  1. 解码器LSTM:维护解码过程中的隐藏状态
  2. 输出投影层:一个矩阵 \(W_{\text{vocab}} \in \mathbb{R}^{30000 \times 4}\),把4维隐藏状态映射到3万维(每个英文词一个维度)

Step 1:生成第一个英文词

输入:  <BOS>(句首标记)的词向量: y₀ = Embed(<BOS>) = [0.01, 0.02, -0.01, 0.03]
初始:  h₀ᵈᵉᶜ = c = [0.58, -0.07, 0.39, 0.44]  ← 用上下文向量初始化!

(a) 解码器LSTM计算新的隐藏状态:

\[ h_1^{\text{dec}} = \text{LSTM}(h_0^{\text{dec}},\ C_0^{\text{dec}},\ y_0) = [0.71,\ 0.15,\ -0.23,\ 0.52] \]

(b) 输出投影——将4维隐藏状态映射到3万个词上:

\[ \text{logits} = W_{\text{vocab}} \cdot h_1^{\text{dec}} + b \in \mathbb{R}^{30000} \]

这一步做的是矩阵乘法。\(W_{\text{vocab}}\) 是一个 \(30000 \times 4\) 的矩阵,每一行对应一个英文词。矩阵乘法的本质是:计算当前隐藏状态与每个英文词的"相关度"分数

\[ \text{logits} = [\underset{\text{"I"}}{2.8},\ \underset{\text{"like"}}{0.3},\ \underset{\text{"cats"}}{-0.5},\ \underset{\text{"dog"}}{-1.2},\ \underset{\text{"the"}}{1.1},\ \ldots,\ \underset{\text{"happy"}}{0.7},\ \ldots] \]

(c) Softmax——将分数转为概率分布:

\[ P(y_1) = \text{softmax}(\text{logits}) = [\underset{\text{"I"}}{\mathbf{0.72}},\ \underset{\text{"like"}}{0.03},\ \underset{\text{"cats"}}{0.01},\ \underset{\text{"dog"}}{0.005},\ \underset{\text{"the"}}{0.08},\ \ldots] \]

所有3万个概率加起来 = 1。"I"的概率最高(0.72),所以模型输出 "I"。

为什么 "I" 的概率最高?因为上下文向量 \(c\) 包含了"第一人称主语"的信号,在大量训练数据中模型学会了:当 \(c\) 中包含这种信号且处于句首位置时,"I" 是最可能的第一个词。

Step 2:生成第二个英文词

输入:  "I" 的词向量: y₁ = Embed("I") = [0.55, 0.18, -0.42, 0.33]
状态:  h₁ᵈᵉᶜ = [0.71, 0.15, -0.23, 0.52]  ← 上一步的隐藏状态

(a) LSTM更新状态:

\[ h_2^{\text{dec}} = \text{LSTM}(h_1^{\text{dec}},\ C_1^{\text{dec}},\ y_1) = [0.38,\ 0.61,\ 0.09,\ -0.15] \]

此时 \(h_2^{\text{dec}}\) 编码了两部分信息:原始的中文语义(继承自 \(c\))+ 已经生成了 "I"(来自 \(y_1\) 的输入)。

(b) 投影 + Softmax:

\[ P(y_2) = \text{softmax}(W_{\text{vocab}} \cdot h_2^{\text{dec}}) = [\underset{\text{"I"}}{0.01},\ \underset{\text{"like"}}{\mathbf{0.68}},\ \underset{\text{"cats"}}{0.02},\ \underset{\text{"love"}}{0.15},\ \ldots] \]

"like"概率最高(0.68),输出 "like"。注意 "love" 的概率也不低(0.15),因为"喜欢"也可以翻译为 "love"——模型在这里做出了一个概率选择。

Step 3:生成第三个英文词

输入:  "like" 的词向量: y₂ = Embed("like")
状态:  h₂ᵈᵉᶜ
\[ h_3^{\text{dec}} = \text{LSTM}(h_2^{\text{dec}},\ C_2^{\text{dec}},\ y_2) \]
\[ P(y_3) = \text{softmax}(W_{\text{vocab}} \cdot h_3^{\text{dec}}) = [\ldots,\ \underset{\text{"cats"}}{\mathbf{0.61}},\ \underset{\text{"dogs"}}{0.12},\ \underset{\text{"cat"}}{0.09},\ \ldots] \]

"cats"概率最高,输出 "cats"。

Step 4:生成结束标记

\[ P(y_4) = \text{softmax}(W_{\text{vocab}} \cdot h_4^{\text{dec}}) = [\ldots,\ \underset{\text{<EOS>}}{\mathbf{0.91}},\ \ldots] \]

模型输出 <EOS>,翻译结束。最终结果:"I like cats"


解码过程的完整图示:

上下文向量 c ──→ 初始化解码器
                    │
                    ↓
<BOS> ─→ Embed ─→ [LSTM] ─→ h₁ ─→ W_vocab × h₁ ─→ softmax ─→ [0.72, 0.03, ...] ─→ "I"
                                                                 ↑ 3万个词的概率      选最大
                    ↓
"I" ──→ Embed ──→ [LSTM] ─→ h₂ ─→ W_vocab × h₂ ─→ softmax ─→ [0.01, 0.68, ...] ─→ "like"
                                                                                     选最大
                    ↓
"like" → Embed ──→ [LSTM] ─→ h₃ ─→ W_vocab × h₃ ─→ softmax ─→ [..., 0.61, ...]  ─→ "cats"
                                                                                     选最大
                    ↓
"cats" → Embed ──→ [LSTM] ─→ h₄ ─→ W_vocab × h₄ ─→ softmax ─→ [..., 0.91, ...]  ─→ <EOS>
                                                                                     停止

一个常见的误解需要澄清: 解码器不是在"查找"或"匹配"——它不会在英文词表中找一个和 \(c\) 最像的词。它做的是条件生成:在给定 \(c\)(原文含义)和已生成词的条件下,通过 \(W_{\text{vocab}}\) 这个学出来的映射,计算下一个词的概率分布。\(W_{\text{vocab}}\) 的每一行可以理解为"某个英文词需要什么样的隐藏状态才会被选中",而LSTM则负责在每一步产生合适的隐藏状态。整个过程是一个条件语言模型

\[ P(\text{"I like cats"} | \text{"我喜欢猫"}) = P(\text{"I"}|c) \times P(\text{"like"}|\text{"I"}, c) \times P(\text{"cats"}|\text{"like, I"}, c) \times P(\text{<EOS>}|\text{"cats, like, I"}, c) \]

训练 vs 推理的区别

训练时使用 Teacher Forcing:不管模型自己预测了什么,每一步都把真实的目标词喂给下一步。这样做可以加速收敛,防止早期错误的预测雪球式传播。

训练时:  <BOS> → [LSTM] → 预测"I"  ✓
         "I"   → [LSTM] → 预测"like" ✓   ← 喂入的是真实标签"I",不是模型的预测
         "like"→ [LSTM] → 预测"dogs" ✗   ← 即使预测错了
         "cats"→ [LSTM] → 预测"<EOS>"     ← 仍然喂入真实标签"cats"继续训练

推理时:  <BOS> → [LSTM] → 预测"I"
         "I"   → [LSTM] → 预测"like"      ← 喂入的是上一步自己的预测
         "like"→ [LSTM] → 预测"cats"
         "cats"→ [LSTM] → 预测"<EOS>"     ← 如果中途预测错了,后面会一错到底

这也暴露了Seq2Seq的另一个问题:训练和推理的输入分布不一致(Exposure Bias)。训练时解码器总是看到正确答案,推理时却要依赖自己可能出错的预测。

固定向量瓶颈(Information Bottleneck)

核心问题:不管源语言句子有多长(10个词还是100个词),所有信息都被压缩到一个固定长度的向量 \(c\) 中。

这就像要求你读完一整本书后,只能写一句话来概括,然后另一个人要根据这句话把整本书翻译出来。

实验证据:Cho et al. (2014) 发现,当句子长度超过20-30个词时,Seq2Seq的BLEU分数急剧下降。

这个瓶颈直接催生了注意力机制(Attention Mechanism)——让解码器不再只依赖一个压缩向量,而是可以在生成每个词时"回头看"编码器的所有隐藏状态。


传统NLP的局限性总结

方法 核心局限 后续解决方案
One-Hot / 词袋 / TF-IDF 无语义信息、丢失词序 Word2Vec / GloVe
Word2Vec / GloVe 静态嵌入,一词多义无法处理 ELMo / BERT(上下文嵌入)
RNN 梯度消失,长距离依赖困难 LSTM / GRU
LSTM / GRU 仍然是顺序处理,无法并行;极长序列仍有困难 Attention Mechanism
Seq2Seq 固定向量瓶颈 Attention
RNN + Attention 仍然无法并行训练 Transformer(完全基于Attention)

Transformer的诞生逻辑:既然Attention已经成为最重要的组件,RNN反而成了并行化的瓶颈,那为什么不完全抛弃RNN,只用Attention?这就是2017年那篇 "Attention is All You Need" 的核心思想。

下一步:→ 注意力机制(详解 Attention 的数学原理)


评论 #