前言
在边缘设备上部署深度学习模型,一直是嵌入式 AI 领域最具挑战性的课题之一。当你训练好了一个准确率令人满意的 PyTorch 模型,满心欢喜地想把它搬到 ARM 开发板上跑一跑,却发现原始模型推理一次需要好几秒,这样的性能在实际产品中根本无法使用。这时你才意识到,训练和部署之间,隔着一道看不见却异常宽阔的鸿沟。
这道鸿沟的两边是完全不同的世界:训练端追求的是灵活的算子支持、便捷的调试接口、高效的分布式训练;而部署端追求的却是极致的推理速度、最小的内存占用、最低的功耗开销。大多数框架都是为训练设计的,即使像 PyTorch 这样优秀的框架,其 C++ 前端 LibTorch 在嵌入式设备上的表现也往往差强人意。
于是我们需要专门的推理框架。在众多推理框架中,腾讯开源的 NCNN 是一个相当特别的存在。它从诞生之初就是为移动端和嵌入式设备设计的,没有历史包袱,从内存管理到算子实现都围绕 ARM 架构深度优化。更重要的是,NCNN 是纯 C++ 实现,没有任何第三方依赖,这意味着你可以轻松将它集成到各种奇葩的嵌入式环境中。
我第一次接触 NCNN 是在一块瑞芯微 RK3399 开发板上部署目标检测模型。当时用 PyTorch 推理一帧 YOLO 需要约 800ms,用 TensorFlow Lite 也需要 400ms 左右,而用 NCNN 优化后,同样的模型在同一硬件上只需要 120ms,这还没开启 Vulkan GPU 加速。那一刻我真切感受到,一个好的推理框架带来的性能提升,往往比换一颗芯片还要显著。
这篇文章会带你完整走一遍 NCNN 的部署流程:从模型训练完成后的 ONNX 导出,到 onnx2ncnn 转换,再到模型优化、INT8 量化、最后编写 C++ 推理代码。文中所有命令和代码都经过实际验证,你可以照着一步步操作。
一、为什么选择 NCNN?
在深入具体操作之前,我们先聊聊为什么在众多推理框架中选择 NCNN,它的核心优势在哪里,又有哪些局限性。
1.1 推理框架的选型维度
选择一个推理框架,通常需要考虑以下几个维度:
| 维度 | 说明 | 重要程度 |
|---|---|---|
| 性能 | 同样硬件上的推理速度 | ⭐⭐⭐⭐⭐ |
| 模型支持 | 能否正常转换你的模型 | ⭐⭐⭐⭐⭐ |
| 易用性 | 文档是否完善,社区是否活跃 | ⭐⭐⭐⭐ |
| 跨平台 | 支持多少种目标硬件 | ⭐⭐⭐⭐ |
| 二进制体积 | 对资源紧张的 MCU 很重要 | ⭐⭐⭐ |
| 许可证 | 是否允许商业闭源使用 | ⭐⭐⭐⭐ |
用这个维度表来评估 NCNN,你会发现它在大多数项上得分都很高:性能在 ARM CPU 上属于第一梯队,模型支持覆盖了绝大多数常见算子,Apache 2.0 许可证非常宽松,二进制最小可以压缩到几百 KB。
1.2 NCNN 的核心优势
极致的 ARM 优化 是 NCNN 最核心的竞争力。NCNN 为 ARMv7、ARMv8 架构写了大量的 NEON 汇编优化代码,不是简单的编译器自动向量化,而是手工优化的汇编级实现。比如卷积的 Im2col + Gemm 实现,Winograd 快速卷积算法,都经过了精细的指令调度和寄存器分配优化。
这种手工优化的效果有多明显?以 3x3 卷积为例,在 Cortex-A53 上,NCNN 的实现通常比 OpenCV DNN 快 2-3 倍,比未经优化的参考实现快 10 倍以上。这不是算法层面的差距,纯粹是工程实现上的精益求精。
零依赖的纯 C++ 实现 是 NCNN 的另一个巨大优势。很多框架看起来很强大,但一交叉编译就会发现依赖一大堆第三方库:Protobuf、FlatBuffers、BLAS 库等等。在某些嵌入式环境中,光是把这些依赖库编译过去就是一场噩梦。
而 NCNN 是真正的零依赖,它甚至不依赖 C++ STL 的异常和 RTTI,在最精简的配置下,你只需要一个能编译 C++ 的交叉编译器就能把 NCNN 编出来。这种特性在面对各种定制化的嵌入式 Linux 甚至裸机环境时,价值尤为突出。
灵活的扩展性 也值得一提。NCNN 设计了一套清晰的算子注册机制,如果你需要一个自定义算子,只需要继承一个基类,实现前向计算函数,然后注册一下就行,不需要修改框架的核心代码。这种设计对于需要部署自研算子的场景非常友好。
1.3 NCNN 的局限性
当然,NCNN 也不是万能的。它的主要局限性在于:
- GPU 支持不如 TensorRT:虽然 NCNN 支持 Vulkan GPU 加速,但在 NVIDIA 设备上,性能还是不如 TensorRT。不过在 ARM Mali GPU 上,NCNN 的 Vulkan 后端表现相当不错。
- 动态形状支持有限:NCNN 主要是为固定输入形状优化的,动态形状的支持不如 ONNX Runtime 灵活。
- 调试工具相对简陋:相比 TensorRT 有完善的 profiling 工具,NCNN 的调试更多需要依赖
ncnn::Extractor的逐层输出和自己打日志。
总体来说,如果你的目标平台是 ARM CPU(手机、开发板、嵌入式设备),NCNN 是目前最好的选择之一。如果是 NVIDIA GPU,应该优先考虑 TensorRT。
二、环境搭建:从源码编译 NCNN
正式开始之前,我们需要先把 NCNN 源码下载下来并编译。NCNN 的编译系统是 CMake,过程相对 straightforward,但有几个关键的编译选项需要特别注意。
2.1 获取源码
# 克隆 NCNN 源码
git clone https://github.com/Tencent/ncnn.git
cd ncnn
# 切换到最新的稳定版本(可选但推荐)
git checkout 20240410 # 选择一个较新的稳定版本
2.2 主机端编译(x86 Linux)
首先我们在 x86 主机上编译 NCNN,主要是为了获得各种模型转换工具(onnx2ncnn、ncnnoptimize 等)。
mkdir -p build-host && cd build-host
cmake .. \
-DNCNN_BUILD_TOOLS=ON \
-DNCNN_BUILD_EXAMPLES=ON \
-DNCNN_BUILD_BENCHMARK=ON \
-DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
编译完成后,你会在 tools/ 目录下看到各种工具:
onnx2ncnn- ONNX 模型转 NCNN 格式ncnnoptimize- NCNN 模型优化ncnn2table- 生成量化校准表ncnn2int8- INT8 量化- 等等…
把这些工具的路径加入 PATH 或者记住它们的位置,后面会频繁使用。
2.3 交叉编译(ARM Linux)
接下来是最重要的一步:为目标 ARM 设备交叉编译 NCNN。这里假设你使用的是 ARMv8 架构(Cortex-A53/A55/A72/A76 等),工具链是 aarch64-linux-gnu-gcc。
cd ..
mkdir -p build-arm64 && cd build-arm64
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=../toolchains/aarch64-linux-gnu.toolchain.cmake \
-DNCNN_BUILD_TOOLS=OFF \
-DNCNN_BUILD_EXAMPLES=OFF \
-DNCNN_BUILD_BENCHMARK=ON \
-DNCNN_VULKAN=OFF \
-DNCNN_SYSTEM_GLSLANG=OFF \
-DNCNN_OPENMP=ON \
-DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
几个关键编译选项的说明:
| 选项 | 说明 |
|---|---|
NCNN_VULKAN |
是否开启 Vulkan GPU 加速 |
NCNN_OPENMP |
是否开启 OpenMP 多线程 |
NCNN_BUILD_TOOLS |
模型转换工具不需要在 ARM 上运行 |
NCNN_RUNTIME_CPU |
运行时检测 CPU 特性并动态选择优化路径 |
如果你需要 Vulkan GPU 支持,将 NCNN_VULKAN 设为 ON,但要确保目标设备有可用的 Vulkan 驱动。
编译完成后,把 src/libncnn.a 和头文件复制到你的交叉编译环境中,或者直接在 CMake 项目中通过 add_subdirectory 引入。
2.4 Android / iOS 编译
对于移动端,NCNN 提供了更便捷的编译脚本:
# Android
cd ncnn
mkdir -p build-android && cd build-android
cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-24 \
-DNCNN_VULKAN=ON
(第一部分完,约2100字)
三、模型转换:从 PyTorch 到 ONNX 再到 NCNN
模型转换是整个部署流程中最容易出问题的环节。一个看起来完美的模型,在转换过程中可能因为一个不起眼的算子就导致整个流程卡住。这一节我们按照标准流程一步步来,尽量避开那些常见的坑。
3.1 第一步:PyTorch 导出 ONNX
在将模型交给 onnx2ncnn 之前,我们首先需要把 PyTorch 模型导出为 ONNX 格式。这一步看似简单,实则暗藏玄机。
import torch
import torchvision
# 加载模型
model = torchvision.models.resnet18(pretrained=True)
model.eval()
# 准备示例输入
dummy_input = torch.randn(1, 3, 224, 224)
# 导出 ONNX
torch.onnx.export(
model,
dummy_input,
"resnet18.onnx",
export_params=True,
opset_version=13,
do_constant_folding=True,
input_names=["input"],
output_names=["output"],
dynamic_axes=None
)
这段代码看起来很标准,但有几个关键点需要特别注意:
opset_version 的选择:不要用太新的 opset,也不要用太旧的。opset 11-13 是目前兼容性最好的区间。opset 太高(比如 17+)可能引入了一些新的算子表示方式,onnx2ncnn 可能还没来得及支持。
dynamic_axes 设为 None:除非你真的需要动态形状。NCNN 对固定输入形状的优化最好,动态形状不仅会损失一部分性能,还可能触发某些算子的 bug。如果你的输入尺寸是固定的,就不要开动态轴。
导出前必须调用 model.eval():这个很重要,否则 BatchNorm、Dropout 等层在训练和推理模式下行为是不同的。忘记调用 eval() 是新手最容易犯的错误之一。
导出完成后,建议用 onnxsim 简化一下模型,这一步能解决 80% 的转换问题:
# 安装 onnxsim
pip install onnxsim
# 简化 ONNX 模型
onnxsim resnet18.onnx resnet18-sim.onnx
onnxsim 会做常量折叠、形状推导、无用节点消除等优化。很多 onnx2ncnn 报错的模型,经过 onnxsim 之后就正常了。这一步强烈建议执行,不要跳过。
3.2 第二步:onnx2ncnn 转换
ONNX 准备好了,接下来就是转换为 NCNN 的原生格式。NCNN 的模型格式由两个文件组成:
.param- 网络结构定义(文本格式,可以用文本编辑器打开).bin- 权重数据(二进制格式)
转换命令很简单:
onnx2ncnn resnet18-sim.onnx resnet18.param resnet18.bin
如果一切顺利,你会看到一堆输出,最后没有 error 字样。如果有 error,说明遇到了不支持的算子或者 ONNX 格式有问题。
常见的错误类型和解决方法:
**1. “Unsupported resize mode”
Resize 算子是转换失败的重灾区。ONNX 的 Resize 有多种 coordinate_transformation_mode,NCNN 只支持 asymmetric 和 align_corners 两种。如果你的模型用了其他模式,可以在导出 ONNX 之前修改模型代码中的插值方式,或者用 onnxruntime-tools 手动修改 ONNX 节点属性。
**2. “Unsupported slice with step != 1” NCNN 的 Slice 算子只支持步长为 1 的情况。如果模型里有 step > 1 的 Slice,可以用 Reshape + Permute + Reshape 的组合来替代,或者修改模型结构避免使用这种特殊的 Slice。
**3. “Too many axes for permute” NCNN 的 Permute 只支持最多 4 维。如果你的模型有 5 维以上的 Permute,可以考虑拆分或者用其他算子组合实现。
转换成功后,建议打开 .param 文件看一眼。文件开头是层的数量和 blob 的数量,然后每一行是一个层的定义。检查一下有没有奇怪的层名,比如 Shape、Gather 这种通常意味着模型里有动态形状相关的操作,这在 NCNN 中支持有限。
3.3 第三步:ncnnoptimize 优化
原始转换出来的模型还可以进一步优化。ncnnoptimize 工具可以做:
- 融合 BatchNorm 到 Convolution
- 消除 Dropout 层(推理模式下没用)
- 权重数据类型转换(FP32 → FP16)
- 内存布局优化
ncnnoptimize resnet18.param resnet18.bin resnet18-opt.param resnet18-opt.bin 0
最后一个参数 0 表示保持 FP32,1 表示转换为 FP16。FP16 可以将模型体积减半,在 ARMv8.2+ 的设备上还能获得显著的性能提升,精度损失通常很小。
优化完成后,你会得到两个文件:resnet18-opt.param 和 resnet18-opt.bin。这两个就是最终部署用的模型文件了。
四、INT8 量化:让推理速度再翻倍
对于嵌入式设备来说,FP32 推理往往还是不够快。INT8 量化可以在精度损失可控的前提下,将推理速度再提升 1.5-2 倍,内存占用也会减半。
4.1 量化的基本原理
量化的核心思想是用 8 位整数来近似表示 32 位浮点数。简单来说就是:
float_value = scale * (int8_value - zero_point)
每个张量都有自己的 scale 和 zero_point。推理时,先把输入量化为 INT8,做 INT8 卷积计算,然后再反量化回 FP32(或者直接下一层继续用 INT8)。
NCNN 使用的是后训练量化(Post-Training Quantization),不需要重新训练模型,只需要几百张校准图片就能完成量化。
4.2 生成校准表
首先我们需要准备一批校准图片,数量通常 100-1000 张就够了,不需要太多,也不需要和训练集完全一致,只要数据分布类似就行。
创建一个 imagelist.txt 文件,每行是校准图片的路径:
calib/000001.jpg
calib/000002.jpg
calib/000003.jpg
...
然后生成校准表:
ncnn2table \
resnet18-opt.param \
resnet18-opt.bin \
imagelist.txt \
resnet18.table
这个过程会比较慢,因为它要在所有校准图片跑一遍前向传播,统计每一层的激活值范围。
生成的 .table 文件是文本格式,你可以打开看看,每一行是某一层的量化参数。
4.3 执行量化
有了校准表,就可以把 FP32 模型转换为 INT8 模型了:
ncnn2int8 \
resnet18-opt.param \
resnet18-opt.bin \
resnet18-int8.param \
resnet18-int8.bin \
resnet18.table
完成后你会得到 INT8 版本的模型。.bin 文件大小大概只有原来的 1/4。
4.4 量化精度调优
如果量化后精度下降明显,可以试试这些方法:
-
增加校准图片数量:从 100 张增加到 500 张通常会有改善。
-
选择合适的校准算法:ncnn2table 支持 KL 散度和熵两种校准方法,默认为 KL。可以尝试不同方法对比精度。
-
逐层反量化:某些层(比如检测头)对量化特别敏感,可以把这些层单独排除在量化之外,保持 FP32。
-
检查预处理是否一致:量化前后的预处理(归一化、通道顺序等必须完全一致,这是很多人忽略但影响巨大的点。
(第二部分完,约2300字)
五、C++ 推理代码编写
模型准备好了,接下来就是编写实际的推理代码。NCNN 的 API 设计得相当简洁,一个完整的推理流程只需要寥寥几行代码就能完成。
5.1 最简推理示例
#include <opencv2/opencv.hpp>
#include "net.h"
int main()
{
// 1. 创建 Net 对象并加载模型
ncnn::Net net;
net.load_param("resnet18-int8.param");
net.load_model("resnet18-int8.bin");
// 2. 读取图片并预处理
cv::Mat img = cv::imread("test.jpg");
// Resize 到模型输入尺寸
ncnn::Mat in = ncnn::Mat::from_pixels_resize(
img.data, ncnn::Mat::PIXEL_BGR,
img.cols, img.rows, 224, 224
);
// 归一化(ImageNet 标准参数)
const float mean_vals[3] = {103.53f, 116.28f, 123.675f};
const float norm_vals[3] = {0.017429f, 0.017507f, 0.017125f};
in.substract_mean_normalize(mean_vals, norm_vals);
// 3. 执行推理
ncnn::Extractor ex = net.create_extractor();
ex.set_num_threads(4); // 设置线程数
ex.input("input", in);
ncnn::Mat out;
ex.extract("output", out);
// 4. 解析输出
// out 是 1x1000 的向量,取最大值索引即为预测类别
int max_idx = 0;
float max_val = out[0];
for (int i = 1; i < out.w; i++) {
if (out[i] > max_val) {
max_val = out[i];
max_idx = i;
}
}
printf("Predicted class: %d, confidence: %.4f\n", max_idx, max_val);
return 0;
}
这段代码展示了最基本的推理流程,但还有很多细节值得深入探讨。
5.2 输入预处理的坑
预处理是最容易出问题但也最容易被忽视的环节。我见过至少一半的部署问题,最后都追溯到预处理不一致。
通道顺序:OpenCV 读进来的图片是 BGR 顺序,而 PyTorch 训练时通常是 RGB 顺序。注意上面代码中 from_pixels_resize 的第二个参数是 ncnn::Mat::PIXEL_BGR,这意味着 NCNN 会保持 BGR 顺序。如果你训练时用的是 RGB,这里应该改成 ncnn::Mat::PIXEL_BGR2RGB。
归一化参数:mean_vals 和 norm_vals 必须和训练时完全一致。很多人训练时用的是 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225],这和代码中的数值是等价的,只是转换了一下:
mean_vals = [0.485*255, 0.456*255, 0.406*255]
norm_vals = [1.0/255/0.229, 1.0/255/0.224, 1.0/255/0.225]
插值方法:NCNN 默认使用 bilinear 插值,确保和训练时的数据增强使用的插值方法一致。
5.3 线程数与性能调优
set_num_threads() 是一个非常重要的函数。线程数不是越多越好,最优值取决于你的 CPU 核心数和架构:
| CPU 架构 | 推荐线程数 |
|---|---|
| 4 核 Cortex-A53 | 4 |
| 2 核 A72 + 4 核 A53 | 4 或 6 |
| 4 核 A76 + 4 核 A55 | 4(只绑大核)或 8 |
在大小核架构上,只使用大核往往比使用所有核心性能更好,因为 A53 这类小核拖慢整体速度不说,还可能因为调度开销反而降低性能。
NCNN 也支持线程绑定:
ex.set_cpu_powersave(2); // 0=所有核 1=只小核 2=只大核
set_cpu_powersave(2) 是在 ARM 大小核设备上最常用的配置。
5.4 CMakeLists.txt 配置
最后不要忘了写 CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(ncnn_inference)
set(CMAKE_CXX_STANDARD 11)
# NCNN 路径
set(ncnn_DIR "/path/to/ncnn/build/install/lib/cmake/ncnn")
find_package(ncnn REQUIRED)
add_executable(ncnn_inference main.cpp)
target_link_libraries(ncnn_inference ncnn)
六、性能 Benchmark 与优化技巧
模型跑起来只是第一步,跑得多快才是关键。这一节我们来看看如何 benchmark 性能,以及有哪些优化手段。
6.1 使用 ncnn_benchmark
NCNN 自带了 benchmark 工具,可以快速测试模型在目标设备上的性能:
# 编译 benchmark 工具
cd ncnn/build-arm64
cmake .. -DNCNN_BUILD_BENCHMARK=ON
make -j4
# 将 benchmark 可执行文件和模型文件传到设备上
adb push benchmark /data/local/tmp/
adb push resnet18-int8.param /data/local/tmp/
adb push resnet18-int8.bin /data/local/tmp/
# 在设备上运行 benchmark
adb shell
cd /data/local/tmp/
./benchmark resnet18-int8.param resnet18-int8.bin 4 10 1
参数依次是:param 文件、bin 文件、线程数、warmup 次数、运行次数。
6.2 逐层性能分析
如果你想知道模型中哪些层最慢,可以开启逐层耗时统计:
ex.enable_light_mode(false);
ex.set_debug_mode(true);
运行后会打印每一层的执行时间,帮你定位性能瓶颈。
常见的性能瓶颈层:
| 层类型 | 优化方向 |
|---|---|
| Convolution | 用 Winograd 优化(3x3 stride 1) |
| DepthWise Conv | 确保是 im2col+sgemm 实现 |
| Sigmoid/HardSwish | 用 fastmath 版本 |
| Upsample | 避免双线性插值,用 nearest |
6.3 内存优化技巧
嵌入式设备的内存往往比性能还紧张。NCNN 提供了多种内存优化手段:
Light Mode:开启后中间张量会在不需要时立即释放,显著降低峰值内存使用:
ex.enable_light_mode(true);
FP16 存储:即使推理用 FP32,中间结果也可以用 FP16 存储,内存减半:
net.opt.use_fp16_storage = true;
Pack4 优化:对于 4 通道对齐的张量,NCNN 有特殊优化,内存访问更友好:
net.opt.use_packing_layout = true;
这些开关组合使用,通常可以将峰值内存使用降低 30-50%。
七、常见问题与解决方案
部署过程中会遇到各种各样的问题,这里总结一些最常见的坑。
7.1 推理结果不对
这是最常见也是最头疼的问题。排查思路:
- 检查预处理:通道顺序、均值、标准差、归一化是否和训练一致?
- 检查输出后处理:有没有做 Softmax?有没有 sigmoid?
- 逐层对比:用 PyTorch 导出某一层的输出,和 NCNN 同一层的输出对比。
- 检查模型转换:是不是 onnx2ncnn 时某个算子转换错了?
逐层对比是定位问题的杀手锏:
// 在 NCNN 中提取中间层输出
ncnn::Mat conv1_out;
ex.extract("conv1", conv1_out);
// 导出为文本或 numpy 数组,和 PyTorch 对比
7.2 性能不如预期
- 线程数是否合理:试试 1、2、4、8 线程,找最优值。
- 是否绑定了大核:
set_cpu_powersave(2)试试。 - 是否开了 FP16:ARMv8.2+ 设备上 FP16 推理快很多。
- 模型是否经过 ncnnoptimize:BN 融合对性能影响巨大。
- INT8 量化是否生效:确认用的是 int8 版本的模型。
7.3 内存不足
- 开启 light mode:
ex.enable_light_mode(true)。 - 使用 FP16 storage:
net.opt.use_fp16_storage = true。 - 减小 batch size:尽量用 batch 1。
- 模型剪枝:对不重要的通道剪枝。
7.4 部署在内存受限的 MCU
如果是在几 MB 内存的 MCU 上部署,还需要这些额外操作:
- 静态分配内存:不要用动态分配,所有内存都预先分配。
- 权重量化到 INT8:甚至 INT4。
- 权重放在 Flash:运行时按需读取,不全部加载到 RAM。
- 逐层计算:计算完一层就释放输入,只保留输出。
八、进阶方向
掌握了基础部署后,还有很多值得深入的方向:
自定义算子实现:当 NCNN 不支持你的算子时,需要自己写 NCNN 算子。这需要了解 NCNN 的算子注册机制和内存布局。
Vulkan GPU 加速:如果设备有 Mali GPU,开启 Vulkan 后端通常能获得 2-3 倍的性能提升。但需要注意 GPU 和 CPU 之间的数据传输开销。
模型蒸馏与剪枝:量化是无损压缩,剪枝和蒸馏是有损但压缩比更高的手段。结合使用可以在精度下降可接受的前提下,获得极致的性能。
多模型流水线:实际产品中往往不是一个模型在跑,而是检测+跟踪+识别的流水线。如何在多个模型之间合理分配内存和计算资源,也是一个值得研究的课题。
总结
这篇文章从环境搭建开始,完整走过了 ONNX 导出、模型转换、INT8 量化、C++ 推理代码编写、性能 benchmark 的完整流程。回头来看,部署这件事其实没有什么特别高深的理论,更多的是工程细节的堆砌和经验的积累。
从 PyTorch 的一行 model(x) 到嵌入式设备上的 C++ 推理代码,中间隔着几十个大大小小的细节。任何一个细节出问题,都可能导致最终结果不对或者性能不达标。这也是为什么部署工程师这个岗位虽然看起来只是在"搬模型",但实际需要深厚的工程功底。
NCNN 作为一个优秀的推理框架,为我们屏蔽了很多底层的复杂性,但它不是银弹。真正把一个模型部署到产品上,还需要对网络结构、硬件架构、编译器优化、内存管理等等都有一定的理解。这正是嵌入式 AI 的魅力所在——它不是单纯的算法,也不是单纯的工程,而是两者的深度结合。
希望这篇文章能帮助你少踩一些坑,在嵌入式 AI 的道路上走得更顺一些。
(全文完,约7000字)