PyTorch torch.nn 到底是什么?

2020-09-10 15:21 更新
原文: https://pytorch.org/tutorials/beginner/nn_tutorial.html

作者:杰里米·霍華德(Jeremy Howard), fast.ai 。 感謝 Rachel Thomas 和 Francisco Ingham。

我們建議將本教程作為筆記本而不是腳本來運(yùn)行。 要下載筆記本(.ipynb)文件,請單擊頁面頂部的鏈接。

PyTorch 提供設(shè)計優(yōu)雅的模塊和類 torch.nn , torch.optim , Dataset 和 DataLoader 來幫助您創(chuàng)建和訓(xùn)練神經(jīng)網(wǎng)絡(luò)。 為了充分利用它們的功能并針對您的問題對其進(jìn)行自定義,您需要真正地了解他們的工作。 為了建立這種理解,我們將首先在 MNIST 數(shù)據(jù)集上訓(xùn)練基本神經(jīng)網(wǎng)絡(luò),而無需使用這些模型的任何功能; 我們最初只會使用最基本的 PyTorch 張量功能。 然后,我們將一次從torch.nn,torch.optim,DatasetDataLoader中逐個添加一個功能,確切地顯示每個功能,以及如何使代碼更簡潔或更靈活。

本教程假定您已經(jīng)安裝了 PyTorch,并且熟悉張量操作的基礎(chǔ)知識。 (如果您熟悉 Numpy 數(shù)組操作,將會發(fā)現(xiàn)此處使用的 PyTorch 張量操作幾乎相同)。

MNIST 數(shù)據(jù)設(shè)置

我們將使用經(jīng)典的 MNIST 數(shù)據(jù)集,該數(shù)據(jù)集由手繪數(shù)字的黑白圖像組成(介于 0 到 9 之間)。

我們將使用 pathlib 處理路徑(Python 3 標(biāo)準(zhǔn)庫的一部分),并下載數(shù)據(jù)集。 我們只會在使用模塊時才導(dǎo)入它們,因此您可以確切地看到正在使用模塊的每個細(xì)節(jié)。

from pathlib import Path
import requests


DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"


PATH.mkdir(parents=True, exist_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)

該數(shù)據(jù)集為 numpy 數(shù)組格式,并已使用 pickle(一種用于序列化數(shù)據(jù)的 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")

每個圖像為 28 x 28,并存儲被拍平長度為 784(= 28x28)的向量。 讓我們來看一個; 我們需要先將其重塑為 2d。

from matplotlib import pyplot
import numpy as np


pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)

../_images/sphx_glr_nn_tutorial_001.png

得出:

(50000, 784)

PyTorch 使用torch.tensor而不是 numpy 數(shù)組,因此我們需要轉(zhuǎn)換數(shù)據(jù)。

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())

得出:

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)

從零開始的神經(jīng)網(wǎng)絡(luò)(無 torch.nn)

首先,我們僅使用 PyTorch 張量操作創(chuàng)建模型。 我們假設(shè)您已經(jīng)熟悉神經(jīng)網(wǎng)絡(luò)的基礎(chǔ)知識。 (如果您不是,則可以在 course.fast.ai 中學(xué)習(xí)它們)。

PyTorch 提供了創(chuàng)建隨機(jī)或零填充張量的方法,我們將使用它們來為簡單的線性模型創(chuàng)建權(quán)重和偏差。 這些只是常規(guī)張量,還有一個非常特殊的附加值:我們告訴 PyTorch 它們需要梯度。 這使 PyTorch 記錄了在張量上完成的所有操作,因此它可以在反向傳播時自動地計算梯度!

對于權(quán)重,我們在初始化之后設(shè)置requires_grad ,因?yàn)槲覀儾幌M摬襟E包含在梯度中。 (請注意,PyTorch 中的尾隨_表示該操作是就地執(zhí)行的。)

Note

我們在這里用 Xavier 初始化(通過乘以 1 / sqrt(n))來初始化權(quán)重。

import math


weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)

