見出し画像

Stable Diffusionを使ったイラスト作成の記録(17) ~ ネガティブプロンプトの重みづけ(続き) ~

前回の記事

シリーズ一覧

Layered Diffusion Pipelineを使うためのリンク集

ライブラリの大きなアップデート

前回の記事で行った分析を元に、ライブラリに大きなアップデートを行いました。その結果、APIの一部に変更が行われています。詳しくはコミット履歴を参照してください。

embeddingテンソルの平均値

前回は、プロンプトがテキストエンコーダーによってembeddingへと変換されると説明しましたが、このテンソル(ベクトルの集合のようなもの)の要素の平均値は、いくつくらいなのでしょうか?

Layered Diffusion Pipelineでテキストエンコーダーを直接呼び出してembeddingの性質を調べてみました。使ったのは次のスクリプトです。

# スクリプト(17-1)
def Tokenize(text):
  return pipe.text_model.decode_tokens(pipe.text_model.tokenize(text).input_ids)
def EncodeText(text):
  return pipe.text_model.EncodeText(text)[0][0]

def Run(text):
  print(f"Running for \"{text}\"")
  display(Tokenize(text))
  emb = EncodeText(text)
  display(emb)
  display(emb.float().mean(axis=[-1]))
  display(emb.float().mean(axis=[-2, -1]).item())

Run("")
Run("monochrome")
Run("1girl")
Run("blue sky and mountain")

このスクリプトは、トークナイザーの結果、embedding、embeddingのトークンごとの平均値、embedding全体の平均値をそれぞれ出力します。

まず、embedding全体の平均値は次のようになりました。

"" => -0.10926830768585205
"monochrome" => -0.10773292928934097
"1girl" => -0.1126093789935112
"blue sky and mountain" => -0.11065850406885147

embedding全体の平均値

これを見ると、embeddingの要素の平均値はおおよそ-0.11の周囲に分布していることが分かります。また各トークンごとに計算した平均値は次のようになりました。

"" =>
[-0.1050, -0.1090, -0.1085, -0.1084, -0.1083, -0.1082, -0.1082, -0.1081, -0.1081, -0.1081, -0.1081, -0.1082, -0.1082, -0.1082, -0.1082, -0.1082, -0.1082, -0.1083, -0.1083, -0.1083, -0.1084, -0.1084, -0.1084, -0.1084, -0.1085, -0.1085, -0.1086, -0.1087, -0.1087, -0.1088, -0.1089, -0.1090, -0.1091, -0.1091, -0.1092, -0.1092, -0.1093, -0.1094, -0.1094, -0.1095, -0.1096, -0.1096, -0.1097, -0.1097, -0.1097, -0.1098, -0.1099, -0.1099, -0.1099, -0.1100, -0.1101, -0.1101, -0.1101, -0.1101, -0.1101, -0.1101, -0.1101, -0.1101, -0.1102, -0.1102, -0.1102, -0.1102, -0.1101, -0.1102, -0.1102, -0.1103, -0.1103, -0.1102, -0.1103, -0.1104, -0.1104, -0.1104, -0.1104, -0.1104, -0.1103, -0.1105, -0.1104]
"monochrome" =>
[-0.1050, -0.1065, -0.1091, -0.1083, -0.1077, -0.1073, -0.1070, -0.1068, -0.1066, -0.1066, -0.1065, -0.1065, -0.1066, -0.1068, -0.1073, -0.1078, -0.1082, -0.1083, -0.1083, -0.1082, -0.1081, -0.1080, -0.1080, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1079, -0.1078, -0.1079, -0.1079, -0.1080, -0.1080, -0.1080, -0.1080, -0.1079, -0.1079, -0.1078, -0.1078, -0.1078, -0.1078, -0.1078, -0.1079, -0.1079, -0.1079, -0.1079, -0.1078, -0.1078, -0.1078, -0.1077, -0.1078, -0.1078, -0.1078, -0.1078, -0.1077, -0.1078, -0.1077, -0.1078, -0.1078, -0.1078, -0.1078, -0.1079, -0.1080, -0.1081, -0.1081, -0.1081, -0.1081, -0.1081, -0.1081, -0.1083]
"1girl" =>
[-0.1050, -0.1075, -0.1134, -0.1158, -0.1160, -0.1156, -0.1151, -0.1148, -0.1145, -0.1143, -0.1141, -0.1139, -0.1138, -0.1137, -0.1136, -0.1135, -0.1135, -0.1134, -0.1133, -0.1132, -0.1131, -0.1131, -0.1130, -0.1129, -0.1129, -0.1128, -0.1128, -0.1127, -0.1127, -0.1126, -0.1126, -0.1126, -0.1126, -0.1125, -0.1125, -0.1125, -0.1125, -0.1125, -0.1124, -0.1124, -0.1124, -0.1124, -0.1124, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1123, -0.1122, -0.1122, -0.1121, -0.1121, -0.1121, -0.1121, -0.1121, -0.1120, -0.1120, -0.1120, -0.1120, -0.1120, -0.1120, -0.1120, -0.1120, -0.1120, -0.1121, -0.1121, -0.1120, -0.1120, -0.1120, -0.1120, -0.1120]
"blue sky and mountain" =>
[-0.1050, -0.1077, -0.1099, -0.1152, -0.1068, -0.1119, -0.1115, -0.1113, -0.1111, -0.1110, -0.1108, -0.1106, -0.1105, -0.1103, -0.1102, -0.1101, -0.1101, -0.1105, -0.1109, -0.1114, -0.1115, -0.1115, -0.1114, -0.1114, -0.1113, -0.1112, -0.1112, -0.1111, -0.1111, -0.1111, -0.1110, -0.1111, -0.1111, -0.1110, -0.1110, -0.1109, -0.1110, -0.1109, -0.1109, -0.1109, -0.1109, -0.1109, -0.1109, -0.1108, -0.1108, -0.1107, -0.1107, -0.1106, -0.1106, -0.1107, -0.1107, -0.1107, -0.1107, -0.1106, -0.1106, -0.1105, -0.1105, -0.1105, -0.1104, -0.1105, -0.1105, -0.1104, -0.1104, -0.1104, -0.1104, -0.1104, -0.1104, -0.1104, -0.1104, -0.1105, -0.1106, -0.1106, -0.1106, -0.1106, -0.1106, -0.1105, -0.1105]

