危险品分类数据集实战:从零训练可落地识别模型
发布时间:2026/9/26 1:53:41来源:尧图网络
简介危险品分类数据集面向工业安全监控、化学品运输仓储与安防智能化场景为需要构建危险品标志自动识别模型的目标检测开发者提供可直接训练的数据支撑。数据集共506张实际场景JPEG图片按447张训练集、37张验证集、22张测试集划分覆盖爆炸物、易燃气体、非易燃无毒气体、氧化剂四类核心危险品标注采用YOLO格式包含边界框与类别标签可直接接入YOLOv5/v8、Faster R-CNN等主流框架。压缩包共1014个文件以506个jpg图像与506个txt标注文件为主另含1个yaml数据配置和1份docx说明文档整体约29.84MB目录结构清晰便于快速配置训练与验证流程。图片来自多样实际环境涵盖不同角度、光照与背景有助于提升模型在复杂场景下的鲁棒性与泛化能力。目前已有228人学习下载适合从事安全生产、危险品管理及工业视觉检测的开发者参考使用。1. 危险品分类数据集从零训练一个能落地的危险品识别模型如果你手头正好有一个「危险品分类数据集.zip」想用它训练一个能识别危险品标志、区分危险品类别的视觉模型那这篇笔记就是为你写的。危险品分类这件事在仓储巡检、物流分拣、化工园区监控里都是刚需——模型要能认出腐蚀品、易燃液体、氧化剂这些标志还要能区分不同危险等级。但真正动手时你会发现数据集拿到手只是第一步类别不均衡、标注质量参差、小目标标志识别率低这些坑一个都跑不掉。我做过几个类似项目从数据清洗到模型部署踩了不少血泪坑这里把完整路径拆开讲清楚适合刚拿到数据集想跑通 baseline 的新手也适合想优化现有方案的熟手。2. 先搞清楚危险品分类数据集里到底有什么2.1 危险品分类的类别体系与标注格式危险品分类不是简单的二分类问题。按照常见的危险货物分类标准至少涉及爆炸品、压缩气体、易燃液体、易燃固体、氧化剂、毒害品、放射性物品、腐蚀品等大类每个大类下还有细分。你拿到的数据集大概率是两种形态之一一种是按文件夹分好的图像分类数据集每个子文件夹是一个类别另一种是目标检测格式每张图有对应的标注文件标注了危险品标志的位置和类别。先别急着写训练脚本花十分钟把数据结构摸清楚。用下面这段脚本统计类别分布和图像尺寸这是后续所有决策的基础import os import json from collections import Counter from PIL import Image def inspect_dataset(root_dir): 统计分类数据集的类别分布和图像基本信息 class_counts Counter() size_stats [] for class_name in os.listdir(root_dir): class_dir os.path.join(root_dir, class_name) if not os.path.isdir(class_dir): continue for img_file in os.listdir(class_dir): if not img_file.lower().endswith((.jpg, .jpeg, .png, .bmp)): continue class_counts[class_name] 1 img_path os.path.join(class_dir, img_file) try: with Image.open(img_path) as im: size_stats.append(im.size) except Exception as e: print(f损坏文件: {img_path}, 错误: {e}) print(类别分布:) for cls, cnt in class_counts.most_common(): print(f {cls}: {cnt}) if size_stats: widths [s[0] for s in size_stats] heights [s[1] for s in size_stats] print(f\n图像尺寸范围: 宽 {min(widths)}-{max(widths)}, 高 {min(heights)}-{max(heights)}) print(f平均尺寸: {sum(widths)//len(widths)} x {sum(heights)//len(heights)}) return class_counts # 用法替换成你的数据集路径 inspect_dataset(./hazardous_dataset/train)这段脚本做三件事遍历每个类别文件夹统计样本数、检查图像文件是否损坏、汇总图像尺寸分布。类别分布直接决定你后面要不要做重采样尺寸分布决定输入分辨率怎么设。如果发现某个类别只有几十张图而最多的类别有几千张那类别不均衡就是你要面对的第一个问题。2.2 判断数据集适合分类还是检测任务这一步很多人会跳过但选错任务类型后面全白做。判断依据很简单看标注文件。如果只有类别文件夹没有位置标注那就是纯分类任务模型只需要回答「这张图里是什么危险品标志」。如果有 XML、JSON 或 TXT 格式的标注文件里面包含边界框坐标那就可以做检测任务模型能同时告诉你「是什么」和「在哪里」。检测任务的价值在于实际部署时一张监控画面里可能有多个危险品标志分类模型只能给整张图一个标签检测模型能框出每一个。但检测任务对标注质量要求高得多标注框偏移几个像素可能影响不大但漏标、错标会直接拉低模型性能。我一般会先跑一个分类 baseline 验证数据质量再决定要不要上检测。import xml.etree.ElementTree as ET import glob def check_annotation_quality(anno_dir): 检查VOC格式标注文件的质量 xml_files glob.glob(os.path.join(anno_dir, *.xml)) if not xml_files: print(未找到XML标注文件可能是纯分类数据集) return empty_files [] tiny_boxes [] class_counter Counter() for xml_file in xml_files: tree ET.parse(xml_file) root tree.getroot() objects root.findall(object) if not objects: empty_files.append(xml_file) continue for obj in objects: cls_name obj.find(name).text class_counter[cls_name] 1 bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) w, h xmax - xmin, ymax - ymin if w 10 or h 10: tiny_boxes.append((xml_file, cls_name, w, h)) print(f标注文件总数: {len(xml_files)}) print(f空标注文件数: {len(empty_files)}) print(f过小标注框数: {len(tiny_boxes)}) print(类别分布:) for cls, cnt in class_counter.most_common(): print(f {cls}: {cnt}) check_annotation_quality(./hazardous_dataset/annotations)空标注文件和过小标注框是检测任务的两个主要噪声源。空标注意味着这张图里没有目标如果直接拿来训练会被当成负样本但如果是漏标就会误导模型。过小标注框通常是标注人员手抖或者目标本身太小训练时可以考虑过滤掉或者用专门的小目标增强策略。3. 用迁移学习跑通第一个危险品分类模型3.1 数据增强策略与类别不均衡处理危险品标志的特点是形状规整、颜色鲜明但实际场景中会遇到光照变化、遮挡、拍摄角度倾斜等问题。数据增强要针对这些真实场景来设计而不是随便套几个变换。我常用的增强组合是随机旋转 ±15 度、随机亮度对比度调整、随机裁剪缩放、轻微透视变换。注意不要用水平翻转——有些危险品标志有方向性翻转后语义就变了。类别不均衡的处理优先级先试加权采样再试损失函数加权最后才考虑过采样。过采样容易导致过拟合尤其是当少数类样本本身多样性不足时。import torch from torch.utils.data import WeightedRandomSampler from torchvision import transforms # 训练集增强针对危险品标志的实际场景 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomRotation(15), # 拍摄角度倾斜 transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), # 光照变化 transforms.RandomAffine(degrees0, translate(0.1, 0.1), scale(0.9, 1.1)), # 位置偏移 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集只做必要的尺寸调整和归一化 val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def make_weighted_sampler(class_counts, num_samples): 根据类别样本数生成加权采样器缓解类别不均衡 class_weights {cls: 1.0 / cnt for cls, cnt in class_counts.items()} sample_weights [] for cls, cnt in class_counts.items(): sample_weights.extend([class_weights[cls]] * cnt) sampler WeightedRandomSampler( weightssample_weights, num_samplesnum_samples, replacementTrue ) return samplerRandomRotation(15)模拟拍摄角度偏差ColorJitter应对不同光照条件RandomAffine的平移和缩放参数模拟拍摄距离和位置变化。归一化用的 ImageNet 均值方差因为后面要用预训练模型。加权采样器的逻辑是给少数类更高的采样概率让每个 batch 里各类别大致均衡。3.2 基于 ResNet 的迁移学习训练脚本选 ResNet50 作为 backbone 是因为它在中小规模数据集上表现稳定预训练权重容易获取而且推理速度在工业场景可接受。如果你追求更高精度可以换 EfficientNet但要注意输入分辨率匹配。import torch.nn as nn import torch.optim as optim from torchvision import models def build_model(num_classes, freeze_backboneTrue): 构建基于ResNet50的迁移学习模型 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) if freeze_backbone: # 冻结除layer4和fc外的所有层 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False # 替换分类头 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, num_classes) ) return model def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in dataloader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc # 训练配置 device torch.device(cuda if torch.cuda.is_available() else cpu) num_classes 9 # 根据你的数据集类别数修改 model build_model(num_classes, freeze_backboneTrue).to(device) # 分阶段学习率分类头用大学习率微调的backbone层用小学习率 optimizer optim.AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer4.parameters(), lr: 1e-4}, ], weight_decay1e-4) criterion nn.CrossEntropyLoss(label_smoothing0.1) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)freeze_backboneTrue时只训练 layer4 和分类头适合数据集规模在几千张以内的场景。如果数据量超过一万张可以解冻更多层做全量微调。优化器用了分组学习率分类头 1e-3layer4 用 1e-4这是因为预训练特征已经很好大学习率会破坏原有特征。label_smoothing0.1是防止模型对标注噪声过拟合危险品数据集里标注错误在所难免。训练轮数建议先跑 30 轮看收敛曲线如果验证集准确率还在涨就继续跑。早停策略设 patience7连续 7 轮验证集指标不提升就停。4. 危险品检测任务的标注转换与训练调参4.1 把 VOC 标注转成 YOLO 格式的完整脚本如果你的数据集是 VOC 格式的 XML 标注想用 YOLO 系列模型训练就需要做格式转换。转换的核心是把绝对坐标的 xmin/ymin/xmax/ymax 转成归一化的中心点坐标和宽高。这个转换看起来简单但有几个边界坑坐标越界、类别名映射不一致、图像尺寸和标注尺寸不匹配。import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(voc_anno_dir, image_dir, output_dir, class_list): 将VOC格式标注转换为YOLO格式 class_list: 类别名称列表顺序决定类别索引 os.makedirs(output_dir, exist_okTrue) class_to_id {name: idx for idx, name in enumerate(class_list)} converted 0 skipped 0 for xml_file in os.listdir(voc_anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(voc_anno_dir, xml_file)) root tree.getroot() # 获取图像尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) # 验证图像文件是否存在且尺寸一致 img_name os.path.splitext(xml_file)[0] .jpg img_path os.path.join(image_dir, img_name) if not os.path.exists(img_path): skipped 1 continue with Image.open(img_path) as im: actual_w, actual_h im.size if actual_w ! img_w or actual_h ! img_h: # 以实际图像尺寸为准 img_w, img_h actual_w, actual_h yolo_lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in class_to_id: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 坐标裁剪到图像范围内 xmin max(0, min(xmin, img_w)) ymin max(0, min(ymin, img_h)) xmax max(0, min(xmax, img_w)) ymax max(0, min(ymax, img_h)) # 过滤无效框 if xmax xmin or ymax ymin: continue # 转YOLO格式中心点x, 中心点y, 宽, 高均归一化 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h cls_id class_to_id[cls_name] yolo_lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if yolo_lines: out_file os.path.join(output_dir, os.path.splitext(xml_file)[0] .txt) with open(out_file, w) as f: f.write(\n.join(yolo_lines)) converted 1 else: skipped 1 print(f转换完成: {converted} 个文件, 跳过: {skipped} 个) # 用法 class_list [explosive, gas, flammable_liquid, flammable_solid, oxidizer, toxic, radioactive, corrosive, misc] voc_to_yolo(./annotations, ./images, ./labels, class_list)坐标裁剪那几行是关键有些标注文件里 xmax 会超出图像宽度不裁剪的话归一化后值大于 1YOLO 训练时会报错或者产生异常梯度。类别名映射要确保和你的 data.yaml 里一致顺序错了模型学出来的类别就是乱的。4.2 YOLOv8 训练危险品检测模型的关键参数YOLOv8 的训练配置里有几个参数对危险品检测影响特别大。输入分辨率 imgsz 建议设 640如果危险品标志在图中占比很小可以提到 1280但显存占用会翻倍。batch size 根据显存来16 或 32 都行。学习率 lr0 初始值设 0.01配合 cos 调度。# data.yaml path: ./hazardous_dataset train: images/train val: images/val nc: 9 names: [explosive, gas, flammable_liquid, flammable_solid, oxidizer, toxic, radioactive, corrosive, misc]# 训练命令 yolo detect train \ data./data.yaml \ modelyolov8m.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience15 \ augmentTrue \ mosaic1.0 \ mixup0.1 \ degrees10.0 \ translate0.1 \ scale0.5 \ fliplr0.0 \ project./runs/hazardous \ nameexp1fliplr0.0关掉水平翻转原因和分类任务一样危险品标志翻转后语义变了。mosaic1.0是 YOLOv8 默认开启的马赛克增强对小目标检测有帮助。mixup0.1轻度混合增强防止过拟合。patience15是早停轮数验证集 mAP 连续 15 轮不提升就停。训练完成后重点看混淆矩阵危险品分类里最容易混的是氧化剂和易燃固体因为标志颜色和形状有相似之处。如果这两类混淆严重可以考虑在损失函数里给它们更高的权重或者专门补充这两类的难例样本。5. 危险品分类模型落地时的避坑与排查5.1 标注噪声导致的模型性能天花板现象训练集准确率能到 95% 以上但验证集准确率卡在 70% 左右上不去loss 曲线震荡明显。原因危险品数据集的标注噪声比通用数据集高得多。标注人员对危险品分类标准不熟悉时容易把氧化剂标成易燃固体或者把某个子类归到错误的父类。这种系统性标注错误不是靠调参能解决的。解决先做一轮标注清洗。具体做法是用一个在干净子集上训练的模型去预测全量数据把预测置信度高但和标注不一致的样本挑出来人工复核。我一般会挑出 top 200 个不一致样本通常能发现 30-50 个真正的标注错误。清洗后再训练验证集准确率能提升 5-10 个百分点。5.2 小目标危险品标志的漏检问题现象大尺寸的危险品标志检测没问题但图像边缘或者远处的标志经常漏检mAP 在小目标上明显偏低。原因默认的 640 输入分辨率下小目标经过 backbone 下采样后特征几乎消失。另外 YOLO 的 anchor 匹配策略对小目标也不友好。解决三个方向同时下手。第一提高输入分辨率到 1280让更多细节保留。第二在数据增强里加随机缩放让模型见过各种尺度的目标。第三如果漏检集中在某个类别检查这个类别的标注框是不是普遍偏小必要时用 SAHI 切片推理做部署时的补救。5.3 类别不均衡引发的少数类召回率崩塌现象样本量大的类别 F1 能到 0.9但样本量少的类别 F1 只有 0.3-0.4模型几乎不预测少数类。原因交叉熵损失下模型倾向于预测多数类就能获得较低 loss少数类的梯度信号被淹没。解决分类任务用 WeightedRandomSampler 做均衡采样检测任务在 YOLO 的损失里给少数类更高的 box loss 和 cls loss 权重。另外可以针对少数类做定向数据增强比如对少数类样本做更多的旋转、裁剪、颜色变换增加其多样性。如果少数类样本实在不够考虑用生成模型合成一些但合成样本要人工筛选质量差的合成样本反而有害。5.4 部署时预处理不一致导致的精度下降现象训练时验证集准确率 85%部署到实际场景后准确率掉到 60% 以下。原因训练时的预处理和推理时的预处理不一致。常见的不一致包括训练用了归一化但推理忘了、训练 resize 用了双线性插值但推理用了最近邻、训练时 BGR 推理时 RGB。解决把预处理逻辑封装成一个独立的类或函数训练和推理共用同一份代码。部署前用同一张测试图分别跑训练 pipeline 和推理 pipeline对比输出是否一致。这个检查花不了十分钟但能避免上线后精度莫名其妙下降的玄学问题。5.5 危险品标志相似类别的混淆现象氧化剂和有机过氧化物混淆腐蚀品和毒害品混淆模型在两个相似类别之间反复横跳。原因这两类危险品标志在颜色和图案上确实相似尤其是低分辨率下。另外训练数据里如果这两类的样本场景分布不同模型可能学到的是场景偏差而不是标志本身的特征。解决在分类头之前加一个注意力模块让模型聚焦在标志区域而不是背景。另外可以引入层次分类策略先分大类再分小类降低单次分类的难度。如果混淆严重考虑收集这两类的对比样本做难例挖掘训练。6. 用混淆矩阵和置信度分布验证模型是否真的可用训练完模型不是看一个准确率数字就完事了。我一般会做两件事来验证模型是否真的能落地一是画混淆矩阵看错误集中在哪些类别对二是看置信度分布判断模型的决策边界是否清晰。混淆矩阵用 sklearn 几行就能画出来重点看非对角线上的数值。如果某个类别对的混淆数量超过该类样本总数的 15%就需要针对性处理。置信度分布则用直方图看正确预测的样本置信度应该集中在 0.8 以上错误预测的样本置信度如果也集中在 0.7-0.9 区间说明模型对自己的错误很自信这种模型部署后风险很大。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay def analyze_model_errors(model, dataloader, device, class_names): 分析模型错误分布和置信度 model.eval() all_preds [] all_labels [] all_probs [] with torch.no_grad(): for images, labels in dataloader: images images.to(device) outputs model(images) probs torch.softmax(outputs, dim1) _, preds outputs.max(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) all_probs.extend(probs.cpu().numpy()) all_preds np.array(all_preds) all_labels np.array(all_labels) all_probs np.array(all_probs) # 混淆矩阵 cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(cm, display_labelsclass_names) disp.plot(xticks_rotation45) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) # 置信度分布正确 vs 错误 correct_mask all_preds all_labels correct_conf all_probs[correct_mask].max(axis1) wrong_conf all_probs[~correct_mask].max(axis1) print(f正确预测置信度均值: {correct_conf.mean():.3f}) print(f错误预测置信度均值: {wrong_conf.mean():.3f}) print(f错误预测中置信度0.8的比例: {(wrong_conf 0.8).mean():.2%}) # 找出最自信的错误样本 wrong_indices np.where(~correct_mask)[0] if len(wrong_indices) 0: wrong_confs all_probs[wrong_indices].max(axis1) top_wrong wrong_indices[np.argsort(wrong_confs)[-10:]] print(\n最自信的10个错误预测:) for idx in top_wrong: print(f 真实: {class_names[all_labels[idx]]}, f预测: {class_names[all_preds[idx]]}, f置信度: {all_probs[idx].max():.3f}) return cm # 用法 class_names [explosive, gas, flammable_liquid, flammable_solid, oxidizer, toxic, radioactive, corrosive, misc] cm analyze_model_errors(model, val_loader, device, class_names)这段代码输出三个关键信息混淆矩阵图、正确和错误预测的置信度均值对比、最自信的十个错误样本。如果错误预测的置信度均值超过 0.7说明模型在犯错时也很自信这种模型直接部署风险很高需要做置信度校准或者引入拒识机制。最自信的错误样本一定要人工看一遍通常能发现标注错误或者数据分布问题。我自己的习惯是每次训练完新模型先跑这个分析脚本把最自信的错误样本导出来人工过一遍。这个习惯帮我发现过好几次标注错误也让我对模型的真实能力边界心里有数。危险品分类这件事模型说「我确定」的时候不一定真确定但模型说「我不确定」的时候通常是真的不确定利用好这个特性做分级预警比单纯追求准确率更有实际价值。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网