深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code)

这篇具有很好参考价值的文章主要介绍了深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code)

图像分割:分割任务就是在原始图像中逐像素的找到你需要的家伙。

分成语义分割和实例分割

  • 语义分割:语义分割就是把每个像素都打上标签(这个像素点是人,树,背景等)(语义分割只区分类别,不区分类别中具体单位)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
    深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
  • 实例分割:实例分割不光要区别类别,还要区分类别中每一个个体[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
    深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

损失函数

给定了一张图像,逐像素点判断,对每一个像素点都得到一个二分类结果,做二分类任务,前景想要的是人就是标签。

逐像素做二分类或者多分类,逐像素的交叉熵

**交叉熵损伤函数:**https://www.zhihu.com/tardis/zm/art/35709485?source_id=1003

根据前景和背景的比例,会加入一个权重项:pos_wight

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

像素点也有难易之分,大前景和大背景都好区分,但是前景和背景相交的边缘点会存在问题。让比较难做的样本点,像素点在损失函数中显得重要一些。

引入gama值之后,使得像素点的难以区分更容易。权重值越高越重要越难区分

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

IOU计算(交并比)

评估的值IOU**:https://blog.csdn.net/lingzhou33/article/details/87901365**

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

MIOU就是计算所有类别的平均值,一般当作分割任务评估指标

Unet网络简介

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
Unet网络,用的不是特别多,16年特别火的一件事,在小目标领域做分割做的相当好,最近的升级版,现在还在用,深度学习往往是越简单的网络用起来效果越好。

Unet最早发表论文是在医学领域。本质的思想解决小目标的问题,物体检测和实例分割很复杂。网络结构越简单,越适合小目标,做改性,做升级能玩的就比较多了。

**编码解码。**思想和谍战片是一样的,现在用的也很广,在小目标用的很广,95%都是用Unet去做的,简单的东西有时候也是高效的。

有数据之后—》做编码(拿一堆卷积层去提特征)特征提取层,输入到网络中—》走几个卷积层,越来越扁,说特征图的个数越来越多了。越来越矮,特征图都越来越小(H,W)。左侧为编码,做成一个特征,右侧为解码,进行上采样的过程,本质就是线性插值。

Unet网络计算过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

深度学习强调特征融合。以前特征最常用的是加法,现在做拼接更多。以前Unet结构做加法比较多,但是太直接了,现在先拼接再下采样得到相同的大小,然后横向进行拼接。

缺点:最开始的特征与最后面的特征代沟太大了。

于是有了Unet++的诞生

Unet++介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

把前面有的特征,能拼的就全部一块去拼,还是特征融合,做拼接,构建网络结构,使其更丰富,特征越多越好。

Unet++继承了Unet的结构,同时又借鉴了DenseNet的稠密连接方式(图1中各种分支)。
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

先看下采样过程,和Unet过程没有什么区别。

Unet++采用了稠密连接的方式,比如X(0,1)是通过X(0,0)和X(1,0)拼接起来的,拼接的过程如下:

假设输入为[8,3,96,96],分别为[batch,C,H,W],经过32个(3,3)卷积核提取特征(stirde=1,padding=1),得到X(0,0)[8, 32, 96, 96]。再经过下采样后进行卷积得到X(1,0)[8, 64, 48, 48]。然后通过对X(1,0)上采样后先进行拼接,再进行卷积得到X(0,1)[8, 32, 96, 96]。
结合后文arch.py代码进行理解。

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

依次类推,可以得到各个特征提取层。

Deep Supervision

Unet++最大的特点是更容易剪枝,如图,如果第三层的Unet++网络已经训练的很好了,就可以直接把最后一层去掉,得到更加轻量化的网络。

第二个特点就是,第一横排的输出都可以计算损失。打个比方:如果你要考清华,那么你不可能最后一次考试突然就680上清华了,肯定是每一次模拟考试的成绩都得好才行。
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

模型实现(文末有代码和数据集)

主要解读模型中的以下5个函数,大家训练的时候用train-20230718.py函数,其中有一个包由于更新,位置发生了变化,因此不采用原有的train函数
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

准备数据集:kaggle细胞数据集(dsb2018_96)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

mask:

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

preprocess_dsb2018.py详细解读如下

由于标签是一个一个标的,首先要做一个预处理操作,将分开的图拼接到一起

import os
from glob import glob

import cv2
import numpy as np
from tqdm import tqdm
# glob库主要用于获取匹配指定模式的文件路径列表,
# tqdm库主要用于在循环中显示进度条,提供可视化的进度反馈。
# 这两个库在数据处理、文件操作和任务执行过程中非常有用。

# 第一步 预处理操作,将分开的图拼接到一起
def main():
    img_size = 96
    # 获取输入的文件路径,从指定目录inputs/stage1_train/下的文件
    paths = glob('inputs/stage1_train/*')
    # 创建了两个目录,
    # 分别是inputs/dsb2018_<img_size>/images和inputs/dsb2018_<img_size>/masks/0,
    # 如果目录已存在则不进行任何操作
    os.makedirs('inputs/dsb2018_%d/images' % img_size, exist_ok=True)
    os.makedirs('inputs/dsb2018_%d/masks/0' % img_size, exist_ok=True)

    for i in tqdm(range(len(paths))):
        path = paths[i]
        # b. 使用cv2.imread()函数读取图像文件,文件路径为path/images/<basename>.png,
        # 其中<basename>是当前路径的基本文件名。
        img = cv2.imread(os.path.join(path, 'images',
                         os.path.basename(path) + '.png'))
        # 创建一个与图像大小相同的全零矩阵mask,用于存储图像。
        mask = np.zeros((img.shape[0], img.shape[1]))
        # 使用glob模块获取当前路径下所有图像文件的路径
        for mask_path in glob(os.path.join(path, 'masks', '*')):
            # 将读取的图像转换为灰度图像,并与阈值127进行比较生成二值图像
            mask_ = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) > 127
            # 将二值图像中的值为True(大于阈值)的像素位置在mask矩阵中对应位置设置为1。
            mask[mask_] = 1

        if len(img.shape) == 2:
            # 如果图像是灰度图(通道数为2),则使用np.tile()函数将其复制为3通道图像。
            # 如果图像有4个通道,则仅保留前3个通道。
            img = np.tile(img[..., None], (1, 1, 3))
        if img.shape[2] == 4:
            img = img[..., :3]
        img = cv2.resize(img, (img_size, img_size))
        mask = cv2.resize(mask, (img_size, img_size))
        cv2.imwrite(os.path.join('inputs/dsb2018_%d/images' % img_size,
                    os.path.basename(path) + '.png'), img)

        cv2.imwrite(os.path.join('inputs/dsb2018_%d/masks/0' % img_size,
                    os.path.basename(path) + '.png'), (mask * 255).astype('uint8'))


