图像分割FCN实战一

数据集是一个包包的图片

数据集的代码

'''
dataset.py
'''
import os
import torch
from torch.utils.data import DataLoader, Dataset, random_split
from torchvision import transforms
import numpy as np
import cv2

# transform是对图像进行预处理、数据增强等。Compose将多个处理步骤整合到一起。
# ToTensor:将原始取值0-255像素值,归一化为0-1
# Normalize:用像素值的均值和标准偏差对像素值进行标准化
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225])])

"""
这是一个用于将分类标签转换为one-hot编码的函数。
函数的输入参数包含data和n,其中data是原始的分类标签,n是分类的类别数。
输出结果是一个形状为(data.shape[0],data.shape[1],n)的numpy数组,表示每个样本在n个类别中的one-hot编码
"""
def onehot(data, n):
    buf = np.zeros(data.shape + (n,)) #创建一个形状为data.shape+(n,)的全零数组buf
    nmsk = np.arange(data.size) * n + data.ravel() #使用numpy的arange函数生成一个长度为data.size索引数组,并乘以n,加上data.ravel()
                                                   # 得到每个元素在buf中的位置。
                                                   # ravel()是一个numpy数组的方法,用于将多维数组展开成一维数组

    buf.ravel()[nmsk - 1] = 1  #将buf数组中nmask数组对应的位置设置为1,以得到每个样本在n个类别中的one—hot编码
    return buf


class BagDataset(Dataset):
    def __init__(self, transform=None):
        self.transform = transform

    def __len__(self):
        return len(os.listdir('./last'))

    def __getitem__(self, idx):
        # 读取原图
        img_name = os.listdir('./last')[idx] # 获取./last目录中的所有文件和子目录的名称列表,然后通过idx索引获取其中的一个文件夹名
        imgA = cv2.imread('./last/' + img_name) # 读取图像文件
        imgA = cv2.resize(imgA, (160, 160)) # 缩放图像大小

        # 读取标签图,即二值图
        imgB = cv2.imread('last_mask/' + img_name, 0) # 读取标签文件,灰度模式
        imgB = cv2.resize(imgB, (160, 160)) # 缩放便签缩放

        imgB = imgB / 255 # 将标签图像素值归一化[0,1]之间
        imgB = imgB.astype('uint8') # 将标签图像素值转为uint8类型
        imgB = onehot(imgB, 2)  # 因为此代码是二分类问题,即分割出手提包和背景两样就行,因此这里参数是2
        imgB = imgB.transpose(2, 0, 1)  # imgB不经过transform处理,所以要手动把(H,W,C)转成(C,H,W)
        imgB = torch.FloatTensor(imgB) # 将标签图转换为PyTorch张量
        if self.transform:
            imgA = self.transform(imgA)  # imgA通道就变成(C,H,W)
        return imgA, imgB


bag = BagDataset(transform)
train_size = int(0.9 * len(bag))  # 整个训练集中,90%为训练集
test_size = len(bag) - train_size

train_dataset, test_dataset = random_split(bag, [train_size, test_size])  # 按照上述比例(9:1)划分训练集和测试集
train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=1)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, num_workers=1)

if __name__ == '__main__':
    for train_batch in train_dataloader:
        print(train_batch)

    for test_batch in test_dataloader:
        print(test_batch)

模型

# -*- coding: utf-8 -*-
"""
model.py
"""
import torch.nn as nn
from torchvision.models.vgg import VGG


# 继承nn.Module,撰写自己的网络层
class FCNs(nn.Module):
    '''
    类FCNs:将最后一个特征图直接上采样32倍(5次步长为2、卷积核为3*3的反卷积操作)得到的最终
    分割结果。
    '''

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        self.n_class = n_class
        self.pretrained_net = pretrained_net

        self.relu = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2,
                                          padding=1, dilation=1,
                                          output_padding=1)
        self.bn1 = nn.BatchNorm2d(512)

        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2,
                                          padding=1, dilation=1,
                                          output_padding=1)
        self.bn2 = nn.BatchNorm2d(256)

        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2,
                                          padding=1, dilation=1,
                                          output_padding=1)
        self.bn3 = nn.BatchNorm2d(128)

        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2,
                                          padding=1, dilation=1,
                                          output_padding=1)
        self.bn4 = nn.BatchNorm2d(64)

        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2,
                                          padding=1, dilation=1,
                                          output_padding=1)
        self.bn5 = nn.BatchNorm2d(32)

        # 分类器是1*1大小的卷积,将channel个数从32减小到n_class
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        x5 = output['x5']
        x4 = output['x4']
        x3 = output['x3']
        x2 = output['x2']
        x1 = output['x1']

        score = self.bn1(self.relu(self.deconv1(x5)))
        score = score + x4
        score = self.bn2(self.relu(self.deconv2(score)))
        score = score + x3
        score = self.bn3(self.relu(self.deconv3(score)))
        score = score + x2
        score = self.bn4(self.relu(self.deconv4(score)))
        score = score + x1
        score = self.bn5(self.relu(self.deconv5(score)))
        score = self.classifier(score)
        return score


