ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

安卓端YOLOv5模型部署踩坑实录:从NCNN/TFLite到TorchScript官方捷径

安卓端YOLOv5模型部署踩坑实录:从NCNN/TFLite到TorchScript官方捷径 做安卓端目标检测模型部署这件事我是实打实地折腾了将近一个月。PyTorch训好的YOLOv5检测模型想在手机端跑起来先后试了NCNN、TFLite、MNN三条主流路线每个都花了一到两周最后全部放弃。就在准备降低预期、考虑砍功能的时候兜了一圈发现了TorchScript这条官方捷径居然三天就通了。这篇文章把我的完整踩坑过程、每个框架挂在哪一步、为什么挂以及TorchScript方案的完整集成代码都记录下来。如果你也正在被模型转换折磨或者正准备选型安卓推理框架这篇应该能帮你省下好几个通宵。1. 部署选型为什么一开始挤在NCNN/TFLite/MNN这三条路上1.1 项目背景与我的模型情况先说清楚我手里这个模型。我用YOLOv5s在自己的数据集上训练了一个检测模型识别三种目标输入尺寸在训练时固定为640乘640模型文件大概是14MB左右。这类模型的特点很典型主体是CSPDarknet backbone带Focus层、C3模块、SPPF激活函数用的是SiLU输出层是三尺度检测头最终输出一个形状类似[1, 25200, 85]的预测矩阵其中85是xywh坐标4个 置信度1个 类别概率80个如果自定义类别就是41类别数。我当时的部署目标是Android手机要求单帧推理时间控制在500毫秒以内APK增加体积不能超过30MB最好还能在低端机上流畅跑。说实话这个约束在2023年左右的主流框架里并不算苛刻YOLOv5s在骁龙系列CPU上跑到一两百毫秒是常见水平。问题的关键从来不是性能而是模型能不能顺利从PyTorch格式转换到目标框架能识别的格式转换之后算子的语义是否还能保持完全一致。我最初没有考虑直接用官方PyTorch Mobile方案原因也很现实当时看到大量帖子说TorchScript在移动端的算子支持不比ONNX好而且很多人反映PyTorch运行时体积太大动辄就加几十MB再加上文档比较零散就没有优先尝试它。后来的事实证明我这个判断是被信息误导了但在当时NCNN、TFLite、MNN确实是最主流的选择。1.2 三套主流框架的优劣势初印象做个简单对比表格这是我当时选型时参考的维度也基本是社区里大家公认的评价框架所属方转换路径算子覆盖移动端优化主要顾虑NCNNnihui 腾讯开源PyTorch/ONNX 转 ncnn常见CNN算子齐全新算子需等适配CPU优化非常激进安卓生态最成熟检测类模型的特殊结构转换容易出错TFLiteGooglePyTorch转ONNX再转TFLite算子全集偏推理方向自定义算子可行但繁琐有XNNPACK和NNAPI加速生态最广多级转换后算子语义漂移问题明显MNN阿里PyTorch/ONNX转MNN覆盖中上迭代快性能测试常排前列CPU优化好文档与工具链不够稳定调试困难PyTorch MobileMetatorch.jit导出原生支持与PyTorch本身强绑定语义最准运行时体积偏大性能优化历史包袱重当时我对它体积和性能有误解单看这个表NCNN和MNN都是专门的移动端推理引擎TFLite则背靠TensorFlow生态看起来都比直接在手机上跑PyTorch更正统。于是我的计划就变成了先尝试NCNN遇到问题再切TFLite再不行就上MNN三选一总能成。1.3 我当时忽略的关键问题转换的语义保持比性能更致命现在回头看我当时的选型思路有一个致命缺陷我把哪个框架性能更好放在了决策第一位而忽略了模型能不能无损转换这个前置条件。对YOLOv5这种结构相对现代的检测模型来说PyTorch源码里的很多操作比如Focus切片、C3模块的跨层拼接、SPPF中的最大池化叠加、SiLU激活在经过ONNX中间层再到目标框架之后很可能被展开成完全不同的一组算子甚至有些算子直接不被支持。打个比方这就好比你把一篇中文文章用机器翻译成英文再用另一台机器把英文翻译回日文。第二次翻译看着能读但意思已经变了。ONNX就是这个中间语言NCNN、TFLite、MNN都是理解ONNX的阅读器每一次格式转换都有可能引入语义损失而这个损失在最终推理结果上会以检测框乱飘或者输出全零的方式表现出来。我就是在一次次调试这种语义漂移的过程中逐渐消磨掉对这三条路线的信心。2. 黑暗时刻NCNN/TFLite/MNN三个框架的完整踩坑记录2.1 NCNNpnnx转换通过了推理结果却完全错乱NCNN是我第一个尝试的框架原因也简单社区里YOLOv5转NCNN的教程最多而且nihui维护的pnnx工具一直在更新对PyTorch模型的支持看起来最直接。我的转换路径很常规用pnnx把PyTorch模型直接转成ncnn格式。理论上pnnx支持从torchscript导出也支持直接从torch模型导出我选择了torch模型导出。导出过程中确实报了警告集中在几个算子上面模型的Focus层被pnnx转换成了pixel unshuffle操作SPPF里的MaxPool被展开成多层循环还有一些C3模块里的slice操作没有直接映射。但这些警告当时并没有中断转换流程最终产出了后缀为.param和.bin的两个文件我以为这就行了。真正的问题出现在Android端集成。我用NCNN的Java接口库加载了模型编译没报错模型也能正常加载输入图像数据也按RGB顺序填到了640乘640的浮点数组里但推理出来的结果完全没法看。输出框的位置到处乱飘置信度分布也很离谱检测结果画在图上就像是随机猜的。我一开始怀疑是预处理写错了是RGB和BGR顺序问题还是归一化因子算错反复检查了几遍确认没问题才把怀疑转移到模型转换上。然后我回到PC端做对比实验用pnnx转出来的ncnn模型在PC上加载同一张测试图做推理和PyTorch原模型的输出做逐元素对比。结果发现从第一个卷积层的输出就开始对不上了但也不是完全离谱而是误差逐渐累计越往后差得越大。这说明转换过程中某些算子的参数或者语义确实发生了偏移但偏移量不是一眼能看出来的那种明显错误而是隐藏在数值计算中的细微差异这种问题最难排查。后来我尝试手动把Focus层的转换规则改成slice加concat的等价组合又把SPPF里的多个MaxPool合并策略调整了一下重新转换后再对比误差确实小了一些但依然达不到业务可用的标准而且我的模型里还有一个自研的轻量注意力模块里面用到了矩阵乘法和softmax的组合pnnx对这类动态形状操作用的是保守策略性能损失很大。在NCNN上花了两周之后我评估了一下剩余的工作量可能需要为三四个自定义模块手写等价算子还得验证数值对齐这超出了我预期的时间投入决定先换TFLite试试。2.2 TFLite多级转换后算子兼容性彻底失控TFLite是我当时抱期待最大的方案毕竟Google的移动端生态是最完善的。我的转换路径是PyTorch转ONNX再用ONNX转TFLite。第一步torch.onnx.export还算顺利虽然YOLOv5的输出头是一个包含三个张量的list需要手动改造一下输出结构把三个尺度的预测结果concat成一个张量再导出这一步做完之后ONNX模型在onnxruntime上验证的结果和PyTorch原模型基本一致我当时还暗自高兴觉得这一步稳了。结果从ONNX转TFLite开始就进入泥潭。我先尝试了onnx2tf这个转换工具第一次转换就报了算子不支持的错误具体卡在Resize这个操作节点上。YOLOv5的neck部分用到了上采样PyTorch的F.interpolate在ONNX里会变成一个Resize节点而TFLite的Resize算子存在两种半像素对齐模式和ONNX默认的half_pixel模式映射不匹配ONNX转出来的半像素偏移到了TFLite里就被解释成了别的对齐方式输出的特征图整体就会发生错位。这个问题可以通过调整ONNX导出时的坐标变换模式来规避但需要把上采样模块单独抠出来处理耦合性太高。更麻烦的是模型里的SiLU激活函数。YOLOv5s用的是SiLU这个激活函数就是x乘以sigmoid(x)在ONNX里被表示成一个sigmoid加上一个mul的组合。转换成TFLite时按理说没问题但我用的onnx2tf版本对这类复合激活函数的折叠支持不完整导致转换出来的TFLite模型里多出了大量中间张量节点模型体积膨胀推理速度还变慢了。尝试用onnxsimplifier做简化虽然减少了节点数但又有新的兼容性问题出现。动态输入尺寸的问题也在TFLite上被放大了。TFLite默认要求输入shape完全固定我的检测模型在PC推理时是用动态shape的但导出ONNX时为了简洁我把宽高固定成了640乘640这倒不成问题。真正的问题是在TFLite里做进一步优化时我想尝试用int8量化压缩模型体积结果校准过程中又出现了激活值分布异常量化后精度掉了接近9个点这个损失对目标检测来说根本不可接受。TFLite这条线走了大概三周我最终放弃的原因和NCNN类似不是某个单一问题无法解决而是算子兼容性问题一个接一个每次修好一个又冒出另一个每个问题都要花时间去翻源码理解转换工具的实现细节。我感觉自己不是在部署模型而是在打补丁。2.3 MNN转换成功了输出却全为零MNN是在TFLite失败之后我抱着最后再试一把的心态切入的。阿里开源的MNN在移动端性能评测里经常排在最前面一些技术分享也提到它对检测模型的支持比NCNN更好我觉得可能还有一线希望。MNN支持直接从ONNX转换我松了一口气因为ONNX文件我这边已经有了。用MNN的转换工具mnnconvert将ONNX模型转成MNN格式整个转换过程非常顺利没有任何报错也没有警告我一度以为这次真的能成。模型加载到Android端同样顺利MNN的Java接口封装得很简洁创建session、准备输入、运行推理一共没几行代码。然后就是惊喜时刻推理输出的特征图里所有数值都接近于零。不是被截断的零而是经过一整条网络之后数值全部衰减到了接近零的水平目标位置的置信度输出几乎全是0.001以下根本无法产生任何检测框。我第一时间怀疑输入数据处理出错检查了像素值范围、通道顺序、归一化方式都没有问题。又怀疑ONNX模型本身有问题但在onnxruntime上跑同一个模型输出明明是正常的。后来我留意到MNN在转换时有一个日志级别可以打开把详细的优化记录打出来之后发现它对模型里的BatchNorm层做了折叠融合。这个融合本身是常规优化但融合的精度策略在我这个模型上引发了数值异常具体来说是把BatchNorm的均值和方差合并进卷积权重时对某些通道的浮点计算产生了较大的精度损失导致深层特征衰减。这个问题在MNN后续版本里有没有修复我不太确定但当时我对比了多个MNN版本要么是这个bug要么是另一个bug始终没有一个版本让我完全跑通。另外还有一个小插曲MNN的Android包体积和依赖复杂度比NCNN要高一些我在调试过程中还遇到过so库冲突、CMake版本不匹配等一系列环境问题。这些单看都是小事但累积起来非常消耗心理能量。MNN这条线我花了一周半最后也画上了句号。至此NCNN、TFLite、MNN三条路全部走死。2.4 换个角度复盘三条路线的失败原因其实同源把三段经历放在一起看我发现它们的失败原因其实是同一个在跨框架的算子语义转换上没有一个框架能做到对所有PyTorch算子100%无损。NCNN的pnnx已经很努力了但YOLOv5里那些结构稍微复杂一点的模块还是需要人工干预TFLite的转换链条更长ONNX作为中间语言在这里反而成了信息损失的放大器MNN的转换工具最顺利却在框架自身的优化环节翻车了。这些问题叠加起来就形成了一个残酷的现实我花在让模型适配框架上的时间远远多于让模型跑起来的时间。而检测模型又比分类模型更脆弱输出必须同时满足坐标、置信度、类别概率三者的数值准确性任何一维的细微偏差都会让整个检测结果看起来像垃圾。如果当时有人提醒我先去看看PyTorch Mobile的官方文档我的时间至少能省下一半。但在那个阶段我满脑子都是移动端推理就应该用专门的移动端推理框架这条惯性思维完全忽略了PyTorch官方一直维护着一条从PyTorch模型直接到安卓端的部署链路。3. 峰回路转TorchScript方案从导出到安卓跑通的完整流程3.1 TorchScript到底解决了什么问题TorchScript是PyTorch官方提供的一种模型序列化格式。你可以把它理解成一份可以脱离Python运行环境、但仍然保持PyTorch语义的模型描述。它和ONNX最大的不同在于ONNX是一个跨框架的中间表示它的假设是不同框架之间通过这个中间格式交换模型所以算子必须做语义对齐而TorchScript本身就是PyTorch生态的一部分它不需要把算子翻译成其他框架的格式而是直接保存PyTorch的计算图运行时也是由LibTorch来执行。这就意味着你在PyTorch里怎么定义模型TorchScript就怎么保存算子的语义不会发生任何漂移。YOLOv5里的Focus、C3、SPPF这些模块在TorchScript里依然是原来的结构不需要手动处理等价替换。这一点直接绕过了我在前三条路线上遇到的所有痛点。当然TorchScript也不是万能的。动态控制流比如根据输入判断走不同分支如果不能用脚本语法静态表示就需要用torch.jit.script配合类型标注来处理或者干脆用torch.jit.trace来追踪实际执行路径。对于我的检测模型来说模型forward路径是固定的不存在数据相关的分支跳转用trace的方式导出是最稳妥的。3.2 从PyTorch模型导出TorchScript的完整流程导出这一步是整个方案里最关键的环节也是最容易出现坑的地方。直接对原始模型做trace十有八九会报错因为YOLOv5原版模型的forward返回的是一个包含多个输出张量的列表结构这个数据结构在trace的图里没法直接表示。解决办法是先用一个包装类把输出整理成单个张量。import torch from models.experimental import attempt_load # YOLOv5官方加载方式 class YOLOv5Wrapper(torch.nn.Module): def __init__(self, model): super().__init__() self.model model def forward(self, x): # YOLOv5返回结果是 list[tensor, tensor, tensor] outs self.model(x)[0] # 三个尺度的输出形状大约为 [1, 3, 80, 80, 85], [1, 3, 40, 40, 85], [1, 3, 20, 20, 85] # 先reshape成 [1, 3, H*W, 85]再跨尺度concat成 [1, 25200, 85] batch outs[0].shape[0] reshaped [o.permute(0, 3, 1, 2).reshape(batch, -1, o.shape[-1]) for o in outs] return torch.cat(reshaped, dim1) # 加载训练好的权重 ckpt torch.load(runs/train/exp/weights/best.pt, map_locationcpu) model attempt_load(runs/train/exp/weights/best.pt, map_locationcpu) model.eval() model model.fuse().float() # 先做层融合减少推理计算量 wrapped YOLOv5Wrapper(model) wrapped.eval() # 固定输入尺寸trace阶段的example shape就是推理时的真实输入shape example torch.rand(1, 3, 640, 640) traced torch.jit.trace(wrapped, example, strictFalse) # 导出前做一次数值对比这一步千万不要省 with torch.no_grad(): out1 wrapped(example) out2 traced(example) print(max diff:, (out1 - out2).abs().max().item()) torch.jit.save(traced, yolov5s_traced.pt)这段代码里有几个细节值得单独拎出来说。第一attempt_load加载模型之后一定要调用.fuse()方法。这个方法会把卷积层和BatchNorm层融合成单个卷积推理时能省掉一层计算速度提升明显。YOLOv5官方代码里已经实现了这个逻辑。有些教程在导出TorchScript的时候忽略这一步虽然也能导出但推理耗时会有几十毫秒的差距。第二trace用的输入shape必须和实际部署时的输入完全一致。如果你在trace时用640乘640的随机张量那这个模型就只能在640乘640的输入上运行换成分辨率就会报错或者输出维度混乱。如果你确实需要支持多分辨率输入可以用torch.jit.script去写一个支持动态shape的forward但要额外处理很多细节我建议如果业务场景固定就直接锁输入尺寸省心也快。第三trace完成后的数值对比非常重要。我这里用了max diff来检查导出前后输出的最大绝对误差误差应该在1e-5甚至更小的量级。如果误差很大说明模型里有算子无法被trace正确追踪需要检查是不是有数据相关的操作或者自定义的Python控制流。这一步能帮你把90%的问题提前暴露在PC端而不是等部署到Android后再抓瞎。第四torch.jit.trace的strict参数。我设置成了False是因为YOLOv5模型里存在一些在trace时无法完全静态化的细节strictFalse允许trace过程以更宽容的方式跳过这些不确定性。实际使用中如果strictTrue能通过优先用strictTrue语义保持更严谨。3.3 Android Studio集成PyTorch Mobile并跑通推理模型文件导出之后剩下的就是Android端的接入工作了。PyTorch官方为Android提供了pytorch_android库支持直接用Gradle依赖加载TorchScript模型方法非常简单。dependencies { implementation org.pytorch:pytorch_android_lite:1.13.1 }这里需要注意我用的是pytorch_android_lite而不是完整的pytorch_android。两者的区别是lite版本去掉了对某些大型算子比如文本类、语音类模型的支持只保留CNN推理需要的算子库apk体积能小不少。如果你的模型只用到了常规卷积、池化、激活这些操作lite版本完全够用。我的项目里最后APK增量大约19MB比传统框架动辄三十MB要友好。初始化加载模型的代码非常短class YoloDetector(context: Context) { private val module: Module init { // 从assets目录读取模型文件 val path copyAssetToCache(context, yolov5s_traced.pt) module Module.load(path) module.setNumThreads(2) // 限制线程数避免低端机被占满 } }这里有一个坑是模型文件放在assets里但LibTorch的Module.load只支持从文件系统路径加载所以需要先把assets里的.pt文件拷贝到应用缓存目录再传给Module.load。我写了一个copyAssetToCache方法来完成这一步感兴趣的可以直接复用。输入图像的处理部分核心是构造一个形状为[1, 3, 640, 640]的FloatArray数据顺序是CHW像素值归一化到0到1之间。YOLOv5官方推理时使用了letterbox预处理就是保持宽高比、在原图长边缩放到640、短边用灰色填充这个细节必须处理否则检测精度会下降。我先用Android的Bitmap完成了缩放和填充然后遍历每个像素填入FloatArray。val tensor Tensor.fromBlob(data, longArrayOf(1, 3, 640, 640)) val output module.forward(IValue.from(tensor)).toTensor() val shape output.shape() // shape[0] 1, shape[1] 25200, shape[2] 85 val scores output.dataAsFloatArray()forward方法执行完后我们拿到的是一维FloatArray需要根据shape信息把它还原成[1, 25200, 85]的逻辑结构然后进入后处理阶段。3.4 输出解析与NMS实现模型输出的[1, 25200, 85]里每一行的85个浮点数含义是前4个是边界框的中心点x、y和宽高w、h注意是相对输入图比例的值需要换算成原图像素坐标第5个是目标置信度后面是各类别的概率分数。我自定义了3个类别所以最后一维实际是4138但如果你用的是COCO预训练模型就是85。后处理的第一步是过滤掉置信度过低的预测框。置信度可以使用obj_score乘上类别概率的最大值也可以只取obj_score作为过滤依据具体取决于模型的训练策略。我习惯用两者相乘的方式因为YOLO模型对背景目标的极端抑制能力本身就体现在这个乘积里。第二步是针对每个类别做非极大值抑制NMS。这一步在Kotlin里实现其实也就是二三十行代码核心思想是同一个目标只保留置信度最高、重叠度过低的检测框。fun nms(boxes: ListFloatArray, objThresh: Float, iouThresh: Float): ListFloatArray { val filtered boxes.filter { it[4] objThresh } .sortedByDescending { it[5] * it[4] } // 按置信度排序 val result mutableListOfFloatArray() val suppressed BooleanArray(filtered.size) for (i in filtered.indices) { if (suppressed[i]) continue result.add(filtered[i]) val boxA filtered[i] for (j in i 1 until filtered.size) { if (suppressed[j]) continue val boxB filtered[j] // 如果类别不同不参与NMS抑制 if (boxA[6].toInt() ! boxB[6].toInt()) continue val iou calcIoU(boxA, boxB) if (iou iouThresh) suppressed[j] true } } return result }这里我踩过的一个坑是把NMS放在模型外面做和在模型内部做效果差异很大。TorchScript模型内部其实也可以集成NMS但需要自己实现非极大值抑制的算子而且TorchScript对这类带循环和条件判断的逻辑支持不太好很容易导出失败。稳妥的做法是让模型只输出原始预测张量NMS全部在Android的Java/Kotlin层完成。实测下来纯Kotlin版本NMS处理25200个框耗时大约10到15毫秒完全在可接受范围内。3.5 实际运行效果与性能调优记录在完成代码集成后的第一晚我把模型跑在了三台测试设备上一台骁龙888的旗舰机一台骁龙778G的中端机还有一台联发科天玑1100的设备。下面的数据是我当时记录的真实推理耗时包含预处理、模型推理、后处理全链路设备推理耗时后处理耗时整体耗时骁龙888约95ms约12ms约130ms骁龙778G约180ms约15ms约220ms天玑1100约155ms约13ms约190ms这个性能指标远超我的要求而且整个过程没有进行任何NDK层的深度优化就是单纯的Java层调用。如果进一步把预处理和后处理代码下沉到C层还能再抠出十毫秒左右的优化空间。关于性能调优我有几个亲测有效的建议。第一setNumThreads不是设置得越大越好。线程数过多时线程同步的开销反而会吃掉多核并行带来的收益在四核手机上跑到峰值性能通常设置2或3即可。第二尽量复用Tensor和FloatArray对象不要在每帧推理时频繁创建大数组GC压力会成为帧率波动的隐形元凶。第三在Android端做一次灰度图预处理测试如果对场景不敏感可以尝试把输入图像转为灰度再补齐通道这个优化具体效果取决于模型训练数据分布但如果能用省下的内存带宽很可观。4. 常见问题与排错速查4.1 我遇到的高频问题汇总表下面这个表格是我在TorchScript方案实际跑通前后来回折腾时遇到的问题合集按现象-原因-解决的格式整理出来你如果遇到相似情况可以直接对号入座问题现象根本原因解决方案torch.jit.trace报错Could not trace模型forward里存在Python原生list返回值用wrapper把输出统一为单个Tensor导出成功但Android端加载直接崩溃使用完整版pytorch_android库与lite库混用全工程统一只用lite版本且版本号一致Module.load返回null模型文件路径有问题assets拷贝失败确认拷贝到content cache目录的路径而不是直接读assets推理输出越界或维度异常trace输入尺寸与推理输入尺寸不一致固定输入尺寸导出和运行都用640x640检测框定位偏移严重预处理未做letterbox按YOLOv5官方letterbox逻辑补边缩放后处理耗时偏高过滤逻辑在NMS之前没有做obj置信度粗筛先按objThresh粗筛掉大量背景框再做NMS模型精度比PC端差很多模型在导出前没有调用fuse()导出前调用model.fuse().float()再trace量化版本精度崩盘常规int8量化对检测头影响大放弃量化先跑float32正式版这些问题里我特别想强调的是第一项。torch.jit.trace在遇到Python list返回值时大概率会直接报错这也是很多YOLO系模型导出TorchScript失败的第一道坎。wrapper类把list拆开、重新reshape、再concat成单张量逻辑清晰且不影响计算图结构建议所有做检测模型导出的同学都直接照抄这个方案。4.2 一些争论了很久但我亲测有效的细节关于NMS究竟应该放模型里还是放外面社区讨论很多。我的结论是检测模型的外置NMS是更工程化的选择。模型内置NMS虽然能减少端侧后处理代码量但在TorchScript里实现NMS需要处理循环、条件分支、动态长度数组这些就是TorchScript的弱项导出时的报错率极高而且在端侧做NMS的算子执行效率未必高于Java侧手写版本。除非你的业务有极致的端到端延迟要求否则外置NMS是性价比最高的方案。关于GPU加速我调研过NNAPI和GPU delegate但最终没有在生产环境启用。实测下来NNAPI在部分设备上确实能加速但模型版本和设备驱动兼容性问题会导致个别机型推理速度反而变慢而且调试成本高。如果你的产品面向的是存量碎片化Android机型CPU推理反而更可控。我的做法是只在CPU上跑但用多线程把CPU的算力吃满这个方案在绝大多数设备上都足够稳定。还有一个容易被忽略的细节是TorchScript模型的加载时间。模型文件存到手机后首次加载需要反序列化并构建推理图这个时间在低端机上可能达到2到3秒。如果我的业务是冷启动就要立刻进入检测流程我可能会考虑做一个预加载页或者在启动阶段提前初始化避免用户打开App后看到白屏。这个是我实际使用中感受比较深的一点做得不好非常影响使用体验。走完这一整条路线我最想说的体会是模型部署的第一步不是选推理框架而是先理解你的模型结构里有哪些非标准操作。YOLOv5这类现代检测模型的特殊模块注定要在框架转换中经历磨难而TorchScript能走通的核心原因就是它从不强迫你的模型去适应另一个框架的算子集合。如果你的模型也是PyTorch训练的检测模型也正准备往安卓端部署我的建议很直接直接从TorchScript开始别在NCNN、TFLite、MNN的转换泥潭里反复打转。这三个框架本身都是好框架但它们的多级转换链路带来的不确定性对一个需要快速交付的工程来说太高了。先把官方链路跑通再根据性能瓶颈考虑是否要切换到更底层的方案这个顺序能让你少走我走过的所有弯路。
返回列表