由于 PyTorch 具有自動計算梯度的功能,我們可以將任何標(biāo)準(zhǔn)的 Python 函數(shù)(或可調(diào)用對象)用作模型! 因此,讓我們編寫一個簡單的矩陣乘法和廣播加法來創(chuàng)建一個簡單的線性模型。 我們還需要激活函數(shù),因此我們將編寫并使用 $log_softmax$ 。 請記?。罕M管 PyTorch 提供了許多預(yù)先編寫的損失函數(shù),激活函數(shù)等,但是您可以使用純 Python 輕松編寫自己的函數(shù)。 PyTorch 甚至?xí)詣訛槟暮瘮?shù)創(chuàng)建快速 GPU 或矢量化的 CPU 代碼。

def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)


def model(xb):
    return log_softmax(xb @ weights + bias)

在上面,@代表點(diǎn)積運(yùn)算。 我們將對一批數(shù)據(jù)(在這種情況下為 64 張圖像)調(diào)用函數(shù)。 這是一個前向傳播。 請注意,由于我們從隨機(jī)權(quán)重開始,因此在這一階段,我們的預(yù)測不會比隨機(jī)預(yù)測更好。

bs = 64  # batch size


xb = x_train[0:bs]  # a mini-batch from x
preds = model(xb)  # predictions
preds[0], preds.shape
print(preds[0], preds.shape)

Out:

tensor([-2.0790, -2.6699, -2.2096, -1.6754, -1.7844, -2.8664, -2.2463, -2.7637,
        -3.0813, -2.6712], grad_fn=<SelectBackward>) torch.Size([64, 10])

如您所見,preds張量不僅包含張量值,還包含梯度函數(shù)。 稍后我們將使用它進(jìn)行反向傳播。

讓我們實(shí)現(xiàn)負(fù)對數(shù)似然作為損失函數(shù)(同樣,我們只能使用標(biāo)準(zhǔn) Python):

def nll(input, target):
    return -input[range(target.shape[0]), target].mean()


loss_func = nll

讓我們用隨機(jī)模型來檢查損失,以便我們以后看向后傳播后是否可以改善。

yb = y_train[0:bs]
print(loss_func(preds, yb))

得出:

tensor(2.3076, grad_fn=<NegBackward>)

我們還實(shí)現(xiàn)一個函數(shù)來計算模型的準(zhǔn)確性。 對于每個預(yù)測,如果具有最大值的索引與目標(biāo)值匹配,則該預(yù)測是正確的。

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

讓我們檢查一下隨機(jī)模型的準(zhǔn)確性,以便我們可以看出隨著損失的增加,準(zhǔn)確性是否有所提高。

print(accuracy(preds, yb))

得出:

tensor(0.1250)

現(xiàn)在,我們可以運(yùn)行一個訓(xùn)練循環(huán)。 對于每次迭代,我們將:

  • 選擇一個小批量數(shù)據(jù)(大小為bs
  • 使用模型進(jìn)行預(yù)測
  • 計算損失
  • loss.backward()更新模型的梯度,在這種情況下為weightsbias。

現(xiàn)在,我們使用這些梯度來更新權(quán)重和偏差。 我們在torch.no_grad()上下文管理器中執(zhí)行此操作,因?yàn)槲覀儾幌M谙乱徊降奶荻扔嬎阒杏涗涍@些操作。 您可以在上閱讀有關(guān) PyTorch 的 Autograd 如何記錄操作的更多信息。

然后,將梯度設(shè)置為零,以便為下一個循環(huán)做好準(zhǔn)備。 否則,我們的梯度會記錄所有已發(fā)生操作的運(yùn)行記錄(即loss.backward() 將梯度添加到已存儲的內(nèi)容中,而不是替換它們)。

TIP

您可以使用標(biāo)準(zhǔn)的 python 調(diào)試器逐步瀏覽 PyTorch 代碼,從而可以在每一步檢查各種變量值。 取消注釋以下set_trace()即可嘗試。

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_()

就是這樣:我們完全從頭開始創(chuàng)建并訓(xùn)練了一個最小的神經(jīng)網(wǎng)絡(luò)(在這種情況下,是邏輯回歸,因?yàn)槲覀儧]有隱藏的層)!

讓我們檢查損失和準(zhǔn)確性,并將其與我們之前獲得的進(jìn)行比較。 我們希望損失會減少,準(zhǔn)確性會增加,而且確實(shí)如此。

