从零实现C++机器学习库:深入理解张量、计算图与自动微分

在实际机器学习项目中,我们经常使用 TensorFlow、PyTorch 等成熟框架。它们功能强大,但内部封装复杂,对于想深入理解机器学习底层原理,特别是想掌握从数学公式到高性能代码实现全过程的开发者来说,直接使用这些框架有时会感觉像在“黑盒”中操作。理解一个算法的核心,莫过于亲手实现它。本文将带领你从零开始,使用纯 C++ 构建一个简易但功能完整的机器学习库。我们将从最基础的数据结构(张量)开始,逐步实现计算图、自动微分、神经网络层(如全连接层、ReLU激活函数)以及反向传播算法。这个过程不仅能让你透彻理解神经网络前向推理和反向传播的每一个细节,还能让你对 C++ 在数值计算和内存管理方面的应用有更深的认识。无论你是想夯实机器学习基础,还是为面试准备 C++ 与算法结合的实战项目,抑或是为特定嵌入式或高性能场景定制轻量级推理引擎,这篇文章都将提供一条清晰的路径。

1. 理解核心基石:张量与计算图

在动手写代码之前,必须理解两个核心概念:张量(Tensor)和计算图(Computational Graph)。它们是所有现代机器学习框架的基石。

1.1 张量:数据的容器

张量可以简单理解为多维数组。在深度学习中,标量是0维张量,向量是1维张量,矩阵是2维张量,彩色图像(高度、宽度、通道)可以看作是3维张量。我们的库需要一个统一的数据结构来存储和操作这些数据。

一个基础的张量类需要包含:

  1. 数据存储:一个连续的内存块(如std::vector<float>),用于存储元素。
  2. 形状信息:一个表示各维度大小的向量(如std::vector<size_t>)。
  3. 基本操作:创建、销毁、获取元素、修改形状(Reshape)等。

为什么用std::vector<float>而不是原生数组?vector自动管理内存,避免了手动new/delete的麻烦和内存泄漏风险,是 C++ 中更安全、便捷的选择。

1.2 计算图与自动微分

机器学习,尤其是神经网络的训练,核心是梯度下降。我们需要计算损失函数相对于每个模型参数(权重和偏置)的梯度。手动为每一个复杂模型推导梯度公式是不现实的。计算图和自动微分(Automatic Differentiation, AD)解决了这个问题。

计算图将计算过程表示为一个有向无环图(DAG)。节点代表运算(如加法、乘法、矩阵乘、ReLU),边代表数据(张量)的流动。前向传播(Forward Pass)沿着图计算输出,反向传播(Backward Pass)则从输出开始,沿着图反向计算每个节点输入相对于输出的梯度。

自动微分分为两种模式:前向模式和反向模式。反向模式自动微分(Reverse-Mode AD)特别适合神经网络(参数多,输出少),它正是反向传播算法的实现方式。在我们的库中,每个运算节点不仅需要实现前向计算,还需要实现其梯度计算(反向传播)。

2. 环境准备与项目结构

2.1 开发环境配置

我们将使用纯 C++ 标准库,不依赖任何第三方机器学习库,但需要一个 C++ 编译器和构建系统。

  • 编译器:支持 C++11 或更高版本的编译器。推荐GCC (g++)Clang (clang++)。在 Windows 上可以使用MinGW-w64Visual Studio的 MSVC 编译器。
  • 构建工具:为了管理编译过程,我们使用CMake。它是一个跨平台的构建系统生成器,可以生成 Makefile、Visual Studio 项目等。
  • 代码编辑器:任何文本编辑器或 IDE 均可。VSCode是一个轻量级且强大的选择,配合 C/C++ 扩展可以提供良好的开发体验。

VSCode 基础 C++ 环境配置(以 Linux/macOS 或 Windows WSL 为例):

  1. 安装 VSCode 和 C/C++ 扩展(ms-vscode.cpptools)。
  2. 确保系统已安装 g++ 和 cmake。在终端中检查:
    g++ --version cmake --version
  3. 如果未安装,在 Ubuntu/Debian 上可以使用sudo apt-get install g++ cmake安装。

