隨機森林 Random Forest & 表格模型(筆記 1 by Claude)

Claude 做的筆記,先記錄一下,之後再來看寫的好不好。

本筆記是 Jeremy Howard 在 “Practical Deep Learning for Coders” 課程中關於隨機森林的教學內容,按照 YouTube 影片的時間順序組織,並結合了 Kaggle 筆記本和 fastbook 中的補充內容。主要參考來源:


課程回顧

00:00-02:08

Jeremy 開始回顧前面課程,提到我們已經學習了如何處理:

  • 圖像數據(使用卷積神經網絡)
  • 文本數據(使用循環神經網絡和 NLP 技術)

現在將轉向表格數據(tabular data)的處理,這是實際應用中最常見的數據類型之一。


TwoR 模型:兩種創建模型的方式

02:09-04:42

Jeremy 提出了兩種創建模型的基本方法:

  1. 定義架構然後訓練參數(Method 1)

    • 先設計模型架構(如神經網絡結構)
    • 然後通過梯度下降等方法調整參數
    • 這是深度學習的典型方法
  2. 直接從數據構建模型(Method 2)

    • 不預先定義固定結構
    • 根據數據特性自動構建模型
    • 決策樹和隨機森林採用這種方法

Jeremy 解釋說,對於表格數據,Method 2(如隨機森林)通常更有效,特別是當數據量不是特別大時。


如何創建決策樹

04:43-07:01

Jeremy 詳細說明了決策樹的創建過程:

# 基本設置代碼
from sklearn.tree import DecisionTreeRegressor
import pandas as pd
import numpy as np

決策樹的基本工作原理:

  1. 從單一節點開始,包含所有數據
  2. 尋找最佳特徵和閾值來分割數據
  3. 重複這個過程,直到達到停止條件(如葉節點夠純或達到最大深度)

Jeremy 使用了一個簡單的例子來說明:

# 創建一個簡單的決策樹
m = DecisionTreeRegressor(max_leaf_nodes=4)
m.fit(xs, y)  # xs是特徵,y是目標值

他強調決策樹的主要優勢是可解釋性:我們可以跟蹤模型如何做出預測。


Gini 不純度和決策樹分割

07:02-10:53

Jeremy 解釋了決策樹如何決定最佳分割點,重點介紹了 Gini 不純度:

# Gini 不純度計算
def gini(x):
    return 1 - ((x/x.sum())**2).sum()

# 計算分割的加權 Gini 不純度
def gini_split(col, y):
    # 尋找最佳分割點
    scores = [gini_xy(col <= t, y) for t in thresholds]
    idx = np.argmin(scores)
    t = thresholds[idx]
    return t, scores[idx]

def gini_xy(mask, y):
    # 計算左右子節點的加權 Gini 值
    g_l = gini(y[mask])
    g_r = gini(y[~mask])
    # 按樣本比例加權
    n = len(y)
    n_l = mask.sum()
    n_r = n - n_l
    return n_l/n*g_l + n_r/n*g_r

關鍵概念:

  • Gini 不純度衡量一組數據的混雜程度
  • 決策樹選擇能最大減少不純度的分割
  • 分類問題使用 Gini 不純度,回歸問題使用均方誤差

製作 Kaggle 提交:Blue Book for Bulldozers

10:54-15:51

Jeremy 轉向實際案例—Blue Book for Bulldozers 競賽,這是一個預測二手推土機拍賣價格的問題:

# 數據加載與準備
path = Path('bulldozers')
df = pd.read_csv(path/'Train.csv', low_memory=False, parse_dates=['saledate'])

# 查看數據
df.head()
df.columns

數據處理步驟:

# 創建時間相關特徵
df['SaleYear'] = df.saledate.dt.year
df['SaleMonth'] = df.saledate.dt.month
df['SaleDay'] = df.saledate.dt.day
df['SaleDayOfWeek'] = df.saledate.dt.dayofweek
df['SaleDayOfYear'] = df.saledate.dt.dayofyear

