模型评估、过拟合欠拟合以及超参数调优方法

作者: 材才才 | 来源:发表于2019-08-19 07:07 被阅读31次

    机器学习入门系列(2)--如何构建一个完整的机器学习项目,第十一篇!

    该系列的前 10 篇文章:

    上一篇文章介绍了性能评估标准,但如何进行模型评估呢,如何对数据集进行划分出训练集、验证集和测试集呢?如何应对可能的过拟合和欠拟合问题,还有超参数的调优,如何更好更快找到最优的参数呢?

    本文会一一介绍上述的问题和解决方法。


    2. 模型评估的方法

    2.1 泛化能力

    1. 泛化能力:指模型对未知的、新鲜的数据的预测能力,通常是根据测试误差来衡量模型的泛化能力,测试误差越小,模型能力越强;
    2. 统计理论表明:如果训练集和测试集中的样本都是独立同分布产生的,则有 模型的训练误差的期望等于模型的测试误差的期望
    3. 机器学习的“没有免费的午餐定理”表明:在所有可能的数据生成分布上,没有一个机器学习算法总是比其他的要好。
      • 该结论仅在考虑所有可能的数据分布时才成立。
      • 现实中特定任务的数据分布往往满足某类假设,从而可以设计在这类分布上效果更好的学习算法。
      • 这意味着机器学习并不需要寻找一个通用的学习算法,而是寻找一个在关心的数据分布上效果最好的算法。
    4. 正则化是对学习算法做的一个修改,这种修改趋向于降低泛化误差(而不是降低训练误差)。
      • 正则化是机器学习领域的中心问题之一。
      • 没有免费的午餐定理说明了没有最优的学习算法,因此也没有最优的正则化形式。

    2.2 泛化能力的评估

    常用的对模型泛化能力的评估方法有以下几种,主要区别就是如何划分测试集。

    • 留出法(Holdout)
    • k-fold 交叉验证(Cross Validation)
    • 留一法(Leave One Out, LOO)
    • 自助法(bootstrapping)
    2.2.1 留出法(Holdout)

    留出法是最简单也是最直接的验证方法,它就是将数据集随机划分为两个互斥的集合,即训练集和测试集,比如按照 7:3 的比例划分,70% 的数据作为训练集,30% 的数据作为测试集。也可以划分为三个互斥的集合,此时就增加一个验证集,用于调试参数和选择模型

    直接采用 sklearn 库的 train_test_split 函数即可实现,一个简单的示例代码如下,这里简单调用 knn 算法,采用 Iris 数据集。

    from sklearn.model_selection import train_test_split
    from sklearn.datasets import load_iris
    from sklearn.neighbors import KNeighborsClassifier
    
    # 加载 Iris 数据集
    dataset = load_iris()
    # 划分训练集和测试集
    (trainX, testX, trainY, testY) = train_test_split(dataset.data, dataset.target, random_state=3, test_size=0.3)
    # 建立模型
    knn = KNeighborsClassifier()
    # 训练模型
    knn.fit(trainX, trainY)
    # 将准确率打印
    print('hold_out, score:', knn.score(testX, testY))
    

    留出法的使用需要注意:

    1. 数据集的划分要尽可能保持数据分布的一致性,避免因为数据划分过程引入额外的偏差而对最终结果产生影响。比如训练、验证和测试集的类别比例差别很大,则误差估计将由于三个集合数据分布的差异而产生偏差。

      因此,分类任务中必须保持每个集合中的类别比例相似。从采样的角度看数据集的划分过程,这种保留类别比例的采样方式称为“分层采样”。

    2. 即便确定了训练、验证、测试集的比例,还是有多种划分方式,比如排序后划分、随机划分等等,这些不同的划分方式导致单次留出法得到的估计结果往往不够稳定可靠。因此,使用留出法的时候,往往采用若干次随机划分、重复进行实验后,取平均值作为最终评估结果

    分层采样的简单代码实现如下所示,主要是调用了 sklearn.model_selection 中的 StratifiedKFold

    from sklearn.datasets import load_iris
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.model_selection import StratifiedKFold
    from sklearn.base import clone
    
    def StratifiedKFold_method(n_splits=3):
        '''
        分层采样
        :return:
        '''
        # 加载 Iris 数据集
        dataset = load_iris()
        data = dataset.data
        label = dataset.target
        # 建立模型
        knn = KNeighborsClassifier()
        print('use StratifiedKFold')
        skfolds = StratifiedKFold(n_splits=n_splits, random_state=42)
        scores = 0.
        for train_index, test_index in skfolds.split(data, label):
            clone_clf = clone(knn)
            X_train_folds = data[train_index]
            y_train_folds = (label[train_index])
            X_test_fold = data[test_index]
            y_test_fold = (label[test_index])
            clone_clf.fit(X_train_folds, y_train_folds)
            y_pred = clone_clf.predict(X_test_fold)
            n_correct = sum(y_pred == y_test_fold)
            print(n_correct / len(y_pred))
            scores += n_correct / len(y_pred)
        print('mean scores:', scores / n_splits)
    

    留出法也存在以下的缺点:

    1. 在验证集或者测试集上的评估结果和划分方式有关系,这也就是为什么需要多次实验,取平均值;
    2. 我们希望评估的是在原始数据集上训练得到的模型的能力,但留出法在划分两个或者三个集合后,训练模型仅使用了原始数据集的一部分,这会降低评估结果的保真性。但这个问题没有完美的解决方法,常见做法是将大约 2/3 ~ 4/5 的样本作为训练集,剩余的作为验证集和测试集。
    2.2.2 k-fold 交叉验证(Cross Validation)

    k-fold 交叉验证 的工作流程:

    1. 将原始数据集划分为 k 个大小相等且互斥的子集;
    2. 选择 k-1 个子集作为训练集,剩余作为验证集进行模型的训练和评估,重复 k 次(每次采用不同子集作为验证集);
    3. k 次实验评估指标的平均值作为最终的评估结果。

    通常,k 取 10。

    但和留出法类似,同样存在多种划分 k 个子集的方法,所以依然需要随时使用不同方式划分 p 次,每次得到 k 个子集。

    同样,采用 sklearn.cross_validationcross_val_score 库可以快速实现 k-fold 交叉验证法,示例如下:

    from sklearn.datasets import load_iris
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.cross_validation import cross_val_score
    # 加载 Iris 数据集
    dataset = load_iris()
    data = dataset.data
    label = dataset.target
    # 建立模型
    knn = KNeighborsClassifier()
    # 使用K折交叉验证模块
    scores = cross_val_score(knn, data, label, cv=10, scoring='accuracy')
    # 将每次的预测准确率打印出
    print(scores)
    # 将预测准确平均率打印出
    print(scores.mean())
    
    2.2.3 留一法

    留一法是 k-fold 交叉验证的一个特例情况,即让 k=N, 其中 N 是原始数据集的样本数量,这样每个子集就只有一个样本,这就是留一法

    留一法的优点就是训练数据更接近原始数据集了,仅仅相差一个样本而已,通过这种方法训练的模型,几乎可以认为就是在原始数据集上训练得到的模型 。

    但缺点也比较明显,计算速度会大大降低特别是原始数据集非常大的时候,训练 N 个模型的计算量和计算时间都很大,因此一般实际应用中很少采用这种方法。

    2.2.4 自助法

    在留出法和 k-fold 交叉验证法中,由于保留了一部分样本用于测试,因此实际训练模型使用的训练集比初始数据集小,这必然会引入一些因为训练样本规模不同而导致的估计偏差

    留一法受训练样本规模变化的影响较小,但是计算复杂度太高

    自助法是一个以自助采样法(bootstrap sampling)为基础的比较好的解决方案。同时,它也是随机森林算法中用到的方法。

    它的做法就是对样本数量为 N 的数据集进行 N 次有放回的随机采样,得到一个大小是 N 的训练集。

    在这个过程中将会有一部分数据是没有被采样得到的,一个样本始终没有被采样出来的概率是 (1-\frac{1}{N})^N,根据极限可以计算得到:
    lim_{N\rightarrow +\infty}(1-\frac{1}{N})^N=\frac{1}{e}\approx 0.368
    也就是采用自助法,会有 36.8% 的样本不会出现在训练集中,使用这部分样本作为测试集。这种方法也被称为包外估计。

    自助法的优点有:

    • 数据集比较小、难以有效划分训练/测试集时很有用:

    • 能从初始数据集中产生多个不同的训练集,这对集成学习等方法而言有很大好处。

    但也存在如下缺点:

    • 产生的数据集改变了初始数据集的分布,这会引入估计偏差。因此在初始数据量足够时,留出法和折交叉验证法更常用

    2.3 训练集、验证集、测试集

    简单介绍下训练集、验证集和测试集各自的作用:

    1. 训练集:主要就是训练模型,理论上越大越好;
    2. 验证集:用于模型调试超参数。通常要求验证集比较大,避免模型会对验证集过拟合;
    3. 测试集:用于评估模型的泛化能力。理论上,测试集越大,评估结果就约精准。另外,测试集必须不包含训练样本,否则会影响对模型泛化能力的评估。

    验证集和测试集的对比:

    • 测试集通常用于对模型的预测能力进行评估,它是提供模型预测能力的无偏估计;如果不需要对模型预测能力的无偏估计,可以不需要测试集;
    • 验证集主要是用于超参数的选择。

    2.4 划分数据集的比例选择方法

    那么一般如何选择划分训练、验证和测试集的比例呢?通常可以按照如下做法:

    1. 对于小批量数据,数据的拆分的常见比例为:
      • 如果未设置验证集,则将数据三七分:70% 的数据用作训练集、30% 的数据用作测试集。
      • 如果设置验证集,则将数据划分为:60% 的数据用作训练集、20%的数据用过验证集、20% 的数据用作测试集。
    2. 对于大批量数据,验证集和测试集占总数据的比例会更小
      • 对于百万级别的数据,其中 1 万条作为验证集、1 万条作为测试集即可。
      • 验证集的目的就是验证不同的超参数;测试集的目的就是比较不同的模型。
        • 一方面它们要足够大,才足够评估超参数、模型。
        • 另一方面,如果它们太大,则会浪费数据(验证集和训练集的数据无法用于训练)。
    3. k-fold 交叉验证中:先将所有数据拆分成 k 份,然后其中 1 份作为测试集,其他 k-1 份作为训练集。
      • 这里并没有验证集来做超参数的选择。所有测试集的测试误差的均值作为模型的预测能力的一个估计。
      • 使用 k-fold 交叉的原因是:样本集太小。如果选择一部分数据来训练,则有两个问题:
        • 训练数据的分布可能与真实的分布有偏离k-fold 交叉让所有的数据参与训练,会使得这种偏离得到一定程度的修正。
        • 训练数据太少,容易陷入过拟合k-fold 交叉让所有数据参与训练,会一定程度上缓解过拟合。

    2.5 分布不匹配

    深度学习时代,经常会发生:训练集和验证集、测试集的数据分布不同

    如:训练集的数据可能是从网上下载的高清图片,测试集的数据可能是用户上传的、低像素的手机照片。

    • 必须保证验证集、测试集的分布一致,它们都要很好的代表你的真实应用场景中的数据分布。
    • 训练数据可以与真实应用场景中的数据分布不一致,因为最终关心的是在模型真实应用场景中的表现。

    如果发生了数据不匹配问题,则可以想办法让训练集的分布更接近验证集

    • 一种做法是:收集更多的、分布接近验证集的数据作为训练集合
    • 另一种做法是:人工合成训练数据,使得它更接近验证集。该策略有一个潜在问题:你可能只是模拟了全部数据空间中的一小部分。导致你的模型对这一小部分过拟合。

    当训练集和验证集、测试集的数据分布不同时,有以下经验原则:

    • 确保验证集和测试集的数据来自同一分布

      因为需要使用验证集来优化超参数,而优化的最终目标是希望模型在测试集上表现更好。

    • 确保验证集和测试集能够反映未来得到的数据,或者最关注的数据

    • 确保数据被随机分配到验证集和测试集上

    当训练集和验证集、测试集的数据分布不同时,分析偏差和方差的方式有所不同

    • 如果训练集和验证集的分布一致,那么当训练误差和验证误差相差较大时,我们认为存在很大的方差问题

    • 如果训练集和验证集的分布不一致,那么当训练误差和验证误差相差较大时,有两种原因:

      • 第一个原因:模型只见过训练集数据,没有见过验证集的数据导致的,是数据不匹配的问题
      • 第二个原因:模型本来就存在较大的方差

      为了弄清楚原因,需要将训练集再随机划分为:训练-训练集训练-验证集。这时候,训练-训练集训练-验证集 是同一分布的。

      • 模型在训练-训练集训练-验证集 上的误差的差距代表了模型的方差
      • 模型在训练-验证集 和 验证集上的误差的差距代表了数据不匹配问题的程度

    3. 过拟合、欠拟合

    机器学习的两个主要挑战是过拟合和欠拟合

    过拟合(overfitting)指算法模型在训练集上的性能非常好,但是泛化能力很差,泛化误差很大,即在测试集上的效果却很糟糕的情况

    • 过拟合的原因:将训练样本本身的一些特点当作了所有潜在样本都具有的一般性质,这会造成泛化能力下降;另一个原因是模型可能学到训练集中的噪声,并基于噪声进行了预测
    • 过拟合无法避免,只能缓解。因为机器学习的问题通常是 NP 难甚至更难的,而有效的学习算法必然是在多项式时间内运行完成。如果可以避免过拟合,这就意味着构造性的证明了 P=NP

    欠拟合(underfitting)模型的性能非常差,在训练数据和测试数据上的性能都不好,训练误差和泛化误差都很大。其原因就是模型的学习能力比较差。

    一般可以通过挑战模型的容量来缓解过拟合和欠拟合问题。模型的容量是指其拟合各种函数的能力

    • 容量低的模型容易发生欠拟合,模型拟合能力太弱。
    • 容量高的模型容易发生过拟合,模型拟合能力太强。

    一般解决过拟合的方法有:

    • 简化模型,这包括了采用简单点的模型、减少特征数量,比如神经网络中减少网络层数或者权重参数,决策树模型中降低树的深度、采用剪枝等;
    • 增加训练数据,采用数据增强的方法,比如人工合成训练数据等;
    • 早停,当验证集上的误差没有进一步改善,训练提前终止;
    • 正则化,常用 L1 或者 L2 正则化。
    • 集成学习方法,训练多个模型,并以每个模型的平均输出作为结果,降低单一模型的过拟合风险,常用方法有 baggingboostingdropout(深度学习中的方法)等;
    • 噪声注入:包括输入噪声注入、输出噪声注入、权重噪声注入。将噪声分别注入到输入/输出/权重参数中,虽然噪声可能是模型过拟合的一个原因,但第一可以通过交叉验证来避免;第二就是没有噪声的完美数据也是很有可能发生过拟合;第三可以选择在特征、权值参数加入噪声,而非直接在数据加入噪声。

    解决欠拟合的方法有:

    • 选择一个更强大的模型,带有更多参数
    • 更好的特征训练学习算法(特征工程)
    • 减小对模型的限制(比如,减小正则化超参数)

    4. 超参数调优

    超参数调优是一件非常头疼的事情,很多时候都需要一些先验知识来选择合理的参数值,但如果没有这部分先验知识,要找到最优的参数值是很困难,非常耗费时间和精力。但超参数调优确实又可以让模型性能变得更加的好。

    在选择超参数调优算法前,需要明确以下几个要素:

    • 目标函数。算法需要最大化/最小化的目标;
    • 搜索范围。一般通过上下限来确定;
    • 算法的其他参数,比如搜索步长。

    4.1 搜索策略

    常用的几种超参数搜索策略如下:

    • 手动搜索:需要较好的先验知识经验
    • 网格搜索:超参数的数据相对较少的时候,这个方法比较实用
    • 随机搜索:通常推荐这种方式
    • 贝叶斯优化算法:基于模型的搜索方法,利用了历史搜索结果
    4.1.1 手动搜索
    1. 手动选择超参数需要了解超参数做了些什么,以及机器学习模型如何才能取得良好的泛化

    2. 手动搜索超参数的任务是:在给定运行时间和内存预算范围的条件下,最小化泛化误差

    3. 手动调整超参数时不要忘记最终目标:提升测试集性能

      • 加入正则化只是实现这个目标的一种方法。

      • 如果训练误差很低,也可以通过收集更多的训练数据来减少泛化误差。

        如果训练误差太大,则收集更多的训练数据就没有意义。

      • 实践中的一种暴力方法是:不断提高模型容量和训练集的大小

        这种方法增加了计算代价,只有在拥有充足的计算资源时才可行

    4.1.2 网格搜索

    网格搜索可能是最简单也是应用最广泛的超参数搜索算法了。它的几种做法如下:

    • 采用较大的搜索范围和较小的搜索步长,很大概率会搜索到全局最优值,但十分耗费计算资源和时间,特别是超参数比较多的时候;
    • 先采用较大搜索范围和较大步长,寻找全局最优的可能位置,然后逐渐缩小搜索范围和步长,来确定更精确的最优值。可以降低所需要的计算时间和计算量,但由于目标函数一般都是非凸的,可能会错过全局最优值。

    网格搜索也可以借助 sklearn 实现,简单的示例代码如下:

    from sklearn.model_selection import GridSearchCV
    from sklearn.ensemble import RandomForestClassifier
    param_grid = [
        {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
        {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
    ]
    forest_reg = RandomForestRegressor()
    grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
                               scoring='neg_mean_squared_error')
    grid_search.fit(data, labels)
    
    4.1.3 随机搜索

    随机搜索是一种可以替代网格搜索的方法,它编程简单、使用方便、能更快收敛到超参数的良好取值。

    • 首先为每个超参数定义一个边缘分布,如伯努利分布(对应着二元超参数)或者对数尺度上的均匀分布(对应着正实值超参数)。
    • 然后假设超参数之间相互独立,从各分布中抽样出一组超参数。
    • 使用这组超参数训练模型。
    • 经过多次抽样 -> 训练过程,挑选验证集误差最小的超参数作为最好的超参数。

    随机搜索的优点如下:

    • 不需要离散化超参数的值,也不需要限定超参数的取值范围。这允许我们在一个更大的集合上进行搜索。
    • 当某些超参数对于性能没有显著影响时,随机搜索相比于网格搜索指数级地高效,它能更快的减小验证集误差

    随机搜索比网格搜索更快的找到良好超参数的原因是:没有浪费的实验

    • 在网格搜索中,两次实验之间只会改变一个超参数 (假设为 m)的值,而其他超参数的值保持不变。如果这个超参数 m 的值对于验证集误差没有明显区别,那么网格搜索相当于进行了两个重复的实验。
    • 在随机搜索中,两次实验之间,所有的超参数值都不会相等,因为每个超参数的值都是从它们的分布函数中随机采样而来。因此不大可能会出现两个重复的实验。
    • 如果 m 超参数与泛化误差无关,那么不同的 m 值:
      • 在网格搜索中,不同 m 值、相同的其他超参数值,会导致大量的重复实验
      • 在随机搜索中,其他超参数值每次也都不同,因此不大可能出现两个重复的实验(除非所有的超参数都与泛化误差无关)。

    随机搜索可以采用 sklearn.model_selection 中的 RandomizedSearchCV 方法。

    4.1.4 贝叶斯优化方法

    贝叶斯优化方法是基于模型的参数搜索算法的一种比较常见的算法。它相比于前面的网格搜索和随机搜索,最大的不同就是利用历史的搜索结果进行优化搜索。主要是由四部分组成的:

    1. 目标函数。大部分情况是模型验证集上的损失;
    2. 搜索空间。各类待搜索的超参数;
    3. 优化策略。建立的概率模型和选择超参数的方式;
    4. 历史的搜索结果。

    贝叶斯优化算法的步骤如下:

    1. 根据先验分布,假设一个搜索函数;
    2. 然后,每一次采用新的采样点来测试目标函数时,利用这个信息更新目标函数的先验分布;
    3. 最后,算法测试由后验分布给出的全局最优最可能出现的位置的点。

    需要特别注意的是,贝叶斯优化算法容易陷入局部最优值:它在找到一个局部最优值后,会不断在该区域进行采样。

    因此,贝叶斯优化算法会在探索和利用之间找到一个平衡点,探索是在还未取样的区域获取采样点,利用则是根据后验分布在最可能出现全局最优的区域进行采样。

    4.2 调整原则

    1. 通常先对超参数进行粗调,然后在粗调中表现良好的超参数区域进行精调

    2. 超参数随机搜索,并不意味着是在有效范围内随机均匀取值。需要选择合适的缩放来进行随机选取。

    3. 通常情况下,建议至少每隔几个月重新评估或者修改超参数。因为随着时间的变化,真实场景的数据会逐渐发生改变:

      • 可能是由于用户的行为、偏好发生了改变。
      • 可能是采样的方式发生了改变。
      • 也可能仅仅是由于数据中心更新了服务器。

      由于这些变化,原来设定的超参数可能不再适用。

    4. 有两种超参数调整策略:

      • 如果数据足够大且没有足够的计算资源,此时只能一次完成一个试验。

        可以每天观察模型的表现,实时的、动态的调整超参数

      • 如果数据不大,有足够的计算资源可以同一时间完成大量的试验,则可以设置多组超参数设定,然后选择其中表现最好的那个


    小结

    关于模型评估方面的内容就介绍这么多,文章有些长,而且内容也比较多。

    关于如何构建一个机器学习项目的内容,基本到本文就介绍完毕了,从开始的评估问题,获取数据,到数据预处理、特征工程,然后就是各种常见机器学习算法的评估,最后就是模型评估部分的内容了。

    当然了,本系列的文章还是偏向于理论,代码比较少,主要也是整理和总结书本以及网上文章的知识点。

    所以下一篇文章会是介绍一篇手把手教你运用机器学习算法来做分类的文章,来自国外一个大神的博客文章,主要是面向机器学习的初学者。


    参考:

    欢迎关注我的微信公众号--算法猿的成长,或者扫描下方的二维码,大家一起交流,学习和进步!

    image

    往期精彩推荐

    机器学习系列
    Github项目 & 资源教程推荐

    相关文章

      网友评论

        本文标题:模型评估、过拟合欠拟合以及超参数调优方法

        本文链接:https://www.haomeiwen.com/subject/etyjsctx.html