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"。
將資料變成我們想要的樣子:數值型資料
錢錢沒有不見,只是變成我們想要的樣子。
將數值處理到我們希望的格式及範圍。
利用長條圖來觀察資料特性:長尾分佈。
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) 將非數值列(Sex
、Pclass
、Embarked
)轉換為虛擬變數,使它們適合用於機器學習模型。這會為每個類別創建一個新的二元列。
Pandas 可以使用 get_dummies
自動創建這些列,這也會刪除原始列。
我們將創建虛擬變量 Pclass
,即使它是數字,數字 1
、2
和3
對應於一等艙、二等艙和三等艙 - 而不是有意義的乘法計數或度量。
我們還將為 Sex
和創建虛擬變量 Embarked
,因為我們希望將它們用作模型中的預測變量。
另一方面,Cabin
、Name
、 和 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)
表示a
減b
。(然後取代原有值)
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_coeff
乘 n_hidden
的矩陣。
把係數除以 n_hidden
,這樣當我們在下一層中求和時,可以得到和開始時同樣幅度的數字。
然後我們的第二層,使用 n_hidden
當作輸入,並創建單一輸出。所以我們需要一個n_hidden
乘 1
的矩陣。
第二層還需要添加一個常數項。
本函數用於初始化神經網絡的權重。它初始化三個部分:第一層權重
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% 的準確率。
研究和工業中使用的 “真正的” 深度學習模型看起來與此非常相似。事實上,如果查看任何深度學習模型的原始碼,你會發現基本步驟是相同的。
實際模型與我們以上實作的最大區別是:
-
如何進行初始化和標準化,以確保模型每次都能正確訓練
-
正則化(避免過度擬合)
-
以該問題領域的知識,修改神經網絡
-
對較小的批次,而不是整個數據集,進行梯度下降。