news 2026/10/2 14:54:57

VGG迁移学习实战:卷积神经网络与珊瑚种类识别指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VGG迁移学习实战:卷积神经网络与珊瑚种类识别指南

简介:这是一份基于PyTorch的VGG卷积神经网络珊瑚种类识别项目,面向深度学习和图像分类初学者,也适合需要快速搭建完整CNN训练流程的开发者。代码仅用三个Python脚本实现,分别负责生成训练集txt、执行CNN训练以及提供PyQt界面,且每一行都附有中文注释,配合说明文档和requirements.txt环境清单,能显著降低从数据准备到模型调用的理解门槛。压缩包共8个文件,包含3个py脚本、1个docx说明文档、1个环境依赖txt及若干图片分类示例,大小仅213KB,轻量便携。虽然不含数据集图片,但已在脑珊瑚、软珊瑚、扇形珊瑚等类别文件夹中放置提示图,用户自备图片放入对应目录即可扩展训练。目前已有70人学习下载,适合入门图像分类、理解卷积神经网络或搭建可视化标注界面时参考。

1. 珊瑚种类识别这种小样本问题,为什么先选VGG而不是追新网络

拿到一个标题是“vgg模型-通过CNN卷积神经网络的珊瑚种类识别-不含数据集图片-含逐行注释和说明文档.zip”的压缩包,很多做海洋生态监测的朋友会犹豫:VGG不是好几年前的CNN结构了吗?我的看法相反:珊瑚识别恰恰适合从VGG起步。这类任务的公开样本往往只有几百张到几千张,类别间差异又很细微,用太深的网络很容易原地过拟合;VGG结构直白、预训练权重好找、逐行注释容易写,是先把流程跑通再谈精度的最佳起点。

这篇笔记按我复现类似代码包的习惯,从数据准备、迁移学习、训练排错一直讲到最后验收。内容适合两类人:刚接触深度学习、手里有珊瑚图片但不知怎么组织和训练的;以及被各种新网络折腾过、想回来用稳定基线的工程师。前者按章节顺序跟,后者可以直接跳到第5章看坑。

2. VGG在CNN结构里为什么不过时:卷积堆叠、迁移学习与分类头改造

实际跑珊瑚分类时,经常会遇到两类人:一类嫌VGG老,非要上ResNet152;另一类连CNN基础都不熟,拿到包就往GPU上丢,结果效果还不如传统颜色纹理特征。这两类情况我都见过。VGG的“老”恰恰是它适合入门和复现的原因,尤其是这种物种识别项目。

2.1 VGG的卷积堆叠逻辑:小卷积核多次叠加为什么适合珊瑚纹理

VGG的骨干是一套固定套路:连续堆叠3x3卷积,每个block后面接一个2x2 max pooling,最后接两层4096全连接。VGG16大致是64-64-pool-128-128-pool-256-256-256-pool-512-512-512-pool-512-512-512-pool-FC4096-FC4096-FC1000。拆开看就两个规律:通道数从64涨到512,分辨率从224降到7;卷积永远是3x3,感受野靠堆叠。两个3x3卷积等效于一个5x5卷积,三个等效于7x7,但参数少,中间还有两次ReLU带来更多非线性。

这个设计对珊瑚识别很友好。珊瑚分类主要看表面孔洞、分支形态和纹路走向,这些是局部纹理特征,小卷积核局部性好,再往上叠加能看到更大范围的形态关系。如果一开始就上7x7大卷积核,小样本下更容易学出边缘毛刺,而不是稳定的物种特征。另外,VGG比ResNet浅了不少,通道也不算夸张,224x224输入下特征层显存占用并不大;真正的参数量主要在全连接部分,这也是下一节要改分类头的原因。

顺带说一句CNN和RNN的区别:珊瑚识别是静态图像的空间模式,用CNN是把纹理和形态编码成特征;RNN适合序列依赖,比如珊瑚视频的逐帧生长分析。你如果只做单张图片分类,不要为了追热度把RNN塞进这个包里,那只会增加调参维度和过拟合风险。

