Unet学习笔记

一、Unet网络图

在这里插入图片描述
这里才用这么一张Unet的网络结构,具体的参数已经在图中标出,可以看图有左右两边编码和解码的过程,编码过程由卷积和下采样构成,解码过程由卷积和上采样构成。

二、编程实现思路

(一)数据的获取

这里采用VOC2007数据集,可以去飞桨直接下载,这里附上链接https://aistudio.baidu.com/aistudio/datasetdetail/20051
在这里插入图片描述
这里是其中的一些内容
我们主要使用的是两个文件夹内的图片:JPEGImages和SegmentationClass
其中JPEGImages是网络输入端的格式,SegmentationClass是网络输出端的格式
思路是将前者输入,得到的输出out与后者进行计算loos从而实现学习

(二)Dataloader

怎样将下载好的数据整理好输送给神经网络是个问题目的这里编写一个继承了torch.utils.data.D ataset的MyDataset类用于将数据格式统一化

class MyDataset(Dataset):
    def __init__(self, path):
        self.path = path
        self.name = os.listdir(os.path.join(path, "SegmentationClass"))  # 获取目标下面所有文件的名称

    def __len__(self):
        return len(self.name)

    def __getitem__(self, index):
        segment_name = self.name[index]   #  xx.png
        segment_path = os.path.join(self.path, "SegmentationClass", segment_name)
        image_path = os.path.join(self.path, "JPEGImages", segment_name.replace('png', 'jpg'))
        segment_image = keep_image_size_open(segment_path)
        image = keep_image_size_open(image_path)
        return transform(image), transform(segment_image)

其中 init(self, path)是初始化函数,由数据位置获取将要训练的数据的分类情况即name,这里的name可以理解为手写体识别里的0-9,即分类的最终结果。
而我们要继承Dataset这个类就必须规定__len__。
__getitem__是获取目标的功能函数:首先我们获取某个目标的类别即name,其次
在这里插入图片描述
分别获取目标的类别和未处理图片本身,而os.path.join()是将括号内的字符串拼接起来,可以看作是将括号内的逗号改为\,此语句达成的结果是self.path\SegmentationClass\segment_name
第二个join的结果则是self.path\SegmentationClass(将后缀名png改为jpg的segment_name)。、
下面我们就把注意力放在keep_image_size_open上面,这是自己写的utils文件里的一个自定义工具:

def keep_image_size_open(path, size=(256,256)):
    img = Image.open(path)
    temp = max(img.size)
    mask = Image.new('RGB', (temp, temp), (0, 0, 0))
    mask.paste(img, (0, 0))
    mask = mask.resize(size)
    return mask

这个工具函数需要输入的参数为路径path和目标整形大小size,首先打开路径的函数,其次取出最大的边即图片矩形的长temp,然后建立一个新的画板mask类型为rgb,画板为边长为temp的矩形,将img的图像粘贴到mask上面,mask.paste(img, (0, 0))语句的作用是将img铁道mask位置为(0,0)的位置上,然后再将mask转化为目标大小,最后返回mask。
我们在回到MyDataset,将目标图片用自定义的keep_image_size_open函数归一化后,使用transform的totensor函数转化为tensor数据类型然后返回,获得数据图片及分好类的结果。

(三)神经网络的搭建

再次回到Unet网络这张图,

在这里插入图片描述
其实在图的右下角已经表明了所用的处理方法,我们可以看作是九个卷积层、四个下采样层和四个上采样层。

1.卷积层

分析:
观察可以得知途中的蓝色箭头是两个一组,所以我们写在一起

class Conv_Block(nn.Module):
    def __init__(self, in_channel, out_channel):
        super(Conv_Block, self).__init__()
        self.layer = nn.Sequential(
            nn.Conv2d(in_channel, out_channel, 3, 1, 1, padding_mode='reflect', bias=False),  # reflect是反射的模板
            nn.BatchNorm2d(out_channel),
            nn.Dropout2d(0.3),
            nn.LeakyReLU(),
            nn.Conv2d(out_channel, out_channel, 3, 1, 1, padding_mode='reflect', bias=False),
            nn.BatchNorm2d(out_channel),
            nn.Dropout2d(0.3),
            nn.LeakyReLU()
        )

    def forward(self, x):
        return self.layer(x)

