课程7. 机器学习的集成算法

发布于:2025-04-10 ⋅ 阅读:(34) ⋅ 点赞:(0)

简单算法的质量

我们之前研究的简单算法通常不能产生很好的预测。让我们尝试应用简单的算法来解决已知问题之一。

from sklearn.datasets import load_breast_cancer
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier

# 预处理逻辑回归的数据
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)

X_scaled = StandardScaler().fit_transform(X) # 正确的

X_train, x_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.3, shuffle=True, random_state=1917
)
tree_clf = DecisionTreeClassifier().fit(X_train, y_train)
knn_clf = KNeighborsClassifier().fit(X_train, y_train)
linear_clf = LogisticRegression().fit(X_train, y_train)
predictions_tree = tree_clf.predict(x_test)
predictions_knn = knn_clf.predict(x_test)
predictions_linear = linear_clf.predict(x_test)
accuracy_tree = accuracy_score(predictions_tree, y_test)
accuracy_knn = accuracy_score(predictions_knn, y_test)
accuracy_linear = accuracy_score(predictions_linear, y_test)
print(f'Accuracy of tree classifier: {accuracy_tree}\nAccuracy of knn classifier: {accuracy_knn}\nAccuracy of linear classifier: {accuracy_linear}')

输出:
在这里插入图片描述

(predictions_linear == predictions_knn).all()
# 检查逻辑回归和 K 近邻分类器的预测结果是否完全相同。如果所有元素都相同,则返回 True,否则返回 False。

输出:
np.False_

每个分类器都能很好地对原始数据集进行分类。如果我们进行投票会怎么样?

# 集成学习(投票法)
sum_of_voices = (
    predictions_tree.astype(int)
    + predictions_knn.astype(int)
    + predictions_linear.astype(int)
)
voiting_result = (sum_of_voices > 1).astype(int)

其中:

  • predictions_tree.astype(int):将决策树分类器的预测结果转换为整数类型。
  • sum_of_voices:将三种分类器的预测结果相加,得到每个样本的投票总数。
  • voiting_result = (sum_of_voices > 1).astype(int):如果某个样本的投票总数大于 1,则认为该样本的最终预测结果为 1,否则为 0。
accuracy_voiting = accuracy_score(voiting_result, y_test)
print(f"Voiting result: {accuracy_voiting}")

输出:
Voiting result: 1.0

通过结合三个不同分类器的独立意见,我们得到了完美的预测!
民主万岁!
这种方法称为集成。

集成

集成是严肃的机器学习模型。它们功能强大,可以让您反映数据之间的复杂依赖关系。然而,集成的“力量”是以牺牲可解释性为代价的。这种算法通常被称为“黑匣子”:很难理解哪些特征影响了特定观测的预测。为了解决这个问题你到底愿意牺牲什么取决于你自己。

模型集成的想法很简单。假设您使用基于决策树的模型 m 1 m_1 m1 解决了二元分类问题。我们得到了准确度指标的值 = 0.8。然而,所有模型都容易过度拟合。一种针对所有训练观测和所有特征进行训练的算法可能会在验证中犯下重大错误。有一个想法是采用另一个模型 - m 2 m_2 m2,以弥补第一个模型的错误。就其本身而言,它可能也无法提供非常高的准确性。例如,准确度=0.79。你可以再次改变条件,构建另一个模型 m 3 m_3 m3,依此类推,直到模型 m n m_n mn。因此, n n n个模型中的每一个模型都会根据其差异而受到不同的随机因素的影响,但同时也会带来自己关于观测的神圣知识。您可以尝试同时将它们全部考虑在内 - 获得一个组合模型 M M M,它将比单独考虑每个模型给出更好的结果,比如准确度 = 0.91。我们将这样的总结模型称为(他们说“强分类器”),而将其所基于的模型称为(或“弱分类器”)。

误差分解*

让我们回顾一下回归问题的公式(类似的推理对于分类也有效)。假设存在某个函数 f ( x ) f(x) f(x) 和正态噪声 ϵ ϵ ϵ,并且我们假设 ϵ ϵ ϵ ~ N ( 0 , σ 2 ) N(0, σ^2) N(0,σ2)。然后,在回归问题陈述的框架内,我们有一组对象 x i ∈ X x_i \in X xiX 和一组相应的标签:

y i = f ( x i ) + ϵ y_i = f(x_i) + ϵ yi=f(xi)+ϵ

