技術約14分で読めます

Qwen3.5-4Bが追加されたAnima-3.8Bで、効かなかった構図指定は通るのか

いけさん目次

層拡張で40層になったAnima-2.9Bに既存のキャラLoRAはそのまま乗るのかの続き。今度は Anima-3.8B が出た。
2.9Bをさらに層拡張して52ブロックにしたもので、それだけなら「もう一段でかくした」で終わりなんだけど、今度はテキストエンコーダ側にQwen3.5-4Bが追加されている。
Animaの4人LoRAで4人バンドを描けるか確かめた記事で、エンコーダをQwen3-4Bに差し替えようとして形状エラーで止まり、「画像生成の損失でLLMAdapterごと学習し直すしかない」と書いて諦めたやつ。それをコミュニティがやった。

狙いもモデルカードにはっきり書いてあって、プロンプト追従、複数キャラの割り当て、キャラ同士のやり取り、空間指示。
Animaは構図の指定が効きにくい。細かく書いても大雑把に書いても指示どおりにならないのは4人の役割指定をまとめた記事でもバンド記事でもやったとおりで、うちの画像生成サーバーの構図プリセットには、何度回しても駄目でサムネイルを作れなかった項目が残っている。
絵が綺麗になったかより、そこが通るようになったのかを確かめてみた。ついでに、どうやって追加したのかをコードと重みから調べた。

検証環境

項目内容
実行機M1 Max 64GB MacBook Pro(macOS 26.5)
ComfyUIv0.33.3(port 8189の別インスタンス)+カスタムノード comfyui-anima-3-8B
比較ベースAnima-Base v1.0、Anima-2.9B preview v1
対象モデルAnima-3.8B preview 0.1(DiT 7.50GB)+progressive cross adapter(88MB)+Qwen3.5-4Bエンコーダ(4.78GB)
テキストエンコーダQwen3-0.6B(従来のまま)とQwen3.5-4Bの併用
VAEQwen-Image VAE
当てるLoRA4人LoRA、ペアLoRA、Turbo LoRA(いずれもAnima-Base学習、ブロック番号を52層へ付け替え)

実験の流れ

graph TD
    A[コードと重みから<br/>Qwen3.5-4Bの追加方法を確認] --> B[テンソル比較<br/>元の40ブロックは2.9Bと同一か]
    B --> C[素の出力を4条件で比較<br/>Base / 2.9B / 3.8B native / expanded]
    C --> D[構図プリセット10種]
    C --> E[複数人での<br/>複雑な構図]
    C --> F[キャラLoRAを<br/>52ブロックへ付け替え]
    D --> G[生成時間とメモリを記録]
    E --> G
    F --> G

追加のやり方は、モデルカードとカスタムノードのコード、アダプタとDiTの重みを突き合わせて確かめた。Codexの読み取り専用実行も使った。
構図プリセットの中身は俯瞰、あおり、真後ろ、真横、引き、余白構図などの10種。生成の比較はどれも同じ内容の指示を4条件に投げて、バンドだけ複数seedで回した。

Anima-3.8Bの中身

モデルカードとアダプタのsafetensorsメタデータに書いてある分。

項目Anima-2.9B preview v1Anima-3.8B preview 0.1
DiTブロック数4052(2.9Bの40を凍結し、挿入12ブロックだけ学習)
テキストエンコーダQwen3-0.6BQwen3-0.6B(そのまま)+Qwen3.5-4B(新設のadapter経由)
学習した部分挿入12層挿入12ブロック+クロスアダプタ44.06M
追加学習データ170万枚booruデータセット(出現率5%超のタグ全部)、キャプションは自然文25%・タグ25%・混合50%
学習環境8×RTX 5080RTX 4090 1枚で40時間(256pxで10時間、512pxで30時間)、step 3220
配布単体safetensorsDiT 7.50GB+アダプタ88MB+Qwen3.5-4B 4.78GB の3ファイル。DiTとアダプタはセットでないと動かない
ComfyUIv0.33.1以降でそのままカスタムノード必須(52ブロック検出のパッチと専用ノード2つ)