注意:如果你在 Windows 上使用 MSVC 编译器,可能会遇到 “error: microsoft visual c++ 14.0 or greater is required” 这类错误。这通常是因为缺少构建工具。你需要安装Visual Studio Build Tools或完整 Visual Studio,并确保在安装时勾选了“使用 C++ 的桌面开发”工作负载。安装后,在开发者命令提示符中运行编译命令。

2.2 项目目录结构

一个清晰的项目结构有助于管理代码。我们创建如下目录:

cpp_ml_lib/ ├── CMakeLists.txt # 项目根 CMake 配置文件 ├── include/ # 公共头文件 │ └── cpp_ml/ │ ├── tensor.h │ ├── ops.h │ ├── nn.h │ └── autograd.h ├── src/ # 源文件 │ ├── tensor.cpp │ ├── ops.cpp │ ├── nn.cpp │ └── autograd.cpp ├── examples/ # 示例代码 │ ├── linear_regression.cpp │ └── simple_nn.cpp └── test/ # 单元测试(可选) └── test_tensor.cpp

根目录的CMakeLists.txt负责定义项目、添加子目录、设置编译选项和链接库。

# CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(cpp_ml_lib VERSION 0.1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 将头文件目录添加到包含路径 include_directories(${PROJECT_SOURCE_DIR}/include) # 添加库 add_library(cpp_ml STATIC src/tensor.cpp src/ops.cpp src/autograd.cpp src/nn.cpp ) # 添加示例可执行文件 add_executable(example_linear examples/linear_regression.cpp) target_link_libraries(example_linear cpp_ml) add_executable(example_nn examples/simple_nn.cpp) target_link_libraries(example_nn cpp_ml)

3. 核心组件实现:从张量到自动微分

3.1 实现基础张量类

首先在include/cpp_ml/tensor.h中定义Tensor类。

// tensor.h #ifndef CPP_ML_TENSOR_H #define CPP_ML_TENSOR_H #include <vector> #include <iostream> #include <initializer_list> #include <memory> namespace cpp_ml { class Tensor { public: // 构造函数 Tensor(); // 空张量 Tensor(const std::vector<size_t>& shape); // 指定形状,数据初始化为0 Tensor(const std::vector<size_t>& shape, const std::vector<float>& data); // 指定形状和数据 Tensor(std::initializer_list<float> data); // 从列表创建1维张量 Tensor(std::initializer_list<std::initializer_list<float>> data); // 从嵌套列表创建2维张量 // 拷贝构造函数和赋值运算符(深拷贝) Tensor(const Tensor& other); Tensor& operator=(const Tensor& other); // 获取形状和元素总数 const std::vector<size_t>& shape() const { return shape_; } size_t size() const { return data_.size(); } size_t ndim() const { return shape_.size(); } // 访问和修改元素(通过扁平索引) float& operator[](size_t index) { return data_[index]; } const float& operator[](size_t index) const { return data_[index]; } // 通过多维索引访问元素(例如 tensor(0, 1)) float& operator()(std::initializer_list<size_t> indices); const float& operator()(std::initializer_list<size_t> indices) const; // 重塑张量形状(不改变数据顺序,总元素数必须一致) 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} 表示2行3列 std::vector<float> data_; // 扁平化存储的数据 }; } // namespace cpp_ml #endif // CPP_ML_TENSOR_H

src/tensor.cpp中实现关键函数,如多维索引计算和reshape

// tensor.cpp #include "cpp_ml/tensor.h" #include <stdexcept> #include <numeric> namespace cpp_ml { // ... 构造函数实现 ... float& Tensor::operator()(std::initializer_list<size_t> indices) { if (indices.size() != ndim()) { throw std::invalid_argument("Number of indices must match tensor dimension."); } size_t flat_index = 0; size_t stride = 1; auto idx_it = indices.end(); auto shape_it = shape_.end(); // 从最后一个维度开始计算,效率更高 for (size_t i = 0; i < ndim(); ++i) { --idx_it; --shape_it; size_t idx = *idx_it; if (idx >= *shape_it) { throw std::out_of_range("Index out of range."); } flat_index += idx * stride; stride *= (*shape_it); } return data_[flat_index]; } // const版本类似,省略... Tensor Tensor::reshape(const std::vector<size_t>& new_shape) const { size_t total_size = std::accumulate(new_shape.begin(), new_shape.end(), 1, std::multiplies<size_t>()); if (total_size != size()) { throw std::invalid_argument("Total size of new shape must match original size."); } Tensor result(*this); // 拷贝数据 result.shape_ = new_shape; return result; } void Tensor::print(const std::string& name) const { if (!name.empty()) std::cout << name << " = "; // 简化打印,仅打印形状和部分数据 std::cout << "Tensor(shape=["; for (size_t i = 0; i < shape_.size(); ++i) { std::cout << shape_[i]; if (i != shape_.size() - 1) std::cout << ", "; } std::cout << "], data=[...])" << std::endl; } } // namespace cpp_ml