if __name__ == '__main__':
    main()

dataset.py详细解读

import os

import cv2
import numpy as np
import torch
import torch.utils.data


class Dataset(torch.utils.data.Dataset):
    def __init__(self, img_ids, img_dir, mask_dir, img_ext, mask_ext, num_classes, transform=None):
        """
        Args:
            img_ids (list): Image ids.
            img_dir: Image file directory.
            mask_dir: Mask file directory.
            img_ext (str): Image file extension.
            mask_ext (str): Mask file extension.
            num_classes (int): Number of classes.
            transform (Compose, optional): Compose transforms of albumentations. Defaults to None.
        
        Note:
            Make sure to put the files as the following structure:
            <dataset name>
            ├── images
            |   ├── 0a7e06.jpg
            │   ├── 0aab0a.jpg
            │   ├── 0b1761.jpg
            │   ├── ...
            |
            └── masks
                ├── 0
                |   ├── 0a7e06.png
                |   ├── 0aab0a.png
                |   ├── 0b1761.png
                |   ├── ...
                |
                ├── 1
                |   ├── 0a7e06.png
                |   ├── 0aab0a.png
                |   ├── 0b1761.png
                |   ├── ...
                ...
        """
        self.img_ids = img_ids      # 用于记录数据集中的图像id
        self.img_dir = img_dir      # 用于存储图像文件的目录路径
        self.mask_dir = mask_dir    # 用于存储标签文件的目录路径
        self.img_ext = img_ext      # 用于指定图像文件的扩展名
        self.mask_ext = mask_ext    # 用于指定标签文件的扩展名
        self.num_classes = num_classes  # 用于记录数据集中的类别数量
        self.transform = transform      # 用于对图像和掩膜进行增强处理。默认为None,表示不进行数据增强。

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

    def __getitem__(self, idx):
        # 构建训练的batch,这是一张图片的处理
        img_id = self.img_ids[idx]
        # 第一步,把数据读进来
        img = cv2.imread(os.path.join(self.img_dir, img_id + self.img_ext))
        # 第二步,读标签
        mask = []
        for i in range(self.num_classes):
            # 使用OpenCV的cv2.imread()函数读取mask图像文件,以灰度图像的形式加载。
            # mask图像文件路径由目录、类别索引、图像id和扩展名拼接而成。
            # 读取的灰度图像通过[..., None]转换为形状为(H, W, 1)的三维数组,并将其添加到mask列表中。
            mask.append(cv2.imread(os.path.join(self.mask_dir, str(i),
                        img_id + self.mask_ext), cv2.IMREAD_GRAYSCALE)[..., None])
        mask = np.dstack(mask)
        # 使用NumPy的np.dstack()函数将mask列表中的多个mask图像沿深度方向堆叠,
        # 生成形状为(H, W, num_classes)的三维数组。
        # 第三步数据增强
        if self.transform is not None:
            augmented = self.transform(image=img, mask=mask) # 这个包比较方便,能把mask也一并做掉
            img = augmented['image']  # 参考https://github.com/albumentations-team/albumentations
            mask = augmented['mask']
        # 第四步归一化
        img = img.astype('float32') / 255
        # 使用NumPy的transpose()函数将图像的维度顺序从(H, W, C)转换为(C, H, W)。
        img = img.transpose(2, 0, 1)
        mask = mask.astype('float32') / 255
        mask = mask.transpose(2, 0, 1)
        
        return img, mask, {'img_id': img_id}

