1

本文将介绍如何编写基于Node.js的AI即服务应用程序。

当今,用于AI的主流编程语言是Python。但是,用于Web的编程语言是JavaScript。为了将AI功能作为Web服务提供,我们需要将AI算法包装在JavaScript中,尤其是Node.js。

但是,Python和JavaScript本身都不适合计算密集型AI应用程序。它们是具有繁重运行时的高级(即慢速)语言。它们的易用性以降低性能为代价。 Python通过将AI计算包装在本机C / C ++模块中来解决此问题。 Node.js可以做同样的事情,但是我们有一个更好的方法 --WebAssembly。

WebAssembly VM提供与Node.js和其他JavaScript运行时的紧密集成。它们具有高性能,内存安全,默认情况下安全且可跨操作系统移植的特点。但是,我们的方法结合了WebAssembly和本机代码的最佳功能。

工作原理
基于Node.js的AI即服务应用程序由三部分组成。

Node.js应用程序提供Web服务并调用WebAssembly函数以执行计算密集型任务,例如AI推理。
数据准备,处理以及与其他系统的集成是通过WebAssembly 函数完成的。最初,我们支持Rust。应用程序开发人员必须编写此函数。
AI模型的实际执行是通过原生代码完成的,以最大限度地提高性能。该代码的这一部分非常简短,并经过了安全性和安全性审查。应用程序开发人员只需从WebAssembly函数调用此原生程序,就像今天在Python和Node.js中使用原生函数的方式一样。

接下来,我们看下示例程序。

人脸检测示例
人脸检测Web服务 允许用户上传照片,并显示绿色框标记的图像。

用于执行MTCNN人脸检测模型的Rust源代码基于Cetra的教程:使用Tensorflow Rust进行人脸检测。我们进行了更改以使Tensorflow库在WebAssembly中工作。

Node.js应用程序处理文件上传和响应。

app.post('/infer', function (req, res) {
let image_file = req.files.image_file;
var result_filename = uuidv4() + ".png";

// Call the infer() function from WebAssembly (SSVM)
var res = infer(req.body.detection_threshold, image_file.data);

fs.writeFileSync("public/" + result_filename, res);
res.send('![]()');
});
如您所见,JavaScript应用程序仅将图像数据和一个名为detection_threshold的参数传递给infer()函数,该参数指定要检测的最小脸部,然后将返回值保存到服务器上的图像文件中。 infer()函数用Rust编写,并编译成WebAssembly,以便可以从JavaScript调用它。

infer()函数将输入图像数据展平为一个数组。它建立了一个TensorFlow模型,并使用扁平化的图像数据作为模型的输入。 TensorFlow模型执行将返回一组数字,这些数字指示每个面框的四个角的坐标。然后,infer()函数在每个面孔周围绘制一个绿色框,然后将修改后的图像保存到Web服务器上的PNG文件中。

[wasm_bindgen]

pub fn infer(detection_threshold: &str, image_data: &[u8]) -> Vec<u8> {

let mut dt = detection_threshold;
... ...
let mut img = image::load_from_memory(image_data).unwrap();

// Run the tensorflow model using the face_detection_mtcnn native wrapper
let mut cmd = Command::new("face_detection_mtcnn");
// Pass in some arguments
cmd.arg(img.width().to_string())
    .arg(img.height().to_string())
    .arg(dt);
// The image bytes data is passed in via STDIN
for (_x, _y, rgb) in img.pixels() {
    cmd.stdin_u8(rgb[2] as u8)
        .stdin_u8(rgb[1] as u8)
        .stdin_u8(rgb[0] as u8);
}
let out = cmd.output();

// Draw boxes from the result JSON array
let line = Pixel::from_slice(&[0, 255, 0, 0]);
let stdout_json: Value = from_str(str::from_utf8(&out.stdout).expect("[]")).unwrap();
let stdout_vec = stdout_json.as_array().unwrap();
for i in 0..stdout_vec.len() {
    let xy = stdout_vec[i].as_array().unwrap();
    let x1: i32 = xy[0].as_f64().unwrap() as i32;
    let y1: i32 = xy[1].as_f64().unwrap() as i32;
    let x2: i32 = xy[2].as_f64().unwrap() as i32;
    let y2: i32 = xy[3].as_f64().unwrap() as i32;
    let rect = Rect::at(x1, y1).of_size((x2 - x1) as u32, (y2 - y1) as u32);
    draw_hollow_rect_mut(&mut img, rect, *line);
}   
let mut buf = Vec::new();
// Write the result image into STDOUT
img.write_to(&mut buf, image::ImageOutputFormat::Png).expect("Unable to write");
return buf;

}
face_detection_mtcnn命令以原生代码运行MTCNN TensorFlow模型。它包含三个参数:图像宽度,图像高度和检测阈值。从WebAssembly infer() 通过STDIN传入RGB值的实际图像数据。模型的结果以JSON编码,并通过STDOUT返回。