推奨設定は832×1216(1MP)、CFG 7〜8、28〜50ステップ、res_multistep + beta。
3.8Bの追加学習分(挿入12ブロックとアダプタ)は512pxまでなので、1MPではアーティファクトが出る場合があり、モデルカードは2パス目(highres)を従来の0.6B conditioningで回すよう勧めている。
プロンプトはタグ列のあとに Description: で区切って自然文を書く書式で学習されている。

元の40ブロックは触っていないのか

アダプタのメタデータに挿入位置がそのまま書いてある。trainable_dit_blocks = [3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47]
4ブロックごとに1個ずつ12個挟み、末尾の48〜51は元のまま。2.9Bのときの insertion_positions と同じ4層周期。
元の番号から新しい番号への対応はこんな感じ。

2.9Bの番号3.8Bの番号2.9Bの番号3.8Bの番号
0〜20〜221〜2328〜30
3〜54〜624〜2632〜34
6〜88〜1027〜2936〜38
9〜1112〜1430〜3240〜42
12〜1416〜1833〜3544〜46
15〜1720〜2236〜3948〜51
18〜2024〜26

この対応で2.9Bと3.8Bの重みをテンソル比較したら、元の40ブロックは40個全部が完全一致だった。DiT内蔵のLLMAdapter(従来の0.6B用接続部)と最終層・埋め込みも一致。
つまり3.8BのDiTは「2.9B+学習済みの挿入12ブロック」で、2.9B部分には1ビットも触っていない。
挿入ブロックの初期化はメタデータに interleaved_deep_copy_zero_residual_outputs、由来は Anima-2.9B / LLaMA-Pro block expansion とあり、隣のコピー+出力ゼロの2.9Bと同じやり方。ただこっちは学習済みなので、今は隣とは別物。

Qwen3.5-4Bはどうやって追加されているのか

バンド記事のときは「0.6Bの1024次元前提のLLMAdapterに2560次元をどう入れるか」で詰まった。3.8Bは入れ替えずに、横から追加している。

Animaの従来経路は、Qwen3-0.6Bの隠れ状態(1024次元)を、6ブロックのLLMAdapterがT5トークン列に合わせて変換してDiTに渡している。
3.8Bはこの6ブロックを凍結したまま、各ブロックの直後に新しいcross-attentionを1個ずつ、計6個挟み込んでいる。
このcross-attentionは、アダプタ内部の系列をクエリに、Qwen3.5-4Bの中間層の隠れ状態(2560次元)をキーとバリューにして、結果を残差として足す。
2560→1024の変換はそのk_proj・v_proj(1024×2560)がやる。線形ブリッジで0.6Bの表現へ変換するわけじゃなくて、画像生成の損失で役に立つ分だけ拾い方を学習させる形。

初期化もLLaMA Proのやり方そのもので、

新設パラメータ初期化
q_proj、q_norm、k_norm、query側のnorm凍結した隣のnativeブロックからコピー
k_proj、v_projXavier一様乱数
o_proj全要素ゼロ

o_projがゼロなので、学習前は残差に何も足さず、従来のconditioningと完全一致する。DiTの挿入ブロックも層拡張の記事と同じ恒等初期化。
つまりこのモデルは、DiTの層拡張とアダプタへのcross-attention挿入の両方を、同じ「コピーして挟んで出力ゼロ」で組んでいる。

Qwen3.5-4B側は32層のハイブリッド構成(linear attentionのGatedDeltaNetが24層、full attentionが8層)で、特徴はfull attention側の層7、15、23、31の4つから取り出す。
各アダプタブロックがこの4層を学習可能な係数(softmax)で混ぜて使う。
生成時のパラメータ adapter_strength は従来conditioningとの線形補間で、0で完全に従来どおり、1が学習時の強さ。

