简介:这份资源围绕全卷积网络(FCN)在Penn-Fudan Database上的行人检测与分割实践展开,面向具备一定深度学习基础、希望掌握像素级语义分割的开发者与学习者。内容涵盖FCN的核心原理,即以卷积层替代全连接层、通过上采样与跳跃连接恢复空间分辨率,并结合Penn-Fudan数据集的精细标注完成端到端训练与预测。压缩包共533个文件,包含340个png图像与分割掩模、172个txt标注文件、6个py训练脚本、4个npy指标文件及4个xml配置,另有若干模型权重与工程文件,整体约184.68MB,目录结构便于按数据、代码与结果模块检索。资源中提供了多组不同batch、epoch与学习率下的训练配置记录,以及meanIU、meanPixel等评估指标文件,可帮助读者复现实验、对比参数影响并理解分割精度评估方式。目前已有504人学习下载,适合用于智能监控、自动驾驶等场景下的分割模型入门与调参参考。
1. 全卷积网络:为什么它能把“逐像素分类”这件事做对
如果你第一次跑 Penn-Fudan Database 上的语义分割,大概率会经历一个反直觉的瞬间:模型输出的不是一张“图”,而是一张和原图同宽同高的整数标签矩阵,每个像素点对应一个类别编号。更反直觉的是,这个矩阵不是靠滑窗逐块分类拼出来的,而是网络一次前向传播直接吐出来的。这背后就是全卷积网络(Fully Convolutional Network, FCN)的核心结论:把传统卷积网络末端的全连接层换成卷积层,网络就能接受任意尺寸输入,并输出同样空间分辨率的密集预测。
这件事为什么值得搞懂?因为 Penn-Fudan Database 这个行人分割数据集,图像里行人尺度差异大、背景杂乱,如果还用“分类网络 + 后处理”的思路,边界会碎、小目标会丢。FCN 把“分类”变成“逐像素分类”,本质上是在特征图上做稠密预测,而不是在整图上做单标签判断。理解了这一点,你再看后面要讲的跳跃连接、上采样、损失函数,就不会觉得它们是零散技巧,而是一套连贯的设计。
2. 从分类到分割:全连接层为什么必须被卷积替代
2.1 全连接层的空间信息丢失问题
传统分类网络(比如早期用于图像分类的卷积结构)在末端会接几个全连接层,把特征图展平成一个长向量,再映射到类别数。这个操作有一个隐含代价:空间维度被彻底压扁。假设最后一层卷积输出是 7×7×512,展平后变成 25088 维向量,此时每个输出神经元都连接了所有空间位置,网络不再保留“哪个特征在哪个位置”的显式结构。
对于分类任务,这没问题,因为只需要一个整图标签。但对于 Penn-Fudan Database 这种需要标出每个行人像素的任务,展平等于把定位信息扔掉了。你可能会想:那我在展平前记录位置不就行了?问题在于,全连接层的权重是全局共享的,同一个权重会作用在所有空间位置上,网络无法区分“左上角的行人和右下角的行人”在特征上的差异。常见的实现思路是:与其在展平后补救,不如根本不让它展平。
2.2 卷积层替代全连接层的等价性
FCN 的做法很直接:把全连接层换成卷积核大小等于输入特征图空间尺寸的卷积层。比如输入是 7×7×512,全连接层输出 4096 维,等价于用一个 7×7×512 的卷积核做卷积,输出 1×1×4096。如果输入变成 14×14×512,同样的卷积核会输出 2×2×4096,相当于在四个空间位置上分别做了一次全连接。
这个等价性带来两个好处:第一,网络可以接受任意尺寸输入,因为卷积核只关心通道数,不关心空间大小;第二,输出变成空间特征图,每个位置对应原图一个区域,为后续上采样回原图尺寸做好了准备。下面这段代码展示了如何用卷积层替换全连接层,并保持参数量不变:
import torch import torch.nn as nn # 假设最后一层卷积输出为 7x7x512 # 传统全连接:展平后接 Linear(512*7*7, 4096) fc = nn.Linear(512 * 7 * 7, 4096) # FCN 等价卷积:卷积核大小 7x7,输出通道 4096 conv_equiv = nn.Conv2d(512, 4096, kernel_size=7, padding=0) # 参数量对比 print(fc.weight.numel()) # 512*7*7*4096 = 102760448 print(conv_equiv.weight.numel()) # 512*7*7*4096 = 102760448参数说明:in_channels=512对应上一层输出通道,out_channels=4096对应原全连接层输出维度,kernel_size=7等于输入特征图空间尺寸。这样替换后,如果输入变成 14×14×512,卷积输出就是 2×2×4096,每个空间位置都相当于一次全连接,但权重共享,参数量不变。这就是 FCN 能处理任意尺寸输入的根本原因。
2.3 上采样与跳跃连接:如何把 1/32 的特征图还原回原图
替换全连接层后,网络输出的是低分辨率特征图。以常见结构为例,经过多次池化后,特征图空间尺寸可能只有原图的 1/32。要得到逐像素预测,必须上采样回原图尺寸。最直接的做法是双线性插值,但这样会丢失细节。FCN 的设计取舍是:把不同池化阶段的特征图拿来做跳跃连接,先上采样深层特征,再与浅层特征逐元素相加,最后再上采样到原图尺寸。
Penn-Fudan Database 里行人边界往往比较细,如果只用最深层的 1/32 特征,边界会模糊。跳跃连接把 1/16、1/8 的特征引进来,相当于让浅层的高分辨率信息参与最终预测。常见的实现思路是:FCN-8s 用 1/8 和 1/16 的特征,FCN-16s 只用 1/16,FCN-32s 不用跳跃连接。下面是一个简化的跳跃连接实现:
class FCN8s(nn.Module): def __init__(self, n_class=2): super().__init__() # 假设 backbone 输出 pool3(1/8), pool4(1/16), pool5(1/32) self.score_pool5 = nn.Conv2d(512, n_class, 1) self.score_pool4 = nn.Conv2d(512, n_class, 1) self.score_pool3 = nn.Conv2d(256, n_class, 1) self.upsample2 = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True) self.upsample8 = nn.Upsample(scale_factor=8, mode='bilinear', align_corners=True) def forward(self, pool3, pool4, pool5): # 1/32 分支上采样到 1/16,与 pool4 分数相加 score5 = self.upsample2(self.score_pool5(pool5)) score4 = self.score_pool4(pool4) + score5 # 再上采样到 1/8,与 pool3 分数相加 score3 = self.score_pool3(pool3) + self.upsample2(score4) # 最后上采样回原图 return self.upsample8(score3)逻辑说明:score_pool5把 1/32 特征映射到类别数,上采样 2 倍后与score_pool4相加,得到 1/16 的融合特征;再上采样 2 倍与score_pool3相加,得到 1/8 的融合特征;最后上采样 8 倍回原图。参数n_class=2对应 Penn-Fudan Database 的行人/背景二分类。这种逐级融合的方式,让深层语义信息和浅层空间信息在同一个尺度上相加,比直接上采样 32 倍要稳得多。
3. 训练机制:损失函数、类别不平衡与梯度传播
3.1 逐像素交叉熵与空间维度对齐
FCN 的训练目标是对每个像素做分类,所以损失函数通常是逐像素交叉熵。假设输出是N×C×H×W,标签是N×H×W,每个位置取对应类别的负对数似然,再对所有像素求平均。这里有一个容易翻车的点:上采样后的输出尺寸必须和标签尺寸严格一致,否则交叉熵会报错或静默错位。
Penn-Fudan Database 的图像尺寸不统一,如果直接 resize 到固定尺寸,标签也要同步 resize,且必须用最近邻插值,不能用双线性,否则标签会出现小数类别。常见的实现思路是:在数据加载阶段就把图像和标签一起 resize 到同一尺寸,标签用NEAREST,图像用BILINEAR。下面这段代码展示了如何对齐尺寸并计算损失:
import torch.nn.functional as F def pixelwise_loss(logits, target): # logits: N×C×H×W, target: N×H×W # 如果尺寸不一致,先上采样 logits 到 target 尺寸 if logits.shape[-2:] != target.shape[-2:]: logits = F.interpolate(logits, size=target.shape[-2:], mode='bilinear', align_corners=True) return F.cross_entropy(logits, target, ignore_index=255)参数说明:ignore_index=255用于忽略边界或未标注像素,Penn-Fudan Database 里有些区域可能没有标注,设为 255 可以避免它们影响梯度。F.interpolate的mode='bilinear'用于 logits,因为 logits 是连续值,双线性插值合理;标签不能用这种方式。
3.2 类别不平衡:行人像素远少于背景像素
Penn-Fudan Database 里行人像素占比通常不到 20%,背景占 80% 以上。如果直接用逐像素交叉熵,网络会倾向于把所有像素预测成背景,因为这样损失也能降得比较低。这不是玄学,是梯度被背景样本主导了。常见的应对方式有三种:给行人类别加权、用 Dice Loss 或 IoU Loss、或者用 Focal Loss 降低易分类样本的权重。
加权交叉熵最直接:给背景类权重 1,行人类权重设为背景像素数除以行人像素数。这样行人像素的梯度会被放大,网络不敢忽略它们。下面是一个加权交叉熵的实现:
def weighted_cross_entropy(logits, target, weight): # weight: 长度为 C 的 tensor,比如 [1.0, 5.0] return F.cross_entropy(logits, target, weight=weight, ignore_index=255)参数说明:weight是每个类别的权重,Penn-Fudan Database 二分类时通常设为[1.0, 4.0]到[1.0, 8.0]之间,具体看行人占比。如果行人占比 20%,背景 80%,权重比可以设为 4:1。注意权重不是越大越好,过大会导致背景误判增多,边界变毛糙。
3.3 梯度传播:上采样层如何回传梯度
上采样操作(双线性插值或反卷积)在反向传播时,梯度会按插值权重回传到对应的低分辨率位置。双线性插值的梯度是固定的线性映射,反卷积的梯度则取决于卷积核权重。FCN 原论文用的是反卷积(转置卷积),但后来很多实现改用双线性插值初始化反卷积核,甚至直接固定为双线性,因为这样更稳定,不容易出现棋盘格伪影。
棋盘格伪影是反卷积的一个经典踩坑:当卷积核大小不能被步长整除时,输出特征图上会出现周期性明暗条纹。Penn-Fudan Database 的行人边界如果出现这种条纹,分割结果会显得很脏。常见的解决方式是:用双线性插值上采样,或者确保反卷积核大小是步长的整数倍。下面这段代码展示了如何用双线性插值初始化反卷积核:
def init_deconv_bilinear(deconv): # deconv: nn.ConvTranspose2d kernel_size = deconv.kernel_size[0] factor = (kernel_size + 1) // 2 if kernel_size % 2 == 1: center = factor - 1 else: center = factor - 0.5 og = torch.ogrid[:kernel_size, :kernel_size] bilinear = (1 - abs(og[0] - center) / factor) * \ (1 - abs(og[1] - center) / factor) weight = torch.zeros_like(deconv.weight) weight[range(deconv.in_channels), range(deconv.in_channels), :, :] = bilinear deconv.weight.data.copy_(weight)逻辑说明:这段代码把反卷积核初始化为双线性插值核,每个输入通道只对应自己的输出通道,不混合其他通道。这样上采样行为接近双线性插值,但保留了可学习参数。如果训练中反卷积核跑偏,可以冻结它,直接当双线性插值用。
4. 避坑与排查:Penn-Fudan Database 上跑 FCN 的常见问题
4.1 损失不下降,输出全为背景
现象:训练几个 epoch 后,损失稳定在一个较低值,但可视化预测结果全是背景类。原因:类别不平衡导致网络找到“全预测背景”的局部最优,梯度被背景样本主导。解决:给行人类别加权,或者改用 Dice Loss。先算一下行人像素占比,如果低于 15%,权重至少设 5.0 以上。同时检查ignore_index是否误设,导致大量行人像素被忽略。
4.2 边界出现棋盘格伪影
现象:分割结果的行人边界有周期性条纹,放大看像网格。原因:反卷积上采样时卷积核大小与步长不匹配,或者双线性插值的align_corners设置不一致。解决:统一用align_corners=True,或者把反卷积核初始化为双线性并冻结。如果已经出现伪影,可以在上采样后接一个 3×3 卷积做平滑,但会轻微模糊边界。
4.3 输入尺寸不统一导致 batch 组装失败
现象:DataLoader 报错,提示 tensor 尺寸不一致。原因:Penn-Fudan Database 原图尺寸不同,直接按 batch 堆叠会失败。解决:在collate_fn里统一 resize 到固定尺寸,比如 256×256 或 512×512,标签用最近邻插值。如果不想 resize,可以设batch_size=1,但训练会慢很多。常见的折中是:训练时随机裁剪到固定尺寸,验证时 resize 到同一尺寸。
4.4 上采样后尺寸差一个像素
现象:输出尺寸是 255×255,标签是 256×256,交叉熵报错。原因:下采样时池化层取整,上采样时倍数相乘后与原图差一两个像素。解决:不要硬算尺寸,直接用F.interpolate把 logits 插值到标签尺寸。注意align_corners=True时,插值后的像素中心对齐,边界更准。如果差得比较多,检查 backbone 的下采样总倍数是否和上采样倍数匹配。
4.5 验证集指标远低于训练集
现象:训练集 IoU 0.8,验证集 IoU 0.4。原因:Penn-Fudan Database 图像数量少,如果训练集和验证集划分时没有按行人尺度分层,验证集可能包含更多小目标或遮挡严重的目标。解决:划分数据时按行人像素占比分层采样,确保验证集分布接近训练集。另外,训练时用数据增强(随机翻转、颜色抖动),验证时不做增强,但要把 resize 方式统一。
5. 设计取舍:为什么 FCN 之后还有那么多分割网络
FCN 的核心贡献是把分割问题统一到卷积框架里,但它有三个明显的取舍。第一,上采样倍数固定,导致小目标分割粗糙;第二,跳跃连接只是逐元素相加,没有学习融合权重;第三,感受野固定,无法自适应不同尺度的行人。Penn-Fudan Database 里行人尺度差异大,FCN 在远距离小行人上容易漏检。
后续网络(如 U-Net、DeepLab 系列)分别从不同角度改进:U-Net 用编码器-解码器对称结构,把浅层特征通过拼接而不是相加融合,保留更多空间细节;DeepLab 用空洞卷积扩大感受野,不降低分辨率就能捕捉多尺度上下文;还有的用注意力机制学习空间权重。这些改进的底层逻辑是一样的:在“语义”和“空间”之间找更好的平衡。
如果你要验证一个分割网络是否真的 work,我一般会走三步:先看输出尺寸是否和标签严格对齐,再看损失是否对行人类别敏感,最后看边界像素的预测是否稳定。这三步走完,大部分翻车都能定位。从那以后我每次跑新的分割任务,都强制先过一遍尺寸对齐和类别权重,再谈调参。希望帮到你。
本文还有配套的精品资源,点击获取