2.2 修改VGG16分类头:目标类别数、冻结卷积层和Dropout的取舍

用PyTorch复现时,我一般直接加载torchvision里的VGG16预训练权重,然后替换最后的全连接层。VGG16在ImageNet上学到的边缘、纹理和局部形状特征,对水下珊瑚照片也有迁移价值,没必要自己从头训。

import torch import torch.nn as nn from torchvision import models def build_vgg16(num_classes=5, freeze_conv=True): weights = models.VGG16_Weights.DEFAULT model = models.vgg16(weights=weights) # vgg16.classifier是一个Sequential,结构大致是: # Linear(512,4096) -> ReLU -> Dropout(0.5) # -> Linear(4096,4096) -> ReLU -> Dropout(0.5) # -> Linear(4096,1000) # 只把最后的1000类输出换成珊瑚种类数 in_features = model.classifier[6].in_features model.classifier[6] = nn.Linear(in_features, num_classes) if freeze_conv: # 冻结所有features参数,只训练新分类头 for param in model.features.parameters(): param.requires_grad = False return model

这里有几个容易踩的细节。models.VGG16_Weights.DEFAULT会自动下载官方预训练权重;如果没联网,有些环境会静默退化到随机初始化,训练效果会非常差。in_features直接读原分类头的输入维度,通常是4096;不要写死,因为换VGG19或调整结构后仍然是4096,但读属性更安全。freeze_conv建议第一次跑设True,显存占用小、收敛快;后面如果准确率不够,再解冻features末尾的卷积层。VGG16默认在classifier里带了两个Dropout(0.5),替换最后一层后它们仍然生效,对小数据集是一种保护;如果仍然过拟合,可以在新的Linear之前再插一个Dropout。

如果你发现VGG16收敛后准确率仍上不去,可以换成VGG19对比一下,只要把构造函数改成models.vgg19(weights=models.VGG19_Weights.DEFAULT),其它代码基本不用动。VGG19只是多了几个卷积层,分类头维度一样。我在珊瑚项目里很少因为“多两层”就获得明显提升,更多是数据侧问题。这个对比也说明,选VGG不是因为它最强,而是因为替换成本最低,排错最容易。

2.3 分阶段微调:先训分类头,再解冻最后一组卷积

用迁移学习时,我习惯分两步走。第一步冻结全部卷积层,只训练分类头,学习率设1e-3,跑5到10个epoch,让随机初始化的最后一层Linear先适应VGG提取的特征。第二步解冻features里最后两组512通道卷积,把学习率降到1e-4,整个网络一起微调。

不要一上来就解冻全部层,否则前面卷积特征被破坏,后面全连接也跟着震荡。珊瑚这种小样本任务,解冻层数越深反而越容易过拟合。如果样本总量不到五百张,我通常只解冻features中最后一个block;如果超过五千张,再考虑解冻两个block。这个经验值不严谨,但能少走弯路。

3. 包不带图片,怎么让VGG训练起来:数据目录、划分脚本和transform参数

拿到不含数据集的压缩包,第一步不是找代码,而是把图片变成VGG能读的目录。很多人在这一步就开始踩坑:找了一堆珊瑚图片全部塞进一个文件夹,然后想靠一个CSV指定标签。用torchvision的ImageFolder时,这种做法会非常别扭。常见做法是每个种类一个子目录。

3.1 从文件夹到训练集:按类别比例切分train/val/test

先把原始图片整理成下面的目录结构。如果你手上的图片不是按文件夹分类,而是一张标注表,那需要先写脚本把图片复制到对应类别目录。

coral_raw/ Acropora_millepora/ IMG_001.jpg IMG_002.jpg Porites_lutea/ IMG_101.jpg ...

整理好之后,用下面的脚本按类别内比例划分数据集。注意是按每个类单独切分,而不是把所有图片混在一起随机切。

