在机器学习的世界里,有一类模型如同自然界的树木般生长 —— 它们从一个根节点出发,通过不断分叉形成复杂的结构,这就是决策树。而当多棵树汇聚成 “森林”,便形成了性能更强大的集成学习模型。本文将从决策树的核心原理出发,解析其构建逻辑与优化方法,深入探讨集成学习的思想,并通过泰坦尼克生还预测实战,展示决策树与集成模型的应用价值,让不同背景的读者都能理解这一重要机器学习分支。

一、决策树的生长逻辑

决策树是一种直观易懂的非线性模型,它模拟人类决策的过程:通过一系列 “是 / 否” 的判断,最终得出结论。比如在相亲场景中,我们可能会先判断 “对方是否有稳定工作”,再根据结果判断 “是否有房”,逐步缩小范围 —— 这就是决策树的工作方式。

1.1 如何找到最佳分裂点?

决策树的核心是每次选择最优特征进行分裂,而衡量 “最优” 的标准主要有两个:信息增益(基于熵)和基尼不纯度。

信息增益与熵(Entropy)

熵是信息论中衡量不确定性的指标,熵值越高,数据的混乱程度越高。对于一个包含多个类别的数据集,其熵的计算公式为:

H(D) = -\sum_{k=1}^{K} p_k \log_2 p_k

其中,p_k 是第 k类样本在数据集 D 中的占比。当所有样本属于同一类别时,熵为 0(完全确定);当两类样本各占 50% 时,熵为 1(最不确定)。

信息增益则是指分裂后熵的减少量,即:

Gain(D, A) = H(D) - \sum_{v=1}^{V} \frac{|D_v|}{|D|} H(D_v)

其中,A 是待选择的特征,D_v是特征 A 取值为 v 的子数据集。信息增益越大,说明使用特征 A 分裂后,数据的不确定性降低得越多,因此应优先选择信息增益最大的特征。

基尼不纯度(Gini Impurity)

基尼不纯度衡量的是从数据集中随机抽取两个样本,其类别不同的概率,计算公式为:

Gini(D) = 1 - \sum_{k=1}^{K} p_k^2

与熵类似,基尼不纯度越小,数据的纯度越高。基于基尼不纯度的特征选择标准是最小化分裂后的基尼不纯度,即选择使子数据集基尼不纯度加权和最小的特征。

实例:用代码计算熵和基尼系数

以一个简单的二分类数据集为例(假设正类占比 60%,负类占比 40%):


import numpy as np

def entropy(p):

"""计算熵(p为正类占比)"""

if p == 0 or p == 1:

return 0

return -p * np.log2(p) - (1 - p) * np.log2(1 - p)

def gini(p):

"""计算基尼不纯度"""

return 1 - p**2 - (1 - p)** 2

# 正类占比60%时

p = 0.6

print(f"熵:{entropy(p):.3f}") # 输出:0.971

print(f"基尼不纯度:{gini(p):.3f}") # 输出:0.480

通过对比不同特征分裂后的信息增益或基尼不纯度,就能找到每次分裂的最优特征。

1.2 递归构建

决策树的构建是一个递归过程,主要步骤如下:

  • 初始化:将所有样本作为根节点的数据集。
  • 选择最优特征:计算当前数据集上所有特征的信息增益(或基尼不纯度),选择最优特征。
  • 分裂节点:根据最优特征的取值,将数据集划分为多个子数据集,每个子数据集对应一个子节点。
  • 递归终止条件:当子数据集所有样本属于同一类别(纯度为 100%),或没有更多特征可分裂时,停止递归,将该节点标记为叶节点,类别为子数据集中多数类。

1.3 防止决策树 “过生长”

决策树如果不加以限制,会不断分裂直到每个叶节点只包含一个样本,这会导致过拟合(对训练数据拟合极好,但泛化能力差)。剪枝是解决这一问题的关键,预剪枝是最常用的方法之一。

预剪枝通过在决策树生长过程中设置限制条件,提前终止分裂,常见策略包括:

  • 最大深度:限制树的最大层数(如max_depth=5)。
  • 最小样本数:当节点样本数少于阈值时,停止分裂(如min_samples_split=10)。
  • 最小信息增益:若分裂带来的信息增益小于阈值,停止分裂。

