日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
復(fù)雜推理模型從服務(wù)器移植到Web瀏覽器的理論和實(shí)戰(zhàn)

一 背景

隨著機(jī)器學(xué)習(xí)的應(yīng)用面越來越廣,能在瀏覽器中跑模型推理的Javascript框架引擎也越來越多了。在項(xiàng)目中,前端同學(xué)可能會(huì)找到一些跑在服務(wù)端的python算法模型,很想將其直接集成到自己的代碼中,以Javascript語言在瀏覽器中運(yùn)行。

創(chuàng)新互聯(lián)成都網(wǎng)站建設(shè)按需開發(fā),是成都網(wǎng)站開發(fā)公司,為石雕提供網(wǎng)站建設(shè)服務(wù),有成熟的網(wǎng)站定制合作流程,提供網(wǎng)站定制設(shè)計(jì)服務(wù):原型圖制作、網(wǎng)站創(chuàng)意設(shè)計(jì)、前端HTML5制作、后臺(tái)程序開發(fā)等。成都網(wǎng)站營(yíng)銷推廣熱線:13518219792

對(duì)于一部分簡(jiǎn)單的模型,推理的前處理、后處理比較容易,不涉及復(fù)雜的科學(xué)計(jì)算,碰到這種模型,最多做個(gè)模型格式轉(zhuǎn)化,然后用推理框架直接跑就可以了,這種移植成本很低。

而很大一部分模型會(huì)涉及復(fù)雜的前處理、后處理,包括大量的矩陣運(yùn)算、圖像處理等Python代碼。這種情況一般的思路就是用Javascript語言將Python代碼手工翻譯一遍,這么做的問題是費(fèi)時(shí)費(fèi)力還容易出錯(cuò)。

Pyodide作為瀏覽器中的科學(xué)計(jì)算框架,很好的解決了這個(gè)問題:瀏覽器中運(yùn)行原生的Python代碼進(jìn)行前、后處理,大量numpy、scipy的矩陣、張量等計(jì)算無需翻譯為Javascript,為移植節(jié)省了很多工作。本文就基于pyodide框架,從理論和實(shí)戰(zhàn)兩個(gè)角度,幫助前端同學(xué)解決復(fù)雜模型的移植這一棘手問題。

二 原理篇

Pyodide是個(gè)可以在瀏覽器中跑的WebAssembly(wasm)應(yīng)用。它基于CPython的源代碼進(jìn)行了擴(kuò)展,使用emscripten編譯成為wasm,同時(shí)也把一大堆科學(xué)計(jì)算相關(guān)的pypi包也編譯成了wasm,這樣就能在瀏覽器中解釋執(zhí)行python語句進(jìn)行科學(xué)計(jì)算了。所以pyodide也必然遵循wasm的各種約束。Pyodide在瀏覽器中的位置如下圖所示:??

??

1 wasm內(nèi)存布局

這是wasm線性內(nèi)存的布局:

?

??

??

Data數(shù)據(jù)段是從0x400開始的, Function Table表也在其中,起始地址為memoryBase(Emscripten中默認(rèn)為1024,即0x400),STACKTOP為棧地址起始,堆地址起始為STACK_MAX。而我們實(shí)際更關(guān)心的是Javascript內(nèi)存與wasm內(nèi)存的互相訪問。

2 Javascript與Python的互訪

瀏覽器基于安全方面的考慮,防止wasm程序把瀏覽器搞崩潰,通過把wasm運(yùn)行在一個(gè)沙箱化的執(zhí)行環(huán)境中,禁止了wasm程序訪問Javascript內(nèi)存,而Javascript代碼卻可以訪問wasm內(nèi)存。因?yàn)閣asm內(nèi)存本質(zhì)上是一個(gè)巨大的ArrayBuffer,接受Javascript的管理。我們稱之為“單向內(nèi)存訪問”。

作為一個(gè)wasm格式的普通程序,pyodide被調(diào)用起來后,當(dāng)然只能直接訪問wasm內(nèi)存。

?

??

??

