エクスポートとモデルをNonx



この記事はチュートリアルシリーズの一部ですtxtai , AI動力セマンティック検索プラットフォーム
The ONNX runtime 機械学習モデルの共通シリアライズフォーマットを提供します.ONNXは多くのdifferent platforms/languages そして、機能は、推論時間を減らすのを助けるために組み込まれます.
PyTorchはOnNxにトーチモデルをエクスポートするための堅牢なサポートをしています.これは、onnxに直接顔トランスと/または他の川下モデルを抱きしめることを可能にします.
ONNXは多くの言語とプラットフォームを使用して直接推論のためのアベニューを開きます.たとえば、モデルは、サードパーティサービスに送信されるデータを制限するために、Android上で直接実行できます.ONNXはたくさんの約束を持つエキサイティングな開発です.マイクロソフトもリリースしましたHummingbird これは伝統的なモデルをエクスポートすることができます.を返します.
この記事はTodtaiを使用してonnxにモデルをエクスポートする方法をカバーします.これらのモデルは、Python、JavaScript、Java、およびRustで直接実行されます.現在、TxtaiはAPIを通してこれらの言語を支持します、そして、それはまだ推薦されたアプローチです.

依存関係のインストール
インストールtxtai すべての依存関係.この記事はONNX量子化を使用しているので、パイプラインエクストラパッケージをインストールする必要があります.
pip install txtai[pipeline]

OnNXでモデルを実行する
それを正しくしましょう!次の例では、感情分析モデルをONXにエクスポートし、推論セッションを実行します.
import numpy as np

from onnxruntime import InferenceSession, SessionOptions
from transformers import AutoTokenizer
from txtai.pipeline import HFOnnx

# Normalize logits using sigmoid function
sigmoid = lambda x: 1.0 / (1.0 + np.exp(-x))

# Export to ONNX
onnx = HFOnnx()
model = onnx("distilbert-base-uncased-finetuned-sst-2-english", "text-classification")

# Start inference session
options = SessionOptions()
session = InferenceSession(model, options)

# Tokenize
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased-finetuned-sst-2-english")
tokens = tokenizer(["I am happy", "I am mad"], return_tensors="np")

# Print results
outputs = session.run(None, dict(tokens))
print(sigmoid(outputs[0]))
[[0.01295124 0.9909526 ]
 [0.9874723  0.0297817 ]]
そして、そのように、結果があります!テキスト分類モデルは2つのラベルを使用して感情を判断します.上記の結果は、テキストスニペットごとのラベルの確率を示します.
onnxパイプラインはモデルを読み込み、グラフをonnxに変換して返します.出力ファイルが提供されていないことに注意してください.この場合、ONXモデルはバイト配列として返されます.出力ファイルが与えられた場合、このメソッドは出力パスを返します.

テキスト分類のためのモデルの訓練とエクスポート
次に、我々は「Nonxへの電車と輸出」ワークフローをつくるために、トレーナーパイプラインでOnNXパイプラインを結合します.
from datasets import load_dataset
from txtai.pipeline import HFTrainer

trainer = HFTrainer()

# Hugging Face dataset
ds = load_dataset("glue", "sst2")
data = ds["train"].select(range(5000)).flatten_indices()

# Train new model using 5,000 SST2 records (in-memory)
model, tokenizer = trainer("google/electra-base-discriminator", data, columns=("sentence", "label"))

# Export model trained in-memory to ONNX (still in-memory)
output = onnx((model, tokenizer), "text-classification", quantize=True)

# Start inference session
options = SessionOptions()
session = InferenceSession(output, options)

# Tokenize
tokens = tokenizer(["I am happy", "I am mad"], return_tensors="np")

# Print results
outputs = session.run(None, dict(tokens))
print(sigmoid(outputs[0]))
[[0.02424305 0.9557785 ]
 [0.95884305 0.05541185]]
このモデルはSST 2データセットの一部でのみ訓練されていますが、結果は前のステップと似ています.後でこのモデルを保存します.
text = onnx((model, tokenizer), "text-classification", "text-classify.onnx", quantize=True)

