95scikit-learn|95scikit-learn 机器学习入门实战--模型验证和模型选择

模型验证和模型选择 模型验证
机器学习中最重要的部分之一就是模型验证:即检查模型对给定数据集的适应程度。这里选择前面实验中用过的手写数字数据集,来看看如何检查模型对数据的拟合程度:

import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_digits# 导入数据集 %matplotlib inlineplt.style.use('seaborn')# 样式美化digits = load_digits()# 加载数据集 X = digits.data y = digits.target y

我们拟合一个 KNN(K 近邻)分类器:
from sklearn.neighbors import KNeighborsClassifier# 导入KNN估计器knn = KNeighborsClassifier(n_neighbors=1)# 选取最近的点的个数 1 knn.fit(X, y)

现在,我们使用此分类器来预测数据的标签并检查预测的效果如何:
y_pred = knn.predict(X)# 得到预测标签 print("{0} / {1} correct".format(np.sum(y == y_pred), len(y)))# 查看预测效果

这结果似乎很完美,预测准确率达到了 100%,但实际上我们犯了个错误,那就是,训练数据和测试数据为同一组数据。这并不是一个好方法,如果我们以这种方法优化估计器,我们将过拟合。也就是说,我们会过度学习训练数据,包括学习训练集中的噪声。
测试模型的更好方法是使用没有参加训练的数据集。在使用 scikit-learn 的 train_test_split 工具的时候,我们已经用到了这个方法:
from sklearn.model_selection import train_test_split# 切分数据集 70%用于训练,30%用于验证 X_train, X_test, y_train, y_test = train_test_split(X, y) X_train.shape, X_test.shape

现在我们根据训练数据进行训练,并根据测试数据进行验证:
knn = KNeighborsClassifier(n_neighbors=1) knn.fit(X_train, y_train)# 模型拟合(训练) y_pred = knn.predict(X_test)# 得到预测标签 print("{0} / {1} correct".format(np.sum(y_test == y_pred), len(y_test)))

这使我们对模型的运行方式有了更可靠的理解。
我们在这里使用的度量标准,将匹配数与样本总数进行比较,称为准确性,可以使用以下方法进行计算:
from sklearn.metrics import accuracy_score# 导入评估函数accuracy_score(y_test, y_pred)

其实,我们也可以直接从 model.score 方法计算得出:
knn.score(X_test, y_test)

使用此方法,我们可以查看随着模型参数(在这种情况下为选取最近的点的个数)的变化,准确性是如何变化的:
for n_neighbors in [1, 5, 10, 20, 30]: knn = KNeighborsClassifier(n_neighbors) knn.fit(X_train, y_train) print(n_neighbors, knn.score(X_test, y_test))

我们看到在这种情况下,选取的最近的点的个数(n_neighbors)较少似乎是最佳选择。
交叉验证
验证集的一个问题是我们“丢失”了一些数据。在上面的步骤中,我们仅将 3/4 的数据用于训练,而将 1/4 的数据用于验证。我们还有另一个选择,使用 2 折交叉验证,即我们将样本分成两半,然后执行两次验证:
# 平分数据集为两半 X1, X2, y1, y2 = train_test_split(X, y, test_size=0.5, random_state=0) print(X1.shape, X2.shape)# 将数据进行两次验证,即第二次验证集为上一次的测试集 print(KNeighborsClassifier(1).fit(X2, y2).score(X1, y1)) print(KNeighborsClassifier(1).fit(X1, y1).score(X2, y2))

2 折交叉验证为我们提供了准确性的两个估计,上面是我们自己写的 2 折交叉验证过程,scikit-learn 中有个函数可以直接执行:
from sklearn.model_selection import cross_val_score# 导入交叉验证函数# cv=2 即 2 折交叉验证 cross_val_score(KNeighborsClassifier(1), X, y, cv=2)

K 折交叉验证
上面,我们使用了 2 折交叉验证。这只是K折交叉验证的一种,在交叉验证中,我们还可以将数据分成K块并执行 KK 次拟合,其中每块都作为验证集轮流进行。我们可以通过更改上面的 cv 参数来实现。让我们试试 10 折交叉验证:
cross_val_score(KNeighborsClassifier(1), X, y, cv=10)

