Python 机器学习 基础 之 模型评估与改进 【模型评估与改进 / 交叉验证】的简单说明
目录
Python 机器学习 基础 之 模型评估与改进 【模型评估与改进 / 交叉验证】的简单说明
一、简单介绍
二、模型评估与改进
三、交叉验证
附录
一、参考文献
一、简单介绍
Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。
Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。
Python 机器学习涵盖了许多任务和技术,包括但不限于:
- 监督学习:包括分类、回归等任务。
- 无监督学习:如聚类、降维等。
- 半监督学习:结合了有监督和无监督学习的技术。
- 强化学习:通过与环境的交互学习来优化决策策略。
- 深度学习:利用深度神经网络进行学习和预测。
通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。
二、模型评估与改进
机器学习模型的评估与改进是确保模型性能的关键步骤。下面是一些常见的方法和技术,用于评估和改进机器学习模型:
1)交叉验证
交叉验证是一种评估模型性能的常用方法,特别是在数据集较小或需要防止过拟合的情况下。
from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.datasets import load_irisiris = load_iris() X, y = iris.data, iris.targetmodel = LogisticRegression(max_iter=200) scores = cross_val_score(model, X, y, cv=5)print("Cross-validation scores:", scores) print("Mean cross-validation score:", scores.mean())
2) 网格搜索和随机搜索
网格搜索(Grid Search)和随机搜索(Random Search)用于超参数优化,以找到最佳的超参数组合。
from sklearn.model_selection import GridSearchCVparam_grid = {'C': [0.1, 1, 10, 100], 'solver': ['lbfgs', 'saga']} grid_search = GridSearchCV(LogisticRegression(max_iter=200), param_grid, cv=5)grid_search.fit(X, y) print("Best parameters:", grid_search.best_params_) print("Best cross-validation score:", grid_search.best_score_)
3)学习曲线
学习曲线用于诊断模型的学习情况,帮助确定模型是否存在过拟合或欠拟合问题。
from sklearn.model_selection import learning_curve import matplotlib.pyplot as plttrain_sizes, train_scores, test_scores = learning_curve(model, X, y, cv=5) train_scores_mean = train_scores.mean(axis=1) test_scores_mean = test_scores.mean(axis=1)plt.plot(train_sizes, train_scores_mean, label="Training score") plt.plot(train_sizes, test_scores_mean, label="Cross-validation score") plt.xlabel("Training examples") plt.ylabel("Score") plt.legend() plt.show()
4) 验证曲线
验证曲线用于观察不同超参数对模型性能的影响。
from sklearn.model_selection import validation_curveparam_range = np.logspace(-3, 3, 7) train_scores, test_scores = validation_curve(model, X, y, param_name="C", param_range=param_range, cv=5)train_scores_mean = train_scores.mean(axis=1) test_scores_mean = test_scores.mean(axis=1)plt.plot(param_range, train_scores_mean, label="Training score") plt.plot(param_range, test_scores_mean, label="Cross-validation score") plt.xlabel("Parameter C") plt.ylabel("Score") plt.xscale("log") plt.legend() plt.show()
5)特征选择
特征选择用于选择对模型性能有显著影响的特征,去除不相关或冗余的特征。
from sklearn.feature_selection import SelectKBest, f_classifselector = SelectKBest(f_classif, k=2) X_new = selector.fit_transform(X, y)print("Selected features shape:", X_new.shape)
6)管道(Pipeline)
管道用于将多个处理步骤整合到一起,确保在交叉验证和网格搜索中,数据处理步骤与模型一起执行。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScalerpipeline = Pipeline([('scaler', StandardScaler()),('classifier', LogisticRegression(max_iter=200)) ])param_grid = {'classifier__C': [0.1, 1, 10, 100]} grid_search = GridSearchCV(pipeline, param_grid, cv=5)grid_search.fit(X, y) print("Best parameters:", grid_search.best_params_) print("Best cross-validation score:", grid_search.best_score_)
7) 模型评估指标
根据具体任务选择合适的评估指标,如分类任务中的准确率、精确率、召回率、F1-score,回归任务中的均方误差、
等。
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_scorey_pred = model.predict(X) print("Accuracy:", accuracy_score(y, y_pred)) print("Precision:", precision_score(y, y_pred, average='macro')) print("Recall:", recall_score(y, y_pred, average='macro')) print("F1 Score:", f1_score(y, y_pred, average='macro'))
通过这些方法和技术,机器学习模型的性能可以得到有效的评估和改进。选择合适的方法取决于具体的任务和数据集的特点。
到目前为止,为了评估我们的监督模型,我们使用 train_test_split
函数将数据集划分为训练集和测试集,在训练集上调用 fit
方法来构建模型,并且在测试集上用 score
方法来评估这个模型——对于分类问题而言,就是计算正确分类的样本所占的比例。
from sklearn.datasets import make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split# 创建一个模拟数据集
X, y = make_blobs(random_state=0)
# 将数据和标签划分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 将模型实例化,并用它来拟合训练集
logreg = LogisticRegression().fit(X_train, y_train)
# 在测试集上评估该模型
print("Test set score: {:.2f}".format(logreg.score(X_test, y_test)))
Test set score: 0.88
请记住,之所以将数据划分为训练集和测试集,是因为我们想要度量模型对前所未见的新数据的泛化 性能。我们对模型在训练集上的拟合效果不感兴趣,而是想知道模型对于训练过程中没有见过的数据的预测能力。
本节我们将从两个方面进行模型评估。我们首先介绍交叉验证,然后讨论评估分类和回归性能的方法,其中前者是一种更可靠的评估泛化性能的方法,后者是在默认度量(score
方法给出的精度和 R2 )之外的方法。
我们还将讨论网格搜索,这是一种调节监督模型参数以获得最佳泛化性能的有效方法。
三、交叉验证
交叉验证 (cross-validation)是一种评估泛化性能的统计学方法,它比单次划分训练集和测试集的方法更加稳定、全面。在交叉验证中,数据被多次划分,并且需要训练多个模型。最常用的交叉验证是 k 折交叉验证 (k -fold cross-validation),其中 k 是由用户指定的数字,通常取 5 或 10。在执行 5 折交叉验证时,首先将数据划分为(大致)相等的 5 部分,每一部分叫作折 (fold)。
交叉验证(Cross-Validation)是机器学习中用于评估模型性能的一种技术。它通过将数据集划分为多个子集,反复训练和验证模型,从而有效地评估模型在未见数据上的表现。以下是交叉验证的主要方法和步骤:
交叉验证的主要方法
k折交叉验证(k-Fold Cross-Validation):
- 将数据集分成k个互不重叠的子集(折),每个子集大小相同。
- 进行k次训练和验证,每次使用k-1个子集进行训练,剩余的一个子集进行验证。
- 计算k次验证结果的平均值作为模型的性能指标。
留一法交叉验证(Leave-One-Out Cross-Validation, LOOCV):
- 是k折交叉验证的一种特殊情况,其中k等于样本数量。
- 每次选择一个样本作为验证集,其余样本作为训练集。
- 适用于小数据集,但计算开销较大。
分层k折交叉验证(Stratified k-Fold Cross-Validation):
- 类似于k折交叉验证,但在每个折内保持类别分布的一致性。
- 适用于类别不平衡的数据集。
时间序列交叉验证(Time Series Split):
- 适用于时间序列数据。
- 保持数据的时间顺序,防止未来数据泄露到训练集中。
- 逐步增加训练集的大小,同时将验证集设为紧跟在训练集之后的一段时间。
交叉验证的优点和局限性
优点:
- 防止过拟合和欠拟合:通过在多个不同的数据集上训练和验证模型,交叉验证可以有效检测过拟合和欠拟合问题。
- 更稳定和可靠的性能估计:相比单次划分训练集和测试集,交叉验证提供了更稳定和可靠的模型性能估计。
- 充分利用数据:在不同的折中,所有数据点都被用作训练集和验证集,充分利用了数据。
局限性:
- 计算开销大:特别是对于大型数据集或复杂模型,交叉验证的计算开销较大。
- 不适用于时间序列数据:标准的交叉验证方法不适用于时间序列数据,需要使用时间序列交叉验证方法。
通过交叉验证,可以更全面地评估模型的性能,从而更好地选择模型和调节超参数,提高模型在实际应用中的表现。
接下来训练一系列模型。使用第 1 折作为测试集、其他折(2~5)作为训练集来训练第一个模型。利用 2~5 折中的数据来构建模型,然后在 1 折上评估精度。之后构建另一个模型,这次使用 2 折作为测试集,1、3、4、5 折中的数据作为训练集。利用 3、4、5 折作为测试集继续重复这一过程。对于将数据划分为训练集和测试集的这 5 次划分 ,每一次都要计算精度。最后我们得到了 5 个精度值。整个过程如图 5-1 所示。
import mglearn
import matplotlib.pyplot as pltmglearn.plots.plot_cross_validation()plt.tight_layout()
plt.savefig('Images/01CrossValidation-01.png', bbox_inches='tight')
plt.show()

