NLP 学习实践:从 RNN 到 Transformer
六个递进项目,覆盖从简单循环网络到自注意力机制的完整技术栈。每一行代码都是手写实现,每一个模型都能独立训练。
模型的进化:一条清晰的梯度流
时序记忆单元
长程依赖 · 双向对比
并行化 · 全局依赖
字符级文本预测:RNN 输入法模拟
基于前 5 个汉字预测下一个字——这是所有序列模型的起手式。
真正理解 RNN 不是通过论文,而是通过这段代码:output[:, -1, :] 从 [batch, 5, 256] 中取最后一个时间步作为「整个序列的表示」。这个操作后来在 LSTM/GRU 中被替换为取最后一个非 pad token 的隐藏状态——从固定长度到变长序列,这是 NLP 工程的第一个分水岭。
情感分析:LSTM vs GRU 对比实验
同一个二分类任务,两种门控架构——这是设计者刻意为之的对照实验。
2 门结构 · 无细胞状态 · 计算更高效 · 小数据集上常优于 LSTM
3 门结构 · 细胞状态 · 长程记忆更强 · 参数量约为 GRU 的 1.3 倍
两套代码共享完全相同的训练管道:Embedding(128) → RNN(256) → Linear(1),唯一的变量就是中间的 RNN 类型。这正好验证了一个经典结论:GRU 和 LSTM 在多数中小规模任务上性能接近,但 GRU 收敛更快。变长序列的处理——lengths = (x != padding_idx).sum(dim=1) 取真实长度——是这两个项目中最重要的工程细节,因为这个操作在后续的 Seq2Seq 中被反复使用。
英中翻译:三次架构跃迁
从最基本的编码器-解码器到完整的 Transformer,同一个翻译任务被实现了三次——每次解决前一个版本的致命缺陷。
Seq2Seq · 编码器-解码器
30 epochs
Encoder:中文 Embedding → GRU → 取最后一个时间步的隐藏状态作为上下文向量 context_vector。
Decoder:英文 Embedding → GRU → Linear(vocab_size),每次只处理一个 token。context_vector 作为 decoder 的初始隐藏状态 h0 传入。
Attention · Bahdanau 注意力
30 epochs编码器不再只输出最后一个隐藏状态,而是保留所有时间步的输出(shape 从 [batch, 256] 变为 [batch, seq_len, 256])。解码器在每个时间步计算对编码器所有位置的注意力权重,动态聚合上下文。
关键改造:Decoder 的 Linear 输入维度从 hidden_size(256) 变为 2*hidden_size(512)——拼接 GRU 输出与注意力上下文向量。这个看似简单的维度翻倍,解决了 v1 的信息瓶颈。bmm 操作要求两矩阵第一维相等(batch_size)、第二三维做矩阵乘法——调试时最容易卡住的地方是维度顺序。
Transformer · Self-Attention
30 epochs · Flask Web 部署
告别 RNN 的串行依赖。用 PyTorch 的 nn.Transformer 直接搭建:dim_model=128, num_heads=4, 2 encoder + 2 decoder layers。手写 PositionEncoding(sin/cos 公式逐元素计算)替代 RNN 的隐式时序建模。
🚀 Web 部署:搭建了 Flask + CORS 推理服务,后台线程加载模型(threading.Thread),/health 健康检查端点,支持中文→英文实时翻译。前后端分离设计——这是整个 NLP 学习路径中唯一实现工程化部署的项目。
统一的技术基座
PyTorch
nn.RNN / GRU / LSTM
nn.Transformer
Jieba
中文分词器
构建 5-gram 训练对
Dataset 基类
自定义 tokenizer
padding & 变长序列
Flask
Transformer Web
后台线程 + CORS
TensorBoard
loss/acc 曲线
tqdm 进度条
bmm
批量矩阵乘法
注意力核心算子
Embedding
128 维词向量
padding_idx 屏蔽
CrossEntropy
序列分类 & 翻译
统一损失函数
三行代码,三个认知跃迁
01 · output[:, -1, :]
RNN 输入法的核心操作:取时间维最后一个元素的隐藏状态作为整个序列的表示。这个简单的切片操作蕴含了 RNN 对"顺序"的基本假设——最后一个时间步看过所有前面的信息。但这也暴露了 RNN 的根本局限:序列开头的信息经过多次非线性变换后已严重衰减。理解了这一点,就理解了 LSTM 的门控机制为什么诞生。
02 · lengths = (x != padding_idx).sum(dim=1)
情感分析中的一个看似不起眼的操作:统计每个样本中非 pad token 的数量,用这个长度去取最后一个有效时间步的隐藏状态。这个操作是变长序列处理的万能钥匙——从情感分析到机器翻译,padding + masking 的组合贯穿了整个 NLP 工程。不理解这个,就看不懂为什么 Transformer 需要 src_pad_mask 和 tgt_mask 两种不同的 mask。
03 · torch.bmm(Q, K.transpose(1,2))
Attention 机制的唯一核心运算:Decoder 当前隐藏状态与 Encoder 所有时间步输出的批量矩阵乘法。Q·K^T 算出注意力分数,softmax 归一化得到权重,再乘以 V(Encoder 输出)得到上下文向量。bmm 要求第一维(batch)相同、第二三维做矩阵乘法——这个维度约束是实际调试时最折磨人的地方,也是真正理解"张量运算"而非"调包"的分界线。
From RNN to Transformer
六个项目、60+ 源文件、三次架构跃迁——这就是理解 NLP 的最短路径。