第 1 讲:NLP 简史与词向量(Introduction & Word Vectors)

Natural Language Processing with Deep Learning · Lecture 01。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应:Stanford CS224N Winter 2026 Lecture 1(History of NLP)+ Lecture 2 前半(Word Vectors);Winter 2025 及更早版本的 Lecture 1(Introduction and Word Vectors)。Christopher Manning 主讲的 Winter 2021、Winter 2023 版第 1 讲在 YouTube(Stanford Online)上有完整录像,可以配合观看。 配套代码:code/lecture01/ 预计阅读时间:4–5 小时(第一遍通读 2 小时,第二遍对照代码、自己推一遍公式 2–3 小时)


0. 本讲导航

0.1 学完本讲,你应该能够

  1. 说清楚 CS224N 这门课学什么、和你学过的 CS336 有什么不同。
  2. 举例说明人类语言为什么难处理:歧义、组合性、长尾、常识、语境。
  3. 讲出 NLP 七十年的主要阶段(规则 → 统计 → 深度学习 → 预训练大模型),以及每一次转变是为了解决上一阶段的什么瓶颈。
  4. 解释为什么人工词典(WordNet)和 one-hot 向量都不是好的”词义表示”。
  5. 说出分布假说的含义,并用它解释”为什么看上下文就能知道一个词的意思”。
  6. 手算一个小语料的共现矩阵,解释 PPMI 加权和 SVD 降维分别解决什么问题。
  7. 写出 word2vec(skip-gram) 的目标函数和 softmax 概率公式,说明每个符号的含义。
  8. 独立推导 ∂J/∂vc\partial J / \partial v_c 和 ∂J/∂uw\partial J / \partial u_w,并用”期望 − 观测”解释结果。
  9. 解释梯度下降与随机梯度下降(SGD),会用梯度检验验证自己推导的公式。
  10. 解释词向量的类比现象(king − man + woman ≈ queen)从何而来,以及词向量的局限(一词多义、偏见)。

0.2 先修知识(不会也没关系,用到时会提醒)

知识点用在哪里最低要求
Python 基础配套代码会写函数、for 循环、列表、字典。其余语法(包括 numpy)代码注释里都会讲
向量与点积第 6 节起知道向量是”一串数”。点积、长度、余弦在第 6.3 节从头讲
求导与链式法则第 10 节知道 (x2)′=2x(x^2)' = 2x。偏导数、梯度、链式法则在第 10.1 节复习
概率第 3.3、9 节知道条件概率 P(A∣B)P(A \mid B) 是”已知 B 时 A 的概率”
神经网络全讲不需要。本讲的 word2vec 只是一个”浅层”模型,第 3 讲才正式讲神经网络

0.3 本讲地图

本讲分成两个部分。第一部分讲”背景”:这门课学什么、语言为什么难、NLP 是怎么一路走到今天的。第二部分讲本讲真正的技术内容:怎么在计算机里表示一个词的意义。

第一部分:课程概述与 NLP 简史
   第1节  这门课学什么?
   第2节  人类语言为什么难?
   第3节  NLP 简史:七十年,五个阶段                ← 代码 01

第二部分:怎么表示词的意义?
   第4节  什么是"意义"?计算机需要什么样的表示?
   第5节  方案一:人工词典(WordNet)               ← 代码 02
   第6节  方案二:离散符号(one-hot)                ← 代码 02
   第7节  分布语义:看一个词的"朋友"                 ← 代码 02
   第8节  路线一:计数 —— 共现矩阵 + SVD            ← 代码 03
   第9节  路线二:预测 —— word2vec                  ← 本讲重点
   第10节 推导 word2vec 的梯度                       ← 代码 04
   第11节 训练:梯度下降与 SGD                       ← 代码 05
   第12节 学到的词向量长什么样?                     ← 代码 05
   第13节 词向量的局限与反思
   第14–18节 小结、与作业的关系、练习、延伸阅读、下一讲预告

第二部分的逻辑是一条”不断发现问题、不断改进”的链条:

人工词典⏟缺新词、缺细节、昂贵  →  one-hot⏟没有相似度  →  共现计数⏟太大、被虚词主导  →  PPMI + SVD⏟仍需存 V×V 矩阵  →  word2vec⏟直接学习稠密向量\underbrace{\text{人工词典}}_{\text{缺新词、缺细节、昂贵}} \;\to\; \underbrace{\text{one-hot}}_{\text{没有相似度}} \;\to\; \underbrace{\text{共现计数}}_{\text{太大、被虚词主导}} \;\to\; \underbrace{\text{PPMI + SVD}}_{\text{仍需存 } V\times V \text{ 矩阵}} \;\to\; \underbrace{\text{word2vec}}_{\text{直接学习稠密向量}}

第一部分:课程概述与 NLP 简史

1. 这门课学什么?

1.1 CS224N 是一门什么样的课

CS224N(Natural Language Processing with Deep Learning,“深度学习自然语言处理”)是斯坦福最经典的 NLP 课程之一。Christopher Manning 长期主讲这门课;2017 年它和 Richard Socher 的 CS224d(深度学习 NLP)合并,形成了今天”用深度学习做 NLP”的面貌。最近几年由 Diyi Yang、Tatsunori Hashimoto、Yejin Choi 等人主讲,内容也随着大语言模型的兴起大幅更新。

这门课历来有三个目标:

  1. 基础:掌握用深度学习做 NLP 的有效方法。先打基础(词向量、前馈网络、循环网络、注意力),再学关键的现代方法(Transformer、预训练、后训练、大语言模型)。
  2. 大局观:理解人类语言本身——它有什么特点,为什么理解和生成语言这么难。
  3. 动手能力:能用 PyTorch 为 NLP 中的主要问题搭建系统,例如词义、句法分析、机器翻译、问答、文本生成。

你会发现,第 2 个目标是 CS224N 区别于很多”深度学习课”的地方:它不只教模型,还会反复问”语言到底是什么样的东西,所以模型需要具备什么能力”。

1.2 课程结构

Winter 2026 版的 19 讲大致分成四块(我们的完整大纲见 SYLLABUS.md,按 20 讲组织):

模块内容对应作业
基础NLP 简史、词向量、神经网络与反向传播、句法分析作业 1:词向量;作业 2:神经网络基础、张量求导、依存句法分析
序列模型语言模型与 RNN、注意力、Transformer作业 3:自注意力与 Transformer
大语言模型预训练、后训练(SFT/RLHF/DPO)、高效适配(提示、LoRA)、智能体与检索增强、评估、推理作业 4:大语言模型的基准测试与评估
专题分词与多语言、可解释性、多模态、社会影响、开放问题期末项目(默认项目:实现一个精简版 GPT-2 并用于三个下游任务)

1.3 和 CS336 有什么不同?

如果你学过 CS336(从零构建语言模型),会发现两门课有重叠(分词、Transformer、预训练、对齐、评估),但视角很不一样:

CS224NCS336
出发点语言和 NLP 任务:语言为什么难?每个任务需要模型具备什么能力?系统和效率:给定算力预算,怎么造出最好的模型?
覆盖面宽:从 1950 年代的规则系统、词向量、句法分析、RNN,一直讲到大模型和智能体深:分词器、GPU、并行、规模定律、数据管线,每一环都自己实现
典型问题“为什么 word2vec 能学到类比?""注意力解决了 RNN 的什么问题?”“这次训练要多少 FLOPs?""怎么把模型切到 1000 块 GPU 上?”

两门课互补:CS224N 讲清楚”为什么这样设计”,CS336 讲清楚”怎么高效地造出来”。重叠的部分,我们的讲义会注明对应的 CS336 讲次,避免重复。


2. 人类语言为什么难?

2.1 语言是什么

在动手之前,先想一想我们要处理的对象。Manning 喜欢强调:人类语言是一个社会系统,是人们为了交流而共同构建、共同解释的。它有几个奇特的性质:

  • 它很”慢”,但信息密度高。人说话的信息传输速率大约每秒几十比特(有研究估计各种语言都在每秒 39 比特左右),比网线慢得多。但我们能用这么低的带宽完成复杂的交流,是因为说话双方共享了大量的背景知识——语言只传递”增量”,其余靠听者自己补全。
  • 它和思维紧密相连。语言不只是交流工具,还帮助我们思考、计划、协作。文字只有约五千年历史,却让知识可以跨越时间和空间积累。
  • 它在不断变化。新词(“内卷""躺平”、“selfie""rizz”)、新用法、方言、网络用语每天都在出现。

这些性质意味着:理解语言不只是”认字”,还需要知识、常识、语境,以及对说话人意图的推测。这正是 NLP 难的根源。

2.2 歧义无处不在

语言最让计算机头疼的特点是歧义(ambiguity):同一串文字可以有多种解释。人类几乎意识不到歧义的存在,因为我们会根据语境瞬间选出最合理的解释;计算机却必须显式地处理它。

歧义类型例子两种(或多种)解释
词汇歧义(一个词多个意思)“我想买个苹果”水果?手机?
“He sat by the bank”河岸?银行?
切分歧义(中文特有的难题)“南京市长江大桥”南京市 / 长江大桥?南京市长 / 江大桥?
结构歧义(句法结构不同)“咬死了猎人的狗”狗被咬死了?还是一只咬死了猎人的狗?
“I saw a man with a telescope”我用望远镜看到一个人?我看到一个拿着望远镜的人?
指代歧义(代词指谁)“The trophy doesn’t fit in the suitcase because it is too big.”it = 奖杯(太大放不进去)。把 big 换成 small,it 就变成了箱子
语用歧义(字面意思 ≠ 真实意图)“你可真行啊”夸奖?讽刺?
“Can you pass the salt?”问你”有没有能力”递盐?其实是请你递盐

上表中奖杯和箱子的例子来自”Winograd 模式”(Winograd schema):两个句子只差一个词,代词的指代就完全反转。要答对,模型必须知道”大的东西放不进小的容器”这样的常识。很长一段时间里,这类题目被认为是检验机器”真正理解”语言的试金石。

2.3 其他难点

除了歧义,还有:

  • 组合性与创造性:用有限的词,可以组合出无限多的句子。你读到的大多数句子,在此之前从没有人说过一模一样的话。所以不可能靠”把所有句子存下来”来处理语言。
  • 长尾:少数词极其常见(“的""是""the”),绝大多数词都很罕见(齐普夫定律,Zipf’s law)。任何语料里都有大量只出现一两次的词,模型很难从这么少的例子里学会它们。
  • 知识与常识:理解”他把冰箱里最后一瓶牛奶喝了,室友很生气”,需要知道牛奶是共享的、喝完了别人就没得喝。这些知识从来不会在文本里明说。
  • 语境依赖:“这个方案不错”在会议上和在吵架时意思可能完全不同。

记住这一节的意义:后面每一种技术的出现,都可以看作是在对付其中某个难点。例如本讲的词向量对付的是”相似但不相同的词”(长尾 + 组合性),第 4 讲的句法分析对付的是”结构歧义”,第 5–7 讲的 RNN 和 Transformer 对付的是”语境依赖”。


3. NLP 简史:七十年,五个阶段

Manning 在 2022 年的文章《Human Language Understanding & Reasoning》中,把 NLP 的历史分成了四个时代。这里沿用他的划分,并把 2018 年之后单独列为第五阶段。读历史时请抓住一个问题:每一次转变,都是为了解决上一阶段的什么瓶颈?

3.1 第一阶段(1950s–1960s):机器翻译的乐观与幻灭

背景:第二次世界大战中密码破译的成功和冷战的需求,让人们相信”翻译就是解码”——俄语只是”加了密的英语”。

  • 1950 年,图灵发表《计算机器与智能》,提出用对话来判断机器是否有智能(“图灵测试”)。语言从一开始就被视为人工智能的核心。
  • 1954 年,乔治城大学和 IBM 公开演示了一个俄译英系统:用约 250 个词和 6 条语法规则,翻译了六十多个精心挑选的句子。媒体一片欢呼,研究者乐观地预言几年内机器翻译就能解决。
  • 1966 年,美国政府的 ALPAC 报告得出结论:机器翻译比人工翻译更慢、更不准、更贵,近期没有希望。资助随之大幅削减。

瓶颈:当时的系统本质上是”词典 + 少量规则”,对歧义毫无办法。人们严重低估了语言的复杂性。

同一年(1966 年),MIT 的 Joseph Weizenbaum 写出了 ELIZA:一个模仿心理治疗师的聊天程序。它只会做模式匹配(“I am X” → “How long have you been X?”),但很多用户真的以为它理解自己。Weizenbaum 本人对此非常不安,后来写书提醒人们不要过度信任计算机。

