尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
自建图像数据集+VGGNet分类:PyTorch迁移学习完整实战教程
最近被问得最多的问题之一是我自己拍了一批图片想用VGGNet训练一个分类模型数据集每一张都是自己整理的标签也是自己手打的到底该怎么从零开始很多人不是不知道VGGNet长什么样而是卡在“流程”上图像格式怎么处理、目录怎么放、代码怎么写、训练时一堆报错怎么排查。这篇文章就是上一篇博客的细化把“自己图像数据集准备 VGGNet分类 PyTorch训练”这一整条链路逐步拆开尽量让你照着操作就能落地。我默认你手头已经有了一批图像类别也清晰至少三个类别每一类大致在100到300张左右。这个规模恰好是VGGNet迁移学习最舒服的场景既能学到数据本身的特征又不会因为数据量太少而完全训不动。读完这份教程你能拿到一套不依赖别人数据集、完全属于自己的分类训练流程从图片预处理到模型推理都给全。1. 动手前先想清楚自建数据集的难点和整体流程很多人一上来就写模型代码结果训练时才发现数据加载全乱了。这是最常见的问题不是模型难而是数据准备这一步的坑最多。自建数据集和下载现成的公开数据集不一样公开数据集目录规范、标签明确、几乎没有坏图自建数据集全得自己把关每一步都影响最终精度。1.1 为什么非要自己造数据集我见过不少同学直接用公开数据集因为省事。但放到真实场景中就会发现公开数据和实际业务图分布差异太大。比如训练集里都是规规矩矩的正面图你实际要分类的图片却是侧面、光照不均、带遮挡模型精度会断崖式下降。自建数据集就是为了让模型匹配真实的使用环境这比“找一个近似数据集”靠谱得多。还有一点很多人不提自己造数据集能让你真正理解“数据决定上限”。VGGNet结构是固定的参数规模也明确换数据集后精度好不好更多取决于你的图片质量、数量、标注一致性和预处理方式。模型本身只是把数据的规律学出来数据一旦乱掉后面的每一步都是白费。自建数据集的痛点也很明确主要集中在三个地方类别不均衡拍得多的类别有300张拍得少的只有50张模型会偏向多数类样本相似度过高很多图片其实是同一场景的连拍信息量重复相当于有效样本没那么多误标和坏图肉眼不细看很难发现但训练时这些图会在损失函数里制造噪声。1.2 整体流程总览我先给你一个完整的主线心里有数之后再动手。整条链路分为6个阶段阶段核心任务主要产出1. 图像采集与清洗拍摄/收集图片去重、删坏图干净的数据文件夹2. 目录划分按train/val/test组织数据标准ImageNet风格目录3. 数据增强与预处理用transforms做尺寸统一、增强、归一化DataLoader可用的数据流4. 模型搭建实现或加载VGG16模型可训练的模型实例5. 训练与验证执行训练循环监控精度和损失训练日志、损失曲线6. 评估与推理在测试集上评估出混淆矩阵写推理脚本最终模型和预测结果后续每个章节都围绕这6个环节展开重点放在“每一步选什么、为什么这样选”上。你不需要完全理解全部原理也能跑通但如果你能把每个决策背后的原因搞明白以后再换数据、换模型时会顺畅得多。2. 自己图像数据集的准备与预处理关于数据准备我没法替你拍照但可以给你一套可复用的方法确保你拍的图不会在训练阶段坑你。2.1 图像采集与命名规范先定类别。假设你做花卉分类三个类别是玫瑰、菊花、蒲公英。每个类别至少准备150到200张类别之间数量不要差太多。少于100张训练时要么严重过拟合要么迁移学习都没法发挥如果真只有几十张建议上更小的模型或者做更强的数据增强而不是直接硬顶VGG16。命名建议统一格式类别名_序号.jpg例如rose_001.jpg rose_002.jpg chrysanthemum_001.jpg dandelion_001.jpg为什么要规范命名因为你后面写Dataset类时需要靠文件名或者文件夹名去解析标签。命名越规整代码越简单。如果你的图片本来就是按文件夹分类存放的那就更方便了直接从文件夹名里拿标签即可。拍摄图形时再强调一个要点别把同一个物体拍几十张连拍放进去。真实业务中数据就应该有场景变化但在起步阶段尽量减少这种“貌似多、实则同”的图片否则模型学不到泛化特征。2.2 目录结构设计train/val/test我强烈建议一开始就按ImageNet风格把目录准备好data/ train/ rose/xxx.jpg chrysanthemum/xxx.jpg dandelion/xxx.jpg val/ rose/xxx.jpg chrysanthemum/xxx.jpg dandelion/xxx.jpg test/ rose/xxx.jpg chrysanthemum/xxx.jpg dandelion/xxx.jpgtrain用于训练val用于调整超参和选择模型test用于最终评估。很多人只分train和test结果在测试集上调参调过头最后汇报的精度虚高。合理的比例是train 70%、val 15%、test 15%。数据量不大时可以8:1:1。写一个独立脚本完成划分不要手动拖文件夹import os import random import shutil source_root data/all_images # 所有原始图片按类别分子文件夹 target_root data train_ratio, val_ratio, test_ratio 0.8, 0.1, 0.1 for cls_name in os.listdir(source_root): cls_path os.path.join(source_root, cls_name) if not os.path.isdir(cls_path): continue imgs os.listdir(cls_path) random.shuffle(imgs) n_train int(len(imgs) * train_ratio) n_val int(len(imgs) * val_ratio) splits { train: imgs[:n_train], val: imgs[n_train:n_train n_val], test: imgs[n_train n_val:] } for split_name, split_imgs in splits.items(): out_dir os.path.join(target_root, split_name, cls_name) os.makedirs(out_dir, exist_okTrue) for img_name in split_imgs: shutil.copy( os.path.join(cls_path, img_name), os.path.join(out_dir, img_name) )这里用随机划分即可。建议设置随机种子保证复现random.seed(42)2.3 数据清洗与去重训练集里最怕两类图一类是模糊到人眼都认不出的图一类是内容高度重复的图。前者模型学不到有效特征后者相当于变相降低样本量。第一步肉眼把严重模糊、过曝、误标的图删掉这活儿不能省。第二步用感知哈希去做简单去重。感知哈希的原理是把图片缩小成固定尺寸计算灰度图的哈希值相近图片的哈希值差异很小。用一个简单实现就能找出相似图import os from PIL import Image import imagehash def dhash(image, hash_size8): image image.convert(L).resize((hash_size 1, hash_size), Image.LANCZOS) diff [] for row in range(hash_size): for col in range(hash_size): left image.getpixel((col, row)) right image.getpixel((col 1, row)) diff.append(0 if left right else 1) return sum(v i for i, v in enumerate(diff)) # 素哈希去重 # 两两对比汉明距离小于5就视为重复图你不需要完全懂感知哈希的数学细节理解它是“相似的图算出的哈希值接近”就够了。如果你不想写代码也可以用图像管理软件的重复图扫描功能效果类似。清洗完之后再进目录划分步骤不然划分完再删两边数量就不一致了。2.4 数据增强PyTorch transforms 的实际搭配数据增强的核心思想是通过随机变化制造“视觉差异”让模型看到同一张图片的多种形态。图像数据比文本更好做增强因为旋转、翻转、调色不会改变主体类别。训练集和测试集要用不同预处理这是新手最容易忽略的。训练集可以加随机扰动测试集要保持“稳定”否则输出结果会因随机性而抖动。我常用的组合如下from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), 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_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]) ])这里的思路是先把图resize到256再随机裁剪224相当于让模型每次看到不同位置的局部信息。Resize到256再Crop到224是VGG时代比较经典的做法能保留一定随机裁剪空间。如果你不想那么麻烦直接Resize到224也可以效果会略糙一点。关于Normalize标准化参数直接用ImageNet均值标准差因为迁移学习用到的预训练权重是在ImageNet上得到的数据分布对齐有利于权重微调。如果你是完全从头训练自己的模型可以用自己数据集的mean和std但迁移学习场景下用ImageNet原值基本没错。3. VGGNet 核心结构拆解与实践选型3.1 为什么VGGNet至今仍有参考价值VGGNet在2014年提出的核心理念是“全用3x3小卷积核”把卷积层叠深。两个3x3卷积堆叠起来感受野大小和一个5x5卷积一样但参数更少而且中间多了一次非线性激活表达能力更强。这个设计被后来很多模型沿用理解它等于理解了卷积神经网络的基础搭建逻辑。举一个直观对比一个5x5卷积的参数是25而两个3x3卷积的参数是18省了接近三成若换成三个3x3卷积则等价于一个7x7卷积参数对比是27对49。叠加小卷积核既加深网络又控制参数量这是VGGNet最有教学价值的地方。VGGNet也有明显的缺点全连接层参数特别大早期VGG16的参数量接近1.4亿其中很大一部分来自最后三个全连接层。所以在实际项目里我通常只取它的卷积特征提取部分分类头换成适配自己类别数的小网络。3.2 从零搭一个VGG16模型代码先给出一个精简但完整的VGG16实现。核心是把卷积层按stage组织每个stage由若干3x3卷积和ReLU构成stage后面接最大池化。import torch.nn as nn class VGG16(nn.Module): def __init__(self, num_classes3, init_weightsTrue): super().__init__() self.features nn.Sequential( # stage 1 nn.Conv2d(3, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # stage 2 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # stage 3 nn.Conv2d(128, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # stage 4 nn.Conv2d(256, 512, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(512, 512, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(512, 512, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # stage 5 nn.Conv2d(512, 512, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(512, 512, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(512, 512, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) self.classifier nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x输入图像尺寸必须是224x224因为经过五次池化后空间尺寸除以32224 / 32 7所以全连接层输入维度是512 * 7 * 7。这个数字不是随便写的推导一下就能明白。如果你用的是更大输入尺寸比如384那最后得到的特征图就不是7x7需要相应调整全连接层的输入维度。这也是为什么很多人直接用前面预处理resize到224省掉这个推导麻烦。3.3 迁移学习用官方预训练权重还是自己训自建数据集通常几百到几千张从头训练VGG16很容易欠拟合或过拟合。更稳的做法是使用在ImageNet上预训练的权重然后只微调参数。PyTorch的torchvision中可以直接加载官方预训练权重import torchvision.models as models model models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) model.classifier[6] nn.Linear(4096, 3) # 替换最后一层注意新版PyTorch中旧写法pretrainedTrue已被weights参数取代。加载预训练模型后最后分类头的输出维度要改成自己的类别数比如3类就改成nn.Linear(4096, 3)。数据量不同训练策略也不同数据量小于200张每类冻结features层只训练新加的分类头数据量在200到1000张每类可以微调部分靠后的卷积层设置较低学习率数据量很大且和ImageNet内容差异极大可以解冻整个模型全量微调。冻结层的方法是设置param.requires_grad False只让分类头参数更新。你如果把整个VGG16全量训练显存占用和训练时间都会明显上涨有时反而会因数据不足导致特征被破坏。4. 训练全流程从数据加载到训练循环4.1 自定义Dataset的正确打开方式PyTorch的ImageFolder能直接读取标准目录结构但为了让你理解背后的机制我写一个自定义Dataset版本。这样换一个非标准数据集也能照葫芦画瓢。import os from PIL import Image from torch.utils.data import Dataset class ImageClassifierDataset(Dataset): def __init__(self, root_dir, transformNone): self.root_dir root_dir self.transform transform self.classes sorted(os.listdir(root_dir)) self.class_to_idx {cls: i for i, cls in enumerate(self.classes)} self.samples [] for cls_name in self.classes: cls_dir os.path.join(root_dir, cls_name) for img_name in os.listdir(cls_dir): if img_name.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(cls_dir, img_name), self.class_to_idx[cls_name])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] image Image.open(path).convert(RGB) if self.transform: image self.transform(image) return image, label很多人问为什么__getitem__要单独读图片而不是在初始化时把所有图片一次性读入内存。原因是数据集可能上千张全塞进内存不仅慢还容易爆内存。PyTorch的DataLoader是多进程并发读取每拿一个batch就自动从磁盘读并做transform内存占用小很多。如果你的图片格式五花八门统一用convert(RGB)转成三通道避免灰度图或RGBA图带来的通道数不匹配错误。4.2 DataLoader参数与优化生成训练和验证的DataLoaderfrom torch.utils.data import DataLoader train_dataset ImageClassifierDataset(data/train, transformtrain_transform) val_dataset ImageClassifierDataset(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)关键参数逐一说明batch_size32显卡显存不够就降到16或8越大梯度越平滑训练越稳shuffleTrue训练集打乱顺序避免模型按批次顺序学到多余规律验证集不需要打乱num_workers4数据读取的进程数Windows上如果报错或卡住设成0最保险pin_memoryTrue加快GPU训练时的数据传输CPU训练可以关掉。4.3 损失函数、优化器与学习率调度分类任务默认使用交叉熵损失criterion nn.CrossEntropyLoss()优化器常用SGD带动量而不是直接上Adam。VGGNet这种大规模网络在分类任务上SGD加动量的收敛结果往往更稳定optimizer torch.optim.SGD( model.parameters(), lr0.001, momentum0.9, weight_decay5e-4 )lr0.001是迁移学习常见的起始值如果你冻结了features层可以调大到0.005因为可训练参数少不容易剧烈震荡。weight_decay5e-4是L2正则防止过拟合太小没效果太大会把模型压变形。学习率调度我用验证损失自动调整scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 )当验证损失连续3个epoch不降时学习率减半。这比固定学习率跑到底更高效也比CosineAnnealing在同数据量下更省心。4.4 训练循环与验证循环的模板代码训练循环是整个流程的心脏。一个标准的epoch包含训练阶段和验证阶段def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in 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() * images.size(0) _, predicted torch.max(outputs, dim1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total torch.no_grad() def validate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, dim1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total注意训练阶段和验证阶段的开头分别调用了model.train()和model.eval()。很多人忘记eval()导致Dropout仍在随机丢弃、BatchNorm仍在用当前batch统计量验证结果起伏不定。这两个模式切换虽然代码只有一行但影响非常大。主循环代码可以这样写import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) epochs 30 best_val_acc 0.0 for epoch in range(epochs): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) val_loss, val_acc validate(model, val_loader, criterion, device) scheduler.step(val_loss) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1}/{epochs} | ftrain loss {train_loss:.4f} acc {train_acc:.4f} | fval loss {val_loss:.4f} acc {val_acc:.4f})打印内容最好包含训练损失、训练精度、验证损失、验证精度信息越全越容易定位问题。保存模型时只保存state_dict比保存整个模型对象更轻量加载时也更可控。5. 训练中的坑与排查经验这一部分不是理论是我实际跑项目时踩过的真实情况。对照着排查能省下大量时间。5.1 一张常见问题速查表现象可能原因解决方案损失完全不下降训练精度异常低学习率太大或太小标签and图像不对齐模型初始化出问题先用小学习率试跑打印一个batch的图片和标签确认正确训练精度很高验证精度很低过拟合Dropout不生效数据增强太弱增加Dropout概率调高weight_decay加数据增强强度验证精度反复横跳忘了model.eval()验证集太少学习率波动大检查是否有eval()增大验证集降低学习率或使用调度器报错“shape mismatch”全连接层输入维度不对类别数改错按224输入对应51277检查重新替换classifier最后一层GPU显存不足batch_size过大输入图像尺寸过大降低batch_size缩小输入尺寸开启梯度累积图片读取报错或返回空 tensor图像损坏数据路径不对不支持的文件格式清洗坏图在Dataset中加异常捕获检查文件扩展名针对图片损坏稳妥的做法是在Dataset中捕获异常并跳过def __getitem__(self, idx): while True: path, label self.samples[idx] try: image Image.open(path).convert(RGB) break except Exception as e: print(fBad image: {path} - {e}) idx (idx 1) % len(self.samples) if self.transform: image self.transform(image) return image, label5.2 经验补充类别不均衡、batch size选择与早停类别不均衡是自建数据集最常见的问题。如果一类有300张另一类只有80张模型会倾向于预测样本量多的类。你有两个办法一是采样调整使用WeightedRandomSampler给少样本更高的采样权重二是做离线增强把少样本类进行随机裁剪、旋转、颜色扰动生成额外图片扩充数量。我更推荐第二种因为离线增强还能增加数据多样性。batch size的选择直接影响训练稳定性。如果batch size太小梯度会非常嘈杂模型损失可能像锯齿一样上下抖动如果太大一个batch就要占用很多显存。我的经验是8GB显存用16到32作为起点如果显存不够优先减小输入尺寸而不是一味调小batch size因为224x224模型对显存的消耗大头是卷积的中间特征图减小输入尺寸效果立竿见影。早停是防止过拟合的最后一道保险。最简单做法是记录验证精度如果连续5个epoch不再提升就停止训练并回滚到最佳模型。可以用一个简单的计数器实现bad_epochs 0 patience 5 for epoch in range(epochs): # ... 训练和验证 ... if val_acc best_val_acc: bad_epochs 1 if bad_epochs patience: print(fEarly stop at epoch {epoch1}) break else: bad_epochs 0 best_val_acc val_acc记录最佳模型建议在验证精度提升时立刻保存不要等到epoch结束再统一保存请回看前面的训练循环代码保存动作已经放在每个epoch验证之后这样崩了也能找到历史最佳。6. 模型评估、保存与单张推理6.1 用混淆矩阵看真实效果训练结束后只凭总体准确率还不足以说明模型好不好。准确率可能会被多数类掩盖问题。比如100张图里80张属于玫瑰你全部预测成玫瑰也能获得80%的准确率可模型其实什么都没学会。混淆矩阵是更直观的评价工具。用sklearn能快速计算from sklearn.metrics import confusion_matrix import numpy as np all_labels [] all_preds [] model.eval() with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, predicted torch.max(outputs, dim1) all_labels.extend(labels.cpu().numpy()) all_preds.extend(predicted.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)矩阵中的每一行代表真实类别每一列代表预测类别。对角线越大越好如果某行有大量非对角线值说明该类别易被混淆到其他类。你可以针对性查看哪些图像被分错再回到数据集看是标注问题还是图像本身太相似。对于小数据集一个很有价值的小操作是把分类错误的图片打印出来from PIL import Image test_dataset ImageClassifierDataset(data/test, transformval_transform) for i in range(len(test_dataset)): if all_labels[i] ! all_preds[i]: path, _ test_dataset.samples[i] print(fTrue: {test_dataset.classes[all_labels[i]]} | fPred: {test_dataset.classes[all_preds[i]]} | {path})这个打印能快速发现错误规律比如某类照片经常带特定背景模型学到的是背景而非主体。6.2 模型保存与加载保存时只保存权重torch.save(model.state_dict(), best_model.pth)加载时先创建对应结构的模型再载入权重model models.vgg16(weightsNone) model.classifier[6] nn.Linear(4096, 3) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.to(device) model.eval()很多人会问我只保存了权重那类别顺序信息在哪里答案是类别顺序需要你自己定义。我的建议是把类别列表单独存成一份JSON或txtimport json class_names sorted(os.listdir(data/train)) with open(class_names.json, w, encodingutf-8) as f: json.dump(class_names, f, ensure_asciiFalse)推理时读取这个文件保证预测的索引能映射回类别名称。6.3 单张图像预测细节预测脚本读取一张图片走一遍与训练时一致的transform然后输出每个类别的概率或最大概率类别。每个类别索引与名称的映射关系必须和训练时一致我建议在数据集构建时就用class_to_idx保存一份映射。推理时直接加载这个映射避免从文件夹名重新排序造成错位。类别映射的顺序不统一是很多项目部署阶段才暴露出来的大坑。推理脚本示例from PIL import Image import torch import torchvision.transforms as transforms device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载模型 model models.vgg16(weightsNone) model.classifier[6] nn.Linear(4096, 3) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model model.to(device) model.eval() # 加载类别映射 with open(class_names.json, r, encodingutf-8) as f: class_names json.load(f) # 处理图片 image Image.open(test.jpg).convert(RGB) image val_transform(image).unsqueeze(0).to(device) with torch.no_grad(): output model(image) probs torch.softmax(output, dim1)[0] conf, idx torch.max(probs, dim0) print(fPredicted class: {class_names[idx.item()]}, confidence: {conf.item():.4f})最后一步的softmax面对多分类任务作用是把logits映射成和为1的概率直观看出模型对结果的“确信度”。有时准确率不变但置信度极低这本身也是数据质量问题的信号。7. 最后一点私货复现时最值得花的功夫训练一个VGGNet分类自己数据集技术层面的流程就是前面这些但你真去跑一遍就会发现最花时间的往往不是训练而是数据清洗和调试。我在第一次做自建数据集时花了两天整理图片训练只花了二十分钟。当时觉得浪费时间后来才明白这是项目里最值得投入的部分。另一个值得花功夫的地方是把实验记录做好。类别怎么划分的、数据增强用了哪些参数、学习率调到过多少、每次改了哪一行代码都记下来。否则你今天跑出88%的精度过两天忘了超参又要重新试错。如果你想把VGGNet当做一个起点后续可以往两个方向扩展一是换更强模型比如ResNet、EfficientNet它们比VGGNet体积更小、效果更好二是在数据增强上加深使用更现代的策略。但不管怎么改数据准备、训练循环、评估流程这些骨架都是通用的。我只补充一点刚开始跑的时候不要一上来就追求99%的精度。先保证流程跑通模型能保存、能加载、能推理再去调精度。流程跑通的成就感足够支撑你继续优化模型和数据了。
RELATED

