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 files

load(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>

Files changed (7) hide show
  1. README.md +15 -0
  2. app/config.py +1 -0
  3. app/evaluation.py +33 -14
  4. app/handlers.py +18 -8
  5. app/hub.py +10 -0
  6. app/predictors.py +10 -5
  7. 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
- revision: str
 
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=1)
28
- def test_split() -> tuple[list[str], list[str], list[str]]:
29
- data = load_dataset(DATASET_REPO, split="test", token=TOKEN)
30
  names = data.features["label"].names
31
- return list(data["id"]), list(data["text"]), [names[i] for i in data["label"]]
 
 
 
 
 
 
 
 
 
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 is not None else load(repo, revision)
81
- ids, texts, gold = test_split()
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
- revision=revision,
91
- hits=sum(want == got for want, got in zip(gold, predicted)),
92
- total=len(gold),
93
- ms_per_case=elapsed * 1000 / len(texts),
94
- confusion=confusion(labels, gold, predicted),
95
- cases=cases(ids, texts, gold, predicted, probabilities),
 
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
- labels, probabilities = load(repo, revision)([text])
 
26
  scores = {display(name): float(p) for name, p in zip(labels, probabilities[0])}
27
- return scores, text, f"`{(time.perf_counter() - started) * 1000:.0f} ms`"
 
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}` · `{report.revision}` · {report.ms_per_case:.0f} ms par cas"
 
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, revision, predict=predictor, track=track)
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 pick_model() -> gr.Dropdown:
55
- choices = model_repos()
56
- return gr.Dropdown(choices=choices, value=choices[0] if choices else None)
 
 
 
 
 
 
 
 
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 load(repo: str, revision: str) -> Predictor:
46
- if has_file(repo, "model_head.pkl", revision):
47
- return setfit(repo, revision)
48
- return automodel(repo, revision)
 
 
 
 
 
 
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, pick_model, pick_revision
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, refresh = selectors()
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,