机器学习算法整理(四)

原创
2021/09/19 00:42
阅读数 3.3K

机器学习算法整理(三)

决策树

什么是决策树

比方说我们在招聘一个机器学习算法工程师的时候,会依照这样的流程进行逐层的评选,从而达到一个树形结构的决策过程。而在这棵树中,它的深度为3.最多通过3次判断,就能将我们的数据进行一个相应的分类。我们在这里每一个节点都可以用yes或者no来回答的问题,实际上我们真实的数据很多内容都是一个具体的数值。对于这些具体的数值,决策树是怎么表征的呢?我们先使用scikit-learn封装的决策树算法进行一下具体的分类。然后通过分类的结果再深入的认识一下决策树。这里我依然先加载鸢尾花数据集。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.scatter(X[y == 2, 0], X[y == 2, 1])
    plt.show()

运行结果

现在我们使用scikit-learn中的决策树来进行分类

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier(max_depth=2, criterion='entropy')
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    plot_decision_boundary(dt_clf, axis=[0.5, 7.5, 0, 3])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.scatter(X[y == 2, 0], X[y == 2, 1])
    plt.show()

运行结果

这就是决策树得到的决策边界。通过这个图,我们来画一个决策树,看看它是如何逐层决策的

这就是决策树在面对属性是一种数值特征的时候是怎样处理的。在这里我们可以看到,在每一个节点上,它选择某一个维度,以及和这个维度相应的阈值,比如说在根节点的时候选的是x这个维度和2.4这个阈值。看我们的数据是大于等于2.4还是小于2.4分成两支。在右分支的子节点,选择了y这个维度和1.8这个阈值,看数据点到了这个节点的话是小于1.8还是大于等于1.8来进行分类。

首先决策树是一个非参数学习算法。其次决策树可以解决分类问题,而且可以天然的解决多分类问题。不像逻辑回归和SVM需要使用OvR或者OvO才能解决多分类问题。同时决策树也可以解决回归问题,我们可以用最终落在这个叶子节点中的所有的样本数据的平均值来当作回归问题的预测值。决策树算法也是具有非常好的可解释性。比如说我们在给用户的信用进行评级,比如说他的信用卡超过3次拖延支付,并且他的驾照平均每年都会被扣去8分,满足这样的条件,他的信用评级就会评为C级。我们可以非常容易的描述出来把样本数据分成某一个类的依据。我们现在的问题就是每个节点到底在哪个维度做划分?我们的数据复杂起来的话可能有成百上千个维度。如果我们选好了一个维度,那么我们要在某个维度的哪个值上做划分?这些都是构建决策树的关键问题。

信息熵

对于处理上面提到的问题——节点到底在哪个维度做划分?某个维度的哪个值上做划分?我们有一种方式来进行处理,那就是信息熵。

信息熵是信息论的基础概念,熵在信息论中代表随机变量不确定度的度量。熵越大,数据的不确定性越高;熵越小,数据的不确定性越低。熵是从物理热力学中引申出来的概念,熵越大,在一个封闭的热力系统中,分子无规则运动越剧烈,即不确定性越高;熵越小,封闭的热力系统中,分子越倾向于静止状态,它们的状态就越确定,即不确定性越低。我们来看一下香农提出来的信息熵公式。

其中表示一个系统中有k类的信息,每一类信息所占的比例就叫做。比如说在我们的鸢尾花数据集中一共有3种鸢尾花,每一种鸢尾花占的比例可能为1/3,则p1,p2,p3就分别等于1/3.由于一定是小于等于1的,log()是小于等于0的,那么H一定是大于等于0的。

对于鸢尾花的这种各占1/3的分类,它的信息熵就为

再比如有一种分类,其中一种分类占1/10,第二种占2/10,第三种占7/10,那么它的信息熵就为

这种分类的信息熵比鸢尾花分类的信息熵要小,意味着这种分类比鸢尾花的分类更确定。这是因为这种分类第三种分类占了70%,所以这种分类是更确定的。而鸢尾花数据每一个类别各占了1/3,所以这个数据整体它的不确定性就越高。我们再来看一组更极端的数据,依然是三类,其中一类占100%。

信息熵为0,说明它的不确定性是最低的,即最确定的。在代码上,我们以二分类为例来看一下这个公式的图像是什么样子的。对于二分类来说,该公式可以转化为

这里x和1-x表示一类和另一类。

import numpy as np
import matplotlib.pyplot as plt

if __name__ == "__main__":

    def entropy(p):
        # 计算二分类信息熵
        return - p * np.log(p) - (1 - p) * np.log(1 - p)

    x = np.linspace(0.01, 0.99, 200)
    plt.plot(x, entropy(x))
    plt.show()

运行结果

从这个图中,我们可以看出,当x=0.5的时候,我们的信息熵来说,如果我们的数据只有两个类别,当每个类别各占一半的时候,此时整个数据的信息熵就是最大的,此时我们整个数据是最不稳定的,确定性是最低的。在0.5两边,无论x值更小还是更大,整个信息熵都在降低,这里因为无论x变小还是变大,我们的数据都偏向于某一类,整体的确定性变高了,所以相应的信息熵就变低了。

明白了这个概念,那么这两个问题就好说了——节点到底在哪个维度做划分?某个维度的哪个值上做划分?我们要让我们的系统划分后使得信息熵降低,换句话说相应的让我们的系统变的更加确定。

比方说在极端情况下,在A、B、C这三个叶子节点的时候,每一个叶子数据都只属于A、B、C这三类的某一类的话,那么此时我们整个系统的信息熵就达到了0。所以我们就是要找在每一个节点上有一个维度,这个维度上有一个取值,根据这个维度和取值,对我们的数据进行划分,划分以后得到的信息熵是所有其他划分方式得到的信息熵的最小值,我们称这样的一个划分就是当前最好的划分。我们只需要对所有的划分可能性进行一次搜索就可以得到这个最好的划分。我们只需要将这方式递归下去就形成了决策树。

使用信息熵寻找最优划分

模拟使用信息熵进行划分

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap
from collections import Counter
from math import log

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier(max_depth=2, criterion='entropy')
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[0.5, 7.5, 0, 3])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()

    def split(X, y, d, value):
        '''
        划分
        :param X: 特征
        :param y: 输出
        :param d: 维度
        :param value: 阈值
        :return:
        '''
        index_a = (X[:, d] <= value)
        index_b = (X[:, d] > value)
        return X[index_a], X[index_b], y[index_a], y[index_b]

    def entropy(y):
        '''
        计算信息熵
        :param y:
        :return:
        '''
        # 建立类别和数量的字典
        counter = Counter(y)
        res = 0
        for num in counter.values():
            p = num / len(y)
            res += - p * log(p)
        return res

    def try_split(X, y):
        '''
        搜索信息熵最小的维度和阈值
        :param X:
        :param y:
        :return:
        '''
        # 定义一个最好的信息熵,初始值为+∞
        best_entropy = float('inf')
        # 定义在哪个维度,哪个阈值上进行划分,初始值都为-1
        best_d, best_v = -1, -1
        # 遍历所有的特征
        for d in range(X.shape[1]):
            # 对d维度上的数据进行排序
            sorted_index = np.argsort(X[:, d])
            # 遍历所有的样本数(不包含第一个样本)
            for i in range(1, len(X)):
                # 拿取候选阈值,为d维的数据中每两个数的中间值,且这两个数不能相等
                if X[sorted_index[i - 1], d] != X[sorted_index[i], d]:
                    v = (X[sorted_index[i - 1], d] + X[sorted_index[i], d]) / 2
                    # 对候选维度和阈值尝试划分
                    X_l, X_r, y_l, y_r = split(X, y, d, v)
                    # 获取对候选维度和候选阈值进行划分后的信息熵
                    e = entropy(y_l) + entropy(y_r)
                    # 获取信息熵最小的维度和阈值
                    if e < best_entropy:
                        best_entropy, best_d, best_v = e, d, v
        return best_entropy, best_d, best_v

    best_entropy, best_d, best_v = try_split(X, y)
    print("best_entropy =", best_entropy)
    print("best_d =", best_d)
    print("best_v =", best_v)

运行结果

best_entropy = 0.6931471805599453
best_d = 0
best_v = 2.45

根据结果,我们可以看出,我们对原始的数据进行划分,是在第0个维度,阈值在2.45的位置进行划分。划分以后信息熵变成了0.693。对照这个结果,我们看一下在scikit-learn中的划分结果

我们可以看到这个决策树第一次划分就是在横轴上,也就是第0个维度,位置大概就是2.45的位置进行的划分。现在我们将划分后的数据进行存储

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap
from collections import Counter
from math import log

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier(max_depth=2, criterion='entropy')
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[0.5, 7.5, 0, 3])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()

    def split(X, y, d, value):
        '''
        划分
        :param X: 特征
        :param y: 输出
        :param d: 维度
        :param value: 阈值
        :return:
        '''
        index_a = (X[:, d] <= value)
        index_b = (X[:, d] > value)
        return X[index_a], X[index_b], y[index_a], y[index_b]

    def entropy(y):
        '''
        计算信息熵
        :param y:
        :return:
        '''
        # 建立类别和数量的字典
        counter = Counter(y)
        res = 0
        for num in counter.values():
            p = num / len(y)
            res += - p * log(p)
        return res

    def try_split(X, y):
        '''
        搜索信息熵最小的维度和阈值
        :param X:
        :param y:
        :return:
        '''
        # 定义一个最好的信息熵,初始值为+∞
        best_entropy = float('inf')
        # 定义在哪个维度,哪个阈值上进行划分,初始值都为-1
        best_d, best_v = -1, -1
        # 遍历所有的特征
        for d in range(X.shape[1]):
            # 对d维度上的数据进行排序
            sorted_index = np.argsort(X[:, d])
            # 遍历所有的样本数(不包含第一个样本)
            for i in range(1, len(X)):
                # 拿取候选阈值,为d维的数据中每两个数的中间值,且这两个数不能相等
                if X[sorted_index[i - 1], d] != X[sorted_index[i], d]:
                    v = (X[sorted_index[i - 1], d] + X[sorted_index[i], d]) / 2
                    # 对候选维度和阈值尝试划分
                    X_l, X_r, y_l, y_r = split(X, y, d, v)
                    # 获取对候选维度和候选阈值进行划分后的信息熵
                    e = entropy(y_l) + entropy(y_r)
                    # 获取信息熵最小的维度和阈值
                    if e < best_entropy:
                        best_entropy, best_d, best_v = e, d, v
        return best_entropy, best_d, best_v

    best_entropy, best_d, best_v = try_split(X, y)
    print("best_entropy =", best_entropy)
    print("best_d =", best_d)
    print("best_v =", best_v)
    # 将第一次划分后的数据进行存储
    X1_l, X1_r, y1_l, y1_r = split(X, y, best_d, best_v)
    # 打印划分后左子树的信息熵
    print(entropy(y1_l))
    # 打印划分后的右子树的信息熵
    print(entropy(y1_r))

运行结果

best_entropy = 0.6931471805599453
best_d = 0
best_v = 2.45
0.0
0.6931471805599453

通过跟图形对比,我们知道第一次划分,我们把所有属于第一类的数据全部划分了进去,它没有不确定性,所以第一次划分的第一类的信息熵为0;而除第一类外的信息熵为0.693

对于第一类我们已经不需要划分了,它的信息熵为0。而对于第一类之外的数据的信息熵为0.693,我们可以继续对其进行划分。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap
from collections import Counter
from math import log

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier(max_depth=2, criterion='entropy')
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[0.5, 7.5, 0, 3])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()

    def split(X, y, d, value):
        '''
        划分
        :param X: 特征
        :param y: 输出
        :param d: 维度
        :param value: 阈值
        :return:
        '''
        index_a = (X[:, d] <= value)
        index_b = (X[:, d] > value)
        return X[index_a], X[index_b], y[index_a], y[index_b]

    def entropy(y):
        '''
        计算信息熵
        :param y:
        :return:
        '''
        # 建立类别和数量的字典
        counter = Counter(y)
        res = 0
        for num in counter.values():
            p = num / len(y)
            res += - p * log(p)
        return res

    def try_split(X, y):
        '''
        搜索信息熵最小的维度和阈值
        :param X:
        :param y:
        :return:
        '''
        # 定义一个最好的信息熵,初始值为+∞
        best_entropy = float('inf')
        # 定义在哪个维度,哪个阈值上进行划分,初始值都为-1
        best_d, best_v = -1, -1
        # 遍历所有的特征
        for d in range(X.shape[1]):
            # 对d维度上的数据进行排序
            sorted_index = np.argsort(X[:, d])
            # 遍历所有的样本数(不包含第一个样本)
            for i in range(1, len(X)):
                # 拿取候选阈值,为d维的数据中每两个数的中间值,且这两个数不能相等
                if X[sorted_index[i - 1], d] != X[sorted_index[i], d]:
                    v = (X[sorted_index[i - 1], d] + X[sorted_index[i], d]) / 2
                    # 对候选维度和阈值尝试划分
                    X_l, X_r, y_l, y_r = split(X, y, d, v)
                    # 获取对候选维度和候选阈值进行划分后的信息熵
                    e = entropy(y_l) + entropy(y_r)
                    # 获取信息熵最小的维度和阈值
                    if e < best_entropy:
                        best_entropy, best_d, best_v = e, d, v
        return best_entropy, best_d, best_v

    best_entropy, best_d, best_v = try_split(X, y)
    print("best_entropy =", best_entropy)
    print("best_d =", best_d)
    print("best_v =", best_v)
    # 将第一次划分后的数据进行存储
    X1_l, X1_r, y1_l, y1_r = split(X, y, best_d, best_v)
    # 打印划分后左子树的信息熵
    print(entropy(y1_l))
    # 打印划分后的右子树的信息熵
    print(entropy(y1_r))
    # 对右子树继续划分
    best_entropy2, best_d2, best_v2 = try_split(X1_r, y1_r)
    print("best_entropy =", best_entropy2)
    print("best_d =", best_d2)
    print("best_v =", best_v2)
    X2_l, X2_r, y2_l, y2_r = split(X1_r, y1_r, best_d2, best_v2)
    # 打印第二次划分后左子树的信息熵
    print(entropy(y2_l))
    # 打印第二次划分后的右子树的信息熵
    print(entropy(y2_r))

运行结果

best_entropy = 0.6931471805599453
best_d = 0
best_v = 2.45
0.0
0.6931471805599453
best_entropy = 0.4132278899361904
best_d = 1
best_v = 1.75
0.30849545083110386
0.10473243910508653

对于第二次划分,我们可以看信息熵降到最低是0.413,是在第1个维度,阈值在1.75的位置进行划分。对比图形,我们可以看到第二次划分是在纵轴上,也就是第1个维度,位置大概就是在1.75的地方进行划分的。并且划分后,两边的信息熵都没降为0,我们可以向更深的方向继续划分。

我们之前在scikit-learn中,max_depth=2,也就是深度值设为了2,也是划分到此为止。当然我们已经知道了使用信息熵来构建决策树,可以使用二叉树的方式来建立这个决策树,并打印这颗二叉树,关于建立的方式可以参考数据结构整理 中关于二分搜索树的内容来进行建立。

基尼系数

除了信息熵可以对决策树的指标进行划分,还有另外一个指标可以对决策树进行划分,这个指标就是基尼系数。基尼系数的公式如下

这个式子和信息熵对应的式子是有同样的性质的,跟信息熵一样,我们先使用几个例子来看一下基尼系数。

通过这三个例子,我们可以看出,基尼系数和信息熵一样可以用来作为数据不确定性的一个度量。我们同样来看一下基尼系数的曲线是什么样子的。对于二分类来说,基尼系数的式子就可以化为

import numpy as np
import matplotlib.pyplot as plt

if __name__ == "__main__":

    def gini(p):
        # 计算二分类基尼系数
        return - 2 * p**2 + 2 * p

    x = np.linspace(0, 1, 200)
    plt.plot(x, gini(x))
    plt.show()

运行结果

现在我们模拟一下使用基尼系数进行划分,看看结果是怎样的。我们依然先使用scikit-learn来绘制一下使用基尼系数的决策树的决策边界。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    dt_clf = DecisionTreeClassifier(max_depth=2, criterion='gini', splitter='best')
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    plot_decision_boundary(dt_clf, axis=[0.5, 7.5, 0, 3])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.scatter(X[y == 2, 0], X[y == 2, 1])
    plt.show()

运行结果

现在我们再使用基尼系数来进行最优划分

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap
from collections import Counter

if __name__ == "__main__":

    iris = datasets.load_iris()
    # 保留鸢尾花数据集的后两个特征
    X = iris.data[:, 2:]
    y = iris.target
    dt_clf = DecisionTreeClassifier(max_depth=2, criterion='gini', splitter='best')
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[0.5, 7.5, 0, 3])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()

    def split(X, y, d, value):
        '''
        划分
        :param X: 特征
        :param y: 输出
        :param d: 维度
        :param value: 阈值
        :return:
        '''
        index_a = (X[:, d] <= value)
        index_b = (X[:, d] > value)
        return X[index_a], X[index_b], y[index_a], y[index_b]

    def gini(y):
        '''
        计算基尼系数
        :param y:
        :return:
        '''
        # 建立类别和数量的字典
        counter = Counter(y)
        res = 1
        for num in counter.values():
            p = num / len(y)
            res -= p**2
        return res

    def try_split(X, y):
        '''
        搜索基尼系数最小的维度和阈值
        :param X:
        :param y:
        :return:
        '''
        # 定义一个最好的基尼系数,初始值为+∞
        best_g = float('inf')
        # 定义在哪个维度,哪个阈值上进行划分,初始值都为-1
        best_d, best_v = -1, -1
        # 遍历所有的特征
        for d in range(X.shape[1]):
            # 对d维度上的数据进行排序
            sorted_index = np.argsort(X[:, d])
            # 遍历所有的样本数(不包含第一个样本)
            for i in range(1, len(X)):
                # 拿取候选阈值,为d维的数据中每两个数的中间值,且这两个数不能相等
                if X[sorted_index[i - 1], d] != X[sorted_index[i], d]:
                    v = (X[sorted_index[i - 1], d] + X[sorted_index[i], d]) / 2
                    # 对候选维度和阈值尝试划分
                    X_l, X_r, y_l, y_r = split(X, y, d, v)
                    # 获取对候选维度和候选阈值进行划分后的基尼系数
                    e = gini(y_l) + gini(y_r)
                    # 获取基尼系数最小的维度和阈值
                    if e < best_g:
                        best_g, best_d, best_v = e, d, v
        return best_g, best_d, best_v

    best_g, best_d, best_v = try_split(X, y)
    print("best_g =", best_g)
    print("best_d =", best_d)
    print("best_v =", best_v)
    # 将第一次划分后的数据进行存储
    X1_l, X1_r, y1_l, y1_r = split(X, y, best_d, best_v)
    # 打印划分后左子树的基尼系数
    print(gini(y1_l))
    # 打印划分后的右子树的基尼系数
    print(gini(y1_r))
    # 对右子树继续划分
    best_g2, best_d2, best_v2 = try_split(X1_r, y1_r)
    print("best_g =", best_g2)
    print("best_d =", best_d2)
    print("best_v =", best_v2)
    X2_l, X2_r, y2_l, y2_r = split(X1_r, y1_r, best_d2, best_v2)
    # 打印第二次划分后左子树的基尼系数
    print(gini(y2_l))
    # 打印第二次划分后的右子树的基尼系数
    print(gini(y2_r))

运行结果

best_g = 0.5
best_d = 0
best_v = 2.45
0.0
0.5
best_g = 0.2105714900645938
best_d = 1
best_v = 1.75
0.1680384087791495
0.04253308128544431

这里通过第一次划分,第一类的基尼系数为0,达到了一个最小值。除第一类的其他类的基尼系数为0.5。第一次划分的维度为0,划分的阈值为2.45。第二次划分的最小基尼系数为0.21,第二次划分的维度为1,划分的阈值为1.75。对于剩下的两个节点,它们的基尼系数依然没有到0,我们可以对这两个节点继续向下划分。

信息熵 vs 基尼系数

通过它们两个的式子,我们可以看出来,信息熵的计算比基尼系数稍慢。在sikit-learn中的决策树默认为基尼系数。在大多数时候,这二者没有特别的效果优劣。对于决策树有其更重要的参数,对比不必纠结这两个参数。

CART与决策树中的超参数

我们使用的这种决策树又叫做CART(Classification And Regression Tree)。它的特点就是在每一个节点上根据某一个维度d和某一个阈值v进行二分。scikit-learn中的决策树都是CART的实现。决策树并不仅仅只有CART这一种实现方式,还有一些如ID3、C4.5、C5.0都是一些其他方式来创建决策树的方法。

当我们建立好了一个决策树,它做预测的时间复杂度为O(logm),m是样本个数。这棵决策树的高度为logm。一个新的样本就会根据这个决策树的高度一步一步做决策一直走到叶子节点。但是我们创建决策树,它的训练的时间复杂度为O(n*m*logm),n就是特征数,这个时间复杂度其实是非常高的。首先这棵树是logm级别的,在每一层上,都要做n*m这么多次的尝试,我们在模拟决策树的划分的时候,要对每一个维度n,每一个样本m进行一个遍历,最终找到那个最佳的划分点,在哪个维度的哪个阈值上进行划分。

还有一个更大的问题就是决策树非常容易产生过拟合,这和KNN算法是一样的,事实上所有的非参数算法都容易产生过拟合。基于这些原因,我们实际在创建决策树的时候,必须对决策树进行剪枝:降低复杂度,解决过拟合。其实之前在鸢尾花数据集的例子中,我们一直在进行剪枝。创建这个决策树的时候,对最大的高度max_depth一直传的是2,就是限制了整棵树的高度,最多是2,这实际上就是一个剪枝手段。使用scikit-learn来创建决策树的时候,所谓的剪枝就是需要对一些参数进行平衡。我们来看看除了max_depth之外还有什么参数可以用于剪枝,它既降低了决策树训练的复杂度,同时大大减少了过拟合的现象。首先我们先画一个月亮的数据集。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

if __name__ == "__main__":

    X, y = datasets.make_moons(noise=0.25, random_state=666)
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

现在我们使用决策树来看一下它的决策边界

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    X, y = datasets.make_moons(noise=0.25, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    plot_decision_boundary(dt_clf, axis=[-1.5, 2.5, -1, 1.5])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

这里需要说明的是,当我们的决策树不传入任何参数的时候,它是使用基尼系数来进行决策的,另外我们也没有传入层数,不做这个设定,决策树就将一直向下划分,直到划分到每一个节点的基尼系数都为0为止。对于上图中的形状显然是不规则的,产生了过拟合。现在我们在构建决策树的时候传入一些参数,看看这些参数是如何遏制过拟合的。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    X, y = datasets.make_moons(noise=0.25, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf2 = DecisionTreeClassifier(max_depth=2)
    dt_clf2.fit(X, y)
    plot_decision_boundary(dt_clf2, axis=[-1.5, 2.5, -1, 1.5])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

很显然现在这个样子比之前规整了很多,不再有了过拟合。不会针对某几个特殊的样本点进行特殊的变化。不过此时又可能是欠拟合,我们继续调整参数。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    X, y = datasets.make_moons(noise=0.25, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf2 = DecisionTreeClassifier(max_depth=2)
    dt_clf2.fit(X, y)
    # plot_decision_boundary(dt_clf2, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf3 = DecisionTreeClassifier(min_samples_split=10)
    dt_clf3.fit(X, y)
    plot_decision_boundary(dt_clf3, axis=[-1.5, 2.5, -1, 1.5])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

这里的min_samples_split的意思就是对于一个节点来说,它至少要有多少个样本数据,我们才对这个节点继续进行拆分下去。上图这样一个决策边界,它显然过拟合的程度是非常低的。对于min_samples_split,我们给它的值调的越高,它就越不容易过拟合。但是把它的值调的太高,就会发生欠拟合的情况。在极端的情况下,如果min_samples_split的值超过样本数总量,那么我们的决策树根本就不需要划分了,此时肯定是一个欠拟合的。现在我们继续调整其他参数。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    X, y = datasets.make_moons(noise=0.25, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf2 = DecisionTreeClassifier(max_depth=2)
    dt_clf2.fit(X, y)
    # plot_decision_boundary(dt_clf2, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf3 = DecisionTreeClassifier(min_samples_split=10)
    dt_clf3.fit(X, y)
    # plot_decision_boundary(dt_clf3, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf4 = DecisionTreeClassifier(min_samples_leaf=6)
    dt_clf4.fit(X, y)
    plot_decision_boundary(dt_clf4, axis=[-1.5, 2.5, -1, 1.5])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

这里的min_samples_leaf的意思就是对于叶子节点来说,它至少应该有几个样本。如果这个值为1就是说对于叶子节点来说至少应该有1个样本,在这种情况下容易过拟合。因为我们真正走这颗决策树走到了叶子节点才决定一个分类,但是其实我们决定的这个分类在这个叶子节点上只有1个样本,它只由这一个样本决定,显然会对某些特殊的样本点会非常的敏感。从上图的图像看,6这个数也有效的遏制了过拟合。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    X, y = datasets.make_moons(noise=0.25, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(dt_clf, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf2 = DecisionTreeClassifier(max_depth=2)
    dt_clf2.fit(X, y)
    # plot_decision_boundary(dt_clf2, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf3 = DecisionTreeClassifier(min_samples_split=10)
    dt_clf3.fit(X, y)
    # plot_decision_boundary(dt_clf3, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf4 = DecisionTreeClassifier(min_samples_leaf=6)
    dt_clf4.fit(X, y)
    # plot_decision_boundary(dt_clf4, axis=[-1.5, 2.5, -1, 1.5])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    dt_clf5 = DecisionTreeClassifier(max_leaf_nodes=4)
    dt_clf5.fit(X, y)
    plot_decision_boundary(dt_clf5, axis=[-1.5, 2.5, -1, 1.5])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

这里的max_leaf_nodes的意思是最多有多少个叶子节点。对于决策树来说,叶子节点越多,决策树整体就越复杂,它就越有可能产生过拟合的情况。

我们在实际使用这些参数的时候也要注意,首先也要避免欠拟合的问题,其次这些参数之间可以互相组合,所以我们可以使用网格搜索的方式来看一看到底使用怎样的参数能得到比较好的结果。决策树本身我们用来分类也好,做回归也好,效果可能无论你调节这些参数,都不会特别的好。

决策树解决回归问题

当我们使用CART的方式将决策树建立出来之后,在每一个叶子节点都包含了若干个数据。如果这些数据它相应的输出值是类别的话,那么我们就可以在叶子节点中让数据进行投票,看哪个类别所包含的数据多,我们就把我们的样本进入到这个叶子节点相应的归为那个类别。如果它的输出是一个具体的数的话,那就是回归问题所解决的问题,那么相应的新的样本点来到这个决策树之后,经过决策树来到某一个叶子节点,就可以用在这个叶子节点中相应的这些数据输出值的平均值来作为一个预测的结果。我们使用波士顿房价数据来进行一个预测。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor

if __name__ == "__main__":

    boston = datasets.load_boston()
    X = boston.data
    y = boston.target
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
    dt_reg = DecisionTreeRegressor()
    dt_reg.fit(X_train, y_train)
    print(dt_reg.score(X_test, y_test))
    print(dt_reg.score(X_train, y_train))

运行结果

0.5934966254524108
1.0

通过打印R方值,我们可以看到对于测试数据集的预测准确度不是一个很好的结果,只有59.3%。但是对于训练数据集的准确度却是100%。这显然是一种过拟合。这个例子也同时告诉我们,决策树是非常容易产生过拟合的。对于回归问题来说,决策树的参数和上一小节是完全一样的,我们可以通过调参来防止过拟合。之前在多项式回归中,我们有过这样的一个模型准确率和模型复杂度的关系图。

意思就是说模型越复杂,它对训练数据的模型准确率越高,拟合的越好。但是对于测试数据却是一个抛物线的形式,它会随着模型复杂度的升高,测试数据模型准确率达到一个峰值,再继续复杂下去就会开始下降。现在我们就用代码来绘制决策树的这两条曲线。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import r2_score

if __name__ == "__main__":

    boston = datasets.load_boston()
    X = boston.data
    y = boston.target
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
    dt_reg = DecisionTreeRegressor()
    dt_reg.fit(X_train, y_train)
    # print(dt_reg.score(X_test, y_test))
    # print(dt_reg.score(X_train, y_train))

    maxSampleLeaf = 506
    train_scores = []
    test_scores = []
    for i in range(1, maxSampleLeaf + 1):
        dt_reg = DecisionTreeRegressor(min_samples_leaf=i)
        dt_reg.fit(X_train, y_train)
        y_train_predict = dt_reg.predict(X_train)
        train_scores.append(r2_score(y_train, y_train_predict))
        test_scores.append(dt_reg.score(X_test, y_test))
    plt.plot([i for i in range(1, maxSampleLeaf + 1)], train_scores, label='train')
    plt.plot([i for i in range(1, maxSampleLeaf + 1)], test_scores, label='test')
    plt.xlim(maxSampleLeaf, 1)
    plt.legend()
    plt.show()

运行结果

min_samples_leaf的意思就是对于叶子节点来说,它至少应该有几个样本。我们知道该值越小越容易过拟合,模型复杂度越高,所以我们图形中的横轴是从大到小,最小到1来排序的。意思就是说我们的模型复杂度是从低到高的一个过程,其中训练数据集的模型准确率是不断上升的,而测试数据集也是在不断上升但是到了一个峰值就开始下降。这里这个下降可能看的不是特别明显,现在我们来调低min_samples_leaf的最大值。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import r2_score

if __name__ == "__main__":

    boston = datasets.load_boston()
    X = boston.data
    y = boston.target
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
    dt_reg = DecisionTreeRegressor()
    dt_reg.fit(X_train, y_train)
    # print(dt_reg.score(X_test, y_test))
    # print(dt_reg.score(X_train, y_train))

    maxSampleLeaf = 506
    train_scores = []
    test_scores = []
    for i in range(1, maxSampleLeaf + 1):
        dt_reg = DecisionTreeRegressor(min_samples_leaf=i)
        dt_reg.fit(X_train, y_train)
        y_train_predict = dt_reg.predict(X_train)
        train_scores.append(r2_score(y_train, y_train_predict))
        test_scores.append(dt_reg.score(X_test, y_test))
    # plt.plot([i for i in range(1, maxSampleLeaf + 1)], train_scores, label='train')
    # plt.plot([i for i in range(1, maxSampleLeaf + 1)], test_scores, label='test')
    # plt.xlim(maxSampleLeaf, 1)
    # plt.legend()
    # plt.show()

    maxSampleLeaf = 100
    train_scores = []
    test_scores = []
    for i in range(1, maxSampleLeaf + 1):
        dt_reg = DecisionTreeRegressor(min_samples_leaf=i)
        dt_reg.fit(X_train, y_train)
        y_train_predict = dt_reg.predict(X_train)
        train_scores.append(r2_score(y_train, y_train_predict))
        test_scores.append(dt_reg.score(X_test, y_test))
    plt.plot([i for i in range(1, maxSampleLeaf + 1)], train_scores, label='train')
    plt.plot([i for i in range(1, maxSampleLeaf + 1)], test_scores, label='test')
    plt.xlim(maxSampleLeaf, 1)
    plt.legend()
    plt.show()

运行结果

现在我们换一个参数来看这个图

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import r2_score

if __name__ == "__main__":

    boston = datasets.load_boston()
    X = boston.data
    y = boston.target
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
    dt_reg = DecisionTreeRegressor()
    dt_reg.fit(X_train, y_train)
    # print(dt_reg.score(X_test, y_test))
    # print(dt_reg.score(X_train, y_train))

    maxSampleLeaf = 506
    train_scores = []
    test_scores = []
    for i in range(1, maxSampleLeaf + 1):
        dt_reg = DecisionTreeRegressor(min_samples_leaf=i)
        dt_reg.fit(X_train, y_train)
        y_train_predict = dt_reg.predict(X_train)
        train_scores.append(r2_score(y_train, y_train_predict))
        test_scores.append(dt_reg.score(X_test, y_test))
    # plt.plot([i for i in range(1, maxSampleLeaf + 1)], train_scores, label='train')
    # plt.plot([i for i in range(1, maxSampleLeaf + 1)], test_scores, label='test')
    # plt.xlim(maxSampleLeaf, 1)
    # plt.legend()
    # plt.show()

    maxSampleLeaf = 100
    train_scores = []
    test_scores = []
    for i in range(1, maxSampleLeaf + 1):
        dt_reg = DecisionTreeRegressor(min_samples_leaf=i)
        dt_reg.fit(X_train, y_train)
        y_train_predict = dt_reg.predict(X_train)
        train_scores.append(r2_score(y_train, y_train_predict))
        test_scores.append(dt_reg.score(X_test, y_test))
    # plt.plot([i for i in range(1, maxSampleLeaf + 1)], train_scores, label='train')
    # plt.plot([i for i in range(1, maxSampleLeaf + 1)], test_scores, label='test')
    # plt.xlim(maxSampleLeaf, 1)
    # plt.legend()
    # plt.show()

    maxSamplesSplit = 300
    train_scores = []
    test_scores = []
    for i in range(2, maxSamplesSplit + 1):
        dt_reg = DecisionTreeRegressor(min_samples_split=i)
        dt_reg.fit(X_train, y_train)
        y_train_predict = dt_reg.predict(X_train)
        train_scores.append(r2_score(y_train, y_train_predict))
        test_scores.append(dt_reg.score(X_test, y_test))
    plt.plot([i for i in range(2, maxSamplesSplit + 1)], train_scores, label='train')
    plt.plot([i for i in range(2, maxSamplesSplit + 1)], test_scores, label='test')
    plt.xlim(maxSamplesSplit, 2)
    plt.legend()
    plt.show()

运行结果

min_samples_split的意思就是对于一个节点来说,它至少要有多少个样本数据,我们才对这个节点继续进行拆分下去。这个值越大,模型复杂度越简单;越小,模型复杂度越复杂,所以上图的模型复杂度也是从小到大。同样对于训练数据集的模型准确率也是不断上升的,但是对于测试数据集来说,也是不断上升,然后到达峰值然后开始下降。

决策树的局限性

从这个图中,我们会发现决策树的决策边界都是横平竖直的。反映在二维图像中,决策边界都一定是跟横轴或纵轴是平行的。这个道理也是很显然的,因为对于决策树来说,每一个都是在某一个维度上选择某一个阈值进行划分,小于这个阈值进入一棵子树,大于这个阈值,进入另外一棵子树。对于这样的决策边界显然是有局限性的。

对于这样的四个点,如果用决策树来进行分类的话,就是这个样子

首先决策树会将数据分成上下两部分,上半部分已经不可再分,它的信息熵或者基尼系数为0。对于下半部分又分成了两部分,最后就是上图的样子。然而对于这四个点来说,它合理的决策边界应该是一根斜线。

对于决策树来说,它是永远不会产生一根斜线这样的决策边界的。更严重的事情,假设我们的数据集是这个样子

这样的数据是非常容易区分的,使用决策树是这个样子

同样是这个分布,但是如果稍微有一些倾斜

此时当我们再使用决策树的话,那么划分结果很有可能就是这个样子

这个决策边界就是横平竖直的样子,这样一个决策边界很有可能是不对的,对比于在中间画一条斜线的决策边界,在两侧逼近无限远的时候,会进行大量的错误划分。另外决策树还有一个局限性就是对个别数据敏感。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    iris = datasets.load_iris()
    X = iris.data[:, 2:]
    y = iris.target
    tree_clf = DecisionTreeClassifier(max_depth=2, criterion='entropy', splitter='best')
    tree_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    plot_decision_boundary(tree_clf, axis=[0.5, 7.5, 0, 3])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.scatter(X[y == 2, 0], X[y == 2, 1])
    plt.show()

运行结果

现在我们来找出一个点,把这个点给删除掉。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from matplotlib.colors import ListedColormap

if __name__ == "__main__":

    iris = datasets.load_iris()
    X = iris.data[:, 2:]
    y = iris.target
    tree_clf = DecisionTreeClassifier(max_depth=2, criterion='entropy', splitter='best')
    tree_clf.fit(X, y)

    def plot_decision_boundary(model, axis):
        # 绘制不规则决策边界
        x0, x1 = np.meshgrid(
            np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(-1, 1),
            np.linspace(axis[2], axis[3], int((axis[3] - axis[2]) * 100)).reshape(-1, 1)
        )
        X_new = np.c_[x0.ravel(), x1.ravel()]
        y_predict = model.predict(X_new)
        zz = y_predict.reshape(x0.shape)
        custom_cmap = ListedColormap(['#EF9A9A', '#FFF59D', '#90CAF9'])
        plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)

    # plot_decision_boundary(tree_clf, axis=[0.5, 7.5, 0, 3])
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.scatter(X[y == 2, 0], X[y == 2, 1])
    # plt.show()

    X_new = np.delete(X, 138, axis=0)
    y_new = np.delete(y, 138)
    print(X_new.shape)
    print(y_new.shape)
    tree_clf2 = DecisionTreeClassifier(max_depth=2, criterion='entropy', splitter='best')
    tree_clf2.fit(X_new, y_new)
    plot_decision_boundary(tree_clf2, axis=[0.5, 7.5, 0, 3])
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.scatter(X[y == 2, 0], X[y == 2, 1])
    plt.show()

运行结果

在这里可以看到,我们只删除了其中的一个样本点,它的决策边界就变的跟之前完全不同了。这也再次应证了了对于决策树算法来说,对于个别的样本点它可能是很敏感的,其实这是所有的非参数学习的缺点,所以它高度依赖调参才能得到一个较好的模型。一般决策树更重要的应用是使用集成学习的方式来创建一种随机森林的算法,而随机森林算法可以得到非常好的学习结果。

集成学习和随机森林

什么是集成学习

我们之前已经学习了诸多的机器学习算法,对于每一种机器学习算法,它们考虑问题的方式都略微有所不同。所以对于同一个问题,不同的算法可能给出不同的结果。那么此时我们听哪种算法的结果呢?此时我们可以把多个算法集中起来,让不同的算法对同一个问题都进行一下运算,看看最终结果是怎样的,最终少数服从多数。这就是集成学习的思路。

scikit-learn为我们提供了一个方便的接口——Voting Classifier。它就是基于不同的算法进行投票的一种分类器。我们用代码来看一下这个集成学习的使用,先画出月亮的数据集

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

现在我们使用多种算法的分类决策,看看最终结果如何

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    # 逻辑回归
    log_clf = LogisticRegression()
    log_clf.fit(X_train, y_train)
    print(log_clf.score(X_test, y_test))
    # 支撑向量机SVM
    svm_clf = SVC()
    svm_clf.fit(X_train, y_train)
    print(svm_clf.score(X_test, y_test))
    # 决策树
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X_train, y_train)
    print(dt_clf.score(X_test, y_test))
    # 用三种模型进行预测
    y_predict1 = log_clf.predict(X_test)
    y_predict2 = svm_clf.predict(X_test)
    y_predict3 = dt_clf.predict(X_test)
    # 综合三个算法,少数服从多数得到分类预测值
    y_predict = np.array((y_predict1 + y_predict2 + y_predict3) >= 2, dtype='int')
    print(accuracy_score(y_test, y_predict))

运行结果

0.864
0.896
0.856
0.912

通过结果,我们可以看出通过三种模型计算出来的分类准确度经过投票,如果预测的结果至少两个一致,就确定该类型。最终结果达到了91.2%。现在我们来看看scikit-learn中为我们提供的集成学习接口

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.ensemble import VotingClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    # 逻辑回归
    log_clf = LogisticRegression()
    log_clf.fit(X_train, y_train)
    print(log_clf.score(X_test, y_test))
    # 支撑向量机SVM
    svm_clf = SVC()
    svm_clf.fit(X_train, y_train)
    print(svm_clf.score(X_test, y_test))
    # 决策树
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X_train, y_train)
    print(dt_clf.score(X_test, y_test))
    # 用三种模型进行预测
    y_predict1 = log_clf.predict(X_test)
    y_predict2 = svm_clf.predict(X_test)
    y_predict3 = dt_clf.predict(X_test)
    # 综合三个算法,少数服从多数得到分类预测值
    y_predict = np.array((y_predict1 + y_predict2 + y_predict3) >= 2, dtype='int')
    print(accuracy_score(y_test, y_predict))
    # 使用scikit-learn提供的集成学习接口
    voting_clf = VotingClassifier(estimators=[
        ('log_clf', LogisticRegression()),
        ('svm_clf', SVC()),
        ('dt_clf', DecisionTreeClassifier())
    ], voting='hard')
    voting_clf.fit(X_train, y_train)
    print(voting_clf.score(X_test, y_test))

运行结果

0.864
0.896
0.872
0.904
0.904

通过代码,我们可以看到这个集成学习接口非常类似于之前的Pipeline,它把所有的学习算法放入其中,最终得到的就是投票后的分类准确度。

Soft Voting Classifier

在上一小节中,我们创建了一个VotingClassifier的时候,使用了一个参数——voting='hard'。这种少数服从多数的集成学习就被称为Hard Voting。但是还有一种更重要的方式,叫做Soft Voting。在很多情况下,少数服从多数并不是最合理的,更合理的投票,应该有权值。对于一个样本数据点,不同的模型可能会根据概率来分成不同的类别。

根据这五个模型,把一个数据分成了五个分类,根据概率的不同,模型1把这个数据肯定划分到了A类,模型2划分到了B类,模型3划分到了B类,模型4划分到了A类,模型5划分到了B类,根据Hard Voting,少数服从多数,有2个模型划分成了A类,有3个模型划分成了B类,我们的集成学习就应该把这个数据最终划分成B类。但是看数据,虽然只有两个模型把这个数据划分成了A类,但是这两个模型都是非常的确定的,概率都很高,99%和90%;相反看另外三个模型把这个数据划分成B类,但是概率都不够高,只有51%、60%、70%。而Soft Voting就是以概率作为权值来进行划分的。

通过加权平均的概率,我们能够看到,这个数据被分成A类的概率为61.6%,而被分成B类的概率只有38.4%。所以Soft Voting最终把结果划分为A类。

既然要以概率为权值,这就要求我们的机器学习算法都能得出分类的概率。对于逻辑回归自不必说,它本身就是以概率来进行划分的。

kNN算法,计算概率的方式就是看离新来的样本点最近的多数分类的样本点数量/离新来的样本最近的总样本数量,下面这个图的k=3,概率就是2/3=66.6%

决策树,计算概率的方式跟kNN是非常相似的,在一个叶子节点所包含的数据是含有多个数据的,此时该节点的基尼系数也好,信息熵也好,可能还不为0。换句话说在这个叶子节点中是包含有不同类的数据的,在这种情况下哪个类的数据量大,相应的我们就把样本点分类给哪一个类。概率就是叶子节点中占比例最大的分类数量/该叶子节点所有的数据点的数量。

支撑向量机SVM,它本身是没有考虑概率的,是计算一个margin的最大值。不过虽然如此,SVM算法依然可以使用一些方式来计算出概率。这样的方式的代价是更多的计算资源。具体略过计算方式,以后补充。在scikit-learn中,SVC类中有一个probability,它默认值是False,如果我们给它设为True的话,对于SVC类来说,它就可以估计出对于每一个样本分给某一个类的概率是多少,但是这样做的话会牺牲一定的计算时间。

这样对于这些算法,或者是直接或者是间接都可以计算出数据分给某一个类对应的概率是多少。有了这个概率,我们就可以相应的使用Soft Voting的方式。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.ensemble import VotingClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    # 逻辑回归
    log_clf = LogisticRegression()
    log_clf.fit(X_train, y_train)
    print(log_clf.score(X_test, y_test))
    # 支撑向量机SVM
    svm_clf = SVC()
    svm_clf.fit(X_train, y_train)
    print(svm_clf.score(X_test, y_test))
    # 决策树
    dt_clf = DecisionTreeClassifier()
    dt_clf.fit(X_train, y_train)
    print(dt_clf.score(X_test, y_test))
    # 用三种模型进行预测
    y_predict1 = log_clf.predict(X_test)
    y_predict2 = svm_clf.predict(X_test)
    y_predict3 = dt_clf.predict(X_test)
    # 综合三个算法,少数服从多数得到分类预测值
    y_predict = np.array((y_predict1 + y_predict2 + y_predict3) >= 2, dtype='int')
    print(accuracy_score(y_test, y_predict))
    # 使用scikit-learn提供的集成学习接口
    # 使用hard voting
    voting_clf = VotingClassifier(estimators=[
        ('log_clf', LogisticRegression()),
        ('svm_clf', SVC()),
        ('dt_clf', DecisionTreeClassifier())
    ], voting='hard')
    voting_clf.fit(X_train, y_train)
    print(voting_clf.score(X_test, y_test))
    # 使用soft voting
    voting_clf2 = VotingClassifier(estimators=[
        ('log_clf', LogisticRegression()),
        ('svm_clf', SVC(probability=True)),
        ('dt_clf', DecisionTreeClassifier(random_state=666))
    ], voting='soft')
    voting_clf2.fit(X_train, y_train)
    print(voting_clf2.score(X_test, y_test))

运行结果

0.864
0.896
0.856
0.904
0.904
0.904

Bagging和Pasting

无论是之前说的Hard Voting还是Soft Voting依然是存在问题的,它最大的问题就是虽然有很多机器学习方法,但是从投票的角度来看,仍然不够多。如果我们要尽量的保证最终有一个好的结果,我们希望有更多的投票者,成百上千甚至上万个投票者,才能保证我们最终的结果更加的可信。概率论中大数定理也指出只有试验次数足够多,频率才能收敛于概率,概率是频率的极限。这样我们就要创建更多的子模型,集成更多的子模型的意见。子模型之间不能一致,子模型之间要有差异性。如果就算我们有上万个子模型,但是所有的子模型它本质都是一个模型的话,那么这个投票最终反映出来的仍然只是这一个算法,一个模型的意见。这种投票其实是没有意义的。怎样才能创造出更多的子模型,又能保证这些子模型存在差异性呢?

一个简单的方法就是每个子模型只看样本数据的一部分。假如有500个样本的话,每个子模型只看100个样本数据,而相应的每个子模型可以是共同的一个算法,这样我们就可以创建很多的子模型,而且这些子模型之间是存在差异性的。这是因为每一个子模型所看的样本数据是不一样的100个样本数据,训练出来的模型肯定是有一定的差异性。当然这么做,每一个子模型的准确率就会变低,但这却是集成学习的一个威力。集成学习集成了诸多的子模型来投票决定最终的分类结果。在这个过程中,对于每一个子模型来说,并不需要太高的准确率。

如果每个子模型只有51%的准确率,如果我们只有1个子模型,那么它整体的准确率就是51%。但是如果我们有3个子模型,这3个子模型彼此是独立的,并且任意一个子模型的分类都是两个,它满足重复的贝努力试验,所以这是一个二项分布(关于二项分布的内容请参考概率论整理 ),从整体来说3个都投票和2个投票,对于一个样本数据都会被分成一类,根据二项分布的公式,这里当K=3的时候P(X=3)=,当k=2的时候P(X=2)=,所以整体的准确率就变成了=51.5%。我们可以看到子模型数从1变成3,整体准确率有了提高,虽然这个提高只有0.5%。那如果我们有500个子模型,这里如果有251个或以上的子模型投相同票,对于一个样本数据都会被分成一类。根据二项分布公式加和,整体准确率就为=65.6%。如果每个子模型只有60%的准确率,也是500个子模型,那么整体准确率就为=99.999%。通过这样的计算,可以看到集成学习的威力,我们每一个子模型不需要有太高的准确率。不过实际中,每一个模型的准确率是有高有低的,不可能都达到60%,可能有的模型的准确率会低于50%。

每个子模型只看样本数据的一部分,有两种方式来看样本数据的一部分,一种方式是放回取样,称为Bagging;另一种方式是不放回取样,称为Pasting。通常来讲Bagging是更常用的。因为如果只有500个样本,使用Pasting,那么只能使用5个子模型,如果采用Bagging,那么可以使用成千上万个子模型都是可以的,这是Bagging的优点之一。Bagging的第二个优点就是没有那么强烈的依赖于随机,对于Pasting来分500个样本,那么怎样分是非常强烈的影响Pasting集成学习所得到的结果。但是Bgging每一次都是从500个样本中取100个样本来进行学习,这个过程相当于重复了非常多次,那么在重复非常多次的过程中,其实一定程度地将这个随机过程所带来的问题给取消了,这也是Bagging更常用的原因。在统计学中,放回取样也有一个名词,叫bootstrap。在scikit-learn中控制这个放回取样还是不放回取样,就是需要调一个叫做bootstrap的参数。现在我们用代码来看一下Bagging的使用方法,先画出月亮的数据。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

if __name__ == "__main__":

    X , y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

现在我们来使用Bagging并使用决策树这一种算法来进行集成学习。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    dec_tree_clf = DecisionTreeClassifier()
    dec_tree_clf.fit(X_train, y_train)
    print(dec_tree_clf.score(X_test, y_test))
    # 使用500个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)
    bagging_clf.fit(X_train, y_train)
    print(bagging_clf.score(X_test, y_test))

运行结果

0.872
0.912

这里我们可以看到,当我们使用单个决策树的时候,它的分类准确度为87.2%。当我们使用了500个决策树的子模型,每个子模型只学习100个训练数据,整体的分类准确度为91.2%。现在我们来增加子模型的数量。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    dec_tree_clf = DecisionTreeClassifier()
    dec_tree_clf.fit(X_train, y_train)
    print(dec_tree_clf.score(X_test, y_test))
    # 使用500个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)
    bagging_clf.fit(X_train, y_train)
    print(bagging_clf.score(X_test, y_test))
    # 使用5000个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf2 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000, max_samples=100, bootstrap=True)
    bagging_clf2.fit(X_train, y_train)
    print(bagging_clf2.score(X_test, y_test))

运行结果

0.824
0.912
0.928

通过结果,我们可以看到使用5000个子模型,它的分类准确度达到了92.8%,这里我们会发现,这个准确率并没有之前在理论上的99.999%,这是因为在具体学习的过程中,不管怎样还是和我们的样本数据相关的,如果我们的样本数据本身存在噪音或者错误的样本的话,依然还是会犯更大的错误。不过这样一种集成学习的策略通常都是有很好的结果的。

oob(Out-of-Bag)和关于Bagging的更多讨论

OOB Out-of-Bag:放回取样有一定的概率导致一部分样本很有可能没有取到。平均大约有37%的样本没有取到。这37%的样本就叫OOB。

存在这种情况,我们就不需要使用测试数据集,而使用这部分没有取到的样本做测试/验证。在scikit-learn中有一个属性oob_score_,使用这个属性就可以直接来看Bagging集成学习来说,使用OOB的样本作为测试数据集最终得到的结果是怎样的。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
import timeit

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    dec_tree_clf = DecisionTreeClassifier()
    dec_tree_clf.fit(X_train, y_train)
    print(dec_tree_clf.score(X_test, y_test))
    # 使用500个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)
    bagging_clf.fit(X_train, y_train)
    print(bagging_clf.score(X_test, y_test))
    # 使用5000个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    start_time = timeit.default_timer()
    bagging_clf2 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000, max_samples=100, bootstrap=True)
    bagging_clf2.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf2.score(X_test, y_test))
    # 使用oob进行测试
    bagging_clf3 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                     max_samples=100, bootstrap=True,
                                     oob_score=True)
    bagging_clf3.fit(X, y)
    print(bagging_clf3.oob_score_)

运行结果

0.888
0.904
5.212921124
0.912
0.918

这里我们可以看到,我们的bagging_clf3并没有使用训练数据集来进行训练,而是使用了全部的数据集,并打印了oob_score_的属性。另外当我们对5000个子模型进行训练的时候,它的训练时间需要5秒多,这算是比较慢的了。

Bagging的更多探讨

由于Bagging的集成学习,使用的大量的子模型,它们彼此之间是没有关联的,所以极易进行并行化处理,而scikit-learn中对并行化处理的参数就是n_jobs,这个值传入几就是使用几个核,传入-1就是使用所有的核。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
import timeit

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    dec_tree_clf = DecisionTreeClassifier()
    dec_tree_clf.fit(X_train, y_train)
    print(dec_tree_clf.score(X_test, y_test))
    # 使用500个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)
    bagging_clf.fit(X_train, y_train)
    print(bagging_clf.score(X_test, y_test))
    # 使用5000个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    start_time = timeit.default_timer()
    bagging_clf2 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000, max_samples=100, bootstrap=True)
    bagging_clf2.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf2.score(X_test, y_test))
    # 使用oob进行测试
    bagging_clf3 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                     max_samples=100, bootstrap=True,
                                     oob_score=True)
    bagging_clf3.fit(X, y)
    print(bagging_clf3.oob_score_)
    # Bagging使用并行处理
    start_time = timeit.default_timer()
    bagging_clf4 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000,
                                     max_samples=100, bootstrap=True, n_jobs=-1)
    bagging_clf4.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf4.score(X_test, y_test))

运行结果

0.848
0.912
4.504785647
0.912
0.922
1.7601140050000001
0.912

在这里我们对5000个子模型训练加入了并行处理,并使用了所有的核,我们可以看到之前不使用并行处理,需要4.5秒,而使用了并行处理只需要1.76秒,大大提升了效率,节省了时间。

之前我们都是让每一个子模型去看更小的样本数据集,其实还有其他产生差异化的方式。一个非常重要的方式就是针对特征进行随机采样(Random Subspaces)。使用这种方式通常都在于我们的样本数据的特征非常多的情况下,比如说在图像识别领域,每一张图片上每一个像素点都是一个特征,我们每一个样本相应的特征量就比较大,那么我们在Bagging的时候就可以对这个特征进行随机的采样。Random Subspaces的意思就是一个随机的子空间,我们本身所有的特征构成了一个特征空间,每一次我们在取样的时候相当于是在一个子空间进行取样。

综合之前的bootstrap对样本的数量进行随机取样,又对样本的特征进行随机取样,被称为Random Patches

Patches是补丁的意思,这也是一种形象的说法,我们的数据本身就是一个矩阵,每一行是一个样本,每一列代表一个特征,Random Patches这种方式其实就是既在行的维度上随机,又在列的维度上随机,得到的结果就好像上图一样,看起来就好像是一块布上的补丁,这些补丁是随机的。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
import timeit

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    dec_tree_clf = DecisionTreeClassifier()
    dec_tree_clf.fit(X_train, y_train)
    print(dec_tree_clf.score(X_test, y_test))
    # 使用500个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)
    bagging_clf.fit(X_train, y_train)
    print(bagging_clf.score(X_test, y_test))
    # 使用5000个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    start_time = timeit.default_timer()
    bagging_clf2 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000, max_samples=100, bootstrap=True)
    bagging_clf2.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf2.score(X_test, y_test))
    # 使用oob进行测试
    bagging_clf3 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                     max_samples=100, bootstrap=True,
                                     oob_score=True)
    bagging_clf3.fit(X, y)
    print(bagging_clf3.oob_score_)
    # Bagging使用并行处理
    start_time = timeit.default_timer()
    bagging_clf4 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000,
                                     max_samples=100, bootstrap=True, n_jobs=-1)
    bagging_clf4.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf4.score(X_test, y_test))
    # Bagging对特征进行随机采样
    random_subspaces_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                     max_samples=500, bootstrap=True,
                                     oob_score=True, n_jobs=-1,
                                     max_features=1, bootstrap_features=True)
    random_subspaces_clf.fit(X, y)
    print(random_subspaces_clf.oob_score_)

