幾個例子幫你梳理PyTorch知識點(張量、autograd)

語言: CN / TW / HK

theme: v-green

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第32天,點選檢視活動詳情


因為我最近想學Pytorch lightning,重構一下之前的程式碼,所以回來梳理一下Pytorch的語法,好進行下一步學習,所以從頭重新回顧一下Pytorch。這個文章是通過幾個簡單例子幫大家回顧一下Pytorch一些重點基礎概念。

Pytorch有兩個重要的特徵:

  • 使用n維張量進行運算,可以使用GPU加速計算。
  • 使用自動微分構建、訓練神經網路

從$sin(x)$開始

Tensor和Numpy的用法差不多,但是Tensor可是使用進行加速計算,這比CPU計算要快50倍甚至更多。

更多區別可以看這個:PyTorch的Tensor這麼簡單,你還用不明白嗎? - 掘金 (juejin.cn)

我們知道,在基礎的迴歸問題中:給定一些資料,符合一定的分佈,我們要建立一個神經網路去擬合這個分佈,神經網路學習出來的表示式就作為我們資料分佈的表示式。

這裡我們用一個三次多項式$y=a+bx+cx^2+dx^3$來擬合$sin(x)$,訓練過程使用隨機梯度下降進行訓練,通過計算最小化預測值和真實值之間的歐氏距離來擬合隨機資料。

程式碼解釋見程式碼中的註釋部分。

```py import torch import math

dtype = torch.float device = torch.device("cpu")

下邊這行程式碼可以用也可以不用,註釋掉就是在CPU上執行

device = torch.device("cuda:0")

建立輸入輸出資料,這裡是x和y代表[-π,π]之間的sin(x)的值

x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype) y = torch.sin(x)

隨機初始化權重

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): # 前向過程,計算y的預測值 y_pred = a + b * x + 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()

# 使用梯度下降更新引數
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') ``` 結果如下:

image.png

如果換成勒讓德多項式呢?

之前講過深扒torch.autograd原理 - 掘金 (juejin.cn)

這裡我們再淺淺簡述一下autograd。

在上邊的例子裡,我們是手動實現了神經網路的前向和反向傳播過程,因為這只是一個簡單的兩層網路,所以現實來也不是很困難,但是放對於一些大的複雜網路,要手動實現整個前向和反向過程就是非常困難的事情了。

現在我們可以使用pytorch提供autograd包去自動求導,自動計算神經網路的反向過程。當我們使用autograd的時候,神經網路前向過程就是定義一個計算圖,計算圖上的節點都是張量,邊是從輸入到輸出的計算函式。用計算圖進行反向傳播可以輕鬆計算梯度。

雖然聽起來很複雜,但是用起來很簡單。每個張量都代表計算圖上的一個節點,如果x是一個張量,並且你設定好了x.requires_grad=True,那x.grad就是另一個儲存x關於某些標量的梯度的張量。

然後我們繼續使用三次多項式來擬合我們的sin(x),但是現在我們就可以不用手動實現反向傳播的過程了。

```py import torch import math

dtype = torch.float device = torch.device("cpu")

下邊這行程式碼可以用也可以不用,註釋掉就是在CPU上執行

device = torch.device("cuda:0")

建立輸入輸出資料,這裡是x和y代表[-π,π]之間的sin(x)的值

x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype) y = torch.sin(x)

隨機初始化權重

注意這裡我們設定了requires_grad=True,讓autograd自動跟蹤計算圖的梯度計算

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):

# 前向過程,計算y的預測值
y_pred = a + b * x + c * x ** 2 + d * x ** 3

# 計算預測值和真實值的loss
loss = (y_pred - y).pow(2).sum()
if t % 100 == 99:
    print(t, loss.item())

# 使用autograd計算反向過程,呼叫之後會計算所有設定了requires_grad=True的張量的梯度
# 呼叫之後 a.grad, b.grad. c.grad  d.grad 會儲存 abcd關於loss的梯度
loss.backward()

# 使用梯度下降更新引數
# 因為權重設定了requires_grad=True,但是在梯度更新這裡我們不需要跟蹤梯度,所以加上with torch.no_grad()
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') ```

image.png

在pytorch這種autograd的情況下,每個基礎的的autograd操作只是兩個作用於張量的方法。

  • forward:由輸入張量計算輸出張量
  • backward:接收輸出張量相對於某個標量值的梯度,並計算輸入張量相對於相同標量值的梯度。

在pytorch中我們可以定義我們自己的autograd操作,你只需要實現一個torch.autograd.Function的子類,寫好forwardbackward函式即可。構造好新的autograd之後我們就可以像呼叫其他函式一樣呼叫它,將輸入張量傳遞進去即可。

比如我們不用$y=a+bx+cx^2+dx^3$了,改成一個三次勒讓德多項式(Legendre polynomial),形式為$y = a+bP_3(c+dx)$,其中$P_3(x) = \frac 1 2 (5x^3-3x)$。

```py import torch import math

class LegendrePolynomial3(torch.autograd.Function): def forward(ctx, input):

    # 在前向過程中我們接受一個輸入張量,並返回一個輸出張量
    # ctx是一個上下文物件,用於儲存反向過程的內容
    # 你可以用save_for_backward方法快取任意在反向計算過程中要用的物件。

    ctx.save_for_backward(input)
    return 0.5 * (5 * input ** 3 - 3 * input)

def backward(ctx, grad_output):
    # 在反向過程中,我們接受一個張量包含了損失關於輸出的梯度,我們需要計算損失關於輸入的梯度。
    input, = ctx.saved_tensors
    return grad_output * 1.5 * (5 * input ** 2 - 1)

dtype = torch.float device = torch.device("cpu")

下邊這行程式碼可以用也可以不用,註釋掉就是在CPU上執行

device = torch.device("cuda:0")

建立輸入輸出資料,這裡是x和y代表[-π,π]之間的sin(x)的值

x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype) y = torch.sin(x)

隨機初始化權重

注意這裡我們設定了requires_grad=True,讓autograd自動跟蹤計算圖的梯度計算

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): # 我們給我們自定義的autograd起個名叫P3,然後用Function.apply方法呼叫 P3 = LegendrePolynomial3.apply

# 前向過程計算y,用的是我們自定義的P3的autograd
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)') ```

image.png