為了實(shí)現(xiàn)互訪,pyodide引入了proxy,類似于指針:在Javascript側(cè),通過一個(gè)PyProxy對(duì)象來引用python內(nèi)存里的對(duì)象;在Python側(cè),通過一個(gè)JsProxy對(duì)象來引用Javascript內(nèi)存里的對(duì)象。

在Javascript側(cè)生成一個(gè)PyProxy對(duì)象:

const arr_pyproxy = pyodide.globals.get('arr')  // arr是python里的一個(gè)全局對(duì)象

在Python側(cè)生成一個(gè)JsProxy對(duì)象:

import js 
from js import foo # foo是Javascript里的一個(gè)全局對(duì)象

互訪時(shí)的類型轉(zhuǎn)換分為如下三個(gè)等級(jí):

  • 【自動(dòng)轉(zhuǎn)換】對(duì)于簡(jiǎn)單類型,如數(shù)字、字符串、布爾等,會(huì)被自動(dòng)拷貝內(nèi)存值,此時(shí)產(chǎn)生的就不是Proxy、而是最終的值了。
  • 【半自動(dòng)轉(zhuǎn)換】非簡(jiǎn)單的內(nèi)置類型,都需要通過to_js()、to_py()方式來顯式轉(zhuǎn)換:
  • 對(duì)于Python內(nèi)置的list、dict、numpy.ndarray等對(duì)象,不屬于簡(jiǎn)單類型,不會(huì)自動(dòng)轉(zhuǎn)換類型,必須通過pyodide.to_js()來轉(zhuǎn),相應(yīng)的會(huì)被轉(zhuǎn)成JS的list、map、TypedArray類型
  • 反過來也類似,通過to_py()方法,JS的TypedArray轉(zhuǎn)為memoryview,list、map轉(zhuǎn)為list、dict
  • 【手動(dòng)轉(zhuǎn)換】各種class、function和用戶自定義類型,因?yàn)閷?duì)方的語言沒有對(duì)應(yīng)的現(xiàn)成類型,所以只能以proxy的形式存在,需要通過運(yùn)算符來間接操縱,就像操縱提線木偶一樣。為了達(dá)到方便操縱的目的,pyodide對(duì)兩種語言進(jìn)行了語法模擬,用一種語言里的操作符模擬另一種語言的類似行為。例如:JS中的let a=new XXX(),在Python中就變?yōu)閍=XXX.new()。

?

??

??

這里列舉了一部分,詳情可以查文檔(見文章底部)。

?

??

??

Javascript的模塊也可以引入到Python中,這樣Python就能直接調(diào)用該模塊的接口和方法了。例如,pyodide沒有編譯opencv包,可以使用opencv.js:

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

這對(duì)于pyodide缺失的pypi包是個(gè)很好的補(bǔ)充。

三 實(shí)踐篇

我們從一個(gè)空白頁面開始。使用瀏覽器打開測(cè)試頁面(測(cè)試頁面見文章底部)。

1 初始化python

為了方便觀察運(yùn)行過程,使用動(dòng)態(tài)的方式加載所需js和執(zhí)行python代碼。打開瀏覽器控制臺(tái),依次運(yùn)行以下語句:

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。當(dāng)然也可以使用其他推理引擎
loadJS('https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/onnxruntime/onnx.min.js', function(){
console.log('js load ok');
});

// 初始化python運(yùn)行環(huán)境
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就安裝完畢了,都位于內(nèi)存文件系統(tǒng)中。我們可以查看一下python被安裝到了哪里:

?

??

??

注意,這個(gè)文件系統(tǒng)是內(nèi)存里虛擬出來的,刷新頁面就丟失了。不過由于瀏覽器本身有緩存,所以刷新頁面后從服務(wù)端再次加載pyodide的引導(dǎo)js和主體wasm還是比較快的,只要不清理瀏覽器緩存。

2 加載pypi包