# 數據分割
dep_var = 'SalePrice'
df_valid = df[df.SaleYear==2012]  # 2012 年數據作為驗證集
df_train = df[df.SaleYear<2012]   # 之前年份作為訓練集

# 使用 fastai 準備數據
cat_names = ['SaleYear', 'SaleMonth', 'SaleDay', 'SaleDayOfWeek', 'SaleDayOfYear']
cont_names = []  # 暫時沒有使用連續特徵
procs = [Categorify, FillMissing]

# 創建 DataLoaders
dls = TabularDataLoaders.from_df(df_train, path, procs=procs, cat_names=cat_names, 
                               cont_names=cont_names, y_names=dep_var, 
                               valid_idx=list(range(len(df_train), len(df))))

Jeremy 展示了如何使用 fastai 的 TabularLearner 來訓練模型並創建 Kaggle 提交文件:

# 創建和訓練模型
learn = tabular_learner(dls, metrics=rmse)
learn.fit_one_cycle(5, 3e-3)

# 獲取預測並創建提交文件
preds, _ = learn.get_preds(dl=test_dl)
sample_sub = pd.read_csv(path/'Sample_Submission.csv')
sample_sub.iloc[:,1] = preds
sample_sub.to_csv('subm.csv', index=False)

Bagging(引導式聚合)

15:52-19:05

Jeremy 介紹了 Bagging(Bootstrap Aggregating)的概念,這是隨機森林的基礎:

# Bagging 的簡單實現
def bagging_preds(m, xs, n_bags=10):
    n,_ = xs.shape
    preds = np.zeros(n)
    for i in range(n_bags):
        # 創建自助樣本
        idx = np.random.choice(n, n, replace=True) 
        # 訓練模型
        m = DecisionTreeRegressor().fit(xs[idx], y[idx])
        # 累加預測
        preds += m.predict(xs)
    # 取平均
    return preds / n_bags

Bagging 的關鍵步驟:

  1. 從原始數據集中抽取多個自助樣本(有放回抽樣)
  2. 在每個樣本上訓練一個單獨的模型
  3. 將所有模型的預測結果平均(回歸問題)或取多數投票(分類問題)

Jeremy 解釋,Bagging 主要通過減少模型方差來提高性能,特別適合高方差模型(如深度決策樹)。


隨機森林介紹

19:06-20:08

Jeremy 解釋隨機森林是 Bagging 的擴展,加入了特徵隨機選擇:

  1. Bagging:使用自助樣本創建多個決策樹
  2. 特徵隨機選擇:在每個節點,只考慮特徵的隨機子集
    • 分類問題通常使用 sqrt(n_features)
    • 回歸問題通常使用 n_features/3

這種雙重隨機性(樣本和特徵)使得樹之間的相關性更低,從而進一步降低集成模型的方差。


創建隨機森林

20:09-22:37

Jeremy 展示了如何使用 scikit-learn 創建隨機森林:

# 使用 sklearn 創建隨機森林
from sklearn.ensemble import RandomForestRegressor

m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.5, 
                        n_jobs=-1, oob_score=True)
m.fit(xs, y)

主要參數說明:

  • n_estimators:森林中樹的數量
  • min_samples_leaf:葉節點最小樣本數
  • max_features:每次分割考慮的特徵比例
  • n_jobs:並行處理的作業數(-1表示使用所有可用處理器)
  • oob_score:是否計算袋外樣本評分

然後展示如何使用 fastai 創建隨機森林:

# 使用 fastai 創建隨機森林
learn = tabular_learner(dls, metrics=rmse)
learn.fit_one_cycle(5, 3e-3)

特徵重要性

22:38-26:36

Jeremy 強調隨機森林的一個主要優勢是能提供特徵重要性:

# 定義一個方便的隨機森林創建函數
def rf(xs, y, n_estimators=40, max_samples=200_000, 
       max_features=0.5, min_samples_leaf=5, **kwargs):
    return RandomForestRegressor(n_estimators=n_estimators, 
                         max_samples=max_samples,
                         max_features=max_features,
                         min_samples_leaf=min_samples_leaf, 
                         n_jobs=-1, oob_score=True, **kwargs).fit(xs, y)

# 創建模型
m = rf(xs, y)

# 可視化特徵重要性
def plot_fi(fi):
    return fi.plot('cols', 'imp', figsize=(12,7), legend=False)

fi = pd.DataFrame({'cols':df.columns, 'imp':m.feature_importances_})
fi = fi.sort_values('imp', ascending=False)

plot_fi(fi[:30]);  # 顯示前30個最重要的特徵

特徵重要性的應用:

  1. 了解哪些變量對預測最有影響
  2. 特徵選擇:移除不重要的特徵
  3. 指導進一步的特徵工程

以上是鐵達尼號的衍生說明,以及 Random Forest 的初步介紹。以下是 fastbook 第 9 章內容,運用在另一個挖土機預測的 Kaggle 專案。


增加樹的數量

26:37-29:31

Jeremy 討論了增加樹的數量如何影響模型性能:

# 嘗試不同樹的數量
for i in [1, 3, 10, 30, 100]:
    m = rf(xs_final, y, n_estimators=i)
    print(f'n_estimators={i}, OOB score={m.oob_score_}')

關鍵發現:

  • 隨著樹數量增加,性能通常會提升
  • 但存在收益遞減點,超過某個數量後提升很小
  • 更多的樹意味著更長的訓練時間和更多的內存使用

Jeremy 建議從 20-40 棵樹開始,根據需要增加。


什麼是 OOB (Out-of-Bag)

29:32-32:07

Jeremy 詳細解釋了袋外樣本(OOB)評估的概念:

# 在建模時啟用 OOB 評分
m = rf(xs, y, oob_score=True)
print(f'OOB score: {m.oob_score_}')

OOB 評估的工作原理:

  1. 在 Bagging 過程中,每棵樹約使用 63.2% 的原始樣本進行訓練
  2. 剩餘 36.8% 未使用的樣本稱為"袋外"樣本
  3. 每棵樹可在其袋外樣本上進行測試
  4. 每個數據點的最終 OOB 預測基於未使用該點訓練的所有樹

OOB 的主要優勢:

  • 提供無需單獨驗證集的性能估計
  • 計算效率高,無需額外訓練或預測
  • 是模型泛化能力的良好估計

模型解釋

32:08-35:46

Jeremy 談到了理解模型決策過程的重要性:

# 查看決策樹的結構
from dtreeviz.trees import dtreeviz

# 可視化單棵樹
estimator = m.estimators_[0]
viz = dtreeviz(estimator, xs, y, target_name='price', 
               feature_names=df.columns)
viz

他提醒我們,雖然單棵樹相對易於解釋,但隨機森林的最終預測是多棵樹的綜合結果,這增加了複雜性。

其他理解決策的方法:

  • 查看個別預測的決策路徑
  • 分析特徵重要性
  • 使用部分依賴圖和 SHAP 值(後面會詳細介紹)

移除冗餘特徵

35:47-35:58

Jeremy 簡要提到如何使用特徵重要性來移除不重要的特徵:

# 保留重要性超過閾值的特徵
to_keep = fi[fi.imp>0.005].cols
xs_final = xs[to_keep]

# 使用減少的特徵集重新訓練
m = rf(xs_final, y)
print(f'New OOB score: {m.oob_score_}')

這一步可以:

  • 減少計算成本
  • 降低模型複雜度
  • 有時還能提高性能(減少噪聲)

部分依賴圖的作用

35:59-39:21

Jeremy 介紹了部分依賴圖(PDP)如何幫助理解特徵與目標之間的關係:

from pdpbox import pdp

def plot_pdp(feat, clusters=None, feat_name=None):
    feat_name = feat_name or feat
    p = pdp.pdp_isolate(m, xs, feat_name=feat, model_features=xs.columns)
    return pdp.pdp_plot(p, feat_name, plot_lines=True, 
                     cluster=clusters is not None, 
                     n_cluster_centers=clusters)

他展示了幾個特徵的部分依賴圖:

plot_pdp('YearMade')  # 顯示製造年份與價格的關係
plot_pdp('ProductSize')  # 產品尺寸與價格的關係

部分依賴圖的原理:

  1. 選擇一個特徵,讓它在其範圍內變化
  2. 對於每個值,計算所有樣本的平均預測
  3. 繪製特徵值與平均預測的關係

這幫助我們理解:

  • 特徵如何影響預測(正相關還是負相關)
  • 關係是線性還是非線性
  • 是否有轉折點或閾值效應

如何解釋特定預測

39:22-46:06

Jeremy 討論了如何解釋模型對單個樣本的預測,介紹了 LIME 和 SHAP 值:

# 使用 SHAP 解釋單個預測
import shap

explainer = shap.TreeExplainer(m)
# 選擇一個樣本
i = 0
row = xs_final.iloc[i:i+1]
# 計算 SHAP 值
shap_values = explainer.shap_values(row)

# 可視化 SHAP 值
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[0], row.iloc[0])

SHAP 圖的解讀:

  • 紅色表示推高預測值的特徵
  • 藍色表示拉低預測值的特徵
  • 特徵的寬度表示影響大小
  • 基準值是數據集的平均預測

Jeremy 說明了 SHAP 值如何提供了一種公平的方法來分解和理解單個預測,使我們能夠解釋為什麼模型做出了特定決策。


是否可以過擬合隨機森林

46:07-49:02

Jeremy 討論了一個常見問題:隨機森林是否會過擬合?

他解釋:

  1. 從技術上講,是可以的

    • 如果樹太深
    • 如果 min_samples_leaf 太小
    • 如果數據量太少
  2. 但實際上很少發生

    • 隨機森林的集成性質天然抵抗過擬合
    • Bagging 和特徵隨機性顯著降低了過擬合風險

Jeremy 進行了實驗,嘗試不同的 min_samples_leaf 值:

for i in [1, 3, 5, 10, 25]:
    m = rf(xs_final, y, min_samples_leaf=i)
    print(f'min_samples_leaf={i}, OOB score={m.oob_score_}')

結果顯示在這個數據集上,較小的 min_samples_leaf 值(允許更複雜的模型)實際上表現更好,說明模型並未過擬合。


什麼是梯度提升

49:03-51:55

Jeremy 簡要介紹了梯度提升(Gradient Boosting)作為隨機森林的替代方法:

from sklearn.ensemble import GradientBoostingRegressor

m = GradientBoostingRegressor(max_leaf_nodes=8)
m.fit(xs_final, y)

梯度提升與隨機森林的主要區別:

  1. 訓練方式

    • 隨機森林:並行訓練獨立的樹
    • 梯度提升:順序訓練,每棵新樹專注於修正前面樹的錯誤
  2. 樹的類型

    • 隨機森林:通常是深度較大的樹
    • 梯度提升:通常使用淺樹(弱學習器)
  3. 調參複雜度

    • 隨機森林:相對容易調參
    • 梯度提升:通常需要更仔細的調參(學習率、樹深度等)

Jeremy 提到 XGBoost、LightGBM 和 CatBoost 是流行的梯度提升實現,在某些競賽中能獲得比隨機森林更好的結果,但代價是更複雜的調參過程。


其他話題和工具介紹

51:56-1:20:17

在課程的後半部分,Jeremy 介紹了多個實用工具和技巧:

Walkthrus 和 fastkaggle (51:56-1:02:51)