请注意,我们如何传递模型参数detection_threshold名为min_size的模型张量,然后使用input张量传递输入图像数据。box张量用于从模型中检索结果。

fn main() -> Result<(), Box<dyn Error>> {

// Get the arguments passed in from WebAssembly
let args: Vec<String> = env::args().collect();
let img_width: u64 = args[1].parse::<u64>().unwrap();
let img_height: u64 = args[2].parse::<u64>().unwrap();
let detection_threshold: f32 = args[3].parse::<f32>().unwrap();
let mut buffer: Vec<u8> = Vec::new();
let mut flattened: Vec<f32> = Vec::new();

// The image bytes are read from STDIN
io::stdin().read_to_end(&mut buffer)?;
for num in buffer {
    flattened.push(num.into());
}

// Load up the graph as a byte array and create a tensorflow graph.
let model = include_bytes!("mtcnn.pb");
let mut graph = Graph::new();
graph.import_graph_def(&*model, &ImportGraphDefOptions::new())?;

let mut args = SessionRunArgs::new();
// The `input` tensor expects BGR pixel data from the input image
let input = Tensor::new(&[img_height, img_width, 3]).with_values(&flattened)?;
args.add_feed(&graph.operation_by_name_required("input")?, 0, &input);

// The `min_size` tensor takes the detection_threshold argument
let min_size = Tensor::new(&[]).with_values(&[detection_threshold])?;
args.add_feed(&graph.operation_by_name_required("min_size")?, 0, &min_size);

// Default input params for the model
let thresholds = Tensor::new(&[3]).with_values(&[0.6f32, 0.7f32, 0.7f32])?;
args.add_feed(&graph.operation_by_name_required("thresholds")?, 0, &thresholds);
let factor = Tensor::new(&[]).with_values(&[0.709f32])?;
args.add_feed(&graph.operation_by_name_required("factor")?, 0, &factor);

// Request the following outputs after the session runs.
let bbox = args.request_fetch(&graph.operation_by_name_required("box")?, 0);

let session = Session::new(&SessionOptions::new(), &graph)?;
session.run(&mut args)?;

// Get the bounding boxes
let bbox_res: Tensor<f32> = args.fetch(bbox)?;
let mut iter = 0;
let mut json_vec: Vec<[f32; 4]> = Vec::new();
while (iter * 4) < bbox_res.len() {
    json_vec.push([
        bbox_res[4 * iter + 1], // x1
        bbox_res[4 * iter],     // y1
        bbox_res[4 * iter + 3], // x2
        bbox_res[4 * iter + 2], // y2
    ]);
    iter += 1;
}
let json_obj = json!(json_vec);
// Return result JSON in STDOUT
println!("{}", json_obj.to_string()); 
Ok(())

}
我们的目标是为常见的AI模型创建原生执行包装,以便开发人员可以将它们用作库。

部署人脸检测示例
作为前提条件,您将需要安装Rust,Node.js,Second State WebAssembly VM 和 ssvmup 工具。查看有关步骤的说明,或仅使用我们的Docker镜像。您还需要TensorFlow库。

$ wget https://storage.googleapis.co...
$ sudo tar -C /usr/ -xzf libtensorflow-gpu-linux-x86_64-1.15.0.tar.gz
为了部署人脸检测示例,我们从本机TensorFlow模型驱动程序开始。您可以从该项目中的Rust源代码进行编译。

