コンテンツにスキップ

C#でのStable Diffusion

C#とONNX RuntimeによるStable Diffusionの推論

Section titled “C#とONNX RuntimeによるStable Diffusionの推論”

このチュートリアルでは、人気のStable Diffusionディープラーニングモデルの推論をC#で行う方法を学びます。Stable Diffusionモデルは、テキストプロンプトを受け取り、そのテキストを表す画像を生成します。以下の例を参照してください。

"make a picture of green tree with flowers around it and a red sky"
(花に囲まれた緑の木と赤い空の絵を描いて)

|----------------------|----------------------| | サンプル画像で推論しているブラウザの画像。 | サンプル画像で推論しているブラウザの画像。 |

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

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

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

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

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

Hugging Faceサイトには、オープンソースモデルの素晴らしいライブラリがあります。Hugging FaceからONNX Stable Diffusionモデルを活用してダウンロードします。

モデルバージョンのリポジトリを選択したら、Files and Versionsをクリックし、ONNXブランチを選択します。ONNXモデルブランチが利用できない場合は、mainブランチを使用してONNXに変換します。詳細については、PyTorchのONNX変換チュートリアルを参照してください。

  • リポジトリをクローンします:
git lfs install
git clone https://huggingface.co/CompVis/stable-diffusion-v1-4 -b onnx
  • ONNXファイルを含むフォルダをC#プロジェクトフォルダ\StableDiffusion\StableDiffusionにコピーします。コピーするフォルダは、unetvae_decodertext_encodersafety_checkerです。

Hugging FaceのDiffusersを使用したPythonでのモデルの理解

Section titled “Hugging FaceのDiffusersを使用したPythonでのモデルの理解”

ビルド済みのモデルを取得して運用化する場合、このパイプラインのモデルを理解するために少し時間を取ると便利です。このコードは、Hugging Face Diffusersライブラリとブログに基づいています。それがどのように機能するかについて詳しく知りたい場合は、この素晴らしいブログ投稿をチェックしてください!

それでは、C#で推論する方法を分析し始めましょう!unetモデルは、テキストと画像を接続するCLIPモデルによって作成されたユーザープロンプトのテキスト埋め込みを受け取ります。潜在的なノイズの多い画像が開始点として作成されます。スケジューラアルゴリズムとunetモデルが連携して画像のノイズを除去し、テキストプロンプトを表す画像を生成します。コードを見てみましょう。

main関数は、プロンプト、推論ステップ数、およびガイダンススケールを設定します。次に、UNet.Inference関数を呼び出して推論を実行します。

設定する必要のあるプロパティは次のとおりです。

  • prompt - 画像に使用するテキストプロンプト
  • num_inference_steps - 推論を実行するステップ数。ステップ数が多いほど、推論ループの実行に時間がかかりますが、画質は向上するはずです。
  • guidance_scale - 分類器なしのガイダンスのスケール。数値が高いほど、プロンプトに似せようとしますが、画質が低下する可能性があります。
  • batch_size - 作成する画像の数
  • height - 画像の高さ。デフォルトは512で、8の倍数である必要があります。
  • width - 画像の幅。デフォルトは512で、8の倍数である必要があります。

* 注:詳細については、Hugging Faceブログを確認してください。

//デフォルト引数
var prompt = "make a picture of green tree with flowers around it and a red sky";
// ステップ数
var num_inference_steps = 10;
// 分類器なしのガイダンスのスケール
var guidance_scale = 7.5;
//要求された画像の数
var batch_size = 1;
// トークナイザーとテキストエンコーダーをロードして、テキストをトークン化およびエンコードします。
var textTokenized = TextProcessing.TokenizeText(prompt);
var textPromptEmbeddings = TextProcessing.TextEncode(textTokenized).ToArray();
// 空のトークンのuncond_inputを作成します
var uncondInputTokens = TextProcessing.CreateUncondInput();
var uncondEmbedding = TextProcessing.TextEncode(uncondInputTokens).ToArray();
// textEmeddingsとuncondEmbeddingを連結します
DenseTensor<float> textEmbeddings = new DenseTensor<float>(new[] { 2, 77, 768 });
for (var i = 0; i < textPromptEmbeddings.Length; i++)
{
textEmbeddings[0, i / 768, i % 768] = uncondEmbedding[i];
textEmbeddings[1, i / 768, i % 768] = textPromptEmbeddings[i];
}
var height = 512;
var width = 512;
// Stable Diffの推論
var image = UNet.Inference(num_inference_steps, textEmbeddings,guidance_scale, batch_size, height, width);
// 画像が失敗したか、安全でなかった場合はnullを返します。
if( image == null )
{
Console.WriteLine("Unable to create image, please try again.");
}

