TL;DR

  • 场景:同一模型多次训练评估波动大,单次划分不可信,K值难定
  • 结论:用K折交叉验证看”平均分+方差”,优先选高均值且方差小的区间
  • 产出:一套可复用的cross_val_score流程+结合学习曲线的稳定性选参方法

交叉验证

背景问题

确定了K值之后,我们还能够观察到一个重要现象:每次运行模型时,学习曲线都会发生变化,模型的效果时好时坏,呈现出不稳定性。这种波动现象主要是由以下原因造成的:

  1. 数据集划分的随机性

    • 每次运行时,训练集和测试集的划分方式不同(通常是通过随机采样)
    • 举例来说,假设我们有1000条数据,第一次可能随机选取700条作为训练集,剩下的300条作为测试集;第二次运行时,这个划分组合又会发生变化
  2. 数据分布的影响

    • 不同的训练集会学到略微不同的模式
    • 测试集的不同组成会导致评估指标波动
    • 例如,如果某次测试集中恰好包含较多边界案例,模型表现就会较差

实际业务场景中的问题

  1. 历史数据与新数据的差异

    • 训练数据通常是静态的历史数据
    • 测试数据则模拟未来新进入系统的实时数据
    • 在电商推荐系统中,用过去3个月的订单数据训练,但需要预测未来一周的购买行为
  2. 模型评估的核心目标

    • 我们追求的是模型在未知数据上的表现
    • 这种能力被称为泛化能力(Generalization Ability)
    • 好的泛化能力意味着:
      • 对噪声数据保持鲁棒性
      • 能处理未见过的数据模式
      • 避免过拟合训练数据的特定特征
  3. 提高泛化能力的方法

    • 使用交叉验证代替单次划分
    • 增加数据多样性
    • 采用正则化技术
    • 例如,在金融风控模型中,会使用5折交叉验证来更可靠地评估模型效果

泛化能力

我们在进行学习的时候,通常会将一个样本集分化为【训练集】和【测试集】,其中训练集用于模型的学习和训练,而后测试集通常用于评估训练好的模型对于数据的预测性的评估。

  • 训练误差:代表模型在训练集上的错分样本比率
  • 测试误差:代表模型在测试集上的错分样本比率

训练误差的大小,用来判断给定问题是不是一个容易学习的问题,测试误差反应了模型对未知数据的预测能力,测试误差小的学习方法具有很好的预测能力,如果得到的训练集和测试集没有交集,通常将此预测能力称为泛化能力(generalization ability)

我们认为,如果模型在一套训练集和数据集上表现优秀,那说明不了问题,只能在众多不同的训练集和测试集上都表现优秀,模型才是一个稳定的模型,模型才是具有真正意义上的泛化能力。

为此,机器学习领域有发挥神作用的技能:【交叉验证】,来帮助我们认识模型。


K折交叉验证

最常用的交叉验证方法是K折交叉验证(K-Fold Cross Validation)。这种方法通过将数据集划分为K个大小相似的互斥子集,每次使用其中K-1个子集作为训练数据,剩下的1个子集作为验证数据,重复这个过程K次,最终得到K个模型评估结果的平均值。

具体步骤

  1. 将原始数据集随机划分为K个等大小的子集(通常K=5或10)
  2. 依次选取第i个子集作为验证集(i=1,2,…,K),其余K-1个子集合并作为训练集
  3. 在训练集上训练模型,在验证集上评估模型性能
  4. 重复步骤2-3直到所有子集都充当过验证集
  5. 计算K次评估结果的平均值作为最终模型性能指标

优势

这种方法相比简单的训练集-测试集划分具有以下优势:

  • 充分利用有限的数据资源
  • 减少数据划分带来的随机性影响
  • 提供更可靠的模型性能评估
  • 特别适用于中小规模数据集

K值选择

实际应用中,K值的选择需要权衡:

  • 较小的K值(如5)计算效率更高
  • 较大的K值(如10)评估结果更稳定
  • 极端情况K=N(样本数)时即为留一法交叉验证

通过多次交叉验证求取均值,可以显著降低单次数据划分带来的评估偏差,为模型选择和参数调优提供更可靠的依据。


带交叉验证的学习曲线

对于带交叉验证的学习曲线,我们需要观察的就不仅仅是最高的准确率了,而是准确率高且方差还相对较小的点,这样的点泛化能力才是最强的。

在交叉验证+学习曲线的作用下,我们选出的超参数能够保证更好的泛化能力。

代码示例

from sklearn.model_selection import cross_val_score as CVS

Xtrain, Xtest, Ytrain, Ytest = train_test_split(X, y, test_size=0.2, random_state=420)
clf = KNeighborsClassifier(n_neighbors=8)
# 训练集对折6次,一共6个预测率输出
cvresult = CVS(clf, Xtrain, Ytrain, cv=6)
# 每次交叉验证运行时估算器得分的数组
cvresult

查看均值、方差

# 均值:查看模型的平均效果
cvresult.mean()
# 方差:查看模型是否稳定
cvresult.var()

绘制学习曲线

score = []
var = []
# 设置不同的k值,从1到19都看看
krange = range(1, 20)
for i in krange:
    clf = KNeighborsClassifier(n_neighbors=i)
    cvresult = CVS(clf, Xtrain, Ytrain, cv=5)
    # 每次交叉验证返回的得分数组,再求数组均值
    score.append(cvresult.mean())
    var.append(cvresult.var())

plt.plot(krange, score, color='k')
plt.plot(krange, np.array(score) + np.array(var) * 2, c='red', linestyle='--')
plt.plot(krange, np.array(score) - np.array(var) * 2, c='red', linestyle='--')

是否需要验证集

最标准,最严谨的交叉验证应该有三组数据:训练集、验证集和测试集。当我们获取一组数据之后:

  • 先将数据集分成整体的训练集和测试集
  • 然后我们把训练集放入交叉验证中
  • 从训练集中分割更小的训练集(k-1份)和验证集(1份)
  • 返回的交叉验证结果其实是验证集上的结果
  • 使用验证集寻找最佳参数,确认一个我们认为泛化能力最佳的模型
  • 将这个模型使用在测试集上,观察模型的表现

通常来说,我们认为经过验证集找出最终参数后的模型的泛化能力是增强了的,因此模型在未知数据(测试集)上的效果会更好,但尴尬的是,模型经过交叉验证在验证集上的调参之后,在测试集上的结果没有变好的情况时有发生。

原因

  • 我们自己分的训练集和测试集,会影响模型的效果
  • 交叉验证后的模型的泛化能力增强了,表现它在未知数据集上方差更小,平均水平更高,但却无法保证它在现在分出来的测试集上预测能力最强

如果我们相信交叉验证的调整结果是增强了模型的泛测试集上的测试化能力,那即便结果并没有变好(甚至变坏),我们也认为模型是成功的。

如果我们不相信交叉验证的调整结果能够增强模型的泛化能力,而一定要依赖测试集来进行判断,我们完全没有进行交叉验证的必要,直接用测试集上的结果用来跑学习曲线就好了。

所以,究竟是否需要验证集,其实是存在争议的,在严谨的情况下,大家还是使用了有验证集的方式。


其他交叉验证方法

交叉验证的方法不止”K折”一种,分割训练集和测试集的方法也不止一种,分门别类的交叉验证占据了sklearn中非常长的一章。

所有的交叉验证都是在分割训练集和测试集,只不过侧重的方向不同:

  • K折:按顺序取训练集和测试集
  • ShuffleSplit:侧重于让测试集分布在数据的全方位之内
  • StratifiedKFold:认为训练数据和测试数据必须在每个标签分类中占有相同的比例

各类交叉验证的原理繁琐,大家在机器学习的道路上一定会逐渐遇到更难的交叉验证,但是万变不离其宗:本质上交叉验证是为了解决训练集和测试集的划分对模型带来的影响,同时检测模型的泛化能力。


避免折数太大

交叉验证的折数不可太大,因为折数越大抽出来的数据集越小,训练数据所带来的信息量就会越小,模型会越来越不稳定。

如果你发现

  • 不使用交叉验证的时候模型表现很好
  • 一使用交叉验证模型的效果就骤降

一定要查看

  • 你的标签是否有问题
  • 然后就是查看你的数据量是否太小,折数是否太高

折数过大的问题

如果将cv从5改成100:

score = []
var = []
krange = range(1, 20)
for i in krange:
    clf = KNeighborsClassifier(n_neighbors=i)
    cvresult = CVS(clf, Xtrain, Ytrain, cv=100)
    score.append(cvresult.mean())
    var.append(cvresult.var())

plt.plot(krange, score, color='k')
plt.plot(krange, np.array(score) + np.array(var) * 2, c='red', linestyle='--')
plt.plot(krange, np.array(score) - np.array(var) * 2, c='red', linestyle='--')

折数过大会导致

  • 运算效率变慢
  • 预测率方差变大,难以保证在新的数据集达到预期预测率

常见问题速查

症状根因修复
每次跑分差异很大,曲线忽高忽低单次随机划分导致评估方差大;数据量小/类别不均衡更明显用K折交叉验证输出mean/var;分类任务优先StratifiedKFold
交叉验证后在测试集反而更差在验证折上”过度调参”;测试集划分本身偏严格三段式:train+(CV调参)+test;或用nested CV
cv设置很大(如100)后分数大幅下降且波动增大每折训练集过小,模型估计不稳;运算量暴涨控制cv(常用5或10)
报错:The least populated class in y has only 1 member分层交叉验证下,某些类别样本太少,无法分折合并稀有类/补样本/降低cv
CV分数异常高,线上效果很差数据泄漏:在CV之前做了全量标准化/特征选择/编码用Pipeline把预处理放进CV
同一份代码在不同机器结果不一致并行/随机性/浮点差异;未固定种子固定random_state;记录依赖版本与硬件信息
指标看起来”稳定”但业务不达标选错scoring(accuracy不适合不均衡);只看均值不看业务约束对比多指标:precision/recall/F1/AUC