ITPub博客

首页 > 人工智能 > 人工智能 > 你的AI助手写了代码。谁检查了默认值?

你的AI助手写了代码。谁检查了默认值?

人工智能 作者:ITPUB社群 时间:1970-01-01 08:33:46 0 删除 编辑

五个 scikit-learn 默认值,在你的下一个模型进入生产环境之前值得仔细审视

让编码助手生成一个随机森林,然后检查它给出的那四行代码:导入正确,估计器能拟合,预测结果也符合预期形状。现在看看那些没有人指定的参数,因为这四行代码替你决定的问题比你提示中问到的还要多。

每棵树应该考虑多少个特征?模型应该施加多少正则化?验证折应该如何反映数据的结构?这就是默认值的作用:它们让一个描述不完整的请求变成一个可执行的程序。

语言模型通过下一词预测来学习代码模式,这使得熟悉的实现成为生成的天然起点。当一个实现省略了某个参数时,库会提供其默认值,让一个统计选择从惯例悄然进入你的流水线,而从未成为对话的一部分。

助手在被提示时可以审视这些选择,但收到能运行的代码并不能证明这些选择已被审视过。这种省略很容易被忽略,恰恰因为没有任何东西需要报错。

以下五个默认值是我在自己的生产工作和同事的项目中遇到过的,它们曾导致调试上的麻烦,因为我们忽略了代码实际在做什么。每一个都值得问同样的问题:省略这个参数替我决定了什么?

1. RandomForestRegressor( max_features=1.0 )

如果你学到的随机森林是“对行做自助采样,对特征做子采样,对树取平均”,那么默认的回归器会给你一个意外:RandomForestClassifier 使用 max_features="sqrt",而 RandomForestRegressor 使用 max_features=1.0,使得每次分裂时所有特征都可用!

这一差异关闭了特征子采样,而正是特征子采样将通常的随机森林配方与普通的 bagging 区分开来。自助采样仍然存在,所以树通过它们接收到的行保持随机性,但第二层随机性(有意限制每次分裂可以考虑哪些特征)缺失了。

为什么这很重要?想象一个数据集有一个特别强的预测变量,比如销售模型中的价格或折扣。即使每棵树接收到不同的自助采样样本,价格可能仍然在早期分裂中胜出,因为它的预测信号在训练行的这些变化中依然存在。因此,树可能发展出相似的结构,并对训练数据的波动做出相似的反应,限制了取平均能在多大程度上减少它们共享的变异性。

特征子采样通过偶尔将主导预测变量排除在考虑之外来打断这种重复,迫使树探索替代分裂。我在 TDS 文章《为什么随机森林需要如此随机》中拆解了其中的数学原理和一个受控实验,其核心观点是:增加更多的树无法消除与其相关性相关的方差分量。

使用 max_features=1.0 时,你让这个额外的去相关机制闲置了,尽管估计器的名称可能让你以为它已经在运作。例如,设置 max_features=0.33 会让每次分裂时大约三分之一的特征可用,从而恢复随机森林配方中的那一部分。

向你的助手提出的问题很简单:如果我们选择随机森林部分是为了特征层面的随机性,为什么我们却让这种随机性处于关闭状态?

2. LogisticRegression( C=1.0 )

要求使用逻辑回归时,你可能期望得到一个能找到最能解释观测结果的系数的模型。而 LogisticRegression() 实际拟合的是一个正则化版本,带有 L2 惩罚项以抑制较大的系数,并且默认 C=1.0 决定了该惩罚项与拟合数据之间的竞争强度。

正则化通常是有用的,尤其是当数据存在噪声或预测变量相互重叠时,但 1.0 这个数值对你的问题并没有任何特殊的知识。较小的 C 值会施加更强的正则化,而较大的值则给系数更多自由,省略该参数意味着接受某个特定设置而不加以审视。

除此之外,一个更微妙的问题是,“大系数”取决于你如何度量特征。假设收入以欧元记录,其系数为 0.0001。将同样的收入以千欧元表示,系数必须变为 0.1,才能保持对预测完全相同的惩罚贡献:

50,000 × 0.0001 = 50 × 0.1 = 5

信息没有改变,但系数现在大了一千倍,因此它对 L2 惩罚项(对系数取平方)的贡献大了一百万倍。当你用相同的 C 重新拟合模型时,优化器面临的权衡不同了,仅仅因为你改变了单位。

一个实用的起点是在 Pipeline 中对连续特征进行标准化,这样预处理会在每个训练折内拟合,然后使用适当的验证来调优 C。否则,你选择欧元还是千欧元,正在悄悄地帮助决定每个特征获得多少正则化。

3. cross_val_score(model, X, y, cv=5)

五折交叉验证听起来像是一种令人安心的彻底检查:在数据的四个部分上训练,在第五部分上评估,重复直到每个观测值都被留出一次。但 cv=5 指定了折数,却悄悄地把另一个决定留给了 scikit-learn:这些折是如何构建的。

对于回归,它使用 shuffle=False 的 KFold,这意味着验证折是现有顺序中连续的行块。如果你的数据框按日期、客户或地区排序,那么这种排序就会成为你评估设计的一部分,无论你是否有意为之。

考虑按时间顺序排列的 100 周销售数据。第一个折在训练第 21–100 周后,在第 1–20 周上进行评估,因此模型使用较晚的观测来预测较早的观测。代码运行完美,但该实验并不代表预测,因为在预测未来时只有过去是可用的。

