注意:這是舊版本的PyTorch教程的一部分。你可以在Learn the Basics檢視最新的開始目錄。
該教程通過幾個獨立的例子較少了PyTorch的基本概念。
核心是:PyTorch提供了兩個主要的特性:
- 一個n維的Tensor,與Numpy相似但可以在GPU上執行
- 構建和訓練神經網路的自動微分
我們將使用一個三階多項式擬合 \(y=sin(x)\) 的問題作為我們的執行示例。該網路會有4個引數,將使用梯度下降來訓練,通過最小化神經網路輸出和真值之間的歐氏距離來擬合隨機資料。
Tensors
熱身:numpy
在介紹PyTorch之前,我們首先使用numpy實現網路
Numpy提供了一個n維的array物件,以及對陣列操作的多種方法。Numpy是一個用於科學計算的通用框架,它沒有關於計算圖、深度學習、梯度的任何內容。但是我們可以利用numpy操作,通過人工實現貫穿網路的前向和後向傳遞,從而簡單的向sin函式擬合一個三階多項式。
# -*- coding: utf-8 -*-
import numpy as np
import math
# Create random input and output data
x = np.linspace(-math.pi, math.pi, 2000) # 生成含有2000個數的-π到π的等差數列
y = np.sin(x)
# Randomly initialize weights
a = np.random.randn() # 返回浮點數
b = np.random.randn()
c = np.random.randn()
d = np.random.randn()
learning_rate = 1e-6
for t in range(2000):
# Forward pass: compute predict y
# y = a + b x + c x^2 + d x^3
y_pred = a + b * x + c * x ** 2 + d * x ** 3
# Compute and print loss
loss = np.square(y_pred - y).sum() # 所有樣本與真值的差值平方的和
if t % 100 == 99:
print(t, loss)
# Backprop to compute gradients of a, b, c, d with respect to loss
grad_y_pred = 2.0 * (y_pred - y) # loss關於y_pred的偏導(梯度),這裡沒有對所有樣本求和
grad_a = grad_y_pred.sum() # 這裡及下面都要對所有樣本得到的梯度求和
grad_b = (grad_y_pred * x).sum()
grad_c = (grad_y_pred * x ** 2).sum()
grad_d = (grad_y_pred * x ** 3).sum()
# Update weights
a -= learning_rate * grad_a
b -= learning_rate * grad_b
c -= learning_rate * grad_c
d -= learning_rate * grad_d
print(f"Result: y = {a} + {b} x + {c} x^2 + {d} x^3")
PyTorch: Tensors
Numpy是一個強大的框架,但是它無法使用GPUs加速數值計算。對於現代的深度神經網路,GPUs通常提供了50倍或更高的加速效能,所以很遺憾,numpy對於現代的深度學習是不夠的。
現在介紹PyTorch基礎中的基礎:Tensor。PyTorch Tensor概念上來說與numpy array相同:一個Tensor就是一個n維陣列,並且PyTorch提供了許多用於tensor的操作。在幕後,張量可以跟蹤計算圖和梯度,但它們也可用作科學計算的通用工具。
而且不像numpy,PyTorch Tensors可以使用GPUs加速數值計算。簡單地制定正確的裝置,即可在GPU上執行PyTorch tensor。
這裡我們使用PyTorch Tensors為sin函式擬合一個3階多項式。像上面的numpy例子一樣,我們需要手動實現貫穿網路的前向和後向傳遞:
# -*- coding: utf-8 -*-
import torch
import math
dtype = torch.float
device = torch.device('cpu')
# device = torch.device('cuda:0') # Uncomment this to run on GPU
# Create random input and output data
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
# Randomly initialize weights
a = torch.randn((), device=device, dtype=dtype)
b = torch.randn((), device=device, dtype=dtype)
c = torch.randn((), device=device, dtype=dtype)
d = torch.randn((), device=device, dtype=dtype)
learning_rate = 1e-6
for t in range(2000):
# Forward pass: compute predicted y
y_pred = a + b * x + c * x ** 2 + d * x ** 3
# Compute and print loss
loss = (y_pred - y).pow(2).sum().item() # .item()是取tensor的數值
if t % 100 == 99:
print(t, loss)
# Backprop to compute gradients of a, b, c, d with respect to loss
grad_y_pred = 2.0 * (y_pred - y)
grad_a = grad_y_pred.sum()
grad_b = (grad_y_pred * x).sum()
grad_c = (grad_y_pred * x ** 2).sum()
grad_d = (grad_y_pred * x ** 3).sum()
# Update weights
a -= learning_rate * grad_a
b -= learning_rate * grad_b
c -= learning_rate * grad_c
d -= learning_rate * grad_d
print(f"Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3")
Autograd
PyTorch: Tensors and autograd
在上面的例子中,我們必須手動實現神經網路的前向和後向傳遞。手動實現後向傳遞對於小型的只有兩層的網路不算什麼,但是對於大型複雜的網路的將變得非常困難。
幸運的是,我們可以使用自動微分來使神經網路反向傳遞的計算自動化。PyTorch中的autograd包提供了該功能。當使用autograd,神經網路前向傳遞將定義一個計算圖,圖中的節點是Tensor,edges是從輸入tensor產生輸出tensor的函式。然後通過該圖,反向傳播可以輕鬆地計算梯度。
這聽起來很複雜,在實踐中使用卻非常簡單。每個Tensor表示計算圖中的一個節點。如果 x
是一個Tensor,它有屬性 x.requires_grad=True
,那麼 x.grad
就是另一個儲存x關於一些標量值的梯度的tensor。
這裡,我們使用PyTorch tensors和autograd實現了擬合3階多項式的例子;現在我們不再需要手動實現網路的反向傳遞了。
# -*- coding: utf-8 -*-
import torch
import math
dtype = torch.float
device = torch.device('cpu')
# device = torch.device('cuda:0') # Uncomment this to run on GPU
# Create Tensors to hold input and outputs.
# 預設情況下,requires_grad=False, 表示在反向傳遞中,無需計算關於這些tensrs的的梯度
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
# Create random Tensors for weights.對於一個3階多項式,我們需要4個權重引數:
# y = a + b x + c x^2 + d x^3
# 設定requires_grad=True表示我們想要在反向傳遞中計算關於這些Tensors的梯度
a = torch.randn((), device=device, dtype=dtype, requires_grad=True)
b = torch.randn((), device=device, dtype=dtype, requires_grad=True)
c = torch.randn((), device=device, dtype=dtype, requires_grad=True)
d = torch.randn((), device=device, dtype=dtype, requires_grad=True)
learning_rate = 1e-6
for t in range(2000):
# 前向傳遞:使用tensor操作計算預測值y
y_pred = a + b * x + c * x ** 2 + d * x ** 3
# 使用tensor操作計算和列印loss
# 現在loss是一個shape為(1,)的Tensor
# loss.item() 獲得loss中儲存的標量值
loss = (y_pred - y).pow(2).sum()
if t % 100 == 99:
print(t, loss.item())
# 使用autograd計算反向傳遞。該呼叫將會計算loss關於所有具有requires_grad=True屬性的tensor的梯度
# 呼叫之後,a.grad, b.grad, c.grad, d.grad將分別稱為儲存loss關於a,b,c,d的梯度的Tensor
loss.backward()
# 使用梯度下降手動更新權重。包圍在torch.no_grad()進行該操作是因為
# 權重具有requires_grad=True屬性,但我們不需要在autograd中跟蹤該操作:
with torch.no_grad():
a -= learning_rate * a.grad
b -= learning_rate * b.grad
c -= learning_rate * c.grad
d -= learning_rate * d.grad
# 更新權重後,手動地將梯度置為0,不清零會累加
a.grad = None
b.grad = None
c.grad = None
d.grad = None
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
PyTorch: 定義個新的autograd函式
在底層,每個原始的autograd操作實際是兩個在tensor上操作的函式,forward函式計算從輸入張量得到的輸出張量。backward函式
在PyToch中,我們可以通過定義一個 torch.autograd.Function
子類,簡單地定義一個autograd操作,並實現 forward
和 backward
函式。然後,我們可以通過構造一個例項並向函式一樣呼叫它,傳遞包含輸入資料的Tensor來使用我們新的autograd操作符。
在這個例子中,我們定義了一個模型 \(y = a + b P_3(c + dx)\) 來代替 \(y = a + bx + cx^2 + dx^3\),\(P_3(x) = \frac{1}{2}(5x^3-3x)\),即3階勒讓德多項式,我們編寫了自己的autograd函式,實現了\(P_3\)的前向和後向計算,並使用它來實現我們的模型。
# -*- coding: utf-8 -*-
import torch
import math
class LegendrePolynomial3(torch.autograd.Function):
"""
我們可以通過繼承torch.autograd.Function來實現自定義autograd Functions。
Function和實現對Tensor進行操作的前向和反向傳遞。
"""
@staticmethod
def forward(ctx, input):
"""
前向傳遞,我們接收包含輸入的Tensor並返回包含輸出的Tensor。ctx是一個上下文物件,可用於儲存資訊以進行反向計算。
你可以使用ctx.save_for_backward方法快取任意物件以用於反向傳遞。
"""
ctx.save_for_backward(input)
return 0.5 * (5 * input ** 3 - 3 * input)
@staticmethod
def backward(ctx, grad_output):
"""
後向傳遞,我們接收了一個包含loss關於output的梯度的Tenor,我們需要計算loss關於input的梯度???
"""
input, = ctx.saved_tensors
return grad_output * 1.5 * (5 * input ** 2 -1)
dtype = torch.float
device = torch.device('cpu')
# device = torch.device('cuda:0') # Uncomment this to run on GPU
# 構建tensors儲存input和output
# 預設情況下,requires_grad=False, 表明我們在後向傳遞中無需計算關於這些tensor的梯度
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
# 建立權重tensor。例如,我們需要4個權重引數:y = a + b * P3(c + d * x)
# 為了確保收斂,這些權重的初始化值需要與正確的結果相近
# 設定requires_grad=True表示我們希望在後向傳遞中計算關於這些tensor的梯度
a = torch,full((), 0.0, device=device, dtype=dtype, requires_grad=True) # 建立元素全為0.0的tensor
b = torch,full((), -1.0, device=device, dtype=dtype, requires_grad=True)
c = torch,full((), 0.0, device=device, dtype=dtype, requires_grad=True)
d = torch,full((), 0.3, device=device, dtype=dtype, requires_grad=True)
learning_rate = 5e-6
for t in range(2000):
# 為了應用我們的Function,使用Function.apply,並賦為'P3'
P3 = LegendrePolynomial3.apply
## 前向傳遞:計算預測值y_pred,使用自動以的autograd操作計算P3
y_pred = a + b * P3(c + d * x)
# Compute and print loss
loss = (y_pred - y).pow(2).sum()
if t % 100 == 9:
print(t, loss.item())
# Use autograd to compute the backward pass
loss.backward()
# Update weights using gradient descent
with torch.no_grad():
a -= learning_rate * a.grad
b -= learning_rate * b.grad
c -= learning_rate * c.grad
d -= learning_rate * d.grad
# Manually zero the gradients after updating weights
a.grad = None
b.grad = None
c.grad = None
d.grad = None
print(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')
nn module
PyTorch: nn
計算圖和autograd是定義複雜運算子合自動求導的非常強大的工具,但是對於大型神經網路,原生的autograd就顯得有些低階了。
構建神經網路時,我們常會思考將計算放入layers,它包含訓練時將被優化的learnable parameters。
在TensorFlow中,類似Keras、TensorFlow-Slim,TFLearn等庫在原生計算圖上提供了更高階別的抽象,這對於構建神經網路很有用。
在PyTorch中,nn
庫同樣為這個目標服務。nn
庫定義了Modules的集合,它與神經網路層大致對等。一個Module接受輸入Tensors,計算輸出Tensors,但也可能保持內部狀態,例如包含可學習引數的Tensors。nn
庫還定義了訓練神經網路時常用的損失函式的集合。
該例中,我們使用 nn
庫實現我們的多項式模型網路:
# -*- coding: utf-8 -*-
import torch
import math
# Create Tensors to hold input and outputs
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
# 對於這個例子,輸出的y是(x, x^2, x^3)的線性函式,所以
# 我們可以將它認為是一個線性神經網路層。
# 準備tensor(x, x^2, x^3)
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p) # 增加維度,原來是(2000,),現在是(2000, 1)
# 在上述程式碼中,x.unsqueeze(-1)的shape是(2000, 1),p的shape是(3,),
# "broadcasting semantics"將會獲得shape為(2000, 3)的張量
# 使用nn庫將我們的模型定義為一系列層。nn.Sequential是一個包含其它Modules的Module,按順序使用以產生輸出。
# 線性Module使用線性函式從輸入計算輸出,並持有內部張量的權重和偏差。
# 為了匹配 'y'的shape,Flatten層將線性層的輸出展平至1D tensor,
model = torch.nn.Sequential(torch.nn.Linear(3, 1),
torch.nn.Linear(3, 1),
torch.nn.Flatten(0, 1)
)
# nn庫還包含了流行的損失函式的定義
# 該例中,我們將使用Mean Squared Error (MSE)
loss_fn = torch.nn.MSELoss(reduction='sum')
learning_rate = 1e-6
for t in range(2000):
# 前向傳遞:將x傳入模型計算預測值y。Module物件重寫了__call__操作,所以你可以向函式一樣呼叫它們。
y_pred = model(xx)
# 計算和列印loss
loss = loss_fn(y_pred, y)
if t % 100 == 99:
print(t, loss.item())
# 在後向傳遞前將梯度置0
model.zero_grad()
# 後向傳遞:計算loss關於所有模型可學習引數的梯度。
# 每個Module的引數都儲存在具有requires_grad=True屬性的Tensors中,
# 所以下面的呼叫將為模型中所有可學習引數計算梯度。
loss.backward()
# 使用梯度下降更新權重。每個引數都是一個Tensor,所以我們可以像之前那樣訪問它的梯度
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
# 你可以像訪問列表的item一樣訪問'model'的第一個layer
linear_layer = model[0]
# 對於線性層,它的引數被儲存為'weight'和'bias'
print(f'Result: y = {learn_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x +
{linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')
PyTorch: optim
到目前為止,我們已經通過使用 torch.no_grad()
手動改變持有可學習引數的張量來更新模型引數。這對於如隨機梯度下降這樣的簡單優化演算法沒有什麼問題,但在實踐中,我們常使用更復雜的優化器如AdaGrad,RMSProp,Adam等來訓練網路。
PyTorch的 optim
庫提供了常用優化演算法的實現
下例中,我們將首先使用 nn
庫定義我們的模型,使用 optim
庫提供的RMSProp演算法優化模型。
# -*- coding: utf-8 -*-
import torch
import math
# Create Tensors to hold input and outputs
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
# 準備輸入張量(x, x^2, x^3)
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)
# 使用nn庫定義模型和損失函式
model = torch.nn.Sequential(
torch.nn.Linear(3, 1)
torch.nn.Flatten(0, 1)
)
loss_fn = torch.nn.MSELoss(reduction='sum')
# 使用optim庫定義優化器更新模型引數,這裡使用RMSProp,optim庫包含許多其它優化演算法。
# RMSProp建構函式的第一個引數是告訴優化器應該更新哪些Tensors。
learning_rate = 1e-3
optitmizer = torch.optim.RMSProp(model.parameters(), lr=learning_rate)
for t in range(2000):
# 前向傳遞:將x傳入模型,計算預測值y
y_pred = model(xx)
# 計算和列印loss
loss = loss_fn(y_pred, y)
if t % 100 == 99:
print(t, loss.item())
# 在反向傳遞之前,使用optimizer物件將所有將更新的變數(即模型的可學習引數)的梯度置0,這是因為預設情況下,每當調
# 用.backward(),梯度在快取中是累加的(而不是重寫),查閱torch.autograd.backward()獲得更多細節。
optimizer.zero_grad()
# 後向傳遞:計算loss關於模型引數的梯度
loss.backward()
# 在optimizer上呼叫step函式用於更新其引數
optimizer.step()
linear_layer = model[0]
print(f'Result: y = {linear_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x +
{linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')
PyTorch: Custom nn Modules
有些時候你想指定比一系列現有Modules更復雜的模型,那麼可以通過繼承 nn.Module
來定義自己的Modules,並且定義 forward
,用以接收輸入Tensors,利用其它modules或其它在Tensor上的autograd操作符產生輸出Tensor。
實現3階多項式,作為一個自定義的Module模組的子類。
# -*- coding: utf-8 -*-
import torch
import math
class Polynomial3(torch.nn.Module):
def __init__(self):
"""
在建構函式中,我們例項化了4個引數,並將它們賦為成員引數
"""
super().__init__()
self.a = torch.nn.Parameter(torch.randn(()))
self.b = torch.nn.Parameter(torch.randn(()))
self.c = torch.nn.Parameter(torch.randn(()))
self.d = torch.nn.Parameter(torch.randn(()))
def forward(self, x):
"""
在前向傳遞中,接收輸入資料tensor,也要返回輸出資料的tensor。可以使用建構函式中定義的Modules,
也可以是其它任意Tensor上的操作。
"""
return self.a + self.b * x + self.c x ** 2 + self.d * x ** 3
def string(self):
"""
就像Python的其它類一樣,你可以在PyTorch modules上自定義方法
"""
return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3'
# 建立tensor儲存input和output
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
# 通過例項化之前定義的類構造模型
model = Polynomial3()
# 構造損失函式和優化器。SGD建構函式中呼叫的model.parameters()包含可學習引數(由torch.nn.Parameter定義的模型成員)
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-6)
for t in range(2000):
# Forward pass: Compute predicted y by passing x to the model
y_pred = model(x)
# Compute and print loss
loss = criterion(y_pred, y)
if t % 100 == 99:
print(t, loss.item())
# Zero gradients, perform a backward pass, and update the weights.
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Result: {model.string()}')
PyTorch: 控制流 + 權重共享
作為動態圖和權重共享的例子,我們實現了一個非常奇怪的模型:一個3到5階的多項式,在每一次前向傳遞時,選擇一個3到5之間的隨機值作為階,並且多次重用相同的權重計算第4和第5階。
對於這個模型,我們可以使用普通的Python控制流實現迴圈,並且在定義前向傳遞時,可以通過簡單的多次複用相同的引數實現權重共享。
我們可以簡單地將其作為Module子類來實現模型。
# -*- coding: utf-8 -*-
import random
import torch
import math
class DynamicNet(torch.nn.Module):
def __init__(self):
"""
建構函式中,我們例項化5個引數並將其賦為成員
"""
super().__init__()
self.a = torch.nn.Parameter(torch.randn(()))
self.b = torch.nn.Parameter(torch.randn(()))
self.c = torch.nn.Parameter(torch.randn(()))
self.d = torch.nn.Parameter(torch.randn(()))
self.e = torch.nn.Parameter(torch.randn(()))
def forward(self, x):
"""
對於模型的前向傳遞,我們隨機選擇4,5並重用引數e計算這兩個階的共享
因為每次前向傳遞都會構建一個動態計算圖,當定義模型前向傳遞時,我們可以使用普通的Python控制流語句,如迴圈或條件語句
這裡我們還可以看到,定義計算圖時,多次重用相同的引數時完全安全的
"""
y = self.a + self.b + self.c * x ** 2 + self.d * x ** 3
for exp in range(4, random.randint(4, 6)):
y = y + self.e * x ** exp
return y
def string(self):
"""
就像Python中的其它任何類一樣,你還可以在PyTorch modules上自定義方法
"""
return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3 + {self.e.item()} x^4 ? + {self.e.item()} x^5 ?'
# 建立Tensors儲存input和outputs
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
# 通過例項化上面定義的類構造模型
model = DynamicNet()
# 構造損失函式和優化器,使用vanilla(batch)梯度下降訓練這個奇怪的網路有些困難,我們使用momentum
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-8, momentum=0.9)
for t in range(30000):
# 前向傳遞:將x傳入模型,計算預測值y
y_pred = model(x)
# 計算並列印loss
loss = criterion(y_pred, y)
if t % 2000 == 1999:
print(t, loss.item())
# 梯度歸0,反向傳遞,權重更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Result: {model.string()}')