首页 / AI教程 / 正文
AI教程

从0到1搭建图像识别小应用:数据标注、模型轻量化、端侧部署与发布全流程

chuanbook chuanbook
发布于 2026 年 10 月 20 日
阅读 约25分钟
浏览 8
评论 0

1.1 明确识别目标、用户流程与成功指标

我最初想做图像识别小应用,是因为看到朋友在公园里举着手机对着一丛花拍半天,最后还是在群里问“这啥花”。这个瞬间让我觉得,识别类小工具有真实需求。可想法一落到纸上,问题就来了:识别什么?识别给谁用?我用一句话把目标钉死——“帮普通用户在户外拍到花之后,两秒内知道它大概叫什么”。目标一具体,后面的决策都变得有依据,什么该做、什么不该做,心里有杆秤。

用户流程我画了十几遍,删繁就简,剩下的是:打开小程序,点拍照或者从相册选图,屏幕上出现一个转圈的小动画,接着是结果卡片——花名、置信度、一张相似图、三行养护提示。整个流程不超过四步,每一步都允许用户反悔。我特别在意那个“等待”的瞬间,超过两秒人就会烦,这里必须给足反馈,进度条也好,小动画也罢,别让用户干等。

成功指标我没敢定太多,三个就够。识别准确率,前五名命中率到八成我就满意;首屏响应时间,弱网下不超过三秒;次周留存,有百分之十五的人在第二周还会打开它。这三个数字看起来朴素,却是我后续所有技术选型和功能取舍的标尺。指标不能定得虚,什么“用户体验好”这种话,写进文档里等于没写,得能拿数据量出来。

1.2 拆解小应用功能边界与MVP范围

我给自己定了一条铁律:MVP只回答一个问题——“拍一张照,能不能给个靠谱的答案”。围绕这条线,我把功能分成三堆。必须有的:拍照、上传、识别、结果展示。可以有的:相册选图、识别历史、结果分享。先不做的:社区、百科详情页、收藏夹、多语言、用户登录。写这个清单的时候手很痒,总觉得少点什么,加着加着就变成一个大而全的产品,赶紧划掉。

功能边界一砍,工期立刻从三个月缩到三周。我见过太多人卡在“先把账号体系搭起来”上,结果核心识别还没跑通,人已经累了。我的做法是连登录都不要,用户打开就能用,识别记录存在本地,想分享就生成一张带水印的图。这样做牺牲了一些运营数据,换来的是极短的启动路径和极低的使用门槛。

边界清晰还有个隐形好处:团队沟通成本骤降。我一个人做这个项目,不存在沟通问题,可要是拉上朋友一起,一句“我们只做拍照识别”能省掉无数争论。范围之外的想法,我都记在一个叫“以后再说”的文档里,那份文档现在已经很长了,但它安安静静地躺着,不干扰我眼下的事。

1.3 选择技术路线:云端API、本地模型与混合方案

技术路线这块我纠结了挺久。云端API最省事,调用现成的视觉服务,传一张图过去,JSON回来,一天就能跑通。代价是每次调用都要花钱,图片要离开设备,没网就抓瞎。我算过一笔账,按一天两百次调用、每次几分钱算,个人项目扛得住,可真要放量,账单会很难看。隐私也是个坎,用户拍的花里可能带人脸、带位置,这些数据传到别人服务器上,心里不踏实。

本地模型是另一条路。把轻量模型塞进手机或者浏览器,离线也能跑,数据不出设备,调用成本几乎为零。麻烦在于精度和体积的平衡,模型压得太狠,识别率掉得厉害,压得不够,包体又大得吓人。我第一次导出TFLite模型,文件有二十多兆,装到低端安卓机上冷启动慢得让人想摔手机。折腾了几轮量化,压到五兆以内,精度只掉了两个点,才算能接受。

我最终走的是混合方案。日常识别走端侧模型,遇到置信度低的图,弹一个“要不要用云端再确认一下”的选项,用户同意才上传。这样大部分请求不花钱,少数难例交给云端兜底,精度和成本都照顾到了。这套方案写起来复杂一点,可它给了我一条平滑的演进路径——先用云端快速验证产品,再把高频场景一点点挪到本地。技术选型别追求一步到位,留个后门,比什么都强。

1.4 规划数据、模型、手机或网页部署的总体链路

