首页 / 资讯中心 / 文章详情

GoogLeNet实现X光危险品检测:从SSD改造到边缘部署实战

GoogLeNet实现X光危险品检测:从SSD改造到边缘部署实战 ★ FEATURED ARTICLE
简介面向安检、车站、地铁等公共场所的危险物品自动识别场景这份资料以GoogLeNetInception V1为核心模型提供了从网络结构搭建、权重文件到推理脚本的完整项目方案适用于深度学习和计算机视觉方向的中级开发者也可作为入门者理解经典卷积网络的实践参考。GoogLeNet通过Inception模块在控制计算量的同时增强特征表达能力本项目正是利用该特性完成危险物品的多标签分类识别。压缩包内共12个文件包含两个Python源码、两个演示视频、一个XML标注文件以及pb、bin、mapping等模型和映射文件可支撑环境准备、模型加载、前向推理、结果演示等环节。资源包整体大小约127MB已有187人学习。通过该资源可掌握GoogLeNet模型在特定任务上的微调方法、模型文件解析与部署要点并能借助演示视频快速了解运行效果节省自行摸索的时间。1. 危险物品检测为什么用 GoogLeNet 而不是更大更深的网络危险物品检测在安检行业里是个很具体的任务X 光机持续过包系统要在几百毫秒内识别刀具、枪支、液体、压力罐这类目标还要当场给出位置框。我做过几次这类落地最大的体感是真正的瓶颈往往不是“模型认不认得出刀”而是目标小、堆叠密、设备算力有限。GoogLeNet 这种十年前的分类骨干在危险品检测这种数据量小、误检代价高的场景里反而比一堆新网络实用它的 Inception 结构用多尺度卷积并行对长条刀具和堆叠物体的响应更稳参数量在几百万量级工控机上 CPU 也能跑实时配合 SSD 风格的检测头能直接输出危险品位置。这篇笔记写给想快速验证“GoogLeNet 能不能做危险品检测”以及准备在边缘设备上线的工程师照着能跑通最小方案也把数据、训练和部署里容易翻车的点说清楚。2. GoogLeNet 的 Inception 结构X 光叠影下的多尺度特征怎么来2.1 多尺度感受野一个 block 同时看到刀尖和刀柄X 光安检图像和自然图像有一个根本差异它是透视叠加图。包里的笔记本电脑、雨伞、金属扣件、液体瓶全部叠在同一个投影平面上危险品的轮廓经常被遮挡得只剩半截。这种情况下模型的感受野策略比网络深度更关键。GoogLeNet 的 Inception block 把 1x1、3x3、5x5 卷积和 3x3 池化在同一个 block 里并联输出拼在一起等于让这一层同时在一小块区域里看不同视野1x1 抓密集的材质纹理3x3 抓边缘和局部形状5x5 覆盖更大范围的密度渐变。对一把折刀来说刀尖可能只占几个像素而刀身能拉到几十上百个像素多尺度感受野正好一次前向把两部分特征都保留下来。GoogLeNet 参数少的关键也在这个 block 的设计每个 5x5 卷积之前先接一个 1x1 卷积降维把输入通道压到原来的四分之一左右再算大卷积计算量被压得很低。以 224 输入为例GoogLeNet 的参数量在 5.6M 左右而 VGG16 是 138MResNet50 是 25.6M。对安检机这种 7x24 小时开机的边缘设备来说这个差距直接决定能不能用 CPU 推理。我一般会在选型时先把各 backbone 的 FLOPs 和参数跑出来判断工控机扛不扛得住from thop import profile import torch from torchvision.models import googlenet net googlenet(weightsNone).eval() flops, params profile(net, inputs(torch.randn(1, 3, 224, 224),)) print(fGoogLeNet: {params / 1e6:.1f}M params, {flops / 1e9:.2f} GFLOPs)这段代码用 thop 统计模型的参数量和计算量。注意weightsNone是为了避免加载预训练权重拖慢统计实际训练时再加载 ImageNet 权重。如果在旧版 torchvision 里没有weights参数就改成googlenet(pretrainedFalse)效果一样。FLOPs 数字的意义不在精确定位而是给部署设备做预算安检机常见的是 Intel 低功耗工控机或 Jetson 系列GoogLeNet 这个量级在 CPU 上单帧 224 推理大约几十毫秒ResNet50 要翻两三倍VGG16 基本告别实时。2.2 和 VGG、ResNet 对比危险品场景的选型逻辑是先算漏检成本很多团队一上来就选 ResNet50 甚至更大的网络理由是 ImageNet 精度高。但危险品检测的评估体系和 ImageNet 完全不同漏一件刀是事故误报一次只是麻烦。所以选型要考虑的不只是 mAP还有数据量、训练稳定性和推理资源。我自己的经验是在目标尺寸集中在 20 到 300 像素、图像噪声大的 X 光场景里深网络带来的精度提升会被过拟合和训练不稳定吃回去。骨干参数规模单张 224 推理 FLOPs多尺度特征自然程度边缘 CPU 实时性VGG16138M15.5 GFLOPs 左右一般需额外设计困难ResNet5025.6M4.1 GFLOPs 左右较自然但首层降采样大可用但偏紧GoogLeNet5.6M 左右1.5 GFLOPs 左右自然三个 stage 输出直接可用轻松这张表里 GoogLeNet 的 FLOPs 是工程经验值不同实现略有出入但量级关系不会变。更关键的一点是危险品检测的公开数据集规模有限常见 X 光安检数据集也就是几千到几万张标注图深网络在这种数据量下很容易把背景纹理背下来。GoogLeNet 结构相对浅辅助分类器还能在训练早期提供梯度回传收敛更稳。我倾向于把选型逻辑总结成一句先定漏检成本再定设备预算最后才看网络精度。GoogLeNet 在参数、推理速度和训练友好度上恰好同时满足前两点这也是它在 SSD 时代被大量生产系统采用的原因。工具链也成熟mmdetection 和各类 SSD 实现里都有现成配置把 backbone 换过来就能跑。3. 把分类网络改成检测器GoogLeNet 接 SSD 检测头的实现3.1 两条路线SSD 式检测头 vs 滑窗分类为什么放弃后者危险品检测要求输出边框而不是只给一个“有没有危险品”的结论。常见做法是把 GoogLeNet 最后的全连接层整个丢掉让网络变成全卷积结构在不同的特征图上接检测头直接预测框和类别这就是 SSD 风格的改造。另一条路是滑窗分类保留分类头在图上切几十个 patch 分别判断。这条路在推理速度上基本是翻车的一张 384 分辨率的图切 30 个窗一次过包就要跑 30 次前向安检机的实时性根本撑不住。还有个选项是用 Faster R-CNN 这类两阶段检测器。RPN 加 RoIHead 的流程对精度有帮助但工程量和推理延迟都更大而且 GoogLeNet 这种浅骨干的优势在两阶段框架里发挥不明显。SSD 的优势在于先验框是固定的、单阶段前向路径简单、每个特征层的预测互不干扰非常适合边缘设备。我做危险品检测的默认方案就是 GoogLeNet 特征提取加三层 SSD 头类别先做二分类背景/危险品后续再按业务需要扩类别。3.2 截取 Inception 主干三个特征层的 PyTorch 代码torchvision 里的 GoogLeNet 结构是清晰的序列conv1 到 conv3然后是三组 Inception block中间用 maxpool 隔开。我把网络截成三个 stage分别输出 38x38、19x19、10x10 的特征图输入 300 时。这三个分辨率正好覆盖危险品的目标尺寸范围小到刀尖大到整瓶液体。import torch.nn as nn from torchvision.models import googlenet, GoogLeNet_Weights class GoogLeNetFeature(nn.Module): def __init__(self, pretrainedTrue): super().__init__() try: weights GoogLeNet_Weights.IMAGENET1K_V1 if pretrained else None except AttributeError: weights IMAGENET1K_V1 if pretrained else None net googlenet(weightsweights) self.stage1 nn.Sequential( net.conv1, net.maxpool1, net.conv2, net.conv3, net.maxpool2, net.inception3a, net.inception3b ) self.stage2 nn.Sequential( net.maxpool3, net.inception4a, net.inception4b, net.inception4c, net.inception4d, net.inception4e ) self.stage3 nn.Sequential( net.maxpool4, net.inception5a, net.inception5b ) def forward(self, x): f1 self.stage1(x) # 300x300 输入 - 38x38通道 480 f2 self.stage2(f1) # 38x38 - 19x19通道 832 f3 self.stage3(f2) # 19x19 - 10x10通道 1024 return f1, f2, f3这段代码的关键是 stage 划分要和原网络前向顺序完全一致。注意net.maxpool1在 conv1 之后而非 inception3a 之后很多自己拼网络的人在这里栽过跟头导致特征图尺寸对不上。try/except是为了兼容 torchvision 0.13 前后预训练权重接口的变化旧版没有GoogLeNet_Weights枚举就回退到字符串。辅助分类器aux1、aux2不需要保留已经被排除在 stage 之外net.fc、net.avgpool、net.dropout也没有用到这部分就是纯特征提取。如果输入分辨率改成 384stage1 输出变为 48x48stage2 是 24x24stage3 是 12x12。GoogLeNet 的池化用了ceil_modeTrue所以奇数尺寸输入也能向上取整输出特征图尺寸不会掉得很快这给输入分辨率留下了灵活的调整空间。实测下来 384 输入对小型刀具的召回有明显帮助代价是耗时大约增加 1.6 倍。3.3 检测头、先验框和损失可复现的参数表特征提取完之后在三个 stage 上各接一组 3x3 卷积作为检测头。每组检测头输出两个分支坐标回归分支预测框的四个偏移量类别分支预测每个先验框是背景还是危险品。def make_head(in_channels, num_anchors, num_classes2): loc nn.Conv2d(in_channels, num_anchors * 4, kernel_size3, padding1) conf nn.Conv2d(in_channels, num_anchors * num_classes, kernel_size3, padding1) return loc, conf # stage1 输出 480 通道每格 4 个 anchor # stage2 输出 832 通道每格 6 个 anchor # stage3 输出 1024 通道每格 4 个 anchor h1_loc, h1_conf make_head(480, 4) h2_loc, h2_conf make_head(832, 6) h3_loc, h3_conf make_head(1024, 4)通道数对应的是每个 stage 最后的 Inception block 输出维度这个数字来自 GoogLeNet 论文结构不是随便拍的。每格 anchor 数量则和先验框设计挂钩。下表是我在危险品目标上常用的先验框参数按统计出来的 GT 框宽高分布调整过特征层特征图尺寸300输出通道min_sizemax_size长宽比stage138x3848016320.5, 1.0, 2.0stage219x1983248960.5, 1.0, 2.0, 3.0stage310x1010241122240.5, 1.0, 2.0, 3.0这里的 min_size/max_size 是经验值危险品里的小刀、打火机在 300 分辨率下通常只有 20 到 50 像素放在 stage1 负责中等刀具和瓶装液体在 50 到 150 像素交给 stage2大号压力罐、整瓶液体可以到 200 像素以上归 stage3。长宽比没有按刀具的 1:10 来设因为极端长宽比的 anchor 会引入大量难负样本实际收益很低。更稳妥的做法是用多尺度训练让网络自适应长条目标靠 stage1 的高分辨率特征而不是畸形 anchor。损失函数沿用 SSD 的设置坐标回归用 Smooth L1类别用交叉熵。危险品场景必须开难例挖掘否则负样本会把训练淹没。常见做法是在一个 batch 内按置信度损失排序强制负正样本比例不超过 3:1。多类别时再考虑 focal loss二分类阶段 OHEM 就够用。4. X 光危险品数据准备灰度三通道、标注转换与负样本策略4.1 X 光图和 ImageNet 差太多预训练权重要不要从头训X 光安检图拿到手通常是单通道灰度图密度高的金属区域发黑或发白材质不同还可能被伪彩色映射成橙色、绿色、蓝色。这和 ImageNet 里的自然图像分布差得很远所以很多人的第一反应是从零训练。我的建议正好相反除非你有几十万张标注图否则一定要用 ImageNet 预训练权重做微调因为 Inception 的低层卷积核学的是边缘、纹理这些通用结构对 X 光图同样有效。要让预训练权重真正生效第一步是把单通道灰度图复制成三通道而不是把网络首层改成单通道输入。torchvision 的 GoogLeNet 首层是Conv2d(3, 64, kernel_size7)改成单通道就得丢掉预训练权重重新学迁移效果大打折扣。复制三通道虽然物理意义上是冗余的但能让原始卷积核的统计分布尽量接近预训练时的输入分布。import numpy as np def xray_to_rgb(gray_np): # 输入 HxW 的灰度图输出 HxWx3保持像素强度范围 0-255 return np.stack([gray_np] * 3, axis-1) def gamma_augment(img, gammaNone): if gamma is None: gamma np.random.uniform(0.7, 1.5) return (img / 255.0) ** gamma * 255.0第一段代码是灰度转三通道的固定处理所有训练和推理输入都走这一步。第二段是随机 gamma 扰动模拟不同安检机射线强度差异。X 光图的对比度随设备型号差异很大同一把刀在 A 设备上清晰、在 B 设备上发灰gamma 增强是成本最低的抗设备差异手段。建议 gamma 范围取 0.7 到 1.5太极端会让图像物理密度信息失真。训练时还有一个细节前几个 epoch 只让检测头学习把 backbone 冻住。因为检测头是随机初始化的如果一开始就解冻 backbone大梯度会把预训练权重冲乱。等检测头对 X 光特征分布有基本响应后再全网络用小学习率微调。这个流程比从头训省一半以上时间稳定很多。4.2 从 VOC 标注到先验框尺寸危险品的锚框参数怎么定危险品检测的标注格式最常见的是 VOC 的 xml每张图对应一个文件记录目标的类别和边界框。转 SSD 训练格式时不需要改标注内容只需要把 xml 解析成数组再按网络输入分辨率缩放坐标。重要的是不要盲目套默认 anchor先统计一遍 GT 框的宽高分布再用统计结果指导先验框参数。import numpy as np # boxes 是归一化到 0-1 的数组形状 N x 4每个框为 (x1, y1, x2, y2) w boxes[:, 2] - boxes[:, 0] h boxes[:, 3] - boxes[:, 1] print(中位宽高:, np.median(w), np.median(h)) print(宽高比分位数:, np.percentile(w / (h 1e-6), [10, 50, 90]))这段代码输出目标的典型尺寸和长宽比分布。拿结果对照上一章的参数表如果中位宽高比在 1.5 到 3.0 之间说明目标以瓶罐类为主anchor 里的 0.5 可以删掉如果 90 分位超过 5.0说明有大量长条刀具需要在 stage1 或 stage2 增加长宽比 4.0 的先验框。危险品检测的 anchor 设计不是玄学完全可以用统计数字替代拍脑袋。还要注意一个容易忽略的点原图分辨率往往很高安检机出来的图常见单边 1000 像素以上而训练输入只有 300 到 512。如果直接整图缩放小目标会缩没。常见做法是把大图切成有重叠的 patch让 GT 框完整的 patch 参与训练目标跨 patch 的部分直接丢弃或做裁剪时保留。patch 尺寸和步长建议按目标中位尺寸的三倍来定这样能保证每个目标至少在一个 patch 里占比合理。4.3 负样本策略安检数据里 99% 是“安全物品”危险品检测最大的数据问题是类别极端不均衡。一个正常航班过包流里危险品出现的频率可能不到千分之一负样本量大到足够淹没模型。如果直接随机采样背景 patch模型很快退化成“所有区域都预测为背景”。我常用的策略分三层。第一层是随机负样本也要有选择不要在纯黑或纯平的袋子区域切 patch优先切包含锁扣、水瓶、雨伞骨架、笔记本结构件的区域让负样本在纹理复杂度上和危险品接近逼着模型学语义而不是学纹理密度。第二层是困难负样本挖掘第一个 epoch 训练完后把模型误判为危险品的背景 patch 收集起来作为第二个 epoch 的额外负样本。这个方法对降低误检非常直接代价是要多存一批 patch。第三层是控制一个 batch 内的正负比例前面的 3:1 规则在数据加载层面就要保证不能只靠损失函数。数据增强方面X 光图适合做水平翻转、小幅旋转、cutout 和 mixup。上下翻转要谨慎虽然行李放进去方向随意但有些密度结构的物理含义会和方向绑定比如压力罐的阀门位置上下翻转后语义仍然成立但可能会引入不必要的特征混淆我的习惯是不做上下翻转水平翻转和旋转各 10 度以内足够。5. 危险品检测训练避坑5 个让我返工的通病5.1 小目标漏检刀尖只有几个像素深层特征图救不回来现象中等以上刀具都能检出但小折叠刀、刀尖部分频繁漏掉。原因GoogLeNet 的 stage3 在原图上做了 16 倍下采样一个 10 像素的刀尖映射到特征图上连一个完整像素都占不满语义信息早就被卷积稀释完了。指望深层特征图找回小目标是违背信息论的做法。解决输入分辨率从 300 提到 384 或 512小目标在特征图上能多占几个像素代价是训练和推理速度下降但安检场景通常能接受。同时把小目标检测主要压在 stage1 的高分辨率特征图上让 stage1 的 anchor 多覆盖 10 到 30 像素区间。还有一个有效手段是把 IoU 匹配阈值从小目标专用的 0.5 降到 0.4因为小目标框稍微偏一点 IoU 就掉很多0.5 的阈值会让它匹配不到任何正样本永远学不出来。提示输入分辨率不是越高越好超过 512 后小目标收益明显变缓但推理时间线性上涨。建议先用 384 做实验看小目标召回是否达标再决定要不要升到 512。5.2 NMS 把堆叠的两把刀删成一个框现象包里同时放了两把叠在一起的刀模型明明输出了两个高置信度框NMS 之后只剩一个甚至置信度较低的那个被直接删掉。原因X 光图像是透视投影多个危险品在投影方向天然重叠两个框的 IoU 很容易超过 0.45。标准 NMS 的逻辑是抑制同类别的高 IoU 框在堆叠场景里它就是漏检的元凶。解决把标准 NMS 换成 Soft-NMSsigma 取 0.5 左右重叠框的置信度按 IoU 衰减而不是直接置零。另一个工程技巧是只做类内抑制不做跨类别全局抑制危险品之间类别各不相同跨类别抑制会把相邻的刀和液体同时删掉。最后要接受一个现实堆叠目标在算法层面很难完美恢复后端的复检流程比模型的框更可靠提示员看到高置信度单框时要留意周围区域而不是只盯着框看。5.3 负样本爆炸模型退化成所有区域都预测为背景现象Loss 不降验证集上 precision 很高但 recall 极低预测结果几乎全是背景。原因负正样本比例可能超过 100:1模型发现把所有位置都预测成背景就能拿到极低的损失梯度被负样本主导正样本学不到东西。这个现象在危险品场景尤其严重因为危险品在数据里是真的稀。解决开启 OHEM每个 batch 按置信度损失排序只取损失最高的负样本参与反向传播把负正比压到 3:1 以内。同时把类别分支的 bias 初始化成负样本先验让模型一开始就不至于太激进。prior_prob 0.995 # 负样本先验按数据统计 bias -np.log((1 - prior_prob) / prior_prob) conf.bias.data.fill_(bias)这段代码是 SSD 里常用的 bias 初始化 trick。prior_prob设成 0.995意思是模型初始化时对每个 anchor 预测为背景的概率约 99.5%对应危险品极稀疏的现实。这样避免了训练前几个 step 因为随机初始化输出过高置信度导致梯度爆炸。实际值根据训练集负样本占比微调如果危险品占比更低可以提到 0.999。5.4 预训练权重在 X 光图上“失效”的假象现象加载 ImageNet 权重后在 X 光图上直接推理一个危险品都检不出来看起来预训练完全没用。原因这里有一个假象预训练权重确实有效失效的是检测头和输入分布。检测头是随机初始化的从来没有见过 Inception 在 X 光输入上产生的特征分布直接推理当然输出垃圾。另外输入如果是单通道直接进网络首层卷积核的统计假设被打破特征提取本身就已经偏了。解决严格走前面的流程单通道复制三通道随机 gamma 增强先冻结 backbone 训练检测头 1 个 epoch再解冻全模型小学习率微调。我见过很多人踩这个坑后转头从零训练结果更差掉进“预训练失效”的陷阱出不来。5.5 推理延迟忽然飙升BN 在训练和推理模式没切现象训练时很流畅到了部署环境单帧推理时间从 40ms 抖到 200ms且不稳定。原因PyTorch 的 BatchNorm 层默认在训练模式下会计算当前 batch 的均值和方差并更新 running stats如果部署脚本忘记调用model.eval()BN 的统计计算会造成严重的延迟抖动而且推理结果也不对。解决部署前确认模型处于 eval 模式所有 BN 层的track_running_stats保持默认开启不要在推理时更新统计量。进一步的做法是把 BN 层 fold 进前面的卷积层减少一次内存访问。这个操作在导出 ONNX 时大部分框架会自动完成所以最稳的路径是直接走 ONNX 导出而不是在 PyTorch 里裸推理。6. 部署前最后一关ONNX 导出把延迟压进 100ms6.1 ONNX 导出与动态轴模型训完不是直接把 PyTorch 权重放到工控机上跑我一般会先导出 ONNX用 ONNX Runtime 做 CPU 推理。这一步的好处是自动完成 BN 折叠、算子融合还能利用 CPU 的 AVX 指令集。导出时要注意输入尺寸的选择如果安检机图像尺寸固定就填固定尺寸导出优化器能做更多常量折叠如果过包图像大小不固定就开动态轴。model.eval() dummy torch.randn(1, 3, 384, 384) torch.onnx.export( model, dummy, danger.onnx, input_names[xray], output_names[boxes, conf], dynamic_axes{xray: {0: batch, 2: H, 3: W}}, opset_version12 )这段导出的关键参数是dynamic_axes只动态化 batch、H、W 三个维度channel 保持 3 固定。开了动态 H/W 之后后端要自己按实际特征图尺寸重算 anchor代码会复杂一些如果业务允许统一缩放到 384建议去掉dynamic_axes推理稳定性更好。opset 版本建议不低于 12太低会缺一些算子融合优化。6.2 阈值校准按误报率而不是按置信度拍脑袋模型输出的置信度阈值不是拍脑袋设的安检业务的考核指标通常是“每万件误报不超过几件”这需要拿验证集完整跑一遍画 PR 曲线然后按误报预算反推阈值。我的习惯是分类别扫描置信度从 0.3 到 0.9 每 0.05 扫一次记录每个阈值下的召回率和误报数选满足业务红线的最高阈值。不同类型目标的阈值差异很大刀具这类高价值目标阈值可以放低液体这类容易误报的类别阈值要拉高。目标类型初始阈值复检策略刀具0.40单类人工复检枪支/部件0.50双人复核瓶装液体0.75密度直方图复核这个表是我在项目里的习惯设定不是通用答案。液体误报极高因为 X 光图里任何密度均匀的椭圆区域都像瓶子阈值放到 0.75 能压掉大量误报刀具误报会直接变成安全事故阈值放低到 0.40 换取高召回用后端人工复检兜底。阈值校准这件事要反复做每个现场设备的光源和传送带速度不同置信度分布都会有偏移。我的习惯是把阈值校准写进验收流程设备上线前先在现场采集一上午真实过包数据跑完 PR 曲线确认误报率达标再交付。模型训练的终点不是 loss 收敛而是在目标设备上稳定跑过验收标准。每台设备的光源老化和传送带噪声都会改变输入分布定期用最近一周的误报切片重训一轮比调网络结构稳妥得多。这些细节里最值钱的往往不是模型本身而是把数据和业务红线对齐的流程。希望这些经验能帮你少走一圈弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站