3.2 实现计算节点与自动微分

这是库的核心。我们需要一个基类Node,所有运算都继承自它。每个Node需要记录它的输入节点、前向计算结果和反向传播所需的梯度。

// autograd.h #ifndef CPP_ML_AUTOGRAD_H #define CPP_ML_AUTOGRAD_H #include "tensor.h" #include <memory> #include <vector> namespace cpp_ml { class Node : public std::enable_shared_from_this<Node> { public: Tensor data; // 前向传播的计算结果 Tensor grad; // 反向传播的梯度 std::vector<std::shared_ptr<Node>> inputs; // 输入节点 std::string op_name; // 操作名称,用于调试 Node(const Tensor& data, const std::vector<std::shared_ptr<Node>>& inputs = {}, const std::string& op_name = ""); virtual ~Node() = default; // 前向传播(通常已在构造函数中计算) // 反向传播:计算该节点对每个输入节点的梯度,并累加到输入的 grad 上 virtual void backward(const Tensor& grad_output) = 0; // 清空梯度(在每次反向传播前调用) void zero_grad(); }; // 包装函数,用于创建计算节点 std::shared_ptr<Node> create_tensor(const Tensor& data, bool requires_grad = false); // 基本运算(返回新的 Node) std::shared_ptr<Node> operator+(std::shared_ptr<Node> a, std::shared_ptr<Node> b); std::shared_ptr<Node> operator*(std::shared_ptr<Node> a, std::shared_ptr<Node> b); std::shared_ptr<Node> matmul(std::shared_ptr<Node> a, std::shared_ptr<Node> b); // 矩阵乘法 std::shared_ptr<Node> relu(std::shared_ptr<Node> a); std::shared_ptr<Node> sigmoid(std::shared_ptr<Node> a); std::shared_ptr<Node> mse_loss(std::shared_ptr<Node> pred, std::shared_ptr<Node> target); // 均方误差损失 } // namespace cpp_ml #endif // CPP_ML_AUTOGRAD_H

src/autograd.cpp中,我们实现一个具体的运算节点,例如加法节点AddNode

// autograd.cpp (部分) #include "cpp_ml/autograd.h" #include <algorithm> namespace cpp_ml { class AddNode : public Node { public: AddNode(std::shared_ptr<Node> a, std::shared_ptr<Node> b) : Node(Tensor(), {a, b}, "add") { // 前向传播:计算 a.data + b.data // 这里假设 a 和 b 形状相同,实际需要广播机制 const Tensor& data_a = a->data; const Tensor& data_b = b->data; std::vector<float> result_data(data_a.size()); for (size_t i = 0; i < result_data.size(); ++i) { result_data[i] = data_a[i] + data_b[i]; } data = Tensor(data_a.shape(), result_data); } void backward(const Tensor& grad_output) override { // 加法操作的梯度:dz/da = 1 * grad_output, dz/db = 1 * grad_output // 将梯度累加到输入节点 // 需要实现 Tensor 的加法操作 inputs[0]->grad = inputs[0]->grad + grad_output; // 需要重载 Tensor 的 operator+ inputs[1]->grad = inputs[1]->grad + grad_output; } }; std::shared_ptr<Node> operator+(std::shared_ptr<Node> a, std::shared_ptr<Node> b) { return std::make_shared<AddNode>(a, b); } // ... 实现其他运算节点,如 MulNode, ReluNode, MatMulNode, MSELossNode ... }

关键点解释

  1. Node类使用shared_ptr管理生命周期,方便构建计算图。
  2. backward是纯虚函数,每个运算节点必须实现自己的梯度计算逻辑。
  3. 反向传播时,梯度从输出层向输入层传递。对于每个节点,它接收来自后续节点的梯度 (grad_output),然后根据链式法则,计算其对每个输入节点的贡献,并累加到输入节点的grad成员上。
  4. 在训练循环中,每次迭代前需要调用zero_grad()清空所有参数的梯度,防止梯度累积。

