fast.ai Lesson 5: Linear model and neural net from scratch

Jeremy 老師:本節課我們將「從頭開始」構建和訓練深度學習模型,不使用任何預構建的架構、優化器或數據載入框架等。

假設現在我們不是在上課,而是面對一個真實的問題。

如何從頭到尾,自己逐步訓練模型:整理資料、根據資料建立模型、梯度下降計算、訓練模型、提高預測準確、使用 S 函數(sigmoid function)、產出結果(提交 Kaggle)。

本節所練習的題目是:鐵達尼號乘客存活率預測

選擇這個題目是因為他的資料量很小,實作也簡單,很適合拿來當作從無到有創建模型的練習。


課程概要

老師推薦的預測筆記

參考資料


整理資料

檢查資料正確性

一開始利用 Pandas 讀入 csv 檔,重點在 檢查資料中是否有 NaN。

Pandas 的 isna() 可以檢查各欄位的值是否有效,如果無效就會回傳 true。

NaN: Not a Number 通常為無資料之意

isna(): is Not Available

import torch, numpy as np, pandas as pd
np.set_printoptions(linewidth=140)
torch.set_printoptions(linewidth=140, sci_mode=False, edgeitems=7)
pd.set_option('display.width', 140)

df = pd.read_csv(path/'train.csv')

df.isna().sum()

輸出:

PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64

課外補充:mode()

Pandas DataFrame mode() Method: 列出最常見的值(眾數)

import pandas as pd
data = [[1, 1, 2], [6, 4, 2], [4, 2, 1], [4, 2, 3]]
df = pd.DataFrame(data)

print(df)
print('==========')
print(df.mode())

輸出:

   0  1  2
0  1  1  2
1  6  4  2
2  4  2  1
3  4  2  3
==========
   0  1  2
0  4  2  2

觀察、研究資料

我們可以看看,df.mode() 後的前幾筆資料:

modes_test = df.mode()
modes_test

輸出(僅列出部分):

PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0.0 3.0 Abbing, Mr. Anthony male 24.0 0.0 0.0 1601 8.05 B96 B98 S
1 2 NaN NaN Abbott, Mr. Rossmore Edward NaN NaN NaN NaN 347082 NaN C23 C25 C27 NaN
2 3 NaN NaN Abbott, Mrs. Stanton (Rosa Hunt) NaN NaN NaN NaN CA. 2343 NaN G6 NaN
3 4 NaN NaN Abelson, Mr. Samuel NaN NaN NaN NaN NaN NaN NaN NaN

列出第一筆資料:

modes = df.mode().iloc[0]
modes
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0.0 3.0 Abbing, Mr. Anthony male 24.0 0.0 0.0 1601 8.05 B96 B98 S

輸出:

PassengerId                      1
Survived                       0.0
Pclass                         3.0
Name           Abbing, Mr. Anthony
Sex                           male
Age                           24.0
SibSp                          0.0
Parch                          0.0
Ticket                        1601
Fare                          8.05
Cabin                      B96 B98
Embarked                         S
Name: 0, dtype: object

老師推薦閱讀的 iloc 文章:

使用眾數取代 NaN

提醒:因為 inplace 設為 True,所以會取代掉資料集中的內容。

# 使用眾數取代 NaN
df.fillna(modes, inplace=True)

# 然後再看一次 NaN 的 count
df.isna().sum()

輸出:

PassengerId    0
Survived       0
Pclass         0
Name           0
Sex            0
Age            0
SibSp          0
Parch          0
Ticket         0
Fare           0
Cabin          0
Embarked       0
dtype: int64

使用 describe 取得常用統計數據

對 DataFrame 中的資料集進行描述性統計,顯示統計數據,如平均值、標準差等。

import numpy as np

df.describe(include=(np.number))

輸出:

PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 891.000000 891.000000 891.000000 891.000000 891.000000
mean 446.000000 0.383838 2.308642 28.566970 0.523008 0.381594 32.204208
std 257.353842 0.486592 0.836071 13.199572 1.102743 0.806057 49.693429
min 1.000000 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 223.500000 0.000000 2.000000 22.000000 0.000000 0.000000 7.910400
50% 446.000000 0.000000 3.000000 24.000000 0.000000 0.000000 14.454200
75% 668.500000 1.000000 3.000000 35.000000 1.000000 0.000000 31.000000
max 891.000000 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

老師推薦去 google 學習什麼是 histogram tutorial"。

將資料變成我們想要的樣子:數值型資料

:dollar: 錢錢沒有不見,只是變成我們想要的樣子。

將數值處理到我們希望的格式及範圍。

利用長條圖來觀察資料特性:長尾分佈

df['Fare'].hist();

長尾分佈是很常見的現象,處理方式也很簡單:取 log 值。

因為 Fare 資料集中有 0,而 0 取 log 值會變無限大。與其特別針對 0 作處理,不如把資料集中所有值通通先+1 後,再取 log 值。

df['LogFare'] = np.log(df['Fare']+1)

然後將取 log 值後的數據,再畫一次長條圖看看:有點變形的鐘形分佈。這就是我們要的。

接著看看其他資料:Pclass。

pclasses = sorted(df.Pclass.unique())
pclasses

# 輸出
[1, 2, 3]

將資料變成我們想要的樣子:非數值型資料

對於非數值型的資料,找方法用數字取代之。

對 DataFrame 中的非數值列(例如,字符串列)進行描述性統計,顯示統計數據,如唯一值數量、最常見的值等。

df.describe(include=[object])
# 比較一下前面數值型資料的語法:
# df.describe(include=(np.number))

輸出:

Name Sex Ticket Cabin Embarked
count 891 891 891 891 891
unique 891 2 681 147 3
top Braund, Mr. Owen Harris male 347082 B96 B98 S
freq 1 577 7 691 646