返回值为每次交叉验证运行得到的准确性的数组。
过拟合、欠拟合和模型选择
现在,我们已经了解了验证和交叉验证的基础知识,是时候进一步深入探讨模型选择了。
与验证和交叉验证相关的问题是机器学习实践中很最重要的部分,而为数据选择最佳模型则至关重要。那么当我们的估计器表现得并不好时,我们应该怎么改进呢?
使用更简单或更复杂的模型?
增加更多训练数据?
答案通常是违反直觉的。特别是,有时使用更复杂的模型会产生更差的结果。另外,有时增加训练数据不会改善您的结果。确定哪些步骤将改善您的模型的能力是成功的机器学习从业者与失败者之间的区别。
接下来,我们将处理一个简单的一维回归问题。我们可以轻松地可视化数据和模型,并将结果推广到高维数据集。我们使用 sklearn.linear_model 模块来完成。首先,创建一个我们想要拟合的简单非线性函数:
def test_func(x, err=0.5): y = 10 - 1. / (x + 0.1) if err > 0: y = np.random.normal(y, err)# 生成均值为y,标准差为err的正态分布 return y

然后,用上面的非线性函数生成数据集:
def make_data(N=40, error=1.0, random_seed=1): np.random.seed(1) X = np.random.random(N)[:, np.newaxis]# 生成40个0-1的随机数并增加一个维度 y = test_func(X.ravel(), error)# X.ravel()又将X变成一维数组return X, yX, y = make_data(40, error=1) plt.scatter(X.ravel(), y)# 绘制散点图

现在我们对该数据进行回归。使用内置的线性回归估计器来计算拟合:
from sklearn.metrics import mean_squared_error# 导入均方误差回归损失计算函数 from sklearn.linear_model import LinearRegression# 导入线性回归估计器 X_test = np.linspace(-0.1, 1.1, 500)[:, None]# 生成 500 个测试数据model = LinearRegression() model.fit(X, y)# 拟合模型 y_test = model.predict(X_test)# 得到预测值 plt.scatter(X.ravel(), y)# 绘制原数据散点图 plt.plot(X_test.ravel(), y_test)# 绘制预测线 plt.title("mean squared error: {0:.3g}".format( mean_squared_error(model.predict(X), y))) # 计算均方误差回归损失

我们已经对数据拟合了一条直线,但是显然此模型并不适合此数据。
我们尝试通过创建更复杂的模型来改善这一点。我们可以通过添加自由度并在输入上计算多项式回归来实现。scikit-learn 通过 PolynomialFeatures 多项式特征生成器使此操作变得容易。
from sklearn.preprocessing import PolynomialFeatures# 导入多项式特征生成器 from sklearn.linear_model import LinearRegression from sklearn.pipeline import make_pipeline# 导入算法串联# 将 2 次多项式与线性函数串联起来def PolynomialRegression(degree=2, **kwargs): return make_pipeline(PolynomialFeatures(degree), LinearRegression(**kwargs))

现在,我们将使用它来用二次曲线拟合数据:
model = PolynomialRegression(2)# 创建二次曲线模型 model.fit(X, y) y_test = model.predict(X_test)# 得到预测值plt.scatter(X.ravel(), y)# 绘制训练数据 plt.plot(X_test.ravel(), y_test)# 绘制预测曲线 plt.title("mean squared error: {0:.3g}".format( mean_squared_error(model.predict(X), y))) # 计算均方误差回归损失

这样可以减少均方误差,并且拟合效果更好。我们再看看用更高阶的多项式的效果:
model = PolynomialRegression(30)# 创建含 30 次多项式的模型 model.fit(X, y) y_test = model.predict(X_test)plt.scatter(X.ravel(), y) plt.plot(X_test.ravel(), y_test) plt.title("mean squared error: {0:.3g}".format( mean_squared_error(model.predict(X), y))) plt.ylim(-4, 14)# 设置 y 轴范围

当我们将多项式次数提高到这种程度时,很明显,拟合结果不再反映真实的分布,而是对训练数据中的噪声更加敏感。因此,我们称其为高方差模型,并且说它过度拟合了数据,即过拟合。
我们还可以用 interact 交互功能来看看随着训练数据数量和多项式次数的变化,拟合曲线的变化:
from ipywidgets import interact# 初始数据数为50,多项式为一次def plot_fit(degree=1, Npts=50): X, y = make_data(Npts, error=1)# 生成 Npts 个数据 X_test = np.linspace(-0.1, 1.1, 500)[:, None]model = PolynomialRegression(degree=degree) model.fit(X, y)# 拟合模型 y_test = model.predict(X_test)plt.scatter(X.ravel(), y)# 绘制数据散点图 plt.plot(X_test.ravel(), y_test)# 绘制预测曲线 plt.ylim(-4, 14) plt.title("mean squared error: {0:.2f}".format( mean_squared_error(model.predict(X), y)))# 计算均方误差回归损失# interact(plot_fit, degree=(1, 30), Npts=(2, 100))

