决策树分类讲解:决策树教程
决策树分类讲解:决策树教程你可能会有一些问题,比如:“如何决定分割的变量?如何选择分割的阈值?“,”我可以再次使用相同的特征吗?或者“它的深度与分裂的数量有关吗?”等等。在图像上,你可以看到整体结构。插图和标题位于相应框的左侧。注意:在编写本文时,scikit learn文档中有12个参数。我们将集中讲解其中6个。此外,单词hyperparameter和parameter可以互换使用。在scikit上实现的决策树是使用CART算法的优化版本。该算法是询问有关数据的问题,将其分为两组。这一点非常重要,因为其他实现可以同时将数据分成两个以上的组。这种情况会反复发生,直到某个参数停止这个过程,或者当数据不能再被分割时(1个示例)。在第一个问题之前,我们拥有所有数据的地方称为“根”。之后,如果每个样本子集后面有一个分割,则它就是一个“节点”。如果这个节点不能被拆分,我们称之为“叶子”。
决策树是KAGLE比赛和实际生活中表现最好的算法的基础。我以前没有仔细学习决策树,后来我决定开始查看每个超参数的作用。仔细研究之后,我脑海中的迷雾慢慢消失了。
动机学习过程不是线性的。如果你想训练一个随机森林模型,你最好学习什么是随机森林,然后再去尝试它。但在进行第一次模型训练之后,你会意识到可能出现的一些问题。
教程对于本教程,你不需要任何关于决策树的先验知识。算法将建立在超参数的基础上。我们可以一举两得:学习决策树的工作原理以及超参数如何修改它们的行为。
为了便于交谈,我想说明scikit learn实现中的参数。其理念是将直觉和数学结合起来。
注意:在编写本文时,scikit learn文档中有12个参数。我们将集中讲解其中6个。此外,单词hyperparameter和parameter可以互换使用。
决策树在scikit上实现的决策树是使用CART算法的优化版本。该算法是询问有关数据的问题,将其分为两组。这一点非常重要,因为其他实现可以同时将数据分成两个以上的组。
这种情况会反复发生,直到某个参数停止这个过程,或者当数据不能再被分割时(1个示例)。在第一个问题之前,我们拥有所有数据的地方称为“根”。之后,如果每个样本子集后面有一个分割,则它就是一个“节点”。如果这个节点不能被拆分,我们称之为“叶子”。
在图像上,你可以看到整体结构。插图和标题位于相应框的左侧。
你可能会有一些问题,比如:“如何决定分割的变量?如何选择分割的阈值?“,”我可以再次使用相同的特征吗?或者“它的深度与分裂的数量有关吗?”等等。
本文的目标是回答这些问题。在每一个(超)参数上,你都会发现新的知识点,帮助你真正了解树是如何生长的。
Criterion这个参数并不能“改变游戏规则”,但却能够有效地理解决策树的基本机制。俗话说,“让我们从头开始”。
正如我们所看到的,在每一次迭代中,都必须确定将选择哪个特征来向数据提问。因此,应该有一种方法来对不同的选项进行排序。
我们将要回顾的方法有两个:基尼和熵。
基尼:Wikipedia的定义——“基尼不纯度是一种度量,如果根据子集中标签的分布随机标记,那么从集合中随机选择的元素被错误标记的频率”。
为了更好地解释这个概念,我们将使用我能想到的最原始的例子:球。想象一个例子,我们有5个球,我们知道这些球的重量,它们可以是大的,也可以是小的,也可以是红的或绿的。
目标是根据球的重量和大小来预测球的颜色。现在是我们应该开始使用我们的基尼度量来选择是否根据球的重量或大小来分割数据。
在数学公式之前,我们试着弄清楚这个度量的作用。我们在“根节点”中复制这个特殊情况下的基尼不纯度定义。首先需要随机选择一个元素,然后,需要按照原始分布对它进行标记。
在这个例子中,我们选择了4公斤重的绿色大球,应该将它标记为红色,概率为0.6,在剩余的0.4中标记为绿色。问题是不能只用一个例子来计算它。我们需要计算更多例子才会更加准确。
一个可能的解决方案是模拟这个过程并重复足够次来得到一个好的估数。
可以用下面的代码来尝试。
importnumpyasnp
mislabeled_cases=0
iterations=1000000
foriinrange(iterations):
color=None
label=None
selection=np.random.rand()
ifselection<0.4:
color="green"
else:
color="red"
labeling=np.random.rand()
iflabeling<0.4:
label="green"
else:
label="red"
ifcolor==label:
pass
else:
mislabeled_cases=mislabeled_cases 1
print(mislabeled_cases/iterations)
你可能会得到类似的数字。这是一个样本错误标记的概率,我们只使用了目标值的分布。
每次迭代都需要计算,这样做可能有点慢,别怕,我们可以做得更好。如果回到流程图,我们可以使用概率来创建一个确定性公式。
可以看到,出错的概率等于选红色绿色样本的概率加上相反情况的概率之和。
太棒了,我们只计算了第一个基尼不纯度!第一步是选择选择第一类的概率(P(Class1)),然后将其乘以错误标记的概率,即(1-P(Class1))。正如我们在例子中看到的,我们必须对每个类求和。
现在可以说,我们真正理解了如何计算基尼不纯度。在看到所有这些信息后,我们可以断言这是一种尝试,以衡量我们的节点将有多少错误标记。算法将选择使其最小化的变量。
离做出第一个决定越来越近了,但我们还得解决其他一些小问题。首先,如何处理不同的变量类型?
离散变量必须是“one-hot编码”这意味着将类别转换成二元变量。因此,分割就非常直观了。对于连续变量则稍有不同:阈值的选择应进行优化,以创建可能的最佳分割。
为了向你展示为一个连续的特征选择精确的分割数的重要性,我将向你展示基于重量的两个可能的分割。你将看到使用同一个变量选择“好”或“坏”拆分之间的显著差异。我们可以用3.5公斤把球分开,然后计算基尼。
我们已经学习了如何计算一个特定的拆分,但是当有两个节点的拆分时该怎么做(当我们想在拆分之间进行考虑时总是这样)。正如你在图中看到的,我们必须根据属于每个节点的样本对它们进行加权。现在让我们试着以2.5公斤作为分割标准。
看上去更好,客观上也更好。算法选择最佳的基尼不纯度来进行分割,现在我们知道如何比较它们。如果你对刚刚看到的还是有点怀疑的话,可以调用scikit learn。
importpandasaspd
fromsklearn.treeimportDecisionTreeClassifier
fromsklearn.model_selectionimporttrain_test_split
fromsklearn.treeimportplot_tree
importmatplotlib.pyplotasplt
df=pd.DataFrame(
[
["small" 1 "red"]
["big" 2 "red"]
["big" 4 "green"]
["small" 4 "red"]
["big" 3 "green"]
]
columns=["size" "weight" "color"]
)
df=pd.get_dummies(df columns=["size" "color"] drop_first=True)
model=DecisionTreeClassifier(max_depth=1 random_state=42)
model.fit(df[["weight" "size_small"]] df["color_red"])
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
可视化显示的是所使用的变量;“X[0]”即本例的重量。它还显示每个节点的基尼值、样本数,并和“value”,你可以看到类的分布(左边是绿色,右侧是红色)。一切都如我们所料。
我将向你展示最后一件有趣的事情。你知道,我们还有另一个特征:大小。这是一个二元变量,因此很容易确定要选择的阈值(0到1之间的任何值)。
哇!大小和重量一样好。那么,scikit为什么选择后者呢?。别担心。如果有一个平局,它将作出随机选择,这就是为什么“随机状态”变得相关。这个参数控制这些决策的随机性。把参数从42改为40就可以了。
model=DecisionTreeClassifier(max_depth=1 random_state=40)
model.fit(df[["weight" "size_small"]] df["color_red"])
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
“X[1]”表示重量,阈值显然设置为0.5。
我们可以结束基尼标准的讨论,让位于下一个客人:熵
熵:如果基尼不纯度试图测量节点的错误标记程度,熵将试图估计结果的不确定性。
熵背后的历史是奇妙的,从热力学开始,发展到信息理论,在决策树上到达我们这里。
对于我们的初始水平,我们知道红球和绿球的概率分别是0.6和0.4,可以计算这种情况下的熵。
importnumpyasnp
Entropy=0
foriin[0.4 0.6]:
Entropy=Entropy-np.log2(i)*i
print(Entropy)
你可以尝试自己的组合,如[1 0]或[0.5 0.5]。对于第一个,熵是0,零不确定性,因为完全确定了。0.97代表非常接近完全不确定的情况。
如果将criterian参数改为“entropy”,scikit将在可视化显示上显示它。
model=DecisionTreeClassifier(max_depth=1 random_state=40 criterion="entropy")
model.fit(df[["weight" "size_small"]] df["color_red"])
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
总而言之,我们可以比较5个球的基尼和熵的波动情况。下面“从头开始计算”两个不同的可能参数。
importnumpyasnp
importmatplotlib.pyplotasplt
importseabornassns
sns.set(rc={"figure.figsize":(13 7.5)} font_scale=1.5)
Entropy_points=np.array([]).reshape(-1 2)
Gini_points=np.array([]).reshape(-1 2)
foriin[[0 1] [0.2 0.8] [0.4 0.6] [0.6 0.4] [0.8 0.2] [1 0]]:
Entropy=0
Gini=0
forjini:
ifj>0:
Entropy=Entropy-np.log2(j)*j
else:
pass
Gini=Gini j*(1-j)
Entropy_points=np.vstack([Entropy_points np.array([i[0] Entropy])])
Gini_points=np.vstack([Gini_points np.array([i[0] Gini])])
sns.scatterplot(
x=Gini_points[: 0] y=Gini_points[: 1] s=150 alpha=0.5 label="Gini"
)
sns.scatterplot(
x=Entropy_points[: 0] y=Entropy_points[: 1] s=150 alpha=0.5 label="Entropy"
)
plt.title("GinivsEntropy")
plt.ylabel("Value")
plt.xlabel("Redballprobability")
在最后一节中,我们假设算法会在每次有连续变量时寻找最佳分割。但是“Splitter”在这个假设上给了我们一些灵活性。有两种选择:“best”和“random”。默认参数为“best”。
当选择“best”时,不出所料,算法将选择最佳分割-就像上一节-,但当选择“random”时,分割将只是一个随机数。为了清楚地说明这一点,我将展示之前使用的相同示例,但这次只使用重量作为解释变量。
fromsklearn.treeimportDecisionTreeClassifier
fromsklearn.model_selectionimporttrain_test_split
fromsklearn.treeimportplot_tree
importmatplotlib.pyplotasplt
model=DecisionTreeClassifier(max_depth=1
random_state=40 splitter='random')
model.fit(df[['weight']] df['color_red'])
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
正如你所看到的,这个阈值被设置为3.975,这给了我们一个不好的分裂。
重要的是,这种“糟糕”的分裂并不总是坏事。现在你知道算法在每次迭代中都会默认地寻找最佳分割,但这不一定是找到全局最优的最佳方法。使用随机拆分可能会导致一些变化,这有时可以帮助我们找到更好的树。
Max_depth为了理解“max_depth是如何工作的,我们必须查看树的内部结构。max depth深度顾名思义是树的深度而不是枝干的数量。
如果你认为从另一个节点接收箭头的节点是子节点,那么将max_depth设置为1将允许根节点在顶部成为母节点。如果我们将参数设置为2,根节点就成为祖母节点。
对于这一节和下面的部分,我们将使用一个新的例子。想象一个简单的分类案例,其中只有一个特征(重量单位为盎司)和四个类别(球的类型)。
为了让这更简单,我们假设它们之间有一个直接关系:当变量在特定范围内时,它将始终属于同一个类(如上图)。
importpandasaspd
importnumpyasnp
df=pd.DataFrame()
df['target']=np.zeros((1000))
df['target'].iloc[250:500]=np.zeros((250)) 1
df['target'].iloc[500:750]=np.zeros((250)) 2
df['target'].iloc[750:1000]=np.zeros((250)) 3
df['feature']=0
df['feature'].iloc[250:500]=np.random.uniform(0 3 250)
df['feature'].iloc[250:500]=np.random.uniform(6 9 250)
df['feature'].iloc[500:750]=np.random.uniform(12 15 250)
df['feature'].iloc[750:1000]=np.random.uniform(18 21 250)
如代码片段所示,如果特征介于0和3之间,则类将始终是一个高尔夫球,其他情况下也会出现相同的行为。我们来了解“max_depth”是如何影响模型的。用“max_depth”为1训练第一个。
fromsklearn.treeimportDecisionTreeClassifier
fromsklearn.model_selectionimporttrain_test_split
fromsklearn.treeimportplot_tree
importmatplotlib.pyplotasplt
X_train X_test y_train y_test=train_test_split(np.array(df['feature']).reshape(-1 1)
np.array(df['target']).reshape(-1 1) test_size=0.2
random_state=42 stratify=df['target'])
model=DecisionTreeClassifier(max_depth=1 random_state=42)
model.fit(X_train y_train)
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
模型确定了最佳阈值(10.495)。左侧的类属于0或1,右侧的类属于2和3。模型没有机会创建另一个步骤来真正分离它们。所以让我们试试“max_depth”为2,看看发生了什么。
model=DecisionTreeClassifier(max_depth=2 random_state=42)
model.fit(X_train y_train)
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
需要注意的第一件重要事情是,变量是可以重复的,因此使用与特征数量相同大小的“max_depth”并不一定涵盖所有基本特征。如果一个变量在特定场景中是最好的分割,那么它可以独立于第一次出现时出现。
在这种情况下,使用“max_depth”为2就足够完美地分离每个分支了。但是没有经验法则来决定这棵树应该有多深。我们使用完全相同的情况,但将测试集所占比例从0.2更改为0.33。
X_train X_test y_train y_test=train_test_split(np.array(df['feature']).reshape(-1 1)
np.array(df['target']).reshape(-1 1) test_size=0.33 random_state=42 stratify=df['target'])
model=DecisionTreeClassifier(max_depth=2 random_state=42)
model.fit(X_train y_train)
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
在这种情况下,将“max_depth”设置为2不足以区分每个案例。这是因为第一次分裂发生在一个更大的阈值上。在这种情况下,使用“max_depth”为3就可以了。
model=DecisionTreeClassifier(max_depth=3 random_state=42)
model.fit(X_train y_train)
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=18)
接下来是第二件要学习的重要事情,数据结构的细微变化可能会导致决策树的不同深度,即使只有一个变量。这是很关键的,因为如果变量有很多信息,我们必须覆盖更广的范围。
欠拟合是指树不够复杂,无法捕捉变量之间的真实关系。例如,当我们将“max_depth”设置为1时,模型就不知道如何区分棒球和高尔夫球。
过度合是指当模型变得如此复杂以至于它开始捕捉噪声而不是理解数据的真实底层结构时。我们的数据没有任何噪音。因此,不可能过拟合。一般来说,数据中有很多噪音,所以“max_depth”设置的很大可能会导致过拟合。
Min_impurity_decreasemin_impurity_decrease是一种控制分裂是否值得的方法。有时,如果我们继续分裂节点,弊大于利。如果我们不使用这个参数,模型可能会过拟合数据。
想象一下,当我们称量球的时候,错误地,称重的是三个棒球而不是一个。数据存储为21.1,类设置为1。让我们回到例子,并且不设置“min_impurity_decrease”。
importpandasaspd
importnumpyasnp
fromsklearn.treeimportDecisionTreeClassifier
fromsklearn.model_selectionimporttrain_test_split
fromsklearn.treeimportplot_tree
importmatplotlib.pyplotasplt
df['target']=np.zeros((1000))
df['target'].iloc[250:499]=np.zeros((249)) 1
df['target'].iloc[499:749]=np.zeros((250)) 2
df['target'].iloc[749:999]=np.zeros((250)) 3
df['target'].iloc[999]=1
df['feature']=0
df['feature'].iloc[0:250]=np.random.uniform(0 3 250)
df['feature'].iloc[250:499]=np.random.uniform(6 9 249)
df['feature'].iloc[499:749]=np.random.uniform(12 15 250)
df['feature'].iloc[749:999]=np.random.uniform(18 21 250)
df['feature'].iloc[999]=21.1
X_train X_test y_train y_test=train_test_split(np.array(df['feature']).reshape(-1 1)
np.array(df['target']).reshape(-1 1) test_size=0.2
random_state=40 stratify=df['target'])
model=DecisionTreeClassifier(random_state=42)
model.fit(X_train y_train)
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=14)
现在的模型认为,每超过21.024的重量,它就是一个棒球。如果你仔细想想,这肯定是个错误,也许一个新的篮球比21盎司重一点,但模型会错误地预测它是棒球。
但“min_impurity_decrease”可以挽救。
如你所见,每个节点都有自己的基尼值。这个参数允许我们选择分割值是值得还是不值得。
在最后一个例子中,问题节点的基尼值是0.01,这对于进行分割来说似乎非常小。如果我们将这个参数设置为0.01或更高,将限制分割的值,解决过拟合问题。
model=DecisionTreeClassifier(random_state=42 min_impurity_decrease=0.01)
model.fit(X_train y_train)
plt.figure(figsize=(15 8))
plot_tree(model filled=True fontsize=14)
随着新的变化,模型接受了节点上的一些杂质,但我们知道,回报是它可以有效地忽略添加到数据中的随机噪声。新树能够通过上一棵树无法通过的测试。
过拟合和欠拟合之间的权衡可能看起来像似曾相识,设置一个既不高也不低的值,与我们希望通过“max_depth”实现的平衡相同,而且在最后一个超参数“min_samples_split”上也是一样的。
min_samples_split“min_samples_split”更多地取决于初始样本的数量,而不是最后一个样本。此参数可以设置为整数,如果是整数,则如果该参数小于节点的采样数,则树将决定继续进行拆分。否则,节点将变为叶子。
如果给定了一个小数,它将用这个小数乘以根号下的样本总数,这将是算法要设置的数字。对于最后一个示例,我们可以看到最后一个节点被分割为399个示例。
今天的重点是:
- 我们看到了决策树的结构,知道了“根”、“节点”和“叶”是什么;
- 我们了解到,“criteria”超参数定义了分割是“好”还是“坏”;
- 我们知道随机性是过程的一部分,看到了“splitter”如何为模型添加一些可变性,而“随机状态”可以给我们提供可重复的条件;
- 我们用“max_depth”、“min_impurity_decrease”和“min_samples_split”处理欠拟合和过拟合。
[1] Scikit-learn decision trees documentation:https://scikit-learn.org/stable/modules/tree.html. [2] Stackoverflow post.:https://stackoverflow.com/questions/46756606/what-does-splitter-attribute-in-sklearns-decisiontreeclassifier-do/46759065