在pyodide初始化完成后,python系統(tǒng)自帶的標(biāo)準(zhǔn)模塊可以直接import。第三方模塊需要用micropip.install()安裝:

  • pypi.org上的純python包可以用micropip.install() 直接安裝
  • 含有C語言擴(kuò)展(編譯為動(dòng)態(tài)鏈接庫(kù))的wheel包,需要對(duì)照官方已編譯包的列表
  • 在列表中的直接用micropip.install()安裝
  • 不在這個(gè)列表里的,就需要自己手動(dòng)編譯后發(fā)布到服務(wù)器后再用micropip.install()安裝。

下圖展示了業(yè)內(nèi)常用的兩種編譯為wasm的方式。

?

??

??

自己編譯wasm package的方法可參考官方手冊(cè),大致步驟就是pull官方的編譯基礎(chǔ)鏡像,把待編譯包的setup.cfg文件放到模塊目錄里,再加上些hack的語句和配置(如果有的話),然后指定目標(biāo)進(jìn)行編譯。編譯成功后部署時(shí),需要注意2點(diǎn):

  • 設(shè)置允許跨域
  • 對(duì)于wasm格式的文件請(qǐng)求,響應(yīng)Header里應(yīng)當(dāng)帶上:"Content-type": "application/wasm"

下面是一個(gè)自建wasm服務(wù)器的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
';
}

回到我們的推理實(shí)例, 現(xiàn)在用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包就已全部導(dǎo)入了。

3 opencv的使用

一般python里的圖片數(shù)組都是從JS里傳過來的,這里我們模擬構(gòu)造一張圖片,然后用opencv對(duì)其resize。上面提到過,pyodide官方的opencv還沒編譯出來。如果涉及到的opencv方法調(diào)用有其他pypi包的替代品,那是最好的:比如,cv.resize可以用Pillow庫(kù)的PIL.resize代替(注意Pillow的resize速度比opencv的resize要慢);cv.threshold可以用numpy.where代替。 否則只能調(diào)用opencv.js的能力了。為了演示pyodide語法,這里都從opencv.js庫(kù)里調(diào)用。

await pyodide.runPythonAsync(` 
# 構(gòu)造一個(gè)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))
# 改成調(diào)用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)
`);

傳參原則:除了簡(jiǎn)單的數(shù)字、字符串類型可以直接傳,其他類型都需要通過pyodide.to_js()轉(zhuǎn)換后再傳入。 返回值的獲取也類似,除了簡(jiǎn)單的數(shù)字、字符串類型可以直接獲取,其他類型都需要通過xx.to_py()轉(zhuǎn)換后獲取結(jié)果。

接著對(duì)一個(gè)mask檢測(cè)其輪廓:

await pyodide.runPythonAsync(` 
# 使用cv2.findContours來檢測(cè)輪廓。假設(shè)mask為二維numpy數(shù)組,只有0、1兩個(gè)值
# 原python代碼:contours = cv2.findContours(mask, cv2.RETR_CCOMP,cv2.CHAIN_APPROX_NONE)
# 改成調(diào)用opencv.js:
contours_jsproxy = cv2.MatVector.new() # cv2.Mat數(shù)組,對(duì)應(yīng)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格式轉(zhuǎn)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加載模型并進(jìn)行推理,詳細(xì)語法可參考o(jì)nnx.js官方文檔。其他js版的推理引擎也都可以參考各自的文檔。

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

通過以上的操作,我們確保了一切都在python語法范圍內(nèi)進(jìn)行,這樣修改原始的Python文件就比較容易了:把不支持的函數(shù)替換成我們自定義的調(diào)用js的方法;原Python里的推理替換成調(diào)用js版的推理引擎;最后在Javascript主程序框架里加少許調(diào)用Python的膠水代碼就完成了。

5 掛載持久存儲(chǔ)文件系統(tǒng)

有時(shí)我們需要對(duì)一些數(shù)據(jù)持久保存,可以利用pyodide提供的持久化文件系統(tǒng)(其實(shí)是emscripten提供的),見手冊(cè)(文章底部)。

// 創(chuàng)建掛載點(diǎn) 
pyodide.FS.mkdir('/mnt');
// 掛載文件系統(tǒng)
pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, '/mnt');
// 寫入一個(gè)文件
pyodide.FS.writeFile('/mnt/test.txt', 'hello world');
// 真正的保存文件到持久文件系統(tǒng)
pyodide.FS.syncfs(function (err) {
console.log(err);
});

這樣文件就持久保存了。即使當(dāng)我們刷新頁面后,仍可以通過掛載該文件系統(tǒng)來讀出里面的內(nèi)容:

// 創(chuàng)建掛載點(diǎn) 
pyodide.FS.mkdir('/mnt');
// 掛載文件系統(tǒng)
pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, '/mnt');
// 寫入一個(gè)文件
pyodide.FS.writeFile('/mnt/test.txt', 'hello world');
// 真正的保存文件到持久文件系統(tǒng)
pyodide.FS.syncfs(function (err) {
console.log(err);
});

運(yùn)行結(jié)果如下:

?

??

??

當(dāng)然,以上語句可以在python中以Proxy的語法方式運(yùn)行。

持久文件系統(tǒng)有很多用處。例如,可以幫我們?cè)诙嗑€程(webworker)之間共享大數(shù)據(jù);可以把模型文件持久存儲(chǔ)到文件系統(tǒng)里,無需每次都通過網(wǎng)絡(luò)加載。

6 打wheel包

單Python文件無需打包,直接當(dāng)成一個(gè)巨大的字符串,交給pyodide.runPythonAsync()運(yùn)行就好了。當(dāng)有多個(gè)Python文件時(shí),我們可以把這些python文件打成普通wheel包,部署到webserver,然后可以用micropip直接安裝該wheel包:

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

注意,打wheel包需要有__init__.py文件,哪怕是個(gè)空文件。

四 存在的缺陷

目前pyodide有如下幾個(gè)缺陷:

  • Python運(yùn)行環(huán)境加載和初始化時(shí)間有點(diǎn)兒長(zhǎng),視網(wǎng)絡(luò)情況,幾秒到幾十秒都有可能。
  • pypi包支持的不完整。雖然pypi.org上的純python包都可以直接使用,但涉及到C擴(kuò)展寫的包,如果官方還沒編譯出來,那就需要自己動(dòng)手編譯了。
  • 個(gè)別很常用的包,例如opencv,還沒成功編譯出來;模型推理框架一個(gè)都沒有。不過還好可以通過相應(yīng)的JS庫(kù)來彌補(bǔ)。
  • 如果python中調(diào)用了js庫(kù)的話:
  • 可能會(huì)產(chǎn)生一定的內(nèi)存拷貝開銷(從wasm內(nèi)存到JS內(nèi)存的來回拷貝)。尤其是大數(shù)組作為參數(shù)或返回值,在速度要求高的場(chǎng)合下,額外的內(nèi)存拷貝開銷就不能忽視了。
  • python庫(kù)的方法接口可能跟其對(duì)應(yīng)的js庫(kù)的接口參數(shù)、返回值格式不一致,有一定的適配工作量。

五 總結(jié)

盡管有上述種種缺陷,得益于代碼移植的高效率和邏輯上1:1復(fù)刻的高可靠性保障,我們還是可以把這種方法運(yùn)用到多種業(yè)務(wù)場(chǎng)景里,為推動(dòng)機(jī)器學(xué)習(xí)技術(shù)的應(yīng)用添磚加瓦。

鏈接:

1、測(cè)試頁面:https://test-bucket-duplicate.oss-cn-hangzhou.aliyuncs.com/public/pyodide/test.html

2、文檔:https://pyodide.org/en/stable/usage/type-conversions.html

3、官方已編譯包的列表:https://github.com/pyodide/pyodide/tree/main/packages

4、手冊(cè):https://emscripten.org/docs/api_reference/Filesystem-API.html


當(dāng)前題目:復(fù)雜推理模型從服務(wù)器移植到Web瀏覽器的理論和實(shí)戰(zhàn)
網(wǎng)頁地址:http://www.dlmjj.cn/article/dhehsjs.html