通過示例學習PYTORCH

Deep_RS發表於2022-02-11

注意:這是舊版本的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操作,並實現 forwardbackward 函式。然後,我們可以通過構造一個例項並向函式一樣呼叫它,傳遞包含輸入資料的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()}')

相關文章