コンテンツにスキップ

C#でのBERT NLPの推論

C# BERT NLPディープラーニングとONNX Runtimeによる推論

Section titled “C# BERT NLPディープラーニングとONNX Runtimeによる推論”

このチュートリアルでは、人気のBERT自然言語処理ディープラーニングモデルの推論をC#で行う方法を学びます。

C#でテキストを前処理できるようにするために、ほとんどのBERTモデルのトークナイザーを含むオープンソースのBERTTokenizersを活用します。サポートされているモデルについては、以下を参照してください。

  • BERT Base
  • BERT Large
  • BERT German
  • BERT Multilingual
  • BERT Base Uncased
  • BERT Large Uncased

これらのベースモデルに基づいてファインチューニングされた多くのモデル(このチュートリアルのモデルを含む)があります。モデルのトークナイザーは、ファインチューニングされたベースモデルと同じです。

このチュートリアルは、ローカルで実行することも、Azure Machine Learningコンピューティングを活用して実行することもできます。

ローカルで実行するには:

Azure Machine Learningを使用してクラウドで実行するには:

Hugging Faceを使用してBERTモデルをダウンロードする

Section titled “Hugging Faceを使用してBERTモデルをダウンロードする”

Hugging Faceには、オープンソースモデルをダウンロードするための優れたAPIがあり、その後、PythonとPytorchを使用してONNX形式にエクスポートできます。これは、ONNXモデルズーにまだ含まれていないオープンソースモデルを使用する場合に最適なオプションです。

Pythonでモデルをダウンロードしてエクスポートする手順

Section titled “Pythonでモデルをダウンロードしてエクスポートする手順”

transformers APIを使用して、bert-large-uncased-whole-word-masking-finetuned-squadという名前のBertForQuestionAnsweringモデルをダウンロードします。

import torch
from transformers import BertForQuestionAnswering
model_name = "bert-large-uncased-whole-word-masking-finetuned-squad"
model_path = "./" + model_name + ".onnx"
model = BertForQuestionAnswering.from_pretrained(model_name)
# モデルを推論モードに設定します
# モデルをエクスポートする前にtorch_model.eval()またはtorch_model.train(False)を呼び出すことが重要です。
# これにより、モデルが推論モードに切り替わります。これは、ドロップアウトやバッチ正規化などの演算子が
# 推論モードとトレーニングモードで異なる動作をするためです。
model.eval()

モデルをダウンロードしたので、ONNX形式にエクスポートする必要があります。これは、torch.onnx.export関数を使用してPytorchに組み込まれています。

  • inputs変数は、入力の形状を示します。以下のようにダミー入力を作成するか、モデルのテストからのサンプル入力を使用できます。

  • opset_versionを、モデルと互換性のある最高のバージョンに設定します。opsetバージョンの詳細については、こちらを参照してください。

  • モデルのinput_namesoutput_namesを設定します。

  • sentencecontext変数は、推論される質問ごとに長さが異なるため、動的長の入力のdynamic_axesを設定します。

# モデルへのダミー入力を生成します。必要に応じて調整してください。
inputs = {
# トークン化されたテキストの数値IDのリスト
'input_ids': torch.randint(32, [1, 32], dtype=torch.long),
# 1のダミーリスト
'attention_mask': torch.ones([1, 32], dtype=torch.long),
# 1のダミーリスト
'token_type_ids': torch.ones([1, 32], dtype=torch.long)
}
symbolic_names = {0: 'batch_size', 1: 'max_seq_len'}
torch.onnx.export(model,
# 実行中のモデル
(inputs['input_ids'],
inputs['attention_mask'],
inputs['token_type_ids']), # モデル入力(複数の入力の場合はタプル)
model_path, # モデルの保存先(ファイルまたはファイルのようなオブジェクト)
opset_version=11, # モデルをエクスポートするONNXバージョン
do_constant_folding=True, # 最適化のために定数畳み込みを実行するかどうか
input_names=['input_ids',
'input_mask',
'segment_ids'], # モデルの入力名
output_names=['start_logits', "end_logits"], # モデルの出力名
dynamic_axes={'input_ids': symbolic_names,
'input_mask' : symbolic_names,
'segment_ids' : symbolic_names,
'start_logits' : symbolic_names,
'end_logits': symbolic_names}) # 可変長の軸/動的入力

