Image Classification
Transformers
Safetensors
qwen3_5
feature-extraction
clef
bitsandbytes
quantized
4-bit precision
Instructions to use Aikimi/clef-nf4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Aikimi/clef-nf4 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-classification", model="Aikimi/clef-nf4") pipe("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/hub/parrots.png")# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModel processor = AutoProcessor.from_pretrained("Aikimi/clef-nf4") model = AutoModel.from_pretrained("Aikimi/clef-nf4", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 16,026 Bytes
aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 8296a67 aa77bd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | """Small, torch-free contracts for images, schemas and immutable decision runs."""
from __future__ import annotations
import copy
import csv
import hashlib
import io
import json
import math
import os
from collections import defaultdict, deque
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
RUNTIME = ROOT / "models" / "Clef"
OUTPUTS = ROOT / "outputs" / "clef"
PREPROCESSING_VERSION = "exif-rgb-size-v2"
MODELS = {
"clef-flash": {"repo": "Cloudflare/clef-flash", "revision": "17f0b0ad64efb65d273590632833508766b2aae6"},
"clef": {"repo": "Cloudflare/clef", "revision": "2f3de3dd85f379784083b0814d997ab627200f0c"},
}
PROFILES = {
"flash-int8": {
"label": "Flash INT8 · 軽量",
"model": "clef-flash",
"precision": "int8",
"cpu_embeddings": False,
"min_vram_gib": 12,
},
"clef-24gb": {
"label": "Clef NF4 · 24GB",
"model": "clef",
"precision": "nf4",
"cpu_embeddings": False,
"min_vram_gib": 20,
},
"clef-16gb": {
"label": "Clef NF4 · 16GB",
"model": "clef",
"precision": "nf4",
"cpu_embeddings": True,
"min_vram_gib": 14,
},
"flash-bf16": {
"label": "Flash BF16 · 比較用",
"model": "clef-flash",
"precision": "bf16",
"cpu_embeddings": False,
"min_vram_gib": 20,
},
}
TEMPLATES = {
"顔・手の見直し(参考)": {
"style": {
"type": "choice",
"instructions": "人物の主な写り方を分類してください。",
"criteria": {
"closeup": "顔のアップ",
"upper": "上半身中心",
"full": "全身または足まで見える",
"other": "人物なし・その他",
},
},
"face": {"type": "noul", "instructions": "人物の顔に、目や口の数・位置の異常や崩れがある。"},
"hands": {"type": "noul", "instructions": "見えている手や指に、本数や形の異常がある。"},
"marks": {"type": "noul", "instructions": "画像に文字、ロゴ、署名、透かしのいずれかがある。"},
"background": {
"type": "choice",
"instructions": "背景を分類してください。",
"criteria": {
"simple": "単色・単純な背景",
"indoor": "屋内の背景",
"outdoor": "屋外の背景",
"other": "その他",
},
},
},
"画像の評価": {
"style": {
"type": "choice",
"instructions": "画像の主な表現形式を分類してください。",
"criteria": {
"photo": "写真または写実的な画像",
"illustration": "イラスト・アニメ・絵画",
"design": "図・ポスター・グラフィックデザイン",
},
},
"quality": {
"type": "score",
"instructions": "画像の見た目の完成度を評価してください。",
"criteria": [
"大きな破綻や欠落がある",
"目立つ問題があり修正が必要",
"おおむね整っている",
"細部まで整い完成度が高い",
],
},
"text": {"type": "noul", "instructions": "画像に読める文字が含まれている。"},
},
"生成画像の確認": {
"artifact": {"type": "noul", "instructions": "画像に不自然な形、重複、形状の破綻が見られる。"},
"composition": {
"type": "score",
"instructions": "構図の明瞭さと主題の伝わりやすさを評価してください。",
"criteria": ["主題が不明瞭", "主題は分かるが構図に問題がある", "主題が明瞭で構図が整っている"],
},
},
"文章・JSONの分類": {
"department": {
"type": "choice",
"instructions": "問い合わせを担当するチームはどれですか。",
"criteria": {"billing": "請求や支払い", "technical": "技術的な問題や障害", "other": "その他"},
},
"outage": {"type": "noul", "instructions": "サービスが停止している。"},
},
}
TEMPLATES["人物イラストの仕分け"] = {
key: copy.deepcopy(TEMPLATES["顔・手の見直し(参考)"][key]) for key in ("style", "marks", "background")
}
class ClefError(ValueError):
"""An input or installation problem to show directly in the Studio."""
def atomic_json(path, data):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False), encoding="utf-8")
os.replace(temporary, path)
def sha256(path):
digest = hashlib.sha256()
with Path(path).open("rb") as stream:
for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def canonical_hash(value):
return hashlib.sha256(
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
).hexdigest()
def _unique(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ClefError(f"IDが重複しています: {key}")
result[key] = value
return result
def parse_schema(value):
if isinstance(value, str):
if len(value) > 100_000:
raise ClefError("判断項目のJSONは100,000文字以内にしてください。")
try:
value = json.loads(value, object_pairs_hook=_unique)
except json.JSONDecodeError as exc:
raise ClefError(f"JSONの{exc.lineno}行目・{exc.colno}文字目を確認してください。") from exc
if not isinstance(value, dict) or not 1 <= len(value) <= 32:
raise ClefError("判断項目は1〜32件のオブジェクトにしてください。")
for qid, question in value.items():
if not isinstance(qid, str) or not qid.strip() or len(qid) > 80:
raise ClefError("項目IDは1〜80文字にしてください。")
if not isinstance(question, dict) or question.get("type") not in {"choice", "score", "noul"}:
raise ClefError(f"{qid}: 種類はchoice / score / noulを指定してください。")
if set(question) - {"type", "instructions", "criteria"}:
raise ClefError(f"{qid}: 未対応の設定があります。")
if not isinstance(question.get("instructions", ""), str):
raise ClefError(f"{qid}: 判断することは文章で指定してください。")
criteria, kind = question.get("criteria"), question["type"]
if kind == "choice":
if not isinstance(criteria, dict) or not 2 <= len(criteria) <= 64:
raise ClefError(f"{qid}: 選択肢はIDと説明の組を2〜64件指定してください。")
if any(
not isinstance(k, str) or not k.strip() or not isinstance(v, str) or not v.strip()
for k, v in criteria.items()
):
raise ClefError(f"{qid}: 選択肢のIDと説明を入力してください。")
elif kind == "score":
if (
not isinstance(criteria, list)
or not 2 <= len(criteria) <= 32
or any(not isinstance(x, str) or not x.strip() for x in criteria)
):
raise ClefError(f"{qid}: 段階の説明を低い順に2〜32件指定してください。")
elif criteria is not None and (
not isinstance(criteria, dict)
or set(criteria) - {"true", "false"}
or any(not isinstance(x, str) for x in criteria.values())
):
raise ClefError(f"{qid}: 真偽の説明はtrue / falseで指定してください。")
return copy.deepcopy(value)
def edit_question(schema, previous, qid, kind, instructions, options):
result = copy.deepcopy(schema)
qid = (qid or "").strip()
if qid in result and qid != previous:
raise ClefError(f"IDが重複しています: {qid}")
question = {"type": kind, "instructions": (instructions or "").strip()}
lines = [x.strip() for x in (options or "").splitlines() if x.strip()]
if kind == "choice":
pairs = []
for line in lines:
if ":" not in line:
raise ClefError("選択肢は1行に「ID: 説明」の形で指定してください。")
key, description = line.split(":", 1)
pairs.append((key.strip(), description.strip()))
question["criteria"] = _unique(pairs)
elif kind == "score":
question["criteria"] = lines
if previous and previous != qid:
result.pop(previous, None)
result[qid] = question
return parse_schema(result)
def integer(value, label, lower, upper):
try:
number = int(value)
if (
isinstance(value, bool)
or not math.isfinite(float(value))
or float(value) != number
or not lower <= number <= upper
):
raise ValueError
return number
except (ValueError, TypeError, OverflowError) as exc:
raise ClefError(f"{label}は{lower}〜{upper}の整数にしてください。") from exc
def validate_request(request):
profile = request.get("profile")
if profile not in PROFILES:
raise ClefError("実行プロファイルを選んでください。")
schema = parse_schema(request.get("questions"))
state = request.get("state", "")
if not isinstance(state, (str, dict, list, int, float, bool, type(None))):
raise ClefError("補足入力は文章またはJSONにしてください。")
if len(json.dumps(state, ensure_ascii=False)) > 60_000:
raise ClefError("補足入力は60,000文字以内にしてください。")
return {
"profile": profile,
"questions": schema,
"state": state,
"max_pixels": integer(request.get("max_pixels", 262144), "画像の最大画素数", 65536, 1048576),
"max_length": integer(request.get("max_length", 4096), "入力トークン上限", 512, 8192),
}
def fingerprint(request, items):
profile = PROFILES[request["profile"]]
return canonical_hash(
{
"request": request,
"model": MODELS[profile["model"]],
"profile": profile,
"inputs": [{"kind": item.get("kind", "image"), "sha256": item["sha256"]} for item in items],
"preprocessing": PREPROCESSING_VERSION,
}
)
def summary(answer, question):
kind = answer["type"]
if kind == "noul":
return f"P(真) {answer['noul']:.3f}"
if kind == "choice":
choice = answer["choice"]
return f"{choice} · p={answer['confidence']:.3f}"
return f"期待値 {answer['score']:.2f} (0–{len(question['criteria']) - 1})"
def ambiguity(answer):
if answer["type"] == "noul":
return 1 - 2 * abs(answer["noul"] - 0.5)
probabilities = list(answer["probabilities"].values())
return -sum(p * math.log(p) for p in probabilities if p > 0) / math.log(len(probabilities))
def compare_runs(left, right):
if left.get("preprocessing") != right.get("preprocessing"):
raise ClefError("比較には画像の前処理の一致が必要です。異なる前処理の結果は比較できません。")
for key in ("questions", "state", "max_pixels"):
if canonical_hash(left["request"].get(key)) != canonical_hash(right["request"].get(key)):
raise ClefError("比較には判断項目・補足入力・処理解像度の一致が必要です。")
other = defaultdict(deque)
for item in right["items"]:
if item["status"] == "done":
other[(item.get("kind", "image"), item["sha256"])].append(item)
rows = []
for item in left["items"]:
key = (item.get("kind", "image"), item["sha256"])
if item["status"] != "done" or not other[key]:
continue
matched = other[key].popleft()
differences = {}
for qid, answer in item["answers"].items():
target = matched["answers"][qid]
if answer["type"] == "noul":
delta = target["noul"] - answer["noul"]
differences[qid] = {
"delta": delta,
"changed": (answer["noul"] >= 0.5) != (target["noul"] >= 0.5),
"max_probability_delta": abs(delta),
}
else:
delta = max(abs(p - target["probabilities"][key]) for key, p in answer["probabilities"].items())
changed = (
answer.get("choice") != target.get("choice")
if answer["type"] == "choice"
else max(answer["probabilities"], key=answer["probabilities"].get)
!= max(target["probabilities"], key=target["probabilities"].get)
)
differences[qid] = {"changed": changed, "max_probability_delta": delta}
if answer["type"] == "score":
differences[qid]["delta"] = target["score"] - answer["score"]
rows.append(
{
"name": item["name"],
"sha256": item["sha256"],
"differences": differences,
"changed": any(x["changed"] for x in differences.values()),
"max_probability_delta": max(x["max_probability_delta"] for x in differences.values()),
}
)
return sorted(rows, key=lambda x: x["max_probability_delta"], reverse=True)
def export_csv(run):
schema = run["request"]["questions"]
columns = ["name", "sha256", "status", "error"] + [
col for qid in schema for col in ("answer:" + qid, "confidence:" + qid)
]
stream = io.StringIO(newline="")
writer = csv.DictWriter(stream, fieldnames=columns)
writer.writeheader()
for item in run["items"]:
row = {key: item.get(key, "") for key in columns[:4]}
for qid, answer in item.get("answers", {}).items():
row["answer:" + qid] = answer.get("choice", answer.get("score", answer.get("noul", "")))
row["confidence:" + qid] = answer.get("confidence", "")
for key, value in row.items():
if isinstance(value, str) and value.lstrip().startswith(("=", "+", "-", "@")):
row[key] = "'" + value
writer.writerow(row)
return stream.getvalue()
def source_manifest(root, model, verify_hashes=False):
directory = Path(root) / "source" / model
try:
manifest = json.loads((directory / "release.json").read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise ClefError(
f"{model}のBF16元重みは未導入です。aikimi-clef-setup.bat --source --model {model}を実行してください。"
) from exc
if manifest.get("revision") != MODELS[model]["revision"] or manifest.get("repo") != MODELS[model]["repo"]:
raise ClefError("Clefの固定リビジョンが一致しません。セットアップを再実行してください。")
for item in manifest["files"]:
path = directory / item["path"]
if (
not path.resolve().is_relative_to(directory.resolve())
or not path.is_file()
or path.stat().st_size != item["size"]
):
raise ClefError(f"Clef配布物が不足・変更されています: {item['path']}")
if (verify_hashes or path.suffix == ".py") and sha256(path) != item["sha256"]:
raise ClefError(f"ClefのSHA-256が一致しません: {item['path']}")
return directory, manifest
|