embeddingのトークンごとの平均値

各トークンごとに見ても平均値はそれほど大きな変化はなく、おおよそ-0.11の周辺に分布しているようです。

全ての要素を-0.11として画像生成してみる

今回行ったLayered Diffusion Pipelineの変更で、全ての要素が一定値であるembeddingを作るConstEncodingというエンコーディングをサポートしました。プロンプトに文字列の代わりにConstEncoding(-0.11)を与えることができます。

これを用いて、次のようなスクリプトで画像生成してみました。

# スクリプト(17-2)
image = pipe(
    num_steps=30,
    size=image_size,
    rand_seed=rand_seed,
    initialize=Randomly(strength=using(0.99999, until=1.0)),
    iterate=Layer(
        prompt="1girl",
        negative_prompt=ConstEncoding(-0.11),
    ),
)
ネガティブプロンプト: ConstEncoding(-0.11)

以下の実験では、この画像を基準にパラメータを変更した画像を比較していきたいと思います。また、この要素が一定のプロンプトを、単位プロンプトと呼ぶことにします。

(注:初稿では、単位プロンプトのことをヌルプロンプトと記述しましたが、他の名称との整合性から単位プロンプトという呼称に変更しました。)

ネガティブプロンプトの文字列を変更して比較

ネガティブプロンプトに単位プロンプトを与える場合と、空文字列の場合、"monochrome"を与えた場合の3種類を比較してみます。さらに、embeddingの各要素に-1を掛けてベクトルを反転したものも並べて比較してみました。

     ← 通常 | 反転(-1を乗算) →

生成された画像には差があるものの、縦の列で比較すると、線の太さや描写の細かさなどで共通点を見ることができます。

embeddingの強さをやや弱め、embeddingの各要素を0.6倍したものを使った比較画像も作成しました。

← 通常( 0.6を乗算) | 反転(-0.6を乗算) →

興味深いことに、各要素を0.6倍した画像の方が画像が安定してネガティブプロンプトの特徴がよく表れたものとなっているように見えます。

例えば、"monochrome"を与えた画像(中段)では、左(通常)ではよりカラフルな生成画像となり、右(反転)では色合いが抑えられ、モノクロにより近い生成画像となっています。

空文字列を与えた画像(上段)の場合、左(通常)では装飾的な要素が増えているのに対し、右(反転)では装飾的な要素のほとんどが消えています。これも空文字列の意味を反映したものではないかと思われます。

