通過實例學習 PyTorch

通過範例學習 PyTorch

本博文通過幾個獨立的例子介紹了 PyTorch 的基礎概念。

其核心,PyTorch 提供了兩個主要的特徵:

  • 一個 n-維張量(n-dimensional Tensor),類似 NumPy 但是可以運行在 GPU 設備上
  • 構建和訓練神經網路,可自動求微分

我們將使用三階多項式去擬合 y=sin(x) 的問題作為我們的例子。神經網路將會有 4 個參數,並且將使用梯度下降通過最小化(minimizing)網路輸出和真實輸出的歐氏距離(Euclidean distance)去擬合隨機數據。

Tensors

熱身:NumPy

在介紹 PyTorch 之前,我們首先使用 NumPy 實現一個神經網路。

NumPy 提供了一種 n-維數組(n-dimensional)數組對象,和許多操縱這些數組的函數。NumPy 對於科學計算是一個充滿活力的框架;它不需要了解任何關於計算圖(computation graphs)、深度學習或梯度。但是,我們可以輕鬆地使用 NumPy 操作手動實現網路的前向傳播和反向傳播,擬合一個三階多項式到 sine 函數。

import numpy as np
import math

# 創建隨機輸入和輸出的數據
x = np.linspace(-math.pi, math.pi, 2000)
y = np.sin(x)

# 隨機初始化權重(weight)
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):
    # 前向傳播:計算預測的 y
    # y = a + bx + cx^2 + dx^3
    y_pred = a + b * c + c * x ** 2 + d * x ** 3
    
    # 計算列印損失值(loss)
    loss = np.square(y_pred - y).sum()
    if t % 100 == 99:
        print(t, loss)
    
    # 反向傳播計算 a, b, c, d 關於 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()
    
    # 更新權重參數(weight)
    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')
99 880.579689608281
199 854.0555627713447
299 835.0649579500803
399 821.369172352883
499 811.4200797717988
599 804.1424592009078
699 798.7835679283971
799 794.8123966679943
899 791.8516135436988
999 789.6312092332105
1099 787.9566727255892
1199 786.6869765834631
1299 785.7192368208233
1399 784.9779487653068
1499 784.4073829648856
1599 783.9661786053822
1699 783.6234753797376
1799 783.3561293941161
1899 783.146697690572
1999 782.9819710274254
Result: y = -0.05099192206877935 + 5.075189949472816 x + 0.004669889031269278 x^2 + 0.028076619049389115 x^3

PyTorch: Tensors

儘管 NumPy 是一個非常棒的框架,但是它不可以利用 GPU 去加速數值計算。對於現代深度神經網路,GPU 通過提供了 50倍或以上 的加速,不幸地是 NumPy 對於深度學習是還不夠的。

這裡我們介紹了 PyTorch 最基礎的概念:Tensor 。一個 PyTorch Tensor 從概念上與 NumPy 數組是完全相同的:Tensor 是一個 n 維數組(n-dimensional array),並且 PyTorch 提供了許多處理這些 Tensor 的函數。幕後,Tensor 記錄了一個計算圖(computation graph)和梯度,並且這些對於科學計算也是一個非常實用並充滿活力的工具。

不像 NumPy,PyTorch Tensor 可以利用 GPU 加速數值計算。為了將 PyTorch Tensor 運行在 GPU 上,你只需要簡單地指定正確的設備。

這裡我們使用 PyTorch Tensor 去擬合一個三階多項式到 sine 函數。就像上面 NumPy 的例子,我們需要手動實現網路的前向傳播和反向傳播。

import torch
import math

dtype = torch.float
device = torch.device("cpu")
# 下面這條注釋,可以使用 GPU
# device = torch.device("cuda:0")

# 創建隨機輸入輸出
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 隨機初始化權重(weight)
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)

