Word2Vec 词嵌入:从零实现


文档摘要

Word2Vec 词嵌入:从零实现 本节摘要:一个词,就是它所结交的伙伴。在这个想法上训练一个浅层网络,几何结构就自然涌现。TF-IDF 知道 和 是两个不同的词,却不知道它们意思几乎一样。在 上训练的分类器无法泛化到一篇写 的评论。Word2Vec 给了我们那个空间:让 和 在空间里落得相近,让 落在 附近。它只是一个两层神经网络,2013 年发表,训练数据以万亿 token 计,架构简单到令人发指,却重塑了之后十年的 NLP。本节从零搭 Skip-gram、负采样、嵌入表与类比推理,看清「词如何第一次带上语义」。 对应原课程:Phase 5 · Lesson 03 · (原英文 )。前置依赖:第 02 节(词袋与 TF-IDF)、Phase 3 · 03(从零实现反向传播)。

Word2Vec 词嵌入:从零实现

本节摘要:一个词,就是它所结交的伙伴。在这个想法上训练一个浅层网络,几何结构就自然涌现。TF-IDF 知道 dogpuppy 是两个不同的词,却不知道它们意思几乎一样。在 dog 上训练的分类器无法泛化到一篇写 puppy 的评论。Word2Vec 给了我们那个空间:让 dogpuppy 在空间里落得相近,让 king - man + woman 落在 queen 附近。它只是一个两层神经网络,2013 年发表,训练数据以万亿 token 计,架构简单到令人发指,却重塑了之后十年的 NLP。本节从零搭 Skip-gram、负采样、嵌入表与类比推理,看清「词如何第一次带上语义」。

对应原课程:Phase 5 · Lesson 03 · word-embeddings-word2vec(原英文 phases/05-nlp-foundations-to-advanced/03-word-embeddings-word2vec/docs/en.md)。前置依赖:第 02 节(词袋与 TF-IDF)、Phase 3 · 03(从零实现反向传播)。

学习目标

阅读完本节,你应当能够:

  1. 说清分布式假设与 Skip-gram、CBOW 两种 Word2Vec 变体的差异。
  2. 从零实现 Skip-gram 训练对、嵌入表、负采样目标与完整训练循环。
  3. 用学到的向量做最近邻检索与类比推理(king - man + woman ≈ queen),并解释其几何含义。
  4. gensim 复现训练,识别 Word2Vec 在 2026 年仍胜出的场景,以及多义性与 OOV 这两大失败。

一、问题与直觉

TF-IDF 知道 dogpuppy 是不同的词,却不知道它们意思几乎一样。在 dog 上训练的分类器,无法泛化到一篇写 puppy 的评论。你可以靠罗列同义词勉强糊弄,但这在罕见词、领域行话、以及每一种你没预料到的语言上都会失效。

你想要一种表示:让 dogpuppy 在空间里落得相近;让 king - man + woman 落在 queen 附近;让在 dog 上训练的模型,把一部分信号免费迁移到 puppy

Word2Vec 给了我们那个空间。两层神经网络,万亿 token 训练,2013 年发表。架构简单到令人发指,结果却重塑了之后十年的 NLP。

分布式假设(Firth, 1957):「你将由一个词所结交的伙伴来认识它。」如果两个词出现在相似的上下文里,它们大概率意思相近。

Word2Vec 有两种变体,都在利用这个想法:

  • Skip-gram:给定中心词,预测周围的词。cat -> (the, sat, on),窗口大小 2。
  • CBOW(连续词袋):给定周围的词,预测中心词。(the, sat, on) -> cat

Skip-gram 训练更慢,但对罕见词更好,于是成了默认。

网络只有一个无非线性的隐藏层。输入是词表上的 one-hot 向量,输出是词表上的 softmax。训练完后,丢掉输出层——隐藏层的权重就是嵌入。

one-hot(center) ── W ──▶ hidden (d-dim) ── W' ──▶ softmax(vocab) ^ 这就是嵌入

💡 关键技巧:对 10 万个词做 softmax 贵得离谱。Word2Vec 用负采样把它变成一个二分类任务——「这个上下文词是否真的出现在这个中心词附近,是或否?」每个训练对采样少量负例(非共现词),而不是对整个词表算 softmax。

二、从零实现

第 1 步:从语料生成训练对

def skipgram_pairs(docs, window=2): pairs = [] for doc in docs: for i, center in enumerate(doc): for j in range(max(0, i - window), min(len(doc), i + window + 1)): if i == j: continue pairs.append((center, doc[j])) return pairs
>>> skipgram_pairs([["the", "cat", "sat", "on", "mat"]], window=2) [('the', 'cat'), ('the', 'sat'), ('cat', 'the'), ('cat', 'sat'), ('cat', 'on'), ('sat', 'the'), ('sat', 'cat'), ('sat', 'on'), ('sat', 'mat'), ...]

