高效使用PyTorch,一套PyTorch的最佳實踐和代碼風格,不可錯過!

作者:IgorSusmelj

編譯:ronghuaiyang

導讀

PyTorch1.0之後,越來越多的人選擇使用PyTorch,今天給大家介紹一個github項目,作者通過自己使用PyTorch的實際工程經驗,總結出了一套非常有用的使用PyTorch的最佳實踐,涉及到使用PyTorch的方方面面,看了之後非常有收穫!

高效使用PyTorch,一套PyTorch的最佳實踐和代碼風格,不可錯過!

不是PyTorch的官方風格指南。本文總結了使用PyTorch框架進行深度學習的一年多經驗中的最佳實踐。請注意,我們分享的經驗大多來自研究和創業的視角。

這是一個開放的項目,歡迎其他合作者編輯和改進文檔。

該文檔有三個主要部分。首先,簡要回顧一下Python中的最佳實踐,然後介紹一些使用PyTorch的技巧和建議。最後,我們分享了一些使用其他框架的見解和經驗,這些框架通常對我們改進工作流有幫助。

我們推薦使用Python 3.6+

根據我們的經驗,我們推薦使用Python 3.6+,因為它具有以下特性,這些特性對於簡潔的代碼非常方便:

  • 從Python 3.6開始支持typing
  • 從Python 3.6開始支持f string

Python風格指南迴顧

我們嘗試遵循Python的谷歌樣式指南。

請參考文檔豐富的谷歌提供的python代碼風格指南。

我們在此提供最常用規則的摘要:

命名規範

高效使用PyTorch,一套PyTorch的最佳實踐和代碼風格,不可錯過!

代碼編輯器

通常,我們推薦使用IDE,比如visual studio code或PyCharm。而VS Code在相對輕量級的編輯器中提供語法高亮和自動完成功能,PyCharm有許多用於處理遠程集群的高級功能。

Jupyter Notebook vs Python Scripts

一般來說,我們建議使用 jupyter notebooks進行初步探索/嘗試新的模型和代碼。

如果你想在更大的數據集上訓練模型,就應該使用Python腳本,因為在更大的數據集上,復現性更重要。

我們的推薦的工作流:

  1. 從jupyter notebook開始
  2. 研究數據和模型
  3. 在notebook的單元格中構建類/方法
  4. 將代碼移動到python腳本
  5. 在服務器上進行訓練/部署
高效使用PyTorch,一套PyTorch的最佳實踐和代碼風格,不可錯過!

常用的庫:

高效使用PyTorch,一套PyTorch的最佳實踐和代碼風格,不可錯過!

文檔結構

不要把所有層和模型都放在同一個文件中。最佳實踐是將最終的網絡分離到一個單獨的文件中(network .py),並將層、損失和操作符保存在各自的文件中(layers.py,loss.py,ops.py)。完成的模型(由一個或多個網絡組成)應該在一個具有其名稱的文件中引用(例如yolov3.py,DCGAN.py)

主例程、各自的訓練腳本和測試腳本應該只從具有模型名稱的文件中導入。

使用PyTorch構建神經網絡

我們建議將網絡分解為更小的可重用部分。網絡是一個神經網絡。模塊由操作或其他神經網絡組成。模塊作為構建塊。損失函數也是nn.Module。因此,可以直接集成到網絡中。

繼承自nn.Module的類,必須有一個forward方法來實現相應層或操作的前向。

nn.module可以在輸入數據上使用self.net(input),這就是使用了call方法來通過模塊提供輸入。

output = self.net(input)

PyTorch的一個簡單的網絡

對於單輸入單輸出的簡單網絡,請使用以下模式:

class ConvBlock(nn.Module):
def __init__(self):
super(ConvBlock, self).__init__
block = [nn.Conv2d(...)]
block += [nn.ReLU()]
block += [nn.BatchNorm2d(...)]
self.block = nn.Sequential(*block)
def forward(self, x):
return self.block(x)

class SimpleNetwork(nn.Module):
def __init__(self, num_resnet_blocks=6):
super(SimpleNetwork, self).__init__
# here we add the individual layers
layers = [ConvBlock(...)]
for i in range(num_resnet_blocks):
layers += [ResBlock(...)]
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)

