CIFAR-10 Tiny CNN 教程:用 C 语言实现小型卷积神经网络图像分类
CIFAR-10 Tiny CNN 教程:用 C 语言实现小型卷积神经网络图像分类
站内搜索
直接问 AI

CIFAR-10 Tiny CNN 教程:用 C 语言实现小型卷积神经网络图像分类

这是一篇 CIFAR-10 Tiny CNN 教程。读完以后,你可以用一个单文件 C 程序完成小型卷积神经网络图像分类:读取 CIFAR-10 binary 数据、做 3 x 3 卷积、ReLU、2 x 2 最大池化、全连接、softmax,并运行一次小样本训练。

这篇文章基于本地项目里的 cifar10_tiny_cnn.c。它不是为了追求高准确率,而是为了把 CNN 的每一层变成能直接阅读和修改的 C 代码。

如果你刚开始学习神经网络,建议先读 神经网络基础:从感知机到多层网络。如果你已经看过 手写数字 Softmax 分类器,这篇就是从线性分类器进入卷积神经网络的下一步。

一、CIFAR-10 输入是什么样

CIFAR-10 的每张图片是 32 x 32 的彩色图像,有红、绿、蓝 3 个通道,所以程序里的输入形状是 3 x 32 x 32。每条样本对应一个类别标签,类别总数是 10。

源码里用几个常量把这个结构固定下来:

#define IMG_C 3
#define IMG_H 32
#define IMG_W 32
#define NCLASS 10

这里最值得注意的是:图像不再像手写数字项目那样拉平成一条 784 维向量后直接分类。CNN 会先保留二维空间结构,让卷积核在局部区域上滑动,提取边缘、颜色块和纹理模式。

二、Tiny CNN 模型结构和参数量

当前代码实际使用 16 个 3 x 3 卷积核。因为是 valid convolution,32 x 32 输入经过 3 x 3 卷积后会变成 30 x 30;再经过 2 x 2 最大池化后,空间大小变成 15 x 15。

#define NF 16
#define K 3
#define CONV_H (IMG_H - K + 1)
#define CONV_W (IMG_W - K + 1)
#define POOL_H (CONV_H / 2)
#define POOL_W (CONV_W / 2)
#define FEAT (NF * POOL_H * POOL_W)

所以完整路径是:

  • 输入:3 x 32 x 32
  • 卷积:16 个 3 x 3 卷积核,输出 16 x 30 x 30
  • ReLU:把负数激活压到 0
  • 最大池化:2 x 2,输出 16 x 15 x 15
  • 全连接:把池化结果映射到 10 个类别 logit
  • softmax:把 logit 转成类别概率

这份 tiny CNN 的参数量很小:卷积层有 16 * 3 * 3 * 3 + 16 = 448 个参数,全连接层有 10 * 3600 + 10 = 36010 个参数,总计 36458 个参数。这个规模适合教学和调试,不适合和现代 CIFAR-10 模型比较分数。

三、卷积层在 C 里怎么写

卷积层的核心就是多层循环。每个卷积核都会扫过输出网格中的每个位置,再对 3 个颜色通道和 3 x 3 局部窗口做乘加。

float sum = net->conv_b[f];
for (int c = 0; c < IMG_C; c++) {
    for (int r = 0; r < K; r++) {
        for (int t = 0; t < K; t++) {
            sum += net->conv_w[f][c][r][t] *
                   get_pixel(s->x, c, i + r, j + t);
        }
    }
}
conv[f][i][j] = sum > 0.0f ? sum : 0.0f;

最后一行同时完成了 ReLU。这样写很朴素,但能清楚看到卷积核、通道、局部窗口和输出位置之间的关系。

四、最大池化保留局部最强响应

池化层把每个 2 x 2 小窗口压成一个值,只保留最大激活。代码还记录了最大值的位置,因为反向传播时梯度只会传回当时胜出的那个位置。

float best = conv[f][base_i][base_j];
int best_idx = 0;
for (int di = 0; di < 2; di++) {
    for (int dj = 0; dj < 2; dj++) {
        float v = conv[f][base_i + di][base_j + dj];
        if (v > best) {
            best = v;
            best_idx = di * 2 + dj;
        }
    }
}

这一步会降低特征图尺寸,也让模型对小范围平移更稳一点。

五、全连接层和 softmax

池化后的特征会被展开成一维数组,再进入全连接层。每个类别有一组权重,输出 10 个 logit。

for (int k = 0; k < NCLASS; k++) {
    float z = net->fc_b[k];
    for (int p = 0; p < FEAT; p++) {
        z += net->fc_w[k][p] * feat[p];
    }
    logits[k] = z;
}

softmax 会把这 10 个原始分数归一化成概率。训练时用交叉熵损失,预测时选择概率最大的类别。

六、反向传播更新了哪些参数

这个 C 程序没有把反向传播藏进框架里。它显式做了四件事:

  • 从 softmax 概率减去真实标签,得到输出层梯度
  • 更新全连接层权重和偏置
  • 把梯度通过最大池化和 ReLU 传回卷积输出
  • 用输入局部窗口更新每个卷积核权重
net->fc_w[k][p] -= LR * dlogits[k] * feat[p];
net->fc_b[k] -= LR * dlogits[k];
...
net->conv_w[f][c][r][t] -= LR * dw;
net->conv_b[f] -= LR * db;

这也是这份代码最有学习价值的部分:你能看到 CNN 训练不是“神秘自动完成”,而是每一层都把自己的梯度算清楚,再把参数往降低损失的方向挪一步。

