当前位置: 首页 > news >正文

刨根问底:手写一个 C++ 深度学习框架,把 Transformer 扒个干净

一、这到底是个啥?

简单来说,这是一个用纯 C++17从零开始实现的深度学习小框架。它的特点很鲜明:

  • 零依赖:不装 PyTorch、不装 TensorFlow、不装任何第三方库,就靠着 C++ 标准库硬刚
  • 自带自动求导:像 PyTorch 一样,你写前向传播,它自动帮你算反向传播
  • CPU 多线程优化:矩阵乘法、逐元素运算这些"苦力活"全部交给线程池并行处理
  • 完整的 Transformer:编码器 + 解码器 + 多头注意力 + 前馈网络,论文《Attention Is All You Need》里有的,它都有

二、为什么要做这个?

用 PyTorch 写模型很爽,一行nn.Linear就搞定全连接层。但爽归爽,很多细节被框架"藏"起来了:

  • 张量数据到底怎么在内存里排布的?
  • reshape的时候数据真的被拷贝了吗?
  • 反向传播的时候梯度是怎么一层一层传回去的?
  • 注意力机制里的 mask 到底长什么样?

三、整体架构:四层金字塔

整个项目可以看成一座四层金字塔,从下往上越来越"高级":

第 1 层:核心张量引擎

这是地基,所有上层建筑都靠它撑着。一个张量(Tensor)里面装了四样东西:

  1. 数据缓冲区:就是一个连续的一维float数组,所有多维数据都塞在这里面
  2. 形状 + 步幅shape告诉你这是几维的、每维多长;stride告诉你怎么从一维数组里"跳"到多维的对应位置
  3. 梯度缓冲区:反向传播时用来存梯度的地方
  4. 计算图链接:指向"父节点"的指针,用来追踪这个张量是怎么算出来的

关键设计:用 stride 做多维索引映射

比如一个shape = [2, 3]的张量,它的stride = [3, 1]。要取位置[i, j]的元素,实际在一维数组里的索引就是:

flat_index = i × stride[0] + j × stride[1] = i × 3 + j × 1

这个设计牛在哪?reshape、view、切片这些操作,很多时候只需要改改 shape 和 stride 的元数据,根本不用动数据本身。内存零拷贝,性能直接起飞。

第 2 层:神经网络层

在张量引擎之上,搭了一层"积木":

组件作用
Linear全连接层,就是y = xW + b
Multi-Head Attention多头注意力,Transformer 的灵魂
Feed-Forward Network位置前馈网络,每个位置独立过两个全连接
Layer Normalization层归一化,稳定训练
Dropout随机丢弃神经元,防止过拟合
Embedding把字符/词转成向量
Positional Encoding给每个位置加上"位置信息"

第 3 层:模型架构

把上面的积木堆起来,就是完整的 Transformer:

  • Encoder Stack:N 层编码器,每层 = Self-Attention + Feed-Forward + Norm
  • Decoder Stack:N 层解码器,每层 = Masked Self-Attention + Encoder-Decoder Attention + Feed-Forward + Norm

第 4 层:数据处理与配置

  • 字符级分词:把文本拆成单个字符,每个字符对应一个 ID
  • DataLoader:把数据切成 batch,方便训练
  • config.ini:所有超参数(学习率、层数、头数等)都写在一个配置文件里,改起来不用碰代码

四、张量 + 自动求导:整个项目的"心脏"

4.1 张量长什么样?

上面说了,张量就是一个"一维数组 + 元数据"的包装。用大白话说:

你看到的[2, 3, 4]这种三维张量,在内存里其实就是一串float数字排成一排。shape 和 stride 就是"翻译器",告诉程序怎么从这一排数字里找到你要的那个。

为什么用 shared_ptr 共享数据?

因为张量经常要做 view、reshape、切片这些操作。如果每次 reshape 都拷贝一份数据,那内存早就爆了。用shared_ptr共享底层数据,多个张量可以"看"同一块内存,但各自有自己的 shape 和 stride。

