复杂推理模型从服务器移植到Web浏览器的理论和实战

简介: 随着机器学习的应用面越来越广,能在浏览器中跑模型推理的Javascript框架引擎也越来越多了。在项目中,前端同学可能会找到一些跑在服务端的python算法模型,很想将其直接集成到自己的代码中,以Javascript语言在浏览器中运行。本文就基于pyodide框架,从理论和实战两个角度,帮助前端同学解决复杂模型的移植这一棘手问题。

image.png

作者 | 道仙
来源 | 阿里技术公众号

一 背景

随着机器学习的应用面越来越广,能在浏览器中跑模型推理的Javascript框架引擎也越来越多了。在项目中,前端同学可能会找到一些跑在服务端的python算法模型,很想将其直接集成到自己的代码中,以Javascript语言在浏览器中运行。

对于一部分简单的模型,推理的前处理、后处理比较容易,不涉及复杂的科学计算,碰到这种模型,最多做个模型格式转化,然后用推理框架直接跑就可以了,这种移植成本很低。

而很大一部分模型会涉及复杂的前处理、后处理,包括大量的矩阵运算、图像处理等Python代码。这种情况一般的思路就是用Javascript语言将Python代码手工翻译一遍,这么做的问题是费时费力还容易出错。

Pyodide作为浏览器中的科学计算框架,很好的解决了这个问题:浏览器中运行原生的Python代码进行前、后处理,大量numpy、scipy的矩阵、张量等计算无需翻译为Javascript,为移植节省了很多工作。本文就基于pyodide框架,从理论和实战两个角度,帮助前端同学解决复杂模型的移植这一棘手问题。

二 原理篇

Pyodide是个可以在浏览器中跑的WebAssembly(wasm)应用。它基于CPython的源代码进行了扩展,使用emscripten编译成为wasm,同时也把一大堆科学计算相关的pypi包也编译成了wasm,这样就能在浏览器中解释执行python语句进行科学计算了。所以pyodide也必然遵循wasm的各种约束。Pyodide在浏览器中的位置如下图所示:

image.png

1 wasm内存布局

这是wasm线性内存的布局:

image.png

Data数据段是从0x400开始的, Function Table表也在其中,起始地址为memoryBase(Emscripten中默认为1024,即0x400),STACKTOP为栈地址起始,堆地址起始为STACK_MAX。而我们实际更关心的是Javascript内存与wasm内存的互相访问。

2 Javascript与Python的互访

浏览器基于安全方面的考虑,防止wasm程序把浏览器搞崩溃,通过把wasm运行在一个沙箱化的执行环境中,禁止了wasm程序访问Javascript内存,而Javascript代码却可以访问wasm内存。因为wasm内存本质上是一个巨大的ArrayBuffer,接受Javascript的管理。我们称之为“单向内存访问”。

作为一个wasm格式的普通程序,pyodide被调用起来后,当然只能直接访问wasm内存。

image.png

为了实现互访,pyodide引入了proxy,类似于指针:在Javascript侧,通过一个PyProxy对象来引用python内存里的对象;在Python侧,通过一个JsProxy对象来引用Javascript内存里的对象。

在Javascript侧生成一个PyProxy对象:

const arr_pyproxy = pyodide.globals.get('arr')  // arr是python里的一个全局对象

在Python侧生成一个JsProxy对象:

import js
from js import foo   # foo是Javascript里的一个全局对象

互访时的类型转换分为如下三个等级:

  • 【自动转换】对于简单类型,如数字、字符串、布尔等,会被自动拷贝内存值,此时产生的就不是Proxy、而是最终的值了。
  • 【半自动转换】非简单的内置类型,都需要通过to_js()、to_py()方式来显式转换:

    • 对于Python内置的list、dict、numpy.ndarray等对象,不属于简单类型,不会自动转换类型,必须通过pyodide.to_js()来转,相应的会被转成JS的list、map、TypedArray类型
    • 反过来也类似,通过to_py()方法,JS的TypedArray转为memoryview,list、map转为list、dict
  • 【手动转换】各种class、function和用户自定义类型,因为对方的语言没有对应的现成类型,所以只能以proxy的形式存在,需要通过运算符来间接操纵,就像操纵提线木偶一样。为了达到方便操纵的目的,pyodide对两种语言进行了语法模拟,用一种语言里的操作符模拟另一种语言的类似行为。例如:JS中的let a=new XXX(),在Python中就变为a=XXX.new()。