我们还有一些函数 f ^ ( x ) \hat{f}(x) f^(x)——我们的回归模型版本。我们引入了经验风险的概念: Q = E X [ L ( x , y ) ] Q = E_X[L(x, y)] Q=EX[L(x,y)]

在问题中我们经常使用标准差作为 Q Q Q
Q = 1 N ∑ i = 1 N ( y i − f ^ ( x i ) ) 2 Q = \frac{1}{N}\sum\limits_{i=1}^N (y_i - \hat{f}(x_i))^2 Q=N1i=1N(yif^(xi))2
让我们尝试估计预测与标签偏差的平方的数学期望*:

E [ ( y − f ^ ( x ) ) 2 ] = E [ y 2 ] − 2 y f ^ ( x ) + ( f ^ ( x ) ) 2 ] = E [ y 2 ] + E [ ( f ^ ( x ) ) 2 ] − 2 E [ y f ^ ( x ) ] = E[(y - \hat{f}(x))^2] = E[y^2] - 2y\hat{f}(x) + (\hat{f}(x))^2] = E[y^2] + E[(\hat{f}(x))^2] - 2E[y\hat{f}(x)] = E[(yf^(x))2]=E[y2]2yf^(x)+(f^(x))2]=E[y2]+E[(f^(x))2]2E[yf^(x)]=

//从现在开始,我们将省略 f ^ ( x ) \hat{f}(x) f^(x) 符号中的 ( x ) (x) (x) 参数,以简化对文本的理解//

= E [ y 2 ] + E [ f ^ 2 ] − 2 E [ y f ^ ] = = E[y^2] + E[\hat{f}^2] - 2E[y\hat{f}] = =E[y2]+E[f^2]2E[yf^]=

回想一下,方差 D y Dy Dy 可以定义为 D y = E [ y 2 ] − ( E [ y ] ) 2 Dy = E[y^2] - (E[y])^2 Dy=E[y2](E[y])2(1)

= D y + ( E [ y ] ) 2 + D f ^ + ( E [ f ^ ] ) 2 − 2 f E [ f ^ ] = = Dy + (E[y])^2 + D\hat{f} + (E[\hat{f}])^2 - 2fE[\hat{f}] = =Dy+(E[y])2+Df^+(E[f^])22fE[f^]=

最后一个转换是通过将等式 (1) 应用于前面的结果而获得的,并且由于 y = f + ϵ y = f + ϵ y=f+ϵ,因此在最后一项中将 f f f 取到数学期望的符号之外,其中 f f f 是确定性值,因此可以取到数学期望的符号之外,而 ϵ ϵ ϵ 具有零数学期望。现在让我们记住 y = f + ϵ y = f + ϵ y=f+ϵ,这意味着 E [ y ] = E [ f ] + E [ ϵ ] = E [ f ] E[y] = E[f] + E[ϵ] = E[f] E[y]=E[f]+E[ϵ]=E[f]

= D y + D f ^ + ( E [ f ] 2 + E [ f ^ ] 2 − 2 f E [ f ^ ] ) = D y + D f ^ + ( f − E [ f ^ ] ) 2 = σ 2 + D f ^ + ( f − E [ f ^ ] ) 2 = Dy + D\hat{f} + (E[f]^2 + E[\hat{f}]^2 - 2fE[\hat{f}]) = Dy + D\hat{f} + (f - E[\hat{f}])^2 = σ^2 + D\hat{f} + (f - E[\hat{f}])^2 =Dy+Df^+(E[f]2+E[f^]22fE[f^])=Dy+Df^+(fE[f^])2=σ2+Df^+(fE[f^])2

获得最后的转变是因为 D y = D [ f + ϵ ] Dy = D[f + ϵ] Dy=D[f+ϵ],其中 f f f 是一个不会对方差产生影响的确定性值。因此, D y = D ϵ = σ 2 Dy = Dϵ = σ^2 Dy==σ2

因此,我们得到:

E [ ( y − f ^ ( x ) ) 2 ] = σ 2 + D f ^ + ( f − E [ f ^ ] ) 2 E[(y - \hat{f}(x))^2] = σ^2 + D\hat{f} + (f - E[\hat{f}])^2 E[(yf^(x))2]=σ2+Df^+(fE[f^])2

