二、全卷积网络(FCN)
全卷积网络(Fully Convolutional Network, FCN)由Long等人于2015年在CVPR上提出(论文:Fully Convolutional Networks for Semantic Segmentation),是首个端到端的像素级语义分割深度学习模型。FCN的核心贡献在于证明了分类网络(如VGG、AlexNet)可以改造为分割网络,为后续所有分割方法奠定了理论基础。
2.1 核心思想:全连接层转卷积
传统分类CNN(如VGG-16)最后使用全连接层将特征图映射为固定长度的分类向量,但这种方法丢失了空间信息。FCN的关键创新是将全连接层替换为卷积层,使网络可以接受任意尺寸的输入图像,并输出相应尺寸的密集预测(dense prediction)。具体来说,将VGG-16最后的4096维全连接层视为一个卷积核大小为7x7的卷积层,将第二个4096维全连接层视为卷积核大小为1x1的卷积层,从而保留特征图的空间维度。
import torch
import torch.nn as nn
import torch.nn.functional as F
class FCN32s(nn.Module):
"""FCN-32s: 直接32倍上采样,无跳跃连接"""
def __init__(self, num_classes):
super().__init__()
self.num_classes = num_classes
# 骨干网络:VGG-16 特征提取部分(去掉分类头)
self.features = nn.Sequential(
# Block 1
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, stride=2, ceil_mode=True), # 1/2
# Block 2
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, stride=2, ceil_mode=True), # 1/4
# Block 3
nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, stride=2, ceil_mode=True), # 1/8
# Block 4
nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, stride=2, ceil_mode=True), # 1/16
# Block 5
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, stride=2, ceil_mode=True), # 1/32
)
# 全连接层 -> 卷积层
self.fc6 = nn.Conv2d(512, 4096, 7, padding=3)
self.fc7 = nn.Conv2d(4096, 4096, 1)
self.score = nn.Conv2d(4096, num_classes, 1)
# 32倍反卷积上采样
self.upscore = nn.ConvTranspose2d(
num_classes, num_classes, 64,
stride=32, padding=16, bias=False
)
def forward(self, x):
x = self.features(x)
x = F.relu(self.fc6(x))
x = F.dropout(x, p=0.5, training=self.training)
x = F.relu(self.fc7(x))
x = F.dropout(x, p=0.5, training=self.training)
x = self.score(x)
x = self.upscore(x)
# 裁剪到输入尺寸(去除额外padding引入的边界)
x = x[:, :, 19:19 + x.size()[2]]
return x
2.2 反卷积上采样(Transposed Convolution)
由于经过多次池化和步长为2的卷积操作后,特征图分辨率降低为输入的1/32,需要将低分辨率的热力图(heatmap)上采样到原始输入尺寸。FCN使用转置卷积(Transposed Convolution, 也称反卷积)来实现可学习的上采样。转置卷积的核参数可以通过网络训练学习得到,相比双线性插值等固定上采样方法能更好地恢复细节信息。FCN-32s使用步长为32的大核转置卷积直接上采样32倍,虽然简单直接,但预测结果较为粗糙。
2.3 跳跃连接(Skip Connection)
直接32倍上采样导致分割结果丢失了大量细节信息。FCN提出了跳跃连接(Skip Connection)来融合浅层高分辨率特征和深层语义特征。核心思路是:浅层(如pool3、pool4)感受野小、分辨率高,包含丰富的空间位置信息;深层(如pool5/conv7)感受野大、语义信息丰富,但分辨率低。通过将深层预测上采样后与浅层预测逐元素相加(element-wise addition),可以融合多尺度特征,显著提升分割精度。
FCN-32s: conv7 (1/32) ----32x upsampling----> prediction
FCN-16s: conv7 (1/32) --2x up--> pool4 (1/16) --16x up--> prediction
FCN-8s: conv7 (1/32) --2x up--> pool4 --2x up--> pool3 (1/8) --8x up--> prediction
class FCN8s(nn.Module):
"""FCN-8s: 融合 pool3, pool4, pool5 三层特征的跳跃连接版本"""
def __init__(self, num_classes):
super().__init__()
self.num_classes = num_classes
# 使用预训练的 VGG-16 特征层
self.pool3 = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(True),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(True),
nn.MaxPool2d(2, stride=2, ceil_mode=True),
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(True),
nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(True),
nn.MaxPool2d(2, stride=2, ceil_mode=True),
nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(True),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(True),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(True),
nn.MaxPool2d(2, stride=2, ceil_mode=True),
)
self.pool4 = nn.Sequential(
nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(True),
nn.MaxPool2d(2, stride=2, ceil_mode=True),
)
self.pool5 = nn.Sequential(
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(True),
nn.MaxPool2d(2, stride=2, ceil_mode=True),
)
# 全连接->卷积分类器
self.classifier = nn.Sequential(
nn.Conv2d(512, 4096, 7, padding=3), nn.ReLU(True),
nn.Dropout2d(p=0.5),
nn.Conv2d(4096, 4096, 1), nn.ReLU(True),
nn.Dropout2d(p=0.5),
nn.Conv2d(4096, num_classes, 1),
)
# 1x1卷积将中间层降维到 num_classes 通道
self.score_pool4 = nn.Conv2d(512, num_classes, 1)
self.score_pool3 = nn.Conv2d(256, num_classes, 1)
# 转置卷积上采样层
self.upscore2 = nn.ConvTranspose2d(
num_classes, num_classes, 4, stride=2, padding=1, bias=False
)
self.upscore4 = nn.ConvTranspose2d(
num_classes, num_classes, 4, stride=2, padding=1, bias=False
)
self.upscore8 = nn.ConvTranspose2d(
num_classes, num_classes, 16, stride=8,
padding=4, bias=False
)
def forward(self, x):
input_shape = x.shape[2:]
# 提取三个阶段的特征
x3 = self.pool3(x) # 1/8
x4 = self.pool4(x3) # 1/16
x5 = self.pool5(x4) # 1/32
# 深层预测
score = self.classifier(x5) # 1/32
# 融合 pool4 特征
score = self.upscore2(score) # -> 1/16
score_pool4 = self.score_pool4(x4)
score = score + score_pool4
# 融合 pool3 特征
score = self.upscore4(score) # -> 1/8
score_pool3 = self.score_pool3(x3)
score = score + score_pool3
# 8倍上采样回到原图尺寸
score = self.upscore8(score)
# 裁剪对齐
score = score[:, :, :input_shape[0], :input_shape[1]]
return score
FCN的历史意义与局限性:
- 开创性贡献:证明了全卷积架构可以端到端地完成像素级预测任务,为后续所有分割模型(U-Net、DeepLab、Mask R-CNN等)奠定了方法论基础
- 效率优势:一次前向传播即可生成密集预测,无需像传统方法那样使用滑动窗口或区域提案
- 局限性:上采样结果仍不够精细,缺乏对全局上下文的显式建模,对物体边界的处理不够精确
- 继承关系:FCN的跳跃连接思想直接启发了U-Net的编码器-解码器架构
三、U-Net:编码器-解码器对称架构
U-Net由Ronneberger等人于2015年在MICCAI上提出(论文:U-Net: Convolutional Networks for Biomedical Image Segmentation),是医学图像分割领域最具影响力的模型。其名称来源于网络结构的U形对称形状——左侧为编码器(下采样路径),右侧为解码器(上采样路径),底部为瓶颈层。U-Net在FCN的跳跃连接思想上进一步发展,将编码器每一层的特征通过拼接(concatenate)方式传递给解码器的对应层,形成更丰富的多尺度特征融合。
3.1 编码器-解码器结构
U-Net的编码器(收缩路径)由重复的两个3x3卷积 + ReLU + 2x2最大池化(步长2)组成,每次下采样后特征通道数加倍。解码器(扩展路径)由2x2转置卷积上采样 + 跳跃连接拼接 + 两个3x3卷积 + ReLU组成,每次上采样后特征通道数减半。最后一层使用1x1卷积将特征映射到目标类别数。这种对称结构使得网络能够同时利用浅层的精确定位信息和深层的语义信息。
Input(1,568,568) -> Conv(64) -> Conv(64) -> Pool -> [Skip] ------+
+-> Conv(128) -> Conv(128) -> Pool -> [Skip] ---+ |
+-> Conv(256) -> Conv(256) -> Pool -> [Skip] -+---+ |
+-> Conv(512) -> Conv(512) -> Pool | | |
+-> Conv(1024) -> Conv(1024) | | |
+- UpConv(512) -> Concat[512] -> Conv(512) -+ | |
+- UpConv(256) -> Concat[256] -> Conv(256) ------+ |
+- UpConv(128) -> Concat[128] -> Conv(128) ---------------+
+- UpConv(64) -> Concat[64] -> Conv(64) -> Conv(num_classes)
3.2 跳跃连接:Concatenate vs Add
U-Net与FCN在跳跃连接上的关键区别在于:FCN使用逐元素相加(Add)的方式融合特征,而U-Net使用通道拼接(Concatenate)。Concatenate方式将编码器特征图和解码器特征图在通道维度上拼接在一起,保留了原始特征的所有信息,让网络自行学习如何融合。实验证明,Concatenate比Add能保留更多的空间定位细节,特别适合医学图像中精细结构的分割任务。
3.3 医学图像分割的优势
U-Net在医学图像分割领域取得了巨大成功,主要原因包括:
- 数据高效(Data Efficient):U-Net通过大量使用数据增强(特别是弹性形变),可以在仅有几十张标注图像的情况下训练出可用的分割模型
- 弹性形变增强(Elastic Deformation):通过在训练时对图像和标签同时施加随机弹性形变,模拟真实组织中可能存在的形变,极大提升了模型的泛化能力
- 精确边界定位:跳跃连接将浅层的高分辨率特征传递到解码器,使得输出保留了清晰的边界信息
- 端到端训练:输入为图像-标签对,输出为逐像素分割图,无需任何后处理或手工特征
- 轻量级推理:相比于两阶段方法,U-Net单次前向传播即可完成分割
3.4 PyTorch完整实现
import torch
import torch.nn as nn
import torch.nn.functional as F
class DoubleConv(nn.Module):
"""双卷积块: Conv2d + BN + ReLU (两次)"""
def __init__(self, in_ch, out_ch):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
)
def forward(self, x):
return self.conv(x)
class Down(nn.Module):
"""下采样: MaxPool + DoubleConv"""
def __init__(self, in_ch, out_ch):
super().__init__()
self.mpconv = nn.Sequential(
nn.MaxPool2d(2),
DoubleConv(in_ch, out_ch),
)
def forward(self, x):
return self.mpconv(x)
class Up(nn.Module):
"""上采样: 转置卷积 + 拼接 + DoubleConv"""
def __init__(self, in_ch, out_ch, bilinear=False):
super().__init__()
# 上采样方式选择
if bilinear:
self.up = nn.Upsample(
scale_factor=2, mode='bilinear',
align_corners=True
)
else:
self.up = nn.ConvTranspose2d(
in_ch, in_ch // 2, 2, stride=2
)
self.conv = DoubleConv(in_ch, out_ch)
def forward(self, x1, x2):
x1 = self.up(x1)
# 处理尺寸不匹配(padding导致的尺寸差)
diff_h = x2.size()[2] - x1.size()[2]
diff_w = x2.size()[3] - x1.size()[3]
x1 = F.pad(x1, [
diff_w // 2, diff_w - diff_w // 2,
diff_h // 2, diff_h - diff_h // 2,
])
# 跳跃连接:通道拼接(Concatenate)
x = torch.cat([x2, x1], dim=1)
return self.conv(x)
class UNet(nn.Module):
"""U-Net: 编码器-解码器对称架构"""
def __init__(self, n_channels, n_classes, base_filters=64):
super().__init__()
self.inc = DoubleConv(n_channels, base_filters) # 64
self.down1 = Down(base_filters, base_filters * 2) # 128
self.down2 = Down(base_filters * 2, base_filters * 4) # 256
self.down3 = Down(base_filters * 4, base_filters * 8) # 512
self.down4 = Down(base_filters * 8, base_filters * 8) # 512 (bottleneck)
self.up1 = Up(base_filters * 8, base_filters * 4) # 256
self.up2 = Up(base_filters * 4, base_filters * 2) # 128
self.up3 = Up(base_filters * 2, base_filters) # 64
self.up4 = Up(base_filters, base_filters)
self.outc = nn.Conv2d(base_filters, n_classes, 1)
def forward(self, x):
# 编码器(收缩路径)
x1 = self.inc(x)
x2 = self.down1(x1)
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)
# 解码器(扩展路径)+ 跳跃连接
x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
logits = self.outc(x)
return logits
def test_unet():
"""验证U-Net输出尺寸"""
model = UNet(n_channels=3, n_classes=2, base_filters=64)
x = torch.randn(4, 3, 256, 256) # batch=4, RGB, 256x256
out = model(x)
print(f"Input shape: {x.shape}")
print(f"Output shape: {out.shape}")
# Expected: torch.Size([4, 2, 256, 256])
assert out.shape == (4, 2, 256, 256), "Output shape mismatch!"
print("U-Net test passed!")
if __name__ == "__main__":
test_unet()
U-Net训练关键技巧:
- 损失函数:推荐使用Dice Loss + Cross-Entropy Loss的组合(混合损失),在类别不平衡时表现优异
- 数据增强:弹性形变(ElasticTransform)、随机旋转、缩放、平移、亮度调整等
- 权重初始化:使用Kaiming初始化(He init)配合ReLU激活函数
- 学习率策略:建议使用余弦退火(Cosine Annealing)或ReduceLROnPlateau
- 批量归一化:在每次卷积后添加BatchNorm层,加速收敛并提高稳定性
- Patch-based训练:对于大尺寸医学图像(如病理切片),采用随机裁剪的patch方式训练
3.5 U-Net变体与演进
U-Net自提出以来衍生出大量变体,形成了U-Net家族:3D U-Net将所有2D操作替换为3D操作,适用于CT/MRI等三维医学图像;Attention U-Net引入注意力门控(Attention Gate)机制,让网络自动聚焦于目标区域;UNet++通过嵌套密集跳跃连接(Nested Dense Skip Pathways)缩短编码器与解码器之间的语义鸿沟;nnU-Net提出自适应框架,能根据数据集自动配置网络结构、预处理和训练策略,在多个医学分割挑战赛中夺冠。
四、Mask R-CNN:实例分割的里程碑
Mask R-CNN由He等人于2017年在ICCV上提出(论文:Mask R-CNN),获得了当年的最佳论文奖。它是在Faster R-CNN目标检测框架的基础上,通过添加一个并行的分割分支(Mask Branch)来实现实例分割。Mask R-CNN在速度和精度上都达到了当时的最优水平,成为实例分割的事实标准。
4.1 Faster R-CNN扩展
Faster R-CNN包含两个阶段:第一阶段使用区域提案网络(RPN)生成候选区域(Region Proposals);第二阶段对每个候选区域进行RoI池化(RoIPool),然后通过分类头和回归头预测类别和边界框。Mask R-CNN在第二阶段增加了第三个并行分支——分割头(Mask Head),对每个RoI区域预测一个二值分割掩码(binary mask)。这种多任务学习框架同时进行目标检测和像素级分割,不同任务之间共享骨干网络的特征,相互促进。
Input Image -> Backbone (ResNet+FPN) -> Feature Maps
+---> RPN (Region Proposal Network) ---> RoIs
+---> RoIAlign ---> [Classification Branch]
| +-> [BBox Regression Branch]
+---> RoIAlign ---> [Mask Branch (Segmentation)]
4.2 RoIAlign:像素级对齐的关键
这是Mask R-CNN最核心的技术创新之一。Faster R-CNN使用的RoIPool(RoI池化)在量化操作中损失了空间精度——当候选区域坐标是浮点数时,RoIPool会通过取整操作将其量化为整数,导致像素级别的偏移。对于目标检测(只需预测边界框坐标),这种偏移影响不大;但对于像素级分割任务,亚像素精度的损失会直接影响掩码质量。
RoIAlign通过双线性插值(Bilinear Interpolation)解决这一问题:在采样过程中不进行任何取整操作,而是保留浮点数坐标,在特征图上通过双线性插值计算出每个采样点的值。这使得RoIAlign能够保持像素级的空间对齐,显著提升了分割掩码的精度。
import torch
import torch.nn as nn
import torchvision
from torchvision.ops import roi_align
# RoIAlign 使用示例
# 特征图: batch=1, channels=256, height=32, width=32
feature_map = torch.randn(1, 256, 32, 32)
# RoI 提议区域 [batch_idx, x1, y1, x2, y2] 坐标在原图尺度
rois = torch.tensor([
[0, 10.0, 15.0, 100.0, 120.0], # 第一个RoI
[0, 50.0, 30.0, 180.0, 200.0], # 第二个RoI
])
# 输出尺寸: 7x7, 空间尺度: 原图/特征图 = 32 (下采样倍数)
output = roi_align(
feature_map,
rois,
output_size=(7, 7),
spatial_scale=1.0 / 32.0, # 原图到特征图的缩放比例
aligned=True, # 启用精确对齐(RoIAlign vs RoIPool)
)
print(f"Output shape: {output.shape}")
# torch.Size([2, 256, 7, 7])
4.3 分割分支与Keypoint分支
Mask R-CNN的分割分支是一个小型FCN,对每个RoI预测一个m x m的掩码(通常28x28)。与语义分割不同,分割分支为每个类别独立预测掩码(class-specific),通过分类分支预测的类别来索引对应的掩码,避免了类别间的竞争。这种解耦设计使得分割分支可以专注于学习像素级定位,而将分类任务交给分类分支。
除了分割分支,Mask R-CNN还可以轻松扩展其他分支。原论文展示了人体关键点检测(Keypoint Detection)分支,将Mask R-CNN扩展到多人姿态估计任务。这种灵活的多任务架构是Mask R-CNN的一大优势——不同任务共享骨干网络的特征提取能力,各任务头专注于自身的预测目标。
4.4 实例分割完整流程
- 特征提取:输入图像经过ResNet+FPN骨干网络,生成多尺度特征图(P2-P6)
- 区域提案:RPN在特征图的每个位置生成锚框(anchors),筛选出前景候选区域
- RoIAlign:对每个候选区域,从对应的特征层级提取固定尺寸(7x7)的特征
- 目标检测:分类头预测类别,回归头微调边界框坐标(使用NMS去除重复检测)
- 掩码预测:对每个检测到的目标,分割头在14x14的RoI上预测28x28的二值掩码
- 上采样融合:将预测掩码上采样到边界框尺寸,融合到原图对应位置
Mask R-CNN的设计哲学:
将实例分割分解为"检测+分割"两个子任务,让每个子任务专注于自己擅长的部分。检测任务(分类+回归)负责"找到物体在哪里、是什么类别",分割任务负责"在检测到的区域内精确勾勒物体轮廓"。这种分而治之的策略不仅使模型更易优化,也为后续的模块化改进提供了清晰的接口。