torch.nn 的本質
本文翻譯自 PyTorch 的官方中 Tutorial 的一篇 WHAT IS TORCH.NN REALLY?。
torch.nn 的本質
PyTorch 提供了各種優雅設計的 modules 和類 torch.nn,torch.optim,Dataset 和 DataLoader 來幫助你創建並訓練神經網絡。為了充分利用它們的力量並且根據你的問題定製它們,你需要真正地準確了解它們在做什麼。為了建立這種理解,我們首先從這些模型(models)上不使用任何特性(features)在 MNIST 數據集上訓練一個基本的神經網絡;我們將從最基本的 PyTorch Tensor 功能開始。然後,我們每次在 torch.nn
,torch.optim
,Dataset
或 DataLoader
逐漸地增加一個特性,準確地展示每一塊做的事情,並且它如何使代碼更簡潔或更靈活。
這篇博文假設你已經安裝了 PyTorch 並且熟悉 Tensor 操作的基礎。(如果你熟悉 NumPy 數組的操作,你會發現這裡使用的 PyTorch Tensor 操作幾乎相同。)
MNISt 數據配置
我們將使用經典的 MNIST 數據集,其是由手寫數字(從 0 到 9)的黑白圖像組成。
我們將使用 pathlib 處理路徑(Python3 的標準庫之一),使用 request 下載數據集。我們在每一步僅導入使用的 modules,所以你可以準確地看到每一步在使用什麼。
from pathlib import Path
import requests
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
PATH.mkdir(parents=True, exist_ok=True)
URL = "//github.com/pytorch/tutorials/raw/master/_static/"
FILENAME = "mnist.pkl.gz"
if not (PATH / FILENAME).exists():
content = requests.get(URL + FILENAME).content
(PATH / FILENAME).open("wb").write(content)
如果網速不給力,可以從這裡下載我下載好的 mnist.pkl.gz。
這個數據集是存儲在 NumPy 數組的格式,而且已經被 pickle 存儲,一種 Python 特有的序列化數據的格式。
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')
每一張圖像是 28×28,並且被存儲為展開的長度為 784(=28×28)的一行。讓我們看一個,首先,我們需要重新將形狀(shape)改為二維的。
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 數組,所以我們需要轉換我們的數據。
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
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)
從頭開始神經網絡(不使用 torch.nn)
讓我們不使用除 PyTorch Tensot 之外的包開始構建一個模型。我們假設你已經熟悉神經網絡的基礎。(如果你還不熟悉,你可從 course.fast.ai 學習它們)。
PyTorch 提供了創建隨機數或零值填充 Tensor 的方法,我們將使用它創建我們簡單線性模型的權重(weight)和偏置單元(bias)。這些只是普通的 Tensor,但是一個非常特殊的附加:我們告訴 PyTorch 它們需要梯度。這讓 PyTorch 記錄所有完成在 Tensor 上的操作,以便它在反向傳播時自動地計算梯度!
對於這些權重(weight),我們初始化之 後 設置 requires_grad
,因為我們不希望這個步驟(初始化)被添加進梯度。(注意下劃線符號 _
,在 PyTorch 中表明某個 Tensor 上的操作就地執行(in-place)。)
這裡我們使用 Xavier initialisation(通過乘以 1/sqrt(n))方法初始化權重。
import math
weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
感謝 PyTorch 的自動計算梯度的能力,我們可以使用任何 Python 的標準函數(或可調對象(callable object))作為模型!所以,讓我們僅僅使用簡單的矩陣相乘和廣播(broadcasted)加法創建一個線性模型。我們也需要一個激活函數(activation function),所以我們將寫一個 log_softmax 使用。記住:即使 PyTorch 提供了許多寫好的損失函數(loss function)、激活函數(activation function)等等,你也可以使用原生的 Python 寫出你自己的函數。甚至 PyTorch 還將為你的函數自動地創建快速的 GPU 或矢量化(vectorized)CPU 代碼。
def log_softmax(x):
return x - x.exp().sum(-1).log().unsqueeze(-1)
def model(xb):
return log_softmax(xb @ weights + bias)
在上面的代碼中,@
符號表示點積(dot product)操作。我們將在一個數據批量上調用我們的函數(在這個例子中,64 張圖片),這是一次前向傳播(forward pass)。注意在這個階段我們的預測不比隨即預測好,因為我們是從隨機權重開始的。
bs = 64 # batch size
xb = x_train[0:bs] # a mini-batch from x
preds = model(xb)
print(preds[0], preds.shape)
tensor([-2.6015, -2.8883, -3.1596, -2.2470, -2.8118, -2.0224, -2.2773, -2.1566,
-1.4275, -2.6397], grad_fn=<SelectBackward>) torch.Size([64, 10])
正如你所見,preds
Tensor 包含了不僅僅是 Tensor 中的值,同樣也有一個梯度函數。我們之後將使用它做反向傳播。
讓我們實現負對數似然(negative log-likelihood)作為我們的損失函數(再次說明,我們只使用原生的 Python)。
def nll(input, target):
return -input[range(target.shape[0]), target].mean()
loss_func = nll
讓我們查看我們的隨機模型的損失值(loss),之後我們經過反向傳播之後看看是否得到了提升。
yb = y_train[0:bs]
print(loss_func(preds, yb))
tensor(2.4096, grad_fn=<NegBackward>)
讓我們實現一個函數計算我們模型的準確率。對於每一個預測,如果最大值的下標(index)和目標值一樣,那麼預測就是正確的。
def accuracy(out, yb):
preds = torch.argmax(out, dim=1)
return (preds == yb).float().mean()
同樣檢查我們隨機模型的準確率,並且在反向傳播之後查看準確率是否得到了提升。
print(accuracy(preds, yb))
tensor(0.0625)
我們現在可以執行訓練循環。對於每一迭代(iteration),我們將:
- 選擇一個數據的批量(大小為
bs
) - 使用模型做預測
- 計算損失值(loss)
loss.backward()
更新模型的梯度,在這個例子中,是weight
和bias
。
我們使用這些梯度更新權重(weight)和偏移(bias)。我們在 torch.no_grad()
上下文管理器內做更新,因為我們不希望這些活動被記錄在我們的下一步梯度的計算。你可以在 這裡 查看更多關於 PyTorch 的 autograd 記錄操作。
下一步我們將梯度設為 0,以便為下一個循環準備。否則,我們的梯度會記錄所有已經發生的運算的運行記錄(比如 loss.backward()
會累加梯度,無論裏面存儲了什麼,而不是替換)。
你可以使用標準的 Python 調試器(debugger)單步調試(step through)PyTorch 的代碼,讓你可以檢查每一個步驟的變量值。
取消注釋
set_trace()
嘗試它。
from IPython.core.debugger import set_trace
lr = 0.5 # 學習率(learning rate)
epochs = 2 # 訓練多少次
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_()
我們已經完全地從零開始創建並訓練了一個最小的神經網絡(在這個例子中,一個邏輯回歸(logistic regression),因為我們沒有隱藏層)。
讓我們檢查損失值(loss)和準確率與我們之前得到的相比。我們期待損失值(loss)將會下降並且準確率將會有所上升。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
tensor(0.0821, grad_fn=<NegBackward>) tensor(1.)
使用 torch.nn.functional
我們下一步就重構(refactor)我們的代碼,以便它和之前做的一樣,只有我們開始利用 PyTorch 的 nn
類使代碼變得更加簡潔和靈活。從這裡開始的每一步,我們應該使我們的代碼變得一個或多個的:簡短、更容易理解或更靈活。
在一開始,最簡單的步驟是通過替換我們手寫的激活函數(activate function)和損失函數(loss function)為 torch.nn.functional
包中的函數(依照慣例,通常我們導入到命名空間(namespace)F
中),讓我們的代碼變得更簡短。這個 module 包含了 torch.nn
庫內的所有函數(而該庫的其它部分包含了類(classes))。以及各種各樣的損失(loss)和激活(activation)函數,你也在這裡可以找到一些方便的函數來構建神經網絡,比如池化函數(pooling functions)。(也有做卷積(convolutions)的函數、線性層(linear layers)等等,但是我們即將看到,這些通常使用庫的其它部分更好地處理。)
如果你正使用負對數似然(negative log likelihood)損失函數和 log softmax 函數,PyTorch 提供了單一的函數 F.cross_entropy
將二者結合起來。所以我們甚至可以從我們的模型移除激活函數(activation function)。
import torch.nn.functional as F
loss_func = F.cross_entropy
def model(xb):
return xb @ weights + bias
注意我們不再在 model
函數里調用 log_softmax
函數。讓我們確認我們的損失值(loss)和準確率是否和之前一樣。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
tensor(0.0821, grad_fn=<NllLossBackward>) tensor(1.)
使用 torch.Module 重構
下一步,為了更清楚和更簡潔的訓練循環(training loop),我們將使用 nn.Module
和 nn.Parameter
。我們的子類 nn.Module
(它本身是一個類並且可以跟蹤狀態)。在這個例子,我們想要創建一個持有權重(weights)、偏移(bias)和前向傳播的方法的類。nn.Module
有一些我們將使用的屬性和方法(比如 .parameters()
和 .zero_grad()
)。
nn.Module
(大寫 M)是 PyTorch 特有的概念,並且是一個我們將經常使用的類。nn.Module
不要與 Python 的 module(小寫 m)概念混淆,後者是可以被導入的 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.randn(10))
def forward(self, xb):
return xb @ self.weights + self.bias
因為我們現在使用的是對象而不是使用函數,所以我們需要先實例化我們的模型。
model = Mnist_Logistic()
現在我們可以和以前一樣以相同的方法計算損失值(loss)。注意 nn.Module
的對象好像和函數一樣使用(即它們是可調用的(callable),而且在後台 PyTorch 將自動調用我們的方法 forward
。
print(loss_func(model(xb), yb))
tensor(3.0925, grad_fn=<NllLossBackward>)
之前對於我們的訓練循環來說,我們必須通過變量名來更新每一個參數的值,並且要單獨地對每一個參數的梯度手動清零,就像這樣。
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad_zeor_()
bias.grad_zero_()
現在,我們可以利用 model.parameters()
和 model.zero_grad()
(都是定義在 PyTorch 的 nn.Module
里)使得那些步驟更簡潔並且更不易於忘記我們的某些參數,尤其是當我們有一個更複雜的模型時。
with torch.no_grad():
for p in model.parameters():
p -= p.grad() * lr
model.zero_grad()
我們將訓練循環封裝到一個 fit
函數,以便我們之後可以多次運行它。
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()
讓我們再次檢查我們的損失值(loss)有所減少。
print(loss_func(model(xb), yb))
tensor(0.0814, grad_fn=<NllLossBackward>)
使用 torch.Linear 重構
我們繼續重構我們的代碼。作為手動定義和初始化 self.weights
和 self.bias
並且計算 xb @ self.weights + self.bias
的替代,我們將用 PyTorch 的類 nn.Linear 為一個為我們做所有事情的線性層(linear layer)。PyTorch 有許多層(layers)的類型,可以極大地簡化我們的代碼,同樣也使其更快。
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.lin = nn.Linear(784, 10)
def forward(self, xb):
return self.lin(xb)
我們實例化我們的模型並和以前同樣的方法計算損失值(loss)。
model = Mnist_Logistic()
print(loss_func(model(xb), yb))
tensor(2.3702, grad_fn=<NllLossBackward>)
我們仍然能夠使用之前的 fit
方法。
fit()
print(loss_func(model(xb), yb))
tensor(0.0813, grad_fn=<NllLossBackward>)
使用 optim 重構
PyTorch 同樣有包含各種優化算法的包 torch.optim
。我們可以從我們的優化器(optimizer)使用 step
方法做一次傳播步驟,而不是手動更新每一個參數。
讓我們把之前手動更新的代碼:
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 並且我們需要在下一個數據批量的計算梯度之前調用它。)
from torch import optim
我們將定義一個小函數來創建我們的模型和優化器(optimizer)讓我們在之後可以重複使用它。
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.2597, grad_fn=<NllLossBackward>)
tensor(0.0809, grad_fn=<NllLossBackward>)
使用 Dataset 重構
PyTorch 有一個抽象 Dataset 類。Dataset 可以具有 __len__
函數(通過 Python 的標準 len
函數調用)和 __getitem__
函數作為對其索引的一種方法。這個例子 是一個非常好的例子來創建一個定製的繼承 Dataset
的 FacialLandmarkDataset
類。
PyTorch 的 TensorDataset 是一個封裝了 Dataset 的 Tensors。通過定義索引的長度和方式,這同樣給我們沿着 Tensor 的第一個維度迭代、索引和切片(slice)的方法。這讓我們訓練時更容易在同一行中訪問自變量和因變量。
from torch.utils.data import TensorDataset
x_train
和 y_train
都可以被綁定在一個更容易迭代和切片(slice)的 TensorDataset
。
train_ds = TensorDataset(x_train, y_train)
在之前,我們必須分別迭代 x 和 y 的數據批量值。
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
現在,我們可以將這兩步合併到一步:
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.0823, grad_fn=<NllLossBackward>)
使用 DataLoader 重構
PyTorch 的 DataLoader
負責管理數據批量。你可以從任何 Dataset
創建一個 DataLoader
。DataLoader
讓迭代數據批量變得更簡單。而不是使用 train_ds[i*bs : i*bs+bs]
,DataLoader
自動地給我們每一個數據批量。
from torch.utils.data import DataLoader
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
在之前,我們的循環迭代每一個數據批量(xb,yb)像這樣:
for i in range((n-1)//bs + 1):
xb,yb = train_ds[i*bs : i*bs+bs]
pred = model(xb)
現在,我們的循環已經更簡潔了,(xb,yb)從 DataLoader 自動地加載:
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.0825, grad_fn=<NllLossBackward>)
感謝 PyTorch 的 nn.Module
,nn.Parameter
,Dataset
和 DataLoader
,我們的訓練循環現在顯著的小並且非常容易理解。讓我們現在嘗試增加在實際中創建高效的模型的基本特徵。
增加驗證(Add validation)
在第一部分,我們只是嘗試建立一個合理的訓練循環以用於我們的訓練數據。在實際上,你總是應該有一個 驗證集(validation set),為了鑒別你是否過擬合(overfitting)。
洗亂(shuffling)訓練數據對於防止數據批量和過擬合之間的相關性(correlation)很 重要。在另一方面,無論我們洗亂(shuffle)驗證集與否,驗證損失(validation loss)都是一樣的。由於洗亂(shuffling)花費額外的時間,洗亂(shuffle)驗證數據是沒有意義的。
我們將設置驗證集(validation set)的批量大小為訓練集的兩倍。這是因為驗證集不需要反向傳播並且佔用更少的內存(它不需要存儲梯度)。我們利用這一點使用大的數據批量並且更快地計算損失值(loss)。
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)
我們將在每一次迭代之後都計算並打印驗證集的損失值。
(注意我們總是在訓練之前調用 model.train()
而且在評估(inference)之前調用 model.eval()
,因為這些被諸如 nn.BatchNorm2d
和 nn.Dropout
使用,確保對於不同的階段的適當的行為。)
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.3125)
1 tensor(0.2864)
創建 fit() 和 get_data()
我們現在對我們自己進行一些小的重構。因為我們經歷了兩次相似的計算訓練集(training set)和驗證集(validation set)的損失值(loss)的處理過程,讓我們把它變成它自己的函數,loss_batch
來計算一個數據批量的損失值(loss)。
當是訓練集時,我們傳入一個優化器(optimizer)並且用它做反向傳播。對於驗證集(validation set),我們不需要傳入優化器(optimizer),所以方法(method)不需要執行反向傳播。
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
運行訓練我們的模型的必要的操作,並且對於每一個迭代(epoch)計算訓練(training)和驗證(validation)的損失(loss)。
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
返回訓練集和驗證集的 DataLoader。
def get_data(train_ds, valid_ds, bs):
return (
DataLoader(train_ds, batch_size=bs, shuffle=True),
DataLoader(valid_ds, batch_size=bs*2)
)
現在,我們的整個獲取 DataLoader 和訓練模型的過程可以運行在 3 行代碼中。
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.3818369417190552
1 0.29548657131195066
你可以使用這些基礎的 3 行代碼訓練種種的模型。讓我們來看看是否我們可以用它們訓練一個卷積神經網絡(CNN)!
切換到 CNN
我們現在要構建一個三層卷積層(convolutional layer)的神經網絡。因為前面部分的沒有一個函數顯露出有關模型形式的信息,所以我們將可以使用它們不做任何修改訓練一個卷積神經網絡(Convolutional Neural Network(CNN))。
我們將使用 PyTorch 的預定義的(predefined)Conv2d 類作為我們的卷積層。我們定義一個有 3 層卷積層的卷積神經網絡。每一個卷積後都跟一個 ReLU。在最後,我們執行一個平均池化(average pooling)。(注意 view
是 NumPy 版的 reshape
。)
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
Momentum 是一種隨機梯度下降(stochastic gradient descent)的變體,把之前的更新也考慮在內並且通常讓訓練更快。
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.38749439089894294
1 0.2610516972362995
nn.Sequential
torch.nn
有另一個方便的類我們可以使用簡化我們的代碼:Sequential。一個 Sequential
對象以一種順序的方式運行包含在它之內的 Modules。這是一種寫神經網絡更簡單的方式。
為了利用它,我們需要可以從一個給定的函數簡單地定義一個 定製層(custom layer)。舉個例子,PyTorch 沒有 view 層,我們需要為我們的神經網絡創建一個。Lambda
將創建一層(layer),我們可以在使用 Sequential
定義一個神經網絡的時候使用它。
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
創建模型是簡單的。
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.3955023421525955
1 0.23224713450670242
封裝 DataLoader
我們的卷積神經網絡相當簡潔,但是它只能運行在 MNIST 上,因為:
- 它假設輸入是一個 \(28\times 28\) 的長向量
- 它假設最終的卷積神經網絡的網格尺寸是 \(4\times 4\)(因為我們使用平均池化(average pooling)的內核尺寸(kernel size))
讓我們丟掉這兩個假設,所以我們的模型可以運行在任何的二維單通道圖像上。首先,我們可以移除最開始的 Lambda 層,但是移動數據預處理到一個生成器(generator)。
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)
下一步,我們可以替換 nn.AvgPool2d
為 nn.AdaptiveAvgPool2d
,這允許我們定義我們想要的 Tensor 的輸出尺寸,而不是我們有的輸入 Tensor。因此,我們的模型可以運行在任何尺寸的輸入上。
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, 16, 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.4092831357955933
1 0.3001906236886978
使用你的 GPU
如果你足夠幸運可以使用一個支持 CUDA(CUDA-capable)的 GPU(你可以以一小時 0.5 刀的價格從很多雲提供商租一個)你可以使用它加速你的代碼。首先在 PyTorch 里檢查你的 GPU 是否可以工作。
print(torch.cuda.is_available())
False
然後為它創建一個設備對象(device object)。
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)
你應該發現它運行的更快了。
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.18722925274372101
1 0.21267506906986236
總結
我們現在有一套通用的數據通道和訓練循環,你可以使用 PyTorch 訓練許多模型的類型。
當然,有很多你想要去添加的事情,比如數據增強(data augmentation)、超參調節(hyperparameter tuning)和轉移學習(transfer learning)等等。這些特徵在 fastai 庫都是可用的,這個庫已被開發為和這篇博文展示的相同的設計方法,為從業人員進一步提升他們的模型提供了自然的下一步。
我們在這篇博文開始的時候保證過我們通過每一個例子解釋 torch.nn
,torch.optim
,Dataset
和 DataLoader
。所以讓我們總結一下我們已經看到的。
torch.nn
Module
:創建可調用對象(callable)其行為就像一個函數,但是也可以包含狀態(state)(比如神經網絡層上的權重(weight))。它知道其包含的Parameter
,並且可以清零所有的梯度,遍歷它們進行權重更新等。Parameter
:一個 Tensor 的包裝,用於告訴Module
它是權重(weight)需要在反向傳播時更新。只有設置了 requires_grad 屬性的 Tensor 才可以被更新。functional
:一個模塊(module),通常導入轉換到F
的命名空間,它包含激活函數(activation function)、損失函數(loss function)等,以及諸如卷積層和線性層之類的無狀態版本。
torch.optim
:包含諸如SGD
的優化器(optimizer),在反向傳播的時候更新Parameter
的權重(weight)。Dataset
:一個帶有__len__
和__getitem__
的對象的抽象接口(abstract interface),包含 PyTorch 提供的類,例如TensorDataset
。DataLoader
:接受任何的Dataset
並且創建一個返回一個數據批量的迭代器(iterator)。