archs.py解读

因为本文主要是针对Unet++进行解读,关于不多做赘述,可以主要看到class NestedUNet(nn.Module)中的forward函数

最重要的也就是forward函数,通过执行函数再去看定义 一目了然,最重要的是该forward函数对应的就是Unet++这个表,最好的办法就是在forward的函数后打一个断点,然后一个一个去观察
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

import torch
from torch import nn

__all__ = ['UNet', 'NestedUNet']

# 基本计算单元
class VGGBlock(nn.Module):
    def __init__(self, in_channels, middle_channels, out_channels):
        super().__init__()
        self.relu = nn.ReLU(inplace=True)
        self.conv1 = nn.Conv2d(in_channels, middle_channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(middle_channels)
        self.conv2 = nn.Conv2d(middle_channels, out_channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        # VGGBlock实际上就是相当于做了两次卷积
        out = self.conv1(x)
        out = self.bn1(out)     # 归一化
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        return out


class UNet(nn.Module):
    def __init__(self, num_classes, input_channels=3, **kwargs):
        super().__init__()

        nb_filter = [32, 64, 128, 256, 512]

        self.pool = nn.MaxPool2d(2, 2)
        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)#scale_factor:放大的倍数  插值

        self.conv0_0 = VGGBlock(input_channels, nb_filter[0], nb_filter[0])
        self.conv1_0 = VGGBlock(nb_filter[0], nb_filter[1], nb_filter[1])
        self.conv2_0 = VGGBlock(nb_filter[1], nb_filter[2], nb_filter[2])
        self.conv3_0 = VGGBlock(nb_filter[2], nb_filter[3], nb_filter[3])
        self.conv4_0 = VGGBlock(nb_filter[3], nb_filter[4], nb_filter[4])

        self.conv3_1 = VGGBlock(nb_filter[3]+nb_filter[4], nb_filter[3], nb_filter[3])
        self.conv2_2 = VGGBlock(nb_filter[2]+nb_filter[3], nb_filter[2], nb_filter[2])
        self.conv1_3 = VGGBlock(nb_filter[1]+nb_filter[2], nb_filter[1], nb_filter[1])
        self.conv0_4 = VGGBlock(nb_filter[0]+nb_filter[1], nb_filter[0], nb_filter[0])

        self.final = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)


    def forward(self, input):
        x0_0 = self.conv0_0(input)
        x1_0 = self.conv1_0(self.pool(x0_0))
        x2_0 = self.conv2_0(self.pool(x1_0))
        x3_0 = self.conv3_0(self.pool(x2_0))
        x4_0 = self.conv4_0(self.pool(x3_0))

        x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))
        x2_2 = self.conv2_2(torch.cat([x2_0, self.up(x3_1)], 1))
        x1_3 = self.conv1_3(torch.cat([x1_0, self.up(x2_2)], 1))
        x0_4 = self.conv0_4(torch.cat([x0_0, self.up(x1_3)], 1))

        output = self.final(x0_4)
        return output