文埋め込みモデルのエクスポート
OnNxパイプラインはまた、sentence-transformers パッケージ.
embeddings = onnx("sentence-transformers/paraphrase-MiniLM-L6-v2", "pooling", "embeddings.onnx", quantize=True)
さて、OnNXでモデルを走らせましょう.
from sklearn.metrics.pairwise import cosine_similarity

options = SessionOptions()
session = InferenceSession(embeddings, options)

tokens = tokenizer(["I am happy", "I am glad"], return_tensors="np")

outputs = session.run(None, dict(tokens))[0]

print(cosine_similarity(outputs))
[[1.0000002 0.8430618]
 [0.8430618 1.       ]]
上記のコードは2つの別々のテキスト断片(「私は幸せです」と「私はうれしいです」)をトークン化して、onnxモデルを通してそれを走らせます.
これは2つの埋込み配列を出力し,それらの配列は余弦類似度を用いて比較する.我々が見ることができるように、2つのテキスト断片は意味のある意味を意味します.

Tontaiでonnxモデルをロードします
Txtai ononxモデルの組み込みサポートしています.ONXモデルをロードすることはシームレスです、そして、埋め込みとパイプラインはそれをサポートします.次のセクションでは、Nonxによってサポートされている分類パイプラインと埋め込みモデルをロードする方法を示します.
from txtai.embeddings import Embeddings
from txtai.pipeline import Labels

labels = Labels(("text-classify.onnx", "google/electra-base-discriminator"), dynamic=False)
print(labels(["I am happy", "I am mad"]))

embeddings = Embeddings({"path": "embeddings.onnx", "tokenizer": "sentence-transformers/paraphrase-MiniLM-L6-v2"})
print(embeddings.similarity("I am happy", ["I am glad"]))
[[(1, 0.9988517761230469), (0, 0.0011482156114652753)], [(0, 0.997488260269165), (1, 0.0025116782635450363)]]
[(0, 0.8581848740577698)]

ジャバスクリプト
これまでのところ、我々はonnxにモデルをエクスポートして、Pythonを通してそれらを走らせました.これは既に高速な推論時間、量子化、およびソフトウェア依存性の少ない多くの利点を持っています.しかし、我々が他の言語/プラットホームでPythonで訓練されるモデルを走らせるとき、OnNxは本当に光ります.
JavaScriptで上記のモデルを実行してみましょう.最初のステップは、ノードを取得しています.環境と依存関係のセットアップ.
import os

!mkdir js
os.chdir("/content/js")
{
  "name": "onnx-test",
  "private": true,
  "version": "1.0.0",
  "description": "ONNX Runtime Node.js test",
  "main": "index.js",
  "dependencies": {
    "onnxruntime-node": ">=1.8.0",
    "tokenizers": "file:tokenizers/bindings/node"
  }
}
# Copy ONNX models
!cp ../text-classify.onnx .
!cp ../embeddings.onnx .

# Save copy of Bert Tokenizer
tokenizer.save_pretrained("bert")

# Get tokenizers project
!git clone https://github.com/huggingface/tokenizers.git

os.chdir("/content/js/tokenizers/bindings/node")

# Install Rust
!apt-get install rustc

# Build tokenizers project locally as version on NPM isn't working properly for latest version of Node.js
!npm install --also=dev
!npm run dev

# Install all dependencies
os.chdir("/content/js")
!npm install
次に、JavaScriptの推論コードをインデックスに書き込みます.jsファイル.
const ort = require('onnxruntime-node');
const { promisify } = require('util');
const { Tokenizer } = require("tokenizers/dist/bindings/tokenizer");

function sigmoid(data) {
    return data.map(x => 1 / (1 + Math.exp(-x)))
}

function softmax(data) { 
    return data.map(x => Math.exp(x) / (data.map(y => Math.exp(y))).reduce((a,b) => a+b)) 
}

