PyTorch torch.nn 到底是什么?

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

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

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

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

本教程假定您已經(jīng)安裝了 PyTorch,并且熟悉張量操作的基礎(chǔ)知識(shí)。 (如果您熟悉 Numpy 數(shù)組操作,將會(huì)發(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)庫(kù)的一部分),并下載數(shù)據(jù)集。 我們只會(huì)在使用模塊時(shí)才導(dǎo)入它們,因此您可以確切地看到正在使用模塊的每個(gè)細(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 特定格式)存儲(chǔ)。

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

每個(gè)圖像為 28 x 28,并存儲(chǔ)被拍平長(zhǎng)度為 784(= 28x28)的向量。 讓我們來(lái)看一個(gè); 我們需要先將其重塑為 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)

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

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

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

對(duì)于權(quán)重,我們?cè)诔跏蓟笤O(shè)置requires_grad ,因?yàn)槲覀儾幌M摬襟E包含在梯度中。 (請(qǐng)注意,PyTorch 中的尾隨_表示該操作是就地執(zhí)行的。)

Note

我們?cè)谶@里用 Xavier 初始化(通過(guò)乘以 1 / sqrt(n))來(lái)初始化權(quán)重。

import math


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

由于 PyTorch 具有自動(dòng)計(jì)算梯度的功能,我們可以將任何標(biāo)準(zhǔn)的 Python 函數(shù)(或可調(diào)用對(duì)象)用作模型! 因此,讓我們編寫(xiě)一個(gè)簡(jiǎn)單的矩陣乘法和廣播加法來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的線性模型。 我們還需要激活函數(shù),因此我們將編寫(xiě)并使用 $log_softmax$ 。 請(qǐng)記?。罕M管 PyTorch 提供了許多預(yù)先編寫(xiě)的損失函數(shù),激活函數(shù)等,但是您可以使用純 Python 輕松編寫(xiě)自己的函數(shù)。 PyTorch 甚至?xí)詣?dòng)為您的函數(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)算。 我們將對(duì)一批數(shù)據(jù)(在這種情況下為 64 張圖像)調(diào)用函數(shù)。 這是一個(gè)前向傳播。 請(qǐng)注意,由于我們從隨機(jī)權(quán)重開(kāi)始,因此在這一階段,我們的預(yù)測(cè)不會(huì)比隨機(jī)預(yù)測(cè)更好。

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

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

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

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


loss_func = nll

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

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

得出:

tensor(2.3076, grad_fn=<NegBackward>)

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

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)行一個(gè)訓(xùn)練循環(huán)。 對(duì)于每次迭代,我們將:

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

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

然后,將梯度設(shè)置為零,以便為下一個(gè)循環(huán)做好準(zhǔn)備。 否則,我們的梯度會(huì)記錄所有已發(fā)生操作的運(yùn)行記錄(即loss.backward() 將梯度添加到已存儲(chǔ)的內(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_()

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

讓我們檢查損失和準(zhǔn)確性,并將其與我們之前獲得的進(jìn)行比較。 我們希望損失會(huì)減少,準(zhǔn)確性會(huì)增加,而且確實(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)代碼,使其與以前相同,只是我們將開(kāi)始利用 PyTorch 的nn類(lèi)使其更加簡(jiǎn)潔和靈活。 從這里開(kāi)始的每一步,我們都應(yīng)該使代碼中的一個(gè)或多個(gè):更短,更易理解和/或更靈活。

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

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

import torch.nn.functional as F


loss_func = F.cross_entropy


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

請(qǐng)注意,我們不再在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)

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

Note

nn.Module(大寫(xiě) M)是 PyTorch 的特定概念,也是我們將經(jīng)常使用的一個(gè)類(lèi)。 nn.Module不要與(小寫(xiě)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)在使用的是對(duì)象而不是僅使用函數(shù),因此我們首先必須實(shí)例化模型:

model = Mnist_Logistic()

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

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

得出:

tensor(2.4205, grad_fn=<NllLossBackward>)

以前,在我們的訓(xùn)練循環(huán)中,我們必須按名稱(chēng)更新每個(gè)參數(shù)的值,并手動(dòng)將每個(gè)參數(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定義)來(lái)使這些步驟更簡(jiǎn)潔,并且更不會(huì)出現(xiàn)忘記某些參數(shù)的錯(cuò)誤,特別是在 我們有一個(gè)更復(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)我們的代碼。 代替手動(dòng)定義和初始化self.weightsself.bias并計(jì)算xb @ self.weights + self.bias,我們將對(duì)線性層使用 Pytorch 類(lèi) nn.Linear ,這將為我們完成所有工作。 Pytorch 具有許多類(lèi)型的預(yù)定義層,可以大大簡(jiǎn)化我們的代碼,并且通常也可以使其速度更快。

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í)例化模型并計(jì)算損失:

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 還提供了一個(gè)包含各種優(yōu)化算法的軟件包torch.optim。 我們可以使用優(yōu)化器中的step方法采取向前的步驟,而不是手動(dòng)更新每個(gè)參數(shù)。

