PyTorch最佳实践,怎样才能写出一手风格优美的代码

作者: 机器之心 2019-05-06 11:06:30

虽然这是一个非官方的 PyTorch 指南,但本文总结了一年多使用 PyTorch 框架的经验,尤其是用它开发深度学习相关工作的***解决方案。请注意,我们分享的经验大多是从研究和实践角度出发的。

这是一个开发的项目,欢迎其它读者改进该文档:

https://github.com/IgorSusmelj/pytorch-styleguide。

本文档主要由三个部分构成:首先,本文会简要清点 Python 中的***装备。接着,本文会介绍一些使用 PyTorch 的技巧和建议。***,我们分享了一些使用其它框架的见解和经验,这些框架通常帮助我们改进工作流。

一、清点 Python 装备

1. 建议使用 Python 3.6 以上版本

根据我们的经验,我们推荐使用 Python 3.6 以上的版本,因为它们具有以下特性,这些特性可以使我们很容易写出简洁的代码:

  • 自 Python 3.6 以后支持「typing」模块
  • 自 Python 3.6 以后支持格式化字符串(f string)

2. Python 风格指南

我们试图遵循 Google 的 Python 编程风格。请参阅 Google 提供的优秀的 python 编码风格指南:

地址:https://github.com/google/styleguide/blob/gh-pages/pyguide.md。

在这里,我们会给出一个最常用命名规范小结:

3. 集成开发环境

一般来说,我们建议使用 visual studio 或 PyCharm 这样的集成开发环境。而 VS Code 在相对轻量级的编辑器中提供语法高亮和自动补全功能,PyCharm 则拥有许多用于处理远程集群任务的高级特性。

4. Jupyter Notebooks VS Python 脚本

一般来说,我们建议使用 Jupyter Notebook 进行初步的探索,或尝试新的模型和代码。如果你想在更大的数据集上训练该模型,就应该使用 Python 脚本,因为在更大的数据集上,复现性更加重要。

我们推荐你采取下面的工作流程:

  • 在开始的阶段,使用 Jupyter Notebook
  • 对数据和模型进行探索
  • 在 notebook 的单元中构建你的类/方法
  • 将代码移植到 Python 脚本中
  • 在服务器上训练/部署

5. 开发常备库

常用的程序库有:

6. 文件组织

不要将所有的层和模型放在同一个文件中。***的做法是将最终的网络分离到独立的文件(networks.py)中,并将层、损失函数以及各种操作保存在各自的文件中(layers.py,losses.py,ops.py)。最终得到的模型(由一个或多个网络组成)应该用该模型的名称命名(例如,yolov3.py,DCGAN.py),且引用各个模块。

主程序、单独的训练和测试脚本应该只需要导入带有模型名字的 Python 文件。

二、PyTorch 开发风格与技巧

我们建议将网络分解为更小的可复用的片段。一个 nn.Module 网络包含各种操作或其它构建模块。损失函数也是包含在 nn.Module 内,因此它们可以被直接整合到网络中。

继承 nn.Module 的类必须拥有一个「forward」方法,它实现了各个层或操作的前向传导。

一个 nn.module 可以通过「self.net(input)」处理输入数据。在这里直接使用了对象的「call()」方法将输入数据传递给模块。

  1. output = self.net(input) 

1. PyTorch 环境下的一个简单网络

使用下面的模式可以实现具有单个输入和输出的简单网络:

  1. class ConvBlock(nn.Module): 
  2.     def __init__(self): 
  3.         super(ConvBlock, self).__init__() 
  4.         block = [nn.Conv2d(...)] 
  5.         block += [nn.ReLU()] 
  6.         block += [nn.BatchNorm2d(...)] 
  7.         self.block = nn.Sequential(*block) 
  8.  
  9.     def forward(self, x): 
  10.         return self.block(x) 
  11.  
  12. class SimpleNetwork(nn.Module): 
  13.     def __init__(self, num_resnet_blocks=6): 
  14.         super(SimpleNetwork, self).__init__() 
  15.         # here we add the individual layers 
  16.         layers = [ConvBlock(...)] 
  17.         for i in range(num_resnet_blocks): 
  18.             layers += [ResBlock(...)] 
  19.         self.net = nn.Sequential(*layers) 
  20.  
  21.     def forward(self, x): 
  22.         return self.net(x) 

