🐕

Azure OpenAI Service の C# SDK (ChatGPT でも使えます)

2023/04/16に公開

ChatGPT 旋風が吹き荒れてますね。
ChatGPT のオフィシャルの C# の SDK は無いのですが Microsoft 側が Azure OpenAI Service の C# SDK を公開しています。
API については互換性があるので、この SDK を使って ChatGPT を使うこともできます。

ありがたいですね。

ということで、この SDK を使って Azure OpenAI Service を試してみようと思います。私は手持ちの環境が Azure OpenAI Service の方が使いやすいので、そちらを使っていますが ChatGPT の方が使いやすい人はそっちを使っても同じコードで動かすことが出来ると思います。違うのはエンドポイントと API キーだけです。

インストール

NuGet から Azure.AI.OpenAI をインストールすれば完了です。注意点としては 2023/04/16 時点で最新が beta.5 なのでプレビュー版になります。インストール時にはプレビュー版のパッケージを対象にして検索しないと出てこないので注意してください。

https://www.nuget.org/packages/Azure.AI.OpenAI/1.0.0-beta.5

使い方

使い方は非常に簡単です。OpenAIClient クラスにエンドポイントと API キーを渡してクライアントのインスタンスを作成します。そして GetCompletionsAsync メソッドで使用するモデルの名前と、CompletionsOptions を渡して結果を取得します。CompletionsOptions には PromptMaxTokens があります。Prompt はプロンプトの文字列、MaxTokens は生成するトークンの数です。MaxTokens に指定可能な値はモデルによって異なります。Azure OpenAI Service の場合には以下のサイトに一覧で載っています。

https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/concepts/models

var client = new OpenAIClient(
    c.GetValue<Uri>("Endpoint"),
    new AzureKeyCredential(c.GetValue<string>("Key")!));
var result = await client.GetCompletionsAsync(
    c.GetValue<string>("ModelName"), 
    new CompletionsOptions
    {
        MaxTokens = 16,
        Prompts =
        {
            "Apple's color is",
            "My name is",
        }
    });

戻り値は以下のような構造のオブジェクトになります。基本的にはインテリセンスが効くので感覚で使えます。

{
  "Id": "cmpl-75ssDZATxGQE4xJOUQS202y3lROop",
  "Created": 1681636929,
  "Model": "gpt-35-turbo",
  "Choices": [
    {
      "Text": " often used for their product packaging and logo, but this isn\u0027t just coincidence.",
      "Index": 0,
      "Logprobs": null,
      "FinishReason": "length"
    },
    {
      "Text": "",
      "Index": 1,
      "Logprobs": null,
      "FinishReason": "content_filter"
    }
  ],
  "Usage": { "CompletionTokens": 32, "PromptTokens": 7, "TotalTokens": 39 }
}

純粋にプロンプトを 1 つ渡したいだけの場合は CompletionsOptions のかわりに文字列を直接渡すことも出来ます。

Completions 以外にも ChatGPT で使っているようなチャット形式にも対応しています。GetChatCompletionsAsync メソッドでモデル名と ChatCompletionsOptions を渡して呼び出すことが出来ます。ChatCompletionsOptions には MaxTokensMessages などを渡すことが出来ます。MessagesChatMessage のリストで ChatMessageRoleContent を渡します。

Role には System, Assistant, User の 3 つがあり、それぞれアシスタントにバックグラウンドや期待すべきことを伝えるための System と AI が回答した言葉を表す Assistant と、利用者の入力を表す User があります。例えば以下のように使用できます。

var client = new OpenAIClient(
    c.GetValue<Uri>("Endpoint"),
    new AzureKeyCredential(c.GetValue<string>("Key")!));
var result = await client.GetChatCompletionsAsync(
    c.GetValue<string>("ModelName"), 
    new ChatCompletionsOptions
    {
        MaxTokens = 200,
        Messages =
        {
            new ChatMessage(ChatRole.System, """
                あなたはピザのソムリエです。与えられた名前のピザを 200 文字以内で美味しそうに解説してください。
                ピザではない名前を言われた場合は解説は行わずに「わかりません。」と答えてください。
                """),
            new ChatMessage(ChatRole.User, "マルゲリータ"),
        }
    });

result は以下のような構造になっています。

{
  "HasValue": true,
  "Value": {
    "Id": "chatcmpl-75t9qAc7U0xc3URmRSw5PhT2GFOP0",
    "Created": "2023-04-16T09:40:22Z",
    "Choices": [
      {
        "Message": {
          "Role": { "Label": "assistant" },
          "Content": "ここに回答内容が入っています。"
        },
        "Index": 0,
        "FinishReason": "stop"
      }
    ],
    "Usage": {
      "CompletionTokens": 169,
      "PromptTokens": 108,
      "TotalTokens": 277
    }
  }
}

