tinydistill 0.2.0__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.2.0/.gitignore +8 -0
- tinydistill-0.2.0/LICENSE +21 -0
- tinydistill-0.2.0/PKG-INFO +294 -0
- tinydistill-0.2.0/README.md +264 -0
- tinydistill-0.2.0/examples/banking77/results-7b.json +84 -0
- tinydistill-0.2.0/examples/banking77/results-7b.md +9 -0
- tinydistill-0.2.0/examples/banking77/results-k8-7b.json +92 -0
- tinydistill-0.2.0/examples/banking77/results-k8-7b.md +10 -0
- tinydistill-0.2.0/examples/banking77/results-k8.json +103 -0
- tinydistill-0.2.0/examples/banking77/results-k8.md +11 -0
- tinydistill-0.2.0/examples/banking77/results.json +95 -0
- tinydistill-0.2.0/examples/banking77/results.md +10 -0
- tinydistill-0.2.0/examples/banking77/run.sh +22 -0
- tinydistill-0.2.0/examples/banking77/task.yaml +92 -0
- tinydistill-0.2.0/examples/financial-tweets/results.json +103 -0
- tinydistill-0.2.0/examples/financial-tweets/results.md +11 -0
- tinydistill-0.2.0/examples/financial-tweets/run.sh +17 -0
- tinydistill-0.2.0/examples/financial-tweets/task.yaml +21 -0
- tinydistill-0.2.0/examples/update_readme.py +20 -0
- tinydistill-0.2.0/pyproject.toml +50 -0
- tinydistill-0.2.0/src/tinydistill/__init__.py +6 -0
- tinydistill-0.2.0/src/tinydistill/cli.py +459 -0
- tinydistill-0.2.0/src/tinydistill/data.py +92 -0
- tinydistill-0.2.0/src/tinydistill/evaluate.py +140 -0
- tinydistill-0.2.0/src/tinydistill/export.py +103 -0
- tinydistill-0.2.0/src/tinydistill/student.py +140 -0
- tinydistill-0.2.0/src/tinydistill/task.py +108 -0
- tinydistill-0.2.0/src/tinydistill/teacher.py +272 -0
- tinydistill-0.2.0/tests/conftest.py +24 -0
- tinydistill-0.2.0/tests/test_cache.py +38 -0
- tinydistill-0.2.0/tests/test_cli.py +149 -0
- tinydistill-0.2.0/tests/test_export.py +34 -0
- tinydistill-0.2.0/tests/test_loss.py +53 -0
- tinydistill-0.2.0/tests/test_scoring.py +186 -0
- tinydistill-0.2.0/tests/test_task.py +78 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andreas-Hum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tinydistill
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Distil an LLM classification prompt into a small, fast model that runs on a CPU.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Andreas-Hum/tinydistill
|
|
6
|
+
Author: Andreas-Hum
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: inference,knowledge distillation,llm,onnx,text classification
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: onnx>=1.15
|
|
15
|
+
Requires-Dist: onnxruntime>=1.17
|
|
16
|
+
Requires-Dist: pyyaml>=6
|
|
17
|
+
Requires-Dist: torch>=2.2
|
|
18
|
+
Requires-Dist: transformers>=4.56
|
|
19
|
+
Provides-Extra: 4bit
|
|
20
|
+
Requires-Dist: accelerate>=0.30; extra == '4bit'
|
|
21
|
+
Requires-Dist: bitsandbytes>=0.43; extra == '4bit'
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
24
|
+
Requires-Dist: datasets>=2.18; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
|
+
Provides-Extra: hf
|
|
28
|
+
Requires-Dist: datasets>=2.18; extra == 'hf'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# tinydistill
|
|
32
|
+
|
|
33
|
+
**Most of a large model's quality on one task, many times faster, on a CPU.**
|
|
34
|
+
|
|
35
|
+
`tinydistill` turns an LLM classification prompt into a small encoder classifier by knowledge distillation.
|
|
36
|
+
You describe the task in a short YAML file (labels, how to say each label, an instruction prompt) and give it
|
|
37
|
+
unlabelled texts. An instruction-tuned LLM labels them with soft targets, a small encoder learns to imitate
|
|
38
|
+
those targets, and the result is exported to ONNX (optionally int8) for fast CPU inference.
|
|
39
|
+
|
|
40
|
+
It continues my bachelor's project on inference optimisation by distilling LLMs.
|
|
41
|
+
|
|
42
|
+
## Results
|
|
43
|
+
|
|
44
|
+
Two tasks with `Qwen/Qwen2.5-1.5B-Instruct` as the zero-shot teacher, and Banking77 also with
|
|
45
|
+
`Qwen/Qwen2.5-7B-Instruct`; the student is always `sentence-transformers/all-MiniLM-L6-v2`. Quality is measured on gold-labelled test texts that
|
|
46
|
+
neither model trains on. Every table is written by the task's `run.sh` from its saved `results*.json` file in
|
|
47
|
+
[`examples/`](https://github.com/Andreas-Hum/tinydistill/tree/main/examples); no number is typed by hand. The teacher's latency includes scoring every surface form
|
|
48
|
+
of every label, which costs most on Banking77 with its many labels, so there the GPU teacher row is the fairer
|
|
49
|
+
reference for speed.
|
|
50
|
+
|
|
51
|
+
### Banking77: the hard case
|
|
52
|
+
|
|
53
|
+
Intent detection with 77 fine-grained classes ([Banking77](https://huggingface.co/datasets/PolyAI/banking77),
|
|
54
|
+
CC BY 4.0), reproduced by [`examples/banking77/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/banking77/run.sh).
|
|
55
|
+
|
|
56
|
+
Teacher only, no gold labels:
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
<!-- results: banking77/results.md -->
|
|
60
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
61
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
62
|
+
| teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19955.9 | n/a | 0.1 | 6175 | 1.0x |
|
|
63
|
+
| teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 60.3 | n/a | 16.6 | 3087 | 331.1x |
|
|
64
|
+
| student, PyTorch | cpu | 0.386 | 0.315 | 0.772 | 3.9 | 78.3 | 408.9 | 91 | 5124.6x |
|
|
65
|
+
| student, ONNX fp32 | cpu | 0.386 | 0.315 | 0.772 | 2.6 | 82.4 | 388.4 | 91 | 7606.9x |
|
|
66
|
+
| student, ONNX int8 | cpu | 0.383 | 0.311 | 0.761 | 1.0 | 34.4 | 931.1 | 23 | 20502.9x |
|
|
67
|
+
|
|
68
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
|
|
69
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
|
|
70
|
+
<!-- /results -->
|
|
71
|
+
|
|
72
|
+
With 8 gold labels per class, and the student trained on all gold labels as an upper bound:
|
|
73
|
+
|
|
74
|
+
<!-- results: banking77/results-k8.md -->
|
|
75
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
76
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
77
|
+
| teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19699.4 | n/a | 0.1 | 6175 | 1.0x |
|
|
78
|
+
| teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 58.3 | n/a | 17.2 | 3087 | 338.1x |
|
|
79
|
+
| student, PyTorch | cpu | 0.404 | 0.324 | 0.699 | 3.9 | 76.7 | 417.4 | 91 | 5056.3x |
|
|
80
|
+
| upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.412 | n/a | n/a | n/a | n/a | n/a |
|
|
81
|
+
| student, ONNX fp32 | cpu | 0.404 | 0.324 | 0.699 | 2.7 | 72.1 | 443.6 | 91 | 7306.9x |
|
|
82
|
+
| student, ONNX int8 | cpu | 0.398 | 0.318 | 0.697 | 1.0 | 36.7 | 872.1 | 23 | 18792.2x |
|
|
83
|
+
|
|
84
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
|
|
85
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
|
|
86
|
+
<!-- /results -->
|
|
87
|
+
|
|
88
|
+
With a stronger teacher, `Qwen/Qwen2.5-7B-Instruct` loaded in 4 bit (timed on the GPU only: 4-bit loading needs
|
|
89
|
+
CUDA), teacher only:
|
|
90
|
+
|
|
91
|
+
<!-- results: banking77/results-7b.md -->
|
|
92
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
93
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
94
|
+
| teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 272.2 | n/a | 3.7 | 5443 | 1.0x |
|
|
95
|
+
| student, PyTorch | cpu | 0.623 | 0.587 | 0.827 | 5.5 | 93.0 | 344.2 | 91 | 49.4x |
|
|
96
|
+
| student, ONNX fp32 | cpu | 0.623 | 0.587 | 0.827 | 3.4 | 86.8 | 368.7 | 91 | 80.8x |
|
|
97
|
+
| student, ONNX int8 | cpu | 0.622 | 0.586 | 0.822 | 1.6 | 43.8 | 730.0 | 23 | 171.2x |
|
|
98
|
+
|
|
99
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
|
|
100
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
|
|
101
|
+
<!-- /results -->
|
|
102
|
+
|
|
103
|
+
The same teacher with 8 gold labels per class and the upper bound:
|
|
104
|
+
|
|
105
|
+
<!-- results: banking77/results-k8-7b.md -->
|
|
106
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
107
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
108
|
+
| teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 295.4 | n/a | 3.4 | 5443 | 1.0x |
|
|
109
|
+
| student, PyTorch | cpu | 0.627 | 0.583 | 0.765 | 4.5 | 93.6 | 341.9 | 91 | 65.5x |
|
|
110
|
+
| upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.631 | n/a | n/a | n/a | n/a | n/a |
|
|
111
|
+
| student, ONNX fp32 | cpu | 0.627 | 0.583 | 0.765 | 3.2 | 80.9 | 395.7 | 91 | 92.5x |
|
|
112
|
+
| student, ONNX int8 | cpu | 0.622 | 0.576 | 0.762 | 1.1 | 40.7 | 786.4 | 23 | 262.6x |
|
|
113
|
+
|
|
114
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
|
|
115
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
|
|
116
|
+
<!-- /results -->
|
|
117
|
+
|
|
118
|
+
### Financial tweets: an easier task
|
|
119
|
+
|
|
120
|
+
Sentiment of financial news tweets, 3 classes
|
|
121
|
+
([Twitter Financial News Sentiment](https://huggingface.co/datasets/zeroshot/twitter-financial-news-sentiment),
|
|
122
|
+
MIT licence per the dataset card), reproduced by
|
|
123
|
+
[`examples/financial-tweets/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/financial-tweets/run.sh). The validation split is the test set.
|
|
124
|
+
|
|
125
|
+
<!-- results: financial-tweets/results.md -->
|
|
126
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
127
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
128
|
+
| teacher, zero-shot | cpu | 0.535 | 0.557 | n/a | 976.3 | n/a | 1.0 | 6175 | 1.0x |
|
|
129
|
+
| teacher, zero-shot | cuda | 0.535 | 0.557 | n/a | 33.7 | n/a | 29.6 | 3087 | 28.9x |
|
|
130
|
+
| student, PyTorch | cpu | 0.517 | 0.542 | 0.815 | 4.5 | 145.4 | 220.1 | 91 | 219.3x |
|
|
131
|
+
| upper bound: student on all gold labels | cpu | 0.867 | 0.824 | 0.531 | n/a | n/a | n/a | n/a | n/a |
|
|
132
|
+
| student, ONNX fp32 | cpu | 0.517 | 0.542 | 0.815 | 3.2 | 148.5 | 215.4 | 91 | 303.9x |
|
|
133
|
+
| student, ONNX int8 | cpu | 0.513 | 0.538 | 0.810 | 1.3 | 71.5 | 447.7 | 23 | 754.7x |
|
|
134
|
+
|
|
135
|
+
Task `financial-tweets`, 2388 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 9543 unlabelled training texts. The upper bound is the same student trained with cross-entropy on all 9543 gold training labels and no teacher; its speed equals the student's.
|
|
136
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
|
|
137
|
+
<!-- /results -->
|
|
138
|
+
|
|
139
|
+
## Quickstart
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
pip install "tinydistill[hf]"
|
|
143
|
+
curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
|
|
144
|
+
tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
|
|
145
|
+
--out runs/banking77 --int8
|
|
146
|
+
tinydistill predict runs/banking77/onnx "I still haven't received my new card"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
|
|
150
|
+
`--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
|
|
151
|
+
Banking77 splits are sorted by intent, so a limited run only sees a few intents.
|
|
152
|
+
|
|
153
|
+
`run` does every step. Each step also has its own command, and they all read and write the run directory:
|
|
154
|
+
|
|
155
|
+
| Command | What it does |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `tinydistill label TASK.yaml --data D [--test T] --out RUN` | teacher scores every label of every text; cached, resumable |
|
|
158
|
+
| `tinydistill train RUN` | distils the student from the teacher's soft targets |
|
|
159
|
+
| `tinydistill export RUN [--int8]` | writes `RUN/onnx` (ONNX, tokenizer, labels) |
|
|
160
|
+
| `tinydistill eval RUN` | writes `RUN/results.md` and `RUN/results.json` |
|
|
161
|
+
| `tinydistill predict RUN/onnx TEXT... [--data FILE]` | classifies texts, prints JSON lines |
|
|
162
|
+
|
|
163
|
+
Data can be `.csv`, `.jsonl` or `hf:<dataset>[:<config>]:<split>` (needs the `hf` extra). Useful options:
|
|
164
|
+
`--teacher` (any Hugging Face causal LM), `--load-4bit` (bitsandbytes, CUDA only: `pip install "tinydistill[4bit]"`),
|
|
165
|
+
`--student` (any Hugging Face encoder), `--epochs`, `--lr`, `--device`, `--threads`, and `--cache-dir` (runs that
|
|
166
|
+
share it label each text only once). See `tinydistill <command> --help`.
|
|
167
|
+
|
|
168
|
+
**Few labels.** With `--gold-per-class K`, the student also sees the gold labels of K randomly chosen training
|
|
169
|
+
examples per class, through a cross-entropy term (weight `--ce-weight`, default 1) added to the distillation
|
|
170
|
+
loss. `--upper-bound` additionally trains the same student on all gold training labels without the teacher,
|
|
171
|
+
and the results table gets an upper-bound row: how far the distilled student is from fully supervised training.
|
|
172
|
+
|
|
173
|
+
From Python:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from tinydistill import Predictor
|
|
177
|
+
|
|
178
|
+
clf = Predictor("runs/banking77/onnx") # uses model.int8.onnx when present
|
|
179
|
+
clf.predict(["How do I top up my card?"]) # list of label names
|
|
180
|
+
clf.predict_proba(["How do I top up my card?"]) # numpy array, one row per text
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### The task file
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
name: banking77
|
|
187
|
+
text_field: text # column holding the input text
|
|
188
|
+
label_field: label # column holding gold labels (ids, or integers indexing `labels`)
|
|
189
|
+
prompt: |-
|
|
190
|
+
You are classifying messages that customers sent to their bank's support chat.
|
|
191
|
+
Choose the single intent from this list that best matches the message:
|
|
192
|
+
{labels}
|
|
193
|
+
|
|
194
|
+
Message: {text}
|
|
195
|
+
|
|
196
|
+
Reply with the intent exactly as written in the list.
|
|
197
|
+
labels:
|
|
198
|
+
- activate_my_card # a bare id: its name is derived, "activate my card"
|
|
199
|
+
- card_arrival
|
|
200
|
+
# ...
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`{labels}` (required) becomes a bulleted list of the label names, and `{text}` is the input. The name is what
|
|
204
|
+
the teacher scores. Each label can be a bare id as above, `id: name`, or carry a description that is shown in
|
|
205
|
+
the list (`- bullish: positive for the stock`) but not scored:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
labels:
|
|
209
|
+
bearish:
|
|
210
|
+
description: negative for the stock, company or market
|
|
211
|
+
lost_card: lost or stolen card
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## How the label scoring works
|
|
215
|
+
|
|
216
|
+
The teacher never generates free text. For an input x, the prompt is rendered with the model's chat template
|
|
217
|
+
(user message = the instruction with the label list, generation prompt for the assistant). A continuation is one
|
|
218
|
+
way of writing a label followed by the end-of-turn token, `c_1 ... c_m`, with log-probability
|
|
219
|
+
|
|
220
|
+
log p(c | prompt(x)) = sum_j log p(c_j | prompt(x), c_1 ... c_(j-1))
|
|
221
|
+
|
|
222
|
+
Multi-token labels are handled exactly, and the end-of-turn token keeps a label that is a prefix of another
|
|
223
|
+
(`card` versus `card arrival`) from winning by stopping early. Models do not always write a label the way it is
|
|
224
|
+
listed: Qwen2.5 tends to answer a sentiment question with "Bullish" even when the list says "bullish". So each
|
|
225
|
+
label is scored over its common surface forms, the name as written, lowercase, Capitalised and UPPERCASE, each
|
|
226
|
+
with and without a leading space (forms that tokenise identically count once):
|
|
227
|
+
|
|
228
|
+
score(label) = log sum over forms c of p(c | prompt(x))
|
|
229
|
+
|
|
230
|
+
This is verbaliser coverage: it only counts more ways of writing the same answer, and there is no calibration or
|
|
231
|
+
prior correction. All forms share the prefix tree below, but with many labels they still add up; a task file can
|
|
232
|
+
list fewer (`surface_forms: [lowercase, capitalised]` drops UPPERCASE, which tokenises into many pieces, and
|
|
233
|
+
`surface_forms: []` scores the name as written only). A softmax over the label scores gives the soft targets. The prompt lists each label in its
|
|
234
|
+
primary form, the name as written. The scores are cached as JSON lines keyed by a hash of the text, in a file
|
|
235
|
+
named after a fingerprint of the teacher, prompt and continuations, so an interrupted run resumes and a changed
|
|
236
|
+
prompt never reuses old scores.
|
|
237
|
+
|
|
238
|
+
To keep this fast, the part of the prompt before the text is run once and its key/value cache is kept. Each
|
|
239
|
+
text then needs a single forward pass that covers the text and all labels together: the tokens of all continuations
|
|
240
|
+
(every surface form of every label) form a prefix tree, so continuations that start alike, such as
|
|
241
|
+
`card arrival` and `card linking`, share nodes, and a 4D attention mask lets each tree node attend only to the
|
|
242
|
+
prompt, the text and its own ancestors. There is no padding and no per-label copy of the cache. Tests check the
|
|
243
|
+
scores against a plain forward pass per continuation.
|
|
244
|
+
|
|
245
|
+
The student minimises KL(teacher || student) over the training texts, plus `--ce-weight` times the
|
|
246
|
+
cross-entropy on the K gold labels per class chosen by `--gold-per-class`, if any (standard knowledge
|
|
247
|
+
distillation, Hinton et al., 2015).
|
|
248
|
+
|
|
249
|
+
## Limitations
|
|
250
|
+
|
|
251
|
+
- **The teacher caps the student.** The student learns to imitate the teacher, mistakes included. A small LLM
|
|
252
|
+
choosing zero-shot among many fine-grained labels can be weak: Qwen2.5-0.5B-Instruct, for example, often
|
|
253
|
+
confuses related Banking77 intents. A bigger teacher (try `--load-4bit` with a 7B model) or a better prompt
|
|
254
|
+
usually helps more than anything on the student side.
|
|
255
|
+
- **Scores are sums, not averages.** Longer verbalisations get lower scores, so very uneven label lengths bias the
|
|
256
|
+
teacher. Keep verbalisations short and of similar length.
|
|
257
|
+
- **Only case and spacing variants are counted.** Plurals, synonyms or rephrasings of a label are not; keep label
|
|
258
|
+
names short and close to how the teacher would answer.
|
|
259
|
+
- **A too-small teacher can collapse.** Qwen2.5-0.5B-Instruct, for example, puts nearly every text into one or two
|
|
260
|
+
labels on the financial-tweets task, whatever the scoring; use 1.5B or larger.
|
|
261
|
+
- **Prompt and label order matter.** Zero-shot LLMs are sensitive to wording and to the order of the options.
|
|
262
|
+
- **Teacher cost grows with labels.** Every text is scored against every surface form of every label (shared
|
|
263
|
+
prefixes are computed once), one text at a time.
|
|
264
|
+
- **Single-label classification only.** Inputs are truncated to 128 tokens by default (`--max-length`).
|
|
265
|
+
- **Timings depend on the machine.** The results table states the threads and devices used; rerun `eval` on your
|
|
266
|
+
own hardware.
|
|
267
|
+
- ONNX export uses PyTorch's TorchScript-based exporter.
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
python -m venv .venv && . .venv/bin/activate
|
|
273
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
274
|
+
pip install -e ".[dev]"
|
|
275
|
+
ruff check . && ruff format --check . && pytest
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The tests run on a CPU in a few seconds with tiny random models from `hf-internal-testing`. They check the label
|
|
279
|
+
scores against a direct computation, the distillation loss and metrics against hand-computed values, cache
|
|
280
|
+
resumption, that the ONNX export reproduces the PyTorch logits, and the whole CLI end to end.
|
|
281
|
+
|
|
282
|
+
## References
|
|
283
|
+
|
|
284
|
+
- G. Hinton, O. Vinyals, J. Dean. [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531). arXiv:1503.02531, 2015.
|
|
285
|
+
- T. Schick, H. Schütze. [Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference](https://arxiv.org/abs/2001.07676). EACL, 2021. (Verbalisers.)
|
|
286
|
+
- I. Casanueva, T. Temčinas, D. Gerz, M. Henderson, I. Vulić. [Efficient Intent Detection with Dual Sentence Encoders](https://arxiv.org/abs/2003.04807). NLP4ConvAI Workshop, 2020. (Banking77.)
|
|
287
|
+
- W. Wang, F. Wei, L. Dong, H. Bao, N. Yang, M. Zhou. [MiniLM: Deep Self-Attention Distillation for Task-Agnostic Compression of Pre-Trained Transformers](https://arxiv.org/abs/2002.10957). NeurIPS, 2020.
|
|
288
|
+
- N. Reimers, I. Gurevych. [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084). EMNLP, 2019.
|
|
289
|
+
- Qwen Team. [Qwen2.5 Technical Report](https://arxiv.org/abs/2412.15115). arXiv:2412.15115, 2024.
|
|
290
|
+
|
|
291
|
+
## Licence
|
|
292
|
+
|
|
293
|
+
[MIT](https://github.com/Andreas-Hum/tinydistill/blob/main/LICENSE). The Banking77 dataset is CC BY 4.0 (PolyAI); models are downloaded from the Hugging Face Hub under
|
|
294
|
+
their own licences.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# tinydistill
|
|
2
|
+
|
|
3
|
+
**Most of a large model's quality on one task, many times faster, on a CPU.**
|
|
4
|
+
|
|
5
|
+
`tinydistill` turns an LLM classification prompt into a small encoder classifier by knowledge distillation.
|
|
6
|
+
You describe the task in a short YAML file (labels, how to say each label, an instruction prompt) and give it
|
|
7
|
+
unlabelled texts. An instruction-tuned LLM labels them with soft targets, a small encoder learns to imitate
|
|
8
|
+
those targets, and the result is exported to ONNX (optionally int8) for fast CPU inference.
|
|
9
|
+
|
|
10
|
+
It continues my bachelor's project on inference optimisation by distilling LLMs.
|
|
11
|
+
|
|
12
|
+
## Results
|
|
13
|
+
|
|
14
|
+
Two tasks with `Qwen/Qwen2.5-1.5B-Instruct` as the zero-shot teacher, and Banking77 also with
|
|
15
|
+
`Qwen/Qwen2.5-7B-Instruct`; the student is always `sentence-transformers/all-MiniLM-L6-v2`. Quality is measured on gold-labelled test texts that
|
|
16
|
+
neither model trains on. Every table is written by the task's `run.sh` from its saved `results*.json` file in
|
|
17
|
+
[`examples/`](https://github.com/Andreas-Hum/tinydistill/tree/main/examples); no number is typed by hand. The teacher's latency includes scoring every surface form
|
|
18
|
+
of every label, which costs most on Banking77 with its many labels, so there the GPU teacher row is the fairer
|
|
19
|
+
reference for speed.
|
|
20
|
+
|
|
21
|
+
### Banking77: the hard case
|
|
22
|
+
|
|
23
|
+
Intent detection with 77 fine-grained classes ([Banking77](https://huggingface.co/datasets/PolyAI/banking77),
|
|
24
|
+
CC BY 4.0), reproduced by [`examples/banking77/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/banking77/run.sh).
|
|
25
|
+
|
|
26
|
+
Teacher only, no gold labels:
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
<!-- results: banking77/results.md -->
|
|
30
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
31
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
32
|
+
| teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19955.9 | n/a | 0.1 | 6175 | 1.0x |
|
|
33
|
+
| teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 60.3 | n/a | 16.6 | 3087 | 331.1x |
|
|
34
|
+
| student, PyTorch | cpu | 0.386 | 0.315 | 0.772 | 3.9 | 78.3 | 408.9 | 91 | 5124.6x |
|
|
35
|
+
| student, ONNX fp32 | cpu | 0.386 | 0.315 | 0.772 | 2.6 | 82.4 | 388.4 | 91 | 7606.9x |
|
|
36
|
+
| student, ONNX int8 | cpu | 0.383 | 0.311 | 0.761 | 1.0 | 34.4 | 931.1 | 23 | 20502.9x |
|
|
37
|
+
|
|
38
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
|
|
39
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
|
|
40
|
+
<!-- /results -->
|
|
41
|
+
|
|
42
|
+
With 8 gold labels per class, and the student trained on all gold labels as an upper bound:
|
|
43
|
+
|
|
44
|
+
<!-- results: banking77/results-k8.md -->
|
|
45
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
46
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
47
|
+
| teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19699.4 | n/a | 0.1 | 6175 | 1.0x |
|
|
48
|
+
| teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 58.3 | n/a | 17.2 | 3087 | 338.1x |
|
|
49
|
+
| student, PyTorch | cpu | 0.404 | 0.324 | 0.699 | 3.9 | 76.7 | 417.4 | 91 | 5056.3x |
|
|
50
|
+
| upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.412 | n/a | n/a | n/a | n/a | n/a |
|
|
51
|
+
| student, ONNX fp32 | cpu | 0.404 | 0.324 | 0.699 | 2.7 | 72.1 | 443.6 | 91 | 7306.9x |
|
|
52
|
+
| student, ONNX int8 | cpu | 0.398 | 0.318 | 0.697 | 1.0 | 36.7 | 872.1 | 23 | 18792.2x |
|
|
53
|
+
|
|
54
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
|
|
55
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
|
|
56
|
+
<!-- /results -->
|
|
57
|
+
|
|
58
|
+
With a stronger teacher, `Qwen/Qwen2.5-7B-Instruct` loaded in 4 bit (timed on the GPU only: 4-bit loading needs
|
|
59
|
+
CUDA), teacher only:
|
|
60
|
+
|
|
61
|
+
<!-- results: banking77/results-7b.md -->
|
|
62
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
63
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
64
|
+
| teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 272.2 | n/a | 3.7 | 5443 | 1.0x |
|
|
65
|
+
| student, PyTorch | cpu | 0.623 | 0.587 | 0.827 | 5.5 | 93.0 | 344.2 | 91 | 49.4x |
|
|
66
|
+
| student, ONNX fp32 | cpu | 0.623 | 0.587 | 0.827 | 3.4 | 86.8 | 368.7 | 91 | 80.8x |
|
|
67
|
+
| student, ONNX int8 | cpu | 0.622 | 0.586 | 0.822 | 1.6 | 43.8 | 730.0 | 23 | 171.2x |
|
|
68
|
+
|
|
69
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
|
|
70
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
|
|
71
|
+
<!-- /results -->
|
|
72
|
+
|
|
73
|
+
The same teacher with 8 gold labels per class and the upper bound:
|
|
74
|
+
|
|
75
|
+
<!-- results: banking77/results-k8-7b.md -->
|
|
76
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
77
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
78
|
+
| teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 295.4 | n/a | 3.4 | 5443 | 1.0x |
|
|
79
|
+
| student, PyTorch | cpu | 0.627 | 0.583 | 0.765 | 4.5 | 93.6 | 341.9 | 91 | 65.5x |
|
|
80
|
+
| upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.631 | n/a | n/a | n/a | n/a | n/a |
|
|
81
|
+
| student, ONNX fp32 | cpu | 0.627 | 0.583 | 0.765 | 3.2 | 80.9 | 395.7 | 91 | 92.5x |
|
|
82
|
+
| student, ONNX int8 | cpu | 0.622 | 0.576 | 0.762 | 1.1 | 40.7 | 786.4 | 23 | 262.6x |
|
|
83
|
+
|
|
84
|
+
Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
|
|
85
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
|
|
86
|
+
<!-- /results -->
|
|
87
|
+
|
|
88
|
+
### Financial tweets: an easier task
|
|
89
|
+
|
|
90
|
+
Sentiment of financial news tweets, 3 classes
|
|
91
|
+
([Twitter Financial News Sentiment](https://huggingface.co/datasets/zeroshot/twitter-financial-news-sentiment),
|
|
92
|
+
MIT licence per the dataset card), reproduced by
|
|
93
|
+
[`examples/financial-tweets/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/financial-tweets/run.sh). The validation split is the test set.
|
|
94
|
+
|
|
95
|
+
<!-- results: financial-tweets/results.md -->
|
|
96
|
+
| Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
|
|
97
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
98
|
+
| teacher, zero-shot | cpu | 0.535 | 0.557 | n/a | 976.3 | n/a | 1.0 | 6175 | 1.0x |
|
|
99
|
+
| teacher, zero-shot | cuda | 0.535 | 0.557 | n/a | 33.7 | n/a | 29.6 | 3087 | 28.9x |
|
|
100
|
+
| student, PyTorch | cpu | 0.517 | 0.542 | 0.815 | 4.5 | 145.4 | 220.1 | 91 | 219.3x |
|
|
101
|
+
| upper bound: student on all gold labels | cpu | 0.867 | 0.824 | 0.531 | n/a | n/a | n/a | n/a | n/a |
|
|
102
|
+
| student, ONNX fp32 | cpu | 0.517 | 0.542 | 0.815 | 3.2 | 148.5 | 215.4 | 91 | 303.9x |
|
|
103
|
+
| student, ONNX int8 | cpu | 0.513 | 0.538 | 0.810 | 1.3 | 71.5 | 447.7 | 23 | 754.7x |
|
|
104
|
+
|
|
105
|
+
Task `financial-tweets`, 2388 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 9543 unlabelled training texts. The upper bound is the same student trained with cross-entropy on all 9543 gold training labels and no teacher; its speed equals the student's.
|
|
106
|
+
CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
|
|
107
|
+
<!-- /results -->
|
|
108
|
+
|
|
109
|
+
## Quickstart
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
pip install "tinydistill[hf]"
|
|
113
|
+
curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
|
|
114
|
+
tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
|
|
115
|
+
--out runs/banking77 --int8
|
|
116
|
+
tinydistill predict runs/banking77/onnx "I still haven't received my new card"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
|
|
120
|
+
`--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
|
|
121
|
+
Banking77 splits are sorted by intent, so a limited run only sees a few intents.
|
|
122
|
+
|
|
123
|
+
`run` does every step. Each step also has its own command, and they all read and write the run directory:
|
|
124
|
+
|
|
125
|
+
| Command | What it does |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `tinydistill label TASK.yaml --data D [--test T] --out RUN` | teacher scores every label of every text; cached, resumable |
|
|
128
|
+
| `tinydistill train RUN` | distils the student from the teacher's soft targets |
|
|
129
|
+
| `tinydistill export RUN [--int8]` | writes `RUN/onnx` (ONNX, tokenizer, labels) |
|
|
130
|
+
| `tinydistill eval RUN` | writes `RUN/results.md` and `RUN/results.json` |
|
|
131
|
+
| `tinydistill predict RUN/onnx TEXT... [--data FILE]` | classifies texts, prints JSON lines |
|
|
132
|
+
|
|
133
|
+
Data can be `.csv`, `.jsonl` or `hf:<dataset>[:<config>]:<split>` (needs the `hf` extra). Useful options:
|
|
134
|
+
`--teacher` (any Hugging Face causal LM), `--load-4bit` (bitsandbytes, CUDA only: `pip install "tinydistill[4bit]"`),
|
|
135
|
+
`--student` (any Hugging Face encoder), `--epochs`, `--lr`, `--device`, `--threads`, and `--cache-dir` (runs that
|
|
136
|
+
share it label each text only once). See `tinydistill <command> --help`.
|
|
137
|
+
|
|
138
|
+
**Few labels.** With `--gold-per-class K`, the student also sees the gold labels of K randomly chosen training
|
|
139
|
+
examples per class, through a cross-entropy term (weight `--ce-weight`, default 1) added to the distillation
|
|
140
|
+
loss. `--upper-bound` additionally trains the same student on all gold training labels without the teacher,
|
|
141
|
+
and the results table gets an upper-bound row: how far the distilled student is from fully supervised training.
|
|
142
|
+
|
|
143
|
+
From Python:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from tinydistill import Predictor
|
|
147
|
+
|
|
148
|
+
clf = Predictor("runs/banking77/onnx") # uses model.int8.onnx when present
|
|
149
|
+
clf.predict(["How do I top up my card?"]) # list of label names
|
|
150
|
+
clf.predict_proba(["How do I top up my card?"]) # numpy array, one row per text
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### The task file
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
name: banking77
|
|
157
|
+
text_field: text # column holding the input text
|
|
158
|
+
label_field: label # column holding gold labels (ids, or integers indexing `labels`)
|
|
159
|
+
prompt: |-
|
|
160
|
+
You are classifying messages that customers sent to their bank's support chat.
|
|
161
|
+
Choose the single intent from this list that best matches the message:
|
|
162
|
+
{labels}
|
|
163
|
+
|
|
164
|
+
Message: {text}
|
|
165
|
+
|
|
166
|
+
Reply with the intent exactly as written in the list.
|
|
167
|
+
labels:
|
|
168
|
+
- activate_my_card # a bare id: its name is derived, "activate my card"
|
|
169
|
+
- card_arrival
|
|
170
|
+
# ...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`{labels}` (required) becomes a bulleted list of the label names, and `{text}` is the input. The name is what
|
|
174
|
+
the teacher scores. Each label can be a bare id as above, `id: name`, or carry a description that is shown in
|
|
175
|
+
the list (`- bullish: positive for the stock`) but not scored:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
labels:
|
|
179
|
+
bearish:
|
|
180
|
+
description: negative for the stock, company or market
|
|
181
|
+
lost_card: lost or stolen card
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## How the label scoring works
|
|
185
|
+
|
|
186
|
+
The teacher never generates free text. For an input x, the prompt is rendered with the model's chat template
|
|
187
|
+
(user message = the instruction with the label list, generation prompt for the assistant). A continuation is one
|
|
188
|
+
way of writing a label followed by the end-of-turn token, `c_1 ... c_m`, with log-probability
|
|
189
|
+
|
|
190
|
+
log p(c | prompt(x)) = sum_j log p(c_j | prompt(x), c_1 ... c_(j-1))
|
|
191
|
+
|
|
192
|
+
Multi-token labels are handled exactly, and the end-of-turn token keeps a label that is a prefix of another
|
|
193
|
+
(`card` versus `card arrival`) from winning by stopping early. Models do not always write a label the way it is
|
|
194
|
+
listed: Qwen2.5 tends to answer a sentiment question with "Bullish" even when the list says "bullish". So each
|
|
195
|
+
label is scored over its common surface forms, the name as written, lowercase, Capitalised and UPPERCASE, each
|
|
196
|
+
with and without a leading space (forms that tokenise identically count once):
|
|
197
|
+
|
|
198
|
+
score(label) = log sum over forms c of p(c | prompt(x))
|
|
199
|
+
|
|
200
|
+
This is verbaliser coverage: it only counts more ways of writing the same answer, and there is no calibration or
|
|
201
|
+
prior correction. All forms share the prefix tree below, but with many labels they still add up; a task file can
|
|
202
|
+
list fewer (`surface_forms: [lowercase, capitalised]` drops UPPERCASE, which tokenises into many pieces, and
|
|
203
|
+
`surface_forms: []` scores the name as written only). A softmax over the label scores gives the soft targets. The prompt lists each label in its
|
|
204
|
+
primary form, the name as written. The scores are cached as JSON lines keyed by a hash of the text, in a file
|
|
205
|
+
named after a fingerprint of the teacher, prompt and continuations, so an interrupted run resumes and a changed
|
|
206
|
+
prompt never reuses old scores.
|
|
207
|
+
|
|
208
|
+
To keep this fast, the part of the prompt before the text is run once and its key/value cache is kept. Each
|
|
209
|
+
text then needs a single forward pass that covers the text and all labels together: the tokens of all continuations
|
|
210
|
+
(every surface form of every label) form a prefix tree, so continuations that start alike, such as
|
|
211
|
+
`card arrival` and `card linking`, share nodes, and a 4D attention mask lets each tree node attend only to the
|
|
212
|
+
prompt, the text and its own ancestors. There is no padding and no per-label copy of the cache. Tests check the
|
|
213
|
+
scores against a plain forward pass per continuation.
|
|
214
|
+
|
|
215
|
+
The student minimises KL(teacher || student) over the training texts, plus `--ce-weight` times the
|
|
216
|
+
cross-entropy on the K gold labels per class chosen by `--gold-per-class`, if any (standard knowledge
|
|
217
|
+
distillation, Hinton et al., 2015).
|
|
218
|
+
|
|
219
|
+
## Limitations
|
|
220
|
+
|
|
221
|
+
- **The teacher caps the student.** The student learns to imitate the teacher, mistakes included. A small LLM
|
|
222
|
+
choosing zero-shot among many fine-grained labels can be weak: Qwen2.5-0.5B-Instruct, for example, often
|
|
223
|
+
confuses related Banking77 intents. A bigger teacher (try `--load-4bit` with a 7B model) or a better prompt
|
|
224
|
+
usually helps more than anything on the student side.
|
|
225
|
+
- **Scores are sums, not averages.** Longer verbalisations get lower scores, so very uneven label lengths bias the
|
|
226
|
+
teacher. Keep verbalisations short and of similar length.
|
|
227
|
+
- **Only case and spacing variants are counted.** Plurals, synonyms or rephrasings of a label are not; keep label
|
|
228
|
+
names short and close to how the teacher would answer.
|
|
229
|
+
- **A too-small teacher can collapse.** Qwen2.5-0.5B-Instruct, for example, puts nearly every text into one or two
|
|
230
|
+
labels on the financial-tweets task, whatever the scoring; use 1.5B or larger.
|
|
231
|
+
- **Prompt and label order matter.** Zero-shot LLMs are sensitive to wording and to the order of the options.
|
|
232
|
+
- **Teacher cost grows with labels.** Every text is scored against every surface form of every label (shared
|
|
233
|
+
prefixes are computed once), one text at a time.
|
|
234
|
+
- **Single-label classification only.** Inputs are truncated to 128 tokens by default (`--max-length`).
|
|
235
|
+
- **Timings depend on the machine.** The results table states the threads and devices used; rerun `eval` on your
|
|
236
|
+
own hardware.
|
|
237
|
+
- ONNX export uses PyTorch's TorchScript-based exporter.
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
python -m venv .venv && . .venv/bin/activate
|
|
243
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
244
|
+
pip install -e ".[dev]"
|
|
245
|
+
ruff check . && ruff format --check . && pytest
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The tests run on a CPU in a few seconds with tiny random models from `hf-internal-testing`. They check the label
|
|
249
|
+
scores against a direct computation, the distillation loss and metrics against hand-computed values, cache
|
|
250
|
+
resumption, that the ONNX export reproduces the PyTorch logits, and the whole CLI end to end.
|
|
251
|
+
|
|
252
|
+
## References
|
|
253
|
+
|
|
254
|
+
- G. Hinton, O. Vinyals, J. Dean. [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531). arXiv:1503.02531, 2015.
|
|
255
|
+
- T. Schick, H. Schütze. [Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference](https://arxiv.org/abs/2001.07676). EACL, 2021. (Verbalisers.)
|
|
256
|
+
- I. Casanueva, T. Temčinas, D. Gerz, M. Henderson, I. Vulić. [Efficient Intent Detection with Dual Sentence Encoders](https://arxiv.org/abs/2003.04807). NLP4ConvAI Workshop, 2020. (Banking77.)
|
|
257
|
+
- W. Wang, F. Wei, L. Dong, H. Bao, N. Yang, M. Zhou. [MiniLM: Deep Self-Attention Distillation for Task-Agnostic Compression of Pre-Trained Transformers](https://arxiv.org/abs/2002.10957). NeurIPS, 2020.
|
|
258
|
+
- N. Reimers, I. Gurevych. [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084). EMNLP, 2019.
|
|
259
|
+
- Qwen Team. [Qwen2.5 Technical Report](https://arxiv.org/abs/2412.15115). arXiv:2412.15115, 2024.
|
|
260
|
+
|
|
261
|
+
## Licence
|
|
262
|
+
|
|
263
|
+
[MIT](https://github.com/Andreas-Hum/tinydistill/blob/main/LICENSE). The Banking77 dataset is CC BY 4.0 (PolyAI); models are downloaded from the Hugging Face Hub under
|
|
264
|
+
their own licences.
|