问题与目标
预训练模型并不是文本分类的唯一入口。数据量较小、类别边界明显或资源受限时,TF-IDF 配合朴素贝叶斯、逻辑回归等传统模型往往能快速建立可靠基线。
本篇使用自创的工作消息完成一个三分类任务,重点不是追求高分,而是跑通数据划分、特征提取、训练、预测、指标和错误样本分析。示例只依赖 Python 标准库。

指标不是流程终点。错误样本会反过来暴露标签定义、数据覆盖和特征表示的问题,再决定是补数据、改规则还是更换模型。
核心概念
先定义标签边界
示例把消息分成三类:
incident:超时、失败、不可用等故障信息。capacity:磁盘、内存、队列积压等容量信息。recovery:恢复、重启成功、任务完成等状态恢复信息。
“磁盘故障已经恢复”同时涉及容量、故障和恢复。如果任务要求单标签,就要规定优先级;如果业务允许多个标签,就应改成多标签分类,不能靠随意标注掩盖歧义。
为什么选择字符 n-gram
中文字符 n-gram 不依赖外部分词器,对设备编号、缩写和新词较稳健。代价是特征量较大,也可能把无意义片段当成特征。这里组合单字和二元组,让小数据示例具有基本区分能力。
朴素贝叶斯在计算什么
多项式朴素贝叶斯比较:
log P(类别) + Σ 特征次数 × log P(特征 | 类别)
“朴素”来自条件独立假设:给定类别后,各特征被当作相互独立。这个假设并不完全符合语言,但模型简单、训练快,适合做文本基线。
可运行实现
import math
from collections import Counter, defaultdict
TRAIN = [
("接口连续超时", "incident"),
("任务执行失败", "incident"),
("服务无法访问", "incident"),
("节点连接中断", "incident"),
("磁盘空间不足", "capacity"),
("内存使用率过高", "capacity"),
("消息队列积压", "capacity"),
("缓存容量接近上限", "capacity"),
("服务已经恢复", "recovery"),
("节点重启成功", "recovery"),
("批处理任务完成", "recovery"),
("网络状态恢复正常", "recovery"),
]
TEST = [
("网络请求超时", "incident"),
("磁盘使用率达到上限", "capacity"),
("定时任务恢复运行", "recovery"),
("接口调用失败", "incident"),
]
def features(text: str) -> list[str]:
clean = "".join(text.lower().split())
chars = list(clean)
pairs = [clean[i : i + 2] for i in range(len(clean) - 1)]
return chars + pairs
class MultinomialNB:
def fit(self, rows: list[tuple[str, str]]) -> None:
self.label_counts = Counter(label for _, label in rows)
self.term_counts = defaultdict(Counter)
self.term_totals = Counter()
self.vocab = set()
for text, label in rows:
counts = Counter(features(text))
self.term_counts[label].update(counts)
self.term_totals[label] += sum(counts.values())
self.vocab.update(counts)
self.total_rows = len(rows)
def predict_one(self, text: str) -> tuple[str, dict[str, float]]:
counts = Counter(features(text))
scores = {}
vocab_size = len(self.vocab)
for label, label_count in self.label_counts.items():
score = math.log(label_count / self.total_rows)
denominator = self.term_totals[label] + vocab_size
for term, count in counts.items():
numerator = self.term_counts[label][term] + 1
score += count * math.log(numerator / denominator)
scores[label] = score
prediction = max(scores, key=scores.get)
return prediction, scores
model = MultinomialNB()
model.fit(TRAIN)
correct = 0
for text, expected in TEST:
predicted, scores = model.predict_one(text)
correct += predicted == expected
mark = "✓" if predicted == expected else "✗"
print(f"{mark} {text:<12} expected={expected:<8} predicted={predicted}")
print(f"accuracy={correct / len(TEST):.2f}")
每次训练和测试使用固定数据,因此结果可以重复。正式项目不应手工挑选测试样本,而应在建模前完成分层划分并固定随机种子。
怎样分析错误样本
若“服务内存不足后已恢复”被预测成 capacity,至少有四种可能:
- 标签规则没有规定“恢复”是否优先。
- 训练集中没有复合表达。
- n-gram 只捕捉局部片段,缺少句子级上下文。
- 单标签任务本身不适合这类消息。
不要只把错误归因于模型弱。先确定问题属于标签、数据、特征还是模型,再选择改进手段。
常见问题与排查
训练分数很高,测试效果很差
检查样本量、重复文本和数据划分。相同模板生成的近重复文本不能分散到训练集与测试集,否则结果会虚高。
某个类别从不被预测
查看类别样本量和关键词覆盖,打印每类得分。如果类别极不平衡,应补充数据或调整类别权重,而不是只改预测阈值。
准确率看起来不错,关键故障漏报很多
单独计算 incident 类的 Recall。业务更关心漏报时,模型选择和阈值都应围绕 Recall 与误报成本权衡。
换成预训练模型后没有明显提升
先确认是否使用同一数据划分和指标,并检查微调、截断和标签映射。复杂模型无法自动修复含糊标签和泄漏数据。
小结
文本分类基线提供了一个可复现的比较起点。它把文本表示、模型、指标和错误分析串在一起,也能帮助判断后续引入深度模型究竟解决了什么问题,而不是只增加依赖和计算量。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


