個人開発を2014年から続けていると、ある時期から「手作業の限界」に気づきます。私が運営する壁紙アプリも、累計5,000万ダウンロードを超えたあたりで、画像の管理コストが無視できない規模になっていました。
数百枚の新しい壁紙を追加するたびに、カテゴリ(「自然」「都市」「抽象」「動物」など)を手動でタグ付けしていたのですが、これが地味に時間を奪っていました。1枚あたり10〜15秒としても、500枚で約2時間。月に複数回アップデートするアプリで、この作業が続くことへの疲弊感は本物でした。
Claude Vision API が使えるようになった頃、「これで壁紙の自動分類ができるかもしれない」と試してみたのが今回の話です。結論を先に言うと、精度は想定より高く、コストは想定より低く、落とし穴は想定通りあった、という感じです。
なぜ既存の画像分類ではダメだったのか
試みは Claude Vision が最初ではありませんでした。Google Cloud Vision API も試しましたし、Apple の Vision フレームワークも組み込んだことがあります。
問題は「精度の方向性」でした。既存の画像認識モデルは、「これは犬の写真」「これは建物」のような客観的なラベリングは得意です。でも、壁紙アプリが必要とするのは、ユーザーが探す際の気持ちに合ったカテゴリです。
たとえば夕焼けの都市風景の写真。Google Vision は「sky」「sunset」「building」と返してきます。でもアプリの中では「都市夜景・夕暮れ」というカテゴリに入れたいし、ユーザーも「夕暮れっぽい壁紙」を探してそこに辿り着く。この「人間の感覚に合ったカテゴリ判断」が、従来の分類モデルには難しかったのです。
Claude は言語モデルとしての文脈理解があるので、「このアプリのカテゴリ体系に合わせて分類してください」という指示が通るのではないか、と仮説を立てました。
実装:バッチ処理スクリプトの基本構成
実際に書いたスクリプトの骨格です。本番環境で動かしているものを整理・簡略化したものです。
import anthropic
import base64
import json
from pathlib import Path
client = anthropic.Anthropic(api_key="YOUR_API_KEY")
# アプリで使用するカテゴリ定義(実際のカテゴリ体系に合わせる)
CATEGORIES = [
"自然・風景", "都市・建築", "抽象・パターン",
"動物・生き物", "花・植物", "海・水辺",
"夜景・夕暮れ", "宇宙・天体", "季節・行事",
"シンプル・ミニマル"
]
def classify_wallpaper(image_path: str) -> dict:
"""1枚の壁紙画像をカテゴリ分類する"""
with open(image_path, "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
# 拡張子から media_type を判定
ext = Path(image_path).suffix.lower()
media_type_map = {".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".webp": "image/webp"}
media_type = media_type_map.get(ext, "image/jpeg")
response = client.messages.create(
model="claude-haiku-4-5-20251001", # コスト重視でHaikuを使用
max_tokens=256,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": image_data
}
},
{
"type": "text",
"text": f"""この画像は壁紙アプリ用の画像です。
以下のカテゴリから最も適切なものを1つ選び、JSON形式で回答してください。
カテゴリ: {json.dumps(CATEGORIES, ensure_ascii=False)}
回答形式:
{{"category": "カテゴリ名", "confidence": 0.0〜1.0, "reason": "選んだ理由を1文で"}}
他の形式での回答は不要です。"""
}
]
}]
)
result_text = response.content[0].text.strip()
return json.loads(result_text)
def batch_classify(image_dir: str, output_file: str):
"""ディレクトリ内の全画像を分類してJSONで保存する"""
image_files = list(Path(image_dir).glob("*.{jpg,jpeg,png,webp}"))
results = {}
for i, img_path in enumerate(image_files):
print(f"[{i+1}/{len(image_files)}] {img_path.name}")
try:
result = classify_wallpaper(str(img_path))
results[img_path.name] = result
except Exception as e:
print(f" ❌ エラー: {e}")
results[img_path.name] = {"category": "未分類", "error": str(e)}
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n✅ 完了: {len(results)}件を {output_file} に保存")使うモデルの選択:SonnetかHaikuか
ここで実際に悩みました。精度を取るなら Sonnet、コストを取るなら Haiku、という判断です。
壁紙500枚をバッチ処理する場合、コスト試算は次の通りでした(2026年5月時点の参考値)。
Claude Haiku 4.5 を使った場合:
- 入力: 画像1枚あたり約1,000〜1,500トークン相当 + テキスト約200トークン
- 出力: 約100トークン
- 500枚処理での概算コスト: $0.5〜$1.0
Claude Sonnet 4.5 を使った場合:
- 同条件で概算コスト: $3〜$6
月に500枚追加する運用で毎月の差は5ドル前後。絶対額で見れば大きくありませんが、個人開発のコスト感覚としては「精度の差が許容できるなら Haiku で十分」という判断になりました。
実際に両モデルで100枚ずつ試したところ、カテゴリ一致率の差は約5%(Sonnet: 94%、Haiku: 89%)でした。残り11%の Haiku の誤分類も、「致命的に間違い」というより「この壁紙なら都市でも自然でも正直どちらでもいい」という境界ケースがほとんどで、アプリ運営上は問題ない範囲でした。
Claude Vision の認識精度を改善する前処理の手法も参考になります。画像の解像度やトリミングで精度が変わる点は、壁紙のような高解像度アセットでも適用できる知見です。
実際の分類精度:75枚の手動検証結果
500枚を処理した後、ランダムに75枚を抜き出して手動で正解カテゴリを付け直し、Claude の分類結果と比較しました。
結果:
- 完全一致: 66枚(88%)
- 許容範囲の違い(夜景と都市など): 7枚(9.3%)
- 明らかな誤分類: 2枚(2.7%)
2枚の誤分類の内訳:
- 深い森の中の小道 → 「抽象・パターン」と分類(実際は「自然・風景」)
- 宇宙背景に人物シルエット → 「宇宙・天体」と分類(実際は「抽象・パターン」寄り)
誤分類ケースを見ると、どちらも「確かにそう見える」内容で、人間でも迷う画像でした。2.7% の誤分類率は、手作業の疲れから来る凡ミスと比べても遜色ない水準だと判断しています。
実運用で踏んだ3つの落とし穴
1. JSON パースエラーが思ったより多い
Claude は基本的に JSON を正確に返してくれますが、稀に余計な説明文が混入することがあります。
# 対策:JSON部分だけを抽出する
import re
def safe_parse_json(text: str) -> dict:
"""レスポンスからJSON部分を安全に抽出する"""
# コードブロック内のJSONを試みる
code_match = re.search(r'```(?:json)?\n(.*?)\n```', text, re.DOTALL)
if code_match:
return json.loads(code_match.group(1))
# 通常のJSONオブジェクトを試みる
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if json_match:
return json.loads(json_match.group())
raise ValueError(f"JSONが見つかりません: {text[:100]}")構造化出力(Structured Outputs)を使えばこの問題はほぼ回避できます。ただし壁紙分類のような軽量タスクでは、Haiku + プロンプト設計で対応する方がシンプルでした。
2. レート制限との衝突
500枚を一気に処理しようとすると、Tier によってはレート制限に引っかかります。バッチ処理には必ず時間あたりのリクエスト数を制御する仕組みが必要です。
import time
def batch_classify_with_rate_limit(image_files: list, max_per_minute: int = 40):
"""レート制限を考慮したバッチ処理"""
results = {}
interval = 60.0 / max_per_minute # リクエスト間隔(秒)
for i, img_path in enumerate(image_files):
start = time.time()
result = classify_wallpaper(str(img_path))
results[str(img_path)] = result
elapsed = time.time() - start
sleep_time = max(0, interval - elapsed)
if sleep_time > 0:
time.sleep(sleep_time)
if (i + 1) % 50 == 0:
print(f" → {i+1}件処理済み")
return resultsレート制限の詳細な扱い方については、API レート制限とベストプラクティスで整理されています。
3. 画像サイズが大きいと処理が遅い
壁紙画像は解像度が高い(3,000×5,000px 以上)ものが多く、base64 エンコードするとデータ量が数 MB になります。API への送信に時間がかかり、バッチ処理全体が遅くなりました。
対策として、分類用には長辺 512px にリサイズしたサムネイルを使うようにしました。カテゴリ判断に高解像度は不要で、処理時間は約1/3 になりました。
from PIL import Image
import io
def resize_for_classification(image_path: str, max_size: int = 512) -> bytes:
"""分類用に画像をリサイズする(アスペクト比保持)"""
with Image.open(image_path) as img:
img.thumbnail((max_size, max_size), Image.LANCZOS)
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
return buffer.getvalue()実体験から得たコスト感覚
2014年からアプリ開発を続けてきた感覚として、自動化ツールの価値は「絶対コスト」より「機会コスト」で測る方が正確だと思っています。
月に2〜3ドルかかる API コストよりも、月に4〜6時間の手作業コストの方が遥かに重い。個人開発者の時間は何よりも有限なリソースです。累計5,000万ダウンロードを超えるアプリを1人で維持しながら新機能を開発し、さらにブログや新プロジェクトに時間を割くには、こういう「小さな自動化の積み重ね」が生命線になります。
Claude Vision API による壁紙分類の自動化は、月額数ドルの投資で毎月数時間を取り戻してくれています。計算すると、自分の時給換算での費用対効果は数十倍以上。個人開発者にとってこれ以上の投資はなかなかありません。
次のステップ:人間レビューと組み合わせた仕組みへ
完全自動化より、「AI で8割分類 → 人間が2割だけ確認」のハイブリッド方式が現実的です。
confidence スコアが 0.8 以下のものだけ人間がチェックする運用にしたところ、確認件数は全体の約15%に絞られました。残り85%はそのまま使える品質になっています。
もし壁紙アプリ以外の用途でも Claude Vision を使った分類を試みるなら、まずカテゴリ定義を丁寧に言語化することを強くお勧めします。「自然・風景」より「森・山・空など自然物が主役の構図」と書く方が、Claude が判断しやすく精度も上がりました。
この実装パターンは、EC サイトの商品画像分類や、写真管理ツールのタグ付けにも応用できると思います。試せる環境があれば、まず 50 枚程度でプロトタイプを動かしてみてください。費用は数十円以内で収まります。