Inferenz mit C# BERT NLP Deep Learning und ONNX Runtime
In diesem Tutorial lernen wir, wie man Inferenz für das beliebte BERT Natural Language Processing Deep Learning-Modell in C# durchführt.
Um unseren Text in C# vorverarbeiten zu können, nutzen wir die Open-Source-BERTTokenizers, die Tokenizer für die meisten BERT-Modelle enthält. Unterstützte Modelle finden Sie unten.
- BERT Base
- BERT Large
- BERT Deutsch
- BERT Multilingual
- BERT Base Uncased
- BERT Large Uncased
Es gibt viele Modelle (einschließlich des Modells für dieses Tutorial), die auf diesen Basismodellen feinabgestimmt wurden. Der Tokenizer für das Modell ist immer noch derselbe wie für das Basismodell, von dem es feinabgestimmt wurde.
Inhalt
- Voraussetzungen
- Hugging Face zum Herunterladen des BERT-Modells verwenden
- Das Modell in Python verstehen
- Inferenz mit C#
- Mit Azure Web App bereitstellen
- Nächste Schritte
Voraussetzungen
Dieses Tutorial kann lokal oder durch Nutzung von Azure Machine Learning Compute ausgeführt werden.
Lokal ausführen
In der Cloud mit Azure Machine Learning ausführen
Hugging Face zum Herunterladen des BERT-Modells verwenden
Hugging Face verfügt über eine hervorragende API zum Herunterladen von Open-Source-Modellen, und wir können dann Python und Pytorch verwenden, um sie in das ONNX-Format zu exportieren. Dies ist eine großartige Option, wenn Sie ein Open-Source-Modell verwenden, das nicht bereits Teil des ONNX Model Zoo ist.
Schritte zum Herunterladen und Exportieren unseres Modells in Python
Verwenden Sie die transformers API, um das Modell BertForQuestionAnswering mit dem Namen bert-large-uncased-whole-word-masking-finetuned-squad herunterzuladen.
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)
# set the model to inference mode
# It is important to call torch_model.eval() or torch_model.train(False) before exporting the model,
# to turn the model to inference mode. This is required since operators like dropout or batchnorm
# behave differently in inference and training mode.
model.eval()
Nachdem wir das Modell heruntergeladen haben, müssen wir es in das ONNX-Format exportieren. Dies ist in Pytorch mit der Funktion torch.onnx.export integriert.
-
Die Variable
inputsgibt die Eingabeform an. Sie können entweder eine Dummy-Eingabe wie unten erstellen oder eine Beispiel-Eingabe aus dem Testen des Modells verwenden. -
Stellen Sie
opset_versionauf die höchste und kompatible Version mit dem Modell ein. Erfahren Sie mehr über die Opsets-Versionen hier. -
Stellen Sie
input_namesundoutput_namesfür das Modell ein. -
Setzen Sie
dynamic_axesfür die Eingabe mit dynamischer Länge, da die Variablensentenceundcontextfür jede inferierte Frage unterschiedliche Längen haben werden.
# Generate dummy inputs to the model. Adjust if necessary.
inputs = {
# list of numerical ids for the tokenized text
'input_ids': torch.randint(32, [1, 32], dtype=torch.long),
# dummy list of ones
'attention_mask': torch.ones([1, 32], dtype=torch.long),
# dummy list of ones
'token_type_ids': torch.ones([1, 32], dtype=torch.long)
}
symbolic_names = {0: 'batch_size', 1: 'max_seq_len'}
torch.onnx.export(model,
# model being run
(inputs['input_ids'],
inputs['attention_mask'],
inputs['token_type_ids']), # model input (or a tuple for multiple inputs)
model_path, # where to save the model (can be a file or file-like object)
opset_version=11, # the ONNX version to export the model to
do_constant_folding=True, # whether to execute constant folding for optimization
input_names=['input_ids',
'input_mask',
'segment_ids'], # the model's input names
output_names=['start_logits', "end_logits"], # the model's output names
dynamic_axes={'input_ids': symbolic_names,
'input_mask' : symbolic_names,
'segment_ids' : symbolic_names,
'start_logits' : symbolic_names,
'end_logits': symbolic_names}) # variable length axes/dynamic input
Das Modell in Python verstehen
Wenn Sie ein vorgefertigtes Modell nehmen und es operationalisieren, ist es nützlich, sich einen Moment Zeit zu nehmen, um die Vor- und Nachverarbeitung des Modells sowie die Ein-/Ausgabeformen und -labels zu verstehen. Viele Modelle haben Beispielcode in Python bereitgestellt. Wir werden die Inferenz unseres Modells mit C# durchführen, aber lassen Sie uns zuerst testen und sehen, wie es in Python gemacht wird. Dies wird uns bei unserer C#-Logik im nächsten Schritt helfen.
-
Der Code zum Testen des Modells ist in diesem Tutorial enthalten. Schauen Sie sich den Quellcode zum Testen und Inferieren dieses Modells in Python an. Unten sehen Sie eine Beispiel-
input-Satz und ein Beispiel-outputaus der Ausführung des Modells. -
Beispiel-
input
input = "{\"question\": \"What is Dolly Parton's middle name?\", \"context\": \"Dolly Rebecca Parton is an American singer-songwriter\"}"
print(run(input))
- So sollte die Ausgabe für die obige Frage aussehen. Sie können die
input_idsverwenden, um die Tokenisierung in C# zu validieren.
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'}
Inferenz mit C#
Nachdem wir das Modell in Python getestet haben, ist es Zeit, es in C# zu erstellen. Das Erste, was wir tun müssen, ist, unser Projekt zu erstellen. Für dieses Beispiel verwenden wir eine Konsolenanwendung, aber Sie könnten diesen Code in jeder C#-Anwendung verwenden.
- Öffnen Sie Visual Studio und Erstellen Sie eine Konsolenanwendung.
Installieren Sie die NuGet-Pakete
- Installieren Sie die NuGet-Pakete
BERTTokenizers,Microsoft.ML.OnnxRuntime,Microsoft.ML.OnnxRuntime.Managed,Microsoft.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
Erstellen Sie die App
- Importieren Sie die Pakete.
using BERTTokenizers;
using Microsoft.ML.Data;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
- Fügen Sie den
namespace, dieclassund dieMain-Funktion hinzu.
namespace MyApp // Note: actual namespace depends on the project name.
{
internal class BertTokenizeProgram
{
static void Main(string[] args)
{
}
}
}
Erstellen Sie die BertInput-Klasse für die Kodierung
- Fügen Sie die
BertInput-Struktur hinzu.
public struct BertInput
{
public long[] InputIds { get; set; }
public long[] AttentionMask { get; set; }
public long[] TypeIds { get; set; }
}
Tokenisieren Sie den Satz mit dem BertUncasedLargeTokenizer
- Erstellen Sie einen Satz (Frage und Kontext) und tokenisieren Sie den Satz mit dem
BertUncasedLargeTokenizer. Das Basismodell istbert-large-uncased, daher verwenden wir denBertUncasedLargeTokenizeraus der Bibliothek. Stellen Sie sicher, dass Sie das Basismodell für Ihr BERT-Modell überprüfen, um zu bestätigen, dass Sie den richtigen Tokenizer verwenden.
var sentence = "{\"question\": \"Where is Bob Dylan From?\", \"context\": \"Bob Dylan is from Duluth, Minnesota and is an American singer-songwriter\"}";
Console.WriteLine(sentence);
// Create Tokenizer and tokenize the sentence.
var tokenizer = new BertUncasedLargeTokenizer();
// Get the sentence tokens.
var tokens = tokenizer.Tokenize(sentence);
// Console.WriteLine(String.Join(", ", tokens));
// Encode the sentence and pass in the count of the tokens in the sentence.
var encoded = tokenizer.Encode(tokens.Count(), sentence);
// Break out encoding to InputIds, AttentionMask and TypeIds from list of (input_id, attention_mask, type_id).
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(),
};
Erstellen Sie die inputs als Name -> OrtValue-Paare, wie für die Inferenz erforderlich
- Holen Sie sich das Modell, erstellen Sie drei OrtValues über den Eingabepuffern und verpacken Sie sie in einem Dictionary, das an Run() übergeben wird. Beachten Sie, dass fast alle Onnxruntime-Klassen native Datenstrukturen umschließen und daher entsorgt werden müssen, um Speicherlecks zu vermeiden.
// Get path to model to create inference session.
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);
// Create input tensors over the input data.
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 });
// Create input data for session. Request all outputs in this case.
var inputs = new Dictionary<string, OrtValue>
{
{ "input_ids", inputIdsOrtValue },
{ "input_mask", attMaskOrtValue },
{ "segment_ids", typeIdsOrtValue }
};
Inferenz ausführen
- Erstellen Sie die
InferenceSession, führen Sie die Inferenz aus und geben Sie das Ergebnis aus.
// Run session and send the input data in to get inference output.
using var output = session.Run(runOptions, inputs, session.OutputNames);
Verarbeiten Sie die output nach und geben Sie das Ergebnis aus
- Hier erhalten wir den Index für die Startposition (
startLogit) und die Endposition (endLogits). Dann nehmen wir die ursprünglichentokensdes Eingabesatzes und ermitteln den Vokabelwert für die vorhergesagten Token-IDs.
// Get the Index of the Max value from the output lists.
// We intentionally do not copy to an array or to a list to employ algorithms.
// Hopefully, more algos will be available in the future for spans.
// so we can directly read from native memory and do not duplicate data that
// can be large for some models
// Local function
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();
// Print the result.
Console.WriteLine(String.Join(" ", predictedTokens));
Mit Azure Web App bereitstellen
In diesem Beispiel haben wir eine einfache Konsolenanwendung erstellt, diese könnte aber auch leicht in etwas wie eine C#-Webanwendung implementiert werden. Schauen Sie sich die Dokumentation an, wie man Quickstart: Eine ASP.NET-Web-App bereitstellen.
Nächste Schritte
Es gibt viele verschiedene BERT-Modelle, die für unterschiedliche Aufgaben feinabgestimmt wurden, und unterschiedliche Basismodelle, die Sie für Ihre spezifische Aufgabe feinabstimmen können. Dieser Code funktioniert für die meisten BERT-Modelle. Aktualisieren Sie einfach die Ein-/Ausgabe- und Vor-/Nachverarbeitung für Ihr spezifisches Modell.