uniapp - tensorflowjs 之小程序环境集成
# 一、简介
tensorflowjs
(简称 tfjs
)是一个用于使用 JavaScript 进行机器学习开发的库。以下是 tfjs
的官网与 github 仓库:
- 官网:https://www.tensorflow.org/js?hl=zh-cn (opens new window)
- github 仓库:https://github.com/tensorflow/tfjs (opens new window)
本系列主要记录如何在微信小程序环境下集成使用 tfjs
,作为本系列的开篇,也是后续篇章的基础,讲解工程环境集成,喜欢的请点赞关注收藏~
# 二、集成环境
官方说 tfjs
是谷歌开发的机器学习开源项目,致力于为 javascript 提供利用硬件加速的机器学习模型训练和部署。这句话中,硬件加速
这 4 个字是重点,机器学习涉及到大量运算,靠 js 的运算效率(cpu)是达不到理想效果的,所以需要借助额外的技术来提升运算速度,tfjs
使用的 硬件加速
技术有以下两种:
- webgl:基于 OpenGL 的 js 接口实现,采用 GPU 加速。
- wasm(WebAssembly):是一种运行在现代网络浏览器中的新型代码,提供了一种在网络平台以接近本地速度的方式运行多种语言(诸如 C、C++和 Rust 等低级源语言)编写的代码。
注:上述描述摘抄自 维基百科、MDN。
以上两种 web 技术在微信小程序上都有对应的实现和支持。tfjs
对这些 硬件加速
统称为后端(backend
),相对应的,分为 webgl
后端、wasm
后端,这里暂且不管使用哪种硬件加速技术,我们需要先安装一个微信小程序插件,用来初始化 tfjs
。
# 1、添加插件
tfjs
官方封装了一个微信小程序插件 tfjsPlugin
,开发者只需按文档步骤集成即可,以下是该小程序插件的仓库及介绍:
- 小程序插件仓库:https://github.com/tensorflow/tfjs-wechat (opens new window)
- 小程序插件详情:https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx6afed118d9e81df9&lang=zh_CN (opens new window)
注:该插件利用小程序 WebGL API 给第三方小程序调用时提供 GPU 加速。
在使用该插件之前,需要在小程序管理后台,按 "设置-第三方设置-添加插件" 步骤,输入 wx6afed118d9e81df9
找到插件并添加:
# 2、注册插件
uniapp 工程需要在 manifest.json
文件中,找到 mp-weixin
配置项,添加 plugins
信息进行插件注册:
/* 小程序特有相关 */
"mp-weixin": {
...
"plugins": {
"tfjsPlugin": {
"version": "0.2.0",
"provider": "wx6afed118d9e81df9"
}
},
"usingComponents": true
},
注:当前该插件的最新版本是
0.2.0
发布于2021-04-29 12:40:00
。这个库已经很久没更新了,不过目前使用上也没遇到什么大问题~
# 3、添加 tfjs 依赖
经过以上两步之后,工程就可以使用 tfjsPlugin
插件了,在此之前,还有几个 npm 库需要安装:
"@tensorflow/tfjs-backend-webgl": "^3.13.0",
"@tensorflow/tfjs-converter": "^3.13.0",
"@tensorflow/tfjs-core": "^3.13.0",
"fetch-wechat": "^0.0.3",
- @tensorflow/tfjs-backend-webgl:webgl 后端
- @tensorflow/tfjs-converter:GraphModel 导入和执行包
- @tensorflow/tfjs-core:基础包
- fetch-wechat:Polyfill fetch 函数
其中 fetch-wechat
最新版本是 0.0.3,直接使用 npm i fetch-wechat
命令安装,而且另外 3 个 @tensorflow/tfjs-xxx
库最新版本是 4.4.0,需要使用 npm i @tensorflow/tfjs-xxx@^3.13.0
命令指定版本安装。之所以不使用最新的 4.4.0 版本,是因为运行时会报如下错误:
TypeError: env(...).platform.isTypedArray is not a function
搜索一番,没找到任何有用的解决方案,不清楚是不是当前 tfjsPlugin
插件太久没更新,没有适配 4.x 的缘故?不过呢,目前 3.13.0 版本已经非常稳定了,不用太纠结~
# 三、webgl 后端
集成环境之后,就可以来初始化并使用 tfjs
了,下面我们来配置 tfjsPlugin
插件,开启 webgl 硬件加速。
# 1、初始化插件
在 App.vue
文件中,找到 onLaunch()
回调函数,初始化 tfjsPlugin
插件,代码如下:
import { fetchFunc } from "fetch-wechat";
import * as tf from "@tensorflow/tfjs-core";
import * as webgl from "@tensorflow/tfjs-backend-webgl";
const plugin = requirePlugin("tfjsPlugin");
const ENABLE_DEBUG = true; // 是否开启debug,输出调试日志
onLaunch(() => {
initTfjs();
});
function initTfjs() {
plugin.configPlugin(
{
// polyfill fetch function
fetchFunc: fetchFunc(),
// inject tfjs runtime
tf,
// inject webgl backend
webgl,
// provide webgl canvas
canvas: uni.createOffscreenCanvas({}),
// backendName: "wechat-webgl-" + Date.now(),
},
ENABLE_DEBUG
);
}
注:没有限定必须在
App.vue
的onLaunch()
中初始化,只要在使用tfjs
相关 API 之前初始化都可以。
在上述代码中,给插件函数 plugin.configPlugin()
传入了一个初始化对象,其中指定了 webgl 和 canvas 这两个属性,之后,插件就会自动开启 webgl 硬件加速。
# 2、解决 requirePlugin 报错
如果你的 uniapp 工程使用了 typescript ,那么上述代码中 requirePlugin("tfjsPlugin")
处会有红线报错:
Cannot find name 'requirePlugin'.ts(2304)
此处报错是因为 ts 不认识 requirePlugin()
函数,解决这个问题有两种方法:
- 自定义
requirePlugin
函数声明 - 配置微信小程序官方
types
# 1)自定义 requirePlugin
函数声明
在工程的 env.d.ts
文件中,追加如下声明即可:
declare function requirePlugin(pluginName: string): any;
注:在工程 src 目录下的任意 d.ts 文件中声明都是可以的,我习惯将自定义声明写在
env.d.ts
文件中。
声明之后,报错就消失了,这种方法简单粗暴。
# 2)配置微信小程序官方 types
安装 types:
npm install @types/wechat-miniprogram -D
配置 tsconfig.json
:
{
...
"compilerOptions": {
...
"types": ["@dcloudio/types", "@types/wechat-miniprogram"]
},
}
这种方法很优雅,但其实官方 types 中的声明跟我们自己声明的完全一样,用哪种方法都可以~
# 3、测试
tfjsPlugin
插件初始化的同时,也会把 tfjs
初始化好,接下来就可以使用 tfjs
的 API 了:
import * as tf from "@tensorflow/tfjs-core";
console.log(tf.getBackend()); // wechat-webgl
tf.tensor([1, 2, 3, 4]).print(); // Tensor [1, 2, 3, 4]
如果集成环境与插件初始化都没问题的话,那么 tf.getBackend()
拿到的后端字符串就是 wechat-webgl
,否则为 undefined
,并且 tf.tensor()
会报错 Error: No backend found in registry.
。至此,使用 webgl 后端的 tfjs
就可以正常使用了。
# 四、wasm 后端
上面已经成功开启了 webgl 后端,下面我们来探索如何开启 wasm 后端。几年前,微信小程序在 Android 手机上提供了 WebAssembly 的支持,但后来又废弃了,改为一个类似 WebAssembly 的 WXWebAssembly,以下是 WXWebAssembly
的相关链接:
- WXWebAssembly 官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/performance/wasm.html (opens new window)
- 微信小程序废弃 WebAssembly:https://developers.weixin.qq.com/community/develop/doc/000e2c019f8a003d5dfbb54c251c00 (opens new window)
# 1、环境集成
要想使用 wasm 后端,还需要安装一个 tfjs
的 wasm 后端依赖:
npm i @tensorflow/tfjs-backend-wasm@^3.13.0
此外,因为 WXWebAssembly 目前只支持加载包内 wasm 文件(即 网络链接,或者下载后的本地链接都不行),所以还需要把 node_modules/@tensorflow/tfjs-backend-wasm/wasm-out
目录中的 tfjs-backend-wasm.wasm
文件复制到工程 static
目录下(放其他目录的话,编译时可能会被自动过滤掉),然后给 tfjs
关联一下 wasm 文件路径:
import { setWasmPaths } from "@tensorflow/tfjs-backend-wasm";
const usePlatformFetch = true;
setWasmPaths(
{
"tfjs-backend-wasm.wasm": "/static/tfjs-backend-wasm.wasm",
"tfjs-backend-wasm-simd.wasm": "/static/tfjs-backend-wasm.wasm",
"tfjs-backend-wasm-threaded-simd.wasm": "/static/tfjs-backend-wasm.wasm",
},
usePlatformFetch
);
其中 simd.wasm
和 threaded-simd.wasm
是性能增强版的 wasm 文件,关于它们之间的性能对比,可以看下面这篇文章:
- 增强 TensorFlow.js WebAssembly 后端:https://www.infoq.cn/article/hi2vrxfevlelcvvov0g7 (opens new window)
虽然但是,这些性能增强的 wasm 文件在微信小程序环境下运行存在兼容性问题,所以这里全部指定为最原始的 wasm 文件;另外,这些 wasm 文件必须放在小程序包内,会增大小程序包体大小,如果对包体大小有较高要求,还需要去了解如何拆分或压缩 wasm 文件。
# 2、初始化插件
将所需的依赖和 wasm 文件集成好之后,就可以来初始化 tfjsPlugin
插件并开启 wasm 后端了,完整代码如下:
import { fetchFunc } from "fetch-wechat";
import * as tf from "@tensorflow/tfjs-core";
import { setWasmPaths } from "@tensorflow/tfjs-backend-wasm";
const plugin = requirePlugin("tfjsPlugin");
const ENABLE_DEBUG = false;
const usePlatformFetch = true;
setWasmPaths(
{
"tfjs-backend-wasm.wasm": "/static/tfjs-backend-wasm.wasm",
"tfjs-backend-wasm-simd.wasm": "/static/tfjs-backend-wasm.wasm",
"tfjs-backend-wasm-threaded-simd.wasm": "/static/tfjs-backend-wasm.wasm",
},
usePlatformFetch
);
onLaunch(() => {
console.log("App Launch");
initTfjs();
});
function initTfjs() {
plugin.configPlugin(
{
// polyfill fetch function
fetchFunc: fetchFunc(),
// inject tfjs runtime
tf,
// // inject webgl backend
// webgl,
// // provide webgl canvas
// canvas: uni.createOffscreenCanvas({}),
// backendName: "wechat-webgl-" + Date.now(),
},
ENABLE_DEBUG
);
tf.setBackend("wasm").then(() => {
console.log(tf.getBackend()); // wasm
tf.tensor([1, 2, 3, 4]).print(); // Tensor [1, 2, 3, 4]
});
}
注:
plugin.configPlugin()
参数对象中的 webgl 和 canvas 这两个属性不注释掉也是可以的,不影响,这个文章末尾解释。
上述代码中,tf.setBackend("wasm")
是最关键的代码,顾名思义,它的作用就是修改 tfjs
的硬件加速后端为 wasm,其返回结果是一个 Promise,即异步操作,需要等待操作完成后,才能调用 tfjs
相关 API,否则会报错。
# 3、解决 wasm 报错
实际上,按照上述步骤还不能成功运行程序,控制台会报如下错误:
Error: The highest priority backend 'wasm' has not yet been initialized. Make sure to await tf.ready() or await tf.setBackend() before calling other methods
这是一个大坑,很多人按照官方仓库 tfjs-wechat
的说明文档操作,可是死活也没办法成功开启 wasm 后端,这是因为微信小程序把 WebAssembly 废弃,改为 WXWebAssembly,而 tfjs-backend-wasm
这个库中的代码还是使用的 WebAssembly,所以 wasm 后端是不可能初始化成功的。
在 tfjs
的官方仓库 issue 中找到一个解决方法:
他的解决思路是编写一个 rollup
插件,将编译代码中的 WebAssembly
相关部分,修改为 WXWebAssembly
,uniapp 的 vue3 工程使用的是 vite,我们知道 vite 是兼容 rollup 的,所以可以直接在工程的 vite.config.ts
文件中使用此插件,代码如下:
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [uni(), codeTransform()],
});
function codeTransform() {
return {
name: "codeTransform",
transform(code, file) {
// 修复wasm
if (
file.endsWith("tfjs-backend-wasm-threaded-simd.worker.js") ||
file.endsWith("tfjs-backend-wasm-threaded-simd.js")
) {
code = code.replace(`require("worker_threads")`, "null");
code = code.replace(`require("perf_hooks")`, "null");
}
if (file.endsWith("backend_wasm.js")) {
code = code.replace(`env().getAsync('WASM_HAS_SIMD_SUPPORT')`, "false");
code = code.replace(
`env().getAsync('WASM_HAS_MULTITHREAD_SUPPORT')`,
"false"
);
code = code.replace(
`return (imports, callback) => {`,
`return (imports, callback) => {
WebAssembly.instantiate(path, imports).then(output => {
callback(output.instance, output.module);
});
return {};`
);
}
code = code.replace(`WebAssembly.`, `WXWebAssembly.`);
code = code.replace(`typeof WebAssembly`, `typeof WXWebAssembly`);
return { code };
},
};
}
工程重新编译运行之后(让插件生效),就可以正常开启 wasm 后端了。
# 五、其他
此处再对 tfjs
后端做一些补充,本系列代码会同步保存到 github 仓库上:
# 1、动态修改后端
有一点需要明白,插件的 plugin.configPlugin()
初始化函数,它只是一个配置,不管你使用哪个后端,都可以配置上 webgl 和 canvas 这 2 个属性,而 tf.setBackend()
也只是指定 tfjs
后端的一个 API 罢了,如果没有调用,则默认使用 webgl
后端,所以,只要提前配置好 webgl
参数 和 wasm
路径,那么在程序运行时,是可以做到动态修改后端的:
setWasmPaths(
{
"tfjs-backend-wasm.wasm": "/static/tfjs-backend-wasm.wasm",
"tfjs-backend-wasm-simd.wasm": "/static/tfjs-backend-wasm.wasm",
"tfjs-backend-wasm-threaded-simd.wasm": "/static/tfjs-backend-wasm.wasm",
},
usePlatformFetch
);
async function initTfjs() {
plugin.configPlugin(
{
// polyfill fetch function
fetchFunc: fetchFunc(),
// inject tfjs runtime
tf,
// inject webgl backend
webgl,
// provide webgl canvas
canvas: uni.createOffscreenCanvas({}),
// backendName: "wechat-webgl-" + Date.now(),
},
ENABLE_DEBUG
);
await tf.setBackend("wasm");
console.log(tf.getBackend()); // wasm
tf.tensor([1, 2, 3, 4]).print(); // Tensor [1, 2, 3, 4]
await tf.setBackend("wechat-webgl");
console.log(tf.getBackend()); // wechat-webgl
tf.tensor([1, 2, 3, 4]).print(); // Tensor [1, 2, 3, 4]
}
# 2、对比
既然 tfjs
有 webgl 后端和 wasm 后端,那应该使用哪个比较好呢?首先 iOS 的系统环境相差不大,而且目前 WXWebAssembly 对 iOS 平台的支持不如 Android 平台完善,建议统一使用 webgl 后端;其次再来说说 Android 平台,Android 因其开放性,国内 Android 手机厂商会进行个性化定制,甚至有的会进行魔改,所以情况会比较复杂,从我个人目前收集到的资料和反馈来看,优先使用 webgl 后端,大部分的 Android 手机使用 webgl 后端比使用 wasm 后端性能要高得多,个别手机厂商(比如华为)则相反,甚至使用 webgl 后端会出现 tfjs
无法正常使用的情况,建议程序运行时要适当判断机型调整后端,或者界面上提供切换后端的功能。
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08