Avatar
NATURAL LANGUAGE PROCESSING

NLP 学习实践:从 RNN 到 Transformer

六个递进项目,覆盖从简单循环网络到自注意力机制的完整技术栈。每一行代码都是手写实现,每一个模型都能独立训练。

PyTorch NLP Transformer Seq2Seq
nlp_learning.py
# 6 projects · 60+ Python files · RNN → LSTM → GRU → Attention → Transformer
class NLPLearningJourney:
"""从字符预测到 Transformer 翻译——NLP 的完整学习路径"""
projects = ['input_method_RNN', 'review_LSTM_GRU', 'translation_Seq2Seq_Attention_Transformer']
print("Ready.")
# architecture_flow

模型的进化:一条清晰的梯度流

🧠
RNN
循环神经网络
时序记忆单元
1 project
🔁
LSTM / GRU
门控机制
长程依赖 · 双向对比
2 projects
Transformer
Self-Attention
并行化 · 全局依赖
3 projects
Embedding(vocab,128) GRU(256) · bidir=False Multi-Head(4) · d=128
# project_01 — input_method_RNN

字符级文本预测:RNN 输入法模拟

基于前 5 个汉字预测下一个字——这是所有序列模型的起手式。

# model.py — InputMethodModel
self.embedding = nn.Embedding(vocab_size, 128)
self.rnn = nn.RNN(128, 256, batch_first=True)
self.linear = nn.Linear(256, vocab_size)
# 取最后一个时间步的隐藏状态映射到词表
last_hidden = output[:, -1, :]
return self.linear(last_hidden)
Jieba 分词器
SEQ_LEN = 5
BATCH = 64
EPOCHS = 10
Adam lr=1e-3
CrossEntropyLoss

真正理解 RNN 不是通过论文,而是通过这段代码:output[:, -1, :] 从 [batch, 5, 256] 中取最后一个时间步作为「整个序列的表示」。这个操作后来在 LSTM/GRU 中被替换为取最后一个非 pad token 的隐藏状态——从固定长度到变长序列,这是 NLP 工程的第一个分水岭。

# project_02_03 — review_analyze_{LSTM,GRU}

情感分析:LSTM vs GRU 对比实验

同一个二分类任务,两种门控架构——这是设计者刻意为之的对照实验。

▸ review_analyze_LSTM/
# 注意:这个文件夹实际使用 nn.GRU
self.gru = nn.GRU(128, 256, batch_first=True)
self.linear = nn.Linear(256, 1)
# GRU 只有两个门(重置门+更新门)
# 比 LSTM 少一个门,参数更少
output = self.linear(last_hidden).squeeze(1)
# output.shape: [batch_size]

2 门结构 · 无细胞状态 · 计算更高效 · 小数据集上常优于 LSTM

▸ review_analyze_GRU/
# 注意:这个文件夹实际使用 nn.LSTM
self.lstm = nn.LSTM(128, 256, batch_first=True)
self.linear = nn.Linear(256, 1)
# LSTM 三个门(遗忘+输入+输出)
# 额外维护一个细胞状态 c_t
output, (h_n, c_n) = self.lstm(embed)
# 返回两个状态: h_n + c_n

3 门结构 · 细胞状态 · 长程记忆更强 · 参数量约为 GRU 的 1.3 倍

两套代码共享完全相同的训练管道:Embedding(128) → RNN(256) → Linear(1),唯一的变量就是中间的 RNN 类型。这正好验证了一个经典结论:GRU 和 LSTM 在多数中小规模任务上性能接近,但 GRU 收敛更快。变长序列的处理——lengths = (x != padding_idx).sum(dim=1) 取真实长度——是这两个项目中最重要的工程细节,因为这个操作在后续的 Seq2Seq 中被反复使用。

# project_04_05_06 — translation_{Seq2Seq,Attention,Transformer}

英中翻译:三次架构跃迁

从最基本的编码器-解码器到完整的 Transformer,同一个翻译任务被实现了三次——每次解决前一个版本的致命缺陷。

v1

Seq2Seq · 编码器-解码器

30 epochs

Encoder:中文 Embedding → GRU → 取最后一个时间步的隐藏状态作为上下文向量 context_vector。
Decoder:英文 Embedding → GRU → Linear(vocab_size),每次只处理一个 token。context_vector 作为 decoder 的初始隐藏状态 h0 传入。

# 核心缺陷:整个源句子的信息被压缩成一个 256 维向量
# 长句子翻译时 information bottleneck 导致性能骤降
v2

Attention · Bahdanau 注意力

30 epochs

编码器不再只输出最后一个隐藏状态,而是保留所有时间步的输出(shape 从 [batch, 256] 变为 [batch, seq_len, 256])。解码器在每个时间步计算对编码器所有位置的注意力权重,动态聚合上下文。

# Attention 的核心:三维批量矩阵乘法
attention_scores = torch.bmm(decoder_hidden, encoder_outputs.transpose(1, 2))
attention_weights = torch.softmax(attention_scores, dim=-1)
context = torch.bmm(attention_weights, encoder_outputs)

关键改造:Decoder 的 Linear 输入维度从 hidden_size(256) 变为 2*hidden_size(512)——拼接 GRU 输出与注意力上下文向量。这个看似简单的维度翻倍,解决了 v1 的信息瓶颈。bmm 操作要求两矩阵第一维相等(batch_size)、第二三维做矩阵乘法——调试时最容易卡住的地方是维度顺序。

v3

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 的隐式时序建模。

class PositionEncoding(nn.Module):
# 手工实现 sin/cos 位置编码
self.pe[pos, i] = sin(pos / 10000^(2i/d))
self.pe[pos, i+1] = cos(...)
# 完整推理管线
def encode(src, src_pad_mask):
embed = zh_embedding + position_encoding
return transformer.encoder(embed, src_pad_mask)
def decode(...):
return transformer.decoder(tgt, memory, tgt_mask)

🚀 Web 部署:搭建了 Flask + CORS 推理服务,后台线程加载模型(threading.Thread),/health 健康检查端点,支持中文→英文实时翻译。前后端分离设计——这是整个 NLP 学习路径中唯一实现工程化部署的项目。

# tech_summary

统一的技术基座

code

PyTorch

nn.RNN / GRU / LSTM
nn.Transformer

translate

Jieba

中文分词器
构建 5-gram 训练对

dataset

Dataset 基类

自定义 tokenizer
padding & 变长序列

dns

Flask

Transformer Web
后台线程 + CORS

monitoring

TensorBoard

loss/acc 曲线
tqdm 进度条

compare_arrows

bmm

批量矩阵乘法
注意力核心算子

layers

Embedding

128 维词向量
padding_idx 屏蔽

functions

CrossEntropy

序列分类 & 翻译
统一损失函数

# key_insights

三行代码,三个认知跃迁

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 的最短路径。

arrow_back Back to Works