动手:代码 01_from_rules_to_statistics.py 演示 1 实现了一个迷你 ELIZA。你会看到它对 “I am worried about my exam” 的回复 “How long have you been worried about your exam?” 看似贴心,但遇到 “The weather is nice today” 就只能说 “Please tell me more.”。

3.2 第二阶段(1970s–1980s):手写规则与知识工程

思路:既然简单的词典不够,那就请语言学家把语法规则和世界知识系统地写下来。

  • 乔姆斯基(1957)的生成语法让人们相信,语言可以用一套形式化的规则精确描述。
  • SHRDLU(Terry Winograd,约 1970 年)能在一个”积木世界”里理解并执行”把红色方块放到蓝色金字塔上”这样的指令,还能回答问题,表现惊艳。
  • 各种”专家系统”把领域知识写成成千上万条 if-then 规则。

瓶颈:

  1. 覆盖不全:人类的说法无穷无尽,规则永远写不完。SHRDLU 一离开积木世界就什么都做不了。
  2. 脆弱:规则之间互相冲突,加一条新规则可能破坏旧的行为。
  3. 知识获取瓶颈:所有知识都要专家手工编写和维护,成本极高。

动手:代码 01 演示 2 给 ELIZA 加了一条”识别用户难过”的规则。第一版只有 2 条规则,在 14 句测试句上召回率只有 20%(10 句难过的话只认出 2 句);第二版加了一条”出现 sad 就算”的宽松规则,召回率升到 70%,却带来了 3 个误报——“I am not sad at all”(不懂否定)、一句反讽、“The movie was sad”(难过的是电影)。规则写得严就漏,写得宽就错,这就是规则时代的困境。

3.3 第三阶段(1990s–2012):统计革命

思路:与其让人写规则,不如让人标注数据,让计算机从数据中统计出规律。

推动这次转变的有几件事:

  • 大规模电子文本出现了(新闻语料、议会记录、后来的互联网);
  • 标注语料库:例如宾州树库(Penn Treebank,1993),人工给几百万词的英文句子标注了句法树,成为后来二十年句法分析研究的基准;
  • IBM 的研究者(Brown 等人,1990、1993)提出统计机器翻译:从大量”英法对照”的议会记录里,自动学习词与词的对应概率,完全不需要语言学家写翻译规则;
  • 语音识别领域的统计方法(隐马尔可夫模型)大获成功。IBM 语音组负责人 Frederick Jelinek 据传说过一句玩笑话:“每当我解雇一个语言学家,语音识别器的性能就会上升。”

这一时期的典型做法是:人工设计特征 + 机器学习分类器。例如判断一封邮件是不是垃圾邮件,人来决定看哪些”特征”(有没有”免费”这个词、有几个感叹号、发件人是否在通讯录里……),机器学习算法(朴素贝叶斯、逻辑回归、支持向量机、条件随机场等)负责学习每个特征的权重。

朴素贝叶斯的直觉:想知道一句话是”难过”还是”开心”,就看句子里每个词在训练数据中更常出现在哪一类句子里。“crying”在难过的句子里出现过 1 次、在开心的句子里 0 次,它就是”难过”的证据;把所有词的证据乘起来(实际计算时取对数相加),哪一类得分高就判给哪一类。每个概率都是直接数出来的,不需要任何人写规则。

动手:代码 01 演示 3 只用 24 句标注好的例句训练了一个朴素贝叶斯分类器。没写一条规则,它就在同样的测试集上达到了 79% 的准确率、90% 的召回率,还认出了 “My heart is heavy and I keep crying”(因为训练集里 heart、crying 出现在难过的句子里)。

瓶颈:

  1. 特征工程:好的特征需要专家花大量时间设计和调试,而且每个任务都要重新设计一遍。
  2. 标注数据:每个任务都需要大量人工标注。
  3. 词是孤立的符号:在统计模型眼里,“sad”和”miserable”是两个毫无关系的编号。代码 01 演示 3(b) 揭示了这一点:训练集里没出现过的 “gloomy and miserable” 和 “ecstatic and delighted” 意思完全相反,模型却给出了完全相同的得分——因为它只认识其中的 “and”。

第 3 个瓶颈正是本讲后半部分要解决的问题。

3.4 第四阶段(2013–2018):深度学习

思路:不再手工设计特征,而是让神经网络自己学习表示(representation learning)。

  • 2013 年,Mikolov 等人的 word2vec 用一个极其简单的模型,从海量无标注文本中学出了”词向量”:意思相近的词向量也相近,甚至能做 king − man + woman ≈ queen 这样的类比。**这是本讲的主角。**2014 年,斯坦福的 Pennington、Socher、Manning 提出了 GloVe(第 2 讲)。
  • 循环神经网络(RNN、LSTM)被用来处理句子这样的序列(第 5 讲)。
  • 2014 年,序列到序列模型(seq2seq)和注意力机制出现,神经机器翻译很快超过了统计机器翻译;2016 年 Google 翻译换成了神经网络(第 6 讲)。
  • 2017 年,Transformer 诞生(第 7 讲)。

瓶颈:模型仍然是”一个任务一个模型”,每个任务还是需要大量标注数据。词向量虽然是无监督学来的,但模型的其余部分都要在标注数据上从头训练。

3.5 第五阶段(2018–今):预训练与大语言模型

思路:先在海量无标注文本上训练一个大模型去做”预测下一个词”(或”填空”)这样的自监督任务,让它学会语言和大量世界知识;再用少量数据或者干脆只用提示(prompt),把它用到各种任务上。

  • 2018 年:ELMo、GPT、BERT 相继出现,“预训练 + 微调”成为标准范式(第 9 讲)。
  • 2020 年:GPT-3 展示了”上下文学习”(in-context learning):不更新参数,只在提示里给几个例子,模型就能完成新任务。
  • 2022 年 11 月:ChatGPT 发布。指令微调和基于人类反馈的强化学习(RLHF)让模型变成了好用的助手(第 10 讲)。
  • 2024–2025 年:OpenAI o1、DeepSeek-R1 等”推理模型”通过强化学习学会了长链条的思考(第 14–15 讲);模型开始使用工具、检索信息、作为智能体完成多步任务(第 12 讲)。

这一阶段最深刻的变化是:NLP 从”为每个任务造一个系统”变成了”造一个通用模型,再教它做各种任务”。

3.6 一条贯穿始终的主线:知识放在哪里?

把五个阶段放在一起看,会发现一条清晰的主线——人类的知识以什么形式进入系统,人类又扮演什么角色:

阶段知识存放在哪里人类的工作主要瓶颈
早期机器翻译双语词典 + 少量规则编词典歧义
规则系统手写的语法规则和知识库写规则覆盖不全、脆弱、维护成本
统计方法人工设计的特征 + 从标注数据学到的权重设计特征、标注数据特征工程、标注成本、词是孤立符号
深度学习神经网络从标注数据中学到的权重(包括词向量)设计网络结构、标注数据每个任务都要大量标注
预训练大模型从海量原始文本中学到的权重准备数据、设计训练目标、对齐算力、数据、可靠性与安全

每一步,人类都把更多的工作”交给数据和计算”。这和 CS336 第 1 讲提到的”苦涩的教训”(The Bitter Lesson)是同一个道理:能随数据和算力扩展的通用方法,长期来看总会胜过精心编码的人类知识。

但请注意:这不等于”语言学没用了”。理解语言的结构,仍然帮助我们提出正确的问题、设计合理的评估、发现模型的盲点。CS224N 的第二个目标(理解人类语言)正是为此而设。

3.7 本讲在历史中的位置

本讲的后半部分,正好对应历史上最关键的一步:从”词是孤立的符号”到”词是向量”。

代码 01 的最后(演示 4)总结了统计方法的困境:如果每个词都有一个”含义坐标”,意思相近的词坐标也相近,那么模型从”sad”学到的知识就能自动迁移到”miserable”上。这个”含义坐标”就是词向量。

接下来我们一步步看:为了得到好的词向量,人们尝试过哪些方案,每个方案又卡在哪里。


第二部分:怎么在计算机里表示词的意义?

4. 什么是”意义”?计算机需要什么样的表示?

4.1 语言学的回答:指称语义

查字典,“意义”(meaning)的解释大致是:一个词、一个短语所表达的想法或所指的东西。语言学里最常见的理解方式叫指称语义(denotational semantics):

符号(signifier)  ⟺  所指的事物或概念(signified)\text{符号(signifier)} \;\Longleftrightarrow\; \text{所指的事物或概念(signified)}

“树”这个字(符号)对应现实中的树(所指)。这种理解很自然,但对计算机没什么直接帮助:计算机里没有”现实中的树”,只有字符串和数字。

4.2 计算机需要的是”可计算”的表示

回到代码 01 结尾发现的问题。我们希望词的表示至少满足:

  1. 能衡量相似度:能算出”sad 和 miserable 很像,sad 和 car 不像”;
  2. 能泛化:从一个词学到的知识,能迁移到意思相近的词上;
  3. 能自动获得:不依赖人工逐词编写(英语有几十万个词,新词每天都在出现);
  4. 紧凑:能高效地存储和计算。

带着这四条标准,我们来看历史上的几种方案。


5. 方案一:人工词典(WordNet)

5.1 WordNet 是什么

在深度学习之前,NLP 中最常用的”词义资源”是 WordNet:普林斯顿大学心理学家 George Miller 从 1985 年开始主持编纂的英语词汇数据库。它的 3.0 版收录了约 11.7 万个同义词集(synset)。它的核心结构有两种:

  • 同义词集:一个词的每个义项(sense),对应一组”在这个义项下可以互换”的词,再配一句释义。例如 good 在”擅长的”这个义项下,和 adept、expert、proficient、skillful 是同义词。
  • 上位词关系(hypernym,“是一种”):把概念连成一棵大树。cat → feline(猫科)→ carnivore(食肉动物)→ placental(有胎盘类)→ mammal(哺乳动物)→ …… → entity(实体)。

中文也有类似的资源,例如《同义词词林》和董振东先生创建的 HowNet(知网)。

5.2 它能做什么

有了上位词树,就能衡量两个词的相似度。最简单的方法叫路径相似度:

sim(w1,w2)=11+两个概念在上位词树上的最短距离\text{sim}(w_1, w_2) = \frac{1}{1 + \text{两个概念在上位词树上的最短距离}}

动手:代码 02_word_meaning_discrete.py 演示 1 实现了一个迷你版的上位词树。cat 和 dog 的距离是 4(cat → feline → carnivore ← canine ← dog),相似度 1/5=0.2001/5 = 0.200;cat 和 horse 是 0.143;cat 和 car 只有 0.062;car 和 bike 是 0.250。结果很符合直觉。

5.3 它的问题

  1. 缺少细微差别:WordNet 说 proficient 是 good 的同义词,但这只在”good at”的义项下成立。“a good person” 不能换成 “a proficient person”。同义词之间的差别(正式程度、褒贬、搭配习惯)很难用一张表记录下来。
  2. 缺少新词和新义:代码 02 里查 “chatgpt”、“selfie” 会直接报错。真实的 WordNet 也一样跟不上语言的变化:新词、网络用语、旧词的新用法(“wicked” 表示”很棒”)都需要有人手工加进去。
  3. 只有”相似”,没有”相关”:car 和 road、coffee 和 cup 关系非常密切,但它们不是”同一种东西”,在上位词树上相距很远。而很多 NLP 任务(比如搜索)恰恰需要”相关性”。
  4. 主观、昂贵、会过时:每一条都要专家手工编写和维护。有个有趣的例子:真实 WordNet 里,熊猫(panda)的上位词是 procyonid(浣熊科)——这反映的是编纂当时的分类观点,而现在一般认为大熊猫属于熊科。人工词典很难跟上知识的更新。
  5. 难以给出连续的相似度:路径长度只能取少数几个离散值,无法精确区分”有点像”和”很像”。

结论:WordNet 满足了标准 1(勉强能算相似度),但不满足标准 3(不能自动获得)。


6. 方案二:离散符号(one-hot 向量)

6.1 做法

传统 NLP(包括第 3.3 节的统计方法)把每个词当作一个离散符号。用向量表示的话,就是 one-hot 向量:向量长度等于词表大小 VV,只有这个词对应的位置是 1,其余全是 0。

motel=[0,0,0,0,0,0,0,0,0,0,1,0,0,0,0]\text{motel} = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0] hotel=[0,0,0,0,0,0,0,1,0,0,0,0,0,0,0]\text{hotel} = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

