一、这玩意儿到底是干嘛的?
说白了,这是一个完全用C++标准库(STL)写的基础神经网络。没有PyTorch,没有TensorFlow,没有Eigen,甚至连OpenCV都不需要——就靠着 vector、string、fstream 这些STL里的老熟人,搭出了一个能跑的三层神经网络。
它能干啥呢?在这个项目里,它用来分类小麦种子的品种。数据集里记录了210颗种子的7个几何特征(比如面积、周长、 compactness 之类的),要判断这颗种子属于3个品种中的哪一个。训练完之后,准确率能到95%左右。
当然,这个网络本身不挑任务。你把CSV数据换成别的分类问题(比如鸢尾花、手写数字),改改输入输出维度,它照样能跑。
二、为什么要自己手写一个?
现在市面上已经有PyTorch、TensorFlow这些成熟框架了,一行代码就能搭个神经网络,为什么还要自己用C++从零写?
原因很实在:
第一,搞懂原理。 很多人用PyTorch调包调得飞起,但问到"反向传播到底怎么把误差传回隐藏层的"、"Sigmoid的导数为什么是 output * (1 - output) ",就答不上来了。自己手写一遍,这些细节就全明白了。
第二,C++性能。 如果你要在嵌入式设备、游戏引擎、或者对延迟敏感的场景里跑神经网络,Python+PyTorch的启动开销和内存占用可能太大了。纯C++实现更轻量。
第三,面试/学习。 这是很多计算机专业课程的经典作业。手写一个神经网络,能同时练到面向对象编程(类设计)、向量运算、文件IO、随机数生成等基本功。
三、设计思路:三个类搭起一座桥
整个代码的架构非常清晰,就三个类,层层递进:
3.1 Neuron(神经元)—— 最小的砖块
每个神经元里存了四样东西:
m_weights[]:权重数组。连向这个神经元的每条线都有一个权重,最后一个是偏置(bias)
m_activation:激活值。输入乘权重加偏置之后的求和结果
m_output:输出值。激活值经过Sigmoid函数压缩到0~1之间
m_delta:误差项。反向传播时算出来的,用来更新权重
神经元会干三件事:
activate(inputs):加权求和,算激活值
transfer():把激活值丢进Sigmoid,得到输出
transfer_derivative():返回Sigmoid的导数,反向传播要用
3.2 Layer(层)—— 把神经元攒成一堆
层的职责很单纯,就是管理一堆神经元。构造函数里根据你指定的神经元数量和权重数量,创建对应数量的Neuron对象。
3.3 Network(网络)—— 把层串起来,管训练
网络是老大,它手里有一堆层,负责把整个流程串起来:
initialize_network():按你指定的输入数、隐藏层神经元数、输出数,搭好网络结构
forward_propagate():前向传播,数据从输入层走到输出层
backward_propagate_error():反向传播,误差从输出层传回隐藏层
update_weights():根据算出来的误差,更新所有权重
train():把上面三步包起来,循环跑很多轮
predict():前向传播一次,看哪个输出最大,就预测为哪一类
这种设计的好处是职责分明:Neuron只管自己的数学运算,Layer只管攒神经元,Network管大局。想改激活函数?去Neuron里改。想加层?去Network里加。结构很干净。
四、核心算法原理:前向、反向、更新
4.1 前向传播:数据怎么从输入走到输出
前向传播很简单,就是一层一层地算:
对于每一层: 对于这一层的每个神经元: 激活值 = 偏置 + Σ(输入 × 权重) 输出 = Sigmoid(激活值) 把这一层的输出,作为下一层的输入Sigmoid函数长这样:
Sigmoid(x) = 1 / (1 + e^(-x))它能把任意实数压缩到0到1之间。输出层有3个神经元,每个输出一个0~1之间的数,代表"属于这个类别的概率"。哪个数最大,就预测为哪一类。
4.2 反向传播:误差怎么一层层传回去
这是神经网络最核心的部分,也是很多人最迷糊的地方。其实原理不难,就是链式法则的应用。
输出层的delta怎么算?
输出层直接和正确答案比:
delta = (expected - output) × output × (1 - output)前半部分 (expected - output) 是误差,后半部分 output × (1 - output) 是Sigmoid的导数。两者相乘,得到这个神经元对总误差的"贡献度"。
隐藏层的delta怎么算?
隐藏层没有直接和正确答案比,但它会影响输出层。所以隐藏层的delta要把下一层的误差"分摊"回来:
delta = Σ(下一层神经元的权重 × 下一层神经元的delta) × output × (1 - output)大白话就是:隐藏层某个神经元连向输出层的几条线,每条线都"背"了一部分输出层的误差。把这几条线背的误差加起来,再乘上自己的Sigmoid导数,就是自己的delta。
4.3 权重更新:怎么让网络越学越好
有了delta,就可以更新权重了:
weight += learning_rate × delta × inputbias += learning_rate × delta大白话:误差越大、输入越大,这个权重改得就越多。learning_rate(学习率)控制每次改多少,太大容易震荡,太小学得太慢。这个项目里设的是0.3。
4.4 训练循环:随机梯度下降
训练就是反复做三件事:
取一个样本,前向传播算预测
和正确答案比,算误差
反向传播算delta,更新权重
把所有样本过一遍,叫一个 epoch。这个项目跑了500个epoch。每轮结束会把总误差打印出来,你应该能看到误差越来越小——这就是网络在"学习"的证据。
五、数据预处理:让网络学得更舒服
原始数据不能直接喂给网络,需要做两件事:
5.1 归一化(Normalization)
种子数据里,有的特征值是十几,有的是零点几,量级差很多。如果不处理,量级大的特征会"欺负"量级小的特征,网络学偏。
所以代码里做了Min-Max归一化:
归一化后的值 = (原始值 - 该列最小值) / (该列最大值 - 该列最小值)这样所有特征都被压到0~1之间,大家平起平坐。
5.2 One-Hot编码
标签是1、2、3,代表三个品种。但神经网络输出的是3个概率值,所以要把标签转成One-Hot形式:
品种1 → [1, 0, 0]品种2 → [0, 1, 0]品种3 → [0, 0, 1]这样输出层的3个神经元就能和标签一一对应了。
六、评估:5折交叉验证
怎么知道网络学得好不好?代码里用了5折交叉验证:
把210条数据随机分成5份
每次拿4份训练,1份测试
轮流测试5次,确保每条数据都被测过一次
最后算5次准确率的平均值
这样做的好处是结果更稳定。如果只用一份固定的测试集,万一那部分数据特别简单或特别难,准确率就不可信了。交叉验证能避免这种运气成分。
七、相关领域知识点速览
知识点 | 一句话解释 |
| Sigmoid | 把任意数压到0~1的"S型"函数,适合做概率输出 |
| 反向传播(Backpropagation) | 用链式法则把输出误差逐层传回,算每个权重该改多少 |
| 梯度下降 | 沿着误差下降最快的方向调整权重,让损失越来越小 |
| 学习率 | 每次更新权重的步长,太大容易震荡,太小收敛慢 |
| 随机梯度下降(SGD) | 每次用一个样本更新权重,比批量梯度下降更快、更省内存 |
| Min-Max归一化 | 把数据压到0~1,消除不同特征之间的量级差异 |
| One-Hot编码 | 把类别标签转成二进制向量,适配多分类输出 |
| 交叉验证 | 把数据分多份轮流测试,评估结果更可靠 |
| 偏置(Bias) | 给神经元一个"基准值",让决策边界可以平移 |
八、怎么跑起来?编译和运行手把手教
8.1 环境要求
C++14 编译器(g++ 8.1.0 或更高,或 Visual Studio 2019+)
不需要任何第三方库,纯STL就行
8.2 文件结构
你需要这几个文件放在同一个目录下:
项目目录/├── main.cpp├── NeuralNetwork.cpp├── NeuralNetwork.h└── seeds_dataset.csv8.3 编译
命令行编译(g++):
g++ -Wall -Wpedantic main.cpp NeuralNetwork.cpp -o nn8.4 运行
./nn程序会自动:
读取 seeds_dataset.csv
归一化数据
做5折交叉验证
每折训练500个epoch,打印误差
最后输出平均准确率
正常输出大概长这样:
[>] epoch=495, l_rate=0.3, error=5.91069[>] epoch=496, l_rate=0.3, error=5.82714[>] epoch=497, l_rate=0.3, error=5.81054[>] epoch=498, l_rate=0.3, error=5.79397[>] epoch=499, l_rate=0.3, error=5.74445Mean accuracy: 95.2381误差从几十慢慢降到个位数,准确率稳定在90%以上,说明网络学到位了。
8.5 自己调参数玩
代码里这几个参数可以改:
int n_folds = 5; // 交叉验证折数float l_rate = 0.3f; // 学习率int n_epoch = 500; // 训练轮数int n_hidden = 5; // 隐藏层神经元数你可以试试:
把 n_hidden 改成3或10,看准确率怎么变
把 l_rate 改成0.1或0.5,看收敛速度
把 n_epoch 改成100或1000,看过拟合还是欠拟合
8.6 换成自己的数据
如果你想用这个网络跑自己的分类任务:
准备CSV文件,最后一列是类别标签,前面全是特征
把文件放到程序目录,改 load_csv_data("你的文件.csv")
根据你的特征数和类别数,调整网络初始化参数
九、代码里还藏了个"小demo"
main.cpp 底部有一段被注释掉的代码,解开注释可以跑一个超小的二分类demo:
// 10条数据,2个输入,1个输出(0或1)std::vector<std::vector<float>> traindata { {2.78, 2.55, 0}, {1.47, 2.36, 0}, // ... {7.63, 2.76, 1},};这个demo只有2个输入、2个隐藏神经元、2个输出,训练50轮就能看懂网络是怎么一步步学出来的。适合第一次看代码的时候用来调试和理解。
If you need the complete source code, please add the WeChat number (c17865354792)
十、总结
这个项目虽然代码量不大,但把神经网络的核心机制都覆盖到了:
面向对象设计:Neuron → Layer → Network,层层封装,结构清晰
完整的前向传播:加权求和 + Sigmoid激活
标准的反向传播:从输出层到隐藏层,逐层计算delta
梯度下降更新:根据误差调整权重,让网络越学越好
数据预处理:归一化 + One-Hot编码,让网络学得舒服
交叉验证评估:5折验证,结果可靠
如果你一直好奇"神经网络底层到底怎么工作的",但又不想啃PyTorch几十万行源码,这就是一个很好的切入点。代码量可控、逻辑清晰、能跑通、有结果,读完之后你会对"反向传播"四个字有完全不同的理解。
---
Welcome to follow WeChat official account【程序猿编码】
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!