整条链路我在白板上画过一遍,从左到右大致是:数据采集、清洗标注、训练评估、模型导出、前端集成、接口联调、监控回流。每一环都有最小可行的做法,不必一上来就上重型工具。数据这块,我先用公开数据集打底,再自己拿手机去公园拍了两千多张,补上本地常见花种。标注用开源工具,两个人一下午标了八百张,够跑第一版模型了。

模型训练我没追求大网络,选了个预训练 backbone 做迁移学习,在消费级显卡上跑几个小时就出结果。评估看混淆矩阵,把容易混的几种花拎出来单独补数据。导出环节试了ONNX和TFLite两种格式,网页端用ONNX Runtime Web,手机端用TFLite,两条腿走路。这里踩过的坑不少,光算子不支持就折腾了两天,记下来以后能省事。

部署链路我拆成前后端两条并行推进。前端先做网页版,浏览器里直接推理,省掉服务器;后端只保留一个低置信度的兜底接口和日志上报。手机端排在网页版验证之后,用同样的模型文件,换个推理引擎。监控这块起步很土,就记识别次数、置信度分布和用户手动纠错的次数,每周看一次。链路不求优雅,先跑通,再谈优化。

2. 数据集与模型:搭建图像识别小应用需要哪些数据集和模型

2.1 数据来源、采集规范与标注方案

数据这事儿我一开始想得太简单了。心想网上公开数据集那么多,随便下几个不就能开跑了。真动手才发现,公开数据集里的花多半是专业摄影棚拍的,背景干净、光线均匀,跟我用户拿手机在公园随手咔嚓的根本不是一回事。模型在干净图上能考九十分,到了真实场景直接掉到及格线以下。我后来把公开数据只当热身,真正管用的是自己揣着手机去公园蹲了三个周末,拍了大概两千多张。拍的时候我定了几个死规矩:同一朵花必须换三个角度、顺光和逆光各来一张、偶尔故意把手指头或者路人拍进去。这些“脏数据”在训练里比干净图片值钱多了。

采集规范其实是在踩坑里长出来的。我头一周拍回来的图,三分之一是糊的,对焦都没对准,标注的时候自己都看不清。从那以后我给自己上了个紧箍咒——拍完当场回看,模糊的直接删,光线太暗的删,花占画面不到四分之一也删。每张能用的图,文件名按“日期_地点_花名_序号”存,文件夹按品种分好,后面标注能省一半力气。这些规矩看着琐碎,可两千张图要是乱成一锅粥,光整理就能把人逼疯。

标注方案我没用太重的工具,LabelImg 和 CVAT 来回试了试,最后在 CVAT 上把八百张图标完了。标的是矩形框加品种标签,没上像素级分割,原因很简单——我的应用只需要告诉用户“这是什么花”,不需要精确轮廓。标注人员就我和一个朋友,标之前先对齐了标准:一朵花被叶子挡了一半算不算、一丛花里有多个品种怎么选主目标。这些边界情况不提前说好,两个人标出来的数据标准差得离谱。标完的每一批我都抽百分之十复查,发现歧义就回头改,改完再统一一遍。数据这关不过,后面模型再好也是空中楼阁。

2.2 数据预处理、增强与训练/验证/测试划分

预处理没太多花哨的东西。图片统一缩到 224×224,这是大多数轻量 backbone 的甜点尺寸,再大精度涨不了多少,推理速度反而掉得厉害。归一化用的 ImageNet 的均值和方差,因为后面挑的预训练模型就是在那上面训的,对齐一下能省不少收敛时间。所有图片转成 RGB,碰到个别带透明通道的 PNG 直接填白底。就这几步,写了不到三十行脚本,批量跑一遍完事。

数据增强是我涨点最猛的一招。随机水平翻转、小角度旋转、亮度对比度抖动、随机裁剪再缩放回去,这几样叠在一起,训练集瞬间“变”出好几倍样本。我特别注意没敢用太狠的垂直翻转,花又不是倒着长的,翻了反而引入噪音。色彩抖动幅度也压了一半,怕把花色本身的信息搅乱。还有个土办法很管用——随机擦除一小块区域,逼模型别老盯着花瓣尖那一处特征,学着看整体轮廓。增强之后我的验证集准确率大概涨了七八个点,过拟合的苗头也压住了。

