续上文8.4章节
Unet实现
1 前期准备
1.1 torch安装
pytorch安装自行解决
1.2 数据集准备
我的是自己模拟的数据,所有数据一共是1600对(inputs,labels),训练集与测试集是9:1抽取的。
我的输入大小为120*240,label大小为256*256.
1.3 网络结构骨架
backbone是Unet,根据自己需求再变。不是改自己网络,而是自己加个卷积适应自己的输入输出。
先down一个基础的Unet图。基于此来修改。
1.4 数据分析、完善网络
【具体的size和channel没所谓的,都是可以直接设置的,怎么设置在实现里面说,这里只说流程】
图中input的size是572x572x1,而我的size是120x240x1,我选择在Unet之前加一个卷积层以让我的输入成为方形120x120x1,为了后续计算方便,通过padding(直接padding或者通过卷积都可以)变成128x128x1。接下来就是常规Unet操作,所以我的网络结构图为:
可以看到整个变化过程:具体如何变在实现中说明(一张破图一下午,骂骂咧咧ing)
120x240x1--卷积-->120x120x1--卷积-->128x128x1--卷积-->128x128x32
--池化-->64x64x32--卷积-->64x64x64
--池化-->32x32x64--卷积-->32x32x128
--池化-->16x16x128--卷积-->16x16x256
--池化-->8x8x256--卷积-->8x8x512--上采样-->16x16x256
--通道拼接-->16x16x512--反卷积-->16x16x256--上采样-->32x32x128
--通道拼接-->32x32x256--反卷积-->32x32x128--上采样-->64x64x64
--通道拼接-->64x64x128--反卷积-->64x64x64--上采样-->128x128x32
--通道拼接-->128x128x64--反卷积-->128x128x32--上采样-->256x256x16
(注意我左边是128开始的,所以没法拼接了,网络结构并不是严格对称的)
--1x1卷积核代替全连接-->256x256x1
2.网络实现
2.1 相关知识
- 首先我们要知道卷积的计算公式:
- 以及反卷积的计算公式:
- 通道channel
说一下我的理解:
2.2 代码实现:
emmmm还是由浅入深地讲解吧:网络的整体代码放在文章最后。
首先导入torch包:
import torch
import torch.nn as nn
然后设计我的网络AdUNet,编写成类,该类继承nn.module。
主要重写两个方法:初始化__init__和参数回传forward
在此之前,为了提高代码复用性,将重复出现的双层卷积设计成一个函数,方便代码复用:
def double_conv(in_channels, out_channels): # 双层卷积模型,神经网络最基本的框架
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels), # 加入Bn层提高网络泛化能力(防止过拟合),加收敛速度
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, 3, padding=1), # 3指kernel_size,即卷积核3*3
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
OK,开始。
2.2.1 初始化方法__init__():
- 输入适配层
首先自主设计让输入适应网络的卷积层adnet放入网络AdUNet的类里,将输入1x120x240卷积为方形1x120x120,利用pytorch自带的卷积核方法Conv2d来实现:
self.adnet = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(2, 1), padding=0, stride=(2, 1)),
nn.BatchNorm2d(1), # 加入Bn层提高网络泛化能力(防止过拟合),加收敛速度
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=5, stride=1),
nn.BatchNorm2d(1), # 加入Bn层提高网络泛化能力(防止过拟合),加收敛速度
nn.ReLU(inplace=True)
)
- 4个下采样时的卷积层+一个底层的卷积层
self.dconv_down0 = double_conv(1, 32)
self.dconv_down1 = double_conv(32, 64)
self.dconv_down2 = double_conv(64, 128)
self.dconv_down3 = double_conv(128, 256)
self.dconv_down4 = double_conv(256, 512)
- 最大池化层
self.maxpool = nn.MaxPool2d(2)
- 4个上采样时的卷积层
self.dconv_up3 = double_conv(256 + 256, 256)
self.dconv_up2 = double_conv(128 + 128, 128)
self.dconv_up1 = double_conv(64 + 64, 64)
self.dconv_up0 = double_conv(64, 32)
- 5个上采样
self.upsample4 = nn.ConvTranspose2d(512, 256, 3, stride=2, padding=1, output_padding=1)
self.upsample3 = nn.ConvTranspose2d(256, 128, 3, stride=2, padding=1, output_padding=1)
self.upsample2 = nn.ConvTranspose2d(128, 64, 3, stride=2, padding=1, output_padding=1)
self.upsample1 = nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1)
self.upsample0 = nn.ConvTranspose2d(16, 8, 3, stride=2, padding=1, output_padding=1)
- 代替全连接层的1x1卷积层
self.conv_last = nn.Conv2d(16, 1, 1)
2.2.2 参数回传方法forward():
按照上图的网络结构将他们拼接起来!就OK了!
哦对,别忘了concat。
为什么不把下采样和上采样的那个重复模块写在一起呢?就是因为我不想传参,因为前面下采样的时候要在pool池之前保留值留给上采样的时候concat,所以就单独写了。concat操作也简单,看看代码就懂了,没什么难点。
def forward(self, x):
# reshape
x = self.adnet(x) # 1x128x128
# encode
conv0 = self.dconv_down0(x) # 32x128x128
x = self.maxpool(conv0) # 32x64x64
conv1 = self.dconv_down1(x) # 64x64x64
x = self.maxpool(conv1) # 64x32x32
conv2 = self.dconv_down2(x) # 128x32x32
x = self.maxpool(conv2) # 128x16x16
conv3 = self.dconv_down3(x) # 256x16x16
x = self.maxpool(conv3) # 256x8x8
x = self.dconv_down4(x) # 512x8x8
# decode
x = self.upsample4(x) # 256x16x16
# 因为使用了3*3卷积核和 padding=1 的组合,所以卷积过程图像尺寸不发生改变,所以省去了crop操作!
x = torch.cat([x, conv3], dim=1) # 512x16x16
x = self.dconv_up3(x) # 256x16x16
x = self.upsample3(x) # 128x32x32
x = torch.cat([x, conv2], dim=1) # 256x32x32
x = self.dconv_up2(x) # 128x32x32
x = self.upsample2(x) # 64x64x64
x = torch.cat([x, conv1], dim=1) # 128x64x64
x = self.dconv_up1(x) # 64x64x64
x = self.upsample1(x) # 32x128x128
x = torch.cat([x, conv0], dim=1) # 64x128x128
x = self.dconv_up0(x) # 32x128x128
x = self.upsample0(x) # 16x256x256
out = self.conv_last(x) # 1x256x256
return out
2.2.3 语义分割实现流程
很遗憾地说,网络的结构虽然实现了,但是距离我们的目标还有一些路,但是还好,这个网络是确确实实可以用的,只要加载数据训练就可以得出结果,甚至可以随机生成一些矩阵当做图像来进行训练。
这里简单说一下流程,预感细节不少,详细实现下篇再说:pytorch快速入门与实战——四、网络训练与测试
训练:
验证:
测试:
2.2.4 整合!(网络完整代码)
import torch
import torch.nn as nn
def double_conv(in_channels, out_channels): # 双层卷积模型,神经网络最基本的框架
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1),
nn.BatchNorm2d(out_channels), # 加入Bn层提高网络泛化能力(防止过拟合),加收敛速度
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, 3, padding=1), # 3指kernel_size,即卷积核3*3
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
class UpSample(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding, output_padding):
super(UpSample, self).__init__()
self.up = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=kernel_size, stride=2, padding=1)
self.conv_relu = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding),
nn.BatchNorm2d(num_features=out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding),
nn.BatchNorm2d(num_features=out_channels),
nn.ReLU(),
)
def forward(self, x, y):
x = self.up(x)
x1 = torch.cat((x, y), dim=0)
x1 = self.conv_relu(x1)
return x1 + x
class AdUNet(nn.Module):
def __init__(self):
super().__init__()
self.adnet = nn.Sequential(
nn.Conv2d(1, 1, (2, 1), padding=0, stride=(2, 1)),
nn.BatchNorm2d(1), # 加入Bn层提高网络泛化能力(防止过拟合),加收敛速度
nn.ReLU(inplace=True),
nn.Conv2d(1, 1, kernel_size=3, padding=5, stride=1),
nn.BatchNorm2d(1), # 加入Bn层提高网络泛化能力(防止过拟合),加收敛速度
nn.ReLU(inplace=True)
)
self.dconv_down0 = double_conv(1, 32)
self.dconv_down1 = double_conv(32, 64)
self.dconv_down2 = double_conv(64, 128)
self.dconv_down3 = double_conv(128, 256)
self.dconv_down4 = double_conv(256, 512)
self.maxpool = nn.MaxPool2d(2)
# self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
self.upsample3 = nn.ConvTranspose2d(512, 256, 3, stride=2, padding=1, output_padding=1)
self.upsample2 = nn.ConvTranspose2d(256, 128, 3, stride=2, padding=1, output_padding=1)
self.upsample1 = nn.ConvTranspose2d(128, 64, 3, stride=2, padding=1, output_padding=1)
self.upsample0 = nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1)
self.upsample = nn.ConvTranspose2d(16, 8, 3, stride=2, padding=1, output_padding=1)
self.dconv_up3 = double_conv(256 + 256, 256)
self.dconv_up2 = double_conv(128 + 128, 128)
self.dconv_up1 = double_conv(64 + 64, 64)
self.dconv_up0 = double_conv(64, 32)
self.conv_last = nn.Conv2d(16, 1, 1)
def forward(self, x):
# reshape
x = self.adnet(x) # 1x128x128
# encode
conv0 = self.dconv_down0(x) # 32x128x128
x = self.maxpool(conv0) # 32x64x64
conv1 = self.dconv_down1(x) # 64x64x64
x = self.maxpool(conv1) # 64x32x32
conv2 = self.dconv_down2(x) # 128x32x32
x = self.maxpool(conv2) # 128x16x16
conv3 = self.dconv_down3(x) # 256x16x16
x = self.maxpool(conv3) # 256x8x8
x = self.dconv_down4(x) # 512x8x8
# decode
x = self.upsample3(x) # 256x16x16
# 因为使用了3*3卷积核和 padding=1 的组合,所以卷积过程图像尺寸不发生改变,所以省去了crop操作!
x = torch.cat([x, conv3], dim=1) # 512x16x16
x = self.dconv_up3(x) # 256x16x16
x = self.upsample2(x) # 128x32x32
x = torch.cat([x, conv2], dim=1) # 256x32x32
x = self.dconv_up2(x) # 128x32x32
x = self.upsample1(x) # 64x64x64
x = torch.cat([x, conv1], dim=1) # 128x64x64
x = self.dconv_up1(x) # 64x64x64
x = self.upsample0(x) # 32x128x128
x = torch.cat([x, conv0], dim=1) # 64x128x128
x = self.dconv_up0(x) # 32x128x128
x = self.upsample(x) # 16x256x256
out = self.conv_last(x) # 1x256x256
return out