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

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

上一篇筆記 的不同在於,前篇筆記以 YouTube 影片 為主,Kaggle 和書籍為輔。

本筆記則以 書籍為主KaggleYouTube 影片 為輔。


Jeremy 強調,雖然深度學習在圖像和文本領域取得了突破性進展,但在表格數據領域,決策樹模型(尤其是隨機森林和梯度提升)仍然是非常強大的工具,通常是解決實際問題的首選方法。

▌1. 表格數據與 Bulldozers 競賽介紹

1.1 什麼是表格數據

表格數據是機器學習中最常見的數據類型之一,其特點是:

  • 以表格形式組織(行和列)
  • 每列代表一個特徵/屬性
  • 每行代表一個實例/樣本
  • 通常包含混合的數據類型(數值、類別等)

Jeremy 指出,雖然深度學習在圖像和文本領域取得了突破性進展,但在表格數據領域,傳統機器學習方法(特別是隨機森林和梯度提升樹)仍然非常有競爭力。

1.2 Blue Book for Bulldozers 競賽概述

這個 Kaggle 競賽的目標是預測二手推土機的拍賣價格。競賽特點:

  • 時間序列問題:訓練數據包括 1989-2011 年的銷售數據,測試數據來自 2012 年
  • 評估指標:平均絕對誤差(RMSLE,root mean squared log error)
  • 數據來源:來自 Tractor Blue Book,包含大量推土機特徵信息

Jeremy 選擇這個競賽作為案例,因為它具有實際應用價值,並包含處理表格數據時常見的諸多挑戰。


▌2. 數據探索與準備

2.1 加載數據

from fastai.tabular.all import *
path = Path('data/bulldozers/')

# 讀取數據
df = pd.read_csv(path/'TrainAndValid.csv', low_memory=False, 
                parse_dates=['saledate'])

# 查看數據結構
df.info()
df.head()

Jeremy 解釋了加載數據時的一些重要考慮因素:

  • low_memory=False:避免 pandas 對數據類型的推斷出現不一致
  • parse_dates=['saledate']:將日期字符串轉換為 datetime 對象
  • 導入 fastai.tabular.all 包含處理表格數據所需的全部工具

2.2 初步數據分析

# 查看目標變量的分布
y = df['SalePrice']
plt.hist(y, bins=50)
plt.hist(np.log(y), bins=50)  # 對數變換後的分布更接近正態

# 查看缺失值
df.isnull().sum().sort_values(ascending=False).head(10)

# 查看唯一值數量
df.nunique().sort_values(ascending=False).head(10)

Jeremy 強調了探索性數據分析的重要性:

  • 了解目標變量的分布,看是否需要變換
  • 評估缺失值的數量和模式
  • 了解類別特徵的基數(唯一值數量)

2.3 特徵工程

在 fastbook 中,Jeremy 特別注重從日期中提取有用信息:

# 從銷售日期提取多個時間特徵
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

他解釋這些衍生特徵有助於捕捉時間模式,如季節性效應和長期趨勢。

2.4 數據分割

# 基於時間的分割(未來數據作為驗證集)
dep_var = 'SalePrice'
df_train = df[df.saledate.dt.year < 2011]  # 2011年之前的數據作為訓練集
df_valid = df[df.saledate.dt.year == 2011]  # 2011年數據作為驗證集

# 創建測試數據集
df_test = pd.read_csv(path/'Test.csv', low_memory=False, parse_dates=['saledate'])

Jeremy 強調了在時間序列問題中正確分割數據的重要性:

  • 使用較新的數據作為驗證集,而不是隨機抽樣
  • 這種方法更準確地反映了實際應用中的情況,我們總是用過去的數據預測未來

▌3. 使用 fastai 建立表格模型

3.1 準備數據加載器

# 確定連續變量和類別變量
cat_names = ['SaleYear', 'SaleMonth', 'SaleDay', 'SaleDayOfWeek', 'SaleDayOfYear',
            'ProductSize', 'fiProductClassDesc']
cont_names = ['SalePrice', 'MachineHoursCurrentMeter']
# 實際上會有更多特徵,這裡簡化顯示

# 設置預處理步驟
procs = [Categorify, FillMissing, Normalize]

# 創建 TabularList
data = TabularList.from_df(df_train, path=path, cat_names=cat_names, 
                          cont_names=cont_names, procs=procs, y_names=dep_var)

# 創建 databunch
dls = data.split_by_idx(valid_idx).label_from_df(cols=dep_var).databunch()

Jeremy 解釋了 fastai 的表格數據處理流程:

  1. 識別連續變量和類別變量
  2. 設置適當的預處理步驟:
    • Categorify:將類別變量轉換為整數
    • FillMissing:處理缺失值
    • Normalize:標準化連續變量

3.2 創建和訓練模型

# 創建表格學習器
learn = tabular_learner(dls, metrics=rmse)

# 使用一循環學習率訓練
learn.fit_one_cycle(5, 3e-3)