训练集、验证集、测试集按七比一点五比一点五切。这里有个坑我差点栽进去——同一朵花拍的多角度照片必须落在同一个集合里。头一回我随手随机切,结果训练集里有一张正面照,测试集里刚好有同朵花的侧面照,模型等于考前看过答案,评估数字虚高得漂亮,上线就露馅。后来我按“拍摄批次”切分,同一个周末拍的图整批归到同一个集合,宁可训练集少一点也要保证评估干净。测试集切完就锁进保险箱,不到最终评估不拿出来碰。这个习惯救了我,后面调模型调得再疯,心里始终有个没被污染过的标尺。

2.3 模型选型:预训练模型、迁移学习与轻量化模型

选 backbone 的时候我在 MobileNetV3、EfficientNet-Lite 和一个小号的 ResNet 之间来回摇摆。没有一上来就冲最猛的 ViT,我手机端要跑,参数一多推理就卡。最后选 MobileNetV3-Small 打底,原因是它在低端安卓芯片上实测冷启动最快,精度虽然比 EfficientNet 差一两个点,可用户感知到的“打开就能拍”比多认对一朵花重要得多。选定之后加载 ImageNet 预训练权重,把最后一层分类头换成我自己的花种数量,前面的层冻结着先训头,等 loss 降稳了再解冻最后两个 block 一起微调。这套迁移学习的流程跑下来,三千张图不到两小时就收敛了,消费级显卡完全够用。

轻量化这块我做了两轮尝试。第一轮直接拿原始模型转 TFLite,文件二十三兆,装到一台旧红米上冷启动四秒多,我自己都等得想退出。第二轮上了量化感知训练,在训练阶段就模拟 int8 推理的误差,导出后模型压到五兆出头,精度掉了不到两个点。第三轮又试了把输入分辨率从 224 降到 192,速度再快一截,精度几乎没动,因为花的关键特征在中等分辨率下已经够清晰了。这三板斧砍完,模型在低端机上冷启动压到一秒以内,包体也在可接受范围。选模型不是选最强,是选最合适——这个道理我是在红米上跑通 Demo 那一刻才真正信了。

还有个细节值得记一笔:我留了一个云端大模型做兜底。端侧模型置信度低于阈值的时候,界面弹一个小提示问用户要不要联网确认。这个设计让端侧模型可以放心地压,不用为了难例硬扛精度。端侧负责日常快问快答,云端负责疑难杂症,两边各干各擅长的事。模型选型走到这一步,我最大的体会是别指望一个模型打天下,把场景拆开,小模型也能干出漂亮活。

2.4 训练、评估与误差分析

训练流程我固定成了模板:固定随机种子、余弦退火学习率、早停耐心设成五轮、batch size 拉到显存允许的最大。这几项定下来之后,每次实验变量就控制住了,跑出来的结果可比。我前后大概跑了四十多次实验,大部分改动都是小修小补——换个增强强度、调一下权重衰减、试试标签平滑。真正带来大提升的就两次,一次是加了随机擦除增强,另一次是把容易混的几种花单独过采样。实验记录我用一个表格手记,日期、改动点、验证集准确率三列,简单但管用,回头翻的时候一眼能看出哪条路走通过、哪条是死胡同。

评估我不只看一个总准确率。混淆矩阵打出来之后,月季和玫瑰混得最厉害,这俩本来长得就像,我自己有时候都分不清。还有连翘和迎春花,颜色形状都接近。针对这几对冤家,我专门补了五百张特写图,把花蕊、叶片排列这些区分点喂给模型。前五命中率从八成二提到八成九,用户实际感受是“猜错的时候第二个选项通常是对的”,体验好了不少。评估指标里我还盯着“高置信度错误率”——模型拍胸脯说九成五把握结果还错的那种,这种错误对用户信任伤害最大。压这个指标比冲总准确率更让我上心。

误差分析我每周做一次,从测试集里抽五十张错例一张张看。看多了发现规律:逆光剪影的图错得最多,花瓣细节全丢了;还有花朵占画面太小的远景图,模型容易忽略主体。前者我在增强里加了模拟逆光的亮度变换,后者在预处理里加了一步智能裁剪,检测到主体太小就先裁再缩。这两招下去,测试集准确率整体涨了四个点左右。误差分析不神秘,就是蹲下来一张张看错在哪,看够一百张,改哪里心里自然有数。

