在深度学习框架百花齐放的今天,TensorFlow、PyTorch等工具极大地降低了模型开发的门槛。然而,对于希望深入理解神经网络底层运作机制、追求极致性能或需要在资源受限环境中部署模型的C++开发者而言,从零构建一个轻量级的机器学习库是一次极具价值的挑战。本文将带你从零开始,用纯C++手搓一个支持基础神经网络(如前馈网络、CNN)的微型机器学习库。我们将从最基础的张量操作开始,逐步实现自动微分、计算图、优化器,最终搭建并训练一个简单的分类模型。整个过程不仅是对C++面向对象、模板编程和内存管理的综合实践,更是对机器学习核心原理的深度剖析。
1. 背景与核心概念:为什么用C++手搓机器学习库?
在深入代码之前,我们首先要明确几个核心概念以及本次实践的价值所在。
机器学习库的核心组件通常包括:
- 张量(Tensor):数据的基本容器,是多维数组的泛化。它是所有计算的基础。
- 计算图(Computational Graph):用于描述计算过程的有向无环图(DAG)。节点代表运算(操作),边代表张量数据流。它使得自动微分成为可能。
- 自动微分(Automatic Differentiation, Autodiff):库的核心魔法。它能够自动计算复合函数的导数,是神经网络反向传播(Backpropagation)得以实现的关键。
- 层(Layer)与激活函数(Activation Function):神经网络的基本构建块,如全连接层、卷积层、ReLU、Sigmoid等。
- 优化器(Optimizer):用于更新网络参数以最小化损失函数的算法,如随机梯度下降(SGD)。
为何选择纯C++?
- 深入理解:抛开高级框架的封装,亲手实现每一个环节,能让你对反向传播、梯度计算、参数更新有刻骨铭心的理解。
- 性能控制:C++允许进行精细的内存管理和算法优化,对于追求低延迟、高吞吐量的推理场景至关重要。
- 轻量与嵌入:最终生成的库可以非常轻量,无需依赖庞大的Python生态系统,易于集成到C++项目或嵌入式设备中。
- 技能提升:这是对C++模板、运算符重载、智能指针、多态等高级特性的绝佳练兵场。
本文的目标不是构建一个与PyTorch媲美的工业级框架,而是打造一个教学级、可运行、模块清晰的迷你库,我们称之为MicroTorch。
2. 环境准备与版本说明
本项目对环境的依赖极简,重点在于C++语言本身。
- 操作系统:Windows (MSVC), Linux (GCC/Clang), macOS (Clang) 均可。
- 编译器:支持C++17标准的编译器。这是硬性要求,因为我们会用到
std::variant,std::optional,std::shared_ptr的模板特性等。- GCC 7 或更高版本
- Clang 5 或更高版本
- MSVC (Visual Studio 2017 或更高版本)
- 构建工具:推荐使用CMake(3.10+) 来管理项目,但为了最简化,我们也可以直接使用命令行编译。
- IDE/编辑器:任何你熟悉的工具即可,如 VSCode (配置C++环境)、CLion、Visual Studio。
- 第三方库:纯C++标准库,零外部依赖。数学运算使用
<cmath>和<functional>。
项目结构预览: 在开始前,我们先规划好目录结构,这有助于模块化管理。
microtorch/ ├── include/microtorch/ # 头文件 │ ├── tensor.hpp │ ├── autograd.hpp │ ├── ops.hpp │ ├── nn/ # 神经网络模块 │ │ ├── module.hpp │ │ ├── linear.hpp │ │ └── functional.hpp # 激活函数等 │ └── optim/ # 优化器模块 │ └── sgd.hpp ├── src/ # 源文件(如果头文件不是纯模板) │ └── ... (可能为空,或放一些实现) ├── examples/ # 示例代码 │ └── mnist_mlp.cpp # MNIST分类示例 └── CMakeLists.txt3. 核心模块设计与实现
3.1 张量(Tensor)类的设计
张量是基石。我们需要一个类来存储多维数据,并跟踪其梯度信息以支持自动微分。
// include/microtorch/tensor.hpp #ifndef MICROTORCH_TENSOR_HPP #define MICROTORCH_TENSOR_HPP #include <vector> #include <memory> #include <initializer_list> #include <iostream> namespace microtorch { class Tensor { public: // 构造函数 Tensor() = default; explicit Tensor(const std::vector<size_t>& shape, bool requires_grad = false); Tensor(const std::vector<float>& data, const std::vector<size_t>& shape, bool requires_grad = false); // 从初始化列表创建(方便测试) Tensor(std::initializer_list<float> data, std::initializer_list<size_t> shape, bool requires_grad = false); // 拷贝控制(深拷贝) Tensor(const Tensor& other); Tensor& operator=(const Tensor& other); Tensor(Tensor&& other) noexcept; Tensor& operator=(Tensor&& other) noexcept; ~Tensor() = default; // 基础属性访问 const std::vector<size_t>& shape() const { return shape_; } size_t dim() const { return shape_.size(); } size_t numel() const { return numel_; } const float* data() const { return data_.get(); } float* data() { return data_.get(); } bool requires_grad() const { return requires_grad_; } void set_requires_grad(bool requires_grad) { requires_grad_ = requires_grad; } // 梯度相关 std::shared_ptr<Tensor> grad() const { return grad_; } void set_grad(const Tensor& grad); void zero_grad(); // 梯度清零 // 索引访问(简化版,仅支持线性索引) float operator[](size_t index) const; float& operator[](size_t index); // 形状重塑(Reshape) Tensor reshape(const std::vector<size_t>& new_shape) const; // 打印张量 void print(const std::string& name = "") const; private: std::vector<size_t> shape_; // 形状,如 {2, 3} size_t numel_ = 0; // 元素总数 std::shared_ptr<float[]> data_; // 实际数据存储 bool requires_grad_ = false; // 是否需要计算梯度 std::shared_ptr<Tensor> grad_; // 梯度张量 // 计算元素总数 size_t compute_numel(const std::vector<size_t>& shape); // 检查形状与数据是否匹配 void check_shape(const std::vector<size_t>& shape, size_t data_size); }; } // namespace microtorch #endif // MICROTORCH_TENSOR_HPP关键点解释:
- 数据存储:使用
std::shared_ptr<float[]>管理堆内存,便于实现拷贝语义和自动内存释放。 - 梯度:
grad_也是一个Tensor的智能指针。只有当requires_grad_为true时,才会在反向传播中为其计算梯度。 - 深拷贝:拷贝构造函数和赋值运算符需要实现深拷贝,因为每个张量应拥有独立的数据副本(除非设计为视图)。
- 简化设计:为了聚焦核心,我们省略了高级索引、广播(Broadcasting)等复杂特性。这是一个教学实现。
对应的源文件src/tensor.cpp需要实现构造函数、内存分配、拷贝控制等逻辑。
3.2 计算图与自动微分(Autograd)
这是最核心的部分。我们需要一个机制来记录运算,并在反向传播时根据链式法则计算梯度。
思路:每个参与运算并需要梯度的张量,都关联一个Function(或Node)节点。这个节点记录了产生该张量的运算类型、输入张量以及一个用于计算梯度的反向函数(backward)。
// include/microtorch/autograd.hpp #ifndef MICROTORCH_AUTOGRAD_HPP #define MICROTORCH_AUTOGRAD_HPP #include "tensor.hpp" #include <memory> #include <vector> namespace microtorch { // 前向函数类型 using ForwardFunc = std::function<Tensor(const std::vector<Tensor>&)>; // 反向函数类型:输入是输出张量的梯度,输出是每个输入张量的梯度列表 using BackwardFunc = std::function<std::vector<Tensor>(const Tensor&)>; class Function { public: Function(ForwardFunc forward, BackwardFunc backward, std::vector<Tensor> inputs) : forward_(std::move(forward)), backward_(std::move(backward)), inputs_(std::move(inputs)) {} // 执行前向计算,返回输出张量 Tensor apply() { output_ = forward_(inputs_); output_.set_requires_grad(true); // 输出需要梯度 // 将本Function设置为输出张量的“创建者” // 这里需要一个机制来关联,例如在Tensor中增加一个`std::shared_ptr<Function> creator_`成员。 // 为简化,我们用一个全局上下文或简化设计来模拟。 return output_; } // 执行反向传播,计算输入张量的梯度 void backward(const Tensor& grad_output) { auto input_grads = backward_(grad_output); for (size_t i = 0; i < inputs_.size(); ++i) { if (inputs_[i].requires_grad()) { // 累积梯度(如果已有梯度则相加) if (!inputs_[i].grad()) { inputs_[i].set_grad(input_grads[i]); } else { // 实现梯度累加 (inputs_[i].grad() += input_grads[i]) // 需要实现Tensor的加法操作 } // 递归调用前驱节点的backward(链式法则) // 需要从inputs_[i]获取其creator_ Function并调用backward } } } private: ForwardFunc forward_; BackwardFunc backward_; std::vector<Tensor> inputs_; Tensor output_; }; // 一个简化的Autograd引擎(单例或静态类) class Autograd { public: static void backward(const Tensor& tensor, const Tensor& grad = Tensor({1.0f}, {1})) { // 从tensor开始,沿着创建它的Function反向传播grad // 这里需要实现一个基于计算图的后序遍历 // 伪代码: // 1. 获取创建tensor的Function `func` // 2. func->backward(grad); // 3. 对func的每个输入input_i,如果input_i.requires_grad(),则递归调用 backward(input_i, input_grad_i) } }; } // namespace microtorch #endif // MICROTORCH_AUTOGRAD_HPP这是一个高度简化的框架。在实际实现中,我们需要:
- 修改
Tensor类,增加std::shared_ptr<Function> creator_成员,记录创建它的运算。 - 实现具体的运算函数(如加法、乘法、矩阵乘、ReLU),每个运算都返回一个关联了正确
ForwardFunc和BackwardFunc的新Tensor。 - 实现
Autograd::backward的递归或迭代算法,正确遍历计算图。
3.3 基础运算(Ops)的实现
让我们实现两个最基本的运算:加法和矩阵乘法,并为其注册反向传播函数。
// include/microtorch/ops.hpp #ifndef MICROTORCH_OPS_HPP #define MICROTORCH_OPS_HPP #include "tensor.hpp" #include "autograd.hpp" #include <cassert> namespace microtorch { // 元素级加法 Tensor add(const Tensor& a, const Tensor& b) { // 简化:假设a和b形状相同 assert(a.shape() == b.shape()); std::vector<float> result_data(a.numel()); for (size_t i = 0; i < a.numel(); ++i) { result_data[i] = a[i] + b[i]; } Tensor result(result_data, a.shape(), a.requires_grad() || b.requires_grad()); // 如果结果需要梯度,为其创建Function(此处简化,直接返回) // 真实实现需要构造ForwardFunc和BackwardFunc // ForwardFunc: [](inputs){ return add(inputs[0], inputs[1]); } // BackwardFunc: [](grad_output){ return {grad_output, grad_output}; } // 加法的梯度传播 return result; } // 矩阵乘法 (简化版,仅支持2D矩阵) Tensor matmul(const Tensor& a, const Tensor& b) { assert(a.dim() == 2 && b.dim() == 2); assert(a.shape()[1] == b.shape()[0]); // 内维相等 size_t m = a.shape()[0], n = b.shape()[1], k = a.shape()[1]; std::vector<size_t> result_shape = {m, n}; std::vector<float> result_data(m * n, 0.0f); const float* a_data = a.data(); const float* b_data = b.data(); float* r_data = result_data.data(); // 朴素矩阵乘法 for (size_t i = 0; i < m; ++i) { for (size_t j = 0; j < n; ++j) { float sum = 0.0f; for (size_t p = 0; p < k; ++p) { sum += a_data[i * k + p] * b_data[p * n + j]; } r_data[i * n + j] = sum; } } Tensor result(result_data, result_shape, a.requires_grad() || b.requires_grad()); // 同样,需要为其关联反向传播函数 // BackwardFunc: 对于C = A @ B, dA = dC @ B^T, dB = A^T @ dC return result; } // 重载运算符,方便使用 Tensor operator+(const Tensor& a, const Tensor& b) { return add(a, b); } // 注意:不能直接重载*为matmul,因为C++中*是元素乘。我们使用函数名。 } // namespace microtorch #endif // MICROTORCH_OPS_HPP3.4 神经网络模块(NN)的实现
基于上面的自动微分系统,我们可以构建神经网络层。首先定义一个所有层的基类Module。
// include/microtorch/nn/module.hpp #ifndef MICROTORCH_NN_MODULE_HPP #define MICROTORCH_NN_MODULE_HPP #include <vector> #include <memory> #include "../tensor.hpp" namespace microtorch { namespace nn { class Module { public: virtual ~Module() = default; // 前向传播 virtual Tensor forward(const Tensor& input) = 0; // 获取所有需要训练的参数 virtual std::vector<Tensor> parameters() const { return {}; } // 梯度清零 virtual void zero_grad() { for (auto& param : parameters()) { param.zero_grad(); } } // 方便使用的运算符 Tensor operator()(const Tensor& input) { return forward(input); } }; } // namespace nn } // namespace microtorch #endif // MICROTORCH_NN_MODULE_HPP接着实现一个全连接层(Linear Layer)。
// include/microtorch/nn/linear.hpp #ifndef MICROTORCH_NN_LINEAR_HPP #define MICROTORCH_NN_LINEAR_HPP #include "module.hpp" #include "../ops.hpp" #include <random> namespace microtorch { namespace nn { class Linear : public Module { public: Linear(size_t in_features, size_t out_features, bool bias = true) : weight_({out_features, in_features}, true), // 权重需要梯度 bias_(bias ? Tensor({out_features}, true) : Tensor()) // 偏置需要梯度 { // Xavier/Glorot 初始化权重 std::default_random_engine generator; float stddev = std::sqrt(2.0f / (in_features + out_features)); std::normal_distribution<float> distribution(0.0f, stddev); for (size_t i = 0; i < weight_.numel(); ++i) { weight_[i] = distribution(generator); } if (bias) { for (size_t i = 0; i < bias_.numel(); ++i) { bias_[i] = 0.0f; // 偏置初始化为0 } } } Tensor forward(const Tensor& input) override { // input shape: [batch_size, in_features] // weight shape: [out_features, in_features] // output = input @ weight^T + bias Tensor output = matmul(input, weight_.transpose(0, 1)); // 需要实现transpose if (bias_.numel() > 0) { // 广播偏置到每个样本 // 简化:假设input是2D,output是2D,bias是1D // output += bias (广播) // 需要实现广播加法 output = add(output, bias_); // 这里add需要支持广播 } return output; } std::vector<Tensor> parameters() const override { std::vector<Tensor> params = {weight_}; if (bias_.numel() > 0) { params.push_back(bias_); } return params; } private: Tensor weight_; Tensor bias_; }; } // namespace nn } // namespace microtorch #endif // MICROTORCH_NN_LINEAR_HPP注意:上述代码中的transpose和广播加法add是我们尚未实现的张量操作。在完整实现中,你需要先补充这些功能。
3.5 优化器(Optimizer)的实现
以最基础的随机梯度下降(SGD)为例。
// include/microtorch/optim/sgd.hpp #ifndef MICROTORCH_OPTIM_SGD_HPP #define MICROTORCH_OPTIM_SGD_HPP #include <vector> #include "../tensor.hpp" namespace microtorch { namespace optim { class SGD { public: SGD(std::vector<Tensor> parameters, float lr = 0.01f, float momentum = 0.0f) : parameters_(std::move(parameters)), lr_(lr), momentum_(momentum) { if (momentum_ > 0.0f) { velocity_.resize(parameters_.size()); for (size_t i = 0; i < parameters_.size(); ++i) { velocity_[i] = Tensor(parameters_[i].shape(), false); // 初始速度为0 } } } void step() { for (size_t i = 0; i < parameters_.size(); ++i) { auto& param = parameters_[i]; if (!param.grad()) continue; // 无梯度,不更新 if (momentum_ > 0.0f) { // 带动量的SGD: v = momentum * v - lr * grad // param = param + v // 需要实现Tensor的标量乘法和减法 velocity_[i] = add(mul(momentum_, velocity_[i]), mul(-lr_, *param.grad())); param = add(param, velocity_[i]); } else { // 普通SGD: param = param - lr * grad param = add(param, mul(-lr_, *param.grad())); } } } void zero_grad() { for (auto& param : parameters_) { param.zero_grad(); } } private: std::vector<Tensor> parameters_; float lr_; float momentum_; std::vector<Tensor> velocity_; // 动量项 // 辅助函数:标量乘张量 (需要实现) Tensor mul(float scalar, const Tensor& tensor); }; } // namespace optim } // namespace microtorch #endif // MICROTORCH_OPTIM_SGD_HPP4. 完整实战案例:训练一个MNIST手写数字分类器
由于从零实现数据加载和完整的CNN较为复杂,我们构建一个极简的示例:用全连接网络对随机生成的数据进行二分类,演示整个库的工作流程。
4.1 创建项目并编写示例
首先,确保你的CMakeLists.txt正确配置,将include目录包含,并编译示例。
// examples/simple_mlp.cpp #include <iostream> #include <vector> #include "microtorch/nn/module.hpp" #include "microtorch/nn/linear.hpp" #include "microtorch/nn/functional.hpp" // 假设实现了ReLU, Sigmoid #include "microtorch/optim/sgd.hpp" // 一个简单的两层神经网络 class SimpleMLP : public microtorch::nn::Module { public: SimpleMLP(size_t input_size, size_t hidden_size, size_t output_size) : fc1(input_size, hidden_size), fc2(hidden_size, output_size) {} microtorch::Tensor forward(const microtorch::Tensor& x) override { auto h = relu(fc1(x)); // 需要实现relu函数 return sigmoid(fc2(h)); // 需要实现sigmoid函数,输出概率 } std::vector<microtorch::Tensor> parameters() const override { auto params = fc1.parameters(); auto fc2_params = fc2.parameters(); params.insert(params.end(), fc2_params.begin(), fc2_params.end()); return params; } private: microtorch::nn::Linear fc1; microtorch::nn::Linear fc2; }; // 二元交叉熵损失 (简化版,未处理数值稳定性) microtorch::Tensor binary_cross_entropy(const microtorch::Tensor& pred, const microtorch::Tensor& target) { // loss = -[target * log(pred) + (1-target)*log(1-pred)] 的均值 // 需要实现log, 元素乘法等操作 // 此处返回一个占位符张量 return microtorch::Tensor({0.5f}, {1}, true); // 假设损失需要梯度 } int main() { // 1. 创建模型 SimpleMLP model(10, 5, 1); // 输入10维,隐藏层5维,输出1维(二分类) // 2. 创建优化器 microtorch::optim::SGD optimizer(model.parameters(), 0.01f); // 3. 模拟训练循环 for (int epoch = 0; epoch < 10; ++epoch) { // 模拟一个批次的数据 (batch_size=4, input_size=10) std::vector<float> dummy_input_data(4 * 10); std::vector<float> dummy_target_data(4 * 1); // ... 这里填充随机数据 ... microtorch::Tensor input(dummy_input_data, {4, 10}); microtorch::Tensor target(dummy_target_data, {4, 1}); // 前向传播 microtorch::Tensor output = model(input); // 计算损失 microtorch::Tensor loss = binary_cross_entropy(output, target); // 反向传播 // 首先清零梯度 optimizer.zero_grad(); // 假设我们的Autograd::backward可以从loss开始 // microtorch::Autograd::backward(loss); // 更新参数 optimizer.step(); std::cout << "Epoch " << epoch << ", Loss: " << loss[0] << std::endl; } std::cout << "Training finished (simulated)." << std::endl; return 0; }4.2 编译与运行
使用CMake或直接命令行编译。以GCC为例:
# 假设头文件在 ./include, 源文件在 ./src, 示例在 ./examples g++ -std=c++17 -I./include -o simple_mlp ./examples/simple_mlp.cpp ./src/*.cpp ./simple_mlp4.3 预期结果与解释
由于我们省略了许多具体实现(如完整的Autograd、激活函数、损失函数),上述示例无法直接运行。但它清晰地展示了使用我们手搓的库进行模型定义、训练的标准流程,与PyTorch的范式高度一致:
- 定义模型:继承
Module,在构造函数中初始化子模块(Linear)。 - 实现
forward:定义数据如何流过各层。 - 获取参数:
parameters()返回所有需要优化的Tensor。 - 初始化优化器:将模型参数传给优化器(如
SGD)。 - 训练循环:
zero_grad()清零上一轮梯度。forward()计算预测和损失。backward()自动计算梯度(通过计算图)。optimizer.step()根据梯度更新参数。
5. 常见问题与排查思路
在实现和调试这样一个库的过程中,你会遇到无数问题。以下是一些典型问题及解决思路:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 编译错误:未定义的引用 | 函数声明在头文件,但定义在.cpp中,链接时找不到。 | 1. 检查CMakeLists.txt或编译命令是否包含了所有.cpp源文件。2. 确保函数定义在正确的命名空间内。 |
| 运行时错误:段错误(Segmentation Fault) | 访问了未初始化或已释放的内存。 | 1. 使用gdb或valgrind定位崩溃点。2. 检查 Tensor的构造函数、拷贝构造函数、赋值运算符是否正确实现了深拷贝。3. 检查 shared_ptr的使用,避免循环引用。 |
梯度为nullptr或全零 | 自动微分系统未正确连接。 | 1. 确保参与运算的输入张量requires_grad()设置为true。2. 检查每个运算的 Function是否正确创建并关联到输出张量。3. 在 backward()函数中打印中间梯度,检查链式法则计算是否正确。 |
| 数值不稳定(NaN/Inf) | 学习率过大、权重初始化不当、损失函数未做数值稳定处理。 | 1. 降低学习率。 2. 使用 Xavier/Kaiming 初始化。 3. 在 Softmax、CrossEntropy 等函数中加入极小值( eps)防止log(0)。 |
| 计算图内存泄漏 | Function节点在反向传播后未被释放。 | 1. 确保Tensor对Function的引用是weak_ptr或能在适当时候断开。2. 实现一个简单的引用计数或使用 std::enable_shared_from_this管理节点生命周期。 |
| 性能极差 | 使用了朴素算法(如三重循环矩阵乘),未利用局部性,大量小对象分配。 | 1. 实现更高效的矩阵乘法(如分块、循环展开)。 2. 使用 std::vector<float>连续存储,避免频繁new/delete。3. 考虑实现张量的视图(View)操作,避免不必要的拷贝。 |
6. 最佳实践与工程建议
如果你想将这个玩具库提升到一个更可用、更健壮的级别,请考虑以下实践:
- 测试驱动开发(TDD):为每个核心模块(
Tensor、Ops、Autograd、NN)编写单元测试。使用 Google Test 或 Catch2。从最简单的加法梯度检查开始。 - 完善的张量操作:
- 广播(Broadcasting):实现 NumPy/PyTorch 风格的广播规则,这是支持各种形状运算的基础。
- 视图(View):实现
reshape、transpose、slice等不拷贝数据的视图操作,使用std::shared_ptr配合偏移量和步长(Stride)。 - 设备支持:抽象出
Device接口,为将来支持 GPU(CUDA)留出可能。
- 健壮的自动微分:
- 计算图释放:实现基于引用计数的计算图节点自动释放,防止内存泄漏。
- 梯度检查(Gradient Checking):实现一个工具函数,用数值微分(有限差分法)验证自动微分结果的正确性,这是调试 Autograd 的利器。
- 模块化与扩展性:
- 注册机制:使用工厂模式或宏来方便地注册新的运算(
Op)和其对应的前向/反向函数。 - 序列化:实现模型参数(
Tensor)的保存与加载功能(如保存为.pt或自定义格式)。
- 注册机制:使用工厂模式或宏来方便地注册新的运算(
- 性能优化:
- 表达式模板(Expression Template):高级C++技术,用于延迟计算和消除临时对象,可以极大提升像
a + b * c这类链式运算的性能。Eigen 库就大量使用了此技术。 - SIMD 指令:在矩阵乘、卷积等关键运算中使用 SSE/AVX 指令集进行向量化。
- 多线程:使用
std::thread或 OpenMP 对大规模运算进行并行化。
- 表达式模板(Expression Template):高级C++技术,用于延迟计算和消除临时对象,可以极大提升像
- 错误处理与日志:
- 使用异常或
std::optional提供清晰的错误信息(如形状不匹配、未实现的操作)。 - 添加调试日志,方便跟踪计算图的构建和梯度流动。
- 使用异常或
- 文档与示例:为每个公共类和函数编写清晰的注释。提供从线性回归到简单CNN的完整示例,展示库的能力。
从零手搓一个机器学习库是一次漫长而富有挑战性的旅程,但收获也是巨大的。你不仅会获得对机器学习底层原理的深刻理解,你的C++工程能力也将得到质的飞跃。这个项目可以作为你技术履历上一个闪亮的亮点。建议你从本文的简化框架出发,选择一个方向(如完善Autograd、实现CNN层、或添加GPU支持)进行深度扩展,逐步构建属于你自己的“MicroTorch”。