ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机器学习实战:决策树训练、回归树、随机森林与垃圾邮件识别

机器学习实战:决策树训练、回归树、随机森林与垃圾邮件识别

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

前面我们已经学习了交叉验证、下采样、过采样,以及 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_splitmin_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 和混淆矩阵。模型评价不能只看一个分数,而应该结合实际业务分析不同错误带来的影响。

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

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