例如,当设置max_depth=3时,决策树最多只能分裂 3 次,避免了过度复杂的结构。

二、Scikit-learn 核心

Scikit-learn 提供了高效的决策树与集成学习实现,不仅简化了模型训练流程,还支持多种优化策略。

2.1 决策树 Classifier

Scikit-learn 的DecisionTreeClassifier与我们手动实现的决策树核心逻辑一致,但在细节上更优化:

  • 支持多种分裂标准(criterion='gini'或'entropy')。
  • 内置丰富的预剪枝参数(max_depth、min_samples_split等)。
  • 优化了高维数据的分裂效率,支持并行计算。

手写版与内置版的对比

  • 手写版更适合理解原理,但未考虑缺失值处理、连续特征离散化等细节。
  • DecisionTreeClassifier能自动处理连续特征(通过寻找最佳阈值分裂)和缺失值,且计算效率更高。

使用示例:

from sklearn.tree import DecisionTreeClassifier

from sklearn.datasets import load_iris

from sklearn.model_selection import train_test_split

# 加载数据

data = load_iris()

X, y = data.data, data.target

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 初始化决策树(预剪枝:最大深度3,最小分裂样本数5)

dt = DecisionTreeClassifier(criterion='gini', max_depth=3, min_samples_split=5, random_state=42)

dt.fit(X_train, y_train)

# 评估性能

print(f"测试集准确率:{dt.score(X_test, y_test):.3f}") # 输出约0.978

2.2 集成学习:Bagging 与 Boosting

集成学习通过组合多个弱模型(性能略优于随机猜测),形成强模型。其核心逻辑是 “三个臭皮匠顶个诸葛亮”,主要分为 Bagging 和 Boosting 两大流派。

Bagging:并行的 “投票机制”

Bagging(Bootstrap Aggregating)的思想是:通过自助采样(从训练集中随机有放回地抽取样本)生成多个不同的训练集,在每个训练集上训练一个弱模型,最终通过投票(分类)或平均(回归)得到结果。

随机森林(Random Forest) 是 Bagging 的典型代表,它在 Bagging 基础上增加了 “随机特征选择”:每次分裂时,只从随机选择的部分特征中寻找最优分裂点。这进一步降低了模型间的相关性,提升了集成效果。

随机森林的核心参数:

  • n_estimators:树的数量(越多通常性能越好,但计算成本增加)。
  • max_depth:单棵树的最大深度(控制过拟合)。
  • max_features:每次分裂时考虑的最大特征数(默认sqrt(n_features))。
Boosting:串行的 “纠错机制”

Boosting 的思想是:串行训练多个弱模型,每个模型专注于纠正前一个模型的错误。通过给错误分类的样本增加权重,让后续模型更关注难分样本,最终加权组合所有模型的结果。

梯度提升树(GBDT,Gradient Boosting Decision Tree) 是 Boosting 的代表,它通过拟合前一轮模型的残差(预测误差)来不断优化。

GBDT 的核心参数:

  • n_estimators:树的数量(需配合学习率调整)。
  • learning_rate:每个树的贡献权重(较小的学习率通常需要更多树,如learning_rate=0.1)。
  • max_depth:单棵树的深度(通常较小,如 3-5,避免过拟合)。

2.3 随机森林与 GBDT 的核心参数调优

参数调优遵循 “先粗调后细调” 的原则,以随机森林为例:

  • 先固定max_depth=10,调整n_estimators(如从 10 到 200),找到性能稳定的最小值。
  • 固定n_estimators,调整max_depth(如 3-15)和min_samples_split,优化过拟合。
  • 最后微调max_features,进一步提升性能。

GBDT 调优则需注意learning_rate与n_estimators的平衡:降低学习率时,需增加树的数量以保证性能。

三、实战项目 

泰坦尼克号数据集是经典的分类任务数据集,包含乘客的年龄、性别、票价等特征,目标是预测乘客是否生还。我们将分别使用随机森林和 GBDT 进行建模,并对比两者的性能。

3.1 数据预处理

首先加载数据并处理缺失值、转换特征:


import pandas as pd

import numpy as np

from sklearn.model_selection import train_test_split

