首页 / 资讯中心 / 文章详情

全卷积网络FCN:语义分割原理、上采样与Penn-Fudan实战

全卷积网络FCN:语义分割原理、上采样与Penn-Fudan实战 ★ FEATURED ARTICLE
简介这是一份围绕全卷积网络FCN与Penn-Fudan Database构建的图像分割实践资料面向已具备CNN基础、希望掌握像素级语义分割与行人检测流程的学习者。内容以FCN端到端分割为主线涉及VGG/ResNet骨干、上采样、跳跃连接及交叉熵损失等关键环节并配合Penn-Fudan街景图像与精细掩模便于理解训练、预测和评估的完整链路。压缩包共533个文件约184.68MB主要由png图像与分割掩模、txt说明及日志、py训练脚本、npy评估指标与xml标注等组成兼顾数据、代码和实验记录目录组织适合按模块查阅。已有507人学习或下载可作为课程作业、分割入门实验和行人分割复现的参考帮助读者快速理清FCN的数据组织、参数配置与结果分析思路。1. 全卷积网络到底是个什么东西从「图里有没有人」到「人占了哪几个像素」给一张照片问「图里有没有人」这是分类换一个问法——「图里每一个像素属于人还是属于背景」这是分割。全卷积网络Fully Convolutional Network常缩写成 FCN就是干后面这件事的模型底座。打个比方分类模型像保安站在门口喊一嗓子「有人进来了」全卷积网络像保安拿荧光笔把监控画面里每个属于人的像素都涂上颜色最后交给你一张描好边的图。区别不在谁更聪明而在输出的形状根本不同——一个是几个数字一个是和原图一样大的一整张标签图。值得搞懂它的原因很实在今天你见到的大多数图像分割模型往回追都能追到这套「全卷积」的思路。搞懂它等于拿到了理解像素级预测这件事的钥匙也才看得懂后面那些模型到底在改什么。2. 为什么非要把全连接层拆掉像素级输出绕不过去的那道坎2.1 分类网络最后那块「压路机」传统图像分类网络的套路很固定几组卷积加池化把图越缩越小、通道越堆越多最后把特征图拉平成一维向量接上两三层全连接输出每个类别的分数。整条链路下来输入一张图输出一句话。问题出在「拉平」这一步。特征图本来是 7×7×512 的三维结构里面记录的是「图左上角有什么、右下角有什么」拉平成一维以后这些位置信息被搅在了一起。再经过全连接层的权重矩阵输出的向量里只剩「整张图整体像猫」这样的结论具体哪块区域像猫信息已经找不回来了。打个比方把一张地图揉成纸团再问「第三行第五列是河还是山」。纸团的重量还在图上画的内容也还在但你没法按坐标去问它问题了。分割任务恰恰全是按坐标问问题的这一步就把路堵死了。还有第二个副作用全连接权重矩阵的形状是死的输入维度一变矩阵就对不上。所以分类网络通常要求固定输入尺寸比如 224×224。分割数据里图像尺寸五花八门硬缩放会让人变形这个限制很要命。2.2 把「打分」换成「逐像素投票」全卷积网络的核心动作只有一句话把最后那几层用于分类的全连接层换成卷积层。这个替换不是硬凑。数学上全连接层可以看成一个核大小等于整张特征图的卷积——输入 7×7×512、输出 1×1×4096 的全连接等价于用 7×7×512 的卷积核卷一次得到 1×1×4096。既然是一回事那就有得商量卷积核不必铺满整张图。换成 1×1 卷积以后输出形状从 1×1×C 变成 h×w×C。这里的 h 和 w 是下采样之后的特征图尺寸C 是类别数。这张 h×w×C 的东西本质上就是一张「低分辨率的分割图」——每个位置存着该位置属于各类的分数。这里有个容易被忽略的细节输出的每个位置对应的不是原图的单个像素而是原图上一块区域也就是感受野。所以这张低分辨率分割图其实是「块级判断」还得放大回原图尺寸这就是上采样要干的事。整套流程的骨架可以概括成一句下采样 → 块级分类 → 上采样。好处跟着来了。输入尺寸不再固定同一套权重能吃 256×256也能吃 512×384只要显存扛得住位置对应关系保住了参数量还降了一大截原来那几层全连接往往是整个网络参数的大头。2.3 跳层连接粗轮廓和细边缘怎么拼只用最后一层特征做上采样会得到一个很尴尬的结果大块区域分对了边界糊成一团。原因不难推——下采样 32 倍以后原图上一个 32×32 的方块被压成了 1 个像素这个像素再去判断自己属于什么边界在哪早就丢了。全卷积网络给出的补丁是跳层连接把浅层的高分辨率特征和深层上采样后的结果逐元素相加。浅层特征分辨率高、语义弱但边界清晰深层特征语义强、分辨率低。两者一加等于把「看得清但看不懂」和「看得懂但看不清」两张草图叠在一起比对。具体做法有三个版本。只从最后一层上采样 32 倍的最省事也最糙把倒数第二层特征融合进来、上采样 16 倍的边界好一截再多融一层、上采样 8 倍的边界最细代价是显存和计算量往上走。融合层数越多浅层的高频细节保留得越完整。注意融合用的是逐元素相加不是通道拼接。相加要求两条支路通道数一致所以深层支路通常得先用 1×1 卷积把通道数压到和浅层一样才能加得上去。这一步漏了张量形状会直接报错。2.4 端到端一次前向省掉了一堆手工拼图在变成可训练的网络之前做图像分割有另一条路先算超像素把图切成几百个小块再对每个小块提特征、分类、后处理合并。这条路能跑但每一步都得手工设计和调参前面的错误会一路传到后面改一个参数整条链路都要重来。全卷积网络把这一串动作收进了一个网络里。输入图、输出掩码中间的所有变换都是权重都靠反向传播自动调。这才是它真正改变行业的地方——不是因为某一层设计得多巧妙而是因为它把「分割」变成了一个标准的监督学习问题能端到端训能换骨干能加损失。反过来说端到端也意味着出问题时排查链变长了数据管道错、标签错位、上采样倍数算错、损失函数接错最终都表现为「掩码不好看」。这也是为什么后面要专门讲一遍训练时最先翻车的几个地方。3. 一张图进、一张掩码出全卷积网络的三步走法3.1 第一步下采样编码把图缩小到底换到了什么卷积核滑动、池化窗口取最大每隔几步把图缩小一半。缩小的过程不只是丢信息它在换两样东西感受野和语义。感受野可以手算。一层 3×3 卷积感受野是 3两层 3×3 堆叠是 5三层是 7。加上 stride 或者池化感受野涨得更快。输出步长output stride这个参数说的就是特征图上 1 个点对应原图多少乘多少的区域。output stride 等于 32意思是特征图一个格子管原图 32×32。举个具体例子。原图 512×512走一遍常见的骨干结构第一层卷积步长 2 变成 256最大池化步长 2 变成 128后面三个阶段各下采样一次分别是 64、32、16。最后拿到一张 16×16 的特征图output stride 就是 32。这张 16×16 的图每个格子要对原图 32×32 的区域做判断——一个只占 40×80 像素的行人在这张图上只剩 1×2 个格子边界信息基本清零。这就是分割任务里小目标难受的根源也是后续调参要盯的第一个数。想让它变好要么把 output stride 降下来要么用别的办法把分辨率保住。3.2 第二步上采样把 16×16 长回 512×512上采样有三条路选哪条直接决定边界质量。最近邻插值放大的时候复制最近的像素。速度快、零参数但边缘是锯齿状的方块放大倍数越大越明显。双线性插值按距离对周围四个点加权平均。平滑同样零参数但会把边界糊软锐利的轮廓变成渐变过渡。转置卷积也叫反卷积带可学习权重。它在输入元素之间插零再用卷积核去卷输出的每个位置由权重决定所以它能「学会」怎么放大。想提边界质量一般从这条路上想办法。这里有句话必须说清楚转置卷积不是卷积的逆运算。它只是把卷积的矩阵乘法转置了一下形状上反着来信息上并不还原。多个不同的输入经过转置卷积可以落到同一片输出区域——所以它只能叫可学习的上采样叫「还原」是不准确的。转置卷积还有一个副作用叫棋盘格伪影卷积核覆盖不均匀输出被周期性放大图上会出现规律性的网格。缓解办法有两条一是让卷积核尺寸能被 stride 整除二是直接用双线性插值的权重去初始化它甚至干脆改成「先插值、再普通卷积」。import torch import torch.nn as nn # 输入: (N, C_in, H, W) 的特征图 up nn.ConvTranspose2d( in_channels256, # 输入通道数必须和上一层的输出对上 out_channels21, # 输出通道数等于类别数(含背景)不是特征通道数 kernel_size4, # 能被 stride 整除减轻棋盘格伪影 stride2, # 放大倍数2 表示 H、W 各翻一倍 padding1, # 与 kernel_size/2 搭配控制输出尺寸 output_padding0 # 只补尺寸不改变卷积核的覆盖方式 ) x torch.randn(1, 256, 16, 16) y up(x) print(y.shape) # torch.Size([1, 21, 32, 32])输出尺寸的公式是H_out (H_in - 1) * stride - 2 * padding kernel_size output_padding。拿上面这组参数代进去(16-1)×2 - 2×1 4 0 32对得上。改 kernel_size 或者 padding 之前先用纸笔算一遍比跑起来看报错快得多。参数里最容易踩的两个坑out_channels要等于类别数不是随便填个特征通道数output_padding只影响输出尺寸不改变卷积核的覆盖方式别拿它当调质量的旋钮。如果发现上采样之后的尺寸和原图差了一两个像素先去检查这两个参数而不是急着改网络结构。3.3 第三步逐像素算损失让每个位置都有话可说全卷积网络输出的是 (N, C, H, W) 的分数图标签是 (N, H, W) 的整型图每个位置一个类别编号。损失函数用逐像素交叉熵本质上是把 H×W 个位置各自当成一次小分类。import torch import torch.nn as nn # logits: (N, C, H, W)未经过 softmax 的原始分数 # target: (N, H, W)每个位置的类别编号取值 0..C-1必须是 long 类型 criterion nn.CrossEntropyLoss(ignore_index255) logits torch.randn(2, 21, 64, 64) target torch.randint(0, 21, (2, 64, 64)) loss criterion(logits, target) print(loss.item())几个点必须对上logits 在传入前不要做 softmax这个损失内部会做 log_softmax多做一次等于把分数压平了target 是 long 类型形状顺序是 (N, C, H, W) 对 (N, H, W)不是 one-hot。ignore_index255这个参数在实际项目里几乎必开。行人数据集通常只标注行人其他区域天空、马路、树丛没有像素级标注这些位置不该算进损失否则模型会被迫去学一个错误的「背景」定义。惯常做法是把未标注像素的标签值设成 255然后忽略掉。还有一个反直觉的点整体像素准确率在分割任务里几乎没有参考价值。一张图里背景可能占 95%模型全部预测背景准确率 95%好看得离谱但 IoU 是 0。所以指标要看 IoU 或 Dice而且要按类别分开看别只看平均数。4. 拿 Penn-Fudan 数据集跑一遍小数据、小目标、真实边界4.1 这个数据集长什么样Penn-Fudan 行人数据集是一个公开的行人检测与分割数据集。规模不大大约 170 张图像取自户外场景每张图里有若干行人合计三百多个行人实例每个实例都带像素级的分割掩码同时也给出边界框标注。它的价值不在大在干净且任务明确只需要区分行人和背景两类标注是逐像素的数据量和显存需求都很友好一张普通显卡就能跑完一轮实验。对第一次接触全卷积网络的人来说这是个很合适的练手场——流程完整代价可控跑通一遍能把前面讲的下采样、上采样、损失对齐全都串起来。需要适应的第一件事是尺寸不统一。分类数据集常被预处理成统一尺寸这个数据集的原图分辨率各不相同。全卷积结构本身不挑输入尺寸但一个 batch 内的张量必须同形所以还是要做 resize 或 padding。这里有取舍缩得太小行人只剩几十个像素边界彻底没了保持原尺寸显存又吃紧。常见折中是等比缩放短边缩到 512 左右长边按比例走batch size 压到 2 或 4配合梯度累积凑出等效的大 batch。还有一件事值得留意这个数据集既有框标注又有掩码标注意味着它同时能喂给检测模型和分割模型。做分割时框标注不是废的可以用来做数据筛选、做裁剪增强或者作为辅助损失的一项。4.2 小数据集的现实它能验证流程不能证明能力170 张图训练一个深度分割网络过拟合是必然不是意外。训练集 IoU 冲到 0.9、验证集停在 0.5这种曲线在小数据集上非常常见看到它别慌也别急着换模型。正确的期待是这套数据用来验证「数据管道对不对、标签有没有错位、损失函数有没有接对、上采样倍数算没算错」而不是用来比较「哪个模型更强」。在这个规模上得出「A 模型比 B 模型好」的结论换一批随机种子可能就翻转了。想在这种规模上拿到能看的掩码基本靠两件事用在大规模分类数据上预训练过的骨干网络初始化编码器然后只微调解码器以及足够强的数据增强随机裁剪、水平翻转、颜色抖动、亮度扰动能上的都上。冻结前几层、只训后几层也是常用的省事办法。打个比方170 张照片学画画能学会的是「怎么握笔、怎么调色」学不会「怎么成为画家」。指望它训出能上生产的分割模型那是把练手场当考场了。它的正确用途是让你在不心疼算力的前提下把整条链路跑顺、把坑踩完。4.3 最先翻车的四个地方第一个掩码和图像用了同一个插值。图像 resize 用双线性没问题掩码也这么干就出事了——标签会被插出 0.5、1.7 这种不存在的类别编号损失函数拿到这些值要么报错要么默默算出一个毫无意义的数。掩码必须用最近邻这条没有例外。第二个小目标在下采样里消失。output stride 32 意味着一个 40 像素高的行人最后只剩 1 个像素出头。可选的路有三条把最后一级下采样去掉、把 output stride 降到 16 或 8用空洞卷积在不降分辨率的前提下扩大感受野多尺度训练让模型见过不同大小的行人。三条可以叠加用。第三个只看准确率。前面说过全预测背景也能有 95% 的准确率。第一次训出来一张全黑的掩码回头一看 loss 还挺低——这不是玄学是类别不平衡加上指标选错的必然结果。改法是在损失里给行人像素加权或者用 Dice 这类对类别比例不那么敏感的指标。第四个忘了归一化。骨干网络在预训练时用的是一套固定的均值方差输入没按同样方式归一化微调阶段收敛会明显变慢甚至干脆不收敛。这个坑踩一次能记很久因为它不报错只是「效果莫名其妙地差」。注意验证阶段做可视化的频率要高过看指标的频率。前几个 epoch 指标几乎不动但掩码图能直接告诉你问题在哪——全黑对应类别不平衡全白对应标签读错只框住一半多半是尺寸或坐标没对齐。5. 易混概念辨析五组最常被搅在一起的说法5.1 全卷积网络和普通卷积网络差在最后那一段易混点名字里都有卷积骨干部分长得几乎一样看起来只是「深度不同」。很多人第一次听到会以为这是两种完全不同的结构。为什么混全卷积网络的编码器本来就是从经典分类网络改过来的前面十几层卷积完全一致只看前半段根本分不出谁是谁。怎么区分看输出形状。分类网络输出 (N, C)一张图一个结论全卷积网络输出 (N, C, H, W)一张图 H×W 个结论。或者更直接看有没有把那几层全连接换成卷积。这个区别决定了前者只能回答「是什么」后者能回答「在哪里」。5.2 转置卷积和卷积的逆运算名字骗人易混点带个「反」字直觉上以为它能把卷积的结果原样还原回来。为什么混形状上确实反着来——卷积把大图变小转置卷积把小图变大一进一出好像能对上于是「反卷积」这个叫法就流传开了。怎么区分转置卷积做的是矩阵转置乘法不是求逆矩阵。卷积在降维过程中会丢信息多个不同输入映射到同一个输出丢了就回不来。转置卷积只是提供了一种可学习的放大方式放大出来的内容是猜的不是还原的。把它理解成「带参数的上采样」比理解成「反卷积」准确得多也少一堆误解。5.3 语义分割、实例分割、全景分割都输出掩码但问题不同易混点三个任务的可视化结果都是彩色色块看起来是一回事指标也都是 IoU。为什么混输出形状类似、评价方式类似不看任务定义很难区分很多教程又都拿「给行人描边」当例子更容易混。怎么区分语义分割只回答「这个像素属于哪一类」两个挨在一起的人会被涂成同一块颜色。实例分割要回答「这个像素属于第几个个体」两个人会被分开涂。全景分割是两者的合并能数清的东西按实例分数不清的东西按语义分。回到全卷积网络它是语义分割的范式天生处理不了「同类个体分开」这件事。想数人头得往实例分割的路子上走要么先检测框再框内分割要么在网络里加一条区分个体的分支。5.4 上采样和池化方向相反但不止方向相反易混点一个把图变大一个把图变小感觉是互逆操作配一对就能还原。为什么混转置卷积的 stride 设成 2正好和 2×2 最大池化的尺寸变化对得上看起来像一对。怎么区分池化是不可学习的只做取最大或取平均信息单向丢失上采样里的插值也不可学习但转置卷积带权重、能在训练中调整。更关键的是池化丢掉的细节不会因为后面做一次上采样就回来——最大值之外的那些值网络再也没见过。所以跳层连接才必须存在从浅层把丢之前的信息另存一份拿过来用。5.5 「全卷积」不等于「一个全连接都没有」易混点以为叫全卷积网络里就绝对不能出现全连接层出现了就是抄错代码。为什么混名字起得太绝对字面上就是「全部由卷积构成」。怎么区分说的是「用卷积替换掉用于分类的那几层全连接」不是禁止一切全局操作。实际实现里全局平均池化、1×1 卷积这类把空间信息汇总的操作依然常见作用接近全连接但保留了对输入尺寸的适应性。判断标准不是「有没有全连接层」而是「输出有没有保留空间维度」。6. 什么时候该上全卷积网络什么时候该换路子需要像素级输出、类别数不多、边界精度不要求到发丝级、数据规模中等偏小、部署端算力有限——这五条同时成立时全卷积这一套依然是最省心的选择。结构简单、推理路径清晰、显存可控而且改起来方便想提边界质量就加跳层连接想控显存就调 output stride想压参数量就换轻量骨干。该换路子的时候也有几种典型信号。要区分同类个体比如画面里有几个人、各自在哪别在语义分割上硬改直接上实例分割或者两段式方案。边界要求极高比如抠图级的人像分割单一上采样路径到不了那个精度得换高分辨率特征融合的思路。数据只有几百张又没有可用的预训练权重先想清楚是不是该用更轻的方法别一上来就堆网络。| 需求 | 更合适的选择 | 原因 | | 只要框不要轮廓 | 目标检测 | 输出是矩形不必逐像素判断 | | 要轮廓同类不分个体 | 全卷积网络 | 语义分割的标准范式 | | 要轮廓还要区分个体 | 实例分割 | 需要额外的个体区分机制 | | 边界要极精细 | 高分辨率特征融合方案 | 单一路径的上采样不够用 |举个具体判断任务是「统计路口行人数并标出区域」。先问一句要不要数个数要数语义分割就不够得上实例分割。如果退一步只要求标出所有行人区域、不数个数那全卷积网络配一个预训练骨干就够了一百多张图也能跑出个能看的基线把最后一级下采样换成空洞卷积保分辨率、跳层连接补边界、损失里给行人像素加权基本到位。我第一次在这个数据集上训练时看到像素准确率 0.96 就以为成了把掩码图导出来一看——全黑一个行人都没标出来那 0.96 全是背景像素撑起来的。想自己跑一遍的话这套流程和可复现的配置都整理在一起了拿下来对照着改比自己从零拼要少走不少弯路。从那以后我每次跑完分割任务都强制先导三张掩码图看一遍再去看任何指标指标只用来横向比较不用来判断「能不能用」。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站