今天的课程主要围绕决策树训练、回归树、随机森林和垃圾邮件识别展开。

前面我们已经学习了交叉验证、下采样、过采样,以及 ID3、C4.5 和 CART 等决策树算法。今天的内容更加偏向代码实践:如何训练一棵决策树、如何筛选参数、如何用回归树预测连续值,以及如何使用随机森林完成垃圾邮件识别。


一、决策树分类模型

决策树是一种结构直观、可解释性较强的机器学习模型。它会根据数据特征不断提出问题,把样本逐步划分到不同分支中,最后在叶节点给出预测结果。

例如,在电信客户流失预测中,模型可能依次提出以下问题:

客户的月消费是否大于100元?
├── 是:客户使用时间是否小于12个月?
│   ├── 是:可能流失
│   └── 否:继续判断其他特征
└── 否:可能不流失

实际训练时,问题和划分条件并不是程序员手动规定的,而是决策树根据训练数据自动寻找的。

可以使用 DecisionTreeClassifier 创建分类决策树:

from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(
    criterion="gini",
    max_depth=6,
    min_samples_split=10,
    min_samples_leaf=3,
    random_state=0
)

主要参数的作用如下:

  • criterion:选择节点划分标准;
  • max_depth:决策树允许达到的最大深度;
  • min_samples_split:一个节点继续划分所需的最少样本数;
  • min_samples_leaf:每个叶节点至少保留的样本数;
  • random_state:固定随机过程,使结果能够复现。

其中,criterion="gini" 表示使用基尼系数选择特征和划分位置。

基尼系数可以简单理解为一个节点中样本的混乱程度。假设一个节点中的样本全部属于同一个类别,那么这个节点非常纯,基尼系数为 0。如果不同类别的样本数量比较接近,节点就比较混乱,基尼系数也会增大。

计算公式为:

Gini = 1 - Σ(pᵢ²)

其中,pᵢ 表示第 i 个类别在当前节点中的样本比例。

决策树会尝试不同的特征和划分位置,然后选择能够让子节点更加纯净的方案。


二、决策树为什么容易过拟合

如果不给决策树设置限制,它可能不断向下划分,直到每个叶节点只剩下很少的样本。

这样做会产生一个现象:训练集准确率非常高,甚至达到 100%,但测试集准确率明显降低。

例如,模型可能记住某些训练样本的偶然特点,而不是真正可以推广到新数据上的规律,这就是过拟合。

我们可以通过几个参数限制树的复杂程度:

model = DecisionTreeClassifier(
    max_depth=6,
    min_samples_split=10,
    min_samples_leaf=3,
    random_state=0
)

max_depth 越小,决策树越简单;min_samples_split 和 min_samples_leaf 越大,节点划分条件越严格。

不过,限制也不能过强。如果树太浅,模型可能连基本规律都没有学会,导致训练集和测试集表现都比较差,这种情况叫作欠拟合。

因此,训练决策树的关键不是让树越复杂越好,而是在欠拟合和过拟合之间寻找合适的平衡。


三、处理客户流失数据的类别不平衡

在电信客户流失数据中,“未流失”客户通常比“流失”客户多。如果两个类别的数量差异很大,模型就可能更加偏向多数类。

例如:

未流失客户:9000人
流失客户:1000人

模型即使把大量客户都预测为“未流失”,也可能得到比较高的准确率。但对于企业来说,真正重要的可能是提前找出会流失的客户。

这时可以使用下采样或者过采样平衡数据,具体内容可以参考上一篇:“机器学习:交叉验证、下采样、过采样和决策树”。

下采样的基本代码如下:

train_data = X_train.copy()
train_data["station"] = y_train

not_churn = train_data[
    train_data["station"] == 0
]

churn = train_data[
    train_data["station"] == 1
]

not_churn_sample = not_churn.sample(
    n=len(churn),
    replace=False,
    random_state=0
)

balanced_data = pd.concat(
    [not_churn_sample, churn]
)

X_train_balanced = balanced_data.drop(
    "station",
    axis=1
)

y_train_balanced = balanced_data["station"]

下采样只能处理训练集,不能处理测试集。测试集需要保留真实的数据分布,否则评价结果无法反映模型在实际数据中的表现。


四、使用交叉验证寻找合适参数

我们无法只凭感觉决定决策树应该有多少层,也很难直接判断叶节点应该保留多少样本。因此,可以准备多组候选参数,然后使用交叉验证比较效果。

首先设置候选值:

split_values = [5, 10, 15, 20, 25]
depth_values = [4, 6, 8, 10, 12, 14]
leaf_values = [1, 2, 3, 5, 8, 10]

