零依赖纯手写:C++ 实现完整神经网络,张量反向传播全打通

一、项目核心释义

这是一套完全从零实现的基础神经网络库,全程采用纯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 && "张量维度不匹配");
    }
};

代码讲解:

  • 同时存储datagrad,前向计算数值,反向计算梯度,不用分开创建两个张量
  • 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

自动执行完整流程:

  1. 生成手写数字数据集
  2. 训练神经网络
  3. 导出权重、损失历史、预测数据到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【程序猿编码

相关推荐
码匠许师傅1 小时前
【C++三方组件】utfcpp:UTF-8 字符串处理避坑
c++
蒸蒸yyyyzwd1 小时前
八股学习 MVCC 慢查询优化和 read 过程
c++·八股
APEX真好玩1 小时前
把高频传感器(IMU)数据封装进 MP4:FFmpeg 私有数据轨实践
c++
晴天的雨.9922 小时前
[C++算法]盛最多水的容器(双指针算法)
开发语言·c++·算法
TAN-90°-2 小时前
Deep Learning for Computer Vision——Generative Models 1
人工智能·深度学习·神经网络·目标检测·机器学习·计算机视觉
布莱克6052 小时前
理解内存泄漏:成因、发现方法与解决策略(C++ 举例)
c语言·c++·内存泄漏
此生决int3 小时前
深入理解C++系列(19)——C++11(上)
开发语言·c++
郝学胜-神的一滴3 小时前
C++20模板元编程 05:吃透变参模板,解锁编译期万能参数能力
服务器·开发语言·c++·windows·vscode
C++ 老炮儿的技术栈12 小时前
我们在设计tcp协议时,要传一个字符串过去,报文:头十长度十内容,是否要把‘\0‘也填入,长度是否包含‘\0‘
开发语言·数据结构·c++·mfc·c