查看全课大纲(第 61 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
文本分类实战:从朴素贝叶斯到逻辑回归
小象实战讲义 · 自然语言处理
文本分类是自然语言处理最基础、应用最广泛的任务之一,从垃圾邮件过滤、情感分析到工单自动分派,其核心都是将一段文本归入预定义的类别。本节我们将通过一个完整的实战微例,亲手实现两个经典的文本分类模型——多项式朴素贝叶斯与逻辑回归,并深入探讨从数据标注、特征工程到模型评估、阈值调优的完整实战流程。学完本节,你将掌握构建一个可靠文本分类系统的核心方法论,并理解不同模型在小样本场景下的行为差异。
💡 核心导读
- 实战起点是标注:类目定义的可判定性决定了标注一致性,而标注一致性是模型性能的上限。
- 两个经典模型对照:实现多项式朴素贝叶斯(带拉普拉斯平滑)与逻辑回归(批梯度下降),观察它们在小样本、高维稀疏特征下的表现。
- 评估不止于准确率:深入理解类别不均衡下的宏平均F1、训练/验证/测试集的正确划分、以及业务代价驱动的阈值调优。
- 特征工程与模型选择:对比字符级n-gram特征的不同表示(计数 vs. 二值(布尔)特征),理解特征独立假设被违反时模型的行为。
- 从微例到真实项目:提炼出项目中“不变”的评估纪律与“会变”的技术栈,建立可迁移的实战框架。
一、问题定义与数据准备
我们模拟一个工单标题分类场景:将用户提交的短文本标题自动分类为“咨询”或“投诉”。这是典型的二分类问题。首先,我们必须明确类目定义:
- 咨询:用户询问产品功能、价格、使用方法、政策条款等,核心意图是获取信息。
- 投诉:用户表达对产品、服务或人员的不满,核心意图是申诉问题并要求解决。
定义必须可判定,例如“涉及‘怎么’、‘多少钱’、‘能否’等疑问词且无负面情绪词归为咨询;包含‘差’、‘慢’、‘坏’、‘不满意’等负面词且表达诉求归为投诉”。清晰的界定是后续标注一致性的基础。
接下来,我们在代码块内硬编码一个极小的数据集,包含12条标题及其人工标注的标签(0代表“咨询”,1代表“投诉”)。这个规模足以演示核心流程,且结果可手算验证。
import numpy as np
# 硬编码数据:12条工单标题,标签 0=咨询, 1=投诉
texts = [
"怎么开通会员", # 0
"会员价格是多少", # 0
"退款流程怎么操作", # 0
"客服态度差", # 1
"物流太慢了", # 1
"商品质量有问题", # 1
"如何修改收货地址", # 0
"优惠券如何使用", # 0
"订单一直不发货", # 1
"app经常闪退", # 1
"会员续费有优惠吗", # 0
"收到的商品是坏的", # 1
]
labels = np.array([0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1])
print("数据集概览:")
for i, (t, l) in enumerate(zip(texts, labels)):
print(f"{i:2d}: [{l}] {t}")
print(f"\n类别分布: 咨询({np.sum(labels==0)}条), 投诉({np.sum(labels==1)}条)")数据集概览:
0: [0] 怎么开通会员
1: [0] 会员价格是多少
2: [0] 退款流程怎么操作
3: [1] 客服态度差
4: [1] 物流太慢了
5: [1] 商品质量有问题
6: [0] 如何修改收货地址
7: [0] 优惠券如何使用
8: [1] 订单一直不发货
9: [1] app经常闪退
10: [0] 会员续费有优惠吗
11: [1] 收到的商品是坏的
类别分布: 咨询(6条), 投诉(6条)二、特征工程:字符级Bigram词袋
对于短文本,字符级bigram(相邻两个字符)能较好地捕捉中文的局部组合信息,且比分词更稳定。我们将文本转化为词袋(Bag-of-Words)向量。这里实现两个版本:
- 计数特征:直接统计每个bigram出现的次数,适用于朴素贝叶斯。
- 二值特征:只记录bigram是否出现(0或1),有时对逻辑回归更稳定。
import numpy as np
texts = [
"怎么开通会员", "会员价格是多少", "退款流程怎么操作", "客服态度差",
"物流太慢了", "商品质量有问题", "如何修改收货地址", "优惠券如何使用",
"订单一直不发货", "app经常闪退", "会员续费有优惠吗", "收到的商品是坏的",
]
labels = np.array([0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1])
def build_char_bigram_vocab(text_list):
vocab = {}
for text in text_list:
chars = list(text)
for i in range(len(chars) - 1):
bigram = chars[i] + chars[i + 1]
if bigram not in vocab:
vocab[bigram] = len(vocab)
return vocab
def texts_to_count_matrix(text_list, vocab):
X = np.zeros((len(text_list), len(vocab)), dtype=int)
for i, text in enumerate(text_list):
chars = list(text)
for j in range(len(chars) - 1):
bigram = chars[j] + chars[j + 1]
if bigram in vocab:
X[i, vocab[bigram]] += 1
return X
def texts_to_binary_matrix(text_list, vocab):
X = np.zeros((len(text_list), len(vocab)), dtype=int)
for i, text in enumerate(text_list):
chars = list(text)
for j in range(len(chars) - 1):
bigram = chars[j] + chars[j + 1]
if bigram in vocab:
X[i, vocab[bigram]] = 1
return X
vocab = build_char_bigram_vocab(texts)
X_count = texts_to_count_matrix(texts, vocab)
X_binary = texts_to_binary_matrix(texts, vocab)
print(f"特征词典大小(唯一bigram数): {len(vocab)}")
print("前10个bigram特征:", sorted(vocab.keys())[:10])
nz = [(b, i, int(X_count[0, i])) for b, i in vocab.items() if X_count[0, i] > 0]
nz.sort(key=lambda x: x[1])
print("\n第一条样本的计数特征向量(非零部分):")
total = 0
for b, i, c in nz:
print(f" '{b}'(索引{i}): 计数={c}")
total += c
print(f" 合计 token 数: {total}")
print(f"\n特征矩阵形状: 计数版 {X_count.shape}, 二值版 {X_binary.shape}")特征词典大小(唯一bigram数): 65
前10个bigram特征: ['ap', 'pp', 'p经', '一直', '不发', '么开', '么操', '价格', '优惠', '会员']
第一条样本的计数特征向量(非零部分):
'怎么'(索引0): 计数=1
'么开'(索引1): 计数=1
'开通'(索引2): 计数=1
'通会'(索引3): 计数=1
'会员'(索引4): 计数=1
合计 token 数: 5
特征矩阵形状: 计数版 (12, 65), 二值版 (12, 65)三、模型A:多项式朴素贝叶斯(Multinomial Naive Bayes)
朴素贝叶斯基于贝叶斯定理与特征条件独立性假设。对于多项式模型,我们使用计数特征 。给定类别 ,特征 (此处为bigram)的似然服从多项式分布。引入拉普拉斯平滑(参数 )避免零概率问题。
预测时计算对数后验概率以避免下溢: 其中 为类先验, 为条件概率。
import numpy as np
texts = [
"怎么开通会员", "会员价格是多少", "退款流程怎么操作", "客服态度差",
"物流太慢了", "商品质量有问题", "如何修改收货地址", "优惠券如何使用",
"订单一直不发货", "app经常闪退", "会员续费有优惠吗", "收到的商品是坏的",
]
labels = np.array([0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1])
def build_vocab(text_list):
v = {}
for t in text_list:
cs = list(t)
for i in range(len(cs) - 1):
bg = cs[i] + cs[i + 1]
v.setdefault(bg, len(v))
return v
def to_matrix(text_list, vocab, binary=False):
X = np.zeros((len(text_list), len(vocab)), dtype=int)
for i, t in enumerate(text_list):
cs = list(t)
for j in range(len(cs) - 1):
bg = cs[j] + cs[j + 1]
if bg in vocab:
X[i, vocab[bg]] = 1 if binary else X[i, vocab[bg]] + 1
return X
vocab = build_vocab(texts)
X = to_matrix(texts, vocab)
alpha = 1.0
classes = [0, 1]
n_samples, n_features = X.shape
class_logprior = np.array([np.log(np.sum(labels == c) / n_samples) for c in classes])
logp = np.zeros((len(classes), n_features))
for k, c in enumerate(classes):
Nc_i = X[labels == c].sum(axis=0)
Nc = Nc_i.sum()
logp[k] = np.log((Nc_i + alpha) / (Nc + alpha * n_features))
scores = class_logprior[None, :] + X @ logp.T
pred = np.array(classes)[scores.argmax(axis=1)]
def metrics(y_true, y_pred, cs):
out = {}
for c in cs:
tp = int(np.sum((y_true == c) & (y_pred == c)))
fp = int(np.sum((y_true != c) & (y_pred == c)))
fn = int(np.sum((y_true == c) & (y_pred != c)))
p = tp / (tp + fp) if tp + fp else 0.0
r = tp / (tp + fn) if tp + fn else 0.0
f = 2 * p * r / (p + r) if p + r else 0.0
out[c] = (p, r, f, tp, fp, fn)
return out
acc = float(np.mean(pred == labels))
m = metrics(labels, pred, classes)
macro_f1 = np.mean([m[c][2] for c in classes])
print("多项式朴素贝叶斯(alpha=1.0,训练集):")
print(f"准确率: {acc:.4f}")
print(f"宏平均F1: {macro_f1:.4f}")
for c in classes:
p, r, f, tp, fp, fn = m[c]
name = "咨询" if c == 0 else "投诉"
print(f" {name}: P={p:.4f} R={r:.4f} F1={f:.4f} (TP={tp} FP={fp} FN={fn})")
print("预测标签:", pred.tolist())
# 首样本的判别分数(可手算复核)
print("\n样本0《怎么开通会员》的两类对数分数:")
for k, c in enumerate(classes):
name = "咨询" if c == 0 else "投诉"
print(f" {name}: log先验={class_logprior[k]:.4f} + 似然和={(X[0] @ logp[k]):.4f} = {scores[0, k]:.4f}")
print(f" 判别: {'咨询' if scores[0, 0] > scores[0, 1] else '投诉'}")多项式朴素贝叶斯(alpha=1.0,训练集):
准确率: 1.0000
宏平均F1: 1.0000
咨询: P=1.0000 R=1.0000 F1=1.0000 (TP=6 FP=0 FN=0)
投诉: P=1.0000 R=1.0000 F1=1.0000 (TP=6 FP=0 FN=0)
预测标签: [0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1]
样本0《怎么开通会员》的两类对数分数:
咨询: log先验=-0.6931 + 似然和=-18.6093 = -19.3024
投诉: log先验=-0.6931 + 似然和=-22.9248 = -23.6180
判别: 咨询手算验证点:以第一条样本“怎么开通会员”为例,其特征为‘怎么’、‘么开’、‘开通’、‘通会’、‘会员’。我们可以近似验证:在“咨询”类中,这些bigram的对数概率之和加上类先验对数,应大于在“投诉”类中的对应和。由于数据完全可分,模型在训练集上达到了100%准确率。
四、模型B:逻辑回归(Logistic Regression)
逻辑回归直接对条件概率 建模,使用sigmoid函数: 我们使用批梯度下降最小化交叉熵损失函数,迭代 200 步、学习率 0.1。这里如实说明一个取舍:文本的二值特征本身尺度就只有 0 和 1,本例不做标准化,直接拿原始二值特征训——这样每一步的权重变化读者可以手工复核。真实项目里若特征尺度差异大(比如把”文本长度”和”某个 bigram 是否出现”混在一起),才必须先标准化,否则梯度会被大量纲特征主导。
import numpy as np
texts = [
"怎么开通会员", "会员价格是多少", "退款流程怎么操作", "客服态度差",
"物流太慢了", "商品质量有问题", "如何修改收货地址", "优惠券如何使用",
"订单一直不发货", "app经常闪退", "会员续费有优惠吗", "收到的商品是坏的",
]
labels = np.array([0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1])
def build_vocab(text_list):
v = {}
for t in text_list:
cs = list(t)
for i in range(len(cs) - 1):
v.setdefault(cs[i] + cs[i + 1], len(v))
return v
def to_binary(text_list, vocab):
X = np.zeros((len(text_list), len(vocab)), dtype=float)
for i, t in enumerate(text_list):
cs = list(t)
for j in range(len(cs) - 1):
bg = cs[j] + cs[j + 1]
if bg in vocab:
X[i, vocab[bg]] = 1.0
return X
vocab = build_vocab(texts)
X = to_binary(texts, vocab)
y = labels.astype(float)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
lr, n_iter = 0.1, 200
w, b = np.zeros(X.shape[1]), 0.0
loss_hist = []
for it in range(n_iter):
p = sigmoid(X @ w + b)
pc = np.clip(p, 1e-15, 1 - 1e-15)
loss = -np.mean(y * np.log(pc) + (1 - y) * np.log(1 - pc))
loss_hist.append(loss)
w -= lr * (X.T @ (p - y) / len(y))
b -= lr * np.mean(p - y)
print(f"逻辑回归(lr={lr}, 迭代={n_iter}, 二值特征, d={X.shape[1]}):")
print("损失轨迹:")
for k in [0, 1, 2, 3, 4, 49, 99, 149, 198, 199]:
print(f" 第{k + 1:>3}步: loss = {loss_hist[k]:.4f}")
probes = ["会员商品退款", "会员怎么退款"]
probe_X = to_binary(probes, vocab)
probe_p = sigmoid(probe_X @ w + b)
print("\n两条边界样本的概率(含“会员”这个咨询侧特征,也含投诉侧词汇):")
for name, pv in zip(probes, probe_p):
print(f" 《{name}》: P(投诉) = {pv:.4f}")
for th in (0.3, 0.5, 0.7):
print(f" 阈值 {th:.1f} → {'投诉' if pv >= th else '咨询'}")
w_sorted = np.argsort(-np.abs(w))[:5]
key_by_idx = {v: k for k, v in vocab.items()}
print("\n权重绝对值最大的5个bigram:")
for idx in w_sorted:
print(f" '{key_by_idx[idx]}': w = {w[idx]:.4f}")
print(f"偏置 b = {b:.4f}")逻辑回归(lr=0.1, 迭代=200, 二值特征, d=65):
损失轨迹:
第 1步: loss = 0.6931
第 2步: loss = 0.6785
第 3步: loss = 0.6643
第 4步: loss = 0.6506
第 5步: loss = 0.6372
第 50步: loss = 0.3052
第100步: loss = 0.1811
第150步: loss = 0.1259
第199步: loss = 0.0961
第200步: loss = 0.0956
两条边界样本的概率(含“会员”这个咨询侧特征,也含投诉侧词汇):
《会员商品退款》: P(投诉) = 0.3996
阈值 0.3 → 投诉
阈值 0.5 → 咨询
阈值 0.7 → 咨询
《会员怎么退款》: P(投诉) = 0.1579
阈值 0.3 → 咨询
阈值 0.5 → 咨询
阈值 0.7 → 咨询
权重绝对值最大的5个bigram:
'会员': w = -0.9318
'怎么': w = -0.6527
'如何': w = -0.6527
'商品': w = 0.6138
'优惠': w = -0.6039
偏置 b = 0.2355这份输出要连着看三处:
- loss 从 0.6931 降到 0.0956:0.6931 就是 ln2,也就是”两眼一抹黑、两类各一半”的起点;200 步后已经明显分开,但没到 0(数据虽可分,梯度下降是渐近逼近)。
- 权重的正负就是”词的立场”:
会员、怎么、如何、优惠都是负权重(推向”咨询”),商品是正权重(推向”投诉”)。这不是人工指定的,是数据推出来的——和上一节 BPE 用频次长出子词是同一个道理。 - 阈值真的会改变结论:《会员商品退款》的 P(投诉)=0.3996,夹在 0.3 和 0.5 之间,所以阈值取 0.3 判投诉、取 0.5 判咨询。同一句话、同一个模型、只改一个数字,业务结论就变了——这就是下一节要讲的”阈值按代价调”,不是调参洁癖。
五、实战要点剖析与逻辑映射
1. 标注口径先于模型
我们的微例假设标注完全一致。真实项目中,必须为每个类目撰写可执行的判定规则,并进行抽样双标计算一致率(如Cohen‘s Kappa)。标注一致性是模型性能的上限,不一致的标注会导致模型学习到噪声。
2. 类别不均衡与评估指标
本例两类样本均衡(6:6)。若不均衡(如咨询:投诉 = 9:1),准确率会被多数类绑架(即使全预测为咨询也有90%准确率)。此时必须同时看每类的精确率、召回率、F1,并以宏平均F1(Macro-F1,对各类F1取平均)作为核心指标,因为它平等看待每个类别,不受样本量影响。Micro-F1会被大类别主导,在此场景不适用。
3. 划分与泄漏
本例未划分训练/测试集,仅为演示。真实项目必须严格分离。关键原则:同一来源的近似重复样本(如同一用户短时间内提交的相同工单)不能分散到训练集和测试集,否则会因数据泄漏导致分数虚高。小数据建议使用分层K折交叉验证,报告均值与标准差。
4. 特征与归一化
我们对比了字符 bigram 的计数特征(用于 NB)和二值特征(用于 LR)。NB 的似然模型假设特征之间条件独立,用的是计数;LR 没有这个假设,靠权重把共现关系学进去。本例刻意没做标准化:二值特征尺度一致,标准化只是把 0/1 换成两个对称值,换来的是每步更新量翻倍、读者不好手算。真实项目里如果混入长度类、计数类特征,标准化就必须做;文本场景更常见的是 TF-IDF 加权,把高频常见词的权重压下去。
5. 阈值与业务代价
默认阈值0.5假设“漏检投诉”和“误判投诉”代价相同。现实中,漏检投诉(本该是投诉却判为咨询)可能导致用户不满升级,代价更高。我们可以通过调整阈值来权衡:
- 降低阈值(如0.3):更多样本被预测为投诉,召回率上升(找到更多真实投诉),但精确率下降(混入更多咨询)。
- 提高阈值(如0.7):相反,精确率上升,召回率下降。
调整依据应是业务代价矩阵。例如,若漏检投诉的代价是误判投诉的5倍,则应倾向于选择高召回率的阈值。
6. 上线后的监控与漂移
模型上线后,语言会随时间变化(新词、新话术)。监控需分两层:
- 输入分布监控:词表外(OOV)比例、文本长度分布、高频词变化。
- 输出分布监控:预测类别占比的偏移、预测置信度的分布变化(如原本高置信度的样本比例下降)。
若线上指标掉点,应先检查输入数据是否发生漂移,再考虑重新训练模型。
逻辑映射:从微例到真实项目
不变的核心纪律:
- 评估口径:严格的训练/验证/测试分离,报告每类指标与宏平均F1。
- 标注决定上限:清晰的类目定义与标注一致性校验。
- 阈值按业务代价调整,而非默认0.5。
会变的技术要素:
- 数据规模:从十几条到上万条 → 需用交叉验证替代手算,并可能引入主动学习策略来高效挑选标注样本。
- 模型选择:从NB/LR到预训练模型(如BERT)微调 → 特征工程消失,模型能力大幅提升,但标注与评估的纪律不变。
- 类别数量:从2类到几十类长尾分布 → 需要分层抽样确保每类有足够样本,并关注尾部类别的F1。
- 服务模式:从离线批处理到在线实时服务 → 需考虑延迟预算、模型版本管理、A/B测试与灰度发布机制。
一句话结论:换模型、换数据规模,改变的是模型性能“上限的斜率”;而不变的是“评估与标注的地基”,它决定了你的项目能否可靠落地。
📝 动手练一练
- 概念辨析:在类别不均衡的场景下(例如90%咨询,10%投诉),为什么准确率(Accuracy)不是一个可靠的评估指标?宏平均F1(Macro-F1)为什么更适合?
👉 点击查看参考答案
准确率计算的是正确预测的样本占总样本的比例。在极度不均衡的场景下(如90%咨询,10%投诉),一个简单的“将所有样本预测为咨询”的模型,就能获得90%的准确率,但这显然没有识别出任何投诉样本,模型是无效的。准确率被多数类“绑架”了。
宏平均F1先为每个类别单独计算F1分数(综合考虑了该类别的精确率和召回率),然后对所有类别的F1分数取算术平均值。这意味着每个类别无论样本多少,对最终指标的贡献是平等的。因此,即使投诉类只占10%,它的F1分数仍然被同等看待,模型必须努力识别出少数类才能获得高的宏平均F1。这使其成为不均衡分类任务中更合适的评估指标。
- 手算验证:根据我们训练的朴素贝叶斯模型,对于bigram特征‘客服’(假设它在词典中的索引是idx),请写出计算 和 的公式(包含拉普拉斯平滑 ),并定性比较哪个类别的条件概率会更高?为什么?
👉 点击查看参考答案
计算公式(基于计数特征): 其中:
- 是类别 中‘客服’这个bigram出现的总次数。
- 是类别 中所有bigram的总次数。
- 是特征总数(本例中为 65,即去重后的字符 bigram 数)。
- 是平滑参数(本例为1.0)。
定性比较:‘客服’这个词出现在标题“客服态度差”中,该标题属于投诉类。因此,在投诉类中 ,而在咨询类中 。代入公式后,分子部分投诉类更大,而分母 在两个类别中相差不会特别悬殊(因为总样本数相同)。因此, 会显著高于 。这也符合直觉:“客服”一词在与负面情绪相关的投诉中更可能出现。
本章小结
本节通过一个完整的文本分类微例,实战演练了从数据准备、特征工程(字符bigram词袋)到实现两个经典模型(多项式朴素贝叶斯与逻辑回归)的全过程。我们深入探讨了文本分类实战中的六个关键要点:标注口径的决定性作用、不均衡数据的评估指标选择、数据划分与泄漏预防、特征表示与归一化、基于业务代价的阈值调优以及上线后的监控策略。最后,我们提炼了从微例到真实项目的逻辑映射,强调了无论技术栈如何演进,严谨的评估与标注纪律始终是项目成功的基石。
📋 行动清单
- 亲手运行本节四个自包含代码块(数据、特征、NB、LR),观察两个模型在小数据集上的差异,并改一条数据(比如把”会员价格是多少”硬标成投诉)看指标怎么塌。
- 设计一个评估方案:假设你有一个1000条、类别比例为85%咨询 vs. 15%投诉的数据集,请描述你将如何划分数据集、选择核心评估指标,并说明理由。
- 思考业务代价:针对工单分类场景,你认为“漏检投诉”和“误判投诉”哪一项业务代价更高?如果前者代价是后者的3倍,你会倾向于调整分类阈值向哪个方向移动?
延伸阅读
- 朴素贝叶斯文本分类的经典论文:Text Classification from Labeled and Unlabeled Documents using EM (Nigam et al., 2000)。该论文深入探讨了EM算法在半监督场景下提升朴素贝叶斯性能的方法。
- 逻辑回归与最大熵模型在NLP中的应用综述:A Maximum Entropy Approach to Natural Language Processing (Berger et al., 1996)。最大熵模型是逻辑回归在多分类上的推广,曾是统计NLP的基石模型之一。
- 文本分类特征选择方法实证比较:An Extensive Empirical Study of Feature Selection Metrics for Text Classification (Forman, G., Journal of Machine Learning Research 3: 1289–1305, 2003)。其中实证比较了十二种特征选择方法(如卡方检验、互信息等),至今仍有参考价值。
—— 小象教研组