from sklearn.preprocessing import OneHotEncoder

from sklearn.compose import ColumnTransformer

from sklearn.pipeline import Pipeline

# 加载数据

data = pd.read_csv('titanic.csv')

# 选择特征与目标

X = data[['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']]

y = data['Survived']

# 处理缺失值

X['Age'].fillna(X['Age'].median(), inplace=True) # 年龄用中位数填充

X['Embarked'].fillna(X['Embarked'].mode()[0], inplace=True) # 登船港口用众数填充

# 划分训练集和测试集

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 特征预处理:类别特征独热编码,数值特征保持不变

categorical_features = ['Pclass', 'Sex', 'Embarked']

numeric_features = ['Age', 'SibSp', 'Parch', 'Fare']

preprocessor = ColumnTransformer(

transformers=[

('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features),

('num', 'passthrough', numeric_features)

])

3.2 模型训练与评估

分别训练随机森林和 GBDT,并使用准确率、精确率、召回率和 F1-score 评估性能:


from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

# 随机森林 pipeline

rf_pipeline = Pipeline([

('preprocessor', preprocessor),

('classifier', RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42))

])

rf_pipeline.fit(X_train, y_train)

rf_pred = rf_pipeline.predict(X_test)

# GBDT pipeline

gbdt_pipeline = Pipeline([

('preprocessor', preprocessor),

('classifier', GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42))

])

gbdt_pipeline.fit(X_train, y_train)

gbdt_pred = gbdt_pipeline.predict(X_test)

# 评估函数

def evaluate(y_true, y_pred, model_name):

acc = accuracy_score(y_true, y_pred)

prec = precision_score(y_true, y_pred)

rec = recall_score(y_true, y_pred)

f1 = f1_score(y_true, y_pred)

print(f"{model_name} 评估结果:")

print(f"准确率:{acc:.3f},精确率:{prec:.3f},召回率:{rec:.3f},F1-score:{f1:.3f}\n")

evaluate(y_test, rf_pred, "随机森林")

evaluate(y_test, gbdt_pred, "GBDT")

典型输出

随机森林 评估结果:

准确率:0.817,精确率:0.792,召回率:0.726,F1-score:0.758

GBDT 评估结果:

准确率:0.825,精确率:0.803,召回率:0.742,F1-score:0.771

可见在该任务中,GBDT 性能略优于随机森林,这是因为 Boosting 的纠错机制更适合处理这种存在明显模式但有噪声的数据。

3.3 特征重要性分析

随机森林和 GBDT 都能输出特征重要性,帮助理解模型决策:

import matplotlib.pyplot as plt

# 获取随机森林特征重要性

rf = rf_pipeline.named_steps['classifier']

feature_names = (preprocessor.named_transformers_['cat'].get_feature_names_out(categorical_features).tolist() +

numeric_features)

rf_importance = pd.Series(rf.feature_importances_, index=feature_names).sort_values(ascending=False)

# 可视化

plt.figure(figsize=(10, 6))

rf_importance[:10].plot(kind='barh')

plt.title('随机森林特征重要性')

plt.show()

分析发现,“性别”(女性生还率更高)、“票价”(高票价乘客更可能生还)和 “年龄” 是影响生还的关键特征,这与历史事实一致。

四、最后小结

决策树以其直观性和可解释性成为机器学习的重要工具,而集成学习通过组合多棵树,解决了单棵树易过拟合、稳定性差的问题。随机森林(Bagging)适合处理高维数据和大规模数据集,计算并行高效;GBDT(Boosting)则在中小型数据集上表现更优,但训练成本较高。

在实际应用中,选择模型需结合数据规模、特征类型和业务需求:

  • 若追求解释性和训练速度,单棵决策树是首选。
  • 若需平衡性能与效率,随机森林是可靠选择。
  • 若追求最高性能且能接受较高计算成本,GBDT 或其改进版(如 XGBoost、LightGBM)更合适。

从单棵树到一片森林,集成学习的思想不仅是机器学习的突破,也给我们带来启示:集体的智慧往往超越个体,而如何有效组合个体优势,是提升性能的关键。无论是数据科学还是日常生活,这一思想都同样适用。未完待续.............

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