請注意以下幾點:

  • 我們重用簡單的循環構建塊,如ConvBlock,它由相同的循環模式(卷積、激活、歸一化)組成,並將它們放入單獨的nn.Module中
  • 我們建立一個所需層的列表,最後使用nn.Sequential將它們轉換成一個模型。我們在list對象之前使用*操作符來展開它。
  • 在前向傳遞中,我們只是通過模型運行輸入

在PyTorch中使用帶有跳躍連接的網絡

class ResnetBlock(nn.Module):
def __init__(self, dim, padding_type, norm_layer, use_dropout, use_bias):
super(ResnetBlock, self).__init__
self.conv_block = self.build_conv_block(...)
def build_conv_block(self, ...):
conv_block =
conv_block += [nn.Conv2d(...),
norm_layer(...),
nn.ReLU()]
if use_dropout:
conv_block += [nn.Dropout(...)]
conv_block += [nn.Conv2d(...),
norm_layer(...)]
return nn.Sequential(*conv_block)

def forward(self, x):
out = x + self.conv_block(x)
return out

在這裡,實現了一個ResNet block的跳躍連接。PyTorch允許在向前傳遞期間進行動態操作。

在PyTorch使用多個輸出的網絡

對於一個需要多個輸出的網絡,例如使用一個預先訓練好的VGG網絡構建感知機loss,我們使用以下模式:

class Vgg19(torch.nn.Module):
def __init__(self, requires_grad=False):
super(Vgg19, self).__init__
vgg_pretrained_features = models.vgg19(pretrained=True).features
self.slice1 = torch.nn.Sequential
self.slice2 = torch.nn.Sequential
self.slice3 = torch.nn.Sequential
for x in range(7):
self.slice1.add_module(str(x), vgg_pretrained_features[x])
for x in range(7, 21):
self.slice2.add_module(str(x), vgg_pretrained_features[x])
for x in range(21, 30):
self.slice3.add_module(str(x), vgg_pretrained_features[x])
if not requires_grad:
for param in self.parameters:
param.requires_grad = False

def forward(self, x):
h_relu1 = self.slice1(x)
h_relu2 = self.slice2(h_relu1)
h_relu3 = self.slice3(h_relu2)
out = [h_relu1, h_relu2, h_relu3]
return out

請注意以下事項:

  • 我們使用torchvision提供的預訓練模型。
  • 我們把網絡分成三個部分。每個切片由來自預訓練模型的層組成。
  • 我們將凍結的網絡設置成requires_grad = False
  • 返回一個包含切片的三個輸出的列表

自定義Loss

即使PyTorch已經有很多標準的損失函數,有時也需要創建自己的損失函數。為此,需要創建一個單獨的文件 losses.py,然後擴展nn.Module類創建自定義損失函數:

class CustomLoss(torch.nn.Module):
def __init__(self):
super(CustomLoss,self).__init__
def forward(self,x,y):
loss = torch.mean((x - y)**2)
return loss

訓練模型的推薦代碼結構

注意,我們使用了以下模式:

  • 我們使用從prefetch_generator中的BackgroundGenerator加載下一個batch的數據
  • 我們使用tqdm來監控訓練進度,並顯示計算效率。這有助於我們發現數據加載管道中的瓶頸。
