核心要点

  • Softmax:p_i = exp(z_i − max(z)) / Σ exp(z_j − max(z)),减最大值保证数值稳定不改结果

  • 交叉熵:对单样本 L = −log p[true],批量取平均;用 log-sum-exp 可避免先 exp 再 log 的溢出

  • 合并后梯度极简:dL/dz = p − y(y 为 one-hot),是反向传播的起点

  • 注意按行(沿类别轴)做归约,keepdims=True 才能正确广播

标准回答

Softmax 把 logits 转成概率分布,交叉熵衡量预测分布与真实 one-hot 的差距。数值稳定的核心是计算 exp 前减去每行最大值,结果不变但避免大数溢出。交叉熵取真实类别概率的负对数。最优雅的结论是:Softmax 与交叉熵复合后,对 logits 的梯度直接化简为 p−y,这让反向传播起步异常简洁。实现如下:

python
import numpy as np

def softmax(z):
    """数值稳定 Softmax:减去每行最大值防止 exp 溢出。z: (N, C)。"""
    z = z - z.max(axis=1, keepdims=True)     # 平移不改变结果
    ez = np.exp(z)
    return ez / ez.sum(axis=1, keepdims=True)

def cross_entropy(logits, labels):
    """logits: (N, C);labels: (N,) 整数类别。返回平均损失与梯度 (p - y)。"""
    N = logits.shape[0]
    p = softmax(logits)
    eps = 1e-12
    # 取每个样本真实类别的概率,做负对数
    loss = -np.mean(np.log(p[np.arange(N), labels] + eps))
    # 反向:dL/dz = p - y,其中 y 为 one-hot
    grad = p.copy()
    grad[np.arange(N), labels] -= 1.0
    grad /= N
    return loss, grad

if __name__ == '__main__':
    rng = np.random.default_rng(0)
    logits = rng.normal(0, 5, (4, 3))         # 故意用大值检验稳定性
    labels = np.array([0, 2, 1, 2])
    loss, grad = cross_entropy(logits, labels)
    print('softmax row sums =', softmax(logits).sum(1))   # 全为 1
    print('loss =', round(float(loss), 4))
    print('grad shape =', grad.shape)

常见误区

⚠️ 常见踩坑

不减最大值直接 exp 在 logits 较大时溢出为 inf,softmax 退化成 NaN;归约时忘记 keepdims=True 会让广播维度错位算错;以及先 softmax 再单独 log 比直接用 log-softmax(log-sum-exp)精度更差,类别多时尤其明显。

追问

追问 1为什么 Softmax+交叉熵的梯度是 p−y?

把交叉熵 L=−Σ y_i log p_i 与 p_i=softmax(z)_i 复合,对 z_k 求偏导,利用 Softmax 雅可比 ∂p_i/∂z_k=p_i(δ_ik−p_k) 代入并求和,y 是 one-hot 求和为 1,化简后恰好得到 p_k−y_k,干净利落,正是工程上不分开实现两者的原因。

追问 2Softmax 的平移与尺度不变性?

Softmax 平移不变:所有 logits 同加常数 c,分子分母同乘 e^c 抵消,结果不变(这正是减 max 的依据);但不是尺度不变:整体乘以 1/T(温度)会改变分布尖锐度,T 越小越接近 argmax,这是采样温度调控的原理。

延伸学习

与本题相关的知识库文章、术语、工具与行业资讯。