1. 决策树概述
决策树(Decision Tree)是一种树形结构的监督学习方法,可用于分类和回归任务。每个内部节点表示一个特征上的判断条件,每个叶节点表示一个类别(分类树)或一个连续值(回归树)。
1.1 决策树示例
天气?
/ | \
晴天 多云 雨天
/ \ \
湿度? ✓出门 风力?
/ \ / \
高 低 强 弱
✗ ✓ ✗ ✓
1.2 核心思想
决策树学习的本质是:从训练数据中归纳出一组分类规则,使得树的结构与数据的分布尽可能一致,同时具有良好的泛化能力。
1.3 决策树学习算法
常见的决策树学习算法包括:
| 算法 | 特征选择标准 | 支持任务 |
|---|---|---|
| ID3 | 信息增益 | 分类 |
| C4.5 | 信息增益比 | 分类 |
| CART | 基尼系数/均方误差 | 分类/回归 |
2. 特征选择
2.1 信息熵
信息熵是衡量随机变量不确定性的度量:
当所有概率相等()时,熵最大;当某一类概率为1时,熵为0。
2.2 条件熵
给定随机变量 后, 的条件熵:
2.3 信息增益
信息增益是得知特征 后,特征 的不确定性减少量:
在ID3算法中,选择信息增益最大的特征进行分裂。
2.4 信息增益比
信息增益偏好取值多的特征,C4.5使用信息增益比进行修正:
2.5 基尼系数
CART算法使用基尼系数:
基尼系数表示从数据集中随机抽取两个样本,其类别不一致的概率。基尼系数越小,数据纯度越高。
3. ID3算法
3.1 算法流程
输入:训练数据集 D,特征集 A
输出:决策树 T
1. 若 D 中所有实例属于同一类 C,则 T 为单节点树
2. 若 A 为空,则 T 为单节点树,将 D 中实例数最多的类作为该节点的类标记
3. 否则,按算法选择最优特征 A_g:
- 计算每个特征的信息增益
- 选择信息增益最大的特征
4. 对 A_g 的每一个可能值 a_i:
- 为 T 生成一个节点
- 将 D 中 A_g = a_i 的子集 D_i 作为该节点的训练数据
5. 递归地对 D_i 调用步骤 1-4
3.2 示例
假设数据集 包含两类,熵 。
特征 将数据分成两部分:
- :,有3个样本
- :,有7个样本
则条件熵:
信息增益:
4. C4.5算法
C4.5是ID3的改进版本,主要改进:
- 使用信息增益比选择特征
- 可以处理连续特征(通过二分法)
- 可以处理缺失值
- 在树构建过程中进行剪枝
5. CART算法
5.1 CART分类树
CART(Classification and Regression Tree)使用二叉树结构,每个节点只产生两个子节点。
对于分类树,使用基尼系数选择最优特征和切分点。
5.2 CART回归树
对于回归树,使用平方误差作为损失函数:
其中:
- 是特征索引
- 是切分点
- 和 是根据 划分的两个区域
6. 决策树剪枝
6.1 预剪枝
在决策树生成过程中,提前停止分裂。
优点:减少过拟合风险,减少训练时间
缺点:可能欠拟合
6.2 后剪枝
先生成完整的决策树,然后自底向上剪枝。
REP剪枝(Reduced Error Pruning):
- 使用验证集评估每个节点
- 如果剪枝后验证集精度不下降,则剪枝
代价复杂度剪枝(CCP,CART使用):
其中 是训练误差, 是叶节点数, 是惩罚参数。
7. 随机森林
7.1 集成学习思想
随机森林(Random Forest)是Bagging的扩展,通过构建多棵决策树并集成它们的预测结果来提高准确性和稳定性。
7.2 随机森林的两重随机性
- Bagging:每棵树的训练数据是通过有放回抽样从原始数据集中抽取的
- 特征随机:每棵树在分裂时,只从随机选取的特征子集中选择最优特征
7.3 算法流程
输入:训练集 D,决策树个数 T,特征子集大小 m
对于 t = 1 到 T:
1. 使用Bootstrap抽样从 D 中抽取训练集 D_t
2. 使用 D_t 训练一棵决策树:
- 在每个节点分裂时,随机选择 m 个特征
- 从这 m 个特征中选择最优特征进行分裂
3. 不进行剪枝(或只做少量剪枝)
输出:随机森林 {T_1, T_2, ..., T_T}
通常 (分类)或 (回归),其中 是特征总数。
7.4 预测
-
分类:多数投票
-
回归:平均
8. 代码实现
8.1 决策树实现
import numpy as np
from collections import Counter
class DecisionTree:
def __init__(self, max_depth=10, min_samples_split=2, criterion='gini'):
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.criterion = criterion
self.tree = None
def _gini(self, y):
counts = Counter(y)
probs = [count / len(y) for count in counts.values()]
return 1 - sum(p ** 2 for p in probs)
def _entropy(self, y):
counts = Counter(y)
probs = [count / len(y) for count in counts.values()]
return -sum(p * np.log2(p) for p in probs if p > 0)
def _best_split(self, X, y):
best_gain = -1
best_feat = None
best_thresh = None
criterion_func = self._gini if self.criterion == 'gini' else self._entropy
current_impurity = criterion_func(y)
for feat in range(X.shape[1]):
thresholds = np.unique(X[:, feat])
for thresh in thresholds:
left_mask = X[:, feat] <= thresh
right_mask = ~left_mask
if sum(left_mask) == 0 or sum(right_mask) == 0:
continue
left_impurity = criterion_func(y[left_mask])
right_impurity = criterion_func(y[right_mask])
n_left, n_right = sum(left_mask), sum(right_mask)
gain = current_impurity - (n_left * left_impurity + n_right * right_impurity) / len(y)
if gain > best_gain:
best_gain = gain
best_feat = feat
best_thresh = thresh
return best_feat, best_thresh
def _build_tree(self, X, y, depth=0):
if len(np.unique(y)) == 1 or depth >= self.max_depth or len(y) < self.min_samples_split:
return Counter(y).most_common(1)[0][0]
feat, thresh = self._best_split(X, y)
if feat is None:
return Counter(y).most_common(1)[0][0]
left_mask = X[:, feat] <= thresh
right_mask = ~left_mask
tree = {
'feature': feat,
'threshold': thresh,
'left': self._build_tree(X[left_mask], y[left_mask], depth + 1),
'right': self._build_tree(X[right_mask], y[right_mask], depth + 1)
}
return tree
def fit(self, X, y):
self.tree = self._build_tree(np.array(X), np.array(y))
def _predict_sample(self, x, tree):
if not isinstance(tree, dict):
return tree
if x[tree['feature']] <= tree['threshold']:
return self._predict_sample(x, tree['left'])
return self._predict_sample(x, tree['right'])
def predict(self, X):
return [self._predict_sample(x, self.tree) for x in X]8.2 随机森林实现
class RandomForest:
def __init__(self, n_trees=100, max_depth=10, min_samples_split=2, max_features='sqrt'):
self.n_trees = n_trees
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_features = max_features
self.trees = []
def _bootstrap_sample(self, X, y):
n_samples = X.shape[0]
indices = np.random.choice(n_samples, n_samples, replace=True)
return X[indices], y[indices]
def fit(self, X, y):
X, y = np.array(X), np.array(y)
n_features = X.shape[1]
if self.max_features == 'sqrt':
max_feat = int(np.sqrt(n_features))
elif self.max_features == 'log2':
max_feat = int(np.log2(n_features))
else:
max_feat = n_features
self.trees = []
for _ in range(self.n_trees):
X_boot, y_boot = self._bootstrap_sample(X, y)
# 随机选择特征子集
feat_indices = np.random.choice(n_features, max_feat, replace=False)
X_boot_subset = X_boot[:, feat_indices]
tree = DecisionTree(max_depth=self.max_depth,
min_samples_split=self.min_samples_split)
tree.fit(X_boot_subset, y_boot)
self.trees.append((tree, feat_indices))
def predict(self, X):
X = np.array(X)
predictions = []
for tree, feat_indices in self.trees:
preds = tree.predict(X[:, feat_indices])
predictions.append(preds)
# 多数投票
result = []
for i in range(X.shape[0]):
votes = [p[i] for p in predictions]
result.append(Counter(votes).most_common(1)[0][0])
return np.array(result)8.3 使用sklearn
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 决策树
dt = DecisionTreeClassifier(max_depth=5, criterion='gini')
dt.fit(X_train, y_train)
dt_pred = dt.predict(X_test)
print(f"Decision Tree Accuracy: {accuracy_score(y_test, dt_pred):.4f}")
# 随机森林
rf = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42)
rf.fit(X_train, y_train)
rf_pred = rf.predict(X_test)
print(f"Random Forest Accuracy: {accuracy_score(y_test, rf_pred):.4f}")
# 特征重要性
print("\nFeature Importances:")
for name, importance in zip(iris.feature_names, rf.feature_importances_):
print(f" {name}: {importance:.4f}")
# Out-of-Bag Score
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X_train, y_train)
print(f"\nOOB Score: {rf_oob.oob_score_:.4f}")9. 随机森林的特有性质
9.1 Out-of-Bag (OOB) 误差
由于每棵树只使用了约63.2%的原始数据(约1 - 1/e),剩余的约36.8%样本可以作为验证集。
9.2 特征重要性
随机森林可以计算特征重要性:
基尼重要性(Mean Decrease in Gini):
9.3 泛化误差上界
随机森林的泛化误差上界为:
其中 是树之间的平均相关性, 是单棵树的分类强度。
10. 优缺点分析
决策树
| 优点 | 缺点 |
|---|---|
| 简单直观,易解释 | 容易过拟合 |
| 可以处理非线性关系 | 对噪声敏感 |
| 不需要特征缩放 | 小的变化可能导致树结构大变化 |
| 可以处理混合特征 | 不稳定 |
随机森林
| 优点 | 缺点 |
|---|---|
| 减少过拟合 | 计算成本高 |
| 可以处理高维数据 | 比单棵决策树难解释 |
| 可以并行训练 | 在某些噪声大的问题上可能过拟合 |
| 不用特征缩放 | |
| 可以估计不确定性 |
11. 应用场景
11.1 特征选择
随机森林的特征重要性可以用于特征选择,筛选出最重要的特征。
11.2 异常检测
孤立森林(Isolation Forest)是基于决策树的异常检测方法。
11.3 回归任务
随机森林回归器(RandomForestRegressor)可用于回归任务。