🧠 第六章 自然语言处理
📄 一句话概括

18 句语料、22 个词、窗口 2,共现矩阵非零 86 个。 SVD 之后前五个奇异值是 5.84、5.42、4.25、3.46、3.29。 第 1 维占 21.9% 方差,装的全是词频,必须丢掉; 取第 2~4 维后,「手机」最像「相机」0.78、「镜头」0.74。

📝 本节和前面几节的关系

前五节是应用场景,从这一节起换到另一头:自己动手做一个 Embedding。 五步走完,你会得到一个能算相似度的词向量表。

一、Embedding 到底是什么

Embedding 就是把「词」变成一个定长的实数向量。要求是:意思相近的词,向量也相近。 为什么需要它?因为 one-hot 分不出远近。 「手机」和「相机」在 one-hot 里任意两维都是正交的,夹角 90 度,余弦 0。 有了向量,「手机」离「相机」近、「离」「苹果」远,这件事就能算了。 训练 Embedding 有很多路子:Word2Vec、GloVe、BERT,还有这一节要做的「共现矩阵加 SVD」。 后者最老、最慢、也最好懂。

二、第一步:数谁和谁一起出现

整件事的原料是共现次数。一句话里,隔得近的词算「一起出现」。 Demo 用窗口 2:一个词的左边两个、右边两个都算邻居。18 句语料、22 个词,数出来 86 个非零共现对。 得到的矩阵是 22 × 22。行是词,列是它的邻居,值是一起出现了几次。 这个矩阵就是全部的「知识」。后面的 SVD 只是把它压扁,不增加任何信息。 所以语料有多小,词向量就有多糙。这一节只有 18 句,做出来的东西只能看趋势,不能拿去用。

三、第二步:SVD 压维度,但别直接用前几维

对共现矩阵做奇异值分解,取前几个奇异向量,就得到低维词向量。 第一版我直接取了前 3 维,结果「手机」和「镜头」的余弦是 0.999——等于没分开。 原因很具体:第一个奇异向量是「词频」这个公共因子。所有词在它上面几乎同号,谁跟谁算余弦都接近 1。 所以标准做法是丢掉第 1 维。 Demo 里第 1 维占了 21.9% 的方差,这份方差装的是「这个词常不常出现」,不是「它是什么意思」。 丢掉之后取第 2~4 维,这三维合起来占 38.1% 的方差,语义关系才显出来。

四、第三步:看邻居,判断有没有学到东西

取第 2~4 维之后:手机最近的词是相机 0.78、镜头 0.74; 电池最近的是续航 0.99、持久 0.98;键盘最近的是打字 1.00、电脑 0.78。 这些关系一条都不是我手写的,全是共现统计自己长出来的。这就是「分布式假设」——意思由上下文决定。 但也要看清限度:这些相似度偏高(0.98、1.00),是因为矩阵太小、很多词的上下文几乎一样, 向量退化成近似共线。 真语料上,Word2Vec 训出来的邻居会更有意思,比如「国王减男人加女人等于女王」。 这一节做不出那种效果,语料量差了好几个数量级。

五、动手 Demo:共现矩阵加 SVD:把词变成能算相似度的向量

18 句短句、22 个词;按窗口 2 统计共现次数,建 22×22 矩阵; 做 SVD 后丢掉第 1 个奇异向量,取第 2~4 维当词向量;打出 12 个目标词的坐标,再算最近邻余弦。

for t in toks:
    for i, w in enumerate(t):
        for j in range(max(0, i - 2), min(len(t), i + 3)):
            if j != i:
                C[ix[w], ix[t[j]]] += 1.0

U, S, Vt = np.linalg.svd(C, full_matrices=False)
E = U[:, 1:4] * np.sqrt(S[1:4])       # 丢掉第 1 维

实跑输出:

--------------------------------------------------------------------
Embedding 训练第一步:共现矩阵做 SVD,词变成向量
--------------------------------------------------------------------
  语料句数                 18
  词表大小                 22
  窗口                     左右各 2 个词
  共现矩阵                 22 × 22,非零 86 个
  奇异值前 5 个            5.84、5.42、4.25、3.46、3.29
  取第 2~4 维              解释方差 38.1%
  第 1 维                  丢掉(21.9%,装的全是词频)

  词        d1      d2      d3
  ---------------------------
  手机   -0.52   +0.11   +0.05
  拍照   -0.63   +0.24   +0.75
  清晰   -0.44   -0.03   -0.33
  相机   -0.51   +0.18   +0.50
  镜头   -0.22   +0.03   -0.17
  屏幕   -0.35   -0.12   -0.87
  键盘   +0.34   -1.16   +0.24
  打字   +0.34   -1.16   +0.24
  电池   +1.00   +0.31   +0.04
  续航   +0.92   +0.39   -0.00
  持久   +0.97   +0.53   +0.04
  电脑   +0.33   -0.41   -0.20

  为什么丢掉第 1 维?因为它是「词频」这个公共因子:几乎所有词在它上面同号,拿它算余弦,任何两个词都接近 1.00。第一版我直接用前 3 维,结果「手机」和「镜头」的相似度是 0.999,等于没分开。
  去掉这一维之后,剩下的成分才装语义上的差别。

  最近邻(余弦):
  词     最近的两个词
  --------------------------
  手机   相机 0.78、镜头 0.74
  电池   续航 0.99、持久 0.98
  键盘   打字 1.00、电脑 0.78
  屏幕   清晰 0.85、镜头 0.83
  只有 18 句话,邻居能看出的东西有限:相机、镜头、拍照挤在一起,电池、续航、持久挤在一起,屏幕靠近清晰。这些关系是共现统计自己长出来的,我一条都没手写。

共现矩阵 22×22 只有 86 个非零,稀疏度很高。 这也解释了为什么后面向量会退化——大部分词只和两三个词共现过。 奇异值那一行从 5.84 掉到 3.29,衰减不算陡,说明信息摊在好几个方向上,不是「一个方向就说完」。 坐标表里能看出分组:手机、拍照、相机、镜头、屏幕的 d1 都是负的,电池、续航、持久、 键盘的 d1 是正的。第一维就在分「影像类」和「硬件类」。 最近邻那四行是这一节最该记住的:相机、镜头、拍照挤在一起,电池、续航、持久挤在一起。 这些关系是数出来的,我没写一条规则。

30 秒实验

  1. 把窗口从 2 改成 1 和 3,看最近邻怎么变。
  2. 故意保留第 1 维(取第 1~3 维),看余弦是不是全部贴近 1.00。
  3. 想一想:为什么「意思由上下文决定」这个假设能成立?

常见误区

  1. 「Embedding 是查表查出来的」——它是从共现统计里算出来的。
  2. 「取奇异值最大的几维总没错」——第 1 维往往只是词频,得丢掉。
  3. 「小语料也能训出好向量」——18 句只能看趋势,拿来用一定出问题。
自测三问(点开对答案) 1. 为什么 one-hot 表示不出词与词的远近? —— 任意两维正交,余弦恒为 0,所有词彼此一样远。 2. 为什么要丢掉第 1 个奇异向量? —— 它装的是词频这个公共因子,所有词同号,留着算余弦全部接近 1。 3. 这一节的词向量能直接拿去用吗? —— 不能。18 句语料太少,相似度偏高、向量近似共线,只能看趋势。

向量怎么来的搞清楚了。下一节讲配套的配置:跑这类模型要占多少显存、多少时间。

相关笔记