class NestedUNet(nn.Module):
    def __init__(self, num_classes, input_channels=3, deep_supervision=False, **kwargs):
        super().__init__()
        # 定义了一个列表,包含NestedUNet中不同层的通道数
        nb_filter = [32, 64, 128, 256, 512]
        # 深度监督:是否需要都计算损失函数
        self.deep_supervision = deep_supervision

        self.pool = nn.MaxPool2d(2, 2)  # 最大池化,池化核大小为2x2,步幅为2
        # 创建一个上采样层实例,尺度因子为2,采用双线性插值的方式进行上采样,边缘对齐方式为True
        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)

        self.conv0_0 = VGGBlock(input_channels, nb_filter[0], nb_filter[0])
        self.conv1_0 = VGGBlock(nb_filter[0], nb_filter[1], nb_filter[1])
        self.conv2_0 = VGGBlock(nb_filter[1], nb_filter[2], nb_filter[2])
        self.conv3_0 = VGGBlock(nb_filter[2], nb_filter[3], nb_filter[3])
        self.conv4_0 = VGGBlock(nb_filter[3], nb_filter[4], nb_filter[4])

        self.conv0_1 = VGGBlock(nb_filter[0]+nb_filter[1], nb_filter[0], nb_filter[0])
        self.conv1_1 = VGGBlock(nb_filter[1]+nb_filter[2], nb_filter[1], nb_filter[1])
        self.conv2_1 = VGGBlock(nb_filter[2]+nb_filter[3], nb_filter[2], nb_filter[2])
        self.conv3_1 = VGGBlock(nb_filter[3]+nb_filter[4], nb_filter[3], nb_filter[3])

        self.conv0_2 = VGGBlock(nb_filter[0]*2+nb_filter[1], nb_filter[0], nb_filter[0])
        self.conv1_2 = VGGBlock(nb_filter[1]*2+nb_filter[2], nb_filter[1], nb_filter[1])
        self.conv2_2 = VGGBlock(nb_filter[2]*2+nb_filter[3], nb_filter[2], nb_filter[2])

        self.conv0_3 = VGGBlock(nb_filter[0]*3+nb_filter[1], nb_filter[0], nb_filter[0])
        self.conv1_3 = VGGBlock(nb_filter[1]*3+nb_filter[2], nb_filter[1], nb_filter[1])

        self.conv0_4 = VGGBlock(nb_filter[0]*4+nb_filter[1], nb_filter[0], nb_filter[0])

        if self.deep_supervision:
            self.final1 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)
            self.final2 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)
            self.final3 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)
            self.final4 = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)
        else:
            self.final = nn.Conv2d(nb_filter[0], num_classes, kernel_size=1)


    def forward(self, input):
        # 入口函数打个断点,看数据的维度很重要
        print('input:', input.shape)
        x0_0 = self.conv0_0(input)  # 第一次卷积
        print('x0_0:',x0_0.shape)   # 升维 input: torch.Size([8, 32, 96, 96])
        x1_0 = self.conv1_0(self.pool(x0_0))
        print('x1_0:', x1_0.shape)  # 升维,降数据量,x1_0: torch.Size([8, 32, 96, 96])
        x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1))
        # cat 拼接,再经历一次卷积,input是96=32+64,output=32
        print('x0_1:', x0_1.shape)   # x0_1: torch.Size([8, 32, 96, 96])
        # 梳理清楚一个关键点即可,后面依次类推,可以打印结果自己手动推一下
        x2_0 = self.conv2_0(self.pool(x1_0))
        print('x2_0:', x2_0.shape)
        x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1))
        print('x1_1:',x1_1.shape)
        x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1))
        print('x0_2:',x0_2.shape)

        x3_0 = self.conv3_0(self.pool(x2_0))
        print('x3_0:',x3_0.shape)
        x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1))
        print('x2_1:',x2_1.shape)
        x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1))
        print('x1_2:',x1_2.shape)
        x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1))
        print('x0_3:',x0_3.shape)
        x4_0 = self.conv4_0(self.pool(x3_0))
        print('x4_0:',x4_0.shape)
        x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))
        print('x3_1:',x3_1.shape)
        x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1))
        print('x2_2:',x2_2.shape)
        x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1))
        print('x1_3:',x1_3.shape)
        x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1))
        print('x0_4:',x0_4.shape)

        if self.deep_supervision:
            output1 = self.final1(x0_1)
            output2 = self.final2(x0_2)
            output3 = self.final3(x0_3)
            output4 = self.final4(x0_4)
            return [output1, output2, output3, output4]

        else:
            # 输出一个结果,结果是0~1之间
            output = self.final(x0_4)
            return output

train.py解读

train函数分为四个部分
深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

第一部分主要是在parameters输入给定参数的,

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络

第二部分函数是训练函数

第三部分是验证函数

最后一部分是执行函数

import argparse
import os
from collections import OrderedDict
from glob import glob

import pandas as pd
import torch
import torch.backends.cudnn as cudnn
import torch.nn as nn
import torch.optim as optim
import yaml
import albumentations as albu
from albumentations.augmentations import transforms
from albumentations.core.composition import Compose, OneOf
from sklearn.model_selection import train_test_split
from torch.optim import lr_scheduler
from tqdm import tqdm

import archs
import losses
from dataset import Dataset
from metrics import iou_score
from utils import AverageMeter, str2bool

