IOAI 補缺|經典機器學習

🏠 IOAI 總覽 | ← 逐課重點 | 視覺語言音訊 →

這一頁怎麼用

  • 大綱第 1 部分有 17 項 fast.ai 教得不夠(見 大綱對照表),這份全部補上。
  • 每一節的格式都一樣:一張表看懂 → 一張圖記住 → 一段程式自己做一次 → 比賽提示。
  • 程式可以直接貼進 Kaggle 或 Colab 執行(已內建 scikit-learn)。輸出結果寫在程式的註解裡,你跑出來的數字應該一樣。
  • 圖可以用 圖表重製程式 自己重畫。
  • 看不懂的詞查 白話小辭典;數學看不懂查 中三數學補充

scikit-learn 只要記一個套路

步驟程式意思
1 建模型m = 模型名稱(參數)選工具
2 訓練m.fit(X_train, y_train)學
3 預測m.predict(X_test)用
4 打分m.score(X_test, y_test)看準不準

所有模型都是這四步,換模型只要改第 1 行。

跟著做

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

X, y = load_iris(return_X_y=True)                 # 150 朵花、4 個特徵、3 種
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
m = DecisionTreeClassifier(random_state=0)        # 1 建模型
m.fit(X_train, y_train)                           # 2 訓練
print(m.predict(X_test[:5]))                      # 3 預測 → [2 1 0 2 0]
print(round(m.score(X_test, y_test), 3))          # 4 打分 → 0.978

§0|畫圖:Matplotlib 與 Seaborn

想看什麼MatplotlibSeaborn
一個數字欄位的分布plt.hist(x)sns.histplot(df, x='欄')
兩個欄位的關係plt.scatter(x, y)sns.scatterplot(df, x=, y=, hue=)
隨時間變化(損失曲線)plt.plot(x, y)sns.lineplot(...)
各類別平均plt.bar(names, values)sns.barplot(df, x=, y=)
欄位之間的相關plt.imshow(df.corr())sns.heatmap(df.corr(), annot=True)
跟著做

import seaborn as sns
import matplotlib.pyplot as plt

df = sns.load_dataset('penguins').dropna()        # 企鵝資料(Kaggle、Colab 可直接載入)
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
sns.scatterplot(df, x='flipper_length_mm', y='body_mass_g', hue='species', ax=axs[0])
sns.heatmap(df.select_dtypes('number').corr(), annot=True, fmt='.2f', ax=axs[1])
plt.tight_layout(); plt.show()
比賽提示

拿到資料第一件事是畫圖,看分布、看缺值、看類別是否平衡。

§1|邏輯斯迴歸(Logistic Regression)

項目內容
做什麼分類(名字有迴歸,其實是分類)
怎麼做線性組合 z = w·x + b,再用 sigmoid 壓成 0 到 1 的機率
損失二元交叉熵
優點快、可解釋(看權重 w 就知道哪個特徵重要)
缺點邊界只能是直線(見下圖第 1 格)
和 fast.aiL05 的 Titanic 線性模型加 sigmoid,就是邏輯斯迴歸
ioai_activations
跟著做

from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)        # 569 個腫瘤、30 個特徵、良性/惡性
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
m = make_pipeline(StandardScaler(), LogisticRegression())   # 先標準化再訓練
m.fit(X_train, y_train)
print(round(m.score(X_test, y_test), 3))          # → 0.977
print(m.predict_proba(X_test[:2]).round(3))       # 每列是 [P(0), P(1)] → [[0.999 0.001] [0.04 0.96]]

§2|正則化:L1、L2、Dropout、Early Stopping

方法做法效果在哪裡用
L1(Lasso)損失 + α × Σ|w|很多權重變成 0 → 自動挑特徵Lasso(alpha=)、LogisticRegression(penalty='l1', solver='liblinear')
L2(Ridge)損失 + α × Σw²權重一起縮小,不會變 0Ridge(alpha=);神經網路的 weight_decay
Dropout訓練時隨機關掉一部分神經元不能依賴某幾個神經元nn.Dropout(0.5)(fast.ai L08、L19)
Early Stopping驗證損失不再下降就停停在過擬合之前每輪記錄驗證損失,留最好的那一版
ioai_l1l2 ioai_curves
跟著做

from sklearn.datasets import load_diabetes
from sklearn.linear_model import Lasso, Ridge
from sklearn.preprocessing import StandardScaler

X, y = load_diabetes(return_X_y=True)             # 10 個特徵預測糖尿病進展
X = StandardScaler().fit_transform(X)
for M in [Lasso(alpha=5), Ridge(alpha=5)]:
    w = M.fit(X, y).coef_
    print(type(M).__name__, '變成 0 的權重數:', (abs(w) < 1e-6).sum())
# → Lasso 變成 0 的權重數: 5
# → Ridge 變成 0 的權重數: 0
比賽提示

特徵很多、懷疑很多沒用 → 用 L1。只是想防過擬合 → 用 L2。α 越大,懲罰越重。