image.png

这里列举了一部分,详情可以查文档(见文章底部)。

image.png

Javascript的模块也可以引入到Python中,这样Python就能直接调用该模块的接口和方法了。例如,pyodide没有编译opencv包,可以使用opencv.js:

import pyodide
import js.cv as cv2
print(dir(cv2))

这对于pyodide缺失的pypi包是个很好的补充。

三 实践篇

我们从一个空白页面开始。使用浏览器打开测试页面(测试页面见文章底部)。

1 初始化python

为了方便观察运行过程,使用动态的方式加载所需js和执行python代码。打开浏览器控制台,依次运行以下语句:

function loadJS( url, callback ){var script = document.createElement('script'),fn = callback || function(){};script.type = 'text/javascript';script.onload = function(){fn();};script.src = url;document.getElementsByTagName('head')[0].appendChild(script);
}
// 加载opencv
loadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/opencv/opencv.js',  function(){console.log('js load ok');
});// 加载推理引擎onnxruntime.js。当然也可以使用其他推理引擎
loadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/onnxruntime/onnx.min.js',  function(){console.log('js load ok');
});// 初始化python运行环境
loadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/pyodide/0.18.0/pyodide.js',  function(){console.log('js load ok');
});
pyodide = await loadPyodide({ indexURL : "https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/pyodide/0.18.0/"});
await pyodide.loadPackage(['micropip']);

image.png

至此,python和pip就安装完毕了,都位于内存文件系统中。我们可以查看一下python被安装到了哪里:

image.png

注意,这个文件系统是内存里虚拟出来的,刷新页面就丢失了。不过由于浏览器本身有缓存,所以刷新页面后从服务端再次加载pyodide的引导js和主体wasm还是比较快的,只要不清理浏览器缓存。

2 加载pypi包

在pyodide初始化完成后,python系统自带的标准模块可以直接import。第三方模块需要用micropip.install()安装:

  • pypi.org上的纯python包可以用micropip.install() 直接安装
  • 含有C语言扩展(编译为动态链接库)的wheel包,需要对照官方已编译包的列表

    • 在列表中的直接用micropip.install()安装
    • 不在这个列表里的,就需要自己手动编译后发布到服务器后再用micropip.install()安装。

下图展示了业内常用的两种编译为wasm的方式。

image.png

自己编译wasm package的方法可参考官方手册,大致步骤就是pull官方的编译基础镜像,把待编译包的setup.cfg文件放到模块目录里,再加上些hack的语句和配置(如果有的话),然后指定目标进行编译。编译成功后部署时,需要注意2点:

  • 设置允许跨域
  • 对于wasm格式的文件请求,响应Header里应当带上:"Content-type": "application/wasm"

下面是一个自建wasm服务器的nginx/openresty示例配置:

        location ~ ^/wasm/ {add_header 'Access-Control-Allow-Origin' "*";add_header 'Access-Control-Allow-Credentials' "true";root /path/to/wasm_dir;header_filter_by_lua 'uri = ngx.var.uriif string.match(uri, ".js$") == nil thenngx.header["Content-type"] = "application/wasm"end';}

回到我们的推理实例, 现在用pip安装模型推理所需的numpy和Pillow包并将其import:

await pyodide.runPythonAsync(`
import micropip
micropip.install(["numpy", "Pillow"])
`);await pyodide.runPythonAsync(`
import pyodide
import js.cv as cv2
import js.onnx as onnxruntime
import numpy as np
`);

这样python所需的opencv、onnxruntime包就已全部导入了。

3 opencv的使用

一般python里的图片数组都是从JS里传过来的,这里我们模拟构造一张图片,然后用opencv对其resize。上面提到过,pyodide官方的opencv还没编译出来。如果涉及到的opencv方法调用有其他pypi包的替代品,那是最好的:比如,cv.resize可以用Pillow库的PIL.resize代替(注意Pillow的resize速度比opencv的resize要慢);cv.threshold可以用numpy.where代替。 否则只能调用opencv.js的能力了。为了演示pyodide语法,这里都从opencv.js库里调用。

