1. 从训练到真机:Paddle-Lite 安卓端部署到底难在哪
Paddle-Lite 是飞桨推出的端侧推理引擎,能做什么?简单说,它把训练好的 PaddlePaddle 模型压缩、优化,然后塞进手机里跑,不依赖网络、不调用云端接口。适合谁?做移动端 AI 应用的 Android 开发者、想把检测或分类模型落地到 App 的算法同学,以及像我这样不太熟 Android 但必须把模型跑通的人。
我最初以为部署就是“导出模型 → 拷进 App → 运行”,结果卡了整整两天。第一个坑是模型格式:PaddleDetection 训练出来的是__model__+__params__两个文件,而 Paddle-Lite 在移动端只认.nb格式的优化模型。第二个坑是输入输出对不上:官方 Demo 用的是 SSD 模型,输入 300×300、输出相对坐标;我换成了 YOLOv3 的 320×320,输出是绝对坐标,预处理和后处理全得改。第三个坑是 NDK 编译,ABI 选多了包体积爆炸,选少了真机直接UnsatisfiedLinkError。
这篇文章就按我踩坑的顺序,把 Paddle-Lite 安卓端部署拆成可复制的步骤:模型转换用opt工具、NDK 编译用 CMake 配置、JNI 调用改 Java 层代码、最后真机验证。每一步都给命令和参数,你跟着做就能在自有 App 里完成一次端侧推理闭环。
核心检索词先明确:Paddle-Lite 安卓端部署,指的是把 PaddlePaddle 模型通过 opt 工具转成 naive_buffer 格式,再用 Paddle-Lite 预编译库或自编译库,在 Android 工程里通过 JNI 调用完成推理。整个链路涉及模型转换、库文件替换、CMake 配置、Java 预处理/后处理四层。
我试过直接拿官方 object_detection_demo 替换模型,不改代码,结果 App 不崩溃但框全错位。原因后面会细说。先把环境理清楚:PaddleDetection 用 0.2 分支配 PaddlePaddle 1.7,Paddle-Lite 用 release 预编译库,Android Studio 用 4.0 以上,NDK 用 r21。版本不匹配是 401 之外最常见的报错来源。
2. TaoToken 前置:模型转换与 API 调试的 Key 准备
Paddle-Lite 本身是本地推理,不需要联网。但在实际工程里,你往往需要两件事:一是用云端大模型辅助生成或检查 JNI 代码、CMake 配置;二是把模型转换、编译过程中的日志丢给模型做排障。这时候需要一个稳定的 API 入口。
TaoToken 的定位是模型调用与 Coding Plan 管理平台,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 请求地址统一用 https://taotoken.net/api ,注意这个地址不加 UTM 参数,直接作为 Base URL 使用。
你需要先拿到 API Key。操作路径:登录后进入控制台,找到 API Keys 页面,新建一个 Key 并复制。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Keys 页面是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。Key 只显示一次,复制后存到本地环境变量里,别硬编码进 Android 工程。
如果你只是想在浏览器里先验证模型能不能正常对话,可以用模型对话页面: https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。长期做端侧部署和 Agent 开发的话,Coding Plan 更适合,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Claude Code 相关配置参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。
这里要强调:TaoToken 不是用来替代 Paddle-Lite 推理的,它解决的是开发过程中的代码生成、日志分析和配置检查。端侧推理仍然在手机本地完成。把 Key 准备好之后,后面遇到local proxy failed或reading choices这类报错,可以直接把日志贴给模型定位。
3. 可复制配置:opt 转换、CMake 与 JNI 三件套
这一节是全文核心,所有配置都可以直接复制。先给模型转换命令。假设你已经用 PaddleDetection 训练完 YOLOv3 模型,导出到inference_model/yolov3_mobilenet_v1_mask/,里面有__model__和__params__。下载 Paddle-Lite release 里的opt工具,放到同目录,执行:
./opt \ --model_file=./inference_model/yolov3_mobilenet_v1_mask/__model__ \ --param_file=./inference_model/yolov3_mobilenet_v1_mask/__params__ \ --optimize_out_type=naive_buffer \ --optimize_out=./mask \ --valid_targets=arm \ --prefer_int8_kernel=false \ --record_tailoring_info=true执行完得到mask.nb。参数说明:optimize_out_type必须选naive_buffer,移动端只认这个;valid_targets选arm,如果要跑华为 NPU 就写npu arm;record_tailoring_info设为 true 会记录 kernel 和 OP 信息,方便后续裁剪库文件。
接下来是 Android 工程的 CMake 配置。如果你用自编译库,CMakeLists.txt里要指定 Paddle-Lite 的头文件和库路径:
cmake_minimum_required(VERSION 3.4.1) project(paddle_lite_jni) set(PADDLE_LITE_DIR ${CMAKE_SOURCE_DIR}/../../../PaddleLite) include_directories(${PADDLE_LITE_DIR}/include) add_library(paddle_lite_jni SHARED ${CMAKE_SOURCE_DIR}/paddle_lite_jni.cpp) find_library(log-lib log) target_link_libraries(paddle_lite_jni ${PADDLE_LITE_DIR}/libs/${ANDROID_ABI}/libpaddle_lite_jni.so ${log-lib})ABI 裁剪在build.gradle里控制,只保留arm64-v8a和armeabi-v7a:
android { defaultConfig { ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } } }如果你用官方预编译库,直接替换三个文件:PaddlePredictor.jar放到app/libs/,libpaddle_lite_jni.so分别放到app/src/main/jniLibs/arm64-v8a/和armeabi-v7a/。注意一定要用同一版本的 jar 和 so,混用会报UnsatisfiedLinkError。
JNI 三件套里最容易被忽略的是 Model ID 和输入输出对齐。在 Java 层,模型路径、标签路径、输入尺寸、均值、标准差都要和转换时一致:
protected long[] inputShape = new long[]{1, 3, 320, 320}; protected float[] inputMean = new float[]{0.485f, 0.456f, 0.406f}; protected float[] inputStd = new float[]{0.229f, 0.224f, 0.225f};YOLOv3 有两个输入:input0是图像张量,input1是图像尺寸。所以还要加:
Tensor inputTensor1 = getInput(1); inputTensor1.resize(new long[]{1, 2}); inputTensor1.setData(new int[]{320, 320});后处理里,YOLO 输出的是绝对坐标,要除以 320 转成相对值,再乘图像宽高:
float rawLeft = outputTensor.getFloatData()[i + 2] / 320; float rawTop = outputTensor.getFloatData()[i + 3] / 320; float rawRight = outputTensor.getFloatData()[i + 4] / 320; float rawBottom = outputTensor.getFloatData()[i + 5] / 320;这三段配置——opt 转换、CMake/ABI、JNI 输入输出——就是 Paddle-Lite 安卓端部署的骨架。缺任何一段,真机都跑不通。
4. 验证请求与真机结果:从 logcat 到推理耗时
配置改完,编译安装到真机。先用adb logcat看日志,过滤PaddleLite标签。正常启动后,你会看到模型加载成功的输出,类似:
I/PaddleLite: Model loaded from assets/models/mask/model.nb I/PaddleLite: Input shape: [1, 3, 320, 320] I/PaddleLite: Output shape: [1, 6, 1, 1]如果模型加载失败,日志会直接报Fail to load model或Invalid model file。这时候先检查model.nb是不是真的 naive_buffer 格式,用file model.nb看文件头,正常应该显示data。如果显示protobuf,说明 opt 转换时optimize_out_type写错了。
推理跑通后,在 Java 层打印预处理、推理、后处理耗时:
Date start = new Date(); // preprocess Date end = new Date(); preprocessTime = (float)(end.getTime() - start.getTime()); start = new Date(); predictor.run(); end = new Date(); inferenceTime = (float)(end.getTime() - start.getTime()); start = new Date(); // postprocess end = new Date(); postprocessTime = (float)(end.getTime() - start.getTime());我在一台骁龙 865 的机器上实测,YOLOv3-mobilenetv1 320×320 输入,预处理约 18ms,推理约 42ms,后处理约 6ms,单帧总耗时 66ms 左右,能跑到 15fps。如果推理时间超过 200ms,检查是不是用了protobuf格式的模型,或者 ABI 选成了x86。
验证结果是否正确,看画框位置。如果框全部挤在左上角,说明坐标没除以 320;如果框大小正常但类别全错,说明标签文件路径不对。标签文件要放在assets/labels/mask_label_list,每行一个类别名,顺序和训练时一致。
还有一个容易忽略的点:strings.xml里的默认值要和 Java 代码一致。如果INPUT_SHAPE_DEFAULT写的是1,3,300,300,而 Java 里是320,320,会以 Java 为准,但容易混淆。建议统一改:
<string name="MODEL_PATH_DEFAULT">models/mask</string> <string name="LABEL_PATH_DEFAULT">labels/mask_label_list</string> <string name="INPUT_SHAPE_DEFAULT">1,3,320,320</string> <string name="INPUT_MEAN_DEFAULT">0.485,0.456,0.406</string> <string name="INPUT_STD_DEFAULT">0.229,0.224,0.225</string>真机验证通过的标准:打开 App,选一张测试图,能正确画出检测框并显示类别和置信度,logcat 无UnsatisfiedLinkError和Fail to load model。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
部署过程中遇到的报错分两类:一类是 Paddle-Lite 本身的,一类是调用云端 API 辅助排障时的。先列 Paddle-Lite 侧的真实报错。
UnsatisfiedLinkError: dlopen failed: library "libpaddle_lite_jni.so" not found。原因通常是 ABI 不匹配。手机是 arm64,但 jniLibs 里只有 armeabi-v7a,或者 gradle 里 abiFilters 写错。解决:确认app/src/main/jniLibs/下同时有arm64-v8a和armeabi-v7a两个目录,且每个目录里都有libpaddle_lite_jni.so。
Fail to load model: invalid model file。原因:模型不是 naive_buffer 格式,或者文件损坏。解决:重新执行 opt 转换,确认--optimize_out_type=naive_buffer,转换后用file mask.nb检查。
Output tensor shape mismatch。原因:Java 层inputShape和模型实际输入不一致。解决:用opt转换时加--print_model_info查看输入输出,或者用 Netron 打开.nb文件确认。
local proxy failed。这个报错通常出现在调用云端 API 时,本地代理配置有问题。检查环境变量HTTP_PROXY和HTTPS_PROXY是否指向了不可用的地址。如果你用的是 TaoToken 的 API,Base URL 直接写https://taotoken.net/api,不要额外加代理。
reading choices报错。这是解析模型返回 JSON 时字段缺失导致的。常见于请求体里model参数写错,或者 API Key 无效返回了错误页。先确认 Key 是从 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 复制的完整字符串,再检查请求头Authorization: Bearer <key>格式。
401 Unauthorized。Key 过期或没带。重新生成 Key,并确认请求头里没有多余空格。
OAuth 相关报错。如果你用 Claude Code 接入,参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 的配置,Base URL、Key、Model ID 三件套要写全。缺 Model ID 会报model not found。
CC Switch 或 Cline MCP 场景下,同样要写全三件套:Base URL 用https://taotoken.net/api,Key 用控制台生成的,Model ID 按文档填。少任何一个都会在请求阶段失败。
排障时把完整报错日志贴到模型对话页面 https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,让模型帮你定位,比搜索引擎快。
6. 语义一致 CTA:把端侧推理闭环真正跑起来
Paddle-Lite 安卓端部署的完整路径,到这里就闭环了:PaddleDetection 训练 → export_model 导出 → opt 转 naive_buffer → 替换 jar 和 so → 改 CMake 和 ABI → 改 JNI 输入输出 → 真机验证。每一步都有可复制的命令和配置,你照着做就能在自有 App 里跑通。
如果你在排障或接入阶段卡住,优先看 API Keys 和接入文档:API Keys 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先验证模型对话能力,用 https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。长期做端侧编码和 Agent 开发,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
最后给一个实用技巧:模型转换后,先用opt的--print_model_info打印输入输出形状,再改 Java 代码。这样能避免 90% 的 shape mismatch。另外,ABI 只保留arm64-v8a和armeabi-v7a,包体积能减少 40% 左右。真机验证时,先用官方 Demo 跑通,再替换自己的模型,出问题容易定位是模型问题还是工程问题。