news 2026/8/13 22:56:27

从Softmax与交叉熵损失原理到NumPy实现:掌握分类任务核心梯度推导

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从Softmax与交叉熵损失原理到NumPy实现:掌握分类任务核心梯度推导

1. 项目概述:从公式到代码的深度穿越

如果你正在入门深度学习,尤其是分类任务,那么“Softmax + 交叉熵损失”这个组合对你来说,就像学开车必须先学会踩油门和刹车一样基础。但很多人可能只是调用了torch.nn.CrossEntropyLoss()或者tf.nn.softmax_cross_entropy_with_logits,对背后发生了什么,尤其是那个至关重要的梯度反向传播过程,总感觉隔着一层毛玻璃。

这个项目,就是要把这层毛玻璃彻底砸碎。我们不满足于当一个“调包侠”,而是要亲手从数学公式开始,一步步推导出 Softmax 函数、交叉熵损失,以及最核心的——损失函数关于网络原始输出(logits)的梯度。最后,我们不依赖任何深度学习框架的自动微分,用纯 NumPy 实现整个前向传播和反向传播过程。这个过程会让你对神经网络训练中最基础的环节产生肌肉记忆般的理解,未来无论是调试损失不下降、梯度爆炸消失,还是自定义新的损失函数,你都会有十足的底气。

2. 核心概念拆解:为什么是它们俩?

在动手之前,我们得先搞清楚,为什么分类问题普遍采用 Softmax + 交叉熵损失这个黄金组合。这背后是严格的数学逻辑和工程实践的完美结合。

2.1 Softmax:将分数转化为概率

神经网络的最后一层,通常是一个全连接层,它会输出一组实数,我们称之为“logits”。这些 logits 可正可负,可大可小,但它们本身并不是概率——概率要求每个值在0到1之间,并且所有值之和为1。

Softmax 函数就是干这个的:它是一个“多分类”的激活函数,负责将这组任意的 logits 转换成一个合法的概率分布。

它的公式看起来很简单: $$S_i = \frac{e^{z_i}}{\sum_{j=1}^{C} e^{z_j}}$$ 其中,$z_i$ 是第 i 个类别的 logit 值,$C$ 是总类别数,$S_i$ 就是该样本属于第 i 类的预测概率。

为什么用指数函数 $e$?这是 Softmax 的一个关键设计。指数函数 $e^x$ 有两个绝佳的性质:

  1. 单调性:它严格单调递增。这意味着 logits 大的,经过 Softmax 后概率依然大,保持了原始分数的相对大小顺序。
  2. 非负性与放大差异:$e^x$ 永远大于0,确保了概率非负。更重要的是,它对大的正数响应剧烈,对负数或小的正数则进行压制。这相当于放大了 logits 之间的差异,让模型对“最可能”的类别更有信心。你可以想象,如果两个 logits 分别是 10 和 9,差值只有1,但 $e^{10}$ 和 $e^{9}$ 的比值是 $e^1 \approx 2.718$,差异被显著放大了。

一个必须警惕的数值稳定性问题: 直接套用上面这个公式,在计算机里是危险的。因为 $e^{z_i}$ 增长极快,如果某个 $z_i$ 很大(比如100),$e^{100}$ 会变成一个天文数字,导致浮点数上溢出(inf)。为了解决这个问题,我们使用一个经典的数学技巧:减去最大值。 $$S_i = \frac{e^{z_i - \text{max}(z)}}{\sum_{j=1}^{C} e^{z_j - \text{max}(z)}}$$ 因为 $e^{z_i - \text{max}(z)} = e^{z_i} / e^{\text{max}(z)}$,分子分母同时除以 $e^{\text{max}(z)}$,结果在数学上是完全等价的。但这样做之后,指数部分的最大值变成了0,$e^0=1$,其他都是负数或零,$e^{\text{负数}}$ 是一个小于1的数,完美避免了溢出。这是实现时必须做的第一步

2.2 交叉熵损失:衡量概率分布的差距

得到了预测概率分布 $S$,我们如何衡量它和真实标签分布 $Y$ 的差距呢?这里就用到了交叉熵(Cross-Entropy)。