class VGGNet(VGG):
    def __init__(self, pretrained=False, model='vgg16', requires_grad=True, remove_fc=True, show_params=False):
        super().__init__(make_layers(cfg[model]))
        self.ranges = ranges[model]

        if pretrained:
            exec("self.load_state_dict(models.%s(pretrained=False).state_dict())" % model)

        if not requires_grad:
            for param in super().parameters():
                param.requires_grad = False

        # 删除多余的全连接层参数,以节省内存。
        # 去掉vgg最后的全连接层(classifier)
        if remove_fc:
            del self.classifier

        if show_params:
            for name, param in self.named_parameters():
                print(name, param.size())

    def forward(self, x):
        output = {}  # 得到每个最大池化层的输出,VGG网络有5个最大池化层。
        for idx, (begin, end) in enumerate(self.ranges):
            # self.ranges = ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)) (vgg16 examples)
            for layer in range(begin, end):
                x = self.features[layer](x)
            output["x%d" % (idx + 1)] = x
        return output


ranges = {
    'vgg11': ((0, 3), (3, 6), (6, 11), (11, 16), (16, 21)),
    'vgg13': ((0, 5), (5, 10), (10, 15), (15, 20), (20, 25)),
    'vgg16': ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)),
    'vgg19': ((0, 5), (5, 10), (10, 19), (19, 28), (28, 37))
}