embeddingに掛けるスケールを変化させて比較

上述で見たように、ネガティブプロンプトをそのまま使うより、0.6倍する時の方が画像が整ってネガティブプロンプトの効果が強く出ました。この効果をさらに分析するため、embeddingに掛けるスケールを変化させて比較してみることにします。

スクリプト

# スクリプト(17-3)
negative_prompt = ""  # ネガティブプロンプト ["", "monochrome", NullPrompt]
scale = 1.0  # ネガティブプロンプトの掛けるスケール [0.2 ~ 2.0]

image = pipe(
    num_steps=30,
    size=image_size,
    rand_seed=rand_seed,
    initialize=Randomly(strength=using(0.99999, until=1.0)),
    iterate=Layer(
        prompt="1girl",
        negative_prompt=(negative_prompt, ScaledEncoding(scale)),
    ),
)

スクリプトは17-2とほぼ同じですが、ネガティブプロンプトが異なります。ScaledEncodingという新機能を使って、embeddingの要素にスケールを掛け算して値を変化させています。

ネガティブプロンプトに与える文字列の種類は前節と同じです。スケールは0.2から2.0まで0.2刻みで変化させました。スケールが1.0(変化なし)の画像は赤枠で囲っています。

生成画像

embeddingのスケールを0.2から2.0まで変化させる

最右列(単位プロンプト)の画像は、上から下まで単調な変化をしていますが、最左列(空文字列)と中列("monochrome")は赤枠の画像(スケール=1.0)を境に変化の様子が変わっています。

特に、赤枠のスケールが1.0のところとその下の1.2のところの画像は、画像の乱れが大きくなっています。

負のスケールを変化させてみる

上ではスケールを正の範囲で変化させましたが、負の範囲で変化させるとどうなるでしょうか。

embeddingのスケールを-0.2から-2.0まで変化させる

こちらの場合では、正のスケールの時とは違い、一貫して単調な変化が見られます。-1.0(赤枠)の付近で変化の様子が変わることもなく、画像が特別に乱れることもありません。

スケール0.0付近での変化

スケールの正と負の切り替わり付近での変化はどのようになっているでしょうか?

embeddingのスケールを0.6から-0.6まで変化させる

最右列(単位プロンプト)は0.0近辺でも変化の単調性が継続していますが、最左列(空文字列)と中列("monochrome")は赤枠の画像(スケール=0.0)の前後で大きな変化が観察されます。

まとめ

単位プロンプト

embeddingテンソルの要素の平均値を計算し、全ての要素の値が平均値(-0.11)であるembeddingを単位プロンプトとして定義しました。

画質の変化はスケールの変化に単調ではない

単位プロンプトに対しては画質の変化はスケールの変化に単調ですが、文字列で与えられたプロンプトに対しての画質の変化はスケールの変化に単調ではありません。

極値点は0.0と、1.0~1.2の付近にあります。また、-1.0は極値点ではありません。

画質のバランスのよいスケール

文字列でプロンプトが与えられた時に、乱れが少なく、プロンプトの影響が大きく見られるバランスのよい画像が生成される傾向にあるスケールを求めることができる可能性があります。

今回の実験から、-0.2, 0.2, 0.6, 1.6の4点の周辺がその候補となりえるように思われます。以下に、その4点の画像を抜き出して表示します。

スケールが -0.2, 0.2, 0.6, 1.6 の画像

なお、0.6と1.6はほぼ逆数の関係にある(1÷1.6 = 0.625)ことは特記してもよい事項であるように思います。

スケール1.0付近で画像が乱れる理由の考察

スケールが1.0付近で画像が乱れることは、興味深い現象です。

現時点での仮説は、スケール1.0のネガティブプロンプトは、プロンプトとのembeddingの類似性が大きくなるため、プロンプトのよい特徴の多くも一緒に消してしまうのではないかということです。

CFGを効率よく機能させるには、プロンプトから作られる画像とネガティブプロンプトから作られる画像の差分が大きいほどよいのではないかと推測しています。

つまり、スケールが1.0から離れると、ネガティブプロンプトのembeddingとプロンプトのembeddingの差分が大きくなり、CFGの実行が効率よくなるのではないかということです。下に、その考え方を簡単に図示します。

差分ベクトルの長さ

この記事が気に入ったらサポートをしてみませんか?