news 2026/10/2 3:14:30

基于ResNet/DenseNet/GoogLeNet的三视图分类迁移学习实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于ResNet/DenseNet/GoogLeNet的三视图分类迁移学习实战

简介:本资源面向深度学习图像分类的学习者与研究者,提供一套基于ResNet、DenseNet、GoogLeNet主干网络的自适应迁移学习方案,用于机械图纸三视图的ABCD四分类任务。项目支持灵活切换是否加载ImageNet预训练权重及是否冻结分类层,仅需调整pretrained与freeze_layers参数即可完成对比实验;优化器涵盖Adam与SGD,损失函数采用多类别交叉熵,学习率策略使用余弦退火。评估环节在训练集与验证集上输出loss与准确率曲线,并给出混淆矩阵、recall、precision、F1 score及特异度等指标,各类别详细结果存于json文件。资源包共121个文件,包含61个png、36个jpg图像数据与结果图、6个py源码、5个json指标文件、3个pth权重及若干txt说明,压缩包约94.45MB,已有一键运行的数据集与标签。目前已有64人学习,适合希望快速复现迁移学习对比实验、掌握多指标评估流程的读者参考。

1. 三视图分类为什么值得单独做一套迁移学习方案

机械图纸的三视图识别,听起来像是个传统视觉分类任务,但真正上手做过的人都知道,它和自然图像分类完全不是一回事。三视图是工程语言的浓缩表达,主视图、俯视图、左视图各自承载不同的几何语义,线条稀疏、背景干净、类间差异极小,abcd 四个类别之间的区分往往只靠几条关键轮廓线的位置关系。用 ImageNet 上训出来的模型直接推理,准确率经常卡在及格线附近晃悠,这不是模型不行,而是域差异太大。

这套资源的核心思路是用 ResNet、DenseNet、GoogLeNet 三个骨干网络做自适应迁移学习,把 ImageNet 学到的通用纹理特征迁移到机械图纸域,再通过自适应层对齐源域和目标域的特征分布。适合两类人:一是手里有几百到几千张三视图样本、想快速搭一个可用分类器的工程师;二是想搞明白迁移学习在工业图像上到底怎么落地、参数怎么调的学生和研究者。下面从骨干选型、数据组织、训练脚本到踩坑排查,一步步拆开讲。

2. 三个骨干网络的选型逻辑与自适应迁移层设计

2.1 ResNet、DenseNet、GoogLeNet 各自吃哪碗饭

ResNet 的残差连接解决了深层网络梯度消失的问题,在机械图纸这种线条结构明显的图像上,残差块能有效保留边缘和角点信息。我一般用 ResNet-50 作为基线,它的感受野和深度在中小规模数据集上比较均衡,不会像 ResNet-101 那样容易过拟合。DenseNet 的特征复用机制是另一个路子,每一层都直接连到后面所有层,特征图之间的冗余度低,对三视图中那种细长线条和密集标注区域的区分能力更强。GoogLeNet 的 Inception 模块用多尺度卷积核并行提取特征,适合处理三视图中不同粗细的轮廓线——粗实线、细实线、虚线在同一个感受野下需要不同尺度的卷积核来响应。

选这三个网络不是拍脑袋,而是覆盖了三种不同的特征提取范式:残差学习、密集连接、多尺度并行。实际跑下来,ResNet-50 收敛最稳,DenseNet-121 在小样本上泛化最好,GoogLeNet 训练最快但需要更仔细的学习率调度。自适应迁移层加在骨干网络之后、分类头之前,通常是一个全连接层加域判别器,用对抗训练的方式让源域和目标域的特征分布尽量对齐。

2.2 自适应迁移层的代码实现与参数含义

下面这段代码定义了一个带域判别器的自适应迁移模块,挂在任意骨干网络后面都能用。

import torch import torch.nn as nn from torch.autograd import Function class GradientReversal(Function): @staticmethod def forward(ctx, x, alpha): ctx.alpha = alpha return x.view_as(x) @staticmethod def backward(ctx, grad_output): # 梯度反转:域判别器的梯度取反后回传给特征提取器 return -ctx.alpha * grad_output, None class AdaptiveTransferLayer(nn.Module): def __init__(self, feature_dim, num_classes, num_domains=2): super().__init__() # 分类头:输出 abcd 四类 self.classifier = nn.Sequential( nn.Linear(feature_dim, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes) ) # 域判别器:判断特征来自源域还是目标域 self.domain_discriminator = nn.Sequential( nn.Linear(feature_dim, 128), nn.ReLU(inplace=True), nn.Linear(128, num_domains) ) def forward(self, features, alpha=1.0): class_out = self.classifier(features) reverse_feat = GradientReversal.apply(features, alpha) domain_out = self.domain_discriminator(reverse_feat) return class_out, domain_out

