主頁 >  其他 > 寫給程式員的機器學習入門 (二) - pytorch 與矩陣計算入門

寫給程式員的機器學習入門 (二) - pytorch 與矩陣計算入門

2020-09-14 21:43:22 其他

pytorch 簡介

pytorch 是目前世界上最流行的兩個機器學習框架的其中之一,與 tensoflow 并峙雙雄,它提供了很多方便的功能,例如根據損失自動微分計算應該怎樣調整引數,提供了一系列的數學函式封裝,還提供了一系列現成的模型,以及把模型組合起來進行訓練的框架,pytorch 的前身是 torch,基于 lua,而 pytorch 基于 python,雖然它基于 python 但底層完全由 c++ 撰寫,支持自動并列化計算和使用 GPU 加速運算,所以它的性能非常好,

傳統的機器學習有的會像前一節的例子中全部手寫,或者利用 numpy 類別庫減少一部分作業量,也有人會利用 scikit-learn (基于 numpy) 類別庫封裝好的各種經典演算法,pytorch 與 tensorflow 和傳統機器學習不一樣的是,它們把重點放在了組建類似人腦的神經元網路 (Neural Network),所以能實作傳統機器學習無法做到的非常復雜的判斷,例如判斷圖片中的物體型別,自動駕駛等,不過,它們組建的神經元網路作業方式是不是真的和人腦類似仍然有很多爭議,目前已經有人開始著手組建原理上更接近人腦的 GNN (Graph Neural Network) 網路,但仍未實用化,所以我們這個系列還是會著重講解當前已經實用化并廣泛應用在各個行業的網路模型,

學 pytorch 還是學 tensorflow 好?

對初學者來說一個很常見的問題是,學 pytorch 還是學 tensorflow 好?按目前的統計資料來說,公司更多使用 tensorflow,而研究人員更多使用 pytorch,pytorch 的增長速度非常快,有超越 tensorflow 的趨勢,我的意見是學哪個都無所謂,如果你熟悉 pytorch,學 tensorflow 也就一兩天的事情,反過來也一樣,并且 pytorch 和 tensorflow 的專案可以互相移植,選一個覺得好學的就可以了,因為我覺得 pytorch 更好學 (封裝非常直觀,使用 Dynamic Graph 使得除錯非常容易),所以這個系列會基于 pytorch 來講,

Dynamic Graph 與 Static Graph

機器學習框架按運算的流程是否需要預先固定可以分為 Dynamic Graph 和 Static Graph,Dynamic Graph 不需要預先固定運算流程,而 Static Graph 需要,舉例來說,對同一個公式 wx + b = y,Dynamic Graph 型的框架可以把 wx+b 分開寫并且逐步計算,計算的程序中隨時都可以用 print 等指令輸出途中的結果,或者把途中的結果發送到其他地方記錄起來;而 Static Graph 型的框架必須預先定好整個計算流程,你只能傳入 w, x, b 給計算器,然后讓計算器輸出 y,中途計算的結果只能使用專門的除錯器來查看,

一般的來說 Static Graph 性能會比 Dynamic Graph 好,Tensorflow (老版本) 使用的是 Static Graph,而 pytorch 使用的是 Dynamic Graph,但兩者實際性能相差很小,因為消耗資源的大部分都是矩陣運算,使用批次訓練可以很大程度減少它們的差距,順帶一提,Tensorflow 1.7 開始支持了 Dynamic Graph,并且在 2.0 默認開啟,但大部分人在使用 Tensorflow 的時候還是會用 Static Graph,

# Dynamic Graph 的印象,運算的每一步都可以插入自定義代碼
def forward(w, x, b):
    wx = w * x
    print(wx)
    y = wx + b
    print(y)
    return y
forward(w, x, b)

# Static Graph 的印象,需要預先編譯整個計算流程
forward = compile("wx+b")
forward(w, x, b)

安裝 pytorch

假設你已經安裝了 python3,執行以下命令即可安裝 pytorch:

pip3 install pytorch

之后在 python 代碼中使用 import torch 即可參考 pytorch 類別庫,

pytorch 的基本操作

接下來我們熟悉一下 pytorch 里面最基本的操作,pytorch 會用 torch.Tensor 型別來統一表現數值,向量 (一維陣列) 或矩陣 (多維陣列),模型的引數也會使用這個型別,(tensorflow 會根據用途分為好幾個型別,這點 pytorch 更簡潔明了)

torch.Tensor 型別可以使用 torch.tensor 函式構建,以下是一些簡單的例子(運行在 python 的 REPL 中):

# 參考 pytorch
>>> import torch

# 創建一個整數 tensor
>>> torch.tensor(1)
tensor(1)

# 創建一個小數 tensor
>>> torch.tensor(1.0)
tensor(1.)

# 單值 tensor 中的值可以用 item 函式取出
>>> torch.tensor(1.0).item()
1.0

# 使用一維陣列創建一個向量 tensor
>>> torch.tensor([1.0, 2.0, 3.0])
tensor([1., 2., 3.])

# 使用二維陣列創建一個矩陣 tensor
>>> torch.tensor([[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]])
tensor([[ 1.,  2.,  3.],
        [-1., -2., -3.]])

tensor 物件的數值型別可以看它的 dtype 成員:

>>> torch.tensor(1).dtype
torch.int64
>>> torch.tensor(1.0).dtype
torch.float32
>>> torch.tensor([1.0, 2.0, 3.0]).dtype
torch.float32
>>> torch.tensor([[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]]).dtype
torch.float32

pytorch 支持整數型別 torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64 ,浮點數型別 torch.float16, torch.float32, torch.float64,還有布林值型別 torch.bool,型別后的數字代表它的位數 (bit 數),而 uint8 前面的 u 代表它是無符號數 (unsigned),實際絕大部分場景都只會使用 torch.float32,雖然精度沒有 torch.float64 高但它占用記憶體小并且運算速度快,注意一個 tensor 物件里面只能保存一種型別的數值,不能混合存放,

創建 tensor 物件時可以通過 dtype 引數強制指定型別:

>>> torch.tensor(1, dtype=torch.int32)
tensor(1, dtype=torch.int32)
>>> torch.tensor([1.1, 2.9, 3.5], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)

>>> torch.tensor(1, dtype=torch.int64)
tensor(1)

>>> torch.tensor(1, dtype=torch.float32)
tensor(1.)

>>> torch.tensor(1, dtype=torch.float64)
tensor(1., dtype=torch.float64)
>>> torch.tensor([1, 2, 3], dtype=torch.float64)
tensor([1., 2., 3.], dtype=torch.float64)

>>> torch.tensor([1, 2, 0], dtype=torch.bool)
tensor([ True,  True, False])

tensor 物件的形狀可以看它的 shape 成員:

# 整數 tensor 的 shape 為空
>>> torch.tensor(1).shape
torch.Size([])
>>> torch.tensor(1.0).shape
torch.Size([])

# 陣列 tensor 的 shape 只有一個值,代表陣列的長度
>>> torch.tensor([1.0]).shape
torch.Size([1])
>>> torch.tensor([1.0, 2.0, 3.0]).shape
torch.Size([3])