窗口内的每一个 (中心, 上下文) 对都是一个正训练样本。

第 2 步:嵌入表

两个矩阵。W 是中心词嵌入表(你要保留的那个);W' 是上下文词表(常丢弃,有时与 W 取平均)。

import numpy as np def init_embeddings(vocab_size, dim, seed=0): rng = np.random.default_rng(seed) W = rng.normal(0, 0.1, size=(vocab_size, dim)) W_prime = rng.normal(0, 0.1, size=(vocab_size, dim)) return W, W_prime

小幅随机初始化。词表 1 万、维度 100 是现实的;教学用 50 词 × 16 维就足以看见几何效果。

第 3 步:负采样目标

对每个正对 (center, context),从词表里采样 k 个随机词作负例。训练模型,让点积 W[center] · W'[context] 对正例高、对负例低。

def sigmoid(x): return 1.0 / (1.0 + np.exp(-np.clip(x, -20, 20))) def train_pair(W, W_prime, center_idx, context_idx, negative_indices, lr): v_c = W[center_idx] u_pos = W_prime[context_idx] u_negs = W_prime[negative_indices] pos_score = sigmoid(v_c @ u_pos) neg_scores = sigmoid(u_negs @ v_c) grad_center = (pos_score - 1) * u_pos for i, u in enumerate(u_negs): grad_center += neg_scores[i] * u W[context_idx] = W[context_idx] W_prime[context_idx] -= lr * (pos_score - 1) * v_c for i, neg_idx in enumerate(negative_indices): W_prime[neg_idx] -= lr * neg_scores[i] * v_c W[center_idx] -= lr * grad_center

魔法公式:正对用 logistic loss(希望 sigmoid 接近 1),负对用 logistic loss(希望 sigmoid 接近 0)。梯度同时流向两张表。完整推导见原论文,想记住的话,拿铅笔推一遍。

第 4 步:在小语料上训练

def train(docs, dim=16, window=2, k_neg=5, epochs=100, lr=0.05, seed=0): vocab = build_vocab(docs) vocab_size = len(vocab) rng = np.random.default_rng(seed) W, W_prime = init_embeddings(vocab_size, dim, seed=seed) pairs = skipgram_pairs(docs, window=window) for epoch in range(epochs): rng.shuffle(pairs) for center, context in pairs: c_idx = vocab[center] ctx_idx = vocab[context] negs = rng.integers(0, vocab_size, size=k_neg) negs = [n for n in negs if n != ctx_idx and n != c_idx] train_pair(W, W_prime, c_idx, ctx_idx, negs, lr) return vocab, W

在大语料上跑够 epoch,共享上下文的词就有相似的中心嵌入。小语料上效果微弱,数十亿 token 上则效果惊人。

第 5 步:类比戏法

def nearest(vocab, W, target_vec, topk=5, exclude=None): exclude = exclude or set() inv_vocab = {i: w for w, i in vocab.items()} norms = np.linalg.norm(W, axis=1, keepdims=True) + 1e-9 W_norm = W / norms target = target_vec / (np.linalg.norm(target_vec) + 1e-9) sims = W_norm @ target order = np.argsort(-sims) out = [] for i in order: if i in exclude: continue out.append((inv_vocab[i], float(sims[i]))) if len(out) == topk: break return out def analogy(vocab, W, a, b, c, topk=5): v = W[vocab[b]] - W[vocab[a]] + W[vocab[c]] return nearest(vocab, W, v, topk=topk, exclude={vocab[a], vocab[b], vocab[c]})

在预训练的 300 维 Google News 向量上:

>>> analogy(vocab, W, "man", "king", "woman") [('queen', 0.71), ('monarch', 0.62), ('princess', 0.59), ...]

king - man + woman = queen。不是模型懂什么是王室,而是向量 (king - man) 捕捉了某种「王室」意味,把它加到 woman 上,就落在了「王室-女性」区域附近。

三、框架对比

从零写 Word2Vec 是为了教学,生产 NLP 用 gensim

from gensim.models import Word2Vec sentences = [ ["the", "cat", "sat", "on", "the", "mat"], ["the", "dog", "ran", "across", "the", "room"], ] model = Word2Vec( sentences, vector_size=100, window=5, min_count=1, sg=1, negative=5, workers=4, epochs=30, ) print(model.wv["cat"]) print(model.wv.most_similar("cat", topn=3))

真干活时,你几乎从不自己训 Word2Vec,而是下载预训练向量:

  • GloVe:斯坦福的共现矩阵分解方法,有 50/100/200/300 维检查点,通用覆盖好。第 04 节专讲。
  • fastText:Facebook 对 Word2Vec 的扩展,嵌入字符 n-gram,通过子词组合处理 OOV。第 04 节。
  • Google News 预训练 Word2Vec:300 维,300 万词表,2013 年发布,至今仍天天被下载。

Word2Vec 在 2026 年仍胜出的场景

  • 轻量级领域特定检索:在笔记本上一小时训完医学摘要,得到通用模型捕捉不到的专用向量。
  • 类比式特征工程:gender_vector = mean(man - woman pairs),从其他词里减掉它就得到性别中性轴,公平性研究仍在用。
  • 可解释性:100 维小到能用 PCA 或 t-SNE 画出来,真看见簇在形成。
  • 任何要在设备上无 GPU 推理的场景:Word2Vec 查表就是一次取行。

Word2Vec 失败在哪里

多义性之墙bank 只有一个向量。river bankfinancial bank 共用它;table(电子表格 vs 家具)也共用。下游分类器无法从向量里区分词义。

上下文嵌入(ELMo、BERT,以及之后的所有 Transformer)通过根据上下文为每次出现产出不同向量解决了这个。这正是从 Word2Vec 到 BERT 的那一跳:从静态到上下文

另一个失败是 OOV:训练数据里没见过的 Zoomer-approved,Word2Vec 无法产出向量,没有回退。fastText 用子词组合修掉了这个(第 04 节)。

四、可复用产物

保存为 outputs/skill-embedding-probe.md:

--- name: embedding-probe description: Inspect a word2vec model. Run analogies, find neighbors, diagnose quality. version: 1.0.0 phase: 5 lesson: 03 tags: [nlp, embeddings, debugging] --- You probe trained word embeddings to verify they are working. Given a `gensim.models.KeyedVectors` object and a vocabulary, you run: 1. Three canonical analogy tests. `king : man :: queen : woman`. `paris : france :: tokyo : japan`. `walking : walked :: swimming : ?`. Report the top-1 result and its cosine. 2. Five nearest-neighbor tests on domain-specific words the user supplies. Print top-5 neighbors with cosines. 3. One symmetry check. `similarity(a, b) == similarity(b, a)` to within float precision. 4. One degenerate check. If any embedding has a norm below 0.01 or above 100, the model has a training bug. Flag it. Refuse to declare a model good on analogy accuracy alone. Analogy benchmarks are gameable and do not transfer to downstream tasks. Recommend intrinsic + downstream evaluation together.

五、练习

  1. 基础:在一个小语料(20 句关于猫和狗的句子)上跑训练循环。200 epoch 后,验证 nearest(vocab, W, W[vocab["cat"]]) 的前 3 里有 dog。若没有,增加 epoch 或词表。
  2. 进阶:加入高频词下采样。频率高于 10^-5 的词,以正比于其频率的概率从训练对里丢弃。测量这对罕见词相似度的影响。
  3. 挑战:在 20 Newsgroups 语料上训一个模型。算两条偏置轴:he - shedoctor - nurse。把职业词投影到这两条轴上,报告哪些职业偏置差距最大——这正是公平性研究者用的探针。

本节要点回顾

  1. 分布式假设:词由其上下文定义,共享上下文的词意思相近。
  2. 两种变体:Skip-gram(中心预测上下文,慢但罕见词好,默认)、CBOW(上下文预测中心)。
  3. 架构极简:两层网络、无非线性隐藏层,丢掉输出层后隐藏层权重即嵌入。
  4. 负采样:把 10 万词的 softmax 换成对 k 个随机负例的二分类,计算量骤降。
  5. 训练对:窗口内每个 (中心, 上下文) 对都是正样本。
  6. 两张嵌入表:W(中心,保留)、W'(上下文,常丢弃)。
  7. 类比几何:king - man + woman ≈ queen,不是懂王室,而是向量方向捕捉了「王室」意味。
  8. gensim 几行复现,真干活时下预训练向量(GloVe/fastText/Google News)。
  9. 2026 年仍胜出:轻量领域检索、类比特征工程、可解释、设备端推理。
  10. 两大失败:多义性之墙(一个词一个向量)、OOV 无回退——上下文嵌入与子词组合分别修掉它们。

下一节,我们将走出 Word2Vec 的单家族,看 GloVe 如何用共现矩阵分解、FastText 如何用子词组合——把静态嵌入推向极限。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U