机器学习 | 模型选择

相关文章:

机器学习 | 目录

1. 模型验证

模型验证:(Model Validation)就是在选择模型和超参数之后,通过对训练数据进行学习,对比已知数据的预测值与实际值的差异。[1]

我们将首先通过一个简单方法实现模型验证,并告诉你为什么那样做行不通。之后,介绍如何用留出集(Holdout Set)与交叉验证(Cross-Validation)实现更可靠的模型验证。

1.1 错误的模型验证方法

我们将使用鸢尾花数据来演示一个简单的模型验证方法。

首先加载数据:

from sklearn.datasets import load_iris

iris = load_iris()
X = iris.data
y = iris.target

然后选择模型和超参数。这里使用一个 k k k近邻分类器,超参数为 n_neighbors=1。这是一个非常简单直观的模型,新数据的标签与其最接近的训练数据的标签相同:

from sklearn.neighbors import KNeighborsClassifier
model = KNeighborsClassifier(n_neighbors=1)

然后训练模型,并由他来预测已知标签的数据:

model.fit(X,y)
y_model = model.predict(X)

最后,计算模型的准确率:

from sklearn.metrics import accuracy_score
accuracy_score(y, y_model)
1.0

准确得分是1.0,也就是模型识别标签的正确率是 100% !但是这样测量的准确率可靠吗?我们真的有一个在任何时候准确率都是 100% 的模型吗?

你可能已经猜到了,答案是否定的。其实这个方法又个根本缺陷:它用同一套数据训练和评估模型。另外,最近邻模型是一种与距离相关的评估器,只会简单地存储训练数据,然后把新数据与储存的已知数据进行对比来预测标签。在理想情况下,模型的准确率总是 100% 。

1.2 正确的模型验证方法

1.2.1 留出集

那么怎么样才能验证模型呢?其实留出集(Holdout Set)可以更好地评估模型性能,也就是说,将数据分为两部分,一部分用来训练模型,另一部分用来验证模型性能。在Sklearn里用train_test_splitSklearn官方文档)工具就可以实现:

from sklearn.datasets import load_iris

iris = load_iris()
train_data = iris.data
test_data = iris.target

# 一半数据做为训练集,一半数据作为测试集(默认test_size = 0.75)
from sklearn.model_selection import train_test_split

X1, X2, y1, y2 = train_test_split(X, y, random_state=0, train_size=0.5)

# 用模型拟合训练数据
model.fit(X1, y1)

# 利用训练好的模型进行预测
y2_model = model.predict(X2)

# 在测试集中评估模型准确率
from sklearn.metrics import accuracy_score

accuracy_score(y2, y2_model)
0.9066666666666666

这样就可以获得更合理的结果了:最近邻分类器在这份留出集上的准确率是 90.67%。 这里的留出集类似于新数据,因为模型之前没有“接触”过它们。

1.2.2 交叉验证

用留出集进行模型验证有一个缺点,就是模型失去了一部分训练机会,在上面的模型里,有一半的数据没有为模型训练作出贡献。这显然不是最优解,而且可能还会出现问题——尤其是在数据集规模比较小的时候。

解决这个问题的方法是交叉验证(Cross Validation),也就是做一组拟合,让数据的每个子集即是训练集,又是验证集。如下图所示,对数据进行两轮交叉验证:

这里进行了两轮验证实验,轮流用一组数据作为留出集。所以我们可以对1.2.1的例子实现交叉验证:

y2_model = model.fit(X1, y1).predict(X2)
y1_model = model.fit(X2, y2).predict(X1)
accuracy_score(y1, y1_model), accuracy_score(y2, y2_model)
(0.96, 0.9066666666666666)

这样就得到了两个准确率,将二者结合(例如求均值)获得一个更为准确的模型总性能,这种形式的交叉验证被称为两轮交叉验证——将数据集分为两个子集,依次将每个子集作为验证集。

