机器学习:软件工程方法与实现
上QQ阅读APP看书,第一时间看更新

4.2.3 交叉验证

下面分三部分介绍交叉验证。

1.什么是交叉验证

首先看如下的一个例子:


from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
# 加载数据
bc = datasets.load_breast_cancer()
X = bc.data
y = bc.target
# 构建逻辑回归模型
clf = LogisticRegression()
# 划分数据:训练样本量占70%,测试样本量占30%
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=None)
# 模型训练
clf.fit(X_train, y_train)
# 模型预测得分
clf.score(X_test, y_test)

上述例子是机器学习中常见的数据集划分和训练方式,但有两个主要问题:

1)每次运行上述的代码得到的结果可能都不一样,到底哪次结果才更“靠谱”呢?选最好的一次?这样会有作弊的嫌疑。

2)该划分方式总有一定比例的测试数据无法参与到训练中(此处例子是30%),如果样本量本就不足,那么将损失宝贵的30%的数据,造成浪费。

为了控制随机性,可人为加入随机种子,使每次分隔保持一致(便于复现),例如可在上例train_test_split中设置参数random_state=42。42是一个很有意思的数字,感兴趣的读者可参考Wiki中的解释。但这样做实际加入了人为的偏差,不利于模型性能的公正评估。

为了解决上述两个问题,我们可以很自然地想到,可以通过多次更细粒度的样本划分,多次训练和评估,得到更准确的无偏估计以减少随机性。很多时候我们可以认为,交叉验证就是平均过程,包含了公平评估的思想。它除了可应用在模型的训练和调参中,也能应用在不同算法的评估选择中,这正是交叉验证的评估本质,即评估分类器性能的一种统计分析方法。

初学者常听到一句话,通过交叉验证调整模型参数。这种说法容易让人误以为交叉验证是调参的方法,而实际上交叉验证只是一种评估方法,可用于非调参的场合。

交叉验证可分为如下两个部分。

1)划分数据集:要求保证每次训练的数据集足够多,即一般情况下大于等于50%,即组数大于等于2;尽量保证划分后的数据集独立同分布(分层方法)。

2)训练和评估:与具体的学习算法和评估指标有关。

一般的交叉验证流程描述如下所示。


1)随机混洗、重排数据集。
2)将数据集拆分为K个组(Fold), 即K折交叉验证(K-Fold cross-validation)。
3)对于每个独特的组:
    a)将该组作为保留或测试数据集;
    b)将剩余的组作为训练数据集;
    c)在训练集上拟合模型并在测试集上评估;
    d)保留评估分数(丢弃模型)。

上述的K折交叉验证有如下两种常见的变体。

1)K×N fold cross-validation,即在K折的基础上再嵌套一层循环,最终取得平均估计,常见的有5×2交叉验证和10×10交叉验证。

2)Least-One-Out cross-validation(LOOCV),留一法,即每次只留一个样例做测试,其余数据作为训练。当数据量极小时,可考虑使用该方法,比如只有100个样本,那么按照LOOCV的方式,将训练100个模型,得到100个评价指标。每个模型使用99个样本训练,使用1个样本测试。

2.交叉验证的实现

上文提到的交叉验证流程方式很简单,完全可以自己实现,当然,这些交叉验证的方式在sklearn中都有接口且十分丰富。

·统一的实现:cross_val_score。

·学习器内自带的CV功能,例如逻辑回归中的LogisticRegressionCV。

·调参网格搜索时也有CV的方法,比如GridSearchCV。

最后,可参考以下示例以更好地理解交叉验证的本质。

1)随机划分数据示例:


from sklearn.model_selection import KFold
# 数据示例
X = np.array([1, 2, 3, 4, 5, 6])
# 数据划分准备: n_splits划分组数K,shuffle是否重排打乱数据,随机种子random_state
kfold = KFold(n_splits=3, shuffle = True, random_state= 42)
# 划分
for train, test in kfold.split(X):
    print('train: {}, test: {}'.format(X[train], X[test]))

输出3组数据集如下:


train: [3 4 5 6], test: [1 2]
train: [1 2 4 5], test: [3 6]
train: [1 2 3 6], test: [4 5]

2)使用分层划分,保持y同分布:


from sklearn.model_selection import StratifiedKFold
y = np.array([0, 0, 0, 1, 1, 1])
skf = StratifiedKFold(n_splits=3,shuffle = True, random_state= 42)
for train_index, test_index in skf.split(X, y):
    print('train: {}, test: {}'.format(X[train_index], X[test_index]))

输出3组数据集如下:


train: [2 3 5 6], test: [1 4]
train: [1 3 4 6], test: [2 5]
train: [1 2 4 5], test: [3 6]

前文提到的train_test_split同样实现了数据划分功能。

3)使用cross_val_score做交叉验证:


# 使用上述breast_cancer的数据
from sklearn.model_selection import cross_val_score
cross_val_score(clf, X, y, cv=3, scoring='accuracy')

输出3组模型评估如下:


array([0.93684211, 0.96842105, 0.94179894])

3.应用场景示例

下面简述了交叉验证两种常见的应用场景:交叉验证作为调参和模型选择的评估方法。

1)使用交叉验证选择参数。


# 使用上述的breast_cancer 数据
X = bc.data
y = bc.target
from sklearn.linear_model import LogisticRegressionCV
# 此处直接使用了学习器自带的CV
# 当然也可以使用统一的接口:cross_val_score
lr = LogisticRegressionCV(
    Cs=[1, 10, 100],cv=5,scoring='accuracy')
# 拟合
lr.fit(X, y)
# 输出将是 5 x 3 的二维矩阵
lr.scores_[1]
# 查看5组平均效果
lr.scores_[1].mean(axis=0)
# 输出3个参数5轮平均的accuracy,正则参数C=10的效果最好,所以选择它
# array([0.94559446, 0.95261254, 0.94913428])

2)使用交叉验证选择模型。

相比上述调过参数的逻辑回归来说,KNN算法表现差一些,所以选择逻辑回归。


# 接上,使用同样的数据
from sklearn.neighbors import KNeighborsClassifier
# 使用KNN默认参数
knn = KNeighborsClassifier()
# 5-fold
scores = cross_val_score(knn, X, y, cv=5, scoring='accuracy')
# 得分:array([0.88695652, 0.93913043, 0.9380531, 0.94690265, 0.92920354])
scores
# 均值:0.9280492497114275
scores.mean()

笔者把交叉验证的方式看作机器学习过程中的一种实验手段,实践中可实现适合自己的实验、测试工具,兼顾效率和公平性。