- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
当词典规模达到数十万甚至上百万词时,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 从玩具模型走向大规模语料预训练的关键技术。
延伸练习与思考
- 噪声词如何采样?在负采样中,噪声词从预定义分布 $P(w)$ 中抽取。工程上(见 d2l/torch.py 的
get_negatives)采用词频的 0.75 次幂作为采样权重,并通过random.choices批量缓存采样结果以提高效率;同时噪声词必须排除当前上下文窗口中的真实上下文词。思考:为什么要对词频取 0.75 次幂而不是直接用词频?这与避免高频词主导负例有关。 - 验证归一化恒等式:利用 $\sigma(x)+\sigma(-x)=1$ 与二叉树的递归划分结构,可以归纳证明对任意 $w_c$,$\sum_{w \in \mathcal{V}} P(w \mid w_c) = 1$ 恒成立——这正是层次 Softmax 省去显式归一化常数的数学依据。
- 迁移到 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多所大学用于教学。
相关推荐
react-native-worklets Bundle Mode 补丁应用指南:为 metro 与 metro-runtime 打补丁的完整实操
react native worklets Bundle Mode 补丁应用指南:为 metro 与 metro runtime 打补丁的完整实操 导读 rea
人工智能深度学习机器学习教程如何快速上手awesome-c:C开发者必备工具集合详解
如何快速上手awesome c:C开发者必备工具集合详解 awesome c是一个精心策划的C语言优质资源列表,专为C开发者打造的一站式工具集合。它包含了从构建
Aptos Move 动态调度引擎:基于 AIP-73 dispatchable_fungible_asset 框架的通用回调调度实现(aptos-core dispatching 示例)
Aptos Move 动态调度引擎:基于 AIP 73 dispatchable_fungible_asset 框架的通用回调调度实现(aptos core d
人工智能深度学习机器学习教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考