通常来说,数据的前五分之一是第 1 折,第二个五分之一是第 2 折,以此类推。
1、scikit-learn
中的交叉验证
scikit-learn
是利用 model_selection
模块中的 cross_val_score
函数来实现交叉验证的。cross_val_score
函数的参数是我们想要评估的模型、训练数据与真实标签。我们在 iris
数据集上对 LogisticRegression
进行评估:
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegressioniris = load_iris()
logreg = LogisticRegression()scores = cross_val_score(logreg, iris.data, iris.target)
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96666667 1. 0.93333333 0.96666667 1. ]
默认情况下,cross_val_score
执行 3 折交叉验证,返回 3 个精度值。可以通过修改 cv
参数来改变折数:
scores = cross_val_score(logreg, iris.data, iris.target, cv=5)
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96666667 1. 0.93333333 0.96666667 1. ]
总结交叉验证精度的一种常用方法是计算平均值:
print("Average cross-validation score: {:.2f}".format(scores.mean()))
Average cross-validation score: 0.97
我们可以从交叉验证平均值中得出结论,我们预计模型的平均精度约为 96%。观察 5 折交叉验证得到的所有 5 个精度值,我们还可以发现,折与折之间的精度有较大的变化,范围为从 100% 精度到 90% 精度。这可能意味着模型强烈依赖于将某个折用于训练,但也可能只是因为数据集的数据量太小。
2、交叉验证的优点
使用交叉验证而不是将数据单次划分为训练集和测试集,这种做法具有下列优点:
首先,
train_test_split
对数据进行随机划分。想象一下,在随机划分数据时我们很“幸运”,所有难以分类的样例都在训练集中。在这种情况下,测试集将仅包含“容易分类的”样例,并且测试集精度会高得不切实际。相反,如果我们“不够幸运”,则可能随机地将所有难以分类的样例都放在测试集中,因此得到一个不切实际的低分数。但如果使用交叉验证,每个样例都会刚好在测试集中出现一次:每个样例位于一个折中,而每个折都在测试集中出现一次。因此,模型需要对数据集中所有样本的泛化能力都很好,才能让所有的交叉验证得分(及其平均值)都很高。对数据进行多次划分,还可以提供我们的模型对训练集选择的敏感性信息。对于
iris
数据集,我们观察到精度在 90% 到 100% 之间。这是一个不小的范围,它告诉我们将模型应用于新数据时在最坏情况和最好情况下的可能表现。与数据的单次划分相比,交叉验证的另一个优点是我们对数据的使用更加高效。在使用
train_test_split
时,我们通常将 75% 的数据用于训练,25% 的数据用于评估。在使用 5 折交叉验证时,在每次迭代中我们可以使用 4/5(80%)的数据来拟合模型。在使用 10 折交叉验证时,我们可以使用 9/10(90%)的数据来拟合模型。更多的数据通常可以得到更为精确的模型。
交叉验证的主要缺点:
是增加了计算成本。现在我们要训练 k 个模型而不是单个模型,所以交叉验证的速度要比数据的单次划分大约慢 k 倍。
重要的是要记住,交叉验证不是一种构建可应用于新数据的模型的方法。
交叉验证不会返回一个模型。在调用 cross_val_score
时,内部会构建多个模型,但交叉验证的目的只是评估给定算法在特定数据集上训练后的泛化性能好坏。
3、分层 k 折交叉验证和其他策略
将数据集划分为 k 折时,从数据的前 k 分之一开始划分(正如上一节所述),这可能并不总是一个好主意。例如,我们来看一下 iris
数据集:
from sklearn.datasets import load_iris
iris = load_iris()
print("Iris labels:\n{}".format(iris.target))
Iris labels: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 11 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 2 2 2 2 2 2 2 2 2 2 22 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 22 2]
如你所见,数据的前三分之一是类别 0,中间三分之一是类别 1,最后三分之一是类别 2。想象一下在这个数据集上进行 3 折交叉验证。第 1 折将只包含类别 0,所以在数据的第一次划分中,测试集将只包含类别 0,而训练集只包含类别 1 和 2。由于在 3 次划分中训练集和测试集中的类别都不相同,因此这个数据集上的 3 折交叉验证精度为 0。这没什么帮助,因为我们在 iris
上可以得到比 0% 好得多的精度。
由于简单的 k 折策略在这里失效了,所以 scikit-learn
在分类问题中不使用这种策略,而是使用分层 k 折交叉验证 (stratified k -fold cross-validation)。在分层交叉验证中,我们划分数据,使每个折中类别之间的比例与整个数据集中的比例相同,如图 5-2 所示。
mglearn.plots.plot_stratified_cross_validation()plt.tight_layout()
plt.savefig('Images/01CrossValidation-02.png', bbox_inches='tight')
plt.show()

