AI活用

LoRA学習用データセットの作り方|画像選定・キャプション・フォルダ構成

LoRA学習前に画像と同名のキャプションを揃える手順。画像の偏りを減らす選び方、sd-scriptsのTOML設定、繰り返しフォルダとの違い、欠落・重複を検出するPythonコードを紹介します。

この記事の目次
  1. 学ばせたい特徴と、毎回変わってよい条件を分ける
  2. 枚数を決め打ちせず、画像の採否と偏りを記録する
  3. キャプションは、共通の呼び名と画像に見える条件を揃える
  4. 画像と同名の.txtを置き、TOMLで読む場所を指定する
  5. 学習前に、欠落・同名・重複を読み取りだけで点検する
  6. 来歴と確認結果を残して、学習ソフトへ引き渡す

LoRAの学習用データセットは、画像を集めるだけでは完成しません。学ばせたい対象が分かる画像を選び、画像ごとの説明を同名のテキストファイルへ保存し、学習ソフトが読む場所を指定するところまで揃えます。

この記事では、sd-scriptsへ画像+同名の.txt+TOML設定を渡す方式で準備します。自分で制作・撮影し、予定する学習や公開に使えると確認した素材を前提にします。画像選定とキャプションの考え方はキャラクター・画風・物体に応用できますが、設定ファイルはどの学習ソフトにも共通する形式ではありません。

最初の完成目標は、枚数を増やすことより、画像と説明が一対一で対応し、何を学ばせる素材なのか説明できる状態です。学習後の再現性や最適な枚数・学習率は、実際の学習と比較で決めます。

学ばせたい特徴と、毎回変わってよい条件を分ける

まず「このLoRAで何を再現したいか」を一文にします。たとえば、自作の陶器マグなら「丸い取っ手と青い釉薬の見た目を、別の背景でも再現したい」です。マグそのものを学ばせたいのに、同じ木の机に置いた正面写真だけを集めると、背景や角度を変えて使えるかが見えにくくなります。

対象 揃えたい特徴 集めるときに変える条件
自作キャラクター 顔・髪・体格など、同じ人物だと分かる要素 視点、表情、ポーズ、背景。衣装を固定したいかも先に決める
自作の画風 線、塗り、配色、質感 描く対象や構図。いつも同じ人物・小物だけになっていないか
商品・物体 形、素材、再現したい色や模様 角度、置く場所、照明、画面内の大きさ

ここでいう「変える」は、同じ画像を大量に加工して増やすことではありません。必要な条件の違う素材を選び、足りない視点を撮る・描くための整理です。キャラクターの右側にだけ印がある、商品の文字を正しく保ちたい、といった場合は、左右反転を気軽に混ぜないようにします。

スポンサーリンク

枚数を決め打ちせず、画像の採否と偏りを記録する

「必ず20枚」「50枚あれば十分」という枚数だけでは、使える素材かを判定できません。まず一覧で角度や背景の偏りを見つけ、次に一枚ずつ、残したい特徴が読めるかを確認します。似た連写を減らした結果、枚数が少なくなっても、別の条件の画像を追加する方針を立てられます。

確認項目 採用の見方 保留・差し替えにする例
解像度・鮮明さ 学ばせたい顔、線、模様、形が見える 強いぼけや圧縮で特徴がつぶれている。拡大しただけで細部が増えたと思い込む
重複・偏り 他の画像とは違う視点や条件を含む 連写のほぼ同じカットばかり。同じ背景に極端に偏る
切り抜き・構図 目的の特徴が画面内に残っている 全身を学ばせたいのに足が切れる。取っ手の形が隠れる
素材の来歴・利用条件 作成者、入手元、予定用途の確認内容を記録できる 出所や利用条件が不明。人物・素材について必要な確認が済んでいない

同一ファイルの重複は後述のコードで探せますが、微妙にトリミングした同じ写真や、ほぼ同じ連写は目視でも比べます。「重複なし」という機械判定だけで、多様な素材が揃ったことにはなりません。

