深入理解 TORCH.NN
原文地址: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 requestsDATA_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 gzipwith 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 nppyplot.imshow(x_train[0].reshape((28,28)),cmap="gray") print(x_train.shape) out: (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 mathweights = 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_tracelr = 0.5 # learning rate epochs = 2 # how many epochs to train forfor epoch in range(epochs):for i in range((n - 1) // bs + 1):# set_trace()start_i = i * bsend_i = start_i + bsxb = 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 * lrbias -= bias.grad * lrweights.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 Floss_func = F.cross_entropydef 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 nnclass 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 * lrbias -= bias.grad * lrweights.grad.zero_()bias.grad.zero_()現在我們可以充分利用 nn.Module 的方法屬性更簡單地完成這些操作,如下所示:
with torch.no_grad():for p in model.parameters(): p -= p.grad * lrmodel.zero_grad()現在我們將整個訓練過程寫進函數 fit中。
def fit():for epoch in range(epoches):for i in range((n - 1) // bs + 1):start_i = i * bsend_i = start_i + bsxb = 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 * lrmodel.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 * lrmodel.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 optimdef 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 *bsend_i = start_i + bsxb = 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 TensorDatasetx_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 DataLoadertrain_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表示進入訓練模式與測試模式,以保證模型運行的準確性)
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 = funcdef 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), yclass WrappedDataLoader:def __init__(self, dl, func):self.dl = dlself.func = funcdef __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,包含了許多激活函數,損失函數等。
- torch.optim: 包含了很多諸如SGD一樣的優化器,用來在反向傳播中跟新參數
- Dataset: 一個帶有 __len__ __getitem__等函數的抽象接口。里面包含了 TensorDataset等類。
- DataLoader: 輸入任意的 Dataset 并按批(batch)迭代輸出數據。
附錄
完整代碼下載地址
- Download Python source code: nn_tutorial.py
- Download Jupyter notebook: nn_tutorial.ipynb
總結
以上是生活随笔為你收集整理的深入理解 TORCH.NN的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019复旦大学计算机分数线,2019复
- 下一篇: 2073:【例2.16 】三角形面积