2.5 模型导出与格式转换准备:ONNX、TFLite、Core ML等

导出环节是我踩坑最多的地方,没有之一。PyTorch 训完的模型不能直接扔到手机里跑,中间得转格式。网页端我走 ONNX 路线,torch.onnx.export 一把梭,动态轴设好 batch 维度,导出后在 ONNX Runtime Web 里加载。头一回导出就报错,说我模型里有个自适应池化层 ONNX 不支持,查了半天文档改成固定尺寸池化才通过。导出完我养成了一个习惯——用 onnxruntime 跑一遍和 PyTorch 输出逐元素比对,误差超过千分之一就回头查,这步不做后面线上出诡异 bug 能查到崩溃。

手机端 TFLite 的坑更深。PyTorch 转 TFLite 得先转 ONNX 再转中间格式,中间那个转换器对算子挑剔得很。我模型里用了个 Swish 激活函数,转换器不认,报错信息还特别模糊。试了几轮不行,干脆把 Swish 换成 ReLU,精度掉了零点几个点,转换一次通过。这件事给我的教训是——选模型架构的时候就得把导出路径想清楚,别等训完了才发现算子不支持,回头重训的成本比换个激活函数高太多了。TFLite 导出后我在三台不同年代的安卓机上跑了 benchmark,冷启动、单帧推理耗时、内存占用都记下来,这些数据后面做性能优化时是基准线。

Core ML 这条线我暂时没深入,iOS 端准备放到二期。粗略试了一下 coremltools 转换,流程和 TFLite 类似,也是对算子有要求。我的打算是等网页版和安卓版跑稳了再碰 iOS,一次搞三个平台容易哪个都搞不精。格式转换这件事我总结了一条经验:训练之前先拿一个 dummy 模型把整条导出链路走通,从 PyTorch 到目标格式再到推理引擎跑通一次,确认没问题再开训。这个“链路预演”花不了一小时,能省掉后面无数返工。模型文件最终交付前我还会做一次版本归档——权重、配置文件、预处理参数、评估报告打包放一起,过两个月回头找东西不至于抓瞎。

3. 应用开发与部署:图像识别小应用如何部署到手机或网页

3.1 应用架构:前端交互、推理引擎与后端服务

模型导出完了,TFLite 和 ONNX 文件静静躺在文件夹里,那会儿我觉得最难的关已经过了。真动手搭应用才发现,模型只是个零件,架构这层要是想岔了,零件再好也装不成一台能跑的车。我一开始把架构画得很重,前端、推理、后端三个模块加消息队列加缓存,画完自己看着都头大。后来逼自己拿张 A4 纸重新画,只画数据怎么流:用户按下快门,图片往哪走,推理在哪跑,结果怎么回到屏幕上。简化完之后就三层,前端交互层、推理层、可选的服务层,中间用标准接口隔开,哪层换实现都不影响其他两层。

这种分层对我这种一个人吭哧吭哧写代码的特别友好。网页版推理层我换过两次实现,第一次用云端 API,后来改成浏览器端 ONNX Runtime Web,前端代码一行没动,只换了推理层的一个适配文件。手机端也是同样的结构,摄像头采集、图像预处理、TFLite 推理、结果渲染,每块单独写单独测。有个朋友后来接手帮我看代码,说你这结构看着平淡但拆得干净,我说平淡就对了,我一个人维护的项目,架构越花哨死得越快。

后端服务这块我走了条折返路。最初想着全放云端,客户端只当个拍图上传的壳,写起来最快。后来用的人多了,服务器账单一路往上蹿,网络差的用户等图片上传能等到骂人。我把日常推理全挪到端侧之后,后端就剩两件事:接收低置信度图片做二次确认,以及收匿名统计看模型在哪类图上犯怵。后端代码从 Django 砍到 FastAPI 一个小服务,部署在一台最低配的云服务器上,一个月花费还没我两杯咖啡贵。架构这东西做到后面我悟出来一点,能少一个组件就少一个,每个多出来的组件都是半夜报警的潜在源头。

3.2 网页部署:浏览器端推理与云端API集成

