Yyatmita

はじめての LoRA 学習 — Anima で漫画キャラを焼く

Cosmos-Predict2 ベースの新しい画像モデル Anima では、SDXL/FLUX 用 IP-Adapter 資産がそのまま使えない。キャラ identity 一貫性を取る現実的な道として LoRA 学習を選んだ。マンガ既存資産から 76 枚を自動抽出 → diffusion-pipe で 3 時間 / rank 32 学習 → ノリとクロコ 2 体のキャラ LoRA を作って検証するまでの全工程。コンテナの nvcc 不在、/dev/shm 64MB、composition タグの罠など、初回で踏む落とし穴を全部まとめる。

AIマンガ制作ワークフロー#ai-manga#lora#diffusion-pipe#comfyui#anima#cosmos-predict2
← 前の記事: 【第30回】Grok Imagine API を漫画コマ生成に使ってみた——OpenAI 互換のフリして全然違う、料金・3枚制限・棲み分け

「キャラの全身を i2i で生成すると、表情だけ差分された別人が出てくる」── マンガ制作で AI 画像を使うと一度はぶつかる壁。本稿は最近出た Anima(Cosmos-Predict2 ベース、NVIDIA 系統 + Qwen3 0.6B + Qwen Image VAE) を題材に、LoRA 学習でキャラ identity を焼き込むまでの全工程をまとめる。manginus に既に溜まっている 1326 枚のキャラ panel を自動抽出してデータセット化し、diffusion-pipe で 76 枚 × 5 epoch / 3 時間学習し、ノリとクロコ 2 体の LoRA を作って検証するところまで。Cosmos 系統には IP-Adapter が無いので、これが現時点での唯一の現実解になっている。


1. なぜ LoRA なのか ── i2i では限界がある

Anima の素の能力は十分強い。t2i だけでも構図・背景・ライティングまで含めてマンガコマとして成立する絵が出る。問題は キャラ identity の一貫性

たとえばノリの全身参照画像を i2i に投げて「夜の机でデバッグしているシーン」を頼むと、こうなる。ref と同じ姿勢の絵を denoise=0.5 で投げただけだと、表情だけ差分されたほぼ同じ絵が返ってきて、構図変化が起きない。

denoise を低くしても表情差分しか出ない初期 PoC

「i2i の denoise を上げれば構図変化が出る」のは事実だが、今度は キャラの顔つき・髪型・服装が ref から離れていく。i2i は構造を残す処理であって、キャラ identity を保証する仕組みではない。同じポーズの ref を毎回用意できるならそれで足りるが、マンガのコマで「立ち姿」「座位」「上から見下ろし」「真横」が混ざるなら無理だ。

ここで普通なら IP-AdapterPuLIDInstantID のような「キャラ参照画像を埋め込みとして注入する」系の技術に逃げる。SDXL や FLUX には豊富にある。Cosmos-Predict2 系統については、本記事執筆時点で公式サポートされた IP-Adapter / PuLID / InstantID 実装を筆者は確認できていない(Cosmos 用に学習し直された IPAdapter モデルが公開されているのを見つけられなかった、というだけかもしれない。検証された情報があれば歓迎)。Anima は Cosmos-Predict2 ベースなので、IPAdapter コミュニティの SDXL/FLUX 用資産がそのまま使えるわけではない。

実用的な選択肢として残ったのが、LoRA でキャラを焼く だった。


2. データセットを「作らずに集める」

LoRA を作るときに一番面倒なのが学習データセット。普通は ref 画像を 30〜100 枚撮影/生成して、キャプションを書いて、整える。

ただし今回は manginus という自前の漫画エディタを使っていて、そこに既に大量のキャラ panel が溜まっている。story.json の中身を覗くと、各 panel の image.characters[].name メタデータが残っているので、「ノリ単独で写ってる panel」だけをフィルタ抽出 すれば自動でデータセットになる。

python3 extract_solo_panels.py --character のり子 --out nori_candidates.tsv

これで 1326 枚 が出てきた(82 プロジェクトを横断した合計)。多すぎる。LoRA は 60〜100 枚で十分(それ以上は過学習リスク)。

構図バランスを取って 76 枚に絞る

そのままシャッフル抽出すると bust shot が圧倒的多数になる。物語が会話中心だからだ。

構図バケツノリ既存クロコ既存
bust660902
close-up306147
medium241174
full / wide293

