☰
基于ResNet、DenseNet、GoogLeNet融合的机械图纸三视图分类与自适应迁移学习
2026/10/2 9:07:37 网站建设 项目流程

简介:本资源面向图像分类初学者与迁移学习实践者,提供一套机械图纸三视图abcd四分类的完整可运行方案。主干网络可选ResNet、DenseNet与GoogLeNet,通过pretrained与freeze_layers参数即可切换是否加载ImageNet预训练权重或仅训练分类输出层,并内置Adam与SGD两种优化器做消融对比,损失函数采用多类别交叉熵,学习率策略为余弦退火。评估环节覆盖训练集与验证集的loss、准确率曲线,以及混淆矩阵、recall、precision、F1 score、特异度等指标输出与图像,各类别详细指标可在json文件中查看。资源包共121个文件,以png、jpg图像与py脚本为主,另含json指标、pth权重及txt说明,压缩包约94.45MB,数据集与标签齐备,按参考猫狗数据集摆放即可一键运行。目前已有64人学习下载,适合希望快速复现多网络对比实验并掌握迁移学习调参思路的读者。

1. 机械图纸三视图分类:为什么 ResNet、DenseNet、GoogLeNet 要一起上

机械图纸的三视图识别,听起来像个传统 CV 问题,但真正做过的人都知道坑有多深。一张 A4 图纸扫描件里,主视图、俯视图、左视图的布局差异、线宽变化、标注遮挡,再加上不同设计院出图风格不统一,直接拿 ResNet 硬训,准确率往往卡在 70% 上下就上不去了。更麻烦的是,工业场景里标注数据极少——一个新产线可能只有几十张已标注图纸,从零训练根本不现实。

这个方向要解决的核心问题就是:在样本量不足、域差异明显的条件下,用 ResNet、DenseNet、GoogLeNet 三种预训练骨干网络做自适应迁移学习,把机械图纸三视图分成 A、B、C、D 四类。适合谁看?做工业质检、图纸归档、PLM 系统集成的工程师,以及手上有一批图纸但不知道怎么高效分类的算法同学。下面从骨干选型、迁移策略、数据管线到训练排错,一步步拆开讲。

2. 三种骨干网络怎么选:ResNet、DenseNet、GoogLeNet 的特征提取差异

2.1 为什么不能只用 ResNet

ResNet 的残差连接解决了深层网络梯度消失问题,在 ImageNet 上表现稳定,作为迁移学习的底座几乎是默认选项。但机械图纸有个特殊之处:三视图之间的差异往往体现在细粒度特征上——比如俯视图里圆孔的投影是椭圆,主视图里是矩形,这种局部几何差异需要网络对浅层特征有更强的保留能力。

ResNet 的 bottleneck 结构在降维时会丢失一部分浅层细节,尤其是 resnet50 以上的版本。实际测试中,resnet18 和 resnet34 在图纸分类上的表现反而比 resnet50 好,因为浅层特征保留得更完整。如果要用 resnet50,建议把 layer1 和 layer2 的学习率设得更低,避免预训练权重被破坏。

DenseNet 的优势在于特征复用。每一层的输出都直接连到后面所有层,浅层提取的边缘、角点信息可以直达分类头。对于三视图这种需要区分“虚线还是实线”“标注箭头方向”的任务,DenseNet 的特征利用率明显更高。但代价是显存占用大,densenet121 在 batch size 32、输入 224×224 时大约需要 8GB 显存,densenet169 直接翻倍。

GoogLeNet 的 Inception 模块用不同尺寸的卷积核并行提取特征,对图纸中不同尺度的符号(比如螺纹孔标注 vs 整体轮廓)有天然优势。但 GoogLeNet 原始版本深度只有 22 层,特征抽象能力不如前两者,通常需要用 InceptionV3 或 InceptionV4 替代。实际项目中我一般用 InceptionV3 作为第三路分支,输入尺寸调到 299×299 以匹配原始设计。

2.2 三路骨干的融合策略

单独用任何一个骨干,在四分类任务上大概能到 82%~87% 的准确率。三路融合后可以稳定在 91% 以上。融合方式有两种常见做法:

第一种是特征拼接。把三个骨干的全局池化输出(ResNet 512 维、DenseNet 1024 维、GoogLeNet 2048 维)直接 concat,后面接两层全连接。这种方式实现简单,但参数量大,容易过拟合。

