决策树概述
### 什么是决策树 决策树(Decision Tree)是一种基本的分类与回归方法,它基于树状图来进行决策。 **核心思想**:通过一系列的规则(if-then)对数据进行递归分割,最终形成一个树形的判断结构。 **直观理解**:类似于流程图,从根节点开始,每个内部节点表示一个属性上的判断条件,每个分支代表一个判断结果的输出,每个叶子节点代表一种分类结果(类别标签或连续值)。 ### 为什么学决策树 - 决策树是机器学习中最基础的算法之一,是理解更复杂算法(随机森林、GBDT、XGBoost)的基石 - 具有极好的**可解释性**,结果直观可视,非技术人员也能理解 - 既可以处理**分类问题**,也可以处理**回归问题** - 不需要复杂的特征工程,能同时处理数值型和类别型特征 - sklearn 中通过 `DecisionTreeClassifier` 和 `DecisionTreeRegressor` 实现 ### 快速上手 ```python from sklearn.tree import DecisionTreeClassifier # 定义模型 model = DecisionTreeClassifier(criterion="gini", max_depth=5) # 训练 model.fit(X_train, y_train) # 预测 y_pred = model.predict(X_test) # 评估准确率 model.score(X_test, y_test) ```
### 决策树的树结构 ``` 根节点(Root Node) / 特征A ≤ 阈值? \ / \ 内部节点(Internal) 内部节点(Internal) / 特征B ≤ 阈值? \ / 特征C ≤ 阈值? \ / \ / \ 叶子节点 叶子节点 叶子节点 叶子节点 (类别A) (类别B) (类别B) (类别C) ``` **节点类型**: | 节点类型 | 说明 | |---------|------| | **根节点** (Root) | 树的起点,包含全部训练数据 | | **内部节点** (Internal) | 表示一个特征上的判断条件,执行二分划分 | | **叶子节点** (Leaf) | 终端节点,表示最终的分类结果或回归值 | **重要特性**: - 决策树是**二叉树**结构(CART算法),每个非叶子节点最多有两个分支 - 从根到任一叶子节点的路径构成一条**分类规则** - 路径上的条件用 `AND` 连接,形成 `if-then` 规则
### 决策树学习的三大步骤 决策树的学习过程包括三个核心步骤:**特征选择 → 决策树生成 → 决策树剪枝**。 #### 1. 特征选择 选取对训练数据具有分类能力的特征,决定使用哪个特征来划分数据空间。常用标准: - **信息增益**(ID3 算法):得知特征 X 的信息而使得类 Y 的不确定性减少的程度 - **信息增益比**(C4.5 算法):信息增益和特征熵的比值,克服偏向取值多的属性 - **基尼指数**(CART 算法):衡量数据集的纯度,基尼指数越小纯度越高 #### 2. 决策树生成 根据选择的特征评估标准,从上至下**递归地**生成子节点: 1. 选择最优特征和切分点 2. 按条件将数据分为两个子集 3. 对每个子集递归重复步骤1~2 4. 直到数据不可再分或满足停止条件 这一过程形成的树往往对训练数据拟合很好,但可能**过拟合**。 #### 3. 决策树剪枝 为了解决过拟合问题,需要剪枝: | 剪枝策略 | 说明 | |---------|------| | **预剪枝** (Pre-pruning) | 在生成过程中,若当前节点划分不能提升泛化性能,则停止划分 | | **后剪枝** (Post-pruning) | 先生成完整树,再自底向上考察,若替换子树为叶节点能提升泛化性能则替换 | > **关键理解**:生成是"从上到下"构建树,剪枝是"从下到上"简化树。两者结合才能得到泛化能力好的模型。
核心算法
### 决策树算法总览与对比 | 算法 | 提出者 | 特征选择标准 | 树的类型 | 支持任务 | 剪枝 | |------|--------|-------------|---------|---------|------| | **ID3** | Quinlan (1986) | 信息增益 | 多叉树 | 分类(离散特征) | 无 | | **C4.5** | Quinlan (1993) | 信息增益比 | 多叉树 | 分类(离散+连续) | 有 | | **CART** | Breiman (1984) | 基尼指数 / 平方误差 | 二叉树 | 分类+回归 | 有 | **选择建议**: - sklearn 中 `DecisionTreeClassifier` 默认使用 CART + 基尼指数 - 基尼指数 vs 信息熵:实际效果基本相同,基尼指数计算更快 - CART 是最常用的决策树算法,sklearn 的实现就是基于 CART ### 核心区别 ``` ID3:根据信息增益选择特征 → 剔除该特征 → 递归 倾向于选择取值多的属性 C4.5:用信息增益比替代信息增益 → 克服ID3偏向 能处理连续特征和缺失值 CART:根据某个指标选择特征 → 将数据分为两类 → 递归 生成二叉树,既能分类也能回归 ```
### ID3 算法 **核心思想**:使用**信息增益**(Information Gain)作为特征选择标准。 **信息增益的含义**:反映在给定条件后不确定性减少的程度。特征取值的信息增益越高,意味着确定性越高,条件熵越小。 **算法流程**: 1. 计算每个特征的信息增益 2. 选择信息增益最大的特征作为当前节点的划分特征 3. 根据该特征的取值创建子节点 4. 剔除已选特征,递归处理子节点 5. 直到所有特征用完或信息增益小于阈值 **ID3 的局限**: - 倾向于选择**取值多的属性**(因为取值越多,信息增益越大) - 只能处理**离散型**变量,无法直接处理连续值 - 对样本**缺失值**比较敏感 - **没有剪枝**过程,容易过拟合 - 多叉树结构,非二叉树
### C4.5 算法 C4.5 是 ID3 的改进版本,由同一作者 Quinlan 提出。 **核心改进**:使用**信息增益比**(Gain Ratio)替代信息增益来选择划分属性。 **信息增益比** = 信息增益 / 特征本身的熵(Split Information) 这个改进**克服了 ID3 偏向取值多属性**的问题,因为信息增益比考虑了特征本身的熵,对取值多的属性进行了惩罚。 **C4.5 相对 ID3 的增强**: | 特性 | ID3 | C4.5 | |------|-----|------| | 特征选择标准 | 信息增益 | 信息增益比 | | 连续数值特征 | 不支持 | 支持(离散化处理) | | 剪枝技术 | 无 | 有(悲观剪枝) | | 缺失值处理 | 不支持 | 支持 | | 树的类型 | 多叉树 | 多叉树 | **C4.5 的局限**: - 效率相对较低,需要对数据集多次扫描和排序 - 只能用于分类问题,不能处理回归问题 - 树结构仍为多叉树,不如二叉树简洁
### CART 算法(Classification and Regression Trees) CART 是目前最主流的决策树算法,sklearn 的决策树实现就是基于 CART。 **核心特点**: - 采用**二分递归分割**技术,每个非叶子节点只有两个分支 - 既能处理**分类问题**(使用基尼指数),也能处理**回归问题**(使用平方误差) - 生成结构简洁的**二叉树** **CART 分类树**: - 使用**基尼指数**作为划分标准 - 每次选择使基尼指数最小的特征和切分点 **CART 回归树**: - 使用**平方误差**(MSE)作为划分标准 - 每个叶子节点的预测值 = 该叶子节点所有样本目标变量的**均值** - 选择最优切分点:遍历所有特征和切分值,使划分后的平方误差最小 **CART 回归树示例**: ```python from sklearn.tree import DecisionTreeRegressor model = DecisionTreeRegressor(max_depth=5) model.fit(X_train, y_train) # y_train 为连续值 y_pred = model.predict(X_test) ``` **CART 的剪枝**: - **预剪枝**:通过设置停止条件限制树的生长(max_depth、min_samples_leaf 等) - **后剪枝**:代价复杂度剪枝(Cost-Complexity Pruning),通过计算表面误差率增益值来选择是否剪枝
### 基尼(Gini)算法详解 基尼指数是 CART 决策树在处理**分类问题**时的核心评价指标。 #### 什么是基尼指数 基尼指数(Gini Index / Gini Impurity)衡量的是:**从数据集中随机选取两个样本,其类别标签不一致的概率**。 - 基尼指数**越小**,数据集的**纯度越高** - 基尼指数 = 0 时,表示节点完全纯净(所有样本属于同一类别) - 基尼指数最大值为 1 - 1/k(k 为类别数) #### 计算公式 $$Gini(D) = 1 - \sum_{k=1}^{K} p_k^2$$ 其中 $p_k$ 表示第 k 类样本所占的比例,K 为类别总数。 **条件基尼指数**(按特征 A 划分后): $$Gini(D, A) = \frac{|D_1|}{|D|} Gini(D_1) + \frac{|D_2|}{|D|} Gini(D_2)$$ #### 基尼指数在决策树中的应用 1. **遍历**每个特征及其所有可能的划分点 2. 计算每个划分点产生的**左右子节点的基尼指数加权平均** 3. 选择使基尼指数**最小化**的特征和划分点作为当前节点 4. 递归对子节点重复步骤 1~3 #### 基尼指数 vs 信息熵 | 对比项 | 基尼指数 | 信息熵 | |--------|---------|--------| | 公式 | $1 - \sum p_k^2$ | $-\sum p_k \log_2(p_k)$ | | 计算速度 | 更快(无需对数运算) | 较慢 | | 分类效果 | 与信息熵基本相同 | 与基尼基本相同 | | sklearn默认 | **是** | 否(可选 `criterion='entropy'`) | > **实际使用建议**:两者效果几乎无差异,sklearn 默认使用基尼指数是因为计算更快。无需纠结选哪个。
数学原理
### 信息熵(Information Entropy) 信息熵是度量数据**不确定性(混乱程度)**的指标。 #### 定义 $$H(D) = -\sum_{k=1}^{K} p_k \log_2(p_k)$$ 其中: - $D$ 是数据集 - $K$ 是类别总数 - $p_k$ 是第 k 类样本所占的比例($p_k = \frac{|C_k|}{|D|}$) #### 直观理解 - 熵**越大** → 数据越**混乱**(各类别均匀分布) - 熵**越小** → 数据越**纯净**(某一类占绝对多数) - 熵 = 0 → 数据完全纯净(所有样本属于同一类) #### 举例 ``` 数据集A: [正,正,正,正,正,负,负,负,负,负] → 5正5负 → H = 1.0(最大混乱) 数据集B: [正,正,正,正,正,正,正,正,正,负] → 9正1负 → H ≈ 0.47(较纯净) 数据集C: [正,正,正,正,正,正,正,正,正,正] → 10正0负 → H = 0(完全纯净) ``` > **决策树的目标**:通过不断划分,使子节点的熵越来越小(越来越纯净)。
### 信息增益(Information Gain) 信息增益 = 划分前的熵 - 划分后的熵(条件熵) $$Gain(D, A) = H(D) - H(D|A)$$ 其中条件熵: $$H(D|A) = \sum_{i=1}^{n} \frac{|D_i|}{|D|} H(D_i)$$ - $A$ 是划分特征 - $D_i$ 是按特征 A 的第 i 个取值划分后的子集 - $n$ 是特征 A 的取值个数 #### 信息增益比 $$Gain\_ratio(D, A) = \frac{Gain(D, A)}{H_A(D)}$$ 其中 $H_A(D)$ 是特征 A 本身的熵(Split Information): $$H_A(D) = -\sum_{i=1}^{n} \frac{|D_i|}{|D|} \log_2 \frac{|D_i|}{|D|}$$ #### 计算示例 假设 10 个样本,6 个正例、4 个反例: ``` 划分前:H(D) = -(6/10)log2(6/10) - (4/10)log2(4/10) ≈ 0.971 按特征A划分后: 子集D1(4正1反):H = -(4/5)log2(4/5) - (1/5)log2(1/5) ≈ 0.722 子集D2(2正3反):H = -(2/5)log2(2/5) - (3/5)log2(3/5) ≈ 0.971 条件熵 = (5/10) × 0.722 + (5/10) × 0.971 ≈ 0.847 信息增益 = 0.971 - 0.847 = 0.124 ``` 信息增益越大,说明该特征对分类的贡献越大。
### 基尼指数计算详解 #### 公式 $$Gini(D) = 1 - \sum_{k=1}^{K} p_k^2$$ #### 计算示例 假设 10 个样本,6 个正例、4 个反例(二分类): ``` Gini(D) = 1 - (6/10)² - (4/10)² = 1 - 0.36 - 0.16 = 0.48 ``` 按特征 A(阈值 t)划分后: ``` 左子集 D1 (≤ t):4正1反 Gini(D1) = 1 - (4/5)² - (1/5)² = 1 - 0.64 - 0.04 = 0.32 右子集 D2 (> t):2正3反 Gini(D2) = 1 - (2/5)² - (3/5)² = 1 - 0.16 - 0.36 = 0.48 划分后的基尼指数 = (5/10) × 0.32 + (5/10) × 0.48 = 0.40 ``` 基尼指数从 0.48 降到 0.40,说明这次划分有效。 #### sklearn 中的验证 ```python from sklearn.tree import DecisionTreeClassifier import numpy as np # 模型使用基尼指数 model = DecisionTreeClassifier(criterion="gini", max_depth=5) model.fit(X_train, y_train) # 查看每个节点的不纯度(基尼指数) print(model.tree_.impurity) # array([0.4675, 0.1589, 0.0299, 0.0074, ...]) # 根节点不纯度最高,叶子节点最低 ``` > **决策树每次划分的目标**:找到使条件基尼指数最小的特征和切分点。
主要参数
### DecisionTreeClassifier 参数总览 | 参数 | 默认值 | 说明 | |------|--------|------| | `criterion` | `'gini'` | 特征选择标准:`'gini'`(基尼系数)或 `'entropy'`(信息熵) | | `splitter` | `'best'` | 划分策略:`'best'`(最优划分)或 `'random'`(随机局部最优) | | `max_depth` | `None` | 树的最大深度,None 表示不限制 | | `min_samples_split` | `2` | 内部节点再划分所需最小样本数 | | `min_samples_leaf` | `1` | 叶子节点最少样本数 | | `min_weight_fraction_leaf` | `0` | 叶子节点最小样本权重和 | | `max_features` | `None` | 划分时考虑的最大特征数 | | `random_state` | `None` | 随机种子,设置后结果可复现 | | `max_leaf_nodes` | `None` | 最大叶子节点数 | | `min_impurity_decrease` | `0` | 节点划分最小不纯度下降值 | **参数调优优先级**:`max_depth` > `min_samples_leaf` > `min_samples_split` > `max_features`
### criterion — 特征选择标准 决定不纯度的计算方法,影响特征选择的结果。 ```python # 基尼系数(默认,计算更快) model = DecisionTreeClassifier(criterion="gini") # 信息熵(效果与基尼基本相同) model = DecisionTreeClassifier(criterion="entropy") ``` | 选项 | 公式 | 特点 | |------|------|------| | `'gini'` | $1 - \sum p_k^2$ | 计算更快,无需对数运算 | | `'entropy'` | $-\sum p_k \log_2(p_k)$ | 理论基础更强,对纯度更敏感 | **两者效果基本相同**,sklearn 默认使用 `'gini'` 主要是因为计算效率更高。在实际项目中无需纠结选哪个。 ### splitter — 划分策略 | 选项 | 说明 | 适用场景 | |------|------|---------| | `'best'` | 在所有划分点中找最优 | 样本量不大(推荐) | | `'random'` | 随机在部分划分点中找局部最优 | 样本量非常大时加快训练 |
### max_depth — 最大深度 决策树的最大深度(从根到最远叶子的最长路径)。 **这是最重要的调参参数**,直接控制模型复杂度。 #### 深度与节点数的关系(完全二叉树) ``` depth=1 → 最多 3 个节点(1根 + 2叶) depth=2 → 最多 7 个节点 depth=3 → 最多 15 个节点 depth=n → 最多 2^(n+1) - 1 个节点 depth=6 → 最多 127 个节点 depth=7 → 最多 255 个节点 ``` > 通常机器学习中的特征列到不了 128,所以 **max_depth 取 6 就算大的了**。 #### 设置建议 | 场景 | 建议 max_depth | |------|---------------| | 特征少(<10) | 3~5 | | 特征中等(10~50) | 5~10 | | 特征多(>50) | 10~20 | | 不确定 | 先不设(None),观察过拟合后调整 | #### 调参策略 1. **开始不设** max_depth,让树自由生长,观察是否过拟合 2. 尝试设置 max_depth 为特征数量的**平方根或对数** 3. 使用**交叉验证**评估不同 max_depth 的效果 4. 结合 `min_samples_leaf` 等参数共同调优
### min_samples_leaf — 叶子节点最少样本数 控制叶子节点上最少的样本数量。如果某叶子节点样本数少于该值,会和兄弟节点一起被**剪枝**。 ```python # 叶子节点至少包含10个样本 model = DecisionTreeClassifier(min_samples_leaf=10) ``` **作用**: - 控制了叶子节点上样本的数量不能过少 - 限制了树的复杂度,防止过拟合 - 值越大,树越简单(越保守) **设置建议**: | 场景 | 建议值 | |------|--------| | 样本量小(<1000) | 1~5 | | 样本量中(1000~10000) | 5~50 | | 样本量大(>10000) | 50~200 | > **经验法则**:通常设置为样本量的 1%~5%,但不低于 5。
### 其他重要参数 #### min_samples_split — 内部节点最小分裂样本数 如果某节点的样本数少于该值,则不会继续划分。 ```python model = DecisionTreeClassifier(min_samples_split=10) # 默认值为 2 ``` #### max_features — 划分时考虑的最大特征数 限制每次划分时考虑的特征数量,有助于防止过拟合。 ```python model = DecisionTreeClassifier(max_features='sqrt') # 特征数的平方根 model = DecisionTreeClassifier(max_features=10) # 最多考虑10个特征 model = DecisionTreeClassifier(max_features=0.5) # 考虑50%的特征 ``` | 值 | 说明 | |----|------| | `None`(默认) | 考虑所有特征 | | `'sqrt'` | 考虑 √n 个特征 | | `'log2'` | 考虑 log₂(n) 个特征 | | `int/float` | 指定数量或比例 | #### min_impurity_decrease — 最小不纯度下降值 如果某次划分的不纯度下降值小于该阈值,则不会划分。 ```python model = DecisionTreeClassifier(min_impurity_decrease=0.01) # 默认值为 0 ``` #### max_leaf_nodes — 最大叶子节点数 直接限制叶子节点的数量,防止树过于复杂。 #### class_weight — 类别权重 防止训练集某些类别的样本过多导致模型偏向。 ```python model = DecisionTreeClassifier(class_weight='balanced') # 自动平衡 ```
代码示例
### Iris 鸢尾花分类示例 ```python import joblib import numpy as np from sklearn.tree import DecisionTreeClassifier from tpf.datasets import load_iris X_train, y_train, X_test, y_test = load_iris() # 模型定义 model = DecisionTreeClassifier(criterion="gini", max_depth=5) # 训练 model.fit(X=X_train, y=y_train) # 预测 y_pred = model.predict(X=X_test) # 评估 score = model.score(X_test, y_test) print(f"准确率: {score:.4f}") # 保存模型(推荐使用 joblib) joblib.dump(model, "model1.pkl") # 加载模型 model_loaded = joblib.load("model1.pkl") ```
### 分类预测与概率 `predict` 返回类别标签,`predict_proba` 返回各类别的概率。 ```python import numpy as np from sklearn.tree import DecisionTreeClassifier from ai.datasets import load_iris X_train, y_train, X_test, y_test = load_iris() model = DecisionTreeClassifier(criterion="gini", max_depth=5) model.fit(X=X_train, y=y_train) # predict 返回标签的类别索引 print(model.predict(X=X_test)) # [1, 0, 2, 0, 1, 1, 0, 2, 1, 2, 2, 2, 0, 0, 0, 2, 1, 2, 1, 2, # 2, 2, 2, 0, 2, 1, 0, 0, 0, 0] # predict_proba 返回各类别的概率(one-hot 向量) print(model.predict_proba(X=X_test)) # [[0., 1., 0.], → 第1个样本属于类别1的概率为100% # [1., 0., 0.], → 第2个样本属于类别0的概率为100% # [0., 0., 1.], → 第3个样本属于类别2的概率为100% # ...] ``` **关键理解**: - `predict` → 返回概率最大的类别索引(确定性预测) - `predict_proba` → 返回所有类别的概率分布(不确定性量化)
### 决策树回归示例 决策树回归使用 `DecisionTreeRegressor`,预测值为叶子节点样本的均值。 ```python from sklearn.tree import DecisionTreeRegressor from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split # 生成回归数据 X, y = make_regression(n_samples=1000, n_features=10, noise=10, random_state=42) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) # 回归树模型 model = DecisionTreeRegressor(max_depth=5, min_samples_leaf=10) model.fit(X_train, y_train) # 预测连续值 y_pred = model.predict(X_test) # 评估(R²分数) score = model.score(X_test, y_test) print(f"R² 分数: {score:.4f}") # 叶子节点的预测值 = 该叶子所有训练样本 y 的均值 ``` **分类树 vs 回归树对比**: | 对比项 | 分类树 | 回归树 | |--------|--------|--------| | sklearn类 | `DecisionTreeClassifier` | `DecisionTreeRegressor` | | 预测输出 | 类别标签 | 连续值 | | 划分标准 | 基尼指数/信息熵 | 平方误差(MSE) | | 叶子节点值 | 多数类别的标签 | 样本目标值的均值 | | 评估指标 | accuracy, F1 | R², MSE, MAE |
特征重要性
### 特征重要性的原理 `model.feature_importances_` 返回每个特征对决策的重要性分数。 **原理**:一个特征的重要性 = 该特征在所有节点上带来的不纯度下降值的加权平均。 ```python model = DecisionTreeClassifier(criterion="gini", max_depth=5) model.fit(X_train, y_train) # 特征重要性数组,与 X 的列一一对应 importances = model.feature_importances_ print(importances) # [0. , 0.0018, 0. , 0. , ..., 0.1199, 0. , 0. ] # 所有特征重要性之和 = 1.0 print(sum(importances)) # 1.0 ``` **解读**: - 值为 0 的特征:该特征未被使用(对划分无贡献) - 值越大:该特征对分类决策越重要 - 可以用于**特征筛选**:剔除重要性为 0 的特征 ```python import numpy as np # 获取重要性排序后的特征索引 sorted_idx = np.argsort(importances)[::-1] # 取前 N 个重要特征的索引 top_n = 10 top_features = sorted_idx[:top_n] ```
### 简易示例:验证特征重要性 通过加入噪声列来验证特征重要性的正确性: ```python import numpy as np from sklearn.tree import DecisionTreeClassifier from ai.datasets import load_iris X_train, y_train, X_test, y_test = load_iris() # 原始特征重要性 model = DecisionTreeClassifier(criterion="gini", max_depth=5) model.fit(X=X_train, y=y_train) print(model.feature_importances_) # [0.0167, 0.0300, 0.4199, 0.5334] # → 后两个特征(花瓣长宽)远比前两个(花萼长宽)重要 # 加入噪声列 a = np.random.randint(-3, 3, size=120).reshape(-1, 1) # 假1 b = np.random.randint(-3, 3, size=120).reshape(-1, 1) # 假2 c = np.random.randint(-3, 3, size=120).reshape(-1, 1) # 假3 X_train = np.concatenate([X_train[:, :2], a, X_train[:, 2:], b, c], axis=1) model = DecisionTreeClassifier(criterion="gini", max_depth=5) model.fit(X=X_train, y=y_train) m = model.feature_importances_ # 排序查看 a = np.argsort(m)[::-1] cols = ["真1", "真2", "假1", "真3", "真4", "假2", "假3"] print(np.array(cols)[a][:2].tolist()) # ['真3', '真4'] # → 真实特征(花瓣长宽)仍是最重要的,噪声列被正确识别为不重要 ```
### 累积列重要性:多轮训练筛选特征 通过多轮训练统计每个特征出现的贡献次数,筛选出真正重要的特征。 **核心思路**: ``` 1. 初始化 list_a = [0, 0, ..., 0](长度=特征数) 2. 每次训练后,对有贡献(importance > 0)的特征,list_a 对应位置 +1 3. 多轮训练后,list_a 中值越大的特征越重要 ``` ```python import joblib import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import make_classification model = DecisionTreeClassifier(criterion="gini", max_depth=10) col_feature_importance = [] def get_feature_col(model, X, y, col_feature_importance=col_feature_importance): model.fit(X, y) fti = model.feature_importances_ fti_len = len(fti) if len(col_feature_importance) == 0: for i in range(fti_len): col_feature_importance.append(0) # 原列重要性列表索引倒序排列 feature_index_desc = np.argsort(fti)[::-1] # 将有贡献的列加1 for i in range(fti_len): index_pos = feature_index_desc[i] if fti[index_pos] == 0: print(f"前{i+1}列有贡献") break else: col_feature_importance[index_pos] += 1 return i # 10轮训练 for i in range(10): X, y = make_classification(n_samples=1000, n_features=1000, n_classes=2) get_feature_col(model, X, y) # 筛选:至少出现过 N 次的列 def get_used_cols(col_feature_importance, max_cols=200, arise_atleast=1): col_feature_desc = np.argsort(col_feature_importance)[::-1] col_feature_not0 = [] for i in range(len(col_feature_desc)): index_value_desc = col_feature_desc[i] importance_value = col_feature_importance[index_value_desc] if importance_value < arise_atleast: col_feature_not0 = col_feature_desc[:i] break use_cols = col_feature_not0[:max_cols] return use_cols # 至少出现过1次:263个列;至少出现过2次:34个列 # 可以分别测试使用34个列与263个列的效果(精度、性能) ```
可视化与策略生成
### sklearn 自带可视化 使用 `sklearn.tree.plot_tree` 快速绘制决策树: ```python import matplotlib.pyplot as plt from sklearn import tree from sklearn.datasets import load_breast_cancer # 加载数据并训练 data = load_breast_cancer() X = pd.DataFrame(data.data, columns=data.feature_names) y = pd.Series(data.target) clf = tree.DecisionTreeClassifier(max_depth=3, min_samples_leaf=50) clf = clf.fit(X, y) # 绘制决策树 plt.figure(figsize=(12, 8), dpi=150) tree.plot_tree(clf, feature_names=data.feature_names, class_names=['good', 'bad'], filled=True) plt.savefig('tree.svg', format='svg', bbox_inches='tight') plt.show() ``` **注意**:二叉树的前序遍历读取顺序为 `20, 27, 1, -2, -2, -2, 7, -2, -2`,其中 `-2` 表示叶子节点。
### dtreeviz 高级可视化 dtreeviz 提供更丰富的可视化效果,包含分布饼图。 ```python import dtreeviz from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import load_breast_cancer data = load_breast_cancer() X = pd.DataFrame(data.data, columns=data.feature_names) y = pd.Series(data.target) clf = DecisionTreeClassifier(max_depth=3, min_samples_leaf=30) clf = clf.fit(X, y) viz_model = dtreeviz.model(clf, X_train=X, y_train=y, target_name='label', feature_names=X.columns, class_names={0: 'good', 1: 'bad'}) v = viz_model.view(fancy=False) v.show() v.save("img.svg") ```
### 决策树内部属性详解 通过 `clf.tree_` 可以访问决策树的内部结构: ```python # 查看所有属性和方法 [x for x in dir(clf.tree_) if not x.startswith('_')] # ['apply', 'capacity', 'children_left', 'children_right', # 'feature', 'impurity', 'max_depth', 'n_leaves', 'n_node_samples', # 'node_count', 'predict', 'threshold', 'value', ...] ``` #### 核心属性说明 | 属性 | 含义 | 示例值 | |------|------|--------| | `children_left` | 每个节点的左子节点索引,-1 为叶子 | `[1, 2, 3, -1, -1, -1, 7, -1, -1]` | | `children_right` | 每个节点的右子节点索引,-1 为叶子 | `[6, 5, 4, -1, -1, -1, 8, -1, -1]` | | `feature` | 节点分裂使用的特征索引,-2 为叶子 | `[20, 27, 1, -2, -2, -2, 7, -2, -2]` | | `threshold` | 分裂阈值(≤阈值进左,>阈值进右) | `[16.795, 0.133, 21.435, ...]` | | `value` | 每个节点的类别分布概率 | `[[[0.37, 0.63]], ...]` | | `impurity` | 每个节点的不纯度(基尼指数) | `[0.468, 0.159, 0.030, ...]` | | `n_node_samples` | 每个节点的样本数 | `[569, 379, 329, 269, ...]` | | `n_leaves` | 叶子节点数量 | `5` | | `node_count` | 总节点数 | `9` | | `capacity` | 预分配存储容量 | `9` | #### 关键解读 ``` 根节点(0): feature=20, threshold=16.795 → 特征20 ≤ 16.795 → 左子节点(1) → 特征20 > 16.795 → 右子节点(6) 9个节点中5个为叶子节点(-1),符合 max_depth=3 的限制 所有叶子节点的样本数之和 = 训练数据总数 569 ``` #### 分裂验证示例 ```python import numpy as np data_arr = np.array(X) label_arr = np.array(Y) # 根节点按特征20、阈值16.795分裂 left_labels = label_arr[data_arr[:, 20] <= clf.tree_.threshold[0]] right_labels = label_arr[data_arr[:, 20] > clf.tree_.threshold[0]] len(left_labels) # 379(左子树样本数) len(right_labels) # 190(右子树样本数) # 379 + 190 = 569 ✓ ```
### 策略生成 将决策树的规则提取为可读的策略表: ```python import numpy as np import pandas as pd from sklearn.datasets import load_breast_cancer data = load_breast_cancer() X = pd.DataFrame(data.data, columns=data.feature_names) y = pd.DataFrame(data.target, columns=['target']) from tpf.link.tree import Decision dcs = Decision() df = dcs.rules(X=X, y=y, max_depth=5) df ``` 策略生成将决策树的每条从根到叶子的路径转换为一条规则,便于业务理解和部署。 #### 属性查看方法 ```python # 查看特征重要性 clf.feature_importances_ # array([0., 0.0018, 0., ..., 0.8616, 0., 0., 0.1199, 0., 0.]) # 分裂节点有4个,特征重要性 > 0 的特征刚好4个 # value 属性详解 clf.tree_.value # shape: (9, 1, 2) # 9个节点,每个节点有2个值代表类别 [0, 1] 的概率 # 计算每个节点的类别样本数 a = clf.tree_.n_node_samples[:, np.newaxis, np.newaxis] value = np.round(a * clf.tree_.value).astype(np.int64) # [[[212, 357]], [[33, 346]], [[5, 324]], ...] ```
超参数优化
### 网格搜索(GridSearchCV) 使用交叉验证自动搜索最优参数组合: ```python from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import GridSearchCV # 定义参数搜索空间 param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [3, 5, 7, 10, None], 'min_samples_split': [2, 5, 10, 20], 'min_samples_leaf': [1, 5, 10, 20], } # 网格搜索 + 5折交叉验证 grid_search = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring='accuracy', n_jobs=-1 # 并行计算 ) grid_search.fit(X_train, y_train) # 最优参数和分数 print(f"最优参数: {grid_search.best_params_}") print(f"最优分数: {grid_search.best_score_:.4f}") # 使用最优模型 best_model = grid_search.best_estimator_ ``` > **注意**:网格搜索会遍历所有参数组合,参数空间大时计算量很大。可以考虑使用 `RandomizedSearchCV` 进行随机搜索。
### max_depth 调参详解 #### 深度与节点数的关系 ``` 2⁰ + 2¹ + 2² + ... + 2ⁿ = 2^(n+1) - 1 2⁶ = 64, 2⁷ = 128 通常特征列到不了128,所以 max_depth 取 6 就算大的了 ``` #### 调参策略 **策略一:不设限制 → 观察过拟合** ```python # 先让树自由生长 model = DecisionTreeClassifier() model.fit(X_train, y_train) print(f"训练集准确率: {model.score(X_train, y_train):.4f}") print(f"测试集准确率: {model.score(X_test, y_test):.4f}") print(f"树的深度: {model.get_depth()}") print(f"叶子节点数: {model.get_n_leaves()}") # 如果训练集准确率远高于测试集 → 过拟合 → 需要限制深度 ``` **策略二:交叉验证选最优深度** ```python from sklearn.model_selection import cross_val_score import matplotlib.pyplot as plt depths = range(1, 21) train_scores = [] test_scores = [] for depth in depths: model = DecisionTreeClassifier(max_depth=depth, random_state=42) model.fit(X_train, y_train) train_scores.append(model.score(X_train, y_train)) test_scores.append(model.score(X_test, y_test)) plt.plot(depths, train_scores, label='Train') plt.plot(depths, test_scores, label='Test') plt.xlabel('max_depth') plt.ylabel('Accuracy') plt.legend() plt.show() # 选择测试集准确率最高且与训练集差距不大的深度 ``` **策略三:结合数据特性** - 数据噪声多/异常值多 → 设置较小的 max_depth - 数据很干净 → 可尝试较大的 max_depth - 结合 `min_samples_leaf`、`min_samples_split` 共同调优
### 学习曲线分析 通过学习曲线判断模型是过拟合还是欠拟合: ```python from sklearn.model_selection import learning_curve import numpy as np model = DecisionTreeClassifier(max_depth=5, random_state=42) train_sizes, train_scores, test_scores = learning_curve( model, X_train, y_train, train_sizes=np.linspace(0.1, 1.0, 10), cv=5, scoring='accuracy' ) train_mean = train_scores.mean(axis=1) test_mean = test_scores.mean(axis=1) # 诊断 if train_mean[-1] - test_mean[-1] > 0.1: print("过拟合:训练集和测试集差距较大") print("建议:减小 max_depth,增大 min_samples_leaf") elif train_mean[-1] < 0.8 and test_mean[-1] < 0.8: print("欠拟合:训练集和测试集都表现不佳") print("建议:增大 max_depth,减小 min_samples_leaf") else: print("模型拟合良好") ``` #### 常见过拟合/欠拟合处理 | 问题 | 表现 | 解决方案 | |------|------|---------| | **过拟合** | 训练精度高,测试精度低 | 减小 max_depth,增大 min_samples_leaf | | **欠拟合** | 训练精度和测试精度都低 | 增大 max_depth,减小 min_samples_leaf |
优缺点与适用场景
### 决策树的优点 1. **可解释性强**:树结构直观可视,规则清晰,非技术人员也能理解 2. **无需特征缩放**:不需要标准化或归一化,对特征的量纲不敏感 3. **能处理混合类型数据**:同时处理数值型和类别型特征 4. **自动特征选择**:通过特征重要性自动筛选关键特征 5. **能处理缺失值**(部分实现) 6. **非线性建模能力**:能捕捉特征间的复杂非线性关系和交互效应 7. **训练和预测速度快**:训练复杂度 O(n·m·log(m)),预测复杂度 O(log(m)) 8. **对异常值有一定鲁棒性**:使用中位数或投票机制 > 其中 n 为特征数,m 为样本数。
### 决策树的缺点 1. **容易过拟合**:如果不限制树的深度,会完美拟合训练数据但泛化能力差 2. **不稳定**:训练数据的微小变化可能导致生成完全不同的树 3. **贪心算法的局限**:局部最优不保证全局最优 4. **对类别不平衡敏感**:容易偏向多数类 5. **难以捕捉线性关系**:对于线性可分的数据,决策树不如线性模型高效 6. **外推能力差**:不能预测训练数据范围之外的值(回归树) 7. **某些特征可能被忽略**:信息增益或基尼指数低的特征可能永远不会被选中 #### 单棵决策树 vs 集成方法 | 对比项 | 单棵决策树 | 随机森林 / GBDT | |--------|-----------|----------------| | 准确率 | 一般 | 高 | | 过拟合风险 | 高 | 低 | | 可解释性 | 强 | 弱 | | 稳定性 | 低 | 高 | | 适用场景 | 需要解释性 | 追求准确率 | > **实践建议**:如果追求准确率,优先使用集成方法(随机森林、XGBoost);如果需要可解释性,使用单棵决策树并控制深度。
### 适用场景 #### 最适合的场景 1. **需要模型可解释性**:医疗诊断、金融审批等需要向非技术人员解释决策过程 2. **特征工程探索**:通过特征重要性快速了解哪些特征对预测有用 3. **快速原型**:作为基线模型快速评估数据集的可分性 4. **规则提取**:将业务规则从数据中提取出来 #### sklearn API 速查 ```python # 分类树 from sklearn.tree import DecisionTreeClassifier clf = DecisionTreeClassifier( criterion='gini', # 特征选择标准 splitter='best', # 划分策略 max_depth=None, # 最大深度 min_samples_split=2, # 内部节点最小分裂样本数 min_samples_leaf=1, # 叶子节点最小样本数 max_features=None, # 划分时考虑的最大特征数 random_state=None, # 随机种子 max_leaf_nodes=None, # 最大叶子节点数 min_impurity_decrease=0, # 最小不纯度下降值 class_weight=None # 类别权重 ) # 回归树 from sklearn.tree import DecisionTreeRegressor reg = DecisionTreeRegressor( criterion='squared_error', # 回归用平方误差 max_depth=5, min_samples_leaf=10 ) ``` #### 不适合的场景 - 数据量大且特征多、需要高精度 → 使用随机森林或 XGBoost - 特征间存在强线性关系 → 使用线性模型(逻辑回归、线性回归) - 数据量非常小(<50样本) → 树容易过拟合
进阶:集成学习
### Bagging:随机森林(Random Forest) 随机森林是决策树的集成,通过构建**多棵决策树**来提高模型的准确性和稳定性。 #### 核心思想 ``` 1. 从训练集中有放回地随机抽取多个子集(Bootstrap Sampling) 2. 对每个子集训练一棵决策树 3. 每棵树在分裂时,随机选择部分特征(特征随机性) 4. 分类:多棵树投票 → 多数决定 回归:多棵树预测 → 取平均值 ``` #### 为什么有效 - 每棵树都有一定的"偏见",但通过**多棵树的平均**,偏见被消除 - 随机选择特征 → 减少强特征被每棵树都选中的概率 → 增加多样性 - 多棵树共同决策 → 大幅降低**过拟合**风险 #### sklearn 实现 ```python from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier( n_estimators=100, # 树的数量 max_depth=10, # 每棵树的最大深度 min_samples_leaf=5, random_state=42, n_jobs=-1 # 并行训练 ) model.fit(X_train, y_train) print(f"准确率: {model.score(X_test, y_test):.4f}") ```
### Boosting:GBDT / XGBoost Boosting 方法的核心思想是:**串行训练多棵树,每棵新树纠正前一棵树的错误**。 #### GBDT(Gradient Boosting Decision Tree) ``` 1. 训练第1棵树,得到预测值 2. 计算残差 = 真实值 - 预测值 3. 用残差作为目标值训练第2棵树 4. 重复步骤2~3,直到满足条件 5. 最终预测 = 所有树的预测值之和 ``` #### XGBoost / LightGBM XGBoost 和 LightGBM 是 GBDT 的高效实现,是目前机器学习竞赛和工业界最常用的算法之一。 ```python # sklearn 的 GBDT from sklearn.ensemble import GradientBoostingClassifier model = GradientBoostingClassifier( n_estimators=100, # 树的数量 max_depth=5, # 每棵树的深度(通常较浅) learning_rate=0.1, # 学习率 random_state=42 ) model.fit(X_train, y_train) ``` #### Boosting vs Bagging | 对比项 | Bagging(随机森林) | Boosting(GBDT) | |--------|-------------------|-----------------| | 训练方式 | 并行(独立训练每棵树) | 串行(依次训练每棵树) | | 目标 | 降低方差 | 降低偏差 | | 树的深度 | 较深(不剪枝) | 较浅(通常 depth=3~5) | | 过拟合风险 | 低 | 相对较高(需要正则化) | | 训练速度 | 快(可并行) | 慢(串行依赖) | | 准确率 | 高 | 通常更高 |
### 常用集成方法对比 #### 三大集成方法总结 | 方法 | 代表算法 | 核心思想 | sklearn 类 | |------|---------|---------|-----------| | **Bagging** | 随机森林 | 多棵独立树投票 | `RandomForestClassifier` | | **Boosting** | GBDT, XGBoost, LightGBM | 串行纠错 | `GradientBoostingClassifier` | | **Stacking** | 多模型融合 | 多个基模型的输出作为新特征 | `StackingClassifier` | #### 如何选择 ``` 单棵决策树:需要可解释性 → 直接用 随机森林:需要高准确率 + 相对简单 → 选它 GBDT/XGBoost/LightGBM:需要最高准确率 → 选它 ``` #### 进阶学习路径 ``` 决策树(基础) ├── 随机森林(Bagging 集成) ├── GBDT(Boosting 集成) ├── XGBoost(高效 GBDT) ├── LightGBM(更快 GBDT) └── CatBoost(处理类别特征) ``` > 决策树是所有这些高级算法的**基础**。理解好单棵决策树的原理,是学习集成学习的关键前提。
信息增益计算实例
### 信息增益核心公式 信息增益是决策树算法(如 **ID3**、**C4.5**)中选择最优划分特征的核心指标。它衡量的是:使用某个特征对数据集进行划分后,**不确定性(熵)减少了多少**。信息增益越大,说明该特征对分类的贡献越大。 $$ \text{信息增益} = \text{Entropy(划分前)} - \text{Entropy(划分后)} $$ #### 熵(Entropy)的定义 熵是信息论中衡量数据不确定性的指标。对于数据集 $S$,其熵定义为: $$ E(S) = -\sum_{i=1}^{c} p_i \log_2(p_i) $$ 其中: - $p_i$ 表示第 $i$ 类样本在数据集 $S$ 中所占的比例 - $c$ 为类别总数 **熵的特点:** - 当数据集完全纯净(所有样本属于同一类)时,$E(S) = 0$ - 当各类样本均匀分布时,熵最大,不确定性最高 #### 信息增益率的引入 > 信息增益倾向于选择**取值种类多**的特征(如用户ID),因为取值种类越多,每个取值对应的子集越小,越容易将数据划分得更"纯净"。这里的"取值多"不是指某个值的比例大,而是指该特征有**很多不同的取值(类别)**。例如"用户ID"有 1000 个不同值,每个 ID 只对应 1 条样本,按 ID 划分后每个子集都是纯的,但这种划分毫无意义。 > > 为解决这一问题,**C4.5 算法引入了"信息增益率"(Gain Ratio)**,通过对信息增益进行归一化来惩罚取值过多的特征。 #### 信息增益率的公式 $$ GainRatio(D, A) = \frac{Gain(D, A)}{SplitInfo(A)} $$ 其中分母 **SplitInfo(A)** 是特征 A 本身的熵(也叫分裂信息),用来衡量特征取值的均匀程度: $$ SplitInfo(A) = -\sum_{i=1}^{n} \frac{|D_i|}{|D|} \log_2 \frac{|D_i|}{|D|} $$ - $n$ 是特征 A 的取值个数 - $|D_i|$ 是取第 $i$ 个值的样本数 - $|D|$ 是总样本数 **直觉理解**: - 如果特征 A 有 1000 个不同取值且均匀分布 → $SplitInfo$ 很大 → $GainRatio$ 被大幅缩小 → 该特征被"惩罚" - 如果特征 A 只有 2 个取值 → $SplitInfo$ 较小 → $GainRatio$ 被缩小的幅度有限 - 因此,信息增益率能自动抑制取值种类过多的特征
### 用户流失预测:按"性别"划分 假设有 15 个用户样本,其中 Positive(已流失)5 人,Negative(未流失)10 人。 **整体熵:** $$ E(S) = -\frac{5}{15}\log_2\left(\frac{5}{15}\right) - \frac{10}{15}\log_2\left(\frac{10}{15}\right) = 0.9182 $$ #### 按"性别"划分 | 性别 | Positive | Negative | 汇总 | |:---:|:---:|:---:|:---:| | 男性 | 3 | 5 | 8 | | 女性 | 2 | 5 | 7 | $$ E(g_1) = -\frac{3}{8}\log_2\left(\frac{3}{8}\right) - \frac{5}{8}\log_2\left(\frac{5}{8}\right) = 0.9543 $$ $$ E(g_2) = -\frac{2}{7}\log_2\left(\frac{2}{7}\right) - \frac{5}{7}\log_2\left(\frac{5}{7}\right) = 0.8631 $$ $$ IGain(S, g) = E(S) - \frac{8}{15}E(g_1) - \frac{7}{15}E(g_2) = 0.0064 $$ **信息增益仅为 0.0064**,说明性别对用户流失的区分能力很弱。
### 用户流失预测:按"活跃度"划分 #### 按"活跃度"划分 | 活跃度 | Positive | Negative | 汇总 | |:---:|:---:|:---:|:---:| | 高 | 0 | 6 | 6 | | 中 | 1 | 4 | 5 | | 低 | 4 | 0 | 4 | $$ E(a_1) = 0, \quad E(a_2) = 0.7219 $$ $$ IGain(S, a) = E(S) - \frac{6}{15}E(a_1) - \frac{5}{15}E(a_2) - \frac{4}{15}E(a_3) = 0.6776 $$ #### 结论 活跃度的信息增益(**0.6776**)远大于性别的信息增益(**0.0064**),说明**活跃度对用户流失的影响比性别大得多**。在做特征选择或数据分析时,应重点考察活跃度这个指标。 #### 纯度(Purity)的概念 | 状态 | 描述 | 纯度 | |:---:|:---|:---:| | 不纯 | 节点中包含多种类别,混乱程度高 | 低 | | 有点不纯 | 节点中某类占主导,但仍含少量其他类 | 中 | | 够纯 | 节点中基本属于同一类别 | 高 | - **熵越低,纯度越高** - **基尼值越低,纯度越高**
基尼不纯度计算实例
### 贷款违约预测数据集 基尼不纯度是 **CART 决策树**(Classification and Regression Trees)算法中使用的划分标准。它表示:从数据集中随机抽取两个样本,其类别标记不一致的概率。 **基尼公式:** $$ Gini(D) = 1 - \sum_{k=1}^{n} p_{i,k}^2 $$ **基尼增益:** $$ \Delta Gini = Gini(父节点) - \sum_{j} \frac{|D_j|}{|D|} Gini(D_j) $$ 决策树选择**基尼增益最大**的特征进行划分。 #### 数据集(共 10 条记录) | 序号 | 是否有房 | 婚姻状况 | 年收入 | 是否拖欠贷款 | |:---:|:---:|:---:|:---:|:---:| | 1 | yes | single | 125K | no | | 2 | no | married | 100K | no | | 3 | no | single | 70K | no | | 4 | yes | married | 120K | no | | 5 | no | divorced | 95K | yes | | 6 | no | married | 60K | no | | 7 | yes | divorced | 220K | no | | 8 | no | single | 85K | yes | | 9 | no | married | 75K | no | | 10 | no | single | 90K | yes | #### 根节点的基尼系数 其中拖欠贷款(yes)3 条,不拖欠(no)7 条: $$ Gini(\text{是否拖欠贷款}) = 1 - \left(\frac{3}{10}\right)^2 - \left(\frac{7}{10}\right)^2 = 0.42 $$
### 按"是否有房"划分 | 是否拖欠贷款 | 有房产 | 无房产 | |:---:|:---:|:---:| | Yes | 0 | 3 | | No | 3 | 4 | - 左子节点(有房产):3 条记录,全部不拖欠 $$Gini(左) = 1 - \left(\frac{0}{3}\right)^2 - \left(\frac{3}{3}\right)^2 = 0$$ - 右子节点(无房产):7 条记录,3 条拖欠,4 条不拖欠 $$Gini(右) = 1 - \left(\frac{3}{7}\right)^2 - \left(\frac{4}{7}\right)^2 = 0.4898$$ **基尼增益:** $$ \Delta Gini(\text{是否有房}) = 0.42 - \frac{7}{10} \times 0.4898 - \frac{3}{10} \times 0 = 0.077 $$ > 有房产的节点 Gini = 0,表示完全纯净(全部不拖欠),这是理想情况。
### 按"婚姻状况"划分 婚姻状况有三种取值:single、married、divorced。CART 只支持二分划分,需要尝试不同的分组方式,选择基尼增益最大的分组。 #### 分组方式一:{single} | {married, divorced} - single 组(4 条):2 yes, 2 no $$Gini(single) = 1 - 0.5^2 - 0.5^2 = 0.5$$ - married+divorced 组(6 条):1 yes, 5 no $$Gini(married+divorced) = 1 - (1/6)^2 - (5/6)^2 = 0.2778$$ $$ \Delta Gini = 0.42 - \frac{4}{10} \times 0.5 - \frac{6}{10} \times 0.2778 = 0.053 $$ #### 分组方式二:{divorced} | {single, married} - divorced 组(2 条):1 yes, 1 no → Gini = 0.5 - single+married 组(8 条):2 yes, 6 no → Gini = 0.375 $$ \Delta Gini = 0.42 - \frac{2}{10} \times 0.5 - \frac{8}{10} \times 0.375 = 0.02 $$ #### 分组方式三:{married} | {single, divorced}(最优) 对比以上分组结果,婚姻状况的最优分组为 **{married} | {single, divorced}**,此时基尼增益最大。 #### 三种特征对比 | 特征 | 基尼增益 | |:---:|:---:| | 是否有房 | 0.077 | | 婚姻状况(最优分组) | > 0.053 | | 年收入(最优划分点) | 0.12 |
连续特征处理
### 连续特征的相邻中值法 对于连续型特征(如"年收入"),决策树不能直接按离散值划分,需要将其转化为二分问题。常用的方法是: 1. 将连续特征的所有取值按**从小到大排序** 2. 取**相邻两个值的中点**作为候选划分点 3. 计算每个候选划分点的基尼增益(或信息增益) 4. 选择增益最大的中点作为**最优划分点** > **补充说明:** 对于连续特征,C4.5 和 CART 的处理方式类似,都是在排序后的取值中寻找最佳分割点。不同的是,C4.5 使用信息增益率,而 CART 使用基尼系数。时间复杂度为 $O(n \log n)$(排序)加上 $O(n)$ 的扫描。
### 年收入排序及候选划分点 将年收入按从小到大排序,并计算相邻中值: | 是否拖欠 | no | no | no | yes | yes | yes | no | no | no | no | |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| | 年收入 | 60 | 70 | 75 | 85 | 90 | 95 | 100 | 120 | 125 | 220 | | 相邻中点 | 65 | 72.5 | 80 | 87.5 | 92.5 | 97.5 | 110 | 122.5 | 172.5 | - | | Gini增益 | 0.02 | 0.045 | 0.077 | 0.003 | 0.02 | **0.12** | 0.077 | 0.045 | 0.02 | - | 由表可知,当划分点为 **97.5** 时,基尼增益最大(**0.12**),因此选择年收入 ≤ 97.5 和 > 97.5 作为划分。 #### 最终各特征基尼增益对比 | 特征 | 最优划分点/分组 | 基尼增益 | |:---:|:---:|:---:| | 是否有房 | yes / no | 0.077 | | 婚姻状况 | {married} / {single, divorced} | > 0.053 | | **年收入** | **≤ 97.5 / > 97.5** | **0.12** | > 年收入的基尼增益最大,因此在根节点应优先选择年收入作为划分特征。但根据实际课件图示,最终的决策树根节点选择了婚姻状况,这可能是因为综合比较后的结果。无论如何,决策树的构建原则是一致的:**每次选择使不纯度下降最多的特征进行划分**。
决策树生成过程
### 第一步:选择根节点 比较各特征的基尼增益: - 是否有房:0.077 - 婚姻状况(最优分组):大于 0.053 - 年收入(最优划分点 97.5):0.12 根据课件第7页的决策树图示,根节点选择了**婚姻状况**,划分为 married 和 single/divorced。 > **说明:** 决策树的构建原则是一致的:**每次选择使不纯度下降最多的特征进行划分**。实际构建中可能综合比较后选择了婚姻状况。
### 决策树结构示例(贷款违约预测) 根据课件图示,最终生成的决策树结构如下: ``` [婚姻状况] / \ married single, divorced | | no(4) [是否有房] / \ yes no | | no(2) [年收入] / \ ≤ 77.5 > 77.5 | | no(1) yes(3) ``` **解读:** - 如果已婚(married)→ 不拖欠(no),共 4 个样本 - 如果未婚/离异(single/divorced)且有房 → 不拖欠(no),共 2 个样本 - 如果未婚/离异且无房,年收入 ≤ 77.5 → 不拖欠(no),1 个样本 - 如果未婚/离异且无房,年收入 > 77.5 → 拖欠(yes),3 个样本
### 递归构建过程 对于每个子节点,重复以下过程: 1. **计算**当前节点数据集的基尼系数(或熵) 2. **计算**对剩余特征计算基尼增益(或信息增益) 3. **选择**增益最大的特征进行划分 4. **递归**直到满足停止条件 #### 停止条件 | 停止条件 | 说明 | |:---|:---| | 节点纯度足够高 | 基尼系数或熵低于阈值 | | 样本数过少 | 节点中样本数少于 `min_samples_split` | | 所有特征已用完 | 没有更多特征可供划分 | | 增益不足 | 信息增益或基尼增益低于 `min_impurity_decrease` | | 达到最大深度 | 树的深度达到 `max_depth` | #### ID3、C4.5、CART 算法对比 | 特性 | ID3 | C4.5 | CART | |:---|:---|:---|:---| | **提出时间** | 1986 | 1993 | 1984 | | **划分标准** | 信息增益 | 信息增益率 | 基尼系数 | | **特征类型** | 离散值 | 离散值 + 连续值 | 离散值 + 连续值 | | **缺失值处理** | 不支持 | 支持 | 支持 | | **树类型** | 多叉树 | 多叉树 | **二叉树** | | **任务类型** | 分类 | 分类 | 分类 + 回归 | | **剪枝策略** | 无 | 后剪枝 | 后剪枝 |
剪枝详解
### 为什么要剪枝 决策树如果生长得太深,会过度分析训练数据中的噪声,导致 **过拟合(Overfitting)**。过拟合的模型在训练集上表现很好,但在测试集上表现很差。 **核心问题:** - 100 = 99 + 1 时,这个"1"可能是噪声 - 没有必要为了分出这个"1"再增加一个分支 - 树的结点总数越多,训练精度越高,但测试精度可能下降 **决策树的最终效果是:数据特征与算法的综合结果**。树最后的分支,熵/基尼系数低到一定阈值即可,不必为 0——这正是剪枝的思想。
### 预剪枝(Pre-Pruning) 在树构建过程中提前停止生长: | 参数/策略 | 说明 | |:---|:---| | **最大深度(max_depth)** | 限制树的最大深度,防止过深 | | **最小样本分裂数(min_samples_split)** | 节点样本数少于该值时不再分裂 | | **最小叶子样本数(min_samples_leaf)** | 叶子节点的最少样本数 | | **信息增益阈值(min_impurity_decrease)** | 增益低于阈值时不分裂 | #### sklearn 决策树核心参数 基于 `sklearn.tree.DecisionTreeClassifier`: | 参数 | 默认值 | 说明 | |:---|:---|:---| | `criterion` | `'gini'` | 划分标准:`'gini'` 基尼系数;`'entropy'` 信息熵 | | `splitter` | `'best'` | 分裂策略:`'best'` 最优分裂;`'random'` 随机分裂 | | `max_depth` | `None` | 树最大深度,`None` 表示不限制 | | `min_samples_split` | `2` | 节点最少样本数,少于该数不再分裂 | | `min_samples_leaf` | `1` | 叶子节点最少样本数 | > 预剪枝实现简单、效率高,但可能因过早停止而错过后续的重要划分("近视"问题)。
### 后剪枝(Post-Pruning)与 CCP 先构建完整的树,再**自底向上**剪去对泛化能力无贡献的分支。 > **重要澄清:后剪枝 ≠ 特征选择** > > 后剪枝**不是**根据"特征重要性"选择前 N 个特征。特征选择是在建模前从所有特征中挑选最相关的子集;而后剪枝是在树**已经生成之后**,直接剪掉某些分支(子树),将其替换为一个叶子节点。 #### 代价复杂度剪枝(CCP)详解 **CCP 是 sklearn 中 `DecisionTreeClassifier` 使用的后剪枝方法**,其核心是为每棵子树定义一个"代价复杂度": $$ R_\alpha(T) = R(T) + \alpha \cdot |T_{leaf}| $$ 其中: - $R(T)$:树 $T$ 在训练数据上的预测误差(如误分类率或 MSE) - $|T_{leaf}|$:树 $T$ 的**叶子节点数量**,代表模型复杂度 - $\alpha$:复杂度参数(非负),$\alpha$ 越大,惩罚越重,剪枝越激进 **剪枝过程:** 1. **生成完全树 $T_0$**:不做任何限制,让树生长到最深层 2. **计算每个内部节点的"剪枝强度"** $g(t)$: $$ g(t) = \frac{R(t) - R(T_t)}{|T_{t,leaf}| - 1} $$ 3. **迭代剪枝**:找到 $g(t)$ 最小的节点,将其子树剪除,得到更小的树 $T_1$,重复直到只剩根节点 4. **交叉验证选择最优树**:用交叉验证在验证集上评估,选择误差最小的 $T_k$ ```python from sklearn.tree import DecisionTreeClassifier # ccp_alpha 就是公式中的 α clf = DecisionTreeClassifier(ccp_alpha=0.01, random_state=42) # 获取不同 ccp_alpha 值对应的剪枝路径 path = clf.cost_complexity_pruning_path(X_train, y_train) ccp_alphas, impurities = path.ccp_alphas, path.impurities ```
### 预剪枝 vs 后剪枝 | 维度 | 预剪枝 | 后剪枝 | |:---|:---|:---| | **时机** | 构建过程中停止 | 构建完成后修剪 | | **数据使用** | 训练集本身 | 通常需要额外验证集 | | **效果** | 容易"近视",可能错过后续重要划分 | 通常泛化能力更好 | | **计算成本** | 低(提前停止) | 高(先生成完全树) | | **代表** | `max_depth`、`min_samples_split` | CCP(sklearn `ccp_alpha`) | | **风险** | 欠拟合(Underfitting) | 计算开销大 | #### 常见的后剪枝算法 | 算法 | 代表实现 | 核心思想 | |:---|:---|:---| | **错误率降低剪枝(REP)** | C4.5 | 用验证集直接比较剪枝前后的分类错误率 | | **悲观错误剪枝(PEP)** | C4.5 早期版本 | 用训练集误差加上统计修正项来估计泛化误差 | | **代价复杂度剪枝(CCP)** | CART、sklearn | 引入复杂度惩罚项,通过参数 α 平衡拟合与复杂度 | | **最小错误剪枝(MEP)** | — | 基于概率估计的贝叶斯方法 | > **实践建议:** 在 sklearn 中,通常先使用预剪枝参数(如 `max_depth`、`min_samples_leaf`)做粗调限制过拟合;如果需要更精细地控制模型复杂度,再结合 `ccp_alpha` 进行后剪枝。也可以**同时使用**预剪枝和后剪枝。**交叉验证**是确定最优剪枝参数的标准方法。
参考