我花了一周搞懂Softmax,发现90%的教程都漏了这一点
文章目录
上周帮学弟调一个三分类模型,死活跑不通。看了半天,发现他把Softmax输出当概率直接用了,但损失函数写的却是MSE。这个坑我踩过,今天想聊聊多分类问题到底该怎么玩。
一句话总结
Softmax负责把模型的"打分"转成概率,交叉熵负责告诉模型"你错在哪"。两者组合后,反向传播梯度简化为预测概率减去真实标签,这是深度学习分类任务的核心基础。
背景:从二分类到多分类
之前写过Sigmoid的推导,那是二分类的情况。但现实中的分类问题往往是多类的:手写数字识别(10类)、ImageNet(1000类)、文本情感分析(3-5类)。
问题来了:Sigmoid只能输出一个概率值,多分类怎么办?
最直观的想法是"每个类别单独训一个二分类器"。但这样做有两个问题:
- 类别之间是互斥的。一张图片不可能同时是"猫"和"狗",但独立的二分类器不会考虑这一点。
- 输出不统一。每个二分类器输出自己的概率,加起来不等于1,没法直接比较。
Softmax就是来解决这个问题的。
Softmax:把打分变成概率
模型在干什么
神经网络最后一层通常输出一个向量:
z = [ z 1 z 2 ⋯ z K ] T \mathbf z = \begin{bmatrix}z_1 & z_2 & \cdots & z_K\end{bmatrix}^T z=[z1z2⋯zK]T
这里的 z k z_k zk 是模型对第 k k k 个类别的原始打分,也叫logit。它可以正、可以负,也不要求加起来等于什么。
但概率必须满足两个条件:非负、加起来等于1。Softmax就是做这个转换的。
定义
p k = e z k ∑ j = 1 K e z j p_k = \frac{e^{z_k}}{\sum_{j=1}^{K}e^{z_j}} pk=∑j=1Kezjezk
用人话说: 对每个打分取指数(保证正数),再除以所有指数之和(保证加起来等于1)。
举个例子:
打分: [2.0, 1.0, 0.1]
指数: [7.39, 2.72, 1.11]
Softmax: [0.66, 0.24, 0.10]
模型最看好第1类(概率66%),但也不完全排除其他类。这就是Softmax的"软化"效果。
为什么要用指数函数
两个原因:
第一,保证输出为正。 指数函数 e x > 0 e^x > 0 ex>0 恒成立,这是概率的基本要求。
第二,放大差距。 指数会把大的值变得更大。比如打分2.0和1.0,指数后变成7.39和2.72,差距从1倍拉到了2.7倍。这让模型更"自信"地选择高分类别。
但这个放大也带来了数值问题,后文会讲怎么解决。
多分类Logistic回归
Softmax配合线性模型,就是多分类Logistic回归:
P ( y = k ∣ x ) = e w k T x + b k ∑ j = 1 K e w j T x + b j P(y=k|\mathbf x)=\frac{e^{\mathbf w_k^T\mathbf x+b_k}}{\sum_{j=1}^{K}e^{\mathbf w_j^T\mathbf x+b_j}} P(y=k∣x)=∑j=1KewjTx+bjewkTx+bk
写成矩阵形式更简洁:
z = W x + b , p = Softmax ( z ) \mathbf z = W\mathbf x + \mathbf b, \quad \mathbf p = \text{Softmax}(\mathbf z) z=Wx+b,p=Softmax(z)
这里 W W W 是 K × d K \times d K×d 的权重矩阵,每一行对应一个类别的参数。
关键理解: 多分类Logistic回归不是"一个二分类器复制K份",而是所有类别共享同一个特征空间,通过不同的权重向量来区分。
交叉熵损失:模型错在哪
从极大似然出发
给定训练集 { ( x i , y i ) } i = 1 m \{ (\mathbf x_i, \mathbf y_i) \}_{i=1}^m {(xi,yi)}i=1m,其中 y i \mathbf y_i yi 是one-hot标签(真实类别为1,其余为0)。
模型对第 i i i 个样本的预测概率为 p i \mathbf p_i pi。由于one-hot的特性,真实类别的概率可以写成:
P ( y i ∣ x i ) = ∏ k = 1 K p i k y i k P(\mathbf y_i|\mathbf x_i) = \prod_{k=1}^{K} p_{ik}^{y_{ik}} P(yi∣xi)=k=1∏Kpikyik
整个数据集的似然:
L = ∏ i = 1 m ∏ k = 1 K p i k y i k L = \prod_{i=1}^{m} \prod_{k=1}^{K} p_{ik}^{y_{ik}} L=i=1∏mk=1∏Kpikyik
取对数:
ln L = ∑ i = 1 m ∑ k = 1 K y i k ln p i k \ln L = \sum_{i=1}^{m} \sum_{k=1}^{K} y_{ik} \ln p_{ik} lnL=i=1∑mk=1∑Kyiklnpik
最大化似然等价于最小化负对数似然,定义损失函数:
J = − 1 m ∑ i = 1 m ∑ k = 1 K y i k ln p i k J = -\frac{1}{m} \sum_{i=1}^{m} \sum_{k=1}^{K} y_{ik} \ln p_{ik} J=−m1i=1∑mk=1∑Kyiklnpik
这就是交叉熵损失。
单样本的交叉熵
对于单个样本:
ℓ = − ∑ k = 1 K y k ln p k \ell = -\sum_{k=1}^{K} y_k \ln p_k ℓ=−k=1∑Kyklnpk
由于one-hot中只有真实类别 c c c 的 y c = 1 y_c=1 yc=1,所以:
ℓ = − ln p c \ell = -\ln p_c ℓ=−lnpc
用人话说: 交叉熵只关心模型给真实类别打了多少分。预测概率越接近1,损失越接近0;越接近0,损失趋向无穷大。
这就是为什么交叉熵比MSE更适合分类:当模型预测错误时,交叉熵的梯度不会消失,而MSE会。
梯度推导:为什么Softmax+交叉熵的组合这么漂亮
这是本文最核心的部分,也是很多教程讲得不清不楚的地方。
Step 1:Softmax的导数
Softmax的第 i i i 个输出:
p i = e z i S , S = ∑ j = 1 K e z j p_i = \frac{e^{z_i}}{S}, \quad S = \sum_{j=1}^{K} e^{z_j} pi=Sezi,S=j=1∑Kezj
对 z l z_l zl 求导,分两种情况:
当 i = l i = l i=l 时:
∂ p i ∂ z i = p i ( 1 − p i ) \frac{\partial p_i}{\partial z_i} = p_i(1-p_i) ∂zi∂pi=pi(1−pi)
当 i ≠ l i \neq l i=l 时:
∂ p i ∂ z l = − p i p l \frac{\partial p_i}{\partial z_l} = -p_i p_l ∂zl∂pi=−pipl
统一写成:
∂ p i ∂ z l = p i ( δ i l − p l ) \frac{\partial p_i}{\partial z_l} = p_i(\delta_{il} - p_l) ∂zl∂pi=pi(δil−pl)
其中 δ i l \delta_{il} δil 是Kronecker delta。
Step 2:交叉熵对logits的梯度
单样本交叉熵:
ℓ = − ∑ k = 1 K y k ln p k \ell = -\sum_{k=1}^{K} y_k \ln p_k ℓ=−k=1∑Kyklnpk
对 z l z_l zl 求导,用链式法则:
∂ ℓ ∂ z l = ∑ k = 1 K ∂ ℓ ∂ p k ∂ p k ∂ z l = ∑ k = 1 K ( − y k p k ) p k ( δ k l − p l ) \frac{\partial \ell}{\partial z_l} = \sum_{k=1}^{K} \frac{\partial \ell}{\partial p_k} \frac{\partial p_k}{\partial z_l} = \sum_{k=1}^{K} \left(-\frac{y_k}{p_k}\right) p_k(\delta_{kl} - p_l) ∂zl∂ℓ=k=1∑K∂pk∂ℓ∂zl∂pk=k=1∑K(−pkyk)pk(δkl−pl)
化简:
∂ ℓ ∂ z l = − ∑ k = 1 K y k ( δ k l − p l ) = − y l + p l ∑ k = 1 K y k = p l − y l \frac{\partial \ell}{\partial z_l} = -\sum_{k=1}^{K} y_k(\delta_{kl} - p_l) = -y_l + p_l\sum_{k=1}^{K} y_k = p_l - y_l ∂zl∂ℓ=−k=1∑Kyk(δkl−pl)=−yl+plk=1∑Kyk=pl−yl
因为 ∑ k = 1 K y k = 1 \sum_{k=1}^{K} y_k = 1 ∑k=1Kyk=1。
最终结果
∂ ℓ ∂ z = p − y \frac{\partial \ell}{\partial \mathbf z} = \mathbf p - \mathbf y ∂z∂ℓ=p−y
这就是90%的教程都漏讲清楚的地方。
Softmax和交叉熵组合后,梯度简化为预测概率减去真实标签。不需要算Jacobian,不需要链式法则展开,直接一减完事。
这个简洁性不是巧合,而是数学上的"完美配合":Softmax的指数形式刚好抵消了交叉熵的对数导数。
参数梯度与反向传播
已知 ∂ ℓ ∂ z = p − y \frac{\partial \ell}{\partial \mathbf z} = \mathbf p - \mathbf y ∂z∂ℓ=p−y,设 g = p − y \mathbf g = \mathbf p - \mathbf y g=p−y。
对于线性模型 z = W x + b \mathbf z = W\mathbf x + \mathbf b z=Wx+b:
∂ ℓ ∂ W = g x T , ∂ ℓ ∂ b = g , ∂ ℓ ∂ x = W T g \frac{\partial \ell}{\partial W} = \mathbf g \mathbf x^T, \quad \frac{\partial \ell}{\partial \mathbf b} = \mathbf g, \quad \frac{\partial \ell}{\partial \mathbf x} = W^T \mathbf g ∂W∂ℓ=gxT,∂b∂ℓ=g,∂x∂ℓ=WTg
批量情况下:
∂ J ∂ W = 1 m ( P − Y ) T X , ∂ J ∂ b = 1 m ∑ i = 1 m ( p i − y i ) \frac{\partial J}{\partial W} = \frac{1}{m}(P-Y)^T X, \quad \frac{\partial J}{\partial \mathbf b} = \frac{1}{m}\sum_{i=1}^{m}(\mathbf p_i - \mathbf y_i) ∂W∂J=m1(P−Y)TX,∂b∂J=m1i=1∑m(pi−yi)
与Sigmoid的关系
Softmax是Sigmoid的多分类推广。当 K = 2 K=2 K=2 时:
p 1 = e z 1 e z 1 + e z 2 = 1 1 + e z 2 − z 1 = 1 1 + e − z p_1 = \frac{e^{z_1}}{e^{z_1}+e^{z_2}} = \frac{1}{1+e^{z_2-z_1}} = \frac{1}{1+e^{-z}} p1=ez1+ez2ez1=1+ez2−z11=1+e−z1
其中 z = z 1 − z 2 z = z_1 - z_2 z=z1−z2。这正是Sigmoid函数。
所以二分类用Sigmoid、多分类用Softmax,本质上是一回事。
数值稳定性
直接计算 e z k e^{z_k} ezk 时,如果 z k z_k zk 很大(比如1000),会发生溢出。
解决方案: 减去最大值
p k = e z k − C ∑ j = 1 K e z j − C , C = max j z j p_k = \frac{e^{z_k - C}}{\sum_{j=1}^{K}e^{z_j - C}}, \quad C = \max_j z_j pk=∑j=1Kezj−Cezk−C,C=jmaxzj
这样最大的指数项变成 e 0 = 1 e^0 = 1 e0=1,有效避免溢出。数学上等价,因为分子分母同乘 e − C e^{-C} e−C。
这是工程实现中必须做的,但很多教程不会提。
Python实现
import numpy as np
def softmax(z):
"""
数值稳定的Softmax实现
z: (m, K) 的logits矩阵
返回: (m, K) 的概率矩阵
"""
# 减去每行的最大值,防止溢出
z = z - np.max(z, axis=1, keepdims=True)
exp_z = np.exp(z)
return exp_z / np.sum(exp_z, axis=1, keepdims=True)
def cross_entropy(p, y):
"""
交叉熵损失
p: (m, K) 的预测概率
y: (m, K) 的one-hot标签
"""
eps = 1e-12 # 防止log(0)
p = np.clip(p, eps, 1.0)
return -np.mean(np.sum(y * np.log(p), axis=1))
def compute_gradients(X, y, W, b):
"""
计算Softmax+交叉熵的梯度
X: (m, d) 输入
y: (m, K) one-hot标签
W: (K, d) 权重
b: (K,) 偏置
"""
m = X.shape[0]
# 前向传播
z = X @ W.T + b # (m, K)
p = softmax(z) # (m, K)
# 计算损失
loss = cross_entropy(p, y)
# 反向传播:核心公式 dz = (p - y) / m
dz = (p - y) / m # (m, K)
# 参数梯度
dW = dz.T @ X # (K, d)
db = np.sum(dz, axis=0) # (K,)
return loss, dW, db
# 简单测试
if __name__ == "__main__":
# 3分类问题,2维输入
np.random.seed(42)
m, d, K = 100, 2, 3
X = np.random.randn(m, d)
y = np.zeros((m, K))
y[np.arange(m), np.random.randint(0, K, m)] = 1
W = np.random.randn(K, d) * 0.01
b = np.zeros(K)
loss, dW, db = compute_gradients(X, y, W, b)
print(f"Loss: {loss:.4f}")
print(f"dW shape: {dW.shape}, db shape: {db.shape}")
关键代码注释:
z = z - np.max(z, axis=1, keepdims=True):数值稳定性处理dz = (p - y) / m:核心梯度公式,简洁到不可思议
常见误区(踩坑记录)
误区1:Softmax输出可以直接当概率用?
可以,但前提是模型训练好了。 训练初期,Softmax可能很"自信"(某个类别接近1),但模型其实还没学会。这时候交叉熵损失已经很小了,梯度也小,更新慢。
解决: 配合学习率预热(warmup),让模型先"谦虚"一点。
误区2:交叉熵和MSE差不多?
差远了。 MSE在分类问题上的梯度会消失,尤其是预测概率接近0或1时。交叉熵的梯度是线性的,不会消失。
直观理解: MSE惩罚"距离",交叉熵惩罚"概率错误"。分类问题关心的是"对不对",不是"差多少"。
误区3:Softmax只能用于分类?
不一定。 任何需要把向量归一化为概率分布的场景都可以用,比如:
- Transformer的注意力权重计算
- 知识蒸馏中的软标签
- 强化学习中的策略分布
误区4:多分类必须用Softmax?
不是必须。 有些任务用Sigmoid配合多个二分类器(multi-label分类),比如一张图片可以同时有"猫"和"狗"。这时候Softmax不合适,因为类别不互斥。
总结
Softmax和交叉熵的组合,是深度学习分类任务的"黄金搭档"。
核心结论:
- Softmax把任意实数向量变成概率分布,保证非负且和为1
- 交叉熵衡量预测概率与真实分布的差异,只关心真实类别的预测概率
- 两者组合后,梯度简化为 p − y \mathbf p - \mathbf y p−y,这是反向传播的核心基础
- 工程实现必须做数值稳定性处理(减去最大值)
一句话记住: Softmax负责"打分转概率",交叉熵负责"告诉模型错在哪"。
写在最后
说实话,第一次看Softmax推导的时候,我也觉得这些公式很吓人。但拆解开来其实就三步:打分→转概率→算损失。真正漂亮的是第三步的梯度化简,预测概率减真实标签,简洁到不可思议。
你在用Softmax的时候踩过什么坑?欢迎在评论区聊聊。下一篇打算写Transformer里的注意力机制,感兴趣的可以关注一下。
参考:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)