
我的雷达跌倒检测朴素基线同样达到了 1.0 召回率。它还在单夜触发了七次虚警。
在我为 Vigil 构建的模拟夜班中,现成市售基线检测器在 02:00 到 06:00 之间触发了九次警报,其中七次都是错误的。一处吊扇,峰值速度达到 5.0 m/s;一只抚慰犬,雷达散射截面为 0.27;一位住户以 2.92 m/s 的速度重重坐到座椅上。在这九次警报中,只有两次是真实的跌倒,其中一次发生在卫生间:质心轨迹从站立时的 1.53 m 下降,依次穿过 1.07、0.84、0.625 和 0.344,最后稳定在 0.119 m(地面高度),伴有呼吸信号,未见起身恢复。
该轮班中的每一个事件都是从固定随机数种子生成的、带标注且符合物理真实性的合成数据,而两个检测器也都是我编写的。这也是为什么我能坦率地说出那句让人不适的实情:那个卫生间跌倒事件,基线检测器同样也捕获到了。
Vigil 是我构建的智能层,位于老年照护机构的雷达特征流与呼叫铃系统之间。它返回 ALERT、SUPPRESS 或 ROUTE TO HUMAN,并为每项决策附带一条机构可用于归档的原因说明。演示地址为 https://veriprajna.com/demos/smart-facility-fall-detection。在开始构建时,我曾以为难点在于发现跌倒。但基准测试在首次运行时就推翻了我的假设。
召回率曾是我最想拿来作为主打的数据
我运行基准测试时,预期跌倒灵敏度会成为头条数据,而这确实是一个不错的数据:在包含 360 个带标注、含噪声的合成事件的固定集合上,级联分类器的召回率为 1.0。然而它旁边的一列彻底改变了我原以为要写的内容。朴素基线仅仅是两条算术子句,任何快速或低位运动都被视为跌倒(peak_v > 2.0 OR min_cz < 0.45),而在同一个数据集上,它的召回率同样达到了 1.0。灵敏度正是跌倒检测营销所围绕的核心,而这两个检测器都死死顶在了最高点。
二者的真正差距完全落在了无人将其放上幻灯片的那一行。级联分类器的干扰项特异度为 1.0,而基线仅为 0.167,相当于每个良性事件的虚警率为 0.833。按基准测试假定的每间房每天 30 次良性动作触发来推算,基线检测器将达到每间房每天 25.0 次虚警。现存市售传感器的公开虚警范围是每间房每天 5 到 15 次,而文献记录表明,报警疲劳而非传感器灵敏度才是导致此类系统部署失败的首要原因。
在技术读者替我指出来之前,我应该先主动说明这一点。位于 data/fall_model.json 中的融合权重是由 tools/fit_fall_classifier.py 在演示自身的场景生成器上拟合而成的,而这些生成器正是生成那组 360 个事件集合的同一个工具。这是任何人对我那两个 1.0 能够提出的最强烈质疑,也正因如此,比起这两个 1.0,我其实更看重那个 0.167。基线的失败并不是我训练设置的偶发伪影。当现实世界中存在吊扇时,单纯依靠阈值就是会出现这种结果。
那七次虚警决定了当真实的警报到来时,是否还会有人在倾听。
干扰项正是为了击败单一特征而构建的
我的第一直觉是把分类器做得更好,但这个直觉是错的。在构建初期,我一直将其视为一个判别问题:找出区分跌倒与非跌倒的特征,赋予重权,然后继续下一步。但我预先编写的场景生成器特意打破了这种可能性。
每个干扰项的生成都是为了在某个单一特征上与真实跌倒重叠。Cam 5 的重坐动作带有 2.92 m/s 的突发速度,已达到跌倒的量级,随后质心稳定在 0.46 m。其在卫生间的对应案例 Cam 10 峰值速度达到 3.31 m/s,质心稳定在 0.44 m。抚慰犬和弯腰捡毛巾都会让质心降低,这正好触发了基线规则的另一半。我能编写的任何单一测试在设计之初就被击败了,这也是为什么基线在 0.167 的特异度上被真实地愚弄,而不是被我为了让其失败而设立的稻草人所欺骗。
速度曾是我最确信的特征,但它最终却未能进入分类器。保留下来的是基于四个特征的逻辑斯蒂模型——地面接近度、冲击能量、下落落差以及雷达散射截面代理指标,它们融合成一个校准后的 P(fall)。没有任何速度项直接参与 P(fall) 的计算。模块文档字符串中甚至还残留着一行当初我以为它会参与计算的陈旧注释。它完全由纯 numpy 编写,代码量小到任何人都可以直接打开 classifier.py 并在脑海中完整理解其逻辑。在生命安全攸关的关键路径上,这种清晰度对我而言远比再提升一个百分点的 AUC 更有价值。
我总是首先把 Cam 10 的抑制案例展示给人们看,因为该面板用短短一行字就道明了二者的全部根本分歧。