在分类任务中,我们通常使用“one-hot”编码来表示真实标签。比如有3个类别,真实类别是第2类,那么 $Y = [0, 1, 0]$。这是一个“标准答案”概率分布,正确的类别概率为1,其他为0。

交叉熵衡量的是用预测分布 $S$ 去描述真实分布 $Y$ 所需要的“平均信息量”。信息论告诉我们,如果两个分布完全一致,交叉熵最小。其公式为: $$H(Y, S) = -\sum_{i=1}^{C} Y_i \log(S_i)$$

由于 $Y$ 是 one-hot 向量,只有真实类别 $t$ 对应的 $Y_t = 1$,其他都为0。因此,上面的求和公式瞬间被简化了: $$L = -\log(S_t)$$ 其中,$t$ 是样本的真实类别索引,$S_t$ 是 Softmax 后模型预测该样本属于真实类别的概率。

这个形式极其优美,也揭示了交叉熵损失的本质:它只关心模型对正确类别的预测概率 $S_t$。$S_t$ 越大(越接近1),$-\log(S_t)$ 就越小(接近0),损失越小;反之,如果模型预测正确类别的概率很小(比如0.1),那么 $-\log(0.1) \approx 2.3$,损失就会很大。

为什么不用均方误差(MSE)?这是一个经典面试题。对于分类问题,MSE(比如 $\sum (Y_i - S_i)^2$)并不是一个好选择。

  1. 优化效率:交叉熵损失配合 Softmax,其梯度形式非常干净(我们马上会推导),能让模型在训练初期快速更新。而 MSE 的梯度在概率接近0或1时会变得非常小,导致学习缓慢,也就是所谓的“梯度饱和”问题。
  2. 概率解释:交叉熵直接来源于最大似然估计,它与 Softmax 输出的概率表示在数学上是自洽的。最小化交叉熵等价于最大化样本的似然函数,这是一个非常坚实的统计学基础。

所以,Softmax 负责产出概率,交叉熵负责用信息论的方式衡量这个概率的好坏,两者是天作之合。

3. 梯度推导:反向传播的灵魂

这是整个项目的核心难点,也是理解神经网络如何学习的关键。我们的目标是:求出损失函数 $L$ 关于网络原始输出 $z_j$(即 Softmax 的输入 logits)的梯度 $\frac{\partial L}{\partial z_j}$。

我们有:

  1. Softmax 函数:$S_i = \frac{e^{z_i}}{\sum_{k} e^{z_k}}$
  2. 交叉熵损失:$L = -\log(S_t)$,其中 $t$ 是真实类别索引。

我们需要 $\frac{\partial L}{\partial z_j}$。根据链式法则: $$\frac{\partial L}{\partial z_j} = \sum_{i=1}^{C} \frac{\partial L}{\partial S_i} \cdot \frac{\partial S_i}{\partial z_j}$$ 因为 $S_i$ 是 $z_j$ 的函数,一个 $z_j$ 的变化会影响所有的 $S_i$。

第一步:求 $\frac{\partial L}{\partial S_i}$由于 $L = -\log(S_t)$,所以:

  • 当 $i = t$(真实类别)时,$\frac{\partial L}{\partial S_t} = -\frac{1}{S_t}$
  • 当 $i \neq t$ 时,$L$ 与 $S_i$ 无关,导数为 0。

第二步:求 $\frac{\partial S_i}{\partial z_j}$这是推导中最需要小心的地方。$S_i = \frac{e^{z_i}}{\sum_k e^{z_k}}$。这里需要分两种情况讨论:

  • 情况一:当 $i = j$ 时,即求 $S_i$ 对自身的输入 $z_i$ 的偏导。 这时,$z_i$ 同时出现在分子和分母中。我们需要使用商的求导法则。 令 $D = \sum_k e^{z_k}$,则 $S_i = e^{z_i} / D$。 $$\frac{\partial S_i}{\partial z_i} = \frac{e^{z_i} \cdot D - e^{z_i} \cdot e^{z_i}}{D^2} = \frac{e^{z_i}}{D} - \frac{e^{z_i}}{D} \cdot \frac{e^{z_i}}{D} = S_i - S_i \cdot S_i = S_i(1 - S_i)$$
  • 情况二:当 $i \neq j$ 时,即求 $S_i$ 对另一个输入 $z_j$ 的偏导。 此时,$z_j$ 只出现在分母 $D$ 中。 $$\frac{\partial S_i}{\partial z_j} = \frac{0 \cdot D - e^{z_i} \cdot e^{z_j}}{D^2} = -\frac{e^{z_i}}{D} \cdot \frac{e^{z_j}}{D} = -S_i S_j$$