也就是说,模型预测误差的数学期望分解为 3 项:

  • σ 2 σ^2 σ2 - 原始数据中的噪声方差。噪声方差越大,模型的预测能力越差,也就是其误差的数学期望越高。发生这种情况的原因是,强噪声会阻止模型对样本进行定性分析并破坏其训练。
  • D f ^ D\hat{f} Df^ – 方差。它衡量了随机性、初始数据中的异质性、特定样本的特征、算法的超参数等因素对最终学习结果的影响程度。
  • f − E [ f ^ ] f - E[\hat{f}] fE[f^] - 偏差。显示算法的系统误差。

*注意:在这种情况下,数学期望是在一组所有可能的样本上考虑的。

在这里插入图片描述
这就引发了寻找偏差和方差之间最佳关系的问题。显然,人们希望降低这两个指标,但事实证明它们之间存在一定的联系。偏差和方差都取决于所选模型的复杂性。增加模型的复杂性会导致散度的增加,而简化模型会导致偏差的增加。

在这里插入图片描述
找到这样的最优解是一项选择模型最佳复杂度、选择超参数等的任务。

然而,事实证明存在一种相当明显的方法可以减少固定偏差下的方差。假设我们有 N N N个同一类的略有不同的算法。如果它们之间的差异确实不是很强,那么它们的偏差就大致相同,也就是说,它们的误差的系统部分是接近的。我们不再预测其中一种算法,而是考虑以某种形式组合预测它们。例如,让我们对所有 N N N 个算法的预测取平均值。显然,这种组合算法的偏差将非常接近每个原始(基础)算法的偏差。方差会减小,因为我们现在是对围绕某个点随机分布的值进行平均,这很可能会帮助我们减少与这个点的偏差。

因此,我们回到集成的想法,现在用将误差分解为偏差和扩散来表达。

分类器的简单投票

假设我们有许多不太好的模型。我们将此类模型称为基础。我们将它们表示为 b 1 . . . b n b_1 ... b_n b1...bn

假设我们有一个二分类问题,即我们需要找到一个算法 A : X → Y = − 1 , 1 A: X → Y = {-1,1} A:XY=1,1(即类标签指定为 1 和 -1)。

我们将以下选择最终答案的规则称为分类器的简单投票: A ( x ) = s i g n ( ∑ i = 1 n b i ( x ) ) A(x) = sign(\sum\limits_{i=1}^n b_i(x)) A(x)=sign(i=1nbi(x))

如果大多数分类器建议+1作为答案,那么最终算法也会建议+1作为答案;如果大多数人同意-1选项,那么最终答案将是相同的。



注意:

让我们想象一下,我们使用的所有分类器对样本中的每个对象都给出相同的答案。因此,最终的集成算法将给出完全相同的答案。与使用任何基本分类器相比,这种情况对我们来说都没有任何优势。

由此得出一个重要的结论:用作基础模型的模型必须不同

此外,我们所说的不同并不一定意味着使用不同的机器学习模型。我们可以选择相同的模型,但使用不同的超参数、在不同的子样本上等来训练它们。

在这种情况下,模型可能是具有足够差异的响应,从而形成一个整体。



集成模型主要有两种类型:随机森林和梯度提升。其中,决策树通常被视为弱模型。树木的选择并不是随机的。例如,与线性模型不同,对于每个单独的弱分类器的树,不仅可以改变观测和特征的子样本,还可以改变其他参数。例如,限制树的深度或每个节点中的最小对象数量。因此,树会产生更多模型来解决同一个问题。当模型不同且能补偿彼此的错误时,平均法效果最佳。但对具有相同缺点的类似模型进行平均不会产生这样的效果。

随机森林

第一类模型集成是随机森林。它以略有不同的方式(采用不同的子样本、不同的特征)生成许多彼此独立的不同树,并根据它们的响应形成最终解决方案。随机森林算法对所有树的答案取平均值(在回归问题中)或对森林中大多数树都认为正确的答案进行投票(在分类问题中)。当谈论模型集合时,会使用术语bagging。它意味着对在不同子样本上训练的模型进行平均。这种方法本身称为自举,即根据来自原始的许多不同子样本来估计各种特征或预测。

使用随机森林时,请记住树是“并行”训练的,彼此独立。每棵树的学习都独立于其他树的结果。这是随机森林和梯度提升的概念区别。

随机子空间方法

使基础模型(在我们的例子中是树)尽可能多样化的一种方法是在随机选择的对象特征子样本上对它们进行训练。

在这里插入图片描述

装袋

装袋涉及选择对象本身的随机子样本。通过重复形成随机子样本(即,如果我们已经从样本中选择了某个对象,则可以再次选择它。通过这种方法,平均而言,原始样本中大约 63% 的对象会包含在子样本中)。
在这里插入图片描述