方法是建立新的列,將這些非數值型資料,轉成我們想要的數值型資料,並填入其值到新的列。

這些值不是 1 就是 0,例如: Sex'male' 時,其值就是 1,否則為 0。

使用獨熱編碼(One-Hot Encoding) 將非數值列(SexPclassEmbarked )轉換為虛擬變數,使它們適合用於機器學習模型。這會為每個類別創建一個新的二元列。

Pandas 可以使用 get_dummies 自動創建這些列,這也會刪除原始列。

我們將創建虛擬變量 Pclass ,即使它是數字,數字 123 對應於一等艙、二等艙和三等艙 - 而不是有意義的乘法計數或度量。

我們還將為 Sex 和創建虛擬變量 Embarked ,因為我們希望將它們用作模型中的預測變量。

另一方面,CabinName 、 和 Ticket 有太多唯一值,因此為它們創建虛擬變量沒有意義。

df = pd.get_dummies(df, columns=["Sex","Pclass","Embarked"])
df.columns

輸出:

Index(['PassengerId', 'Survived', 'Name', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'LogFare', 'Sex_female', 'Sex_male',
       'Pclass_1', 'Pclass_2', 'Pclass_3', 'Embarked_C', 'Embarked_Q', 'Embarked_S'],
      dtype='object')
原參數(列) 新參數(列)
Sex ‘Sex_female’, ‘Sex_male’
Pclass ‘Pclass_1’, ‘Pclass_2’, ‘Pclass_3’
Embarked ‘Embarked_C’, ‘Embarked_Q’, ‘Embarked_S’

這些新的列轉換,用於將類別變數(例如性別、艙位等級、登船港口)轉換為虛擬變數,以便在機器學習模型中使用。

每個原始列都被轉換成一個或多個二進制列(不是 1 就是 0),其中每個可能的值都有一個相對應的列,用1表示存在,0表示不存在。

這種轉換有助於模型處理類別變數,因為模型通常需要數值輸入,無法處理非數值資料。

參考資料:

將上述新參數(列) 製作成 list,顯示前面幾行來觀察 值的改變。

added_cols = ['Sex_male', 'Sex_female', 'Pclass_1', 'Pclass_2', 'Pclass_3', 'Embarked_C', 'Embarked_Q', 'Embarked_S']
df[added_cols].head()
Sex_male Sex_female Pclass_1 Pclass_2 Pclass_3 Embarked_C Embarked_Q Embarked_S
0 1 0 0 0 1 0 0 1
1 0 1 1 0 0 1 0 0
2 0 1 0 0 1 0 0 1
3 0 1 1 0 0 0 0 1
4 1 0 0 0 1 0 0 1

建一個名為 t_dep 的 PyTorch tensor(張量),其中包含了目標變數 Survived 的數據。

from torch import tensor

t_dep = tensor(df.Survived)

建立一個包含特徵列名的列表,其中包括年齡(Age)、兄弟姐妹/配偶(SibSp)、父母/子女(Parch)和轉換後的票價(LogFare),以及剛剛新增的虛擬變數列。

indep_cols = ['Age', 'SibSp', 'Parch', 'LogFare'] + added_cols

t_indep = tensor(df[indep_cols].values, dtype=torch.float)
t_indep

輸出:

tensor([[22.0000,  1.0000,  0.0000,  2.1102,  1.0000,  0.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000],
        [38.0000,  1.0000,  0.0000,  4.2806,  0.0000,  1.0000,  1.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000],
        [26.0000,  0.0000,  0.0000,  2.1889,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000],
        [35.0000,  1.0000,  0.0000,  3.9908,  0.0000,  1.0000,  1.0000,  0.0000,  0.0000,  0.0000,  0.0000,  1.0000],
        [35.0000,  0.0000,  0.0000,  2.2028,  1.0000,  0.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000],
        [24.0000,  0.0000,  0.0000,  2.2469,  1.0000,  0.0000,  0.0000,  0.0000,  1.0000,  0.0000,  1.0000,  0.0000],
        [54.0000,  0.0000,  0.0000,  3.9677,  1.0000,  0.0000,  1.0000,  0.0000,  0.0000,  0.0000,  0.0000,  1.0000],
        ...,
        [25.0000,  0.0000,  0.0000,  2.0857,  1.0000,  0.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000],
        [39.0000,  0.0000,  5.0000,  3.4054,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000,  0.0000,  1.0000,  0.0000],
        [27.0000,  0.0000,  0.0000,  2.6391,  1.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000,  0.0000,  1.0000],
        [19.0000,  0.0000,  0.0000,  3.4340,  0.0000,  1.0000,  1.0000,  0.0000,  0.0000,  0.0000,  0.0000,  1.0000],
        [24.0000,  1.0000,  2.0000,  3.1966,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000],
        [26.0000,  0.0000,  0.0000,  3.4340,  1.0000,  0.0000,  1.0000,  0.0000,  0.0000,  1.0000,  0.0000,  0.0000],
        [32.0000,  0.0000,  0.0000,  2.1691,  1.0000,  0.0000,  0.0000,  0.0000,  1.0000,  0.0000,  1.0000,  0.0000]])

檢查一下資料集大小(891 x 12)

t_indep.shape

# 輸出
torch.Size([891, 12])

根據資料建立模型

把資料集調整成我們想要的格式和數值後,接下來就是建立模型。

現在我們已經有了矩陣(自變量因變量向量),我們可以計算我們的 預測損失。在本節中,我們將手動執行一個步驟,來 計算每行數據的預測和損失

第一個模型:線性模型

我們需要為 t_indep 中的每一列提供一個係數,在 (-0.5,0.5) 範圍內選擇隨機數。

## 設置 PyTorch 的隨機種子,以確保結果的可重複性
torch.manual_seed(442)  

## 計算特徵數量,即獨立變數的數目(12)
n_coeff = t_indep.shape[1]  

## 創建一個包含隨機數的 PyTorch 張量,這些隨機數範圍在 -0.5 到 0.5 之間
## 這些數字將作為模型的權重。
coeffs = torch.rand(n_coeff)-0.5  

## 顯示查看結果
coeffs

輸出:

tensor([-0.4629,  0.1386,  0.2409, -0.2262, -0.2632, -0.3147,  0.4876,  0.3136,  0.2799, -0.4392,  0.2103,  0.3625])

我們的預測將每行乘以係數並將它們相加來計算。

這裡我們不需要單獨的常數項(也稱為“偏差”或“截距”項),也不需要一列所有 1 來給出具有常數項的相同效果。

因為我們的虛擬變量已經覆蓋了整個數據集。例如:有一個 “男性” 列和一個 “女性”列,並且數據集中的每個人都恰好屬於其中一個。

因此,我們不需要單獨的截距項來覆蓋不屬於列的行。

線性模型的計算步驟:對 t_indep (獨立變數)和 coeffs (權重)進行逐元素相乘,產生一個預測值。

t_indep*coeffs

輸出:

tensor([[-10.1838,   0.1386,   0.0000,  -0.4772,  -0.2632,  -0.0000,   0.0000,   0.0000,   0.2799,  -0.0000,   0.0000,   0.3625],
        [-17.5902,   0.1386,   0.0000,  -0.9681,  -0.0000,  -0.3147,   0.4876,   0.0000,   0.0000,  -0.4392,   0.0000,   0.0000],
        [-12.0354,   0.0000,   0.0000,  -0.4950,  -0.0000,  -0.3147,   0.0000,   0.0000,   0.2799,  -0.0000,   0.0000,   0.3625],
        [-16.2015,   0.1386,   0.0000,  -0.9025,  -0.0000,  -0.3147,   0.4876,   0.0000,   0.0000,  -0.0000,   0.0000,   0.3625],
        [-16.2015,   0.0000,   0.0000,  -0.4982,  -0.2632,  -0.0000,   0.0000,   0.0000,   0.2799,  -0.0000,   0.0000,   0.3625],
        [-11.1096,   0.0000,   0.0000,  -0.5081,  -0.2632,  -0.0000,   0.0000,   0.0000,   0.2799,  -0.0000,   0.2103,   0.0000],
        [-24.9966,   0.0000,   0.0000,  -0.8973,  -0.2632,  -0.0000,   0.4876,   0.0000,   0.0000,  -0.0000,   0.0000,   0.3625],
        ...,
        [-11.5725,   0.0000,   0.0000,  -0.4717,  -0.2632,  -0.0000,   0.0000,   0.0000,   0.2799,  -0.0000,   0.0000,   0.3625],
        [-18.0531,   0.0000,   1.2045,  -0.7701,  -0.0000,  -0.3147,   0.0000,   0.0000,   0.2799,  -0.0000,   0.2103,   0.0000],
        [-12.4983,   0.0000,   0.0000,  -0.5968,  -0.2632,  -0.0000,   0.0000,   0.3136,   0.0000,  -0.0000,   0.0000,   0.3625],
        [ -8.7951,   0.0000,   0.0000,  -0.7766,  -0.0000,  -0.3147,   0.4876,   0.0000,   0.0000,  -0.0000,   0.0000,   0.3625],
        [-11.1096,   0.1386,   0.4818,  -0.7229,  -0.0000,  -0.3147,   0.0000,   0.0000,   0.2799,  -0.0000,   0.0000,   0.3625],
        [-12.0354,   0.0000,   0.0000,  -0.7766,  -0.2632,  -0.0000,   0.4876,   0.0000,   0.0000,  -0.4392,   0.0000,   0.0000],
        [-14.8128,   0.0000,   0.0000,  -0.4905,  -0.2632,  -0.0000,   0.0000,   0.0000,   0.2799,  -0.0000,   0.2103,   0.0000]])

我把以上幾串資料,重新整理以下表格(僅放第一行),應該就可以一眼看清楚了。

  • t_indep:精心整理過的乘客數值(僅放第一行)。

  • coeffs: (-0.5,0.5) 範圍內的隨機數值。

Age SibSp Parch LogFare Sex_male Sex_female Pclass_1 Pclass_2 Pclass_3 Embarked_C Embarked_Q Embarked_S
t_indep 22.0000 1.0000 0.0000 2.1102 1.0000 0.0000 0.0000 0.0000 1.0000 0.0000 0.0000 1.0000
coeffs -0.4629 0.1386 0.2409 -0.2262 -0.2632 -0.3147 0.4876 0.3136 0.2799 -0.4392 0.2103 0.3625
相乘結果 -10.1838 0.1386 0.0000 -0.4772 -0.2632 -0.0000 0.0000 0.0000 0.2799 -0.0000 0.0000 0.3625

檢視資料,很明顯 Age 的值與其他相比太大,需要處理。

我們將 Age 的值,改為除以最大值,這樣它的值,就會在 0~1 之間。(這樣才不會某一列因為值太高,而影響了其權重)

vals,indices = t_indep.max(dim=0)
t_indep = t_indep / vals

然後再做一次。

t_indep*coeffs

輸出:

tensor([[-0.1273,  0.0173,  0.0000, -0.0765, -0.2632, -0.0000,  0.0000,  0.0000,  0.2799, -0.0000,  0.0000,  0.3625],
        [-0.2199,  0.0173,  0.0000, -0.1551, -0.0000, -0.3147,  0.4876,  0.0000,  0.0000, -0.4392,  0.0000,  0.0000],
        [-0.1504,  0.0000,  0.0000, -0.0793, -0.0000, -0.3147,  0.0000,  0.0000,  0.2799, -0.0000,  0.0000,  0.3625],
        [-0.2025,  0.0173,  0.0000, -0.1446, -0.0000, -0.3147,  0.4876,  0.0000,  0.0000, -0.0000,  0.0000,  0.3625],
        [-0.2025,  0.0000,  0.0000, -0.0798, -0.2632, -0.0000,  0.0000,  0.0000,  0.2799, -0.0000,  0.0000,  0.3625],
        [-0.1389,  0.0000,  0.0000, -0.0814, -0.2632, -0.0000,  0.0000,  0.0000,  0.2799, -0.0000,  0.2103,  0.0000],
        [-0.3125,  0.0000,  0.0000, -0.1438, -0.2632, -0.0000,  0.4876,  0.0000,  0.0000, -0.0000,  0.0000,  0.3625],
        ...,
        [-0.1447,  0.0000,  0.0000, -0.0756, -0.2632, -0.0000,  0.0000,  0.0000,  0.2799, -0.0000,  0.0000,  0.3625],
        [-0.2257,  0.0000,  0.2008, -0.1234, -0.0000, -0.3147,  0.0000,  0.0000,  0.2799, -0.0000,  0.2103,  0.0000],
        [-0.1562,  0.0000,  0.0000, -0.0956, -0.2632, -0.0000,  0.0000,  0.3136,  0.0000, -0.0000,  0.0000,  0.3625],
        [-0.1099,  0.0000,  0.0000, -0.1244, -0.0000, -0.3147,  0.4876,  0.0000,  0.0000, -0.0000,  0.0000,  0.3625],
        [-0.1389,  0.0173,  0.0803, -0.1158, -0.0000, -0.3147,  0.0000,  0.0000,  0.2799, -0.0000,  0.0000,  0.3625],
        [-0.1504,  0.0000,  0.0000, -0.1244, -0.2632, -0.0000,  0.4876,  0.0000,  0.0000, -0.4392,  0.0000,  0.0000],
        [-0.1852,  0.0000,  0.0000, -0.0786, -0.2632, -0.0000,  0.0000,  0.0000,  0.2799, -0.0000,  0.2103,  0.0000]])

提醒:不只是 Age 這樣做,而是 Age, SibSp, Parch & LogFare 全都這樣做。

其他我們新建的 dummy 參數,不是 1 就是 0,除以最大值的事對他們沒影響。

老師特別提醒,這一行程式超級酷:t_indep = t_indep / vals

這就是矩陣除以向量。

這裡的技巧是我們利用 numpy 和 PyTorch(以及許多其他語言,一直追溯到 APL)中稱為 broadcasting 的技術。

這就像矩陣的每一行都有一個單獨的向量副本一樣,因此它將矩陣的每一行除以向量。

在實務中,它並不製作任何副本,而是以高度優化的方式完成整個過程,充分利用現代 CPU(或 GPU,如果我們使用 GPU)。

broadcasting(廣播)是使程式碼簡潔、可維護且快速的最重要技術之一,非常值得學習和實踐。

現在,我們可以通過將乘積的行相加,從線性模型創建預測:

〉 計算預測值,將歸一化後的獨立變數 t_indep 與權重 coeffs 相乘,然後對每一行的結果進行求和,得到最終的預測結果。

preds = (t_indep*coeffs).sum(axis=1)

## 顯示前十筆資料看看
preds[:10]

輸出:

tensor([ 0.1927, -0.6239,  0.0979,  0.2056,  0.0968,  0.0066,  0.1306,  0.3476,  0.1613, -0.6285])

這些預測不會有任何用處,因為我們的係數是隨機的。但我們可以利用它們做為 梯度下降 的起點。

為了進行梯度下降,我們需要一個 損失函數。取 行的平均誤差(即預測與因變量之間的差的絕對值)通常是一種合理的方法。

損失函數的計算,用於評估模型的性能。

計算預測值與實際目標值之間的絕對誤差的平均值,並將其存儲在 loss 變數中。

loss = torch.abs(preds - t_dep).mean()
loss

輸出:

tensor(0.5382)

將前面的推斷公式,整理成 function:

calc_preds :計算預測值,接受權重 coeffs 和獨立變數 indeps 作為參數,並返回預測結果。

calc_loss:計算損失,接受權重 coeffs 、獨立變數 indeps 和目標變數 deps 作為參數,並返回平均絕對誤差(MAE)的值,這也是模型的損失函數。

def calc_preds(coeffs, indeps): 
    return (indeps*coeffs).sum(axis=1)

def calc_loss(coeffs, indeps, deps): 
    return torch.abs(calc_preds(coeffs, indeps)-deps).mean()

實作梯度下降( gradient descent)

啟用 coeffs 張量的梯度追蹤,使其能夠計算梯度。(請看輸出最後一個欄位)

coeffs.requires_grad_()

輸出:

tensor([-0.4629,  0.1386,  0.2409, -0.2262, -0.2632, -0.3147,  0.4876,  0.3136,  0.2799, -0.4392,  0.2103,  0.3625], requires_grad=True)

計算當前權重 coeffs (之前亂數產生的)下的模型損失,並將其存儲在 loss 變數中。

loss = calc_loss(coeffs, t_indep, t_dep)
loss

輸出:(除了和前面相同的 0.5382 外,還有一個 grad_fn function)

tensor(0.5382, grad_fn=<MeanBackward0>)

使用 PyTorch backward() 計算梯度:

backward():根據損失函數的梯度,計算 coeffs 張量的梯度。

這個步驟是反向傳播(Backpropagation)的一部分,用於計算模型權重的更新方向。

loss.backward()

檢查 coeffs 張量的梯度,顯示當前權重下的梯度值。

coeffs.grad

輸出:

tensor([-0.0106,  0.0129, -0.0041, -0.0484,  0.2099, -0.2132, -0.1212, -0.0247,  0.1425, -0.1886, -0.0191,  0.2043])