§3|K-NN、SVM 與樹模型比較

模型一句話邊界形狀要標準化嗎關鍵參數
邏輯斯迴歸畫一條直線分開直線要C(越小正則越強)
K-NN看最近 k 個鄰居投票彎曲一定要(用距離)n_neighbors
決策樹一連串是非題方塊不用max_depth
隨機森林很多樹投票(bagging)方塊、較平滑不用n_estimators、max_depth
Gradient Boosting一棵接一棵修正錯誤方塊、較平滑不用learning_rate、n_estimators
SVM找離兩邊最遠的邊界;核函數讓邊界彎曲直線或彎曲一定要C、kernel='rbf'、gamma
ioai_boundaries
跟著做

from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier

X, y = make_moons(500, noise=0.3, random_state=0)  # 兩個月牙形
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
models = {
    '邏輯斯迴歸': make_pipeline(StandardScaler(), LogisticRegression()),
    'K-NN': make_pipeline(StandardScaler(), KNeighborsClassifier(15)),
    'SVM': make_pipeline(StandardScaler(), SVC()),
    '決策樹': DecisionTreeClassifier(max_depth=4, random_state=0),
    '隨機森林': RandomForestClassifier(200, max_depth=6, random_state=0),
    'Boosting': GradientBoostingClassifier(random_state=0),
}
for name, m in models.items():
    print(f'{name:6} {m.fit(X_train, y_train).score(X_test, y_test):.3f}')
# → 邏輯斯迴歸 0.813、K-NN 0.873、SVM 0.880、決策樹 0.880、隨機森林 0.893、Boosting 0.893
比賽提示

表格資料先試隨機森林或 Boosting 當基準線;資料少、特徵少時 SVM 常常很強;K-NN 和 SVM 忘了標準化會差很多。

§4|分群:K-Means、DBSCAN、階層、譜分群

方法一句話要先給群數嗎適合不適合
K-Means反覆把點分給最近的中心,再更新中心要(k)圓團、大小相近月牙、長條
DBSCAN密的地方連成一群,稀的是雜訊不用(給 eps 距離)任意形狀、有雜訊密度差很多
階層分群從每點一群開始,最近的兩群一直合併可事後切想看樹狀關係資料很大
譜分群先建「誰和誰相近」的圖再切要彎曲形狀資料很大
均值漂移(fast.ai L12)每點往附近平均移動不用任意形狀慢
ioai_cluster
跟著做

from sklearn.datasets import make_moons
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering, SpectralClustering
from sklearn.metrics import adjusted_rand_score

X, y = make_moons(300, noise=0.06, random_state=0)
for name, m in [('K-Means', KMeans(2, n_init=10, random_state=0)),
                ('DBSCAN', DBSCAN(eps=0.2)),
                ('階層', AgglomerativeClustering(2, linkage='single')),
                ('譜分群', SpectralClustering(2, affinity='nearest_neighbors', random_state=0))]:
    print(f'{name:8} 和正確答案的相似度 ARI = {adjusted_rand_score(y, m.fit_predict(X)):.2f}')
# → K-Means 0.23(切錯)、DBSCAN 1.00、階層 1.00、譜分群 0.87

§5|降維:PCA、t-SNE、UMAP

方法一句話能用在新資料嗎用途
PCA找變化最大的方向投影過去(線性)能(transform)壓縮特徵、去雜訊、當特徵工程
t-SNE讓原本相近的點在 2D 也相近不能只拿來看圖
UMAP和 t-SNE 類似,更快,整體結構保留較好能看圖、也可當特徵
ioai_pca_tsne
跟著做

from sklearn.datasets import load_digits
from sklearn.decomposition import PCA

X, y = load_digits(return_X_y=True)               # 1797 張 8×8 手寫數字 → 64 維
pca = PCA(n_components=0.95).fit(X)               # 留住 95% 的變化量
print(X.shape[1], '維 →', pca.n_components_, '維')   # → 64 維 → 29 維
print(pca.explained_variance_ratio_[:3].round(3)) # 前三個方向各占多少 → [0.149 0.136 0.118]

UMAP 要另外安裝:pip install umap-learn,用法 umap.UMAP(n_components=2).fit_transform(X)。

比賽提示

PCA 前一定要標準化(除非各欄單位相同,像像素)。t-SNE 的圖上「群和群的距離」沒有意義。

§6|評估指標:混淆矩陣、精確率、召回率、F1、ROC

指標公式白話什麼時候最重要
準確率 Accuracy(TP+TN) ÷ 全部答對幾成類別平衡時
精確率 PrecisionTP ÷ (TP+FP)說「是」的裡面有幾成真的是誤報代價高(垃圾郵件誤刪)
召回率 RecallTP ÷ (TP+FN)真的「是」的裡面抓到幾成漏報代價高(癌症篩檢)
F12PR ÷ (P+R)P 和 R 的調和平均類別不平衡時
ROC-AUCROC 曲線下面積隨便抽一正一負,正的分數較高的機率要比較模型,不想選門檻時
ioai_cm_roc
為什麼不能只看準確率

