一句话总结
用最简单的方法跑出第一个"能用"的分类器:TF-IDF + LogisticRegression 和 FastText.
不追求最高精度,但能在几分钟内给出可量化的 baseline,为后续深度模型提供对比基准.
为什么要先跑传统方法
直觉上可能想直接上 BERT--效果最好,何必绕路? 但实战中传统方法有不可替代的价值:
| 传统方法 | 深度模型 |
|---|---|
| 训练几秒到几分钟 | 训练几十分钟到数小时 |
| CPU 即可运行 | 通常需要 GPU |
| 100 条数据就能看趋势 | 通常需要 1000+ 条 |
| 模型可解释(能看到哪些特征重要) | 黑箱,难以解释 |
| 部署极轻量(几 KB 模型文件) | 模型文件几百 MB |
传统方法的定位是快速验证数据质量和任务可行性:
- F1 只有 0.5 → 数据有问题(标注不一致或样本太少),换 BERT 也救不回来
- F1 达到 0.75 → 数据质量 OK,深度模型能进一步提升
- F1 已经 0.90+ → 任务简单,可能不需要深度模型
传统方法 = 先用 fmt.Println 打日志确认流程通了, 再换 structured logging. 先证明逻辑对,再优化工具链.
方法一:TF-IDF + LogisticRegression
这是最经典的文本分类组合--上一篇已经见过了,本篇完整展开.
TF-IDF 回顾
TF-IDF 把文本变成一个稀疏向量,每一维代表一个词的"区分度":
|
完整流水线
|
|
fit_transform vs transform 的区别
fit_transform = 学习词表 + 转换(只对训练集用).
transform = 用已学的词表转换(对测试集用).
如果测试集也用 fit_transform,会学到测试集的词--相当于考试前偷看了考卷,评估结果虚高.
关键参数调优
| 参数 | 作用 | 建议值 |
|---|---|---|
max_features |
最多保留多少个词 | 3000-10000(数据少时用小值) |
ngram_range |
考虑几个词的组合 | (1,2) 比 (1,1) 效果好 |
min_df |
至少出现在几个文档中才保留 | 2-5(过滤噪声词) |
sublinear_tf |
对 TF 取对数(减弱高频词的影响) | True |
|
ngram_range=(1,2) 的威力
单词级别 "脑子""有""病" 分别出现在很多正常语句中.
但二元组 "脑子有""有病" 几乎只出现在对骂场景.
N-gram 能捕获词序信息--对分类极其有用.
方法二:FastText 文本分类
FastText 是 Facebook(Meta)开源的轻量级文本分类工具,特点是极快--几秒训练完,毫秒级推理.
FastText vs TF-IDF + LR
| 维度 | TF-IDF + LR | FastText |
|---|---|---|
| 文本表示 | 稀疏向量(词袋) | 密集向量(词嵌入平均) |
| 考虑词序 | N-gram 部分考虑 | N-gram + 子词(subword) |
| 未登录词 | 直接忽略 | 子词拆解兜底 |
| 训练速度 | 秒级 | 秒级 |
| 可解释性 | 高(看 feature weight) | 中(看近义词) |
FastText 的核心优势是子词(subword):它把词拆成字符 N-gram. 比如 "nmsl" 这种不在词表中的缩写,TF-IDF 只能忽略,FastText 能通过字符组合 "nm""ms""sl" 捕获一定的信息.
数据格式
FastText 需要特定格式的训练文件:
|
每行格式:__label__标签 文本内容,标签和文本之间用空格分隔.
训练与评估
|
FastText 的安装
Python 包名是 fasttext(原版)或 fasttext-wheel(预编译,推荐).
安装命令:uv add fasttext-wheel. 如果遇到编译问题优先用 wheel 版本.
关键参数
| 参数 | 作用 | 建议值 |
|---|---|---|
epoch |
训练轮数 | 25-50(数据少时多训几轮) |
lr |
学习率 | 0.1-1.0(数据少用较大值) |
wordNgrams |
词级 N-gram | 2(捕获短语) |
dim |
词向量维度 | 50-100(数据少不需要高维) |
minCount |
最小词频 | 1-2(数据少时设 1) |
特征工程:超越词袋
传统方法的效果天花板往往取决于特征工程--能从文本中提取多少有区分力的信号.
文本特征清单
| 特征类型 | 具体特征 | 对对骂检测的意义 |
|---|---|---|
| 词袋特征 | TF-IDF 向量 | 基础特征,捕获关键词 |
| 统计特征 | 文本长度,标点数量,感叹号数量 | 对骂消息通常更短,感叹号更多 |
| 词表特征 | 脏话词命中数,脏话占比 | 最直接的信号 |
| 字符特征 | 大写比例,特殊字符比例 | 缩写脏话(SB,NM)大写居多 |
| 情感特征 | 负面情感词数,情感极性分 | 补充词袋无法捕获的情感方向 |
|
特征工程的收益递减
前 3-5 个特征通常贡献了 80% 的提升,之后每加一个特征边际收益急剧下降. 发现手工特征已经榨不出更多提升时,就是切换到深度模型的时机.
三种方法对比实验
用同一份数据集跑三种方法,横向对比:
|
实际数字取决于数据
上面的 F1 只是量级参考. 如果扩充数据太同质化(模板生成的变体),F1 可能虚高到 0.95+--这不代表真实效果好,只代表测试集和训练集太相似. 引入真实数据后 F1 会下降,这才是真实水平.
错误分析:baseline 告诉什么
跑完 baseline 后最重要的不是看 F1 数字,而是分析错误案例--它告诉数据或模型的短板在哪.
常见错误类型
| 错误类型 | 例子 | 原因 | 改进方向 |
|---|---|---|---|
| 漏报:隐晦对骂 | "你可真行啊" | 没有脏话关键词 | 需要上下文 → 深度模型 |
| 漏报:新词/缩写 | "yyds反面" | 不在词表中 | 扩充词表或用子词模型 |
| 误报:引用脏话 | "他说了sb这个词" | 关键词命中 | 需要语义理解 → BERT |
| 误报:自嘲 | "我真是废物" | 关键词命中 | 加"主语是自己"特征 |
|
错误分析 = Go 中的 benchmark + profiling. 不是看总耗时(F1),而是看哪些函数最慢(哪类错误最多). pprof 告诉优化方向,错误分析告诉数据/模型的改进方向.
什么时候该换深度模型
传统方法的能力边界在这几个场景:
- 隐晦表达:阴阳怪气,反讽--没有关键词,纯靠语义
- 上下文依赖:同一句话在不同语境含义不同
- 长尾新词:网络缩写,谐音梗,不断演变的表达
- F1 瓶颈:加特征,加数据都不再提升
如果错误分析中大部分错误属于前三类,说明问题出在"理解能力"而非"数据量"--这时该换能建模上下文语义的深度模型了(下一篇讲 TextCNN 和 LSTM).
快速回顾
- 传统方法的价值:快速验证数据质量,提供对比基准,可解释,轻量部署
- TF-IDF + LR:经典组合,bigram + sublinear_tf 是关键调优点
- FastText:子词拆解处理新词,一行代码训练,适合快速迭代
- 特征工程:脏话词表命中,文本长度,标点特征--前几个特征贡献最大
- 错误分析:比 F1 数字更重要,决定下一步是加数据还是换模型
- 升级时机:隐晦表达,上下文依赖,F1 瓶颈 → 切深度模型
动手练习
- 跑 TF-IDF baseline:用
dataset_v1.csv跑 TF-IDF(bigram) + LogisticRegression,调优参数使 F1 最大化 - 训练 FastText:将数据转换为 FastText 格式,训练 FastText 分类器,对比 F1
- 实现手工特征:实现
extract_features()函数,将手工特征拼接到 TF-IDF 向量后面,观察 F1 变化 - 做错误分析:打印所有误分类样本,归类为"隐晦对骂""新词""引用""自嘲"等类型,统计占比