以下のようなコードを実行すると...

using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Configuration;

var c = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

var client = new OpenAIClient(
    c.GetValue<Uri>("Endpoint"),
    new AzureKeyCredential(c.GetValue<string>("Key")!));
var result = await client.GetChatCompletionsAsync(
    c.GetValue<string>("ModelName"), 
    new ChatCompletionsOptions
    {
        MaxTokens = 200,
        Messages =
        {
            new ChatMessage(ChatRole.System, """
                あなたはピザのソムリエです。与えられた名前のピザを 200 文字以内で美味しそうに解説してください。
                ピザではない名前を言われた場合は解説は行わずに「わかりません。」と答えてください。
                """),
            new ChatMessage(ChatRole.User, "マルゲリータ"),
        }
    });

Console.WriteLine(result.Value.Choices[0].Message.Content);

以下のような結果が得られます。

マルゲリータは、トマトソースとモッツァレラチーズをベースにバジルとオリーブオイルがトッピングされた、シンプルかつ王道的なイタリアンピザです。トマトの酸味とモッツァレラチーズのコクが絶妙にマッチし、バジルの香りが一層引き立てます。さらに、オーブンで薄く焼き上げた香ばしい生地の食感が口の中に広がり、食欲をそそります。イタリアのナポリ地方発祥のピザであり、

200 文字以内って言ったのに…。マルゲリータの部分を焼肉定食にして実行すると以下のような結果になりました。

わかりません。

優秀ですね。

その他にも埋め込みというものがあります。

https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/how-to/embeddings?tabs=console

これを使うと似たような雰囲気の文章をとってこれるような使い方が出来るみたいです。ちょっとやってみましょう。
これも OpenAIClient に、そのものズバリの名前の GetEmbeddingsAsync というメソッドがあります。引数はモデル名と EmbeddingsOptions というクラスのインスタンスを渡します。

試しに平家物語の現代語訳の冒頭と、適当な2フレーズの類似度を見てみたいと思います。類似度はコサイン類似度を使うと書いてあるのですが自前実装しないといけないのかな…?と思って自前で書きました。

using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Configuration;

var c = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

var client = new OpenAIClient(
    c.GetValue<Uri>("Endpoint"),
    new AzureKeyCredential(c.GetValue<string>("Key")!));
var document = await client.GetEmbeddingsAsync(
    "text-embedding-ada-002",
    new EmbeddingsOptions("祇園精舎の鐘の音は、諸行無常の響きをたてる。釈迦入滅の時に、白色に変じたという沙羅双樹の花の色は、盛者必衰の道理を表している。驕り高ぶった人も、末長く驕りにふける事はできない、ただ春の夜の夢のようにはかないものである。勇猛な者もついには滅びてしまう、全く風の前の塵と同じである。"));
var query1 = await client.GetEmbeddingsAsync(
    "text-embedding-ada-002",
    new EmbeddingsOptions("いけ!!ピカチュウ!!"));
var query2 = await client.GetEmbeddingsAsync(
    "text-embedding-ada-002",
    new EmbeddingsOptions("諸行無常の響きで何か沙羅双樹とかってのがあった"));

Console.WriteLine($"「いけ!!ピカチュウ!!」とのコサイン類似度: {CosineSimilarity(document.Value.Data[0], query1.Value.Data[0])}");
Console.WriteLine($"「諸行無常の響きで何か沙羅双樹とかってのがあった」とのコサイン類似度: {CosineSimilarity(document.Value.Data[0], query2.Value.Data[0])}");

// めっちゃ愚直に実装した…
// 参考元: https://mathlandscape.com/cos-similar/
static double CosineSimilarity(EmbeddingItem x, EmbeddingItem y)
{
    var numerator = x.Embedding.Zip(y.Embedding, (a, b) => a * b).Sum();
    var denominator = Math.Sqrt(x.Embedding.Sum(a => a * a)) * Math.Sqrt(y.Embedding.Sum(a => a * a));
    return numerator / denominator;
}

実行結果は、query2 のほうのそれっぽいフレーズのほうが元の平家物語との類似度が高いという結果になりました。あってるのかな???

「いけ!!ピカチュウ!!」とのコサイン類似度: 0.7888419628143366
「諸行無常の響きで何か沙羅双樹とかってのがあった」とのコサイン類似度: 0.8811832858189902

まとめ

ということで、C# で試してみましたが Completions, ChatCompletions, Embeddings については C# で簡単に出来ることがわかりました。
誰かコサイン類似度ってあれでいいのかわかる人がいたら教えてください m(_ _)m

Microsoft (有志)

Discussion