第三步:组合求 $\frac{\partial L}{\partial z_j}$现在我们将两部分组合起来。记住,$\frac{\partial L}{\partial S_i}$ 只在 $i=t$ 时有值。 $$\frac{\partial L}{\partial z_j} = \sum_{i} \frac{\partial L}{\partial S_i} \cdot \frac{\partial S_i}{\partial z_j} = \frac{\partial L}{\partial S_t} \cdot \frac{\partial S_t}{\partial z_j}$$ 因为只有当 $i=t$ 时,求和项才不为零。

将 $\frac{\partial L}{\partial S_t} = -\frac{1}{S_t}$ 代入:

  • 当 $j = t$ 时(对真实类别的 logit 求导): $$\frac{\partial L}{\partial z_t} = -\frac{1}{S_t} \cdot \frac{\partial S_t}{\partial z_t} = -\frac{1}{S_t} \cdot [S_t(1 - S_t)] = S_t - 1$$
  • 当 $j \neq t$ 时(对其他类别的 logit 求导): $$\frac{\partial L}{\partial z_j} = -\frac{1}{S_t} \cdot \frac{\partial S_t}{\partial z_j} = -\frac{1}{S_t} \cdot [-S_t S_j] = S_j$$

最终,我们得到了一个极其简洁、优美的结果:$$\frac{\partial L}{\partial z_j} = S_j - Y_j$$ 其中,$Y$ 是 one-hot 编码的真实标签向量。因为 $Y_t = 1$,其他 $Y_j = 0$。

这个结果的意义非同小可: 它告诉我们,损失函数关于网络原始输出 $z$ 的梯度,就是模型的预测概率分布 $S$ 减去真实标签分布 $Y$。这个梯度非常“干净”:

  • 对于真实类别($j=t$),梯度是 $S_t - 1$,一个负数。这意味着在反向传播时,这个 logit 会被增大(因为梯度下降是朝负梯度方向更新)。
  • 对于其他类别($j \neq t$),梯度是 $S_j$,一个正数。这意味着这些 logits 会被减小
  • 梯度的幅度直接由预测概率 $S_j$ 决定。如果模型对某个错误类别的预测概率很高($S_j$ 大),那么对应的负向梯度也会很大,惩罚力度就强。

这个优雅的梯度形式,正是 Softmax 与交叉熵搭配能高效训练的根本原因。

4. 纯 NumPy 实现:脱离框架的裸奔

理解了所有数学原理后,我们现在用纯 NumPy 来实现它,彻底摆脱对 PyTorch/TensorFlow 自动微分(autograd)的依赖。我们会实现三个核心函数:softmaxcross_entropy_lossgradient

4.1 Softmax 的稳健实现

首先实现 Softmax,必须包含数值稳定化技巧。

import numpy as np def softmax(z): """ 计算 softmax 函数,具有数值稳定性。 参数: z: 一个形状为 (N, C) 的 numpy 数组,N 是样本数,C 是类别数。 返回: s: softmax 概率,形状同 z。 """ # 数值稳定化:减去每行的最大值 # keepdims=True 确保形状为 (N, 1),便于广播 z_stable = z - np.max(z, axis=1, keepdims=True) # 计算指数 exp_z = np.exp(z_stable) # 计算每行的和,并保持维度用于广播 sum_exp_z = np.sum(exp_z, axis=1, keepdims=True) # 计算 softmax 概率 s = exp_z / sum_exp_z return s

关键细节解析

  • np.max(z, axis=1, keepdims=True)axis=1表示对每行(每个样本)求最大值。keepdims=True至关重要,它让结果的形状从(N,)变为(N, 1)。这样,z - max时,(N, 1)的数组会自动广播(broadcast)到(N, C),与每一列相减。如果不用keepdims,就需要手动reshape,代码不够简洁且易错。
  • 整个操作是向量化的,一次处理一个批次(N个样本)的数据,效率远高于循环。

