PyTorch 作為主流深度學習框架,其核心概念張量與自動微分引擎是理解與應用 PyTorch 的關鍵。張量作為多維資料容器,儲存和操作資料,其階數、形狀、資料型別等屬性決定了資料的結構和運算方式。PyTorch 提供了便捷的函式建立和操作張量,例如 torch.tensor().reshape().view() 等,方便開發者靈活處理資料。自動微分引擎則簡化了梯度計算,透過追蹤張量運算構建計算圖,並利用反向傳播演算法自動計算梯度,有效提升模型訓練效率。理解這些基礎概念能幫助開發者更有效地建構和訓練深度學習模型。

PyTorch 張量基礎介紹

PyTorch 是一個功能強大的深度學習框架,而張量(Tensor)是其核心資料結構。張量是一種多維度的資料容器,用於儲存和操作資料。

張量的基本概念

張量是數學中的一個概念,可以被視為向量和矩陣的推廣。張量的秩(Rank)代表了其維度數量。例如,純量(Scalar)是 0 階張量,向量(Vector)是 1 階張量,矩陣(Matrix)是 2 階張量。

張量的建立

PyTorch 提供了 torch.tensor() 函式來建立張量。以下是一個範例:

import torch

# 建立 0 階張量(純量)
tensor0d = torch.tensor(1)

# 建立 1 階張量(向量)
tensor1d = torch.tensor([1, 2, 3])

# 建立 2 階張量(矩陣)
tensor2d = torch.tensor([[1, 2], [3, 4]])

# 建立 3 階張量
tensor3d = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])

張量的資料型別

PyTorch 張量的資料型別預設為 64 位元整數。若要建立浮點數張量,可以使用浮點數列表:

floatvec = torch.tensor([1.0, 2.0, 3.0])
print(floatvec.dtype)  # 輸出:torch.float32

預設的浮點數精確度是 32 位元,這是因為大多數深度學習任務不需要太高的精確度,而使用 32 位元浮點數可以節省記憶體和計算資源。

張量的資料型別轉換

可以使用 .to() 方法來轉換張量的資料型別:

floatvec = tensor1d.to(torch.float32)
print(floatvec.dtype)  # 輸出:torch.float32

常見的 PyTorch 張量操作

PyTorch 提供了多種張量操作,以下是一些常見的操作:

建立張量

使用 torch.tensor() 函式可以建立新的張量:

tensor2d = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(tensor2d)

輸出:

tensor([[1, 2, 3],
        [4, 5, 6]])

取得張量的形狀

可以使用 .shape 屬性來取得張量的形狀:

print(tensor2d.shape)  # 輸出:torch.Size([2, 3])

重塑張量

可以使用 .reshape().view() 方法來重塑張量:

print(tensor2d.reshape(3, 2))
# 或
print(tensor2d.view(3, 2))

輸出:

tensor([[1, 2],
        [3, 4],
        [5, 6]])

張量操作的靈活性

PyTorch 對許多操作提供了多種語法選擇,這使得程式碼更加靈活和方便。

詳細解說:

在這段程式碼中,我們使用了 torch.tensor() 建立了不同階數的張量,並演示瞭如何取得和轉換張量的資料型別。同時,我們也介紹瞭如何使用 .shape 屬性取得張量的形狀,以及如何使用 .reshape().view() 方法重塑張量。

這些操作是 PyTorch 中最基本也是最重要的部分,熟練掌握這些操作可以幫助我們更好地進行深度學習模型的開發和訓練。

# 詳細範例程式碼
import torch

# 建立不同階數的張量
tensor0d = torch.tensor(1)
tensor1d = torch.tensor([1, 2, 3])
tensor2d = torch.tensor([[1, 2], [3, 4]])
tensor3d = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])

# 列印張量的資料型別
print(tensor1d.dtype)

# 建立浮點數張量
floatvec = torch.tensor([1.0, 2.0, 3.0])
print(floatvec.dtype)

# 資料型別轉換
floatvec = tensor1d.to(torch.float32)
print(floatvec.dtype)

# 取得張量的形狀
tensor2d = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(tensor2d.shape)

# 重塑張量
print(tensor2d.reshape(3, 2))
print(tensor2d.view(3, 2))

程式碼解析:

在這個範例中,我們首先匯入了 PyTorch 的 torch 模組,然後使用 torch.tensor() 建立了不同階數的張量。接著,我們演示瞭如何取得和轉換張量的資料型別。最後,我們展示瞭如何使用 .shape 屬性取得張量的形狀,以及如何使用 .reshape().view() 方法重塑張量。

