文本的 CNN 与 RNN


文档摘要

文本的 CNN 与 RNN 本节摘要:卷积学 n-gram,循环记住过去,两者都被注意力取代,但两者在受约束的硬件上仍然要紧。TF-IDF 和 Word2Vec 产出的是无视词序的扁平向量,建在其上的分类器分不清 和 ,而词序有时恰恰就是信号。Transformer 到来前,有两个架构家族补上了这个缺口:文本卷积网(TextCNN)在词嵌入序列上做一维卷积,宽 3 的滤波器就是一个可学的三元检测器,堆叠不同宽度再最大池化,扁平、并行、快;循环网(RNN、LSTM、GRU)逐个处理 token,靠隐状态把信息带向未来,顺序、有记忆、输入长度灵活,2014 到 2017 统治序列建模,然后注意力来了。本节从零搭两者,再点出那个催生注意力的失败。

文本的 CNN 与 RNN

本节摘要:卷积学 n-gram,循环记住过去,两者都被注意力取代,但两者在受约束的硬件上仍然要紧。TF-IDF 和 Word2Vec 产出的是无视词序的扁平向量,建在其上的分类器分不清 dog bites manman bites dog,而词序有时恰恰就是信号。Transformer 到来前,有两个架构家族补上了这个缺口:文本卷积网(TextCNN)在词嵌入序列上做一维卷积,宽 3 的滤波器就是一个可学的三元检测器,堆叠不同宽度再最大池化,扁平、并行、快;循环网(RNN、LSTM、GRU)逐个处理 token,靠隐状态把信息带向未来,顺序、有记忆、输入长度灵活,2014 到 2017 统治序列建模,然后注意力来了。本节从零搭两者,再点出那个催生注意力的失败。

对应原课程:Phase 5 · Lesson 08 · cnns-rnns-for-text(原英文 phases/05-nlp-foundations-to-advanced/08-cnns-rnns-for-text/docs/en.md)。前置依赖:Phase 3 · 11(PyTorch 入门)、第 03 节(词嵌入)、Phase 4 · 02(从零实现卷积)。

学习目标

阅读完本节,你应当能够:

  1. 说清 TextCNN 与 RNN/LSTM/GRU 各自如何把词序信息编码进向量,以及双向 RNN 为何对标注任务必不可少。
  2. 从零实现 TextCNN 与 LSTM 分类器,理解最大池化选哪种、为何选它。
  3. 梯度消失模拟直观理解为何普通 RNN 学不到长程依赖,而 LSTM 的门控与细胞状态如何修复。
  4. 识别 CNN/RNN 的三大遗留难题(顺序瓶颈、定长上下文压缩、远距依赖天花板),看清注意力为何是必然的下一跳。

一、问题与直觉

TF-IDF 和 Word2Vec 产出的是无视词序的扁平向量。建在其上的分类器分不清 dog bites manman bites dog。词序有时恰恰就是信号。

Transformer 到来前,两个架构家族补上了这个缺口。

文本卷积网(TextCNN):在词嵌入序列上做一维卷积。宽 k 的滤波器滑过连续的 k 元嵌入,产出特征图。全局最大池化挑出最强激活,拼接几个宽度的池化输出,喂给分类头。宽 3 的滤波器就是一个可学的三元检测器,堆叠宽度 2/3/4/5 检测多尺度模式,最大池化到定长表示。扁平、并行、快。

循环网(RNN、LSTM、GRU):逐个处理 token,维护一个把信息带向未来的隐状态。顺序、有记忆、输入长度灵活。2014 到 2017 统治序列建模,然后注意力来了。

TextCNN(Kim, 2014)为何奏效:滤波器就是可学的 n-gram,最大池化位置不变,所以 "not good" 无论在评论开头还是中间都激活同一特征。三个宽度各 100 个滤波器,就是 300 个学到的 n-gram 检测器。训练并行,无顺序依赖。

