PyTorch最佳實踐,怎樣才能寫出一手風(fēng)格優(yōu)美的代碼
雖然這是一個非官方的 PyTorch 指南,但本文總結(jié)了一年多使用 PyTorch 框架的經(jīng)驗,尤其是用它開發(fā)深度學(xué)習(xí)相關(guān)工作的***解決方案。請注意,我們分享的經(jīng)驗大多是從研究和實踐角度出發(fā)的。
這是一個開發(fā)的項目,歡迎其它讀者改進(jìn)該文檔:
https://github.com/IgorSusmelj/pytorch-styleguide。
本文檔主要由三個部分構(gòu)成:首先,本文會簡要清點(diǎn) Python 中的***裝備。接著,本文會介紹一些使用 PyTorch 的技巧和建議。***,我們分享了一些使用其它框架的見解和經(jīng)驗,這些框架通常幫助我們改進(jìn)工作流。
一、清點(diǎn) Python 裝備
1. 建議使用 Python 3.6 以上版本
根據(jù)我們的經(jīng)驗,我們推薦使用 Python 3.6 以上的版本,因為它們具有以下特性,這些特性可以使我們很容易寫出簡潔的代碼:
- 自 Python 3.6 以后支持「typing」模塊
- 自 Python 3.6 以后支持格式化字符串(f string)
2. Python 風(fēng)格指南
我們試圖遵循 Google 的 Python 編程風(fēng)格。請參閱 Google 提供的優(yōu)秀的 python 編碼風(fēng)格指南:
地址:https://github.com/google/styleguide/blob/gh-pages/pyguide.md。
在這里,我們會給出一個最常用命名規(guī)范小結(jié):
3. 集成開發(fā)環(huán)境
一般來說,我們建議使用 visual studio 或 PyCharm 這樣的集成開發(fā)環(huán)境。而 VS Code 在相對輕量級的編輯器中提供語法高亮和自動補(bǔ)全功能,PyCharm 則擁有許多用于處理遠(yuǎn)程集群任務(wù)的高級特性。
4. Jupyter Notebooks VS Python 腳本
一般來說,我們建議使用 Jupyter Notebook 進(jìn)行初步的探索,或嘗試新的模型和代碼。如果你想在更大的數(shù)據(jù)集上訓(xùn)練該模型,就應(yīng)該使用 Python 腳本,因為在更大的數(shù)據(jù)集上,復(fù)現(xiàn)性更加重要。
我們推薦你采取下面的工作流程:
- 在開始的階段,使用 Jupyter Notebook
- 對數(shù)據(jù)和模型進(jìn)行探索
- 在 notebook 的單元中構(gòu)建你的類/方法
- 將代碼移植到 Python 腳本中
- 在服務(wù)器上訓(xùn)練/部署
5. 開發(fā)常備庫
常用的程序庫有:
6. 文件組織
不要將所有的層和模型放在同一個文件中。***的做法是將最終的網(wǎng)絡(luò)分離到獨(dú)立的文件(networks.py)中,并將層、損失函數(shù)以及各種操作保存在各自的文件中(layers.py,losses.py,ops.py)。最終得到的模型(由一個或多個網(wǎng)絡(luò)組成)應(yīng)該用該模型的名稱命名(例如,yolov3.py,DCGAN.py),且引用各個模塊。
主程序、單獨(dú)的訓(xùn)練和測試腳本應(yīng)該只需要導(dǎo)入帶有模型名字的 Python 文件。
二、PyTorch 開發(fā)風(fēng)格與技巧
我們建議將網(wǎng)絡(luò)分解為更小的可復(fù)用的片段。一個 nn.Module 網(wǎng)絡(luò)包含各種操作或其它構(gòu)建模塊。損失函數(shù)也是包含在 nn.Module 內(nèi),因此它們可以被直接整合到網(wǎng)絡(luò)中。
繼承 nn.Module 的類必須擁有一個「forward」方法,它實現(xiàn)了各個層或操作的前向傳導(dǎo)。
一個 nn.module 可以通過「self.net(input)」處理輸入數(shù)據(jù)。在這里直接使用了對象的「call()」方法將輸入數(shù)據(jù)傳遞給模塊。
- output = self.net(input)
1. PyTorch 環(huán)境下的一個簡單網(wǎng)絡(luò)
使用下面的模式可以實現(xiàn)具有單個輸入和輸出的簡單網(wǎng)絡(luò):
- 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)
請注意以下幾點(diǎn):
- 我們復(fù)用了簡單的循環(huán)構(gòu)建模塊(如卷積塊 ConvBlocks),它們由相同的循環(huán)模式(卷積、激活函數(shù)、歸一化)組成,并裝入獨(dú)立的 nn.Module 中。
- 我們構(gòu)建了一個所需要層的列表,并最終使用「nn.Sequential()」將所有層級組合到了一個模型中。我們在 list 對象前使用「*」操作來展開它。
- 在前向傳導(dǎo)過程中,我們直接使用輸入數(shù)據(jù)運(yùn)行模型。
2. PyTorch 環(huán)境下的簡單殘差網(wǎng)絡(luò)
- class ResnetBlock(nn.Module):
- def __init__(self, dim, padding_type, norm_layer, use_dropout, use_bias):
- super(ResnetBlock, self).__init__()
- selfself.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 ou
在這里,ResNet 模塊的跳躍連接直接在前向傳導(dǎo)過程中實現(xiàn)了,PyTorch 允許在前向傳導(dǎo)過程中進(jìn)行動態(tài)操作。
3. PyTorch 環(huán)境下的帶多個輸出的網(wǎng)絡(luò)
對于有多個輸出的網(wǎng)絡(luò)(例如使用一個預(yù)訓(xùn)練好的 VGG 網(wǎng)絡(luò)構(gòu)建感知損失),我們使用以下模式:
- 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
請注意以下幾點(diǎn):
- 我們使用由「torchvision」包提供的預(yù)訓(xùn)練模型
- 我們將一個網(wǎng)絡(luò)切分成三個模塊,每個模塊由預(yù)訓(xùn)練模型中的層組成
- 我們通過設(shè)置「requires_grad = False」來固定網(wǎng)絡(luò)權(quán)重
- 我們返回一個帶有三個模塊輸出的 list
4. 自定義損失函數(shù)
即使 PyTorch 已經(jīng)具有了大量標(biāo)準(zhǔn)損失函數(shù),你有時也可能需要創(chuàng)建自己的損失函數(shù)。為了做到這一點(diǎn),你需要創(chuàng)建一個獨(dú)立的「losses.py」文件,并且通過擴(kuò)展「nn.Module」創(chuàng)建你的自定義損失函數(shù):
- 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
5. 訓(xùn)練模型的***代碼結(jié)構(gòu)
對于訓(xùn)練的***代碼結(jié)構(gòu),我們需要使用以下兩種模式:
- 使用 prefetch_generator 中的 BackgroundGenerator 來加載下一個批量數(shù)據(jù)
- 使用 tqdm 監(jiān)控訓(xùn)練過程,并展示計算效率,這能幫助我們找到數(shù)據(jù)加載流程中的瓶頸
- # 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:
- netnet = 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:
- imgimg = img.cuda()
- labellabel = 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 訓(xùn)練
PyTorch 中有兩種使用多 GPU 進(jìn)行訓(xùn)練的模式。
根據(jù)我們的經(jīng)驗,這兩種模式都是有效的。然而,***種方法得到的結(jié)果更好、需要的代碼更少。由于第二種方法中的 GPU 間的通信更少,似乎具有輕微的性能優(yōu)勢。
1. 對每個網(wǎng)絡(luò)輸入的 batch 進(jìn)行切分
最常見的一種做法是直接將所有網(wǎng)絡(luò)的輸入切分為不同的批量數(shù)據(jù),并分配給各個 GPU。
這樣一來,在 1 個 GPU 上運(yùn)行批量大小為 64 的模型,在 2 個 GPU 上運(yùn)行時,每個 batch 的大小就變成了 32。這個過程可以使用「nn.DataParallel(model)」包裝器自動完成。
2. 將所有網(wǎng)絡(luò)打包到一個超級網(wǎng)絡(luò)中,并對輸入 batch 進(jìn)行切分
這種模式不太常用。下面的代碼倉庫向大家展示了 Nvidia 實現(xiàn)的 pix2pixHD,它有這種方法的實現(xiàn)。
地址:https://github.com/NVIDIA/pix2pixHD
四、PyTorch 中該做和不該做的
1. 在「nn.Module」的「forward」方法中避免使用 Numpy 代碼
Numpy 是在 CPU 上運(yùn)行的,它比 torch 的代碼運(yùn)行得要慢一些。由于 torch 的開發(fā)思路與 numpy 相似,所以大多數(shù) Numpy 中的函數(shù)已經(jīng)在 PyTorch 中得到了支持。
2. 將「DataLoader」從主程序的代碼中分離
載入數(shù)據(jù)的工作流程應(yīng)該獨(dú)立于你的主訓(xùn)練程序代碼。PyTorch 使用「background」進(jìn)程更加高效地載入數(shù)據(jù),而不會干擾到主訓(xùn)練進(jìn)程。
3. 不要在每一步中都記錄結(jié)果
通常而言,我們要訓(xùn)練我們的模型好幾千步。因此,為了減小計算開銷,每隔 n 步對損失和其它的計算結(jié)果進(jìn)行記錄就足夠了。尤其是,在訓(xùn)練過程中將中間結(jié)果保存成圖像,這種開銷是非常大的。
4. 使用命令行參數(shù)
使用命令行參數(shù)設(shè)置代碼執(zhí)行時使用的參數(shù)(batch 的大小、學(xué)習(xí)率等)非常方便。一個簡單的實驗參數(shù)跟蹤方法,即直接把從「parse_args」接收到的字典(dict 數(shù)據(jù))打印出來:
- # saves arguments to config.txt file
- opt = parser.parse_args()with open("config.txt", "w") as f:
- f.write(opt.__str__())
5. 如果可能的話,請使用「Use .detach()」從計算圖中釋放張量
為了實現(xiàn)自動微分,PyTorch 會跟蹤所有涉及張量的操作。請使用「.detach()」來防止記錄不必要的操作。
6. 使用「.item()」打印出標(biāo)量張量
你可以直接打印變量。然而,我們建議你使用「variable.detach()」或「variable.item()」。在早期版本的 PyTorch(< 0.4)中,你必須使用「.data」訪問變量中的張量值。
7. 使用「call」方法代替「nn.Module」中的「forward」方法
這兩種方式并不完全相同,正如下面的 GitHub 問題單所指出的:
https://github.com/IgorSusmelj/pytorch-styleguide/issues/3
- output = self.net.forward(input)
- # they are not equal!
- output = self.net(input)
原文鏈接:https://github.com/IgorSusmelj/pytorch-styleguide
【本文是51CTO專欄機(jī)構(gòu)“機(jī)器之心”的原創(chuàng)譯文,微信公眾號“機(jī)器之心( id: almosthuman2014)”】