跳到主要内容

圈内内参|0.938 这个数,在评估边界公开之前只能当自评

盖文
盖文

· 阅读约 9 分钟

这期凭一份公开材料记录一个 AI 医疗辅助工具项目的评估口径缺失:Emma Sofia 10 月 4 日发在 dev.to 的 NightGuard AI,给室友 Liam Vance 做夜间低血糖预测,90 天评估里 TabPFN 的 ROC-AUC 是 0.938。结论先放一句:0.938 在评估边界没有公开之前,只能当作者自评,不该在任何地方被当成可核验的数字引用。信源层级说明一下,这篇只有一个公开信源,没有独立第二方交叉核实,所以事实层和未证实层的标注会收得比以往更紧。

先把我信的那一半说清楚。这半边不依赖评估口径,是仓库结构和文章明写出来的:项目在本地跑 TabPFN,输入是 Liam 大约 90 天的 CGM 记录,做上下文贝叶斯推断;前端 Next.js 16 App Router 加 TypeScript 纯 CSS,后端 FastAPI,代码在 GitHub 的 EmmaSofiaDev/nightguard-ai 仓库,带 Dockerfile、render.yaml、requirements.txt、run.py 和 MIT 许可证。这些是工程侧的底子,能落地,说不上激进,在单人都能完成的范围内。完全本地跑这件事,在这个项目里有实质理由。作者写到 Liam 拒绝把 CGM 记录、胰岛素剂量、饮食、运动心率和饮酒数据上传到云端,理由是这些属于高度敏感的生物特征数据。数据出域对血糖预测工具来说不是风格偏好,是前置约束。这个约束反而比文章里很多技术选择更值得记住:它把整个项目钉死在本地推理上,也让评估、部署、可复现这三件事同时变难了。部署上,Render 一键部署能解决;可复现上,repo 里还有个 synthetic_data.py 生成 90 天 CGM 数据,感兴趣的可以自己跑。但评估那一步,没有边界。

数字本身文章给得不含糊:TabPFN ROC-AUC 0.938、Log-Loss 0.241;Random Forest 0.812 和 0.485;XGBoost 0.835 和 0.432;Logistic Regression 0.741 和 0.579。单个病人的 90 天数据,模型跑赢三条基线,看起来不差。但这个数字是怎么来的?90 天窗口里怎么划分训练和评估?时序边界在哪?文章一句话都没给。

对于单一个人连续 90 天的监测数据来说,这不是方法论上的吹毛求疵,而是评估能不能成立的根本前提。为什么这么说,这里得把机制拆开。

一个人身上的 CGM 数据、胰岛素剂量、饮食、运动、饮酒这些变量,在时间上有很强的自相关性。同一个人在周六晚上做的 8 英里越野跑、喝的深色精酿、吃的全麦意面,会在接下来好几个夜晚以类似模式重复出现。如果不做时间截断,把 90 天窗口随机切开,训练集和评估集就会共享大量同模式夜晚,模型看起来是在预测,实际上是在回溯。随机抽样对独立同分布数据尚可接受,对连续时序数据是直接把信息泄漏进了评估。

TabPFN 在这个案例里做的是上下文推断,上下文就是那 90 天。如果 90 天评估在这个上下文内部完成,模型已经见过那些被评估的夜晚。上下文内评估不是特例,是这个模型架构下一种自然但无效的操作。上下文内留出到底有几种可能做法,每种会给 0.938 带来完全不同的解释,下面这几种是我从这篇文章和 repo 里无法排除的:

第一种,随机按夜抽样。把 90 天里所有夜晚打乱,随机拉出一部分做评估。这种方式里,训练和评估共享太多邻近夜晚,信息泄漏几乎不可避免,单病人时间序列下直接失效。第二种,按日期前切后。前 80% 训练,后 20% 评估。这种方式稍微干净,但同一患者的生活规律在后段并不会突然变成另一个域,评估用的后 20% 和训练段之间的相似度仍然很高。而且文章没给日期,不知道切点在哪,也没法验证。第三种,按完整夜晚留出,每次留一整个夜晚,其余训练。这在时间序列里算比较硬的一种做法,但它衡量的更多是普通夜的重现,不是危险夜的泛化。而真正该警惕的,恰恰是文章周六案例里描述的那种晚上七点完成 8 英里越野跑、晚上十点多趋势箭头还在缓慢下降的特殊夜。如果评估集中普通夜占比高,0.938 描述的是模型在容易夜里的表现,不是在最该预测的那类夜里的表现。第四种,全量重代入,90 天窗口既做训练又做评估。这种情况下 0.938 基本不反映预测能力,只反映对已见数据的拟合程度。

四种做法里,有三种会让 0.938 从预测未见过的夜晚变成别的意思。文章没写是哪种,也没给任何边界相关的说明,所以外界无法判断这个 0.938 里到底含了多少来自训练和评估重叠的水分。这不是说作者一定做错了;它完全可能只是评估报告写得不完整。但对外界来说,结果一样——这个数字不能被引用。

