首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >机器学习经典算法:ID3决策树(Iterative Dichotomiser 3)原理、手动计算与Python/Java双代码实战

机器学习经典算法:ID3决策树(Iterative Dichotomiser 3)原理、手动计算与Python/Java双代码实战

原创
作者头像
jack.yang
发布2026-03-29 15:12:31
发布2026-03-29 15:12:31
1890
举报
文章被收录于专栏:大模型系列大模型系列

关键词:机器学习、ID3算法、决策树、信息增益、熵、手动计算、天气打球数据集、Python ID3、Java ID3、Iterative Dichotomiser 3

一句话答案:ID3 是最早的决策树算法之一,通过信息增益选择最优划分特征,递归构建树结构。它奠定了 CART、C4.5 等后续算法的基础!

如果你在搜索:

  • “ID3算法怎么算的?”
  • “信息增益手动计算例子”
  • “如何用ID3做天气是否打球预测?”
  • “Python 和 Java 怎么实现 ID3 决策树?”

那么,这篇文章就是为你写的——从熵到树根,一步不跳


一、什么是ID3?它解决了什么问题?

ID3(Iterative Dichotomiser 3)由 Ross Quinlan 于1986年提出,是首个基于信息论的决策树算法

🎯 核心目标

给定一个带标签的表格数据集,自动构建一棵if-else规则树,用于分类新样本。

🔑 关键特点

  • 仅支持离散特征(连续特征需先离散化)
  • 使用信息增益(Information Gain)作为特征选择标准
  • 不能处理缺失值
  • 容易过拟合(无剪枝机制)

💡 虽然ID3已被C4.5、CART取代,但它是理解决策树思想的最佳起点


二、数学原理:熵、条件熵与信息增益

1️⃣ 熵(Entropy)——衡量不确定性

  • 熵越大 → 数据越混乱(纯度越低)
  • 熵=0 → 所有样本属于同一类(纯节点)

2️⃣ 条件熵(Conditional Entropy)

3️⃣ 信息增益(Information Gain)

ID3选择信息增益最大的特征作为当前节点的划分依据


三、手工推演:一步步构建“是否打球”决策树

📊 经典数据集:天气与打球决策(14个样本)

编号

天气

温度

湿度

风力

打球?

1

2

3

4

正常

5

正常

6

正常

7

正常

8

9

正常

10

正常

11

正常

12

13

正常

14

目标:构建ID3决策树,预测新天气条件下是否打球。


🔢 步骤1:计算根节点的熵


🔢 步骤2:计算每个特征的信息增益

✅ 特征1:天气(晴/阴/雨)
  • (5个):是=2,否=3 → Entropy=0.971
  • (4个):是=4,否=0 → Entropy=0
  • (5个):是=3,否=2 → Entropy=0.971
✅ 特征2:温度(热/凉/冷)
  • 热(4):是=2,否=2 → Entropy=1.0
  • 凉(6):是=4,否=2 → Entropy=0.918
  • 冷(4):是=3,否=1 → Entropy=0.811
✅ 特征3:湿度(高/正常)
  • 高(7):是=3,否=4 → Entropy=0.985
  • 正常(7):是=6,否=1 → Entropy=0.592
✅ 特征4:风力(弱/强)
  • 弱(8):是=6,否=2 → Entropy=0.811
  • 强(6):是=3,否=3 → Entropy=1.0

🌳 步骤3:选择根节点

特征

信息增益

天气

0.247 ← 最大

湿度

0.151

风力

0.048

温度

0.029

根节点 = 天气


🌳 步骤4:递归构建子树

分支1:天气 = 晴(5个样本:2是,3否)
  • 计算子集熵:≈0.971
  • 候选特征:温度、湿度、风力
以“湿度”划分:
  • 高(3):是=0,否=3 → Entropy=0
  • 正常(2):是=2,否=0 → Entropy=0
  • IG = 0.971 - 0 = 0.971(最大)

→ 创建节点“湿度”,其下:

  • 湿度=高 →
  • 湿度=正常 →
分支2:天气 = 阴(4个样本:全是“是”)

→ 叶节点:

分支3:天气 = 雨(5个样本:3是,2否)
  • 子集熵 ≈0.971
  • 测试“风力”:
    • 弱(3):是=3,否=0 → Entropy=0
    • 强(2):是=0,否=2 → Entropy=0
    • IG = 0.971

→ 创建节点“风力”,其下:

  • 风力=弱 →
  • 风力=强 →

🌲 最终ID3决策树

代码语言:javascript
复制
                天气?
               /  |  \
            晴    阴   雨
           /      |     \
        湿度?     是     风力?
       /   \             /   \
    高     正常        弱     强
    |       |          |      |
   否       是         是     否

✅ 完美分类训练集(但可能过拟合!)


四、Python 实现(手写ID3核心逻辑)

代码语言:javascript
复制
import math
from collections import Counter

def entropy(data):
    labels = [row[-1] for row in data]
    label_counts = Counter(labels)
    total = len(labels)
    ent = 0.0
    for count in label_counts.values():
        p = count / total
        ent -= p * math.log2(p)
    return ent

def split_data(data, feature_idx, value):
    return [row for row in data if row[feature_idx] == value]

def information_gain(data, feature_idx):
    base_entropy = entropy(data)
    feature_values = set(row[feature_idx] for row in data)
    weighted_entropy = 0.0
    for val in feature_values:
        subset = split_data(data, feature_idx, val)
        prob = len(subset) / len(data)
        weighted_entropy += prob * entropy(subset)
    return base_entropy - weighted_entropy

def id3(data, features, feature_names):
    labels = [row[-1] for row in data]
    if len(set(labels)) == 1:
        return labels[0]  # 纯节点
    
    if not features:
        return Counter(labels).most_common(1)[0][0]  # 投票
    
    # 选择信息增益最大的特征
    best_feature_idx = max(features, key=lambda i: information_gain(data, i))
    best_feature_name = feature_names[best_feature_idx]
    
    tree = {best_feature_name: {}}
    feature_values = set(row[best_feature_idx] for row in data)
    
    remaining_features = [i for i in features if i != best_feature_idx]
    
    for val in feature_values:
        subset = split_data(data, best_feature_idx, val)
        if not subset:
            tree[best_feature_name][val] = Counter(labels).most_common(1)[0][0]
        else:
            subtree = id3(subset, remaining_features, feature_names)
            tree[best_feature_name][val] = subtree
    
    return tree

# 数据准备
data = [
    ["晴", "热", "高", "弱", "否"],
    ["晴", "热", "高", "强", "否"],
    ["阴", "热", "高", "弱", "是"],
    ["雨", "凉", "正常", "弱", "是"],
    ["雨", "冷", "正常", "弱", "是"],
    ["雨", "冷", "正常", "强", "否"],
    ["阴", "冷", "正常", "强", "是"],
    ["晴", "凉", "高", "弱", "否"],
    ["晴", "冷", "正常", "弱", "是"],
    ["雨", "凉", "正常", "弱", "是"],
    ["晴", "凉", "正常", "强", "是"],
    ["阴", "凉", "高", "强", "是"],
    ["阴", "热", "正常", "弱", "是"],
    ["雨", "凉", "高", "强", "否"]
]

features = list(range(len(data[0]) - 1))  # [0,1,2,3]
feature_names = ["天气", "温度", "湿度", "风力"]

tree = id3(data, features, feature_names)
print("ID3决策树:", tree)

五、Java 实现(纯手写递归构建)

代码语言:javascript
复制
import java.util.*;

public class ID3 {
    static class TreeNode {
        String feature;
        Map<String, TreeNode> children = new HashMap<>();
        String label; // if leaf

        TreeNode(String feature) { this.feature = feature; }
        TreeNode(String feature, String label) { this.feature = feature; this.label = label; }
    }

    public static double entropy(List<String[]> data) {
        Map<String, Integer> count = new HashMap<>();
        for (String[] row : data) {
            String label = row[row.length - 1];
            count.put(label, count.getOrDefault(label, 0) + 1);
        }
        double ent = 0.0;
        int total = data.size();
        for (int c : count.values()) {
            double p = (double) c / total;
            ent -= p * (Math.log(p) / Math.log(2));
        }
        return ent;
    }