ARCH_NAMES = archs.__all__
LOSS_NAMES = losses.__all__
LOSS_NAMES.append('BCEWithLogitsLoss')

"""

指定参数:
--dataset dsb2018_96 
--arch NestedUNet

"""
# 解析命令行参数并返回配置信息。
def parse_args():
    parser = argparse.ArgumentParser()
# 指定网络的名字-unet++
    parser.add_argument('--name', default=None,
                        help='model name: (default: arch+timestamp)')
# 迭代多少轮
    parser.add_argument('--epochs', default=100, type=int, metavar='N',
                        help='number of total epochs to run')
    # batch_size
    parser.add_argument('-b', '--batch_size', default=8, type=int,
                        metavar='N', help='mini-batch size (default: 16)')
    
    # model
    parser.add_argument('--arch', '-a', metavar='ARCH', default='NestedUNet',
                        choices=ARCH_NAMES,
                        help='model architecture: ' +
                        ' | '.join(ARCH_NAMES) +
                        ' (default: NestedUNet)')
    parser.add_argument('--deep_supervision', default=False, type=str2bool)
    parser.add_argument('--input_channels', default=3, type=int,
                        help='input channels')
    parser.add_argument('--num_classes', default=1, type=int,
                        help='number of classes')
    parser.add_argument('--input_w', default=96, type=int,
                        help='image width')
    parser.add_argument('--input_h', default=96, type=int,
                        help='image height')
    
    # loss
    parser.add_argument('--loss', default='BCEDiceLoss',
                        choices=LOSS_NAMES,
                        help='loss: ' +
                        ' | '.join(LOSS_NAMES) +
                        ' (default: BCEDiceLoss)')
    
    # dataset
    parser.add_argument('--dataset', default='dsb2018_96',
                        help='dataset name')
    parser.add_argument('--img_ext', default='.png',
                        help='image file extension')
    parser.add_argument('--mask_ext', default='.png',
                        help='mask file extension')

    # optimizer
    parser.add_argument('--optimizer', default='SGD',
                        choices=['Adam', 'SGD'],
                        help='loss: ' +
                        ' | '.join(['Adam', 'SGD']) +
                        ' (default: Adam)')
    parser.add_argument('--lr', '--learning_rate', default=1e-3, type=float,
                        metavar='LR', help='initial learning rate')
    parser.add_argument('--momentum', default=0.9, type=float,
                        help='momentum')
    parser.add_argument('--weight_decay', default=1e-4, type=float,
                        help='weight decay')
    parser.add_argument('--nesterov', default=False, type=str2bool,
                        help='nesterov')

    # scheduler
    parser.add_argument('--scheduler', default='CosineAnnealingLR',
                        choices=['CosineAnnealingLR', 'ReduceLROnPlateau', 'MultiStepLR', 'ConstantLR'])
    parser.add_argument('--min_lr', default=1e-5, type=float,
                        help='minimum learning rate')
    parser.add_argument('--factor', default=0.1, type=float)
    parser.add_argument('--patience', default=2, type=int)
    parser.add_argument('--milestones', default='1,2', type=str)
    parser.add_argument('--gamma', default=2/3, type=float)
    parser.add_argument('--early_stopping', default=-1, type=int,
                        metavar='N', help='early stopping (default: -1)')
    
    parser.add_argument('--num_workers', default=0, type=int)

    config = parser.parse_args()

    return config

# 定义训练过程。使用训练数据集进行训练,计算损失函数和评估指标,并更新模型参数。
def train(config, train_loader, model, criterion, optimizer):
    # 初始化平均指标(损失和IOU)的AverageMeter对象
    avg_meters = {'loss': AverageMeter(),
                  'iou': AverageMeter()}
    # 将模型设置为训练模式
    model.train()
    # 进度条可视化
    pbar = tqdm(total=len(train_loader))
    # 遍历训练数据加载器,获取输入和目标数据。
    for input, target, _ in train_loader:
        # 将输入和目标数据移至GPU
        input = input.cuda()
        target = target.cuda()

        # compute output
        # 计算模型的输出和损失。
        if config['deep_supervision']:
            outputs = model(input)
            loss = 0
            for output in outputs:
                loss += criterion(output, target)
            loss /= len(outputs)
            iou = iou_score(outputs[-1], target)
        else:
            output = model(input)
            loss = criterion(output, target)
            iou = iou_score(output, target)

        # compute gradient and do optimizing step
        optimizer.zero_grad()   # 梯度清零
        loss.backward()         # 反向传播
        optimizer.step()        # 更新模型参数
        # 更新平均指标(损失和IOU)的值
        avg_meters['loss'].update(loss.item(), input.size(0))
        avg_meters['iou'].update(iou, input.size(0))
        # 根据当前训练进度,更新并显示进度条的后缀信息,包括平均损失和平均IOU
        postfix = OrderedDict([
            ('loss', avg_meters['loss'].avg),
            ('iou', avg_meters['iou'].avg),
        ])
        pbar.set_postfix(postfix)
        pbar.update(1)
    pbar.close()

    return OrderedDict([('loss', avg_meters['loss'].avg),
                        ('iou', avg_meters['iou'].avg)])


