一、项目释义
深度学习框架的本质是什么?
PyTorch、TensorFlow这类框架底层核心就是张量存储 + 算子实现 + 自动微分引擎。很多同学学习深度学习,直接调用高层API,只懂调包,不明白反向传播、梯度怎么一步步算出来。
本项目使用标准C语言,仅依靠C标准库,用大约500行代码,实现一个极简张量库。
项目目标:
- 自定义N维多维数组(张量)结构,实现shape、stride步长机制,支持任意维度张量;
- 实现基础算子:矩阵乘法、ReLU、LogSoftmax、逐元素乘、均值,同时编写对应反向传播函数;
- 手写计算图Autograd自动微分,基于链式法则递归求梯度;
- 搭建简易全连接网络,完成MNIST手写数字训练,实现端到端训练闭环;
- 所有计算逻辑全部手写,零外部机器学习库依赖,编译仅需要C编译器。
二、行业技术知识点
1. 张量、Shape、Stride步长
张量就是N维数组。不管是1维向量、2维矩阵、3维图像,底层全部平铺存储在一块一维连续内存float*数组中。
shape:每个维度的长度,如MNIST图片(28,28);stride:在这个维度前进1个下标,底层一维数组需要跳过多少个元素。
举例子:shape=(4,2)的二维矩阵
stride自动计算:从最后一维向前推导,最后一维stride=1;
第0维stride = 2,访问
[i][j],底层索引 =i*stride[0] + j*stride[1]。步长最大优势:做reshape不需要拷贝原始数据,只修改shape、stride,零内存开销。
2. 计算图 & Autograd自动微分
正向推理时,记录所有算子操作、输入张量依赖关系,构建计算图;
反向传播时,从损失张量出发,链式法则反向遍历计算图,逐层计算每个张量梯度。
- 正向(Forward):输入→网络算子→输出损失;
- 反向(Backward):损失梯度从后往前,依次计算每个参数梯度;
- 权重参数拿到梯度后,梯度下降更新权重,循环迭代训练。
3. 基础算子前向+反向
所有算子都成对实现:Forward计算输出,Backward计算输入梯度。
本库实现核心算子:矩阵乘法matmul、ReLU激活、LogSoftmax、逐元素乘法、均值。
4. 梯度下降训练
梯度代表:参数微小变化,会带来损失增大还是减小。
权重更新公式:w = w - lr * grad,lr为学习率,不断减小损失值。
三、整体架构设计思路
整个库分为4大模块,模块之间低耦合:
- N维数组模块(Arr)
负责原始浮点数据存储、shape、stride、总元素size;提供内存分配、释放、索引寻址。
c
typedef struct {
float* values;
int* shape;
int* strides;
int ndim;
int size;
} Arr;
- values:一维连续float数组,存放所有元素;
- shape:数组每个维度尺寸;
- strides:步长数组;
- ndim:维度数量;
- size:总元素个数。
- 自动微分张量封装模块(Tensor)
在Arr基础上增加梯度、计算图依赖信息,构建Autograd能力:
c
typedef union {
int ival;
float fval;
} Arg;
#define MAX_PREVS 4
#define MAX_ARGS 4
typedef struct Tensor {
Arr* data;
Arr* grad;
int op;
struct Tensor* prevs[MAX_PREVS];
int num_prevs;
Arg args[MAX_ARGS];
} Tensor;
字段说明:
data:存储张量正向计算值;grad:同shape,存储梯度;op:生成该张量的算子类型(MATMUL / RELU / LOGSOFTMAX等);prevs:保存输入依赖张量,也就是计算图上游节点;num_prevs:上游张量数量;args:算子额外参数(例如运算轴)。
- 算子库模块
每个算子由两部分组成:
- forward函数:接收输入Tensor,输出新Tensor,记录算子类型与依赖;
- backward函数:接收输出张量,回传梯度到上游prevs张量。
-
训练循环模块
加载数据集,构造网络,前向推理计算损失 → backward求梯度 → 更新权重 → 清空梯度,循环迭代。输入图像Tensor
↓
matmul(w1) → ReLU → matmul(w2) → LogSoftmax
↓
和OneHot标签逐元素相乘 → 求均值,得到Loss损失张量
↓ 反向传播入口:loss.grad = 1.0
backward递归遍历计算图
↓
w1、w2权重张量填充梯度grad
↓
梯度下降更新w1 w2权重,梯度清零,进入下一轮batch
四、核心代码实现原理解析
4.1 Arr步长自动计算函数
根据shape自动生成stride,核心代码:
c
// 计算strides,从最后一维向前迭代
void compute_strides(Arr* arr) {
int s = 1;
for (int i = arr->ndim - 1; i >= 0; i--) {
arr->strides[i] = s;
s *= arr->shape[i];
}
}
原理:最后一维移动1个下标,内存只+1;更高维度的步长=后续所有维度乘积。
4.2 矩阵乘法Matmul前向实现
(P,Q) × (Q,R) = (P,R)
c
Tensor* matmul(Tensor* a, Tensor* b) {
int P = a->data->shape[0];
int Q = a->data->shape[1];
int R = b->data->shape[1];
Tensor* t = create_zero_tensor((int[]) {P, R}, 2);
for (int i = 0; i < P; i++) {
for (int j = 0; j < R; j++) {
float tmp = 0.0f;
for (int k = 0; k < Q; k++) {
int pos_a = i * a->data->strides[0] + k * a->data->strides[1];
int pos_b = k * b->data->strides[0] + j * b->data->strides[1];
tmp += a->data->values[pos_a] * b->data->values[pos_b];
}
int pos_c = i * R + j;
t->data->values[pos_c] = tmp;
}
}
t->op = MATMUL;
t->num_prevs = 2;
t->prevs[0] = a;
t->prevs[1] = b;
return t;
}
读取元素使用stride寻址,不是简单二维数组下标;生成张量标记算子与两个输入依赖。
Matmul反向传播
矩阵求导公式:
C=A@BC=A @ BC=A@B
dA=dC@BTdA = dC @ B^TdA=dC@BT
dB=AT@dCdB = A^T @ dCdB=AT@dC
c
void matmul_backward(Tensor* out) {
int P = out->prevs[0]->data->shape[0];
int Q = out->prevs[0]->data->shape[1];
int R = out->prevs[1]->data->shape[1];
// dA = dC × B.T
for (int i = 0; i < P; i++) {
for (int j = 0; j < Q; j++) {
float tmp = 0.0f;
for (int k = 0; k < R; k++) {
int pos_b = j * out->prevs[1]->data->strides[0] + k * out->prevs[1]->data->strides[1];
tmp += out->grad->values[i * R + k] * out->prevs[1]->data->values[pos_b];
}
int pos_da = i * Q + j;
out->prevs[0]->grad->values[pos_da] = tmp;
}
}
// dB = A.T × dC
for (int i = 0; i < Q; i++) {
for (int j = 0; j < R; j++) {
float tmp = 0.0f;
for (int k = 0; k < P; k++) {
int pos_a = k * out->prevs[0]->data->strides[0] + i * out->prevs[0]->data->strides[1];
tmp += out->grad->values[k * R + j] * out->prevs[0]->data->values[pos_a];
}
int pos_db = i * R + j;
out->prevs[1]->grad->values[pos_db] = tmp;
}
}
}
4.3 ReLU算子前向与反向
ReLU(x)=max(0,x)ReLU(x) = max(0,x)ReLU(x)=max(0,x)
导数:x>0梯度不变;x≤0梯度置0
c
Tensor* relu(Tensor* inp) {
Tensor* t = create_zero_tensor(inp->data->shape, inp->data->ndim);
for (int i = 0; i < inp->data->size; i++) {
t->data->values[i] = (inp->data->values[i] > 0) ? inp->data->values[i] : 0;
}
t->op = RELU;
t->num_prevs = 1;
t->prevs[0] = inp;
return t;
}
void relu_backward(Tensor* out) {
for (int i = 0; i < out->data->size; i++) {
out->prevs[0]->grad->values[i] = (out->prevs[0]->data->values[i] > 0) ? out->grad->values[i] : 0;
}
}
4.4 LogSoftmax前向&反向(分类任务核心)
用于多分类,数值稳定版,减去每行最大值防止exp溢出
c
Tensor* logsoftmax(Tensor* inp) {
int B = inp->data->shape[0];
int C = inp->data->shape[1];
Tensor* t = create_zero_tensor(inp->data->shape, inp->data->ndim);
for (int b = 0; b < B; b++) {
float maxv = inp->data->values[b * inp->data->strides[0]];
for (int c = 1; c < C; c++) {
int pos = b * inp->data->strides[0] + c * inp->data->strides[1];
if (maxv < inp->data->values[pos]) {
maxv = inp->data->values[pos];
}
}
float sumexp = 0.0f;
for (int c = 0; c < C; c++) {
int pos = b * inp->data->strides[0] + c * inp->data->strides[1];
float expval = expf(inp->data->values[pos] - maxv);
sumexp += expval;
}
for (int c = 0; c < C; c++) {
int pos = b * inp->data->strides[0] + c * inp->data->strides[1];
t->data->values[pos] = inp->data->values[pos] - maxv - logf(sumexp);
}
}
t->op = LOGSOFTMAX;
t->num_prevs = 1;
t->prevs[0] = inp;
return t;
}
void logsoftmax_backward(Tensor* out) {
int B = out->data->shape[0];
int C = out->data->shape[1];
for (int b = 0; b < B; b++) {
float gradsum = 0.0f;
for (int c = 0; c < C; c++) {
gradsum += out->grad->values[b * C + c];
}
for (int c = 0; c < C; c++) {
int pos = b*C + c;
out->prevs[0]->grad->values[pos] += out->grad->values[pos] - expf(out->data->values[pos]) * gradsum;
}
}
}
4.5 Autograd反向入口函数backward
递归实现链式求导,匹配算子调用对应backward函数
c
void backward(Tensor* t) {
if (t->op == MUL) {
mul_backward(t);
} else if (t->op == MEAN) {
mean_backward(t);
} else if (t->op == MATMUL) {
matmul_backward(t);
} else if (t->op == RELU) {
relu_backward(t);
} else if (t->op == LOGSOFTMAX) {
logsoftmax_backward(t);
}
for (int i = 0; i < t->num_prevs; i++) {
backward(t->prevs[i]);
}
}
逻辑:拿到当前张量算子,执行反向计算,然后递归上游张量,一直追溯到权重参数。
4.6 MNIST训练循环完整代码片段
c
#define BATCH_SIZE 128
float lr = 0.005f;
// w1:784→128, w2:128→10
Tensor* w1 = rand_tensor((int[]){784,128},2);
Tensor* w2 = rand_tensor((int[]){128,10},2);
for (int iter = 0; iter < 1000; iter++) {
Tensor* batch_x = get_random_batch_x();
Tensor* batch_y = get_random_batch_y();
Tensor* w1_out = matmul(batch_x, w1);
Tensor* relu_out = relu(w1_out);
Tensor* w2_out = matmul(relu_out, w2);
Tensor* lout = logsoftmax(w2_out);
Tensor* mul_out = mul(lout, batch_y);
Tensor* loss = mean(mul_out);
loss->grad->values[0] = 1.0f;
backward(loss);
if (iter % 100 == 0) {
printf("batch: %d loss: %.6f\n", iter, loss->data->values[0]);
}
// SGD更新权重
for (int i = 0; i < w1->data->size; i++) {
w1->data->values[i] -= w1->grad->values[i] * lr;
w1->grad->values[i] = 0.0f;
}
for (int i = 0; i < w2->data->size; i++) {
w2->data->values[i] -= w2->grad->values[i] * lr;
w2->grad->values[i] = 0.0f;
}
free_tensor(w1_out);
free_tensor(relu_out);
free_tensor(w2_out);
free_tensor(lout);
free_tensor(mul_out);
free_tensor(loss);
}
训练流程:
- 随机采样batch图像、onehot标签;
- 前向传播得到logsoftmax输出;
- 负对数似然损失;
- 启动backward,自动计算w1,w2梯度;
- SGD更新权重,梯度清零;
- 释放临时张量内存,循环。
五、环境配置与完整测试教程
环境依赖
仅C语言编译器,支持C99标准
- Linux:gcc(gcc 7.5+)
- Windows:MinGW / MSVC
无任何第三方库,不需要BLAS、OpenCV,仅依赖标准库stdio.hstdlib.hmath.h
执行训练
bash
./train_mnist
测试用例全集
测试用例1:张量基础读写与stride验证(单元测试)
目的:验证Arr结构、stride寻址正确性
测试逻辑:创建shape(2,3)张量,手动赋值,通过stride读取元素。
预期:索引寻址结果与二维数组一致;reshape后不拷贝内存,仅修改shape/stride。
c
// 单元测试代码
void test_arr_stride() {
int shape[] = {2,3};
Arr* a = create_arr(shape,2);
a->values[0] = 1; a->values[1]=2; a->values[2]=3;
a->values[3] = 4; a->values[4]=5; a->values[5]=6;
compute_strides(a);
// a[1][0] = 4
int pos = 1 * a->strides[0] + 0 * a->strides[1];
printf("val = %.2f\n", a->values[pos]);
}
输出预期:val = 4.00
测试用例2:算子正向单测(matmul/relu/logsoftmax)
单独跑矩阵乘法,输入已知矩阵,核对输出结果。
校验:输出矩阵数值与手写矩阵乘法结果完全匹配。
测试用例3:反向传播梯度校验(数值梯度验证)
使用有限差分法 校验自动微分是否正确:
对权重w增加微小扰动eps=1e-5,计算损失变化率,对比Autograd输出梯度。
测试用例4:MNIST完整训练测试
参数配置:
-
网络:784输入 → 128隐层ReLU → 10输出LogSoftmax
-
Batch size:128,学习率0.005,迭代1000轮
预期日志输出:batch: 0 loss: -0.092145
batch: 100 loss: -0.621431
batch: 200 loss: -0.782312
batch: 300 loss: -0.841256
...
训练趋势:损失值不断下降;训练结束在测试集达到约95%识别准确率。
测试用例5:内存泄漏测试
循环多次训练、创建销毁Tensor,使用valgrind检测内存:
bash
valgrind ./train_mnist
预期:无内存泄漏,所有Tensor、Arr内存全部释放。
六、落地用途
- 深度学习底层原理学习
跳过高层框架封装,理解张量存储、步长、计算图、自动微分,适合AI工程师面试底层原理学习。 - 嵌入式端轻量神经网络推理原型
代码极简纯C,无依赖,可移植到MCU、嵌入式设备,快速验证小型全连接网络训练/推理逻辑。 - 教学Demo
课堂/技术分享演示自动微分原理,直观展示前向、反向传播完整链路。 - 二次扩展
可继续增加卷积算子、Adam优化器、GPU CUDA算子扩展,作为自定义深度学习框架起点。
If you need the complete source code, please add the WeChat number (c17865354792)
七、总结
这个微型张量库剥离了工业级框架所有复杂工程封装,只保留深度学习最核心数学逻辑。
张量不是黑盒,自动微分也不是魔法:本质就是记录计算链路,链式法则逐层求导 。
读懂这套C代码,就理解PyTorch Autograd底层最核心原理。后续可以基于这个基础,继续扩展卷积、优化器,或者迁移CUDA加速算子。
Welcome to follow WeChat official account【程序猿编码】