await pyodide.runPythonAsync(`
# 构造一个1080p图片
h,w = 1080,1920
img = np.arange(h * w * 3, dtype=np.uint8).reshape(h, w, 3)# 使用cv2.resize将其缩小为1/10
# 原python代码:small_img = cv2.resize(img, (h_small, w_small))
# 改成调用opencv.js:
h_small,w_small = 108, 192
mat = cv2.matFromArray(h, w, cv2.CV_8UC3, pyodide.to_js(img.reshape(h * w * 3)))
dst = cv2.Mat.new(h_small, w_small, cv2.CV_8UC3)  
cv2.resize(mat, dst, cv2.Size.new(w_small, h_small), 0, 0, cv2.INTER_NEAREST)
small_img = np.asarray(dst.data.to_py()).reshape(h_small, w_small, 3)
`);

传参原则:除了简单的数字、字符串类型可以直接传,其他类型都需要通过pyodide.to_js()转换后再传入。 返回值的获取也类似,除了简单的数字、字符串类型可以直接获取,其他类型都需要通过xx.to_py()转换后获取结果。

接着对一个mask检测其轮廓:

await pyodide.runPythonAsync(`
# 使用cv2.findContours来检测轮廓。假设mask为二维numpy数组,只有0、1两个值
# 原python代码:contours  = cv2.findContours(mask, cv2.RETR_CCOMP,cv2.CHAIN_APPROX_NONE)
# 改成调用opencv.js:
contours_jsproxy = cv2.MatVector.new()   # cv2.Mat数组,对应opencv.js中的 contours = new cv.MatVector()语句
hierarchy_jsproxy = cv2.Mat.new()
mat = cv2.matFromArray(mask.shape[0], mask.shape[1],  cv2.CV_8UC1, pyodide.to_js(mask.reshape(mask.size)))
cv2.findContours(mat, pyodide.to_js(contours_jsproxy), pyodide.to_js(hierarchy_jsproxy), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# contours js格式转python格式
contours = []
for i in range(contours_jsproxy.size()):c_jsproxy = contours_jsproxy.get(i)c = np.asarray(c_jsproxy.data32S.to_py()).reshape(c_jsproxy.rows, c_jsproxy.cols, 2)contours.append(c)
`);

4 推理引擎的使用

最后,用onnx.js加载模型并进行推理,详细语法可参考onnx.js官方文档。其他js版的推理引擎也都可以参考各自的文档。

await pyodide.runPythonAsync(`
model_url="onnx模型的地址"
session = onnxruntime.InferenceSession.new()
session.loadModel(model_url)
session.run(......)
`);

通过以上的操作,我们确保了一切都在python语法范围内进行,这样修改原始的Python文件就比较容易了:把不支持的函数替换成我们自定义的调用js的方法;原Python里的推理替换成调用js版的推理引擎;最后在Javascript主程序框架里加少许调用Python的胶水代码就完成了。

5 挂载持久存储文件系统

有时我们需要对一些数据持久保存,可以利用pyodide提供的持久化文件系统(其实是emscripten提供的),见手册(文章底部)。

// 创建挂载点
pyodide.FS.mkdir('/mnt');
// 挂载文件系统
pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, '/mnt');
// 写入一个文件
pyodide.FS.writeFile('/mnt/test.txt', 'hello world');
// 真正的保存文件到持久文件系统
pyodide.FS.syncfs(function (err) {console.log(err);
});

这样文件就持久保存了。即使当我们刷新页面后,仍可以通过挂载该文件系统来读出里面的内容:

// 创建挂载点
pyodide.FS.mkdir('/mnt');
// 挂载文件系统
pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, '/mnt');
// 写入一个文件
pyodide.FS.writeFile('/mnt/test.txt', 'hello world');
// 真正的保存文件到持久文件系统
pyodide.FS.syncfs(function (err) {console.log(err);
});

运行结果如下:

image.png

当然,以上语句可以在python中以Proxy的语法方式运行。

持久文件系统有很多用处。例如,可以帮我们在多线程(webworker)之间共享大数据;可以把模型文件持久存储到文件系统里,无需每次都通过网络加载。

6 打wheel包

单Python文件无需打包,直接当成一个巨大的字符串,交给pyodide.runPythonAsync()运行就好了。当有多个Python文件时,我们可以把这些python文件打成普通wheel包,部署到webserver,然后可以用micropip直接安装该wheel包:

micropip.install("https://foo.com/bar-1.2.3-xxx.whl")
from bar import ...