然后把参数逐个组合:

import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier

scores = []
params_list = []

for split in split_values:
    for depth in depth_values:
        for leaf in leaf_values:
            model = DecisionTreeClassifier(
                criterion="gini",
                max_depth=depth,
                min_samples_split=split,
                min_samples_leaf=leaf,
                random_state=0
            )

            score = cross_val_score(
                model,
                X_train_balanced,
                y_train_balanced,
                cv=5,
                scoring="recall"
            )

            mean_score = score.mean()

            scores.append(mean_score)
            params_list.append(
                (split, depth, leaf)
            )

这里使用了三层 for 循环,分别遍历:

min_samples_split
max_depth
min_samples_leaf

假设分别有 5、6、6 个候选值,总共需要测试:

5 × 6 × 6 = 180组参数

每组参数又需要进行 5 折交叉验证,所以模型会被训练很多次。

找到最高召回率对应的位置:

best_idx = np.argmax(scores)

best_split, best_depth, best_leaf = params_list[
    best_idx
]

print(
    "最优参数:",
    best_split,
    best_depth,
    best_leaf
)

print(
    f"最优交叉验证召回率:"
    f"{scores[best_idx]:.4f}"
)

这里必须使用 np.argmax(),因为召回率越大越好。

如果评价指标是准确率、召回率、精确率、F1-score 或 R²,一般寻找较大的值;如果评价指标是 MSE、RMSE 或 MAE,则通常寻找较小的值,可以使用 np.argmin()。


五、为什么选择召回率

在客户流失预测中,召回率表示:所有实际会流失的客户中,有多少被模型成功找了出来。

公式为:

Recall = TP / (TP + FN)

其中:

TP:实际流失,并且预测为流失;

FN:实际流失,但预测为不流失。

假设实际有 100 名客户会流失,模型成功发现其中 80 名,那么召回率就是:

80 / 100 = 0.8

不同业务需要关注的评价指标也不同:

希望尽可能发现流失客户:关注召回率;

希望减少正常客户被误判:关注精确率;

希望兼顾精确率和召回率:关注 F1-score;

数据类别比较均衡:可以参考准确率;

希望全面查看分类结果:使用分类报告和混淆矩阵。

这些概念也可以参考:“从线性回归到逻辑回归:一个初学者的机器学习入门笔记”。


六、使用最佳参数重新训练

找到最优参数后,需要重新创建决策树,并使用完整的训练数据进行训练:

model = DecisionTreeClassifier(
    criterion="gini",
    max_depth=best_depth,
    min_samples_split=best_split,
    min_samples_leaf=best_leaf,
    random_state=0,
    class_weight="balanced"
)

model.fit(
    X_train_balanced,
    y_train_balanced
)

然后分别在训练集和测试集上预测:

train_predicted = model.predict(
    X_train_balanced
)

test_predicted = model.predict(
    X_test
)

输出分类报告:

from sklearn.metrics import classification_report

print("训练集结果:")
print(
    classification_report(
        y_train_balanced,
        train_predicted
    )
)

print("测试集结果:")
print(
    classification_report(
        y_test,
        test_predicted
    )
)

如果训练集表现非常好,但测试集表现明显下降,说明模型可能过拟合。

如果训练集和测试集表现都比较差,说明模型可能欠拟合,或者当前特征无法提供足够的信息。

需要注意,class_weight="balanced" 会根据类别数量自动调整权重。如果训练数据已经通过下采样完全平衡,这个参数带来的影响可能比较小。可以分别比较只使用下采样、只使用类别权重以及两者同时使用的结果。


七、决策树可视化

决策树的一大优点是可解释性强。我们可以把训练后的树画出来:

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

fig, ax = plt.subplots(
    figsize=(32, 32)
)

plot_tree(
    model,
    filled=True,
    feature_names=X_train_balanced.columns,
    class_names=["未流失", "流失"],
    rounded=True,
    ax=ax
)

plt.show()

图中的常见信息包括:

当前节点使用的划分特征;特征的判断条件;当前节点的基尼系数;当前节点包含的样本数量;不同类别的样本分布;节点最终预测的类别。

例如:

月消费 <= 85.5
gini = 0.42
samples = 320
value = [210, 110]
class = 未流失

表示当前节点有 320 个训练样本,其中 210 个属于未流失,110 个属于流失。

如果树太深,生成的图片会非常复杂,甚至无法正常阅读。从侧面也说明模型可能划分得过细,存在过拟合风险。


八、回归树

决策树不仅能解决分类问题,也可以用于预测连续数值。

分类树输出离散类别,例如:

流失或未流失
垃圾邮件或正常邮件
患病或未患病

