🧮 第4章 评估方法和各种数据的处理
📄 一句话概括

文本进模型之前必须变成数字。 四步走: 数词得到词袋矩阵、去掉停用词、乘上 TF-IDF、再用余弦相似度比「像不像」。 Demo 里四句话、 17 个词,去掉 4 个停用词变成 13 个; 「清晰」只在第 1 句出现,IDF 1.9163,「手机」两句都有,IDF 被压到 1.5108; 同主题的句子余弦相似度 1.0000,跨主题的 0.0000。

📝 本节和前面几节的关系

3.2 节的 LSA 已经用过词频矩阵了,但那里是「假设表已经有了」。 这一节补上前面的步骤:这张表到底怎么造出来,造的时候有哪些坑。 顺序上它是 LSA 的前置工序, 但放在第 4 章讲,因为它和「图像怎么进来」是同一类问题——都叫「特征工程」, 都是把现实世界的东西翻译成数字。

一、第一步:把句子切成词

英文天生有空格,split() 就能切。 中文没有空格, 切词得靠专门的工具和词典——「南京市长江大桥」是「南京市/长江大桥」还是「南京/市长/江大桥」, 不同切法意思完全不同。这是中文 NLP 独有的第一道坎。 这一节为了看清流程,先假设词已经切好了(用空格分开)。 真实项目里会用 jieba、HanLP 这类分词器,或者干脆用字级别的切分绕开这个问题。 切完还有一堆清洗要做:统一大小写、去掉标点、把「ABC」和「abc」归一、把数字替换成占位符。 这些步骤不加分,但不做会掉分——同一个词因为大小写不同被当成两个词,词表凭空膨胀一倍。

二、第二步:数成词袋矩阵

建一个词典(所有出现过的词,去重后排序),然后每一句数一遍这些词出现了几次。 得到的就是词袋矩阵:行是词,列是文档,格子是次数。 Demo 里四句话、17 个词,得到一张 17 乘 4 的表。 这张表就是后面所有文本算法(LSA、NMF、朴素贝叶斯)的输入。 「词袋」这个名字已经说明了它的缺点:它把词序扔了。 「狗咬人」和「人咬狗」在这张表里完全一样。 对主题分类影响不大,对情感分析、问答这类任务就是灾难。 还有两个常见的加工。 二元语法(bigram):把相邻两个词当成一个新词,「不」+「好」变成「不好」, 这样否定词就能被区分出来。词频截断:只保留出现次数最多的前 N 个词,把长尾砍掉,控制词表大小。

三、第三步:停用词与 TF-IDF

「的」「是」「这个」「很」这些词每句都有, 它们不携带任何区分信息,却会因为词频高而主导整个矩阵。 处理办法有两种。 停用词表:直接删掉。Demo 里删了 4 个,词表从 17 降到 13。 这招简单粗暴,但停用词表要按语料定——法律文书里的「根据」「依照」在别的场景可能是有信息的。 TF-IDF:不删,但降权。IDF 的想法是「一个词如果在很多文档里都出现,它就没有区分度」。 公式是 log(总文档数 / 出现该词的文档数) 再加 1,然后拿词频去乘。 结果是:稀有词权重高,常见词权重低。 Demo 里的数:只在一句里出现的词 IDF 是 1.9163,两句都出现的词 IDF 是 1.5108。 同样是出现一次,稀有词拿到的权重高出 27%。 这就是 TF-IDF 干的活。

四、第四步:用余弦相似度比「像不像」

