美文网首页temp深度学习框架PyTorch入门与实践 陈云
深度学习框架PyTorch入门与实践:第六章 PyTorch实战

深度学习框架PyTorch入门与实践:第六章 PyTorch实战

作者: 金字塔下的小蜗牛 | 来源:发表于2019-10-02 22:26 被阅读0次

    通过前面几章的学习,我们已经掌握了PyTorch中大部分的基础知识,本章将结合之前讲的内容,带领读者从头实现一个完整的深度学习项目。本章的重点不在于如何使用PyTorch的接口,而在于合理地设计程序的结构,使得程序更具可读性、更易用。

    6.1 编程实战:猫和狗二分类

    在学习某个深度学习框架时,掌握其基本知识和接口固然重要,但如何合理组织代码,使得代码具有良好的可读性和可扩展性也必不可少。本文不会深入讲解过多知识性的东西,更多的则是传授一些经验,这些内容可能有些争议,因其受我个人喜好和coding风格影响较大,读者可以将这部分当成是一种参考或提议,而不是作为必须遵循的准则。归根到底,都是希望你能以一种更为合理的方式组织自己的程序。

    在做深度学习实验或项目时,为了得到最优的模型结果,中间往往需要很多次的尝试和修改。而合理的文件组织结构,以及一些小技巧可以极大地提高代码的易读易用性。根据笔者的个人经验,在从事大多数深度学习研究时,程序都需要实现以下几个功能:

    • 模型定义
    • 数据处理和加载
    • 训练模型(Train&Validate)
    • 训练过程的可视化
    • 测试(Test/Inference)

    另外程序还应该满足以下几个要求:

    • 模型需具有高度可配置性,便于修改参数、修改模型,反复实验。
    • 代码应具有良好的组织结构,使人一目了然。
    • 代码应具有良好的说明,使其他人能够理解。

    在之前的章节中,我们已经讲解了PyTorch中的绝大部分内容。本章我们将应用这些内容,并结合实际的例子,来讲解如何用PyTorch完成Kaggle上的经典比赛:Dogs vs. Cats。本文所有示例程序均在github上开源 。

    6.1.1 比赛介绍

    Dogs vs. Cats是一个传统的二分类问题,其训练集包含25000张图片,均放置在同一文件夹下,命名格式为<category>.<num>.jpg, 如cat.10000.jpgdog.100.jpg,测试集包含12500张图片,命名为<num>.jpg,如1000.jpg。参赛者需根据训练集的图片训练模型,并在测试集上进行预测,输出它是狗的概率。最后提交的csv文件如下,第一列是图片的<num>,第二列是图片为狗的概率。

    id,label
    10001,0.889
    10002,0.01
    ...

    image.png
    6.1.2 文件组织架构

    前面提到过,程序主要包含以下功能:

    • 模型定义
    • 数据加载
    • 训练和测试

    首先来看程序文件的组织结构:

    ├── checkpoints/
    ├── data/
    │   ├── __init__.py
    │   ├── dataset.py
    │   └── get_data.sh
    ├── models/
    │   ├── __init__.py
    │   ├── AlexNet.py
    │   ├── BasicModule.py
    │   └── ResNet34.py
    └── utils/
    │   ├── __init__.py
    │   └── visualize.py
    ├── config.py
    ├── main.py
    ├── requirements.txt
    ├── README.md
    

    其中:

    • checkpoints/: 用于保存训练好的模型,可使程序在异常退出后仍能重新载入模型,恢复训练。
    • data/:数据相关操作,包括数据预处理、dataset实现等。
    • models/:模型定义,可以有多个模型,例如上面的AlexNet和ResNet34,一个模型对应一个文件。
    • utils/:可能用到的工具函数,在本次实验中主要是封装了可视化工具。
    • config.py:配置文件,所有可配置的变量都集中在此,并提供默认值。
    • main.py:主文件,训练和测试程序的入口,可通过不同的命令来指定不同的操作和参数。
    • requirements.txt:程序依赖的第三方库。
    • README.md:提供程序的必要说明。
    6.1.3 关于init.py

    可以看到,几乎每个文件夹下都有__init__.py,一个目录如果包含了__init__.py 文件,那么它就变成了一个包(package)。__init__.py可以为空,也可以定义包的属性和方法,但其必须存在,其它程序才能从这个目录中导入相应的模块或函数。例如在data/文件夹下有__init__.py,则在main.py 中就可以from data.dataset import DogCat。而如果在__init__.py中写入from .dataset import DogCat,则在main.py中就可以直接写为:from data import DogCat,或者import data; dataset = data.DogCat,相比于from data.dataset import DogCat更加便捷。

    6.1.4 数据加载

    数据的相关处理主要保存在data/dataset.py中。关于数据加载的相关操作,在上一章中我们已经提到过,其基本原理就是使用Dataset提供数据集的封装,再使用Dataloader实现数据并行加载。Kaggle提供的数据包括训练集和测试集,而我们在实际使用中,还需专门从训练集中取出一部分作为验证集。对于这三类数据集,其相应操作也不太一样,而如果专门写三个Dataset,则稍显复杂和冗余,因此这里通过加一些判断来区分。对于训练集,我们希望做一些数据增强处理,如随机裁剪、随机翻转、加噪声等,而验证集和测试集则不需要。下面看dataset.py的代码:

    # coding:utf8
    import os
    
    from PIL import Image
    from torch.utils import data
    from torchvision import transforms as T
    
    
    class DogCat(data.Dataset):
    
        def __init__(self, root, transforms=None, train=True, test=False):
            """
            主要目标: 获取所有图片的地址,并根据训练,验证,测试划分数据
            """
            self.test = test
            imgs = [os.path.join(root, img) for img in os.listdir(root)]
    
            # test1: data/test1/8973.jpg
            # train: data/train/cat.10004.jpg 
            if self.test:
                imgs = sorted(imgs, key=lambda x: int(x.split('.')[-2].split('/')[-1]))
            else:
                imgs = sorted(imgs, key=lambda x: int(x.split('.')[-2]))
    
            imgs_num = len(imgs)
    
            if self.test:
                self.imgs = imgs
            elif train:
                self.imgs = imgs[:int(0.7 * imgs_num)]
            else:
                self.imgs = imgs[int(0.7 * imgs_num):]
    
            if transforms is None:
                normalize = T.Normalize(mean=[0.485, 0.456, 0.406],
                                        std=[0.229, 0.224, 0.225])
    
                if self.test or not train:
                    self.transforms = T.Compose([
                        T.Resize(224),
                        T.CenterCrop(224),
                        T.ToTensor(),
                        normalize
                    ])
                else:
                    self.transforms = T.Compose([
                        T.Resize(256),
                        T.RandomCrop(224),
                        T.RandomHorizontalFlip(),
                        T.ToTensor(),
                        normalize
                    ])
    
        def __getitem__(self, index):
            """
            一次返回一张图片的数据
            """
            img_path = self.imgs[index]
            if self.test:
                label = int(self.imgs[index].split('.')[-2].split('/')[-1])
            else:
                label = 1 if 'dog' in img_path.split('/')[-1] else 0
            data = Image.open(img_path)
            data = self.transforms(data)
            return data, label
    
        def __len__(self):
            return len(self.imgs)
    
    

    关于数据集使用的注意事项,在上一章中已经提到,将文件读取等费时操作放在__getitem__函数中,利用多进程加速。避免一次性将所有图片都读进内存,不仅费时也会占用较大内存,而且不易进行数据增强等操作。另外在这里,我们将训练集中的30%作为验证集,可用来检查模型的训练效果,避免过拟合。在使用时,我们可通过dataloader加载数据。

    train_dataset = DogCat(opt.train_data_root, train=True)
    trainloader = DataLoader(train_dataset,
                            batch_size = opt.batch_size,
                            shuffle = True,
                            num_workers = opt.num_workers)
                      
    for ii, (data, label) in enumerate(trainloader):
        train()
    
    6.1.5 模型定义

    模型的定义主要保存在models/目录下,其中BasicModule是对nn.Module的简易封装,提供快速加载和保存模型的接口。

    # coding:utf8
    import time
    
    import torch as t
    
    
    class BasicModule(t.nn.Module):
        """
        封装了nn.Module,主要是提供了save和load两个方法
        """
    
        def __init__(self):
            super(BasicModule, self).__init__()
            self.model_name = str(type(self))  # 默认名字
    
        def load(self, path):
            """
            可加载指定路径的模型
            """
            self.load_state_dict(t.load(path))
    
        def save(self, name=None):
            """
            保存模型,默认使用“模型名字+时间”作为文件名
            """
            if name is None:
                prefix = 'checkpoints/' + self.model_name + '_'
                name = time.strftime(prefix + '%Y%m%d%H%M%S.pth')
            t.save(self.state_dict(), name)
            return name
    
        def get_optimizer(self, lr, weight_decay):
            return t.optim.Adam(self.parameters(), lr=lr, weight_decay=weight_decay)
    
    
    class Flat(t.nn.Module):
        """
        把输入reshape成(batch_size,dim_length)
        """
    
        def __init__(self):
            super(Flat, self).__init__()
            # self.size = size
    
        def forward(self, x):
            return x.view(x.size(0), -1)
    
    

    在实际使用中,直接调用model.save()model.load(opt.load_path)即可。

    其它自定义模型一般继承BasicModule,然后实现自己的模型。其中AlexNet.py实现了AlexNet,ResNet34实现了ResNet34。在models/__init__py中,代码如下:

    from .AlexNet import AlexNet
    from .ResNet34 import ResNet34
    

    这样在主函数中就可以写成:

    from models import AlexNet
    或
    import models
    model = models.AlexNet()
    或
    import models
    model = getattr('models', 'AlexNet')()
    

    其中最后一种写法最为关键,这意味着我们可以通过字符串直接指定使用的模型,而不必使用判断语句,也不必在每次新增加模型后都修改代码。新增模型后只需要在models/__init__.py中加上from .new_module import new_module即可。

    其它关于模型定义的注意事项,在上一章中已详细讲解,这里就不再赘述,总结起来就是:

    • 尽量使用nn.Sequential(比如AlexNet)。
    • 将经常使用的结构封装成子Module(比如GoogLeNet的Inception结构,ResNet的Residual Block结构)。
    • 将重复且有规律性的结构,用函数生成(比如VGG的多种变体,ResNet多种变体都是由多个重复卷积层组成)。
    6.1.6 工具函数

    在项目中,我们可能会用到一些helper方法,这些方法可以统一放在utils/文件夹下,需要使用时再引入。在本例中主要是封装了可视化工具visdom的一些操作,其代码如下,在本次实验中只会用到plot方法,用来统计损失信息。

    # coding:utf8
    import time
    
    import numpy as np
    import visdom
    
    
    class Visualizer(object):
        """
        封装了visdom的基本操作,但是你仍然可以通过`self.vis.function`
        调用原生的visdom接口
        """
    
        def __init__(self, env='default', **kwargs):
            self.vis = visdom.Visdom(env=env, use_incoming_socket=False, **kwargs)
    
            # 画的第几个数,相当于横座标
            # 保存(’loss',23) 即loss的第23个点
            self.index = {}
            self.log_text = ''
    
        def reinit(self, env='default', **kwargs):
            """
            修改visdom的配置
            """
            self.vis = visdom.Visdom(env=env, **kwargs)
            return self
    
        def plot_many(self, d):
            """
            一次plot多个
            @params d: dict (name,value) i.e. ('loss',0.11)
            """
            for k, v in d.items():
                self.plot(k, v)
    
        def img_many(self, d):
            for k, v in d.items():
                self.img(k, v)
    
        def plot(self, name, y, **kwargs):
            """
            self.plot('loss',1.00)
            """
            x = self.index.get(name, 0)
            self.vis.line(Y=np.array([y]), X=np.array([x]),
                          win=name,
                          opts=dict(title=name),
                          update=None if x == 0 else 'append',
                          **kwargs
                          )
            self.index[name] = x + 1
    
        def img(self, name, img_, **kwargs):
            """
            self.img('input_img',t.Tensor(64,64))
            self.img('input_imgs',t.Tensor(3,64,64))
            self.img('input_imgs',t.Tensor(100,1,64,64))
            self.img('input_imgs',t.Tensor(100,3,64,64),nrows=10)
    
            !!!don‘t ~~self.img('input_imgs',t.Tensor(100,64,64),nrows=10)~~!!!
            """
            self.vis.images(img_.cpu().numpy(),
                            win=name,
                            opts=dict(title=name),
                            **kwargs
                            )
    
        def log(self, info, win='log_text'):
            """
            self.log({'loss':1,'lr':0.0001})
            """
    
            self.log_text += ('[{time}] {info} <br>'.format(
                time=time.strftime('%Y%m%d %H:%M:%S'),
                info=info))
            self.vis.text(self.log_text, win)
    
        def __getattr__(self, name):
            return getattr(self.vis, name)
    
    
    6.1.7 配置文件

    在模型定义、数据处理和训练等过程都有很多变量,这些变量应提供默认值,并统一放置在配置文件中,这样在后期调试、修改代码或迁移程序时会比较方便,在这里我们将所有可配置项放在config.py中。

    # coding:utf8
    import warnings
    
    import torch as t
    
    
    class DefaultConfig(object):
        env = 'default'  # visdom 环境
        vis_port = 8097  # visdom 端口
        model = 'SqueezeNet'  # 使用的模型,名字必须与models/__init__.py中的名字一致
    
        train_data_root = './data/train/'  # 训练集存放路径
        test_data_root = './data/test/'  # 测试集存放路径
        load_model_path = None  # 加载预训练的模型的路径,为None代表不加载
    
        batch_size = 32  # batch size
        use_gpu = True  # user GPU or not
        num_workers = 0  # how many workers for loading data
        print_freq = 20  # print info every N batch
    
        debug_file = './debug/debug.txt'  # if os.path.exists(debug_file): enter ipdb
        result_file = 'result.csv'
    
        max_epoch = 10
        lr = 0.001  # initial learning rate
        lr_decay = 0.5  # when val_loss increase, lr = lr*lr_decay
        weight_decay = 0e-5  # 损失函数
    
        def _parse(self, kwargs):
            """
            根据字典kwargs 更新 config参数
            """
            for k, v in kwargs.items():
                if not hasattr(self, k):
                    warnings.warn("Warning: opt has not attribut %s" % k)
                setattr(self, k, v)
    
            opt.device = t.device('cuda') if opt.use_gpu else t.device('cpu')
    
            print('user config:')
            for k, v in self.__class__.__dict__.items():
                if not k.startswith('_'):
                    print(k, getattr(self, k))
    
    
    opt = DefaultConfig()
    
    

    可配置的参数主要包括:

    • 数据集参数(文件路径、batch_size等)
    • 训练参数(学习率、训练epoch等)
    • 模型参数

    这样我们在程序中就可以这样使用:

    import models
    from config import DefaultConfig
    
    opt = DefaultConfig()
    lr = opt.lr
    model = getattr(models, opt.model)
    dataset = DogCat(opt.train_data_root)
    

    这些都只是默认参数,在这里还提供了更新函数,根据字典更新配置参数。

    def _parse(self, kwargs):
            """
            根据字典kwargs 更新 config参数
            """
            for k, v in kwargs.items():
                if not hasattr(self, k):
                    warnings.warn("Warning: opt has not attribut %s" % k)
                setattr(self, k, v)
    
            opt.device = t.device('cuda') if opt.use_gpu else t.device('cpu')
    
            print('user config:')
            for k, v in self.__class__.__dict__.items():
                if not k.startswith('_'):
                    print(k, getattr(self, k))
    

    这样我们在实际使用时,并不需要每次都修改config.py,只需要通过命令行传入所需参数,覆盖默认配置即可。

    例如:

    opt = DefaultConfig()
    new_config = {'lr':0.1,'use_gpu':False}
    opt.parse(new_config)
    opt.lr == 0.1
    
    6.1.8 main.py

    在讲解主程序main.py之前,我们先来看看2017年3月谷歌开源的一个命令行工具fire,通过pip install fire即可安装。下面来看看fire的基础用法,假设example.py文件内容如下:

    import fire
    
    def add(x, y):
      return x + y
      
    def mul(**kwargs):
        a = kwargs['a']
        b = kwargs['b']
        return a * b
    
    if __name__ == '__main__':
      fire.Fire()
    

    那么我们可以使用:

    python example.py add 1 2 # 执行add(1, 2)
    python example.py mul --a=1 --b=2 # 执行mul(a=1, b=2), kwargs={'a':1, 'b':2}
    python example.py add --x=1 --y==2 # 执行add(x=1, y=2)
    

    可见,只要在程序中运行fire.Fire(),即可使用命令行参数python file <function> [args,] {--kwargs,}。fire还支持更多的高级功能,具体请参考官方指南《The Python Fire Guide》

    在主程序main.py中,主要包含四个函数,其中三个需要命令行执行,main.py的代码组织结构如下:

    def train(**kwargs):
        """
        训练
        """
        pass
         
    def val(model, dataloader):
        """
        计算模型在验证集上的准确率等信息,用以辅助训练
        """
        pass
    
    def test(**kwargs):
        """
        测试(inference)
        """
        pass
    
    def help():
        """
        打印帮助的信息 
        """
        print('help')
    
    if __name__=='__main__':
        import fire
        fire.Fire()
    

    根据fire的使用方法,可通过python main.py <function> --args=xx的方式来执行训练或者测试。

    训练

    训练的主要步骤如下:

    • 定义网络
    • 定义数据
    • 定义损失函数和优化器
    • 计算重要指标
    • 开始训练
      • 训练网络
      • 可视化各种指标
      • 计算在验证集上的指标

    训练函数的代码如下:

    def train(**kwargs):
        opt._parse(kwargs)
        vis = Visualizer(opt.env, port=opt.vis_port)
    
        # step1: configure model
        model = getattr(models, opt.model)()
        if opt.load_model_path:
            model.load(opt.load_model_path)
        model.to(opt.device)
    
        # step2: data
        train_data = DogCat(opt.train_data_root, train=True)
        val_data = DogCat(opt.train_data_root, train=False)
        train_dataloader = DataLoader(train_data, opt.batch_size,
                                      shuffle=True, num_workers=opt.num_workers)
        val_dataloader = DataLoader(val_data, opt.batch_size,
                                    shuffle=False, num_workers=opt.num_workers)
    
        # step3: criterion and optimizer
        criterion = t.nn.CrossEntropyLoss()
        lr = opt.lr
        optimizer = model.get_optimizer(lr, opt.weight_decay)
    
        # step4: meters
        loss_meter = meter.AverageValueMeter()
        confusion_matrix = meter.ConfusionMeter(2)
        previous_loss = 1e10
    
        # train
        for epoch in range(opt.max_epoch):
            loss_meter.reset()
            confusion_matrix.reset()
            for ii, (data, label) in tqdm(enumerate(train_dataloader)):
    
                # train model 
                input = data.to(opt.device)
                target = label.to(opt.device)
    
                optimizer.zero_grad()
                score = model(input)
                loss = criterion(score, target)
                loss.backward()
                optimizer.step()
    
                # meters update and visualize
                loss_meter.add(loss.item())
                # detach 一下更安全保险
                confusion_matrix.add(score.detach(), target.detach())
    
                if (ii + 1) % opt.print_freq == 0:
                    vis.plot('loss', loss_meter.value()[0])
                    print("loss:", loss_meter.value()[0])
    
                    # 进入debug模式
                    # if os.path.exists(opt.debug_file):
                    #     import ipdb;
                    #     ipdb.set_trace()
            print("保存检查点...")
            model.save()
    
            cm_value = confusion_matrix.value()
            vis.plot('train_accuracy', 100. * (cm_value[0][0] + cm_value[1][1]) / cm_value.sum())
    
            # validate and visualize
            val_cm, val_accuracy = val(model, val_dataloader)
            vis.plot('val_accuracy', val_accuracy)
            vis.log("\tepoch:{epoch},\tlr:{lr},\tloss:{loss},\ttrain_cm:{train_cm},\tval_cm:{val_cm}\t".format(
                epoch=epoch, lr=lr, loss=loss_meter.value()[0], train_cm=str(confusion_matrix.value()),
                val_cm=str(val_cm.value())))
    
            # update learning rate
            if loss_meter.value()[0] > previous_loss:
                lr = lr * opt.lr_decay
                # 第二种降低学习率的方法:不会有moment等信息的丢失
                for param_group in optimizer.param_groups:
                    param_group['lr'] = lr
    
            previous_loss = loss_meter.value()[0]
    
    

    这里用到了PyTorchNet里面的一个工具: meter。meter提供了一些轻量级的工具,用于帮助用户快速统计训练过程中的一些指标。AverageValueMeter能够计算所有数的平均值和标准差,这里用来统计一个epoch中损失的平均值。confusionmeter用来统计分类问题中的分类情况,是一个比准确率更详细的统计指标。例如对于表格6-1,共有50张狗的图片,其中有35张被正确分类成了狗,还有15张被误判成猫;共有100张猫的图片,其中有91张被正确判为了猫,剩下9张被误判成狗。相比于准确率等统计信息,混淆矩阵更能体现分类的结果,尤其是在样本比例不均衡的情况下。

    表6-1 混淆矩阵

    样本 判为狗 判为猫
    实际是狗 35 15
    实际是猫 9 91

    PyTorchNet从TorchNet迁移而来,提供了很多有用的工具,但其目前开发和文档都还不是很完善,本书不做过多的讲解。

    验证

    验证相对来说比较简单,但要注意需将模型置于验证模式(model.eval()),验证完成后还需要将其置回为训练模式(model.train()),这两句代码会影响BatchNormDropout等层的运行模式。验证模型准确率的代码如下。

    @t.no_grad()
    def val(model, dataloader):
        """
        计算模型在验证集上的准确率等信息
        """
        model.eval()
        confusion_matrix = meter.ConfusionMeter(2)
        for ii, (val_input, label) in tqdm(enumerate(dataloader)):
            val_input = val_input.to(opt.device)
            score = model(val_input)
            confusion_matrix.add(score.detach().squeeze(), label.type(t.LongTensor))
    
        model.train()
        cm_value = confusion_matrix.value()
        accuracy = 100. * (cm_value[0][0] + cm_value[1][1]) / (cm_value.sum())
        return confusion_matrix, accuracy
    
    

    测试

    测试时,需要计算每个样本属于狗的概率,并将结果保存成csv文件。测试的代码与验证比较相似,但需要自己加载模型和数据。

    @t.no_grad()  # pytorch>=0.5
    def test(**kwargs):
        opt._parse(kwargs)
    
        # configure model
        model = getattr(models, opt.model)().eval()
        if opt.load_model_path:
            model.load(opt.load_model_path)
        model.to(opt.device)
    
        # data
        train_data = DogCat(opt.test_data_root, test=True)
        test_dataloader = DataLoader(train_data, batch_size=opt.batch_size, shuffle=False, num_workers=opt.num_workers)
        results = []
        for ii, (data, path) in tqdm(enumerate(test_dataloader)):
            input = data.to(opt.device)
            score = model(input)
            probability = t.nn.functional.softmax(score, dim=1)[:, 0].detach().tolist()
    
            batch_results = [(path_.item(), probability_) for path_, probability_ in zip(path, probability)]
    
            results += batch_results
        write_csv(results, opt.result_file)
    
        return results
    
    
    def write_csv(results, file_name):
        import csv
        with open(file_name, 'w') as f:
            writer = csv.writer(f)
            writer.writerow(['id', 'label'])
            writer.writerows(results)
    
    

    帮助函数

    为了方便他人使用, 程序中还应当提供一个帮助函数,用于说明函数是如何使用。程序的命令行接口中有众多参数,如果手动用字符串表示不仅复杂,而且后期修改config文件时,还需要修改对应的帮助信息,十分不便。这里使用了Python标准库中的inspect方法,可以自动获取config的源代码。help的代码如下:

    def help():
        """
        打印帮助的信息: python file.py help
        """
    
        print("""
        usage : python file.py <function> [--args=value]
        <function> := train | test | help
        example: 
                python {0} train --env='env0701' --lr=0.01
                python {0} test --dataset='path/to/dataset/root/'
                python {0} help
        avaiable args:""".format(__file__))
    
        from inspect import getsource
        source = (getsource(opt.__class__))
        print(source)
    

    当用户执行python main.py help的时候,会打印如下帮助信息:

        usage : python main.py <function> [--args=value,]
        <function> := train | test | help
        example: 
                python main.py train --env='env0701' --lr=0.01
                python main.py test --dataset='path/to/dataset/'
                python main.py help
        avaiable args:
    class DefaultConfig(object):
        env = 'default' # visdom 环境
        model = 'AlexNet' # 使用的模型
        
        train_data_root = './data/train/' # 训练集存放路径
        test_data_root = './data/test' # 测试集存放路径
        load_model_path = 'checkpoints/model.pth' # 加载预训练的模型
    
        batch_size = 128 # batch size
        use_gpu = True # user GPU or not
        num_workers = 4 # how many workers for loading data
        print_freq = 20 # print info every N batch
    
        debug_file = './debug/debug.txt' 
        result_file = 'result.csv' # 结果文件
          
        max_epoch = 10
        lr = 0.1 # initial learning rate
        lr_decay = 0.95 # when val_loss increase, lr = lr*lr_decay
        weight_decay = 1e-4 # 损失函数
    
    6.1.9 使用

    正如help函数的打印信息所述,可以通过命令行参数指定变量名.下面是三个使用例子,fire会将包含-的命令行参数自动转层下划线_,也会将非数值的值转成字符串。所以--train-data-root=data/train--train_data_root='data/train'是等价的。

    # 训练模型
    python main.py train 
            --train-data-root=data/train/ 
            --lr=0.005 
            --batch-size=32 
            --model='ResNet34'  
            --max-epoch = 20
    
    # 测试模型
    python main.py test
           --test-data-root=data/test 
           --load-model-path='checkpoints/resnet34_00:23:05.pth' 
           --batch-size=128 
           --model='ResNet34' 
           --num-workers=12
    
    # 打印帮助信息
    python main.py help
    

    实验过程

    本章程序及数据下载:百度网盘,提取码:aw26。

    首先,在命令行cmd红启动visdom服务器:

    python -m visdom.server
    

    然后,训练模型:

    python main.py train
    

    训练结果如下:

    image.png

    从上述结果可以看出,模型的精度可以达到97%以上。你也可以手动更改模型,通过调节参数来进一步提升模型的准确率。

    最后,测试模型:

    python main.py test
    

    第二列表示预测为狗的概率:

    image.png

    我们来看一下测试集图片:

    image.png

    可以看到,模型能够正确识别出很多狗和猫了,但是还存在很大的改进空间。

    6.1.10 争议

    以上的程序设计规范带有作者强烈的个人喜好,并不想作为一个标准,而是作为一个提议和一种参考。上述设计在很多地方还有待商榷,例如对于训练过程是否应该封装成一个trainer对象,或者直接封装到BaiscModuletrain方法之中。对命令行参数的处理也有不少值得讨论之处。因此不要将本文中的观点作为一个必须遵守的规范,而应该看作一个参考。

    本章中的设计可能会引起不少争议,其中比较值得商榷的部分主要有以下两个方面:

    • 命令行参数的设置。目前大多数程序都是使用Python标准库中的argparse来处理命令行参数,也有些使用比较轻量级的click。这种处理相对来说对命令行的支持更完备,但根据作者的经验来看,这种做法不够直观,并且代码量相对来说也较多。比如argparse,每次增加一个命令行参数,都必须写如下代码:
    parser.add_argument('-save-interval', type=int, default=500, help='how many steps to wait before saving [default:500]')
    

    在读者眼中,这种实现方式远不如一个专门的config.py来的直观和易用。尤其是对于使用Jupyter notebook或IPython等交互式调试的用户来说,argparse较难使用。

    • 模型训练。有不少人喜欢将模型的训练过程集成于模型的定义之中,代码结构如下所示:
      class MyModel(nn.Module):
        
          def __init__(self,opt):
              self.dataloader = Dataloader(opt)
              self.optimizer  = optim.Adam(self.parameters(),lr=0.001)
              self.lr = opt.lr
              self.model = make_model()
          
          def forward(self,input):
              pass
          
          def train_(self):
              # 训练模型
              for epoch in range(opt.max_epoch)
                for ii,data in enumerate(self.dataloader):
                    train_epoch()
                  
                model.save()
        
          def train_epoch(self):
              pass
    

    抑或是专门设计一个Trainer对象,形如:

        """
      code simplified from:
      https://github.com/pytorch/pytorch/blob/master/torch/utils/trainer/trainer.py
      """
      import heapq
      from torch.autograd import Variable
    
      class Trainer(object):
    
          def __init__(self, model=None, criterion=None, optimizer=None, dataset=None):
              self.model = model
              self.criterion = criterion
              self.optimizer = optimizer
              self.dataset = dataset
              self.iterations = 0
    
          def run(self, epochs=1):
              for i in range(1, epochs + 1):
                  self.train()
    
          def train(self):
              for i, data in enumerate(self.dataset, self.iterations + 1):
                  batch_input, batch_target = data
                  self.call_plugins('batch', i, batch_input, batch_target)
                  input_var = Variable(batch_input)
                  target_var = Variable(batch_target)
        
                  plugin_data = [None, None]
        
                  def closure():
                      batch_output = self.model(input_var)
                      loss = self.criterion(batch_output, target_var)
                      loss.backward()
                      if plugin_data[0] is None:
                          plugin_data[0] = batch_output.data
                          plugin_data[1] = loss.data
                      return loss
        
                  self.optimizer.zero_grad()
                  self.optimizer.step(closure)
        
              self.iterations += i
    

    还有一些人喜欢模仿keras和scikit-learn的设计,设计一个fit接口。对读者来说,这些处理方式很难说哪个更好或更差,找到最适合自己的方法才是最好的。

    BasicModule 的封装,可多可少。训练过程中的很多操作都可以移到BasicModule之中,比如get_optimizer方法用来获取优化器,比如train_step用来执行单歩训练。对于不同的模型,如果对应的优化器定义不一样,或者是训练方法不一样,可以复写这些函数自定义相应的方法,取决于自己的喜好和项目的实际需求。

    6.2 PyTorch Debug指南

    6.2.1 ipdb介绍

    很多初学者用print或log调试程序,这在小规模的程序下很方便。但是更好的调试方法是一边运行一边检查里面的变量和方法。pdb是一个交互式的调试工具,集成于Python的标准库之中,由于其强大的功能,被广泛应用于Python环境中。pdb能让你根据需求跳转到任意的Python代码断点、查看任意变量、单步执行代码,甚至还能修改代码的值,而不必重启程序。ipdb是一个增强版的pdb,可通过pip install ipdb安装。ipdb提供了调试模式下的代码补全,还具有更好的语法高亮和代码溯源,以及更好的内省功能,更关键的是,它与pdb接口完全兼容。

    在本书第2章曾粗略地提到过ipdb的基本使用,本章将继续介绍如何结合PyTorch和ipdb进行调试。首先看一个例子,要是用ipdb,只需在想要进行调试的地方插入ipdb.set_trace(),当代码运行到此处时,就会自动进入交互式调试模式。

    假设有如下程序:

    try:
        import ipdb
    except:
        import pdb as ipdb
    
    def sum(x):
        r = 0
        for ii in x:
            r += ii
        return r
        
    def mul(x):
        r = 1
        for ii in x:
            r *= ii
        return r
        
    ipdf.set_trace()
    x = [1,2,3,4,5]
    r = sum(x)
    r = mul(x)
    

    当程序运行至ipdb.set_trace(),会自动进入debug模式,在该模式中,我们可使用调试命令,如next或缩写n单步执行,也可查看Python变量,或是运行Python代码。如果Python变量名和调式命令冲突,需要在变量名前加"!",这样ipdb会执行对应的Python代码,而不是调试命令。下面举例说明ipdb的调试,这里重点讲解ipdb的两大功能。

    • 查看:在函数调用堆栈中自由跳转,并查看函数的局部变量
    • 修改:修改程序中的变量,并能以此影响程序的运行结果。
    > e:\debug.py(19)<module>()
         18 ipdb.set_trace()
    ---> 19 x = [1,2,3,4,5]
         20 r = sum(x)
    
    ipdb> l 1,21    # list 1,21的缩写,查看第1行到第21行的代码,光标所指的这一行尚未运行
          1 try:
          2         import ipdb
          3 except:
          4         import pdb as ipdb
          5
          6 def sum(x):
          7         r = 0
          8         for ii in x:
          9                 r += ii
         10         return r
         11
         12 def mul(x):
         13         r = 1
         14         for ii in x:
         15                 r *= ii
         16         return r
         17
         18 ipdb.set_trace()
    ---> 19 x = [1,2,3,4,5]
         20 r = sum(x)
         21 r = mul(x)
    
    ipdb> n    # next的缩写,执行下一步
    > e:\debug.py(20)<module>()
         19 x = [1,2,3,4,5]
    ---> 20 r = sum(x)
         21 r = mul(x)
    
    ipdb> s    # step的缩写,进入sum函数内部
    --Call--
    > e:\debug.py(6)sum()
          5
    ----> 6 def sum(x):
          7         r = 0
    
    ipdb> n    # next单步执行
    > e:\debug.py(7)sum()
          6 def sum(x):
    ----> 7         r = 0
          8         for ii in x:
    
    ipdb> n    # next单步执行
    > e:\debug.py(8)sum()
          7         r = 0
    ----> 8         for ii in x:
          9                 r += ii
    
    ipdb> n    # next单步执行
    > e:\debug.py(9)sum()
          8         for ii in x:
    ----> 9                 r += ii
         10         return r
    
    ipdb> u    # up的缩写,跳回到上一层的调用
    > e:\debug.py(20)<module>()
         19 x = [1,2,3,4,5]
    ---> 20 r = sum(x)
         21 r = mul(x)
    
    ipdb> d    # down的缩写,跳到调用的下一层
    > e:\debug.py(9)sum()
          8         for ii in x:
    ----> 9                 r += ii
         10         return r
    
    ipdb> !r    # !r 查看变量r的值,该变量名与调试命令`r(eturn)`冲突 
    0
    
    ipdb> r    # return的缩写,继续运行直到函数返回
    --Return--
    15
    > e:\debug.py(10)sum()
          9                 r += ii
    ---> 10         return r
         11
    
    ipdb> n    # 下一步
    > e:\debug.py(21)<module>()
         19 x = [1,2,3,4,5]
         20 r = sum(x)
    ---> 21 r = mul(x)
    
    ipdb> x    # 查看变量x的值
    [1, 2, 3, 4, 5]
    
    ipdb> x[0] = 10000    # 修改变量x的值
    
    ipdb> b 13    # break的缩写,AI第13行设置断点
    Breakpoint 1 at e:\debug.py:13
    
    ipdb> c    # continue的缩写,继续运行,直到遇到断点
    > e:\debug.py(13)mul()
         12 def mul(x):
    1--> 13         r = 1
         14         for ii in x:
    
    ipdb> return    # 返回的是修改后x的乘积
    --Return--
    1200000
    > e:\debug.py(16)mul()
         15                 r *= ii
    ---> 16         return r
         17
    
    ipdb> q    # quit的缩写,退出debug模式
    Exiting Debugger.
    

    关于ipdb的使用还有一些技巧:

    • <tab>键能够自动补齐,补齐用法与IPython中的类似。
    • j(ump) <lineno>能够跳过中间某些行代码的执行
    • 可以直接在ipdb中修改变量的值
    • h(elp)能够查看调试命令的用法,比如h h可以查看h(elp)命令的用法,h jump能够查看j(ump)命令的用法。
    6.2.2 在PyTorch中Debug

    PyTorch作为一个动态图框架,与ipdb结合使用能为调试过程带来便捷。对TensorFlow等静态图框架来说,使用Python接口定义计算图,然后使用C++代码执行底层运算,在定义图的时候不进行任何计算,而在计算的时候又无法使用pdb进行调试,因为pdb调试只能调试Python代码,故调试一直是此类静态图框架的一个痛点。与TensorFlow不同,PyTorch可以在执行计算的同时定义计算图,这些计算定义过程是使用Python完成的。虽然底层的计算也是用C/C++完成的,但是我们能够查看Python定义部分的变量值,这就已经足够了。下面我们将举例说明。

    • 如何AIPyTorch中查看神经网络各个层的输出。
    • 如何在PyTorch中分析各个参数的梯度。
    • 如何动态修改PyTorch的训练过程。

    首先,运行第一节给出的“猫狗大战”程序:

    python main.py train --debug-file='debug/debug.txt'
    

    程序运行一段时间后,在debug目录下创建debug.txt标识文件,当程序检测到这个文件存在时,会自动进入debug模式。

    99it [00:17,  6.07it/s]loss: 0.22854854568839075
    119it [00:21,  5.79it/s]loss: 0.21267264398435753
    139it [00:24,  5.99it/s]loss: 0.19839374726372108
    > e:\workspace\python\pytorch\chapter6\main.py(80)train()
         79         loss_meter.reset()
    ---> 80         confusion_matrix.reset()
         81         for ii, (data, label) in tqdm(enumerate(train_dataloader)):
    
    ipdb> break 88    # 在第88行设置断点,当程序运行到此处进入debug模式
    Breakpoint 1 at e:\workspace\python\pytorch\chapter6\main.py:88
    
    ipdb> # 打印所有参数及其梯度的标准差
    for (name,p) in model.named_parameters(): \
        print(name,p.data.std(),p.grad.data.std())
    model.features.0.weight tensor(0.2615, device='cuda:0') tensor(0.3769, device='cuda:0')
    model.features.0.bias tensor(0.4862, device='cuda:0') tensor(0.3368, device='cuda:0')
    model.features.3.squeeze.weight tensor(0.2738, device='cuda:0') tensor(0.3023, device='cuda:0')
    model.features.3.squeeze.bias tensor(0.5867, device='cuda:0') tensor(0.3753, device='cuda:0')
    model.features.3.expand1x1.weight tensor(0.2168, device='cuda:0') tensor(0.2883, device='cuda:0')
    model.features.3.expand1x1.bias tensor(0.2256, device='cuda:0') tensor(0.1147, device='cuda:0')
    model.features.3.expand3x3.weight tensor(0.0935, device='cuda:0') tensor(0.1605, device='cuda:0')
    model.features.3.expand3x3.bias tensor(0.1421, device='cuda:0') tensor(0.0583, device='cuda:0')
    model.features.4.squeeze.weight tensor(0.1976, device='cuda:0') tensor(0.2137, device='cuda:0')
    model.features.4.squeeze.bias tensor(0.4058, device='cuda:0') tensor(0.1798, device='cuda:0')
    model.features.4.expand1x1.weight tensor(0.2144, device='cuda:0') tensor(0.4214, device='cuda:0')
    model.features.4.expand1x1.bias tensor(0.4994, device='cuda:0') tensor(0.0958, device='cuda:0')
    model.features.4.expand3x3.weight tensor(0.1063, device='cuda:0') tensor(0.2963, device='cuda:0')
    model.features.4.expand3x3.bias tensor(0.0489, device='cuda:0') tensor(0.0719, device='cuda:0')
    model.features.6.squeeze.weight tensor(0.1736, device='cuda:0') tensor(0.3544, device='cuda:0')
    model.features.6.squeeze.bias tensor(0.2420, device='cuda:0') tensor(0.0896, device='cuda:0')
    model.features.6.expand1x1.weight tensor(0.1211, device='cuda:0') tensor(0.2428, device='cuda:0')
    model.features.6.expand1x1.bias tensor(0.0670, device='cuda:0') tensor(0.0162, device='cuda:0')
    model.features.6.expand3x3.weight tensor(0.0593, device='cuda:0') tensor(0.1917, device='cuda:0')
    model.features.6.expand3x3.bias tensor(0.0227, device='cuda:0') tensor(0.0160, device='cuda:0')
    model.features.7.squeeze.weight tensor(0.1207, device='cuda:0') tensor(0.2179, device='cuda:0')
    model.features.7.squeeze.bias tensor(0.1484, device='cuda:0') tensor(0.0381, device='cuda:0')
    model.features.7.expand1x1.weight tensor(0.1235, device='cuda:0') tensor(0.2279, device='cuda:0')
    model.features.7.expand1x1.bias tensor(0.0450, device='cuda:0') tensor(0.0100, device='cuda:0')
    model.features.7.expand3x3.weight tensor(0.0609, device='cuda:0') tensor(0.1628, device='cuda:0')
    model.features.7.expand3x3.bias tensor(0.0132, device='cuda:0') tensor(0.0079, device='cuda:0')
    model.features.9.squeeze.weight tensor(0.1093, device='cuda:0') tensor(0.2459, device='cuda:0')
    model.features.9.squeeze.bias tensor(0.0646, device='cuda:0') tensor(0.0135, device='cuda:0')
    model.features.9.expand1x1.weight tensor(0.0840, device='cuda:0') tensor(0.1860, device='cuda:0')
    model.features.9.expand1x1.bias tensor(0.0177, device='cuda:0') tensor(0.0033, device='cuda:0')
    model.features.9.expand3x3.weight tensor(0.0476, device='cuda:0') tensor(0.1393, device='cuda:0')
    model.features.9.expand3x3.bias tensor(0.0058, device='cuda:0') tensor(0.0030, device='cuda:0')
    model.features.10.squeeze.weight tensor(0.0872, device='cuda:0') tensor(0.1676, device='cuda:0')
    model.features.10.squeeze.bias tensor(0.0484, device='cuda:0') tensor(0.0088, device='cuda:0')
    model.features.10.expand1x1.weight tensor(0.0859, device='cuda:0') tensor(0.2145, device='cuda:0')
    model.features.10.expand1x1.bias tensor(0.0160, device='cuda:0') tensor(0.0025, device='cuda:0')
    model.features.10.expand3x3.weight tensor(0.0456, device='cuda:0') tensor(0.1429, device='cuda:0')
    model.features.10.expand3x3.bias tensor(0.0070, device='cuda:0') tensor(0.0021, device='cuda:0')
    model.features.11.squeeze.weight tensor(0.0786, device='cuda:0') tensor(0.2003, device='cuda:0')
    model.features.11.squeeze.bias tensor(0.0422, device='cuda:0') tensor(0.0069, device='cuda:0')
    model.features.11.expand1x1.weight tensor(0.0690, device='cuda:0') tensor(0.1400, device='cuda:0')
    model.features.11.expand1x1.bias tensor(0.0138, device='cuda:0') tensor(0.0022, device='cuda:0')
    model.features.11.expand3x3.weight tensor(0.0366, device='cuda:0') tensor(0.1517, device='cuda:0')
    model.features.11.expand3x3.bias tensor(0.0109, device='cuda:0') tensor(0.0023, device='cuda:0')
    model.features.12.squeeze.weight tensor(0.0729, device='cuda:0') tensor(0.1736, device='cuda:0')
    model.features.12.squeeze.bias tensor(0.0814, device='cuda:0') tensor(0.0084, device='cuda:0')
    model.features.12.expand1x1.weight tensor(0.0977, device='cuda:0') tensor(0.1385, device='cuda:0')
    model.features.12.expand1x1.bias tensor(0.0102, device='cuda:0') tensor(0.0032, device='cuda:0')
    model.features.12.expand3x3.weight tensor(0.0365, device='cuda:0') tensor(0.1312, device='cuda:0')
    model.features.12.expand3x3.bias tensor(0.0038, device='cuda:0') tensor(0.0026, device='cuda:0')
    model.classifier.1.weight tensor(0.0285, device='cuda:0') tensor(0.0865, device='cuda:0')
    model.classifier.1.bias tensor(0.0362, device='cuda:0') tensor(0.0192, device='cuda:0')
    
    ipdb> opt.lr    # 查看学习率
    0.001
    
    ipdb> opt.lr = 0.002    # 更改学习率
    
    ipdb> for p in optimizer.param_groups: \
        p['lr'] = opt.lr
    
    ipdb> model.save()    # 保存模型
    'checkpoints/squeezenet_20191004212249.pth'
    
    ipdb> c    # 继续运行,直到第88行暂停
    222it [16:38, 35.62s/it]> e:\workspace\python\pytorch\chapter6\main.py(88)train()
         87             optimizer.zero_grad()
    1--> 88             score = model(input)
         89             loss = criterion(score, target)
    
    ipdb> s    # 进入model(input)内部,即model.__call__(input)
    --Call--
    > c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(537)__call__()
        536 
    --> 537     def __call__(self, *input, **kwargs):
        538         for hook in self._forward_pre_hooks.values():
    
    ipdb> n    # 下一步
    > c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(538)__call__()
        537     def __call__(self, *input, **kwargs):
    --> 538         for hook in self._forward_pre_hooks.values():
        539             result = hook(self, input)
    
    ipdb> n    # 下一步
    > c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(544)__call__()
        543                 input = result
    --> 544         if torch._C._get_tracing_state():
        545             result = self._slow_forward(*input, **kwargs)
    
    ipdb> n    # 下一步
    > c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(547)__call__()
        546         else:
    --> 547             result = self.forward(*input, **kwargs)
        548         for hook in self._forward_hooks.values():
    
    ipdb> s    # 进入forward函数内容
    --Call--
    > c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\loss.py(914)forward()
        913 
    --> 914     def forward(self, input, target):
        915         return F.cross_entropy(input, target, weight=self.weight,
    
    ipdb> input    # 查看input变量值
    tensor([[4.5005, 2.0725],
            [3.5933, 7.8643],
            [2.9086, 3.4209],
            [2.7740, 4.4332],
            [6.0164, 2.3033],
            [5.2261, 3.2189],
            [2.6529, 2.0749],
            [6.3259, 2.2383],
            [3.0629, 3.4832],
            [2.7008, 8.2818],
            [5.5684, 2.1567],
            [3.0689, 6.1022],
            [3.4848, 5.3831],
            [1.7920, 5.7709],
            [6.5032, 2.8080],
            [2.3071, 5.2417],
            [3.7474, 5.0263],
            [4.3682, 3.6707],
            [2.2196, 6.9298],
            [5.2201, 2.3034],
            [6.4315, 1.4970],
            [3.4684, 4.0371],
            [3.9620, 1.7629],
            [1.7069, 7.8898],
            [3.0462, 1.6505],
            [2.4081, 6.4456],
            [2.1932, 7.4614],
            [2.3405, 2.7603],
            [1.9478, 8.4156],
            [2.7935, 7.8331],
            [1.8898, 3.8836],
            [3.3008, 1.6832]], device='cuda:0', grad_fn=<AsStridedBackward>)
    
    ipdb> input.data.mean()    # 查看input的均值和标准差
    tensor(3.9630, device='cuda:0')
    ipdb> input.data.std()
    tensor(1.9513, device='cuda:0')
    
    ipdb> u    # 跳回上一层
    > c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(547)__call__()
        546         else:
    --> 547             result = self.forward(*input, **kwargs)
        548         for hook in self._forward_hooks.values():
    
    ipdb> u    # 跳回上一层
    > e:\workspace\python\pytorch\chapter6\main.py(88)train()
         87             optimizer.zero_grad()
    1--> 88             score = model(input)
         89             loss = criterion(score, target)
    
    ipdb> clear    # 清除所有断点
    Clear all breaks? y
    Deleted breakpoint 1 at e:\workspace\python\pytorch\chapter6\main.py:88
    
    ipdb> c    # 继续运行,记得先删除"debug/debug.txt",否则很快又会进入调试模式
    59it [06:21,  5.75it/s]loss: 0.24856307208538073
    76it [06:24,  5.91it/s]
    
    

    当我们想要进入debug模式,修改程序中某些参数值或者想分析程序时,就可以通过创建debug标识文件,此时程序会进入调试模式,调试完成之后删除这个文件并在ipdb调试接口输入c继续运行程序。如果想退出程序,也可以使用这种方式,先创建debug标识文件,然后输入quit在退出debug的同时退出程序。这种退出程序的方式,与使用Ctrl+C的方式相比更安全,因为这能保证数据加载的多进程程序也能正确地退出,并释放内存、显存等资源。

    PyTorch和ipdb集合能完成很多其他框架所不能完成或很难完成的功能。根据笔者日常使用的总结,主要有以下几个部分:
    (1)通过debug暂停程序。当程序进入debug模式后,将不再执行PCU和GPU运算,但是内存和显存及相应的堆栈空间不会释放。
    (2)通过debug分析程序,查看每个层的输出,查看网络的参数情况。通过u(p)、d(own)、s(tep)等命令,能够进入指定的代码,通过n(ext)可以单步执行,从而看到每一层的运算结果,便于分析网络的数值分布等信息。
    (3)作为动态图框架,PyTorch拥有Python动态语言解释执行的优点,我们能够在运行程序时,用过ipdb修改某些变量的值或属性,这些修改能够立即生效。例如可以在训练开始不久根据损失函数调整学习率,不必重启程序。
    (4)如果在IPython中通过%run魔法方法运行程序,那么在程序异常退出时,可以使用%debug命令,直接进入debug模式,通过u(p)和d(own)跳到报错的地方,查看对应的变量,找出原因后修改相应的代码即可。有时我们的模式训练了好几个小时,却在将要保存模式之前,因为一个小小的拼写错误异常退出。此时,如果修改错误再重新运行程序又要花费好几个小时,太浪费时间。因此最好的方法就是看利用%debug进入调试模式,在调试模式中直接运行model.save()保存模型。在IPython中,%pdb魔术方法能够使得程序出现问题后,不用手动输入%debug而自动进入debug模式,建议使用。

    PyTorch调用CuDNN报错时,报错信息诸如CUDNN_STATUS_BAD_PARAM,从这些报错内容很难得到有用的帮助信息,最后先利用PCU运行代码,此时一般会得到相对友好的报错信息,例如在ipdb中执行model.cpu()(input.cpu()),PyTorch底层的TH库会给出相对比较详细的信息。

    常见的错误主要有以下几种:

    • 类型不匹配问题。例如CrossEntropyLoss的输入target应该是一个LongTensor,而很多人输入FloatTensor。
    • 部分数据忘记从CPU转移到GPU。例如,当model存放于GPU时,输入input也需要转移到GPU才能输入到model中。还有可能就是把多个model存放于一个list对象,而在执行model.cuda()时,这个list中的对象是不会被转移到CUDA上的,正确的用法是用ModuleList代替。
    • Tensor形状不匹配。此类问题一般是输入数据形状不对,或是网络结构设计有问题,一般通过u(p)跳到指定代码,查看输入和模型参数的形状即可得知。

    此外,可能还会经常遇到程序正常运行、没有报错,但是模型无法收敛的问题。例如对于二分类问题,交叉熵损失一直徘徊在0.69附近(ln2),或者是数值出现溢出等问题,此时可以进入debug模式,用单步执行查看,每一层输出的均值和方差,观察从哪一层的输出开始出现数值异常。还要查看每个参数梯度的均值和方差,查看是否出现梯度消失或者梯度爆炸等问题。一般来说,通过再激活函数之前增加BatchNorm层、合理的参数初始化、使用Adam优化器、学习率设为0.001,基本就能确保模型在一定程度收敛。

    本章带领读者从头实现了一个Kaggle上的经典竞赛,重点讲解了如何合理地组合安排程序,同时介绍了一些在PyTorch中调试的技巧。

    相关文章

      网友评论

        本文标题:深度学习框架PyTorch入门与实践:第六章 PyTorch实战

        本文链接:https://www.haomeiwen.com/subject/vdelyctx.html