通過PyTorch 進(jìn)行深度學(xué)習(xí)

數(shù)學(xué)公式如下所示:

對(duì)于向量來說,為兩個(gè)向量的點(diǎn)積/內(nèi)積:

我們可以將這些單元神經(jīng)元組合為層和堆棧,形成神經(jīng)元網(wǎng)絡(luò)。一個(gè)神經(jīng)元層的輸出變成另一層的輸入。對(duì)于多個(gè)輸入單元和輸出單元,我們現(xiàn)在需要將權(quán)重表示為矩陣。


張量
實(shí)際上神經(jīng)網(wǎng)絡(luò)計(jì)算只是對(duì)張量進(jìn)行一系列線性代數(shù)運(yùn)算,矩陣是張量的一種形式。向量是一維張量,矩陣是二維張量,包含 3 個(gè)索引的數(shù)組是三維向量(例如 RGB 顏色圖像)。神經(jīng)網(wǎng)絡(luò)的基本數(shù)據(jù)結(jié)構(gòu)是張量,PyTorch(以及幾乎所有其他深度學(xué)習(xí)框架)都是以張量為基礎(chǔ)。

使用pytorch 構(gòu)建神經(jīng)網(wǎng)絡(luò)
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import numpy as np
import torch
import helper

一般而言,PyTorch 張量的行為和 Numpy 數(shù)組相似。它們的索引都以 0 開始,并且支持切片。


改變形狀
改變張量的形狀是一個(gè)很常見的運(yùn)算。首先使用 .size()獲取張量的大小和形狀。然后,使用 .resize_()改變張量的形狀。注意下劃線,改變形狀是原地運(yùn)算。

在 Numpy 與 Torch 之間轉(zhuǎn)換
在 Numpy 數(shù)組與 Torch 張量之間轉(zhuǎn)換非常簡(jiǎn)單并且很實(shí)用。要通過 Numpy 數(shù)組創(chuàng)建張量,使用 torch.from_numpy()。要將張量轉(zhuǎn)換為 Numpy 數(shù)組,使用 .numpy() 方法。

內(nèi)存在 Numpy 數(shù)組與 Torch 張量之間共享,因此如果你原地更改一個(gè)對(duì)象的值,另一個(gè)對(duì)象的值也會(huì)更改。

通過Pytorch 構(gòu)建神經(jīng)網(wǎng)絡(luò)
# Import things like usual
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import numpy as np
import torch
import helper
import matplotlib.pyplot as plt
from torchvision import datasets, transforms
需要獲取數(shù)據(jù)集。這些數(shù)據(jù)位于 torchvision 軟件包中。以下代碼將下載 MNIST 數(shù)據(jù)集,然后為我們創(chuàng)建訓(xùn)練數(shù)據(jù)集和測(cè)試數(shù)據(jù)集
# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])
# Download and load the training data
trainset = datasets.MNIST('MNIST_data/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# Download and load the test data
testset = datasets.MNIST('MNIST_data/', download=True, train=False, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
dataiter = iter(trainloader)
images, labels = dataiter.next()
我們將訓(xùn)練數(shù)據(jù)加載到了 trainloader 中,并使用 iter(trainloader)使其變成迭代器。我們將用它循環(huán)訪問數(shù)據(jù)集以進(jìn)行訓(xùn)練,但是現(xiàn)在我只獲取了第一批數(shù)據(jù),以便查看數(shù)據(jù)。從下方可以看出,images 是一個(gè)大小為 (64, 1, 28, 28) 的張量。因此,每批有 64 個(gè)圖像、1 個(gè)顏色通道,共有 28x28 個(gè)圖像。
構(gòu)建神經(jīng)網(wǎng)絡(luò)

要通過 PyTorch 構(gòu)建神經(jīng)網(wǎng)絡(luò),你需要使用 torch.nn 模塊。網(wǎng)絡(luò)本身是繼承自 torch.nn.Module 的類。你需要單獨(dú)定義每個(gè)運(yùn)算,例如針對(duì)具有 784 個(gè)輸入和 128 個(gè)單元的全連接層定義為 nn.Linear(784, 128)。
該類需要包含對(duì)網(wǎng)絡(luò)實(shí)現(xiàn)前向傳遞的 forward 方法。在此方法中,你將對(duì)之前定義的每個(gè)運(yùn)算傳遞輸入張量 x。torch.nn 模塊在 torch.nn.functional 中還具有一些對(duì)等的功能,例如 ReLU。此模塊通常導(dǎo)入為 F。要對(duì)某個(gè)層(只是一個(gè)張量)使用 ReLU 激活函數(shù),你需要使用 F.relu(x)。以下是一些常見的不同激活函數(shù)。

對(duì)于此網(wǎng)絡(luò),我將添加三個(gè)全連接層,然后添加一個(gè)預(yù)測(cè)類別的 softmax 輸出。softmax 函數(shù)和 S 型函數(shù)相似,都會(huì)將輸入調(diào)整到 0 到 1 之間,但是還會(huì)標(biāo)準(zhǔn)化這些輸入,以便所有值的和為 1,就像正常的概率分布一樣。
from torch import nn
from torch import optim
import torch.nn.functional as F
class Network(nn.Module):
def __init__(self):
super().__init__()
# Defining the layers, 128, 64, 10 units each
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 64)
# Output layer, 10 units - one for each digit
self.fc3 = nn.Linear(64, 10)
def forward(self, x):
''' Forward pass through the network, returns the output logits '''
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = F.relu(x)
x = self.fc3(x)
x = F.softmax(x, dim=1)
return x
model = Network()
model

權(quán)重等參數(shù)是系統(tǒng)自動(dòng)初始化的,但是你也可以自定義如何初始化這些權(quán)重。權(quán)重和偏差是附加到你所定義的層的張量,你可以通過 net.fc1.weight 獲取它們。
初始化權(quán)重和偏差
print(net.fc1.weight)
print(net.fc1.bias)
要自定義初始化過程,請(qǐng)?jiān)匦薷倪@些張量。實(shí)際上存在 autograd 變量,因此我們需要通過 net.fc1.weight.data 獲取真正的張量。獲得張量后,可以用 0(針對(duì)偏差)或隨機(jī)正常值填充這些張量。
# Set biases to all zeros
net.fc1.bias.data.fill_(0);
# sample from random normal with standard dev = 0.01
net.fc1.weight.data.normal_(std=0.01);
前向傳遞
我們已經(jīng)創(chuàng)建好網(wǎng)絡(luò),看看傳入圖像后會(huì)發(fā)生什么。這一過程稱之為前向傳遞。我們將圖像數(shù)據(jù)轉(zhuǎn)換為張量,然后傳遞給網(wǎng)絡(luò)架構(gòu)定義的運(yùn)算。
# Grab some data
dataiter = iter(trainloader)
images, labels = dataiter.next()
images.resize_(64, 1, 784)
# Need to wrap it in a Variable, will explain in next notebook
inputs = Variable(images)
# Forward pass through the network
img_idx = 0
logits = net.forward(inputs[img_idx,:])
# Predict the class from the network output
ps = F.softmax(logits, dim=1)
img = images[img_idx]
helper.view_classify(img.resize_(1, 28, 28), ps)