# 矩陣 tensor 的 shape 根據它的維度而定,每個值代表各個維度的大小,這個例子代表矩陣有 2 行 3 列
>>> torch.tensor([[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]]).shape
torch.Size([2, 3])

tensor 物件與數值,tensor 物件與 tensor 物件之間可以進行運算:

>>> torch.tensor(1.0) * 2
tensor(2.)
>>> torch.tensor(1.0) * torch.tensor(2.0)
tensor(2.)
>>> torch.tensor(3.0) * torch.tensor(2.0)
tensor(6.)

向量和矩陣還可以批量進行運算(內部會并列化運算):

# 向量和數值之間的運算
>>> torch.tensor([1.0, 2.0, 3.0])
tensor([1., 2., 3.])
>>> torch.tensor([1.0, 2.0, 3.0]) * 3
tensor([3., 6., 9.])
>>> torch.tensor([1.0, 2.0, 3.0]) * 3 - 1
tensor([2., 5., 8.])

# 矩陣和單值 tensor 物件之間的運算
>>> torch.tensor([[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]])
tensor([[ 1.,  2.,  3.],
        [-1., -2., -3.]])
>>> torch.tensor([[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]]) / torch.tensor(2)
tensor([[ 0.5000,  1.0000,  1.5000],
        [-0.5000, -1.0000, -1.5000]])

# 矩陣和與矩陣最后一個維度相同長度向量之間的運算
>>> torch.tensor([[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0]]) * torch.tensor([1.0, 1.5, 2.0])
tensor([[ 1.,  3.,  6.],
        [-1., -3., -6.]])

tensor 物件之間的運算一般都會生成一個新的 tensor 物件,如果你想避免生成新物件 (提高性能),可以使用 _ 結尾的函式,它們會修改原有的物件:

# 生成新物件,原有物件不變,add 和 + 意義相同
>>> a = torch.tensor([1,2,3])
>>> b = torch.tensor([7,8,9])
>>> a.add(b)
tensor([ 8, 10, 12])
>>> a
tensor([1, 2, 3])

# 在原有物件上執行操作,避免生成新物件
>>> a.add_(b)
tensor([ 8, 10, 12])
>>> a
tensor([ 8, 10, 12])

pytorch 還提供了一系列方便的函式求最大值,最小值,平均值,標準差等:

>>> torch.tensor([1.0, 2.0, 3.0])
tensor([1., 2., 3.])
>>> torch.tensor([1.0, 2.0, 3.0]).min()
tensor(1.)
>>> torch.tensor([1.0, 2.0, 3.0]).max()
tensor(3.)
>>> torch.tensor([1.0, 2.0, 3.0]).mean()
tensor(2.)
>>> torch.tensor([1.0, 2.0, 3.0]).std()
tensor(1.)

pytorch 還支持比較 tensor 物件來生成布林值型別的 tensor:

# tensor 物件與數值比較
>>> torch.tensor([1.0, 2.0, 3.0]) > 1.0
tensor([False,  True,  True])
>>> torch.tensor([1.0, 2.0, 3.0]) <= 2.0
tensor([ True,  True, False])

# tensor 物件與 tensor 物件比較
>>> torch.tensor([1.0, 2.0, 3.0]) > torch.tensor([1.1, 1.9, 3.0])
tensor([False,  True, False])
>>> torch.tensor([1.0, 2.0, 3.0]) <= torch.tensor([1.1, 1.9, 3.0])
tensor([ True, False,  True])

pytorch 還支持生成指定形狀的 tensor 物件:

# 生成 2 行 3 列的矩陣 tensor,值全部為 0
>>> torch.zeros(2, 3)
tensor([[0., 0., 0.],
        [0., 0., 0.]])

# 生成 3 行 2 列的矩陣 tensor,值全部為 1
torch.ones(3, 2)
>>> torch.ones(2, 3)
tensor([[1., 1., 1.],
        [1., 1., 1.]])

# 生成 3 行 2 列的矩陣 tensor,值全部為 100
>>> torch.full((3, 2), 100)
tensor([[100., 100.],
        [100., 100.],
        [100., 100.]])

# 生成 3 行 3 列的矩陣 tensor,值為范圍 [0, 1) 的隨機浮點數
>>> torch.rand(3, 3)
tensor([[0.4012, 0.2412, 0.1532],
        [0.1178, 0.2319, 0.4056],
        [0.7879, 0.8318, 0.7452]])

# 生成 3 行 3 列的矩陣 tensor,值為范圍 [1, 10] 的隨機整數
>>> (torch.rand(3, 3) * 10 + 1).long()
tensor([[ 8,  1,  5],
        [ 8,  6,  5],
        [ 1,  6, 10]])

# 和上面的寫法效果一樣
>>> torch.randint(1, 11, (3, 3))
tensor([[7, 1, 3],
        [7, 9, 8],
        [4, 7, 3]])

這里提到的操作只是常用的一部分,如果你想了解更多 tensor 物件支持的操作,可以參考以下檔案:

  • https://pytorch.org/docs/stable/tensors.html

pytorch 保存 tensor 使用的資料結構

為了減少記憶體占用與提升訪問速度,pytorch 會使用一塊連續的儲存空間 (不管是在系統記憶體還是在 GPU 記憶體中) 保存 tensor,不管 tensor 是數值,向量還是矩陣,

我們可以使用 storage 查看 tensor 物件使用的儲存空間:

# 數值的儲存空間長度是 1
>>> torch.tensor(1).storage()
 1
[torch.LongStorage of size 1]

# 向量的儲存空間長度等于向量的長度
>>> torch.tensor([1, 2, 3], dtype=torch.float32).storage()
 1.0
 2.0
 3.0
[torch.FloatStorage of size 3]

# 矩陣的儲存空間長度等于所有維度相乘的結果,這里是 2 行 3 列總共 6 個元素
>>> torch.tensor([[1, 2, 3], [-1, -2, -3]], dtype=torch.float64).storage()
 1.0
 2.0
 3.0
 -1.0
 -2.0
 -3.0
[torch.DoubleStorage of size 6]

pytorch 會使用 stride 來確定一個 tensor 物件的維度:

# 儲存空間有 6 個元素
>>> torch.tensor([[1, 2, 3], [-1, -2, -3]]).storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]

# 第一個維度是 2,第二個維度是 3 (2 行 3 列)
>>> torch.tensor([[1, 2, 3], [-1, -2, -3]]).shape
torch.Size([2, 3])

# stride 的意義是表示每個維度之間元素的距離
# 第一個維度會按 3 個元素來切分 (6 個元素可以切分成 2 組),第二個維度會按 1 個元素來切分 (3 個元素)
>>> torch.tensor([[1, 2, 3], [-1, -2, -3]])
tensor([[ 1,  2,  3],
        [-1, -2, -3]])
>>> torch.tensor([[1, 2, 3], [-1, -2, -3]]).stride()
(3, 1)

pytorch 的一個很強大的地方是,通過 view 函式可以修改 tensor 物件的維度 (內部改變了 stride),但是不需要創建新的儲存空間并復制元素:

# 創建一個 2 行 3 列的矩陣
>>> a = torch.tensor([[1, 2, 3], [-1, -2, -3]])
>>> a
tensor([[ 1,  2,  3],
        [-1, -2, -3]])
>>> a.shape
torch.Size([2, 3])
>>> a.stride()
(3, 1)

# 把維度改為 3 行 2 列
>>> b = a.view(3, 2)
>>> b
tensor([[ 1,  2],
        [ 3, -1],
        [-2, -3]])
>>> b.shape
torch.Size([3, 2])
>>> b.stride()
(2, 1)

# 轉換為向量
>>> c = b.view(6)
>>> c
tensor([ 1,  2,  3, -1, -2, -3])
>>> c.shape
torch.Size([6])
>>> c.stride()
(1,)

# 它們的儲存空間是一樣的
>>> a.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]
>>> b.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]
>>> c.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]

使用 stride 確定維度的另一個意義是它可以支持共用同一個空間實作轉置 (Transpose) 操作:

# 創建一個 2 行 3 列的矩陣
>>> a = torch.tensor([[1, 2, 3], [-1, -2, -3]])
>>> a
tensor([[ 1,  2,  3],
        [-1, -2, -3]])
>>> a.shape
torch.Size([2, 3])
>>> a.stride()
(3, 1)

# 使用轉置操作交換維度 (行轉列)
>>> b = a.transpose(0, 1)
>>> b
tensor([[ 1, -1],
        [ 2, -2],
        [ 3, -3]])
>>> b.shape
torch.Size([3, 2])
>>> b.stride()
(1, 3)

# 它們的儲存空間是一樣的
>>> a.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]
>>> b.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]

轉置操作內部就是交換了指定維度在 stride 中對應的值,你可以根據前面的描述想想物件在轉置后的矩陣中會如何劃分,

現在再想想,如果把轉置后的矩陣用 view 函式專為向量會變為什么?會變為 [1, -1, 2, -2, 3, -3] 嗎?

實際上這樣的操作會導致出錯??:

>>> b
tensor([[ 1, -1],
        [ 2, -2],
        [ 3, -3]])
>>> b.view(6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.

這是因為轉置后矩陣元素的自然順序和儲存空間中的順序不一致,我們可以用 is_contiguous 函式來檢測:

>>> a.is_contiguous()
True
>>> b.is_contiguous()
False

解決這個問題的方法是首先用 contiguous 函式把儲存空間另外復制一份使得順序一致,然后再用 view 函式改變維度;或者用更方便的 reshape 函式,reshape 函式會檢測改變維度的時候是否需要復制儲存空間,如果需要則復制,不需要則和 view 一樣只修改內部的 stride

>>> b.contiguous().view(6)
tensor([ 1, -1,  2, -2,  3, -3])
>>> b.reshape(6)
tensor([ 1, -1,  2, -2,  3, -3])

pytorch 還支持截取儲存空間的一部分來作為一個新的 tensor 物件,基于內部的 storage_offsetsize 屬性,同樣不需要復制:

# 截取向量的例子
>>> a = torch.tensor([1, 2, 3, -1, -2, -3])
>>> b = a[1:3]
>>> b
tensor([2, 3])
>>> b.storage_offset()
1
>>> b.size()
torch.Size([2])
>>> b.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]

# 截取矩陣的例子
>>> a.view(3, 2)
tensor([[ 1,  2],
        [ 3, -1],
        [-2, -3]])
>>> c = a.view(3, 2)[1:] # 第一維度 (行) 截取 1~結尾, 第二維度不截取
>>> c
tensor([[ 3, -1],
        [-2, -3]])
>>> c.storage_offset()
2
>>> c.size()
torch.Size([2, 2])
>>> c.stride()
(2, 1)
>>> c.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]

# 截取轉置后矩陣的例子,更復雜一些
>>> a.view(3, 2).transpose(0, 1)
tensor([[ 1,  3, -2],
        [ 2, -1, -3]])
>>> c = a.view(3, 2).transpose(0, 1)[:,1:] # 第一維度 (行) 不截取,第二維度 (列) 截取 1~結尾
>>> c
tensor([[ 3, -2],
        [-1, -3]])
>>> c.storage_offset()
2
>>> c.size()
torch.Size([2, 2])
>>> c.stride()
(1, 2)
>>> c.storage()
 1
 2
 3
 -1
 -2
 -3
[torch.LongStorage of size 6]

好了,看完這一節你應該對 pytorch 如何儲存 tensor 物件有一個比較基礎的了解,為了容易理解本節最多只使用二維矩陣做例子,你可以自己試試更多維度的矩陣是否可以用同樣的方式操作,

矩陣乘法簡介

接下來我們看看矩陣乘法 (Matrix Multiplication),這是機器學習中最最最頻繁的操作,高中學過并且還記得的就當復習一下吧,

以下是一個簡單的例子,一個 2 行 3 列的矩陣乘以一個 3 行 4 列的矩陣可以得出一個 2 行 4 列的矩陣:

矩陣乘法會把第一個矩陣的每一行與第二個矩陣的每一列相乘的各個合計值作為結果,可以參考下圖理解:

按這個規則來算,一個 n 行 m 列的矩陣和一個 m 行 p 列的矩陣相乘,會得出一個 n 行 p 列的矩陣 (第一個矩陣的列數與第二個矩陣的行數必須相同),

那矩陣乘法有什么意義呢?矩陣乘法在機器學習中的意義是可以把對多個輸入輸出或者中間值的計算合并到一個操作中 (在數學上也可以大幅簡化公式),框架可以在內部并列化計算,因為高端的 GPU 有幾千個核心,把計算分布到幾千個核心中可以大幅提升運算速度,在接下來的例子中也可以看到如何用矩陣乘法實作批次訓練,

使用 pytorch 進行矩陣乘法計算

在 pytorch 中矩陣乘法可以呼叫 mm 函式:

>>> a = torch.tensor([[1,2,3],[4,5,6]])
>>> b = torch.tensor([[4,3,2,1],[8,7,6,5],[9,9,9,9]])
>>> a.mm(b)
tensor([[ 47,  44,  41,  38],
        [110, 101,  92,  83]])