カスタムノードのQwen3.5実装は、GatedDeltaNetのdelta ruleをCUDAカーネルなしのPyTorchループで書いてあり、Apple SiliconのMPSでもそのまま動く。この点はCodexにコード全体を読ませた確認でも同じ結論だった。

アダプタの学習の跡

アダプタファイル(49テンソル、44.06M)の中身も覗いた。o_projは6ブロックとも全要素が非ゼロで、ゼロ初期化から学習で更新されている。一方、k_proj・v_projのノルムはXavier初期値の期待値とほぼ同じで、大きくは変わっていない。4層を混ぜる係数 layer_mix_logits に至っては6ブロック全部が初期値のまま同一(softmax後 0.14 / 0.20 / 0.28 / 0.39 で深い層ほど重い)で、学習で1ステップも変わった形跡がない。source側のRMSNorm重みも全要素ちょうど1.000で初期値のままだった。

メタデータに fp32_master_training = 0 とあり、bf16のまま学習している。bf16は1.0付近の刻みが0.0078なので、それより小さい更新は丸められて消える。混合係数とRMSNormが初期値のままなのは、これで説明がつきそう。
学習率もDiT側1e-5でステップ3220と控えめなので、44Mのアダプタで実質変わったのは、o_projと(コピー初期化された)q_proj周辺くらいだと思う。
それにしてもすごい学習のさせ方で、効果があるのかはあんまり分からないが。

生成の設定

条件モデルconditioningサンプラー
BaseAnima-Base v1.0Qwen3-0.6B(従来)euler / sgm_uniform / CFG 4 / 28step(2.9B記事と同じ)
2.9BAnima-2.9B preview v1同上同上
3.8B nativeAnima-3.8B DiTUnified Promptノード、strength 0(=従来と同一のconditioning)res_multistep / beta / CFG 8 / 40step(モデルカード推奨)
3.8B expandedAnima-3.8B DiT同ノード、strength 1(Qwen3.5-4B経路あり)同上

3.8Bの2条件は同じDiT・同じサンプラー設定で、違いはQwen3.5-4Bの経路だけ。
3.8Bのプロンプトはモデルカードの書式に合わせてタグ列+ Description: +自然文にし、Base・2.9Bには同じ内容をタグ列+文で渡す。ネガティブはモデルカードの推奨どおり3.8B側もexpandedで通した。

素の出力

構図の話に入る前の、絵としての基準点として、2.9B記事と同じローブ娘のプロンプト(seed 42)を4条件で出した。

Base
白いローブの少女の全身立ち絵(Base、seed42)
2.9B
白いローブの少女の全身立ち絵(2.9B、seed42)
3.8B native
白いローブの少女の全身立ち絵(3.8B native、seed42)
3.8B expanded
白いローブの少女の全身立ち絵(3.8B expanded、seed42)

4条件とも破綻はない。3.8Bの2枚は金の刺繍が濃く出たが、こっちはCFGもサンプラーも違うので、モデルの差かどうかは分からない。
追加学習が512pxまでという話から警戒していた1MPでのアーティファクトは、この立ち絵では出てこなかった。
ノードとMPS動作の確認で最初に出した1枚(狐娘が柵を飛び越える指示、20ステップ)も、ちゃんと飛び越えポーズと柵が出ていた。

柵を飛び越える狐娘(3.8B expanded、動作確認の1枚目)

従来のAnimaで効かなかった構図指定は通るのか

画像生成サーバーの構図プリセット10種を、同じ内容の指示で4条件に投げた(seed 42固定)。
プリセットの英文は、効かせるために念押しを重ねた長文になっている。たとえば俯瞰は「The image must unmistakably read as an overhead view」のような文まで入れてある。サムネイルは、前に試したときにAnima-Baseで構図指定して出した文と画像の組で保存している。ただ出ないものは出ないという感じだったので、サムネイルが無い項目は、その文で出るかどうかも不明のまま残っていた。
判定は大枠で、指示した画角・切り取りになっているかだけ。