因此,也可以利用Sklearn的交叉验证函数cross_val_scoreSklearn官方文档)实现两轮交叉验证

from sklearn.model_selection import cross_val_score

cross_val_score(model, X, y, cv=2)
array([0.94666667, 0.94666667])

交叉验证不是一种构建可以应用于新数据的模型的方法:交叉验证不会返回一个模型。在调用cross_val_score时,内部会构建多个模型,但交叉验证的目的只是评估给定算法在特定数据集上训练后的泛化性能的好坏。(若采用交叉验证来评价模型,则可以利用所有样本来训练模型,而无需单独留出验证集)

1.2.3 K折交叉验证

K折交叉验证(KFold Cross Validation)及在交叉验证的基础上,将数据划分为 k k k组(cv=k),其中 k − 1 k-1 k1组进行训练,剩下1组作为验证集,重复进行 k k k次,如下图所示:

我们可以将1.2.2的数据分为5组,进行5轮交叉检验:

from sklearn.model_selection import cross_val_score

cross_val_score(model, X, y, cv=5)
array([0.96666667, 0.96666667, 0.93333333, 0.93333333, 1.        ])

1.2.4 留一法 LOO

当我们交叉验证的轮数与样本数相同时,即每次只有一个样本做测试,其他样本全用于训练,这种交叉验证类型被称为留一法(LOO, Leave One Out),如下所示:

from sklearn.model_selection import LeaveOneOut
scores = cross_val_score(model, X, y, cv=LeaveOneOut())
print(scores)
scores.mean()
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1.
 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1.]





0.96

2. 偏差-方差

2.1 泛化误差、偏差及方差

泛化误差的含义,其实就是想看看训练后的模型是否具有代表性。我们使用偏差(bias)和方差(variance)来描述。

偏差是什么?给定了一些新的样本,我们使用训练好的模型对这个新样本进行估值,这个估值与真实值的差距就是偏差。

方差是什么?在不同的训练集上,即使是同一模型我们可能也会得到不同的参数,那么不同训练集上得到的假设函数对新样本的所做出的估值就是不同的。我们用这些不同估值的期望作为最终这个模型对新样本的估值,而方差就是估值与不同训练集得到的估值之间的离散程度。

这和我们统计上的期望与方差是类似的,可以对比来看。我们希望最终的估值与实际值相差不大,而且得到的模型也要相对稳定,这是就可以说该模型的通用型比较强,也就是泛化。[2]

2.2 泛化误差与偏差及方差的关系

我们刚才说偏差和方差可以衡量一个模型是否具有代表性,那么当我们在验证集上得到了泛化误差后怎么评价这个模型呢?我们来看一下泛化误差的构成:

泛 化 误 差 = 偏 差 2 + 方 差 + 噪 声 2 泛化误差=偏差^2+方差+噪声^2 =2++2

为了推导出这个公式,首先定义几个概念:

  1. 在训练集 d d d上,我们训练后的模型为 f d ( x ) f_d(x) fd(x)

  2. 该模型对数据 x x x的预测输出: f ( x ) ˉ = E d [ f d ( x ) ] \bar{f(x)}=E_d[f_d(x)] f(x)ˉ=Ed[fd(x)]

  3. 验证集样本的真实值: y y y,标签值: y d y_d yd

  4. 噪声为样本的标签值与真实值的出入: ε = y − y d \varepsilon=y-y_d ε=yyd,服从均值为0的高斯分布 ε ∼ N ( 0 , σ 2 ) \varepsilon \thicksim N(0, \sigma^2) εN(0,σ2)

  5. 偏差为预测输出与样本标签的差值: b i a s = y − f ( x ) ˉ bias=y-\bar{f(x)} bias=yf(x)ˉ

  6. 方差为预测输出与不同测试集差的离散程度: v a r = E d [ ( f d ( x ) − f ( x ) ˉ ) 2 ] var=E_d[(f_d(x)-\bar{f(x)})^2] var=Ed[(fd(x)f(x)ˉ)2]

  7. 泛化误差: E d [ ( y d − f d ( x ) ) 2 ] E_d[(y_d-f_d(x))^2] Ed[(ydfd(x))2]

