🌞欢迎来到PyTorch深度学习实战的世界 
🌈博客主页:卿云阁

💌欢迎关注🎉点赞👍收藏⭐️留言📝

📆首发时间:🌹2026年4月9日🌹

✉️希望可以和大家一起完成进阶之路!

🙏作者水平很有限,如果发现错误,请留言轰炸哦!万分感谢!


目录

背景介绍

Segnet核心剖析

池化索引(pooling Indices)

网络结构和代码实现

背景介绍

          如果说 FCN(全卷积网络)和 U-Net 是为了解决通用图像和医学切片的分割而生,那么几

乎与它们同时期诞生的 SegNet,则是带着极强的工程使命下凡的——专攻自动驾驶与增强现实

(AR)。虽然 FCN 开启了深度学习语义分割的先河,但它在自动驾驶的严苛要求面前却显得力不

从心, FCN 在上采样(放大图像)时,过度依赖转置卷积(反卷积)和粗糙的跳跃连接。这导致

它在恢复图像尺寸时,丢失了极其重要的“高频空间位置信息”,预测出来的物体边缘往往是糊成一

团的。在 U-Net 中,为了让解码器(Decoder)能完美复原图像细节,我们把编码器(Encoder)

的每一层特征图都存进了“备忘录”,在跳跃连接时与深层特征进行通道拼接(Concat)。致命缺

点: 一张 [1, 512, 224, 224] 的 Float32 特征图极其庞大。U-Net 存了整整四五层这样的“高清原

图”,导致显存极易爆炸,模型非常臃肿。


Segnet核心剖析

池化索引(pooling Indices)

      其实我认为Segnet最核心的创新部分就是采用了池化索引(pooling Indices)的方法了。什么意

思呢,看下图,我们来详细讲解一下。

Segnet同样的也是使用了编码器-解码器的结构。Segnet的上采样方式与FCN不同,FCN采用的是

反卷积的方式,而Segnet每次池化操作时,它不仅保留了池化后的特征图,还记录了每个池化区

域中最大值的位置(即池化索引)。在解码器部分,它将这些池化索引传递过来,用于指导上采样

过程。通过这样的方式,编码器在低分辨率下检测到的语义信息(通过卷积特征)和原始图像中的

精确空间位置信息(通过池化索引)得以结合,显著提高了分割边界的定位精度。并且非常重要的

一点,SegNet通过使用池化索引进行指导的上采样,避免了使用计算量大的反卷积操作。这使得

SegNet模型参数量少,计算复杂度低,非常适合需要实时处理的场景,就像无人驾驶或者AR场

景。通过pooling Indices获得的上采样feature map是稀疏的,其实从图中就能看出,所以在解码

器部分会有对应的卷积结构,将稀疏的feature map通过卷积变成稠密的feature map。当然其弊

端也会非常明显,在上采样的过程中缺少了学习的过程,所以其在精度上是无法与当时的FCN进行

比较的,但是综合精度、实时性、轻便性来说,Segnet则是更好的考量。


网络结构和代码实现

    保存多个尺度上提取到的特征和全局的上下文信息,为上采样时提供更多的可用信息,从而保留

更多高频细节,实现精细的分割。

带索引的池化和反池化

nn.MaxPool2d

import torch
import torch.nn as nn

# 构造一个 1个Batch, 1个通道, 4x4大小 的输入张量
x = torch.tensor([[[
    [10.,  2.,   3., 14.],
    [ 5.,  6.,   7.,  8.],
    [ 9., 20.,  11., 12.],
    [13., 14.,  15.,  1.]
]]])
# 定义池化层,极其重要的一步:开启 return_indices=True
max_pool = nn.MaxPool2d(kernel_size=2, stride=2, return_indices=True)
# 执行池化
pooled_features, indices = max_pool(x)
print("Pooled features:\n", pooled_features)
print("Indices of max values:\n", indices)
Pooled features:
 tensor([[[[10., 14.],
          [20., 15.]]]])
Indices of max values:
 tensor([[[[ 0,  3],
          [ 9, 14]]]])
# 执行反池化:必须同时传入 特征 + 索引
unpooled_features = max_unpool(pooled_features, indices)
print("Unpooled features:\n", unpooled_features)
Unpooled features:
 tensor([[[[10.,  0.,  0., 14.],
          [ 0.,  0.,  0.,  0.],
          [ 0., 20.,  0.,  0.],
          [ 0.,  0., 15.,  0.]]]])

完整代码

import torch
import torch.nn as nn
import torch.nn.functional as F

