Machine Learning: 决策树与随机森林 — 集成思想与高精度分类回归指南
决策树像一棵问答树——每个节点问一个问题,沿着答案走到叶子就是预测结果。
1. 你将学到
- 决策树原理:信息增益/增益率/基尼系数,ID3/C4.5/CART算法族
- 树的剪枝:预剪枝(max_depth/min_samples_leaf)与后剪枝
- 随机森林:Bagging思想、随机特征选择、OOB估计
- 特征重要性分析:mean decrease impurity与permutation importance
- Bob的品类销售分级:用随机森林预测商品是否会成为爆款
2. 一个电商选品经理的真实故事
(1) 痛点:爆款预测全靠经验,命中率不到30%
Bob需要从1 thousand个新品中挑选可能成为爆款的商品,过去全靠经验判断,命中率只有30%。每个爆款平均贡献500 thousand USD月收入,错过一个就损失巨大。经验判断无法量化、无法复用、无法迭代。
(2) 随机森林的解法
随机森林能从历史商品数据中自动发现爆款模式——价格区间、品类特征、首周销量趋势——且天然提供特征重要性排名。
PYTHON
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf.fit(X_train, y_train)
print(f"OOB Accuracy: {rf.oob_score_:.3f}")
print(f"Top feature: {feature_names[rf.feature_importances_.argmax()]}")
(3) 收益:爆款预测命中率从30%提升到65%
Bob用随机森林替代经验判断,爆款预测命中率从30%提升到65%,每月多识别5个爆款,年增收约3 million USD。
3. 决策树原理
(1) 分裂准则
决策树每个节点选择最佳特征和阈值进行分裂,标准有三种:
graph TB
ROOT[Root Node<br/>All Data] --> SPLIT1{Feature: price<br/>Threshold: 50 USD}
SPLIT1 -->|≤ 50| LEFT[Left Child<br/>60% hit products]
SPLIT1 -->|> 50| RIGHT[Right Child<br/>10% hit products]
LEFT --> SPLIT2{Feature: first_week_sales}
SPLIT2 -->|> 500| LEAF1[Leaf: HIT<br/>90% confidence]
SPLIT2 -->|≤ 500| LEAF2[Leaf: NOT HIT<br/>40% confidence]
RIGHT --> LEAF3[Leaf: NOT HIT<br/>5% confidence]
| 准则 | 公式核心 | 偏好 | 算法族 |
|---|---|---|---|
| 信息增益(Information Gain) | $H(D) - H(D | A)$ | 多值特征 |
| 增益率(Gain Ratio) | 信息增益/固有值 | 修正多值偏好 | C4.5 |
| 基尼指数(Gini Impurity) | $1 - \sum p_i^2$ | 大类纯度 | CART |
▶ 示例:决策树分类Iris
PYTHON
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42, stratify=iris.target
)
# Train with pre-pruning
tree = DecisionTreeClassifier(max_depth=3, min_samples_leaf=5, random_state=42)
tree.fit(X_train, y_train)
y_pred = tree.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"Tree depth: {tree.get_depth()}")
print(f"Number of leaves: {tree.get_n_leaves()}")
# Visualize tree
fig, ax = plt.subplots(figsize=(14, 8))
plot_tree(tree, feature_names=iris.feature_names,
class_names=iris.target_names, filled=True, rounded=True, ax=ax)
plt.title("Decision Tree (max_depth=3)")
plt.tight_layout()
plt.savefig("decision_tree.png", dpi=150)
输出:
TEXT
📖 仅展示
# 执行成功
(2) 剪枝策略
▶ 示例:预剪枝参数对比
PYTHON
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
configs = {
"No pruning": DecisionTreeClassifier(random_state=42),
"max_depth=2": DecisionTreeClassifier(max_depth=2, random_state=42),
"max_depth=3": DecisionTreeClassifier(max_depth=3, random_state=42),
"min_samples_leaf=5": DecisionTreeClassifier(min_samples_leaf=5, random_state=42),
"max_depth=3 + min_samples_leaf=5": DecisionTreeClassifier(
max_depth=3, min_samples_leaf=5, random_state=42),
}
for name, model in configs.items():
scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
model.fit(X, y)
print(f"{name:35s}: Accuracy={scores.mean():.3f}, Leaves={model.get_n_leaves()}")
输出:
TEXT
📖 仅展示
# 执行成功
| 剪枝方式 | 参数 | 效果 | 推荐值 |
|---|---|---|---|
| 预剪枝 | max_depth | 限制树深度 | 3-10 |
| 预剪枝 | min_samples_leaf | 叶节点最少样本 | 5-20 |
| 预剪枝 | min_samples_split | 分裂最少样本 | 10-40 |
| 预剪枝 | max_features | 每次分裂考虑特征数 | sqrt(n_features) |
| 后剪枝 | ccp_alpha | 代价复杂度剪枝 | GridSearch选 |
4. 随机森林
(1) Bagging思想
随机森林 = Bagging(Bootstrap Aggregating) + 随机特征选择。
graph TB
DATA[Original Data] --> B1[Bootstrap Sample 1]
DATA --> B2[Bootstrap Sample 2]
DATA --> B3[Bootstrap Sample 3]
DATA --> BN[Bootstrap Sample N]
B1 --> T1[Tree 1<br/>Random Feature Subset]
B2 --> T2[Tree 2<br/>Random Feature Subset]
B3 --> T3[Tree 3<br/>Random Feature Subset]
BN --> TN[Tree N<br/>Random Feature Subset]
T1 --> VOTE[Majority Vote<br/>/ Average]
T2 --> VOTE
T3 --> VOTE
TN --> VOTE
▶ 示例:随机森林分类 + OOB评估
PYTHON
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rf = RandomForestClassifier(
n_estimators=100,
max_depth=None,
oob_score=True,
random_state=42,
)
rf.fit(X_train, y_train)
print(f"OOB Score: {rf.oob_score_:.4f}")
print(f"Test Accuracy: {accuracy_score(y_test, rf.predict(X_test)):.4f}")
print(f"Number of trees: {rf.n_estimators}")
输出:
TEXT
📖 仅展示
# 执行成功
(2) 超参数调优
▶ 示例:随机森林回归 — SalesPredict销售预测
PYTHON
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import mean_absolute_error, r2_score
import numpy as np
rng = np.random.default_rng(42)
n = 500
X = rng.uniform(0, 100, (n, 6))
y = 50 + 0.8 * X[:, 0] + 1.2 * X[:, 1] - 0.5 * X[:, 2] + rng.normal(0, 5, n)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Compare different n_estimators
for n_est in [10, 50, 100, 200]:
rf = RandomForestRegressor(n_estimators=n_est, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
score = rf.score(X_test, y_test)
mae = mean_absolute_error(y_test, rf.predict(X_test))
print(f"n_estimators={n_est:3d}: R²={score:.4f}, MAE={mae:.2f}")
输出:
TEXT
📖 仅展示
# 执行成功
5. 特征重要性分析
(1) Mean Decrease Impurity vs Permutation Importance
▶ 示例:两种特征重要性对比
PYTHON
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
import numpy as np
X, y = load_iris(return_X_y=True)
feature_names = load_iris().feature_names
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
# Method 1: MDI (built-in, fast but biased)
mdi_importance = rf.feature_importances_
# Method 2: Permutation importance (slower but more reliable)
perm_result = permutation_importance(rf, X, y, n_repeats=30, random_state=42, n_jobs=-1)
perm_importance = perm_result.importances_mean
# Compare
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
y_pos = np.arange(len(feature_names))
axes[0].barh(y_pos, mdi_importance, color="#2196F3")
axes[0].set_yticks(y_pos)
axes[0].set_yticklabels(feature_names)
axes[0].set_title("MDI Feature Importance")
axes[1].barh(y_pos, perm_importance, color="#4CAF50")
axes[1].set_yticks(y_pos)
axes[1].set_yticklabels(feature_names)
axes[1].set_title("Permutation Feature Importance")
plt.tight_layout()
plt.savefig("feature_importance.png", dpi=150)
输出:
TEXT
📖 仅展示
# 执行成功
| 维度 | MDI Importance | Permutation Importance |
|---|---|---|
| 计算速度 | 快(训练时计算) | 慢(需重复预测) |
| 偏差 | 高基数特征偏高 | 无偏 |
| 适用范围 | 树模型内置 | 任何模型 |
| 可靠性 | 一般 | 更可靠 |
▶ 示例:Bob的爆款预测特征排名
PYTHON
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 1000
df = pd.DataFrame({
"price_usd": rng.uniform(10, 200, n),
"first_week_sales": rng.integers(10, 2000, n),
"category_trend_score": rng.uniform(0, 1, n),
"ad_budget_k": rng.uniform(1, 50, n),
"review_score": rng.uniform(1, 5, n),
"return_rate": rng.uniform(0, 0.3, n),
"stock_depth": rng.integers(10, 500, n),
})
df["is_hit"] = (
(df["first_week_sales"] > 500)
& (df["category_trend_score"] > 0.6)
& (df["price_usd"] < 100)
).astype(int) | (rng.random(n) < 0.05) # Add 5% noise
X = df.drop(columns=["is_hit"])
y = df["is_hit"]
rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X, y)
importance = pd.DataFrame({
"feature": X.columns,
"importance": rf.feature_importances_,
}).sort_values("importance", ascending=False)
print("Feature Importance for Hit Product Prediction:")
print(importance.to_string(index=False))
输出:
TEXT
📖 仅展示
Feature Importance for Hit Product Prediction:
❓ 常见问题
Q 随机森林为什么比单棵决策树好?
A 两个原因——1) Bagging降低方差(多棵树投票取平均);2) 随机特征选择降低树之间的相关性,让集成效果更好。单棵树容易过拟合。
Q n_estimators越大越好吗?
A 不一定。超过某个值后收益递减,只增加计算时间。通常100-500够用。可以通过OOB score观察何时稳定。
Q OOB Score是什么?
A Out-of-Bag评估。每棵树用约63%的数据训练,剩余37%自然形成验证集。OOB Score是所有树对各自OOB样本预测的平均,等价于交叉验证但免费。
Q 决策树需要标准化吗?
A 不需要。决策树基于特征阈值分裂,不受量纲影响。但随机森林如果包含其他模型(如Pipeline中的Scaler)则需要。
Q 特征重要性都接近0怎么办?
A 说明特征可能对目标确实没有强关系。尝试特征交互、非线性变换,或用permutation importance确认MDI是否存在偏差。
Q 随机森林能处理类别不平衡吗?
A 能。设置class_weight="balanced"或class_weight={0:1, 1:10}。也可以用过采样(SMOTE)或欠采样配合使用。
📖 小节
- 决策树通过分裂准则(信息增益/基尼系数)递归建树,直到满足停止条件
- 剪枝防止过拟合:预剪枝(限制深度/叶节点样本数) + 后剪枝(ccp_alpha)
- 随机森林 = Bagging + 随机特征选择,降低方差,抗过拟合
- OOB Score是随机森林的"免费"交叉验证,替代train/test split评估
- 特征重要性两种方法:MDI(快速但有偏) vs Permutation(慢但可靠)
- 随机森林是ML工具箱的"瑞士军刀"——几乎总是不错的基线选择
📝 作业
- 基础题(难度⭐):用DecisionTreeClassifier对Iris分类,调整max_depth从1到5,画出accuracy vs depth曲线。提示:循环训练+
cross_val_score。 - 进阶题(难度⭐⭐):用RandomForestRegressor对California Housing预测,对比n_estimators=[10,50,100,200]的R²和OOB Score。提示:设置
oob_score=True。 - 挑战题(难度⭐⭐⭐):实现Bob的爆款预测完整流程——生成模拟数据,训练RandomForestClassifier,用permutation_importance分析特征,调整class_weight处理不平衡(爆款占比<10%),输出分类报告。提示:参考第5节的爆款预测示例。