阶段 2 · 看懂图像

语义分割:分类说"这是猫"
分割要画出猫的轮廓

分类输出一个标签,检测输出一个框。分割要的是 一张和原图一样大的图,每个像素都标着它属于什么。 这个要求看起来只多了"精细"两个字,但它把 CNN 的结构逼出了一个根本性的矛盾。

1

矛盾:CNN 一直在丢位置信息

回看《CNN 卷积神经网络》:分类用的 CNN 会一路把分辨率缩下去——224 → 112 → 56 → 28 → 14 → 7。 这是它"越深看得越抽象"的机制,但代价是位置精度一路降低。

分类需要什么 它只需要知道"图里有没有猫"。猫在左上角还是右下角都无所谓——下采样丢掉位置信息,正是它想要的。
分割需要什么 它必须精确知道每个像素在哪一边。下采样丢掉的东西,恰恰是它最需要的东西。

互动 · 同一张图,分辨率一路降下去,位置精度跟着丢

💡 一句话

分割 = 在"看得懂"和"看得清"之间做一次往返旅行。
降分辨率让它看得懂(知道这是猫还是狗),升分辨率让它看得清(知道猫的耳朵在第几个像素)。

2

U-Net 的解法:把下采样时的特征抄一份留下来

"先降后升"这个结构叫编码器-解码器。编码器常直接拿一个在 ImageNet 上预训练好的分类 CNN 来当。 ImageNet 是约 120 万张图、1000 类的图像分类数据集;在它上面训过的 CNN 已经会认各种物体, 拿来当编码器正好——它早就学会了"看懂",这一章只要补上"看清"。但光这样还不够——升回来的过程是凭空猜的, 细节早就丢了。U-Net 加的那一招才是关键:跳连(skip connection)。

下面那几个读数先交代清楚:TP = 这一类预测对的像素,FP = 多标的, FN = 漏标的;IoU = 对的 ÷(对的 + 多的 + 漏的),1 最好; mIoU = 各类 IoU 的平均,第 4 节会细讲。 这个互动用"把降采样前的清晰图按比例混回来"近似跳连,真实 U-Net 是把编码器的特征图拼(cat)到解码器同样大小那一层。

互动 · 亲手做一次分割往返(真实的下采样与上采样)

原图 + 真实标注

模型的预测

预测正确的像素(占整张 32×32)
—
边界附近的错误(占边界那一圈)
—

💡 关掉跳连看差别

把「使用跳连」关掉:像素准确率从 99.8% 掉到 78.1%,边界从锐利变成一格一格的台阶——因为解码器只能靠猜。
打开跳连:解码器在每一层都能直接拿到编码器同一层的清晰特征,边界立刻锐利起来。
注意:这里的跳连和 ResNet 的跳连不是一回事。ResNet 的跳连是把一层的输入直接加到输出上,让梯度好传; U-Net 的跳连是把编码器某一层的特征图直接送到解码器同样大小的那一层,补回位置细节。名字一样,目的不同。

3

上采样:怎么把图变大

四种上采样方式的对比(前三种真的算,PixelShuffle 是示意)

棋盘伪影来自转置卷积的工作方式:每个输入像素乘上整个卷积核,铺到输出上,再叠加。 既然要叠加,输出上每一格被叠的次数就可能不一样——下面这张图真的数给你看。

互动 · 棋盘伪影到底是哪来的(真的数每一格被叠了几次)

方法怎么算结果用在哪
最近邻直接复制最近的像素 锯齿状方块,能明显看出格子分割里更常用双线性;而 Distill 上 Odena 等人 2016 年讲棋盘伪影的文章(链接)发现,在生成图片的任务(后面的生成模型章节讲)里「最近邻放大 + 卷积」效果最好
双线性周围 4 个点按距离加权平均 平滑,无参数,计算便宜 现代分割网络的主流选择
转置卷积把卷积"反过来"做(每个输入像素铺开成整个核) 可学习,但容易产生棋盘伪影早期分割、生成模型
PixelShuffle把每个像素身上的多个通道值摊开、铺成更大的图 无伪影;它本身没有参数,可学习的是它前面那层卷积超分辨率
⚠️ 转置卷积的棋盘伪影是怎么回事

