十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从零实现逻辑回归决策树:融合线性与树模型的混合分类器

从零实现逻辑回归决策树:融合线性与树模型的混合分类器 简介本资源是一份面向机器学习初学者与课程实践者的决策树构建教学实现聚焦于融合对率回归Logistic Regression原理的决策树构造方法解决传统ID3/C4.5依赖信息增益导致理论抽象、实操门槛高的问题。压缩包共4个文件2个Python脚本、1个数据文本、1张生成的决策树PNG图总大小362KB其中核心代码实现西瓜数据集3.0的字符串属性数值化、连续属性离散化、基于分类正确率递归选取最优划分属性并通过自定义绘图函数生成可视化决策树结构图。已有2841人学习下载配套watermelon3.txt真实数据集与完整可运行流程读者可直接复现从数据预处理、模型评估到树结构生成与保存的全流程亦能快速替换其他离散型数据集进行迁移实验具备清晰的教学逻辑与工程延展性。1. 项目概述当决策树遇上逻辑回归在机器学习的工具箱里决策树和逻辑回归都是我们耳熟能详的“老朋友”。决策树以其直观的树形结构和“if-else”式的推理逻辑成为分类任务中解释性极强的模型而逻辑回归虽然名字里带着“回归”实则是处理二分类问题的经典线性模型它通过Sigmoid函数将线性组合映射为概率输出清晰且稳定。但你是否想过把这两个看似迥异的模型“嫁接”在一起会产生什么样的化学反应这就是“对率回归决策树”的核心思想——它不是简单的模型堆叠而是一种精巧的模型融合与结构创新。简单来说对率回归决策树Logistic Regression Tree, LRT是一种混合模型。它不再使用传统决策树中信息增益、基尼系数等指标来划分节点而是在每个决策树的非叶子节点即内部节点上嵌入一个逻辑回归模型。当数据流经树结构时首先在节点处经过一个逻辑回归模型的“判决”根据其输出的概率值例如是否大于0.5来决定数据应该流向左子树还是右子树直到抵达最终的叶子节点叶子节点则给出最终的类别预测或概率。这相当于用一系列局部的、简单的线性决策边界逻辑回归来逼近复杂的全局非线性边界既保留了决策树的分段处理能力又引入了逻辑回归的连续概率特性往往能获得比单一模型更优的预测性能尤其是在特征间存在复杂交互关系的数据集上。这个项目非常适合已经对Python、Scikit-learn有初步了解并希望深入理解模型本质、动手实现自定义算法的学习者。通过从零构建一个对率回归决策树你将不仅仅调用sklearn.tree.DecisionTreeClassifier的API而是亲手设计树的结构、实现节点的逻辑回归拟合、编写递归的构建与预测流程。这个过程能让你透彻理解集成学习与模型融合的底层逻辑为后续学习梯度提升树如XGBoost、LightGBM它们的思想有相通之处打下坚实的基础。接下来我将带你一步步拆解这个项目的核心设计、关键实现以及那些只有亲手编码才会遇到的“坑”。2. 核心设计思路与模型架构拆解在动手写代码之前我们必须把模型的设计思路理清楚。一个对率回归决策树可以看作一个决策树框架但其每个内部节点的“分裂规则”被替换成了一个逻辑回归模型。这意味着整个模型的构建和预测流程需要重新设计。2.1 为什么是“逻辑回归”“决策树”首先我们需要理解这种组合的优势。传统决策树如CART在每一个节点上会遍历所有特征的所有可能分割点对于连续特征选择一个使得子节点“纯度”提升最大的特征和阈值进行分裂。这种方法简单有效但也有其局限性它产生的决策边界是轴平行的axis-parallel即边界由类似于“特征X 阈值t”这样的规则构成。对于某些数据分布例如需要斜线或更复杂线性边界才能更好分离的情况传统决策树可能需要很深的深度和大量的节点才能近似导致模型复杂、容易过拟合。逻辑回归则通过sigmoid(w^T * x b)计算一个样本属于正类的概率。它的决策边界是w^T * x b 0这是一个在特征空间中的超平面。这个边界可以是斜的能够捕捉特征之间的线性组合关系。将对率回归决策树的核心思想就是在树的每个节点上学习这样一个超平面作为分裂规则。数据根据w^T * x b的值是否大于0对应sigmoid输出是否大于0.5来决定向左还是向右子树传递。这样整棵树就用一系列局部的超平面边界组合成了一个复杂的分段线性分类器其表达能力理论上强于传统的轴平行树。2.2 模型架构与关键组件设计我们的自定义对率回归决策树类需要包含以下几个核心部分节点类LRTNode这是树的基本构建单元。每个节点需要存储以下信息is_leaf: 布尔值标识该节点是否为叶子节点。logistic_model: 如果非叶子节点这里存储一个训练好的逻辑回归模型的参数权重w和偏置b。我们将自己实现一个简单的逻辑回归拟合而不是直接调用sklearn.linear_model.LogisticRegression以加深理解。left_child/right_child: 指向左子节点和右子节点的引用。label或value: 如果是叶子节点存储该叶子节点所代表类别的预测值对于分类任务通常是该节点中多数类的标签或各类别的概率分布。树类LogisticRegressionTree这是主类包含核心方法fit(X, y): 训练方法接收特征矩阵X和标签向量y递归地构建整棵树。predict(X): 预测方法对输入样本遍历树结构得到最终预测类别。predict_proba(X): 可选但推荐预测概率方法输出样本属于各个类别的概率。一些内部辅助方法如_build_tree,_find_best_split这里被逻辑回归拟合替代_make_leaf_node等。递归构建策略这是实现的重点和难点。构建树的过程是一个递归分裂的过程递归终止条件当节点中的样本数量小于某个最小值min_samples_split、树深度达到最大值max_depth、或者节点中样本的类别完全一致纯度达到100%时停止分裂创建叶子节点。节点分裂逻辑对于需要分裂的节点我们用当前节点上的所有样本(X_node, y_node)来训练一个逻辑回归模型。这个模型的目标是尽可能好地区分这些样本尽管它们可能属于多个类别对于多分类问题需要特殊处理我们后续会讲。训练完成后我们得到权重向量w和偏置b。数据划分根据w^T * x b 0的判定将当前节点的样本划分为左子集和右子集。递归调用对左子集和右子集分别递归调用构建函数创建左子树和右子树。这里有一个关键问题逻辑回归是二分类模型如果当前节点的样本包含多个类别多分类问题我们该如何训练这个节点的逻辑回归一个常用的策略是将其转化为“一对多”One-vs-Rest, OvR的形式。即在构建树的每个节点时我们临时将当前节点的多分类问题转化为一个二分类问题将样本数最多的类别作为正类其余所有类别作为负类然后训练逻辑回归模型。这样节点的逻辑回归学习的是“是否属于主流类别”的边界。这种策略简单有效也是我们实现中将采用的方法。3. 关键实现细节与核心代码解析理论清晰后我们进入实战环节。我们将使用Python的NumPy库进行核心的矩阵运算。为了避免循环提高效率我们将尽量使用向量化操作。3.1 实现一个简单的逻辑回归拟合器虽然可以用Scikit-learn但为了理解透彻我们实现一个基于梯度下降的逻辑回归。这里我们采用批量梯度下降。import numpy as np class SimpleLogisticRegression: def __init__(self, learning_rate0.01, n_iters1000): self.lr learning_rate self.n_iters n_iters self.weights None self.bias None def _sigmoid(self, z): # 防止数值溢出对z进行裁剪 z np.clip(z, -500, 500) return 1 / (1 np.exp(-z)) def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) self.bias 0 # 梯度下降 for _ in range(self.n_iters): # 线性模型输出 linear_model np.dot(X, self.weights) self.bias # 通过sigmoid得到预测概率 y_predicted self._sigmoid(linear_model) # 计算梯度 (交叉熵损失函数的梯度) dw (1 / n_samples) * np.dot(X.T, (y_predicted - y)) db (1 / n_samples) * np.sum(y_predicted - y) # 更新参数 self.weights - self.lr * dw self.bias - self.lr * db def predict_proba(self, X): linear_model np.dot(X, self.weights) self.bias return self._sigmoid(linear_model) def predict(self, X, threshold0.5): proba self.predict_proba(X) return (proba threshold).astype(int)注意这个简易实现没有添加正则化如L1/L2在实际复杂数据中容易过拟合。在真正的对率回归决策树节点中由于数据量可能随着树深增加而减少强烈建议在节点逻辑回归中加入L2正则化。这可以通过在梯度计算中添加 (lambda_ * self.weights)项来实现其中lambda_是正则化系数。3.2 对率回归决策树节点与主类的实现接下来是核心部分。我们先定义节点再定义主树类。class LRTNode: def __init__(self, depth0): self.depth depth self.is_leaf False self.logistic_model None # 存储SimpleLogisticRegression实例 self.left_child None self.right_child None self.label None # 叶子节点的预测标签 self.proba None # 叶子节点的类别概率分布用于predict_proba class LogisticRegressionTree: def __init__(self, max_depth5, min_samples_split2, lr_learning_rate0.1, lr_n_iters1000): self.max_depth max_depth self.min_samples_split min_samples_split self.lr_learning_rate lr_learning_rate self.lr_n_iters lr_n_iters self.root None def fit(self, X, y): self.n_classes len(np.unique(y)) self.root self._build_tree(X, y, depth0) def _build_tree(self, X_node, y_node, depth): # 创建当前节点 node LRTNode(depthdepth) # 终止条件检查 if (depth self.max_depth or len(X_node) self.min_samples_split or len(np.unique(y_node)) 1): node.is_leaf True node.label self._compute_leaf_label(y_node) node.proba self._compute_leaf_proba(y_node) return node # 非叶子节点训练逻辑回归作为分裂器 # 策略将多数类作为正类1其他作为负类0 unique, counts np.unique(y_node, return_countsTrue) majority_class unique[np.argmax(counts)] y_binary (y_node majority_class).astype(int) # 训练逻辑回归模型 lr_model SimpleLogisticRegression(learning_rateself.lr_learning_rate, n_itersself.lr_n_iters) # 注意这里可以添加特征标准化有助于逻辑回归收敛 lr_model.fit(X_node, y_binary) node.logistic_model lr_model # 根据逻辑回归预测结果划分数据 pred_direction lr_model.predict(X_node) # 0 - 左子树 1 - 右子树 # 我们约定预测为0即不属于多数类的去左子树预测为1属于多数类的去右子树。 # 但这个约定可以根据实际情况调整关键是划分要一致。 left_mask (pred_direction 0) right_mask (pred_direction 1) # 检查划分是否有效防止某一侧无样本 if np.sum(left_mask) 0 or np.sum(right_mask) 0: # 如果划分无效则退化为叶子节点 node.is_leaf True node.label self._compute_leaf_label(y_node) node.proba self._compute_leaf_proba(y_node) return node # 递归构建左右子树 node.left_child self._build_tree(X_node[left_mask], y_node[left_mask], depth1) node.right_child self._build_tree(X_node[right_mask], y_node[right_mask], depth1) return node def _compute_leaf_label(self, y): # 返回叶子节点中样本数最多的类别 values, counts np.unique(y, return_countsTrue) return values[np.argmax(counts)] def _compute_leaf_proba(self, y): # 计算叶子节点中各类别的经验概率 proba np.zeros(self.n_classes) unique, counts np.unique(y, return_countsTrue) for cls, cnt in zip(unique, counts): proba[cls] cnt proba proba / np.sum(proba) return proba def predict(self, X): predictions [] for sample in X: node self.root while not node.is_leaf: lr_model node.logistic_model # 使用逻辑回归模型预测样本走向 pred lr_model.predict(sample.reshape(1, -1))[0] # 预测0或1 if pred 0: node node.left_child else: node node.right_child predictions.append(node.label) return np.array(predictions) def predict_proba(self, X): probas [] for sample in X: node self.root while not node.is_leaf: lr_model node.logistic_model pred lr_model.predict(sample.reshape(1, -1))[0] if pred 0: node node.left_child else: node node.right_child probas.append(node.proba) return np.array(probas)3.3 实现中的核心要点与避坑指南逻辑回归的收敛性与数据尺度在树的高层节点数据可能分布广泛。逻辑回归的梯度下降对特征尺度敏感。一个非常重要的技巧是在每个节点训练逻辑回归前对该节点的数据X_node进行标准化例如减去均值除以标准差。将标准化参数均值、标准差也保存在节点中在预测时对传入的样本进行同样的变换。这能极大提高逻辑回归模型的训练稳定性和分裂质量。节点分裂的有效性检查代码中有一个关键检查if np.sum(left_mask) 0 or np.sum(right_mask) 0:。这是因为逻辑回归可能对所有样本都预测到同一侧例如当数据在当前节点已经线性可分但划分方向与我们的“多数类为正”的设定可能产生矛盾或者模型权重导致所有sigmoid输出都大于0.5。一旦发生本次分裂无效必须立即终止将当前节点设为叶子节点否则递归会陷入错误。这是区别于传统决策树实现的一个特殊点。多分类处理策略的局限性我们采用了“节点内OvR”的策略。这种方法在根节点或靠近根节点的节点上效果尚可但随着树的下沉节点内数据分布可能严重偏斜将“多数类vs其余”作为分裂目标可能不是最优的。更高级的策略可以是为每个节点动态选择“最具区分度”的两个类别进行逻辑回归训练类似一对一的策略但这会显著增加计算复杂度。对于初次实现采用OvR策略是合理且可行的。递归深度与过拟合max_depth和min_samples_split是控制模型复杂度的关键超参数。由于逻辑回归节点本身具有一定拟合能力对率回归决策树通常比同深度的传统决策树更容易过拟合。因此建议设置相对保守的max_depth例如3-5并配合使用min_samples_split例如10或20或者在节点逻辑回归中加入较强的L2正则化。4. 模型训练、评估与可视化分析实现完核心代码后我们需要在一个真实数据集上测试其性能并与传统决策树进行对比。我们使用Scikit-learn中的葡萄酒分类数据集。from sklearn.datasets import load_wine from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report import matplotlib.pyplot as plt # 加载数据 data load_wine() X, y data.data, data.target X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 训练我们的对率回归决策树 print(训练自定义对率回归决策树...) lrt LogisticRegressionTree(max_depth4, min_samples_split10, lr_learning_rate0.05, lr_n_iters2000) lrt.fit(X_train, y_train) y_pred_lrt lrt.predict(X_test) acc_lrt accuracy_score(y_test, y_pred_lrt) print(f自定义LRT准确率: {acc_lrt:.4f}) print(classification_report(y_test, y_pred_lrt, target_namesdata.target_names)) # 训练Scikit-learn的传统决策树作为基准 print(\n训练Scikit-learn决策树...) dt DecisionTreeClassifier(max_depth4, min_samples_split10, random_state42) dt.fit(X_train, y_train) y_pred_dt dt.predict(X_test) acc_dt accuracy_score(y_test, y_pred_dt) print(fScikit-learn DT准确率: {acc_dt:.4f}) print(classification_report(y_test, y_pred_dt, target_namesdata.target_names))运行上述代码你可以比较两种模型在测试集上的表现。在我的多次试验中对率回归决策树LRT在适当参数下其准确率有时能小幅超越传统决策树尤其是在特征间存在线性相关性的场景下。但更重要的是我们获得了一个具有不同归纳偏好的模型。4.1 决策边界可视化以二维特征为例为了直观理解模型的运作方式我们可以将数据投影到两个最重要的特征上绘制决策边界。from sklearn.decomposition import PCA from matplotlib.colors import ListedColormap # 使用PCA降维至2维以便可视化仅用于演示非模型训练 pca PCA(n_components2) X_train_2d pca.fit_transform(X_train) X_test_2d pca.transform(X_test) # 在降维后的数据上重新训练模型注意这改变了特征空间仅用于可视化理解 lrt_2d LogisticRegressionTree(max_depth3, min_samples_split5, lr_learning_rate0.1, lr_n_iters1000) lrt_2d.fit(X_train_2d, y_train) dt_2d DecisionTreeClassifier(max_depth3, min_samples_split5, random_state42) dt_2d.fit(X_train_2d, y_train) # 创建网格点 x_min, x_max X_train_2d[:, 0].min() - 1, X_train_2d[:, 0].max() 1 y_min, y_max X_train_2d[:, 1].min() - 1, X_train_2d[:, 1].max() 1 xx, yy np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02)) # 绘制决策边界 fig, axes plt.subplots(1, 2, figsize(12, 5)) titles [Logistic Regression Tree (Depth3), Decision Tree (Depth3)] models [lrt_2d, dt_2d] for ax, title, model in zip(axes, titles, models): # 预测网格上每个点的类别 Z model.predict(np.c_[xx.ravel(), yy.ravel()]) Z Z.reshape(xx.shape) # 绘制等高线图决策区域 ax.contourf(xx, yy, Z, alpha0.4, cmapListedColormap([#FFAAAA, #AAFFAA, #AAAAFF])) # 绘制训练数据点 scatter ax.scatter(X_train_2d[:, 0], X_train_2d[:, 1], cy_train, edgecolork, cmapListedColormap([#FF0000, #00FF00, #0000FF])) ax.set_xlabel(PCA Component 1) ax.set_ylabel(PCA Component 2) ax.set_title(title) plt.tight_layout() plt.show()通过对比两张图你可以观察到传统决策树的边界是由水平和垂直线段组成的阶梯状区域而对率回归决策树的边界则是由斜线逻辑回归超平面在二维的投影是直线分段连接而成边界更加平滑能够形成倾斜的分割区域。这直观地验证了我们之前关于模型表达能力的分析。5. 常见问题、优化方向与实战心得在亲手实现和调试这个模型的过程中我遇到了不少典型问题也总结出一些优化思路。5.1 常见问题与排查表问题现象可能原因解决方案训练时逻辑回归损失不下降或变为NaN1. 学习率(lr_learning_rate)设置过大导致梯度爆炸。2. 特征尺度差异巨大导致梯度不稳定。3. 数据未添加偏置项或bias且特征全为0时sigmoid输入为0梯度更新异常。1.大幅降低学习率尝试0.01, 0.001。2.在每个节点内部进行特征标准化零均值单位方差。这是最关键的步骤之一。3. 确保逻辑回归实现中包含了偏置项b。模型预测准确率极低~33%对于三分类1. 树构建过程中节点逻辑回归划分总是失效某侧无样本导致几乎所有节点都提前变成了叶子节点模型退化为简单的多数类预测器。2. 在多分类的“OvR”转换中正负类样本极度不均衡如99:1逻辑回归难以学习。1. 检查_build_tree中分裂有效性判断后的退化逻辑是否正确。2. 在节点逻辑回归的损失函数中引入类别权重或者对负类进行欠采样/对正类过采样仅在节点内进行。3. 尝试调整min_samples_split让节点有更多数据供逻辑回归学习。模型严重过拟合训练集准确率100%测试集很低1. 树深度(max_depth)过大。2. 节点逻辑回归迭代次数(lr_n_iters)过多或学习率太小导致它在该节点数据上“学得太好”记住了噪声。3. 缺乏正则化。1.严格控制树深从浅树如3层开始尝试。2.在节点逻辑回归中加入L2正则化。修改SimpleLogisticRegression.fit中的梯度计算dw (1/n_samples)*np.dot(X.T, (y_pred-y)) (lambda_/n_samples)*self.weights。3. 增加min_samples_split。预测速度非常慢对每个样本进行预测时都需要从根节点遍历到叶子节点并在每个非叶子节点计算一次w^T*xb点积。如果树很深或特征很多预测开销大。1. 这是模型结构固有的代价。可以通过设置更小的max_depth来缓解。2. 在predict和predict_proba中使用矩阵运算一次性处理多个样本批量预测而不是用for循环。这需要重写遍历逻辑稍微复杂但能极大提升效率。5.2 高级优化与扩展方向如果你已经成功实现了基础版本并希望进一步提升可以考虑以下方向引入特征选择在节点训练逻辑回归前可以先用卡方检验、F检验或基于树的方法如ExtraTrees筛选出对当前节点区分度最高的几个特征只用这些特征来训练逻辑回归。这能减少噪声干扰加速训练并提升模型泛化能力。实现代价敏感学习在分类问题中不同类别的误判代价可能不同。可以在计算叶子节点标签_compute_leaf_label时不单纯看样本数量而是考虑误分类代价选择期望代价最小的类别。也可以在节点逻辑回归的损失函数中为不同类别赋予不同的权重。处理缺失值传统决策树有处理缺失值的成熟方法如surrogate splits。在对率回归决策树中可以在节点逻辑回归训练前用该节点数据的均值、中位数或通过其他特征预测来填充缺失值。回归任务扩展本实现专注于分类。将其扩展到回归任务可称为“线性回归树”也很有趣。只需将节点中的逻辑回归模型替换为线性回归模型并将叶子节点的输出从类别标签/概率改为该节点样本目标值的平均值。5.3 个人实操心得最后分享几点从零实现这个模型过程中最深的体会“标准化是逻辑回归的命门”这是我踩过最大的坑。最初没有做节点内标准化模型效果时好时坏极不稳定。一旦在每个节点分裂前加入标准化模型的收敛性和最终性能有了质的飞跃。切记任何基于梯度下降的线性模型在训练前都要考虑特征缩放。终止条件要“宁严勿宽”对率回归决策树比传统树更容易过拟合。我的经验是max_depth的设置应该比你对传统决策树的经验值小1到2层。同时min_samples_split可以设置得大一些比如10或20确保节点有足够的数据来训练一个相对稳定的逻辑回归模型。理解模型的“混合”本质调试过程中不妨单独打印出某个节点逻辑回归的权重并可视化该节点数据的分布和学到的决策边界。这能帮你直观理解这棵树是如何通过多个“小逻辑回归”协作来完成复杂分类的。这种调试方式对理解模型内部工作机制非常有帮助。从“可用”到“好用”的距离我们实现的这个版本是一个教学和原理验证版本。要使其达到生产可用级别还需要大量的工程优化比如使用更高效的优化算法如L-BFGS来拟合节点逻辑回归、添加完整的正则化选项、实现并行化构建、以及用Cython或Numba加速核心循环等。但这个实现过程无疑让你牢牢掌握了模型最核心的精髓。本文还有配套的精品资源点击获取
返回列表