一、项目核心释义
这是一套完全从零实现的基础神经网络库,全程采用纯C++编写,不依赖任何外部机器学习框架,完整覆盖多维张量运算、模块化网络层、前向传播、手动反向传播、损失函数计算全链路。
项目配套程序化生成的手写数字数据集,支持笔画抖动、旋转、形变、笔宽变化等数据增强策略,训练完成后可自动导出权重与训练数据到前端网页,实现浏览器端的实时推理可视化,完整呈现从底层矩阵运算到前端推理验证的全流程。
核心特性:
- 零依赖纯C++实现:所有矩阵运算、网络层、训练逻辑全部手写,不引入任何第三方机器学习库
- 完整训练链路:前向传播→损失计算→反向传播→权重更新,全流程手动实现
- 数值稳定优化:Softmax与交叉熵损失采用log-sum-exp技巧,避免数值溢出与下溢
- 程序化数据集:自动生成0-9手写风格数字,内置多种数据增强,训练效果稳定
- 前端可视化推理:训练权重直接导出为JS文件,网页端实时画图推理,验证训练效果
二、行业核心技术知识点
2.1 从零实现神经网络的工程价值
很多开发者用框架只会调参,对底层原理一知半解。从零手写神经网络的核心价值在于:
- 彻底理解张量运算、梯度传播、权重更新每一步的数学逻辑
- 没有框架黑盒,每一行代码都可控,可针对特定场景深度优化
- 体积极小,可嵌入到嵌入式设备、离线工具,不需要任何运行时依赖
- 是学习深度学习底层原理的最佳实践,比单纯调用框架更能掌握本质
2.2 张量:神经网络的基础数据结构
所有神经网络的计算,本质上都是张量运算。张量就是带形状信息的多维数组,同时存储前向值和反向梯度。
- 支持多维:标量、向量、矩阵、更高维都能统一表示
- 形状检查:运算前自动校验维度匹配,避免维度错误
- 梯度伴随:每个张量同时存前向值和反向梯度,反向传播直接调用
2.3 反向传播:手工推导的梯度链
反向传播是神经网络训练的核心,本质是链式法则层层反向传递梯度。手工实现反向传播的关键点:
- 每一层都要实现前向计算和反向梯度计算两个方法
- 损失层先算损失对输出的梯度,然后一层层往回传递
- 线性层、激活层各有对应的梯度公式,必须严格对应数学推导
- 最后梯度同步到权重和偏置,再按学习率统一更新
2.4 数值稳定性:深度学习的隐形坑
新手写神经网络最容易踩的坑就是数值不稳定:Softmax输入值大了指数溢出,交叉熵概率接近0的时候对数负无穷。
行业通用的解决技巧:
- Softmax最大值平移:每个值减去最大值再算指数,数学上完全等价,避免数值溢出
- LogSumExp技巧:交叉熵不用先算Softmax再取对数,直接用LogSumExp公式计算,数值更稳定
- Xavier初始化:权重按输入输出维度缩放初始化,避免前向值过大或过小
2.5 程序化数据集:不用下载数据也能训练
很多入门项目都要下载外部数据集,麻烦还容易出环境问题。程序化生成数据集的优势:
- 自动生成手写风格数字,不用下载任何外部数据
- 内置数据增强:抖动、旋转、挤压、笔宽变化、噪声,提升泛化能力
- 自动做MNIST式归一化:裁剪边界→缩放20px→质心居中,和标准处理流程一致
- 前端推理也用完全相同的归一化逻辑,保证训练和推理效果一致
三、整体架构设计思路
3.1 五层模块化架构
┌───────────────────────────────────────────────────────┐
│ 应用与可视化层 │
│ 网页可视化 / 实时推理 / 训练曲线展示 │
└──────────────────┬───────────────────────────────────┘
│
┌───────────────────────────────────────────────────────┐
│ 数据导出层 │
│ 权重导出 / 损失历史导出 / 预测矩阵导出 │
└──────────────────┬───────────────────────────────────┘
│
┌───────────────────────────────────────────────────────┐
│ 模型训练层 │
│ 前向传播 / 损失计算 / 反向传播 / 权重更新 │
└──────────────────┬───────────────────────────────────┘
│
┌───────────────────────────────────────────────────────┐
│ 网络层抽象 │
│ Linear层 / ReLU激活 / Softmax激活 / 损失层 │
└──────────────────┬───────────────────────────────────┘
│
┌───────────────────────────────────────────────────────┐
│ 基础张量层 │
│ 多维张量 / 形状检查 / 数值运算 / 梯度存储 │
└───────────────────────────────────────────────────────┘
各层职责:
- 基础张量层:最底层,封装多维数组、形状管理、基础运算、梯度存储
- 网络层抽象:所有层统一接口,各自实现前向与反向计算
- 模型训练层:调度层层前向反向,计算损失,更新权重
- 数据导出层:把训练好的权重、损失、预测导出成前端可用的格式
- 应用与可视化层:网页端展示结构、训练曲线、实时画板推理
3.2 核心设计原则
- 零外部依赖:全部用C++标准库实现,不引入任何第三方机器学习库
- 接口统一:所有网络层继承同一个基类,前向反向方法统一
- 数值稳定优先:所有指数、对数运算都做数值稳定处理
- 训练推理一致:数据集和前端推理用完全一样的归一化逻辑
- 模块化可扩展:加新的层、新的损失函数只需要加子类,不用修改原有代码
四、核心代码实现原理
4.1 Tensor张量类:基础数据结构
张量类是整个库的基石,封装多维数据、形状信息、梯度存储,提供基础运算能力。
核心结构定义:
cpp
class Tensor {
public:
std::vector<int> shape; // 张量形状,例如 {784}、{128, 784}
std::vector<float> data; // 前向传播的数值
std::vector<float> grad; // 反向传播的梯度
// 构造函数:按形状分配内存
explicit Tensor(const std::vector<int>& shape)
: shape(shape), data(num_elements()), grad(num_elements(), 0.0f) {}
// 计算元素总数
int num_elements() const {
int total = 1;
for (int s : shape) total *= s;
return total;
}
// 形状检查:运算前校验维度匹配
void check_shape(const Tensor& other) const {
assert(shape == other.shape && "张量维度不匹配");
}
};
代码讲解:
- 同时存储
data和grad,前向计算数值,反向计算梯度,不用分开创建两个张量 shape做维度校验,矩阵乘法、逐元素运算前都检查,避免维度错误- 用连续
vector存储数据,内存连续,计算效率高,也方便批量导出
4.2 模块化网络层:统一接口
所有网络层继承同一个基类,都实现前向传播和反向传播两个方法,模型可以统一调度。
基类定义:
cpp
class Layer {
public:
virtual ~Layer() = default;
virtual Tensor forward(const Tensor& input) = 0; // 前向传播
virtual Tensor backward(const Tensor& out_grad) = 0; // 反向传播
virtual void update_weights(float lr) {} // 可选:更新权重
};
4.2.1 Linear全连接层
最核心的计算层,权重和偏置采用Xavier初始化,前向做矩阵乘法加偏置,反向传梯度并计算权重梯度。
核心实现:
cpp
class Linear : public Layer {
Tensor weight_; // 形状 {out_dim, in_dim}
Tensor bias_; // 形状 {out_dim}
Tensor input_cache_; // 缓存前向输入,反向要用
public:
Linear(int in_dim, int out_dim)
: weight_({out_dim, in_dim}), bias_({out_dim}) {
// Xavier初始化:根据输入输出维度缩放,保证数值稳定
float scale = std::sqrt(2.0f / (in_dim + out_dim));
for (auto& w : weight_.data) {
w = random_normal() * scale;
}
}
Tensor forward(const Tensor& input) override {
input_cache_ = input;
// 输出形状 {batch, out_dim}
Tensor output({input.shape[0], weight_.shape[0]});
// 矩阵乘法 + 偏置
matmul(input, weight_, output);
add_bias(output, bias_);
return output;
}
Tensor backward(const Tensor& out_grad) override {
// 累加权重梯度和偏置梯度
matmul_weight_grad(out_grad, input_cache_, weight_.grad);
sum_batch(out_grad, bias_.grad);
// 计算输入梯度,往回传递
Tensor in_grad = matmul_back(out_grad, weight_);
return in_grad;
}
void update_weights(float lr) override {
// 按学习率更新权重和偏置
for (size_t i = 0; i < weight_.data.size(); ++i) {
weight_.data[i] -= lr * weight_.grad[i];
weight_.grad[i] = 0.0f;
}
}
};
代码讲解:
- 权重用Xavier初始化,避免前向值过大梯度消失,或者过小梯度爆炸
- 前向做矩阵乘法加偏置,是网络最核心的计算部分
- 反向同时计算权重梯度、偏置梯度、输入梯度,权重梯度累加,训练结束统一清零
4.2.2 ReLU激活层
非线性激活函数,引入非线性变换,让网络能拟合复杂函数。反向就是大于0的地方梯度直接传递,小于0的地方梯度截断。
cpp
class ReLU : public Layer {
Tensor input_cache_;
public:
Tensor forward(const Tensor& input) override {
input_cache_ = input;
Tensor output = input.empty_like();
for (int i = 0; i < input.num_elements(); ++i) {
output.data[i] = std::max(0.0f, input.data[i]);
}
return output;
}
Tensor backward(const Tensor& out_grad) override {
Tensor in_grad = out_grad.empty_like();
for (int i = 0; i < out_grad.num_elements(); ++i) {
// 输入大于0的地方梯度直接传,否则为0
in_grad.grad[i] = input_cache_.data[i] > 0 ? out_grad.grad[i] : 0.0f;
}
return in_grad;
}
};
4.2.3 Softmax激活层(数值稳定版)
把输出转换成概率分布,采用最大值平移技巧保证数值稳定,不会出现指数溢出。
cpp
class Softmax : public Layer {
public:
Tensor forward(const Tensor& input) override {
Tensor output = input.empty_like();
int class_num = input.shape.back();
for (int b = 0; b < input.shape[0]; ++b) {
// 1. 找最大值,用于平移避免指数溢出
float max_val = input.data[b * class_num];
for (int i = 1; i < class_num; ++i) {
max_val = std::max(max_val, input.data[b * class_num + i]);
}
// 2. 每个值减最大值再算指数
float sum = 0.0f;
for (int i = 0; i < class_num; ++i) {
float e = std::exp(input.data[b * class_num + i] - max_val);
output.data[b * class_num + i] = e;
sum += e;
}
// 3. 归一化得到概率
for (int i = 0; i < class_num; ++i) {
output.data[b * class_num + i] /= sum;
}
}
return output;
}
};
代码讲解:
- 每个样本先减最大值再算指数,数学上和原Softmax完全等价,但不会数值溢出
- 避免了输入值很大的时候exp爆炸,数值稳定性大幅提升
- 分类任务最后一层用Softmax,输出就是各个类别的概率
4.3 交叉熵损失:LogSumExp数值稳定版
分类任务采用交叉熵损失,配合Softmax一起实现,用LogSumExp技巧,不用先算Softmax再取对数,数值更稳定。
核心实现:
cpp
class CrossEntropyLoss {
public:
// 计算损失,同时把梯度写回logits.grad
float compute(Tensor& logits, const std::vector<int>& labels) {
int batch = logits.shape[0];
int classes = logits.shape[1];
float total_loss = 0.0f;
for (int b = 0; b < batch; ++b) {
// 1. 找最大值
float max_val = logits.data[b * classes];
for (int i = 1; i < classes; ++i) {
max_val = std::max(max_val, logits.data[b * classes + i]);
}
// 2. LogSumExp:log(sum(exp(x - max))) + max
float log_sum_exp = 0.0f;
for (int i = 0; i < classes; ++i) {
log_sum_exp += std::exp(logits.data[b * classes + i] - max_val);
}
log_sum_exp = std::log(log_sum_exp) + max_val;
// 3. 正确类别的log概率 = 正确类logit - log_sum_exp
float log_prob = logits.data[b * classes + labels[b]] - log_sum_exp;
total_loss += -log_prob;
// 4. 直接算梯度:softmax概率 - 正确类为1
for (int i = 0; i < classes; ++i) {
float p = std::exp(logits.data[b * classes + i] - log_sum_exp);
logits.grad[b * classes + i] = (p - (i == labels[b] ? 1.0f : 0.0f)) / batch;
}
}
return total_loss / batch;
}
};
代码讲解:
- 用LogSumExp技巧直接从logits算损失,不用中间Softmax,减少一步计算,数值更稳定
- 反向梯度直接在内部计算完成,softmax概率减去正确类的1,就是交叉熵对logits的梯度
- 除以batch做平均,保证损失大小和batch大小无关
4.4 程序化数字数据集生成
不用下载外部数据集,程序自动生成手写风格数字,内置多种数据增强。
核心生成逻辑:
cpp
std::vector<Sample> make_digit_dataset(int samples_per_class) {
std::vector<Sample> dataset;
for (int digit = 0; digit < 10; ++digit) {
for (int i = 0; i < samples_per_class; ++i) {
// 1. 生成笔画骨架数字
cv::Mat img = draw_stroke_digit(digit);
// 2. 数据增强:抖动、旋转、挤压、笔宽、噪声
augment(img,
random_jitter(), // 控制点抖动
random_rotation(), // ±10度旋转
random_squeeze(), // 宽高挤压
random_pen_width() // 笔宽变化
);
// 3. MNIST式归一化:裁剪→缩放20px→质心居中
normalize_mnist_style(img);
// 4. 展平成784维向量
Tensor vec = flatten(img);
dataset.push_back({vec, digit});
}
}
return dataset;
}
讲解:
- 先生成基础笔画数字,然后做各种增强,提升模型泛化能力
- 归一化方式和MNIST完全一致,训练出来的模型通用性强
- 前端推理也用完全一样的归一化,保证训练和推理效果一致
4.5 模型训练完整流程
模型类调度所有层,完成前向、损失、反向、更新完整训练流程。
核心训练循环:
cpp
class Model {
std::vector<std::unique_ptr<Layer>> layers_;
CrossEntropyLoss loss_fn_;
public:
void add_layer(std::unique_ptr<Layer> layer) {
layers_.push_back(std::move(layer));
}
float train_step(const Tensor& x, const std::vector<int>& labels, float lr) {
// 1. 前向传播
Tensor out = x;
for (auto& layer : layers_) {
out = layer->forward(out);
}
// 2. 计算损失,梯度直接写回out.grad
float loss = loss_fn_.compute(out, labels);
// 3. 反向传播,从最后一层往回传
Tensor grad = out;
for (int i = layers_.size() - 1; i >= 0; --i) {
grad = layers_[i]->backward(grad);
}
// 4. 更新所有权重和偏置
for (auto& layer : layers_) {
layer->update_weights(lr);
}
return loss;
}
};
代码讲解:
- 前向逐层计算,反向从后往回传递,标准的反向传播流程
- 损失函数直接把梯度写在输出张量的grad里,不用额外返回
- 最后统一更新所有权重,按学习率减去梯度
4.6 权重导出:对接前端可视化
训练完成后把权重、损失历史、预测矩阵导出成JS文件,网页可以直接加载使用。
核心导出逻辑:
cpp
void export_nn_data(const Model& model, const std::vector<float>& loss_history) {
std::ofstream f("nn_data.js");
f << "const nn_data = {\n";
// 导出各层权重
f << " weights: [";
for (auto& layer : model.layers()) {
f << layer->export_weights() << ",";
}
f << "],\n";
// 导出损失历史
f << " loss_history: [";
for (float l : loss_history) f << l << ",";
f << "],\n";
f << "};\n";
}
讲解:导出成JS常量,网页直接引用,不用后端服务,打开HTML就能运行推理。
五、环境配置与运行全教程
5.1 环境要求
- 编译器:支持C++11及以上的C++编译器(GCC、Clang、MSVC均可)
- 构建工具:GNU Make
- 浏览器:任意现代浏览器,用于查看可视化与实时推理
- 不需要任何第三方机器学习库、不需要Python环境
5.2 编译构建
make
直接make即可编译,生成训练程序。
5.3 运行训练
make run
自动执行完整流程:
- 生成手写数字数据集
- 训练神经网络
- 导出权重、损失历史、预测数据到
nn_data.js
训练完成会输出准确率,通常干净数字接近100%,增强样本也接近100%,约10万参数训练不到一秒。
5.4 前端可视化
直接用浏览器打开index.html,即可看到:
- 神经网络结构动画图
- 训练损失下降曲线
- 数字样本图库
- 10类预测概率热力图
- 实时画板:鼠标画数字,实时运行前向传播识别结果
前端完全用原生JS实现矩阵运算,和C++训练的权重完全一致,推理结果完全相同。
5.5 常见调整
- 修改学习率:在训练代码里修改lr参数
- 修改网络结构:增减网络层、修改每层维度
- 修改数据增强:调整增强幅度、增强种类
- 修改训练轮数:修改训练循环的epoch数
六、落地用途与场景
6.1 深度学习入门教学
非常适合高校、培训班做深度学习入门教学,从张量运算到反向传播,每一步都有对应代码,没有框架黑盒,学习者能真正理解底层原理。
6.2 嵌入式轻量推理
纯C++实现,体积极小,零依赖,可以嵌入到嵌入式设备、单片机,做简单的分类、识别任务,不需要操作系统和重型框架。
6.3 离线轻量工具
可以做成离线的小工具,执行简单的图像识别、分类任务,不需要网络、不需要安装Python环境,开箱即用。
6.4 课程实验与作业
作为深度学习课程的实验项目,从零实现、训练、可视化全链路,比直接调用框架的作业更有价值,能真正考察学生的理解程度。
6.5 定制小模型快速落地
有特定小分类需求的场景,可以基于这个库快速定制训练,不用引入重型框架,部署简单,可控性强。
If you need the complete source code, please add the WeChat number (c17865354792)
七、总结
这套从零构建的纯C++神经网络库,用最少的代码,完整实现了从底层张量运算、网络层、反向传播到训练可视化的全流程,没有任何第三方机器学习框架依赖。
它最大的价值不是性能有多高,而是把深度学习的黑盒完全打开,每一步计算都清晰可见,是学习底层原理、做嵌入式部署、定制轻量模型的绝佳参考实现。
Welcome to follow WeChat official account【程序猿编码】