简单地添加 shuffle=True 仍然会在训练和验证之间混合过去和未来的数据,这就是为什么正确的分割器取决于“未见数据”对你的问题意味着什么。对于行顺序任意的独立观测,打乱的 KFold 是一个合理的选择;对于预测,使用基于时间的方案,在较早的观测上训练并在较晚的观测上评估;对于关于新客户的预测,使用分组分割器,将每个客户完全保留在训练-验证边界的一侧。

对于普通的二分类或多分类,scikit-learn 使用 StratifiedKFold 代替,近似保持类别比例,但默认仍然不打乱,也不会自动尊重客户分组或时间。

通过传递 cv=5,你选择了评估模型的次数,同时将该评估的结构留为隐式。你的助手可以提供有效的交叉验证调用,但你仍然需要定义模型应该能够泛化到什么。

4. KMeans(n_init="auto")

“auto”这个词听起来令人放心,尤其是对于一个控制算法尝试寻找良好解多少次数的参数。你可能合理地不去动它,假设库会处理这个决定,但在默认 init="k-means++" 的情况下,n_init="auto" 意味着恰好一次完整运行。

要理解为什么这很重要,请记住 KMeans 的工作原理:它从一组初始聚类中心开始,将每个观测分配到最近的中心,并反复更新这些中心直到收敛。中心的起始位置会影响它们的最终位置,因为算法可能陷入局部最小值,进一步的迭代无法逃脱。

多次初始化通过从不同起始位置重复整个过程来解决这个问题。使用 n_init=10 时,KMeans 执行十次运行,并保留惯性最低的那一次,即观测与其分配的聚类中心之间距离平方和。

默认的 k-means++ 初始化比简单的随机选择更仔细地选择起始中心,这使得单次运行成为更合理的计算捷径,但它不能保证另一次初始化不会产生更好的解。“auto”设置遵循基于初始化方法的固定规则;它不会检查你的结果并不断重新启动直到它们看起来稳定。

增加 max_iter 不会恢复那些缺失的重新启动,因为它只允许每次运行中有更多更新。同样,设置 random_state=42 使初始化可复现,因此你可以一致地返回到相同的解,而不会了解另一个起始点是否会改进它。

如果你想要多次尝试,请使用诸如 n_init=10 之类的值显式请求它们,接受额外的计算。否则,你继承的自动选择是接受一次初始化的结果,而不将其与替代起始点进行比较。

5. SimpleImputer(keep_empty_features=False)

你将 SimpleImputer() 添加到管道中,期望它填充缺失值,但它的默认值也允许它删除整列。使用默认的均值策略时,任何在拟合期间仅包含缺失值的特征都没有均值可计算,因此插补器会将其从转换后的输出中删除。

想象一个包含 20 个特征(feature)的训练数据集,其中包含一个从未在任何训练观测中记录过的测量值。插补器(imputer)会移除该列,下游模型则在剩余的 19 个特征上进行训练,即使你可能仍然认为你的流水线(pipeline)是一个 20 特征的模型。

现在新数据到达,在之前为空的列中带有实际值。已拟合的插补器仍然会移除它,因为该决策在训练期间就已确立;新的测量值永远不会到达模型。

你可能预期会出现特征数量错误,但一致的流水线不会产生任何错误:模型在训练期间接收 19 个特征,在预测期间也接收相同的 19 个特征。一切正常运行,这使得如果你只在预处理之前检查数据,就很容易错过这一变化。

这也可能发生在交叉验证(cross-validation)内部,其中某个稀疏填充的列在一个训练折(fold)中可能完全缺失,尽管它在数据集的其他地方包含值。

设置 keep_empty_features=True 会保留这些列,在默认的均值策略下用零填充其缺失值。这保留了特征结构,但它无法教会模型训练数据中不存在的关系,因此保留该列并不会自动使其未来的值变得有用。

关键在于检查你的预处理实际向下游传递了什么,包括输出特征名称和形状。一个为填充缺失条目而引入的步骤也可能决定哪些特征得以保留,而一次成功的 .predict() 调用不会告诉你某个特征消失了。

要点

这些都不是 bug,而且默认值的存在有充分的理由,从计算效率到使常见工作流更易于运行。问题始于我们在没有注意到它们所做的选择的情况下接受它们,尤其是当代码干净地执行且输出看起来完全合理时。

这些都是我在生产工作中遇到的行为,无论是在我自己的流水线中还是在同事的项目中,它们造成的调试麻烦远超其在代码中的微小占比所暗示的程度。当你正在调查数据、模型以及其他一切可能解释意外结果的因素时,一个被省略的参数很容易被忽视。

编码助手使这些遗漏更容易被延续下去,因为一个熟悉的实现可能在我们讨论其背后的假设之前就已经完整成形。助手可以帮助审视这些假设,但一个“编写代码”的请求并不能保证会有一场关于行顺序、初始化或预处理后什么得以保留的对话。

这就是经验仍然重要的地方:认识到哪些看似常规的选择值得再看一眼,并在实现的同时要求解释默认值。有时正确的结果是保留默认值,并清楚地理解它为什么合适。

这项工作从来不只是敲那四行代码;而是知道那四行代码留下了哪些未回答的问题。


转载声明:本文为转载整理,原文版权归原作者及原站点所有。

原文来源:Towards Data Science

原创地址:https://towardsdatascience.com/your-ai-assistant-wrote-the-code-who-checked-the-defaults/

原文作者:Spyros Georgopoulos;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-11-21

  • 博文量
    1090
  • 访问量
    546675