通過PyTorch 進行深度學習
數學公式如下所示:
對于向量來說,為兩個向量的點積/內積:
我們可以將這些單元神經元組合為層和堆棧,形成神經元網絡。一個神經元層的輸出變成另一層的輸入。對于多個輸入單元和輸出單元,我們現在需要將權重表示為矩陣。
張量
實際上神經網絡計算只是對張量進行一系列線性代數運算,矩陣是張量的一種形式。向量是一維張量,矩陣是二維張量,包含 3 個索引的數組是三維向量(例如 RGB 顏色圖像)。神經網絡的基本數據結構是張量,PyTorch(以及幾乎所有其他深度學習框架)都是以張量為基礎。
使用pytorch 構建神經網絡
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import numpy as np
import torch
import helper
一般而言,PyTorch 張量的行為和 Numpy 數組相似。它們的索引都以 0 開始,并且支持切片。
改變形狀
改變張量的形狀是一個很常見的運算。首先使用 .size()
獲取張量的大小和形狀。然后,使用 .resize_()
改變張量的形狀。注意下劃線,改變形狀是原地運算。
在 Numpy 與 Torch 之間轉換
在 Numpy 數組與 Torch 張量之間轉換非常簡單并且很實用。要通過 Numpy 數組創建張量,使用 torch.from_numpy()
。要將張量轉換為 Numpy 數組,使用 .numpy()
方法。
內存在 Numpy 數組與 Torch 張量之間共享,因此如果你原地更改一個對象的值,另一個對象的值也會更改。
通過Pytorch 構建神經網絡
# 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
需要獲取數據集。這些數據位于 torchvision 軟件包中。以下代碼將下載 MNIST 數據集,然后為我們創建訓練數據集和測試數據集
# 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()
我們將訓練數據加載到了 trainloader 中,并使用 iter(trainloader)使其變成迭代器。我們將用它循環訪問數據集以進行訓練,但是現在我只獲取了第一批數據,以便查看數據。從下方可以看出,images 是一個大小為 (64, 1, 28, 28) 的張量。因此,每批有 64 個圖像、1 個顏色通道,共有 28x28 個圖像。
構建神經網絡
要通過 PyTorch 構建神經網絡,你需要使用 torch.nn 模塊。網絡本身是繼承自 torch.nn.Module 的類。你需要單獨定義每個運算,例如針對具有 784 個輸入和 128 個單元的全連接層定義為 nn.Linear(784, 128)。
該類需要包含對網絡實現前向傳遞的 forward 方法。在此方法中,你將對之前定義的每個運算傳遞輸入張量 x。torch.nn 模塊在 torch.nn.functional 中還具有一些對等的功能,例如 ReLU。此模塊通常導入為 F。要對某個層(只是一個張量)使用 ReLU 激活函數,你需要使用 F.relu(x)。以下是一些常見的不同激活函數。
對于此網絡,我將添加三個全連接層,然后添加一個預測類別的 softmax 輸出。softmax 函數和 S 型函數相似,都會將輸入調整到 0 到 1 之間,但是還會標準化這些輸入,以便所有值的和為 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
權重等參數是系統自動初始化的,但是你也可以自定義如何初始化這些權重。權重和偏差是附加到你所定義的層的張量,你可以通過 net.fc1.weight 獲取它們。
初始化權重和偏差
print(net.fc1.weight)
print(net.fc1.bias)
要自定義初始化過程,請原地修改這些張量。實際上存在 autograd 變量,因此我們需要通過 net.fc1.weight.data 獲取真正的張量。獲得張量后,可以用 0(針對偏差)或隨機正常值填充這些張量。
# 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);
前向傳遞
我們已經創建好網絡,看看傳入圖像后會發生什么。這一過程稱之為前向傳遞。我們將圖像數據轉換為張量,然后傳遞給網絡架構定義的運算。
# 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)
從上圖中可以看出,我們的網絡基本上根本不知道這個數字是什么,因為我們還沒訓練它,所有權重都是隨機的!接下來,我們將了解如何訓練該網絡,使其能學習如何正確地對這些數字進行分類。
PyTorch提供了一種方便的方法來構建這樣的網絡,其中張量通過操作順序傳遞。使用它來構建等效網絡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來命名各個圖層和操作。 請注意,字典鍵必須是唯一的,因此每個操作必須具有不同的名稱。
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
訓練神經網絡
一開始網絡很樸素,不知道將輸入映射到輸出的函數。我們通過向網絡展示實際數據樣本訓練網絡,然后調整網絡參數,使其逼近此函數。
要找到這些參數,我們需要了解網絡預測真實輸出的效果如何。為此,我們將計算損失函數(也稱為成本),一種衡量預測錯誤的指標。例如,回歸問題和二元分類問題經常使用均方損失
其中 n 是訓練樣本的數量,yi是真正的標簽,y ? i 是預測標簽
通過盡量減小相對于網絡參數的這一損失,我們可以找到損失最低且網絡能夠以很高的準確率預測正確標簽的配置。我們使用叫做梯度下降法的流程來尋找這一最低值。梯度是損失函數的斜率,指向變化最快的方向。要以最短的時間找到最低值,我們需要沿著梯度(向下)前進。可以將這一過程看做沿著最陡的路線下山。
反向傳播
對于單層網絡,梯度下降法實現起來很簡單。但是,對于更深、層級更多的神經網絡(例如我們構建的網絡),梯度下降法實現起來更復雜。我們通過反向傳播來實現,實際上是采用的微積分中的鏈式法則。最簡單的理解方法是將兩層網絡轉換為圖形表示法。
在網絡的前向傳遞過程中,我們的數據和運算從右到左。要通過梯度下降法訓練權重,我們沿著網絡反向傳播成本梯度。從數學角度來講,其實就是使用鏈式法則計算相對于權重的損失梯度。
我們使用此梯度和學習速率 α 更新權重。
對于訓練步驟來說,首先我們需要定義損失函數。在 PyTorch 中,通常你會看到它寫成了 criterion 形式。在此例中,我們使用 softmax 輸出,因此我們希望使用 criterion = nn.CrossEntropyLoss() 作為損失函數。稍后在訓練時,你需要使用 loss = criterion(output, targets) 計算實際損失。
我們還需要定義優化器,例如 SGD 或 Adam 等。我將使用 SGD,即 torch.optim.SGD,并傳入網絡參數和學習速率。
Autograd 自動計算梯度
Torch提供了一個自動編程模塊,用于自動計算張量的梯度。 它通過跟蹤在張量上執行的操作來實現此目的。 為了確保PyTorch跟蹤張量上的運算并計算梯度,您需要在張量上設置requires_grad
。 您可以使用requires_grad關鍵字在創建時執行此操作,也可以隨時使用x.requires_grad_(True)
執行此操作。
您可以使用torch.no_grad()內容關閉代碼塊的漸變:
x = torch.zeros(1,requires_grad = True)
with torch.no_grad():
... y = x * 2
y.requires_grad
false
此外,您可以使用torch.set_grad_enabled(True | False)完全打開或關閉梯度。
使用z.backward()相對于某個變量z計算梯度。 這會向后傳遞創建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)
下面我們可以看到創建y的操作,一個冪運算操作PowBackward0。
## grad_fn shows the function that generated this variable
print(y.grad_fn)
autgrad模塊會跟蹤這些操作,并知道如何計算每個操作的梯度。 通過這種方式,它能夠針對任何一個張量計算一系列操作的梯度。 讓我們將張量y減小到標量值,即平均值。
你可以檢查X和Y的梯度,但是它們現在是空的
要計算梯度,您需要在Variable z上運行.backward方法。 這將計算z相對于x的梯度
這些梯度計算對神經網絡特別有用。 對于訓練,我們需要權重的梯度與成本。 使用PyTorch,我們通過網絡向前運行數據來計算成本,然后向后計算與成本相關的梯度。 一旦我們得到了梯度,我們就可以做出梯度下降步驟。
定義網絡結構
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
構建一個網絡。 與上一部分的唯一區別是我實際上并沒有在輸出上使用softmax,而只是使用最后一層的原始輸出。 這是因為softmax的輸出是概率分布。 通常,輸出的值確實接近于零或非常接近于1。 由于將數字表示為浮點的不準確性,具有softmax輸出的計算可能會失去準確性并變得不穩定。 為了解決這個問題,我們將使用稱為logits的原始輸出來計算損失。
# 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))]))
我們需要做的第一件事就是定義我們的損失函數。 在PyTorch中,您通常會將此視為標準。 這里我們使用softmax輸出,所以我們想使用criterion = nn.CrossEntropyLoss()
作為我們的損失。 稍后在訓練時,您使用loss = criterion(output, targets)
來計算實際損失。
我們還需要定義我們正在使用的優化器,SGD或Adam,或者其他類似的東西。 在這里,我將使torch.optim.SGD
,傳遞網絡參數和學習速率。
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
首先,在循環遍歷所有數據之前,我們只考慮一個學習步驟。 PyTorch的一般過程:
- 通過網絡進行正向傳遞以獲取logits
- 使用logits計算損失
- 使用
loss.backward()
執行向后傳遞網絡以計算漸變 - 使用優化器更新權重
下面我將完成一個訓練步驟并打印出權重和梯度
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)
實際訓練
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)
推理與驗證
在訓練神經網絡之后,你現在可以使用它來進行預測。這種步驟通常被稱作推理,這是一個借自統計學的術語。然而,神經網絡在面對訓練數據時往往表現得太過優異,因而無法泛化未見過的數據。這種現象被稱作過擬合,它損害了推理性能。為了在訓練時檢測過擬合,我們測量并不在名為驗證集的訓練集中數據的性能。在訓練時,我們一邊監控驗證性能,一邊進行正則化,如 Dropout,以此來避免過擬合。在這個 notebook 中,我將向你展示如何在 PyTorch 中做到這一點。
首先,我會實現我自己的前饋神經網絡,這個網絡基于第四部分的練習中的 Fashion-MNIST 數據集構建。它是第四部分練習的解決方案,也是如何進行 Dropout 和驗證的例子。
%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)
構建網絡
跟 MNIST 數據集一樣,Fashion-MNIST 數據集中每張圖片的像素為 28x28,共 784 個數據點和 10 個類。我使用了nn.ModuleList
來加入任意數量的隱藏層。這個模型中的hidden_layers
參數為隱藏層大小的列表(以整數表示)。使用 nn.ModuleList
來寄存每一個隱藏模塊,這樣你可以在之后使用模塊方法。
由于每個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])
獲得這些輸入和輸出大小對可以通過使用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 是一種對數概率,它有許多優點。使用這種對數概率,計算往往會更加迅速和準確。為了在之后獲得類的概率,我將需要獲得輸出的指數(torch.exp
)。
我們可以使用
nn.Dropout
來在我們的網絡中加入 Dropout。這與 nn.Linear
等其他模塊的作用相似。它還將 Dropout 概率作為一種輸入傳遞到網絡中。
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)
訓練網絡
由于該模型的前向方法返回 log-softmax,因此我使用了負對數損失 作為標準。我還選用了Adam 優化器。這是一種隨機梯度下降的變體,包含了動量,并且訓練速度往往比基本的 SGD 要快。
還加入了一個代碼塊來測量驗證損失和精確度。由于我在這個神經網絡中使用了 Dropout,在推理時我需要將其關閉,否則這個網絡將會由于許多連接的關閉而表現糟糕。在 PyTorch 中,你可以使用 model.train()
和 model.eval()
來將模型調整為“訓練模式”或是“評估模式”。在訓練模式中,Dropout 為開啟狀態,而在評估模式中,Dropout 為關閉狀態。這還會影響到其他模塊,包括那些應該在訓練時開啟、在推理時關閉的模塊。
這段驗證代碼由一個通過驗證集(并分裂成幾個批次)的前向傳播組成。根據 log-softmax 輸出來計算驗證集的損失以及預測精確度。
# Create the network, define the criterion and optimizer
model = Network(784, 10, [516, 256], drop_p=0.5)## 784輸入,10輸出,兩個隱藏層
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()
推理
模型已經訓練好了,我們現在可以使用它來進行推理。之前已經進行過這一步驟,但現在我們需要使用 model.eval() 來將模型設置為推理模式。
# 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,:]);
我將模型架構和訓練代碼從最后一部分移動到一個名為fc_model的文件中。 導入此內容后,我們可以使用fc_model.Network輕松創建完全連接的網絡,并使用fc_model.train訓練網絡。
# 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)
可以想象,在每次使用神經網絡時都重新進行訓練很不現實。因此,我們可以保存之前訓練好的網絡,并在繼續訓練或是進行預測時加載網絡。 PyTorch 網絡的參數都存儲在模型的 state_dict
中。可以看到這個狀態字典包含了每個層的權重和偏差矩陣。
print("Our model: \n\n", model, '\n')
print("The state dict keys: \n\n", model.state_dict().keys())
最簡單的做法是使用
torch.save
來保存狀態字典。比如,我們可以將它保存到文件 'checkpoint.pth'
中。
torch.save(model.state_dict(), 'checkpoint.pth')
接著,我們可以使用 torch.load 來加載這個狀態字典。
state_dict = torch.load('checkpoint.pth')
print(state_dict.keys())
要將狀態字典加載到神經網絡中,你需要使用 model.load_state_dict(state_dict)
這看上去十分簡單,但實際情況更加復雜。只有當模型結構與檢查點的結構完全一致時,狀態字典才能成功加載。如果我在創建模型時使用了不同的結構,便無法順利加載。
# 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)
這意味著我們需要重建一個與訓練時完全相同的模型。有關模型結構的信息需要與狀態字典一起存儲在檢查點中。為了做到這一點,你需要構建一個字典,字典中包含重建模型的全部信息。
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')
現在,檢查點中包含了重建訓練模型所需的全部信息。你可以隨意將它編寫為函數。相似地,我們也可以編寫一個函數來加載檢查點。
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)
加載圖像數據
在實際項目中,你可能會處理一些全尺寸的圖像,比如手機相機拍攝的圖片。在這個 notebook 中,我們將會學習如何加載圖像,并使用它們來訓練神經網絡。
我們將用到來自 Kaggle 的貓狗照片數據集。
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
import torch
from torchvision import datasets, transforms
import helper
加載圖像數據最簡單是方法是使用 torchvision
中的 datasets.ImageFolder
(資料)。dataset = datasets.ImageFolder('path/to/data', transform=transforms)
'path/to/data'
是通往數據目錄的文件路徑,transforms
是一個處理步驟的列表,使用 torchvision
中的 transforms
模塊構建。ImageFolder 中的文件和目錄應按以下格式構建:
root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png
root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
每個類都有各自存儲圖像的目錄(cat 和 dog)。接著,這些圖像將被貼上摘自目錄名的標簽。所以在這里,圖像 123.png 在加載時將被貼上類標簽 cat。
Transform
當你使用 ImageFolder 加載數據后,你需要定義一些轉換。舉個例子,這些圖像的尺寸都不相同,但我們需要統一尺寸以便進行訓練。你可以使用 transforms.Resize() 來重新確定圖像尺寸,也可以使用 transforms.CenterCrop()、transforms.RandomResizedCrop() 等進行切割。我們還需要使用 transforms.ToTensor() 來將圖像轉換為 PyTorch 張量。通常,你會使用 transforms.Compose() 來將這些轉換結合到一條流水線中,這條流水線接收包含轉換的列表,并按順序運行。如下面的例子所示,它首先進行縮放,接著切割,再轉換為張量:
transforms = transforms.Compose([transforms.Resize(255),
transforms.CenterCrop(224),
transforms.ToTensor()])
我們可以使用許多種轉換,接下來我會逐步講解,你也可以查看這里的資料。
Data Loader
在加載 ImageFolder
后,你需要將它傳遞給一個 DataLoader
。DataLoader
接收數據集(比如你從 ImageFolder
中獲取的數據集),并返回不同批次的圖像以及對應的標簽。你可以設置不同參數,比如批次大小,也可以設置是否在每個階段后重組數據。
dataloader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)
在這里,dataloader
是一個生成器。要想從這個生成器中提取數據,你需要遍歷這個生成器,或是將它轉換為一個迭代器并調用 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)
數據增強
訓練神經網絡的常用策略是在輸入數據中添加隨機性。舉個例子,你可以在訓練時隨意旋轉、鏡像、縮放以及/或剪切你的圖像。這樣一來,你的神經網絡在處理位置、大小、方向不同的相同圖像時,可以更好地進行泛化。
要想隨機旋轉、縮放、剪切和翻轉圖像,你需要按以下格式定義轉換:
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 來標準化圖像。在傳入均值和標準差的列表后,顏色通道將按以下方法進行標準化:
減去 mean 能讓數據以 0 為中心,除以 std 能夠將值集中在 -1 和 1 之間。標準化有助于神經網絡使權重接近 0,這能使反向傳播更為穩定。倘若沒有標準化,網絡往往無法進行學習。
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)
遷移學習
ImageNet 是一個龐大的數據集,其中有超過一百萬張帶有標簽的圖像,來自一千個不同類別。通常,我們使用一種名為卷積層的結構訓練深度神經網絡。在這里,我并不會深入介紹卷積網絡,但如果你感興趣,可以查看這個視頻。
一旦經過訓練,這些模型便能以絕佳表現檢測未見過的圖像的特征。這種使用預先訓練的網絡來分析訓練集之外的圖像的方法被稱為遷移學習。在這里,我們將使用遷移學習來訓練一個能夠以近乎完美的準確性分類貓狗圖像的網絡。
使用 torchvision.models,你可以下載這些預先訓練的網絡,并用于你的應用中。我們現在將導入 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
大多數預先訓練的模型要求輸入為 224x224 像素的圖像。同樣地,我們需要匹配訓練模型時進行的標準化。每個顏色通道都分別進行了標準化,均值為 [0.485, 0.456, 0.406],標準差為 [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)
我們可以載入一個模型,比如 DenseNet。現在讓我們打印出這個模型的結構,以便了解細節。
這個模型主要有兩個部分,即特征和分類器。特征部分是一堆卷積層,能作為特征檢測器輸入分類器中。分類器部分是一個單獨的全連接層 (classifier): Linear(in_features=1024, out_features=1000)。這個層根據 ImageNet 數據集訓練,因此無法解決我們指定的問題。這意味著我們需要替換這個分類器,不過這些特征本身能起到很大的作用。一般來說,我認為預先訓練的網絡是絕佳的特征檢測器,可以作為簡單的前饋分類器的輸入。
# 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
在構建好模型之后,我們需要訓練分類器。然而,現在我們使用的是一個非常深的神經網絡。如果你還像之前一樣試圖在 CPU 上訓練它,這會耗費相當長的時間。因此,我們將使用 GPU 來進行運算。在 GPU 上,線性代數運算同步進行,這使得運算速度提升了 100x。我們還可以在多個 GPU 上進行訓練,這能進一步縮短訓練時間。 PyTorch 和其他深度學習框架一樣,使用 CUDA 來高效地在 GPU 上計算前向和后向傳播。在 PyTorch 中,你可以使用 model.to(cuda) 將模型參數和其他張量轉移到 GPU 內存中。當你需要在 PyTorch 之外處理網絡的輸出時,你也可以使用 model.to(cpu) 再將它們從 GPU 上轉移回去。我將分別使用 GPU 和不使用 GPU 進行前向傳播和后向傳播,好為你展示著兩者之間計算速度的差異。
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)