tinydistill 0.2.0__tar.gz → 0.3.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.
Files changed (88) hide show
  1. {tinydistill-0.2.0 → tinydistill-0.3.0}/.gitignore +1 -0
  2. tinydistill-0.3.0/PKG-INFO +515 -0
  3. tinydistill-0.3.0/README.md +485 -0
  4. tinydistill-0.3.0/examples/banking77/banking77-1.5b-8shot.json +198 -0
  5. tinydistill-0.3.0/examples/banking77/banking77-1.5b-8shot.md +24 -0
  6. tinydistill-0.3.0/examples/banking77/banking77-1.5b.json +184 -0
  7. tinydistill-0.3.0/examples/banking77/banking77-1.5b.md +24 -0
  8. tinydistill-0.3.0/examples/banking77/banking77-7b-8shot.json +198 -0
  9. tinydistill-0.3.0/examples/banking77/banking77-7b-8shot.md +24 -0
  10. tinydistill-0.3.0/examples/banking77/banking77-7b.json +172 -0
  11. tinydistill-0.3.0/examples/banking77/banking77-7b.md +23 -0
  12. tinydistill-0.3.0/examples/banking77/run.sh +39 -0
  13. tinydistill-0.3.0/examples/banking77/students.md +9 -0
  14. {tinydistill-0.2.0 → tinydistill-0.3.0}/examples/banking77/task.yaml +4 -0
  15. tinydistill-0.3.0/examples/banking77/teachers.md +8 -0
  16. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-1.5b-8shot.json +211 -0
  17. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-1.5b-8shot.md +25 -0
  18. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-1.5b.json +185 -0
  19. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-1.5b.md +24 -0
  20. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-7b.json +172 -0
  21. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-7b.md +23 -0
  22. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-ensemble.json +209 -0
  23. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-ensemble.md +26 -0
  24. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-synthetic.json +146 -0
  25. tinydistill-0.3.0/examples/financial-tweets/financial-tweets-synthetic.md +22 -0
  26. tinydistill-0.3.0/examples/financial-tweets/run.sh +40 -0
  27. {tinydistill-0.2.0 → tinydistill-0.3.0}/examples/financial-tweets/task.yaml +1 -0
  28. tinydistill-0.3.0/examples/financial-tweets/teachers.md +9 -0
  29. tinydistill-0.3.0/examples/go-emotions/go-emotions-1.5b.json +197 -0
  30. tinydistill-0.3.0/examples/go-emotions/go-emotions-1.5b.md +12 -0
  31. tinydistill-0.3.0/examples/go-emotions/run.sh +17 -0
  32. tinydistill-0.3.0/examples/go-emotions/task.yaml +41 -0
  33. tinydistill-0.3.0/examples/vllm-smoke.json +6 -0
  34. tinydistill-0.3.0/examples/vllm-smoke.md +5 -0
  35. tinydistill-0.3.0/examples/vllm-smoke.sh +43 -0
  36. {tinydistill-0.2.0 → tinydistill-0.3.0}/pyproject.toml +5 -1
  37. tinydistill-0.3.0/src/tinydistill/__init__.py +32 -0
  38. tinydistill-0.3.0/src/tinydistill/analyse.py +148 -0
  39. tinydistill-0.3.0/src/tinydistill/cli.py +1061 -0
  40. {tinydistill-0.2.0 → tinydistill-0.3.0}/src/tinydistill/data.py +40 -3
  41. tinydistill-0.3.0/src/tinydistill/evaluate.py +325 -0
  42. {tinydistill-0.2.0 → tinydistill-0.3.0}/src/tinydistill/export.py +65 -2
  43. tinydistill-0.3.0/src/tinydistill/hub.py +73 -0
  44. tinydistill-0.3.0/src/tinydistill/progress.py +122 -0
  45. tinydistill-0.3.0/src/tinydistill/serve.py +67 -0
  46. tinydistill-0.3.0/src/tinydistill/shortlist.py +47 -0
  47. tinydistill-0.3.0/src/tinydistill/static.py +104 -0
  48. {tinydistill-0.2.0 → tinydistill-0.3.0}/src/tinydistill/student.py +76 -11
  49. tinydistill-0.3.0/src/tinydistill/suggest.py +33 -0
  50. tinydistill-0.3.0/src/tinydistill/synth.py +106 -0
  51. tinydistill-0.3.0/src/tinydistill/task.py +201 -0
  52. tinydistill-0.3.0/src/tinydistill/teacher.py +616 -0
  53. tinydistill-0.3.0/src/tinydistill/tune.py +103 -0
  54. tinydistill-0.3.0/src/tinydistill/web.py +171 -0
  55. {tinydistill-0.2.0 → tinydistill-0.3.0}/tests/test_cache.py +13 -3
  56. tinydistill-0.3.0/tests/test_cli.py +841 -0
  57. {tinydistill-0.2.0 → tinydistill-0.3.0}/tests/test_export.py +26 -0
  58. tinydistill-0.3.0/tests/test_loss.py +153 -0
  59. tinydistill-0.3.0/tests/test_progress.py +39 -0
  60. tinydistill-0.3.0/tests/test_scoring.py +382 -0
  61. tinydistill-0.3.0/tests/test_serve.py +112 -0
  62. {tinydistill-0.2.0 → tinydistill-0.3.0}/tests/test_task.py +55 -0
  63. tinydistill-0.3.0/tests/test_vllm.py +63 -0
  64. tinydistill-0.2.0/PKG-INFO +0 -294
  65. tinydistill-0.2.0/README.md +0 -264
  66. tinydistill-0.2.0/examples/banking77/results-7b.json +0 -84
  67. tinydistill-0.2.0/examples/banking77/results-7b.md +0 -9
  68. tinydistill-0.2.0/examples/banking77/results-k8-7b.json +0 -92
  69. tinydistill-0.2.0/examples/banking77/results-k8-7b.md +0 -10
  70. tinydistill-0.2.0/examples/banking77/results-k8.json +0 -103
  71. tinydistill-0.2.0/examples/banking77/results-k8.md +0 -11
  72. tinydistill-0.2.0/examples/banking77/results.json +0 -95
  73. tinydistill-0.2.0/examples/banking77/results.md +0 -10
  74. tinydistill-0.2.0/examples/banking77/run.sh +0 -22
  75. tinydistill-0.2.0/examples/financial-tweets/results.json +0 -103
  76. tinydistill-0.2.0/examples/financial-tweets/results.md +0 -11
  77. tinydistill-0.2.0/examples/financial-tweets/run.sh +0 -17
  78. tinydistill-0.2.0/src/tinydistill/__init__.py +0 -6
  79. tinydistill-0.2.0/src/tinydistill/cli.py +0 -459
  80. tinydistill-0.2.0/src/tinydistill/evaluate.py +0 -140
  81. tinydistill-0.2.0/src/tinydistill/task.py +0 -108
  82. tinydistill-0.2.0/src/tinydistill/teacher.py +0 -272
  83. tinydistill-0.2.0/tests/test_cli.py +0 -149
  84. tinydistill-0.2.0/tests/test_loss.py +0 -53
  85. tinydistill-0.2.0/tests/test_scoring.py +0 -186
  86. {tinydistill-0.2.0 → tinydistill-0.3.0}/LICENSE +0 -0
  87. {tinydistill-0.2.0 → tinydistill-0.3.0}/examples/update_readme.py +0 -0
  88. {tinydistill-0.2.0 → tinydistill-0.3.0}/tests/conftest.py +0 -0