验证曲线检测过拟合
显然,仅计算训练数据上的均方误差是不够的。如上所述,我们可以使用交叉验证来更好地判断模型拟合,并绘制验证曲线直观看看拟合效果。
为了使结果更明显,我们将使用稍大的数据集:
# 生成 120 个非线性数据 X, y = make_data(120, error=1.0) plt.scatter(X, y)

接下来,让我们使用 validation_curve 函数来实现验证曲线的绘制,代码如下:
from sklearn.model_selection import validation_curve# 导入验证曲线函数# 均方误差计算def rms_error(model, X, y): y_pred = model.predict(X) return np.sqrt(np.mean((y - y_pred) ** 2))# 设置多项式次数 degree = np.arange(0, 18) # 以均方误差计算训练集和测试集得分 val_train, val_test = validation_curve(PolynomialRegression(), X, y, 'polynomialfeatures__degree', degree, cv=7, scoring=rms_error) # 绘制训练和验证曲线def plot_with_err(x, data, **kwargs): mu, std = data.mean(1), data.std(1) lines = plt.plot(x, mu, '-', **kwargs) plt.fill_between(x, mu - std, mu + std, edgecolor='none', facecolor=lines[0].get_color(), alpha=0.2)# 填充plot_with_err(degree, val_train, label='training scores') plot_with_err(degree, val_test, label='validation scores') plt.xlabel('degree') plt.ylabel('rms error') plt.legend()

请注意此处的趋势,这是此类图的常见趋势。
模型复杂度较小时,训练误差和验证误差非常相似。这表明模型不适合此数据:它没有足够的复杂度来表示数据。换句话说,这是一个高偏差模型。
随着模型复杂度的增加,训练和验证分数也会有所不同。这表明该模型过拟合了:它具有很大的灵活性,适合噪声而不是潜在趋势。换句话说,这是一个高方差模型。
验证数据通常有一个最佳点,这里大概在 5 左右。
根据交叉验证,这就是我们最合适的模型:
model = PolynomialRegression(4).fit(X, y)# 4 次多项式模型 plt.scatter(X, y) plt.plot(X_test, model.predict(X_test))

学习曲线检测数据充足性
不难猜到,偏差和方差之间的确切转折点与训练数据量有关。在这里,我们将说明学习曲线的用法,该曲线显示了不同数据量下的学习得分。
根据训练点数绘制训练和测试集的均方误差:
from sklearn.model_selection import learning_curve# 导入学习曲线函数def plot_learning_curve(degree=3): train_sizes = np.linspace(0.05, 1, 120) # 得到5折交叉验证后的训练示例数、训练集得分和验证集得分 N_train, val_train, val_test = learning_curve(PolynomialRegression(degree), X, y, train_sizes, cv=5, scoring=rms_error) plot_with_err(N_train, val_train, label='training scores')# 绘制训练得分 plot_with_err(N_train, val_test, label='validation scores')# 绘制验证得分 plt.xlabel('Training Set Size') plt.ylabel('rms error') plt.ylim(0, 3)# 设置参数范围 plt.xlim(5, 80) plt.legend()

我们看看 degree=1 时,即线性模型的学习曲线:
plot_learning_curve(1)

这显示了一条典型的学习曲线:对于很少的训练点,训练误差和测试误差之间存在很大的距离,这表明过拟合。给定相同的模型,对于大量的训练点,训练和测试误差收敛,这表明欠拟合。
随着数据量的增加,训练误差将不会增加,测试误差也将不会减少。
可以很容易地看出,在此图中,如果您希望将 MSE 降低到标准值1.0(这是我们在构建数据时输入的分散量的大小),样本数的增加并没有什么用。对于 degree=1,两条曲线已经收敛并且不能向下移动。 那么我们增加 degreedegree 的值呢?
plot_learning_curve(3)

从上图结果看到,通过增加更多的模型复杂度,我们可以将收敛水平降低到均方误差为 1.0。
那如果模型复杂度再进一步提高呢?
plot_learning_curve(10)

对于一个更复杂的模型,仍然会收敛,但是收敛要求的数据量越来越大。因此:
可以通过增加数据量或简化模型来使曲线收敛。
只能通过增加模型的复杂度来减小收敛误差。
【95scikit-learn|95scikit-learn 机器学习入门实战--模型验证和模型选择】学习曲线告诉我们如何优化模型。如果曲线已经很靠近,则需要更多的模型复杂度。如果曲线相距较远,则还可以通过增加数据量来改进模型。

    推荐阅读