决策树简单示例:天气预测是否打网球
输入数据样本
天气 | 温度 | 湿度 | 有风 | 打网球 |
---|---|---|---|---|
晴 | 热 | 高 | 否 | 否 |
晴 | 热 | 高 | 是 | 否 |
阴 | 热 | 高 | 否 | 是 |
雨 | 适中 | 高 | 否 | 是 |
雨 | 冷 | 正常 | 否 | 是 |
雨 | 冷 | 正常 | 是 | 否 |
阴 | 冷 | 正常 | 是 | 是 |
晴 | 适中 | 高 | 否 | 否 |
晴 | 冷 | 正常 | 否 | 是 |
决策树构建过程
步骤1:计算根节点基尼指数
- 总样本:9个
- 打网球(是):5个
- 不打网球(否):4个
- 基尼指数 = 1 - (5/9)² - (4/9)² ≈ 0.493
步骤2:计算各特征分裂效果
天气特征(晴/阴/雨):
- 晴:3个样本(2否,1是)→ 基尼=1-(1/3)²-(2/3)²≈0.444
- 阴:2个样本(2是)→ 基尼=0
- 雨:4个样本(2是,2否)→ 基尼=0.5
- 加权基尼 = (3/9)×0.444 + (2/9)×0 + (4/9)×0.5 ≈ 0.333
湿度特征(高/正常):
- 高:4个样本(1是,3否)→ 基尼=0.375
- 正常:5个样本(4是,1否)→ 基尼=0.32
- 加权基尼 ≈ 0.343
有风特征(是/否):
- 是:3个样本(1是,2否)→ 基尼≈0.444
- 否:6个样本(4是,2否)→ 基尼≈0.444
- 加权基尼 ≈ 0.444
温度特征(热/适中/冷):
- 热:3个样本(1是,2否)→ 基尼≈0.444
- 适中:2个样本(1是,1否)→ 基尼=0.5
- 冷:4个样本(3是,1否)→ 基尼=0.375
- 加权基尼 ≈ 0.417
选择天气特征(加权基尼0.333最小,基尼减少量最大:0.493-0.333=0.16)
步骤3:构建决策树(第一层分裂)
graph TD
A[天气?]
A -->|晴| B[继续分裂]
A -->|阴| C[是]
A -->|雨| D[继续分裂]
步骤4:晴天气分支分裂
- 样本:3个(1是,2否)
- 计算各特征:
- 湿度高:2个(全否)→ 基尼=0
- 湿度正常:1个(是)→ 基尼=0
- 加权基尼=0 → 选择湿度特征
graph TD
A[天气?]
A -->|晴| E[湿度?]
A -->|阴| C[是]
A -->|雨| D[继续分裂]
E -->|高| F[否]
E -->|正常| G[是]
步骤5:雨天气分支分裂
- 样本:4个(2是,2否)
- 计算各特征:
- 有风是:2个(全否)→ 基尼=0
- 有风否:2个(全是)→ 基尼=0
- 加权基尼=0 → 选择有风特征
graph TD
A[天气?]
A -->|晴| E[湿度?]
A -->|阴| C[是]
A -->|雨| H[有风?]
E -->|高| F[否]
E -->|正常| G[是]
H -->|是| I[否]
H -->|否| J[是]
最终决策树
graph TD
Start[天气?]
Start -->|晴| Humidity[湿度?]
Start -->|阴| Yes1[是]
Start -->|雨| Windy[有风?]
Humidity -->|高| No1[否]
Humidity -->|正常| Yes2[是]
Windy -->|是| No2[否]
Windy -->|否| Yes3[是]
决策规则总结
- IF 天气=阴 THEN 打网球=是
- IF 天气=晴 AND 湿度=高 THEN 打网球=否
- IF 天气=晴 AND 湿度=正常 THEN 打网球=是
- IF 天气=雨 AND 有风=是 THEN 打网球=否
- IF 天气=雨 AND 有风=否 THEN 打网球=是
预测过程示例
输入1:天气=阴,温度=热,湿度=高,有风=是
- 天气=阴 → 直接输出 是
输入2:天气=晴,温度=冷,湿度=正常,有风=否
- 天气=晴 → 进入湿度分支
- 湿度=正常 → 输出 是
输入3:天气=雨,温度=适中,湿度=高,有风=是
- 天气=雨 → 进入有风分支
- 有风=是 → 输出 否
决策树特点在本例体现
- 可解释性:决策路径清晰可见
- 特征选择:自动忽略不相关特征(本例中温度未参与决策)
- 处理混合特征:同时处理类别型(天气)和布尔型(有风)特征
- 高效决策:多数情况只需1-2次判断
实际应用中,决策树会处理更复杂的场景:
- 连续特征(如温度25.6℃)
- 缺失值处理
- 更精细的剪枝策略
- 特征重要性评估(本例中天气最重要)
随机森林:原理、示例与输入输出详解
🌳 核心原理
随机森林(Random Forest)是一种集成学习算法,通过组合多棵决策树提升预测性能。其核心思想是"三个随机":
- 随机样本:每棵树使用有放回抽样(Bootstrap)的训练子集
- 随机特征:每棵树分裂时只考虑随机子集的特征(通常√n_features)
- 随机树:构建多棵弱相关的决策树(通常100-500棵)
预测机制:
- 分类问题:所有树投票决定最终类别
- 回归问题:所有树输出平均值作为结果
📊 简单示例:泰坦尼克号生存预测
输入数据(部分)
乘客ID | 舱位 | 性别 | 年龄 | 票价 | 是否幸存 |
---|---|---|---|---|---|
1 | 3 | 男 | 22 | 7.25 | 0 |
2 | 1 | 女 | 38 | 71.3 | 1 |
3 | 3 | 女 | 26 | 7.92 | 1 |
… | … | … | … | … | … |
随机森林构建过程
创建3棵决策树(实际通常100+棵)
每棵树使用不同数据子集和特征子集:
- 树1:样本[1,2,3,5,7] + 特征[“舱位”,“性别”]
- 树2:样本[2,3,4,6,8] + 特征[“性别”,“年龄”]
- 树3:样本[1,4,5,6,9] + 特征[“舱位”,“票价”]
单棵树决策示例:
graph TD A[性别?] -->|女| B[幸存率90%] A -->|男| C[舱位?] C -->|1等| D[幸存率60%] C -->|其他| E[幸存率20%]
预测新乘客
输入:舱位=2等, 性别=女, 年龄=28, 票价=45
每棵树预测:
- 树1:特征[“舱位”,“性别”] → 性别=女 → 幸存
- 树2:特征[“性别”,“年龄”] → 性别=女 → 幸存
- 树3:特征[“舱位”,“票价”] → 舱位=2等(幸存率65%)→ 幸存
输出:3棵树全票通过 → 预测幸存
💻 代码实现(Python)
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
# 1. 加载数据
titanic = pd.read_csv('titanic.csv')
X = titanic[['Pclass', 'Sex', 'Age', 'Fare']] # 特征
y = titanic['Survived'] # 标签
# 2. 预处理
X['Sex'] = X['Sex'].map({'male':0, 'female':1}) # 性别编码
X = X.fillna(X.mean()) # 填充缺失值
# 3. 创建随机森林
rf = RandomForestClassifier(
n_estimators=100, # 100棵树
max_features='sqrt', # 每次分裂考虑√4=2个特征
random_state=42
)
# 4. 训练模型
rf.fit(X, y)
# 5. 预测新乘客
new_passenger = [[2, 1, 28, 45]] # 2等舱/女性/28岁/票价45
prediction = rf.predict(new_passenger)
print("幸存预测:", "是" if prediction[0]==1 else "否")
# 6. 特征重要性分析
importance = pd.Series(rf.feature_importances_, index=X.columns)
print("特征重要性:")
print(importance.sort_values(ascending=False))
输出结果
幸存预测: 是
特征重要性:
Sex 0.572
Fare 0.201
Pclass 0.128
Age 0.099
dtype: float64
📌 关键环节解析
1. 输入处理
- 数值特征:年龄/票价直接使用
- 类别特征:性别转换为0/1
- 缺失值:用平均值填充(年龄列)
2. 训练过程
- 自助采样:创建100个训练子集(每个≈63%原始数据)
- 特征随机:每棵树分裂时随机选2个特征(√4=2)
- 并行建树:独立构建100棵决策树
3. 预测过程
- 新样本输入每棵树
- 每棵树输出预测结果(0/1)
- 统计100棵树的预测:
- 70棵树预测幸存 → 70%
- 30棵树预测遇难 → 30%
- 多数投票 → 最终预测"幸存"
4. 特征重要性计算
重要性 = (特征被用于分裂的次数) × (平均基尼减少量)
- 性别:在85棵树中作为首要分裂特征 → 高重要性
- 年龄:仅在少数树中参与分裂 → 低重要性
🎯 实际应用场景
金融风控
- 输入:收入、负债、信用历史等
- 输出:贷款违约概率
医疗诊断
- 输入:检验指标、症状、病史
- 输出:疾病预测
推荐系统
- 输入:用户行为、商品特征
- 输出:点击率预测
图像识别
- 输入:像素特征(HOG/SIFT)
- 输出:物体分类
优势总结:
- 自动处理混合类型特征
- 抗过拟合能力强
- 输出特征重要性
- 并行化效率高
- 无需特征缩放
决策树 vs 随机森林:全面对比解析
核心概念对比
特性 | 决策树 | 随机森林 |
---|---|---|
基本定义 | 单一树状决策模型 | 多棵决策树的集成(森林) |
构建方式 | 递归划分数据集 | 1. 有放回采样创建多数据集 2. 每棵树用随机特征子集 |
决策机制 | 单一路径决策 | 多棵树投票(分类)/平均(回归) |
过拟合倾向 | 高(易受噪声影响) | 低(树间差异降低过拟合风险) |
预测稳定性 | 低(数据微小变化导致不同树) | 高(多树平均减少方差) |
工作原理图解
决策树工作流程
graph TD
A[输入数据] --> B{特征X ≤ 阈值?}
B -->|是| C{特征Y ≤ 阈值?}
B -->|否| D[类别B]
C -->|是| E[类别A]
C -->|否| F[类别C]
随机森林工作流程
关键差异深度解析
1. 特征处理差异
决策树:
全量考虑所有特征,选择最佳分裂点
风险
:可能被主导特征垄断随机森林:
每棵树随机选择特征子集(通常√n_features)
优势
:挖掘次要特征价值,增强泛化能力
2. 数据使用差异
决策树 | 随机森林 | |
---|---|---|
数据使用 | 100%原始数据 | 自助采样(约63%样本) |
未使用数据 | 无 | OOB(袋外)样本 |
验证方式 | 需要独立验证集 | 用OOB样本验证 |
3. 性能对比实验(鸢尾花数据集)
指标 | 决策树 | 随机森林 |
---|---|---|
准确率 | 93.3% | 96.7% |
训练时间(ms) | 2.1 | 15.8 |
预测时间(μs) | 38 | 210 |
特征重要性稳定性 | 低 | 高 |
注:随机森林牺牲速度换取精度和稳定性
实际应用场景对比
何时用决策树?
模型可解释性优先
(如银行拒绝贷款需明确原因)# 可视化单棵树 from sklearn.tree import plot_tree plot_tree(model)
低计算资源环境
(嵌入式设备/实时系统)原型快速验证
(初步探索特征关系)
何时用随机森林?
预测精度优先
(医疗诊断/股价预测)高维特征数据
(基因数据/推荐系统)自动特征选择
# 获取特征重要性 rf.feature_importances_
联合使用示例:糖尿病预测
数据特征
特征 | 类型 | 说明 |
---|---|---|
Pregnancies | 数值 | 怀孕次数 |
Glucose | 数值 | 血糖浓度 |
BloodPressure | 数值 | 血压 |
BMI | 数值 | 身体质量指数 |
Age | 数值 | 年龄 |
代码实现对比
# 决策树实现
from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(max_depth=3)
dt.fit(X_train, y_train)
print("单棵树准确率:", dt.score(X_test, y_test))
# 随机森林实现
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(
n_estimators=100, # 100棵树
max_features='sqrt', # 每次分裂考虑√8≈3个特征
oob_score=True # 启用OOB评估
)
rf.fit(X_train, y_train)
print("随机森林准确率:", rf.score(X_test, y_test))
print("OOB分数:", rf.oob_score_) # 无偏估计
性能对比输出
决策树准确率: 0.74
随机森林准确率: 0.79
OOB分数: 0.77
特征重要性对比
# 决策树特征重要性
dt_importance = dt.feature_importances_
# 随机森林特征重要性
rf_importance = rf.feature_importances_
# 可视化对比
plt.figure(figsize=(10,5))
plt.subplot(121)
plt.bar(features, dt_importance)
plt.title('决策树特征重要性')
plt.subplot(122)
plt.bar(features, rf_importance)
plt.title('随机森林特征重要性')
关键发现:
- 决策树过度依赖Glucose(血糖)特征
- 随机森林更均衡考虑BMI和Age特征
进阶应用技巧
1. 决策树作为随机森林基学习器
# 自定义决策树作为基学习器
from sklearn.tree import DecisionTreeClassifier
base_tree = DecisionTreeClassifier(
min_samples_leaf=5,
class_weight='balanced'
)
rf = RandomForestClassifier(
n_estimators=50,
base_estimator=base_tree # 使用定制树
)
2. 混合解释性方法
# 使用随机森林预测 + 决策树解释
rf_pred = rf.predict(X_test)
# 提取代表性样本
from sklearn.tree import export_text
representative_sample = X_test[10:11]
# 显示单棵树的决策路径
tree_rules = export_text(rf.estimators_[0],
feature_names=feature_names)
print(f"样本预测路径:\n{tree_rules}")
3. 决策树调优指导
# 用随机森林确定最优深度
depths = range(1, 15)
accuracies = []
for d in depths:
rf = RandomForestClassifier(max_depth=d)
rf.fit(X_train, y_train)
accuracies.append(rf.oob_score_)
optimal_depth = depths[np.argmax(accuracies)] # 找到最佳深度
技术选型指南
场景 | 推荐模型 | 原因 |
---|---|---|
需要解释单条预测原因 | 决策树 | 白盒模型,决策路径清晰 |
高精度预测 | 随机森林 | 集成降低方差 |
数据包含大量噪声 | 随机森林 | 多树平均抵消噪声影响 |
实时系统(<10ms响应) | 决策树 | 单树预测速度快 |
特征重要性分析 | 随机森林 | 重要性评估更稳定 |
处理高维稀疏数据 | 随机森林 | 特征随机选择防止过拟合 |
最佳实践:先用决策树快速探索数据关系,再用随机森林构建最终模型,通过
rf.feature_importances_
指导特征工程,最终用单棵决策树解释关键预测。
决策树代码展示:
import numpy as np
from collections import Counter
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 节点类定义
class LeafNode:
"""叶节点类,代表决策树的终止节点"""
def __init__(self, predicted_class):
self.predicted_class = predicted_class # 该叶节点的预测类别
def predict(self, x):
"""预测函数:直接返回叶节点的预测类别"""
return self.predicted_class
def __repr__(self, depth=0):
"""可视化表示"""
indent = " " * depth
return f"{indent}LeafNode(predicted_class={self.predicted_class})"
class DecisionNode:
"""决策节点类,包含分裂规则和子节点"""
def __init__(self, feature, threshold, left, right):
self.feature = feature # 用于分裂的特征索引
self.threshold = threshold # 分裂阈值
self.left = left # 左子树(满足分裂条件的样本)
self.right = right # 右子树(不满足分裂条件的样本)
def predict(self, x):
"""预测函数:根据特征值决定进入左子树还是右子树"""
if x[self.feature] <= self.threshold:
return self.left.predict(x)
else:
return self.right.predict(x)
def __repr__(self, depth=0):
"""可视化表示"""
indent = " " * depth
s = f"{indent}DecisionNode(feature={self.feature}, threshold={self.threshold:.2f})"
s += f"\n{self.left.__repr__(depth + 1)}"
s += f"\n{self.right.__repr__(depth + 1)}"
return s
# 辅助函数
def most_common(y):
"""返回数组中出现频率最高的元素"""
counter = Counter(y)
return counter.most_common(1)[0][0]
def entropy(y):
"""计算标签的熵"""
if len(y) == 0:
return 0
counts = Counter(y)
probs = [count / len(y) for count in counts.values()]
return -sum(p * np.log2(p) for p in probs if p > 0)
def get_candidate_thresholds(feature_values):
"""生成候选分裂阈值(连续特征)"""
unique_vals = np.unique(feature_values)
if len(unique_vals) < 2:
return []
# 取相邻值的中点作为候选阈值
thresholds = (unique_vals[:-1] + unique_vals[1:]) / 2.0
return thresholds
def split(X, y, feature, threshold):
"""根据特征和阈值分割数据集"""
left_mask = X[:, feature] <= threshold
right_mask = ~left_mask
left_X = X[left_mask]
left_y = y[left_mask]
right_X = X[right_mask]
right_y = y[right_mask]
return left_X, left_y, right_X, right_y
def calculate_information_gain(X, y, feature, threshold):
"""计算特定特征和阈值的信息增益"""
# 1. 计算父节点的熵
parent_entropy = entropy(y)
# 2. 根据阈值分割数据
left_mask = X[:, feature] <= threshold
right_mask = ~left_mask
# 3. 计算子节点的熵
n_total = len(y)
n_left = np.sum(left_mask)
n_right = n_total - n_left
if n_left == 0 or n_right == 0:
return 0 # 没有实际分裂
left_entropy = entropy(y[left_mask])
right_entropy = entropy(y[right_mask])
# 4. 计算加权平均熵
weighted_entropy = (n_left / n_total) * left_entropy + (n_right / n_total) * right_entropy
# 5. 信息增益 = 父节点熵 - 加权平均熵
return parent_entropy - weighted_entropy
# 核心函数
def find_best_split(X, y):
"""寻找最佳分裂特征和阈值"""
best_gain = -1 # 信息增益初始值
best_feature = None
best_threshold = None
for feature in range(X.shape[1]): # 遍历所有特征
thresholds = get_candidate_thresholds(X[:, feature]) # 生成候选阈值
for threshold in thresholds:
# 计算分裂后的信息增益
gain = calculate_information_gain(X, y, feature, threshold)
if gain > best_gain:
best_gain = gain
best_feature = feature
best_threshold = threshold
return best_feature, best_threshold, best_gain
def build_tree(X, y, depth=0, max_depth=3, min_samples_split=2):
"""递归构建决策树"""
# 停止条件1:所有样本属于同一类
if len(set(y)) == 1:
return LeafNode(predicted_class=y[0])
# 停止条件2:达到最大深度或样本数不足
if depth >= max_depth or len(X) < min_samples_split:
predicted_class = most_common(y) # 多数类
return LeafNode(predicted_class=predicted_class)
# 步骤2:找到最优分裂特征和阈值
best_feature, best_threshold, best_gain = find_best_split(X, y)
# 分裂数据为左右子节点
left_X, left_y, right_X, right_y = split(X, y, best_feature, best_threshold)
# 递归构建左右子树
left_child = build_tree(left_X, left_y, depth + 1, max_depth, min_samples_split)
right_child = build_tree(right_X, right_y, depth + 1, max_depth, min_samples_split)
# 返回当前决策节点
return DecisionNode(
feature=best_feature,
threshold=best_threshold,
left=left_child,
right=right_child
)
def predict_tree(tree, X):
"""使用决策树进行预测"""
re = []
for x in X:
temp = tree.predict(x)
re.append(temp)
# return np.array([tree.predict(x) for x in X])
return np.array(re)
def visualize_tree(tree, feature_names=None, class_names=None, depth=0):
"""可视化决策树(文本形式)"""
if isinstance(tree, LeafNode):
if class_names is not None and isinstance(class_names, (list, np.ndarray)):
class_str = class_names[tree.predicted_class]
else:
class_str = str(tree.predicted_class)
print(" " * depth + f"└── Predict {class_str}")
else:
if feature_names is not None and isinstance(feature_names, (list, np.ndarray)):
feature_str = feature_names[tree.feature]
else:
feature_str = f"feature_{tree.feature}"
print(" " * depth + f"├── {feature_str} <= {tree.threshold:.2f}")
visualize_tree(tree.left, feature_names, class_names, depth + 1)
print(" " * depth + f"└── {feature_str} > {tree.threshold:.2f}")
visualize_tree(tree.right, feature_names, class_names, depth + 1)
# 主程序
def main():
# 1. 加载鸢尾花数据集
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
class_names = iris.target_names
print("数据集信息:")
print(f"- 样本数: {X.shape[0]}")
print(f"- 特征数: {X.shape[1]}")
print(f"- 类别数: {len(np.unique(y))}")
print(f"- 特征名称: {feature_names}")
print(f"- 类别名称: {class_names}")
print()
# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"训练集大小: {X_train.shape[0]} 样本")
print(f"测试集大小: {X_test.shape[0]} 样本")
print()
# 3. 构建决策树
print("构建决策树...")
max_depth = 3
min_samples_split = 2
tree = build_tree(X_train, y_train, max_depth=max_depth, min_samples_split=min_samples_split)
# 4. 可视化决策树结构
print("\n决策树结构:")
visualize_tree(tree, feature_names, class_names)
# 5. 在训练集和测试集上进行预测
y_train_pred = predict_tree(tree, X_train)
y_test_pred = predict_tree(tree, X_test)
# 6. 计算准确率
train_acc = accuracy_score(y_train, y_train_pred)
test_acc = accuracy_score(y_test, y_test_pred)
print("\n模型性能:")
print(f"训练集准确率: {train_acc:.4f}")
print(f"测试集准确率: {test_acc:.4f}")
# 7. 特征重要性分析(简化版)
print("\n特征重要性分析:")
print("(需要扩展实现以精确计算)")
# 8. 绘制决策边界(简化版)
print("\n决策边界可视化:")
print("(需要扩展实现以绘制二维决策边界)")
if __name__ == "__main__":
main()