Spaces:
Sleeping
Sleeping
Cyprien Claude Opus 5 (1M context) commited on
Commit ·
9668975
1
Parent(s): b64175d
Resolve refs to commits so nothing serves a stale model or split
Browse filesload(repo, 'main') was lru_cached on the literal 'main', pinning whichever
commit was current at first use. Refs now resolve to a sha per call and the
caches key on the sha. Same for the test split, which grew 46 -> 51 rows in
two days. Reports name both commits, and a Timer refreshes the pickers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- README.md +15 -0
- app/config.py +1 -0
- app/evaluation.py +33 -14
- app/handlers.py +18 -8
- app/hub.py +10 -0
- app/predictors.py +10 -5
- app/ui.py +6 -4
README.md
CHANGED
|
@@ -63,6 +63,21 @@ sélectionne le chemin SetFit, sinon le modèle est chargé comme un
|
|
| 63 |
L'onglet **Évaluation** rejoue le split de test et affiche la matrice de
|
| 64 |
confusion puis les cas un par un, erreurs d'abord.
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
## Structure
|
| 67 |
|
| 68 |
```
|
|
|
|
| 63 |
L'onglet **Évaluation** rejoue le split de test et affiche la matrice de
|
| 64 |
confusion puis les cas un par un, erreurs d'abord.
|
| 65 |
|
| 66 |
+
## Rien n'est jamais périmé
|
| 67 |
+
|
| 68 |
+
`main` est résolu en commit à chaque appel, et les caches sont indexés sur le
|
| 69 |
+
commit — pas sur le nom de la branche. Un `train.py --push` est donc pris en
|
| 70 |
+
compte sans redémarrage : sans cela, `main` désignait pour toujours le modèle
|
| 71 |
+
chargé au premier clic. Le split de test est résolu de la même façon, ce qui
|
| 72 |
+
compte : il est passé de 46 à 51 cas en deux jours.
|
| 73 |
+
|
| 74 |
+
Le score affiché nomme les deux commits (`modèle @ sha · données sha`), donc un
|
| 75 |
+
chiffre se rattache toujours à ce qui l'a produit.
|
| 76 |
+
|
| 77 |
+
La liste des dépôts et des révisions se recharge toutes les
|
| 78 |
+
`REFRESH_SECONDS` secondes en gardant la sélection en cours, et `↻` force le
|
| 79 |
+
rechargement.
|
| 80 |
+
|
| 81 |
## Structure
|
| 82 |
|
| 83 |
```
|
app/config.py
CHANGED
|
@@ -4,6 +4,7 @@ ORG = "bee2link"
|
|
| 4 |
MODEL_SEARCH = "breakdown-risk-"
|
| 5 |
DATASET_REPO = "bee2link/breakdown-risk"
|
| 6 |
CALLER_TURNS = 3
|
|
|
|
| 7 |
TOKEN = os.environ.get("HF_TOKEN")
|
| 8 |
|
| 9 |
LABEL_NAMES = {"risk": "Risque de panne", "no_risk": "Pas de risque"}
|
|
|
|
| 4 |
MODEL_SEARCH = "breakdown-risk-"
|
| 5 |
DATASET_REPO = "bee2link/breakdown-risk"
|
| 6 |
CALLER_TURNS = 3
|
| 7 |
+
REFRESH_SECONDS = 30
|
| 8 |
TOKEN = os.environ.get("HF_TOKEN")
|
| 9 |
|
| 10 |
LABEL_NAMES = {"risk": "Risque de panne", "no_risk": "Pas de risque"}
|
app/evaluation.py
CHANGED
|
@@ -7,6 +7,7 @@ import numpy as np
|
|
| 7 |
from datasets import load_dataset
|
| 8 |
|
| 9 |
from .config import DATASET_REPO, TOKEN, display
|
|
|
|
| 10 |
from .predictors import Predictor, load
|
| 11 |
|
| 12 |
BATCH_SIZE = 8
|
|
@@ -14,9 +15,17 @@ BATCH_SIZE = 8
|
|
| 14 |
Track = Callable[[Iterable], Iterable]
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
class Report(NamedTuple):
|
| 18 |
repo: str
|
| 19 |
-
|
|
|
|
| 20 |
hits: int
|
| 21 |
total: int
|
| 22 |
ms_per_case: float
|
|
@@ -24,11 +33,20 @@ class Report(NamedTuple):
|
|
| 24 |
cases: list[list]
|
| 25 |
|
| 26 |
|
| 27 |
-
@lru_cache(maxsize=
|
| 28 |
-
def
|
| 29 |
-
data = load_dataset(DATASET_REPO, split="test", token=TOKEN)
|
| 30 |
names = data.features["label"].names
|
| 31 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
def confusion(labels: list[str], gold: list[str], predicted: list[str]) -> list[list]:
|
|
@@ -77,20 +95,21 @@ def run(
|
|
| 77 |
predict: Predictor | None = None,
|
| 78 |
track: Track | None = None,
|
| 79 |
) -> Report:
|
| 80 |
-
predictor = predict if predict
|
| 81 |
-
|
| 82 |
|
| 83 |
started = time.perf_counter()
|
| 84 |
-
labels, probabilities = classify_all(predictor, texts, track)
|
| 85 |
elapsed = time.perf_counter() - started
|
| 86 |
|
| 87 |
predicted = [labels[int(row.argmax())] for row in probabilities]
|
| 88 |
return Report(
|
| 89 |
repo=repo,
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
| 96 |
)
|
|
|
|
| 7 |
from datasets import load_dataset
|
| 8 |
|
| 9 |
from .config import DATASET_REPO, TOKEN, display
|
| 10 |
+
from .hub import resolve
|
| 11 |
from .predictors import Predictor, load
|
| 12 |
|
| 13 |
BATCH_SIZE = 8
|
|
|
|
| 15 |
Track = Callable[[Iterable], Iterable]
|
| 16 |
|
| 17 |
|
| 18 |
+
class Split(NamedTuple):
|
| 19 |
+
ids: list[str]
|
| 20 |
+
texts: list[str]
|
| 21 |
+
gold: list[str]
|
| 22 |
+
sha: str
|
| 23 |
+
|
| 24 |
+
|
| 25 |
class Report(NamedTuple):
|
| 26 |
repo: str
|
| 27 |
+
model_sha: str
|
| 28 |
+
dataset_sha: str
|
| 29 |
hits: int
|
| 30 |
total: int
|
| 31 |
ms_per_case: float
|
|
|
|
| 33 |
cases: list[list]
|
| 34 |
|
| 35 |
|
| 36 |
+
@lru_cache(maxsize=2)
|
| 37 |
+
def at_sha(sha: str) -> Split:
|
| 38 |
+
data = load_dataset(DATASET_REPO, split="test", revision=sha, token=TOKEN)
|
| 39 |
names = data.features["label"].names
|
| 40 |
+
return Split(
|
| 41 |
+
ids=list(data["id"]),
|
| 42 |
+
texts=list(data["text"]),
|
| 43 |
+
gold=[names[i] for i in data["label"]],
|
| 44 |
+
sha=sha,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_split() -> Split:
|
| 49 |
+
return at_sha(resolve(DATASET_REPO, "main", repo_type="dataset"))
|
| 50 |
|
| 51 |
|
| 52 |
def confusion(labels: list[str], gold: list[str], predicted: list[str]) -> list[list]:
|
|
|
|
| 95 |
predict: Predictor | None = None,
|
| 96 |
track: Track | None = None,
|
| 97 |
) -> Report:
|
| 98 |
+
predictor, model_sha = (predict, revision) if predict else load(repo, revision)
|
| 99 |
+
split = test_split()
|
| 100 |
|
| 101 |
started = time.perf_counter()
|
| 102 |
+
labels, probabilities = classify_all(predictor, split.texts, track)
|
| 103 |
elapsed = time.perf_counter() - started
|
| 104 |
|
| 105 |
predicted = [labels[int(row.argmax())] for row in probabilities]
|
| 106 |
return Report(
|
| 107 |
repo=repo,
|
| 108 |
+
model_sha=model_sha,
|
| 109 |
+
dataset_sha=split.sha,
|
| 110 |
+
hits=sum(want == got for want, got in zip(split.gold, predicted)),
|
| 111 |
+
total=len(split.gold),
|
| 112 |
+
ms_per_case=elapsed * 1000 / len(split.texts),
|
| 113 |
+
confusion=confusion(labels, split.gold, predicted),
|
| 114 |
+
cases=cases(split.ids, split.texts, split.gold, predicted, probabilities),
|
| 115 |
)
|
app/handlers.py
CHANGED
|
@@ -22,15 +22,18 @@ def classify(repo: str, revision: str, transcript: str) -> tuple[dict[str, float
|
|
| 22 |
return {}, "", ""
|
| 23 |
|
| 24 |
started = time.perf_counter()
|
| 25 |
-
|
|
|
|
| 26 |
scores = {display(name): float(p) for name, p in zip(labels, probabilities[0])}
|
| 27 |
-
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
def summary(report: evaluation.Report) -> str:
|
| 31 |
return (
|
| 32 |
f"### {report.hits}/{report.total} — exactitude {report.hits / report.total:.1%}\n"
|
| 33 |
-
f"`{report.repo}`
|
|
|
|
| 34 |
)
|
| 35 |
|
| 36 |
|
|
@@ -39,10 +42,10 @@ def evaluate(repo: str, revision: str, progress=gr.Progress()):
|
|
| 39 |
return NO_MODEL, [], []
|
| 40 |
|
| 41 |
progress(0, desc=LOADING)
|
| 42 |
-
predictor = load(repo, revision)
|
| 43 |
track = partial(progress.tqdm, desc=SCORING)
|
| 44 |
|
| 45 |
-
report = evaluation.run(repo,
|
| 46 |
return summary(report), report.confusion, report.cases
|
| 47 |
|
| 48 |
|
|
@@ -51,6 +54,13 @@ def pick_revision(repo: str) -> gr.Dropdown:
|
|
| 51 |
return gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
|
| 52 |
|
| 53 |
|
| 54 |
-
def
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
return {}, "", ""
|
| 23 |
|
| 24 |
started = time.perf_counter()
|
| 25 |
+
predictor, sha = load(repo, revision)
|
| 26 |
+
labels, probabilities = predictor([text])
|
| 27 |
scores = {display(name): float(p) for name, p in zip(labels, probabilities[0])}
|
| 28 |
+
elapsed = (time.perf_counter() - started) * 1000
|
| 29 |
+
return scores, text, f"`{sha[:7]}` · `{elapsed:.0f} ms`"
|
| 30 |
|
| 31 |
|
| 32 |
def summary(report: evaluation.Report) -> str:
|
| 33 |
return (
|
| 34 |
f"### {report.hits}/{report.total} — exactitude {report.hits / report.total:.1%}\n"
|
| 35 |
+
f"`{report.repo}` @ `{report.model_sha[:7]}` · données `{report.dataset_sha[:7]}`"
|
| 36 |
+
f" · {report.ms_per_case:.0f} ms par cas"
|
| 37 |
)
|
| 38 |
|
| 39 |
|
|
|
|
| 42 |
return NO_MODEL, [], []
|
| 43 |
|
| 44 |
progress(0, desc=LOADING)
|
| 45 |
+
predictor, sha = load(repo, revision)
|
| 46 |
track = partial(progress.tqdm, desc=SCORING)
|
| 47 |
|
| 48 |
+
report = evaluation.run(repo, sha, predict=predictor, track=track)
|
| 49 |
return summary(report), report.confusion, report.cases
|
| 50 |
|
| 51 |
|
|
|
|
| 54 |
return gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
|
| 55 |
|
| 56 |
|
| 57 |
+
def refresh(repo: str, revision: str) -> tuple[gr.Dropdown, gr.Dropdown]:
|
| 58 |
+
repos = model_repos()
|
| 59 |
+
chosen = repo if repo in repos else (repos[0] if repos else None)
|
| 60 |
+
choices = revisions(chosen) if chosen else []
|
| 61 |
+
shas = [sha for _, sha in choices]
|
| 62 |
+
keep = revision if revision in shas else (shas[0] if shas else None)
|
| 63 |
+
return (
|
| 64 |
+
gr.Dropdown(choices=repos, value=chosen),
|
| 65 |
+
gr.Dropdown(choices=choices, value=keep),
|
| 66 |
+
)
|
app/hub.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
from huggingface_hub import HfApi, hf_hub_download
|
|
@@ -6,6 +7,15 @@ from .config import MODEL_SEARCH, ORG, TOKEN
|
|
| 6 |
|
| 7 |
api = HfApi(token=TOKEN)
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def model_repos() -> list[str]:
|
| 11 |
return sorted(model.id for model in api.list_models(author=ORG, search=MODEL_SEARCH))
|
|
|
|
| 1 |
+
import re
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
from huggingface_hub import HfApi, hf_hub_download
|
|
|
|
| 7 |
|
| 8 |
api = HfApi(token=TOKEN)
|
| 9 |
|
| 10 |
+
SHA = re.compile(r"[0-9a-f]{40}")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def resolve(repo: str, revision: str, repo_type: str = "model") -> str:
|
| 14 |
+
if revision and SHA.fullmatch(revision):
|
| 15 |
+
return revision
|
| 16 |
+
info = api.repo_info(repo, revision=revision or None, repo_type=repo_type, token=TOKEN)
|
| 17 |
+
return info.sha
|
| 18 |
+
|
| 19 |
|
| 20 |
def model_repos() -> list[str]:
|
| 21 |
return sorted(model.id for model in api.list_models(author=ORG, search=MODEL_SEARCH))
|
app/predictors.py
CHANGED
|
@@ -9,7 +9,7 @@ from sentence_transformers import SentenceTransformer
|
|
| 9 |
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 10 |
|
| 11 |
from .config import TOKEN
|
| 12 |
-
from .hub import download, has_file
|
| 13 |
|
| 14 |
Predictor = Callable[[list[str]], tuple[list[str], np.ndarray]]
|
| 15 |
|
|
@@ -42,7 +42,12 @@ def automodel(repo: str, revision: str) -> Predictor:
|
|
| 42 |
|
| 43 |
|
| 44 |
@lru_cache(maxsize=4)
|
| 45 |
-
def
|
| 46 |
-
if has_file(repo, "model_head.pkl",
|
| 47 |
-
return setfit(repo,
|
| 48 |
-
return automodel(repo,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 10 |
|
| 11 |
from .config import TOKEN
|
| 12 |
+
from .hub import download, has_file, resolve
|
| 13 |
|
| 14 |
Predictor = Callable[[list[str]], tuple[list[str], np.ndarray]]
|
| 15 |
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
@lru_cache(maxsize=4)
|
| 45 |
+
def at_sha(repo: str, sha: str) -> Predictor:
|
| 46 |
+
if has_file(repo, "model_head.pkl", sha):
|
| 47 |
+
return setfit(repo, sha)
|
| 48 |
+
return automodel(repo, sha)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def load(repo: str, revision: str) -> tuple[Predictor, str]:
|
| 52 |
+
sha = resolve(repo, revision)
|
| 53 |
+
return at_sha(repo, sha), sha
|
app/ui.py
CHANGED
|
@@ -2,8 +2,8 @@ from pathlib import Path
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
-
from .config import display
|
| 6 |
-
from .handlers import EXAMPLES, classify, evaluate,
|
| 7 |
from .hub import model_repos, revisions
|
| 8 |
from .text import HEADER, PLACEHOLDER, TRANSCRIPT_INFO
|
| 9 |
|
|
@@ -78,7 +78,7 @@ def build() -> gr.Blocks:
|
|
| 78 |
gr.Markdown(HEADER)
|
| 79 |
|
| 80 |
with gr.Row():
|
| 81 |
-
model, revision,
|
| 82 |
selection = [model, revision]
|
| 83 |
|
| 84 |
with gr.Tab("Classer"):
|
|
@@ -87,8 +87,10 @@ def build() -> gr.Blocks:
|
|
| 87 |
with gr.Tab("Évaluation"):
|
| 88 |
evaluate_button, report, progress_target = evaluation_tab()
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
model.change(pick_revision, model, revision)
|
| 91 |
-
refresh.click(pick_model, None, model)
|
| 92 |
gr.on(
|
| 93 |
[run.click, transcript.submit],
|
| 94 |
classify,
|
|
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
+
from .config import REFRESH_SECONDS, display
|
| 6 |
+
from .handlers import EXAMPLES, classify, evaluate, pick_revision, refresh
|
| 7 |
from .hub import model_repos, revisions
|
| 8 |
from .text import HEADER, PLACEHOLDER, TRANSCRIPT_INFO
|
| 9 |
|
|
|
|
| 78 |
gr.Markdown(HEADER)
|
| 79 |
|
| 80 |
with gr.Row():
|
| 81 |
+
model, revision, refresh_button = selectors()
|
| 82 |
selection = [model, revision]
|
| 83 |
|
| 84 |
with gr.Tab("Classer"):
|
|
|
|
| 87 |
with gr.Tab("Évaluation"):
|
| 88 |
evaluate_button, report, progress_target = evaluation_tab()
|
| 89 |
|
| 90 |
+
timer = gr.Timer(REFRESH_SECONDS)
|
| 91 |
+
timer.tick(refresh, selection, selection, show_progress="hidden")
|
| 92 |
+
refresh_button.click(refresh, selection, selection)
|
| 93 |
model.change(pick_revision, model, revision)
|
|
|
|
| 94 |
gr.on(
|
| 95 |
[run.click, transcript.submit],
|
| 96 |
classify,
|