诚实的统计,而非"AI魔法":我如何在不使用神经网络的情况下构建预测引擎#
如今App Store上几乎每一款应用的介绍都在承诺"AI-powered insights"。打开一看,要么只是套了层壳的外部大语言模型,要么就是一个只会吐出"风险:高"却不做任何解释的黑箱。在开发MeteoHealth——一款寻找天气、睡眠、活动与身体状况之间关联的应用——时,我刻意选择了另一条路。内部没有神经网络,也没有Core ML,只有经典统计学:皮尔逊相关系数、带p值的方差分析,以及针对多重比较的Benjamini–Hochberg校正。一切都在设备端离线完成计算,而更重要的是——每一个结论都能用一句话解释清楚。这篇文章讲的就是这套引擎是如何运作的,以及为什么我认为诚实的统计学是比时髦的"AI-powered"标签更强的产品决策。
"AI-powered"的炒作,以及我为何变得谨慎#
问题不在于神经网络不好——在图像识别或文本生成方面,它们是不可替代的。问题在于"AI"变成了一个可以随意贴到任何东西上的营销词汇,包括最简单的if-else规则。在健康科技领域,这尤其危险:用户看到"恶化风险78%"这样的提示,却无法向应用追问"为什么"。监管机构也注意到了这一点——例如欧盟《人工智能法案》按风险等级对医疗AI系统分类,自2026年8月起要求高风险系统提供解释,让使用者真正理解输出结果。学术文献将这称为"黑箱问题":模型给出预测,却不解释推理过程,而事后解释方法(如LIME、SHAP等)只是对真实逻辑的近似,而非重现——批评者指出,这可能掩盖信任问题,而非真正解决它。
对一款关注身体状况的应用来说,这是双重不可接受的。如果MeteoHealth告诉某人头痛风险正在上升,对方应该能理解:这是因为气压下降,同时睡眠时间也变短了——而不是因为"模型就是这么判断的"。这正是这套引擎背后的决定:把可解释性内建在数学本身之中,而不是事后作为一个独立模块补上去。
皮尔逊相关系数:引擎中的第一个工具#
引擎中最简单的第一个工具是皮尔逊相关系数。它衡量两个变量线性联动的程度——比如气压与每日身体状况评分。取值范围从−1到1:0表示没有关系,接近任一极值则表示关系很强。
/// Pearson correlation coefficient between two data series
func pearsonCorrelation(_ x: [Double], _ y: [Double]) -> Double? {
guard x.count == y.count, x.count > 1 else { return nil }
let n = Double(x.count)
let meanX = x.reduce(0, +) / n
let meanY = y.reduce(0, +) / n
var numerator = 0.0
var sumSqX = 0.0
var sumSqY = 0.0
for i in 0..<x.count {
let dx = x[i] - meanX
let dy = y[i] - meanY
numerator += dx * dy
sumSqX += dx * dx
sumSqY += dy * dy
}
let denominator = (sumSqX * sumSqY).squareRoot()
guard denominator != 0 else { return nil }
return numerator / denominator
}这里没有任何魔法——只有求和、均值和平方根。但这段简单的代码有一个很重要的特性:我可以在调试器里打开它,输入用户的真实数据,看到与引擎完全一致的数字。没有任何神经网络能提供这种透明度——哪怕是只有两三个隐藏层的小型全连接网络,也已经拥有成千上万个权重,根本无法从中提取出人类可读的"为什么"。
方差分析、p值,以及多重比较问题#
光有相关系数还不够——还需要知道在现有数据量下这个结果到底有多可信。这时方差分析(ANOVA)和p值就派上用场:它们回答的问题是"如果实际上根本不存在任何关系,这个结果是否可能纯属巧合?"。p值越小,这种模式仅仅是噪音的可能性就越低。
但这里隐藏着一个经典的统计陷阱。MeteoHealth会同时检验数十组"因素—身体状况"配对:气压、湿度、睡眠、步数、咖啡因、心率变异性等等。如果对每一组配对单独以p < 0.05为阈值检验,那么在20次检验中,平均会有一次仅仅因为偶然就显得"显著"——这就是多重比较问题。如果不加以校正,应用就会开始向用户展示那些看起来很有说服力、却在新数据上根本不成立的虚假规律。
设想一下:某位用户连续三天都是阴天且头痛。单独来看,这个相关性可能会跨过p < 0.05的门槛——但考虑到同时检验的另外十五个因素,这几乎肯定只是巧合。这正是为什么单独一个p值是不够的——需要一个能一次性审视全局、而不是孤立看待某一对变量的步骤。
Benjamini–Hochberg校正:过滤掉巧合#
Benjamini–Hochberg校正用于在同时检验多个假设时,控制所有"显著"结果中的错误发现率。思路很简单:将p值按升序排列,找到仍然低于经过缩放的阈值的最大那个。
/// Benjamini–Hochberg correction for multiple comparisons.
/// Returns the indices of hypotheses that remain significant
/// after controlling the false discovery rate at `alpha`.
func benjaminiHochberg(pValues: [Double], alpha: Double = 0.05) -> Set<Int> {
let m = Double(pValues.count)
let indexed = pValues.enumerated().sorted { $0.element < $1.element }
var lastSignificantRank = -1
for (rank, item) in indexed.enumerated() {
let k = Double(rank + 1)
let threshold = (k / m) * alpha
if item.element <= threshold {
lastSignificantRank = rank
}
}
guard lastSignificantRank >= 0 else { return [] }
return Set(indexed.prefix(lastSignificantRank + 1).map { $0.offset })
}实际情况是:如果引擎检验了身体状况与15个因素的关系,其中三个单独看p < 0.05,校正之后可能只剩下一个真正稳健的结果。展示给用户的正是这一个,措辞类似"湿度超过80%与精力下降之间存在统计学上显著的关联"——这是一个真正经受住了随机性检验的结论。
个人基线,以及约10条记录后的自适应#
绝对数值脱离上下文毫无意义:静息心率68对一个人来说是正常的,对另一个人来说可能已经是偏离。因此引擎会为每个指标建立个人基线,并随着新记录的到来不断更新。
/// Updates a personal baseline using an exponentially weighted moving average,
/// so the model adapts as new daily entries arrive.
struct PersonalBaseline {
private(set) var mean: Double
private(set) var sampleCount: Int
private let smoothing: Double
init(initialMean: Double = 0, smoothing: Double = 0.2) {
self.mean = initialMean
self.sampleCount = 0
self.smoothing = smoothing
}
mutating func update(with value: Double) {
sampleCount += 1
if sampleCount <= 10 {
// Cold start: simple running average for the first ~10 entries
mean = mean + (value - mean) / Double(sampleCount)
} else {
// Warmed up: exponentially weighted average reacts to recent trends
mean = mean + smoothing * (value - mean)
}
}
}在最初大约十条记录中,引擎使用简单的累计平均——这是一个"冷启动"阶段,数据稀少,更适合均匀地平滑掉剧烈波动。之后它会切换到指数加权平均,对近期记录的反应稍微更敏感,同时依然能抵御偶发的异常值。24小时和72小时的身体状况预测正是建立在这些个人基线之上:引擎会将当前的天气、睡眠、活动等条件,与这个人自己的正常范围以及针对他专属确认为显著的相关性进行比较,而不是与所有用户汇总的平均数据比较。
统计学 vs 神经网络:一次诚实的比较#
我本可以在聚合数据上训练一个小模型,或者接入一个云端大语言模型来提供"智能洞察"。我刻意没有这么做,原因如下。
| 标准 | 统计引擎 | 神经网络 / 云端AI |
|---|---|---|
| 可解释性 | 每个结果都可归结为一个公式和一个p值 | 常常是黑箱,需要事后解释 |
| 隐私 | 数据从不离开设备,分析离线完成 | 通常需要将数据发送到服务器 |
| 启动所需数据量 | 每人约10条记录后即可工作 | 通常需要成千上万个标注样本 |
| 离线可用 | 始终可以 | 没有内置模型通常不行 |
| 可预测性 | 确定性、可复现 | 在不同运行和模型版本间可能不同 |
按任务类型划分的第二个对比:
| 任务 | 合适的工具 |
|---|---|
| 找出两个指标之间的线性关系 | 皮尔逊相关系数 |
| 识别照片中的物体 | 神经网络(Core ML、Vision) |
| 检验某个发现是否具有统计学意义 | 方差分析 + p值 |
| 生成自然语言文本 | 大语言模型 |
| 从数十个假设中过滤掉巧合 | Benjamini–Hochberg校正 |
这两张表说明了一点:统计学和神经网络解决的是不同类型的问题,二者并非彼此替代的关系。但对于这个具体任务——在一个人的健康数据中找出个人化、可解释且私密的规律——就实践中真正重要的每一项标准而言,经典统计学都胜过了神经网络:它在小样本下依然有效,不需要服务器,不会"产生幻觉",并且作为开发者,我可以向用户展示预测变得令人担忧的确切原因。Core ML非常适合从照片识别情绪或根据加速度计数据分类活动——但并不适合向一个人解释他明天为什么很可能会感觉更糟这项任务。
这整套引擎都运行在MeteoHealth内部,这也是为什么那里的每一个预测都附带解释,而不只是一个数字。"AI-powered"的炒作终将退去,而用户的信任建立在应用能否解释自己为什么这么说之上。有时候,对"你的AI在哪里"这个问题,最诚实的回答是:"这里没有AI,只有一套真正管用的统计学"。
我觉得健康科技行业正在经历Web开发当年经历过的同样阶段:一开始所有人都想要最时髦的工具,后来所有人才想要真正解决问题的工具。经典统计学不如"神经网络预测你的健康"那样激动人心,但它可复现、可解释,也不要求用户信任一个黑箱。对于一款关乎具体某个人身体状况的应用来说,这不是妥协,而是唯一明智的选择。