ビルド済みのモデルを取得して運用化する場合、モデルの前処理と後処理、および入力/出力の形状とラベルを理解するために少し時間を取ると便利です。多くのモデルには、Pythonで提供されているサンプルコードがあります。C#でモデルを推論しますが、まずPythonでテストしてどのように行われるかを確認します。これは、次のステップでC#のロジックを理解するのに役立ちます。

  • モデルをテストするためのコードは、このチュートリアルで提供されています。Pythonでこのモデルをテストおよび推論するためのソースを確認してください。以下は、モデルを実行した際のサンプルinput文とサンプルoutputです。

  • サンプルinput

input = "{\"question\": \"What is Dolly Parton's middle name?\", \"context\": \"Dolly Rebecca Parton is an American singer-songwriter\"}"
print(run(input))
  • 上記の質問に対する出力は次のようになります。input_idsを使用して、C#でのトークン化を検証できます。
Output:
{'input_ids': [101, 2054, 2003, 19958, 2112, 2239, 1005, 1055, 2690, 2171, 1029, 102, 19958, 9423, 2112, 2239, 2003, 2019, 2137, 3220, 1011, 6009, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
{'answer': 'Rebecca'}

Pythonでモデルをテストしたので、C#でビルドします。最初に行う必要があるのは、プロジェクトを作成することです。この例ではコンソールアプリを使用しますが、このコードはどのC#アプリケーションでも使用できます。

  • NugetパッケージBERTTokenizersMicrosoft.ML.OnnxRuntimeMicrosoft.ML.OnnxRuntime.ManagedMicrosoft.MLをインストールします
dotnet add package Microsoft.ML.OnnxRuntime --version 1.16.0
dotnet add package Microsoft.ML.OnnxRuntime.Managed --version 1.16.0
dotnet add package Microsoft.ML
dotnet add package BERTTokenizers --version 1.1.0
  • パッケージをインポートします
using BERTTokenizers;
using Microsoft.ML.Data;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
  • namespaceclassMain関数を追加します。
namespace MyApp // 注:実際の名前空間はプロジェクト名によって異なります。
{
internal class BertTokenizeProgram
{
static void Main(string[] args)
{
}
}
}

エンコード用のBertInputクラスを作成する

Section titled “エンコード用のBertInputクラスを作成する”
  • BertInput構造体を追加します
public struct BertInput
{
public long[] InputIds { get; set; }
public long[] AttentionMask { get; set; }
public long[] TypeIds { get; set; }
}

BertUncasedLargeTokenizerで文をトークン化する

Section titled “BertUncasedLargeTokenizerで文をトークン化する”
  • 文(質問とコンテキスト)を作成し、BertUncasedLargeTokenizerで文をトークン化します。ベースモデルはbert-large-uncasedであるため、ライブラリのBertUncasedLargeTokenizerを使用します。BERTモデルのベースモデルを確認して、正しいトークナイザーを使用していることを確認してください。
var sentence = "{\"question\": \"Where is Bob Dylan From?\", \"context\": \"Bob Dylan is from Duluth, Minnesota and is an American singer-songwriter\"}";
Console.WriteLine(sentence);
// トークナイザーを作成し、文をトークン化します。
var tokenizer = new BertUncasedLargeTokenizer();
// 文のトークンを取得します。
var tokens = tokenizer.Tokenize(sentence);
// Console.WriteLine(String.Join(", ", tokens));
// 文をエンコードし、文のトークン数を渡します。
var encoded = tokenizer.Encode(tokens.Count(), sentence);
// (input_id, attention_mask, type_id)のリストからInputIds、AttentionMask、TypeIdsにエンコーディングを分割します。
var bertInput = new BertInput()
{
InputIds = encoded.Select(t => t.InputIds).ToArray(),
AttentionMask = encoded.Select(t => t.AttentionMask).ToArray(),
TypeIds = encoded.Select(t => t.TokenTypeIds).ToArray(),
};

推論に必要なname -> OrtValueペアのinputsを作成する

Section titled “推論に必要なname -> OrtValueペアのinputsを作成する”
  • モデルを取得し、入力バッファの上に3つのOrtValueを作成し、それらを辞書にラップしてRun()に供給します。 Onnxruntimeクラスのほぼすべてがネイティブデータ構造をラップしているため、メモリリークを防ぐために破棄する必要があることに注意してください。
// 推論セッションを作成するためのモデルへのパスを取得します。
var modelPath = @"C:\code\bert-nlp-csharp\BertNlpTest\BertNlpTest\bert-large-uncased-finetuned-qa.onnx";
using var runOptions = new RunOptions();
using var session = new InferenceSession(modelPath);
// 入力データの上にテンソルを作成します。
using var inputIdsOrtValue = OrtValue.CreateTensorValueFromMemory(bertInput.InputIds,
new long[] { 1, bertInput.InputIds.Length });
using var attMaskOrtValue = OrtValue.CreateTensorValueFromMemory(bertInput.AttentionMask,
new long[] { 1, bertInput.AttentionMask.Length });
using var typeIdsOrtValue = OrtValue.CreateTensorValueFromMemory(bertInput.TypeIds,
new long[] { 1, bertInput.TypeIds.Length });
// セッションの入力データを作成します。この場合、すべての出力を要求します。
var inputs = new Dictionary<string, OrtValue>
{
{ "input_ids", inputIdsOrtValue },
{ "input_mask", attMaskOrtValue },
{ "segment_ids", typeIdsOrtValue }
};
  • InferenceSessionを作成し、推論を実行して結果を出力します。
// セッションを実行し、入力データを送信して推論出力を取得します。
using var output = session.Run(runOptions, inputs, session.OutputNames);

outputを後処理して結果を出力する

Section titled “outputを後処理して結果を出力する”
  • ここで、開始位置(startLogit)と終了位置(endLogits)のインデックスを取得します。次に、入力文の元のtokensを取得し、予測されたトークンIDの語彙値を取得します。
// 出力リストから最大値のインデックスを取得します。
// アルゴリズムを使用するために、意図的に配列やリストにコピーしません。
// 将来、スパンでより多くのアルゴリズムが利用可能になることを願っています。
// これにより、ネイティブメモリから直接読み取り、
// 一部のモデルでは大きくなる可能性のあるデータを複製しないようにできます。
// ローカル関数
int GetMaxValueIndex(ReadOnlySpan<float> span)
{
float maxVal = span[0];
int maxIndex = 0;
for (int i = 1; i < span.Length; ++i)
{
var v = span[i];
if (v > maxVal)
{
maxVal = v;
maxIndex = i;
}
}
return maxIndex;
}
var startLogits = output[0].GetTensorDataAsSpan<float>();
int startIndex = GetMaxValueIndex(startLogits);
var endLogits = output[output.Count - 1].GetTensorDataAsSpan<float>();
int endIndex = GetMaxValueIndex(endLogits);
var predictedTokens = tokens
.Skip(startIndex)
.Take(endIndex + 1 - startIndex)
.Select(o => tokenizer.IdToToken((int)o.VocabularyIndex))
.ToList();
// 結果を出力します。
Console.WriteLine(String.Join(" ", predictedTokens));

この例では、単純なコンソールアプリを作成しましたが、これはC# Webアプリのようなもので簡単に実装できます。クイックスタート:ASP.NET Webアプリをデプロイするのドキュメントを確認してください。

さまざまなタスクに合わせてファインチューニングされたさまざまなBERTモデルや、特定のタスクに合わせてファインチューニングできるさまざまなベースモデルがあります。このコードはほとんどのBERTモデルで機能しますが、特定のモデルに合わせて入力、出力、前処理/後処理を更新してください。