当你第一次听到"语义分割"这个词时,可能会觉得很高深。其实说白了,它就是给图片中的每个像素"贴标签"的过程。比如一张街景照片,我们要让AI自动识别哪里是道路、哪里是行人、哪里是车辆,这就是语义分割的典型应用场景。
在众多语义分割模型中,DeepLabV3-ResNet50的组合特别值得推荐。我去年做过一个宠物医院的智能诊断项目,就是用这个架构实现了对X光片中宠物骨骼的精确分割。为什么选择它?三个理由:
第一,扩张卷积(也叫空洞卷积)的设计很巧妙。普通卷积在提取特征时会缩小图像尺寸,但DeepLabV3通过间隔采样(比如每隔一个像素取一次)既能扩大感受野,又不会丢失细节。就像我们用"跳着看"的方式快速浏览一本书,既把握了整体内容,又不会漏掉关键段落。
第二,残差连接解决了深层网络的训练难题。ResNet50的50层结构本应面临严重的梯度消失问题,但通过引入"短路连接",让信息可以跨层传递。这就像在爬山时设置休息站,既不会累垮(梯度消失),又能登顶(完成训练)。
第三,预训练权重大幅降低训练成本。PyTorch官方提供的在ImageNet上预训练的ResNet50权重,能让模型快速收敛。实测下来,用预训练权重比从头训练快3-5倍,这对计算资源有限的开发者特别友好。
很多教程都假设你使用标准VOC格式数据集,但现实项目中我们往往要处理各种非标数据。去年我给一家园艺公司做植物病害分割时,就遇到了数据格式混乱的问题。下面分享我的实战经验:
labelme标注时,按Ctrl+鼠标滚轮可以快速调整标注精度安装labelme很简单:
bash复制pip install labelme -i https://pypi.tuna.tsinghua.edu.cn/simple
但有几个坑要注意:
_background_转换掩码的Python脚本可以这样写:
python复制import json
import numpy as np
from PIL import Image
from labelme import utils
def json_to_mask(json_path, output_path):
with open(json_path) as f:
data = json.load(f)
img = utils.img_b64_to_arr(data['imageData'])
lbl, _ = utils.shape.labelme_shapes_to_label(img.shape, data['shapes'])
Image.fromarray(lbl.astype(np.uint8)).save(output_path)
虽然PyTorch支持自定义数据集格式,但用VOC格式兼容性最好。目录结构应该是:
code复制VOCdevkit/
└── VOC2012/
├── Annotations/ # 原始JSON标注
├── ImageSets/
│ └── Segmentation/ # 训练/验证集名单
├── JPEGImages/ # 原始图像
└── SegmentationClass/ # 转换后的掩码
数据划分建议比例:
直接从PyTorch官方vision库引用DeepLabV3是最稳妥的做法:
python复制from torchvision.models.segmentation import deeplabv3_resnet50
model = deeplabv3_resnet50(pretrained=True, progress=True)
python复制model.classifier[4] = nn.Conv2d(256, num_classes, kernel_size=1)
python复制optimizer = torch.optim.SGD([
{'params': model.backbone.parameters(), 'lr': base_lr*0.1},
{'params': model.classifier.parameters(), 'lr': base_lr}
], momentum=0.9)
python复制criterion = nn.CrossEntropyLoss(ignore_index=255)
loss = criterion(output['out'], target) + 0.5*criterion(output['aux'], target)
python复制scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
output = model(input)
loss = criterion(output, target)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
训练完成后,模型优化是最后一道坎。我总结了几点经验:
python复制model = torch.quantization.quantize_dynamic(
model, {torch.nn.Conv2d}, dtype=torch.qint8
)
python复制from torch.nn.utils import prune
parameters_to_prune = [(module, 'weight') for module in model.backbone]
prune.global_unstructured(parameters_to_prune, pruning_method=prune.L1Unstructured, amount=0.2)
| 方案 | 延迟(ms) | 内存占用 | 适用场景 |
|---|---|---|---|
| ONNX Runtime | 45 | 1.2GB | 跨平台部署 |
| TorchScript | 38 | 1.5GB | PyTorch生态 |
| TensorRT | 22 | 0.8GB | NVIDIA硬件 |
导出ONNX格式的示例:
python复制dummy_input = torch.randn(1, 3, 512, 512)
torch.onnx.export(
model, dummy_input, "model.onnx",
input_names=["input"], output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}
)
最后提醒大家,语义分割是个数据饥渴型任务。在算力有限的情况下,与其纠结模型结构,不如多花时间优化数据质量。我在实际项目中测试过,清洗掉20%的低质量标注数据,能让mIoU直接提升5个点以上。