3.3 实现神经网络层:以全连接层和 ReLU 为例

有了自动微分系统,构建神经网络层就变得简单了。层本质上是一组参数(Node)和定义好的计算步骤。

// nn.h #ifndef CPP_ML_NN_H #define CPP_ML_NN_H #include "autograd.h" #include <vector> namespace cpp_ml { namespace nn { class LinearLayer { public: LinearLayer(size_t input_size, size_t output_size); std::shared_ptr<Node> forward(std::shared_ptr<Node> input); std::vector<std::shared_ptr<Node>> parameters() const; private: std::shared_ptr<Node> weight_; // 权重参数,形状 [output_size, input_size] std::shared_ptr<Node> bias_; // 偏置参数,形状 [output_size, 1] 或 [output_size] }; // ReLU 激活函数层(无参数) std::shared_ptr<Node> relu(std::shared_ptr<Node> input); } // namespace nn } // namespace cpp_ml #endif // CPP_ML_NN_H
// nn.cpp #include "cpp_ml/nn.h" #include <random> namespace cpp_ml { namespace nn { LinearLayer::LinearLayer(size_t input_size, size_t output_size) { // 初始化权重和偏置 // 使用 Xavier/Glorot 初始化 std::random_device rd; std::mt19937 gen(rd()); float stddev = std::sqrt(2.0f / (input_size + output_size)); std::normal_distribution<float> dist(0.0f, stddev); std::vector<float> weight_data(output_size * input_size); std::vector<float> bias_data(output_size); for (auto& val : weight_data) val = dist(gen); for (auto& val : bias_data) val = dist(gen); // 创建可训练参数节点,requires_grad 设为 true weight_ = create_tensor(Tensor({output_size, input_size}, weight_data), true); bias_ = create_tensor(Tensor({output_size}, bias_data), true); } std::shared_ptr<Node> LinearLayer::forward(std::shared_ptr<Node> input) { // 计算 output = input * weight^T + bias // 注意:我们的 matmul 可能要求 weight 是 [output, input], input 是 [batch, input] // 这里简化处理,假设 input 是 [input_size] 向量 auto weighted = matmul(weight_, input); // 需要调整维度匹配 auto output = weighted + bias_; return output; } std::vector<std::shared_ptr<Node>> LinearLayer::parameters() const { return {weight_, bias_}; } std::shared_ptr<Node> relu(std::shared_ptr<Node> input) { return cpp_ml::relu(input); // 调用 autograd 中定义的 relu 函数 } } // namespace nn } // namespace cpp_ml

为什么使用 Xavier 初始化?这是为了在前向传播和反向传播过程中,保持各层激活值和梯度的方差大致稳定,避免梯度消失或爆炸,从而加速训练收敛。

4. 实战:构建并训练一个简单神经网络

现在,我们将使用上面构建的库,实现一个简单的全连接神经网络,用于解决一个经典的二分类问题(例如异或问题 XOR)。

4.1 定义网络结构

我们构建一个两层网络:输入层(2维) -> 隐藏层(4维,ReLU激活) -> 输出层(1维,Sigmoid激活)。

// examples/simple_nn.cpp #include "cpp_ml/nn.h" #include "cpp_ml/autograd.h" #include <iostream> #include <vector> #include <cmath> using namespace cpp_ml; using namespace cpp_ml::nn; class SimpleNet { public: SimpleNet() : fc1(2, 4), fc2(4, 1) {} std::shared_ptr<Node> forward(std::shared_ptr<Node> x) { auto h1 = fc1.forward(x); h1 = relu(h1); auto out = fc2.forward(h1); out = sigmoid(out); // 二分类输出用 sigmoid return out; } std::vector<std::shared_ptr<Node>> parameters() { auto params = fc1.parameters(); auto params2 = fc2.parameters(); params.insert(params.end(), params2.begin(), params2.end()); return params; } private: LinearLayer fc1; LinearLayer fc2; };

4.2 准备数据与训练循环