4.2 交叉熵损失计算

接下来实现交叉熵损失。这里假设标签y是类别索引(例如[2, 0, 1]),而不是 one-hot 编码,因为这是更常见的接口形式。

def cross_entropy_loss(s, y): """ 计算交叉熵损失。 参数: s: softmax 概率,形状 (N, C)。 y: 真实标签索引,形状 (N,)。每个元素在 [0, C-1] 范围内。 返回: loss: 平均交叉熵损失(标量)。 s: 返回 softmax 概率,供后续梯度计算使用。 """ N = s.shape[0] # 样本数量 # 获取每个样本对应真实类别的预测概率 # s[np.arange(N), y] 是高级索引(fancy indexing),非常高效。 # np.arange(N) 生成行索引 [0, 1, ..., N-1],y 是对应的列索引。 correct_class_probs = s[np.arange(N), y] # 计算每个样本的损失: L_i = -log(p_{i, y_i}) # 添加一个极小值 epsilon 防止 log(0) 导致 -inf epsilon = 1e-12 losses = -np.log(correct_class_probs + epsilon) # 计算整个批次的平均损失 loss = np.mean(losses) return loss, s # 返回损失和 softmax 概率(后者用于梯度计算)

实操心得与避坑指南

  1. 高级索引(Fancy Indexing)s[np.arange(N), y]是 NumPy 的精华操作之一,它一次性提取了所有样本的正确类别概率,比用for循环快几个数量级。务必掌握这种写法。
  2. 对数防御:一定要加一个极小值epsilon(如1e-12)。理论上,经过 Softmax 的概率不会绝对为0,但由于浮点数精度问题,有可能出现np.exp(很大负数)=0的情况,导致log(0)产生负无穷(-inf),进而使损失和梯度变成nan。这是一个非常隐蔽的 bug。
  3. 返回s:注意我们把计算好的s也返回了。因为在计算梯度时我们需要用到它,避免在梯度函数中重复计算 Softmax,这是提高代码效率的一个小技巧。

4.3 梯度计算实现

最后,实现我们推导出的核心梯度公式:$\frac{\partial L}{\partial z} = S - Y$。

def gradient(z, s, y): """ 计算交叉熵损失关于输入 logits z 的梯度。 参数: z: 原始 logits,形状 (N, C)。(注意:此函数内未直接使用,但接口保留以明确含义) s: softmax 概率,形状 (N, C)。 y: 真实标签索引,形状 (N,)。 返回: dz: 梯度,形状同 z (N, C)。 """ N, C = s.shape dz = s.copy() # 梯度初始化为预测概率 S # 创建 one-hot 编码的真实标签矩阵 Y # 方法:先生成全0矩阵,然后在每个样本的真实类别位置置1 y_one_hot = np.zeros_like(s) y_one_hot[np.arange(N), y] = 1 # 再次使用高级索引 # 核心梯度公式: dL/dz = S - Y dz -= y_one_hot # 注意:这里计算的是每个样本损失的梯度之和。 # 在损失函数中我们计算的是平均损失 (mean),因此梯度也需要除以 N 来保持一致性。 # 但更常见的做法是在优化器更新参数时,考虑批次大小。这里我们先返回未平均的梯度。 # 如果使用 SGD,通常会用 `dz / N` 作为平均梯度。 # 为了通用性,我们先返回这个“总和梯度”,使用者可根据优化器决定是否平均。 return dz

梯度公式的工程化细节

  1. One-hot 编码的创建np.zeros_like(s)创建了一个和s形状相同的全0矩阵。y_one_hot[np.arange(N), y] = 1这行代码,又一次利用高级索引,高效地将所有样本的真实类别位置赋值为1。这是 NumPy 实现 one-hot 编码的标准且高效的方式。
  2. 梯度公式的实现dz = s.copy()然后dz -= y_one_hot,清晰对应了 $\frac{\partial L}{\partial z} = S - Y$。使用.copy()是为了避免直接修改传入的s数组。
  3. 关于平均梯度:这是一个容易混淆的点。我们的损失函数cross_entropy_loss返回的是平均损失np.mean(losses))。根据微积分,平均损失的梯度,等于每个样本损失梯度的平均值。因此,严格来说,这里返回的dz是 N 个样本梯度之和。在参数更新时,如果你用的是朴素的 SGD,更新公式是w = w - learning_rate * gradient。这时,你应该使用gradient / N作为平均梯度。而像 PyTorch 这样的框架,在调用loss.backward()时,如果loss是一个标量(通常是平均损失),它会自动计算并累积平均梯度。我们的实现为了清晰,返回了“总和梯度”,把是否平均的决策权交给调用者。