请注意以下几点:

  • 我们复用了简单的循环构建模块(如卷积块 ConvBlocks),它们由相同的循环模式(卷积、激活函数、归一化)组成,并装入独立的 nn.Module 中。
  • 我们构建了一个所需要层的列表,并最终使用「nn.Sequential()」将所有层级组合到了一个模型中。我们在 list 对象前使用「*」操作来展开它。
  • 在前向传导过程中,我们直接使用输入数据运行模型。

2. PyTorch 环境下的简单残差网络

  1. class ResnetBlock(nn.Module): 
  2.     def __init__(self, dim, padding_type, norm_layer, use_dropout, use_bias): 
  3.         super(ResnetBlock, self).__init__() 
  4.         selfself.conv_block = self.build_conv_block(...) 
  5.  
  6.     def build_conv_block(self, ...): 
  7.         conv_block = [] 
  8.  
  9.         conv_block += [nn.Conv2d(...), 
  10.                        norm_layer(...), 
  11.                        nn.ReLU()] 
  12.         if use_dropout: 
  13.             conv_block += [nn.Dropout(...)] 
  14.  
  15.         conv_block += [nn.Conv2d(...), 
  16.                        norm_layer(...)] 
  17.  
  18.         return nn.Sequential(*conv_block) 
  19.  
  20.     def forward(self, x): 
  21.         out = x + self.conv_block(x) 
  22.         return ou 

在这里,ResNet 模块的跳跃连接直接在前向传导过程中实现了,PyTorch 允许在前向传导过程中进行动态操作。

3. PyTorch 环境下的带多个输出的网络

对于有多个输出的网络(例如使用一个预训练好的 VGG 网络构建感知损失),我们使用以下模式:

  1. class Vgg19(torch.nn.Module): 
  2.   def __init__(self, requires_grad=False): 
  3.     super(Vgg19, self).__init__() 
  4.     vgg_pretrained_features = models.vgg19(pretrained=True).features 
  5.     self.slice1 = torch.nn.Sequential() 
  6.     self.slice2 = torch.nn.Sequential() 
  7.     self.slice3 = torch.nn.Sequential() 
  8.  
  9.     for x in range(7): 
  10.         self.slice1.add_module(str(x), vgg_pretrained_features[x]) 
  11.     for x in range(7, 21): 
  12.         self.slice2.add_module(str(x), vgg_pretrained_features[x]) 
  13.     for x in range(21, 30): 
  14.         self.slice3.add_module(str(x), vgg_pretrained_features[x]) 
  15.     if not requires_grad: 
  16.         for param in self.parameters(): 
  17.             param.requires_grad = False 
  18.  
  19.   def forward(self, x): 
  20.     h_relu1 = self.slice1(x) 
  21.     h_relu2 = self.slice2(h_relu1)         
  22.     h_relu3 = self.slice3(h_relu2)         
  23.     out = [h_relu1, h_relu2, h_relu3] 
  24.     return out 

请注意以下几点:

  • 我们使用由「torchvision」包提供的预训练模型
  • 我们将一个网络切分成三个模块,每个模块由预训练模型中的层组成
  • 我们通过设置「requires_grad = False」来固定网络权重
  • 我们返回一个带有三个模块输出的 list

4. 自定义损失函数

即使 PyTorch 已经具有了大量标准损失函数,你有时也可能需要创建自己的损失函数。为了做到这一点,你需要创建一个独立的「losses.py」文件,并且通过扩展「nn.Module」创建你的自定义损失函数:

  1. class CustomLoss(torch.nn.Module): 
  2.  
  3.     def __init__(self): 
  4.         super(CustomLoss,self).__init__() 
  5.  
  6.     def forward(self,x,y): 
  7.         loss = torch.mean((x - y)**2) 
  8.         return loss 

