torch.nn 的本質

本文翻譯自 PyTorch 的官方中 Tutorial 的一篇 WHAT IS TORCH.NN REALLY?

torch.nn 的本質

PyTorch 提供了各種優雅設計的 modules 和類 torch.nntorch.optimDatasetDataLoader 來幫助你創建並訓練神經網絡。為了充分利用它們的力量並且根據你的問題定製它們,你需要真正地準確了解它們在做什麼。為了建立這種理解,我們首先從這些模型(models)上不使用任何特性(features)在 MNIST 數據集上訓練一個基本的神經網絡;我們將從最基本的 PyTorch Tensor 功能開始。然後,我們每次在 torch.nntorch.optimDatasetDataLoader 逐漸地增加一個特性,準確地展示每一塊做的事情,並且它如何使代碼更簡潔或更靈活。

這篇博文假設你已經安裝了 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)

output_6_1

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() 更新模型的梯度,在這個例子中,是 weightbias

我們使用這些梯度更新權重(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 包中的函數(依照慣例,通常我們導入到命名空間(namespaceF 中),讓我們的代碼變得更簡短。這個 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.Modulenn.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.weightsself.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__ 函數作為對其索引的一種方法。這個例子 是一個非常好的例子來創建一個定製的繼承 DatasetFacialLandmarkDataset 類。

PyTorch 的 TensorDataset 是一個封裝了 Dataset 的 Tensors。通過定義索引的長度和方式,這同樣給我們沿着 Tensor 的第一個維度迭代、索引和切片(slice)的方法。這讓我們訓練時更容易在同一行中訪問自變量和因變量。

from torch.utils.data import TensorDataset

x_trainy_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 創建一個 DataLoaderDataLoader 讓迭代數據批量變得更簡單。而不是使用 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.Modulenn.ParameterDatasetDataLoader,我們的訓練循環現在顯著的小並且非常容易理解。讓我們現在嘗試增加在實際中創建高效的模型的基本特徵。

增加驗證(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.BatchNorm2dnn.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 的預定義的(predefinedConv2d 類作為我們的卷積層。我們定義一個有 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.AvgPool2dnn.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.nntorch.optimDatasetDataLoader。所以讓我們總結一下我們已經看到的。

  • 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)。