「自分の在宅ワークのデスク環境、客観的に見て何点なんだろう?」——この疑問に答えるために、デスクの写真を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つ:
- pos/negの2択にして softmax:絶対的な類似度は環境差が大きいので、「良い/悪いどちらに近いか」の相対比較にすると安定する
-
*100でスケール:そのままだと差が小さく全部50点付近に寄るので、スケールして鋭くする - 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のピンだけ注意)
- 厳密さより「おおまかな目安+次の一手」を返す設計にすると体感が良い
同じ発想で「部屋の片付き度」「コーデの○○度」みたいなゆるい採点ツールは色々作れると思います。よかったら自分のデスクで試してみてください👇