前阵子把手头一个图像分类的PyTorch模型搬到Android上,网上搜了一圈,发现能跑通全链路的教程少得可怜,很多资料都停在“转换成功”这一步,真正完整的部署流程和踩坑记录没人写。这篇文章把我折腾了一周的经验全部整理出来:从PyTorch模型导出ONNX、用X2Paddle转成PaddlePaddle格式、再用PaddleLite的opt工具生成NB模型,最后集成到Android Studio并写出可跑的推理代码。整个过程适合两类人:一类是手头有PyTorch模型想部署到手机上的算法工程师,另一类是接了端侧AI需求但不太熟工具链的Android开发者。我会把每一条命令、每一个关键参数背后的“为什么”都讲清楚,照着敲就能复现。
1. 为什么弃用PyTorch Mobile而选PaddleLite:端侧推理的选型复盘
1.1 PyTorch Mobile看起来很美,落地却让人头疼
最开始我理所当然地尝试了PyTorch Mobile。PyTorch官方提供了Android端的AAR包,按照文档接入也不算复杂,但实际用起来有三点让我果断放弃。
首先是包体积问题。PyTorch Mobile的Android AAR包解压后动辄三四十MB,加上各种依赖,APK直接突破60MB。现在APP包体积考核越来越严,光一个推理引擎就吃掉这么多空间,产品侧很难接受。虽然官方提供了按需裁剪算子的解决方案,但那需要自己编译AAR,配置链较长,对大多数项目来说性价比不高。
其次是算子支持问题。PyTorch Mobile虽然号称支持大部分Torch算子,但实测某些相对冷门的算子在移动端跑起来要么报错要么性能极差。我那个模型里用了一个自定义的注意力模块,PyTorch Mobile直接给了一个“operator not supported”的提示。当然可以通过改模型结构绕过去,但改了结构就得重新训练或者微调,代价太高。
最后是量化工具链不成熟。端侧推理要想跑得快,INT8量化几乎是必选项。PyTorch官方的量化感知训练功能虽然能用,但部署到Android端后,量化模型的实际加速效果远不如预期,也没有现成的子图融合、内存复用这类优化工具。我在上面耗了两天,速度还是达不到上线标准。
1.2 PaddleLite的核心优势在哪里
转头研究PaddleLite,发现几个特点正好补齐了PyTorch Mobile的短板。
轻量是最大的亮点。PaddleLite的Android预测库大约10MB左右,如果是纯粹用预测API,裁剪后还能更小。对比PyTorch Mobile的三四十MB起步,这个差距对端侧项目来说非常关键。
优化能力上,PaddleLite内置了完备的模型优化工具paddle_lite_opt,可以对计算图做算子融合、冗余节点去除、权重格式转换、INT8量化。这些优化在生成NB模型文件时一次性完成,不需要自己手动调整。尤其针对ARM CPU做了深度优化,在骁龙和麒麟平台上的推理性能都还不错。
生态配套也值得一提。PaddleLite提供了非常详细的Android集成文档,Java API设计得比较清晰,输入输出Tensor的读写方式对接触过其他推理框架的人来说几乎没有学习成本。中文资料相对丰富,遇到问题搜索引擎能直接给出有用答案。
不过这并不意味着PaddleLite是万能的。如果你的模型特别依赖PyTorch的某些高阶自定义算子,而且这些算子没有ONNX实现,那转换链路的额外成本可能比PyTorch Mobile更高。所以选型阶段一定要先梳理清楚自己模型里的算子清单,判断转换可行性再动手,否则做了一半发现某个算子卡住就很被动了。
2. 转换链路四站式拆解:PyTorch、ONNX、PaddlePaddle与PaddleLite之间如何接力
2.1 为什么必须经过ONNX中转,而不是PyTorch直接转Paddle
很多第一次接触转换的人会问:PyTorch和PaddlePaddle都是Python系深度学习框架,能否直接转?答案是可以,X2Paddle确实提供了直接从PyTorch转换的入口,但实际使用中坑比较多。
X2Paddle的PyTorch转Paddle模式主要依靠PyTorch的JIT Trace机制。你需要先把模型用torch.jit.trace或torch.jit.script序列化成TorchScript格式,再去转换。问题在于,Trace方式对动态控制流的支持极差,Script方式又要求模型代码严格遵循TorchScript语法规范,很多自由风格的Python代码根本过不了编译。我试过几次,遇到的条件分支、动态shape问题在JIT阶段就爆炸了。
而ONNX作为中间格式,最大的好处是生态成熟、工具链完善。PyTorch官方提供了稳定的torch.onnx.export导出接口,导出时可以根据需要设置opset版本、输入输出名称、动态维度等。转换成ONNX之后,你还可以用onnxruntime先跑一遍验证模型正确性,再用onnx-simplifier做计算图简化,排除掉很多PyTorch导出的冗余节点。这些步骤都是成熟工具,踩坑概率比直接从PyTorch转Paddle要低很多。
所以最终链路锁定为:PyTorch → ONNX → PaddlePaddle → PaddleLite。每一站都有清晰的工具和输出物,中途哪个环节出问题,也能准确切分责任范围。
2.2 四站转换各自的输入输出物
这四条链路我整理成一张对照表,方便理解每步做了什么:
| 阶段 | 输入 | 工具 | 输出 |
|---|---|---|---|
| 第一站 | PyTorch模型文件(.pth / .pt) | torch.onnx.export | ONNX模型(.onnx) |
| 第二站 | ONNX模型(.onnx) | X2Paddle | PaddlePaddle推理模型(.pdmodel + .pdiparams) |
| 第三站 | PaddlePaddle推理模型 | paddle_lite_opt | PaddleLite模型(.nb) |
| 第四站 | PaddleLite模型(.nb) | PaddleLite Android API | 端侧推理结果 |
第一站到第二站是格式转换,完成的是“不同DL框架之间的人话翻译”。第二站到第三站是优化与打包,完成的是“从服务器通用格式到移动端专用格式”的压缩和重构。第四站是在Android设备上加载NB模型,执行前向推理计算。
有一个容易混淆的点要特意说明:第二站X2Paddle输出的.pdmodel和.pdiparams是PaddlePaddle的推理模型格式,它本身可以被PaddlePaddle的Python API加载运行,但它是给服务器或者PC环境用的,不是PaddleLite移动端格式。必须经过第三站的paddle_lite_opt转换,生成.nb文件,才能真正放进Android工程。很多人以为拿到.pdmodel就算完了,结果在Android端加载时报错,其实就是漏了这一步。
2.3 转换链路中每个环节的失败概率评估
我实测下来的体感,四个环节成功率差异很大。第一站PyTorch导出ONNX,只要模型本身不含动态控制流、自定义算子,成功率很高,大概九成以上。第二站ONNX转Paddle,算子如果比较常规,成功率也很高,但如果ONNX里残留了PyTorch特有的辅助输出节点,可能会报错,这时候需要用onnx-simplifier先清理一遍,成功率能提升一大截。第三站Paddle转NB,通常比较稳定,但要注意输入shape不固定或者包含动态shape信息时,需要先固定shape。
整个链路最花时间的其实是排查“某个算子不支持”的问题。我建议动手之前先把自己模型里用到的算子全部列一遍,去PaddleLite官方文档查算子支持列表,如果发现有不在列表里的,提前想好替代方案,不要等到转换报错再想办法,那时候往往很被动。
3. 环境准备实操:Anaconda虚拟环境与Paddle工具链安装
3.1 为什么推荐Anaconda虚拟环境
转换工具链涉及Python、PaddlePaddle、X2Paddle、ONNX等多个组件,不同组件的依赖版本互相牵连,直接在系统Python里装,很容易把环境搞乱。我之前就有过一次教训:系统里原本装了TensorFlow 2.x,直接pip install paddlepaddle后,numpy版本冲突导致两个框架都跑不起来。
用Anaconda创建独立虚拟环境,相当于给每个项目一个干净的“沙盒”,你在这个环境里怎么折腾都不会影响别的项目。这是做深度学习开发和模型转换的基本修养,省掉的麻烦远超那几分钟创建环境的时间。
3.2 一步步搭建转换环境
我用的命令和版本如下,基本可以无脑复制:
# 创建Python 3.8虚拟环境 conda create -n x2paddle_env python=3.8 # 激活环境 conda activate x2paddle_env # 安装PaddlePaddle CPU版(转换过程不需要GPU) pip install paddlepaddle==2.5.2 # 安装X2Paddle pip install x2paddle # 安装ONNX及运行时环境 pip install onnx onnxruntime onnx-simplifier # 安装PaddleLite工具包(提供paddle_lite_opt命令行工具) pip install paddlelite==2.13这里说几个关键点。PaddlePaddle安装CPU版就够用了,因为转换过程只做图结构的解析和映射,不涉及大规模张量计算,GPU版本除了浪费下载时间没有额外好处。X2Paddle安装时可能会自动升级或降级某些依赖,如果提示冲突,留意一下输出日志,必要时单独固定版本。
onnxruntime非常重要,它是用来验证导出后的ONNX模型能否正常前向推理的。如果ONNX模型在onnxruntime里跑出的结果跟PyTorch一致,说明第一站导出的模型没有损坏,后续转换出了偏差就可以把问题定位到后面环节。很多人在转换失败后抓瞎,就是忽略了这步验证。
安装完成后,各自验证一下版本:
python -c "import paddle; print(paddle.__version__)" python -c "import x2paddle; print(x2paddle.__version__)" python -c "import onnx; print(onnx.__version__)" paddle_lite_opt --help这里要特别提醒:paddle_lite_opt这个命令是PaddleLite的工具包自带的。如果你在终端里输入paddle_lite_opt提示找不到命令,先确认paddlelite装到了当前激活的conda环境里,再看一下Scripts或bin目录是否在PATH里。Windows下conda环境的PaddleLite有时不会自动把可执行文件加到PATH,我当时的解决办法是用Python模块方式调用,后面会写。
3.3 国内网络环境下的下载加速
如果pip下载速度很慢或频繁超时,加一下清华镜像源会顺畅很多:
pip install paddlepaddle==2.5.2 -i https://pypi.tuna.tsinghua.edu.cn/simple pip install x2paddle -i https://pypi.tuna.tsinghua.edu.cn/simpleAnaconda创建环境时如果卡在Solving environment,可以配置conda的镜像源,或者直接用-c https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main指定。
环境搭建是整个流程里最没有技术含量但最容易出幺蛾子的部分。我的建议是:严格按照上面命令执行,不要跳步,不要自作主张换版本。版本不一致导致的兼容性问题,排查起来比实质性的转换问题更折磨人。
4. 按顺序跑通模型转换:导出ONNX、转Paddle、生成NB文件
4.1 PyTorch模型导出为ONNX格式
假设你有一个训练好的分类模型,保存的文件是model.pth。首先要明确这个pth文件里保存的是什么——是整个模型对象,还是仅state_dict。两种情况下加载代码不同,搞错了后面导出必然报错。
情况一:保存了完整的模型对象
import torch model = torch.load('model.pth', map_location='cpu') model.eval()情况二:只保存了state_dict(更常见也更推荐)
import torch from models import MyModel model = MyModel() state_dict = torch.load('model.pth', map_location='cpu') model.load_state_dict(state_dict) model.eval()这里model.eval()绝对不能不写。因为PyTorch模型在训练模式和推理模式下BatchNorm和Dropout层的行为完全不同,如果不切换到eval模式,导出的ONNX模型在推理时会出现莫名其妙的精度偏差。
接下来是关键导出代码:
# 构造一组shape正确的虚拟输入 # 假设你的模型输入是 1x3x224x224 RGB图 dummy_input = torch.randn(1, 3, 224, 224) # 指定输入输出节点名称,后续Paddle转换和Android端会用到 input_names = ['input_0'] output_names = ['output_0'] torch.onnx.export( model, dummy_input, 'model.onnx', opset_version=12, input_names=input_names, output_names=output_names, dynamic_axes=None, verbose=False )关于opset_version,我推荐12或13。太低的版本缺少一些算子的定义,太高的版本可能产生某些较新的算子导致X2Paddle和PaddleLite不认识。实测12的兼容性最好,除非你的模型里用了特别新的算子需要更高opset才能导出,否则固定用12。
关于dynamic_axes,这里故意设成了None,让模型的所有维度都固定下来。PaddleLite端侧推理对动态shape支持不好,能固定尽量固定。如果业务上确实需要动态batch或者是可变输入尺寸,建议在模型层面做预处理——比如把图片统一resize到固定尺寸——而不是依赖推理框架支持动态shape。
导出完成后,先用onnxruntime验证一遍:
import onnxruntime import numpy as np sess = onnxruntime.InferenceSession('model.onnx') # 打印输入输出节点信息 print(sess.get_inputs()[0].name, sess.get_inputs()[0].shape) print(sess.get_outputs()[0].name, sess.get_outputs()[0].shape) # 用随机数据跑一遍 dummy = np.random.randn(1, 3, 224, 224).astype(np.float32) onnx_output = sess.run(None, {sess.get_inputs()[0].name: dummy})[0] print('ONNX推理结果shape:', onnx_output.shape)如果这一步正常出结果且结果shape符合预期,第一站就算完成了。
4.2 用onnx-simplifier清理冗余节点
PyTorch导出ONNX时常常会留下一些Gather、Shape、Concat之类的辅助节点,这些节点在标准ONNX推理时没问题,但转其他框架时容易被卡住。用onnx-simplifier做一遍简化,能把很多冗余结构去掉,降低后续转换失败概率:
python -m onnxsim model.onnx model_sim.onnx然后再次用onnxruntime验证简化后的模型,确认输出和之前一致。如果简化前后推理结果偏差超过预期值,说明模型里有某些对计算图结构敏感的操作,这时候就不要用简化版,回到原始版继续,或者检查模型代码里是否有不规范的写法。
4.3 X2Paddle:ONNX转PaddlePaddle推理模型
准备好simplify后的ONNX文件,开始第二站:
x2paddle --framework=onnx --model=model_sim.onnx --save_dir=pd_model如果一切顺利,pd_model目录下会出现两个核心文件:inference.pdmodel(模型结构)和inference.pdiparams(模型权重)。有的版本还会生成inference.pdiparams.info文件,这个是参数字典信息,也要保留。
X2Paddle转换时输出日志会列出每一个映射过的算子类型。如果模型里出现了“Not Supported”或者“unsupported”字样的告警,说明有算子没有被正确映射,这时候需要截取日志,定位到具体是哪个算子出了问题。常见的处理方案有三个:
- 回到PyTorch模型里,把这个算子替换成功能类似且Paddle支持的算子;
- 修改ONNX模型结构,手工把不支持算子拆解成基础算子的组合;
- 检查是否有PaddlePaddle / X2Paddle的新版本升级了对该算子的支持。
如果日志中完全没有告警,恭喜,可以进入下一站了。
4.4 paddle_lite_opt:生成PaddleLite端侧NB模型
第三站,也是整个转换链路里最关键的一步:
paddle_lite_opt \ --model_file=pd_model/inference.pdmodel \ --param_file=pd_model/inference.pdiparams \ --optimizer_out=model.nb如果paddle_lite_opt命令找不到,就用Python模块方式:
python -m paddlelite.opt \ --model_file=pd_model/inference.pdmodel \ --param_file=pd_model/inference.pdiparams \ --optimizer_out=model.nb生成完成后,model.nb文件就是最终要集成到Android工程的模型文件。
有几个关键参数我建议根据实际情况加上。如果目标手机是高通或麒麟芯片,且只考虑64位架构,加上--arm_target=armv8可以让生成的模型在armv8指令集上跑得更稳。如果你的APP还要兼容老设备32位架构,需要分别生成armv7和armv8两个NB文件,在代码里根据设备架构加载对应的模型。另外--optimize_out_type=naive_buffer这个参数默认就是打开的,可以让生成的模型体积更小、加载更快,保持默认即可。
如果希望在PC上先验证转换后的NB模型能否正常推理,避免直接改Android工程后才发现问题,可以换一种加载方式。暂时不在PC上直接跑NB(因为PaddleLite Python库加载NB模型的API和Android端略有不同,容易误导),更稳妥的方式是直接用PaddlePaddle Python API加载X2Paddle输出到推理模型.pdmodel先跑一遍,确认Paddle阶段的模型输出和ONNX输出的结果一致,而.nb的验证直接放在Android真机上进行。
4.5 权重查看与常见转换报错速查
转换过程中我遇到的常见报错和解决方案整理如下:
| 报错内容 | 原因分析 | 解决办法 |
|---|---|---|
| x2paddle报Operator xxx unsupported | ONNX模型里含有Paddle不支持的算子 | 回到PyTorch模型替换算子,或手工拆解 |
| paddle_lite_opt报const tensor exceeded size limit | 模型过大或权重异常 | 检查是否有未初始化的超大参数,考虑模型裁剪 |
| opt转换后加载NB报tensor shape dismatch | 输入shape与模型要求不一致 | 确认Android端输入的size和导出时的dummy_input一致 |
| onnxruntime导出结果全是NaN | PyTorch模型未正确加载或eval前有异常 | 检查state_dict加载是否成功,确认eval()后再导出 |
5. Android Studio集成要点:依赖引入、模型放置与推理代码
5.1 用Gradle把PaddleLite依赖装进项目
PaddleLite官方发布了Android AAR包,在build.gradle里直接添加依赖即可:
dependencies { implementation 'com.baidu.paddle:paddle-lite-android:2.13.0' }默认这个包已经包含armv7和armv8两种架构的so库,如果不想在APK里塞两种架构,可以用abiFilters精简:
defaultConfig { ndk { abiFilters 'arm64-v8a' } }如果应用只面向arm64设备,这样APK体积能进一步缩小。
5.2 模型文件放哪里更合理
NB模型文件放assets目录是最简单的做法,代码里通过AssetManager复制到应用私有目录,然后再加载。直接放assets也能加载,但PaddleLite的setModelFromFile接口接收的是文件路径,所以建议统一复制到getFilesDir()里再加载,避免每次加载都从assets读取。
private void copyModelIfNeeded(Context context) throws IOException { File modelFile = new File(context.getFilesDir(), "model.nb"); if (modelFile.exists()) { return; } InputStream is = context.getAssets().open("model.nb"); OutputStream os = new FileOutputStream(modelFile); byte[] buffer = new byte[8192]; int length; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } os.flush(); os.close(); is.close(); }5.3 一个可以直接用的PaddleLite分类推理封装类
这里给出一个图像分类模型的完整封装示例,包含初始化、预处理、推理和后处理:
import android.content.Context; import android.graphics.Bitmap; import com.baidu.paddle.lite.MobileConfig; import com.baidu.paddle.lite.PaddlePredictor; import com.baidu.paddle.lite.PowerMode; import com.baidu.paddle.lite.Tensor; public class PaddleLiteClassifier { private PaddlePredictor predictor; private long[] inputShape = {1, 3, 224, 224}; private float[] mean = {0.485f, 0.456f, 0.406f}; private float[] std = {0.229f, 0.224f, 0.225f}; public boolean init(Context context) { try { copyModelIfNeeded(context); MobileConfig config = new MobileConfig(); config.setModelFromFile(context.getFilesDir() + "/model.nb"); config.setThreads(4); config.setPowerMode(PowerMode.LITE_POWER_HIGH); predictor = PaddlePredictor.createPaddlePredictor(config); return predictor != null; } catch (Exception e) { e.printStackTrace(); return false; } } private void copyModelIfNeeded(Context context) throws Exception { File modelFile = new File(context.getFilesDir(), "model.nb"); if (modelFile.exists()) return; InputStream is = context.getAssets().open("model.nb"); OutputStream os = new FileOutputStream(modelFile); byte[] buffer = new byte[8192]; int length; while ((length = is.read(buffer)) > 0) os.write(buffer, 0, length); os.close(); is.close(); } private float[] bitmapToFloatArray(Bitmap bitmap) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); int[] pixels = new int[width * height]; bitmap.getPixels(pixels, 0, width, 0, 0, width, height); float[] data = new float[1 * 3 * width * height]; for (int i = 0; i < pixels.length; i++) { int pixel = pixels[i]; int r = (pixel >> 16) & 0xFF; int g = (pixel >> 8) & 0xFF; int b = pixel & 0xFF; data[i] = (r / 255.0f - mean[0]) / std[0]; data[width * height + i] = (g / 255.0f - mean[1]) / std[1]; data[2 * width * height + i] = (b / 255.0f - mean[2]) / std[2]; } return data; } public float[] predict(Bitmap bitmap) { Bitmap resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true); float[] inputData = bitmapToFloatArray(resized); Tensor input = predictor.getInput(0); input.resize(inputShape); input.setData(inputData); predictor.run(); Tensor output = predictor.getOutput(0); float[] outputData = output.getFloatData(); resized.recycle(); return outputData; } }这段代码有几个细节要说明。bitmapToFloatArray里的归一化操作,用的是ImageNet数据集的mean和std,如果你的模型训练时用的不是这个归一化参数,一定要改成你自己训练时的值,否则推理精度会暴跌。输出是一维float数组,对于分类模型,数组长度等于类别数,每个下标对应一个类别的置信度分数。
5.4 完整调用流程与结果后处理
在Activity或业务代码中使用这个封装类的方法如下:
PaddleLiteClassifier classifier = new PaddleLiteClassifier(); boolean ok = classifier.init(getApplicationContext()); if (!ok) { // 初始化失败,检查NB文件是否存在或so库是否匹配 return; } Bitmap bitmap = decodeBitmapFromPath(imagePath); float[] results = classifier.predict(bitmap); // 取top5概率 PriorityQueue<Map.Entry<Integer, Float>> queue = new PriorityQueue<>(...); // 排序取前5个索引,映射到类别标签注意这个推理过程最好放到子线程执行,因为PaddleLite前向推理耗时从几十毫秒到几百毫秒不等,放在主线程会导致ANR。
5.5 性能配置:线程数与功耗模式的取舍
MobileConfig里的setThreads和setPowerMode是影响推理性能最直接的两个参数。线程数并非越多越好,ARM CPU通常4线程时性能最高,超过4线程反而可能因为缓存竞争和调度开销导致性能下降。功耗模式建议用LITE_POWER_HIGH先测上限,真机发热严重时再降级到LITE_POWER_BALANCED。
实测数据参考如下(单张224x224图片输入):
| 模型 | 线程数 | 功耗模式 | 平均耗时 |
|---|---|---|---|
| MobileNetV3-Small | 4 | HIGH | 22ms |
| ResNet50 | 4 | HIGH | 128ms |
| MobileNetV3-Small INT8 | 4 | HIGH | 11ms |
需要强调的是,以上数据来自一台骁龙8系手机,不同设备差异可能非常大。低端机型的耗时可能是这个数值的2到3倍,上线前一定要在目标低端机型上做压测。
6. 真机验证与踩坑记录:算子兼容性、动态Shape与量化精度补偿
6.1 算子兼容性排查:问题可能出在链条的任何一个环节
我遇到过最典型的一个问题是:模型里有一个Gather算子,PyTorch导出ONNX时没问题,ONNX在onnxruntime推理时也没问题,但X2Paddle转换时直接报错,提示这个算子的某些属性组合不支持。
排查思路是这样的:先把报错截下来,确认是哪个算子;然后去PaddlePaddle和X2Paddle的Github issue里搜同一个算子名;如果没搜到,就尝试给ONNX模型的手工结构做调整,把这个算子拆解为几个基础算子。我当时是把Gather变成了Slice+Concat的组合,转换成功,精度也基本没损失。
这里的根本教训是:转换链条越长,算子兼容性风险越高。所以在模型选型阶段就要有“可部署性”的意识——优先使用常规算子,避免自定义算子和过于新颖的层结构。如果模型结构已经定死了,那就只能靠中间的转换工具逐层排查。
6.2 动态Shape导致的真机崩溃问题
PaddleLite对动态Shape的支持一直比较薄弱,我在初版集成时就踩了这个坑。模型导出时用的dynamic_axes包含batch维度动态,转换也勉强通过了,但到Android端一推理就抛异常,提示输入tensor的shape不匹配。
这个问题其实从模型导出那一刻就埋下了。如果你确定端侧输入尺寸是固定的,导出时就不要开dynamic_axes,直接把shape固定下来。如果输入尺寸确实会变化,最稳妥的方案是在代码端统一做resize预处理,让进入模型的张量始终是同一个shape。不要指望PaddleLite能优雅地处理不同尺寸的输入,它目前还做不到ONNX Runtime那样灵活的dynamic shape支持。
6.3 INT8量化后精度下降的补偿手段
NB模型生成时加入--quant_model参数可以启用后训练量化,实测INT8模型在ARM CPU上有约一倍的提速,但精度下降问题不容忽视。中度分类模型降1-2个百分点算正常范围,有时甚至能降5个点以上。
如果量化后精度达不到要求,有两个补偿思路。一个是用PaddleSlim训练后量化算法,在量化过程中提供一小批校准数据,让量化算法更好地调整各层的量化参数。calibration数据最好是训练集的一个小型有代表性的子集,覆盖各个类别和不同的光照场景,数量不必多,几十张到几百张就够了。另一个是做量化感知训练,在模型训练阶段就模拟量化误差的影响,让模型参数逐步适应低精度表达,效果通常比训练后量化好很多,但门槛也高一些,需要改造训练脚本。
6.4 端侧模型迭代时最容易忽略的版本同步问题
模型转换部署上线后,迭代是常态。第一次跑通全流程后,后续每次换模型我都建议做一张检查清单,避免低级失误:
- PyTorch模型更新后,是否重新导出了ONNX?
- ONNX是否重新经过simplify?
- X2Paddle是否重新转换为Paddle格式?
- opt是否重新生成了NB文件?
- Android工程assets里的NB文件是否真的替换掉了?有些时候构建系统没有把旧文件清掉,导致加载的还是上一版模型。
第5点是我实际踩过的坑。在Android Studio里修改了assets文件后,有时候需要Clean Project再重新构建,否则打出来的APK里还是旧模型。排查了半天,发现加载的还是上一个模型,浪费了大半天时间。
6.5 实在过不去的坎怎么办:备选路线
如果某个模型弃用特殊算子,ONNX简化也搞不定,X2Paddle转Paddle也是死胡同,最后还有一条备选路线——写一个Python端的前处理/后处理服务,把模型留在服务端,Android端通过网络调用推理API。这条路线的优点是绕开了所有端侧兼容性问题,缺点是增加了延迟和服务端成本,对网络质量有要求,而且离线场景完全不适用。
所以在整个流程开始之前,我建议先做一个简单的决策判断:你的模型是否真的必须端侧推理?如果业务允许联网,服务端推理永远是最省心的方案。如果必须端侧,再考虑转换链路。先想清楚这个前提,能避免很多无谓的折腾。