in the native_model_zoo/face_detection_mtcnn directory

$ cargo install --path .
接下来,转到Web应用程序项目。运行ssvmup命令以从Rust构建WebAssembly函数。回想一下,此WebAssembly函数为Web应用程序执行数据准备逻辑。

in the nodejs/face_detection_service directory

$ ssvmup build
借助构建的WebAssembly功能,您现在可以启动Node.js应用程序。

$ npm i express express-fileupload uuid

$ cd node
$ node server.js
现在可以在计算机的端口8080上使用Web服务。尝试使用自己的自拍照或家人和集体照!

TensorFlow 模型动物园
原生Rust 包 face_detection_mtcnn是基于TensorFlow库的简单包装器。它加载经过训练的TensorFlow模型(称为冻结保存的模型),设置模型的输入,执行模型,并从模型中检索输出值。

实际上,我们的包装器仅检索检测到的脸部周围的盒子坐标。该模型实际上为每个检测到的脸部以及每个脸部的眼睛,嘴巴和鼻子的位置提供了置信度。通过更改模型中的检索张量名称,包装器可以获取此信息并返回到WASM函数。

如果您想使用其他模型,则遵循该示例并为您自己的模型创建包装器应该相当容易。您只需要了解张量名称及其数据类型的输入和输出即可。

为了实现此目标,我们创建了一个名为“原生模型动物园”的项目,以为尽可能多的TensorFlow模型开发现成的Rust包装器。

总结
在本文中,我们演示了如何使用Rust和WebAssembly在Node.js中实现真实的AI即服务用例。我们的方法为整个社区提供了一个为“模型动物园”做出贡献的框架,该模型可以用作更多应用程序开发人员的AI库。

PS: 本文属于翻译,原文

本文将介绍如何编写基于Node.js的AI即服务应用程序。

当今,用于AI的主流编程语言是Python。但是,用于Web的编程语言是JavaScript。为了将AI功能作为Web服务提供,我们需要将AI算法包装在JavaScript中,尤其是Node.js。
但是,Python和JavaScript本身都不适合计算密集型AI应用程序。它们是具有繁重运行时的高级(即慢速)语言。它们的易用性以降低性能为代价。 Python通过将AI计算包装在本机C / C ++模块中来解决此问题。 Node.js可以做同样的事情,但是我们有一个更好的方法 --WebAssembly。
WebAssembly VM提供与Node.js和其他JavaScript运行时的紧密集成。它们具有高性能,内存安全,默认情况下安全且可跨操作系统移植的特点。但是,我们的方法结合了WebAssembly和本机代码的最佳功能。

工作原理

基于Node.js的AI即服务应用程序由三部分组成。

  • Node.js应用程序提供Web服务并调用WebAssembly函数以执行计算密集型任务,例如AI推理。
  • 数据准备,处理以及与其他系统的集成是通过WebAssembly 函数完成的。最初,我们支持Rust。应用程序开发人员必须编写此函数。
  • AI模型的实际执行是通过原生代码完成的,以最大限度地提高性能。该代码的这一部分非常简短,并经过了安全性和安全性审查。应用程序开发人员只需从WebAssembly函数调用此原生程序,就像今天在Python和Node.js中使用原生函数的方式一样。

接下来,我们看下示例程序。

人脸检测示例

人脸检测Web服务 允许用户上传照片,并显示绿色框标记的图像。

用于执行MTCNN人脸检测模型的Rust源代码基于Cetra的教程:使用Tensorflow Rust进行人脸检测。我们进行了更改以使Tensorflow库在WebAssembly中工作。

Node.js应用程序处理文件上传和响应。

app.post('/infer', function (req, res) {
  let image_file = req.files.image_file;
  var result_filename = uuidv4() + ".png";

  // Call the infer() function from WebAssembly (SSVM)
  var res = infer(req.body.detection_threshold, image_file.data);

  fs.writeFileSync("public/" + result_filename, res);
  res.send('<img src="' +  result_filename + '"/>');
});