Jeremy 提到 fastai 的 tabular_learner 默認情況下創建一個由兩部分組成的模型:

  1. 一個處理類別變量的嵌入層
  2. 一個簡單的前馈神經網絡

fit_one_cycle 使用了 Leslie Smith 的一循環學習率策略,這種方法通常比固定學習率表現更好。

3.3 解釋模型結果

# 查看模型性能
learn.show_results()

# 查看最大誤差的樣本
learn.show_top_losses()

Jeremy 強調理解模型錯誤的重要性,特別是檢查那些預測誤差最大的樣本,這有助於發現數據問題或改進模型的方向。


▌4. 使用隨機森林進行表格建模

在 fastbook 和 YouTube 視頻中,Jeremy 特別強調了隨機森林在表格數據上的有效性。

4.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)

# 準備數據
xs, y = df_train[cat_names + cont_names].copy(), df_train[dep_var].copy()

# 對類別特徵進行編碼
for c in cat_names:
    xs[c] = xs[c].astype('category').cat.codes

# 訓練模型
m = rf(xs, y)

Jeremy 講解了隨機森林的關鍵參數:

  • n_estimators:樹的數量
  • max_samples:每棵樹使用的樣本數量
  • max_features:每次分割考慮的特徵比例
  • min_samples_leaf:每個葉節點的最小樣本數
  • n_jobs=-1:使用所有可用 CPU 核心
  • oob_score=True:計算袋外(OOB)評分

4.2 評估隨機森林模型

# 查看 OOB 評分
print(f'OOB score: {m.oob_score_}')

# 預測驗證集
preds = m.predict(xs_valid)
rmse = np.sqrt(((preds - y_valid)**2).mean())
print(f'RMSE: {rmse}')

Jeremy 解釋了袋外(OOB)評分的概念:

  • 每棵樹在訓練時只使用約 2/3 的數據
  • 剩餘 1/3 的"袋外"數據可用於評估
  • OOB 評分提供了對模型泛化能力的良好估計,無需單獨的驗證集

4.3 特徵重要性分析

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

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

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

Jeremy 強調特徵重要性是隨機森林的主要優勢之一,它有助於:

  1. 理解哪些特徵對預測最有影響
  2. 指導進一步的特徵工程
  3. 可用於特徵選擇,移除不重要的特徵

4.4 特徵選擇

# 選擇重要性超過閾值的特徵
to_keep = fi[fi.imp>0.005].cols
xs_imp = xs[to_keep]

# 重新訓練模型
m = rf(xs_imp, y)
print(f'New OOB score: {m.oob_score_}')

Jeremy 解釋了特徵選擇不僅可以簡化模型,還可能提高性能:

  • 移除噪聲特徵
  • 降低計算成本
  • 減少可能的過擬合

▌5. 使用 fastai 創建隨機森林

Jeremy 展示了如何使用 fastai 的高階 API 創建隨機森林:

# 使用 fastai 創建隨機森林
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)

# 使用 fastai 創建的隨機森林進行預測
preds, targets = learn.get_preds()

他解釋了 fastai 的優勢:

  • 自動處理數據預處理(類別編碼、填充缺失值等)
  • 集成了最佳實踐(如一循環學習)
  • 提供了豐富的評估和解釋工具

▌6. 深入理解模型

6.1 部分依賴圖分析

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')

Jeremy 詳細解釋了部分依賴圖的工作原理和解讀方法:

  • 顯示特徵與目標變量之間的關係,控制其他特徵
  • 幫助理解特徵如何影響預測(正相關或負相關)
  • 揭示非線性關係和閾值效應

6.2 SHAP 值解釋單個預測

import shap

# 創建 SHAP 解釋器
explainer = shap.TreeExplainer(m)

# 選擇一個特定樣本
i = 0
row = xs.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 強調,這種解釋能力是隨機森林相對於黑盒深度學習模型的重要優勢。


▌7. 模型參數優化

7.1 樹的數量

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

Jeremy 討論了增加樹數量的效果:

  • 一般而言,更多的樹會提高性能
  • 但存在收益遞減點
  • 通常 20-40 棵樹就足以獲得不錯的結果

7.2 最小葉節點樣本數

# 嘗試不同的 min_samples_leaf 值
for i in [1, 3, 5, 10, 25]:
    m = rf(xs_imp, y, min_samples_leaf=i)
    print(f'min_samples_leaf={i}, OOB score={m.oob_score_}')

Jeremy 解釋了這個參數如何控制模型複雜度:

  • 較小的值允許更複雜的模型(可能導致過擬合)
  • 較大的值創建更簡單的模型(可能導致欠擬合)
  • 需要通過實驗找到最佳平衡點

7.3 隨機森林超參數調優

# 簡單網格搜索
params = {'min_samples_leaf': [1, 3, 5], 'max_features': [0.3, 0.5, 0.7]}
results = {}

