机器学习之决策树
一、决策树是什么?
决策树是机器学习中一种经典的监督学习模型,既可以用于分类任务,也可以用于回归任务。它的核心思想是模拟人类的决策过程—— 通过对数据特征的层层判断,将复杂问题拆解为一系列简单的二元或多元判断,最终得出结论。
决策树的结构像一棵倒置的树,主要包含三类节点:
- 根节点:位于树的最顶端,包含全部训练数据,是决策的起点。
- 内部节点:也叫 “决策节点”,代表对一个特征的判断条件(例如 “花瓣长度> 2.5cm”),每个内部节点会将数据划分为多个子集。
- 叶节点:位于树的最底端,代表最终的决策结果—— 分类任务中是类别标签(如 “鸢尾花 - 山鸢尾”),回归任务中是连续值(如 “房价 - 100 万”)。
二、决策树的常用划分方法
决策树的关键是如何选择最优特征进行节点划分,目标是让划分后的子集尽可能 “纯净”(即同一子集内的数据类别尽量一致)。常用的划分准则有 3 种:
1.信息增益(ID3 算法)
基于信息熵(衡量数据的混乱程度,熵越大,数据越混乱)。
信息增益 = 划分前的熵 - 划分后的加权平均熵。
优先选择信息增益最大的特征作为当前节点的划分特征。
缺点:倾向于选择取值较多的特征(如 “身份证号” 这类唯一特征),容易过拟合。
2.信息增益比(C4.5 算法)
对信息增益做了归一化修正,引入 “分裂信息”(衡量特征取值的均匀程度)。
信息增益比 = 信息增益 / 分裂信息。
解决了 ID3 倾向于取值多特征的问题,是 ID3 的改进版。
3.基尼系数(CART 算法)

基于基尼不纯度(衡量数据被分错的概率,基尼系数越小,数据越纯净)。
优势:计算效率高,可同时用于分类和回归任务。
三、属性划分的介绍
1.熵
熵是表示随机变量不确定性的度量,换言之即是物体内部的混乱程度。对于决策树的某个结点来说,对样本进行分类后可以使样本集尽可能有序,最大程度地降低样本数据的熵。
随机变量x 的熵定义为:

k:x所有可能取值的类别总数;p(x):随机变量x取第i个值的概率
熵越小,数据纯度越高、越好
四、实际问题应用
问题引入:贷款审批需要根据申请人的个人信息(如年龄、工作状况、房产情况、信贷历史等)判断其还款能力和信用水平,进而决定是否批准贷款申请。
实验数据:


代码实现:
import numpy as np
import pandas as pd
from collections import Counter
from sklearn.metrics import accuracy_score
class DecisionTree:
def __init__(self, algorithm='id3'):
self.algorithm = algorithm
self.tree = None
def _cal_entropy(self, y):
"""计算数据集的熵(信息不确定性)"""
counter = Counter(y)
total = len(y)
entropy = 0.0
for count in counter.values():
p = count / total
entropy -= p * np.log2(p + 1e-10) # 避免log(0)报错
return entropy
def _cal_info_gain(self, X, y, feature_idx):
"""计算信息增益(ID3核心)"""
parent_entropy = self._cal_entropy(y)
feature_values = X[:, feature_idx]
value_counter = Counter(feature_values)
total_samples = len(y)
child_entropy = 0.0
for value, count in value_counter.items():
mask = (feature_values == value)
child_y = y[mask]
child_entropy += (count / total_samples) * self._cal_entropy(child_y)
return parent_entropy - child_entropy
def _cal_info_gain_ratio(self, X, y, feature_idx):
"""计算信息增益比(C4.5核心)"""
info_gain = self._cal_info_gain(X, y, feature_idx)
feature_values = X[:, feature_idx]
value_counter = Counter(feature_values)
total_samples = len(feature_values)
feature_entropy = 0.0
for count in value_counter.values():
p = count / total_samples
feature_entropy -= p * np.log2(p + 1e-10)
return info_gain / (feature_entropy + 1e-10) # 避免除以0
def _select_best_feature(self, X, y):
"""选择最优分裂特征(ID3用信息增益,C4.5用信息增益比)"""
n_features = X.shape[1]
best_feature_idx = -1
best_score = -1.0
for idx in range(n_features):
if self.algorithm == 'id3':
score = self._cal_info_gain(X, y, idx)
else:
score = self._cal_info_gain_ratio(X, y, idx)
if score > best_score:
best_score = score
best_feature_idx = idx
return best_feature_idx
def _create_tree(self, X, y, feature_names):
"""递归构建决策树"""
# 终止条件1:所有样本同一类别
if len(set(y)) == 1:
return y[0]
# 终止条件2:无特征可分裂,返回样本数最多的类别
if X.shape[1] == 0:
return Counter(y).most_common(1)[0][0]
# 选择最优特征
best_feature_idx = self._select_best_feature(X, y)
best_feature_name = feature_names[best_feature_idx]
# 构建树结构
tree = {best_feature_name: {}}
new_feature_names = [name for idx, name in enumerate(feature_names) if idx != best_feature_idx]
# 按特征值分裂数据集
feature_values = X[:, best_feature_idx]
unique_values = set(feature_values)
for value in unique_values:
mask = (feature_values == value)
sub_X = X[mask, :]
sub_X = np.delete(sub_X, best_feature_idx, axis=1)
sub_y = y[mask]
tree[best_feature_name][value] = self._create_tree(sub_X, sub_y, new_feature_names)
return tree
def fit(self, X, y, feature_names):
"""训练决策树(对外接口)"""
X = np.array(X)
y = np.array(y).flatten()
self.tree = self._create_tree(X, y, feature_names)
def _get_leaf_labels(self, subtree):
"""递归获取子树中所有叶子节点的标签(修复Counter统计问题)"""
leaf_labels = []
if isinstance(subtree, dict):
# 如果是子树,递归遍历所有分支
for branch in subtree.values():
leaf_labels.extend(self._get_leaf_labels(branch))
else:
# 如果是叶子节点,直接添加标签
leaf_labels.append(subtree)
return leaf_labels
def _predict_sample(self, sample, tree, feature_names):
"""预测单个样本(修复未知特征值处理)"""
root_name = list(tree.keys())[0]
root_dict = tree[root_name]
feature_idx = feature_names.index(root_name)
sample_value = sample[feature_idx]
if sample_value in root_dict:
subtree = root_dict[sample_value]
if isinstance(subtree, dict):
return self._predict_sample(sample, subtree, feature_names)
else:
return subtree
else:
# 修复:获取所有叶子节点标签,统计出现次数最多的
leaf_labels = self._get_leaf_labels(root_dict)
return Counter(leaf_labels).most_common(1)[0][0]
def predict(self, X, feature_names):
"""预测多个样本(对外接口)"""
X = np.array(X)
predictions = []
for sample in X:
predictions.append(self._predict_sample(sample, self.tree, feature_names))
return np.array(predictions)
def load_dataset(file_path):
"""加载训练集(格式:特征1,特征2,特征3,特征4,标签)"""
try:
data = np.loadtxt(file_path, delimiter=',', dtype=int)
X = data[:, :-1] # 前n-1列为特征
y = data[:, -1] # 最后一列为标签
# 校验训练集特征数量(必须为4个,与特征名称对应)
if X.shape[1] != 4:
raise ValueError(f"训练集特征数量错误!应为4个,实际为{X.shape[1]}个")
return X, y
except Exception as e:
print(f"读取训练集失败:{e}")
exit(1)
def load_test_dataset_with_id(file_path):
"""加载测试集(格式:样本ID,特征1,特征2,特征3,特征4,实际标签)"""
try:
data = np.loadtxt(file_path, delimiter=',', dtype=int)
ids = data[:, 0] # 第一列为样本ID
X = data[:, 1:-1] # 中间列为特征(第2-5列)
y_true = data[:, -1] # 最后一列为实际类别
# 校验测试集特征数量(必须为4个,与训练集一致)
if X.shape[1] != 4:
raise ValueError(f"测试集特征数量错误!应为4个,实际为{X.shape[1]}个")
return ids, X, y_true
except Exception as e:
print(f"读取测试集失败:{e}")
exit(1)
def map_label(label):
"""数字标签转文字描述(适配贷款场景)"""
return "是(贷款)" if label == 1 else "否(不贷款)"
def map_feature_value(feature_idx, value):
"""特征值转文字描述(按特征索引映射)"""
# 特征映射表:索引对应特征(0=年龄段,1=有工作,2=有房子,3=信贷情况)
feature_maps = [
{0: "青年", 1: "中年", 2: "老年"}, # 年龄段
{0: "否", 1: "是"}, # 有工作
{0: "否", 1: "是"}, # 有自己的房子
{0: "一般", 1: "好", 2: "非常好"} # 信贷情况
]
return feature_maps[feature_idx].get(value, f"未知({value})")
if __name__ == "__main__":
# ---------------------- 配置参数 ----------------------
TRAIN_FILE = "data.txt"
TEST_FILE = "test.txt"
feature_names = ['年龄段', '有工作', '有自己的房子', '信贷情况'] # 4个特征名称(固定)
# ---------------------- 加载数据(含特征数量校验) ----------------------
print("=== 加载数据集 ===")
X_train, y_train = load_dataset(TRAIN_FILE)
test_ids, X_test, y_true = load_test_dataset_with_id(TEST_FILE)
print(f"训练集:{len(X_train)} 个样本,{X_train.shape[1]} 个特征")
print(f"测试集:{len(X_test)} 个样本,{X_test.shape[1]} 个特征")
# 额外校验训练集和测试集特征数量一致
if X_train.shape[1] != X_test.shape[1]:
print(f"错误:训练集({X_train.shape[1]}个特征)和测试集({X_test.shape[1]}个特征)特征数量不一致!")
exit(1)
# ---------------------- 训练模型 ----------------------
print("\n=== 训练决策树模型 ===")
# 训练ID3模型
id3_model = DecisionTree(algorithm='id3')
id3_model.fit(X_train, y_train, feature_names)
print("ID3决策树训练完成")
# 训练C4.5模型
c45_model = DecisionTree(algorithm='c4.5')
c45_model.fit(X_train, y_train, feature_names)
print("C4.5决策树训练完成")
# ---------------------- 预测与评估 ----------------------
print("\n=== 测试集预测结果 ===")
# 预测
y_pred_id3 = id3_model.predict(X_test, feature_names)
y_pred_c45 = c45_model.predict(X_test, feature_names)
# 计算准确率
accuracy_id3 = accuracy_score(y_true, y_pred_id3)
accuracy_c45 = accuracy_score(y_true, y_pred_c45)
# ---------------------- 构建结果表格 ----------------------
result_data = []
for i in range(len(test_ids)):
sample_id = test_ids[i]
# 按特征索引映射文字(确保只访问存在的特征索引)
age = map_feature_value(0, X_test[i, 0]) if X_test.shape[1] > 0 else "无"
work = map_feature_value(1, X_test[i, 1]) if X_test.shape[1] > 1 else "无"
house = map_feature_value(2, X_test[i, 2]) if X_test.shape[1] > 2 else "无"
credit = map_feature_value(3, X_test[i, 3]) if X_test.shape[1] > 3 else "无"
# 实际类别和预测类别
actual = map_label(y_true[i])
pred_id3 = map_label(y_pred_id3[i])
pred_c45 = map_label(y_pred_c45[i])
result_data.append([
sample_id, age, work, house, credit, actual, pred_id3, pred_c45
])
# 创建DataFrame并设置列名(与示例图一致)
result_df = pd.DataFrame(
result_data,
columns=["样本ID", "年龄段", "有工作", "有房子", "信贷情况", "实际类别", "ID3预测", "C4.5预测"]
)
# ---------------------- 输出结果 ----------------------
# 打印表格(隐藏索引,优化格式)
print("\n" + "=" * 120)
print(result_df.to_string(index=False, max_colwidth=12))
print("=" * 120)
# 打印准确率
print(f"\n=== 算法准确率 ===")
print(f"ID3算法准确率:{accuracy_id3:.4f} ({int(accuracy_id3 * len(X_test))}/{len(X_test)})")
print(f"C4.5算法准确率:{accuracy_c45:.4f} ({int(accuracy_c45 * len(X_test))}/{len(X_test)})")
运行结果:

五、实验总结
机器实验的大致流程为:数据加载->探索性分析->模型训练->模型评估->可视化->应用。在整个实验过程中通过ai的协助,我们掌握了决策树算法的使用方法。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)