mp-juuuns commited on
Commit
470798b
·
verified ·
1 Parent(s): e859ae0

Draw three tables the repository shipped but never showed

Browse files

Where the parameters live: cutting 24L to 4L removes 84% of the layer stack and does not touch the embedding, so the embedding's share climbs from 33.8% to 76.1% — which is why the vocabulary was the next cut. At 39,866 entries it is back to 33.8%, the teacher's own balance.

The ladder's losses: not monotone as totals (0.176 / 0.034 / 0.047) and monotone rising per layer removed (0.0110 / 0.0169 / 0.0235). The last two layers cost 2.1x per layer what the first sixteen did.

Thresholds: the frozen grid selects a threshold that moves up to 4x between seeds of one arm, and every run over-predicts — 34.4% of pairs are positive, predictions land at 41-71%, and one control run returns 1.34 false positives per true one. Added with the advice not to inherit these thresholds.

README_KO.md mirrors all three. SHA256SUMS: 358 -> 362 entries.

.gitattributes CHANGED
@@ -28,3 +28,6 @@ benchmark/figures/16_field_depth_effect.png filter=lfs diff=lfs merge=lfs -text
28
  benchmark/figures/17_field_resolution_floor.png filter=lfs diff=lfs merge=lfs -text
29
  benchmark/figures/18_vocabulary_cost_inversion.png filter=lfs diff=lfs merge=lfs -text
30
  benchmark/figures/19_window_cost.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
28
  benchmark/figures/17_field_resolution_floor.png filter=lfs diff=lfs merge=lfs -text
29
  benchmark/figures/18_vocabulary_cost_inversion.png filter=lfs diff=lfs merge=lfs -text
30
  benchmark/figures/19_window_cost.png filter=lfs diff=lfs merge=lfs -text
31
+ benchmark/figures/20_where_the_parameters_live.png filter=lfs diff=lfs merge=lfs -text
32
+ benchmark/figures/21_ladder_loss.png filter=lfs diff=lfs merge=lfs -text
33
+ benchmark/figures/22_threshold_and_overprediction.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -218,6 +218,30 @@ root. If you want this vocabulary, start from this root.
218
 
219
  ---
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  ## Architecture
222
 
223
  ```
@@ -246,6 +270,38 @@ the map, that choice is in the weights. Cutting the vocabulary does not remove i
246
 
247
  ---
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  ## The path, step by step
250
 
251
  Everything below is measured. Each section is a **situation we assumed**, and it names the data
@@ -840,7 +896,7 @@ described at the top of this card.
840
  | [`root_manifest.json`](root_manifest.json) | its vocabulary rule, layer maps, hashes, why this grid point |
841
  | [`benchmark/full/`](benchmark/full/) | the 61-arm table, the grid, the same-host cuts |
842
  | [`benchmark/cost_probe/`](benchmark/cost_probe/) | the tokenisation-cost and window-cost measurements, scripts and results |
843
- | [`benchmark/figures/`](benchmark/figures/) | the nineteen figures above |
844
  | [`runtime/`](runtime/) | the two relaxed sources of the dedicated runtime, and its own README |
845
  | [`docker/`](docker/) | a container that runs the backbone with a head on it, CPU only |
846
  | [`docker-train/`](docker-train/) | train a classifier on your own data, convert, serve |
 
218
 
219
  ---
220
 
221
+
222
+ ### If you train a head, the threshold is yours to pick
223
+
224
+ One thing the benchmark's own tables show clearly, and this card had not: the decision threshold
225
+ the protocol lands on is unstable, and at whatever it lands on the head over-predicts.
226
+
227
+ ![threshold and over-prediction](benchmark/figures/22_threshold_and_overprediction.png)
228
+
229
+ Across three seeds of one arm the frozen 18-point grid selected 0.10, 0.15 and 0.30, and for
230
+ another it selected 0.05, 0.20 and 0.20 — a 3x and a 4x range for one model on one split, from
231
+ nothing but the seed. The task-blind arms behave the same way: across the thirteen task-blind runs
232
+ of the final test the selected threshold spans 0.05 to 0.45.
233
+
234
+ And every run predicts more positives than the data contains. 34.4% of the (document, label)
235
+ pairs are actually positive; the selected thresholds put predictions at 41% to 71%. One control
236
+ run returns 1.34 false positives for every true one. That is what a threshold picked to maximise
237
+ macro F1 on a 14-label problem with rare labels does — macro F1 rewards getting *something* right
238
+ on the rare labels, and the cheapest way to do that is to say yes more often.
239
+
240
+ None of this is a defect in the backbone; it is a property of the frozen protocol's selection
241
+ rule and of the split's label sparsity. It is here because it is the part of these numbers most
242
+ likely to surprise you when you attach your own head: **do not inherit these thresholds, and
243
+ calibrate on your own data for the precision/recall balance your application wants.**
244
+
245
  ## Architecture
246
 
247
  ```
 