# 如果大小不匹配會出錯
>>> a = torch.tensor([[1,2,3],[4,5,6]])
>>> b = torch.tensor([[4,3,2,1],[8,7,6,5]])
>>> a.mm(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: size mismatch, m1: [2 x 3], m2: [2 x 4] at ../aten/src/TH/generic/THTensorMath.cpp:197

# mm 函式也可以用 @ 運算子代替,結果是一樣的
>>> a = torch.tensor([[1,2,3],[4,5,6]])
>>> b = torch.tensor([[4,3,2,1],[8,7,6,5],[9,9,9,9]])
>>> a @ b
tensor([[ 47,  44,  41,  38],
        [110, 101,  92,  83]])

針對更多維度的矩陣乘法,pytorch 提供了 matmul 函式:

# n x m 的矩陣與 q x m x p 的矩陣相乘會得出 q x n x p 的矩陣
>>> a = torch.ones(2,3)
>>> b = torch.ones(5,3,4)
>>> a.matmul(b)
tensor([[[3., 3., 3., 3.],
         [3., 3., 3., 3.]],

        [[3., 3., 3., 3.],
         [3., 3., 3., 3.]],

        [[3., 3., 3., 3.],
         [3., 3., 3., 3.]],

        [[3., 3., 3., 3.],
         [3., 3., 3., 3.]],

        [[3., 3., 3., 3.],
         [3., 3., 3., 3.]]])
>>> a.matmul(b).shape
torch.Size([5, 2, 4])

pytorch 的自動微分功能 (autograd)

pytorch 支持自動微分求導函式值 (即各個引數的梯度),利用這個功能我們不再需要通過數學公式求各個引數的導函式值,使得機器學習的門檻低了很多????,以下是這個功能的例子:

# 定義引數
# 創建 tensor 物件時設定 requires_grad 為 True 即可開啟自動微分功能
>>> w = torch.tensor(1.0, requires_grad=True)
>>> b = torch.tensor(0.0, requires_grad=True)

# 定義輸入和輸出的 tensor
>>> x = torch.tensor(2)
>>> y = torch.tensor(5)

# 計算預測輸出
>>> p = x * w + b
>>> p
tensor(2., grad_fn=<AddBackward0>)

# 計算損失
# 注意 pytorch 的自動微分功能要求損失不能為負數,因為 pytorch 只會考慮減少損失而不是讓損失接近 0
# 這里用 abs 讓損失變為絕對值
>>> l = (p - y).abs()
>>> l
tensor(3., grad_fn=<AbsBackward>)

# 從損失自動微分求導函式值
>>> l.backward()

# 查看各個引數對應的導函式值
# 注意 pytorch 會假設讓引數減去 grad 的值才能減少損失,所以這里是負數(引數會變大)
>>> w.grad
tensor(-2.)
>>> b.grad
tensor(-1.)

# 定義學習比率,即每次根據導函式值調整引數的比率
>>> learning_rate = 0.01

# 調整引數時需要用 torch.no_grad 來臨時禁止自動微分功能
>>> with torch.no_grad():
...     w -= w.grad * learning_rate
...     b -= b.grad * learning_rate
...

# 我們可以看到 weight 和 bias 分別增加了 0.02 和 0.01
>>> w
tensor(1.0200, requires_grad=True)
>>> b
tensor(0.0100, requires_grad=True)

# 最后我們需要清空引數的 grad 值,這個值不會自動清零(因為某些模型需要疊加導函式值)
# 你可以試試再調一次 backward,會發現 grad 把兩次的值疊加起來
>>> w.grad.zero_()
>>> b.grad.zero_()

我們再來試試前一節提到的讓損失等于相差值平方的方法:

# 定義引數
>>> w = torch.tensor(1.0, requires_grad=True)
>>> b = torch.tensor(0.0, requires_grad=True)

# 定義輸入和輸出的 tensor
>>> x = torch.tensor(2)
>>> y = torch.tensor(5)

# 計算預測輸出
>>> p = x * w + b
>>> p
tensor(2., grad_fn=<AddBackward0>)

# 計算相差值
>>> d = p - y
>>> d
tensor(-3., grad_fn=<SubBackward0>)

# 計算損失 (相差值的平方, 一定會是 0 或者正數)
>>> l = d ** 2
>>> l
tensor(9., grad_fn=<PowBackward0>)

# 從損失自動微分求導函式值
>>> l.backward()

# 查看各個引數對應的導函式值,跟我們上一篇用數學公式求出來的值一樣吧
# w 的導函式值 = 2 * d * x = 2 * -3 * 2 = -12
# b 的導函式值 = 2 * d = 2 * -3 = -6
>>> w.grad
tensor(-12.)
>>> b.grad
tensor(-6.)

# 之后和上一個例子一樣調整引數即可

膩害叭??,再復雜的模型只要呼叫 backward 都可以自動幫我們計算出導函式值,從現在開始我們可以把數學課本丟掉了 (這是開玩笑的,一些問題仍然需要用數學來理解,但大部分情況下只有基礎數學知識的人也能玩得起),

pytorch 的損失計算器封裝 (loss function)

pytorch 提供了幾種常見的損失計算器的封裝,我們最開始看到的也稱 L1 損失 (L1 Loss),表示所有預測輸出與正確輸出的相差的絕對值的平均 (有的場景會有多個輸出),以下是使用 L1 損失的例子:

# 定義引數
>>> w = torch.tensor(1.0, requires_grad=True)
>>> b = torch.tensor(0.0, requires_grad=True)

# 定義輸入和輸出的 tensor
# 注意 pytorch 提供的損失計算器要求預測輸出和正確輸出均為浮點數,所以定義輸入與輸出的時候也需要用浮點數
>>> x = torch.tensor(2.0)
>>> y = torch.tensor(5.0)

# 創建損失計算器
>>> loss_function = torch.nn.L1Loss()

# 計算預測輸出
>>> p = x * w + b
>>> p
tensor(2., grad_fn=<AddBackward0>)

# 計算損失
# 等同于 (p - y).abs().mean()
>>> l = loss_function(p, y)
>>> l
tensor(3., grad_fn=<L1LossBackward>)

而計算相差值的平方作為損失稱為 MSE 損失 (Mean Squared Error),有的地方又稱 L2 損失,以下是使用 MSE 損失的例子:

# 定義引數
>>> w = torch.tensor(1.0, requires_grad=True)
>>> b = torch.tensor(0.0, requires_grad=True)

# 定義輸入和輸出的 tensor
>>> x = torch.tensor(2.0)
>>> y = torch.tensor(5.0)

# 創建損失計算器
>>> loss_function = torch.nn.MSELoss()

# 計算預測輸出
>>> p = x * w + b
>>> p
tensor(2., grad_fn=<AddBackward0>)

# 計算損失
# 等同于 ((p - y) ** 2).mean()
>>> l = loss_function(p, y)
>>> l
tensor(9., grad_fn=<MseLossBackward>)

方便叭???,如果你想看更多的損失計算器可以參考以下地址:

  • https://pytorch.org/docs/stable/nn.html#loss-functions

pytorch 的引數調整器封裝 (optimizer)

pytorch 還提供了根據導函式值調整引數的調整器封裝,我們在這兩篇文章中看到的方法 (隨機初始化引數值,然后根據導函式值 * 學習比率調整引數減少損失) 又稱隨機梯度下降法 (Stochastic Gradient Descent),以下是使用封裝好的調整器的例子:

# 定義引數
>>> w = torch.tensor(1.0, requires_grad=True)
>>> b = torch.tensor(0.0, requires_grad=True)

# 定義輸入和輸出的 tensor
>>> x = torch.tensor(2.0)
>>> y = torch.tensor(5.0)

# 創建損失計算器
>>> loss_function = torch.nn.MSELoss()

# 創建引數調整器
# 需要傳入引數串列和指定學習比率,這里的學習比率是 0.01
>>> optimizer = torch.optim.SGD([w, b], lr=0.01)

# 計算預測輸出
>>> p = x * w + b
>>> p
tensor(2., grad_fn=<AddBackward0>)

# 計算損失
>>> l = loss_function(p, y)
>>> l
tensor(9., grad_fn=<MseLossBackward>)

# 從損失自動微分求導函式值
>>> l.backward()

# 確認引數的導函式值
>>> w.grad
tensor(-12.)
>>> b.grad
tensor(-6.)

# 使用引數調整器調整引數
# 等同于:
# with torch.no_grad():
#     w -= w.grad * learning_rate
#     b -= b.grad * learning_rate
optimizer.step()

# 清空導函式值
# 等同于:
# w.grad.zero_()
# b.grad.zero_()
optimizer.zero_grad()

# 確認調整后的引數
>>> w
tensor(1.1200, requires_grad=True)
>>> b
tensor(0.0600, requires_grad=True)
>>> w.grad
tensor(0.)
>>> b.grad
tensor(0.)

SGD 引數調整器的學習比率是固定的,如果我們想在學習程序中自動調整學習比率,可以使用其他引數調整器,例如 Adam 調整器,此外,你還可以開啟沖量 (momentum) 選項改進學習速度,該選項開啟后可以在引數調整時參考前一次調整的方向 (正負),如果相同則調整更多,而不同則調整更少,

如果你對 Adam 調整器的實作和沖量的實作有興趣,可以參考以下文章 (需要一定的數學知識):

  • https://mlfromscratch.com/optimizers-explained

如果你想查看 pytorch 提供的其他引數調整器可以訪問以下地址:

  • https://pytorch.org/docs/stable/optim.html

使用 pytorch 實作上一篇文章的例子

好了,學到這里我們應該對 pytorch 的基本操作有一定了解,現在我們來試試用 pytorch 實作上一篇文章最后的例子,

上一篇文章最后的例子代碼如下:

# 定義引數
weight = 1
bias = 0

# 定義學習比率
learning_rate = 0.01

# 準備訓練集,驗證集和測驗集
traning_set = [(2, 5), (5, 11), (6, 13), (7, 15), (8, 17)]
validating_set = [(12, 25), (1, 3)]
testing_set = [(9, 19), (13, 27)]

# 記錄 weight 與 bias 的歷史值
weight_history = [weight]
bias_history = [bias]

for epoch in range(1, 10000):
    print(f"epoch: {epoch}")

    # 根據訓練集訓練并修改引數
    for x, y in traning_set:
        # 計算預測值
        predicted = x * weight + bias
        # 計算損失
        diff = predicted - y
        loss = diff ** 2
        # 列印除錯資訊
        print(f"traning x: {x}, y: {y}, predicted: {predicted}, loss: {loss}, weight: {weight}, bias: {bias}")
        # 計算導函式值
        derivative_weight = 2 * diff * x
        derivative_bias = 2 * diff
        # 修改 weight 和 bias 以減少 loss
        # diff 為正時代表預測輸出 > 正確輸出,會減少 weight 和 bias
        # diff 為負時代表預測輸出 < 正確輸出,會增加 weight 和 bias
        weight -= derivative_weight * learning_rate
        bias -= derivative_bias * learning_rate
        # 記錄 weight 和 bias 的歷史值
        weight_history.append(weight)
        bias_history.append(bias)

    # 檢查驗證集
    validating_accuracy = 0
    for x, y in validating_set:
        predicted = x * weight + bias
        validating_accuracy += 1 - abs(y - predicted) / y
        print(f"validating x: {x}, y: {y}, predicted: {predicted}")
    validating_accuracy /= len(validating_set)

    # 如果驗證集正確率大于 99 %,則停止訓練
    print(f"validating accuracy: {validating_accuracy}")
    if validating_accuracy > 0.99:
        break

# 檢查測驗集
testing_accuracy = 0
for x, y in testing_set:
    predicted = x * weight + bias
    testing_accuracy += 1 - abs(y - predicted) / y
    print(f"testing x: {x}, y: {y}, predicted: {predicted}")
testing_accuracy /= len(testing_set)
print(f"testing accuracy: {testing_accuracy}")

# 顯示 weight 與 bias 的變化
from matplotlib import pyplot
pyplot.plot(weight_history, label="weight")
pyplot.plot(bias_history, label="bias")
pyplot.legend()
pyplot.show()

使用 pytorch 實作后代碼如下:

# 參考 pytorch
import torch

# 定義引數
weight = torch.tensor(1.0, requires_grad=True)
bias = torch.tensor(0.0, requires_grad=True)

# 創建損失計算器
loss_function = torch.nn.MSELoss()

# 創建引數調整器
optimizer = torch.optim.SGD([weight, bias], lr=0.01)

# 準備訓練集,驗證集和測驗集
traning_set = [
    (torch.tensor(2.0), torch.tensor(5.0)),
    (torch.tensor(5.0), torch.tensor(11.0)),
    (torch.tensor(6.0), torch.tensor(13.0)),
    (torch.tensor(7.0), torch.tensor(15.0)),
    (torch.tensor(8.0), torch.tensor(17.0))
]
validating_set = [
    (torch.tensor(12.0), torch.tensor(25.0)),
    (torch.tensor(1.0), torch.tensor(3.0))
]
testing_set = [
    (torch.tensor(9.0), torch.tensor(19.0)),
    (torch.tensor(13.0), torch.tensor(27.0))
]

# 記錄 weight 與 bias 的歷史值
weight_history = [weight.item()]
bias_history = [bias.item()]

for epoch in range(1, 10000):
    print(f"epoch: {epoch}")

    # 根據訓練集訓練并修改引數
    for x, y in traning_set:
        # 計算預測值
        predicted = x * weight + bias
        # 計算損失
        loss = loss_function(predicted, y)
        # 列印除錯資訊
        print(f"traning x: {x}, y: {y}, predicted: {predicted}, loss: {loss}, weight: {weight}, bias: {bias}")
        # 從損失自動微分求導函式值
        loss.backward()
        # 使用引數調整器調整引數
        optimizer.step()
        # 清空導函式值
        optimizer.zero_grad()
        # 記錄 weight 和 bias 的歷史值
        weight_history.append(weight.item())
        bias_history.append(bias.item())

    # 檢查驗證集
    validating_accuracy = 0
    for x, y in validating_set:
        predicted = x * weight.item() + bias.item()
        validating_accuracy += 1 - abs(y - predicted) / y
        print(f"validating x: {x}, y: {y}, predicted: {predicted}")
    validating_accuracy /= len(validating_set)

    # 如果驗證集正確率大于 99 %,則停止訓練
    print(f"validating accuracy: {validating_accuracy}")
    if validating_accuracy > 0.99:
        break

# 檢查測驗集
testing_accuracy = 0
for x, y in testing_set:
    predicted = x * weight.item() + bias.item()
    testing_accuracy += 1 - abs(y - predicted) / y
    print(f"testing x: {x}, y: {y}, predicted: {predicted}")
testing_accuracy /= len(testing_set)
print(f"testing accuracy: {testing_accuracy}")

# 顯示 weight 與 bias 的變化
from matplotlib import pyplot
pyplot.plot(weight_history, label="weight")
pyplot.plot(bias_history, label="bias")
pyplot.legend()
pyplot.show()

輸出如下:

epoch: 1
traning x: 2.0, y: 5.0, predicted: 2.0, loss: 9.0, weight: 1.0, bias: 0.0
traning x: 5.0, y: 11.0, predicted: 5.659999847412109, loss: 28.515602111816406, weight: 1.1200000047683716, bias: 0.05999999865889549
traning x: 6.0, y: 13.0, predicted: 10.090799331665039, loss: 8.463448524475098, weight: 1.6540000438690186, bias: 0.16679999232292175
traning x: 7.0, y: 15.0, predicted: 14.246713638305664, loss: 0.5674403309822083, weight: 2.0031042098999023, bias: 0.22498400509357452
traning x: 8.0, y: 17.0, predicted: 17.108564376831055, loss: 0.011786224320530891, weight: 2.1085643768310547, bias: 0.24004973471164703
validating x: 12.0, y: 25.0, predicted: 25.33220863342285
validating x: 1.0, y: 3.0, predicted: 2.3290724754333496
validating accuracy: 0.8815345764160156
epoch: 2
traning x: 2.0, y: 5.0, predicted: 4.420266628265381, loss: 0.3360907733440399, weight: 2.0911941528320312, bias: 0.2378784418106079
traning x: 5.0, y: 11.0, predicted: 10.821391105651855, loss: 0.03190113604068756, weight: 2.1143834590911865, bias: 0.24947310984134674
traning x: 6.0, y: 13.0, predicted: 13.04651165008545, loss: 0.002163333585485816, weight: 2.132244348526001, bias: 0.25304529070854187
traning x: 7.0, y: 15.0, predicted: 15.138755798339844, loss: 0.019253171980381012, weight: 2.1266629695892334, bias: 0.25211507081985474
traning x: 8.0, y: 17.0, predicted: 17.107236862182617, loss: 0.011499744839966297, weight: 2.1072371006011963, bias: 0.24933995306491852
validating x: 12.0, y: 25.0, predicted: 25.32814598083496
validating x: 1.0, y: 3.0, predicted: 2.3372745513916016
validating accuracy: 0.8829828500747681
epoch: 3
traning x: 2.0, y: 5.0, predicted: 4.427353858947754, loss: 0.32792359590530396, weight: 2.0900793075561523, bias: 0.24719521403312683
traning x: 5.0, y: 11.0, predicted: 10.82357406616211, loss: 0.0311261098831892, weight: 2.112985134124756, bias: 0.2586481273174286
traning x: 6.0, y: 13.0, predicted: 13.045942306518555, loss: 0.002110695466399193, weight: 2.1306276321411133, bias: 0.26217663288116455
traning x: 7.0, y: 15.0, predicted: 15.137059211730957, loss: 0.018785227090120316, weight: 2.1251144409179688, bias: 0.2612577974796295
traning x: 8.0, y: 17.0, predicted: 17.105924606323242, loss: 0.011220022104680538, weight: 2.105926036834717, bias: 0.2585166096687317
validating x: 12.0, y: 25.0, predicted: 25.324134826660156
validating x: 1.0, y: 3.0, predicted: 2.3453762531280518
validating accuracy: 0.8844133615493774

省略途中的輸出

epoch: 202
traning x: 2.0, y: 5.0, predicted: 4.950470924377441, loss: 0.0024531292729079723, weight: 2.0077908039093018, bias: 0.9348894953727722
traning x: 5.0, y: 11.0, predicted: 10.984740257263184, loss: 0.00023285974748432636, weight: 2.0097720623016357, bias: 0.9358800649642944
traning x: 6.0, y: 13.0, predicted: 13.003972053527832, loss: 1.5777208318468183e-05, weight: 2.0112979412078857, bias: 0.9361852407455444
traning x: 7.0, y: 15.0, predicted: 15.011855125427246, loss: 0.00014054399798624218, weight: 2.0108213424682617, bias: 0.9361057877540588
traning x: 8.0, y: 17.0, predicted: 17.00916290283203, loss: 8.39587883092463e-05, weight: 2.0091617107391357, bias: 0.9358686804771423
validating x: 12.0, y: 25.0, predicted: 25.028034210205078
validating x: 1.0, y: 3.0, predicted: 2.9433810710906982
validating accuracy: 0.9900028705596924
testing x: 9.0, y: 19.0, predicted: 19.004947662353516
testing x: 13.0, y: 27.0, predicted: 27.035730361938477
testing accuracy: 0.9992080926895142

同樣的訓練成功了??,你可能會發現輸出的值和前一篇文章的值有一些不同,這是因為 pytorch 默認使用 32 位浮點數 (float32) 進行運算,而 python 使用的是 64 位浮點數 (float64), 如果你把引數定義的部分改成這樣:

# 定義引數
weight = torch.tensor(1.0, dtype=torch.float64, requires_grad=True)
bias = torch.tensor(0.0, dtype=torch.float64, requires_grad=True)

然后計算損失的部分改成這樣,則可以得到和前一篇文章一樣的輸出:

# 計算損失
loss = loss_function(predicted, y.double())

使用矩陣乘法實作批次訓練

前面的例子雖然使用 pytorch 實作了訓練,但還是一個一個值的計算,我們可以用矩陣乘法來實作批次訓練,一次計算多個值,以下修改后的代碼:

# 參考 pytorch
import torch

# 定義引數
weight = torch.tensor([[1.0]], requires_grad=True) # 1 行 1 列
bias = torch.tensor(0.0, requires_grad=True)

# 創建損失計算器
loss_function = torch.nn.MSELoss()

# 創建引數調整器
optimizer = torch.optim.SGD([weight, bias], lr=0.01)

# 準備訓練集,驗證集和測驗集
traning_set_x = torch.tensor([[2.0], [5.0], [6.0], [7.0], [8.0]]) # 5 行 1 列,代表有 5 組,每組有 1 個輸入
traning_set_y = torch.tensor([[5.0], [11.0], [13.0], [15.0], [17.0]]) # 5 行 1 列,代表有 5 組,每組有 1 個輸出
validating_set_x = torch.tensor([[12.0], [1.0]]) # 2 行 1 列,代表有 2 組,每組有 1 個輸入
validating_set_y = torch.tensor([[25.0], [3.0]]) # 2 行 1 列,代表有 2 組,每組有 1 個輸出
testing_set_x = torch.tensor([[9.0], [13.0]]) # 2 行 1 列,代表有 2 組,每組有 1 個輸入
testing_set_y = torch.tensor([[19.0], [27.0]]) # 2 行 1 列,代表有 2 組,每組有 1 個輸出

# 記錄 weight 與 bias 的歷史值
weight_history = [weight[0][0].item()]
bias_history = [bias.item()]

for epoch in range(1, 10000):
    print(f"epoch: {epoch}")

    # 根據訓練集訓練并修改引數

    # 計算預測值
    # 5 行 1 列的矩陣乘以 1 行 1 列的矩陣,會得出 5 行 1 列的矩陣
    predicted = traning_set_x.mm(weight) + bias
    # 計算損失
    loss = loss_function(predicted, traning_set_y)
    # 列印除錯資訊
    print(f"traning x: {traning_set_x}, y: {traning_set_y}, predicted: {predicted}, loss: {loss}, weight: {weight}, bias: {bias}")
    # 從損失自動微分求導函式值
    loss.backward()
    # 使用引數調整器調整引數
    optimizer.step()
    # 清空導函式值
    optimizer.zero_grad()
    # 記錄 weight 和 bias 的歷史值
    weight_history.append(weight[0][0].item())
    bias_history.append(bias.item())

    # 檢查驗證集
    with torch.no_grad(): # 禁止自動微分功能
        predicted = validating_set_x.mm(weight) + bias
        validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).mean()
    print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")

    # 如果驗證集正確率大于 99 %,則停止訓練
    print(f"validating accuracy: {validating_accuracy}")
    if validating_accuracy > 0.99:
        break

