0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CLIPで「デスクの写真をAIが100点満点で採点」するWebツールをHugging Face無料枠で作った

0
Posted at

「自分の在宅ワークのデスク環境、客観的に見て何点なんだろう?」——この疑問に答えるために、デスクの写真を1枚アップすると、AIが椅子・モニター・照明など8項目を100点満点で採点するWebツールを作りました。

👉 実際に動くもの: https://thedeskovery.com/desk-score/

この記事では、学習なし(ゼロショット)のCLIPで“それっぽい採点”をどう実現したか、そしてHugging Faceの無料枠だけで完結させた構成とハマりどころを共有します。「画像をなんとなく分類したい」「無料でAI推論APIを公開したい」人の参考になれば。

全体構成

[ブラウザ] 写真アップ
   │  fetch (multipart/form-data)
   ▼
[Hugging Face Space (Docker)]
   FastAPI + CLIP (openai/clip-vit-base-patch32, CPU)
   │  8項目を pos/neg プロンプトで採点
   ▼
   JSON { overall, rank, categories[], weakest } を返す
  • フロント:WordPress固定ページに素のJavaScript(採点UI+結果表示)
  • バックエンド:Hugging Face Spaces の Docker SDK(無料・CPU)に FastAPI を載せる
  • モデル:CLIP(zero-shot)。学習データもファインチューニングも無し

CLIPでどう「採点」するか

CLIPは画像とテキストを同じ空間に埋め込めるので、「対になる状態」のテキストを用意して、写真がどちらに近いかを測るだけで“それっぽい採点”ができます。

たとえば「椅子」カテゴリならこう:

CATS = [
  {"key":"chair", "label":"椅子", "weight":20,
   "pos":"a high-end ergonomic office chair with a high backrest and armrests",
   "neg":"a plain simple dining chair or a stool"},
  {"key":"monitor", "label":"モニター", "weight":20,
   "pos":"a large external computer monitor on the desk",
   "neg":"only a small laptop screen with no external monitor"},
  # ... 照明 / 姿勢 / 配線 / 快適性 / 会議環境 など全8項目
]

各カテゴリで pos(良い状態)と neg(悪い状態)の2文を用意し、画像との類似度を比較 → softmax で「posに近い度合い」を0〜100点に変換します。

import torch
from transformers import CLIPModel, CLIPProcessor

model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").eval()
proc  = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

def score_image(img):
    texts = []
    for c in CATS:
        texts += [c["pos"], c["neg"]]

    # サブコンポーネントで個別にtensor化(結合__call__でこける環境があったため)
    text_inputs  = proc.tokenizer(texts, return_tensors="pt", padding=True, truncation=True)
    image_inputs = proc.image_processor(images=img.resize((224, 224)), return_tensors="pt")

    with torch.no_grad():
        tf  = model.get_text_features(**text_inputs)
        imf = model.get_image_features(pixel_values=image_inputs["pixel_values"])

    # 正規化してコサイン類似度
    tf  = tf  / tf.norm(dim=-1, keepdim=True)
    imf = imf / imf.norm(dim=-1, keepdim=True)
    sims = (imf @ tf.T)[0]   # [len(texts)]

    cats, total, wsum = [], 0, 0
    for i, c in enumerate(CATS):
        pair = torch.stack([sims[2*i], sims[2*i+1]]) * 100.0   # 差を鋭くするスケール
        p = torch.softmax(pair, dim=0)[0].item()               # 0..1: posに近い度合い
        s = max(5, min(98, round(p * 100)))                    # 0/100の振り切れを少し丸める
        cats.append({"key": c["key"], "label": c["label"], "score": s, "weight": c["weight"]})
        total += s * c["weight"]; wsum += c["weight"]

    overall = round(total / wsum)
    weak = sorted(cats, key=lambda x: x["score"])[0]
    return {"overall": overall, "rank": rank_of(overall), "categories": cats, "weakest": weak["key"]}

ポイントは3つ:

  1. pos/negの2択にして softmax:絶対的な類似度は環境差が大きいので、「良い/悪いどちらに近いか」の相対比較にすると安定する
  2. *100 でスケール:そのままだと差が小さく全部50点付近に寄るので、スケールして鋭くする
  3. 5〜98でクリップ:0点/100点の振り切れは体感に合わないので軽く丸める

“厳密なスコアリング”ではなく、「おおまかな目安」として納得感のある点数を返すのが狙いです。重み付き平均で総合点+S/A/B/C/Dランク、最弱カテゴリ(=伸びしろ)も返しています。

FastAPI側

from fastapi import FastAPI, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image
import io

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

@app.on_event("startup")
def _preload():
    get_model()  # 起動時にモデルをロード→初回採点のコールドスタートを短縮

@app.post("/score")
async def score(file: UploadFile = File(...)):
    data = await file.read()
    img = Image.open(io.BytesIO(data)).convert("RGB")
    return score_image(img)

CORSは全許可(ブラウザから直接叩くため)。startupでモデルをプリロードしておくと、無料枠がスリープから復帰した後の初回採点が体感速くなります。

Hugging Face 無料枠デプロイのハマりどころ

① numpy 2.0 で transformers 4.40 が壊れる

これが一番時間を溶かしました。requirements.txt を素直に書くと numpy 2.x が入り、テンソル生成まわりで実行時エラーになります。解決は numpyを1系にピン

numpy==1.26.4   ← これを1行目に
fastapi==0.110.0
uvicorn[standard]==0.29.0
transformers==4.40.0
torch==2.2.2
pillow==10.3.0
python-multipart==0.0.9

② Dockerfileでモデルを事前DL

ビルド時にモデルを取得しておくと、起動が安定します。

FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN python -c "from transformers import CLIPModel, CLIPProcessor; \
  CLIPModel.from_pretrained('openai/clip-vit-base-patch32'); \
  CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')"
COPY . .
EXPOSE 7860
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]

README.md のフロントマターで sdk: docker / app_port: 7860 を指定すればOK。torchを毎ビルド入れ直すのでビルドは5〜10分かかります。

③ 無料枠はスリープする

一定時間アクセスがないとスリープし、初回リクエストでコールドスタート(コンテナ起動+モデルロード)が走ります。割り切って、フロント側で「AI起動中…」を出す or ページを開いた瞬間にヘルスチェックを叩いてウォームアップする、で体感をカバーしています。

実際の精度感

  • きれいなデュアルモニター+良い椅子のデスク → 高得点(S〜Aランク)
  • ノートPCのみ・物が多い机 → 低めでD寄り
  • 「一番の伸びしろ」も概ね妥当(暗い写真→照明、ケーブルだらけ→配線)

照明や撮影角度で点数は揺れますが、“今の自分のデスクの立ち位置をざっくり知る”用途には十分ハマっています。学習ゼロ・無料でここまで動くのはCLIPの強さですね。

まとめ

  • CLIPのゼロショットpos/negのsoftmaxで、学習なしでも“納得感のある採点”ができる
  • **Hugging Face無料枠(Docker Space)**でAI推論APIを公開できる(numpyのピンだけ注意)
  • 厳密さより「おおまかな目安+次の一手」を返す設計にすると体感が良い

同じ発想で「部屋の片付き度」「コーデの○○度」みたいなゆるい採点ツールは色々作れると思います。よかったら自分のデスクで試してみてください👇

👉 https://thedeskovery.com/desk-score/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?