ONNX Runtime Extensionsによるトークン化

Section titled “ONNX Runtime Extensionsによるトークン化”

TextProcessingクラスには、テキストプロンプトをトークン化し、CLIPモデルテキストエンコーダーでエンコードする関数があります。

C#でCLIPトークナイザーを再実装する代わりに、ONNX Runtime ExtensionsのクロスプラットフォームCLIPトークナイザー実装を活用できます。ONNX Runtime Extensionsには、テキストプロンプトのトークン化に使用されるcustom_op_cliptok.onnxファイルトークナイザーがあります。トークナイザーは、テキストを単語に分割し、単語をトークンに変換する単純なトークナイザーです。

  • テキストプロンプト:作成したい画像を表す文またはフレーズ。
make a picture of green tree with flowers aroundit and a red sky
  • テキストのトークン化:テキストプロンプトはトークンのリストにトークン化されます。各トークンIDは文中の単語を表す数値であり、次に空白のトークンで埋められて77トークンのmaxLengthが作成されます。トークンIDは、形状(1,77)のテンソルに変換されます。

  • 以下は、ONNX Runtime Extensionsでテキストプロンプトをトークン化するためのコードです。

public static int[] TokenizeText(string text)
{
// トークナイザーを作成し、文をトークン化します。
var tokenizerOnnxPath = Directory.GetCurrentDirectory().ToString() + ("\\text_tokenizer\\custom_op_cliptok.onnx");
// extensionsのカスタムopのセッションオプションを作成します
using var sessionOptions = new SessionOptions();
var customOp = "ortextensions.dll";
sessionOptions.RegisterCustomOpLibraryV2(customOp, out var libraryHandle);
// onnxクリップトークナイザーからInferenceSessionを作成します。
using var tokenizeSession = new InferenceSession(tokenizerOnnxPath, sessionOptions);
// テキストから入力テンソルを作成します
using var inputTensor = OrtValue.CreateTensorWithEmptyStrings(OrtAllocator.DefaultInstance, new long[] { 1 });
inputTensor.StringTensorSetElementAt(text.AsSpan(), 0);
var inputs = new Dictionary<string, OrtValue>
{
{ "string_input", inputTensor }
};
// セッションを実行し、入力データを送信して推論出力を取得します。
using var runOptions = new RunOptions();
using var tokens = tokenizeSession.Run(runOptions, inputs, tokenizeSession.OutputNames);
var inputIds = tokens[0].GetTensorDataAsSpan<long>();
// inputIdsをInt32にキャストします
var InputIdsInt = new int[inputIds.Length];
for(int i = 0; i < inputIds.Length; i++)
{
InputIdsInt[i] = (int)inputIds[i];
}
Console.WriteLine(String.Join(" ", InputIdsInt));
var modelMaxLength = 77;
// 配列の長さがmodelMaxLengthになるまで49407でパディングします
if (InputIdsInt.Length < modelMaxLength)
{
var pad = Enumerable.Repeat(49407, 77 - InputIdsInt.Length).ToArray();
InputIdsInt = InputIdsInt.Concat(pad).ToArray();
}
return InputIdsInt;
}
tensor([[49406, 1078, 320, 1674, 539, 1901, 2677, 593, 4023, 1630,
585, 537, 320, 736, 2390, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407,
49407, 49407, 49407, 49407, 49407, 49407, 49407]])

CLIPテキストエンコーダーモデルによるテキスト埋め込み

Section titled “CLIPテキストエンコーダーモデルによるテキスト埋め込み”

