enzofrnt HilarionLefuneste Cursor commited on
Commit
36a680b
·
1 Parent(s): 300ec53

feat: charge labeled_emails@main par défaut et finalise model card v2

Browse files

- Ajoute mailsort.dataset (révision main = dernière version)
- eval utilise FlowRank/mailSort/model par défaut
- README model card HF (20 labels, accuracy 96%)

Co-authored-by: Hilarion Lefuneste <hilarionlefuneste@tutamail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (4) hide show
  1. README.md +57 -72
  2. src/mailsort/dataset.py +21 -0
  3. src/mailsort/eval.py +15 -11
  4. src/mailsort/train.py +6 -12
README.md CHANGED
@@ -6,118 +6,103 @@ tags:
6
  - text-classification
7
  language:
8
  - en
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
  ## mailSort
12
 
13
- Repo minimal (Python) pour **entraîner, évaluer et publier** un modèle de classification multi-classes d’e-mails à partir du dataset Hugging Face [`FlowRank/labeled_emails`](https://huggingface.co/datasets/FlowRank/labeled_emails).
14
 
15
- **v2** (branche `main`, tag `v2.0`) : **20 classes** (catégorie + urgence), entraîné sur `FlowRank/labeled_emails@main`.
16
- **v1** (tag `v1.0`) : 10 classes — `revision="v1.0"` pour le dataset et le modèle.
17
 
18
- Le script principal est `mailsort.train` (Transformers `Trainer`).
 
 
 
19
 
20
- ### Prérequis
21
 
22
- - Python géré par `uv` (ce repo est prévu pour être lancé avec `uv run`)
23
- - (Optionnel) un GPU CUDA si tu veux accélérer l’entraînement
24
- - (Optionnel) un token HF si tu veux publier sur le Hub
25
 
26
- ### Installation
27
-
28
- `uv` installe/synchronise automatiquement les dépendances la première fois.
29
 
30
- ```bash
31
- uv sync
32
- ```
33
-
34
- ### Entraîner + évaluer (train + test)
35
 
36
- Par défaut, le script charge le dataset **depuis le Hub** et utilise ses splits `train` et `test`.
37
- L’évaluation se fait à chaque epoch, puis une évaluation finale est exécutée à la fin.
38
- Les artefacts (modèle, tokenizer) sont sauvegardés dans `outputs/` (ou le dossier passé via `--output-dir`).
39
 
40
- ```bash
41
- uv run python -m mailsort.train \
42
- --dataset-id FlowRank/labeled_emails \
43
- --model-name distilbert-base-uncased \
44
- --hub-model-id FlowRank/mailSort \
45
- --num-train-epochs 2
46
  ```
47
 
48
- Pour réentraîner sur une révision précise du dataset (ex. avant merge) :
49
 
50
- ```bash
51
- uv run python -m mailsort.train \
52
- --dataset-id FlowRank/labeled_emails \
53
- --dataset-revision enrichment \
54
- --num-train-epochs 2
55
- ```
56
 
57
- ### Tester / évaluer uniquement
58
 
59
- Le script n’a pas (encore) de mode “eval-only”.
60
- Le **minimum** pour faire uniquement une passe rapide est de mettre `--num-train-epochs 0` (ce qui évite l’entraînement) et de garder la phase `evaluate`.
61
 
62
  ```bash
63
- uv run python -m mailsort.train --num-train-epochs 0
64
  ```
65
 
66
- ### Évaluer sur le split `test` du dataset (recommandé)
67
-
68
- Après ton entraînement dans `outputs/`, tu peux évaluer proprement sur le **split `test`** de `FlowRank/labeled_emails` :
69
 
70
  ```bash
71
- uv run python -m mailsort.eval --model outputs --dataset-id FlowRank/labeled_emails --split test
 
 
 
72
  ```
73
 
74
- (Optionnel) Pour un test rapide :
75
 
76
  ```bash
77
- uv run python -m mailsort.eval --model outputs --split test --max-samples 200
78
  ```
79
 
80
- ### Publier sur le Hub (FlowRank/mailSort)
81
-
82
- Le push se fait automatiquement si la variable d’environnement `HF_TOKEN` (ou `HUGGINGFACE_HUB_TOKEN`) est définie.
83
 
84
  ```bash
85
- export HF_TOKEN="..."
86
- uv run python -m mailsort.train --hub-model-id FlowRank/mailSort
87
  ```
88
 
89
- ### Publier via Git (README à la racine + artefacts dans `model/`)
90
 
91
- Pour avoir un repo Hugging Face “complet” (doc + poids) tout en gardant une structure propre, on met :
92
-
93
- - `README.md` à la racine (documentation + model card)
94
- - les artefacts (config, poids, tokenizer) dans `model/`
95
-
96
- Hugging Face pourra charger le modèle via `subfolder="model"`.
97
-
98
- 1) Préparer le dossier `model/` à partir de `outputs/` :
99
 
