1. 概要
こちらのリンク先の Google Colab のコードセルを実行し、FLUX.2 [klein] 4B Base の guidance scale について調査しました。
guidance scale はテキストプロンプトをどの程度強く画像生成に反映させるかを制御する係数です。
2. diffusers の Flux2KleinPipeline における guidance scale の参照について
まず、diffusers の下記のファイルに記載された class Flux2KleinPipeline で guidance scale がどのように参照されているかを確認しました。
src/diffusers/pipelines/flux2/pipeline_flux2_klein.py
guidance scale は、上記のファイルに下記のように記載されています。FLUX.2 [klein] 4B では、self.config.is_distilled が true のため、guidance scale に 1.0 より大きな値をセットしても無視されます。
if guidance_scale > 1.0 and self.config.is_distilled:
logger.warning(f"Guidance scale {guidance_scale} is ignored for step-wise distilled models.")
@property
def guidance_scale(self):
return self._guidance_scale
@property
def do_classifier_free_guidance(self):
return self._guidance_scale > 1 and not self.config.is_distilled
FLUX.2 [klein] 4B Base では、self.config.is_distilled が false のため、guidance scale に 1.0 より大きな値をセットすると生成される画像が変化します。今回は、FLUX.2 [klein] 4B Base を使用して動作確認しました。
3. diffusers の Flux2KleinPipeline における guidance scale の効果について
画像を生成する際に実行される Flux2KleinPipeline の __call__ メソッドの Denoising loop 内に下記のようなスクリプトが記載されています。二カ所で呼ばれているself.transformer(...)の引数 guidance には None がセットされています。
with self.transformer.cache_context("cond"):
noise_pred = self.transformer(
hidden_states=latent_model_input, # (B, image_seq_len, C)
timestep=timestep / 1000,
guidance=None,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids, # B, text_seq_len, 4
img_ids=latent_image_ids, # B, image_seq_len, 4
joint_attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]
noise_pred = noise_pred[:, : latents.size(1) :]
if self.do_classifier_free_guidance:
with self.transformer.cache_context("uncond"):
neg_noise_pred = self.transformer(
hidden_states=latent_model_input,
timestep=timestep / 1000,
guidance=None,
encoder_hidden_states=negative_prompt_embeds,
txt_ids=negative_text_ids,
img_ids=latent_image_ids,
joint_attention_kwargs=self._attention_kwargs,
return_dict=False,
)[0]
neg_noise_pred = neg_noise_pred[:, : latents.size(1) :]
noise_pred = neg_noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
上記のスクリプトのnoise_pred = self.transformer(...)は、更新前の潜在空間における画像データ latent_model_input、テキストデータ prompt_embeds、timestep を渡して生成画像を更新するための速度ベクトル noise_pred を計算します。
次に、一つ目のハイライトした行if self.do_classifier_free_guidance:の条件が満たされたら、neg_noise_pred = self.transformer(...)が計算されます。先ほどのself.transformer(...)の呼び出しとの違いは、通常の prompt の代わりに negative prompt のデータが渡される点です。
上記のスクリプト末尾のハイライトした行で下記の計算をしています。
noise_pred = neg_noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
guidance scale が 1.0 ならば、noise_pred は、neg_noise_pred の影響を受けないことになります。guidance scale が 1.0 より大きいときは、prompt の影響を強くすると同時に negative prompt の出力を抑える効果が働きます。
今回の動作確認では、negative prompt は特に指定しませんでした。このとき、何も指定しない空文字列が negative prompt として参照されることをデバッグ実行で確認しています。
prompt を指定して生成した画像更新の速度ベクトル noise_pred と prompt に何も指定しないで生成した画像更新の速度ベクトル neg_noise_pred の差を guidance scale 倍して加えることにより、guidance scale が大きいほどより prompt に忠実な画像が生成されるようにしています。
補足:
noise_pred は、画像に対応する潜在空間の多次元配列データを更新する速度ベクトルなので、上記の式で guidance scale を大きくし過ぎるとモデルの学習分布の外側に多次元配列データがはみ出てしまい、画像生成処理が破綻してしまいます。
4. デバッグ実行による確認
こちらのリンク先の Google Colab のコードセルでは、下記のようにFlux2KleinPipeline.__call__のif self.do_classifier_free_guidance:の直前にブレイクポイントがセットされたコードで動作確認しています。
diff --git a/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py b/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py
index 936d2c380..4ebcc84c8 100644
--- a/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py
+++ b/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py
@@ -738,7 +738,9 @@ class Flux2KleinPipeline(DiffusionPipeline, Flux2LoraLoaderMixin):
max_sequence_length=max_sequence_length,
text_encoder_out_layers=text_encoder_out_layers,
)
-
+
+ import ipdb; ipdb.set_trace()
+
if self.do_classifier_free_guidance:
negative_prompt = ""
if prompt is not None and isinstance(prompt, list):
下記のログは、デバッグ実行のログになります。下記のログでは、negative_prompt に空文字列、negative_prompt_embeds に NaN がセットされた状態から、空文字列に対応する negative_prompt_embeds が計算されることを確認しました。
ipdb> n
> /content/diffusers/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py(749)__call__()
748 negative_prompt_embeds, negative_text_ids = self.encode_prompt(
--> 749 prompt=negative_prompt,
750 prompt_embeds=negative_prompt_embeds,
ipdb> n
> /content/diffusers/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py(750)__call__()
749 prompt=negative_prompt,
--> 750 prompt_embeds=negative_prompt_embeds,
751 device=device,
ipdb> p negative_prompt
''
ipdb> p negative_prompt_embeds
None
下記のログは Denoising loop の直前までスクリプトを実行し、negative_prompt_embeds と timesteps を確認したログです。negative_prompt_embeds は空文字列から生成されたトークンのデータで、torch.Size([1, 512, 7680]) のような多次元配列となっています。
timesteps は、noise_pred = self.transformer(...)に引数として順に渡していく timestep データの配列です。FLUX.2 [klein] 4B Base は、FLUX.2 [klein] 4B よりも画像生成に多くのステップ数を要します。今回の動作確認では、num_inference_steps=50 を指定しました。50 ステップの実行で、timestep は、1000 から始まり、少しずつ小さな値になっています。1000.0000, 997.3091, 994.5214 のように最初は少しずつ小さくなり、325.5933, 239.6312, 133.7189 のように末尾の 50 ステップ目に近付くほど値が大きく変化しています。
> /content/diffusers/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py(744)__call__()
743
--> 744 if self.do_classifier_free_guidance:
745 negative_prompt = ""
ipdb> until 827
> /content/diffusers/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py(827)__call__()
826 # Check out more details here: https://github.com/huggingface/diffusers/pull/11696
--> 827 self.scheduler.set_begin_index(0)
828 with self.progress_bar(total=num_inference_steps) as progress_bar:
ipdb> p negative_prompt
''
ipdb> p negative_prompt_embeds
tensor([[[-7.7200e+02, 6.0312e+00, -2.5875e+01, ..., 1.3750e+00,
5.3438e+00, 1.7734e+00],
[ 3.4250e+01, 2.4062e+00, -4.5938e+00, ..., -2.1289e-01,
-8.6328e-01, 1.0859e+00],
[ 2.9250e+01, 3.0156e+00, -2.0938e+00, ..., 1.0312e+00,
-8.4766e-01, 1.7812e+00],
...,
[ 2.3750e+01, 7.1875e-01, -5.1562e-01, ..., -1.6875e+00,
7.9688e-01, 1.5156e+00],
[ 2.4125e+01, 7.6172e-01, -5.8594e-01, ..., -1.4609e+00,
7.7344e-01, 1.2344e+00],
[ 2.4250e+01, 8.2812e-01, -7.5781e-01, ..., -1.0469e+00,
1.3281e+00, 1.9375e+00]]], device='cuda:0', dtype=torch.bfloat16)
ipdb> p negative_prompt_embeds.shape
torch.Size([1, 512, 7680])
ipdb> p timesteps
tensor([1000.0000, 997.3091, 994.5214, 991.6316, 988.6340, 985.5225,
982.2905, 978.9307, 975.4355, 971.7964, 968.0046, 964.0501,
959.9222, 955.6093, 951.0987, 946.3763, 941.4270, 936.2340,
930.7789, 925.0412, 918.9985, 912.6257, 905.8951, 898.7756,
891.2326, 883.2271, 874.7151, 865.6472, 855.9669, 845.6102,
834.5034, 822.5620, 809.6884, 795.7689, 780.6708, 764.2374,
746.2838, 726.5886, 704.8854, 680.8509, 654.0879, 624.1038,
590.2800, 551.8284, 507.7296, 456.6408, 396.7571, 325.5933,
239.6312, 133.7189], device='cuda:0')
5. guidance scale を変えて出力画像を確認
こちらのリンク先の Google Colab のコードセルを順に実行し、最後の下記のコードセルで指定する guidance scale の値を変え、出力画像を確認しました。(デバッグ実行で c を入力してエンターを押し、そのまま最後まで実行しました。)
device = "cuda"
prompt = """
A majestic kingfisher perched on a moss-covered tree branch above a flowing forest stream,
wings slightly open, droplets of water frozen in midair,
early morning mist, soft volumetric sunlight rays filtering through dense green leaves,
clear reflection in the water surface,
ultra detailed iridescent feathers with blue and orange tones,
macro photography, 200mm telephoto lens, shallow depth of field,
professional wildlife photography, high dynamic range.
"""
image = pipe(
prompt=prompt,
height=1024,
width=1024,
guidance_scale=4.0,
num_inference_steps=50,
generator=torch.Generator(device=device).manual_seed(0)
).images[0]
image.save("flux-klein.png")
guidance scale の値を変えたときに生成される画像がどのように変化するかを確認するため、下記のように少し長いプロンプトを使用しました。Chat GPT に長めの画像生成用のプロンプトの出力を依頼して用意したプロンプトになります。
最後の「マクロ撮影、200mmの望遠レンズ、浅い被写界深度。プロフェッショナルな野生動物写真、ハイダイナミックレンジ。」という点が自然かどうか気になりましたが、Google Colab で画像を生成した後で気付いたため、今回はこの設定の動作確認結果を載せることにします。
使用した英文のプロンプト:
A majestic kingfisher perched on a moss-covered tree branch above a flowing forest stream, wings slightly open, droplets of water frozen in midair, early morning mist, soft volumetric sunlight rays filtering through dense green leaves, clear reflection in the water surface, ultra detailed iridescent feathers with blue and orange tones, macro photography, 200mm telephoto lens, shallow depth of field, professional wildlife photography, high dynamic range.
上記のプロンプトの和訳:
苔むした木の枝にとまる、堂々としたカワセミ。 森の小川が流れるその上で、翼をわずかに広げ、 水滴が空中で静止したかのようにきらめいている。 早朝の霧が立ちこめ、柔らかで立体的な太陽光線が濃い緑の葉の間から差し込んでいる。 水面にはくっきりとした反射が映り、 青とオレンジの色調を帯びた虹色に輝く羽毛が、超高精細に描写されている。 マクロ撮影、200mmの望遠レンズ、浅い被写界深度。 プロフェッショナルな野生動物写真、ハイダイナミックレンジ。
左から、guidance scale が 1.0, 2.0, 3.0 の生成画像。1.0 はカワセミに見えません。2.0 は翼の広げ方が不自然です。3.0 は問題なさそうです。
左から、guidance scale が 4.0, 5.0, 6.0 の生成画像。4.0 は問題なさそうですが、翼をわずかにではなく大きく広げています。5.0 も問題なさそうですが、翼をわずかにではなく大きく広げています。水面にカワセミの姿の一部も映っています。6.0 は 5.0 とほぼ同じです。
左から、guidance scale が 7.0, 8.0, 10.0 の生成画像。7.0 は 5.0 とほぼ同じです。ただ、水面へのカワセミの映り方が不自然です。8.0 は 7.0 とほぼ同じです。10.0 も 7.0 とほぼ同じです。
左から、guidance scale が 20.0, 50.0, 100.0 の生成画像。20.0 は少し画像の質が悪くなったように感じられました。50.0 は画像の質が悪くなりました。100.0 は 50.0 よりもさらに画像の質が悪くなりました。











