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コンピューティングを活用してクラウドで実行することもできます。
ローカルで実行するには:
-
Visual Studio または VS Code
-
Windows上のCUDAまたはDirectMLを備えたGPU対応マシン
- CUDA EPを構成します。このチュートリアルに従って、Windows 11でONNX RuntimeとC#を使用してGPU用のCUDAとcuDNNを構成します
- WindowsにはDirectMLサポートが付属しています。追加の構成は必要ありません。このオプションを選択する場合は、このリポジトリの
direct-ML-EPブランチをクローンしてください。 - これはGTX 3070で構築されており、それより小さいものではテストされていません。
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 installgit clone https://huggingface.co/CompVis/stable-diffusion-v1-4 -b onnx- ONNXファイルを含むフォルダをC#プロジェクトフォルダ
\StableDiffusion\StableDiffusionにコピーします。コピーするフォルダは、unet、vae_decoder、text_encoder、safety_checkerです。
Hugging FaceのDiffusersを使用したPythonでのモデルの理解
Section titled “Hugging FaceのDiffusersを使用したPythonでのモデルの理解”ビルド済みのモデルを取得して運用化する場合、このパイプラインのモデルを理解するために少し時間を取ると便利です。このコードは、Hugging Face Diffusersライブラリとブログに基づいています。それがどのように機能するかについて詳しく知りたい場合は、この素晴らしいブログ投稿をチェックしてください!
C#での推論
Section titled “C#での推論”それでは、C#で推論する方法を分析し始めましょう!unetモデルは、テキストと画像を接続するCLIPモデルによって作成されたユーザープロンプトのテキスト埋め込みを受け取ります。潜在的なノイズの多い画像が開始点として作成されます。スケジューラアルゴリズムとunetモデルが連携して画像のノイズを除去し、テキストプロンプトを表す画像を生成します。コードを見てみましょう。
Main関数
Section titled “Main関数”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スケジューラ”スケジューラ
Section titled “スケジューラ”スケジューラアルゴリズムとunetモデルが連携して画像のノイズを除去し、テキストプロンプトを表す画像を生成します。使用できるさまざまなスケジューラアルゴリズムがあります。詳細については、Hugging Faceのこのブログを確認してください。この例では、Hugging Faceのscheduling_lms_discrete.pyに基づいて作成されたLMSDiscreteSchedulerを使用します。
タイムステップ
Section titled “タイムステップ”推論ループは、スケジューラアルゴリズムと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);}VAEDecoderによるoutputの後処理
Section titled “VAEDecoderによるoutputの後処理”推論ループが完了すると、結果のテンソルがスケーリングされ、次に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;}結果の画像:

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