首页 系统 复杂推理模型从服务器移植到Web浏览器的理论和实践

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

一 背景
ASP站长网随着机器学习的应用面越来越广,能在浏览器中跑模型推理的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还是比较快的,只要不清理浏览器缓存。

关于作者: dawei

【声明】:九江站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。

热门文章