网页版是我最早做出来的能用的东西,因为迭代最快,改一行刷新就能看效果。头一版走的是云端 API 路线,用户选张图,前端 POST 到我的服务器,服务器跑 PyTorch 推理再把结果吐回来。开发阶段在本地跑得挺欢,上线第一天就让现实抽了一耳光——手机 4G 网络下上传一张两兆的图要三四秒,服务器那边冷启动再等两秒,用户盯着转圈圈等到第六秒的时候基本已经想关了。我在日志里看到跳出率高得离谱,才下决心把推理搬到浏览器里。

浏览器端推理我用的是 ONNX Runtime Web,加载第 2 章导出的那个 ONNX 文件。头一回加载慢得我以为卡死了,十二兆的模型在主线程里解析,页面直接冻住三秒多。后来把加载放到 Web Worker 里,主线程该渲染渲染,模型在后台悄悄下,下完了再切到推理模式。还有个坑是后端选择,默认走 wasm 单线程,推理一张图要八百毫秒。我在初始化时加了段检测,发现浏览器支持 WebGL 就切到 WebGL 后端,速度直接掉到两百毫秒出头。这段检测代码不到二十行,效果立竿见影。

云端 API 我没彻底扔掉,留了个开关。模型置信度低于阈值的时候,前端把压缩过的图发到我的 FastAPI 服务,那边跑一个精度更高的模型做复核。这样网页版等于端侧快跑日常、云端兜底难例,用户体验和服务器成本之间找了个平衡。接口联调这块我吃过没定协议的亏,前端传的字段名和 FastAPI 里 Pydantic 模型对不上,一个叫 image_data 一个叫 file,调试了半天才发现。从那以后我强制自己先写 OpenAPI 描述,前后端都照着契约生成代码,这种低级错再没犯过。网页版跑稳之后我给自己定了个规矩,每次改推理层都要在低端安卓机的 Chrome 和一台老 iPad 的 Safari 上各测一遍,浏览器兼容这事不能靠猜。

3.3 手机部署:Android/iOS端侧推理与系统相机集成

手机端是我花时间最长的部分,也是用户真正会天天打开的地方。Android 我用 Kotlin 加 TFLite 写,相机走 CameraX。CameraX 的好处是帮你把不同厂商的相机差异兜住了,坏处是它给的默认输出格式是 YUV_420_888,TFLite 要吃 RGB 位图,中间得自己转。我头一版转格式的代码写得又长又慢,每帧转一次要二十多毫秒,相机预览都开始掉帧。后来改成用 RenderScript 的 intrinsic(现在叫 RuntimeShader 了),转换压到三毫秒以内,画面立刻顺了。

还有个细节差点把我坑哭——相机帧率是三十帧每秒,我一开始每帧都往模型里塞,结果手机热得能煎蛋,电量肉眼可见地往下掉。后来我加了帧抽稀,每五帧取一帧做推理,中间帧只做预览渲染。用户按快门那一刻再单独跑一次高分辨率推理,保证拍照结果准。这个改动之后机身温度从烫手降到温热,续航测试里连续用十分钟掉电从百分之八降到百分之三。手机端省电这事,真的是靠一帧一帧抠出来的。

iOS 我按第 2 章说的放到二期,但提前踩了踩坑。Core ML 的模型集成比我想的顺,Xcode 直接把 .mlmodel 拖进去就能用,AVFoundation 采集视频流也熟。麻烦的是权限和相册的选择器,iOS 的相册访问分“允许全部”和“允许选中的照片”,用户选后者的时候你得处理返回的是个受限的 PHAsset 集合。我写了个兼容层,不管用户给什么权限都先降级到系统图片选择器,能拿到图就推理,拿不到就提示去设置里开。安卓和 iOS 两头写下来,最大的感受是别指望一套代码通吃,两端能共用的只有业务逻辑和模型文件,界面和系统集成该分就分,硬凑一套跨平台方案在相机这种重系统能力的地方会反噬。

3.4 接口联调、权限管理与隐私合规

权限这块我在安卓上第一次被拒得莫名其妙。应用装好点开,相机黑屏,日志里一个 SecurityException 都没打,我查了半小时才发现是 Manifest 里漏了 <uses-permission android:name="android.permission.CAMERA"/>。补上之后又碰到 Android 13 的新规矩,读相册的权限拆成了 READ_MEDIA_IMAGES,旧代码直接失效。这类系统升级带来的 breaking change 每年都来一波,我的应对办法是把权限申请逻辑包成一个小工具类,每次 targetSdk 升级先跑一遍全流程,哪步炸了在哪步修,不等到用户投诉才动手。

