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 \
--