相关推荐

QT5+OpenCV摄像头采集显示:从VideoCapture到QImage的完整实现

QT5+OpenCV摄像头采集显示:从VideoCapture到QImage的完整实现

简介:面向正在学习Qt界面开发与OpenCV图像处理的开发者,基于QT5的摄像头视频采集显示示例解决了从打开摄像头到画面实时上屏的核心流程。资源为完整可编译的Qt Widgets工程,共9个文件、压缩包仅21KB,含main.cpp主入口、mainwindow…

📅 2026/10/19 5:25:42
智能座舱开发实战:域控制器架构、多模交互与场景引擎设计

智能座舱开发实战:域控制器架构、多模交互与场景引擎设计

1. 从一块屏幕说起:智能座舱到底在解决什么问题很多人第一次听到"智能座舱"这个词,脑子里浮现的画面大概是中控台上那块越来越大的屏幕,或者语音助手帮你调个空调温度。但如果只停留在这个层面,就完全低估了这个领域正在…

📅 2026/10/19 5:25:42
用Nova Act实现浏览器自动化:从快速原型到生产部署的实战指南

用Nova Act实现浏览器自动化:从快速原型到生产部署的实战指南

半夜十二点,一个做电商运营的朋友突然发消息问我:能不能把供应商后台的每日库存、价格变动自动盯一遍,有异常就截图发群里。我第一反应是写个爬虫,但对面马上补了一句:那后台登录要短信验证,页面还是各种动…

