☰
《动手学深度学习》word2vec 近似训练深度解析:负采样与层次 Softmax 的原理、推导与源码实现
2026/10/1 17:35:31 网站建设 项目流程
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

当词典规模达到数十万甚至上百万词时,word2vec 跳元模型(skip-gram)原始 softmax 目标函数的梯度计算代价随词典大小线性增长,直接训练变得不可承受。本文以《动手学深度学习》(d2l-zh)中 近似训练 一章为主线,系统拆解两种主流近似训练方法——负采样(Negative Sampling)与层次 Softmax(Hierarchical Softmax)——的数学动机、完整公式推导与计算复杂度分析,并结合本仓库 d2l/torch.py 中的数据集构建、噪声词采样与带掩码二元交叉熵损失的源码实现,帮助读者理解「为何要近似」以及「近似如何落地」,最终能够在 PTB 等大规模语料上独立完成 word2vec 的预训练与词向量应用。

从问题出发:全词典 softmax 的梯度灾难

在 词嵌入(word2vec) 一章中,跳元模型(skip-gram)用 softmax 建模给定中心词 $w_c$ 时生成上下文词 $w_o$ 的条件概率:

$$P(w_o \mid w_c) = \frac{\text{exp}(\mathbf{u}_o^\top \mathbf{v}c)}{ \sum{i \in \mathcal{V}} \text{exp}(\mathbf{u}_i^\top \mathbf{v}_c)},$$

其中 $\mathcal{V}$ 为词典(词表)索引集合,每个词同时维护两个 $d$ 维向量:用作中心词的 $\mathbf{v}_i$ 与用作上下文词的 $\mathbf{u}_i$。对应的对数损失为:

$$\log P(w_o \mid w_c) =\mathbf{u}_o^\top \mathbf{v}c - \log\left(\sum{i \in \mathcal{V}} \text{exp}(\mathbf{u}_i^\top \mathbf{v}_c)\right).$$

问题在于:由于上下文词可能是词典中的任意一个词,分母的求和项数等于整个词典大小$|\mathcal{V}|$。对中心词向量 $\mathbf{v}_c$ 求梯度得到:

$$\frac{\partial \text{log}, P(w_o \mid w_c)}{\partial \mathbf{v}_c}= \mathbf{u}o - \sum{j \in \mathcal{V}} P(w_j \mid w_c) \mathbf{u}_j,$$

即梯度计算同样需要对全部词典词的条件概率求和。连续词袋(CBOW)模型的梯度(word2vec.md 中 :eqref:eq_cbow-gradient)同样包含这一求和。当词典包含几十万甚至上百万个词时,每步梯度的计算代价是灾难性的——这就是引入近似训练的根源。

为降低上述计算复杂度,本节引入两种近似训练方法:负采样(negative sampling)与层次 Softmax(hierarchical softmax)。由于跳元模型与连续词袋模型高度相似,下文均以跳元模型为例阐述,两种方法可同样迁移到 CBOW 上。

负采样:用 K 个噪声词近似整个词典

从「正样本事件」到有意义的联合概率

负采样修改了原始目标函数。给定中心词 $w_c$ 的上下文窗口,将「上下文词 $w_o$ 确实来自该窗口」视为一个事件,其概率用 sigmoid 建模:

$$P(D=1\mid w_c, w_o) = \sigma(\mathbf{u}_o^\top \mathbf{v}_c),$$

其中 sigmoid 激活函数定义为:

$$\sigma(x) = \frac{1}{1+\exp(-x)}.$$

若直接最大化文本序列中所有这类事件的联合概率(序列长度 $T$、上下文窗口 $m$):

$$ \prod_{t=1}^{T} \prod_{-m \leq j \leq m,\ j \neq 0} P(D=1\mid w^{(t)}, w^{(t+j)}),$$

会立刻暴露出一个致命缺陷:该目标只包含正样本事件,因此只有当所有词向量都趋于无穷大时联合概率才会收敛到 1——这样的解毫无意义。

引入噪声词:目标函数的改写

为了让目标函数更有意义,负采样从预定义分布 $P(w)$ 中为每个上下文词 $w_o$ 采样 $K$ 个不在该上下文窗口内的噪声词 $w_k$($k=1,\ldots,K$)。设 $S$ 为「$w_o$ 来自 $w_c$ 的上下文窗口」这一事件,$N_k$ 为「噪声词 $w_k$ 不来自该窗口」的事件,并假设 $S, N_1, \ldots, N_K$ 相互独立。于是原联合概率被改写为:

$$ \prod_{t=1}^{T} \prod_{-m \leq j \leq m,\ j \neq 0} P(w^{(t+j)} \mid w^{(t)}),$$

其中条件概率通过事件 $S, N_1, \ldots, N_K$ 近似为:

$$ P(w^{(t+j)} \mid w^{(t)}) =P(D=1\mid w^{(t)}, w^{(t+j)})\prod_{k=1,\ w_k \sim P(w)}^K P(D=0\mid w^{(t)}, w_k).$$

记 $i_t$ 为时间步 $t$ 处中心词 $w^{(t)}$ 的索引,$h_k$ 为噪声词 $w_k$ 的索引,则对应的对数损失为:

$$ \begin{aligned} -\log P(w^{(t+j)} \mid w^{(t)}) =& -\log P(D=1\mid w^{(t)}, w^{(t+j)}) - \sum_{k=1,\ w_k \sim P(w)}^K \log P(D=0\mid w^{(t)}, w_k)\ =&- \log, \sigma\left(\mathbf{u}{i{t+j}}^\top \mathbf{v}{i_t}\right) - \sum{k=1,\ w_k \sim P(w)}^K \log\left(1-\sigma\left(\mathbf{u}{h_k}^\top \mathbf{v}{i_t}\right)\right)\ =&- \log, \sigma\left(\mathbf{u}{i{t+j}}^\top \mathbf{v}{i_t}\right) - \sum{k=1,\ w_k \sim P(w)}^K \log\sigma\left(-\mathbf{u}{h_k}^\top \mathbf{v}{i_t}\right), \end{aligned} $$

其中最后一步利用了 $\sigma(-x)=1-\sigma(x)$ 的恒等变换,把「正样本的 sigmoid 分数」与「K 个噪声样本的负 sigmoid 分数」统一成标准的二元分类形式。

复杂度:与词典解耦,随 K 线性增长

从上式可以清楚看到:每一步梯度计算的代价与词典大小 $|\mathcal{V}|$ 无关,而只与噪声词数量 $K$ 线性相关。超参数 $K$ 取值越小,每步梯度计算开销越小;同时,负例数量也直接决定了模型区分「真实上下文」与「随机噪声」的强度,是负采样训练中最关键的超参数之一(仓库默认取 $K=5$,见下文)。

源码佐证:d2l 中噪声词的采样实现

