描述
决策树是广泛用于分类和回归任务的模型。本质上,它从一层层的 if/else 问题中进行学习,并得出结论。
决策树的基本概念:
- 节点:树中的每个点称为节点。根节点是树的起点,内部节点是决策点,叶节点是最终的决策结果。
- 分支:从一个节点到另一个节点的路径称为分支。
- 分裂:根据某个特征将数据集分成多个子集的过程。
- 纯度:衡量一个子集中样本的类别是否一致。纯度越高,说明子集中的样本越相似。
决策树通过树状结构来表示决策过程,每个内部节点代表一个特征或属性的测试,每个分支代表测试的结果,每个叶节点代表一个类别或值。
构造决策树
构造决策树,就是学习一系列 if/else 问题,使我们能够以最快的速度得到正确答案。在机器学习中,这些问题叫作测试(注意不是测试集,测试集是用来测试模型的泛化能力的数据)。
数据通常不是具有二元特征(是/否)形式,而是表示为连续特征。为了构造决策树,算法搜遍所有可能的测试(通过递归的方式),找出对目标变量来说信息量最大的那一个作为根节点。然后先生成一个二元决策树,其中每个结点都包含一个测试。
对数据反复进行递归划分,直到划分后的每个区域(决策树的每个叶结点)只包含单一目标值(单一类别或单一回归值)。如果树中某个叶结点所包含数据点的目标值都相同,那么这个叶结点就是纯的(pure)
控制决策树的复杂度
通常来说,构造决策树直到所有叶结点都是纯的叶结点,这会导致模型非常复杂,并且对训练数据高度过拟合。纯叶结点的存在说明这棵树在训练集上的精度是 100%。训练集中的每个数据点都位于分类正确的叶结点中。
防止过拟合有两种常见的策略:
- 预剪枝:及早停止树的生长
- 后剪枝:先构造数,随后删除或折叠信息量很少的节点
注意:scikit-learn 的决策树在 DecisionTreeRegressor 类和 DecisionTreeClassifier 类中实现,但是只实现了预剪枝,没有实现后剪枝。
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train,X_test,Y_train,Y_test= train_test_split(cancer.data,cancer.target,random_state=42)
tree = DecisionTreeClassifier(random_state=0).fit(X_train,Y_train)
print(tree.score(X_train,Y_train),tree.score(X_test,Y_test))
执行上例可以看到,训练能拿满分(这是因为叶结点都是纯的,树的深度很大,能记住训练数据的所有标签),测试分数略低于线性模型
未剪枝的树容易过拟合,对新数据的泛化性能不佳。现在通过预剪枝的方式进行调整:设置 max_depth=4,限制树的深度。限制树的深度可以减少过拟合。这会降低训练集的精度,但可以提高测试集的精度
tree = DecisionTreeClassifier(random_state=0,max_depth=4).fit(X_train,Y_train)
print(tree.score(X_train,Y_train),tree.score(X_test,Y_test))
执行上例,可以明显看到测试得分有所提高。
分析决策树
可以利用 tree 模块的 export_graphviz 函数来将树可视化。需要安装graphviz和graphviz软件,否则无法查看!
from sklearn.tree import export_graphviz
import graphviz
dot_data = export_graphviz(tree,out_file=None,class_names=["malignant","benign"],
feature_names=cancer.feature_names,impurity=False,filled=True)
graph = graphviz.Source(dot_data)
graph.render('tree.dot')
graph.view()
树的可视化有助于深入理解算法是如何进行预测的。
树的特征重要性
可以利用一些有用的属性来总结树的工作原理。其中最常用的是特征重要性,它为每个特征对树的决策的重要性进行排序。对于每个特征来说,它都是一个介于 0 和 1 之间的数字,其中 0 表示“根本没用到”,1 表示“完美预测目标值”。特征重要性的求和始终为 1。
# tree.feature_importances_ 获取重要特征
# 通过可视化展示特征
import pandas as pd
import numpy as np
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
df_feature = pd.DataFrame(data=tree.feature_importances_,columns=['feature_importance_'],index=cancer.feature_names)
sns.barplot(df_feature,y=df_feature.index,x='feature_importance_')
执行上例,可以看出“mean concave points ”是最重要的特征,这与tree.dot.pdf(导出的文件)中的信息一致,“mean concave points ”作为根节点。
如果某个特征的 feature_importance_ 很小,并不能说明这个特征没有提供任何信息。这只能说明该特征没有被树选中,可能是因为另一个特征也包含了同样的信息(与线性模型的系数不同,特征重要性始终为正数,也不能说明该特征对应哪个类别)。
回归决策树
回归决策树在DecisionTreeRegressor中实现,其内容与分类决策树类似(DecisionTreeClassifier)。
特殊性质:DecisionTreeRegressor不能外推,也不能在训练数据范围之外进行预测。看一个例子:
import pandas as pd
import numpy as np
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression
df = pd.read_csv(r'../../seaborn-data/ram_prices.csv',usecols=[1,2])
# 对比树和线性模型对未来数据的预测
data_train = df[df.date<2000]
data_test = df[df.date>=2000]
X_train = data_train.date[:,np.newaxis] # np.newaxis是一个非常实用的属性,它的作用是在数组中增加一个新的维度。
Y_train = np.log(data_train.price) # 对数变换得到数据和目标之间更简单的关系
tree = DecisionTreeRegressor().fit(X_train, Y_train)
linear_reg = LinearRegression().fit(X_train, Y_train)
X_all = df.date[:, np.newaxis]
pred_tree = tree.predict(X_all)
pred_lr = linear_reg.predict(X_all)
# 对数变换逆运算
price_tree = np.exp(pred_tree)
price_lr = np.exp(pred_lr)
df['tree_all'] = price_tree
df['linear_all'] = price_lr
px.line(df,x=df.date,y=['price','tree_all','linear_all'],log_y=True).show()
#import seaborn as sns
#sns.lineplot(df,x='date',y='price',c='red')
#sns.lineplot(df,x='date',y='tree_all',c='blue')
#sns.lineplot(df,x='date',y='linear_all',c='green')
#plt.yscale('log')
两个模型之间的差异非常明显。线性模型用一条直线对数据做近似,对测试数据(2000 年后的价格)给出了相当好的预测。
树模型完美预测了训练数据(没有限制树的复杂度,因此它记住了整个数据集)。一旦输入超出了模型训练数据的范围,模型
就只能持续预测最后一个已知数据点,所有基于树的模型都有这个缺点。
优缺点、参数
控制决策树模型复杂度的参数是预剪枝参数,它在树完全展开之前停止树的构造。通常来说,选择一种预剪枝策略(设置 max_depth、max_leaf_nodes 或 min_samples_leaf)足以防止过拟合。
决策树有两个优点:一是得到的模型很容易可视化,非专家也很容易理解(至少对于较小的树而言);二是算法完全不受数据缩放的影响。由于每个特征被单独处理,而且数据的划分也不依赖于缩放,因此决策树算法不需要特征预处理,比如归一化或标准化。特别是特征的尺度完全不一样时或者二元特征和连续特征同时存在时,决策树的效果很好。
决策树的主要缺点在于,即使做了预剪枝,它也经常会过拟合,泛化性能很差。因此,在大多数应用中,往往使用决策树集成来替代单棵决策树。
决策树集成
集成是合并多个机器学习模型来构建更强大模型的方法。
随机深林
决策树的一个主要缺点在于经常对训练数据过拟合。随机森林是解决这个问题的一种方法。随机森林本质上是许多决策树的集合,其中每棵树都和其他树略有不同。随机森林背后的思想是,每棵树的预测可能都相对较好,但可能对部分数据过拟合。如果构造很多树,并且每棵树的预测都很好,但都以不同的方式过拟合,那么我们可以对这些树的结果取平均值来降低过拟合。既能减少过拟合又能保持树的预测能力,这可以在数学上严格证明。
随机森林的名字来自于将随机性添加到树的构造过程中,以确保每棵树都各不相同。随机森林中树的随机化方法有两种:
- 选择用于构造树的数据点
- 选择每次划分测试的特征
构造随机深林
需要定用于构造的树的个数RandomForestRegressor或RandomForestClassifier的n_estimators参数)。这些树在构造时彼此完全独立,算法对每棵树进行不同的随机选择,以确保树和树之间是有区别的。
比如需要构造10棵树,这些树在构造时彼此完全独立,算法对每棵树进行不同的随机选择。想要构造一棵树,首先要对数据进行自助采样(bootstrap sample)。也就是说,从 n_samples 个数据点中有放回地(即同一样本可以被多次抽取)重复随机抽取一个样本,共抽取 n_samples 次。这样会创建一个与原数据集大小相同的数据集,但有些数据点会缺失(大约三分之一),有些会重复。
决策树算法稍作修改:在每个结点处,算法随机选择特征的一个子集,并对其中一个特征寻找最佳测试,而不是对每个结点都寻找最佳测试。选择的特征个数由 max_features 参数来控制。每个结点中特征子集的选择是相互独立的,这样树的每个结点可以使用特征的不同子集来做出决策。
由于使用了自助采样,随机森林中构造每棵决策树的数据集都是略有不同的。由于每个结点的特征选择,每棵树中的每次划分都是基于特征的不同子集。这两种方法共同保证随机森林中所有树都不相同。
关键参数是 max_features:设置 max_features 等于n_features,那么每次划分都要考虑数据集的所有特征,在特征选择的过程中没有添加随机性(不过自助采样依然存在随机性)。如果设置 max_features 等于 1,那么在划分时将无法选择对哪个特征进行测试,只能对随机选择的某个特征搜索不同的阈值。因此,如果 max_features 较大,那么随机森林中的树将会十分相似,利用最独特的特征可以轻松拟合数据。如果 max_features 较小,那么随机森林中的树将会差异很大,为了很好地拟合数据,每棵树的深度都要很大。
对于分类问题,首先对森林中的每棵树进行预测,给出每个可能的输出标签的概率。对所有树的预测概率取平均值,然后将概率最大的类别作为预测结果。
对于回归问题,首先对森林中的每棵树进行预测,对这些结果取平均值作为最终预测。
随机森林比单独每一棵树的过拟合都要小,给出的决策边界也更符合直觉。在任何实际应用中,我们会用到更多棵树(通常是几百或上千),从而得到更平滑的边界。
将包含 100 棵树的随机森林应用在乳腺癌数据集上:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train,X_test,Y_train,Y_test= train_test_split(cancer.data,cancer.target,random_state=42)
forest = RandomForestClassifier(n_estimators=100, random_state=0)
forest.fit(X_train, Y_train)
print(forest.score(X_train, Y_train),forest.score(X_test, Y_test)) # 测试得分超过了线性模型
执行上例可以看到在没有调节任何参数的情况下得到了一个不错的模型。可以调节 max_features 参数,或者像单棵决策树那样进行预剪枝对模型进行调优。
与决策树类似,随机森林也可以给出特征重要性,计算方法是将森林中所有树的特征重要性求和并取平均。。一般来说,随机森林给出的特征重要性要比单棵树给出的更为可靠。
df_feature = pd.DataFrame(data=forest.feature_importances_,columns=['val'],index=cancer.feature_names)
sns.barplot(df_feature,x='val',y=df_feature.index)
优缺点
用于回归和分类的随机森林是目前应用最广泛的机器学习方法之一。这种方法非常强大,通常不需要反复调节参数就可以给出很好的结果,也不需要对数据进行缩放。
随机森林本质上是随机的,设置不同的随机状态(或者不设置 random_state参数)可以彻底改变构建的模型。森林中的树越多,它对随机状态选择的鲁棒性就越好。
对于维度非常高的稀疏数据(比如文本数据),随机森林的表现往往不是很好。对于这种数据,使用线性模型可能更合适。
需要调节的重要参数有 n_estimators 和 max_features,可能还包括预剪枝选项(如 max_depth)。n_estimators 总是越大越好。对更多的树取平均可以降低过拟合,从而得到鲁棒性更好的集成(树越多需要的内存也越多,训练时间也越长)。
max_features 决定每棵树的随机性大小,较小的 max_features 可以降低过拟合。一般来说,好的经验就是使用默认值:对于分类,默认值是 max_features=sqrt(n_features); 对 于 回 归, 默 认 值 是 max_features=n_features。增大 max_features 或 max_leaf_nodes 有时也可以提高性能。
梯度提升回归树
梯度提升回归树是另一种集成方法,通过合并多个决策树来构建一个更为强大的模型。虽然名字中含有“回归”,但这个模型既可以用于回归也可以用于分类。与随机森林方法不同,梯度提升采用连续的方式构造树,每棵树都试图纠正前一棵树的错误。默认情况下,梯度提升回归树中没有随机化,而是用到了强预剪枝。梯度提升树通常使用深度很小(1到 5 之间)的树,这样模型占用的内存更少,预测速度也更快。与随机森林相比,它通常对参数设置更为敏感,但如果参数设置正确的话,模型精度更高。
梯度提升的主要思想是合并许多简单的模型,每棵树只能对部分数据做出好的预测,因此,添加的树越来越多,可以不断迭代提高性能。
重要参数 learning_rate(学习率):用于控制每棵树纠正前一棵树的错误的强度。较高的学习率意味着每棵树都可以做出较强的修正,这样模型更为复杂。通过增大 n_estimators 来向集成中添加更多树,也可以增加模型复杂度,因为模型有更多机会纠正训练集上的错误。
在乳腺癌数据集上应用 GradientBoostingClassifier (使用 100 棵树,最大深度是 3,学习率为 0.1):
from sklearn.ensemble import GradientBoostingClassifier
gbrt = GradientBoostingClassifier(random_state=0,n_estimators=100,max_depth=3,learning_rate=0.1)
gbrt = gbrt.fit(X_train, Y_train)
print(gbrt.score(X_train, Y_train),gbrt.score(X_test, Y_test))
执行上例,训练得满分,很可能存在过拟合,为了降低过拟合,可以限制最大深度来加强预剪枝,也可以降低学习率:
gbrt = GradientBoostingClassifier(random_state=0,n_estimators=100,max_depth=1,learning_rate=0.05)
gbrt = gbrt.fit(X_train, Y_train)
print(gbrt.score(X_train, Y_train),gbrt.score(X_test, Y_test))
执行上例发现测试得分有所提高
查看重要特性:
df_feature = pd.DataFrame(data=gbrt.feature_importances_,columns=['val'],index=cancer.feature_names)
sns.barplot(df_feature,x='val',y=df_feature.index)
优缺点
梯度提升决策树是监督学习中最强大也最常用的模型之一。其主要缺点是需要仔细调参,而且训练时间可能会比较长。与其他基于树的模型类似,这一算法不需要对数据进行缩放就可以表现得很好,而且也适用于二元特征与连续特征同时存在的数据集。与其他基于树的模型相同,它也通常不适用于高维稀疏数据。
梯度提升树模型的主要参数包括树的数量 n_estimators 和学习率 learning_rate,后者用于控制每棵树对前一棵树的错误的纠正强度。这两个参数高度相关,因为 learning_rate 越低,就需要更多的树来构建具有相似复杂度的模型。随机森林的 n_estimators 值总是越大越好,但梯度提升不同,增大 n_estimators 会导致模型更加复杂,进而可能导致过拟合。
另一个重要参数是 max_depth(或 max_leaf_nodes),用于降低每棵树的复杂度。梯度提升模型的 max_depth 通常都设置得很小,一般不超过 5。
注释:模型调优可以通过学习曲线来确定最优参数值。