以下内容将介绍如何将大模型转换为 NCNN 格式并在 微信小程序 中进行调用。我们会从整体流程、模型转换工具、NCNN WebAssembly(WASM)编译与集成、小程序前端代码示例等方面进行详细讲解,并在最后给出优化方向与未来建议。
随着深度学习的发展,大模型在各种任务(如图像识别、目标检测、NLP 等)中表现优异。但在资源受限的移动端和小程序环境下,常规大模型往往在算力、内存和网络下载量等方面具有较大挑战。为此,腾讯开源的 NCNN 推理框架提供了在移动端、嵌入式甚至 WebAssembly(浏览器、小程序)环境中运行高效推理的能力。
总体流程:
大多数深度学习框架(PyTorch、TensorFlow、PaddlePaddle 等)都可以先导出为 ONNX 格式,然后通过官方的 onnx2ncnn
工具将其转成 NCNN 专用的 .param
和 .bin
文件。这也是 NCNN 官方推荐的路线之一。若是直接使用 Caffe / ncnn 原生支持,也可用 caffe2ncnn、ncnnoptimize 等,但这里重点介绍 ONNX -> NCNN 这条通用路径。
安装 onnx2ncnn
tools/onnx
目录可找到 onnx2ncnn 的 CMake 工程。onnx2ncnn
二进制加入到环境变量或复制到你常用的路径里。NCNN 源码编译(后续做 WASM)
WeChat 小程序开发者工具
假设你已经有一个大模型(例如图像分类的 ResNet50 或其他网络),并成功导出了 model.onnx
。以下 PyTorch 示例仅作参考:
import torch
import torchvision
model = torchvision.models.resnet50(pretrained=True)
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
"model.onnx",
input_names=["input"],
output_names=["output"],
opset_version=11
)
成功后,你将得到一个 model.onnx
文件。
运行命令:
onnx2ncnn model.onnx model.param model.bin
这将生成 NCNN 的两个文件:
model.param
:NCNN 网络结构model.bin
:模型权重可选的优化:
ncnnoptimize model.param model.bin model-opt.param model-opt.bin 65536
ncnnoptimize
能对网络进行一些融合和优化,比如算子融合、常量折叠等。确保你最终准备了 model.param
(或 model-opt.param
) 和 model.bin
(或 model-opt.bin
)。
为什么需要 WASM?
ncnn.js
示例,底层是通过 Emscripten 将 NCNN 编译为 WebAssembly。核心步骤:
安装 Emscripten:详见 Emscripten 官方文档。
使用 CMake 配置并编译:在 ncnn 源码目录下,执行类似:
mkdir build-wasm
cd build-wasm
emcmake cmake -DNCNN_VULKAN=OFF -DNCNN_WEBASSEMBLY=ON -DNCNN_THREADS=OFF -DNCNN_OPENMP=OFF ..
emmake make -j4
NCNN_WEBASSEMBLY=ON
:启用 WASM 编译NCNN_THREADS=OFF
、NCNN_OPENMP=OFF
:多线程特性在部分小程序环境可能无法使用,需要关闭build-wasm
目录下生成 ncnn.js
和 ncnn.wasm
等文件(具体名称和目录根据版本可能不同)。裁剪与减小体积:
.wasm
文件体积。miniprogram/libs/ncnn/
下,或其他合适的目录。WX.createSelectorQuery()
或 FileSystemManager
)读取 .param 和 .bin 文件,再传给 ncnn.js。整体逻辑:
下面给出一个最简化的例子,演示如何在微信小程序中调用 NCNN WASM 做一次推理。示例做了如下假设:
ncnn.js
+ ncnn.wasm
model.param
和 model.bin
假设你的 小程序 项目结构如下(只列出关键部分):
my-weapp/
├─ miniprogram/
│ ├─ libs/
│ │ └─ ncnn/
│ │ ├─ ncnn.js
│ │ ├─ ncnn.wasm
│ │ ├─ model.param (NCNN模型结构)
│ │ ├─ model.bin (NCNN模型权重)
│ ├─ pages/
│ │ └─ index/
│ │ ├─ index.js
│ │ ├─ index.wxml
│ │ ├─ index.wxss
│ ├─ app.js
│ ├─ app.json
├─ project.config.json
以 pages/index/index.js
为例,演示如何在 onLoad 时初始化 ncnn,并在点击按钮时进行推理。具体逻辑可根据项目需求调整。
// pages/index/index.js
Page({
data: {
resultText: "Inference result will show here",
imagePath: "", // 用于展示推理图像
},
onLoad() {
this._initNCNN();
},
// 1. 初始化 ncnn.js
async _initNCNN() {
// 载入 ncnn.js
// 注意:小程序中使用 require 或 import, 需要根据实际情况配置
this.ncnnModule = require("../../libs/ncnn/ncnn.js")();
// 也可能需要采用动态加载或 promisify 方式,这里仅演示
// 等待 ncnnModule 加载完成
if (!this.ncnnModule) {
console.error("Failed to load ncnn.js");
return;
}
console.log("NCNN WASM module loaded.");
},
// 2. 选择本地图片
chooseImage() {
wx.chooseImage({
count: 1,
success: (res) => {
if (res.tempFilePaths.length > 0) {
this.setData({
imagePath: res.tempFilePaths[0],
});
}
},
});
},
// 3. 执行推理
async runInference() {
if (!this.data.imagePath || !this.ncnnModule) {
wx.showToast({ title: "No image or ncnn not initialized", icon: "none" });
return;
}
// 读取模型文件并执行分类
try {
// 加载模型 .param / .bin
// 小程序需先读取文件到 ArrayBuffer, 再传给ncnn
const fs = wx.getFileSystemManager();
// model.param
const paramPath = `${wx.env.USER_DATA_PATH}/model.param`;
await this._copyToUserDataPath("/libs/ncnn/model.param", paramPath);
const paramBuffer = fs.readFileSync(paramPath);
// model.bin
const binPath = `${wx.env.USER_DATA_PATH}/model.bin`;
await this._copyToUserDataPath("/libs/ncnn/model.bin", binPath);
const binBuffer = fs.readFileSync(binPath);
// 初始化 NCNN Net
const net = new this.ncnnModule.Net();
net.load_param(new Uint8Array(paramBuffer));
net.load_model(new Uint8Array(binBuffer));
// 加载并预处理图像
const imageData = await this._loadImageAsRGBA(this.data.imagePath, 224, 224);
// imageData为Uint8ClampedArray(RGBA), 需要转 float 并去掉A
// 创建 Mat
const inputMat = this._createMatFromImageData(imageData, 224, 224, this.ncnnModule);
// 推理
const extractor = net.create_extractor();
extractor.input("input", inputMat);
const { ptr } = extractor.extract("output"); // 输出节点名根据你的模型
// ptr 是在 WASM 内存中的浮点指针
// 假设输出是 [1, 1000] 的分类结果
const resultArray = new Float32Array(this.ncnnModule.HEAPF32.buffer, ptr, 1000);
// 找到最大值和对应索引
let maxIndex = 0, maxValue = resultArray[0];
for (let i = 1; i < resultArray.length; i++) {
if (resultArray[i] > maxValue) {
maxValue = resultArray[i];
maxIndex = i;
}
}
this.setData({
resultText: `Class Index: ${maxIndex}, Score: ${maxValue.toFixed(4)}`,
});
// 释放资源
inputMat.delete();
net.delete();
} catch (e) {
console.error("Inference error:", e);
this.setData({ resultText: "Error in inference: " + e });
}
},
// 工具函数:复制小程序内资源到 userDataPath
_copyToUserDataPath(srcPath, destPath) {
return new Promise((resolve, reject) => {
wx.getFileSystemManager().copyFile({
srcPath: srcPath, // 形如 "小程序项目根目录/libs/ncnn/model.param"
destPath: destPath,
success: () => resolve(),
fail: (err) => reject(err),
});
});
},
// 工具函数:将小程序图片加载为 RGBA 图像数据
_loadImageAsRGBA(imagePath, width, height) {
return new Promise((resolve, reject) => {
// 需要离屏Canvas (2D) 来绘制并获取像素
const query = wx.createSelectorQuery();
// 假设在当前页面有一个
query.select("#tempCanvas")
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
const img = canvas.createImage();
img.onload = () => {
// 设置 Canvas 尺寸
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height).data;
resolve(imageData);
};
img.onerror = reject;
img.src = imagePath;
});
});
},
// 将 RGBA 转为 ncnn Mat (float32)
_createMatFromImageData(rgbaData, w, h, ncnnModule) {
// 假设通道顺序RGB, 每通道 float
const channels = 3;
// 申请 ncnn 的 Mat
const size = w * h * channels;
const bufferPtr = ncnnModule._malloc(size * 4); // float32 -> 4 bytes each
const f32Buffer = new Float32Array(ncnnModule.HEAPF32.buffer, bufferPtr, size);
let idx = 0;
for (let i = 0; i < w * h; i++) {
const r = rgbaData[4 * i] / 255.0;
const g = rgbaData[4 * i + 1] / 255.0;
const b = rgbaData[4 * i + 2] / 255.0;
// A通道 rgbaData[4*i+3] 通常不需要
f32Buffer[idx++] = r;
f32Buffer[idx++] = g;
f32Buffer[idx++] = b;
}
// 创建 Mat
const mat = new ncnnModule.Mat(w, h, channels, bufferPtr, 4); // 4=elemSize of float
return mat;
},
});
配套的 index.wxml
示例可简单写:
{{resultText}}
上述示例展示了一个简化的推理流程。实际项目中需根据你的大模型结构、输入形状、算子需求等进行修改。
model.param
/ model.bin
放在服务器,首次启动时下载到 wx.env.USER_DATA_PATH
,减小小程序包体积(有 2M / 4M / 8M 限制)。通过以上步骤,就能将大模型(或精简后的模型)转换为 NCNN 格式并部署到微信小程序中。
onnx2ncnn
得到 .param + .bin
。ncnn.js + ncnn.wasm
形式集成到小程序。对于真正的大模型,需结合量化、剪枝、知识蒸馏等手段进行瘦身,以平衡推理速度、内存占用和模型精度。随着微信小程序对 WebAssembly 多线程和硬件加速的逐步开放,以及 ncnn 对更多低比特量化和 SIMD 优化的支持,未来在端侧也能运行更大更复杂的模型推理。祝你在实际项目中部署顺利!
【哈佛博后带小白玩转机器学习】 哔哩哔哩_bilibili
总课时超400+,时长75+小时