    public static List<String[]> splitData(List<String[]> data, int featureIdx, String value) {
        List<String[]> subset = new ArrayList<>();
        for (String[] row : data) {
            if (row[featureIdx].equals(value)) {
                subset.add(row);
            }
        }
        return subset;
    }

    public static double informationGain(List<String[]> data, int featureIdx) {
        double baseEnt = entropy(data);
        Set<String> values = new HashSet<>();
        for (String[] row : data) values.add(row[featureIdx]);

        double weightedEnt = 0.0;
        for (String val : values) {
            List<String[]> subset = splitData(data, featureIdx, val);
            double prob = (double) subset.size() / data.size();
            weightedEnt += prob * entropy(subset);
        }
        return baseEnt - weightedEnt;
    }

    public static String majorityLabel(List<String[]> data) {
        Map<String, Integer> count = new HashMap<>();
        for (String[] row : data) {
            String label = row[row.length - 1];
            count.put(label, count.getOrDefault(label, 0) + 1);
        }
        return Collections.max(count.entrySet(), Map.Entry.comparingByValue()).getKey();
    }

    public static TreeNode id3(List<String[]> data, List<Integer> features, String[] featureNames) {
        // 检查是否纯节点
        Set<String> labels = new HashSet<>();
        for (String[] row : data) labels.add(row[row.length - 1]);
        if (labels.size() == 1) {
            return new TreeNode("leaf", labels.iterator().next());
        }
        if (features.isEmpty()) {
            return new TreeNode("leaf", majorityLabel(data));
        }

        // 选择最佳特征
        int bestIdx = -1;
        double bestGain = -1;
        for (int idx : features) {
            double gain = informationGain(data, idx);
            if (gain > bestGain) {
                bestGain = gain;
                bestIdx = idx;
            }
        }

        TreeNode node = new TreeNode(featureNames[bestIdx]);
        Set<String> values = new HashSet<>();
        for (String[] row : data) values.add(row[bestIdx]);

        List<Integer> remainingFeatures = new ArrayList<>(features);
        remainingFeatures.remove(Integer.valueOf(bestIdx));

        for (String val : values) {
            List<String[]> subset = splitData(data, bestIdx, val);
            if (subset.isEmpty()) {
                node.children.put(val, new TreeNode("leaf", majorityLabel(data)));
            } else {
                node.children.put(val, id3(subset, remainingFeatures, featureNames));
            }
        }

        return node;
    }

    // 测试
    public static void main(String[] args) {
        List<String[]> data = Arrays.asList(
            new String[]{"晴", "热", "高", "弱", "否"},
            new String[]{"晴", "热", "高", "强", "否"},
            new String[]{"阴", "热", "高", "弱", "是"},
            new String[]{"雨", "凉", "正常", "弱", "是"},
            new String[]{"雨", "冷", "正常", "弱", "是"},
            new String[]{"雨", "冷", "正常", "强", "否"},
            new String[]{"阴", "冷", "正常", "强", "是"},
            new String[]{"晴", "凉", "高", "弱", "否"},
            new String[]{"晴", "冷", "正常", "弱", "是"},
            new String[]{"雨", "凉", "正常", "弱", "是"},
            new String[]{"晴", "凉", "正常", "强", "是"},
            new String[]{"阴", "凉", "高", "强", "是"},
            new String[]{"阴", "热", "正常", "弱", "是"},
            new String[]{"雨", "凉", "高", "强", "否"}
        );

        List<Integer> features = Arrays.asList(0, 1, 2, 3);
        String[] featureNames = {"天气", "温度", "湿度", "风力"};

        TreeNode tree = id3(data, features, featureNames);
        System.out.println("ID3决策树已构建(结构见控制台输出逻辑)");
        // 实际打印需额外遍历函数,此处略
    }
}

六、优缺点 & 适用场景总结

优点

缺点

✅ 模型可解释性强(if-else规则)

❌ 容易过拟合(无剪枝)

✅ 无需数据标准化

❌ 信息增益偏向多值特征

✅ 训练速度快

❌ 不能处理连续特征(原生ID3)

✅ 天然支持多分类

❌ 对噪声敏感