这种表示也叫局部表示(localist representation):一个词的全部信息都集中在一个维度上。

6.2 问题:所有词两两正交

one-hot 向量有两个致命问题:

  1. 维度太高:维度等于词表大小。英语词表动辄几十万;代码 02 演示 2 算了一下,V=500,000V = 500{,}000 时,一个 one-hot 向量按 float32 存就要 2 MB(尽管可以用稀疏格式压缩,但维度本身没变)。
  2. 没有任何相似度信息:任意两个不同词的 one-hot 向量的点积都是 0——它们正交(互相垂直)。在这种表示下,“hotel 和 motel”与”hotel 和 cat”一样毫不相关。

第 2 点对实际应用的伤害很大。例如在网页搜索中,用户搜 “Seattle motel”,我们希望也能找到写着 “Seattle hotel” 的网页。但按 one-hot 逐词比较,两者只有 Seattle 重合。

能不能用 WordNet 补救? 比如事先整理一张同义词表,搜 motel 时自动也搜 hotel。可以,但同义词表永远不完整,而且会遇到第 5.3 节的所有问题。

更好的思路是:让相似度直接体现在向量本身里。

6.3 补课:向量、点积、余弦相似度

在继续之前,先把后面要反复用到的几个概念讲清楚。

向量就是一串数,例如 a=[1.0,0.0,0.1,0.9,0.5]a = [1.0, 0.0, 0.1, 0.9, 0.5]。你可以把它想象成高维空间中的一个点,或者从原点出发指向这个点的一支箭头。

点积(dot product):对应位置相乘再求和。

a⋅b=∑i=1daibia \cdot b = \sum_{i=1}^{d} a_i b_i

长度(L2 范数):∥a∥=a⋅a=∑iai2\|a\| = \sqrt{a \cdot a} = \sqrt{\sum_i a_i^2}。

点积有一个几何解释:a⋅b=∥a∥ ∥b∥cos⁡θa \cdot b = \|a\| \, \|b\| \cos\theta,其中 θ\theta 是两支箭头的夹角。所以:

余弦相似度(cosine similarity):

cos⁡(a,b)=a⋅b∥a∥ ∥b∥∈[−1,1]\cos(a, b) = \frac{a \cdot b}{\|a\| \, \|b\|} \in [-1, 1]
  • cos⁡=1\cos = 1:方向完全相同;
  • cos⁡=0\cos = 0:互相垂直(one-hot 向量之间就是这样);
  • cos⁡=−1\cos = -1:方向完全相反。

为什么比较词向量时用余弦而不是点积? 点积会随着向量长度变大而变大,而后面会看到,词向量的长度往往和词频有关(常见的词向量更长)。我们关心的是”两个词的意思是否接近”,这体现在方向上,而不是长度上。余弦把长度除掉了,只保留方向。

动手:代码 02 演示 3 给 5 个词手工设计了 5 个”含义维度”(是不是动物、是不是交通工具、体型、是否家养、速度),cat 和 dog 的余弦相似度是 0.99,cat 和 car 只有 0.24。把 dog 的向量放大 10 倍,cat·dog 从 2.23 变成 22.30,余弦却保持 0.99 不变。

手工设计的向量说明:只要每个词是一个稠密的”含义坐标”,相似度就会自然出现。问题是,几十万个词、成百上千个说不清的”含义维度”,不可能由人来设计。我们需要一种从数据里自动学出这些向量的方法。


7. 分布语义:看一个词的”朋友”

7.1 分布假说

从数据里学习词义的关键想法,来自 20 世纪 50 年代的语言学:

分布假说(distributional hypothesis):出现在相似上下文中的词,往往有相似的意义。

语言学家 Zellig Harris(1954)系统地阐述了这个想法;英国语言学家 J. R. Firth(1957)有一句流传最广的表述:“You shall know a word by the company it keeps.”(看一个词和谁做伴,就知道它是什么意思。)

这其实就是你学外语时猜生词的方法:不查字典,只看这个词出现在什么样的句子里。

7.2 一个例子:tezgüino 是什么?

假设你从没见过 tezgüino 这个词,只看到它出现在下面这些句子里(这个例子出自语言学家 Eugene Nida,Dekang Lin 1998 年的论文也用过):

  • A bottle of tezgüino is on the table.(桌上有一瓶 tezgüino。)
  • Everybody likes tezgüino.(大家都喜欢 tezgüino。)
  • Tezgüino makes you drunk.(tezgüino 会让人喝醉。)
  • We make tezgüino out of corn.(我们用玉米做 tezgüino。)

你很可能已经猜到了:这是一种用玉米酿的酒。你是怎么知道的?因为能”装在瓶子里""让人喝醉""用粮食做”的东西,你知道的有 wine、beer……tezgüino 和它们出现在相同的上下文中。

动手:代码 02 演示 4 把这个推理过程变成了计算。对每个词统计它左右 3 个位置内出现过的词(上下文词),把这些次数排成一个向量,再算余弦相似度。tezgüino 和 wine 的相似度是 0.94,beer 0.88,bread 0.70,water 0.63,car 0.38。没有任何人告诉计算机 tezgüino 是什么,它只靠上下文就把它归到了酒类。

但注意 bread 也有 0.70:它和 tezgüino 共享了 of、is、on 这些高频虚词。直接用原始次数时,虚词的影响过大。第 8.4 节会处理这个问题。

7.3 上下文窗口

“上下文”具体指什么?最常用的定义是固定大小的窗口:一个词左右各 mm 个位置内的词。例如 m=2m = 2 时:

 ... government debt problems turning into  banking  crises as   happened in 2009 ...
                     └──── 左边 2 个 ────┘ 中心词 └─ 右边 2 个 ─┘

banking 的上下文词是 turning、into、crises、as。把语料中所有 banking 出现处的上下文汇总起来,就刻画了 banking 的”用法”,也就是它的意义。

窗口大小 mm 是一个可以调的选择:

  • 小窗口(m=1∼2m = 1 \sim 2):上下文主要反映句法和功能上的相似(可以互相替换的词),比如 cat 和 dog;
  • 大窗口(m=5∼10m = 5 \sim 10,甚至整篇文档):上下文更多反映主题上的相关,比如 doctor 和 hospital。

7.4 词向量:稠密的分布式表示

基于分布假说,我们给每个词学习一个稠密的实数向量(通常几十到几百维),使得出现在相似上下文中的词,向量也相似(点积或余弦大):

banking=[0.2860.792−0.177−0.1070.109−0.5420.3490.271]monetary=[0.4130.582−0.0070.2470.216−0.7180.1470.051]\text{banking} = \begin{bmatrix} 0.286 \\ 0.792 \\ -0.177 \\ -0.107 \\ 0.109 \\ -0.542 \\ 0.349 \\ 0.271 \end{bmatrix} \qquad \text{monetary} = \begin{bmatrix} 0.413 \\ 0.582 \\ -0.007 \\ 0.247 \\ 0.216 \\ -0.718 \\ 0.147 \\ 0.051 \end{bmatrix}

(数值只是示意。)这种向量有几个名字:词向量(word vector)、词嵌入(word embedding,因为它把词”嵌入”到一个向量空间里)、神经词表示(neural word representation)。

注意区分两个很像的术语:

  • 分布式(distributed)表示:意义分布在向量的很多维度上,每一维都参与表示很多个词(和 one-hot 的”局部表示”相对);
  • 分布(distributional)语义:意义来自词的分布,即它出现的上下文。

词向量同时具有这两个性质:它是分布式的表示,用分布语义的方法学出来。

7.5 两条路线:计数 vs 预测

怎样从语料中得到词向量?历史上有两条路线:

路线做法代表方法本讲位置
计数(count-based)先统计词与上下文共同出现的次数,得到一个大矩阵,再压缩成低维向量LSA(1990)、PPMI + SVD、COALS第 8 节,代码 03
预测(prediction-based)训练一个模型去预测上下文,词向量是模型的参数word2vec(2013)第 9–12 节,代码 04、05

第 2 讲会介绍 GloVe,它把两条路线结合了起来。


8. 路线一:计数 —— 共现矩阵 + SVD

8.1 共现矩阵

把第 7.2 节 tezgüino 的做法推广到整个词表,就得到共现矩阵(co-occurrence matrix)MM:

Mij=词 j 出现在词 i 的窗口内的总次数M_{ij} = \text{词 } j \text{ 出现在词 } i \text{ 的窗口内的总次数}

MM 是一个 V×VV \times V 的矩阵,第 ii 行就是词 ii 的”上下文画像”,可以直接当作词 ii 的向量。因为”ii 在 jj 的窗口里”等价于”jj 在 ii 的窗口里”,MM 是对称的。

(另一种做法是统计”词 × 文档”矩阵:MijM_{ij} = 词 ii 在文档 jj 中出现的次数。它反映的是主题相关性,是信息检索中潜在语义分析(LSA,Deerwester 等,1990)的基础。)

8.2 一个手算的例子

CS224N 课件里的经典例子,语料只有三句话,窗口大小 m=1m = 1:

  • I like deep learning .
  • I like NLP .
  • I enjoy flying .
Ilikedeeplearning.NLPenjoyflying
I02000010
like20100100
deep01010000
learning00101000
.00010101
NLP01001000
enjoy10000001
flying00001010

例如 I 和 like 相邻出现了 2 次(前两句),所以 MI,like=2M_{\text{I},\text{like}} = 2。三句话一共有 4+3+3=104 + 3 + 3 = 10 对相邻的词,每对在矩阵里记两次(对称),所以矩阵所有元素之和是 20。

观察:like 和 enjoy 两行都在 I 那一列有值,NLP 和 learning 两行都在 ”.” 那一列有值。上下文相似的词,行向量也相似。

动手:代码 03_cooccurrence_svd.py 演示 1 会打印出这个矩阵(并用 assert 检查它和课件一致)。

8.3 一个”可控”的玩具语料

三句话太少了。为了在笔记本上几秒钟内做完实验,同时又能定量地检查结果,配套代码用 corpus.py 按模板生成了一份玩具语料:6000 个句子、27,836 个词、171 个不同的词。它的设计是这样的:

  • 三类普通名词,每类 6 个:动物(cat、dog……)、水果(apple、banana……)、交通工具(car、bus……)。同类词出现在同一组句子模板里(“i fed the ___”、“the ___ is sweet”),也有所有名词共用的通用模板(“i saw the ___“)和两类共用的模板(动物和交通工具都可以 “is fast”)。同一类里的词频率不同(cat 约是 pig 的 3 倍),模仿真实语言。
  • 8 个人物词,每个由三个”特征”组合而成:性别(男/女)× 身份(王室/平民)× 年龄(成年/年幼)。例如 queen = 女 + 王室 + 成年,boy = 男 + 平民 + 年幼。造句时随机挑一个特征,用这个特征的模板,比如 “the queen said she was tired”(性别)、“the royal queen smiled”(身份)、“the queen drinks coffee”(年龄)。
  • 6 组”国家—首都—语言”:france—paris—french、japan—tokyo—japanese……国家和首都共享 “people in ___ speak french” 这样的模板。
  • 还有约 8% 的”填充句”(“it was a nice day”),不含任何内容词。

这样做的好处是:我们事先知道哪些词应该相似(同类的词),也知道人物词之间有哪些”可加的关系”(第 12.3 节的类比)。代价是:玩具语料比真实语言整齐得多,在它上面得到的结论只适用于它本身,不能直接推广到真实语料。讲义里每次引用实验数字,都请记住这一点。

为了衡量词向量的好坏,代码用了三个指标:

  • 类别纯度:每个”内容词”(44 个有类别的词)的 5 个最近邻里,同类词占多少比例。只看排名。随机向量的期望纯度约为 0.125。
  • 区分度:同类词之间的平均余弦相似度,减去异类词之间的平均余弦相似度。看绝对值,越大越好。
  • 类比准确率:96 道类比题(第 12.3 节)的正确率。

8.4 问题一:高频虚词主导一切 → PPMI

在玩具语料上算出共现矩阵(窗口 m=2m = 2),看 cat 这一行次数最多的上下文词(代码 03 演示 2):

the:155, a:107, is:62, fed:43, saw:41, sleeping:26, made:20, keeps:19

排在最前面的是 the、a、is。它们几乎出现在每个名词旁边,说明不了 cat 是什么,却在向量里占了最大的分量。后果是:所有名词的向量都被 the、a 拉向同一个方向,彼此都很像。

补救办法 1:对数压缩。把每个计数 xx 换成 log⁡(1+x)\log(1 + x),压低大数的影响。155 和 26 原本差 6 倍,取对数后只差约 1.5 倍。

