
简介面向毕业设计及深度学习初学者这份基于Python卷积神经网络CNN的图像分类系统源码覆盖LeNet-5、AlexNet、GoogLeNet、ResNet等经典网络结构并配有可直接使用的数据集与预训练模型。压缩包共25个文件以13个py源码文件为主另有模型备份zbak、类别索引json、说明文档及前端静态资源整体约62KB代码精简、目录明确便于本地运行与二次修改。目前已有64人学习下载适合作为课程设计、毕业设计或CNN入门实践的参考基座。资源同时提供TensorFlow与PyTorch两套框架实现包含数据预处理、模型训练、准确率/召回率等评估指标及可视化模块结合README可快速理解从数据加载到分类输出的完整流程还能基于class_indices.json调整类别映射进而开展网络调优与功能扩展实验。系统采用模块化设计py文件分工明确搭配详尽文档可逐步研读并复现CNN图像分类全流程。1. 图像分类系统能直接跑通的CNN完整工程如果你做图像分类相关的毕业设计或者工作中临时要搭一个图像识别Demo最怕遇到的事我经历过网上找到一个源码包下载下来发现数据集是空的、权重文件不存在、代码里到处是路径报错。花一晚上把环境配好最后连一个完整的训练流程都跑不起来。这份基于Python的CNN图像分类系统没有这个毛病——数据、训练好的模型、四代经典网络结构LeNet-5、AlexNet、GoogLeNet、ResNet的TensorFlow和PyTorch两套实现、Flask推理页面全都在压缩包里。拿到手最快十分钟内就能跑通一次完整预测然后你再去啃训练代码和网络定义。适合正在做课程设计的学生也适合第一次正经搭建图像分类系统的人。CNN基础过关之后这套工程能帮你把理论落地成可交付的项目。2. 网络结构选型从LeNet-5到ResNet的演进逻辑2.1 CNN四个基本组件在代码里的具体落点卷积层、池化层、激活函数、全连接层——这四个组件组成了CNN的基本骨架。很多人学CNN的时候书看了好几遍卷积公式背得下来但一打开源码不知道哪一行对应哪个组件。这套工程里的LeNet-5实现就是很好的参照物。# TensorFlow版LeNet-5网络定义节选 from tensorflow.keras import layers, models model models.Sequential([ # 卷积层提取局部特征核大小为5x5 layers.Conv2D(6, (5, 5), activationrelu, input_shape(32, 32, 3)), # 池化层下采样降低特征图尺寸 layers.AveragePooling2D((2, 2)), # 第二组卷积池化 layers.Conv2D(16, (5, 5), activationrelu), layers.AveragePooling2D((2, 2)), # 展平把二维特征图拉成一维向量 layers.Flatten(), # 全连接层学习全局组合特征并分类 layers.Dense(120, activationrelu), layers.Dense(84, activationrelu), layers.Dense(10, activationsoftmax) ])Conv2D第一个参数是输出通道数第二个参数是卷积核尺寸。LeNet-5原始论文输入是单通道灰度图但这份工程里input_shape是(32, 32, 3)也就是RGB三通道彩色图说明作者已经为现代数据集做了适配。AveragePooling2D是平均池化LeNet-5原始设计用的就是它。后来AlexNet时代大家更习惯用最大池化区别在于取邻域最大值还是平均值——平均池化对背景噪声更平滑最大池化对纹理边缘的响应更强烈。具体哪个好取决于你的图像内容不是绝对的。Flatten层不产生新参数它只是把卷积输出从二维特征图转成一维向量让全连接层能够接收。三个Dense层里前两个用relu激活最后一层用softmax输出10个类别的概率分布。如果你的数据集不是10类最后这个数字必须修改这个坑我在第5章详细讲。2.2 AlexNet、GoogLeNet与ResNet结构演进解决了什么问题LeNet-5是1998年的设计在简单手写数字集上表现优秀但把它搬到复杂自然图像上就吃力了。AlexNet在2012年拿下ImageNet冠军它把通道数拉大、卷积核变大、堆叠更多卷积层并且引入了ReLU和Dropout。ReLU解决了深层网络中sigmoid梯度消失的问题训练速度明显变快。Dropout则在训练时随机丢弃一部分神经元让模型不容易记住训练集里的偶然噪声。GoogLeNet换了一条思路它不再单纯把网络做深而是设计Inception模块——同一个层里并列使用1x1、3x3、5x5三种尺寸的卷积核再加上一个池化分支然后把这四路输出在通道维度拼接起来。这样做的意义是不同尺寸的卷积核感受野不同小卷积核捕捉细节纹理大卷积核捕捉更大范围的结构让网络自己决定当前层应该侧重哪种尺度。工程里给出的GoogLeNet实现对比LeNet-5明显复杂但你训练图片时可以直观感受到它对复杂场景的泛化能力更好。ResNet是2015年提出的残差网络。它的核心设计是跳跃连接skip connection每一层除了正常映射F(x)之外还直接让输入x绕路加到输出上去让这一层实际学习的是F(x) - x这个残差。这种做法的好处是梯度可以沿着跳跃连接直接回传即使网络叠到五六十层也不会出现梯度消失。在这套工程里ResNet是训练效果最稳定、准确率上限最高的模型代价是训练时间更长。我一般会把ResNet当作最终方案把LeNet-5当作快速验证的试水方案。2.3 框架与网络的对应关系TensorFlow和PyTorch双版本这个工程最直观的一个设计是同一种网络结构同时给了TensorFlow和PyTorch两个实现。目录里很清楚地列出了2.AlexNet、4.GoogLeNet、5.ResNet这些目录名每个目录里应该同时有tf和torch两种文件。# PyTorch版ResNet残差块实现节选 import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() # 第一个卷积改变通道数和尺寸 self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) # 第二个卷积保持尺寸不变 self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # 跳跃连接输入与输出尺寸不同时用1x1卷积对齐 self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity self.shortcut(x) out self.conv1(x) out self.bn1(out) out nn.ReLU()(out) out self.conv2(out) out self.bn2(out) # 残差连接将输入加到卷积输出上 out identity return nn.ReLU()(out)这个BasicBlock是ResNet-18和ResNet-34的基本单元。注意里面加入了BatchNorm层它的作用是在每一层对输入做归一化让数值分布稳定在零附近能有效加速收敛。shortcut分支处理了通道数不一致的情况——当stride不等于1或输入输出通道数不同跳跃连接必须经过1x1卷积调整形状才能相加。选TensorFlow还是PyTorch取决于你后续的工作流。TensorFlow的Keras API更简洁适合快速把工程跑起来PyTorch的动态计算图让调试更灵活改网络结构时更容易定位问题。这份工程两边都有正好可以拿同一个网络结构在两个框架下分别训练对比一下收敛速度和最终精度这对理解框架本身也是很好的训练。3. 训练全流程落地从数据预处理到评估指标3.1 数据集组织目录结构决定了训练脚本怎么读拿到压缩包解压之后你会看到数据集和训练好的模型放在一起还有多个以网络结构命名的目录。一个实际能跑的CNN项目数据集目录结构通常遵循ImageNet风格的分类方式。# 数据集目录的标准组织方式 dataset/ ├── train/ │ ├── class_a/ │ │ ├── img_001.jpg │ │ └── img_002.jpg │ └── class_b/ └── validation/ ├── class_a/ └── class_b/每个类别一个子文件夹文件夹名就是类别名里面放对应的图片。keras的ImageDataGenerator.flow_from_directory和PyTorch的torchvision.datasets.ImageFolder都是按这个约定来读取的。数据预处理环节做的事情是缩放、归一化和数据增强。# 训练时的数据增强配置TensorFlow from tensorflow.keras.preprocessing.image import ImageDataGenerator # 对训练集做实时增强验证集只做归一化 train_datagen ImageDataGenerator( rescale1.0 / 255, # 像素归一化到[0,1] rotation_range20, # 随机旋转20度 width_shift_range0.2, # 水平平移20% height_shift_range0.2, # 垂直平移20% shear_range0.2, # 错切变换 zoom_range0.2, # 随机缩放 horizontal_flipTrue # 随机水平翻转 ) val_datagen ImageDataGenerator(rescale1.0 / 255)这里的rescale必须放在最前面它把原始0到255的像素值压缩到0到1区间这是深度学习模型的默认输入范围。rotation_range、shift、zoom这些参数叠加之后每轮epoch模型看到的图片都是经过随机变换的版本相当于变相扩充了训练数据量——这是防止过拟合最有效的手段之一。参数大小需要根据你的数据量和任务来调整。数据只有几百张图片时rotation_range给到20、horizontal_flip开启就够了往大了给比如90度旋转反而会让模型学到不真实的图像分布——现实中不会出现倒立的猫。3.2 超参数设置学习率、batch size与epoch的搭配逻辑训练CNN最关键的三个超参数是学习率、batch size和epoch数。工程训练好的模型你直接用没问题但如果你想从头训一个自己的模型这三个参数得理解清楚。# 训练超参数配置PyTorch版 import torch.optim as optim # 学习率控制每次参数更新的步长 learning_rate 0.001 batch_size 32 epochs 50 optimizer optim.Adam(model.parameters(), lrlearning_rate) # 每经过10个epoch学习率降为原来的10分之一 scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1)学习率是这些参数里最敏感的一个。0.001是Adam优化器下最常见的学习率起点不算快也不算慢。如果你的显存允许更大的batch size可以适当把学习率调大因为更大batch的梯度估计更稳定。常见做法是batch 64配合0.001batch 32配合0.0005。scheduler的作用是训练后期缩小步长——刚起步时大步往前冲接近收敛时小步微调这样能把模型推进到更优化点。epoch数的判断标准不是固定的关键看验证集loss。工程文档里建议观察训练过程曲线在验证集loss连续5个epoch不再下降时提前停止。训练集准确率和验证集准确率的差距如果越拉越大说明模型开始过拟合训练集此时要么加Dropout、要么增强数据、要么提前结束训练。3.3 评估指标准确率之外还应该关心什么这套工程的模型文件里带了class_indices.json这个JSON文件记录的是类别索引和类别名称的映射关系。查看模型评估结果时不能只看准确率——准确率是预测正确的样本总数除以全部样本数在类别不平衡时它会产生比较大的误导。# 分类评估脚本的核心内容 from sklearn.metrics import classification_report # 加载class_indices.json得到类别映射 # predictions: 模型输出的预测标签 # true_labels: 真实标签 print(classification_report(true_labels, predictions))classification_report会输出每一类的精确率、召回率、F1分数。精确率解决的是模型预测成A类的样本里有多少真的属于A类召回率解决的是真实的A类样本里有多少被模型正确找出来了。F1是两者的调和平均类别不平衡时比准确率更能反映模型真实水平。图片分类场景里还有一个常见操作是查看混淆矩阵。工程里Matrix.py明显就是干这个用的——预测结果和真实标签生成一个二维矩阵横轴是预测类别纵轴是真实类别对角线越亮说明分类越准非对角线的亮点暴露的是模型容易混淆的类别对。比如猫和狗互相误判说明模型学到的是相似特征如果某个类别被大面积误判为另一个优先检查数据集里两个类别的图片标注是否有误。4. 把源码跑起来环境配置与Web推理全流程4.1 环境搭建Python版本与依赖库怎么配这套工程跨TensorFlow和PyTorch两个框架环境配置是第一道坎。首先确保Python版本在3.8到3.10之间太新的Python版本有时会遇到某些库还没出对应whl包的问题。# 建议创建独立虚拟环境 python -m venv cnn_env source cnn_env/bin/activate # PyTorch版本安装CPU版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # TensorFlow版本安装 pip install tensorflow # Web与工具库 pip install flask numpy pillow scikit-learn这里有个容易被忽略的点如果你的机器没有NVIDIA显卡不要直接pip install tensorflow默认版本之外的东西也不要强行装CUDA版PyTorch。CPU版本虽然训练慢但推理完全够用。工程里训练好的模型可以直接加载做预测CPU上单张图片推理时间在几百毫秒级别完全能接受。我自己一般会把训练和推理环境分开训练在GPU服务器上跑部署推理时用CPU版本打包这样目标机器不用装巨大的CUDA运行时。4.2 Flask推理服务main.py和class_indices.json如何分工这套工程带了一个完整的Web推理界面入口就是APP目录下的main.py配合templates目录里的HTML模板。加载模型、处理上传图片、输出预测结果这条链路从main.py里的Flask路由开始。# APP/main.py中的预测核心逻辑 from flask import Flask, request, jsonify, render_template import numpy as np from PIL import Image import json app Flask(__name__) # 启动时加载训练好的模型权重 model load_model(path/to/resnet_model.h5) # 读取类别索引映射 with open(class_indices.json, r) as f: class_names {int(k): v for k, v in json.load(f).items()} def preprocess_image(img): # 缩放到网络输入尺寸 (224, 224) img img.resize((224, 224)) # 转成numpy数组并归一化 arr np.array(img) / 255.0 # 增加batch维度 (224,224,3) - (1,224,224,3) return np.expand_dims(arr, axis0) app.route(/predict, methods[POST]) def predict(): file request.files[image] img Image.open(file.stream) processed preprocess_image(img) # 模型前向推理 preds model.predict(processed)[0] top_idx int(np.argmax(preds)) # 返回类别名和置信度 return jsonify({ class: class_names[top_idx], confidence: float(preds[top_idx]) }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)class_indices.json在这里起到桥梁作用。模型输出的数字索引比如0、1、2本身没有可读性JSON文件把每个索引对应到真实类别名。这个JSON文件必须在模型训练时同步生成——训练完成后遍历训练数据集的文件夹名按照顺序写入索引映射。如果后续重训模型时改了类别顺序这个JSON文件必须重新生成否则预测结果会对不上。templates目录里的HTML文件提供了页面上传控件Flask把渲染的页面和预测接口串起来浏览器端就能直接操作。这个设计很适合毕业设计演示——不用写前端代码打开浏览器就能用。4.3 用自己的图片做一次完整预测把服务启动之后验证整个流程是否通了。在命令行里跑python main.py启动服务浏览器打开本地地址上传一张图片看到的预测结果如果和直觉一致说明整个链路是通的。# 启动Flask服务在APP目录下执行 python main.py # 另开一个终端用curl测试预测接口 curl -X POST -F imagetest_cat.jpg http://127.0.0.1:5000/predictcurl命令会返回JSON格式的预测结果比浏览器操作更直接。注意图片尺寸不是固定的代码里已经做了resize处理但不同类型任务的resize策略不同——224x224是ResNet标准输入而LeNet-5要求32x32的输入。所以如果你换网络main.py里preprocess_image的resize尺寸必须同步改否则模型会直接报数组维度的错误。5. CNN训练与部署避坑指南五次翻车记录5.1 现象加载模型时报尺寸不匹配第一次加载工程自带的训练好的模型直接报错提示层名称不匹配或者维度对不上。原因大概率是加载代码里指定的网络结构和训练时保存的不一致——我用TensorFlow的ResNet定义去加载PyTorch训练出来的权重文件。这个工程两个框架的模型都存在文件名也区分了框架但容易看走眼。解决方法是先看清权重文件所在目录里是tf还是torch后缀再编写对应的加载代码。经验是模型加载之前先打印一遍网络结构的摘要和权重文件对应一下不要跳过这步直接跑。5.2 现象训练时loss下降很慢甚至不下降按工程给的默认参数跑训练每个epoch的loss几乎不动。先检查学习率是不是被scheduler降得太狠了StepLR的gamma设为0.1意味着每经过一个step学习率直接除以10如果初始值设置不当后期学习率会小到几乎没有更新。另一个常见原因是没有做数据归一化。有些代码忘了除以255就把原始像素输入了网络取值范围0到255和归一化后的0到1相比梯度传播会变得非常不稳定。黑白检查一遍输入数据归一化、学习率不是0.0001级别、权重初始化没被随机破坏。5.3 现象训练集精度99%验证集只有60%这是典型的过拟合在网络参数足够多、训练数据量不足时极易发生。工程里的ResNet如果直接在自己少量图片上从零训练两个epoch就能把训练集完全记住。解决路径有三条数据增强从无到有打开或加大强度Dropout比率从0.5调高到0.7最省力的是加载预训练权重做迁移学习把前面卷积层冻结住只训练最后几层全连接层。第三类是这类工程里泛化最稳定的方案。5.4 现象启动Flask时端口被占用跑通了所有代码启动main.py直接报Address already in use这是端口冲突因为默认的5000端口可能被系统或其他进程占用了。检查一下端口占用情况# 查看5000端口被谁占用 netstat -ano | grep 5000Windows上netstat结合findstrLinux上是grep找到占用进程后杀掉或者简单改main.py最后一行代码的port参数比如改成5001。真实部署时这种细节很影响效率——我后来习惯在启动脚本里用变量控制端口避免每次手工改代码。5.5 现象换了数据集直接报类别数量错误拿了工程里的模型想直接用在相同的数据集上改dropout或者调整卷积核通道数时全连接层输出维度和类别数不匹配。原因很直接最后全连接层的输出必须是类别个数数据集的类别数改了却忘了同步修改这一层。替换掉最后一层Dense把输出改成新数据集的类别数重新训练。如果是用预训练模型做迁移学习这就是标准的迁移学习操作新模型依然能继承前面卷积层提取到的通用视觉特征。6. 把模型迁移到自己的数据集类别数调整与实战闭环从零训练一个ResNet需要大量数据、GPU算力和不少时间但绝大多数现实场景里你不需要这么做。加载别人在大规模数据上训练好的权重保留前面的卷积层作为通用特征提取器只重训最后几层分类头——这就是迁移学习也是把这套工程用在自定义数据上最实用的路径。# TensorFlow迁移学习核心代码 base_model tf.keras.applications.ResNet50( include_topFalse, # 不包含分类头 weightsimagenet, # 加载预训练权重 input_shape(224, 224, 3) ) # 冻结前100层只训练后面的部分 for layer in base_model.layers[:100]: layer.trainable False model tf.keras.Sequential([ base_model, layers.GlobalAveragePooling2D(), # 全局平均池化 layers.Dense(256, activationrelu), layers.Dropout(0.5), # 防止过拟合 # 你的类别数在这里 layers.Dense(num_classes, activationsoftmax) ]) model.compile( optimizertf.keras.optimizers.Adam(lr0.0001), losscategorical_crossentropy, metrics[accuracy] )include_topFalse去掉了预训练模型最顶层的分类器只保留卷积主干。weightsimagenet意味着加载在ImageNet数据集上训练好的权重——这个权重学到了通用视觉特征比如边缘、纹理、形状组合。冻结前100层让它们不被反向传播更新只训练后来的全连接层。这样即使你只有几百张图片分类效果也能不错。类别数改完之后训练数据目录结构必须与类别数对应每类少说准备五十张以上图片。class_indices.json重新生成是必做操作用脚本遍历训练集目录构建新的映射关系# 重新生成class_indices.json import os, json classes os.listdir(dataset/train) class_indices {str(i): name for i, name in enumerate(sorted(classes))} with open(class_indices.json, w) as f: json.dump(class_indices, f)迁移学习的另外一个关键点是学习率。预训练权重已经处在一个相对稳定的状态基于它的微调通常用更小的学习率——0.0001是常见的选择比从零训练低一个数量级避免一步更新就把预训练学到的特征破坏掉。训练过程观察验证集曲线通常十几个epoch就能达到可接受的效果。从那以后我每次拿到类似工程都会先走一遍这个流程确认数据、核对类别、看一遍网络定义、再动手训练。因为很多看起来像是模型效果差的玄学问题排查到最后都是数据组织或类别映射的小错误——顺序和耐心比调参技巧更可靠。希望这套流程对你也有帮助。本文还有配套的精品资源点击获取