photo by seachaos @ Pleasanton, CA

手刻 Deep Learning -第壹章-PyTorch入門教學-基礎概念與再探線性迴歸

PyTorch 的 auto grad (導數) / 優化器 / 損失函數 概念說明與範例

--

前言

本章會需要 微分線性迴歸矩陣 的基本觀念

這次我們要來做 PyTorch 的簡單教學,我們先從簡單的計算與自動導數( auto grad / 微分 )開始,使用優化器與誤差計算,然後使用 PyTorch 做線性迴歸,還有 PyTorch 於 GPU 顯示卡( CUDA ) 的使用範例

本文的重點是學會 loss function 與 optimizer 使用

本文目錄:

  • 為什麼選擇 PyTorch?
  • 名詞與概念介紹 導數(partial derivative), 優化器(optimizer), 損失函數 ( loss function )
  • 自動導數 / Auto grad (手工優化與損失函數的實作)<- 此節很無聊
  • 優化器, 損失函數, 矩陣與 partial derivative 範例
  • 線性迴歸與矩陣範例 — Model 概念
  • GPU 顯示卡 (CUDA) 運算範例

本文不講解如何安裝 PyTorch ,且假設讀者已經擁有 Numpy 使用經驗
如果手邊沒有環境,可以使用
Google Colab ( Google 已經幫各位安裝好許多常用的套件 )

為什麼選擇 PyTorch?

Machine Learning / Deep Learning 有許多 Framework 可以使用,其中 Keras (Tensorflow) 與 PyTorch 本人都有使用經驗,但如果想要研究與瞭解 Deep Learning 如何運作,本人認為最好的方式是使用 PyTorch

一般使用 Keras (Tensorflow) 雖然可以快速的建立模型,但是底層原理沒有打好基礎的人一定會是霧裡看花

知其然,知其所以然

PyTorch 使用上很接近 Numpy,另外有強大的數學計算功能,例如:自動微分,自動優化,方便配置到顯卡上運算 (Nvidia CUDA,而且就本人使用經驗來說,安裝比 TensorFlow 容易 )… 如果知道 PyTorch 這些功能,可以使用在各種數學應用,而且對於 Machine Learning / Deep Learning 底層運作也會更加瞭如指掌

名詞與概念介紹 導數(partial derivative), 優化器(optimizer), 損失函數 ( loss function )

其實我們之前的章節,就是在為這些鋪路

損失函數 ( loss function )

這個名詞中文看來奇怪,其實他是計算數值差距用的,計算與答案的誤差,幫助我們找目標的公式,例如:
我們有 18 元去買 3塊錢的蘋果,我們只買了 5顆,我們是不是還可以買更多 ? ( 18–3*5=3,我們剩下 3元 ,誤差就是 3 )
線性迴歸 中就是計算和答案的差距,主要用來取得導數

導數 ( partial derivative )

就是 微分概念 ,透過損失函數還可以找出我們要移動的方向和距離

優化器 ( optimizer )

開始修正我們的數值使用,也有人稱最佳化器,就是依靠導數去調整數值

導數部分其實不用特別關心,他實務上隱藏在 loss function 與 optimizer 之間,但是這三者的組合就是 Machine Learning 中常用的概念
流程就是

誤差計算 -> 尋找導數 -> 優化 ->誤差計算 -> 尋找導數 -> 優化 ->誤差計算 -> 尋找導數 -> 優化 ->誤差計算 -> 尋找導數 -> 優化 ->…

自動導數 / Auto grad ( 手工優化與損失函數的實作 )

先說此節很無聊, 是介紹損失函數與優化器如何運作,但是可以跳到下節沒關係

本節承接 線性迴歸 概念,一樣依照 loss function 與 optimizer 架構

微分在 Machine Learning 中是非常重要的計算,而這種計算已經有公式可循,所以 PyTorch 會自動的幫我們計算(也就是我們之前章節說過,不會算沒關係,電腦可以幫我們算),但是概念與原理要懂

這邊的 grad 其實就是微積分中的 partial derivative (導數 / 斜率),PyTorch 會幫我們追蹤每個變數的 導數/斜率 ,即自動追蹤變數的任何變化

現在我們來看最基礎的 PyTorch 與 Auto grad 範例 :
假設今天在商場買蘋果結帳,假設蘋果 3 元,我們手上有 18 元,紙袋 不用錢(這邊為了方便範例先忽略) ,我們要去盡可能的買越多蘋果越好
公式如下:

結帳總額(Y) = 蘋果數量(a) * 蘋果單價(X) + 紙袋單價(b)
換成數學公式是 Y = aX + b

  • Y 是我們手上有的錢,這個我們不能改變
  • X 是蘋果單價,這個我們不能改變
  • a 是蘋果數量,這是我們要找出來的
  • b 這邊因為紙袋 0 元,所以我們直接省略不看
x = torch.tensor([3.0])  # 蘋果單價
y = torch.tensor([18.0]) # 我們的預算
a = torch.tensor([1.0], requires_grad=True) # 追蹤導數print('grad:', a.grad)
loss = y - (a * x) # loss function ( 中文稱 損失函數 )
loss.backward()
print('grad:', a.grad)

解說:

  • torch.tensor: 就是建立一個 Tensor (類似於 np.array )
  • requires_grad=True 我們有需要追蹤的變數才要加上這個參數,不用追蹤的不用,例如這個範例中我們只希望 a 改變,不要去動其他數值,所以只有 a 有 requires_grad=True

關於損失函數 ( loss function )

實務上是讓他越接近 0 越好,我們後面會套用 PyTorch 內建的 function ,就不用每次的手工打造

  • 上面程式碼中,loss 就是差價公式,概念就是 Y= ax + b,但是我們的目標是要把錢花光光,所以 loss 要越趨近於 0 越好
    所以數學上就成了 Y-(ax + b) = 0 是我們的目標
  • loss.backward() 就是和 PyTorch 説開始反向追蹤

這是 print 出來的結果

# grad: None
# grad: tensor([-3.])

可以看到我們 print(‘grad:’, a.grad) ,在 loss.backward 之前是沒有數值的
但是之後冒出一個 -3,這個 -3 就是 partial derivative (會微積分的人可以算看看 ),然後我們可以靠這個數值去調整 a 的值,如下

優化器

PyTorch 有內建好的優化器,但是我們這邊為了示範原理,一樣手工計算 ( 下節我們會使用內建的)
下面程式碼是我們開始進行線性迴歸優化 ( 計算 100 次 )

for _ in range(100):
a.grad.zero_()
loss = y - (a * x)
loss.backward()
with torch.no_grad():
a -= a.grad * 0.01 * loss

解說:

  • 每一次的 backward ,a 的 grad 都會相加,所以我們要先做歸零
    (就像是實驗室儀器每次都要先歸零校正 )
  • loss 與 loss.backward 上面已經解說過,用來反向追蹤導數
  • with torch.no_grad(): 這部分概念很重要,前面提過 PyTorch 會自動追蹤 a 的任何計算,所以我們手動調整 a 一定要和 PyTorch 説,不要追蹤我們的手動調整

我們看看跑 100 次迴歸的結果

損失函跑 100 次的變化
print('a:', a)
print('loss:', (y - (a * x)))
print('result:', (a * x))
# a: 5.999598503112793
# loss: 0.0012054443359375
# result: 17.998794555664062

可以看到 a 趨近於 6, 誤差 (loss ) 趨近於 0
表示我們真的可以用 18 塊錢去買 6 顆蘋果 3元的蘋果

優化器, 損失函數, 矩陣與 partial derivative 範例

這邊我們要用 PyTorch 內建的 loss function 與 optimizer 來做計算,會方便很多,而且可以做更多複雜的問題

本文的重點是學會 loss function 與 optimizer 使用

直接看程式碼(一樣是蘋果 3元範例 )

x = torch.tensor([3.0])
y = torch.tensor([18.0])
a = torch.tensor([1.0], requires_grad=True)
loss_func = torch.nn.MSELoss()
optimizer = torch.optim.SGD([a], lr=0.01)
for _ in range(100):
optimizer.zero_grad()
loss = loss_func(y, a * x)
loss.backward()
optimizer.step()

解說:

  • loss_func 就是損失函數,我們這邊使用 torch.nn.MSELoss
    他其實是計算 均方誤差,loss function 有很多種,我們日後有機會再介紹,但是這邊要知道很多情況下的數值比較其實用 MSELoss 就很好用了
    細節: https://pytorch.org/docs/stable/nn.html#loss-functions
  • optimizer 就是優化器
    torch.optim.SGD([a], lr=0.01) 這邊是我們使用 SGD ,一樣優化器有很多種,我們這邊為了簡單示範先用 SGD
    -> [a] 是我們和優化器說照顧好我們的 a 變數
    -> lr 是學習率, 數值都是小於 1,實際看場合調整,我們這邊 0.01 是為了快速示範
    細節 : https://pytorch.org/docs/stable/optim.html
  • optimizer.zero_grad 是為了歸零調整,因為每次 backward 都會增加 grad 的數值(就像是實驗室每次做實驗都要歸零調整,不然上個使用者的操作會干擾實驗結果 )
  • loss.backward() 做反向傳導,就是找出導數
  • optimizer.step 告訴優化器做優化,他會自動幫我們調整 a 的數值