第二种是加权投票。每个骨干单独接一个分类头,推理时对三个 softmax 输出做加权平均。权重可以根据验证集表现动态调整,我一般设 ResNet 0.3、DenseNet 0.4、GoogLeNet 0.3。这种方式训练时可以分别微调,显存压力小,适合单卡场景。

import torch import torch.nn as nn import torchvision.models as models class TriBackboneFusion(nn.Module): def __init__(self, num_classes=4, fusion='concat'): super().__init__() self.fusion = fusion # ResNet18 作为浅层特征提取分支 self.resnet = models.resnet18(pretrained=True) self.resnet.fc = nn.Identity() # 去掉原始分类头,输出 512 维 # DenseNet121 作为特征复用分支 self.densenet = models.densenet121(pretrained=True) self.densenet.classifier = nn.Identity() # 输出 1024 维 # InceptionV3 作为多尺度分支 self.googlenet = models.inception_v3(pretrained=True, aux_logits=False) self.googlenet.fc = nn.Identity() # 输出 2048 维 if fusion == 'concat': # 拼接融合:512 + 1024 + 2048 = 3584 维 self.classifier = nn.Sequential( nn.Linear(3584, 512), nn.ReLU(), nn.Dropout(0.5), nn.Linear(512, num_classes) ) else: # 投票融合:每个分支独立分类头 self.resnet_fc = nn.Linear(512, num_classes) self.densenet_fc = nn.Linear(1024, num_classes) self.googlenet_fc = nn.Linear(2048, num_classes) def forward(self, x): # 三路特征提取 f_res = self.resnet(x) f_den = self.densenet(x) f_goo = self.googlenet(x) if self.fusion == 'concat': feat = torch.cat([f_res, f_den, f_goo], dim=1) return self.classifier(feat) else: # 投票模式返回三个 logits,训练时分别计算损失 return self.resnet_fc(f_res), self.densenet_fc(f_den), self.googlenet_fc(f_goo)

这段代码的关键点在于:三个骨干的预训练权重全部加载,但分类头全部替换。nn.Identity()的作用是保留特征提取部分,去掉原始 1000 类输出。concat 模式下注意输入尺寸要统一,ResNet 和 DenseNet 用 224×224,InceptionV3 需要 299×299,实际写 dataloader 时要分别处理。投票模式下三个分支可以共享同一个输入尺寸,但 InceptionV3 对 224 输入的适应需要额外微调几轮。

2.3 预训练权重的加载与冻结策略

迁移学习的核心操作是冻结部分层。我的经验是:ResNet 冻结 layer1 和 layer2,DenseNet 冻结前两个 dense block,GoogLeNet 冻结前 10 个 Inception 模块。解冻太多容易过拟合,解冻太少特征适配不够。

def freeze_layers(model): # ResNet:冻结浅层 for name, param in model.resnet.named_parameters(): if 'layer1' in name or 'layer2' in name: param.requires_grad = False # DenseNet:冻结前两个 dense block for name, param in model.densenet.named_parameters(): if 'denseblock1' in name or 'denseblock2' in name: param.requires_grad = False # GoogLeNet:冻结前 10 个 Inception 模块 for name, param in model.googlenet.named_parameters(): if any(f'Mixed_{i}' in name for i in range(1, 6)): param.requires_grad = False return model

冻结之后,可训练参数量大约降到原来的 40% 左右。学习率设置上,解冻部分用 1e-4,新加的分类头用 1e-3,这样既能微调预训练特征,又能让分类头快速收敛。

3. 自适应迁移学习的落地:从数据管线到训练循环

3.1 机械图纸数据集的构建与增强

三视图分类的数据集通常来自扫描件或 PDF 导出,分辨率参差不齐。我一般先做三步预处理:灰度化、自适应二值化、尺寸归一化到 512×512。二值化用 OpenCV 的adaptiveThreshold,blockSize 设 35,C 设 10,这个参数对工程图纸的线条保留效果最好。

数据增强不能太激进。随机旋转超过 5 度会让三视图的投影关系失真,颜色抖动对黑白图纸没意义。有效的增强手段是:随机裁剪(crop 比例 0.85~1.0)、水平翻转(仅对俯视图和左视图有效,主视图翻转会改变语义)、高斯噪声(模拟扫描质量差异)。