トークンはテキストエンコーダーモデルに送信され、形状が(1, 77, 768)のテンソルに変換されます。ここで、最初の次元はバッチサイズ、2番目の次元はトークン数、3番目の次元は埋め込みサイズです。テキストエンコーダーは、テキストと画像を接続するOpenAI CLIPモデルです。

テキストエンコーダーは、テキストプロンプトを画像生成のガイドとして使用されるベクトルにエンコードするようにトレーニングされたテキスト埋め込みを作成します。テキスト埋め込みは、uncond埋め込みと連結されて、推論のためにunetモデルに送信されるテキスト埋め込みを作成します。

  • テキスト埋め込み:トークン化の結果から作成されたテキストプロンプトを表す数値のベクトル。テキスト埋め込みはtext_encoderモデルによって作成されます。
public static float[] TextEncoder(int[] tokenizedInput)
{
// 入力テンソルを作成します。OrtValueはコピーせず、マネージドメモリから読み取ります
using var input_ids = OrtValue.CreateTensorValueFromMemory<int>(tokenizedInput,
new long[] { 1, tokenizedInput.Count() });
var textEncoderOnnxPath = Directory.GetCurrentDirectory().ToString() + ("\\text_encoder\\model.onnx");
using var encodeSession = new InferenceSession(textEncoderOnnxPath);
// マネージドバッファに出力されるように出力を事前に割り当てます
// 形状がわかっています
var lastHiddenState = new float[1 * 77 * 768];
using var outputOrtValue = OrtValue.CreateTensorValueFromMemory<float>(lastHiddenState, new long[] { 1, 77, 768 });
string[] input_names = { "input_ids" };
OrtValue[] inputs = { input_ids };
string[] output_names = { encodeSession.OutputNames[0] };
OrtValue[] outputs = { outputOrtValue };
// 推論を実行します。
using var runOptions = new RunOptions();
encodeSession.Run(runOptions, input_names, inputs, output_names, outputs);
return lastHiddenState;
}
torch.Size([1, 77, 768])
tensor([[[-0.3884, 0.0229, -0.0522, ..., -0.4899, -0.3066, 0.0675],
[ 0.0520, -0.6046, 1.9268, ..., -0.3985, 0.9645, -0.4424],
[-0.8027, -0.4533, 1.7525, ..., -1.0365, 0.6296, 1.0712],
...,
[-0.6833, 0.3571, -1.1353, ..., -1.4067, 0.0142, 0.3566],
[-0.7049, 0.3517, -1.1524, ..., -1.4381, 0.0090, 0.3777],
[-0.6155, 0.4283, -1.1282, ..., -1.4256, -0.0285, 0.3206]]],

推論ループ:UNetモデル、タイムステップ、LMSスケジューラ

Section titled “推論ループ:UNetモデル、タイムステップ、LMSスケジューラ”

スケジューラアルゴリズムとunetモデルが連携して画像のノイズを除去し、テキストプロンプトを表す画像を生成します。使用できるさまざまなスケジューラアルゴリズムがあります。詳細については、Hugging Faceのこのブログを確認してください。この例では、Hugging Faceのscheduling_lms_discrete.pyに基づいて作成されたLMSDiscreteSchedulerを使用します。

推論ループは、スケジューラアルゴリズムとunetモデルを実行するメインループです。ループは、推論ステップ数やその他のパラメータに基づいてスケジューラアルゴリズムによって計算されるtimestepsの数だけ実行されます。

この例では、次のタイムステップを計算した10の推論ステップがあります。

// 推論セッションを作成するためのモデルへのパスを取得します。
var modelPath = Directory.GetCurrentDirectory().ToString() + ("\\unet\\model.onnx");
var scheduler = new LMSDiscreteScheduler();
var timesteps = scheduler.SetTimesteps(numInferenceSteps);
tensor([999., 888., 777., 666., 555., 444., 333., 222., 111., 0.])

latentsは、モデル入力で使用されるノイズの多い画像テンソルです。GenerateLatentSample関数を使用して作成され、形状(1,4,64,64)のランダムなテンソルが作成されます。seedは、乱数または固定数に設定できます。seedが固定数に設定されている場合、毎回同じ潜在テンソルが使用されます。これは、デバッグや毎回同じ画像を作成したい場合に便利です。

var seed = new Random().Next();
var latents = GenerateLatentSample(batchSize, height, width,seed, scheduler.InitNoiseSigma);
サンプル画像で推論しているブラウザの画像。

各推論ステップで、潜在画像が複製されて(2,4,64,64)のテンソル形状が作成され、次にスケーリングされてunetモデルで推論されます。出力テンソル(2,4,64,64)は分割され、ガイダンスが適用されます。結果のテンソルは、ノイズ除去プロセスの一部としてLMSDiscreteSchedulerステップに送信され、スケジューラステップからの結果のテンソルが返され、num_inference_stepsに達するまでループが再び完了します。

var modelPath = Directory.GetCurrentDirectory().ToString() + ("\\unet\\model.onnx");
var scheduler = new LMSDiscreteScheduler();
var timesteps = scheduler.SetTimesteps(numInferenceSteps);
var seed = new Random().Next();
var latents = GenerateLatentSample(batchSize, height, width, seed, scheduler.InitNoiseSigma);
// 推論セッションを作成
using var options = new SessionOptions();
using var unetSession = new InferenceSession(modelPath, options);
var latentInputShape = new int[] { 2, 4, height / 8, width / 8 };
var splitTensorsShape = new int[] { 1, 4, height / 8, width / 8 };
for (int t = 0; t < timesteps.Length; t++)
{
// torch.cat([latents] * 2)
var latentModelInput = TensorHelper.Duplicate(latents.ToArray(), latentInputShape);
// 入力をスケーリング
latentModelInput = scheduler.ScaleInput(latentModelInput, timesteps[t]);
// テキスト埋め込み、スケーリングされた潜在画像、タイムステップのモデル入力を作成
var input = CreateUnetModelInput(textEmbeddings, latentModelInput, timesteps[t]);
// 推論を実行
using var output = unetSession.Run(input);
var outputTensor = output[0].Value as DenseTensor<float>;
// 2,4,64,64から1,4,64,64にテンソルを分割
var splitTensors = TensorHelper.SplitTensor(outputTensor, splitTensorsShape);
var noisePred = splitTensors.Item1;
var noisePredText = splitTensors.Item2;
// ガイダンスを実行
noisePred = performGuidance(noisePred, noisePredText, guidanceScale);
// LMSスケジューラーステップ
latents = scheduler.Step(noisePred, timesteps[t], latents);
}

推論ループが完了すると、結果のテンソルがスケーリングされ、次にvae_decoderモデルに送信されて画像がデコードされます。最後に、デコードされた画像テンソルが画像に変換され、ディスクに保存されます。

public static Tensor<float> Decoder(List<NamedOnnxValue> input)
{
// 潜在変数を画像空間にデコードするために使用されるモデルをロードします。
var vaeDecoderModelPath = Directory.GetCurrentDirectory().ToString() + ("\\vae_decoder\\model.onnx");
// モデルパスからInferenceSessionを作成します。
var vaeDecodeSession = new InferenceSession(vaeDecoderModelPath);
// セッションを実行し、入力データを送信して推論出力を取得します。
var output = vaeDecodeSession.Run(input);
var result = (output.ToList().First().Value as Tensor<float>);
return result;
}
public static Image<Rgba32> ConvertToImage(Tensor<float> output, int width = 512, int height = 512, string imageName = "sample")
{
var result = new Image<Rgba32>(width, height);
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
result[x, y] = new Rgba32(
(byte)(Math.Round(Math.Clamp((output[0, 0, y, x] / 2 + 0.5), 0, 1) * 255)),
(byte)(Math.Round(Math.Clamp((output[0, 1, y, x] / 2 + 0.5), 0, 1) * 255)),
(byte)(Math.Round(Math.Clamp((output[0, 2, y, x] / 2 + 0.5), 0, 1) * 255))
);
}
}
result.Save($@"C:/code/StableDiffusion/{imageName}.png");
return result;
}

結果の画像:

image

これは、C#でStable Diffusionを実行する方法の概要です。主要な概念をカバーし、実装方法の例を提供しました。完全なコードを入手するには、Stable Diffusion C#サンプルを確認してください。