この記事では、Phi3 モデルと ONNX Runtime Generative AI ライブラリを使用して単純な生成 AI チャット アプリを実装する WinUI アプリを作成する手順について説明します。 大規模言語モデル (LLM) を使用すると、テキスト生成、変換、推論、翻訳の機能をアプリに追加できます。 Windows アプリで AI モデルと機械学習モデルを使用する方法の詳細については、「Windows での AI の使用」を参照してください。 ONNX ランタイムと生成 AI の詳細については、「ONNX Runtime を使用した生成 AI」を参照してください。
AI 機能を利用する場合は、「Windows でのレスポンシブル生成 AI アプリケーションと機能の開発」を確認することをお勧めします。
ONNX Runtime とは
ONNX Runtime は、ハードウェア固有のライブラリを統合するための柔軟なインターフェイスを備えたクロスプラットフォーム機械学習モデル アクセラレータです。 ONNX Runtime は、PyTorch、Tensorflow/Keras、TFLite、scikit-learn、およびその他のフレームワークのモデルで使用できます。 詳しくは、ONNX Runtime の https://onnxruntime.ai/docs/ Web サイトを参照してください。
必須コンポーネント
- デバイスで開発者モードが有効になっている必要があります。 詳しくは、「デバイスを開発用に有効にする」をご覧ください。
- .NET デスクトップ開発ワークロードを含む Visual Studio 2022 以降。
新しい C# WinUI アプリを作成する
Visual Studio で、新しいプロジェクトを作成します。 [新しいプロジェクトの作成] ダイアログで言語フィルターを "C#"に、プロジェクト タイプ フィルターを "winui" に設定し、[空のアプリ 、パッケージ (デスクトップの WinUI3)] テンプレートを選択します。 新しいプロジェクトに「GenAIExample」という名前を付けます。
ONNX Runtime Generative AI NuGet パッケージへの参照を追加する
ソリューション エクスプローラーで、[依存関係] を右クリックし、[NuGet パッケージの管理...] を選択します。NuGet パッケージ マネージャーで、[参照] タブを選択します。"Microsoft.ML.OnnxRuntimeGenAI.WinML" を検索し、[バージョン] ドロップダウンで最新の安定バージョンを選択し、[インストール] をクリックします。
このパッケージでは、Windows ML を使用して、使用可能な最適なハードウェア実行プロバイダー (NPU → GPU → CPU) を自動的に選択します。 DirectML、QNN、または CPU 固有のパッケージのどれを選ぶかを気にする必要はありません。Windows ML が処理します。
注
.WinML パッケージには、Windows固有のターゲット フレームワーク (net8.0-windows10.0.19041.0 以降など) が必要です。 クロスプラットフォーム パッケージが必要な場合、または特定の実行プロバイダーをターゲットにする場合は、この記事の最後にある 代替パッケージ のセクションを参照してください。
モデルとボキャブラリ ファイルをプロジェクトに追加する
ソリューション エクスプローラーで、プロジェクトを右クリックして [追加]>[新しいフォルダー] を選択します。 新しいフォルダーに「Models」という名前を付けます。 この例では https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/directml/directml-int4-awq-block-128 のモデルを使用します。
モデルを取得するにはいくつかの方法があります。 このチュートリアルでは、Hugging Face コマンド ライン インターフェイス (CLI) を使用します。 別の方法を使用してモデルを取得する場合は、サンプル コードでファイル パスをモデルに合わせる必要がある場合があります。 Hugging Face CLI をインストールし、それを使用するようにアカウントを設定する方法については、「コマンド ライン インターフェイス (CLI)」を参照してください。
CLI をインストールしたら、ターミナルを開き、作成した Models ディレクトリに移動して次のコマンドを入力します。
huggingface-cli download microsoft/Phi-3-mini-4k-instruct-onnx --include directml/* --local-dir .
操作が完了したら、次のファイルが存在することを確認します: [Project Directory]\Models\directml\directml-int4-awq-block-128\model.onnx。
ソリューション エクスプローラーで "directml-int4-awq-block-128" フォルダーを展開し、フォルダー内のすべてのファイルを選択します。 ファイルのプロパティ ペインで、[出力ディレクトリにコピーする] を [新しい場合はコピーする] に設定します。
モデルを操作するためのシンプルな UI を追加する
この例では、プロンプトを指定するための TextBox、プロンプトを送信するための Button、ステータス メッセージとモデルからの応答を表示するための TextBlock を含む非常にシンプルな UI を作成します。
の既定の MainWindow.xaml 要素を次の XAML に置き換えます。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column ="0">
<TextBox x:Name="promptTextBox" Text="Compose a haiku about coding."/>
<Button x:Name="myButton" Click="myButton_Click">Submit prompt</Button>
</StackPanel>
<Border Grid.Column="1" Margin="20">
<TextBlock x:Name="responseTextBlock" TextWrapping="WrapWholeWords"/>
</Border>
</Grid>
モデルを初期化する
MainWindow.xaml.cs で、Microsoft.ML.OnnxRuntimeGenAI 名前空間の using ディレクティブを追加します。
using Microsoft.ML.OnnxRuntimeGenAI;
Model と Tokenizer の MainPage クラス定義内でメンバー変数を宣言します。 前の手順で追加したモデル ファイルの場所を設定します。
private Model? model = null;
private Tokenizer? tokenizer = null;
private readonly string ModelDir =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
@"Models\directml\directml-int4-awq-block-128");
モデルを非同期的に初期化するヘルパー メソッドを作成します。 このメソッドは Model クラスのコンストラクターを呼び出し、モデル ディレクトリへのパスを渡します。 次に、モデルから新しい Tokenizer を作成します。
public Task InitializeModelAsync()
{
DispatcherQueue.TryEnqueue(() =>
{
responseTextBlock.Text = "Loading model...";
});
return Task.Run(() =>
{
var sw = Stopwatch.StartNew();
model = new Model(ModelDir);
tokenizer = new Tokenizer(model);
sw.Stop();
DispatcherQueue.TryEnqueue(() =>
{
responseTextBlock.Text = $"Model loading took {sw.ElapsedMilliseconds} ms";
});
});
}
この例では、メイン ウィンドウがアクティブになったときにモデルを読み込みます。 ページ コンストラクターを更新して、Activated イベントのハンドラーを登録します。
public MainWindow()
{
this.InitializeComponent();
this.Activated += MainWindow_Activated;
}
Activated イベントは複数回発生させることができます。そのため、イベント ハンドラーで、モデルが初期化する前に null であることを確認します。
private async void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (model == null)
{
await InitializeModelAsync();
}
}
プロンプトをモデルに送信する
プロンプトをモデルに送信してから、IAsyncEnumerable を使用して呼び出し元に非同期的に結果を返すヘルパー メソッドを作成します。
このメソッドでは、Generator クラスがループで使用され、各パスで GenerateNextToken 呼び出して、モデルが次のいくつかの文字 (トークンと呼ばれる) を予測するものを取得します。これは、入力プロンプトに基づく必要があります。 入力トークン シーケンスは、 AppendTokenSequences を介してジェネレーターに提供されます。 このループは、ジェネレーター IsDone メソッドが true を返すか、またはトークン "<|end|>"、"<|system|>"、または "<|user|>" のいずれかを受信するまで実行され、これはトークンの生成を停止できることを示します。
public async IAsyncEnumerable<string> InferStreaming(string prompt)
{
if (model == null || tokenizer == null)
{
throw new InvalidOperationException("Model is not ready");
}
var generatorParams = new GeneratorParams(model);
var sequences = tokenizer.Encode(prompt);
generatorParams.SetSearchOption("max_length", 2048);
using var tokenizerStream = tokenizer.CreateStream();
using var generator = new Generator(model, generatorParams);
generator.AppendTokenSequences(sequences);
StringBuilder stringBuilder = new();
while (!generator.IsDone())
{
string part;
try
{
await Task.Delay(10).ConfigureAwait(false);
generator.GenerateNextToken();
part = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
stringBuilder.Append(part);
if (stringBuilder.ToString().Contains("<|end|>")
|| stringBuilder.ToString().Contains("<|user|>")
|| stringBuilder.ToString().Contains("<|system|>"))
{
break;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
break;
}
yield return part;
}
}
プロンプトを送信して結果を表示する UI コードを追加する
Button クリック ハンドラーで、最初にモデルが null ではないことを確認します。 システム プロンプトとユーザー プロンプトを使用してプロンプト文字列を作成し、InferStreamingを呼び出し、応答の各部分で TextBlock を更新します。
この例で使用されるモデルは、次の形式でプロンプトを受け入れるようにトレーニングされています。ここで、systemPrompt はモデルの動作に関する指示であり、userPrompt はユーザーからの質問です。
<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>
モデルでは、プロンプト規則を文書化する必要があります。 このモデルの形式は Huggingface モデル カードに記載されています。
private async void myButton_Click(object sender, RoutedEventArgs e)
{
responseTextBlock.Text = "";
if(model != null)
{
var systemPrompt = "You are a helpful assistant.";
var userPrompt = promptTextBox.Text;
var prompt = $@"<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>";
await foreach (var part in InferStreaming(prompt))
{
responseTextBlock.Text += part;
}
}
}
例を実行する
Visual Studio の [ソリューション プラットフォーム] ドロップダウンで、ターゲット プロセッサが x64 に設定されていることを確認します。 ONNXRuntime Generative AI ライブラリは x86 をサポートしていません。 プロジェクトをビルドして実行します。 モデルが読み込まれたことを TextBlock が示すのを待ちます。 プロンプト テキスト ボックスにプロンプトを入力し、送信ボタンをクリックします。 結果がテキストブロックに徐々に表示されるのを確認できるはずです。
代替 GenAI パッケージ
Windows ML の自動選択を使用するのではなく、特定の実行プロバイダーを対象にする必要がある場合は、.WinMLではなく、次のいずれかのパッケージを使用できます。
| Package | 利用シーン |
|---|---|
Microsoft.ML.OnnxRuntimeGenAI.DirectML |
GPU のみ (NVIDIA、AMD、Intel) |
Microsoft.ML.OnnxRuntimeGenAI.QNN |
NPU 専用 (Qualcomm) |
Microsoft.ML.OnnxRuntimeGenAI |
CPU 専用 (クロスプラットフォーム) |
同じプロジェクト内のこれらのパッケージを複数参照しないでください。競合するonnxruntime.dll ファイルが付属しています。 GenAI API コード (モデル、トークナイザー、ジェネレーター) は、使用するパッケージに関係なく同じです。
関連項目
- Windows で AI の使用を開始する
- 生成 AI を ONNX Runtime を使用して実行する
- AI 開発ギャラリー
- Windows AI API のサンプル