Jeremy 介紹了課程的 “walkthru” 概念和 fastkaggle 工具:

# 安裝 fastkaggle
!pip install -Uq fastkaggle
from fastkaggle import *

fastkaggle 提供了簡化 Kaggle 工作流程的功能,如數據下載、提交等。

並行處理與數據處理技巧 (1:02:52-1:07:21)

# 使用 fastcore.parallel 加速計算
from fastcore.parallel import *
parallel(lambda i: random.random(), range(5))

# 圖像處理方法
item_tfms=Resize(480, method='squish')

模型評估與迭代策略

1:07:22-1:27:52

Jeremy 討論了評估模型的多種標準和系統化改進模型的方法:

評估標準:

  • 準確性
  • 計算效率
  • 推理時間
  • 可解釋性
  • 部署複雜度

模型迭代策略:

  1. 從簡單基線開始
  2. 分析錯誤
  3. 有針對性地改進
  4. 持續評估和優化

高級模型架構與數據增強

1:27:53-1:38:16

Jeremy 簡要提到了一些高級主題,如 ConvNeXt 架構和測試時增強(TTA):

# 測試時增強
preds, _ = learn.tta()

▌隨機森林課程總結

Jeremy 在課程中強調了隨機森林的關鍵優勢:

  1. 處理表格數據的強大工具

    • 通常是表格數據的首選方法
    • 相對於深度學習,需要較少的數據即可獲得良好性能
  2. 易於使用

    • 相對容易訓練和調參
    • 很少需要特徵縮放或複雜的預處理
  3. 提供豐富解釋信息

    • 特徵重要性
    • 部分依賴圖
    • SHAP 值
  4. 實用性強

    • 可處理缺失值
    • 可處理類別和數值特徵
    • 訓練速度相對較快

主要應用案例包括:

  • Blue Book for Bulldozers:預測二手推土機價格
  • Titanic 數據集:預測乘客存活率

這些應用展示了隨機森林在不同類型表格數據任務中的有效性和實用性。


▌關鍵代碼總結

1. 使用 sklearn 創建隨機森林

from sklearn.ensemble import RandomForestRegressor

def rf(xs, y, n_estimators=40, max_samples=200_000, 
       max_features=0.5, min_samples_leaf=5, **kwargs):
    return RandomForestRegressor(n_estimators=n_estimators, 
                         max_samples=max_samples,
                         max_features=max_features,
                         min_samples_leaf=min_samples_leaf, 
                         n_jobs=-1, oob_score=True, **kwargs).fit(xs, y)

m = rf(xs, y)

2. 使用 fastai 創建隨機森林

procs = [Categorify, FillMissing]
dls = TabularDataLoaders.from_df(df_train, path, procs=procs, 
                              cat_names=cat_names, 
                              cont_names=cont_names, 
                              y_names=dep_var, 
                              valid_idx=valid_idx)
learn = tabular_learner(dls, metrics=rmse)
learn.fit_one_cycle(5, 3e-3)

3. 特徵重要性可視化

def plot_fi(fi):
    return fi.plot('cols', 'imp', figsize=(12,7), legend=False)

fi = pd.DataFrame({'cols':df.columns, 'imp':m.feature_importances_})
fi = fi.sort_values('imp', ascending=False)

plot_fi(fi[:30]);

4. 部分依賴圖

from pdpbox import pdp

def plot_pdp(feat, clusters=None, feat_name=None):
    feat_name = feat_name or feat
    p = pdp.pdp_isolate(m, xs, feat_name=feat, model_features=xs.columns)
    return pdp.pdp_plot(p, feat_name, plot_lines=True, 
                     cluster=clusters is not None, 
                     n_cluster_centers=clusters)

plot_pdp('YearMade')

5. SHAP 值解釋

import shap

explainer = shap.TreeExplainer(m)
shap_values = explainer.shap_values(row)

shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[0], row.iloc[0])

▌參考資料