决策树(Decision Tree)是机器学习中最经典、最具解释性的模型之一,也是理解更高级集成模型(如随机森林、XGBoost)的绝对基石。
模块一:直觉与基础(浅层概念与物理意义)
决策树的名字听起来很有极客感,但它的底层逻辑其实极其古老且符合人类直觉。本质上,决策树就是一套"If-Then"(如果-那么)的规则系统。
1.1 生活中的决策树:人类的决策逻辑
为了理解决策树,我们先来看一个日常生活中最经典的例子:相亲决策。
假设你有一套自己的相亲标准,当有人给你介绍对象时,你的大脑会自动运行以下逻辑:
-
第一个问题:"对方年龄多大?"
-
如果大于 30 岁,直接拒绝(不见)。
-
如果小于等于 30 岁,进入下一个考量。
-
-
第二个问题(针对小于等于 30 岁的人):"月收入多少?"
-
如果小于 2 万,拒绝(不见)。
-
如果大于等于 2 万,进入下一个考量。
-
-
第三个问题(针对满足前两个条件的人):"长相如何?"
-
如果长相普通,拒绝(不见)。
-
如果长相好,决定去见一面(见)。
-
原理解析: 在这个过程中,你没有使用任何复杂的微积分或矩阵运算,仅仅是通过一连串的特征判断 (年龄、收入、长相),一步步排除了不符合要求的情况,最终得出了一个明确的结论 (见或不见)。这就是决策树最原始的物理意义:通过不断地提问,降低系统的不确定性,直到得出确定的答案。
1.2 决策树的解剖学(原子知识点)
将上述相亲的过程抽象成数学和计算机科学的模型,我们就得到了一棵倒立的"树"。一棵完整的决策树由四个最原子的结构组成:
-
根节点(Root Node)
-
定义:树的最顶端节点。
-
物理意义:代表整个数据集(包含所有来相亲的人)。它是第一个被测试的特征(例如"年龄"),也是区分度最大的特征。
-
-
内部节点(Internal Node)
-
定义:除了根节点和叶子节点之外的所有节点。
-
物理意义 :代表一个特征测试条件(例如"收入"或"长相")。每一个内部节点都会将进入该节点的数据再次切分。
-
-
分支(Branch / Edge)
-
定义:连接各个节点的线。
-
物理意义 :代表特征测试的结果路径。例如由"年龄"节点引出的">30岁"和"<=30岁"这两条路。数据顺着分支流向下一个节点。
-
-
叶子节点(Leaf Node / Terminal Node)
-
定义:树的最末端,没有任何子节点的节点。
-
物理意义 :代表最终的决策结果。在分类任务中,它是具体的类别(如"见"或"不见");在回归任务中,它是具体的连续数值。当数据落入叶子节点时,测试结束。
-
核心原则 :数据在决策树中只能沿着分支单向向下流动 ,不可能出现回头路。一个样本最终且只能落入一个叶子节点中。
1.3 决策树的几何意义:正交空间的切割
理解了逻辑结构后,我们需要建立数学直觉。决策树在多维特征空间中到底做了什么?
假设我们只有两个连续特征:(年龄)和
(月收入)。我们把这两个特征画在一个二维坐标系中:X 轴是年龄,Y 轴是收入。数据集里的每一个人都是这个二维平面上的一个散点。
决策树的每一次分裂,在几何上的意义就是在特征空间中画一条与坐标轴平行的直线(或超平面),将空间一分为二。
-
第一次切分 :根节点判断
。
- 几何表现:在 X 轴的 30 处,画一条垂直于 X 轴的直线。平面被分成了左右两块。
-
第二次切分 :内部节点判断
(仅在
的区域内进行)。
- 几何表现:在左侧区域内,在 Y 轴的 20000 处,画一条水平的线段。左侧区域又被上下分成了两块。
结论 :决策树的几何本质,就是通过一系列平行于坐标轴(正交)的边界,将复杂的特征空间切割成一个个互不重叠的矩形区域(或高维超矩形) 。每一个矩形区域,就对应着树上的一个叶子节点。
这种"正交切割"的特性,既是决策树具有极强可解释性的原因(你可以清楚地读出边界规则),也是它的局限性所在(面对需要斜线分割的数据时,决策树只能用锯齿状的阶梯线去逼近,效率很低)。
在模块一中,我们知道了决策树就是不断提问来划分数据。但这就引出了一个致命问题:面对成百上千个特征,我们在当前节点到底应该先问哪一个问题?(比如相亲时,是先问年龄,还是先问收入?)
这就是决策树的核心数学要解决的问题:寻找最佳分裂准则。
模块二:核心数学与分裂准则(深入原子知识点)
要想让计算机学会"挑好问题",我们必须给它一把衡量"问题好坏"的数学尺子。这把尺子的核心思想是:一个好的特征分裂,应该能让数据的"混乱程度"大幅度降低。
2.1 尺子的刻度:信息论基础
要衡量"混乱程度",我们需要借用信息论鼻祖香农(Claude Shannon)提出的概念。这里有两个最原子的数学概念:
原子点 1:信息量(Information Content)
信息量是用来衡量一个事件发生时,带给你的"惊讶程度"。
-
太阳从东边升起(必然事件,概率为 1),你不会惊讶,信息量为 0。
-
假如明天外星人降临地球(极小概率事件),你会极度震惊,信息量无穷大。
所以,信息量 I(x) 和事件发生的概率 P(x) 成反比。它的严谨公式是:
原子点 2:信息熵(Entropy)
如果一个系统里有多种可能发生的事情,信息熵就是这个系统所有可能事件信息量的数学期望(平均数) 。它直接代表了整个系统的混乱程度/不确定性 。 假设一个数据集里有 K 个类别,第 k 个类别出现的概率是,那么该系统的信息熵
公式为:
抛一枚正常 硬币,正反面概率都是 。代入公式:
。系统极度混乱,熵达到最大值 1。
2.2 第一代准则:信息增益(Information Gain) ------ ID3 算法的心脏
有了熵的概念,我们就可以衡量一个特征的好坏了。
逻辑:
-
先计算分裂前整个数据集的混乱程度,记为 H(D)。
-
假设我们用特征 A(比如"是否大于30岁")把数据集切分成了几个子集。我们分别计算这些子集的熵,然后按样本比例加权求和,这就叫条件熵,记为 H(D|A)。
-
两者相减,就得到了信息增益:
物理意义 :使用特征 A 进行划分后,系统混乱程度减少的量。增益越大,说明这个特征把数据分得越纯,我们就该选它!
致命缺陷(重要考点):
信息增益有一个天生的癖好------它极度偏好取值分支多的特征。
-
假设数据集里有一个特征是"身份证号"。如果用身份证号来分裂,每个子集里都只有 1 个人。
-
此时每个子集绝对纯净(全是同一类),条件熵 H(D|A) = 0。
-
那么信息增益达到最大值。但这种分裂毫无泛化意义,模型直接记住了每个人,导致严重过拟合。
2.3 第二代准则:信息增益比(Gain Ratio) ------ C4.5 算法的进化
为了解决 ID3 的"身份证号"缺陷,C4.5 算法引入了信息增益比。
逻辑 :既然你偏好分支多的特征,那我就给你加上一个"惩罚项"。这个惩罚项叫做特征 A 的内部信息(Intrinsic Value, IV):
其中 V 是特征 A 切分出的子集个数。特征取值越多,IV(A) 的值通常越大。
最终的分裂准则变为:
物理意义:不仅要求特征带来的信息增益大,还要求它不能把数据切得太碎。
2.4 第三代准则:基尼指数(Gini Impurity) ------ CART 算法的工业级选择
在工业界(如 Scikit-Learn 中的默认设置),最常用的是 CART(分类与回归树)算法。CART 在做分类树时,抛弃了信息熵,改用了基尼指数。
什么是基尼指数?
它原本是经济学中衡量收入不平等的指标。在决策树中,它的物理意义是:从数据集中随机抽取两个样本,其类别标签不一致的概率。 概率越大,说明数据越混乱(不纯)。
假设有 K 个类别,第 k类的概率是 ,基尼指数公式为:
为什么有了信息熵,还要搞个基尼指数? 因为信息熵公式里有(对数运算)。在计算机底层,对数运算非常消耗 CPU 算力。 而在数学上,如果你把信息熵公式中的 -\ln(x) 在 x=1 处进行一阶泰勒展开 ,你会神奇地发现,它近似等于 1-x。 所以,基尼指数本质上是信息熵的一种极速近似版。它去掉了复杂的对数运算,只保留了加法和乘法,极大地提升了大规模数据的训练速度。
2.5 回归树准则:均方误差(MSE)
因为在前面学信息熵的时候,我们处理的是分类问题 (比如"晴天/雨天"、"见/不见"),目标是让每一类的数据尽可能纯粹(全是同一类)。
但在回归问题中,我们要预测的是连续的数字(比如预测一个人的"月收入"是 10000 还是 10500)。连续数字没有"纯不纯"的概念,只有"准不准"的概念。
这个时候,熵和基尼指数失效了(因为连续值有无数种可能,概率无限小)。CART 回归树采用的准则是:最小化均方误差(Mean Squared Error)。
逻辑:
-
选取一个切分点,把数据分成左右两半。
-
每一半数据的预测值 ,就是这半边所有真实值的平均数。
-
计算每一半数据中,真实值与平均值的方差之和(即均方误差)。
-
寻找那个能让分裂后总方差最小的特征和切分点。 物理意义:我们要让切分出来的每一个子集内部的数据,尽可能长得一模一样(方差小)。
假设我们要预测"年龄(特征)对月收入(目标值)的影响"。
我们有 4 个人,数据如下:
-
A: 25岁,收入 5000
-
B: 28岁,收入 6000
-
C: 35岁,收入 20000
-
D: 40岁,收入 25000
步骤 1:如果不切分,我们怎么预测?
如果现在不看年龄,强行让你预测一个新来的人的收入,你最理性的瞎猜策略是什么?
是猜所有人的平均值。
-
平均收入 = (5000 + 6000 + 20000 + 25000) / 4 = 14000。
如果你都猜 14000,误差有多大?我们用真实值减去平均值,再平方求和(这就是均方误差的分子部分):
-
总误差 = (5000-14000)^2 + (6000-14000)^2 + (20000-14000)^2 + (25000-14000)^2。这个数字非常大。
步骤 2:尝试找一刀切下去(设定切分点)
现在,我们尝试用"年龄"来切分。假设我们在 30岁 这里切一刀。
-
左子集(<=30岁):A(5000),B(6000)
-
右子集(>30岁):C(20000),D(25000)
步骤 3:切分后,左右子集如何预测?
回归树的规矩是:落在哪个叶子节点,预测值就是那个叶子节点里所有真实值的平均数。
-
左子集预测值 = (5000 + 6000) / 2 = 5500
-
右子集预测值 = (20000 + 25000) / 2 = 22500
步骤 4:计算切分后的总误差(MSE)
现在我们来看看,按 30 岁切分后,总误差是不是变小了。
-
左侧误差:A的误差 (5000-5500)^2 + B的误差 (6000-5500)^2 = 25万 + 25万 = 50万
-
右侧误差:C的误差 (20000-22500)^2 + D的误差 (25000-22500)^2 = 625万 + 625万 = 1250万
-
总误差 = 50万 + 1250万 = 1300万
核心结论:
如果总误差(1300万)比切分前那极其庞大的误差要小,说明"年龄=30"是一个很好的切分点。
计算机在底层做的,就是穷举所有的年龄点(比如26岁切一刀、32岁切一刀),把每一种切法对应的"总误差"都算一遍。谁能让切分后的"总误差"最小,谁就是最佳切分点!
模块三:三大经典算法构建(算法演进史)
3.1 创世者:ID3 算法 (Iterative Dichotomiser 3)
-
诞生时间:1986 年(由 Ross Quinlan 提出)。
-
核心引擎 :信息增益 (Information Gain)。
-
树的形态 :多叉树。如果一个特征有 3 种取值(比如颜色:红、黄、蓝),节点就会直接长出 3 个分支。
-
致命局限性:
-
偏好问题:我们在模块二提过,它极度偏好取值分支多的特征(如"身份证号"),容易导致严重的过拟合。
-
挑食 :只能处理离散型变量(类别),完全无法处理连续型的数值变量(如身高 175.5 cm)。
-
怕残缺:对缺失值束手无策,数据里只要有空值它就报错。
-
3.2 进化者:C4.5 算法
-
诞生时间:1993 年(依然是由 Ross Quinlan 提出,是对他自己 ID3 的全方位升级)。
-
核心引擎 :信息增益比 (Information Gain Ratio)。
-
树的形态 :依然是多叉树。
-
划时代的改进:
-
克服偏好:引入了内部信息惩罚项,解决了 ID3 容易过拟合"身份证号"的问题。
-
连续值处理:引入了"阈值划分"机制,终于可以处理连续型数字了(具体底层原理我们在模块四详讲)。
-
缺失值处理:引入了"样本概率分配"机制,容错率大幅提升。
-
增加剪枝:开始引入剪枝机制来防止整棵树长得太深。
-
-
局限性:算法中有大量的对数运算,且处理连续值时需要反复排序,在面对海量数据时,计算速度非常慢。
3.3 现代工业霸主:CART 算法 (Classification And Regression Tree)
-
诞生时间 :1984 年(由 Breiman 等人提出,虽然时间比 C4.5 早,但最终一统江湖,成为了目前 Scikit-Learn、XGBoost、LightGBM 等现代机器学习库的绝对底层基石)。
-
核心引擎:
-
分类树:基尼指数 (Gini Impurity)
-
回归树:均方误差 (MSE)
-
-
树的形态 :强制二叉树。(永远只问"是/否"问题,每次分裂只产生 2 个分支)。
深入底层拷问:为什么 CART 强制要求"二叉树"?
这是一个非常高频的面试题,也是理解模型底层逻辑的关键。
在 ID3 和 C4.5 中,如果一个特征是"中国省份",它有 34 个取值。那么这棵多叉树在这个节点会瞬间长出 34 个分支。 这会带来一个极其可怕的灾难:数据碎片化 (Data Fragmentation) 。 假设你只有 1000 条数据,这一刀砍下去,每个分支里平均只剩下不到 30 条数据。数据量一旦太少,统计学规律就失效了,接下来的继续分裂纯粹是在"背诵"噪音数据,瞬间陷入过拟合。
CART 的解决哲学(二分法): CART 拒绝多叉。面对"34 个省份",它不会一次性分 34 叉,而是穷举所有的二元组合。 它会问:"这个省份是不是 {北京、上海、广东} 中的一个?"
-
是(向左走,包含 3 个省份的数据)
-
否(向右走,包含剩下 31 个省份的数据)
二叉树的优势:
-
防止数据过快碎片化:保证每次分裂后,左右子集依然有足够的样本量进行后续学习。
-
特征的重复利用 :在多叉树中,一个特征被用过一次后(分裂成34份),这条路径上就不能再用它了。但在 CART 二叉树中,右侧的分支(剩下31个省份)在更深层的节点里,依然可以继续拿"省份"这个特征来问问题(例如:"是不是江浙沪?")。这极大地丰富了模型的非线性表达能力。
在前面的模块中,我们假设的数据都非常"完美":要么是离散的类别(晴天/雨天),要么数据干干净净没有任何空缺。但在真实的业务工程中,数据通常是连续的 (如精确到小数点的金额)且千疮百孔的(用户经常不填某些信息)。
今天这一篇,我们就来看看决策树是如何向现实妥协,处理这两种特殊数据的。
模块四:特殊数据处理(工程与现实的妥协)
4.1 连续型变量的处理:二分法(Bi-partition)
在模块三的表格中,你可能已经猜到了,C4.5 和 CART 都能处理连续型变量。但树的分支总得是个明确的"门槛"(比如">30岁"),面对无限的连续数字,计算机是如何自动找到这个门槛(阈值)的呢?
核心底层逻辑:排序 + 找中点 + 穷举
我们通过一个最原子的 3 步拆解 来看: 假设我们有 5 个人的"芝麻信用分",并且已经按照从小到大排好序: 600, 620, 650, 700, 710
-
第 1 步(找中点) :连续数字有无数个切分点,为了减少计算量,算法会提取相邻两个数值的平均数作为候选切分点(阈值)。
-
600 和 620 的中点:610
-
620 和 650 的中点:635
-
650 和 700 的中点:675
-
700 和 710 的中点:705 这样,我们就得到了 4 个候选切分点。
-
-
第 2 步(转化为离散问题):现在,我们把这 4 个候选点当成 4 个"是/否"问题。比如用 610 切分,数据就被分成了"<=610"和">610"两拨。
-
第 3 步(优胜劣汰):把这 4 种切分情况各自的信息增益(或基尼指数/均方误差)算一遍。谁带来的增益最大,这个连续特征的最佳切分点就是谁!
从上面可以看出,每次处理连续变量,计算机都必须先做一次排序 (Sorting)。在海量数据下,排序是非常消耗 CPU 和内存的操作。这就是为什么传统的决策树在面对庞大数据集时训练速度会变慢的核心原因之一。(后来的 LightGBM 算法用"直方图"技术解决了这个问题,这是后话)。
4.2 缺失值的处理
如果数据里有空值(NaN/Null),比如用户注册时没填"月收入",决策树该怎么处理?
直接删掉这行数据?太浪费了!填个平均值?可能会扭曲真实分布。
C4.5 算法提出了一种极其优雅的底层机制:基于概率的样本权重分配(俗称"影分身")。
面对缺失值,算法必须回答两个灵魂拷问:
灵魂拷问 1:在评估"月收入"这个特征好不好用时,怎么计算它的信息增益?
-
解法:打折机制。
-
假设有 10 个人,只有 7 个人填了月收入,3 个人没填。
-
算法会先假装那 3 个人不存在,只用这 7 个"无缺失样本"算出月收入的信息增益(假设算出来是 0.8)。
-
然后,乘以一个打折系数(无缺失样本占比 = 7/10 = 0.7)。
-
最终,"月收入"这个特征的真实信息增益 = 0.56。
-
物理意义:一个特征缺失越严重,我越不信任它,给它的得分就越低。
灵魂拷问 2:假设"月收入"最终胜出,成为了当前节点的分裂特征。阈值是">1万"。那刚才没填月收入的 3 个人,该去左子节点还是右子节点?
-
解法:按比例分身。
-
算法会观察刚才那 7 个有数据的人:假设 7 个人里,有 4 个人月收入 >1万(占比约 57%),3 个人 <=1万(占比约 43%)。
-
对于一个没填收入的人(样本 X),算法不会强行把他归到左边或右边,而是把他撕裂成两半!
-
产生一个带有 0.57 权重的样本 X 进入">1万"的分支。
-
产生一个带有 0.43 权重的样本 X 进入"<=1万"的分支。
-
物理意义:既然我不知道你具体多少钱,那我就按照大众的概率分布,把你以不同的权重分配到所有可能的路径下去。在后续的叶子节点计算中,这个权重会一直跟随着你。
模块五:过拟合与剪枝机制(深入底层正则化)
5.1 决策树的"原罪":为什么它天生容易过拟合?
决策树是一个极度"贪婪"且"记忆力超群"的模型。 如果你不加任何限制,让一棵树自由生长,它会一直分裂下去,直到每一个叶子节点里只包含一个样本(纯度达到 100%)。
物理意义的灾难: 这就好比一个学生为了应付期末考试,不总结规律,而是把整本《五年高考三年模拟》的所有题目和答案死记硬背下来。
-
在训练集(做过的题)上,它的准确率是完美的 100%。
-
在测试集 (考场上的新题)上,只要题目稍微改一个数字,它就完全懵了,错误率极高。 这种现象就是过拟合(Overfitting)。为了防止树长得太深、太茂盛,我们必须给它"剪枝"。
5.2 预剪枝(Pre-pruning):防患于未然
预剪枝的思想很简单:在树生长的过程中,设定一些规则,一旦触发规则,就强行停止分裂。
在工程代码(如 Scikit-Learn)中,这体现为几个核心的超参数(Hyperparameters):
-
max_depth(最大深度):比如设定为 5,树长到 5 层就强制停止。 -
min_samples_split(分裂所需最小样本数):如果当前节点只剩 10 个样本了,而我们设定阈值为 15,那么该节点拒绝继续分裂,直接变成叶子节点。 -
min_impurity_decrease(最小不纯度下降值):如果某一刀切下去,信息增益微乎其微(低于设定的阈值),说明这刀没什么意义,干脆不切了。
评价 :预剪枝计算速度快,但它很"短视"。有时候当前这一刀看似增益不大,但切完之后,下一步可能会有巨大的增益。预剪枝容易导致提前停止,造成欠拟合(Underfitting)。
5.3 后剪枝(Post-pruning):秋后算账
后剪枝是目前公认更科学的做法(CART 算法的核心逻辑之一)。
它的思想是:先让树完全放飞自我,长到最大最深。然后我们自底向上,评估每一个非叶子节点,如果把它剪掉(把它下面的分支合并成一个叶子节点)带来的好处大于坏处,我们就"咔嚓"一剪刀。
这里最核心的底层原理叫做:代价复杂度剪枝(Cost-Complexity Pruning, CCP)。
我们来拆解它的核心损失函数公式(绝对的原子知识点):
这个公式是衡量一棵树"好坏"的终极标准。越小越好。我们把它劈成三半来看:
-
C(T):模型对训练数据的预测误差。比如整棵树所有叶子节点的基尼指数之和,或者均方误差之和。树越深,分得越细,C(T) 就越小。
-
|T|:树的复杂度 。用叶子节点的总数来代表。叶子越多,树越复杂。
-
:惩罚系数(超参数) 。这就是你手中的那把"剪刀"。
越大,说明你对复杂度的容忍度越低,惩罚越狠。
底层逻辑推演:
假设在树的最底部,有一个节点分支了。
-
如果不剪它,误差 C(T) 很小,但多出了几个叶子节点,导致
变大。
-
如果把它剪了(合并),误差 C(T)会稍微变大一点点,但是叶子节点数量 |T|减少了,导致
变小。
-
动作:计算机一算,发现"剪掉它增加的误差"居然比"省下的复杂度惩罚"还要小,那这笔买卖划算!剪!
通过调节 的值,我们就能控制树的最终规模。
假设我们有 10 个病人(5 个健康,5 个患病)。整个模型只有 1 个根节点(它也就是唯一的叶子节点)。
-
样本数
-
患病概率
-
基尼指数
-
此时整棵树的误差:
【切分一刀:长出两个叶子节点】
现在我们用"是否发烧"切了一刀,产生了两个叶子节点(叶子 A 和 叶子 B):
-
叶子 A(发烧):分到了 6 个人(其中 5 个患病,1 个健康)。
-
样本数
-
基尼指数
-
-
叶子 B(不发烧):分到了 4 个人(其中 0 个患病,4 个健康。非常纯净!)。
-
样本数
-
基尼指数
-
【计算切分后整棵树的 C(T)】
现在这棵树有 2 个叶子节点,我们将它们的(样本数 基尼指数)加起来:
核心结论:
没切分前,误差 C(T) 是 5 ;切分一刀后,误差 C(T) 降到了 1.668。
只要你继续往下切(比如把叶子 A 里的那 1 个健康人再单独切出来),这个数值就会继续变小,直到每个叶子节点里只剩同一种人(基尼指数全为 0),此时 C(T) = 0。