【决策树深度探索(三)】树的骨架:节点、分支与叶子,构建你的第一个分类器!



文章目录


亲爱的AI探索者们,大家好!🎉 欢迎来到"决策树深度探索"系列的第三站。如果说第一站是决策树的"大脑"------信息增益,第二站是决策树的"思维模式"------决策流程,那么今天,我们要深入探究的,就是这棵"智慧之树"的**"骨架"**!我们将像生物学家解剖植物一样,详细拆解决策树的各个结构单元:根节点、内部节点、分支和叶子节点,并亲手组装它们,构建一个能够清晰分类的决策器。

决策树的DNA:基本结构单元

想象一下一棵真实的树:它有一个主干,主干上分出枝桠,枝桠上再长出叶子。决策树的结构与此异曲同工,每一个部分都有其特定的功能和意义。理解这些基本单元,是掌握决策树的基石。

1. 根节点 (Root Node):决策的起点

决策树的根节点是整个树的起始点,也是我们进行决策的第一个关卡。它代表了整个数据集,所有的样本数据都从这里开始进入决策流程。

  • 功能:在根节点,算法会选择第一个、也是最重要的一个特征来进行划分。这个特征的选择基于它能够带来最大的信息增益(或最小的Gini不纯度),从而将最复杂的初始数据集进行最有效的第一次分离。
  • 类比:就像你早晨醒来,做的第一个决定(例如"今天穿什么衣服?")会影响接下来一系列的子决定。
2. 内部节点 (Internal Node):提出问题与分叉口

除了根节点和叶节点,树中所有的中间节点都称为内部节点。它们是决策树的核心决策点。

  • 功能 :每个内部节点都代表了对某个特征的测试。例如,"天气是否晴朗?"、"学生成绩是否大于60?"。根据这个特征的取值,样本会被引导到不同的分支。
  • 如何选择测试:算法会在当前节点包含的子数据集中,再次寻找能够最大化信息增益的特征来作为新的划分标准。
  • 类比:想象一个选择题,每个内部节点就是一个问题,你根据问题的答案选择走向不同的选项。
3. 分支 (Branch):决策的路径与条件

从内部节点延伸出去的每一条连线,就是分支

  • 功能:每个分支都代表了内部节点测试结果的一个特定取值或条件。例如,如果内部节点测试的是"天气",那么一个分支可能代表"晴朗",另一个分支代表"阴天",还有一个分支代表"下雨"。样本会沿着与其特征值匹配的分支向下移动。
  • 类比:在迷宫中,每当你遇到一个岔路口,你都需要根据某个条件(例如"这条路通向出口吗?")选择一条路径。
4. 叶子节点 (Leaf Node):最终的结论与答案

当一个分支无法再进行进一步划分时(因为它满足了停止条件),它就会终止于一个叶子节点

  • 功能 :叶子节点是决策树的终点,它不再包含任何测试,而是直接给出一个分类结果 (在分类树中)或预测值(在回归树中)。这个结果通常是该叶节点所包含的所有样本中数量最多的类别。
  • 纯度:理想情况下,一个叶子节点应该包含尽可能"纯净"的数据,即所有样本都属于同一类别。
  • 类比:当你沿着决策路径一路走下来,最终到达一个房间,这个房间就是你的最终目的地和结论。

手撕代码:构建一个最简单的分类器骨架!

为了更直观地理解这些结构,我们将再次回到"手撕代码"的模式,用最朴素的方式来模拟构建决策树的骨架。这次,我们将使用一个更简单、更"玩具"的数据集,专注于如何将数据递归地分解,直到形成叶节点。

我们将尝试识别一种水果是"苹果"还是"橙子",只根据两个特征:"颜色"和"形状"。

python 复制代码
import collections

# 示例数据集:[颜色, 形状, 标签(目标变量)]
# 0: 红色, 1: 圆形, 2: 苹果
# 1: 橙色, 2: 椭圆, 3: 橙子
toy_data = [
    ['Red', 'Round', 'Apple'],
    ['Red', 'Round', 'Apple'],
    ['Red', 'Round', 'Apple'],
    ['Orange', 'Round', 'Orange'],
    ['Orange', 'Oval', 'Orange'],
    ['Orange', 'Oval', 'Orange'],
    ['Red', 'Oval', 'Apple'],
]

feature_names = ['Color', 'Shape']
target_index = 2 # 目标变量在数据集中的索引

# 1. 定义节点结构 (这是树的骨架!)
class SimpleDecisionTreeNode:
    def __init__(self, feature_name=None, predicted_class=None):
        self.feature_name = feature_name    # 如果是内部节点,表示用于划分的特征名称
        self.predicted_class = predicted_class # 如果是叶节点,表示最终预测的类别
        self.children = {}                  # 字典 {特征值: 子节点}

    def add_child(self, feature_value, child_node):
        """为当前节点添加一个子节点,通过特定的特征值连接。"""
        self.children[feature_value] = child_node

    def __repr__(self):
        if self.predicted_class is not None:
            return f"Leaf({self.predicted_class})"
        else:
            return f"Node({self.feature_name}, branches={list(self.children.keys())})"

# 2. 辅助函数:计算当前数据集中出现最多的类别 (用于叶节点)
def get_majority_class(data):
    """
    计算数据集中目标变量出现次数最多的类别。
    """
    if not data:
        return None
    labels = [sample[target_index] for sample in data]
    return collections.Counter(labels).most_common(1)[0][0]

# 3. 核心构建函数:递归地创建树的骨架
def build_simple_tree(data, available_feature_indices):
    """
    递归构建一个简化的决策树。
    这个版本不计算信息增益,只是演示节点的递归构建和停止条件。
    """
    # 停止条件1: 数据集为空
    if not data:
        return SimpleDecisionTreeNode(predicted_class="Unknown")

    # 停止条件2: 所有样本都属于同一类别 (纯净)
    labels_in_data = [sample[target_index] for sample in data]
    if len(set(labels_in_data)) == 1:
        return SimpleDecisionTreeNode(predicted_class=labels_in_data[0])

    # 停止条件3: 没有更多特征可以用来划分
    if not available_feature_indices:
        return SimpleDecisionTreeNode(predicted_class=get_majority_class(data))

    # --- 简化处理:这里我们不计算信息增益,而是简单地按顺序选择下一个可用特征 ---
    # 在实际决策树中,这里会选择信息增益最大的特征
    current_feature_idx = available_feature_indices[0]
    current_feature_name = feature_names[current_feature_idx]

    print(f"  🌳 当前节点根据特征 '{current_feature_name}' 进行划分...")
    root_node = SimpleDecisionTreeNode(feature_name=current_feature_name)

    # 按照当前特征的不同取值进行分组
    feature_value_groups = collections.defaultdict(list)
    for sample in data:
        feature_value = sample[current_feature_idx]
        feature_value_groups[feature_value].append(sample)

    # 递归构建子树
    new_available_feature_indices = available_feature_indices[1:] # 移除已用特征
    for value, group_data in feature_value_groups.items():
        print(f"    ➡️ 分支: '{current_feature_name}' = '{value}'")
        child_node = build_simple_tree(group_data, new_available_feature_indices)
        root_node.add_child(value, child_node)

    return root_node

# 4. 可视化树的结构 (简单打印)
def print_simple_tree(node, indent=""):
    if node.predicted_class is not None:
        print(f"{indent}└── Predict: {node.predicted_class}")
    else:
        for value, child in node.children.items():
            print(f"{indent}├── IF {node.feature_name} is '{value}':")
            print_simple_tree(child, indent + "│   ")

# --- 运行你的骨架分类器! ---
print("🚀 开始构建决策树骨架...")
all_features_indices = list(range(len(feature_names)))
my_tree_skeleton = build_simple_tree(toy_data, all_features_indices)

print("\n🌳 构建完成的决策树骨架:")
print_simple_tree(my_tree_skeleton)

# 预测功能 (与前面类似)
def simple_predict(tree, sample):
    if tree.predicted_class is not None:
        return tree.predicted_class
    
    feature_idx = feature_names.index(tree.feature_name)
    feature_value = sample[feature_idx]

    if feature_value in tree.children:
        return simple_predict(tree.children[feature_value], sample)
    else:
        # 如果遇到训练集中没有出现过的特征值,返回默认多数类别
        return "Unknown" # 或者可以返回树的多数类别