import os import random import shutil random.seed(2024) src_root = "coral_raw" dst_root = "data" split_ratios = {"train": 0.7, "val": 0.15, "test": 0.15} for class_name in os.listdir(src_root): class_path = os.path.join(src_root, class_name) if not os.path.isdir(class_path): continue images = [f for f in os.listdir(class_path) if f.lower().endswith((".jpg", ".jpeg", ".png"))] if not images: continue random.shuffle(images) n = len(images) n_train = int(n * split_ratios["train"]) n_val = int(n * split_ratios["val"]) for split_name, start, end in [ ("train", 0, n_train), ("val", n_train, n_train + n_val), ("test", n_train + n_val, n), ]: out_dir = os.path.join(dst_root, split_name, class_name) os.makedirs(out_dir, exist_ok=True) for img_name in images[start:end]: shutil.copy2( os.path.join(class_path, img_name), os.path.join(out_dir, img_name) )

按类内比例切分的好处是,即使某个稀少种类只有30张图,它在train/val/test里的比例也和其他类一致;如果全局随机切,稀有类可能全部落到train或test,val里根本没有这个类,最终验证指标会骗你。random.seed(2024)保证重复执行时划分一致。shutil.copy2会保留EXIF信息,方便后面排查拍摄时间、光照方向;如果磁盘不够,可以用os.symlink建软链接,但Windows下需要注意权限。

如果原始图片不是按文件夹分好的,我建议先用VGG Image Annotator这类基于浏览器的标注工具,把“哪张图属于哪个种类”标成JSON,再写一个十几行的转换脚本,按JSON里的文件名拷到对应目录。这个工具的输出格式简单,和自己写解析脚本不冲突。珊瑚识别一般只需要文件名和类别,不需要多边形坐标,标注时不要过度设计。

3.2 transform参数:RandomResizedCrop和ImageNet均值归一化怎么设

ImageFolder会按子文件夹名自动生成类别索引,顺序是字典序。也就是说“Acropora_millepora”排在“Porites_lutea”前面,索引就是0、1。后面inference和保存checkpoint都要依赖这个映射,不要靠肉眼记。

from torch.utils.data import DataLoader from torchvision import datasets, transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_transform = 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]), ]) train_dataset = datasets.ImageFolder("data/train", transform=train_transform) val_dataset = datasets.ImageFolder("data/val", transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)

这里有几个参数容易被改坏。Normalize的mean/std是ImageNet统计值,VGG预训练权重就是在这个分布上学的,输入必须用同一套值;如果换成自己数据集的均值,等于把预训练特征分布又改了,迁移学习优势就没了。RandomResizedCrop(224)比直接Resize(224,224)更适合珊瑚:它随机裁一块再缩放到224,相当于免费的尺度增强,让模型适应不同拍摄距离下的珊瑚。scale=(0.6, 1.0)表示保留原图60%到100%面积,太小的scale会把珊瑚主体裁掉,变成背景分类。

ColorJitter要保守。珊瑚颜色在分类里有一定作用,但水下照片白平衡差异很大,亮度和对比度增强有助于泛化;色相改动过大会把物种颜色语义改掉,所以我只调brightness、contrast、saturation,不调hue。验证阶段不要加随机增强,Resize(256)后CenterCrop(224)比直接Resize(224,224)能减少边缘形变,保留稍大视野。

3.3 类别数量差异太大时:用WeightedRandomSampler做有放回采样

水下调查数据里,常见种类可能有三四百张,稀有种类只有二三十张。如果直接用原始分布训练,模型会偏向常见类。最简单的数据层处理是用WeightedRandomSampler替代shuffle=True。

from torch.utils.data import WeightedRandomSampler class_counts = {} for _, label in train_dataset.samples: class_name = train_dataset.classes[label] class_counts[class_name] = class_counts.get(class_name, 0) + 1 sample_weights = [1.0 / class_counts[train_dataset.classes[label]] for _, label in train_dataset.samples] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=False, sampler=sampler, num_workers=4, pin_memory=True)