class SegNet(nn.Module):
    def __init__(self, num_classes=12):
        super(SegNet, self).__init__()
        
        # ==========================================
        # 1. 编码器 (Encoder) - 完全照搬 VGG16 前5层
        # ==========================================
        # 优化提示:在 BatchNorm 前的 Conv2d 设置 bias=False 可以节省显存
        self.encoder1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
        )
        self.encoder2 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
        )
        self.encoder3 = nn.Sequential(
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
        )
        self.encoder4 = nn.Sequential(
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
        )
        self.encoder5 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
        )

        # ==========================================
        # 2. 解码器 (Decoder) - 完美的对称结构
        # ==========================================
        self.decoder1 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
        )
        self.decoder2 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 256, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
        )
        self.decoder3 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 128, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
        )
        self.decoder4 = nn.Sequential(
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
        )
        self.decoder5 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            # 最后一层:输出维度为类别数,无 BN,无 ReLU,直接输出 Logits
            nn.Conv2d(64, num_classes, kernel_size=3, stride=1, padding=1)
        )

        # ==========================================
        # 3. 核心机制:带索引的池化与反池化
        # ==========================================
        self.max_pool = nn.MaxPool2d(2, 2, return_indices=True)
        self.max_unpool = nn.MaxUnpool2d(2, 2)

        # 初始化参数
        self.initialize_weights()

    def initialize_weights(self):
        """Kaiming 初始化,保证网络在训练初期的稳定性"""
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        # ---------- 编码阶段 (记录索引) ----------
        dim1 = self.encoder1(x)
        x, indices1 = self.max_pool(dim1)
        
        dim2 = self.encoder2(x)
        x, indices2 = self.max_pool(dim2)
        
        dim3 = self.encoder3(x)
        x, indices3 = self.max_pool(dim3)
        
        dim4 = self.encoder4(x)
        x, indices4 = self.max_pool(dim4)
        
        dim5 = self.encoder5(x)
        x, indices5 = self.max_pool(dim5)

        # ---------- 解码阶段 (使用索引还原) ----------
        # 神级操作:直接传入 output_size=dim5.size(),PyTorch 会自动对齐奇数分辨率造成的尺寸差异!
        x = self.max_unpool(x, indices5, output_size=dim5.size())
        x = self.decoder1(x)
        
        x = self.max_unpool(x, indices4, output_size=dim4.size())
        x = self.decoder2(x)
        
        x = self.max_unpool(x, indices3, output_size=dim3.size())
        x = self.decoder3(x)
        
        x = self.max_unpool(x, indices2, output_size=dim2.size())
        x = self.decoder4(x)
        
        x = self.max_unpool(x, indices1, output_size=dim1.size())
        x = self.decoder5(x)

        return x

# === 验证尺寸是否一致 ===
if __name__ == "__main__":
    # 模拟输入一张 475x475 的奇数图片,如果不加 output_size,普通网络极易崩溃
    dummy_input = torch.randn(1, 3, 475, 475)
    model = SegNet(num_classes=12)
    output = model(dummy_input)
    print(f"输入尺寸: {dummy_input.shape}")
    print(f"输出尺寸: {output.shape}") 
    # 完美输出: torch.Size([1, 12, 475, 475])

=== 🚀 开始前向传播 ===

0. 初始输入图像尺寸: torch.Size([1, 3, 475, 475])

Encoder 1: 卷积后 torch.Size([1, 64, 475, 475]) -> 池化后 torch.Size([1, 64, 237, 237])

Encoder 2: 卷积后 torch.Size([1, 128, 237, 237]) -> 池化后 torch.Size([1, 128, 118, 118])

Encoder 3: 卷积后 torch.Size([1, 256, 118, 118]) -> 池化后 torch.Size([1, 256, 59, 59])

Encoder 4: 卷积后 torch.Size([1, 512, 59, 59]) -> 池化后 torch.Size([1, 512, 29, 29])

Encoder 5 (谷底): 卷积后 torch.Size([1, 512, 29, 29]) -> 池化后 torch.Size([1, 512, 14, 14])

Decoder 1: 反池化还原 torch.Size([1, 512, 29, 29]) -> 卷积后 torch.Size([1, 512, 29, 29])

Decoder 2: 反池化还原 torch.Size([1, 512, 59, 59]) -> 卷积后 torch.Size([1, 256, 59, 59])

Decoder 3: 反池化还原 torch.Size([1, 256, 118, 118]) -> 卷积后 torch.Size([1, 128, 118, 118])

Decoder 4: 反池化还原 torch.Size([1, 128, 237, 237]) -> 卷积后 torch.Size([1, 64, 237, 237])

Decoder 5: 反池化还原 torch.Size([1, 64, 475, 475]) -> 最终输出 torch.Size([1, 12, 475, 475])

=== 🏁 前向传播结束 ===

输入尺寸: torch.Size([1, 3, 475, 475])

输出尺寸: torch.Size([1, 12, 475, 475])

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