
简介本资源面向图像分类初学者与迁移学习实践者提供一套机械图纸三视图abcd四分类的完整可运行方案。主干网络支持resnet、densenet、googleNet三种模型通过pretrained与freeze_layers参数即可灵活切换是否加载ImageNet预训练权重或仅训练分类输出层优化器内置Adam与SGD用于对比消融实验损失函数采用多类别交叉熵学习率策略为余弦退火。评估环节覆盖训练集与验证集的loss、准确率曲线并输出混淆矩阵、recall、precision、F1 score及特异度等指标各类别详细指标存于json文件。资源包共121个文件含61个png、36个jpg图像样本与结果图、6个py源码、5个json指标文件、3个pth权重及若干txt说明压缩包约94.45MB数据集与标签齐备按参考猫狗数据集摆放即可一键训练。已有64人学习适合快速复现与二次改进。1. 机械图纸三视图分类从 ResNet 到自适应迁移学习的落地路径机械图纸的三视图识别说白了就是把一张包含主视图、俯视图、左视图的工程图自动分到 ABCD 四个类别里。这件事在车间数字化、图纸归档、PLM 系统入库环节是刚需——人工分拣一天几百张图眼睛看花还容易串号。但直接拿 ResNet 从头训准确率卡在 70% 上下死活上不去原因是工业图纸样本太少一个类别能凑出三百张就算家底厚实。这时候迁移学习就是那根救命稻草用 ImageNet 预训练模型当起点再针对三视图的线条、标注、剖视特征做自适应微调。ResNet、DenseNet、GoogleNet 三个骨干网各有脾气选哪个、怎么冻、怎么解冻、学习率怎么设直接决定你是三天收工还是三周翻车。这篇把我自己跑过的完整方案拆开从数据准备到模型融合每一步都给可复现的命令和参数。2. 三个骨干网在三视图上的选型逻辑与最小跑通方案2.1 ResNet、DenseNet、GoogleNet 的特征提取差异ResNet 靠残差连接把梯度直接传到浅层在机械图纸上表现最稳。三视图的线条是细粒度特征ResNet 的 bottleneck 结构在 stage3、stage4 能抓到尺寸标注和剖面线的组合模式。我一般用 ResNet50 起步它的 7x7 卷积核加 3x3 堆叠对图纸里粗细不均的轮廓线响应很好。但要注意ImageNet 预训练的 ResNet 第一层是 7x7 stride 2对 224x224 输入刚好如果你的图纸扫描件是 2000x3000 像素直接 resize 会丢标注文字得先切图再送网络。DenseNet 的特点是每一层都跟前面所有层连特征复用率极高。在样本量少于 500 张每类时DenseNet121 比 ResNet50 的验证集准确率高 3 到 5 个百分点。原因是三视图里主视图和俯视图的对应关系——比如同一个孔在主视图是圆、在俯视图是虚线——DenseNet 的密集连接能把这些跨视图的弱相关特征反复传递。但代价是显存占用大batch size 只能开到 ResNet 的一半。GoogleNetInception v3用多尺度卷积核并行1x1、3x3、5x5 同时扫。机械图纸里既有大面积的剖面线区域又有细小的尺寸数字Inception 模块天然适合这种尺度差异。但 GoogleNet 的辅助分类器在微调时容易干扰主损失我一般把 aux_logits 关掉再训。提示三个骨干网不要同时训先跑 ResNet50 拿到 baseline再换 DenseNet121 对比最后用 GoogleNet 做融合。单卡 8G 显存足够跑 ResNet50 和 GoogleNetDenseNet121 建议 12G 以上。2.2 用 torchvision 加载预训练模型并替换分类头先装环境PyTorch 2.x 加 torchvisionCUDA 版本按自己显卡选。数据目录按 ImageFolder 格式组织train/abcd 四个子文件夹val 同样。下面是最小跑通代码直接抄。import torch import torch.nn as nn import torchvision.models as models from torchvision import transforms, datasets from torch.utils.data import DataLoader # 数据增强三视图需要保留线条不要用 ColorJitter 和 RandomRotation train_tf transforms.Compose([ transforms.Resize((256, 256)), # 先放大再裁剪保留标注 transforms.RandomCrop(224), # 随机裁剪模拟图纸偏移 transforms.RandomHorizontalFlip(), # 三视图左右翻转不改变类别语义 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) def build_model(archresnet50, num_classes4): if arch resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) model.fc nn.Linear(model.fc.in_features, num_classes) elif arch densenet121: model models.densenet121(weightsmodels.DenseNet121_Weights.IMAGENET1K_V1) model.classifier nn.Linear(model.classifier.in_features, num_classes) elif arch inception_v3: model models.inception_v3(weightsmodels.Inception_V3_Weights.IMAGENET1K_V1, aux_logitsFalse) model.fc nn.Linear(model.fc.in_features, num_classes) return model model build_model(resnet50).cuda()逻辑说明Resize 到 256 再 RandomCrop 224比直接 Resize 224 多保留 14% 的像素信息对细线条友好。RandomHorizontalFlip 对三视图安全因为左右翻转不改变“这是主视图还是俯视图”的类别定义。Normalize 用 ImageNet 统计量因为预训练权重就是在这个分布上学的。参数说明batch_size 32 是 8G 显存的保守值ResNet50 可以开到 64DenseNet121 降到 16。num_workers 设 4 是经验值Windows 下如果报错就改 0。weights 参数用新版枚举老代码的 pretrainedTrue 在 torchvision 0.13 之后会警告。2.3 冻结与解冻自适应迁移学习的核心操作自适应迁移学习的关键不是“冻不冻”而是“什么时候解冻、解冻几层”。我的做法分三段第一段只训分类头冻结所有卷积层学习率 1e-3跑 10 个 epoch第二段解冻 layer4ResNet或 denseblock4DenseNet学习率降到 1e-4跑 20 个 epoch第三段全部解冻学习率 1e-5跑 10 个 epoch。这样做的原因是分类头随机初始化一开始梯度大如果直接全网络微调会把预训练权重冲垮。def set_freeze(model, arch, stage): # stage 1: 只训分类头 # stage 2: 解冻最后一块 # stage 3: 全部解冻 if arch resnet50: blocks [model.layer1, model.layer2, model.layer3, model.layer4] elif arch densenet121: blocks [model.features.denseblock1, model.features.denseblock2, model.features.denseblock3, model.features.denseblock4] else: blocks [model.Mixed_5b, model.Mixed_6a, model.Mixed_7a] for p in model.parameters(): p.requires_grad False # 分类头始终可训 if arch resnet50: for p in model.fc.parameters(): p.requires_grad True elif arch densenet121: for p in model.classifier.parameters(): p.requires_grad True else: for p in model.fc.parameters(): p.requires_grad True if stage 2: for p in blocks[-1].parameters(): p.requires_grad True if stage 3: for blk in blocks: for p in blk.parameters(): p.requires_grad True # 训练循环骨架 optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss(label_smoothing0.1)逻辑说明filter(lambda p: p.requires_grad, ...) 保证优化器只更新解冻的参数。label_smoothing 0.1 对工业图纸有用因为有些图纸介于 A 和 B 之间硬标签会过拟合。AdamW 的 weight_decay 比 Adam 更稳1e-4 是微调场景的常用值。参数说明stage1 学习率 1e-3stage2 换 1e-4stage3 换 1e-5。每个 stage 结束时保存验证集准确率最高的权重。如果 stage2 验证集准确率下降超过 2%说明解冻太早退回 stage1 多跑 5 个 epoch。3. 三视图数据增强与类别不平衡的工程处理3.1 针对机械图纸的增强策略通用图像增强在机械图纸上有一半不能用。ColorJitter 会改变线条对比度导致细线消失RandomRotation 会把水平标注转成斜的OCR 特征全乱GaussianBlur 直接糊掉尺寸数字。能用的只有随机裁剪、水平翻转、轻微透视变换、以及 Cutout。Cutout 在图纸上效果意外地好因为遮挡一部分区域强迫网络看其他视图的线索。from torchvision.transforms import RandomApply import torchvision.transforms as T train_tf_advanced T.Compose([ T.Resize((256, 256)), T.RandomCrop(224), T.RandomHorizontalFlip(p0.5), T.RandomApply([T.RandomPerspective(distortion_scale0.1, p0.3)], p0.3), T.ToTensor(), T.RandomErasing(p0.25, scale(0.02, 0.1), ratio(0.3, 3.3)), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])逻辑说明RandomPerspective 的 distortion_scale 只给 0.1模拟扫描时的轻微倾斜。RandomErasing 就是 Cutout 的 torchvision 实现scale 0.02 到 0.1 是擦除小方块不会盖住整个视图。p0.25 表示 25% 的概率执行。参数说明如果验证集准确率波动大于 3%把 RandomErasing 的 p 降到 0.1。RandomPerspective 的 p 不要超过 0.3否则图纸变形太厉害。3.2 类别不平衡的加权采样与损失函数ABCD 四类图纸数量往往不均A 类可能 500 张D 类只有 80 张。直接训会让网络偏向多数类。两种做法WeightedRandomSampler 过采样少数类或者 Focal Loss 降权易分样本。我一般两个一起用采样器保证每个 batch 里四类都有Focal Loss 让网络关注难分的 D 类。from torch.utils.data import WeightedRandomSampler import numpy as np targets [s[1] for s in train_ds.samples] class_count np.bincount(targets) class_weights 1.0 / class_count sample_weights [class_weights[t] for t in targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4) # Focal Loss class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2): super().__init__() self.alpha alpha self.gamma gamma def forward(self, inputs, targets): ce nn.functional.cross_entropy(inputs, targets, reductionnone) pt torch.exp(-ce) return (self.alpha * (1 - pt) ** self.gamma * ce).mean()逻辑说明class_weights 取倒数样本越少权重越大。WeightedRandomSampler 的 replacementTrue 允许重复采样保证每个 epoch 看到的样本数跟原数据集一致。Focal Loss 的 gamma2 是原论文推荐值alpha1 表示不额外调类别权重因为采样器已经处理了。参数说明如果 D 类验证集召回率低于 60%把 gamma 提到 3。如果训练 loss 震荡把 alpha 降到 0.5。4. 训练、验证与模型融合的完整命令流4.1 分阶段训练脚本与日志监控把前面的模块拼起来写一个 train.py用 argparse 传参。每个 stage 结束打印混淆矩阵不要只看准确率。机械图纸的 A 类和 B 类容易混因为都是主视图加剖视区别在标注符号。混淆矩阵能看出到底哪两类在互相误判。import argparse, torch, numpy as np from sklearn.metrics import confusion_matrix, classification_report def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (out.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, device): model.eval() preds, gts [], [] for imgs, labels in loader: imgs imgs.to(device) out model(imgs) preds.extend(out.argmax(1).cpu().numpy()) gts.extend(labels.numpy()) return np.array(gts), np.array(preds) # 主流程 parser argparse.ArgumentParser() parser.add_argument(--arch, defaultresnet50) parser.add_argument(--stage, typeint, default1) parser.add_argument(--epochs, typeint, default10) parser.add_argument(--lr, typefloat, default1e-3) args parser.parse_args() device cuda if torch.cuda.is_available() else cpu model build_model(args.arch).to(device) set_freeze(model, args.arch, args.stage) optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lrargs.lr, weight_decay1e-4) criterion FocalLoss(gamma2) for epoch in range(args.epochs): tr_loss, tr_acc train_one_epoch(model, train_loader, optimizer, criterion, device) gts, preds evaluate(model, val_loader, device) val_acc (gts preds).mean() print(fstage{args.stage} epoch{epoch} loss{tr_loss:.4f} train{tr_acc:.4f} val{val_acc:.4f}) if epoch % 5 0: print(confusion_matrix(gts, preds)) print(classification_report(gts, preds, target_names[A,B,C,D]))逻辑说明train_one_epoch 返回平均 loss 和训练准确率evaluate 收集所有预测和标签算验证准确率。每 5 个 epoch 打印混淆矩阵和分类报告方便定位问题类别。参数说明stage1 用 lr1e-3 epochs10stage2 用 lr1e-4 epochs20stage3 用 lr1e-5 epochs10。命令行依次跑python train.py --arch resnet50 --stage 1 --epochs 10 --lr 1e-3 python train.py --arch resnet50 --stage 2 --epochs 20 --lr 1e-4 python train.py --arch resnet50 --stage 3 --epochs 10 --lr 1e-54.2 三个骨干网的预测概率融合单模型准确率到 88% 左右就上不去了融合能拉到 92% 以上。做法很简单三个模型分别对验证集输出 softmax 概率然后加权平均。权重按各自验证集准确率分配ResNet50 给 0.4DenseNet121 给 0.35GoogleNet 给 0.25。def ensemble_predict(models, loader, device, weights): all_probs [] for model in models: model.eval() probs [] with torch.no_grad(): for imgs, _ in loader: imgs imgs.to(device) out torch.softmax(model(imgs), dim1) probs.append(out.cpu()) all_probs.append(torch.cat(probs, dim0)) # 加权平均 final sum(w * p for w, p in zip(weights, all_probs)) return final.argmax(1) resnet build_model(resnet50).cuda() densenet build_model(densenet121).cuda() googlenet build_model(inception_v3).cuda() resnet.load_state_dict(torch.load(resnet_stage3_best.pth)) densenet.load_state_dict(torch.load(densenet_stage3_best.pth)) googlenet.load_state_dict(torch.load(googlenet_stage3_best.pth)) preds ensemble_predict([resnet, densenet, googlenet], val_loader, cuda, [0.4, 0.35, 0.25])逻辑说明每个模型先算 softmax 概率再按权重加权。权重不是拍脑袋是拿验证集准确率归一化得到的。如果某个模型准确率明显低权重降到 0.1 以下或者直接踢掉。参数说明融合前确保三个模型用的是同一套验证集增强val_tf否则概率分布不可比。如果显存不够同时加载三个模型可以分两次跑把概率存成 npy 文件再融合。5. 避坑与排查三视图分类翻车实录5.1 验证集准确率比训练集高 5 个点现象训练 loss 降到 0.2训练准确率 95%验证准确率 97%。原因验证集用了 CenterCrop训练集用 RandomCrop验证集图像更“标准”网络在验证集上反而好分。解决把验证集的 Resize 和 Crop 改成跟训练集一致的 RandomCrop但推理时用 CenterCrop。或者干脆把验证集也做轻微增强取多次预测的平均。5.2 DenseNet121 训到第 3 个 epoch 显存爆了现象batch_size 设 32跑到第 3 个 epoch 报 CUDA out of memory。原因DenseNet 的密集连接导致中间激活值累积PyTorch 默认不释放。解决batch_size 降到 16加 torch.cuda.empty_cache()或者用 gradient checkpointing。torchvision 的 DenseNet 不支持原生 checkpointing得手动包。5.3 GoogleNet 的 aux_logits 导致 loss 不下降现象Inception v3 训练 loss 在 1.2 附近震荡验证准确率随机水平。原因aux_logitsTrue 时辅助分类器的 loss 跟主 loss 相加但辅助分类器在微调时梯度方向跟主任务不一致。解决build_model 里设 aux_logitsFalse或者训练时只取主输出 out[0]。5.4 混淆矩阵显示 A 类和 D 类互相误判现象A 类召回率 95%D 类召回率 40%D 类样本大量被分到 A。原因D 类样本太少WeightedRandomSampler 虽然过采样但 Focal Loss 的 alpha1 没有额外加权。解决Focal Loss 的 alpha 按类别频率设成 tensorD 类给 3.0A 类给 0.5。或者把 D 类的增强强度加大RandomErasing 的 p 提到 0.4。5.5 推理时单张图预测结果跟验证集不一致现象验证集准确率 92%但拿单张图预测结果跟验证集里同一张图的预测不同。原因验证集用了 DataLoader 的 shuffleFalse但 batch 内的 BatchNorm 统计量受同 batch 其他图影响。解决推理时设 model.eval()并且 batch_size1 逐张预测。如果必须批量确保 batch 内图像来自同一分布。6. 用 Grad-CAM 验证模型到底在看三视图的哪个区域训完模型不算完得知道它是不是真的在看视图而不是在背背景。Grad-CAM 能把最后一层卷积的梯度加权回原图生成热力图。我一般对每个类别抽 5 张验证集图看热力图是否落在视图轮廓和标注区域。如果热力图集中在图纸边框或标题栏说明模型学到了捷径得重新设计增强或裁剪。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import cv2, numpy as np model build_model(resnet50).cuda() model.load_state_dict(torch.load(resnet_stage3_best.pth)) model.eval() target_layers [model.layer4[-1]] # ResNet50 最后一层 bottleneck cam GradCAM(modelmodel, target_layerstarget_layers) img cv2.imread(data/val/A/sample_001.png) img cv2.resize(img, (224, 224)) rgb np.float32(img) / 255.0 input_tensor val_tf(imageimg)[image].unsqueeze(0).cuda() # 需配合 albumentations grayscale_cam cam(input_tensorinput_tensor, targetsNone) visualization show_cam_on_image(rgb, grayscale_cam[0], use_rgbTrue) cv2.imwrite(cam_A_001.jpg, visualization)逻辑说明target_layers 选 layer4 的最后一个 bottleneck这是 ResNet50 语义最强的层。targetsNone 表示用预测类别作为目标。show_cam_on_image 把热力图叠加到原图。参数说明如果热力图太散把 target_layers 换成 layer3[-1]感受野更小定位更准。DenseNet121 用 model.features.denseblock4GoogleNet 用 model.Mixed_7c。我自己的习惯是每次换骨干网或改增强策略先跑 10 张 Grad-CAM 图扫一眼。如果热力图集中在视图区域继续训如果跑到标题栏或空白处立刻停回去查数据增强和裁剪逻辑。这个习惯帮我省了至少两周的无效训练。希望帮到你。本文还有配套的精品资源点击获取