🎯 最佳应用场景:

  • 小规模离散数据集
  • 需要可解释规则的业务场景(如风控规则)
  • 教学演示决策树基本原理

七、ID3 vs C4.5 vs CART 对比

算法

特征类型

分裂标准

剪枝

输出

ID3

离散

信息增益

❌ 无

分类树

C4.5

离散+连续

信息增益率

✅ 有

分类树

CART

离散+连续

基尼系数/平方误差

✅ 有

分类+回归树

💡 建议:实际项目优先使用 C4.5(J48)或 CART(sklearn DecisionTree),ID3主要用于学习。


✅ 结语

ID3用信息论为决策树注入了科学灵魂。它告诉我们:最好的划分,是让结果最确定的那个

记住:在机器学习中,减少不确定性就是进步。

现在,你已经能:

  • 手动计算信息增益并构建ID3树
  • 用Python或Java从零实现ID3
  • 理解其在决策树家族中的历史地位

相关链接

  • 📂 大模型技术专栏: 欢迎您到访 「大模型系列」。 在这个由参数驱动、以数据为燃料的新智能时代,大语言模型(LLM)已不再是实验室里的前沿概念,而是正在重塑搜索、办公、编程、教育、医疗乃至整个数字世界的底层引擎。从 GPT 到 Llama,从 Claude 到 Qwen,从推理到多模态,大模型正以前所未有的速度进化——它们既是工具,也是平台,更可能是下一代人机交互的“操作系统”。 本系列将带你:
    • 🔍 深入原理:从 Transformer 架构、注意力机制到训练范式(预训练、微调、RLHF);
    • ⚙️ 动手实践:本地部署、模型微调、RAG 构建、Agent 设计等实战指南;
    • 🧠 理解边界:幻觉、偏见、安全对齐、推理瓶颈与当前能力天花板;
    • 🌍 洞察趋势:开源 vs 闭源、端侧部署、MoE 架构、世界模型与 AGI 路径;
    • 💼 落地应用:如何在企业中安全、高效、低成本地集成大模型能力。

    无论你是想写代码调用 API 的开发者,设计 AI 产品的 PM,评估技术路线的管理者,还是单纯好奇智能本质的思考者,这里都有值得你驻足的内容。 不追 hype,只讲逻辑;不谈玄学,专注可复现的认知。 让我们一起,在这场百年一遇的智能革命中,看得更清,走得更稳 https://cloud.tencent.com/developer/column/107314

  • 👤 关于作者专注技术落地,深耕硬核干货 本文作者致力于大模型相关技术的生态建设与实战落地。不同于浅层的概念科普,作者坚持 “手算 + 代码” 的深度分享模式,主张通过手动推演理解算法本质,结合生产级代码验证理论可行性。 请关注我主页:https://cloud.tencent.com/developer/user/2276240

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是ID3?它解决了什么问题?
    • 🎯 核心目标
    • 🔑 关键特点
  • 二、数学原理:熵、条件熵与信息增益
    • 1️⃣ 熵(Entropy)——衡量不确定性
    • 2️⃣ 条件熵(Conditional Entropy)
    • 3️⃣ 信息增益(Information Gain)
  • 三、手工推演:一步步构建“是否打球”决策树
    • 📊 经典数据集:天气与打球决策(14个样本)
    • 🔢 步骤1:计算根节点的熵
    • 🔢 步骤2:计算每个特征的信息增益
      • ✅ 特征1:天气(晴/阴/雨)
      • ✅ 特征2:温度(热/凉/冷)
      • ✅ 特征3:湿度(高/正常)
      • ✅ 特征4:风力(弱/强)
    • 🌳 步骤3:选择根节点
    • 🌳 步骤4:递归构建子树
      • 分支1:天气 = 晴(5个样本:2是,3否)
      • 分支2:天气 = 阴(4个样本:全是“是”)
      • 分支3:天气 = 雨(5个样本:3是,2否)
    • 🌲 最终ID3决策树
  • 四、Python 实现(手写ID3核心逻辑)
  • 五、Java 实现(纯手写递归构建)
  • 六、优缺点 & 适用场景总结
    • 🎯 最佳应用场景:
  • 七、ID3 vs C4.5 vs CART 对比
  • ✅ 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档