本文主要介绍机器学习中的决策树模型。决策树模型是一类算法的集合,在数据挖掘十大算法中,具体的决策树算法占有两席位置,即C4.5和CART算法。决策树是通过一系列规则对数据进行分类的过程。它提供一种在什么条件下会得到什么值的类似规则的方法。决策树分为分类树和回归树两种,分类树对离散变量做决策树,回归树对连续变量做决策树。同时也特别适合集成学习比如随机森林。
一棵决策树的生成过程主要分为以下3个部分:
特征选择: 是指从训练数据中众多的特征中选择一个特征作为当前节点的分裂标准,如何选择特征有着很多不同量化评估标准标准,从而衍生出不同的决策树算法。
决策树生成: 根据选择的特征评估标准,从上至下递归地生成子节点,直到数据集不可分则停止决策树停止生长。 树结构来说,递归结构是最容易理解的方式。
决策树剪枝: 决策树容易过拟合,一般来需要剪枝,缩小树结构规模、缓解过拟合。剪枝技术有预剪枝和后剪枝两种。
决策树(Decision Tree),又称判断树,它是一种以树形数据结构来展示决策规则和分类结果的模型,作为一种归纳学习算法,其重点是将看似无序、杂乱的已知实例,通过某种技术手段将它们转化成可以预测未知实例的树状模型,每一条从根结点(对最终分类结果贡献最大的属性)到叶子结点(最终分类结果)的路径都代表一条决策的规则。
划分数据集的最大原则是:使无序的数据变的有序。 如果一个训练数据中有20个特征,那么选取哪个做划分依据?这就必须采用量化的方法来判断,量化划分方法有多重,其中一项就是“信息论度量信息分类”。基于信息论的决策树算法有ID3
、C4.5
和 CART
等算法,其中C4.5
和CART
两种算法从ID3
算法中衍生而来。
ID3
算法使用 信息增益
作为分裂的规则,信息增益越大,则选取该分裂规则。多分叉树。信息增益可以理解为,有了x以后对于标签p的不确定性的减少,减少的越多越好,即信息增益越大越好。
ID3算法可用于划分标称型数据集,没有剪枝的过程,为了去除过度数据匹配的问题,可通过裁剪合并相邻的无法产生大量信息增益的叶子节点(例如设置信息增益阀值)。使用信息增益选择属性,其实是有一个缺点,那就是它偏向于具有大量值的属性——就是说在训练集中,某个属性所取的不同值的个数越多,那么越有可能拿它来作为分裂属性,而这样做有时候是没有意义的,另外ID3不能处理连续分布的数据特征,于是就有了C4.5算法。CART算法也支持连续分布的数据特征。
C4.5算法
是ID3
的一个改进算法,继承了ID3算法的优点。使用信息增益率作为分裂规则(需要用信息增益除以,该属性本身的熵),此方法避免了ID3算法
中的归纳偏置问题,因为ID3算法
会偏向于选择类别较多的属性(形成分支较多会导致信息增益大)。多分叉树。连续属性的分裂只能二分裂,离散属性的分裂可以多分裂,比较分裂前后信息增益率,选取信息增益率最大的。
C4.5算法用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足,在树构造过程中进行剪枝;能够完成对连续属性的离散化处理;能够对不完整数据进行处理。C4.5算法产生的分类规则易于理解、准确率较高;但效率低,因树构造过程中,需要对数据集进行多次的顺序扫描和排序。也是因为必须多次数据集扫描,C4.5只适合于能够驻留于内存的数据集。
CART
的全称为Classification And Regression Tree
,即分类回归树(只能形成二叉树)。采用的是Gini指数(选Gini指数最小的特征s)作为分裂标准,同时它也是包含后剪枝操作。ID3算法和C4.5算法虽然在对训练样本集的学习中可以尽可能多地挖掘信息,但其生成的决策树分支较大,规模较大。为了简化决策树的规模,提高生成决策树的效率,就出现了根据GINI系数来选择测试属性的决策树算法CART。
对于分类树(目标变量为离散变量):使用基尼系数作为分裂规则。比较分裂前的gini和分裂后的gini减少多少,减少的越多,则选取该分裂规则,这里的求解方法只能是离散穷举。关于基尼系数,可以参考周志华的西瓜书决策树那章,讲得比较简洁,也比较易懂。“直观来说,(数据集D的基尼系数)Gini(D)反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率,因此Gini(D)越小,则数据集D的纯度越高。”
具体这个的计算,我觉得有例子才好理解,下面这个红绿球的例子很好的说明了,如何根据损失函数最小(也就是基尼系数最小)来选取分裂规则。
对于回归树(目标变量为连续变量):使用最小方差作为分裂规则。只能生成二叉树。
决策树算法既可用于解决分类问题,也可以用于解决回归问题。scikit-learn
中提供了一个DecisionTreeRegressor
实现回归决策树,用于回归问题:DecisionTreeRegressor;提供了一个DecisionTreeClassifier
实现分类决策树,用于分类问题。这里,我们主要讲分类决策树:DecisionTreeClassifier 。
其原型为: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, min_impurity_split=None, class_weight=None, presort=False)
criterion
:str
,指定切分质量的评价标准。
gini
:表示切分时评价准则是Gini
系数。entropy
:表示切分时评价标准是熵。splitter
:str
,指定切分原则。
best
:表示选择最优的切分。random
: 表示随机切分。max_depth
:int
或者None
,指定树的最大深度。
min_samples_split
:int
,指定每个内部节点(非叶节点)包含的最少的样本数。
min_samples_leaf
:int
,指定每个叶节点包含的最少的样本数。
min_weight_fraction_leaf
:float
,叶节点中样本的最小权重系数。
max_features
:str
、float
、int
或者None
,指定寻找best split
时考虑的特征数量。
int
,则每次切分只考虑max_features
个特征。float
,则每次切分只考虑max_features*n_feature
个特征(max_features
指定了百分比)。auto
或者sqrt
,则max_features
等于sqrt(n_features)
。log2
,则max_features
等于log2(n_features)
。None
,则max_features
等于n_features
。random_state
:int
、RandomState
实例,或者为None
。
int
:指定随机数生成器的种子。RandomState实例
:指定随机数生成器。None
:使用默认的随机数生成器。max_leaf_nodes
:int
或者None
,指定最大的叶节点数量。如果为None
,此时叶节点数量不限;如果非None
,则max_depth
被忽略。
class_weight
:字典、字典的列表,字符串balanced
或者None
,指定分类的权重。权重的形式为:{class_label: weight}
balanced
:则每个分类的权重与该分类在样本集中出现的概率成反比。presort
:bool
,指定是否要提前排序数据,从而加速寻找最优切分的过程。设置为True
,对于大数据集会减慢总体的训练过程;但是对于一个小数据集或者设定最大深度的情况下,会加速训练过程。
classes_
: 分类的标签值。
feature_importances_
: 给出特征的重要程度。该值越高,说明该特征越重要(也称为Gini importance
)。
max_features_
: max_features
的推断值。
n_classes_
: 给出分类的数量。
n_features_
: 执行fit
之后,特征的数量。
n_outputs_
:执行fit
之后,输出的数量。
tree_
:一个Tree
对象,即底层的决策树。
fit(X, y[, sample_weight, check_input, ...])
: 训练模型。
predict(X[, check_input])
: 用模型进行预测,返回预测值。
predict_log_proba(X)
: 返回一个数组,数据的元素依次是X预测为各个类别的概率的对数值。
predict_proba(X)
: 返回一个数组,数组的元素依次是X预测为各个类别的概率值。
score(X, y[, sample_weight])
: 返回在(X, y)
上预测的准确率(accuracy
)。
本文使用决策树算法,对泰坦尼克号上哪些人可能成为幸存者进行预测,数据来源于Kaggle
,泰坦尼克数据地址。
我们先下载csv文件,然后利用pandas
加载这些数据,train.csv
是一个892行,12列的数据。意
__author__ = "fpZRobert"
"""
决策树算法—泰坦尼克幸存者预测
"""
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
"""
加载数据
"""
# 加载训练集
train_data = pd.read_csv('./data/train.csv')
# 查看数据形状
print("train_data.shape: ", train_data.shape) # (981, 12)
print(train_data.head()) # 查看数据前5行
特征 | 含义 |
---|---|
PassengerID | 乘客的ID号,用来唯一标识乘客。 |
Survived | 1表示幸存,0表示遇难,这是类别 |
Pclass | 仓位等级 |
Name | 乘客名字 |
Sex | 乘客性别 |
Age | 乘客年龄 |
SibSp | 兄弟姐妹同在船上的数量 |
Parch | 同船的父辈人员数量 |
Ticket | 乘客票号 |
Fare | 乘客的体热特征 |
Cabin | 乘客所在的船舱号 |
Embarked | 乘客登船的港口 |
我们先查看一下数据信息总览,然后我们需要先分析这些特征,以便决定哪些特征可以用来进行模型训练:
# 数据信息总览
print(train_data.info())
首先,我们发现PassengerID
、Name
、Ticket
这三个特征只做标识乘客的作用,与是否幸存无关,所以我们去掉这两个特征。另外,通过输出数据信息可知,age
、Cabin
、Embarked
均存在缺失值的情况,尤其是Cabin
,缺失了大部分信息,所以我们暂且先丢弃这个特征。Embarked
是港口信息,我们需要将其转换为数值型数据。
总结一下,我们需要做以下数据的预处理,包括:
Survived
列的数据作为类别标签。"""
数据预处理
"""
# 指定第一列作为行索引
train_data = pd.read_csv("./data/train.csv", index_col=0)
# 丢弃无用的数据
train_data.drop(["Name", "Ticket", "Cabin"], axis=1, inplace=True)
# print(train_data.head())
# 处理性别数据
train_data["Sex"] = (train_data["Sex"] == "male").astype(int) # male: 1 female: 0
# 处理港口数据
labels = train_data["Embarked"].unique().tolist()
# 处理缺失数据:这里用最简单的0值填充
train_data = train_data.fillna(0)
处理完的数据样本如下图所示:
然后,我们需要将Survived
列提取出来作为标签,并在原始数据集中将其丢弃,同时将数据集分成训练集和交叉验证集:
"""
拆分数据集
"""
from sklearn.model_selection import train_test_split
y = train_data["Survived"].values
X = train_data.drop("Survived", axis=1).values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
print("X_train shape:", X_train.shape, "X_test shape:", X_test.shape)
Out:
X_train shape: (712, 7) X_test shape: (179, 7)
接下来,我们利用决策树模型对数据进行拟合,并计算得分:
"""
模型训练
"""
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
print("train score:{0:.3f}; test score:{1:.3f}".format(train_score, test_score))
Out:
train score:0.979; test score:0.754
从输出结果来看,训练样本的评分很高,但交叉验证集的评分比较低。很明显,这是过拟合的现象。解决决策树过拟合的方法是剪枝,包括前剪枝和后剪枝。不幸的是,scikit-learn
不支持后剪枝,但提供了一系列模型参数进行前剪枝。例如,我们通过max_depth
参数限定决策树的深度,当决策树达到限定的深度时,就不再进行分裂了,这样在一定程度上可以避免过拟合。
"""
模型参数调优
"""
import numpy as np
# 优化模型参数:max_depth
def cv_score(d):
"""
在不同depth值下,train_score和test_score的值
:param d: max_depth值
:return: (train_score, test_score)
"""
clf = DecisionTreeClassifier(max_depth=d)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
return (train_score, test_score)
# 指定参数的范围,训练模型计算得分
depths = range(2, 15)
scores = [cv_score(d) for d in depths]
train_scores = [s[0] for s in scores]
cv_scores = [s[1] for s in scores]
# 找出交叉验证集评分最高的模型参数
best_score_index = np.argmax(cv_scores)
best_score = cv_scores[best_score_index]
best_param = depths[best_score_index] # 找出对应的参数
print("best param: {0}; best score: {1:.3f}".format(best_param, best_score))
Out:
best param: 4; best score: 0.844
可以看到,针对模型深度这个参数,最优值为4, 其对应的交叉验证集的评分为0.844,具有较大的提升。我们可以把模型参数和模型评分可视化,更加直观的观察变化规律:
"""
参数调优可视化
"""
import matplotlib.pyplot as plt
plt.figure(figsize=(6, 4), dpi=200)
plt.grid()
plt.xlabel("Max depth of Decision Tree")
plt.ylabel("score")
plt.plot(depths, cv_scores, ".g--", label="cross validation score")
plt.plot(depths, train_scores, ".r--", label="training score")
plt.legend()
plt.show()
我们可以使用同样的方法,考察参数mini_impurity_split
。这个参数用来指定信息熵或者基尼不纯度的阈值,当决策树分裂后,其信息增益低于这个阈值时,则不再分裂:
# 优化模型参数:在criterion="gini"下的min_impurity_split
def cv_score(val):
"""
在不同depth值下,train_score和test_score的值
:param d: max_depth值
:return: (train_score, test_score)
"""
clf = DecisionTreeClassifier(criterion="gini", min_impurity_split=val)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
return (train_score, test_score)
# 指定参数的范围,训练模型计算得分
values = np.linspace(0, 0.5, 50)
scores = [cv_score(v) for v in values]
train_scores = [s[0] for s in scores]
cv_scores = [s[1] for s in scores]
# 找出交叉验证集评分最高的模型参数
best_score_index = np.argmax(cv_scores)
best_score = cv_scores[best_score_index]
best_param = values[best_score_index] # 找出对应的参数
print("best param: {0}; best score: {1:.3f}".format(best_param, best_score))
# 画出模型参数与模型评分的关系
plt.figure(figsize=(6, 4), dpi=200)
plt.grid()
plt.xlabel("Min_impurity_split of Decision Tree")
plt.ylabel("score")
plt.plot(values, cv_scores, ".g--", label="cross validation score")
plt.plot(values, train_scores, ".r--", label="training score")
plt.legend()
plt.show()
关于问题二,sklearn.model_selection
包中提供了大量模型选择和评估的工具供我们使用。针对以上问题,可以使用GridSearchCV
类来解决,我们先简单介绍下GridSearchCV
:
"""
模型参数选择工具包
"""
from sklearn.model_selection import GridSearchCV
thresholds = np.linspace(0, 0.5, 50)
# 设置参数矩阵
param_grid = {"min_impurity_split": thresholds}
clf = GridSearchCV(DecisionTreeClassifier(), param_grid, cv=5)
clf.fix(X, y)
print("best param: {0} \nbest score: {1}".format(clf.best_params_, clf.best_score_))
Out:
best param: {'min_impurity_split': 0.2040816326530612}
best score: 0.8215488215488216
其中关键的参数是param_grid
,它是一个字典,字典关键字所对应的值是一个列表。GridSearchCV
会枚举列表里的所有值来构建模型,多次计算训练模型,并计算模型评分,最终得到指定参数值的平均评分及标准差。另外一个关键的参数是cv
,它用来指定交叉验证集的生成规则,代码中cv=5
表示每次计算都把数据集分成5份,其中一份作为交叉验证集,其他作为训练集。最终得到的最优参数及最优评分保存在clf.best_params_
和clf.best_score_
里。此外,clf.cv_results_
保存了计算过程的中间结果。
接下来看一下如何在多组参数之间选择最优的参数:
from sklearn.model_selection import GridSearchCV
entropy_thresholds = np.linspace(0, 1, 50)
gini_thresholds = np.linspace(0, 0.5, 50)
# 设置参数矩阵
param_grid = [{"criterion": ["entropy"], "min_impurity_split": entropy_thresholds},
{"criterion": ["gini"], "min_impurity_split": gini_thresholds},
{"max_depth": range(2, 10)},
{"min_samples_split": range(2, 30, 2)}]
clf = GridSearchCV(DecisionTreeClassifier(), param_grid, cv=5)
clf.fit(X, y)
print("best param: {0} \nbest score: {1}".format(clf.best_params_, clf.best_score_))
Out:
best param: {'criterion': 'entropy', 'min_impurity_split': 0.5306122448979591}
best score: 0.8282828282828283
最后,使用最优参数的决策树到底是什么样呢?我们可以使用sklearn.tree.export_graphviz()
函数把决策树模型参数导出到文件中,然后使用graphviz
工具包生成决策树示意图:
"""
生成决策树图形
"""
"""
生成决策树图形
"""
from sklearn.tree import export_graphviz
clf = DecisionTreeClassifier(criterion='entropy', min_impurity_split=0.5306122448979591)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
print('train score: {0:.3f}; test score: {1:.3f}'.format(train_score, test_score))
# 导出 titanic.dot 文件
with open("titanic.dot", 'w') as f:
f = export_graphviz(clf, out_file=f)
Out:
train score: 0.930; test score: 0.832
生成决策树图形:
dot -Tpng titanic.dot -o titanic.png
决策树实战泰坦尼克幸存者预测全部代码:
__author__ = "fpZRobert"
"""
决策树算法—泰坦尼克幸存者预测
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.tree import DecisionTreeClassifier,export_graphviz
import warnings
warnings.filterwarnings('ignore')
"""
加载数据
"""
# 加载训练集
train_data = pd.read_csv('./data/train.csv')
test_data = pd.read_csv('./data/test.csv')
# 查看数据形状
print("train_data.shape: ", train_data.shape) # (981, 12)
print(train_data.head()) # 查看数据前5行
# 数据信息总览
print(train_data.info())
"""
数据预处理
"""
# 指定第一列作为行索引
train_data = pd.read_csv("./data/train.csv", index_col=0)
# 丢弃无用的数据
train_data.drop(["Name", "Ticket", "Cabin"], axis=1, inplace=True)
# print(train_data.head())
# 处理性别数据
train_data["Sex"] = (train_data["Sex"] == "male").astype(int) # male: 1 female: 0
# 处理港口数据
labels = train_data["Embarked"].unique().tolist()
train_data["Embarked"] = train_data["Embarked"].apply(lambda n: labels.index(n))
# 处理缺失数据:这里用最简单的0值填充
train_data = train_data.fillna(0)
"""
拆分数据集
"""
y = train_data["Survived"].values
X = train_data.drop("Survived", axis=1).values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
print("X_train shape:", X_train.shape, "X_test shape:", X_test.shape)
"""
模型训练
"""
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
print("train score:{0:.3f}; test score:{1:.3f}".format(train_score, test_score))
"""
模型参数调优
"""
# 优化模型参数:max_depth
def cv_score(d):
"""
在不同depth值下,train_score和test_score的值
:param d: max_depth值
:return: (train_score, test_score)
"""
clf = DecisionTreeClassifier(max_depth=d)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
return (train_score, test_score)
# 指定参数的范围,训练模型计算得分
depths = range(2, 15)
scores = [cv_score(d) for d in depths]
train_scores = [s[0] for s in scores]
cv_scores = [s[1] for s in scores]
# 找出交叉验证集评分最高的模型参数
best_score_index = np.argmax(cv_scores)
best_score = cv_scores[best_score_index]
best_param = depths[best_score_index] # 找出对应的参数
print("best param: {0}; best score: {1:.3f}".format(best_param, best_score))
"""
参数调优可视化
"""
plt.figure(figsize=(6, 4), dpi=200)
plt.grid()
plt.xlabel("Max depth of Decision Tree")
plt.ylabel("score")
plt.plot(depths, cv_scores, ".g--", label="cross validation score")
plt.plot(depths, train_scores, ".r--", label="training score")
plt.legend()
plt.show()
# 优化模型参数:在criterion="gini"下的min_impurity_split
def cv_score(val):
"""
在不同depth值下,train_score和test_score的值
:param d: max_depth值
:return: (train_score, test_score)
"""
clf = DecisionTreeClassifier(criterion="gini", min_impurity_split=val)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
return (train_score, test_score)
# 指定参数的范围,训练模型计算得分
values = np.linspace(0, 0.5, 50)
scores = [cv_score(v) for v in values]
train_scores = [s[0] for s in scores]
cv_scores = [s[1] for s in scores]
# 找出交叉验证集评分最高的模型参数
best_score_index = np.argmax(cv_scores)
best_score = cv_scores[best_score_index]
best_param = values[best_score_index] # 找出对应的参数
print("best param: {0}; best score: {1:.3f}".format(best_param, best_score))
# 画出模型参数与模型评分的关系
plt.figure(figsize=(6, 4), dpi=200)
plt.grid()
plt.xlabel("Min_impurity_split of Decision Tree")
plt.ylabel("score")
plt.plot(values, cv_scores, ".g--", label="cross validation score")
plt.plot(values, train_scores, ".r--", label="training score")
plt.legend()
plt.show()
# """
# 模型参数选择工具包
# """
# thresholds = np.linspace(0, 0.5, 50)
# # 设置参数矩阵
# param_grid = {"min_impurity_split": thresholds}
# clf = GridSearchCV(DecisionTreeClassifier(), param_grid, cv=5)
# clf.fit(X, y)
# print("best param: {0} \nbest score: {1}".format(clf.best_params_, clf.best_score_))
# 参数
entropy_thresholds = np.linspace(0, 1, 50)
gini_thresholds = np.linspace(0, 0.5, 50)
# 设置参数矩阵
param_grid = [{"criterion": ["entropy"], "min_impurity_split": entropy_thresholds},
{"criterion": ["gini"], "min_impurity_split": gini_thresholds},
{"max_depth": range(2, 10)},
{"min_samples_split": range(2, 30, 2)}]
clf = GridSearchCV(DecisionTreeClassifier(), param_grid, cv=5)
clf.fit(X, y)
print("best param: {0} \nbest score: {1}".format(clf.best_params_, clf.best_score_))
"""
生成决策树图形
"""
clf = DecisionTreeClassifier(criterion='entropy', min_impurity_split=0.5306122448979591)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
print('train score: {0:.3f}; test score: {1:.3f}'.format(train_score, test_score))
# 导出 titanic.dot 文件
with open("titanic.dot", 'w') as f:
f = export_graphviz(clf, out_file=f)