泛化误差即每一组训练集得到结果后与验证集计算误差,误差的均值就作为衡量泛化的标准。

E d [ ( y d − f d ( x ) ) 2 ] = E d [ ( y d − f ( x ) ˉ + f ( x ) ˉ − f d ( x ) ) 2 ] = E d [ ( y d − f ( x ) ˉ ) 2 ] + E d [ ( f ( x ) ˉ − f d ( x ) ) 2 ] + 2 ⋅ E [ ( y d − f ( x ) ˉ ) ( f ( x ) ˉ − f d ( x ) ) ] = E d [ ( y d − f ( x ) ˉ ) 2 ] + E d [ ( f ( x ) ˉ − f d ( x ) ) 2 ] + 2 ⋅ 0 ⋅ E [ ( y d − f ( x ) ˉ ) ] = E d [ ( y d − f ( x ) ˉ ) 2 ] + E d [ ( f ( x ) ˉ − f d ( x ) ) 2 ] = E d [ ( y d − y + y − f ( x ) ˉ ) 2 ] + E d [ ( f ( x ) − f d ( x ) ˉ ) 2 ] = E d [ ( y d − y ) 2 ] + E d [ ( y − f ( x ) ˉ ) 2 ] + 2 ⋅ 0 ⋅ E d [ y − f ( x ) ˉ ] + E d [ ( f ( x ) − f d ( x ) ˉ ) 2 ] = E d [ ( y d − y ) 2 ] + E d [ ( y − f ( x ) ˉ ) 2 ] + E d [ ( f ( x ) − f d ( x ) ˉ ) 2 ] = ε 2 + b i a s 2 + v a r \begin{aligned} E_d[(y_d-f_d(x))^2] & = E_d[(y_d-\bar{f(x)}+\bar{f(x)}-f_d(x))^2] \\ & = E_d[(y_d-\bar{f(x)})^2]+E_d[(\bar{f(x)}-f_d(x))^2]+2\cdot E[(y_d-\bar{f(x)})(\bar{f(x)}-f_d(x))] \\ & = E_d[(y_d-\bar{f(x)})^2]+E_d[(\bar{f(x)}-f_d(x))^2]+2\cdot 0 \cdot E[(y_d-\bar{f(x)})] \\ & = E_d[(y_d-\bar{f(x)})^2]+E_d[(\bar{f(x)}-f_d(x))^2]\\ & = E_d[(y_d-y+y-\bar{f(x)})^2]+E_d[(f(x)-\bar{f_d(x)})^2]\\ & = E_d[(y_d-y)^2]+E_d[(y-\bar{f(x)})^2]+2\cdot 0\cdot E_d[y-\bar{f(x)}]+E_d[(f(x)-\bar{f_d(x)})^2]\\ & = E_d[(y_d-y)^2]+E_d[(y-\bar{f(x)})^2]+E_d[(f(x)-\bar{f_d(x)})^2]\\ & = \varepsilon^2+bias^2+var \end{aligned} Ed[(ydfd(x))2]=Ed[(ydf(x)ˉ+f(x)ˉfd(x))2]=Ed[(ydf(x)ˉ)2]+Ed[(f(x)ˉfd(x))2]+2E[(ydf(x)ˉ)(f(x)ˉfd(x))]=Ed[(ydf(x)ˉ)2]+Ed[(f(x)ˉfd(x))2]+20E[(ydf(x)ˉ)]=Ed[(ydf(x)ˉ)2]+Ed[(f(x)ˉfd(x))2]=Ed[(ydy+yf(x)ˉ)2]+Ed[(f(x)fd(x)ˉ)2]=Ed[(ydy)2]+Ed[(yf(x)ˉ)2]+20Ed[yf(x)ˉ]+Ed[(f(x)fd(x)ˉ)2]=Ed[(ydy)2]+Ed[(yf(x)ˉ)2]+Ed[(f(x)fd(x)ˉ)2]=ε2+bias2+var