100
  ```bash
101
  uv run python -m mailsort.prepare_model --outputs-dir outputs --model-dir model
102
- ```
103
-
104
- 2) Commit + push vers le repo Hugging Face `FlowRank/mailSort` :
105
-
106
- ```bash
107
  git add README.md model
108
- git commit -m "Add model artifacts under model/ + docs"
109
  git push
110
  ```
111
 
112
- ### Inférence (utiliser le modèle publié)
113
-
114
- ```python
115
- from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
116
 
117
- tok = AutoTokenizer.from_pretrained("FlowRank/mailSort", subfolder="model")
118
- model = AutoModelForSequenceClassification.from_pretrained("FlowRank/mailSort", subfolder="model")
119
- clf = pipeline("text-classification", model=model, tokenizer=tok, truncation=True)
120
- text = "Subject: Insurance claim\n\nBody: Hello, I need to update my policy..."
121
- print(clf(text))
122
  ```
123
-
 
6
  - text-classification
7
  language:
8
  - en
9
+ datasets:
10
+ - FlowRank/labeled_emails
11
+ base_model: distilbert-base-uncased
12
+ license: mit
13
+ metrics:
14
+ - accuracy
15
+ model-index:
16
+ - name: mailSort
17
+ results:
18
+ - task:
19
+ type: text-classification
20
+ dataset:
21
+ name: FlowRank/labeled_emails
22
+ type: FlowRank/labeled_emails
23
+ metrics:
24
+ - type: accuracy
25
+ value: 0.9601
26
+ widget:
27
+ - text: "Subject: Insurance claim\n\nBody: Hello, I need to update my policy urgently before the deadline."
28
  ---
29
 
30
  ## mailSort
31
 
32
+ Classifieur d’e-mails **20 classes** (catégorie + urgence), basé sur DistilBERT.
33
 