從上圖中可以看出,我們的網(wǎng)絡(luò)基本上根本不知道這個(gè)數(shù)字是什么,因?yàn)槲覀冞€沒訓(xùn)練它,所有權(quán)重都是隨機(jī)的!接下來,我們將了解如何訓(xùn)練該網(wǎng)絡(luò),使其能學(xué)習(xí)如何正確地對(duì)這些數(shù)字進(jìn)行分類。
PyTorch提供了一種方便的方法來構(gòu)建這樣的網(wǎng)絡(luò),其中張量通過操作順序傳遞。使用它來構(gòu)建等效網(wǎng)絡(luò)nn.Sequential (documentation):
# Hyperparameters for our network
input_size = 784
hidden_sizes = [128, 64]
output_size = 10
# Build a feed-forward network
model = nn.Sequential(nn.Linear(input_size, hidden_sizes[0]),
nn.ReLU(),
nn.Linear(hidden_sizes[0], hidden_sizes[1]),
nn.ReLU(),
nn.Linear(hidden_sizes[1], output_size),
nn.Softmax(dim=1))
print(model)
# Forward pass through the network and display output
images, labels = next(iter(trainloader))
images.resize_(images.shape[0], 1, 784)
ps = model.forward(images[0,:])
helper.view_classify(images[0].view(1, 28, 28), ps)

還可以傳入OrderedDict來命名各個(gè)圖層和操作。 請(qǐng)注意,字典鍵必須是唯一的,因此每個(gè)操作必須具有不同的名稱。
from collections import OrderedDict
model = nn.Sequential(OrderedDict([
('fc1', nn.Linear(input_size, hidden_sizes[0])),
('relu1', nn.ReLU()),
('fc2', nn.Linear(hidden_sizes[0], hidden_sizes[1])),
('relu2', nn.ReLU()),
('output', nn.Linear(hidden_sizes[1], output_size)),
('softmax', nn.Softmax(dim=1))]))
model
訓(xùn)練神經(jīng)網(wǎng)絡(luò)
一開始網(wǎng)絡(luò)很樸素,不知道將輸入映射到輸出的函數(shù)。我們通過向網(wǎng)絡(luò)展示實(shí)際數(shù)據(jù)樣本訓(xùn)練網(wǎng)絡(luò),然后調(diào)整網(wǎng)絡(luò)參數(shù),使其逼近此函數(shù)。
要找到這些參數(shù),我們需要了解網(wǎng)絡(luò)預(yù)測(cè)真實(shí)輸出的效果如何。為此,我們將計(jì)算損失函數(shù)(也稱為成本),一種衡量預(yù)測(cè)錯(cuò)誤的指標(biāo)。例如,回歸問題和二元分類問題經(jīng)常使用均方損失

其中 n 是訓(xùn)練樣本的數(shù)量,yi是真正的標(biāo)簽,y ? i 是預(yù)測(cè)標(biāo)簽
通過盡量減小相對(duì)于網(wǎng)絡(luò)參數(shù)的這一損失,我們可以找到損失最低且網(wǎng)絡(luò)能夠以很高的準(zhǔn)確率預(yù)測(cè)正確標(biāo)簽的配置。我們使用叫做梯度下降法的流程來尋找這一最低值。梯度是損失函數(shù)的斜率,指向變化最快的方向。要以最短的時(shí)間找到最低值,我們需要沿著梯度(向下)前進(jìn)??梢詫⑦@一過程看做沿著最陡的路線下山。
反向傳播
對(duì)于單層網(wǎng)絡(luò),梯度下降法實(shí)現(xiàn)起來很簡(jiǎn)單。但是,對(duì)于更深、層級(jí)更多的神經(jīng)網(wǎng)絡(luò)(例如我們構(gòu)建的網(wǎng)絡(luò)),梯度下降法實(shí)現(xiàn)起來更復(fù)雜。我們通過反向傳播來實(shí)現(xiàn),實(shí)際上是采用的微積分中的鏈?zhǔn)椒▌t。最簡(jiǎn)單的理解方法是將兩層網(wǎng)絡(luò)轉(zhuǎn)換為圖形表示法。