4.2 自动求导怎么工作的?

这是整个项目最精彩的部分。核心思想就一句话:

每个操作都记住自己是怎么来的,并且知道"如果输出变了,输入该怎么变"。

具体实现上,每个张量节点维护一个"父节点列表"。当你做z = x + y时:

  1. z这个张量会记住:我是 x 和 y 加出来的
  2. z还自带一个backward()函数:如果 z 的梯度是 dz,那 x 的梯度 += dz,y 的梯度 += dz

然后反向传播的时候,从 loss 节点开始,沿着计算图一层一层往回传,用链式法则把梯度"分发"给每个参数。

动态图 vs 静态图

这里用的是动态图(也叫 define-by-run)。啥意思?就是前向传播跑一遍,计算图就自动建好了。好处是你可以用 C++ 的ifforwhile这些控制流,图会随着实际执行路径动态变化。PyTorch 也是这个思路。

内存怎么释放?

靠引用计数。当反向传播做完,loss 张量出了作用域,引用计数归零,整个计算图就自动被回收了。不需要写垃圾回收器,C++ 的shared_ptr帮你搞定。


五、Multi-Head Attention:Transformer 的灵魂

注意力机制是 Transformer 的核心,也是很多人第一次看论文时最懵的地方。其实拆开来看,就是一套"查字典"的流程:

5.1 三步投影:Q、K、V

输入一个序列X,先过三个不同的线性层,得到三个"分身":

  • Q(Query):我要查什么?
  • K(Key):字典里每个条目的"关键词"
  • V(Value):字典里每个条目的"实际内容"
Q = X × W_Q K = X × W_K V = X × W_V

5.2 算注意力分数

把 Q 和 K 做矩阵乘法,得到"相似度分数":

scores = Q × K^T

然后除以√d_k(缩放,防止 softmax 梯度消失),再过一个 softmax 变成概率分布:

attention_weights = softmax( scores / √d_k + Mask )

最后用这个概率分布去加权求和 V:

output = attention_weights × V

5.3 多头 = 多组"查字典"的视角

上面的过程只算了一组 Q、K、V。多头注意力就是同时算多组,每组关注不同的"角度"。比如:

  • 第 1 个头关注语法关系
  • 第 2 个头关注语义关系
  • 第 3 个头关注位置关系

具体实现上,就是把embed_dim切成num_heads份,每份独立算注意力,最后把结果拼接起来,再过一个线性层投影回去。

5.4 Mask 是干嘛的?

在 Decoder 里,预测第 t 个词的时候,不能"偷看"后面的词。所以用一个上三角掩码把未来的位置分数变成-inf,softmax 之后这些位置的概率就是 0,模型就"看不到"未来了。


六、训练 vs 推理:两条不同的路

6.1 训练流程

加载文本 → 字符分词 → 构建 batch → Forward → 算 Loss → Backward → Adam 更新权重 → 循环
  • tiny_shakespeare.txt这种小数据集就能跑
  • 损失函数是交叉熵(CrossEntropy)
  • 优化器用 Adam,带偏差修正
  • 训练完把权重保存成.bin文件

6.2 推理流程

加载权重 → 输入 prompt → 分词 → Forward → 取最后一个位置的预测 → 选下一个 token → 拼回序列 → 循环
  • 一次只生成一个 token,然后把这个 token 拼回输入序列,再预测下一个
  • 循环直到达到max_generate_length或者遇到结束符
  • 选 token 可以用 argmax(贪心)或者采样(带温度)

七、CPU 多线程:怎么把性能榨干

深度学习框架通常跑在 GPU 上,但这个是 CPU 优先的。怎么让 CPU 也跑得快?

答案:线程池 + 任务分区

项目里实现了一个固定大小的线程池(默认 500 个线程)。当遇到大矩阵乘法时,把输出矩阵按行切分成若干块,每块交给一个线程独立计算。因为各线程算的是输出矩阵的不同区域,没有数据竞争,不需要锁,性能损耗很小。

