GoogLeNet在危险物品检测中的微调实践与工程优化 简介该资源为面向计算机视觉学习者的危险物品检测项目以GoogLeNetInception V1为核心实现检测识别适合希望了解经典卷积网络在安检、公共安全等场景落地的开发者和学生。压缩包内共12个文件主要包括Python检测脚本、模型文件pb/bin与配置xml/mapping以及两段演示视频便于对照运行与效果查看压缩包整体约127MB。目前已有187人学习。资源可帮助读者快速复现基于GoogLeNet的检测流程理解多标签分类与目标检测结合思路镜像文件结构明朗适合入门到进阶参考。1. 危险物品检测这类任务为什么值得拿 GoogLeNet 来做骨干机场安检、地铁进站、物流分拨X 光机拍出来的图像几乎都是物品倾斜、互相堆叠的投影。模型要去判别的是刀、枪、打火机这类尺寸跨度极大的物品小的可能只占十几个像素大的几乎铺满整张图。这类场景的部署条件又很苛刻工控机、老 GPU、百毫秒级出结果。GoogLeNet 的 Inception 结构在同一个卷积层里并联了多个感受野对这种目标尺寸分布乱的数据天然更友好同时它参数量小在同样的显存预算下可以把输入分辨率抬得更高。后端换掉分类层之后它还能直接作为 Faster R-CNN、SSD 这些检测框架的主干特征网络。这篇内容按一条实际可落地的开发路径来写先讲清 GoogLeNet 的关键机制和选型理由再给一套可复现的微调代码最后补上类别失衡、部署量化以及热力图验证这几个工程里最容易被忽略的环节。适合刚开始做危险物品检测、又不想一上来就扛大型检测模型的开发者和算法工程师。2. GoogLeNet 的网络结构里哪些设计是危险物品检测真正受益的2.1 Inception 同层多尺度打火机和折叠刀不会被同一个卷积核逼到二选一普通卷积网络一层只有一种固定尺寸的卷积核。危险物品检测面对的却是尺寸范围极大的目标一把美工刀片在 X 光图上可能只占 10×10 像素而一把展开的改锥能占到标准检测框的三分之一。只用 3×3 卷积一路堆叠网络就得靠更深层去重新组合特征小目标的信息经过几次步长为 2 的下采样之后就很难找回来漏检率会明显偏高。GoogLeNet 在每个 Inception 模块里把 1×1、3×3、5×5 卷积以及一个池化分支并联四个分支的输出在通道维度上拼接同一层里同时存在几个不同尺寸的有效感受野。小目标的特征在浅层没有被下采样直接丢弃大目标则交给 5×5 分支做适当放缩两个尺寸的梯度信息在一层内并行传递互相不干扰。这个特点和危险物品检测的数据分布是直接对上的安检项目的训练集里很少有一类目标能占到 70% 以上往往是几类小物品混在一起。需要注意Inception 不是简单堆多个分支就算完。每个分支都有各自的结构输入特征图会被 1×1 卷积先降维后面的 3×3、5×5 卷积才不至于把计算量撑爆。理解了这个约束后面调输入分辨率心里才有数。2.2 1×1 卷积把通道压下去省出来的显存是高输入分辨率的来源X 光设备出图最低也是 1024×768 这个级别公开的危险物品训练集也基本按这个分辨率保存。目标检测直接把全图缩到 224 是行不通的小物品会直接丢失。如果把 1024×1024 的图整体送进一个深度网络显存压力主要来自中间特征图而不是模型参数。GoogLeNet 控制显存的思路很直接每个分支第一层都放 1×1 卷积先把输入通道从 192 压到 64 左右再做 3×3 或 5×5 运算。这个“先压缩再卷积”的顺序让特征图的计算量下降一个量级。在同样一张 1080Ti 上不少项目只能让 VGG16 跑到 512 输入GoogLeNet 却可以上到 768 甚至 896。检测小尺寸危险物时与其急着换模型不如先确认你的骨干吃不吃得下大分辨率输入这一步对一个项目的收益往往比模型结构更明显。2.3 辅助分类器对 X 光小数据集的额外价值浅层梯度更有保障GoogLeNet 在 Inception(4a) 和 Inception(4d) 之后各接了一个辅助输出用一个小全连接把中间特征拉出来单独计算损失。原论文把这里描述成缓解梯度消失同时起正则化作用。做危险物品训练时会发现另一层收益X 光数据集规模通常只有几千张正负样本比例又悬殊浅层卷积很容易被背景噪声带着走因为分类任务回传到浅层的梯度信号太弱。辅助分类器相当于给中间层单独开了一条梯度回传通道浅层卷积每一轮都能收到明确的监督信号BN 层的累计统计量也因此平稳不少。PyTorch 的 torchvision 实现里aux_logits默认是 True真正微调时这里有一个很容易踩的坑创建模型后光改model.fc是不够的两个辅助分类器最后的全连接层还停在 ImageNet 的 1000 类上。不替换的话前向能走反向传播报错的时候会非常难排查。后面 3.2 节会给出对应处理代码。2.4 和 VGG16、ResNet50 放在一起比较GoogLeNet 什么时候最划算不少团队一上来就选 ResNet50理由是参考实现多、调参资料全。ResNet 的残差结构在深层网络上确实稳但它不是所有危险物品场景的最优解。直接说结论当你的数据量不大、目标是快速试验和部署GoogLeNet 通常更合适。对比项GoogLeNet (Inception v1)VGG16ResNet50参数量约 7M约 138M约 25.6M小目标浅层特征Inception 多分支保留明显单路下采样后易退化残差支路有补充但依赖更深层小数据集微调辅助分类器帮助梯度稳定极易过拟合收敛依赖 BN 统计量稳定高分辨率输入1×1 压通道显存友好特征图全尺寸占用高中等部署复杂度需处理辅助输出结构最简单引用最多资料最全这表不是让你在所有场合都换 GoogLeNet。如果训练集超过几万张、目标数量多且互相遮挡严重ResNet50 加深版本往往会反超。GoogLeNet 的定位是“在预算有限的条件下让项目先跑起来并且达到可用的基线”。实际项目里我一般会拿 GoogLeNet 先出一个版本并行用 ResNet50 再训练一版用验证集对比结果再定最终主干两边都用同一个检测头和同一套增强对比才公平。3. 危险物品检测最小可跑流程从数据集整理到 GoogLeNet 微调脚本3.1 先定两件事做二分类还是做带框检测全图输入还是切图输入“检测”这个词在做安检项目时有两种常见实现。第一种是判图软件只需要回答“图像里有没有危险物品”本质是图像分类第二种是给分拣机械臂或者复核工作站用的需要输出物品坐标框这要走目标检测。两种任务对 GoogLeNet 的用法不一样。做分类时直接用 GoogLeNet 的分类头把最后一层全连接换成自己的类别数。做带框检测时常见做法是砍掉model.fc之后的部分把从卷积到 Inception(5b) 的输出当作特征图接到 Faster R-CNN 或 SSD 的检测头上。两个任务千万别混着设计分类任务可以接受全图缩放到 512×512检测任务如果也这样缩小尺寸危险物在锚框匹配阶段就会因为特征太少而回不来梯度。自己采集数据时至少保证每个危险物类别有 300 张以上有效样本并且多角度、多遮挡状态都要覆盖纯正样本单一角度拍出来的数据会让模型非常脆。3.2 用 PyTorch 加载预训练 GoogLeNet 并微调的完整代码下面给一个图像分类版本的最小训练脚本检测版本只需把模型替换成检测框架的骨干部分训练循环逻辑是一样的。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import models, transforms from PIL import Image class DangerGoodsDataset(Dataset): def __init__(self, image_paths, labels, transformNone): self.image_paths image_paths self.labels labels self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img Image.open(self.image_paths[idx]).convert(RGB) label self.labels[idx] if self.transform: img self.transform(img) return img, label def load_googlenet(num_classes): model models.googlenet( weightsmodels.GoogLeNet_Weights.IMAGENET1K_V1, aux_logitsTrue ) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) # 辅助分类器的最后一层也要跟着类别数改 model.aux1.fc2 nn.Linear(model.aux1.fc2.in_features, num_classes) model.aux2.fc2 nn.Linear(model.aux2.fc2.in_features, num_classes) return model def train_one_step(model, images, targets, criterion, optimizer): logits, aux_logits model(images) loss criterion(logits, targets) if aux_logits is not None: loss loss 0.3 * criterion(aux_logits, targets) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()这段代码里最需要留意的就是辅助分类器那两行aux1.fc2和aux2.fc2。刚才说过torchvision 预训练权重里这两个辅助分支的输出还是 1000 类只改主分类层反向传播到辅助分支时会报维度不匹配的错误。0.3是辅助损失的权重我一般取 0.3 而不是默认的 0.5因为 X 光数据集规模不大辅助损失权重过高会把过多训练压力放到中间特征上最终结果有时候反而不如 0.3 稳定。训练时的优化器配置我习惯用带动量的 SGD 而不是 Adam。对微调任务来说SGD 加 momentum 在数据量小的时候收敛更稳最终精度也更可预期。学习率从 1e-3 起步骨干网络前几层因为预训练权重已经学到通用纹理特征可以单独设置更低的学习率。model load_googlenet(num_classes5) backbone_params [] head_params [] for name, param in model.named_parameters(): if fc in name or aux in name: backbone_params.append(param) else: head_params.append(param) optimizer torch.optim.SGD([ {params: backbone_params, lr: 1e-3}, {params: head_params, lr: 1e-4}, ], momentum0.9, weight_decay5e-4)head_params指的是刚新建的分类层它没有预训练权重初始梯度大需要给更高的学习率backbone_params是预训练好的 Inception 部分学习率要给小防止微调初期把 ImageNet 上继承来的通用特征直接冲坏。3.3 微调阶段三个影响最大的参数输入尺寸、冻结层数、学习率步长第一个参数是输入尺寸。建议分类任务从 512×512 起跳检测任务从 768×768 起跳。如果显存允许优先保证输入尺寸而不是批次大小危险物品检测的数据增强在小图上做再多也不如大输入尺寸提供的信息量实在。第二个参数是冻结层数。数据量少于 5000 张时我一般把 conv1 和 maxpool 之前的层直接冻结数据量过万之后全部解冻。判别方式很简单第一轮训练完看 loss 有没有下降趋势没有就减少冻结层数或者调大学习率。第三个参数是学习率衰减策略。微调场景下最常用的是每 N 个 epoch 衰减 0.1。批量大小 16、数据量几千张时N 取 20 到 30 比较合适。如果验证集 loss 在第 15 个 epoch 就开始震荡说明步长太长了把衰减周期挪到 10 再跑一轮。4. 危险物品检测训练稳定性类别失衡、X 光增强与推理压缩4.1 类别不均衡的三种处置手段按优先级排危险物品训练集里最常见的现象是剪刀、钳子样本占 80%打火机可能只有几百张。直接训练模型会把打火机全部判成背景。第一优先是采样层面的调整。用WeightedRandomSampler给样本量少的类别提高采样概率代码很少但效果立竿见影。from torch.utils.data import WeightedRandomSampler class_counts torch.bincount(torch.tensor(all_labels)) weights 1.0 / class_counts[all_labels].float() sampler WeightedRandomSampler(weights, num_sampleslen(all_labels), replacementTrue) loader DataLoader(dataset, batch_size16, samplersampler)weights按类别出现次数的倒数生成出现越少的类别被采样到的概率越高。注意num_samples还是原始样本总数只是让少数类在一个 epoch 里被重复抽取本质上做的是过采样。第二优先是在损失函数层面解决。交叉熵在正负样本比达到 1:100 时依然压不住这时可以把CrossEntropyLoss换成 Focal Loss。它的核心是给置信度高的样本一个衰减系数让模型持续关注难分样本。第三优先级才是后处理调阈值这个放到第 5 章讲。4.2 X 光图像增强不要照搬自然图像的套路X 光图像和普通照片有三个区别颜色信息近似于材质密度物体之间是叠加关系背景没有自然纹理。直接套用ColorJitter、RandomGrayscale这类增强没有任何收益还会让模型误以为密度信息偏移是真实的材质差异。在危险物品检测训练里我用得最多的增强是下面几类。import albumentations as A train_transform A.Compose([ A.LongestMaxSize(max_size768), A.PadIfNeeded(min_height768, min_width768), A.Rotate(limit15, border_mode0, value0), A.RandomScale(scale_limit0.1), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里没有用随机裁剪因为 X 光图里的危险物品经常贴着图像边缘随机裁剪容易把目标裁出去一半。Rotate的border_mode0, value0表示旋转后空白区域填黑对应 X 光底片的无遮挡区域填白反而会引入伪密度。RandomScale控制在 0.1 以内比例太大会让金属物品的纹理形态失真。如果训练集里物品互相遮挡情况多可以考虑加入 MixUp但必须注意一点两张图混合后标签不能只做线性叠加。危险物品检测里经常一张图有正样本、一张图只有负样本线性混合会让正样本的密度信息被背景稀释模型学到的特征不稳定。除非已经确认遮挡是你场景里的主要难点否则我不建议先上 MixUp。4.3 部署时的输入分档与 INT8 量化注意点推理优化有一个常见的误区一上来就套 TensorRT完全不分析模型瓶颈。GoogLeNet 的推理速度在 224 输入下确实极快但危险物品检测不可能用 224。实际落地时把输入尺寸设置成 640、768、896 三档用你自己的测试集分别测延迟和召回率。很多项目的真实瓶颈不是模型卷积本身而是预处理里Resize的短边不对齐导致显存碎片化或者转成 TensorRT 时动态输入尺寸没有固定。做 INT8 量化时最常出问题的地方是 BN 层和激活值分布。GoogLeNet 的 Inception 分支多每个分支结束后的拼接会让特征图数值范围差异较大。常见的做法是先用校准集统计每层激活值的 min/max校准集里必须包含各类危险物品如果只用负样本校准量化后的模型会对小目标出现非常集中的漏检。5. 用 CAM 热力图验证 GoogLeNet 抓对了位置再做置信度阈值校准5.1 一个足够用的 CAM 可视化写法不需要额外装库危险物品检测模型有时候准确率很高但高在错误的地方上——比如背景里的钥匙串、金属拉链和刀具在 X 光下纹理接近模型记住的是“金属纹理”而不是“刀具形状”。要检查这件事最容易上手的是 CAM类别激活映射利用 GoogLeNet 最后的一个卷积特征层和分类权重生成热力图。import torch import cv2 import numpy as np class FeatureStore: def __init__(self): self.features None def hook(self, module, input, output): self.features output store FeatureStore() model.inception5b.register_forward_hook(store.hook) def cam_for_class(model, image_tensor, class_idx): with torch.no_grad(): f_map store.features # [1, C, H, W] logits model(image_tensor) fc_weight model.fc.weight[class_idx] # [C] # 用分类权重对特征图通道做加权求和 cam (f_map[0] * fc_weight.view(-1, 1, 1)).sum(dim0) cam cam.cpu().numpy() cam np.maximum(cam, 0) # 只保留正向激活区域 cam cv2.resize(cam, (w, h)) cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam原理实际很简单GoogLeNet 的fc层之前是全局平均池化类别分数等于每个通道特征图的空间平均值乘以对应的权重系数。把权重乘回特征图通道再求空间和得到的就是这个类别主要依赖的图像区域。实际项目里我会把 20 到 50 张预测正确的刀具图像全部生成热力图然后叠在一张图上统计公共激活区域。如果公共高亮区集中在手柄说明模型依赖形状如果集中在刀刃说明依赖密度纹理。后者在遇到陶瓷刀时会出问题陶瓷刀在 X 光下密度明显低于金属刀。5.2 用验证集扫一遍置信度阈值比调模型参数更快见效训练结束后模型的默认阈值是 0.5但危险物品检测类别分布通常不平衡0.5 往往不是 F1 最优值。将各验证图片的概率保存下来做一次最简单的一维搜索。import numpy as np def best_threshold(probs, labels, lo0.05, hi0.95, step0.05): best_t, best_f1 lo, 0.0 for t in np.arange(lo, hi, step): preds (probs t).astype(int) tp ((preds 1) (labels 1)).sum() fp ((preds 1) (labels 0)).sum() fn ((preds 0) (labels 1)).sum() precision tp / (tp fp 1e-8) recall tp / (tp fn 1e-8) f1 2 * precision * recall / (precision recall 1e-8) if f1 best_f1: best_f1, best_t f1, t return best_t, best_f1参数说明lo和hi是扫描范围step是步长。危险物品检测的阈值不会低于 0.3低于这个值的判图结果基本没法用。扫描结束后把最优阈值写进部署配置再在真实安检图上验证一轮。如果某个类别怎么调阈值都拉不回来精度优先检查负样本采集边界比如是否把钥匙串、手机这类高密度物品的负样本采集得太干净模型没见过足够多类似背景上线后误报才会集中在这些物体上。本文还有配套的精品资源点击获取