• 使用Pytorch快速训练ResNet网络模型


    写在最前面:
    本次博客不涉及模型原理的解释,可以看作是一个纯工程性的一次实验。之前看了很多论文模型中的代码,我只是不求甚解,把大概的流程理解了就放下了。本次实验就是为了仔细的体会其中的细节。

    大家都知道,pytorch已经将底层的代码封装的很好的,我们只需要写很少的代码就能跑一个模型。所以本次实验还有一个目的,让写的代码尽量能够复用。

    1. SVHN数据集

    在实验开始之前的第一步,就是选取数据集。我之前看到顶会论文中很多使用的是这个数据集,在这里我们也跟风一下,想要下载的小伙伴可以点击这里。这个数据集是一个关于数字彩色图像设别的数据集,可以理解为更加复杂的Mnist数据集。给大家展示一下它的复杂度。有些样本我都看不清楚,真不知道大佬些是怎么干到90+的,可怕!
    在这里插入图片描述

    2. Dataset与DataLoader

    这两个类是将数据集加载过程与预处理过程封装,让上层忽略底层实现细节。
    Dataset:

    import scipy.io as sio
    from torch.utils.data import Dataset
    from torch.utils.data.dataset import T_co
    
    
    class SVHN(Dataset):
        def __init__(self, file_path) -> None:
            super().__init__()
            self.file_path = file_path
            data_mat = sio.loadmat(self.file_path)
            self.X = data_mat["X"]
            self.y = data_mat["y"]
    
        def __getitem__(self, index) -> T_co:
            return self.X[:, :, :, index], self.y[index]
    
        def __len__(self):
            return self.y.shape[0]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    值得注意的是,我们需要重写父类Dataset的两个方法,__getitem____len____getitem__方法就是返回一个训练样本与标签, __len__方法是返回数据集的长度。

    DataLoader:

    dataLoader = DataLoader(dataset, batch_size=batchSize, shuffle=True)
    
    • 1

    有的同学看到这儿就会问了,Dataset不是已经有返回数据的接口了吗?为什么还有包一层DataLoader呢?原因就是在网络训练的过程中,样本不是一个一个输入的,而是一个Batch一个Batch的输入。这里的Batch可以理解为是一个训练样本的集合(多个样本打包在一起)。DataLoader还有很多可选的参数,在这里就不详细介绍了,感兴趣的同学可以去查阅pytoch的API文档

    3. ResNet Model

    在这里就不自己写模型结构了,pytorch有官方的实现,我们这里偷一下懒。

    from torchvision import models
    
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    resnet18 = models.resnet18()
    # 修改全连接层的输出
    num_ftrs = resnet18.fc.in_features
    # 十分类,将输出层修改成10
    resnet18.fc = nn.Linear(num_ftrs, 10)
    # 模型参数放大GPU上,加快训练速度
    resnet18 = resnet18.to(device)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4. 训练

    这部分其实才是本次主要的工作量。这其中充斥着大量的模板代码,几乎每个模型都会用上。这部分主要是计算损失,反向传播,优化器。其中优化器就优化反向传播的。比较无奈的是,这部分也已经有实现了,直接用就是了,非常的方便。

    def train(model, dataLoader, optimizer, lossFunc, n_epoch):
        start_time = time.time()
        test_best_loss = float('inf')
        last_improve = 0  # 记录上次验证集loss下降的batch数
        flag = False  # 记录是否很久没有效果提升
        total_batch = 0  # 记录进行到多少batch
        writer = SummaryWriter(log_dir=log_path + '/' + time.strftime('%m-%d_%H.%M', time.localtime()))
        for epoch in range(n_epoch):
            print('Epoch [{}/{}]'.format(epoch + 1, n_epoch))
            model.train()
            sum_loss = 0.0
            correct = 0.0
            total = 0.0
            for batch_idx, dataset in enumerate(dataLoader):
                length = len(dataLoader)
                optimizer.zero_grad()
                data, labelOrg = dataset
                data = data.to(device)
                label = F.one_hot(labelOrg.to(torch.long), 10).to(torch.float).to(device)
                predict = model(data)
                loss = lossFunc(predict, label)
                loss.backward()
                optimizer.step()
                # Tensor.item() 类型转换,返回一个数
                sum_loss += loss.item()
                # maxIdx, maxVal = torch.max
                _, predicted = torch.max(predict.data, dim=1)
                total += label.size(0)
                correct += predicted.cpu().eq(labelOrg.data).sum()
                # 注意这里是以一个batch为一个单位
                print("[epoch:%d, iter:%d] Loss: %.03f | Acc: %.3f%% "
                      % (epoch + 1, (batch_idx + 1 + epoch * length), sum_loss / (batch_idx + 1), 100. * correct / total))
                # 每一百个batch计算模型再测试集或者验证集的正确率
                if total_batch % 100 == 0:
                    testDataLoss, testDataAcc = evalTestAcc(model)
                    time_dif = get_time_dif(start_time)
                    if testDataLoss < test_best_loss:
                        test_best_loss = testDataLoss
                        torch.save(model.state_dict(), save_path)
                        improve = '*'
                        last_improve = total_batch
                    else:
                        improve = ''
                    msg = 'Iter: {0:>6},  Train Loss: {1:>5.2},  Train Acc: {2:>6.2%},  Test Loss: {3:>5.2},  Test Acc: {4:>6.2%},  Time: {5} {6}'
                    print(msg.format(total_batch, sum_loss / (batch_idx + 1), correct / total, testDataLoss, testDataAcc, time_dif, improve))
                    writer.add_scalar("loss/train", loss.item(), total_batch)
                    writer.add_scalar("loss/dev", testDataLoss, total_batch)
                    writer.add_scalar("acc/train", correct / total, total_batch)
                    writer.add_scalar("acc/dev", testDataAcc, total_batch)
                # 提供训练程序的两个出口: n_epoch, require_improvement个batch没有提升
                total_batch += 1
                model.train()
                if total_batch - last_improve > require_improvement:
                    # 验证集loss超过1000batch没下降,结束训练
                    print("No optimization for a long time, auto-stopping...")
                    flag = True
                    break
            if flag:
                break
        writer.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    def evalTestAcc(net):
        net.eval()
        totalAcc = 0.0
        sumLoss = 0.0
        total = 0.0
        with torch.no_grad():
            for idx, dataset in enumerate(testDataLoader):
                data, labelOrg = dataset
                predict = net(data.to(device))
                _, predicted = torch.max(predict.data, dim=1)
                totalAcc += predicted.cpu().eq(labelOrg).sum()
                label = F.one_hot(labelOrg.to(torch.long), 10).to(torch.float).to(device)
                sumLoss += lossFunc(predict, label).item()
                total += label.size(0)
        return sumLoss / len(testDataLoader), totalAcc / total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    看了一下,感觉没什么讲的,几乎都是模板代码,放在任何一个模型中都可以使用。值得注意的是,在本次实验中没有区分测试集与验证集,可以理解为没有测试集,实验中的testDataset被用作是验证集,调整训练参数了。

    5. 调用

    if __name__ == '__main__':
        # filePath = r"E:\dataset\SVHN\train_32x32.mat"
        save_path = r"model_save/net.pt"
        log_path = r"logs"
        require_improvement = 1000
        batchSize = 256
        n_epoch = 10
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        resnet18 = models.resnet18()
        # 修改全连接层的输出
        num_ftrs = resnet18.fc.in_features
        resnet18.fc = nn.Linear(num_ftrs, 10)
        resnet18 = resnet18.to(device)
        # SVHNTrainData = SVHN(filePath)
        train_dataset = torchvision.datasets.SVHN(
            root=r'E:\dataset\SVHN',
            split='train',
            download=False,
            transform=torchvision.transforms.ToTensor()
        )
    
        test_dataset = torchvision.datasets.SVHN(
            root=r'E:\dataset\SVHN',
            split='test',
            download=False,
            transform=torchvision.transforms.ToTensor()
        )
        dataLoader = DataLoader(train_dataset, batch_size=batchSize, shuffle=True)
        testDataLoader = DataLoader(test_dataset, batch_size=batchSize, shuffle=True)
        optimizer = optim.SGD(resnet18.parameters(), lr=0.01, momentum=0.9)
        lossFunc = nn.CrossEntropyLoss()
        train(resnet18, dataLoader, optimizer, lossFunc, n_epoch)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这里把所有的内容串起来了。在运行完成后,在当前目录会产生于一个logs文件夹, 大家可以运行tensorboard --logdir 文件夹地址,就可以看到如下图所示,记录训练过程中,损失与准确率在测试集与验证集上的变化曲线。
    在这里插入图片描述

    6. 序列化与反序列化

    序列化与反序列化,我们可以理解为保存于加载。我们的模型训练好之后,就可以直接进行预测任务,这时候就不会在反向传播更新模型参数了。
    参考,这篇博客讲得太清楚了,几乎包括了所有的内容,我都不想在讲了。我这里就记录一下我的反序列化过程吧。

    import random
    
    import numpy as np
    import torch
    import torchvision
    from matplotlib import pyplot as plt
    from torch import nn
    from torch.utils.data import DataLoader
    from torchvision import models
    
    if __name__ == '__main__':
        path = r"model_save/net.pt"
        batchSize = 256
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        resnet18 = models.resnet18()
        # 修改全连接层的输出
        num_ftrs = resnet18.fc.in_features
        resnet18.fc = nn.Linear(num_ftrs, 10)
        # resnet18 = resnet18.to(device)
        resnet18.load_state_dict(torch.load(path, map_location=torch.device("cpu")))
        resnet18.eval()
        test_dataset = torchvision.datasets.SVHN(
            root=r'E:\dataset\SVHN',
            split='test',
            download=False,
            transform=torchvision.transforms.ToTensor()
        )
        testDataLoader = DataLoader(test_dataset, batch_size=batchSize, shuffle=True)
        trains, labels = iter(testDataLoader).__next__()
        predicts = resnet18(trains)
        # 其实可以只用预测一个样本,而不是一个batch
        # resnet18(trains[0].unsqueeze(0))
        _, predictLabels = torch.max(predicts, dim=1)
        fig, axs = plt.subplots(1, 5, figsize=(10, 10))  # 建立子图
        print("predictLabels: {}".format(predictLabels))
        print("labels: {}".format(labels))
        print("Acc: {:.2f}".format(predictLabels.data.eq(labels).sum() / labels.shape[0]))
        for i in range(5):
            num = random.randint(0, batchSize)  # 首先选取随机数,随机选取五次
            npimg, nplabel = trains[num], labels[num]
            axs[i].imshow(np.transpose(npimg, (1, 2, 0)))
            axs[i].set_title("GroundTruth: {}, Predict: {}".format(nplabel, predictLabels[num]))  # 给每个子图加上标签
            axs[i].axis("off")  # 消除每个子图的坐标轴
        plt.show()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    在这里插入图片描述

  • 相关阅读:
    docker基础篇:安装tomcat
    两万字长文世界编程语言大串讲
    使用Node.js开发一个文件上传功能
    【redis】springboot 用redis stream实现MQ消息队列 考虑异常ack重试场景
    4.9每日一题(多元抽象复合函数求二阶偏导)
    VBA技术资料MF70:从单元格文本中取消或删除上标
    java计算机毕业设计贵州农产品交易系统源码+mysql数据库+系统+lw文档+部署
    springboot框架中生成一个md5文件校验类,md5文件校验类必须包括传入的一个key值秘钥,还有上传内容是byte[]类型
    【数据结构与算法】第七篇:集合,映射
    break、continue、return中选择一个,我们结束掉它
  • 原文地址:https://blog.csdn.net/ssjq123/article/details/126042804