print(loss_func(model(xb), yb), accuracy(model(xb), yb))

得出:

tensor(0.0799, grad_fn=<NegBackward>) tensor(1.)

使用 torch.nn.functional

現(xiàn)在,我們將重構(gòu)代碼,使其與以前相同,只是我們將開始利用 PyTorch 的nn類使其更加簡潔和靈活。 從這里開始的每一步,我們都應(yīng)該使代碼中的一個或多個:更短,更易理解和/或更靈活。

第一步也是最簡單的步驟,就是用torch.nn.functional(通常按照慣例將其導(dǎo)入到名稱空間F中)替換我們的手寫激活和損失函數(shù),從而縮短代碼長度。 該模塊包含torch.nn庫中的所有函數(shù)(而該庫的其他部分包含類)。 除了廣泛的損失和激活函數(shù)外,您還會在這里找到一些合適的函數(shù)來創(chuàng)建神經(jīng)網(wǎng)絡(luò),例如池化函數(shù)。 (還有一些用于進(jìn)行卷積,線性圖層等的函數(shù),但是正如我們將看到的那樣,通??梢允褂脦斓钠渌糠謥砀玫靥幚磉@些函數(shù)。)

如果您使用的是負(fù)對數(shù)似然損失和 log softmax 激活,那么 Pytorch 會提供將兩者結(jié)合的單個函數(shù)F.cross_entropy。 因此,我們甚至可以從模型中刪除激活函數(shù)。

import torch.nn.functional as F


loss_func = F.cross_entropy


def model(xb):
    return xb @ weights + bias

請注意,我們不再在model函數(shù)中調(diào)用log_softmax。 讓我們確認(rèn)我們的損失和準(zhǔn)確性與以前相同:

print(loss_func(model(xb), yb), accuracy(model(xb), yb))

得出:

tensor(0.0799, grad_fn=<NllLossBackward>) tensor(1.)

使用 nn.Module 進(jìn)行重構(gòu)

接下來,我們將使用nn.Modulenn.Parameter進(jìn)行更清晰,更簡潔的訓(xùn)練循環(huán)。 我們將nn.Module子類化(它本身是一個類并且能夠跟蹤狀態(tài))。 在這種情況下,我們要創(chuàng)建一個類,該類包含前進(jìn)步驟的權(quán)重,偏差和方法。 nn.Module具有許多我們將要使用的屬性和方法(例如.parameters().zero_grad())。

Note

nn.Module(大寫 M)是 PyTorch 的特定概念,也是我們將經(jīng)常使用的一個類。 nn.Module不要與(小寫m模塊的 Python 概念混淆,該模塊是可以導(dǎo)入的 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.zeros(10))


    def forward(self, xb):
        return xb @ self.weights + self.bias

由于我們現(xiàn)在使用的是對象而不是僅使用函數(shù),因此我們首先必須實(shí)例化模型:

model = Mnist_Logistic()

現(xiàn)在我們可以像以前一樣計算損失。 請注意,nn.Module對象的使用就像它們是函數(shù)一樣(即,它們是可調(diào)用的),但是在后臺 Pytorch 會自動調(diào)用我們的forward方法。

print(loss_func(model(xb), yb))

得出:

tensor(2.4205, grad_fn=<NllLossBackward>)

以前,在我們的訓(xùn)練循環(huán)中,我們必須按名稱更新每個參數(shù)的值,并手動將每個參數(shù)的 grads 分別歸零,如下所示:

with torch.no_grad():
    weights -= weights.grad * lr
    bias -= bias.grad * lr
    weights.grad.zero_()
    bias.grad.zero_()

現(xiàn)在我們可以利用 model.parameters()和 model.zero_grad()(它們都由 PyTorch 為nn.Module定義)來使這些步驟更簡潔,并且更不會出現(xiàn)忘記某些參數(shù)的錯誤,特別是在 我們有一個更復(fù)雜的模型:

with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

我們將把小的訓(xùn)練循環(huán)包裝在fit函數(shù)中,以便稍后再運(yùn)行。

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()

讓我們仔細(xì)檢查一下我們的損失是否下降了:

print(loss_func(model(xb), yb))

得出:

tensor(0.0796, grad_fn=<NllLossBackward>)

使用 nn.Linear 重構(gòu)

我們繼續(xù)重構(gòu)我們的代碼。 代替手動定義和初始化self.weightsself.bias并計算xb @ self.weights + self.bias,我們將對線性層使用 Pytorch 類 nn.Linear ,這將為我們完成所有工作。 Pytorch 具有許多類型的預(yù)定義層,可以大大簡化我們的代碼,并且通常也可以使其速度更快。

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)


    def forward(self, xb):
        return self.lin(xb)

我們用與以前相同的方式實(shí)例化模型并計算損失:

model = Mnist_Logistic()
print(loss_func(model(xb), yb))

得出:

tensor(2.3077, grad_fn=<NllLossBackward>)

我們?nèi)匀豢梢允褂门c以前相同的fit方法。

fit()


print(loss_func(model(xb), yb))

得出:

tensor(0.0824, grad_fn=<NllLossBackward>)

使用優(yōu)化重構(gòu)

Pytorch 還提供了一個包含各種優(yōu)化算法的軟件包torch.optim。 我們可以使用優(yōu)化器中的step方法采取向前的步驟,而不是手動更新每個參數(shù)。

這就是我們將要替換之前手動編碼的優(yōu)化步驟:

with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

我們只需使用下面的代替:

opt.step()
opt.zero_grad()

(optim.zero_grad()將梯度重置為 0,我們需要在計算下一個小批量的梯度之前調(diào)用它。)

from torch import optim

我們將定義一個小函數(shù)來創(chuàng)建模型和優(yōu)化器,以便將來再次使用。

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.2542, grad_fn=<NllLossBackward>)
tensor(0.0811, grad_fn=<NllLossBackward>)

使用數(shù)據(jù)集進(jìn)行重構(gòu)

PyTorch 有一個抽象的 Dataset 類。 數(shù)據(jù)集可以是具有__len__函數(shù)(由 Python 的標(biāo)準(zhǔn)len函數(shù)調(diào)用)和具有__getitem__函數(shù)作為對其進(jìn)行索引的一種方法。 本教程演示了一個不錯的示例,該示例創(chuàng)建一個自定義FacialLandmarkDataset類作為Dataset的子類。

PyTorch 的 TensorDataset 是一個數(shù)據(jù)集包裝張量。 通過定義索引的長度和方式,這也為我們提供了沿張量的一維進(jìn)行迭代,索引和切片的方法。 這將使我們在訓(xùn)練的同一行中更容易訪問自變量和因變量。

from torch.utils.data import TensorDataset

x_trainy_train都可以合并為一個TensorDataset,這將更易于迭代和切片。

train_ds = TensorDataset(x_train, y_train)

以前,我們不得不分別遍歷 x 和 y 值的迷你批處理:

xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]

現(xiàn)在,我們可以將兩個步驟一起執(zhí)行:

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))

得出:

tensor(0.0819, grad_fn=<NllLossBackward>)

使用 DataLoader 進(jìn)行重構(gòu)

Pytorch 的DataLoader負(fù)責(zé)批次管理。 您可以從任何Dataset創(chuàng)建一個DataLoader。 DataLoader使迭代迭代變得更加容易。 不必使用train_ds[i*bs : i*bs+bs],DataLoader 會自動為我們提供每個小批量。

from torch.utils.data import DataLoader


train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)

以前,我們的循環(huán)遍歷批處理(xb,yb),如下所示:

for i in range((n-1)//bs + 1):
    xb,yb = train_ds[i*bs : i*bs+bs]
    pred = model(xb)

現(xiàn)在,我們的循環(huán)更加簡潔了,因?yàn)?xb,yb)是從數(shù)據(jù)加載器自動加載的:

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))

得出:

tensor(0.0822, grad_fn=<NllLossBackward>)

得益于 Pytorch 的nn.Modulenn.ParameterDataset和DataLoader,我們的訓(xùn)練循環(huán)現(xiàn)在變得更小,更容易理解。 現(xiàn)在,讓我們嘗試添加在實(shí)踐中創(chuàng)建有效模型所需的基本功能。