学習用とは別に、完成後に見比べたい画像を少数、別フォルダへ残しておく方法もあります。ここではそのフォルダを reviewと呼びます。これは目視比較用で、学習ソフトが自動で使う検証データではありません。同じ画像やほぼ同じ連写を両方へ入れないようにします。

キャプションは、共通の呼び名と画像に見える条件を揃える

キャプションは、その画像を説明する短い文章やタグです。この例では、自作マグを呼び出すための文字列を sksmugに揃え、カンマ区切りで見えている内容を続けます。文字列自体に特別な機能があるわけではなく、入力しただけで必ず一つのトークンになるわけでもありません。

次は、同じ青いマグを別の条件で撮影したと仮定した作例です。実際に写っている内容へ置き換えてください。

# mug-001.txt:白い背景、正面からの全体写真
sksmug, ceramic mug, blue glaze, front view, white background

# mug-002.txt:机の上、横からの写真
sksmug, ceramic mug, blue glaze, side view, on a wooden table

# mug-003.txt:手で持ち、斜めからの写真
sksmug, ceramic mug, blue glaze, three-quarter view, held in a hand

上の #行は説明用です。各.txtへ保存するのは、その下のキャプション一行だけです。三枚とも同じ説明を貼り付けるのではなく、角度・背景・持ち方を画像に合わせます。

キャプションの要素 この例の編集ルール
共通の呼び名 sksmugの綴りと先頭位置を揃える
対象の種類・特徴 ceramic mug, blue glazeなど、実物と目的に合う語を使う
変わる条件 角度、背景、服、ポーズなどを画像ごとに書き分ける
見えない情報 隠れた模様、人物の感情や属性などを推測で追加しない
宣伝的な評価語 何となく付いた「最高品質」などを、画像の説明として残す必要があるか見直す

固定したい属性をキャプションから全部消す、逆に必ずすべて書く、という一律の決め方はしません。上の例は色も書くところから始めます。色を変えたい用途なら、別の色の素材を含めるかも検討し、学習後に色を指定して比較します。同じ色だけの素材に色名を書いたからといって、自由な色変更が保証されるわけではありません。

ローカルの画像説明モデルやタグ付けツールを使う場合も、出力は下書きにします。画像と説明を並べる → 誤認識を消す → 共通の呼び名を揃える → 見える条件を補う → UTF-8で保存する、の順で一枚ずつ確認してください。画像を外部サービスへ送るかどうかは、下書きを作る前に決めます。

画像と同名の.txtを置き、TOMLで読む場所を指定する

採用した画像とキャプションを、次のように配置します。画像ファイルは自分の素材を入れてください。mug-001.pngに対応する説明は mug-001.txtです。mug-001.png.txtではありません。

lora-project/
  README.md
  sources.csv
  dataset.toml
  audit_dataset.py
  train/
    mug-001.png
    mug-001.txt
    mug-002.png
    mug-002.txt
    mug-003.png
    mug-003.txt
  review/
    mug-reference-001.png

sd-scriptsの --dataset_configを使う場合、image_dirには画像が直接入っているtrainフォルダを指定します。従来の 10_名前のようなフォルダ方式とは異なり、この設定方式ではフォルダ名から繰り返し回数や呼び名を読み取りません。公式のデータセット設定ガイドに両方式の違いがあります。

次はSDXL向けに解像度を1024とした準備用の設定例です。繰り返し回数1・バッチサイズ1は、素材の読み込みを確かめるための開始値で、最適な学習設定ではありません。image_dirを自分の絶対パスに替えて保存します。

[general]
caption_extension = ".txt"
shuffle_caption = false
flip_aug = false
color_aug = false

[[datasets]]
resolution = 1024
batch_size = 1
enable_bucket = true
bucket_no_upscale = true

[[datasets.subsets]]
image_dir = "/absolute/path/to/lora-project/train"
num_repeats = 1

