计算机收到“这部电影很耐看”时,最先看到的不是“耐看”背后的评价,而是一串离散符号。我们可以给每个词分配一个编号,却不能指望编号 38 比编号 12 多出某种语义。词嵌入要解决的就是这道接口问题:把离散的词或子词映射成可训练的稠密向量,让后续模型能从数据中利用它们之间的统计关系。

这一章会把经典静态词嵌入讲完整。我们从 one-hot 和查表操作出发,拆开 CBOW、Skip-gram、负采样与 GloVe 的目标,再讨论相似度、类比、子词、预训练、PyTorch 工程细节和情感分类。最后还要认真处理两个边界:词向量测到的是语料中的关联,不是词典定义;一个向量在相似度题上表现不错,也不保证下游系统准确或公平。
本章讨论的 Word2Vec、GloVe 和 fastText 主要产生静态词向量:同一个词在不同句子中使用同一条向量。现代上下文化语言模型会根据整句生成不同表示,但词表索引、嵌入查表、padding、预训练与评估这些基础问题仍然存在。
设词表大小为 。最直接的编码是给第 个词一条 维 one-hot 向量 :第 个位置为 1,其余位置为 0。它有一个优点——身份绝不会混淆;问题也同样直接——它只记录“是谁”,没有记录“像谁”。
任意两个不同词的 one-hot 内积都是 0,欧氏距离都是 。于是“猫”和“狗”的距离,与“猫”和“发票”的距离完全相同。词表有 50 万个词时,每条向量还要占 50 万维;即便用稀疏结构避免真的存满零,后续模型仍然需要为每个词学习独立参数。
词嵌入把词表写成一个矩阵:
其中 通常远小于 。输入词的编号是 ,查表得到第 行:
如果坚持写成 one-hot 乘法,同一个操作是 。实现不会真的构造 one-hot 再做矩阵乘法,而是直接索引行。一个形状为 (batch, length) 的整数张量,经过嵌入层后会变成 (batch, length, dim)。

这里有两个常见误解。第一,稠密向量的每一维通常没有稳定的人工标签。我们可以观察某些方向与性别、时态或地域相关,但不应把第 17 维直接命名为“是否是动物”。第二,低维不等于无损压缩。嵌入保留的是训练目标认为有用的统计结构,没被目标奖励的信息可能消失。
词向量为何能出现关系?经典回答是分布假设:在相似上下文中出现的词,往往具有相近用法。模型并没有先读词典,而是不断接收“谁与谁在附近出现”这样的训练信号,把能完成预测的词摆到合适的位置。
Word2Vec 原始工作提出了两种高效架构。我们先拿句子“这个 镜头 真的 很 有张力”做例子,窗口半径设为 2。中心词是“真的”时,窗口里的上下文是“这个、镜头、很、有张力”。窗口决定了模型能看到多远,但它通常不保留这些词的精确顺序。
CBOW 用上下文预测中心词。它把若干上下文向量求和或平均,再预测中间缺失的词:
Skip-gram 方向相反:给定中心词,分别预测窗口中的上下文词。若中心词为 ,窗口半径为 ,一个基本目标是:

两种方法都会维护输入向量和输出向量。中心词查输入矩阵,待预测词使用输出矩阵打分。训练完成后,人们常取输入向量,也可能尝试输入、输出向量的和或平均;这不是一条跨实现都固定的规则,评估时必须记录你实际导出了哪一份。
CBOW 每个窗口形成一次聚合预测,训练通常更省;Skip-gram 把一个中心词展开成多个中心—上下文词对,对罕见词可能获得更直接的信号,但训练样本也更多。真正影响结果的不只架构名,还包括窗口宽度、最低词频、采样方式、维度、语料规模和分词规则。
窗口也改变“相似”的含义。窄窗口更容易抓到句法或局部替换关系,宽窗口会引入较多主题共现。比如“咖啡”和“茶”可能在相似位置出现,也可能“咖啡”和“杯子”总在同一话题中共现;词向量把相似与相关混在同一个几何空间里。
上一节的 softmax 分母要对 个词打分。词表很大时,每遇到一个训练对都扫描全词表,开销过高。Mikolov 等人在负采样论文中给出一种实用替代:真实中心—上下文词对记为正样本,再抽取 个噪声词作为负样本,让模型完成二分类。
设中心词输入向量为 ,真实上下文的输出向量为 ,负样本为 ,单个训练对的损失可以写成:
第一项把真实词对的点积推高,第二项把采到的噪声词点积压低。一次更新只触碰中心词、正样本和少量负样本的向量,避免完整 softmax。它训练的是“数据词对与噪声词对能否区分”的目标,不是每一步都显式得到一个对整个词表严格归一化的概率分布。

