C4.5 算法由 Quinlan 于 1993 年提出,核心部分与 ID3 算法相似,只是在 ID3 算法的基础上进行了改造——在特征选择过程以信息增益比作为选择准则。
【具体方法】:
- 从根结点(root node)出发,对结点计算所有可能特征的信息增益比,选择信息增益比最大的特征作为结点的特征,由该特征的不同取值建立子结点;
- 再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止。
- 最后得到一个决策树。
【算法】:C4.5 生成算法。
- 输入:训练数据集 D,特征集 A,阈值 e。
- 输出:决策树 T。
- 过程:
- 如果 D 中所有实例属于同一类 C k C_k Ck,则置 T 为单结点树,并将 C k C_k Ck 作为该结点的类,返回 T;
- 如果 A = ∅ A = \emptyset A=∅,则置 T 为单结点树,并将 D 中实例数最大的类 C k C_k Ck 作为该结点的类,返回 T;
- 否则,按照信息增益比公式计算 A 中各特征对 D 的信息增益比,选择信息增益比最大的特征 A g A_g Ag;
- 如果 A g A_g Ag 的信息增益比小于阈值 e,则置 T 为单结点树,并将 D 中实例数最大的类 C k C_k Ck 作为该结点的类,返回 T;
- 否则,对 A g A_g Ag 的每一可能值 a i a_i ai,依 A g = a i A_g = a_i Ag=ai 将 D 分割为若干非空子集 D i D_i Di,将 D i D_i Di 中实例数最大的类作为标记,构建子结点,由结点及其子结点构成树 T,返回 T;
- 对结点 i,以 D i D_i Di 为训练集,以 A − A g A - {A_g} A−Ag 为特征集,递归地调用 (1) ~ (5) 步,得到子树 T i T_i Ti,返回 T i T_i Ti。
关于信息熵、信息增益以及信息增益比的具体内容请参考博客 模型选择-决策树
【信息熵计算公式】
E
n
t
(
D
)
=
∑
i
=
i
k
−
∣
Y
i
∣
∣
Y
∣
l
o
g
2
∣
Y
i
∣
∣
Y
∣
Ent(D) = \sum_{i=i}^{k}-\frac{|Y_i|}{|Y|}log_2\frac{|Y_i|}{|Y|}
Ent(D)=i=i∑k−∣Y∣∣Yi∣log2∣Y∣∣Yi∣
其中,|Y| 表示当前数据集 D 的标签数,k 表示数据集 D 的标签种类数。例如,现在有 5 条样本,每条样本的分类为 1、0、1、2、0,那么 |Y| = 5,k = 3。
∣
Y
i
∣
|Y_i|
∣Yi∣ 表示指定标签种类的标签数,例如
Y
0
Y_0
Y0 对应标签种类 0,那么
∣
Y
0
∣
=
2
|Y_0| = 2
∣Y0∣=2,同理可得
∣
Y
1
∣
=
2
,
∣
Y
2
∣
=
1
|Y_1| = 2, |Y_2|= 1
∣Y1∣=2,∣Y2∣=1。
【信息增益计算公式】:
G
a
i
n
(
D
,
a
)
=
∑
i
=
1
∣
a
∣
∣
D
i
∣
∣
D
∣
E
n
t
(
D
i
)
Gain(D, a) = \sum_{i=1}^{|a|}\frac{|D_i|}{|D|}Ent(D_i)
Gain(D,a)=i=1∑∣a∣∣D∣∣Di∣Ent(Di)
其中,a 表示数据集 D 上的一个特征,|a| 表示该特征的所有可能的取值数,例如 a 表示性别,则 a 有两种取值,即“男”和“女”。
D
i
D_i
Di 表示根据指定特征取值划分的数据集,
∣
D
i
∣
|D_i|
∣Di∣ 表示该数据集的样本数。
【信息增益比计算公式】:
I
V
(
a
)
=
∑
i
=
1
∣
a
∣
−
∣
D
i
∣
∣
D
∣
l
o
g
2
∣
D
i
∣
∣
D
∣
G
a
i
n
r
a
t
i
o
(
D
,
a
)
=
G
a
i
n
(
D
,
a
)
I
V
(
a
)
IV(a) = \sum_{i=1}^{|a|}-\frac{|D_i|}{|D|}log_2\frac{|D_i|}{|D|} \\ Gain_{ratio}(D, a) = \frac{Gain(D, a)}{IV(a)}
IV(a)=i=1∑∣a∣−∣D∣∣Di∣log2∣D∣∣Di∣Gainratio(D,a)=IV(a)Gain(D,a)
代码实现
C4.5 算法同 ID3 算法,只能用来处理离散型数据以及分类任务,因此我们选用 sklearn.datasets 中的 iris 鸢尾花数据集。
import numpy as np
from sklearn.datasets import load_iris
dataset = load_iris()
x = dataset.data
y = dataset.target
features = dataset.feature_names
决策树结点结构代码:
class Node:
def __init__(self, value, type='decision'):
self.value = value
self.type = type
self.children = {}
calc_shannon_ent()
calc_shannon_ent 信息增益计算函数,该函数接受两个参数 data 和 labels。
- data:数据集
- labels:标签集
首先,获取数据集的个数和特征数以及分类的个数。
data_count = float(data.shape[0])
features = data.shape[1]
labels_count = np.array([labels[labels == label].size for label in set(labels)])
注意:labels_count / data_count 的结果即为原始数据集各分类的概率。
接着,计算原始数据集的信息熵以及创建存放划分后各特征信息增益比信息的列表 gain_list。
base_ent = -np.sum((labels_count / data_count) * np.log2(labels_count / data_count))
gain_list = []
然后,计算每个特征划分后的数据集的信息熵以及该特征对应 IV。
for feature in range(0, features):
# 获取特征对应的数据
feature_data = data[:, feature]
# 获取特征信息,以特征值为 key,特征值的数目为 value
feature_info = {feature: feature_data[feature_data == feature].size for feature in set(feature_data)}
feature_shannonEnt, IV = 0, 0
# 获取每个特征值的分类信息并计算条件信息熵
for feature_value in feature_info:
# 当前特征值的数目
feature_count = float(feature_info[feature_value])
label_data = labels[feature_data == feature_value]
labels_feature = np.array([label_data[label_data == label].size for label in set(label_data)])
# 计算每个分类的概率
p_label = labels_feature / feature_count
feature_shannonEnt += (feature_count / data_count) * np.sum(-p_label * np.log2(p_label))
# 计算 IV
IV += (feature_count / data_count) * np.log2(feature_count / data_count)
# 将当前特征对应的信息增益比添加到 gain_list 列表中
gain_list.append((base_ent - feature_shannonEnt) / IV)
最后,返回其中信息增益比最大特征的下标。
gain_list = np.array(gain_list)
return np.argmax(gain_list)
【完整代码】:
def calc_shannon_ent(data, labels):
data_count = float(data.shape[0])
features = data.shape[1]
# 统计每个分类的个数
labels_count = np.array([labels[labels == label].size for label in set(labels)])
# 计算数据集的信息熵
base_ent = -np.sum((labels_count / data_count) * np.log2(labels_count / data_count))
# 存放各种特征值信息增益比
gain_list = []
# 计算每个特征划分后的数据集的信息熵
for feature in range(0, features):
# 获取特征对应的数据
feature_data = data[:, feature]
# 获取特征信息,以特征值为 key,特征值的数目为 value
feature_info = {feature: feature_data[feature_data == feature].size for feature in set(feature_data)}
feature_shannonEnt, IV = 0, 0
# 获取每个特征值的分类信息并计算条件信息熵
for feature_value in feature_info:
# 当前特征值的数目
feature_count = float(feature_info[feature_value])
label_data = labels[feature_data == feature_value]
labels_feature = np.array([label_data[label_data == label].size for label in set(label_data)])
# 计算每个分类的概率
p_label = labels_feature / feature_count
feature_shannonEnt += (feature_count / data_count) * np.sum(-p_label * np.log2(p_label))
# 计算 IV
IV += (feature_count / data_count) * np.log2(feature_count / data_count)
# 将当前特征对应的信息增益比添加到 gain_list 列表中
gain_list.append((base_ent - feature_shannonEnt) / IV)
gain_list = np.array(gain_list)
return np.argmax(gain_list)
split_dataset()
split_dataset() 方法根据特征和特征值划分数据集,该参数接受四个参数:
- data:数据集
- labels:标签列表
- feature:特征
- value:特征值
split_dataset() 返回一个包含划分后数据集和标签集的元组。
【完整代码】:
def split_dataset(data, labels, feature, value):
"""
:param data: 数据集 ndarray
:param labels: 标签列表 ndarray
:param feature: 特征
:param value: 特征值
:return:
"""
feature_data = data[:, feature]
select_rows = feature_data == value
return (np.delete(data[select_rows], feature, axis=1), labels[select_rows])
voting_label()
voting_label() 投票函数,用以确定叶结点的分类结果。该方法仅接受一个参数 labels,标签集。
【完整代码】:
def voting_label(labels):
return sorted([(label, len(labels[label == label])) for label in set(labels)])[-1][1]
create_tree()
create_tree() 递归创建决策树函数,该方法接受三个参数:
- data:数据
- labels:标签集
- features:特征集
首先,判断特征集是否存在,若不存在,则当前结点作为叶结点。
if len(features) == 0:
return Node(voting_label(labels))
然后,判断标签集,若仅有一种标签,则当前结点作为叶结点。
if len(set(labels)) == 1:
return Node(labels[0])
接着,调用 calc_shannon_ent() 方法获取最优特征的下标,并根据下标得到最优特征。
best_feature_index = calc_shannon_ent(data, labels)
best_feature = features[best_feature_index]
第四步,创建结点,并将已划分的特征从特征集中移除。
node = Node(best_feature)
features = np.delete(features, best_feature_index)
第五步,根据最优特征划分数据集。
best_feature_data = data[:, best_feature_index]
best_feature_info = {feature: best_feature_data[best_feature_data == feature].size for feature in set(best_feature_data)}
注意:best_feature_info 是一个包含各特征值所对应数据集的字典。
第六步,根据各特征值对应的数据集,递归调用 create_tree() 方法来创建子结点。
for feature_value in best_feature_info:
split_data, split_labels = split_dataset(data, labels, best_feature_index, feature_value)
node.children[feature_value] = create_tree(split_data, split_labels, features)
最后,返回根结点。
return node
【完整代码】:
def create_tree(data, labels, features):
# 判断:特征集是否存在,如果不存在,则当前结点作为叶结点
if len(features) == 0:
return Node(voting_label(labels))
# 判断:标签集,若标签只有一种,则当前结点作为叶结点
if len(set(labels)) == 1:
return Node(labels[0])
# 获取最优特征的下标
best_feature_index = calc_shannon_ent(data, labels)
best_feature = features[best_feature_index]
# 创建结点
node = Node(best_feature)
# 将已划分的特征从特征集中移除
features = np.delete(features, best_feature_index)
# 根据最优特征划分数据集
best_feature_data = data[:, best_feature_index]
best_feature_info = {feature: best_feature_data[best_feature_data == feature].size for feature in set(best_feature_data)}
for feature_value in best_feature_info:
split_data, split_labels = split_dataset(data, labels, best_feature_index, feature_value)
node.children[feature_value] = create_tree(split_data, split_labels, features)
return node
【调用 create_tree() 函数】:
root = create_tree(x, y, list(range(x.shape[1])))
print(root.value) # 输出 1
对比 ID3 算法的输出结果,发现根节点的特征发生了变化。也就是说,基于不同的特征选择标准,决策树内部节点的特征也会相应地变化。
完整代码以及调用过程可从 传送门 中获取。
参考
- 《统计学习方法》李航
- 《机器学习》周志华