简介:这份资源围绕全卷积网络(FCN)在Penn-Fudan Database上的行人检测与分割实践展开,面向已掌握卷积神经网络基础、希望深入图像分割的中高级学习者,可用于理解端到端像素级预测、上采样与跳跃连接等关键机制。压缩包共533个文件,约184.68MB,以340张png图像与分割掩模、172个txt标注说明为主,辅以6个py训练脚本、4个npy统计指标文件及4个xml标注,完整覆盖数据加载、训练与评估流程。已有505人学习下载。资源中保留了不同batch与epoch配置下的训练记录,以及meanIU、meanPixel等评估结果,便于读者对照实验设置、复现训练过程并分析分割精度变化,是学习FCN语义分割与行人分割任务的实用素材。
1. 全卷积网络跑 Penn-Fudan:一份能直接复现的语义分割资源
拿到一份标注为「全卷积网络(Penn-Fudan Database 数据集)」的资源包,很多人第一反应是——这不就是个行人分割的练手项目吗?但真正拆开跑一遍就会发现,Penn-Fudan 这个数据集小得有点“刁钻”:170 张图、20 个行人、场景集中在校园步道,背景单一、目标尺度跨度大,用它来验证 FCN 的端到端流程,恰恰能把数据加载、标签对齐、上采样还原这几处最容易翻车的地方暴露出来。这份资源的价值不在于刷多高的 mIoU,而在于给你一条从原始图像到逐像素掩码的完整链路,适合刚接触语义分割、想搞懂 FCN 到底怎么把分类网络变成分割网络的从业者,也适合需要一个小数据集快速验证训练脚本的老手。下面按“资源是什么、怎么跑、坑在哪”的顺序拆。
2. FCN 的结构选型与 Penn-Fudan 的适配逻辑
2.1 为什么用全卷积替换全连接
FCN 的核心动作是把分类网络末端的全连接层换成卷积层,让输出从“一个类别向量”变成“一张类别热力图”。以 VGG16 为骨干时,原始网络的 fc6、fc7 是两个 4096 维全连接,FCN 把它们分别替换成 7×7×4096 和 1×1×4096 的卷积,这样网络就能接受任意尺寸输入,输出空间维度也不再被压成一维。这一步的意义在于:分割需要每个像素都有预测,全连接层会把空间信息彻底抹掉,换成卷积后空间结构得以保留。
在 Penn-Fudan 上,输入图像常见尺寸在 400×300 到 700×500 之间,如果沿用固定尺寸裁剪再缩放,行人边缘的细长结构(比如腿部)容易在缩放中丢失。全卷积结构允许直接喂原图,配合后面要讲的标签对齐,能减少一次不必要的插值误差。资源里的骨干选择通常是 VGG16 或 ResNet50,前者结构简单、特征图通道数规整,适合第一次跑通;后者残差连接在浅层小数据集上收敛更稳,但要注意下采样倍率不同带来的上采样参数调整。
2.2 跳级结构(Skip)在 170 张图上的取舍
FCN-32s 只对最后一层特征做 32 倍上采样,边缘非常粗糙;FCN-16s 融合 pool4 的特征,FCN-8s 再融合 pool3,边缘逐步变细。Penn-Fudan 只有 170 张训练图,FCN-8s 参数量相对多、浅层特征融合后对小目标更敏感,但也更容易过拟合背景纹理。我的建议是:先用 FCN-32s 跑通全流程确认标签对齐无误,再切 FCN-8s 看 mIoU 是否真的提升;如果提升不明显甚至下降,说明数据量撑不起多级融合,退回 16s 更稳。
资源里如果提供了预训练权重加载脚本,注意骨干部分(VGG 的 conv1 到 conv5)可以加载 ImageNet 预训练,而新增的 score 层和上采样层必须随机初始化。常见做法是冻结前几层训练几个 epoch 再解冻,这在 170 张图上能明显减少早期震荡。
2.3 数据管线的关键参数
Penn-Fudan 的标注是每张图对应一个 PNG 掩码,像素值 0 为背景、1 为行人(部分版本用 255 表示前景)。数据加载时最容易出错的是掩码和原图尺寸不一致——原始标注可能经过缩放,直接 resize 原图而不处理掩码会导致像素错位。下面这段是资源里数据加载的核心逻辑,我按可复现的方式重写并加了注释:
import os import numpy as np from PIL import Image import torch from torch.utils.data import Dataset class PennFudanDataset(Dataset): def __init__(self, root, transforms=None): self.root = root self.transforms = transforms # 只取 PNG 原图,掩码单独匹配 self.imgs = [f for f in os.listdir(root) if f.endswith('.png') and '_mask' not in f] def __getitem__(self, idx): img_path = os.path.join(self.root, self.imgs[idx]) mask_path = img_path.replace('.png', '_mask.png') img = Image.open(img_path).convert('RGB') mask = Image.open(mask_path) mask = np.array(mask) # 统一二值化:部分标注用 255,这里强制成 0/1 mask = (mask > 0).astype(np.uint8) if self.transforms is not None: # 注意:图像和掩码必须用同一组几何变换参数 img, mask = self.transforms(img, mask) return img, torch.from_numpy(mask).long() def __len__(self): return len(self.imgs)这段代码的逻辑说明:imgs列表过滤掉掩码文件,避免把_mask.png当原图读入;掩码二值化用> 0而不是== 1,是因为不同来源的 Penn-Fudan 标注像素值不统一,这一步是血泪经验,直接写== 1在部分图上会得到全零掩码。transforms必须同时作用于图像和掩码,且几何变换(裁剪、翻转、缩放)参数要一致,颜色变换只作用于图像。参数上,convert('RGB')不能省,Penn-Fudan 里有灰度图混入,不转三通道会在骨干第一层卷积报通道数不匹配。
3. 从零跑通训练:损失、上采样与评估
3.1 损失函数与类别不平衡处理
Penn-Fudan 的行人像素占比通常不到 10%,背景占绝对多数,直接用交叉熵会让网络倾向于全预测背景。资源里常见做法是给前景加权,或者用带 ignore_index 的交叉熵配合权重张量。我一般会先统计训练集前景像素比例,按weight = [1, 1/ratio]量级设置,再根据验证集 mIoU 微调。下面是一个可抄的损失配置:
import torch import torch.nn as nn # 前景权重按实际统计调整,这里给一个经验起点 class_weights = torch.tensor([1.0, 8.0]).float().cuda() criterion = nn.CrossEntropyLoss(weight=class_weights, ignore_index=255) # 前向:输出 shape 为 [N, C, H, W],标签为 [N, H, W] outputs = model(images) # C=2,背景+行人 loss = criterion(outputs, masks)逻辑说明:CrossEntropyLoss内部对 logits 做 softmax,所以模型最后一层不要加 softmax。ignore_index=255用于跳过标注中的边界模糊像素(如果资源标注里有 255 边界)。权重 8.0 只是起点,实际跑的时候看验证集里前景召回,如果漏检多就往上加,如果误检多就往下调。参数上,weight张量的长度必须等于类别数,顺序和模型输出通道一致,搞反了会把背景当行人训练,这是新手最容易翻的车之一。
3.2 上采样还原与输出对齐
FCN 的上采样常用转置卷积或双线性插值加卷积。转置卷积可以学习上采样核,但容易产生棋盘格伪影;双线性插值加 1×1 卷积更平滑,在 Penn-Fudan 这种小数据上更稳。资源里如果用的是nn.ConvTranspose2d,注意output_padding参数要配合 stride 设置,否则输出尺寸会比输入少几个像素,和标签对不上时报错信息往往只提示 shape mismatch,不告诉你差在哪。
# 双线性上采样 + 1x1 卷积的写法,输出尺寸严格对齐输入 upsample = nn.Sequential( nn.Upsample(scale_factor=8, mode='bilinear', align_corners=False), nn.Conv2d(256, 2, kernel_size=1) # 256 为融合后特征通道,2 为类别数 )参数说明:scale_factor=8对应骨干总下采样倍率,VGG16 的 pool5 是 32 倍,如果只取到 pool4 融合则是 16 倍,这里要按实际特征图层级改。align_corners=False在 PyTorch 新版里是推荐值,和标签对齐更准。kernel_size=1的卷积负责把通道压到类别数,不改变空间尺寸。跑之前用一张图打印outputs.shape和masks.shape,两者后两维必须完全相等,不等就先查上采样倍率,别急着调学习率。
3.3 训练循环与评估指标
训练循环本身不复杂,关键是每个 epoch 后在验证集上算 mIoU,而不是只看 loss。Penn-Fudan 的验证集很小,建议按 8:2 划分并固定随机种子,否则每次跑出来的 mIoU 波动会让你误以为模型在退化。评估时把预测的 argmax 结果和标签都展平,逐类算交并比再平均:
def compute_miou(pred, target, num_classes=2): pred = pred.argmax(dim=1).view(-1) target = target.view(-1) ious = [] for cls in range(num_classes): pred_inds = pred == cls target_inds = target == cls intersection = (pred_inds & target_inds).sum().item() union = (pred_inds | target_inds).sum().item() if union == 0: continue ious.append(intersection / union) return sum(ious) / len(ious)逻辑说明:argmax(dim=1)把通道维压成类别索引,view(-1)展平后逐类算交集并集。union == 0时跳过,避免某类在 batch 里完全没出现导致除零。参数上num_classes=2对应背景加行人,如果资源里把边界单独设了一类,这里要改成 3 并同步损失权重。训练时学习率建议从 1e-4 起,骨干冻结阶段可以到 1e-3,解冻后降一个量级,batch size 在显存允许下尽量大,小数据集上 batch 太小会让 BN 统计不稳。
4. 避坑与排查:Penn-Fudan 上最容易翻车的五处
4.1 掩码全零或全一
现象:训练几个 epoch 后 loss 几乎不动,验证集 mIoU 恒为某个固定值。原因:掩码读取时像素值判断写死成== 1,而部分标注用 255 表示前景,导致前景全被当背景。解决:统一用> 0二值化,并在数据加载后随机抽几张图打印前景像素占比,占比为 0 或 1 的样本要单独查。
4.2 图像与掩码几何变换不同步
现象:训练 loss 能降,但预测掩码整体偏移几个像素,行人边缘对不齐。原因:图像做了随机裁剪或翻转,掩码没跟着做,或者用了两套独立的 transform 实例导致随机参数不同。解决:把几何变换封装成同时接收(img, mask)的函数,内部只生成一次随机参数;颜色变换单独作用于图像。验证方法是取一个 batch 把图像和掩码叠加可视化,边缘重合才算过。
4.3 上采样后尺寸差几个像素
现象:CrossEntropyLoss报 shape 不匹配,或者强行 resize 后 mIoU 异常低。原因:转置卷积的output_padding没配好,或者骨干输入尺寸不是 32 的整数倍导致下采样取整丢失。解决:输入前把图像 pad 到 32 的倍数,记录 pad 量,输出后再裁回去;或者改用双线性上采样加 1×1 卷积,尺寸严格按scale_factor还原。跑之前固定打印一次 shape 对比。
4.4 前景权重设反
现象:模型几乎把所有像素预测成行人,背景 mIoU 崩掉。原因:CrossEntropyLoss的weight顺序和类别索引不一致,把大权重给了背景。解决:确认类别索引 0 是背景、1 是行人,权重张量按同序排列;先用不加权的损失跑一个 baseline,再逐步加权重观察前景召回变化。
4.5 验证集波动误判模型退化
现象:同一份代码两次跑,mIoU 差十几个点。原因:Penn-Fudan 验证集只有三十来张图,随机划分和随机种子没固定,单张难例就能拉低整体指标。解决:固定划分文件和随机种子,评估时同时输出每张图的 IoU,看是整体下降还是个别难例拖累;对比实验至少跑三个种子取均值再下结论。
5. 进阶验证:用单图推理和混淆矩阵确认模型真的学到了东西
跑通训练只是第一步,要确认模型不是靠背景先验蒙对,得做两件事:单图推理可视化和混淆矩阵。单图推理时把原图、预测掩码、真值掩码并排保存,重点看行人腿部和两人重叠区域,这两处是 FCN 在 Penn-Fudan 上最容易糊的地方。混淆矩阵则能告诉你错误主要来自前景漏检还是背景误检。
import matplotlib.pyplot as plt model.eval() with torch.no_grad(): img, mask = dataset[0] pred = model(img.unsqueeze(0).cuda()).argmax(dim=1).squeeze(0).cpu() fig, axes = plt.subplots(1, 3, figsize=(12, 4)) axes[0].imshow(img.permute(1, 2, 0).numpy()) axes[1].imshow(pred.numpy(), cmap='gray') axes[2].imshow(mask.numpy(), cmap='gray') plt.savefig('infer_check.png', dpi=150)这段代码的逻辑:unsqueeze(0)补 batch 维,argmax取类别索引,squeeze(0)去掉 batch 维后转 CPU 画图。参数上dataset[0]换成任意索引即可,建议挑一张有两人重叠的图,单人和多人各看一张。保存的对比图里如果预测掩码比真值“胖一圈”,通常是上采样平滑过度,可以把双线性换成最近邻试一版;如果“瘦一圈”,多半是前景权重不够或训练 epoch 不足。
混淆矩阵的算法很简单,把预测和标签展平后统计四格:真背景预测背景、真背景预测前景、真前景预测背景、真前景预测前景。Penn-Fudan 上如果“真前景预测背景”明显偏多,说明召回不足,优先加前景权重或增加 FCN-8s 的浅层融合;如果“真背景预测前景”偏多,说明模型对背景纹理过拟合,可以加随机裁剪和颜色抖动增强。我一般会把混淆矩阵和单图可视化一起存进实验记录,换骨干或换损失时对照看,比只盯一个 mIoU 数字靠谱得多。
从那以后我每次拿到小数据集的分割资源,都强制先跑一遍单图推理加混淆矩阵再谈调参,因为指标好看但掩码糊成一片的情况见得太多了。希望这份拆解能帮你少走几个弯路。
本文还有配套的精品资源,点击获取