🧠 第四章 机器学习算法
📄 一句话概括

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 秒实验

  1. 把 bigram 去掉,只留单字,重跑看准确率变多少。
  2. 把语料扩到 100 条以上(自己再写几十条),看准确率会不会稳定下来。
  3. 想一想:为什么「按片段数取平均」能修好一边倒的判断?

常见误区

  1. 「中文分词一定要用现成库」——字符 bigram 也能跑,只是粗。
  2. 「准确率高就是好模型」——10 条测试集,换次划分就变。
  3. 「概率直接相乘就行」——长文本会天然占优,得先归一。
自测三问(点开对答案) 1. 没有分词库时,这个 Demo 怎么切中文? —— 切成单字,再加上相邻两字的 bigram。 2. 为什么第一版把所有样本都判成正面? —— 对数概率直接累加,长文本片段多、绝对值大,把先验压过去了。 3. 评估小语料模型时该看什么? —— 看它有没有错得离谱,而不是看那个两位小数的准确率。

文本之后是推荐。下一节算用户相似度,并和「推热门」比一比。

相关笔记