举个例子,如果 90% 的样本属于类别 A 而 10% 的样本属于类别 B,那么分层交叉验证可以确保,在每个折中 90% 的样本属于类别 A 而 10% 的样本属于类别 B。
使用分层 k 折交叉验证而不是 k 折交叉验证来评估一个分类器,这通常是一个好主意,因为它可以对泛化性能做出更可靠的估计。在只有 10% 的样本属于类别 B 的情况下,如果使用标准 k 折交叉验证,很可能某个折中只包含类别 A 的样本。利用这个折作为测试集的话,无法给出分类器整体性能的信息。
对于回归问题,scikit-learn
默认使用标准 k 折交叉验证。也可以尝试让每个折表示回归目标的不同取值,但这并不是一种常用的策略,也会让大多数用户感到意外。
1)对交叉验证的更多控制
我们之前看到,可以利用 cv
参数来调节 cross_val_score
所使用的折数。但 scikit-learn
允许提供一个交叉验证分离器 (cross-validation splitter)作为 cv
参数,来对数据划分过程进行更精细的控制。对于大多数使用场景而言,回归问题默认的 k 折交叉验证与分类问题的分层 k 折交叉验证的表现都很好,但有些情况下你可能希望使用不同的策略。比如说,我们想要在一个分类数据集上使用标准 k 折交叉验证来重现别人的结果。为了实现这一点,我们首先必须从 model_selection
模块中导入 KFold
分离器类,并用我们想要使用的折数来将其实例化:
from sklearn.model_selection import KFold
kfold = KFold(n_splits=5)
然后我们可以将 kfold
分离器对象作为 cv
参数传入 cross_val_score
:
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores: [1. 1. 0.86666667 0.93333333 0.83333333]
通过这种方法,我们可以验证,在 iris
数据集上使用 3 折交叉验证(不分层)确实是一个非常糟糕的主意:
kfold = KFold(n_splits=3)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
请记住,在 iris
数据集中每个折对应一个类别,因此学不到任何内容。解决这个问题的另一种方法是将数据打乱来代替分层,以打乱样本按标签的排序。可以通过将 KFold
的 shuffle
参数设为 True
来实现这一点。如果我们将数据打乱,那么还需要固定 random_state
以获得可重复的打乱结果。否则,每次运行 cross_val_score
将会得到不同的结果,因为每次使用的是不同的划分(这可能并不是一个问题,但可能会出人意料)。在划分数据之前将其打乱可以得到更好的结果:
kfold = KFold(n_splits=3, shuffle=True, random_state=0)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores: [0.98 0.96 0.96]
2)留一法交叉验证
另一种常用的交叉验证方法是留一法 (leave-one-out)。你可以将留一法交叉验证看作是每折只包含单个样本的 k 折交叉验证。对于每次划分,你选择单个数据点作为测试集。这种方法可能非常耗时,特别是对于大型数据集来说,但在小型数据集上有时可以给出更好的估计结果:
from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
scores = cross_val_score(logreg, iris.data, iris.target, cv=loo)
print("Number of cv iterations: ", len(scores))
print("Mean accuracy: {:.2f}".format(scores.mean()))
Number of cv iterations: 150 Mean accuracy: 0.97
3)打乱划分交叉验证
另一种非常灵活的交叉验证策略是打乱划分交叉验证 (shuffle-split cross-validation)。在打乱划分交叉验证中,每次划分为训练集取样 train_size
个点,为测试集取样 test_size
个(不相交的)点。将这一划分方法重复 n_iter
次。图 5-3 显示的是对包含 10 个点的数据集运行 4 次迭代划分,每次的训练集包含 5 个点,测试集包含 2 个点(你可以将 train_size
和 test_size
设为整数来表示这两个集合的绝对大小,也可以设为浮点数来表示占整个数据集的比例):
mglearn.plots.plot_shuffle_split()plt.tight_layout()
plt.savefig('Images/01CrossValidation-03.png', bbox_inches='tight')
plt.show()