注意,设置sampler后shuffle必须为False,否则两个机制会冲突。replacement=True表示每次从全量数据里有放回抽取,稀有类被抽到的概率会明显提高。num_samples可以设成远超样本总数的值,比如num_samples=len(sample_weights) * 2,这样每个epoch会多跑一些step,相当于做了过采样,但不需要真的复制图片文件,省磁盘。如果你在损失函数里又加了class weight,建议先只用sampler,因为两者叠加可能让稀有类被过度放大,导致训练震荡。

4. 从加载VGG到保存checkpoint:训练脚本、断点续训与说明文档的取舍

这一章解决的是“包能跑”到“包能复现”之间的差距。很多人下载到代码包后,最关心的是有没有逐行注释和说明文档,但说明文档如果只写“运行python train.py”,那遇到问题还是得自己猜。我觉得真正值得写的说明,是把关键参数、目录约定和断点逻辑写清楚。

4.1 训练循环的最小实现:loss、optimizer、scheduler和验证代码

把训练脚本写成可复现的最小骨架,我的习惯是保留一个train函数,不要把所有逻辑都塞进Jupyter Notebook。下面的代码是一个完整的最小训练循环。

import torch import torch.nn as nn from torch.optim import SGD from torch.optim.lr_scheduler import StepLR model = build_vgg16(num_classes=5, freeze_conv=True).cuda() criterion = nn.CrossEntropyLoss() optimizer = SGD(model.classifier.parameters(), lr=1e-3, momentum=0.9, weight_decay=5e-4) scheduler = StepLR(optimizer, step_size=5, gamma=0.1) for epoch in range(30): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) scheduler.step() model.eval() correct = total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.cuda(), labels.cuda() outputs = model(images) _, preds = torch.max(outputs, 1) total += labels.size(0) correct += (preds == labels).sum().item() val_acc = correct / total print(f"epoch {epoch + 1:02d} val_acc {val_acc:.4f}")

这是CNN代码里最常见的骨架。CrossEntropyLoss内部先做log_softmax再算NLL,所以模型最后一层不用加softmax,直接输出裸logits即可。SGD带momentum和weight_decay,比Adam在微调阶段更容易收敛到平坦的极值;如果分类头很大,可以先试Adam(lr=1e-3),但我的经验是小数据集上SGD更稳。StepLR每5个epoch把lr乘0.1,VGG16分类头通常第3到第5个epoch就能到不错的位置,然后逐步精细。验证时必须用torch.no_grad(),否则历史计算图会一直堆积,很快就把显存耗尽。

4.2 保存“能接着训”的checkpoint:class_to_idx和optimizer_state都不能少

很多新手保存模型时只写一句torch.save(model.state_dict(), "best.pth")。这在本地测试也许够用,但训练中断后想恢复,就会发现优化器状态丢了、学习率调度器状态也丢了,甚至类别顺序都不记得了。我一般会保存成checkpoint字典。

best_acc = 0.0 best_epoch = 0 for epoch in range(30): # ... 训练和验证代码 ... if val_acc > best_acc: best_acc = val_acc best_epoch = epoch checkpoint = { "epoch": epoch + 1, "model_state": model.state_dict(), "optimizer_state": optimizer.state_dict(), "scheduler_state": scheduler.state_dict(), "class_to_idx": train_dataset.class_to_idx, "best_val_acc": best_acc, } torch.save(checkpoint, "checkpoints/vgg16_coral_best.pth")

恢复训练时,不能只加载模型参数,要把optimizer和scheduler状态也加载回来,并且将epoch重置为checkpoint里的值。class_to_idx是最容易被忽略的字段。珊瑚项目后期往往要补充新图片,一旦重新跑ImageFolder,类别索引可能变化;如果没有保存这个映射,之前训练好的模型预测新图时会把标签张冠李戴。如果有混合精度训练,还要额外保存scaler.state_dict(),否则AMP的状态不同步。

另外,只要验证集acc创新高就保存一次,而不是最后一个epoch才保存。小数据集的验证曲线通常上下震荡,最后一个epoch不一定是泛化最好的点。你可以结合早停:连续10个epoch验证集acc不上升,就恢复best checkpoint并停止训练。