特にクロコは 全身カットが既存タグだとたったの 3 枚しかない。会話の脇でずっと立ってる役なので、composition フィールドに「bust shot」と書いてある panel が多数を占めていた。

python3 sample_dataset.py --src nori_candidates.tsv --out dataset/ \
  --quota full=29 --quota bust=25 --quota closeup=15 --quota medium=15 \
  --per-project-cap 10

1 プロジェクト max 10 枚で画風偏り防止。これでノリは bust 偏重を避けつつ 84 枚揃った。

ノリ用データセット 29 枚モンタージュ(全身バケツのみ抜粋)


3. クロコで詰まる ── nemu の composition タグは粗かった

ノリは何とかなったが、クロコの「全身 3 枚しかない」は重い。LoRA 学習で「全身がほぼ写ってないデータセット」を回すと、出来上がりは ほぼバストアップ専用 の LoRA になり、クロコのオレンジサイバーアーマー全身デザインがほぼ学習されない。

ここで疑問が湧いた。「クロコは本当に全身カットが 3 枚しかないのか?」

調べると、ある事実が見えた。

  • nemu(ネーム = ネタの YAML)の composition タグは、ネーム会議の段階で人間/AI が書いている粗いラベル
  • 実際の生成画像を見ると、composition が「bust shot」でも、画像自体は膝下まで写ってる縦長の構図、というケースが結構ある

そこで画像 アスペクト比 で再判定するようにした:

# composition タグが full 以外でも、h/w >= 1.4 なら full に格上げ
if r["_bucket"] != "full":
    w, h = Image.open(r["image_path"]).size
    if h / w >= 1.4:
        r["_bucket"] = "full"

これでクロコは 71 枚が full に格上げされた。実態は composition タグの過小評価で、画像を見れば全身寄りカットが大量にあった。

sample_dataset.py--use-aspect フラグを追加。クロコ用データセットは bust 26 / full 25 / medium 13 / closeup 12 で揃った。

クロコ全身データセット 25 枚モンタージュ(アスペクト比格上げで救済)


4. caption は「変動要素だけ」書く

LoRA 学習の caption(キャプション)の書き方には流派が分かれるが、今回は 「不変なキャラ identity はトリガーワードに焼く / 変動要素のみ caption に書く」 方針を採った。

noricloco_nori, anime illustration, full body, focused frown
noricloco_kuroko, anime illustration, upper body, gentle smile

トリガーワード(noricloco_nori / noricloco_kuroko)は他の語彙と衝突しない一意の文字列。これに「顔・髪・服装」を全部焼く。caption には bucket(構図) と表情だけ書く。

理由は明快で、画像に映っている不変要素を caption に書くと、その単語と「ノリの顔」が二重に結びついて identity が拡散する。「ML NO TRADE フード」を caption に書くと、noricloco_nori を呼ばずに「ML NO TRADE hoodie」とだけ書いてもノリの顔が出てきてしまう。それは LoRA の目的(トリガーワード呼び出しで一意に identity を出す)と矛盾する。


5. diffusion-pipe で学習する

Anima の作者 tdrussell が同時に公開している学習ツール diffusion-pipe が、2026-02-04 から Anima を公式サポートしている。これを使う。

設定の要点(training.toml)

公式ドキュメント(docs/supported_models.md の Anima セクション)に従う:

epochs = 5
blocks_to_swap = 20          # 24GB GPU で 2B + LoRA はこれが安定
map_num_proc = 1             # 後述、/dev/shm 64MB 対策で必須
activation_checkpointing = true
 
[model]
type = "anima"
transformer_path = "/.../anima-base-v1.0.safetensors"
vae_path = "/.../qwen_image_vae.safetensors"
llm_path = "/.../qwen_3_06b_base.safetensors"
dtype = "bfloat16"
llm_adapter_lr = 0           # LLM adapter freeze (小データなら必須)
 
[adapter]
type = "lora"
rank = 32
dtype = "bfloat16"
 
[optimizer]
type = "AdamW8bitKahan"
lr = 2e-5

llm_adapter_lr = 0 がポイント。Anima には Qwen3 のテキスト埋め込みを diffusion 側に渡す adapter が挟まっているが、小さいデータセットだとここを学習するとかえって不安定になる。Anima 作者推奨は freeze

学習時間は 24GB GPU(RTX 3090)で 1 step 約 3.2 秒。76 枚 × num_repeats=10 × 5 epoch = 3800 step なので、約 3〜3.4 時間で完走する。


