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) |
| ComfyUI | v0.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の併用 |
| VAE | Qwen-Image VAE |
| 当てるLoRA | 4人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 v1 | Anima-3.8B preview 0.1 |
|---|---|---|
| DiTブロック数 | 40 | 52(2.9Bの40を凍結し、挿入12ブロックだけ学習) |
| テキストエンコーダ | Qwen3-0.6B | Qwen3-0.6B(そのまま)+Qwen3.5-4B(新設のadapter経由) |
| 学習した部分 | 挿入12層 | 挿入12ブロック+クロスアダプタ44.06M |
| 追加学習データ | 170万枚 | booruデータセット(出現率5%超のタグ全部)、キャプションは自然文25%・タグ25%・混合50% |
| 学習環境 | 8×RTX 5080 | RTX 4090 1枚で40時間(256pxで10時間、512pxで30時間)、step 3220 |
| 配布 | 単体safetensors | DiT 7.50GB+アダプタ88MB+Qwen3.5-4B 4.78GB の3ファイル。DiTとアダプタはセットでないと動かない |
| ComfyUI | v0.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〜2 | 0〜2 | 21〜23 | 28〜30 |
| 3〜5 | 4〜6 | 24〜26 | 32〜34 |
| 6〜8 | 8〜10 | 27〜29 | 36〜38 |
| 9〜11 | 12〜14 | 30〜32 | 40〜42 |
| 12〜14 | 16〜18 | 33〜35 | 44〜46 |
| 15〜17 | 20〜22 | 36〜39 | 48〜51 |
| 18〜20 | 24〜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_proj | Xavier一様乱数 |
| 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 | サンプラー |
|---|---|---|---|
| Base | Anima-Base v1.0 | Qwen3-0.6B(従来) | euler / sgm_uniform / CFG 4 / 28step(2.9B記事と同じ) |
| 2.9B | Anima-2.9B preview v1 | 同上 | 同上 |
| 3.8B native | Anima-3.8B DiT | Unified Promptノード、strength 0(=従来と同一のconditioning) | res_multistep / beta / CFG 8 / 40step(モデルカード推奨) |
| 3.8B expanded | Anima-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条件で出した。




4条件とも破綻はない。3.8Bの2枚は金の刺繍が濃く出たが、こっちはCFGもサンプラーも違うので、モデルの差かどうかは分からない。
追加学習が512pxまでという話から警戒していた1MPでのアーティファクトは、この立ち絵では出てこなかった。
ノードとMPS動作の確認で最初に出した1枚(狐娘が柵を飛び越える指示、20ステップ)も、ちゃんと飛び越えポーズと柵が出ていた。
従来のAnimaで効かなかった構図指定は通るのか
画像生成サーバーの構図プリセット10種を、同じ内容の指示で4条件に投げた(seed 42固定)。
プリセットの英文は、効かせるために念押しを重ねた長文になっている。たとえば俯瞰は「The image must unmistakably read as an overhead view」のような文まで入れてある。サムネイルは、前に試したときにAnima-Baseで構図指定して出した文と画像の組で保存している。ただ出ないものは出ないという感じだったので、サムネイルが無い項目は、その文で出るかどうかも不明のまま残っていた。
判定は大枠で、指示した画角・切り取りになっているかだけ。
| プリセット | Base | 2.9B | 3.8B native | 3.8B expanded |
|---|---|---|---|---|
| 俯瞰(from above) | 取り違え | 取り違え | 惜しい | 惜しい |
| あおり(from below) | 通る | 通る | 通る | 通る |
| 真後ろ(from behind) | 通る | 失敗 | 失敗 | 通る |
| 真横(from side) | 通る | 破綻 | 惜しい | 失敗 |
| ダッチアングル | 弱い | 弱い | 弱い | 弱い |
| 引き(wide shot) | 通る | 通る | 惜しい | 惜しい |
| 余白構図(negative space) | 惜しい | 通る | 通る | 惜しい |
| カウボーイショット | 失敗 | 失敗 | 惜しい | 惜しい |
| 顔をフレーム外 | 破綻 | 惜しい | 惜しい | 失敗 |
| 下半身のみ | 破綻 | 失敗 | 失敗 | 失敗 |
俯瞰




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




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




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




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




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




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




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




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




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




腰から下だけという指定はどの条件も守れなかった。expandedは頭は切れているが表情は見える、太ももまでのほぼカウボーイショット。Baseは人体が破綻している。
通った指定と通らなかった指定
10種を通しで比べて、expandedではっきり通るようになったのは真後ろくらい。カウボーイショットも、頭頂部が切れた以外は指定の範囲だった。
逆に、顔をフレーム外・下半身のみのような体の一部をフレームの外に出す指定では、expandedが一番、顔までフレームに入れてしまった。余白構図も、左1/3の指定が左半分止まりだった。
学習データが512px以下のbooru画像で、キャラが全身で写る絵が大半だったとすれば、体をフレームで切る指定ほど通らないのは学習分布そのままだと思う。
複数人での複雑な構図
4人バンドの記事で出せなかった役割指定を、LoRAなしの汎用キャラでやり直した。
左からベース、中央がボーカル兼白ギター、右が赤ギター、後列にドラム。3シード回した。画像はseed 42のもの。




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



これは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でかなを軽い指定(トリガー+立ち姿だけ、服指定なし)で出した。




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



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×1216 | 241秒 |
| 2.9B(28step) | 832×1216 | 343秒 |
| 3.8B(40step、native/expanded) | 832×1216 | 637秒 |
| 3.8B(40step) | 1344×768 | 650秒 |
| 3.8B + Turbo LoRA付け替え(8step) | 832×1216 | 78秒 |
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。