tinydistill 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tinydistill-0.3.2 → tinydistill-0.3.3}/PKG-INFO +49 -3
- {tinydistill-0.3.2 → tinydistill-0.3.3}/README.md +45 -2
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/run.sh +7 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/pyproject.toml +3 -2
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/__init__.py +1 -1
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/analyse.py +23 -11
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/cli.py +35 -10
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/evaluate.py +12 -1
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/export.py +8 -3
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/hub.py +7 -0
- tinydistill-0.3.3/src/tinydistill/plots.py +326 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/student.py +26 -9
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_cli.py +18 -1
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_export.py +16 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_loss.py +2 -1
- {tinydistill-0.3.2 → tinydistill-0.3.3}/.gitignore +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/LICENSE +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-1.5b-8shot.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-1.5b-8shot.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-1.5b.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-1.5b.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-7b-8shot.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-7b-8shot.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-7b.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/banking77-7b.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L12-v2-20-epochs.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L12-v2.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L6-v2-10-epochs.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L6-v2-20-epochs.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-mpnet-base-v2.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/bge-small-en-v1.5.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/static.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/task.yaml +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/teachers.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b-8shot.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b-8shot.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-7b-20-epochs.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-7b-20-epochs.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-7b.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-7b.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-ensemble.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-ensemble.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-synthetic.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-synthetic.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/run.sh +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/task.yaml +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/teachers.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/go-emotions/go-emotions-1.5b.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/go-emotions/go-emotions-1.5b.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/go-emotions/run.sh +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/go-emotions/task.yaml +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/headline.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/update_readme.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/vllm-smoke.json +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/vllm-smoke.md +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/examples/vllm-smoke.sh +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/data.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/progress.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/serve.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/shortlist.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/static.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/suggest.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/synth.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/task.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/teacher.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/tune.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/src/tinydistill/web.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/browser/build_tiny_site.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/browser/check_page.mjs +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/conftest.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_cache.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_progress.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_scoring.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_serve.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_task.py +0 -0
- {tinydistill-0.3.2 → tinydistill-0.3.3}/tests/test_vllm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tinydistill
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Distil an LLM classification prompt into a small, fast model that runs on a CPU.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Andreas-Hum/tinydistill
|
|
6
6
|
Author: Andreas-Hum
|
|
@@ -28,10 +28,13 @@ Requires-Dist: bitsandbytes>=0.43; extra == '4bit'
|
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
30
30
|
Requires-Dist: datasets>=2.18; extra == 'dev'
|
|
31
|
+
Requires-Dist: matplotlib>=3.7; extra == 'dev'
|
|
31
32
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
33
|
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
34
|
Provides-Extra: hf
|
|
34
35
|
Requires-Dist: datasets>=2.18; extra == 'hf'
|
|
36
|
+
Provides-Extra: plot
|
|
37
|
+
Requires-Dist: matplotlib>=3.7; extra == 'plot'
|
|
35
38
|
Description-Content-Type: text/markdown
|
|
36
39
|
|
|
37
40
|
# tinydistill
|
|
@@ -86,6 +89,7 @@ browser and shows what happens inside it.</i></p>
|
|
|
86
89
|
pip install tinydistill # label, train, export, predict
|
|
87
90
|
pip install "tinydistill[hf]" # + Hugging Face datasets as input (hf:<dataset>:<split>)
|
|
88
91
|
pip install "tinydistill[hf,4bit]" # + 4-bit teachers with bitsandbytes (CUDA)
|
|
92
|
+
pip install "tinydistill[plot]" # + charts of a run (matplotlib)
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
Python 3.11 or newer, on Linux, macOS or Windows. Labelling with a large teacher wants a GPU; training and
|
|
@@ -94,6 +98,19 @@ inference are fine on a CPU. For a CPU-only PyTorch, install it first:
|
|
|
94
98
|
|
|
95
99
|
## Quickstart
|
|
96
100
|
|
|
101
|
+
To try a distilled model before training your own, the Banking77 student from the [results](#results) is on
|
|
102
|
+
the [Hugging Face Hub](https://huggingface.co/Shroomy/tinydistill-banking77-minilm):
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from tinydistill import Predictor # pip install tinydistill
|
|
106
|
+
|
|
107
|
+
clf = Predictor("hf:Shroomy/tinydistill-banking77-minilm")
|
|
108
|
+
clf.describe(["I still haven't received my new card", "My card got stolen"])
|
|
109
|
+
# [{'label': 'card_arrival', 'confidence': 0.4672}, {'label': 'lost_or_stolen_card', 'confidence': 0.9921}]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To distil your own:
|
|
113
|
+
|
|
97
114
|
```sh
|
|
98
115
|
curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
|
|
99
116
|
tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
|
|
@@ -110,8 +127,13 @@ clf = Predictor("runs/banking77/onnx") # or a model on the Hub: Predictor("hf:u
|
|
|
110
127
|
clf.predict(["I still haven't received my new card"]) # a list of label names
|
|
111
128
|
```
|
|
112
129
|
|
|
113
|
-
Or try it without installing anything
|
|
114
|
-
|
|
130
|
+
Or try it without installing anything, on a free Colab GPU:
|
|
131
|
+
|
|
132
|
+
| Notebook | What it shows |
|
|
133
|
+
|---|---|
|
|
134
|
+
| [Quickstart](https://colab.research.google.com/github/Andreas-Hum/tinydistill/blob/main/notebooks/quickstart.ipynb) | distil a sentiment classifier in about ten minutes, with charts of what the student learned |
|
|
135
|
+
| [Your own task](https://colab.research.google.com/github/Andreas-Hum/tinydistill/blob/main/notebooks/your_task.ipynb) | write a task for your labels, check the teacher, distil on your texts, find the mistakes, download the model |
|
|
136
|
+
| [Deploy](https://colab.research.google.com/github/Andreas-Hum/tinydistill/blob/main/notebooks/deploy.ipynb) | the model from Python, behind an HTTP server, in a web page, and on the Hugging Face Hub |
|
|
115
137
|
|
|
116
138
|
Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
|
|
117
139
|
`--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
|
|
@@ -299,6 +321,30 @@ results show the mean and standard deviation.
|
|
|
299
321
|
adds them to the training texts and trains, exports and evaluates a new student on the same test set, so
|
|
300
322
|
the student learns the inputs it found hard in production.
|
|
301
323
|
|
|
324
|
+
### Charts and metrics
|
|
325
|
+
|
|
326
|
+
`tinydistill plot RUN` (with the `plot` extra) writes the charts of a run to `RUN/plots/`; in Python every
|
|
327
|
+
chart is a function in `tinydistill.plots` that returns a matplotlib figure, so it shows inline in a notebook:
|
|
328
|
+
|
|
329
|
+
| Chart | What it shows |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `soft_labels` | for a few test texts, the teacher's probabilities (the soft labels) next to the student's |
|
|
332
|
+
| `speed_accuracy` | accuracy against latency for the teacher and every form of the student, with the baseline and upper bound |
|
|
333
|
+
| `training` | the loss per epoch on the training texts and, with `--val-share`, on the held-out texts and the kept epoch |
|
|
334
|
+
| `per_label` | F1 of every label for teacher and student, the teacher's weakest first |
|
|
335
|
+
| `confusions` | the confusion matrix over the labels involved in the most mistakes |
|
|
336
|
+
| `cascade` | accuracy as the student passes more of its least confident texts to the teacher |
|
|
337
|
+
|
|
338
|
+
<p align="center">
|
|
339
|
+
<img alt="Soft labels on three Banking77 test texts: the teacher's probabilities next to the student's" src="https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/docs/images/banking77-soft-labels.png" width="49%">
|
|
340
|
+
<img alt="Accuracy against latency on Banking77: the students on a CPU, the teacher on a GPU, the baseline and the upper bound" src="https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/docs/images/banking77-speed-accuracy.png" width="49%">
|
|
341
|
+
</p>
|
|
342
|
+
|
|
343
|
+
Every run's results report accuracy and macro-F1, agreement with the teacher, latency at batch 1 and 32,
|
|
344
|
+
throughput and size; tasks with more than five labels also get top-3 accuracy, multi-label tasks micro-F1, and
|
|
345
|
+
ordered labels the mean distance in label steps. `tinydistill analyse RUN` adds a page of per-label scores and
|
|
346
|
+
example mistakes.
|
|
347
|
+
|
|
302
348
|
### From Python
|
|
303
349
|
|
|
304
350
|
The exported model:
|
|
@@ -50,6 +50,7 @@ browser and shows what happens inside it.</i></p>
|
|
|
50
50
|
pip install tinydistill # label, train, export, predict
|
|
51
51
|
pip install "tinydistill[hf]" # + Hugging Face datasets as input (hf:<dataset>:<split>)
|
|
52
52
|
pip install "tinydistill[hf,4bit]" # + 4-bit teachers with bitsandbytes (CUDA)
|
|
53
|
+
pip install "tinydistill[plot]" # + charts of a run (matplotlib)
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
Python 3.11 or newer, on Linux, macOS or Windows. Labelling with a large teacher wants a GPU; training and
|
|
@@ -58,6 +59,19 @@ inference are fine on a CPU. For a CPU-only PyTorch, install it first:
|
|
|
58
59
|
|
|
59
60
|
## Quickstart
|
|
60
61
|
|
|
62
|
+
To try a distilled model before training your own, the Banking77 student from the [results](#results) is on
|
|
63
|
+
the [Hugging Face Hub](https://huggingface.co/Shroomy/tinydistill-banking77-minilm):
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from tinydistill import Predictor # pip install tinydistill
|
|
67
|
+
|
|
68
|
+
clf = Predictor("hf:Shroomy/tinydistill-banking77-minilm")
|
|
69
|
+
clf.describe(["I still haven't received my new card", "My card got stolen"])
|
|
70
|
+
# [{'label': 'card_arrival', 'confidence': 0.4672}, {'label': 'lost_or_stolen_card', 'confidence': 0.9921}]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
To distil your own:
|
|
74
|
+
|
|
61
75
|
```sh
|
|
62
76
|
curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
|
|
63
77
|
tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
|
|
@@ -74,8 +88,13 @@ clf = Predictor("runs/banking77/onnx") # or a model on the Hub: Predictor("hf:u
|
|
|
74
88
|
clf.predict(["I still haven't received my new card"]) # a list of label names
|
|
75
89
|
```
|
|
76
90
|
|
|
77
|
-
Or try it without installing anything
|
|
78
|
-
|
|
91
|
+
Or try it without installing anything, on a free Colab GPU:
|
|
92
|
+
|
|
93
|
+
| Notebook | What it shows |
|
|
94
|
+
|---|---|
|
|
95
|
+
| [Quickstart](https://colab.research.google.com/github/Andreas-Hum/tinydistill/blob/main/notebooks/quickstart.ipynb) | distil a sentiment classifier in about ten minutes, with charts of what the student learned |
|
|
96
|
+
| [Your own task](https://colab.research.google.com/github/Andreas-Hum/tinydistill/blob/main/notebooks/your_task.ipynb) | write a task for your labels, check the teacher, distil on your texts, find the mistakes, download the model |
|
|
97
|
+
| [Deploy](https://colab.research.google.com/github/Andreas-Hum/tinydistill/blob/main/notebooks/deploy.ipynb) | the model from Python, behind an HTTP server, in a web page, and on the Hugging Face Hub |
|
|
79
98
|
|
|
80
99
|
Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
|
|
81
100
|
`--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
|
|
@@ -263,6 +282,30 @@ results show the mean and standard deviation.
|
|
|
263
282
|
adds them to the training texts and trains, exports and evaluates a new student on the same test set, so
|
|
264
283
|
the student learns the inputs it found hard in production.
|
|
265
284
|
|
|
285
|
+
### Charts and metrics
|
|
286
|
+
|
|
287
|
+
`tinydistill plot RUN` (with the `plot` extra) writes the charts of a run to `RUN/plots/`; in Python every
|
|
288
|
+
chart is a function in `tinydistill.plots` that returns a matplotlib figure, so it shows inline in a notebook:
|
|
289
|
+
|
|
290
|
+
| Chart | What it shows |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `soft_labels` | for a few test texts, the teacher's probabilities (the soft labels) next to the student's |
|
|
293
|
+
| `speed_accuracy` | accuracy against latency for the teacher and every form of the student, with the baseline and upper bound |
|
|
294
|
+
| `training` | the loss per epoch on the training texts and, with `--val-share`, on the held-out texts and the kept epoch |
|
|
295
|
+
| `per_label` | F1 of every label for teacher and student, the teacher's weakest first |
|
|
296
|
+
| `confusions` | the confusion matrix over the labels involved in the most mistakes |
|
|
297
|
+
| `cascade` | accuracy as the student passes more of its least confident texts to the teacher |
|
|
298
|
+
|
|
299
|
+
<p align="center">
|
|
300
|
+
<img alt="Soft labels on three Banking77 test texts: the teacher's probabilities next to the student's" src="https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/docs/images/banking77-soft-labels.png" width="49%">
|
|
301
|
+
<img alt="Accuracy against latency on Banking77: the students on a CPU, the teacher on a GPU, the baseline and the upper bound" src="https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/docs/images/banking77-speed-accuracy.png" width="49%">
|
|
302
|
+
</p>
|
|
303
|
+
|
|
304
|
+
Every run's results report accuracy and macro-F1, agreement with the teacher, latency at batch 1 and 32,
|
|
305
|
+
throughput and size; tasks with more than five labels also get top-3 accuracy, multi-label tasks micro-F1, and
|
|
306
|
+
ordered labels the mean distance in label steps. `tinydistill analyse RUN` adds a page of per-label scores and
|
|
307
|
+
example mistakes.
|
|
308
|
+
|
|
266
309
|
### From Python
|
|
267
310
|
|
|
268
311
|
The exported model:
|
|
@@ -47,4 +47,11 @@ tinydistill compare runs/banking77-1.5b runs/banking77-1.5b-8shot runs/banking77
|
|
|
47
47
|
mkdir -p examples/banking77/students
|
|
48
48
|
for dir in runs/banking77-students/*/; do cp "$dir/results.json" "examples/banking77/students/$(basename "$dir").json"; done
|
|
49
49
|
tinydistill compare runs/banking77-7b-8shot runs/banking77-students/* > examples/banking77/students.md
|
|
50
|
+
python - <<'PY' # the charts in the README
|
|
51
|
+
from tinydistill import plots
|
|
52
|
+
plots.speed_accuracy("runs/banking77-7b-8shot").savefig("docs/images/banking77-speed-accuracy.png", dpi=130)
|
|
53
|
+
plots.soft_labels("runs/banking77-students/all-MiniLM-L6-v2-20-epochs").savefig(
|
|
54
|
+
"docs/images/banking77-soft-labels.png", dpi=130
|
|
55
|
+
)
|
|
56
|
+
PY
|
|
50
57
|
python examples/update_readme.py
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tinydistill"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Distil an LLM classification prompt into a small, fast model that runs on a CPU."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -34,7 +34,8 @@ dependencies = [
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
hf = ["datasets>=2.18"]
|
|
36
36
|
4bit = ["bitsandbytes>=0.43", "accelerate>=0.30"]
|
|
37
|
-
|
|
37
|
+
plot = ["matplotlib>=3.7"]
|
|
38
|
+
dev = ["pytest>=8", "ruff>=0.5", "datasets>=2.18", "build>=1.2", "matplotlib>=3.7"]
|
|
38
39
|
|
|
39
40
|
[project.scripts]
|
|
40
41
|
tinydistill = "tinydistill.cli:main"
|
|
@@ -53,30 +53,42 @@ CSS = """
|
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def
|
|
57
|
-
"""
|
|
56
|
+
def predictions(run: Path) -> dict:
|
|
57
|
+
"""For the gold-labelled test texts of a single-label run: the task, texts, gold labels, and the teacher's
|
|
58
|
+
and the deployed student's predicted labels."""
|
|
58
59
|
from .export import Predictor
|
|
59
60
|
from .task import load_task
|
|
60
61
|
|
|
61
62
|
task = load_task(run / "task.yaml")
|
|
62
63
|
if task.multi_label:
|
|
63
|
-
raise SystemExit("
|
|
64
|
+
raise SystemExit("this view is for single-label tasks; see results.md for multi-label runs")
|
|
64
65
|
rows = [
|
|
65
66
|
r
|
|
66
67
|
for r in map(json.loads, (run / "test.jsonl").read_text(encoding="utf-8").splitlines())
|
|
67
68
|
if r.get("gold") is not None
|
|
68
69
|
]
|
|
69
70
|
if not rows:
|
|
70
|
-
raise SystemExit(f"{run}/test.jsonl has no gold labels
|
|
71
|
-
names = task.labels
|
|
71
|
+
raise SystemExit(f"{run}/test.jsonl has no gold labels")
|
|
72
72
|
texts = [r["text"] for r in rows]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
n = len(task.labels)
|
|
74
|
+
return {
|
|
75
|
+
"task": task,
|
|
76
|
+
"texts": texts,
|
|
77
|
+
"gold": [r["gold"] for r in rows],
|
|
78
|
+
"teacher": [max(range(n), key=r["scores"].__getitem__) for r in rows],
|
|
79
|
+
"student": [int(i) for i in Predictor(run / "onnx").logits(texts).argmax(-1)],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build(run: Path, top: int = 15) -> tuple[str, str]:
|
|
84
|
+
"""(HTML page, short Markdown summary) for a single-label run with a gold-labelled test set."""
|
|
85
|
+
p = predictions(run)
|
|
86
|
+
task, texts, gold, teacher, student = p["task"], p["texts"], p["gold"], p["teacher"], p["student"]
|
|
87
|
+
names = task.labels
|
|
76
88
|
|
|
77
89
|
t_lab, s_lab = per_label(teacher, gold, len(names)), per_label(student, gold, len(names))
|
|
78
90
|
order = sorted(range(len(names)), key=lambda c: t_lab[c]["f1"]) # the teacher's weakest labels first
|
|
79
|
-
disagree = [i for i in range(len(
|
|
91
|
+
disagree = [i for i in range(len(texts)) if teacher[i] != student[i]]
|
|
80
92
|
t_acc = sum(t == g for t, g in zip(teacher, gold, strict=True)) / len(gold)
|
|
81
93
|
s_acc = sum(s == g for s, g in zip(student, gold, strict=True)) / len(gold)
|
|
82
94
|
e = html.escape
|
|
@@ -115,7 +127,7 @@ def build(run: Path, top: int = 15) -> tuple[str, str]:
|
|
|
115
127
|
<title>Error analysis: {e(task.name)}</title><style>{CSS}</style></head>
|
|
116
128
|
<body><main>
|
|
117
129
|
<h1>Error analysis: {e(task.name)}</h1>
|
|
118
|
-
<p class="sub">{len(
|
|
130
|
+
<p class="sub">{len(texts)} gold-labelled test texts. Teacher accuracy {t_acc:.3f}, student (deployed model)
|
|
119
131
|
accuracy {s_acc:.3f}; they disagree on {len(disagree)} texts. Written by <code>tinydistill analyse</code>.</p>
|
|
120
132
|
|
|
121
133
|
<h2>Per label, the teacher's weakest first</h2>
|
|
@@ -141,7 +153,7 @@ on the student side.</p>
|
|
|
141
153
|
worst = [names[c] for c in order[:5]]
|
|
142
154
|
pairs = Counter({(names[g], names[p]): n for g, p, n, _ in confusions(teacher, gold, 5)})
|
|
143
155
|
summary = (
|
|
144
|
-
f"Teacher accuracy {t_acc:.3f}, student {s_acc:.3f}; they disagree on {len(disagree)} of {len(
|
|
156
|
+
f"Teacher accuracy {t_acc:.3f}, student {s_acc:.3f}; they disagree on {len(disagree)} of {len(texts)} texts.\n"
|
|
145
157
|
f"The teacher's weakest labels: {', '.join(worst)}.\n"
|
|
146
158
|
"Its most common mistakes: "
|
|
147
159
|
+ "; ".join(f"{g} -> {p} ({n})" for (g, p), n in pairs.most_common(5))
|
|
@@ -358,6 +358,7 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
358
358
|
ordinal_metrics,
|
|
359
359
|
render,
|
|
360
360
|
time_calls,
|
|
361
|
+
top_k_accuracy,
|
|
361
362
|
)
|
|
362
363
|
from .export import Predictor
|
|
363
364
|
from .student import load_student, student_logits
|
|
@@ -386,7 +387,9 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
386
387
|
raise SystemExit("no texts to evaluate on: label a --test set (or training data) first")
|
|
387
388
|
threads = torch.get_num_threads()
|
|
388
389
|
|
|
389
|
-
def quality(pred: list) -> dict:
|
|
390
|
+
def quality(pred: list, scores: list | None = None) -> dict:
|
|
391
|
+
"""Metrics of predictions; with each text's label scores, also top-3 accuracy (tasks with more than
|
|
392
|
+
five labels)."""
|
|
390
393
|
if not gold:
|
|
391
394
|
return {}
|
|
392
395
|
if multi:
|
|
@@ -394,6 +397,8 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
394
397
|
"agreement": accuracy(pred, teacher_pred)
|
|
395
398
|
}
|
|
396
399
|
extra = ordinal_metrics(pred, gold) if task.ordinal else {}
|
|
400
|
+
if scores is not None and len(task.labels) > 5:
|
|
401
|
+
extra["top3_accuracy"] = top_k_accuracy(scores, gold)
|
|
397
402
|
return extra | {
|
|
398
403
|
"accuracy": accuracy(pred, gold),
|
|
399
404
|
"macro_f1": macro_f1(pred, gold),
|
|
@@ -418,8 +423,8 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
418
423
|
examples = [(r["text"], r["gold"]) for r in _read_jsonl(run / "examples.jsonl")] or None
|
|
419
424
|
members: list[dict] = []
|
|
420
425
|
for ti, name in enumerate(teachers):
|
|
421
|
-
|
|
422
|
-
q = quality(
|
|
426
|
+
own_scores = [r["teacher_scores"][ti] if len(teachers) > 1 else r["scores"] for r in labelled]
|
|
427
|
+
q = quality([decide(s) for s in own_scores], own_scores)
|
|
423
428
|
q.pop("agreement", None)
|
|
424
429
|
for dev in devices:
|
|
425
430
|
label = f"teacher, {prompt}" + (f": {name.split('/')[-1]}" if len(teachers) > 1 else "")
|
|
@@ -463,7 +468,7 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
463
468
|
models.extend(members)
|
|
464
469
|
else: # the ensemble is the actual teacher: it runs every member, so its latency is their sum
|
|
465
470
|
models.extend(m | {"role": "teacher member"} for m in members)
|
|
466
|
-
q = quality(teacher_pred)
|
|
471
|
+
q = quality(teacher_pred, [r["scores"] for r in labelled])
|
|
467
472
|
q.pop("agreement", None)
|
|
468
473
|
for dev in devices:
|
|
469
474
|
on_dev = [m for m in members if m["device"] == dev]
|
|
@@ -493,9 +498,8 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
493
498
|
per_seed = []
|
|
494
499
|
for d in dirs:
|
|
495
500
|
model, tok = load_student(d, "cpu")
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
501
|
+
z = student_logits(model, tok, texts).tolist() if texts else []
|
|
502
|
+
per_seed.append(quality([decide(x) for x in z], z))
|
|
499
503
|
bar.update()
|
|
500
504
|
row = {"role": role, "name": row_name, "device": "cpu", **aggregate(per_seed)}
|
|
501
505
|
if role == "student": # timings and size of the shipped (first-seed) model
|
|
@@ -517,15 +521,19 @@ def cmd_eval(a: argparse.Namespace) -> None:
|
|
|
517
521
|
continue
|
|
518
522
|
log(f"evaluating {f.name} (onnxruntime, CPU)")
|
|
519
523
|
pr = Predictor(run / "onnx", int8=int8, threads=threads)
|
|
520
|
-
|
|
524
|
+
z = pr.logits(texts).tolist() if texts else []
|
|
525
|
+
pred = [decide(x) for x in z]
|
|
521
526
|
row = {
|
|
522
527
|
"role": "student",
|
|
523
528
|
"name": f"student, ONNX {'int8' if int8 else 'fp32'}",
|
|
524
529
|
"device": "cpu",
|
|
525
|
-
**quality(pred),
|
|
530
|
+
**quality(pred, z),
|
|
526
531
|
"size_mb": dir_size_mb([f]),
|
|
527
532
|
}
|
|
528
|
-
|
|
533
|
+
# timed on real batches (batch 32 = one onnxruntime call with 32 texts)
|
|
534
|
+
row.update(
|
|
535
|
+
bench(lambda b, pr=pr: pr.logits(b, batch_size=len(b)), bench_texts, a.reps, max(3, a.reps // 5))
|
|
536
|
+
)
|
|
529
537
|
models.append(row)
|
|
530
538
|
bar.update()
|
|
531
539
|
|
|
@@ -680,6 +688,16 @@ def cmd_serve(a: argparse.Namespace) -> None:
|
|
|
680
688
|
server.server_close()
|
|
681
689
|
|
|
682
690
|
|
|
691
|
+
def cmd_plot(a: argparse.Namespace) -> None:
|
|
692
|
+
from .plots import save_all
|
|
693
|
+
|
|
694
|
+
files = save_all(Path(a.run), a.out)
|
|
695
|
+
if not files:
|
|
696
|
+
raise SystemExit(f"{a.run} has no data to plot: run `tinydistill eval` first")
|
|
697
|
+
for f in files:
|
|
698
|
+
print(f)
|
|
699
|
+
|
|
700
|
+
|
|
683
701
|
def cmd_web(a: argparse.Namespace) -> None:
|
|
684
702
|
from .export import model_path
|
|
685
703
|
from .web import build_site
|
|
@@ -1044,6 +1062,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1044
1062
|
srv.add_argument("--threshold", type=float, default=0.5, help="with --cascade: see `predict --help`")
|
|
1045
1063
|
srv.add_argument("--log", help="with --cascade: append the texts the teacher answered (for `refresh`)")
|
|
1046
1064
|
srv.set_defaults(fn=cmd_serve)
|
|
1065
|
+
plo = sub.add_parser(
|
|
1066
|
+
"plot",
|
|
1067
|
+
help="charts of a run as PNG files: training, speed and accuracy, per label, confusions, cascade",
|
|
1068
|
+
)
|
|
1069
|
+
plo.add_argument("run", help="run directory")
|
|
1070
|
+
plo.add_argument("--out", help="folder for the PNG files (default: RUN/plots)")
|
|
1071
|
+
plo.set_defaults(fn=cmd_plot)
|
|
1047
1072
|
web = sub.add_parser("web", help="a static web page that runs the model in the browser")
|
|
1048
1073
|
web.add_argument("model", help="directory written by `export` (run/onnx), or hf:user/name on the Hub")
|
|
1049
1074
|
web.add_argument("--out", default="site", help="folder to write the page and model into")
|
|
@@ -87,6 +87,12 @@ def bench(fn: Callable[[list[str]], object], texts: list[str], reps1: int, reps3
|
|
|
87
87
|
return out
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def top_k_accuracy(scores: list[list[float]], gold: list[int], k: int = 3) -> float:
|
|
91
|
+
"""Share of texts whose gold label is among the k highest-scoring labels."""
|
|
92
|
+
hits = sum(g in sorted(range(len(s)), key=s.__getitem__)[-k:] for s, g in zip(scores, gold, strict=True))
|
|
93
|
+
return hits / len(gold) if gold else float("nan")
|
|
94
|
+
|
|
95
|
+
|
|
90
96
|
def aggregate(per_seed: list[dict]) -> dict:
|
|
91
97
|
"""Mean of each metric over seeds; with several seeds also the sample standard deviation and values."""
|
|
92
98
|
if not per_seed or not per_seed[0]:
|
|
@@ -141,7 +147,12 @@ def _pm(row: dict, key: str) -> str:
|
|
|
141
147
|
return _fmt(row.get(key), ".3f") + (f" ± {std:.3f}" if std is not None else "")
|
|
142
148
|
|
|
143
149
|
|
|
144
|
-
EXTRA_COLUMNS = (
|
|
150
|
+
EXTRA_COLUMNS = (
|
|
151
|
+
("top3_accuracy", "Top-3 accuracy"),
|
|
152
|
+
("micro_f1", "Micro-F1"),
|
|
153
|
+
("mae", "MAE (label steps)"),
|
|
154
|
+
("within_one", "Within one step"),
|
|
155
|
+
)
|
|
145
156
|
|
|
146
157
|
|
|
147
158
|
def markdown_table(results: dict) -> str:
|
|
@@ -178,8 +178,13 @@ class Predictor:
|
|
|
178
178
|
self.multi_label = config.get("problem_type") == "multi_label_classification"
|
|
179
179
|
self.max_length = max_length
|
|
180
180
|
self.file = file
|
|
181
|
+
self.int8 = int8
|
|
181
182
|
|
|
182
|
-
def logits(self, texts: list[str], batch_size: int =
|
|
183
|
+
def logits(self, texts: list[str], batch_size: int | None = None) -> np.ndarray:
|
|
184
|
+
"""Label scores per text. The int8 model runs one text at a time by default: its dynamic quantisation
|
|
185
|
+
scales activations over the whole batch, so in a batch a text's scores would depend on the other texts
|
|
186
|
+
(on a CPU this costs little throughput). The fp32 model runs 32 texts at a time."""
|
|
187
|
+
batch_size = batch_size or (1 if self.int8 else 32)
|
|
183
188
|
out = []
|
|
184
189
|
for b in range(0, len(texts), batch_size):
|
|
185
190
|
enc = self.tok(
|
|
@@ -196,7 +201,7 @@ class Predictor:
|
|
|
196
201
|
out.append(self.session.run(["logits"], feed)[0])
|
|
197
202
|
return np.concatenate(out) if out else np.zeros((0, len(self.labels)), dtype=np.float32)
|
|
198
203
|
|
|
199
|
-
def predict_proba(self, texts: list[str], batch_size: int =
|
|
204
|
+
def predict_proba(self, texts: list[str], batch_size: int | None = None) -> np.ndarray:
|
|
200
205
|
"""Label probabilities per text: a softmax, or for a multi-label model each label's sigmoid."""
|
|
201
206
|
z = self.logits(texts, batch_size)
|
|
202
207
|
if self.multi_label:
|
|
@@ -204,7 +209,7 @@ class Predictor:
|
|
|
204
209
|
z = np.exp(z - z.max(-1, keepdims=True))
|
|
205
210
|
return z / z.sum(-1, keepdims=True)
|
|
206
211
|
|
|
207
|
-
def predict(self, texts: list[str], batch_size: int =
|
|
212
|
+
def predict(self, texts: list[str], batch_size: int | None = None) -> list:
|
|
208
213
|
"""The top label per text, or for a multi-label model the list of labels with probability >= 0.5."""
|
|
209
214
|
z = self.logits(texts, batch_size)
|
|
210
215
|
if self.multi_label:
|
|
@@ -25,6 +25,13 @@ def model_card(onnx_dir: Path, repo: str, license: str | None = None) -> str:
|
|
|
25
25
|
]
|
|
26
26
|
if license:
|
|
27
27
|
front.append(f"license: {license}")
|
|
28
|
+
student_id = str(meta.get("student", ""))
|
|
29
|
+
if "/" in student_id and not student_id.startswith("static"): # links the card to the encoder's page
|
|
30
|
+
front.append(f"base_model: {student_id}")
|
|
31
|
+
data = [str(meta.get(k) or "") for k in ("data", "test")]
|
|
32
|
+
datasets = sorted({d.split(":")[1] for d in data if d.startswith("hf:") and len(d.split(":")) > 1})
|
|
33
|
+
if datasets: # links the card to the datasets' pages
|
|
34
|
+
front += ["datasets:", *(f"- {d}" for d in datasets)]
|
|
28
35
|
front.append("---")
|
|
29
36
|
teacher, student = meta.get("teacher", "an LLM"), meta.get("student", "a small encoder")
|
|
30
37
|
lines = [
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Charts of a run, as matplotlib figures (``pip install "tinydistill[plot]"``). Each function takes a run
|
|
2
|
+
directory and returns a figure, so in a notebook it shows inline; ``save_all`` writes every chart the run has
|
|
3
|
+
data for to ``RUN/plots/*.png`` (that is what `tinydistill plot RUN` does).
|
|
4
|
+
|
|
5
|
+
The student is blue and the teacher orange throughout, as in the web page."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
STUDENT, TEACHER, REFERENCE = "#2a78d6", "#eb6834", "#8a8984"
|
|
14
|
+
SURFACE, INK, MUTED, GRID = "#fcfcfb", "#0b0b0b", "#52514e", "#e4e3df"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _plt():
|
|
18
|
+
try:
|
|
19
|
+
import matplotlib
|
|
20
|
+
|
|
21
|
+
if not _in_notebook():
|
|
22
|
+
matplotlib.use("Agg") # files only, no window
|
|
23
|
+
import matplotlib.pyplot as plt
|
|
24
|
+
except ImportError as e:
|
|
25
|
+
raise SystemExit('plots need matplotlib: pip install "tinydistill[plot]"') from e
|
|
26
|
+
plt.rcParams.update(
|
|
27
|
+
{
|
|
28
|
+
"figure.facecolor": SURFACE,
|
|
29
|
+
"axes.facecolor": SURFACE,
|
|
30
|
+
"axes.edgecolor": GRID,
|
|
31
|
+
"axes.labelcolor": MUTED,
|
|
32
|
+
"axes.titlecolor": INK,
|
|
33
|
+
"axes.titlesize": 12,
|
|
34
|
+
"axes.titleweight": "bold",
|
|
35
|
+
"axes.titlelocation": "left",
|
|
36
|
+
"axes.spines.top": False,
|
|
37
|
+
"axes.spines.right": False,
|
|
38
|
+
"axes.grid": True,
|
|
39
|
+
"grid.color": GRID,
|
|
40
|
+
"grid.linewidth": 0.8,
|
|
41
|
+
"xtick.color": MUTED,
|
|
42
|
+
"ytick.color": MUTED,
|
|
43
|
+
"text.color": INK,
|
|
44
|
+
"legend.frameon": False,
|
|
45
|
+
"font.size": 10,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
return plt
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _in_notebook() -> bool:
|
|
52
|
+
try:
|
|
53
|
+
from IPython import get_ipython
|
|
54
|
+
|
|
55
|
+
return get_ipython() is not None
|
|
56
|
+
except ImportError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _results(run: Path) -> dict:
|
|
61
|
+
f = Path(run) / "results.json"
|
|
62
|
+
if not f.exists():
|
|
63
|
+
raise FileNotFoundError(f"{f} is missing: run `tinydistill eval` first")
|
|
64
|
+
return json.loads(f.read_text(encoding="utf-8"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _name(label: str) -> str:
|
|
68
|
+
return label.replace("_", " ")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def training(run: str | Path):
|
|
72
|
+
"""Training loss per epoch, and the held-out loss and the kept epoch when `--val-share` was used."""
|
|
73
|
+
plt = _plt()
|
|
74
|
+
f = Path(run) / "student" / "history.json"
|
|
75
|
+
if not f.exists():
|
|
76
|
+
raise FileNotFoundError(f"{f} is missing: train with tinydistill 0.3.3 or later")
|
|
77
|
+
h = json.loads(f.read_text(encoding="utf-8"))
|
|
78
|
+
epochs = [e["epoch"] for e in h["epochs"]]
|
|
79
|
+
fig, ax = plt.subplots(figsize=(7, 3.6))
|
|
80
|
+
held = [e.get("held_out_loss") for e in h["epochs"]]
|
|
81
|
+
has_held = any(v is not None for v in held)
|
|
82
|
+
ax.plot( # both lines are the student; the held-out loss decides, so it gets the student's colour
|
|
83
|
+
epochs,
|
|
84
|
+
[e["loss"] for e in h["epochs"]],
|
|
85
|
+
color=REFERENCE if has_held else STUDENT,
|
|
86
|
+
lw=2,
|
|
87
|
+
marker="o",
|
|
88
|
+
ms=5,
|
|
89
|
+
label="training texts",
|
|
90
|
+
)
|
|
91
|
+
if has_held:
|
|
92
|
+
ax.plot(
|
|
93
|
+
epochs, held, color=STUDENT, lw=2, marker="o", ms=5, label=f"{h['held_out_texts']} held-out texts"
|
|
94
|
+
)
|
|
95
|
+
ax.axvline(h["kept_epoch"], color=REFERENCE, lw=1, ls="--")
|
|
96
|
+
ax.annotate(
|
|
97
|
+
f" kept: epoch {h['kept_epoch']}", (h["kept_epoch"], ax.get_ylim()[1]), va="top", color=MUTED
|
|
98
|
+
)
|
|
99
|
+
ax.set(xlabel="epoch", ylabel="loss (lower is better)", title="Training")
|
|
100
|
+
ax.set_xticks(epochs)
|
|
101
|
+
ax.legend(loc="upper right")
|
|
102
|
+
fig.tight_layout()
|
|
103
|
+
return fig
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def per_label(run: str | Path, limit: int | None = None):
|
|
107
|
+
"""F1 of every label for the teacher and the student, the teacher's weakest labels at the top. With
|
|
108
|
+
``limit``, only that many of the labels where the two differ most."""
|
|
109
|
+
from .analyse import per_label as scores
|
|
110
|
+
from .analyse import predictions
|
|
111
|
+
|
|
112
|
+
plt = _plt()
|
|
113
|
+
p = predictions(Path(run))
|
|
114
|
+
names = p["task"].labels
|
|
115
|
+
t = [s["f1"] for s in scores(p["teacher"], p["gold"], len(names))]
|
|
116
|
+
s = [x["f1"] for x in scores(p["student"], p["gold"], len(names))]
|
|
117
|
+
order = sorted(range(len(names)), key=lambda c: t[c])
|
|
118
|
+
if limit:
|
|
119
|
+
order = sorted(sorted(order, key=lambda c: -abs(s[c] - t[c]))[:limit], key=lambda c: t[c])
|
|
120
|
+
fig, ax = plt.subplots(figsize=(7, 0.24 * len(order) + 1.4))
|
|
121
|
+
y = range(len(order))
|
|
122
|
+
ax.hlines(y, [min(t[c], s[c]) for c in order], [max(t[c], s[c]) for c in order], color=GRID, lw=2)
|
|
123
|
+
ax.scatter([t[c] for c in order], y, color=TEACHER, s=36, zorder=3, label="teacher")
|
|
124
|
+
ax.scatter([s[c] for c in order], y, color=STUDENT, s=36, zorder=3, label="student")
|
|
125
|
+
ax.set_yticks(list(y), [_name(names[c]) for c in order], fontsize=8)
|
|
126
|
+
ax.set_ylim(len(order) - 0.5, -0.5)
|
|
127
|
+
ax.set_xlim(0, 1.02)
|
|
128
|
+
ax.grid(axis="y", visible=False)
|
|
129
|
+
ax.set(xlabel="F1 on the test texts", title="Per label: teacher and student")
|
|
130
|
+
ax.legend(loc="lower right", bbox_to_anchor=(1, 1), ncols=2, borderaxespad=0.2)
|
|
131
|
+
fig.tight_layout()
|
|
132
|
+
return fig
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def confusions(run: str | Path, who: str = "student", labels: int = 12):
|
|
136
|
+
"""Confusion matrix of the student (or ``who="teacher"``) over the ``labels`` labels involved in the most
|
|
137
|
+
mistakes: rows are the gold labels, columns the predicted ones."""
|
|
138
|
+
from .analyse import predictions
|
|
139
|
+
|
|
140
|
+
plt = _plt()
|
|
141
|
+
p = predictions(Path(run))
|
|
142
|
+
names, gold, pred = p["task"].labels, p["gold"], p[who]
|
|
143
|
+
errors = Counter()
|
|
144
|
+
for g, x in zip(gold, pred, strict=True):
|
|
145
|
+
if g != x:
|
|
146
|
+
errors[g] += 1
|
|
147
|
+
errors[x] += 1
|
|
148
|
+
keep = sorted(c for c, _ in errors.most_common(labels)) or list(range(min(labels, len(names))))
|
|
149
|
+
index = {c: i for i, c in enumerate(keep)}
|
|
150
|
+
m = [[0] * len(keep) for _ in keep]
|
|
151
|
+
for g, x in zip(gold, pred, strict=True):
|
|
152
|
+
if g in index and x in index:
|
|
153
|
+
m[index[g]][index[x]] += 1
|
|
154
|
+
fig, ax = plt.subplots(figsize=(0.5 * len(keep) + 3, 0.45 * len(keep) + 2))
|
|
155
|
+
ax.imshow(m, cmap="Blues")
|
|
156
|
+
top = max(max(r) for r in m) or 1
|
|
157
|
+
for i, r in enumerate(m):
|
|
158
|
+
for j, v in enumerate(r):
|
|
159
|
+
if v:
|
|
160
|
+
ax.text(
|
|
161
|
+
j, i, v, ha="center", va="center", fontsize=8, color="#ffffff" if v > top / 2 else INK
|
|
162
|
+
)
|
|
163
|
+
ticks = [_name(names[c]) for c in keep]
|
|
164
|
+
ax.set_xticks(range(len(keep)), ticks, rotation=45, ha="right", fontsize=8)
|
|
165
|
+
ax.set_yticks(range(len(keep)), ticks, fontsize=8)
|
|
166
|
+
ax.grid(False)
|
|
167
|
+
ax.set(xlabel="predicted", ylabel="gold", title=f"Most confused labels ({who})")
|
|
168
|
+
fig.tight_layout()
|
|
169
|
+
return fig
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def speed_accuracy(run: str | Path):
|
|
173
|
+
"""Accuracy against latency per text (batch 1, log scale) for every timed model, with the upper bound and
|
|
174
|
+
the gold-only baseline as reference lines when the run has them."""
|
|
175
|
+
plt = _plt()
|
|
176
|
+
rows = _results(Path(run))["models"]
|
|
177
|
+
timed = [r for r in rows if r.get("latency_ms_b1") and r.get("accuracy") is not None]
|
|
178
|
+
if not timed:
|
|
179
|
+
raise ValueError("no timed model with an accuracy in results.json")
|
|
180
|
+
fig, ax = plt.subplots(figsize=(7, 4))
|
|
181
|
+
students = [r for r in timed if not r["role"].startswith("teacher")]
|
|
182
|
+
deployed = min(students, key=lambda r: r["latency_ms_b1"]) if students else None # e.g. ONNX int8
|
|
183
|
+
for r in timed:
|
|
184
|
+
color = TEACHER if r["role"].startswith("teacher") else STUDENT
|
|
185
|
+
ax.scatter(r["latency_ms_b1"], r["accuracy"], color=color, s=60, zorder=3, edgecolor=SURFACE, lw=1.5)
|
|
186
|
+
if color == STUDENT and r is not deployed:
|
|
187
|
+
continue # label only the teacher and the fastest student, so labels never collide
|
|
188
|
+
label = r["name"].replace("teacher, ", "teacher ").replace("student, ", "student ")
|
|
189
|
+
ax.annotate(
|
|
190
|
+
f"{label} ({r['device']})",
|
|
191
|
+
(r["latency_ms_b1"], r["accuracy"]),
|
|
192
|
+
textcoords="offset points",
|
|
193
|
+
xytext=(6, 4),
|
|
194
|
+
fontsize=8,
|
|
195
|
+
color=MUTED,
|
|
196
|
+
)
|
|
197
|
+
for r in rows:
|
|
198
|
+
if r["role"] in ("upper bound", "baseline") and r.get("accuracy") is not None:
|
|
199
|
+
ls = "--" if r["role"] == "upper bound" else ":"
|
|
200
|
+
ax.axhline(r["accuracy"], color=REFERENCE, lw=1, ls=ls)
|
|
201
|
+
ax.annotate(
|
|
202
|
+
r["role"],
|
|
203
|
+
(1, r["accuracy"]),
|
|
204
|
+
xycoords=("axes fraction", "data"),
|
|
205
|
+
ha="right",
|
|
206
|
+
va="bottom",
|
|
207
|
+
fontsize=8,
|
|
208
|
+
color=MUTED,
|
|
209
|
+
)
|
|
210
|
+
from matplotlib.ticker import FuncFormatter
|
|
211
|
+
|
|
212
|
+
ax.set_xscale("log")
|
|
213
|
+
ax.margins(x=0.3) # room for the labels on both sides
|
|
214
|
+
ax.xaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:g}"))
|
|
215
|
+
ax.xaxis.set_minor_formatter(FuncFormatter(lambda v, _: f"{v:g}" if f"{v:g}"[0] in "25" else ""))
|
|
216
|
+
accs = [r["accuracy"] for r in rows if r.get("accuracy") is not None]
|
|
217
|
+
ax.set_ylim(max(0.0, min(accs) - 0.1), min(1.0, max(accs) + 0.05)) # never zoom in on a tiny gap
|
|
218
|
+
ax.set(
|
|
219
|
+
xlabel="latency per text, batch 1 (ms, log scale)", ylabel="test accuracy", title="Accuracy and speed"
|
|
220
|
+
)
|
|
221
|
+
roles = {r["role"].startswith("teacher") for r in timed}
|
|
222
|
+
for is_teacher, color, label in ((True, TEACHER, "teacher"), (False, STUDENT, "student")):
|
|
223
|
+
if is_teacher in roles:
|
|
224
|
+
ax.scatter([], [], color=color, label=label)
|
|
225
|
+
ax.legend(loc="lower left")
|
|
226
|
+
fig.tight_layout()
|
|
227
|
+
return fig
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cascade(run: str | Path):
|
|
231
|
+
"""Accuracy as the student passes its least confident texts to the teacher."""
|
|
232
|
+
plt = _plt()
|
|
233
|
+
points = _results(Path(run)).get("cascade")
|
|
234
|
+
if not points:
|
|
235
|
+
raise ValueError("results.json has no cascade (single-label runs with a gold-labelled test set)")
|
|
236
|
+
x = [100 * c["share_to_teacher"] for c in points]
|
|
237
|
+
y = [c["accuracy"] for c in points]
|
|
238
|
+
fig, ax = plt.subplots(figsize=(7, 3.6))
|
|
239
|
+
ax.plot(x, y, color=STUDENT, lw=2, marker="o", ms=6)
|
|
240
|
+
for i, text, ha in ((0, "student only", "left"), (-1, "teacher only", "right")):
|
|
241
|
+
ax.annotate(
|
|
242
|
+
text, (x[i], y[i]), textcoords="offset points", xytext=(0, -16), ha=ha, fontsize=8, color=MUTED
|
|
243
|
+
)
|
|
244
|
+
ax.margins(y=0.15)
|
|
245
|
+
ax.set(
|
|
246
|
+
xlabel="texts sent to the teacher (%)",
|
|
247
|
+
ylabel="test accuracy",
|
|
248
|
+
title="Cascade: unsure texts go to the teacher",
|
|
249
|
+
)
|
|
250
|
+
fig.tight_layout()
|
|
251
|
+
return fig
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def soft_labels(run: str | Path, n: int = 3, k: int = 4):
|
|
255
|
+
"""For ``n`` test texts with different gold labels: the teacher's probabilities (the soft labels the
|
|
256
|
+
student learned from) next to the student's, over the ``k`` most likely labels."""
|
|
257
|
+
import math
|
|
258
|
+
|
|
259
|
+
from .export import Predictor
|
|
260
|
+
from .task import load_task
|
|
261
|
+
from .web import _samples
|
|
262
|
+
|
|
263
|
+
plt = _plt()
|
|
264
|
+
run = Path(run)
|
|
265
|
+
task = load_task(run / "task.yaml")
|
|
266
|
+
if task.multi_label:
|
|
267
|
+
raise ValueError("soft_labels is for single-label tasks")
|
|
268
|
+
rows = [
|
|
269
|
+
json.loads(line) for line in (run / "test.jsonl").read_text(encoding="utf-8").splitlines() if line
|
|
270
|
+
]
|
|
271
|
+
rows = _samples([r for r in rows if r.get("scores")], n)
|
|
272
|
+
if not rows:
|
|
273
|
+
raise ValueError("the run has no test texts with teacher scores")
|
|
274
|
+
student = Predictor(run / "onnx").predict_proba([r["text"] for r in rows])
|
|
275
|
+
fig, axes = plt.subplots(
|
|
276
|
+
len(rows), 1, figsize=(7, 1.55 * len(rows) * max(1, k / 4) + 0.6), squeeze=False, sharex=True
|
|
277
|
+
)
|
|
278
|
+
for ax, r, q in zip(axes[:, 0], rows, student, strict=True):
|
|
279
|
+
m = max(r["scores"])
|
|
280
|
+
e = [math.exp(x - m) for x in r["scores"]]
|
|
281
|
+
p = [x / sum(e) for x in e]
|
|
282
|
+
top = sorted(range(len(p)), key=lambda c: -(p[c] + q[c]))[:k]
|
|
283
|
+
y = range(len(top))
|
|
284
|
+
ax.barh([i - 0.2 for i in y], [p[c] for c in top], height=0.38, color=TEACHER, label="teacher")
|
|
285
|
+
ax.barh([i + 0.2 for i in y], [q[c] for c in top], height=0.38, color=STUDENT, label="student")
|
|
286
|
+
gold = r.get("gold")
|
|
287
|
+
ax.set_yticks(
|
|
288
|
+
list(y), [_name(task.labels[c]) + (" (gold)" if c == gold else "") for c in top], fontsize=8
|
|
289
|
+
)
|
|
290
|
+
ax.set_ylim(len(top) - 0.5, -0.5)
|
|
291
|
+
ax.set_xlim(0, 1)
|
|
292
|
+
ax.grid(axis="y", visible=False)
|
|
293
|
+
text = r["text"] if len(r["text"]) <= 90 else r["text"][:87] + "..."
|
|
294
|
+
ax.set_title(f"\u201c{text}\u201d", fontsize=9, fontweight="normal", color=MUTED)
|
|
295
|
+
axes[0, 0].legend(loc="lower right", bbox_to_anchor=(1, 1.18), ncols=2, borderaxespad=0)
|
|
296
|
+
axes[-1, 0].set_xlabel("probability")
|
|
297
|
+
fig.suptitle("Soft labels: teacher and student", x=0.02, ha="left", fontweight="bold", fontsize=12)
|
|
298
|
+
fig.tight_layout()
|
|
299
|
+
return fig
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
CHARTS = {
|
|
303
|
+
"training": training,
|
|
304
|
+
"speed_accuracy": speed_accuracy,
|
|
305
|
+
"per_label": per_label,
|
|
306
|
+
"confusions": confusions,
|
|
307
|
+
"cascade": cascade,
|
|
308
|
+
"soft_labels": soft_labels,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def save_all(run: str | Path, out: str | Path | None = None) -> list[Path]:
|
|
313
|
+
"""Write every chart the run has data for to ``out`` (default ``RUN/plots``); returns the files."""
|
|
314
|
+
plt = _plt()
|
|
315
|
+
out = Path(out) if out else Path(run) / "plots"
|
|
316
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
317
|
+
written = []
|
|
318
|
+
for name, chart in CHARTS.items():
|
|
319
|
+
try:
|
|
320
|
+
fig = chart(run)
|
|
321
|
+
except (FileNotFoundError, ValueError, SystemExit):
|
|
322
|
+
continue # this run has no data for the chart (e.g. multi-label, or no cascade)
|
|
323
|
+
fig.savefig(out / f"{name}.png", dpi=150)
|
|
324
|
+
plt.close(fig)
|
|
325
|
+
written.append(out / f"{name}.png")
|
|
326
|
+
return written
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import math
|
|
6
7
|
import random
|
|
7
8
|
import time
|
|
@@ -117,15 +118,23 @@ def train_student(
|
|
|
117
118
|
if static := encoder_of(model_name):
|
|
118
119
|
model, tok = new_static(static, labels, multi_label)
|
|
119
120
|
else:
|
|
121
|
+
from transformers.utils import logging as hf_logging
|
|
122
|
+
|
|
120
123
|
tok = AutoTokenizer.from_pretrained(model_name)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
level = hf_logging.get_verbosity()
|
|
125
|
+
hf_logging.set_verbosity_error() # its report of the new (missing) classifier head is expected noise
|
|
126
|
+
try:
|
|
127
|
+
model = AutoModelForSequenceClassification.from_pretrained(
|
|
128
|
+
model_name,
|
|
129
|
+
num_labels=len(labels),
|
|
130
|
+
id2label=dict(enumerate(labels)),
|
|
131
|
+
label2id={name: i for i, name in enumerate(labels)},
|
|
132
|
+
ignore_mismatched_sizes=True,
|
|
133
|
+
problem_type="multi_label_classification" if multi_label else "single_label_classification",
|
|
134
|
+
)
|
|
135
|
+
finally:
|
|
136
|
+
hf_logging.set_verbosity(level)
|
|
137
|
+
log(f"{model_name}: a new classification head for {len(labels)} labels on the pretrained encoder")
|
|
129
138
|
model = model.to(device)
|
|
130
139
|
gold = gold or [None] * len(texts)
|
|
131
140
|
if multi_label: # multi-hot gold, and which texts have it
|
|
@@ -169,6 +178,7 @@ def train_student(
|
|
|
169
178
|
)
|
|
170
179
|
|
|
171
180
|
best_loss, best_state, best_epoch = math.inf, None, epochs
|
|
181
|
+
history: list[dict] = [] # per epoch, saved as history.json next to the model
|
|
172
182
|
for epoch in range(epochs):
|
|
173
183
|
model.train()
|
|
174
184
|
rng.shuffle(order)
|
|
@@ -185,6 +195,7 @@ def train_student(
|
|
|
185
195
|
if on_step:
|
|
186
196
|
on_step()
|
|
187
197
|
line = f"epoch {epoch + 1}/{epochs}: loss {total / len(order):.4f}"
|
|
198
|
+
history.append({"epoch": epoch + 1, "loss": total / len(order)})
|
|
188
199
|
if val:
|
|
189
200
|
model.eval()
|
|
190
201
|
with torch.no_grad():
|
|
@@ -193,6 +204,7 @@ def train_student(
|
|
|
193
204
|
)
|
|
194
205
|
v /= len(val)
|
|
195
206
|
line += f", held-out loss {v:.4f}"
|
|
207
|
+
history[-1]["held_out_loss"] = v
|
|
196
208
|
if v < best_loss:
|
|
197
209
|
best_loss, best_epoch = v, epoch + 1
|
|
198
210
|
best_state = {k: t.detach().clone() for k, t in model.state_dict().items()}
|
|
@@ -206,7 +218,12 @@ def train_student(
|
|
|
206
218
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
207
219
|
model.save_pretrained(out_dir)
|
|
208
220
|
tok.save_pretrained(out_dir)
|
|
209
|
-
|
|
221
|
+
kept = best_epoch if best_state is not None else len(history)
|
|
222
|
+
(out_dir / "history.json").write_text(
|
|
223
|
+
json.dumps({"epochs": history, "kept_epoch": kept, "held_out_texts": len(val)}, indent=1),
|
|
224
|
+
encoding="utf-8",
|
|
225
|
+
)
|
|
226
|
+
return kept
|
|
210
227
|
|
|
211
228
|
|
|
212
229
|
def load_student(path: Path, device: str = "cpu"):
|
|
@@ -293,11 +293,14 @@ def test_push_writes_a_model_card_and_uploads(tmp_path, monkeypatch):
|
|
|
293
293
|
onnx = tmp_path / "run" / "onnx"
|
|
294
294
|
onnx.mkdir(parents=True)
|
|
295
295
|
(onnx / "labels.json").write_text('["neg", "pos"]')
|
|
296
|
-
(tmp_path / "run" / "meta.json").write_text(
|
|
296
|
+
(tmp_path / "run" / "meta.json").write_text(
|
|
297
|
+
'{"teacher": "T", "student": "org/S", "data": "hf:org/ds:train", "test": "hf:org/ds:test"}'
|
|
298
|
+
)
|
|
297
299
|
(tmp_path / "run" / "results.md").write_text("| Model |\n|---|\n| x |\n")
|
|
298
300
|
main(["push", str(onnx), "--repo", "me/clf", "--private", "--license", "apache-2.0"])
|
|
299
301
|
card = (onnx / "README.md").read_text(encoding="utf-8")
|
|
300
302
|
assert "license: apache-2.0" in card and "`T`" in card and "## Results" in card and '"hf:me/clf"' in card
|
|
303
|
+
assert "base_model: org/S" in card and "datasets:\n- org/ds\n---" in card
|
|
301
304
|
assert calls == [("create", "me/clf", True), ("upload", str(onnx), "me/clf")]
|
|
302
305
|
|
|
303
306
|
|
|
@@ -959,3 +962,17 @@ def test_train_keeps_the_best_epoch_on_held_out_texts(tmp_path):
|
|
|
959
962
|
assert "holds out 20% of its training texts and keeps the epoch" in (run / "results.md").read_text(
|
|
960
963
|
encoding="utf-8"
|
|
961
964
|
)
|
|
965
|
+
|
|
966
|
+
history = json.loads((run / "student" / "history.json").read_text(encoding="utf-8"))
|
|
967
|
+
assert (
|
|
968
|
+
history["kept_epoch"] == meta["kept_epochs"]["student"][0] and "held_out_loss" in history["epochs"][0]
|
|
969
|
+
)
|
|
970
|
+
main(["plot", str(run)])
|
|
971
|
+
assert {p.name for p in (run / "plots").glob("*.png")} == {
|
|
972
|
+
"training.png",
|
|
973
|
+
"speed_accuracy.png",
|
|
974
|
+
"per_label.png",
|
|
975
|
+
"confusions.png",
|
|
976
|
+
"cascade.png",
|
|
977
|
+
"soft_labels.png",
|
|
978
|
+
}
|
|
@@ -85,3 +85,19 @@ def test_explain_export_matches_the_model(tmp_path):
|
|
|
85
85
|
np.testing.assert_allclose(attention[0, :, : real[0]].sum(-1), 1, atol=1e-4) # a distribution over tokens
|
|
86
86
|
assert np.allclose(attention[0, :, real[0] :], 0, atol=1e-6) # padding gets none
|
|
87
87
|
assert export_explain(tmp_path / "student", tmp_path / "q", int8=True).name == "explain.int8.onnx"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_int8_scores_do_not_depend_on_the_batch(tmp_path):
|
|
91
|
+
torch.manual_seed(0)
|
|
92
|
+
model = AutoModelForSequenceClassification.from_pretrained(
|
|
93
|
+
TINY_ENCODER, num_labels=3, id2label={0: "a", 1: "b", 2: "c"}, ignore_mismatched_sizes=True
|
|
94
|
+
)
|
|
95
|
+
model.save_pretrained(tmp_path / "student")
|
|
96
|
+
AutoTokenizer.from_pretrained(TINY_ENCODER).save_pretrained(tmp_path / "student")
|
|
97
|
+
export_onnx(tmp_path / "student", tmp_path / "onnx", int8=True)
|
|
98
|
+
p = Predictor(tmp_path / "onnx")
|
|
99
|
+
assert p.int8
|
|
100
|
+
text = "the same text every time"
|
|
101
|
+
alone = p.logits([text])[0]
|
|
102
|
+
for others in (["short"], ["a much longer text with many more words in it than the first one", "x"]):
|
|
103
|
+
np.testing.assert_allclose(p.logits([text, *others])[0], alone, rtol=0, atol=1e-6)
|
|
@@ -175,7 +175,8 @@ def test_best_epoch_is_kept_and_training_stops_early(tmp_path):
|
|
|
175
175
|
patience=1,
|
|
176
176
|
) # nothing is learned, so the held-out loss never improves after epoch 1
|
|
177
177
|
assert kept == 1
|
|
178
|
-
|
|
178
|
+
epochs = [line for line in lines if line.startswith("epoch ")]
|
|
179
|
+
assert len(epochs) == 2 and "held-out loss" in epochs[0]
|
|
179
180
|
assert any("keeping epoch 1" in line for line in lines) and (tmp_path / "s" / "config.json").exists()
|
|
180
181
|
assert (
|
|
181
182
|
train_student(texts, probs, ["a", "b"], tmp_path / "t", TINY_ENCODER, epochs=2, log=lines.append) == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L12-v2-20-epochs.json
RENAMED
|
File without changes
|
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L6-v2-10-epochs.json
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/banking77/students/all-MiniLM-L6-v2-20-epochs.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b-8shot.json
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b-8shot.md
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-1.5b.json
RENAMED
|
File without changes
|
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-7b-20-epochs.json
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-7b-20-epochs.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-ensemble.json
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-ensemble.md
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-synthetic.json
RENAMED
|
File without changes
|
{tinydistill-0.3.2 → tinydistill-0.3.3}/examples/financial-tweets/financial-tweets-synthetic.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|