注意,打wheel包需要有__init__.py文件,哪怕是个空文件。

四 存在的缺陷

目前pyodide有如下几个缺陷:

  • Python运行环境加载和初始化时间有点儿长,视网络情况,几秒到几十秒都有可能。
  • pypi包支持的不完整。虽然pypi.org上的纯python包都可以直接使用,但涉及到C扩展写的包,如果官方还没编译出来,那就需要自己动手编译了。
  • 个别很常用的包,例如opencv,还没成功编译出来;模型推理框架一个都没有。不过还好可以通过相应的JS库来弥补。
  • 如果python中调用了js库的话:

    • 可能会产生一定的内存拷贝开销(从wasm内存到JS内存的来回拷贝)。尤其是大数组作为参数或返回值,在速度要求高的场合下,额外的内存拷贝开销就不能忽视了。
    • python库的方法接口可能跟其对应的js库的接口参数、返回值格式不一致,有一定的适配工作量。

五 总结

尽管有上述种种缺陷,得益于代码移植的高效率和逻辑上1:1复刻的高可靠性保障,我们还是可以把这种方法运用到多种业务场景里,为推动机器学习技术的应用添砖加瓦。

原文链接
本文为阿里云原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/512281.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国家网络安全宣传周:勒索病毒利如刀,上网备好技能包

简介: 近年来,勒索病毒携带着日趋成熟的手段革新和愈发隐蔽、复杂的“进化”能力,开启了“重装上阵”的疯狂模式,“出镜率”大有提高。由于大型政企机构的网络资产价值高,就成了勒索病毒的头号“猎物”。 政企机构信息…

漫画:什么是“低代码”开发平台?

作者 | 小灰来源 | 程序员小灰什么是低代码?其实,这并不是最近才出现的新概念。自从计算机诞生以来,人们就一直在想方设法提升计算机程序编写的效率。从最初的机器语言,发展到现在的C、Java、Python等高级语言;从完全独…

OpenKruise 如何实现应用的可用性防护?

简介: OpenKruise 在 2021.9.6 发布了最新的 v0.10.0 版本新增了弹性拓扑管理和应用安全防护等能力,本文将为大家揭晓 OpenKruise 是如何实现应用的可用性防护能力。 前言 OpenKruise 是阿里云开源的云原生应用自动化管理套件,也是当前托管…

Serverless 工程实践 | Serverless 应用优化与调试秘诀

简介: 本文将以阿里云函数计算为例,提供了在线调试、本地调试等多种应用优化与调试方案。 作者|刘宇 前言:本文将以阿里云函数计算为例,提供了在线调试、本地调试等多种应用优化与调试方案。 Serverless 应用调试秘…

新一代容器平台ACK Anywhere,来了

简介: 近日,阿里云容器服务全面升级为ACK Anywhere,让企业在任何需要云的地方,都能获得一致的容器基础设施能力。 5G、AR、AIoT 等场景在推动新一代云架构的演进,而容器重塑了云的使用方式。 近日,阿里云…

高密自智,体小量大,希捷Exos Corvault存储系统为数据洞察赋能

2022年2月24日——全球领先的海量数据存储基础设施解决方案提供商希捷科技(NASDAQ:STX)在线上举办了主题为“高密自智,体小量大”的新一代PB级自修复存储系统——Exos Corvault新品鉴赏会。 此次鉴赏会邀请到了E企研究院首席研究…

Apache Flink 在汽车之家的应用与实践

简介: 汽车之家如何基于 Flink 上线了 AutoStream 平台并持续打磨。 本文整理自汽车之家实时计算平台负责人邸星星在 Flink Forward Asia 2020 分享的议题《Apache Flink 在汽车之家的应用及实践》。主要内容包括: 背景及现状AutoStream 平台基于 Flink …

以色列安全厂商Check Point发布全新logo与重要安全新品,持续深耕中国市场

作者 | 宋慧 出品 | CSDN 云计算 2022年伊始,以色列老牌安全厂商Check Point在自家主办的安全行业大会CPX360上,正式宣布将启用全新企业logo以及企业口号。 Check Point公司中国区总经理陈石磊在对国内媒体分享时指出:“公司1993年成立时&am…

Facebook宕机背后,我们该如何及时发现DNS问题