随机森林

在经典的随机森林算法中,这两种方法都会用到。
在这里插入图片描述

梯度提升

从根本上来说,想法是一样的:我们收集许多简单模型(树),以便它们相互补偿错误和随机性。只有在这种情况下,错误的补偿不是通过平均而是通过学习来实现的。在梯度提升中,树是按顺序训练的:每个后续树的构建都会考虑到前一棵树的结果。您已经了解梯度下降的原理:当朝着正确的方向移动时,它们会不断尝试在每一步中最小化错误。
梯度提升 (gradient boosting) 的思想是类似的:在某一步骤有一个模型,它会做出带有一些误差的预测。然后,他们建立第二个模型,该模型预测的不是原始值,而是第一个模型的误差,并考虑到这一点,调整最终预测。第三个模型将预测第二个模型的误差,依此类推。最后,我们根据整个序列预测特定的观察结果。我们说第一个模型给出了这个答案,但是我们在它上面添加了第二个模型的修正,然后是第三个模型的修正,依此类推,直到得到最后一个模型。因此,在每个后续步骤(每个后续模型)中,前一个步骤的预测都会得到改进。 Boosting 的意思是“改善”。并且梯度提升是“逐步改进”。

分类器的线性组合

从技术上讲,梯度提升的构造有着根本的不同(与随机森林相比)。

我们讨论分类器的线性组合。

线性组合是形式为 ∑ i = 1 n c i b i ( x ) \sum\limits_{i=1}^nc_ib_i(x) i=1ncibi(x) 的表达式

同时,可能存在一些决策规则 D D D 根据这个总和来确定最终答案是什么(例如, D D D 可能是上述总和与某个阈值的比较:如果总和中我们得到一个超过阈值的值,则答案为 1,否则为 0(或 -1))。

然后我们的集成算法根据规则 A ( x ) = D ( ∑ i = 1 n c i b i ( x ) ) A(x) = D(\sum\limits_{i=1}^nc_ib_i(x)) A(x)=D(i=1ncibi(x)) 构建

这种算法的训练是按顺序构建的:

  1. 首先我们建立第一个分类器 b 1 b_1 b1。这里没有什么技巧,它只是所选类型中最常见的模型。

  2. 直到我们达到所需的质量,我们将向我们的组合中添加新的分类器 b i b_i bi

  3. 假设我们已经建立了分类器 b 1 . . . b m b_1...b_m b1...bm。我们如何将 b m + 1 b_{m+1} bm+1 添加到它们中?

回想一下梯度下降算法:如果算法 A A A由参数向量 w ⃗ \vec{w} w 参数化,则 w ⃗ n + 1 = w ⃗ n − α ∇ w Q ( x , w ⃗ ) = w ⃗ n − α g n ⃗ \vec{w}_{n+1} = \vec{w}_n - α∇_wQ(x,\vec{w}) = \vec{w}_n - α\vec{g_n} w n+1=w nαwQ(x,w )=w nαgn ,其中 α α α是指定梯度步骤大小的参数。

我们的任务是形成 A m + 1 = ∑ i = 1 m c i b i ( x ) + c m + 1 b m + 1 = A m + c m + 1 b m + 1 A_{m+1} = \sum\limits_{i=1}^mc_ib_i(x) + c_{m+1}b_{m+1} = A_m + c_{m+1}b_{m+1} Am+1=i=1mcibi(x)+cm+1bm+1=Am+cm+1bm+1
再次: w ⃗ n + 1 = w ⃗ n − α g n ⃗ \vec{w}_{n+1} = \vec{w}_n - α\vec{g_n} w n+1=w nαgn A m + 1 = A m + c m + 1 b m + 1 A_{m+1} = A_m + c_{m+1}b_{m+1} Am+1=Am+cm+1bm+1
为什么我们不构造 b m + 1 b_{m+1} bm+1 以便它近似于反梯度向量 − g m ⃗ -\vec{g_m} gm ?然后我们得到了扩展到集合的梯度下降的类似物。

用人类语言来说,这意味着我们将以这样一种方式构建每个新的分类器,使其近似于前一个分类器的错误并进行纠正。

在这里插入图片描述

树集成的实现

sklearn 中的随机森林是在 ensemble 模块中实现的。该模块中最常用的算法是RandomForestClassifier()RandomForestRegressor(),分别用于分类和回归问题。使用分类问题的示例的公告、训练和预测的语法:

from sklearn.ensemble import RandomForestClassifier

# 定义一个基于随机森林算法的新模型的算法
rf_model = RandomForestClassifier(max_depth=2, n_estimators=2)
rf_model.fit(X_train, y_train)
y_pred = rf_model.predict(x_test)
accuracy_score(y_pred, y_test)

输出:
0.9473684210526315

在这里,在声明模型时,我们指定了“n_estimators”——我们将在此基础上构建森林的树的数量。同时,我们没有设置其他树参数——例如树深度“max_depth”、特征子样本的大小“max_features”、节点中的最小对象数“min_samples_leaf”——并保留它们的默认设置。在实践中,您可以尝试这些值并看看它如何影响结果。

sklearn 中的梯度提升是在 ensemble 模块中实现的。它包含分别用于分类和回归问题的 GradientBoostingClassifier()GradientBoostingRegressor() 算法。以分类问题为例的语法:

from sklearn.ensemble import GradientBoostingClassifier

# 让我们定义一个基于梯度提升算法的新模型算法
gb_model = GradientBoostingClassifier(n_estimators=100)
gb_model.fit(X_train, y_train)
y_pred = gb_model.predict(x_test)
accuracy_score(y_pred, y_test)

输出:
0.9707602339181286

结论:

  1. 使用集成算法可以显著提高分类或回归的质量。
  2. 需要使用不同的模型作为基础算法。
  3. 最流行的集成类型有两种——梯度提升随机森林。两者都是在决策树上实现的。

案例

让我们考虑以下流行集成算法的应用示例:

  • 基于 california_housing 数据集,根据区域描述预测房地产价值的任务
  • 合成二维二元分类问题并对所得分割面进行可视化
  • 对 MNIST 数据集中的手写数字图像进行分类的任务

加州住房

住房数据集在数据科学中非常流行。此类数据集的主题是根据社区描述来预测房价,社区描述通常略有不同,但此类不同数据集具有许多共同特征。这些数据集包括流行的数据集波士顿住房、肯塔基住房、梅尔本住房等。Cian 和 Avito 等公司主要基于这些数据集开发了他们的产品。

在这个例子中,我们将考虑加州住房数据集。
以下是其原始描述:


加州住房数据集

数据集特征:

:实例数:20640
:属性数量:8 个数值、预测属性和目标
:属性信息:

  • 街区组中 MedInc 收入中位数
  • HouseAge 街区组中房屋年龄中位数
  • AveRooms 每户平均房间数
  • AveBedrms 每户平均卧室数量
  • 人口街区组人口
  • AveOccup 平均家庭成员数
  • 纬度块组纬度
  • 经度块组长度

:缺少属性值:无

该数据集是从 StatLib 存储库获得的:链接

目标变量是加州各地区的房屋中位价,以数十万美元(100,000 美元)表示。

该数据集源自 1990 年美国人口普查,每个人口普查区块组占一行。区块组是美国人口普查局发布样本数据的最小地理单位(一个区块组的人口通常为 600 至 3,000 人)。

家庭是指居住在一个房屋内的一群人。由于此数据集中提供的是每个家庭的平均房间和卧室数量,因此对于家庭数量少、空置房屋较多的街区组(例如度假村),这些列的值可能会大得惊人。

可以使用 :func:sklearn.datasets.fetch_california_housing 函数下载/加载。

参考资料:

  • Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions, Statistics and Probability Letters, 33 (1997) 291-297
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

housing = fetch_california_housing()

X, y = housing.data, housing.target
X_train, x_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, shuffle=True, random_state=42
)
X.shape

输出:(20640, 8)

为了解决这个问题,我们将使用基于梯度提升的回归模型。我们从“xgboost”库中选择一个流行的梯度提升实现


参考:

目前,梯度提升算法有很多不同的实现。其中,最流行的实现有三种:

  • XGBoost 是 Tianqi Chen 和 DMLC 研究团队于 2014 年开发的一个库。该算法最初是作为希格斯机器学习挑战赛的一部分开发的,表现出非凡的性能,赢得了第一名。此后,社区对xgboost产生了兴趣,并针对Python,R,Java,Scala,Julia,Perl等编程语言进行了实现(链接
  • Catboost 是 2017 年的项目。维基百科对“catboost”提供了全面的描述:

Yandex 开发的开源软件库,使用原始梯度增强方案之一实现构建机器学习模型的独特专利算法。该库的主要 API 是针对 Python 语言实现的,另外还有一个针对 R 编程语言的实现 ©Wikipedia

  • LightgbmMicrosoft 于 2016 年开发的一个项目。与上面提到的两个框架相比,它的使用有些困难,尽管一些专家指出了它的特殊有效性。

当今最流行的“默认”选择可能是“xgboost”。一般来说,选择特定的框架是一个个人喜好的问题。


import xgboost
from xgboost import XGBRegressor
from sklearn.metrics import r2_score, mean_absolute_percentage_error

xgb_reg = XGBRegressor(n_estimators=10, max_depth=5, learning_rate=0.4)

其中:

  • n_estimators:该参数代表提升树的数量,也就是在构建集成模型时所使用的弱学习器(这里是决策树)的数量。在梯度提升框架里,模型会逐步地添加新的决策树,每一棵新树都会去拟合之前所有树的残差。更多的树往往能让模型学习到更复杂的数据模式,不过这也会增加计算量和过拟合的风险。例如,n_estimators = 10意味着模型会构建 10 棵决策树来进行回归预测。
  • max_depth:此参数用于限制每棵决策树的最大深度。决策树的深度指的是从根节点到叶节点的最长路径上的节点数。更大的max_depth值会让树能够学习到更复杂的特征和模式,因为它可以进行更多次的分裂。然而,这也可能导致过拟合,因为模型可能会过于紧密地适应训练数据中的噪声。当max_depth = 5时,意味着每棵决策树的最大深度为 5,即树最多会进行 5 次分裂。
  • learning_rate:这个参数也被称作步长,它控制着每棵树对最终预测结果的贡献程度。在梯度提升过程中,每一棵新树都会被乘以一个学习率后再加入到之前的模型中。较小的学习率会使模型的训练过程更加缓慢,因为每棵树的贡献相对较小,但它能让模型更加稳定,并且可能会得到更好的泛化性能。相反,较大的学习率会使模型更快地收敛,但可能会跳过最优解,导致过拟合。当learning_rate = 0.4时,每棵新树的预测结果会乘以 0.4 后再加入到之前的模型中。
xgb_reg.fit(X_train, y_train)
preds = xgb_reg.predict(x_test)

r2 = r2_score(y_test, preds)
mean_absolute_percentage_error(y_test, preds)

输出:0.2180191181662406

r2

输出:0.777403998271669

为了进行比较,我们构造一棵二叉树:

from sklearn.tree import DecisionTreeRegressor

reg = DecisionTreeRegressor(max_depth=5).fit(X_train, y_train)
preds_tree = reg.predict(x_test)

r2_score(y_test, preds_tree)

输出:0.6029986793705844

mean_absolute_percentage_error(y_test, preds_tree)

输出:0.32935074797935937

让我们用预测目标坐标来表示得到的结果。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme()

plt.figure(figsize=(10, 8))
plt.scatter(preds, y_test, label="Grad Boosting", c="y", alpha=0.5)
plt.scatter(preds_tree, y_test, label="Tree", c="g", alpha=0.5)
plt.xlabel("Prediction")
plt.ylabel("Bullet")

输出:
在这里插入图片描述
这个可视化清楚地表明,决策树只能从给定的离散集合中产生一个预测,而梯度提升产生连续的结果。如果降低 max_depth 值,这种情况会更加明显。

为了更好地理解情况,我们可以展示预测误差分布的直方图

ae = preds - y_test
ae_tree = preds_tree - y_test
plt.figure(figsize=(10,8))
plt.title('Distribution of prediction error on the test sample')
plt.xlabel('Error')
h = plt.hist(ae, alpha=0.5, color='y')
h = plt.hist(ae_tree, alpha=0.5, color='g')

输出:
在这里插入图片描述

(ae**2).mean()
(ae_tree**2).mean()
ae.std()
ae_tree.std() # 按列计算标准差

输出:
np.float64(0.2921661801074385)
np.float64(0.5210801561811792)
np.float64(0.5405239086381965)
np.float64(0.7218491942877675)

还可以看出,决策树的预测误差的方差比集成的要高,并且均方误差对于梯度提升也更好。总体来说,两种算法的表现都相当高。(标准差和方差越小通常越好,也就是说梯度提升算法XGBoost更好点)

随机森林用于解决二元分类问题

现在让我们考虑一个随机森林应用的例子。本例的主要目的是展示该算法生成的类分离面的典型形式。

我们将从“sklearn.ensemble”模块中获取随机森林算法。对于分类任务,我们将使用指定模块的“RandomForestClassifier”类。

生成数据:

# 将点添加到第一类的平面
np.seed = 10
train_data = np.random.normal(size=(50, 2))
train_data = np.r_[train_data, np.random.normal(size=(50, 2), loc=0.5, scale=2)]
train_labels = np.zeros(100)

# 添加第二个类别的点
train_data = np.r_[train_data, np.random.normal(size=(100, 2), loc=4, scale=2)]
train_labels = np.r_[train_labels, np.ones(100)]
%%time # 这是 Jupyter Notebook 中的魔法命令,用于测量该代码单元格的执行时间。执行完单元格后,会在输出中显示代码运行所花费的时间。
from sklearn.ensemble import RandomForestClassifier

# 训练算法
rf_model = RandomForestClassifier(n_estimators=50, max_depth=3)
rf_model.fit(train_data, train_labels)

# 分割面
def get_grid(data):
    x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1
    y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1
    return np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))


xx, yy = get_grid(train_data)
predicted = rf_model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.figure(figsize=(20, 12))
plt.pcolormesh(xx, yy, predicted, cmap="Blues")
plt.scatter(
    train_data[:, 0],
    train_data[:, 1],
    c=train_labels,
    s=100,
    cmap="YlGn",
    edgecolors="black",
    linewidth=1.5,
);

输出:
在这里插入图片描述
其中:

  • get_grid(data):定义一个名为get_grid的函数,用于生成一个二维网格。
    • x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1:计算数据集中第一列特征的最小值减 1 和最大值加 1,作为网格在 x 轴方向的范围。
    • y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1:计算数据集中第二列特征的最小值减 1 和最大值加 1,作为网格在 y 轴方向的范围。
    • np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01)):使用np.meshgrid函数生成二维网格,网格的步长为 0.01。
  • xx, yy = get_grid(train_data):调用get_grid函数,根据训练数据生成二维网格。
  • np.c_[xx.ravel(), yy.ravel()]:将二维网格展平为一维数组,并按列拼接,得到一个二维数组,用于作为随机森林模型的输入。
  • rf_model.predict(np.c_[xx.ravel(), yy.ravel()]):使用训练好的随机森林模型对网格中的每个点进行分类预测。
    .reshape(xx.shape):将预测结果重新调整为与网格相同的形状。

MNIST

MNIST 是最流行的计算机视觉数据集之一。该数据集包含一组以 28 × 28 28×28 28×28 数组表示的手写数字图像。该数组的每个元素以颜色强度值的形式表示像素的描述。这些矩阵被扩展为相应的784维向量,从而产生每个图像的向量描述。在这些向量上训练多类分类算法。

为了多样性,这次我们将使用“sklearn”库中的梯度提升实现。 verbose 参数允许您自定义树构建期间统计信息的输出。

import pandas as pd

mnist_train = pd.read_csv("./sample_data/mnist_train_small.csv")
mnist_test = pd.read_csv("./sample_data/mnist_test.csv")

X_train = mnist_train.values[:, 1:]
X_test = mnist_test.values[:, 1:]

y_train = mnist_train.values[:, 0]
y_test = mnist_test.values[:, 0]
X_train.shape

输出:(19999, 784)

显示个图片看看

plt.imshow(X_train[0].reshape(28, 28))

输出:
在这里插入图片描述
看看标签:

y_train[0]

输出:5

from sklearn.ensemble import GradientBoostingClassifier

clf = GradientBoostingClassifier(n_estimators=20, verbose=True, random_state=42)
clf.fit(X_train, y_train)

输出:
在这里插入图片描述

其中:

  • GradientBoostingClassifier则是用于实现梯度提升分类算法的类。梯度提升是一种集成学习技术,它通过迭代地训练一系列弱分类器(通常是决策树),并将它们组合起来形成一个强分类器。
  • n_estimators=20:此参数指定了要训练的弱分类器(决策树)的数量,这里设置为 20 意味着模型会依次训练 20 棵决策树。一般来说,增加n_estimators的值可以提升模型的性能,但同时也会增加训练时间,并且可能导致过拟合。
  • verbose=True:当设置为True时,在训练过程中会输出详细的信息,比如每一轮训练的损失值等,这有助于你了解模型的训练进度和性能变化。
  • random_state=42:该参数用于设置随机数种子,保证每次运行代码时模型的初始化和训练过程是可重复的。设置相同的random_state值,每次运行代码得到的结果都会一致。

我们加深一点树值

from sklearn.ensemble import GradientBoostingClassifier

clf = GradientBoostingClassifier(n_estimators=40, verbose=True, random_state=42)
clf.fit(X_train, y_train)

输出:
在这里插入图片描述
对于分类任务,可以使用 .staged_decision_function(X) 方法,该方法根据算法的每个顺序构建的版本(即沿着第 i i i 棵树的切割)返回样本 X X X 中对象属于每个可能类别的概率。

该函数返回一个生成器类型的对象。让我们将其转换为“np.array”类型

import numpy as np

array_type_train = np.array(list(clf.staged_decision_function(X_train)))
array_type_test = np.array(list(clf.staged_decision_function(X_test)))

sdf1 = np.argmax(array_type_train, axis=-1)
sdf2 = np.argmax(array_type_test, axis=-1)
"""
sdf1 = np.argmax(np.array([stage for stage in clf.staged_decision_function(X_train)]), axis=-1)
sdf2 = np.argmax(np.array([stage for stage in clf.staged_decision_function(X_test )]), axis=-1)
"""

输出:
sdf1 = np.argmax(np.array([stage for stage in clf.staged_decision_function(X_train)]), axis=-1)
sdf2 = np.argmax(np.array([stage for stage in clf.staged_decision_function(X_test )]), axis=-1)

sdf1.shape

输出:(40, 19999)

让我们计算构建集成算法的每次迭代中质量度量的值并显示结果图。

from sklearn.metrics import accuracy_score

train_accuracy = [accuracy_score(sdf1[i], y_train) for i in range(40)]
test_accuracy = [accuracy_score(sdf2[i], y_test) for i in range(40)]
from matplotlib import pyplot as plt

plt.figure(figsize = (20,12))
plt.title('Score')

plt.plot(train_accuracy, label='train')
plt.plot(test_accuracy, label='test')

plt.legend()

输出:
在这里插入图片描述
其中:

  • staged_decision_function 方法返回一个生成器,它会在每次迭代中返回当前阶段模型对输入数据的决策函数输出。决策函数输出可以理解为模型对每个样本属于不同类别的 “得分”。
  • np.argmax(array_type_train, axis=-1) 会找出每个样本在不同阶段中决策函数输出最大值所在的类别索引。

我们观察到训练集质量图与测试集质量的偏差越来越大。这让我们思考模型可能存在的过度拟合问题。让我们更详细地谈论再培训。

再培训

使用这样的可视化,可以追踪模型变得更加复杂时发生的过度拟合效应。它可以通过训练和测试样本上的误差图之间的差异来检测。

为了说明这种效果,我们将专门选择一个小型训练样本和一个大型测试样本。

X = mnist_train.values[:, 1:]
y = mnist_train.values[:, 0]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.8, shuffle=True, random_state=42
)
clf = GradientBoostingClassifier(n_estimators=50, max_depth=15, verbose=True)

clf.fit(X_train, y_train)

输出:
在这里插入图片描述

array_type_train = np.array(list(clf.staged_decision_function(X_train)))
array_type_test = np.array(list(clf.staged_decision_function(X_test)))

sdf1 = np.argmax(array_type_train, axis=-1)
sdf2 = np.argmax(array_type_test, axis=-1)

train_accuracy = [accuracy_score(sdf1[i], y_train) for i in range(40)]
test_accuracy = [accuracy_score(sdf2[i], y_test) for i in range(40)]

plt.figure(figsize=(20, 12))
plt.title("Score")

plt.plot(train_accuracy, label="train")
plt.plot(test_accuracy, label="test")

plt.legend()

输出:
在这里插入图片描述

from sklearn.metrics import log_loss

test_loss = [
    log_loss(y_test, array_type_test[i], labels=np.arange(10)) for i in range(len(sdf2))
]
train_loss = [
    log_loss(y_train, array_type_train[i], labels=np.arange(10))
    for i in range(len(sdf1))
]

plt.figure(figsize=(20, 12))
plt.title("Loss")

plt.plot(train_loss, label="train")
plt.plot(test_loss, label="test")

plt.legend()

输出:
在这里插入图片描述
在这种情况下,过度拟合的影响非常明显地体现在训练和测试样本上的损失函数图形之间的明显差异上。通过这种方式,可以在保持其他超参数固定的同时找到集成中树的最佳数量。