(确认)文章页面有 22 条评论。评论者在这一点上已经指出了大部分缺口:KanuniLabs 质疑仅 90 天数据下 0.938 是否来自真正留出的测试集;Onizuka 要求看假阴性率和真实低血糖夜的召回率;多位评论者讨论了 CGM 缺失段、校准曲线、时间泄漏、边缘设备量化和假阴性风险。可以说评论区的核心关切,和我这里说的评估边界,是同一件事的不同侧面。

这里有一个缺口比评估边界更具体、更贴近这个工具的真实用途:假阴性率,或者说在真实低血糖夜里到底漏掉多少。ROC-AUC 不是不够好,是它压不平一个结构性的代价差异。对一个夜间低血糖预警工具来说,假阳性的代价是用户多醒一次、吃点东西;假阴性的代价是模型说今晚安全,用户睡过去,等 CGM 的 70 mg/dL 警报响起时已经处在神经性低血糖状态。这不是假设,文章开头写的 Liam 那次凌晨 3:15 警报就是后者:3:18 醒来时冷汗、心率 135、手抖,三个神经低血糖的典型症状都出现了。那个场景里,前半夜如果有系统提前发出警告,情况会完全不同。

所以这个项目的核心问题,不是 TabPFN 够不够强,也不是前端架构选得好不好。是评估没有回答这个工具本身的用途:能救一个人免于半夜醒来面对低血糖吗?0.938 回答不了这个,因为没有召回率,没有混淆矩阵,没有假阴性率。AUC 解决的是排序问题,不是代价对称问题。

文章里那个周六夜晚的案例,比 0.938 更能说明系统的工作机制。确认过的是:22:15 Dexcom G7 显示 114 mg/dL、趋势缓慢下降;输入 IOB 2.6 单位、运动 55 分钟、饮酒 1 杯;TabPFN 在 35 毫秒内给出 96% 概率、预测最低值 47.4 mg/dL、窗口 2:30-4:30 AM;建议 26 克复合碳水加花生酱、临时基础率下调 30% 持续 3.5 小时;用户按建议处理后夜间血糖 94-108 mg/dL,最低 91 mg/dL 出现在 4:15 AM。这个叙事在机制上是闭合的。但它是一个个案,不能替代评估边界。个案证明系统在某一个夜晚起了作用,不证明 0.938 在时间截断下依然成立。

关于 TabPFN 本身,需要单独说一句。TabPFN 在数百万个由因果图和微分方程生成的合成数据集上预训练,免调参,CPU 上 40 毫秒出校准后的后验概率,这些特性对单病人小数据场景是有吸引力的。但也正是因为它这么容易端到端跑起来,很多项目在集成时会把评估这一步省略掉,因为模型出来得太顺了,不报错、不调参、直接给概率。工具越顺,评估越容易被跳过这件事,在这一类项目里可能是个普遍性问题,不止这篇文章。

(分析)对这类 N=1 的本地健康预警工具,评估边界从来就不可能是有没有的问题,只能是一个谱系。越贴近数据本身,数字越好看;越把时间截断,数字越接近真实的预测能力。0.938 落在谱系的哪一段,外界不知道。这就是它只能当自评的唯一原因。

到这里要补一句,我没有说这个项目没有价值。恰好相反,在本地推理、界面复杂度、把一个真实的医疗辅助需求做成 MVP 这几件事上,这个项目完成度不低,能跑通、能部署、有完整前端交互和一个真实的用户反馈闭环。对 Hacktoberfest 的 Build for a Friend 赛道来说,这套东西是成立的,主类别 Best Use of TabPFN、次类别 Best Use of Render 也没问题。但项目价值和 0.938 这个数字的可引用性,是两回事,不能因为前一半成立,就把后一半也当作成立。

几个值得盯的信号,盖文不下注,只列出来一起看:一、这篇文章后续是否补上评估划分方式和时间截断说明,如果补上,0.938 才有机会从自评抬到可核验。二、Onizuka 提出的假阴性率问题,作者有没有回应,回应里有没有混淆矩阵或召回率数字,这是比 AUC 更贴近这个工具用途的一层。三、repo 里 engine.py 的评估逻辑——如果有提交历史,能看到评估是怎么跑出来的,这是公开可查的。四、评论区后续如果出现别的在职医疗 AI 或工程同学交叉印证,这篇稿子的信源层级才可能升级。五、同类单人糖尿病辅助项目在 dev.to 后续更新里是否普遍标注时间序列划分。如果大家开始标,这个项目就成了一个明确的反例;如果没人标,说明这个盲区不只存在于这一个项目。这五条都是公开可核验的,到时候回头对着看,比今天拍脑袋说这个项目行不行靠谱。本期到这,有更准确信源的同学欢迎匿名补充。

盖文
盖文

用信源 + 数据 + 内部 how-it-works 记录大厂怎么运作,分层标注、不下注。

查看主页 →