阶段二 · 文本分类与情感分析

数据标注与数据集构建

一句话总结

模型效果的天花板不是模型复杂度,而是标注数据的质量.
本篇讲如何设计标注规范,如何从 40 条种子数据冷启动扩充到可用规模,以及如何验证标注一致性.

为什么数据比模型更重要

上一篇用 40 条数据跑了 baseline,结果极不稳定--换一个划分比例 F1 就变了. 这不是模型的问题,是数据太少.

一个直觉性的量级参考:

数据量 能做什么 效果预期
40 条 验证流程能跑通 不可靠,结果靠运气
200-500 条 训练简单模型(LR,FastText) 粗筛可用,F1 约 0.7-0.8
1000-3000 条 微调 BERT 生产可用,F1 约 0.85-0.95
10000+ 条 精细多分类 每个子类别都有足够样本

但数据不是越多越好--质量比数量重要. 1000 条标注一致的数据,效果远好于 5000 条标注混乱的数据.

标注数据 = 给模型写的"测试用例". 100 个目标明确,断言精准的 test case,比 1000 个随手写的,assertion 模糊的测试有价值得多. 每一条都要明确告诉模型"这个输入,答案是什么".

标注规范设计:什么算"对骂"

标注规范是整个数据集的宪法--它定义了分类的边界. 没有规范,不同标注者(包括未来的自己)会给出矛盾的标签.

边界问题:最难的不是典型 case

"你妈死了"是对骂吗? 当然是. "今天天气不错"是正常吗? 当然是. 这些不需要规范.

难的是边界 case:

文本 争议 需要规范定义
你可真行啊 阴阳怪气还是真夸? 单条无上下文时如何判定
sb 缩写是否算对骂? 常见脏话缩写的处理规则
我服了你了 无奈还是攻击? 情绪表达 vs 人身攻击的界限
这代码写的跟屎一样 对事还是对人? 评价行为/作品 vs 评价人
哈哈哈哈废物 朋友互损还是真骂? 关系亲密度是否纳入判断

规范的价值就在于决定这些边界 case--给出明确的,可执行的规则,而不是靠感觉.

标注规范模板

一份最小可用的标注规范应该包含:

[对骂检测标注规范 v1.0]

一,任务定义
- 判断单条群聊消息是否构成"对骂"
- 二分类:对骂(1) / 正常(0)

二,判定为"对骂"的充分条件(命中任一即标1)
1. 包含明确的脏话/辱骂词(含谐音,缩写:nmsl,sb,nc 等)
2. 针对对方人身的攻击性评价("你脑子有病""废物一个")
3. 恶意诅咒("全家暴毙""去死")
4. 明确的人身威胁("信不信我打你")

三,判定为"正常"的情况
1. 对事/对作品的负面评价("这方案不行""代码有bug")
2. 情绪表达但无攻击对象("我好烦""累死了")
3. 自嘲/自我贬低("我真是废物"--骂的是自己)
4. 引用/讨论脏话本身("他说了sb这个词对吗")

四,边界规则
- 无上下文时,"阴阳怪气"类默认标0(需要上下文才能确定的不标)
- 朋友间的互损/玩梗("老废物来打游戏")标0
- 缩写脏话(sb,nm,wc)无论完整与否一律标1
- 存在疑义时标0,宁可漏标不要错标(Precision优先)

五,标注格式
text,label
"你脑子有病吧",1
"今天的需求什么时候排",0

规范是活的,需要迭代

第一版规范不可能覆盖所有情况. 标注过程中会不断遇到新的边界 case,每次都要回来更新规范并追溯修正已标数据. 这不是"规范写得不好",是正常流程--就像 API 接口设计也会随需求迭代.

冷启动:从 40 条到 500 条

已经有 40 条种子数据. 如何快速扩充到可用规模? 三个策略并用:

策略一:关键词规则批量生成候选

用脏话词表对未标注文本做初筛--命中关键词的大概率是对骂,没命中的大概率是正常:

# 关键词规则:快速筛选候选样本
abuse_keywords = [
"废物", "垃圾", "sb", "nmsl", "脑残", "白痴", "滚",
"去死", "傻逼", "智障", "nc", "弱智", "猪", "狗东西"
]

def rule_predict(text: str) -> int:
"""关键词命中 → 候选对骂,人工确认后才写入数据集"""
text_lower = text.lower()
for kw in abuse_keywords:
if kw in text_lower:
return 1 # 候选对骂
return 0 # 候选正常

规则不是最终标签

关键词命中只是"候选"--最终标签必须人工确认. 规则的作用是提高标注效率:与其从海量消息里逐条看,不如先筛出"大概率是对骂"的子集,人工快速过一遍确认.

策略二:模板扩充

对已有的种子数据做变体扩充:

# 模板扩充:对已有样本做变形
templates_abuse = [
"你{adj}什么{noun}", # 你算什么东西
"{person}真是{adj}", # 你真是废物
"滚{direction}点", # 滚远点
"{verb}你{modal}的", # 打你妈的
]

# 同义替换
synonyms = {
"废物": ["垃圾", "废柴", "没用的东西"],
"滚": ["爬", "消失", "get out"],
"脑残": ["弱智", "智障", "nc"],
}

# 语气变体
# "你脑子有病" → "你脑子有病吧" / "你脑子有病?" / "你是不是脑子有病"

每条种子数据可以扩充出 3-5 条变体,40 条 → 120-200 条. 但注意:变体太相似会导致模型只学会表面模式,要保证一定的多样性.

策略三:真实数据采集

这是数据质量最高的来源,但需要获取真实群聊数据:

  • 历史群聊消息:从负责的群中导出近期消息(注意脱敏)
  • 公开数据集:搜索中文 abusive language / hate speech 数据集(如 COLD,THUCNews 评论区)
  • 主动收集:在测试群中模拟对骂场景,积累真实表达

