从RNN到Transformer:注意力机制原理与200行Python实现

发布时间:2026/7/28 16:34:15

从RNN到Transformer:注意力机制原理与200行Python实现 如果你在2017年之前问一个NLP研究员,如何让模型理解“猫坐在垫子上”这句话,他大概率会跟你聊起RNN、LSTM和GRU。这些模型像一台老式打字机,一个字一个字地处理,上一个字处理完才能打下一个。这种“顺序处理”的机制,让模型在处理长文本时,不仅速度慢,还容易“忘记”开头说了什么。2017年,一篇名为《Attention Is All You Need》的论文横空出世,它提出的Transformer架构,彻底抛弃了这种顺序处理模式。它不再一个字一个字地“读”,而是让句子里的所有字同时“互相看”,瞬间捕捉全局关系。这个看似简单的改变,却引爆了此后至今的AI革命:GPT、BERT、ChatGPT、Sora、Stable Diffusion……这些改变世界的模型,其核心无一不是Transformer。但问题来了:网上关于Transformer的教程汗牛充栋,为什么你还需要读这一篇?因为大多数文章要么陷入数学公式的泥潭,让人望而生畏;要么过于简化,只告诉你“注意力机制很牛”,却说不清它到底如何工作、为什么能工作,以及在实际项目中如何从零构建一个。本文将采取一种**“从问题出发,到代码落地”**的路径。我们不会满足于复述论文,而是会拆解三个核心问题:Transformer到底解决了RNN的什么根本性瓶颈?(不仅仅是“并行化”那么简单)“注意力”这个抽象概念,在代码层面究竟是如何计算和实现的?如何用不到200行Python代码,实现一个可运行的、结构清晰的迷你Transformer?本文的目标是:让你不仅能理解Transformer的原理,更能亲手“搭”出一个,并清楚地知道每一行代码在做什么。我们开始吧。1. 从RNN的困境到Transformer的破局要理解Transformer的价值,必须回到它诞生前的时代背景。在Transformer之前,序列建模(如机器翻译、文本生成)的王者是循环神经网络(RNN)及其变体LSTM/GRU。它们的工作原理可以想象为一条传送带:模型依次读取输入序列的每个词(如“猫”、“坐”、“在”、“垫子”、“上”),每读一个词,就更新一次内部的“记忆状态”。这个状态是一个固定长度的向量,试图浓缩之前所有词的信息。RNN的核心瓶颈在于“信息瓶颈”和“顺序依赖”:长距离依赖丢失:当句子很长时(比如50个词),开头的“猫”信息需要经过几十步计算才能传递到句末。这就像传话游戏,传到最后信息早已失真或丢失。这就是所谓的“长期依赖”问题。LSTM通过“门控”机制缓解了此问题,但未能根除。无法并行计算:由于必须等第t个词处理完,才能处理第t+1个词,计算本质上是串行的。这极大限制了在GPU等并行硬件上的训练速度。注意力机制的初现:2014-2015年,研究者们在Seq2Seq模型中引入了注意力机制。它允许解码器在生成每一个目标词时,直接“回顾”编码器对所有输入词的隐藏状态,并动态决定关注哪些输入词。这解决了“信息瓶颈”问题,模型不再需要将所有信息压缩进一个固定向量。但此时的注意力是加在RNN之上的,计算本身仍然是顺序的。Transformer的颠覆性思想:2017年的论文提出了一个激进的想法:既然注意力机制如此强大,我们能不能完全抛弃循环结构,只靠注意力来构建模型?这就是“Attention Is All You Need”的含义。Transformer的解决方案简单而暴力:彻底并行:放弃循环,让模型在第一步就同时看到整个输入序列的所有词。自注意力(Self-Attention):不仅让解码器关注编码器,更让序列中的每个词都去关注序列中的所有其他词(包括自己),从而直接建模任意两个词之间的关系,无论它们相距多远。这种设计带来了两个革命性优势:完美的并行能力:所有词对之间的注意力分数可以同时计算,充分利用GPU的矩阵运算能力,训练速度比RNN快一个数量级。强大的远程建模能力:任意两个词的距离在计算上都是“1步”,直接解决了长距离依赖问题。下面这张图直观展示了从RNN到Transformer的范式转变: (想象图:左侧是RNN的链式顺序处理,右侧是Transformer的全连接注意力矩阵)2. Transformer核心架构全景拆解一个标准的Transformer模型(原始论文中的Encoder-Decoder结构)就像一座精密的工厂,我们将其拆解为以下几个核心车间:2.1 整体工作流程(以翻译任务“Hello World” - “你好 世界”为例)输入处理:将“Hello World”转换成数字ID(Tokenization),然后通过词嵌入层转换为稠密向量,并加上位置编码以注入词序信息。编码器堆栈(Encoder Stack):处理输入序列。由N个(原论文N=6)相同的层堆叠而成。每一层都包含一个多头自注意力机制和一个前馈神经网络,并配有残差连接和层归一化。解码器堆栈(Decoder Stack):生成输出序列“你好 世界”。也由N个相同的层堆叠。每层包含三个子层:掩码多头自注意力层:确保在生成第t个词时,只能看到前面t-1个已生成的词(防止信息泄露)。编码器-解码器注意力层(又称交叉注意力):让解码器中的词去“询问”编码器输出的最终表示。前馈神经网络。输出层:解码器最后的输出通过一个线性层和Softmax,映射到目标词汇表上,得到每个位置是每个词的概率,从而生成“你好”、“世界”。2.2 核心组件深度解析2.2.1 词嵌入与位置编码模型无法直接理解文字,所以第一步是词元化(Tokenization),将文本切分成子词或词片段,并映射为整数ID。然后通过一个可学习的嵌入层,将每个ID转换为一个d_model维的向量(例如512维)。但注意力机制本身是无序的,它不知道“猫”在“垫子”前面。因此必须显式地加入位置信息。Transformer使用了正弦余弦位置编码:import numpy as np def get_positional_encoding(seq_len, d_model): """生成位置编码矩阵,形状为 (seq_len, d_model)""" pe = np.zeros((seq_len, d_model)) position = np.arange(seq_len).reshape(-1, 1) # (seq_len, 1) div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model)) # (d_model/2,) pe[:, 0::2] = np.sin(position * div_term) # 偶数维度用sin pe[:, 1::2] = np.cos(position * div_term) # 奇数维度用cos return pe # 示例:序列长度10,模型维度512 pos_encoding = get_positional_encoding(10, 512) print(pos_encoding.shape) # 输出: (10, 512)这种编码的妙处在于:对于任意固定的偏移量k,PE(pos+k)可以表示为PE(pos)的线性函数,这使得模型能够轻松学习到相对位置关系。最终,输入模型的向量 =词嵌入向量 + 位置编码向量。2.2.2 缩放点积注意力(Scaled Dot-Product Attention)这是Transformer的灵魂。其核心思想是:对于序列中的每一个词(称为“查询”Query),计算它与序列中所有词(作为“键”Key)的相关性分数,然后用这个分数作为权重,对所有词对应的“值”Value进行加权求和,从而得到一个融合了全局上下文的新表示。计算步骤(矩阵形式):将输入序列矩阵X(形状[seq_len, d_model])通过三个不同的线性层,投影到Query、Key、Value空间:Q = XW^Q,K = XW^K,V = XW^V。计算注意力分数:scores = Q @ K.T / sqrt(d_k)。除以sqrt(d_k)是为了防止点积结果过大,导致Softmax梯度消失。对分数矩阵应用Softmax,得到注意力权重(每行和为1):attention_weights = softmax(scores, dim=-1)。输出是权重对V的加权和:output = attention_weights @ V。import torch import torch.nn.functional as F def scaled_dot_product_attention(Q, K, V, mask=None): """ 计算缩放点积注意力。 参数: Q: 查询矩阵,形状 (..., seq_len_q, d_k)

相关新闻