每次我們呼叫 backward() 計算梯度,梯度都會被加到 .grad 屬性中。

例如:我們把上述三步驟再執行一次。

loss = calc_loss(coeffs, t_indep, t_dep)
loss.backward()
coeffs.grad

輸出:就會發現所有的值都變兩倍。

tensor([-0.0212,  0.0258, -0.0082, -0.0969,  0.4198, -0.4265, -0.2424, -0.0494,  0.2851, -0.3771, -0.0382,  0.4085])

所以我們每次呼叫後,必須先歸 0(coeffs.grad.zero_())。

loss = calc_loss(coeffs, t_indep, t_dep)
loss.backward()

___

## 進入一個不計算梯度的程式段落,這表示以下的操作不會影響模型權重的梯度計算。
with torch.no_grad():
    ## 使用梯度下降法更新權重 coeffs。這是一個常見的權重更新步驟。
    ## 將當前的權重減去學習率(0.1)乘以梯度的值,以向損失函數最小化的方向調整權重。
    coeffs.sub_(coeffs.grad * 0.1)

    ## 將 coeffs 張量的梯度歸零,以準備進行下一次梯度計算。
    coeffs.grad.zero_()

    ## 列印更新後的權重 coeffs 下的新損失,以利觀察
    print(calc_loss(coeffs, t_indep, t_dep))

輸出:

tensor(0.4945)  ## 原為 0.5382

程式補充:

在 PyTorch 中,結尾是 _ 的 method,表示計算後的值,會取代原有值。

a.sub_(b) 表示 ab 。(然後取代原有值)

a.zero_() 同理,這表示將值歸 0。(然後取代原有值)


訓練模型

開始訓練模型前,要準備一組驗證集。

老方法,我們將原有資料集拆為兩份,一組訓練集、一組驗證集。(使用 fastai 庫中的 RandomSplitter 函數)

from fastai.data.transforms import RandomSplitter

trn_split,val_split=RandomSplitter(seed=42)(df)

上述的訓練集和驗證集(trn_split,val_split),運用在實際資料中。

## 從獨立變數 t_indep 中,拆份訓練集和驗證集
trn_indep,val_indep = t_indep[trn_split],t_indep[val_split]

## 從目標變數 t_dep 中,拆份訓練集和驗證集
trn_dep,val_dep = t_dep[trn_split],t_dep[val_split]

## 查看訓練集和驗證集的長度(樣本數量)各為多少
len(trn_indep),len(val_indep)

輸出:

(713, 178)

update_coeffs 用於根據梯度下降法更新權重 coeffs

這個函數將權重減去學習率 lr 乘以梯度的值,然後將梯度歸零,以準備下一次梯度計算。

def update_coeffs(coeffs, lr):
    coeffs.sub_(coeffs.grad * lr)
    coeffs.grad.zero_()

one_epoch 代表一個訓練周期。

在每個訓練周期內,計算訓練集上的損失,然後根據梯度下降法更新權重,最後列印出損失的值當參考。

def one_epoch(coeffs, lr):
    loss = calc_loss(coeffs, trn_indep, trn_dep)
    loss.backward()
    with torch.no_grad(): update_coeffs(coeffs, lr)
    print(f"{loss:.3f}", end="; ")

init_coeffs 用於初始化權重 coeffs

它產生一個包含隨機數的 PyTorch 張量,並啟用梯度追蹤。

def init_coeffs(): 
    return (torch.rand(n_coeff)-0.5).requires_grad_()

train_model 訓練模型:接受訓練的周期數 epochs 和學習率 lr 作為參數。

首先初始化權重 coeffs ,然後進行指定數量的訓練周期,每個周期使用 one_epoch 函數進行訓練和權重更新,最終返回訓練完成後的權重。

def train_model(epochs=30, lr=0.01):
    torch.manual_seed(442)
    coeffs = init_coeffs()
    for i in range(epochs): 
        one_epoch(coeffs, lr=lr)
    return coeffs

實際來試試看吧!

coeffs = train_model(18, lr=0.2)

輸出:感覺還不錯,做 18次,損失從 0.536 降到 0.289。

0.536; 0.502; 0.477; 0.454; 0.431; 0.409; 0.388; 0.367; 0.349; 0.336; 0.330; 0.326; 0.329; 0.304; 0.314; 0.296; 0.300; 0.289; 

show_coeffs 將權重和對應的特徵列名稱組合成一個字典,並返回該字典。

該函數將權重的梯度追蹤關閉,以防止對其進行梯度計算。

def show_coeffs(): 
    return dict(zip(indep_cols, coeffs.requires_grad_(False)))

show_coeffs()

輸出:

{'Age': tensor(-0.2694),
 'SibSp': tensor(0.0901),
 'Parch': tensor(0.2359),
 'LogFare': tensor(0.0280),
 'Sex_male': tensor(-0.3990),
 'Sex_female': tensor(0.2345),
 'Pclass_1': tensor(0.7232),
 'Pclass_2': tensor(0.4112),
 'Pclass_3': tensor(0.3601),
 'Embarked_C': tensor(0.0955),
 'Embarked_Q': tensor(0.2395),
 'Embarked_S': tensor(0.2122)}

提高預測準確度

Kaggle 競賽並不是通過絕對誤差(我們的損失函數)來評分,它是根據 準確性 評分,也就是我們正確預測存活的比例。

簡單的說,就是用驗證集來調整參數,提高預測存活的比例。

preds = calc_preds(coeffs, val_indep)

當預測預測存活比率大於 0.5時(preds > 0.5),表示預測結果為存活,反之則是死亡。

所以我們可以用驗證集資料來預測每個人是否存活,然後和實際結果作比對。

正確預測存活的比例,就是正確預測的總數,除以驗證集資料總數。

results = val_dep.bool()==(preds>0.5)
results[:16]