📅 2026/10/19 5:25:42
MORE NEWS

更多资讯

📰

物联网设备WiFi配网实战:ESP32/ESP8266 SoftAP与SmartConfig详解

上个月帮朋友调一块 ESP32 开发板,设备本身跑得好好的,最后却卡在“怎么让设备连上他家的 WiFi”这一步。我说你直接把 SSID 和密码写在代码里不就行了?他说你自己调试当然没问题,可这个设备是要交给一个完全不会用串口、不会看代…

📰

artcraft:基于WebGL的轻量级创意合成工具实战

1. 项目缘起与整体设计思路1.1 为什么会有 artcraft 这个想法做创意工具这些年,我最大的感受就是:灵感来得快,工具跟不上。脑子里冒出一个画面,想快速拼贴、调色、加纹理,结果打开专业软件要等半分钟加载,图…

📰

VS2017 64位下OSGQT整合实战:从编译到渲染的避坑指南

简介:这是一份面向Qt与OpenSceneGraph(OSG)三维渲染开发者的整合示例工程,基于Github上口碑较好的OSGtree项目整理而来,已在VS2017搭配64位OSG环境下完成调试,可直接运行,适合正在学习OSG与Qt混…

📰

曼哈顿距离在机器人路径规划中的原理与实战详解

前两天在调我们一台小型自动清扫机器人的任务调度时,又看到有同事对着坐标嘀咕:机器人停在(3,5),任务点在(7,2),两点间直线距离明明是5,为什么程序最后算出的是7?这个问题背后,正是曼哈顿距离&a…

📰

Vite打包常见报错与解决方案:从内存溢出到体积优化全记录

Vite用久了你会发现一个规律:开发环境(dev server)跑得飞起,一旦执行npm run build,各种奇奇怪怪的问题就冒出来了。不是内存溢出,就是资源路径不对,再不然就是打包体积大得离谱,警告…

📰

diagram-design:从随手画图到系统性图表设计,告别技术债

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题很多人第一次听到 diagram-design 这个词,会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道,画图本身从来不是难点,难点在于图表的可维护…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