每年到了毕业设计选题季总有人问我想做图像识别方向有没有一个能落地又不太难啃的题目我通常会推荐基于Python卷积网络对甘蔗叶子病识别这个方向。它本质上是用卷积神经网络做作物叶片图像的分类任务拿足量的甘蔗叶部病害图片训练模型让模型自动判断叶片是健康还是患了某种病并给出类别与置信度。对计算机、人工智能、电子信息相关专业的学生来说这个题目很友好因为它覆盖了从数据处理到模型部署的完整流程演示起来也非常直观答辩时好讲。这里说句实在话这类题目看起来普通但每年真正做好的同学并不多。原因不是模型难而是很多人拿到数据后直接塞进网络里训练忽略了数据清洗、预处理、增强和评估这些细环节。这篇博文就是我按自己完整实践路线整理的记录尽量把每一步为什么这么做、踩过什么坑都写清楚方便你照着复现。1. 项目概述与整体思路1.1 选题背景与实际价值写论文时别急着堆模型先想清楚一个问题这个问题为什么值得做甘蔗是制糖工业的主要原料在热带和亚热带地域种植面积非常大叶部一旦爆发病害比如叶斑病、锈病、褐条病轻则影响光合作用重则导致整株蔗茎糖分积累下降甚至大面积减产。传统上农户把病叶送到农技部门或请专家下田看效率很低等确诊可能已经传播开了。一个能拍照自动识别病害的系统理论上能极大缩短诊断时间这种实用价值在答辩时特别加分。从技术角度看识别甘蔗叶片病害就是一个标准的图像分类问题输入是叶片照片输出是病害类别。常见的病害图像在叶片纹理、斑点形状、颜色分布上有明显差异正好适合卷积神经网络来提取特征。比起做人脸识别、车牌识别那些已经被做到极致的类目选作物病害识别既不容易被质疑是不是照搬公开项目又确实有研究空间比如数据量少、拍摄环境复杂、病斑尺度差异大这些都是值得讨论的问题。对基础薄弱的人来说这个题目最友好的地方在于技术门槛可控。用预训练模型微调或者搭一个三到五层的小型CNN都能在几十个epoch内得到可用的效果。最终交付物可以是一个Python脚本加命令行工具也可以配一个简单的Web页面演示上传图片→输出识别结果这种闭环会让整篇论文显得非常完整。1.2 技术栈与框架选型我一般推荐用Python 3.8以上版本加PyTorch作为主框架辅助库包括OpenCV、Pillow、NumPy、Matplotlib和scikit-learn。OpenCV主要做图像读取和尺寸调整Pillow处理图像增强NumPy处理数组Matplotlib画训练曲线scikit-learn用来快速输出分类报告和混淆矩阵。整套环境在Anaconda里几分钟就能搭好显卡不是必需品——简单的图像分类任务用CPU训练小模型也能出结果只是慢一些有NVIDIA显卡就上CUDA训练速度会快很多。对比项PyTorchTensorFlow/Keras调试体验动态图机制print中间张量方便静态图需要习惯Graph和Session概念生态资源学术界和开源项目大多基于PyTorch生产部署工具链更广学习成本贴近Python直觉代码量少需要理解计算图思想毕业设计适配度容易讲清楚训练细节适合已有TF基础的人这个对比不是绝对的但我这些年帮人看代码的经验是如果项目周期只有三个月左右选PyTorch出活的概率更高。网络上有大量成熟教程遇到报错也更容易搜到解决方案。TensorFlow也不是不行只是遇到自定义逻辑时调试体验相对差一些毕业设计阶段没必要给自己加这种无谓的复杂度。1.3 整体流程拆解完整流程可以分成六步数据准备、数据预处理、模型设计、模型训练、模型评估、模型部署。前两步解决喂什么的问题中间两步解决怎么学的问题最后两步解决学得好不好、用不用得上的问题。很多人一上来就调模型顺序反了会非常痛苦。按这个顺序走每完成一步都有明确的产出物后续出了问题也容易定位是哪一环。数据准备阶段最耗时但也是最值得投入的。模型性能的天花板往往在数据质量上标注混乱、类别不平衡、背景干扰严重这些都是常见硬伤。预处理阶段把图片统一成模型能接受的格式顺便用增强变出更多变体。模型设计阶段考虑自建CNN还是迁移学习训练阶段重点盯loss和准确率的变化评估阶段用混淆矩阵看细分类表现最后部署成可演示的小应用。这条流水线跑通一次后续换任何图像分类题目都能快速复用。2. 数据集构建与预处理2.1 病害类别与数据规模数据是识别项目的地基地基不稳模型搞得再花哨也没用。我建议从5个类别开始健康叶片、叶斑病、锈病、褐条病、黄叶病。类别太少体现不出分类问题的复杂度太多又会导致某些类别样本不足5类是均衡的选择。甘蔗叶片病害在视觉上既有相似性又有差异比如叶斑病和褐条病都表现为叶片上的条状斑点而锈病则是明显的锈色孢子堆这些视觉特征是模型区分的依据。数据来源方面首选公开数据集。网上可以搜到一些农业病害公开图片库里面通常包含甘蔗叶部图像也有高校和科研院所发布过自己的甘蔗病害图像数据。如果找不到合适的自己拍摄也行但要注意拍摄环境的一致性尽量选晴天、自然光、叶片平铺在单一背景上避免背景里有其他物体干扰。每类至少准备300张原始图片再配合数据增强扩充训练起来才不会捉襟见肘。整个项目从零开始能采集到800到1000张有效样本配合增强使用对于小型CNN已经够跑了。实际做的时候建议把原始图片按品种、拍摄时间、设备分开记录因为后期做训练集和验证集划分时最忌讳的是同一株甘蔗的照片既出现在训练集又出现在验证集那样验证准确率会虚高答辩时被问到泛化能力会很被动。2.2 数据清洗与目录组织拿到原始图片后第一件事是清洗。把模糊的、拍糊的、叶片占画面比例过小的、明显标注错误的统统删掉。这一步听起来简单但真的不能省。我见过不少同学数据集里有大量重复图或错类图训练出来的模型准确率始终上不去最后排查半天发现是数据标错了。清洗时最好用看图软件按类别过一遍不要只看文件名就对号入座因为网络上下载的图片命名规则往往不一致。清洗完之后按下面的目录结构整理这在PyTorch里可以直接用torchvision.datasets.ImageFolder加载省去自己写标签映射的麻烦dataset/ ├── train/ │ ├── healthy/ │ ├── leaf_spot/ │ ├── rust/ │ ├── brown_stripe/ │ └── yellow_leaf/ ├── val/ │ └── ... └── test/ └── ...训练集、验证集、测试集按6:2:2划分。划分时用随机种子固定比如在代码开头设置random.seed(42)保证每次跑实验划分结果完全一致。这个细节在复现实验和整理论文数据时特别重要否则每次运行结果都对不上分析对比就无从下手。2.3 数据增强策略CNN非常吃数据量数据少就容易过拟合。增强的本质是对原图做一系列轻度扰动让模型看到更多变体从而学到更鲁棒的特征。常用手段包括随机水平翻转、随机旋转15度以内、随机裁剪缩放、调节亮度和对比度、轻微加噪。比如同一种病害可能因为拍摄角度不同、光照条件不同而表现不一样增强就是在模拟这些实际差异。增强方法参数示例解决的实际问题RandomResizedCropscale(0.8, 1.0)模拟拍摄距离和叶片占比变化RandomHorizontalFlipp0.5模拟叶片朝向的不同RandomRotationdegrees15模拟拍摄角度倾斜ColorJitterbrightness0.2, contrast0.2模拟光照条件变化需要注意两个原则一是增强强度不能太大旋转超过30度或亮度变化过狠会让图片失真反而干扰学习。病斑形态本身方向性不强所以旋转和翻转相对安全但颜色抖动要控制幅度。二是验证集和测试集不要做增强只做中心裁剪和归一化否则指标会虚高。还有一个容易忽略的点增强操作必须在训练每个epoch时随机生成而不是提前把图片固化增强好这样才能保证每个epoch都看到新的变体。2.4 图像预处理与数据加载预处理部分建议统一为224×224。具体做法先短边缩放到256再中心裁剪成224×224主要特征区域基本都能保留然后把像素值从0~255归一化到0~1最后按通用的均值标准差做标准化。如果你用了迁移学习这一步尤其重要因为预训练权重对输入分布是有要求的。在PyTorch里这一步可以组合成torchvision.transforms.Compose链配合torch.utils.data.DataLoader使用。batch size我一般设32如果你的显存只有4G以下降到16是可以接受的。num_workers设4左右能让GPU不吃亏。数据加载这一层写好后训练时模型几乎只需要关心张量输入和标签不用重复操心图像解码代码会干净很多。3. 卷积神经网络模型设计3.1 卷积神经网络为什么适合图像简单说卷积层通过一个固定大小的卷积核在图片上滑动提取局部特征比如边缘、颜色块、纹理多个卷积层叠起来底层提取低级特征、高层组合成高级语义特征。相比全连接网络卷积层的参数是共享的同一个卷积核会在整张图不同位置复用所以参数量小得多不容易过拟合。池化层则做下采样缩小特征图尺寸、扩大感受野相当于让网络用更大的视野看问题。这里有一个常被忽视的点感受野。做病害识别时有些病斑很小可能只有十几个像素如果下采样太快小病斑的特征会在深层特征图中消失。所以在设计模型时前几层尽量保留高分辨率特征图避免一开始就大步长池化。这也是为什么一些经典网络先用小步长和小卷积核保留细节再逐步下采样。3.2 自建轻量级CNN架构工程实践里我倾向先搭一个小型CNN作为baseline。这里给一个可以直接跑的结构输入是224×224的RGB图像import torch.nn as nn class SugarcaneLeafCNN(nn.Module): def __init__(self, num_classes5): super(SugarcaneLeafCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x输入224×224×3的图经过三层卷积加三次下采样后特征图尺寸变成28×28×128再通过全局平均池化压缩成一个128维的特征向量最后送进全连接层输出5个类别的得分。这个设计的核心优势是全局平均池化把全连接层的参数量压得非常低同时保留了每个通道的全局信息对防止过拟合有实际帮助。如果想把模型做得更扎实可以在第二个卷积层后插入BatchNorm2d。BN能稳定中间层的数据分布加快收敛速度并且对学习率没有那么敏感。我实测在数据集规模不大的时候加了BN的模型比不加BN的稳定很多训练曲线也更平滑。关于计算量可以实际估算一下第一个卷积层参数量是3×32×3×332896第二层是32×64×3×36418496第三层是64×128×3×312873856整个模型加起来不到10万参数。在毕业设计的语境里这个规模完全够用CPU上跑一个epoch也很轻松。3.3 训练配置与超参数选择有了模型还要定好怎么训练。损失函数用多分类交叉熵PyTorch里就是nn.CrossEntropyLoss它内部已经包含了softmax逻辑不需要在模型末尾额外接softmax。优化器我推荐Adam初始学习率设为0.001配合weight_decay设置为1e-4相当于加了一点L2正则抑制过拟合。如果训练过程中loss震荡明显可以把学习率降到0.0003或者在每10个epoch后按0.5倍衰减。epoch设多少合适我的经验是30到50轮就够。更关键的是加早停连续10个epoch验证集准确率没有提升就保存当前最优模型并停止训练。这样可以避免盲目训练到固定epoch反而把最好的模型跑丢。你可以在验证集准确率连续不升时打印一条提示最后说明模型是在第几个epoch保存的论文里写到这个细节会让训练策略显得很严谨。还有一个非常实用的小技巧训练过程中每个epoch结束时都计算验证集准确率一旦超过历史最大值就把模型权重保存为checkpoints/best_model.pth。不要只保存最后一轮的权重——最后一轮不一定是最好的尤其是碰到过拟合之后最后一轮权重可能已经明显退化。3.4 迁移学习对照实验为了让论文更有说服力我建议同时做一组迁移学习实验。选一个在大型通用图像数据集上预训练的轻量级网络就行常用的ResNet18就够用。做法很简单把该网络的最后一层分类全连接层替换成新的Linear层输出维度改成自己的类别数5然后把前面大部分层设为不更新只训练新加的全连接层等全连接层收敛后再解冻最后两三个block用小学习率做整体微调。这个两阶段微调法比一上来就全部解冻要稳定得多。迁移学习的好处是收敛极快通常十几个epoch就能达到比自建CNN高不少的效果因为预训练特征已经学到了丰富的通用视觉特征比如纹理、边缘、形状。坏处是如果评委追问为什么预训练模型在你的数据上效果更好你得能解释清楚迁移学习把在大型通用数据集上学到的知识迁移到小数据集这个逻辑。提前准备好这个解释答辩时会非常有底气。做对照实验时最好保持两个模型使用完全相同的训练集、验证集、随机种子和epoch数只有网络结构和预训练权重不同这样得出的对比结论才是干净的。表格可以这样整理自建CNN准确率多少、迁移学习准确率多少、推理速度多少。这种有意识的对比设计论文的层次感会明显提升。4. 核心代码实现与训练过程4.1 项目文件组织实际写代码时不建议把所有内容都堆在一个main.py里最好按职责拆分成模块将来不管是调试还是写论文都会舒服很多。我常用的目录结构如下sugarcane_disease/ ├── train.py ├── predict.py ├── utils.py ├── model.py ├── dataset/ │ ├── train/ │ ├── val/ │ └── test/ └── checkpoints/train.py负责数据加载、训练循环、早停、模型保存model.py放网络定义utils.py放一些辅助函数比如设置随机种子、绘制混淆矩阵predict.py负责加载训练好的模型做单张图片预测。这几个文件加起来也就两三百行代码但结构清晰答辩展示时也好一步步讲解。如果你想做Web端再加一个app.py放Flask逻辑目录依然不乱。4.2 数据加载与增强实现在train.py里最关键是正确构建transforms。训练集用增强验证集和测试集不用增强这个区分必须做对。训练集上的transforms链顺序很重要先RandomResizedCrop再做翻转和颜色抖动最后归一化。顺序反了可能导致某些操作作用在非图像张量上直接报错。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transforms transforms.Compose([ transforms.RandomResizedCrop(size224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transforms transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])加载时用ImageFolder它自动按子目录名生成类别标签。例如healthy子目录中的所有图片都会被标识为类别0leaf_spot标识为类别1以此类推。这比手动写标签映射要简单也少很多出错机会。4.3 训练循环实现训练循环最核心的结构是外层epoch循环内层batch循环。每个batch先清零梯度前向得到预测计算loss反向传播再更新参数。验证环节记得加model.eval()和torch.no_grad()防止dropout和BN在验证时产生不一致的随机行为。下面这段代码基本就是整个训练的骨架for epoch in range(1, args.epochs 1): model.train() train_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * images.size(0) model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_correct (predicted labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total print(fEpoch {epoch}, loss{train_loss / len(train_loader.dataset):.4f}, val_acc{val_acc:.4f})这里有个细节loss累加时乘以了images.size(0)这样算出来的是整个epoch的平均样本损失而不是平均batch损失当最后一个batch比其他batch小时更公平。另外device要提前统一我习惯在加载训练数据前就写好device torch.device(cuda if torch.cuda.is_available() else cpu)把模型和tensor都移到同一个设备上避免出现device mismatch这种一问就露馅的报错。4.4 早停与模型保存早停的写法不复杂但很多人会忽略应该保存最佳验证集模型。我的建议是每轮判断当前val_acc是否大于历史最佳如果大于就保存当前权重并记录下这个epoch如果连续多轮没有超过历史最佳就提前结束训练。这样即使后面继续训练出现过拟合最终拿出来演示的还是之前表现最好的权重。best_acc 0.0 best_epoch 0 patience_counter 0 patience 10 for epoch in range(1, args.epochs 1): ... if val_acc best_acc: best_acc val_acc best_epoch epoch patience_counter 0 torch.save(model.state_dict(), checkpoints/best_model.pth) else: patience_counter 1 if patience_counter patience: print(fEarly stopping at epoch {epoch}, best epoch {best_epoch}) break用这个逻辑哪怕模型在30轮里第22轮达到了最高点之后出现波动最终被保存的也还是第22轮那个最优权重。我帮人调过不少代码发现很多人最后拿去演示的是最后一轮权重训练曲线明明显示后面已经过拟合了准确率明显掉了这就是完全可以避免的失误。保存模型时建议连同训练超参数一起存在一个json文本里方便写论文时对照。4.5 训练曲线与结果可视化训练期间记录每个epoch的loss和acc结束后用Matplotlib画两条曲线训练loss下降曲线和验证准确率曲线。这两张图放在论文里几乎是模型训练过程合理的铁证。如果训练loss持续下降、验证acc也稳步上升说明训练正常如果训练loss一直降但验证acc不再涨甚至下降说明过拟合如果两个都震荡说明学习率太大或数据本身有问题。学会看曲线比会调参更基础。如果有多组对照实验把自建CNN和迁移学习的曲线画在同一张图里用不同颜色区分论文的对比分析会非常有说服力。很多人只放最终数值但曲线能展示训练过程中两组模型收敛速度的差异这是文字描述替代不了的。Matplotlib画完可以直接保存为PNG放到论文里记得把横轴标成Epoch、纵轴标成Accuracy。5. 评估指标与识别应用5.1 准确率之外还需要看什么模型训练结束后不能只给一个准确率就交差。答辩时评委经常问模型对哪一类识别得最好、哪一类最容易混淆。要回答这个问题需要看混淆矩阵和分类报告。分类报告里每一类的精确率、召回率和F1值会暴露很多问题。比如只有叶斑病和褐条病特别容易混淆人工看早期病斑都可能分不清。出现这种情况不能全怪模型很可能是两类图像本身视觉相似。处理思路有两个一是检查标注数据确保训练样本类别是后期症状明显的图二是把这两类再细分或合并成一个大类后续再进一步区分。把这种分析过程写进论文会显得你有诊断能力不只是跑了个基线模型。5.2 单张图片预测实现做部署演示之前最好先把单张图片预测写好这样可以在命令行里快速验证效果。核心流程加载最优模型权重对图片做与验证集完全一致的预处理前向推理softmax得到每个类别的概率取最大概率类别输出。这里很容易踩的坑是预测时用的预处理必须和验证集完全一致包括Resize大小、裁剪方式、归一化参数。很多人训练时用224测试时直接读原图图片尺寸五花八门结果推理结果全乱套。def predict_image(model, image_path, class_names, device): from PIL import Image img Image.open(image_path).convert(RGB) img_tensor val_transforms(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): output model(img_tensor) prob torch.softmax(output, dim1) max_prob, max_idx torch.max(prob, 1) return class_names[max_idx.item()], max_prob.item()返回的max_prob就是模型对该类别的置信度通常在0.6到0.99之间。如果置信度低于0.5说明模型对这张图并不确定可以提示无法可靠识别而不是硬给出一个分类结果。这种处理在演示时很加分说明你考虑到了模型的失败模式。5.3 用Flask做一个简易演示页面光有命令行还不够直观。毕业设计答辩现场评委更愿意看到网页交互效果。用Flask做一个最简单的上传页面只需几十行代码一个HTML表单接收图片文件后端调用predict_image返回类别和置信度。模板里做两个按钮一个选择图片一个开始识别白底居中显示结果就够了。from flask import Flask, request, jsonify, render_template app Flask(__name__) model load_model(checkpoints/best_model.pth) app.route(/, methods[GET, POST]) def index(): if request.method POST: file request.files[image] img_path uploads/ file.filename file.save(img_path) label, prob predict_image(model, img_path, class_names, device) return jsonify({label: label, prob: round(prob, 4)}) return render_template(index.html)做成前端页面的好处在演示时立竿见影本地起Flask服务打开浏览器上传一张病叶照片页面立刻回显识别结果。这一整套流程跑通基本就把数据—模型—应用闭环展示完整了。我用过几次答辩时评委视线明显会被交互页面吸引提问也会往为什么选这个方向模型怎么部署这种方向走反而更容易把控现场节奏。6. 常见问题与避坑指南6.1 数据太少导致过拟合症状很典型训练集准确率很快到95%以上验证集却只有60%多。这是作物病害识别里最典型的问题。对策按优先级排序第一加大数据增强强度尤其是随机裁剪和颜色抖动第二使用迁移学习替代自建CNN依靠预训练权重稳住特征提取第三在模型中增加dropout或全连接层之后加BatchNorm第四把图片分辨率降到160或128间接减小模型容量。如果这些都做了还过拟合那说明数据集真的太小老实补充拍摄数据比硬调模型更实际。6.2 某些类别准确率低原因通常是样本不均衡比如锈病样本只有健康叶片的三分之一模型自然倾向猜多数类。解决思路一是对少数类做更强的增强比如每个epoch里重复抽取少数类样本二是改用带类别权重的损失函数把少数类的loss放大比如nn.CrossEntropyLoss直接传入weight参数weight可以设为总样本数除以类别数与该类样本数的乘积三是补充少数类的原始图片。建议在论文里选一种方案并附上处理前后各类别准确率的对比这样的实验设计老师挑不出毛病。6.3 训练loss震荡不收敛出现这个情况先检查学习率是不是太大把0.001改成0.0003往往就好转了。其次检查数据增强强度如果颜色抖动幅度太大同一张图增强前后的语义都可能发生变化网络学起来当然震荡。还有一个容易忽视的地方标签和图片是否对得上清洗后的文件名如果和标签错位就是严重bug训练loss会一直在高位下不来。遇到这种情况先把DataLoader一个batch里的图片打印出来对照标签肉眼确认一下再来调模型。现象可能原因优先处理方案训练集acc高验证集acc低过拟合增强、Dropout、迁移学习某类准确率明显偏低样本不均衡损失函数加权、过采样少数类loss震荡不收敛学习率过大或标签错位降低学习率、检查DataLoader输出推理结果全乱预处理不一致统一使用验证集transforms训练很慢GPU占用不高数据加载成为瓶颈调大num_workers6.4 显存不足问题如果显卡显存只有4G以下ResNet18大batch可能跑不起来。不用慌把batch size降到8或4同时降低图片输入尺寸到160×160问题就缓解了。更稳的方案是训练时用自建小模型推理时再切换到大模型。还有一个技巧是梯度累积每两个batch合在一起更新一次等效于在不增加显存的情况下扩大batch size。这些细节写进论文能体现你对资源受限环境下的工程能力。6.5 答辩提问准备给做毕业设计的同学提前准备几类问题。一是为什么用卷积神经网络不用传统机器学习方法你要答传统方法依赖人工设计特征比如颜色直方图、纹理特征而CNN自动提取分层特征且鲁棒性更强二是数据是怎么来的、有多大规模要如实回答来源和增强后规模三是模型为什么这么设计如果效果不好怎么改你只需要结合自己的实验数据说明对比结论就行。提前把这些问题想好答辩过程会从容很多。最后再给两个过来人的建议。第一个是从一开始就在有GPU的环境里训练哪怕是云服务也比本地笔记本跑大epoch省时间时间成本真的很贵。第二个是所有中间结果和模型权重都按日期和准确率命名保存好例如resnet18_epoch22_acc0.915.pth写论文整理实验记录时能节省大量时间。这套流程做完你手里不仅有一个能演示的识别系统更重要的是把深度学习项目的完整思路跑通了一遍以后换任何图像分类题目都能快速复用。