6. 初回で踏んだ落とし穴 6 つ

ここからが本番。初回学習は確実に詰まる。順番に書く。

落とし穴 1: CUDA_HOME does not exist(deepspeed)

ComfyUI 公式コンテナ(yanwk/comfyui-boot:cu128-slim)には CUDA SDK が入っていない。nvcc も無い。PyTorch は cu128 で動いているのに、deepspeed は import 時に nvcc -V を実行して CUDA バージョンを確認しようとして例外で落ちる。

deepspeed.ops.op_builder.builder.MissingCUDAException: CUDA_HOME does not exist

pip install nvidia-cuda-nvcc-cu12 は ptxas しか入れない。nvcc 本体は含まれていない。これは罠だった。

解決は単純で、stub の nvcc を作って CUDA_HOME に指定する。deepspeed は nvcc -V の出力を parse するだけなので、本物である必要はない:

mkdir -p /opt/fake_cuda/bin
cat > /opt/fake_cuda/bin/nvcc << 'EOF'
#!/bin/bash
echo "nvcc: NVIDIA (R) Cuda compiler driver"
echo "Cuda compilation tools, release 12.8, V12.8.93"
EOF
chmod +x /opt/fake_cuda/bin/nvcc
 
CUDA_HOME=/opt/fake_cuda PATH=/opt/fake_cuda/bin:$PATH deepspeed ...

CUDA op の JIT ビルドは必要ない(pre-built whl で動く)し、deepspeed の compatible check が False を返すだけで実害は無い。

落とし穴 2: comfy_aimdo.vram_buffer not found

diffusion-pipe の submodule にある最新 ComfyUI が comfy_aimdo の新しいサブモジュールを要求するが、PyPI の comfy-aimdo 0.2.x には未収録。

pip install --upgrade comfy-aimdo  # 0.4+ に上げる

落とし穴 3: /dev/shm 64MB で shm エラー

Docker のデフォルト /dev/shm は 64MB しかない。PyTorch DataLoader の multiprocess worker が共有メモリを取りに来ると爆発する。

RuntimeError: unable to allocate shared memory(shm) for file <...>

本筋は docker-composeshm_size: 8gb だが、recreate で pip install したものが全部消えるので、training.tomlmap_num_proc = 1 を追加する方が軽い。multiprocess 自体を切れば shm 不要。学習速度に大きな差はなかった。

落とし穴 4: tail -60 パイプで進捗が見えなくなる

これが一番ハマった。

... deepspeed ... train.py ... 2>&1 | tail -60

このまま走らせると、Python の stdout が tail -60 側のバッファに溜まって flush されない。プロセスはちゃんと動いてるのに「スタックしてる」ようにしか見えない。1 時間 25 分眺めて気付いた。

# 正解: 直接ファイルにリダイレクト、別シェルで tail -F
PYTHONUNBUFFERED=1 deepspeed ... > /root/train.log 2>&1

これは LoRA 関係なく一般則として覚えておく価値がある。

落とし穴 5: HuggingFace の HTTP/2 RST_STREAM

Anima の anima-base-v1.0.safetensors は 4.18GB。途中で SSL EOF / HTTP/2 stream reset で落ちることがある。

curl --http1.1 -C - --retry 5 --retry-delay 3 -fL -o anima-base-v1.0.safetensors ...

HTTP/1.1 にして、-C - で再開可能にする。

落とし穴 6: 9p drvfs(Windows D: マウント)の遅さ

dataset を D:\ComfyUI-models\training_data\ 配下に置いて学習に使うと、メタデータキャッシュ生成が異常に遅い。

cp -r /root/ComfyUI/models/training_data/nori_lora_v1/. /root/dataset_nori/

コンテナの overlay fs にコピーしてから学習に使えば解決する。


7. 結果 ── ノリとクロコ

学習後の LoRA(133MB の safetensors)を loras/anima/ に配置して、同じプロンプト・同じ seed で LoRA on/off 4 通り を生成して比較する。

ノリ(ML NO TRADE フードのハッカー女子)

参照画像:

ノリの参照画像(全身 1024×1216)

LoRA なし(ベースライン) vs LoRA あり(strength 1.0)

t2ii2i (denoise 0.75)
baselinenori t2i baselinenori i2i baseline
+ LoRAnori t2i + LoRAnori i2i + LoRA

