深度學習系列——PyTorch深度學習60分鐘(一)
寫在前面的話:這是跟著ApacheCN團隊學習pytorch的學習筆記,主要資源來自pytorch官網和ApacheCN社群
一、PyTorch 是什麼?
它是一個基於 Python 的科學計算包, 其主要是為了解決兩類場景:
- NumPy 的替代品, 以使用 GPU 的強大加速功能
- 一個深度學習研究平臺, 提供最大的靈活性和速度
個人感覺,其實無論是pytorch還是tensorflow其實都是幫忙解決了在GPU上的自動求導問題,這是對我們這些深度學習使用者來說最關鍵的。也就是說,通過這些框架,我們不用去過多地操心反向求導的過程,而是可以更多地專注於神經網路(或者說深度學習)的結構等問題。當然,它們也提供了相應的封裝來滿足一些基本需求。
二、新手入門
對於深度學習來說,一個比較重要的概念就是張量。數學上的定義是張量(Tensor)是一個定義在的一些向量空間和一些對偶空間的笛卡兒積上的多重線性對映。而簡單來說其實就是向量的推廣。在同構的意義下,第零階張量為標量,第一階張量為向量 (Vector), 第二階張量則成為矩陣 (Matrix)。對我們來說,常用的其實也就3階和4階的張量(這裡沒有把矩陣它們當成張量),更高階張量其實也很難遇到。例如一張圖片就是3階張量,包括長、寬和通道(通常是RGB3通道)。在使用時也會變成4階張量,因為會有batch的值。(也就是有很多3階張量堆在一起)。
1.初識張量
Tensors 與 NumPy 的 ndarrays 非常相似, 除此之外還可以在 GPU 上使用張量來加速計算。
from __future__ import print_function import torch # 構建一個 5x3 的矩陣 # 張量只是建立了,但是未初始化 # 可以看出,其實這裡就是生成了一個5行3列的矩陣 x = torch.Tensor(5, 3) print(x) >>tensor([[ 0.0000, -2.0000,0.0000], [-2.0000,0.0000,0.0000], [ 0.0000,0.0000,0.0000], [ 0.0000,0.0000,0.0000], [ 0.0000,0.0000,0.0000]]) # 獲取 size,注:torch.Size 實際上是一個 tuple(元組),所以它支援所有 tuple 的操作。 print(x.size()) >>torch.Size([5, 3]) #PS:torch.Size 實際上是一個 tuple(元組), 所以它支援所有 tuple(元組)的操作.
2.基本操作
加操作:
#第一種寫法 y = torch.rand(5, 3) print(x + y) >>tensor([[ 0.8042, -1.5267,0.5508], [-1.0805,0.2719,0.9532], [ 0.8435,0.5595,0.8556], [ 0.6867,0.8612,0.7824], [ 0.9080,0.1819,0.2504]]) #這裡的y就是tensor([[ 0.8042,0.4733,0.5508], #[ 0.9195,0.2719,0.9532], #[ 0.8435,0.5595,0.8556], #[ 0.6867,0.8612,0.7824], #[ 0.9080,0.1819,0.2504]]) # # x是第一部分程式碼中的x
#第二種寫法 print(torch.add(x, y)) >>tensor([[ 0.8042, -1.5267,0.5508], [-1.0805,0.2719,0.9532], [ 0.8435,0.5595,0.8556], [ 0.6867,0.8612,0.7824], [ 0.9080,0.1819,0.2504]])
#第三種寫法,提供一個輸出 tensor 作為引數 result = torch.Tensor(5, 3) torch.add(x, y, out = result) print(result) >>>>tensor([[ 0.8042, -1.5267,0.5508], [-1.0805,0.2719,0.9532], [ 0.8435,0.5595,0.8556], [ 0.6867,0.8612,0.7824], [ 0.9080,0.1819,0.2504]])
#第四種寫法,in-place(就地操作) # adds x to y y.add_(x) print(y) >>>>>>tensor([[ 0.8042, -1.5267,0.5508], [-1.0805,0.2719,0.9532], [ 0.8435,0.5595,0.8556], [ 0.6867,0.8612,0.7824], [ 0.9080,0.1819,0.2504]])
索引(類似Numpy的索引):
print(x[:, 1]) >>tensor([-2.0000,0.0000,0.0000,0.0000,0.0000])
改變大小:
x = torch.randn(4, 4) >>tensor([[ 0.2755, -0.1519,0.0257, -0.7659], [ 0.7431, -1.0414,0.5645, -1.0806], [ 0.7274, -0.5298, -1.5444, -0.2279], [-0.9928, -1.0443,0.4778, -0.2496]]) y = x.view(16) >>tensor([ 0.2755, -0.1519,0.0257, -0.7659,0.7431, -1.0414,0.5645, -1.0806,0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443, 0.4778, -0.2496]) z = x.view(-1, 8)# -1就是根據情況,由計算機自己推斷這個維數 >>tensor([[ 0.2755, -0.1519,0.0257, -0.7659,0.7431, -1.0414,0.5645, -1.0806], [ 0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443,0.4778, -0.2496]])
3.NumPy Bridge
將一個 Torch Tensor 轉換為 NumPy 陣列, 反之亦然。
Torch Tensor 和 NumPy 陣列將會共享它們的實際的記憶體位置, 改變一個另一個也會跟著改變。
#轉換一個 Torch Tensor 為 NumPy 陣列 a = torch.ones(5) print(a) >>tensor([ 1.,1.,1.,1.,1.]) b = a.numpy() print(b) >>array([1., 1., 1., 1., 1.], dtype=float32) #儘管轉換了,但是兩者依然共享記憶體 a.add_(1) print(a) print(b) >>tensor([ 2.,2.,2.,2.,2.]) >>[2. 2. 2. 2. 2.]
#轉換 NumPy 陣列為 Torch Tensor import numpy as np a = np.ones(5) >>array([1., 1., 1., 1., 1.]) b = torch.from_numpy(a) >>tensor([ 1.,1.,1.,1.,1.], dtype=torch.float64) #同樣兩者共享記憶體 np.add(a, 1, out = a) print(a) print(b) >>[2. 2. 2. 2. 2.] >>tensor([ 2.,2.,2.,2.,2.], dtype=torch.float64)
Note:
除了 CharTensor 之外, CPU 上的所有 Tensor 都支援與Numpy進行互相轉換
4.CUDA Tensors
可以使用 .cuda 方法將 Tensors 在GPU上執行.
# 只要在CUDA 是可用的情況下, 我們可以執行這段程式碼 if torch.cuda.is_available(): x = x.cuda() y = y.cuda() x + y
三、自動求導
PyTorch 中所有神經網路的核心是 autograd
自動求導包. 我們先來簡單介紹一下, 然後我們會去訓練我們的第一個神經網路。
autograd
自動求導包針對張量上的所有操作都提供了自動微分操作. 這是一個逐個執行的框架, 這意味著您的反向傳播是由您的程式碼如何執行來定義的, 每個單一的迭代都可以不一樣。
1.Variable(變數)
autograd.Variable
是包的核心類. 它包裝了張量, 並且支援幾乎所有的操作. 一旦你完成了你的計算, 你就可以呼叫 .backward()
方法, 然後所有的梯度計算會自動進行。
pytorch允許通過 .data
屬性來訪問原始的張量, 而關於該 variable
(變數)的梯度會被累計到 .grad
上去。