プリセットBase2.9B3.8B native3.8B expanded
俯瞰(from above)取り違え取り違え惜しい惜しい
あおり(from below)通る通る通る通る
真後ろ(from behind)通る失敗失敗通る
真横(from side)通る破綻惜しい失敗
ダッチアングル弱い弱い弱い弱い
引き(wide shot)通る通る惜しい惜しい
余白構図(negative space)惜しい通る通る惜しい
カウボーイショット失敗失敗惜しい惜しい
顔をフレーム外破綻惜しい惜しい失敗
下半身のみ破綻失敗失敗失敗

俯瞰

Base
俯瞰指定(Base、seed42)
2.9B
俯瞰指定(2.9B、seed42)
3.8B native
俯瞰指定(3.8B native、seed42)
3.8B expanded
俯瞰指定(3.8B expanded、seed42)

Baseと2.9Bは「前屈して自分の脚の間から顔を覗かせる」取り違えで、俯瞰ですらない。
3.8Bは2枚とも「仰向けに寝た子を真上から、顔はカメラへ」で、上からのカメラと視線の指示どおりになっていた。立ち姿を上から、にはならなかったので惜しい止まり。

あおり

Base
あおり指定(Base、seed42)
2.9B
あおり指定(2.9B、seed42)
3.8B native
あおり指定(3.8B native、seed42)
3.8B expanded
あおり指定(3.8B expanded、seed42)

これは元から通るプリセット。4条件とも下からの見上げで、3.8Bの2枚は靴が大きく写る遠近の誇張が一段強い。

真後ろ

Base
真後ろ指定(Base、seed42)
2.9B
真後ろ指定(2.9B、seed42)
3.8B native
真後ろ指定(3.8B native、seed42)
3.8B expanded
真後ろ指定(3.8B expanded、seed42)

Baseは通ったのに、2.9Bと3.8B nativeは正面を向いた。
同じDiT・同じ設定でQwen3.5の経路だけ有効にしたexpandedは真後ろを向いた。「顔を見せない」の指示が、0.6Bの経路では反映されなくて、Qwen3.5の経路では通った。

真横

Base
真横指定(Base、seed42)
2.9B
真横指定(2.9B、seed42)
3.8B native
真横指定(3.8B native、seed42)
3.8B expanded
真横指定(3.8B expanded、seed42)

Baseは指示どおりの直立プロフィールだが、プロンプト中の「camera」を実物のカメラとして画面に描いてしまった。2.9Bも向きは真横なものの、等身が小さい。
3.8B nativeは3/4視点で止まり、expandedは振り向いてこちらに目線をくれた。「目線を寄越すな」まで書いてあるのに逆のことをした。

ダッチアングル

Base
ダッチアングル指定(Base、seed42)
2.9B
ダッチアングル指定(2.9B、seed42)
3.8B native
ダッチアングル指定(3.8B native、seed42)
3.8B expanded
ダッチアングル指定(3.8B expanded、seed42)

4条件とも街角のカウボーイショットになり、傾きはどれも弱い。差がつかなかった。

引き

Base
引きの遠景指定(Base、seed42)
2.9B
引きの遠景指定(2.9B、seed42)
3.8B native
引きの遠景指定(3.8B native、seed42)
3.8B expanded
引きの遠景指定(3.8B expanded、seed42)

Baseは背景が怪しいが、遠くの入口に小さく立っているのは分かる。2.9Bも奥の入口に立っている。3.8B nativeはでかいドアと小さいキャラで若干あおり気味になり、expandedは入口なのかどうかもよく分からない。

余白構図

Base
余白構図指定(Base、seed42)
2.9B
余白構図指定(2.9B、seed42)
3.8B native
余白構図指定(3.8B native、seed42)
3.8B expanded
余白構図指定(3.8B expanded、seed42)

左1/3に人物、右半分は空けろという指定。Baseは単純にキャラが1/3に見切れている。2.9Bと3.8B nativeは構図のコンセプトが同じで、キャラのポーズが若干違うだけ。expandedはキャラが左半分に立っていて、1/3にはなっていない。

