决策树 >> 随机森林

发布于:2025-08-16 ⋅ 阅读:(21) ⋅ 点赞:(0)

决策树简单示例:天气预测是否打网球

输入数据样本
天气 温度 湿度 有风 打网球
适中
正常
正常
正常
适中
正常

决策树构建过程

步骤1:计算根节点基尼指数
  • 总样本:9个
  • 打网球(是):5个
  • 不打网球(否):4个
  • 基尼指数 = 1 - (5/9)² - (4/9)² ≈ 0.493
步骤2:计算各特征分裂效果
  1. 天气特征(晴/阴/雨):

    • 晴: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
  2. 湿度特征(高/正常):

    • 高:4个样本(1是,3否)→ 基尼=0.375
    • 正常:5个样本(4是,1否)→ 基尼=0.32
    • 加权基尼 ≈ 0.343
  3. 有风特征(是/否):

    • 是:3个样本(1是,2否)→ 基尼≈0.444
    • 否:6个样本(4是,2否)→ 基尼≈0.444
    • 加权基尼 ≈ 0.444
  4. 温度特征(热/适中/冷):

    • 热: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[是]

决策规则总结

  1. IF 天气=阴 THEN 打网球=是
  2. IF 天气=晴 AND 湿度=高 THEN 打网球=否
  3. IF 天气=晴 AND 湿度=正常 THEN 打网球=是
  4. IF 天气=雨 AND 有风=是 THEN 打网球=否
  5. IF 天气=雨 AND 有风=否 THEN 打网球=是

预测过程示例

输入1:天气=阴,温度=热,湿度=高,有风=是
  1. 天气=阴 → 直接输出
输入2:天气=晴,温度=冷,湿度=正常,有风=否
  1. 天气=晴 → 进入湿度分支
  2. 湿度=正常 → 输出
输入3:天气=雨,温度=适中,湿度=高,有风=是
  1. 天气=雨 → 进入有风分支
  2. 有风=是 → 输出

决策树特点在本例体现

  1. 可解释性:决策路径清晰可见
  2. 特征选择:自动忽略不相关特征(本例中温度未参与决策)
  3. 处理混合特征:同时处理类别型(天气)和布尔型(有风)特征
  4. 高效决策:多数情况只需1-2次判断

实际应用中,决策树会处理更复杂的场景:

  • 连续特征(如温度25.6℃)
  • 缺失值处理
  • 更精细的剪枝策略
  • 特征重要性评估(本例中天气最重要)

随机森林:原理、示例与输入输出详解

🌳 核心原理

随机森林(Random Forest)是一种集成学习算法,通过组合多棵决策树提升预测性能。其核心思想是"三个随机":

  1. 随机样本:每棵树使用有放回抽样(Bootstrap)的训练子集
  2. 随机特征:每棵树分裂时只考虑随机子集的特征(通常√n_features)
  3. 随机树:构建多棵弱相关的决策树(通常100-500棵)

预测机制

  • 分类问题:所有树投票决定最终类别
  • 回归问题:所有树输出平均值作为结果
原始数据
随机样本1
随机样本2
随机样本3
决策树1
决策树2
决策树3
投票/平均
最终预测

📊 简单示例:泰坦尼克号生存预测

输入数据(部分)
乘客ID 舱位 性别 年龄 票价 是否幸存
1 3 22 7.25 0
2 1 38 71.3 1
3 3 26 7.92 1
随机森林构建过程
  1. 创建3棵决策树(实际通常100+棵)

  2. 每棵树使用不同数据子集和特征子集

    • 树1:样本[1,2,3,5,7] + 特征[“舱位”,“性别”]
    • 树2:样本[2,3,4,6,8] + 特征[“性别”,“年龄”]
    • 树3:样本[1,4,5,6,9] + 特征[“舱位”,“票价”]
  3. 单棵树决策示例

    graph TD
    A[性别?] -->|女| B[幸存率90%]
    A -->|男| C[舱位?]
    C -->|1等| D[幸存率60%]
    C -->|其他| E[幸存率20%]
    
预测新乘客