在網(wǎng)絡(luò)的前向傳遞過程中,我們的數(shù)據(jù)和運(yùn)算從右到左。要通過梯度下降法訓(xùn)練權(quán)重,我們沿著網(wǎng)絡(luò)反向傳播成本梯度。從數(shù)學(xué)角度來講,其實(shí)就是使用鏈?zhǔn)椒▌t計(jì)算相對(duì)于權(quán)重的損失梯度。

我們使用此梯度和學(xué)習(xí)速率 α 更新權(quán)重。

對(duì)于訓(xùn)練步驟來說,首先我們需要定義損失函數(shù)。在 PyTorch 中,通常你會(huì)看到它寫成了 criterion 形式。在此例中,我們使用 softmax 輸出,因此我們希望使用 criterion = nn.CrossEntropyLoss() 作為損失函數(shù)。稍后在訓(xùn)練時(shí),你需要使用 loss = criterion(output, targets) 計(jì)算實(shí)際損失。
我們還需要定義優(yōu)化器,例如 SGD 或 Adam 等。我將使用 SGD,即 torch.optim.SGD,并傳入網(wǎng)絡(luò)參數(shù)和學(xué)習(xí)速率。
Autograd 自動(dòng)計(jì)算梯度
Torch提供了一個(gè)自動(dòng)編程模塊,用于自動(dòng)計(jì)算張量的梯度。 它通過跟蹤在張量上執(zhí)行的操作來實(shí)現(xiàn)此目的。 為了確保PyTorch跟蹤張量上的運(yùn)算并計(jì)算梯度,您需要在張量上設(shè)置requires_grad。 您可以使用requires_grad關(guān)鍵字在創(chuàng)建時(shí)執(zhí)行此操作,也可以隨時(shí)使用x.requires_grad_(True)執(zhí)行此操作。
您可以使用torch.no_grad()內(nèi)容關(guān)閉代碼塊的漸變:
x = torch.zeros(1,requires_grad = True)
with torch.no_grad():
... y = x * 2
y.requires_grad
false
此外,您可以使用torch.set_grad_enabled(True | False)完全打開或關(guān)閉梯度。
使用z.backward()相對(duì)于某個(gè)變量z計(jì)算梯度。 這會(huì)向后傳遞創(chuàng)建z的操作。
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
from collections import OrderedDict
import numpy as np
import time
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
import helper
x = torch.randn(2,2, requires_grad=True)
print(x)

y = x**2
print(y)

下面我們可以看到創(chuàng)建y的操作,一個(gè)冪運(yùn)算操作PowBackward0。
## grad_fn shows the function that generated this variable
print(y.grad_fn)

autgrad模塊會(huì)跟蹤這些操作,并知道如何計(jì)算每個(gè)操作的梯度。 通過這種方式,它能夠針對(duì)任何一個(gè)張量計(jì)算一系列操作的梯度。 讓我們將張量y減小到標(biāo)量值,即平均值。

你可以檢查X和Y的梯度,但是它們現(xiàn)在是空的

要計(jì)算梯度,您需要在Variable z上運(yùn)行.backward方法。 這將計(jì)算z相對(duì)于x的梯度


這些梯度計(jì)算對(duì)神經(jīng)網(wǎng)絡(luò)特別有用。 對(duì)于訓(xùn)練,我們需要權(quán)重的梯度與成本。 使用PyTorch,我們通過網(wǎng)絡(luò)向前運(yùn)行數(shù)據(jù)來計(jì)算成本,然后向后計(jì)算與成本相關(guān)的梯度。 一旦我們得到了梯度,我們就可以做出梯度下降步驟。
定義網(wǎng)絡(luò)結(jié)構(gòu)
from torchvision import datasets, transforms
# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])
# Download and load the training data
trainset = datasets.MNIST('MNIST_data/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
我將在這里使用nn.Sequential構(gòu)建一個(gè)網(wǎng)絡(luò)。 與上一部分的唯一區(qū)別是我實(shí)際上并沒有在輸出上使用softmax,而只是使用最后一層的原始輸出。 這是因?yàn)閟oftmax的輸出是概率分布。 通常,輸出的值確實(shí)接近于零或非常接近于1。 由于將數(shù)字表示為浮點(diǎn)的不準(zhǔn)確性,具有softmax輸出的計(jì)算可能會(huì)失去準(zhǔn)確性并變得不穩(wěn)定。 為了解決這個(gè)問題,我們將使用稱為logits的原始輸出來計(jì)算損失。
# Hyperparameters for our network
input_size = 784
hidden_sizes = [128, 64]
output_size = 10
# Build a feed-forward network
model = nn.Sequential(OrderedDict([
('fc1', nn.Linear(input_size, hidden_sizes[0])),
('relu1', nn.ReLU()),
('fc2', nn.Linear(hidden_sizes[0], hidden_sizes[1])),
('relu2', nn.ReLU()),
('logits', nn.Linear(hidden_sizes[1], output_size))]))
我們需要做的第一件事就是定義我們的損失函數(shù)。 在PyTorch中,您通常會(huì)將此視為標(biāo)準(zhǔn)。 這里我們使用softmax輸出,所以我們想使用criterion = nn.CrossEntropyLoss()作為我們的損失。 稍后在訓(xùn)練時(shí),您使用loss = criterion(output, targets)來計(jì)算實(shí)際損失。
我們還需要定義我們正在使用的優(yōu)化器,SGD或Adam,或者其他類似的東西。 在這里,我將使torch.optim.SGD,傳遞網(wǎng)絡(luò)參數(shù)和學(xué)習(xí)速率。
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
首先,在循環(huán)遍歷所有數(shù)據(jù)之前,我們只考慮一個(gè)學(xué)習(xí)步驟。 PyTorch的一般過程:
- 通過網(wǎng)絡(luò)進(jìn)行正向傳遞以獲取logits
- 使用logits計(jì)算損失
- 使用
loss.backward()執(zhí)行向后傳遞網(wǎng)絡(luò)以計(jì)算漸變 - 使用優(yōu)化器更新權(quán)重
下面我將完成一個(gè)訓(xùn)練步驟并打印出權(quán)重和梯度
print('Initial weights - ', model.fc1.weight)
images, labels = next(iter(trainloader))
images.resize_(64, 784)
# Clear the gradients, do this because gradients are accumulated
optimizer.zero_grad()
# Forward pass, then backward pass, then update weights
output = model.forward(images)
loss = criterion(output, labels)
loss.backward()
print('Gradient -', model.fc1.weight.grad)
optimizer.step()

