Learning PyTorch with examples
來自這里。
本教程通過自包含的示例來介紹PyTorch的基本概念。
PyTorch的核心是兩個主要功能:
- 可在GPU上運行的,類似於numpy的多維tensor
- 自動區分構建的和訓練的神經網絡
我們將使用全連接ReLU網絡作為示例。網絡中包含單個隱藏層,通過最小化網絡輸出與真實輸出之間的歐氏距離,用梯度下降訓練來擬合隨機數據。
Tensor
Warm-up: numpy
在介紹PyTorch之前,我們先用numpy來實現網絡。
Numpy提供了一個多維數組對象和很多操作數組的函數。Numpy是一個常用的科學計算框架,它對計算圖形、深度學習或梯度一無所知。但是,通過使用numpy操作來手動實現網絡的前向和后向傳遞,我們可以輕松地使用numpy將兩層網絡適配到隨機數據:
#!/usr/bin/env python3
import numpy as np
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = np.random.randn(N, D_in)
y = np.random.randn(N, D_out)
# 隨機初始化權重
w1 = np.random.randn(D_in, H)
w2 = np.random.randn(H, D_out)
learning_rate = 1e-6
for i in range(500):
# 前向傳遞:計算預測y
h = x.dot(w1)
h_relu = np.maximum(h, 0)
y_pred = h_relu.dot(w2)
# 計算並輸出 loss
loss = np.square(y_pred-y).sum()
print(i, loss)
# 反向傳遞,計算相對於loss的w1和w2的梯度
grad_y_pred = 2.0*(y_pred-y)
grad_w2 = h_relu.T.dot(grad_y_pred)
grad_h_relu = grad_y_pred.dot(w2.T)
grad_h = grad_h_relu.copy()
grad_h[h < 0] = 0
grad_w1 = x.T.dot(grad_h)
# 更新權重
w1 -= learning_rate*grad_w1
w2 -= learning_rate*grad_w2
PyTorch: Tensor
Numpy是一個非常好的框架,但它不能使用GPU加速它的數值計算。在現代深度神經網絡中,GPU通常提供50倍甚至更高的加速效果,所以很不幸,對現代深度神經學習來說,numpy是不夠的。
這里我們引入最基本的PyT概念:Tensor。PyTorch張量在概念上與numpy相同:張量是一個多維數組,同時PyTorch提供了很多操作張量的函數。此外,張量可以跟蹤計算圖形和梯度,這些同樣是科學計算的常用工具。
不像numpy,PyTorch張量可以使用GPU加速它的數據計算。為了在GPU上運行PyTorch張量,你只需要將它轉換成新的數據類型。
現在我們使用PyTorch張量將兩層網絡適配到隨機數據。像numpy例子那樣,我們需要手動實現網絡的前向和后向傳遞:
import torch
dtype = torch.float
device = torch.device('cpu')
# device = torch.device('cuda:0') # 若在GPU上執行,取消注釋
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
# 隨機初始化權重
w1 = torch.randn(D_in, H, device=device, dtype=dtype)
w2 = torch.randn(H, D_out, device=device, dtype=dtype)
learning_rate = 1e-6
for i in range(500):
# 前向傳遞:計算預測y
h = x.mm(w1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)
# 計算並輸出 loss
loss = (y_pred-y).pow(2).sum().item()
print(i, loss)
# 反向傳遞,計算相對於loss的w1和w2的梯度
grad_y_pred = 2.0*(y_pred-y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h)
# 更新權重
w1 -= learning_rate*grad_w1
w2 -= learning_rate*grad_w2
Autograd
PyTorch:Tensor and autograd
在上面的例子中,我們必須手動實現我們神經網絡的前向和反向傳播。手動實現反向傳播對於只有兩層的網絡來說並不是什么難事,但對於大型復雜的網絡來說會變得非常繁瑣。
謝天謝地,在神經網絡中我們可以使用自動分化(automatic differentiation)來自動計算反向傳播。在PyTorch中的autograd包提供了完整的功能。在使用autograd
時,網絡的前向傳遞會定義一個計算圖,圖中的節點是張量,邊界是從輸入張量產生輸出張量的函數。提供這個圖的反饋,你可以輕松計算梯度。
這聽起來很復雜,在實踐中使用起來相當簡單。每個張量表示計算圖中的一個節點。如果x
是一個x.requires_grad=True
的張量,那么x.grad
就是另一個包含x
對某個標量的梯度的另一個張量。
現在我們使用PyTorch的Tensor和autograd來實現我們的兩層網絡,我們不再需要手動實現網絡中反向傳遞。
import torch
dtype = torch.float
device = torch.device('cpu')
# device = torch.device('cuda:0') # 若在GPU上執行,取消注釋
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
# 隨機初始化權重
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)
learning_rate = 1e-6
for i in range(500):
# 前向傳遞:使用Tensor的操作來計算預測y,這些操作跟使用Tensor計算前向傳遞的操作很相似,
# 但是我們不需要保留中間變量的引用,因為我們不用手動執行后向傳遞。
y_pred = x.mm(w1).clamp(min=0).mm(w2)
# 計算並輸出 loss
loss = (y_pred-y).pow(2).sum()
print(i, loss.item())
# 反向傳遞,計算相對於loss的w1和w2的梯度
loss.backward()
# 更新權重
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
PyTorch:定義新的autograd函數
實際上,每個原始的autograd運算符實際上是兩個作用於張量的函數。forward函數從輸入張量計算出輸出張量,backward函數接收輸出張量相對於某個標量值的梯度,並計算輸入張量相對於該標量值的梯度。
在PyTorch中,通過定義torch.autograd.Function
的子類和實現其中的forward
和backward
我們可以很容易地定義我們自己的autograd操作。然后,我們可以通過構造一個實例並像函數一樣調用它,傳遞包含輸入數據的張量來使用新的autograd操作符。
在這個例子中,我們定義了自定義的autograd函數來執行ReLU非線性計算,並使用它來實現我們的兩層網絡:
import torch
class MyReLU(torch.autograd.Function):
@staticmethod
def forward(ctx,input):
'''
在正向傳遞中,我們收到一個包含輸入和返回的張量
包含輸出的張量。ctx是一個可以使用的上下文對象
隱藏信息以備向后計算。在向后傳遞過程中你可以
使用ctx.save_for_backward方法任意緩存使用的對象。
'''
ctx.save_for_backward(input)
return input.clamp(min=0)
@staticmethod
def backward(ctx,grad_output):
'''
在后向傳遞中,我們得到一個關於輸出的包含損失梯度的張量,我們需要計算關於輸入的損失梯度。
'''
input, = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input[input < 0] = 0
return grad_input
dtype = torch.float
device = torch.device('cpu')
# device = torch.device('cuda:0') # 若在GPU上執行,取消注釋
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
# 隨機初始化權重
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)
learning_rate = 1e-6
for i in range(500):
# 使用我們自定義的函數
relu = MyReLU.apply
# 前向傳遞:使用Tensor的操作來計算預測y,這些操作跟使用Tensor計算前向傳遞的操作很相似,
# 但是我們不需要保留中間變量的引用,因為我們不用手動執行后向傳遞。
y_pred = relu(x.mm(w1)).mm(w2)
# 計算並輸出 loss
loss = (y_pred-y).pow(2).sum()
print(i, loss.item())
# 反向傳遞,計算相對於loss的w1和w2的梯度
loss.backward()
# 更新權重
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
TensorFlow: 靜態圖
PyTorch的autograd跟TensorFlow很相似:在兩個框架中我們都定義計算圖,並且利用自動分化計算梯度。它們兩最大的不同之處在於TensorFlow的計算圖是靜態,而PyTorch使用動態計算圖。
在TensorFlow中,一旦我們定義了計算圖,那么它將被一遍遍地執行,盡管可能會輸入不同的數據到圖中。在PyTorch中,每次前向傳遞都將定義一個新的計算圖。
靜態圖很好,因為可以預先優化圖。例如,為了提高效率,框架可能決定融合一些圖形操作,或者提出一種策略,將圖形分布到許多gpu或許多機器上。如果您反復重用相同的圖,那么這種潛在的昂貴的預先優化可以在反復運行相同的圖時進行分攤。
靜態和動態圖不同之處在與控制流(control flow)。對於某些模型,我們可能希望對每個數據點執行不同的計算。例如,對於每個數據點,可以按不同的時間步長展開一個遞歸網絡,這種展開可以實現為一個循環。對於靜態圖,循環結構需要成為圖的一部分;因此,TensorFlow提供了tf.scan
等操作符掃描是否將循環嵌入到圖中。對於動態圖,情況更簡單:因為我們為每個示例動態構建圖,所以我們可以使用常規命令式流控制來執行每個輸入不同的計算。
與上面的PyTorch autograd例子相反,這里我們使用TensorFlow來安裝一個簡單的兩層網絡:
import tensorflow as tf
import numpy as np
# 首先定義計算圖
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 為輸入和目標數據創建占位符;它們在執行時會被真實數據填充
x = tf.placeholder(tf.float32, shape=(None, D_in))
y = tf.placeholder(tf.float32, shape=(None, D_out))
# 隨機初始化權重
w1 = tf.Variable(tf.random_normal((D_in, H)))
w2 = tf.Variable(tf.random_normal((H, D_out)))
# 前向傳遞:通過TensorFlow張量上的操作計算預測的y值
# 注意,這段代碼並不執行任何數值操作,它僅僅是設置計算圖以便我們之后執行
h = tf.matmul(x, w1)
h_relu = tf.maximum(h, tf.zeros(1))
y_pred = tf.matmul(h_relu, w2)
# 通過TensorFlow張量上的操作計算損失
loss = tf.reduce_sum((y-y_pred)**2.0)
# 計算loss關於w1和w2的梯度
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2])
# 使用梯度更新權重。實際上在更新權重時,我們需要在執行圖的過程中評估新的權重new_w1和new_w2。
# 注意,在TensorFlow中更新權重值得操作是計算圖的一部分,而在PyTorch中,這些操作發生在計算圖之外。
learning_rate = 1e-6
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2)
# 現在,我們已經構建玩計算圖,那么我們輸入一個TensorFlow回話來實際執行圖
with tf.Session() as sess:
# 執行圖之前需要先初始化變量w1和w2
sess.run(tf.global_variables_initializer())
# 創建numpy數組來存儲輸入x和目標y的真實數據
x_value = np.random.randn(N, D_in)
y_value = np.random.randn(N, D_out)
for _ in range(500):
# 重復執行多次。它每次執行時,我們使用參數`feed_dict`將x_value賦值給x,y_value賦值給y。
# 我們每次執行計算圖時,我們需要計算loss、new_w1和new_w2的值,這些值得張量將以numpy數組的形式返回
loss_value, _, _ = sess.run([loss, new_w1, new_w2], feed_dict={
x: x_value, y: y_value})
print(loss_value)
nn 模塊
PyTorch: nn
計算圖和autograd
是定義復雜運算和自動求導的非常強大的典范,然而對於大型的神經網絡,原始的autograd
是遠遠不夠。
在構建神經網絡時,我們經常考慮將計算分層進行,其中一些計算具有可學習的參數,這些參數將在學習過程中得到優化。
在TensorFlow中,Keras、TensorFlow-Slim和TFLearn等包提供了對原始計算圖的高級抽象,這些抽象對構建神經網絡有很大幫助。
在PyTorch中,nn
包提供了同樣的服務。nn
包中定義了一系列的Modules,可以看做是神經網的層。模型接受輸入張量並計算輸出張量,同時像張量包含可學習參數一樣保留內部狀態。nn
包中同樣定義了一系列的損失函數以便訓練神經網。
在接下來的例子里使用nn
包來實現我們的兩層網絡:
import torch
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 使用nn包將我們的模型定義為一系列的層。nn.Sequential是一個模型,它包含其它模型並按順序使用它們來生成輸出。
# 每次模型使用線性函數從輸入中計算輸出,並保留權重和偏置的張量。
model = torch.nn.Sequential(
torch.nn.Linear(D_in,H),
torch.nn.ReLU(),
torch.nn.Linear(H,D_out),
)
# nn包中也包含了常用的損失函數定義,這里我們使用Mean Squared Error(MSE,均方誤差)作為我們的損失函數
loss_fn = torch.nn.MSELoss(reduction='sum')
learning_rate = 1e-4
for i in range(500):
# 前向傳遞:使用模型計算關於x的預測y
y_pred = model(x)
# 計算損失
loss = loss_fn(y_pred,y)
print(i,loss.item())
# 反向傳遞前零化梯度
model.zero_grad()
# 反向傳遞
loss.backward()
# 更新權重
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
PyTorch: optim(優化)
到目前為止,我們通過手動改變帶有可學習參數的張量的方式(使用torch.no_grad()
或者.data
的方式避免在autograd過程中追蹤歷史記錄)來更新我們模型的權重。對於像隨機梯度下降這樣的簡單優化算法,這並不是一個巨大的負擔,但在實踐中,我們經常使用更復雜的優化器(如AdaGrad、RMSProp、Adam等)來訓練神經網絡。
在PyTorch中,optim
包抽象了優化算法的概念,並提供了常用優化算法的實現。
在接下來的例子中,我們想之前一樣使用nn
包定義我們的模型,但是我們使用optim
包提供的Adam算法來優化我們的模型:
import torch
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 使用nn包將我們的模型定義為一系列的層。nn.Sequential是一個模型,它包含其它模型並按順序使用它們來生成輸出。
# 每次模型使用線性函數從輸入中計算輸出,並保留權重和偏置的張量。
model = torch.nn.Sequential(
torch.nn.Linear(D_in,H),
torch.nn.ReLU(),
torch.nn.Linear(H,D_out),
)
# nn包中也包含了常用的損失函數定義,這里我們使用Mean Squared Error(MSE,均方誤差)作為我們的損失函數
loss_fn = torch.nn.MSELoss(reduction='sum')
# 學習率和優化算法
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(),lr=learning_rate)
for i in range(500):
# 前向傳遞:使用模型計算關於x的預測y
y_pred = model(x)
# 計算損失
loss = loss_fn(y_pred,y)
print(i,loss.item())
# 反向傳遞前零化梯度
optimizer.zero_grad()
# 反向傳遞
loss.backward()
# 調用優化算法的setp()函數來更新參數
optimizer.step()
PyTorch: 自定義nn模型
有時候你可能需要比現有的一系列模型更復雜的特殊模型,這是你可以通過將nn.Module
子類化的方式定義你自己的模型,並定義forward
函數來接受輸入張量並使用其他模塊或張量上的其他autograd操作來生成輸出張量。
接下來我們使用自定義模型子類來實現我們的兩次網絡:
import torch
class TwoLayerNet(torch.nn.Module):
def __init__(self,D_in,H,D_out):
super(TwoLayerNet,self).__init__()
self.linear1 = torch.nn.Linear(D_in,H)
self.linear2 = torch.nn.Linear(H,D_out)
def forward(self,x):
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 實例化自定義模型
model = TwoLayerNet(D_in,H,D_out)
# 損失函數和優化函數
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(),lr=1e-4)
for i in range(500):
# 前向傳遞:使用模型計算關於x的預測y
y_pred = model(x)
# 計算損失
loss = criterion(y_pred,y)
print(i,loss.item())
# 反向傳遞前零化梯度
optimizer.zero_grad()
# 反向傳遞
loss.backward()
# 調用優化算法的setp()函數來更新參數
optimizer.step()
PyTorch: 控制流 + 權重共享
作為動態圖和共享權重的一個例子,我們實現一個分成奇怪的模型:一個全連接ReLU網絡,每個前向傳遞從1到4之間選擇一個隨機數並且使用那么多隱藏層,重復多次使用相同的權重計算最深的隱藏層。
對於這個模型,我們可以使用普通的Python流控制來實現循環,並且我們可以通過在定義正向傳遞時多次重用同一個模型來實現最內層之間的權重共享。
通過子類化的方式我們可以輕松實現這個模型:
import random
import torch
class DynamicNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
super(DynamicNet, self).__init__()
self.input_linear = torch.nn.Linear(D_in, H)
self.middle_linear = torch.nn.Linear(H, H)
self.output_linear = torch.nn.Linear(H, D_out)
def forward(self, x):
h_relu = self.input_linear(x).clamp(min=0)
for _ in range(random.randint(0, 3)):
h_relu = self.middle_linear(h_relu).clamp(min=0)
y_pred = self.output_linear(h_relu)
return y_pred
# N是批量大小,D_in 是輸入維度,H 是隱藏維度, D_out 是輸出維度
N, D_in, H, D_out = 64, 1000, 100, 10
# 隨機創建輸入輸出數據
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 實例化自定義模型
model = DynamicNet(D_in, H, D_out)
# 損失函數和優化函數
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for i in range(500):
# 前向傳遞:使用模型計算關於x的預測y
y_pred = model(x)
# 計算損失
loss = criterion(y_pred, y)
print(i, loss.item())
# 反向傳遞前零化梯度
optimizer.zero_grad()
# 反向傳遞
loss.backward()
# 調用優化算法的setp()函數來更新參數
optimizer.step()