Spaces:
Sleeping
Sleeping
| """ํ์ต ํธ๋ฆฌ๊ฑฐ + Gradient ๊ณ์ฐ (PoC ๋จ์ํ ๋ฒ์ ) | |
| ================================================= | |
| SPEC ยง1 ๊ธฐ๋ฅ 6 โ *"ํ์ต ์กฐ๊ฑด ๋๋ฌ ์ gradient ์์ฑ"* ์ ๋ฐ๋ชจ/๊ฒ์ฆ์ฉ ๊ตฌํ. | |
| ์ค๊ณ ์๋ | |
| --------- | |
| - ํ์ฌ ๋ถ๋ฅ๊ธฐ `rule-v1` ์ **์ ํ ์ ์ ๋ชจ๋ธ**: ``score = ฮฃ w_e ยท x_e`` | |
| (x_e = entity ๋๋ keyword ์ ๋ณธ๋ฌธ ๋ด ๊ฐ์) | |
| - "ํ์ต" ์ ๊ฐ์ฅ ๋จ์ํ๊ณ ์๋ฏธ์๋ ํํ = **์ ํ ํ๊ท์ 1-step gradient descent**. | |
| - ์ฌ์ฉ์๊ฐ ๋ถ์ฌํ ๋ฑ๊ธ์ *๋ชฉํ ์ ์* ๋ก ๋งคํํ ๋ค, ์์ฐจ ร feature ๋ก ๊ฐ์ค์น ์ ๋ฐ์ดํธ. | |
| - ๊ฒฐ๊ณผ = **proposed weight deltas** + ์ ๊ฐ์ค์น + ์ ์ฉ ์ ํ SSE / ์ ํ๋. | |
| - ์ด ๊ฒฐ๊ณผ๋ฅผ **๊ทธ๋๋ก ENTITY_WEIGHTS / GRADE_KEYWORDS ์ ๋ฐ์ํ๋ฉด ๋ชจ๋ธ ํซ์ค์ ์๋ฃ**. | |
| ํ์ต ์กฐ๊ฑด (SPEC ยง9 ์ ๋จ์ํ) | |
| ----------------------------- | |
| PoC ๋ฐ๋ชจ ํธ์๋ฅผ ์ํด ์๊ณ๊ฐ์ ๋ฎ์ท๋ค โ ์์ฐ ์ 5~10 ๊ฑด์ด๋ฉด ๊ตฌ๋ ๊ฐ๋ฅ: | |
| - ๋์ ๋ ์ด๋ธ โฅ 10 | |
| - ๋ฑ๊ธ๋ณ ์ต์ โฅ 3 (C, S, O ๊ฐ๊ฐ) | |
| - ๊ฐญ(gap > 0) โฅ 3 | |
| - ๋ชจ๋ ๋ง์กฑํ๋ฉด ready=True | |
| Gradient ์ ์ (์ ํ ํ๊ท) | |
| ------------------------- | |
| ๊ฐ ๊ฒฐ์ ์ ๋ํด: | |
| target = TARGET_SCORE[user_grade] # Oโ1.0, Sโ3.5, Cโ6.5 (๊ฐ ๋ฑ๊ธ ๋ฐด๋ ์ค์) | |
| pred = ฮฃ w_f ยท x_f # ํ์ฌ ๋ถ๋ฅ๊ธฐ๊ฐ ๋ถ์ฌํ ์ ์ | |
| resid = target - pred | |
| ฮw_f += ฮท ยท resid ยท x_f / (1 + ฮฃ x_f) | |
| ํ์ต ๊ฐ์ค์น (SPEC ยง10.2): sample_weight = 1 + 1.5 ยท gap | |
| โ ๊ฐญ ํฐ ์ํ์ผ์๋ก grad ์ ๋ ํฌ๊ฒ ๊ธฐ์ฌ. ๋ฐ์ดํฐ ์ ์ PoC ์์ ํต์ฌ. | |
| ฮท (ํ์ต๋ฅ ) = 0.05 (๊ธฐ๋ณธ). ํฐ deltas ๋์ ๋ฐฉ์ง๋ฅผ ์ํ max_step clip. | |
| """ | |
| from __future__ import annotations | |
| from datetime import datetime, timezone | |
| from typing import Iterable | |
| import classifier # ENTITY_WEIGHTS / GRADE_KEYWORDS ์ *์๋ณธ* (in-memory) ์ฐธ์กฐ | |
| # --------------------------------------------------------------------------- | |
| # ํ์ต ํธ๋ฆฌ๊ฑฐ ์กฐ๊ฑด (PoC ๋ฐ๋ชจ์ฉ โ SPEC ยง9 ์ ์ํ ๋ฒ์ ) | |
| # --------------------------------------------------------------------------- | |
| MIN_LABELED = 10 | |
| MIN_PER_GRADE = 3 | |
| MIN_GAP_DECISIONS = 3 | |
| # Gradient hyperparameters | |
| LEARNING_RATE = 0.05 | |
| MAX_DELTA = 0.5 # |ฮw| ์ํ โ ํ ๋ผ์ด๋์์ ๊ฐ์ค์น๊ฐ ๋๋ฌด ํ๋ค๋ฆฌ์ง ์๊ฒ | |
| # ๋ฑ๊ธ โ ๋ชฉํ ์ ์ (rule-v1 ์ ์๊ณ๊ฐ 5.0/2.0 ๊ธฐ์ค) | |
| TARGET_SCORE = {"O": 1.0, "S": 3.5, "C": 6.5} | |
| def _now() -> str: | |
| return datetime.now(timezone.utc).isoformat(timespec="seconds") | |
| # --------------------------------------------------------------------------- | |
| # ํ์ต ํธ๋ฆฌ๊ฑฐ ์กฐ๊ฑด ํ๊ฐ | |
| # --------------------------------------------------------------------------- | |
| def evaluate_readiness(decisions: list[dict]) -> dict: | |
| """์ ์ฅ๋ ๊ฒฐ์ ๋ฆฌ์คํธ โ ํ์ต ๊ฐ๋ฅ ์ฌ๋ถ + ์ฌ์ .""" | |
| n = len(decisions) | |
| per = {"C": 0, "S": 0, "O": 0} | |
| gap_pos = 0 | |
| for d in decisions: | |
| u = d.get("user_grade") | |
| if u in per: | |
| per[u] += 1 | |
| if int(d.get("gap", 0)) > 0: | |
| gap_pos += 1 | |
| checks = [ | |
| {"key": "labeled", "label": "๋์ ๋ ์ด๋ธ", | |
| "value": n, "threshold": MIN_LABELED, | |
| "ok": n >= MIN_LABELED}, | |
| {"key": "per_grade_C", "label": "C ๋ฑ๊ธ ๋์ ", | |
| "value": per["C"], "threshold": MIN_PER_GRADE, | |
| "ok": per["C"] >= MIN_PER_GRADE}, | |
| {"key": "per_grade_S", "label": "S ๋ฑ๊ธ ๋์ ", | |
| "value": per["S"], "threshold": MIN_PER_GRADE, | |
| "ok": per["S"] >= MIN_PER_GRADE}, | |
| {"key": "per_grade_O", "label": "O ๋ฑ๊ธ ๋์ ", | |
| "value": per["O"], "threshold": MIN_PER_GRADE, | |
| "ok": per["O"] >= MIN_PER_GRADE}, | |
| {"key": "gap", "label": "์ฌ์ฉ์-AI ๊ฐญ(>0)", | |
| "value": gap_pos, "threshold": MIN_GAP_DECISIONS, | |
| "ok": gap_pos >= MIN_GAP_DECISIONS}, | |
| ] | |
| return { | |
| "ready": all(c["ok"] for c in checks), | |
| "checks": checks, | |
| "totals": {"labeled": n, "per_grade": per, "gap_positive": gap_pos}, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Feature ์ถ์ถ โ decision 1๊ฑด โ {feature_name: count} | |
| # --------------------------------------------------------------------------- | |
| def _features_of(decision: dict) -> dict[str, int]: | |
| """์ ์ฅ๋ reasons ์์ feature counts ๋ณต์. | |
| `reasons` ๋ ๋ถ๋ฅ ์์ ์ค๋ ์ท์ด๊ณ ๊ทธ ์์ฒด๊ฐ (entity_type or keyword_label, count) | |
| ๋ฆฌ์คํธ๋ผ ๊ทธ๋๋ก features ๋ก ์ฌ์ฉ. (entity_type ๊ณผ keyword ๋ผ๋ฒจ์ด ์ถฉ๋ํ์ง ์๋๋ก | |
| keyword ๋ 'kw:' prefix ๋ฅผ ๋ถ์ธ ๋ณ๋ namespace ๋ก ๋ค๋ฃฌ๋ค.) | |
| """ | |
| feats: dict[str, int] = {} | |
| for r in decision.get("reasons") or []: | |
| kind = r.get("kind") | |
| label = r.get("label") | |
| if not label: | |
| continue | |
| if kind == "keyword": | |
| # keyword ๋ cap ์ ์ฉ๋ counted ์ฌ์ฉ (์์ผ๋ฉด count) | |
| cnt = int(r.get("counted") or r.get("count") or 0) | |
| feats[f"kw:{label}"] = feats.get(f"kw:{label}", 0) + cnt | |
| else: | |
| cnt = int(r.get("count") or 0) | |
| feats[label] = feats.get(label, 0) + cnt | |
| return feats | |
| def _current_weight(feature: str) -> float: | |
| """ENTITY_WEIGHTS / GRADE_KEYWORDS ์์ ํ์ฌ ๊ฐ์ค์น ์กฐํ.""" | |
| if feature.startswith("kw:"): | |
| label = feature[3:] | |
| for _kw, w, lbl in classifier.GRADE_KEYWORDS: | |
| if lbl == label: | |
| return float(w) | |
| return 0.0 | |
| return classifier.ENTITY_WEIGHTS.get(feature, classifier.DEFAULT_ENTITY_WEIGHT) | |
| def _predict_score(feats: dict[str, int]) -> float: | |
| return sum(_current_weight(f) * c for f, c in feats.items()) | |
| # --------------------------------------------------------------------------- | |
| # Training step (1 epoch, ์ ์ฒด ๋ฐฐ์น) | |
| # --------------------------------------------------------------------------- | |
| def train_one_round( | |
| decisions: list[dict], | |
| learning_rate: float = LEARNING_RATE, | |
| max_delta: float = MAX_DELTA, | |
| ) -> dict: | |
| """์ ์ฒด ๊ฒฐ์ ์ ๋ํด ํ๊ท gradient ๋ฅผ ๊ณ์ฐํ์ฌ ๊ฐ์ค์น ์ ๋ฐ์ดํธ ์ ์. | |
| Returns: | |
| { | |
| decisions_count, accuracy_before, accuracy_after, | |
| sse_before, sse_after, | |
| weight_deltas: {feature: signed_delta}, | |
| new_weights: {feature: new_w}, | |
| per_decision: [{id, target, pred_before, pred_after, residual}, ...] | |
| } | |
| """ | |
| if not decisions: | |
| return {"decisions_count": 0, "weight_deltas": {}, "new_weights": {}} | |
| # 1) ๋ชจ๋ feature ์์ง + ๊ธฐ์กด ๊ฐ์ค์น ์ค๋ ์ท | |
| all_feats: set[str] = set() | |
| for d in decisions: | |
| all_feats.update(_features_of(d).keys()) | |
| base_w: dict[str, float] = {f: _current_weight(f) for f in all_feats} | |
| grad_sum: dict[str, float] = {f: 0.0 for f in all_feats} | |
| weight_sum = 0.0 | |
| sse_before = 0.0 | |
| correct_before = 0 | |
| per_decision = [] | |
| # 2) ๋์ grad ๊ณ์ฐ | |
| for d in decisions: | |
| feats = _features_of(d) | |
| target = TARGET_SCORE[d["user_grade"]] | |
| pred = sum(base_w[f] * c for f, c in feats.items()) | |
| resid = target - pred | |
| sw = 1.0 + 1.5 * int(d.get("gap", 0)) # SPEC ยง10.2 | |
| weight_sum += sw | |
| denom = 1.0 + sum(feats.values()) | |
| for f, c in feats.items(): | |
| grad_sum[f] += sw * resid * c / denom | |
| sse_before += (target - pred) ** 2 | |
| if d["ai_grade"] == d["user_grade"]: | |
| correct_before += 1 | |
| per_decision.append({ | |
| "id": d.get("id"), | |
| "target": round(target, 2), | |
| "pred_before": round(pred, 3), | |
| "residual": round(resid, 3), | |
| "sample_weight": round(sw, 2), | |
| "user_grade": d["user_grade"], | |
| "ai_grade": d["ai_grade"], | |
| }) | |
| # 3) ํ๊ท gradient โ delta (clip) | |
| deltas: dict[str, float] = {} | |
| new_weights: dict[str, float] = {} | |
| for f, g in grad_sum.items(): | |
| d_w = learning_rate * (g / max(weight_sum, 1e-9)) | |
| d_w = max(-max_delta, min(max_delta, d_w)) | |
| new_w = max(0.0, base_w[f] + d_w) # ์์ ๊ฐ์ค์น ๋ฐฉ์ง | |
| deltas[f] = round(d_w, 4) | |
| new_weights[f] = round(new_w, 4) | |
| # 4) After ํ๊ฐ | |
| sse_after = 0.0 | |
| correct_after = 0 | |
| for i, d in enumerate(decisions): | |
| feats = _features_of(d) | |
| target = TARGET_SCORE[d["user_grade"]] | |
| pred_after = sum(new_weights.get(f, base_w.get(f, 0.0)) * c for f, c in feats.items()) | |
| sse_after += (target - pred_after) ** 2 | |
| # ์ ๊ฐ์ค์น๋ก ๋ค์ ๋ฑ๊ธ ์ฐ์ ํด ์ ํ๋ ์ธก์ | |
| new_grade = ( | |
| "C" if pred_after >= classifier.C_THRESHOLD else | |
| "S" if pred_after >= classifier.S_THRESHOLD else | |
| "O" | |
| ) | |
| if new_grade == d["user_grade"]: | |
| correct_after += 1 | |
| per_decision[i]["pred_after"] = round(pred_after, 3) | |
| per_decision[i]["new_grade"] = new_grade | |
| n = len(decisions) | |
| return { | |
| "decisions_count": n, | |
| "accuracy_before": round(correct_before / n, 3), | |
| "accuracy_after": round(correct_after / n, 3), | |
| "sse_before": round(sse_before, 3), | |
| "sse_after": round(sse_after, 3), | |
| "weight_deltas": deltas, | |
| "new_weights": new_weights, | |
| "per_decision": per_decision, | |
| "hyperparams": { | |
| "learning_rate": learning_rate, | |
| "max_delta": max_delta, | |
| "target_score": TARGET_SCORE, | |
| "loss": "MSE on linear score", | |
| "objective": "ฮฃ sample_weight ยท (target - ฮฃ wยทx)ยฒ", | |
| }, | |
| "started_at": _now(), | |
| "finished_at": _now(), | |
| "status": "completed", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # ํซ์ค์ โ ๊ฒฐ๊ณผ๋ฅผ in-memory ENTITY_WEIGHTS / GRADE_KEYWORDS ์ ๋ฐ์ | |
| # --------------------------------------------------------------------------- | |
| def apply_new_weights( | |
| new_weights: dict[str, float], | |
| *, | |
| training_run_id: int | None = None, | |
| decisions_count: int | None = None, | |
| accuracy: float | None = None, | |
| sse: float | None = None, | |
| notes: str | None = None, | |
| ) -> dict: | |
| """SPEC ยง1 ๊ธฐ๋ฅ 10 โ "๋ชจ๋ธ ํซ์ค์". ๋ค์ ๋ถ์๋ถํฐ ์ ๊ฐ์ค์น ์ ์ฉ. | |
| ํ๋ก์ธ์ค ์ฌ์์ ์: | |
| - in-memory ENTITY_WEIGHTS / GRADE_KEYWORDS ์์ฒด๋ ์๋ณต๋์ง๋ง, | |
| - storage.model_versions ํ ์ด๋ธ์ ์ค๋ ์ท์ด ๋ณด์กด๋๋ฏ๋ก | |
| ๋ถํ ์ active ๋ฒ์ ์ ๋ค์ ๋ก๋ํ๋ฉด ๋ณต์ ๊ฐ๋ฅ. | |
| """ | |
| import storage # ์ํ import ๋ฐฉ์ง | |
| applied_entities: dict[str, list[float]] = {} | |
| applied_keywords: dict[str, list[float]] = {} | |
| for f, new_w in new_weights.items(): | |
| if f.startswith("kw:"): | |
| label = f[3:] | |
| for i, (kw, old_w, lbl) in enumerate(classifier.GRADE_KEYWORDS): | |
| if lbl == label: | |
| classifier.GRADE_KEYWORDS[i] = (kw, float(new_w), lbl) | |
| applied_keywords[label] = [round(old_w, 4), round(new_w, 4), | |
| round(new_w - old_w, 4)] | |
| break | |
| else: | |
| old_w = classifier.ENTITY_WEIGHTS.get(f, classifier.DEFAULT_ENTITY_WEIGHT) | |
| classifier.ENTITY_WEIGHTS[f] = float(new_w) | |
| applied_entities[f] = [round(old_w, 4), round(new_w, 4), | |
| round(new_w - old_w, 4)] | |
| # ---- ์ ๋ฒ์ ๋ผ๋ฒจ ๋ถ์ฌ ---- | |
| parent = classifier.active_version() | |
| new_version = storage.next_version_label(base="rule-v1") | |
| classifier.set_active_version(new_version) | |
| # ---- ๋ชจ๋ธ ๋ฒ์ ๊ธฐ๋ก ---- | |
| diff = { | |
| "entities_changed": applied_entities, | |
| "keywords_changed": applied_keywords, | |
| } | |
| storage.insert_model_version({ | |
| "version": new_version, | |
| "parent_version": parent, | |
| "training_run_id": training_run_id, | |
| "decisions_count": decisions_count, | |
| "accuracy": accuracy, | |
| "sse": sse, | |
| "weights": dict(classifier.ENTITY_WEIGHTS), | |
| "keywords": [(kw, w, lbl) for kw, w, lbl in classifier.GRADE_KEYWORDS], | |
| "diff": diff, | |
| "notes": notes or "linear-regression 1-step gradient hot-swap", | |
| "is_active": True, | |
| }) | |
| return { | |
| "new_version": new_version, | |
| "parent_version": parent, | |
| "entities_changed": applied_entities, | |
| "keywords_changed": applied_keywords, | |
| "ENTITY_WEIGHTS_now": dict(classifier.ENTITY_WEIGHTS), | |
| "GRADE_KEYWORDS_now": [(kw, w, lbl) for kw, w, lbl in classifier.GRADE_KEYWORDS], | |
| } | |
| def bootstrap_initial_version() -> None: | |
| """์ฑ ๋ถํ ์ 1ํ ํธ์ถ โ ๋ฒ ์ด์ค 'rule-v1' ๊ฐ model_versions ์ ์์ผ๋ฉด ๋ฑ๋ก. | |
| ์ด๋ ๊ฒ ํด๋๋ฉด ํ์ต/์ด๋ ฅ ํญ์ ๋ฒ์ ์ด๋ ฅ ํ๊ฐ ํญ์ ์ต์ 1ํ์ ๊ฐ์ง๋ค. | |
| """ | |
| import storage | |
| if storage.active_model_version(): | |
| return | |
| storage.insert_model_version({ | |
| "version": classifier.CLASSIFIER_VERSION, | |
| "parent_version": None, | |
| "training_run_id": None, | |
| "decisions_count": 0, | |
| "accuracy": None, | |
| "sse": None, | |
| "weights": dict(classifier.ENTITY_WEIGHTS), | |
| "keywords": [(kw, w, lbl) for kw, w, lbl in classifier.GRADE_KEYWORDS], | |
| "diff": {}, | |
| "notes": "initial baseline weights (hand-tuned)", | |
| "is_active": True, | |
| }) | |