上周帮学弟调一个三分类模型,死活跑不通。看了半天,发现他把Softmax输出当概率直接用了,但损失函数写的却是MSE。这个坑我踩过,今天想聊聊多分类问题到底该怎么玩。


一句话总结

Softmax负责把模型的"打分"转成概率,交叉熵负责告诉模型"你错在哪"。两者组合后,反向传播梯度简化为预测概率减去真实标签,这是深度学习分类任务的核心基础。


背景:从二分类到多分类

之前写过Sigmoid的推导,那是二分类的情况。但现实中的分类问题往往是多类的:手写数字识别(10类)、ImageNet(1000类)、文本情感分析(3-5类)。

问题来了:Sigmoid只能输出一个概率值,多分类怎么办?

最直观的想法是"每个类别单独训一个二分类器"。但这样做有两个问题:

  1. 类别之间是互斥的。一张图片不可能同时是"猫"和"狗",但独立的二分类器不会考虑这一点。
  2. 输出不统一。每个二分类器输出自己的概率,加起来不等于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=[z1z2zK]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=kx)=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(yixi)=k=1Kpikyik

整个数据集的似然:

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=1mk=1Kpikyik

取对数:

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=1mk=1Kyiklnpik

最大化似然等价于最小化负对数似然,定义损失函数:

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=1mk=1Kyiklnpik

这就是交叉熵损失

单样本的交叉熵

对于单个样本:

ℓ = − ∑ k = 1 K y k ln ⁡ p k \ell = -\sum_{k=1}^{K} y_k \ln p_k =k=1Kyklnpk

由于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=1Kezj

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) zipi=pi(1pi)

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 zlpi=pipl

统一写成:

∂ p i ∂ z l = p i ( δ i l − p l ) \frac{\partial p_i}{\partial z_l} = p_i(\delta_{il} - p_l) zlpi=pi(δilpl)

其中 δ 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=1Kyklnpk

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=1Kpkzlpk=k=1K(pkyk)pk(δklpl)

化简:

∂ ℓ ∂ 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=1Kyk(δklpl)=yl+plk=1Kyk=plyl

因为 ∑ 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=py

这就是90%的教程都漏讲清楚的地方。

Softmax和交叉熵组合后,梯度简化为预测概率减去真实标签。不需要算Jacobian,不需要链式法则展开,直接一减完事。

这个简洁性不是巧合,而是数学上的"完美配合":Softmax的指数形式刚好抵消了交叉熵的对数导数。


参数梯度与反向传播

已知 ∂ ℓ ∂ z = p − y \frac{\partial \ell}{\partial \mathbf z} = \mathbf p - \mathbf y z=py,设 g = p − y \mathbf g = \mathbf p - \mathbf y g=py

对于线性模型 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) WJ=m1(PY)TX,bJ=m1i=1m(piyi)


与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+ez2z11=1+ez1

其中 z = z 1 − z 2 z = z_1 - z_2 z=z1z2。这正是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=1KezjCezkC,C=jmaxzj

这样最大的指数项变成 e 0 = 1 e^0 = 1 e0=1,有效避免溢出。数学上等价,因为分子分母同乘 e − C e^{-C} eC

这是工程实现中必须做的,但很多教程不会提。


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和交叉熵的组合,是深度学习分类任务的"黄金搭档"。

核心结论:

  1. Softmax把任意实数向量变成概率分布,保证非负且和为1
  2. 交叉熵衡量预测概率与真实分布的差异,只关心真实类别的预测概率
  3. 两者组合后,梯度简化为 p − y \mathbf p - \mathbf y py,这是反向传播的核心基础
  4. 工程实现必须做数值稳定性处理(减去最大值)

一句话记住: Softmax负责"打分转概率",交叉熵负责"告诉模型错在哪"。


写在最后

说实话,第一次看Softmax推导的时候,我也觉得这些公式很吓人。但拆解开来其实就三步:打分→转概率→算损失。真正漂亮的是第三步的梯度化简,预测概率减真实标签,简洁到不可思议。

你在用Softmax的时候踩过什么坑?欢迎在评论区聊聊。下一篇打算写Transformer里的注意力机制,感兴趣的可以关注一下。

参考:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