一篇彻底讲透分层Softmax的手算全流程博客,附带完整数学推导
前言:一个参数引发的"血案"
在训练Word2Vec模型时,你可能会在代码中看到这样的参数设置:
python
model = Word2Vec(sentences, hs=1, negative=0) # 使用分层Softmax
或者在深度学习框架中:
python
loss='hs' # 或者 loss='ns'
你或许会产生一连串疑问:
-
分层Softmax是激活函数吗?还是损失函数?
-
为什么它被归类在
loss参数里? -
它的内部到底是怎么工作的?
-
那个"连乘出来的极小概率"不会导致梯度消失吗?
-
它名字里带着"Softmax",但Softmax激活函数到底在哪一步用了?
本文将从零开始,手算一个完整的分层Softmax训练流程,把所有这些谜题一次性解开。
一、先搞清楚:分层Softmax到底是什么?
1.1 它不是激活函数,也不是损失函数
| 名称 | 是激活函数吗? | 是损失函数吗? | 内部实际用的激活函数 | 底层实际用的损失公式 |
|---|---|---|---|---|
| 分层Softmax | ❌(是一种树结构) | ❌(是计算模式) | Sigmoid(树上每个节点) | 二元交叉熵(路径上累加) |
| 负采样 | ❌(是一种采样法) | ❌(是计算模式) | Sigmoid(二分类) | 二元交叉熵 |
结论 :分层Softmax本质上是一种近似训练算法/策略 ,它通过构建一棵哈夫曼树,将标准Softmax的 O(V) 复杂度降为O(logV)。代码接口之所以叫loss,是因为它决定了"如何计算损失",这是工程上的命名习惯,而非学术上的严格分类。
1.2 标准Softmax在分层Softmax中"消失"了(核心!)
这是绝大多数初学者最迷惑的地方。严格来说,在分层Softmax的整个计算流程中,标准的Softmax激活函数根本就没有被使用 ,它被完全替换掉了。
| 模块 | 标准 Word2Vec | 分层 Softmax(Hierarchical Softmax) |
|---|---|---|
| 输出层激活函数 | Softmax(多分类,算所有词) | 已被整体移除,不存在了 |
| 替代它的结构 | 无 | 哈夫曼树(二叉树结构) |
| 树上节点的激活函数 | 无(因为没有树) | Sigmoid(每个内部节点做二分类) |
| 最终输出概率 | 直接由Softmax算出 | 路径上所有Sigmoid概率连乘 |
那为什么名字里还带着"Softmax"?
因为它在数学功能上"等效近似"了标准Softmax的结果:
-
标准Softmax:一次性算出所有词的概率,且这些概率总和为 1(全局归一化)。
-
分层Softmax :虽然没有算所有词,但哈夫曼树的树形结构天生保证了 "从根节点到所有叶子节点的概率总和等于 1"(局部二分类概率相乘,在树结构上天然归一化)。
它模仿 了Softmax输出概率分布的特性(总和为1),但内部实现完全抛弃了那个复杂的指数求和运算,改用Sigmoid来节省计算量。学术界为了表明它"起到Softmax的作用但做了层次化加速",所以取名"分层Softmax"。
一句话记住:名字里有"Softmax",但代码里没"Softmax",全是"Sigmoid"!
二、准备阶段:构建哈夫曼树
在训练开始前,模型会根据语料库中所有词的词频构建一棵哈夫曼树:
-
叶子节点:代表词汇表中的每一个词(共 V 个)。
-
内部节点:代表一个二分类逻辑回归分类器(共 V−1 个),每个节点有自己的参数向量 θj。
-
编码规则:词频越高的词,距离根节点越近(路径越短);词频越低,路径越长。这极大提升了高频词的计算效率。
-
二进制编码:每个词被赋予唯一的二进制编码(如左子树为0,右子树为1)。
示例 :假设词汇表为 {我, 苹果, 吃, 水果, 好},目标词"水果"的哈夫曼编码为 [1, 0](先走右子树,再走左子树),路径上有2个内部节点。
三、前向传播:如何计算"输出是目标词"的概率?
3.1 核心思想:沿着路径做二分类(注意:用的是Sigmoid,不是Softmax!)
模型通过上下文(或中心词)计算出一个投影向量 ℎh(即词向量)。现在要计算P(目标词∣上下文)。
标准Softmax需要计算V 次,分层Softmax只需要沿着目标词的路径走 L 步(L≈log2V)。
对于路径上的每个内部节点 j (激活函数是Sigmoid,不是Softmax):
-
计算得分:

-
通过Sigmoid激活:
(代表"往右走"的概率) -
"往左走"的概率为

最终概率:路径上所有节点"猜对方向"的概率连乘。
3.2 完整数值案例
让我们手算一遍。
预设参数:
-
模型:Skip-Gram(输入"苹果",预测"水果")
-
词向量维度:d=3
-
目标词"水果"的编码:
[1, 0](先右后左) -
学习率:η=0.1
初始参数:
-
输入词向量("苹果"):ℎ=0.2,−0.1,0.4h=0.2,−0.1,0.4
-
内部节点参数:
-
θ1=0.5,0.3,−0.2(根节点)
-
θ2=−0.1,0.4,0.6(左子节点)
-
第 1 关:根节点 N1(真实编码 d1=1,往右)
-
计算得分:

-
Sigmoid激活(再次强调:这里不是Softmax):

-
这一关"猜对"(往右)的概率:

第 2 关:节点 N2(真实编码 d2=0,往左)
-
计算得分:

-
Sigmoid激活:

-
这一关"猜对"(往左)的概率:

前向最终结果
总概率:
重要澄清 :模型在计算时并没有"做出选择"往左还是往右走,它只是分别计算了每一关"猜对方向"的概率。无论第一关算出的概率是0.1还是0.9,第二关的计算都在真实路径规定的那个节点上进行,不受第一关结果影响。
四、损失函数:从极大似然到最小损失
4.1 极大似然估计的思想
模型算出"水果"的概率是 0.2264。既然"水果"真实出现在语料中,我们就希望这个概率越大越好(最好等于1.0)。
这就是**极大似然估计(MLE)**的核心:让观测到的样本出现的概率最大化。
4.2 为什么要转换成最小损失?
直接最大化 P 面临三个问题:
-
P 是连乘形式(0.4975 × 0.4551),求导不方便。
-
多个小于1的数相乘会得到极小的数值(如 10−610−6),计算机容易下溢。
-
工程师习惯用"损失(Loss)"来度量错误,损失越小代表模型越好。
神级转换:最大化P 等价于最小化 −log(P)。
-
当 P=0.2264 时,−log(0.2264)≈1.485−log(0.2264)≈1.485(损失很大)
-
当 P=0.9 时,−log(0.9)≈0.105−log(0.9)≈0.105(损失很小)
-
当 P=1.0 时,−log(1.0)=0−log(1.0)=0(完美)
且 loglog 能把连乘变成连加,数值稳定。
4.3 计算损失值
总损失 = 两关损失之和:
-
第1关损失:

-
第2关损失:

总损失:
五、反向传播:梯度推导(核心!)
5.1 你的担忧:极小概率不会导致梯度消失吗?
这是很多人最困惑的地方。直觉上,总概率 0.22640.2264 这么小,用它去更新权重,梯度应该微乎其微才对。
但事实是:总概率 0.22640.2264 根本不会出现在梯度公式里!
下面我用链式法则完整推导一遍,你就知道那个"极小概率"是怎么被约掉的。
5.2 对第1关参数θ1 求导

逐项计算:
① 损失对预测概率求导:

② Sigmoid对输入的导数(著名性质 σ′=σ(1−σ)):

③ 得分对参数求导:

三项相乘:

见证奇迹:分子分母的 σ1 直接约掉!

因为真实编码 d1=1,所以:

代入数值:

5.3 对第2关参数 θ2 求导

三项相乘:

因为真实编码 d2=0,所以:

代入数值:

5.4 总结:为什么梯度是σ−d?
| 你的担心 | 数学真相 |
|---|---|
| 总概率 0.2264 太小,梯度会消失 | 总概率从未进入梯度公式。链式求导时, 互相约分,只剩下 σ−d。 |
| 为什么公式这么干净? | 因为 Sigmoid导数 与 对数导数 恰好互为倒数。这就是BP算法高效的原因------复杂的指数运算在求导时被抵消,只剩下简单的线性误差。 |
误差信号 σ−d 永远在 -1 到 1 之间,模型不会因为路径长而梯度消失。
六、更新参数:执行一步优化
6.1 更新内部节点参数
更新θ1:

更新 θ2:

6.2 更新输入词向量 ℎh(即"苹果"的词向量)
将路径上所有节点的误差通过各自的θ 传回给 ℎh:

6.3 验证效果
用新参数重新计算第1关:
-
新得分:

-
新

对比 :旧值σ1=0.4975 → 新值 σ1′=0.5054,更接近真实编码 1 了!
损失会从 1.485 下降到约 1.38,模型在进步。
七、完整闭环:一句话串起整个流程
| 步骤 | 数学操作 | 数值示例 |
|---|---|---|
| 前向传播 | 路径概率连乘(节点激活函数是Sigmoid) | 0.4975×0.4551=0.22640.4975×0.4551=0.2264 |
| 极大似然估计 | 目标:让 P→1.0 | 希望 0.2264 → 1.0 |
| 转最小损失 | L=−log(P) | −log(0.2264)=1.485−log(0.2264)=1.485 |
| 反向求导 | 链式法则 + Sigmoid约分 | δ1=−0.5025,δ2=+0.5449 |
| 更新参数 | 梯度下降 | ℎθ新=θ−η⋅δ⋅h |
| 循环迭代 | 重复上述过程 | 损失持续下降,直至收敛 |
八、终极对比:标准Softmax vs 分层Softmax
| 维度 | 标准 Softmax | 分层 Softmax(HS) |
|---|---|---|
| 输出层是否存在 | 是,全连接层输出 V 个得分 | 否,输出层被整棵哈夫曼树替换 |
| 使用的激活函数 | Softmax(多分类,全局归一化) | Sigmoid(每个内部节点做二分类) |
| 计算复杂度 | O(V) | O(logV) |
| 损失函数形式 | 多分类交叉熵 | 路径上二分类交叉熵之和 |
| 更新参数范围 | 更新所有 V 个输出向量 | 只更新路径上 L 个内部节点的向量 |
| 梯度信号强度 | 依赖全词汇表得分,可能稀疏 | 每关误差 σ−d,始终在 -1~1 之间,稳定 |
| 名字来源 | 数学函数原名 | 功能上等效Softmax(输出归一化概率),但实现完全不同 |
九、常见FAQ
Q1:分层Softmax里到底有没有Softmax激活函数?
A:绝对没有! 它的内部全是Sigmoid。名字里带"Softmax"是因为它起到了"输出归一化概率分布"的作用,而不是因为调用了softmax()这个函数。
Q2:既然没用Softmax,为什么代码参数叫 loss='hs' 而不叫 activation='hs'?
A: 因为分层Softmax是一种训练策略/算法 ,它决定了损失如何计算(路径上的二分类交叉熵之和)。在框架设计者看来,用户选它就是为了"用这种方式计算损失并更新参数",所以归类在loss参数下。
Q3:如果路径有10层,连乘出来的概率极小(如 10−510−5),反向传播时梯度真的不会消失吗?
A:绝对不会! 如第五章推导所示,链式求导时,连乘出来的总概率被彻底约掉了。梯度永远等于(σ−d)⋅h,其中σ−d 在 -1 到 1 之间。路径长短不影响梯度幅值,只影响需要累加多少个这样的误差信号。
后记
本文用一个完整的数值案例,手算了分层Softmax从前向传播 → 损失计算 → 反向求导 → 参数更新 的全过程,并专门澄清了**"Softmax激活函数在分层Softmax中完全消失,被Sigmoid树取代"**这一核心误区。
希望你看完后再遇到 loss='hs' 或模型名称里的"Hierarchical Softmax"时,能够会心一笑------因为你已经知道底层发生的每一个数学细节。


互相约分,只剩下 σ−d。