计算机视觉实战:从被产品追着改需求,到搞定一个商品识别小模型

★邓文
2025-12-14 19:14
阅读 473

大家好,我是阿哲,在腾讯做客户端开发快三年了,主要搞微信小程序相关业务。说白了,就是天天和 wx.requestsetDataonLoad 这些 API 打交道,偶尔还得和产品经理“友好沟通”一下需求合理性。最近一年,我们组开始尝试把一些轻量级的 AI 能力集成进小程序——比如扫一扫识物、拍照搜同款之类的。这活儿本来该算法团队干,但你知道的,大厂里“全栈化”是种玄学,前端要懂点后端,客户端也得会点 CV(Computer Vision),不然开会连 PPT 都看不懂。

去年双11前,产品突然甩过来一个 PRD:“用户拍一张商品图,我们要在 2 秒内返回相似商品列表。” 我当时就懵了:我一个写 JS 的,咋还让我搞图像识别? 但领导一句“你不是喜欢研究底层原理嘛”,直接给我安排上了。行吧,为了 KPI,硬着头皮上。


为啥非得自己训模型?不能调现成 API 吗?

其实一开始我也想偷懒,直接调腾讯云的图像识别 API。但一测发现两个问题:

  1. 延迟太高:走公网 + 云端推理,平均 800ms+,加上网络抖动,偶尔飙到 2s+,用户体验直接崩。
  2. 成本爆炸:双11预估流量 500w 次/天,按次计费的话,财务看了报表怕是要报警。

于是技术方案很快定下来:前端采集图片 → 小程序上传 → 后端轻量模型本地推理 → 返回结果。重点在于“轻量”——模型必须小、快、准,最好能塞进 Docker 容器跑在边缘节点上。

这时候我才意识到:客户端开发的尽头,真的是算法工程化。


算法选型:MobileNetV3 vs EfficientNet-Lite,谁才是真·轻量王者?

我翻遍了 TensorFlow Lite 和 PyTorch Mobile 的文档,对比了几种主流轻量模型:

模型 参数量 (MB) 推理速度 (ms, CPU) Top-1 Acc (%) 是否支持量化
MobileNetV2 14.3 45 72.0
MobileNetV3-Small 5.7 28 67.4
EfficientNet-Lite0 4.9 32 74.4
ResNet-18 46.8 120+ 70.1 ❌(太大)

数据来自 ImageNet,实测环境:Intel Xeon E5-2686v4(我们测试机配置)。

一眼看去,EfficientNet-Lite0 准确率最高,但速度略逊于 MobileNetV3-Small。不过我们业务场景不是通用分类,而是商品细粒度识别(比如区分 iPhone 14 Pro 和 14 Pro Max),准确率优先级更高。

但!坑来了——EfficientNet-Lite0 在 TensorFlow Lite 上的量化支持有点拉胯,官方 demo 跑起来一堆 warning。而 MobileNetV3-Small 社区生态成熟,连微信小程序官方文档都拿它当例子。

最终拍板:先用 MobileNetV3-Small 快速验证,不行再切 EfficientNet。


数据集:别信公开数据集,真实业务全是脏数据

我天真地以为,下载个 ImageNet 子集或者 Open Images 就能开训。结果第一次 demo 给产品看,人家随手拍了个模糊反光的手机壳,模型输出 “apple fruit” —— 当时我真的想砸键盘。

现实教训:业务场景的数据分布,和公开数据集天差地别。

我们紧急启动了“土法炼钢”:

  • 内部爬取商城商品图(约 10w 张)
  • 发动全组同事用手机拍办公桌上的物品(水杯、键盘、工牌…)
  • 甚至去华强北蹲点拍山寨耳机(别问,问就是业务需要)

最后清洗出 8.2w 张高质量样本,覆盖 1200 个 SKU。标签体系按三级类目设计:电子产品 > 手机 > iPhone 14 Pro

开发心得 #1:算法再牛,没有对齐业务的数据,都是耍流氓。


训练 & 调优:那些让我凌晨三点还在调参的日子

环境搭在公司内部的 GPU 集群(感谢运维大哥没锁 quota)。代码基于 PyTorch Lightning,主要流程如下:

# train.py 核心片段
from torchvision.models import mobilenet_v3_small
import torch.nn as nn

class ProductClassifier(nn.Module):
    def __init__(self, num_classes=1200):
        super().__init__()
        self.backbone = mobilenet_v3_small(pretrained=True)
        # 替换最后分类层
        self.backbone.classifier[3] = nn.Linear(
            self.backbone.classifier[3].in_features, 
            num_classes
        )
    
    def forward(self, x):
        return self.backbone(x)

# 关键训练技巧
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)  # 防过拟合神器

踩过的坑:

  1. 过拟合严重:训练集 acc 95%,验证集只有 68%。
    → 解法:加 RandomErasing + CutMix 数据增强,效果立竿见影。

  2. 类别不平衡:iPhone 样本 5000 张,某冷门数据线只有 20 张。
    → 解法:采用 ClassBalanced Loss,给稀有类更高权重。

  3. 量化掉点:FP32 模型 76.2% → INT8 量化后 71.5%。
    → 解法:量化感知训练(QAT)!在训练后期插入 fake quant 节点,让模型提前适应量化噪声。

# QAT 关键代码(PyTorch)
model.train()
model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm')
torch.quantization.prepare_qat(model, inplace=True)

for epoch in range(10):  # 只需微调几轮
    train_one_epoch(model, ...)
torch.quantization.convert(model, inplace=True)

开发心得 #2:调参不是玄学,是科学试错。每次实验必须记录 seed、lr、augment policy,否则回头根本不知道哪步有效。


部署上线:从 Docker 到小程序,链路打通有多难?

模型训好了,.pt 文件 15MB,转成 TensorFlow Lite 后只有 3.2MB。部署流程:

小程序拍照 → 压缩至 224x224 → 上传 base64 → Flask API → TFLite 推理 → 返回 top5 商品ID

Flask 服务用 Gunicorn + Gevent 跑,单核 CPU 下 QPS 约 80。压测时发现内存泄漏——原来是每次推理都新建 interpreter 实例。改成全局单例后稳如老狗:

# inference.py
import tensorflow as tf

# 全局加载,避免重复初始化
_interpreter = tf.lite.Interpreter(model_path="model.tflite")
_interpreter.allocate_tensors()

def predict(image_tensor):
    input_details = _interpreter.get_input_details()
    output_details = _interpreter.get_output_details()
    
    _interpreter.set_tensor(input_details[0]['index'], image_tensor)
    _interpreter.invoke()
    output = _interpreter.get_tensor(output_details[0]['index'])
    return output

上线前夜,测试同学报了个诡异 bug:某些安卓机返回全是 NaN
查了一晚上,发现是 TFLite 版本兼容性问题——低端机用的旧版 runtime 不支持某些 ops。最终解决方案:在转换模型时加 --target_ops=TFLITE_BUILTINS 强制兼容。

开发心得 #3:线上环境永远比你想象的复杂。测试机 ≠ 用户机,尤其在国内千元机生态下。


效果 & 复盘:准确率 82.3%,但产品又提新需求了…

最终线上指标(灰度 10% 流量):

指标 目标 实际
端到端延迟 ≤2000ms 1420ms
识别准确率(Top-1) ≥75% 82.3%
服务器 CPU 峰值 ≤60% 48%

老板还算满意,但产品周五下午又来找我:“能不能加个‘以图搜视频’功能?” ……我默默打开了 Boss 直聘。


面试题挑战:如果你面试被问“如何优化移动端 CV 模型”?

现在回想,这段经历完美踩中了大厂常考的几个点。如果面试官问我:

“你们怎么保证模型在低端机上跑得快?”

我会这么答:

  1. 模型层面:选 MobileNet/EfficientNet 等轻量架构,用 depthwise conv 减少计算量;
  2. 训练层面:通过知识蒸馏(teacher-student)或神经架构搜索(NAS)进一步压缩;
  3. 部署层面:TensorFlow Lite / Core ML 量化(INT8)、算子融合、CPU 绑核;
  4. 工程层面:图片预处理在前端做(小程序 canvas resize),减少传输体积;

关键思维:不要只谈算法,要讲清楚端到端链路优化。


最后的碎碎念

说实话,搞完这个项目,我对“客户端开发”的认知彻底刷新了。以前觉得写好 UI 逻辑、处理好兼容性就 OK,现在发现:现代 App 开发,本质是“体验 × 性能 × 智能”的三角平衡

虽然过程中被 Bug 折磨到秃头,但看到用户真的用“拍照识物”找到心仪商品时,那种成就感……嗯,大概能抵消三次和产品经理吵架的血压升高吧。

如果你也在被“顺便加个 AI 功能”折磨,别慌。记住:所有复杂的系统,都是从一行 import torch 开始的。

对了,最近在刷 LeetCode 的“图像处理”专题,准备跳槽面试。要是你也想交流 CV 实战 or 小程序性能优化,欢迎评论区唠嗑!(或者私信我吐槽产品经理,我超会的)


附:工具链清单(亲测可用)

  • 数据标注:LabelImg + 内部自研半自动打标平台
  • 训练框架:PyTorch 1.12 + Lightning
  • 模型转换:torch.onnx.exporttf.import_graph_def → TFLite Converter
  • 压测工具:locust + 自定义 image payload generator
  • 监控:Prometheus + Grafana(盯死 CPU/内存/QPS)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