机器学习入门笔记机器学习和人工智能入门机器学习与计算机视觉

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

2019-08-19  本文已影响31人  材才才

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

该系列的前 10 篇文章:

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

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


2. 模型评估的方法

2.1 泛化能力

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

2.2 泛化能力的评估

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

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)指算法模型在训练集上的性能非常好,但是泛化能力很差,泛化误差很大,即在测试集上的效果却很糟糕的情况

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

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

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

解决欠拟合的方法有:

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 随机搜索

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

随机搜索的优点如下:

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

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

4.1.4 贝叶斯优化方法

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

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

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

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

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

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

4.2 调整原则

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

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

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

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

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

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

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

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

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


小结

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

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

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

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


参考:

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

image

往期精彩推荐

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

猜你喜欢

热点阅读