决策树(Decision Tree)是一种常用的机器学习算法,它通过对数据集进行分割来实现分类或回归。决策树的基本思想是,将数据集分成许多小的子集,直到每个子集都变成纯的单一类别或满足某个停止条件。
决策树由节点和边组成,其中节点分为两种类型:内部节点和叶子节点。内部节点表示一个特征或属性,边表示该特征的取值。叶子节点表示一个类别或数值。决策树从根节点开始,沿着每个节点的边逐步向下,直到达到叶子节点,然后根据叶子节点的类别或数值进行预测。
构建决策树的过程可以分为两个阶段:树的生成和树的剪枝。树的生成阶段是指从训练集中构建决策树的过程,最常用的方法是基于信息增益或基尼指数选择最优的特征来进行分割。树的剪枝阶段是指对已经生成的决策树进行优化,以避免过拟合或提高泛化能力。
特征选择
决策树算法的特征选择是指在构建决策树的过程中,选择最佳的特征来进行数据集的分割。特征选择的目的是找到那些能够提供最大分类能力的特征,以便构建出具有较高预测精度的决策树模型。常用的特征选择方法包括信息增益(Information Gain)、基尼指数(Gini Index)和增益率(Gain Ratio)等。
特征选择的方法:
- 信息增益(Information Gain,也叫 ID3): 信息增益是决策树算法中最常用的特征选择方法之一。它基于信息论的概念,通过计算每个特征对数据集分类所带来的信息增益来选择最佳的特征。信息增益越大,表示该特征对分类的贡献越大,因此被选为分裂节点的概率就越大。
- 基尼指数(Gini Index,也叫 CART): 基尼指数是另一种常用的特征选择方法,它衡量了从数据集中随机选择两个样本,其类别标记不一致的概率。基尼指数越小,表示数据集纯度越高,因此被选为分裂节点的概率就越大。
- 增益率(Gain Ratio,也叫 C4.5): 增益率是信息增益的一种改进方法,它对信息增益进行了归一化处理,解决了信息增益偏向于选择取值较多的特征的问题。增益率考虑了特征取值的数量对信息增益的影响,使得特征选择更加公平和准确。
特征选择的过程通常包括以下步骤:
- 计算每个特征的信息增益、基尼指数或增益率。
- 选择信息增益、基尼指数或增益率最大的特征作为当前节点的分裂特征。
- 将数据集根据选定的特征进行分割,并递归地在子节点上重复以上步骤,直到满足停止条件为止。
特征选择旨在选取对训练数据具备区分能力的特征,如果使用一个特征进行分类的结果与随机分类的结果没有太大差别,那么我们可以说这个特征是不太具有分类能力的,从经验来说丢掉这样的特征对决策树模型的预测精度影响不大。在实际应用中,特征选择是决策树构建过程中的关键环节,合理的特征选择能够提高决策树模型的预测性能,降低过拟合的风险。
贷款决策
有一个是否提供贷款的案例,我们需要根据数据集中的特征信息,决策出是否给他人提供贷款,数据集如下表所示:
ID | 年龄 | 是否有工作 | 是否有房子 | 信贷情况 | 是否提供贷款(类别) |
---|---|---|---|---|---|
1 | 青年 | 否 | 否 | 一般 | 否 |
2 | 青年 | 否 | 否 | 好 | 否 |
3 | 青年 | 是 | 否 | 好 | 是 |
4 | 青年 | 是 | 是 | 一般 | 是 |
5 | 青年 | 否 | 否 | 一般 | 否 |
6 | 中年 | 否 | 否 | 一般 | 否 |
7 | 中年 | 否 | 否 | 好 | 否 |
8 | 中年 | 是 | 是 | 好 | 是 |
9 | 中年 | 否 | 是 | 非常好 | 是 |
10 | 中年 | 否 | 是 | 非常好 | 是 |
11 | 老年 | 否 | 是 | 非常好 | 是 |
12 | 老年 | 否 | 是 | 好 | 是 |
13 | 老年 | 是 | 否 | 好 | 是 |
14 | 老年 | 是 | 否 | 非常好 | 是 |
15 | 老年 | 否 | 否 | 一般 | 否 |
计算信息增益的详细步骤如下所示:
- 计算数据集的熵(Entropy): 数据集的熵表示了数据的不确定性,计算公式为 E n t r o p y ( S ) = − ∑ i = 1 c p i ⋅ log 2 ( p i ) Entropy(S) = -\sum_{i=1}^{c} p_i \cdot \log_2(p_i) Entropy(S)=−∑i=1cpi⋅log2(pi);其中, S S S 为数据集, c c c 为类别的数量, p i p_i pi 为类别 i i i 在数据集 S S S 中出现的概率。
- 对于每个特征 A A A,计算其对数据集 S S S 的条件熵(Conditional Entropy): 特征 A A A 的条件熵表示了在特征 A A A 的情况下,数据集 S S S 的不确定性,计算公式为 H ( S ∣ A ) = ∑ j = 1 v ∣ S j ∣ ∣ S ∣ ⋅ E n t r o p y ( S j ) H(S|A) = \sum_{j=1}^{v} \frac{\lvert S_j \rvert}{\lvert S \rvert} \cdot Entropy(S_j) H(S∣A)=∑j=1v∣S∣∣Sj∣⋅Entropy(Sj);其中, v v v 为特征 A A A 取值的数量, S j S_j Sj 是特征 A A A 取值为 j j j 时对应的数据子集。
- 计算信息增益(Information Gain): 信息增益表示了特征 A A A 带来的条件熵的减少程度,计算公式为 I G ( S , A ) = E n t r o p y ( S ) − H ( S ∣ A ) IG(S, A) = Entropy(S) - H(S|A) IG(S,A)=Entropy(S)−H(S∣A)。
通过以上步骤,可以计算出特征 A A A 对数据集 S S S 的信息增益 I G ( S , A ) IG(S, A) IG(S,A),信息增益越大,表示特征 A A A 对分类的贡献越大,因此被选为分裂节点的概率就越大。
信息增益的计算过程主要涉及了熵和条件熵的计算,其中熵衡量了数据集的不确定性,而条件熵衡量了在特征已知的情况下数据集的不确定性。信息增益则通过比较熵和条件熵的差异来评估特征对分类的贡献程度。
以上述数据集中的年龄一列为例,阐述信息增益的计算过程:
- 在计算数据集的熵时,我们重点关注在整个数据集中目标值(标签值)的类别,我们可以看到目标值分为提供贷款和不提供贷款,提供贷款的样本个数为 9,不提供贷款的样本个数为 6,因此提供贷款的概率为 9/15,不提供贷款的概率为 6/15。
- 我们将年龄特征标记为 A 1 A1 A1,在计算年龄特征的条件熵时,我们重点关注特征 A 1 A1 A1 的类别及特征熵的计算, A 1 A1 A1 一共有三个类别,分别是青年、中年、老年。年龄为青年的样本数据一共有 5 个,分母是整个数据集的样本数,因此年龄为青年的样本数据在训练数据集中出现的概率是 5/15。同理,年龄为中年和老年的样本数据在训练集中出现的概率分别为 5/15、5/15。
- 在计算年龄这个特征的熵时,我们重点关注年龄的类别和目标值,我们需要依据类别将整个数据集划分成若干子集。根据年龄类别,数据集可以划分成青年集、中年集、老年集,每个子集的样本数都为 5。年龄为青年的样本数据最终得到贷款的有 2 个,因此青年这个类别能得到贷款的概率就是 2/5。年龄为中年的样本数据最终得到贷款的有 3 个,因此中年这个类别能得到贷款的概率就是 3/5。年龄为老年的样本数据最终得到贷款的有 4 个,因此老年这个类别能得到贷款的概率就是 4/5。
- 最后根据公式计算出年龄这个特征的信息增益。
熵的计算结果如下所示:
E
n
t
r
o
p
y
(
S
)
=
−
∑
i
=
1
c
p
i
⋅
log
2
(
p
i
)
=
−
9
15
l
o
g
2
9
15
−
6
15
l
o
g
2
6
15
=
0.971
Entropy(S) = -\sum_{i=1}^{c} p_i \cdot \log_2(p_i) = -\frac{9}{15}log_2\frac{9}{15}-\frac{6}{15}log_2\frac{6}{15} = 0.971
Entropy(S)=−i=1∑cpi⋅log2(pi)=−159log2159−156log2156=0.971
条件熵的计算结果如下所示:
H
(
S
∣
A
)
=
∑
j
=
1
v
∣
S
j
∣
∣
S
∣
⋅
E
n
t
r
o
p
y
(
S
j
)
=
5
15
E
n
t
r
o
p
y
(
S
1
)
+
5
15
E
n
t
r
o
p
y
(
S
2
)
+
5
15
E
n
t
r
o
p
y
(
S
3
)
=
5
15
(
−
2
5
l
o
g
2
2
5
−
3
5
l
o
g
2
3
5
)
+
5
15
(
−
3
5
l
o
g
2
3
5
−
2
5
l
o
g
2
2
5
)
+
5
15
(
−
4
5
l
o
g
2
4
5
−
1
5
l
o
g
2
1
5
)
=
0.888
H(S|A) = \sum_{j=1}^{v} \frac{\lvert S_j \rvert}{\lvert S \rvert} \cdot Entropy(S_j) = \frac{5}{15}Entropy(S_1) + \frac{5}{15}Entropy(S_2) + \frac{5}{15}Entropy(S_3) \\ =\frac{5}{15}(-\frac{2}{5}log_2\frac{2}{5}-\frac{3}{5}log_2\frac{3}{5}) + \frac{5}{15}(-\frac{3}{5}log_2\frac{3}{5}-\frac{2}{5}log_2\frac{2}{5}) + \frac{5}{15}(-\frac{4}{5}log_2\frac{4}{5}-\frac{1}{5}log_2\frac{1}{5}) \\ = 0.888
H(S∣A)=j=1∑v∣S∣∣Sj∣⋅Entropy(Sj)=155Entropy(S1)+155Entropy(S2)+155Entropy(S3)=155(−52log252−53log253)+155(−53log253−52log252)+155(−54log254−51log251)=0.888
信息增益的计算结果如下所示:
I
G
(
S
,
A
)
=
E
n
t
r
o
p
y
(
S
)
−
H
(
S
∣
A
)
=
0.971
−
0.888
=
0.083
IG(S, A) = Entropy(S) - H(S|A) = 0.971 - 0.888 = 0.083
IG(S,A)=Entropy(S)−H(S∣A)=0.971−0.888=0.083
同理,我们计算出特征
A
2
A2
A2、
A
3
A3
A3、
A
4
A4
A4 的信息增益分别为 0.324、0.420、0.363,比较所有特征的信息增益,由于
A
3
A3
A3 的信息增益最大,因此我们选择
A
3
A3
A3 作为最佳特征来分割数据集。
接下来,我们通过代码实现上述案例,在编写代码之前,我们需要先将数据集的属性值进行数值转换:
- 年龄:1 代表青年,2 代表中年,3 代表老年
- 是否有工作:1 代表是,2 代表否
- 是否有房子:1 代表是,2 代表否
- 信贷情况:1 代表一般,2 代表好,3 代表非常好
- 是否提供贷款(类别):yes 代表是,no 代表否
代码如下:
import math
import operator
import copy
# 划分数据子集,并返回某一特征列中的某一类别子集
def split_dataset(dataset: list, feature_num: int, key: any) -> list:
"""
:param dataset: 整个数据集
:param feature_num: 特征列的索引
:param key: 特征列的某一类别
:return: 依据特征列中某一类别划分的子数据集
"""
sub_dataset = []
for sample in dataset:
if sample[feature_num] == key:
sub_dataset.append(sample)
return sub_dataset
# 划分数据子集,并返回某一特征列中的某一类别子集,该子集删除了对应特征
def split_del_dataset(dataset: list, feature_num: int, key: any) -> list:
"""
:param dataset: 整个数据集
:param feature_num: 特征列的索引
:param key: 特征列的某一类别
:return: 依据特征列中某一类别划分的子数据集(删除了对应特征)
"""
sub_dataset = []
for sample in dataset:
if sample[feature_num] == key:
sample.pop(feature_num)
sub_dataset.append(sample)
return sub_dataset
# 判定主要类别
def primary_category(label_targets: list) -> str:
"""
:param label_targets: 标签列
:return: 返回出现次数最多的类别
"""
label_targets_dict = {}
for label in label_targets:
if label not in label_targets_dict.keys():
label_targets_dict[label] = 0
label_targets_dict[label] += 1
sorted_label_targets = sorted(label_targets_dict.items(), key=operator.itemgetter(1), reverse=True) # 将键值对以元组形式按值的大小进行从大到小排列
return sorted_label_targets[0][0]
# 计算数据集或特征列的熵
def calculate_entropy(dataset: list, feature_num: int) -> float:
"""
:param dataset: 整个数据集
:param feature_num: 特征列或目标列的索引
:return: 数据集或特征列的熵
"""
num_samples = len(dataset) # 数据集的样本个数
# 将特征列或目标列的类别及出现次数存储到字典
targets = {} # 用于存储标签出现的次数
for sample in dataset:
if sample[feature_num] not in targets.keys():
targets[sample[feature_num]] = 0 # 初始化标签次数
targets[sample[feature_num]] += 1 # 标签次数 +1
# 计算特征列或目标列的熵
entropy = 0.0
for key in targets:
prob = targets[key] / num_samples # 特征列或目标列中某一类别出现的概率
entropy -= prob * math.log(prob, 2) # 计算熵
return entropy
# 计算特征列的条件熵
def calculate_conditional_entropy(dataset: list, feature_num: int) -> float:
"""
:param dataset: 整个数据集
:param feature_num: 特征列的索引
:return: 特征列的条件熵
"""
num_samples = len(dataset) # 数据集的样本个数
# 将特征列的类别及出现次数存储到字典
feature_targets = {} # 用于存储特征列标签出现的次数
for sample in dataset:
if sample[feature_num] not in feature_targets.keys():
feature_targets[sample[feature_num]] = 0 # 初始化标签次数
feature_targets[sample[feature_num]] += 1 # 标签次数 +1
# 计算特征列的条件熵
conditional_entropy = 0.0
for key in feature_targets:
sub_dataset = split_dataset(dataset, feature_num, key) # 获取 feature_num 特征列中的 key 类别子集
prob = feature_targets[key] / num_samples # 特征列中某一类别出现的概率
entropy = calculate_entropy(sub_dataset, -1)
conditional_entropy += prob * entropy
return conditional_entropy
# 计算信息增益
def information_gain(entropy: float, conditional_entropy: float) -> float:
"""
:param entropy: 数据集的熵
:param conditional_entropy: 特征列的条件熵
:return: 特征的信息增益
"""
ig = entropy - conditional_entropy
return ig
# 计算所有特征的信息增益,并选择信息增益最大的特征作为最佳特征
def choose_best_feature(dataset: list) -> int:
"""
:param dataset: 整个数据集
:return: 信息增益最大的特征的索引
"""
num_features = len(dataset[0]) - 1 # 特征个数
ig_lst = [] # 用于存储所有特征的信息增益
entropy = calculate_entropy(dataset, -1) # 计算数据集的熵
# 遍历计算所有特征的信息增益
for idx in range(num_features):
conditional_entropy = calculate_conditional_entropy(dataset, idx) # 计算索引为 idx 的特征的条件熵
ig = information_gain(entropy, conditional_entropy) # 计算索引为 idx 的特征的信息增益
ig_lst.append(ig) # 将当前特征的信息增益添加到存储列表
best_feature_idx = ig_lst.index(max(ig_lst)) # 获取信息增益最大的特征的索引
return best_feature_idx
# 读取数据集
def create_dataset() -> (list, list):
"""
:return: 返回数据集和特征名称
"""
dataset = [[1, 2, 2, 1, 'no'],
[1, 2, 2, 2, 'no'],
[1, 1, 2, 2, 'yes'],
[1, 1, 1, 1, 'yes'],
[1, 2, 2, 1, 'no'],
[2, 2, 2, 1, 'no'],
[2, 2, 2, 2, 'no'],
[2, 1, 1, 2, 'yes'],
[2, 2, 1, 3, 'yes'],
[2, 2, 1, 3, 'yes'],
[3, 2, 1, 3, 'yes'],
[3, 2, 1, 2, 'yes'],
[3, 1, 2, 2, 'yes'],
[3, 1, 2, 3, 'yes'],
[3, 2, 2, 1, 'no']]
feature_labels = ['年龄', '是否有工作', '是否有房子', '信贷情况']
return dataset, feature_labels
# 构建决策树
def create_decision_tree(dataset: list, feature_labels: list, best_feature_labels: list) -> dict:
"""
:param dataset: 整个数据集
:param feature_labels: 存放了所有特征的名称
:param best_feature_labels: 用于存放最佳特征的名称
:return: 以字典形式返回生成的决策树及以列表形式返回最佳特征
"""
label_targets = [sample[-1] for sample in dataset] # 数据集的目标列(是否提供贷款,yes 或 no)
# 判断数据集的目标列中类别是否完全相同,即只有一个类别,并返回该类别
if label_targets.count(label_targets[0]) == len(label_targets):
return label_targets[0]
# 判断是否遍历完所有特征,并返回出现次数最多的类别
if len(dataset[0]) == 1:
return primary_category(label_targets)
best_feature_idx = choose_best_feature(dataset) # 获取当前数据集最佳特征的索引
best_feature_name = feature_labels[best_feature_idx] # 获取当前数据集最佳特征的名称
best_feature_labels.append(best_feature_name) # 将当前数据集最佳特征的名称添加到列表
decision_tree = {best_feature_name: {}} # 根据最佳特征的名称生成树
del feature_labels[best_feature_idx] # 删除已经使用过的特征名称
best_feature_values = [sample[best_feature_idx] for sample in dataset] # 获取最佳特征的所有属性值
unique_feature_values = set(best_feature_values) # 去掉重复的属性值,剩下的就是类别
# 遍历特征,构造决策树
for value in unique_feature_values:
new_dataset = copy.deepcopy(dataset) # 这里需要使用深拷贝创建一个新的完全相同的数据集,否则后面的改动会影响原始数据集
sub_dataset = split_del_dataset(new_dataset, best_feature_idx, value)
decision_tree[best_feature_name][value] = create_decision_tree(sub_dataset, feature_labels, best_feature_labels)
return decision_tree
# 利用决策树进行分类预测
def classify_predict(decision_tree: dict, best_feature_labels: list, test_data: list) -> str:
"""
:param decision_tree: 当前决策树
:param best_feature_labels: 存放了最佳特征的名称
:param test_data: 预测数据
:return: 返回预测标签
"""
root_node = next(iter(decision_tree)) # decision_tree 中的第一个键就是根节点,也就是第一个最佳特征
root_node_value = decision_tree[root_node] # 获取当前根节点的值,是一个字典或字符串
predict_result = 'none'
if root_node in best_feature_labels:
idx = best_feature_labels.index(root_node)
for key in root_node_value.keys():
if test_data[idx] == key:
if type(root_node_value[key]).__name__ == 'str':
predict_result = root_node_value[key]
else:
predict_result = classify_predict(root_node_value[key], best_feature_labels, test_data)
return predict_result
if __name__ == '__main__':
dataset, feature_labels = create_dataset() # 获取数据集和特征名称
best_feature_labels = []
decision_tree = create_decision_tree(dataset, feature_labels, best_feature_labels) # 生成决策树
test_data = [1, 1] # 预测数据,维数取决于决策树的内部节点数
predict_result = classify_predict(decision_tree, best_feature_labels, test_data)
print(decision_tree)
print(predict_result)
---------
{'是否有房子': {1: 'yes', 2: {'是否有工作': {1: 'yes', 2: 'no'}}}}
yes
构造决策树是比较耗时的事,即使处理很小的数据集也可能需要花上几秒钟的时间,如果数据集很大,那将耗费大量的计算时间。如果我们能将构造好的决策树存储起来,在下次需要时直接将其调出来使用,这样就能节省很多时间。为了实现这个想法,我们可以使用 Python 的一个标准库 pickle,它能实现对象的序列化和反序列化,具体来说,就是可以将 Python 对象转化为字节流(序列化),也可以将字节流转化为 Python 对象(反序列化)。
使用 pickle 库,我们可以将 Python 对象保存到文件或将其传输到网络上,以便在需要时进行持久化存储。pickle 库提供了一种方便的方式来处理各种复杂的数据结构,包括列表、字典、类实例等。
示例代码如下:
import pickle
# 序列化对象并保存到文件
data = {'name': 'Alice', 'age': 25}
with open('data.pkl', 'wb') as file:
pickle.dump(data, file)
# 从文件中加载并反序列化对象
with open('data.pkl', 'rb') as file:
loaded_data = pickle.load(file)
print(loaded_data) # 输出: {'name': 'Alice', 'age': 25}
存储与调用决策树的代码如下:
import pickle
# 存储决策树
def store_decision_tree(decision_tree, filename):
with open(filename, 'wb') as file:
pickle.dump(decision_tree, file)
# 调用决策树
def obtain_decision_tree(filename):
with open(filename, 'rb') as file:
decision_tree = pickle.load(file)
return decision_tree
if __name__ == '__main__':
store_decision_tree(decision_tree, './my_tree.txt')
my_tree = obtain_decision_tree('./my_tree.txt')
print(my_tree)
---------
{'是否有房子': {1: 'yes', 2: {'是否有工作': {1: 'yes', 2: 'no'}}}}
佩戴隐形眼镜决策
隐形眼镜数据集是一个非常典型的数据集,一共有 24 个样本数据,特征列和目标列依次为 age(年龄)、prescript(症状)、astigmatic(是否散光)、tearRate(眼泪数量)、class(分类标签)。数据格式如下图所示:
我们将使用 sklearn 模块中的决策树模型来预测结果。
sklearn.tree 模块实现了决策树算法,其函数实现如下所示:
sklearn.tree.DecisionTreeClassifier(criterion='gini', splitter='best', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, class_weight=None, ccp_alpha=0.0)
- criterion:用于特征选择标准,是衡量节点纯度的指标,通过计算节点的纯度,从而确定最佳划分特征,即在哪个特征上进行划分;支持的标准有 gini、entropy、log_loss,默认为 gini;gini 表示基尼不纯度,entropy 和 log_loss 都表示香农信息增益;ID3 算法使用 entropy,CART 算法使用 gini
- splitter:特征划分点选择标准,用于选择每个节点的分割策略;通俗点说就是定义了如何选择最佳划分特征的策略,支持的策略有 best、random,默认为 best;best 表示使用遍历所有特征来选择最佳的划分特征,random 表示随机选择一个特征作为划分特征;best 适合样本数据量不大的情况,当样本数据量很大时,更推荐 random
- max_depth:树的最大深度,默认为 None;如果为 None,则不会限制树的深度,直到所有叶节点都是纯样本或所有叶节点包含的样本数少于 min_samples_split;一般来说,特征少时可以忽略这个值,当特征很多时,可以设定一个值来限制树的最大深度,具体的取值取决于数据的分布,常用的取值范围为 10-100
- min_samples_split:内部节点再划分所需的最小样本数,默认为 2;如果该值为整数,则 min_samples_split 为最小样本数,当数据集样本少于它时会停止切分;如果该值为浮点数,则 ceil(min_samples_split * n_samples) 为最小样本数;样本量不大时可以忽略这个值,当样本量特别大时,推荐增大这个值
- min_samples_leaf:叶节点所需的最小样本数,默认为 1;该值表示最少需要几个样本才能作为叶节点;如果该值为整数,则 min_samples_leaf 为最小样本数;如果该值为浮点数,则 ceil(min_samples_leaf * n_samples) 为最小样本数;样本量不大时可以忽略这个值,当样本量特别大时,推荐增大这个值
- min_weight_fraction_leaf:叶节点所需的权重总和的最小加权分数,默认为 0.0;未提供 sample_weight 时,样本权重相等;这个值限制了叶节点所有样本权重和的最小值,如果小于这个值,就会和兄弟节点一起被剪枝。一般来说,如果样本数据有较多缺失值,或者数据的分布类别偏差很大,就会引入样本权重,这时就需要注意这个值
- max_features:划分时考虑的最大特征数,默认为 None;如果该值为整数,则每次划分时都要考虑 max_features 个特征;如果该值为浮点数,则考虑 max(1, int(max_features * n_features)) 个特征;如果该值为 auto 或 sqrt,则考虑 sqrt(n_features) 个特征;如果该值为 log2,则考虑 log2(n_features) 个特征;如果该值为 None,则考虑 n_features 个特征,即所有特征;一般来说,如果样本特征数不多,比如小于 50,则使用默认的 None 就可以;如果特征数非常多,则可以灵活使用上述取值来控制划分时考虑的最大特征数,以控制决策树的生成时间
- random_state:控制估计器的随机性,可以是 int、RandomState instance、None,默认为 None;在每次划分时,即使 splitter 设置为 best,特征也总是随机排列的;当 max_features < n_features 时,算法会在每次划分时随机选择 max_features,然后再从中找出最佳划分;即使 max_features = n_features,在不同的运行中找到的最佳划分也可能不同;为了在拟合过程中获得确定的行为,必须将 random_state 设为整数;如果为 RandomState instance,则 random_state 为随机数生成器;如果为 None,则随机数生成器使用 np.random
- max_leaf_nodes:最大叶节点数,默认为 None;可以通过限制最大叶节点数来防止过拟合;如果特征不多,可以忽略该值,如果特征特别多,可以加以限制,具体的取值可以通过交叉验证得到
- min_impurity_decrease:划分节点所需的最小不纯度减少量,默认为 0.0;如果划分导致的不纯度减少大于或等于该值,则会进行划分,否则不进行划分
- class_weight:类别权重,默认为 None;指定样本各类别的权重,主要为了防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别;可以自己指定各个样本的权重,也可以使用 balanced,如果使用 balanced,则算法会自己计算权重,样本量少的类别所对应的权重会更高;如果样本类别分布没有太大偏差,可以忽略该值
- ccp_alpha:用于最小成本复杂度剪枝方法的复杂度参数,默认为 0.0;非负数,指定了剪枝阈值
由 DecisionTreeClassifier 创建的实例对象 clf 具有以下方法:
apply(X, check_input=True) # 返回每个样本预测为叶节点的索引
- X:训练数据,形状为 (n_samples, n_features),内部将转换为 dtype=np.float32
- check_input:允许绕过多项输入检查,默认为 True;除非知道在做什么,否则不要轻易使用此参数
对于 X 中的每个样本数据,返回其最终所在的叶节点的索引,形状为 (n_samples,)
decision_path(X, check_input=True) # 返回决策树中的决策路径
- X:训练数据,形状为 (n_samples, n_features),内部将转换为 dtype=np.float32
- check_input:允许绕过多项输入检查,默认为 True;除非知道在做什么,否则不要轻易使用此参数
返回形状为 (n_samples, n_nodes) 的节点指示器 CSR 矩阵,其中非零元素表示样本经过节点
fit(X, y, sample_weight=None, check_input=True) # 根据训练集建立决策树分类器
- X:训练数据,形状为 (n_samples, n_features)
- y:目标值(训练样本对应的标签),形状为 (n_samples,)
- sample_weight:样本权重,如果为 None,则样本权重相同
- check_input:允许绕过多项输入检查,默认为 True;除非知道在做什么,否则不要轻易使用此参数
返回拟合的决策树分类器
get_depth() # 返回决策树的深度
返回树的最大深度
get_n_leaves() # 返回决策树的叶节点数
返回叶节点的个数
get_params(deep=True) # 以字典形式返回 DecisionTreeClassifier 类的参数
- deep:布尔值,默认为 True
返回参数
predict(X, check_input=True) # 预测所提供数据的类别标签
- X:预测数据,形状为 (n_samples, n_features)
- check_input:允许绕过多项输入检查,默认为 True;除非知道在做什么,否则不要轻易使用此参数
以 np.ndarray 形式返回形状为 (n_samples,) 的每个数据样本的类别标签
predict_proba(X, check_input=True) # 返回预测数据 X 在各类别标签中所占的概率
- X:预测数据,形状为 (n_samples, n_features)
- check_input:允许绕过多项输入检查,默认为 True;除非知道在做什么,否则不要轻易使用此参数
返回该样本在各类别标签中的预测概率,类别的顺序与属性 classes_ 中的顺序一致
predict_log_proba(X) # 返回预测数据 X 在各类别标签中所占的对数概率
- X:预测数据,形状为 (n_samples, n_features)
返回该样本在各类别标签中的预测对数概率,类别的顺序与属性 classes_ 中的顺序一致
score(X, y, sample_weight=None) # 返回预测结果和标签之间的平均准确率
- X:预测数据,形状为 (n_samples, n_features)
- y:预测数据的目标值(真实标签)
- sample_weight:默认为 None
返回预测数据的平均准确率,相当于先执行了 self.predict(X),而后再计算预测值和真实值之间的平均准确率
完整的佩戴隐形眼镜决策树模型代码实现如下:
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier
if __name__ == '__main__':
"""
数据集中的数据都是 str 类型,无法直接作为 fit() 函数的参数,因此需要对数据进行转换
通过 LabelEncoder 或 OneHotEncoder 编码可以将字符串转换为整数
将数据集数据转换成 pandas 数据,方便后续操作,原始数据 -> 字典 -> DataFrame
"""
file_path = r'D:\MachineLearning\lenses.txt'
# 读取数据
with open(file_path, 'r') as file:
lenses = [line.strip().split('\t') for line in file.readlines()]
# 提取样本标签
lenses_targets = []
for sample in lenses:
lenses_targets.append(sample[-1])
feature_labels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 定义特征标签
# 将特征存放到字典
lenses_dict = {} # 以键值对方式存储特征标签与特征属性值
for label in feature_labels:
lenses_list = [] # 存储某一特征标签对应的特征属性值
for sample in lenses:
lenses_list.append(sample[feature_labels.index(label)])
lenses_dict[label] = lenses_list
lenses_pd = pd.DataFrame(lenses_dict) # 将字典转换成 DataFrame
# 将字符串转换成整数
le = LabelEncoder()
for col in lenses_pd.columns:
lenses_pd[col] = le.fit_transform(lenses_pd[col])
clf = DecisionTreeClassifier() # 实例化 DecisionTreeClassifier 类对象
clf.fit(lenses_pd.values.tolist(), lenses_targets) # 拟合
test_data = [[1, 1, 1, 0],
[2, 1, 1, 1]]
predict_result = clf.predict(test_data) # 预测结果
print(predict_result)
---------
['hard' 'no lenses']
决策树算法的优缺点
优点
- 可解释性强:决策树的结果可以用树形结构进行可视化,易于理解和解释。
- 适用于多类别问题:决策树可以处理多类别问题,不受类别数量的限制。
- 对异常值不敏感:由于决策树是通过对数据集进行分割来实现分类或回归,因此对异常值的影响较小。
- 可以处理连续特征:决策树可以处理离散特征和连续特征,不需要对数据进行特征缩放。
缺点
- 容易过拟合:由于决策树会不断进行数据分割,容易出现过拟合的情况。可以通过剪枝等方法来避免过拟合,例如设置一个叶节点需要的最小样本数或树的最大深度。
- 不适用于高维数据:随着特征数量的增加,决策树的计算复杂度会呈指数级增长,因此不适用于高维数据。
- 对噪声敏感:如果数据中存在噪声,决策树可能会将噪声误认为特征值,从而影响预测精度。
- 不稳定性高:当数据发生变化时,决策树可能需要重新训练,否则预测精度可能会下降;即使很小的变化都可能产生一颗完全不同的树。
综上所述,决策树算法具有可解释性强、适用于多类别问题、对异常值不敏感、可以处理连续特征等优点。但是,它容易过拟合、不适用于高维数据、对噪声敏感、不稳定性高等缺点也需要注意。在实际应用中,需要根据具体问题的特点选择合适的机器学习算法。