LoRA を入れると、髪の流れ・前髪の束感・顔のバランスが ref に明確に寄る。ただし服のロゴ "ML NO TRADE" は "MNO TRDACE" のように字化けが残る。これは Qwen3 0.6B の text encoder + Anima の限界で、LoRA でも救えなかった。重要なロゴ文字は後処理で合成するのが現実解。

クロコ(オレンジサイバーアーマーの AI コンパニオン)

参照画像:

クロコの参照画像(全身)

t2ii2i (denoise 0.75)
baselinekuroko t2i baselinekuroko i2i baseline
+ LoRAkuroko t2i + LoRAkuroko i2i + LoRA

クロコの方は ノリより明確に LoRA が効いた。理由は 3 つある:

  1. 服のロゴ文字が無い ── ノリのロゴ字化け問題が起きない
  2. オレンジサイバーアーマー = 視覚シグネチャが強烈 ── LoRA が焼きやすい
  3. アスペクト比格上げで全身カットを 3 → 25 に救済できた ── ノリと同等の学習が回せた

特に i2i + LoRA(右下)は、ref のアーマー構造(胸の V 字、腰のトライアングル、太もものオレンジ装飾)がほぼ完全コピーされている。


8. わかったこと

Anima での LoRA の位置づけ

  • Cosmos-Predict2 系統で、本記事執筆時点で公式サポートされた IP-Adapter / PuLID / InstantID 実装を確認できていない(SDXL/FLUX 用の資産はそのまま使えない)
  • 実用的にキャラ identity 一貫性を取りに行くなら LoRA がいまの所現実解
  • 76 枚 / 5 epoch / 3 時間で実用ラインに乗る
  • ベースモデル自体の絵の質が高いので、LoRA は identity だけを上乗せする運用がよい

データセット作りで効いた工夫

  • composition タグだけでなく 画像アスペクト比 で bucketing し直すと、過小評価されていた全身カットを救える(クロコは 71 枚救済)
  • 1 プロジェクトあたり上限を設けて画風偏り防止
  • caption は「変動要素のみ」、不変はトリガーワードに焼く

Anima 個別の弱点

  • 服のロゴや看板の 小さな英字テキストは再現できない(LoRA でも救えない)
  • 章立て構造のプロンプト(【スタイル】等の見出し付き)は通らない。Danbooru タグ + 自然文に揃える
  • 日本語プロンプトはほぼ通らない(Qwen3 ベースだが diffusion 側が日本語データで訓練されていないため)

推奨運用

用途設定
純粋な identity が欲しいt2i + LoRA strength 1.0、プロンプト冒頭に <trigger>,
既存コマと構図を合わせたいi2i (denoise 0.5-0.7) + LoRA strength 0.7-1.0
背景マシマシで派手なコマt2i + LoRA strength 0.7-0.9、cinematic 指示を盛る

9. まとめ

i2i だけだと表情差分しか出ない、という壁にぶつかったときは、構造的にそれを救う技術がそのモデル系統に存在するかをまず確認する。今回 Cosmos-Predict2 系統では IPAdapter 等の公式実装を見つけられず、LoRA が現実的な道として残った(より良い道を後から知ったら本記事を改訂したい)。

実装としては、diffusion-pipe の Anima 公式サポートと、ComfyUI の LoraLoader の組み合わせで、初回学習でも 3 時間で実用 LoRA が焼ける。データセットも、既に manginus に溜まった panel を extract_solo_panels.py + アスペクト比格上げで自動的に作れた。新規に ref 画像を撮ったり生成したりする必要は無かった。

落とし穴は確実にあるが、6 つのうち 4 つは「環境の不備を 1 行で stub する」系で、本質的なものではない。tail -60 で進捗が見えなくなる罠だけは、LoRA 関係なく一般則として覚えておきたい。

次は 2 体の LoRA を同時 stack して共演シーンを出すところ。それと、本記事の手順を ~/.claude/skills/comfyui-anima/ にスキル化したので、他キャラ(お料理部、Lost Valkyries 等)を量産するときは同じパイプラインで 4 時間 / 1 体 のペースで増やせる見込み。


参考

  • Anima (HuggingFace)
  • diffusion-pipe (GitHub)
  • 本記事で確立した手順はローカルの Claude Code skill (~/.claude/skills/comfyui-anima/) として整理した(他キャラの量産時にそのまま使い回せる)