由推导可知,对于每一次交叉验证我们可以得到一组误差 ( y d − f d ( x ) ) 2 (y_d-f_d(x))^2 (ydfd(x))2,当我们把所有误差求均值后得到泛化误差,泛化误差又可以分解为偏差、方差以及噪声:

(1) E d [ ( y d − f d ( x ) ) 2 ] = b i a s 2 + v a r + ε 2 E_d[(y_d-f_d(x))^2] = bias^2+var+\varepsilon^2\tag{1} Ed[(ydfd(x))2]=bias2+var+ε2(1)

3. 拟合程度

3.1 欠拟合与过拟合

欠拟合\高偏差:(Underfitting\High Bias)指过度简化了模型,而无法捕捉到数据的复杂度。表现为在训练集和测试集上正确率都很低。

过拟合\高方差:(Overfitting\High Variance)指过度复杂化了模型,因此这种学习器偏向于记住数据,而不是学习数据,这样就会导致泛化性能下降。表现为在训练集上表现很好(甚至在训练集上准确率到达100%),而在测试集上正确率却很低。

举个例子,对以下数据,分别使用一次线性模型,二次方程模型和六次多项式模型进行拟合,可以看到:

对于一次模型,它对与数据的拟合程度并不好,因此在测试集上表现不好;

对于六次模型,它记住了数据,但未能找到训练集的良好属性,以很好的泛化到测试剂,所以即使在训练集上表现很好,在测试集上也会表现很差;

而对于二次模型,它很好的学习到了训练集的特点,因此在训练集和测试集上表现都会不错。

3.2 影响拟合程度的因素

影响拟合程度的因素有很多,如模型的复杂程度和训练集规模。

对于一定数量的训练样本,简单的模型容易造成欠拟合,而太过复杂的模型容易造成过拟合;

对于一定复杂度的模型,过少的训练样本容易导致模型过拟合,过多的训练样本容易导致过拟合。

3.3 模型复杂度

模型复杂度与拟合程度的关系:[3]

我们可以利用公式(1)对不同模型复杂度下的泛化误差进行拆解,可以得到下面的图像,左侧代表了欠拟合,右侧代表了过拟合,可以看到左侧呈现出了高偏差的特点,而右侧呈现出了高方差的特点。

2.3.1 模型复杂度图表

x 轴:模型复杂度

y 轴:预测误差

图的左侧为一个欠拟合(高偏差)模型,右侧为一个过拟合(高误差)模型,

举个例子:对以下数据,分别使用一次线性模型,二次方程模型和六次多项式模型进行分类,我们将数据分为训练集和测试集,利用训练集进行训练,之后利用训练集和测试集进行预测并计算误差。我们将误差画在下面的表上,就得到了模型复杂度图表。

左侧是一个欠拟合模型,它具有较高的训练误差和测试误差;

右侧是一个过拟合模型,它具有较低的训练误差和较高的测试误差;

中间的一个较好的模型,它具有相对较低的训练和测试误差。

3.3.2 验证曲线

对之前的回归模型,我们利用回归指标 R 2 R^2 R2机器学习 | 分类评估指标)分别计算不同复杂度下的训练得分和验证得分:

将模型复杂度图标的 y 轴指标由误差改为模型得分,就得到了验证曲线。相同的,在验证曲线的左侧为欠拟合,在验证曲线的右侧为过拟合。

3.3.2.1 Sklearn 验证曲线

用交叉验证计算一个多项式回归模型,其多项式的次数是一个可调参数。在 Sklern 中,可以用一个带多项式的预处理器的简单线性回归模型实现。我们将用一个管道命令来组合这两种操作:

from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline

def PolynomialRegression(degree=2, **kwargs):
    return make_pipeline(PolynomialFeatures(degree),
                         LinearRegression(**kwargs))