function similarity(v1, v2) {
    let dot = 0.0;
    let norm1 = 0.0;
    let norm2 = 0.0;

    for (let x = 0; x < v1.length; x++) {
        dot += v1[x] * v2[x];
        norm1 += Math.pow(v1[x], 2);
        norm2 += Math.pow(v2[x], 2);
    }

    return dot / (Math.sqrt(norm1) * Math.sqrt(norm2));
}

function tokenizer(path) {
    let tokenizer = Tokenizer.fromFile(path);
    return promisify(tokenizer.encode.bind(tokenizer));
}

async function predict(session, text) {
    try {
        // Tokenize input
        let encode = tokenizer("bert/tokenizer.json");
        let output = await encode(text);

        let ids = output.getIds().map(x => BigInt(x))
        let mask = output.getAttentionMask().map(x => BigInt(x))
        let tids = output.getTypeIds().map(x => BigInt(x))

        // Convert inputs to tensors    
        let tensorIds = new ort.Tensor('int64', BigInt64Array.from(ids), [1, ids.length]);
        let tensorMask = new ort.Tensor('int64', BigInt64Array.from(mask), [1, mask.length]);
        let tensorTids = new ort.Tensor('int64', BigInt64Array.from(tids), [1, tids.length]);

        let inputs = null;
        if (session.inputNames.length > 2) {
            inputs = { input_ids: tensorIds, attention_mask: tensorMask, token_type_ids: tensorTids};
        }
        else {
            inputs = { input_ids: tensorIds, attention_mask: tensorMask};
        }

        return await session.run(inputs);
    } catch (e) {
        console.error(`failed to inference ONNX model: ${e}.`);
    }
}

async function main() {
    let args = process.argv.slice(2);
    if (args.length > 1) {
        // Run sentence embeddings
        const session = await ort.InferenceSession.create('./embeddings.onnx');

        let v1 = await predict(session, args[0]);
        let v2 = await predict(session, args[1]);

        // Unpack results
        v1 = v1.embeddings.data;
        v2 = v2.embeddings.data;

        // Print similarity
        console.log(similarity(Array.from(v1), Array.from(v2)));
    }
    else {
        // Run text classifier
        const session = await ort.InferenceSession.create('./text-classify.onnx');
        let results = await predict(session, args[0]);

        // Normalize results using softmax and print
        console.log(softmax(results.logits.data));
    }
}

main();

テキストの分類を実行する
!node . "I am happy"
!node . "I am mad"
Float32Array(2) [ 0.001104647060856223, 0.9988954067230225 ]
Float32Array(2) [ 0.9976443648338318, 0.00235558208078146 ]
最初に、これは言う必要があります🔥🔥🔥! ちょうどこのモデルが完全にJavascriptで動くことができるという驚くべきこと.それは、NLPになる素晴らしい時間です!
上記の手順では、JavaScript環境でインストールされた依存関係を持ち、NNXを実行し、JavaScriptでデータをトークン化します.以前に作成されたテキスト分類モデルはJavaScript OnNXランタイムにロードされ、推論が実行されます.
リマインダーとして、テキスト分類モデルは2つのラベルを使用して感情を判断しています.上記の結果は、テキストスニペットごとのラベルの確率を示します.

ビルド文の組み込みとの類似性を比較する
!node . "I am happy", "I am glad"
0.8414919420066624
もう一度……すごい!文埋込みモデルは意味論的類似性を比較するために使用できるベクトルを生成する.
結果は正確にエクスポートされたモデルと一致しませんが、それは非常に近いです.これは100 % JavaScript、APIやリモートコール、すべてのノード内であることを再度言及する価値がある.

ジャバ
Javaと同じようにしましょう.次のセクションでは、Javaビルド環境を初期化し、OnNx推論を実行するために必要なコードを書き出します.
import os

os.chdir("/content")
!mkdir java
os.chdir("/content/java")

# Copy ONNX models
!cp ../text-classify.onnx .
!cp ../embeddings.onnx .

# Save copy of Bert Tokenizer
tokenizer.save_pretrained("bert")

!mkdir -p src/main/java