カウボーイショット

Base
カウボーイショット指定(Base、seed42)
2.9B
カウボーイショット指定(2.9B、seed42)
3.8B native
カウボーイショット指定(3.8B native、seed42)
3.8B expanded
カウボーイショット指定(3.8B expanded、seed42)

頭頂から腿の半ばまでという指定。Baseと2.9Bは腹・腰・太ももしか写っていない。3.8B nativeは顔が見切れ、expandedは頭頂部が切れた。

顔をフレーム外

Base
顔をフレーム外に出す指定(Base、seed42)
2.9B
顔をフレーム外に出す指定(2.9B、seed42)
3.8B native
顔をフレーム外に出す指定(3.8B native、seed42)
3.8B expanded
顔をフレーム外に出す指定(3.8B expanded、seed42)

今度は逆に頭をフレームに入れるなという指定で、Baseは人体として破綻していて、写っているのは腹とスカートと足の一部くらい。2.9Bは目より上が、3.8B nativeは鼻より上が切れていて、expandedは目も鼻も軽く見えている。

下半身のみ

Base
下半身のみの指定(Base、seed42)
2.9B
下半身のみの指定(2.9B、seed42)
3.8B native
下半身のみの指定(3.8B native、seed42)
3.8B expanded
下半身のみの指定(3.8B expanded、seed42)

腰から下だけという指定はどの条件も守れなかった。expandedは頭は切れているが表情は見える、太ももまでのほぼカウボーイショット。Baseは人体が破綻している。

通った指定と通らなかった指定

10種を通しで比べて、expandedではっきり通るようになったのは真後ろくらい。カウボーイショットも、頭頂部が切れた以外は指定の範囲だった。
逆に、顔をフレーム外・下半身のみのような体の一部をフレームの外に出す指定では、expandedが一番、顔までフレームに入れてしまった。余白構図も、左1/3の指定が左半分止まりだった。
学習データが512px以下のbooru画像で、キャラが全身で写る絵が大半だったとすれば、体をフレームで切る指定ほど通らないのは学習分布そのままだと思う。

複数人での複雑な構図

4人バンドの記事で出せなかった役割指定を、LoRAなしの汎用キャラでやり直した。
左からベース、中央がボーカル兼白ギター、右が赤ギター、後列にドラム。3シード回した。画像はseed 42のもの。

Base
4人バンドの役割指定(Base、seed42)
2.9B
4人バンドの役割指定(2.9B、seed42)
3.8B native
4人バンドの役割指定(3.8B native、seed42)
3.8B expanded
4人バンドの役割指定(3.8B expanded、seed42)

Baseと2.9Bは3シードとも前列に4人並ぶだけで、ドラムキットが画面端に写ってもドラマーはいなかった。バンド記事で14Bに作文させたときも、後列のドラマーは出なかった。
3.8B expandedは3シード全部で、後列のドラムキットに黒髪の小さい子が座った。指定した構図に近くなったが、3シードとも前列に4人いて、ドラマーを足すと5人になった。
左のベースと中央のマイクは大体出ているが、右の赤ギターの子は、髪の色と髪型がシードごとに違う。
バンドの場面自体は出るのに、人数と一人ひとりの見た目は指定どおりにならない。構図プリセットのときと同じ傾向だった。

花の受け渡し(左の子が右手で差し出し、右の子が左手で受け取る)はこんな感じ。

2.9B
2人の花の受け渡し指定(2.9B、seed42)
3.8B native
2人の花の受け渡し指定(3.8B native、seed42)
3.8B expanded
2人の花の受け渡し指定(3.8B expanded、seed42)

これは3条件とも花畑で2人が花を受け渡す場面になって、左右の配置も守った。
ただ「どちらが差し出しているか」は3シードの中で入れ替わり、expandedだから固定できるわけでもなかった。2人までのやり取りは従来でも描けていて、差が出るのは4人+後列のような複雑な指定からだと思う。