learnign_rate = 1e-6
for t in range(2000):
    # 前向傳播:計算預測的 y
    y_pred = a + b * c + c * x ** 2 + d * x ** 3
    
    # 計算和輸出損失值(loss)
    loss = (y_pred - y).pow(2).sum().item()
    if t % 100 == 99:
        print(t, loss)
        
    # 反向傳播計算 a, b, c, d 關於損失值(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()
    
    # 使用梯度下降更新權重(weight)
    a -= learnign_rate * grad_a
    b -= learnign_rate * grad_b
    c -= learnign_rate * grad_c
    d -= learnign_rate * grad_d
    
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
99 883.4296875
199 846.044921875
299 823.0543823242188
399 808.742919921875
499 799.6961059570312
599 793.8943481445312
699 790.1231079101562
799 787.6404418945312
899 785.9865112304688
999 784.872314453125
1099 784.113525390625
1199 783.5916748046875
1299 783.2293090820312
1399 782.9754028320312
1499 782.7958984375
1599 782.6681518554688
1699 782.5762939453125
1799 782.509765625
1899 782.4613647460938
1999 782.4259033203125
Result: y = 0.015941178426146507 + 2.6255147457122803 x + -0.0018874391680583358 x^2 + 0.028076613321900368 x^3

自動求導

PyTorch: Tensor and autograd

在上面的例子中,我們必須手動實現神經網路的前向傳播和反向傳播。

對於一個小的兩層網路手動實現反向傳播並沒有什麼大不了的,但是對於更大更複雜的網路是一件非常可怕的事情。