我們測試一下矩陣概念的計算
假設一樣是 18 元的預算,三個種不同的蘋果
售價分別是 3 元, 5 元, 6元,這樣可以買幾顆 ?

我們只要修改一下數值

x = torch.tensor([3.0, 5.0, 6.0,])   # 不同種蘋果售價
y = torch.tensor([18.0, 18.0, 18.0]) # 我們的預算
a = torch.tensor([1.0, 1.0, 1.0], requires_grad=True) # 先假設都只能買一顆

然後我們跑計算 ( 這裡跑 1000 次,讓數值精準些 )

loss_func = torch.nn.MSELoss()
optimizer = torch.optim.SGD([a], lr=0.01)
for _ in range(1000):
optimizer.zero_grad()
loss = loss_func(y, a * x)
loss.backward()
optimizer.step()
print('a:', a)# a: tensor([6.0000, 3.6000, 3.0000], requires_grad=True)

看起來沒錯,如果只有 18 元,分別去買不同的蘋果
我們只能買 6 顆 ( 3元 ), 3 顆(5元), 3 顆(6元)

線性迴歸與矩陣範例 — Model 概念

有了上面的概念,現在我們來做更進階的範例

data -> model -> output

我們將輸入資料到輸出資料這個過程稱為 “Model” ,概念上就是可以當作是一個黑箱,我們把資料丟進去 (一般稱為 X ),就會產生出預測資料 ( 稱為 Y ),例如:

  • 影像分類:照片丟入 model ,然後 model 告訴我們影像的類別
  • 資料預測:過去幾天的氣象資料(溫度、濕度、氣壓.. . ) 丟入 model ,然後得到幾天後的預測
  • 語音辨識:使用者說話的音訊丟入 model ,得到文字輸出

準備資料

因為本文是在示範線性迴歸,我們先不探討複雜問題,先做最簡單的計算,所以這次我們使用 sklearn 來產生假資料練習
(打好基礎比較重要)

from sklearn.datasets import make_regression
np_x, np_y = make_regression(n_samples=500, n_features=10)
x = torch.from_numpy(np_x).float()
y = torch.from_numpy(np_y).float()

np_x 是產生出來的測試線性迴歸資料,有 10 個不同的屬性,500 筆資料
(可以使用 np_x.shape 看到 )
np_y 是產生出來的答案

因為, make_regression 產生出來的是 numpy,他不是 PyTorch 的 Tensor,所以我們要轉換一下 ( torch.from_numpy(… ).float() )

建立我們的 Model

w = torch.randn(10, requires_grad=True)
b = torch.randn(1, requires_grad=True)
optimizer = torch.optim.SGD([w, b], lr=0.01)
def model(x):
return x @ w + b

解說:

  • w:就是亂數產生的”權重”,我們會用矩陣計算將 x 的每個特徵相乘,記得我們上面產生的資料有 10 個特徵嗎?所以他是 torch.randn(10),而 requires_grad=True 就是和 PyTorch 說我們要優化這個數值
  • b:源自於 y=aX + b 公式,這次我們把 b 放進來了
  • torch.optim.SGD([w, b], lr=0.01) 就是優化器,和他說明我們要追蹤 w 和 b

再來 model function 就是我們這次的主角,基本精神就是 y=aX + b (這邊的 a 我們已經改用 w 替代 )

先預測

我們還沒有訓練我們的 model ,先看直接丟入 資料 (x) 會生出什麼

predict_y = model(x)

然後我們視覺化一下

plt.figure(figsize=(20, 10))
plt.subplot(1, 2, 1)
plt.plot(y)
plt.plot(predict_y.detach().numpy())
plt.subplot(1, 2, 2)
plt.scatter(y.detach().numpy(), predict_y.detach().numpy())
什麼都不訓練,直接預測

說明一下,左圖是資料範圍,理想上兩個顏色的線應該會看起來一致;右圖是資料預測與目標資料的分佈圖,理想上應該要是一條直線(左右到右上),看我們之後訓練過的 model 就可以知道差異

開始訓練

直接看程式

loss_func = torch.nn.MSELoss() # 之前提過的 loss function 
history = [] # 紀錄 loss(誤差/損失)的變化
for _ in range(300): # 訓練 300 次
predict_y = model(x)
loss = loss_func(predict_y, y)
# 優化與 backward 動作,之前介紹過
optimizer.zero_grad()
loss.backward()
optimizer.step()


