前向传播的核心是矩阵乘加与激活函数组合:先计算 $ z = wx + b $,再应用非线性激活函数(如relu或sigmoid),需注意维度匹配、浮点精度、权重初始化(如xavier)及数值稳定性。

前向传播的核心就是矩阵乘加 + 激活函数
前向传播不是黑箱,本质就是两步:对上一层输出 X 做 W * X + b(线性变换),再套一层 relu 或 sigmoid(非线性变换)。这一步必须用浮点运算,别用 int 存权重或输入,否则梯度全为 0。
常见错误是把维度搞反:W 应该是 [output_size][input_size],而输入 X 是列向量([input_size][1]),才能做 W * X。OpenCV 的 Mat 默认行优先,但矩阵乘法要求严格对齐,建议统一用 Eigen::MatrixXf,它默认列主序更贴合数学习惯。
-
W初始化不能全零——会导致所有神经元输出相同,梯度坍缩;推荐用 Xavier 初始化:weight = Eigen::MatrixXd::Random(out, in) * sqrt(2.0 / in) - 偏置
b可初始化为 0,但要确保和W * X结果维度一致(即[output_size][1]) - 激活函数别手写除法或指数——
sigmoid(x) = 1.0 / (1.0 + exp(-x))在x > 88时会溢出,改用std::tanh近似或分段处理
用 Eigen 实现 layer.forward() 最简写法
不用 OpenCV 或自定义矩阵类,Eigen 足够轻量且支持表达式模板优化。一个典型层的前向函数长这样:
Eigen::VectorXf Layer::forward(const Eigen::VectorXf& x) {
Eigen::VectorXf z = W * x + b;
return z.array().max(0.0f); // relu: x > 0 ? x : 0
}
注意三点:
-
z.array()是关键——它把矩阵转成数组模式才能逐元素操作;直接写z.max(0)是错的,那是求整个向量最大值 - 别在循环里反复 new/delete 向量——前向传播高频调用,用成员变量缓存中间结果(如
z和output)能省下 20%+ 时间 - 如果输入是 batch 数据(多样本),就用
Eigen::MatrixXf,列代表样本,此时W * X仍成立,但激活要写成X.colwise().maxCoeff(0.0f)或用.unaryExpr()
为什么 relu 的 C++ 实现容易出 NaN?
不是函数逻辑错,而是数值不稳定。常见诱因:
- 权重初始化过大(比如用
rand() % 100),导致z值超过float表达范围,后续exp(z)直接爆成inf,再参与计算就变NaN - 没做输入归一化——原始图像像素值 0–255 直接送进网络,第一层
W * X就可能超限;务必在数据预处理时除以 255.0 并减均值 - 某些编译器对
-0.0f和0.0f处理不一致,relu写成(x > 0).select(x, 0)比x.max(0)更安全(Eigen 支持)
调试时加一句 assert(!std::isnan((z.array() 能快速定位爆炸点。
多层串联时,中间结果怎么传?
别用全局变量或裸指针,易内存泄漏且难调试。推荐两种方式:
- 每层持有自己的
output和input_grad(反向用),前向时只读上层output,写自己output——这是最清晰的职责分离 - 如果追求极致性能且层数固定,可把所有中间结果 flat 成一个大
std::vector<float></float>,用 offset 管理各层起始位置;但修改层结构就得重算 offset,适合部署阶段而非开发期
最容易被忽略的是:前向传播本身不保存梯度,但必须保证每个 output 在反向传播开始前还活着。如果某层 output 是临时局部变量,反向时读到的就是垃圾值——这点在用 RAII 管理资源时尤其要检查生命周期。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