下面的代码将数据集划分为 50% 的训练集和 50% 的测试集,共运行 10 次迭代 :
from sklearn.model_selection import ShuffleSplit
shuffle_split = ShuffleSplit(test_size=.5, train_size=.5, n_splits=10)
scores = cross_val_score(logreg, iris.data, iris.target, cv=shuffle_split)
print("Cross-validation scores:\n{}".format(scores))
Cross-validation scores: [0.92 0.97333333 0.94666667 0.96 0.94666667 0.960.96 0.93333333 0.96 0.96 ]
打乱划分交叉验证可以在训练集和测试集大小之外独立控制迭代次数,这有时是很有帮助的。它还允许在每次迭代中仅使用部分数据,这可以通过设置 train_size
与 test_size
之和不等于 1 来实现。用这种方法对数据进行二次采样可能对大型数据上的试验很有用。
ShuffleSplit
还有一种分层的形式,其名称为 StratifiedShuffleSplit
,它可以为分类任务提供更可靠的结果。
4)分组交叉验证
另一种非常常见的交叉验证适用于数据中的分组高度相关时。比如你想构建一个从人脸图片中识别情感的系统,并且收集了 100 个人的照片的数据集,其中每个人都进行了多次拍摄,分别展示了不同的情感。我们的目标是构建一个分类器,能够正确识别未包含在数据集中的人的情感。你可以使用默认的分层交叉验证来度量分类器的性能。但是这样的话,同一个人的照片可能会同时出现在训练集和测试集中。对于分类器而言,检测训练集中出现过的人脸情感比全新的人脸要容易得多。因此,为了准确评估模型对新的人脸的泛化能力,我们必须确保训练集和测试集中包含不同人的图像。
为了实现这一点,我们可以使用 GroupKFold
,它以 groups
数组作为参数,可以用来说明照片中对应的是哪个人。这里的 groups
数组表示数据中的分组,在创建训练集和测试集的时候不应该将其分开,也不应该与类别标签弄混。
数据分组的这种例子常见于医疗应用,你可能拥有来自同一名病人的多个样本,但想要将其泛化到新的病人。同样,在语音识别领域,你的数据集中可能包含同一名发言人的多条记录,但你希望能够识别新的发言人的讲话。
下面这个示例用到了一个由 groups
数组指定分组的模拟数据集。这个数据集包含 12 个数据点,且对于每个数据点,groups
指定了该点所属的分组(想想病人的例子)。一共分成 4 个组,前 3 个样本属于第一组,接下来的 4 个样本属于第二组,以此类推:
from sklearn.model_selection import GroupKFold
from sklearn.datasets import make_blobs# 创建模拟数据集
X, y = make_blobs(n_samples=12, random_state=0)
# 假设前3个样本属于同一组,接下来的4个属于同一组,以此类推
groups = [0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3]
# 使用 GroupKFold 进行交叉验证
scores = cross_val_score(logreg, X, y, groups=groups, cv=GroupKFold(n_splits=3))
print("Cross-validation scores:\n{}".format(scores))
Cross-validation scores: [0.75 0.6 0.66666667]
样本不需要按分组进行排序,我们这么做只是为了便于说明。基于这些标签计算得到的划分如图 5-4 所示。如你所见,对于每次划分,每个分组都是整体出现在训练集或测试集中:
mglearn.plots.plot_group_kfold()plt.tight_layout()
plt.savefig('Images/01CrossValidation-04.png', bbox_inches='tight')
plt.show()

scikit-learn 中还有很多交叉验证的划分策略,适用于更多的使用场景 [ 你可以在 scikit-learn
的用户指南页面查看这些内容(3.1. Cross-validation: evaluating estimator performance — scikit-learn 1.5.0 documentation )]。但标准的 KFold
、StratifiedKFold
和 GroupKFold
是目前最常用的几种。
附录
一、参考文献
参考文献:[德] Andreas C. Müller [美] Sarah Guido 《Python Machine Learning Basics Tutorial》