# Install gradle
!wget https://services.gradle.org/distributions/gradle-7.2-bin.zip
!unzip -o gradle-7.2-bin.zip
!gradle-7.2/bin/gradle wrapper
apply plugin: "java"

repositories {
    mavenCentral()
}

dependencies {
    implementation "com.robrua.nlp:easy-bert:1.0.3"
    implementation "com.microsoft.onnxruntime:onnxruntime:1.8.1"
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(8)
    }
}

jar {
    archiveBaseName = "onnxjava"
}

task onnx(type: JavaExec) {
    description = "Runs ONNX demo"
    classpath = sourceSets.main.runtimeClasspath
    main = "OnnxDemo"
}
import java.io.File;

import java.nio.LongBuffer;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ai.onnxruntime.OnnxTensor;
import ai.onnxruntime.OrtEnvironment;
import ai.onnxruntime.OrtSession;
import ai.onnxruntime.OrtSession.Result;

import com.robrua.nlp.bert.FullTokenizer;

class Tokens {
    public long[] ids;
    public long[] mask;
    public long[] types;
}

class Tokenizer {
    private FullTokenizer tokenizer;

    public Tokenizer(String path) {
        File vocab = new File(path);
        this.tokenizer = new FullTokenizer(vocab, true);
    }

    public Tokens tokenize(String text) {
        // Build list of tokens
        List<String> tokensList = new ArrayList();
        tokensList.add("[CLS]"); 
        tokensList.addAll(Arrays.asList(tokenizer.tokenize(text)));
        tokensList.add("[SEP]");

        int[] ids = tokenizer.convert(tokensList.toArray(new String[0]));

        Tokens tokens = new Tokens();

        // input ids    
        tokens.ids = Arrays.stream(ids).mapToLong(i -> i).toArray();

        // attention mask
        tokens.mask = new long[ids.length];
        Arrays.fill(tokens.mask, 1);

        // token type ids
        tokens.types = new long[ids.length];
        Arrays.fill(tokens.types, 0);

        return tokens;
    }
}

class Inference {
    private Tokenizer tokenizer;
    private OrtEnvironment env;
    private OrtSession session;

    public Inference(String model) throws Exception {
        this.tokenizer = new Tokenizer("bert/vocab.txt");
        this.env = OrtEnvironment.getEnvironment();
        this.session = env.createSession(model, new OrtSession.SessionOptions());
    }

    public float[][] predict(String text) throws Exception {
        Tokens tokens = this.tokenizer.tokenize(text);

        Map<String, OnnxTensor> inputs = new HashMap<String, OnnxTensor>();
        inputs.put("input_ids", OnnxTensor.createTensor(env, LongBuffer.wrap(tokens.ids),  new long[]{1, tokens.ids.length}));
        inputs.put("attention_mask", OnnxTensor.createTensor(env, LongBuffer.wrap(tokens.mask),  new long[]{1, tokens.mask.length}));
        inputs.put("token_type_ids", OnnxTensor.createTensor(env, LongBuffer.wrap(tokens.types),  new long[]{1, tokens.types.length}));

        return (float[][])session.run(inputs).get(0).getValue();
    }
}

class Vectors {
    public static double similarity(float[] v1, float[] v2) {
        double dot = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;

        for (int x = 0; x < v1.length; x++) {
            dot += v1[x] * v2[x];
            norm1 += Math.pow(v1[x], 2);
            norm2 += Math.pow(v2[x], 2);
        }

        return dot / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }

    public static float[] softmax(float[] input) {
        double[] t = new double[input.length];
        double sum = 0.0;

        for (int x = 0; x < input.length; x++) {
            double val = Math.exp(input[x]);
            sum += val;
            t[x] = val;
        }

        float[] output = new float[input.length];
        for (int x = 0; x < output.length; x++) {
            output[x] = (float) (t[x] / sum);
        }

        return output;
    }
}