feature_dim是骨干网络输出的特征维度,ResNet-50 是 2048,DenseNet-121 是 1024,GoogLeNet 是 1024。alpha控制梯度反转的强度,训练初期设小一点比如 0.1,让分类损失先主导,后期逐步增大到 1.0,让域对齐发挥作用。Dropout(0.5)在分类头里是必须的,三视图数据集通常不大,不加 dropout 很容易过拟合。域判别器用两层全连接就够了,太深反而会让对抗训练不稳定。

2.3 骨干网络的加载与冻结策略

迁移学习的关键一步是决定冻结哪些层。我的经验是:先冻结骨干网络的全部卷积层,只训练自适应层和分类头 5 个 epoch,让随机初始化的新层先跟上节奏;然后解冻骨干网络的后两个 stage,用较小的学习率(比如 1e-4)做微调;最后如果验证集准确率还在涨,再解冻全部层,学习率降到 1e-5。

import torchvision.models as models def build_model(backbone_name='resnet50', num_classes=4, pretrained=True): if backbone_name == 'resnet50': backbone = models.resnet50(pretrained=pretrained) feature_dim = backbone.fc.in_features backbone.fc = nn.Identity() # 去掉原始分类头 elif backbone_name == 'densenet121': backbone = models.densenet121(pretrained=pretrained) feature_dim = backbone.classifier.in_features backbone.classifier = nn.Identity() elif backbone_name == 'googlenet': backbone = models.googlenet(pretrained=pretrained) feature_dim = backbone.fc.in_features backbone.fc = nn.Identity() else: raise ValueError(f'不支持的骨干网络: {backbone_name}') # 冻结全部参数 for param in backbone.parameters(): param.requires_grad = False model = nn.Sequential(backbone, AdaptiveTransferLayer(feature_dim, num_classes)) return model def unfreeze_stages(model, backbone_name, stage='last2'): # 按 stage 解冻,具体层名根据骨干网络结构调整 backbone = model[0] if backbone_name == 'resnet50': stages = [backbone.layer3, backbone.layer4] if stage == 'last2' else [backbone.layer4] elif backbone_name == 'densenet121': stages = [backbone.features.denseblock3, backbone.features.denseblock4] if stage == 'last2' else [backbone.features.denseblock4] else: stages = [backbone.inception4, backbone.inception5] if stage == 'last2' else [backbone.inception5] for s in stages: for param in s.parameters(): param.requires_grad = True

pretrained=True会加载 ImageNet 预训练权重,这是迁移学习的基础。nn.Identity()把原始的全连接分类头替换成恒等映射,保留骨干网络的特征提取能力。解冻策略里last2表示解冻最后两个 stage,这是最常用的折中方案——既能让模型适应新域,又不会因为解冻太多层导致灾难性遗忘。

3. 三视图数据集的目录组织与增强策略

3.1 按类别分文件夹的 ImageFolder 结构

这套代码默认用torchvision.datasets.ImageFolder加载数据,目录结构必须按类别组织。三视图的 abcd 四个类别各建一个文件夹,每个文件夹里放对应的图纸图像。

dataset/ ├── train/ │ ├── a/ │ │ ├── a_001.png │ │ └── ... │ ├── b/ │ ├── c/ │ └── d/ ├── val/ │ ├── a/ │ ├── b/ │ ├── c/ │ └── d/ └── test/ ├── a/ ├── b/ ├── c/ └── d/

训练集、验证集、测试集的比例建议 7:1.5:1.5,如果样本总数少于 500 张,验证集可以再小一点,把更多数据留给训练。图像格式统一成 PNG 或 JPG,尺寸不要求一致,后面的 transform 会做统一缩放。文件名不要用中文和特殊字符,避免在某些系统上读取失败。

3.2 针对机械图纸的增强策略