幸虧的是,我們可以使用 自動微分(automatic differentiation 自動化神經網路的反向傳播的計算。在 PyTorch 的 autograd 包正好提供了這個功能。當使用 autograd 時,神經網路的前向傳播將定義一個 計算圖(Computational graph ,圖中的結點(nodes)將會是一個 Tensor,每條邊(edges)將會是從一個輸入 Tensor 產生輸出 Tensor 的函數。反向傳播通過計算圖讓我們輕鬆地計算梯度。

這聽起來有些複雜,但是實際上是相當簡單的。每一個 Tensor 代表了計算圖中的結點。如果 x 是一個 Tensor,它就有 x.requires_grad=True 然後 x.grad 就是另一個張量,其持有 x 關於某個標量值的梯度。

這裡我們使用 PyTorch Tensor 和 autograd 實現使用一個三階多項式去擬合 sine 曲線的例子;現在,我們不再需要手動地實現網路的反向傳播。

import torch
import math

dtype = torch.float
device = torch.device("cpu")
# 下面這條注釋,可以使用 GPU
# device = torch.device("cuda:0")

# 創建 Tensor
# 默認情況下,requires_grad=False 表示我們不需要計算關於這些 Tensor 的梯度
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 創建隨機權重(weight) Tensor
# 對於一個三階多項式,我們需要 4 個參數
# 設置 requires_grad=True 表明我們在反向傳播的時候想要計算關於這些 Tensor 的梯度
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)

learnign_rate = 1e-6
for t in range(2000):
    # 使用 Tensor 上的運算前向傳播計算預測的 y
    y_pred = a + b * c + c * x ** 2 + d * x ** 3
    
    # 使用 Tensor 上的操作計算並列印損失值(loss)
    # 現在,loss 是一個 Tensor,shape 是 (1,)
    # 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 將得到 a, b, c, d 關於 loss 的梯度
    loss.backward()
    
    # 使用梯度下降手動更新權重。使用 torch.no_grad() 包起來。
    # 因為這些權重(weight)都有 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
        
        # 更新完參數之後,需要手動地將這些梯度清零
        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')
99 943.6873168945312
199 903.8673095703125
299 873.939453125
399 851.4177856445312
499 834.4534301757812
599 821.6660766601562
699 812.0220336914062
799 804.74560546875
899 799.2537841796875
999 795.108154296875
1099 791.9780883789062
1199 789.6143798828125
1299 787.8292846679688
1399 786.4810791015625
1499 785.4628295898438
1599 784.693603515625
1699 784.1126708984375
1799 783.6737060546875
1899 783.34228515625
1999 783.091796875
Result: y = 0.032735299319028854 + 0.6361034512519836 x + -0.005412467289716005 x^2 + 0.028076613321900368 x^3

PyTorchL 定義一個新的 autograd 函數

在底層,每一個原始自動求導(autograd)運算符實際上是兩個操作在 Tensor 上的函數。前向傳播(forward 函數計算出從輸入 Tensor 到輸出 Tensor。反向傳播(backward 函數收到輸出 Tensor 關於某個標量值的梯度,並且計算輸入 Tensor 關於那些相同標量值的梯度。

在 PyTorch 中我們可以輕鬆地定義我們自己的自動求導運算符,一個 torch.autograd.Function 的子類並且實現 forwardbackward 函數。然後我們可以使用我們新定義的自動求導運算符,構建一個類實例(instance)然後像函數樣調用它,傳入輸入數據的 Tensor。

在這個例子中,我們定義我們的模型為 \(y=a+b P_3(c+dx)\) 而不是 \(y=a+bx+cx^2+dx^3\),其中 \(P_3(x)=\frac{1}{2}\left(5x^3-3x\right)\) 是一個 3 次(degreeLegendre 多項式。對於計算 \(P_3\) 的前向傳播和反向傳播,我們寫下自定義的自動求導函數,並用它實現我們的模型。

import torch
import math

class LegendrePolynomial3(torch.autograd.Function):
    """
    我們可以通過繼承 torch.autograd.Function 實現我們自定義自動求導函數,
    並實現操作在 Tensor 上的前向傳播和反向傳播。
    """
    
    @staticmethod
    def forward(ctx, input):
        """
        在前向傳播中,我們接受一個包含輸入的 Tensor 並返回包含輸出的 Tensor。
        ctx 是一個上下文(context)對象,用來存放反向傳播計算時用到的資訊。
        你可以使用 ctx.save_for_backward 方法快取任意對象以供反向傳播使用。
        """
        ctx.save_for_backward(input)
        return 0.5 * (5 * input ** 3 - 3 * input)
    
    @staticmethod
    def backward(ctx, grad_output):
        """
        在反向傳播中,我們接收一個張量,其持有 loss 關於輸出的梯度,
        並且我們需要計算 loss 關於輸入的梯度。
        """
        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")  # 取消注釋可以使用 GPU

# 創建輸入輸出 Tensor。
# 默認情況下,requires_grad=False 表明我們在反向傳播時不需要計算這些 Tensor 的梯度。
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 創建隨機權重(weight)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)
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):
    # 為了應用(apply)我們的函數,我們使用 Function.apply
    # 並將這個函數取個別名 P3
    P3 = LegendrePolynomial3.apply
    
    # 前向傳播:使用操作(operations)計算預測的 y。
    # 我們使用我們自定義的 autograd 操作計算 P3。
    y_pred = a + b * P3(c + d * x)
    
    # 計算並輸出損失值(loss)
    loss = (y_pred - y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())
        
    # 使用 autograd 計算反向傳播。
    loss.backward()
    
    # 使用梯度下降更新權重
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad
        
        # 更新完權重後,手動清零梯度
        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)')
99 209.95834350585938
199 144.66018676757812
299 100.70249938964844
399 71.03519439697266
499 50.97850799560547
599 37.403133392333984
699 28.206867218017578
799 21.973188400268555
899 17.7457275390625
999 14.877889633178711
1099 12.931766510009766
1199 11.610918045043945
1299 10.714258193969727
1399 10.10548210144043
1499 9.692106246948242
1599 9.411375045776367
1699 9.220745086669922
1799 9.091285705566406
1899 9.003361701965332
1999 8.943639755249023
Result: y = -5.423830273798558e-09 + -2.208526849746704 * P3(1.3320399228078372e-09 + 0.2554861009120941 x)

nn module

PyTorch: nn

對於定義一個複雜的運算符和自動微分,計算圖和 autograd 是一個非常強大的範例(paradigm),但是對於一個很大的神經網路來說,原生的(raw)autograd 就有一點低級(low-level)了。

當構建神經網路的時候,我們經常考慮將這些計算安排整理到一個 層(layers 中,在學習期間,一些 可學習參數(learnable parameters 將會被優化。

在 TensorFlow 中,像 Keras, TensorFlow-SlimTFLearn 包在原生計算圖之上提供了更高階的抽象,這非常有益於構建神經網路。

在 PyTorch 中,nn 包服務於相同的目的。nn 包定義了一套 Modules,大致上等價於神經網路中的層(layers)。一個 Module 接受輸入 Tensor 並計算輸出 Tensor,也許也會持有內部的狀態(state),比如包含可學習的參數的(learnable parameters)Tensor。當訓練神經網路時,nn 包也定了一套損失函數(loss function)。

在這個例子中,我們使用 nn 包實現我們的多項式模型。

import torch
import math

# 創建輸入輸出 Tensor
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 在這個例子中,輸出 y 是一個(x,x^2,x^3)的線性函數,所以
# 我們可以考慮它是一個線性(linear layer)神經網路層。
# 讓我們準備 Tensor(x,x^2,x^3)
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)

# 在上面的程式碼中,x.unsqueeze(-1) 有著 (2000,1)的形狀(shape),並且 p 有著 (3,)的形狀
# 對於這個例子,廣播語義(broadcasting semantics),得到一個 (2000,3)的 Tensor。

# 使用 nn 包定義我們一系列的層的模型。nn.Sequential 是一個 Module,其包含其它 Modules
# 並按順序應用它們產生輸出。線性 Modules 從輸入使用一個線性函數計算輸出
# 並在內部持有模型的 weight 和 bias 的 Tensor。Flatten layer 展開線性層的輸出到
# 一個匹配 y 的形狀(shape)的一維(1D)的 Tensor。
model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)

# nn 包同樣包含流行的損失函數(loss function)的定義;在這個例子中,
# 我們將使用均方誤差(Mean Square Error——MSE)作為我們的損失函數。
loss_fn = torch.nn.MSELoss(reduction='sum')

learning_rate = 1e-6
for t in range(2000):
    # 前向傳播:通過傳入 x 到 model 計算預測的 y。Module 對象重寫了
    # __call__ 函數,所我們可以就像調用函數一樣調用他們。當你這麼做的時候
    # 傳入輸入 Tensor 到 Module 然後它計算產生輸出的 Tensor。
    y_pred = model(xx)
    
    # 計算並列印損失值(loss)。我們傳入 y 的預測值和真實值的 Tensor,
    # 之後 loss function 返回損失值(loss)的 Tensor。
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
        
    # 運行反向傳播之前清零一下梯度
    model.zero_grad()
    # 反向傳播:計算 loss 關於所有 model 的可學習參數的梯度。從底層上來說,
    # 每一個 requires_grad=True 的 Module 的參數(parameters)都被存儲在一個 Tensor 中,
    # 所以下面這個調用將計算 model 中所有可學習參數的的梯度。
    loss.backward()
    
    # 使用梯度下降更新權重。每一個參數都是一個 Tensor,
    # 所以我們就像之前一樣得到它的梯度。
    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad
    
# 你也可以就像得到列表(list)的第一個元素一樣,得到 model 的第一層
linear_layer = model[0]

# 對於 linear layer,它的參數被存儲為 weight 和 bias。
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')
99 672.3364868164062
199 448.7064208984375
299 300.4986572265625
399 202.26097106933594
499 137.13494873046875
599 93.9523696899414
699 65.31460571289062
799 46.318885803222656
899 33.71611785888672
999 25.35308074951172
1099 19.802162170410156
1199 16.116811752319336
1299 13.669382095336914
1399 12.043644905090332
1499 10.963366508483887
1599 10.245325088500977
1699 9.7678804397583
1799 9.450323104858398
1899 9.23902416229248
1999 9.098373413085938
Result: y = -0.006202241405844688 + 0.8414672017097473 x + 0.0010699888225644827 x^2 + -0.09115784615278244 x^3

PyTorch: optim

到目前為止,我們通過 torch.no_grad() 手動更改持有可學習參數的 Tensor 來更新了我們模型的權重。這對於一些簡單的優化演算法,比如隨機梯度下降(stochastic gradient descent),並不是一個太大的負擔,但是實際上,我們經常使用更複雜的優化器(Optimizer),比如 AdaGradRMSpropAdam等,訓練神經網路。

PyTorch 里的 optim 包抽象了一個優化演算法的思想,並且提供了常用的優化演算法的實現。

在這個例子,我們依舊使用 nn 包定義我們的模型,但是我們將使用 optim 包提供的 RMSprop 演算法優化模型。

import torch
import math


# 創建輸入輸出的 Tensor。
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 準備輸入的 Tensor(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 包定義一個將會為我們更新模型的權重(weight)的優化器(Optimizer)。
# 這裡我們將使用 RMSprop,optim 包包含了許多其它優化演算法。
# RMSprop 構造器的第一個參數是告訴優化器哪些 Tensor 應該被更新。
learning_rate = 1e-3
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)
for t in range(2000):
    # 前向傳播:傳入 x 到 model 計算預測的 y
    y_pred = model(xx)
    
    # 計算並列印損失值(loss)
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
    
    # 在反向傳播之前,需要使用優化器(optimizer)對象清零所有將被更新的變數的梯度
    # (模型的可學習參數(learnable weight))。這是因為在默認情況下,不論何時 .backward() 被調用時,
    # 梯度會被累積在緩衝區(換言之,不會被覆蓋)。可以通過 torch.autograd.backward 的官方文檔查看更多細節。
    optimizer.zero_grad()
    
    # 反向傳播:計算 loss 關於模型參數的梯度。
    loss.backward()
    
    # 調用優化器上的 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')
99 1610.013671875
199 844.4555053710938
299 525.6090698242188
399 334.3529968261719
499 213.2096405029297
599 135.09706115722656
699 83.0303955078125
799 48.23293685913086
899 26.453231811523438
999 14.763155937194824
1099 10.078741073608398
1199 9.046186447143555
1299 8.941937446594238
1399 8.894529342651367
1499 8.899309158325195
1599 8.912175178527832
1699 8.913202285766602
1799 8.911144256591797
1899 8.92212200164795
1999 8.92563247680664
Result: y = -0.0005531301139853895 + 0.8562383651733398 x + -0.0005647626821883023 x^2 + -0.0938328206539154 x^3

PyTorch:訂製 nn Modules

某些時候,你想要指定的模型比 Modules 存在的順序(sequence)模型還要複雜,在這種情況下,你可以通過繼承 nn.Module 的子類並且定義一個 forward 函數,這個函數使用其它 Modules 或者其它 autograd 操作符,接收輸入 Tensor 並計算輸出 Tensor。

在這個例子中,我們實現我們的三階多項式作為訂製的 Module 的子類。

import torch
import math

class Polynomial3(torch.nn.Module):
    def __init__(self):
        """
        在這個構造器,我們實例化四個參數並賦它們為成員 parameters。
        """
        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 中的任意一個類一樣,你也可以隨便定義任何的方法(method)在 PyTorch Modules中。
        """
        return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3'
    
# 創建輸入輸出的 Tensor。
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 實例化上面定義的類,構造我們的模型。
model = Polynomial3()

# 構造我們的損失函數(loss function)和一個優化器(Optimizer)。在 SGD 構造器里
# 調用 model.parameters(),構造器將包含 nn.Linear Module 的可學習的參數(learnable parameters)
# 其是模型(model)的成員變數。
criterion = torch.nn.MSELoss(reduce='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-6)
for t in range(2000):
    # 前向傳播:傳入 x 到模型計算預測的 y
    y_pred = model(x)
    
    # 計算並列印損失值(loss)
    loss = criterion(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
        
    # 清零梯度,執行反向傳播,更新參數。
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
print(f'Result: {model.string()}')
99 62.18498229980469
199 58.930118560791016
299 55.8530387878418
399 52.94403076171875
499 50.19393539428711
599 47.59403991699219
699 45.136138916015625
799 42.812469482421875
899 40.61569595336914
999 38.538875579833984
1099 36.575462341308594
1199 34.71923828125
1299 32.964359283447266
1399 31.305295944213867
1499 29.736791610717773
1599 28.2539005279541
1699 26.85195541381836
1799 25.52651023864746
1899 24.273393630981445
1999 23.08867073059082
Result: y = -1.397053837776184 + -0.8716074824333191 x + 0.35939672589302063 x^2 + -0.23259805142879486 x^3

PyTorch:控制流 + 權重(參數)共享

作為一個動態圖和權重共享的例子,我們實現一個非常強大的模型:一個 3-5 階的多項式,在前向傳播時選擇一個 3-5 之間的隨機數,並且使用許多階,多次重複使用相同的權重計算第四階和第五階。

對於這個模型,我們可以使用典型的 Python 控制流實現循環,並且當定義前向傳播時,我們可以簡單地復用相同的參數多次實現權重共享。

我們可以繼承 Module 類輕鬆地實現這個模型。

import random
import torch
import math

class DynamicNet(torch.nn.Module):
    def __init__(self):
        """
        在這個構造器中,我們實例化五個參數並且將它們賦值給成員變數。
        """
        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 計算這些階的貢獻(contribution)。
        因為每一次前向傳播都構建了一個動態的計算圖,當定義模型的前向傳播時,
        我們可以使用常規的 Python 控制流操作符,像循環或條件語句。
        這裡我們也看到了當定義一個計算圖時重複使用相同的參數多次是相當安全。
        """
        y = self.a + self.b * x + 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 中的任意一個類一樣,你也可以隨便定義任何的方法(method)在 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 ?'
    
# 創建持有輸入輸出的 Tensor。
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 通過實例化上面定義的類,構造我們的模型。
model = DynamicNet()

# 構造我們的損失函數(loss fcuntion)和一個優化器(Optimizer)。使用毫無特色的
# 隨機梯度下降(stochastic gradient descent)訓練這個強大的模型是艱難的,
# 所以我們使用動量(momentum)。
criterion = torch.nn.MSELoss(reduce='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-8, momentum=0.9)
for t in range(30000):
    # 前向傳播:傳入 x 到 model 計算預測的 y。
    y_pred = model(x)
    
    # 計算並列印損失值(loss)
    loss = criterion(y_pred, y)
    if t % 2000 == 1999:
        print(t, loss.item())
        
    # 清零梯度、執行反向傳播、更新參數
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

print(f'Result: {model.string()}')
1999 31.825870513916016
3999 30.36163330078125
5999 28.165559768676758
7999 4.4947919845581055
9999 25.11688804626465
11999 4.422863960266113
13999 22.777265548706055
15999 21.440027236938477
17999 20.374134063720703
19999 19.437679290771484
21999 18.513486862182617
23999 17.685436248779297
25999 16.829214096069336
27999 16.081615447998047
29999 15.38708782196045
Result: y = 0.5208832621574402 + -2.605482578277588 x + 0.06938754767179489 x^2 + 0.6473004221916199 x^3 + -0.033068109303712845 x^4 ? + -0.033068109303712845 x^5 ?