# 檢查測驗集
with torch.no_grad(): # 禁止自動微分功能
    predicted = testing_set_x.mm(weight) + bias
    testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")

# 顯示 weight 與 bias 的變化
from matplotlib import pyplot
pyplot.plot(weight_history, label="weight")
pyplot.plot(bias_history, label="bias")
pyplot.legend()
pyplot.show()

輸出如下:

epoch: 1
traning x: tensor([[2.],
        [5.],
        [6.],
        [7.],
        [8.]]), y: tensor([[ 5.],
        [11.],
        [13.],
        [15.],
        [17.]]), predicted: tensor([[2.],
        [5.],
        [6.],
        [7.],
        [8.]], grad_fn=<AddBackward0>), loss: 47.79999923706055, weight: tensor([[1.]], requires_grad=True), bias: 0.0
validating x: tensor([[12.],
        [ 1.]]), y: tensor([[25.],
        [ 3.]]), predicted: tensor([[22.0200],
        [ 1.9560]])
validating accuracy: 0.7663999795913696
epoch: 2
traning x: tensor([[2.],
        [5.],
        [6.],
        [7.],
        [8.]]), y: tensor([[ 5.],
        [11.],
        [13.],
        [15.],
        [17.]]), predicted: tensor([[ 3.7800],
        [ 9.2520],
        [11.0760],
        [12.9000],
        [14.7240]], grad_fn=<AddBackward0>), loss: 3.567171573638916, weight: tensor([[1.8240]], requires_grad=True), bias: 0.13199999928474426