RNN:每个时间步 t,隐状态 h_t = f(W * x_t + U * h_{t-1} + b),跨时间共享 WUb。时刻 T 的隐状态是整个前缀的摘要。分类时,对 h_1 ... h_T 池化(最大、均值或取末态)。

普通 RNN 受梯度消失之苦。LSTM 加门决定遗忘什么、存什么、输出什么,稳定长序列梯度。GRU 把 LSTM 简化成两门,参数更少,表现相近。

双向 RNN 一个前向、一个后向,拼接隐状态。每个 token 的表示同时看到左右上下文,对标注任务必不可少。

二、从零实现

第 1 步:PyTorch 里的 TextCNN

import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, n_classes, filter_widths=(2, 3, 4), n_filters=64, dropout=0.3): super().__init__() self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, n_filters, kernel_size=k) for k in filter_widths ]) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(n_filters * len(filter_widths), n_classes) def forward(self, token_ids): x = self.embed(token_ids).transpose(1, 2) pooled = [] for conv in self.convs: c = F.relu(conv(x)) p = F.max_pool1d(c, c.size(2)).squeeze(2) pooled.append(p) h = torch.cat(pooled, dim=1) return self.fc(self.dropout(h))

transpose(1, 2)[batch, seq_len, embed_dim] 换成 [batch, embed_dim, seq_len],因为 nn.Conv1d 把中间轴当通道。池化输出定长,与输入长度无关。

第 2 步:LSTM 分类器

class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, n_classes, bidirectional=True, dropout=0.3): super().__init__() self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=bidirectional) factor = 2 if bidirectional else 1 self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden_dim * factor, n_classes) def forward(self, token_ids): x = self.embed(token_ids) out, _ = self.lstm(x) pooled = out.max(dim=1).values return self.fc(self.dropout(pooled))

对序列做最大池化,而非取末态。分类任务上,最大池化通常胜过取末隐状态——因为长序列末尾的信息容易在末态里一家独大。

第 3 步:梯度消失演示(直觉)

无门控的普通 RNN 学不到长程依赖。想一个玩具任务:预测序列里是否出现过 token A。若 A 在位置 1、序列长 100,损失梯度要回流过 99 次循环权重的乘法。权重小于 1 则梯度消失,大于 1 则爆炸。

def vanishing_gradient_sim(seq_len, recurrent_weight=0.9): import math return math.pow(recurrent_weight, seq_len) # weight=0.9、100 步时: # 0.9 ^ 100 ≈ 2.7e-5 # 从第 100 步回传到第 1 步的梯度,实际上就是零。

LSTM 用一个只有加性交互的细胞状态修掉了这个(遗忘门是乘性缩放,但梯度仍沿「高速路」流动)。GRU 用更少参数做了类似的事。两者都能让你在 100+ 步序列上稳定训练。

第 4 步:为何这仍然不够

即便有 LSTM,三个问题依旧:

  1. 顺序瓶颈:在长 1000 的序列上训 RNN,需要 1000 步串行的前向/反向,无法跨时间并行。
  2. 编码器-解码器里定长上下文向量:解码器只看编码器的最终隐状态,它是被压缩到定长的整个输入。长输入丢细节,第 09 节直接讲这个。
  3. 远距依赖准确率天花板:LSTM 优于普通 RNN,但跨 200+ 步传播特定信息仍吃力。

注意力把三个全解决了。Transformer 干脆丢掉循环。第 10 节就是那个转折点。

三、框架对比

PyTorch 的 nn.LSTMnn.GRUnn.Conv1d 都是生产就绪的,训练代码是标准的。

Hugging Face 提供预训练嵌入,你把它当输入层插进去:

from transformers import AutoModel encoder = AutoModel.from_pretrained("bert-base-uncased") for param in encoder.parameters(): param.requires_grad = False class BertCNN(nn.Module): def __init__(self, n_classes, filter_widths=(2, 3, 4), n_filters=64): super().__init__() self.encoder = encoder self.convs = nn.ModuleList([nn.Conv1d(768, n_filters, kernel_size=k) for k in filter_widths]) self.fc = nn.Linear(n_filters * len(filter_widths), n_classes) def forward(self, input_ids, attention_mask): with torch.no_grad(): out = self.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state x = out.transpose(1, 2) pooled = [F.max_pool1d(F.relu(conv(x)), kernel_size=conv(x).size(2)).squeeze(2) for conv in self.convs] return self.fc(torch.cat(pooled, dim=1))

「合约束时就用」清单:

  • 边缘/设备端推理:TextCNN 配 GloVe 嵌入,比 Transformer 小 10~100 倍。部署目标是手机时,就用这套。
  • 流式/在线分类:RNN 一次处理一个 token,Transformer 要整段序列。实时流入文本时,LSTM 仍赢。
  • 做基线的小模型:新任务上快速迭代,CPU 上 5 分钟训完一个 TextCNN。
  • 数据有限的序列标注:BiLSTM-CRF(第 06 节)在 1 千到 1 万标注句上仍是生产级 NER 架构。

其余一切,交给 Transformer。

四、可复用产物

保存为 outputs/prompt-text-encoder-picker.md:

--- name: text-encoder-picker description: Pick a text encoder architecture for a given constraint set. phase: 5 lesson: 08 --- Given constraints (task, data volume, latency budget, deploy target, compute budget), output: 1. Encoder architecture: TextCNN, BiLSTM, BiLSTM-CRF, transformer fine-tune, or "use a pretrained transformer as a frozen encoder + small head". 2. Embedding input: random init, GloVe / fastText frozen, or contextualized transformer embeddings. 3. Training recipe in 5 lines: optimizer, learning rate, batch size, epochs, regularization. 4. One monitoring signal. For RNN/CNN models: attention mechanism absence means they miss long-range deps; check per-length accuracy. For transformers: fine-tuning collapse if LR too high; check train loss. Refuse to recommend fine-tuning a transformer when data is under ~500 labeled examples without showing that a TextCNN / BiLSTM baseline has plateaued. Flag edge deployment as needing architecture-before-everything.

五、练习

  1. 基础:在一个三类的玩具数据集(你自己造)上训 TextCNN。验证宽度 (2, 3, 4) 的平均 F1 优于单宽度 (3)。
  2. 进阶:为 LSTM 分类器实现最大池、均值池、末态池,在小数据集上对比,记录哪种池化赢,并假设原因。
  3. 挑战:搭一个 BiLSTM-CRF NER 标注器(结合第 06 节与本节)。在 CoNLL-2003 上训,对比第 06 节的纯 CRF 基线和 BERT 微调,报告训练时间、内存、F1。

本节要点回顾

  1. 扁平向量无视词序:dog bites manman bites dog 分不清,词序有时就是信号。
  2. TextCNN 学 n-gram:一维卷积+全局最大池,位置不变、并行、快。
  3. RNN 逐 token 更新隐状态:h_t = f(W x_t + U h_{t-1}),跨时间共享权重,h_T 是前缀摘要。
  4. LSTM 加门控与细胞状态修梯度消失,GRU 简化成两门表现相近。
  5. 双向 RNN 拼前后向隐状态,每个 token 同时看左右上下文,标注任务必需。
  6. TextCNN 的 transpose:把嵌入维度换到通道轴,池化输出定长。
  7. 分类用最大池而非末态:长序列末尾信息易在末态一家独大。
  8. 梯度消失:0.9^100 ≈ 2.7e-5,普通 RNN 学不到长程依赖。
  9. 三大遗留难题:顺序瓶颈、定长上下文压缩、远距依赖天花板——注意力全解决。
  10. 仍要紧的场景:设备端、流式、小基线、有限数据序列标注;其余交 Transformer。

下一节,我们走进编码器-解码器的核心——进入「序列到序列」,看定长上下文向量如何成为瓶颈,为注意力铺路。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U