补救办法 2:PPMI(正点互信息,Positive Pointwise Mutual Information)。这是更有原则的办法。它问的问题是:词 ww 和上下文词 cc 一起出现的次数,比”碰巧”多多少?

PMI(w,c)=log⁡P(w,c)P(w) P(c)\text{PMI}(w, c) = \log \frac{P(w, c)}{P(w)\, P(c)}
  • 分子 P(w,c)P(w, c):ww 和 cc 实际一起出现的概率;
  • 分母 P(w)P(c)P(w) P(c):如果 ww 和 cc 毫无关系(相互独立),它们碰巧一起出现的概率。

the 和 cat 一起出现了 155 次,看起来很多,但 the 本身出现了将近 5000 次,和谁都”碰巧”一起出现很多次,所以 PMI 不高。fed 和 cat 一起出现 43 次,而 fed 本身并不常见,所以 PMI 很高——它真正说明了 cat 的特点(可以被喂养)。

PMI 也可能是负数(比碰巧还少一起出现),但负值的估计非常不可靠(需要极大的语料才能确认”两个词很少一起出现”),所以通常直接截成 0:

PPMI(w,c)=max⁡(PMI(w,c), 0)\text{PPMI}(w, c) = \max\big(\text{PMI}(w, c),\ 0\big)

所有概率都从共现矩阵里直接估计:P(w,c)=Mwc/∑MP(w, c) = M_{wc} / \sum M,P(w)=∑cMwc/∑MP(w) = \sum_c M_{wc} / \sum M,P(c)P(c) 同理。

动手:代码 03 演示 3 比较了三种做法(都不降维,直接用 171 维的行向量):

加权方式类别纯度同类平均余弦异类平均余弦区分度
原始计数1.0000.9250.3960.529
log⁡(1+计数)\log(1 + \text{计数})1.0000.8870.2910.596
PPMI1.0000.7780.0980.680

三种做法的纯度都是满分——我们的玩具语料里,类别本身很容易分开。真正的区别在异类平均余弦:原始计数下,cat 和 car 这种毫不相干的词平均也有 0.4 的相似度(都是被 the、a 拉过去的);PPMI 把这部分”碰巧的共现”去掉后,异类相似度降到 0.1 左右。再看 king 的最近邻:原始计数下 man、prince、queen、boy 的相似度全都在 0.98 以上,几乎分不出差别;PPMI 下 queen 0.84、man 0.69、prince 0.68、princess 0.55,有了清楚的层次。

真实系统中还有很多类似的技巧,例如 Rohde 等人(2005)的 COALS 模型:截断过大的计数、用相关系数代替原始计数、让离中心词近的上下文权重更高等。它们的共同目标都是削弱高频词的影响,突出有信息量的共现。

8.5 问题二:太大、太稀疏 → SVD 降维

共现矩阵还有一个问题:太大。词表 V=10V = 10 万时,矩阵有 101010^{10} 个格子,按 float32 存需要 40 GB;V=40V = 40 万时需要 640 GB(代码 03 演示 2)。而且绝大多数格子是 0(我们的玩具语料里只有 7.6% 非零)。另外,同一个”意思”往往被分散在很多列里:drinks milk、plays with toys、little 都在暗示”年幼”,却占了好几个不同的维度。

解决办法是降维:把每个词的 VV 维向量压缩成 kk 维(kk 通常是 25 ~ 1000),同时尽量保留重要信息。最经典的工具是奇异值分解(SVD,Singular Value Decomposition)。

SVD 是什么? 任何矩阵 MM(形状 V×nV \times n)都可以分解成三个矩阵的乘积:

M=U Σ V⊤M = U \, \Sigma \, V^\top
  • UU(V×rV \times r)和 VV(n×rn \times r)的列都是正交的单位向量——可以理解为两组”新的坐标轴”;
  • Σ\Sigma 是对角矩阵,对角线上的数 σ1≥σ2≥⋯≥0\sigma_1 \ge \sigma_2 \ge \cdots \ge 0 叫奇异值,表示每个坐标轴的”重要程度”。

(注意:这里的 UU、VV 是 SVD 的记号,和后面 word2vec 的 UU、VV 不是一回事。)

降维:只保留最大的 kk 个奇异值及对应的坐标轴,用 U:,:kΣ:kU_{:, :k} \Sigma_{:k} 的每一行作为每个词的 kk 维向量。数学上可以证明(Eckart–Young 定理),这样得到的 UkΣkVk⊤U_k \Sigma_k V_k^\top 是在”平方误差最小”意义下对 MM 最好的秩 kk 近似。

直觉:把每个词想象成高维空间中的一个点。SVD 找到的第一个坐标轴,是这些点”分布最散开”的方向(这个方向上的差异最能区分不同的词);第二个坐标轴是与第一个垂直的方向中最散开的,以此类推。大的奇异值对应语料中最主要的规律(比如”这是动物还是水果”),小的奇异值往往对应噪声或偶然的细节。

为什么丢掉信息反而可能更好? 两个原因:

  1. 合并同义维度:drinks milk、plays with toys、little 这几列总是一起出现在 boy、girl、prince、princess 的行里,SVD 会把它们合并成一个”年幼”方向。
  2. 去噪:小奇异值对应的往往是语料中偶然的、不可靠的共现。丢掉它们,相似的词被”压”得更紧。

动手:代码 03 演示 4 对 PPMI 矩阵做 SVD,看保留前 kk 个奇异值能保留多少”能量”(奇异值平方和的占比):k=2k = 2 时 17.8%,k=10k = 10 时 50.9%,k=50k = 50 时 83.8%。不同 kk 下的效果:

kk类别纯度区分度类比准确率
20.6640.54821.9%
51.0000.66736.5%
101.0000.83431.2%
201.0000.84547.9%
501.0000.78766.7%
171(不降维)1.0000.68071.9%

三个观察:

  1. k=2k = 2 时,7 个类别挤在 2 维里,纯度明显下降;k=5k = 5 起类别就完全分开了。
  2. 适度降维(k=10、20k = 10、20)的区分度高于不降维——这就是”去噪”的效果。
  3. 类比需要的是类别内部的细微差别(男/女、王室/平民),它们藏在较小的奇异值里,kk 小时首先被丢掉,所以类比准确率整体上随 kk 增大而提高(中间有波动)。

所以”最好的 kk“取决于你要做什么:区分大类只要很少几维,捕捉细微关系需要更多维。

8.6 计数方法的优缺点

优点缺点
训练快:数一遍语料 + 一次矩阵分解矩阵大小是 V2V^2;完整 SVD 的计算量约为 O(V3)O(V^3),词表一大就算不动
充分利用了全局统计信息(每个共现都被计入)加入新词或新文档时,要重新计算整个矩阵和分解
结果确定,没有随机性需要很多”手工技巧”(加权、截断、平滑)才能得到好结果

CS224N 作业 1 的第一部分就是实现这条路线(见第 15 节)。

下一节我们看另一条路线:不数了,直接学习一个能预测上下文的模型。


9. 路线二:预测 —— word2vec

9.1 背景

2013 年,Google 的 Tomas Mikolov 等人发表了两篇论文,提出了 word2vec。它不是第一个”神经网络词向量”(Bengio 等人 2003 年的神经语言模型就已经在学习词向量),但它做对了一件关键的事:把模型简化到极致,从而能在几十亿词的语料上快速训练。Google 公开的预训练词向量在约 1000 亿词的新闻语料上训练,覆盖 300 万个词和短语,每个 300 维。

这些词向量表现出的性质——近义词聚在一起、king − man + woman ≈ queen——让整个领域意识到”从无标注文本中学习表示”的威力。十年后(2023 年),word2vec 的第二篇论文获得了 NeurIPS 的”时间检验奖”。

word2vec 其实是一个框架,包含两种模型和两种训练技巧:

  • 模型:skip-gram(用中心词预测上下文词)和 CBOW(用上下文词预测中心词);
  • 训练技巧:负采样(negative sampling)和层次 softmax(hierarchical softmax)。

本讲讲 skip-gram + 最朴素的 softmax(naive softmax),把数学彻底搞清楚。第 2 讲再讲负采样和 CBOW。

9.2 核心想法

word2vec 的想法可以用四句话说完:

  1. 我们有一个大语料(一长串词)。
  2. 词表中的每个词都用一个向量表示(一开始是随机的)。
  3. 遍历语料中的每个位置 tt,它有一个中心词 cc 和若干上下文词(outside words)oo。
  4. 用 cc 和 oo 的向量的相似度来计算”给定 cc,看到 oo 的概率” P(o∣c)P(o \mid c);不断调整词向量,让这个概率变大。

还是用 banking 的例子,窗口大小 m=2m = 2,在位置 tt(中心词 banking)上,我们希望模型给出的四个概率都尽量大:

   problems   turning   into   banking   crises   as
                 ↑        ↑       │         ↑      ↑
   P(w_{t-2}|w_t)  P(w_{t-1}|w_t)  │  P(w_{t+1}|w_t)  P(w_{t+2}|w_t)
                              中心词 w_t

然后窗口右移一位,crises 成为新的中心词,重复同样的事情。

注意这个想法和第 8 节的区别:计数方法先把所有共现数完再处理;word2vec 是一个窗口一个窗口地学习,每看到一个 (中心词, 上下文词) 对,就把它们的向量往”更搭”的方向调一点。

9.3 目标函数

对整个语料,我们希望所有位置、所有上下文词的概率乘起来尽量大。这叫似然(likelihood):

L(θ)=∏t=1T ∏−m≤j≤mj≠0P(wt+j∣wt; θ)L(\theta) = \prod_{t=1}^{T} \ \prod_{\substack{-m \le j \le m \\ j \ne 0}} P(w_{t+j} \mid w_t;\ \theta)

其中 TT 是语料的长度,θ\theta 代表所有要学习的参数(所有词向量)。

实际优化的是目标函数(也叫损失函数、代价函数)J(θ)J(\theta),它是平均负对数似然:

J(θ)=−1Tlog⁡L(θ)=−1T∑t=1T∑−m≤j≤mj≠0log⁡P(wt+j∣wt; θ)J(\theta) = -\frac{1}{T} \log L(\theta) = -\frac{1}{T} \sum_{t=1}^{T} \sum_{\substack{-m \le j \le m \\ j \ne 0}} \log P(w_{t+j} \mid w_t;\ \theta)

最小化 J(θ)J(\theta) 等价于最大化预测准确度。为什么要做这三个变换?

  • 取对数:把连乘变成连加。几百亿个小于 1 的概率相乘会变成计算机无法表示的极小数(下溢),相加则没有这个问题;而且对数是单调递增的,最大化 log⁡L\log L 和最大化 LL 是一回事。
  • 取负号:优化领域的习惯是”最小化损失”。
  • 除以 TT:取平均,让目标函数的大小不依赖于语料长度,不同语料上的数值可以比较。

9.4 用 softmax 定义 P(o∣c)P(o \mid c)

剩下的问题是:P(o∣c)P(o \mid c) 怎么算?word2vec 给每个词 ww 准备两个向量:

  • vwv_w:ww 作为中心词时用的向量;
  • uwu_w:ww 作为上下文词时用的向量。

然后定义:

P(o∣c)=exp⁡(uo⊤vc)∑w∈Vexp⁡(uw⊤vc)P(o \mid c) = \frac{\exp(u_o^\top v_c)}{\sum_{w \in V} \exp(u_w^\top v_c)}

逐部分理解:

  1. uo⊤vcu_o^\top v_c(点积):衡量 oo 和 cc 的”相似度”或”搭配程度”。点积越大,oo 越可能出现在 cc 旁边。
  2. exp⁡(⋅)\exp(\cdot):把任意实数变成正数(概率必须为正)。
  3. 除以 ∑wexp⁡(uw⊤vc)\sum_w \exp(u_w^\top v_c):对词表中所有词求和做归一化,保证所有 P(w∣c)P(w \mid c) 加起来等于 1,构成一个概率分布。

这个”取 exp 再归一化”的函数叫 softmax,它把任意实数向量 x=(x1,…,xn)x = (x_1, \dots, x_n) 映射成概率分布:

softmax(x)i=exp⁡(xi)∑j=1nexp⁡(xj)\text{softmax}(x)_i = \frac{\exp(x_i)}{\sum_{j=1}^{n} \exp(x_j)}

为什么叫 softmax?

  • “max”:它会放大最大的那个值——最大的 xix_i 得到最大的概率;
  • “soft”:但其他值也保留一些概率,而不是像 max 那样”赢者通吃”。而且它处处可导,这对梯度下降至关重要。