validating x: tensor([[12.],
        [ 1.]]), y: tensor([[25.],
        [ 3.]]), predicted: tensor([[24.7274],
        [ 2.2156]])
validating accuracy: 0.8638148307800293

省略途中的輸出

epoch: 1103
traning x: tensor([[2.],
        [5.],
        [6.],
        [7.],
        [8.]]), y: tensor([[ 5.],
        [11.],
        [13.],
        [15.],
        [17.]]), predicted: tensor([[ 4.9567],
        [10.9867],
        [12.9966],
        [15.0066],
        [17.0166]], grad_fn=<AddBackward0>), loss: 0.0004764374461956322, weight: tensor([[2.0100]], requires_grad=True), bias: 0.936755359172821
validating x: tensor([[12.],
        [ 1.]]), y: tensor([[25.],
        [ 3.]]), predicted: tensor([[25.0564],
        [ 2.9469]])
validating accuracy: 0.99001544713974
testing x: tensor([[ 9.],
        [13.]]), y: tensor([[19.],
        [27.]]), predicted: tensor([[19.0265],
        [27.0664]])
testing accuracy: 0.998073160648346

嗯?這回怎么用了 1103 次才訓練成功?這是因為 weight 和 bias 調整的方向始終都是一致的,所以只用一個批次訓練反而會更慢,在之后的文章中,我們會用更多的引數 (神經元) 來訓練,而它們可以有不同的調整方向,所以不會出現這個例子中的問題,當然,業務上有的時候會出現因為引數調整方向全部一致導致訓練很慢,或者根本無法收斂的問題,這個時候我們可以通過更換模型,或者切分多個批次來解決,