自然图像的增强策略直接搬到三视图上会出问题。随机裁剪可能把关键轮廓线裁掉,颜色抖动对黑白图纸没有意义,旋转增强要小心——三视图的视角是固定的,旋转 90 度可能让主视图变成侧视图,语义就变了。

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((512, 512)), # 统一尺寸,保留足够细节 transforms.RandomAffine( degrees=5, # 只做 ±5 度微旋转 translate=(0.02, 0.02), # 轻微平移 scale=(0.95, 1.05) # 轻微缩放 ), transforms.RandomHorizontalFlip(p=0.3), # 低概率水平翻转 transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], # ImageNet 统计量 std=[0.229, 0.224, 0.225] ) ]) val_transform = transforms.Compose([ transforms.Resize((512, 512)), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])

Resize((512, 512))比常用的 224 大,因为三视图的线条细节在 224 分辨率下会丢失严重。RandomAffine的degrees=5是保守设置,再大就可能改变视图语义。RandomHorizontalFlip的概率设 0.3 而不是默认的 0.5,因为水平翻转对某些对称性不强的图纸可能产生不合理的镜像。Normalize 用 ImageNet 的统计量是因为骨干网络是在 ImageNet 上预训练的,保持输入分布一致很重要。

3.3 类别不平衡的处理

三视图数据集中 abcd 四类的样本数往往不均衡,某些类别的图纸可能特别少。直接在 DataLoader 里用WeightedRandomSampler做重采样是最简单的办法。

from torch.utils.data import DataLoader, WeightedRandomSampler import numpy as np def make_balanced_loader(dataset, batch_size=16, num_workers=4): targets = [s[1] for s in dataset.samples] class_counts = np.bincount(targets) class_weights = 1.0 / class_counts sample_weights = [class_weights[t] for t in targets] sampler = WeightedRandomSampler( weights=sample_weights, num_samples=len(sample_weights), replacement=True ) return DataLoader( dataset, batch_size=batch_size, sampler=sampler, num_workers=num_workers, pin_memory=True )

class_weights是每个类别的倒数,样本少的类别权重高,采样时被选中的概率就大。replacement=True表示有放回采样,保证每个 epoch 看到的样本数一致。pin_memory=True在 GPU 训练时能加速数据传输,这个参数容易被忽略但效果明显。

4. 训练脚本、学习率调度与域对抗的平衡

4.1 完整的训练循环与损失函数组合

自适应迁移学习的损失由两部分组成:分类损失和域对抗损失。分类损失用交叉熵,域对抗损失也用交叉熵,但域判别器的梯度要反转。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts def train_one_epoch(model, dataloader, optimizer, device, alpha=1.0): model.train() total_class_loss = 0.0 total_domain_loss = 0.0 correct = 0 total = 0 criterion_class = nn.CrossEntropyLoss() criterion_domain = nn.CrossEntropyLoss() for images, labels in dataloader: images, labels = images.to(device), labels.to(device) batch_size = images.size(0) # 源域标签为 0,目标域标签为 1(这里简化处理,实际需混合两个域的数据) domain_labels = torch.zeros(batch_size, dtype=torch.long).to(device) optimizer.zero_grad() class_out, domain_out = model(images, alpha=alpha) loss_class = criterion_class(class_out, labels) loss_domain = criterion_domain(domain_out, domain_labels) loss = loss_class + 0.1 * loss_domain # 域损失权重 0.1 loss.backward() optimizer.step() total_class_loss += loss_class.item() total_domain_loss += loss_domain.item() _, predicted = class_out.max(1) correct += predicted.eq(labels).sum().item() total += labels.size(0) return total_class_loss / len(dataloader), total_domain_loss / len(dataloader), correct / total

域损失的权重设 0.1 而不是 1.0,是因为域对抗训练容易主导梯度,把分类性能带偏。alpha参数从 0.1 线性增加到 1.0,前 10 个 epoch 让分类损失先收敛,后面再加强域对齐。如果只有源域数据没有目标域数据,可以把域判别器关掉,退化成普通的迁移学习。

4.2 学习率调度与早停策略

学习率调度用余弦退火加 warm restart,比 StepLR 更平滑,不容易在局部最优附近震荡。