4.3 说明文档应该写什么:运行顺序、参数表和常见问题

好的说明文档不是把代码贴一遍,而是让一个从没看过这个包的人能在20分钟内跑通。我给珊瑚识别代码包写README时,一定会包含下面这几块。

# 训练说明 ## 环境 - Python 3.9+ - PyTorch 2.x - GPU显存建议 8G 以上 ## 目录 - data/train, data/val, data/test - checkpoints/ ## 运行顺序 1. python prepare_data.py 2. python train.py 3. python inference.py ## 参数 - num_classes=5 - batch_size=32 - lr=1e-3(第一阶段) - 冻结卷积层,只训练分类头 ## 常见问题 - 如果加载预训练权重报错,先删除本地缓存后重新下载 - 如果GPU显存不足,把batch_size降到16,并开启梯度累积

逐行注释也不是每行都写“这行定义了变量”,而是在关键决策点写“为什么”。比如“为什么用SGD而不是Adam”“为什么这里先冻结卷积层”“为什么Normalize用ImageNet的均值和标准差”。这些注释对新手最有价值,也是这类不带数据集的代码包里最值得保留的部分。如果说明文档连“图片放在哪个目录”“类别文件夹怎么命名”都没写,那这份文档是不合格的,因为包本身就不含数据,用户必须知道怎么替换数据。

5. 珊瑚识别中的5个典型事故:避坑排查记录

把类似代码包跑通时,我几乎每次都会撞见下面这些问题。每一项按现象、原因、解决来写,可以直接对照排查。前几章的内容也可以根据这里的建议回改。

5.1 loss不降、准确率等于猜:先确认加载的是预训练权重

现象:训练了十几个epoch,loss在2附近不下降,验证集准确率在“1/类别数”附近抖动。看起来模型能跑,但效果和瞎猜差不多。

原因:最常见的是模型构建时用了models.vgg16(weights=None),或者预训练权重下载失败后代码静默降级为随机初始化。VGG16有上亿参数,珊瑚图片只有几百张,随机初始化根本训不动。

解决:训练前先打印一个特征层权重的统计值,或者直接检查weights参数。更稳妥的办法是打印model.classifier[0].weight.abs().mean().item(),预训练权重通常数值较小,随机初始化通常偏大且分布不同。

print(model.classifier[0].weight.abs().mean().item()) # 预训练权重一般明显小于随机初始化

如果发现是随机初始化,停下来修正加载逻辑,不要硬着头皮继续跑。

5.2 训练集准确率虚高、验证集崩:检查ImageFolder的类别排序和划分种子

现象:训练集准确率接近100%,验证集却只有60%,而且不同epoch之间波动很大。换一批新图,效果更差。很多人第一反应是过拟合,但有时候是数据划分错位。

原因:ImageFolder按文件夹名的字典序生成类别索引,如果你自己写的标签表是按另一个顺序排的,那么index和类别名就对不上。另一个常见原因是划分数据集时没有固定random seed,同一个潜点拍的一组照片被随机分到了train和val,看起来验证集不容易,实际是不同来源的光照差异被误当成类别差异。

解决:先打印train_dataset.class_to_idx,和你自己的标签表核对。其次,在划分脚本里固定seed,并且尽量按采集点或批次分层划分,不要把同一批照片拆到两个集合里。如果某个种类只有二三十张,宁可在val里多放几张,也不要让这个类在val里消失。

5.3 GPU显存OOM:VGG16全连接层太大,batch_size和梯度累积是关键

现象:设置batch_size为32,输入224x224,在8G显存不到的GPU上直接报CUDA out of memory。这不是因为你写错了,而是VGG16的classifier里有两个4096全连接,非常占显存。

原因:VGG16的feature部分其实不算夸张,真正的显存大头是全连接层和它保存的中间激活值。尤其在训练阶段,反向传播需要保留前向计算图,batch_size稍大就爆。