动手:代码 04_word2vec_gradients.py 演示 1:分数 [2,1,0.1][2, 1, 0.1] 经过 softmax 变成 [0.659,0.242,0.099][0.659, 0.242, 0.099]。所有分数加 100,结果完全不变(平移不变性);分数放大 5 倍,变成 [0.993,0.007,0.000][0.993, 0.007, 0.000],接近 max;缩小到 1/5,变成 [0.400,0.327,0.273][0.400, 0.327, 0.273],接近均匀分布。

一个实现上的坑:分数是 [1000,999,998][1000, 999, 998] 时,exp⁡(1000)\exp(1000) 会溢出成无穷大,朴素写法得到 [nan nan nan]。正确做法是利用平移不变性,先减去最大值再算 exp,结果是 [0.665,0.245,0.090][0.665, 0.245, 0.090]。所有深度学习框架里的 softmax 都是这样实现的。

9.5 为什么每个词要两个向量?

一个自然的问题:为什么不让每个词只有一个向量?

  1. 数学上更简单。如果只有一个向量,中心词恰好也出现在自己的上下文里时(比如 “the cat and the dog” 中 the 的窗口里还有一个 the),分子里会出现 vc⊤vcv_c^\top v_c 这一项,求导时多出一个 2vc2 v_c,推导变得麻烦。用两个向量,所有的项都是”一个 uu 乘一个 vv“,干净利落。
  2. 避免一个不合理的约束。单个向量时,词和自己的得分是 ∥vc∥2≥0\|v_c\|^2 \ge 0,而且向量越长得分越高。但一个词通常很少出现在自己的上下文里,模型却会被迫认为”我很可能出现在我自己旁边”。
  3. “一起出现”和”用法相似”是两回事。cat 旁边常出现 the,所以 uthe⊤vcatu_\text{the}^\top v_\text{cat} 应该很大;但 cat 和 the 的用法毫不相似。两套向量让模型可以同时表达这两件事(第 12.2 节有实验验证)。

训练完之后,通常把两套向量相加或取平均作为最终的词向量(vw+uwv_w + u_w)。

9.6 参数

模型的全部参数 θ\theta 就是所有词的两套向量,把它们排成两个矩阵:

  • V∈R∣V∣×dV \in \mathbb{R}^{|V| \times d}:第 ww 行是 vwv_w(中心词向量);
  • U∈R∣V∣×dU \in \mathbb{R}^{|V| \times d}:第 ww 行是 uwu_w(上下文向量)。

参数总数是 2 ∣V∣ d2 \, |V| \, d。在我们的玩具语料上,∣V∣=171|V| = 171、d=10d = 10,共 3420 个参数;Google 新闻词向量是 ∣V∣=300|V| = 300 万、d=300d = 300,共 18 亿个参数。

记号说明:为了和 CS224N 的课件一致,VV 有时表示词表(w∈Vw \in V),有时表示中心词向量矩阵。在不会混淆的地方我们直接用 VV;需要强调词表大小时写 ∣V∣|V|。

整个 word2vec 没有任何隐藏层、没有非线性激活函数——它就是”查两个向量、算一个点积、过一个 softmax”。它之所以有效,完全来自海量数据和一个好的训练目标。


10. 推导 word2vec 的梯度

要用梯度下降训练,就得知道 JJ 对每个参数的偏导数。这一节是本讲数学最多的部分,但每一步都只用到高中的求导规则。**强烈建议你拿出纸笔,跟着推一遍。**CS224N 历年作业里都有这道推导题。

10.1 补课:导数、偏导数、梯度、链式法则

导数:函数在某一点的变化率。f(x)=x2f(x) = x^2,f′(x)=2xf'(x) = 2x:在 x=3x = 3 处,xx 增加一点点,ff 大约增加 6 倍那么多。

偏导数:多元函数中,只让一个变量变化、其他变量固定时的变化率。f(x,y)=x2yf(x, y) = x^2 y,∂f/∂x=2xy\partial f / \partial x = 2xy。

梯度:把所有偏导数排成一个向量。对向量 v=(v1,…,vd)v = (v_1, \dots, v_d) 求导,结果也是一个 dd 维向量:

∂f∂v=(∂f∂v1,…,∂f∂vd)\frac{\partial f}{\partial v} = \left( \frac{\partial f}{\partial v_1}, \dots, \frac{\partial f}{\partial v_d} \right)

梯度指向函数增长最快的方向。所以要让 JJ 变小,就沿着负梯度方向走。

本节要用到的四条规则:

规则公式说明
点积对向量求导∂ (u⊤v)∂v=u\dfrac{\partial\, (u^\top v)}{\partial v} = u因为 u⊤v=∑iuiviu^\top v = \sum_i u_i v_i,对 viv_i 求偏导得 uiu_i
对数ddxlog⁡x=1x\dfrac{d}{dx} \log x = \dfrac{1}{x}自然对数
指数ddxex=ex\dfrac{d}{dx} e^x = e^x
链式法则ddxf(g(x))=f′(g(x))⋅g′(x)\dfrac{d}{dx} f(g(x)) = f'(g(x)) \cdot g'(x)复合函数的导数 = 外层的导数 × 内层的导数

10.2 对中心词向量 vcv_c 求导

考虑一个样本(中心词 cc,上下文词 oo)的损失:

J=−log⁡P(o∣c)=−log⁡exp⁡(uo⊤vc)∑wexp⁡(uw⊤vc)J = -\log P(o \mid c) = -\log \frac{\exp(u_o^\top v_c)}{\sum_{w} \exp(u_w^\top v_c)}

第 1 步:利用 log⁡(a/b)=log⁡a−log⁡b\log(a/b) = \log a - \log b 拆开。

log⁡P(o∣c)=uo⊤vc⏟①  −  log⁡∑wexp⁡(uw⊤vc)⏟②\log P(o \mid c) = \underbrace{u_o^\top v_c}_{\text{①}} \; - \; \underbrace{\log \sum_{w} \exp(u_w^\top v_c)}_{\text{②}}

(第一项里 log⁡exp⁡(x)=x\log \exp(x) = x。)

**第 2 步:① 对 vcv_c 求导。**直接用点积规则:

∂∂vcuo⊤vc=uo\frac{\partial}{\partial v_c} u_o^\top v_c = u_o

**第 3 步:② 对 vcv_c 求导。**这是一个复合函数:外层是 log⁡(⋅)\log(\cdot),内层是 ∑wexp⁡(uw⊤vc)\sum_w \exp(u_w^\top v_c)。用链式法则:

∂∂vclog⁡∑wexp⁡(uw⊤vc)=1∑wexp⁡(uw⊤vc)⏟外层 log⁡ 的导数⋅∂∂vc∑xexp⁡(ux⊤vc)⏟内层的导数\frac{\partial}{\partial v_c} \log \sum_{w} \exp(u_w^\top v_c) = \underbrace{\frac{1}{\sum_{w} \exp(u_w^\top v_c)}}_{\text{外层 } \log \text{ 的导数}} \cdot \underbrace{\frac{\partial}{\partial v_c} \sum_{x} \exp(u_x^\top v_c)}_{\text{内层的导数}}

内层是很多项的和,和的导数等于导数的和;每一项 exp⁡(ux⊤vc)\exp(u_x^\top v_c) 再用一次链式法则(外层 exp、内层点积):

∂∂vc∑xexp⁡(ux⊤vc)=∑xexp⁡(ux⊤vc)⋅ux\frac{\partial}{\partial v_c} \sum_{x} \exp(u_x^\top v_c) = \sum_{x} \exp(u_x^\top v_c) \cdot u_x

(求和变量换成了 xx,免得和外面分母里的 ww 混淆。)代回去:

∂∂vc②=∑xexp⁡(ux⊤vc)∑wexp⁡(uw⊤vc) ux=∑xP(x∣c) ux\frac{\partial}{\partial v_c} \text{②} = \sum_{x} \frac{\exp(u_x^\top v_c)}{\sum_{w} \exp(u_w^\top v_c)} \, u_x = \sum_{x} P(x \mid c) \, u_x

最后一步认出了 softmax:exp⁡(ux⊤vc)∑wexp⁡(uw⊤vc)\frac{\exp(u_x^\top v_c)}{\sum_w \exp(u_w^\top v_c)} 正是 P(x∣c)P(x \mid c)。

第 4 步:合起来。

∂∂vclog⁡P(o∣c)=uo−∑xP(x∣c) ux\frac{\partial}{\partial v_c} \log P(o \mid c) = u_o - \sum_{x} P(x \mid c)\, u_x

损失 JJ 带一个负号,所以:

 ∂J∂vc=∑x∈VP(x∣c) ux  −  uo \boxed{\ \frac{\partial J}{\partial v_c} = \sum_{x \in V} P(x \mid c)\, u_x \; - \; u_o\ }

10.3 直觉:期望 − 观测

这个结果非常漂亮,值得停下来想一想:

  • uou_o 是实际观测到的上下文词的向量;
  • ∑xP(x∣c) ux\sum_x P(x \mid c)\, u_x 是按照模型当前的预测,上下文向量的期望(加权平均)。

所以梯度 = 期望 − 观测。梯度下降的更新是 vc←vc−α⋅(期望−观测)=vc+α⋅(观测−期望)v_c \leftarrow v_c - \alpha \cdot (\text{期望} - \text{观测}) = v_c + \alpha \cdot (\text{观测} - \text{期望}):把 vcv_c 往实际观测到的 uou_o 方向拉,往模型目前”以为”的方向推。

当模型的预测和观测完全一致时(期望 = 观测),梯度为零,不再更新。这种”期望 − 观测”的梯度形式在机器学习中反复出现(逻辑回归、最大熵模型、条件随机场……),以后看到它,你就知道它在说”让模型的预测向数据靠拢”。

动手:代码 04 演示 2 在一个 5 个词、3 维的小例子里把每个数都打印了出来。中心词 cat、真实上下文词 sat,模型目前给 sat 的概率是 0.231(损失 −log⁡0.231=1.466-\log 0.231 = 1.466;如果 5 个词等概率,损失是 log⁡5=1.609\log 5 = 1.609)。期望的上下文向量是 [0.107,−0.081,−0.049][0.107, -0.081, -0.049],观测到的 usatu_\text{sat} 是 [0.683,−0.333,0.176][0.683, -0.333, 0.176],两者之差 [−0.576,0.251,−0.225][-0.576, 0.251, -0.225] 正好等于函数算出的梯度。

10.4 对上下文向量 uwu_w 求导

同样的方法,对任意一个词 ww 的上下文向量 uwu_w 求导。仍然从 log⁡P(o∣c)=uo⊤vc−log⁡∑xexp⁡(ux⊤vc)\log P(o \mid c) = u_o^\top v_c - \log \sum_x \exp(u_x^\top v_c) 出发:

  • 第一项 uo⊤vcu_o^\top v_c:只有当 w=ow = o 时才含 uwu_w,导数是 vcv_c;否则是 0。可以写成 1[w=o] vc\mathbb{1}[w = o]\, v_c(1[⋅]\mathbb{1}[\cdot] 是指示函数:条件成立为 1,否则为 0)。
  • 第二项:求和中只有 x=wx = w 那一项含 uwu_w,链式法则给出 exp⁡(uw⊤vc)∑xexp⁡(ux⊤vc)vc=P(w∣c) vc\frac{\exp(u_w^\top v_c)}{\sum_x \exp(u_x^\top v_c)} v_c = P(w \mid c)\, v_c。

所以:

 ∂J∂uw=(P(w∣c)−1[w=o]) vc \boxed{\ \frac{\partial J}{\partial u_w} = \big( P(w \mid c) - \mathbb{1}[w = o] \big)\, v_c\ }

分两种情况看:

  • w=ow = o(真实上下文词):系数 P(o∣c)−1<0P(o \mid c) - 1 < 0。更新 uo←uo−α(P−1)vcu_o \leftarrow u_o - \alpha (P - 1) v_c 把 uou_o 拉向 vcv_c。
  • w≠ow \ne o(其他所有词):系数 P(w∣c)>0P(w \mid c) > 0,把 uwu_w 推离 vcv_c,推的力度和模型目前给它的概率成正比——模型越是”误以为” ww 会出现,就推得越狠。

10.5 矩阵形式,以及和交叉熵的关系

