機器學習入門¶
我們先載入這個章節範例程式碼中會使用到的第三方套件、模組或者其中的部分類別、函式。
[1]:
from pyvizml import CreateNBAData
import numpy as np
import requests
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_boston
from sklearn.datasets import fetch_california_housing
from sklearn.datasets import make_classification
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import StandardScaler
關於 Scikit-Learn¶
Scikit-Learn 是 Python 使用者入門機器學習的一個高階、設計成熟且友善的套件模組,其建構於 NumPy、SciPy 與 Matplotlib,是開源並可作為商業使用的套件模組,主要的撰寫程式語言是 Python,並在其中廣泛使用 NumPy 進行線性代數、陣列運算。此外,也有運用 Cython 撰寫了部分核心演算法提高運算的效能。Scikit-learn 與我們已經介紹過的套件模組諸如 NumPy 以及 Matplotlib 能夠產生非常良好的綜效,其應用場景可以被簡單分類為:
預處理(Preprocessing)
監督式學習(Supervised learning)
分類(Classification)
迴歸(Regression)
非監督式學習(Unsupervised learning)
分群(Clustering)
降維(Dimensionality reduction)
模型選擇(Model selection)
預處理的功能呼應了資料科學專案中的整併以及轉換;監督式學習、非監督式學習與模型選擇的功能則呼應了專案中的預測。
為何 Scikit-Learn¶
Scikit-Learn 設計對於使用者非常友善,在開發上圍繞著五個核心理念打造:
一致性(Consistency)
檢查性(Inspection)
不自行創建類別(Nonproliferation of classes)
模組化(Composition)
提供合理的預設參數(Sensible defaults)
其中,一致性指的是 Scikit-Learn 定義的類別都具有相同的 API 介面,像是進行資料預處理的轉換器(Transformer)都具備 fit_transform()
方法;進行資料預測的預測器(Predictor)都具備 fit()
與 predict()
方法;檢查性指的是 Scikit-Learn 定義的類別所依據的參數、結果都可以透過屬性擷取出來檢視;不自行創建類別指的是輸入與輸出的資料型態或結構,多數都以內建資料與 ndarray
來處理;模組化指的是同為 Scikit-Learn
的類別可以進行組裝,像是將轉換器與預測器組裝成為一個稱為管線(Pipeline
)的類別;提供合理的預設參數指的是在初始化轉換器與預測器時,都會使用一組預設參數作為初始化的依據,而這些依據通常是多數使用者習慣的參數設計或基本標竿。
五個核心理念¶
我們使用 NBA 球員的範例資料來演繹 Scikit-Learn 的五個核心理念。
[2]:
cnb = CreateNBAData(2019)
players = cnb.create_players_df()
X = players['heightMeters'].values.reshape(-1, 1)
y = players['weightKilograms'].values
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.33, random_state=42)
Creating players df...
模組化¶
可以將 ss
轉換器與 lr
預測器組裝起來成為一個管線(Pipeline)類別。
[4]:
pipeline = Pipeline([('scaler', ss), ('lr', lr)])
type(pipeline)
[4]:
sklearn.pipeline.Pipeline
一致性¶
包含預測器的管線類別具有 fit
與 predict
方法。
[5]:
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_valid)
檢查性¶
在訓練完成之後,可以 intercept_
屬性提取常數項、以 coef_
屬性提取係數項觀察。
[6]:
print(lr.intercept_)
print(lr.coef_)
98.44183976261127
[8.93801801]
不自行創建類別¶
lr
在訓練完成之後,其 intercept_
屬性是 np.float64
、 coef_
屬性則是 ndarray
。
[7]:
print(type(lr.intercept_))
print(type(lr.coef_))
<class 'numpy.float64'>
<class 'numpy.ndarray'>
機器學習的資料表達¶
機器學習的資料表達意象有兩個分類:特徵矩陣(Feature matrix)與目標向量(Target vector),特徵矩陣是二維的數值陣列,外型為 (m, n)
,意指有 m
個觀測值、每個觀測值具有 n
個特徵,慣常以 \(X\) 做為標註;目標向量是一維的數值陣列,外型為 (m,)
,意指有 m
個觀測值,慣常以 \(y\) 作為標註。
舉例來說,前述範例中的 players
資料框外觀是:
[8]:
players.shape
[8]:
(503, 20)
假如我們改以身高(呎)與身高(吋)做為預測體重(磅)的依據:
[9]:
X = players[['heightFeet', 'heightInches']].values.astype(float)
y = players['weightPounds'].values.astype(float)
特徵矩陣與目標向量的維度數及其外觀就分別為:
[10]:
# 特徵矩陣
print(X.ndim)
print(X.shape)
2
(503, 2)
[11]:
# 目標向量
print(y.ndim)
print(y.shape)
1
(503,)
Scikit-Learn 的支援場景¶
一個資料科學專案中包含有資料的獲取、整併、轉換、探索、預測以及溝通等環節,而 Scikit-Learn 能夠支援資料獲取、轉換與預測這三個主要應用場景,針對這些階段以包裝妥善的函式、自定義類別來協助使用者。
在資料獲取的環節,sklearn.datasets
提供三種介面讓讓使用者可以載入玩具資料集、現實世界資料集與生成資料集:
load_dataset()
fetch_dataset()
make_dataset()
一如「機器學習的資料表達」所述,資料獲取功能所回傳的特徵矩陣 \(X\) 符合 (m, n)
外觀、目標向量 \(y\) 符合 (m,)
外觀;其中在載入玩具資料集與現實世界資料集中,Scikit-Learn 預設是以 bunch
這樣類似 dict
的資料結構回傳,指定參數 return_X_y=True
能夠直接獲得 \(X\) 與 \(y\)。
[12]:
# 載入玩具資料集
X, y = load_boston(return_X_y=True)
print(X.shape)
print(y.shape)
(506, 13)
(506,)
[13]:
# 載入現實世界資料集
X, y = fetch_california_housing(return_X_y=True)
print(X.shape)
print(y.shape)
(20640, 8)
(20640,)
[14]:
# 載入生成資料集
X, y = make_classification()
print(X.shape)
print(y.shape)
(100, 20)
(100,)
在資料轉換的環節,sklearn.preprocessing
提供一種稱為轉換器(Transformer)的自定義類別,初始化後可以透過 fit_transform
方法將輸入資料轉換為指定的輸出格式。常用的轉換器有高次項特徵與標準化,其中高次項特徵轉換器可以為特徵矩陣中的特徵生成截距項(即 \(x_0 = 1\))、高次項與交叉項:
[15]:
X = players[['heightFeet', 'heightInches']].values.astype(int)
X_before_poly = X.copy()
poly = PolynomialFeatures()
X_after_poly = poly.fit_transform(X_before_poly)
[16]:
# 輸入高次項特徵轉換器之前的 X: x_1, x_2
X_before_poly[:10, :]
[16]:
array([[ 6, 0],
[ 6, 11],
[ 6, 9],
[ 6, 11],
[ 6, 10],
[ 6, 5],
[ 6, 4],
[ 6, 11],
[ 6, 8],
[ 6, 9]])
[17]:
# 高次項特徵轉換器輸出的 X: x_0, x_1, x_2, x_1**2, x_1*x_2, x_2**2
X_after_poly
[17]:
array([[ 1., 6., 0., 36., 0., 0.],
[ 1., 6., 11., 36., 66., 121.],
[ 1., 6., 9., 36., 54., 81.],
...,
[ 1., 6., 11., 36., 66., 121.],
[ 1., 6., 10., 36., 60., 100.],
[ 1., 7., 0., 49., 0., 0.]])
而標準化轉換器則是可以為特徵矩陣中的特徵進行量度的標準化,像是最小最大標準化(Min-max scaler)或者常態標準化(Standard scaler)。
[18]:
X_before_scaled = X.copy()
ms = MinMaxScaler()
ss = StandardScaler()
X_after_ms = ms.fit_transform(X_before_scaled)
X_after_ss = ss.fit_transform(X_before_scaled)
[19]:
X_before_scaled[:10, :]
[19]:
array([[ 6, 0],
[ 6, 11],
[ 6, 9],
[ 6, 11],
[ 6, 10],
[ 6, 5],
[ 6, 4],
[ 6, 11],
[ 6, 8],
[ 6, 9]])
[20]:
X_after_ms[:10, :]
[20]:
array([[0.5 , 0. ],
[0.5 , 1. ],
[0.5 , 0.81818182],
[0.5 , 1. ],
[0.5 , 0.90909091],
[0.5 , 0.45454545],
[0.5 , 0.36363636],
[0.5 , 1. ],
[0.5 , 0.72727273],
[0.5 , 0.81818182]])
[21]:
X_after_ss[:10, :]
[21]:
array([[-0.15164926, -1.88887461],
[-0.15164926, 1.6276608 ],
[-0.15164926, 0.98829072],
[-0.15164926, 1.6276608 ],
[-0.15164926, 1.30797576],
[-0.15164926, -0.29044943],
[-0.15164926, -0.61013446],
[-0.15164926, 1.6276608 ],
[-0.15164926, 0.66860568],
[-0.15164926, 0.98829072]])
在資料預測的環節,sklearn
提供一種稱為預測器(Predictor)的自定義類別,初始化後可以透過 fit
方法對訓練資料進行「配適」,透過 predict
方法對驗證或測試資料進行「預測」。
[22]:
X = players[['heightFeet', 'heightInches']].values.astype(int)
y = players['weightKilograms'].values
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.33, random_state=42)
[23]:
# 初始化
lr = LinearRegression()
# 對訓練資料進行「配適」
lr.fit(X_train, y_train)
# 對驗證或測試資料進行「預測」
y_pred = lr.predict(X_valid)
關於訓練、驗證與測試資料¶
訓練資料(Training data,前述的 X_train
與 y_train
)是具有實際值或標籤的已實現歷史資料,作用是讓演算法能夠在其中尋找出一組能夠讓 \(h\) 與 \(f\) 的係數組合,訓練過程中透過比較預測結果與已實現的實際值或標籤,在能力可及範圍下尋找出一組相似度最高的係數組合;就像求學時課本中附有詳解的練習題一般,訓練我們對一個觀念的暸解。
驗證資料(Validation data,前述的 X_valid
與 y_valid
)同樣是具有實際值或標籤的已實現歷史資料,但是在使用上偽裝成不具有實際值或標籤的待預測資料,作用是在把 \(h\) 拿去面對未知資料之前,就能夠對 \(h\) 的可能表現心底有數;就像求學時參加模擬考試一般,在過程中就像真的參加考試一般,但是在之後有解答可以參考。
測試資料(Test data)是不具有實際值或標籤的待預測資料,作用是輸入訓練完成、驗證結果良好的 \(h\),藉此達成資料預測目的;就像求學時參加的大型考試一般。
以 Kaggle 網站所下載回來的資料為例,我們會將具有實際值或標籤的已實現歷史資料 train.csv
分割為訓練與驗證資料;不具有實際值或標籤的待預測資料 test.csv
就是測試資料,兩個資料在維度上的差別就是實際值或標籤的已實現歷史資料:目標向量 \(y\)。
[24]:
train = pd.read_csv("https://kaggle-getting-started.s3-ap-northeast-1.amazonaws.com/titanic/train.csv")
test = pd.read_csv("https://kaggle-getting-started.s3-ap-northeast-1.amazonaws.com/titanic/test.csv")
print(train.shape)
print(test.shape)
(891, 12)
(418, 11)
[25]:
# 差別在 Survived 這個目標向量
train.columns.difference(test.columns)
[25]:
Index(['Survived'], dtype='object')
使用 Scikit-Learn 包裝妥善的函式 train_test_split
可以將輸入分割為訓練與驗證資料,常見的觀測值比例由 6:4
到 9:1
不等,原則是訓練資料筆數應該大過於驗證資料筆數,透過函式中的 test_size
參數來設定驗證資料的比例。
[26]:
players_train, players_valid = train_test_split(players, test_size=0.3, random_state=42)
[27]:
players_train.iloc[:5, :4]
[27]:
firstName | lastName | temporaryDisplayName | personId | |
---|---|---|---|---|
116 | Terence | Davis | Davis, Terence | 1629056 |
45 | Bojan | Bogdanovic | Bogdanovic, Bojan | 202711 |
16 | Trevor | Ariza | Ariza, Trevor | 2772 |
465 | Moritz | Wagner | Wagner, Moritz | 1629021 |
358 | Elie | Okobo | Okobo, Elie | 1629059 |
[28]:
players_valid.iloc[:5, :4]
[28]:
firstName | lastName | temporaryDisplayName | personId | |
---|---|---|---|---|
268 | Skal | Labissiere | Labissiere, Skal | 1627746 |
73 | Trey | Burke | Burke, Trey | 203504 |
289 | Timothe | Luwawu-Cabarrot | Luwawu-Cabarrot, Timothe | 1627789 |
155 | Wenyen | Gabriel | Gabriel, Wenyen | 1629117 |
104 | Pat | Connaughton | Connaughton, Pat | 1626192 |
分割訓練與驗證資料的原則有二,先做資料集的隨機排序,像是我們玩撲克牌時所操作的洗牌(Shuffle),再來是依據 test_size
參數將具有實際值或標籤的已實現歷史資料水平切割,上方分配給驗證資料、下方分配給訓練資料;隨機排序是為避免訓練過程 \(h\) 的配適受到資料源本來的排序樣態所影響;依據這兩個原則自行定義一個 trainTestSplit
函式看是否可以獲得與前述相同的分割結果。
[29]:
def trainTestSplit(df, test_size, random_state):
df_index = df.index.values.copy()
m = df_index.size
np.random.seed(random_state)
np.random.shuffle(df_index)
test_index = int(np.ceil(m * test_size))
test_indices = df_index[:test_index]
train_indices = df_index[test_index:]
df_valid = df.loc[test_indices, :]
df_train = df.loc[train_indices, :]
return df_train, df_valid
[30]:
players_train, players_valid = trainTestSplit(players, test_size=0.3, random_state=42)
[31]:
players_train.iloc[:5, :4]
[31]:
firstName | lastName | temporaryDisplayName | personId | |
---|---|---|---|---|
116 | Terence | Davis | Davis, Terence | 1629056 |
45 | Bojan | Bogdanovic | Bogdanovic, Bojan | 202711 |
16 | Trevor | Ariza | Ariza, Trevor | 2772 |
465 | Moritz | Wagner | Wagner, Moritz | 1629021 |
358 | Elie | Okobo | Okobo, Elie | 1629059 |
[32]:
players_valid.iloc[:5, :4]
[32]:
firstName | lastName | temporaryDisplayName | personId | |
---|---|---|---|---|
268 | Skal | Labissiere | Labissiere, Skal | 1627746 |
73 | Trey | Burke | Burke, Trey | 203504 |
289 | Timothe | Luwawu-Cabarrot | Luwawu-Cabarrot, Timothe | 1627789 |
155 | Wenyen | Gabriel | Gabriel, Wenyen | 1629117 |
104 | Pat | Connaughton | Connaughton, Pat | 1626192 |
比對資料框的索引值可以驗證自行定義的 trainTestSplit
與 Scikit-Learn 的 train_test_split
分割邏輯相同。
延伸閱讀¶
Getting Started - scikit-learn (https://scikit-learn.org/stable/getting_started.html)
Kaggle (https://www.kaggle.com)
Introducing Scikit-Learn In: Jake VanderPlas, Python Data Science Handbook (https://jakevdp.github.io/PythonDataScienceHandbook/05.02-introducing-scikit-learn.html)
Sebastian Raschka, Vahid Mirjalili: Python Machine Learning (https://www.amazon.com/Python-Machine-Learning-scikit-learn-TensorFlow/dp/1789955750/)