在机器学习领域,模型的泛化误差通常可以分解为偏差、方差和噪声三个部分。偏差指的是模型在不同训练集上的平均误差,反映了模型的复杂度。方差则描述了模型对训练集变化的敏感程度。噪声是数据本身固有的属性,不受模型影响。
以下面的函数f(x) = cos(3/2 * pi * x)
为例,可以看到一些带有噪声的样本点。使用三种不同的估计器来拟合这个函数:线性回归、一阶多项式特征的线性回归和十五阶多项式特征的线性回归。发现,第一个估计器由于过于简单(高偏差),最多只能提供对样本和真实函数的粗略拟合;第二个估计器几乎完美地近似了真实函数;而最后一个估计器虽然完美地拟合了训练数据,但对真实函数的拟合并不理想,即它对变化的训练数据非常敏感(高方差)。
偏差和方差是估计器固有的属性,通常需要选择学习算法和超参数,使得偏差和方差尽可能低。另一种降低模型方差的方法是使用更多的训练数据。然而,只有在真实函数过于复杂,无法用方差较低的估计器近似时,才应该收集更多的训练数据。
在简单的一维问题中,很容易看出估计器是否受到偏差或方差的影响。然而,在高维空间中,模型可能变得非常难以可视化。因此,使用下面介绍的工具通常很有帮助。
欠拟合是指模型过于简单,无法捕捉数据的基本结构。过拟合则是指模型过于复杂,学习了数据中的噪声。通过绘制验证曲线和学习曲线,可以评估模型是否欠拟合或过拟合。
要验证模型,需要一个评分函数,例如分类器的准确率。选择多个超参数的正确方法是使用网格搜索或类似方法,选择在验证集或多个验证集上得分最高的超参数。需要注意的是,如果基于验证分数优化超参数,验证分数就会有偏差,不再是泛化能力的准确估计。要获得泛化能力的准确估计,必须在另一个测试集上计算分数。
有时,绘制单个超参数对训练分数和验证分数的影响有助于了解估计器是否对某些超参数值过拟合或欠拟合。函数validation_curve
可以帮助实现这一点。
import numpy as np
from sklearn.model_selection import validation_curve
from sklearn.datasets import load_iris
from sklearn.svm import SVC
np.random.seed(0)
X, y = load_iris(return_X_y=True)
indices = np.arange(y.shape[0])
np.random.shuffle(indices)
X, y = X[indices], y[indices]
train_scores, valid_scores = validation_curve(
SVC(kernel="linear"), X, y, param_name="C", param_range=np.logspace(-7, 3, 3))
如果训练分数和验证分数都很低,估计器就会欠拟合。如果训练分数高而验证分数低,估计器就是过拟合,否则它工作得很好。训练分数低而验证分数高通常是不可能的。欠拟合、过拟合和工作良好的模型在下面的图中显示,变化SVM的参数gamma在数字数据集上。
学习曲线显示了估计器在不同数量的训练样本上的验证和训练分数。这是一个工具,用于了解从添加更多训练数据中受益多少,以及估计器是否更多地受到方差错误或偏差错误的影响。
考虑以下示例,绘制了一个朴素贝叶斯分类器和SVM的学习曲线。对于朴素贝叶斯,随着训练集大小的增加,验证分数和训练分数都收敛到一个相当低的值。因此,可能不会从更多的训练数据中受益太多。
相比之下,对于少量数据,SVM的训练分数远高于验证分数。添加更多的训练样本可能会提高泛化能力。可以使用函数learning_curve
生成绘制此类学习曲线所需的值(使用的样本数量,训练集上的平均分数和验证集上的平均分数):
from sklearn.model_selection import learning_curve
from sklearn.svm import SVC
train_sizes, train_scores, valid_scores = learning_curve(
SVC(kernel='linear'), X, y, train_sizes=[50, 80, 110], cv=5)
from sklearn.datasets import load_iris
from sklearn.model_selection import LearningCurveDisplay
from sklearn.svm import SVC
from sklearn.utils import shuffle
X, y = load_iris(return_X_y=True)
X, y = shuffle(X, y, random_state=0)
LearningCurveDisplay.from_estimator(
SVC(kernel="linear"), X, y, train_sizes=[50, 80, 110], cv=5)