for msleaf in params['min_samples_leaf']:
    for mfeat in params['max_features']:
        m = rf(xs_imp, y, min_samples_leaf=msleaf, max_features=mfeat)
        results[(msleaf, mfeat)] = m.oob_score_

# 找出最佳參數組合
best_params = max(results.items(), key=lambda x: x[1])
print(f'Best params: {best_params[0]}, OOB score: {best_params[1]}')

在 YouTube 視頻中,Jeremy 提到隨機森林的超參數調優通常比深度學習模型更簡單:

  • 較少的超參數需要調整
  • 對超參數不太敏感
  • OOB 評分提供快速反饋,無需單獨的驗證循環

▌8. 從隨機森林到梯度提升

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

from sklearn.ensemble import GradientBoostingRegressor

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

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

  1. 訓練方式

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

    • 在競賽中,梯度提升(如 XGBoost、LightGBM)往往能獲得比隨機森林更好的結果
    • 但梯度提升需要更仔細的調參,學習率尤其重要

▌9. 準備競賽提交

# 準備測試數據
xs_test = df_test[xs_imp.columns].copy()
for c in cat_names:
    if c in xs_test.columns:
        xs_test[c] = xs_test[c].astype('category').cat.codes

# 預測測試集
preds = m.predict(xs_test)

# 創建提交文件
sub = pd.DataFrame({'SalesID': df_test.SalesID, 'SalePrice': preds})
sub.to_csv('submission.csv', index=False)

Jeremy 解釋了提交 Kaggle 競賽的典型流程:

  1. 確保測試數據使用與訓練數據相同的預處理
  2. 生成預測
  3. 創建符合競賽格式的提交文件

▌10. 實用技巧與最佳實踐

在 fastbook 和 YouTube 視頻中,Jeremy 分享了一些表格數據建模的重要技巧:

10.1 處理日期特徵

# 從日期提取多個特徵
def add_datepart(df, fldname):
    fld = df[fldname]
    fld_dtype = fld.dtype
    if isinstance(fld_dtype, pd.core.dtypes.dtypes.DatetimeTZDtype):
        fld_dtype = np.datetime64
    
    if not np.issubdtype(fld_dtype, np.datetime64):
        df[fldname] = pd.to_datetime(df[fldname], infer_datetime_format=True)
    
    df[fldname+'_year'] = fld.dt.year
    df[fldname+'_month'] = fld.dt.month
    df[fldname+'_week'] = fld.dt.isocalendar().week
    df[fldname+'_day'] = fld.dt.day
    df[fldname+'_dayofweek'] = fld.dt.dayofweek
    df[fldname+'_dayofyear'] = fld.dt.dayofyear
    df[fldname+'_is_month_end'] = fld.dt.is_month_end
    df[fldname+'_is_month_start'] = fld.dt.is_month_start
    df[fldname+'_is_quarter_end'] = fld.dt.is_quarter_end
    df[fldname+'_is_quarter_start'] = fld.dt.is_quarter_start
    df[fldname+'_is_year_end'] = fld.dt.is_year_end
    df[fldname+'_is_year_start'] = fld.dt.is_year_start
    
    df.drop(fldname, axis=1, inplace=True)

10.2 處理高基數類別特徵

# 對高基數類別使用目標編碼
class TargetEncoder:
    def __init__(self, cols, target_col):
        self.cols = cols
        self.target_col = target_col
        self.encodings = {}
        
    def fit(self, df):
        for col in self.cols:
            self.encodings[col] = df.groupby(col)[self.target_col].mean()
        return self
    
    def transform(self, df):
        df_copy = df.copy()
        for col in self.cols:
            df_copy[col+'_encoded'] = df_copy[col].map(self.encodings[col])
        return df_copy

10.3 使用 OOB 樣本進行模型評估

# 獲取 OOB 預測
preds = np.zeros(len(xs))
for i, (pred, idx) in enumerate(zip(m.oob_prediction_, m.estimators_samples_)):
    mask = ~idx  # 袋外樣本的掩碼
    preds[mask] += pred[mask]
    
# 計算 OOB 評分
oob_score = np.sqrt(((preds - y)**2).mean())

▌11. 總結:表格數據建模的關鍵經驗

Jeremy 在 fastbook 和課程中總結了處理表格數據的主要經驗:

  1. 從簡單開始

    • 首先嘗試隨機森林,它通常是一個很好的基線
    • 只有在需要更高性能時才考慮更複雜的模型
  2. 特徵工程很重要

    • 從日期提取多個特徵
    • 適當處理類別特徵(One-hot 編碼或目標編碼)
    • 處理缺失值(填充或建模)
  3. 利用模型解釋性

    • 使用特徵重要性來理解數據
    • 利用部分依賴圖理解特徵效應
    • 使用 SHAP 值解釋具體預測
  4. 適當的驗證策略

    • 在時間序列問題中使用時間分割
    • 利用 OOB 評分進行快速迭代
  5. 謹慎比較模型

    • 使用相同的數據和前處理
    • 考慮多種評估指標
    • 權衡性能與計算成本

▌參考資料