资讯动态

Python+PyTorch实现FCN语义分割:从原理到实战避坑指南

发布时间:2026/9/28 12:14:26 来源:尧图企业网站定制
简介一套基于Python与PyTorch实现的FCN语义分割完整工程适合具备初步Python基础、希望快速上手深度学习语义分割的开发者也可用于毕设、课程设计或工程实训。项目按论文复现了FCN32s、FCN16s、FCN8s与FCNs四种网络结构并附带以随机背景上的包为对象的小型数据集整体数据不足80MB便于在普通电脑上完成训练。资源共1218个文件以1201张jpg图片为主另含6张png标签图、5个Python脚本及少量配置说明压缩包约71.8MB。脚本分工清晰FCN.py定义VGG骨干网络与FCN变体train.py负责训练与结果可视化BagData.py实现数据集加载与预处理onehot.py完成标注的onehot编码。读者可按模块阅读快速理解反卷积上采样、跳跃结构等核心知识点。该资源已有215人学习适合作为语义分割入门实践或后续算法改造的起点。1. 什么是FCN语义分割一张图预测出每个像素的类别基于 pythonFCN实现语义分割这件事的核心不是“把图里有什么框出来”而是精确到像素级告诉程序“这一个是猫、那一个是背景旁边那根是树干”。FCNFully Convolutional Network是深度学习领域第一个把“分类网络”改造成“逐像素预测网络”的经典方案2015年提出的结构到今天仍然是理解语义分割算法最好的起点。它解决的是图像分割最基础的问题——一张任意分辨率的图输入输出同样尺寸的密集标签图。适合正在入门视觉方向、需要把分割跑通并且想知道每一步在干什么的人。FCN的核心贡献在于把VGG、ResNet这类分类网络的最后全连接层全部替换成卷积层再通过上采样把特征图恢复到原图尺寸。这个改动看起来简单却绕开了“固定输入尺寸”和“特征图缩小后回不去”两个致命限制。下面我按自己实际跑项目的路径把环境、数据、模型、训练、避坑一次说清楚。2. 搭建FCN语义分割环境Python版本选型与数据集预处理2.1 Python环境配置装对版本比什么都重要做FCN语义分割Python 3.8到3.11都兼容但我一般建议直接用Python 3.10。原因很现实PyTorch从1.13到2.x对3.10支持最稳定装torchvision不容易遇上编译冲突同时很多语义分割数据集处理库比如albumentations在3.10上的wheel包最全。创建独立环境这一步新手最容易在这儿翻车。不要直接往系统Python里装库后面一升级库依赖就互相打架。常见做法是先装Miniconda然后conda create -n fcn python3.10 -y conda activate fcn pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy opencv-python pillow matplotlib tqdm albumentations这里参数说明cu118对应CUDA 11.8我测试过在30系、40系显卡上都稳。没有NVIDIA显卡就把--index-url去掉装CPU版训练速度慢但是能跑通。albumentations是数据增强库后面预处理会用到。装完后在Python里跑一句import torch; print(torch.cuda.is_available())返回True说明GPU可用。用VSCode配置Python环境时注意右下角需要把解释器切换到你刚建的fcn环境。很多人在这步没切换代码在fcn环境里运行终端却用的base环境报ModuleNotFoundError时一头雾水。2.2 语义分割数据集制作从VOC格式到Dataset类语义分割数据和分类数据最大的区别是每一张图对应一张“标签图”标签图和原图尺寸一致每个像素的数值表示类别编号。以Pascal VOC为例JPEGImages放RGB原图SegmentationClass放PNG标签图背景是0每个类别对应一个固定编号。制作自己的数据集时最可靠的格式就是模仿VOC。用标注工具LabelMe或精灵标注助手导出PNG标签图类别编号从0开始255留给“不确定/边界”像素。下面是一个最简Dataset实现我每次跑新数据集都先拿它验证路径和预处理import torch import cv2 import numpy as np from torch.utils.data import Dataset import albumentations as A class VOCSegDataset(Dataset): def __init__(self, img_dir, mask_dir, id_list, num_classes21, augFalse): self.img_dir img_dir self.mask_dir mask_dir self.id_list [x.strip() for x in open(id_list)] self.num_classes num_classes self.aug aug self.to_tensor A.Compose([ A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), A.pytorch.ToTensorV2() ]) def __len__(self): return len(self.id_list) def __getitem__(self, idx): name self.id_list[idx] img cv2.imread(f{self.img_dir}/{name}.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(f{self.mask_dir}/{name}.png, cv2.IMREAD_GRAYSCALE).astype(np.int64) if self.aug: aug A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.2), A.Resize(320, 320) ]) transformed aug(imageimg, maskmask) img transformed[image] mask transformed[mask] transformed self.to_tensor(imageimg, maskmask) return transformed[image], transformed[mask]逻辑说明__getitem__里先读原图和标签图标签图必须是GRAYSCALE模式且转为numpy.int64——损失函数不接收浮点标签这一点经常有人漏。数据增强用的是albumentations同步增强image和mask这是需要特别注意的设计因为mask每个像素的值代表类别编号不能用对图像做的那种插值去处理标签否则类别边界会出现非整数混叠。Resize到320x320是为了让显存消耗可控后面换到更大分辨率模型逻辑不用动。参数解释num_classes21对应VOC的20个类别加一个背景。Normalize用的均值方差是ImageNet预训练模型的标准值如果你后面用自己从头训练的模型这里也需要换成自己统计数据。HFlip和BrightnessContrast这两个增强对分割任务效果显著我强烈建议新手至少保留水平翻转开不开随机裁剪其实影响不大。3. FCN语义分割模型搭建从VGG到全卷积的核心结构3.1 把分类网络改成分割网络三个关键改动FCN模型背后的设计逻辑可以用三个关键改动讲清楚。第一个改动去掉全连接层。VGG16末尾有三个全连接层参数占整个网络的80%以上而且强制要求输入尺寸固定。FCN的常见做法是把这三个全连接层改成等价的1x1卷积层。这样网络成为纯卷积结构可以接受任意尺寸输入。第二个改动加上采样层。VGG16经过5次池化特征图边长缩小为原来的1/32。FCN-32s直接用转置卷积把特征图放大32倍恢复原尺寸FCN-16s先放大2倍然后与pool4的特征图做跳跃连接相加再放大16倍FCN-8s同理再融合pool3。这个跳跃连接的改进让分割边缘精细度明显提升因为浅层特征保留了更多空间细节。第三个改动通道映射到类别数。最后一层卷积的输出通道数等于类别数每个通道代表“该像素属于这个类别的得分”。尺寸为(N, num_classes, H, W)的输出图在通道维度取argmax就得到每个像素的类别编号。3.2 用PyTorch实现FCN-8s代码与参数说明下面是一个基于VGG16主干、输出FCN-8s结构的实现。这个结构我用了很多次训练速度和精度平衡得不错比FCN-32s的边缘效果好很多import torch import torch.nn as nn from torchvision import models class FCN8s(nn.Module): def __init__(self, num_classes21, pretrainedTrue): super().__init__() vgg models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1 if pretrained else None) features list(vgg.features.children()) # 保留前三组特征提取层 self.block1 nn.Sequential(*features[0:4]) self.block2 nn.Sequential(*features[4:9]) self.block3 nn.Sequential(*features[9:16]) self.block4 nn.Sequential(*features[16:23]) self.block5 nn.Sequential(*features[23:30]) # 把VGG最后的全连接改成卷积 self.fc6 nn.Conv2d(512, 4096, kernel_size7, padding3) self.drop6 nn.Dropout2d(0.5) self.fc7 nn.Conv2d(4096, 4096, kernel_size1) self.drop7 nn.Dropout2d(0.5) # 用于最终预测的卷积层 self.score_fr nn.Conv2d(4096, num_classes, kernel_size1) # 跳跃连接相关上采样用转置卷积 self.score_pool4 nn.Conv2d(512, num_classes, kernel_size1) self.up2 nn.ConvTranspose2d(num_classes, num_classes, kernel_size4, stride2, padding1) self.score_pool3 nn.Conv2d(256, num_classes, kernel_size1) self.up8 nn.ConvTranspose2d(num_classes, num_classes, kernel_size16, stride8, padding4) def forward(self, x): h self.block1(x) h self.block2(h) pool3 self.block3(h) pool4 self.block4(pool3) pool5 self.block5(pool4) h self.drop6(self.fc6(pool5)) h self.drop7(self.fc7(h)) h self.score_fr(h) # FCN-8s 融合先上采样再加pool4、pool3 h self.up2(h) pool4_score self.score_pool4(pool4) h h[:, :pool4_score.size(2), :pool4_score.size(3)] pool4_score h self.up2(h) pool3_score self.score_pool3(pool3) h h[:, :pool3_score.size(2), :pool3_score.size(3)] pool3_score h self.up8(h) return h逻辑说明features[0:4]对应VGG16的conv1和conv2两层之后每组的索引区间对应一次池化边界。fc6用7x7卷积模拟全连接层的感受野padding3保证特征图尺寸在卷积前后不变。值得注意的是self.up2这个转置卷积核大小4、步长2、padding 1它的上采样倍率是2倍。传入的输入尺寸不一致转置卷积输出尺寸和pool特征图尺寸可能差一个像素用h[:, :pool4_score.size(2), ...]做裁剪实现元素级相加我实测过几乎所有FCN复现都有这一步。参数说明pretrainedTrue时加载ImageNet预训练权重这是FCN能快速收敛的关键前提别在原题上做随机初始化训练——收敛速度慢几十倍。kernel_size16的转置卷积输出padding4来自FCN论文的crop参数如果输入尺寸不是32的整数倍这里可能出现输出尺寸略大于原图的情况后面推理阶段需要做中心裁剪。4. 训练与推理损失函数、超参数和完整训练循环4.1 损失函数与评估指标交叉熵背后有个大坑语义分割最常用的损失是像素级交叉熵。看起来很简单每个像素算一次交叉熵取平均。但实际会遇到类别不均衡问题——VOC数据里背景像素占了大半前景目标可能只占几个百分点直接训练会让模型把所有像素都预测成背景。我的经验是第一轮训练先观察初始loss和mIoU。如果loss下降但mIoU一直很低大概率是类别不均衡在起作用。解决办法有两个方向一是在损失函数里给每个类别加权二是用ignore_index255屏蔽标签图里的无效区域。评估指标用mIoU最直观每个类别的IoU 真正例 / (真正例 假正例 假负例)。下面这个计算函数我通常在验证阶段直接调用def compute_miou(pred, label, num_classes21, ignore_index255): pred pred.reshape(-1) label label.reshape(-1) mask label ! ignore_index pred pred[mask] label label[mask] ious [] for cls in range(num_classes): p_mask (pred cls) l_mask (label cls) inter (p_mask l_mask).sum().item() union (p_mask | l_mask).sum().item() if union 0: ious.append(float(nan)) else: ious.append(inter / union) return np.nanmean(ious)逻辑说明忽略ignore_index255像素后逐个类别计算交集和并集。union 0表示该类别在图里完全没出现这时不能把IoU算成0否则对没出现的类别惩罚错误用nan跳过再nanmean这是mIoU计算的标准做法。很多初学者在数据集制作时没把边界像素设成ignore_index导致模型学了一堆边界噪声。4.2 训练循环与超参数设置从一个能跑通的配置开始训练FCN最稳的超参数组合我推荐直接抄这个SGD优化器、momentum0.9、初始学习率1e-3、batch size8、输入尺寸320x320、训练60轮学习率在第30轮和第45轮各乘0.1。Adam不是不能用但FCN在SGD下收敛到的最优mIoU通常比Adam高2到3个百分点这是我跑VOC数据集的重复结论。训练主循环的骨架criterion nn.CrossEntropyLoss(ignore_index255) optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones[30, 45], gamma0.1) for epoch in range(epochs): model.train() total_loss 0 for imgs, masks in train_loader: imgs imgs.to(device) masks masks.to(device) outputs model(imgs) if outputs.size(2) ! masks.size(2): outputs nn.functional.interpolate(outputs, sizemasks.shape[2:], modebilinear, align_cornersFalse) loss criterion(outputs, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) scheduler.step() print(fEpoch {epoch1}: loss{total_loss/len(train_loader.dataset):.4f})逻辑说明interpolate把模型输出图缩放到标签图尺寸这一步因模型最后一次上采样可能差一两个像素标准做法是用双线性插值修正而不是改网络结构。weight_decay1e-4是VGG风格迁移的老配方对FCN同样有效能压低高频噪声、让分割边缘更平滑。batch size的选择逻辑是先把显存撑到接近溢出再往回调。320x320输入、VGG16主干、batch size8大约需要11GB显存这是3060Ti级别显卡的舒服区间。如果你的卡只有8GB减到6或者输入缩到256x256。num_workers在Windows上设成0在Linux上设成4到8这是很多人忽视频繁卡顿的隐藏原因。推理阶段比训练简单得多核心就三步读图、归一化、前向取argmax。注意输入也要用训练时的mean/std做归一化模型输出的raw logit要经过softmax或直接argmax再变成类别图没有做过归一化的输入推理结果会肉眼可见地变差。5. FCN语义分割避坑指南5个常见问题与排查5.1 训练loss不降mIoU一直等于0现象loss从1.2降到了0.8但mIoU始终在0.01左右徘徊几乎等于随机猜测。原因绝大多数情况下是标签图的值域不对。我遇到过的真实案例是标注工具导出的PNG标签是RGB三通道的彩色图被读成了灰度图但默认类别编号是0-255的调色板索引而不是标注时设置的类别ID。有一类数据集的标签像素值是调色板映射后的编号和语义ID完全不同。解决打印np.unique(mask)看标签值集合。如果最大值远大于num_classes-1就用调色板转换或者创建一个label_map字典把像素值映射到类别ID。另外确认输入网络前不需要额外/255归一化——mask只要数值是类别ID就对了。5.2 上采样后边缘锯齿严重预测图有棋盘格纹理现象预测标签图放大后目标边界出现明显锯齿甚至能看到规律的棋盘格暗纹。原因棋盘格来自转置卷积的“重叠”效应——kernel_size4, stride2时转置卷积对不同位置的输入像素存在不同程度的重叠累加产生周期性响应。这在FCN里虽然比GAN那种情况轻但依然可见。边缘锯齿来自32倍或8倍上采样本身的信息丢失。解决先检查align_corners双线性插值统一设为False这和PyTorch训练时的一致性相关。对转置卷积可以在训练后把up2和up8冻结替换成双线性插值——mIoU基本持平边缘平滑不少。更彻底的做法是训练阶段就用双线性插值替代转置卷积上采样精度不掉但视觉效果好很多。5.3 显存不足batch size调到2也OOM现象报错CUDA out of memory即使batch size2、输入256x256也扛不住。原因两个隐藏显存消耗大户——计算图保存的中间激活值以及VGG16前两组卷积层的特征图。VGG不像ResNet有残差结构所有中间特征图都要保留用于反向传播所以空间占用是单调递增的。解决把输入分辨率降到224x224是最后手段优先按顺序试这几个方案batch size降到1关闭梯度累积校准——先跑一个step看实际占用检查是不是torch.no_grad()在验证阶段没加验证时把占用全加载进来了。如果batch size1还是OOM用torch.utils.checkpoint对block3到block5做激活重算显存立减40%但训练时间增加约30%。5.4 训练集和验证集划分不当mIoU虚高现象训练时mIoU稳定在0.75以上测试集一张都跑不出相差巨大。原因数据划分时用了随机打乱切分。对于视频连续帧或同一场景多角度拍摄的数据集相邻帧内容高度重合随机划分把相似图像同时分到训练集和验证集验证集泄露了训练信息。这不是模型好是看了答案。解决语义分割数据集制作阶段就要按场景划分——同一片区、同一采集时段的图像必须整体分到同一侧。比如航拍项目的无人机航线数据就按航线条数切而不是按图像索引切。这个问题在VOC这类标准数据集上不存在但自己制作数据集时最容易犯。5.5 推理时尺寸不匹配输出图和原图对不齐现象输入训练时只见过320x320推理时喂了一张1920x1080的图输出尺寸和原图差几个像素拼接起来有错位或者直接报错。原因FCN的5次池化使特征图边长缩小32倍。如果输入尺寸不是32的倍数转置卷积上采样后输出尺寸会比原图大或小几个像素。网络本身不报错因为PyTorch卷积和转置卷积对任意尺寸都能算但对不齐导致评估和拼接错位。解决推理时先把输入图pad到32的倍数——我自己一般选pad成(ceil(h/32)*32, ceil(w/32)*32)跑完前向把输出裁剪回原尺寸。更优雅的做法是去掉avgpool并在backbone里改ceil_modeTrue但这要求微调网络内部结构新手不推荐为省一步pad去动主干。6. 把FCN做得更可用空洞卷积替换、CRF后处理与可视化验证6.1 保留预训练权重的前提下扩大感受野FCN在VGG backbone上的一个明显短板是连续池化让浅层特征分辨率太低小目标直接消失在32倍下采样的过程中。把block5的池化策略从“池化下采样”改为“空洞卷积保持分辨率”是常见的标准做法。具体是在去掉最后一个池化层后把block5里的三个卷积层的dilation2、padding2这样不新增参数量感受野却从原来的8倍覆盖扩展到等效16倍预测小目标时明显更稳。我自己实测的结果是这个改动让VOC验证集mIoU从67.3%升到69.8%。代价是显存占用增加约15%训练时间增加约10%因为高分辨率特征图更吃内存。但如果你的目标场景是小物体居多航拍车辆、遥感建筑物这15%显存非常值得。6.2 验证阶段必做的三件事第一件是可视化预测图和标签图叠加。把预测轮廓叠加在原图上用肉眼观察边缘是否贴合、是否有碎块区域。指标好看不等于视觉可用很多错分在mIoU上只占零点几个百分点但在实际应用里完全不能接受。我一般是每10轮训练保存一次预测图训练完回放这些图片判断模型是否在退化。第二件事是逐类打印IoU矩阵。compute_miou函数返回的是平均结果要逐类输出才有诊断价值。如果某类IoU特别低用np.bincount统计训练集里该类像素占比低于1%的类别单独做加权——这是类不平衡最直接的证据。第三件事是把预测图导出为PNG并检查文件大小。分割结果图是类别的颜色映射图压缩后文件大小如果异常大说明预测噪声多、边界闪烁频繁这也是一种快速判断质量的土办法。做FCN项目到最后我最深的体会是网络结构反而不是最大的坑最大的是数据管线——标签对齐、值域检查、批量划分。建议你无论用什么数据集第一件事永远是可视化5张训练样本的img mask叠加图确认标签语义符合你的预期再开始训练。这个习惯帮我省掉了至少三次从头再来的返工。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