确认自己有没有学进去就是看自己是不是能讲清楚!
以下内容仅作为记录自己学习的历程使用 ^ ^
理论篇
前置知识
一.U-net
分割:输入图像时候,模型对图像中的每个像素点进行类别分类,如下图
图像中属于小孩的像素点被分割模型分类为粉色,机车被分类为绿色。有了这个概念之后,我们再来看下图。
上图是VGG16的网络架构。原始图像的尺寸为224x224x3,经过若干卷积层之后在被全连接层拉平之前,维度变成了7x7x512。通道数比之前大但是宽度和长度都比之前小。这是因为在普通卷积和最大池化层中,每次经过一次卷积图像特征的通道数被加深,每经过一次最大池化层,图像特征的长度和宽度会被减半(池化窗口是2,步长合适2的情况下)。
但是对于U-net,它的网路结构如下图所示
其中右半段的绿色的向上箭头为反卷积,当图像特征经过反卷积之后,特征尺寸的长和宽会被放大。下面我们快速看一下反卷积是怎么实现的。
1.1 反卷积
如上图所示(图自吴恩达老师的深度学习课),输入为一个2x2的特征,卷积核为3x3,padding填充为1,步长stride为2,接下来来看看它具体是怎么算的。
首先输入图像左上角的特征值2和卷积核的所有权重相乘,然后“贴到”输出上,padding的区域先忽视掉,所以值填充了背景色为浅红色的地方,填充的值如图所示。
接下来是输入特征的右上角特征值为1,和卷积核的所有权重分别相乘,因为我们的步长为2,所以计算后的矩阵向右走了2个格子的步长。忽视掉padding的区域之后,发现绿色的部分和红色的部分会有重叠,在重叠区域我们将红色的部分计算的特征值和绿色的部分计算的特征值相加。最终结果如上图所示。
接下来是输入特征值的左下角部分3,分别和卷积核的权重分别相乘,因为步长为2,所以向下走了2个格子。忽视掉padding的区域,重叠的部分分别相加,得到结果如上图所示。
重复上述步骤,完成粉色部分的计算。在熟悉了这个内容之后,再看U-net网络结构部分。这里为了方便阅读我再把U-net的图搬过来一遍。
右下角深蓝色的箭头:conv 3x3,Relue 就是卷积层,卷积核为3x3,然后经过Relu激活。
灰色箭头:copy and crop 复制和裁剪。这里的意思是对于输出的尺寸,进行复制并且进行中心裁剪。输出的特征再和其他特征进行拼接。
红色箭头: max pool 2x2, 最大池化层,卷积核为2x2。
绿色箭头: up-conv 2x2,就是上面讲到的反卷积,卷积核2x2。
淡蓝色箭头: conv 1x1, 卷积核为1x1的卷积层。
现在我们结合代码走一遍网络结构。
先导包
import torch
import torch.nn as nn
输入的图像是1x572x572,1代表通道数。经过3x3的卷积层变为了64x570x570,由此可以得到相关参数: in_channels = 1,out_channels = 64,kernel_size = 3, stride = 1, padding = 0.后续所有蓝色的3x3卷积层都是这样,所以 kernel_size = 3, stride = 1, padding = 0 可以 固定下来,我们只需改输入输出的通道数就行了。
# 由572x572x1变成了570x570x64
self.conv1_1 = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=0)
self.relu1_1 = nn.ReLU(inplace=True)
# 由570x570x64变成了568x568x64
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=0)
self.relu1_2 = nn.ReLU(inplace=True)
接下来就是最大池化层,卷积核和步长为2,输出的特征w和h都减半,通道数不变
self.maxpool_1 = nn.MaxPool2d(kernel_size=2, stride=2)
经过最大池化层之后尺寸变为284x284x64。接下来就是先后经过2层3x3的卷积层
self.conv2_1 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=0) # 284x284x64->282x282x128
self.relu2_1 = nn.ReLU(inplace=True)
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=0)
# 282x282x128->280x280x128
self.relu2_2 = nn.ReLU(inplace=True)
又是经过最大池化层,输出特征w和h减半,通道数不变。
self.maxpool_2 = nn.MaxPool2d(kernel_size=2, stride=2)
尺寸由280x280x128变为140x140x128.然后再次经过两层3x3卷积层。
self.conv3_1 = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=0) # 140x140x128->138x138x256
self.relu3_1 = nn.ReLU(inplace=True)
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0)
# 138x138x256->136x136x256
self.relu3_2 = nn.ReLU(inplace=True)
继续经过最大池化层,输出特征w和h减半,通道数不变。
self.maxpool_3 = nn.MaxPool2d(kernel_size=2, stride=2)
尺寸由136x136x256变为68x68x256.然后再次经过两层3x3卷积层。
self.conv4_1 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=0) # 68x68x256->66x66x512
self.relu4_1 = nn.ReLU(inplace=True)
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=0)
# 66x66x512->64x64x512
self.relu4_2 = nn.ReLU(inplace=True)
继续经过最大池化层,输出特征w和h减半,通道数不变。
self.maxpool_4 = nn.MaxPool2d(kernel_size=2, stride=2)
尺寸由64x64x512变为32x32x512.然后再次经过两层3x3卷积层。
self.conv5_1 = nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=0)
# 32x32x512->30x30x1024
self.relu5_1 = nn.ReLU(inplace=True)
self.conv5_2 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=0)
# 30x30x1024->28x28x1024
self.relu5_2 = nn.ReLU(inplace=True)
因为涉及到复制,所以forward函数每次的变量名得注意一下,下面是前半段U-net的forward
def forward(self, x):
x1 = self.conv1_1(x)
x1 = self.relu1_1(x1)
x2 = self.conv1_2(x1)
x2 = self.relu1_2(x2) # 这个后续需要使用
down1 = self.maxpool_1(x2)
x3 = self.conv2_1(down1)
x3 = self.relu2_1(x3)
x4 = self.conv2_2(x3)
x4 = self.relu2_2(x4) # 这个后续需要使用
down2 = self.maxpool_2(x4)
x5 = self.conv3_1(down2)
x5 = self.relu3_1(x5)
x6 = self.conv3_2(x5)
x6 = self.relu3_2(x6) # 这个后续需要使用
down3 = self.maxpool_3(x6)
x7 = self.conv4_1(down3)
x7 = self.relu4_1(x7)
x8 = self.conv4_2(x7)
x8 = self.relu4_2(x8) # 这个后续需要使用
down4 = self.maxpool_4(x8)
x9 = self.conv5_1(down4)
x9 = self.relu5_1(x9)
x10 = self.conv5_2(x9)
x10 = self.relu5_2(x10)
接下来不一样的来了,绿色箭头反卷积一次。数据维度从28x28x1024变成56x56x512。
#上采样中的up-conv2*2
self.up_conv_1 = nn.ConvTranspose2d(in_channels=1024, out_channels=512, kernel_size=2, stride=2, padding=0)
# 28x28x1024->56x56x512
然后最下面的灰色箭头复制和中心裁剪,把64x64x512特征的尺寸变为56x56x512,这样子和反卷积出来的特征尺寸一致,就可以直接拼接了。
# 中心裁剪,
def crop_tensor(self, tensor, target_tensor):
target_size = target_tensor.size()[2]
tensor_size = tensor.size()[2]
delta = tensor_size - target_size
delta = delta // 2
# 如果原始张量的尺寸为10,而delta为2,那么"delta:tensor_size - delta"将截取从索引2到索引8的部分,长度为6,以使得截取后的张量尺寸变为6。
return tensor[:, :, delta:tensor_size - delta, delta:tensor_size - delta]
第一次的反卷积+拼接就是下面代码展示
# 第一次上采样,需要"Copy and crop"(复制并裁剪)
up1 = self.up_conv_1(x10) # 得到56x56x512
# 需要对x8进行裁剪,从中心往外裁剪
crop1 = self.crop_tensor(x8, up1)
# 拼接操作
up_1 = torch.cat([crop1, up1], dim=1)
使用torch.cat()函数对张量列表在指定维度上进行拼接,这里就是将crop1和up1进行在通道数维度上的拼接,最后拼接成1024x56x56大小的数据(由unet架构图中可以看出,crop1在前面,up1在后面)。
拼接后的特征尺寸为56x56x1024,然后经过两层3x3卷积到52x52x512,这部分卷积和前半段卷积一模一样,在这里不再赘述。 右半段经过反卷积 + 拼接 + 后(中间的可以自己推一下,都是重复内容不再赘述)特征尺寸到了388x388x64,最后再接一个1x1的卷积层
# 最后的conv1*1
self.conv_1x1 = nn.Conv2d(in_channels=64, out_channels=2, kernel_size=1, stride=1, padding=0)
尺寸由388x388x64变为338x338x2。 整体的代码框架如下:
import torch
import torch.nn as nn
class Unet(nn.Module):
def __init__(self):
super(Unet, self).__init__()
self.conv1_1 = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=0) # 由572*572*1变成了570*570*64
self.relu1_1 = nn.ReLU(inplace=True)
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=0) # 由570*570*64变成了568*568*64
self.relu1_2 = nn.ReLU(inplace=True)
self.maxpool_1 = nn.MaxPool2d(kernel_size=2, stride=2) # 采用最大池化进行下采样,图片大小减半,通道数不变,由568*568*64变成284*284*64
self.conv2_1 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=0) # 284*284*64->282*282*128
self.relu2_1 = nn.ReLU(inplace=True)
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=0) # 282*282*128->280*280*128
self.relu2_2 = nn.ReLU(inplace=True)
self.maxpool_2 = nn.MaxPool2d(kernel_size=2, stride=2) # 采用最大池化进行下采样 280*280*128->140*140*128
self.conv3_1 = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=0) # 140*140*128->138*138*256
self.relu3_1 = nn.ReLU(inplace=True)
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0) # 138*138*256->136*136*256
self.relu3_2 = nn.ReLU(inplace=True)
self.maxpool_3 = nn.MaxPool2d(kernel_size=2, stride=2) # 采用最大池化进行下采样 136*136*256->68*68*256
self.conv4_1 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=0) # 68*68*256->66*66*512
self.relu4_1 = nn.ReLU(inplace=True)
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=0) # 66*66*512->64*64*512
self.relu4_2 = nn.ReLU(inplace=True)
self.maxpool_4 = nn.MaxPool2d(kernel_size=2, stride=2) # 采用最大池化进行下采样 64*64*512->32*32*512
self.conv5_1 = nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=0) # 32*32*512->30*30*1024
self.relu5_1 = nn.ReLU(inplace=True)
self.conv5_2 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=0) # 30*30*1024->28*28*1024
self.relu5_2 = nn.ReLU(inplace=True)
# 接下来实现上采样中的up-conv2*2
self.up_conv_1 = nn.ConvTranspose2d(in_channels=1024, out_channels=512, kernel_size=2, stride=2, padding=0) # 28*28*1024->56*56*512
self.conv6_1 = nn.Conv2d(in_channels=1024, out_channels=512, kernel_size=3, stride=1, padding=0) # 56*56*1024->54*54*512
self.relu6_1 = nn.ReLU(inplace=True)
self.conv6_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=0) # 54*54*512->52*52*512
self.relu6_2 = nn.ReLU(inplace=True)
self.up_conv_2 = nn.ConvTranspose2d(in_channels=512, out_channels=256, kernel_size=2, stride=2, padding=0) # 52*52*512->104*104*256
self.conv7_1 = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=3, stride=1, padding=0) # 104*104*512->102*102*256
self.relu7_1 = nn.ReLU(inplace=True)
self.conv7_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=0) # 102*102*256->100*100*256
self.relu7_2 = nn.ReLU(inplace=True)
self.up_conv_3 = nn.ConvTranspose2d(in_channels=256, out_channels=128, kernel_size=2, stride=2, padding=0) # 100*100*256->200*200*128
self.conv8_1 = nn.Conv2d(in_channels=256, out_channels=128, kernel_size=3, stride=1, padding=0) # 200*200*256->198*198*128
self.relu8_1 = nn.ReLU(inplace=True)
self.conv8_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=0) # 198*198*128->196*196*128
self.relu8_2 = nn.ReLU(inplace=True)
self.up_conv_4 = nn.ConvTranspose2d(in_channels=128, out_channels=64, kernel_size=2, stride=2, padding=0) # 196*196*128->392*392*64
self.conv9_1 = nn.Conv2d(in_channels=128, out_channels=64, kernel_size=3, stride=1, padding=0) # 392*392*128->390*390*64
self.relu9_1 = nn.ReLU(inplace=True)
self.conv9_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=0) # 390*390*64->388*388*64
self.relu9_2 = nn.ReLU(inplace=True)
# 最后的conv1*1
self.conv_10 = nn.Conv2d(in_channels=64, out_channels=2, kernel_size=1, stride=1, padding=0)
# 中心裁剪,
def crop_tensor(self, tensor, target_tensor):
target_size = target_tensor.size()[2]
tensor_size = tensor.size()[2]
delta = tensor_size - target_size
delta = delta // 2
# 如果原始张量的尺寸为10,而delta为2,那么"delta:tensor_size - delta"将截取从索引2到索引8的部分,长度为6,以使得截取后的张量尺寸变为6。
return tensor[:, :, delta:tensor_size - delta, delta:tensor_size - delta]
def forward(self, x):
x1 = self.conv1_1(x)
x1 = self.relu1_1(x1)
x2 = self.conv1_2(x1)
x2 = self.relu1_2(x2) # 这个后续需要使用
down1 = self.maxpool_1(x2)
x3 = self.conv2_1(down1)
x3 = self.relu2_1(x3)
x4 = self.conv2_2(x3)
x4 = self.relu2_2(x4) # 这个后续需要使用
down2 = self.maxpool_2(x4)
x5 = self.conv3_1(down2)
x5 = self.relu3_1(x5)
x6 = self.conv3_2(x5)
x6 = self.relu3_2(x6) # 这个后续需要使用
down3 = self.maxpool_3(x6)
x7 = self.conv4_1(down3)
x7 = self.relu4_1(x7)
x8 = self.conv4_2(x7)
x8 = self.relu4_2(x8) # 这个后续需要使用
down4 = self.maxpool_4(x8)
x9 = self.conv5_1(down4)
x9 = self.relu5_1(x9)
x10 = self.conv5_2(x9)
x10 = self.relu5_2(x10)
# 第一次上采样,需要"Copy and crop"(复制并裁剪)
up1 = self.up_conv_1(x10) # 得到56*56*512
# 需要对x8进行裁剪,从中心往外裁剪
crop1 = self.crop_tensor(x8, up1)
up_1 = torch.cat([crop1, up1], dim=1)
y1 = self.conv6_1(up_1)
y1 = self.relu6_1(y1)
y2 = self.conv6_2(y1)
y2 = self.relu6_2(y2)
# 第二次上采样,需要"Copy and crop"(复制并裁剪)
up2 = self.up_conv_2(y2)
# 需要对x6进行裁剪,从中心往外裁剪
crop2 = self.crop_tensor(x6, up2)
up_2 = torch.cat([crop2, up2], dim=1)
y3 = self.conv7_1(up_2)
y3 = self.relu7_1(y3)
y4 = self.conv7_2(y3)
y4 = self.relu7_2(y4)
# 第三次上采样,需要"Copy and crop"(复制并裁剪)
up3 = self.up_conv_3(y4)
# 需要对x4进行裁剪,从中心往外裁剪
crop3 = self.crop_tensor(x4, up3)
up_3 = torch.cat([crop3, up3], dim=1)
y5 = self.conv8_1(up_3)
y5 = self.relu8_1(y5)
y6 = self.conv8_2(y5)
y6 = self.relu8_2(y6)
# 第四次上采样,需要"Copy and crop"(复制并裁剪)
up4 = self.up_conv_4(y6)
# 需要对x2进行裁剪,从中心往外裁剪
crop4 = self.crop_tensor(x2, up4)
up_4 = torch.cat([crop4, up4], dim=1)
y7 = self.conv9_1(up_4)
y7 = self.relu9_1(y7)
y8 = self.conv9_2(y7)
y8 = self.relu9_2(y8)
# 最后的conv1*1
out = self.conv_10(y8)
return out
if __name__ == '__main__':
input_data = torch.randn([1, 1, 572, 572])
unet = Unet()
output = unet(input_data)
print(output.shape)
# torch.Size([1, 2, 388, 388])