简介: 国庆期间,Facebook 及其旗下 Instagram 和 WhatsApp 等应用全网宕机,停机时间将近 7 小时 5 分钟,Facebook 市值损失 643 亿美元。针对Facebook的宕机问题,我们该如何未雨绸缪,看看云拨测如何帮助客户…

KubeVela 1.1 发布,开启混合环境应用交付新里程碑

简介: KubeVela 作为一个开箱即用、面向现代微服务架构的应用交付与管理平台,今天正式发布了 1.1 版本,以更加用户友好和完善的功能集,开启了“让混合环境应用交付更加简单高效”的重要里程碑。 在云原生理念迅速普及的今天&…

云原生消息、事件、流超融合平台——RocketMQ 5.0 初探

简介: 今天分享的主题是云原生消息事件流超融合平台 RocketMQ 5.0 初探,内容主要分为三个部分: 首先,带大家回顾业务消息领域首选 RocketMQ 4 发展历史以及 4.x 版本的演进与发展。 其次,会为大家详细介绍 RocketMQ 5.…

mysql查找无根节点sql_SQL 双亲节点查找所有子节点的实现方法

怎么保存树状结构的数据呢?在 SQL 中常用的是双亲节点法。创建表如下CREATE TABLE category ( id LONG, parentId LONG, name String(20) )INSERT INTO category VALUES ( 1, NULL, Root )INSERT INTO category VALUES ( 2, 1, Branch1 )INSERT INTO category VALUE…

一文看懂微服务背后的技术演进与应用实践

简介: 2021年7月2日,阿里云用户组(AUG)第一次线下活动在济南召开。阿里云云原生资深专家李国强结合自身微服务领域经验,现场跟数十家山东企业分享了云原生的代表技术之一“微服务”的演进和应用实践。本文根据作者的现…

1 分钟记住 docker 镜像和容器常用基本命令

作者 | xiaochuhe来源 | CSDN博客镜像常用基本命令查看自己服务器中docker 镜像列表docker images搜索镜像docker search 镜像名 docker search --filterSTARS9000 mysql 搜索 STARS >9000的 mysql 镜像拉取镜像docker pull 镜像名 docker pull 镜像名:tag运行镜像docker ru…

业界首个机密计算容器运行时—Inclavare Containers正式进入CNCF!

简介: Inclavare Containers 通过云原生计算基金会(CNCF)TOC 投票正式成为 CNCF 官方沙箱项目。 作者|彦荣 2021 年 9月 15 日,Inclavare Containers 通过云原生计算基金会(CNCF)TOC 投票正式成…

python 矩阵库_NumPy 矩阵库(Matrix)

NumPy 中包含了一个矩阵库 numpy.matlib,该模块中的函数返回的是一个矩阵,而不是 ndarray 对象。由 m n 个数aij排成的 m 行 n 列的数表称为 m 行 n 列的矩阵,简称 m n 矩阵。记作这 mn 个数称为矩阵 A的元素,简称为元&#xff…

顺丰科技 Hudi on Flink 实时数仓实践

简介: 介绍了顺丰科技数仓的架构,趟过的一些问题、使用 Hudi 来优化整个 job 状态的实践细节,以及未来的一些规划。 本文作者为刘杰,介绍了顺丰科技数仓的架构,趟过的一些问题、使用 Hudi 来优化整个 job 状态的实践细…

Paillier半同态加密:原理、高效实现方法和应用

简介: 《数据安全法》已于9月1日起正式实施,两个月后《个人信息保护法》也将开始施行,意味着数据安全和隐私保护方面的监管将会在年内陆续到位。在合规收紧大背景下,“数据孤岛”现象日渐明显。如何实现安全的数据流通&#xff0c…

django给mysql配主从_django中的mysql主从读写分离:一、配置mysql主从分离

一、配置mysql主从同步的步骤:(1) 在主服务器上,必须开启二进制日志机制和配置一个独立的ID(2) 在每一个从服务器上,配置一个唯一的ID,创建一个用来专门复制主服务器数据的账号(3) 在开始复制进程前,在主服务器上记录二…

从 40% 跌至 4%,“糊”了的 Firefox 还能重回巅峰吗?

作者 | 丁广辉 责编 | 张红月出品 | CSDN(ID:CSDNnews)Mozilla Firefox,通常简称为Firefox,中文名叫做火狐,是由Mozilla基金会及其子公司Mozilla公司开发的一款自由、开源的网页浏览器。2004年&#x…