次の方法で共有


ONNX Runtime Generative AI を使用して、Windows アプリで Phi3 やその他の言語モデルを使ってみる

この記事では、Phi3 モデルと ONNX Runtime Generative AI ライブラリを使用して単純な生成 AI チャット アプリを実装する WinUI 3 アプリを作成する手順について説明します。 大きな言語モデル (LLM) を使用すると、テキスト生成、変換、推論、翻訳の機能をアプリに追加できます。 Windows アプリで AI モデルと機械学習モデルを使用する方法の詳細については、「Windows アプリで AI モデルと Machine Learning モデルの使用を開始する」を参照してください。 ONNX ランタイムとジェネレーティブ AI の詳細については、「を使用した Generative AI」を参照してください。

AI 機能を利用する場合は、「Windows でのレスポンシブル生成 AI アプリケーションと機能の開発」を確認することをお勧めします。

ONNX Runtime とは

ONNX Runtime は、ハードウェア固有のライブラリを統合するための柔軟なインターフェイスを備えたクロスプラットフォーム機械学習モデル アクセラレータです。 ONNX Runtime は、PyTorch、Tensorflow/Keras、TFLite、scikit-learn、およびその他のフレームワークのモデルで使用できます。 詳細については、https://onnxruntime.ai/docs/の ONNX Runtime Web サイトを参照してください。

前提 条件

  • デバイスで開発者モードが有効になっている必要があります。 詳しくは、「デバイスを開発用に有効にする」をご覧ください。
  • .NET デスクトップ開発ワークロードを含む Visual Studio 2022 以降。

新しい C# WinUI アプリを作成する

Visual Studio で、新しいプロジェクトを作成します。 [新しいプロジェクト の作成] ダイアログで、言語フィルターを "C#" に設定し、プロジェクトの種類フィルターを "winui" に設定し、空のアプリ 、パッケージ (デスクトップの WinUI3) テンプレートを選択します。 新しいプロジェクトに "GenAIExample" という名前を付けます。

ONNX Runtime Generative AI Nuget パッケージへの参照を追加する

ソリューション エクスプローラーで、[依存関係] を右クリックし、[NuGet パッケージの管理] を選択します。NuGet パッケージ マネージャーで、[参照] タブを選択します。「Microsoft.ML.OnnxRuntimeGenAI.DirectML」を検索し、[バージョン] ドロップダウンで最新の安定バージョンを選択して [インストール] をクリックします。

モデルとボキャブラリ ファイルをプロジェクトに追加する

ソリューション エクスプローラーで、プロジェクトを右クリックし、[追加][新しいフォルダー]選択します。 新しいフォルダーに "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 の既定の StackPanel 要素を次の 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;

ModelTokenizerMainPage クラス定義内でメンバー変数を宣言します。 前の手順で追加したモデル ファイルの場所を設定します。

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 呼び出して、モデルが次のいくつかの文字 (トークンと呼ばれる) を予測するものを取得します。これは、入力プロンプトに基づく必要があります。 このループは、ジェネレーター 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);
    generatorParams.SetInputSequences(sequences);
    generatorParams.TryGraphCaptureWithMaxBatchSize(1);

    using var tokenizerStream = tokenizer.CreateStream();
    using var generator = new Generator(model, generatorParams);
    StringBuilder stringBuilder = new();
    while (!generator.IsDone())
    {
        string part;
        try
        {
            await Task.Delay(10).ConfigureAwait(false);
            generator.ComputeLogits();
            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 コードを追加する

ボタン クリック ハンドラーで、最初にモデルが 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 がモデルが読み込まれたことを示すのを待ちます。 プロンプト テキスト ボックスにプロンプトを入力し、[送信] ボタンをクリックします。 結果がテキストブロックに徐々に表示されるのを確認できるはずです。

関連項目