资讯动态

语义分割逐类mIoU计算详解:从混淆矩阵到遥感影像实践

发布时间:2026/10/10 18:18:55 来源:尧图企业网站定制
简介面向语义分割模型评估需求的PyTorch脚本集旨在解决mIoU指标计算繁琐的问题帮助开发者和研究人员快速量化模型在各类别上的分割精度适合正在开展分割实验、撰写论文对比或调试模型性能的读者使用。压缩包仅含2个Python文件共4KB体积轻量demo_mIoU.py负责将模型输出转换为8位类别索引预测图mIoU.py读取预测图与真实mask逐类统计IoU后汇总整个测试集的整体mIoU两个脚本直接运行即可完成评估。已有7160人浏览学习说明该脚本在分割精度评估场景中具有较好的实用参考价值尤其适用于需要快速评估分割结果的实验环节。读者可省去从零搭建评估流程的精力获得一套可复用的mIoU计算方案同时通过拆解两个脚本的配合逻辑也能理解预测图掩码转换、逐类指标统计与整体指标汇总的具体实现便于迁移到自有数据集或其他分割网络。1. 语义分割中的总指标像黑匣子只看整体mIoU你看不见哪一类在拖后腿当实验里跑完一个语义分割模型终端打印出“mIoU 0.723”很多人就以为任务完成得不错。但把结果按类别拆开来看你常常会看到这样的场面road和building两大类的IoU都在0.9附近而cartrack、pole这类细物的IoU只有0.1甚至直接没被预测出来。整体mIoU把各类别一平均就把这问题“平均”掉了。类别mIoU计算要回答的不是“模型整体好不好”而是“究竟哪一类让模型翻车了”。本笔记讲的就是如何快速、准确、可复现地计算语义分割中每一个类别的IoU并在遥感影像分割这类类别不平衡任务中写对它。2. 从像素到混淆矩阵类别mIoU计算的数学根基与矩阵构建语义分割和分类任务最大的区别在于它是对每个像素做分类再对整张图的所有像素做统计。所以逐类mIoU计算不能靠“眼睛对比预测图和标注图”这种定性判断必须落到精确的像素级统计上。我一般会把计算拆成两步先构建一个类别×类别的混淆矩阵再从混淆矩阵里算出每个类别的IoU。这两步逻辑分得越清后面排查问题时就越省事。2.1 为什么 IoU 是语义分割的事实标准对比像素精度与 Dice很多初学者会问我直接算逐类像素准确率不行吗像素准确率Pixel AccuracyPA的公式是(TPTN)/(TPTNFPFN)它把类别频率当成了隐式权重。在遥感图像语义分割里道路、建筑、水体这些大类别经常占据百分之六七十的像素模型只要把这三类猜对PA 就能轻松上 0.9而稀疏的小类别发生大量漏检也不影响 PA。这正是 PA 不适合做分割算法性能评估的根本原因——它掩盖了长尾类别的问题。Dice 系数也就是 F1 的像素级变体在医学影像分割里很常见公式是2TP/(2TPFPFN)它对小目标重叠程度更敏感但它的数值通常比 IoU 高不少而且不是 DeepLab、FCN 这类语义分割模型论文里的“通用货币”。IoU 的公式是TP/(TPFPFN)分母把漏检和误检都算进去几何意义是预测区域与真实区域的交集比上并集既不会夸大性能又对类别不平衡有一定的鲁棒性。指标公式二分类语义主要短板Pixel Accuracy(TPTN) / (TPTNFPFN)大类像素占比高时极易虚高Dice / F12TP / (2TPFPFN)数值偏乐观论文基准里不常作为主指标IoUTP / (TPFPFN)对漏检和误检同等惩罚适合逐类比较“mIoU”里的 m 是 mean也就是对所有类别上的 IoU 直接求算术平均。这就是逐类mIoU计算的核心逻辑——每个类别在最终分数里的权重完全一样哪怕某个类别在整张图里只出现几百个像素它也会拉低整体的分数。所以模型如果牺牲一小类去讨好大类mIoU 一定会有所体现这是它相比 PA 最重要的价值。2.2 混淆矩阵类别mIoU计算的地基与实现前奏从算法上说要得到某个类别的 TP、FP、FN根本不需要对每个像素做三重遍历只要建立一个类别×类别的混淆矩阵就行。设混淆矩阵为conf行为真实类别、列为预测类别那么conf[i][j]表示“标签为 i、却被模型预测成 j”的像素数量。对角线conf[i][i]就是该类被正确分类的像素数。构建混淆矩阵有两种常见做法。第一种是两轮 for 循环逐个像素判断 i 和 j然后累加计数器这种写法在百万像素的遥感影像上会慢得让人怀疑人生。第二种是把“真实类别 i、预测类别 j”编码成一个唯一整数i * num_classes j再调np.bincount一次性统计所有组合的出现次数最后 reshape 成二维矩阵。下面是我项目中一直在用的实现不依赖任何深度学习框架import numpy as np def build_confusion_matrix(mask_true, mask_pred, num_classes, ignore_indexNone): # 将 HxW 的标签图拉平成向量方便做一次性的全局统计 t mask_true.flatten() p mask_pred.flatten() # 去掉忽略点ignore_index 通常对应标注里的 void / 不感兴趣区域 if ignore_index is not None: valid (t ! ignore_index) (p ! ignore_index) t t[valid] p p[valid] # 把“真实类别 i、预测类别 j”映射成唯一整数 i * num_classes j idx t * num_classes p # bincount 返回每个组合出现的像素个数minlength 保证所有组合都被统计 hist np.bincount(idx, minlengthnum_classes * num_classes) # 重排成标准的 C x C 混淆矩阵 conf hist.reshape((num_classes, num_classes)) return conf逻辑说明t * num_classes p是整个计算的灵魂。假设真实类别是 2、预测类别是 5、类别总数是 21那么这个像素对应的整数是2*21547hist[47]的数值就代表“GT 是 2 但被模型预测成 5”的像素个数reshape后它落在conf[2][5]上。这一步把原本需要双重循环的操作降成了三次向量化运算。参数说明ignore_index用于处理标注里不确定的区域PyTorch 习惯用 255很多遥感影像数据集则把边界或不感兴趣区域单独编一个号。如果数据集中存在“忽略类”在构建矩阵前必须先过滤否则这些像素会以真实类别的身份混进统计让 IoU 虚高。minlength必须写成num_classes * num_classes因为bincount默认只统计到输入数组里出现的最大索引某几个类别组合在一张图中没出现时reshape会直接报错或者输出形状错乱。一张百万像素的图这个函数在普通 CPU 上也能在几十毫秒内跑完完全没必要用 GPU 加速。2.3 类别编号对齐RGB 标签的 remap 与注意类别的正确打开方式构建矩阵之前有一个特别容易被忽略的前置步骤检查标签数据是不是“序号 0 到 C-1”的整数。在遥感图像语义分割中大量数据集提供的标注不是掩膜序号而是方便人眼查看的 RGB 真彩色图例如“植被(0,128,0)道路(128,128,128)”。如果你直接把 RGB 三通道当成一维整数去建矩阵那mask_true的形状都不对算出来的各类别 mIoU 毫无意义。这种情况必须先做颜色到类别序号的映射def remap_rgb_labels(mask_rgb, color_to_class): # mask_rgb: [H, W, 3] 的 RGB 标注图dtype 通常是 uint8 # color_to_class: 字典键为 (r, g, b) 元组值为类别序号 h, w, _ mask_rgb.shape out np.zeros((h, w), dtypenp.int32) for color, label in color_to_class.items(): match np.all(mask_rgb color, axis-1) out[match] label return out这段代码的作用是逐类别地把颜色写入输出掩膜。它虽然是一个 for 循环但内层用的全是向量化比较一张 512×512 的影像做一次完整 remap 也就在 0.5 秒左右通常是离线预处理阶段执行一次然后保存成.npy即可不会影响训练速度。需要特别注意的是标注里有些颜色并没有出现在color_to_class里这时out默认值为 0就变成了“背景类”的像素这是一个隐性 bug。更稳妥的做法是先把out初始化为一个不存在的编号比如 255再在 map 里对每个真实类别赋值最后检查剩余像素避免把未知颜色错误地并入背景。在 Cityscapes 这类数据集里还有一个常见做法原始标注的像素值并不连续例如通过 19 类训练时会把原始 id 重映射到 0 到 18。这个 remap 步骤也属于类别对齐的范畴。很多语义分割模型跑出来的逐类 mIoU 看起来和其他论文对不上往往不是模型问题而是标签映射表没有对上。把 remap 理清楚后面计算才不会白忙一场。3. 逐类mIoU落地实现一套不依赖框架的 Python 计算模块有了混淆矩阵逐类mIoU计算就变成非常机械的查表操作。在这一章里我给出从混淆矩阵到最终报告的一整套代码并把容易出现边界情况的细节都写在注释和参数说明里。这套实现我平时直接放在项目的metrics.py里训练和验证共用避免两套评价代码结果不一致。3.1 从混淆矩阵到逐类 IoU核心公式与分母为 0 的处理每个类别的 IoU 定义是“该类预测正确的像素数”除以“真实为该类的像素数 预测为该类的像素数 – 重叠的数”。换成混淆矩阵的写法对第 c 类来说def compute_per_class_iou(conf, skip_zeroTrue): num_classes conf.shape[0] ious np.full(num_classes, np.nan, dtypenp.float64) for c in range(num_classes): # 对角线是该类被正确预测的像素数 intersection conf[c, c] # 行求和是真实属于 c 类的像素数列求和是模型预测为 c 类的像素数 # 两者相加时对角线被加了两次所以要减一次 union conf[c, :].sum() conf[:, c].sum() - intersection if union 0: ious[c] intersection / union else: # GT 和预测中都没有出现该类别 ious[c] np.nan if skip_zero else 0.0 return ious逻辑说明conf[c, :].sum()是“真实类别为 c”的全部像素也就是 TP FNconf[:, c].sum()是“预测类别为 c”的全部像素也就是 TP FP。两者相加把对角线上的 TP 算了两次所以需要减掉一次intersection。最终分母就是 TP FP FN分子是 TP这个比值严格对应两个集合的交并比。参数说明skip_zero控制的是某种特殊场景——某些类别在整张验证集里一次都没出现过。比如训练时用 21 类但测试集里没有“摩托车”类此时它的 union 和 intersection 都是 0。我的做法是默认将这样的类标记为np.nan计算均值时用np.nanmean让不存在的小类不参与总分。但要注意这个选择在学术评测里有讲究在 PASCAL VOC 上常见做法是不统计缺失类而在一些遥感影像分割榜单上官方会把缺失类直接算成 0以此惩罚模型“完全没学过这个类”。所以这个开关必须是显式参数而不能在代码里写死。3.2 最容易翻车的三个类用调查报表定位模型盲区算出每个类别的 IoU 只是一个数组光盯着数字看不出问题在哪。我一般会写一个print_report函数把 IoU、真实像素占比、预测像素占比放在同一张表里输出这样大类小类一眼就能分出层次。尤其是当某个类真实像素很少但预测像素很多说明模型在该类上严重误报这时候只看 IoU 可能还是不够的还要看 FP 集中在哪里。def print_class_report(conf, class_namesNone): num_classes conf.shape[0] gt_pixels conf.sum(axis1) pred_pixels conf.sum(axis0) ious compute_per_class_iou(conf, skip_zeroTrue) total conf.sum() print(f{Class:18}{IoU:8}{GT%:8}{Pred%:8}{Sample:10}) for c in range(num_classes): name class_names[c] if class_names else fclass_{c} gt_ratio gt_pixels[c] / max(total, 1) * 100 pred_ratio pred_pixels[c] / max(total, 1) * 100 if np.isnan(ious[c]): print(f{name:18}{NaN:8}{gt_ratio:7.2f}%{pred_ratio:7.2f}%{gt_pixels[c]:10}) else: print(f{name:18}{ious[c]:8.4f}{gt_ratio:7.2f}%{pred_ratio:7.2f}%{gt_pixels[c]:10})逻辑说明gt_pixels是混淆矩阵按行求和pred_pixels是按列求和。把两者并排打印后如果某个类GT%是 10% 但Pred%是 40%那就说明模型在这个类别上疯狂误报把别的东西都圈了进来。这时候即便 IoU 看起来只有 0.3光优化这个类也不能解决根本问题还得去看它最容易和哪些类混淆——加一个“按类 FP 前十像素来源”的统计会更直观但这些在打印报告里暂时够用了。参数说明class_names建议传数据集规范里的名字不要用class_0这类匿名编号。遥感影像语义分割模型训练时我习惯把类别名和 RGB 映射保持同一份配置这样打印出来的报告可以直接丢进周报别人也能看懂。输出的 Sample 列是真实像素总数可以快速判断某个类是不是样本太少IoU 的波动本身可能就有很大随机性。3.3 把逐类统计复用进一轮验证循环上面的函数单独好用但实际训练时需要连续处理很多批数据。正确做法不是把每张图的 mIoU 先算出来再平均而是把所有批次的混淆矩阵按元素相加最终只做一次逐类mIoU计算。类级别的 mIoU 在数学上不等价于“每张图 mIoU 的平均值”后者在大类占比不均匀的验证集上会失真。def evaluate(model, loader, num_classes, device): model.eval() total_conf np.zeros((num_classes, num_classes), dtypenp.int64) with torch.no_grad(): for images, masks in loader: images images.to(device) logits model(images) # 从 logits 得到每个像素的类别索引 pred torch.argmax(logits, dim1).cpu().numpy() masks masks.numpy() for p, t in zip(pred, masks): # 每批都把混淆矩阵累加进总矩阵 total_conf build_confusion_matrix( t, p, num_classes, ignore_index255 ) ious compute_per_class_iou(total_conf, skip_zeroTrue) print_class_report(total_conf, class_namesCLASS_NAMES) return np.nanmean(ious)逻辑说明torch.argmax(logits, dim1)把[B, C, H, W]的输出压缩成[B, H, W]的类别序号这是深度学习框架做语义分割推理时最标准的转换。zip(pred, masks)按 batch 维度逐张处理把每一张图的混淆矩阵累加到总的total_conf里。最后从总矩阵直接算 IoU 和均值能保证验证集的每个像素对结果有且仅有一次贡献。参数说明ignore_index255要和数据加载时的 ignore 设置保持一致。如果训练时损失函数用的ignore_index255而验证时统计却把它当成了真实类别 255 去参与 IoU那最终分数会明显偏低。另外total_conf一定要用dtypenp.int64而不是默认的int或uint8大尺寸遥感影像一个 epoch 的像素量级可能高达数十亿累加时很容易溢出成负数出现这种 bug 时 mIoU 会莫名其妙地高到 0.9 以上。4. 遥感影像里的样本不均衡分类别 mIoU 的加权修正方案逐类mIoU计算在普通城区驾驶数据集上已经能帮我们定位不少问题但到了遥感影像语义分割场景直接看裸的 mIoU 还是有明显的局限性。遥感图像通常是一张几万像素宽的大图背景占比巨大感兴趣的小目标稀疏且尺度小。这里如果还只报一个算术平均 mIoU常常出现“总分还行真正关注的类别根本不能用”的情况。4.1 遥感图像语义分割中的类别失衡现状遥感影像分割任务里常见的类别是建筑、道路、水体、农田、林地还有一类容易被忽略的“其他”。像建筑和道路在空间上连续动辄占整张图百分之二三十的像素而独立树冠、小汽车、船只这类目标只占极少数像素。用标准 mIoU 逐类去算时大类由于像素充足分割边界稍微差一点也能拿到 0.8 以上的 IoU而小类只要漏掉一两栋建筑IoU 就直接掉到 0.1 以下。整体 mIoU 在 0.7 附近看着还行实际上这类小目标类别完全没有达到可用状态。更麻烦的是遥感影像的语义分割模型在大图上推理时通常要切成 512×512 或 1024×1024 的 patch。切块会切断建筑、道路这类目标的连续性小目标如果恰好被切在 patch 边缘几乎必然被模型漏掉。分类别 mIoU 报告里这些边缘小类的分数会特别低但整体 mIoU 依然看不出问题。我习惯在 patch 推理模式下额外打印一张“类别 IoU vs 目标中心距 patch 边缘距离”的散点图能从统计上证明边缘切块是漏检的主因。4.2 频率加权 IoUFWIoU的计算与解读为了量化类别占比对分数的实际影响SegNet 论文里使用过一个改进指标叫 Frequency Weighted IoUFWIoU。它把每个类别的 IoU 按该类在真实标注中的像素频率加权求和公式为sum( (GT像素占比) * IoU(c) )。FWIoU 衡量的是“假设我随机抽一个像素它所属类别的 IoU 大概是多少”这个指标更贴近模型在真实目标上的整体表现。def compute_frequency_weighted_iou(conf, class_weightsNone): # 按 GT 的像素频率给每类分配权重 gt_counts conf.sum(axis1) total gt_counts.sum() if class_weights is None: class_weights gt_counts / max(total, 1) ious compute_per_class_iou(conf, skip_zeroTrue) # 跳过 NaN 类别并把权重重新归一化 valid ~np.isnan(ious) if not valid.any(): return 0.0 weights class_weights[valid].astype(np.float64) weights weights / weights.sum() return float(np.sum(weights * ious[valid]))逻辑说明gt_counts / total得到每个类别在整个验证集中的真实占比。这个权重往往高度倾斜道路和建筑加起来可能超过 0.7。当compute_per_class_iou返回 NaN 表示某类完全没有出现时这里也要同步过滤并把剩余权重重新归一化否则最终数值会偏低。FWIoU 的值通常会比直接np.nanmean(ious)高不少因为大类表现好、权重又高小类即使 IoU 低也被稀释了。参数说明class_weights参数允许你手动覆盖频率权重。在遥感图像语义分割评测中有些榜单担心纯频率权重导致小类被彻底忽视会给定一整套人工权重来平衡大中小目标。用这个函数时我一般会把三个指标一起打印mIoU 代表类别平均表现FWIoU 代表像素平均表现再加一个 tail-mean只取 IoU 最低的 20% 类别求平均。这三个值互相参照后才能判断分数有没有虚高。4.3 类别级 mIoU 的加权扩展不要用平均掩盖真实需求除了 FWIoU还有一种扩展思路是给每类设置一个“关注权重”比如在小目标检测上把车辆、船这类类别的权重调到 2 或 3再算加权 mIoU。这个加权 mIoU 不是标准评测指标但它对业务很有用如果客户明确说“我只要监测违建房屋”那么房屋类 IoU 就要比道路类重要得多。代码上可以直接复用上面的函数只要传一个class_weights数组进去就行。WEIGHTS np.array([1.0, 1.0, 2.5, 1.0, 3.0]) # 假设第 2、4 类是小目标 weighted_miou compute_frequency_weighted_iou(total_conf, WEIGHTS)这里的数值只是示例实际取值应该根据项目要求和难易程度来定。需要提醒的是我见过有些团队为了榜单分数把难识别类别直接不参与平均这本质上是在躲问题。比较诚实的做法是同时汇报标准 mIoU 和业务加权 mIoU让读者一眼看出“标准指标不够好但业务关心的小类确实被优化过”。遥感影像语义分割模型的最终价值在于对特定地物的提取精度而不是一个漂亮的算术平均。5. 逐类mIoU计算避坑记录五个说多了都是泪的典型踩坑这一章专门写我过去两年在各类项目里被 mIoU 坑过的记录。每一条都是先看到异常现象再倒查原因最后给出我现在在用的处理方案。希望你不再重复走这些弯路。5.1 现象某个在真值里大量存在的类别 mIoU 恒为 0有次在遥感影像语义分割模型验证时打印报告发现“水体”这一类 IoU 一直是 0其他类都正常。一开始以为是模型没学会水体但采样了几张预测图水体明明分割得很好。后来倒查数据预处理发现数据集的标签文件是 RGB 彩色图颜色到类别的映射没有把水体的深蓝色准确覆盖导致remap_labels之后水体标签全部变成了 0背景。模型预测的水体像素和真实像素对不上号混淆矩阵对角线自然为 0。原因就是color_to_class映射字典漏了某种水体的颜色变体比如阴影下的深蓝和阳光下的浅蓝被当成不同的 RGB 值存储。解决方法是先把整个训练集标签图里出现的所有 RGB 颜色做成直方图再和类别映射表对照保证每种颜色都对应到了正确类别。我后来在预处理时专门加了一段颜色去重代码如果有未知 RGB 出现就直接抛异常而不是静默当背景处理。5.2 现象整体 mIoU 比大多数类别的 IoU 都要高这听起来很矛盾但确实发生过。当时用的混淆矩阵实现是某一套开源代码里抄来的它把矩阵转置了一下行是预测类别列是真实类别。对角线不受影响所以对角线数值还是对的但我在计算每个类别 IoU 时用行和分别代表 GT 和预测行和列一旦颠倒每个类别的 TP 一样但 FP 和 FN 全都互换了算出的各类别 IoU 全都低于正常值。最后求平均的时候由于多算了一些错误分母整体 mIoU 反而高于某些本来应该很高的类别。原因就是混淆矩阵的“行GT、列预测”没有固定下来代码在转手时方向变了。解决的方法是写一个单元测试构造一个全 0 类别的 mask_true预测也全 0然后检查混淆矩阵应该满足conf[0,0] 像素总数其余位置为 0。如果转置了conf[0,0]不变但conf[0,:]和conf[:,0]会不同。这个测试我在每次调新环境时都会先跑一遍。5.3 现象多 GPU 验证时各卡上的 mIoU 和全局 mIoU 不一致在分布式训练里我用两张卡分别对验证集的一半做推理然后各自算 mIoU最后再把两边的分数平均作为最终结果。发现这样得出的 mIoU 和把全部验证集跑完再算的结果差了 0.02 左右虽然不大但在对比精度时足够影响结论。原因是验证集两个子集的类别分布完全不同比如一半包含港口另一半几乎全是农田两个子集的逐类 IoU 本来就不可直接平均。正确做法是把每张卡上所有 batch 的混淆矩阵都按元素相加然后拉回到主进程做一次compute_per_class_iou。也就是说混淆矩阵的聚合必须在像素层面完成而不是在分数层面完成。在我自己的代码里total_conf是在每个 batch 上用累加的分布式场景下只需要在最后对所有 GPU 的total_conf做一次 all-reduce 相加即可。类别分布差异再大最终结果也跟单卡完全一致。5.4 现象验证集 mIoU 很高但目视检查发现大量边缘锯齿和漏检有一次我用低分辨率训练了一个遥感影像语义分割模型验证 mIoU 到了 0.82给人感觉已经可以上线。但在放大的目视检查里建筑和道路边界的锯齿非常严重很多细碎目标直接没识别出来。问题出在指标本身IoU 对大型目标的中心区域很敏感哪怕边界错得离谱中心区域的正确像素也占据了并集的大头所以分数依然很高。这和边界质量、碎片目标数量是两个维度。解决方法是补充报告“边缘 IoU”或至少把预测图里小目标的独立连通域数量统计出来看它们是不是掉了。对于遥感影像尤其需要关注细小目标一个建筑漏掉半截看起来不明显但对业务方来说就是一次漏报。我不再用 mIoU 作为唯一上线闸门上线前必须附上按类别缩放的预测-标注叠加图人眼抽查最差的三类。5.5 现象验证阶段 mIoU 在小类上剧烈波动同一模型两次评测相差很大有一次我把验证代码从单卡切到多卡后小类别如水体的 IoU 从 0.43 跳到 0.51还以为模型权重出了问题。反复排查后才发现问题在验证时忘了调用model.eval()模型里的 BatchNorm 在验证阶段还在用当前 batch 的统计量更新已 running_mean导致每一批数据对同一张图的输出都略微不同。尤其在小类上像素数量少预测结果只要偏移几个百分点IoU 就会剧烈变化。原因本质是验证流程里混入了训练态操作。解决方法是把model.eval()放在验证循环最开始并且用torch.no_grad()把模型包起来。这里还需要额外检查一些在 eval 状态下会变形的层例如 Dropout 和随机深度如果忘了关结果同样会抖动。现在我把这个状态切换直接写进evaluate()函数的开头就不会再犯这种低级错误了。6. 把逐类诊断接进训练管线早停、学习率与各类别 mIoU 的联动技巧前面几章解决了“怎么算”和“算得对”的问题最后这一章讲“算完之后怎么用”。直接在训练脚本里接入逐类 mIoU 报告会对模型调优方向产生非常大影响。尤其是当你在调学习率时只看整体 mIoU 很容易错过小类开始过拟合的时刻。6.1 用逐类报告验证数据增强和损失函数改动我现在每次训练都会在验证集上打印逐类 IoU 的升序排序重点关注排名末三位的类别。如果某个版本改动数据增强后最差类别的 IoU 明显提升而其他类没有掉说明这个增强对该类有效反之如果最差类别连续三个 epoch 不涨我就知道这个改动对长尾目标没有帮助。这比比较整体 mIoU 的 0.1 个点来得可靠。6.2 给最差类别单独设置早停条件常见的早停是监控验证 mIoU但小类的过拟合往往比整体 mIoU 更早发生。我写了一个简单的日志监控记录每个 epoch 各类别 IoU 的最小值如果min_iou连续 8 个 epoch 下滑即使整体 mIoU 还在上升也会把学习率调低一档。这个做法的代价很小却经常能挽回小类精度。def should_early_stop(history, patience8): if len(history) patience 1: return False recent [epoch[min_iou] for epoch in history[-patience:]] return all(recent[i] recent[i-1] for i in range(1, len(recent)))这段代码的含义很直接history里每个元素是某个 epoch 的各类 IoU 字典取最小的那个类别如果最近 8 个 epoch 这个最小值一直在下降就认为模型开始忘记小类符合早停条件。配合整体 mIoU 一起判断能避免训练后期模型被大类主导而退化。6.3 把逐类指标保存为 CSV 方便每个版本间对比我习惯把每个 epoch 的各类别 mIoU 写进 CSV文件名带上模型结构和数据集描述方便后期翻旧账。这个 CSV 只有三列epoch、class_name、iou。对比两个版本的训练曲线时我直接选同一个类画两条曲线叠加能直观看出哪一版在小类上更稳。这种逐类诊断的习惯最初也是被一个只报整体 mIoU 的项目逼出来的。模型上线后发现业务最关心的小船目标漏检严重而整体分数很好看。现在每次汇报我都会附带一张最差三个类别的预测-标注对比截图不再让数字替模型“说话”。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