文本向量化之后,「像不像」就变成了几何问题。 最常用的是余弦相似度:两个向量的点积除以它们的长度乘积,等于夹角的余弦。 为什么不用欧氏距离?因为文档长度差别很大。 一篇 1000 字的文章和一篇 100 字的摘要讲同一件事,它们的词频向量长度差 10 倍,欧氏距离会很大, 但余弦相似度仍然接近 1——它只看方向,不看长度。 Demo 里同主题的两句相似度是 1.0000,跨主题的是 0.0000。 这个 0 是因为两组词完全不重叠,真实语料里不会这么干净,通常落在 0.1 到 0.6 之间, 只能比大小不能定阈值。 这套东西是搜索、推荐、去重的基础。 它就是「TF-IDF 加余弦」的经典组合,在词向量普及之前统治了十几年信息检索。 现在虽然被神经网络盖过去了,但它仍然是最便宜、最容易解释的基线。 做新项目时先跑一个 TF-IDF 基线,后面复杂的模型如果打不过它,说明有地方搞错了。

五、动手 Demo:四句话,从文字到可以算的向量

四句话:两句讲手机(屏幕、电池、耐用),两句讲耳机(音质、价格、做工)。 词已经用空格切好——这一步在真实项目里要靠分词器,这里先跳过。 代码四步:数词、去停用词、算 TF-IDF、算余弦。每一步都只做一件事,这样出了问题也好定位。

import numpy as np

# 第一步:数词,得到词袋矩阵(行是词,列是句子)
A = np.zeros((len(vocab), len(DOCS)))
for j, s in enumerate(DOCS):
    for w in s.split():
        A[vocab.index(w), j] += 1.0

# 第二步:去掉停用词
keep = [i for i, w in enumerate(vocab) if w not in STOP]
A2 = A[keep]

# 第三步:TF-IDF —— 词频乘上「这个词有多稀有」
df = np.array([(A2[i] > 0).sum() for i in range(len(keep))], float)
idf = np.log((1.0 + len(DOCS)) / (1.0 + df)) + 1.0
tfidf = A2 * idf[:, None]

# 第四步:余弦相似度 —— 只看方向,不看长度
def cos(a, b):
    return (a * b).sum() / np.sqrt((a * a).sum() * (b * b).sum())

实跑输出:

--------------------------------------------------------------------
文本:切成词,再数成表
--------------------------------------------------------------------

  四句话,两句讲手机、两句讲耳机。**算法拿到的必须是数字,所以第一步永远是「数词」。**
   这一节只讲一件事:怎么把一段话变成一张能算的表。

  【一】第一步:分词(这里的「词」是空格切出来的)
  英文天生有空格,切词几乎白送。**中文没有空格,切词本身就是一个专门的研究方向。**
   这里为了看清流程,先用空格把词隔开,假装已经切好了。
  词        第 1 句 第 2 句 第 3 句 第 4 句
  ----------------------------------------
  这个      1       0       1       0
  手机      1       1       0       0
  屏幕      1       1       0       0
  很        1       0       1       0
  清晰      1       0       0       0
  电池      1       1       0       0
  也        1       0       0       0
  耐用      1       1       0       0
  不        0       1       0       0
  一般      0       1       0       1
  耳机      0       0       1       1
  音质      0       0       1       1
  好        0       0       1       1
  价格      0       0       1       0
  便宜      0       0       1       0
  但        0       0       0       1
  做工      0       0       0       1
  **这张表就是「词袋」。**
   17 个词乘 4 句话,每格是出现次数。注意「这个」「很」「也」这些词——它们出现了,
  但不携带任何主题信息,下一段就把它们处理掉。

  【二】停用词:把没信息量的词扔掉
  去掉 4 个停用词,词表从 17 降到 13。**这一步看着粗暴,其实很关键**
  ——停用词在每一句里都出现,它们的词频会被当成「共性」,反而把真正区分主题的词压下去。
  保留的词
  ----------
  手机
  屏幕
  清晰
  电池
  耐用
  不
  一般
  耳机
  音质
  好
  价格
  便宜
  做工

  【三】TF-IDF:常见的词反而要降权
  词        出现在几句里  IDF       在第 1 句里的权重
  --------------------------------------------------
  清晰      1             1.9163    1.9163
  不        1             1.9163    0.0000
  价格      1             1.9163    0.0000
  便宜      1             1.9163    0.0000
  手机      2             1.5108    1.5108
  屏幕      2             1.5108    1.5108
  **IDF 的意思是「越少见的词越值钱」。**
   一个词只在一句里出现,说明它有区分度;两句都有,说明它对区分帮助不大。
  **TF-IDF 就是「词频乘上这个稀有度」。**
  看第 1 句那一列:「清晰」只在这一句出现,IDF 1.9163,权重就高;「手机」两句都有,
  IDF 被压到 1.5108。**同样是出现一次,值钱的词拿到的权重更高。**

  【四】余弦相似度:两句话像不像
  句子对                        余弦相似度
  ------------------------------------------
  第 1 句 vs 第 2 句(都是手机)1.0000
  第 3 句 vs 第 4 句(都是耳机)1.0000
  第 1 句 vs 第 3 句(跨主题)  0.0000
  **同主题的相似度 1.00 左右,跨主题的是 0。**
   余弦相似度看的是「方向像不像」,不看长度,所以长文档不会因为词多就自动和所有文档都像。
  这就是搜索、推荐、去重背后最基础的一步。
  **把文本变成向量之后,「像不像」就变成了「夹角小不小」,剩下的事交给几何。**

