前置知识: 自然语言处理

词嵌入Word2Vec

00:00
9 min Intermediate

从零构建Word2Vec,理解词嵌入的几何与语义关系

词嵌入 — 从零构建Word2Vec

一个词由它周围的词决定。在这个想法上训练一个浅层网络,几何就自然涌现。

类型: 构建 语言: Python 前置条件: Phase 5 · 02(BoW + TF-IDF),Phase 3 · 03(从零构建反向传播) 时间: ~75 分钟

问题

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维) ── W' ──▶ softmax(词表)
                          ^
                          这就是嵌入

技巧: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

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

步骤 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

步骤 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

魔法公式:正对上的逻辑损失(希望sigmoid接近1)加负对上的逻辑损失(希望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]})

在预训练的300d 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 — Stanford的共现矩阵分解方法。50d、100d、200d、300d检查点。良好通用覆盖。课程04专门覆盖GloVe。
  • fastText — Facebook的Word2Vec扩展,嵌入字符n-gram。通过组合子词处理词外词。课程04。
  • Google News上的预训练Word2Vec — 300d,300万词词表,2013年发布。至今每天被下载。

Word2Vec在2026年何时仍然赢

  • 轻量级领域特定检索。在笔记本上一小时训练医学摘要,获得通用模型无法捕获的专用向量。
  • 类比式特征工程。gender_vector = mean(man - woman pairs)。从其他词中减去它获得性别中性轴。仍在公平性研究中使用。
  • 可解释性。100d足够小,可以通过PCA或t-SNE绘图并实际看到聚类形成。
  • 任何必须在无GPU设备上运行推理的地方。Word2Vec查找就是单行获取。

Word2Vec在哪里失败

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

上下文嵌入(ELMo、BERT、之后每个Transformer)通过根据周围上下文为每次出现的词产生不同向量解决了这个问题。这就是从Word2Vec到BERT的跳跃:从静态到上下文。Phase 7覆盖Transformer部分。

词外问题是另一个失败。Word2Vec从未见过 Zoomer-approved 如果它不在训练数据中。没有后备。fastText用子词组合修复这个(课程04)。

交付它

将结果保存为 outputs/skill-embedding-probe.md

练习

  1. 简单。 在微型语料(20句关于猫和狗的句子)上运行训练循环。200个epoch后,验证 nearest(vocab, W, W[vocab["cat"]]) 在前3中返回 dog。如果不是,增加epoch或词
  2. 中等。 添加频词采样。频率高于 10^-5 的词按与其频率比例概率训练对中丢弃。测量罕见词相似影响
  3. 困难。 在20 Newsgroups语料上训练模型。计算两个偏差he - shedoctor - nurse。将职业投影到两个上。报告哪些职业有最大偏差差距。这是公平研究人员使用的那种探测。

关键术语

术语通俗说法实际含义
嵌入词作为向量上下文学习的密集、低维(通常100-300)表示
Skip-gramWord2Vec技巧从中心词预测上下文词。比CBOW慢,罕见词更好。
负采样训练捷径 k随机词的二分类替代整个词的softmax。
静态嵌入每词一向量无论上下文相同向量义词失败
上下文嵌入上下文敏感向量基于周围词每次出现不同向量。Transformer产生的。
OOV词外训练中未见过的词。Word2Vec无法为这些产生向量

延伸阅读

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式