回归树输出连续数值,例如:

房屋价格
商品销量
血压数值
温度

假设我们根据体重和年龄预测血压收缩值:

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import r2_score

data = pd.read_csv(
    "多元回归.csv",
    encoding="gbk"
)

X = data[["体重", "年龄"]]
y = data["血压收缩"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=0
)

model = DecisionTreeRegressor(
    max_depth=4,
    min_samples_leaf=3,
    random_state=0
)

model.fit(X_train, y_train)

predicted = model.predict(X_test)

回归树仍然会不断划分数据,但判断划分好坏的方式与分类树不同。

分类树希望划分后的类别更加纯净,而回归树希望同一个叶节点中的目标值更加接近。叶节点的预测结果通常是该节点中训练样本目标值的平均数。


九、回归树的评价指标

常见的回归评价指标包括 MSE、RMSE、MAE 和 R²。

1. 均方误差 MSE

mse = mean_squared_error(
    y_test,
    predicted
)

MSE 会计算真实值与预测值之间误差的平方平均值。它会对较大的误差给予更严重的惩罚,数值越小越好。

2. 均方根误差 RMSE

import numpy as np

rmse = np.sqrt(mse)

RMSE 是 MSE 开平方后的结果。它与目标变量的单位一致,因此更加直观。

3. 平均绝对误差 MAE

mae = mean_absolute_error(
    y_test,
    predicted
)

MAE 是预测误差绝对值的平均数,对极端误差没有 MSE 那么敏感,数值越小越好。

4. 决定系数 R²

r2 = r2_score(
    y_test,
    predicted
)

R² 通常越接近 1 越好:

接近 1:模型解释能力较好;

接近 0:模型与直接预测平均值差不多;

小于 0:模型可能还不如直接预测平均值。

不应该只在训练数据上评价回归树。决策树很容易记住训练样本,如果训练和测试使用相同数据,得到的结果可能非常高,却不能代表模型预测新数据的能力。


十、随机森林

单棵决策树容易受到训练数据变化的影响。如果数据稍有改变,生成的树结构也可能发生明显变化。

随机森林通过训练多棵不同的决策树,再综合它们的预测结果,提高模型稳定性。

分类问题通常采用投票方式:

第1棵树:垃圾邮件
第2棵树:正常邮件
第3棵树:垃圾邮件
第4棵树:垃圾邮件
第5棵树:正常邮件

最终结果:垃圾邮件

创建随机森林:

from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    min_samples_split=4,
    min_samples_leaf=2,
    random_state=0,
    n_jobs=-1
)

主要参数如下:

n_estimators:森林中决策树的数量;

max_depth:每棵树的最大深度;

min_samples_split:节点继续划分所需的最少样本数;

min_samples_leaf:每个叶节点至少保留的样本数;

max_features:每次划分时查看的特征数量;

bootstrap:是否通过有放回抽样构造训练数据;

class_weight:是否调整不同类别的权重;

n_jobs=-1:使用全部可用 CPU 核心训练;

random_state:固定随机过程。


十一、随机森林为什么更加稳定

随机森林主要通过两种随机性增加不同树之间的差异。

第一种是样本随机。每棵树不会直接使用完全相同的训练数据,而是通过有放回抽样获得自己的训练样本。

第二种是特征随机。在节点划分时,每棵树只从部分随机特征中选择划分条件,而不是每次查看所有特征。

如果所有树使用相同样本和相同特征,最后可能生成大量相似的决策树,综合结果就没有太大意义。

正是由于样本和特征具有随机性,每棵树会从不同角度学习数据。某一棵树可能判断错误,但多棵树投票后,个别错误的影响就会被削弱。

不过,随机森林也不是完全不会过拟合。如果每棵树都非常深、训练数据存在泄漏或者特征中包含答案信息,随机森林仍然可能得到不真实的高分。


十二、随机森林识别垃圾邮件

spambase.csv 中保存了邮件的统计特征和分类标签。可以使用随机森林判断一封邮件是否属于垃圾邮件。

首先读取数据:

import pandas as pd

data = pd.read_csv("spambase.csv")

X = data.iloc[:, :-1]
y = data.iloc[:, -1]

划分训练集和测试集:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=0,
    stratify=y
)

stratify=y 可以让训练集和测试集中的垃圾邮件比例尽量保持一致,避免某个集合中的某类样本过少。

创建并训练模型:

from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    min_samples_split=4,
    min_samples_leaf=2,
    random_state=0,
    n_jobs=-1
)

model.fit(X_train, y_train)

进行预测:

train_predicted = model.predict(
    X_train
)

test_predicted = model.predict(
    X_test
)

输出评价结果:

from sklearn.metrics import classification_report

print("训练集结果:")
print(
    classification_report(
        y_train,
        train_predicted
    )
)

print("测试集结果:")
print(
    classification_report(
        y_test,
        test_predicted
    )
)

如果训练集得分非常高,而测试集得分明显偏低,通常说明模型可能过拟合。可以尝试:

减小 max_depth;

增大 min_samples_split;

增大 min_samples_leaf;

增加有效训练数据;

删除无意义或者存在泄漏的特征;

使用交叉验证选择参数。


十三、垃圾邮件识别中的评价重点

垃圾邮件识别中,不能只看准确率。

如果把垃圾邮件设置为正类,那么:

召回率低:很多垃圾邮件没有被拦截;

精确率低:很多正常邮件被错误拦截;

F1-score:综合反映精确率和召回率。

两种错误产生的影响不同:

正常邮件被判断为垃圾邮件
→ 用户可能错过重要信息

垃圾邮件被判断为正常邮件
→ 用户会收到广告、诈骗或骚扰内容

因此,需要根据系统实际需求决定更重视精确率还是召回率。

还可以查看混淆矩阵:

from sklearn.metrics import confusion_matrix

matrix = confusion_matrix(
    y_test,
    test_predicted
)

print(matrix)

混淆矩阵可以直接显示:

真正例、假正例
假负例、真正例

它比单独的准确率更容易看出模型具体错在什么地方。


十四、查看随机森林特征重要性

训练完成后,可以通过 feature_importances_ 查看各个特征的重要程度:

importance = model.feature_importances_
feature_names = X.columns

importance_data = pd.DataFrame({
    "feature": feature_names,
    "importance": importance
})

importance_data = importance_data.sort_values(
    "importance",
    ascending=False
)

print(importance_data.head(15))

绘制特征重要性图:

import matplotlib.pyplot as plt

top15 = importance_data.head(15)

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

plt.barh(
    top15["feature"],
    top15["importance"]
)

plt.gca().invert_yaxis()
plt.xlabel("特征重要性")
plt.title("随机森林特征重要性 Top 15")
plt.tight_layout()
plt.show()

特征重要性可以帮助我们理解模型主要使用了哪些信息,也可以作为特征筛选的参考。

但需要注意,特征重要性描述的是特征在当前模型中的贡献,并不能直接证明某个特征与垃圾邮件之间存在因果关系。


十五、可以使用 GridSearchCV 简化参数搜索

三层 for 循环可以帮助初学者理解参数组合过程,但参数较多时代码会比较长。

Scikit-learn 提供了 GridSearchCV 自动完成网格搜索:

from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

param_grid = {
    "max_depth": [None, 5, 10, 15],
    "min_samples_split": [2, 4, 8],
    "min_samples_leaf": [1, 2, 4]
}

model = RandomForestClassifier(
    n_estimators=100,
    random_state=0,
    n_jobs=-1
)

grid_search = GridSearchCV(
    estimator=model,
    param_grid=param_grid,
    scoring="recall",
    cv=5,
    n_jobs=-1
)

grid_search.fit(
    X_train,
    y_train
)

print("最优参数:")
print(grid_search.best_params_)

print("最优交叉验证分数:")
print(grid_search.best_score_)

得到最佳模型:

best_model = grid_search.best_estimator_

test_predicted = best_model.predict(
    X_test
)

网格搜索本质上仍然是在尝试不同参数组合,只是把循环、交叉验证和最佳参数选择封装了起来。


十六、总结

今天的课程从单棵决策树逐步学习到了随机森林,并完成了客户流失预测、血压数值预测和垃圾邮件识别等练习。

决策树通过特征不断划分数据,结构直观且容易解释,但容易出现过拟合。通过限制树的深度、节点划分样本数和叶节点样本数,可以控制模型复杂度。

回归树与分类树的结构相似,但分类树输出类别,回归树输出连续数值。评价回归模型时,可以使用 MSE、RMSE、MAE 和 R²。

随机森林会训练多棵具有差异的决策树,再通过投票得到分类结果。它利用随机样本和随机特征降低单棵决策树的不稳定性,通常拥有更好的泛化能力。

在垃圾邮件识别中,除了准确率,还需要关注精确率、召回率、F1-score 和混淆矩阵。模型评价不能只看一个分数,而应该结合实际业务分析不同错误带来的影响。

完整的模型训练过程应该是:

读取数据
→ 划分训练集和测试集
→ 只对训练集进行采样
→ 使用交叉验证选择参数
→ 使用最佳参数重新训练
→ 在测试集上进行最终评价
→ 分析过拟合和错误类型

更多推荐