Bootstrap

决策树和梯度提升的视觉理解

决策树是一种非参数监督学习算法,可用于分类和回归。它使用树状结构来表示决策及其潜在结果。决策树易于理解和解释,并且易于可视化。但是,当决策树模型变得过于复杂时,它无法很好地从训练数据中概括出来,从而导致过度拟合。

梯度提升是一种集成学习模型,我们将许多弱学习器组合起来以开发强学习器。弱学习器是单独的决策树,每个学习器都试图关注先前学习器的错误。与单个深度决策树相比,梯度提升通常不太容易过度拟合。

本文将直观地解释分类和回归问题中决策树背后的直觉。我们将了解该模型的工作原理以及它为什么会导致过度拟合。接下来,我们将介绍梯度提升,并了解它如何提高单个决策树的性能。我们将用 Python 从头开始​​实现梯度提升回归器和分类器。最后,彻底解释梯度提升背后的数学原理。

所有未注明来源的图片均由作者创作。

决策树分类器

决策树模型既可用于分类问题,也可用于回归问题。让我们看看决策树分类器是如何工作的。首先,我们创建一个玩具数据集。该数据集与二元分类问题相关。我们有 1160 个 数据点,每个数据点有两个特征(x ₁、x ₂),以及一个带有两个标签(y =0、y =1)的二元目标。数据点是从均匀分布中随机选择的,但y =0 和y =1的数据点之间存在一条边界,看起来像一条圆弧。清单 1 创建了此数据集并将其绘制在图 1 中。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400"># 清单 1</span>

<span style="color:#aa0d91">导入</span>pandas<span style="color:#aa0d91">作为</span>pd
<span style="color:#aa0d91">导入</span>numpy<span style="color:#aa0d91">作为</span>np
<span style="color:#aa0d91">导入</span>matplotlib.pyplot<span style="color:#aa0d91">作为</span>plt

<span style="color:#aa0d91">从</span>sklearn.tree<span style="color:#aa0d91">导入</span>DecisionTreeClassifier
<span style="color:#aa0d91">从</span>sklearn.tree<span style="color:#aa0d91">导入</span>DecisionTreeRegressor
<span style="color:#aa0d91">从</span>sklearn<span style="color:#aa0d91">导入</span>tree
<span style="color:#aa0d91">从</span>matplotlib.colors<span style="color:#aa0d91">导入</span>ListedColormap 

np.random.seed( <span style="color:#1c00cf">7</span> )   
low_r = <span style="color:#1c00cf">10</span>  
 high_r = <span style="color:#1c00cf">15</span>
 n = <span style="color:#1c00cf">1550</span>
 X = np.random.uniform(low=[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ], high=[ <span style="color:#1c00cf">4</span> , <span style="color:#1c00cf">4</span> ], size=(n, <span style="color:#1c00cf">2</span> )) 
drop = (X[:, <span style="color:#1c00cf">0</span> ]** <span style="color:#1c00cf">2</span> + X[:, <span style="color:#1c00cf">1</span> ]** <span style="color:#1c00cf">2</span> > low_r) & (X[:, <span style="color:#1c00cf">0</span> ]** <span style="color:#1c00cf">2</span> + X[:, <span style="color:#1c00cf">1</span> ]** <span style="color:#1c00cf">2</span> < high_r) 
X = X[~drop] 
y = (X[:, <span style="color:#1c00cf">0</span> ]** <span style="color:#1c00cf">2</span> + X[:, <span style="color:#1c00cf">1</span> ]** <span style="color:#1c00cf">2</span> >= high_r).astype( <span style="color:#5c2699">int</span> ) 
colors = [ <span style="color:#c41a16">'red'</span> , <span style="color:#c41a16">'blue'</span> ] 
plt.figure(figsize=( <span style="color:#1c00cf">6</span> , <span style="color:#1c00cf">6</span> )) 
<span style="color:#aa0d91">for</span> i <span style="color:#aa0d91">in</span> np.unique(y): 
    plt.scatter(X[y==i, <span style="color:#1c00cf">0</span> ], X[y==i, <span style="color:#1c00cf">1</span> ], label = <span style="color:#c41a16">"y="</span> + <span style="color:#5c2699">str</span> (i), 
                color=colors[i], edgecolor= <span style="color:#c41a16">"white"</span> , s= <span style="color:#1c00cf">50</span> ) 
circle = plt.Circle(( <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ), <span style="color:#1c00cf">3.5</span> , color= <span style="color:#c41a16">'black'</span> , fill= <span style="color:#aa0d91">False</span> , 
                    linestyle= <span style="color:#c41a16">"--"</span> , label= <span style="color:#c41a16">"实际边界"</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">4.2</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">5</span> ]) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
ax.add_patch(circle) 
plt.xlabel( <span style="color:#c41a16">'$x_1$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.legend(loc= <span style="color:#c41a16">'best'</span> , fontsize= <span style="color:#1c00cf">11</span> ) 

plt.show()</span></span></span></span>

图 1

接下来,我们使用 Scikit-learn 库在此数据集上创建和训练决策树分类器。拟合模型后,我们可以使用该函数可视化决策树plot_tree()。生成的树如图 2 所示。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 2</span>

 tree_clf = DecisionTreeClassifier(random_state= <span style="color:#1c00cf">0</span> )   
tree_clf.fit(X, y) 
plt.figure(figsize=( <span style="color:#1c00cf">17</span> , <span style="color:#1c00cf">12</span> )) 
tree.plot_tree(tree_clf, fontsize= <span style="color:#1c00cf">17</span> , feature_names=[ <span style="color:#c41a16">"x1"</span> , <span style="color:#c41a16">"x2"</span> ]) 
plt.show()</span></span></span></span>

图 2

现在让我们更深入地看看这棵树。决策树是一种分层结构。它是通过边连接的节点的集合。决策树的最顶层节点称为根是其起点。根连接到较低级别的两个节点。这两个节点称为根的子节点,即左子节点右子节点。这些节点也各有两个子节点。最低级别没有子节点的节点称为树的叶子。节点的深度是指从节点到树的根节点的边数。因此,决策树的最大深度是其最深叶子的深度。

该树用于对数据点进行分类。我们将一个数据点(来自训练数据集或看不见的数据点)传递给它,它将确定其标签(y = 0 或y = 1)。这是通过将数据点的特征值传递给决策树来完成的。假设我们想使用图 2 中的决策树确定数据点(x ₁,x ₂)的标签。让我们仔细看看根。根将特征x ₂ 的值与阈值(此处为 2.73)进行比较。如果x ₂≤2.73,我们转到左孩子。否则,我们转到左孩子。因此,根通过使用阈值定义的直线x ₂=2.73 将特征空间分为两个区域。一个区域有x ₂≤2.73,另一个区域有x ₂>2.73。如果我们将根视为一个简单的分类器,则这条线充当决策边界。如图 3 所示。

图 3

理想情况下,决策边界应该将所有具有不同标签的数据点分开。因此,在由决策边界产生的每个区域中,我们应该只有具有相同标签的数据点。如图 3 所示,根线不是我们玩具数据集的良好分类器,因为每个区域中都有y =0 和y =1 的数据点混合。因此,决策树应该添加更多节点以提高分类的准确性。

让我们看看添加更多节点时会发生什么。如果我们将根的左子节点和右子节点添加到树中,则它看起来像图 4。根将原始空间分成两个区域。每个区域都传递给其中一个子节点。每个子节点都有一个阈值,将相应区域分成两个新区域(图 4)。

图 4

现在,我们可以添加更多节点并使树更深。添加新节点时,它会获取从其父节点传递的区域,并使用垂直或水平线将其拆分为两个新区域(此线表示该节点的决策边界)。我们可以添加更多节点,直到生成的区域是纯净的,这意味着所有区域仅包含具有相同标签的数据点。清单 3 中的递归函数绘制了决策树中所有节点的决策边界。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 3 </span>

<span style="color:#aa0d91">def </span> plot_boundary_lines ( <span style="color:#5c2699">tree_model</span> ): 
    <span style="color:#aa0d91">def </span> helper ( <span style="color:#5c2699">node, x1_min, x1_max, x2_min, x2_max</span> ):
        <span style="color:#aa0d91">如果</span>feature[node] == <span style="color:#1c00cf">0</span> : 
            plt.plot([threshold[node], Threshold[node]], 
                     [x2_min, x2_max], color= <span style="color:#c41a16">"black"</span> )
        <span style="color:#aa0d91">如果</span>feature[node] == <span style="color:#1c00cf">1</span> : 
            plt.plot([x1_min, x1_max], [threshold[node], 
                                        Threshold[node]], color= <span style="color:#c41a16">"black"</span> )
        <span style="color:#aa0d91">如果</span>children_left[node] != children_right[node]:
            <span style="color:#aa0d91">如果</span>feature[node] == <span style="color:#1c00cf">0</span> : 
                    helper(children_left[node], x1_min, 
                           Threshold[node], x2_min, x2_max) 
                    helper(children_right[node], Threshold[node], 
                           x1_max, x2_min, x2_max) 
            <span style="color:#aa0d91">else</span> : 
                    helper(children_left[node], x1_min,x1_max,
                           x2_min,阈值[节点])
                    辅助函数(children_right[节点],x1_min,x1_max,
                           阈值[节点],x2_max)
    特征 = tree_model.tree_.feature
    阈值 = tree_model.tree_.threshold 
    children_left = tree_model.tree_.children_left 
    children_right = tree_model.tree_.children_right 

    x1_min = x2_min = - <span style="color:#1c00cf">1</span>
     x1_max = x2_max = <span style="color:#1c00cf">5</span>
    辅助函数(<span style="color:#1c00cf">0</span>,x1_min,x1_max,x2_min,x2_max)</span></span></span></span>

在清单 4 中,我们还定义了另一个函数,该函数在 2-D 空间上创建网格,并获取该网格上每个点的训练决策树的预测。它为预测标签为 1 ( y ^=1 ) 的点分配浅蓝色,为预测标签为 0 ( y ^=0 ) 的点分配橙色。使用此函数,我们可以在 2-D 图中看到决策树对所有点的预测。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 4 </span>

<span style="color:#aa0d91">def </span> plot_boundary ( <span style="color:#5c2699">X, y, clf, lims</span> ): 
    gx1, gx2 = np.meshgrid(np.arange(lims[ <span style="color:#1c00cf">0</span> ], lims[ <span style="color:#1c00cf">1</span> ], 
                                     (lims[ <span style="color:#1c00cf">1</span> ]-lims[ <span style="color:#1c00cf">0</span> ])/ <span style="color:#1c00cf">300.0</span> ), 
                           np.arange(lims[ <span style="color:#1c00cf">2</span> ], lims[ <span style="color:#1c00cf">3</span> ], 
                                     (lims[ <span style="color:#1c00cf">3</span> ]-lims[ <span style="color:#1c00cf">2</span> ])/ <span style="color:#1c00cf">300.0</span> )) 
    cmap_light = ListedColormap([ <span style="color:#c41a16">'lightsalmon'</span> , <span style="color:#c41a16">'aqua'</span> ]) 
    gx1l = gx1.flatten() 
    gx2l = gx2.flatten() 
    gx = np.vstack((gx1l,gx2l)).T 
    gyhat = clf.predict(gx) 
    gyhat = gyhat.reshape(gx1.shape) 

    plt.pcolormesh(gx1, gx2, gyhat, cmap=cmap_light) 
    plt.scatter(X[y== <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ], X[y== <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ], 标签= <span style="color:#c41a16">"y=0"</span> , alpha= <span style="color:#1c00cf">0.7</span> , 
                color= <span style="color:#c41a16">"红色"</span> , 边缘颜色= <span style="color:#c41a16">"白色"</span> , s= <span style="color:#1c00cf">50</span> ) 
    plt.scatter(X[y== <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">0</span> ], X[y== <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">1</span> ], 标签= <span style="color:#c41a16">"y=1"</span> , alpha= <span style="color:#1c00cf">0.7</span> , 
                color= <span style="color:#c41a16">"蓝色"</span> , 边缘颜色= <span style="color:#c41a16">"白色"</span> , s= <span style="color:#1c00cf">50</span> ) 
    plt.legend(loc= <span style="color:#c41a16">'左上'</span> )</span></span></span></span>

现在,我们可以将它用于我们在玩具数据集上训练的决策树(图 2 中显示的树)。图 5 显示了结果。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 5</span>

 plt.figure(figsize=( <span style="color:#1c00cf">6</span> , <span style="color:#1c00cf">6</span> )) 
plot_boundary(X, y, tree_clf, lims=[- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> , - <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
plot_boundary_lines(tree_clf) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.xlim([- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
plt.ylim([- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
plt.xlabel( <span style="color:#c41a16">'$x_1$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.show()</span></span></span></span>

图 5

如您所见,原始数据集被分成 9 个矩形。这是因为我们的决策树中有 9 个叶子(图 2)。每个矩形代表这棵树中的一片叶子。这些矩形创建纯区域。每个区域中的数据点具有相同的标签,并且此标签分配给相应的叶子。当我们将测试点提供给这棵经过训练的决策树时,它首先确定该点属于哪个叶子,然后将叶子的标签分配给该点。换句话说,数据点的预测标签只是该点所在区域(或矩形)的标签。

每条水平线或垂直线代表该树中一个节点的阈值。这些线可以组合起来以创建树的总决策边界,如图 6 所示。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 6</span>

 plt.figure(figsize=( <span style="color:#1c00cf">6</span> , <span style="color:#1c00cf">6</span> )) 
plot_boundary(X, y, tree_clf, lims=[- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> , - <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
circle = plt.Circle(( <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ), <span style="color:#1c00cf">3.5</span> , color= <span style="color:#c41a16">'black'</span> , fill= <span style="color:#aa0d91">False</span> , 
                    linestyle= <span style="color:#c41a16">"--"</span> , label= <span style="color:#c41a16">"实际边界"</span> ) 
plt.text( <span style="color:#1c00cf">3.5</span> , <span style="color:#1c00cf">4.5</span> , <span style="color:#c41a16">r"$\hat{y}=1$"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
plt.text( <span style="color:#1c00cf">2.35</span> , <span style="color:#1c00cf">2.1</span> , <span style="color:#c41a16">r"$\hat{y}=0$"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
ax.add_patch(circle) 
plt.xlabel( <span style="color:#c41a16">'$x_1$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">4.2</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">5</span> ]) 
plt.legend(loc= <span style="color:#c41a16">'左上'</span> ) 

plt.show()</span></span></span></span>

图 6

该图还显示了训练数据集的实际边界,即一条弧线。值得注意的是决策树分类器如何使用垂直线和水平线的组合来估计这条弧线。

到目前为止,我们只研究了具有两个特征的数据集,但相同的想法可以应用于具有更高维度的数据集。如果我们有 3 个特征,则每个节点获取从其父节点传递的区域,并使用与其中一个轴(x ₁、x ₂ 或x ₃)平行的平面将其拆分为两个新区域。当这些平面组合在一起时,它们会创建树的决策边界。通常,对于具有n 个特征的数据集,每个节点使用其阈值创建一个超平面,这些超平面组合在一起形成决策树分类器的决策边界。

决策树分类器的过度拟合

在二元分类问题中,我们可以假设具有不同标签的数据点由假设边界分隔。此边界由生成数据的过程创建。在我们的玩具数据集中,此边界是一条弧线。决策树分类器是一种强大的机器学习模型,理论上,它可以添加尽可能多的节点来解决任何非线性分类问题。在二维空间中,无论实际边界有多复杂,都可以通过添加更多水平和垂直线来近似(图 7)。这个阶梯式边界就像决策树的签名。

图 7

同样的情况也适用于n维,我们可以在其中添加越来越多的超平面来模拟边界。然而,这种稳健的模型有一个明显的缺点:过度拟合。当机器学习模型变得过于复杂并开始学习训练数据的噪声时,就会发生过度拟合。因此,它将无法很好地推广到新的未见过的数据。

当决策树的决策边界变得比原始数据集的实际边界复杂得多时,就会发生过度拟合。下面是一个例子。假设我们有一个嘈杂的数据集,其中一条直线是具有不同标签的数据点的边界。此数据集在清单 7 中定义,并在图 8 中绘制。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 7</span>

 np.random.seed( <span style="color:#1c00cf">1</span> ) 
n = <span style="color:#1c00cf">550</span>
 X1 = np.random.uniform(low=[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ], high=[ <span style="color:#1c00cf">4</span> , <span style="color:#1c00cf">4</span> ], size=(n, <span style="color:#1c00cf">2</span> )) 
drop = (X1[:, <span style="color:#1c00cf">0</span> ] > <span style="color:#1c00cf">1.8</span> ) & (X1[:, <span style="color:#1c00cf">0</span> ] < <span style="color:#1c00cf">1.9</span> ) 
X1 = X1[~drop] 
y1 = (X1[:, <span style="color:#1c00cf">0</span> ] > <span style="color:#1c00cf">1.9</span> ).astype( <span style="color:#5c2699">int</span> ) 
X2 = np.random.uniform(low=[ <span style="color:#1c00cf">1.7</span> , <span style="color:#1c00cf">0</span> ], high=[ <span style="color:#1c00cf">1.9</span> , <span style="color:#1c00cf">4</span> ], size=( <span style="color:#1c00cf">15</span> , <span style="color:#1c00cf">2</span> ))   
y2 = np.ones( <span style="color:#1c00cf">15</span> ).astype( <span style="color:#5c2699">int</span> ) 
X = np.concatenate((X1, X2), axis= <span style="color:#1c00cf">0</span>)
y = np.concatenate((y1,y2))
colors = [ <span style="color:#c41a16">'red'</span> , <span style="color:#c41a16">'blue'</span> ] 
<span style="color:#aa0d91">for</span> i <span style="color:#aa0d91">in</span> np.unique(y):
    plt.scatter(X[y==i,<span style="color:#1c00cf">0</span> ],X[y==i,<span style="color:#1c00cf">1</span> ],label = <span style="color:#c41a16">“y=”</span> + <span style="color:#5c2699">str</span>(i),
                color=colors[i],edgecolor= <span style="color:#c41a16">“white”</span>,s= <span style="color:#1c00cf">50</span>)
plt.axvline(x= <span style="color:#1c00cf">1.8</span>,color= <span style="color:#c41a16">“black”</span>,linestyle= <span style="color:#c41a16">“--”</span>)
plt.legend(loc= <span style="color:#c41a16">'best'</span>)
plt.xlim([- 0.5,4.5 <span style="color:#1c00cf">]</span>)plt.ylim([- <span style="color:#1c00cf">0.2,5 </span><span style="color:#1c00cf">]</span>)ax = plt.gca()   ax.set_aspect(<span style="color:#c41a16">'equal' </span><span style="color:#1c00cf">)</span> plt.xlabel (' <span style="color:#c41a16">$x_1$'</span>,fontsize= <span style="color:#1c00cf">16</span>)plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> ,字体大小= <span style="color:#1c00cf">16</span> ) plt.show()







</span></span></span></span>

图 8

现在,我们在这个数据集上训练一个决策树分类器。该树如图9所示。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 8</span>

 tree_clf = DecisionTreeClassifier(random_state= <span style="color:#1c00cf">1</span> ) 
tree_clf.fit(X, y) 
plt.figure(figsize=( <span style="color:#1c00cf">13</span> , <span style="color:#1c00cf">10</span> )) 
tree.plot_tree(tree_clf, fontsize= <span style="color:#1c00cf">9</span> , feature_names=[ <span style="color:#c41a16">"x1"</span> , <span style="color:#c41a16">"x2"</span> ]) 
plt.show()</span></span></span></span>

图 9

最后,我们在图 9 中绘制树分类器的决策边界。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 9</span>

 plt.figure(figsize=( <span style="color:#1c00cf">6</span> , <span style="color:#1c00cf">6</span> )) 
plot_boundary(X, y, tree_clf, lims=[- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> , - <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
plt.axvline(x= <span style="color:#1c00cf">1.8</span> , color= <span style="color:#c41a16">"black"</span> , linestyle= <span style="color:#c41a16">"--"</span> , label= <span style="color:#c41a16">"实际边界"</span> ) 
plt.text( <span style="color:#1c00cf">0</span> , - <span style="color:#1c00cf">0.3</span> , <span style="color:#c41a16">r"$\hat{y}=0$"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
plt.text( <span style="color:#1c00cf">3</span> , - <span style="color:#1c00cf">0.3</span> , <span style="color:#c41a16">r"$\hat{y}=1$"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">4.5</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">4.5</span> ]) 
plt.xlabel( <span style="color:#c41a16">'$x_1$'</span> ,字体大小= <span style="color:#1c00cf">16</span> ) 
plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> ,字体大小= <span style="color:#1c00cf">16</span> ) 
plt.legend(loc= <span style="color:#c41a16">“best”</span> ) 

plt.show()</span></span></span></span>

图 10

如您所见,实际边界是一条直线,但适合此数据集的树分类器的决策边界是一条弯曲的线。这是因为模型试图学习嘈杂的数据点,因此它通过添加更多节点来包含它们来扩展决策边界。这是一个明显的过度拟合的例子。我们可以通过限制树的最大深度来降低过度拟合的风险。为此,我们使用max_depth中的参数DecisionTreeClassifier()

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 10</span>

 tree_clf1 = DecisionTreeClassifier(random_state= <span style="color:#1c00cf">1</span> , max_depth= <span style="color:#1c00cf">1</span> ) 
tree_clf1.fit(X, y) 
plt.figure(figsize=( <span style="color:#1c00cf">10</span> , <span style="color:#1c00cf">5</span> )) 
tree.plot_tree(tree_clf1, fontsize= <span style="color:#1c00cf">9</span> , feature_names=[ <span style="color:#c41a16">"x1"</span> , <span style="color:#c41a16">"x2"</span> ]) 
plt.show()</span></span></span></span>

图 11

图 11 显示了生成的树。现在决策树叶的最大深度为 1,因此我们只有带有两片叶子的根节点。这棵新树的决策边界如图 12 所示。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 11</span>

 plt.figure(figsize=( <span style="color:#1c00cf">6</span> , <span style="color:#1c00cf">6</span> )) 
plot_boundary(X, y, tree_clf1, lims=[- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> , - <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
plt.axvline(x= <span style="color:#1c00cf">1.8</span> , color= <span style="color:#c41a16">"black"</span> , linestyle= <span style="color:#c41a16">"--"</span> , label= <span style="color:#c41a16">"实际边界"</span> ) 
plt.text( <span style="color:#1c00cf">0</span> , - <span style="color:#1c00cf">0.3</span> , <span style="color:#c41a16">r"$\hat{y}=0$"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
plt.text( <span style="color:#1c00cf">3</span> , - <span style="color:#1c00cf">0.3</span> , <span style="color:#c41a16">r"$\hat{y}=1$"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">4.5</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">4.5</span> ]) 
plt.xlabel( <span style="color:#c41a16">'$x_1$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.legend(loc= <span style="color:#c41a16">"best"</span> ) 

plt.show()</span></span></span></span>

图 12

由于我们只有一个节点,因此决策边界是一条直线,并且不存在过度拟合。当我们限制决策树的深度时,我们可能不会得到纯粹的叶子区域。在这种情况下,每个叶子的标签由该叶子的相应区域投票决定。例如,在图 12 中,左侧区域的标签为零。这是因为该区域中的大多数训练数据点的标签为零(y =0)。同样,右侧区域的标签为零。

杂质测量和基尼指数

决策树如何确定每个节点应使用哪个特征和阈值?它使用杂质测量来做到这一点。在我们的示例中,使用的杂质测量是基尼指数。基尼指数是样本中杂质的度量。当节点将给定空间分成两个区域时,我们可以使用该区域中数据点的标签计算每个区域的基尼指数。区域的基尼指数定义为:

其中C是所有标签(类)的集合,i遍历每个标签。这里pᵢ是在该区域中选择标签为i 的数据点的概率。在二元分类问题中,如果某个区域中的所有数据点都具有相同的标签(例如标签 0):

基尼指数为 0 表示纯度极高,即某个区域内的所有数据点都具有相同的标签。这是基尼指数可以取的最低值。基尼指数越高,纯度越高。如果某个区域内具有不同标签的数据点数量相等,则基尼指数为:

基尼系数 0.5 表示杂质最多,也是基尼系数的最高值。因此,基尼系数始终在 0 到 0.5 之间。

当决策树中的节点将其空间分成两个区域时,我们可以计算每个所得区域的基尼指数。当决策树算法添加新节点时,它会针对不同的潜在阈值评估每个特征的基尼指数。然后,它选择导致该节点的平均基尼指数最低的特征和阈值(这意味着该节点产生的两个区域的平均纯度最高)。让我们看一个例子。在图 11 所示的决策树中,发送到根节点的原始数据集中有 552 个数据点,其中 247 个的标签为 0,305 个的标签为 1。原始数据集的基尼指数计算如下:

这是根节点的基尼指数。根将初始数据集分成两个区域。在左侧区域中,我们有 254 个数据点:247 个y =0 的点和 7 个y =1的数据点。因此,基尼指数为:

这是左子节点的基尼指数。在右侧区域,我们有 298 个数据点,所有数据点的标签均为 1。因此,右子节点的基尼指数就是 0。接下来,我们计算两个区域的基尼指数的加权和:

因此,根节点处的根分裂将原始基尼系数 0.494 降低到平均基尼系数 0.025(对于两个区域)。根节点中使用的特征 ( x ₁) 和阈值 (1.801) 被选择为在分裂后给出最低平均基尼系数,即 0.025。请注意,scikit-learn 绘制的决策树显示了传递到每个节点的数据点数量、每个标签的数量以及节点的基尼系数。

决策树回归器

如上所述,我们可以将决策树用于分类和回归问题。本节介绍如何创建决策树来解决回归问题。我们使用清单 12 为此目的创建了另一个玩具数据集。该数据集如图 13 所示。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 12</span>

 np.random.seed( <span style="color:#1c00cf">4</span> ) 
x = np.linspace( <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">3</span> , <span style="color:#1c00cf">60</span> ) 
x1 = np.linspace( <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">20</span> ) 
x2 = np.linspace( <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">3</span> , <span style="color:#1c00cf">40</span> ) 
y = x.copy() 
y[x>= <span style="color:#1c00cf">1</span> ] = <span style="color:#1c00cf">1</span> 
 y = y + np.random.normal(scale= <span style="color:#1c00cf">0.1</span> , size= <span style="color:#1c00cf">60</span> ) 
X = x.reshape(- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">1</span> ) 

plt.figure(figsize=( <span style="color:#1c00cf">8</span> , <span style="color:#1c00cf">8</span> )) 
plt.scatter(x, y, label= <span style="color:#c41a16">"噪声数据点"</span> ) 
plt.plot(x1, x1, color= <span style="color:#c41a16">"蓝色"</span> , alpha= <span style="color:#1c00cf">0.5</span> , label= <span style="color:#c41a16">"趋势"</span> ) 
plt.plot(x2, <span style="color:#5c2699">len</span> (x2)*[ <span style="color:#1c00cf">1</span> ], color= <span style="color:#c41a16">"blue"</span> , alpha= <span style="color:#1c00cf">0.5</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">3.1</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">2</span> ]) 
plt.xlabel( <span style="color:#c41a16">'$x$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
plt.ylabel( <span style="color:#c41a16">'$y$'</span> , fontsize= <span style="color:#1c00cf">16</span> ) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.legend(loc= <span style="color:#c41a16">"best"</span> , fontsize= <span style="color:#1c00cf">14</span> ) 

plt.show()</span></span></span></span>

图 13

该数据集是通过向两个线段(y = xy =1)上的点添加噪声(具有正态分布)创建的。这里x是此数据集的唯一特征,y是目标。现在我们要使用决策树回归器来学习此数据集。下一个清单创建一个决策树回归器并将其拟合到数据集。该树回归器绘制在图 14 中。请注意,这棵树的最大深度为 3。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 13</span>

 tree_regres = DecisionTreeRegressor(random_state= <span style="color:#1c00cf">0</span> , max_depth= <span style="color:#1c00cf">3</span> ) 
tree_regres.fit(X, y) 
plt.figure(figsize=( <span style="color:#1c00cf">17</span> , <span style="color:#1c00cf">8</span> )) 
tree.plot_tree(tree_regres, fontsize= <span style="color:#1c00cf">10</span> , feature_names=[ <span style="color:#c41a16">"x"</span> ]) 
plt.show()</span></span></span></span>

图 14

让我们看看树回归器是如何工作的。树回归器中的节点与树分类器中的节点类似。每个节点都有一个与阈值进行比较的特征。决策树分类器和决策树回归器之间的主要区别在于叶子值。在树分类器中,数据集的目标是带有一些标签的离散变量,每个叶子代表其中一个标签。然而,在树回归器中,目标是一个连续变量,每个叶子代表该目标的一个可能值。例如,在图 14 中绘制的树回归器中,从左边开始的第一片叶子的值为 0.036。因此,当我们最终到达这片叶子时,目标的预测值将为 0.036。如果我们给这个经过训练的决策树回归器一个测试点,它首先需要确定它属于哪片叶子,然后将叶子的值分配给该点。

我们从根节点开始。数据集有 60 个数据点。目标的平均值是:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">y.平均值()</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">0.828</span></span></span></span>

如果我们将这个平均值用作整个数据集的简单预测因子,其均方误差(MSE)将是:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">((y.平均值()-y)** <span style="color:#1c00cf">2</span> ).平均值()</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">0.102</span></span></span></span>

此信息显示在根节点上。因此,在根节点开始分割数据集之前,y的平均值是我们拥有的最佳估计量。根节点使用其阈值 0.585 分割数据。如图 15 所示。分割后,左侧区域有 12 个数据点(传递到左侧子节点),右侧区域有 48 个数据点(传递到右侧子节点)。

图 15

在每个区域中, y的平均值代表模型预测。例如,在图 15 的左侧区域中,我们只有x ≤0.605 的数据点。这些数据点的目标平均值为:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">y[(X <= <span style="color:#1c00cf">0.585</span> ).flatten()].mean()</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">0.262</span></span></span></span>

如果我们使用这些值作为这些数据点的模型预测,我们可以计算其均方误差(MSE):

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">(( <span style="color:#1c00cf">0.262</span> - y[(X <= <span style="color:#1c00cf">0.585</span> ).flatten()])** <span style="color:#1c00cf">2</span> ).mean()</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">0.037</span></span></span></span>

这些数字在图 15 中的左侧子节点中显示为valuesquared_error。类似地,右侧区域的预测和 MSE 分别为 0.97 和 0.018。图 16 显示了当我们将树的最大深度增加一时发生的情况。添加新节点时,它会获取从其父节点传递的区域并使用其阈值将其拆分为两个新区域。随着树变得更深,节点的 MSE 会减小,并且每个区域中的预测会更接近实际数据点。

图 16

清单 14 绘制了最终树的预测(该树如图 14 所示)。结果绘制在图 17 中。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 14</span>

 x1 = np.linspace( <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">20</span> ) 
x2 = np.linspace( <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">3</span> , <span style="color:#1c00cf">40</span> ) 
X_space = np.linspace(- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> , <span style="color:#1c00cf">1000</span> ).reshape(- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">1</span> ) 
yhat = tree_regres.predict(X_space) 
plt.figure(figsize=( <span style="color:#1c00cf">8</span> , <span style="color:#1c00cf">6</span> )) 
plt.scatter(x, y, label= <span style="color:#c41a16">"训练数据"</span> ) 
plt.plot(X_space, yhat, color= <span style="color:#c41a16">"红色"</span> , label= <span style="color:#c41a16">"预测"</span> ) 
plt.plot(x1, x1, color= <span style="color:#c41a16">"蓝色"</span> , alpha= <span style="color:#1c00cf">0.5</span> , label= <span style="color:#c41a16">"趋势"</span> ) 
plt.plot(x2, <span style="color:#5c2699">len</span> (x2)*[ <span style="color:#1c00cf">1</span> ], color= <span style="color:#c41a16">"蓝色"</span> , alpha= <span style="color:#1c00cf">0.5</span> ) 
plt.legend(loc= <span style="color:#c41a16">"best"</span> , fontsize= <span style="color:#1c00cf">14</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">2</span> ]) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.xlabel( <span style="color:#c41a16">'$x$'</span> , fontsize= <span style="color:#1c00cf">14</span> ) 
plt.ylabel( <span style="color:#c41a16">'$y$'</span> , fontsize= <span style="color:#1c00cf">14</span> ) 
plt.show()</span></span></span></span>

图 17

图 14 中绘制的树有 8 片叶子,图 17 中的绘图包含 8 条水平红线段,每条线段代表一片叶子的预测。当这些线段组合在一起时,它们会形成一条阶梯线,代表整个决策树在给定间隔内的预测。这条阶梯线是对训练数据集实际趋势的估计,是决策树回归器的一个特性。您可能认为,通过增加树的最大深度并添加更多线段(更多节点),阶梯线将更接近实际趋势。不幸的是,这不会发生,增加最大深度只会增加过度拟合的风险。

外推问题

查看前面的一些图,您可能已经注意到决策树的一个问题。玩具数据集定义在区间 [0, 3] 内。由于预测是一条水平线,因此我们对所有小于零的x值都得到一个恒定的预测(图 18)。类似地,我们对所有大于 3 的x值都得到一个恒定的预测。决策树回归器无法推断出训练数据的范围之外。

图 18

请记住,树在每个区域中的预测只是该区域中数据点的目标的平均值。该预测由水平线段表示。树中具有最低阈值的节点会创建一个位于训练数据集边缘的区域。例如,在图 14 所示的树中,此节点是最左侧的节点,x ≤0.127。此节点的左叶在训练数据集的左边缘创建一个区域,其线段延伸到负无穷大。类似地,具有最大阈值的节点会创建一个叶子,其区域位于训练数据集的右边缘,其线段延伸到无穷大。

决策树分类器也存在同样的问题。如图 6 所示,决策边界总是以垂直线或水平线结束,不能采用任何其他形式。外推问题与决策树的结构有关。在决策树中,每个节点都会创建一个简单的预测。在训练数据集间隔内,这些预测可以组合成一个复杂的形式,但是,在这个间隔之外,我们只剩下一个覆盖该区域的节点的简单预测。

决策树回归器的过度拟合

当我们在玩具数据集上训练图 14 所示的决策树回归器时,我们将最大深度设置为 3。让我们看看如果我们移除这个限制会发生什么。清单 15 将一个新的决策树回归器拟合到玩具数据集,这次没有最大深度限制。它在图 19 中绘制了树的预测与原始数据集的关系。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 15</span>

 X_space = np.linspace(- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> , <span style="color:#1c00cf">1000</span> ).reshape(- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">1</span> ) 
tree_regres = DecisionTreeRegressor(random_state= <span style="color:#1c00cf">1</span> ) 
tree_regres.fit(X, y) 
yhat = tree_regres.predict(X_space) 
plt.figure(figsize=( <span style="color:#1c00cf">8</span> , <span style="color:#1c00cf">6</span> )) 
plt.scatter(X, y, label= <span style="color:#c41a16">"训练数据"</span> ) 
plt.plot(X_space, yhat, color= <span style="color:#c41a16">"红色"</span> , label= <span style="color:#c41a16">"预测"</span> ) 

plt.xlim([- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">2</span> ]) 
plt.legend(loc= <span style="color:#c41a16">"最佳"</span> , fontsize= <span style="color:#1c00cf">14</span> ) 
plt.xlabel( <span style="color:#c41a16">'$x$'</span> , fontsize= <span style="color:#1c00cf">14</span>)
plt.ylabel(<span style="color:#c41a16">'$y$'</span>,fontsize= <span style="color:#1c00cf">14</span>)
ax = plt.gca()   
ax.set_aspect(<span style="color:#c41a16">'equal'</span>)
plt.show()</span></span></span></span>

图 19

我们可以使用以下语句显示该树的最大深度:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">tree_regres.tree_.max_depth</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">11</span></span></span></span>

我们还可以打印这棵树的叶子数量:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">tree_regres.get_n_leaves()</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">60</span></span></span></span>

如图 19 所示,这棵树正遭受过度拟合,并且正在学习原始数据集中的噪声。主要问题是树回归器看不到实际趋势。它唯一看到的是原始数据点,当添加新节点时,它们会尝试更接近这些点。最后,叶子的数量将等于数据点的数量。在最后一次分裂之后,每个叶子只剩下一个数据点,并且它的值被简单地返回。因此,在每个叶子中,MSE 为零。事实上,训练数据集的预测误差为零,因此训练数据集的 R² 为 1:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">tree_regres.分数(X,y)</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">1.0</span></span></span></span>

现在,树回归器对训练数据集给出了完美的预测,但是,由于它无法了解训练数据集的实际趋势,因此它无法很好地推广到新的未见数据。

决策树是一种非参数模型

机器学习模型主要分为两类:参数和非参数。参数模型假设将特征映射到目标的函数具有特定形式。这通常涉及假设数据遵循已知分布。另一方面,非参数模型不假设映射函数具有特定形式。它们也不对数据的分布做出任何假设。因此,它们更灵活,可以适应数据结构。

决策树是非参数模型的一个例子。让我们将决策树回归器与线性回归模型进行比较。线性回归是一种参数模型,假设目标和特征之间存在线性关系。当数据集只有一个特征时,这种线性关系由以下公式给出:

其中θ ₀ 和θ ₁ 是在训练过程中确定的模型参数。根据这个方程,模型预测 ( y^ ) 位于一条直线上。图 20 比较了线性模型的预测和决策树回归器的预测。这里我们有两个不同的数据集,我们将两个模型分别拟合到它们中。

图 20

线性模型对两个数据集的预测都是一条直线,如左图所示。当数据集发生变化时,直线的斜率和截距也会发生变化(因为θ ₀ 和θ ₁ 正在变化),但它仍然是一条直线。这是因为该模型是参数化的,并假设特征和目标之间存在线性关系。树回归器的预测在右图中有所说明。它是一个非参数模型,因此它试图模仿数据的趋势,并且模型的预测会适应训练数据集的形状。因此,随着训练数据集的变化,模型预测的形状也会随之变化。

我们可以将非参数模型想象成一根橡皮筋,它会改变形状来模仿训练数据集的形状。相反,线性模型就像一根刚性杆,可以改变位置但不能改变形状。

梯度提升

梯度提升是集成学习的一个例子。集成学习是一种机器学习方法,通过组合来自多个模型的预测来提高预测性能。提升是一种集成学习方法,它依次组合多个弱学习模型以提高预测性能。梯度提升可用于分类和回归问题。由于它基于提升的概念,它通过依次组合几个弱学习器来创建一个强学习器。

理论上,梯度提升可以使用任何弱模型,但是,决策树是最常见的选择,因为它们能够捕捉复杂的交互和非线性关系。在这里,我们首先解释回归的梯度提升,因为它更容易理解。之后,我们将讨论梯度提升分类器。

梯度提升回归

梯度提升回归涉及几个步骤。这里我们使用我们的玩具数据集来解释这些步骤。梯度提升首先使用训练数据集对预测进行初始猜测。初始猜测是训练数据集中所有数据点的平均目标。因此,我们有:

我们可以将F ₀( x )视为预测x目标值的简单基础模型。该基础模型对训练数据集中所有示例的残差计算如下:

然后,我们创建一个浅层决策树回归器来预测训练数据集的残差。该树回归器用h ₁ 表示,以x作为特征,以y - F ₀( x ) 作为目标。该回归器的预测用h ₁( x )表示。我们将该模型的预测添加到基础模型的预测中:

这里,F ₁( x ) 比原始预测F ₀( x ) 对目标的预测更好。现在我们计算训练数据集中所有示例的F ₁( x )残差:

然后,我们可以训练另一个浅层决策树,以x为特征,以y - F ₁( x ) 为目标。该模型的预测用h ₂( x ) 表示,并添加到F ₁( x ) 中:

F ₁( x ) 相比, F ₂( x ) 现在是一个更好的预测。这个过程可以重复M次。每次我们计算训练数据集中所有示例的前一个模型的残差:

然后我们以x作为特征、y - F_m-1 ( x ) 作为目标,训练决策树h _ m。这棵树的预测结果被添加到前一棵树的预测结果中,以改进它:

实际上,我们在这个方程中添加了一个称为学习率的参数,用η表示,其范围在 0 到 1 之间:

现在,如果只是这个递归方程,则可得出:

这里F_m ( x ) 是增强模型的最终预测。它是将此集成中所有模型的预测添加到基础模型F ₀ 的结果。我们使用训练数据集的示例来获得F_m ( x ),但现在它也可以用于预测训练数据集中不存在的未见特征x的目标。图 21 演示了梯度增强过程。

图 21

因此,最终模型是M 个弱模型的集成,其预测为F _ M ( x )。每个模型都会学习前一个模型的错误并尝试改进其预测。学习率是这个集成模型的超参数,其作用是通过缩小集成中每个模型的预测来抑制过度拟合。在每一步中,我们都希望改进前一个模型的预测,但如果我们改进太多,我们可能会开始学习训练数据集中的噪声,结果就是过度拟合。通常,学习率和集成模型中的树的数量之间存在权衡。随着我们增加树的数量,学习率应该降低以减轻过度拟合。

清单 16 在 Python 中实现了梯度提升算法。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 16 </span>

<span style="color:#aa0d91">class </span> GradBoostingRegressor (): 
    <span style="color:#aa0d91">def </span> __init__ ( <span style="color:#5c2699">self, num_estimators, learning_rate, max_depth= <span style="color:#1c00cf">1</span></span> ): 
        self.num_estimators = num_estimators 
        self.learning_rate = learning_rate 
        self.max_depth = max_depth 
        self.tree_list = [] 
    <span style="color:#aa0d91">def </span> fit ( <span style="color:#5c2699">self, X, y</span> ): 
        self.F0 = y.mean() 
        Fm = self.F0 
        <span style="color:#aa0d91">for</span> i <span style="color:#aa0d91">in </span> <span style="color:#5c2699">range</span> (self.num_estimators): 
            tree_reg = DecisionTreeRegressor(max_depth=self.max_depth, 
                                             random_state= <span style="color:#1c00cf">0</span> ) 
            tree_reg.fit(X, y - Fm) 
            Fm += self.learning_rate * tree_reg.predict(X) 
            self.tree_list.append(tree_reg) 
    <span style="color:#aa0d91">def </span> predict ( <span style="color:#5c2699">self, X</span> ): 
        y_hat = self.F0 + self.learning_rate * \ 
                np. <span style="color:#5c2699">sum</span>([t.predict(X)<span style="color:#aa0d91">for</span> t <span style="color:#aa0d91">in</span> self.tree_list],axis = <span style="color:#1c00cf">0</span>)
        <span style="color:#aa0d91">返回</span>y_hat</span></span></span></span>

该类GradientBoostingRegressor()有两种方法用于拟合数据集和预测目标。请注意,此类采用的估计器数量包括F ₀。因此,如果我们有M棵树,则num_estimatorsM +1。我们可以使用此类将梯度提升算法应用于 Listing 12 中定义的数据集。Listing 17 使用此类绘制梯度提升回归器的不同步骤。结果如图 22 所示。该回归器是 9 棵深度为 1(M =9, num_estimators=10)的决策树的集合。回归器在 Listing 12 中定义的数据集上进行训练。左侧绘制了Fᵢ ( x ),右侧显示了残差(y - F _ m -1( x ))和在它们上训练的浅决策树的预测。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 17</span>

 M = <span style="color:#1c00cf">9</span>
 X_space = np.linspace(- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> , <span style="color:#1c00cf">1000</span> ).reshape(- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">1</span> ) 
gbm_reg = GradBoostingRegressor(num_estimators=M+ <span style="color:#1c00cf">1</span> , learning_rate= <span style="color:#1c00cf">0.3</span> ) 
gbm_reg.fit(X, y) 

fig, axs = plt.subplots(M+ <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">2</span> , figsize=( <span style="color:#1c00cf">11</span> , <span style="color:#1c00cf">45</span> )) 
plt.subplots_adjust(hspace= <span style="color:#1c00cf">0.3</span> ) 

axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ].axis( <span style="color:#c41a16">'off'</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].scatter(X, y, label= <span style="color:#c41a16">"y"</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].axhline(y=gbm_reg.F0, color= <span style="color:#c41a16">"red"</span> , label= <span style="color:#c41a16">"$F_0(x)$"</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].set_title( <span style="color:#c41a16">"m=0"</span> , fontsize= <span style="color:#1c00cf">14</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].set_xlim([- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> ]) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].set_ylim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">2</span> ]) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].legend(loc= <span style="color:#c41a16">"best"</span> , fontsize= <span style="color:#1c00cf">12</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].set_aspect( <span style="color:#c41a16">'equal'</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].set_xlabel( <span style="color:#c41a16">"x"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 
axs[ <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">1</span> ].set_ylabel( <span style="color:#c41a16">"y"</span> , fontsize= <span style="color:#1c00cf">13</span> ) 

<span style="color:#aa0d91">for</span> i <span style="color:#aa0d91">in </span> <span style="color:#5c2699">range</span> ( <span style="color:#1c00cf">1</span> , M+ <span style="color:#1c00cf">1</span> ): 
    Fi_minus_1 = gbm_reg.F0 + gbm_reg.learning_rate * \ 
                 np. <span style="color:#5c2699">sum</span> ([t.predict(X) <span style="color:#aa0d91">for</span> t <span style="color:#aa0d91">in</span> gbm_reg.tree_list[:i- <span style="color:#1c00cf">1</span> ]], 
                        axis= <span style="color:#1c00cf">0</span> ) 
    axs[i, <span style="color:#1c00cf">0</span> ].scatter(X, y-Fi_minus_1, label= <span style="color:#c41a16">f"$y-F_{{ <span style="color:#000000">{i- <span style="color:#1c00cf">1</span> }</span> }(x)$"</span> ) 
    axs[i, <span style="color:#1c00cf">0</span> ].plot(X_space, gbm_reg.tree_list[i- <span style="color:#1c00cf">1</span> ].predict(X_space), 
                   color= <span style="color:#c41a16">"red"</span> ,label= <span style="color:#c41a16">f"$h_{{<span style="color:#000000">{i}</span> }}(x)$"</span> )
    axs[i,<span style="color:#1c00cf"> 0</span> ].set_title(<span style="color:#c41a16"> "m={}"</span> .<span style="color:#5c2699">格式</span>(i), fontsize=<span style="color:#1c00cf"> 14</span> )
    axs[i,<span style="color:#1c00cf"> 0</span> ].set_xlim([-<span style="color:#1c00cf"> 0.3</span> ,<span style="color:#1c00cf"> 3.3</span> ])
    axs[i,<span style="color:#1c00cf"> 0</span> ].set_ylim([-<span style="color:#1c00cf"> 1</span> ,<span style="color:#1c00cf"> 2</span> ])
    axs[i,<span style="color:#1c00cf"> 0</span> ].set_xlabel (<span style="color:#c41a16"> "x"</span> , fontsize=<span style="color:#1c00cf"> 13</span> )
    axs[i,<span style="color:#1c00cf"> 0</span> ].set_ylabel(<span style="color:#c41a16"> "residual"</span> , fontsize=<span style="color:#1c00cf"> 13</span> )
    axs[i,<span style="color:#1c00cf"> 0</span> ].legend(loc=<span style="color:#c41a16"> "best"</span> , fontsize=<span style="color:#1c00cf"> 12</span> )
    axs[i,<span style="color:#1c00cf"> 0</span> ].set_aspect(<span style="color:#c41a16"> 'equal'</span> )
    
    axs[i,<span style="color:#1c00cf"> 1</span> ].scatter(X, y, label=<span style="color:#c41a16"> "y"</span> )
    Fi = gbm_reg.F0 + gbm_reg.learning_rate * \
         np.<span style="color:#5c2699"> sum</span> ([t.predict(X_space)<span style="color:#aa0d91"> for</span> t<span style="color:#aa0d91"> in</span> gbm_reg.tree_list[:i]],
                axis=<span style="color:#1c00cf"> 0</span> )
    axs[i,<span style="color:#1c00cf"> 1</span> ].plot(X_space, Fi, color=<span style="color:#c41a16"> "red"</span> , label=<span style="color:#c41a16"> f"$F_{{ <span style="color:#000000">{i}</span> }}(x)$"</span> )
    axs[i,<span style="color:#1c00cf"> 1</span> ].set_title(<span style="color:#c41a16"> "m={}"</span> .format<span style="color:#5c2699"> (</span> i), fontsize=<span style="color:#1c00cf"> 14</span> )
    axs[i,<span style="color:#1c00cf"> 1</span> ].set_xlim([-<span style="color:#1c00cf"> 0.3</span> ,<span style="color:#1c00cf"> 3.3</span> ])
    axs[i    ,<span style="color:#1c00cf"> 1</span> ].set_ylim([-<span style="color:#1c00cf"> 0.5</span> ,<span style="color:#1c00cf"> 2</span> ])<span style="color:#1c00cf"> axs[i, 1</span> ].set_xlabel(<span style="color:#c41a16"> "x"</span> , fontsize=<span style="color:#1c00cf"> 13</span> )    axs[i,<span style="color:#1c00cf"> 1</span> ].set_ylabel(<span style="color:#c41a16"> "y"</span> , fontsize=<span style="color:#1c00cf"> 13</span> )    axs[i,<span style="color:#1c00cf"> 1</span> ].legend(loc=<span style="color:#c41a16"> "best"</span> , fontsize=<span style="color:#1c00cf"> 13</span> )    axs[i,<span style="color:#1c00cf"> 1</span> ].set_aspect(<span style="color:#c41a16"> 'equal'</span> )plt.show()




</span></span></span></span>

图 22

如图 22 所示,模型Fᵢ ( x ) 的总体预测在每一步中都有所改善。在此示例中,我们仅使用了 9 棵决策树,但如果使用更多决策树会发生什么情况?清单 18 显示了具有 50 个估计器(49 棵树)的梯度提升回归器的预测(图 23)。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400">#清单 18</span>

 X_space = np.linspace(- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> , <span style="color:#1c00cf">1000</span> ).reshape(- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">1</span> ) 
gbm_reg = GradBoostingRegressor(num_estimators= <span style="color:#1c00cf">50</span> , learning_rate= <span style="color:#1c00cf">0.3</span> ) 
gbm_reg.fit(X, y) 
y_hat = gbm_reg.predict(X_space) 

plt.figure(figsize=( <span style="color:#1c00cf">8</span> , <span style="color:#1c00cf">6</span> )) 
plt.scatter(x, y, label= <span style="color:#c41a16">"训练数据"</span> ) 
plt.plot(X_space, y_hat, color= <span style="color:#c41a16">"红色"</span> , label= <span style="color:#c41a16">"预测"</span> ) 

plt.xlim([- <span style="color:#1c00cf">0.3</span> , <span style="color:#1c00cf">3.3</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.1</span> , <span style="color:#1c00cf">2</span> ]) 
plt.legend(loc= <span style="color:#c41a16">"最佳"</span> , fontsize= <span style="color:#1c00cf">14</span> ) 
plt.xlabel( <span style="color:#c41a16">'$x$'</span> ,字体大小= <span style="color:#1c00cf">14</span> ) 
plt.ylabel( <span style="color:#c41a16">'$y$'</span> ,字体大小= <span style="color:#1c00cf">14</span> ) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.show()</span></span></span></span>

图 23

GradientBoostingRegressor库中的类scikit-learn也可用于梯度提升回归。这里我们使用这个类来测试我们的实现:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#aa0d91">从</span>sklearn.ensemble<span style="color:#aa0d91">导入</span>GradientBoostingRegressor 
gbm_reg_sklrean = GradientBoostingRegressor(n_estimators= <span style="color:#1c00cf">50</span> , 
                                            learning_rate= <span style="color:#1c00cf">0.3</span> , 
                                            max_depth= <span style="color:#1c00cf">1</span> ) 
gbm_reg_sklrean.fit(X, y) 
y_hat_sklrean = gbm_reg_sklrean.predict(X_space) 
np.allclose(y_hat, y_hat_sklrean)</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">真的</span></span></span></span>

让我们将其与图 19 中所示的决策树回归器的预测进行比较。请记住,该树的深度为 11,并且在相同数据集上训练时会过度拟合。为什么具有 49 棵树的梯度提升回归器不会出现过度拟合?要回答这个问题,我们应该更深入地了解这两个模型以及它们如何看待训练数据集。图 24 显示了决策树回归器中的不同节点如何处理训练数据集。

图 24

最顶层的节点根节点可以看到整个数据集,并能正确地检测出数据集的增长趋势。由于根节点可以看到全局,因此它能够区分数据集的趋势和噪声。相反,较深的节点只能看到从其父节点传递给它的原始数据集的一小部分。这一小部分的变化主要是由于噪声,但是,由于这是节点唯一能看到的东西,因此它将噪声视为趋势并预测其下降趋势。随着树越来越深,节点会更多地暴露在噪声中并尝试学习它。结果就是过度拟合。

图 25 说明了梯度提升回归器中的不同模型(或节点)如何查看训练数据集。这里每个模型都可以看到整个数据集的残差。因此,它可以更可靠地检测主要趋势。因此,它对过度拟合更具鲁棒性。

图 25

有一个古老的东方故事,讲的是一些盲人第一次遇到大象的故事。他们试图通过触摸大象来想象大象的样子。每个盲人触摸大象身体的不同部位。然后,他们根据自己有限的经验描述大象,而他们的描述最终各不相同。他们都错了,因为他们看不到全貌,只能根据自己有限的经验进行判断。深度决策树也是如此。深度节点只能看到整个数据集的有限部分,因此它们倾向于学习噪音而不是趋势。

图 26(图片来源: https: //pixabay.com/vectors/blind-men-elephant-story-feel-see-1458438/

请记住,虽然梯度提升更能抵抗过度拟合,但它仍然容易出现过度拟合。例如,在清单 18 中使用的数据集中,数据点的噪声来自方差为常数的正态分布。如果噪声方差发生变化或变得太大,梯度提升模型仍然可能过度拟合。

梯度提升回归器背后的数学原理(可选)

到目前为止,我们了解了梯度提升回归器的工作原理。但为什么它有这样的名字,为什么我们需要在每一步学习残差?梯度提升和梯度下降算法之间存在微妙的联系。梯度下降是一种用于寻找可微函数局部最小值的优化算法。它是许多机器学习模型(如逻辑回归和神经网络)的主力。

在机器学习中,模型通过优化成本函数进行训练,该成本函数衡量模型预测与实际数据的匹配程度。梯度下降可以确定最小化成本函数的模型参数值。对于回归问题,我们可以使用均方误差 (MSE) 作为成本函数,其定义如下:

其中yᵢ是第 i 个示例的训练数据集的目标,而yᵢ ^ 是该示例的模型预测。训练数据集中的示例数为nθ ₀ 和θ ₁,… 是模型参数,为简便起见,用θ 表示。梯度下降是一种迭代算法。它从每个参数θᵢ的初始猜测开始,并使用成本函数的梯度在每次迭代中更新其值。参数θᵢ的初始猜测用θᵢ^ (0)表示。在每次迭代中, θᵢ的下一个值是使用其当前值和在其当前值处求出的成本函数(相对于该参数)的导数来计算的。

这里θᵢ^ ( m -1) θᵢ^ ( m ) 分别是参数的当前值和下一个值,α是一个常数,称为学习率。例如,对于具有一个特征的线性回归模型,成本函数写为:

其中xᵢ是数据集中第 i个示例的特征。使用此成本函数,可得出以下结论:

现在想象一下,我们想将梯度下降算法用于非参数回归模型,比如决策树回归器。该模型没有参数,那么每一步将更新什么,成本函数的梯度应该如何计算?

首先,虽然成本函数不再是参数的函数,但对于回归模型来说,它保持不变:

接下来,我们可以修改梯度下降公式来更新模型预测而不是模型参数:

因此,这里每一步要更新的是模型预测。现在,如果我们将成本函数代入这个方程,我们得到:

但是,这个等式存在一个根本问题。要计算y ^ 的下一个值,我们需要目标yᵢ的实际值。这对于训练数据集中的示例是可能的,但是如果我们想要获取训练数据集中不存在的未见数据点的模型预测,该怎么办?这里,我们不知道目标的实际值。在参数模型中,我们使用梯度下降算法来找到模型参数的最优值。一旦找到它们,我们就可以使用它们来预测任何未见数据点的目标。例如,在线性回归的情况下,一旦我们确定了θ ₀ 和θ ₁ 的最优值,我们就可以使用该模型来预测未见数据点 x 的目标:

在非参数模型中,无法使用这种方法。为了避免这个问题,我们在这里采取了不同的方法。对于单个任意数据点,成本函数定义为:

利用这个成本函数,梯度下降公式可以写成:

当然,如果数据点不在训练数据集中,我们不知道y的值。因此,在每一步中,我们训练一个弱模型来预测它。该模型采用特征x并预测残差

我们使用训练数据集来训练这个模型。如果我们用h_m ( x )表示这个弱模型的预测,我们可以将梯度下降公式写成:

最后,如果我们改变符号并替换

其中F _ m ( x ) 和αη有关系,我们得到:

这是我们之前使用的梯度提升的递归方程。因此,我们看到梯度提升就像是非参数模型的传统梯度下降算法的修改。我们训练浅树回归器来预测残差,因为它们代表了成本函数的梯度。每次我们向我们的集合添加一棵新的浅树时,我们都会再增加一次迭代,并且每次迭代都会更新和改进提升模型的预测。

在梯度下降中,我们从参数的初始随机猜测开始,但在梯度提升中,我们做得更明智。我们的起点是一个常数值,它可以最小化训练数据集的平均成本函数。如果我们假设:

然后我们可以将训练数据集的成本函数写为:

如果我们最小化成本函数,我们可以找到c的值:

因此,基础模型(F₀x))只是训练数据集中所有数据点的平均目标。

梯度提升分类

在本节中,我们将解释梯度提升分类的算法,但我们仅关注二元分类。在二元分类问题中,目标只能采用两个标签,可以用 0 和 1 表示。设得到 1 的概率为 p 事件的几率是事件发生的概率与不发生的概率之比。因此,得到 1 的几率是:

几率对数(又称 logit 函数)定义为:

但是为什么我们需要赔率对数呢?如果我们将p设置为 0,赔率对数为 −∞,如果我们将 p 设置为 1,赔率对数将为 +∞。概率 ( p ) 始终介于 0 和 1 之间,但赔率对数将其转换为介于 −∞ 和 +∞ 之间的实数。如果我们有 log (odds),我们可以使用以下公式获得相应的概率:

现在我们介绍梯度提升分类算法:

请记住,梯度提升首先要对训练数据集的预测进行初始猜测。在梯度提升回归器中,初始猜测是训练数据集中所有数据点的平均目标。这里,初始猜测是在训练数据集的目标中找到 1 的概率。这个概率由以下公式给出:

其中N是训练数据集中的示例数。函数F ₀(x) 是一个简单的基础模型,可以预测此概率的对数:

该基础模型针对训练数据集示例的残差计算如下:

请注意,残差是使用概率而不是对数概率计算的。接下来,我们创建一个浅层决策树回归器来预测训练数据集的残差。这个树回归器,用h ₁ 表示,以x作为特征,以y - p ( x ) 作为目标。训练树回归器后,我们需要修改其叶子的值。对于树中的每个叶子(用l表示),我们将叶子的值(vₗ)更改为:

其中L是训练数据集中落入该叶子节点的所有示例的集合。我们用 ( xᵢ , yᵢ ) 表示L中的每个示例,pᵢ是L中特征xᵢ的目标等于 1 的预测概率。因此,分子是 L 中所有示例的残差之和分母是L中所有示例的p (1- p ) 之和。使用修改后的树,我们现在可以预测训练数据集中所有示例的目标。

修改后的树回归器的预测用h ₁( x )表示。我们将这棵树的预测添加到基础模型的预测中:

这里,F ₁( x ) 比原始预测F ₀( x )对目标的预测更好,它预测的是特征x的目标概率对数。现在,我们使用以下公式计算训练数据集中所有示例的目标等于 1 的预测概率:

接下来,我们计算训练数据集的F ₁( x )的残差:

现在,我们训练另一棵浅层决策树,以x为特征,y - p ( x ) 为目标。该模型的预测用h ₂( x ) 表示,并添加到F ₁( x ) 中:

此过程可重复M次。每次我们从先前的模型计算训练数据集的残差,并训练决策树h _ m tp 来预测这些残差。因此,h _ mx为特征,以y - p ( x ) 为目标。这棵树的预测被添加到前一棵树的预测中以改进它:

简化这个递归方程,我们得到了 boosting 模型的最终预测结果如下:

这与我们用于梯度提升回归器的方程完全相同。请注意,方程给出了特征x 的预测对数概率,它也可以用于训练数据集中不存在的未见特征x 。特征x的目标等于 1 的预测概率由以下方程给出:

我们可以将此概率与阈值进行比较,以获得二元目标的最终预测。此阈值通常为 0.5。如果p ( x )≥0.5,则预测目标为 1,否则为 0。

清单 19 实现了梯度提升分类器。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400"># 清单 19 </span>

<span style="color:#aa0d91">class </span> GradBoostingClassifier ():   
    <span style="color:#aa0d91">def </span> __init__ ( <span style="color:#5c2699">self, num_estimators, learning_rate, max_depth= <span style="color:#1c00cf">1</span></span> ): 
        self.num_estimators = num_estimators 
        self.learning_rate = learning_rate 
        self.max_depth = max_depth 
        self.tree_list = [] 
    <span style="color:#aa0d91">def </span> fit ( <span style="color:#5c2699">self, X, y</span> ):    
        probability = y.mean() 
        log_of_odds = np.log(probability / ( <span style="color:#1c00cf">1</span> - probability)) 
        self.F0 = log_of_odds 
        Fm = np.array([log_of_odds]* <span style="color:#5c2699">len</span> (y)) 
        probs = np.array([probability]* <span style="color:#5c2699">len</span> (y)) 
        <span style="color:#aa0d91">for</span> i <span style="color:#aa0d91">in </span> <span style="color:#5c2699">range</span> (self.num_estimators): 
            residuals = y - probs 
            tree_reg = DecisionTreeRegressor(max_depth=self.max_depth) 
            tree_reg.fit(X, residuals) 
            <span style="color:#007400"># 校正叶值</span>
            h = probs * ( <span style="color:#1c00cf">1</span> - probs) 
            leaf_nodes = np.nonzero(tree_reg.tree_ .children_left == - <span style="color:#1c00cf">1</span> )[ <span style="color:#1c00cf">0</span> ] 
            leaf_node_for_each_sample = tree_reg.apply(X) 
            <span style="color:#aa0d91">for</span> leaf <span style="color:#aa0d91">in</span> leaf_nodes: 
                leaf_samples = np.where(leaf_node_for_each_sample == leaf)[ <span style="color:#1c00cf">0</span> ] 
                residuals_in_leaf = residuals.take(leaf_samples,axis= <span style="color:#1c00cf">0</span> ) 
                h_in_leaf = h.take(leaf_samples,axis= <span style="color:#1c00cf">0</span> ) 
                value = np.sum <span style="color:#5c2699">(</span> residuals_in_leaf) / np. <span style="color:#5c2699">sum</span> (h_in_leaf) 
                tree_reg.tree_.value[leaf, <span style="color:#1c00cf">0</span> , <span style="color:#1c00cf">0</span> ] = value 
            
            self.tree_list.append(tree_reg) 
            reg_pred = tree_reg.predict(X) 
            Fm += self.learning_rate * reg_pred 
            probs = np.exp(Fm) / ( <span style="color:#1c00cf">1</span> + np.exp(Fm)) 
    
    <span style="color:#aa0d91">def </span> predict_proba ( <span style="color:#5c2699">self, X</span> ): 
        FM = self.F0 + self.learning_rate * \ 
            np. <span style="color:#5c2699">sum</span> ([t.predict(X) <span style="color:#aa0d91">for</span> t <span style="color:#aa0d91">in</span> self.tree_list], axis= <span style="color:#1c00cf">0</span> ) 
        prob = np.exp(FM) / ( <span style="color:#1c00cf">1</span> + np.exp(FM)) 
        <span style="color:#aa0d91">return</span> prob 
    
    <span style="color:#aa0d91">def </span> predict ( <span style="color:#5c2699">self, X</span> ): 
        yhat = (self.predict_proba(X) >= <span style="color:#1c00cf">0.5</span>).astype( <span style="color:#5c2699">int</span> )
        <span style="color:#aa0d91">返回</span>yhat</span></span></span></span>

此类中的函数predict_proba()返回p ( x ),函数返回预测的二进制目标。请注意,此类采用包括Fpredict() ₀在内的估计器数量。因此,如果我们有M棵树,则= M +1。现在让我们在清单 7 中定义的数据集上尝试此类(绘制在图 8 中)。请记住,我们之前已将决策树分类器拟合到此数据集。这棵树的深度为 8(参见图 9),并且过度拟合(参见图 10)。现在我们将梯度提升分类器拟合到此数据集。num_estimators

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">gbm_clf = GradBoostingClassifier(num_estimators= <span style="color:#1c00cf">30</span> , 
                                 learning_rate= <span style="color:#1c00cf">0.1</span> , max_depth= <span style="color:#1c00cf">1</span> ) 
gbm_clf.fit(X, y)</span></span></span></span>

清单 20 绘制了该模型的决策边界,图 27 展示了结果。

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#007400"># 清单 20</span>

 plt.figure(figsize=( <span style="color:#1c00cf">8</span> , <span style="color:#1c00cf">8</span> )) 
plot_boundary(X, y, gbm_clf, lims=[- <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> , - <span style="color:#1c00cf">1</span> , <span style="color:#1c00cf">5</span> ]) 
plt.axvline(x= <span style="color:#1c00cf">1.8</span> , color= <span style="color:#c41a16">"black"</span> , linestyle= <span style="color:#c41a16">"--"</span> , label= <span style="color:#c41a16">"实际边界"</span> ) 
plt.text( <span style="color:#1c00cf">0</span> , - <span style="color:#1c00cf">0.3</span> , <span style="color:#c41a16">r"$\hat{y}=0$"</span> , fontsize= <span style="color:#1c00cf">15</span> ) 
plt.text( <span style="color:#1c00cf">3</span> , - <span style="color:#1c00cf">0.3</span> , <span style="color:#c41a16">r"$\hat{y}=1$"</span> , fontsize= <span style="color:#1c00cf">15</span> ) 
ax = plt.gca()   
ax.set_aspect( <span style="color:#c41a16">'equal'</span> ) 
plt.xlim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">4.5</span> ]) 
plt.ylim([- <span style="color:#1c00cf">0.5</span> , <span style="color:#1c00cf">4.6</span> ]) 
plt.xlabel( <span style="color:#c41a16">'$x_1$'</span> , fontsize= <span style="color:#1c00cf">18</span> ) 
plt.ylabel( <span style="color:#c41a16">'$x_2$'</span> , fontsize= <span style="color:#1c00cf">18</span> ) 
plt.legend(loc= <span style="color:#c41a16">"best"</span> , fontsize= <span style="color:#1c00cf">14</span> ) 

plt.show()</span></span></span></span>

图 27

您会发现,即使集成中有 29 棵树,模型也不会过度拟合,并能正确预测边界。库GradientBoostingClassifier中的类scikit-learn也可用于梯度提升分类,我们可以用它来测试我们的实现:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424"><span style="color:#aa0d91">从</span>sklearn.ensemble<span style="color:#aa0d91">导入</span>GradientBoostingClassifier 
gbm_clf_sklrean = GradientBoostingClassifier(n_estimators= <span style="color:#1c00cf">30</span> , 
                                             learning_rate= <span style="color:#1c00cf">0.1</span> , 
                                             max_depth= <span style="color:#1c00cf">1</span> ) 
gbm_clf_sklrean.fit(X, y) 
phat_sklrean = gbm_clf_sklrean.predict_proba(X)[:, <span style="color:#1c00cf">1</span> ] 
phat = gbm_clf.predict_proba(X) 
np.allclose(phat, phat_sklrean)</span></span></span></span>
<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">真的</span></span></span></span>

梯度提升分类器背后的数学原理(可选)

如前所述,梯度提升就像是非参数模型的梯度下降算法的修改。请记住,对于梯度提升回归器,我们最终得到了以下等式:

其中 y^ 是模型对目标的预测。在梯度提升分类器中,我们修改梯度下降公式来更新对数概率的模型预测:

但是我们如何定义成本函数(J)呢?分类器模型预测的是目标等于 1 的概率。如果目标的实际值为 1,我们希望最大化这个概率,如果为零,我们希望最小化这个概率。从数学上讲,这相当于最大化这个项:

y =1 时,该项等于log ( p )。因此,最大化它将最大化p。当y =0 时,它等于log (1- p ),最大化它将最小化p。该项称为对数似然函数。我们通常更喜欢最小化成本函数,因此我们的最终成本函数定义为负对数似然:

我们已经证明:

现在利用这个公式,我们可以简化成本函数:

接下来,我们计算成本函数关于log赔率)的导数:

因此,可以得出以下结论:

如果数据点不在训练数据集中,我们不知道y的值。因此,在每一步中,我们训练一个弱模型来预测它。该模型采用特征x并预测残差y - p 。训练数据集用于训练该模型。如果我们用h_m ( x )表示这个弱模型的预测,假设F _ m ( x ) 给出预测的对赔率),并用η替换α,我们得到梯度提升分类器的递归公式:

我们还需要解释一下修改每棵树的叶子值的公式。树中叶子l的值(用vₗ表示)是通过求解以下方程获得的:

首先,我们把之前推导的成本函数写成F _ m -1( x )的形式:

然后,我们使用泰勒级数来近似落在叶子节点中的训练数据集的一个示例的成本函数:

如果我们把所有的例子都包括进去,我们得到:

现在,为了最小化成本函数,我们取其对vₗ的导数并将其设置为零:

通过求解vₗ这个方程,我们得到:

接下来我们计算成本函数的一阶和二阶导数:

如果我们将它们代入vₗ方程,则得出:

在本文中,我们尝试提供对决策树的直观理解。决策树是由一些节点组成的非参数模型。每个节点只是一个线性分类器,但当它们组合在一起时,它们可以学习数据集中的任何非线性模式。这种灵活性是以过度拟合为代价的,这意味着当树长得太多时,它会开始学习数据点的噪声。梯度提升是一种集成方法。它由一系列弱决策树组成,其中每棵树都试图改善前一棵树的预测。梯度提升保持了决策树的灵活性,但对过度拟合更具鲁棒性。我们展示了如何在 Python 中从头开始实现梯度提升,并解释了为什么它更能抵抗过度拟合。

;