首先此卷积类继承了nn.Mdule类,由于图中各卷积层内的输入输出channel数都是不一样的,所以我们定义的卷积类需要两个参数也就是in_channel和out_channel。
我们把这几个方法Sequential起来:第一是一个二维卷积Conv2d,括号内的3,1,1分别表示卷积核大小、卷积步长和padding,这里padding模板我们选做是reflec即反射图内的数据对图做补充, 目前我正在初学阶段,所以这里的BatchNorm2d暂时理解为使一批(Batch)feature map满足均值为0,方差为1的分布规律。这样不仅数据分布一致,而且避免发生梯度消失,是一种优化手段,Dropout设置是为了防止训练的网络过拟合,而LeakyReLU是一段激活函数。leak
这就是此激活函数图。
随后我们将这个卷积处理过程照本宣科再来一遍。最后在前向过程中使用这个卷积过程,并返回结果。

2.下采样过程

UNet网络结构图中要使用maxpool也就是用最大池化实现下采样,但是因为用最大池化进行下采样会丢失细节,所以我们使用步长为二的卷积来代替最大池化。

class DownSample(nn.Module):
    def __init__(self, channel):
        super(DownSample, self).__init__()
        self.layer = nn.Sequential(
            nn.Conv2d(channel, channel, 3, 2, 1, padding_mode='reflect', bias=False),
            nn.BatchNorm2d(channel),
            nn.LeakyReLU()
        )

    def forward(self, x):
        return self.layer(x)

池化后,在进行正则化BatchNorm2d和神经节LeakyReLU处理一下。

3.上采样过程

上采样的方法有:像素插值、转置卷积即反卷积和像素融合,我们在这里使用插值法。
插值法使用的是torch.nn.functional里的interpolate进行插值。