劃分訓練集,驗證集和測驗集的例子

上面的例子定義訓練集,驗證集和測驗集的時候都是一個個 tensor 的定義,有沒有覺得很麻煩?我們可以通過 pytorch 提供的 tensor 操作來更方便的劃分它們:

# 原始資料集
>>> dataset = [(1, 3), (2, 5), (5, 11), (6, 13), (7, 15), (8, 17), (9, 19), (12, 25), (13, 27)]

# 轉換原始資料集到 tensor,并且指定數值型別為浮點數
>>> dataset_tensor = torch.tensor(dataset, dtype=torch.float32)
>>> dataset_tensor
tensor([[ 1.,  3.],
        [ 2.,  5.],
        [ 5., 11.],
        [ 6., 13.],
        [ 7., 15.],
        [ 8., 17.],
        [ 9., 19.],
        [12., 25.],
        [13., 27.]])

# 給亂數生成器分配一個初始值,使得每次運行都可以生成相同的亂數
# 這是為了讓訓練程序可重現,你也可以選擇不這樣做
>>> torch.random.manual_seed(0)
<torch._C.Generator object at 0x10cc03070>

# 生成隨機索引值, 用于打亂資料順序防止分布不均
>>> dataset_tensor.shape
torch.Size([9, 2])
>>> random_indices = torch.randperm(dataset_tensor.shape[0])
>>> random_indices
tensor([8, 0, 2, 3, 7, 1, 4, 5, 6])

# 計算訓練集,驗證集和測驗集的索引值串列
# 60 % 的資料劃分到訓練集,20 % 的資料劃分到驗證集,20 % 的資料劃分到測驗集
>>> traning_indices = random_indices[:int(len(random_indices)*0.6)]
>>> traning_indices
tensor([8, 0, 2, 3, 7])
>>> validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
>>> validating_indices
tensor([1, 4])
>>> testing_indices = random_indices[int(len(random_indices)*0.8):]
>>> testing_indices
tensor([5, 6])

# 劃分訓練集,驗證集和測驗集
>>> traning_set_x = dataset_tensor[traning_indices][:,:1] # 第一維度不截取,第二維度截取索引值小于 1 的元素
>>> traning_set_y = dataset_tensor[traning_indices][:,1:] # 第一維度不截取,第二維度截取索引值大于或等于 1 的元素
>>> traning_set_x
tensor([[13.],
        [ 1.],
        [ 5.],
        [ 6.],
        [12.]])
>>> traning_set_y
tensor([[27.],
        [ 3.],
        [11.],
        [13.],
        [25.]])
>>> validating_set_x = dataset_tensor[validating_indices][:,:1]
>>> validating_set_y = dataset_tensor[validating_indices][:,1:]
>>> validating_set_x
tensor([[2.],
        [7.]])
>>> validating_set_y
tensor([[ 5.],
        [15.]])
>>> testing_set_x = dataset_tensor[testing_indices][:,:1]
>>> testing_set_y = dataset_tensor[testing_indices][:,1:]
>>> testing_set_x
tensor([[8.],
        [9.]])
>>> testing_set_y
tensor([[17.],
        [19.]])

寫成代碼如下:

# 原始資料集
dataset = [(1, 3), (2, 5), (5, 11), (6, 13), (7, 15), (8, 17), (9, 19), (12, 25), (13, 27)]

