36 条中文短评,不用分词库,只用单字加相邻两字的 bigram,得到 419 个片段。 字符 bigram 加朴素贝叶斯,10 条测试集上准确率 0.600——不比猜高多少,但整条链是真的。
上一节处理图像,这一节处理文本。 图像是连续的像素,文本是离散的字,第一步永远是「把字变成数字」。
一、中文的第一个坑:没有空格
英文天生用空格分开词,中文没有。所以「分词」是中文 NLP 的第一道关。 现成的分词库不少,但本书的沙箱里一个都没装(jieba 之类全缺)。 Demo 就用了一个最土也最稳的办法:单字 + 相邻两字的 bigram。 「这个功能很好用」切出来是: 单字「这个功能很好用」加两字片段「这个」「个功」「功能」「能很」「很好」「好用」。 听着粗糙,但它保留了「功」「能」挨在一起这个信息——而「功能」这个词在好坏评价里正好是关键。
二、从片段到向量:TF-IDF 的简化版
有了片段,第二步是数数。Demo 建了一个 419 维的表,每一维是一个片段。 朴素贝叶斯不看词频高低,只看「这个词在正面里出现得多,还是负面里多」。 用对数几率差来衡量:正数偏正面,负数偏负面。 Demo 打出来的头几名片段是「一直」(-0.94)、「一整」(0.85)、「一致」(0.85)、「个功」(0.85)。 「一直」偏负面,是因为负面语料里「一直没解决」「一直不回复」出现了; 「一致」偏正面,来自「和图片一致」。这些片段不讲道理,但它们确实有区分力。
三、一个坑:长文本天然占便宜
第一版 Demo 直接累加对数概率,结果准确率 0.600,而且「正面」的召回是 1.000、 精度只有 0.600——它把所有测试样本都判成了正面。 原因很简单:长文本有更多片段,累加的对数概率绝对值更大,于是先天的先验被彻底压过去了。 改法是把累加值除以命中片段数——取平均。 改完之后精度 0.750、召回 0.500、F1 0.600,准确率还是 0.600,但至少不再是一边倒。 这个坑在真实文本分类里天天出现。 凡是用「概率相乘」的模型,都要先处理长度差异,否则模型学到的就是「谁字数多」。
四、准确率 0.600 该怎么看
10 条测试集对 6 条,准确率 0.600。 这个数本身没什么说服力——样本太少,换一次随机划分就能上下跳 20 个百分点。 真正有价值的是三条判断:分类器判「这次真的很满意」为正面,判「还是没解决,失望」为负面, 判「东西一般般」为负面。前两条对,第三条把中性判成负面,也算合理。 所以读这种小实验的正确方式:看它会不会错得离谱,而不是看那个两位小数。 它没有把「很满意」判成负面,说明方向是对的。
五、动手 Demo:文本分类:字符 bigram 加朴素贝叶斯
手写 36 条中文短评(正面 18、负面 18),切单字与相邻两字 bigram 建词表; 随机切 26 条训练、10 条测试,用带拉普拉斯平滑的朴素贝叶斯分类; 输出词表大小、准确率、正面类精度/召回/F1、最有判别力的片段,最后判三条没见过的句子。
def grams(s):
s = s.replace(u',', u'').replace(u' ', u'')
out = list(s)
out += [s[i:i + 2] for i in range(len(s) - 1)]
return out
cnt = np.ones((2, len(vocab))) # 拉普拉斯平滑
for d, lab in tr:
for g in d:
cnt[lab, vi[g]] += 1
ll = np.log(cnt[:, vi[g]] / tot)
sc = np.log(prior) + ll / max(n, 1) # 按片段数取平均
实跑输出:
--------------------------------------------------------------------
文本分类:字符 bigram 加朴素贝叶斯,不用分词库
--------------------------------------------------------------------
训练语料 36 条(正面 18/负面 18)
分词方式 不用分词库:单字 + 相邻两字的 bigram
词表大小 419 个
训练/测试划分 26 条 / 10 条
测试集准确率 0.600(6/10)
正面类 精度/召回/F1 0.750/0.500/0.600
测试集正面条数 6
片段 对数几率差 偏向
-----------------------
一直 -0.94 负面
一整 0.85 正面
一致 0.85 正面
个功 0.85 正面
书写 0.85 正面
买了 0.85 正面
了还 0.85 正面
二次 0.85 正面
判断「这次真的很满意」 正面(好)
判断「还是没解决,失望」 负面(差)
判断「东西一般般」 负面(差)
语料只有 36 条,训练集只有 26 条,这个准确率不能外推——
但它走的路径和真实文本分类一模一样:切片段 → 计数 → 算对数几率 → 取大。
词表 419 个片段,来自 36 条短评。这个数比字数多得多,因为每条都多出了一倍的 bigram。 准确率 0.600(6/10),正面类精度 0.750、召回 0.500、F1 0.600。 训练集只有 26 条,这个结果只能说「没崩」。 判别力前几名:负面是「一直」(-0.94),正面是「一整」「一致」「个功」(各 0.85)。 「个功」这种碎片看着荒唐,但它确实只在「这个功能」里出现。 三条新句子的判断:满意→正面、失望→负面、一般般→负面。 中性被判负面是常见偏差,因为语料里没有中性这一档。
30 秒实验
- 把 bigram 去掉,只留单字,重跑看准确率变多少。
- 把语料扩到 100 条以上(自己再写几十条),看准确率会不会稳定下来。
- 想一想:为什么「按片段数取平均」能修好一边倒的判断?
常见误区
- 「中文分词一定要用现成库」——字符 bigram 也能跑,只是粗。
- 「准确率高就是好模型」——10 条测试集,换次划分就变。
- 「概率直接相乘就行」——长文本会天然占优,得先归一。
自测三问(点开对答案)
1. 没有分词库时,这个 Demo 怎么切中文? —— 切成单字,再加上相邻两字的 bigram。 2. 为什么第一版把所有样本都判成正面? —— 对数概率直接累加,长文本片段多、绝对值大,把先验压过去了。 3. 评估小语料模型时该看什么? —— 看它有没有错得离谱,而不是看那个两位小数的准确率。文本之后是推荐。下一节算用户相似度,并和「推热门」比一比。
- ☐ (本节完,下一节 4.2.3 推荐系统:电商平台商品推荐、社交媒体内容推荐)