共计 3994 个字符,预计需要花费 10 分钟才能阅读完成。
决策树基础与 ID3 原理
决策树是一种模仿人类决策过程的树形结构,而 ID3 算法是最经典的决策树构建算法之一。理解它的核心在于掌握两个关键概念:信息熵和条件熵。

-
信息熵(Entropy):度量数据不确定性的指标,公式为:
$$H(D)=-\sum_{k=1}^{K}p_k\log_2 p_k$$
其中 $p_k$ 是第 k 类样本在数据集 D 中的比例 -
条件熵(Conditional Entropy):已知特征 X 的条件下,数据集 D 的不确定性:
$$H(D|X)=\sum_{i=1}^{n}\frac{|D_i|}{|D|}H(D_i)$$ -
信息增益(Information Gain):选择划分特征的依据:
$$Gain(D,X)=H(D)-H(D|X)$$
ID3 算法就是通过递归选择信息增益最大的特征来构建决策树。
为什么选择 ID3 而不是其他算法
- ID3:
- 仅支持离散特征
- 使用信息增益作为划分标准
-
容易偏向取值多的特征
-
C4.5:
- 引入信息增益率解决 ID3 的偏向问题
- 支持连续特征(通过二分法离散化)
-
支持缺失值处理
-
CART:
- 支持分类和回归
- 使用基尼系数作为划分标准
- 二叉树结构
对于初学者来说,ID3 算法实现简单,是理解决策树的最佳起点。
C++ 实现核心数据结构
class TreeNode {
public:
std::string featureName; // 划分特征名
std::string label; // 叶节点的类别标签
std::unordered_map<std::string, std::shared_ptr<TreeNode>> children; // 子节点
bool isLeaf = false; // 是否是叶节点
};
class DecisionTree {
private:
std::shared_ptr<TreeNode> root;
// 计算数据集的信息熵
double calculateEntropy(const std::vector<std::vector<std::string>>& data,
int labelIndex);
// 计算特征的信息增益
double calculateInfoGain(const std::vector<std::vector<std::string>>& data,
int featureIndex, int labelIndex);
// 递归构建决策树
void buildTree(std::shared_ptr<TreeNode> node,
const std::vector<std::vector<std::string>>& data,
const std::vector<std::string>& featureNames,
int labelIndex);
public:
void train(const std::vector<std::vector<std::string>>& data,
const std::vector<std::string>& featureNames,
int labelIndex);
std::string predict(const std::vector<std::string>& sample);
};
信息增益计算优化
计算信息增益是 ID3 中最耗时的部分,我们可以通过以下方式优化:
-
提前计算类别分布 :
std::unordered_map<std::string, int> labelCounts; for (const auto& sample : data) {labelCounts[sample[labelIndex]]++; } -
并行计算各特征的信息增益 :
std::vector<double> infoGains(featureNames.size()); #pragma omp parallel for for (int i = 0; i < featureNames.size(); ++i) {if (i != labelIndex) {infoGains[i] = calculateInfoGain(data, i, labelIndex); } } -
缓存中间结果 :对于大型数据集,可以缓存特征值的分布情况。
递归构建决策树
构建决策树的核心递归函数需要考虑以下终止条件:
-
所有样本属于同一类别 :
if (uniqueLabels.size() == 1) { node->isLeaf = true; node->label = uniqueLabels[0]; return; } -
没有可用特征 :
if (remainingFeatures.empty()) { node->isLeaf = true; node->label = majorityLabel; return; } -
样本集为空 (处理边缘情况):
if (data.empty()) { node->isLeaf = true; node->label = parentMajorityLabel; return; }
完整代码实现
以下是训练方法的完整实现:
void DecisionTree::train(const vector<vector<string>>& data,
const vector<string>& featureNames,
int labelIndex) {root = make_shared<TreeNode>();
vector<int> remainingFeatures;
// 初始化剩余特征(排除标签列)for (int i = 0; i < featureNames.size(); ++i) {if (i != labelIndex) remainingFeatures.push_back(i);
}
buildTree(root, data, featureNames, labelIndex, remainingFeatures);
}
void DecisionTree::buildTree(shared_ptr<TreeNode> node,
const vector<vector<string>>& data,
const vector<string>& featureNames,
int labelIndex,
vector<int>& remainingFeatures) {
// 检查终止条件
auto uniqueLabels = getUniqueValues(data, labelIndex);
if (uniqueLabels.size() == 1) {
node->isLeaf = true;
node->label = uniqueLabels[0];
return;
}
if (remainingFeatures.empty()) {
node->isLeaf = true;
node->label = getMajorityLabel(data, labelIndex);
return;
}
// 选择最佳划分特征
int bestFeatureIndex = -1;
double maxInfoGain = -1.0;
for (int featureIndex : remainingFeatures) {double gain = calculateInfoGain(data, featureIndex, labelIndex);
if (gain > maxInfoGain) {
maxInfoGain = gain;
bestFeatureIndex = featureIndex;
}
}
node->featureName = featureNames[bestFeatureIndex];
// 从剩余特征中移除当前特征
vector<int> newRemainingFeatures;
for (int idx : remainingFeatures) {if (idx != bestFeatureIndex) newRemainingFeatures.push_back(idx);
}
// 按特征值划分子集
auto featureValues = getUniqueValues(data, bestFeatureIndex);
for (const auto& value : featureValues) {auto subset = getSubset(data, bestFeatureIndex, value);
auto childNode = make_shared<TreeNode>();
if (subset.empty()) {
childNode->isLeaf = true;
childNode->label = getMajorityLabel(data, labelIndex);
} else {buildTree(childNode, subset, featureNames, labelIndex, newRemainingFeatures);
}
node->children[value] = childNode;
}
}
性能优化技巧
- 处理连续特征 :
- 将连续特征离散化为多个区间
-
使用二分法找到最佳分割点
-
剪枝策略 :
- 预剪枝 :在构建过程中提前停止(如设置最大深度、最小样本数)
-
后剪枝 :构建完整树后,自底向上剪枝
-
内存优化 :
- 使用智能指针(shared_ptr)管理节点内存
- 对于大型数据集,可以考虑使用磁盘存储部分数据
生产环境注意事项
- 特征缺失处理 :
- 最简单方法:忽略缺失值的样本
-
高级方法:根据其他特征预测缺失值
-
线程安全 :
- 预测过程是只读的,天然线程安全
-
训练过程需要加锁或采用并行算法
-
模型序列化 :
- 可以使用 JSON 格式保存决策树结构
- 也可以实现二进制序列化以提高效率
扩展思考
- 如何扩展实现 C4.5 算法?
- 实现信息增益率计算
- 添加连续特征处理
-
支持缺失值处理
-
何时选择随机森林?
- 当单棵决策树容易过拟合时
- 需要提升模型稳定性时
- 处理高维数据时
通过这个实现,我们不仅掌握了 ID3 算法的核心思想,也了解了决策树在实际工程中的各种考虑因素。这个基础实现可以作为更复杂机器学习系统的构建模块。尝试用不同的数据集测试你的实现,观察决策树如何根据数据特征做出决策,这将帮助你更直观地理解机器学习模型的决策过程。
