機器學習入門

我們先載入這個章節範例程式碼中會使用到的第三方套件、模組或者其中的部分類別、函式。

[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 預測器時可以選擇採用預設參數。

[3]:
ss = StandardScaler()
lr = LinearRegression()

模組化

可以將 ss 轉換器與 lr 預測器組裝起來成為一個管線(Pipeline)類別。

[4]:
pipeline = Pipeline([('scaler', ss), ('lr', lr)])
type(pipeline)
[4]:
sklearn.pipeline.Pipeline

一致性

包含預測器的管線類別具有 fitpredict 方法。

[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.float64coef_ 屬性則是 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_trainy_train)是具有實際值或標籤的已實現歷史資料,作用是讓演算法能夠在其中尋找出一組能夠讓 \(h\)\(f\) 的係數組合,訓練過程中透過比較預測結果與已實現的實際值或標籤,在能力可及範圍下尋找出一組相似度最高的係數組合;就像求學時課本中附有詳解的練習題一般,訓練我們對一個觀念的暸解。

驗證資料(Validation data,前述的 X_validy_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:49: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 分割邏輯相同。

延伸閱讀

  1. Getting Started - scikit-learn (https://scikit-learn.org/stable/getting_started.html)

  2. Kaggle (https://www.kaggle.com)

  3. Introducing Scikit-Learn In: Jake VanderPlas, Python Data Science Handbook (https://jakevdp.github.io/PythonDataScienceHandbook/05.02-introducing-scikit-learn.html)

  4. Sebastian Raschka, Vahid Mirjalili: Python Machine Learning (https://www.amazon.com/Python-Machine-Learning-scikit-learn-TensorFlow/dp/1789955750/)