torch.nn 的本質
阿新 • • 發佈:2021-01-17
> 本文翻譯自 [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