34
+ Entraîné sur [`FlowRank/labeled_emails`](https://huggingface.co/datasets/FlowRank/labeled_emails) (révision `main`, ~19,8k exemples).
35
+ **Accuracy test : 96,01 %** (1 900 / 1 979).
36
 
37
+ | Version | Labels | Révision dataset | Révision modèle |
38
+ |---------|--------|------------------|-----------------|
39
+ | **v2** (courante) | 20 | `main` ou `v2.0` | `main` ou `v2.0` |
40
+ | v1 | 10 | `v1.0` | `v1.0` |
41
 
42
+ ### Labels (v2)
43
 
44
+ Chaque catégorie est déclinée en `_urgent` / `_not_urgent` : `family`, `finance`, `games`, `human resources`, `medical`, `pets`, `school`, `software engineering`, `sport`, `work/airbus`.
 
 
45
 
46
+ ## Inférence
 
 
47
 
48
+ ```python
49
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
 
 
 
50
 
51
+ tok = AutoTokenizer.from_pretrained("FlowRank/mailSort", subfolder="model")
52
+ model = AutoModelForSequenceClassification.from_pretrained("FlowRank/mailSort", subfolder="model")
53
+ clf = pipeline("text-classification", model=model, tokenizer=tok, truncation=True)
54
 
55
+ text = "Subject: Insurance claim\n\nBody: I need to update my policy urgently."
56
+ print(clf(text))
 
 
 
 
57
  ```
58
 
59
+ ## Développement local
60
 
61
+ Repo Python minimal pour entraîner, évaluer et publier. Le script principal est `mailsort.train`.
 
 
 
 
 
62
 
63
+ Par défaut, le code charge la **dernière version du dataset** (`FlowRank/labeled_emails@main`).
64
 
65
+ ### Installation
 
66
 
67
  ```bash
68
+ uv sync
69
  ```
70
 
71
+ ### Entraîner + évaluer
 
 
72
 
73
  ```bash
74
+ uv run python -m mailsort.train \
75
+ --model-name distilbert-base-uncased \
76
+ --hub-model-id FlowRank/mailSort \
77
+ --num-train-epochs 2
78
  ```
79
 
80
+ Pour épingler une version précise du dataset :
81
 
82
  ```bash
83
+ uv run python -m mailsort.train --dataset-revision v1.0
84
  ```
85
 
86
+ ### Évaluer le modèle publié
 
 
87
 
88
  ```bash
89
+ uv run python -m mailsort.eval --split test
 
90
  ```
91
 
92
+ Évalue par défaut `FlowRank/mailSort` (sous-dossier `model/`) sur `FlowRank/labeled_emails@main`.
93
 
94
+ ### Publier via Git (README + `model/`)
 
 
 
 
 
 
 
95
 
96
  ```bash
97
  uv run python -m mailsort.prepare_model --outputs-dir outputs --model-dir model
 
 
 
 
 
98
  git add README.md model
99
+ git commit -m "chore(model): update published artifacts"
100
  git push
101
  ```
102
 
103
+ Le push Hub automatique via `Trainer` nécessite `HF_TOKEN` :
 
 
 
104
 
105
+ ```bash
106
+ export HF_TOKEN="..."
107
+ uv run python -m mailsort.train --hub-model-id FlowRank/mailSort
 
 
108
  ```
 
src/mailsort/dataset.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datasets import DatasetDict, load_dataset
4
+
5
+ DEFAULT_DATASET_ID = "FlowRank/labeled_emails"
6
+ DEFAULT_DATASET_REVISION = "main"
7
+
8
+
9
+ def load_labeled_emails(
10
+ dataset_id: str = DEFAULT_DATASET_ID,
11
+ revision: str | None = None,
12
+ *,
13
+ seed: int = 42,
14
+ ) -> DatasetDict:
15
+ """Charge le dataset labeled_emails (révision `main` = dernière version publiée)."""
16
+ ds = load_dataset(dataset_id, revision=revision or DEFAULT_DATASET_REVISION)
17
+ if "train" in ds and "test" in ds:
18
+ return ds
19
+ if "train" in ds and "test" not in ds:
20
+ return ds["train"].train_test_split(test_size=0.1, seed=seed)
21
+ return ds
src/mailsort/eval.py CHANGED
@@ -4,9 +4,10 @@ import argparse
4
  from collections import Counter, defaultdict
5
  from dataclasses import dataclass
6
 
7
- from datasets import load_dataset
8
  from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
9
 
 
 
10
 
11
  @dataclass(frozen=True)
12
  class Config:
@@ -28,14 +29,18 @@ def _build_text(subject: str, body: str) -> str:
28
 
29
  def _parse_args() -> Config:
30
  p = argparse.ArgumentParser(description="Evaluate a model (local or Hub) against a HF dataset split.")
31
- p.add_argument("--dataset-id", default="FlowRank/labeled_emails")
32
  p.add_argument(
33
  "--dataset-revision",
34
  default=None,
35
- help="Git revision of the dataset repo (branch, tag, or commit).",
 
 
 
 
 
36
  )
37
- p.add_argument("--model", default="outputs", help="Local path OR Hugging Face repo id (e.g. FlowRank/mailSort).")
38
- p.add_argument("--subfolder", default=None, help="Optional subfolder (e.g. model).")
39
  p.add_argument("--split", default="test", help="Which split to evaluate (e.g. test).")
40
  p.add_argument("--max-samples", type=int, default=None, help="Limit evaluation to N samples.")
41
  a = p.parse_args()
@@ -52,8 +57,7 @@ def _parse_args() -> Config:
52
  def main() -> int:
53
  cfg = _parse_args()
54
 
55
- kwargs = {"revision": cfg.dataset_revision} if cfg.dataset_revision else {}
56
- ds = load_dataset(cfg.dataset_id, **kwargs)
57
  if cfg.split not in ds:
58
  raise SystemExit(f"Split '{cfg.split}' not found. Available: {list(ds.keys())}")
59
 
@@ -61,12 +65,12 @@ def main() -> int:
61
  if cfg.max_samples is not None:
62
  rows = rows.select(range(min(cfg.max_samples, len(rows))))
63
 
64
- kwargs = {}
65
  if cfg.subfolder:
66
- kwargs["subfolder"] = cfg.subfolder
67
 
68
- tokenizer = AutoTokenizer.from_pretrained(cfg.model_id_or_path, **kwargs)
69
- model = AutoModelForSequenceClassification.from_pretrained(cfg.model_id_or_path, **kwargs)
70
  clf = pipeline(
71
  "text-classification",
72
  model=model,
 
4
  from collections import Counter, defaultdict
5
  from dataclasses import dataclass
6
 
 
7
  from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
8
 
9
+ from mailsort.dataset import DEFAULT_DATASET_ID, DEFAULT_DATASET_REVISION, load_labeled_emails
10
+
11
 
12
  @dataclass(frozen=True)
13
  class Config:
 
29
 
30
  def _parse_args() -> Config:
31
  p = argparse.ArgumentParser(description="Evaluate a model (local or Hub) against a HF dataset split.")
32
+ p.add_argument("--dataset-id", default=DEFAULT_DATASET_ID)
33
  p.add_argument(
34
  "--dataset-revision",
35
  default=None,
36
+ help=f"Révision Git du dataset (défaut: {DEFAULT_DATASET_REVISION} = dernière version).",
37
+ )
38
+ p.add_argument(
39
+ "--model",
40
+ default="FlowRank/mailSort",
41
+ help="Chemin local OU repo Hugging Face (ex. FlowRank/mailSort).",
42
  )
43
+ p.add_argument("--subfolder", default="model", help="Sous-dossier des artefacts (ex. model).")
 
44
  p.add_argument("--split", default="test", help="Which split to evaluate (e.g. test).")
45
  p.add_argument("--max-samples", type=int, default=None, help="Limit evaluation to N samples.")
46
  a = p.parse_args()
 
57
  def main() -> int:
58
  cfg = _parse_args()
59
 
60
+ ds = load_labeled_emails(cfg.dataset_id, revision=cfg.dataset_revision)
 
61
  if cfg.split not in ds:
62
  raise SystemExit(f"Split '{cfg.split}' not found. Available: {list(ds.keys())}")
63
 
 
65
  if cfg.max_samples is not None:
66
  rows = rows.select(range(min(cfg.max_samples, len(rows))))
67
 
68
+ model_kwargs = {}
69
  if cfg.subfolder:
70
+ model_kwargs["subfolder"] = cfg.subfolder
71
 
72
+ tokenizer = AutoTokenizer.from_pretrained(cfg.model_id_or_path, **model_kwargs)
73
+ model = AutoModelForSequenceClassification.from_pretrained(cfg.model_id_or_path, **model_kwargs)
74
  clf = pipeline(
75
  "text-classification",
76
  model=model,
src/mailsort/train.py CHANGED
@@ -5,7 +5,7 @@ import os
5
  from dataclasses import dataclass
6
 
7
  import numpy as np
8
- from datasets import DatasetDict, load_dataset
9
  from transformers import (
10
  AutoModelForSequenceClassification,
11
  AutoTokenizer,
@@ -14,6 +14,8 @@ from transformers import (
14
  TrainingArguments,
15
  )
16
 
 
 
17
 
18
  @dataclass(frozen=True)
19
  class Config:
@@ -41,11 +43,11 @@ def _build_text(subject: str, body: str) -> str:
41
 
42
  def _parse_args() -> Config:
43
  p = argparse.ArgumentParser(description="Train & push email classifier to Hugging Face Hub.")
44
- p.add_argument("--dataset-id", default="FlowRank/labeled_emails")
45
  p.add_argument(
46
  "--dataset-revision",
47
  default=None,
48
- help="Git revision of the dataset repo (branch, tag, or commit).",
49
  )
50
  p.add_argument("--model-name", default="distilbert-base-uncased")
51
  p.add_argument("--hub-model-id", default="FlowRank/mailSort")
@@ -75,15 +77,7 @@ def _parse_args() -> Config:
75
 
76
 
77
  def _load_ds(dataset_id: str, seed: int, revision: str | None = None) -> DatasetDict:
78
- kwargs = {"revision": revision} if revision else {}
79
- ds = load_dataset(dataset_id, **kwargs)
80
- if "train" in ds and "test" in ds:
81
- return ds # already split
82
- # fallback: split if only a single split exists
83
- if "train" in ds and "test" not in ds:
84
- return ds["train"].train_test_split(test_size=0.1, seed=seed)
85
- # if weird structure, just return as-is and let Trainer fail loudly
86
- return ds
87
 
88
 
89
  def _prepare(ds: DatasetDict, tokenizer: AutoTokenizer, label2id: dict[str, int], max_length: int) -> DatasetDict:
 
5
  from dataclasses import dataclass
6
 
7
  import numpy as np
8
+ from datasets import DatasetDict
9
  from transformers import (
10
  AutoModelForSequenceClassification,
11
  AutoTokenizer,
 
14
  TrainingArguments,
15
  )
16
 
17
+ from mailsort.dataset import DEFAULT_DATASET_ID, DEFAULT_DATASET_REVISION, load_labeled_emails
18
+
19
 
20
  @dataclass(frozen=True)
21
  class Config:
 
43
 
44
  def _parse_args() -> Config:
45
  p = argparse.ArgumentParser(description="Train & push email classifier to Hugging Face Hub.")
46
+ p.add_argument("--dataset-id", default=DEFAULT_DATASET_ID)
47
  p.add_argument(
48
  "--dataset-revision",
49
  default=None,
50
+ help=f"Révision Git du dataset (défaut: {DEFAULT_DATASET_REVISION} = dernière version).",
51
  )
52
  p.add_argument("--model-name", default="distilbert-base-uncased")
53
  p.add_argument("--hub-model-id", default="FlowRank/mailSort")
 
77
 
78
 
79
  def _load_ds(dataset_id: str, seed: int, revision: str | None = None) -> DatasetDict:
80
+ return load_labeled_emails(dataset_id=dataset_id, revision=revision, seed=seed)
 
 
 
 
 
 
 
 
81
 
82
 
83
  def _prepare(ds: DatasetDict, tokenizer: AutoTokenizer, label2id: dict[str, int], max_length: int) -> DatasetDict: