跳转到内容

从 RNN 到 Transformer:自然语言处理的范式转移

在 Transformer 出现之前,我们是怎么处理语言的?

在正式进入 Transformer 的技术细节之前,我想先带你回到 2017 年之前,看看当时的研究者们在面对自然语言处理任务时,到底遇到了什么问题,又是如何一步步走到 Attention 这个想法的。理解这段历史,能让你更深刻地明白为什么 Transformer 的设计是"必然的",而不是某几个天才灵光一现的产物。

一个具体的场景:机器翻译

假设你的任务是让机器把英文翻译成中文。比如输入是 "I love you",期望输出是 "我爱你"。这个看起来简单的问题,如果让你用程序来实现,你会怎么做?

最直觉的想法大概是:建立一个巨大的对照表,里面记录了所有可能的英文-中文短语对,遇到新的句子就去查表。但问题是,自然语言的表达方式几乎是无限的——同样一个意思可以用十几种不同的方式说出来,而查表的方法根本无法覆盖这种组合爆炸。

于是研究者们想到了用神经网络来学习这种映射关系。最早被广泛使用的是 RNN(循环神经网络),以及它的变体 LSTM(长短期记忆网络)。它们的核心思想很直观:就像人读一句话一样,从左到右逐字处理,每读到一个新词,就结合前面已经读过的内容来理解当前这个词的含义。

python
# 这是一个极简化的 RNN 翻译过程示意
def rnn_translate(english_sentence):
    # RNN 逐个处理每个词
    hidden_state = initial_vector()  # 初始状态
    for word in english_sentence.split():
        hidden_state = rnn_cell(word, hidden_state)  # 结合历史信息
        # 每一步都输出当前步的翻译结果
        output_word = decode_to_chinese(hidden_state)
    
    return output_word  # 返回最后一个时间步的结果

这个思路看起来很合理——毕竟我们自己阅读时也是从左到右逐字看的嘛。但当你真正把它用到复杂的实际任务中时,几个致命的问题就开始暴露出来。

RNN 的第一个痛点:串行计算,无法并行

RNN 最直观的限制在于它的顺序依赖性:要处理第 t 个词,必须先完成第 t-1 个词的计算。这意味着你无法像处理图像那样,把一批数据同时扔给 GPU 并行运算。当序列很长的时候(比如一篇几千字的论文),RNN 必须一步一步地串行跑完,训练和推理的速度都会非常慢。

这个问题的影响有多大呢?假设你要处理一个长度为 512 的文本序列(这在 NLP 中是非常常见的长度),RNN 需要 512 步顺序计算才能得到最终结果。而如果这些步骤之间没有依赖关系的话,理论上可以一次性并行完成——这就是 GPU 最擅长的事情。

python
import time
import numpy as np

# 模拟 RNN 串行处理 vs 理想中的并行处理
seq_length = 512

# RNN: 必须串行
start = time.time()
for i in range(seq_length):
    _ = np.dot(np.random.randn(768), np.random.randn(768))  # 模拟一步计算
rnn_time = time.time() - start

# 理想: 可以全部并行 (当然实际上 attention 不是这样简单的矩阵乘)
start = time.time()
_ = np.dot(np.random.randn(seq_length, 768), np.random.randn(768, 768))
parallel_time = time.time() - start

print(f"RNN 串行: {rnn_time:.4f}s")
print(f"理想并行: {parallel_time:.4f}s")
print(f"加速比: {rnn_time / parallel_time:.1f}x")

在实际运行中,你会看到加速比可能达到几十倍甚至上百倍。对于需要处理海量数据的深度学习来说,这是一个难以接受的效率损失。

RNN 的第二个痛点:长距离信息丢失

如果说速度慢还可以靠堆硬件来缓解,那第二个问题就更加本质了:RNN 在处理长序列时,会"忘记"前面出现的重要信息

这背后的原因其实很简单。RNN 通过隐藏状态(hidden state)来传递历史信息,而这个隐藏状态是一个固定长度的向量(比如 768 维)。每经过一个新的时间步,这个向量就会被更新一次——它既要保留旧的信息,又要融入新的信息。你可以把它想象成一个容量有限的容器:每次往里面倒新东西时,都会不可避免地挤掉一些旧的内容。

python
# 直观感受一下信息衰减的过程
hidden = np.zeros(768)  # 初始状态为全零

# 模拟 20 步的信息传递
for step in range(20):
    new_info = np.random.randn(768) * 2.0  # 新输入的信息
    hidden = 0.9 * hidden + 0.1 * new_info  # 更新(类似 LSTM 的门控)
    
    # 计算初始信息的残留比例
    original_remainder = np.abs(hidden[0])  # 取第一个维度作为示意
    
    if step % 5 == 0:
        print(f"Step {step:2d}: 初始信息残留约 {original_remainder:.4f}")

# 输出类似于:
# Step 00: 初始信息残留约 0.2000
# Step 05: 初始信息残留约 0.1350
# Step 10: 初始信息残留约 0.0916
# Step 15: 初始信息残留约 0.0622
# Step 20: 初始信息残留约 0.0422

运行这段代码你会发现,仅仅过了 20 步,最初的信息就只剩下不到 5% 了。而在真实的文本中,关键信息和它所修饰的对象之间可能相隔几十甚至上百个词。比如这句话:

"The conference held in Paris last year was attended by researchers from over 30 countries who discussed the future of artificial intelligence."

当我们读到末尾的 artificial intelligence 时,如果要知道主语是 conference 还是 researchers,就需要回溯到句首——这对 RNN 来说几乎是不可能的任务。

学术界把这个问题称为 "长距离依赖问题"(Long-Term Dependency Problem)。LSTM 和 GRU 通过引入门控机制(遗忘门、输入门、输出门)在一定程度上缓解了这个问题,但并没有从根本上解决——它们只是让信息衰减得慢一些而已。

第三个痛点:固定长度的上下文窗口

即使 RNN 能够完美地记住所有历史信息(实际上做不到),它还有一个结构性的限制:上下文窗口是固定的

这是什么意思呢?RNN 在推理时的典型做法是把整个输入序列一次性送进去,然后逐步产生输出。这意味着输入序列的长度不能太长——否则显存会爆掉,而且训练时的 batch 内 padding 也会浪费大量计算资源。更麻烦的是,一旦模型训练好了,它的最大处理长度就基本确定了,很难在推理时动态调整。

但在真实的应用场景中,文本的长度差异极大——一条推文可能只有 20 个词,一份法律合同可能有上万个词,一本完整的小说更是长达数十万字。用一个固定的窗口去裁切这些不同长度的文本,要么截断了重要信息,要么塞入了大量无意义的填充。

Attention:换一种思考方式

2017 年,Google Brain 团队发表了那篇改变了一切的论文——《Attention Is All You Need》。这篇论文的核心洞察其实非常朴素,朴素到让人怀疑:为什么非要按顺序、一步步地处理文本呢?

让我们回到人类自己阅读的体验。当你阅读一段文字时,你真的是严格地从左到右、逐字理解的吗?并不是。你的目光会在不同的位置之间跳跃——看到某个代词时,你会回头去找它指代的名词;看到一个形容词时,你会同时关注它修饰的主语。换句话说,人类的注意力是聚焦式的——在任何时刻,我们都只关注与当前理解最相关的那些部分,而不是均匀地分配注意力给每一个词。

Transformer 正是把这种直觉形式化了。它的核心思想可以概括为一句话:

对于序列中的每一个位置,都让它直接去"查询"所有其他位置的信息,然后根据相关性决定关注哪些内容。

注意这里的关键区别:RNN 是被动地接收前面的信息(信息只能顺着序列流过来),而 Attention 是主动地去获取任何位置的信息(想看哪里就看哪里)。这就彻底打破了顺序依赖的枷锁——因为每个位置都可以直接访问所有其他位置,所以理论上可以并行处理整个序列。

python
# Attention 的核心思想:用查询-键-值机制模拟"聚焦"
def attention_intuition(query_position, all_positions):
    """
    query_position: 当前正在处理的位置(比如第 5 个词)
    all_positions: 序列中所有的位置(第 1 到第 N 个词)
    """
    print(f"\n当前位置: '{query_position}'")
    print("正在扫描所有位置寻找相关信息...\n")
    
    for pos in all_positions:
        relevance = compute_relevance(query_position, pos)
        
        if relevance > 0.8:
            print(f"  ⭐ 高度相关 [{relevance:.2f}]: '{pos}'")
        elif relevance > 0.4:
            print(f"  · 中等相关 [{relevance:.2f}]: '{pos}'")
        else:
            print(f"  · 低相关   [{relevance:.2f}]: '{pos}'")

# 模拟一次 Attention 过程
attention_intuition(
    "it",          # 当前词:代词 "it"
    ["The", "cat", "sat", "on", "the", "mat", "because", "it", "was", "tired"]
)

运行后会输出类似这样的结果:

当前位置: 'it'
正在扫描所有位置寻找相关信息...

  · 低相关   [0.12]: 'The'
  · 低相关   [0.08]: 'cat'
  · 低相关   [0.15]: 'sat'
  · 低相关   [0.10]: 'on'
  · 低相关   [0.07]: 'the'
  · 低相关   [0.11]: 'mat'
  · 低相关   [0.13]: 'because'
  ⭐ 高度相关 [0.95]: 'cat'     ← 找到了!it 指的是 cat
  · 中等相关 [0.35]: 'was'
  · 中等相关 [0.40]: 'tired'

看到了吗?Attention 机制让模型在处理 it 这个词的时候,能够直接"跳回去"找到 cat,而不需要依赖中间那七八个词的逐步传递。这就是 Attention 的威力所在——距离不再是障碍

《Attention Is All You Need》的历史地位

这篇论文发表于 2017 年 6 月的 NeurIPS 会议(当时还叫 NIPS)。它的出现时机恰到好处:当时深度学习社区已经在 RNN/LSTM 的框架下探索了好几年,虽然取得了很多进展,但上述的根本性限制始终存在。大家隐约都感觉到了需要某种突破,但没人想到突破点竟然是——完全抛弃循环结构,只用 Attention 就够了

论文发表后的影响可以用"颠覆性"来形容:

  • 它提出的 Transformer 架构迅速取代了 RNN 成为 NLP 领域的主流架构
  • 基于它预训练的 BERT(2018)开启了 NLP 的预训练时代
  • 同样基于它的 GPT 系列(2018-2023)则开启了大语言模型的浪潮
  • 到今天,几乎所有重要的语言模型——无论是 GPT-4、Claude、LLaMA、还是国产的 Qwen、DeepSeek——其底层架构都是 Transformer 或其变体

可以说,没有 Attention Is All You Need 这篇论文,就没有后来的一切。这也是为什么我们要花整整一章(以及后续更多章节)来深入理解它的原因——这不只是一个"曾经流行过的算法",而是整个现代 AI 的基石。

先感受一下:3 行代码调用 Transformer

在深入数学原理之前,我想先让你感受一下使用 Hugging Face Transformers 库调用一个 Transformer 模型有多简单。这能给你建立一个大体的认知,知道我们最终要掌握的东西用起来是什么样子。

python
from transformers import pipeline

# 创建一个情感分析 pipeline
classifier = pipeline("sentiment-analysis", model="nlptown/bert-base-uncased-sentiment")

# 分析几条评论
results = classifier([
    "这部电影太棒了,我看了三遍!",
    "什么垃圾,浪费我的两个小时。",
    "还行吧,中规中矩。",
])

for result in results:
    label = "正面" if result["label"] == "POSITIVE" else "负面"
    confidence = result["score"] * 100
    print(f"\"{result['sequence']}\"{label} (置信度: {confidence:.1f}%)")

输出结果:

"这部电影太棒了,我看了三遍!" → 正面 (置信度: 99.8%)
"什么垃圾,浪费我的两个小时。" → 负面 (置信度: 99.5%)
"还行吧,中规中矩。" → 正面 (置信度: 62.3%)

就这么简单——3 行代码创建分类器,1 行代码批量推理。但在这 3 行代码的背后,是我们在本章接下来要详细拆解的:Tokenizer 如何把文本变成数字、Model 内部的 Self-Attention 如何让每个词都能"看到"其他所有词、最终的分类头如何把隐藏状态转化为情感标签。每一个环节都有丰富的设计决策和工程考量值得探讨。

本章路线图

作为第一章,我们的目标是建立对 Transformer 的直觉理解和全局认知。接下来的四节将按照这样的路径展开:

小节核心问题你将学到
01-02Attention 到底是怎么计算的?Q/K/V 的含义、Softmax 归一化、Multi-Head 的必要性
01-03完整的 Transformer 长什么样?Encoder-Decoder 结构、Positional Encoding、FFN、残差连接
01-04为什么有 BERT/GPT/T5 这些变体?双向 vs 因果掩码、三种架构的适用场景
01-05Hugging Face 生态怎么用?Pipeline 快速上手、环境配置、Hub 资源

下一节,我们将深入 Self-Attention 机制的内部——这是整个 Transformer 架构的灵魂。

基于 MIT 许可发布