def validate(config, val_loader, model, criterion):
    avg_meters = {'loss': AverageMeter(),
                  'iou': AverageMeter()}

    # switch to evaluate mode
    model.eval()

    with torch.no_grad():
        pbar = tqdm(total=len(val_loader))
        for input, target, _ in val_loader:
            input = input.cuda()
            target = target.cuda()

            # compute output
            if config['deep_supervision']:
                outputs = model(input)
                loss = 0
                for output in outputs:
                    loss += criterion(output, target)
                loss /= len(outputs)
                iou = iou_score(outputs[-1], target)
            else:
                output = model(input)
                loss = criterion(output, target)
                iou = iou_score(output, target)

            avg_meters['loss'].update(loss.item(), input.size(0))
            avg_meters['iou'].update(iou, input.size(0))

            postfix = OrderedDict([
                ('loss', avg_meters['loss'].avg),
                ('iou', avg_meters['iou'].avg),
            ])
            pbar.set_postfix(postfix)
            pbar.update(1)
        pbar.close()

    return OrderedDict([('loss', avg_meters['loss'].avg),
                        ('iou', avg_meters['iou'].avg)])


def main():
    # 解析命令行参数
    config = vars(parse_args())

    if config['name'] is None:
        if config['deep_supervision']:
            config['name'] = '%s_%s_wDS' % (config['dataset'], config['arch'])
        else:
            config['name'] = '%s_%s_woDS' % (config['dataset'], config['arch'])
    os.makedirs('models/%s' % config['name'], exist_ok=True)

    print('-' * 20)
    for key in config:
        print('%s: %s' % (key, config[key]))
    print('-' * 20)
    # 配置参数config保存到文件'models/%s/config.yml' % config['name']中。
    with open('models/%s/config.yml' % config['name'], 'w') as f:
        yaml.dump(config, f)

    # define loss function (criterion)
    if config['loss'] == 'BCEWithLogitsLoss':
        criterion = nn.BCEWithLogitsLoss().cuda()
        # WithLogits 就是先将输出结果经过sigmoid再交叉熵
    else:
        criterion = losses.__dict__[config['loss']]().cuda()
    # 启用CUDNN加速
    cudnn.benchmark = True

    # create model,建模
    print("=> creating model %s" % config['arch'])
    model = archs.__dict__[config['arch']](config['num_classes'],
                                           config['input_channels'],
                                           config['deep_supervision'])

    model = model.cuda()
# 过滤出需要更新的模型参数,并根据配置参数config中的优化器类型和参数设置创建优化器对象optimizer:
    #
    # 如果config['optimizer']为'Adam',使用optim.Adam()创建Adam优化器,
    # 指定学习率config['lr']和权重衰减config['weight_decay']。
    # 如果config['optimizer']为'SGD',使用optim.SGD()创建SGD优化器,
    # 指定学习率config['lr']、动量config['momentum']、是否使用Nesterov动量config['nesterov']和权重衰减config['weight_decay']。
    # 否则,抛出NotImplementedError异常
    params = filter(lambda p: p.requires_grad, model.parameters())

    if config['optimizer'] == 'Adam':
        optimizer = optim.Adam(
            params, lr=config['lr'], weight_decay=config['weight_decay'])
    elif config['optimizer'] == 'SGD':
        optimizer = optim.SGD(params, lr=config['lr'], momentum=config['momentum'],
                              nesterov=config['nesterov'], weight_decay=config['weight_decay'])
    else:
        raise NotImplementedError