print('Updated weights - ', net.fc1.weight)

實(shí)際訓(xùn)練
optimizer = optim.SGD(model.parameters(), lr=0.003)
epochs = 3
print_every = 40
steps = 0
for e in range(epochs):
running_loss = 0
for images, labels in trainloader:
steps += 1
# Flatten MNIST images into a 784 long vector
images.resize_(images.size()[0], 784)
optimizer.zero_grad()
# Forward and backward passes
output = model.forward(images)
loss = criterion(output, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if steps % print_every == 0:
print("Epoch: {}/{}... ".format(e+1, epochs),
"Loss: {:.4f}".format(running_loss/print_every))
running_loss = 0

images, labels = next(iter(trainloader))
img = images[0].view(1, 784)
# Turn off gradients to speed up this part
with torch.no_grad():
logits = model.forward(img)
# Output of the network are logits, need to take softmax for probabilities
ps = F.softmax(logits, dim=1)
helper.view_classify(img.view(1, 28, 28), ps)

推理與驗(yàn)證
在訓(xùn)練神經(jīng)網(wǎng)絡(luò)之后,你現(xiàn)在可以使用它來進(jìn)行預(yù)測(cè)。這種步驟通常被稱作推理,這是一個(gè)借自統(tǒng)計(jì)學(xué)的術(shù)語。然而,神經(jīng)網(wǎng)絡(luò)在面對(duì)訓(xùn)練數(shù)據(jù)時(shí)往往表現(xiàn)得太過優(yōu)異,因而無法泛化未見過的數(shù)據(jù)。這種現(xiàn)象被稱作過擬合,它損害了推理性能。為了在訓(xùn)練時(shí)檢測(cè)過擬合,我們測(cè)量并不在名為驗(yàn)證集的訓(xùn)練集中數(shù)據(jù)的性能。在訓(xùn)練時(shí),我們一邊監(jiān)控驗(yàn)證性能,一邊進(jìn)行正則化,如 Dropout,以此來避免過擬合。在這個(gè) notebook 中,我將向你展示如何在 PyTorch 中做到這一點(diǎn)。
首先,我會(huì)實(shí)現(xiàn)我自己的前饋神經(jīng)網(wǎng)絡(luò),這個(gè)網(wǎng)絡(luò)基于第四部分的練習(xí)中的 Fashion-MNIST 數(shù)據(jù)集構(gòu)建。它是第四部分練習(xí)的解決方案,也是如何進(jìn)行 Dropout 和驗(yàn)證的例子。
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
import numpy as np
import time
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
from torch.autograd import Variable
from torchvision import datasets, transforms
import helper
# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# Download and load the training data
trainset = datasets.FashionMNIST('F_MNIST_data/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# Download and load the test data
testset = datasets.FashionMNIST('F_MNIST_data/', download=True, train=False, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
構(gòu)建網(wǎng)絡(luò)
跟 MNIST 數(shù)據(jù)集一樣,F(xiàn)ashion-MNIST 數(shù)據(jù)集中每張圖片的像素為 28x28,共 784 個(gè)數(shù)據(jù)點(diǎn)和 10 個(gè)類。我使用了nn.ModuleList來加入任意數(shù)量的隱藏層。這個(gè)模型中的hidden_layers參數(shù)為隱藏層大小的列表(以整數(shù)表示)。使用 nn.ModuleList 來寄存每一個(gè)隱藏模塊,這樣你可以在之后使用模塊方法。
由于每個(gè)nn.Linear操作都需要輸入大小和輸出大小
# Create ModuleList and add input layer
hidden_layers = nn.ModuleList([nn.Linear(input_size, hidden_layers[0])])
# Add hidden layers to the ModuleList
hidden_layers.extend([nn.Linear(h1, h2) for h1, h2 in layer_sizes])
獲得這些輸入和輸出大小對(duì)可以通過使用zip的方便技巧完成。
hidden_layers = [512, 256, 128, 64]
layer_sizes = zip(hidden_layers[:-1], hidden_layers[1:])
for each in layer_sizes:
print(each)
>> (512, 256)
>> (256, 128)
>> (128, 64)
我還使用了 forward 方法來返回輸出的 log-softmax。由于 softmax 是類的概率分布,因此 log-softmax 是一種對(duì)數(shù)概率,它有許多優(yōu)點(diǎn)。使用這種對(duì)數(shù)概率,計(jì)算往往會(huì)更加迅速和準(zhǔn)確。為了在之后獲得類的概率,我將需要獲得輸出的指數(shù)(torch.exp)。

我們可以使用
nn.Dropout 來在我們的網(wǎng)絡(luò)中加入 Dropout。這與 nn.Linear 等其他模塊的作用相似。它還將 Dropout 概率作為一種輸入傳遞到網(wǎng)絡(luò)中。
class Network(nn.Module):
def __init__(self, input_size, output_size, hidden_layers, drop_p=0.5):
''' Builds a feedforward network with arbitrary hidden layers.
Arguments
---------
input_size: integer, size of the input
output_size: integer, size of the output layer
hidden_layers: list of integers, the sizes of the hidden layers
drop_p: float between 0 and 1, dropout probability
'''
super().__init__()
# Add the first layer, input to a hidden layer
self.hidden_layers = nn.ModuleList([nn.Linear(input_size, hidden_layers[0])])
# Add a variable number of more hidden layers
layer_sizes = zip(hidden_layers[:-1], hidden_layers[1:])
self.hidden_layers.extend([nn.Linear(h1, h2) for h1, h2 in layer_sizes])
self.output = nn.Linear(hidden_layers[-1], output_size)
self.dropout = nn.Dropout(p=drop_p)
def forward(self, x):
''' Forward pass through the network, returns the output logits '''
for each in self.hidden_layers:
x = F.relu(each(x))
x = self.dropout(x)
x = self.output(x)
return F.log_softmax(x, dim=1)
訓(xùn)練網(wǎng)絡(luò)
由于該模型的前向方法返回 log-softmax,因此我使用了負(fù)對(duì)數(shù)損失 作為標(biāo)準(zhǔn)。我還選用了Adam 優(yōu)化器。這是一種隨機(jī)梯度下降的變體,包含了動(dòng)量,并且訓(xùn)練速度往往比基本的 SGD 要快。
還加入了一個(gè)代碼塊來測(cè)量驗(yàn)證損失和精確度。由于我在這個(gè)神經(jīng)網(wǎng)絡(luò)中使用了 Dropout,在推理時(shí)我需要將其關(guān)閉,否則這個(gè)網(wǎng)絡(luò)將會(huì)由于許多連接的關(guān)閉而表現(xiàn)糟糕。在 PyTorch 中,你可以使用 model.train() 和 model.eval() 來將模型調(diào)整為“訓(xùn)練模式”或是“評(píng)估模式”。在訓(xùn)練模式中,Dropout 為開啟狀態(tài),而在評(píng)估模式中,Dropout 為關(guān)閉狀態(tài)。這還會(huì)影響到其他模塊,包括那些應(yīng)該在訓(xùn)練時(shí)開啟、在推理時(shí)關(guān)閉的模塊。
這段驗(yàn)證代碼由一個(gè)通過驗(yàn)證集(并分裂成幾個(gè)批次)的前向傳播組成。根據(jù) log-softmax 輸出來計(jì)算驗(yàn)證集的損失以及預(yù)測(cè)精確度。
# Create the network, define the criterion and optimizer
model = Network(784, 10, [516, 256], drop_p=0.5)## 784輸入,10輸出,兩個(gè)隱藏層
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Implement a function for the validation pass
def validation(model, testloader, criterion):
test_loss = 0
accuracy = 0
for images, labels in testloader:
images.resize_(images.shape[0], 784)
output = model.forward(images)
test_loss += criterion(output, labels).item()
ps = torch.exp(output)
equality = (labels.data == ps.max(dim=1)[1])
accuracy += equality.type(torch.FloatTensor).mean()
return test_loss, accuracy
epochs = 2
steps = 0
running_loss = 0
print_every = 40
for e in range(epochs):
model.train()
for images, labels in trainloader:
steps += 1
# Flatten images into a 784 long vector
images.resize_(images.size()[0], 784)
optimizer.zero_grad()
output = model.forward(images)
loss = criterion(output, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if steps % print_every == 0:
# Make sure network is in eval mode for inference
model.eval()
# Turn off gradients for validation, saves memory and computations
with torch.no_grad():
test_loss, accuracy = validation(model, testloader, criterion)
print("Epoch: {}/{}.. ".format(e+1, epochs),
"Training Loss: {:.3f}.. ".format(running_loss/print_every),
"Test Loss: {:.3f}.. ".format(test_loss/len(testloader)),
"Test Accuracy: {:.3f}".format(accuracy/len(testloader)))
running_loss = 0
# Make sure training is back on
model.train()

推理
模型已經(jīng)訓(xùn)練好了,我們現(xiàn)在可以使用它來進(jìn)行推理。之前已經(jīng)進(jìn)行過這一步驟,但現(xiàn)在我們需要使用 model.eval() 來將模型設(shè)置為推理模式。
# Test out your network!
model.eval()
dataiter = iter(testloader)
images, labels = dataiter.next()
img = images[0]
# Convert 2D image to 1D vector
img = img.view(1, 784)
# Calculate the class probabilities (softmax) for img
with torch.no_grad():
output = model.forward(img)
ps = torch.exp(output)
# Plot the image and probabilities
helper.view_classify(img.view(1, 28, 28), ps, version='Fashion')

保存和加載模型
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import helper
import fc_model
# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# Download and load the training data
trainset = datasets.FashionMNIST('F_MNIST_data/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# Download and load the test data
testset = datasets.FashionMNIST('F_MNIST_data/', download=True, train=False, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
image, label = next(iter(trainloader))
helper.imshow(image[0,:]);

我將模型架構(gòu)和訓(xùn)練代碼從最后一部分移動(dòng)到一個(gè)名為fc_model的文件中。 導(dǎo)入此內(nèi)容后,我們可以使用fc_model.Network輕松創(chuàng)建完全連接的網(wǎng)絡(luò),并使用fc_model.train訓(xùn)練網(wǎng)絡(luò)。
# Create the network, define the criterion and optimizer
model = fc_model.Network(784, 10, [512, 256, 128])
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
fc_model.train(model, trainloader, testloader, criterion, optimizer, epochs=2)
可以想象,在每次使用神經(jīng)網(wǎng)絡(luò)時(shí)都重新進(jìn)行訓(xùn)練很不現(xiàn)實(shí)。因此,我們可以保存之前訓(xùn)練好的網(wǎng)絡(luò),并在繼續(xù)訓(xùn)練或是進(jìn)行預(yù)測(cè)時(shí)加載網(wǎng)絡(luò)。 PyTorch 網(wǎng)絡(luò)的參數(shù)都存儲(chǔ)在模型的 state_dict中。可以看到這個(gè)狀態(tài)字典包含了每個(gè)層的權(quán)重和偏差矩陣。
print("Our model: \n\n", model, '\n')
print("The state dict keys: \n\n", model.state_dict().keys())

最簡(jiǎn)單的做法是使用
torch.save 來保存狀態(tài)字典。比如,我們可以將它保存到文件 'checkpoint.pth' 中。
torch.save(model.state_dict(), 'checkpoint.pth')
接著,我們可以使用 torch.load 來加載這個(gè)狀態(tài)字典。
state_dict = torch.load('checkpoint.pth')
print(state_dict.keys())
要將狀態(tài)字典加載到神經(jīng)網(wǎng)絡(luò)中,你需要使用 model.load_state_dict(state_dict)
這看上去十分簡(jiǎn)單,但實(shí)際情況更加復(fù)雜。只有當(dāng)模型結(jié)構(gòu)與檢查點(diǎn)的結(jié)構(gòu)完全一致時(shí),狀態(tài)字典才能成功加載。如果我在創(chuàng)建模型時(shí)使用了不同的結(jié)構(gòu),便無法順利加載。
# Try this
model = fc_model.Network(784, 10, [400, 200, 100])
# This will throw an error because the tensor sizes are wrong!
model.load_state_dict(state_dict)

這意味著我們需要重建一個(gè)與訓(xùn)練時(shí)完全相同的模型。有關(guān)模型結(jié)構(gòu)的信息需要與狀態(tài)字典一起存儲(chǔ)在檢查點(diǎn)中。為了做到這一點(diǎn),你需要構(gòu)建一個(gè)字典,字典中包含重建模型的全部信息。
checkpoint = {'input_size': 784,
'output_size': 10,
'hidden_layers': [each.out_features for each in model.hidden_layers],
'state_dict': model.state_dict()}
torch.save(checkpoint, 'checkpoint.pth')
現(xiàn)在,檢查點(diǎn)中包含了重建訓(xùn)練模型所需的全部信息。你可以隨意將它編寫為函數(shù)。相似地,我們也可以編寫一個(gè)函數(shù)來加載檢查點(diǎn)。
def load_checkpoint(filepath):
checkpoint = torch.load(filepath)
model = fc_model.Network(checkpoint['input_size'],
checkpoint['output_size'],
checkpoint['hidden_layers'])
model.load_state_dict(checkpoint['state_dict'])
return model
model = load_checkpoint('checkpoint.pth')
print(model)

加載圖像數(shù)據(jù)
在實(shí)際項(xiàng)目中,你可能會(huì)處理一些全尺寸的圖像,比如手機(jī)相機(jī)拍攝的圖片。在這個(gè) notebook 中,我們將會(huì)學(xué)習(xí)如何加載圖像,并使用它們來訓(xùn)練神經(jīng)網(wǎng)絡(luò)。
我們將用到來自 Kaggle 的貓狗照片數(shù)據(jù)集。
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
import torch
from torchvision import datasets, transforms
import helper
加載圖像數(shù)據(jù)最簡(jiǎn)單是方法是使用 torchvision 中的 datasets.ImageFolder(資料)。dataset = datasets.ImageFolder('path/to/data', transform=transforms)
'path/to/data' 是通往數(shù)據(jù)目錄的文件路徑,transforms 是一個(gè)處理步驟的列表,使用 torchvision 中的 transforms 模塊構(gòu)建。ImageFolder 中的文件和目錄應(yīng)按以下格式構(gòu)建:
root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png
root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
每個(gè)類都有各自存儲(chǔ)圖像的目錄(cat 和 dog)。接著,這些圖像將被貼上摘自目錄名的標(biāo)簽。所以在這里,圖像 123.png 在加載時(shí)將被貼上類標(biāo)簽 cat。
Transform
當(dāng)你使用 ImageFolder 加載數(shù)據(jù)后,你需要定義一些轉(zhuǎn)換。舉個(gè)例子,這些圖像的尺寸都不相同,但我們需要統(tǒng)一尺寸以便進(jìn)行訓(xùn)練。你可以使用 transforms.Resize() 來重新確定圖像尺寸,也可以使用 transforms.CenterCrop()、transforms.RandomResizedCrop() 等進(jìn)行切割。我們還需要使用 transforms.ToTensor() 來將圖像轉(zhuǎn)換為 PyTorch 張量。通常,你會(huì)使用 transforms.Compose() 來將這些轉(zhuǎn)換結(jié)合到一條流水線中,這條流水線接收包含轉(zhuǎn)換的列表,并按順序運(yùn)行。如下面的例子所示,它首先進(jìn)行縮放,接著切割,再轉(zhuǎn)換為張量:
transforms = transforms.Compose([transforms.Resize(255),
transforms.CenterCrop(224),
transforms.ToTensor()])
我們可以使用許多種轉(zhuǎn)換,接下來我會(huì)逐步講解,你也可以查看這里的資料。
Data Loader
在加載 ImageFolder 后,你需要將它傳遞給一個(gè) DataLoader。DataLoader 接收數(shù)據(jù)集(比如你從 ImageFolder 中獲取的數(shù)據(jù)集),并返回不同批次的圖像以及對(duì)應(yīng)的標(biāo)簽。你可以設(shè)置不同參數(shù),比如批次大小,也可以設(shè)置是否在每個(gè)階段后重組數(shù)據(jù)。
dataloader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)
在這里,dataloader 是一個(gè)生成器。要想從這個(gè)生成器中提取數(shù)據(jù),你需要遍歷這個(gè)生成器,或是將它轉(zhuǎn)換為一個(gè)迭代器并調(diào)用 next()。
# Looping through it, get a batch on each loop
for images, labels in dataloader:
pass
# Get one batch
images, labels = next(iter(dataloader))
# Run this to test your data loader
images, labels = next(iter(dataloader))
helper.imshow(images[0], normalize=False)
數(shù)據(jù)增強(qiáng)
訓(xùn)練神經(jīng)網(wǎng)絡(luò)的常用策略是在輸入數(shù)據(jù)中添加隨機(jī)性。舉個(gè)例子,你可以在訓(xùn)練時(shí)隨意旋轉(zhuǎn)、鏡像、縮放以及/或剪切你的圖像。這樣一來,你的神經(jīng)網(wǎng)絡(luò)在處理位置、大小、方向不同的相同圖像時(shí),可以更好地進(jìn)行泛化。
要想隨機(jī)旋轉(zhuǎn)、縮放、剪切和翻轉(zhuǎn)圖像,你需要按以下格式定義轉(zhuǎn)換:
train_transforms = transforms.Compose([transforms.RandomRotation(30),
transforms.RandomResizedCrop(100),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5, 0.5],
[0.5, 0.5, 0.5])])
你通常還需要使用 transforms.Normalize 來標(biāo)準(zhǔn)化圖像。在傳入均值和標(biāo)準(zhǔn)差的列表后,顏色通道將按以下方法進(jìn)行標(biāo)準(zhǔn)化:
減去 mean 能讓數(shù)據(jù)以 0 為中心,除以 std 能夠?qū)⒅导性?-1 和 1 之間。標(biāo)準(zhǔn)化有助于神經(jīng)網(wǎng)絡(luò)使權(quán)重接近 0,這能使反向傳播更為穩(wěn)定。倘若沒有標(biāo)準(zhǔn)化,網(wǎng)絡(luò)往往無法進(jìn)行學(xué)習(xí)。
data_dir = 'Cat_Dog_data'
# TODO: Define transforms for the training data and testing data
train_transforms = transforms.Compose([transforms.RandomRotation(30),
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
test_transforms = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
# Pass transforms in here, then run the next cell to see how the transforms look
train_data = datasets.ImageFolder(data_dir + '/train', transform=train_transforms)
test_data = datasets.ImageFolder(data_dir + '/test', transform=test_transforms)
trainloader = torch.utils.data.DataLoader(train_data, batch_size=32)
testloader = torch.utils.data.DataLoader(test_data, batch_size=32)
# change this to the trainloader or testloader
data_iter = iter(testloader)
images, labels = next(data_iter)
fig, axes = plt.subplots(figsize=(10,4), ncols=4)
for ii in range(4):
ax = axes[ii]
helper.imshow(images[ii], ax=ax)
遷移學(xué)習(xí)
ImageNet 是一個(gè)龐大的數(shù)據(jù)集,其中有超過一百萬張帶有標(biāo)簽的圖像,來自一千個(gè)不同類別。通常,我們使用一種名為卷積層的結(jié)構(gòu)訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)。在這里,我并不會(huì)深入介紹卷積網(wǎng)絡(luò),但如果你感興趣,可以查看這個(gè)視頻。
一旦經(jīng)過訓(xùn)練,這些模型便能以絕佳表現(xiàn)檢測(cè)未見過的圖像的特征。這種使用預(yù)先訓(xùn)練的網(wǎng)絡(luò)來分析訓(xùn)練集之外的圖像的方法被稱為遷移學(xué)習(xí)。在這里,我們將使用遷移學(xué)習(xí)來訓(xùn)練一個(gè)能夠以近乎完美的準(zhǔn)確性分類貓狗圖像的網(wǎng)絡(luò)。
使用 torchvision.models,你可以下載這些預(yù)先訓(xùn)練的網(wǎng)絡(luò),并用于你的應(yīng)用中。我們現(xiàn)在將導(dǎo)入 models
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
from torch.autograd import Variable
from torchvision import datasets, transforms, models
import helper
大多數(shù)預(yù)先訓(xùn)練的模型要求輸入為 224x224 像素的圖像。同樣地,我們需要匹配訓(xùn)練模型時(shí)進(jìn)行的標(biāo)準(zhǔn)化。每個(gè)顏色通道都分別進(jìn)行了標(biāo)準(zhǔn)化,均值為 [0.485, 0.456, 0.406],標(biāo)準(zhǔn)差為 [0.229, 0.224, 0.225]。
data_dir = 'Cat_Dog_data'
# TODO: Define transforms for the training data and testing data
train_transforms = transforms.Compose([transforms.RandomRotation(30),
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
test_transforms = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
# Pass transforms in here, then run the next cell to see how the transforms look
train_data = datasets.ImageFolder(data_dir + '/train', transform=train_transforms)
test_data = datasets.ImageFolder(data_dir + '/test', transform=test_transforms)
trainloader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(test_data, batch_size=32)
我們可以載入一個(gè)模型,比如 DenseNet。現(xiàn)在讓我們打印出這個(gè)模型的結(jié)構(gòu),以便了解細(xì)節(jié)。
這個(gè)模型主要有兩個(gè)部分,即特征和分類器。特征部分是一堆卷積層,能作為特征檢測(cè)器輸入分類器中。分類器部分是一個(gè)單獨(dú)的全連接層 (classifier): Linear(in_features=1024, out_features=1000)。這個(gè)層根據(jù) ImageNet 數(shù)據(jù)集訓(xùn)練,因此無法解決我們指定的問題。這意味著我們需要替換這個(gè)分類器,不過這些特征本身能起到很大的作用。一般來說,我認(rèn)為預(yù)先訓(xùn)練的網(wǎng)絡(luò)是絕佳的特征檢測(cè)器,可以作為簡(jiǎn)單的前饋分類器的輸入。
# Freeze parameters so we don't backprop through them
for param in model.parameters():
param.requires_grad = False
from collections import OrderedDict
classifier = nn.Sequential(OrderedDict([
('fc1', nn.Linear(1024, 500)),
('relu', nn.ReLU()),
('fc2', nn.Linear(500, 2)),
('output', nn.LogSoftmax(dim=1))
]))
model.classifier = classifier
在構(gòu)建好模型之后,我們需要訓(xùn)練分類器。然而,現(xiàn)在我們使用的是一個(gè)非常深的神經(jīng)網(wǎng)絡(luò)。如果你還像之前一樣試圖在 CPU 上訓(xùn)練它,這會(huì)耗費(fèi)相當(dāng)長的時(shí)間。因此,我們將使用 GPU 來進(jìn)行運(yùn)算。在 GPU 上,線性代數(shù)運(yùn)算同步進(jìn)行,這使得運(yùn)算速度提升了 100x。我們還可以在多個(gè) GPU 上進(jìn)行訓(xùn)練,這能進(jìn)一步縮短訓(xùn)練時(shí)間。 PyTorch 和其他深度學(xué)習(xí)框架一樣,使用 CUDA 來高效地在 GPU 上計(jì)算前向和后向傳播。在 PyTorch 中,你可以使用 model.to(cuda) 將模型參數(shù)和其他張量轉(zhuǎn)移到 GPU 內(nèi)存中。當(dāng)你需要在 PyTorch 之外處理網(wǎng)絡(luò)的輸出時(shí),你也可以使用 model.to(cpu) 再將它們從 GPU 上轉(zhuǎn)移回去。我將分別使用 GPU 和不使用 GPU 進(jìn)行前向傳播和后向傳播,好為你展示著兩者之間計(jì)算速度的差異。
import time
for device in ['cpu', 'cuda']:
criterion = nn.NLLLoss()
# Only train the classifier parameters, feature parameters are frozen
optimizer = optim.Adam(model.classifier.parameters(), lr=0.001)
model.to(device)
for ii, (inputs, labels) in enumerate(trainloader):
# Move input and label tensors to the GPU
inputs, labels = inputs.to(device), labels.to(device)
start = time.time()
outputs = model.forward(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
if ii==3:
break
print(f"CUDA = {cuda}; Time per batch: {(time.time() - start)/3:.3f} seconds")
# Putting the above into functions, so they can be used later
def do_deep_learning(model, trainloader, epochs, print_every, criterion, optimizer, device='cpu'):
epochs = epochs
print_every = print_every
steps = 0
# change to cuda
model.to('cuda')
for e in range(epochs):
running_loss = 0
for ii, (inputs, labels) in enumerate(trainloader):
steps += 1
inputs, labels = inputs.to('cuda'), labels.to('cuda')
optimizer.zero_grad()
# Forward and backward passes
outputs = model.forward(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if steps % print_every == 0:
print("Epoch: {}/{}... ".format(e+1, epochs),
"Loss: {:.4f}".format(running_loss/print_every))
running_loss = 0
def check_accuracy_on_test(testloader):
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
do_deep_learning(model, trainloader, 3, 40, criterion, optimizer, 'gpu')
check_accuracy_on_test(testloader)