添加驗(yàn)證

在第 1 部分中,我們只是試圖建立一個合理的訓(xùn)練循環(huán)以用于我們的訓(xùn)練數(shù)據(jù)。 實(shí)際上,您總是也應(yīng)該具有驗(yàn)證集,以便識別您是否過度擬合。

打亂訓(xùn)練數(shù)據(jù)順序?qū)τ诜乐古闻c過度擬合之間的相關(guān)性很重要。 另一方面,無論我們是否打亂驗(yàn)證集,驗(yàn)證損失都是相同的。 由于打亂順序需要花費(fèi)更多時間,因此打亂驗(yàn)證集數(shù)據(jù)順序沒有任何意義。

我們將驗(yàn)證集的批次大小設(shè)為訓(xùn)練集的兩倍。 這是因?yàn)轵?yàn)證集不需要反向傳播,因此占用的內(nèi)存更少(不需要存儲漸變)。 我們利用這一優(yōu)勢來使用更大的批量,并更快地計算損失。

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 結(jié)束時計算并打印驗(yàn)證損失。

(請注意,我們總是在訓(xùn)練之前調(diào)用model.train(),并在推斷之前調(diào)用model.eval(),因?yàn)橹T如nn.BatchNorm2dnn.Dropout之類的圖層會使用它們,以確保這些不同階段的行為正確。)

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.2903)
1 tensor(0.3343)

創(chuàng)建 fit()和 get_data()

現(xiàn)在,我們將自己進(jìn)行一些重構(gòu)。 由于我們經(jīng)歷了兩次相似的過程來計算訓(xùn)練集和驗(yàn)證集的損失,因此我們將其設(shè)為自己的函數(shù)loss_batch,該函數(shù)可計算一批損失。

我們將優(yōu)化器傳入訓(xùn)練集中,并使用它執(zhí)行反向傳播。 對于驗(yàn)證集,我們沒有通過優(yōu)化程序,因此該方法不會執(zhí)行反向傳播。

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運(yùn)行必要的操作來訓(xùn)練我們的模型,并計算每個時期的訓(xùn)練和驗(yàn)證損失。

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返回用于訓(xùn)練和驗(yàn)證集的數(shù)據(jù)加載器。

def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )

現(xiàn)在,我們獲取數(shù)據(jù)加載器和擬合模型的整個過程可以在 3 行代碼中運(yùn)行:

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.34931180425286296
1 0.28620736759901044

您可以使用這些基本的 3 行代碼來訓(xùn)練各種各樣的模型。 讓我們看看是否可以使用它們來訓(xùn)練卷積神經(jīng)網(wǎng)絡(luò)(CNN)!

切換到 CNN

現(xiàn)在,我們將構(gòu)建具有三個卷積層的神經(jīng)網(wǎng)絡(luò)。 由于上一節(jié)中的所有函數(shù)都不包含任何有關(guān)模型組合的內(nèi)容,因此我們將能夠使用它們來訓(xùn)練 CNN,而無需進(jìn)行任何修改。

我們將使用 Pytorch 的預(yù)定義 Conv2d 類作為我們的卷積層。 我們定義具有 3 個卷積層的 CNN。 每個卷積后跟一個 ReLU。 最后,我們執(zhí)行平均池化。 (請注意,view是 numpy 的reshape的 PyTorch 版本)

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

動量是隨機(jī)梯度下降的一種變體,它也考慮了以前的更新,通??梢约涌煊?xùn)練速度。

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.33537127304077147
1 0.24059089585542678

nn.Sequential

torch.nn還有另一個靈活的類,可以用來簡化我們的代碼: Sequential 。 Sequential對象以順序方式運(yùn)行其中包含的每個模塊。 這是編寫神經(jīng)網(wǎng)絡(luò)的一種簡單方法。

要利用此優(yōu)勢,我們需要能夠從給定的函數(shù)輕松定義自定義層。 例如,PyTorch 沒有視圖圖層,我們需要為網(wǎng)絡(luò)創(chuàng)建一個圖層。 Lambda將創(chuàng)建一個層,然后在使用Sequential定義網(wǎng)絡(luò)時可以使用該層。

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創(chuàng)建的模型很簡單:

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.4098783682346344
1 0.2799181687355041

