第03回 词的灵魂——嵌入与 Word2vec
独热万点空寂寥,词词孤影各逍遥。
一入低维同席坐,亲疏远近自能招。
上回说到,独热能点名,词袋能计数,TF‑IDF 能称分量——可它们都像“编户齐民的名册”:写得清楚,却不懂人情。
“吃饭”与“用餐”明明是亲戚,在独热里却如陌路;“苹果”(水果)与“苹果”(公司)在词袋里也难分真假。
要让算法懂这份“词的灵魂”,就得教它一门新功夫:嵌入(embedding)。
一、嵌入是何物:给每个词一把“方向”
嵌入的意思很朴素:
把每个词从“稀疏的高维灯阵”搬到“稠密的低维江湖”里。
我们不再用十万维的独热去“点名”,而是为每个词分配一个长度为 的向量(比如 或 ):
这向量不再是人手规定,而是让模型从大量文本里自己学出来。
学成之后,有三件好处立刻见效:
- 相似可算:用余弦相似度一比,“吃饭”会靠近“用餐”
- 类比可做:某些方向代表“性别”“时态”“国家‑首都”等属性
- 可喂神经网:稠密向量更适合后续 RNN、注意力、Transformer 的计算
从此,词不再是一张身份证,而像一把带方向的剑:剑锋所指,便是它在语义江湖里的位置。
二、江湖祖训:同伴决定你是谁
嵌入背后有句名言,常被译作“同伴决定你是谁”:
你若常与“锅”“菜”“米”“香”结伴,旁人多半猜你与吃喝相关;
你若常与“股票”“发布会”“iPhone”“生态”一同出现,“苹果”便更像公司而非水果。
这条祖训,就是分布式语义的直觉:词义来自上下文。
Word2vec 便是把这条直觉炼成算法的代表武功。
三、Word2vec 两路武功:CBOW 与 Skip‑gram
Word2vec 是一套“用预测练内功”的法门:
不靠人工标注意义,而是让模型做一个简单任务——猜词。
设一句话是一串词:
取某个位置 ,把它周围一圈当“上下文”。窗口大小为 时,上下文可写作:
然后便分两路练法。
1)CBOW:合围出招,猜中间那一位
CBOW(Continuous Bag of Words)像“群侠围攻”:
把周围上下文的向量汇在一起,去猜中心词 。
它训练快、稳健,像练基本拳架。
2)Skip‑gram:以一敌多,猜四方来者
Skip‑gram 像“一剑开路”:
拿中心词 去预测它周围的每个上下文词。
它更偏向把稀有词练得精细,因为每次出招都围着中心词打磨。
Word2vec 在工程里最常被提起的,是 Skip‑gram + 负采样这一组合拳。
四、难点与妙解:负采样(Negative Sampling)
若要“猜词”,最直接的做法是 softmax:
中心词向量与所有词表里的词逐个点积,算出概率,选最大者。
可词表往往有十万、百万之多,每次更新都要扫全词表,开销极大。
于是 Word2vec 出一奇招:
不再每次把天下英雄都叫来比武,而是——
- 拿一对真实共现的词当“正样本”(比如“吃饭‑香”)
- 再从词表里抽若干个“不太可能一起出现”的词当“负样本”(比如“吃饭‑火箭”)
- 训练一个二分类器:分清“真同伴”与“假路人”
把这事写成一句话,就是:
用少量对比,逼出向量方向。
在实现层面,我们只需要一个 sigmoid:
- 点积大 → sigmoid 近 1 → 更像同伴
- 点积小 → sigmoid 近 0 → 更像路人
这样一来,每次更新只算“正样本 + 几个负样本”,速度大大提升。
2024 年仍有研究在更一般的图嵌入与对比目标里讨论“负采样这件事本身的结构意义”,把它看作一种防止表示塌缩的“排斥力”。1
五、两套向量:一词两身,方成武功
很多初学者会疑惑:
“同一个词,为何要两套向量?”
在 Skip‑gram + 负采样里,常见做法是:
- 一套当“出招者”(input / target embedding)
- 一套当“接招者”(output / context embedding)
训练时两套都更新;训练后有的人取其中一套,有的人取两套相加或平均。
你可以把它理解为:
一个人既有“自己是谁”的内在气质,也有“别人如何看他”的江湖名声;两者合在一起,才更像完整的人。
六、从 Word2vec 到 2026:嵌入仍是底座
看官读到这里,也许会问:
“2026 年都推理模型、慢思考、Mamba、SAE 了,为何还学 Word2vec?”
原因很简单:
再大的模型,也离不开“把符号变成向量”的第一步。
今天的大模型多用子词 token;检索系统用句向量;多模态模型用图像/音频向量。嵌入早已不止“词向量”,而是“万物向量”。
以 OpenAI 在 2024‑01‑25 发布的 embedding v3 模型为例,就强调了在多语言检索与通用评测基准上的提升,并提供可缩短向量维度的参数,方便工程落地。2
所以你学 Word2vec,不是为了在 2026 年拿它去打擂台,而是为了:
- 明白“相似度为何能算出来”
- 明白“训练目标如何把语义压进向量”
- 明白“后面一切复杂系统,都在这上面加招式”
七、极简代码:用 PyTorch 练一套 Skip‑gram + 负采样
下面是一段极简可运行示例:
用很小的语料训练词向量,并打印每个词的近邻(用余弦相似度)。
import math
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
def build_vocab(tokens):
vocab = {}
for w in tokens:
if w not in vocab:
vocab[w] = len(vocab)
inv = {i: w for w, i in vocab.items()}
return vocab, inv
def make_pairs(tokens, window):
pairs = []
for i, w in enumerate(tokens):
for j in range(max(0, i - window), min(len(tokens), i + window + 1)):
if j != i:
pairs.append((w, tokens[j]))
return pairs
class SGNS(nn.Module):
def __init__(self, vocab_size, dim):
super().__init__()
self.in_embed = nn.Embedding(vocab_size, dim)
self.out_embed = nn.Embedding(vocab_size, dim)
bound = 1.0 / math.sqrt(dim)
nn.init.uniform_(self.in_embed.weight, -bound, bound)
nn.init.uniform_(self.out_embed.weight, -bound, bound)
def forward(self, center_ids, pos_ids, neg_ids):
v = self.in_embed(center_ids)
u_pos = self.out_embed(pos_ids)
u_neg = self.out_embed(neg_ids)
pos_score = (v * u_pos).sum(dim=1)
neg_score = torch.bmm(u_neg, v.unsqueeze(2)).squeeze(2)
loss_pos = -F.logsigmoid(pos_score).mean()
loss_neg = -F.logsigmoid(-neg_score).mean()
return loss_pos + loss_neg
def cosine_neighbors(emb, inv, topk=5):
w = F.normalize(emb, dim=1)
sim = w @ w.t()
for i in range(w.size(0)):
vals, idx = torch.topk(sim[i], k=min(topk + 1, w.size(0)))
idx = [j.item() for j in idx if j.item() != i][:topk]
print(inv[i], "->", [inv[j] for j in idx])
if __name__ == "__main__":
torch.manual_seed(0)
random.seed(0)
text = """
我 爱 吃饭 我 爱 米饭
你 爱 吃饭 吗
吃饭 真 香
用餐 真 香
我 喜欢 做饭
""".strip().split()
vocab, inv = build_vocab(text)
pairs = make_pairs(text, window=2)
dim = 32
neg_k = 8
model = SGNS(len(vocab), dim)
opt = torch.optim.Adam(model.parameters(), lr=0.05)
unigram = torch.zeros(len(vocab))
for w in text:
unigram[vocab[w]] += 1
prob = (unigram ** 0.75)
prob = prob / prob.sum()
for step in range(800):
center_w, pos_w = random.choice(pairs)
c = torch.tensor([vocab[center_w]])
p = torch.tensor([vocab[pos_w]])
neg = torch.multinomial(prob, num_samples=neg_k, replacement=True).unsqueeze(0)
loss = model(c, p, neg)
opt.zero_grad()
loss.backward()
opt.step()
emb = model.in_embed.weight.detach().cpu()
cosine_neighbors(emb, inv, topk=4)
你可以把语料改成自己的小文本,观察近邻如何变化。
在这个玩具世界里,“吃饭”“用餐”“香”往往会互相靠近——这就是嵌入在做的事:把“同桌的人”拉到一起。
八、小结:本回解了“认亲”,下回要讲“记事”
这一回,我们解决了上回留下的悬念:
词不再是孤灯,而能在低维空间里结亲缘、分远近。
可新的问题又来了:
词有了向量,句子怎么办?
词袋不看顺序,但语言偏偏讲顺序:
“我 不 爱 你”与“我 爱 你”只差一个“不”,却天差地别。
因此下回我们要请“时序记忆”登场:
让模型不只看“有哪些词”,还要记“词是怎么排的”。
正是:同伴相随词义显,一维一念各生情。
若问长句谁来记,还看循环起潮声。
欲知后事如何,且听下回分解。
引用与溯源
Footnotes
-
Bypassing Skip‑Gram Negative Sampling: Dimension Regularization … arXiv:2405.00172(2024-05)https://arxiv.org/abs/2405.00172 ↩
-
OpenAI. New embedding models and API updates(2024-01-25)https://openai.com/index/new-embedding-models-and-api-updates/ ↩