# Vgg网络结构配置
cfg = {
    'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}


# make layers using Vgg-Net config(cfg)
# 由cfg构建vgg-Net
def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3
    for v in cfg:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    return nn.Sequential(*layers)


if __name__ == "__main__":
    pass

训练

'''
train.py
'''
from datetime import datetime
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from dataset import test_dataloader, train_dataloader
from model import FCNs, VGGNet

# 我将FCN文件名改成了model,所以是from model import...
# 同样将BagData文件名改成了dataset,所以是from dataset import...


"""
定义一个device变量,用于确定使用CPU还是GPU进行训练。
如果该变量的值为“cuda”,则表示使用GPU进行训练
如果该变量的值为“cpu”,则表示使用CPU进行训练
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')


device=torch.device("cpu")      # 使用cpu
device=torch.device("cuda")     # 使用GPU


"""
定义一个名为train()的函数,用于执行模型的训练过程。
该函数包含epo_num和show_vgg_params,分别表示训练的epoch数量和是否显示VGG16的参数信息。
函数内部,首先创建一个VGG16的实例vgg_model加载到指定设备上(即之前定义的device变量),
并且定义交叉熵损失函数和SGD优化器。
"""
def train(epo_num=50, show_vgg_params=False): # epoch_num表示训练的epoch数目,默认值为50
                                              # show_vgg_params表示是否显示VGG模型的参数
    # vis = visdom.Visdom()   #pytorch中的可视化工具

    # 创建VGGNet和FCNs模型并赋值给vgg_model和fcn_model
    vgg_model = VGGNet(requires_grad=True, show_params=show_vgg_params) # requires_grad=True表示模型的参数需要计算梯度用于反向传播
                                                                        # show_params=show_vgg_params表示是否需要打印VGGNet模型的参数信息
                                                                        # 当show_vgg_params=Ture时,会在控制台上打印VGGNet模型的参数信息
    fcn_model = FCNs(pretrained_net=vgg_model, n_class=2)               # pretrained_net=vgg_model表示使用预训练的VGGNet模型作为FCNs模型的预训练网络
                                                                        # n_class=2表示分类的类别数目为2(可能是黑白色二分类)

    # 将模型加载到指定设备上
    fcn_model = fcn_model.to(device)
    criterion = nn.BCELoss().to(device)
    optimizer = optim.SGD(fcn_model.parameters(), lr=1e-2, momentum=0.7)

    # 定义两个空列表all_train_iter_loss和all_test_iter_loss用于存储训练接和测试集的损失值
    all_train_iter_loss = []
    all_test_iter_loss = []

    # 计算时间
    prev_time = datetime.now()

    """
    使用两个for循环,分别对训练数据和测试数据进行迭代,
    在迭代中,
    首先将数据和标签加载到指定设备上,
    然后将优化器的梯度清零,并对网络输出和标签进行前向传播和损失计算。
    在计算损失的过程中,代码还将网络输出进行了sigmoid操作,从而将输出值缩放到了0到1之间。
    在计算损失的过程之后,代码还将损失值记录到all_train_iter_loss或all_test_loss中
    并将损失值累加到total_loss中。
    最后,通过反向传播和优化器的更新,更新网络参数。
    """
    for epo in range(epo_num): # 循环遍历每个epoch的训练过程,epoch_num表示总的训练epoch数
        train_loss = 0
        fcn_model.train() # 将模式设置为训练模式,这是因为在训练模式下,模型会启用dropout等正则化
        # 循环遍历数据集中的每个batch
        for index, (bag, bag_msk) in enumerate(train_dataloader): # train_dataloader是一个Pytorch数据加载器,
                                                                  # 可以将训练数据集分成多个batch,并自动对每个batch进行shuffle和数据增强等操作
            # bag.shape is torch.Size([4, 3, 160, 160])
            # bag_msk.shape is torch.Size([4, 2, 160, 160])
            bag = bag.to(device)
            bag_msk = bag_msk.to(device)

            optimizer.zero_grad() # 将优化器的梯度缓存清零,以便计算新的梯度
            output = fcn_model(bag) # 使用FCNs模型对输入数据进行前向传播,得到模型的输出。
            output = torch.sigmoid(output)  # output.shape is torch.Size([4, 2, 160, 160])
                                            # 将模型输出应用sigmoid函数,将输出值缩放到0到1之间,为了适应交叉熵损失函数的要求。
            # print(output)
            # print(bag_msk)
            loss = criterion(output, bag_msk) # 根据模型输出的标签数据计算损失函数值,这里使用的是交叉熵损失函数
            loss.backward() # 对损失函数进行反向传播,计算参数的梯度
            iter_loss = loss.item()
            all_train_iter_loss.append(iter_loss) # 将当前batch的损失函数值添加到all_train_iter_loss列表中
            train_loss += iter_loss # 累加每个batch的损失函数值,以便最后计算每个epoch的平均损失函数值
            optimizer.step() # 使用优化器更新模型参数

            output_np = output.cpu().detach().numpy().copy()  # output_np.shape = (4, 2, 160, 160);将模型输出转移到CPU上,并转化为numpy数组
            output_np = np.argmin(output_np, axis=1) # 对输出数组沿着第一个轴求最小值的索引,得到二分类预测结果
            bag_msk_np = bag_msk.cpu().detach().numpy().copy()  # bag_msk_np.shape = (4, 2, 160, 160);将标签数据移动到CPU上,并转换为numpy数组
            bag_msk_np = np.argmin(bag_msk_np, axis=1) # 对标签数组沿着第一个轴求最小值的索引,得到二分类结果

        test_loss = 0
        """
        fcn_model.eval()是将模型设置为评估模式,这个方法通常用于测试集上进行模型推理。
        在评估模式下,模型将会关闭一些具有随机性的操作,例如dropout、batch normalization等,以避免对模型推理的结果产生影响。
        此外,在评估模式下,模型的权重参数也不会被更新,因此可以减少内存的使用,提高模型推理的速度
        """
        fcn_model.eval() # 将模式设置为评估模式
        with torch.no_grad():
            for index, (bag, bag_msk) in enumerate(test_dataloader):
                bag = bag.to(device)
                bag_msk = bag_msk.to(device)
                optimizer.zero_grad()
                output = fcn_model(bag)
                output = torch.sigmoid(output)  # output.shape is torch.Size([4, 2, 160, 160])
                loss = criterion(output, bag_msk)  # 预测和原标签图的差
                iter_loss = loss.item()  # item得到一个元素张量里面的元素值,一般用于返回loss,acc
                all_test_iter_loss.append(iter_loss)
                test_loss += iter_loss

                output_np = output.cpu().detach().numpy().copy()  # output_np.shape = (4, 2, 160, 160)
                output_np = np.argmin(output_np, axis=1)
                bag_msk_np = bag_msk.cpu().detach().numpy().copy()  # bag_msk_np.shape = (4, 2, 160, 160)
                bag_msk_np = np.argmin(bag_msk_np, axis=1)

        cur_time = datetime.now()
        h, remainder = divmod((cur_time - prev_time).seconds, 3600)
        m, s = divmod(remainder, 60)
        time_str = "Time %02d:%02d:%02d" % (h, m, s)
        prev_time = cur_time

        print('epoch train loss = %f, epoch test loss = %f, %s'
              % (train_loss / len(train_dataloader), test_loss / len(test_dataloader), time_str))

        """
        这段代码是训练过程中的一个保存模型的操作。
        具体来说,代码中使用了python中的取模运算符%(这里使用了np.mod函数)判断epoch是否为5的倍数,
        如果是,则将当前模型保存到文件中。文件名为“fun_model_{}.pt”,其中{}表示epoch的编号,
        因此每5个epoch保存一次模型,文件名的数字会对应增加
        
        备注:模型的保存可以在训练过程中定期执行,以便在训练中断或出现错误时,可以从已保存的模型中恢复训练
        从而不需要从头训练。此外,也可以使用已保存的模型来进行模型的推理或部署到生产环境中。
        保存模型的方法在Pytorch中有多种,包括使用torch.save()函数保存整个模型或仅保存模型的状态字典(state_dict)
        """
        if np.mod(epo+1, 5) == 0:
            torch.save(fcn_model, './fcn_model_{}.pt'.format(epo))
            print('fcn_model_{}.pt'.format(epo))


if __name__ == "__main__":
    train(epo_num=20, show_vgg_params=False)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值