机器学习算法笔记(十七):过拟合与欠拟合、模型准确率与学习曲线

所谓欠拟合(Underfitting),就是算法所训练的模型不能完整表述数据之间的关系;所谓过拟合(Overfitting),就是算法所训练的模型过多表达了数据间的噪音关系(在拟合的过程中将数据的噪音当作了特征)。在我们学习使用机器学习算法的过程中,如何平衡过拟合和欠拟合的问题,将是我们永恒的话题。我们首先以先前的多项式回归为例,来看一下过拟合与欠拟合带来的问题。

一、欠拟合和过拟合在多项式回归中的表现

我们新建一个工程,使用Pipeline来实现一个多项式回归的回归器:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error

np.random.seed(666)
x = np.random.uniform(-3.0, 3.0, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x**2 + x + 2 + np.random.normal(0, 1, size=100)

#使用Pipeline实现多项式回归
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler

def PolynomialRegression(degree):
    return Pipeline([
        ("poly", PolynomialFeatures(degree=degree)),
        ("std_scaler", StandardScaler()),
        ("lin_reg", LinearRegression())
    ])

#令degree=1,即线性回归
poly1_reg = PolynomialRegression(degree=1)
poly1_reg.fit(X, y)
y1_predict = poly1_reg.predict(X)
print(mean_squared_error(y, y1_predict))
plt.scatter(x, y)
plt.plot(np.sort(x), y1_predict[np.argsort(x)], color='r')
plt.show()

#令degree=2
poly2_reg = PolynomialRegression(degree=2)
poly2_reg.fit(X, y)
y2_predict = poly2_reg.predict(X)
print(mean_squared_error(y, y2_predict))
plt.scatter(x, y)
plt.plot(np.sort(x), y2_predict[np.argsort(x)], color='r')
plt.show()

#令degree=10
poly10_reg = PolynomialRegression(degree=10)
poly10_reg.fit(X, y)
y10_predict = poly10_reg.predict(X)
print(mean_squared_error(y, y10_predict))
plt.scatter(x, y)
plt.plot(np.sort(x), y10_predict[np.argsort(x)], color='r')
plt.show()

#令degree=100
poly100_reg = PolynomialRegression(degree=100)
poly100_reg.fit(X, y)
y100_predict = poly100_reg.predict(X)
print(mean_squared_error(y, y100_predict))
plt.scatter(x, y)
plt.plot(np.sort(x), y100_predict[np.argsort(x)], color='r')
plt.show()

degree分别取1、2、10、100时,输出的图像和MSE如下:

红色曲线并不是所计算出的拟合曲线,而此红色曲线只是原有的数据点对应的 y 的预测值连接出来的结果,而且有的地方没有数据点,因此连接的结果和原来的曲线不一样,对degree=100的曲线,我们可以做如下处理来绘制真实图像:

# np.linspace(-3, 3, 100):在 [-3, 3] 之间均匀取 100 个值,包含 -3 和 3
X_plot = np.linspace(-3, 3, 100).reshape(100, 1)

# X_plot 的预测值
y_plot = poly100_reg.predict(X_plot)

# 绘制 X_plot 和 y_plot :绘制结果比之前的结果更准确,因为 X_plot 的取值是在[-3, 3]之间均匀取值,所以不会出现两点之间间隔太大
plt.scatter(x, y)
plt.plot(X_plot[:, 0], y_plot, color='r')

# 设置坐标轴的范围
plt.axis([-3, 3, -1, 10])
plt.show()

绘制结果比之前的结果更准确,因为 X_plot 的取值是在 [-3, 3] 之间均匀取值,所以不会出现两点之间间隔太大。对degree值取 1、2、10不用做处理,因为对于低次曲线来说,这样的绘图精度已经足够了。 

得到的degree=100时相对准确的图像

根据打印的MSE值可以看到,degree 越大拟合的效果越好,因为样本点是一定的,我们总能找到一条曲线将所有的样本点拟合,也就是说将所有的样本点都完全落在这根曲线上,使得整体的均方误差为 0。那么我们就不禁要问:随着 degree 的增大,虽然均方误差会逐渐减小,但均方误差更小时得到的拟合曲线真的是反应样本数值走势相应的曲线吗?

从后面绘制的degree=100的图像可以看出,显然并不是这样的。换句话说,我们用了一个很高维的数据,虽然学习到的曲线拟合了几乎所有的点,误差变小了,但这根曲线完全不是我们想要的样子,它为了能够拟合给定的所有的样本点变得太过复杂了,这种情况我们称之为过拟合(Overfitting)。相反的,在 degree 取 1,也就是我们直接用一条直线来拟合数据。这种方式显然也没有非常好的反映样本特征,但它犯的错误并不是太过复杂,而是太过简单,这种情况我们称之为欠拟合(Underfitting)

运用多项式回归能非常直观的描述过拟合和欠拟合。若我们用二次方程来生成数据,使用线性回归会是欠拟合的;使用degree=10甚至100来拟合显然会是过拟合的。

二、模型的泛化能力

所谓“泛化能力”,就是“由此及彼”的能力。也就是说我们根据已知的训练数据得到了一条曲线,但这条曲线在面对新的数据的时候,它预测新数据能力就非常的弱,也就是所谓的“泛化能力非常差”(比如上面 degree=100 的模型,在靠近-3与3时泛化能力非常差)。我们在训练模型时,不是为了最大程度拟合这些点,而是为了获得一个可以预测的模型,当出现一个新的样本时,模型能够很好的进行解答。正因如此,我们去衡量模型对于训练数据的拟合程度是没有意义的,我们需要去衡量这个模型的泛化能力有多好。

为了衡量模型的泛化能力,我们就要将数据集分割成训练集和测试集,使用训练集来训练模型。若训练得到的模型对于测试集来说也能给出很好的结果,我们就说这个模型泛化能力比较强,是一个好的模型;若训练得到的模型对于测试集不能给出很好的结果,那我们的模型泛化能力是很弱的,多半就遭遇了过拟合。这就是我们之前将数据集分离成测试集和训练集的更大意义。

三、模型复杂度与模型准确率的关系

“模型复杂度”这个概念在不同机器学习算法中是不同的。在多项式回归中,degree 越大,多项式回归的模型越复杂;在kNN算法中,k 越小,kNN算法的模型就越复杂。

当 k 取最大值,即与样本总数一样时,模型最简单,此时 kNN算法就变成了看整个样本里哪个样本最多,就选谁;当 k=1时,是 kNN算法中最复杂的模型,因为对于每一个预测点我们都要找到离它最近的一个点。

对于模型来说,也有这么一个“准确率”的概念,就是它能够多么好的预测我们的数据。在这种情况下,通常模型复杂度与模型准确率有着这样的关系:

对于训练数据集来说,随着模型越来越复杂,模型的准确率对于训练数据集越来越高。因为模型越复杂,对于训练数据集的拟合程度越高,对应训练数据集的准确率越来越高。对于测试数据集来说,随着模型复杂度的增高,模型对新的样本数据的预测准确度会先逐渐升高,后逐渐降低。模型复杂度对于新的样本的拟合准确率变化的过程,其实就是模型从欠拟合到过拟合的过程。我们真正要找的就是泛化能力最好的模型(即上图中黑色竖线的部分),通常我们需要通过调参等手段来找到这个泛化能力最好的模型去运用到生产环境中。

四、学习曲线

学习曲线反映了一个算法随着训练样本的逐渐增多,算法训练出的模型的表现能力。绘制模型的学习曲线可以查看模型的学习效果,也可以通过学习曲线可以清晰的看出模型对数据的过拟合和欠拟合。

下面我们来通过编程来实现绘制一个算法学习曲线,实现如下:

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(666)
x = np.random.uniform(-3.0, 3.0, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x**2 + x + 2 + np.random.normal(0, 1, size=100)

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=10)

from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# 存储每一次训练的模型的均方误差
train_score = []
test_score = []

# for 循环:进行 75 次模型训练,每次训练出 1 个模型,第一次给 1 个数据,第二次给 2 个数据,... ,最后一次给 75 个数据
for i in range(1, 76):
    lin_reg = LinearRegression()
    lin_reg.fit(X_train[:i], y_train[:i])
    # LinearRegression().fit(X_train[:i], y_train[:i])

    # 查看模型的预测情况:两种,模型基于训练数据集预测的情况(可以理解为模型拟合训练数据集的情况),模型基于测试数据集预测的情况
    # 此处使用 lin_reg.predict(X_train[:i]),为训练模型的全部数据集
    y_train_predict = lin_reg.predict(X_train[:i])
    train_score.append(mean_squared_error(y_train[:i], y_train_predict))

    y_test_predict = lin_reg.predict(X_test)
    test_score.append(mean_squared_error(y_test, y_test_predict))

# np.sqrt(train_score):将列表 train_score 中的数开平方
plt.plot([i for i in range(1, 76)], np.sqrt(train_score), label='train')
plt.plot([i for i in range(1, 76)], np.sqrt(test_score), label='test')

# plt.legend():显示图例(如图形的 label);
plt.legend()
plt.show()

绘制得到的曲线如下:

可以看出,随着数据集的增大:
● 所训练出的模型相对于训练数据集的均方误差在逐渐增大.因为随着数据集的增大,样本点的增多,模型越难拟合住所有的数据,相应的均方误差会逐渐的累积。但随着训练数据集的增大,均方差的逐渐累积量越来越小,模型相应的会越来越稳定。
● 相对于测试数据集的测试误差逐渐减小,当数据量增大到一定程度,测试误差趋于相对稳定。
● 最终的训练误差和测试误差几乎接近,相当于一个级别,不过测试误差比训练误差相对较大一点,因为训练数据集训练出的模型对训练数据集拟合的程度较好,相对的模型对应训练数据集的均方误差相对较小。

我们把绘制学习曲线的方法封装成一个函数,具体实现如下:

def plot_learning_curve(algo, X_train, X_test, y_train, y_test):
    #绘制学习曲线:只需要传入算法(或实例对象)、X_train、X_test、y_train、y_test
    #当使用该函数时传入算法,该算法的变量要进行实例化,如:PolynomialRegression(degree=2),变量 degree 要进行实例化
    train_score = []
    test_score = []
    for i in range(1, len(X_train) + 1):
        algo.fit(X_train[:i], y_train[:i])

        y_train_predict = algo.predict(X_train[:i])
        train_score.append(mean_squared_error(y_train[:i], y_train_predict))

        y_test_predict = algo.predict(X_test)
        test_score.append(mean_squared_error(y_test, y_test_predict))

    plt.plot([i for i in range(1, len(X_train) + 1)],
             np.sqrt(train_score), label="train")
    plt.plot([i for i in range(1, len(X_train) + 1)],
             np.sqrt(test_score), label="test")

    plt.legend()
    plt.axis([0, len(X_train) + 1, 0, 4])
    plt.show()

封装好之后,我们可以运用这个函数来绘制多项式回归在degree=1、2、20时的学习曲线:

from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

def PolynomialRegression(degree):
    return Pipeline([
        ("poly", PolynomialFeatures(degree=degree)),
        ("std_scaler", StandardScaler()),
        ("lin_reg", LinearRegression())
    ])

poly1_reg = PolynomialRegression(degree=1)
plot_learning_curve(poly1_reg, X_train, X_test, y_train, y_test)

poly2_reg = PolynomialRegression(degree=2)
plot_learning_curve(poly2_reg, X_train, X_test, y_train, y_test)

poly20_reg = PolynomialRegression(degree=20)
plot_learning_curve(poly20_reg, X_train, X_test, y_train, y_test)

绘制的结果如下:

三幅图像分别表示欠拟合、最佳、过拟合的情况

● 对于欠拟合情况,和最佳的情况相比较,相对的 train 和 test 两根曲线趋于稳定的位置,比最佳的两个曲线趋于稳定的位置较高。说明无论对于训练数据集还是测试数据集,相应的误差都比较大,这是因为本身模型选的不正确,即使在训练数据集上,误差也比较大。
● 对于过拟合情况,在训练数据集上,相应的误差和最佳时的误差差不多,甚至当 degree 取值更大时,过拟合的误差比最佳时的误差小。但测试数据集的误差相比较大,并且从图上看出,测试数据集的误差曲线距离训练数据集的误差曲线较远。说明此时模型的泛化能力不够好,对于新的数据预测误差较大。

发表评论

电子邮件地址不会被公开。 必填项已用*标注