輸出:

tensor([ True,  True,  True,  True,  True,  True,  True,  True,  True,  True, False, False, False,  True,  True, False])
results.float().mean()

輸出:

tensor(0.7865)

寫個函數來專門計算預測正確比例。

def acc(coeffs): 
    return (val_dep.bool()==(calc_preds(coeffs, val_indep)>0.5)).float().mean()

acc(coeffs)

輸出:

tensor(0.7865)

使用 S 函數提高預測正確率

S 函數(sigmoid function)

我們之前的預測,有個明顯的問題:對存活機率的預測,有些 >1 ,也有些 <0,這顯然是要修正的。

顯示模型對前 28 個樣本的預測結果。

preds[:28]

輸出:

tensor([ 0.8160,  0.1295, -0.0148,  0.1831,  0.1520,  0.1350,  0.7279,  0.7754,  0.3222,  0.6740,  0.0753,  0.0389,  0.2216,  0.7631,
         0.0678,  0.3997,  0.3324,  0.8278,  0.1078,  0.7126,  0.1023,  0.3627,  0.9937,  0.8050,  0.1153,  0.1455,  0.8652,  0.3425])

我們使用 S 函數(sigmoid function)來解決這個問題。

繪製一個 S 形曲線圖,表示邏輯函數(Logistic Function),其公式為 1 / (1 + exp(-x))。

這個函數通常用於二元分類問題中,將連續的輸入映射到 0 到 1 之間,用於計算概率。

以下函數將最小值設為零,最大值為一。

import sympy

sympy.plot("1/(1+exp(-x))", xlim=(-5, 5));

本處繪圖略

因為 S 函數很常用,PyTorch 已經預先定義了該函數(torch.sigmoid)。

我們將之前的 calc_preds 改寫,使用 torch.sigmoid 將線性組合的結果映射到 0 到 1 之間。:

def calc_preds(coeffs, indeps): 
    return torch.sigmoid((indeps*coeffs).sum(axis=1))

現在用這個更新後的函數來計算預測,訓練一個新模型:

重新訓練模型,但這次使用更高的學習率(lr=100)。看看使用不同學習率訓練模型會有什麼效果。

coeffs = train_model(lr=100)

輸出:感覺 loss 更低

0.510; 0.327; 0.294; 0.207; 0.201; 0.199; 0.198; 0.197; 0.196; 0.196; 0.196; 0.195; 0.195; 0.195; 0.195; 0.195; 0.195; 0.195; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 

使用剛剛的函數來計算預測正確比例:

acc(coeffs)

輸出:也是比較好

tensor(0.8258)
show_coeffs()

輸出:蠻合理。老年人和男生存活機會低些,而頭等艙乘客存活機會高些。

{'Age': tensor(-1.5061),
 'SibSp': tensor(-1.1575),
 'Parch': tensor(-0.4267),
 'LogFare': tensor(0.2543),
 'Sex_male': tensor(-10.3320),
 'Sex_female': tensor(8.4185),
 'Pclass_1': tensor(3.8389),
 'Pclass_2': tensor(2.1398),
 'Pclass_3': tensor(-6.2331),
 'Embarked_C': tensor(1.4771),
 'Embarked_Q': tensor(2.1168),
 'Embarked_S': tensor(-4.7958)}

產出結果(提交 Kaggle)

將資料填入一開始的 csv 檔,其中的專有欄位。

首先讀取 csv 檔。

tst_df = pd.read_csv(path/'test.csv')

對測試集中的票價(Fare)列進行處理,將缺失值(NaN)填充為 0

tst_df['Fare'] = tst_df.Fare.fillna(0)

將訓練集執行的相同步驟,在測試集上執行來預處理數據:

## 使用訓練數據集中的眾數 modes 對測試集中的缺失值進行填充。
## 這確保了測試數據的處理方式與訓練數據一致。
tst_df.fillna(modes, inplace=True)

## 對測試集中的票價(Fare)列進行對數轉換,並將結果存儲在新的 LogFare 列中
tst_df['LogFare'] = np.log(tst_df['Fare']+1)

## 使用獨熱編碼對測試集中的非數值列(Sex、Pclass、Embarked)進行轉換
tst_df = pd.get_dummies(tst_df, columns=["Sex","Pclass","Embarked"])

## 從測試數據中選擇獨立變數列並轉換為 PyTorch 張量
tst_indep = tensor(tst_df[indep_cols].values, dtype=torch.float)

## 對測試數據中的獨立變數進行歸一化處理
tst_indep = tst_indep / vals

使用訓練好的模型權重 coeffs 對測試數據進行預測,並將預測結果存儲在新的 Survived 列中。

預測值大於0.5的被視為生存(Survived=1),否則被視為未生存(Survived=0)。

tst_df['Survived'] = (calc_preds(tst_indep, coeffs)>0.5).int()

選擇測試數據中的乘客ID(PassengerId)和預測的生存狀態(Survived)列,存在 sub_df 的 DataFrame 中。

sub_df = tst_df[['PassengerId','Survived']]
sub_df.to_csv('sub.csv', index=False)

檢視前幾行的數據:

!head sub.csv

輸出:

PassengerId,Survived
892,0
893,0
894,0
895,0
896,0
897,0
898,1
899,0
900,1

在 Kaggle 中點擊 “保存版本”並等待筆記本運行時,會看到 sub.csv 出現在 “數據” 選項卡中。

點擊該文件將顯示 提交 按鈕,點擊即可提交參賽作品。


使用矩陣乘積

計算使用權重 coeffs 預測的結果。這是將驗證集的獨立變數 val_indep 逐元素與權重 coeffs 相乘,然後對每一行的結果進行求和,得到最終的預測值。

(val_indep*coeffs).sum(axis=1)

輸出:

tensor([ 12.3288, -14.8119, -15.4540, -13.1513, -13.3512, -13.6469,   3.6248,   5.3429, -22.0878,   3.1233, -21.8742, -15.6421, -21.5504,
          3.9393, -21.9190, -12.0010, -12.3775,   5.3550, -13.5880,  -3.1015, -21.7237, -12.2081,  12.9767,   4.7427, -21.6525, -14.9135,
         -2.7433, -12.3210, -21.5886,   3.9387,   5.3890,  -3.6196, -21.6296, -21.8454,  12.2159,  -3.2275, -12.0289,  13.4560, -21.7230,
         -3.1366, -13.2462, -21.7230, -13.6831,  13.3092, -21.6477,  -3.5868, -21.6854, -21.8316, -14.8158,  -2.9386,  -5.3103, -22.2384,
        -22.1097, -21.7466, -13.3780, -13.4909, -14.8119, -22.0690, -21.6666, -21.7818,  -5.4439, -21.7407, -12.6551, -21.6671,   4.9238,
        -11.5777, -13.3323, -21.9638, -15.3030,   5.0243, -21.7614,   3.1820, -13.4721, -21.7170, -11.6066, -21.5737, -21.7230, -11.9652,
        -13.2382, -13.7599, -13.2170,  13.1347, -21.7049, -21.7268,   4.9207,  -7.3198,  -5.3081,   7.1065,  11.4948, -13.3135, -21.8723,
        -21.7230,  13.3603, -15.5670,   3.4105,  -7.2857, -13.7197,   3.6909,   3.9763, -14.7227, -21.8268,   3.9387, -21.8743, -21.8367,
        -11.8518, -13.6712, -21.8299,   4.9440,  -5.4471, -21.9666,   5.1333,  -3.2187, -11.6008,  13.7920, -21.7230,  12.6369,  -3.7268,
        -14.8119, -22.0637,  12.9468, -22.1610,  -6.1827, -14.8119,  -3.2838, -15.4540, -11.6950,  -2.9926,  -3.0110, -21.5664, -13.8268,
          7.3426, -21.8418,   5.0744,   5.2582,  13.3415, -21.6289, -13.9898, -21.8112,  -7.3316,   5.2296, -13.4453,  12.7891, -22.1235,
        -14.9625,  -3.4339,   6.3089, -21.9839,   3.1968,   7.2400,   2.8558,  -3.1187,   3.7965,   5.4667, -15.1101, -15.0597, -22.9391,
        -21.7230,  -3.0346, -13.5206, -21.7011,  13.4425,  -7.2690, -21.8335, -12.0582,  13.0489,   6.7993,   5.2160,   5.0794, -12.6957,
        -12.1838,  -3.0873, -21.6070,   7.0744, -21.7170, -22.1001,   6.8159, -11.6002, -21.6310])

這是使用矩陣乘法符號 @ 來計算預測的另一種方式。

它表示將 val_indep 矩陣與 coeffs 張量相乘,會得到相同的預測結果。

val_indep@coeffs

重新定義 calc_preds 函數,這次使用矩陣乘法。

它計算模型的預測結果,並將結果通過 sigmoid 函數進行轉換。

def calc_preds(coeffs, indeps): 
    return torch.sigmoid(indeps@coeffs)

重新定義了 init_coeffs 函數,這次初始化權重 coeffs 為一個列向量,而不是行向量。

每個權重值都乘以 0.1,並啟用梯度追蹤。

為了進行矩陣-矩陣乘積(我們將在下一節中需要),我們需要轉換coeffs 為列向量(即具有單列的矩陣),我們可以通過將第二個參數傳遞1 給 來實現torch.rand() ,表示我們希望我們的係數只有一列:

def init_coeffs(): return (torch.rand(n_coeff, 1)*0.1).requires_grad_()

將因變量轉換為列向量,我們可以通過使用特殊值 索引列維度來完成此操作,None 該值告訴 PyTorch 在該位置添加新維度:

將訓練集和驗證集的目標變數轉換為列向量,以適應一些後續的矩陣運算。這樣做是為了確保矩陣乘法的維度相容性。

trn_dep = trn_dep[:,None]
val_dep = val_dep[:,None]
coeffs = train_model(lr=100)

輸出:

0.512; 0.323; 0.290; 0.205; 0.200; 0.198; 0.197; 0.197; 0.196; 0.196; 0.196; 0.195; 0.195; 0.195; 0.195; 0.195; 0.195; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 0.194; 
acc(coeffs)

輸出:

tensor(0.8258)  ## 一樣的數字

神經網絡

現在我們已經有了實現神經網絡所需的東西。

首先,我們需要為每一層創建係數。

第一組係數使用 n_coeff 當作輸入,並創建輸出 n_hidden

我們可以選擇任何 n_hidden:數字愈高彈性愈大,但速度會變慢、更難訓練。

所以我們需要一個大小為 n_coeffn_hidden 的矩陣。

把係數除以 n_hidden ,這樣當我們在下一層中求和時,可以得到和開始時同樣幅度的數字。

然後我們的第二層,使用 n_hidden 當作輸入,並創建單一輸出。所以我們需要一個n_hidden1 的矩陣。

第二層還需要添加一個常數項。

本函數用於初始化神經網絡的權重。它初始化三個部分:第一層權重 layer1 、第二層權重 layer2 ,以及一個常數 const 。所有這些都啟用了梯度追蹤。

還有一個參數 n_hidden ,表示隱藏層的大小。

def init_coeffs(n_hidden=20):
    layer1 = (torch.rand(n_coeff, n_hidden)-0.5)/n_hidden
    layer2 = torch.rand(n_hidden, 1)-0.3
    const = torch.rand(1)[0]
    return layer1.requires_grad_(),layer2.requires_grad_(),const.requires_grad_()
import torch.nn.functional as F