把上面的结果写成矩阵形式,代码实现会非常简洁。记:

  • y^=softmax(Uvc)∈R∣V∣\hat{y} = \text{softmax}(U v_c) \in \mathbb{R}^{|V|}:模型预测的概率分布(第 ww 个元素是 P(w∣c)P(w \mid c));
  • y∈R∣V∣y \in \mathbb{R}^{|V|}:真实上下文词 oo 的 one-hot 向量。

那么:

∂J∂vc=U⊤(y^−y),∂J∂U=(y^−y) vc⊤\frac{\partial J}{\partial v_c} = U^\top (\hat{y} - y), \qquad \frac{\partial J}{\partial U} = (\hat{y} - y)\, v_c^\top

其中 (y^−y)vc⊤(\hat{y} - y) v_c^\top 是一个外积,得到 ∣V∣×d|V| \times d 的矩阵,第 ww 行正是 (P(w∣c)−1[w=o]) vc(P(w \mid c) - \mathbb{1}[w = o])\, v_c。

y^−y\hat{y} - y 就是”预测分布 − 真实分布”,期望 − 观测的向量版本。

另外请注意,J=−log⁡y^o=−∑wywlog⁡y^wJ = -\log \hat{y}_o = -\sum_w y_w \log \hat{y}_w,这正是 yy 和 y^\hat{y} 之间的交叉熵(cross-entropy)。换句话说,skip-gram 就是一个多分类逻辑回归(softmax 回归):输入是中心词向量 vcv_c,要从 ∣V∣|V| 个类别里分类出上下文词。特别之处在于,它的”输入特征” vcv_c 本身也是学出来的。第 3 讲讲神经网络时,你会发现”softmax + 交叉熵,梯度是 y^−y\hat{y} - y“这个结论会一再出现。

10.6 梯度检验:怎么知道自己推对了?

手推梯度很容易出错(少个负号、漏一项),而且出错时程序往往不会崩溃,只是训练效果莫名其妙地差。梯度检验(gradient check)是最重要的调试手段:用数值方法近似计算梯度,和解析公式比较。

数值梯度用中心差分:

∂J∂θi≈J(θ+ϵ ei)−J(θ−ϵ ei)2ϵ\frac{\partial J}{\partial \theta_i} \approx \frac{J(\theta + \epsilon\, e_i) - J(\theta - \epsilon\, e_i)}{2\epsilon}

其中 eie_i 是第 ii 个分量为 1、其余为 0 的向量,ϵ\epsilon 取 10−510^{-5} 左右。它就是导数的定义:“参数变一点点,损失变多少”。中心差分的误差是 O(ϵ2)O(\epsilon^2),比单边差分 J(θ+ϵei)−J(θ)ϵ\frac{J(\theta + \epsilon e_i) - J(\theta)}{\epsilon} 的 O(ϵ)O(\epsilon) 精确得多。

数值梯度很慢(每个参数都要算两次损失),所以只用来在小规模上检查,不能用来训练。比较时用相对误差:经验上 <10−7< 10^{-7} 说明公式正确,>10−4> 10^{-4} 几乎肯定有 bug。

动手:代码 04 演示 3 对一个窗口(1 个中心词、4 个上下文词)做了梯度检验:中心词向量梯度的相对误差是 4.6×10−114.6 \times 10^{-11},上下文向量梯度是 1.4×10−101.4 \times 10^{-10}——公式正确。作为对照,一个”忘了减去 one-hot”(把 y^−y\hat{y} - y 写成了 y^\hat{y})的错误版本,相对误差高达 0.88,梯度检验立刻就发现了问题。


11. 训练:梯度下降与 SGD

11.1 梯度下降

有了梯度,训练就是反复执行:

θnew=θold−α ∇θJ(θ)\theta^{\text{new}} = \theta^{\text{old}} - \alpha \, \nabla_\theta J(\theta)

α\alpha 叫学习率(learning rate),控制每一步走多远。直觉上,想象你在大雾中的山上,看不清全貌,只能感觉脚下哪个方向最陡,然后往下坡方向迈一步;重复很多次,就能走到山谷里。

学习率的选择很关键:

  • 太小:每步只挪一点点,要走很久才能到谷底;
  • 太大:一步跨过谷底,跑到对面的山坡上,甚至越跳越高(损失发散,变成 nan)。

动手:代码 04 演示 4 对同一个样本反复做梯度下降(学习率 0.5),P(o∣c)P(o \mid c) 依次是 0.231 → 0.303 → 0.433 → 0.636 → 0.807 → 0.888。但注意,只对一个样本优化会让 P(o∣c)→1P(o \mid c) \to 1,也就是”cc 的上下文永远是 oo“——显然不对。真实训练时,同一个中心词会和很多不同的上下文词配对,梯度互相”拉扯”,最终的平衡点是让 P(⋅∣c)P(\cdot \mid c) 尽量接近语料中 cc 的真实上下文分布。

11.2 随机梯度下降(SGD)

J(θ)J(\theta) 是对语料中所有位置的平均。真实语料有几十亿到上万亿个词,每更新一次参数都要扫一遍全部语料,太慢了。

随机梯度下降(Stochastic Gradient Descent,SGD)的做法是:每次只随机取一个窗口(或一小批窗口,即 mini-batch),用它的梯度近似整体梯度,立即更新参数。

  • 单个窗口的梯度是整体梯度的一个有噪声的估计,但它的期望等于整体梯度;
  • 一次更新的计算量从”整个语料”降到”一个窗口”,可以做成千上万倍多的更新;
  • 噪声并不全是坏事,它有时能帮助模型跳出不好的局部区域;
  • 小批量(比如一次 32 或 64 个样本)可以利用矩阵运算并行计算,在 GPU 上尤其高效。

几乎所有深度学习模型都用 SGD 或它的变体(第 3 讲会讲 Adam 等)训练。

11.3 稀疏更新与一个实现上的坑

看一个窗口的梯度:

  • 对中心词向量矩阵 VV:只有中心词 cc 那一行有梯度,其余行全是 0。每次只需要更新一行;
  • 对上下文向量矩阵 UU:按朴素 softmax 的公式,每一行都有梯度(每个词都要被推开一点)。这意味着每一步都要更新整个 UU——这是朴素 softmax 慢的另一个原因(见第 11.6 节)。

实现小批量时有一个新手很容易踩的坑:同一批样本里,同一个中心词可能出现多次,它的梯度必须累加。在 numpy 里写 grad[centers] += g,重复的下标只会生效一次;必须用 np.add.at(grad, centers, g)。

动手:代码 04 演示 5 验证了批量版本 naive_softmax_batch 和逐个样本循环的结果完全一致(相对误差 10−1610^{-16} 量级);如果误用 grad[centers] += ...,相对误差高达 0.86。

11.4 初始化与学习率衰减

  • 初始化:所有向量用很小的随机数初始化(代码里是 ±0.5/d\pm 0.5/d 之间的均匀分布)。为什么不全设成 0?如果所有词的向量一样,它们收到的梯度也一样,就会永远一样,这叫”对称性无法打破”。为什么要很小?向量很小时所有分数接近 0,softmax 接近均匀分布,这是一个”不偏不倚”的起点。
  • 学习率衰减:训练过程中让学习率从初始值逐渐减小(代码里线性衰减到初始值的 10%,原始 word2vec 也是线性衰减)。早期步子大、学得快;后期步子小、收敛得稳。

11.5 一次完整的训练

动手:代码 05_word2vec_training.py 演示 1 在玩具语料上训练 skip-gram:171 个词,窗口 2 产生 75,344 个 (中心词, 上下文词) 对,d=10d = 10,共 3420 个参数,批量大小 64,初始学习率 0.5,训练 15 轮(在编写讲义的电脑上约 1.4 秒)。

平均损失
训练前(模型对所有词一视同仁):log⁡171\log 1715.1417
第 1 轮4.5452
第 5 轮3.0097
第 10 轮2.7362
第 15 轮2.6859
理论最低损失2.5800

理论最低损失是什么?损失不可能降到 0,因为上下文本身就有随机性:“the ___ is sweet” 里填 apple 还是 pear 本来就不确定。如果模型能完美地预测每个中心词的真实上下文分布 q(⋅∣c)q(\cdot \mid c),平均损失就等于这些分布的平均熵:

min⁡J=∑ccount(c)N H(q(⋅∣c)),H(q)=−∑oq(o)log⁡q(o)\min J = \sum_{c} \frac{\text{count}(c)}{N} \, H\big(q(\cdot \mid c)\big), \qquad H(q) = -\sum_o q(o) \log q(o)

这是信息论中”交叉熵不小于熵”的直接结论。我们的模型最终损失 2.686,离下界 2.580 只差约 0.1:它已经把语料中的大部分规律学到了。剩下的差距主要来自容量限制(每个词只有 10 维)。

11.6 朴素 softmax 的代价

softmax 的分母 ∑w∈Vexp⁡(uw⊤vc)\sum_{w \in V} \exp(u_w^\top v_c) 要对词表中每一个词算一次点积。所以每个样本的计算量和 ∣V∣|V| 成正比。

动手:代码 04 演示 6 测量了 d=100d = 100 时处理单个样本的耗时。在编写讲义的电脑上,∣V∣=1000|V| = 1000 时约 0.1 毫秒,∣V∣=1|V| = 1 万时约 0.8 毫秒,∣V∣=10|V| = 10 万时约 8 ~ 10 毫秒(具体数字取决于你的电脑,每次运行也会有波动,但耗时与 ∣V∣|V| 成正比这一点不会变)。按此外推到 ∣V∣=300|V| = 300 万,一个样本要两三百毫秒;如果语料产生 10 亿个样本对,单线程过一遍要好几年。

我们的玩具词表只有 171 个词,所以朴素 softmax 没问题。真实的 word2vec 用负采样来解决这个问题:不和全部词比较,只和随机抽取的几个”负例”比较,把每个样本的代价从 O(∣V∣)O(|V|) 降到 O(k)O(k)(kk 通常是 5 ~ 20)。这是第 2 讲的第一个主题。


12. 学到的词向量长什么样?

以下结果都来自代码 05,词向量取 vw+uwv_w + u_w,d=10d = 10。再次提醒:这是在我们自己设计的玩具语料上的结果。

12.1 最近邻

只在 44 个内容词中找余弦相似度最高的 4 个:

cat    → horse(1.00), dog(1.00), sheep(0.99), cow(0.98)
apple  → orange(1.00), banana(0.99), peach(0.99), grape(0.99)
car    → truck(0.99), taxi(0.99), train(0.99), bus(0.98)
king   → queen(0.99), princess(0.98), prince(0.97), man(0.96)
girl   → boy(0.99), woman(0.98), man(0.97), princess(0.96)
paris  → tokyo(0.99), beijing(0.99), berlin(0.99), rome(0.99)
french → german(1.00), chinese(0.99), japanese(0.99), italian(0.99)

类别纯度 1.000;同类词平均余弦 0.983,异类词平均余弦 −0.038,区分度 1.021。模型从来没被告知任何类别信息,只是在”预测上下文”,却把每一类词都聚到了一起,而且不同类别的词几乎正交。

注意 king 的邻居:queen(只差性别)> princess > prince > man。在人物词内部,模型也学到了”谁和谁共享更多特征”。

12.2 VV、UU 还是 V+UV + U?

使用的向量异类平均余弦区分度类比准确率
只用 VV(中心词向量)0.3050.68151.0%
只用 UU(上下文向量)0.0960.89062.5%
V+UV + U−0.0381.02161.5%

在这份语料上,V+UV + U 的区分度最好,类比准确率和只用 UU 差不多。实践中 V+UV + U(或平均)是最常见的选择。

一个有意思的细节:vcat⊤uthe=+8.14v_\text{cat}^\top u_\text{the} = +8.14(cat 旁边经常出现 the,所以这个点积很大),而 vcat⊤vthe=−0.99v_\text{cat}^\top v_\text{the} = -0.99(cat 和 the 的用法毫不相似)。这验证了第 9.5 节说的:“经常一起出现”体现在 v⊤uv^\top u 上,“上下文相似”体现在 v⊤vv^\top v(或 u⊤uu^\top u)上。语言学里把前者叫组合关系(syntagmatic,如 drink 和 coffee),后者叫聚合关系(paradigmatic,如 coffee 和 tea)。

12.3 类比:king − man + woman ≈ queen

word2vec 最出名的性质是类比:

vking−vman+vwoman≈vqueenv_\text{king} - v_\text{man} + v_\text{woman} \approx v_\text{queen}

做法:要回答 “a : b :: c : ?”(a 之于 b,相当于 c 之于什么?),就计算目标向量 x=vb−va+vcx = v_b - v_a + v_c,在词表里找和 xx 余弦相似度最高的词。按惯例排除 a、b、c 三个输入词,否则答案常常就是 c 本身。