Windowsでも C:/datasets/lora-project/trainのようにスラッシュで書けます。設定ファイルの置き場所と実行場所が違っても迷いにくいよう、ここでは絶対パスに揃えています。

enable_bucketで縦横比の異なる画像を扱い、bucket_no_upscaleで拡大を避ける設定にしています。すべてがそのまま1024×1024になるという意味ではありません。実際に使われるサイズと切り取り範囲は、学習ソフト側のプレビューや読み込みログで確認します。

初回はキャプションのシャッフルと画像の反転・色変更を無効にし、用意した素材を確認しやすくしています。タグをシャッフルする設定へ変えるなら、先頭の呼び名を残す keep_tokens = 1も検討します。この値は、既定のカンマ区切りで先頭の一項目を残す指定です。言語モデルのトークン数1を指定するものではありません。

設定ファイルを指定すると --train_data_dirなど一部の引数は無視されます。GUIのフォルダ欄を変更したのに別の画像を読んでいる場合は、どのデータセット設定が有効かを確認してください。

学習前に、欠落・同名・重複を読み取りだけで点検する

次のPythonコードを audit_dataset.pyとして保存します。PNG・JPEG・WebPを対象に、同名の.txt、空ファイル、UTF-8、先頭の呼び名、同じ名前の画像、同一ファイルの重複、画像を読み込めるかを点検します。対象フォルダの直下だけを読み、画像やキャプションを書き換えたり、送信したりしません。

この点検は、この記事の「一行・カンマ区切り・先頭に共通の呼び名」という運用に合わせています。sd-scriptsが扱えるキャプション全般を判定するものではありません。画像の意味、似た構図、素材の利用条件も人が確認する部分です。

"""Read-only checks for flat image + UTF-8 .txt pairs; no training or uploads."""
import argparse
import hashlib
import json
from pathlib import Path

from PIL import Image


def audit(folder, trigger):
    root = Path(folder)
    errors, warnings, rows = [], [], []
    if not root.is_dir():
        return {"images": [], "errors": ["directory not found"], "warnings": []}
    files = sorted(root.iterdir())
    images = [p for p in files if p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}]
    if not images:
        errors.append("no supported images directly inside directory")
    stems, hashes = {}, {}
    for p in images:
        if p.is_symlink() or not p.is_file():
            errors.append(f"{p.name}: expected a regular image file")
            continue
        key = p.stem.casefold()
        if key in stems:
            errors.append(f"{p.name}: ambiguous stem with {stems[key]}")
        stems[key] = p.name
        try:
            digest = hashlib.sha256(p.read_bytes()).hexdigest()
            if digest in hashes:
                errors.append(f"{p.name}: identical file to {hashes[digest]}")
            hashes[digest] = p.name
            with Image.open(p) as im:
                im.load()
                rows.append({"file": p.name, "width": im.width, "height": im.height, "sha256": digest})
                if im.mode != "RGB":
                    warnings.append(f"{p.name}: mode {im.mode}; check colors/transparency in loader")
                if im.getexif().get(274, 1) != 1:
                    warnings.append(f"{p.name}: EXIF orientation; check loader preview")
                if getattr(im, "n_frames", 1) > 1:
                    errors.append(f"{p.name}: animated/multiple frames; select a still image")
        except (OSError, ValueError, Image.DecompressionBombError) as exc:
            errors.append(f"{p.name}: image read failed ({type(exc).__name__})")
        caption = p.with_suffix(".txt")
        if caption.is_symlink() or not caption.is_file():
            errors.append(f"{p.name}: missing regular {caption.name}")
            continue
        try:
            text = caption.read_text(encoding="utf-8")
            if not text.strip():
                errors.append(f"{caption.name}: empty caption")
            elif text.strip().split(",", 1)[0].strip() != trigger:
                errors.append(f"{caption.name}: first comma-separated phrase must be {trigger!r}")
            if "\ufeff" in text:
                errors.append(f"{caption.name}: remove UTF-8 BOM")
            if len(text.strip().splitlines()) > 1:
                errors.append(f"{caption.name}: use one line in this example")
        except (OSError, UnicodeError):
            errors.append(f"{caption.name}: cannot read UTF-8 caption")
    expected = {p.with_suffix(".txt").name for p in images}
    for p in files:
        if p.suffix.lower() == ".txt" and p.name not in expected:
            errors.append(f"{p.name}: caption without matching image")
        elif p.is_dir():
            warnings.append(f"{p.name}: subdirectory not scanned")
    return {"images": rows, "errors": errors, "warnings": warnings}


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("folder")
    parser.add_argument("--trigger", required=True)
    args = parser.parse_args()
    if not args.trigger.strip() or "," in args.trigger or "\n" in args.trigger:
        parser.error("trigger must be one nonempty comma-separated phrase")
    report = audit(args.folder, args.trigger.strip())
    print(json.dumps(report, ensure_ascii=False, indent=2))
    raise SystemExit(1 if report["errors"] else 0)

