AI 工程地基 34|两份演示,两个冠军
六个旋钮,每个拧五个档位,一共多少种组合?5 的 6 次方,15625 种。每种组合训练一次要 10 秒,全部试完是 43 个小时的机器时间。这是超参数调优这堂课给自己设的开场:调参是机器学习里最贵的杂活之一,它想教你怎么把 43 个小时省下来。
AI 工程地基 34|两份演示,两个冠军
六个旋钮,每个拧五个档位,一共多少种组合?5 的 6 次方,15625 种。每种组合训练一次要 10 秒,全部试完是 43 个小时的机器时间。这是超参数调优这堂课给自己设的开场:调参是机器学习里最贵的杂活之一,它想教你怎么把 43 个小时省下来。
它给了三招。
第一招,网格搜索。把每个旋钮的档位排成一张表,所有组合挨个试。最直觉,最好懂,也最先爆炸——参数个数一多,组合数指数往上翻。
第二招,随机搜索。不排表了,在合理范围里随机撒点。2012 年 Bergstra 和 Bengio 有一篇专门论证这件事的论文,核心观察很朴素:一堆超参数里,通常只有一两个真正重要。网格把预算浪费在不重要的维度上——9 次评估,你只见过 3 个不同的学习率;随机搜索同样 9 次,9 个学习率全都见过。
第三招,贝叶斯优化。给昂贵的目标函数配一个便宜的替身。替身通常是个高斯过程,你可以把它理解成一条会同时报告两件事的曲线:这里的预测分数是多少,我对这一带有多不确定。每次真模型评估完,替身更新一次对全局的印象,再用一个叫「期望改进」的规则挑下一个点——要么是替身觉得分数高的地方,要么是替身没去过、心里没底的地方。前期哪里都不确定,它就到处看;后期好区域摸清了,它就往那儿扎。
课文的动手环节给了第一份演示:一个合成的目标函数,学习率和树深度两个变量,满分 10,最优点在学习率 0.01、深度 4。三种方法同样的 28 次预算,代码正下方课文总结说:同样的预算下,贝叶斯优化通常最快找到最好分数。
跑起来不是这回事。
表格印出来:网格 10.0000,贝叶斯 10.0000,随机 9.7170。前两个表面打平,展开小数点后六位,网格的 10 是精确值,贝叶斯是 9.999991。再把过程展开看——网格第 10 次评估就摸到了满分,贝叶斯的最好成绩第 20 次才追到同样的显示精度,随机 28 次预算全部花完,还差着 0.28。「贝叶斯最快」这句话,在它自家这份演示里不成立。快的是网格。
为什么?看网格那张表。学习率一列写的是 0.001、0.01、0.1、1.0——0.01 在里面。深度一列是 2 到 8 的整数——4 在里面。这道题的标准答案,恰好长在网格的格点上,网格只要排队走过去就能撞上。随机搜索想赢,得从连续分布里恰好抽中 0.01 这个数,概率严格等于零。
课后的练习 1 让你自己动手验证:用同样的总预算跑网格和随机,换不同种子来 10 遍,看随机搜索赢几次。我用这道题自己的目标函数跑了(28 次预算,正好是网格的全部组合数):0 胜 10 负。不是随机搜索不行,是考场设在了网格的主场。
我顺手做了个课本没做的实验:把目标函数里的最优学习率从 0.01 挪到 0.00316,也就是 0.001 和 0.01 这两个格点在对数尺度上的正中间,改动只有一个常数。同样 28 次预算、10 个种子重跑。排位一夜翻转:网格 10 个种子全部卡死在 9.75,两个最近的格点各差半档,谁也够不着正中间;贝叶斯 10 个种子全胜,追平网格的终身最好成绩大多只花 7 次评估上下,最快的一次第 2 次就到了;随机有 9 个种子冲到 9.9 附近,剩一个 8.94,十个种子没有一个拿第一。考卷上改一个数字,冠军全换了人。
我还试了另一种挪法:把最优深度从 4 挪到 4.5。这回谁也别想赢。深度这个参数只取整数,4.5 对三种方法都不存在,大家齐齐停在 9.75。这正好补上了 Bergstra 那篇论文论证的另一半:随机和贝叶斯的优势长在连续的、或者取值很多的参数上;离散参数的最优落在两个整数之间时,神仙也只能取整。
这是第一份演示的故事。同一堂课还有第二份演示,换成真实数据训练梯度提升模型。这次排序跟课文写的一模一样:网格搜 36 个组合,最好的验证误差 0.5412;随机搜索 25 次就摸到 0.5172,之后预算加到 50 次、100 次,最好成绩一次也没再变好,多花的 75 次试验全部白买;贝叶斯 50 次,0.4442。贝叶斯第一,随机第二,网格垫底。
两份演示,两个冠军。谁都没说谎——合成题的最优解长在格点上,真实数据的最优解不在。方法没有绝对的强弱,最优解住在哪里,决定了谁先找到它。课文说 60 次随机试验有 95% 的概率命中最优点的 5% 邻域,这个数我核过,是真的(每次 5% 命中、独立 60 次,算出来 95.4%,最少要 59 次)。但命中邻域和刷新纪录是两回事:纪录在前 25 次里就停了,后面的试验买到的是把握,不是分数。
顺带说,课文的实用建议大多站得住:学习率在对数尺度上搜,因为 0.001 到 0.01 的差别跟 0.1 到 1.0 的差别一样大,线性撒点会把预算浪费在大数那一头;先调学习率,其余留默认;预算的六成给最关键的两个参数。拿不准的时候,随机搜索,次数至少是参数个数的两倍。这些都值得照做。
下面是几根刺,全扎在同一篇课文内部。
第二份演示里,网格搜出的最优参数是树数 100、学习率 0.3、深度 2。树数的候选列表是 20、50、100,最优值在最顶上;学习率候选是 0.01、0.05、0.1、0.3,最优值在最顶上;深度候选是 2、3、5,最优值在最底下。三个参数全贴着搜索空间的边界。贝叶斯那段的冠军参数也没好到哪去:深度 2 是下界,叶子最小样本数 20 是上界,五个参数里两个贴边。而这堂课「常见错误」一节自己写着:最好的值贴边,说明范围不够宽,最优解可能在门外,跑完永远检查参数是不是贴边。建议印在同一篇课文里,自家演示的输出却没人回头查。这个检查便宜得很,值得每次都做——包括在这门课自己的演示上。
Use It 一节教 Optuna 的剪枝。剪枝是给搜索配的止损机制:中途成绩明显没戏的试验提前砍掉,省下它后面的算力。模板代码这样写:先让 cross_val_score 把三折交叉验证全部跑完,拿到平均分,再 trial.report(分数, step=0) 报告一次,问剪枝器该不该停。剪枝器 MedianPruner 的规则里有一条 n_warmup_steps=5,意思是前 5 步不剪。问题来了:一个只在第 0 步报告过一次的试验,永远处在热身期里,结构上不可能被剪。我把这段代码原样跑了 60 个试验,0 个被剪。课文说这个模式通常省下总算力的 40% 到 60%。对照组我改了两处:让试验逐轮报告成绩,60 个里剪掉 1 个,这份数据的分数太不分化,中位数不好当门槛;把热身期清零,60 个剪掉 30 个。剪枝本身真能省算力,但前提是报告发生在算力花掉之前——一边训练一边报中间成绩,而不是三折全跑完了再报一个总分。课文自己写得很清楚:「剪枝要求调用 trial.report() 报告中间指标」,然后示例代码交上去的,是三折全部跑完之后的一个总分。汇报写在花钱之后,剪枝器永远插不上话。
还有个更隐蔽的坑,留给抄代码的人。同一堂课两段示例都在调 max_depth,都写着「2 到 10」。sklearn 那段用 scipy 的 randint(2, 10),抽样范围是 2 到 9,不含 10;Optuna 那段用 suggest_int(2, 10),范围是 2 到 10,含头含尾。同名的参数、同样两个数字,抄哪一段,搜索空间悄悄差一格。两个库的边界习惯不同,一个含头不含尾,一个两头都含,课上一个字没提。
课文还有一张超参数重要性的表,把 max_features——随机森林每次分裂看多少列——归进「低重要性」。同一页的延伸阅读引了 Probst 2019,那篇专门量化「哪些超参数值得调」的论文。原文写的是:mtry(就是 max_features)是最值得调的参数,这早就是常识。引用来的论文和引用它的表格,意见正好相反。重要性排序这种东西本来就跟模型和数据走,抄通用表格,不如看自己任务上的实测。
实用提示一节有句话:「用早停代替调 n_estimators——把 n_estimators 或轮数设高,让早停决定什么时候停。」说得对。但这堂课的四段演示代码——网格、随机、贝叶斯、Optuna——每一段都把 n_estimators 当普通超参数认认真真搜了一遍。口号和实践住在同一篇课文里,互相没打招呼。
最后说句公道话,这堂课的算术是认真的。15625 种组合乘 10 秒是 43.4 小时,笔算可验。Hyperband 那个锦标赛方案——81 个配置各跑 1 轮,留前三分之一跑 3 轮,再留三分之一跑 9 轮,一路到 1 个配置跑满 81 轮——总开销 405 个轮次,对照全量跑满的 6561 个,省 16 倍,落在课文宣称的 10 到 50 倍区间里。嵌套交叉验证——外层估成绩、内层调参数的双层循环——5 个外层折乘 5 个内层折乘 27 个网格点,675 次模型拟合,一笔不差。代理模型的开销可以忽略这句也实测了:28 轮建议加观测,每轮还要给 500 个候选点打分,总共 0.27 秒。便宜的替身确实便宜。
课后的练习 1 值得做,但更有信息量的版本是我前面改的那道:把目标函数里的一个常数挪半格,一行代码,看冠军换人。或者走练习 4 的路子,拿 Optuna 在真实数据上画一张超参数重要性图,看看你的任务里真正值钱的旋钮是哪个,再跟 Probst 的排序对一对。对不上的地方,就是你自己的数据在说话。