现在来创造一些数据给模型拟合:

import numpy as np

def make_data(N, err=1.0, rseed=1):
    # randomly sample the data
    rng = np.random.RandomState(rseed)
    X = rng.rand(N, 1) ** 2
    y = 10 - 1. / (X.ravel() + 0.1)
    if err > 0:
        y += err * rng.randn(N)
    return X, y

X, y = make_data(40)

通过数据可视化,将不同次数的多项式拟合曲线画出来:

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()  # plot formatting

X_test = np.linspace(-0.1, 1.1, 500)[:, None]

plt.scatter(X.ravel(), y, color='black')
axis = plt.axis()
for degree in [1, 3, 5]:
    y_test = PolynomialRegression(degree).fit(X, y).predict(X_test)
    plt.plot(X_test.ravel(), y_test, label='degree={0}'.format(degree))
plt.xlim(-0.1, 1.0)
plt.ylim(-2, 12)
plt.legend(loc='best');

通过validation_curve函数(Sklearn 官方文档)画验证曲线,只需要提供模型、数据、参数名称和验证范围学习,函数就会自动计算验证范围内的训练得分和验证的分:

from sklearn.model_selection import validation_curve
degree = np.arange(0, 21)
train_score, val_score = validation_curve(PolynomialRegression(), X, y,
                                          'polynomialfeatures__degree', degree, cv=7)

plt.plot(degree, np.median(train_score, 1), color='blue', label='training score')
plt.plot(degree, np.median(val_score, 1), color='red', label='validation score')
plt.legend(loc='best')
plt.ylim(0, 1)
plt.xlabel('degree')
plt.ylabel('score');

从验证曲线中可以看出,偏差与方差的均衡性最好的是三次多项式。我们可以计算结果,并将模型画在原始数据上:

plt.scatter(X.ravel(), y)
lim = plt.axis()
y_test = PolynomialRegression(3).fit(X, y).predict(X_test)
plt.plot(X_test.ravel(), y_test);
plt.axis(lim);

虽然寻找最优模型并不需要我们计算训练得分,但是检查训练得分与验证得分之间的关系可以让我们对模型的性能有更加直观的认识。

3.4 训练集规模

影响模型复杂度的一个重要因素就是最优模型往往收到训练数据量的影响。例如对于3.3.2.1的例子,我们增加5倍数据量。与之前相同,画出200个样本点的验证曲线并叠加到40个样本点的验证曲线上,其中虚线为40个样本点的验证曲线,实现为200个样本点的验证曲线:

从验证曲线就可以明显看出,大数据集支持更复杂的模型,虽然得分顶点大概是六次多项式,但是即使到了20次多项式,过拟合的情况也不太严重——验证得分与训练得分依然十分接近。

通过观察验证曲线的变化趋势,可以发现有两个影响模型效果的因素:模型复杂度训练集规模

3.4.1 学习曲线

学习曲线(Learning Curve):通常,我们将模型看成是与训练数据规模相官的函数,通过不断增大数据集的规模来拟合模型,一次来观察模型的行为。反映训练集规模的训练得分/验证得分曲线被称为学习曲线[4]

通过画出不同训练集大小时训练集和交叉验证的准确率,可以看到模型在新数据上的表现,进而来判断模型是否方差偏高或偏差过高,以及增大训练集是否可以减小过拟合。

学习曲线的特征包含以下三点:

  1. 特定复杂度的模型对较少的数据集容易过拟合:此时训练得分较高,验证得分较低。

  2. 特定复杂度的模型对较大的数据集容易欠拟合:随着数据的增大,训练得分会不断降低,而验证得分会不断升高。

  3. 模型的验证集得分永远不会高于训练集得分:两条曲线一直在靠近,但永远不会交叉。

