这是一篇 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.bin、test_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。所有配套文件也可以从 资源库 集中打开。