深度學習網絡一般量級都很大,包含數百個層級,這也是爲什麼叫“深度”學習網絡。你可以像在上個 notebook 展示的一樣,僅使用權重矩陣構建深度網絡,但是這通常很繁瑣並且不好實施。PyTorch 有一個很方便的模塊 nn
,可以有效地構建大型神經網絡。
# Import necessary packages
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import numpy as np
import torch
import helper
import matplotlib.pyplot as plt
現在我們要構建一個大型網絡,並用它解決識別圖像中的文字的這一難題。我們將使用 MNIST 數據集,這個數據集由灰色的手寫數字組成。每個圖像都是 28x28,如以下示例所示:
我們的目標是構建一個神經網絡,可以預測圖像中的數字。
首先,我們需要獲取數據集。這些數據位於 torchvision
軟件包中。以下代碼將下載 MNIST 數據集,然後爲我們創建訓練數據集和測試數據集。
### Run this cell
from torchvision import datasets, transforms
# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)),
])
# Download and load the training data
trainset = datasets.MNIST('~/.pytorch/MNIST_data/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
我們將訓練數據加載到 trainloader
中,並使用 iter(trainloader)
使其變成迭代器。之後,我們將用它循環訪問數據集以進行訓練,例如
for image, label in trainloader:
## do things with images and labels
我在創建 trainloader
時,將批次大小設爲 64,並設置爲 shuffle=True
。批次大小是指我們在一次迭代中從數據加載器獲取並經過網絡的圖像數量。shuffle=True
表示每次重新訪問數據加載器時,隨機重排數據集。但是現在我僅獲取第一批數據,以便查看數據。從下方可以看出,images
是一個張量,大小爲 (64, 1, 28, 28)
。因此,每批有 64 個圖像,圖像有 1 個顏色通道,共有 28x28 個圖像。
dataiter = iter(trainloader)
images, labels = dataiter.next()
print(type(images))
print(images.shape)
print(labels.shape)
<class 'torch.Tensor'>
torch.Size([64, 1, 28, 28])
torch.Size([64])
下面是一個圖像示例。
plt.imshow(images[1].numpy().squeeze(), cmap='Greys_r');
首先,我們要使用權重矩陣和矩陣乘法,用此數據集構建一個簡單的網絡。然後,我們將學習如何使用 PyTorch 的 nn
模塊構建該網絡。
神經網絡又稱爲全連接或密集網絡。一個層級中的每個單元都與下個層級中的每個單元相連。在全連接網絡中,每個層級的輸入必須是一維向量(可以作爲一批樣本堆疊爲二維張量)。但是,我們的圖像是 28x28 二維張量,因此我們需要將其轉換爲一維向量。考慮到大小問題,我們需要將形狀爲 (64, 1, 28, 28)
的批次圖像變形爲 (64, 784)
,784 等於 28 x 28。這一步通常稱爲扁平化,我們將二維圖像扁平化爲一維向量。
之前,我們試過了構建具有一個輸出單元的簡單網絡。現在,我們需要 10 個輸出單元,每個數字對應一個單元。如果要預測出圖像中顯示的數字,我們必須計算該圖像屬於任何數字或類別的概率。我們會得到一個離散概率分佈,告訴我們圖像最有可能屬於哪個類別。這就是說,我們需要 10 個輸出單元,對應 10 個類別(數字)。下面講解如何將網絡輸出轉換爲概率分佈。
**練習:**將
images
扁平化。然後構建一個多層網絡,有 784 個輸入單元、256 個隱藏單元和 10 個輸出單元,並對權重和偏差使用隨機張量。目前,我們對隱藏層使用 S 型激活函數。輸出層暫時不需要激活函數,下一步我們將添加計算概率分佈的激活函數。
images[0].shape
torch.Size([1, 28, 28])
## Your solution
def activation(x):
return 1./(1+torch.exp(-x))
# Flatten the input images
inputs = images.view(images.shape[0], -1)
print(inputs.shape)
w1 = torch.randn(784,256)
b1 = torch.randn(256)
w2 = torch.randn(256,10)
b2 = torch.randn(10)
h = activation(torch.mm(inputs,w1) + b1)
#out = # output of your network, should have shape (64,10)
out = torch.mm(h,w2) + b2
torch.Size([64, 784])
out.shape
torch.Size([64, 10])
現在網絡有 10 個輸出了。我們向網絡中傳入一個圖像,並獲得類別概率分佈,告訴我們圖像最有可能屬於哪個數字/類別。結果如下所示:
可以看出每個類別的概率大致相等。這是未訓練網絡的結果,網絡尚未見過任何數據,因此返回均勻分佈,每個類別的概率相等。
可以用 [softmax 函數]計算概率分佈(https://en.wikipedia.org/wiki/Softmax_function)。數學公式爲:
它會將每個輸入 都縮放到 0 和 1 之間並標準化值,得出標準概率分佈,其中概率總和是 1。
**練習:**實現一個進行 softmax 運算的函數
softmax
,並針對批次中的每個樣本返回概率分佈。注意,在運算時需要注意形狀。如果有一個張量a
的形狀爲(64, 10)
,另一個張量b
的形狀爲(64,)
,進行a/b
運算將出錯,因爲 PyTorch 會對列進行除法運算(稱爲廣播),但是大小不匹配。提示:對於 64 個樣本中的每個樣本,你可以除以一個值,即分母中的和。因此需要使b
變形爲(64, 1)
。這樣的話,PyTorch 將使a
中每行的10 個值除以b
中每行的一個值。另外,要注意求和的方式。你要在torch.sum
中定義dim
。dim=0
會對行求和,而dim=1
會對列求和。
def softmax(x):
## TODO: Implement the softmax function here
return torch.exp(x)/(torch.sum(torch.exp(x),dim=1).view(-1,1))
# Here, out should be the output of the network in the previous excercise with shape (64,10)
probabilities = softmax(out)
# Does it have the right shape? Should be (64, 10)
print(probabilities.shape)
# Does it sum to 1?
print(probabilities.sum(dim=1))
torch.Size([64, 10])
tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000])
通過 PyTorch 構建網絡
PyTorch 提供了nn
模塊,大大地簡化了網絡構建過程。我將演示如何構建上述同一個網絡,即包含 784 個輸入、256 個隱藏單元、10 個輸出單元和一個 softmax 輸出。
from torch import nn
class Network(nn.Module):
def __init__(self):
super().__init__()
# Inputs to hidden layer linear transformation
self.hidden = nn.Linear(784, 256)
# Output layer, 10 units - one for each digit
self.output = nn.Linear(256, 10)
# Define sigmoid activation and softmax output
self.sigmoid = nn.Sigmoid()
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
# Pass the input tensor through each of our operations
x = self.hidden(x)
x = self.sigmoid(x)
x = self.output(x)
x = self.softmax(x)
return x
分步講解。
class Network(nn.Module):
先繼承 nn.Module
。與 super().__init__()
相結合,創建一個跟蹤架構的類,並提供大量有用的方法和屬性。注意,在爲網絡創建類時,必須繼承 nn.Module
。類可以隨意命名。
self.hidden = nn.Linear(784, 256)
這行創建一個線性轉換模塊 ,其中有 784 個輸入和 256 個輸出,並賦值給 self.hidden
。該模塊會自動創建權重和偏差張量,供我們在 forward
方法中使用。創建網絡 (net
) 後,你可以使用 net.hidden.weight
和 net.hidden.bias
訪問權重和偏差張量。
self.output = nn.Linear(256, 10)
同樣,這裏會創建另一個有 256 個輸入和 10 個輸出的線性轉換。
self.sigmoid = nn.Sigmoid()
self.softmax = nn.Softmax(dim=1)
然後,我定義了 S 型激活函數和 softmax 輸出的運算。在 nn.Softmax(dim=1)
中設置 dim=1
會計算各個列的 softmax 值。
def forward(self, x):
用 nn.Module
創建的 PyTorch 網絡必須定義 forward
方法。它會接受一個張量 x
並將其傳入你在 __init__
方法中定義的運算。
x = self.hidden(x)
x = self.sigmoid(x)
x = self.output(x)
x = self.softmax(x)
我們將輸入張量 x
傳入重新賦值給 x
的每個運算。可以看出輸入張量經過隱藏層,然後經過 S 型函數、輸出層,最終經過 softmax 函數。.變量可以命名爲任何名稱,只要運算的輸入和輸出與你要構建的網絡架構匹配即可。你在 __init__
方法中的定義順序不重要,但是需要在 forward
方法中正確地設定運算順序。
現在我們可以創建一個 Network
對象。
# Create the network and look at it's text representation
model = Network()
model
Network(
(hidden): Linear(in_features=784, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
(sigmoid): Sigmoid()
(softmax): Softmax(dim=1)
)
你可以使用 torch.nn.functional
模塊來更簡練清晰地定義網絡。這是最常見的網絡定義方式,因爲很多運算是簡單的元素級函數。我們通常將此模塊導入爲 F
,即 import torch.nn.functional as F
。
import torch.nn.functional as F
class Network(nn.Module):
def __init__(self):
super().__init__()
# Inputs to hidden layer linear transformation
self.hidden = nn.Linear(784, 256)
# Output layer, 10 units - one for each digit
self.output = nn.Linear(256, 10)
def forward(self, x):
# Hidden layer with sigmoid activation
x = F.sigmoid(self.hidden(x))
# Output layer with softmax activation
x = F.softmax(self.output(x), dim=1)
return x
激活函數
到目前爲止,我們只學習了 softmax 激活函數,但是通常任何函數都可以用作激活函數。但是,要使網絡能逼近非線性函數,激活函數必須是非線性函數。下面是一些常見的激活函數示例:Tanh(雙曲正切)和 ReLU(修正線性單元)。
在實踐中,ReLU 幾乎一直用作隱藏層激活函數。
構建神經網絡
**練習:**請創建如下網絡:輸入層有 784 個單元,然後是有 128 個單元的隱藏層和一個 ReLU 激活函數,接着是有 64 個單元的隱藏層和一個 ReLU 激活函數,最終是一個應用 softmax 激活函數的輸出層(如上所示)。你可以通過
nn.ReLU
模塊或F.relu
函數應用 ReLU 激活函數。
## Your solution here
import torch.nn.functional as F
class Network2(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784,128)
self.fc2 = nn.Linear(128,64)
self.fc3 = nn.Linear(64,10)
def forward(self,x):
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 = Network2()
model
Network2(
(fc1): Linear(in_features=784, out_features=128, bias=True)
(fc2): Linear(in_features=128, out_features=64, bias=True)
(fc3): Linear(in_features=64, out_features=10, bias=True)
)
初始化權重和偏差
權重和偏差會自動初始化,但是你可以自定義它們的初始化方式。權重和偏差是附加到所定義層級上的張量,例如,你可以通過 model.fc1.weight
獲取權重。
print(model.fc1.weight)
print(model.fc1.bias)
Parameter containing:
tensor([[-1.6068e-02, 2.8058e-02, -2.9757e-02, ..., 8.4808e-03,
2.6421e-02, -3.4739e-02],
[-6.4635e-03, -3.3026e-02, -3.5060e-02, ..., 3.0519e-02,
2.3519e-02, -1.4705e-02],
[-2.1029e-02, 3.2024e-02, 3.2000e-02, ..., -6.7857e-03,
-1.1438e-02, -2.6054e-02],
...,
[-2.3466e-02, 1.2761e-02, -2.2726e-02, ..., 3.4432e-02,
2.3199e-02, -1.9637e-02],
[ 2.5058e-02, -5.8062e-05, 1.8142e-02, ..., 1.9909e-02,
-2.6190e-02, 3.1056e-02],
[-2.2445e-02, 5.3423e-03, 1.7461e-02, ..., -9.7509e-03,
3.2743e-02, -2.5118e-02]], requires_grad=True)
Parameter containing:
tensor([ 0.0014, 0.0233, 0.0219, -0.0185, 0.0222, -0.0024, -0.0255, 0.0157,
0.0072, 0.0201, 0.0038, -0.0251, -0.0132, 0.0017, 0.0061, 0.0101,
-0.0210, 0.0258, 0.0300, 0.0309, 0.0084, -0.0286, 0.0284, -0.0032,
0.0276, -0.0298, -0.0030, 0.0282, -0.0100, -0.0056, -0.0128, -0.0031,
-0.0280, 0.0332, -0.0323, 0.0226, 0.0114, -0.0356, 0.0230, 0.0247,
-0.0313, -0.0151, 0.0189, -0.0210, 0.0009, -0.0101, 0.0129, 0.0146,
-0.0115, 0.0252, 0.0207, -0.0214, 0.0151, 0.0304, 0.0309, -0.0351,
-0.0306, -0.0242, -0.0215, 0.0264, 0.0217, 0.0054, -0.0261, -0.0332,
-0.0015, 0.0269, 0.0251, 0.0146, -0.0188, 0.0071, 0.0028, 0.0005,
0.0147, 0.0264, 0.0317, 0.0100, 0.0232, 0.0255, -0.0185, -0.0340,
-0.0313, 0.0149, -0.0054, 0.0191, -0.0319, 0.0168, 0.0066, -0.0026,
0.0063, -0.0117, -0.0267, -0.0316, -0.0011, -0.0104, 0.0209, -0.0148,
-0.0124, 0.0105, 0.0202, 0.0283, -0.0299, -0.0349, -0.0305, -0.0013,
-0.0300, -0.0196, 0.0119, -0.0356, 0.0186, 0.0018, -0.0122, 0.0341,
-0.0061, 0.0178, 0.0072, 0.0165, 0.0109, 0.0207, -0.0104, 0.0335,
-0.0340, -0.0267, 0.0083, 0.0019, -0.0068, 0.0283, -0.0017, 0.0217],
requires_grad=True)
要自定義初始化過程,我們需要原地修改這些張量。這些實際上是 autograd 變量,因此我們需要使用 model.fc1.weight.data
獲取實際張量。獲得張量後,我們可以用 0(對於偏差)或隨機正常值填充這些張量。
# Set biases to all zeros
model.fc1.bias.data.fill_(0)
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., 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.,
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., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0.])
# sample from random normal with standard dev = 0.01
model.fc1.weight.data.normal_(std=0.01)
tensor([[-0.0089, -0.0081, 0.0015, ..., -0.0061, 0.0139, -0.0154],
[ 0.0182, -0.0032, -0.0079, ..., -0.0045, 0.0023, 0.0129],
[ 0.0056, -0.0079, -0.0092, ..., 0.0078, 0.0002, 0.0045],
...,
[-0.0131, -0.0106, 0.0117, ..., -0.0040, 0.0003, 0.0138],
[ 0.0101, -0.0044, -0.0056, ..., 0.0275, -0.0006, -0.0011],
[-0.0067, 0.0041, -0.0168, ..., 0.0028, -0.0021, 0.0058]])
前向傳遞
我們已經創建好網絡,看看傳入圖像後會發生什麼。
# Grab some data
dataiter = iter(trainloader)
images, labels = dataiter.next()
# Resize images into a 1D vector, new shape is (batch size, color channels, image pixels)
images.resize_(64, 1, 784)
# or images.resize_(images.shape[0], 1, 784) to automatically get batch size
# Forward pass through the network
img_idx = 0
ps = model.forward(images[img_idx,:])
img = images[img_idx]
helper.view_classify(img.view(1, 28, 28), ps)
是的,我們的網絡還不能判斷出這個數字。這是因爲我們尚未訓練它,所有權重都是隨機的!
使用 nn.Sequential
PyTorch 提供了一種方便的方法來構建這類網絡(其中張量按順序執行各種運算):nn.Sequential
(文檔)。使用它來構建等效網絡:
# 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)
Sequential(
(0): Linear(in_features=784, out_features=128, bias=True)
(1): ReLU()
(2): Linear(in_features=128, out_features=64, bias=True)
(3): ReLU()
(4): Linear(in_features=64, out_features=10, bias=True)
(5): Softmax(dim=1)
)
模型和之前一樣:輸入層有 784 個單元,然後是有 128 個單元的隱藏層和一個 ReLU 激活函數,然後是有 64 個單元的隱藏層和另一個 ReLU 激活函數,然後是有 10 個單元的輸出層和 softmax 輸出。
通過傳入相應的索引即可執行運算。例如,如果你想獲得第一個線性運算並查看權重,可以使用 model[0]
。
print(model[0])
model[0].weight
Linear(in_features=784, out_features=128, bias=True)
Parameter containing:
tensor([[-0.0339, 0.0239, 0.0196, ..., -0.0213, 0.0327, -0.0168],
[ 0.0067, 0.0287, -0.0282, ..., -0.0050, -0.0125, -0.0198],
[ 0.0200, 0.0315, 0.0276, ..., 0.0041, -0.0310, 0.0041],
...,
[-0.0131, -0.0002, -0.0304, ..., -0.0311, 0.0086, -0.0118],
[ 0.0228, -0.0122, 0.0024, ..., -0.0216, 0.0327, -0.0198],
[ 0.0094, 0.0240, 0.0078, ..., 0.0053, -0.0037, -0.0161]],
requires_grad=True)
還可以傳入 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
Sequential(
(fc1): Linear(in_features=784, out_features=128, bias=True)
(relu1): ReLU()
(fc2): Linear(in_features=128, out_features=64, bias=True)
(relu2): ReLU()
(output): Linear(in_features=64, out_features=10, bias=True)
(softmax): Softmax(dim=1)
)
現在你可以通過整數或名稱訪問層級了
print(model[0])
print(model.fc1)
Linear(in_features=784, out_features=128, bias=True)
Linear(in_features=784, out_features=128, bias=True)
在下個 notebook 中,我們將學習如何訓練神經網絡,以便準確預測 MNIST 圖像中出現的數字。