包裝 DataLoader

雖然我們的 CNN 網(wǎng)絡(luò)很簡潔,但是它只能在 MNIST 數(shù)據(jù)集上面有效,因?yàn)?/p>

  • MNIST 數(shù)據(jù)集假設(shè)輸入為 28 * 28 長向量
  • MNIST 數(shù)據(jù)集假設(shè) CNN 的最終網(wǎng)格尺寸為 4 * 4(這是因?yàn)?/li>

我們使用的平均池化卷積核的大小)

讓我們擺脫這兩個假設(shè),因此我們的模型需要適用于任何 2d 單通道圖像。 首先,我們可以刪除初始的 Lambda 層,但將數(shù)據(jù)預(yù)處理移至生成器中:

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,這使我們可以定義所需的輸出張量的大小,而不是所需的輸入張量的大小。 結(jié)果,我們的模型將適用于任何大小的輸入。

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, 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)

試試看:

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

得出:

0 0.34252993125915526
1 0.28579100420475007

使用您的 GPU

如果您足夠幸運(yùn)地能夠使用具有 CUDA 功能的 GPU(您可以從大多數(shù)云提供商處以每小時$ 0.50 的價格租用一個 GPU),則可以使用它來加速代碼。 首先檢查您的 GPU 是否在 Pytorch 中正常工作:

print(torch.cuda.is_available())

得出:

True

然后為其創(chuàng)建一個設(shè)備對象:

dev = torch.device(
    "cuda") if torch.cuda.is_available() else torch.device("cpu")

讓我們更新preprocess,將批次移至 GPU:

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。

model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

您應(yīng)該發(fā)現(xiàn)它現(xiàn)在運(yùn)行得更快:

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

得出:

0 0.1909765040397644
1 0.180943009185791

總結(jié)思想

現(xiàn)在,我們有了一個通用的數(shù)據(jù)管道和訓(xùn)練循環(huán),您可以將其用于使用 Pytorch 訓(xùn)練多種類型的模型。 要了解現(xiàn)在可以輕松進(jìn)行模型訓(xùn)練,請查看 mnist_sample 示例筆記本。

當(dāng)然,您需要添加很多內(nèi)容,例如數(shù)據(jù)增強(qiáng),超參數(shù)調(diào)整,監(jiān)控訓(xùn)練,轉(zhuǎn)移學(xué)習(xí)等。 這些功能在 fastai 庫中可用,該庫是使用本教程中所示的相同設(shè)計方法開發(fā)的,為希望進(jìn)一步推廣模型的從業(yè)人員提供了自然的下一步。

我們承諾在本教程開始時將通過示例分別說明torch.nn,torch.optim,DatasetDataLoader。 因此,讓我們總結(jié)一下我們所看到的:

torch.nnModule:創(chuàng)建一個類似函數(shù)行為功能的,但可以包含狀態(tài)(例如神經(jīng)網(wǎng)絡(luò)層權(quán)重)的可調(diào)用對象。它知道它包含的Parameter,并且可以將其所有梯度歸零,通過其循環(huán)進(jìn)行權(quán)重更新等 。Parameter:張量的包裝器,它告訴Module具有在反向傳播期間需要更新的權(quán)重。僅更新具有 require_grad 屬性集的張量functional:一個模塊(通常按照常規(guī)導(dǎo)入到F名稱空間中),包含激活函數(shù),損失函數(shù)等。以及卷積和線性層之類的無狀態(tài)版本。torch.optim:包含其中SGD之類的優(yōu)化程序,這些優(yōu)化程序可以在反向傳播期間更新權(quán)重參數(shù)Dataset:一個具有__len____getitem__的抽象接口對象,包括 Pytorch 提供的類,例如TensorDatasetDataLoader:獲取任何Dataset并創(chuàng)建一個迭代器,該迭代器返回批量數(shù)據(jù)。

腳本的總運(yùn)行時間:(1 分鐘 7.131 秒)

Download Python source code: nn_tutorial.py Download Jupyter notebook: nn_tutorial.ipynb


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號