原文地址:WHAT IS TORCH.NN REALLY?
本人英語學渣,如有錯誤請及時指出以便更正,使用的源碼可點擊原文地址進行下載。
pytorch提供了許多優雅的類和模塊幫助我們構建與訓練網絡,比如 torch.nn
, torch.optim
,Dataset
等。爲了充分利用這些模塊的功能,靈活操作它們解決各種不同的問題,我們需要更好地理解當我們調用這些模塊時它們到底幹了些什麼,爲此,我們首先不調用這些模塊實現MNIST手寫字識別,僅使用最基本的 pytorch 張量函數。然後,我們逐漸增加 torch.nn
, torch.optim
, Dataset
, or DataLoader
,具體地展示每個模塊具體幹了些什麼,展示這些模塊是怎樣使代碼變得更加優雅靈活。
此教程適用範圍:熟悉pytorch的張量操作
加載 MNIST 數據集
我們使用經典的 MNIST
數據集,一個包含了0-9數字的二值圖像庫。
還會用到 pathlib
庫用於目錄操作,一個python3自帶的標準庫。使用 requests
下載數據集。當用到一個模塊時纔會進行導入,而不會一開始全部導入,以便更好地理解每個步驟。
from pathlib import Path
import requests
DATA_PATH = Path('data')
PATH = DATA_PATH / "mnist"
PATH.mkdir(parents=True,exit_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)
該數據集採用numpy數組格式,並使用pickle存儲,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")
每張訓練圖片分辨率爲 28x28, 被存儲爲 784(=28x28) 的一行。我們輸出看一下數據,首先需要轉換回 28x28的圖像。
form 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
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())
從頭創建神經網絡(不使用torch.nn)
讓我們僅僅使用 pytorch 中的張量操作來創建模型,假設你已經熟悉神經網絡的基礎知識(不熟悉請參考corse.fast.ai )
pytorch提供了很多創建張量的操作,我們將用這些方法來初始化權值weights和偏置 bais來創建一個線性模型。這些只是常規張量,有一個非常特別的補充:我們告訴PyTorch這些張量需要支持求導(requires_grad=True)。這樣PyTorch將記錄在張量上完成的所有操作,以便它可以在反向傳播過程中自動計算梯度!
對於權值weights,我們再初始化之後再設置 requires_grad
,因爲我們不想這一步包含在梯度的計算中(注:pytorch中以 _
結尾的操作都是在原變量中(in-place)執行的)
import math
weights = torch.randn(780,10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
多虧了pytorch的自動求導功能,我們可以使用python的所有標準函數來構建模型。 我們這兒利用矩陣乘法,加法來構建線性模型。我們編寫 log_softmax
函數作爲激活函數。 雖然pytorch提供了大量寫好的損失函數,激活函數,你依然可以自由地編寫自己的函數替代它們。 pytorch 甚至支持創建自己的 GPU函數或者CPU矢量函數。
def log_softmax(x):
return x - x.exp().sum(-1).log().unsqueeze(-1)
def model(xb):
return log_softmax(xb @ weights + bias) # python的廣播機制
上面的 @
符號表示向量的點乘,接下來我們會調用一批數據(batch,64張圖片)輸入此模型。
bs = 64 # batch size
xb = x_train[0:bs] # a mini-batch from x
preds = model(xb) # predictions
print(preds[0],preds.shape)
out:
tensor([-2.4513, -2.5024, -2.0599, -3.1052, -3.2918, -2.2665, -1.9007, -2.2588,
-2.0149, -2.0287], grad_fn=<SelectBackward>) torch.Size([64, 10])
正如我們看到的,preds
張量不僅包含了一組張量,還包含了求導函數。反向傳播的時候會用到此函數。讓我們使用標準的python語句接着來實現 negative log likelihood loss 損失函數(譯者加:也被稱爲交叉熵損失函數):
def nll(input,target):
return -input[range(target.shape[0]),target].mean()
loss_func = nll
現在用我們的損失函數來檢查我們隨機初始化的模型,待會就能看到再反向傳播之後是否會改善模型性能。
yb = y_train[0:bs]
print(loss_func(preds,yb))
out:
tensor(2.3620, grad_fn=<NegBackward>)
接下來定義一個計算準確度的函數
def accuracy(out,yb):
preds = torch.argmax(out,dim=1) # 得到最大值的索引
return (preds == yb).float().mean()
檢查模型的準確度:
print(accuracy(preds, yb))
out:
tensor(0.0938)
現在我們開始循環訓練模型,每一步我們執行以下操作:
- 選擇一批數據(a batch)
- 使用模型進行預測
- 計算損失函數
- 反向傳播更新參數 weights 和 bias
我們現在使用 torch.no_grad()
更新參數,以避免參數更新過程被記錄入求導函數中。
然後我們清零導數,以便開始下一輪循環,否則導數會在原來的基礎上累加,而非替代原來的數
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_()
目前爲止,我們從頭創建一個迷你版的神經網絡
讓我們來檢查一下損失和準確率,並於迭代更新參數之前進行比較,我們期望得到更小的損失於更高的準確率。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
out:
tensor(0.0822, grad_fn=<NegBackward>) tensor(1.)
使用 torch.nn.functional 簡化代碼
現在我們使用torch.nn.functional
重構之前的代碼,這樣會使代碼變得更加簡潔與靈活,更易理解。
首先最簡單的一步是,用 torch.nn.functional
( 爲了方便後面統一稱作F) 中帶有的損失函數來代替我們自己編寫的函數,使得代碼變得更簡短。這些函數都包包含於模塊 torch.nn
裏面,除了大量的損失函數與激活函數,裏面還包含了大量用於構建網絡的函數。
如果我們的網絡中使用 negative log likelihood loss 作爲損失函數, log softmax activation 作爲激活函數 (即我們上面實現的損失函數與激活函數)。在pytorch中我們直接使用函數 F.cross_entropy
便可實現上面兩個函數的功能。所以我們可以用此函數代替上面實現的激活函數與損失函數。
import torch.nn.functional as F
loss_func = F.cross_entropy
def model(xb):
return xb @ weights + bias
讓我測試一下是否和上面自己實現的函數效果一致:
print(loss_func(model))
out:
tensor(0.0822, grad_fn=<NllLossBackward>) tensor(1.)
引入 nn.Module 重構代碼
接下來我們引入 nn.Module
和nn.Parameter
改進代碼。我們創建 nn.Module
的子類。這個例子中我們創建一個包含權重,偏置,以及包含前向傳播的類。nn.Module
含有許多的屬性與方法可供調用 (比如: .parameters
.zero_grad()
)
from torch import nn
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
sefl.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
接下來實例化我們的模型:
model = Mnist_Logistic()
現在我們可以和之前一樣使用損失函數了。注意:nn.Module
對象可以像函數一樣調用,但實際上是自動調用了對象內部的函數 forward
print(loss_func(model(xb),yb))
out:
tensor(2.2082, grad_fn=<NllLossBackward>)
在之前,我們必須進行如下得操作對權重,偏置進行更新,梯度清零:
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()
現在我們可以充分利用 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(epoches):
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()
讓我們再一次確認損失情況:
print(loss_func(model(xb),yb))
out:
tensor(0.0812, grad_fn=<NllLossBackward>)
引入 nn.Linear 重構代碼
比起手動定義 權重 與 偏置,並且使用 self.weights
和 self.bias
來計算 xb @ self.weights + self.bias
的方式,我們可以使用pytorch中的 nn.Linear
來定義線性層,他自動爲我們實現以上權重參數的定義以及計算的過程。除了線性模型之外,pytorch還有一系列的其它網絡層供我們使用,大大簡化了我們的編程過程。
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.lin = nn.Linear(784,10)
def forward(self,xb):
return self.lin(xb)
同上面一樣實例化模型,計算損失
model = Mnist_Logistic()
print(loss_func(model(xb),yb))
out:
tensor(2.2731, grad_fn=<NllLossBackward>)
訓練,並查看訓練之後的損失
fit()
print(loss_func(model(xb), yb))
out:
tensor(0.0820, grad_fn=<NllLossBackward>)
引入 optim 重構代碼
接下來使用torch.optim
改進訓練過程,而不用手動更新參數
之前的手動優化過程如下:
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()
使用如下代碼替代手動的參數更新:
opt.step()
# optim.zero_grad() resets the gradient to 0 and we need to call it
# before computing the gradient for the next minibatch.
opt.zero_grad()
結合之前的完整跟新代碼如下:
from torch import optim
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(epoches):
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))
out:
tensor(2.3785, grad_fn=<NllLossBackward>)
tensor(0.0802, grad_fn=<NllLossBackward>)
引入 Dataset 處理數據
pytorch定義了 Dataset 類,其中主要包含了 __len__
函數與 __getitem__
函數。此教程以創建 FacialLandmarkDataset
爲例詳細地介紹了Dataset類的使用。
pytorch的 TensorDataset
是一個包含張量的數據集。通過定義長度索引等方式,使我們更好地利用索引,切片等方法迭代數據。這會讓我們很容易地在一行代碼中獲取我們地數據。
form torch.utils.data import TensorDataset
x_train
y_train
可以被組合進一個TensorDataset
中,這會使得迭代切片更加簡單。
train_ds = TensorDataset(x_train,y_train)
之前我們獲取數據的方法如下:
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))
out:
tensor(0.0817, grad_fn=<NllLossBackward>)
引入DataLoader加載數據
DataLoader
用於批量加載數據,你可以用他來加載任何來自 Dataset
的數據,它使得數據的批量加載十分容易。
from torch.utils.data import DataLoader
train_ds = TensorDataset(x_train,y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
之前我們讀取數據的方式:
for i in range((n-1)//bs + 1):
xb,yb = train_ds[i*bs : i*bs+bs]
pred = model(xb)
現在使用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))
out:
tensor(0.0817, grad_fn=<NllLossBackward>)
目前爲止訓練模型部分我們就已經完成了,通過使用nn.Module
, nn.Parameter
, DataLoader
, 我們的訓練模型以及得到了很大的改進。接下來讓我們開始模型的測試部分。
添加測試集
在前一部分,我們嘗試了使用訓練集訓練網絡。實際工作中,我們還會使用測試集來觀察訓練的模型是否過擬合。
打亂數據的分佈有助於減小每一批(batch)數據間的關聯,有利於模型的泛化。但對於測試集來說,是否打亂數據對結果並沒有影響,反而會花費多餘的時間,所以我們沒有必要打亂測試集的數據。
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)
在每訓練完一輪數據(epoch)後我們輸出測試得到的損失值。
(注:如下代碼中,我們調用model.train()
和model.eval
表示進入訓練模式與測試模式,以保證模型運行的準確性)
model,opt = get_model()
for epoch in range(epoches):
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))
out:
0 tensor(0.3456)
1 tensor(0.2988)
創建 fit() 和 get_data() 優化代碼
我們再繼續做一點改進。因爲我們再計算訓練損失和驗證損失時執行了兩次相同的操作,所以我們用一個計算每一個batch損失的函數封裝這部分代碼。
我們爲訓練集添加優化器,並執行反向傳播。對於訓練集我們不添加優化器,當然也不會執行反向傳播。
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過程中訓練和驗證的必要操作
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.sum(np.multiply(losses, nums)). np.sum(nums))
print(epoch, val_loss)
現在,獲取數據加載模型進行訓練的整個過程只需要三行代碼便能實現了
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epoches, model, loss_func, opt, train_dl, valid_dl)
out:
0 0.2961075816631317
1 0.28558296990394594
我們可以用這簡單的三行代碼訓練各種模型。下面讓我們看看怎麼用它訓練一個卷積神經網絡。
使用卷積神經網絡
現在我們用三個卷積層來構造我們的卷積網絡。因爲之前的實現的函數都沒有假定模型形式,這兒我們依然可以使用它們而不需要任何修改。
我們pytorch預定義的Conv2d
類來構建我們的卷積層。我們模型有三層,每一層卷積之後都跟一個 ReLU,然後跟一個平均池化層。
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是隨機梯度下降的一個參數,它考慮到了之前的梯度值使得訓練更快。
model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
out:
0 0.3829730714321136
1 0.2258522843360901
使用 nn.Sequential 搭建網絡
torch.nn
還有另外一個方便的類可以簡化我們的代碼:Sequential
, 一個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
是一種簡化代碼的好方法。 一個Sequential
對象按順序執行包含在內的每一個module,使用它可以很方便地建立一個網絡。
爲了更好地使用Sequential
模塊,我們需要自定義 pytorch中沒實現地module。例如pytorch中沒有自帶 改變張量形狀地層,我們創建 Lambda
層,以便在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)
out:
0 0.32739396529197695
1 0.25574398956298827
簡易的DataLoader
我們的網絡以及足夠精簡了,但是隻能適用於MNIST數據集,因爲
- 網絡默認輸入爲 28x28 的張量
- 網絡默認最後一個卷積層大小爲 4x4 (因爲我們的池化層大小爲4x4)
現在我們去除這兩個假設,使得網絡可以適用於所有的二維圖像。首先我們移除最初的 Lambda
層,用數據預處理層替代。
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.AdaptiveAvgPool2d
代替nn.AvgPool2d
。它允許我們自定義輸出張量的維度,而於輸入的張量無關。這樣我們的網絡便可以適用於各種size的網絡。
model = nn.Sequential(
nn.Conv2d(1, 16, kernal_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)
out:
0 0.32888883714675904
1 0.31000419993400574
使用GPU
如果你的電腦有支持CUDA的GPU(你可以很方便地以 0.5美元/小時 的價格租到支持的雲服務器),便可以使用GPU加速訓練過程。首先檢測設備是否正常支持GPU:
print(torch.cuda.is_available())
out:
Ture
接着創建一個設備對象:
dev = torch.device(
"cuda") if torch.cuda.is_available() else torch.device("cpu")
更新 preprocess(x,y)
把數據移到GPU:
def preprocess(x, y):
return x.view(-1, 1, 28, 28).to(dev), y.to(device)
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)
out:
0 0.21190375366210937
1 0.18018000435829162
總結
我們現在得到了一個通用的數據加載和模型訓練方法,我們可以在pytorch種用這種方法訓練大多的模型。想知道訓練一個模型有多簡單,回顧一下本次的代碼便可以了。
當然,除此之外本篇內容還有很多需求沒有講到,比如數據增強,超參調試,數據監控(monitoring training),遷移學習等。這些特點都以與本篇教程相似的設計方法包含於 fastai庫中。
本篇教程開頭我們承諾將會通過例程解釋 torch.nn
torch.optim
Dataset
DataLoader
等模塊,下面我們就這些模型進行總結。
- torch.nn
- Module: 創建一個可以像函數一樣調用地對象,包含了網絡的各種狀態,可以使用
parameter
方便地獲取模型地參數,並有清零梯度,循環更新參數等功能。 - Parameter: 將模型中需要更新的參數全部打包,方便反向傳播過程中進行更新。有
requires_grad
屬性的參數纔會被更新。 - functional:通常導入爲
F
,包含了許多激活函數,損失函數等。
- Module: 創建一個可以像函數一樣調用地對象,包含了網絡的各種狀態,可以使用
- torch.optim: 包含了很多諸如
SGD
一樣的優化器,用來在反向傳播中跟新參數 - Dataset: 一個帶有
__len__
__getitem__
等函數的抽象接口。裏面包含了TensorDataset
等類。 - DataLoader: 輸入任意的
Dataset
並按批(batch)迭代輸出數據。