回归算法
明智地修剪分支,通过成本复杂度修剪
https://medium.com/@samybaladram?source=post_page---byline--fbd2836c3bef--------------------------------https://towardsdatascience.com/?source=post_page---byline--fbd2836c3bef-------------------------------- Samy Baladram
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 10 月 10 日
–
对我们喜爱的倒立树的全新视角
towardsdatascience.com
决策树不仅仅限于数据分类——它们同样擅长预测数值!分类树经常占据焦点,但决策树回归器(或回归树)在连续变量预测的世界中是强大且多功能的工具。
虽然我们将讨论回归树构建的机制(这些机制与分类树大致相同),但在这里,我们还将超越分类器文章中介绍的前修剪方法,如“最小样本叶节点”和“最大树深度”。我们将探索最常见的后修剪方法——成本复杂度修剪,它为决策树的成本函数引入了一个复杂度参数。
所有可视化图:作者使用 Canva Pro 创作。优化为移动端显示;在桌面端可能显得过大。
定义
回归决策树是一种使用树状结构预测数值的模型。它根据关键特征拆分数据,从根问题开始并分支。每个节点询问一个特征,继续拆分数据,直到达到叶节点并作出最终预测。要获得结果,你需要从根节点到叶节点,沿着匹配数据特征的路径进行跟踪。
回归决策树通过一系列基于数据的问题来预测数值结果,逐步缩小范围直到最终结果。
📊 使用的数据集
为了演示我们的概念,我们将使用我们的标准数据集。该数据集用于预测某一天访客高尔夫球场的人数,包含天气展望、温度、湿度和风力等变量。
列:‘天气展望’(通过独热编码转为晴天、阴天、雨天),‘温度’(以华氏度表示),‘湿度’(以百分比表示),‘风力’(是/否)和‘玩家数量’(数值型,目标特征)
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Num_Players': [52, 39, 43, 37, 28, 19, 43, 47, 56, 33, 49, 23, 42, 13, 33, 29, 25, 51, 41, 14, 34, 29, 49, 36, 57, 21, 23, 41]
}
df = pd.DataFrame(dataset_dict)
# One-hot encode 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'],prefix='',prefix_sep='')
# Convert 'Wind' column to binary
df['Wind'] = df['Wind'].astype(int)
# Rearrange columns
column_order = ['sunny', 'overcast', 'rain', 'Temperature', 'Humidity', 'Wind', 'Num_Players']
df = df[column_order]
# Split features and target
X, y = df.drop('Num_Players', axis=1), df['Num_Players']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
主要机制
回归决策树通过递归地根据最能减少预测误差的特征来拆分数据。以下是一般过程:
-
从根节点开始,使用整个数据集。
-
选择最小化特定误差指标(如均方误差或方差)的特征来拆分数据。
-
根据拆分创建子节点,每个子节点表示与相应特征值对齐的数据子集。
-
对每个子节点重复步骤 2–3,继续拆分数据直到达到停止条件。
-
为每个叶节点分配一个最终的预测值,通常是该节点中目标值的平均值。
训练步骤
我们将探索决策树算法 CART(分类与回归树)中的回归部分。它构建二叉树,通常遵循以下步骤:
- 从根节点开始,使用所有训练样本。
- 对于数据集中的每个特征:
a. 按升序排序特征值。
b. 将相邻值之间的所有中点视为潜在的拆分点。
总共需要检查 23 个拆分点。
- 对每个潜在拆分点:
a. 计算当前节点的均方误差(MSE)。
b. 计算结果拆分的加权平均误差。
例如,在这里,我们计算了拆分点“温度”值为 73.5 时的均方误差(MSE)的加权平均值。
def calculate_split_mse(X_train, y_train, feature_name, split_point):
# Create DataFrame and sort by feature
analysis_df = pd.DataFrame({
'feature': X_train[feature_name],
'y_actual': y_train
}).sort_values('feature')
# Split data and calculate means
left_mask = analysis_df['feature'] <= split_point
left_mean = analysis_df[left_mask]['y_actual'].mean()
right_mean = analysis_df[~left_mask]['y_actual'].mean()
# Calculate squared differences
analysis_df['squared_diff'] = np.where(
left_mask,
(analysis_df['y_actual'] - left_mean) ** 2,
(analysis_df['y_actual'] - right_mean) ** 2
)
# Calculate MSEs and counts
left_mse = analysis_df[left_mask]['squared_diff'].mean()
right_mse = analysis_df[~left_mask]['squared_diff'].mean()
n_left = sum(left_mask)
n_right = len(analysis_df) - n_left
# Calculate weighted average MSE
weighted_mse = (n_left * left_mse + n_right * right_mse) / len(analysis_df)
# Print results
print(analysis_df)
print(f"\nResults for split at {split_point} on feature '{feature_name}':")
print(f"Left child MSE (n={n_left}, mean={left_mean:.2f}): {left_mse:.2f}")
print(f"Right child MSE (n={n_right}, mean={right_mean:.2f}): {right_mse:.2f}")
print(f"Weighted average MSE: {weighted_mse:.2f}")
# Example usage:
calculate_split_mse(X_train, y_train, 'Temperature', 73.5)
- 在评估所有特征和拆分点后,选择均方误差(MSE)加权平均值最低的一个。
def evaluate_all_splits(X_train, y_train):
"""Evaluate all possible split points using midpoints for all features"""
results = []
for feature in X_train.columns:
data = pd.DataFrame({'feature': X_train[feature], 'y_actual': y_train})
splits = [(a + b)/2 for a, b in zip(sorted(data['feature'].unique())[:-1],
sorted(data['feature'].unique())[1:])]
for split in splits:
left_mask = data['feature'] <= split
n_left = sum(left_mask)
if not (0 < n_left < len(data)): continue
left_mean = data[left_mask]['y_actual'].mean()
right_mean = data[~left_mask]['y_actual'].mean()
left_mse = ((data[left_mask]['y_actual'] - left_mean) ** 2).mean()
right_mse = ((data[~left_mask]['y_actual'] - right_mean) ** 2).mean()
weighted_mse = (n_left * left_mse + (len(data) - n_left) * right_mse) / len(data)
results.append({'Feature': feature, 'Split_Point': split, 'Weighted_MSE': weighted_mse})
return pd.DataFrame(results).round(2)
# Example usage:
results = evaluate_all_splits(X_train, y_train)
print(results)
5. 根据选择的特征和切分点创建两个子节点:
-
左子节点:特征值 <= 切分点的样本
-
右子节点:特征值 > 切分点的样本
6. 对每个子节点递归重复步骤 2 到 5。(继续直到满足停止准则。)
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/60ff1e00eadc6faef39b8fb6a57fe968.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3ad498b5148c0d1e27ed7cf1a7f885e7.png
7. 在每个叶节点,分配该节点中样本的平均目标值作为预测值。
from sklearn.tree import DecisionTreeRegressor, plot_tree
import matplotlib.pyplot as plt
# Train the model
regr = DecisionTreeRegressor(random_state=42)
regr.fit(X_train, y_train)
# Visualize the decision tree
plt.figure(figsize=(26,8))
plot_tree(regr, feature_names=X.columns, filled=True, rounded=True, impurity=False, fontsize=16, precision=2)
plt.tight_layout()
plt.show()
在这个 scikit-learn 的输出中,展示了叶节点和中间节点的样本及其值。
回归/预测步骤
这是回归树如何对新数据进行预测的过程:
1. 从树的顶部(根节点)开始。
2. 在每个决策点(节点):
-
查看特征和切分值。
-
如果数据点的特征值较小或相等,向左走。
-
如果它更大,向右走。
3. 一直向下移动直到到达树的末端(叶节点)。
4. 预测值是该叶节点中存储的平均值。
评估步骤
这个 RMSE 值比虚拟回归器的结果要好得多。
预剪枝与后剪枝
在构建完树之后,我们唯一需要担心的就是如何使树变小,以防止过拟合。通常,修剪的方法可以分为以下几类:
预剪枝
预剪枝,也称为早期停止,是指在训练过程中基于某些预定义的标准,停止决策树的生长。此方法旨在防止树变得过于复杂并导致过拟合。常见的预剪枝技术包括:
-
最大深度:限制树的最大深度。
-
每次切分的最小样本数:要求切分一个节点时,必须满足最小样本数的条件。
-
每个叶节点的最小样本数:确保每个叶节点至少包含一定数量的样本。
-
最大叶节点数:限制树中叶节点的总数。
-
最小纯度减少:仅允许那些减少纯度达到指定值的切分。
这些方法在满足指定条件时停止树的生长,有效地在树的构建阶段进行“修剪”。
(我们之前已经讨论过这些方法,它们在回归问题中完全相同。)
后剪枝
后修剪方法允许决策树生长到最大,然后修剪回去以减少复杂性。这种方法首先构建完整的树,然后移除或合并那些对模型表现贡献不大的分支。一种常见的后修剪技术被称为成本复杂度修剪。
成本复杂度修剪
步骤 1:计算每个节点的不纯度
对于每个中间节点,计算不纯度(回归问题中的 MSE)。然后我们将这个值从小到大排序。
# Visualize the decision tree
plt.figure(figsize=(26,8))
plot_tree(regr, feature_names=X.columns, filled=True, rounded=True, impurity=True, fontsize=16, precision=2)
plt.tight_layout()
plt.show()
在这个 scikit-learn 的输出中,节点的不纯度显示为每个节点的“squared_error”。
我们给这些中间节点(从 A 到 J)命名。然后我们根据它们的 MSE 从小到大进行排序。
步骤 2:通过修剪最弱的链接来创建子树
目标是从均方误差(MSE)最小的节点开始,逐渐将中间节点转化为叶子节点。我们可以基于此创建一个修剪路径。
我们将它们命名为“子树i”,基于它被修剪的次数(i)。从原始树开始,树会在具有最低 MSE 的节点上修剪(从节点 J 开始,M(已经被 J 修剪掉),L,K,依此类推)。
步骤 3:计算每棵子树的总叶子不纯度
对于每一棵子树T,可以计算总叶子不纯度(R(T)):
R(T) = (1/N) Σ I(L) * n_L
其中:
· L 遍历所有叶子节点
· n_L 是叶子L中样本的数量 · N 是树中样本的总数
· I(L) 是叶子L的不纯度(MSE)
我们修剪得越多,总的叶子不纯度就越高。
步骤 4:计算成本函数
为了控制何时停止将中间节点转化为叶子节点,我们首先使用以下公式检查每棵子树T的成本复杂度:
Cost(T) = R(T) + α * |T|
其中:
· R(T) 是总叶子不纯度
· |T| 是子树中的叶子节点数**·** α 是复杂度参数
步骤 5:选择 Alpha
α的值控制我们最终得到哪一棵子树。具有最低成本的子树将是最终的树。
当α较小时,我们更关心准确性(较大的树)。当α较大时,我们更关心简洁性(较小的树)。
虽然我们可以自由设定α,在 scikit-learn 中,你也可以获取最小的α值来获得特定的子树。这被称为有效的α。
这个有效的α 也可以计算出来。
# Compute the cost-complexity pruning path
tree = DecisionTreeRegressor(random_state=42)
effective_alphas = tree.cost_complexity_pruning_path(X_train, y_train).ccp_alphas
impurities = tree.cost_complexity_pruning_path(X_train, y_train).impurities
# Function to count leaf nodes
count_leaves = lambda tree: sum(tree.tree_.children_left[i] == tree.tree_.children_right[i] == -1 for i in range(tree.tree_.node_count))
# Train trees and count leaves for each complexity parameter
leaf_counts = [count_leaves(DecisionTreeRegressor(random_state=0, ccp_alpha=alpha).fit(X_train_scaled, y_train)) for alpha in effective_alphas]
# Create DataFrame with analysis results
pruning_analysis = pd.DataFrame({
'total_leaf_impurities': impurities,
'leaf_count': leaf_counts,
'cost_function': [f"{imp:.3f} + {leaves}α" for imp, leaves in zip(impurities, leaf_counts)],
'effective_α': effective_alphas
})
print(pruning_analysis)
最终备注
预剪枝方法通常更快且更节省内存,因为它们从一开始就防止了树形过大。
后剪枝可能创建出更优的树形结构,因为它会在做出剪枝决定之前考虑整个树的结构。然而,这可能会消耗更多的计算资源。
两种方法的目标都是在模型复杂度和性能之间找到平衡,目的是创建一个能很好地对未见数据进行泛化的模型。选择预剪枝还是后剪枝(或两者结合)通常取决于具体的数据集、当前问题以及可用的计算资源。
在实际应用中,通常会结合使用这些方法,比如先应用一些预剪枝标准来防止树形过大,再使用后剪枝对模型的复杂度进行微调。
🌟 决策树回归器(带成本复杂度剪枝)代码总结
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import root_mean_squared_error
from sklearn.tree import DecisionTreeRegressor
from sklearn.preprocessing import StandardScaler
# Create dataset
dataset_dict = {
'Outlook': ['sunny', 'sunny', 'overcast', 'rain', 'rain', 'rain', 'overcast', 'sunny', 'sunny', 'rain', 'sunny', 'overcast', 'overcast', 'rain', 'sunny', 'overcast', 'rain', 'sunny', 'sunny', 'rain', 'overcast', 'rain', 'sunny', 'overcast', 'sunny', 'overcast', 'rain', 'overcast'],
'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
'Num_Players': [52,39,43,37,28,19,43,47,56,33,49,23,42,13,33,29,25,51,41,14,34,29,49,36,57,21,23,41]
}
df = pd.DataFrame(dataset_dict)
# One-hot encode 'Outlook' column
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
# Convert 'Wind' column to binary
df['Wind'] = df['Wind'].astype(int)
# Split data into features and target, then into training and test sets
X, y = df.drop(columns='Num_Players'), df['Num_Players']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)
# Initialize Decision Tree Regressor
tree = DecisionTreeRegressor(random_state=42)
# Get the cost complexity path, impurities, and effective alpha
path = tree.cost_complexity_pruning_path(X_train, y_train)
ccp_alphas, impurities = path.ccp_alphas, path.impurities
print(ccp_alphas)
print(impurities)
# Train the final tree with the chosen alpha
final_tree = DecisionTreeRegressor(random_state=42, ccp_alpha=0.1)
final_tree.fit(X_train_scaled, y_train)
# Make predictions
y_pred = final_tree.predict(X_test)
# Calculate and print RMSE
rmse = root_mean_squared_error(y_test, y_pred)
print(f"RMSE: {rmse:.4f}")
进一步阅读
关于决策树回归器、成本复杂度剪枝及其在 scikit-learn 中的实现,读者可以参考其官方文档。该文档提供了关于使用方法和参数的全面信息。
技术环境
本文使用 Python 3.7 和 scikit-learn 1.5。虽然讨论的概念通常适用,但具体的代码实现可能会因版本不同而略有差异。
关于插图
除非另有说明,所有图像均由作者创作,并结合了 Canva Pro 授权的设计元素。
𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝙍𝙚𝙜𝙧𝙚𝙨𝙨𝙞𝙤𝙣 𝘼𝙡𝙜𝙤𝙧𝙞𝙩𝙝𝙢𝙨 𝙝𝙚𝙧𝙚:
回归算法
查看列表5 个故事https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/aa7eeaa18e4bb093f5ce4ab9b93a8a27.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/44e6d84e61c895757ff31e27943ee597.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7f3e5f3e2aca2feec035ca92e1bc440a.png
𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:
分类算法
查看列表 8 个故事!https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/6ea70d9d2d9456e0c221388dbb253be8.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7221f0777228e7bcf08c1adb44a8eb76.png
决策树回归与剪枝详解

被折叠的 条评论
为什么被折叠?



