Day05|NLP 文本分类入门:词向量 + RNN 从 0 训练情感分类

📅 2026/6/25 16:21:28
Day05|NLP 文本分类入门:词向量 + RNN 从 0 训练情感分类
苦猿的大模型日记 · Day05 · 词向量 RNN 文本分类-帮普通人把AI学进简历系列前言:学弟的一通电话上周三晚上 11 点,我接到学弟的电话。他第一句就是:哥,MNIST 我跑通了,准确率 99.1%,老板让我下周一交个差评分类的 demo。我心想这有什么难的,Day04 的训练循环搬过去改改就是。结果他下一句:……但评论是文字,我不知道怎么把它喂给神经网络。我愣了两秒。对啊。MNIST 那种图像,像素天然是 0-255 的数字,扔进模型就行。可文字呢?这部电影太好看了——这 7 个字,是几个数?CV 那套地基建好的管道,到了 NLP 这一步直接断。电话里我跟他说了一个小时,讲明白:文本要进神经网络,中间隔着一座桥——词向量 RNN。挂完电话我就想,这个卡点不止他一个人有。Day04 我们把 PyTorch 训练循环的模板钉死了,但那张模板只跑过图像。今天 Day05,把这张模板复用到文本上,顺便为后面的 LLM 铺最后一块地基。读完你能:搞懂文本如何变成数字(Token / 词表 / ID 序列)理解 Embedding 层——从 one-hot 到稠密词向量的进化掌握 RNN 结构,知道为啥要循环、隐状态怎么传知道 LSTM 解决了 RNN 的什么毛病(三个门直觉)把 Day04 训练循环模板复用到 IMDb 情感分类,跑出第一组数字PART 01:文本如何变成数字回到那个问题——这部电影太好看了,是几个数?答案是:先把它拆开。第一步:分词(Token)中文要先用工具切(比如 jieba),英文省事,按空格和标点切。this movie is awesome → [this, movie, is, awesome] 这部电影太好看了 → [这部, 电影, 太, 好看, 了]第二步:建词表(Vocab)把训练集里所有的 token 扫一遍,统计出现频率,给每个 token 编个号。vocab {pad: 0, unk: 1, this: 2, movie: 3, is: 4, awesome: 5, ...}pad是占位符(等下讲为啥要它)unk是 unknown——词表里没收录的生词统一塞这第三步:转 ID 序列把每条评论的 token 流,换成 ID 流。[this, movie, is, awesome] → [2, 3, 4, 5]到这里,人话已经被翻译成机器话了。但还有个坑:长度不齐。模型要批处理,一批的输入必须等长。可评论有长有短,咋办?Padding:短的补 0,长的截断,统一到固定长度(比如 200)。# 原始:[2, 3, 4, 5] # pad 到长度 8:[2, 3, 4, 5, 0, 0, 0, 0]这就是pad那个 0 的用处——告诉模型这里是占位,不是真的词。文本不是数字,但任何 AI 任务的第一步,都是把人话翻译成机器话。PART 02:Embedding 层——从 one-hot 到稠密向量ID 序列有了,但直接把[2, 3, 4, 5]喂给神经网络,模型啥也学不到。为啥?因为 ID 是个编号,编号之间没有语义关系。movie 是 3,awesome 是 5,数字上 5 比 3 大,可这俩词的关系凭啥用大小来表达?最早的笨办法:one-hot。词表 5 万,每个词就用 5 万维向量表示,自己的位置是 1,其他全是 0。movie [0, 0, 1, 0, ..., 0] # 第 3 位是 1 awesome [0, 0, 0, 0, 1, ..., 0] # 第 5 位是 1两个致命毛病:稀疏到爆炸:5 万维向量只有一个 1,其余全 0,白白浪费内存完全没语义:好和棒在 one-hot 里完全正交,模型看不出它俩意思接近Embedding 的核心思想:换个表示方式。不要 5 万维,要100 维稠密向量——每个词被映射到 100 维空间里的一个点。关键魔法:让语义接近的词,在空间里也接近。这就是 Word2Vec 那篇神论文干的事。最经典的一个例子:向量(国王) - 向量(男人) 向量(女人) ≈ 向量(王后)模型自己学出来:国王和王后在空间里靠得近,好和棒靠得近,好和差离得远。PyTorch 里 Embedding 长这样:self.embedding nn.Embedding(num_embeddings50000, embedding_dim100) # 输入:[batch, seq_len] 的整数 ID # 输出:[batch, seq_len, 100] 的稠密向量本质就是查表——给个词 ID,返回它对应的 100 维向量。embedding_dim选多大?小数据集 50-100,大数据集 300,LLM 里能到几千。Embedding 是 LLM 的第一块砖。GPT-4 内部的第一个模块,就是它。PART 03:RNN——为啥要循环Embedding 解决了词怎么表示,但还有个更要命的硬伤:顺序。我喜欢这部电影 和 这部电影我喜欢,词向量都一样,但语序不同。MLP 和 CNN 都把它们当独立单元看——一句评论进去,把每个词向量拼一拼,前向一次出结果。可语言是有顺序的。我喜欢后面接这部电影,和后面接这个垃圾,意思天差地别。模型得一边读一边积累上下文。RNN 就是为这事发明的。RNN 的核心思想:带记忆地读想象你读一本小说——每读一段,脑子里都在积累剧情。新的一段来了,你不是从零开始,而是基于上一段留下的记忆 当前段落,更新你的理解。RNN 干的就是这个事。它有一个叫隐状态(hidden state)的东西,记作h。每读一个词x_t,它就更新一次h:h_t tanh(W_x · x_t W_h · h_{t-1} b) ↑ ↑ ↑ 新记忆 当前词 上一时刻记忆关键就这一行——同一组参数W_x、W_h在每个时间步重复使用,这就是循环的来源。PyTorch 调用一行:self.rnn nn.RNN(input_size100, hidden_size128, batch_firstTrue)读完一整句话,最后那个h就承载了整句话的信息——拿它去做分类就行。RNN 的死穴:长期依赖崩溃但 RNN 有个致命毛病:记不住远处的词。经典反例:我出生在法国……(中间隔了 50 个词的废话)……我能流利地说__。读到说的时候,RNN 早把法国忘了。梯度消失 / 爆炸,长距离信号传不过去。LSTM 救场:三个门LSTM(Long Short-Term Memory)就是来修这个毛病的。它加了一条主线记忆c,配三个门控制信息流。门干啥的类比遗忘门决定从主线记忆里扔掉啥笔记本里划掉过期信息输入门决定把新信息写进主线记忆往笔记本里记新内容输出门决定现在用哪部分输出翻到对应页给当前问题用答案PyTorch 接口跟 RNN 几乎一样,换一行就行:self.rnn nn.LSTM(input_size100, hidden_size128, batch_firstTrue)LSTM 比 RNN 稳——长评论里的关键信号(比如不过但是然而这种转折词)能被它记住,RNN 看一眼就忘。RNN 把顺序引进了神经网络,LSTM 让这份记忆能扛过 50 个词的距离。PART 04:PyTorch 定义 RNN 分类器(复用 Day04 训练循环)理论讲完了,上代码。模型骨架(关键片段)import torch import torch.nn as nn class TextRNN(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.rnn nn.LSTM(embed_dim, hidden_dim, batch_firstTrue) self.fc nn.Linear(hidden_dim, num_classes) def forward(self, x): x self.embedding(x) # [B, seq] → [B, seq, embed] out, (h, c) self.rnn(x) # h: [1, B, hidden] return self.fc(h[-1]) # 用最后一步隐状态做分类几个关键工程直觉:padding_idx0:告诉 Embedding,位置 0 是pad,它的向量永远保持全 0,不参与训练——免得模型把占位符也学成一个有意义的词用h[-1]做分类:LSTM 输出的h是[num_layers, batch, hidden],取[-1]就是最后一层的最终隐状态——它读完了整句话,信息最全替代方案:out.mean(dim1)做平均池化,比取最后一步更稳,实战常用重头戏:复用 Day04 训练循环下面这段,和 Day04 一字不差——def train(model, loader, epochs5, lr1e-3): loss_fn nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lrlr) model.to(device) for epoch in range(epochs): model.train() for x, y in loader: x, y x.to(device), y.to(device) pred model(x) loss loss_fn(pred, y) optimizer.zero_grad() loss.backward() optimizer.step()唯一区别:Day04 的x是[B, 1, 28, 28]图像张量,Day05 的x是[B, seq_len]整数 ID。损失函数还是CrossEntropyLoss,优化器还是 Adam,反向传播还是那三行。这就是 Day04 的价值——训练循环是模板,会写一次就会写一百次。PART 05:IMDb 实战结果 踩坑 下篇预告IMDb 数据集简介IMDb(Internet Movie Database)电影评论——5 万条训练 5 万条测试,标签就两类:正面 / 负面。NLP 入门的标准MNIST。英文按空格分词,词表控制在2.5 万-5 万,序列长度截到200-300,大部分评论够用。三模型对比我用三种架构跑了一遍 IMDb,结果:模型测试准确率参数量单 epoch(CPU)MLP(平均池化)~82%~500k~60sRNN~85%~340k~120sLSTM~88%~380k~180s几个关键洞察:顺序信息值钱:RNN/LSTM 比 MLP 高出 3-6 个百分点,纯靠能读上下文LSTM 比 RNN 稳:那些藏在长评论里不过但是的转折信号,只有 LSTM 扛得住88% 不是天花板:这个数在 NLP 圈属于能写进简历的入门分数,但远不是终点——后面 Transformer 直接吊打到 92%简历写法示例(承接 Day04 的简历级项目叙事):用 PyTorch 实现 LSTM 文本分类,IMDb 数据集准确率 ~88%踩坑 5 个坑 1:nn.Embedding输入忘了转 LongTensorEmbedding 只吃整数 ID,但你从 DataLoader 取出来的可能是 float。x x.long() # 一行救命坑 2:padding_idx0没设,模型把pad学成了一个有意义的词不设的话,所有那些 0 都被 Embedding 当真词学,模型开始对长度过度敏感。坑 3:LSTM 输出取错维度out, (h, c) self.rnn(x) # ❌ h 形状 [num_layers, batch, hidden]——不能直接喂 Linear # ✅ h[-1] 形状 [batch, hidden]——对坑 4:序列太长,LSTM 训得巨慢500 词以上的评论直接喂,LSTM 单 epoch 能跑 5 分钟。截到 200-300,准确率不掉,速度快 3 倍。坑 5:测试漏model.eval()torch.no_grad()Day04 坑 1 的复现。这俩在 NLP 任务里一样致命,漏一个准确率掉 2 个点 / 显存爆。写完这些坑我又意识到一遍:深度学习的门槛,从来不是数学,是工程——CV 是,NLP 也是。结尾:训练循环没变,变的只是模型Day04 跑 MNIST 的时候,我说过一句话:学深度学习最大的错觉,是以为自己懂了;唯一的解药,是亲手训一个模型。Day05 把这句话从图像搬到了文本。从 MNIST 到 IMDb,模型在变(MLP/CNN → Embedding/RNN/LSTM),数据在变(像素 → 词向量),训练循环没变。这就是工程能力——底层模板打牢,换领域只是换输入。但 RNN/LSTM 也有它的天花板:长距离依赖还是没那么稳,训练慢,并行差。下一篇 Day06,我们正式扔掉循环这个包袱,进Transformer / 自注意力——让模型一次性看完整个句子,而不是一个词一个词读。这是 LLM 的真正起点。从 MNIST 到 IMDb,模型在变,训练循环没变;从 RNN 到 Transformer,结构在变,把人话变成向量这件事,从来不会变。互动时间:你做 NLP 时最头疼的是数据预处理还是模型调参?评论区聊聊,我挑高频的写进 Day06。— END —苦猿 · 帮普通人把 AI 学进简历