## 參數:接受三個部分的權重 coeffs(l1、l2 和 const)和獨立變數 indeps。
def calc_preds(coeffs, indeps):
    ## 首先將獨立變數 indeps 通過第一層權重 l1 進行線性變換,然後使用 ReLU 激活函數。
    l1,l2,const = coeffs
    res = F.relu(indeps@l1)

    ## 接著將結果通過第二層權重 l2 進行線性變換,再加上常數 const。
    res = res@l2 + const

    ## 最後,使用 sigmoid 函數將結果映射到 0 到 1 之間,以獲得最終的預測結果。
    return torch.sigmoid(res)

用於更新神經網絡權重的函數

它接受權重 coeffs 和學習率 lr ,然後遍歷所有的權重層,使用梯度下降法來更新權重。

def update_coeffs(coeffs, lr):
    for layer in coeffs:
        layer.sub_(layer.grad * lr)
        layer.grad.zero_()

使用學習率 1.4 重新訓練神經網絡模型。

這是用來調整模型的超參數,以觀察不同學習率下的模型效能。

coeffs = train_model(lr=1.4)

輸出:

0.543; 0.532; 0.520; 0.505; 0.487; 0.466; 0.439; 0.407; 0.373; 0.343; 0.319; 0.301; 0.286; 0.274; 0.264; 0.256; 0.250; 0.245; 0.240; 0.237; 0.234; 0.231; 0.229; 0.227; 0.226; 0.224; 0.223; 0.222; 0.221; 0.220; 

再次使用不同的學習率(lr=20)重新訓練模型,以進行進一步的超參數調整。

coeffs = train_model(lr=20)

輸出:

0.543; 0.400; 0.260; 0.390; 0.221; 0.211; 0.197; 0.195; 0.193; 0.193; 0.193; 0.193; 0.193; 0.193; 0.193; 0.193; 0.193; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 0.192; 
acc(coeffs)

輸出:

tensor(0.8258)

結論:本例中,神經網絡並沒有比線性模型更好。

這並不奇怪,這個數據集非常小而且非常簡單,並不是我們期望看到神經網絡擅長的事情。

此外,我們的驗證集太小,無法可靠地看到很大的準確性差異。

但關鍵是我們現在確切地知道真正的神經網絡是什麼樣子!


深度學習

上一節中的神經網絡僅使用一個隱藏層,因此它不算“深度”學習。但是我們可以使用同樣的技術,添加更多的矩陣乘法來使我們的神經網絡變得更深。

首先,我們需要為每一層創建額外的係數:

重新定義了 init_coeffs 函數,這次允許配置多個隱藏層。

在這個函數中,你可以設定 hiddens 列表,其中包含每個隱藏層的大小。

函數會自動計算出各層的大小,然後初始化相應的權重矩陣和常數。這個函數的返回值包括多個層的權重和常數。

def init_coeffs():
    hiddens = [10, 10]  # <-- set this to the size of each hidden layer you want
    sizes = [n_coeff] + hiddens + [1]
    n = len(sizes)
    layers = [(torch.rand(sizes[i], sizes[i+1])-0.3)/sizes[i+1]*4 for i in range(n-1)]
    consts = [(torch.rand(1)[0]-0.5)*0.1 for i in range(n-1)]
    for l in layers+consts: l.requires_grad_()
    return layers,consts

重新定義 calc_preds 函數,這次可以處理多層神經網絡。

函數接受 coeffs ,其中包含了多個層的權重和常數。

它遍歷每一層,進行線性變換和激活操作(使用 ReLU 函數),最後使用 sigmoid 函數映射到0到1之間,獲得最終的預測結果。

這裡有很多混亂的常量來使隨機數處於正確的範圍內。

訓練模型時,你會發現這些初始化的最微小的變化,都可能導致我們的模型根本無法訓練!

這是深度學習在早期未能取得太大進展的一個關鍵原因:為我們的係數找到一個好的起點是非常挑剔的。

如今,我們有辦法解決這個問題,之後將在其他筆記本中了解這些方法。

import torch.nn.functional as F

def calc_preds(coeffs, indeps):
    layers,consts = coeffs
    n = len(layers)
    res = indeps
    for i,l in enumerate(layers):
        res = res@l + consts[i]
        if i!=n-1: res = F.relu(res)
    return torch.sigmoid(res)

重新定義 update_coeffs 函數,這次可以處理多層神經網絡。

函數接受 coeffs ,其中包含了多個層的權重和常數。

它遍歷每一層的權重和常數,使用梯度下降法來更新它們。

def update_coeffs(coeffs, lr):
    layers,consts = coeffs
    for layer in layers+consts:
        layer.sub_(layer.grad * lr)
        layer.grad.zero_()
coeffs = train_model(lr=4)

輸出:

0.521; 0.483; 0.427; 0.379; 0.379; 0.379; 0.379; 0.378; 0.378; 0.378; 0.378; 0.378; 0.378; 0.378; 0.378; 0.378; 0.377; 0.376; 0.371; 0.333; 0.239; 0.224; 0.208; 0.204; 0.203; 0.203; 0.207; 0.197; 0.196; 0.195; 
acc(coeffs)

輸出:

tensor(0.8258)

結語

我們已經成功地從頭開始創建了一個真正的深度學習模型,並對其進行了訓練,使其在該任務上獲得了超過 80% 的準確率。

研究和工業中使用的 “真正的” 深度學習模型看起來與此非常相似。事實上,如果查看任何深度學習模型的原始碼,你會發現基本步驟是相同的​​。

實際模型與我們以上實作的最大區別是:

  • 如何進行初始化和標準化,以確保模型每次都能正確訓練

  • 正則化(避免過度擬合)

  • 以該問題領域的知識,修改神經網絡

  • 對較小的批次,而不是整個數據集,進行梯度下降。


觀看課程影片