全书导航
大模型之路:从图灵、感知机到 ChatGPT · 卷 3

第 21 章:Word2Vec:词语如何变成向量

本章问题:机器如何理解词语之间的关系?


RNN 能处理序列了,但词只是整数编号——"猫"和"狗"的编号距离不等于它们的语义距离。这一章解决:词语如何变成有语义信息的稠密向量。

21.1 词的困境

语言的最小操作单元是词。但在 RNN 和 CNN 看来,一切都是数字。那么,"猫"这个字怎么变成数字?

最简单的答案是给每个词一个编号:猫 = 1, 狗 = 2, 太阳 = 3, …… 这叫整数编码。但这个编号完全没有任何语言意义——"1"和"2"之间的距离无法告诉你"猫"和"狗"的关系比"猫"和"太阳"的关系更近。

再复杂一点,可以用one-hot 向量:如果词典有 10,000 个词,"猫"用一个长度 10,000 的向量表示——"猫"对应的位置是 1,其余 9,999 位全是 0。

但 one-hot 有两个致命的问题。第一,向量极度稀疏——两个词的 one-hot 向量之间的内积永远是 0(正交),不管它们在语义上多接近。第二,向量的维度随词典大小线性增长——一个 50 万词的词典要求每个词有一个 50 万维的向量。

词向量的想法就是:把稀疏的高维 one-hot 向量压缩成稠密的低维连续向量。 比如每个词不是 10,000 维,而是 100 维或 300 维,每一维都是一个连续数,并且语义相近的词在向量空间中互相靠近。


21.2 分布式假设:一个词的意义由它的邻居决定

词向量的理论基础是一个被称为分布式假设的语言学概念。它最简洁的表述来自语言学家弗斯(J.R. Firth, 1957):

你可以通过一个词的同伴来认识这个词。

如果两个词频繁出现在相同的上下文窗口中,它们的语义大概率是相似的。

验证一下这个直觉:看一个真实的语料库(如维基百科),提取"猫"经常在哪些词附近出现——"宠物""跳""毛""鱼""喵"。再提取"狗"经常在哪些词附近出现——"宠物""跑""毛""骨头""汪"。"太阳"呢?"光""系""能""月亮"。

"猫"和"狗"的上下文高度重叠。"猫"和"太阳"几乎没有共有上下文。如果你能在海量文本中统计这些共现模式,"猫"和"狗"的向量就应该被推到相近的位置,"太阳"则被推远。

这个直觉是 word2vec 的基础。它不是让语言学家去定义词语的语义相似性——给足够多的文本数据(几亿到几十亿词),词的邻域信息会从数据中自组织出语义结构。


21.3 Word2Vec:两种架构

2013 年,米科洛夫等人在 Google 发布了 word2vec。他们提供了两种训练架构,核心思想是对称的。

Skip-gram:给定中心词,预测它周围的词。输入 "猫" → 输出 "一只""在""沙发""上""睡觉"。训练目标是让中心词的向量在给定窗口内能较准确地推回周围词的向量。

CBOW(连续词袋):给定周围词,预测中心词。输入 "一只""在""沙发""上""睡觉" → 输出 "猫"。这是 Skip-gram 的对偶形式。

Skip-gram 在小语料上效果更好,尤其是对低频词;CBOW 训练更快。两者的核心输出都是训练完成后的权重矩阵——这个矩阵的每一行,就是一个词的向量表示。

有趣的是——word2vec 的训练目标本身并不是"造一个好用的词向量",而是"让一个简单的预测任务做好"。但训练完成后的副产品——那些被学好的内部权重——恰好成为了极其有用的词表示。


21.4 "王 - 男 + 女 ≈ 后":向量空间里的语义运算

word2vec 最让大众感到震撼的发现之一来自它内嵌的类比性质

vec("国王") - vec("男人") + vec("女人") ≈ vec("王后")

在大量文本中,"国王"和"男人"在大多数句子中的差异在于性别成分,且这个性别方向在很多其他相关的词对(如父亲/母亲、丈夫/妻子、叔叔/阿姨)中也是相同的——在向量空间中对应着一个大致平行的向量差。

同样地:vec("巴黎") - vec("法国") + vec("意大利") ≈ vec("罗马")(首都关系方向),vec("大") - vec("更大") + vec("好") ≈ vec("更好")(比较级方向)。

这些不是被编程进去的规则。这些差异是训练过程中自动浮现的。大量人类语言使用者在千百年中形成表述问题时反复使用的模式——性别对立、国家-首都关系、时态变化——在足够多的文本中呈现出统计信号。这些信号被编码为向量空间中的一致位移方向。

这个现象传达了一个强有力的信息:语义关系——在高层次上——是在许多独立实例的共现模式中浮现出来的,而不是被预设的。 这和视觉 CNN 中学到的层级特征(边缘→纹理→语义)有异曲同工的地方——两者都是"从数据中涌现的表示"。


21.5 词向量的局限

词向量的模式是革命性的,但它也有明显局限。

每个词只有一个向量。 "苹果"在"我吃了一个苹果"和"苹果发布了新手机"中是完全同一个向量。它无法根据上下文区分同一个词的不同意思——这和多义词的处理是天然矛盾。

词向量是静态的。 训练完成后,"猫"的向量就固定了。无论模型在新的任务中想要更关注"猫"的不同方面——行为属性、视觉特征、或者分类学上的位置——它都无法获得新信息。

词序信息被丢失了。 "猫追狗"和"狗追猫"在词袋模型下几乎是一样的(同样的词,几乎同样的上下文窗口),但语义截然不同。

这些局限推动了后来的上下文词向量——ELMo(2018)、BERT(2018)——不再给每个词一个固定的向量,而是给每个词在具体句子中的特定出现位置一个依赖于上下文的动态向量。"苹果"在"吃"的旁边和在"发布"的旁边,产生完全不同的表示。

但上下文词向量的底层逻辑,仍然依赖 word2vec 所奠定的核心思想:词的意义,来自它和其他词的互动模式。


21.6 最小代码:训练 Skip-gram 词向量

以下代码用 PyTorch 训练一个最小 Skip-gram,使用"文本中的词预测周围的词"目标。语料是手动构造的几十句英文短句——重在流程,不在数据大小。

python
import torchimport torch.nn as nnimport torch.nn.functional as F# 1. 手工小语料corpus = [    "the cat sits on the mat".split(),    "the dog sits on the rug".split(),    "the cat chases the mouse".split(),    "the dog chases the cat".split(),    "the mouse eats cheese".split(),    "the dog eats meat".split(),]# 2. 构建词表words = sorted(set(w for s in corpus for w in s))word2idx = {w: i for i, w in enumerate(words)}idx2word = {i: w for w, i in word2idx.items()}vocab_size = len(words)# 3. 生成 Skip-gram 训练对 (center, context), window=2pairs = []for sent in corpus:    for i, center in enumerate(sent):        for j in range(max(0, i - 2), min(len(sent), i + 3)):            if i != j:                pairs.append((word2idx[center], word2idx[sent[j]]))# 4. Skip-gram 模型class SkipGram(nn.Module):    def __init__(self, vocab_size, emb_dim=10):        super().__init__()        self.in_embed = nn.Embedding(vocab_size, emb_dim)   # 中心词嵌入        self.out_embed = nn.Embedding(vocab_size, emb_dim)  # 上下文词嵌入    def forward(self, center, context):        v_c = self.in_embed(center)    # (batch, emb_dim)        u_o = self.out_embed(context)  # (batch, emb_dim)        # 点积得分,按词汇表归一 → 预测周围词分布        scores = v_c @ self.out_embed.weight.T  # (batch, vocab_size)        return scoresmodel = SkipGram(vocab_size, emb_dim=10)opt = torch.optim.Adam(model.parameters(), lr=0.01)# 5. 训练center_t = torch.tensor([p[0] for p in pairs])context_t = torch.tensor([p[1] for p in pairs])for epoch in range(2000):    scores = model(center_t, context_t)    loss = F.cross_entropy(scores, context_t)    opt.zero_grad()    loss.backward()    opt.step()# 6. 检查词向量相似度:在训练后获取 in_embed 层的权重作为词向量with torch.no_grad():    vecs = model.in_embed.weight.data  # (vocab_size, 10)    cat_idx = word2idx["cat"]    dog_idx = word2idx["dog"]    mouse_idx = word2idx["mouse"]    mat_idx = word2idx["mat"]    cos = nn.CosineSimilarity(dim=0)    print(f"cos(cat, dog)   = {cos(vecs[cat_idx], vecs[dog_idx]):.3f}")    print(f"cos(cat, mouse) = {cos(vecs[cat_idx], vecs[mouse_idx]):.3f}")    print(f"cos(cat, mat)   = {cos(vecs[cat_idx], vecs[mat_idx]):.3f}")

在这个超小语料上,cos(cat, dog) 大概率高于 cos(cat, mat)——"猫"与"狗"被相似的上下文推到了一起。


21.7 本章小实验:用浏览器探索词向量

打开一个预训练词向量的在线可视化网站(搜索"word embedding projector")。键入三个词:猫、狗、沙发。看它们在屏幕上的 3D 空间里——猫和狗靠在一起,沙发远在另一区。

再试一个经典类比。搜索国王、男人、女人——看这三个向量的空间差,对"王后"位置的近邻词。你大概率在附近的候选里能一眼看到"女王"或"王后"——那是纯靠词共现频率学习出来的,没有任何语言学规则被写进程序。


21.8 本章地图

text
问题:机器如何理解词语之间的关系?方法:利用词共现模式(分布式假设)——相似词出现在相似上下文中,在向量空间中被推近。架构:Skip-gram(中心词→周围词)或 CBOW(周围词→中心词),通过训练结果得到的权重矩阵作为词向量。突破:"国王 - 男 + 女 ≈ 王后"——语义关系以向量位移的形式从海量文本中自动浮现。局限:静态词向量(一词一向量,无法处理多义词),缺少词序信息。今天:上下文词向量(BERT、GPT 的 token embedding + Transformer 隐藏状态)取代了静态词向量,但基本思想——"语义来自上下文"——不变。

21.9 本章结语:从离散符号到稠密语义空间

word2vec 在 AI 史中的位置不在于它是最好的词向量方法——它后来被 ELMo、BERT 和 GPT 的表示超越了。它的贡献是证明了一个核心思想:离散符号可以获得稠密的、结构化的向量表示——而且不需要人定义规则。

当你把一个词从一个离散编号映射为 300 个浮点数时,你让它进入了微积分的世界——你可以求导("这句语义和那句语义之间的差异应该如何修正"),可以做内积("这两个词的表示有多相似"),可以做加减法("在这个语义方向上偏移以获得近义词")。

这一步,是语言从"符号处理的领域"跨入"连续数学"的入口。

下一章,我们看 Seq2Seq——如何用一个完整模型,把一个序列变成另一个序列。这是机器翻译的旧时代,也是 Attention 的前夜。

SECTION §02 · ENGAGE

Discussion

留言区 · GitHub-powered comments via Giscus