转置卷积的做法是:每个输入像素乘上整个卷积核,铺到输出上,然后叠加。
如果核大小不能被步长整除(比如核 3、步长 2),那么输出图上不同位置的叠加次数不一样—— 有的位置被叠了 2 次、有的被叠了 1 次。于是出现规则的明暗格子,就是棋盘伪影。
解法:用"先插值再卷积",或者把核大小设成步长的整数倍。 现代做法干脆换成双线性插值 + 一层 3×3 卷积:在分割里(DeepLab v3+ 就是这么做的)效果同样好,也不会有伪影。

4

怎么打分:IoU 和 mIoU

分割的准确率不能简单用"对的像素比例"。想象一张图 95% 是背景—— 一个把所有像素都预测成背景的模型,准确率就有 95%,但它毫无用处: 背景类 IoU 是 0.95,猫类 IoU 是 0,mIoU 只有 0.475。IoU 的分母是"并集",多圈(FP)和漏掉(FN)都要受罚。

互动 · 真实的 IoU 计算(拖动混淆矩阵)

把鼠标停在公式里的字母上(TP / FP / FN / TN 都可以)——下面哪一个滑块会亮,就说明那个符号指的就是它。

⚠️ 注意上面那条公式里的一个细节

在很多论文里你会看到 TP / (TP + FP + FN) 和 (预测正确 ∩ 真实正确) / (并集) 两种写法。它们其实等价——只是分子记法不同。
但有个真实的歧义:不同框架算 mIoU 时,"背景类"算不算进去、 "忽略区"(如边界附近的 don't care 像素)怎么处理,都可能有差别。 比较两个模型的 mIoU 时,先确认口径一致。

要一个真实世界的参照:PASCAL VOC 2012 上,DeepLab v3+ 报的 mIoU 是 89.0% (test set;模型直接输出,不再用额外的算法修边)——那是 21 个类(20 个前景 + 背景)在所有测试图上的平均, 比上面这个只有两类的小例子难得多。

5

分割模型走过的四代

代际代表核心贡献
第一代
全卷积
FCN (2015) 第一次把分类网络的全连接层换成卷积, 让网络输出从"一个标签"变成"一张标签图"。是所有分割网络的地基
第二代
编码器-解码器
U-Net (2015)、SegNet 跳连。U-Net 至今仍是医学图像分割的默认选择—— 因为它在标注图很少时表现极好:原文的训练集只有 30 张图
第三代
多尺度 + 空洞卷积
DeepLab v1-v3+、PSPNet 用空洞卷积(在卷积核的格子之间插空)在不降分辨率的情况下扩大感受野; 用 ASPP 在不同尺度上并行提取特征(几路不同空洞率的卷积同时看,小孔看细节、大孔看全局;回看感受野那一章)
第四代
Transformer 与通用分割
SegFormer、Mask2Former、SAM Transformer 做分割(后面有专章);SAM 把"分割任何东西"做成了一个可提示的通用模型—— 给个点或框就能分割,不需要针对新类别重新训练。套路和后面大语言模型的 「先大规模预训练、再用提示指挥它干活」一样
M

数学 · 三块像素,一个分数

前面五节一直在说「分割要逐像素判对」。但「判对」到底怎么变成一个数字? 这一章的打分公式只有一个分数,它的上下两个数都是数像素数出来的。 拖那个预测框:三块颜色一变,分数就跟着变。

核心大图 · 三块像素,就是公式里的三个字母

这个分数就是 IoU:你圈住的,和真的,重叠了多少? 而 mIoU 就是把它对每个类别各算一次、再平均——下面这张图就是那个「平均」。

微型图解 · 三个类别各算一个分数,等权平均成 mIoU

6

分割是所有视觉任务里最贵的

成本说明
标注成本高得离谱 分类要点 1 下,检测要画 1 个框,分割要沿着物体边缘描一圈—— 像素级标注是这三个任务里最费人的
计算量大 输出和输入同尺寸。224×224 的图,输出是 50176 个像素 × 类别数, 而分类只需要输出 1 个标签。这些分数最后每个像素取最大的那一类(argmax),就得到标签图
边界永远有争议 猫的耳朵边缘算不算猫毛?半透明的玻璃属于室内还是室外? 不同标注员会把同一个像素标成不同类别——这是 mIoU 天花板的一部分
实例分割更难 语义分割只要"这里是人",实例分割还要区分"这是第 1 个人、这是第 2 个人"。 两只重叠的羊怎么分?这是 Mask R-CNN 专门解决的问题
类别极不平衡 背景像素往往远多于前景。损失函数必须专门设计—— Focal Loss 把容易样本的权重压低,Dice Loss 直接优化重叠比例。 否则模型会躺平全猜背景:损失会被占绝大多数的背景像素主导,全猜背景的损失也很低

互动 · 三个任务,各要吐出多少个数字

7

小结

这一章的每一处结构,都可以用一句平常的话解释:你没有的东西,就不能指望它回来。

它对应哪条线 ④ 学习即压缩——而且这一章是那条线最直白的一章: 编码器就是在做有损压缩(本页的 U-Net:224 → 112 → 56 → 28,分辨率降 8 倍), 而整章都在讨论:压掉的东西还能不能弄回来。
一句话 分割这一章,本质上是在回答“有损压缩之后,怎么把丢掉的位置信息还回去”—— U-Net 的答案不是“恢复”(那不可能),而是“当初就抄一份留着”。
它牺牲了什么 牺牲了显存与计算量。跳连不是免费的:编码器每一级的输出都必须留着, 这就是 U-Net 显存开销的主要来源(也正是 DenseNet 被批评的同一个原因)。
而且它换回的是“能被找回的细节”,不是“全部细节”—— 压缩掉的东西永远回不来,跳连只是保住了没被压掉的那部分。 要省显存,可以只保留部分层的跳连。
🎬 自己验一遍

回到第 2 节那张「亲手做一次分割往返」。把「跳连」切到「不用跳连」—— 下面那行 mIoU 从 0.992 掉到 0.519,圆和方块的边变成带台阶的糊边; 再把「下采样级数」从 1 一路拖到 4(跳连仍然关着),mIoU 是 0.903 → 0.763 → 0.519 → 0.194——每多压一级,回来的东西就少一截。

那几步你看到的就是“学习即压缩”:编码器压掉的是分辨率, 解码器没有别的东西可依靠,只能猜,而“猜”在边界上一定是错的。 指着那个 mIoU 数字能说出这句话,这一节才算真的装进脑子里——没指过就回去再拖一次。

它挂在哪几条暗线上

这一章主要落在下面三条暗线上(字母是六条暗线的编号)。

暗线这一章的回答
A 信息流动 括号里是“高 × 宽 × 通道数”:(H, W, 3) → 编码器逐级 (H/2, 2C) → … → 最深处 (H/8, 512) (本页的 U-Net 只缩到 1/8)→ 解码器逐级上采样 → (H, W, 类数)。
跳连额外引入一条横向的数据流:同一分辨率的编码器特征被“抄”进解码器。 出口是分类、检测、分割这三个任务里唯一一个和输入同尺寸的输出:224×224×21 类 = 105 万个数字, 而分类只要 21 个
C 参数账本 输出尺寸就是账单:224×224×21(Pascal VOC 的 21 类)= 105 万个输出值, 而分类只要 21 个——同样的骨架,分割头的输出要贵 50176 倍。
U-Net 的账更直观:最深处 28×28×512 → 上采样 ×2 得到 56×56×256 → 拼上编码器的 56×56×256 → 变成 56×56×512,再卷积回 256。 跳连让解码器每层的输入通道数翻倍,参数量和显存跟着翻倍—— 至于它保住了多少位置细节,第 2 节那个开关一拖就知道。
F 违背了哪个直觉 两条。
① “输出和输入一样大”听起来更简单,实际上更难: 更多的像素、更贵的计算、更难的标注,而且必须先把图弄糊才能看懂它—— 为了看得清,先得看不见。
② 像素准确率 95% 的分割模型可能毫无用处(把每个像素都猜成背景就能拿到), 这就是为什么这个任务用 mIoU 而不是准确率(回看第 4 节)
🎯 前后钩子

它接住了上一章的什么:《目标检测》给出的是“框”—— 一个粗略的矩形。这一章把要求推到了像素级,而且直接继承了检测的一个坑: 类别极不平衡。

它给后面留了什么:第四代分割(SegFormer / Mask2Former)已经是 Transformer 了, 因为 U-Net 那种“先降后升”的局部结构仍然受限—— 完整的答案在 《ViT》。

一句话带走语义分割

分割要求输出和原图一样大的标签图,而 CNN 的设计恰恰在不断丢位置信息—— 这个矛盾决定了整个结构:先降分辨率提取语义,再升回来恢复位置。
U-Net 的跳连是解决细节丢失的关键,注意它和 ResNet 的跳连名字一样、目的完全不同 (一个传位置细节,一个传梯度)。
打分用 mIoU 而不是像素准确率——因为它惩罚"乱标",而准确率几乎不会。
SAM 把分割变成了"给个提示就能抠任何东西"的通用能力。

8

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式