第一课跑通 hello world 之后很多人会卡在同一个地方模型能跑但换个自己的任务就完全不知道怎么下手。我见过不少自学 AI 的朋友学了一堆理论一开 IDE 就懵。第二课最该补的不是更深的算法而是一条完整的最小闭环——从原始数据到能用的模型。这一课我选文本分类来做因为它数据好找、效果直观、踩坑有代表性而且这套思路你理解了换到图像、音频任务一样通用。这篇内容主要适合已经装了 Python、跑过 PyTorch 官方 demo但还没独立完成过整个项目的人如果你正准备做第一个属于自己的 AI 小任务顺着这篇的思路走能省下大量瞎折腾的时间。1. 第一课到第二课之间缺的不是模型而是流程1.1 为什么第二课强烈建议做文本分类很多初学者第二课会去追最新的模型说实话没必要。第二课的核心目标不是 用上某某大模型而是建立我能自己把一个问题拆成 AI 能解的流程这个信心。文本分类是一个非常合适的载体数据可以自己收集比如商品评论情感判断、新闻分类、垃圾短信识别每个都贴近日常问题定义清晰模型输出就是类别标签效果好不好一眼能看出来而且文本不像图像那样对机器资源要求高普通笔记本就能跑。我把整个流程拆成了五个环节数据准备、预处理、模型搭建、训练观察、评估部署。这五步走完你就能体会到什么是完整的项目节奏。后面很多任务无非是把其中的数据换一换、模型换一换流程骨架基本不变。你把这个骨架刻在脑子里才算真正入行。1.2 这篇课文的成果基线与目标设定为了让目标足够具体我们用中文商品评论情感二分类作为贯穿案例。输入一句话输出两个标签正面或负面。你可以顺手扩展成一个三分类加中性但第一版先做二分类因为样本容易标注、指标好解释训练也快。预期成果是训练完一个准确率在 85% 以上的模型并且能通过一个简单的命令行脚本或接口输入新的评论返回 正面/负面 置信度。注意85% 这个数字不是拍脑袋中文商品评论短文本上传统机器学习方法大概能到 80% 左右TextCNN 这类简单深度学习模型做到 85% 以上是合理预期。如果你的数据分布更干净做到 90% 也正常。2. 数据准备这一环节决定了你的模型天花板2.1 从哪拿数据、怎么判断数据够不够你当然可以直接用公开数据集但既然是第二课我建议你自己爬一部分、找公开部分混合来用。重点是体会数据来龙去脉不清模型全是白搭。我用的语料是约 5000 条电商评论正负各半。你可以用现成的开源中文情感语料也可以手工收集几百条先跑通流程之后再扩充。有一个很实用的经验第一版模型不要追求大数据量。5000 条数据对二分类任务已经足够跑通全流程训练时间也短。数据量超过 1 万条时训练时间增加但 learning 效果提升变缓对学习来说反而是负担。当你发现验证集指标波动大时再考虑加数据这个时机很重要。2.2 原始数据清洗最常见的五个坑原始评论不是干净的文本直接喂给模型会有问题。我实践中碰到最多的五类脏数据重复字符太好看了啊啊啊啊啊这类要标准化或者截断。重复的啊其实就是语气强调但不处理会让模型误以为长序列是情绪强度。无效字符URL、用户、HTML 标签用正则清掉或者替换成空格。注意别把表情符号清了很多评论里好评的感叹号和表情是强信号。标签噪音标注错误。人在情绪化评论时常常写快递太慢了但客服态度不错这种到底是正面还是负面我的处理办法是以主要情绪为主必要时引入第三类混合。但二分类第一版简单按多数情绪归正或负。文本过短/过长长度分布极偏后面会影响幂等填充。先画一个长度分布直方图再定截断长度这个后面会细说。空白/全符号样本这种直接删除留着只会让模型更容易过拟合。2.3 训练集、验证集、测试集的切分逻辑很多教程说三七分就完事了但对学习项目有个坑切分前必须打乱顺序。原始评论往往是按时间或按商品排列的不打乱就切会造成验证集分布和训练集不一致——比如训练集全是某个商品的好评、验证集全是另一个品牌的差评最终指标会异常高或者异常低给你一种模型很强的错觉。我推荐使用按标签分层的随机切分保证训练集/验证集/测试集里正负样本比例大致相同。比例用 8:1:1即 5000 条数据划出 4000 训练、500 验证、500 测试。验证集用于调参测试集只在最后用一次——千万别拿测试集反复调模型否则你的测试集就变成验证集了失去摸盲盒的意义。3. 文本预处理词表、长度与填充的三重选择3.1 中文要不要分词三种粒度的实测感受预处理是文本任务最大的分水岭。对中文来说第一个决策是分词粒度。我三种都试过基于词如 jieba 分词符合语义直觉词表大OOV未登录词问题多。基于字词表小常用汉字也就几千OOV 大幅减少但对短语级语义捕捉弱。子词BPE/WordPiece是词和字之间的折中但需要额外工具对第二课来说引入过重。我的建议是中文短文本情感分类直接用字级粒度起步。好处是无需额外分词依赖、词表 3000-5000 字基本覆盖绝大多数评论、训练速度快。很多人在第一步就死在分词调优上其实对情感分类来说字的上下文足够表达语义了。你后期想追求更高精度再切到词级或子词不迟。3.2 词表构建与序列长度少样本下必须做的两件事词表构建很简单统计训练集所有字符的出现频率保留出现次数前 N 个作为索引。要预留序号0 留给填充符1 留给未知字符。用字级粒度时N 通常取 3000 到 5000。如果数据来自领域化文本比如医疗评论可以适当加大到 10000通用商品评论 3000 够了。序列长度选择更需要些思考。先对训练集做长度统计画出分布图。中文商品评论大多在 20-80 字之间长尾到 200 字。我的截断策略是取 95 分位的长度比如统计结果是 128 字那就把序列统一截断到 128。为什么不用 256因为你对情感分类来说尾部多出来的往往是售后偶发描述对类别判断贡献低却会明显增加计算量和模型参数量。短于 128 的填充到 128。3.3 停用词、数据增强与标签编码关于哈工大停用词表我实测下来在文本分类任务里收益并不大。情感词、程度副词很太超往往就是分类核心信号而这些词大概率在常用停用词表里。删了它们等于自毁特征。所以第二课阶段建议不要用停用词表。你先跑一版不加任何处理的再对照加停用词版的指标自己判断而不是迷信标准流程。数据增强可以玩一下但别当主角同义词替换好换棒和随机删除字符是两种最容易实现的方式。不过注意数据增强是锦上添花不是从 60% 涨到 90% 的手段核心还是数据质量和模型结构。标签编码二分类用 0/1 即可损失函数用交叉熵。4. 模型搭建先用 TextCNN 而不是 BERT是刻意的4.1 为什么我不推荐你第二课直接上 BERT很多人一上来就上 BERT因为听说效果最好。但第二课的目的是建立流程感不是刷榜。BERT 的隐患在于它把复杂度藏在了加载预训练权重这一步你很难直观理解数据是怎么被处理的同时微调 BERT 的坑学习率、层数冻结、GPU 显存比 TextCNN 多得多容易把时间耗在工程配置上而不是建模思维上。所以我选 TextCNN 作为主模型。它的核心思想很朴素把句子看成一行字用多个不同尺寸的卷积核去扫过整句话捕捉相邻词/字的局部组合模式。就像人看一句话目光会扫过几个词形成的短语这个模型就是在自动学习哪些短语组合对判断情感最有用。太差、超好用这类连续字组合会被不同尺寸的卷积核捕捉到。这就是为什么短文本分类任务里TextCNN 简单却难被超越。4.2 TextCNN 的完整实现从 Embedding 到全连接我用 PyTorch 实现一个标准 TextCNN代码路径如下输入是 [batch_size, seq_len] 的整数索引序列经过 Embedding 层变成 [batch_size, seq_len, embed_dim]然后分别经过 3 组卷积核尺寸为 2、3、4每组有 128 个卷积核得到不同长度的特征图对每个特征图做 1-max pooling得到固定长度向量拼接三个向量后过全连接层最后输出 2 类 logits。import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim100, num_classes2, kernel_sizes(2, 3, 4), num_filters128, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (k, embed_dim)) # 输入通道1卷积核尺寸沿seq_len方向 for k in kernel_sizes ]) self.fc nn.Linear(len(kernel_sizes) * num_filters, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): # x: [batch_size, seq_len] x self.embedding(x) # [batch, seq_len, embed_dim] x x.unsqueeze(1) # [batch, 1, seq_len, embed_dim] pooled [] for conv in self.convs: # conv 输出: [batch, num_filters, seq_len - k 1, 1] feature_map conv(x).squeeze(3) # 1-max pooling: 取每个特征图的最大值 pooled.append(torch.max(feature_map, dim2).values) x torch.cat(pooled, dim1) # [batch, 3 * num_filters] x self.dropout(x) return self.fc(x)这里面有个初学者最容易忽略的点卷积核的宽度是embed_dim不是 seq_len 方向的一维。因为卷积输入是 [batch, 1, seq_len, embed_dim]卷积核尺寸(k, embed_dim)表示同时在字序方向扫k个字在语义维度方向覆盖整个 embedding 向量。等价于对一句话里每连续k个字的完整语义进行组合。这个设计是 TextCNN 能捕捉短语模式的关键。4.3 训练脚本中的超参数与训练配置给出我实测后适合项目规模的配置Embedding 维度 100这个值对训练数据有限的任务足够调大容易过拟合。卷积核尺寸 2/3/4各 128 个卷积核。尺寸 2 捕捉双字词太差3 捕捉三字组合不好用4 捕捉更长的习惯表达。Dropout 0.5防止全连接层过拟合。优化器用 Adam学习率 1e-3。这里有个经验TextCNN 用 Adam 时1e-3 通常能稳定收敛如果 loss 不降先降到 5e-4而不是改模型。batch size 64因为数据量小batch 太大梯度方向不稳定。epoch 数设 10-20配合早停机制后面讲。训练代码里要加三件事记录每个 epoch 的训练 loss 和验证 loss保存验证集上指标最好的一版模型每轮打乱训练集顺序。第三件事常被省略但它在小数据下对收敛很重要。5. 训练过程观察别只看准确率要看 loss 曲线和收敛速度5.1 模型训练后最容易出现的假象你跑完一个 epoch看到训练准确率 90%、验证准确率 84%很可能觉得很不错。但在小数据上这个数字很能骗人。真正的训练观察要从三个方向看第一验证 loss 是否还在下降降到多少开始回升回升点就是过拟合开始。第二训练集和验证集的准确率差距是否持续扩大差距超过 10 个点通常说明过拟合。第三从验证集预测结果里随机抽 20 条看错误的样本长什么样你会发现很多所谓错误其实是标注争议比如那条物流慢但质量不错。5.2 过拟合的三个信号与对应处理优先级信号一训练 loss 几乎降为 0验证 loss 上升。处理顺序先加 dropout0.5 提到 0.6再减小卷积核数量或 embedding 维度最后再考虑加数据。别一上来就加数据代价高且不一定解决根本问题。信号二训练准确率与验证差 15 个点以上。先确认是否存在数据泄漏——比如测试集混杂了训练集里出现过的相同商品名短评。数据泄漏在文本任务里很隐蔽最典型的就是直接复制粘贴数据集切分代码但没把同一用户/同一商品的评论合并去重。信号三验证集预测几乎全是多数类。这类是类别不平衡的表现就算准确率 90% 也可能全是预测某一类。直接用 F1 分数来验收不要被准确率误导。5.3 一个真实的调参踩坑记录我第一版训练时选了 lr 3e-3结果前 3 个 epoch 的 loss 一直剧烈震荡验证准确率在 70% 附近摇摆。我当时以为是模型结构问题换了卷积核数量也没用。后来打印每个 batch 的 loss 分布才发现部分长样本的 loss 特别高拉偏了梯度方向。换成 lr 1e-3 并且对梯度做 clipmax_norm1.0之后第 2 个 epoch 验证准确率直接跳到 82%。这个坑的启示是遇到训练波动大先检查学习率和梯度范数不要急着换模型。梯度裁剪代码写起来很简单torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)建议从第一版就加上。5.4 early stopping 与最优模型选择最理想的模型不是最后一个 epoch 的模型而是验证集指标最好的那一个。我在训练循环里维护一个best_val_loss每当验证集 loss 比历史最优低就保存模型权重同时重置一个计数器连续 5 个 epoch 没有下降就早停。代码逻辑如下best_val_loss float(inf) patience 0 for epoch in range(max_epochs): train_one_epoch(epoch) val_loss, val_acc evaluate() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pt) patience 0 else: patience 1 if patience 5: break记住早停的判定用 loss 而不是准确率。准确率是离散值在验证集上经常连续几个 epoch 一样用准确率早停容易过早停掉还在缓慢变好的训练。loss 的变化更细腻更适合做早停信号。6. 评估与部署把模型变成一个能用的小接口6.1 F1 分数、混淆矩阵和阈值选择训练完成之后你要在测试集上做一次正儿八经的验收。对二分类我建议输出四件东西准确率、F1 分数、混淆矩阵、每一条预测的置信度。为什么 F1 必须看因为准确率只回答所有预测里对的比例而 F1 综合了正类的召回率和精确率。在商品评论里如果负面评论只占 3%模型全预测正面准确率 97% 但毫无用处。F1 会让这种假厉害现原形。如果你想要更好的正类召回率比如更看重找出差评可以在模型输出的概率上调整阈值。因为模型输出的是两类的概率默认取 argmax 等于阈值 0.5但你可以把负面判断阈值降到 0.3这样更多疑似负面会被召回。代价是误报增加。阈值怎么选要看你的业务诉求没有绝对正确的默认值。你可以把测试集的预测概率保存下来画一条 PR 曲线然后按曲线选择你要的平衡点。6.2 模型导出与推理脚本实现PyTorch 模型推理最少代码版本加载权重、预处理一条文本、输出概率。def predict(text: str, vocab: dict, model: nn.Module, max_len: int 128) - tuple: ids [vocab.get(ch, 1) for ch in text[:max_len]] # 1: 未登录字 ids [0] * (max_len - len(ids)) # 0: padding input_tensor torch.tensor([ids], dtypetorch.long) logits model(input_tensor) prob torch.softmax(logits, dim1).squeeze().tolist() label 1 if prob[1] 0.5 else 0 return label, prob注意这里的掩码问题ids [0] * (max_len - len(ids))把填充位置全部填 0而词表里 0 是 padding_idx模型在 Embedding 层会对 0 不贡献梯度。如果你没用 padding_idx等于让模型把填充位当成真实字符学推理时长度不同结果会发飘。这是新手最容易忽略的细节。推理脚本写成文件之后下一步很简单用 Flask 或 FastAPI 包一层 HTTP 接口输入 JSON 里的text字段返回label和confidence。至此你完成了一个真实可用的 NLP 小服务而不是一个躺在 Jupyter 里的 demo。6.3 第二课结束之后的三个扩展方向跑通上面的流程你已经把 AI 项目的完整骨架装在脑子里了。之后可以按三条路线扩展数据路线扩充到 5 万条数据引入类别不平衡问题尝试过采样/欠采样、Focal Loss。你会看到数据分布如何影响模型行为。模型路线把 TextCNN 换成 BiLSTM再换成 BERT。同一份数据、同一个流程切换模型体会重参数轻流程的感觉。模型只是流程里的一个组件。任务路线把二分类改成多分类如新闻主题分类你要处理类别数增加带来的一系列问题输出维度、指标选择、样本均衡。这比学一个新模型收益更大。我个人带人做项目时反复讲一句话AI 项目的难点从来不在模型结构而在你对数据的理解和流程的控制力。第二课把这两点练扎实了后面学什么都很快。最后分享一个我实际操作中的小技巧训练之前先把所有预处理写成一个函数用 5 条样本打印出处理前和处理后的结果人工确认一下输入到模型的东西是对的。这一步只花两分钟却能在你训练到一半发现 loss 不对时帮你排除掉一大半可能的原因。很多训练异常根本不在模型而在数据管道。先确认数据管道干净再怀疑模型这是排查问题的正确顺序。