适用多线程的操作包括:

  • 矩阵乘法(matmul)
  • 逐元素运算(add、mul、div)
  • Softmax 归一化
  • Layer Normalization
  • 注意力分数计算
  • 前馈网络计算

八、核心代码思路

从项目结构可以还原出核心逻辑:

张量类骨架

classTensor{// 数据std::shared_ptr<std::vector<float>>data;// 元数据std::vector<int>shape;std::vector<int>stride;// 自动求导std::vector<std::shared_ptr<Tensor>>parents;// 父节点std::function<void()>backward_fn;// 反向传播函数std::shared_ptr<std::vector<float>>grad;// 梯度缓冲区boolrequires_grad;// 核心操作staticTensormatmul(constTensor&a,constTensor&b);staticTensoradd(constTensor&a,constTensor&b);Tensorreshape(conststd::vector<int>&new_shape);Tensorview(conststd::vector<int>&new_shape);// 反向传播入口voidbackward();};

自动求导的 backward 流程

voidTensor::backward(){// 1. 拓扑排序:从当前节点往回走,确定计算顺序autotopo_order=topological_sort(this);// 2. 初始化:loss 的梯度是 1this->grad=ones_like(this->data);// 3. 从后往前,逐个调用 backward_fnfor(auto&node:topo_order){node->backward_fn();// 每个节点知道自己该怎么传梯度}}

多头注意力的 forward

TensorMultiHeadAttention::forward(constTensor&x){// 1. 投影得到 Q, K, VautoQ=linear_q(x);autoK=linear_k(x);autoV=linear_v(x);// 2. 拆成多组 headautoQ_heads=split(Q,num_heads);autoK_heads=split(K,num_heads);autoV_heads=split(V,num_heads);// 3. 每个 head 独立算注意力std::vector<Tensor>head_outputs;for(inti=0;i<num_heads;i++){autoscores=matmul(Q_heads[i],transpose(K_heads[i]));scores=scores/sqrt(d_k);if(use_mask)scores=scores+mask;autoweights=softmax(scores);autohead_out=matmul(weights,V_heads[i]);head_outputs.push_back(head_out);}// 4. 拼接 + 最终投影autoconcat=concatenate(head_outputs);returnlinear_out(concat);}

If you need the complete source code, please add the WeChat number (c17865354792)


本地运行指南:从零跑起来

编译

mkdirbuildcdbuild cmake..make

编译完成后会生成两个可执行文件:

  • ./neural_network— 主程序(训练/推理)
  • ./test_tensor— 张量操作测试

第四步:运行张量测试

这是最快速验证环境的方式,不需要数据文件:

./test_tensor

预期输出类似:

Running tensor tests... Test 1: Tensor creation - PASSED Test 2: Matrix multiplication - PASSED Test 3: Broadcasting - PASSED Test 4: Autograd backward - PASSED ... All tests passed!

第五步:准备数据文件

项目需要tiny_shakespeare.txt作为训练数据。从项目根目录:

# 下载莎士比亚文本wgethttps://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txtmvinput.txt data/tiny_shakespeare.txt

第六步:修改配置文件

编辑config.ini

# 先试试训练模式 inference_mode = false data_filename = ../data/tiny_shakespeare.txt # 小模型参数(跑得快) embed_dim = 64 num_layers = 2 num_heads = 2 ff_hidden_dim = 256 batch_size = 4 num_epochs = 10 num_threads = 4 # 根据你的CPU核心数调整

第七步:运行训练

./neural_network

你会看到输出:

Epoch 1/10, Loss: 2.3456 Epoch 2/10, Loss: 2.1234 ... Training complete. Weights saved to transformer_weights.bin

第八步:运行推理

修改config.ini

inference_mode = true load_existing_weights = true weights_filename = transformer_weights.bin initial_prompt = ROMEO: max_generate_length = 50

再运行:

./neural_network

输出:

ROMEO: What light through yonder window breaks...

十、总结:这个项目教会了我们什么?

  1. 自动求导不只是微积分问题,更是内存管理问题——计算图的生命周期、引用计数、共享所有权,这些工程细节比数学公式更难搞。

  2. Stride 是 tensor 系统里最高杠杆的概念——一个设计好的 stride 机制,能让 reshape、view、广播、切片全部零拷贝实现。

  3. 手写注意力比看十张图更有用——当你真的把 Q×K^T、缩放、mask、softmax、乘 V 这一串操作用代码写出来,注意力的直觉就建立了。

  4. 框架设计全是 trade-off,没有绝对的对错——动态图 vs 静态图、共享所有权 vs 唯一所有权、工厂模式 vs 直接构造……每个选择都有代价。

  5. CPU 也能跑深度学习,关键看你怎么并行——线程池 + 任务分区 + 无锁设计,能把 CPU 的多核优势发挥出来。

Welcome to follow WeChat official account【程序猿编码

http://www.jsqmd.com/news/1131747/

相关文章:

  • 一个中层是怎么突然变强的?看完你就是中层的天花板
  • 小产月子一般坐多少天?科学小产休养与子宫修护指南
  • a place to crash临时过夜落脚的地方;凑合一晚的住处
  • 【VRP问题】基于遗传算法求解应急物资配送路径最低成本优化问题附Matlab代码
  • Java Swing贪吃蛇游戏完整实现(MVC架构+MySQL排行榜+音效系统)
  • 大模型:MessagesPlaceholder 是什么?
  • 3种CNN架构对比:从零搭建、VGG16迁移学习与ResNet50在猫狗识别上的性能实测
  • 如何用15分钟完成传统需要3小时的Hackintosh配置?OpCore-Simplify的智能革命
  • 【OpenHarmony/HarmonyOs 】单位换算引擎实战:长度、面积、体积、温度、速度的端侧计算方案
  • YOLOv3 与 RealSense D435i 协同:600张图像训练,实现多目标无序抓取位姿估计
  • Gensim 4.3.3 Word2Vec 参数调优实战:5个关键参数对藏文词向量质量的影响
  • 从Wafer到Chip:图解芯片制造5大核心工艺与10个关键测试节点
  • AIPCowork运维实战:从微信告警到中间件巡检,一句话就够了
  • 基于51单片机智能手势识别系统 PAG7620 9种手势成品21(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • APKMirror客户端开发实战:构建安全高效的安卓应用下载平台
  • Devicetree Specification v0.4 核心属性实战:5分钟掌握 reg、interrupts 与 ranges 配置
  • 2026最新8款AI编程工具学生党平替实测合集
  • 3 种朴素贝叶斯变体对比:高斯 vs 多项式 vs 伯努利,sklearn 实战 5 分钟
  • 2026年纸托包装如何选?看烘干房产能和模具设计避坑
  • 【OpenHarmony/HarmonyOs 】限时答题状态机实践:倒计时、暂停、自动提交与实况窗结束态设计
  • 基于multisim的RC有源滤波器的设计
  • 2026年电销机器人值不值得用?从成本、效果到选型的完整拆解
  • QGC V5.0 gstreamer视频流在安卓端画面卡顿、冻结,硬件解码失败的问题解决方案
  • ClaudeCode本地源码编译、调试、自定义接入大模型实操案例
  • LLaMA 2 / ChatGLM 等5款大模型位置编码对比:RoPE vs 绝对 vs 相对
  • 华为matepad pro运行jupyter
  • 【claude code实践】CLAUDE.md 应该写什么:命令、规范、架构与禁区
  • 2026最新7款AI编程工具学生党平替实测
  • 2026最新8款AI编程助手平替实测 覆盖全场景选型参考
  • 【claude code实践】CLAUDE.md 入门:给 AI 写一份项目说明书