学习曲线最重要的特征是:随着训练样本数量的增加,两条得分曲线会收敛到一个定值。因此,一旦数据多到使模型得分已经收敛,**那么增加更多的训练样本也无济于事!**改善模型性能的唯一方法就是换模型(通常是换成更复杂的模型)。

3.4.1.1 学习曲线与拟合程度
  1. 欠拟合:当训练集和测试集的误差收敛但却很高时,为高偏差。左上角的偏差很高,训练集和验证集的准确率都很低,很可能是欠拟合。我们可以增加模型参数,比如,构建更多的特征,减小正则项。此时通过增加数据量是不起作用的。

  2. 过拟合:当训练集和测试集的误差之间有大的差距时,为高方差。当训练集的准确率比其他独立数据集上的测试结果的准确率要高时,一般都是过拟合。右上角方差很高,训练集和验证集的准确率相差太多,应该是过拟合。我们可以增大训练集,降低模型复杂度,增大正则项,或者通过特征选择减少特征数。

  3. 理想情况是是找到偏差和方差都很小的情况,即收敛且误差较小。[5]

3.4.1.2 Sklean 学习曲线

下面计算前面数据集的二次多项式模型和九次多项式模型的学习曲线:

import numpy as np

def make_data(N, err=1.0, rseed=1):
    # randomly sample the data
    rng = np.random.RandomState(rseed)
    X = rng.rand(N, 1) ** 2
    y = 10 - 1. / (X.ravel() + 0.1)
    if err > 0:
        y += err * rng.randn(N)
    return X, y

X, y = make_data(40)


from sklearn.model_selection import learning_curve

fig, ax = plt.subplots(1, 2, figsize=(16, 6))
fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)

for i, degree in enumerate([2, 9]):
    N, train_lc, val_lc = learning_curve(PolynomialRegression(degree),
                                         X, y, cv=7,
                                         train_sizes=np.linspace(0.3, 1, 25))

    ax[i].plot(N, np.mean(train_lc, 1), color='blue', label='training score')
    ax[i].plot(N, np.mean(val_lc, 1), color='red', label='validation score')
    ax[i].hlines(np.mean([train_lc[-1], val_lc[-1]]), N[0], N[-1],
                 color='gray', linestyle='dashed')

    ax[i].set_ylim(0, 1)
    ax[i].set_xlim(N[0], N[-1])
    ax[i].set_xlabel('training size')
    ax[i].set_ylabel('score')
    ax[i].set_title('degree = {0}'.format(degree), size=14)
    ax[i].legend(loc='best')

从图中可以看出,不同模型复杂度下,要达到收敛所需要的训练集数据量是不同的;而当学习曲线已经收敛时,**再增加训练数据也不能显著改善拟合效果!**这种情况就类似于左图显示的二次多项式模型的学习曲线。

提高收敛得分的唯一方法就是换模型(通常也是更复杂的模型)。如右图所示:采用复杂度更高的模型之后,虽然学习曲线的收敛得分提高了(对比虚线所在位置),但是模型的方差也变大了(对比训练得分与验证得分的差异即可看出)。

如果我们为复杂度更高的模型继续增加训练数据,那么学习曲线最终也会收敛。

参考资料

[1] Jake VanderPlas, 陶俊杰, 陈小莉. Python 数据科学手册[M]. 北京: 人民邮电出版社, 2019: 314-317.

[2] Cerisier.谈谈对泛化误差的理解[EB/OL].https://blog.csdn.net/Cerisier/article/details/78122653, 2017-09-28.

[3] Scott Fortmann-Roe.Understanding the Bias-Variance Tradeoff[EB/OL].http://scott.fortmann-roe.com/docs/BiasVariance.html, 2012-06.

[4] Jake VanderPlas, 陶俊杰, 陈小莉. Python 数据科学手册[M]. 北京: 人民邮电出版社, 2019: 322-326.

[5] 不会停的蜗牛.用学习曲线 learning curve 来判别过拟合问题[EB/OL].https://www.jianshu.com/p/d89dee94e247, 2017-06-22.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值