100 人裡 1 人有病,模型全部說「沒病」,準確率 99%,召回率 0%。

跟著做

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import confusion_matrix, classification_report, roc_auc_score

X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
m = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)
pred = m.predict(X_test)
print(confusion_matrix(y_test, pred))             # [[TN FP] [FN TP]] → [[60 3] [1 107]]
print(classification_report(y_test, pred, digits=3))
print('AUC', round(roc_auc_score(y_test, m.predict_proba(X_test)[:, 1]), 3))   # → AUC 0.994
手算練習

TP = 40、FP = 10、FN = 20、TN = 30。算出準確率、精確率、召回率、F1。

答案

準確率 = 70 ÷ 100 = 0.70;精確率 = 40 ÷ 50 = 0.80;召回率 = 40 ÷ 60 = 0.67;F1 = 2 × 0.8 × 0.67 ÷ 1.47 = 0.73

§7|交叉驗證與超參數調整

方法做法用途
訓練/驗證/測試切分70/15/15最基本
K 折交叉驗證切 K 份,輪流當驗證集,取平均資料少時估計更穩
分層(Stratified)每一折類別比例一樣類別不平衡
時間序列切分只用過去預測未來有時間順序的資料(不能打亂)
Grid Search把參數組合全部試一次參數少
Random Search隨機抽組合試參數多,通常更有效率
ioai_split_cv
跟著做

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import cross_val_score, GridSearchCV
from sklearn.ensemble import RandomForestClassifier

X, y = load_breast_cancer(return_X_y=True)
scores = cross_val_score(RandomForestClassifier(random_state=0), X, y, cv=5)
print(scores.round(3), '平均', scores.mean().round(3))   # → 平均 0.963

grid = GridSearchCV(RandomForestClassifier(random_state=0),
                    {'n_estimators': [50, 200], 'max_depth': [3, None]}, cv=5)
grid.fit(X, y)
print(grid.best_params_, round(grid.best_score_, 3))   # → {'max_depth': None, 'n_estimators': 50} 0.963
比賽提示

調參數只能看驗證分數。偷看測試集調參數=作弊,分數會虛高。

§8|特徵工程與資料前處理

大綱特別列出的技巧:

問題技巧程式
缺值(數字)補平均或中位數SimpleImputer(strategy='median')
缺值(類別)補眾數(fast.ai L05)df.fillna(df.mode().iloc[0])
缺值(時間序列)用前一個值往前填df.ffill()
類別欄位One-hot 編碼pd.get_dummies(df, columns=['性別'])
數值大小差很多標準化(平均 0、標準差 1)StandardScaler()
想壓到 0 到 1正規化MinMaxScaler()
長尾數字(票價、收入)取 lognp.log1p(x)
時間序列滑動視窗統計s.rolling(7).mean()、.std()
長短不一的序列補零到一樣長(padding)nn.utils.rnn.pad_sequence
高維資料PCA 或神經網路嵌入見 §5、fast.ai L07
圖片切成小塊Patching(ViT 的做法)x.unfold(...)
跟著做

import numpy as np
import pandas as pd

df = pd.DataFrame({'年齡': [22, np.nan, 35, 58],
                   '城市': ['澳門', '香港', np.nan, '澳門'],
                   '票價': [7.25, 71.3, 8.05, 512.3]})
df['年齡'] = df['年齡'].fillna(df['年齡'].median())    # 缺值補中位數 → 35.0
df['城市'] = df['城市'].fillna(df['城市'].mode()[0])    # 補眾數 → 澳門
df['log票價'] = np.log1p(df['票價'])                    # 長尾取 log
df = pd.get_dummies(df, columns=['城市'], dtype=int)   # one-hot
print(df.round(2))

s = pd.Series([3, 5, 4, 6, 8, 7, 9])
print(s.rolling(3).mean().tolist())   # 滑動平均 → [nan, nan, 4.0, 5.0, 6.0, 7.0, 8.0]
比賽提示

前處理的參數(平均、中位數)只能用訓練集算,再套到驗證和測試集,否則就是資料外洩。用 Pipeline 包起來最安全。

自我檢核

  • 用四步套路訓練任一 scikit-learn 模型
  • 說出 L1 和 L2 的差別,各舉一個用途
  • 哪兩個模型一定要先標準化?為什麼?
  • 月牙形資料用哪個分群方法?
  • t-SNE 能不能拿來轉換測試資料?
  • 從混淆矩陣手算精確率、召回率、F1
  • 時間序列資料為什麼不能隨機切分?
  • 為什麼前處理只能用訓練集的統計值?
答案提示(先自己想再點開)

3 是 K-NN 和 SVM(靠距離);5 不能;7 會用未來資料預測過去;8 避免資料外洩。


🏠 IOAI 總覽 | ← 逐課重點 | 視覺語言音訊 →

Built with LogoFlowershow