int main() { // 1. 准备数据 (XOR 问题) // 输入: [[0,0], [0,1], [1,0], [1,1]] // 标签: [0, 1, 1, 0] std::vector<std::shared_ptr<Node>> inputs; std::vector<std::shared_ptr<Node>> targets; inputs.push_back(create_tensor(Tensor({2}, {0.0f, 0.0f}), false)); inputs.push_back(create_tensor(Tensor({2}, {0.0f, 1.0f}), false)); inputs.push_back(create_tensor(Tensor({2}, {1.0f, 0.0f}), false)); inputs.push_back(create_tensor(Tensor({2}, {1.0f, 1.0f}), false)); targets.push_back(create_tensor(Tensor({1}, {0.0f}), false)); targets.push_back(create_tensor(Tensor({1}, {1.0f}), false)); targets.push_back(create_tensor(Tensor({1}, {1.0f}), false)); targets.push_back(create_tensor(Tensor({1}, {0.0f}), false)); // 2. 初始化网络和优化器(这里实现一个简单的 SGD) SimpleNet net; float learning_rate = 0.1f; int epochs = 1000; // 3. 训练循环 for (int epoch = 0; epoch < epochs; ++epoch) { float total_loss = 0.0f; // 遍历所有样本(这里使用全批量梯度下降) for (size_t i = 0; i < inputs.size(); ++i) { // 清空梯度 for (auto& param : net.parameters()) { param->zero_grad(); } // 前向传播 auto prediction = net.forward(inputs[i]); auto loss = mse_loss(prediction, targets[i]); // 使用均方误差损失 total_loss += loss->data[0]; // 假设 loss 是标量张量 // 反向传播 // 从 loss 节点开始反向传播 // 需要实现一个从 loss 节点触发整个图反向传播的函数 // 这里简化:假设 loss->backward() 会触发整个图 loss->backward(Tensor({1}, {1.0f})); // 损失对自身的梯度是1 // 梯度下降更新参数 for (auto& param : net.parameters()) { // param->data = param->data - learning_rate * param->grad // 需要实现 Tensor 的 operator- 和 operator* (float) for (size_t j = 0; j < param->data.size(); ++j) { param->data[j] -= learning_rate * param->grad[j]; } } } if (epoch % 100 == 0) { std::cout << "Epoch " << epoch << ", Loss: " << total_loss / inputs.size() << std::endl; } } // 4. 测试 std::cout << "\nTesting trained network:" << std::endl; for (size_t i = 0; i < inputs.size(); ++i) { auto out = net.forward(inputs[i]); std::cout << "Input [" << inputs[i]->data[0] << ", " << inputs[i]->data[1] << "] -> Output " << out->data[0] << " (target: " << targets[i]->data[0] << ")" << std::endl; } return 0; }

4.3 编译与运行

在项目根目录下执行:

mkdir build cd build cmake .. make ./example_nn

如果一切顺利,你应该能看到损失逐渐下降,并且在训练结束后,网络对 XOR 输入的预测接近目标值(0或1)。

5. 常见问题与排查路径

在实现和运行上述库的过程中,你可能会遇到各种问题。下面是一个排查清单。

问题现象可能原因检查方式处理建议
编译错误:未定义的引用1. 源文件未添加到CMakeLists.txtadd_libraryadd_executable中。
2. 函数声明与定义不匹配(参数、返回值、命名空间)。
1. 检查CMakeLists.txt
2. 检查头文件中的函数签名与.cpp文件中的定义是否完全一致。
1. 在CMakeLists.txt中添加遗漏的源文件。
2. 修正函数签名,确保一致。使用nmobjdump查看目标文件中的符号。
运行时错误:段错误 (Segmentation fault)1. 访问了空指针或野指针。
2. 数组或vector越界访问。
3. 在Tensor::operator()中索引计算错误。
1. 使用gdb调试器运行程序,在崩溃时查看堆栈跟踪 (bt)。
2. 在可能越界的访问前添加边界检查并打印日志。
1. 确保所有shared_ptr在使用前已被正确初始化。
2. 在Tensor的访问函数中添加更严格的边界检查,并抛出带有信息的异常。
梯度计算为 0 或 NaN1. 参数初始化不当(如全零初始化)。
2. 学习率设置过高导致梯度爆炸。
3. ReLU 激活函数导致“神经元死亡”(梯度为0)。
4. 反向传播算法实现有误。
1. 打印前几轮训练中参数的梯度和数据值。
2. 检查损失函数值的变化。
1. 使用 Xavier 或 He 初始化。
2. 降低学习率,或使用学习率衰减。
3. 可以尝试使用 Leaky ReLU 代替 ReLU。
4. 用一个小型计算图(如z = x * y)手动验证梯度计算是否正确。
程序运行缓慢1. 使用了大量小张量操作,开销大。
2. 未启用编译器优化。
3. 算法复杂度高(如嵌套循环实现矩阵乘法)。
1. 使用性能分析工具(如gprof,perf)。
2. 检查最耗时的函数。
1. 尽量使用向量化操作,减少循环。考虑使用 BLAS 库(如 OpenBLAS)进行矩阵运算。
2. 在CMakeLists.txt中设置set(CMAKE_CXX_FLAGS "-O2 -march=native")启用优化。
3. 实现分块矩阵乘法。
内存泄漏1. 循环引用导致shared_ptr无法释放。
2. 原始指针和智能指针混用。
1. 使用 Valgrind (valgrind --leak-check=full ./your_program) 检查内存泄漏。
2. 检查计算图中节点间的引用关系。
1. 确保计算图是单向的(从输入到输出)。如果需要双向引用,考虑使用weak_ptr打破循环。
2. 统一使用智能指针管理资源。

6. 扩展方向与最佳实践

我们实现的库是一个教学原型,距离生产级库还有很大距离。以下是几个关键的扩展方向和工程化建议。

6.1 功能扩展清单

  1. 更多运算:实现卷积 (conv2d)、池化 (max_pool)、批量归一化 (batch_norm)、Dropout 等现代神经网络常用层。
  2. 广播机制:使张量运算支持 NumPy 风格的广播,这是向量化编程的基础。
  3. 优化器:实现 SGD with Momentum、Adam、RMSprop 等更先进的优化算法。
  4. 数据加载与预处理:实现一个简单的DataLoader,支持批量加载、打乱顺序和数据增强。
  5. 序列化:实现模型参数保存 (save) 和加载 (load) 功能,便于模型持久化。
  6. 计算图优化:实现简单的静态图优化,如常量折叠、公共子表达式消除。
  7. GPU 支持:使用 CUDA 或 OpenCL 将核心计算(如矩阵乘、卷积)移植到 GPU 上。

6.2 性能优化最佳实践

  • 避免小对象频繁分配:在张量运算中,频繁创建小的临时Tensor对象会带来巨大开销。可以考虑使用内存池或预分配工作空间。
  • 使用 SIMD 指令:在 CPU 上,使用 SSE、AVX 等 SIMD 指令集可以大幅提升浮点运算性能。许多编译器在-O3优化级别下会自动向量化简单循环,但对于复杂循环可能需要手动内联汇编或使用 intrinsics。
  • 依赖成熟数值库:不要重复造轮子。对于线性代数运算(如 GEMM),链接 OpenBLAS、Intel MKL 或 Eigen 库可以获得接近硬件极限的性能。
  • 惰性求值与操作融合:在构建计算图时,不立即执行计算,而是记录操作。在最终求值时,可以将多个连续的元素级操作(如ReLU->Add)融合为一个内核,减少内存读写。

6.3 代码质量与测试

  • 单元测试:为Tensor类、每个运算节点、每个网络层编写单元测试。使用 Google Test 或 Catch2 框架。
  • 梯度检验:实现梯度检验(Gradient Checking),通过数值方法(如有限差分)计算梯度,与自动微分计算的梯度对比,这是验证反向传播实现正确性的金标准。
  • 异常安全:确保代码在发生异常时不会泄漏资源。充分利用 RAII 原则,让构造函数获取资源,析构函数释放资源。
  • 接口设计:保持接口简洁、一致。学习 PyTorch 的torch::TensorAPI 设计思想。

通过这个从零手搓机器学习库的过程,你不仅深入理解了神经网络的前向与反向传播、计算图、自动微分等核心概念,还锻炼了用 C++ 进行系统级编程和数值计算的能力。这个库的骨架为你探索更深入的领域(如自定义算子、编译器优化、硬件加速)打下了坚实的基础。下一步,你可以选择其中一个扩展方向深入实践,例如尝试集成一个 BLAS 库来替换手写的矩阵乘法,并对比性能提升,这将是又一个宝贵的学习项目。