隐私合规这件事是我被朋友一句话点醒的。他在我演示完应用之后问了一句,你这照片传不传服务器。我说传啊,难例要复核。他说那你隐私政策里写清楚了吗,用户知道自己的照片会被上传吗。我当时愣住,回去翻我的网页,只写了句“我们重视您的隐私”这种废话。后来我把数据处理方式掰开揉碎写明白:默认在本地推理,图片不出设备;开启云端复核时图片会传到服务器,处理完立即删除不留存,传输走 TLS。还加了个显眼的开关让用户自己决定要不要开云端增强,默认关。这个开关加完,我自己心里也踏实了,之前总有种偷偷摸摸的感觉。

接口联调里最容易埋雷的是文件大小和超时。网页端上传我限了五兆,结果有用户拿单反拍的图十几兆直接传失败,前端还没给友好提示,白屏。后来加了前端压缩,用 Canvas 把图缩到长边 1024 再上传,同时后端 FastAPI 那边设了十秒超时和明确的错误码。手机端我干脆不上传原图,只在云端复核时发压缩过的小图。这些琐碎的边界处理不写进代码里,上线之后就会以各种奇怪的用户反馈回来找你,而且每次找你的方式都不一样。

3.5 性能与体验优化:延迟、包体、功耗和离线能力

性能优化这块我排了个优先级,延迟第一,包体第二,功耗第三,离线能力作为加分项。延迟又拆成冷启动和单次推理两段看。冷启动最伤体验,用户点开图标到能拍照这段时间要是超过两秒,流失就开始明显。我把启动流程里跟拍照无关的初始化全挪到拍照界面之后再异步做,模型加载放后台线程,先让用户看到取景框。这招把冷启动从两秒二压到八百毫秒左右,体感上就是“点开就能用”了。

单次推理延迟我靠三样东西往下压:模型量化、输入分辨率、推理后端。前两样第 2 章交代过了,int8 量化加 192 分辨率,端侧单帧从三百毫秒降到九十毫秒上下。推理后端手机端用 TFLite 的 NNAPI 委托,网页端用 WebGL,能把计算甩给 GPU 或神经加速器,比纯 CPU 快出一大截。有个小细节我调了很久,就是拍照到结果出来之间加不加 loading 动画。测下来发现哪怕只有九十毫秒,给一个轻微的进度反馈也比什么都不给体验好,用户会觉得“它在认真干活”。体验这事有时候不是纯拼速度,是拼用户对等待的感知。

包体我控制在三十兆以内,主要靠模型瘦身和资源压缩。两个模型文件加起来不到七兆,剩下的全是依赖库和图标。图标我全换成 webp,散图全塞进 spritesheet,省下来的空间够我多塞一个兜底模型。功耗前面提过了,帧抽稀是关键,另外我在应用切到后台时立刻释放相机和推理引擎,回到前台再重建,这比让它们挂着省电得多。离线能力是我最后加的,也最让我有成就感——模型内置在安装包里,不联网也能拍照识别,联网只是用来做云端复核和模型更新。有次在地铁里没信号,我打开自己的应用拍了一朵不知道什么花,两秒出来结果,那一刻觉得前面这些折腾都值了。

回头看整个部署阶段,真正卡住我的从来不是算法,是相机兼容、权限、网络状况、发热这些看起来不酷的工程细节。这些东西没人写在论文里,可用户能感知到的质量全在里头。我把每个平台的踩坑记在一个叫 deploy-notes.md 的文件里,现在翻回去看,密密麻麻全是当时觉得过不去的坎,现在读起来反而有点亲切。

4. 测试、迭代与发布:让图像识别小应用稳定可用

4.1 功能测试、模型鲁棒性测试与真实场景验证

功能测试这块我一开始想得很简单,界面能点开、拍照能识别、结果能显示,齐活。上线前一周我让五个朋友帮忙试用,反馈回来差点把我心态搞崩。有人的安卓机拍完照直接闪退,有人的结果永远显示“识别中”,还有个哥们拿手机对着我家猫拍了十张,六张识别成狗。我盯着那堆反馈坐了一下午,才明白功能测试和真实场景之间的距离比我想的大得多。