這些範例程式碼能夠幫助讀者更好地理解 PyTorch 中張量的基本操作。

此圖示顯示了不同階數的張量之間的關係。

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title PyTorch 張量基礎與自動微分詳解

package "PyTorch 張量與自動微分" {
    package "張量基礎" {
        component [torch.tensor] as tensor
        component [形狀 shape] as shape
        component [資料型別 dtype] as dtype
    }

    package "張量操作" {
        component [reshape 重塑] as reshape
        component [view 視圖] as view
        component [索引切片] as index
    }

    package "自動微分" {
        component [requires_grad] as grad
        component [計算圖] as graph
        component [backward] as backward
    }
}

tensor --> shape : 建立張量
shape --> dtype : 型別轉換
dtype --> reshape : 維度調整
reshape --> view : 記憶體共享
view --> index : 資料存取
tensor --> grad : 追蹤運算
grad --> graph : 動態建構
graph --> backward : 梯度計算

note right of tensor : 0D~nD 張量\nScalar, Vector, Matrix
note right of backward : 反向傳播\n自動求導

@enduml

A.3 將模型視為計算圖

PyTorch 的自動微分引擎,也被稱為 autograd,為動態計算圖中的梯度計算提供了自動化的功能。計算圖是一種有向圖,它允許我們表達和視覺化數學表示式。在深度學習的背景下,計算圖列出了計算神經網路輸出所需的計算順序——這將用於計算反向傳播所需的梯度,反向傳播是神經網路的主要訓練演算法。

讓我們透過一個具體的例子來說明計算圖的概念。下面的程式碼實作了一個簡單的邏輯迴歸分類別器的前向傳遞(預測步驟),這可以被視為單層神經網路。它傳回一個介於 0 和 1 之間的分數,在計算損失時與真實類別標籤(0 或 1)進行比較。

import torch.nn.functional as F

y = torch.tensor([1.0])
x1 = torch.tensor([1.1])
w1 = torch.tensor([2.2])
b = torch.tensor([0.0])

z = x1 * w1 + b
a = torch.sigmoid(z)
loss = F.binary_cross_entropy(a, y)

內容解密:

  • y 代表真實標籤,是一個 PyTorch 張量。
  • x1 是輸入特徵,w1 是權重引數,b 是偏差單元,這些都是可訓練的引數。
  • z 是淨輸入,是 x1w1 的乘積加上偏差 b 的結果。
  • a 是經過 sigmoid 啟用函式處理後的輸出,代表模型的預測結果。
  • loss 是透過比較模型輸出 a 和真實標籤 y 計算出的二元交叉熵損失。

這個過程可以被視覺化為一個計算圖,如圖 A.7 所示。PyTorch 在後台構建這樣的計算圖,我們可以利用它來計算損失函式相對於模型引數的梯度,以訓練模型。

A.4 自動微分變得簡單

如果我們在 PyTorch 中進行計算,並且其中一個終端節點的 requires_grad 屬性設定為 True,PyTorch 將預設構建一個計算圖。這在我們想要計算梯度時非常有用。梯度是透過流行的反向傳播演算法訓練神經網路時所必需的,可以被視為微積分中鏈式規則在神經網路中的實作,如圖 A.8 所示。

圖 A.8 的內容解密:

  • 圖中展示了偏導數的概念,它衡量了一個函式相對於其變數之一的變化率。
  • 梯度是一個向量,包含了多變數函式的所有偏導數。
  • 鏈式規則是一種在計算圖中計算損失函式相對於模型引數梯度的方法,提供更新每個引數以最小化損失函式所需的資訊。
# 假設我們有以下的 PyTorch 張量和運算
x = torch.tensor([1.0], requires_grad=True)
y = x * x
z = y * 2

# 對 z 進行反向傳播
z.backward()

# 輸出 x 的梯度
print(x.grad)

內容解密:

  • x 是一個具有 requires_grad=True 的張量,表示我們想要追蹤它的梯度。
  • yx 的平方,zy 的兩倍。
  • 呼叫 z.backward() 會自動計算 z 相對於 x 的梯度,並將結果儲存在 x.grad 中。
  • 最後,我們列印預出 x 的梯度,這裡應該是 4.0,因為 z = 2*x^2,其導數是 4*x,當 x=1 時,導數為 4

PyTorch 自動微分引擎與多層神經網路實作

PyTorch 是一個強大的深度學習框架,其核心元件之一是自動微分(autograd)引擎。該引擎能夠自動計算損失函式相對於模型引數的梯度,從而實作高效的模型訓練。

自動微分引擎

