热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

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

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

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

一 背景

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

对于一部分简略的模型,推理的前解决、后处理比拟容易,不波及简单的科学计算,碰到这种模型,最多做个模型格局转化,而后用推理框架间接跑就能够了,这种移植老本很低。

而很大一部分模型会波及简单的前解决、后处理,包含大量的矩阵运算、图像处理等Python代码。这种状况个别的思路就是用Javascript语言将Python代码手工翻译一遍,这么做的问题是费时费力还容易出错。

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

二 原理篇

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

1 wasm内存布局

这是wasm线性内存的布局:

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内存。

为了实现互访,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()。

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

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']);

至此,python和pip就装置结束了,都位于内存文件系统中。咱们能够查看一下python被装置到了哪里:

留神,这个文件系统是内存里虚构进去的,刷新页面就失落了。不过因为浏览器自身有缓存,所以刷新页面后从服务端再次加载pyodide的疏导js和主体wasm还是比拟快的,只有不清理浏览器缓存。

2 加载pypi包

在pyodide初始化实现后,python零碎自带的规范模块能够间接import。第三方模块须要用micropip.install()装置:

pypi.org上的纯python包能够用micropip.install() 间接装置
含有C语言扩大(编译为动态链接库)的wheel包,须要对照官网已编译包的列表

  • 在列表中的间接用micropip.install()装置
  • 不在这个列表里的,就须要本人手动编译后公布到服务器后再用micropip.install()装置。

下图展现了业内罕用的两种编译为wasm的形式。

本人编译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.uri
     if string.match(uri, ".js$") == nil then
         ngx.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);
});

运行后果如下:

当然,以上语句能够在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复刻的高可靠性保障,咱们还是能够把这种办法使用到多种业务场景里,为推动机器学习技术的利用添砖加瓦。

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


推荐阅读
  • Python语言简介、环境搭建、PyCharm的安装和配置
    一、Python语言简介1.1Python语言的基本概念官方对Python语言的介绍如下:Python是一款易于学习且功能强大的编程语言。它具有高效率的数据结构,能够简单又有效地实 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • 开发笔记:python协程的理解
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了python协程的理解相关的知识,希望对你有一定的参考价值。一、介绍什么是并发?并发的本质就是 ... [详细]
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文讨论了如何使用GStreamer来删除H264格式视频文件中的中间部分,而不需要进行重编码。作者提出了使用gst_element_seek(...)函数来实现这个目标的思路,并提到遇到了一个解决不了的BUG。文章还列举了8个解决方案,希望能够得到更好的思路。 ... [详细]
  • 本文介绍了使用C++Builder实现获取USB优盘序列号的方法,包括相关的代码和说明。通过该方法,可以获取指定盘符的USB优盘序列号,并将其存放在缓冲中。该方法可以在Windows系统中有效地获取USB优盘序列号,并且适用于C++Builder开发环境。 ... [详细]
  • Ihavebeenworkingwithbufferingafileonmylocaldrivetoparseandobtaincertaindata.Forte ... [详细]
  • 丛api的python的简单介绍
    本文目录一览:1、如何使用python利用api获取天气预报 ... [详细]
  • pandas的自带数据集_Pandas到底是个怎样的包?
    sh说明:本pandas非卧龙的pandas,而是Python众多科学计算包中的pandas。本次Pandas的简洁介绍,针对的是此包的新手࿰ ... [详细]
author-avatar
手机用户2502869145_913
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有