说明:内容来自菜菜的sklearn机器学习和ai生成
回归树
调用对象的参数
class sklearn.tree.DecisionTreeRegressor (criterion=’mse’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, presort=False)
几乎所有参数,属性及接口都和分类树一模一样。需要注意的是,在回归树中,没有标签分布是否均衡的问题,因此没有class_weight这样的参数。
回归树衡量分枝质量的指标,支持的标准有三种:
1)输入"mse"使用均方误差mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为 特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失
2)输入“friedman_mse”使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差
3)输入"mae"使用绝对平均误差MAE(mean absolute error),这种指标使用叶节点的中值来最小化L1损失 属性中最重要的依然是feature_importances_,接口依然是apply, fit, predict, score最核心。
M S E = 1 N ∑ i = 1 N ( f i − y i ) 2 MSE=\frac1N\sum_{i=1}^N(f_i-y_i)^2 MSE=N1i=1∑N(fi−yi)2
其中N是样本数量,i是每一个数据样本,fi是模型回归出的数值,yi是样本点i实际的数值标签。所以MSE的本质, 其实是样本真实数据与回归结果的差异。在回归树中,MSE不只是我们的分枝质量衡量指标,也是我们最常用的衡 量回归树回归质量的指标,当我们在使用交叉验证,或者其他方式获取回归树的结果时,我们往往选择均方误差作 为我们的评估(在分类树中这个指标是score代表的预测准确率)。在回归中,我们追求的是,MSE越小越好。
回归树的接口score返回的是R平方,并不是MSE。R平方被定义如下:
R
2
=
1
−
u
v
u
=
∑
i
=
1
N
(
f
i
−
y
i
)
2
v
=
∑
i
=
1
N
(
y
i
−
y
^
)
2
R^2=1-\frac uv\\u=\sum_{i=1}^N(f_i-y_i)^2\quad v=\sum_{i=1}^N(y_i-\hat{y})^2
R2=1−vuu=i=1∑N(fi−yi)2v=i=1∑N(yi−y^)2
其中u是残差平方和(MSE * N)
预测值和真实值
v是总平方和
真实值和均值
u/v意义的思考
如果v高,说明模型本身分布比较复杂,不容易用回归树预测,波动是比较大的,在分母的位置上,我们认为可以容忍u有较大的值,可以认为是卷子的难度系数
对于u来说,直接衡量了回归树和真实值的计算结果,可以作为打分。当然是值越低越好,也就是说好的回归树是让 R 2 R^2 R2接近于1,u/v接近于0的
u/v越小,这个回归树越好
N是样本数量
i是每一个数据样本
f i f_i fi是模型回归出的数值
y i y_i yi 是样本点i实际的数值标签。
y ^ \hat{y} y^是真实数值标签的平均数。
R平方可以为正为负(如果模型的残差平方和远远大于模型的总平方和,模型非常糟糕,R平方就会为负),而均方误差永远为正。
值得一提的是,虽然均方误差永远为正,但是sklearn当中使用均方误差作为评判标准时,却是计算”负均方误差“(neg_mean_squared_error)。这是因为sklearn在计算模型评估指标的时候,会考虑指标本身的性质,均方误差本身是一种误差,所以被sklearn划分为模型的一种损失(loss),因此在sklearn当中,都以负数表示。真正的均方误差MSE的数值,其实就是neg_mean_squared_error去掉负号的数字。
中间是交叉验证的内容,在另一个文章中交叉验证
实例:一维回归的图像绘制
在二维平面观察决策树是怎样拟合一条曲线的。用回归树来拟合正弦曲线
import numpy as np
from sklearn.tree import DecisionTreeRegressor
import matplotlib.pyplot as plt
#创建一条含有噪声的正弦曲线
rng = np.random.RandomState(1)
X = np.sort(5*rng.rand(80,1),axis = 0)
#从[80,1]变到80
y = np.sin(X).ravel()#将多维数组展平为一维数组
# .ravel()返回的是数组的视图,而.flatten()返回的是数组的拷贝
#每隔五个添加一个噪声
y[::5] += 3 * (0.5 - rng.rand(16))
#实例化和训练模型
regr_1 = DecisionTreeRegressor(max_depth=2)
regr_2 = DecisionTreeRegressor(max_depth=5)
regr_1.fit(X, y)
regr_2.fit(X, y)
X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]#类切片,添加一个新的维度
#回归树传入的数据必须是二维的
y_1 = regr_1.predict(X_test)
y_2 = regr_2.predict(X_test)
plt.figure()
#先绘制散点图
plt.scatter(X, y, s=20, edgecolor="black",c="darkorange", label="data")
plt.plot(X_test, y_1, color="cornflowerblue",label="max_depth=2", linewidth=2)
plt.plot(X_test, y_2, color="yellowgreen", label="max_depth=5", linewidth=2)
plt.xlabel("data")
plt.ylabel("target")
plt.title("Decision Tree Regression")
plt.legend()
plt.show()
手写一个回归树的算法的思路
数据准备:
- 特征选择:选择相关的特征进行建模。
- 数据预处理:包括处理缺失值、标准化等。
构建树:
- 选择最佳分割点:这是算法的核心,包括遍历特征、计算MSE、选择最优分割点。
- 递归分割数据:根据选择的分割点,将数据分为左右子树。
- 确定叶子节点值:当达到停止条件时,计算当前节点数据的平均值作为预测值。
停止条件:
- 达到最大深度
- 样本数小于设定阈值
- 所有样本具有相同的目标值
预测:
- 遍历树结构:根据新数据的特征值,遍历树直到达到叶子节点。
- 返回叶子节点值:将叶子节点的值作为预测结果。
评估和优化:
- 交叉验证:评估模型性能
- 剪枝:减少过拟合
- 超参数调优:如最大深度、最小样本分割数等
class TreeNode:
def __init__(self, feature_index=None, threshold=None, left=None, right=None, value=None):
self.feature_index = feature_index # 特征索引
self.threshold = threshold # 分割点
self.left = left # 左子树
self.right = right # 右子树
self.value = value # 叶子节点值(如果是叶子节点)
class DecisionTreeRegressor:
#深度和最小不可分割的样本数量
def __init__(self, max_depth=None, min_samples_split=2):
self.max_depth = max_depth # 最大深度
self.min_samples_split = min_samples_split # 最小分割样本数
self.root = None # 根节点
def fit(self, X, y):
self.root = self._build_tree(X, y)
def _build_tree(self, X, y, depth=0):
# 检查是否达到了停止条件
if len(X) < self.min_samples_split or depth >= self.max_depth:
return TreeNode(value=np.mean(y))
# 找到最佳分割点
feature_index, threshold = self._best_split(X, y)
if feature_index is None:
return TreeNode(value=np.mean(y))
# 根据最佳分割点分割数据
left_indices = X[:, feature_index] < threshold
right_indices = X[:, feature_index] >= threshold
left_child = self._build_tree(X[left_indices], y[left_indices], depth + 1)
right_child = self._build_tree(X[right_indices], y[right_indices], depth + 1)
# 返回当前节点
return TreeNode(feature_index, threshold, left_child, right_child)
def _best_split(self, X, y):
# 初始化最小误差
best_mse = float('inf')
best_feature_index = None
best_threshold = None
# 遍历所有特征和分割点
for feature_index in range(X.shape[1]):
thresholds = np.unique(X[:, feature_index])
for threshold in thresholds:
# 计算分割后的左右子集
left_indices = X[:, feature_index] < threshold
right_indices = X[:, feature_index] >= threshold
left_y, right_y = y[left_indices], y[right_indices]
# 计算均方误差
mse = (len(left_y) * np.var(left_y) + len(right_y) * np.var(right_y)) / len(y)
# 如果当前分割更好,更新最佳分割点
if mse < best_mse:
best_mse = mse
best_feature_index = feature_index
best_threshold = threshold
return best_feature_index, best_threshold
def predict(self, X):
return np.array([self._predict_one(x) for x in X])
def _predict_one(self, x):
node = self.root
while node.value is None:
if x[node.feature_index] < node.threshold:
node = node.left
else:
node = node.right
return node.value
核心方法最佳分割:
def _find_best_split(self, X, y):
#最佳特征,最好阈值,最好的均方误差
best_feature, best_threshold, best_mse = None, None, float('inf')
#对于每个特征进行遍历
for feature in range(X.shape[1]):
#对于每个特征,我们找出所有唯一的值作为可能的阈值。这减少了需要检查的阈值数量。
thresholds = np.unique(X[:, feature])
#遍历阈值
for threshold in thresholds:
#创建布尔掩码来进行数据分割
#数据比阈值小的
left_mask = X[:, feature] <= threshold
right_mask = ~left_mask
# 如果掩码为0,说明是一个失败的分割,进行下一次分割
if np.sum(left_mask) == 0 or np.sum(right_mask) == 0:
continue
#计算左侧和右侧的均方误差
left_mse = np.mean((y[left_mask] - np.mean(y[left_mask])) ** 2)
right_mse = np.mean((y[right_mask] - np.mean(y[right_mask])) ** 2)
#加权的MSE
mse = (np.sum(left_mask) * left_mse + np.sum(right_mask) * right_mse) / len(y)
#如果这个mse是小于最优的,就进行更新
if mse < best_mse:
best_feature, best_threshold, best_mse = feature, threshold, mse
return best_feature, best_threshold
构建树
这个方法是构建回归树的核心,它通过递归的方式构建整个树结构。让我们深入分析这个方法:
首先,让我们回顾一下_build_tree
方法的代码:
def _build_tree(self, X, y, depth=0):
n_samples, n_features = X.shape
# 检查停止条件
if (depth >= self.max_depth or
n_samples < self.min_samples_split or
np.all(y == y[0])):
return np.mean(y)
best_feature, best_threshold = self._find_best_split(X, y)
if best_feature is None:
return np.mean(y)
left_mask = X[:, best_feature] <= best_threshold
right_mask = ~left_mask
left_tree = self._build_tree(X[left_mask], y[left_mask], depth + 1)
right_tree = self._build_tree(X[right_mask], y[right_mask], depth + 1)
return {
'feature': best_feature,
'threshold': best_threshold,
'left': left_tree,
'right': right_tree
}
现在,让我们一步步解析这个方法:
-
初始化:
n_samples, n_features = X.shape
我们首先获取当前数据集的样本数和特征数。
-
检查停止条件:
if (depth >= self.max_depth or n_samples < self.min_samples_split or np.all(y == y[0])): return np.mean(y)
这里检查三个停止条件:
- 如果达到最大深度
- 如果样本数小于最小分割数
- 如果所有样本的目标值相同
如果满足任何一个条件,我们就停止分割,返回当前节点样本的平均值作为叶子节点的预测值。
-
寻找最佳分割点:
best_feature, best_threshold = self._find_best_split(X, y)
调用
_find_best_split
方法来找到最佳的分割特征和阈值。 -
检查分割是否可能:
if best_feature is None: return np.mean(y)
如果找不到有效的分割(例如,所有特征值都相同),我们也停止分割,返回平均值。
-
分割数据:
left_mask = X[:, best_feature] <= best_threshold right_mask = ~left_mask
根据最佳分割点将数据分为左右两部分。
-
递归构建左右子树:
left_tree = self._build_tree(X[left_mask], y[left_mask], depth + 1) right_tree = self._build_tree(X[right_mask], y[right_mask], depth + 1)
对左右两部分数据递归调用
_build_tree
方法,深度加1。 -
返回节点信息:
return { 'feature': best_feature, 'threshold': best_threshold, 'left': left_tree, 'right': right_tree }
返回一个字典,包含当前节点的分割特征、阈值,以及左右子树。
这个过程的关键点在于:
-
递归构建:树的构建是自顶向下的递归过程。每次分割后,我们对左右子树继续应用相同的构建过程。
-
贪心策略:在每个节点,我们都选择当前最优的分割,而不考虑未来的分割。这是一种局部最优策略。
-
自适应结构:树的结构会根据数据的特点自适应地生长。有些分支可能很深,而其他分支可能很快就到达叶子节点。
-
停止条件的重要性:停止条件防止树过度生长,这对于避免过拟合非常重要。
-
叶子节点的预测值:在叶子节点,我们使用该节点样本的平均值作为预测值。这是因为在回归问题中,均值是平方误差损失下的最优预测。
这个构建过程的结果是一个嵌套的字典结构,每个非叶子节点包含分割信息和子树,叶子节点则直接是一个数值(预测值)。
使用树回归预测
这个过程涉及到如何使用构建好的树结构来对新的数据点进行预测。让我们深入了解这个过程。
首先,让我们回顾一下与预测相关的代码:
def predict(self, X):
return np.array([self._traverse_tree(x, self.tree) for x in X])
def _traverse_tree(self, x, node):
if not isinstance(node, dict):
return node
if x[node['feature']] <= node['threshold']:
return self._traverse_tree(x, node['left'])
else:
return self._traverse_tree(x, node['right'])
现在,让我们详细解析预测过程:
-
预测方法
predict
:- 这个方法接收一个包含多个样本的数据集
X
。 - 它对
X
中的每个样本x
调用_traverse_tree
方法。 - 最后返回一个包含所有预测结果的 numpy 数组。
- 这个方法接收一个包含多个样本的数据集
-
树遍历方法
_traverse_tree
:- 这个方法接收一个单独的样本
x
和当前的树节点node
。 - 它递归地遍历树,直到到达叶子节点。
- 这个方法接收一个单独的样本
-
预测过程详解:
a. 开始于根节点:
- 预测从树的根节点开始。
b. 检查是否为叶子节点:
if not isinstance(node, dict): return node
- 如果当前节点不是字典(即是一个数值),说明我们到达了叶子节点。
- 在这种情况下,直接返回该值作为预测结果。
c. 在内部节点进行决策:
if x[node['feature']] <= node['threshold']: return self._traverse_tree(x, node['left']) else: return self._traverse_tree(x, node['right'])
- 如果是内部节点,我们比较样本在分割特征上的值与阈值。
- 如果小于等于阈值,我们进入左子树。
- 否则,我们进入右子树。
d. 递归遍历:
- 这个过程递归进行,直到到达叶子节点。
e. 返回预测值:
- 当到达叶子节点时,返回该节点存储的值(通常是该节点训练样本的平均值)作为预测结果。
-
预测的特点:
- 路径唯一性: 对于每个输入样本,在树中只有一条唯一的路径从根到叶。
- 快速预测: 预测过程的时间复杂度是 O(log n),其中 n 是树的节点数。这使得决策树在预测阶段非常高效。
- 可解释性: 我们可以跟踪样本在树中的路径,理解为什么会得到特定的预测结果。
-
示例:
假设我们有一个简单的树结构:{ 'feature': 0, 'threshold': 0.5, 'left': { 'feature': 1, 'threshold': 0.3, 'left': 2.5, 'right': 3.7 }, 'right': 4.2 }
对于输入样本
[0.4, 0.2]
:- 首先检查特征 0:0.4 <= 0.5,所以进入左子树。
- 然后检查特征 1:0.2 <= 0.3,所以进入左子树的左子树。
- 到达叶子节点,返回预测值 2.5。
这个预测过程展示了决策树的一个主要优势:预测速度快且直观。每个预测本质上是一系列简单的if-else决策,这使得决策树模型非常适合需要快速决策的实时应用。预测类似于一个二叉树查找。