为什么会成立? 回想玩具语料的设计:queen 的上下文 = “女性的上下文” + “王室的上下文” + “成年人的上下文” 的混合。如果模型学到的向量也近似是这几个”特征方向”之和:

vking≈男+王室+成年,vman≈男+平民+成年,vwoman≈女+平民+成年v_\text{king} \approx \text{男} + \text{王室} + \text{成年}, \quad v_\text{man} \approx \text{男} + \text{平民} + \text{成年}, \quad v_\text{woman} \approx \text{女} + \text{平民} + \text{成年}

那么 vking−vman+vwoman≈女+王室+成年≈vqueenv_\text{king} - v_\text{man} + v_\text{woman} \approx \text{女} + \text{王室} + \text{成年} \approx v_\text{queen}。类比之所以成立,是因为语义特征在向量空间里近似是”可加的”。真实语料没有我们这么整齐,但”性别""单复数""国家—首都”这类关系确实会在上下文中留下比较一致的痕迹。第 2 讲会介绍 GloVe 论文对此的解释:类比来自共现概率的比值。

动手:代码 05 的 96 道类比题,word2vec 总准确率 61.5%:

关系例子准确率
gender(男 → 女)king : queen :: boy : ?12/12
royal(平民 → 王室)man : king :: woman : ?10/12
age(年幼 → 成年)boy : man :: girl : ?10/12
capital(国家 → 首都)france : paris :: japan : ?12/30
language(国家 → 语言)france : french :: italy : ?15/30

几道题的前 3 个候选:

man : woman :: king : ?     → queen(1.00), princess(0.97), prince(0.95)
king : queen :: boy : ?     → girl(1.00), woman(0.98), man(0.98)
france : paris :: japan : ? → tokyo(1.00), madrid(0.99), beijing(0.99)
france : french :: italy : ? → german(1.00), italian(1.00), chinese(0.99)

人物关系几乎全对,“国家—首都”和”国家—语言”只对了一半左右,而且答错时正确答案往往就在第二、三名(相似度只差 0.01)。一个可能的解释是:在我们的语料中,6 个首都(或 6 种语言)之间的差别只来自很少几个模板(例如 “people in ___ speak french”),这个”区分信号”很弱,被”它们都是首都”这个强得多的共性盖过了;而人物词的三个特征各有三个专属模板,信号强得多。

两点提醒:

  1. “排除输入词”这个惯例会让类比结果看起来比实际更好。后来的研究发现,很多类比之所以”成功”,主要是因为 vcv_c 本身就和答案很接近,减法和加法起的作用没有想象中大。
  2. 类比准确率只是衡量词向量的一种内在评估(intrinsic evaluation),它和词向量在下游任务上的表现并不总是一致。第 2 讲会专门讨论词向量的评估。

12.4 和计数方法比一比

同样的语料、同样的 10 维:

方法类别纯度同类平均余弦异类平均余弦区分度类比准确率
PPMI + SVD(k=10k = 10)1.0000.9980.1640.83431.2%
word2vec(d=10d = 10,V+UV + U)1.0000.983−0.0381.02161.5%

在这份玩具语料上,word2vec 的区分度和类比准确率都更好。但请不要据此断言”预测比计数好”:

  • 计数方法这边我们没有做任何调参(窗口、平滑、对奇异值加权等),而 PPMI 不降维时的类比准确率是 71.9%(第 8.5 节),说明信息本来就在矩阵里,是 SVD 降到 10 维时丢掉了。
  • Levy 和 Goldberg(2014)证明,带负采样的 skip-gram 在某种意义上等价于对一个(平移过的)PMI 矩阵做分解;他们后续的研究(2015)还发现,把 word2vec 的各种工程技巧移植到计数方法上,两者的差距大幅缩小。两条路线其实殊途同归。
  • 第 2 讲的 GloVe 明确地把两条路线结合了起来。

12.5 维度 dd 的影响

dd最终训练损失类别纯度类比准确率
23.97330.40513.5%
53.09810.92338.5%
102.68591.00061.5%
202.63721.00071.9%

维度太小,模型没有足够的”空间”同时表示类别和类别内部的特征;维度越大,训练损失越接近理论下界,类比也越准。但维度越大,参数越多、越慢,在数据有限时也越容易过拟合。真实的 word2vec 和 GloVe 常用 100 ~ 300 维。

12.6 二维投影

把 8 个人物词的向量(先归一化)用 PCA 投影到二维(代码 05 演示 6):

+------------------------------------------------------------+
|        *woman                           *queen             |
|                                                            |
|              *man                                *king     |
|                                                            |
|                                                            |
|                                                            |
|                                                            |
|                                                            |
|                                                            |
|                                                            |
|*girl                                                       |
|                                            *princess       |
|          *boy                                              |
|                                                   *prince  |
+------------------------------------------------------------+

横轴把平民(左)和王室(右)分开,纵轴把成年(上)和年幼(下)分开——PCA 找到的两个最主要的方向,恰好对应我们设计进语料的两个特征。性别是第三个方向,在这张图里只表现为每对词之间一个小而一致的偏移(woman 在 man 的左上,queen 在 king 的左上……)。这正是第 12.3 节”特征可加”的直观体现。(PCA/SVD 结果的正负号是任意的,如果你的 numpy 版本不同,图可能左右或上下翻转,但这不影响上面的结论。)

(如果你装了 matplotlib,代码会把这张图和其他几张图保存到 code/lecture01/figures/ 下。)


13. 词向量的局限与反思

13.1 一词多义

word2vec 给每个词形(word type)只分配一个向量。但 bank 有”银行”和”河岸”两个意思,“苹果”可以是水果也可以是公司。一个向量怎么同时表示两个意思?

研究发现,这个向量近似是各个义项向量的加权平均(按每个义项出现的频率加权),而且在高维空间里,各个义项的信息往往还能从这个平均向量中”分离”出来。第 2 讲会讨论。但无论如何,静态词向量不能根据上下文区分词义:“I went to the bank to deposit money” 和 “I sat on the river bank” 里的 bank 用的是同一个向量。

13.2 偏见

词向量从语料中学习统计规律,也会学到语料中的偏见。Bolukbasi 等人(2016)在 Google 新闻词向量上发现,“man : computer programmer :: woman : ?” 的答案是 homemaker(家庭主妇);Caliskan 等人(2017,《科学》)用类似心理学”内隐联想测验”的方法系统地证明,词向量中存在与人类类似的性别、种族等刻板印象。

在我们的玩具语料里,性别关系是”设计”出来的,不涉及任何刻板印象。但想象一下,如果语料里大量出现 “the doctor said he”、“the nurse said she”,模型就会学到 doctor 偏向男性、nurse 偏向女性——它忠实地反映了语料,而语料反映了社会。当这些词向量被用在简历筛选、搜索排序等系统中时,偏见就会被放大。CS224N 作业 1 里有专门的题目让你在真实词向量上观察这种偏见。

13.3 静态 vs 上下文相关

word2vec 和 GloVe 的词向量是静态的:每个词一个固定向量,与上下文无关。2018 年起,ELMo、BERT、GPT 等模型能根据上下文为每次出现的词计算不同的向量(上下文相关的表示,contextual representation),一举解决了一词多义问题。这是第 7–9 讲的内容。

13.4 词向量今天还有用吗?

直接使用 word2vec 词向量的场景已经不多了,但它的思想无处不在:

  1. 嵌入层:今天的每一个大语言模型,第一层都是一个”查表”操作——把每个 token 的编号映射成一个向量。这就是词向量的直接后代,只不过 token 是子词(见 CS336 第 1 讲的 BPE),向量是和整个模型一起端到端训练的。
  2. 自监督学习:word2vec 的训练信号来自文本本身(“预测上下文”),不需要任何人工标注。大语言模型的预训练(“预测下一个词”)是完全相同的原则,只是模型大了几个数量级。
  3. 向量空间中的语义:用向量的距离表示语义相似度,是今天检索增强生成(RAG)、语义搜索、推荐系统的基础(第 12 讲)。

14. 本讲小结

第一部分:课程与历史

  1. CS224N 的三个目标:深度学习 NLP 的方法、对人类语言的理解、动手搭建系统的能力。它比 CS336 更”宽”,更关注”为什么这样设计”。
  2. 语言难在歧义(词汇、切分、结构、指代、语用)、组合性、长尾、常识和语境。
  3. NLP 经历了五个阶段:早期机器翻译 → 手写规则 → 统计方法 → 深度学习 → 预训练大模型。每一步都在解决上一阶段的瓶颈,人类把越来越多的工作交给数据和计算。

第二部分:词向量

  1. 人工词典(WordNet)缺新词、缺细节、只有相似没有相关、昂贵;one-hot 向量两两正交,没有任何相似度信息。
  2. 分布假说:词的意义由它的上下文决定。词向量是稠密的分布式表示,相似的词向量相近(用余弦相似度衡量)。
  3. 计数路线:共现矩阵 → PPMI 去掉高频虚词的影响 → SVD 降维、合并同义维度、去噪。
  4. 预测路线(word2vec skip-gram):每个词两个向量,P(o∣c)=softmax(uo⊤vc)P(o \mid c) = \text{softmax}(u_o^\top v_c),最小化平均负对数似然。
  5. 梯度:∂J/∂vc=∑xP(x∣c)ux−uo\partial J / \partial v_c = \sum_x P(x \mid c) u_x - u_o(期望 − 观测),∂J/∂uw=(P(w∣c)−1[w=o])vc\partial J / \partial u_w = (P(w \mid c) - \mathbb{1}[w = o]) v_c;矩阵形式中 y^−y\hat{y} - y 反复出现。用梯度检验验证推导。
  6. 用 SGD 训练;朴素 softmax 的代价与词表大小成正比 → 第 2 讲的负采样。
  7. 词向量能捕捉类别、近义关系和可加的语义特征(类比);局限是一词多义、偏见、与上下文无关。

15. 与作业的关系

作业 1(Exploring Word Vectors) 基本就是本讲第 7–8、12–13 节的内容:

  1. 基于计数的词向量:实现 distinct_words(找出语料中所有不同的词)、compute_co_occurrence_matrix(窗口共现矩阵)、reduce_to_k_dim(用截断 SVD 降维)、plot_embeddings(画二维图),然后在一份真实语料上观察词的聚类。配套代码 03 中的同名函数思路一致,但作业会使用真实语料和 sklearn 的 TruncatedSVD。
  2. 基于预测的词向量:加载预训练的 GloVe 词向量(第 2 讲会讲它是怎么来的),回答一系列问题:找一个多义词,看它的最近邻是否混合了多个义项;找一组同义词和反义词,看为什么反义词的余弦相似度有时比同义词还高(提示:反义词的上下文往往非常相似,“hot”和”cold”都能填进”the water is ___”);做类比;观察并讨论词向量中的偏见。

作业 2 的书面部分(历年版本中常见)会要求你推导 word2vec 的梯度:证明朴素 softmax 损失等于交叉熵,推导 ∂J/∂vc\partial J / \partial v_c、∂J/∂uw\partial J / \partial u_w,再推导负采样版本的梯度。本讲第 10 节和练习 4–6 就是为此准备的。

作业的具体题目以当年官方发布的版本为准。建议先独立完成,卡住时再看配套代码和讲义。


16. 练习

练习 1(歧义):下面每句话属于哪种歧义?各有哪几种解释?

(a) “他在床上发现了一只老鼠的尸体。” (b) “Time flies like an arrow.” (c) “研究生命起源” (d) “小明对小刚说他考上了。”

参考答案
  • (a) 结构歧义:“在床上”修饰”发现”(人在床上时发现)还是修饰”尸体”(尸体在床上)。
  • (b) 结构 + 词汇歧义:最常见的理解是”光阴似箭”(time 是名词,flies 是动词);但也可以理解成”时间苍蝇喜欢箭”(time flies 是一种苍蝇,like 是动词),甚至”像测量箭一样给苍蝇计时”(time 是动词)。这是 NLP 课上最著名的歧义例子之一。
  • (c) 切分歧义:“研究 / 生命 / 起源” 还是 “研究生 / 命 / 起源”。
  • (d) 指代歧义:“他”指小明还是小刚。

练习 2(共现矩阵与 PMI)★:语料只有一句 “the cat sat on the mat”,窗口大小 m=1m = 1。

(a) 写出共现矩阵(行列顺序:the、cat、sat、on、mat)。 (b) 计算 PMI(the, cat) 和 PMI(cat, sat)(用自然对数),哪个更大?为什么?

