深入理解 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 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)
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 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.Modulenn.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.weightsself.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,包含了許多激活函數,損失函數等。
  • torch.optim: 包含了很多諸如SGD一樣的優化器,用來在反向傳播中跟新參數
  • Dataset: 一個帶有 __len__ __getitem__等函數的抽象接口。裏面包含了 TensorDataset等類。
  • DataLoader: 輸入任意的 Dataset 並按批(batch)迭代輸出數據。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章