负样本也不能随便抽。原论文使用按词频 次幂重整后的噪声分布:
与直接按词频抽样相比, 次幂削弱了超高频词的统治程度,同时仍让常见词比极罕见词更容易被采到。 越大,每个正样本看到的反例越多,计算也越贵;它不是越大越好,需要结合语料和吞吐量验证。
原工作还对高频词做下采样。像英文 the 或中文高频功能词会制造大量可预测但信息有限的训练对。按频率概率丢弃一部分,可以降低开销并改变训练信号分布。请注意,负采样是“为正样本配噪声词”,高频词下采样是“决定语料中的某个词是否保留”,两者不是同一个操作。
import torch
import torch.nn.functional as F
def negative_sampling_loss(center, positive, negatives):
"""
center: (batch, dim)
positive: (batch, dim)
negatives: (batch, k, dim)
"""
positive_logit = (center * positive).sum(dim=-1)
negative_logits = torch.bmm(
negatives, center.unsqueeze(-1)
).squeeze(-1)
positive_loss = -
Word2Vec 反复从局部窗口抽训练事件。GloVe 则先把整个语料汇总成词—词共现矩阵 ,其中 表示词 在词 上下文中出现的加权次数。它并不只是要求“共现多的词向量更近”,而是用向量点积去拟合对数共现量。
GloVe 原论文的加权最小二乘目标是:
与 是中心和上下文两套向量, 与 是偏置。常用权重函数为:
太稀少的共现可能只是偶然,若完全等权会带来噪声;超高频共现也不该无限放大,所以 在低频区平滑增长、达到阈值后封顶。训练通常只遍历 的条目,而不是把巨大的稀疏矩阵逐格扫完。

GloVe 的推导还强调共现概率之比。例如两个词 与探针词 的关系,可以由 显露:如果 只与 相关,比值会很大;只与 相关则很小;与二者都相关或都无关时更接近 1。向量差因而可能表达某些关系方向。
Word2Vec 与 GloVe 不能简单概括成“局部方法对全局方法,前者不看全局”。Word2Vec 的许多局部样本同样来自整份语料,长期训练后也吸收全局统计;区别主要在训练目标和统计量如何进入优化。选型时应该在相同语料、分词、维度和下游任务上比较,而不是只凭算法名称。
词向量常用余弦相似度比较方向:
它忽略向量长度,关注夹角。若要找近邻,可以先把所有向量归一化,再用矩阵乘法一次得到余弦分数。真实系统还应检查零向量、排除查询词自身,并在大词表中使用近似最近邻索引控制延迟。
经典类比使用 b - a + c,寻找与目标向量最接近的候选 :
这种运算有时能找出时态、比较级、国家—首都等关系,但答案依赖语料、词频、算法、分词和评测候选集。它不是模型“理解了逻辑”的证明,更不是每组概念都服从直线平移。

