决策树是分类和回归方面最直观和最广泛使用的机器学习算法之一。它们通过将数据分割成基于特征值的分支,模仿人类的决策方式来工作。 类似sikit ⁇ learn这样的图书馆使建设决策树变得微不足道,从零开始实施决策树是初学者掌握算法内在功能的极好方法。 这个指导会指导你通过理论和代码,从而你能够从地面上建立自己的决策树。

决策树是什么?

决策树是一种流程图式结构,每个内部节点代表对特性的测试(如“年龄大于30岁? ”),每个分支代表测试结果,每个叶子节点都持有类标签或连续值。目标是通过学习从数据特征中推断出来的简单的决策规则来创建预测目标变量的模型。决策树很受欢迎,因为它们容易解释,不需要多少数据预处理(不缩放或正常化)。

树是递归式构建的: 从根开始, 算法选择最干净的数据分离的最佳特征和分割点。 在每个子集中重复此过程直到满足停止条件。 对于更多的背景, 维基百科在决策树学习上的条目 [[FLT: 0] [[FLT: 1] 提供了坚实的概览 。

您必须理解的核心概念

节点、分支和叶子

根节点包含整个训练数据集. 内部节点测试一个特性,并将数据分成两个或两个以上的子节点. 分支是代表测试结果的连接. 叶节点(terminal nodes)输出最终预测 — — 分类中最常见的类或回归中的平均值.

拆分标准

要建立树,需要一种方法来测量潜在分裂的质量。最常见的标准是:

  • Gini杂质 – 分类中用于衡量随机选择的元素如果按照子集中的分类分布随机标记,其标记会多多错。下Gini更好。
  • Entropy — 在一个集合中测量混乱或不确定性的量。 目标是在分裂后最小化Entropy(信息收益 ) 。
  • 变量还原 – 用于回归树。它计算了拆分实现的差(或平均平方差)的还原。

该算法评价每个特征上每一个可能的分裂,并选择产生最大幅度的杂质减少(或信息增益)的分法.

信息损益比率

信息增益是父节点杂质与子杂质加权和的差数。虽然简单,但它倾向于偏爱具有许多值的特性。增益比(在 C4.5 中使用) 使这个规范化。对于此教程,我们将使用 Gini杂质来坚持标准的信息增益,这是 CART( 分类和递归树) 中的默认值。

逐步建立决策树

1. 准备数据

您需要一个带有特性和目标标签的数据集。为了简单起见,请使用带有数字特性的二进制分类数据集。例如:

  • 年龄、收入
  • 目标: 核准(1)或不核准(0)

清除数据:处理缺失值、删除重复数据并确保数字类型。 决策树可以处理混合数据类型,但我们会坚持数字执行。

2. 定义分块标准函数

我们将实施基尼杂质。

其中p i是类i中项目的比例。对于二进制分割,总体基尼是子节点的加权平均值。

3. 实施分块评价

对每个特性, 排序独有的值。 测试每个可能的阈值( 连续排序值之间的中间点)。 对于每个候选阈值, 将数据分为左右组, 计算吉尼, 并跟踪最佳的分割 。

4. 依次建设树木

创建一个需要子集数据和当前深度的函数。它检查停止条件(例如最大深度、每个节点的最小样本,或者没有信息收益)。如果满足了条件,则创建一个带有多数类的叶节点。否则,找到最佳的分解并创建一个内部节点,然后在左右分解时循环调用函数。

5. 作出预测

树建成后,预测是直截了当的:从根开始,通过对新样品的特征测试来跟踪树枝,并返回你降落在叶子上的值.

在 Python 中全面实施

下面是使用基尼杂质分类的完整、最小的操作决策树。 这个代码是用于学习的 — — 并不是用于大数据集的优化。

import numpy as np
from collections import Counter

class DecisionTree:
 def __init__(self, max_depth=None, min_samples_split=2):
 self.max_depth = max_depth
 self.min_samples_split = min_samples_split
 self.tree = None

 def fit(self, X, y):
 dataset = np.column_stack((X, y))
 self.tree = self._grow_tree(dataset)

 def _grow_tree(self, dataset, depth=0):
 X, y = dataset[:, :-1], dataset[:, -1]
 n_samples, n_features = X.shape
 n_labels = len(np.unique(y))

 # Stopping conditions
 if (n_labels == 1 or depth == self.max_depth or n_samples < self.min_samples_split):
 leaf_value = Counter(y).most_common(1)[0][0]
 return {'leaf': True, 'value': leaf_value}

 best_feature, best_threshold = self._best_split(dataset, n_features)
 if best_feature is None:
 leaf_value = Counter(y).most_common(1)[0][0]
 return {'leaf': True, 'value': leaf_value}

 left_idx, right_idx = self._split(dataset[:, best_feature], best_threshold)
 left_subtree = self._grow_tree(dataset[left_idx], depth+1)
 right_subtree = self._grow_tree(dataset[right_idx], depth+1)
 return {'leaf': False,
 'feature': best_feature,
 'threshold': best_threshold,
 'left': left_subtree,
 'right': right_subtree}

 def _best_split(self, dataset, n_features):
 best_gini = float('inf')
 best_feature, best_threshold = None, None
 for feature in range(n_features):
 thresholds = np.unique(dataset[:, feature])
 for i in range(len(thresholds)-1):
 thresh = (thresholds[i] + thresholds[i+1]) / 2
 left_idx, right_idx = self._split(dataset[:, feature], thresh)
 if len(left_idx) == 0 or len(right_idx) == 0:
 continue
 gini = self._gini_gain(dataset, left_idx, right_idx)
 if gini < best_gini:
 best_gini = gini
 best_feature = feature
 best_threshold = thresh
 return best_feature, best_threshold

 def _split(self, values, threshold):
 left_idx = np.where(values <= threshold)[0]
 right_idx = np.where(values > threshold)[0]
 return left_idx, right_idx

 def _gini_gain(self, dataset, left_idx, right_idx):
 total = len(left_idx) + len(right_idx)
 gini_left = self._gini(dataset[left_idx, -1])
 gini_right = self._gini(dataset[right_idx, -1])
 return (len(left_idx)/total) * gini_left + (len(right_idx)/total) * gini_right

 def _gini(self, labels):
 _, counts = np.unique(labels, return_counts=True)
 p = counts / np.sum(counts)
 return 1 - np.sum(p**2)

 def predict(self, X):
 return np.array([self._predict_row(x, self.tree) for x in X])

 def _predict_row(self, x, node):
 if node['leaf']:
 return node['value']
 if x[node['feature']] <= node['threshold']:
 return self._predict_row(x, node['left'])
 else:
 return self._predict_row(x, node['right'])

测试树

使用一个简单的数据集, 如经典的 iris 数据集( 二进制分类有两个功能 ) 。 [[ FLT: 0]] 的 scikit liarn Iris 数据集 [[ [FLT: 1]] 效果良好 。 将您的树的准确性与 scikit liarn 的 [ [FLT: 2] 比较以验证正确性 。

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

data = load_iris()
X = data.data[:100] # take only first two classes (binary)
y = data.target[:100]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

tree = DecisionTree(max_depth=3)
tree.fit(X_train, y_train)
preds = tree.predict(X_test)
accuracy = np.mean(preds == y_test)
print(f'Accuracy: {accuracy:.2f}')

改进树种的高级技术

运行以避免过度配齐

完全生长的树可以记忆训练数据中的噪音。 运行会清除没有预测力的树枝。 常见的方法包括: 预 + 运行( 通过 或 [ [FLT: 5] 提前停止生长) 和 后 + 运行( 生长全树然后使用验证集或成本+ 复杂度 运行去除树枝)。 我们的执行已经支持预 + 运行 。

处理连续和分类特征

对于连续特性,我们使用排序值之间的中点作为阈值。对于绝对特性(例如“Color = 红/绿/蓝”),每个类别都可以成为单独的分支(multil-way division)或者可以二进制编码它们。大多数现代执行(如 scikit-learn)甚至通过评价所有子集来使用二进制分割来进行绝对特性。

处理缺失值

Real world 数据往往有缺失值。一个简单的方法就是在具有该特征的训练样本中指定最频繁的分支缺失值。C4.5 使用概率法。由于这是一个初学者的教程,我们假设数据是完整的。

与图书馆和进一步阅读的比较

虽然从零开始建设是教育性的,但生产系统使用图书馆,如提供优化C执行的sikit ⁇ learn。您可以从官方[]sikit ⁇ learn决策树文献中学习更多。对于更深入的理论,Hastie,Tibshirani和Friedman的著作“统计学习要素”是一个权威资源。另一个极好的参考文献是Breiman等人的CART原始书。

结论

从头开始建立决策树,可以解密机器学习中最根本的算法之一。你已经学会了简单的递归分解程序如何产生强大的模型。通过自己写代码,你就能更深入地了解杂质度量、分拣选择以及偏差和偏差之间的权衡。下一步,尝试加入回归支持、推算或处理绝对特征。你在这里开发的技能将服务于你,同时你将转向更复杂的组合方法,如随机森林和梯度提升。