import cv2 import numpy as np from torch.utils.data import Dataset, DataLoader from torchvision import transforms class DrawingDataset(Dataset): def __init__(self, image_paths, labels, mode='train'): self.image_paths = image_paths self.labels = labels self.mode = mode # 训练和验证使用不同的增强策略 if mode == 'train': self.transform = transforms.Compose([ transforms.ToPILImage(), transforms.RandomCrop(480), # 从 512 随机裁到 480 transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) else: self.transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # 读取并预处理图纸 img = cv2.imread(self.image_paths[idx], cv2.IMREAD_GRAYSCALE) # 自适应二值化,保留线条细节 img = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 35, 10) img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # 转三通道适配预训练模型 img = self.transform(img) return img, self.labels[idx]

这里有个容易翻车的点:adaptiveThreshold的 blockSize 必须是奇数,设成 34 会直接报错。另外二值化后的图像转 RGB 时,三个通道值完全相同,预训练模型的归一化参数仍然适用,不需要额外调整。

3.2 自适应迁移学习的损失函数设计

普通交叉熵在域差异大时容易过拟合源域。自适应迁移学习的常见做法是加一个域对齐损失,让源域和目标域的特征分布尽量接近。工程上最稳定的是 MMD(最大均值差异)损失,计算量小,不需要额外判别器。

def mmd_loss(source_features, target_features, kernel_mul=2.0, kernel_num=5): """计算源域和目标域特征的最大均值差异""" batch_size = source_features.size(0) total = torch.cat([source_features, target_features], dim=0) # 计算核矩阵 total0 = total.unsqueeze(0).expand(total.size(0), -1, -1) total1 = total.unsqueeze(1).expand(-1, total.size(0), -1) L2_distance = ((total0 - total1) ** 2).sum(2) # 多核 MMD bandwidth = torch.sum(L2_distance.data) / (batch_size ** 2) bandwidth /= kernel_mul ** (kernel_num // 2) bandwidth_list = [bandwidth * (kernel_mul ** i) for i in range(kernel_num)] kernel_val = [torch.exp(-L2_distance / bw) for bw in bandwidth_list] kernels = sum(kernel_val) XX = kernels[:batch_size, :batch_size] YY = kernels[batch_size:, batch_size:] XY = kernels[:batch_size, batch_size:] YX = kernels[batch_size:, :batch_size] loss = torch.mean(XX + YY - XY - YX) return loss

MMD 损失的权重一般设 0.1~0.5。太大时模型会过度关注域对齐而忽略分类精度,太小时起不到自适应效果。我一般从 0.3 开始,根据验证集准确率调整。

3.3 训练循环与学习率调度

三路骨干的训练不能一视同仁。ResNet 和 DenseNet 的预训练权重比较稳定,学习率可以低一些;GoogLeNet 的 Inception 模块对输入尺寸敏感,需要稍高的学习率来适应。

def train_epoch(model, dataloader, optimizer, criterion, device, mmd_weight=0.3): model.train() total_loss = 0 correct = 0 total = 0 for images, labels in dataloader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() # 投票模式下返回三个 logits outputs = model(images) if isinstance(outputs, tuple): # 分别计算三个分支的损失 loss = 0 for out in outputs: loss += criterion(out, labels) loss = loss / 3 else: loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() # 计算准确率 if isinstance(outputs, tuple): # 投票取平均 pred = sum(torch.softmax(out, dim=1) for out in outputs) / 3 else: pred = torch.softmax(outputs, dim=1) correct += (pred.argmax(1) == labels).sum().item() total += labels.size(0) return total_loss / len(dataloader), correct / total

学习率调度用余弦退火,初始 lr 设 1e-3,每 10 个 epoch 降到 0.8 倍。如果验证集准确率连续 5 个 epoch 不提升,就提前停止。实际项目中,30 个 epoch 左右模型基本收敛。

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

4.1 输入尺寸不匹配导致 InceptionV3 报错

现象:训练时 InceptionV3 分支报RuntimeError: Expected 4D input或者特征图尺寸对不上。

原因:InceptionV3 的原始设计输入是 299×299,如果 dataloader 统一输出 224×224,经过前面的卷积层后特征图会小于预期,导致某些 Inception 模块的 concat 操作维度不匹配。

