
简介面向计算机视觉、深度学习相关方向的大四毕业生与课程设计/项目实践学生这套“昆虫识别和数目统计”毕业设计资源提供了从图像预处理、模型训练到目标计数与结果输出的完整链路覆盖图像数据集、Python源码、标注信息、模型权重、统计表格和可视化界面适合作为毕设主体框架或课设参考方案。压缩包共164个文件整体大小14.59MB以97张昆虫图片和23个Python脚本为主搭配13个npy数组、10个xml标注文件以及model、csv、ui、PDF等辅助材料数据、模型、文档层次分明便于按需取用。目前已有140人学习入手后可直接运行现有流程也可以结合csv数据和脚本调参或替换数据集快速生成实验对比与演示效果从而节省程序搭建与调试时间。资源体量紧凑但覆盖完整尤其适合希望独立完成一个图像识别项目或进一步扩展算法功能的学生。1. 昆虫识别与数目统计这份毕设资源包里到底装了什么拿到这份压缩包的时候我第一反应是文件名写得很直白——大四毕业设计昆虫识别和数目统计。解压之后你会看到几个关键文件data.csv、datatest.csv、ques.csv、.gitignore还有一个 IDEA 工程文件 Insect_Identification.iml加上 fly2.jpg、fly11.jpg、fly15.jpg、fly16.jpg、fly17.jpg 这一批昆虫图片样本。整个项目走的是画像分类路线不是目标检测标注格式也不是常见的 VOC 或 YOLO而是用 CSV 文件存文件名和类别标签。这套结构非常适合课程设计、大作业、毕设起步数据可见、格式透明、模型替换方便。它的核心价值在于两条线用训练集 data.csv 训练一个能区分昆虫类别的分类模型再用模型输出的概率分布做数目统计。也就是说识别输出的是“这张图是什么虫”数目统计输出的是“这批图里某种虫有多少”。对刚开始接触深度学习项目的同学来说这份资源最大的意义不是给你一个黑盒子而是把数据处理、训练、推理、计数整条链路拆开摆在你面前。文本接下来的内容我会按数据格式、模型选型、训练流程、计数实现、典型踩坑、进阶扩展的顺序把整个项目完整复现一遍。2. 先拆数据三个 CSV 各管什么事怎么读出正确标签2.1 data.csv、datatest.csv、ques.csv 的分工看文件列表的时候很多人会下意识认为这只是普通表格其实在毕设项目里CSV 就是标注文件本身。这个项目的设计思路是data.csv 作为训练集标注datatest.csv 作为测试集标注ques.csv 作为推理阶段要预测的图片清单。三者分工很明确拿过来先不要动模型先把这三个文件读明白。常见的标注内容大概是这样的格式文件典型列名作用data.csvfilename, label训练图片路径 类别标签datatest.csvfilename, label测试图片路径 验证标签ques.csvfilename仅提供文件名用于最终推理预测很多人第一次拿到手就直接用 pandas 的 read_csv 去读结果发现 ques.csv 只有一列跟 data.csv 的列数不一样读进来之后怪错频出。实际上 ques.csv 没有 label 列它的定位是“待预测名单”。datatest.csv 的作用也不是让你拿去训练而是评估模型泛化能力考试卷不能当练习册。2.2 先读一遍别急着写网络在你开始写 Dataset 类之前我一般会先做一件事把三个 CSV 全部读出来打印头部信息并且检查每个 filename 对应的图片文件是否真的存在于磁盘上。这一步能拦住后面 80% 的路径错误。import pandas as pd import os # 读入三个 CSV train_df pd.read_csv(data.csv) test_df pd.read_csv(datatest.csv) ques_df pd.read_csv(ques.csv, headerNone, names[filename]) # ques.csv 只有一列 # 检查训练集结构和图片是否存在 print(train_df.head()) print(train size:, len(train_df)) print(label unique:, train_df[label].unique()) # 逐个检查文件路径 missing [] for fname in train_df[filename]: if not os.path.exists(fname): missing.append(fname) if len(missing) 0: print(missing files:, missing[:10]) else: print(all train images exist)这段代码有三个关键点值得说。第一ques.csv 读取时设置headerNone否则 pandas 会把第一行图片名当成列名整个 DataFrame 只剩一列还错位。第二检查文件是否存在这一步非常廉价却能把“No such file or directory”这类问题在训练启动前就拦掉。第三打印 label unique 是为了看清类别分布如果某种昆虫只有一张图后面训练时就要特别小心过拟合。2.3 从 CSV 到 PyTorch Dataset路径拼接要统一处理好 CSV 之后下一步就是把标注文件跟图片读取逻辑绑定起来。这里最常见的翻车点就是路径拼接不规范比如 CSV 里写的是images/fly2.jpg但实际文件放在dataset/images/fly2.jpg相对路径一偏直接全军覆没。import torch from torch.utils.data import Dataset from PIL import Image import pandas as pd class InsectDataset(Dataset): def __init__(self, csv_path, img_dir, transformNone): self.df pd.read_csv(csv_path) self.img_dir img_dir self.transform transform # 假设 CSV 的 filename 列是相对 img_dir 的路径 self.labels sorted(self.df[label].unique()) self.label_map {name: idx for idx, name in enumerate(self.labels)} def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path os.path.join(self.img_dir, str(row[filename])) image Image.open(img_path).convert(RGB) label self.label_map[row[label]] if self.transform: image self.transform(image) return image, labelimg_dir用完整路径传入CSV 里不要带目录前缀或者只带相对子路径这是一个工程习惯问题。label_map是在整个 Dataset 内部构建的这样保证训练集和测试集的类别索引一致不会出现训练时“蛾子”是 0测试时“蛾子”变成 2 的错位事故。convert(RGB)的细节也要注意灰度图或者带透明通道的 PNG 如果没有这一步后面模型输入维度会对不上。3. 模型选型与训练ResNet18 迁移学习跑通识别分支3.1 为什么选 ResNet18而不是自己搭 CNN昆虫数据集在毕设场景下通常不会很大可能整个训练集也就几百到两三千张图片。这种情况下从零训练一个深层 CNN 非常容易过拟合你辛辛苦苦调三天参数验证集准确率可能还不如别人用预训练模型直接微调十分钟。ResNet18 是这里最稳妥的选择它只有 1100 万参数左右在一张普通显卡上训练非常快CPU 推理也能勉强跑动改成 MobileNet 后甚至能塞进嵌入式设备适合做后续扩展。ResNet18 的残差结构能够有效避免梯度消失在浅层特征提取上已经足够应付昆虫这种纹理特征明显的任务。如果需要更高的准确率可以把 ResNet18 换成 ResNet50但训练时间会翻倍毕设答辩场景下其实不太划算。我的建议是先拿 ResNet18 跑通全流程如果验证集准确率确实不理想再升级模型深度。3.2 完整训练脚本直接用import torch import torch.nn as nn import torch.optim as optim from torchvision import models, transforms from torch.utils.data import DataLoader # 数据增强与归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset InsectDataset(data.csv, img_dirimages, transformtrain_transform) val_dataset InsectDataset(datatest.csv, img_dirimages, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers0) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers0) # 迁移学习加载预训练权重替换最后一层 model models.resnet18(pretrainedTrue) num_classes len(train_dataset.label_map) model.fc nn.Linear(model.fc.in_features, num_classes) # 只训练最后一层前面的层全部冻结 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.fc.parameters(), lr1e-3) for epoch in range(15): model.train() running_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() running_loss loss.item() # 每个 epoch 验证一次 model.eval() correct, total 0, 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) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1}, Loss: {running_loss:.4f}, Val Acc: {100*correct/total:.2f}%)这里几个参数值得展开。batch_size32是通用保守值如果你的显卡显存只有 4G建议降到 16。num_workers0在 Windows 上最省心因为 Windows 下多进程数据加载经常报 BrokenPipeError学生机没有 Linux 环境支持的时候这个参数能避开一个经典大坑。冻结预训练层只训练model.fc是迁移学习的标准做法因为 ResNet 前面的卷积层提取的是通用视觉特征在昆虫数据集上依然有效而最后分类层需要重新学习你的具体类别。lr1e-3对新的 fc 层是安全的初始值如果用的是 ImageNet 预训练权重不冻结直接全量训练那学习率必须降到 1e-4 以下否则 loss 会原地爆炸。3.3 训练日志与效果检查正确率以外还要看什么训练跑完之后终端打印的 Val Acc 只是一个粗糙指标。对于昆虫识别这个任务我更建议你做三件事第一保存训练曲线图看 loss 和准确率的走势第二计算每一类的精确率和召回率防止某种昆虫类别样本极少导致整体准确率虚高第三把验证集中预测错误的图片单独存一个文件夹人眼扫一遍确认是数据标注错误还是模型能力不足。python train.py 21 | tee training_log.txt这行命令将训练输出同时打印到屏幕和写入文件后面用脚本解析日志里的 epoch、loss、val_acc 字段就能生成训练曲线答辩展示比干巴巴贴代码强很多。另外保存模型时不要只存权重建议把 label_map 用 json 同步导出否则预测阶段你拿到一个数字类别 3根本不知道它对应的是“蝗虫”还是“蝴蝶”。import json with open(label_map.json, w) as f: json.dump(train_dataset.label_map, f) print(label_map saved:, train_dataset.label_map)当验证集准确率稳定在 90% 以上识别分支就算跑通了。此时不要急着高兴接下来要处理的是数目统计这个更实际的诉求。4. 数目统计的实现从置信度向量到计数输出的完整换算4.1 识别和计数为什么可以共用一套输出昆虫识别和昆虫数目统计在很多人的理解里是两个完全不同的任务识别是图像分类计数是目标检测或者密度估计。但这套毕设项目没有提供任何目标检测框标注那计数怎么实现答案是识别模型的输出可以同时承担计数任务。每张图片经过网络得到的是一个概率分布也就是 softmax 之后的置信度向量。对于置信度特别高的类别可以认为模型“确定看到了”一只该类昆虫再通过阈值过滤和峰值计数把这个概率向量换算成数目。这套方案在单一昆虫、干净背景的图片上是有效的。如果图片里出现了多只昆虫那么模型输出的置信度向量前几名会相差不大比如“蝗虫”置信度 0.78“飞蛾”置信度 0.72这个时候单纯用 argmax 去判断类别就会损失信息。计数逻辑要做的是把所有超过阈值的目标类别都保留下来再对同一张图做多尺度预测验证。4.2 实现计数阈值和峰值别拍脑袋定import torch import torch.nn.functional as F from PIL import Image from torchvision import models, transforms # 加载训练好的模型 model models.resnet18(pretrainedFalse) model.fc torch.nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict_and_count(image_path, model, threshold0.6): image Image.open(image_path).convert(RGB) input_tensor transform(image).unsqueeze(0) with torch.no_grad(): outputs model(input_tensor) probs F.softmax(outputs, dim1)[0] # 找出所有置信度高于阈值的类别 top_probs, top_indices torch.topk(probs, k3) detected [] for prob, idx in zip(top_probs, top_indices): if prob.item() threshold: detected.append((idx.item(), prob.item())) # 计数每个高于阈值的峰值类别算一只 count len(detected) return detected, count image_path question_images/fly16.jpg detected, count predict_and_count(image_path, model, threshold0.6) print(fDetected classes: {detected}) print(fEstimated insect count: {count})这个脚本的关键是 threshold 的取值。它并不是一个玄学调出来的数而是在验证集上通过网格搜索选出来的。在验证集上对每个候选阈值 [0.3, 0.4, 0.5, 0.6, 0.7, 0.8] 分别计算计数准确率选准确率最高的那个作为全局阈值。如果你的场景是“图上只会有一只虫”阈值可以设到 0.7 以上因为置信度高代表输出稳定但如果你拍的图经常有多只虫同框阈值降到 0.4 左右反而更合理因为多目标时单个目标的置信度会被分散。对同一张图做多尺度预测可以小幅提升计数稳定性把图片 resize 成 224、256、288 三种尺寸分别预测取三次结果的平均置信度再跟阈值比较。这一招本质是 TTA测试时增强不需要重新训练模型推理时间多花两倍但计数结果会更稳。4.3 把识别和计数结果写回文件交付才完整项目里 ques.csv 的作用在推理阶段才真正体现出来。最终交付不能只打印在终端要把图片名、预测类别、置信度、计数结果全部汇总到一个输出 CSV作为答辩材料或者算法效果展示。import pandas as pd ques_df pd.read_csv(ques.csv, headerNone, names[filename]) results [] for fname in ques_df[filename]: detected, count predict_and_count(os.path.join(images, fname), model) if detected: main_class train_dataset.labels[detected[0][0]] main_conf round(detected[0][1], 4) else: main_class unknown main_conf 0.0 results.append({ filename: fname, predicted_label: main_class, confidence: main_conf, count: count }) out_df pd.DataFrame(results) out_df.to_csv(output_predictions.csv, indexFalse, encodingutf-8-sig) print(out_df.head())输出文件用utf-8-sig编码是因为 Windows 下的 Excel 默认用 ANSI 编码读取 CSV如果你直接输出 utf-8用户双击文件时中文标签会全部变成乱码。这是一个非常现实且多发的交付问题。count字段的值等于检测到的高置信度类别数量对单目标图片通常是 1对无目标图片是 0对多目标图片可能大于 1这个逻辑在答辩时一条一条讲清楚比空口说“模型效果好”有说服力得多。5. 避坑清单路径分隔符、类别不平衡和验证集乱序5.1 报错 No such file or directoryCSV 里的路径分隔符是罪魁祸首现象用 data.csv 训练程序刚启动就报FileNotFoundError: [Errno 2] No such file or directory: images\\fly2.jpg但文件明明就在 images 目录下。原因在 Windows 上操作 CSV 文件时Excel 打开另存后偶尔会把路径里的正斜杠替换成反斜杠CSV 里存的是images\fly2.jpg。PyTorch 在 Windows 上对反斜杠的处理非常敏感甚至可能出现转义错误。解决统一用正斜杠。读取 CSV 后强制替换一遍df[filename] df[filename].str.replace(\\, /)再把完整的img_dir和文件名拼起来。从那以后我每次读取这类 CSV 标注都会强制走一遍路径清洗不要在路径问题上浪费两小时。5.2 datatest.csv 预测结果对不上DataLoader 的 shuffle 忘关现象模型训练完成后用 val_loader 做预测得到的预测标签顺序跟 datatest.csv 里的原始顺序完全对不上抽样检查前 20 个全是错位。原因验证阶段如果 DataLoader 设置shuffleTrue每次迭代加载样本的顺序是随机的预测结果自然无法按照文件顺序对齐回 CSV 表。解决验证和推理阶段统统设置shuffleFalse。如果你已经用打乱顺序的预测结果跑了也别慌用文件名作为唯一标识重新对齐而不是靠行索引。核心原则训练阶段 shuffle评估和推理阶段永远不 shuffle。5.3 识别率虚高类别不平衡和随机划分的陷阱现象整个训练流程没有被报错模型在验证集上的准确率 95%你很高兴地拿去答辩结果现场测试一张真实照片识别结果完全不对。原因大多数昆虫数据集存在严重的类别不平衡。比如 data.csv 里“蝴蝶”有 500 张“蝼蛄”只有 20 张。模型学到的最优策略就是不管输入是什么都输出“蝴蝶”因为这样可以获得最高的整体准确率。验证集因为同一分布也显示不出问题。解决训练前打印类别频次表发现不平衡后用两个手段。第一用WeightedRandomSampler给少数类更高的采样概率第二评估时报告混淆矩阵而不是只看整体准确率。如果你的毕业设计有什么特别值得在答辩时展示的那就是你用混淆矩阵发现了数据不平衡并针对性地做了采样改进。5.4 模型参数怎么调都停在原地冻结层和学习率组合不当现象迁移学习训练 10 个 epoch训练集 loss 从 2.3 降到 1.8 就不再下降验证集准确率停留在 60% 上下怎么调学习率都没有起色。原因只训练 fc 层时如果 fc 层的学习率用得太小而前面层又全部冻结网络就只能在一个很小的参数空间内微调收敛非常吃力。反过来说如果解冻了全部层但学习率还是 1e-3那么预训练权重很快就会被破坏loss 开始震荡。解决分层设置学习率。冻结层不更新fc 层用1e-3如果加上全量微调backbone 用1e-5到1e-4。单卡环境下这个配置在 ResNet 系列上是通用的安全区间。如果实在不行把 LR 调整成ReduceLROnPlateau模式让 loss 平台期自动降学习率。5.5 ques.csv 只有一列时 pandas 读错列名现象pd.read_csv(ques.csv)之后打印出来第一行图片名成了 DataFrame 的列名图片数量少了一条。原因ques.csv 没有表头pandas 默认把第一行当作 header。解决显式传入headerNone然后自己命名列如[filename]。同样的问题也会出现在 datatest.csv 或 data.csv 如果某次导出时没有写表头的情况。我现在的习惯是所有标注 CSV 一律显式指定列名读取绝不依赖默认行为。6. 进阶自定义类别扩展与批量推理输出6.1 把识别和计数接到批量图片目录如果你的使用场景不是单个文件预测而是一整个文件夹里几百张图片要批量识别并统计数目需要写一个批量遍历版本。核心逻辑很简单递归遍历目录下所有 jpg、png 文件对每张图调用predict_and_count把所有结果汇总成一张 DataFrame再按类别做聚合统计得到每个昆虫类别的图片张数。import glob import os import pandas as pd image_dir batch_images/ all_images glob.glob(os.path.join(image_dir, *.jpg)) \ glob.glob(os.path.join(image_dir, *.png)) batch_results [] for img_path in all_images: fname os.path.basename(img_path) detected, count predict_and_count(img_path, model, threshold0.6) label train_dataset.labels[detected[0][0]] if detected else unknown batch_results.append({filename: fname, label: label, count: count}) result_df pd.DataFrame(batch_results) label_summary result_df.groupby(label)[count].sum().reset_index() print(label_summary) result_df.to_csv(batch_predictions.csv, indexFalse, encodingutf-8-sig)这段代码有两个细节值得注意。glob的路径模式在 Windows 上也要用正斜杠不然可能出现匹配不到文件的情况。groupby(label)[count].sum()统计的是每个类别在图片里被检测到的总个体数这正是“数目统计”的核心交付指标比单纯列每张图的预测结果更直观也更方便填到毕业论文的实验结果表里。6.2 换成自己的昆虫类别只需要动两个文件如果不想用自带的那几类昆虫想换成农田害虫或城市常见昆虫完全不需要改动模型结构。操作是第一准备新的图片数据集按类别分文件夹存放第二重新生成一份新的 data.csv格式保持两列 filename, label第三重新执行训练脚本label_map 会自动根据新的类别名重建。这里必须提醒一个易错点新数据集的类别数量如果跟原来预训练分类时的类别数不一致model.fc的输出维度也要同步修改。训练脚本里已经写了num_classes len(train_dataset.label_map)所以只要你重新跑了数据准备流程这个维度会自动跟上。不要手工修改网络代码里的数字。6.3 从显卡到 CPU 部署的降级参数毕设答辩现场不一定有 GPU 机器演示用的笔记本大概率是纯 CPU 环境。ResNet18 在 CPU 上跑一张 224 图片大约需要 200 到 500 毫秒具体取决于机器。如果觉得慢有两个降级手段。第一在加载模型时强制map_locationcpu避免现场因为没有 CUDA 直接抛错第二把输入尺寸从 224 降到 160推理时间可以缩短近一半对昆虫这种大尺度目标160 分辨率依然保留足够的纹理特征。最后一个个人习惯模型训练完成后我会立刻在 CPU 模式下做一次推理自检确认权重文件和 label_map 能被正确加载而不是到答辩前一晚才发现模型在实验室的 GPU 环境好好的换到笔记本上寸步难行。这个“最后一公里”验证曾经救过我一命。从那以后我每次做毕设项目都强制把 CPU 推理验证放进流程清单里。希望帮到你。本文还有配套的精品资源点击获取