
- 个人首页: 永远都不秃头的程序员(互关)
- C语言专栏:从零开始学习C语言
- C++专栏:C++的学习之路
- K-Means专栏:K-Means深度探索系列
- 本章所属专栏:决策树深度探索系列
文章目录
-
-
- 决策树的DNA:基本结构单元
-
- [1. 根节点 (Root Node):决策的起点](#1. 根节点 (Root Node):决策的起点)
- [2. 内部节点 (Internal Node):提出问题与分叉口](#2. 内部节点 (Internal Node):提出问题与分叉口)
- [3. 分支 (Branch):决策的路径与条件](#3. 分支 (Branch):决策的路径与条件)
- [4. 叶子节点 (Leaf Node):最终的结论与答案](#4. 叶子节点 (Leaf Node):最终的结论与答案)
- 手撕代码:构建一个最简单的分类器骨架!
- 结语与展望
-
亲爱的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}")
代码解读:
-
SimpleDecisionTreeNode类:- 这是我们定义树的骨架 的核心。
feature_name存储当前节点用于划分的特征名称(对于内部节点)。predicted_class存储这个节点最终预测的类别(对于叶节点)。children是一个字典,通过特征值将当前节点与它的子节点连接起来,完美体现了分支的概念。
- 这是我们定义树的骨架 的核心。
-
get_majority_class(data):- 一个辅助函数,用于在达到停止条件时,确定叶节点应该预测的类别------通常是当前数据集中出现次数最多的类别。
-
build_simple_tree(data, available_feature_indices):- 这是一个递归函数,负责树的生长。
- 它首先检查一系列停止条件:数据集是否为空?是否所有样本都属于同一类别(纯净的叶节点)?是否已经没有更多特征可以用来划分?这些条件确保了树不会无限生长,最终能够形成叶节点。
- 在本次简化实现中,我们省略了信息增益的计算 ,而是简单地按
available_feature_indices中的顺序选择下一个特征进行划分。这突出了"节点-分支-子节点"的递归构建过程,而不是特征选择的复杂性。 - 它创建当前节点(
root_node),根据所选特征的值将数据分组,然后对每个分组递归调用build_simple_tree来构建子节点,并将子节点通过add_child方法连接到当前节点,形成分支。
-
print_simple_tree和simple_predict:- 这两个函数帮助我们可视化和使用构建好的骨架树。
print_simple_tree可以让你清晰地看到树的层次结构,而simple_predict则模拟了样本如何在树中沿着分支移动,最终得到预测结果。
- 这两个函数帮助我们可视化和使用构建好的骨架树。
通过这个亲手构建的"骨架"分类器,你是不是对决策树的结构有了更深层次的理解?你不仅看到了这些抽象的概念,更是亲手将它们代码化,组成了这棵"智慧之树"!💡
结语与展望
在未来的"深度探索"系列中,我们将继续升级我们的知识:
- 连续型特征的处理:如何为数值型数据找到最佳的划分点?
- 决策树的剪枝:如何让树变得更"健壮",避免过拟合?
- 多分类问题处理:决策树如何优雅地处理多个类别?
- 回归决策树:如何用决策树来预测连续值?
机器学习的道路充满了无限可能,每一次探索都是一次成长。保持这份热情,我们下篇再见!如果你有任何疑问或想分享你的见解,欢迎在评论区留言哦!💬