原文: https://pytorch.org/tutorials/beginner/nn_tutorial.html
作者:杰里米·霍華德(Jeremy Howard), fast.ai 。 感謝 Rachel Thomas 和 Francisco Ingham。
我們建議將本教程作為筆記本而不是腳本來(lái)運(yùn)行。 要下載筆記本(.ipynb)文件,請(qǐng)單擊頁(yè)面頂部的鏈接。
PyTorch 提供設(shè)計(jì)優(yōu)雅的模塊和類(lèi) torch.nn , torch.optim , Dataset 和 DataLoader 來(lái)幫助您創(chuàng)建和訓(xùn)練神經(jīng)網(wǎng)絡(luò)。 為了充分利用它們的功能并針對(duì)您的問(wèn)題對(duì)其進(jìn)行自定義,您需要真正地了解他們的工作。 為了建立這種理解,我們將首先在 MNIST
數(shù)據(jù)集上訓(xùn)練基本神經(jīng)網(wǎng)絡(luò),而無(wú)需使用這些模型的任何功能; 我們最初只會(huì)使用最基本的 PyTorch 張量功能。 然后,我們將一次從torch.nn
,torch.optim
,Dataset
或DataLoader
中逐個(gè)添加一個(gè)功能,確切地顯示每個(gè)功能,以及如何使代碼更簡(jiǎn)潔或更靈活。
本教程假定您已經(jīng)安裝了 PyTorch,并且熟悉張量操作的基礎(chǔ)知識(shí)。 (如果您熟悉 Numpy 數(shù)組操作,將會(huì)發(fā)現(xiàn)此處使用的 PyTorch 張量操作幾乎相同)。
我們將使用經(jīng)典的 MNIST 數(shù)據(jù)集,該數(shù)據(jù)集由手繪數(shù)字的黑白圖像組成(介于 0 到 9 之間)。
我們將使用 pathlib 處理路徑(Python 3 標(biāo)準(zhǔn)庫(kù)的一部分),并下載數(shù)據(jù)集。 我們只會(huì)在使用模塊時(shí)才導(dǎo)入它們,因此您可以確切地看到正在使用模塊的每個(gè)細(xì)節(jié)。
from pathlib import Path
import requests
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
PATH.mkdir(parents=True, exist_ok=True)
URL = "http://deeplearning.net/data/mnist/"
FILENAME = "mnist.pkl.gz"
if not (PATH / FILENAME).exists():
content = requests.get(URL + FILENAME).content
(PATH / FILENAME).open("wb").write(content)
該數(shù)據(jù)集為 numpy 數(shù)組格式,并已使用 pickle(一種用于序列化數(shù)據(jù)的 python 特定格式)存儲(chǔ)。
import pickle
import gzip
with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
每個(gè)圖像為 28 x 28,并存儲(chǔ)被拍平長(zhǎng)度為 784(= 28x28)的向量。 讓我們來(lái)看一個(gè); 我們需要先將其重塑為 2d。
from matplotlib import pyplot
import numpy as np
pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)
得出:
(50000, 784)
PyTorch 使用torch.tensor
而不是 numpy 數(shù)組,因此我們需要轉(zhuǎn)換數(shù)據(jù)。
import torch
x_train, y_train, x_valid, y_valid = map(
torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())
得出:
tensor([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]]) tensor([5, 0, 4, ..., 8, 4, 8])
torch.Size([50000, 784])
tensor(0) tensor(9)
首先,我們僅使用 PyTorch 張量操作創(chuàng)建模型。 我們假設(shè)您已經(jīng)熟悉神經(jīng)網(wǎng)絡(luò)的基礎(chǔ)知識(shí)。 (如果您不是,則可以在 course.fast.ai 中學(xué)習(xí)它們)。
PyTorch 提供了創(chuàng)建隨機(jī)或零填充張量的方法,我們將使用它們來(lái)為簡(jiǎn)單的線性模型創(chuàng)建權(quán)重和偏差。 這些只是常規(guī)張量,還有一個(gè)非常特殊的附加值:我們告訴 PyTorch 它們需要梯度。 這使 PyTorch 記錄了在張量上完成的所有操作,因此它可以在反向傳播時(shí)自動(dòng)地計(jì)算梯度!
對(duì)于權(quán)重,我們?cè)诔跏蓟笤O(shè)置requires_grad
,因?yàn)槲覀儾幌M摬襟E包含在梯度中。 (請(qǐng)注意,PyTorch 中的尾隨_表示該操作是就地執(zhí)行的。)
Note
我們?cè)谶@里用 Xavier 初始化(通過(guò)乘以 1 / sqrt(n))來(lái)初始化權(quán)重。
import math
weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
由于 PyTorch 具有自動(dòng)計(jì)算梯度的功能,我們可以將任何標(biāo)準(zhǔn)的 Python 函數(shù)(或可調(diào)用對(duì)象)用作模型! 因此,讓我們編寫(xiě)一個(gè)簡(jiǎn)單的矩陣乘法和廣播加法來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的線性模型。 我們還需要激活函數(shù),因此我們將編寫(xiě)并使用 $log_softmax$ 。 請(qǐng)記?。罕M管 PyTorch 提供了許多預(yù)先編寫(xiě)的損失函數(shù),激活函數(shù)等,但是您可以使用純 Python 輕松編寫(xiě)自己的函數(shù)。 PyTorch 甚至?xí)詣?dòng)為您的函數(shù)創(chuàng)建快速 GPU 或矢量化的 CPU 代碼。
def log_softmax(x):
return x - x.exp().sum(-1).log().unsqueeze(-1)
def model(xb):
return log_softmax(xb @ weights + bias)
在上面,@
代表點(diǎn)積運(yùn)算。 我們將對(duì)一批數(shù)據(jù)(在這種情況下為 64 張圖像)調(diào)用函數(shù)。 這是一個(gè)前向傳播。 請(qǐng)注意,由于我們從隨機(jī)權(quán)重開(kāi)始,因此在這一階段,我們的預(yù)測(cè)不會(huì)比隨機(jī)預(yù)測(cè)更好。
bs = 64 # batch size
xb = x_train[0:bs] # a mini-batch from x
preds = model(xb) # predictions
preds[0], preds.shape
print(preds[0], preds.shape)
Out:
tensor([-2.0790, -2.6699, -2.2096, -1.6754, -1.7844, -2.8664, -2.2463, -2.7637,
-3.0813, -2.6712], grad_fn=<SelectBackward>) torch.Size([64, 10])
如您所見(jiàn),preds
張量不僅包含張量值,還包含梯度函數(shù)。 稍后我們將使用它進(jìn)行反向傳播。
讓我們實(shí)現(xiàn)負(fù)對(duì)數(shù)似然作為損失函數(shù)(同樣,我們只能使用標(biāo)準(zhǔn) Python):
def nll(input, target):
return -input[range(target.shape[0]), target].mean()
loss_func = nll
讓我們用隨機(jī)模型來(lái)檢查損失,以便我們以后看向后傳播后是否可以改善。
yb = y_train[0:bs]
print(loss_func(preds, yb))
得出:
tensor(2.3076, grad_fn=<NegBackward>)
我們還實(shí)現(xiàn)一個(gè)函數(shù)來(lái)計(jì)算模型的準(zhǔn)確性。 對(duì)于每個(gè)預(yù)測(cè),如果具有最大值的索引與目標(biāo)值匹配,則該預(yù)測(cè)是正確的。
def accuracy(out, yb):
preds = torch.argmax(out, dim=1)
return (preds == yb).float().mean()
讓我們檢查一下隨機(jī)模型的準(zhǔn)確性,以便我們可以看出隨著損失的增加,準(zhǔn)確性是否有所提高。
print(accuracy(preds, yb))
得出:
tensor(0.1250)
現(xiàn)在,我們可以運(yùn)行一個(gè)訓(xùn)練循環(huán)。 對(duì)于每次迭代,我們將:
bs
)loss.backward()
更新模型的梯度,在這種情況下為weights
和bias
。現(xiàn)在,我們使用這些梯度來(lái)更新權(quán)重和偏差。 我們?cè)?code>torch.no_grad()上下文管理器中執(zhí)行此操作,因?yàn)槲覀儾幌M谙乱徊降奶荻扔?jì)算中記錄這些操作。 您可以在上閱讀有關(guān) PyTorch 的 Autograd 如何記錄操作的更多信息。
然后,將梯度設(shè)置為零,以便為下一個(gè)循環(huán)做好準(zhǔn)備。 否則,我們的梯度會(huì)記錄所有已發(fā)生操作的運(yùn)行記錄(即loss.backward()
將梯度添加到已存儲(chǔ)的內(nèi)容中,而不是替換它們)。
TIP
您可以使用標(biāo)準(zhǔn)的 python 調(diào)試器逐步瀏覽 PyTorch 代碼,從而可以在每一步檢查各種變量值。 取消注釋以下set_trace()
即可嘗試。
from IPython.core.debugger import set_trace
lr = 0.5 # learning rate
epochs = 2 # how many epochs to train for
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
# set_trace()
start_i = i * bs
end_i = start_i + bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()
就是這樣:我們完全從頭開(kāi)始創(chuàng)建并訓(xùn)練了一個(gè)最小的神經(jīng)網(wǎng)絡(luò)(在這種情況下,是邏輯回歸,因?yàn)槲覀儧](méi)有隱藏的層)!
讓我們檢查損失和準(zhǔn)確性,并將其與我們之前獲得的進(jìn)行比較。 我們希望損失會(huì)減少,準(zhǔn)確性會(huì)增加,而且確實(shí)如此。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
得出:
tensor(0.0799, grad_fn=<NegBackward>) tensor(1.)
現(xiàn)在,我們將重構(gòu)代碼,使其與以前相同,只是我們將開(kāi)始利用 PyTorch 的nn
類(lèi)使其更加簡(jiǎn)潔和靈活。 從這里開(kāi)始的每一步,我們都應(yīng)該使代碼中的一個(gè)或多個(gè):更短,更易理解和/或更靈活。
第一步也是最簡(jiǎn)單的步驟,就是用torch.nn.functional
(通常按照慣例將其導(dǎo)入到名稱(chēng)空間F中)替換我們的手寫(xiě)激活和損失函數(shù),從而縮短代碼長(zhǎng)度。 該模塊包含torch.nn
庫(kù)中的所有函數(shù)(而該庫(kù)的其他部分包含類(lèi))。 除了廣泛的損失和激活函數(shù)外,您還會(huì)在這里找到一些合適的函數(shù)來(lái)創(chuàng)建神經(jīng)網(wǎng)絡(luò),例如池化函數(shù)。 (還有一些用于進(jìn)行卷積,線性圖層等的函數(shù),但是正如我們將看到的那樣,通??梢允褂脦?kù)的其他部分來(lái)更好地處理這些函數(shù)。)
如果您使用的是負(fù)對(duì)數(shù)似然損失和 log softmax 激活,那么 Pytorch 會(huì)提供將兩者結(jié)合的單個(gè)函數(shù)F.cross_entropy
。 因此,我們甚至可以從模型中刪除激活函數(shù)。
import torch.nn.functional as F
loss_func = F.cross_entropy
def model(xb):
return xb @ weights + bias
請(qǐng)注意,我們不再在model函數(shù)中調(diào)用log_softmax。 讓我們確認(rèn)我們的損失和準(zhǔn)確性與以前相同:
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
得出:
tensor(0.0799, grad_fn=<NllLossBackward>) tensor(1.)
接下來(lái),我們將使用nn.Module
和nn.Parameter
進(jìn)行更清晰,更簡(jiǎn)潔的訓(xùn)練循環(huán)。 我們將nn.Module
子類(lèi)化(它本身是一個(gè)類(lèi)并且能夠跟蹤狀態(tài))。 在這種情況下,我們要?jiǎng)?chuàng)建一個(gè)類(lèi),該類(lèi)包含前進(jìn)步驟的權(quán)重,偏差和方法。 nn.Module
具有許多我們將要使用的屬性和方法(例如.parameters()
和.zero_grad()
)。
Note
nn.Module
(大寫(xiě) M)是 PyTorch 的特定概念,也是我們將經(jīng)常使用的一個(gè)類(lèi)。 nn.Module
不要與(小寫(xiě)m
)模塊的 Python 概念混淆,該模塊是可以導(dǎo)入的 Python 代碼文件。
from torch import nn
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
self.bias = nn.Parameter(torch.zeros(10))
def forward(self, xb):
return xb @ self.weights + self.bias
由于我們現(xiàn)在使用的是對(duì)象而不是僅使用函數(shù),因此我們首先必須實(shí)例化模型:
model = Mnist_Logistic()
現(xiàn)在我們可以像以前一樣計(jì)算損失。 請(qǐng)注意,nn.Module
對(duì)象的使用就像它們是函數(shù)一樣(即,它們是可調(diào)用的),但是在后臺(tái) Pytorch 會(huì)自動(dòng)調(diào)用我們的forward
方法。
print(loss_func(model(xb), yb))
得出:
tensor(2.4205, grad_fn=<NllLossBackward>)
以前,在我們的訓(xùn)練循環(huán)中,我們必須按名稱(chēng)更新每個(gè)參數(shù)的值,并手動(dòng)將每個(gè)參數(shù)的 grads 分別歸零,如下所示:
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()
現(xiàn)在我們可以利用 model.parameters()和 model.zero_grad()(它們都由 PyTorch 為nn.Module
定義)來(lái)使這些步驟更簡(jiǎn)潔,并且更不會(huì)出現(xiàn)忘記某些參數(shù)的錯(cuò)誤,特別是在 我們有一個(gè)更復(fù)雜的模型:
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()
我們將把小的訓(xùn)練循環(huán)包裝在fit
函數(shù)中,以便稍后再運(yùn)行。
def fit():
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
start_i = i * bs
end_i = start_i + bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
with torch.no_grad():
for p in model.parameters():
p -= p.grad * lr
model.zero_grad()
fit()
讓我們仔細(xì)檢查一下我們的損失是否下降了:
print(loss_func(model(xb), yb))
得出:
tensor(0.0796, grad_fn=<NllLossBackward>)
我們繼續(xù)重構(gòu)我們的代碼。 代替手動(dòng)定義和初始化self.weights
和self.bias
并計(jì)算xb @ self.weights + self.bias
,我們將對(duì)線性層使用 Pytorch 類(lèi) nn.Linear ,這將為我們完成所有工作。 Pytorch 具有許多類(lèi)型的預(yù)定義層,可以大大簡(jiǎn)化我們的代碼,并且通常也可以使其速度更快。
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.lin = nn.Linear(784, 10)
def forward(self, xb):
return self.lin(xb)
我們用與以前相同的方式實(shí)例化模型并計(jì)算損失:
model = Mnist_Logistic()
print(loss_func(model(xb), yb))
得出:
tensor(2.3077, grad_fn=<NllLossBackward>)
我們?nèi)匀豢梢允褂门c以前相同的fit方法。
fit()
print(loss_func(model(xb), yb))
得出:
tensor(0.0824, grad_fn=<NllLossBackward>)
Pytorch 還提供了一個(gè)包含各種優(yōu)化算法的軟件包torch.optim
。 我們可以使用優(yōu)化器中的step
方法采取向前的步驟,而不是手動(dòng)更新每個(gè)參數(shù)。
這就是我們將要替換之前手動(dòng)編碼的優(yōu)化步驟:
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()
我們只需使用下面的代替:
opt.step()
opt.zero_grad()
(optim.zero_grad()
將梯度重置為 0,我們需要在計(jì)算下一個(gè)小批量的梯度之前調(diào)用它。)
from torch import optim
我們將定義一個(gè)小函數(shù)來(lái)創(chuàng)建模型和優(yōu)化器,以便將來(lái)再次使用。
def get_model():
model = Mnist_Logistic()
return model, optim.SGD(model.parameters(), lr=lr)
model, opt = get_model()
print(loss_func(model(xb), yb))
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
start_i = i * bs
end_i = start_i + bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
print(loss_func(model(xb), yb))
得出:
tensor(2.2542, grad_fn=<NllLossBackward>)
tensor(0.0811, grad_fn=<NllLossBackward>)
PyTorch 有一個(gè)抽象的 Dataset 類(lèi)。 數(shù)據(jù)集可以是具有__len__
函數(shù)(由 Python 的標(biāo)準(zhǔn)len
函數(shù)調(diào)用)和具有__getitem__
函數(shù)作為對(duì)其進(jìn)行索引的一種方法。 本教程演示了一個(gè)不錯(cuò)的示例,該示例創(chuàng)建一個(gè)自定義FacialLandmarkDataset
類(lèi)作為Dataset
的子類(lèi)。
PyTorch 的 TensorDataset 是一個(gè)數(shù)據(jù)集包裝張量。 通過(guò)定義索引的長(zhǎng)度和方式,這也為我們提供了沿張量的一維進(jìn)行迭代,索引和切片的方法。 這將使我們?cè)谟?xùn)練的同一行中更容易訪問(wèn)自變量和因變量。
from torch.utils.data import TensorDataset
x_train
和y_train
都可以合并為一個(gè)TensorDataset
,這將更易于迭代和切片。
train_ds = TensorDataset(x_train, y_train)
以前,我們不得不分別遍歷 x 和 y 值的迷你批處理:
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
現(xiàn)在,我們可以將兩個(gè)步驟一起執(zhí)行:
xb,yb = train_ds[i*bs : i*bs+bs]
model, opt = get_model()
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
xb, yb = train_ds[i * bs: i * bs + bs]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
print(loss_func(model(xb), yb))
得出:
tensor(0.0819, grad_fn=<NllLossBackward>)
Pytorch 的DataLoader
負(fù)責(zé)批次管理。 您可以從任何Dataset
創(chuàng)建一個(gè)DataLoader
。 DataLoader
使迭代迭代變得更加容易。 不必使用train_ds[i*bs : i*bs+bs]
,DataLoader 會(huì)自動(dòng)為我們提供每個(gè)小批量。
from torch.utils.data import DataLoader
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
以前,我們的循環(huán)遍歷批處理(xb,yb),如下所示:
for i in range((n-1)//bs + 1):
xb,yb = train_ds[i*bs : i*bs+bs]
pred = model(xb)
現(xiàn)在,我們的循環(huán)更加簡(jiǎn)潔了,因?yàn)?xb,yb)是從數(shù)據(jù)加載器自動(dòng)加載的:
for xb,yb in train_dl:
pred = model(xb)
model, opt = get_model()
for epoch in range(epochs):
for xb, yb in train_dl:
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
print(loss_func(model(xb), yb))
得出:
tensor(0.0822, grad_fn=<NllLossBackward>)
得益于 Pytorch 的nn.Module
,nn.Parameter
,Dataset和DataLoader
,我們的訓(xùn)練循環(huán)現(xiàn)在變得更小,更容易理解。 現(xiàn)在,讓我們嘗試添加在實(shí)踐中創(chuàng)建有效模型所需的基本功能。
在第 1 部分中,我們只是試圖建立一個(gè)合理的訓(xùn)練循環(huán)以用于我們的訓(xùn)練數(shù)據(jù)。 實(shí)際上,您總是也應(yīng)該具有驗(yàn)證集,以便識(shí)別您是否過(guò)度擬合。
打亂訓(xùn)練數(shù)據(jù)順序?qū)τ诜乐古闻c過(guò)度擬合之間的相關(guān)性很重要。 另一方面,無(wú)論我們是否打亂驗(yàn)證集,驗(yàn)證損失都是相同的。 由于打亂順序需要花費(fèi)更多時(shí)間,因此打亂驗(yàn)證集數(shù)據(jù)順序沒(méi)有任何意義。
我們將驗(yàn)證集的批次大小設(shè)為訓(xùn)練集的兩倍。 這是因?yàn)轵?yàn)證集不需要反向傳播,因此占用的內(nèi)存更少(不需要存儲(chǔ)漸變)。 我們利用這一優(yōu)勢(shì)來(lái)使用更大的批量,并更快地計(jì)算損失。
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)
valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)
我們將在每個(gè) epoch 結(jié)束時(shí)計(jì)算并打印驗(yàn)證損失。
(請(qǐng)注意,我們總是在訓(xùn)練之前調(diào)用model.train()
,并在推斷之前調(diào)用model.eval()
,因?yàn)橹T如nn.BatchNorm2d
和nn.Dropout
之類(lèi)的圖層會(huì)使用它們,以確保這些不同階段的行為正確。)
model, opt = get_model()
for epoch in range(epochs):
model.train()
for xb, yb in train_dl:
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
model.eval()
with torch.no_grad():
valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)
print(epoch, valid_loss / len(valid_dl))
得出:
0 tensor(0.2903)
1 tensor(0.3343)
現(xiàn)在,我們將自己進(jìn)行一些重構(gòu)。 由于我們經(jīng)歷了兩次相似的過(guò)程來(lái)計(jì)算訓(xùn)練集和驗(yàn)證集的損失,因此我們將其設(shè)為自己的函數(shù)loss_batch
,該函數(shù)可計(jì)算一批損失。
我們將優(yōu)化器傳入訓(xùn)練集中,并使用它執(zhí)行反向傳播。 對(duì)于驗(yàn)證集,我們沒(méi)有通過(guò)優(yōu)化程序,因此該方法不會(huì)執(zhí)行反向傳播。
def loss_batch(model, loss_func, xb, yb, opt=None):
loss = loss_func(model(xb), yb)
if opt is not None:
loss.backward()
opt.step()
opt.zero_grad()
return loss.item(), len(xb)
fit
運(yùn)行必要的操作來(lái)訓(xùn)練我們的模型,并計(jì)算每個(gè)時(shí)期的訓(xùn)練和驗(yàn)證損失。
import numpy as np
def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
for epoch in range(epochs):
model.train()
for xb, yb in train_dl:
loss_batch(model, loss_func, xb, yb, opt)
model.eval()
with torch.no_grad():
losses, nums = zip(
*[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
)
val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)
print(epoch, val_loss)
get_data
返回用于訓(xùn)練和驗(yàn)證集的數(shù)據(jù)加載器。
def get_data(train_ds, valid_ds, bs):
return (
DataLoader(train_ds, batch_size=bs, shuffle=True),
DataLoader(valid_ds, batch_size=bs * 2),
)
現(xiàn)在,我們獲取數(shù)據(jù)加載器和擬合模型的整個(gè)過(guò)程可以在 3 行代碼中運(yùn)行:
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
得出:
0 0.34931180425286296
1 0.28620736759901044
您可以使用這些基本的 3 行代碼來(lái)訓(xùn)練各種各樣的模型。 讓我們看看是否可以使用它們來(lái)訓(xùn)練卷積神經(jīng)網(wǎng)絡(luò)(CNN)!
現(xiàn)在,我們將構(gòu)建具有三個(gè)卷積層的神經(jīng)網(wǎng)絡(luò)。 由于上一節(jié)中的所有函數(shù)都不包含任何有關(guān)模型組合的內(nèi)容,因此我們將能夠使用它們來(lái)訓(xùn)練 CNN,而無(wú)需進(jìn)行任何修改。
我們將使用 Pytorch 的預(yù)定義 Conv2d 類(lèi)作為我們的卷積層。 我們定義具有 3 個(gè)卷積層的 CNN。 每個(gè)卷積后跟一個(gè) ReLU。 最后,我們執(zhí)行平均池化。 (請(qǐng)注意,view
是 numpy 的reshape
的 PyTorch 版本)
class Mnist_CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)
def forward(self, xb):
xb = xb.view(-1, 1, 28, 28)
xb = F.relu(self.conv1(xb))
xb = F.relu(self.conv2(xb))
xb = F.relu(self.conv3(xb))
xb = F.avg_pool2d(xb, 4)
return xb.view(-1, xb.size(1))
lr = 0.1
動(dòng)量是隨機(jī)梯度下降的一種變體,它也考慮了以前的更新,通常可以加快訓(xùn)練速度。
model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
得出:
0 0.33537127304077147
1 0.24059089585542678
torch.nn還有另一個(gè)靈活的類(lèi),可以用來(lái)簡(jiǎn)化我們的代碼: Sequential 。 Sequential
對(duì)象以順序方式運(yùn)行其中包含的每個(gè)模塊。 這是編寫(xiě)神經(jīng)網(wǎng)絡(luò)的一種簡(jiǎn)單方法。
要利用此優(yōu)勢(shì),我們需要能夠從給定的函數(shù)輕松定義自定義層。 例如,PyTorch 沒(méi)有視圖圖層,我們需要為網(wǎng)絡(luò)創(chuàng)建一個(gè)圖層。 Lambda
將創(chuàng)建一個(gè)層,然后在使用Sequential
定義網(wǎng)絡(luò)時(shí)可以使用該層。
class Lambda(nn.Module):
def __init__(self, func):
super().__init__()
self.func = func
def forward(self, x):
return self.func(x)
def preprocess(x):
return x.view(-1, 1, 28, 28)
用Sequential
創(chuàng)建的模型很簡(jiǎn)單:
model = nn.Sequential(
Lambda(preprocess),
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AvgPool2d(4),
Lambda(lambda x: x.view(x.size(0), -1)),
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
得出:
0 0.4098783682346344
1 0.2799181687355041
雖然我們的 CNN 網(wǎng)絡(luò)很簡(jiǎn)潔,但是它只能在 MNIST 數(shù)據(jù)集上面有效,因?yàn)?/p>
我們使用的平均池化卷積核的大?。?/p>
讓我們擺脫這兩個(gè)假設(shè),因此我們的模型需要適用于任何 2d 單通道圖像。 首先,我們可以刪除初始的 Lambda 層,但將數(shù)據(jù)預(yù)處理移至生成器中:
def preprocess(x, y):
return x.view(-1, 1, 28, 28), y
class WrappedDataLoader:
def __init__(self, dl, func):
self.dl = dl
self.func = func
def __len__(self):
return len(self.dl)
def __iter__(self):
batches = iter(self.dl)
for b in batches:
yield (self.func(*b))
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
接下來(lái),我們可以將nn.AvgPool2d
替換為nn.AdaptiveAvgPool2d
,這使我們可以定義所需的輸出張量的大小,而不是所需的輸入張量的大小。 結(jié)果,我們的模型將適用于任何大小的輸入。
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
Lambda(lambda x: x.view(x.size(0), -1)),
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
試試看:
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
得出:
0 0.34252993125915526
1 0.28579100420475007
如果您足夠幸運(yùn)地能夠使用具有 CUDA 功能的 GPU(您可以從大多數(shù)云提供商處以每小時(shí)$ 0.50 的價(jià)格租用一個(gè) GPU),則可以使用它來(lái)加速代碼。 首先檢查您的 GPU 是否在 Pytorch 中正常工作:
print(torch.cuda.is_available())
得出:
True
然后為其創(chuàng)建一個(gè)設(shè)備對(duì)象:
dev = torch.device(
"cuda") if torch.cuda.is_available() else torch.device("cpu")
讓我們更新preprocess
,將批次移至 GPU:
def preprocess(x, y):
return x.view(-1, 1, 28, 28).to(dev), y.to(dev)
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
最后,我們可以將模型移至 GPU。
model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
您應(yīng)該發(fā)現(xiàn)它現(xiàn)在運(yùn)行得更快:
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
得出:
0 0.1909765040397644
1 0.180943009185791
現(xiàn)在,我們有了一個(gè)通用的數(shù)據(jù)管道和訓(xùn)練循環(huán),您可以將其用于使用 Pytorch 訓(xùn)練多種類(lèi)型的模型。 要了解現(xiàn)在可以輕松進(jìn)行模型訓(xùn)練,請(qǐng)查看 mnist_sample 示例筆記本。
當(dāng)然,您需要添加很多內(nèi)容,例如數(shù)據(jù)增強(qiáng),超參數(shù)調(diào)整,監(jiān)控訓(xùn)練,轉(zhuǎn)移學(xué)習(xí)等。 這些功能在 fastai 庫(kù)中可用,該庫(kù)是使用本教程中所示的相同設(shè)計(jì)方法開(kāi)發(fā)的,為希望進(jìn)一步推廣模型的從業(yè)人員提供了自然的下一步。
我們承諾在本教程開(kāi)始時(shí)將通過(guò)示例分別說(shuō)明torch.nn
,torch.optim
,Dataset
和DataLoader
。 因此,讓我們總結(jié)一下我們所看到的:
torch.nnModule
:創(chuàng)建一個(gè)類(lèi)似函數(shù)行為功能的,但可以包含狀態(tài)(例如神經(jīng)網(wǎng)絡(luò)層權(quán)重)的可調(diào)用對(duì)象。它知道它包含的Parameter
,并且可以將其所有梯度歸零,通過(guò)其循環(huán)進(jìn)行權(quán)重更新等 。Parameter
:張量的包裝器,它告訴Module
具有在反向傳播期間需要更新的權(quán)重。僅更新具有 require_grad 屬性集的張量functional
:一個(gè)模塊(通常按照常規(guī)導(dǎo)入到F名稱(chēng)空間中),包含激活函數(shù),損失函數(shù)等。以及卷積和線性層之類(lèi)的無(wú)狀態(tài)版本。torch.optim
:包含其中SGD
之類(lèi)的優(yōu)化程序,這些優(yōu)化程序可以在反向傳播期間更新權(quán)重參數(shù)Dataset
:一個(gè)具有__len__
和__getitem__
的抽象接口對(duì)象,包括 Pytorch 提供的類(lèi),例如TensorDatasetDataLoader
:獲取任何Dataset
并創(chuàng)建一個(gè)迭代器,該迭代器返回批量數(shù)據(jù)。
腳本的總運(yùn)行時(shí)間:(1 分鐘 7.131 秒)
Download Python source code: nn_tutorial.py
Download Jupyter notebook: nn_tutorial.ipynb
更多建議: