YOLO813

机器学习(9)- 深度学习理论基础之决策树

    机器学习的基本问题是利用模型对数据进行拟合,学习的目的并非是对有限训练集进行正确预测,而是对未曾在训练集合出现的样本能够正确预测。模型对训练集数据的误差称为经验误差,对测试集数据的误差称为泛化误差。模型对训练集以外样本的预测能力就称为模型的泛化能力,追求这种泛化能力始终是机器学习的目标。

    过拟合(overfitting)和欠拟合(underfitting)是导致模型泛化能力不高的两种常见原因,都是模型学习能力与数据复杂度之间失配的结果。“欠拟合”常常在模型学习能力较弱,而数据复杂度较高的情况出现,此时模型由于学习能力不足,无法学习到数据集中的“一般规律”,因而导致泛化能力弱。与之相反,“过拟合”常常在模型学习能力过强的情况中出现,此时的模型学习能力太强,以至于将训练集单个样本自身的特点都能捕捉到,并将其认为是“一般规律”,同样这种情况也会导致模型泛化能力下降。过拟合与欠拟合的区别在于,欠拟合在训练集和测试集上的性能都较差,而过拟合往往能较好地学习训练集数据的性质,而在测试集上的性能较差。在神经网络训练的过程中,欠拟合主要表现为输出结果的高偏差,而过拟合主要表现为输出结果的高方差


    代码示例如下:

    建立测试数据:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
x = np.linspace(start=0,
               stop=10,
               num=30)
y = x**3 + np.random.rand(30)*200
plt.scatter(x,y)

    假设使用一元线性回归方程式来建立模型,妥妥的欠拟合

from sklearn.linear_model import LinearRegression
model1 = LinearRegression()
model1.fit(x.reshape(-1,1),
           y.reshape(-1,1))
plt.scatter(x,y)
plt.plot(x,model1.predict(x.reshape(-1,1)),
        c='r')

    可以看到这在训练集上面的拟合程度都很低,更不用说测试集上了。


    过拟合的模拟

#使用20阶方程式
#快速构建多项式
from sklearn.preprocessing import PolynomialFeatures
q20 = PolynomialFeatures(degree=20)
x_ = q20.fit_transform(x.reshape(-1,1))
model2 = LinearRegression()
model2.fit(x_,
           y.reshape(-1,1))
plt.scatter(x,y)
plt.plot(x,model2.predict(x_),
        c='g')

    可以看到这个训练集上面的模拟效果是极好的。


    如果使用3阶方程式呢

q3 = PolynomialFeatures(degree=3)
x_q3 = q3.fit_transform(x.reshape(-1,1))
model3 = LinearRegression()
model3.fit(x_q3,
           y.reshape(-1,1))
plt.scatter(x,y)
plt.plot(x,model3.predict(x_q3),
        c='r')


    效果看起来也还不错。

    但假如我们现在扩充一下数据

x = np.linspace(start=0,
               stop=20,
               num=40)
y = x**3 + np.random.rand(40)*200
q20 = PolynomialFeatures(degree=20)
x_ = q20.fit_transform(x.reshape(-1,1))
model2 = LinearRegression()
model2.fit(x_,
           y.reshape(-1,1))
q3 = PolynomialFeatures(degree=3)
x_q3 = q3.fit_transform(x.reshape(-1,1))
model3 = LinearRegression()
model3.fit(x_q3,
           y.reshape(-1,1))

    绘图查看,可以看到20阶方程式的在17.5之后一飞冲天,泛化能力太差,这就是所谓的过拟合。

plt.scatter(x,y)
plt.plot(x,model2.predict(x_),
        c='g')
plt.plot(x,model3.predict(x_q3),
        c='r')

 


决策树的初步了解

    决策树的算法原理:决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。

    举个简单例子,张三想要跟好人做朋友,但怎么定义好人呢?于是他做了个决策树模型:

    以上,根节点包含了样本的全集,每个分枝点代表对某一特征属性的一次测试,每条边代表一个测试结果,叶子顶点代表某个类或类的分布。


    从这个算法原理里面,我们可以考虑一个问题,上图根节点下面的“坐过牢”和“喜欢喝酒”两个特征可以互换吗?换句话说,特征值的顺序能否更改?

    这里面需要引入名词“信息熵”,熵本质指一个系统“内在的混乱程度”,1948年,信息论之父香农(C.E.Shannon)用信息熵的概念来描述信源的不确定度;另外一个名词叫做“信息增益”,信息增益代表了在一个条件下,信息复杂度(不确定性)减少的程度,决策树算法中,到底按照什么标准来选择哪一个特征以用信息增益来度量,如果选择一个特征后,信息增益最大(信息不确定性减少的程度最大),那么我们就选取这个特征。

    还是以上面为例,假设有份表格数据中显示,喜欢喝酒的好人和坏人各占一半,但坐过牢的好人和坏人1:9开,那么此时我们选择什么特征进行分类呢?如果以“喝酒”为特征进行分类没有任何意义,信息还是很混乱,而以“坐牢”为特征进行分类,区分就很明显,可以获得更大的信息增益。


    由于决策树的会对训练样本完美的分类,所以经常会出现过拟合情况。解决办法有三种:

  •     预剪枝(prepruning):设定一个阈值,信息嫡减小的数量小于这个值,停止创建分支;一旦停止,节点就成为树叶。该树叶可以持有子集元组中最频繁的类
  •     后剪枝(postpruning):决策树创建完成后,对节点检查其信息嫡的增益。
  •     控制决策树的最大深度

 

决策树算法实例

    泰坦尼克号获救预测数据,来源于互联网。

data = pd.read_csv('train.csv')
data.head()


    通过上面这张图,我们取出对获救可能有影响的特征:

df = data[["Survived","Pclass","Sex","Age","SibSp","Parch","Fare","Embarked"]]
df.head()

df = df.dropna(how='all')
df

    通过unique函数观察,发现这些特征里面存在空值和NaN,对于年龄,我想以均值来填充,而对于其它特征为空的内容统一填充为0

df.Age.unique()
df.Embarked.unique()

values= {'Age':df.Age.mean(),
        'Embarked':0}
df.fillna(value=values,
         inplace=True)
df.fillna(0,inplace=True)

    还有一些数据预处理的工作,例如将性别转为数字类型

# 这种快捷方式一定要用括号包裹的列表推导式
[1 if x =='male' else 0 for x in df.Sex]
df.Sex = [1 if x =='male' else 0 for x in df.Sex]

    为了防止出现线性关系,将Pclass进行one hot编码,在之前的文章中也记录过这个原因

pd.get_dummies(df.Pclass,
              prefix="p",
              prefix_sep="")


df = df.merge(pd.get_dummies(df.Pclass,
              prefix="p",
              prefix_sep=""),
         left_index=True,
        right_index=True)
df.drop('Pclass',
        axis=1,
       inplace=True)

    同样的登船地点处理:

#第一列的0是我们填充的缺失值
#没有任何意义,合并时就给去了
df = df.merge(pd.get_dummies(df.Embarked,
              prefix="em",
              prefix_sep="-",
              drop_first=True),
         left_index=True,
        right_index=True)
df.drop('Embarked',
        axis=1,
       inplace=True)

    最终的数据样式


    制作训练、测试数据

data_train = df[[x for x in df.columns if x !='Survived']]
data_test = df['Survived']
x_train,x_test,y_train,y_test = train_test_split(data_train, data_test)


    建立决策树模型,不得不说sklearn封装的实在是太好了,调用的方法基本一样,我们先使用默认的参数

from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier()
model.fit(x_train, y_train)
model.score(x_test, y_test)

    可以看到,测试集上73%的准确率,如果想自己计算准确率也是可以的

predict_df = pd.DataFrame({"ori": y_test,
                          "pre": model.predict(x_test)})
predict_df = predict_df.assign(answer = predict_df.ori == predict_df.pre)
predict_df

predict_df.answer.sum()/len(predict_df)

    训练集上98%的准确率,测试集只有73%,很明显,泛化能力不行,需要解决过拟合的问题。

    我们先来看下官方文档DecisionTreeClassifier介绍

Init signature:
DecisionTreeClassifier(
    *,
    criterion='gini',
    splitter='best',
    max_depth=None,
    min_samples_split=2,
    min_samples_leaf=1,
    min_weight_fraction_leaf=0.0,
    max_features=None,
    random_state=None,
    max_leaf_nodes=None,
    min_impurity_decrease=0.0,
    min_impurity_split=None,
    class_weight=None,
    ccp_alpha=0.0,
)

    看看max_depth(控制深度)

max_depth : int, default=None
    The maximum depth of the tree. If None, then nodes are expanded until
    all leaves are pure or until all leaves contain less than
    min_samples_split samples.

    上面说过,有三种解决方案,其中第三种就是控制深度,max_depth如果不设置,则节点会不停的(通过决策)扩张直到所有叶子变成纯的或者所有的叶子包含的样本少于min_samples_split(内部节点再划分所需最小样本数)

    我们可以人为的一个个调试max_depth,也可以写一个函数来进行测试,例如:

def setMaxDepth(max_depth):
    model = DecisionTreeClassifier(max_depth=max_depth)
    model.fit(x_train, y_train)
    train_score =model.score(x_train, y_train)
    test_score = model.score(x_test, y_test)
    return train_score, test_score
scores = [setMaxDepth(x) for x in range(1,20)]
scores

d_train = [s[0] for s in scores]
d_test = [s[1] for s in scores]
plt.plot(d_train)
plt.plot(d_test)

    可以看到depth=2时,测试集的效果最佳。


    再来看下min_impurity_split(基尼系数),

min_impurity_split : float, default=0
    Threshold for early stopping in tree growth. A node will split
    if its impurity is above the threshold, otherwise it is a leaf.

    .. deprecated:: 0.19
       ``min_impurity_split`` has been deprecated in favor of
       ``min_impurity_decrease`` in 0.19. The default value of
       ``min_impurity_split`` has changed from 1e-7 to 0 in 0.23 and it
       will be removed in 1.0 (renaming of 0.25).
       Use ``min_impurity_decrease`` instead.

    提前停止决策树生长的(信息增益)阈值。如果一个节点的基尼不纯度在这个阈值之上则会继续往下分支,否则它就是叶子节点,即小于这个阈值时节点不会再生成子节点。使用min_impurity_decrease来替换。

def setMinImpuritySplit(min_impurity_split):
    model = DecisionTreeClassifier(min_impurity_decrease=min_impurity_split)
    model.fit(x_train, y_train)
    train_score =model.score(x_train, y_train)
    test_score = model.score(x_test, y_test)
    return train_score, test_score
min_range = np.linspace(0,1,50)
scores_2 = [setMinImpuritySplit(x) for x in min_range]
data_train_2 = [ x[0] for x in scores_2]
data_test_2 = [ x[1] for x in scores_2]
plt.plot(data_train_2)
plt.plot(data_test_2

    获取最大值

best_index = np.argmax(data_test_2)
data_test_2[best_index],data_train_2[best_index]

    同时获得最好的min_impurity_split

min_range[best_index]


    到了这里,我们就需要考虑一个问题了,在我再次使用这个流程测试了几遍之后,发现每一次的结果不尽相同,主要有三个点可能存在影响

  •     train_test_split划分数据时,由于数据的随机性以及测试数据集的size选取
  •     max_depth参数
  •     min_impurity_decrease参数

    我们可以多次测试以取得平均值。例如,在测试集为30%时,设定max_depth参数为1,循环取min_impurity_decrease为0-1,再设定max_depth为2,再循环,以此类推,取得最优解,但是这种做法太费代码。

    在sklearn中,我们可以使用交叉验证(解决测试集问题)解决随机划分的差异问题,例如将数据分为5份,第一份为训练集,其它四份为测试集,再使用第二份为训练集,其它四份为测试集,以此类推,获得其平均值,这样可以避免某一份数据只是用于训练而不参与测试(例如上面使用train_test_split划分的数据);而关于多参数的选择,sklearn中也有现成的工具GridSearchCV(和train_test_split位于同一个库),CV就是指Cross-validation。

    照例,还是先看下GridSearchCV的官方文档介绍:

Init signature:
GridSearchCV(
    estimator,
    param_grid,
    *,
    scoring=None,
    n_jobs=None,
    refit=True,
    cv=None,
    verbose=0,
    pre_dispatch='2*n_jobs',
    error_score=nan,
    return_train_score=False,
)

    其中我目前接触的有param_grid和cv:

estimator : estimator object.
    This is assumed to implement the scikit-learn estimator interface.
    Either estimator needs to provide a ``score`` function,
    or ``scoring`` must be passed.
param_grid : dict or list of dictionaries
    Dictionary with parameters names (`str`) as keys and lists of
    parameter settings to try as values, or a list of such
    dictionaries, in which case the grids spanned by each dictionary
    in the list are explored. This enables searching over any sequence
    of parameter settings.
cv : int, cross-validation generator or an iterable, default=None
    Determines the cross-validation splitting strategy.
    Possible inputs for cv are:

    - None, to use the default 5-fold cross validation,
    - integer, to specify the number of folds in a `(Stratified)KFold`,
    - :term:`CV splitter`,
    - An iterable yielding (train, test) splits as arrays of indices.

    For integer/None inputs, if the estimator is a classifier and ``y`` is
    either binary or multiclass, :class:`StratifiedKFold` is used. In all
    other cases, :class:`KFold` is used. These splitters are instantiated
    with `shuffle=False` so the splits will be the same across calls.

    Refer :ref:`User Guide <cross_validation>` for the various
    cross-validation strategies that can be used here.

    .. versionchanged:: 0.22
        ``cv`` default value if None changed from 3-fold to 5-fold.

    可以看到自0.22版本之后cv默认5折交叉验证。

from sklearn.model_selection import GridSearchCV
max_depth=range(1,10)
min_impurity_decrease = np.linspace(0,1,50)
param_grid ={"max_depth":max_depth,
            "min_impurity_decrease":min_impurity_decrease}
model = GridSearchCV(estimator=DecisionTreeClassifier(),
             param_grid=param_grid,
            cv=5)
model

#不用划分数据了
#全部数据丢进去训练
model.fit(data_train, data_test)

model.best_params_

#测试集上最好的得分
model.best_score_  

 


参考:

https://www.cnblogs.com/zhhfan/p/10476761.html
https://blog.csdn.net/weixin_42575020/article/details/82949285
https://www.cnblogs.com/xiaoyh/p/11321780.html
https://www.cnblogs.com/baby-lily/p/10646226.html