数据分布要贴近生产环境

如果训练数据全是极端脏话("你妈死了")而没有隐晦的讽刺,缩写,谐音,上线后模型对真实群聊的效果会远低于测试指标. 数据分布要尽量覆盖实际会遇到的表达方式.

类别平衡:正负样本比例

真实场景中正常消息远多于对骂消息(可能 20:1 甚至 100:1). 但训练数据不能按这个比例来--否则模型会学到"全猜正常"这个捷径(上一篇 Accuracy 陷阱的例子).

推荐的训练数据比例:

策略 正负比例 适用场景
均衡采样 1:1 起步阶段,样本少时优先保证每类数量
轻度不平衡 2:1 ~ 3:1 有一定数据量后模拟真实分布趋势
配合过采样/欠采样 任意 数据量充足后,用技术手段处理不平衡

当前的种子数据是 20:20(1:1),扩充时保持大致 1:1 即可.

标注一致性验证

如果是一个人标注,一致性问题不大. 但如果团队多人标注(或隔了一周再标),就需要验证标注质量.

核心指标:标注者一致率(IAA)

标注一致率 = 两位标注者对同一批数据标注一致的比例

示例:100 条数据让两人分别标注
- 85 条标签相同 → IAA = 85%
- 通常 IAA > 80% 认为规范可用
- IAA < 70% 说明规范有歧义,需要修订

单人标注的替代方案:自检

如果只有一个人标注,可以用时间差做一致性检查:

  1. 标注 200 条后,隔一周随机抽 50 条重新标注
  2. 对比两次标注结果,统计不一致的条数
  3. 不一致的 case 逐条分析--是规范没写清,还是当时状态不好
  4. 更新规范,追溯修正冲突数据

时间差自检 = code review 自己的代码. 自己写的代码隔一周再看,经常会发现"当时为什么这样写"的地方. 标注也一样--时间差能帮助发现规范中的模糊地带.

数据集文件结构

标注数据用 CSV 格式存储,简单直接:

data/
├── seed_dataset.csv # 种子数据(40条,已有)
├── dataset_v1.csv # 第一版扩充数据(目标 200+ 条)
├── dataset_v2.csv # 迭代后的数据(修正标注 + 新增样本)
└── labeling_spec.md # 标注规范文档(版本化管理)

CSV 格式:

text,label,source,note
"你脑子有病吧",1,seed,"明确辱骂"
"今天的版本什么时候发",0,seed,"正常工作沟通"
"你可真行啊",0,augment,"无上下文默认标0"
"nmsl",1,keyword_rule,"缩写脏话一律标1"

额外字段(可选但推荐):

  • source:数据来源(seed / keyword_rule / augment / real),方便追溯
  • note:标注理由,遇到边界 case 时记录判断依据

完整标注工作流

flowchart TD A["设计标注规范 v1"] --> B["种子数据标注 40 条"] B --> C["关键词规则扩充候选"] C --> D["人工确认/修正候选标签"] D --> E["模板扩充变体"] E --> F["合并去重 → dataset_v1.csv"] F --> G["跑 baseline 模型"] G --> H["分析错误案例"] H --> I{"边界规则需修订?"} I -->|"是"| J["更新规范 + 追溯修正"] J --> K["采集更多真实数据"] K --> D I -->|"否"| L["继续扩充至目标量"] L --> M["dataset_v2.csv"]

这个循环就是"数据驱动的迭代"--不是标完一次就结束,而是标注 → 训练 → 看错误 → 改规范 → 再标注.

常见陷阱

这些错误会浪费大量时间

  • 不写规范就开标:标到第 100 条发现前 50 条标准不统一,全部返工
  • 只标典型 case:模型在极端样本上全对,边界样本全错--因为从没见过边界
  • 扩充数据同质化:200 条数据是 40 条种子的近义词替换,模型学到的是同一批模式
  • 忽略"正常"类的多样性:对骂数据标了很多花样,正常数据全是"收到""好的"--上线后把任何长句都判为对骂
  • 标注后不看错误:训练完不分析错误案例,无法发现规范缺陷和数据盲区

本篇产出与下一步

本篇完成后应该有:

  • 一份标注规范文档(labeling_spec.md)
  • 一个关键词规则脚本(快速筛选候选)
  • 一个扩充后的数据集(目标 200+ 条,1:1 比例)

下一篇(03 - 传统方法快速出 baseline)会用这份数据集训练 FastText 和 TF-IDF + LogisticRegression,跑出第一个有意义的 F1 分数.

快速回顾

  • 标注规范:定义分类边界的"宪法",必须先写规范再开标
  • 边界 case:规范的价值在于处理模糊情况,典型 case 不需要规范
  • 冷启动三策略:关键词规则筛选 + 模板扩充 + 真实数据采集
  • 类别平衡:训练数据保持约 1:1,不照搬真实分布
  • 一致性验证:隔一周重标 50 条自检,IAA > 80% 才可靠
  • 迭代循环:标注 → 训练 → 错误分析 → 改规范 → 再标注,不是一次性的

动手练习

  1. 编写标注规范:编写一份 labeling_spec.md(参照本篇模板,至少包含判定条件和边界规则)
  2. 实现关键词规则脚本:输入一批文本,输出命中关键词的候选对骂列表
  3. 扩充数据集:用模板扩充 + 关键词规则,将种子数据扩充到 200 条以上,保存为 data/dataset_v1.csv
  4. 对比数据量影响:对扩充后的数据跑 LogisticRegression baseline,对比 40 条和 200 条的 F1 差异