5. 训练模型的***代码结构

对于训练的***代码结构,我们需要使用以下两种模式:

  • 使用 prefetch_generator 中的 BackgroundGenerator 来加载下一个批量数据
  • 使用 tqdm 监控训练过程,并展示计算效率,这能帮助我们找到数据加载流程中的瓶颈
  1. # import statements 
  2. import torch 
  3. import torch.nn as nn 
  4. from torch.utils import data 
  5. ... 
  6.  
  7. # set flags / seeds 
  8. torch.backends.cudnn.benchmark = True 
  9. np.random.seed(1) 
  10. torch.manual_seed(1) 
  11. torch.cuda.manual_seed(1) 
  12. ... 
  13.  
  14. # Start with main code 
  15. if __name__ == '__main__': 
  16.     # argparse for additional flags for experiment 
  17.     parser = argparse.ArgumentParser(description="Train a network for ..."
  18.     ... 
  19.     opt = parser.parse_args()  
  20.  
  21.     # add code for datasets (we always use train and validation/ test set) 
  22.     data_transforms = transforms.Compose([ 
  23.         transforms.Resize((opt.img_size, opt.img_size)), 
  24.         transforms.RandomHorizontalFlip(), 
  25.         transforms.ToTensor(), 
  26.         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) 
  27.     ]) 
  28.  
  29.     train_dataset = datasets.ImageFolder( 
  30.         root=os.path.join(opt.path_to_data, "train"), 
  31.         transform=data_transforms
  32.     train_data_loader = data.DataLoader(train_dataset, ...) 
  33.  
  34.     test_dataset = datasets.ImageFolder( 
  35.         root=os.path.join(opt.path_to_data, "test"), 
  36.         transform=data_transforms
  37.     test_data_loader = data.DataLoader(test_dataset ...) 
  38.     ... 
  39.  
  40.     # instantiate network (which has been imported from *networks.py*) 
  41.     net = MyNetwork(...) 
  42.     ... 
  43.  
  44.     # create losses (criterion in pytorch) 
  45.     criterion_L1 = torch.nn.L1Loss() 
  46.     ... 
  47.  
  48.     # if running on GPU and we want to use cuda move model there 
  49.     use_cuda = torch.cuda.is_available() 
  50.     if use_cuda: 
  51.         netnet = net.cuda() 
  52.         ... 
  53.  
  54.     # create optimizers 
  55.     optim = torch.optim.Adam(net.parameters(), lr=opt.lr) 
  56.     ... 
  57.  
  58.     # load checkpoint if needed/ wanted 
  59.     start_n_iter = 0 
  60.     start_epoch = 0 
  61.     if opt.resume: 
  62.         ckpt = load_checkpoint(opt.path_to_checkpoint) # custom method for loading last checkpoint 
  63.         net.load_state_dict(ckpt['net']) 
  64.         start_epoch = ckpt['epoch'] 
  65.         start_n_iter = ckpt['n_iter'] 
  66.         optim.load_state_dict(ckpt['optim']) 
  67.         print("last checkpoint restored") 
  68.         ... 
  69.  
  70.     # if we want to run experiment on multiple GPUs we move the models there 
  71.     net = torch.nn.DataParallel(net) 
  72.     ... 
  73.  
  74.     # typically we use tensorboardX to keep track of experiments 
  75.     writer = SummaryWriter(...) 
  76.  
  77.     # now we start the main loop 
  78.     n_iter = start_n_iter 
  79.     for epoch in range(start_epoch, opt.epochs): 
  80.         # set models to train mode 
  81.         net.train() 
  82.         ... 
  83.  
  84.         # use prefetch_generator and tqdm for iterating through data 
  85.         pbar = tqdm(enumerate(BackgroundGenerator(train_data_loader, ...)), 
  86.                     total=len(train_data_loader)) 
  87.         start_time = time.time() 
  88.  
  89.         # for loop going through dataset 
  90.         for i, data in pbar: 
  91.             # data preparation 
  92.             img, label = data 
  93.             if use_cuda: 
  94.                 imgimg = img.cuda() 
  95.                 labellabel = label.cuda() 
  96.             ... 
  97.  
  98.             # It's very good practice to keep track of preparation time and computation time using tqdm to find any issues in your dataloader 
  99.             prepare_time = start_time-time.time() 
  100.  
  101.             # forward and backward pass 
  102.             optim.zero_grad() 
  103.             ... 
  104.             loss.backward() 
  105.             optim.step() 
  106.             ... 
  107.  
  108.             # udpate tensorboardX 
  109.             writer.add_scalar(..., n_iter) 
  110.             ... 
  111.  
  112.             # compute computation time and *compute_efficiency* 
  113.             process_time = start_time-time.time()-prepare_time 
  114.             pbar.set_description("Compute efficiency: {:.2f}, epoch: {}/{}:".format( 
  115.                 process_time/(process_time+prepare_time), epoch, opt.epochs)) 
  116.             start_time = time.time() 
  117.  
  118.         # maybe do a test pass every x epochs 
  119.         if epoch % x == x-1: 
  120.             # bring models to evaluation mode 
  121.             net.eval() 
  122.             ... 
  123.             #do some tests 
  124.             pbar = tqdm(enumerate(BackgroundGenerator(test_data_loader, ...)), 
  125.                     total=len(test_data_loader))  
  126.             for i, data in pbar: 
  127.                 ... 
  128.  
  129.             # save checkpoint if needed 
  130.             ... 

三、PyTorch 的多 GPU 训练

PyTorch 中有两种使用多 GPU 进行训练的模式。

根据我们的经验,这两种模式都是有效的。然而,***种方法得到的结果更好、需要的代码更少。由于第二种方法中的 GPU 间的通信更少,似乎具有轻微的性能优势。

1. 对每个网络输入的 batch 进行切分

最常见的一种做法是直接将所有网络的输入切分为不同的批量数据,并分配给各个 GPU。

这样一来,在 1 个 GPU 上运行批量大小为 64 的模型,在 2 个 GPU 上运行时,每个 batch 的大小就变成了 32。这个过程可以使用「nn.DataParallel(model)」包装器自动完成。

2. 将所有网络打包到一个超级网络中,并对输入 batch 进行切分

这种模式不太常用。下面的代码仓库向大家展示了 Nvidia 实现的 pix2pixHD,它有这种方法的实现。

地址:https://github.com/NVIDIA/pix2pixHD

四、PyTorch 中该做和不该做的

1. 在「nn.Module」的「forward」方法中避免使用 Numpy 代码

Numpy 是在 CPU 上运行的,它比 torch 的代码运行得要慢一些。由于 torch 的开发思路与 numpy 相似,所以大多数 Numpy 中的函数已经在 PyTorch 中得到了支持。

2. 将「DataLoader」从主程序的代码中分离

载入数据的工作流程应该独立于你的主训练程序代码。PyTorch 使用「background」进程更加高效地载入数据,而不会干扰到主训练进程。

3. 不要在每一步中都记录结果

通常而言,我们要训练我们的模型好几千步。因此,为了减小计算开销,每隔 n 步对损失和其它的计算结果进行记录就足够了。尤其是,在训练过程中将中间结果保存成图像,这种开销是非常大的。

4. 使用命令行参数

使用命令行参数设置代码执行时使用的参数(batch 的大小、学习率等)非常方便。一个简单的实验参数跟踪方法,即直接把从「parse_args」接收到的字典(dict 数据)打印出来:

  1. # saves arguments to config.txt file 
  2. opt = parser.parse_args()with open("config.txt", "w") as f: 
  3.     f.write(opt.__str__()) 

5. 如果可能的话,请使用「Use .detach()」从计算图中释放张量

为了实现自动微分,PyTorch 会跟踪所有涉及张量的操作。请使用「.detach()」来防止记录不必要的操作。

6. 使用「.item()」打印出标量张量

你可以直接打印变量。然而,我们建议你使用「variable.detach()」或「variable.item()」。在早期版本的 PyTorch(< 0.4)中,你必须使用「.data」访问变量中的张量值。

7. 使用「call」方法代替「nn.Module」中的「forward」方法

这两种方式并不完全相同,正如下面的 GitHub 问题单所指出的:

https://github.com/IgorSusmelj/pytorch-styleguide/issues/3

  1. output = self.net.forward(input) 
  2. # they are not equal! 
  3. output = self.net(input) 

原文链接:https://github.com/IgorSusmelj/pytorch-styleguide

【本文是51CTO专栏机构“机器之心”的原创译文,微信公众号“机器之心( id: almosthuman2014)”】

戳这里,看该作者更多好文

PyTorch 深度学习 框架
上一篇:揭秘:AI 的神话与现实 下一篇:AI专家巴特菲尔德:已有33个国家采用了统一的AI规范
评论
取消
暂无评论,快去成为第一个评论的人吧

更多资讯推荐

百度CTO王海峰CNCC2019演讲:深度学习平台支撑产业智能化

百度CTO王海峰在会上发表题为《深度学习平台支撑产业智能化》的演讲,分享了百度关于深度学习技术推动人工智能发展及产业化应用的思考,并深度解读百度飞桨深度学习平台的优势,以及与百度智能云结合助力产业智能化的成果。

佚名 ·  1天前
深度学习/计算机视觉常见的8个错误总结及避坑指南

人类并不是完美的,我们经常在编写软件的时候犯错误。有时这些错误很容易找到:你的代码根本不工作,你的应用程序会崩溃。但有些 bug 是隐藏的,很难发现,这使它们更加危险。

skura ·  3天前
2019年深度学习自然语言处理十大发展趋势 精选

自然语言处理在深度学习浪潮下取得了巨大的发展,FloydHub 博客上Cathal Horan介绍了自然语言处理的10大发展趋势,是了解NLP发展的非常好的文章。

HU数据派 ·  4天前
基于PyTorch的CV模型框架,北大学生出品TorchCV

在机器学习带来的所有颠覆性技术中,计算机视觉领域吸引了业内人士和学术界最大的关注。

张倩、泽南 ·  2019-10-15 14:04:29
图灵奖得主Yoshua Bengio:深度学习当务之急,是理解因果关系

深度学习擅长在大量数据中发现模式,但无法解释它们之间的联系,而图灵奖获得者Yoshua Bengio想要改变这一点。

佚名 ·  2019-10-15 05:15:00
2019机器学习框架之争:与Tensorflow竞争白热化,进击的PyTorch赢在哪里?

2019年,机器学习框架之争进入了新阶段:PyTorch与TensorFlow成为最后两大玩家,PyTorch占据学术界领军地位,TensorFlow在工业界力量依然强大,两个框架都在向对方借鉴,但是都不太理想。

大数据文摘 ·  2019-10-11 23:18:15
18个挑战项目带你快速入门深度学习

AlphaGo 大战李世?h之后,深度学习技术便在国内变得异常火。吸引了大批的技术人员争相学习,那么到底如何才能更快速的入门深度学习呢?下面给大家介绍的 18 个挑战项目,通过实践动手带你快速入门深度学习!

实验楼 ·  2019-10-10 14:48:19
盘点 | 8个你可能不知道的深度学习应用案例

深度学习与传统机器学习系统的不同之处在于,它能够在分析大型数据集时进行自我学习和改进,因此能应用在许多不同的领域。

天极网 ·  2019-10-10 14:15:18
Copyright©2005-2019 51CTO.COM 版权所有 未经许可 请勿转载