# 轉換原始資料集到 tensor
dataset_tensor = torch.tensor(dataset, dtype=torch.float32)

# 給亂數生成器分配一個初始值,使得每次運行都可以生成相同的亂數
torch.random.manual_seed(0)

# 切分訓練集,驗證集和測驗集
random_indices = torch.randperm(dataset_tensor.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_tensor[traning_indices][:,:1]
traning_set_y = dataset_tensor[traning_indices][:,1:]
validating_set_x = dataset_tensor[validating_indices][:,:1]
validating_set_y = dataset_tensor[validating_indices][:,1:]
testing_set_x = dataset_tensor[testing_indices][:,:1]
testing_set_y = dataset_tensor[testing_indices][:,1:]

注意改變資料分布可以影響訓練速度,你可以試試上面的代碼經過多少次訓練可以訓練成功 (達到 99 % 的正確率),不過,資料越多越均勻,分布對訓練速度的影響就越少,

定義模型類 (torch.nn.Module)

如果我們想把自己寫好的模型提供給別人用,或者用別人寫好的模型,應該怎么辦呢?pytorch 提供了封裝模型的基礎類 torch.nn.Module,上面例子中的模型可以改寫如下:

# 參考 pytorch 和顯示圖表使用的 matplotlib
import torch
from matplotlib import pyplot

# 定義模型
# 模型需要定義 forward 函式接收輸入并回傳預測輸出
# add_history 和 show_history 是自定義函式,它們僅用于幫助我們理解機器學習的原理,實際不需要這樣做
class MyModle(torch.nn.Module):
    def __init__(self):
        # 初始化基類
        super().__init__()
        # 定義引數
        # 需要使用 torch.nn.Parameter 包裝,requires_grad 不需要設定 (會統一幫我們設定)
        self.weight = torch.nn.Parameter(torch.tensor([[1.0]]))
        self.bias = torch.nn.Parameter(torch.tensor(0.0))
        # 記錄 weight 與 bias 的歷史值
        self.weight_history = [self.weight[0][0].item()]
        self.bias_history = [self.bias.item()]

    def forward(self, x):
        # 計算預測值
        predicted = x.mm(self.weight) + self.bias
        return predicted

    def add_history(self):
        # 記錄 weight 和 bias 的歷史值
        self.weight_history.append(self.weight[0][0].item())
        self.bias_history.append(self.bias.item())

    def show_history(self):
        # 顯示 weight 與 bias 的變化
        pyplot.plot(self.weight_history, label="weight")
        pyplot.plot(self.bias_history, label="bias")
        pyplot.legend()
        pyplot.show()

# 創建模型實體
model = MyModle()

# 創建損失計算器
loss_function = torch.nn.MSELoss()

# 創建引數調整器
# 呼叫 parameters 函式可以自動遞回獲取模型中的引數串列 (注意是遞回獲取,嵌套模型也能支持)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 原始資料集
dataset = [(1, 3), (2, 5), (5, 11), (6, 13), (7, 15), (8, 17), (9, 19), (12, 25), (13, 27)]

# 轉換原始資料集到 tensor
dataset_tensor = torch.tensor(dataset, dtype=torch.float32)

# 給亂數生成器分配一個初始值,使得每次運行都可以生成相同的亂數
# 這是為了讓訓練程序可重現,你也可以選擇不這樣做
torch.random.manual_seed(0)

# 切分訓練集,驗證集和測驗集
random_indices = torch.randperm(dataset_tensor.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_tensor[traning_indices][:,:1]
traning_set_y = dataset_tensor[traning_indices][:,1:]
validating_set_x = dataset_tensor[validating_indices][:,:1]
validating_set_y = dataset_tensor[validating_indices][:,1:]
testing_set_x = dataset_tensor[testing_indices][:,:1]
testing_set_y = dataset_tensor[testing_indices][:,1:]

# 開始訓練程序
for epoch in range(1, 10000):
    print(f"epoch: {epoch}")

    # 根據訓練集訓練并修改引數
    # 切換模型到訓練模式,將會啟用自動微分,批次正規化 (BatchNorm) 與 Dropout
    model.train()

    # 計算預測值
    predicted = model(traning_set_x)
    # 計算損失
    loss = loss_function(predicted, traning_set_y)
    # 列印除錯資訊
    print(f"traning x: {traning_set_x}, y: {traning_set_y}, predicted: {predicted}, loss: {loss}, weight: {model.weight}, bias: {model.bias}")
    # 從損失自動微分求導函式值
    loss.backward()
    # 使用引數調整器調整引數
    optimizer.step()
    # 清空導函式值
    optimizer.zero_grad()
    # 記錄 weight 和 bias 的歷史值
    model.add_history()

    # 檢查驗證集
    # 切換模型到驗證模式,將會禁用自動微分,批次正規化 (BatchNorm) 與 Dropout
    model.eval()
    predicted = model(validating_set_x)
    validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).mean()
    print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")

    # 如果驗證集正確率大于 99 %,則停止訓練
    print(f"validating accuracy: {validating_accuracy}")
    if validating_accuracy > 0.99:
        break

# 檢查測驗集
predicted = model(testing_set_x)
testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")

# 顯示 weight 與 bias 的變化
model.show_history()

定義和使用模型類需要注意以下幾點:

  • 必須在建構式 __init__ 中呼叫 super().__init__() 初始化基類 (一般 python 繼承類也需要這樣做)
  • 必須定義 forward 函式接收輸入并回傳預測輸出
  • 模型中定義引數需要使用 torch.nn.Parameter 包裝,requires_grad 不需要設定 (會統一幫我們設定)
  • 呼叫 model.parameters() 可以遞回獲取引數串列 (支持嵌套模型),創建引數調整器時需要這個引數串列
  • 在訓練前呼叫 model.train() 開啟自動微分等功能
  • 在驗證或者使用訓練好的模型前呼叫 model.eval 關閉自動微分等功能

我們在后面繼續使用 pytorch 進行機器學習時,代碼的結構會基本和上面的例子一樣,只是模型和檢查驗證集測驗集的部分不同,此外,批次正規化與 Dropout 等功能會在后面的文章中介紹,

本篇就到此結束了,相信看到這里你已經掌握了用 pytorch 進行機器學習的基本模式??,

寫在最后

本篇介紹的東西也很基礎,但是這些基礎對應用機器學習必不可少,這兩篇的內容在很多機器學習教程中都沒有提到,它們直接就從多層線性模型開始講了,所以很多人會抱怨入門好難??,如果你看過 pytorch 出的官方書籍 《Deep Learning with Pytorch》可能會發現,這兩篇的介紹順序和這本書的介紹順序很接近,是的,寫這兩篇的時候我參考了這本書,按這個順序來理解是最容易的,

下一篇開始將會講解線性模型,激活函式和多層線性模型,并且會給出更接近實際的例子,但可能會需要更多時間,想看的耐心等等叭???,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/39011.html

標籤:其他

上一篇:計算機視覺影像預處理中的 Zero-mean(零均值化) 和 Normalization(歸一化)

下一篇:機器學習概述

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more