词袋矩阵那张表要竖着看。每一列是一句话,非零的位置就是这句话用到的词。 前四列和后四列几乎不重叠——这就是后面余弦相似度能到 0 的原因。 停用词那一步删掉了 4 个,词表从 17 降到 13。删掉的「这个」「很」「也」「但」在两句话里都出现, 对区分手机和耳机毫无帮助。 但注意「不」被保留了——它虽然短,却能翻转语义,是典型「不该进停用词表」的词。 TF-IDF 表里 IDF 只有两个值:1.9163 和 1.5108。因为只有 4 句文档, 「出现 1 次」和「出现 2 次」就只有两档。真实语料几万篇,IDF 会是一条连续的曲线。 余弦那张表 1.0000、1.0000、0.0000。前两个 1.0 有点假——因为两句用的词完全一样,只是长短不同, 方向完全一致。真实语料不会这样,别拿这个数当预期。

30 秒实验

  1. 把第 2 句的「一般」改成「很好」,它和第 1 句的相似度会升到多少?
  2. 把停用词表里的「不」也删掉,会不会有哪句话的相似度变化?为什么?
  3. 只保留出现次数最多的 5 个词(词频截断),余弦相似度会变成什么样?

常见误区

  1. 忘了做归一化就比距离。文档长短不一,欧氏距离会被长度主导。 文本相似度用余弦,不要用欧氏。
  2. 停用词表照抄别人的。「不」「没」在某些停用词表里被当成停用词,删掉之后情感分析直接失灵。 表要按任务定,不能通用。
  3. 以为 TF-IDF 权重可以跨语料比较。IDF 是相对当前语料的统计量,换一批文档, 同一个词的 IDF 就变了。两个不同语料的 TF-IDF 向量不能直接比大小。
自测三问(点开对答案) 1. 为什么文本相似度用余弦而不是欧氏距离? —— 因为文档长度差别大,欧氏距离会被向量长度主导;余弦只看方向,长度不同的同主题文档仍然相似。 2. TF-IDF 里的 IDF 解决了什么问题? —— 解决了「常见词权重过大」的问题。每篇文档都出现的词区分度低,IDF 把它压低; 只在少数文档出现的词区分度高,IDF 把它抬高。 3. 词袋模型扔掉了什么信息?会造成什么后果? —— 扔掉了词序。「狗咬人」和「人咬狗」在词袋里完全一样。 对主题分类影响不大,对情感分析、问答这类依赖语序的任务是致命伤。

文本的处理路线是「切词、数数、加权」。 图像走的是另一条路——它天生就是数字,问题只在于怎么把这些数字摆成模型喜欢的样子。

相关笔记