参考答案

(a) 相邻对:the-cat、cat-sat、sat-on、on-the、the-mat,每对双向各记一次:

thecatsatonmat
the01011
cat10100
sat01010
on10100
mat10000

元素总和是 10。

(b) P(the,cat)=1/10P(\text{the}, \text{cat}) = 1/10,P(the)=3/10P(\text{the}) = 3/10(the 那一行之和),P(cat)=2/10P(\text{cat}) = 2/10。PMI =log⁡0.10.3×0.2=log⁡1.67≈0.51= \log \frac{0.1}{0.3 \times 0.2} = \log 1.67 \approx 0.51。 P(cat,sat)=1/10P(\text{cat}, \text{sat}) = 1/10,P(cat)=P(sat)=2/10P(\text{cat}) = P(\text{sat}) = 2/10。PMI =log⁡0.10.04=log⁡2.5≈0.92= \log \frac{0.1}{0.04} = \log 2.5 \approx 0.92。 PMI(cat, sat) 更大:两者共现次数相同,但 the 本身更常见,“和 the 一起出现”更可能是碰巧,信息量更小。这正是 PPMI 削弱高频虚词的机制。

练习 3(softmax):计算 softmax([1,2,3])([1, 2, 3]),保留三位小数。再计算 softmax([11,12,13])([11, 12, 13]),解释为什么结果一样。

参考答案

e1≈2.718e^1 \approx 2.718、e2≈7.389e^2 \approx 7.389、e3≈20.086e^3 \approx 20.086,和为 30.193,所以结果是 [0.090,0.245,0.665][0.090, 0.245, 0.665]。 softmax([11,12,13])([11, 12, 13]) 结果相同,因为 exi+C∑jexj+C=eCexieC∑jexj=exi∑jexj\frac{e^{x_i + C}}{\sum_j e^{x_j + C}} = \frac{e^C e^{x_i}}{e^C \sum_j e^{x_j}} = \frac{e^{x_i}}{\sum_j e^{x_j}}(平移不变性)。实现 softmax 时正是利用这一点,先减去最大值来避免溢出。

练习 4(推导 ∂J/∂vc\partial J / \partial v_c)★★:不看讲义,从 J=−log⁡exp⁡(uo⊤vc)∑wexp⁡(uw⊤vc)J = -\log \frac{\exp(u_o^\top v_c)}{\sum_w \exp(u_w^\top v_c)} 出发,推导 ∂J/∂vc\partial J / \partial v_c。然后回答:什么情况下梯度为零?在有限的向量下能达到吗?

参考答案

推导见第 10.2 节,结果 ∂J∂vc=∑xP(x∣c)ux−uo\frac{\partial J}{\partial v_c} = \sum_x P(x \mid c) u_x - u_o。 梯度为零,要求”期望的上下文向量”恰好等于 uou_o。一种情况是 P(o∣c)=1P(o \mid c) = 1(其他词概率都为 0),此时期望就是 uou_o。但 softmax 的输出永远严格大于 0,所以只有当 uo⊤vc−uw⊤vc→∞u_o^\top v_c - u_w^\top v_c \to \infty(向量长度趋于无穷)时才能无限逼近,有限的向量达不到。这也说明了为什么只对一个样本反复优化,向量会越变越长。真实训练中,同一个 cc 有很多不同的 oo,平衡点是让 P(⋅∣c)P(\cdot \mid c) 接近真实的上下文分布,此时所有样本的梯度之和为零,而不是每个样本的梯度都为零。

练习 5(交叉熵):设 yy 是真实上下文词 oo 的 one-hot 向量,y^\hat{y} 是模型预测的概率分布。证明朴素 softmax 损失 −log⁡P(o∣c)-\log P(o \mid c) 等于交叉熵 −∑wywlog⁡y^w-\sum_w y_w \log \hat{y}_w。

参考答案

yy 只有第 oo 个元素为 1,其余为 0,所以 −∑wywlog⁡y^w=−1⋅log⁡y^o−∑w≠o0⋅log⁡y^w=−log⁡y^o=−log⁡P(o∣c)-\sum_w y_w \log \hat{y}_w = -1 \cdot \log \hat{y}_o - \sum_{w \ne o} 0 \cdot \log \hat{y}_w = -\log \hat{y}_o = -\log P(o \mid c)。

练习 6(∂J/∂U\partial J / \partial U)★:(a) 推导 ∂J/∂uw\partial J / \partial u_w,分 w=ow = o 和 w≠ow \ne o 两种情况。(b) 写出 ∂J/∂U\partial J / \partial U 的矩阵形式。(c) 计算一个样本的梯度需要多少次乘法(用 ∣V∣|V| 和 dd 表示,数量级即可)?

参考答案

(a) w=ow = o:(P(o∣c)−1)vc(P(o \mid c) - 1) v_c;w≠ow \ne o:P(w∣c)vcP(w \mid c) v_c。推导见第 10.4 节。 (b) ∂J∂U=(y^−y)vc⊤\frac{\partial J}{\partial U} = (\hat{y} - y) v_c^\top,形状 ∣V∣×d|V| \times d。 (c) 算分数 UvcU v_c 要 ∣V∣d|V| d 次乘法,算 U⊤(y^−y)U^\top (\hat{y} - y) 要 ∣V∣d|V| d 次,算外积要 ∣V∣d|V| d 次,总共 O(∣V∣d)O(|V| d)。∣V∣=300|V| = 300 万、d=300d = 300 时约 10910^9 量级,这就是需要负采样的原因。

练习 7(为什么要两个向量):如果每个词只用一个向量 vwv_w(即 uw=vwu_w = v_w),P(o∣c)=exp⁡(vo⊤vc)∑wexp⁡(vw⊤vc)P(o \mid c) = \frac{\exp(v_o^\top v_c)}{\sum_w \exp(v_w^\top v_c)}。(a) 当 o=co = c 时,分子是什么?它有什么特点?(b) 这会导致什么不合理的现象?

参考答案

(a) 分子是 exp⁡(∥vc∥2)\exp(\|v_c\|^2)。∥vc∥2\|v_c\|^2 永远非负,而且向量越长越大。 (b) 模型倾向于认为”每个词都很可能出现在自己的上下文里”,尤其是向量较长的词;但真实语言中一个词很少紧挨着自己出现。为了压低 P(c∣c)P(c \mid c),模型只能让 vcv_c 变短,可这又和”让 vcv_c 与真实上下文词的点积变大”相冲突。用两个向量就没有这个约束。另外,单向量时对 vcv_c 求导会多出 2vc2 v_c 这样的项,推导和实现都更麻烦。

练习 8(参数量):Google 新闻词向量的词表有 300 万个词,每个 300 维。(a) 训练时 skip-gram 有多少个参数?(b) 公开发布的文件只包含一套向量,按 float32 存储约多大?

参考答案

(a) 2×3,000,000×300=182 \times 3{,}000{,}000 \times 300 = 18 亿个参数。 (b) 3,000,000×300×43{,}000{,}000 \times 300 \times 4 字节 =3.6= 3.6 GB。

练习 9(梯度检验):为什么中心差分 f(x+ϵ)−f(x−ϵ)2ϵ\frac{f(x + \epsilon) - f(x - \epsilon)}{2\epsilon} 比单边差分 f(x+ϵ)−f(x)ϵ\frac{f(x + \epsilon) - f(x)}{\epsilon} 更精确?(提示:泰勒展开。)

参考答案

泰勒展开:f(x±ϵ)=f(x)±ϵf′(x)+ϵ22f′′(x)±ϵ36f′′′(x)+⋯f(x \pm \epsilon) = f(x) \pm \epsilon f'(x) + \frac{\epsilon^2}{2} f''(x) \pm \frac{\epsilon^3}{6} f'''(x) + \cdots。 单边差分:f(x+ϵ)−f(x)ϵ=f′(x)+ϵ2f′′(x)+⋯\frac{f(x + \epsilon) - f(x)}{\epsilon} = f'(x) + \frac{\epsilon}{2} f''(x) + \cdots,误差是 O(ϵ)O(\epsilon)。 中心差分:两式相减,偶数次项抵消,f(x+ϵ)−f(x−ϵ)2ϵ=f′(x)+ϵ26f′′′(x)+⋯\frac{f(x + \epsilon) - f(x - \epsilon)}{2\epsilon} = f'(x) + \frac{\epsilon^2}{6} f'''(x) + \cdots,误差是 O(ϵ2)O(\epsilon^2)。ϵ=10−5\epsilon = 10^{-5} 时,前者误差约 10−510^{-5},后者约 10−1010^{-10}。

练习 10(动手):

(a) 在 05_word2vec_training.py 中,把学习率 lr 改成 0.05 和 5.0,观察损失曲线。 (b) 把窗口大小从 2 改成 1 和 4(skipgram_pairs(..., window=...)),类比准确率和最近邻有什么变化? (c) 在 corpus.py 里加一个新类别(比如颜色:red、blue、green……和几个模板),重新运行 03 和 05,新类别能被聚到一起吗? (d) 在 04_word2vec_gradients.py 的 naive_softmax_loss_and_gradient 里故意引入一个错误(比如把 delta[outside_idx] -= 1.0 改成 += 1.0),运行演示 3,看梯度检验能否发现。

练习 11(思考:偏见)★:假设在玩具语料里加入大量 “the doctor said he was tired” 和 “the nurse said she was tired”,但没有反过来的句子。训练后 “man : doctor :: woman : ?” 的答案可能是什么?这个问题能通过”删掉语料中的性别词”解决吗?会带来什么新问题?

参考答案

答案很可能是 nurse:doctor 的上下文里只有男性代词,nurse 的只有女性代词,词向量会把”性别方向”编码进这两个职业词里。 删掉性别词会让模型无法学习”he/she""king/queen”等正当的性别信息,也无法正确理解指代。而且偏见还会通过其他相关词(名字、衣着、爱好)间接进入向量。Bolukbasi 等人提出的思路是:找出向量空间中的”性别方向”,让本应中性的词(如职业)在这个方向上的投影为零,同时保留 king/queen 这类本身有性别的词。但后续研究发现这种”去偏”往往只是把偏见藏了起来,而没有真正消除它。这至今仍是一个开放问题。


17. 延伸阅读

  • CS224N 课程笔记(Lecture Notes Part I: Word Vectors I)和 Winter 2021 / 2023 第 1 讲录像。
  • Christopher Manning,Human Language Understanding & Reasoning(Daedalus,2022):本讲第 3 节 NLP 简史的主要参考,十几页,通俗易读。
  • Mikolov et al.,Efficient Estimation of Word Representations in Vector Space(2013)和 Distributed Representations of Words and Phrases and their Compositionality(2013):word2vec 的两篇原始论文。第二篇介绍了负采样,是第 2 讲的主要内容。
  • Jurafsky & Martin,Speech and Language Processing(第 3 版草稿,网上免费),第 6 章 Vector Semantics and Embeddings:对 PPMI、TF-IDF、word2vec 有非常清楚的讲解。
  • Turney & Pantel,From Frequency to Meaning: Vector Space Models of Semantics(2010):深度学习之前”计数路线”的全面综述。
  • Levy & Goldberg,Neural Word Embedding as Implicit Matrix Factorization(2014);Levy, Goldberg & Dagan,Improving Distributional Similarity with Lessons Learned from Word Embeddings(2015):计数和预测两条路线的联系。
  • Bolukbasi et al.,Man is to Computer Programmer as Woman is to Homemaker? Debiasing Word Embeddings(2016)。
  • Harris,Distributional Structure(1954);Firth,A Synopsis of Linguistic Theory(1957):分布假说的源头。

18. 下一讲预告

本讲用最朴素的 softmax 训练了 skip-gram,并留下了几个问题。下一讲(词向量进阶)会逐一回答:

  • 朴素 softmax 太慢,负采样怎么把代价从 O(∣V∣)O(|V|) 降到 O(k)O(k)?它的损失函数和梯度是什么?
  • CBOW 和 skip-gram 有什么区别?高频词下采样(subsampling)又是做什么的?
  • GloVe:怎样把”计数”和”预测”两条路线结合起来?为什么共现概率的比值能解释类比现象?
  • 怎么评估词向量?内在评估(类比、词相似度数据集)和外在评估(下游任务)有什么区别?
  • 一词多义:一个向量能表示多个义项吗?

下一讲之前,建议你:跑通 code/lecture01/ 下的 5 个脚本;不看讲义,自己在纸上推一遍第 10 节的两个梯度;完成练习 2、4、6、7。

← 返回课程目录