4.4 整合测试:一个完整的训练步骤模拟

让我们把这三个函数串起来,模拟一个简单的前向-反向传播过程,并验证梯度计算的正确性。

# 模拟一个批次的训练数据 np.random.seed(42) # 固定随机种子,确保结果可复现 N = 5 # 批次大小 C = 3 # 类别数 # 随机生成 logits(可以理解为线性层的输出) z = np.random.randn(N, C) * 0.1 # 小随机数,模拟训练初期 # 随机生成真实标签 y = np.random.randint(0, C, size=(N,)) print("Logits z (raw scores):\n", z) print("True labels y:", y) # 1. 前向传播 s = softmax(z) loss, s_retained = cross_entropy_loss(s, y) # s_retained 就是 s print("\nSoftmax probabilities S:\n", np.round(s, 4)) print("Cross-entropy loss:", loss) # 2. 反向传播(计算梯度) dz = gradient(z, s_retained, y) print("\nGradient dL/dz (S - Y):\n", np.round(dz, 4)) # 3. 验证梯度数值(有限差分法) def numerical_gradient(f, x, eps=1e-6): """使用中心差分法计算标量函数 f 在点 x 处的数值梯度。""" grad = np.zeros_like(x) it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite']) while not it.finished: idx = it.multi_index original_val = x[idx].copy() x[idx] = original_val + eps f_plus = f(x) x[idx] = original_val - eps f_minus = f(x) grad[idx] = (f_plus - f_minus) / (2 * eps) x[idx] = original_val # 恢复原值 it.iternext() return grad # 定义一个包装函数,计算给定 logits 下的损失 def loss_wrapper(z_single): """假设其他样本的 logits 不变,只计算第一个样本的损失变化。用于数值梯度检验。""" z_test = z.copy() z_test[0] = z_single # 只改变第一个样本的 logits s_test = softmax(z_test) loss_test, _ = cross_entropy_loss(s_test, y) return loss_test # 计算第一个样本 logits 的数值梯度 z0 = z[0].copy() grad_num = numerical_gradient(loss_wrapper, z0) print("\nNumerical gradient for sample 0 (finite difference):\n", np.round(grad_num, 4)) print("Analytical gradient for sample 0 (our derivation):\n", np.round(dz[0], 4)) print("\nDifference (should be very small):\n", np.round(grad_num - dz[0], 8))

运行这段代码,你会看到我们推导的解析梯度(dz)和通过微扰法计算的数值梯度(grad_num)基本一致(差异在1e-7量级或更小)。这强有力地验证了我们梯度推导和代码实现的正确性。数值梯度检验是验证自定义梯度实现是否正确的“金标准”,在实现复杂的损失函数或层时,这个步骤必不可少。

5. 深入讨论与扩展

掌握了基础实现后,我们可以看看一些相关的变体和工程实践中的细节。

5.1 LogSoftmax 与 NLLLoss:更稳定的组合

在实际的深度学习框架中,你经常会看到LogSoftmaxNLLLoss(Negative Log Likelihood Loss)的组合,而不是直接的Softmax+CrossEntropyLoss。这其实是数学等价的,但数值上更稳定。

回忆一下,我们的损失是 $L = -\log(S_t)$,其中 $S_t = \frac{e^{z_t}}{\sum e^{z_j}}$。那么: $$\log(S_t) = \log(e^{z_t}) - \log(\sum e^{z_j}) = z_t - \log(\sum e^{z_j})$$ 所以 $L = -z_t + \log(\sum e^{z_j})$。