解决:把batch_size降到8或16,然后用梯度累积模拟大batch。梯度累积是把一个batch的更新拆成几个小batch分别计算梯度,再统一更新一次。

accum_steps = 4 optimizer.zero_grad() for step, (images, labels) in enumerate(train_loader): images, labels = images.cuda(), labels.cuda() outputs = model(images) loss = criterion(outputs, labels) / accum_steps loss.backward() if (step + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()

注意,把loss除以accum_steps,等效于把真实batch_size放大了accum_steps倍。学习率可以保持原值,也可以适当调大。如果你把输入从224降到192,显存占用会更低,但VGG的预训练是按224设计的,不建议降太多。

5.4 准确率还行但稀有类F1很低:类别不平衡不能靠调lr解决

现象:测试集整体准确率有85%,但分类报告里某个稀有类的recall只有30%,大部分被预测成另一个常见类。整体acc很好看,实际业务却不可用。

原因:珊瑚类别天然不平衡,常见类在loss里占主导。交叉熵损失会倾向于把不确定样本分到先验概率高的类别,也就是常见类。

解决:在损失函数里按类别频率加权,让稀有类每个样本的loss贡献更大。我一般用sklearn直接计算class weight。

import numpy as np from sklearn.utils.class_weight import compute_class_weight y_train = [label for _, label in train_dataset.samples] classes = np.array(train_dataset.classes) class_weights = compute_class_weight( class_weight="balanced", classes=classes, y=y_train, ) weight_tensor = torch.tensor(class_weights, dtype=torch.float32).cuda() criterion = nn.CrossEntropyLoss(weight=weight_tensor)

如果你在第3章已经用了WeightedRandomSampler过采样,不要两处同时使用,开始可以用weighted loss,如果稀有类召回还是上不来,再叠加采样器。评估指标也要看macro F1,不要只看accuracy。

5.5 加载checkpoint报missing keys:DataParallel和num_classes不一致

现象:保存模型后换一台机器加载,报Missing key(s) in state_dict,或者size mismatch。常见指向classifier.6.weight。

原因:有两种典型情况。一是你训练时用了nn.DataParallel,保存的state_dict里所有参数名都多了module.前缀,加载到单卡模型时对不上;二是保存时num_classes是5,恢复时却用num_classes=1000构建模型,最后的全连接层尺寸当然不一致。

解决:如果是DataParallel,在构建单卡模型后手动去掉键名里的module.前缀。

from collections import OrderedDict state_dict = torch.load("best.pth", map_location="cpu")["model_state"] new_state_dict = OrderedDict() for k, v in state_dict.items(): new_state_dict[k.replace("module.", "")] = v model.load_state_dict(new_state_dict)

如果是num_classes不一致,只能重新构建正确数量的模型。所以在保存checkpoint时一定要同时保存class_to_idx,它的长度就是类别数,恢复时直接用这个长度构建模型最安全。

6. 用混淆矩阵和Top-2准确率验收珊瑚识别模型

训练完,第一件事不要只看test accuracy。珊瑚识别里类间差异极不均衡,比如杯形珊瑚和鹿角珊瑚在远处照片里可能几乎一样,一个acc数字会掩盖“到底哪两类最常被搞混”。我一般在验收脚本里加一个混淆矩阵和一个Top-2统计。

import torch import numpy as np from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() y_true, y_pred = [], [] with torch.no_grad(): for images, labels in test_loader: images = images.cuda() outputs = model(images) preds = outputs.argmax(dim=1).cpu().numpy() y_true.extend(labels.numpy()) y_pred.extend(preds) cm = confusion_matrix(y_true, y_pred) class_names = list(test_dataset.class_to_idx.keys()) sns.heatmap(cm, annot=True, fmt="d", xticklabels=class_names, yticklabels=class_names) plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150) print(classification_report(y_true, y_pred, target_names=class_names))