class UpSample(nn.Module):
    def __init__(self, channel):
        super(UpSample, self).__init__()
        self.layer = nn.Conv2d(channel, channel//2, 1, 1)

    def forward(self, x, feature_map):
        up = F.interpolate(x, scale_factor=2, mode='nearest')
        out = self.layer(up)
        return torch.cat((out, feature_map), dim=1)

首先对输入的数据进行使用最近邻插值法,scale_factor参数是倍增参数,也就是你希望处理后的数据是原来尺寸的多少倍,然后插值的结果进行卷积使其通道数减半(看图)。最后的torch.cat是使上采样后的out结果和与之处于同一水平线的下采样得到的featuremap混合,以达到unet的目的。

4.合成为整个网络

定义了卷积和上、下采样的类,就需要把他们具现化然后搭建网络了。

class UNet(nn.Module):
    def __init__(self):
        super(UNet, self).__init__()
        self.c1 = Conv_Block(3, 64)
        self.d1 = DownSample(64)
        self.c2 = Conv_Block(64, 128)
        self.d2 = DownSample(128)
        self.c3 = Conv_Block(128, 256)
        self.d3 = DownSample(256)
        self.c4 = Conv_Block(256, 512)
        self.d4 = DownSample(512)
        self.c5 = Conv_Block(512, 1024)
        self.u1 = UpSample(1024)
        self.c6 = Conv_Block(1024, 512)
        self.u2 = UpSample(512)
        self.c7 = Conv_Block(512, 256)
        self.u3 = UpSample(256)
        self.c8 = Conv_Block(256, 128)
        self.u4 = UpSample(128)
        self.c9 = Conv_Block(128, 64)
        self.out = nn.Conv2d(64, 3, 3, 1, 1)
        self.Th = nn.Sigmoid()


    def forward(self, x):
        R1 = self.c1(x)
        R2 = self.c2(self.d1(R1))
        R3 = self.c3(self.d2(R2))
        R4 = self.c4(self.d3(R3))
        R5 = self.c5(self.d4(R4))
        O1 = self.c6(self.u1(R5, R4))
        O2 = self.c7(self.u2(O1, R3))
        O3 = self.c8(self.u3(O2, R2))
        O4 = self.c9(self.u4(O3, R1))
        return self.Th(self.out(O4))

这里我想使用Sequential方法是编程简洁一点,但是不能直接使用我们定义的类,需要将其具现化,所以具现化之后再Sequential结合他们供使用又麻烦了,因为只使用一次。
self.out是将最后一次卷积变换为3通道,然后再使用nn.Sigmoid得到最终结果。
这里的nn.Sigmoid函数作用是将输入的数据映射到0-1之间,是激活函数的一种。

(四)训练神经网络

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
weight_path = 'params/unet.pth'
data_path = r'E:\pycharm工程目录\VOCdevkit\VOC2007'
save_path = 'train_image'

先启动cuda以便用GPU训练神经网络
定义神经网络权重的路径
定义数据的路径
定义训练结果的保存路径

data_loader = DataLoader(MyDataset(data_path), batch_size=2, shuffle=True)

将数据使用自定义的MyDataset整形后使用DataLoader加载

net = UNet().to(device)
if os.path.exists(weight_path):
    net.load_state_dict(torch.load(weight_path))
    print("successful load weight")
else:
    print("not load weight")

首先将网络实体化,判断一下weight_path代表的文件是否存在,如果存在则将weight_path代表的文件加载进刚才实体化的net网络。

opt = optim.Adam(net.parameters())
loss_fun = nn.BCELoss()

这里定义了优化器opt,将net网络的参数net.parameters加载进去。然后再定义一下损失函数loss_fun,这里使用的是nn.BCELoss计算网路输出结果和目标实际分类的二元交叉熵,计算公式是:
在这里插入图片描述
下面进行周期训练

for i, (image, segment_image) in enumerate(data_loader):

首先按照将data_loader的一对imga和sement_image取出来的过程做循环条件

image, segment_image = image.to(device), segment_image.to(device)

将取出来的image和segment_image加入device以便使用gpu训练

out_image = net(image)
train_loss = loss_fun(out_image, segment_image)

将要识别的图片输入后得到输出out_image,然后计算out_image和segment_image之间的二元交叉熵作为损失函数train_loss,train_loss应该是关于卷积核参数的一个函数。

opt.zero_grad()
train_loss.backward()
opt.step()

zero_grad的作用是将上次计算的节点的梯度清零,因为对这次的没有用
train_loss.backward后得到可调节参数的调节梯度
step就是对载入opt的net的参数进行调优

_image = image[0]
_segment_image = segment_image[0]
_out_image = out_image[0]
img = torch.stack([_image, _segment_image, _out_image],dim=0)
save_image(img, f'{save_path}/{i}.png')

因为我定义的batchsize是2,我将每次训练的第一幅图拿出来保存一下,拼接并保存。
至此所有工作完毕。

三、参考资料

[1]https://www.bilibili.com/video/BV11341127iK/?spm_id_from=333.337.search-card.all.click&vd_source=1ef0d15c367b17ac9e4081ab17bd34f5
[2]https://blog.csdn.net/BGoodHabit/article/details/106217527?ops_request_misc=&request_id=&biz_id=102&utm_term=LeakyReLU&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-106217527.142v68wechat,201v4add_ask,213v2t3_control2&spm=1018.2226.3001.4187
[3]https://blog.csdn.net/qq_43115981/article/details/115357394
[4]https://blog.csdn.net/lj2048/article/details/114889359?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167128073016800182798658%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=167128073016800182798658&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-114889359-null-null.142v68wechat,201v4add_ask,213v2t3_control2&utm_term=optim.Adam&spm=1018.2226.3001.4187

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值