@@ -6,3 +6,4 @@ __pycache__/
6
6
  *.egg-info/
7
7
  .pytest_cache/
8
8
  .ruff_cache/
9
+ .venv-vllm/
@@ -0,0 +1,515 @@
1
+ Metadata-Version: 2.5
2
+ Name: tinydistill
3
+ Version: 0.3.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
+ ```mermaid
43
+ flowchart LR
44
+ A["Task prompt +<br/>unlabelled texts"] --> B["LLM teacher(s)<br/>Qwen · Mistral · Phi · ..."]
45
+ S["Synthetic texts<br/>(synth)"] -.-> B
46
+ V["vLLM server"] -.-> B
47
+ K["A few gold labels<br/>per class"] -.-> B
48
+ K -.-> C
49
+ B -- "soft labels<br/>(ensembled)" --> C["Small encoder<br/>student"]
50
+ C --> D["ONNX / int8<br/>export"]
51
+ D --> E["Fast CPU<br/>inference"]
52
+ D --> W["Browser · server ·<br/>Hugging Face Hub"]
53
+ E -.-> F["Cascade: unsure<br/>texts to the LLM"]
54
+ ```
55
+
56
+ Each step is one command: `tinydistill label` (teacher), `train` (student), `export` (ONNX / int8) and
57
+ `predict` (inference), or `tinydistill run` for all of them. Dashed parts are optional: synthetic texts when
58
+ there is no unlabelled data, a vLLM server as the teacher, a few gold labels for the teacher's prompt and the
59
+ student's loss, and a cascade that hands the student's least confident texts to the LLM. The exported model
60
+ runs on a CPU, in a [browser](#deploying-cascade-server-browser), behind `tinydistill serve`, or from the
61
+ Hugging Face Hub.
62
+
63
+ ## Results
64
+
65
+ Teachers are `Qwen/Qwen2.5-1.5B-Instruct` and `Qwen/Qwen2.5-7B-Instruct` (4 bit), zero-shot or with 8 gold
66
+ examples per class in the prompt; the student is `sentence-transformers/all-MiniLM-L6-v2` unless a table says
67
+ otherwise. Quality is measured on gold-labelled test texts that no model trains on, and students are trained
68
+ with 3 seeds (mean ± standard deviation). Every table below is filled by the task's `run.sh` from its saved
69
+ results files in [`examples/`](https://github.com/Andreas-Hum/tinydistill/tree/main/examples); the text only
70
+ quotes them.
71
+
72
+ **Try it:** the [live demo](https://andreas-hum.github.io/tinydistill/) runs the Banking77 student (7B 8-shot
73
+ teacher, int8, 23 MB) in your browser.
74
+
75
+ ### Banking77: the hard case
76
+
77
+ Intent detection with 77 fine-grained classes ([Banking77](https://huggingface.co/datasets/PolyAI/banking77),
78
+ CC BY 4.0), reproduced by [`examples/banking77/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/banking77/run.sh).
79
+ One row per teacher; the student columns are the deployed int8 model:
80
+
81
+ <!-- results: banking77/teachers.md -->
82
+ | Run | Teacher | Teacher accuracy | Student | Parameters (M) | Accuracy | Macro-F1 | Agreement with teacher | Deployed as | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Size (MB) |
83
+ |---|---|---|---|---|---|---|---|---|---|---|---|
84
+ | banking77-1.5b | zero-shot `Qwen/Qwen2.5-1.5B-Instruct` | 0.404 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.386 ± 0.004 | 0.313 ± 0.005 | 0.766 ± 0.004 | ONNX int8 | 1.2 | 37.3 | 23 |
85
+ | banking77-1.5b-8shot | 8-shot `Qwen/Qwen2.5-1.5B-Instruct` | 0.513 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.485 ± 0.009 | 0.406 ± 0.007 | 0.706 ± 0.002 | ONNX int8 | 1.1 | 36.4 | 23 |
86
+ | banking77-7b | zero-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.629 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.618 ± 0.003 | 0.579 ± 0.005 | 0.817 ± 0.007 | ONNX int8 | 1.4 | 39.2 | 23 |
87
+ | banking77-7b-8shot | 8-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.783 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.729 ± 0.011 | 0.697 ± 0.015 | 0.800 ± 0.003 | ONNX int8 | 1.4 | 40.2 | 23 |
88
+
89
+ All latencies on the CPU with 2 threads.
90
+ <!-- /results -->
91
+
92
+ The student follows its teacher, so the teacher sets the ceiling: a bigger model and 8 examples per class
93
+ each help a lot. The full table for the best teacher, with the student trained on the same 616 gold labels
94
+ alone (baseline) and on all 10,003 gold labels (upper bound), and the cascade:
95
+
96
+ <!-- results: banking77/banking77-7b-8shot.md -->
97
+ | 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 |
98
+ |---|---|---|---|---|---|---|---|---|---|
99
+ | teacher, 8-shot, 4-bit | cuda | 0.783 | 0.779 | n/a | 264.0 | 7434.4 | 4.3 | 5443 | 1.0x |
100
+ | student, PyTorch | cpu | 0.729 ± 0.011 | 0.697 ± 0.015 | 0.800 ± 0.003 | 4.4 | 89.3 | 358.5 | 91 | 60.3x |
101
+ | student, ONNX fp32 | cpu | 0.727 | 0.696 | 0.801 | 3.4 | 90.9 | 352.2 | 91 | 77.6x |
102
+ | student, ONNX int8 | cpu | 0.726 | 0.694 | 0.797 | 1.4 | 40.2 | 795.7 | 23 | 186.3x |
103
+ | baseline: student on 8 gold labels per class, no teacher | cpu | 0.490 ± 0.014 | 0.449 ± 0.018 | 0.473 ± 0.015 | n/a | n/a | n/a | n/a | n/a |
104
+ | upper bound: student on all gold labels | cpu | 0.914 ± 0.005 | 0.914 ± 0.005 | 0.775 ± 0.001 | n/a | n/a | n/a | n/a | n/a |
105
+
106
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The teacher's prompt shows 8 gold training examples per class (616 texts). 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 baseline is the same student trained with cross-entropy on those 616 gold labels only, without the teacher. 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. Values with ± are the mean and standard deviation over 3 training seeds; the ONNX rows are the exported first-seed model.
107
+ 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 packs several texts into one forward pass; it is timed at batch 32 only when that takes under three minutes, otherwise its throughput is 1 / batch-1 latency. Speed-up is relative to the teacher, 8-shot, 4-bit on cuda.
108
+
109
+ ### Cascade: student first, unsure texts to the teacher
110
+
111
+ | Share sent to the teacher | Student confidence threshold | Accuracy | Macro-F1 | Mean latency per text (ms) |
112
+ |---|---|---|---|---|
113
+ | 0% | 0.080 | 0.726 | 0.694 | 1.4 |
114
+ | 10% | 0.231 | 0.744 | 0.718 | 27.8 |
115
+ | 20% | 0.302 | 0.750 | 0.730 | 54.2 |
116
+ | 30% | 0.355 | 0.764 | 0.748 | 80.6 |
117
+ | 50% | 0.440 | 0.778 | 0.769 | 133.4 |
118
+ | 100% | 1.000 | 0.783 | 0.779 | 265.4 |
119
+
120
+ The student answers every text and passes its least confident ones to the teacher. Thresholds are set on the test texts to hit each share; in deployment, set them on a validation split. Latency is the student's batch-1 latency plus the share times the batch-1 latency of the teacher, 8-shot, 4-bit on cuda.
121
+ <!-- /results -->
122
+
123
+ The distilled student is far better than the same student trained on the few gold labels alone, and runs on a
124
+ CPU in about a millisecond. Sending its least confident texts to the teacher closes most of the remaining gap
125
+ to the teacher.
126
+
127
+ Other students, the same teacher and soft labels. `static` is the static-embedding student
128
+ (`--student static`: token vectors from MiniLM-L6, averaged, no attention), trained for 20 epochs at learning
129
+ rate 1e-3 instead of 5 epochs at 1e-4:
130
+
131
+ <!-- results: banking77/students.md -->
132
+ | Run | Teacher | Teacher accuracy | Student | Parameters (M) | Accuracy | Macro-F1 | Agreement with teacher | Deployed as | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Size (MB) |
133
+ |---|---|---|---|---|---|---|---|---|---|---|---|
134
+ | banking77-7b-8shot | 8-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.783 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.729 ± 0.011 | 0.697 ± 0.015 | 0.800 ± 0.003 | ONNX int8 | 1.4 | 40.2 | 23 |
135
+ | all-MiniLM-L12-v2 | 8-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.783 | `sentence-transformers/all-MiniLM-L12-v2` | 33 | 0.743 ± 0.011 | 0.710 ± 0.016 | 0.815 ± 0.004 | ONNX int8 | 3.3 | 79.2 | 34 |
136
+ | all-mpnet-base-v2 | 8-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.783 | `sentence-transformers/all-mpnet-base-v2` | 110 | 0.814 ± 0.004 | 0.805 ± 0.007 | 0.863 ± 0.001 | ONNX int8 | 7.3 | 220.4 | 110 |
137
+ | bge-small-en-v1.5 | 8-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.783 | `BAAI/bge-small-en-v1.5` | 33 | 0.739 ± 0.008 | 0.706 ± 0.011 | 0.815 ± 0.004 | ONNX int8 | 2.7 | 75.2 | 34 |
138
+ | static | 8-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.783 | `static` | 8 | 0.803 ± 0.001 | 0.801 ± 0.001 | 0.824 ± 0.001 | ONNX int8 | 0.1 | 5.1 | 8 |
139
+
140
+ All latencies on the CPU with 2 threads.
141
+ <!-- /results -->
142
+
143
+ Two students are more accurate than their own teacher: the 110M-parameter mpnet and the static student.
144
+ Learning from the teacher's soft labels on 10,000 texts, plus the 616 gold labels, smooths out some of its
145
+ individual mistakes. The static student does this at about a tenth of a millisecond per text and 8 MB. It
146
+ also trained four times longer, so the gap to the small transformers is not only about architecture.
147
+
148
+ ### Financial tweets: an easier task
149
+
150
+ Sentiment of financial news tweets, 3 classes
151
+ ([Twitter Financial News Sentiment](https://huggingface.co/datasets/zeroshot/twitter-financial-news-sentiment),
152
+ MIT licence per the dataset card), reproduced by
153
+ [`examples/financial-tweets/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/financial-tweets/run.sh).
154
+ The validation split is the test set. The rows include an ensemble of three model families (Qwen2.5-7B,
155
+ Mistral-7B and Phi-3.5-mini, all 4 bit) and a student trained only on texts the teacher wrote itself
156
+ (`tinydistill synth`, no real training texts):
157
+
158
+ <!-- results: financial-tweets/teachers.md -->
159
+ | Run | Teacher | Teacher accuracy | Student | Parameters (M) | Accuracy | Macro-F1 | Agreement with teacher | Deployed as | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Size (MB) |
160
+ |---|---|---|---|---|---|---|---|---|---|---|---|
161
+ | financial-tweets-1.5b | zero-shot `Qwen/Qwen2.5-1.5B-Instruct` | 0.537 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.514 ± 0.004 | 0.538 ± 0.004 | 0.810 ± 0.004 | ONNX int8 | 1.6 | 85.1 | 23 |
162
+ | financial-tweets-1.5b-8shot | 8-shot `Qwen/Qwen2.5-1.5B-Instruct` | 0.314 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.295 ± 0.003 | 0.276 ± 0.003 | 0.870 ± 0.006 | ONNX int8 | 1.8 | 77.2 | 23 |
163
+ | financial-tweets-7b | zero-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.832 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.825 ± 0.007 | 0.768 ± 0.009 | 0.870 ± 0.005 | ONNX int8 | 2.5 | 105.7 | 23 |
164
+ | financial-tweets-ensemble | teacher ensemble of 3, zero-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct,mistralai/Mistral-7B-Instruct-v0.3,microsoft/Phi-3.5-mini-instruct` | 0.842 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.828 ± 0.006 | 0.778 ± 0.005 | 0.884 ± 0.007 | ONNX int8 | 1.6 | 99.4 | 23 |
165
+ | financial-tweets-synthetic | zero-shot, 4-bit `Qwen/Qwen2.5-7B-Instruct` | 0.832 | `sentence-transformers/all-MiniLM-L6-v2` | 23 | 0.596 ± 0.037 | 0.571 ± 0.027 | 0.615 ± 0.045 | ONNX int8 | 1.6 | 110.4 | 23 |
166
+
167
+ All latencies on the CPU with 2 threads.
168
+ <!-- /results -->
169
+
170
+ Here the 7B teacher is close to the student trained on all gold labels, and the student is close to the
171
+ teacher. The ensemble adds a little. Few-shot examples hurt on this skewed task (see
172
+ [Limitations](#limitations)). The student that saw only about 1,500 texts the teacher wrote itself is far
173
+ behind the one trained on real tweets: generated texts are much less varied than real ones, so `synth` is a
174
+ way to start without data, not a replacement for it. Full tables per run are in
175
+ [`examples/financial-tweets/`](https://github.com/Andreas-Hum/tinydistill/tree/main/examples/financial-tweets).
176
+
177
+ ### GoEmotions: multi-label
178
+
179
+ Reddit comments with any of 27 emotions or neutral
180
+ ([GoEmotions](https://huggingface.co/datasets/google-research-datasets/go_emotions), Apache 2.0), reproduced
181
+ by [`examples/go-emotions/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/go-emotions/run.sh).
182
+ The teacher answers one yes/no question per label; accuracy is the share of texts with exactly the right set
183
+ of labels.
184
+
185
+ <!-- results: go-emotions/go-emotions-1.5b.md -->
186
+ | Model | Device | Accuracy | Macro-F1 | Micro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
187
+ |---|---|---|---|---|---|---|---|---|---|---|
188
+ | teacher, zero-shot | cpu | 0.004 | 0.191 | 0.186 | n/a | 9990.9 | n/a | 0.1 | 6175 | 1.0x |
189
+ | teacher, zero-shot | cuda | 0.004 | 0.191 | 0.186 | n/a | 74.3 | 2131.6 | 15.0 | 3087 | 134.5x |
190
+ | student, PyTorch | cpu | 0.002 ± 0.001 | 0.111 ± 0.004 | 0.141 ± 0.002 | 0.015 ± 0.002 | 5.4 | 116.3 | 275.2 | 91 | 1857.3x |
191
+ | student, ONNX fp32 | cpu | 0.002 | 0.106 | 0.141 | 0.016 | 4.8 | 115.7 | 276.6 | 91 | 2072.9x |
192
+ | student, ONNX int8 | cpu | 0.002 | 0.108 | 0.141 | 0.016 | 2.0 | 66.3 | 482.8 | 23 | 5026.0x |
193
+ | baseline: student on 8 gold labels per class, no teacher | cpu | 0.000 ± 0.000 | 0.000 ± 0.000 | 0.000 ± 0.000 | 0.007 ± 0.000 | n/a | n/a | n/a | n/a | n/a |
194
+ | upper bound: student on all gold labels | cpu | 0.225 ± 0.021 | 0.034 ± 0.019 | 0.320 ± 0.034 | 0.007 ± 0.000 | n/a | n/a | n/a | n/a | n/a |
195
+
196
+ Task `go-emotions`, 5427 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 10000 training texts, plus cross-entropy on 8 gold labels per class (223 texts, weight 1.0). The baseline is the same student trained with cross-entropy on those 223 gold labels only, without the teacher. The upper bound is the same student trained with cross-entropy on all 10000 gold training labels and no teacher; its speed equals the student's. Values with ± are the mean and standard deviation over 3 training seeds; the ONNX rows are the exported first-seed model.
197
+ 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 packs several texts into one forward pass; it is timed at batch 32 only when that takes under three minutes, otherwise its throughput is 1 / batch-1 latency. Speed-up is relative to the teacher, zero-shot on cpu.
198
+ <!-- /results -->
199
+
200
+ This is a hard setting for a 1.5B teacher: it answers yes far too often, so exact-set accuracy is near zero,
201
+ and the student inherits that. The run shows that multi-label tasks work end to end, not that a small
202
+ teacher is good at them; use a larger teacher.
203
+
204
+ ### A vLLM teacher agrees with the local one
205
+
206
+ [`examples/vllm-smoke.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/vllm-smoke.sh)
207
+ labels 300 financial tweets with `Qwen/Qwen2.5-1.5B-Instruct` locally and through a vLLM server:
208
+
209
+ <!-- results: vllm-smoke.md -->
210
+ | Texts | Same top label | Largest difference in a label score | Mean difference |
211
+ |---|---|---|---|
212
+ | 300 | 100.0% | 0.611 | 0.0900 |
213
+
214
+ Qwen2.5-1.5B-Instruct scoring 300 financial tweets locally (bf16, tinydistill) and through vLLM (prompt log-probabilities); label scores are log-probabilities, so small differences are numerical.
215
+ <!-- /results -->
216
+
217
+ ## Quickstart
218
+
219
+ ```sh
220
+ pip install "tinydistill[hf]"
221
+ curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
222
+ tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
223
+ --out runs/banking77 --int8
224
+ tinydistill predict runs/banking77/onnx "I still haven't received my new card"
225
+ ```
226
+
227
+ Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
228
+ `--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
229
+ Banking77 splits are sorted by intent, so a limited run only sees a few intents.
230
+
231
+ `run` does every step. Each step also has its own command, and they all read and write the run directory:
232
+
233
+ | Command | What it does |
234
+ |---|---|
235
+ | `tinydistill init DATA [--out task.yaml]` | writes a starter task file from a dataset's label names |
236
+ | `tinydistill label TASK.yaml --data D [--test T] --out RUN` | teacher scores every label of every text; cached, resumable |
237
+ | `tinydistill train RUN` | distils the student from the teacher's soft targets |
238
+ | `tinydistill export RUN [--int8]` | writes `RUN/onnx` (ONNX, tokenizer, labels) |
239
+ | `tinydistill eval RUN` | writes `RUN/results.md` and `RUN/results.json` |
240
+ | `tinydistill predict RUN/onnx TEXT... [--data FILE]` | classifies texts, prints JSON lines |
241
+ | `tinydistill compare RUN...` | one table comparing runs, e.g. teachers or student sizes |
242
+ | `tinydistill serve RUN/onnx [--cascade RUN]` | a JSON HTTP endpoint (`POST /predict`) |
243
+ | `tinydistill web RUN/onnx --out site` | a static page that runs the model in the browser |
244
+ | `tinydistill push RUN/onnx --repo user/name` | uploads the model to the Hugging Face Hub with a model card |
245
+ | `tinydistill synth TASK.yaml --per-label N --out texts.jsonl` | has the teacher write unlabelled example texts |
246
+ | `tinydistill status RUN` | how far a running step is, and whether it is still running |
247
+ | `tinydistill tune TASK.yaml --data D --teacher a,b --few-shot 0,8` | compares teacher setups on a dev split |
248
+ | `tinydistill analyse RUN` | an error-analysis page: weakest labels, common confusions |
249
+ | `tinydistill suggest RUN --n 200 --out to_label.jsonl` | the texts most worth labelling by hand next |
250
+ | `tinydistill refresh RUN --log cascade.jsonl --out RUN2` | retrains with the texts the cascade sent to the teacher |
251
+ | `tinydistill report RUN` | rebuilds `results.md` from `results.json` |
252
+
253
+ Data can be `.csv`, `.jsonl` or `hf:<dataset>[:<config>]:<split>` (needs the `hf` extra). Useful options:
254
+ `--teacher` (any Hugging Face causal LM), `--load-4bit` (bitsandbytes, CUDA only: `pip install "tinydistill[4bit]"`),
255
+ `--student` (any Hugging Face encoder), `--epochs`, `--lr`, `--device`, `--threads`, `--seed`, and `--cache-dir`
256
+ (runs that share it label each text only once). See `tinydistill <command> --help`.
257
+
258
+ Long steps print their progress, speed and time left every few seconds and write it to `RUN/status.json`;
259
+ `tinydistill status RUN` reads it from another terminal and tells you whether the process is still alive.
260
+
261
+ **Static student.** `--student static` (or `static:<encoder>`) trains a student with no attention at all: one
262
+ vector per token, averaged, then a linear layer, started from the encoder's own token representations (as in
263
+ Model2Vec). It is much smaller and faster than a transformer student. It ignores word order, so it can miss
264
+ meaning that depends on it (negation, "A rather than B"); on Banking77 it beat the small transformer
265
+ students and came close to mpnet (see the results). It exports to ONNX and runs with `predict` and `serve` like any other student, but
266
+ not in the browser page. Train it longer and at a higher learning rate, such as `--epochs 20 --lr 1e-3`.
267
+
268
+ ### Choosing the teacher
269
+
270
+ `--teacher` takes any Hugging Face causal LM with a chat template; the tests check the scoring against a
271
+ plain forward pass for Llama, Mistral, Phi, Gemma 2 and GPT-2 style models as well as Qwen. Several teachers,
272
+ comma-separated, are an **ensemble**: each scores every text (one model in memory at a time) and the soft
273
+ labels are the mean of their label probabilities. `tinydistill compare` then puts the runs side by side.
274
+
275
+ A **vLLM server** can be the teacher instead of a local model, so a large model on a GPU cluster does the
276
+ labelling: start it with `vllm serve MODEL --enable-prefix-caching` and pass `--vllm-url http://host:8000/v1`
277
+ with `--teacher MODEL`. Only the tokenizer is loaded locally; every continuation is scored exactly from the
278
+ server's prompt log-probabilities, so the soft labels are the same as with the local model.
279
+
280
+ **Many labels.** With hundreds or thousands of labels, scoring every one for every text gets slow.
281
+ `label --shortlist K` first compares each text with the label names (and descriptions) using a small
282
+ sentence-embedding model (`--shortlist-model`, MiniLM by default) and has the teacher score only the K most
283
+ similar labels; the others get probability zero. `eval` reports the **shortlist recall**: how often the gold
284
+ label was among the K. If it is low, raise K.
285
+
286
+ **Tuning the setup.** `tinydistill tune TASK.yaml --data D --teacher a,b --few-shot 0,8 --prompt other.yaml`
287
+ scores every combination of teacher, number of few-shot examples and prompt on a dev split of gold-labelled
288
+ *training* texts (`--dev-size`, default 200; never the test set) and writes `tune.md` and `tune.json` (`--out`), best first, with
289
+ the `label` command for the winner.
290
+
291
+ ### No unlabelled data?
292
+
293
+ `tinydistill synth TASK.yaml --per-label 200 --out texts.jsonl` asks the teacher to write example inputs for
294
+ each label, from the task's own instruction. This is the only place where the teacher generates text, and only
295
+ inputs: the file has no label field, and the texts are labelled by the usual scoring when you pass them as
296
+ `--data`. Generated texts are less varied than real ones; use real texts when you have them.
297
+
298
+ ### Deploying: cascade, server, browser
299
+
300
+ **Cascade.** `eval` adds a table of what happens when the student answers and passes the texts it is least
301
+ sure about to the teacher: accuracy and mean latency for 0 % to 100 % of the texts sent on. At inference,
302
+ `tinydistill predict RUN/onnx --cascade RUN --threshold T` (and `serve --cascade`) does this for real: texts
303
+ whose top student probability is below T go to the run's teacher. Choose T on a validation split.
304
+
305
+ **Server.** `tinydistill serve RUN/onnx` answers `POST /predict` with `{"texts": [...]}` and `GET /health`,
306
+ using only the standard library. It listens on 127.0.0.1 unless you pass `--host`.
307
+
308
+ **Browser.** `tinydistill web RUN/onnx --out site` writes a static page plus the model in the layout
309
+ [transformers.js](https://github.com/huggingface/transformers.js) expects; the int8 model then runs in the
310
+ visitor's browser with no server at all. Serve the folder with any static host (GitHub Pages works).
311
+
312
+ ### A few gold labels
313
+
314
+ If some training texts have gold labels, they can help in three places. All of them use the same K randomly
315
+ chosen examples per class (fixed by `--seed`):
316
+
317
+ - `label --few-shot K` shows the examples in the teacher's prompt, in place of an `{examples}` line in the task's
318
+ prompt. The prompt prefix is computed once, so even hundreds of examples cost little per text.
319
+ - `train --gold-per-class K` adds a cross-entropy term on their gold labels (weight `--ce-weight`, default 1) to
320
+ the distillation loss.
321
+ - `train --gold-only` also trains the student on those labels alone, without the teacher, as a baseline row:
322
+ what the teacher adds over the few labels.
323
+
324
+ `train --upper-bound` trains the same student on all gold training labels, again without the teacher: how far
325
+ distillation is from full supervision. `train --seeds N` repeats every student with N training seeds, and the
326
+ results show the mean and standard deviation.
327
+
328
+ ### Improving a run
329
+
330
+ - `tinydistill analyse RUN` writes `RUN/analysis.html`: precision, recall and F1 of every label for the teacher
331
+ and the student, the most common confusions with example texts, and texts where the two disagree. Labels
332
+ the teacher gets wrong usually need a clearer name, a description, or merging.
333
+ - `tinydistill suggest RUN --n 200 --out to_label.jsonl` picks the training texts most worth labelling by hand:
334
+ where the student and the teacher disagree, then where the teacher is least sure, spread over labels. Fill
335
+ in the empty label field and pass the file back with `train RUN --gold-file to_label.jsonl`; those labels
336
+ join the cross-entropy term.
337
+ - `predict --cascade RUN --log cascade.jsonl` (and `serve --cascade ... --log`) records every text the cascade
338
+ passed to the teacher, with the teacher's scores. `tinydistill refresh RUN --log cascade.jsonl --out RUN2`
339
+ adds them to the training texts and trains, exports and evaluates a new student on the same test set, so
340
+ the student learns the inputs it found hard in production.
341
+
342
+ ### From Python
343
+
344
+ The exported model:
345
+
346
+ ```python
347
+ from tinydistill import Predictor
348
+
349
+ clf = Predictor("runs/banking77/onnx") # uses model.int8.onnx when present
350
+ clf.predict(["How do I top up my card?"]) # list of label names
351
+ clf.predict_proba(["How do I top up my card?"]) # numpy array, one row per text
352
+ ```
353
+
354
+ The whole pipeline, without the command line:
355
+
356
+ ```python
357
+ from pathlib import Path
358
+
359
+ import torch
360
+ from tinydistill import Predictor, export_onnx, load_task, load_teacher, read_examples, train_student
361
+
362
+ task = load_task("task.yaml")
363
+ texts = [e.text for e in read_examples("hf:PolyAI/banking77:train", task, limit=1000)]
364
+ teacher = load_teacher("Qwen/Qwen2.5-1.5B-Instruct", task, device="cuda")
365
+ soft_targets = torch.tensor(teacher.score_many(texts)).softmax(-1) # one row of label scores per text
366
+ train_student(texts, soft_targets, task.labels, Path("student"))
367
+ export_onnx(Path("student"), Path("student-onnx"), int8=True)
368
+ Predictor("student-onnx").predict(["Where is my card?"])
369
+ ```
370
+
371
+ ### The task file
372
+
373
+ ```yaml
374
+ name: banking77
375
+ text_field: text # column holding the input text
376
+ label_field: label # column holding gold labels (ids, or integers indexing `labels`)
377
+ prompt: |-
378
+ You are classifying messages that customers sent to their bank's support chat.
379
+ Choose the single intent from this list that best matches the message:
380
+ {labels}
381
+ {examples}
382
+
383
+ Message: {text}
384
+
385
+ Reply with the intent exactly as written in the list.
386
+ surface_forms: [lowercase, capitalised] # optional, see below
387
+ labels:
388
+ - activate_my_card # a bare id: its name is derived, "activate my card"
389
+ - card_arrival
390
+ # ...
391
+ ```
392
+
393
+ For **multi-label** tasks, where a text can have any number of labels, add `multi_label: true` and ask about
394
+ one label at a time, with `{label}` after `{text}`:
395
+
396
+ ```yaml
397
+ multi_label: true
398
+ prompt: |
399
+ Review: {text}
400
+ Does the review talk about {label}? Answer yes or no.
401
+ labels: [price, delivery, product_quality]
402
+ ```
403
+
404
+ The teacher answers every label's question in the same forward pass, and a label's score is
405
+ log P(yes) − log P(no) over the surface forms of both answers (`answers: [yes, no]` changes them). The student
406
+ gets a multi-label head trained against the teacher's per-label probabilities, and the results show exact-match
407
+ accuracy, macro-F1 and micro-F1, with 0.5 as the cut-off. Gold labels can be lists or strings like `a|b`.
408
+ Few-shot examples and the cascade are for single-label tasks.
409
+
410
+ For labels with an order, such as 1 to 5 stars, add `ordinal: true`: the results then also show the mean
411
+ absolute error in label steps and the share of predictions at most one step off.
412
+
413
+ `{labels}` (required) becomes a bulleted list of the label names, `{text}` is the input, and an optional
414
+ `{examples}` line holds few-shot examples (it disappears when there are none). The name is what the teacher
415
+ scores. Each label can be a bare id as above, `id: name`, or carry a description that is shown in
416
+ the list (`- bullish: positive for the stock`) but not scored:
417
+
418
+ ```yaml
419
+ labels:
420
+ bearish:
421
+ description: negative for the stock, company or market
422
+ lost_card: lost or stolen card
423
+ ```
424
+
425
+ ## How the label scoring works
426
+
427
+ The teacher never generates free text. For an input x, the prompt is rendered with the model's chat template
428
+ (user message = the instruction with the label list, generation prompt for the assistant). A continuation is one
429
+ way of writing a label followed by the end-of-turn token, `c_1 ... c_m`, with log-probability
430
+
431
+ log p(c | prompt(x)) = sum_j log p(c_j | prompt(x), c_1 ... c_(j-1))
432
+
433
+ Multi-token labels are handled exactly, and the end-of-turn token keeps a label that is a prefix of another
434
+ (`card` versus `card arrival`) from winning by stopping early. Models do not always write a label the way it is
435
+ listed: Qwen2.5 tends to answer a sentiment question with "Bullish" even when the list says "bullish". So each
436
+ label is scored over its common surface forms, the name as written, lowercase, Capitalised and UPPERCASE, each
437
+ with and without a leading space (forms that tokenise identically count once):
438
+
439
+ score(label) = log sum over forms c of p(c | prompt(x))
440
+
441
+ This is verbaliser coverage: it only counts more ways of writing the same answer, and there is no calibration or
442
+ prior correction. All forms share the prefix tree below, but with many labels they still add up; a task file can
443
+ list fewer (`surface_forms: [lowercase, capitalised]` drops UPPERCASE, which tokenises into many pieces, and
444
+ `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
445
+ primary form, the name as written. The scores are cached as JSON lines keyed by a hash of the text, in a file
446
+ named after a fingerprint of the teacher, prompt and continuations, so an interrupted run resumes and a changed
447
+ prompt never reuses old scores.
448
+
449
+ To keep this fast, the part of the prompt before the text is run once and its key/value cache is kept. Each
450
+ text then needs a single forward pass that covers the text and all labels together: the tokens of all continuations
451
+ (every surface form of every label) form a prefix tree, so continuations that start alike, such as
452
+ `card arrival` and `card linking`, share nodes, and a 4D attention mask lets each tree node attend only to the
453
+ prompt, the text and its own ancestors. There is no padding and no per-label copy of the cache. When a task has
454
+ few labels and short texts, several texts are packed into one forward pass the same way, each attending only to
455
+ the prompt and itself (`--teacher-tokens`, default 2048 tokens per pass). Tests check the scores against a plain
456
+ forward pass per continuation, and packed against unpacked scores.
457
+
458
+ The student minimises KL(teacher || student) over the training texts, plus `--ce-weight` times the
459
+ cross-entropy on the K gold labels per class chosen by `--gold-per-class`, if any (standard knowledge
460
+ distillation, Hinton et al., 2015).
461
+
462
+ ## Limitations
463
+
464
+ - **The teacher caps the student.** The student learns to imitate the teacher, mistakes included. A small LLM
465
+ choosing zero-shot among many fine-grained labels can be weak: Qwen2.5-0.5B-Instruct, for example, often
466
+ confuses related Banking77 intents. A bigger teacher (try `--load-4bit` with a 7B model) or a better prompt
467
+ usually helps more than anything on the student side.
468
+ - **Scores are sums, not averages.** Longer verbalisations get lower scores, so very uneven label lengths bias the
469
+ teacher. Keep verbalisations short and of similar length.
470
+ - **Only case and spacing variants are counted.** Plurals, synonyms or rephrasings of a label are not; keep label
471
+ names short and close to how the teacher would answer.
472
+ - **A too-small teacher can collapse.** Qwen2.5-0.5B-Instruct, for example, puts nearly every text into one or two
473
+ labels on the financial-tweets task, whatever the scoring; use 1.5B or larger.
474
+ - **Few-shot examples can hurt.** K examples per class tell the teacher that the classes are equally common,
475
+ and the last examples in the prompt pull it towards their labels. On a skewed task the few-shot teacher can
476
+ be worse than the zero-shot one; compare both (`tinydistill compare`) before relying on it.
477
+ - **Prompt and label order matter.** Zero-shot LLMs are sensitive to wording and to the order of the options.
478
+ - **Teacher cost grows with labels.** Every text is scored against every surface form of every label (shared
479
+ prefixes are computed once). With very many labels use `--shortlist K`; a gold label outside the shortlist
480
+ cannot be learned from the teacher for that text.
481
+ - **Multi-label tasks ask one question per label**, so they cost one forward pass per label and text.
482
+ Inputs are truncated to 128 tokens by default (`--max-length`).
483
+ - **Timings depend on the machine.** The results table states the threads and devices used; rerun `eval` on your
484
+ own hardware.
485
+ - ONNX export uses PyTorch's TorchScript-based exporter.
486
+
487
+ ## Development
488
+
489
+ ```sh
490
+ python -m venv .venv && . .venv/bin/activate
491
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
492
+ pip install -e ".[dev]"
493
+ ruff check . && ruff format --check . && pytest
494
+ ```
495
+
496
+ The tests run on a CPU in a few seconds with tiny random models from `hf-internal-testing`. They check the label
497
+ scores against a direct computation, the distillation loss and metrics against hand-computed values, cache
498
+ resumption, that the ONNX export reproduces the PyTorch logits, and the whole CLI end to end.
499
+
500
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to propose changes, [SECURITY.md](SECURITY.md) for reporting
501
+ vulnerabilities and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for the code of conduct.
502
+
503
+ ## References
504
+
505
+ - G. Hinton, O. Vinyals, J. Dean. [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531). arXiv:1503.02531, 2015.
506
+ - 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.)
507
+ - 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.)
508
+ - 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.
509
+ - N. Reimers, I. Gurevych. [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084). EMNLP, 2019.
510
+ - Qwen Team. [Qwen2.5 Technical Report](https://arxiv.org/abs/2412.15115). arXiv:2412.15115, 2024.
511
+
512
+ ## Licence
513
+
514
+ [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
515
+ their own licences.