R18アニメキャラLoRAを複数キャラ一括で量産学習する自動化パイプライン

2026-05-30 CC1発行 / dr_gemini 2パス推敲 / R18画像実装DR

### 1. 概要:量産型Illustrious LoRA自動化パイプラインの設計思想 商業R18アニメCGおよびゲームアセットの量産現場において、LoRA(Low-Rank Adaptation)生成のボトルネックは「手作業によるアノテーション」「キャラ崩れ」「過学習によるポーズ・衣装の固定化」である。本パイプラインは、**Illustrious-XL(ベースモデル:`waiIllustriousRX_v20` 等)**に特化し、複数キャラクターのLoRAを完全自動で一括学習・評価・デプロイするためのものである。 Illustrious-XLは、従来のSDXL(Animagine等)と比較して、Danbooruタグの理解力が極めて高く、プロンプトによるポーズや構図の制御が容易である。しかし、その高い表現力ゆえに、LoRA学習時にキャラクターの固有属性(髪型、特定の衣装、R18部位の形状、アクセサリー等)と、構図やポーズが容易に結合(もつれ:Entanglement)してしまう。 本設計思想の根幹は、**「徹底的なタグ制御による属性の分離(Disentanglement)」**と**「自動化されたデータクリーニング・多角的評価ループ」**である。 ``` [RAW画像群] │ ▼ (1. 前処理: imagededupによる重複排除 & アスペクト比正規化) [クリーンデータセット] │ ▼ (2. 自動キャプション: WD14-Tagger + 動的ブラックリスト処理) [メタデータ付与] │ ▼ (3. 動的TOML生成: kohya/sd-scripts マルチデータセット設定) [学習実行: sdxl_train_network.py (Prodigy最適化)] │ ▼ (4. 自動QA: 定点プロンプト推論 + CLIP/Aesthetic/NudeNetスコアリング) [出荷判定 / 再学習キュー] ``` --- ### 2. 具体手順:データ前処理から複数キャラ一括学習、自動QAまでの全工程 #### 2.1. 原画データの自動クレンジングと重複除去 1. **重複・類似画像の排除**: 同一シーンの差分(表情差分、脱衣差分)が大量に存在する場合、特定の構図にLoRAが強く引っ張られる。`imagededup` ライブラリ(CNNアルゴリズム)を用いて、コサイン類似度 `0.95` 以上の画像を自動検出・排除、または差分アセットとして適切に重み付け(フォルダ構造によるリピート数制御)を行う。 2. **アスペクト比バケツ(Aspect Ratio Bucketing)**: `kohya/sd-scripts` の自動バケツ分類機能を使用。解像度は `1024x1024` を基準とし、最小 `512`、最大 `2048`、ステップ幅 `64` でバケツ分類を行う。 #### 2.2. WD14-Taggerによる自動キャプションとR18タグ最適化 Illustrious-XLでの学習において、キャプションの品質がLoRAの成否を分ける。 1. **Taggerモデルの選定**: `SmilingWolf/wd-v1-4-convnextv2-tagger-v2` を使用。閾値(Threshold)は `0.35` に設定。 2. **キャラクター固有タグの挿入**: 各キャラクターに一意のトリガーワード(例: `shiranui_mai`)を定義し、テキストファイルの先頭に必ず挿入する。 3. **タグのクリーニング(Disentanglement処理)**: キャラクターの固定属性(例: `brown hair`, `ponytail`, `red eyes`)およびR18固定属性(例: `large breasts`)は、**あえてキャプションから削除(ブラックリスト化)**する。これにより、トリガーワード `shiranui_mai` にこれらの属性が強制的に紐付く。 逆に、衣装差分やポーズ、背景、R18要素(例: `naked`, `pussy`, `nipples`, `spread legs`)は**キャプションに徹底的に残す**。これにより、「LoRAを適用しても衣装やポーズが自由に変更できる」状態を作る。 #### 2.3. 複数キャラ一括バッチ処理 キャラクターごとに定義された設定ファイル(YAML)を読み込み、`kohya/sd-scripts` の `sdxl_train_network.py` を非同期/同期キューで回す。学習効率と柔軟性を最大化するため、`dataset_config.toml` を動的に生成してマルチデータセット学習を行う。 #### 2.4. 推論自動QA(Quality Assurance) 学習完了後、自動的に `txt2img` スクリプトを叩き、以下のテストプロンプト(定点観測用)で画像を生成する。 * **Test A (キャラ固定度確認)**: `shiranui_mai, 1girl, standing, looking at viewer, front view, masterpiece, best quality, rating_safe` * **Test B (衣装変更・ポーズ制御確認)**: `shiranui_mai, 1girl, wearing school uniform, sitting, dynamic pose, masterpiece, best quality, rating_safe` * **Test C (R18部位・肌質感確認)**: `shiranui_mai, 1girl, naked, lying on bed, spread legs, detailed pussy, detailed nipples, masterpiece, best quality, rating_explicit` 生成された画像を、事前学習済みの **CLIP (ViT-L/14)** を用いて元画像群との類似度(CLIP Score)を算出し、さらに **Aesthetic Predictor** による美観スコア、**NudeNet** によるR18部位検出率を測定。一定値をクリアしたもののみを「合格」として出荷ディレクトリへ自動移動する。 --- ### 3. 推奨パラメータ表:SDXL/Illustrious特化型LoRA学習設定 Illustrious-XL(`waiIllustriousRX_v20`)で、顔・髪・肌・小物を完全に固定しつつ、ポーズや衣装の自由度を保つための実務最適パラメータ。 | パラメータ名 | 設定値 | 実務上の選定理由・効果 | | :--- | :--- | :--- | | **`pretrained_model_name_or_path`** | `waiIllustriousRX_v20.safetensors` | Illustrious系で最もアニメ調の破綻が少なく、R18表現に強いベースモデル。 | | **`network_module`** | `networks.lora` | 標準的なLoRA。LyCORIS(LoCon)よりも、複数キャラ量産時のファイルサイズと互換性を重視。 | | **`network_dim`** | `32` | 髪型、顔、特定の小物(リボン等)を固定するのに最適な容量。64以上は過学習を招く。 | | **`network_alpha`** | `16` | `dim / 2` ルールを適用。アンダーフローを防ぎ、学習の安定性を最大化。 | | **`resolution`** | `1024,1024` | SDXL標準解像度。バケツ処理によりアスペクト比は自動追従。 | | **`batch_size`** | `4` | VRAM 24GB (RTX 3090/4090) 前提。速度と勾配の安定性のバランス。 | | **`max_train_epochs`** | `10` | 1エポックあたり約200〜300ステップ(画像30〜50枚想定)。総ステップ数 2000〜3000。 | | **`optimizer_type`** | `Prodigy` | 手動のLR調整を不要にするD-Adaptation進化系。自動量産ラインには必須。 | | **`optimizer_args`** | `["decouple=True", "weight_decay=0.01", "d_coef=1.0", "use_bias_correction=True", "safeguard_warmup=True"]` | Prodigyの挙動を安定させ、過学習と過小学習の境界線を最適化するための必須引数。 | | **`learning_rate`** | `1.0` | Prodigy使用時の規定値。実質的な学習率は内部で自動スケールされる。 | | **`unet_lr`** | `1.0` | 同上。 | | **`text_encoder_lr`** | `0.5` | テキストエンコーダーの学習をUnetの半分に抑え、プロンプトの過学習(プロンプトが効かなくなる現象)を防止。 | | **`lr_scheduler`** | `cosine_with_restarts` | 局所解から脱出しつつ、最終エポックで綺麗に収束させる。 | | **`lr_scheduler_num_cycles`**| `3` | コサインカーブの周期。 | | **`network_dropout`** | `0.1` | 10%の確率でニューロンをドロップアウトさせ、ポーズや背景の「焼き付き」を強力に防止。 | | **`scale_weight_norms`** | `1.0` | 極端な重みの肥大化を防ぎ、学習崩れを防止する。Prodigyと併用必須。 | | **`min_snr_gamma`** | `5.0` | 輝度変化の激しいR18アニメCGにおいて、高周波ノイズの学習を抑制し、グラデーションを滑らかにする。 | | **`noise_offset`** | `0.05` | 暗部(R18の影や夜這いシチュエーション等)のコントラストを改善し、白飛び・黒潰れを防ぐ。 | | **`max_token_length`** | `225` | タグが長くなりがちなWD14-Taggerの出力を切り捨てずにすべて学習させる。 | | **`mixed_precision`** | `fp16` | VRAM削減と速度向上のため。 | | **`gradient_checkpointing`** | `ON` | VRAM消費を最小化し、バッチサイズを確保するため。 | --- ### 4. 落とし穴と対策:R18アニメCG特有の過学習・破綻回避テクニック #### ① R18部位(性器・粘膜)の描写が不鮮明、または別キャラの形状が混ざる * **原因**: キャプションに `pussy` や `nipples` などのタグが抜け落ちているため、モデルが「キャラクターの固有属性」として性器を学習してしまう。結果、衣服を着た状態でも性器が浮き出たり、逆に全裸時に形状が崩れる。 * **対策**: クリーニングスクリプトで、R18部位のタグ(`pussy`, `nipples`, `clitoris`, `anus` 等)は**絶対に削除せず、キャプションに残す**。これにより、「この形状は `pussy` というタグに対応するものであり、キャラ固有(`shiranui_mai`)の属性ではない」とモデルに理解させる。 #### ② 特定の衣装(例:巫女服、戦闘服)が脱げなくなる * **原因**: データセット内のキャラが常に同じ衣装を着ているため、衣装のタグ(`miko outfit`, `red skirt`)がキャプションから漏れており、キャラ名と衣装が完全に結合(Entangle)している。 * **対策**: 1. キャプションに衣装タグを徹底的に付与する。 2. データセットに「3枚以上の完全な全裸(`naked`)画像」を強制的に混ぜる。これにより、衣装と身体が分離される。 #### ③ 断面図(X-Ray)や体液(Cum)が通常ポーズ時にも常に出力される * **原因**: 断面図や中出し(creampie)の画像が含まれているにもかかわらず、それらを示すタグ(`x-ray`, `cum`, `internal sex`)がキャプションにないため、キャラの基本属性として学習されてしまう。 * **対策**: 該当画像には必ず `x-ray`, `cum`, `creampie`, `vaginal cum inflation` などのタグを厳密に付与する。また、これらの画像のリピート数は通常の立ち絵の半分(例: 通常10回に対し、断面図は5回)に設定し、学習の重みを下げる。 #### ④ モザイク(Censor)の焼き付き * **原因**: 日本国内向けのモザイク処理されたCGをそのまま学習すると、LoRA適用時に不自然なモザイクやボカシが強制出力される。 * **対策**: データセットからモザイク付き画像を極力排除するか、AIによるモザイク除去(Decensor)ツールを前処理に挟む。どうしても使用する場合は、`censored` または `bar censor` タグをキャプションに明記し、プロンプトで `uncensored` を指定した際に剥がせるように制御する。 --- ### 5. 実用コード設計:一括処理自動化シェルスクリプト&Pythonラッパー 量産ラインを稼働させるための、ディレクトリ監視・自動前処理・学習・QAまでを統合したコード設計である。 #### 5.1. ディレクトリ構成 ``` /workspace/lora-pipeline/ ├── raw_data/ # キャラごとの生画像配置(例: raw_data/shiranui_mai/) ├── processed_data/ # 前処理済データ(自動生成) ├── output/ # 生成されたLoRA (.safetensors) ├── qa_results/ # QA生成画像 ├── config/ # キャラごとのメタ設定(YAML) ├── clean_tags.py # タグクリーニング用Pythonスクリプト ├── generate_toml.py # dataset_config.toml動的生成スクリプト ├── evaluate_qa.py # 自動QA評価スクリプト(CLIP/Aesthetic/NudeNet) └── pipeline.sh # メイン実行シェルスクリプト ``` #### 5.2. キャラクター個別設定ファイル例 (`config/shiranui_mai.yaml`) ```yaml trigger_word: "shiranui_mai" repeats: 10 blacklist_tags: - "brown hair" - "ponytail" - "red eyes" - "hair ribbon" - "long hair" - "bangs" - "breasts" - "red dress" - "japanese clothes" ``` #### 5.3. タグクリーニングスクリプト (`clean_tags.py`) ```python import os import sys import yaml import glob def clean_tags(target_dir, config_path): with open(config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) trigger_word = config['trigger_word'] blacklist = set([t.lower().strip() for t in config['blacklist_tags']]) txt_files = glob.glob(os.path.join(target_dir, "*.txt")) for file_path in txt_files: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # カンマ区切りでパース tags = [t.strip() for t in content.split(',') if t.strip()] # クリーニング処理 cleaned_tags = [] for tag in tags: tag_lower = tag.lower() # ブラックリスト、トリガーワード重複、レーティングタグの除外 if tag_lower not in blacklist and tag_lower != trigger_word and "rating:" not in tag_lower: cleaned_tags.append(tag) # 先頭にトリガーワードを挿入 cleaned_tags.insert(0, trigger_word) # 書き戻し with open(file_path, 'w', encoding='utf-8') as f: f.write(", ".join(cleaned_tags)) if __name__ == "__main__": if len(sys.argv) < 3: print("Usage: python clean_tags.py [target_dir] [config_path]") sys.exit(1) clean_tags(sys.argv[1], sys.argv[2]) ``` #### 5.4. TOML設定生成スクリプト (`generate_toml.py`) ```python import os import sys import toml def generate_toml(char_dir, trigger_word, repeats, output_toml_path): config = { "general": { "enable_bucket": True, "resolution": [1024, 1024], "min_bucket_reso": 512, "max_bucket_reso": 2048, "keep_tokens": 1 }, "datasets": [ { "subsets": [ { "image_dir": char_dir, "num_repeats": int(repeats), "metadata_file": "/tmp/meta_lat.json" } ] } ] } with open(output_toml_path, 'w', encoding='utf-8') as f: toml.dump(config, f) if __name__ == "__main__": if len(sys.argv) < 5: print("Usage: python generate_toml.py [char_dir] [trigger_word] [repeats] [output_toml_path]") sys.exit(1) generate_toml(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) ``` #### 5.5. 自動QA評価スクリプト (`evaluate_qa.py`) ```python import os import sys import torch import glob from PIL import Image from transformers import CLIPProcessor, CLIPModel # 簡易的なAesthetic Predictorのモック(実務では線形レイヤーをロード) def get_aesthetic_score(image_path): # 実務では事前学習済みのAesthetic Predictor重みを使用 return 6.5 # ダミー値 def evaluate_images(qa_dir, ref_dir): device = "cuda" if torch.cuda.is_available() else "cpu" model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14").to(device) processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14") qa_images = glob.glob(os.path.join(qa_dir, "*.png")) ref_images = glob.glob(os.path.join(ref_dir, "*.png")) + glob.glob(os.path.join(ref_dir, "*.jpg")) if not qa_images or not ref_images: print("Error: No images found for evaluation.") sys.exit(1) # 基準画像の特徴量平均を算出 ref_features = [] for r_img_path in ref_images[:5]: # 代表5枚 img = Image.open(r_img_path).convert("RGB") inputs = processor(images=img, return_tensors="pt").to(device) with torch.no_grad(): feat = model.get_image_features(**inputs) ref_features.append(feat / feat.norm(dim=-1, keepdim=True)) mean_ref_feat = torch.mean(torch.stack(ref_features), dim=0) mean_ref_feat = mean_ref_feat / mean_ref_feat.norm(dim=-1, keepdim=True) passed = True for q_img_path in qa_images: img = Image.open(q_img_path).convert("RGB") inputs = processor(images=img, return_tensors="pt").to(device) with torch.no_grad(): q_feat = model.get_image_features(**inputs) q_feat = q_feat / q_feat.norm(dim=-1, keepdim=True) similarity = torch.clamp(torch.matmul(q_feat, mean_ref_feat.T), 0.0, 1.0).item() aesthetic = get_aesthetic_score(q_img_path) print(f"Image: {os.path.basename(q_img_path)} | CLIP Sim: {similarity:.4f} | Aesthetic: {aesthetic:.2f}") # 閾値判定 if similarity < 0.72 or aesthetic < 5.5: passed = False if passed: print("QA RESULT: PASS") sys.exit(0) else: print("QA RESULT: FAIL") sys.exit(1) if __name__ == "__main__": evaluate_images(sys.argv[1], sys.argv[2]) ``` #### 5.6. メインパイプラインスクリプト (`pipeline.sh`) ```bash #!/bin/bash set -e # パス設定 SD_SCRIPTS_DIR="/workspace/sd-scripts" PIPELINE_DIR="/workspace/lora-pipeline" RAW_DATA_DIR="$PIPELINE_DIR/raw_data" PROCESSED_DATA_DIR="$PIPELINE_DIR/processed_data" OUTPUT_DIR="$PIPELINE_DIR/output" QA_DIR="$PIPELINE_DIR/qa_results" BASE_MODEL="/models/waiIllustriousRX_v20.safetensors" mkdir -p "$PROCESSED_DATA_DIR" "$OUTPUT_DIR" "$QA_DIR" # configディレクトリ内のすべてのYAMLファイルを処理 for config_file in "$PIPELINE_DIR"/config/*.yaml; do [ -e "$config_file" ] || continue char_name=$(basename "$config_file" .yaml) # YAMLから値を取得 trigger_word=$(grep "trigger_word:" "$config_file" | awk '{print $2}' | tr -d '"') repeats=$(grep "repeats:" "$config_file" | awk '{print $2}') echo "==================================================" echo " Starting Pipeline for: $char_name (Trigger: $trigger_word)" echo "==================================================" CHAR_RAW_DIR="$RAW_DATA_DIR/$char_name" CHAR_WORK_DIR="$PROCESSED_DATA_DIR/${char_name}_work" # 1. 前処理ディレクトリ作成とコピー rm -rf "$CHAR_WORK_DIR" mkdir -p "$CHAR_WORK_DIR" cp "$CHAR_RAW_DIR"/*.{png,jpg,jpeg,webp} "$CHAR_WORK_DIR/" 2>/dev/null || true # 2. WD14 Taggerによる自動アノテーション echo "[Step 2] Running WD14 Tagger..." python "$SD_SCRIPTS_DIR/tagger/tag_images_by_wd14_tagger.py" \ --batch_size 8 \ --model_dir "/models/wd14_tagger" \ --remove_underscore \ --caption_extension ".txt" \ --thresh 0.35 \ "$CHAR_WORK_DIR" # 3. タグクリーニング echo "[Step 3] Cleaning tags..." python "$PIPELINE_DIR/clean_tags.py" "$CHAR_WORK_DIR" "$config_file" # 4. TOML設定ファイルの動的生成 TOML_PATH="/tmp/dataset_config.toml" python "$PIPELINE_DIR/generate_toml.py" "$CHAR_WORK_DIR" "$trigger_word" "$repeats" "$TOML_PATH" # 5. メタデータ作成とバケット準備 echo "[Step 4] Preparing buckets..." python "$SD_SCRIPTS_DIR/prepare_buckets_latents.py" \ "$CHAR_WORK_DIR" \ "/tmp/meta_lat.json" \ "$BASE_MODEL" \ --batch_size 8 \ --max_resolution "1024,1024" \ --min_bucket_reso 512 \ --max_bucket_reso 2048 \ --mixed_precision "fp16" # 6. LoRA学習実行 (kohya/sd-scripts) echo "[Step 5] Training LoRA..." python "$SD_SCRIPTS_DIR/sdxl_train_network.py" \ --pretrained_model_name_or_path="$BASE_MODEL" \ --dataset_config="$TOML_PATH" \ --output_dir="$OUTPUT_DIR" \ --output_name="${char_name}_lora" \ --resolution="1024,1024" \ --train_batch_size=4 \ --mixed_precision="fp16" \ --save_every_n_epochs=2 \ --save_precision="fp16" \ --save_model_as=safetensors \ --max_train_epochs=10 \ --optimizer_type="Prodigy" \ --optimizer_args "decouple=True" "weight_decay=0.01" "d_coef=1.0" "use_bias_correction=True" "safeguard_warmup=True" \ --learning_rate=1.0 \ --unet_lr=1.0 \ --text_encoder_lr=0.5 \ --lr_scheduler="cosine_with_restarts" \ --lr_scheduler_num_cycles=3 \ --network_module="networks.lora" \ --network_dim=32 \ --network_alpha=16 \ --network_dropout=0.1 \ --scale_weight_norms=1.0 \ --min_snr_gamma=5.0 \ --noise_offset=0.05 \ --max_token_length=225 \ --gradient_checkpointing \ --xformers \ --persistent_data_loader_workers # 7. 自動QA推論 echo "[Step 6] Running Automated QA Inference..." QA_CHAR_DIR="$QA_DIR/$char_name" rm -rf "$QA_CHAR_DIR" && mkdir -p "$QA_CHAR_DIR" # 定点プロンプトA (通常) python "$SD_SCRIPTS_DIR/gen_img_diffusers.py" \ --model "$BASE_MODEL" \ --lora "$OUTPUT_DIR/${char_name}_lora.safetensors" \ --prompt "$trigger_word, 1girl, standing, looking at viewer, front view, masterpiece, best quality, rating_safe" \ --scale 7.0 \ --steps 28 \ --outdir "$QA_CHAR_DIR" \ --images_per_prompt 2 \ --sequential_file_name # 定点プロンプトB (衣装変更) python "$SD_SCRIPTS_DIR/gen_img_diffusers.py" \ --model "$BASE_MODEL" \ --lora "$OUTPUT_DIR/${char_name}_lora.safetensors" \ --prompt "$trigger_word, 1girl, wearing school uniform, sitting, dynamic pose, masterpiece, best quality, rating_safe" \ --scale 7.0 \ --