不用 PyTorch!纯 C 实现 N 维张量 + 反向传播,完成手写数字识别训练

一、项目释义

深度学习框架的本质是什么?

PyTorch、TensorFlow这类框架底层核心就是张量存储 + 算子实现 + 自动微分引擎。很多同学学习深度学习,直接调用高层API,只懂调包,不明白反向传播、梯度怎么一步步算出来。

本项目使用标准C语言,仅依靠C标准库,用大约500行代码,实现一个极简张量库。

项目目标:

  1. 自定义N维多维数组(张量)结构,实现shape、stride步长机制,支持任意维度张量;
  2. 实现基础算子:矩阵乘法、ReLU、LogSoftmax、逐元素乘、均值,同时编写对应反向传播函数;
  3. 手写计算图Autograd自动微分,基于链式法则递归求梯度;
  4. 搭建简易全连接网络,完成MNIST手写数字训练,实现端到端训练闭环;
  5. 所有计算逻辑全部手写,零外部机器学习库依赖,编译仅需要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大模块,模块之间低耦合:

  1. 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:总元素个数。
  1. 自动微分张量封装模块(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:算子额外参数(例如运算轴)。
  1. 算子库模块
    每个算子由两部分组成:
  • forward函数:接收输入Tensor,输出新Tensor,记录算子类型与依赖;
  • backward函数:接收输出张量,回传梯度到上游prevs张量。
  1. 训练循环模块
    加载数据集,构造网络,前向推理计算损失 → 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);
}

训练流程:

  1. 随机采样batch图像、onehot标签;
  2. 前向传播得到logsoftmax输出;
  3. 负对数似然损失;
  4. 启动backward,自动计算w1,w2梯度;
  5. SGD更新权重,梯度清零;
  6. 释放临时张量内存,循环。

五、环境配置与完整测试教程

环境依赖

仅C语言编译器,支持C99标准

  • Linux:gcc(gcc 7.5+)
  • Windows:MinGW / MSVC
    无任何第三方库,不需要BLAS、OpenCV,仅依赖标准库stdio.h stdlib.h math.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内存全部释放。

六、落地用途

  1. 深度学习底层原理学习
    跳过高层框架封装,理解张量存储、步长、计算图、自动微分,适合AI工程师面试底层原理学习。
  2. 嵌入式端轻量神经网络推理原型
    代码极简纯C,无依赖,可移植到MCU、嵌入式设备,快速验证小型全连接网络训练/推理逻辑。
  3. 教学Demo
    课堂/技术分享演示自动微分原理,直观展示前向、反向传播完整链路。
  4. 二次扩展
    可继续增加卷积算子、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【程序猿编码】

相关推荐
云和数据.ChenGuang1 小时前
langchain4j的RAG入门
人工智能·深度学习·机器学习·语言模型·fastapi
CallFay云起未来1 小时前
AI客服上线后多久才能回本?从TCO到ROI的完整测算方法
java·大数据·人工智能·架构·文心一言
Funny_AI_LAB1 小时前
重构 Coding Agent:当 LLM 没有 KV Cache 时,上下文工程该怎么做?
人工智能·经验分享·语言模型·重构
GISMagic1 小时前
AI 时代的软件工程与架构能力提升路线
人工智能·架构·软件工程
中伟视界2 小时前
化工检修作业新规明日施行,边缘AI如何守好现场?
人工智能
OpenCSG2 小时前
OpenCSG Agentic-27B 正式开源:27B Dense 模型,Agent 执行能力实现全面跃升
人工智能·开源·opencsg
Elastic 中国社区官方博客2 小时前
使用 Lucene 搜索你的 Bean —— Elasticsearch
大数据·开发语言·人工智能·elasticsearch·搜索引擎·全文检索·lucene
雪兽软件2 小时前
AI爆改学术研究?
人工智能·学术研究
合调于形2 小时前
Rengong zhzzneng 《人工智能》词条汉语拼音字母标调拼写实测案例
人工智能·自然语言处理·人机交互·语音识别·学习方法