仓库 d2l/torch.py 中,噪声词的采样逻辑由get_negatives实现,核心代码如下(d2l/torch.py#L2099-L2115):

def get_negatives(all_contexts, vocab, counter, K): # 索引为1、2、...(索引0是词表中排除的未知标记) sampling_weights = [counter[vocab.to_tokens(i)]**0.75 for i in range(1, len(vocab))] all_negatives, generator = [], RandomGenerator(sampling_weights) for contexts in all_contexts: negatives = [] while len(negatives) < len(contexts) * K: neg = generator.draw() # 噪声词不能是上下文词 if neg not in contexts: negatives.append(neg) all_negatives.append(negatives) return all_negatives

可以验证文档中「从预定义分布 $P(w)$ 采样」这一抽象的工程实现要点:

  • 采样分布为词频的 0.75 次幂:sampling_weights = counter[...] ** 0.75。这是 word2vec 论文中提出的经典经验设置,对词频做了平滑,避免高频词(如 "the")被过度采样为噪声;
  • 噪声词排除上下文词:if neg not in contexts保证采样出的噪声词确实不在当前上下文窗口中,对应文档中「不来自该窗口」的 $N_k$ 事件定义;
  • 高效缓存采样:RandomGenerator.draw()(d2l/torch.py#L2077-L2094)底层使用random.choices(population, sampling_weights, k=10000)一次性批量缓存 1 万个采样结果,之后逐个消费,避免对每个噪声词都做一次完整加权抽样,是面向大规模语料的性能优化。

此外,训练数据构建链路load_data_ptb(d2l/torch.py#L2133-L2166)会依次执行:read_ptb读取 PTB 语料 →subsample按概率 $\sqrt{10^{-4} \cdot \text{num_tokens} / \text{counter}[w]}$ 对高频词随机下采样(见 d2l/torch.py#L2040-L2056)→get_centers_and_contexts为每个中心词生成窗口大小为随机整数(1 到max_window_size)的上下文词列表 →get_negatives为每个中心词生成噪声词。

训练落地:带掩码的二元交叉熵损失

负采样的损失天然对应二元交叉熵。在 预训练 word2vec 一章中,PyTorch 版本通过SigmoidBCELoss实现带掩码的二元交叉熵(word2vec-pretraining.md):

class SigmoidBCELoss(nn.Module): # 带掩码的二元交叉熵损失 def forward(self, inputs, target, mask=None): out = nn.functional.binary_cross_entropy_with_logits( inputs, target, weight=mask, reduction="none") return out.mean(dim=1)

其中inputs是skip_gram前向传播输出的点积分数,target为标签(正样本为 1、噪声样本为 0),mask用于屏蔽因**变长上下文 + 填充(padding)**引入的无意义位置。batchify(d2l/torch.py#L2117-L2131)把每个样本的上下文词与噪声词拼接到同一序列,并用 0 填充至批量内最大长度,同时生成masks(有效位为 1)与labels(上下文词位为 1、噪声词位为 0),由此可在一次前向中同时处理正负样本。训练时还按mask.shape[1] / mask.sum(axis=1)做规范化,使损失仅在被掩码的有效位置上求平均。

在 d2l/torch.py 的load_data_ptb默认配置(batch_size=512, max_window_size=5, num_noise_words=5)下,配合 Adam 优化器(学习率 0.002、5 个 epoch),即可在 PTB 数据集上完成跳元模型的负采样预训练;训练结束后可用get_similar_tokens依据中心词向量矩阵的余弦相似度检索语义相近词,例如以'chip'为查询词返回最相似的若干词元。

层次 Softmax:用二叉树把求和化为路径乘积

数据结构:叶子为词的二叉树

作为另一种近似训练方法,层次 Softmax 使用二叉树数据结构:每个叶子节点代表词典 $\mathcal{V}$ 中的一个词。训练前需依据词频构建 Huffman 树,高频词更靠近根节点,从而缩短其路径长度。下图展示了层次 Softmax 的基本结构:

记 $L(w)$ 为二叉树上从根节点到代表词 $w$ 的叶子节点的路径节点数(含两端),$n(w,j)$ 为该路径上的第 $j$ 个节点,其上下文词向量为 $\mathbf{u}_{n(w, j)}$。例如上图中 $L(w_3) = 4$。

概率建模:沿路径的 sigmoid 连乘

层次 Softmax 将跳元模型的条件概率近似为沿根到叶子路径上各非叶子节点sigmoid 的连乘:

$$P(w_o \mid w_c) = \prod_{j=1}^{L(w_o)-1} \sigma\left( [![ n(w_o, j+1) = \text{leftChild}(n(w_o, j)) ]!] \cdot \mathbf{u}_{n(w_o, j)}^\top \mathbf{v}_c\right),$$

其中 $\text{leftChild}(n)$ 表示节点 $n$ 的左子节点;若 $x$ 为真则 $[![x]!]=1$,否则 $[![x]!]=-1$。直观理解:每一步都做一次「向左走还是向右走」的二元决策,向左为 sigmoid 正分数,向右则取负号,整条路径的概率是各步决策概率的连乘。

以图例中的 $w_3$ 为例,从根到 $w_3$ 的路径依次为左、右、左,需要计算中心词向量 $\mathbf{v}_c$ 与路径上 3 个非叶子节点向量的点积:

$$P(w_3 \mid w_c) = \sigma(\mathbf{u}_{n(w_3, 1)}^\top \mathbf{v}c) \cdot \sigma(-\mathbf{u}{n(w_3, 2)}^\top \mathbf{v}c) \cdot \sigma(\mathbf{u}{n(w_3, 3)}^\top \mathbf{v}_c).$$

关键性质:概率分布自动归一化

层次 Softmax 最重要的理论性质是归一化。由于 $\sigma(x)+\sigma(-x) = 1$,对任意中心词 $w_c$,词典中所有词的近似条件概率之和恒为 1:

$$\sum_{w \in \mathcal{V}} P(w \mid w_c) = 1.$$

这意味着一棵二叉树的每个内部节点都以「左子树概率 + 右子树概率 = 1」的方式对概率质量做了递归划分,整个词典的概率分布天然合法,无需像原始 softmax 那样显式计算归一化常数——这正是省掉全词典求和的关键。

复杂度:从 $|\mathcal{V}|$ 降到 $\mathcal{O}(\log_2|\mathcal{V}|)$

由于二叉树结构,$L(w_o)-1$ 的量级为 $\mathcal{O}(\log_2|\mathcal{V}|)$。当词典规模巨大时,层次 Softmax 每步训练的计算代价相比原始 softmax(全词典求和)显著降低:只需沿一条从根到叶子的路径做约 $\log_2|\mathcal{V}|$ 次向量点积与 sigmoid 运算,而无需遍历整个词典。

两种方法对比与选型小结

维度负采样(Negative Sampling)层次 Softmax(Hierarchical Softmax)
核心思想用 $K$ 个负例 + 1 个正例构造二元分类目标用根到叶子的二叉树路径乘积近似条件概率
每步计算复杂度与词典大小无关,线性依赖噪声词数 $K$依赖词典大小的对数$\mathcal{O}(\log_2\mathcal{V})$
概率分布无需显式归一化,通过正负样本对抗拟合由 $\sigma(x)+\sigma(-x)=1$ 保证自动归一化
采样需求需要按预定义分布 $P(w)$ 采样噪声词(如词频 0.75 次幂)需要按词频构建二叉树(如 Huffman 树)
典型实践d2l 预训练章节采用,$K=5$,配合带掩码二元交叉熵适合词典极大且需完整概率分布的场景

小结:负采样通过构造包含正例与负例的相互独立事件来构建损失函数,每步训练计算代价随噪声词数 $K$ 线性增长;层次 Softmax 则利用二叉树根节点到叶子节点的路径构建损失函数,每步训练计算代价随词典大小的对数增长。两者都以「用少量运算近似全词典求和」为目标,是 word2vec 从玩具模型走向大规模语料预训练的关键技术。

延伸练习与思考

  1. 噪声词如何采样?在负采样中,噪声词从预定义分布 $P(w)$ 中抽取。工程上(见 d2l/torch.py 的get_negatives)采用词频的 0.75 次幂作为采样权重,并通过random.choices批量缓存采样结果以提高效率;同时噪声词必须排除当前上下文窗口中的真实上下文词。思考:为什么要对词频取 0.75 次幂而不是直接用词频?这与避免高频词主导负例有关。
  2. 验证归一化恒等式:利用 $\sigma(x)+\sigma(-x)=1$ 与二叉树的递归划分结构,可以归纳证明对任意 $w_c$,$\sum_{w \in \mathcal{V}} P(w \mid w_c) = 1$ 恒成立——这正是层次 Softmax 省去显式归一化常数的数学依据。
  3. 迁移到 CBOW:负采样与层次 Softmax 均可用于训练连续词袋模型。区别仅在于把「给定中心词预测上下文词」的方向反过来:CBOW 用上下文词向量的平均值 $\bar{\mathbf{v}}o = (\mathbf{v}{o_1} + \cdots + \mathbf{v}{o{2m}})/(2m)$ 与中心词向量做点积(见 word2vec.md),正负样本的构造、损失函数形式与复杂度分析完全同理;且 CBOW 训练后通常使用上下文词向量作为词表示,与跳元模型取中心词向量恰好相反。

在仓库中继续深入

  • 近似训练的理论推导原文:chapter_natural-language-processing-pretraining/approx-training_origin.md
  • 跳元模型与 CBOW 的完整数学定义:chapter_natural-language-processing-pretraining/word2vec.md
  • 基于负采样的完整预训练实现(嵌入层、skip-gram 前向、损失、训练循环、相似词检索):chapter_natural-language-processing-pretraining/word2vec-pretraining.md
  • 数据构建与采样源码:d2l/torch.py(read_ptb、subsample、get_centers_and_contexts、get_negatives、batchify、load_data_ptb等函数均可直接查阅),MXNet、TensorFlow、PaddlePaddle 版本位于 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

相关推荐

上一篇:AList性能优化实战:3步让你的大文件传输速度提升10倍!🚀
下一篇:7个实用技巧:让Google Drive CLI Client与终端多窗口高效集成,实现并行操作提速

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询