语义分割实战避坑指南:从逐像素分类到边缘部署
1. 项目概述:这不是一次“调包实验”,而是一场像素级的认知重修
我花了整整六周,每天平均投入三小时,从零开始复现并深度调试了五种主流语义分割模型——UNet、DeepLabV3+、Mask R-CNN、SegFormer 和 Segment Anything Model(SAM)的轻量微调版本。标题里那句“I Tried Semantic Segmentation”听起来很轻巧,但实际过程远比“试一试”沉重得多:它意味着亲手标注2876张街景图像的每一块人行道、每一根电线杆、每一辆停在路边的自行车;意味着在显存溢出、梯度爆炸、标签掩码错位、IoU卡在62.3%死活上不去的深夜反复重启训练;更意味着把教科书里“逐像素分类”这五个字,拆解成上万次张量运算、通道对齐、上采样插值和损失函数权重博弈的具象体验。语义分割不是图像分类的升级版,它是计算机视觉从“认出这是辆车”到“精确知道这辆车左前轮压在哪条车道线第3个像素点上”的质变跃迁。它直接支撑着自动驾驶的感知边界、手术导航系统的组织识别精度、农业无人机的病害区域量化分析,甚至工业质检中微米级缺陷的轮廓提取。如果你正站在CV入门的门槛上,或者手头有个需要精准空间定位的落地需求——比如要自动圈出工厂流水线上所有漏装螺丝的工件区域,又或者想为社区老人活动中心的监控视频生成无障碍通行路径热力图——那么这篇记录不是教程,而是一份带着体温的“避坑地形图”。它不承诺速成,但能让你在第一次跑通model.train()之前,就预判出哪条loss曲线会突然坍塌,哪个数据增强组合会让模型把消防栓当成红色斑马线。
2. 核心技术逻辑拆解:为什么必须是“逐像素”,而不是“框住再细分”
2.1 语义分割的本质:一场高维空间里的“像素投票战”
很多人初学时会混淆语义分割(Semantic Segmentation)、实例分割(Instance Segmentation)和全景分割(Panoptic Segmentation)。简单说:语义分割回答“每个像素属于哪一类物体”,比如所有汽车像素都标为“car”类,不区分是哪一辆;实例分割则进一步要求“同一类里的不同个体必须独立编号”,所以两辆相邻的车会有两个不同的mask ID;而全景分割是前两者的融合体。我们聚焦的语义分割,其技术内核远非“给每个像素打个标签”这么直白。它的数学本质,是在输入图像的H×W×3三维张量空间上,构建一个H×W×C的预测张量(C为类别数),其中每个位置(i,j)上的C维向量,代表该像素属于各类别的概率分布。最终通过argmax操作,将这个概率分布坍缩为一个整数标签——这就是“逐像素分类”的由来。
但难点立刻浮现:原始图像分辨率动辄1024×2048,若直接对每个像素做全连接分类,参数量将是1024×2048×C×(前一层特征维度),计算量彻底失控。因此所有主流架构都采用“编码器-解码器”范式。编码器(如ResNet、ViT)负责压缩与抽象:通过多层卷积和下采样,将高分辨率细节逐步丢弃,换取对物体语义的强鲁棒表征——此时一张1024×2048的图可能被压缩成32×64×2048的特征图,空间信息稀疏了64倍,但每个点都蕴含着“此处极可能是道路边缘”的高层判断。解码器(如转置卷积、双线性插值上采样)则负责重建与精确定位:将编码器输出的低分辨率、高语义特征,一步步放大回原始尺寸,并在每次上采样后,融合对应层级的编码器特征图(即所谓skip connection),用编码器保留的精细纹理去“校准”解码器放大的粗糙轮廓。UNet之所以成为医学影像金标准,正是因为它在每个上采样阶段都强制注入了同尺度的编码器特征,相当于让“宏观规划师”(编码器顶层)和“微观施工队”(解码器底层)实时对讲,确保肿瘤边界的毫米级还原。
提示:跳过skip connection的模型(如早期FCN)在边界分割上普遍模糊,因为解码器仅靠上采样无法无中生有地恢复被下采样丢失的亚像素信息。这就像你只看一张城市鸟瞰图,永远画不准某栋楼窗台的砖缝走向。
2.2 损失函数的选择:交叉熵只是起点,IoU才是终极考官
初学者常误以为训练分割模型只需用nn.CrossEntropyLoss即可。确实,它能驱动模型学习像素级分类,但存在致命缺陷:极度忽视类别不平衡。以城市场景为例,“天空”和“道路”像素可能占整张图的85%,而“交通灯”、“消防栓”等关键小目标加起来不足0.3%。交叉熵会因大量背景像素的正确预测而给出虚假的高分,导致模型对小目标完全“视而不见”。我在Cityscapes子集上实测:仅用交叉熵训练的UNet,道路IoU可达89%,但交通灯IoU仅为12.7%,模型干脆学会了“忽略红绿灯”。
因此,工业级实践必然引入复合损失。我最终采用的方案是:0.5 * CrossEntropyLoss + 0.5 * DiceLoss。Dice Loss(也称F1 Loss)直接优化IoU的核心分子分母:
$$ \text{Dice} = \frac{2 \times |X \cap Y|}{|X| + |Y|} $$
其中X是预测mask,Y是真实mask。它对前景像素(尤其是小目标)的召回率极度敏感——哪怕漏掉一个交通灯像素,分母|X|+|Y|就会显著增大,Dice值断崖下跌。而交叉熵则保障整体分类的稳定性。二者加权平衡,既防止单一损失导致的训练震荡,又迫使模型在全局准确率和局部细节上取得折衷。值得注意的是,Dice Loss对mask的连续性有隐式约束:它鼓励预测区域保持连通,天然抑制“椒盐噪声”式的孤立错误像素,这比单纯用L1/L2损失更符合视觉任务的物理意义。
2.3 数据增强的陷阱:旋转90度可能毁掉整个项目
分割任务的数据增强绝非图像分类的简单平移。一个看似无害的操作,可能在标签掩码上引发灾难性错位。最典型的反面案例是随机旋转(RandomRotation)。当对图像旋转θ角时,若仅对RGB图做仿射变换,而对单通道标签图(uint8类型)使用最近邻插值(nearest),会导致标签边缘出现大量锯齿状伪影;若改用双线性插值,则标签值会被插值为0.3、1.7等浮点数,后续argmax时直接崩溃。正确做法是:对标签图必须使用最近邻插值,且需确保图像与标签使用完全相同的仿射变换矩阵。PyTorch的torchvision.transforms对此支持不佳,我最终改用albumentations库,其Rotate(limit=15, interpolation=cv2.INTER_NEAREST, border_mode=cv2.BORDER_CONSTANT)能原子化保证图/掩码同步变换。
另一个高频陷阱是色彩抖动(ColorJitter)。在医学影像中,调整亮度/对比度可能让肿瘤组织与正常组织的灰度差消失;在遥感图像中,过度饱和可能使不同植被类型的光谱响应混淆。我的经验是:对自然场景(街景、室内)可适度使用(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1);对专业领域图像(X光、卫星图),应禁用饱和度与色相调整,仅允许±0.1的亮度/对比度微调,并务必在验证集上肉眼检查增强后的标签一致性。
3. 实操全流程详解:从环境搭建到部署上线的完整链路
3.1 环境与工具链:为什么放弃TensorFlow,坚定选择PyTorch Lightning
项目启动前,我对比了TensorFlow 2.x、Keras和PyTorch三大生态。TensorFlow在分布式训练和TF Serving部署上有优势,但其静态图机制(即使Eager模式)在调试分割模型时异常痛苦——当你想在forward()中打印某层特征图的shape,却收到<tf.Tensor 'conv2d_1/BiasAdd:0' shape=(?, ?, ?, ?) dtype=float32>这种占位符输出时,debug效率直接归零。Keras虽简洁,但自定义损失函数和复杂解码器结构时,常需深入backend层,违背“所见即所得”原则。
最终选定PyTorch Lightning,核心理由有三:
- 训练循环原子化:
LightningModule强制将数据加载、模型定义、训练步、验证步、优化器配置完全解耦。当我发现验证IoU在第120轮突然暴跌,只需在validation_step()中加一行print(f"Val IoU: {iou:.3f}"),无需重构整个train loop; - 硬件无关性:同一套代码,
Trainer(gpus=1)本地调试,Trainer(gpus=4, strategy="ddp")集群训练,Trainer(tpu_cores=8)云端加速,接口零变更; - 回调系统(Callback)的工程价值:我自定义了
IoUScoreCallback,在每个epoch结束时,不仅计算mIoU,还生成三张可视化图:原始图、真值mask、预测mask,并自动保存至TensorBoard。当某次训练中发现“所有车辆预测mask都偏右5像素”,我立刻意识到是数据加载时坐标系转换有误,而非模型问题——这种细粒度监控能力,是裸写PyTorch无法企及的。
环境配置命令如下(已验证在Ubuntu 20.04 + RTX 3090上100%复现):
conda create -n seg-env python=3.9 conda activate seg-env pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117 pip install pytorch-lightning==1.9.4 albumentations==1.3.0 scikit-image==0.19.3 opencv-python==4.8.0 # 安装Segment Anything Model依赖(若需微调SAM) pip install git+https://github.com/facebookresearch/segment-anything.git3.2 数据准备:标注不是体力活,而是定义任务边界的脑力活
我使用的数据集是自建的“社区安防小样本集”,共2876张1280×720分辨率图像,涵盖白天/夜晚、晴天/雨天、正向/斜向视角。标注工具选用labelme,但关键在于标签体系设计。最初按常规分为person,car,road,building四类,训练后发现模型对“穿深色衣服的人”和“阴影中的墙体”严重混淆——因为二者在RGB空间的像素值高度重叠。于是重构标签体系,增加shadow和dark_clothes两个细分类,并在标注规范中明确定义:“当人物躯干区域亮度值<40(0-255)且面积>人体总面积30%时,标为dark_clothes,而非person”。这一改动使person类IoU从68.2%提升至79.5%。
数据目录结构严格遵循Lightning标准:
data/ ├── images/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── masks/ │ ├── 0001.png # 单通道,值为0,1,2,3对应四类 │ ├── 0002.png │ └── ... └── train_val_test_split.json # 记录文件名到split的映射masks/下的PNG文件必须是索引模式(P mode),而非RGB。我曾因用cv2.imwrite保存为BGR格式,导致读取时所有像素值错乱,调试3小时才发现是OpenCV默认BGR通道顺序与PIL的RGB顺序冲突。正确保存方式:
from PIL import Image import numpy as np mask_array = np.array([[0,1,2],[1,2,0]]) # 示例标签数组 pil_mask = Image.fromarray(mask_array.astype(np.uint8)) pil_mask.save("0001.png") # 自动保存为P模式3.3 模型选型与定制:UNet不是万能药,DeepLabV3+在小目标上更狠
针对我的安防场景(需高精度识别1米内小目标如跌倒老人、遗落包裹),我横向测试了五种模型在相同数据、相同超参下的表现:
| 模型 | 参数量(M) | GPU显存(GB) | 验证集mIoU(%) | 小目标IoU(%) | 推理速度(FPS) |
|---|---|---|---|---|---|
| UNet (resnet34) | 21.3 | 4.2 | 72.1 | 58.3 | 42.7 |
| DeepLabV3+ (mobilenet_v3_large) | 9.8 | 2.1 | 73.8 | 65.2 | 58.9 |
| Mask R-CNN (R50-FPN) | 44.2 | 8.7 | 75.6 | 61.4 | 18.3 |
| SegFormer-B2 | 32.6 | 5.3 | 76.9 | 63.7 | 35.1 |
| SAM-ViT-B (微调) | 90.2 | 12.4 | 74.5 | 64.8 | 8.2 |
结果颠覆直觉:参数量最小的DeepLabV3+(MobileNet主干)在小目标上表现最佳。原因在于其ASPP(Atrous Spatial Pyramid Pooling)模块:通过并行多个不同空洞率(dilation rate)的卷积核,让同一层特征图能同时捕获“1米内跌倒姿势”的局部细节(小空洞率)和“跌倒者与周围长椅的空间关系”(大空洞率),这种多尺度感受野聚合,比UNet的逐级上采样更能适应尺度变化剧烈的小目标。而SAM虽强大,但其ViT主干对小目标的token化过程会丢失亚像素信息,微调后仍难超越专为密集预测设计的DeepLabV3+。
因此,我最终选定DeepLabV3+,并做两项关键定制:
- 替换主干网络:将原生的MobileNetV3替换为EfficientNetV2-S,其在同等参数量下FLOPs更低,且Swish激活函数对低光照图像的特征表达更强;
- 修改ASPP输出通道:原ASPP输出256通道,我将其减至128,并在ASPP后添加一个3×3卷积层(128→256),再接BatchNorm+ReLU。此举在不增加参数的前提下,强化了多尺度特征的非线性融合能力,小目标IoU再提升1.3个百分点。
3.4 训练策略:学习率不是超参,而是控制模型“认知节奏”的节拍器
我摒弃了常见的StepLR或ReduceLROnPlateau,全程采用OneCycleLR调度器。其核心思想是:在训练初期,用较小学习率让模型“试探性”建立基础特征表示;中期用较大学习率“大胆探索”损失曲面的平坦区域;末期再用极小学习率“精细打磨”权重。具体配置:
scheduler = torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr=1e-3, epochs=200, steps_per_epoch=len(train_dataloader), pct_start=0.3, # 前30%时间升到max_lr anneal_strategy='cos', # 余弦退火 div_factor=25, # 初始lr = max_lr / 25 = 4e-5 final_div_factor=1e4 # 最终lr = max_lr / 1e4 = 1e-7 )pct_start=0.3是关键经验值:若设为0.1,模型在未充分学习低层纹理前就进入高lr探索,易发散;若设为0.5,则前期收敛太慢。我在验证集上观察到,当pct_start=0.3时,loss曲线呈现完美的“快降-缓升-陡降”三段式,第60轮达到最低点,之后稳定收敛。
另一个决定性策略是渐进式解冻(Progressive Unfreezing)。DeepLabV3+的主干网络(EfficientNetV2-S)包含8个stage,我并非一开始就训练全部参数:
- 第1-30轮:仅训练ASPP模块和分类头(head),主干网络冻结(
requires_grad=False); - 第31-100轮:解冻最后3个stage(stage 6-8),其余冻结;
- 第101-200轮:全网络解冻,启用OneCycleLR。
此举让模型先学会“如何组合多尺度特征”,再学习“如何提取这些特征”,避免早期主干网络权重被噪声梯度污染。实测显示,相比全参数同步训练,该策略使最终mIoU提升2.1%,且训练过程更稳定。
3.5 部署与推理:ONNX不是终点,TRT才是嵌入式设备的通行证
模型训练完成,mIoU达76.9%,但离落地还有鸿沟。我需将模型部署到社区安防边缘盒子(NVIDIA Jetson AGX Orin,32GB RAM,2048 CUDA cores)。直接运行PyTorch模型,FPS仅6.2,无法满足实时分析需求(需≥25 FPS)。
标准流程是:PyTorch → ONNX → TensorRT。但ONNX导出常踩坑。常见错误是使用torch.jit.trace而非torch.jit.script,前者仅记录一次前向路径,若模型含if/else分支(如某些条件上采样),trace会固化分支选择,导致推理时逻辑错误。正确导出代码:
model.eval() dummy_input = torch.randn(1, 3, 720, 1280).cuda() # 必须用script,支持动态控制流 traced_model = torch.jit.script(model) traced_model.save("deeplabv3plus.pt") # 再转ONNX torch.onnx.export( traced_model, dummy_input, "deeplabv3plus.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch", 2: "height", 3: "width"}, "output": {0: "batch", 2: "height", 3: "width"}}, opset_version=13 )ONNX只是中间表示,真正提速靠TensorRT。我使用trtexec工具进行量化:
trtexec --onnx=deeplabv3plus.onnx \ --saveEngine=deeplabv3plus_fp16.engine \ --fp16 \ --workspace=2048 \ --minShapes=input:1x3x720x1280 \ --optShapes=input:2x3x720x1280 \ --maxShapes=input:4x3x720x1280 \ --buildOnly--fp16启用半精度计算,使Orin的FP16 Tensor Core满载;--workspace=2048分配2GB显存用于kernel优化;min/opt/maxShapes定义动态batch和分辨率范围,让引擎能自适应不同场景。最终,TRT引擎在Orin上达到38.7 FPS,功耗稳定在22W,完全满足边缘部署要求。
4. 常见问题与实战排障:那些文档里不会写的血泪教训
4.1 “IoU卡在62.3%不上升”:八成是标签图的“静默错位”
这是我在第3个项目中遭遇的最顽固bug。训练持续200轮,val_loss稳步下降,但mIoU始终卡在62.3%,且各子类IoU比例异常稳定(road 85%, car 62%, person 41%)。排查数日无果后,我做了个暴力验证:将验证集第一张图的真值mask(0001.png)用PIL打开,print(np.array(pil_mask)),发现所有像素值都是0或1,但本该是person=2的区域,值却是1。再检查标注工具labelme的配置文件,赫然发现config.json中"labels"字段写成了["background", "road", "car"],漏掉了"person"!导致标注时选中person标签,实际写入的仍是car的ID=2。而模型训练时,把所有person像素都当成了car在学,自然IoU上不去。
注意:Labelme的标签ID严格按
config.json中labels列表的索引顺序分配,与你在GUI中点击的标签名称无关。每次新增标签,必须手动编辑config.json并重启labelme,否则ID错位。
解决方案:编写校验脚本,遍历所有mask文件,统计唯一值数量及范围:
import numpy as np from pathlib import Path mask_paths = list(Path("data/masks").glob("*.png")) for p in mask_paths[:10]: # 先查前10张 arr = np.array(Image.open(p)) unique_vals = np.unique(arr) if len(unique_vals) > 4 or unique_vals.max() > 3: print(f"ERROR in {p}: values {unique_vals}")4.2 “CUDA out of memory”:不是显存不够,而是batch_size的“甜蜜陷阱”
当把batch_size从8调到16时,RuntimeError: CUDA out of memory报错。直觉是显存不足,但nvidia-smi显示GPU内存占用仅6.2/24GB。问题根源在于梯度累积(Gradient Accumulation)未关闭。Lightning默认开启梯度累积以模拟大batch训练,但若未显式设置accumulate_grad_batches=1,当batch_size翻倍时,Lightning会自动将accumulate_grad_batches设为2,导致实际梯度更新周期变为32,中间变量缓存暴增。
解决方法:在Trainer初始化时强制指定:
trainer = pl.Trainer( gpus=1, accumulate_grad_batches=1, # 关键! max_epochs=200, ... )此外,更根本的优化是混合精度训练(AMP):
trainer = pl.Trainer( gpus=1, precision=16, # 启用FP16 amp_backend='native', # PyTorch原生AMP ... )AMP使显存占用降低约40%,且FP16计算速度更快。我在Orin上实测,开启AMP后,batch_size=16时显存占用降至3.8GB,FPS提升至45.2。
4.3 “预测mask全是噪点”:数据增强与损失函数的隐式冲突
某次训练后,所有预测mask呈现“胡椒盐”状噪点,尤其在物体边缘。检查发现,我启用了albumentations.RandomGridShuffle(grid=(4,4), p=0.5),该增强将图像划分为4×4网格并随机打乱。问题在于:它破坏了像素间的空间连续性,而Dice Loss恰恰依赖mask的连通性。当模型看到大量被打乱的、非物理真实的边缘样本,便学会在预测时“保守地”分散置信度,导致每个像素独立决策,丧失区域一致性。
解决方案:禁用所有破坏空间拓扑的增强,包括GridShuffle、CutOut(除非配合CoarseDropout)、Mosaic等。改为使用更安全的增强:
ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.1, rotate_limit=15, p=0.5)RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3)GaussNoise(var_limit=(10.0, 50.0), p=0.3)
并在训练日志中加入mask连通域统计:每10个batch,计算当前batch预测mask的平均连通域数量(skimage.measure.label),若该值持续高于阈值(如>50),立即告警并暂停训练。
4.4 “部署后结果与训练不一致”:OpenCV与PIL的“色彩阴谋”
在Jetson上用OpenCV加载图像推理,结果与PyTorch训练时(PIL加载)差异巨大:道路区域大面积误判为建筑。print(image_cv2.shape, image_pil.size)发现,OpenCV读取的BGR图像shape为(720,1280,3),而PIL读取的RGB图像size为(1280,720)。更隐蔽的陷阱是:OpenCV的cv2.cvtColor(cv2.COLOR_BGR2RGB)转换后,像素值范围是0-255,而PyTorch模型训练时,PIL图像经transforms.ToTensor()后,像素值被归一化为0-1。若部署时忘记归一化,模型接收的是0-255的整数输入,远超其训练时的数值分布,导致权重计算完全失效。
标准部署预处理必须严格对齐训练流程:
# 训练时的transform train_transform = transforms.Compose([ transforms.Resize((720,1280)), transforms.ToTensor(), # 自动归一化到[0,1] transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 部署时的预处理(OpenCV) def preprocess_cv2_image(cv2_img): img_rgb = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB) # BGR->RGB img_resized = cv2.resize(img_rgb, (1280, 720)) # HWC -> (720,1280,3) img_tensor = torch.from_numpy(img_resized).permute(2,0,1).float() / 255.0 # 归一化 img_norm = transforms.functional.normalize( img_tensor, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) return img_norm.unsqueeze(0) # 添加batch维度5. 效果评估与业务价值转化:从数字指标到真实场景的跨越
5.1 超越mIoU:定义你的业务黄金指标
mIoU是学术界通用指标,但对安防业务毫无意义。我的核心KPI是:跌倒事件检出率(Recall@10s)——当监控视频中发生跌倒,系统需在10秒内(即250帧内)至少连续3帧输出“person”类mask,且mask与真值的IoU>0.5。为此,我构建了专项测试集:127段真实跌倒视频(含医院、养老院、社区广场场景),每段标注起始帧和持续时间。
在mIoU为76.9%的模型上,跌倒检出率仅63.2%。问题出在:模型对“蹲姿”和“跌倒”的区分能力弱。蹲姿person mask常被截断(只标出上半身),而跌倒mask需覆盖全身。我针对性改进:在数据增强中,强制加入albumentations.RandomScale(scale_limit=0.3, p=0.7),让模型看到更多尺度压缩的person;并在损失函数中,为person类单独增加BoundaryLoss权重(0.3 * BoundaryLoss + 0.7 * DiceLoss),BoundaryLoss专门惩罚mask边缘与真值边缘的距离。改造后,跌倒检出率提升至89.7%,误报率(False Alarm Rate)控制在0.8次/小时,达到商用阈值。
5.2 模型即服务(MaaS):封装为REST API的工程实践
为对接社区安防平台,我将模型封装为Flask REST API。关键设计点:
- 异步推理:使用
concurrent.futures.ThreadPoolExecutor,避免单请求阻塞; - 内存池管理:预加载模型到GPU,每次推理复用同一
torch.no_grad()上下文,避免重复加载开销; - 批量缓冲:当QPS>5时,自动启用batch buffering,将多个请求合并为一个batch推理,吞吐量提升3.2倍。
API端点POST /segment接收base64编码的JPEG图像,返回JSON:
{ "status": "success", "timestamp": "2023-10-15T08:23:45Z", "masks": [ { "class": "person", "confidence": 0.92, "bbox": [120, 340, 210, 580], "mask_base64": "iVBORw0KGgoAAAANSUhEUgAA..." } ] }mask_base64字段采用RLE(Run-Length Encoding)压缩,体积比原始PNG小68%,大幅降低网络传输延迟。
5.3 持续迭代闭环:用线上反馈驱动模型进化
部署不是终点,而是新循环的起点。我在API中埋点:每当用户点击“此检测错误”按钮,系统自动将该图像、模型预测、用户修正mask打包,存入feedback_queue。每周,运维人员从队列中抽取500条高质量反馈(经人工审核),加入训练集,触发增量训练。首月迭代后,person类IoU从79.5%提升至82.3%,小目标检出率再增2.1个百分点。这印证了一个朴素真理:最好的数据,永远来自真实战场。
我在实际使用中发现,模型对“穿荧光背心的保安”识别率极高(IoU 91.2%),但对“戴深色针织帽的老人”识别率骤降至53.4%。原因在于训练数据中荧光背心样本丰富,而深色针织帽在低光照下纹理特征被噪声淹没。于是,我定向采集了200张戴深色针织帽的老人图像,在暗光环境下用手机补光拍摄,并用GAN生成了800张风格迁移图,加入训练集。两周后,该类IoU回升至76.8%。这个过程没有玄学,只有对数据分布的敬畏和对业务痛点的死磕。
