这里保留的是一份手写实现的学习用代码:从零推导并编码前向传播与反向传播,用它拼出一个最小可用的神经网络。 同一思路的通用化实现,已在我的 modforge 仓库的
deep_learning模块中独立演进(基于 Tensor 的任意多层全连接与 CNN,激活 / 损失 / 优化器均可插拔)。 本仓库保留最初的原型代码,仅作记录,不再更新。
一个用 C++ 手写的极简神经网络,任务是拟合浮点数加法 y = a + b。
没有用任何深度学习框架:梯度是自己在纸上推导的,权重更新是自己在循环里写的,sigmoid 的导数也是手动代进去的。换句话说,本仓库的价值不在于"能算加法",而在于把神经网络训练的完整链路摊开、逐行可见。
输入层 (2) 隐层 (100, sigmoid) 输出层 (1, 线性)
a ───────────┐
├──> h[i] = σ(w1[i]·a + w2[i]·b)
b ───────────┘
y = Σ h[i] · v[i]
- 输入:2 个操作数
a、b - 隐层:100 个神经元,激活函数
σ(x) = 1 / (1 + e^(-x)) - 输出层:1 个神经元,线性加权求和(无激活)
- 参数规模:输入权重 2×100 + 隐层权重 100 = 300 个
对每个隐层神经元计算加权和,过 sigmoid;输出层对隐层激活值做线性组合:
void front(double left, double right) {
for (int i = 0; i < Nerou; ++i) {
double x = InputWeight[0][i] * left + InputWeight[1][i] * right;
HindValue[i] = 1.0 / (1 + expf(-x));
}
Output = 0;
for (int i = 0; i < Nerou; ++i)
Output += HindValue[i] * HindWeight[i];
}损失函数取 E = ½(ŷ − y)²,按链式法则手动求导:
- 隐层权重梯度:
∂E/∂v[i] = (ŷ − y) · h[i] - 输入权重梯度:
∂E/∂w[i] = (ŷ − y) · v[i] · h[i] · (1 − h[i]) · x
其中 h[i]·(1 − h[i]) 就是 sigmoid 的导数。权重按 w ← w − η·∂E/∂w 更新。
void back(double left, double right, double result) {
for (int i = 0; i < Nerou; ++i) {
d_hind[i] = (Output - result) * HindValue[i];
HindWeight[i] -= LearningRate * d_hind[i];
}
for (int i = 0; i < Nerou; ++i) {
d_input[0][i] = (Output - result) * HindWeight[i]
* HindValue[i] * (1 - HindValue[i]) * left;
InputWeight[0][i] -= LearningRate * d_input[0][i];
// ... 对 right 同理
}
}- 归一化:训练样本在
[0, 1000]区间随机生成,送入网络前统一除以 1000 缩放到[0, 1],避免 sigmoid 饱和导致梯度消失。 - 学习率调度:随训练轮数分段衰减(0.05 → 0.001 → 0.0001 → 0.00001)。
- 滑动窗口误差:用一个定长环形队列
AverageQueue统计最近若干次的平均相对误差,作为收敛判据(阈值5e-6)。 - 权重持久化:每轮训练后写入
config.txt;启动时若文件存在则直接加载,跳过训练。
cmake -B build -S .
cmake --build build
./build/Adder首次运行会先训练(无 config.txt 时),训练过程中每轮打印一次平均相对误差。
同一个 2 → 100(sigmoid) → 1 的加法器,用 modforge 的 deep_learning.bp 大约是这个样子。
cmake_minimum_required(VERSION 3.30.0)
include("/path/to/modforge/cmake/modforge-init.cmake") # 必须在 project() 之前
project(adder LANGUAGES CXX)
add_subdirectory(modforge)
target_link_libraries(adder PRIVATE modforge)modforge-init.cmake 负责打开 import std 的实验开关;也可以 find_package(modforge CONFIG REQUIRED)
链接 modforge::modforge,细节见 modforge 的 README。
import std;
import modforge;
using modforge::Vector;
// Activate 是导出的接口,可自行派生。
// Adder 的输入层与输出层都是线性的,用 Linear 才能精确复刻
// 「裸输入 -> sigmoid 隐层 -> 线性输出」这个拓扑。
struct Linear : modforge::Activate {
double action(double num) override { return num; }
double deaction(double) override { return 1.0; }
};
int main() {
// 数据集与权重初始化共用全局随机引擎,固定种子保证结果可复现
modforge::seed_random(42);
// 与 Adder 一样拟合 a + b,输入先归一化到 [0, 1]
std::vector<std::pair<Vector<float>, Vector<float>>> data;
for (int i = 0; i < 2000; ++i) {
Vector<float> in(2), out(1);
in[0] = static_cast<float>(modforge::GetRandom(0.0, 0.5));
in[1] = static_cast<float>(modforge::GetRandom(0.0, 0.5));
out[0] = in[0] + in[1];
data.emplace_back(std::move(in), std::move(out));
}
// 2 -> 100 -> 1,与 Adder 同规模、同拓扑
modforge::BP bp;
bp.add_layer(2, std::make_shared<Linear>());
bp.add_layer(100, std::make_shared<modforge::Sigmoid>());
bp.add_layer(1, std::make_shared<Linear>());
// 余弦退火:学习率在 0.05 ~ 0.00001 之间周期衰减(Adder 里是手写分段衰减)
bp.set_optimizer(std::make_shared<modforge::CosineAnnealing>(0.05, 0.00001, 300));
// 前 70% 作训练集、其余作测试集,训练 300 轮,每轮打印测试集平均相对误差
bp.train(data, 0.7f, 300, 42, true);
Vector<float> in(2);
in[0] = 0.15f;
in[1] = 0.21f;
std::cout << std::format("0.15 + 0.21 = {:.6f} (expect 0.36)\n", bp.forecast(in)[0]);
}实测(GCC trunk、关闭反射,约 80 秒):末轮测试集平均相对误差 0.004633,
0.15 + 0.21 输出 0.361249。这里只跑了 300 轮示范用法,没有追到 Adder 的 5e-6 阈值。
这是一份练手与验证性质的代码,未做工程化打磨(全局变量、固定网络规模、硬编码超参、训练/推理耦合在同一个 main.cpp 里)。
见 LICENSE。