# import statements
import torch
import torch.nn as nn
from torch.utils import data
...
# set flags / seeds
torch.backends.cudnn.benchmark = True
np.random.seed(1)
torch.manual_seed(1)
torch.cuda.manual_seed(1)
...
# Start with main code
if __name__ == '__main__':
# argparse for additional flags for experiment
parser = argparse.ArgumentParser(description="Train a network for ...")
...
opt = parser.parse_args
# add code for datasets (we always use train and validation/ test set)
data_transforms = transforms.Compose([
transforms.Resize((opt.img_size, opt.img_size)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = datasets.ImageFolder(
root=os.path.join(opt.path_to_data, "train"),
transform=data_transforms)
train_data_loader = data.DataLoader(train_dataset, ...)
test_dataset = datasets.ImageFolder(
root=os.path.join(opt.path_to_data, "test"),
transform=data_transforms)
test_data_loader = data.DataLoader(test_dataset ...)
...
# instantiate network (which has been imported from *networks.py*)
net = MyNetwork(...)
...
# create losses (criterion in pytorch)
criterion_L1 = torch.nn.L1Loss
...
# if running on GPU and we want to use cuda move model there
use_cuda = torch.cuda.is_available
if use_cuda:
net = net.cuda
...
# create optimizers
optim = torch.optim.Adam(net.parameters, lr=opt.lr)
...
# load checkpoint if needed/ wanted
start_n_iter = 0
start_epoch = 0
if opt.resume:
ckpt = load_checkpoint(opt.path_to_checkpoint) # custom method for loading last checkpoint
net.load_state_dict(ckpt['net'])
start_epoch = ckpt['epoch']
start_n_iter = ckpt['n_iter']
optim.load_state_dict(ckpt['optim'])
print("last checkpoint restored")
...
# if we want to run experiment on multiple GPUs we move the models there
net = torch.nn.DataParallel(net)
...
# typically we use tensorboardX to keep track of experiments
writer = SummaryWriter(...)
# now we start the main loop
n_iter = start_n_iter
for epoch in range(start_epoch, opt.epochs):
# set models to train mode
net.train
...
# use prefetch_generator and tqdm for iterating through data
pbar = tqdm(enumerate(BackgroundGenerator(train_data_loader, ...)),
total=len(train_data_loader))
start_time = time.time
# for loop going through dataset
for i, data in pbar:
# data preparation
img, label = data
if use_cuda:
img = img.cuda
label = label.cuda
...
# It's very good practice to keep track of preparation time and computation time using tqdm to find any issues in your dataloader
prepare_time = start_time-time.time
# forward and backward pass
optim.zero_grad
...
loss.backward
optim.step
...
# udpate tensorboardX
writer.add_scalar(..., n_iter)
...
# compute computation time and *compute_efficiency*
process_time = start_time-time.time-prepare_time
pbar.set_description("Compute efficiency: {:.2f}, epoch: {}/{}:".format(
process_time/(process_time+prepare_time), epoch, opt.epochs))
start_time = time.time
# maybe do a test pass every x epochs
if epoch % x == x-1:
# bring models to evaluation mode
net.eval
...
#do some tests
pbar = tqdm(enumerate(BackgroundGenerator(test_data_loader, ...)),
total=len(test_data_loader))
for i, data in pbar:
...
# save checkpoint if needed
...

在PyTorch使用多GPU訓練

PyTorch中有兩種使用多個gpu進行訓練的模式。

從我們的經驗來看,這兩種模式都是有效的。然而,第一個方法的結果是代碼更好、更少。由於gpu之間的通信更少,第二種方法似乎具有輕微的性能優勢。

分割每個網絡的batch

最常見的一種方法是簡單地將所有“網絡”的batch分配給各個gpu。

因此,如果一個模型運行在一個批處理大小為64的GPU上,那麼它將運行在兩個GPU上,每個GPU的批處理大小為32。這可以通過使用nn.DataParallel(model)自動完成。

將所有的網絡打包進一個super網絡,並把輸入batch分割

這種模式不太常用。實現這種方法的repository在pix2pixHD implementation by Nvidia

該做的和不該做的

避免在nn.Module的forward方法找那個使用Numpy代碼

Numpy運行在CPU上,比torch代碼慢。由於torch的開發思路與numpy相似,所以大多數numpy函數已經得到了PyTorch的支持。

從main代碼中分離DataLoader

數據加載管道應該獨立於你的主訓練代碼。PyTorch使用後臺來更有效地加載數據,並且不會干擾主訓練過程。

不要在每一次迭代中打印日誌結果

通常我們訓練我們的模型數千個迭代。因此,每n步記錄損失和其他結果就足以減少開銷。特別是,在訓練過程中,將中間結果保存為圖像可能非常耗時。

使用命令行參數

使用命令行參數在代碼執行期間設置參數(批處理大小、學習率等)非常方便。跟蹤實驗參數的一個簡單方法是打印從parse_args接收到的字典:

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

可能的話,使用.detach將張量從圖中釋放出來

PyTorch跟蹤所有涉及張量的操作,以實現自動微分。使用.detach防止記錄不必要的操作。

使用.item打印標量數據

你可以直接打印變量,但是建議使用variable.detachvariable.item。在早期的PyTorch版本< 0.4中,必須使用.data訪問一個變量的張量。

在nn.Module中使用函數調用而不是直接用forward

下面這兩種方式是不一樣的:

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

FAQ

如何讓實驗可復現?

我們建議在代碼開頭設置以下種子:

np.random.seed(1)
torch.manual_seed(1)
torch.cuda.manual_seed(1)

如何進一步提升訓練和推理速度?

在Nvidia GPUs上,你可以在代碼的開頭添加以下行。這將允許cuda後端在第一次執行時優化你的圖。但是,要注意,如果改變網絡輸入/輸出張量的大小,那麼每次發生變化時,圖都會被優化。這可能導致運行非常慢和內存不足錯誤。只有當輸入和輸出總是相同的形狀時才設置此標誌。通常情況下,這將導致大約20%的改善。

torch.backends.cudnn.benchmark = True

使用tqdm + prefetch_generator模式計算效率的最佳值是什麼?

這取決於使用的機器、預處理管道和網絡大小。在一個1080Ti GPU上使用SSD硬盤,我們看到一個幾乎為1.0的計算效率,這是一個理想的場景。如果使用淺(小)網絡或慢速硬盤,這個數字可能會下降到0.1-0.2左右,這取決於你的設置。

即使我沒有足夠的內存,我如何讓batch size > 1?

在PyTorch中,我們可以很容易地實現虛擬batch sizes。我們只是不讓優化器每次都更新參數,並把batch_size個梯度加起來。

...
# in the main loop
out = net(input)
loss = criterion(out, label)
# we just call backward to sum up gradients but don't perform step here
loss.backward
total_loss += loss.item / batch_size
if n_iter % batch_size == batch_size-1:
# here we perform out optimization step using a virtual batch size
optim.step
optim.zero_grad
print('Total loss: ', total_loss)
total_loss = 0.0
...

在訓練過程中如何調整學習率?

我們可以直接使用實例化的優化器得到學習率,如下所示:

...
for param_group in optim.param_groups:
old_lr = param_group['lr']
new_lr = old_lr * 0.1
param_group['lr'] = new_lr
print('Updated lr from {} to {}'.format(old_lr, new_lr))
...

在訓練中如何使用一個預訓練的模型作為損失(沒有後向傳播)

如果你想使用一個預先訓練好的模型,如VGG來計算損失,但不訓練它(例如在style-transfer/GANs/Auto-encoder中的感知損失),你可以使用以下模式:

...
# instantiate the model
pretrained_VGG = VGG19(...)
# disable gradients (prevent training)
for p in pretrained_VGG.parameters: # reset requires_grad
p.requires_grad = False
...
# you don't have to use the no_grad namespace but can just run the model
# no gradients will be computed for the VGG model
out_real = pretrained_VGG(input_a)
out_fake = pretrained_VGG(input_b)
loss = any_criterion(out_real, out_fake)
...

在PyTorch找那個為什麼要用.train和 .eval?

這些方法用於將BatchNorm2dDropout2d等層從訓練模式設置為推理模式。每個模塊都繼承自nn.Module有一個名為istrain的屬性。.eval.train只是簡單地將這個屬性設置為True/ False。有關此方法如何實現的詳細信息,請參閱PyTorch中的module代碼。

我的模型在推理過程中使用了大量內存/如何在PyTorch中正確運行推理模型?

確保在代碼執行期間沒有計算和存儲梯度。你可以簡單地使用以下模式來確保:

with torch.no_grad:
# run model here
out_tensor = net(in_tensor)

如何微調預訓練模型?

在PyTorch你可以凍結層。這將防止在優化步驟中更新它們。

# you can freeze whole modules using
for p in pretrained_VGG.parameters: # reset requires_grad
p.requires_grad = False

什麼時候用Variable(...)?

從PyTorch 0.4開始Variable和Tensor就合併了,我們不用再顯式的構建Variable對象了。

PyTorch在C++上比Python快嗎?

C++版本的速度快10%

TorchScript / JIT可以加速代碼嗎?

Todo...

PyTorch代碼使用cudnn.benchmark=True會變快嗎?

根據我們的經驗,你可以獲得約20%的加速。但是,第一次運行模型需要相當長的時間來構建優化的圖。在某些情況下(前向傳遞中的循環、沒有固定的輸入形狀、前向中的if/else等等),這個標誌可能會導致內存不足或其他錯誤。

如何使用多GPUs訓練?

Todo...

PyTorch中的.detach是怎麼工作的?

如果從計算圖中釋放一個張量,這裡有一個很好的圖解:http://www.bnikolic.co.uk/blog/pytorch-detach.html

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

更多文章,請關注微信公眾號:AI公園

相關推薦

推薦中...