optimizer = optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=1e-4 ) scheduler = CosineAnnealingWarmRestarts( optimizer, T_0=10, # 第一个 restart 周期为 10 个 epoch T_mult=2, # 每次 restart 后周期翻倍 eta_min=1e-6 # 最小学习率 ) best_acc = 0.0 patience = 15 counter = 0 for epoch in range(100): alpha = min(1.0, 0.1 + epoch * 0.02) # alpha 从 0.1 逐步增到 1.0 train_loss, domain_loss, train_acc = train_one_epoch( model, train_loader, optimizer, device, alpha ) val_acc = evaluate(model, val_loader, device) scheduler.step() if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') counter = 0 else: counter += 1 if counter >= patience: print(f'早停于 epoch {epoch},最佳验证准确率 {best_acc:.4f}') break

AdamW比 Adam 多了正确的权重衰减实现,在迁移学习里更稳。T_0=10和T_mult=2让学习率周期性重启,有助于跳出局部最优。早停的patience=15是经验值,如果验证集准确率连续 15 个 epoch 不涨就停,避免过拟合。alpha的线性增长策略让域对抗逐步介入,比一开始就强对抗更稳定。

4.3 三个骨干网络的训练参数对比

不同骨干网络对学习率和 batch size 的敏感度不一样,下面这张表是我实际跑下来比较稳的配置。

骨干网络初始学习率Batch Size解冻策略典型收敛 Epoch
ResNet-501e-416last240-60
DenseNet-1215e-512last250-70
GoogLeNet2e-424last230-50

DenseNet 的学习率要设小一点,因为密集连接让梯度更容易累积,学习率大了容易发散。GoogLeNet 参数量相对少,可以用更大的 batch size 和更高的学习率,收敛也更快。ResNet-50 居中,是最稳妥的基线选择。如果显存不够,batch size 减半的同时学习率也要相应降低,但不要低于 1e-5。

5. 避坑与排查:三视图分类里最容易翻车的五个地方

5.1 验证集准确率远高于测试集

现象:训练时验证集准确率能到 90% 以上,换测试集掉到 60% 多。原因通常是验证集和测试集的分布不一致,或者验证集被无意中参与了模型选择导致过拟合。解决:确保验证集和测试集来自同一批数据的不同划分,用分层抽样保证每个类别的比例一致。如果数据量允许,做 5 折交叉验证,取平均准确率作为最终指标。

5.2 域判别器 Loss 不下降或剧烈震荡

现象:域判别器的损失一直在 0.69 附近(二分类的随机水平),或者上下剧烈跳动。原因是梯度反转的 alpha 设得太大,或者域判别器太强导致对抗失衡。解决:把 alpha 从 0.1 开始逐步增加,域判别器的层数和宽度不要超过分类头,域损失的权重降到 0.05 甚至 0.01。如果还是震荡,先冻结域判别器训练几个 epoch 再开启对抗。

5.3 图像尺寸不统一导致 DataLoader 报错

现象:训练时报RuntimeError: stack expects each tensor to be equal size。原因是数据集里混了不同尺寸的图像,而 transform 里没有做 Resize。解决:在transforms.Compose的第一步就加Resize((512, 512)),确保所有图像输出尺寸一致。如果原始图像长宽比差异很大,用Resize加CenterCrop组合,但要注意别裁掉关键轮廓。

5.4 预训练权重加载失败

现象:model.load_state_dict报 key 不匹配,或者加载后准确率反而下降。原因是骨干网络的层名和预训练权重不对应,或者pretrained=True时下载的权重不完整。解决:先用torchvision.models.resnet50(pretrained=True)单独测试能否正常加载,再检查自定义模型里有没有改动骨干网络的层名。如果用了nn.Identity()替换分类头,加载权重时要加strict=False。

5.5 显存溢出与 batch size 的权衡

现象:训练到一半报CUDA out of memory。原因是 512 分辨率的图像加三个骨干网络,显存占用比预期大。解决:先把 batch size 降到 8 或 4,用梯度累积模拟大 batch。如果还不够,把输入分辨率降到 384 或 320,但准确率可能会掉 2-3 个百分点。另一个办法是用混合精度训练,torch.cuda.amp能省将近一半显存。

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for images, labels in dataloader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() with autocast(): class_out, domain_out = model(images, alpha=alpha) loss = criterion_class(class_out, labels) + 0.1 * criterion_domain(domain_out, domain_labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

混合精度训练里autocast自动把部分运算转成 float16,GradScaler防止梯度下溢。这个改动几乎不影响准确率,但显存能省 30%-40%,训练速度也能快 20% 左右。

6. 模型融合与推理加速的实战技巧

三个骨干网络各自训练完之后,最直接的提升方式是把它们的预测概率做加权平均。权重不用手动调,用验证集上的准确率做 softmax 归一化就行。

import torch.nn.functional as F def ensemble_predict(models, images, weights=None): # models: 列表,每个元素是训练好的模型 # weights: 列表,每个模型的权重,默认按验证准确率归一化 all_probs = [] for model in models: model.eval() with torch.no_grad(): class_out, _ = model(images) probs = F.softmax(class_out, dim=1) all_probs.append(probs) if weights is None: weights = [1.0 / len(models)] * len(models) weights = torch.tensor(weights).to(images.device) weights = weights / weights.sum() weighted_probs = sum(w * p for w, p in zip(weights, all_probs)) return weighted_probs.argmax(dim=1)

融合的时候有个细节:三个模型的输入 transform 必须完全一致,否则概率分布不可比。如果某个骨干网络的验证准确率明显低,它的权重会被自动压低,不会拖累整体。我一般还会做一个 TTA(测试时增强),把原图、水平翻转图、轻微旋转图的预测概率再平均一次,通常能再涨 1-2 个百分点。

推理加速方面,如果部署环境是 CPU,用torch.jit.trace把模型转成 TorchScript 能提速 20%-30%。如果是 GPU 部署,用 TensorRT 或者 ONNX Runtime 收益更大,但转换过程容易踩坑,建议先用 TorchScript 跑通再考虑更激进的优化。

# TorchScript 导出示例 model.eval() dummy_input = torch.randn(1, 3, 512, 512).to(device) traced_model = torch.jit.trace(model, dummy_input) traced_model.save('traced_model.pt')

导出时dummy_input的尺寸要和实际推理尺寸一致,否则 trace 出来的计算图可能不兼容。如果模型里有动态控制流(比如 if 判断),trace 会失败,得改用torch.jit.script。导完之后用torch.jit.load加载,在 CPU 上跑一遍验证输出和原模型一致,再上线。

从那以后我每次做完迁移学习训练,都会强制走一遍「验证集评估 → 测试集评估 → 单张推理 → 批量推理」的完整链路,确认没有数据泄漏和预处理不一致的问题才敢交付。这套三视图分类的方案在几个实际项目里跑下来,ResNet-50 单模型能到 85% 左右,三个模型融合后稳定在 90% 上下,对于机械图纸这种细粒度分类任务算是可用的水平。希望帮到你。

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

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

单身狗题解:把单身期过成完整的生活系统

说实话,我第一次看到"单身狗题解"这四个字的时候,先笑了一下,然后突然觉得这个说法特别妙。它把单身这件事比喻成了一道"题",好像真的存在一个标准答案等着你去求解。我在情感和生活方式这个领域写了快十年&a…

作者头像 李华
网站建设 2026/10/2 3:13:27

基于SpringBoot微服务的医疗健康管理系统设计与实现

陆陆续续有学弟学妹拿着毕业设计来找我,十个里面至少六七个是做管理系统,题目都是“某某管理系统设计与实现”的套路。这类项目本身不难,难的是怎么在千篇一律的增删改查里做出让导师点头的亮点。这套基于SpringBoot和微服务架构的医疗健康管…

作者头像 李华
网站建设 2026/10/2 3:13:27

Python测井课设实战:岩性识别与曲线回归全流程

简介:这份资源面向计算机、人工智能、自动化、电子信息等专业的高校学生与科研人员,围绕人工智能在石油测井领域的应用,提供Python岩性识别与测井曲线回归的完整课程设计资料。项目代码经过测试可稳定运行,适合作为课程设计、毕业…

作者头像 李华
网站建设 2026/10/2 3:12:29

迪士尼动画十二黄金法则深度拆解:动画师必修的表演底层逻辑

我是靠这个入的行。入行第一年,带我的组长丢给我一本翻得起了毛边的《生命的幻象》(The Illusion of Life),他原话是:“软件操作一个月能学会,这十二条动画黄金法则你一年都不一定吃得透。”当时我不服气&a…

作者头像 李华
网站建设 2026/10/2 3:11:15

基于Kingscada自带历史库的车间日报表与趋势曲线实现方案

车间上位机数据报表改造的事,我一开始差点绕远路。客户生产现场是三班倒,每班结束后都要看产量、温度、压力、电量的日报表,还要能随时调出任意一个变量的趋势曲线,用来分析设备状态和产品质量。我们用的上位机组态软件是 Kingsca…

作者头像 李华
网站建设 2026/10/2 3:11:04

Smartstore内容营销实战:博客、论坛与投票功能完整指南

Smartstore内容营销实战:博客、论坛与投票功能完整指南 【免费下载链接】Smartstore A modular, scalable and ultra-fast open-source all-in-one eCommerce platform built on ASP.NET Core 10 项目地址: https://gitcode.com/GitHub_Trending/smar/Smartstore …

作者头像 李华