尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

刨根问底:手写一个 C++ 深度学习框架,把 Transformer 扒个干净
📅 发布时间:2026/7/6 1:43:48

一、这到底是个啥?

简单来说,这是一个用纯 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++ 的if、for、while这些控制流,图会随着实际执行路径动态变化。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【程序猿编码】

相关新闻

  • 计算机导论_第4章_笔记
  • 企业认证与安全体系(九):单点登录 SSO 到底是怎么实现的?一篇讲透企业统一身份认证
  • 5分钟掌握SPT-AKI存档编辑器:逃离塔科夫单机版终极修改指南

最新新闻

  • 机器人产业演进逻辑与商业化落地全景攻略
  • 豆包和通义千问智能体突遭下线——AI拟人化监管正式落地,影响有多大?
  • Windows C++编译 Paddle Inference 3.5.0 GPU 版本完整指南
  • 第18周周报
  • x64dbg 逆向实战:3步定位小程序密码验证逻辑并绕过(附修改汇编指令)
  • VIA键盘配置工具:3个场景教你打造专属机械键盘工作流

日新闻

  • AI智能体安全防护框架AgentGuard:从原理到实战部署指南
  • KMX63与PIC18F26K40硬件组合及低功耗设计实践
  • 基于YOLO13改进的门体检测模型:C3k2模块与PoolingFormer技术解析

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号