ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

森林火灾图像分类实操:13,000张标注数据的训练与避坑指南

森林火灾图像分类实操:13,000张标注数据的训练与避坑指南 简介一套约13000张已标注的森林火灾图像分类数据集围绕“有火/无火”二分类任务构建面向计算机视觉与深度学习方向的研究人员、算法工程师以及森林火情智能监测相关项目的开发者。压缩包内共2000个文件以1998张jpg图像为主体另含1个py可视化脚本与1个json标注配置文件整体大小约338.73MB。数据集已预先划分训练集与测试集按类别分别存放训练集用于模型特征学习测试集便于检验泛化能力可直接进入分类模型训练与评估流程。json文件明确了两个类别定义运行附带的show脚本可直观浏览各类样本图像文件采用统一编号命名便于批量处理与二次划分。资源结构清晰既适合作为火灾图像分类与网络改进实验的数据基础也可配合完整视觉项目流程进行算法验证与效果优化目前已有439人学习下载。1. 森林火灾图像分类数据集13,000张已标注图能做的事与不能做的事森林火灾图像分类数据集说白了就是给你一个已经标好类别的文件夹里面约 13,000 张图像每张图要么是火、要么是烟、要么是正常林地。图像分类这个方向现在不缺模型缺的是干净可复现的数据集。刚拿到这类数据的人通常会犯两个毛病要么不盘点直接开训要么只盯着准确率结果发现部署后根本不敢用。我会按一线工程习惯从数据盘点、划分、训练、避坑一直聊到置信度阈值目标是让你照着做一遍就能产出一个可信的基线模型。它适合谁适合正在做火灾预警、林区监控或者相关图像识别项目手上有这个数据集但不知道怎么训练的工程师也适合想用一个小型真实场景数据跑通图像分类全流程的入门者。先说结论13,000 张足够用迁移学习微调出一个可用的分类器真正的瓶颈在标注噪声和验证集划分方式不在模型。2. 动手前先盘点数据类别分布、标注格式与质量检查图像分类数据集和检测数据集不一样检测要读 bounding box分类只需要图片和标号。但这个“标号”具体存在哪里影响后面所有代码。森林火灾数据集常见的组织方式有两种一是按文件夹名分好类例如fire/、smoke/、normal/二是给你一个 CSV/JSON里面写image_path,label两列。第一种用torchvision.datasets.ImageFolder三行就能读第二种要自己写 Dataset。我拿到任何数据集第一件事不是建模而是先写个脚本把底数摸清每一类有多少张、有没有损坏图片、分辨率是否统一。这是后面所有指标的地基。2.1 先别急着训练用脚本盘点类别和图片状态这个脚本不复杂但会避免你后续在训练到一半时因为一张损坏的图片而翻车。我会先列目录、统计每个子目录的图片数量然后用 PIL 的verify()检查文件头是不是完整。图像分类数据集最容易出现的问题不是模型选型而是数据里混着零字节文件或截断的 JPEGPyTorch 的 DataLoader 一读到就直接炸。import os from collections import Counter from PIL import Image data_root ./forest_fire_dataset # 1. 列出所有子目录把目录名当作类别 class_dirs [d for d in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, d))] counts Counter() # 2. 统计每个类别的图片数 for c in class_dirs: imgs [f for f in os.listdir(os.path.join(data_root, c)) if f.lower().endswith((.jpg, .jpeg, .png, .bmp))] counts[c] len(imgs) print(类别分布, dict(counts)) # 3. 检查损坏图片 bad_images [] for c in class_dirs: for f in os.listdir(os.path.join(data_root, c)): p os.path.join(data_root, c, f) if not f.lower().endswith((.jpg, .jpeg, .png, .bmp)): continue try: with Image.open(p) as img: img.verify() except Exception as e: bad_images.append((p, str(e))) print(损坏图片数量, len(bad_images)) for p, err in bad_images[:5]: print(损坏文件, p, err)这段脚本的逻辑分三步先把目录名当作类别名统计每个目录下合法后缀的图片数量然后用PIL.Image.verify()检查图片文件能否被解码注意verify()只会检查文件完整性不会把整张图加载进内存速度很快最后把损坏文件列出来数量多的话需要找数据集提供方补或者直接删除。跑完之后你应该得到类似{fire: 5200, smoke: 3100, normal: 4700}的分布。如果某一类只有几百张后面训练时要考虑类别权重。如果损坏图片超过几十张建议先修数据再做任何训练因为哪怕只有一张坏图混进ImageFolderDataLoader可能随机崩溃且不好定位这种“随机出现、换一个 epoch 又好了”的现象最浪费时间。2.2 标注格式的两种常见形态目录即标签与 CSV 映射确认好数据完整后就要决定怎么把图片和标签喂给模型。绝大多数公开分类数据集会直接给你目录结构这时没有必要自己写 Dataset直接用torchvision.datasets.ImageFolder最省事。它的底层逻辑是每个子目录名对应一个类别内部图片按字母序排类别索引按目录名排序生成。一个容易踩的细节是如果目录是normal, fire, smoke那么class_to_idx会是{fire: 0, normal: 1, smoke: 2}因为字母序f n s所以图像分类时千万不要想当然地认为“第一个目录就是第 0 类”。如果数据集提供的是 CSV那就必须自己写一个轻量 Dataset。常见格式是image_path,labellabel 可能是数字也可能是字符串字符串需要先映射成整数因为CrossEntropyLoss只接受整数标签。import os import pandas as pd from PIL import Image import torch from torch.utils.data import Dataset class FireCSVDataset(Dataset): 读取森林火灾 CSV 标注文件的自定义 Dataset 参数: csv_path: CSV 文件路径 img_root: 图片所在的根目录CSV 里的路径是相对这个根目录的 transform: torchvision transforms label_map: 字符串标签到整数标签的映射如 {normal: 0, fire: 1} def __init__(self, csv_path, img_root, transformNone, label_mapNone): self.df pd.read_csv(csv_path) self.img_root img_root self.transform transform self.label_map label_map or {} # 如果没有映射就按字母序自动生成 if not self.label_map: unique_labels sorted(self.df[label].unique()) self.label_map {lab: i for i, lab in enumerate(unique_labels)} def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path os.path.join(self.img_root, row[image_path]) image Image.open(img_path).convert(RGB) label_str row[label] label self.label_map[label_str] if isinstance(label_str, str) else int(label_str) if self.transform: image self.transform(image) return image, torch.tensor(label, dtypetorch.long)这个类只有三个方法__init__负责读 CSV 和建立字符串到整数的映射__len__返回样本数DataLoader 会用它算 batch 数量__getitem__按索引读图、读标签、做 transform最后返回(tensor_img, tensor_label)二元组。注意一个细节Image.open().convert(RGB)很有必要。很多森林火灾图像是 PNG可能带 alpha 通道直接ToTensor()会变成 4 通道输入模型第一层卷积就报错而且报错信息藏在不明显的位置。参数说明transform在第 3 章里展开label_map最好在外部定义好传进来不要依赖自动生成否则训练和推理时如果 CSV 的类别顺序变了模型输出索引就错位了。我的做法是在项目根目录固定一个labels.json存映射训练代码和部署代码都读它避免“训练时是 0fire推理时 0normal”这种黑匣子错误。3. 数据划分与预处理分层切分、尺度统一和增强策略数据盘点过后接下来就是划分训练集、验证集和测试集。很多做图像分类的工程师会在这一步偷懒直接shuffleTrue随机切但这个数据集只有 13,000 张类别分布又不一定均衡随机切最直接的后果是某一小类在验证集里只有几十张导致 F1 波动特别大。所以我会用分层抽样保证每个子集的类别比例和原始数据集一致。然后才是预处理和增强。顺序不要反先划分再做增强因为增强后的图像属于同一个样本的多个副本划分时不加处理会引入数据泄露。3.1 分层划分别让某一类全跑进测试集如果你的数据已经是ImageFolder最干净的方式是拿索引数组做划分。先把dataset.samples里的标签抽出来用sklearn.model_selection.train_test_split把stratify参数传成标签数组random_state固定。这一步的可复现性很重要后面无论调多少次参数对比的模型都面对同一个验证集。import numpy as np from sklearn.model_selection import train_test_split from torch.utils.data import Subset from torchvision import datasets dataset datasets.ImageFolder(./forest_fire_dataset, transformtrain_transform) idx np.arange(len(dataset)) labels [label for _, label in dataset.samples] # 和 dataset.targets 等效 train_idx, val_idx train_test_split( idx, test_size0.2, stratifylabels, # 保证类别比例一致 random_state42 ) train_dataset Subset(dataset, train_idx) val_dataset Subset(dataset, val_idx) print(训练集大小:, len(train_dataset), 验证集大小:, len(val_dataset))这里test_size0.2表示留出 20% 做验证集。如果你的最终目标是评估模型泛化能力建议再从训练集里留出 10% 做测试集即训练 72%、验证 18%、测试 10%。但很多项目只需要一个可信的验证集因为最后会用全量数据重新训练并部署。注意不要用“验证集”反复调阈值后还说它有最终泛化能力严格讲阈值也是该在验证集上学的想得到无偏指标请再切一个测试集。另一个重点Subset传入的是索引不是路径。它会去原dataset里按索引取样本所以train_transform和val_transform要提前设置好但Subset不会阻止你在训练时单独给子集换 transform。如果你在ImageFolder初始化时已经用了带增强的 transform验证集也跟着增强那验证指标就会不稳定。正确做法是先用不带 transform 的ImageFolder创建原始 dataset划分完再给训练集和验证集分别指定 transform或者干脆在Subset外面再包一层带 transform 的 Dataset。3.2 预处理与增强224 尺寸、ImageNet 归一化与有限的色彩增强图像分类的预处理有“标准答案”吗没有但迁移学习里有惯用套路把输入缩放到模型训练时的尺寸使用 ImageNet 的均值和标准差做标准化因为预训练权重的统计量就是基于 ImageNet 图像算出来的。13,000 张的森林火灾图像分辨率可能差异很大我一般会统一到 224×224这是 ResNet 和 EfficientNet 默认输入调换到 256 或 384 会显著增加显存占用收益却不一定大。from torchvision import transforms # 训练集增强不要让火的颜色失衡 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 验证集/测试集固定尺寸 中心裁剪 val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])参数说明RandomResizedCrop的scale(0.6, 1.0)表示裁剪区域占原图的 60% 到 100%比默认的(0.08, 1.0)保守因为火灾里真正起关键作用的往往是火或烟在画面中占比较大裁得太狠会把火苗裁掉。ColorJitter三个参数都只给了 0.2没有给hue这是因为色调翻转容易把“红火”变成“紫火”、“橙烟”变成“绿烟”在语义上制造错误样本。这是我在火灾项目里实际踩过坑后才收敛的参数。还有一点Normalize的三个通道对应 RGB不要觉得自己数据集是火灾图就要自己重算均值和方差。用 ImageNet 的统计量不影响模型收敛只是让输入分布接近预训练分布自己统计反而会让迁移学习起点偏移效果不稳定。当然如果你从头训练才需要按自己的数据算。3.3 视频帧数据集必须按场景分组划分这是森林火灾数据集的另一个特点很多图片不是独立拍摄的而是从监控视频中抽帧来的。如果同一段视频的相邻帧一半在训练集、一半在验证集那模型学到的其实是“像素级背景记忆”而不是“火灾外观特征”。验证集指标很好看一旦换一个林区就崩。判断方法很简单看文件名里有没有类似scene01_0001.jpg这种前缀有的话就要按场景分组。from sklearn.model_selection import GroupShuffleSplit import os idx np.arange(len(dataset)) labels [label for _, label in dataset.samples] groups [os.path.basename(p).split(_)[0] for p, _ in dataset.samples] gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(idx, labels, groups)) print(训练集包含哪些组, set([groups[i] for i in train_idx])) print(验证集包含哪些组, set([groups[i] for i in val_idx]))GroupShuffleSplit的核心是groups数组它把每个样本归到某个组划分时保证同一个组的样本全部进入同一个集合。如果你确认数据不是视频帧可以跳过这步如果不确定宁可花一分钟做组划分也不要为了简单随机切最后被数据泄露坑掉几个星期。这个问题在论文里常叫“数据泄漏”在工程里更形象的说法是“模型背答案”。4. 训练图像分类模型ResNet 与 EfficientNet 的迁移学习参数到了这个环节你手里已经有干净的训练集和验证集。选模型时不要被“最新的图像分类模型”带偏。13,000 张的规模Vision Transformer 和 ConvNeXt 虽然效果好但需要更大的数据量、更长的训练时间、更精细的调度对做火灾预警来说性价比不高。更关键的是绝大多数开源预训练权重是在 ImageNet-1K 上得到的而火灾图像和普通自然图像差异明显所以迁移学习的微调策略比选哪一版模型重要得多。我一般会在 ResNet18 和 EfficientNet-B0 之间选一个起步先把流程跑通再考虑更重的模型。4.1 模型选型为什么中小规模数据集更适合 ResNet/EfficientNet先给个对比表方便你按硬件条件选模型参数量相对输入尺寸训练速度我的使用建议ResNet18小224快流程验证、基线模型ResNet50中224/256中追求精度显存有余EfficientNet-B0小224中精度和速度均衡部署友好ViT-B/16大224慢不推荐13,000 张不够它吃这张表不是精确数据参数多少、速度多快你可以在自己机器上跑但选型逻辑是清楚的数据集只有 13,000 张模型容量太大不仅训练慢还容易过拟合。ResNet18 作为一个 3 层卷积堆叠的深度网络提取火灾火焰纹理、烟雾边缘已经足够EfficientNet-B0 用了神经架构搜索得到的缩放系数同样精度下 FLOPs 更小如果你的推理设备是边缘盒子它是更好的目标。ResNet50 能不能用能但如果你的训练集只有 6,000 张效果未必比 ResNet18 好多少反而显存翻倍。所以我建议先拿 ResNet18 跑通全流程再根据验证集 F1 决定要不要换 EfficientNet-B0 或 ResNet50。4.2 迁移学习训练脚本冻结骨干还是全量微调确定模型后直接加载 ImageNet-1K 预训练权重。PyTorch 从某个版本开始不推荐pretrainedTrue而是用weights参数这样能避免版本兼容性问题。加载后把最后的全连接层替换成我们的类别数。那么问题来了是冻结前面的卷积层只训练最后一层还是全部解开一起训练对 13,000 张森林火灾图我的建议是全量微调。理由很简单火灾图像和 ImageNet 里的自然图像分布差异大如果只训练最后的全连接层前面的特征提取器可能无法感知“烟雾的半透明性”和“火焰的高亮纹理”。当然全量微调也需要控制学习率初始学习率不要超过 1e-4否则预训练权重会被打得稀烂。import torch import torch.nn as nn import torch.optim as optim from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes 3 # 根据你的类别数改 model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) # 如果想冻结前两层做特征提取风格可以这样 # for name, param in model.named_parameters(): # if name.startswith(conv1) or name.startswith(layer1): # param.requires_grad False criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)参数说明lr1e-4是迁移学习微调的常见起点如果你用的是 SGD可以设lr1e-3同时加momentum0.9。AdamW 的weight_decay1e-4控制正则强度防止最后一层过拟合。T_max30要和你的训练总 epoch 数一致表示余弦退火从初始学习率降到 0 的周期。这里我把T_max30默认计划训 30 个 epoch。下面是最小训练循环。评估函数在 4.3 小节给出这里先留调用。train_loader torch.utils.data.DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, drop_lastTrue) val_loader torch.utils.data.DataLoader( val_dataset, batch_size64, shuffleFalse, num_workers4) epochs 30 best_acc 0.0 for epoch in range(epochs): model.train() total_loss 0.0 for images, labels in train_loader: images images.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) scheduler.step() avg_loss total_loss / len(train_dataset) # 调用 4.3 的 evaluate_model返回预测结果和概率 val_labels, val_probs, val_preds evaluate_model(model, val_loader, device, dataset.classes) acc (val_preds val_labels).mean() print(fEpoch {epoch:2d} | Loss {avg_loss:.4f} | Val Acc {acc:.4f})这个循环里值得注意的细节drop_lastTrue是为了防止最后一个 batch 样本数过少导致 BatchNorm 更新不稳定虽然 13,000 张未必能被 32 整除但建议加上。scheduler.step()放在每个 epoch 结束后而不是每个 batch 后CosineAnnealingLR 默认是 epoch 粒度更新的。如果你发现验证集 loss 在后期反弹可以把T_max调大些或者改用ReduceLROnPlateau它会在指标不涨时自动降学习率更适合真实项目。4.3 评估指标准确率会骗人用混淆矩阵和 F1 说话刚才代码里留了evaluate_model的缺口。火灾检测的本质是代价不对称的分类问题把火判成正常是重大事故把正常判成火只是一个小告警。准确率只统计“所有预测中答对的比例”但如果 80% 是正常类模型全判正常也能有 80% 准确率这对你没有任何意义。所以至少要输出每个类别的 precision、recall、F1以及混淆矩阵。from sklearn.metrics import classification_report, confusion_matrix import torch def evaluate_model(model, loader, device, class_names): model.eval() all_preds [] all_labels [] all_probs [] with torch.no_grad(): for images, labels in loader: images images.to(device) outputs model(images) probs torch.softmax(outputs, dim1) preds torch.argmax(outputs, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) all_probs.extend(probs.cpu().numpy()) print(classification_report(all_labels, all_preds, target_namesclass_names, digits4)) print(confusion_matrix(all_labels, all_preds)) return (all_labels, all_probs, all_preds)这里多存了all_probs是给第 6 章卡置信度阈值用的。分类报告里你会看到每个类别的召回率如果“火”这个类别的召回率只有 0.6那就别急着部署。参数说明class_names必须和 ImageFolder 的classes顺序一致digits4显示四位小数方便对比小差异。这样评估出来的 F1 才是你后续调参的目标。5. 避坑森林火灾图像分类的五个常见翻车点这一章把我在实际项目中遇到的翻车点一次说清。每个坑都按“现象 → 原因 → 解决”来讲你可以直接对照自己实验的现象看。5.1 类别不平衡导致的“假准确率”多数类把火情淹没了现象训练完看到验证准确率 94%但翻看分类报告火灾类的召回率只有 25%。原因是 13,000 张图里可能 80% 都是正常林地模型只需全判成正常就能拿到高准确率根本不需要学会火灾特征。解决训练前打印每类样本数看到分布明显偏斜就在损失函数里加类别权重。最常见的做法是用sklearn.utils.class_weight.compute_class_weight算出 balanced 权重再传给CrossEntropyLoss(weight...)。这个权重会放大样本量少的类别的 loss让模型“被迫”注意它。import numpy as np from sklearn.utils.class_weight import compute_class_weight classes np.unique(labels) weights compute_class_weight(balanced, classesclasses, ylabels) class_weights torch.tensor(weights, dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)注意加了类别权重后学习率可能需要稍微调低因为少数类的 loss 变大容易震荡。另外也可以在采样层面解决用WeightedRandomSampler但权重方式更干净。5.2 视频帧随机划分模型“背答案”导致验证集虚高现象验证集 F1 达到 0.95可拿到现场实际拍摄的一段视频上一测漏报严重。原因是训练集和验证集来自同一段监控视频模型把场景背景背下来了而不是真的理解火。解决在看文件名是否有scene01、camera3这类前缀有就按分组划分方法已经在第 3 章给过。我在这里再强调一次因为这是我踩过最深的一个坑。如果一个数据集是从原始视频里抽帧得到的且没有显式提供分组信息最简单的做法是看相邻帧的 md5把连续相同的场景归为一组。更实用的是在划分前直接跑一个“相似度检查”连续帧如果像素级差异很小大概率来自同一视频。这个坑的破坏力在于它会让指标完全失真而且很难察觉。5.3 标注噪声烟雾和雾气、远处的火光很难分清现象训练损失下降到最后几轮开始抖动验证集上固定某几张图反复预测错。把这些图打印出来人眼都很难判断是烟还是雾有些是林间清晨的水雾标成了烟雾。原因标注者主观性导致边界样本标签不干净。森林火灾图像分类数据集的难点不在于“熊熊大火”这种一眼就能看出的样本而在于早期火情——一小缕烟、远处若隐若现的橙色光斑。标注规范如果不强调这些边界的判断标准数据里就会充满错标。解决训练之前先用一个大模型如预训练 ResNet 或 CLIP对数据做一次预测把预测置信度高但和标注不同的样本挑出来人工复核。代码思路是记录预测类别 ! 真实类别且softmax 概率 0.8的样本这些大概率是错标而不是模型问题。这个流程也叫“错标清洗”实际能清理掉 1% 到 5% 的错误标签。清理完再训练效果立竿见影。5.4 增强副作用把红色增强成紫色模型学了个寂寞现象训练集的 loss 降得很低但验证集的精度始终上不去甚至部分测试图只要稍微偏色就错判。检查增强后的图片发现Hue抖动把火焰的红色调成紫色把背景的绿色调成红色模型学到的是“橙色 火”而不是“火苗纹理 烟雾扩散”。原因很多默认增强模板会带ColorJitter(hue0.2)这在通用图像分类上没问题但对火灾语义是负优化。林区的叶子颜色和火焰颜色在 RGB 空间本来接近再经过 hue 扰动类别边界被彻底搅乱。解决把hue参数去掉saturation也不宜超过 0.2。还有RandomChoice里的RandomGrayscale要慎用火和烟的灰度区分度远低于彩色区分度。增强的目标是制造“朝向不变性”而不是“颜色改变”。5.5 验证集、测试集没分开调参调到“自欺欺人”现象你在验证集上反复调阈值、改增强最终验证集 F1 0.97但上线后跌到 0.80。原因是验证集参与了你的决策它已经变成了训练过程的一部分不再代表未知数据。解决一开始就把数据切成三份训练集、验证集、测试集其中测试集只允许跑一次用于最终评估。这个原则在竞赛里是天条在工程里却经常被忽略。如果你手上只有 13,000 张切片比例建议 72/18/10不要担心少那 10% 训练数据可信的测试集比 1% 的准确率提升重要得多。6. 进阶用置信度阈值卡召回率给分类模型装上“后悔药”火情识别宁可多报不可漏报但很多分类模型默认在 softmax 输出大于 0.5 时判为正类。这个阈值默认值是 0.5它假设两类代价对称这在消防场景里完全错误。一个更实用的做法把验证集所有样本的模型输出概率保留下来画一条 precision-recall 曲线然后直接指定目标召回率比如 95%反推出需要的置信度阈值。这条曲线能告诉你你想记住 95% 的火能付出多少误报代价。import numpy as np from sklearn.metrics import precision_recall_curve # 在验证集上得到 all_probs, all_labels # 假设二分类fire 是索引 1 fire_probs np.array(all_probs)[:, 1] y_true np.array(all_labels) precision, recall, thresholds precision_recall_curve(y_true, fire_probs) # 找召回率 0.95 的最严阈值使误报尽可能少 valid_idx np.where(recall 0.95)[0] idx valid_idx[-1] # 在满足召回率的前提下取最后一个对应最大阈值 thr thresholds[idx] print(f当阈值设为 {thr:.4f}召回率 {recall[idx]:.4f}精确率 {precision[idx]:.4f})说明precision_recall_curve返回的 thresholds 长度比 precision 少一个遍历时别越界。valid_idx[-1]是召回率刚跌破目标前的最后一个阈值如果取valid_idx[0]会选一个更低也更容易误报的阈值。逻辑要么是取“满足目标召回率下最高精确率”的阈值要么取“不超过某个召回率要求下的最大阈值”前者更普遍。如果你是多分类可以对每个类别单独处理把该类当作正类其余当作负类分别画 PR 曲线。对火灾这个业务通常只关心 fire 这一类的召回率所以整个流程只需要一个类别。在实际部署时我不会把阈值写死在模型代码里而是把模型输出的概率传给一个决策函数def fire_alert(prob, fire_threshold0.58): if prob fire_threshold: return ALERT return NORMAL这样阈值可以随着季节、时段、风险等级调整比如白天设置 0.7 减少误报晚上设置 0.4 提高灵敏度。不要把“调阈值”看作模型出来之后的补丁它其实是业务需求到机器学习模型之间最直接的翻译层。如果你把这个阈值也根据验证集调好最后一定要在测试集上重新评估一次否则仍然属于“用验证集当测试集”的自欺欺人。我吃过这方面的亏当时做一个森林火灾图像分类模型验证集准确率 98%很快乐。部署第一天系统对着落日误报 11 次因为落日的橙色和火焰色在 224×224 下几乎一样。后来我不再信任默认阈值把所有误报样本拉出来统计概率分布发现它们都集中在 0.450.75 之间而真正的火情样本几乎都超过 0.8。把阈值抬到 0.82 后漏报没有增加误报反而少了很多。这个经验后来变成了一个习惯任何分类模型上线前先看验证集的置信度分布再决定阈值而不是直接拿argmax当输出。希望帮到你。本文还有配套的精品资源点击获取
返回列表