首页 / 资讯中心 / 文章详情

基于卷积神经网络的核桃外观好坏识别与质检实践

基于卷积神经网络的核桃外观好坏识别与质检实践 ★ FEATURED ARTICLE
1. 项目概述与为什么选核桃这几年计算机视觉相关的毕业设计选题数量最多的两类一类是人脸、车牌这类通用任务另一类就是农产品外观质检。核桃好坏识别属于后者但我接触下来发现它比想象中更适合做深度学习入门项目卡在一个“不难但足够典型”的位置上。先说这个项目要解决的问题。核桃在采收、晾晒、仓储过程中很容易出现霉变、空壳、虫眼、开裂这类问题。传统做法是人工分拣靠老师傅眼睛看、手掂量效率低不说标准还不统一。工厂里每小时要过好几吨核桃人工盯久了疲劳漏检率会明显上升。用卷积网络自动识别核桃外观好坏本质上就是做一个二分类图像分类器输入一张核桃照片输出“好”或者“坏”。从技术难度看核桃识别比人脸识别、物体检测简单得多它没有复杂背景、没有多目标重叠、没有姿态剧烈变化。在固定光源、固定角度下拍摄一张图里放一到三颗核桃分类精度做到95%以上并不难。这也是我推荐把它作为毕业设计题目的原因数据集自建可控、模型训练周期短、可视化效果好答辩时能讲清楚每一个环节。这个项目适合三类人第一类是计算机、人工智能专业的本科生需要完成毕业设计或课程设计第二类是打算转行做算法工程师、想用真实小项目练手的人第三类是农业信息化、食品工程方向的学生想做农产品质检相关的交叉课题。对这三类人来说这个题目的价值在于它麻雀虽小五脏俱全从数据采集、模型训练到评估部署把计算机视觉的完整流程走了一遍。下面我会把这个题目从零拆开包括数据集怎么做、模型怎么选、参数怎么调、坑怎么踩尽量写到可以直接照着复现的程度。代码基于 Python 和 TensorFlow/Keras但思路同样适用于 PyTorch。2. 总体方案设计与技术选型思路2.1 场景约束决定了技术路线核桃好坏识别本质上是一个受控场景下的图像二分类问题。在动手写代码之前先要明确场景约束因为不同约束条件会导向完全不同的技术方案。如果是工厂流水线场景图片背景相对固定光源稳定核桃的摆放角度可能有限制。这种情况不需要用目标检测去寻找核桃位置直接用分类网络就够了。如果场景是手机随手拍照背景复杂、光线变化大、核桃可能被遮挡那就要先做检测再分类工程复杂度一下子高了很多。作为毕业设计我建议优先做受控场景把核心精力放在分类模型本身而不是陷入检测和跟踪的泥潭。基于这个判断整个系统分为四层数据层采集与标注、预处理层图像增强与数据集划分、模型层卷积网络训练、评估层准确率/召回率/可视化。每一层都有独立的评判标准最终串起来就是一个可演示的完整系统。2.2 为什么选择卷积神经网络而不是传统方法很多同学第一次看到这个题目会问用 OpenCV 提取颜色、纹理特征再扔给 SVM 分类行不行答案是行但效果天花板很低。核桃的好坏差异有些是全局性的比如整体发霉导致颜色发暗有些是局部性的比如某个角落有一个虫眼或者壳上有一道裂纹。传统方法靠人工设计特征提取器很难同时捕捉这两种尺度。如果只用颜色直方图局部虫眼完全看不出来如果只做边缘检测霉变区域的纹理变化又不够显著。卷积神经网络通过多层卷积核自动学习特征浅层学边缘纹理深层学语义部件天然适合这种“既看整体又看局部”的任务。而且核桃表面纹理复杂深浅不一靠人工定义“什么样的纹理算霉变”非常困难让网络自己学是最省力的方式。我自己做过对比同样的数据集HOG 特征加 SVM 的准确率大概在 85% 左右而一个简单的 CNN 轻松超过 95%差距非常明显。2.3 模型选型自建轻量网络还是迁移学习方案确定用 CNN 之后下一个问题是自己搭一个网络还是用预训练模型做迁移学习我给这个项目的建议是两条路都走一遍。第一条路自建一个 3 到 5 层的轻量卷积网络参数量控制在 50 万以内。这条路的好处是训练速度快对显存要求低且每一层的特征图都可以可视化答辩时能直观展示网络学到了什么。第二条路用迁移学习加载在 ImageNet 上预训练过的 ResNet50 或 MobileNetV3冻结前面大部分层只训练最后的全连接层。这条路的好处是准确率更高小数据集也不容易过拟合。实际操作中我见过不少人直接上 ResNet50但数据集只有两三千张训练集误差和验证集误差差距很大过拟合严重。后来换成轻量网络加数据增强效果反而更好。所以我的建议是先跑一个简单的自建网络作为 baseline再尝试迁移学习做对比这样论文里可以多一个对比实验评分也更好看。2.4 评估指标不能只看准确率核桃好坏识别里两类样本的数量往往不均衡。好核桃占多数坏核桃占少数比如好核桃 3500 个坏核桃 1000 个。如果模型把所有图都判成好核桃准确率仍然有 77.8%看起来好像还行但坏核桃一个都没找出来生产线上的霉变核桃全漏掉了。这种场景下真正的核心指标不是准确率而是坏核桃的召回率也就是“所有坏核桃里被模型抓出来多少”。漏掉一个好核桃损失一点点漏掉一个坏核桃整批货可能被退货。所以评估时必须同时看准确率、精确率、召回率和 F1-Score还要画出混淆矩阵直观展示每一类的错分情况。我在项目里会额外关注“坏核桃召回率”这一个指标训练时如果发现这个指标低于 90%就说明模型太保守需要调整分类阈值或者增加坏样本的权重。这是很多教程不会强调的但实际项目里恰恰是最关键的一步。3. 数据集构建采集、标注与预处理3.1 图像采集的设备与拍摄规范数据集是整个项目的基石模型再强数据不行都白搭。核桃识别的最佳数据来源有三个自己买核桃拍摄、网上爬取公开图片、使用某农产品检测机构提供的脱敏数据。对大多数学生来说第一种最靠谱完全可控且没有版权问题。拍摄时建议用手机或普通相机均可但必须固定拍摄环境。我用的方案是一个 500 瓦的 LED 摄影棚灯、一张黑色哑光背景纸、一个手机三脚架核桃平放在背景纸上镜头垂直向下拍摄。固定光源的目的是消除阴影干扰避免核桃本身纹理和阴影混杂在一起让网络更专注于核桃表面的缺陷特征。分辨率方面手机拍摄的原始照片一般是 3000x3000 以上这个尺寸直接扔给网络训练会非常慢而且没有必要。统一缩放为 224x224 或 256x256 都可以这也是大多数预训练模型的输入尺寸。如果自建网络128x128 也能跑但会丢失细小的裂纹纹理不建议低于这个值。我最终选了 224x224兼顾细节和训练速度。3.2 好坏核桃的定义与标注标准标注之前必须先定义什么算好、什么算坏不然不同人标出来的结果不一致模型学到的东西就会很混乱。在我的项目里“好核桃”定义为外壳完整、无明显裂纹、无虫眼、无霉斑、颜色均匀。“坏核桃”包括四类霉变核桃表面有白色或黑色霉菌斑、开裂核桃外壳有明显裂缝、虫眼核桃表面有圆形小孔洞、空壳核桃重量轻但外观可能正常这类主要靠声音和手感判断视觉上不一定能看出来。这个项目只做外观识别所以空壳核桃不作为主要目标如果混进数据集反而会影响模型训练。坏核桃的标注里有一个细节容易踩坑霉变的初期阶段在 RGB 图像上并不明显只有淡淡的灰白色区域普通手机拍出来对比度很低。解决方法是拍摄时适当提高光源强度或者在预处理时做对比度增强把霉变区域和正常纹理的差异拉开。我身边一位同学最开始没注意这一点模型对霉变的识别准确率一直在 60% 左右徘徊后来重新调整拍摄和预处理才提上来。3.3 数据集规模与划分策略深度学习虽然对数据量有要求但二分类任务没有想象中那么“吃数据”。我建议最低标准是总样本量 2000 张以上好核桃与坏核桃的比例控制在 1.5:1 到 3:1 之间不要太极端。如果你想追求更好的效果5000 张以上会非常宽裕。数据划分比例我采用训练集 70%、验证集 15%、测试集 15%。这里有一个常见误区很多人只切训练集和测试集训练时反复用测试集调参最后测试集准确率高得离谱但换一批真实数据就崩了。正确做法是“三折”训练集用来更新权重验证集用来调整超参数、监控过拟合测试集只在最终模型确定后用于一次性的最终评估。这样评估结果才可信。拍摄时还有一个技巧不要只在同一个背景下拍完所有照片。可以稍微变化背景颜色、旋转核桃角度、调整光照强度人为增加数据的多样性。深度学习最怕的是“特定环境下的记忆”而不是“核桃特征的泛化”。如果一个模型只在黑色背景上效果好换个背景就垮掉说明数据多样性不足。3.4 图像增强策略适度即可不要过度数据增强是深度学习练好模型的必备手段但对于核桃这个场景增强策略需要谨慎选择。我实际使用的增强组合是随机水平翻转、随机垂直翻转、随机旋转 90 度、随机亮度调整范围 0.8 到 1.2、随机对比度调整范围 0.9 到 1.1。这些增强方式不会改变核桃的语义特征只是增加视角多样性可以有效降低过拟合。但有两个增强操作需要避免。第一是随机裁剪核桃可能被裁掉一半模型被迫学会“看到半个核桃”的特征这对整颗核桃识别没有帮助。第二是过度色彩扭曲比如随机改变色相饱和度会导致核桃本身的颜色特征失真霉变区域的颜色差异被抹平。我踩过这个坑加了强色彩增强之后训练集准确率很高测试集反而掉了好几个点排查半天才发现是增强策略的问题。3.5 数据集构建的完整流程总结为了方便复现我把整个数据集构建流程整理为五步拍摄原始照片每个核桃至少从两个角度各拍一张总计收集 3000 张以上。人工筛选剔除模糊、过曝、被遮挡的无效图片。按照“好核桃”和“坏核桃”两类分别放入不同文件夹文件命名规则统一为good_001.jpg和bad_001.jpg的格式。使用脚本统一缩放为 224x224并转换为 JPEG 格式。按照 70%、15%、15% 的比例随机划分三份保存为独立的文件夹。第四步有一个隐藏的好处统一尺寸后训练时数据加载的速度会明显加快不用每次都在 CPU 上做实时缩放。对于没有 GPU 的同学来说这一步能节省大量时间。4. 卷积网络模型搭建与关键参数解析4.1 轻量自建网络结构详解我推荐的自建网络结构如下参数量约 46 万在普通笔记本 CPU 上训练一轮大约需要 3 到 5 分钟在入门级 GPU 上不到 1 分钟。前面四个卷积块的结构类似每一块包含一个卷积层、一个批归一化层、一个 ReLU 激活和最大池化。卷积核数量从 32 开始倍增32、64、128、256卷积核大小统一为 3x3padding 设置为 same保证特征图尺寸不缩小池化层再减半。这个设计的好处是特征图尺寸逐步减半、通道数逐步增加符合卷积网络的基本设计原则在空间分辨率降低的同时用更多通道保存信息。最后接一个全局平均池化把最后一个卷积块得到的 8x8x256 特征图压缩成 256 维向量再通过一个 Dropout 层丢弃一部分神经元最后接一个输出维度为 1 的全连接层激活函数用 sigmoid输出值代表“是坏核桃”的概率。全局平均池化取代 Flatten 加全连接是减少参数量的关键设计。import tensorflow as tf from tensorflow.keras import layers, models def build_simple_cnn(input_shape(224, 224, 3)): model models.Sequential([ layers.Input(shapeinput_shape), layers.Conv2D(32, 3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D(2), layers.Conv2D(64, 3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D(2), layers.Conv2D(128, 3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D(2), layers.Conv2D(256, 3, paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D(2), layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(1, activationsigmoid) ]) return modelbatch size 我建议取 32这个值在绝大多数设备上都能跑梯度更新也比较稳定。如果显存不够降到 16如果数据集很大且 GPU 很强可以试 64但要注意 batch size 过大会影响泛化性能。4.2 迁移学习路线ResNet50 的微调技巧自建网络作为 baseline 跑通之后迁移学习路线建议用 ResNet50 作为第二套对比方案。具体做法是加载 ImageNet 预训练权重去掉最顶层的全连接分类层保留卷积基新接一个全局平均池化和一个输出维度为 1 的 sigmoid 层。迁移学习里最重要的超参数是冻结策略。我的建议分两个阶段训练第一阶段冻结整个卷积基只训练新加的分类层用较大学习率如 0.001训练 10 个 epoch第二阶段解冻最后 20 到 30 层卷积层用较小学习率如 0.0001继续训练 10 个 epoch。这种“先训头、再微调”的方式可以避免在随机初始化的分类层还没收敛时就去调整预训练特征导致破坏原有的良好特征表示。from tensorflow.keras.applications import ResNet50 base_model ResNet50(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) base_model.trainable False model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(1, activationsigmoid) ])这里有一个需要特别提醒的点预训练模型要求的输入预处理和自建网络不一样。ResNet50 需要把像素值归一化到特定范围并且要按通道进行标准化如果直接复用自建网络的预处理代码精度会明显下降。Keras 自带的preprocess_input函数专门干这个直接调用即可不要自己写归一化。4.3 损失函数、优化器与学习率策略二分类任务最常用的损失函数是二元交叉熵binary crossentropyKeras 里对应BinaryCrossentropy。这个损失函数在模型输出严重错误时梯度大接近正确时梯度小训练过程比较平稳是二分类的标准选择。优化器我推荐 Adam默认学习率 0.001。Adam 自适应调节每个参数的学习率在大多数任务上都能快速收敛对新手非常友好。我也试过 SGD 加动量收敛速度慢但最终精度可能略高一点不过调参成本高不太适合作为毕业设计的时间预算。学习率是最需要手调的超参数。我的经验是在 Adam 默认 0.001 的基础上每训练 10 个 epoch 后验证集准确率如果不再提升就让学习率降为原来的五分之一。也可以用 Keras 的ReduceLROnPlateau回调自动完成这件事。我见过不少同学从 0.1 开始训练损失直接爆炸NAN 不断就是因为学习率太大。0.001 这个起点要保守得多。4.4 类不平衡的处理方法如果数据集中坏核桃占比明显少有两种常见修正手段。第一种是给少数类更高的损失权重在 Keras 的class_weight参数里设置比如好核桃权重设为 1.0坏核桃权重设为 2.5这样模型在训练时会更重视坏核桃的误判。第二种是过采样在数据加载时对坏核桃样本多循环几次让每一轮训练看到同等数量的两类样本。我实际测试下来class_weight的效果更稳定因为过采样本质上是在重复使用少数类样本容易让模型对特定几个坏核桃“背答案”泛化性稍差。权重设置也不是越高越好权重过大会导致模型把大量好核桃误判为坏核桃。在项目里我调了几组对比坏核桃权重设为总样本中好与坏的数量比值减半最终召回率和精确率比较均衡。5. 训练过程完整实录与调优心得5.1 训练环境配置代码能跑起来的前提是环境装好。TensorFlow 2.x 搭配 Python 3.8 到 3.10 都可以我建议用 Anaconda 创建独立虚拟环境避免和系统其他项目的依赖冲突。conda create -n walnut python3.9 conda activate walnut pip install tensorflow2.13.0 pip install numpy matplotlib scikit-learn opencv-python硬件条件不用太担心。纯 CPU 训练轻量自建网络3000 张图片一轮大约需要 3 分钟30 个 epoch 大概一个半小时完全可以接受。ResNet50 迁移学习在 CPU 上会慢一些如果有 NVIDIA 显卡就很快没有的话可以把图片尺寸降到 160x160减少计算量。5.2 训练过程监控技巧训练过程中不能只盯着 loss 曲线看我的做法是同时监控训练集 loss、验证集 loss、训练集准确率和验证集准确率四条曲线。验证集 loss 是最重要的过拟合信号当训练集 loss 持续下降但验证集 loss 开始回升时就是典型的过拟合应该早停或者降低模型容量。我在项目里加入了三个 Keras 回调函数ModelCheckpoint保存验证集准确率最高的权重EarlyStopping在验证集 loss 连续 5 个 epoch 不下降时自动停止训练ReduceLROnPlateau自动降低学习率。三者配合基本不需要人工干预。callbacks [ tf.keras.callbacks.ModelCheckpoint(best_model.h5, save_best_onlyTrue, monitorval_accuracy, modemax), tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue, monitorval_loss, modemin), tf.keras.callbacks.ReduceLROnPlateau(factor0.2, patience3, monitorval_loss, modemin) ] model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.fit(train_dataset, validation_dataval_dataset, epochs30, callbackscallbacks)这里有一个很实用的技巧restore_best_weightsTrue会在早停时自动恢复到验证集表现最好的那一步权重而不是停在最后一个 epoch 的权重上。很多同学不知道这一点训练被早停后以为模型已经是最优的实际可能相差好几个点。5.3 训练曲线解读与典型调参案例我记录了一组典型训练过程自建网络在初始默认参数下训练了 20 个 epoch结果如下训练集准确率从第 1 个 epoch 的 68% 稳步上升到第 15 个 epoch 的 99% 以上验证集准确率却只从 70% 上升到 92%之后基本不再提升。同时验证集 loss 在第 12 个 epoch 触底随后缓慢回升。这是明显的过拟合信号说明模型容量对于这个数据集来说偏大。针对这个情况我的处理是三步第一把最后一个卷积层的 Dropout 从 0.3 提高到 0.5第二数据增强力度适当增加亮度调整范围从 0.9-1.1 扩大到 0.8-1.2第三开启 EarlyStopping。调整后验证集准确率提升到 95.3%验证集 loss 也不再反弹过拟合问题基本解决。另一个调参案例发生在迁移学习阶段。我一开始全模型以 0.0001 的学习率从头微调训练 10 个 epoch 后验证集准确率只有 89%。后来改为先冻结训练分类层、再解冻微调的两阶段策略同样的数据量下验证集准确率提升到 97.1%。这说明迁移学习的微调顺序和节奏比单纯调学习率重要得多。5.4 测试集评估与结果可视化训练完模型最终评估要在测试集上进行。测试集是模型从未见过的 450 张图片这时候得出的指标才有说服力。我在项目里用model.evaluate得到整体损失和准确率再用predict得到每一张图片的预测概率最后算精确率、召回率和 F1。除了数字指标还要做可视化。用 matplotlib 把预测错误的图片单独列出来标注真实标签和预测概率会非常有用。比如我发现很多误判案例都是光线偏暗导致的阴影被误判为霉斑于是重新调整了训练数据中的亮度增强范围这比盲目调网络结构有效得多。6. 常见问题与排查技巧实录6.1 问题速查表项目过程中会遇到很多实际问题我把最常见的整理了如下现象可能原因解决方案训练 loss 为 NAN学习率过大降到 0.0001 或更低重试验证集准确率很低但训练集很高过拟合增加 Dropout、数据增强、早停好核桃全对但坏核桃全漏类不平衡设置 class_weight 或调整阈值迁移学习精度比自建网络还差预处理不一致或微调策略错误改用 preprocess_input冻结后分阶段训练预测时单张图片结果不稳定模型过度依赖背景特征增加背景多样性统一拍摄条件训练速度极慢图片没有预缩放统一 224x224 后再进网络这里面最容易被忽视的是第一项。学习率过大导致 loss 变成 NAN很多新手会以为是自己代码写错了实际上只需调小学习率或者引入梯度裁剪就能解决。6.2 单个坏核桃识别失败案例分析我在测试集上发现一个典型的失败案例一颗轻度霉变的核桃霉斑面积不到整个外壳的 5%网络给出的坏核桃概率只有 0.32被误判为好核桃。这类样本的特点是霉斑颜色和正常核桃壳的浅棕色非常接近肉眼也需要凑近看才能分辨在 224x224 的图片上可能只占十几个像素。针对这类样本我尝试了两个方向的改进。第一个方向是图像预处理把图片缩放前先做一次对比度增强让霉斑和正常区域的差值变大。第二个方向是模型结构在输入后加一层RandomBrightness作为数据增强模拟不同光源条件。两个方向结合后该案例的预测概率从 0.32 上升到 0.61虽然还是不高但整体坏核桃召回率提升了约 2 个百分点。这说明一个问题轻度缺陷识别是整个任务的难点不能指望单靠模型结构解决数据处理层面的配合同样重要。6.3 部署环节的简化方案毕业设计做到模型训练和测试集评估基本已经满足要求。但如果想做得更完整可以做一个简单的推理脚本输入一张图片路径输出预测类别和概率。这个脚本可以直接给老师现场演示效果比只看训练曲线好很多。def predict_single_image(image_path, model_pathbest_model.h5): model tf.keras.models.load_model(model_path) img tf.keras.utils.load_img(image_path, target_size(224, 224)) img tf.keras.utils.img_to_array(img) / 255.0 img tf.expand_dims(img, axis0) prob model.predict(img, verbose0)[0][0] label 坏核桃 if prob 0.5 else 好核桃 return label, prob这个脚本里有几个可以扩展的方向把阈值 0.5 改成可配置参数方便调整判别严格程度加入 Grad-CAM 可视化把模型的关注热区显示出来对答辩演示很有说服力用 ONNX 导出模型后续可以在嵌入式设备上部署。这些可选功能根据自己的时间精力加不必全部做完。7. 项目收尾的一些个人体会跑了几个版本的模型我最深的感受是核桃好坏识别这个题目看起来简单但要把准确率从 90% 推到 96% 以上每一个环节都需要打磨。数据采集不规范后面怎么调模型都补不回来标注标准不统一模型学到的特征就是混乱的评估指标只看准确率坏核桃召回率低的问题根本发现不了。如果让我给后来者一个最直接的建议那就是不要一上来就追求复杂的网络结构。先把数据做规范用最简单的 CNN 跑通一个模型看训练曲线、看错误案例再决定下一步怎么优化。很多问题在简单模型上反而更容易暴露复杂模型只会让问题更难排查。最后再分享一个小技巧整个项目过程中每调整一次数据或模型就把实验结果记录在一个表格里包括模型结构、输入尺寸、增强策略、超参数、准确率、召回率这几项。答辩时老师最常问的问题就是“你做了哪些尝试、效果如何”有一份完整的实验记录回答起来逻辑非常清晰也是论文实验部分最扎实的素材。
阅读完成 · 觉得有帮助?
咨询建站