反向传播的核心是链式求导,需明确区分dl/dx(传给上层)、dl/dw和dl/db(仅linear层内计算并累加),固定矩阵存储顺序,用valarray或轻量tensor类避免手动索引错误,并每次backward前清零梯度。

反向传播的核心是链式求导,不是手动算梯度
很多人卡在“怎么手推公式”,其实 C++ 里真正要写的不是数学推导,而是对每个层的 forward 和 backward 做好责任划分:输入输出张量、参数、梯度缓存。关键在于把 sigmoid、ReLU、Linear 这些层的导数封装成可复用的函数,而不是每次反向都重写一遍链式法则。
常见错误是把损失对权重的导数(dL/dW)和损失对输入的导数(dL/dX)混为一谈——前者用于更新参数,后者必须传给上一层。一旦搞反,整个网络就学不动。
-
dL/dX是每个层backward的返回值,必须和该层forward的输入维度一致 -
dL/dW和dL/db只在Linear层内计算并保存,不向外传递 - 激活层(如
ReLU)没有可训练参数,所以只算dL/dX,不存dW
Linear 层的 backward 必须处理 batch 维度对齐
Linear 层前向是 X @ W.T + b,其中 X 形状是 (batch_size, in_features),W 是 (out_features, in_features)。反向时:dL/dW = (dL/dY).T @ X,dL/dX = (dL/dY) @ W。这里最容易出错的是矩阵乘顺序和转置位置。
如果用裸数组或 std::vector<:vector>></:vector>,很容易因行列搞反导致梯度爆炸或静默收敛失败。建议一开始就固定约定:所有矩阵按行优先存储,A[i][j] 表示第 i 行、第 j 列。
- 别用
std::vector套std::vector做运算 —— 内存不连续,cache 不友好,且.size()容易误读 - 批量计算
dL/dW时,必须对 batch 维度求和:dL/dW += dY_i.T * X_i,不能漏掉累加 -
b的梯度是dL/dY沿 batch 维度求和,即sum(dL/dY, axis=0)
损失函数选 MSE 还是 CrossEntropy?影响梯度形式
初学者常忽略损失函数对最后一层梯度的影响。MSE 对输出 Y 的导数是 2*(Y - target);而 CrossEntropy 配合 Softmax 输出时,dL/dY = Y - target(前提是 target 是 one-hot)。这个差异直接决定你第一段反向代码写对没。
如果你用 Sigmoid + MSE 做二分类,没问题;但换成 Softmax + MSE,梯度会比 CrossEntropy 更平缓,收敛慢很多——不是代码错,是设计不合理。
- 二分类推荐
Sigmoid+MSE或Sigmoid+BCELoss(dL/dY = (Y - target) / (Y * (1-Y))) - 多分类必须用
Softmax+CrossEntropy,否则dL/dY会引入额外数值不稳定项 - 别在
forward里直接算 loss,loss 应独立调用,backward只接收dL/dY
用 std::valarray 或轻量 tensor 类比裸循环更可靠
纯 for 循环写梯度更新容易索引越界、符号写反、漏除 batch size。与其反复调试下标,不如用 std::valarray 做基础容器(支持逐元素运算、切片、apply),或者抄一段 50 行以内的 Tensor 类(只管 shape + data + 两个 operator+= 和 *=)。
例如 ReLU::backward 只需一行:return grad_output * (input > 0); —— 这里 input 和 grad_output 都是 valarray<double></double>,> 返回布尔数组,* 自动广播。比手写 for 快且不易错。
- 避免用
std::vector实现自动广播;它不支持v1 * v2按元素相乘(除非自己重载) - 如果不用第三方库,
valarray是标准库里最接近“张量语义”的选择,slice和shift能简化一些操作 - 别过早优化:先让单样本(batch_size=1)反向跑通,再扩展到 batch;否则维度错位问题会掩盖逻辑错误
最常被跳过的细节是梯度清零 —— 每次 backward 前,Linear::dW 和 db 必须置零,否则上一轮残留梯度会叠加进来。这不是理论问题,是实操中十次有八次训练不收敛的根源。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