# 根据配置参数config中的学习率调度器类型和参数设置创建学习率调度器对象scheduler:

    if config['scheduler'] == 'CosineAnnealingLR':
        scheduler = lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=config['epochs'], eta_min=config['min_lr'])
    elif config['scheduler'] == 'ReduceLROnPlateau':
        scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, factor=config['factor'], patience=config['patience'],
                                                   verbose=1, min_lr=config['min_lr'])
    elif config['scheduler'] == 'MultiStepLR':
        scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[int(e) for e in config['milestones'].split(',')], gamma=config['gamma'])
    elif config['scheduler'] == 'ConstantLR':
        scheduler = None
    else:
        raise NotImplementedError

    # Data loading code
    img_ids = glob(os.path.join('inputs', config['dataset'], 'images', '*' + config['img_ext']))
    # 提取文件名
    img_ids = [os.path.splitext(os.path.basename(p))[0] for p in img_ids]
    # 将文件名列表img_ids分割为训练集和验证集,使用train_test_split()函数,并指定验证集占比、随机种子。
    train_img_ids, val_img_ids = train_test_split(img_ids, test_size=0.2, random_state=41)
    # 数据增强:
    train_transform = Compose([
        transforms.RandomRotate90(),
        transforms.Flip(),
        OneOf([
            transforms.HueSaturationValue(),
            transforms.RandomBrightness(),
            transforms.RandomContrast(),
        ], p=1),# 按照归一化的概率选择执行哪一个
        transforms.Resize(config['input_h'], config['input_w']),
        transforms.Normalize(),
    ])

    val_transform = Compose([
        transforms.Resize(config['input_h'], config['input_w']),
        transforms.Normalize(),
    ])
# 构建数据集
    train_dataset = Dataset(
        img_ids=train_img_ids,
        img_dir=os.path.join('inputs', config['dataset'], 'images'),
        mask_dir=os.path.join('inputs', config['dataset'], 'masks'),
        img_ext=config['img_ext'],
        mask_ext=config['mask_ext'],
        num_classes=config['num_classes'],
        transform=train_transform)
    
    val_dataset = Dataset(
        img_ids=val_img_ids,
        img_dir=os.path.join('inputs', config['dataset'], 'images'),
        mask_dir=os.path.join('inputs', config['dataset'], 'masks'),
        img_ext=config['img_ext'],
        mask_ext=config['mask_ext'],
        num_classes=config['num_classes'],
        transform=val_transform)

    train_loader = torch.utils.data.DataLoader(
        train_dataset,
        batch_size=config['batch_size'],
        shuffle=True,
        num_workers=config['num_workers'],
        drop_last=True)     # 不能整除的batch是否就不要了
    val_loader = torch.utils.data.DataLoader(
        val_dataset,
        batch_size=config['batch_size'],
        shuffle=False,
        num_workers=config['num_workers'],
        drop_last=False)

    log = OrderedDict([
        ('epoch', []),
        ('lr', []),
        ('loss', []),
        ('iou', []),
        ('val_loss', []),
        ('val_iou', []),
    ])

    best_iou = 0
    trigger = 0        # 计数器
    
    for epoch in range(config['epochs']):
        print('Epoch [%d/%d]' % (epoch, config['epochs']))

        # train for one epoch
        train_log = train(config, train_loader, model, criterion, optimizer)
        # evaluate on validation set
        val_log = validate(config, val_loader, model, criterion)

        if config['scheduler'] == 'CosineAnnealingLR':
            scheduler.step()
        elif config['scheduler'] == 'ReduceLROnPlateau':
            scheduler.step(val_log['loss'])

        print('loss %.4f - iou %.4f - val_loss %.4f - val_iou %.4f'
              % (train_log['loss'], train_log['iou'], val_log['loss'], val_log['iou']))

        log['epoch'].append(epoch)
        log['lr'].append(config['lr'])
        log['loss'].append(train_log['loss'])
        log['iou'].append(train_log['iou'])
        log['val_loss'].append(val_log['loss'])
        log['val_iou'].append(val_log['iou'])

        pd.DataFrame(log).to_csv('models/%s/log.csv' %
                                 config['name'], index=False)

        trigger += 1

        if val_log['iou'] > best_iou:
            torch.save(model.state_dict(), 'models/%s/model.pth' %
                       config['name'])
            best_iou = val_log['iou']
            print("=> saved best model")
            trigger = 0

        # early stopping
        if config['early_stopping'] >= 0 and trigger >= config['early_stopping']:
            print("=> early stopping")
            break

        torch.cuda.empty_cache()


if __name__ == '__main__':
    main()

训练结束后,可以通过valid看下模型的训练效果

效果还是不错的。

深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code),深度学习,人工智能,图像处理,python,pytorch,卷积神经网络
代码和数据集请百度网盘自取~
链接:https://pan.baidu.com/s/1OztNLJ0wjcDaJeY1Cs06Pw?pwd=h0ca
提取码:h0ca
–来自百度网盘超级会员V4的分享文章来源地址https://www.toymoban.com/news/detail-760604.html