运行结果

0.848
0.912
4.455713805
0.912
0.92
1.8603084690000005
0.912
0.818

由于我们的样本本身只有2个特征,在这里采用对特征进行随机采样其实是不合适的,结果也看出分类准确度只有81.8%,这里只是为了看看这种对特征进行随机采样的使用方法。这里的max_features就是随机采样的特征数,bootstrap_features就是对特征采样进行放回取样的意思。max_samplesn_estimators相等都为500,表示这里并没有对样本数进行随机采样,而是对所有的子模型采用了全部的样本。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier
import timeit

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=42)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
    dec_tree_clf = DecisionTreeClassifier()
    dec_tree_clf.fit(X_train, y_train)
    print(dec_tree_clf.score(X_test, y_test))
    # 使用500个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    bagging_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, bootstrap=True)
    bagging_clf.fit(X_train, y_train)
    print(bagging_clf.score(X_test, y_test))
    # 使用5000个决策树子模型,每个模型只学习100个样本数据,采用放回取样的方式
    start_time = timeit.default_timer()
    bagging_clf2 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000, max_samples=100, bootstrap=True)
    bagging_clf2.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf2.score(X_test, y_test))
    # 使用oob进行测试
    bagging_clf3 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                     max_samples=100, bootstrap=True,
                                     oob_score=True)
    bagging_clf3.fit(X, y)
    print(bagging_clf3.oob_score_)
    # Bagging使用并行处理
    start_time = timeit.default_timer()
    bagging_clf4 = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5000,
                                     max_samples=100, bootstrap=True, n_jobs=-1)
    bagging_clf4.fit(X_train, y_train)
    print(timeit.default_timer() - start_time)
    print(bagging_clf4.score(X_test, y_test))
    # Bagging对特征进行随机采样
    random_subspaces_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                     max_samples=500, bootstrap=True,
                                     oob_score=True, n_jobs=-1,
                                     max_features=1, bootstrap_features=True)
    random_subspaces_clf.fit(X, y)
    print(random_subspaces_clf.oob_score_)
    # 既对特征进行随机采样,又对样本数量进行随机采样
    random_patches_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
                                             max_samples=100, bootstrap=True,
                                             oob_score=True, n_jobs=-1,
                                             max_features=1, bootstrap_features=True)
    random_patches_clf.fit(X, y)
    print(random_patches_clf.oob_score_)

运行结果

0.864
0.928
4.535023132
0.912
0.92
1.7864520659999998
0.912
0.842
0.854

这里我们将max_samples调回了100,表示既对样本特征进行了随机采样,又对样本数量进行随机采样。

随机森林和Extra-Trees

之前我们使用Bagging的时候,使用的基础分类器都是决策树(Base Estimator:Decision Tree),那么我们整个集成学习就集成了成百上千个决策树,对于这样的集成学习的模型,称为随机森林。scikit-learn还专门封装了一个随机森林的类,还提供了更多的随机性。对于训练的每一棵决策树,在训练的节点上进行一个划分,在随机的特征子集上寻找最优划分特征(阈值)。这样就不是在所有的特征上去寻找最优划分,这样就增加了集成学习中每一个子模型的随机性。在集成学习中,每一个子模型和其他的子模型越有差异,其实是越好的。创造这种差异的一个方式就是增加这种随机性。现在我们依然画出一个月亮的数据集。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

现在我们使用随机森林的模型

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.ensemble import RandomForestClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    rf_cls = RandomForestClassifier(n_estimators=500, random_state=666,
                                    oob_score=True, n_jobs=-1)
    rf_cls.fit(X, y)
    print(rf_cls.oob_score_)

    rf_cls2 = RandomForestClassifier(n_estimators=500, random_state=666, max_leaf_nodes=16,
                                    oob_score=True, n_jobs=-1)
    rf_cls2.fit(X, y)
    print(rf_cls2.oob_score_)

运行结果

0.892
0.906

在这里我们可以看到,在第二个随机森林模型中,我们添加了max_leaf_nodes的,这说明随机森林模型包含了决策树模型的参数,可以供我们调节,结果也说明加入了这些参数调节,分类准确度也得到了提高。

Extra-Trees