ブロック番号を付け替えたキャラLoRAは乗るのか

Anima-Baseで焼いた手持ちLoRAを、2.9B記事で作った40ブロック用の付け替えに加えて、挿入位置 [3, 7, …, 47] の対応でもう一段52ブロックへ付け替えた(Base→2.9B→3.8Bの二段)。
DiTの40ブロックが2.9Bと完全一致なのは確認済みなので、対応表が合っていれば理屈の上では2.9Bと同じように乗るはず。

まず4人LoRAでかなを軽い指定(トリガー+立ち姿だけ、服指定なし)で出した。

2.9B + 付け替え40
かなの軽い指定(2.9B + 付け替え40)
3.8B + 40のまま
かなの軽い指定(3.8B + 40のまま)
3.8B + 付け替え52(native)
かなの軽い指定(3.8B + 付け替え52(native))
3.8B + 付け替え52(expanded)
かなの軽い指定(3.8B + 付け替え52(expanded))

40番のまま3.8Bに当てたら、3シードとも黒髪の別人が出た。2.9Bのときと同じことがまた起きた。
52へ付け替えるとnativeでは3シードとも本人が出た。サイドポニー、あほ毛、茶目、いつもの制服まで出た。
expandedでは、顔と髪は3シードとも本人のまま、服だけが3シードとも黒いボディスーツに化けた。服を指定していないプロンプトだと、LoRAが焼き込んだ制服のままにはならないらしい。この黒スーツがQwen3.5経路のせいなのか、追加学習データのせいなのかは分からない。

服まで全指定する4人立ちも出した。

2.9B + 付け替え40
4人立ちの全指定(2.9B + 付け替え40)
3.8B + 付け替え52(native)
4人立ちの全指定(3.8B + 付け替え52(native))
3.8B + 付け替え52(expanded)
4人立ちの全指定(3.8B + 付け替え52(expanded))

3条件とも、3シード全部でくらら・けい・かな・こはるの並び順と顔立ちは出た。ただし3.8Bはnative・expandedともどのシードも頭の上が切れていて、全身が入りきっていない。制服を指定してあるので、expandedでもボディスーツにはならない。
けい・かなのペアLoRA(付け替え52)も、native・expandedとも2人が混ざらず本人で出た。
Turbo LoRAの付け替え52も8ステップで破綻なく出て、生成は78秒まで短くなった。

本人が出るかどうかだけなら、LoRAの互換性は2.9Bのときと同じ理屈で、番号を付け替えたら本人、付け替えなかったら別人だった。
expandedを使うときだけ、プロンプトで指定していない要素(今回は服)が勝手に置き換わった。服まで指定した4人立ちでは起きていないので、キャラLoRAと併用するなら指定は全部書くことになりそうだ。
頭が切れるのも服が変わるのも、ここまでのでかい方の挙動だけ見ると、指定していないときに余計な処理が入って、デフォルトがAnimaやLoRAとは違うところで挿入されている感じがする。ただ、それが追加学習のせいなのかQwenの差なのかは、やっぱり分からない。

生成時間とメモリ

条件解像度 / ステップ1枚の中央値
Base(28step)832×1216241秒
2.9B(28step)832×1216343秒
3.8B(40step、native/expanded)832×1216637秒
3.8B(40step)1344×768650秒
3.8B + Turbo LoRA付け替え(8step)832×121678秒

3.8Bの40ステップは2.9Bの28ステップの約1.9倍。ステップ数の差を除くと、52ブロック化とCFG 8で1ステップあたり1.3倍ほど重い。
Qwen3.5-4Bのエンコードは、GatedDeltaNetがPythonループのfp32実装なのに1プロンプト20〜30秒で、生成全体からすれば誤差の範囲。expandedとnativeの差は1枚あたり20秒前後だった。
メモリはDiT 7.9GBと両エンコーダを載せて、64GBの中で問題は出なかった。93枚を通してエラー0。

参考リンク