到了这里,关于深度学习分割任务——Unet++分割网络代码详细解读(文末附带作者所用code)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • (CVPR) PointNet:用于3D分类和分割的点集深度学习 - 详细解读

    目录 知识补充 网络解读 概括 局部和全局特征的融合(分割任务) 联合对准网络(T网络) 总结 点云存在的问题: 详细理解: 参考内容: 点云转换为体素的问题:数据庞大 本文设计了一种直接消耗点云的神经网络(保持了输入中心点的排列不变性) 刚性变换: 指保持物

    2024年03月21日
    浏览(63)
  • 《图像分割Unet网络分析及其Pytorch版本代码实现》

      最近两个月在做学习图像分割方面的学习,踩了无数的坑,也学到了很多的东西,想了想还是趁着国庆节有时间来做个总结,以后有这方面需要可以来看看。   神经网络被大规模的应用到计算机视觉中的分类任务中,说到神经网络的分类任务这里不得不提到CNN(卷积神经网

    2024年02月05日
    浏览(30)
  • UNet深度学习模型在医学图像分割中的应用及其Python实现细节

    第一部分:引言和UNet架构简介 引言 : 医学图像分割是医疗图像处理的重要领域,它涉及将图像划分为多个区域,以标识和隔离感兴趣的区域(如器官、肿瘤等)。近年来,随着深度学习技术的发展,多种神经网络模型被应用于这一领域。其中,UNet模型因其出色的表现而受

    2024年02月12日
    浏览(24)
  • 论文阅读—2023.7.13:遥感图像语义分割空间全局上下文信息网络(主要为unet网络以及改unet)附加个人理解与代码解析

    前期看的文章大部分都是深度学习原理含量多一点,一直在纠结怎么改模型,论文看的很吃力,看一篇忘一篇,总感觉摸不到方向。想到自己是遥感专业,所以还是回归遥感影像去谈深度学习,回归问题,再想着用什么方法解决问题。 1、易丢失空间信息 在 Decoder 阶段输出多

    2024年02月16日
    浏览(34)
  • 【图像分割】Unet系列深度讲解(FCN、UNET、UNET++)

    1.1 背景介绍: 自2015年以来,在生物医学图像分割领域,U-Net得到了广泛的应用,目前已达到四千多次引用。至今,U-Net已经有了很多变体。目前已有许多新的卷积神经网络设计方式,但很多仍延续了U-Net的核心思想,加入了新的模块或者融入其他设计理念。 编码和解码,早在

    2024年02月03日
    浏览(27)
  • 深度学习(8)之 UNet详解(附图文和代码实现)

    卷积神经网络被大规模的应用在分类任务中,输出的结果是整个图像的类标签。但是UNet是像素级分类,输出的则是每个像素点的类别,且不同类别的像素会显示不同颜色,UNet常常用在生物医学图像上,而该任务中图片数据往往较少。所以,Ciresan等人训练了一个卷积神经网络

    2024年02月16日
    浏览(23)
  • 经典神经网络论文超详细解读(五)——ResNet(残差网络)学习笔记(翻译+精读+代码复现)

    《Deep Residual Learning for Image Recognition》这篇论文是何恺明等大佬写的,在深度学习领域相当经典,在2016CVPR获得best paper。今天就让我们一起来学习一下吧! 论文原文:https://arxiv.org/abs/1512.03385 前情回顾: 经典神经网络论文超详细解读(一)——AlexNet学习笔记(翻译+精读)

    2024年02月08日
    浏览(27)
  • 经典神经网络论文超详细解读(八)——ResNeXt学习笔记(翻译+精读+代码复现)

    今天我们一起来学习何恺明大神的又一经典之作:  ResNeXt(《Aggregated Residual Transformations for Deep Neural Networks》) 。这个网络可以被解释为 VGG、ResNet 和 Inception 的结合体,它通过重复多个block(如在 VGG 中)块组成,每个block块聚合了多种转换(如 Inception),同时考虑到跨层

    2024年02月03日
    浏览(37)
  • 经典神经网络论文超详细解读(六)——DenseNet学习笔记(翻译+精读+代码复现)

    上一篇我们介绍了ResNet:经典神经网络论文超详细解读(五)——ResNet(残差网络)学习笔记(翻译+精读+代码复现) ResNet通过短路连接,可以训练出更深的CNN模型,从而实现更高的准确度。今天我们要介绍的是 DenseNet(《Densely connected convolutional networks》) 模型,它的基本

    2024年02月03日
    浏览(41)
  • 计算机视觉与深度学习-图像分割-视觉识别任务03-实例分割-【北邮鲁鹏】

    论文题目:Mask R-CNN 论文链接:论文下载 论文代码:Facebook代码链接;Tensorflow版本代码链接; Keras and TensorFlow版本代码链接;MxNet版本代码链接 参考:Mask R-CNN详解 将图像中的每个像素与其所属的目标实例进行关联,并为每个像素分配一个特定的标签,以实现像素级别的目标

    2024年02月07日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包