细粒度分类项目源码:数字图像处理+迁移学习+注意力机制实战
发布时间:2026/9/28 15:53:16来源:尧图网络
简介这是数字图像处理期末大作业中高分通过的图像细粒度分类项目源码98分面向计算机专业正在完成课程大作业或毕业设计的学生也适合有一定Python基础、想入门细粒度图像分类实战的学习者。项目基于CUB_200_2011数据集使用PyTorch实现从数据准备、特征提取到模型训练与评估形成完整链路并提供BCNN与迁移学习两种实现方案代码结构清晰便于二次开发。资源包共14个文件体积仅4.76MB包含Python源码、PDF文档、PPT演示文稿、Word说明、模型文件与运行指引。其中源码负责核心训练与测试逻辑PDF和PPT用于方案讲解与答辩展示README与TXT辅助快速上手目录紧凑完整。已有53人学习浏览该项目。整个项目经导师指导并获评98分所有源码均已调试运行通过可放心使用。配套的细分类讲解文档、最终报告和答辩幻灯片能帮助理解算法原理、复现实验结果也能直接支撑课程汇报与毕业设计展示。1. 数字图像处理期末大作业细粒度分类项目源码为什么能拿98分期末大作业最怕的不是代码跑不通而是做出来的东西看不出“数字图像处理”的影子。拿普通猫狗分类去交差老师只会觉得你套了一个现成分类器换成图像细粒度分类就不一样了——鸟的亚种、花的品种、汽车型号这些任务要求模型在同一个大类里分辨细微差别。难点不在“认识物体”而在“看清局部细节”正好能把这一学期学过的边缘检测、直方图均衡、颜色空间、特征提取全串起来。我按“预处理 预训练模型微调 注意力机制”三部分组织了一套图像细粒度分类项目源码跑通后在班级大作业里拿了98分。这篇笔记就从选型到训练把参数和踩坑记录完整写出来给做数字图像处理课程设计的人一条能直接照做的路。2. 细粒度分类的原理和选题理由和普通图像分类差在哪2.1 细粒度分类到底难在哪类间差异小于类内差异细粒度分类在数字图像处理期末大作业里是个很聪明的选题。普通分类面对的是“猫 vs 狗”你只需要抓住整体轮廓就能分对细粒度分类面对的是“这只鸟是黄眉柳莺还是黄腰柳莺”两种鸟的体型、颜色几乎一样可能只有腰上的一小片羽毛颜色不同。如果拿汽车来说就是大众迈腾和大众帕萨特正面看前脸格栅区别可能就是镀铬条数量。这类任务的特征是类间差异小于类内差异。同一个物种在不同姿态、不同光照下拍出来的照片比不同物种之间的差异还要大。这个特点决定了直接套用普通分类网络会吃亏。预训练模型在ImageNet上学习的是“全局形状 整体纹理”它会在特征图里把整只鸟作为主要目标翅膀上的花纹、喙的弧度这些局部判别性区域很容易被背景或躯干平均掉。我最早拿ResNet18直接微调CUB-200-2011鸟类数据验证集准确率只有78%左右跟随便占一个类的概率比起不差这说明它没有真正“看见”细粒度细节。所以做这个方向的第一步不是急着写代码而是先定义一个问题怎么让模型关注到那些尺寸很小、颜色差异很微弱的局部区域。解决思路无非两条一条是在数据端做文章用数字图像处理的手段把边缘、颜色和纹理加强另一条是在模型端加注意力让网络自动搜索判别性区域。两条都不难组合起来就是最后那个高分项目的基础。2.2 传统图像特征路线的局限边缘、颜色、纹理只能做到某一步如果翻开数字图像处理教材能直接用来做分类的传统特征不少颜色直方图、LBP纹理、方向梯度直方图HOG、SIFT特征点再配合SVM做分类。它们的好处是每一层都能解释老师看了会觉得你基础扎实。但细粒度分类恰恰是传统特征最不擅长的领域。原因很实际颜色直方图会把整张图的颜色统计混在一起两只鸟的羽毛颜色分布相近时直方图几乎一样HOG对边缘和轮廓敏感但无法区分“同样形状但表面纹理不同”的局部区域SIFT匹配在视角变化大时稳定性尚可但在条件有限的期末数据集上往往不够。我本科做课程设计时试过HOG SVM在CUB-200的一个子集上训练花了一个周末调cell_size和block_size最后准确率也没超过60%。不是说这条路线不能用而是它更适合作为对比实验。期末报告中如果把传统特征作为baseline再用深度学习方法超越它这本身就是一条完整的“消融实验”逻辑老师很吃这一套。而且你在报告里可以明确写传统特征受限于人工设计判别性信息不足所以引入可学习的深度网络。这比单纯吹深度学习结果要有说服力得多。2.3 为什么推荐“数字图像处理 迁移学习”的组合路线那到底怎么组织期末大作业的源码我见过很多90分以上的项目几乎都是“组合路线”把数字图像处理放在前后两端中间用预训练卷积神经网络做特征提取。前端的图像处理负责去噪、对比度增强、边缘增强、数据增强让模型从一开始就看到更清晰的局部纹理后端用一个轻量的注意力模块去加权特征通道让分类头更关注判别性区域。这样做有三个好处。第一你能不动声色地把课程知识全部揉进工程里。老师在答辩时问你“直方图均衡在哪里用到的”你直接打开preprocess.py那条带CLAHE的代码问“拉普拉斯算子怎么用”你给他看边缘增强的叠加逻辑。第二迁移学习让训练在几百张图片的条件下也能收敛。细粒度数据集的公开样本通常很少比如斯坦福狗狗只有120个类别、每类100多张不迁移基本没戏。第三改进点是可量化的。你可以分别跑“纯微调”“加前端图像处理”“加注意力模块”“两者都加”四组实验每组的结果放进一个表格准确率的上升过程清清楚楚这就是98分的底气。这一章说得有点多但选型这件事确实值得想清楚。下一个章节直接写前端图像处理的完整代码因为这部分最容易被低估也最容易在答辩时被追问细节。3. 把数字图像处理知识融进细粒度分类预处理与图像增强的实现3.1 预处理流程固定尺寸、去噪、拉普拉斯边缘增强图像细粒度分类最怕的就是“干净”的数据直接喂给网络。真实拍摄的鸟类图像经常带噪声、背景杂乱、边缘不清晰预训练模型见过的是规范化图片分布一旦不匹配迁移效果就会打折扣。所以我习惯在进入网络之前做三步固定处理先统一尺寸再轻量去噪最后把边缘信息用拉普拉斯算子叠加回原图。这一步是纯数字图像处理代码很直观import cv2 import numpy as np def resize_and_pad(img, size(224, 224)): # 保持长宽比缩放到目标尺寸剩余区域用灰色填充 h, w img.shape[:2] scale min(size[0] / h, size[1] / w) nh, nw int(h * scale 0.5), int(w * scale 0.5) img cv2.resize(img, (nw, nh), interpolationcv2.INTER_LINEAR) canvas np.full((size[0], size[1], 3), 128, dtypenp.uint8) canvas[0:nh, 0:nw] img return canvas def edge_enhance(img, edge_weight0.3): # 拉普拉斯提取边缘归一化后与原始图像加权叠加 gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) lap cv2.Laplacian(gray, cv2.CV_64F, ksize3) lap cv2.normalize(lap, None, 0, 1, cv2.NORM_MINMAX) lap_img (lap * 255).astype(np.uint8) lap_rgb cv2.cvtColor(lap_img, cv2.COLOR_GRAY2RGB) enhanced cv2.addWeighted(img, 1.0, lap_rgb, edge_weight, 0) return enhanced这里resize_and_pad中的scale取长宽比的小值避免拉伸变形。细粒度分类对形变非常敏感一只鸟被拉长成扁的模型的形状先验就会错乱。灰色填充而不是黑色填充是为了避免黑色边框在归一化后产生过强的暗区响应。edge_enhance里的ksize3是拉普拉斯卷积核大小小核能保留更细的边缘适合羽毛纹理k5也行但会把噪声一起放大。edge_weight控制边缘叠加的强度我一般在0.2~0.4之间尝试默认0.3。注意这里不要做全局直方图均衡原因在3.3小节展开。这部分的实现要点是把图像处理当成“输入清洗”而不是“数据增强”保证训练和验证阶段使用完全相同的预处理流程。我在源码里会把resize_and_pad和edge_enhance组合成一个preprocess_for_inference函数验证和测试阶段直接复用避免数据分布不一致。3.2 用图像处理算子做数据增强随机裁剪、翻转、非锐化掩蔽数据增强对于细粒度分类是不可或缺的。普通的随机裁剪、翻转能模拟拍摄视角变化但只有这些还不够。我一般会把数字图像处理里的非锐化掩蔽Unsharp Mask偷换成一种轻量增强把原图与高斯模糊后的图加权相减让纹理边缘更锐利以一定的概率应用。这个操作对鸟的羽毛纹路和花的叶脉很有效因为它放大了高频细节。def train_aug(img, output_size(224, 224)): h, w img.shape[:2] # 随机裁剪一个边长约为原图长边60%~100%的区域 crop_ratio np.random.uniform(0.6, 1.0) crop_h int(h * crop_ratio) crop_w int(w * crop_ratio) x1 np.random.randint(0, w - crop_w 1) y1 np.random.randint(0, h - crop_h 1) img_crop img[y1:y1 crop_h, x1:x1 crop_w] # 缩放到统一输出尺寸 img_crop cv2.resize(img_crop, output_size, interpolationcv2.INTER_LINEAR) # 随机水平翻转概率0.5 if np.random.rand() 0.5: img_crop cv2.flip(img_crop, 1) # 非锐化掩蔽原图加权后减去模糊图的一半强化边缘 blur cv2.GaussianBlur(img_crop, (5, 5), 0) if np.random.rand() 0.5: img_crop cv2.addWeighted(img_crop, 1.5, blur, -0.5, 0) return img_crop这里我把随机裁剪放在缩放之前先裁出一块包含局部细节的区域再缩放到224这样模型每次看到的“取景范围”都不一样。crop_ratio的0.6~1.0是关键参数范围越大每次裁剪的区域变化越大模型越需要从局部判别但取值太小会把一只鸟裁掉一半反而增大学习难度。非锐化掩蔽里addWeighted的参数1.5和-0.5本质是“原图 0.5 × (原图 - 模糊图)”的展开形式只在随机概率下应用避免模型过拟合到锐化图像上。你可能会有疑问为什么不直接用Albumentations库Albumentations很好用但期末大作业的答辩现场老师如果让你解释每步图像处理的物理意义你总不能说是库函数自动做的。手写OpenCV实现的另一个好处是能放在“特征工程与分析”章节里报告里可以贴出增强前后的对比图这对得分很有帮助。3.3 直方图均衡与颜色空间转换对细粒度分类到底有没有用直方图均衡是数字图像处理课程的招牌内容很多人会在期末项目里直接对每张图做全局均衡。实际上全局均衡在细粒度分类里往往适得其反。鸟类照片的直方图中背景天空占了很大比例全局均衡会把天空的灰度范围拉宽羽毛区域的对比度反而被压缩。我试过用cv2.equalizeHist做验证集预处理准确率掉了3个点原因就是这个。更好的做法是利用CLAHE对比度受限的自适应直方图均衡在局部小区域做均衡并限制对比度放大幅度def clahe_lightness(img, clip_limit2.0, tile_grid(8, 8)): # 转到LAB空间只对L亮度通道做CLAHE lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimitclip_limit, tileGridSizetile_grid) l_enhanced clahe.apply(l) # 与原始亮度做线性混合避免过度增强 l_mix cv2.addWeighted(l, 0.6, l_enhanced, 0.4, 0) lab cv2.merge([l_mix, a, b]) return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)CLAHE作用在亮度通道上不会破坏颜色信息clip_limit2.0表示把局部直方图高于该阈值的部分截断再重新分配数值越大对比度增强越猛。tile_grid是局部分块的大小8x8在224分辨率下每块约28x28像素足够覆盖一片羽毛区域。如果图像本身对比度已经很好可以把clip_limit调到1.0甚至不做。对于期末考试把这个函数作为可选的增强分支并在报告中做一个“有CLAHE与无CLAHE”的小对比比盲目加在所有图上要更显功力。颜色空间方面我建议不要改。预训练模型期望的输入分布是RGB且符合ImageNet统计如果强行转换到HSV或GRAY第一层卷积核学到的颜色权重会被浪费。所以我的源码里颜色空间转换只用于边缘提取等中间处理最终喂给网络的仍然是RGB三通道。这和很多数字图像处理实验书里“先转灰度再分类”的做法不一样因为迁移学习的输入必须和预训练时对齐。4. 用PyTorch实现细粒度分类项目源码数据加载、模型、训练与评估4.1 数据集组织与自定义Dataset文件夹结构、标签映射细粒度分类项目源码通常选择公开数据集比如CUB-200-2011鸟类、Flowers-102鲜花或Stanford Cars。期末场景下不一定要用完整数据集取其中的8~10个相近类别、每个类别100张以上已经足够体现“细粒度”了。数据集的目录结构我习惯组织成data/train/类名/图片和data/val/类名/图片这样写Dataset最简单也便于答辩时展示。import os import cv2 import torch from torch.utils.data import Dataset class FineGrainedDataset(Dataset): def __init__(self, root, transformNone): self.root root self.transform transform # 按字母序生成类别列表保证多次运行标签一致 self.classes sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) self.class_to_idx {cls: i for i, cls in enumerate(self.classes)} self.samples [] for cls in self.classes: cls_path os.path.join(root, cls) for fname in os.listdir(cls_path): if fname.lower().endswith((.jpg, .jpeg, .png)): path os.path.join(cls_path, fname) self.samples.append((path, self.class_to_idx[cls])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if self.transform: img self.transform(img) # transform作用于np.ndarray返回np.ndarray # 转成CHW的Tensor并归一化到[0, 1] img torch.from_numpy(img).float().permute(2, 0, 1) return img, label这个类的关键点在于class_to_idx必须在第一次初始化时就生成且使用sorted排序。如果两个进程分别扫描目录顺序不一致会导致标签错位。另外cv2.imread读进来是BGR必须转回RGB如果漏转网络看到的是反色的图像准确率会崩到接近随机。这里的transform可以直接传入前面第3章写好的train_aug也可以传入验证用的resize_and_pad edge_enhance只要保证输入输出都是np.ndarray即可。关于标准化我并没有把它写进Dataset里而是在训练循环里统一处理。这样做的原因是标准化对应的mean和std是从ImageNet统计来的和预处理是两件事分开更清晰。注意这套标准化参数必须与预训练模型一致mean[0.485,0.456,0.406]std[0.229,0.224,0.225]。4.2 模型搭建预训练ResNet SE注意力模型选择上ResNet18在数据量小的时候是性价比最高的骨干网络ResNet50的参数量是18的数倍但小数据下不一定有稳定提升。我最后用的源码是一个“ResNet18 SE通道注意力”的组合SE模块提取特征图每个通道的全局统计然后自适应加权。这个加权行为从数字图像处理的角度看相当于在频域对特征通道做一个增强滤波解释起来很顺。import torch import torch.nn as nn import torchvision.models as models class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) def forward(self, x): b, c, _, _ x.size() w self.avg_pool(x).view(b, c) w self.fc(w).view(b, c, 1, 1) return x * w class FineGrainedModel(nn.Module): def __init__(self, num_classes): super().__init__() backbone models.resnet18(pretrainedTrue) # 去掉ResNet自带的avgpool和fc保留到layer4 self.features nn.Sequential(*list(backbone.children())[:-2]) self.se SEBlock(512, reduction16) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(512, num_classes) ) def forward(self, x): x self.features(x) x self.se(x) x self.classifier(x) return xSEBlock里的reduction是通道压缩比例。设为16时输入512通道会先压到32再还原到512这个瓶颈能迫使全连接层学习到通道间的非线性关系。如果数据集很小比如每类只有几十张可以把reduction改到4或8减少参数量防止全连接层过拟合。self.features用list(backbone.children())[:-2]的做法在ResNet18上正好保留conv1、bn1、relu、maxpool、layer1到layer4输出是[B, 512, 7, 7]后续SE和分类头都能对上。这里还有一个细节新版PyTorch在调用pretrainedTrue时会提示改用weights参数但期末代码里不影响运行。如果实验室不能联网需要手动下载预训练权重再用torch.load加载建议在FineGrainedModel里加一个model_path参数读本地文件避免答辩现场网络不稳定。4.3 训练参数与损失函数学习率、优化器、类别不平衡训练一个细粒度模型最忌讳一上来就用大学习率全量微调。常见做法是分两个阶段第一阶段冻结骨干网络只更新分类头和SE模块用学习率1e-3训练5~10个epoch第二阶段解冻整个网络用学习率1e-4微调10~20个epoch。这样做的原因是避免在训练早期破坏预训练特征的分布。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in loader: images images.to(device) / 255.0 labels labels.to(device) # 标准化使用ImageNet的mean/std mean torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1).to(device) std torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1).to(device) images (images - mean) / std outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() # 梯度裁剪防止个别batch带来梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * images.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max20, eta_min1e-5)这里的/255.0是把0-255的像素缩放到0-1然后再做mean/std标准化。clip_grad_norm_的max_norm5.0是个很实用的参数细粒度分类的特征图较大偶尔会出现梯度值达到几十的情况不裁剪的话一个batch就能把学习率冲乱。优化器用AdamW而不是SGD因为AdamW对学习率的敏感度低一些期末场景更容易调出可用的结果。SGD加动量如果调好了可以更高但需要更多调参时间。损失函数默认用CrossEntropyLoss。如果你的数据类别数不平衡比如一个类别有200张、另一个只有80张需要给损失函数传入weight。计算方式在下一章避坑部分会给出。对于一般期末数据我建议采用标签平滑设置smoothing0.1会让模型不过分自信验证集f1常常会提高一点。4.4 评估与可视化准确率、分类报告、混淆矩阵训练结束后不能只看整体准确率要在验证集上输出每类的precision、recall和混淆矩阵。细粒度分类的高分项目往往赢在这些细节你能指出哪两个类别容易混淆并说明原因老师会认为你真的在解决问题而不只是跑完一个脚本。from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, val_loader, classes, device): model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) / 255.0 mean torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1).to(device) std torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1).to(device) images (images - mean) / std outputs model(images) all_preds.extend(outputs.argmax(dim1).cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 打印带类名的precision/recall/f1 print(classification_report(all_labels, all_preds, target_namesclasses)) cm confusion_matrix(all_labels, all_preds) return all_preds, all_labels, cmclassification_report会把每个类别的F1列出来报告里可以直接截图使用。混淆矩阵cm的形状是[num_classes, num_classes]第i行第j列表示“真实类别i被预测成j”行加起来是每个类的真实样本数。画图时最好加上annot和colorbar后面第6章会给出完整绘制代码。评估阶段还有一个容易被忽略的点验证集绝不能做随机裁剪等随机增强只保留固定中心裁剪和与训练一致的预处理。这样验证集的结果才稳定不会因为随机性出现一次92%、一次88%的情况。我在源码里把train和val的分支分开val只做resize、边缘增强和中心裁剪。5. 避坑与排查细粒度分类项目从80分到98分的关键问题下面这些问题是我在调试过程中真实遇到过的每一条都按现象、原因、解决三步写方便你在报告里直接引用。5.1 现象验证集准确率很高但换一批图或现场测试结果明显下滑先说最常见的问题。代码在验证集上跑出90%多但答辩现场用手机拍一张同类物体扔进去预测完全不对。原因是验证集和训练集来自同一份分布而真实场景的光照、背景、分辨率都变了模型没有见过这种域变化。解决的办法分两块。第一严格划分数据用sklearn.model_selection.train_test_split对每个类别的样本做分层划分并固定random_state42。不要让同一张图的不同裁剪同时出现在训练和验证集里否则验证集会虚高。第二在训练阶段引入更强的随机增强比如随机亮度、随机饱和度、高斯噪声让模型对真实条件下的图像更鲁棒。我一般把随机亮度的范围设成0.8~1.2强度不大但效果很明显。5.2 现象训练loss不下降一直卡在1.9~2.1附近震荡这个现象几乎每个做细粒度分类的人都会遇到。如果类别数是10初始loss应该接近ln(10)≈2.3然后快速下降。如果loss卡在1.9附近震荡很久说明模型根本没有学习到内容输出几乎全是均匀分布的猜测。原因一般是两个一个是学习率过大另一个是骨干网络的所有权重都在随机初始化时被梯度破坏了。解决步骤是先把骨干网络冻结只训练分类头学习率用1e-3等分类头收敛后再解冻骨干网络把学习率降到1e-4。另一个经验是初始阶段不要直接上随机裁剪CLAHE全套数据增强太猛模型在起步阶段会学不到稳定的低频轮廓。我习惯前5个epoch只做轻量增强后面再逐步打开所有增强。5.3 现象GPU利用率只有20%训练一个epoch要好几分钟细粒度样本数量不大但每次图像处理都要做Laplacian、CLAHE这些操作如果都在主进程里跑会严重拖慢数据加载。线程卡在cv2.imread和cv2.filter2D上的时候GPU只能空转。原因是没有合理使用数据加载的多进程能力。解决方式是给DataLoader设置num_workers4或8并打开pin_memoryTrue。注意在Windows系统上num_workers大于0时需要在if __name__ __main__保护下创建DataLoader否则多进程会递归炸掉。还有一个小技巧把预处理函数移到Dataset.__getitem__里而不是在主循环里做这样多进程才能并行执行。5.4 现象混淆矩阵里总有两三个类互相分不清f1明显低于其他类细粒度分类里最容易分不清的是那些颜色、形状都接近的类比如黄腰柳莺和黄眉柳莺它们的区别可能只在腰部的羽毛面积。单纯的通道注意力对这类细微局部分辨能力有限。原因是这些类之间真正的判别性区域太窄网络没有足够的监督信号去聚焦。解决思路有三个一是针对混淆类别在训练时多采样难分类对比如使用Focal Loss它会对置信度高的样本降低损失权重让模型更关注低置信度的难样本二是增强输入分辨率把224提升到320让局部区域在特征图里占据更多像素三是增加数据量去收集这些容易混淆的类别的更多图片哪怕只多50张效果都会比调模型更明显。我最后的模型就是把验证分辨率从224提到256混淆率降了约2个百分点。5.5 现象训练出来的曲线锯齿严重报告里看不清趋势很多同学提交报告时会直接截图训练日志里的loss数值然后点个折线图。如果epoch数少、batch噪声大曲线会像心电图一样老师根本看不出你的改进过程。原因是没有对曲线做平滑而且用的是每step的loss不是每epoch的loss。解决方法是记录每个epoch的loss和准确率然后在绘图时用指数移动平均EMA平滑。给一个可以抄的平滑函数def smooth_curve(values, alpha0.95): smoothed [] last values[0] for v in values: last last * alpha (1 - alpha) * v smoothed.append(last) return smoothedalpha0.95表示当前值只占5%权重曲线会非常平滑。如果alpha太大真实的掉点会被掩盖我一般用0.9~0.95。画图时还要设置plt.figure(dpi200)避免插入Word后被压缩得糊掉。记住报告里的图表清晰度直接影响老师对工作量的判断。6. 最后的提分技巧把实验结果做成答辩级图表细粒度分类项目做到准确率达标只算完成了一半。我拿98分那次模型准确率在班级里排第二但结论展示做得比较充分所以总分第一。这里说几个能直接提升答辩观感的技巧。第一画一张消融对比表。表格要包含“直接微调”“图像预处理增强”“SE注意力”“两者”四行列是准确率和F1。示例值可以是88.2 / 91.6 / 93.4 / 94.8具体数值以你实际实验为准。这张表能让老师一眼看到你的工作量比任何文字描述都有说服力。第二把混淆矩阵画得规范一些。使用matplotlib的imshow每个格子显示数字类别名称旋转45度防止重叠import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(10, 10), dpi200) im ax.imshow(cm, cmapBlues) ax.figure.colorbar(im, axax) ax.set_xticks(range(len(classes))) ax.set_yticks(range(len(classes))) ax.set_xticklabels(classes, rotation45, haright) ax.set_yticklabels(classes) for i in range(len(classes)): for j in range(len(classes)): ax.text(j, i, str(cm[i][j]), hacenter, vacenter, fontsize8) ax.set_xlabel(Predicted) ax.set_ylabel(True) ax.set_title(Confusion Matrix)第三把超参数全部记录到json或表格中。我见过不少人在答辩时被问“你这个项目的batch size是多少学习率怎么调”然后现场翻代码。提前把学习率、优化器、轮数、随机种子、数据规模、每类准确率整理成一张参数表放在报告附录里既显专业也能避免紧张出错。最后说一个我的教训第一次做大作业时我把所有实验代码塞进一个Jupyter Notebookloss曲线没平滑混淆矩阵没标签老师看了一眼就没兴趣了。后来我把代码拆成data.py、model.py、train.py、evaluate.py四个文件用README记录复现步骤实验表格单独列一章整个项目看起来才真正像一份“源码”。细粒度分类能不能拿高分拼的不只是准确率而是你能不能把数字图像处理、模型改进和实验结果串成一条完整的故事。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网