270
 
271
  ---
272
 
273
+
274
+ ### Where the parameters actually live
275
+
276
+ The two cuts had to happen in that order, and the reason is arithmetic rather than judgement.
277
+
278
+ ![where the parameters live](benchmark/figures/20_where_the_parameters_live.png)
279
+
280
+ At 24 layers the embedding table is a third of the model. Cutting to 4 layers removes 84% of the
281
+ layer stack — 498M parameters down to 80M — and touches the embedding not at all, so the model
282
+ only falls from 752M to 334M and the embedding's share climbs from 33.8% to **76.1%**. After the
283
+ depth cut, three quarters of what remained was a lookup table for 248,320 entries that this model
284
+ would never need, which is what made the vocabulary the next thing worth cutting.
285
+
286
+ Cutting it to 39,866 brings the embedding back to 33.8% of the model — the same balance the
287
+ 24-layer teacher had — and the model to 120.6M. The two grid points below this root keep going:
288
+ at 15,380 entries the embedding is 16% and the layer stack is nearly everything left.
289
+
290
+ ### What the ladder itself cost
291
+
292
+ Distillation is three stages, and the tables in this repository record each one's loss.
293
+
294
+ ![the ladder's losses](benchmark/figures/21_ladder_loss.png)
295
+
296
+ Read as totals the sequence is not monotone: 24→8 drops sixteen layers for 0.176, 8→6 drops two
297
+ for 0.034, and then 6→4 drops two for 0.047 — 39% more than the step before it for the same
298
+ number of layers. Read per layer removed it is monotone and rising: 0.0110, 0.0169, 0.0235. The
299
+ last two layers cost 2.1x per layer what the first sixteen did.
300
+
301
+ We do not read a mechanism into that. It is one run per stage with no spread, on a matching
302
+ objective that never sees a label, and the interface loss is larger than the final loss at every
303
+ stage, which is what one would expect when the boundaries being matched are the constraint.
304
+
305
  ## The path, step by step
306
 
307
  Everything below is measured. Each section is a **situation we assumed**, and it names the data
 
896
  | [`root_manifest.json`](root_manifest.json) | its vocabulary rule, layer maps, hashes, why this grid point |
897
  | [`benchmark/full/`](benchmark/full/) | the 61-arm table, the grid, the same-host cuts |
898
  | [`benchmark/cost_probe/`](benchmark/cost_probe/) | the tokenisation-cost and window-cost measurements, scripts and results |
899
+ | [`benchmark/figures/`](benchmark/figures/) | the twenty-two figures above |
900
  | [`runtime/`](runtime/) | the two relaxed sources of the dedicated runtime, and its own README |
901
  | [`docker/`](docker/) | a container that runs the backbone with a head on it, CPU only |
902
  | [`docker-train/`](docker-train/) | train a classifier on your own data, convert, serve |
README_KO.md CHANGED
@@ -194,6 +194,30 @@ qwen35-distill finetune \
194
 
195
  ---
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  ## 아키텍처
198
 
199
  ```
@@ -222,6 +246,37 @@ macro F1 기준 1위입니다. 이 저장소의 예전 `config.json` 파일들
222
 
223
  ---
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  ## 경로, 단계별로
226
 
227
  아래의 모든 것은 측정된 값입니다. 각 절은 **우리가 가정한 상황**이며, 그 상황을 세우기 위해 사용한
@@ -805,7 +860,7 @@ A/B/A, 스레드 수 1/3/4, 라이브 서비스 경로도 모두 일치했습니
805
  | [`root_manifest.json`](root_manifest.json) | 그 어휘 규칙, 계층 맵, 해시, 왜 이 그리드 포인트인지 |
806
  | [`benchmark/full/`](benchmark/full/) | 61개 arm 표, 그리드, 동일 호스트 절단 |
807
  | [`benchmark/cost_probe/`](benchmark/cost_probe/) | 토크나이즈 비용과 윈도 비용 측정 스크립트와 결과 |
808
- | [`benchmark/figures/`](benchmark/figures/) | 위의 열아홉 개 그림 |
809
  | [`runtime/`](runtime/) | 전용 런타임의 완화된 두 소스와 그 자신의 README |
810
  | [`docker/`](docker/) | 헤드를 얹�� 백본을 돌리는 컨테이너, CPU 전용 |
811
  | [`docker-train/`](docker-train/) | 자기 데이터로 분류기를 학습시키고, 변환하고, 서빙 |
 
194
 
195
  ---
196
 
197
+
198
+ ### 헤드를 학습시키신다면, 임계값은 직접 고르셔야 합니다
199
+
200
+ 벤치마크 자신의 표가 분명히 보여 주는데 이 카드가 말하지 않은 것이 하나 있습니다. 프로토콜이
201
+ 고르는 결정 임계값은 불안정하고, 무엇에 안착하든 헤드는 과다예측합니다.
202
+
203
+ ![임계값과 과다예측](benchmark/figures/22_threshold_and_overprediction.png)
204
+
205
+ 한 arm의 세 시드에서 동결된 18점 그리드는 0.10, 0.15, 0.30을 골랐고, 다른 arm에서는 0.05, 0.20,
206
+ 0.20을 골랐습니다. 한 모델, 한 분할에서 오직 시드만 달랐는데 3배와 4배의 범위입니다. task-blind
207
+ arm들도 같습니다. 최종 테스트의 task-blind 실행 열세 개에서 선택된 임계값은 0.05에서 0.45까지
208
+ 퍼져 있습니다.
209
+
210
+ 그리고 모든 실행이 데이터에 있는 것보다 더 많은 양성을 예측합니다. (문서, 레이블) 쌍의 34.4%가
211
+ 실제 양성인데, 선택된 임계값들은 예측을 41%에서 71%에 놓습니다. 한 대조 실행은 참 양성 하나당 거짓
212
+ 양성 1.34개를 돌려줍니다. 드문 레이블이 있는 14개 레이블 문제에서 macro F1을 최대화하도록 고른
213
+ 임계값이 하는 일이 이것입니다 — macro F1은 드문 레이블에서 *무언가*를 맞히는 것을 보상하고, 그
214
+ 가장 싼 방법은 더 자주 예라고 말하는 것입니다.
215
+
216
+ 이 중 어느 것도 백본의 결함이 아니라 동결된 프로토콜의 선택 규칙과 이 분할의 레이블 희소성의
217
+ 성질입니다. 이것을 여기 적는 이유는, 여러분이 자신의 헤드를 붙이실 때 이 수치들 중 가장 놀라기
218
+ 쉬운 부분이기 때문입니다. **이 임계값들을 그대로 물려받지 마시고, 여러분의 응용이 원하는
219
+ 정밀도/재현율 균형에 맞게 여러분의 데이터에서 캘리브레이션하십시오.**
220
+
221
  ## 아키텍처
222
 
223
  ```
 
246
 
247
  ---
248
 
249
+
250
+ ### 파라미터는 실제로 어디에 있는가
251
+
252
+ 두 번의 절단이 그 순서여야 했던 이유는 판단이 아니라 산수입니다.
253
+
254
+ ![파라미터가 있는 곳](benchmark/figures/20_where_the_parameters_live.png)
255
+
256
+ 24계층에서 임베딩 테이블은 모델의 3분의 1입니다. 4계층으로 자르면 계층 스택의 84% — 498M에서
257
+ 80M으로 — 가 사라지지만 임베딩은 전혀 건드려지지 않으므로, 모델은 752M에서 334M으로만 줄고
258
+ 임베딩의 비중은 33.8%에서 **76.1%**로 올라갑니다. 깊이를 자르고 나니 남은 것의 4분의 3이 이 모델이
259
+ 결코 쓰지 않을 248,320개 항목의 참조표였고, 그래서 다음에 자를 가치가 있는 것이 어휘였습니다.
260
+
261
+ 39,866으로 자르면 임베딩은 모델의 33.8%로 돌아오는데, 이는 24계층 교사가 가지고 있던 것과 같은
262
+ 균형이며, 모델은 120.6M이 됩니다. 이 루트 아래의 두 그리드 포인트는 더 내려갑니다. 15,380 항목에서는
263
+ 임베딩이 16%이고 남은 것의 거의 전부가 계층 스택입니다.
264
+
265
+ ### 사다리 자체는 무엇을 치렀는가
266
+
267
+ 증류는 세 단계이고, 이 저장소의 표가 각 단계의 손실을 기록하고 있습니다.
268
+
269
+ ![사다리의 손실](benchmark/figures/21_ladder_loss.png)
270
+
271
+ 총합으로 읽으면 순서가 단조롭지 않습니다. 24→8은 열여섯 계층을 0.176에, 8→6은 두 계층을 0.034에,
272
+ 그리고 6→4는 두 계층을 0.047에 — 같은 계층 수에 대해 바로 앞 단계보다 39% 더 — 떨어뜨립니다.
273
+ 제거된 계층당으로 읽으면 단조롭게 올라갑니다. 0.0110, 0.0169, 0.0235. 마지막 두 계층은 처음 열여섯
274
+ 계층이 치른 값의 계층당 2.1배를 치릅니다.
275
+
276
+ 여기에서 기전을 읽어 내지는 않습니다. 단계당 한 번의 실행이라 산포가 없고, 레이블을 한 번도 보지
277
+ 않는 정합 목적함수이며, 모든 단계에서 인터페이스 손실이 최종 손실보다 큰데 이는 정합 대상이 되는
278
+ 경계가 제약일 때 예상되는 모습입니다.
279
+
280
  ## 경로, 단계별로
281
 
282
  아래의 모든 것은 측정된 값입니다. 각 절은 **우리가 가정한 상황**이며, 그 상황을 세우기 위해 사용한
 
860
  | [`root_manifest.json`](root_manifest.json) | 그 어휘 규칙, 계층 맵, 해시, 왜 이 그리드 포인트인지 |
861
  | [`benchmark/full/`](benchmark/full/) | 61개 arm 표, 그리드, 동일 호스트 절단 |
862
  | [`benchmark/cost_probe/`](benchmark/cost_probe/) | 토크나이즈 비용과 윈도 비용 측정 스크립트와 결과 |
863
+ | [`benchmark/figures/`](benchmark/figures/) | 위의 스물두 개 그림 |
864
  | [`runtime/`](runtime/) | 전용 런타임의 완화된 두 소스와 그 자신의 README |
865
  | [`docker/`](docker/) | 헤드를 얹�� 백본을 돌리는 컨테이너, CPU 전용 |
866
  | [`docker-train/`](docker-train/) | 자기 데이터로 분류기를 학습시키고, 변환하고, 서빙 |
SHA256SUMS CHANGED
@@ -1,6 +1,6 @@
1
  77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 LICENSE
2
- 9cf91e7d56c96ed3901391cdb4ea14e43e78eeaaf7b647b8ae59ae2811e7a02b README.md
3
- 30d3e1ed49635d513c27e5b615c425d0c56bfb591cfef2a112bcaf1ef7a11d08 README_KO.md
4
  bf4073de38c3905effd2dcce6f38894f325c8a0a79bedfb2779f0545c86b9c9f benchmark/BENCHMARK_CARD.md
5
  e01bd7ac59a3212e44b77e5fcfbc7c41ec91d74c998306d2438209d6ef1a3aa7 benchmark/arm_summary.csv
6
  2c544c3907c4f592d724f658850b8c6b9634845cb5b50a66ebd2ea6c5d48ce0b benchmark/compression_ladder.csv
@@ -39,6 +39,9 @@ cfb3071db4077bed7aee06c141f5f12873779a732516242580c889fb223c14fa benchmark/figu
39
  dc621e266a694496ba30d9d5c03e89e4d6c65616f5c25df0216d012102cd3f53 benchmark/figures/17_field_resolution_floor.png
40
  8941a6f6bcf3a354d5a7b3b53367c36508a2aed73c6094df7536401d41e0cfd7 benchmark/figures/18_vocabulary_cost_inversion.png
41
  261f2fddb02eb02d2d7d696d650405cea153941eb6e93073bf21483732f9db0e benchmark/figures/19_window_cost.png
 
 
 
42
  7c238ed969ed64ac7c63084c663ecc531b618622199a77b09d28f461db11a657 benchmark/figures/README.md
43
  afd203646513c49140a25ef2a562a064286a4fa5efb495e6a73f83f0106e7933 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.csv
44
  22f531ea586f9c79ac8f114345cec56e74ea5ed97aa3726f7310018666ae6312 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.pdf
@@ -54,6 +57,7 @@ c3312e98b8abbbdc488e5dfde77c4110732e8d08abe9ed931bd985ad799ecad8 benchmark/figu
54
  d42a85f828b719a6b24e226013ae660f3ff88280b256490b9eb08e19616c96d7 benchmark/figures/make_cost_figures.py
55
  3f6b1b82af1619afab8c753cd836040a7353a3226646b8b99fb641617e4ecec7 benchmark/figures/make_more_figures.py
56
  5f6b1979dcb7ff4ee897659b2f902dd3cc36339987f42f681bff7be5ae06d304 benchmark/figures/make_readout_figures.py
 
57
  078b4f34897e0a1b9bcff66ce21ba0feb28d72c86eaa8cef608f110c48f52f2e benchmark/figures/v128k_compression.csv
58
  014da12a435e3404b31470e2642d180dc111eb608fb8513fd993097019698b7a benchmark/figures/v128k_quality_efficiency.csv
59
  644e5b7469c0b4e0b63d5ecaa1c0f2fc8e07af8bb5d9e4d3ae5a88166de374f3 benchmark/figures/v128k_quality_storage.png
 
1
  77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 LICENSE
2
+ 92ea56cfedc17d67ad2dcd4fa3d40378ac6a8ed27456bb24721284030e3349b3 README.md
3
+ a62448e225d5b10ec7a8bc998be7cc7db209ba59f1ff6ce66e4b2293ddbaa2b6 README_KO.md
4
  bf4073de38c3905effd2dcce6f38894f325c8a0a79bedfb2779f0545c86b9c9f benchmark/BENCHMARK_CARD.md
5
  e01bd7ac59a3212e44b77e5fcfbc7c41ec91d74c998306d2438209d6ef1a3aa7 benchmark/arm_summary.csv
6
  2c544c3907c4f592d724f658850b8c6b9634845cb5b50a66ebd2ea6c5d48ce0b benchmark/compression_ladder.csv
 
39
  dc621e266a694496ba30d9d5c03e89e4d6c65616f5c25df0216d012102cd3f53 benchmark/figures/17_field_resolution_floor.png
40
  8941a6f6bcf3a354d5a7b3b53367c36508a2aed73c6094df7536401d41e0cfd7 benchmark/figures/18_vocabulary_cost_inversion.png
41
  261f2fddb02eb02d2d7d696d650405cea153941eb6e93073bf21483732f9db0e benchmark/figures/19_window_cost.png
42
+ 87c6e141f8f28c293382e3198efe360fc4a4d6bdd42fc5b73bc2eb5a170eb5a3 benchmark/figures/20_where_the_parameters_live.png
43
+ 8dc8781f83f6c3de22b287b86db544da986e0225345db5e43dd77e54ab4b1443 benchmark/figures/21_ladder_loss.png
44
+ b08436d88b8ad64dccebf9d04629fd1dfbd1e5f21e0908f2a5284f4b7243642d benchmark/figures/22_threshold_and_overprediction.png
45
  7c238ed969ed64ac7c63084c663ecc531b618622199a77b09d28f461db11a657 benchmark/figures/README.md
46
  afd203646513c49140a25ef2a562a064286a4fa5efb495e6a73f83f0106e7933 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.csv
47
  22f531ea586f9c79ac8f114345cec56e74ea5ed97aa3726f7310018666ae6312 benchmark/figures/cross_task/05_why_4l_edge_tradeoff.pdf
 
57
  d42a85f828b719a6b24e226013ae660f3ff88280b256490b9eb08e19616c96d7 benchmark/figures/make_cost_figures.py
58
  3f6b1b82af1619afab8c753cd836040a7353a3226646b8b99fb641617e4ecec7 benchmark/figures/make_more_figures.py
59
  5f6b1979dcb7ff4ee897659b2f902dd3cc36339987f42f681bff7be5ae06d304 benchmark/figures/make_readout_figures.py
60
+ 8acf0dcd72bb77dc650aa8253db35de2d31df0a67ef20f928075f82297242283 benchmark/figures/make_structure_figures.py
61
  078b4f34897e0a1b9bcff66ce21ba0feb28d72c86eaa8cef608f110c48f52f2e benchmark/figures/v128k_compression.csv
62
  014da12a435e3404b31470e2642d180dc111eb608fb8513fd993097019698b7a benchmark/figures/v128k_quality_efficiency.csv
63
  644e5b7469c0b4e0b63d5ecaa1c0f2fc8e07af8bb5d9e4d3ae5a88166de374f3 benchmark/figures/v128k_quality_storage.png
benchmark/figures/20_where_the_parameters_live.png ADDED

Git LFS Details

  • SHA256: 87c6e141f8f28c293382e3198efe360fc4a4d6bdd42fc5b73bc2eb5a170eb5a3
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
benchmark/figures/21_ladder_loss.png ADDED

Git LFS Details

  • SHA256: 8dc8781f83f6c3de22b287b86db544da986e0225345db5e43dd77e54ab4b1443
  • Pointer size: 131 Bytes
  • Size of remote file: 165 kB
benchmark/figures/22_threshold_and_overprediction.png ADDED

Git LFS Details

  • SHA256: b08436d88b8ad64dccebf9d04629fd1dfbd1e5f21e0908f2a5284f4b7243642d
  • Pointer size: 131 Bytes
  • Size of remote file: 244 kB
benchmark/figures/make_structure_figures.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Three figures from tables the card shipped but never drew.
2
+
3
+ 20 where the parameters actually live, and why the vocabulary was cut after the layers
4
+ 21 the distillation ladder's own losses: the difficulty is not monotone
5
+ 22 the threshold the protocol selects moves between seeds, and every arm over-predicts
6
+
7
+ Run from benchmark/figures/ . Reads ../compression_ladder.csv and ../seed_metrics.csv,
8
+ both of which ship in this repository.
9
+ """
10
+ import csv
11
+ import json
12
+ import os
13
+ import statistics as st
14
+
15
+ import matplotlib
16
+ matplotlib.use("Agg")
17
+ import matplotlib.pyplot as plt
18
+ from matplotlib.lines import Line2D
19
+ from matplotlib.patches import Patch
20
+
21
+ HERE = os.path.dirname(os.path.abspath(__file__))
22
+ BENCH = os.path.join(HERE, "..") + os.sep # benchmark/ holds the CSVs this reads
23
+ OUT = HERE + os.sep
24
+
25
+ INK, MUTED, FAINT, GRID = "#1a1a1a", "#6a6a72", "#9a9aa2", "#e6e6ea"
26
+ ROOTC, GRIDC, QWENC, ENCC, DOWN = "#1a5496", "#0d7a52", "#b05512", "#9a9aa2", "#c0392b"
27
+ H = 1024 # hidden size, unchanged down the whole ladder
28
+
29
+
30
+ def style(ax, xgrid=True, ygrid=True):
31
+ ax.grid(axis="both" if (xgrid and ygrid) else ("x" if xgrid else "y"), color=GRID, lw=.8)
32
+ ax.set_axisbelow(True)
33
+ for s in ("top", "right"):
34
+ ax.spines[s].set_visible(False)
35
+ for s in ("bottom", "left"):
36
+ ax.spines[s].set_color("#c9c9cf")
37
+ ax.tick_params(length=0, colors="#4a4a52")
38
+
39
+
40
+ def footer(fig, text):
41
+ fig.text(.006, .012, text, fontsize=8.6, color=MUTED, ha="left")
42
+
43
+
44
+ def fig_parameters():
45
+ """Two cuts in sequence: depth first, then vocabulary. Embedding share is the story."""
46
+ ladder = list(csv.DictReader(open(BENCH + "compression_ladder.csv")))
47
+ emb248 = 248320 * H
48
+ depth = [(f"{r['layers']}L", int(r["parameters"]), emb248) for r in ladder]
49
+ layer_at_4l = depth[-1][1] - emb248
50
+ vocab = [("248,320", 248320), ("128,000", 128000), ("72,455", 72455),
51
+ ("39,866", 39866), ("23,551", 23551), ("15,380", 15380)]
52
+ vocab = [(lab, v * H + layer_at_4l + 1024, v * H) for lab, v in vocab]
53
+
54
+ fig, (ax, ax2) = plt.subplots(1, 2, figsize=(14.6, 6.4))
55
+ fig.patch.set_facecolor("white")
56
+
57
+ for axis, data, xlab, title, sub in (
58
+ (ax, depth, "layers kept (vocabulary held at 248,320)",
59
+ "Cut 1 — depth: 24 layers to 4",
60
+ "the layer stack loses 84% of its parameters and the embedding does not move"),
61
+ (ax2, vocab, "vocabulary entries kept (depth held at 4 layers)",
62
+ "Cut 2 — vocabulary: 248,320 entries to 39,866",
63
+ "by then the embedding was three quarters of the model \u2014 which is why it came second")):
64
+ labs = [d[0] for d in data]
65
+ tot = [d[1] / 1e6 for d in data]
66
+ embm = [d[2] / 1e6 for d in data]
67
+ lays = [t - e for t, e in zip(tot, embm)]
68
+ axis.bar(labs, embm, width=.62, color=ROOTC, zorder=3, label="embedding table")
69
+ axis.bar(labs, lays, bottom=embm, width=.62, color=ENCC, zorder=3, label="layer stack")
70
+ for i, (t, e) in enumerate(zip(tot, embm)):
71
+ axis.text(i, t + max(tot) * .035, f"{t:,.0f}M", ha="center", fontsize=9.2,
72
+ color=INK, fontweight="bold")
73
+ axis.text(i, e / 2, f"{e/t:.0%}", ha="center", va="center", fontsize=9,
74
+ color="white", fontweight="bold")
75
+ axis.set_xlabel(xlab, fontsize=10)
76
+ axis.set_ylabel("parameters (millions)", fontsize=10)
77
+ axis.set_ylim(0, max(tot) * 1.24)
78
+ style(axis, xgrid=False)
79
+ axis.set_title(f"{title}\n{sub}", fontsize=11.5, loc="left", color=INK, pad=12)
80
+
81
+ # mark the two endpoints that actually shipped
82
+ ax.get_xticklabels()[-1].set_color(ROOTC)
83
+ ax.get_xticklabels()[-1].set_fontweight("bold")
84
+ ax2.get_xticklabels()[3].set_color(ROOTC)
85
+ ax2.get_xticklabels()[3].set_fontweight("bold")
86
+ ax2.annotate("this root", (3, vocab[3][1] / 1e6), textcoords="offset points",
87
+ xytext=(0, 52), ha="center", fontsize=9.6, color=ROOTC, fontweight="bold",
88
+ arrowprops=dict(arrowstyle="-|>", color=ROOTC, lw=1.3,
89
+ shrinkA=2, shrinkB=22))
90
+ ax.legend(handles=[Patch(color=ROOTC, label="embedding table"),
91
+ Patch(color=ENCC, label="layer stack")],
92
+ fontsize=9, frameon=False, loc="upper right")
93
+
94
+ footer(fig, "Percentages are the embedding's share of the model. Cutting depth alone raised that "
95
+ "share from 33.8% to 76.1%; cutting the vocabulary brought it back to 33.8%, the "
96
+ "same balance the 24-layer teacher had. Hidden size is 1,024 throughout.")
97
+ fig.tight_layout(rect=(0, .055, 1, 1))
98
+ fig.savefig(OUT + "20_where_the_parameters_live.png", dpi=170, facecolor="white")
99
+ plt.close(fig)
100
+
101
+
102
+ def fig_ladder_loss():
103
+ rows = [r for r in csv.DictReader(open(BENCH + "compression_ladder.csv")) if r["kd_stage"]]
104
+ stages = [r["kd_stage"].replace("to", " → ") for r in rows]
105
+ tot = [float(r["mean_total_loss"]) for r in rows]
106
+ itf = [float(r["mean_interface_loss"]) for r in rows]
107
+ fin = [float(r["mean_final_loss"]) for r in rows]
108
+ dropped = [16, 2, 2]
109
+
110
+ fig, (ax, ax2) = plt.subplots(1, 2, figsize=(13.8, 5.8),
111
+ gridspec_kw={"width_ratios": [1.12, 1]})
112
+ fig.patch.set_facecolor("white")
113
+
114
+ x = range(len(stages))
115
+ w = .26
116
+ for off, vals, c, lab in ((-w, itf, ROOTC, "interface loss"),
117
+ (0.0, tot, GRIDC, "total loss"),
118
+ (w, fin, QWENC, "final-layer loss")):
119
+ ax.bar([i + off for i in x], vals, width=w * .92, color=c, zorder=3, label=lab)
120
+ for i, v in zip(x, vals):
121
+ ax.text(i + off, v + .004, f"{v:.3f}", ha="center", fontsize=8.4,
122
+ color=INK, fontweight="bold")
123
+ ax.set_xticks(list(x))
124
+ ax.set_xticklabels([f"{s}\n{d} layers removed" for s, d in zip(stages, dropped)], fontsize=9.4)
125
+ ax.set_ylabel("mean KD loss over the distillation run", fontsize=10)
126
+ ax.set_ylim(0, max(itf) * 1.22)
127
+ style(ax, xgrid=False)
128
+ ax.legend(fontsize=9, frameon=False, loc="upper right", ncol=1)
129
+ ax.set_title("The ladder's own losses are not monotone\n"
130
+ "24\u21928 drops 16 layers for 0.176; 8\u21926 drops 2 for 0.034; then 6\u21924 "
131
+ "drops 2 for 0.047",
132
+ fontsize=11.5, loc="left", color=INK, pad=12)
133
+
134
+ # per-layer-removed difficulty
135
+ per = [t / d for t, d in zip(tot, dropped)]
136
+ ax2.plot(list(x), per, "-o", color=ROOTC, lw=2.4, ms=11, mec="white", mew=1.7, zorder=4)
137
+ for i, v in zip(x, per):
138
+ ax2.annotate(f"{v:.4f}", (i, v), textcoords="offset points", xytext=(0, 14),
139
+ ha="center", fontsize=9.2, color=INK, fontweight="bold")
140
+ ax2.set_xticks(list(x))
141
+ ax2.set_xticklabels(stages, fontsize=9.6)
142
+ ax2.set_xlim(-.45, len(stages) - .55)
143
+ ax2.set_ylabel("total loss per layer removed", fontsize=10)
144
+ ax2.set_ylim(0, max(per) * 1.30)
145
+ style(ax2, xgrid=False)
146
+ ax2.set_title("Per layer removed, each step is harder than the last\n"
147
+ "the final two layers cost 2.1x per layer what the first sixteen did",
148
+ fontsize=11.5, loc="left", color=INK, pad=12)
149
+
150
+ footer(fig, "Interface loss matches hidden states at the kept-layer boundaries; final loss matches "
151
+ "the last hidden state. No logits and no labels enter either. One run per stage, so "
152
+ "there is no spread to report and no significance is claimed.")
153
+ fig.tight_layout(rect=(0, .055, 1, 1))
154
+ fig.savefig(OUT + "21_ladder_loss.png", dpi=170, facecolor="white")
155
+ plt.close(fig)
156
+
157
+
158
+ def fig_thresholds():
159
+ rows = list(csv.DictReader(open(BENCH + "seed_metrics.csv")))
160
+ NAMES = {"task_agnostic_base": "task-free 4L root",
161
+ "structural_copy_control": "structural copy control",
162
+ "existing_specialized_separate_lineage": "specialised 4L, separate lineage"}
163
+ arms = list(dict.fromkeys(r["arm"] for r in rows))
164
+ actual = [(int(r["tp"]) + int(r["fn"])) / (55 * 14) for r in rows]
165
+ assert max(actual) - min(actual) < 1e-9
166
+ actual = actual[0]
167
+
168
+ fig, (ax, ax2) = plt.subplots(1, 2, figsize=(14.2, 5.8))
169
+ fig.patch.set_facecolor("white")
170
+
171
+ colours = {a: c for a, c in zip(arms, (ROOTC, QWENC, GRIDC))}
172
+ for a in arms:
173
+ rs = [r for r in rows if r["arm"] == a]
174
+ th = [float(r["threshold"]) for r in rs]
175
+ pp = [float(r["predicted_positive_rate"]) for r in rs]
176
+ ax.plot(th, pp, "o", ms=11, color=colours[a], mec="white", mew=1.6, zorder=4)
177
+ for r, t, p in zip(rs, th, pp):
178
+ ax.annotate(f"seed {r['seed']}", (t, p), textcoords="offset points",
179
+ xytext=(11, -3), fontsize=8.2, color=MUTED)
180
+ ax.axhline(actual, color=DOWN, ls=(0, (5, 3)), lw=1.6, zorder=3)
181
+ ax.text(.328, actual + .006, f"actual positive rate {actual:.3f}",
182
+ fontsize=8.8, color=DOWN, ha="right", va="bottom")
183
+ ax.set_xlabel("threshold the frozen grid selected for that run", fontsize=10)
184
+ ax.set_ylabel("fraction of (document, label) pairs predicted positive", fontsize=10)
185
+ ax.set_xlim(.02, .335)
186
+ ax.set_ylim(.335, .775)
187
+ style(ax)
188
+ ax.legend(handles=[Line2D([], [], marker="o", ls="", color=colours[a], ms=9, label=NAMES[a])
189
+ for a in arms], fontsize=8.8, frameon=False, loc="upper right")
190
+ ax.set_title("Every run predicts more positives than the data has\n"
191
+ "and the threshold the grid selects moves up to 4x between seeds of one arm",
192
+ fontsize=11.5, loc="left", color=INK, pad=12)
193
+
194
+ labels, tps, fps, fns, cols = [], [], [], [], []
195
+ for a in arms:
196
+ for r in [r for r in rows if r["arm"] == a]:
197
+ labels.append(f"{NAMES[a].split(',')[0]}\nseed {r['seed']} thr {r['threshold']}")
198
+ tps.append(int(r["tp"]))
199
+ fps.append(int(r["fp"]))
200
+ fns.append(int(r["fn"]))
201
+ cols.append(colours[a])
202
+ y = range(len(labels))
203
+ ax2.barh(list(y), tps, height=.6, color=ROOTC, zorder=3, label="true positives")
204
+ ax2.barh(list(y), fps, left=tps, height=.6, color=QWENC, zorder=3, label="false positives")
205
+ ax2.barh(list(y), fns, left=[t + f for t, f in zip(tps, fps)], height=.6,
206
+ color=ENCC, zorder=3, label="false negatives")
207
+ for i, (t, f) in enumerate(zip(tps, fps)):
208
+ ax2.text(t + f + 12, i, f"{f/t:.2f} fp per tp", va="center", fontsize=8.4,
209
+ color=DOWN if f > t else MUTED, fontweight="bold" if f > t else "normal")
210
+ ax2.set_yticks(list(y))
211
+ ax2.set_yticklabels(labels, fontsize=7.8)
212
+ ax2.set_xlabel("(document, label) pairs, out of 770", fontsize=10)
213
+ ax2.set_xlim(0, 720)
214
+ ax2.invert_yaxis()
215
+ style(ax2, xgrid=True, ygrid=False)
216
+ ax2.spines["left"].set_visible(False)
217
+ ax2.legend(fontsize=8.6, frameon=False, loc="lower right", ncol=1)
218
+ ax2.set_title("What that costs, run by run\n"
219
+ "one control run returns more false positives than true ones",
220
+ fontsize=11.5, loc="left", color=INK, pad=12)
221
+
222
+ footer(fig, "Opened 56-document calibration split, 55 scored articles x 14 labels. If you train a "
223
+ "head on this backbone, expect to pick the threshold yourself: the protocol's own grid "
224
+ "lands on a different one for every seed.")
225
+ fig.tight_layout(rect=(0, .055, 1, 1))
226
+ fig.savefig(OUT + "22_threshold_and_overprediction.png", dpi=170, facecolor="white")
227
+ plt.close(fig)
228
+
229
+
230
+ if __name__ == "__main__":
231
+ os.makedirs(OUT, exist_ok=True)
232
+ for fn in (fig_parameters, fig_ladder_loss, fig_thresholds):
233
+ fn()
234
+ print("ok", fn.__name__)