這就是我們將要替換之前手動(dòng)編碼的優(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,我們需要在計(jì)算下一個(gè)小批量的梯度之前調(diào)用它。)

from torch import optim

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

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

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

from torch.utils.data import TensorDataset

x_trainy_train都可以合并為一個(gè)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)在,我們可以將兩個(gè)步驟一起執(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)建一個(gè)DataLoader。 DataLoader使迭代迭代變得更加容易。 不必使用train_ds[i*bs : i*bs+bs],DataLoader 會(huì)自動(dòng)為我們提供每個(gè)小批量。

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)更加簡(jiǎn)潔了,因?yàn)?xb,yb)是從數(shù)據(jù)加載器自動(dòng)加載的:

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.Module,nn.Parameter,Dataset和DataLoader,我們的訓(xùn)練循環(huán)現(xiàn)在變得更小,更容易理解。 現(xiàn)在,讓我們嘗試添加在實(shí)踐中創(chuàng)建有效模型所需的基本功能。

添加驗(yàn)證

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

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

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

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)

我們將在每個(gè) epoch 結(jié)束時(shí)計(jì)算并打印驗(yàn)證損失。

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

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)歷了兩次相似的過(guò)程來(lái)計(jì)算訓(xùn)練集和驗(yàn)證集的損失,因此我們將其設(shè)為自己的函數(shù)loss_batch,該函數(shù)可計(jì)算一批損失。

我們將優(yōu)化器傳入訓(xùn)練集中,并使用它執(zhí)行反向傳播。 對(duì)于驗(yàn)證集,我們沒(méi)有通過(guò)優(yōu)化程序,因此該方法不會(huì)執(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)行必要的操作來(lái)訓(xùn)練我們的模型,并計(jì)算每個(gè)時(shí)期的訓(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ù)加載器和擬合模型的整個(gè)過(guò)程可以在 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 行代碼來(lái)訓(xùn)練各種各樣的模型。 讓我們看看是否可以使用它們來(lái)訓(xùn)練卷積神經(jīng)網(wǎng)絡(luò)(CNN)!

切換到 CNN

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

我們將使用 Pytorch 的預(yù)定義 Conv2d 類(lèi)作為我們的卷積層。 我們定義具有 3 個(gè)卷積層的 CNN。 每個(gè)卷積后跟一個(gè) ReLU。 最后,我們執(zhí)行平均池化。 (請(qǐng)注意,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

動(dòng)量是隨機(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還有另一個(gè)靈活的類(lèi),可以用來(lái)簡(jiǎn)化我們的代碼: Sequential 。 Sequential對(duì)象以順序方式運(yùn)行其中包含的每個(gè)模塊。 這是編寫(xiě)神經(jīng)網(wǎng)絡(luò)的一種簡(jiǎn)單方法。

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

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)建的模型很簡(jiǎn)單:

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ò)很簡(jiǎn)潔,但是它只能在 MNIST 數(shù)據(jù)集上面有效,因?yàn)?/p>

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

我們使用的平均池化卷積核的大?。?/p>

讓我們擺脫這兩個(gè)假設(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)

接下來(lái),我們可以將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ù)云提供商處以每小時(shí)$ 0.50 的價(jià)格租用一個(gè) GPU),則可以使用它來(lái)加速代碼。 首先檢查您的 GPU 是否在 Pytorch 中正常工作:

print(torch.cuda.is_available())

得出:

True

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

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

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

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)