深入 Android 端侧 AI 推理的效果评估与持续优化:从离线评测基准到在线实验的指标驱动迭代
去年在做端侧 OCR 识别时,我们换了三个模型版本,每次用几张图片跑一下看了个大概就上线了。灰度阶段用户反馈识别准确率下降了 3 个百分点。回滚之后复盘,发现整个团队没有一套可靠的评测机制——“感觉差不多”就是我们的上线标准。
端侧 AI 的质量保障比服务端更棘手:模型跑在用户设备上,出了问题没法像服务端一样热修,更没有现成的日志和监控数据回流。大量团队在聚光灯下做模型训练和压缩,却在评测环节靠”人肉抽检”。我花了大半年时间从零搭建了一套评测体系,思路是:离线 Benchmark 做初筛,在线实验做验证,两者形成闭环持续迭代。
离线评测:用 Benchmark 把模型逼到墙角
工程侧的离线评测和学术界的 Benchmark 不一样,它要回答三个问题:这个模型在我的场景下表现如何?在极端条件下会崩吗?和当前线上版本比是变好还是变差?三个问题答不上来,离线评测就是摆设。
构建领域数据集
学术界的数据集不能直接拿来用。我们做移动端文档扫描,ImageNet 的指标再高也没意义。我从线上日志里采样了 2000 张真实用户上传的图片,覆盖 7 种光照条件、15 种倾斜角度、4 种文档类型。
数据集结构:
benchmark_dataset/
├── easy/ # 平整、光线均匀,预期高准确率
├── medium/ # 轻微倾斜、阴影,线上常见场景
├── hard/ # 严重畸变、弱光、模糊,极限条件
└── ground_truth/ # 每张图片的标注结果
分难度等级很关键。一个模型在 easy 集上提升 5% 但 hard 集上下降 10%,这个模型就不能上——线上用户大部分场景其实落在 medium 和 hard 区间。
量化指标设计
OCR 场景单纯看字符识别率不够。我设计了三个维度的指标:
- 字段级准确率:按业务字段(姓名、金额、身份证号)分别统计,避免模型”偏科”
- 端到端耗时:P50/P95/P99 延迟,必须在主线程外测量,否则数据会被 UI 渲染帧干扰
- 模型稳定性:同一张图片重复推理 100 次,计算输出结果的方差,检测随机性崩溃
data class BenchmarkResult(
val fieldAccuracy: Map<String, Float>, // 字段级准确率
val latencyP50: Long, // 毫秒
val latencyP95: Long,
val stabilityScore: Float // 0-1,越高越稳定
)
自动化评测管道
我搭了一个 Gradle Task,每次有新的模型文件提交,CI 自动跑完 Benchmark 并生成对比报告:
./gradlew runBenchmark \
-PmodelPath=./models/ocr_v3.tflite \
-PdatasetPath=./benchmark_dataset \
-PbaselineModel=./models/ocr_v2.tflite
如果新模型在任一维度低于基线超过 5%,CI 直接标红阻断合并。这个阀门在后续迭代中至少拦住了 4 次回退版本的合入。
在线指标:用户不会按你的 Benchmark 出牌
离线评测再好,也模拟不了真实世界的多样性。手机上跑模型,变量太多:芯片型号、系统版本、后台进程抢占 CPU、内存压力导致的 GC 卡顿,甚至手机温度过高时的降频。
端侧埋点方案
思路是轻量埋点 + 采样上报。全量上报会打爆用户流量和电量,只对 5% 的用户开启详细日志采样。
data class InferenceTrace(
val modelVersion: String,
val deviceInfo: DeviceInfo, // SoC 型号、RAM、Android 版本
val inputSize: Int, // 输入图片像素数
val preprocessMs: Long, // 前处理耗时
val inferenceMs: Long, // 纯推理耗时
val postprocessMs: Long, // 后处理耗时
val confidenceScore: Float, // 模型输出的置信度
val batteryTemp: Int, // 推理时的电池温度
val isCharging: Boolean
)
实际跑下来发现,置信度分布比单次推理结果更能反映模型质量。如果线上大量请求的置信度集中在 0.5-0.6 区间,说明模型在大量场景下是”蒙的”,即使最终准确率不算低,也是靠后处理逻辑强行兜底。
梯度监控看板
在线指标分三个梯队:
L1 - 业务指标(每天看):功能成功率、用户投诉率。最直接的信号,出了问题立刻感知。
L2 - 模型质量指标(每周看):按机型和 Android 版本分组的置信度分布、P95 耗时。能帮你发现”三星 S22 上置信度系统性偏低”这类问题。
L3 - 工程指标(按需看):模型加载失败率、OOM 率、各阶段耗时拆解。用于排查具体问题。
踩过的一个坑是:初期把置信度阈值设得太低,导致大量低质量结果透出到业务层。后来加了一个动态阈值——根据当前设备性能档位和输入复杂度,自适应调整置信度门槛。低端机上宁可不识别,也不给一个错误结果。
A/B 实验:把直觉判断变成数据决策
离线评测说模型 A 更好,产品经理觉得模型 B 更准。这种争论没有意义,直接上在线 A/B 实验。
实验设计
端侧 AI 的 A/B 实验和常规功能实验有一个关键区别:模型包的分发受限于包体积。无法把两个模型都打进 APK,所以采用按需下载策略——用户进入实验组后,首次使用功能时从 CDN 拉取对应的模型文件。
object ModelExperiment {
fun getModelConfig(userId: String): ModelConfig {
val bucket = userId.hashCode() % 100
return when {
bucket < 50 -> ModelConfig("ocr_v2", downloadUrlV2) // 对照组
else -> ModelConfig("ocr_v3", downloadUrlV3) // 实验组
}
}
}
用 userId 哈希分流而非随机数,保证同一用户多次进入实验时分配一致,避免体验跳变。
核心观测指标
A/B 实验不是只看准确率。我定义了一个综合质量分:
质量分 = 功能成功率 × 0.4 + 用户留存率 × 0.3 + (1 - 投诉率) × 0.3
权重的确定来自历史数据的回归分析——功能成功率对用户留存的影响最大,因此权重最高。用户不会告诉你模型好不好,但会用脚投票。
实际跑下来,一个在离线 Benchmark 上提升 8% 的模型,在 A/B 实验中只跑出了 1.2% 的综合质量分提升。Benchmark 数据集里缺少大量”用户拍完就放弃识别”的场景——那些图片质量极差,模型照样处理不了。
闭环迭代:从发现问题到验证修复
有了离线评测、在线监控和 A/B 实验三套机制,就能形成闭环:
- A/B 实验发现:某机型上置信度分布异常
- 离线 Benchmark 复现:用同 SoC 的测试机跑一遍,定位到特定算子(如 Conv2D)在骁龙 7 系上耗时异常
- 针对性优化:调整模型结构或量化策略,重新训练
- 离线验证通过后重新上线:CI Benchmark 绿灯 → 灰度 → 全量
闭环跑通后,团队从”拍脑袋上线”变成了”看数据迭代”。模型迭代周期从 3 周缩到了 1 周半。
几条实践建议
不要迷信 Benchmark 数据集。线上数据永远比你手里的数据集更脏、更乱、更多样。建议每月从线上采样一批新图片补充到 Benchmark 数据集中。
优先监控 P95 而非平均值。平均值会掩盖尾部用户的问题。一个模型在高端机上跑 50ms 在低端机上跑 2s,平均值可能只有 200ms,但 5% 的用户体验极差。P95 能帮你看到这些用户。
把模型质量指标告警接进 On-Call 系统。端侧模型出问题不会有服务端报警,你需要主动监控。我设的规则是:任一机型上 P95 置信度低于 0.55 超过 30 分钟,自动触发告警。这条规则在上线第二周就帮我们拦截了一次因模型文件损坏导致的大面积识别失败。