1. 程式人生 > >torch.nn 的本質

torch.nn 的本質

> 本文翻譯自 [PyTorch](https://pytorch.org/) 的官方中 [Tutorial](https://pytorch.org/tutorials/) 的一篇 [WHAT IS TORCH.NN REALLY?](https://pytorch.org/tutorials/beginner/nn_tutorial.html)。 ## torch.nn 的本質 PyTorch 提供了各種優雅設計的 modules 和類 [torch.nn](https://pytorch.org/docs/stable/nn.html),[torch.optim](https://pytorch.org/docs/stable/optim.html),[Dataset](https://pytorch.org/docs/stable/data.html?highlight=dataset#torch.utils.data.Dataset) 和 [DataLoader](https://pytorch.org/docs/stable/data.html?highlight=dataloader#torch.utils.data.DataLoader) 來幫助你建立並訓練神經網路。為了充分利用它們的力量並且根據你的問題定製它們,你需要真正地準確瞭解它們在做什麼。為了建立這種理解,我們首先從這些模型(*models*)上不使用任何特性(*features*)在 MNIST 資料集上訓練一個基本的神經網路;我們將從最基本的 PyTorch Tensor 功能開始。然後,我們每次在 `torch.nn`,`torch.optim`,`Dataset` 或 `DataLoader` 逐漸地增加一個特性,準確地展示每一塊做的事情,並且它如何使程式碼更簡潔或更靈活。 **這篇博文假設你已經安裝了 PyTorch 並且熟悉 Tensor 操作的基礎。**(如果你熟悉 NumPy 陣列的操作,你會發現這裡使用的 PyTorch Tensor 操作幾乎相同。) ## MNISt 資料配置 我們將使用經典的 [MNIST](http://deeplearning.net/data/mnist/) 資料集,其是由手寫數字(從 0 到 9)的黑白影象組成。 我們將使用 [pathlib](https://docs.python.org/3/library/pathlib.html) 處理路徑(Python3 的標準庫之一),使用 [request](http://docs.python-requests.org/en/master/) 下載資料集。我們在每一步僅匯入使用的 modules,所以你可以準確地看到每一步在使用什麼。 ```python from pathlib import Path import requests DATA_PATH = Path("data") PATH = DATA_PATH / "mnist" PATH.mkdir(parents=True, exist_ok=True) URL = "https://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](https://yun.zyxweb.cn/index.php?explorer/share/file&hash=d2c000X2Zjdah_kFQGDi-NZlCu_arYoPxF1VNwj5PuujHo4ROpze1DHe&name=mnist.pkl.gz)。 這個資料集是儲存在 NumPy 陣列的格式,而且已經被 pickle 儲存,一種 Python 特有的序列化資料的格式。 ```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') ``` 每一張影象是 28x28,並且被儲存為展開的長度為 784(=28x28)的一行。讓我們看一個,首先,我們需要重新將形狀(*shape*)改為二維的。 ```python 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](https://tvax2.sinaimg.cn/large/006VTcCxly1gmqhrf7g0cj306z06wjr7.jpg) PyTorch 使用 `torch.tensor` 而不是 NumPy 陣列,所以我們需要轉換我們的資料。 ```python 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](https://course.fast.ai/) 學習它們)。 PyTorch 提供了建立隨機數或零值填充 Tensor 的方法,我們將使用它建立我們簡單線性模型的權重(*weight*)和偏置單元(*bias*)。這些只是普通的 Tensor,但是一個非常特殊的附加:我們告訴 PyTorch 它們需要梯度。這讓 PyTorch 記錄所有完成在 Tensor 上的操作,以便它在反向傳播時自動地計算梯度! 對於這些權重(*weight*),我們初始化之 **後** 設定 `requires_grad`,因為我們不希望這個步驟(初始化)被新增進梯度。(注意下劃線符號 `_`,在 PyTorch 中表明某個 Tensor 上的操作就地執行(*in-place*)。) > 這裡我們使用 [Xavier initialisation](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)(通過乘以 1/sqrt(n))方法初始化權重。 ```python 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 程式碼。 ```python 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*)。注意在這個階段我們的預測不比隨即預測好,因為我們是從隨機權重開始的。 ```python 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=) torch.Size([64, 10]) 正如你所見,`preds` Tensor 包含了不僅僅是 Tensor 中的值,同樣也有一個梯度函式。我們之後將使用它做反向傳播。 讓我們實現負對數似然(*negative log-likelihood*)作為我們的損失函式(再次說明,我們只使用原生的 Python)。 ```python def nll(input, target): return -input[range(target.shape[0]), target].mean() loss_func = nll ``` 讓我們檢視我們的隨機模型的損失值(*loss*),之後我們經過反向傳播之後看看是否得到了提升。 ```python yb = y_train[0:bs] print(loss_func(preds, yb)) ``` tensor(2.4096, grad_fn=) 讓我們實現一個函式計算我們模型的準確率。對於每一個預測,如果最大值的下標(*index*)和目標值一樣,那麼預測就是正確的。 ```python def accuracy(out, yb): preds = torch.argmax(out, dim=1) return (preds == yb).float().mean() ``` 同樣檢查我們隨機模型的準確率,並且在反向傳播之後查看準確率是否得到了提升。 ```python print(accuracy(preds, yb)) ``` tensor(0.0625) 我們現在可以執行訓練迴圈。對於每一迭代(*iteration*),我們將: - 選擇一個數據的批量(大小為 `bs`) - 使用模型做預測 - 計算損失值(*loss*) - `loss.backward()` 更新模型的梯度,在這個例子中,是 `weight` 和 `bias`。 我們使用這些梯度更新權重(*weight*)和偏移(*bias*)。我們在 `torch.no_grad()` 上下文管理器內做更新,因為我們不希望這些活動被記錄在我們的下一步梯度的計算。你可以在 [這裡](https://pytorch.org/docs/stable/notes/autograd.html) 檢視更多關於 PyTorch 的 autograd 記錄操作。 下一步我們將梯度設為 0,以便為下一個迴圈準備。否則,我們的梯度會記錄所有已經發生的運算的執行記錄(比如 `loss.backward()` 會累加梯度,無論裡面儲存了什麼,而不是替換)。 > 你可以使用標準的 Python 偵錯程式(*debugger*)單步除錯(*step through*)PyTorch 的程式碼,讓你可以檢查每一個步驟的變數值。 > > 取消註釋 `set_trace()` 嘗試它。 ```python 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*)將會下降並且準確率將會有所上升。 ```python print(loss_func(model(xb), yb), accuracy(model(xb), yb)) ``` tensor(0.0821, grad_fn=) 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*)。 ```python import torch.nn.functional as F loss_func = F.cross_entropy def model(xb): return xb @ weights + bias ``` 注意我們不再在 `model` 函式裡呼叫 `log_softmax` 函式。讓我們確認我們的損失值(*loss*)和準確率是否和之前一樣。 ```python print(loss_func(model(xb), yb), accuracy(model(xb), yb)) ``` tensor(0.0821, grad_fn=) 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](https://docs.python.org/3/tutorial/modules.html)(小寫 m)概念混淆,後者是可以被匯入的 Python 程式碼的一個檔案。 ```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 ``` 因為我們現在使用的是物件而不是使用函式,所以我們需要先例項化我們的模型。 ```python model = Mnist_Logistic() ``` 現在我們可以和以前一樣以相同的方法計算損失值(*loss*)。注意 `nn.Module` 的物件好像和函式一樣使用(即它們是可呼叫的(*callable*),而且在後臺 PyTorch 將自動呼叫我們的方法 `forward`。 ```python print(loss_func(model(xb), yb)) ``` tensor(3.0925, grad_fn=) 之前對於我們的訓練迴圈來說,我們必須通過變數名來更新每一個引數的值,並且要單獨地對每一個引數的梯度手動清零,就像這樣。 ```py 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` 裡)使得那些步驟更簡潔並且更不易於忘記我們的某些引數,尤其是當我們有一個更復雜的模型時。 ```py with torch.no_grad(): for p in model.parameters(): p -= p.grad() * lr model.zero_grad() ``` 我們將訓練迴圈封裝到一個 `fit` 函式,以便我們之後可以多次執行它。 ```python 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*)有所減少。 ```python print(loss_func(model(xb), yb)) ``` tensor(0.0814, grad_fn=) ## 使用 torch.Linear 重構 我們繼續重構我們的程式碼。作為手動定義和初始化 `self.weights` 和 `self.bias` 並且計算 `xb @ self.weights + self.bias` 的替代,我們將用 PyTorch 的類 [nn.Linear](https://pytorch.org/docs/stable/nn.html#linear-layers) 為一個為我們做所有事情的線性層(*linear layer*)。PyTorch 有許多層(*layers*)的型別,可以極大地簡化我們的程式碼,同樣也使其更快。 ```python 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*)。 ```python model = Mnist_Logistic() print(loss_func(model(xb), yb)) ``` tensor(2.3702, grad_fn=) 我們仍然能夠使用之前的 `fit` 方法。 ```python fit() print(loss_func(model(xb), yb)) ``` tensor(0.0813, grad_fn=) ## 使用 optim 重構 PyTorch 同樣有包含各種優化演算法的包 `torch.optim`。我們可以從我們的優化器(*optimizer*)使用 `step` 方法做一次傳播步驟,而不是手動更新每一個引數。 讓我們把之前手動更新的程式碼: ```py with torch.no_grad(): for p in model.parameters(): p -= p.grad * lr model.zero_grad() ``` 使用下面的程式碼替換: ```py opt.step() opt.zero_grad() ``` (`optim.zero_grad()` 將梯度設為 0 並且我們需要在下一個資料批量的計算梯度之前呼叫它。) ```python from torch import optim ``` 我們將定義一個小函式來建立我們的模型和優化器(*optimizer*)讓我們在之後可以重複使用它。 ```python 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=) tensor(0.0809, grad_fn=) ## 使用 Dataset 重構 PyTorch 有一個抽象 Dataset 類。Dataset 可以具有 `__len__` 函式(通過 Python 的標準 `len` 函式呼叫)和 `__getitem__` 函式作為對其索引的一種方法。[這個例子](https://pytorch.org/tutorials/beginner/data_loading_tutorial.html) 是一個非常好的例子來建立一個定製的繼承 `Dataset` 的 `FacialLandmarkDataset` 類。 PyTorch 的 [TensorDataset](https://pytorch.org/docs/stable/_modules/torch/utils/data/dataset.html#TensorDataset) 是一個封裝了 Dataset 的 Tensors。通過定義索引的長度和方式,這同樣給我們沿著 Tensor 的第一個維度迭代、索引和切片(*slice*)的方法。這讓我們訓練時更容易在同一行中訪問自變數和因變數。 ```python from torch.utils.data import TensorDataset ``` `x_train` 和 `y_train` 都可以被繫結在一個更容易迭代和切片(*slice*)的 `TensorDataset`。 ```python train_ds = TensorDataset(x_train, y_train) ``` 在之前,我們必須分別迭代 x 和 y 的資料批量值。 ```py xb = x_train[start_i:end_i] yb = y_train[start_i:end_i] ``` 現在,我們可以將這兩步合併到一步: ```py xb,yb = train_ds[i*bs : i*bs+bs] ``` ```python 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=) ## 使用 DataLoader 重構 PyTorch 的 `DataLoader` 負責管理資料批量。你可以從任何 `Dataset` 建立一個 `DataLoader`。`DataLoader` 讓迭代資料批量變得更簡單。而不是使用 `train_ds[i*bs : i*bs+bs]`,`DataLoader` 自動地給我們每一個數據批量。 ```python from torch.utils.data import DataLoader train_ds = TensorDataset(x_train, y_train) train_dl = DataLoader(train_ds, batch_size=bs) ``` 在之前,我們的迴圈迭代每一個數據批量(xb,yb)像這樣: ```py for i in range((n-1)//bs + 1): xb,yb = train_ds[i*bs : i*bs+bs] pred = model(xb) ``` 現在,我們的迴圈已經更簡潔了,(xb,yb)從 DataLoader 自動地載入: ```py for xb,yb in train_dl: pred = model(xb) ``` ```python 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=) 感謝 PyTorch 的 `nn.Module`,`nn.Parameter`,`Dataset` 和 `DataLoader`,我們的訓練迴圈現在顯著的小並且非常容易理解。讓我們現在嘗試增加在實際中建立高效的模型的基本特徵。 ## 增加驗證(Add validation) 在第一部分,我們只是嘗試建立一個合理的訓練迴圈以用於我們的訓練資料。在實際上,你總是應該有一個 [驗證集(*validation set*)](https://www.fast.ai/2017/11/13/validation-sets/),為了鑑別你是否過擬合(*overfitting*)。 洗亂(*shuffling*)訓練資料對於防止資料批量和過擬合之間的相關性(*correlation*)很 [重要](https://www.quora.com/Does-the-order-of-training-data-matter-when-training-neural-networks)。在另一方面,無論我們洗亂(*shuffle*)驗證集與否,驗證損失(*validation loss*)都是一樣的。由於洗亂(*shuffling*)花費額外的時間,洗亂(*shuffle*)驗證資料是沒有意義的。 我們將設定驗證集(*validation set*)的批量大小為訓練集的兩倍。這是因為驗證集不需要反向傳播並且佔用更少的記憶體(它不需要儲存梯度)。我們利用這一點使用大的資料批量並且更快地計算損失值(*loss*)。 ```python 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` 使用,確保對於不同的階段的適當的行為。) ```python 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*)不需要執行反向傳播。 ```python 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*)。 ```python 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。 ```python 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 行程式碼中。 ```python 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](https://pytorch.org/docs/stable/nn.html#torch.nn.Conv2d) 類作為我們的卷積層。我們定義一個有 3 層卷積層的卷積神經網路。每一個卷積後都跟一個 *ReLU*。在最後,我們執行一個平均池化(*average pooling*)。(注意 `view` 是 NumPy 版的 `reshape`。) ```python 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](https://cs231n.github.io/neural-networks-3/#sgd) 是一種隨機梯度下降(*stochastic gradient descent*)的變體,把之前的更新也考慮在內並且通常讓訓練更快。 ```python 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](https://pytorch.org/docs/stable/nn.html#torch.nn.Sequential)。一個 `Sequential` 物件以一種順序的方式執行包含在它之內的 *Modules*。這是一種寫神經網路更簡單的方式。 為了利用它,我們需要可以從一個給定的函式簡單地定義一個 **定製層(*custom layer*)**。舉個例子,PyTorch 沒有 *view* 層,我們需要為我們的神經網路建立一個。`Lambda` 將建立一層(*layer*),我們可以在使用 `Sequential` 定義一個神經網路的時候使用它。 ```python 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` 建立模型是簡單的。 ```python 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*)。 ```python 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。因此,我們的模型可以執行在任何尺寸的輸入上。 ```python 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) ``` 讓我們試它一試。 ```python 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 是否可以工作。 ```python print(torch.cuda.is_available()) ``` False 然後為它建立一個裝置物件(*device object*)。 ```python dev = torch.device( "cuda") if torch.cuda.is_available() else torch.device("cpu") ``` 讓我們更新 `preprocess` 將資料批量移進 GPU。 ```python 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。 ```python model.to(dev) opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9) ``` 你應該發現它執行的更快了。 ```python 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