Extra-Trees依然是使用的基础分类器都是决策树(Base Estimator:Decision Tree),它跟上面的随机森林最大的不同就是决策树在节点划分上,使用随机的特征和随机的阈值,我们上面的随机森林虽然也是使用随机的特征,但是阈值是最优划分的。这种方式显然提供了额外的随机性,抑制过拟合,但缺点就是增大了偏差(bias),遏制了方差。对于是否要使用Extra-Trees,要根据我们的数据,要解决的问题是否适合使用Extra-Trees这样的一种方式,要根据实际情况进行判断。由于使用了随机的特征,随机的阈值,无需计算,比起随机森林而言,它有更快的训练速度。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()

    rf_cls = RandomForestClassifier(n_estimators=500, random_state=666,
                                    oob_score=True, n_jobs=-1)
    rf_cls.fit(X, y)
    print(rf_cls.oob_score_)

    rf_cls2 = RandomForestClassifier(n_estimators=500, random_state=666, max_leaf_nodes=16,
                                    oob_score=True, n_jobs=-1)
    rf_cls2.fit(X, y)
    print(rf_cls2.oob_score_)

    et_cls = ExtraTreesClassifier(n_estimators=500, bootstrap=True,
                                  oob_score=True, random_state=666)
    et_cls.fit(X, y)
    print(et_cls.oob_score_)

运行结果

0.892
0.906
0.892

集成学习解决回归问题

我们之前都是在说用集成学习解决分类问题,但集成学习也可以解决回归问题,在scikit-learn中,对应于

from sklearn.ensemble import BaggingClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier

这三个类就有对应下面这三个类来解决回归问题

from sklearn.ensemble import BaggingRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import ExtraTreesRegressor

Ada Boosting和Gradient Boosting

除了之前说的Bagging集成学习的思路,还有另外一类集成学习的思路,叫做Boosting。它也是要集成多个模型,Boosting各个模型之间不是一个独立的关系,而是一个相互增强的关系,每个模型都在尝试增强(Boosting)整体的效果。Boosting中最典型的例子就是Ada Boosting

首先我们有原始的数据集(下一),所有数据的权重一样,可以用某一种学习的算法得到一个模型(上一),所有的算法都犯错误,就会得到一个能够被成功预测的点(浅色的点)和无法被预测的点,和模型差距比较远的点(深色的点)(下二),在这样的样本点中,数据和数据之间产生了权重的差别,我们让上一个模型中没有被预测的点的权值增大一些,而在上一个模型中被预测的点的权值减少一些,这样形成的新的样本数据,我们再使用某一种学习算法进行学习,由于有权值的差距,我们就会得到一个新的模型(上二),新的模型同样有一些数据被准确预测了,也有一些数据和真值差距比较大。这样又形成了一组新的数据(下三),这组数据中的权重又有差距,这组数据再使用一个学习算法又得到一个新的模型(上三),再一次有些样本点被成功预测,有些样本点没有被预测。这样又得到了新的数据(下四),这个数据中每个样本点又被赋上了新的权值。这个过程依次进行下去,这个过程每一次生成的子模型其实都是在想办法弥补上一次生成的模型没有被成功预测的那些样本点,换句话说每一个子模型都是在推动(Boosting)上一个模型所犯的错误。通过这个过程,Ada Boosting依然是可以生成非常多的子模型,对于每一个子模型,它是基于同样的一组样本点,只不过在这些样本点中,点和点之间的权重不一样,每一个子模型认为哪些点更重要,哪些点不重要的程度不同,所以每一个子模型也是有差异的。最终用这些子模型综合投票,来作为Ada Boosting整体最终的学习结果。这样一种方式就叫做Ada Boosting。

对于Ada Boosting来说,我们怎样为这些数据点赋上权值,相应的依然是可以把这个整体的思路转换成是一个求极值的问题。它背后是有严格的数学推导的。对于Boosting跟Bagging不同,它不再有oob了。所以我们依然要将数据集分成训练数据集和测试数据集。依然是先绘制月亮数据集。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)
    plt.scatter(X[y == 0, 0], X[y == 0, 1])
    plt.scatter(X[y == 1, 0], X[y == 1, 1])
    plt.show()

运行结果

现在我们来使用scikit-learn为我们封装的Boosting方法

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
    ada_clf = AdaBoostClassifier(DecisionTreeClassifier(max_depth=2), n_estimators=500)
    ada_clf.fit(X_train, y_train)
    print(ada_clf.score(X_test, y_test))

运行结果

0.872

Gradient Boosting

首先针对所有的数据集进行训练,训练出一个模型m1,产生错误e1;针对e1训练第二个模型m2,产生错误e2;针对e2训练第三个模型m3,产生错误e3......

最终预测结果是:m1+m2+m3+...

首先原始数据就是一个抛物线形的数据(左一),我们对它拟合,就是一根绿色的线,这就是模型m1,对于m1犯的错误的数据e1就是左二的样子,我们再对m1犯的错误的数据e1进行拟合,所形成的绿线就是模型m2,m1+m2结合的模型就是右二的样子,它整体就比m1要好。m2同样会犯错误,m2犯的错误的数据e2就是左三的样子,我们基于e2又可以再训练一个m3,我们将m1+m2+m3就是右三的样子。这幅图比右二m1+m2和右一的m1都要好。以此类推,这样的一种方式就叫做Gradient Boosting。Gradient Boosting跟Ada Boosting不同之处就是它的基础分类器就是决策树,而Ada Boosting的基础分类器是可以选择的。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import GradientBoostingClassifier

if __name__ == "__main__":

    X, y = datasets.make_moons(n_samples=500, noise=0.3, random_state=666)
    # plt.scatter(X[y == 0, 0], X[y == 0, 1])
    # plt.scatter(X[y == 1, 0], X[y == 1, 1])
    # plt.show()
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
    ada_clf = AdaBoostClassifier(DecisionTreeClassifier(max_depth=2), n_estimators=500)
    ada_clf.fit(X_train, y_train)
    print(ada_clf.score(X_test, y_test))

    gb_clf = GradientBoostingClassifier(max_depth=2, n_estimators=30)
    gb_clf.fit(X_train, y_train)
    print(gb_clf.score(X_test, y_test))

运行结果

0.848
0.904

Boosting解决回归问题

在scikit-learn中,对应于

from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import GradientBoostingClassifier

这两个类就有对应下面这两个类来解决回归问题

from sklearn.ensemble import AdaBoostRegressor
from sklearn.ensemble import GradientBoostingRegressor

Stacking

我们再另外介绍一种集成学习的思路,叫做Stacking。之前我们在讲Voting这种集成学习的时候,是讲将不同的机器学习算法将我们的数据都进行一个预测,最后综合这些算法的结果来获得最终结果。对于分类问题可以少数服从多数,对于回归问题可以取平均值。

对于Stacking来说,也是使用多种机器学习的算法来分别取得它们的预测结果,比如说上图中就是使用三种机器学习算法。但是我们不直接使用这些预测结果进行综合来得到最终的结果,而是使用这些预测结果作为输入再添加一层算法,训练一个新的模型,用这个新的模型的输出作为最终的预测结果。这样的一种方式就叫做Stacking。现在我们知道了什么是Stacking,我们来简单的看一下怎样训练一个Stacking这样一个集成学习的分类器。

我们首先是要把训练数据集分成两份,对于其中的一份用来训练多个机器学习模型,而另一份的意义是在于训练上面的那层模型。在使用第一份数据集训练好了多个机器学习的模型后,再把第二部分的样本直接扔进训练好的这些模型中,那么相应训练好的这些模型中也就有了输出的结果。这些新的输出结果再和第二份样本数据相应的真值的输出就形成了一个新的数据集,用这个新的数据集来训练上面的那层模型,最终形成整体的Stacking集成学习的模型。

我们可以再将这种思路推广,构建出更加复杂的Stacking的模型,比如说上图这个样子,我们落了三层模型,第一层有三个模型,第一层的三个模型相应的就可以得到三个输出,把这三个输出作为输入可以分别的再训练三个模型,在第二层的位置。这三个模型以第一层的三个模型的输出作为输入,分别又可以得到三个输出,我们最终再以这三个输出作为第三层这一个模型的输入,第三层的这一个模型最终得到的结果作为整个Stacking集成学习模型的输出结果。如果我们要训练这样的一个Stacking模型,就要将我们原始的训练数据集分成三份。第一份用于训练第一层的这三个模型;第二份用于训练第二层的这三个模型,第三份用于训练第三层的最后一个模型。对于Stacking这样一个模型有几层也是一个超参数,每层使用多少个模型也是一个超参数。正因为如此,Stacking这个模型就会复杂很多,也因为这种复杂性,其实它也是非常容易过拟合的。我们不难发现,这样一个方式非常像神经网络了,只不过对于神经网络来说,每一个神经元相应的不是一个全新的算法,而只是计算一个函数的值而已。相应的对于神经网络来说,如果我们的层数增多的话,就已经是深度学习的模型了。对于神经网络来说,如果层数增多的话,相应的就会引发出很多新的问题,这些新的问题就是深度学习需要解决的问题。神经网络也正是因为这种灵活性,非常容易发生过拟合。在深度学习中很多话题的本质其实就是探讨这样的一个模型来说,我们要如何解决过拟合的问题。其中的很多技术也适用于Stacking这种模型。scikit-learn中没有提供Stacking这种模型的接口。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部