Word2Vec 词嵌入:从零实现 本节摘要:一个词,就是它所结交的伙伴。在这个想法上训练一个浅层网络,几何结构就自然涌现。TF-IDF 知道 和 是两个不同的词,却不知道它们意思几乎一样。在 上训练的分类器无法泛化到一篇写 的评论。Word2Vec 给了我们那个空间:让 和 在空间里落得相近,让 落在 附近。它只是一个两层神经网络,2013 年发表,训练数据以万亿 token 计,架构简单到令人发指,却重塑了之后十年的 NLP。本节从零搭 Skip-gram、负采样、嵌入表与类比推理,看清「词如何第一次带上语义」。 对应原课程:Phase 5 · Lesson 03 · (原英文 )。前置依赖:第 02 节(词袋与 TF-IDF)、Phase 3 · 03(从零实现反向传播)。
本节摘要:一个词,就是它所结交的伙伴。在这个想法上训练一个浅层网络,几何结构就自然涌现。TF-IDF 知道
dog和puppy是两个不同的词,却不知道它们意思几乎一样。在dog上训练的分类器无法泛化到一篇写puppy的评论。Word2Vec 给了我们那个空间:让dog和puppy在空间里落得相近,让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(从零实现反向传播)。
阅读完本节,你应当能够:
king - man + woman ≈ queen),并解释其几何含义。TF-IDF 知道 dog 和 puppy 是不同的词,却不知道它们意思几乎一样。在 dog 上训练的分类器,无法泛化到一篇写 puppy 的评论。你可以靠罗列同义词勉强糊弄,但这在罕见词、领域行话、以及每一种你没预料到的语言上都会失效。
你想要一种表示:让 dog 和 puppy 在空间里落得相近;让 king - man + woman 落在 queen 附近;让在 dog 上训练的模型,把一部分信号免费迁移到 puppy。
Word2Vec 给了我们那个空间。两层神经网络,万亿 token 训练,2013 年发表。架构简单到令人发指,结果却重塑了之后十年的 NLP。
分布式假设(Firth, 1957):「你将由一个词所结交的伙伴来认识它。」如果两个词出现在相似的上下文里,它们大概率意思相近。
Word2Vec 有两种变体,都在利用这个想法:
cat -> (the, sat, on),窗口大小 2。(the, sat, on) -> cat。Skip-gram 训练更慢,但对罕见词更好,于是成了默认。
网络只有一个无非线性的隐藏层。输入是词表上的 one-hot 向量,输出是词表上的 softmax。训练完后,丢掉输出层——隐藏层的权重就是嵌入。
one-hot(center) ── W ──▶ hidden (d-dim) ── W' ──▶ softmax(vocab) ^ 这就是嵌入
💡 关键技巧:对 10 万个词做 softmax 贵得离谱。Word2Vec 用负采样把它变成一个二分类任务——「这个上下文词是否真的出现在这个中心词附近,是或否?」每个训练对采样少量负例(非共现词),而不是对整个词表算 softmax。
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'), ...]
窗口内的每一个 (中心, 上下文) 对都是一个正训练样本。
两个矩阵。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 维就足以看见几何效果。
对每个正对 (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)。梯度同时流向两张表。完整推导见原论文,想记住的话,拿铅笔推一遍。
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 上则效果惊人。
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,而是下载预训练向量:
gender_vector = mean(man - woman pairs),从其他词里减掉它就得到性别中性轴,公平性研究仍在用。多义性之墙。bank 只有一个向量。river bank 和 financial 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.
nearest(vocab, W, W[vocab["cat"]]) 的前 3 里有 dog。若没有,增加 epoch 或词表。10^-5 的词,以正比于其频率的概率从训练对里丢弃。测量这对罕见词相似度的影响。he - she 和 doctor - nurse。把职业词投影到这两条轴上,报告哪些职业偏置差距最大——这正是公平性研究者用的探针。k 个随机负例的二分类,计算量骤降。W(中心,保留)、W'(上下文,常丢弃)。king - man + woman ≈ queen,不是懂王室,而是向量方向捕捉了「王室」意味。下一节,我们将走出 Word2Vec 的单家族,看 GloVe 如何用共现矩阵分解、FastText 如何用子词组合——把静态嵌入推向极限。