输入:舱位=2等, 性别=女, 年龄=28, 票价=45

每棵树预测

  1. 树1:特征[“舱位”,“性别”] → 性别=女 → 幸存
  2. 树2:特征[“性别”,“年龄”] → 性别=女 → 幸存
  3. 树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. 训练过程
  1. 自助采样:创建100个训练子集(每个≈63%原始数据)
  2. 特征随机:每棵树分裂时随机选2个特征(√4=2)
  3. 并行建树:独立构建100棵决策树
3. 预测过程
  1. 新样本输入每棵树
  2. 每棵树输出预测结果(0/1)
  3. 统计100棵树的预测:
    • 70棵树预测幸存 → 70%
    • 30棵树预测遇难 → 30%
  4. 多数投票 → 最终预测"幸存"
4. 特征重要性计算

重要性 = (特征被用于分裂的次数) × (平均基尼减少量)

  • 性别:在85棵树中作为首要分裂特征 → 高重要性
  • 年龄:仅在少数树中参与分裂 → 低重要性

🎯 实际应用场景

  1. 金融风控

    • 输入:收入、负债、信用历史等
    • 输出:贷款违约概率
  2. 医疗诊断

    • 输入:检验指标、症状、病史
    • 输出:疾病预测
  3. 推荐系统

    • 输入:用户行为、商品特征
    • 输出:点击率预测
  4. 图像识别

    • 输入:像素特征(HOG/SIFT)
    • 输出:物体分类

优势总结

  • 自动处理混合类型特征
  • 抗过拟合能力强
  • 输出特征重要性
  • 并行化效率高
  • 无需特征缩放

决策树 vs 随机森林:全面对比解析

核心概念对比
特性 决策树 随机森林
基本定义 单一树状决策模型 多棵决策树的集成(森林)
构建方式 递归划分数据集 1. 有放回采样创建多数据集
2. 每棵树用随机特征子集
决策机制 单一路径决策 多棵树投票(分类)/平均(回归)
过拟合倾向 高(易受噪声影响) 低(树间差异降低过拟合风险)
预测稳定性 低(数据微小变化导致不同树) 高(多树平均减少方差)

工作原理图解

决策树工作流程
graph TD
    A[输入数据] --> B{特征X ≤ 阈值?}
    B -->|是| C{特征Y ≤ 阈值?}
    B -->|否| D[类别B]
    C -->|是| E[类别A]
    C -->|否| F[类别C]
随机森林工作流程
输入数据
树1
树2
树3
...
树N
投票/平均
最终预测

关键差异深度解析

1. 特征处理差异
  • 决策树
    全量考虑所有特征,选择最佳分裂点
    风险:可能被主导特征垄断

  • 随机森林
    每棵树随机选择特征子集(通常√n_features)
    优势:挖掘次要特征价值,增强泛化能力

2. 数据使用差异
决策树 随机森林
数据使用 100%原始数据 自助采样(约63%样本)
未使用数据 OOB(袋外)样本
验证方式 需要独立验证集 用OOB样本验证
3. 性能对比实验(鸢尾花数据集)
指标 决策树 随机森林
准确率 93.3% 96.7%
训练时间(ms) 2.1 15.8
预测时间(μs) 38 210
特征重要性稳定性

注:随机森林牺牲速度换取精度和稳定性


实际应用场景对比

何时用决策树?
  1. 模型可解释性优先
    (如银行拒绝贷款需明确原因)

    # 可视化单棵树
    from sklearn.tree import plot_tree
    plot_tree(model)
    
  2. 低计算资源环境
    (嵌入式设备/实时系统)

  3. 原型快速验证
    (初步探索特征关系)

何时用随机森林?
  1. 预测精度优先
    (医疗诊断/股价预测)

  2. 高维特征数据
    (基因数据/推荐系统)

  3. 自动特征选择

    # 获取特征重要性
    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('随机森林特征重要性')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

关键发现:

  1. 决策树过度依赖Glucose(血糖)特征
  2. 随机森林更均衡考虑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()

网站公告

今日签到

点亮在社区的每一天
去签到