LogSoftmax就是计算 $\log(S_i) = z_i - \log(\sum e^{z_j})$。而NLLLoss就是取对应真实类别的负值。这样做的好处是,logsumexp函数(即 $\log(\sum e^{z_j})$)有专门的数值稳定实现,它一次性完成了“减最大值”和“取对数求和”的操作,避免了先算exp可能导致的中间溢出问题。虽然我们的softmax实现已经做了稳定性处理,但LogSoftmax在极端情况下通常更鲁棒。PyTorch 的F.log_softmaxF.nll_loss就是基于此。

5.2 标签平滑(Label Smoothing):一种实用的正则化

在分类任务中,我们一直使用 one-hot 编码,即把正确的类别设为1,其他为0。这有时会导致模型过于“自信”,对预测概率过度拟合,从而可能降低泛化能力。标签平滑是一种简单有效的正则化技术。

它的思想是将真实标签的“1”稍微调低一点,把减掉的部分平均分给其他类别。例如,对于平滑参数 $\epsilon = 0.1$,原来的 one-hot 标签 $[0, 1, 0]$ 会变成 $[0.05, 0.9, 0.05]$。

在我们的梯度公式 $\frac{\partial L}{\partial z_j} = S_j - Y_j$ 中,这相当于改变了 $Y_j$。对于非目标类,梯度从 $S_j - 0 = S_j$ 变成了 $S_j - \epsilon/(C-1)$,这是一个更小的正数;对于目标类,梯度从 $S_t - 1$ 变成了 $S_t - (1-\epsilon)$,这是一个绝对值更小的负数。整体上,这缓和了梯度的强度,起到了正则化的作用,通常能带来轻微但稳定的性能提升。在 PyTorch 中,可以通过CrossEntropyLoss(..., label_smoothing=0.1)直接使用。

5.3 与 Focal Loss 等进阶损失的关联

你提供的热词中提到了focal lossasymmetric loss等。这些都是在交叉熵损失基础上的改进,主要解决类别不平衡或难易样本不平衡的问题。

以 Focal Loss 为例,它的核心思想是:降低那些“容易分类”的样本(即预测概率很高的样本)对总损失的贡献,让模型更专注于学习那些“难分类”的样本。其公式为: $$FL(p_t) = -\alpha_t (1 - p_t)^{\gamma} \log(p_t)$$ 其中 $p_t$ 就是模型预测的正确类别概率 $S_t$。相比标准交叉熵 $-\log(p_t)$,它多了两个因子:

  • $(1-p_t)^{\gamma}$:当 $p_t$ 很大(接近1,易分样本)时,这个因子接近0,从而降低了该样本的损失权重。$\gamma$ 是调节因子,越大,对易分样本的抑制越强。
  • $\alpha_t$:用于平衡不同类别权重的因子,可以缓解类别不平衡。

当你深刻理解了标准交叉熵损失的梯度($S - Y$)后,再去推导 Focal Loss 的梯度,思路是完全一样的,只是链式法则中多了一些项。理解基础版本,是理解和自定义这些高级变种的前提。

6. 常见问题与调试技巧

在实际实现和使用中,你可能会遇到以下问题:

1. 损失为 NaN 或无限大(inf)

  • 原因A:Logits 数值过大。即使我们做了“减最大值”操作,如果 logits 的原始值相差巨大,exp计算后仍可能导致浮点数溢出(得到inf)或下溢(得到0)。检查网络初始化,避免初始权重过大。可以考虑使用更小的学习率或梯度裁剪。
  • 原因B:标签错误。如果真实标签y的索引超出了类别范围[0, C-1],在索引s[np.arange(N), y]时会出错,或者导致取到无意义的概率值。
  • 原因C:对数输入为0。这就是为什么我们要在np.log里加epsilon的原因。确保你的softmax函数正确实现了数值稳定化。