還有一個針對自動求導實現來說非常重要的類 - Function
。
Variable
和 Function
是相互聯絡的, 並且它們構建了一個非迴圈的圖, 編碼了一個完整的計算曆史資訊. 每一個 variable
(變數)都有一個 .grad_fn
屬性, 它引用了一個已經建立了 Variable
的 Function
(除了使用者建立的 Variable
之外 - 它們的 grad_fn is None
)
如果你想計算導數, 你可以在 Variable
上呼叫 .backward()
方法. 如果 Variable
是標量的形式(例如, 它包含一個元素資料), 你不必指定任何引數給 backward()
, 但是, 如果它有更多的元素. 你需要去指定一個 grad_output
引數, 該引數是一個匹配 shape
(形狀)的張量。
import torch from torch.autograd import Variable #建立 variable(變數) x = Variable(torch.ones(2, 2), requires_grad = True) print(x) >>tensor([[ 1.,1.], [ 1.,1.]]) #y 由操作建立,所以它有 grad_fn 屬性. y = x + 2 print(y) >>tensor([[ 3.,3.], [ 3.,3.]]) z = y * y * 3 out = z.mean() print(z, out) >>tensor([[ 27.,27.], [ 27.,27.]]) >>tensor(27.)
2.梯度
pytorch之類的框架對於我們學習者來說最大的幫助莫過於反向求導的簡單化。
我們考慮上述例子的反向求導過程,首先,先寫出整體的前向過程:
所以在反向求導時:
故而:
如果沒有框架,單純編寫這段程式碼其實比較的繁瑣。而在使用了pytorch框架後,只需要呼叫 out.backward()
,pytorch就會自動求導其導數,將其存放在 .grad
中:
out.backward() print(x.grad) >>tensor([[ 4.5000,4.5000], [ 4.5000,4.5000]])
同時,梯度的有趣應用:
x = torch.randn(3) x = Variable(x, requires_grad = True) y = x * 2 gradients = torch.FloatTensor([0.1, 1.0, 0.0001]) y.backward(gradients) print(x.grad) >>tensor([ 0.2000,2.0000,0.0002])
四、實戰之基本的卷積神經網路
神經網路可以使用 torch.nn
包構建。
autograd
實現了反向傳播功能, 但是直接用來寫深度學習的程式碼在很多情況下還是稍顯複雜, torch.nn
是專門為神經網路設計的模組化介面. nn
構建於 Autograd
之上, 可用來定義和執行神經網路. nn.Module
是 nn
中最重要的類, 可把它看成是一個網路的封裝, 包含網路各層定義以及 forward
方法, 呼叫 forward(input)
方法, 可返回前向傳播的結果。
1.定義一個網路
import torch from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 卷積層 '1'表示輸入圖片為單通道, '6'表示輸出通道數, '5'表示卷積核為5*5 # 核心 # 初始化的過程中其實沒有再定義網路結構,只是定義了一些函式 self.conv1 = nn.Conv2d(1, 6, 5) self.conv2 = nn.Conv2d(6, 16, 5) # 仿射層/全連線層: y = Wx + b self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) # 這裡的前向過程才定義了整個網路結構 def forward(self, x): # 在由多個輸入平面組成的輸入訊號上應用2D最大池化. # (2, 2) 代表的是池化操作的步幅 # 這裡正是從輸入層,通過一個卷積之後,在經過一個pool x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # 如果大小是正方形, 則只能指定一個數字 x = F.max_pool2d(F.relu(self.conv2(x)), 2) # 這邊便是將x拉值,以便用於全連線 x = x.view(-1, self.num_flat_features(x)) # 接下來就是普通的兩個全連線層 x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) # 下面是輸出層 x = self.fc3(x) return x def num_flat_features(self, x): size = x.size()[1:]# 除批量維度外的所有維度 num_features = 1 for s in size: num_features *= s return num_features net = Net() print(net) >>Net( (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1)) (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)) (fc1): Linear(in_features=400, out_features=120, bias=True) (fc2): Linear(in_features=120, out_features=84, bias=True) (fc3): Linear(in_features=84, out_features=10, bias=True) )
只要在 nn.Module
的子類中定義了 forward
函式, backward
函式就會自動被實現(利用 autograd
)。 在 forward
函式中可使用任何 Tensor
支援的操作。
並不像tensorflow需要顯式地定義引數,pytorch在上述過程中只需要使用者輸入維度資訊,引數的維度便可由計算機自動給出。網路的可學習引數通過 net.parameters()
返回, net.named_parameters
可同時返回學習的引數以及名稱。
params = list(net.parameters()) print(len(params)) print(params[0].size())# conv1的weight >>10 >>torch.Size([6, 1, 5, 5])
向前的輸入是一個 autograd.Variable, 輸出也是如此。注意: 這個網路(LeNet)的預期輸入大小是 32x32, 使用這個網上 MNIST 資料集, 請將資料集中的影象調整為 32x32
input = Variable(torch.randn(1, 1, 32, 32)) out = net(input) print(out) >>tensor([[-0.0821,0.1081,0.0103,0.1502,0.0191,0.0097, -0.0175, -0.0804, -0.0055, -0.0382]])
Note:
- torch.nn 只支援小批量(mini-batches), 不支援一次輸入一個樣本, 即一次必須是一個 batch
- nn.Conv2d 的輸入必須是 4 維的, 形如 nSamples x nChannels x Height x Width
2.損失函式
損失函式採用 (output, target) 輸入對, 並計算預測輸出結果與實際目標的距離。
在 nn 包下有幾種不同的損失函式。一個簡單的損失函式是: nn.MSELoss
計算輸出和目標之間的均方誤差
output = net(input) target = Variable(torch.arange(1, 11))# 一個虛擬的目標 criterion = nn.MSELoss() loss = criterion(output, target) print(loss)
現在, 如果你沿著 loss 反向傳播的方向使用 .grad_fn
屬性, 你將會看到一個如下所示的計算圖:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss
所以, 當我們呼叫 loss.backward()
, 整個圖與損失是有區別的, 圖中的所有變數都將用 .grad 梯度累加它們的變數。
3.反向傳播
為了反向傳播誤差, 我們所要做的就是 loss.backward()
。你需要清除現有的梯度, 否則梯度會累加之前的梯度。
現在我們使用 loss.backward()
, 看看反向傳播之前和之後 conv1
的梯度。
net.zero_grad()# 把之前的梯度清零 print('conv1.bias.grad before backward') print(net.conv1.bias.grad) >>None loss.backward() print('conv1.bias.grad after backward') print(net.conv1.bias.grad) >>tensor([ 0.1580, -0.0348, -0.1106,0.0706, -0.0937, -0.0539])
4.更新權重
實踐中使用的最簡單的更新規則是隨機梯度下降( SGD ):
可以使用簡單的 python 程式碼來實現這個:
learning_rate = 0.01 for f in net.parameters(): f.data.sub_(f.grad.data * learning_rate)
當然,更新權重的方法pytorch也已經做了封裝( torch.optim
),方便我們呼叫:
import torch.optim as optim # 新建一個優化器, 指定要調整的引數和學習率 optimizer = optim.SGD(net.parameters(), lr = 0.01) # 在訓練過程中: optimizer.zero_grad()# 首先梯度清零(與 net.zero_grad() 效果一樣) output = net(input) loss = criterion(output, target) loss.backward() optimizer.step()# 更新引數