Pythonの仮想環境にPillowを入れ、lora-projectで実行します。macOS・Linuxの例です。本文のコードはPython 3.14.6/Pillow 12.3.0で確認しています。

python3 -m venv .venv
.venv/bin/python -m pip install Pillow==12.3.0
.venv/bin/python audit_dataset.py train --trigger sksmug

Windowsでは py -m venv .venvで作成し、Pythonの実行パスを .venv\Scripts\python.exeに替えます。imagesには読み込めた画像のサイズとSHA-256、errorsには修正が必要な項目、warningsには目視で確認する項目が出ます。エラーがあれば終了コード1、なければ0です。警告が残っていないかも読んでください。

同じ綴りで mug-001.pngと mug-001.jpgがある場合は、どちらも同じ.txtを参照してしまいます。どちらを使うか選ぶか、別の名前とキャプションへ分けます。また、学習ソフトがキャプション欠落時に必ず停止するとは限らないため、開始前に欠落を探しておく意味があります。

このコードは正常な画像に加え、キャプション欠落・空欄・同名衝突・完全重複・壊れた画像など10ケースで検証しました。検証画像は構造を確かめるための単色画像で、学習データとしての品質評価やGPU学習は行っていません。

来歴と確認結果を残して、学習ソフトへ引き渡す

最後に、採用した素材の来歴を sources.csvなどへ記録します。次の行は記入例であり、実際に確認した人・条件へ書き換えます。第三者の素材を含む場合は、その素材ごとの確認内容も残してください。

file,creator,source,planned_use,permission_record,checked_by,checked_at,notes
mug-001.png,自分,自作マグの撮影原本,学習と生成画像公開,確認記録の保存先,確認者名,YYYY-MM-DD,正面の形を確認
mug-002.png,自分,自作マグの撮影原本,学習と生成画像公開,確認記録の保存先,確認者名,YYYY-MM-DD,取っ手が見える横向き

READMEには、目的、基盤モデル、学習ソフトの版、共通の呼び名、キャプション方針、採用枚数、保留理由、比較用画像の場所を残します。素材の利用確認と基盤モデルの利用条件は、別々の記録にすると追いやすくなります。

  • 学ぶ対象が一文で説明でき、背景や角度の偏りを確認した。
  • 全画像と.txtが対応し、キャプションを画像と並べて読んだ。
  • 点検コードのエラーを解消し、警告の内容も確認した。
  • TOMLが実在するtrainフォルダを指し、reviewを学習対象へ入れていない。
  • 素材・基盤モデルの来歴と、予定用途の確認内容を残した。

この段階で、学習ソフトのデータセット読み込みへ進めます。実行時は、画像枚数、読まれたキャプション、繰り返し回数、バケットと切り取りを確認します。SDXLの学習手順など、使うモデルに対応するスクリプトの案内へ引き継いでください。学習済みLoRAを読み込む段階は、ComfyUIでLoRAを使う方法で扱っています。

スポンサーリンク