后来我整理了一份测试清单,照着清单一条条过。功能层面从安装、首次启动、授权、拍照、选图、推理、结果展示、历史记录、分享,每个环节都列出正常路径和异常路径。异常路径里我塞了很多“恶心”用例:无网络、存储空间满、相机被其他应用占用、图片是纯黑或纯白、图片尺寸极端小或极端大。每条都手写测试步骤和预期结果,跑一遍差不多要两小时。烦是烦,可跑完三轮之后,闪退这类硬伤基本绝迹了。

模型鲁棒性测试是另一套打法。我专门凑了一个“对抗集”,里头有逆光的人脸、模糊的标签、反光的包装盒、旋转九十度的文字、被手指挡住一半的物体。拿这批图跑模型,把置信度低的、识别错的单独挑出来看,慢慢就摸清了模型的软肋。我家那只橘猫识别率低,原因是训练集里橘猫样本太少,这个发现直接影响了我后面补数据的策略。真实场景验证我放到最后做,选了个周末去超市、地铁站、公园各拍五十张图,回来统计识别准确率和用户操作路径。超市里的促销标签识别最差,反光加密集文字,模型基本在瞎猜。我把这批数据标注完补进训练集,重新训了一版,准确率从六成出头爬到八成五。

4.2 监控反馈、数据回流与模型迭代

监控这件事我吃过没做的亏。第一版发出去之后我只有用户主动点“反馈”按钮才能收到问题,实际上没人愿意为了一次识别错误去打字。应用跑了两周,反馈区空空如也,我还挺得意,以为没 bug。后来找了个做后端的朋友帮我看日志,他扫了一眼说你这崩溃率千分之三,失败请求两千多条,你管这叫没问题?我当场脸就红了。

回去之后我把监控拆成三块:崩溃上报、推理失败统计、低置信度采样。崩溃用 Sentry,嵌进去就几行代码,每次崩溃带堆栈和设备信息。推理失败统计我在端侧埋了个计数器,成功、失败、超时、取消各算各的,每天汇总一次上传。低置信度采样最有意思,模型置信度低于零点六的图片,在用户同意的前提下匿名上传到我的 FastAPI 服务,我每周筛一遍看看模型在哪些图上犯怵。这三块数据一上来,模型的问题立刻变得具体了,不是“感觉不准”,而是“周三周四的晚餐时段对火锅菜品识别率掉了十五个点”,排查方向清晰得很。

数据回流到模型迭代这条链路我跑通了半自动。每周一我把低置信度采样和用户主动反馈的图片拉下来,人工过一遍,挑出标注错的重新标,标完按八二比例并进训练集和验证集。然后用第 2 章那套训练脚本重新训一版,跑完评估看指标有没有涨,涨了就留,没涨就回滚。这个流程刚开始一次要花我大半天,后来脚本越写越顺,现在一个上午能走完。迭代节奏我从月度改成双周一次,模型更新的边际收益开始变缓,这个频率刚好。

4.3 发布渠道、版本管理与灰度策略

发布这事我第一回干得特别莽。安卓打一个签名包,直接丢到应用商店,网页端也是改完就 git push 自动部署。结果有次周五下午推了个更新,晚上回家发现相机权限的申请逻辑写错了,新用户装完直接卡在授权页。我半夜爬起来回滚,回滚完了坐在椅子上缓了十分钟,心想这要是用户量大点,我这周末就交代了。

安卓的发布渠道我后来分了两条线,一条是国内应用商店,一条是 Google Play。国内商店每家要的资质材料不一样,有的要软著,有的要备案号,我花了一个多星期才把材料凑齐。Google Play 那边相对省心,但审核对隐私政策的要求特别细,数据收集声明要跟实际行为完全对上,我改了三版才过。网页端发布简单得多,用 GitHub Actions 配了个流水线,推标签自动构建自动部署,回滚就是切回上一个标签,一分钟的事。

灰度策略是我从那次半夜回滚里学的。安卓包现在先放百分之五的用户,观察两天崩溃率和核心功能成功率,没问题再放百分之二十,最后全量。网页端按用户 ID 哈希分流,新版本先给一成流量,盯着错误率和推理耗时的曲线看,曲线平稳了再逐步放开。版本管理用语义化版本号,每次发布写清楚改了什么、修了哪些 bug、模型版本是多少。我还在应用里加了个隐藏的版本信息页,长按标题栏五下能调出来,用户报问题时我第一句就问这个页面上的版本号,省了大量猜来猜去的沟通。

4.4 成本、合规与可持续运营

成本这件事在我把推理挪到端侧之后压力小了很多,但没归零。服务器上跑着 FastAPI 那个小服务,管云端复核和统计接收,一台最低配的云主机加对象存储,一个月几十块钱。真正花钱的地方是模型训练,我本地机器跑小模型够用,偶尔要训大一点的版本就租按小时的 GPU 实例,训完立刻释放。域名和 SSL 证书一年百来块,CDN 流量因为网页端模型文件不大,每月十几块。我拿表格记了三个月的支出,平均下来一个月不到两百块,这个数字让我敢继续让应用免费跑着。

合规这块我踩过的坑比预想的多。隐私政策我按模板改了又改,把收集什么数据、用来干什么、存多久、传给谁,一条条写清楚。用户第一次打开应用会弹一个同意框,不同意也能用基础功能,只是云端复核和匿名统计不生效。欧盟用户那边我做了个地理检测,如果是欧盟 IP 就把数据收集默认关掉,等用户主动开。这个逻辑写起来不复杂,但把“默认保护”和“明示同意”这两条守住了,心里踏实。应用商店下架过一次,原因是隐私政策链接失效,我换了个静态托管之后把链接检查加进了发布清单,这种事不想再来第二次。

可持续运营靠的是把维护成本压到最低。我把所有自动化脚本放在一个仓库里,训练、评估、导出、发布各一个脚本,换台新电脑克隆下来装好依赖就能跑。文档写在 README 和 deploy-notes.md 里,重点是记录“为什么这么做”而不是“做了什么”,因为半年后的我大概率会忘了当初的决策理由。那个帮我试用应用的朋友后来问我要不要收费,我想了想说暂时不,用户量还小,收费会逼着我做客服和发票,这些事会挤掉我迭代模型的时间。等哪天月活过万了再琢磨商业化,现在先把产品本身磨好。

4.5 从Demo到产品的扩展路径

回头看这个项目,从第一版能拍照识别到现在稳定跑了几个月,中间隔着的不是某个技术大突破,是一堆琐碎决定的累积。Demo阶段我只关心“能不能识别对”,产品阶段我关心的是“识别不对的时候用户会不会生气”、“断网了还能不能用”、“手机烫不烫”。这些问题在写论文和做实验的时候没人问,用户打开应用三秒内就能感知到。

如果要往下走,我心里有三条扩展路径。第一条是横向扩品类,现在只认植物和菜品,接下来可以加宠物品种、商品条码、地标建筑,每加一类补一批数据训一版模型,架构上不用动,工作量在数据上。第二条是纵向做深,比如植物识别可以接着做养护建议、病害诊断,从单次识别变成持续服务,这条路对产品设计的要求高得多,我还在琢磨。第三条是平台化,把端侧推理这套框架抽出来做成模板,让想做类似小应用的人能直接套,这个想法目前只是记在备忘录里,等前两条路走顺了再说。

有件事我挺确定,就是别在没验证需求的时候铺大摊子。我见过不少项目一开始就想做大做全,功能列表列了三页,做出来发现没人用。我这个应用现在用户量不大,可留存曲线是健康的,每周都有人回来用,这比下载量更让我踏实。接下来半年我打算保持一个月一次功能更新、双周一次模型更新的节奏,重点补真实场景的难例数据,把准确率从八成五往九成推。推不上去也没关系,至少我知道每一分提升是怎么来的,这种确定感对一个独立开发的人来说比什么都重要。

测试、迭代、发布这三件事串起来,本质上是在回答一个问题:你怎么知道你做的东西在变好。我现在的答案是看数据、看反馈、看自己愿不愿意天天打开它用。三个答案都是肯定的时候,这版就可以发了。

赞0
踩0
☆收藏0
版权声明
文章版权声明:除非注明,否则均为ZBLOG原创文章,转载或复制请以超链接形式并注明出处。
分享到
chuanbook

链接已复制到剪贴板