public class OnnxDemo {
    public static void main(String[] args) {
        try {
            if (args.length < 2) {
              Inference inference = new Inference("text-classify.onnx");

              float[][] v1 = inference.predict(args[0]);

              System.out.println(Arrays.toString(Vectors.softmax(v1[0])));
            }
            else {
              Inference inference = new Inference("embeddings.onnx");
              float[][] v1 = inference.predict(args[0]);
              float[][] v2 = inference.predict(args[1]);

              System.out.println(Vectors.similarity(v1[0], v2[0]));
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Javaでテキストの分類を実行する
!./gradlew -q --console=plain onnx --args='"I am happy"' 2> /dev/null
!./gradlew -q --console=plain onnx --args='"I am mad"' 2> /dev/null
[0.0011046471, 0.99889535]
[0.9976444, 0.002355582]
上記のコマンドは、入力を実行して、以前にJava OnNx推論セッションを使ってつくられたテキスト分類モデルで推論を走らせます.
リマインダーとして、テキスト分類モデルは2つのラベルを使用して感情を判断しています.上記の結果は、テキストスニペットごとのラベルの確率を示します.

ビルド文の埋め込みとは、Javaでの類似性を比較する
!./gradlew -q --console=plain onnx --args='"I am happy" "I am glad"' 2> /dev/null
0.8581848568615768
文埋込みモデルは意味論的類似性を比較するために使用できるベクトルを生成する.
これは、JVM内のすべての100 % Java、APIまたはリモートコールです.それでもそれは驚くべきだと思う!


最後に、少なくとも、錆を試してみましょう.次のセクションでは、さびのビルド環境を初期化し、OnNx推論を実行するために必要なコードを書き出します.
import os

os.chdir("/content")
!mkdir rust
os.chdir("/content/rust")

# Copy ONNX models
!cp ../text-classify.onnx .
!cp ../embeddings.onnx .

# Save copy of Bert Tokenizer
tokenizer.save_pretrained("bert")

# Install Rust
!apt-get install rustc

!mkdir -p src
[package]
name = "onnx-test"
version = "1.0.0"
description = """
ONNX Runtime Rust test
"""
edition = "2018"

[dependencies]
onnxruntime = { version = "0.0.14"}
tokenizers = { version = "0.10.1"}
use onnxruntime::environment::Environment;
use onnxruntime::GraphOptimizationLevel;
use onnxruntime::ndarray::{Array2, Axis};
use onnxruntime::tensor::OrtOwnedTensor;

use std::env;

use tokenizers::decoders::wordpiece::WordPiece as WordPieceDecoder;
use tokenizers::models::wordpiece::WordPiece;
use tokenizers::normalizers::bert::BertNormalizer;
use tokenizers::pre_tokenizers::bert::BertPreTokenizer;
use tokenizers::processors::bert::BertProcessing;
use tokenizers::tokenizer::{Result, Tokenizer, EncodeInput};

fn tokenize(text: String, inputs: usize) -> Vec<Array2<i64>> {
    // Load tokenizer
    let mut tokenizer = Tokenizer::new(Box::new(
        WordPiece::from_files("bert/vocab.txt")
            .build()
            .expect("Vocab file not found"),
    ));

    tokenizer.with_normalizer(Box::new(BertNormalizer::default()));
    tokenizer.with_pre_tokenizer(Box::new(BertPreTokenizer));
    tokenizer.with_decoder(Box::new(WordPieceDecoder::default()));
    tokenizer.with_post_processor(Box::new(BertProcessing::new(
        (
            String::from("[SEP]"),
            tokenizer.get_model().token_to_id("[SEP]").unwrap(),
        ),
        (
            String::from("[CLS]"),
            tokenizer.get_model().token_to_id("[CLS]").unwrap(),
        ),
    )));

    // Encode input text
    let encoding = tokenizer.encode(EncodeInput::Single(text), true).unwrap();

    let v1: Vec<i64> = encoding.get_ids().to_vec().into_iter().map(|x| x as i64).collect();
    let v2: Vec<i64> = encoding.get_attention_mask().to_vec().into_iter().map(|x| x as i64).collect();
    let v3: Vec<i64> = encoding.get_type_ids().to_vec().into_iter().map(|x| x as i64).collect();

    let ids = Array2::from_shape_vec((1, v1.len()), v1).unwrap();
    let mask = Array2::from_shape_vec((1, v2.len()), v2).unwrap();
    let tids = Array2::from_shape_vec((1, v3.len()), v3).unwrap();

    return if inputs > 2 { vec![ids, mask, tids] } else { vec![ids, mask] };
}

fn predict(text: String, softmax: bool) -> Vec<f32> {
    // Start onnx session
    let environment = Environment::builder()
        .with_name("test")
        .build().unwrap();

    // Derive model path
    let model = if softmax { "text-classify.onnx" } else { "embeddings.onnx" };

    let mut session = environment
        .new_session_builder().unwrap()
        .with_optimization_level(GraphOptimizationLevel::Basic).unwrap()
        .with_number_threads(1).unwrap()
        .with_model_from_file(model).unwrap();

    let inputs = tokenize(text, session.inputs.len());

    // Run inference and print result
    let outputs: Vec<OrtOwnedTensor<f32, _>> = session.run(inputs).unwrap();
    let output: &OrtOwnedTensor<f32, _> = &outputs[0];

    let probabilities: Vec<f32>;
    if softmax {
        probabilities = output
            .softmax(Axis(1))
            .iter()
            .copied()
            .collect::<Vec<_>>();
    }
    else {
        probabilities= output
            .iter()
            .copied()
            .collect::<Vec<_>>();
    }

    return probabilities;
}

fn similarity(v1: &Vec<f32>, v2: &Vec<f32>) -> f64 {
    let mut dot = 0.0;
    let mut norm1 = 0.0;
    let mut norm2 = 0.0;

    for x in 0..v1.len() {
        dot += v1[x] * v2[x];
        norm1 += v1[x].powf(2.0);
        norm2 += v2[x].powf(2.0);
    }

    return dot as f64 / (norm1.sqrt() * norm2.sqrt()) as f64
}

fn main() -> Result<()> {
    // Tokenize input string
    let args: Vec<String> = env::args().collect();

    if args.len() <= 2 {
      let v1 = predict(args[1].to_string(), true);
      println!("{:?}", v1);
    }
    else {
      let v1 = predict(args[1].to_string(), false);
      let v2 = predict(args[2].to_string(), false);
      println!("{:?}", similarity(&v1, &v2));
    }

    Ok(())
}

ONNXでさびのテキスト分類を走らせてください
!cargo run "I am happy" 2> /dev/null
!cargo run "I am mad" 2> /dev/null
[0.0011003953, 0.99889964]
[0.9976444, 0.0023555849]
上記のコマンドは入力をトークン化して、以前にさびたOnNx推論セッションを使ってつくられたテキスト分類モデルで推論を走らせます.
リマインダーとして、テキスト分類モデルは2つのラベルを使用して感情を判断しています.上記の結果は、テキストスニペットごとのラベルの確率を示します.

ビルド文の埋め込みとonnxとさびの類似性を比較する
!cargo run "I am happy" "I am glad" 2> /dev/null
0.8583641740656903
文埋込みモデルは意味論的類似性を比較するために使用できるベクトルを生成する.
もう一度、これは100 %錆、APIやリモートコールです.はい、まだ驚くべきだと思う!

ラッピング
このノートブックは、Txtaiを使用してonnxにモデルをエクスポートする方法をカバーしました.これらのモデルは、Python、JavaScript、JavaおよびRustで実行されました.golangも評価されました、しかし、現在安定したonnxランタイムが利用できるように見えません.
このメソッドはいくつかのプラットフォームで多数のプログラミング言語を使用して機械学習モデルを訓練し実行する方法を提供します.
以下はユースケースのリストです.
  • モバイル/エッジデバイスのローカル実行モデルのビルド
  • チームが混合物にパイソンを加えないのを好むとき、Java/JavaScript/Rust開発スタックでモデルを走らせてください
  • Pythonの推論のためにONXにモデルをエクスポートして、CPU性能を改善して/またはソフトウェア依存の数を減らす