在深度學習中,模型的訓練過程涉及計算損失函式相對於模型引數的梯度。PyTorch 的 autograd 引擎透過追蹤張量上的運算來構建計算圖,然後利用該圖計算梯度。

使用 grad 函式計算梯度

import torch.nn.functional as F
from torch.autograd import grad

y = torch.tensor([1.0])
x1 = torch.tensor([1.1])
w1 = torch.tensor([2.2], requires_grad=True)
b = torch.tensor([0.0], requires_grad=True)

z = x1 * w1 + b
a = torch.sigmoid(z)
loss = F.binary_cross_entropy(a, y)

grad_L_w1 = grad(loss, w1, retain_graph=True)
grad_L_b = grad(loss, b, retain_graph=True)

print(grad_L_w1)
print(grad_L_b)

內容解密:

  1. requires_grad=True 表示需要對該張量計算梯度。
  2. grad 函式用於計算損失函式相對於指定引數的梯度。
  3. retain_graph=True 確保計算圖在計算梯度後仍然保留,以便重複使用。

使用 .backward 方法自動計算梯度

loss.backward()
print(w1.grad)
print(b.grad)

內容解密:

  1. .backward 方法自動計算損失函式相對於所有葉節點(即模型引數)的梯度。
  2. 梯度結果儲存在張量的 .grad 屬性中。

多層神經網路實作

PyTorch 提供了 torch.nn.Module 類別作為實作自定義神經網路架構的基礎。透過繼承該類別,可以定義自己的網路架構。

定義多層感知器(MLP)

class NeuralNetwork(torch.nn.Module):
    def __init__(self, num_inputs, num_outputs):
        super().__init__()
        self.layers = torch.nn.Sequential(
            torch.nn.Linear(num_inputs, 30),
            torch.nn.ReLU(),
            torch.nn.Linear(30, 20),
            torch.nn.ReLU(),
            torch.nn.Linear(20, num_outputs),
        )

    def forward(self, x):
        logits = self.layers(x)
        return logits

model = NeuralNetwork(50, 3)
print(model)

內容解密:

  1. torch.nn.Sequential 用於將多個層按順序堆積疊起來。
  2. torch.nn.Linear 表示全連線層,定義了輸入和輸出的節點數量。
  3. torch.nn.ReLU 是非線性啟用函式,用於增加模型的非線性表達能力。
  4. forward 方法定義了資料在網路中的前向傳遞過程。

多層神經網路的實作細節探討

在前面的章節中,我們已經瞭解瞭如何建立一個簡單的神經網路模型。在本文中,我們將探討多層神經網路的實作細節,特別是如何檢查模型的引數、理解前向傳遞的過程,以及如何使用 PyTorch 進行有效的模型推論。

檢查模型的訓練引數

首先,讓我們檢查我們建立的神經網路模型的總訓練引數數量。這可以透過以下程式碼實作:

num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("Total number of trainable model parameters:", num_params)

內容解密:

  • model.parameters() 傳回模型中所有引數的迭代器。
  • p.numel() 計算每個引數的元素數量。
  • if p.requires_grad 確保只計算需要梯度的引數,即在訓練過程中會被更新的引數。

這段程式碼輸出了模型的總訓練引數數量,例如在本例中為 2213。這些引數主要存在於 torch.nn.Linear 層中,分別是權重矩陣和偏差向量。

檢視模型的權重矩陣

接下來,我們可以檢視模型的第一個線性層的權重矩陣:

print(model.layers[0].weight)
print(model.layers[0].weight.shape)

內容解密:

  • model.layers[0].weight 傳回第一個線性層的權重矩陣。
  • model.layers[0].weight.shape 傳回權重矩陣的形狀,在本例中為 torch.Size([30, 50]),表示這是一個 30x50 的矩陣。

權重矩陣中的數值是隨機初始化的,並且在每次模型例項化時都會不同。這種隨機初始化有助於打破訓練過程中的對稱性,從而使神經網路能夠學習到更複雜的對映關係。

設定隨機種子以確保可重現性

為了確保實驗的可重現性,我們可以設定 PyTorch 的隨機種子:

torch.manual_seed(123)
model = NeuralNetwork(50, 3)
print(model.layers[0].weight)

內容解密:

  • torch.manual_seed(123) 設定隨機種子為 123,以確保隨機數生成的一致性。

這樣做可以保證每次執行程式碼時,模型的初始權重都相同。

前向傳遞的過程

現在,讓我們來看看神經網路的前向傳遞過程:

X = torch.rand((1, 50))
out = model(X)
print(out)

內容解密:

  • torch.rand((1, 50)) 生成一個形狀為 (1, 50) 的隨機張量作為輸入資料。
  • model(X) 執行模型的前向傳遞,傳回輸出結果。