2. 梯度消失或爆炸

  • 现象:模型参数更新非常慢(梯度太小)或更新剧烈导致损失突变为 NaN(梯度太大)。
  • 检查梯度:像我们上面做的那样,实现一个数值梯度检查函数。这是验证自定义层或损失函数梯度正确性的唯一可靠方法。如果解析梯度与数值梯度差异很大,说明推导或代码有误。
  • 梯度裁剪:如果梯度范数过大,可以在更新参数前对其进行裁剪:gradient = np.clip(gradient, -clip_value, clip_value)。这是训练 RNN 或深层网络时的常用技巧。
  • 权重初始化:不恰当的初始化(如权重全为0)可能导致所有神经元的输出相同,使得 Softmax 的输出退化为均匀分布,梯度也可能出现问题。使用 Xavier 或 He 初始化等方法。

3. 训练初期损失不下降

  • 检查数据:确保输入数据经过了适当的归一化或标准化。确保标签是正确的。
  • 检查学习率:学习率可能太大了(损失震荡)或太小了(下降极慢)。可以尝试一个学习率范围(如[1e-5, 1e-1])进行搜索。
  • 检查前向传播:打印出第一批数据的 Softmax 输出s和损失loss。如果s的概率分布看起来基本均匀(例如3分类每个类都接近0.33),而损失接近-log(1/C)(如3分类约为1.099),这是正常的训练起点。如果损失一开始就异常大或小,需要检查网络结构。

4. 批次大小的影响我们的实现和推导都是基于一个批次(N个样本)的。在计算最终损失时,我们取了平均(np.mean)。这意味着,无论批次大小如何,损失的大致尺度是稳定的。但在计算梯度时,我们返回的是每个样本梯度的总和。当使用不同的优化器时要注意:

  • SGD:通常使用梯度 / N作为平均梯度。
  • SGD with Momentum / Adam:这些优化器内部会处理梯度的缩放。通常直接使用我们返回的“总和梯度”即可,因为优化器的学习率参数已经隐含了对批次大小的考虑。但最稳妥的方式是查阅你所使用的优化器的具体约定。

亲手推导一遍 Softmax 和交叉熵损失的梯度,再用 NumPy 实现出来,这个过程带来的理解深度是只看文档或调用 API 无法比拟的。它让你对神经网络最基础的反向传播有了直接的掌控感。下次当你模型的损失出现异常时,你不再只能盲目地调整超参,而是可以有条理地检查数据流、梯度值,甚至自己计算数值梯度来验证。这才是从一个框架使用者向一个真正的深度学习实践者迈进的关键一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 22:54:52

C++ RAII技术:资源管理的核心原理与实践

1. RAII技术概述在C开发中,资源管理一直是个令人头疼的问题。记得我刚入行时,经常因为忘记释放资源导致内存泄漏,直到遇到RAII这个"救星"。RAII(Resource Acquisition Is Initialization)是C特有的资源管理范…

作者头像 李华
网站建设 2026/8/13 22:51:25

软件工程毕业设计容易的方向大全

1 引言 毕业设计是大家学习生涯的最重要的里程碑,它不仅是对四年所学知识的综合运用,更是展示个人技术能力和创新思维的重要过程。选择一个合适的毕业设计题目至关重要,它应该既能体现你的专业能力,又能满足实际应用需求&#xf…

作者头像 李华
网站建设 2026/8/13 22:50:09

Golang HTTP 中间件机制

HTTP 中间件机制 一、知识点总结 1.1 什么是中间件(Middleware) 中间件是 HTTP 处理流程中的可插拔处理单元,它在请求到达最终 Handler 之前执行预处理,在 Handler 返回响应之后执行后处理。典型中间件包括:日志记录、…

作者头像 李华
网站建设 2026/8/13 22:48:06

Vue3可扩展Provider系统设计:IoC容器与依赖注入在AI平台的应用

1. 项目概述:为什么我们需要一个可扩展的 Provider 系统?在构建现代前端应用,尤其是基于 Vue3 的复杂中后台系统时,我们常常会遇到一个核心挑战:如何优雅地管理那些分散在应用各个角落的、与外部服务或复杂内部状态打交…

作者头像 李华
网站建设 2026/8/13 22:43:02

Linux系统运维:使用ps命令深入排查多线程应用性能问题

1. 从一次线上故障排查说起:为什么只看进程不够 那天下午,监控系统突然报警,提示某个核心服务的CPU使用率飙升到200%以上,但内存和网络IO都还正常。我第一反应是登录服务器,用最熟悉的 top 命令看了一眼,…

作者头像 李华