history.append(loss.item())
plt.plot(history)
loss history

可以看到 loss 隨時間降低,這表示我們的 model 確實有學到東西,再來看看預測結果

predict_y = model(x)plt.figure(figsize=(20, 10))
plt.subplot(1, 2, 1)
plt.plot(y, label='target')
plt.plot(predict_y.detach().numpy(), label='predict')
plt.legend()
plt.subplot(1, 2, 2)
plt.scatter(y.detach().numpy(), predict_y.detach().numpy())
預測結果

如上所言,我們輸入 x 到 model ,可以得到很好的 y ( 預測與答案幾乎相似)

GPU 顯示卡 (CUDA) 運算範例

之前的其他文章提過, 顯示卡對於矩陣計算非常在行
而 PyTorch 對於使用顯卡計算很容易,如果讀者是 Nvidia 顯卡使用者,安裝 CUDA 相關套件即可(這部分就不在本文教學範圍了),而 AMD 使用者,聽說有 AMD ROCm 可以使用,不過本人沒有使用過也不清楚(本人手上都是老黃牌的顯卡)

我們要在 PyTorch 使用 cuda ,先檢測一下環境支不支援

torch.cuda.is_available()

看到結果是 true,表示應該(可能?或許?大概?)沒有問題 (因為實際上可能會有 CUDA 版本 / 驅動 各種奇怪狀況 )

再來就是我們把 device 抓出來

device = torch.device('cuda')    # cuda 即 nvidia gpu 計算
# device = torch.device('cpu') # 如果想回到 cpu 計算

再來,為了我們要示範 GPU 和 CPU 的差異,我們需要更多資料

from sklearn.datasets import make_regression
np_x, np_y = make_regression(n_samples=5000, n_features=5000)
x = torch.from_numpy(np_x).float().to(device)
y = torch.from_numpy(np_y).float().to(device)

為了要讓矩陣更大,所以我們這次增加到 5000 個特徵
然後注意上面的 to(device) 這就是把 PyTorch 的 Tensor 搬移到設備上面(我們已經指定成 cuda ),所以這些資料已經被移動到 GPU 上

只有位在相同的設備的 Tensor 才能互相計算,例如 gpu 只能算 gpu,cpu 只能算 cpu,所以計算之前要 .to(device) 到相同設備

現在我們建立 model (這個 model 我們要移動到 GPU 上 )

w = torch.randn(5000).to(device)
b = torch.randn(1).to(device)
w.requires_grad=True
b.requires_grad=True
optimizer = torch.optim.SGD([w, b], lr=0.01)
def model(x):
x = x @ w + b
return x

一樣的 to(device),然後 requires_grad 是因為移動到 GPU 上已經不同於 CPU,所以我們要另外設定要求追蹤導數 ( 優化器這些就一樣的程式碼)

再來跑訓練

loss_func = torch.nn.MSELoss()
history = []
ts = time.time()
for _ in range(1000): # 訓練 1000 次
predict_y = model(x)
loss = loss_func(predict_y, y)
# 優化與 backward 動作
optimizer.zero_grad()
loss.backward()
optimizer.step()

history.append(loss.item())
print(f'run_time: {time.time()-ts:.5f}')
plt.plot(history)

我們另外加了 time.time 來看執行時間

以下是本人的一些紀錄

  • AMD 3900X (CPU): 2.8 sec
  • Nvidia 3090(GPU) : 0.327 sec
  • Jetson Nano 4GB (CPU): 52.7 sec
  • Jetson Nano 4GB (GPU):16.47 sec

實務上如果是影像處理(CNN)或是隨 model 複雜度增加,時間差距會更大,另外也看到 GPU 的 RAM 被佔用

gpu ram 被使用

這也是為什麼 Deep Learning 會需要大記憶體顯卡的原因,當你越多的資料搬移到 GPU 就會需要更多空間

結語

本文的重點是學會 loss function 與 optimizer 使用

其實掌握正確的 loss function 與 optimizer,在 model 的設計上就會順利很多,這個我們日後有機會介紹會看到,另外本文其實很多觀念與細節礙於篇幅與時間沒有很詳細的交代

如果有興趣的讀者可以自行輸入程式碼與試驗不同的數值,去發現其中的細節變化,熟悉這些操作與概念對於 Machine Learning / Deep Learning 會很有幫助

本系列文的下一篇 激勵函數 連結如下:

--

--