上周在 dev.to 翻到一篇标题很朴素的帖子,作者 Siddhartha Bhattarai 给朋友 Anuka 做了个小工具——她家在尼泊尔 Kaski 县,靠博卡拉那边,雨季要上山看祖父,之前被季风堵回去过一次,错过了一个节日。
界面没什么花活。选一个县,点 "Check next 3 days",每天一行:
Risk: 68% (680 of 1,000 forecast scenarios)
我盯着这个 68% 看了挺久。不是它算得漂亮。是它太容易被读歪——第一反应"明天下雨概率 68%",再一拐就变成"明天山体滑坡概率 68%"。两个都不对,第二个要命。
作者自己清楚。文章里专门写了一行:这个百分比是"情景超过历史滑坡相关雨量的比例",不是滑坡概率。
画外音:这行解释我见过太多,塞在折叠起来的"关于"页里、扔在 README 最后一节、藏在没人点的 tooltip 里。写在页面底部的,不多。
但这工具最唬人的不是免责声明,是那个分母。1000 个情景不是 1000 次独立采样。
members = 51 + 31 + 40 # ECMWF + GFS + ICON,一共 122 个集合成员
# 122 个成员 → 插值 / 扰动 → 1000 个情景
"680 of 1,000"看着像精确到千分之一。有效信息量其实是那 122 个成员的离散度,剩下的全是一份不确定性被翻来覆去数了好几遍。他这么写不算错,但"一千个情景"比"55.7%"唬人多了——听着像跑了一千次,其实没有。
这不是重点,只是提一句:这种写法以后会被一堆人抄,抄的时候没人问分母哪来的。
我认真读完这篇,不是冲着免责声明去的。是他先把自己的模型从里到外拆了一遍。
他手上有三条降雨规则:三日累计降雨、前期降水指数,还有一条为 Kanti Roadway 发表的 I-D 曲线。注意,Kanti 那份研究在 Bagmati 省,Kaski 不归它管——他照用了,但把这件事写在明面上。这个态度先记一分。
77 mm 那条规则在测试年份上分数很好看。然后他做了件很多人不会做的事:把阈值挪到训练年份去调参,再回来验。
阈值 183 mm → F1 0.17
阈值 186 mm → F1 0.13
3 毫米,两个点。这就是"看起来对"和"是对的"中间那条缝,我掉进去过不止一次。
为什么这么敏感?测试集是 2022—2024 年季风季,366 天,33 个滑坡日。F1 的分母就这么大,翻一个样本,小数点后面就肉眼可见地动。这个量级下,什么方法排名都是噪声——他自己也写了,各方法的置信区间全在 0.00 到 0.49 之间互相压着。
我见太多人拿这个规模的验证集画柱状图,然后指着最高的那根说"我们的方案更优"。别问,问就是缴智商税。
所以他说"没有任何方法明显胜出",不是谦虚,是陈述事实。
TabPFN 那段我读了两遍。
上了更丰富的特征——前期降雨、土壤湿度、滞后降雨——名义 F1 0.33,全场最高。然后分年一拆:2022 年 0.00,2023 年 0.11,2024 年 0.58。优势全来自 2024 年。
再往下还有一句更狠的:训练年份内部交叉验证,AUC 0.45,低于随机水平。
这个数比 0.33 那个 F1 重要一百倍,但它躺在正文里,不在摘要里,也不在任何排行榜上。模型在训练集内部的区分度还不如抛硬币,这意味着什么同行都懂,我不展开。
顺带一句,他上 TabPFN 是因为比赛要评"Best Use of TabPFN"——这不是黑点,是规则,换我我也上。真正的分水岭在后面:他试完没把它放进应用。
理由是上线要多一路来自不同数据源的土壤湿度。多一个数据源就多一个失败模式,山区的网络本来就脆。这个理由比 AUC 0.45 更实在,也更像真做过东西的人会说的话——不是"它不够准",是"它多带了一个会坏的东西进来"。
真正的题眼在这句。
他最后选了那条重标定过的 I-D 曲线。已发表的版本在 Kaski 大约 28% 的季风日会触发,一场大暴雨之后两周还挂着"触发"状态。重标定之后降到 6% 左右。
他给的理由:误报更少——不是因为评分更高。
我把这句话单独拎出来,是因为它跟现在流行的做法反着来。
给专业预报员看的系统,误报和漏报可以按代价函数权衡——两者在一条曲线上。给 Anuka 看的系统不这么运作。她不会因为少收一次警报出事,但会因为一周收五次警报、跑过去发现路好好的,从此不看这工具了。
误报的代价不是线性的,是断崖式的。越过那个点,工具在用户心里就死了,你都不知道它哪天死的。
所以在这个场景里,"误报率"不是评估指标,是生死线。他选了误报少的那个,哪怕评分平庸。这个判断我完全认,虽然我怀疑他当时也没想这么细,更多是手感——但手感对了,理由可以后补。
整篇文章里我最想抄的是这一段。
输出不只是一个百分比。应用会给一条可转发的消息,英文尼泊尔文各一份;给一条兜底通知链;还有一个 "Network available?" 的开关。
短信 → Viber 群 → 社区志愿者 → 警报器 / 旗子
关掉这个开关,界面只剩这条链。预报拿不到,模型跑不了,Streamlit 那个本地原型基本废了,屏幕上剩下的是一串人名和一面旗子。
他做了个 AI 降雨风险工具,然后承认最靠得住的部分是旗子。这句话我读了三遍。
这不是自嘲,是设计。山区的网络不是"偶尔不稳",是"大部分时候没有"。任何把联网当默认前提的方案,正好在最需要它的那几天是废的。这道理说出来谁都懂,但真去做的时候,绝大多数人会把它写进"未来工作"一节,然后接着调模型。
Gemma 那段本来想跳过,但有个细节我忍不住提一句。
英文消息是 Gemma 3 4B 走 Ollama 在同一台笔记本上跑的,数据不出机器——务实,省了 API 费,也没把山里人的位置信息发出去。尼泊尔文用固定模板,因为他测下来发现 Gemma 会生造词、把月份名写错。
早期英文也翻过车,其中一种错法是把风险百分比写反。68% 写成 86%,两个数字一换位,意思全反,人眼扫过去不一定抓得住。
现在的规则是硬条件:
if (
extract_pct(text) == screen_pct # 百分比必须和屏幕数字完全一致
and text.rstrip().endswith(DISCLAIMER) # 必须以指定免责声明结尾
and len(text.split()) < 60 # 少于 60 词
):
use(text)
else:
use(TEMPLATE) # 尼泊尔文那条线从一开始就走这里
这个校验写得比很多正经"AI 产品"实在——可执行、可复现、不靠"我觉得读起来还行"。他说最终测试大概十几次调用通过,样本很小,措辞可能还生硬。这句我也信,因为大部分人不会主动说自己的样本小 😅。
局限那节我挑两个说,其余不铺开。
一个是结构性的:模型只看降雨。8 月那次事件是冰岩崩,跟降雨没关系,这类模型怎么调都预测不了。不是数据不够,是问题不在它的定义域里。
另一个是输入:降雨用的是县域质心附近约 1,615 米高程格点的再分析数据。既不是雨量站观测,也不对应 Anuka 走的那条具体的路。他抽查过,次日预报和再分析值平均差 5—8 毫米每天——所以集合的离散度很可能低估了真实误差。
我一直觉得这段是全文最诚实的地方。这类工具最不想碰的就是"输入本身就不对",因为一碰,上面所有 F1 都变得可疑。他写了,还写了具体数字。
"只在 10 月非季风的实时预报和合成高风险案例上测过""其他县直接复用 Kaski 模型、未验证""没算置信区间"——全在明面上。一个工具敢把自己的测试范围说得这么窄,反而更让我信它。
评论区有人问了个挺专业的问题:关掉 "Network available?" 之后,应用会不会缓存上一次的预报。问到点上了,我猜答案是没做,或者做了没写进文章。真要做,缓存过期反而更危险——用户拿着一份三天前的预报,以为是实时的。
这大概是下一步最该补的东西,比再调一版 TabPFN 值钱。
我现在做这种给人用的小工具,顺序换了:先写免责声明和兜底名单,再写模型。前者决定这东西会不会害人,后者决定它准不准。顺序反了,就会做出一个很准但没人敢用的东西。
他把免责声明钉在页面底部而不是塞进设置里,把兜底链做成开关而不是一句注释,把回退模板写成硬编码而不是"以后再说"——这三件事,一件都不需要模型。少了任何一件,这工具就是个会说话的百分比。
本期没缴税,纯围观。但学到的比我上一次自己翻车还多。