U-Net 模型 Python 代码实现
import torch
from torch import nn
import torch.nn.functional as F
# 一次卷积操作,包括 卷积 + BN(可选) + ReLU
class ConvBNReLU(nn.Module):
"""
Conv + BN[optional] + ReLU
"""
def __init__(self, in_ch, out_ch, isBN=True):
super(ConvBNReLU, self).__init__()
self.isBN = isBN
self.conv = nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1)
self.relu = nn.ReLU(inplace=True)
if isBN:
self.bn = nn.BatchNorm2d(out_ch)
def forward(self, x):
x = self.conv(x)
if self.isBN:
x = self.bn(x)
x = self.relu(x)
return x
# 两次卷积,可以选择是否预先进行MaxPool
class DoubleConv(nn.Module):
"""
MaxPool[optional] + ConvBNReLU + ConvBNReLU
"""
def __init__(self, in_ch, out_ch, isBN=True, is_pool=False):
super(DoubleConv, self).__init__()
self.is_pool = is_pool
if is_pool:
self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv1 = ConvBNReLU(in_ch, out_ch, isBN)
self.conv2 = ConvBNReLU(out_ch, out_ch, isBN)
def forward(self, x):
if self.is_pool:
x = self.maxpool(x)
x = self.conv1(x)
x = self.conv2(x)
return x
# 上采样,包括跳线连接encoder部分的feature map
class UpsampleConv(nn.Module):
"""
skip feature ---------------|
[x2 Upsample | Deconv] -- concat -- DoubleConv
"""
def __init__(self, in_ch, skip_ch, out_ch, isDeconv=True, isBN=True):
super(UpsampleConv, self).__init__()
if isDeconv:
self.up = nn.ConvTranspose2d(in_ch, in_ch, kernel_size=2, stride=2)
else:
self.up = nn.UpsamplingBilinear2d(scale_factor=2)
self.double_conv = DoubleConv(in_ch + skip_ch, out_ch, isBN, is_pool=False)
def forward(self, x, x_skip):
x = self.up(x)
# 对于奇数尺寸的下采样,需要进行padding才能与跳连的特征图同尺寸
# 如 5x5 -> 2x2,上采样后 4x4,diffX = diffY = 1, pad: (0, 1, 0, 1)
diffX = x_skip.size()[3] - x.size()[3]
diffY = x_skip.size()[2] - x.size()[2]
x = F.pad(x, (diffX // 2, diffX - diffX//2, diffY // 2, diffY - diffY//2))
x = torch.cat([x, x_skip], dim=1)
x = self.double_conv(x)
return x
# 激活函数选择,支持sigmoid、softmax和无激活函数
def select_activation(name=None):
if not name or name.lower() == 'identity':
return nn.Identity()
elif name.lower() == 'sigmoid':
return nn.Sigmoid()
elif name.lower() == 'softmax':
return nn.Softmax()
else:
raise ValueError(f"current unsupport activation name {name}")
# 最后一层,用于输出大小和通道数符合任务需求的特征图
class SegmentHead(nn.Module):
"""
Conv + Upsample[optional] + Activation[optional]
"""
def __init__(self, in_ch, out_ch, act=None, upsample=1):
super(SegmentHead, self).__init__()
self.conv = nn.Conv2d(in_ch, out_ch, kernel_size=1)
self.upsample = nn.UpsamplingBilinear2d(scale_factor=upsample)
self.act = select_activation(act)
def forward(self, x):
x = self.conv(x)
x = self.upsample(x)
x = self.act(x)
return x
# 利用上面定义的模块,搭建UNet结构
class UNet(nn.Module):
def __init__(self, img_ch=3, base_ch=16, num_class=2, isBN=True, isDeconv=False):
super(UNet, self).__init__()
# Encoder 部分
self.conv_in = DoubleConv(img_ch, base_ch, isBN, is_pool=False)
self.down1 = DoubleConv(base_ch, base_ch * 2, isBN, is_pool=True)
self.down2 = DoubleConv(base_ch * 2, base_ch * 4, isBN, is_pool=True)
self.down3 = DoubleConv(base_ch * 4, base_ch * 8, isBN, is_pool=True)
self.down4 = DoubleConv(base_ch * 8, base_ch * 16, isBN, is_pool=True)
# Decoder 部分
self.up1 = UpsampleConv(base_ch * 16, base_ch * 8, base_ch * 8, isDeconv, isBN)
self.up2 = UpsampleConv(base_ch * 8, base_ch * 4, base_ch * 4, isDeconv, isBN)
self.up3 = UpsampleConv(base_ch * 4, base_ch * 2, base_ch * 2, isDeconv, isBN)
self.up4 = UpsampleConv(base_ch * 2, base_ch, base_ch, isDeconv, isBN)
self.seghead = SegmentHead(base_ch, num_class, act=None, upsample=1)
def forward(self, x_in):
feat = self.conv_in(x_in)
print("feat size : ", feat.size())
d1 = self.down1(feat)
print("d1 size : ", d1.size())
d2 = self.down2(d1)
print("d2 size : ", d2.size())
d3 = self.down3(d2)
print("d3 size : ", d3.size())
d4 = self.down4(d3)
print("d4 size : ", d4.size())
u1 = self.up1(d4, d3)
print("u1 size : ", u1.size())
u2 = self.up2(u1, d2)
print("u2 size : ", u2.size())
u3 = self.up3(u2, d1)
print("u3 size : ", u3.size())
u4 = self.up4(u3, feat)
print("u4 size : ", u4.size())
out = self.seghead(u4)
return out
x_in = torch.randn((1, 3, 224, 224))
unet = UNet(img_ch=3, base_ch=64, num_class=3, isBN=True, isDeconv=False)
x_out = unet(x_in)
print("unet input size : ", x_in.size())
print("unet output size : ", x_out.size())
代码实现了U-Net模型,这是一种经典的语义分割网络架构,广泛用于医疗图像分割等领域。U-Net的核心特点是U形结构,包括编码器(Encoder,下采样提取特征)、解码器(Decoder,上采样恢复分辨率)和跳跃连接(Skip Connections,融合多尺度特征)。该实现支持可选的Batch Normalization(BN)和上采样方式(转置卷积或双线性插值),并通过模块化设计(如ConvBNReLU、DoubleConv等)提高了代码的可读性和灵活性。
下面将按照代码的逻辑结构,从导入库开始,一步一步逐段分析。每段解释包括代码的目的、关键概念、数学公式(如果适用)、实现细节,以及为什么这样设计。代码整体分为辅助模块定义(ConvBNReLU、DoubleConv、UpsampleConv、select_activation、SegmentHead)和主模型类(UNet),以及测试部分。假设输入图像为[1, 3, 224, 224](batch=1,RGB通道=3,高度/宽度=224),base_ch=64,num_class=3,isBN=True,isDeconv=False。
基于计算,特征图尺寸变化如下(所有尺寸均为[batch, channels, height, width]):
- feat: [1, 64, 224, 224]
- d1: [1, 128, 112, 112]
- d2: [1, 256, 56, 56]
- d3: [1, 512, 28, 28]
- d4: [1, 1024, 14, 14]
- u1: [1, 512, 28, 28]
- u2: [1, 256, 56, 56]
- u3: [1, 128, 112, 112]
- u4: [1, 64, 224, 224]
- 输入: [1, 3, 224, 224]
- 输出: [1, 3, 224, 224]
这些尺寸通过卷积公式计算得出:卷积输出尺寸 Hout=Hin+2×padding−kernel_sizestride+1H_{out} = \frac{H_{in} + 2 \times padding - kernel\_size}{stride} + 1Hout=strideHin+2×padding−kernel_size+1(stride默认为1);池化/上采样为2倍变化。实际运行时,代码中的print语句会输出这些尺寸。
1. 导入库
import torch
from torch import nn
import torch.nn.functional as F
- 目的:导入PyTorch核心库,用于定义神经网络和张量操作。
- 详细解释:
import torch:PyTorch主库,提供张量(如torch.randn)和自动微分。from torch import nn:神经网络模块,提供Conv2d、ReLU、BatchNorm2d、ConvTranspose2d、UpsamplingBilinear2d等层。import torch.nn.functional as F:函数式模块,提供pad(填充)和cat(拼接)等操作,无需实例化。
- 为什么需要这些库:U-Net是一个深度学习模型,需要PyTorch的模块化构建。F用于forward中的pad操作,避免边界尺寸不匹配。
2. 单次卷积模块:ConvBNReLU
# 一次卷积操作,包括 卷积 + BN(可选) + ReLU
class ConvBNReLU(nn.Module):
"""
Conv + BN[optional] + ReLU
"""
def __init__(self, in_ch, out_ch, isBN=True):
super(ConvBNReLU, self).__init__()
self.isBN = isBN
self.conv = nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1)
self.relu = nn.ReLU(inplace=True)
if isBN:
self.bn = nn.BatchNorm2d(out_ch)
def forward(self, x):
x = self.conv(x)
if self.isBN:
x = self.bn(x)
x = self.relu(x)
return x
- 目的:定义基本卷积单元,包括卷积、可选BN和ReLU激活,用于构建更复杂的块。
- 详细解释:
__init__:初始化层。nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1):3x3卷积,padding=1保持输入输出尺寸相同。isBN控制是否添加nn.BatchNorm2d(批标准化,加速训练、稳定梯度)。nn.ReLU(inplace=True):就地激活,节省内存。forward:前向传播。顺序:Conv → [BN] → ReLU。
- 数学概念:卷积:yi,j,c=∑k=0Cin−1∑m,nxi+m,j+n,k⋅wm,n,k,c+bcy_{i,j,c} = \sum_{k=0}^{C_{in}-1} \sum_{m,n} x_{i+m, j+n, k} \cdot w_{m,n,k,c} + b_cyi,j,c=∑k=0Cin−1∑m,nxi+m,j+n,k⋅wm,n,k,c+bc。BN:x^=x−μσ2+ϵ⋅γ+β\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \betax^=σ2+ϵx−μ⋅γ+β。
- 为什么这样设计:模块化,便于复用。isBN可选,支持实验(如无BN的轻量模型)。U-Net原版无BN,但添加BN可提升性能。
3. 双卷积模块:DoubleConv
# 两次卷积,可以选择是否预先进行MaxPool
class DoubleConv(nn.Module):
"""
MaxPool[optional] + ConvBNReLU + ConvBNReLU
"""
def __init__(self, in_ch, out_ch, isBN=True, is_pool=False):
super(DoubleConv, self).__init__()
self.is_pool = is_pool
if is_pool:
self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv1 = ConvBNReLU(in_ch, out_ch, isBN)
self.conv2 = ConvBNReLU(out_ch, out_ch, isBN)
def forward(self, x):
if self.is_pool:
x = self.maxpool(x)
x = self.conv1(x)
x = self.conv2(x)
return x
- 目的:定义U-Net的基本构建块,双Conv单元,可选预池化,用于编码器下采样。
- 详细解释:
__init__:is_pool控制是否添加nn.MaxPool2d(2x2最大池化,stride=2,下采样到1/2)。然后两个ConvBNReLU:第一个改变通道(in_ch → out_ch),第二个保持(out_ch → out_ch)。forward:顺序:[MaxPool] → Conv1 → Conv2。
- 尺寸变化:如果is_pool=True,尺寸减半;Conv保持尺寸(padding=1)。
- 为什么:U-Net每个编码层使用双Conv增强非线性表达。is_pool用于下采样路径,便于编码器构建。
4. 上采样模块:UpsampleConv
# 上采样,包括跳线连接encoder部分的feature map
class UpsampleConv(nn.Module):
"""
skip feature ---------------|
[x2 Upsample | Deconv] -- concat -- DoubleConv
"""
def __init__(self, in_ch, skip_ch, out_ch, isDeconv=True, isBN=True):
super(UpsampleConv, self).__init__()
if isDeconv:
self.up = nn.ConvTranspose2d(in_ch, in_ch, kernel_size=2, stride=2)
else:
self.up = nn.UpsamplingBilinear2d(scale_factor=2)
self.double_conv = DoubleConv(in_ch + skip_ch, out_ch, isBN, is_pool=False)
def forward(self, x, x_skip):
x = self.up(x)
# 对于奇数尺寸的下采样,需要进行padding才能与跳连的特征图同尺寸
# 如 5x5 -> 2x2,上采样后 4x4,diffX = diffY = 1, pad: (0, 1, 0, 1)
diffX = x_skip.size()[3] - x.size()[3]
diffY = x_skip.size()[2] - x.size()[2]
x = F.pad(x, (diffX // 2, diffX - diffX//2, diffY // 2, diffY - diffY//2))
x = torch.cat([x, x_skip], dim=1)
x = self.double_conv(x)
return x
- 目的:定义解码器上采样单元,包括上采样、跳跃连接拼接和双Conv精炼。
- 详细解释:
__init__:isDeconv选择上采样方式:True为nn.ConvTranspose2d(转置卷积,可学习);False为nn.UpsamplingBilinear2d(双线性插值,固定)。然后DoubleConv处理拼接后特征(通道in_ch + skip_ch → out_ch)。forward:上采样(2倍)→ pad填充(处理奇数尺寸不匹配,如224偶数无pad)→ torch.cat拼接(dim=1通道维)→ DoubleConv。
- 数学概念:转置卷积输出:Hout=(Hin−1)×2+2H_{out} = (H_{in} - 1) \times 2 + 2Hout=(Hin−1)×2+2(k=2, s=2, p=0)。pad:对称填充确保尺寸匹配。
- 为什么:跳跃连接融合浅层细节(x_skip)和深层语义(x),提升边界精度。pad处理池化导致的尺寸问题(U-Net常见)。
5. 激活函数选择:select_activation
# 激活函数选择,支持sigmoid、softmax和无激活函数
def select_activation(name=None):
if not name or name.lower() == 'identity':
return nn.Identity()
elif name.lower() == 'sigmoid':
return nn.Sigmoid()
elif name.lower() == 'softmax':
return nn.Softmax()
else:
raise ValueError(f"current unsupport activation name {name}")
- 目的:提供灵活的输出激活函数选择,支持二元(sigmoid)、多类(softmax)或无激活(identity)。
- 详细解释:根据name返回对应模块。nn.Identity():恒等映射,无变化。nn.Sigmoid():σ(x)=11+e−x\sigma(x) = \frac{1}{1 + e^{-x}}σ(x)=1+e−x1。nn.Softmax():Softmax(xi)=exi∑exj\text{Softmax}(x_i) = \frac{e^{x_i}}{\sum e^{x_j}}Softmax(xi)=∑exjexi(默认dim=1,但代码未指定,实际调用需注意)。
- 为什么:语义分割任务多样(如二元用sigmoid,多类用softmax)。默认identity,便于在损失函数中处理(如CrossEntropyLoss内置softmax)。
6. 输出头:SegmentHead
# 最后一层,用于输出大小和通道数符合任务需求的特征图
class SegmentHead(nn.Module):
"""
Conv + Upsample[optional] + Activation[optional]
"""
def __init__(self, in_ch, out_ch, act=None, upsample=1):
super(SegmentHead, self).__init__()
self.conv = nn.Conv2d(in_ch, out_ch, kernel_size=1)
self.upsample = nn.UpsamplingBilinear2d(scale_factor=upsample)
self.act = select_activation(act)
def forward(self, x):
x = self.conv(x)
x = self.upsample(x)
x = self.act(x)
return x
- 目的:最终层,将解码器输出转换为目标通道和尺寸的分割图。
- 详细解释:
__init__:1x1卷积(in_ch → out_ch,降维到num_class)。nn.UpsamplingBilinear2d(scale_factor=upsample):可选上采样(默认1,无变化)。act从select_activation选择。forward:Conv → Upsample → Act。
- 为什么:适应不同任务(如多类分割)。upsample支持额外放大(虽默认1,但可扩展)。
7. 主模型:UNet
# 利用上面定义的模块,搭建UNet结构
class UNet(nn.Module):
def __init__(self, img_ch=3, base_ch=16, num_class=2, isBN=True, isDeconv=False):
super(UNet, self).__init__()
# Encoder 部分
self.conv_in = DoubleConv(img_ch, base_ch, isBN, is_pool=False)
self.down1 = DoubleConv(base_ch, base_ch * 2, isBN, is_pool=True)
self.down2 = DoubleConv(base_ch * 2, base_ch * 4, isBN, is_pool=True)
self.down3 = DoubleConv(base_ch * 4, base_ch * 8, isBN, is_pool=True)
self.down4 = DoubleConv(base_ch * 8, base_ch * 16, isBN, is_pool=True)
# Decoder 部分
self.up1 = UpsampleConv(base_ch * 16, base_ch * 8, base_ch * 8, isDeconv, isBN)
self.up2 = UpsampleConv(base_ch * 8, base_ch * 4, base_ch * 4, isDeconv, isBN)
self.up3 = UpsampleConv(base_ch * 4, base_ch * 2, base_ch * 2, isDeconv, isBN)
self.up4 = UpsampleConv(base_ch * 2, base_ch, base_ch, isDeconv, isBN)
self.seghead = SegmentHead(base_ch, num_class, act=None, upsample=1)
def forward(self, x_in):
feat = self.conv_in(x_in)
print("feat size : ", feat.size())
d1 = self.down1(feat)
print("d1 size : ", d1.size())
d2 = self.down2(d1)
print("d2 size : ", d2.size())
d3 = self.down3(d2)
print("d3 size : ", d3.size())
d4 = self.down4(d3)
print("d4 size : ", d4.size())
u1 = self.up1(d4, d3)
print("u1 size : ", u1.size())
u2 = self.up2(u1, d2)
print("u2 size : ", u2.size())
u3 = self.up3(u2, d1)
print("u3 size : ", u3.size())
u4 = self.up4(u3, feat)
print("u4 size : ", u4.size())
out = self.seghead(u4)
return out
- 目的:组装U-Net模型,包括编码器(5层下采样)、解码器(4层上采样)和输出头。
- 详细解释:
__init__:参数包括输入通道img_ch、基础通道base_ch(通道倍增,如64→128→…→1024)、类别数num_class、isBN、isDeconv。编码器:conv_in(初始双Conv,无池化);down1-4(带池化,下采样)。解码器:up1-4(上采样+跳连)。seghead输出。forward:编码:x_in → feat(初始) → d1-d4(下采样)。解码:u1(d4+d3) → u2(u1+d2) → … → u4(u3+feat)。out:seghead处理。print调试尺寸。
- 为什么:标准U-Net结构,base_ch控制模型大小(小值轻量)。跳连从浅到深融合。print便于验证尺寸匹配。
8. 测试部分
x_in = torch.randn((1, 3, 224, 224))
unet = UNet(img_ch=3, base_ch=64, num_class=3, isBN=True, isDeconv=False)
x_out = unet(x_in)
print("unet input size : ", x_in.size())
print("unet output size : ", x_out.size())
- 目的:创建随机输入,实例化模型,运行前向传播,并打印输入/输出尺寸。
- 详细解释:
torch.randn生成模拟输入。unet实例化(base_ch=64,num_class=3,双线性上采样)。x_out = unet(x_in)调用forward。print验证输出恢复原尺寸。 - 为什么:调试模型,确保端到端工作。输出通道=num_class(3),尺寸与输入相同。
总体总结
- 代码流程:辅助模块 → UNet组装 → 测试验证。
- 适用场景:语义分割,如医疗图像(肿瘤分割)。灵活参数支持自定义(BN提升稳定性,双线性上采样减少伪影)。
- 潜在改进:添加Dropout防过拟合;用ResNet替换编码器;训练时用DiceLoss或CrossEntropyLoss。
- 局限:对奇数尺寸需pad;无act的输出适合logits输入损失函数。
更多推荐
所有评论(0)