Claude 做的筆記,先記錄一下,之後再來看寫的好不好。
和 上一篇筆記 的不同在於,前篇筆記以 YouTube 影片 為主,Kaggle 和書籍為輔。
本筆記則以 書籍為主,Kaggle 和 YouTube 影片 為輔。
- fastbook 第 9 章 “Tabular Modeling Deep Dive”
- Kaggle 的 Blue Book for Bulldozers 競賽
- YouTube 影片 “Practical Deep Learning for Coders” 中,關於 Bulldozers 的部分
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 的表格數據處理流程:
- 識別連續變量和類別變量
- 設置適當的預處理步驟:
Categorify
:將類別變量轉換為整數FillMissing
:處理缺失值Normalize
:標準化連續變量
3.2 創建和訓練模型
# 創建表格學習器
learn = tabular_learner(dls, metrics=rmse)
# 使用一循環學習率訓練
learn.fit_one_cycle(5, 3e-3)
Jeremy 提到 fastai 的 tabular_learner
默認情況下創建一個由兩部分組成的模型:
- 一個處理類別變量的嵌入層
- 一個簡單的前馈神經網絡
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 強調特徵重要性是隨機森林的主要優勢之一,它有助於:
- 理解哪些特徵對預測最有影響
- 指導進一步的特徵工程
- 可用於特徵選擇,移除不重要的特徵
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)
梯度提升與隨機森林的主要區別:
-
訓練方式:
- 隨機森林:並行訓練獨立的樹
- 梯度提升:順序訓練,每棵新樹專注於修正前面樹的錯誤
-
性能與調參:
- 在競賽中,梯度提升(如 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 競賽的典型流程:
- 確保測試數據使用與訓練數據相同的預處理
- 生成預測
- 創建符合競賽格式的提交文件
▌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 和課程中總結了處理表格數據的主要經驗:
-
從簡單開始:
- 首先嘗試隨機森林,它通常是一個很好的基線
- 只有在需要更高性能時才考慮更複雜的模型
-
特徵工程很重要:
- 從日期提取多個特徵
- 適當處理類別特徵(One-hot 編碼或目標編碼)
- 處理缺失值(填充或建模)
-
利用模型解釋性:
- 使用特徵重要性來理解數據
- 利用部分依賴圖理解特徵效應
- 使用 SHAP 值解釋具體預測
-
適當的驗證策略:
- 在時間序列問題中使用時間分割
- 利用 OOB 評分進行快速迭代
-
謹慎比較模型:
- 使用相同的數據和前處理
- 考慮多種評估指標
- 權衡性能與計算成本