Python回归树模型:从原理到代码实现与剪枝优化

林见深
林见深
发布时间:2026-10-14| 2人浏览过
原创

本文深入解析Python中回归树模型的实现原理,基于CART算法讲解特征分裂与方差计算逻辑。通过完整代码示例,演示如何利用scikit-learn加载波士顿房价数据进行模型训练,并详细阐述预剪枝与后剪枝策略对防止过拟合的作用,帮助读者掌握从理论到工程落地的完整流程。

回归树模型核心原理

回归树模型旨在利用树形结构解决回归预测问题,其基础架构源自经典的决策树。尽管核心结构相似,但根据叶子节点的预测方式不同,可划分为两类:一是叶子节点直接输出一个具体的预测值,这类模型被称为回归树;二是叶子节点对应一个线性模型,最终结果由该线性模型计算得出,称为模型树。

本文重点聚焦于回归树的实现与原理分析。

回归树模型核心机制

CART算法的关键在于每次特征分裂时强制对数据集进行二分处理,无论特征是离散型还是连续型,处理方式保持一致。此外,CART使用基尼指数(Gini Index)而非信息增益或增益比来选择分裂特征。在回归场景下,由于损失函数采用均方差(MSE)而非交叉熵,熵的概念难以直接衡量连续值的准确度,因此基尼指数在此处不适用,分裂标准主要依据均方误差的减少量。

在分类树中,叶子节点代表所属类别,其预测值为落入该节点的训练样本类别的众数。而在回归树中,叶子节点输出的是连续数值,具体为落入该节点的训练样本目标值的均值。此时,评估误差的标准是这些样本的均方差。

在特征划分阈值的选择上,分类树通常优化为仅考虑那些能引起预测类别变化的阈值点。然而,在回归问题中,由于预测值是浮点数,这种针对类别变化的优化策略不再适用。总体而言,回归树的实现逻辑相比分类树更为简洁直接。

Python代码实战与数据预处理

实战环节

首先进行数据加载,本次实验选用scikit-learn库中经典的波士顿房价预测数据集。虽然Kaggle平台上存在类似比赛(如house-prices-advanced-regression-techniques),但其特征维度更高且包含缺失值,需进行大量特征工程。本文使用的数据集质量较高,已预先完成筛选与处理,便于直接应用。

获取数据方面,利用sklearn内置API即可轻松获取,操作简便:


import numpy as np
import pandas as pd
from sklearn.datasets import load_boston
boston = load_boston()

X, y = boston.data, boston.target

查看前几条数据以确认数据形态:

该数据集质量优良,sklearn已代为完成预处理工作,可直接使用。为便于数据传递,需将特征矩阵X与目标向量y合并。由于y为一维数组,无法直接与二维的X拼接,需先对y进行reshape操作重塑形状,再执行合并。


y = y.reshape(-1, 1)
X = np.hstack((X, y))

使用hstack函数可将两个numpy数组横向拼接,对应地vstack用于纵向拼接。合并后,y作为最后一列追加至X之后。数据准备就绪,接下来开始模型构建。

在实现决策树主体逻辑前,先定义两个辅助函数:第一个用于计算一批样本的方差和,第二个用于获取样本均值,即子节点的预测值。


def node_mean(X):
 return np.mean(X[:, -1])


def node_variance(X):
 return np.var(X[:, -1]) * X.shape[0]

随后实现根据阈值拆分数据的函数,此部分逻辑可复用之前的代码:


from collections import defaultdict
def split_dataset(X, idx, thred):
 split_data = defaultdict(list)
 for x in X:
  split_data[x[idx] < thred].append(x)
 return list(split_data.values()), list(split_data.keys())

接下来定义两个关键函数:get_thresholds用于获取分裂阈值,split_variance用于根据阈值拆分数据并返回拆分后的方差和。理论上,回归模型中特征的每个取值均可作为切分点,但需对特征值去重以处理重复值情况。


def get_thresholds(X, i):
 return set(X[:, i].tolist())

# 每次迭代方差优化的底线
MINIMUM_IMPROVE = 2.0
# 每个叶子节点最少样本数
MINIMUM_SAMPLES = 10

def split_variance(dataset, idx, threshold):
 left, right = [], []
 n = dataset.shape[0]
 for data in dataset:
  if data[idx] < threshold:
   left.append(data)
  else:
   right.append(data)
 left, right = np.array(left), np.array(right)
 # 预剪枝
 # 如果拆分结果有一边过少,则返回None,防止过拟合
 if len(left) < MINIMUM_SAMPLES or len(right) < MINIMUM_SAMPLES:
  return None
 # 拆分之后的方差和等于左子树的方差和加上右子树的方差和
 # 因为是方差和而不是均方差,所以可以累加
 return node_variance(left) + node_variance(right)

代码中引入了MINIMUM_SAMPLES参数用于预剪枝。若不限制决策树的生长,叶子节点数量可能与训练样本数一致,导致严重过拟合。因此,通过设定每个节点的最小样本数来限制树的深度,该参数可根据实际需求调整。

随后实现特征与阈值筛选函数,该函数遍历所有可能的特征及其阈值,计算拆分后的方差,从而找出最佳分裂点。


def choose_feature_to_split(dataset):
 n = len(dataset[0])-1
 m = len(dataset)
 # 记录最佳方差,特征和阈值
 var_ = node_variance(dataset)
 bestVar = float('inf')
 feature = -1
 thred = None
 for i in range(n):
  threds = get_thresholds(dataset, i)
  for t in threds:
   # 遍历所有的阈值,计算每个阈值的variance
   v = split_variance(dataset, i, t)
   # 如果v等于None,说明拆分过拟合了,跳过
   if v is None:
    continue
   if v < bestVar:
    bestVar, feature, thred = v, i, t
 # 如果最好的拆分效果达不到要求,那么就不拆分,控制子树的数量
 if var_ - bestVar < MINIMUM_IMPROVE:
  return None, None
 return feature, thred

该函数同样包含预剪枝参数MINIMUM_IMPROVE,用于衡量生成子树带来的收益。若某次分裂带来的方差减少量低于该阈值,则认为收益不足,放弃分裂,这也是控制模型复杂度的预剪枝手段。

完成上述辅助函数后,开始构建决策树。由于当前数据集中未包含特征名称,因此省略了与特征名称相关的逻辑处理。


def create_decision_tree(dataset):
 dataset = np.array(dataset)
 
 # 如果当前数量小于10,那么就不再继续划分了
 if dataset.shape[0] < MINIMUM_SAMPLES:
  return node_mean(dataset)
 
 # 记录最佳拆分的特征和阈值
 fidx, th = choose_feature_to_split(dataset)
 if fidx is None:
  return th
 
 node = {}
 node['feature'] = fidx
 node['threshold'] = th
 
 # 递归建树
 split_data, vals = split_dataset(dataset, fidx, th)
 for data, val in zip(split_data, vals):
  node[val] = create_decision_tree(data)
 return node

进行完整测试前,需将原始数据划分为训练集和测试集。鉴于场景相对简单,此处未设置验证集。

数据划分无需手动实现,直接调用sklearn提供的工具函数即可:


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=23)

该函数主要涉及两个参数:test_size,可为整数(表示测试集样本数)或0-1.0之间的浮点数(表示测试集占比);random_state为随机种子,用于保证结果的可重复性。

输出生成的树结构,由于数据量较大,树结构显得庞大。建树逻辑完成后,剩余部分为预测逻辑。

预测部分的代码与分类树类似,整体逻辑一致,仅去除了feature_names相关的处理。


def classify(node, data):
 key = node['feature']
 pred = None
 thred = node['threshold']

 if isinstance(node[data[key] < thred], dict):
  pred = classify(node[data[key] < thred], data)
 else:
  pred = node[data[key] < thred]
   
 # 放置pred为空,挑选一个叶子节点作为替补
 if pred is None:
  for key in node:
   if not isinstance(node[key], dict):
    pred = node[key]
    break
 return pred

由于现有函数仅支持单条数据预测,为实现批量预测,需额外实现一个predict函数。


def predict(node, X):
 y_pred = []
 for x in X:
  y = classify(node, x)
  y_pred.append(y)
 return np.array(y_pred)

后剪枝策略与效果验证

后剪枝策略

后剪枝(post-prune)有时也被译为事后剪枝,此处沿用英文术语。

回归树的后剪枝思想较为朴素:首先构建一棵尽可能复杂的树,然后利用测试集对其进行修剪。具体逻辑是比较子树存在分叉与合并为单一叶子节点时的误差,若合并后误差更小,则执行剪枝操作。

剪枝过程与建树过程类似,采用自上而下的递归方式执行。

逻辑清晰,直接参考代码实现:


def is_dict(node):
 return isinstance(node, dict)


def prune(node, testData):
 testData = np.array(testData)
 if testData.shape[0] == 0:
  return node
 
 # 拆分数据
 split_data, _ = split_dataset(testData, node['feature'], node['threshold'])
 # 对左右子树递归修剪
 if is_dict(node[0]):
  node[0] = prune(node[0], split_data[0])
 if is_dict(node[1]) and len(split_data) > 1:
  node[1] = prune(node[1], split_data[1])

 # 如果左右都是叶子节点,那么判断当前子树是否需要修剪
 if len(split_data) > 1 and not is_dict(node[0]) and not is_dict(node[1]):
  # 计算修剪前的方差和
  baseError = np.sum(np.power(np.array(split_data[0])[:, -1] - node[0], 2)) + np.sum(np.power(np.array(split_data[1])[:, -1] - node[1], 2))
  # 计算修剪后的方差和
  meanVal = (node[0] + node[1]) / 2
  mergeError = np.sum(np.power(meanVal - testData[:, -1], 2))
  if mergeError < baseError:
   return meanVal
  else:
   return node
 return node

最后,验证剪枝后的模型效果:

从结果图可见,剪枝前测试集的均方差为19.65,剪枝后降至19.48。数值上的提升证明了方法的有效性。尽管由于训练数据较少及预剪枝的存在,后剪枝效果受限,但在实际机器学习工程中,只要方法在可承受代价下能带来明确提升,即具有价值,不应因提升微小而否定其意义。

计算均方差时使用了sklearn库中的mean_square_error函数,该函数专门用于计算两个Numpy数组之间的均方差。

模型总结与意义

总结

回归树模型的相关内容至此结束。我们不仅手动实现了模型,还在真实数据集上进行了实验验证。亲手实现代码的过程将带来深刻的理解与收获。

尽管实际应用中很少单独使用树模型进行回归任务,但回归树模型本身具有极高的学术与工程价值。它是许多高性能模型的基础,如著名的GBDT。在深度学习普及之前,随机森林、GBDT、Adaboost等高效模型均基于树模型构建。可以说,树模型曾占据机器学习的半壁江山,其重要性不言而喻。

本文到此结束,若觉得内容有用,欢迎关注以获取更多技术文章。

以上为Python实现回归树模型的详细内容,更多相关技术资讯请关注本站其他文章。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。