四个条件,一个 8 秒窗口
我编写时序叙事验证器时,是将其定位为我自己作为外部人员会希望阅读的代码。temporal.py 要求在 同一个 8 秒窗口内满足四个条件,且在窗口的前五分之一时间内确立站立状态:中位数质心高于 1.2 m;在窗口内某处发生大于 0.6 m 的下落并伴有高于 1.8 m/s 的峰值速度;3 帧滑动平均超过 0.50 的持续宽带冲击;且质心实际落到 0.30 m 以下。设立持续冲击测试的原因在于,单帧脉冲很容易出现,而人体撞击地面则截然不同。
应用程序输出的原因字符串显示为“standing → descent → impact → floor”,这也是护士理解事件的方式,但底层实现是在整个窗口内对这些条件执行逻辑与(AND),而非强制要求严格的时序。它不是状态机,我宁愿自己主动写明这一点,也不希望有工程师在源码中发现它后怀疑文案还掩盖了哪些细节。
只有在此之后,门控才会加入高于 0.20 的呼吸确认以及至少 0.70 的跌倒置信度。这三个数值——地面高度 0.30 m、呼吸 0.20 以及 0.70 的置信度底线——完全以纯代码形式独立存在于所有模型之外。逻辑走向才是关键所在:在查询模型评分之前,确定性条件必须首先成立,因此单独一个置信度数字永远无法自行凭空产生警报。较低的 P(fall) 仍可将 ALERT 变为 SUPPRESS,对于一个被允许保持沉默却绝不允许凭空捏造的层级而言,这是恰当的非对称设计。在该文档记录的代码块之外,还存在一个决定性的硬编码阈值:p_fall >= 0.40,位于 gate.py 中,它仅凭模型评分就能将多人同室事件转交人工核查。我特意指出这一点,是因为否则所谓“三个文档记录的阈值”的说辞就有些言过其实了。
被抑制的记录正是州监管审查真正要求的凭证
在做出任何看起来像产品的东西之前,我就先构建了决策台账(Decision Ledger),因为我最难回答的问题从来不是“你们检测到了吗”。而是“为什么凌晨 2:13 在 203 房间没有报警”,而在任何人发问之前,答案就必须已经存在于书面记录中。当班的十二个事件中有十个是被抑制的,每个事件都附带了记录在案的特征值:Cam 6 非人目标的雷达散射截面为 0.27(人体最低标准为 0.55);Cam 7 和 Cam 12 的弯腰动作质心分别停在 0.60 m 和 0.59 m,冲击能量为 0.07(阈值为 0.50)。

那十条被抑制的记录正是审查员会重点询问的内容,因为在这些事件中表面上什么都没发生,但仍必须有人解释清楚其中的缘由。
每间房间的校准数据中只有一部分真正接入了决策逻辑。Cam 1 的吊扇之所以被抑制,是因为 214 房间的杂波图在 (1.5, 1.5, 2.45 m) 处包含一个固定位置的多普勒条目,并且 check_clutter 在该体素处对其进行了掩蔽。这条路径是真实生效的。而位于 rooms.json 中的每间房座椅与床铺高度(118 房间卫生间为 0.42 m,203 房间为 0.45 m)以及旁边的扶手条目,则是 V1 代码路径均未读取 的校准数据;我用来标记重坐的座椅高度区间是一项全局 0.38 至 0.60 m 的测试。该文件中甚至还有一个 long_lie_sec: 180.0 键完全没有被任何代码使用。分房校准是真实项目部署需要付费的集成工作,而在本版本中,只有多普勒掩模真正连接到了决策中。
系统导出的轮班审计 JSON 涵盖了每一次报警、转交和抑制,并附带决定性特征值和策略原因,这正是 CMS F689 规范或 QAPI 档案所需要的材料。临床事件记录则是由这些结构化证据单独整理生成,并显示在事件面板中,而非存放在 JSON 内部。
我允许系统发出的警报,与我坚决不允许它断言的跌倒
我特意将这轮值班的高潮安排在卫生间。那是风险最高的房间,也是摄像头完全不可行的场所:美国有 19 个州已颁布管理养老院房间内摄像头的法律,通常在征得同意的情况下允许在住户房间安装,但在实践中出于隐私考量,卫生间始终被排除在外。雷达特征不包含任何图像,这正是它能进入摄像头禁区的原因所在。
Cam 3 正是那个事件,Vigil 返回了 ALERT,类别为 long_lie,置信度 0.99,地面停留时间 4.8 s,升级梯队设置为:立即通知注册护士助理(CNA),90 秒通知当班主管护士(Charge Nurse),180 秒通知护理总监(DON)。呼叫铃徽章文本显示:“118B 房间卫生间:检测到跌倒,置信度 99%。住户倒地 5 秒。呼吸已确认。”调度系统同时发出传统的 Rauland 干接点信号和 Ascom/Austco MQTT/REST 载荷,两者均通过记录在案的适配器桩模块传输。整个系统目前并未连接任何实体呼叫铃硬件。即便如此它依然值得构建的原因在于长时间倒地不起(long lie):在倒地超过一小时的老人中,有一半会在六个月内离世。

那个面板上有一个数字我坚决不愿拿来夸大宣传。从撞击到报警的 7.0 秒是在 gate.py 中通过保持计时器加上 3 秒计算得出的常数。界面上展示了它,我也会如实引用该展示值,但它仅仅是算术结果而非实测的系统响应速度;如果将它称为基准测试延迟,那就是一种微小的不诚实,而这种不实会让你失去它身旁那些更为重要的真实价值。
让我倍感自豪的反而是一起被 Vigil 拒绝断言的事件。Cam 2 在真实真值中确属跌倒,且 P(fall) 达到了 0.99,但 Vigil 依然没有武断判定:该房间内存在两个目标,而单人跟踪超出了 V1 的覆盖范围,因此门控返回了 低置信度的 ROUTE TO HUMAN(转交人工)。朴素基线则直接自动触发,将本不属于它的捕获功劳据为己有。那次值班中发生了两起真实跌倒。Vigil 对其中一起发出警报,将另一起交由工作人员核查,我绝不会将其粉饰为捕获了每一次跌倒,因为事实并非如此。在整个基准测试中,全部 40 起多人同室跌倒事件均被转交人工,既无过度报警,也无一遗漏。
记分牌究竟被允许宣称什么
“值班结果”弹窗下方的限制说明行是我在该面板中最先起草的内容。该弹窗报告了本班次中引擎的 0 次虚警(对比现有系统的 7 次),捕获 2 起真实跌倒中的 1 起,1 起转交人工,在 360 个标注事件中实现了 100% 的干扰项特异度,以及对比现有系统预计每间房每天 25 次虚警的 0.0 次。

这些数据仅代表一个固定的合成黄金数据集,别无其他。它们不代表生产环境准确率,不是临床结论,不是经过验证的医疗声明,更绝非对机构的保证。在影子模式校准后,真实的试点项目目标是每间房每天虚警低于 2 次,而这才是我会呈现在护理总监面前的数据,因为这是一个我能够兑现并承担责任的承诺。0.0 仅证明该机制在我可以交给你的数据集上能够区分跌倒与干扰项;它并不是对我从未涉足过的建筑做出的保证。
我现在衡量生命安全警报的标准
完成这次开发后,我对跌倒检测到底应该擅长什么有了更为严苛的定义。检测只是一个阈值,而在我自己的测试集上,单一阈值就已经能拿到 1.0 的召回率。真正能赢得护士信任的工作在于拒绝虚警:明确知道风扇位于哪个体素的杂波图、绝不接受单帧毛刺的冲击测试、区分重坐与跌倒的触地条件,以及经过专门编写使得州监管人员(而非模型本身)能够看懂系统决策缘由的门控逻辑。
完整的演示流程请访问 https://veriprajna.com/demos/smart-facility-fall-detection,而台账中那十条被抑制的记录,才是真正决定这轮值班成败的地方。
如果你更愿意亲眼观看这次值班过程,而不是阅读我的文字描述,这里是整夜运行的端到端完整记录。
一个会对吊扇报警的系统不出一周就会被静音,而一个被静音的系统什么也检测不到。我能赋予这一智能层最精密复杂的行为,就是记录在案并附带决定性数据的拒绝能力。在 Cam 2 上,那个数据是 0.99,而正确的抉择依然是将事件交由人工处理。