七、如何本地编译运行

站点只发布源码、权重样例、预测样例和说明文件,不上传完整 CIFAR-10 数据包。你需要从 CIFAR-10 官方页面下载 binary version,解压出 cifar-10-batches-bin 后再运行。

gcc -O2 -std=c11 cifar10_tiny_cnn.c -lm -o cifar10_tiny_cnn
./cifar10_tiny_cnn ./cifar-10-batches-bin 1 2000 1000

这四个命令行参数分别表示:

  • 数据目录
  • 训练轮数
  • 训练样本上限
  • 测试样本上限

用小样本先跑通更适合学习和调试。确认流程没问题后,再逐步提高训练样本数量和轮数。

八、训练结果:loss 和 accuracy

我用 2000 条训练样本、1000 条测试样本、1 个 epoch 跑了一次小样本训练,真实输出如下:

Loaded train=2000, test=1000
epoch 1 step 1000/2000 loss=2.0507 train_acc_recent_est=0.809
epoch 1 step 2000/2000 loss=1.9440 train_acc_recent_est=0.835
epoch 1 done: avg_loss=1.9440 train_acc=0.835 test_acc=0.284
Model saved to model_weights.bin
Predictions saved to test_predictions.csv

这里最重要的是不要误读结果。训练集估计准确率比较高,但测试准确率只有 0.284,说明这个小模型和这次小样本训练更适合教学演示,而不是最终性能模型。后续可以通过更多训练数据、更多 epoch、mini-batch、数据增强和更稳定的优化策略来提高泛化能力。

九、输出文件怎么看

程序结束后会写出两个文件:

  • model_weights.bin:当前模型参数,方便保留一次训练结果
  • test_predictions.csv:测试样本预测结果,可用来检查输出格式和类别分布

站点资源库里放的是小型公开配套资源,不包含完整训练数据。这样可以让文章、下载资源和实验记录保持可访问,同时避免把大型公开数据集重复托管到网站上。

十、如何改进 CIFAR-10 Tiny CNN 的效果

如果你的目标是提升准确率,而不只是理解 CNN 结构,可以优先尝试这些方向:

  • 训练更多数据:把训练样本上限从 2000 提高到 10000、20000 或完整训练集
  • 增加训练轮数:1 个 epoch 只能说明流程能跑通,不能代表模型充分训练
  • 加入 mini-batch:当前代码是单样本更新,速度和稳定性都有限
  • 做数据增强:随机裁剪、水平翻转、颜色扰动都能帮助 CIFAR-10 泛化
  • 加深模型:增加第二个卷积层或使用更大的通道数
  • 改用框架复现:如果目标是工程效率,可以把这份 C 代码改写成 PyTorch 版本

十一、这个项目的边界

这份 tiny CNN 更像教学实现,不是生产级图像分类器。它的边界很明确:

  • 纯 C 单样本训练,速度明显慢于现代深度学习框架
  • 没有 mini-batch、数据增强、正则化和学习率调度
  • 网络很小,准确率不会接近现代 CIFAR-10 模型
  • 主要目标是理解卷积网络的前向传播和反向传播

如果你已经读过手写数字 softmax 项目,可以把这篇看成下一层台阶:从线性分类器进入有局部感受野的图像模型。

十二、CIFAR-10 实验审计表

CIFAR-10 Tiny CNN 的目标不是刷榜,而是把卷积、池化、softmax 和反向传播拆开验证。下面的审计表可以帮助读者复现实验时判断:一次运行证明了什么,没有证明什么。

审计项 应该记录 说明什么 不能说明什么
数据来源 CIFAR-10 binary 目录、训练/测试样本上限、类别映射。 输入是否来自正确格式,类别是否对齐。 不能证明全量数据训练表现。
训练设置 epoch、学习率、样本数、随机初始化和编译参数。 这次输出是否可复现。 不能代表所有超参数组合。
泛化信号 train_acc、test_acc、loss 变化和预测类别分布。 小模型是否过拟合,测试集是否明显弱于训练集。 不能证明模型适合生产图像分类。
输出文件 model_weights.bintest_predictions.csv、样例行。 训练结果是否被完整导出。 不能替代独立验证集和错误样本分析。

十三、常见问题 FAQ

这个教程是 PyTorch CIFAR-10 教程吗?

不是。这篇文章故意使用 C 语言实现 CNN,目的是看清卷积、池化、softmax 和反向传播的底层计算。后续如果写 PyTorch 版本,会单独作为一篇工程实现文章。

为什么测试准确率不高?

因为这是一个很小的 CNN,而且示例只跑了 2000 条训练样本和 1 个 epoch。它的价值是教学和可解释,不是刷新 CIFAR-10 分数。

完整 CIFAR-10 数据在哪里?

站点不重复托管完整 CIFAR-10 数据包。请从 CIFAR-10 官方页面下载 binary version,再把解压后的 cifar-10-batches-bin 目录传给程序。

这篇文章和手写数字项目有什么关系?

手写数字数据结构文章Softmax C 分类器文章 先讲线性分类;这篇文章继续讲图像分类里的局部感受野、卷积核和池化。

十四、配套资源和下一步阅读

可以在资源库中下载 C 源码模型权重样例预测样例CNN 说明 PDF。完整 CIFAR-10 数据请从官方来源获取。

继续学习可以按这个顺序走:先补 神经网络基础,再看 C 语言 Softmax 分类器,然后进入这篇 CIFAR-10 CNN。所有配套文件也可以从 资源库 集中打开。

发表回复

向下探索