混淆矩阵里出现连续两格都不是对角线的高亮块时,就要警惕了。比如“Acropora_millepora”经常被预测成“Acropora_digitifera”,说明这两个species在现有特征下太相似。下一步不是马上换网络,而是把这两类的原始图片拉出来做配对目检,确认是不是标注错误、水下偏色还是真的存在中间形态。

再看Top-2准确率。水下照片常常模糊,专家遇到不确定时也会给出“可能是A或B”的结论,所以Top-2在物种识别里非常实用。

top2 = torch.topk(outputs, k=2, dim=1).indices.cpu().numpy() top2_correct = sum(true in pred for true, pred in zip(y_true, top2)) print("Top-2 Acc:", top2_correct / len(y_true))

如果Top-2准确率明显高于Top-1,说明错误集中在少数几类,优先补充这些类的训练图片,或者对它们单独做一个细分类模型,比直接换ResNet更划算。下面是单个GPU、224输入时的常见起步配置表,可以作为参考。

样本量batch_size起始lr冻结策略epochs
500张以下8-161e-4全部冻结,只训分类头30
500-5000张16-32第一阶段1e-3,微调1e-4先冻结,再解冻最后一组卷积50
5000张以上32-641e-3解冻最后两组卷积50-80

这个表不是死的。如果遇到OOM,先降batch_size并乘上梯度累积步数;如果训练震荡,把lr整体除以10。珊瑚种类之间边界模糊,我习惯在交付前做一次配对抽检,把混淆矩阵里最常搞混的两类图片放在一起看,十有八九能发现是标注问题还是自然形态重叠。这个习惯救过我好几次,也是我认为比模型结构更值得投入的地方。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 14:54:54

人体姿态识别实战:OpenPose源码训练、模型导出与摄像头部署全解析

简介:人体姿态估计是计算机视觉的核心方向之一,通过深度学习模型从图像或视频中定位人体关键点(如关节、五官),生成骨架结构以描述动作状态。其实现通常依赖卷积神经网络提取特征,并输出关键点的热图表示&a…

作者头像 李华
网站建设 2026/10/2 14:54:37

南山口碑好的客家菜聚餐服务商挑选全攻略

什么是符合本地需求的客家菜聚餐服务,先建立基础认知客家菜是广东三大菜系之一,发源于客家民系的南迁聚居历史,自带浓郁的乡土烟火气,核心特点选料讲究本土新鲜食材,口味偏重原汁原味咸鲜适口,保留了不少传…

作者头像 李华
网站建设 2026/10/2 14:52:06

TK海外抢单源码实战:PHP+uniapp前后端分离与并发控制解析

简介:TikTok海外抢单源码是一套面向跨境TikTok接单场景的完整网站源码,采用前后端分离架构:前端基于uniapp(Vue生态)可跨端编译,后端使用PHP 7.2开发,配套MySQL 5.6数据库,内置指定派…

作者头像 李华
网站建设 2026/10/2 14:52:00

强化学习稀疏奖励难题与HER事后经验回放实战解析

很多人第一次听到“hindsight”这个词,第一反应是“事后聪明”——没错,英文里就是这意思。但在强化学习领域,它对应着一个绕不开的经典方案:Hindsight Experience Replay,也就是事后经验回放。我自己第一次被它惊艳到…

作者头像 李华
网站建设 2026/10/2 14:50:32

基于1708张COCO JSON数据的驾驶接打电话识别YOLOv8实战

简介:这是一份面向智能驾驶与车载行为识别方向的图像数据集,主要用于训练模型判断驾驶员在行车过程中是否存在接打电话、玩手机等分心行为,适合计算机视觉入门者、算法工程师及交通安全相关课题研究者使用。压缩包共包含2000个文件&#xff0…

作者头像 李华
网站建设 2026/10/2 14:49:49

Harness Learning:工业AI的测试时自适应新范式

1. 项目概述:这不是“打补丁”,而是让模型在考试现场自己调参 “Harness Learning Enables Generalizable Test-Time Adaptation”——这个标题乍看像论文摘要里一句拗口的结论,但拆开来看,它描述的是一种正在改变AI落地逻辑的新范…

作者头像 李华