如您所见,JavaScript应用程序仅将图像数据和一个名为detection_threshold的参数传递给infer()函数,该参数指定要检测的最小脸部,然后将返回值保存到服务器上的图像文件中。 infer()函数用Rust编写,并编译成WebAssembly,以便可以从JavaScript调用它。
infer()函数将输入图像数据展平为一个数组。它建立了一个TensorFlow模型,并使用扁平化的图像数据作为模型的输入。 TensorFlow模型执行将返回一组数字,这些数字指示每个面框的四个角的坐标。然后,infer()函数在每个面孔周围绘制一个绿色框,然后将修改后的图像保存到Web服务器上的PNG文件中。

#[wasm_bindgen]
pub fn infer(detection_threshold: &str, image_data: &[u8]) -> Vec<u8> {
    let mut dt = detection_threshold;
    ... ...
    let mut img = image::load_from_memory(image_data).unwrap();

    // Run the tensorflow model using the face_detection_mtcnn native wrapper
    let mut cmd = Command::new("face_detection_mtcnn");
    // Pass in some arguments
    cmd.arg(img.width().to_string())
        .arg(img.height().to_string())
        .arg(dt);
    // The image bytes data is passed in via STDIN
    for (_x, _y, rgb) in img.pixels() {
        cmd.stdin_u8(rgb[2] as u8)
            .stdin_u8(rgb[1] as u8)
            .stdin_u8(rgb[0] as u8);
    }
    let out = cmd.output();

    // Draw boxes from the result JSON array
    let line = Pixel::from_slice(&[0, 255, 0, 0]);
    let stdout_json: Value = from_str(str::from_utf8(&out.stdout).expect("[]")).unwrap();
    let stdout_vec = stdout_json.as_array().unwrap();
    for i in 0..stdout_vec.len() {
        let xy = stdout_vec[i].as_array().unwrap();
        let x1: i32 = xy[0].as_f64().unwrap() as i32;
        let y1: i32 = xy[1].as_f64().unwrap() as i32;
        let x2: i32 = xy[2].as_f64().unwrap() as i32;
        let y2: i32 = xy[3].as_f64().unwrap() as i32;
        let rect = Rect::at(x1, y1).of_size((x2 - x1) as u32, (y2 - y1) as u32);
        draw_hollow_rect_mut(&mut img, rect, *line);
    }   
    let mut buf = Vec::new();
    // Write the result image into STDOUT
    img.write_to(&mut buf, image::ImageOutputFormat::Png).expect("Unable to write");
    return buf;
}

face_detection_mtcnn命令以原生代码运行MTCNN TensorFlow模型。它包含三个参数:图像宽度,图像高度和检测阈值。从WebAssembly infer() 通过STDIN传入RGB值的实际图像数据。模型的结果以JSON编码,并通过STDOUT返回。

请注意,我们如何传递模型参数detection_threshold名为min_size的模型张量,然后使用input张量传递输入图像数据。box张量用于从模型中检索结果。