解决:要么把 InceptionV3 的输入单独调到 299×299,要么在模型里加一个自适应池化层。我一般用后者,在self.googlenet后面加nn.AdaptiveAvgPool2d((8, 8)),强制统一特征图尺寸。

4.2 冻结层数太多导致欠拟合

现象:训练 loss 下降很慢,验证集准确率卡在 60% 左右上不去。

原因:冻结了太多层,可训练参数太少,模型没有足够的容量去适应机械图纸的域特征。

解决:逐步解冻。先冻结所有骨干只训分类头 5 个 epoch,然后解冻最后两个 block 再训 10 个 epoch,最后全部解冻微调 5 个 epoch。这种渐进式解冻比一次性解冻效果更稳。

4.3 二值化参数不当导致线条断裂

现象:预处理后的图纸线条断断续续,细线完全消失。

原因:adaptiveThreshold的 blockSize 设得太大,局部阈值计算窗口覆盖了太多背景区域,导致细线被判定为背景。

解决:blockSize 从 35 开始试,如果线条断裂就降到 25 或 15。C 值从 10 开始,线条太粗就增大 C,线条太细就减小 C。不同来源的图纸需要单独调参,没有万能值。

4.4 三路输出权重不均衡导致投票失效

现象:投票融合时某个分支的准确率明显低于其他两个,拉低整体表现。

原因:三个骨干的预训练域和机械图纸域差异不同。GoogLeNet 在 ImageNet 上学的特征偏自然图像,迁移到工程图纸时适应最慢。

解决:给每个分支单独设学习率。GoogLeNet 用 5e-4,ResNet 用 1e-4,DenseNet 用 2e-4。或者在损失函数里给每个分支加权,表现差的分支权重调低。

4.5 显存溢出导致训练中断

现象:训练到一半报CUDA out of memory。

原因:DenseNet 的特征复用机制导致中间激活值占用大量显存,batch size 设大了直接爆。

解决:用梯度累积。batch size 设 8,累积 4 次梯度再更新,等效 batch size 32。或者用混合精度训练,torch.cuda.amp可以省 40% 左右的显存。

5. 进阶技巧:用特征可视化验证三路骨干是否真的学到了东西

训练完模型不能只看准确率,还得确认三个骨干确实在提取不同维度的特征。我常用的方法是 Grad-CAM 可视化,分别看 ResNet、DenseNet、GoogLeNet 对同一张图纸的关注区域。

import matplotlib.pyplot as plt from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image def visualize_three_backbones(model, image, device): """分别可视化三个骨干的注意力区域""" model.eval() img_tensor = image.unsqueeze(0).to(device) # 分别对三个骨干做 Grad-CAM backbones = { 'ResNet': model.resnet.layer4[-1], 'DenseNet': model.densenet.features.denseblock4, 'GoogLeNet': model.googlenet.Mixed_7c } fig, axes = plt.subplots(1, 4, figsize=(20, 5)) axes[0].imshow(image.permute(1, 2, 0).cpu().numpy()) axes[0].set_title('Original') for idx, (name, target_layer) in enumerate(backbones.items(), 1): cam = GradCAM(model=model, target_layers=[target_layer]) grayscale_cam = cam(input_tensor=img_tensor)[0] visualization = show_cam_on_image( image.permute(1, 2, 0).cpu().numpy(), grayscale_cam, use_rgb=True ) axes[idx].imshow(visualization) axes[idx].set_title(f'{name} Attention') plt.tight_layout() plt.savefig('three_backbone_attention.png', dpi=150)

跑完这个可视化,你会看到 ResNet 更关注整体轮廓,DenseNet 对细线和小符号敏感,GoogLeNet 则在标注密集区域激活更强。如果三个热力图几乎一样,说明融合没起作用,需要检查特征拼接前是否做了独立的归一化。

另一个验证方法是特征相似度分析。把三个骨干的输出特征做余弦相似度,如果两两相似度都超过 0.9,说明特征冗余,需要调整骨干组合。我一般要求两两相似度在 0.6~0.8 之间,这样融合才有增益。

最后说个血泪教训:别在验证集上反复调参。我见过太多人把验证集当测试集用,调到最后模型在验证集上 95%,上线后实际只有 70%。正确做法是留一个独立的测试集,只在最终确定模型时跑一次。训练过程中只看验证集的 loss 曲线,不看准确率,避免过拟合验证集。

希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询