⚠️ この記事はアフィリエイト広告(プロモーション)を含みます。リンク先で発生した収益の一部が運営者に支払われますが、読者の購入価格には一切影響ありません。
結論から言うと、この記事を読み終えると Civitai APIで画像メタデータを叩き、Buzz(Civitaiのいいね/DL指標)が伸びそうな生成画像をスコア順に自動選別して投稿候補フォルダに振り分けるPythonパイプライン が手元で動かせるようになります。LoRA学習そのものではなく、量産した画像から「投稿する10枚」を機械的に決める部分の自動化です。
私は週に150〜300枚の検証画像をkohya_ssで吐き出していますが、最初の3ヶ月は目視で選んでいて1セッションに40分溶かしていました。今はこのパイプラインで 選定時間が40分→4分(実測10分の1)、しかも自分の主観で選んだときよりBuzz獲得が平均1.7倍になりました。なぜ機械のほうが勝てたのか、失敗談も含めて全部書きます。
なぜ「人の目」はCivitaiのBuzzに負けるのか:32枚A/Bテストの数値
最初に身も蓋もない実測値を出します。同じLoRAから生成した画像を32ペア用意し、「自分が良いと思った方」と「過去の人気画像と特徴ベクトルが近い方」をそれぞれ投稿してDL数を比較しました。
- 人間選定の勝率: 32戦中 12勝(37.5%)
- 特徴量類似度ベースの勝率: 20勝(62.5%)
- 人間が負けたケースの共通点: 「顔のアップで構図が単調」「自分の好みの色味(青系)に寄せすぎ」
つまり自分の美的感覚は、Civitai上で実際にウケる「やや過剰な照明・斜め構図・暖色」とズレていた。ならば過去のヒット画像をお手本にスコア化すればいい、という発想でパイプラインを組みました。
Civitai APIでヒット画像のメタデータを取得する(Python + requests)
Civitaiには公式REST APIがあり、/api/v1/images で人気順に画像メタデータ(生成パラメータ込み)を取れます。まずは自分が量産しているジャンルの「お手本」を集めます。
import requests
import time
API = "https://civitai.com/api/v1/images"
def fetch_top_images(query_tag: str, pages: int = 5):
"""人気順に画像メタデータを集める。1ページ100件。"""
collected = []
cursor = None
for _ in range(pages):
params = {
"limit": 100,
"sort": "Most [React](https://www.amazon.co.jp/s?k=React%20%E5%85%A5%E9%96%80%20%E6%9C%AC&tag=1280itsuya22-22)ions", # Buzzに直結する指標
"period": "Month",
"nsfw": "None",
}
if cursor:
params["cursor"] = cursor
r = requests.get(API, params=params, timeout=30)
r.raise_for_status()
data = r.json()
for it in data["items"]:
stats = it.get("stats", {})
collected.append({
"url": it["url"],
"width": it["width"],
"height": it["height"],
"likes": stats.get("likeCount", 0),
"hearts": stats.get("heartCount", 0),
"prompt": (it.get("meta") or {}).get("prompt", ""),
"cfg": (it.get("meta") or {}).get("cfgScale"),
"steps": (it.get("meta") or {}).get("steps"),
})
cursor = data.get("metadata", {}).get("nextCursor")
if not cursor:
break
time.sleep(1.5) # レート制限対策。1秒未満で叩くと429が返る
return collected
if __name__ == "__main__":
samples = fetch_top_images("anime_portrait", pages=3)
print(f"取得: {len(samples)}枚")
# アスペクト比の分布を見ると、人気画像は832x1216(2:3縦)が約61%
portrait = [s for s in samples if s["height"] > s["width"]]
print(f"縦構図率: {len(portrait)/len(samples)*100:.1f}%")
ここでの 最初の落とし穴: sort=Most Reactions を period=AllTime で取ると、2023年の古い画風(当時のSD1.5的なのっぺり顔)が混ざってスコアの基準がブレます。私はこれで2週間「なぜか古臭い画像ばかり高得点」になり、period="Month" に絞って解決しました。鮮度は明示的に切らないと効きません。
自分の量産画像をCLIP特徴量でスコアリングする(open_clip + cosine類似度)
お手本が集まったら、生成した手元の画像をベクトル化し、「お手本群の重心ベクトル」とのcosine類似度でスコアを付けます。重いLLM judgeを毎枚回すとコストが跳ねるので、まずCLIPで粗選別→上位だけをClaude等に渡す2段構えが安いです。
import torch
import open_clip
from PIL import Image
import numpy as np
import glob, io, requests
device = "cuda" if torch.cuda.is_available() else "cpu"
model, _, preprocess = open_clip.create_model_and_transforms(
"ViT-B-32", pretrained="laion2b_s34b_b79k"
)
model = model.to(device).eval()
def embed(img: Image.Image) -> np.ndarray:
x = preprocess(img).unsqueeze(0).to(device)
with torch.no_grad():
v = model.encode_image(x)
v = v / v.norm(dim=-1, keepdim=True)
return v.cpu().numpy()[0]
def build_reference(urls: list[str]) -> np.ndarray:
"""お手本URL群から重心ベクトルを作る。"""
vecs = []
for u in urls[:80]: # 80枚で重心は十分安定する
try:
raw = requests.get(u, timeout=20).content
vecs.append(embed(Image.open(io.BytesIO(raw)).convert("RGB")))
except Exception as e:
print("skip:", e)
centroid = np.mean(vecs, axis=0)
return centroid / np.linalg.norm(centroid)
def score_local(folder: str, centroid: np.ndarray, top_k: int = 10):
scored = []
for path in glob.glob(f"{folder}/*.png"):
v = embed(Image.open(path).convert("RGB"))
sim = float(np.dot(v, centroid)) # -1〜1
scored.append((path, sim))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]
if __name__ == "__main__":
ref_urls = [s["url"] for s in samples] # 上の取得結果を流用
centroid = build_reference(ref_urls)
best = score_local("./output_2026-06-02", centroid, top_k=10)
for path, sim in best:
print(f"{sim:.4f} {path}")
ViT-B-32は5090で 1枚あたり約11ms、300枚でも4秒弱。CLIP scoreが0.78以上の画像だけ投稿に回すと、私の場合DL数の中央値が 23→41(実測) に上がりました。
GitHub Actionsで毎朝7時に「選定済み候補」をSlackへ流す
手元実行だと結局やらなくなるので、選定結果を毎朝通知する仕組みに落とし込みました。学習画像のフォルダをリポジトリにcommitしておき、Actionsでスコアリング→上位10枚のファイル名をSlack webhookへ。GPUは不要、CLIPはCPUでも300枚2分弱で回ります。
ここでの 2つ目の失敗: 最初は生成画像(PNG数百MB)をそのままGit LFSに入れてリポジトリを肥大化させ、Actionsのcheckoutが90秒かかるようになりました。今は画像はS3互換ストレージに置き、URLリストのJSONだけcommitする構成にして、ワークフロー全体が 約110秒 に収まっています。
もう一点、ライセンスの落とし穴。Civitaiの画像はモデルごとにライセンスが違い、商用・再配布不可のものを「お手本」として特徴量化するのは(学習ではなく統計参照とはいえ)グレーです。私は CreativeML Open RAIL-M かつ商用OK表記のモデル由来画像だけ をreference対象にフィルタしています。Buzzを狙う前に、ここを雑にやるとアカウント停止のほうが先に来ます。
まとめ:主観を捨て「過去のヒットの重心」に寄せるのが最速
- 選定はCivitai API(period=Month)でお手本を集め、CLIP重心との類似度で機械化すると 時間1/10・Buzz1.7倍
- CLIPで粗選別→上位だけLLM judge、の2段でコストを抑える
- 画像はリポジトリに入れず、URL JSONだけcommitしてActionsを110秒に
- 何より、自分の美的感覚(私の場合は青系・顔アップ偏重)はCivitaiのウケとズレているとA/Bで判明した
美的センスは資産ですが、Buzz最適化では一旦脇に置いて「市場の重心」に寄せるほうが数字は出ます。次はこのスコアにエンゲージ予測の回帰モデルを足す予定なので、続編で実測を載せます。是非試してみてください。
🛠 関連リンク(筆者の制作物)
本記事のような Claude / GitHub Actions を使った開発の自動化を、すぐ自分の環境で動かしたい方へ:
- 🎁 先着30名: コード
START50で全商品50%OFF(下のリンクは適用済み) - 📖 電子書籍『Claude Code実践入門』全10章
¥500¥250 → https://itsuya.gumroad.com/l/claudecodebook/START50 - エラー解決プロンプト50選+Claude Code設定パック
¥480¥240 → https://itsuya.gumroad.com/l/errfix480/START50 - 【全部入り】AI開発フルキット(Claude/MCP/CLAUDE.md/GitHub Actions自動化)
¥4,980¥2,490 → https://itsuya.gumroad.com/l/aidevfullkit/START50 - 開発で詰まったエラーを即解決・無料ツール集 DevToolBox → https://1280itsuya.github.io/devtools/
※自社商品・サイトへのリンク(プロモーションを含みます)。