fn main() -> Result<(), Box<dyn Error>> {
    // Get the arguments passed in from WebAssembly
    let args: Vec<String> = env::args().collect();
    let img_width: u64 = args[1].parse::<u64>().unwrap();
    let img_height: u64 = args[2].parse::<u64>().unwrap();
    let detection_threshold: f32 = args[3].parse::<f32>().unwrap();
    let mut buffer: Vec<u8> = Vec::new();
    let mut flattened: Vec<f32> = Vec::new();

    // The image bytes are read from STDIN
    io::stdin().read_to_end(&mut buffer)?;
    for num in buffer {
        flattened.push(num.into());
    }

    // Load up the graph as a byte array and create a tensorflow graph.
    let model = include_bytes!("mtcnn.pb");
    let mut graph = Graph::new();
    graph.import_graph_def(&*model, &ImportGraphDefOptions::new())?;

    let mut args = SessionRunArgs::new();
    // The `input` tensor expects BGR pixel data from the input image
    let input = Tensor::new(&[img_height, img_width, 3]).with_values(&flattened)?;
    args.add_feed(&graph.operation_by_name_required("input")?, 0, &input);

    // The `min_size` tensor takes the detection_threshold argument
    let min_size = Tensor::new(&[]).with_values(&[detection_threshold])?;
    args.add_feed(&graph.operation_by_name_required("min_size")?, 0, &min_size);

    // Default input params for the model
    let thresholds = Tensor::new(&[3]).with_values(&[0.6f32, 0.7f32, 0.7f32])?;
    args.add_feed(&graph.operation_by_name_required("thresholds")?, 0, &thresholds);
    let factor = Tensor::new(&[]).with_values(&[0.709f32])?;
    args.add_feed(&graph.operation_by_name_required("factor")?, 0, &factor);

    // Request the following outputs after the session runs.
    let bbox = args.request_fetch(&graph.operation_by_name_required("box")?, 0);

    let session = Session::new(&SessionOptions::new(), &graph)?;
    session.run(&mut args)?;

    // Get the bounding boxes
    let bbox_res: Tensor<f32> = args.fetch(bbox)?;
    let mut iter = 0;
    let mut json_vec: Vec<[f32; 4]> = Vec::new();
    while (iter * 4) < bbox_res.len() {
        json_vec.push([
            bbox_res[4 * iter + 1], // x1
            bbox_res[4 * iter],     // y1
            bbox_res[4 * iter + 3], // x2
            bbox_res[4 * iter + 2], // y2
        ]);
        iter += 1;
    }
    let json_obj = json!(json_vec);
    // Return result JSON in STDOUT
    println!("{}", json_obj.to_string()); 
    Ok(())
}

我们的目标是为常见的AI模型创建原生执行包装,以便开发人员可以将它们用作库。

部署人脸检测示例

作为前提条件,您将需要安装Rust,Node.js,Second State WebAssembly VMssvmup 工具。查看有关步骤的说明,或仅使用我们的Docker镜像。您还需要TensorFlow库。

$ wget https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-gpu-linux-x86_64-1.15.0.tar.gz
$ sudo tar -C /usr/ -xzf libtensorflow-gpu-linux-x86_64-1.15.0.tar.gz

为了部署人脸检测示例,我们从本机TensorFlow模型驱动程序开始。您可以从该项目中的Rust源代码进行编译。

# in the native_model_zoo/face_detection_mtcnn directory
$ cargo install --path .

接下来,转到Web应用程序项目。运行ssvmup命令以从Rust构建WebAssembly函数。回想一下,此WebAssembly函数为Web应用程序执行数据准备逻辑。

# in the nodejs/face_detection_service directory
$ ssvmup build

借助构建的WebAssembly功能,您现在可以启动Node.js应用程序。

$ npm i express express-fileupload uuid

$ cd node
$ node server.js

现在可以在计算机的端口8080上使用Web服务。尝试使用自己的自拍照或家人和集体照!

TensorFlow 模型动物园

原生Rust 包 face_detection_mtcnn是基于TensorFlow库的简单包装器。它加载经过训练的TensorFlow模型(称为冻结保存的模型),设置模型的输入,执行模型,并从模型中检索输出值。
实际上,我们的包装器仅检索检测到的脸部周围的盒子坐标。该模型实际上为每个检测到的脸部以及每个脸部的眼睛,嘴巴和鼻子的位置提供了置信度。通过更改模型中的检索张量名称,包装器可以获取此信息并返回到WASM函数。
如果您想使用其他模型,则遵循该示例并为您自己的模型创建包装器应该相当容易。您只需要了解张量名称及其数据类型的输入和输出即可。
为了实现此目标,我们创建了一个名为“原生模型动物园”的项目,以为尽可能多的TensorFlow模型开发现成的Rust包装器。

总结

在本文中,我们演示了如何使用Rust和WebAssembly在Node.js中实现真实的AI即服务用例。我们的方法为整个社区提供了一个为“模型动物园”做出贡献的框架,该模型可以用作更多应用程序开发人员的AI库。

PS: 本文属于翻译,原文


iyacontrol
1.4k 声望2.7k 粉丝

专注kubernetes,devops,aiops,service mesh。