前向傳遞是指輸入資料透過神經網路的所有層,從輸入層到輸出層的整個過程。在本例中,輸出結果是一個包含三個元素的張量,分別對應於三個輸出節點的分數。

使用 torch.no_grad() 進行模型推論

當我們使用訓練好的模型進行推論時,可以使用 torch.no_grad() 上下文管理器來避免不必要的梯度計算,從而節省記憶體和計算資源:

with torch.no_grad():
    out = model(X)
    print(out)

內容解密:

  • torch.no_grad() 禁止 PyTorch 追蹤梯度資訊,以減少記憶體消耗和加快計算速度。

這在模型佈署和預測階段非常有用,因為此時不需要進行反向傳播。

計算類別成員機率

最後,如果我們需要計算類別成員機率,可以顯式呼叫 torch.softmax() 函式:

with torch.no_grad():
    out = torch.softmax(model(X), dim=1)
    print(out)

內容解密:

  • torch.softmax(model(X), dim=1) 對模型的輸出應用 softmax 函式,將輸出轉換為機率分佈。

這裡得到的機率值代表了輸入資料屬於各個類別的可能性,並且這些機率值的總和為 1。對於一個隨機初始化且未經訓練的模型,這些機率值通常會比較接近。

高效能資料載入器(Data Loaders)在 PyTorch 中的設定

在訓練模型之前,我們需要了解如何在 PyTorch 中建立高效的資料載入器(data loaders),這些載入器將在訓練過程中被反覆迭代。PyTorch 中的資料載入概念如圖 A.10 所示。

自定義資料集類別(Custom Dataset Class)

首先,我們將實作一個自定義的 Dataset 類別,用於建立訓練和測試資料集。接著,我們將使用這些資料集來建立資料載入器。讓我們從建立一個簡單的玩具資料集開始,該資料集包含五個訓練範例,每個範例有兩個特徵。

X_train = torch.tensor([
    [-1.2, 3.1],
    [-0.9, 2.9],
    [-0.5, 2.6],
    [2.3, -1.1],
    [2.7, -1.5]
])
y_train = torch.tensor([0, 0, 0, 1, 1])
X_test = torch.tensor([
    [-0.8, 2.8],
    [2.6, -1.6],
])
y_test = torch.tensor([0, 1])

自定義資料集類別的實作

接下來,我們透過繼承 PyTorch 的 Dataset 父類別來建立一個自定義的 ToyDataset 類別。

from torch.utils.data import Dataset

class ToyDataset(Dataset):
    def __init__(self, X, y):
        self.features = X
        self.labels = y

    def __getitem__(self, index):
        one_x = self.features[index]
        one_y = self.labels[index]
        return one_x, one_y

    def __len__(self):
        return self.labels.shape[0]

train_ds = ToyDataset(X_train, y_train)
test_ds = ToyDataset(X_test, y_test)

資料載入器的實作

現在,我們可以使用 PyTorch 的 DataLoader 類別來從資料集中取樣資料。

from torch.utils.data import DataLoader

torch.manual_seed(123)
train_loader = DataLoader(
    dataset=train_ds,
    batch_size=2,
    shuffle=True,
    num_workers=0
)
test_loader = DataLoader(
    dataset=test_ds,
    batch_size=2,
    shuffle=False,
    num_workers=0
)

#### 內容解密:

  • DataLoader 類別用於從資料集中載入資料,支援批次處理、洗牌等功能。
  • batch_size 引數指定每個批次的大小。
  • shuffle=True 表示在訓練過程中隨機洗牌資料,以避免模型陷入重複的更新迴圈中。
  • num_workers=0 表示不使用背景程式來載入資料。

資料載入器的迭代

例項化訓練資料載入器後,我們可以對其進行迭代。

for idx, (x, y) in enumerate(train_loader):
    print(f"Batch {idx+1}:", x, y)

輸出結果:

Batch 1: tensor([[-1.2000, 3.1000],
        [-0.5000, 2.6000]]) tensor([0, 0])
Batch 2: tensor([[ 2.3000, -1.1000],
        [-0.9000, 2.9000]]) tensor([1, 0])
Batch 3: tensor([[ 2.7000, -1.5000]]) tensor([1])

#### 內容解密:

  • 資料載入器會迭代整個訓練資料集,每個訓練範例只被存取一次,這被稱為一個訓練 epoch。
  • 由於我們設定了 batch_size=2,但是訓練範例的數量(5)不能被 2 整除,所以最後一個批次只包含一個範例。
  • torch.manual_seed(123) 用於固定隨機種子,以確保結果的可重現性。