print("\n🔮 进行预测:")
test_fruit1 = ['Red', 'Round']
prediction1 = simple_predict(my_tree_skeleton, test_fruit1)
print(f"Sample: {test_fruit1} -> Prediction: {prediction1}")

test_fruit2 = ['Orange', 'Oval']
prediction2 = simple_predict(my_tree_skeleton, test_fruit2)
print(f"Sample: {test_fruit2} -> Prediction: {prediction2}")

test_fruit3 = ['Green', 'Round'] # 训练集中未出现的颜色
prediction3 = simple_predict(my_tree_skeleton, test_fruit3)
print(f"Sample: {test_fruit3} -> Prediction: {prediction3}")
代码解读:
  1. SimpleDecisionTreeNode

    • 这是我们定义树的骨架 的核心。feature_name 存储当前节点用于划分的特征名称(对于内部节点)。predicted_class 存储这个节点最终预测的类别(对于叶节点)。children 是一个字典,通过特征值将当前节点与它的子节点连接起来,完美体现了分支的概念。
  2. get_majority_class(data)

    • 一个辅助函数,用于在达到停止条件时,确定叶节点应该预测的类别------通常是当前数据集中出现次数最多的类别。
  3. build_simple_tree(data, available_feature_indices)

    • 这是一个递归函数,负责树的生长。
    • 它首先检查一系列停止条件:数据集是否为空?是否所有样本都属于同一类别(纯净的叶节点)?是否已经没有更多特征可以用来划分?这些条件确保了树不会无限生长,最终能够形成叶节点。
    • 在本次简化实现中,我们省略了信息增益的计算 ,而是简单地按available_feature_indices中的顺序选择下一个特征进行划分。这突出了"节点-分支-子节点"的递归构建过程,而不是特征选择的复杂性。
    • 它创建当前节点(root_node),根据所选特征的值将数据分组,然后对每个分组递归调用 build_simple_tree 来构建子节点,并将子节点通过 add_child 方法连接到当前节点,形成分支。
  4. print_simple_treesimple_predict

    • 这两个函数帮助我们可视化和使用构建好的骨架树。print_simple_tree 可以让你清晰地看到树的层次结构,而 simple_predict 则模拟了样本如何在树中沿着分支移动,最终得到预测结果。

通过这个亲手构建的"骨架"分类器,你是不是对决策树的结构有了更深层次的理解?你不仅看到了这些抽象的概念,更是亲手将它们代码化,组成了这棵"智慧之树"!💡

结语与展望

在未来的"深度探索"系列中,我们将继续升级我们的知识:

  • 连续型特征的处理:如何为数值型数据找到最佳的划分点?
  • 决策树的剪枝:如何让树变得更"健壮",避免过拟合?
  • 多分类问题处理:决策树如何优雅地处理多个类别?
  • 回归决策树:如何用决策树来预测连续值?

机器学习的道路充满了无限可能,每一次探索都是一次成长。保持这份热情,我们下篇再见!如果你有任何疑问或想分享你的见解,欢迎在评论区留言哦!💬


相关推荐
Σίσυφος19002 小时前
OpenCV - SVM算法
人工智能·opencv·算法
臭东西的学习笔记8 小时前
论文学习——机器学习引导的蛋白质工程
人工智能·学习·机器学习
清酒难咽8 小时前
算法案例之递归
c++·经验分享·算法
让我上个超影吧8 小时前
【力扣26&80】删除有序数组中的重复项
算法·leetcode
张张努力变强9 小时前
C++ Date日期类的设计与实现全解析
java·开发语言·c++·算法
沉默-_-10 小时前
力扣hot100滑动窗口(C++)
数据结构·c++·学习·算法·滑动窗口
钱彬 (Qian Bin)10 小时前
项目实践19—全球证件智能识别系统(优化检索算法:从MobileNet转EfficientNet)
算法·全球证件识别
feifeigo12310 小时前
基于EM算法的混合Copula MATLAB实现
开发语言·算法·matlab
漫随流水10 小时前
leetcode回溯算法(78.子集)
数据结构·算法·leetcode·回溯算法