静态词向量尤其有三条边界:
词向量评估研究显示,不同评估方法可能给嵌入模型排出不同顺序。词相似度、类比准确率属于内在评估,便宜而直观;文本分类、检索或序列标注属于外在评估,更贴近用途。没有一项分数能单独宣判某套嵌入“最好”。
固定词表总会遇到 OOV(out-of-vocabulary)词。最粗的做法是把所有未知词映射到同一个 UNK 向量,这能让程序继续运行,却让“轻科幻”“超耐磨”和一串输入错误共享完全相同的表示。
fastText 的子词词向量论文沿用 Skip-gram,把一个词表示成字符 -gram 向量之和。加入词边界符后,英文 where 可以产生类似 <wh、whe、her、ere、re> 的片段。若一个词的子词集合为 ,打分可写成:
是字符片段向量, 是上下文词向量。训练时,形态相近的词会共享部分参数;推理时,即使整词没在词表中,只要能拆出已学习或哈希到桶中的子词片段,也能组合出向量。

这不等于“从未见过的词一定能理解”。拼写相似可能没有语义关系,随机字符串也能被拆出片段;哈希桶还会发生碰撞。子词方法缓解了形态变化、罕见词和拼写变体的问题,但最终效果仍受语料和任务约束。
中文要多想一步。中文文本通常没有天然空格,先用分词器切词会决定 Word2Vec/GloVe 的词表边界。字符 -gram 可以复用字形序列,却不等同于语言学上的词素;如果产品中有大量新品牌、型号、网络词,常见方案是同时评估字粒度、词粒度和现代子词分词。训练、验证、线上必须使用同一个 tokenizer 和词表版本。
词表也不能从全部数据提前构造。若把测试集中的词频用于决定哪些词进表,就把测试分布泄漏进了训练流程。正确顺序是先按样本或时间切分数据,再只用训练集拟合 tokenizer、词表和频率阈值,验证集与测试集只做转换。
PyTorch 的 nn.Embedding 是固定大小词典的查表层。num_embeddings 是词表行数,embedding_dim 是每行维度;输入必须是整数索引,输出在末尾增加嵌入维。
import torch
from torch import nn
PAD_ID = 0
VOCAB_SIZE = 30_000
DIM = 200
embedding = nn.Embedding(
num_embeddings=VOCAB_SIZE,
embedding_dim=DIM,
padding_idx=PAD_ID,
)
token_ids = torch.tensor([
[18, 72,
padding_idx=0 有两层实际含义。新建层时,该行默认是零向量;反向传播时,这一行不贡献梯度,因此不会被普通训练更新。但 padding 仍然出现在输出张量中。若你直接对序列维求平均,短句会被更多零向量稀释,所以池化时仍要使用 mask。
def masked_mean(x, token_ids, pad_id=0):
# x: (batch, length, dim)
mask = token_ids.ne(pad_id).unsqueeze(-1) # (batch, length, 1)
total = (x * mask).sum(dim=1)
count = mask.sum(dim=1).clamp_min(1)
return total / count若从预训练矩阵初始化,先确认 PAD 行真的为零,再传入 from_pretrained。freeze=True 会冻结整张嵌入表,而 padding_idx 只固定指定行,两者不能混为一谈。
weights = torch.tensor(pretrained_matrix, dtype=torch.float32)
weights[PAD_ID].zero_()
embedding = nn.Embedding.from_pretrained(
weights,
freeze=False,
padding_idx=PAD_ID,
)sparse=True 表示嵌入权重的梯度使用稀疏张量,只记录本批实际访问的行;它不表示输出词向量是稀疏的。官方文档还提醒,支持稀疏梯度的优化器有限,例如 optim.SGD、optim.SparseAdam,以及 CPU 上的 optim.Adagrad。若分类头是普通稠密参数,常见做法是为嵌入表和其他参数使用各自兼容的优化器。
预训练词向量把大规模无标注语料中学到的统计关系迁移到小任务中。流程看似只是把矩阵复制进 Embedding.weight,真正容易出错的是行对齐:预训练文件有自己的词顺序,你的词表也有自己的编号,必须逐词匹配,不能假设两边第 500 行表示同一个词。
对每个任务词,可以采用三种初始化:找到预训练词就复制;PAD 明确置零;未找到的词用小范围随机值初始化。随后统计覆盖率时,最好同时报告“词表类型覆盖率”和“按实际 token 频次加权的覆盖率”。丢了很多只出现一次的长尾词,与丢了一个占输入 8% 的高频领域词,风险完全不同。
是否冻结可以按数据和域差异分阶段决定:
微调不是免费升级。它可能改善目标分类,却破坏原本的通用近邻结构;低频词更新不稳定,较大学习率还会造成灾难性漂移。工程上应给嵌入层使用更小学习率、记录冻结状态,并比较随机初始化、冻结预训练、微调预训练三组基线。
下面的分组学习率示例假设嵌入层使用默认的 sparse=False;若启用稀疏梯度,应改用兼容的独立优化器。
optimizer = torch.optim.AdamW([
{"params": model.embedding.parameters(), "lr": 2e-5},
{"params": model.classifier.parameters(), "lr": 2e-4},
])还要检查许可、来源与版本。不同下载包可能训练自不同语料、大小写规则和分词方式;“300 维 GloVe”不是足以复现实验的描述。至少记录文件校验值、语料版本、维度、词表大小、预处理方式和未知词策略。
词嵌入不是完整的文本分类器,它只把 token 变成向量。一个可靠的情感分类基线还需要数据切分、分词、词表、序列聚合、分类头、损失和评估。最小路线可以按下面的顺序搭起来:
PAD、UNK 编号。
平均池化是很有价值的基线:它忽略词序,却能迅速暴露数据和词表问题。若复杂模型只比它高一点,先检查数据泄漏、截断长度和标签噪声,不要立刻继续堆层。
import torch
from torch import nn
class MeanEmbeddingClassifier(nn.Module):
def __init__(self, vocab_size, dim, num_classes, pad_id=0):
super().__init__()
self.pad_id = pad_id
self.embedding = nn.Embedding(
vocab_size, dim, padding_idx=pad_id
)
self.dropout =
评估指标要跟业务错误对应。类别相对均衡时可以先看 accuracy;类别不平衡时补充每类 precision、recall、F1 和混淆矩阵。若输出分数要触发人工审核,还要单独选阈值并检查校准。不要用测试集反复试阈值,也不要只报一个总体准确率。
误差分析至少按四组抽样:否定与转折、反讽或隐含态度、领域新词与拼写变体、超长文本截断。静态词向量在“本来很期待,结果……”这类依赖词序的表达上可能吃亏;这时升级序列模型才有明确理由。
词嵌入从共现学习,语料中的职业、性别、地域和族群关联也会进入几何空间。Caliskan 等人的研究用词嵌入关联测试展示了语料可恢复出人类式偏差;Bolukbasi 等人的工作则分析了性别方向与刻板类比。
我们要避免两个相反的误判。其一,近邻或类比出现刻板关系,不代表这种关系是正确知识;它可能只是历史文本的分布痕迹。其二,把某个定义下的“偏差方向”投影掉,也不代表系统已经公平。Gonen 与 Goldberg发现,某些去偏方法降低了表面指标后,原有群体结构仍可能从邻域中恢复。

实际评估可以分三层:
偏差审计必须围绕明确用途设计。情感分析中的身份词可能被错误当成负面线索,招聘分类中的职业词风险又不同。我们需要保留语料说明、预训练向量来源、模型版本和切片指标,任何去偏处理都同时复测效用与公平性,防止“某个探针分数下降、下游伤害却没降”的假安全感。
最后给出一套可执行检查表:确认切分在词表之前;确认 tokenizer 版本一致;记录 PAD/UNK 与覆盖率;比较随机、冻结、微调基线;同时做内在与下游评估;按目标人群切片;上线后监控 OOV、分布漂移与误伤案例。词嵌入的价值不在于把语言压成一张漂亮的二维图,而在于它能否在明确约束下稳定服务任务。