XiaoyanLi commited on
Commit
83112d8
·
verified ·
1 Parent(s): aea8aec

Initial upload: code, training data, tokenizers, notes

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. .gitignore +24 -0
  3. data/8_sample_B/train.txt +3 -0
  4. data/8_sample_C/train.txt +3 -0
  5. data/rule_calibration.json +360 -0
  6. data/rule_vocab_categories.json +0 -0
  7. data/vocab.txt +0 -0
  8. experiments.csv +0 -0
  9. models/tokenizer/special_tokens_map.json +9 -0
  10. models/tokenizer/tokenizer.json +0 -0
  11. models/tokenizer/tokenizer_config.json +54 -0
  12. models/tokenizer_morfessor/morfessor.bin +3 -0
  13. models/tokenizer_morfessor/special_tokens_map.json +9 -0
  14. models/tokenizer_morfessor/tokenizer.json +0 -0
  15. models/tokenizer_morfessor/tokenizer_config.json +54 -0
  16. notes/babylm_leaderboard_2025.csv +91 -0
  17. notes/encyclopedia.md +0 -0
  18. notes/experiment_log.md +111 -0
  19. notes/experiment_plan.md +685 -0
  20. notes/progress_report.md +273 -0
  21. notes/ref_datasets.md +440 -0
  22. notes/ref_leaderboard_2025.md +54 -0
  23. requirements.txt +40 -0
  24. scripts/01_data/analyze_blimp_coverage.py +123 -0
  25. scripts/01_data/find_oov_words.py +75 -0
  26. scripts/01_data/run_all.py +190 -0
  27. scripts/01_data/stage1_noise_filter.py +189 -0
  28. scripts/01_data/stage2_oov_filter.py +158 -0
  29. scripts/01_data/stage3_dedup.py +119 -0
  30. scripts/01_data/stage4_preprocess.py +372 -0
  31. scripts/01_data/stage5_rule_label.py +1076 -0
  32. scripts/01_data/stage5a_vocab_build.py +800 -0
  33. scripts/01_data/stage6_qwen_label.py +292 -0
  34. scripts/01_data/stage7a_extract_eval.py +192 -0
  35. scripts/01_data/stage8_config.py +224 -0
  36. scripts/01_data/stage8_sample_A.py +129 -0
  37. scripts/01_data/stage8_sample_B.py +192 -0
  38. scripts/01_data/stage8_sample_C.py +198 -0
  39. scripts/01_data/stage8_sample_D.py +500 -0
  40. scripts/01_data/stage9_paraphrase.py +452 -0
  41. scripts/01_data/vocab_coverage.py +99 -0
  42. scripts/02_model/train_tokenizer.py +186 -0
  43. scripts/02_model/train_tokenizer_morfessor.py +361 -0
  44. scripts/03_training/__init__.py +0 -0
  45. scripts/03_training/compute_aoa.py +287 -0
  46. scripts/03_training/config.py +185 -0
  47. scripts/03_training/configs/exp_A.yaml +48 -0
  48. scripts/03_training/configs/exp_B.yaml +39 -0
  49. scripts/03_training/configs/exp_C.yaml +39 -0
  50. scripts/03_training/configs/exp_D.yaml +39 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/8_sample_B/train.txt filter=lfs diff=lfs merge=lfs -text
37
+ data/8_sample_C/train.txt filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Secrets
2
+ .env
3
+
4
+ # Large model (18GB, download separately)
5
+ models/qwen3.5-9b/
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+
12
+ # Virtual environments
13
+ envs/
14
+
15
+ # Training artifacts (regenerated per experiment)
16
+ checkpoints/*
17
+ experiment_results/*
18
+
19
+ # Temp
20
+ .cache/
21
+ .tmp/
22
+
23
+ # OS
24
+ .DS_Store
data/8_sample_B/train.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2c715e81e23c39b497a396e10702c248e0bdbd72c68562b58eb3d1d030e88098
3
+ size 53928171
data/8_sample_C/train.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5ff8f32aa30fcd8338c90b2251527a9793a3f36150e7ebc4a37b0a679559567a
3
+ size 53840442
data/rule_calibration.json ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "summary": {
3
+ "n_tasks": 70,
4
+ "avg_recall": 0.9068,
5
+ "n_high_recall": 56,
6
+ "n_low_recall": 4
7
+ },
8
+ "tasks": {
9
+ "adjunct_island": {
10
+ "total": 928,
11
+ "hits": 928,
12
+ "recall": 1.0
13
+ },
14
+ "anaphor_gender_agreement": {
15
+ "total": 971,
16
+ "hits": 942,
17
+ "recall": 0.9701
18
+ },
19
+ "anaphor_number_agreement": {
20
+ "total": 931,
21
+ "hits": 931,
22
+ "recall": 1.0
23
+ },
24
+ "animate_subject_passive": {
25
+ "total": 895,
26
+ "hits": 895,
27
+ "recall": 1.0
28
+ },
29
+ "animate_subject_trans": {
30
+ "total": 923,
31
+ "hits": 880,
32
+ "recall": 0.9534
33
+ },
34
+ "causative": {
35
+ "total": 818,
36
+ "hits": 417,
37
+ "recall": 0.5098
38
+ },
39
+ "complex_NP_island": {
40
+ "total": 846,
41
+ "hits": 846,
42
+ "recall": 1.0
43
+ },
44
+ "coordinate_structure_constraint_complex_left_branch": {
45
+ "total": 906,
46
+ "hits": 906,
47
+ "recall": 1.0
48
+ },
49
+ "coordinate_structure_constraint_object_extraction": {
50
+ "total": 949,
51
+ "hits": 949,
52
+ "recall": 1.0
53
+ },
54
+ "determiner_noun_agreement_1": {
55
+ "total": 929,
56
+ "hits": 929,
57
+ "recall": 1.0
58
+ },
59
+ "determiner_noun_agreement_2": {
60
+ "total": 931,
61
+ "hits": 931,
62
+ "recall": 1.0
63
+ },
64
+ "determiner_noun_agreement_irregular_1": {
65
+ "total": 681,
66
+ "hits": 681,
67
+ "recall": 1.0
68
+ },
69
+ "determiner_noun_agreement_irregular_2": {
70
+ "total": 820,
71
+ "hits": 820,
72
+ "recall": 1.0
73
+ },
74
+ "determiner_noun_agreement_with_adj_2": {
75
+ "total": 941,
76
+ "hits": 941,
77
+ "recall": 1.0
78
+ },
79
+ "determiner_noun_agreement_with_adj_irregular_1": {
80
+ "total": 718,
81
+ "hits": 718,
82
+ "recall": 1.0
83
+ },
84
+ "determiner_noun_agreement_with_adj_irregular_2": {
85
+ "total": 840,
86
+ "hits": 840,
87
+ "recall": 1.0
88
+ },
89
+ "determiner_noun_agreement_with_adjective_1": {
90
+ "total": 933,
91
+ "hits": 933,
92
+ "recall": 1.0
93
+ },
94
+ "distractor_agreement_relational_noun": {
95
+ "total": 788,
96
+ "hits": 438,
97
+ "recall": 0.5558
98
+ },
99
+ "distractor_agreement_relative_clause": {
100
+ "total": 871,
101
+ "hits": 871,
102
+ "recall": 1.0
103
+ },
104
+ "drop_argument": {
105
+ "total": 920,
106
+ "hits": 511,
107
+ "recall": 0.5554
108
+ },
109
+ "ellipsis_n_bar_2": {
110
+ "total": 828,
111
+ "hits": 72,
112
+ "recall": 0.087
113
+ },
114
+ "existential_there_object_raising": {
115
+ "total": 812,
116
+ "hits": 812,
117
+ "recall": 1.0
118
+ },
119
+ "existential_there_quantifiers_1": {
120
+ "total": 930,
121
+ "hits": 930,
122
+ "recall": 1.0
123
+ },
124
+ "existential_there_quantifiers_2": {
125
+ "total": 911,
126
+ "hits": 911,
127
+ "recall": 1.0
128
+ },
129
+ "existential_there_subject_raising": {
130
+ "total": 924,
131
+ "hits": 924,
132
+ "recall": 1.0
133
+ },
134
+ "expletive_it_object_raising": {
135
+ "total": 759,
136
+ "hits": 759,
137
+ "recall": 1.0
138
+ },
139
+ "inchoative": {
140
+ "total": 855,
141
+ "hits": 384,
142
+ "recall": 0.4491
143
+ },
144
+ "intransitive": {
145
+ "total": 868,
146
+ "hits": 109,
147
+ "recall": 0.1256
148
+ },
149
+ "irregular_past_participle_adjectives": {
150
+ "total": 961,
151
+ "hits": 961,
152
+ "recall": 1.0
153
+ },
154
+ "irregular_past_participle_verbs": {
155
+ "total": 942,
156
+ "hits": 900,
157
+ "recall": 0.9554
158
+ },
159
+ "irregular_plural_subject_verb_agreement_1": {
160
+ "total": 804,
161
+ "hits": 747,
162
+ "recall": 0.9291
163
+ },
164
+ "irregular_plural_subject_verb_agreement_2": {
165
+ "total": 892,
166
+ "hits": 881,
167
+ "recall": 0.9877
168
+ },
169
+ "left_branch_island_echo_question": {
170
+ "total": 947,
171
+ "hits": 947,
172
+ "recall": 1.0
173
+ },
174
+ "left_branch_island_simple_question": {
175
+ "total": 951,
176
+ "hits": 951,
177
+ "recall": 1.0
178
+ },
179
+ "matrix_question_npi_licensor_present": {
180
+ "total": 929,
181
+ "hits": 929,
182
+ "recall": 1.0
183
+ },
184
+ "npi_present_1": {
185
+ "total": 909,
186
+ "hits": 909,
187
+ "recall": 1.0
188
+ },
189
+ "npi_present_2": {
190
+ "total": 914,
191
+ "hits": 914,
192
+ "recall": 1.0
193
+ },
194
+ "only_npi_licensor_present": {
195
+ "total": 882,
196
+ "hits": 882,
197
+ "recall": 1.0
198
+ },
199
+ "only_npi_scope": {
200
+ "total": 837,
201
+ "hits": 837,
202
+ "recall": 1.0
203
+ },
204
+ "passive_1": {
205
+ "total": 840,
206
+ "hits": 777,
207
+ "recall": 0.925
208
+ },
209
+ "passive_2": {
210
+ "total": 903,
211
+ "hits": 834,
212
+ "recall": 0.9236
213
+ },
214
+ "principle_A_c_command": {
215
+ "total": 946,
216
+ "hits": 946,
217
+ "recall": 1.0
218
+ },
219
+ "principle_A_case_1": {
220
+ "total": 912,
221
+ "hits": 912,
222
+ "recall": 1.0
223
+ },
224
+ "principle_A_case_2": {
225
+ "total": 915,
226
+ "hits": 915,
227
+ "recall": 1.0
228
+ },
229
+ "principle_A_domain_1": {
230
+ "total": 914,
231
+ "hits": 914,
232
+ "recall": 1.0
233
+ },
234
+ "principle_A_domain_2": {
235
+ "total": 915,
236
+ "hits": 915,
237
+ "recall": 1.0
238
+ },
239
+ "principle_A_domain_3": {
240
+ "total": 941,
241
+ "hits": 941,
242
+ "recall": 1.0
243
+ },
244
+ "principle_A_reconstruction": {
245
+ "total": 967,
246
+ "hits": 902,
247
+ "recall": 0.9328
248
+ },
249
+ "regular_plural_subject_verb_agreement_1": {
250
+ "total": 890,
251
+ "hits": 670,
252
+ "recall": 0.7528
253
+ },
254
+ "regular_plural_subject_verb_agreement_2": {
255
+ "total": 945,
256
+ "hits": 932,
257
+ "recall": 0.9862
258
+ },
259
+ "sentential_negation_npi_licensor_present": {
260
+ "total": 919,
261
+ "hits": 919,
262
+ "recall": 1.0
263
+ },
264
+ "sentential_negation_npi_scope": {
265
+ "total": 871,
266
+ "hits": 871,
267
+ "recall": 1.0
268
+ },
269
+ "sentential_subject_island": {
270
+ "total": 961,
271
+ "hits": 961,
272
+ "recall": 1.0
273
+ },
274
+ "superlative_quantifiers_1": {
275
+ "total": 979,
276
+ "hits": 979,
277
+ "recall": 1.0
278
+ },
279
+ "superlative_quantifiers_2": {
280
+ "total": 986,
281
+ "hits": 986,
282
+ "recall": 1.0
283
+ },
284
+ "supplement_hypernym": {
285
+ "total": 842,
286
+ "hits": 688,
287
+ "recall": 0.8171
288
+ },
289
+ "supplement_qa_congruence_easy": {
290
+ "total": 64,
291
+ "hits": 64,
292
+ "recall": 1.0
293
+ },
294
+ "supplement_qa_congruence_tricky": {
295
+ "total": 165,
296
+ "hits": 165,
297
+ "recall": 1.0
298
+ },
299
+ "supplement_subject_aux_inversion": {
300
+ "total": 3867,
301
+ "hits": 3731,
302
+ "recall": 0.9648
303
+ },
304
+ "supplement_turn_taking": {
305
+ "total": 280,
306
+ "hits": 280,
307
+ "recall": 1.0
308
+ },
309
+ "tough_vs_raising_1": {
310
+ "total": 948,
311
+ "hits": 761,
312
+ "recall": 0.8027
313
+ },
314
+ "tough_vs_raising_2": {
315
+ "total": 920,
316
+ "hits": 793,
317
+ "recall": 0.862
318
+ },
319
+ "transitive": {
320
+ "total": 868,
321
+ "hits": 276,
322
+ "recall": 0.318
323
+ },
324
+ "wh_island": {
325
+ "total": 960,
326
+ "hits": 960,
327
+ "recall": 1.0
328
+ },
329
+ "wh_questions_object_gap": {
330
+ "total": 859,
331
+ "hits": 812,
332
+ "recall": 0.9453
333
+ },
334
+ "wh_questions_subject_gap_long_distance": {
335
+ "total": 857,
336
+ "hits": 857,
337
+ "recall": 1.0
338
+ },
339
+ "wh_vs_that_no_gap": {
340
+ "total": 861,
341
+ "hits": 667,
342
+ "recall": 0.7747
343
+ },
344
+ "wh_vs_that_no_gap_long_distance": {
345
+ "total": 875,
346
+ "hits": 676,
347
+ "recall": 0.7726
348
+ },
349
+ "wh_vs_that_with_gap": {
350
+ "total": 919,
351
+ "hits": 569,
352
+ "recall": 0.6192
353
+ },
354
+ "wh_vs_that_with_gap_long_distance": {
355
+ "total": 910,
356
+ "hits": 910,
357
+ "recall": 1.0
358
+ }
359
+ }
360
+ }
data/rule_vocab_categories.json ADDED
The diff for this file is too large to render. See raw diff
 
data/vocab.txt ADDED
The diff for this file is too large to render. See raw diff
 
experiments.csv ADDED
The diff for this file is too large to render. See raw diff
 
models/tokenizer/special_tokens_map.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<s>",
3
+ "cls_token": "<s>",
4
+ "eos_token": "</s>",
5
+ "mask_token": "<mask>",
6
+ "pad_token": "<pad>",
7
+ "sep_token": "</s>",
8
+ "unk_token": "<unk>"
9
+ }
models/tokenizer/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
models/tokenizer/tokenizer_config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<unk>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<s>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "</s>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<pad>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "4": {
36
+ "content": "<mask>",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ }
43
+ },
44
+ "bos_token": "<s>",
45
+ "clean_up_tokenization_spaces": true,
46
+ "cls_token": "<s>",
47
+ "eos_token": "</s>",
48
+ "mask_token": "<mask>",
49
+ "model_max_length": 1000000000000000019884624838656,
50
+ "pad_token": "<pad>",
51
+ "sep_token": "</s>",
52
+ "tokenizer_class": "PreTrainedTokenizerFast",
53
+ "unk_token": "<unk>"
54
+ }
models/tokenizer_morfessor/morfessor.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c81fb6f4ae3052676df8c8e33222b747be11be8a15b7bbfc33a01147d47d65fe
3
+ size 2734346
models/tokenizer_morfessor/special_tokens_map.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<s>",
3
+ "cls_token": "<s>",
4
+ "eos_token": "</s>",
5
+ "mask_token": "<mask>",
6
+ "pad_token": "<pad>",
7
+ "sep_token": "</s>",
8
+ "unk_token": "<unk>"
9
+ }
models/tokenizer_morfessor/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
models/tokenizer_morfessor/tokenizer_config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<unk>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<s>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "</s>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<pad>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "4": {
36
+ "content": "<mask>",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ }
43
+ },
44
+ "bos_token": "<s>",
45
+ "clean_up_tokenization_spaces": true,
46
+ "cls_token": "<s>",
47
+ "eos_token": "</s>",
48
+ "mask_token": "<mask>",
49
+ "model_max_length": 1000000000000000019884624838656,
50
+ "pad_token": "<pad>",
51
+ "sep_token": "</s>",
52
+ "tokenizer_class": "PreTrainedTokenizerFast",
53
+ "unk_token": "<unk>"
54
+ }
notes/babylm_leaderboard_2025.csv ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === Strict Track ===
2
+ eval_name,Model,HF Repo,Revision commit,Text Average,Available on the hub,Main Contributions,Model Type,Base Architecture,Optimizer,Learning Rate,Learning Rate Scheduler,Num Training Epochs,Batch Size,Tokenizer,Token Set Size,Random Seed,Number of Layers,Number of Attention Heads,Max Sequence Length,Total Number of Parameters (M),Total Training PFLOPS,GPU Development Hours,GPU Train Hours,Training Dataset,Number of Words in Dataset (M),Genre of Dataset,Preprocessing of Dataset,Human Annotation in Dataset,Data Augmentation / Synthetic Data,Other Hyperparameters,Model Description,BLiMP,BLiMP Supplement,EWoK,Entity Tracking,WUG Adjective Nominalization,WUG Past Tense,COMPS,Reading,Self-paced Reading Time,Eye Tracking,AoA,(Super)GLUE,BoolQ,MNLI,MRPC,MultiRC,QQP,RTE,WSC,Track
3
+ Baseline-gpt-bert-base-mixed (mntp)_strict,Baseline-gpt-bert-base-mixed (mntp),BabyLM-community/babylm-baseline-100m-gpt-bert-mixed,main,47.3,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,131072,BPE,16384,,12,12,512,120,1000,48,144,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.5}","Baseline model for BabyLM 2025, trained with a 50-50 mix of both MNTP and CLM objectives and evaluated masked",80.5,73,52.4,39.9,41.2,27.1,59.7,6.3,3.4,9.2,22.3,70.9,73.4,63.4,85.8,69.8,81.2,59,63.5,strict
4
+ Baseline-gpt-bert-base-causal-focus (causal)_strict,Baseline-gpt-bert-base-causal-focus (causal),BabyLM-community/babylm-baseline-100m-gpt-bert-causal-focus,main,46.7,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,131072,BPE,16384,,12,12,512,120,1000,48,144,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.9375}","Baseline model for BabyLM 2025, trained with a 1-15 mix of both MNTP and CLM objectives and evaluated causally",79.3,70.4,52.3,30.9,63,26.8,58.3,5.7,3,8.4,10.7,70.1,72,60.4,80.9,67.5,81.3,64.7,63.5,strict
5
+ Baseline-gpt-bert-base-causal-focus (mntp)_strict,Baseline-gpt-bert-base-causal-focus (mntp),BabyLM-community/babylm-baseline-100m-gpt-bert-causal-focus,main,46.2,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,131072,BPE,16384,,12,12,512,120,1000,48,144,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.9375}","Baseline model for BabyLM 2025, trained with a 1-15 mix of both MNTP and CLM objectives and evaluated masked",78.1,68.6,53.5,41.9,39.9,32.7,60,6.1,3.1,9.1,11.5,70.1,72,60.4,80.9,67.5,81.3,64.7,63.5,strict
6
+ Baseline-gpt-bert-base-mixed (causal)_strict,Baseline-gpt-bert-base-mixed (causal),BabyLM-community/babylm-baseline-100m-gpt-bert-mixed,main,45.3,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,131072,BPE,16384,,12,12,512,120,1000,48,144,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.5}","Baseline model for BabyLM 2025, trained with a 50-50 mix of both MNTP and CLM objectives and evaluated causally",78.4,69.2,51.8,33.2,58.2,17.9,56.9,6.2,3.6,8.7,10.4,70.9,73.4,63.4,85.8,69.8,81.2,59,63.5,strict
7
+ Baseline-gpt-bert-base-masked-focus (mntp)_strict,Baseline-gpt-bert-base-masked-focus (mntp),BabyLM-community/babylm-baseline-100m-gpt-bert-masked-focus,main,45.2,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,131072,BPE,16384,,12,12,512,120,1000,48,144,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.0625}","Baseline model for BabyLM 2025, trained with a 15-1 mix of both MNTP and CLM objectives and evaluated masked",80.7,75.3,51.8,41.5,35,23.2,58.3,6.3,3.3,9.3,9.6,70.6,72,63.5,87.3,69.1,81.7,59,61.5,strict
8
+ Baseline-gpt-bert-base-masked-focus (causal)_strict,Baseline-gpt-bert-base-masked-focus (causal),BabyLM-community/babylm-baseline-100m-gpt-bert-masked-focus,main,44.2,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,131072,BPE,16384,,12,12,512,120,1000,48,144,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.0625}","Baseline model for BabyLM 2025, trained with a 15-1 mix of both MNTP and CLM objectives and evaluated causally",74.6,63.6,51.6,31,51.1,25.1,55.7,6,3.3,8.8,12.8,70.6,72,63.5,87.3,69.1,81.7,59,61.5,strict
9
+ blalm-100m-dynmod-bounded_strict,blalm-100m-dynmod-bounded,PatrickHaller/babylm_2025_submission_strict,main,42.1,false,Architectural innovations,Decoder only,LSTM,Muon,0.00055,cosine with warmup,10,64,WordPiece,15000,42,24,16,512,270,20250,200,10,PatrickHaller/BabyLM2025-Strict-Dataset,100,"CHILDES,TinyStories,Simple Wikipedia, Cosmopedia, Project Gutenberg Fiction Books,Fineweb-Edu","Grammar Correction, Filtering Short Sequences",Not applicable,Not applicable,,Local context enhancement applied to a mLSTM token mixer backend,74.7,61,53.6,22.2,47.5,37.5,58.3,0.8,0.6,1.1,8.6,56.7,64,34.3,69.1,57.5,58.9,51.8,61.5,strict
10
+ simple_diffusion_cosine_strict,simple_diffusion_cosine,despoinakk/diffusion_cosine_babylm,main,40.1,true,Training objective innovations,Encoder only,LTG-BERT,lamb,0,cosine_schedule_with_warmup_cooldown,10,197228,bpe,16384,0,12,12,512,127,0,5000,28,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,,This is a model based on the LTG-BERT architecture that was trained with a simple masked diffusion objective with a cosine noise schedule and a token frequency-informed masking strategy.,76.9,72.4,51.8,40.8,49.6,15.4,56.4,7.4,4.1,10.6,-22,52.3,62,31.8,32.8,63,63.8,54.7,57.7,strict
11
+ babylm-baseline-100m-gpt2_strict,babylm-baseline-100m-gpt2,BabyLM-community/babylm-baseline-100m-gpt2,main,40.1,true,Baseline,Decoder only,GPT-2,AdamW,0.00005,Cosine scheduler with warmup and decay,10,8192,BPE,16384,242601,12,12,512,,1290,6,4,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""base_folder"": ""experiments"", ""batch_size"": 16, ""checkpoint_dir"": ""experiments/strict_proper_tokenizer/checkpoints"", ""datapoint_length"": 512, ""expdir"": ""experiments/strict_proper_tokenizer"", ""experiment_name"": ""strict_proper_tokenizer"", ""gradient_clip_norm"": 1, ""learning_rate"": 5e-05, ""logdir"": ""experiments/strict_proper_tokenizer/logging"", ""n_epochs"": 10, ""num_training_steps"": 211650, ""num_warmup_steps"": 2116, ""seed"": 242601, ""sft_learning_rate"": 5e-05, ""training_type"": ""strict"", ""use_wandb"": false, ""wandb_experiment_name"": ""testing_new_acc"", ""wandb_project_name"": ""babylm_gpt2_sweeps"", ""weight_decay"": 0}",A naive autoregressive baseline with the GPT-2 architecture for the strict track,74.9,63.3,51.7,31.5,50.2,7.3,56.2,5.5,3.2,7.9,5.3,54.7,57.1,33.1,57.4,58.8,67.6,53.2,55.8,strict
12
+ CLASS-IT_140M_strict,CLASS-IT_140M,colinglab/CLASS_IT-140M,main,39.9,true,"Curriculum learning, Data augmentation, Dataset creation",Decoder only,Llama,AdamW,0.0002,"linear, cosine with restarts",,15,bpe,32000,na,12,11,6144,,,50,60,BabyLM strict,100,BabyLM,"For the conversational section, the Switchboard data were adapted to meet the requirements of IT training task. Consecutive utterances from the same speaker were merged to ensure a consistent alternation between speakers’ turns (e.g., A, B, A, B). The dialogues were then segmented into prompt–reply pairs using a sliding window approach with the following schema: (A1, B1), (B1, A2), (A2, B2).
13
+ For the instructional section, Simple Wikipedia data were augmented using LLaMA-3.2-3B-Instruct. For each article text, three question–answer pairs were generated using structured generation with outlines and the following prompt: Based on the following text, generate 3 questions and detailed, informative answers. Each answer should be easy for a young person to understand and at least 2–3 sentences long. Explain things in simple language, with clear and friendly sentences. Avoid short or vague replies and give enough detail so a kid can learn something new.",Not applicable,We used LLaMA-3.2-3B-Instruct to generate an instruction tuning dataset,,"This is a small-scale LLaMA-style model, fine-tuned in sequence on a conversational dataset followed by an instructional dataset.",75.6,58.2,51.1,19.9,60.1,9.6,56,0.3,0.1,0.5,11.8,56.6,64,34.3,70.1,57.5,62.5,46,61.5,strict
14
+ 100m-bert-FRE-single-sorted_strict,100m-bert-FRE-single-sorted,JungwonPark02/FRE_single_sorted_bert,main,37.5,true,Curriculum learning,Encoder only,BERT,AdamW,0.00005,linear,10,32768,Custom WordPiece tokenizer trained on the same custom datasets used for model training,30522,42,12,12,512,110,66000,100,4,BabyLM strict,100,BabyLM,"We used the subset of COSMOPEDIA (Ben Allal et al., 2024) corpus where each sentence was assigned a FRE score, which served as the basis for organizing the training data according to linguistic difficulty.",No,No,"{""architectures"": [""BertForMaskedLM""], ""attention_probs_dropout_prob"": 0.1, ""classifier_dropout"": null, ""hidden_act"": ""gelu"", ""hidden_dropout_prob"": 0.1, ""hidden_size"": 768, ""initializer_range"": 0.02, ""intermediate_size"": 3072, ""layer_norm_eps"": 1e-12, ""max_position_embeddings"": 512, ""model_type"": ""bert"", ""num_attention_heads"": 12, ""num_hidden_layers"": 12, ""pad_token_id"": 0, ""position_embedding_type"": ""absolute"", ""torch_dtype"": ""float32"", ""transformers_version"": ""4.51.3"", ""type_vocab_size"": 2, ""use_cache"": true, ""vocab_size"": 30522}","Curriculum learning model trained by ordering training data according to the Flesch Reading Ease score, using sentence-level curriculum strategies.",74,57.5,51.2,36,44.9,-6.4,55.8,4.4,2.9,6,0,57.3,63.5,34,56.9,63,68.4,54,61.5,strict
15
+ babylm2025-strict-batch-wise-convergent-pretraining-main-2_strict,babylm2025-strict-batch-wise-convergent-pretraining-main-2,ko-yoshida/exp1_averaged,main,34,true,"Curriculum learning, Training objective innovations",Decoder only,Qwen2,"AdamW, our custom optimizer",0.007,cosine,2,128,unigram,16000,42,24,14,512,372,2908,5,5,our custom corpus,100,single words/short sentences/essay/dialogues/stories,"We implemented a three-stage preprocessing approach to create a curriculum-based language learning dataset. First, we extracted single-word data from English Age of Acquisition (AoA) ratings, selecting words typically acquired by age 13 to serve as foundational lexical stimuli for the earliest training stage. Second, we generated synthetic short-sentence data by using a large language model (Qwen3-14B) to create 1-15 word sentences for each selected word, specifically prompting for simple, literal usage while excluding named entities and complex syntax. Sentences exceeding the length limit were discarded, and all markup was stripped with punctuation normalized. Third, we cleaned three existing child-directed corpora—KidLM (essays), TinyStories (narratives), and TinyDialogues (dialogues)—by shortening overly long sentences through splitting or compression while preserving meaning, removing tags and metadata, and condensing redundantly verbose paragraphs. This preprocessing pipeline produced streamlined, semantically coherent material that progresses from pure lexical items to minimal phrasal structures and finally to more complex narrative and dialogue content, all aligned with our staged curriculum approach.",Not applicable,We used Qwen3-14B to generate and clean data.,,"We conducted training in two stages with distinct configurations. In the first stage, we used a Qwen2-0.5B model trained on a custom corpus with a learning rate of 0.007 and cosine scheduling, incorporating a warmup period of 0.05 (5% of total steps). The training used a global batch size of 128, sequence length of 512 tokens, and was executed on a single node with H200 GPUs. For the second stage, we continued training from the checkpoint, significantly reducing the learning rate to 0.001 while maintaining the cosine scheduler but removing the warmup period entirely. The second stage utilized the same batch size and sequence length configurations, with custom corpus data that included specific parameters of p=1, C=1.0e-6, and 9 repetitions, running on a single H200 GPU. This two-stage approach allowed for initial rapid learning followed by fine-tuned optimization with reduced learning rates.",53,48.4,50,32.1,69.9,-1.3,50.4,0.1,0,0.2,-19.7,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict
16
+ babylm2025-strict-batch-wise-convergent-pretraining-main_strict,babylm2025-strict-batch-wise-convergent-pretraining-main,ko-yoshida/exp1_averaged,main,34,true,"Curriculum learning, Training objective innovations",Decoder only,Qwen2,our custom optimizer,0.001,cosine,1,128,unigram,16000,42,24,14,512,372,2903,5,5,BabyLM strict,100,BabyLM,"We implemented a three-stage preprocessing approach to create a curriculum-based language learning dataset. First, we extracted single-word data from English Age of Acquisition (AoA) ratings, selecting words typically acquired by age 13 to serve as foundational lexical stimuli for the earliest training stage. Second, we generated synthetic short-sentence data by using a large language model (Qwen3-14B) to create 1-15 word sentences for each selected word, specifically prompting for simple, literal usage while excluding named entities and complex syntax. Sentences exceeding the length limit were discarded, and all markup was stripped with punctuation normalized. Third, we cleaned three existing child-directed corpora—KidLM (essays), TinyStories (narratives), and TinyDialogues (dialogues)—by shortening overly long sentences through splitting or compression while preserving meaning, removing tags and metadata, and condensing redundantly verbose paragraphs. This preprocessing pipeline produced streamlined, semantically coherent material that progresses from pure lexical items to minimal phrasal structures and finally to more complex narrative and dialogue content, all aligned with our staged curriculum approach.",Not applicable,We used Qwen3-14B to generate and clean data,,"We used a Qwen2-0.5B model trained on a custom corpus with a learning rate of 0.001 and cosine scheduling, incorporating a warmup period of 0.05 (5% of total steps). The training used a global batch size of 128, sequence length of 512 tokens, and was executed on a single node with H200 GPUs. We utilized specific parameters of p=1, C=1.0e-6, and 10 repetitions, running on a single H200 GPU.",53,48.4,50,32.1,69.9,-1.3,50.4,0.1,0,0.2,-19.7,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict
17
+ babylm2025-strict-batch-wise-convergent-pretraining-1_strict,babylm2025-strict-batch-wise-convergent-pretraining-1,ko-yoshida/sample1,main,33.6,true,"Curriculum learning, Training objective innovations",Decoder only,Qwen2,"AdamW, our custom optimizer",0.007,cosine,2,128,unigram,16000,42,24,14,512,372,2903,5,5,BabyLM multimodal,50,BabyLM,"We implemented a three-stage preprocessing approach to create a curriculum-based language learning dataset. First, we extracted single-word data from English Age of Acquisition (AoA) ratings, selecting words typically acquired by age 13 to serve as foundational lexical stimuli for the earliest training stage. Second, we generated synthetic short-sentence data by using a large language model (Qwen3-14B) to create 1-15 word sentences for each selected word, specifically prompting for simple, literal usage while excluding named entities and complex syntax. Sentences exceeding the length limit were discarded, and all markup was stripped with punctuation normalized. Third, we cleaned three existing child-directed corpora—KidLM (essays), TinyStories (narratives), and TinyDialogues (dialogues)—by shortening overly long sentences through splitting or compression while preserving meaning, removing tags and metadata, and condensing redundantly verbose paragraphs. This preprocessing pipeline produced streamlined, semantically coherent material that progresses from pure lexical items to minimal phrasal structures and finally to more complex narrative and dialogue content, all aligned with our staged curriculum approach.",Not applicable,We used Qwen3-14B to generate and clean data.,,"We conducted training in two stages with distinct configurations. In the first stage, we used a Qwen2-0.5B model trained on a custom corpus with a learning rate of 0.007 and cosine scheduling, incorporating a warmup period of 0.05 (5% of total steps). The training used a global batch size of 128, sequence length of 512 tokens, and was executed on a single node with H200 GPUs. For the second stage, we continued training from the checkpoint, significantly reducing the learning rate to 0.001 while maintaining the cosine scheduler but removing the warmup period entirely. The second stage utilized the same batch size and sequence length configurations, with custom corpus data that included specific parameters of p=2, C=1, and 9 repetitions, running on a single H200 GPU. This two-stage approach allowed for initial rapid learning followed by fine-tuned optimization with reduced learning rates.",55.5,49.9,49.8,32,61.1,-8.9,49.8,0.1,0,0.2,-10.5,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict
18
+ babylm2025-strict-batch-wise-convergent-pretraining-2_strict,babylm2025-strict-batch-wise-convergent-pretraining-2,ko-yoshida/exp4_averaged,main,32.3,true,"Curriculum learning, Training objective innovations",Decoder only,Qwen2,"AdamW, our custom optimizer",0.007,cosine,2,128,unigram,16000,42,24,14,512,372,2908,5,5,our custom corpus,100,single words/short sentences/essay/dialogues/stories,"We implemented a three-stage preprocessing approach to create a curriculum-based language learning dataset. First, we extracted single-word data from English Age of Acquisition (AoA) ratings, selecting words typically acquired by age 13 to serve as foundational lexical stimuli for the earliest training stage. Second, we generated synthetic short-sentence data by using a large language model (Qwen3-14B) to create 1-15 word sentences for each selected word, specifically prompting for simple, literal usage while excluding named entities and complex syntax. Sentences exceeding the length limit were discarded, and all markup was stripped with punctuation normalized. Third, we cleaned three existing child-directed corpora—KidLM (essays), TinyStories (narratives), and TinyDialogues (dialogues)—by shortening overly long sentences through splitting or compression while preserving meaning, removing tags and metadata, and condensing redundantly verbose paragraphs. This preprocessing pipeline produced streamlined, semantically coherent material that progresses from pure lexical items to minimal phrasal structures and finally to more complex narrative and dialogue content, all aligned with our staged curriculum approach.",Not applicable,We used Qwen3-14B to generate and clean data.,,"We conducted training in two stages with distinct configurations. In the first stage, we used a Qwen2-0.5B model trained on a custom corpus with a learning rate of 0.007 and cosine scheduling, incorporating a warmup period of 0.05 (5% of total steps). The training used a global batch size of 128, sequence length of 512 tokens, and was executed on a single node with H200 GPUs. For the second stage, we continued training from the checkpoint, significantly reducing the learning rate to 0.001 while maintaining the cosine scheduler but removing the warmup period entirely. The second stage utilized the same batch size and sequence length configurations, with custom corpus data that included specific parameters of p=2, C=1, and 9 repetitions, running on a single H200 GPU. This two-stage approach allowed for initial rapid learning followed by fine-tuned optimization with reduced learning rates.",56.1,51.4,50.2,40.8,46.4,-2.1,49.6,0.9,0,1.8,-27.7,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict
19
+
20
+ === Strict-small Track ===
21
+ eval_name,Model,HF Repo,Revision commit,Text Average,Available on the hub,Main Contributions,Model Type,Base Architecture,Optimizer,Learning Rate,Learning Rate Scheduler,Num Training Epochs,Batch Size,Tokenizer,Token Set Size,Random Seed,Number of Layers,Number of Attention Heads,Max Sequence Length,Total Number of Parameters (M),Total Training PFLOPS,GPU Development Hours,GPU Train Hours,Training Dataset,Number of Words in Dataset (M),Genre of Dataset,Preprocessing of Dataset,Human Annotation in Dataset,Data Augmentation / Synthetic Data,Other Hyperparameters,Model Description,BLiMP,BLiMP Supplement,EWoK,Entity Tracking,WUG Adjective Nominalization,WUG Past Tense,COMPS,Reading,Self-paced Reading Time,Eye Tracking,AoA,(Super)GLUE,BoolQ,MNLI,MRPC,MultiRC,QQP,RTE,WSC,Track
22
+ amlm_hd_fail_strict-small,amlm_hd_fail,leukas/amlm_hd_fail,main,45.4,true,Curriculum learning,Encoder only,DeBERTa,LAMB,0.007,Cosine,10,4096,BPE,40000,0,12,12,256,35,16.31,1,1,BabyLM strict-small,10,BabyLM,Concatenated columns together,No,No,,"Deberta model trained with Adaptive MLM, with decaying probability from 40% to 15%.
23
+ This particular model's loss spiked due to too low batch size, but the results were strangely good for some metrics.",59,51.7,56,41.3,78.1,-12.1,82.2,6.4,2.9,9.9,34.2,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict-small
24
+ MoEP_strict-small,MoEP,Jtapsa/moep,main,44.5,true,Architectural innovations,Decoder only,GPT-2,AdamW,0.0003,Linear,,16,BPE,512,42,12,,1024,28,,,1,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""ModelArgs"": {""dim"": 384, ""n_layers"": 12, ""n_heads"": 6, ""ffn_m"": 4, ""head_dim"": 64, ""max_seq_len"": 1024, ""train_seq_len"": 512, ""batch_size"": 16, ""epochs"": 10, ""pad_id"": 3, ""vocab_size"": 16384, ""weight_tying"": true, ""model_name"": ""MoEP_0sw_64"", ""device"": ""cuda:0"", ""learning_rate"": 0.0003, ""weight_decay"": 0.1, ""betas"": [0.9, 0.95], ""adam_eps"": 1e-08, ""warmup_steps"": 800, ""scheduler_type"": ""cosine"", ""k"": 2, ""n_experts"": 4, ""sliding_window"": null, ""router_aux_weight"": 0.02}, ""ModelArgs_Parallel"": {""dim"": 192, ""n_heads"": 3, ""head_dim"": 64, ""n_parallel_blocks"": 4, ""device"": ""cuda:0""}}","Model follows GPT-2 baseline in Tokenizer, model + training parameters. Only smaller hidden dim and fewer attention heads. But head dimension still same 64.
25
+
26
+ Model is trained using BabyLM strict-small dataset, in total 10 Epochs, where each epoch is ended after approximate 10M whitespace separated words inputted to model. Initially I pre-tokenize training data with stride 128 and randomly select with epoch based seed training examples.
27
+
28
+
29
+ Model architecture: Large layer (equal to GPT-2 layer) --> MoE Block with 4 experts and top_2 routing. instead of FFNN only Linear layer used. Purpose to shrink hidden dimension to smaller size, suitable for parallel layers. --> 10 times parallel layer. Parallel layer uses top_2 routing among 4 'parallel blocks', where 'parallel blocks' equals to Large layer, but only operates in smaller hidden dimension (192 vs 384). this allow more sparse token flow, but don't increase model total parameters. after parallel layers --> Moe Block to increase hidden dimension back to original and then --> Large layer --> norm and Linear.",60.8,57.5,50.2,35.6,66.8,-1.1,50.7,6.7,3.8,9.6,53.7,64.2,66.2,48.1,70.1,64.5,70.8,62.6,67.3,strict-small
30
+ amlm_hard_nhot_strict-small,amlm_hard_nhot,leukas/amlm_hard_nhot,main,41.9,true,"Architectural innovations, Curriculum learning",Encoder only,DeBERTa,LAMB,0.007,Cosine,10,16384,BPE,40000,0,12,12,1256,50,22.59,1,1,BabyLM strict-small,10,BabyLM,Concatenated columns together,No,No,,"Deberta model trained with Adaptive MLM, with constant mask at 15% and n-hot encodings.",65.6,54.8,49.7,43.7,64.1,0.7,51.3,5.7,3.3,8.1,16.3,67.4,67.8,60,68.1,67.7,76.4,62.6,69.2,strict-small
31
+ Baseline-gpt-bert-small-causal-focus (causal)_strict-small,Baseline-gpt-bert-small-causal-focus (causal),BabyLM-community/babylm-baseline-10m-gpt-bert-causal-focus,main,41.2,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,16384,BPE,16384,42,12,6,512,31,30,33,11,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.9375}","Baseline model for BabyLM 2025, trained with a 1-15 mix of both MNTP and CLM objectives and evaluated causally",71.7,63.2,49.5,34.6,59.2,12.9,52.8,6.7,3.4,9.9,-3.9,65.1,68.1,46.9,74.5,68.3,76.7,56.1,65.4,strict-small
32
+ Baseline-gpt-bert-small-causal-focus (mntp)_strict-small,Baseline-gpt-bert-small-causal-focus (mntp),BabyLM-community/babylm-baseline-10m-gpt-bert-causal-focus,main,41,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,16384,BPE,16384,42,12,6,512,31,30,33,11,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.9375}","Baseline model for BabyLM 2025, trained with a 1-15 mix of both MNTP and CLM objectives and evaluated masked",69.1,64.3,49.6,39.2,46.2,11.2,53,6.5,3.5,9.5,5.9,65.1,68.1,46.9,74.5,68.3,76.7,56.1,65.4,strict-small
33
+ Baseline-gpt-bert-small-masked-focus (causal)_strict-small,Baseline-gpt-bert-small-masked-focus (causal),BabyLM-community/babylm-baseline-10m-gpt-bert-masked-focus,main,40,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,16384,BPE,16384,42,12,6,512,31,30,33,11,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.0625}","Baseline model for BabyLM 2025, trained with a 15-1 mix of both MNTP and CLM objectives and evaluated causally",65.2,59.5,49.5,31.1,44.7,20.7,52.9,6.5,3.4,9.5,3.8,66,67.6,51.8,78.9,67.4,77.4,57.6,61.5,strict-small
34
+ Baseline-gpt-bert-small-mixed (mntp)_strict-small,Baseline-gpt-bert-small-mixed (mntp),BabyLM-community/babylm-baseline-10m-gpt-bert-mixed,main,39.8,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,16384,BPE,16384,42,12,6,512,31,30,33,11,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.5}","Baseline model for BabyLM 2025, trained with a 50-50 mix of both MNTP and CLM objectives and evaluated masked",71.3,63.3,49.9,40,18.4,12.3,53.7,6.5,3.3,9.8,16.4,65.7,66.7,53.3,77.5,67,76.6,55.4,63.5,strict-small
35
+ alexandertam_babylm-rta_strict-small,alexandertam/babylm-rta,babylm-anon/rta,main,39.3,true,Data augmentation,Decoder only,GPT-2,AdamW,0.00005,linear,10,16,BPE,16384,42,12,12,512,98,79.41,18,1,BabyLM strict,100,BabyLM,BabyLM,No,Only statistical information of the BabyLM corpus was used to generate synthetic data,,RecombiText Augmentation is a novel purely statistical NLP method to get the most out of low-resource datasets in LLM pre-training.,61.9,61.1,50.8,16.5,56.5,24,50.8,8.4,4.7,12.1,5.4,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict-small
36
+ Baseline-gpt-bert-small-mixed (causal) _strict-small,Baseline-gpt-bert-small-mixed (causal),BabyLM-community/babylm-baseline-10m-gpt-bert-mixed,main,39.2,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,16384,BPE,16384,42,12,6,512,31,30,33,11,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.5}","Baseline model for BabyLM 2025, trained with a 50-50 mix of both MNTP and CLM objectives and evaluated causally.",69.6,61.6,50.2,25.4,48.5,-3,53,6.4,3.4,9.5,14.5,65.7,66.7,53.3,77.5,67,76.6,55.4,63.5,strict-small
37
+ gpt_bert_ACLM_mixed_4k (causal)_strict-small,gpt_bert_ACLM_mixed_4k (causal),EleniF/babylm_1_2_bs_64_4k_ACLM,main,39.1,true,"Curriculum learning, Hyperparameter tuning",Encoder-decoder,GPT-BERT,LAMB,0.0141,cosine,380,262,BPE,4000,42,12,6,128,31,0,5,6,BabyLM strict-small,10,BabyLM,BabyLM,No,No,,"This is the gpt-bert model trained using the Active Curriculum Language Modeling method with a 50:50 masked to causal ratio, a 4k BPE Tokenize, sequence length of 128 and batch size 64.",56.1,53.5,49.4,32.9,69.9,2.1,50.3,3.5,2.4,4.7,10.4,62.5,65.4,39.2,71.1,65.7,70.9,61.9,63.5,strict-small
38
+ gpt_bert_ACLM_mixed_6k (mntp)_strict-small,gpt_bert_ACLM_mixed_6k (mntp),EleniF/babylm_1_2_6k_ACLM,main,38.8,true,"Curriculum learning, Hyperparameter tuning, Controlled experiments",Encoder-decoder,GPT-BERT,LAMB,0.0141,cosine,91,1048576,BPE,6000,42,12,6,128,31,0,25,8,BabyLM strict-small,10,BabyLM,BabyLM,No,No,,"This is the gpt-bert model trained using the Active Curriculum Language Modelling method with a 50:50 masked to causal objective ratio, a 6k BPE Tokenizer, sequence length of 128 and batch size 256.",54.6,50,49.9,36,79.3,4.3,50,3.5,2,4.9,-1,61.5,65.4,39.3,70.1,66.4,69.6,58.3,61.5,strict-small
39
+ amlm_hard_decay_strict-small,amlm_hard_decay,leukas/amlm_hard_decay,main,38.3,true,Curriculum learning,Encoder only,DeBERTa,LAMB,0.007,Cosine,10,16384,BPE,40000,0,12,12,256,35,16.31,1,1,BabyLM strict,100,BabyLM,Concatenated columns together,No,No,,"Deberta model trained with Adaptive MLM, with mask probability decaying from 40% to 15%",71.4,59.2,51,44.2,22.3,6.2,54.2,5.9,3.5,8.3,-0.9,69.8,68.6,62.1,86.3,68.4,76.8,63.3,63.5,strict-small
40
+ Baseline-gpt-bert-small-masked-focus (mntp)_strict-small,Baseline-gpt-bert-small-masked-focus (mntp),BabyLM-community/babylm-baseline-10m-gpt-bert-masked-focus,main,38.2,true,Baseline,Encoder and Decoder,GPT-BERT,LAMB,0.007,cosine with cooldown,10,16384,BPE,16384,42,12,6,512,31,30,33,11,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""warmup_ratio"": 0.016, ""weight_decay"": 0.1, ""causal_ratio"": 0.0625}","Baseline model for BabyLM 2025, trained with a 15-1 mix of both MNTP and CLM objectives and evaluated masked",70.4,63.7,50,40.1,2.7,28.7,53.6,6.4,3.4,9.4,0.3,66,67.6,51.8,78.9,67.4,77.4,57.6,61.5,strict-small
41
+ gpt_bert_ACLM_mixed_6k (causal)_strict-small,gpt_bert_ACLM_mixed_6k (causal),EleniF/babylm_1_2_6k_ACLM,main,38,true,"Curriculum learning, Hyperparameter tuning, Controlled experiments",Encoder-decoder,GPT-BERT,LAMB,0.0141,cosine,91,1048576,BPE,6000,42,12,6,128,31,0,25,8,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,"This is the gpt-bert model trained using the Active Curriculum Language Modelling method with a 50:50 masked to causal objective ratio, a 6k BPE Tokenizer, sequence length of 128 and batch size 256.",55.3,50.7,49.9,33.5,78.8,-1.8,49.9,3.8,2.1,5.4,-1.5,61.5,65.4,39.3,70.1,66.4,69.6,58.3,61.5,strict-small
42
+ gpt_bert_ACLM_mixed_4k (mntp)_strict-small,gpt_bert_ACLM_mixed_4k (mntp),EleniF/babylm_1_2_bs_64_4k_ACLM,main,38,true,"Curriculum learning, Hyperparameter tuning",Encoder-decoder,GPT-BERT,LAMB,0.0141,cosine,380,262,BPE,4000,42,12,6,128,31,0,5,6,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,"This is the gpt-bert model trained using the Active Curriculum Language Modeling method with a 50:50 masked to causal ratio, a 4k BPE Tokenize, sequence length of 128 and batch size 64.",56.5,55.3,49.6,25.5,64,12.1,49.9,3.5,2.5,4.6,0.8,62.5,65.4,39.2,71.1,65.7,70.9,61.9,63.5,strict-small
43
+ babylm-baseline-10m-gpt2_strict-small,babylm-baseline-10m-gpt2,BabyLM-community/babylm-baseline-10m-gpt2,main,37.4,true,Baseline,Decoder only,GPT-2,AdamW,0.00005,Cosine scheduler with warmup and decay,10,8192,BPE,16384,482549,12,12,512,,130,4,1,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,"{""base_folder"": ""experiments"", ""batch_size"": 16, ""checkpoint_dir"": ""experiments/strict_small_proper_tokenizer/checkpoints"", ""datapoint_length"": 512, ""expdir"": ""experiments/strict_small_proper_tokenizer"", ""experiment_name"": ""strict_small_proper_tokenizer"", ""gradient_clip_norm"": 1, ""learning_rate"": 5e-05, ""logdir"": ""experiments/strict_small_proper_tokenizer/logging"", ""n_epochs"": 10, ""num_training_steps"": 21300, ""num_warmup_steps"": 213, ""seed"": 482549, ""sft_learning_rate"": 5e-05, ""training_type"": ""strict_small"", ""use_wandb"": false, ""wandb_experiment_name"": ""testing_new_acc"", ""wandb_project_name"": ""babylm_gpt2_sweeps"", ""weight_decay"": 0}",A naive autoregressive baseline with the GPT-2 architecture for the strict-small track,66.4,57.1,49.9,13.9,66.1,-5,51.7,6.5,4.3,8.7,11.7,55.9,52.1,33.1,67.6,57.5,63.6,56.1,61.5,strict-small
44
+ TICL_strict-small,TICL,babylm-anon/TICL,main,37.3,true,"Curriculum learning, Controlled experiments",Encoder only,RoBERTa,torch.optim.AdamW,0.0005,polynomial,10,2048,transformers.RobertaTokenizerFast,52000,42,12,12,514,126,674.31,150,3,babylm-anon/stratified_10m_curriculum,10,"Child Directed Speech, Children's Books, Dialogue, Educational, Written English",As in the babylm/babylm_data_preprocessing repository,No,No,"{""_name_or_path"": ""models/stratified_10m_curriculum_roberta_roberta_influence_incr_bins_lognorm_babylm"", ""architectures"": [""RobertaForMaskedLM""], ""attention_probs_dropout_prob"": 0.1, ""bos_token_id"": 0, ""classifier_dropout"": null, ""eos_token_id"": 2, ""hidden_act"": ""gelu"", ""hidden_dropout_prob"": 0.1, ""hidden_size"": 768, ""initializer_range"": 0.02, ""intermediate_size"": 3072, ""layer_norm_eps"": 1e-05, ""max_position_embeddings"": 514, ""model_type"": ""roberta"", ""num_attention_heads"": 12, ""num_hidden_layers"": 12, ""pad_token_id"": 1, ""position_embedding_type"": ""absolute"", ""torch_dtype"": ""float32"", ""transformers_version"": ""4.47.0"", ""type_vocab_size"": 1, ""use_cache"": true, ""vocab_size"": 52000}",A RoBERTa model pre-trained on a dataset of 10M words using (**T**raining Data) **I**nfluence-driven **C**urriculum **L**earning.,69.9,53.9,50.3,34.3,61.5,-8.9,50.7,2.1,0.2,4,-5.1,64.6,67.6,48.4,73,64.4,70.3,57.6,71.2,strict-small
45
+ gpt2-10M-mop-eng_strict-small,gpt2-10M-mop-eng,NeTS-lab/babylm-mop-10m-gpt2,main,37,true,"Linguistic bias, Evaluation methods, Tokenization",Decoder only,GPT-2,AdamW,0.00005,Linear with warmup,10,16,MorPiece,23405,-1,12,12,1024,104,2000,15,1,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,"This is a GPT-2 model trained with MorPiece, a custom tokenizer designed to build a character-based TRIE structure out of the training corpus and identify morphological boundaries by looking at the frequency of father/child nodes.",63.5,52.6,50.6,64.4,37.6,12.1,55.8,1,0.7,1.2,-25.6,57.7,64,35.7,68.1,57.5,62.8,54,61.5,strict-small
46
+ mtp-gpt2_strict-small,mtp-gpt2,aynetdia/babylm-mtp-gpt2,main,36.5,true,"Curriculum learning, Training objective innovations",Decoder only,GPT-2,AdamW,0.00005,Cosine with min LR,10,16,BPE,16384,42,12,12,512,0,0,50,1,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,"Baseline GPT-2 model with additional language modeling head on top of the last layer. Trained for the first 5 epochs with a 2-token prediction objective (multi-token prediction), and for the last 5 epochs with a regular next token prediction objective.",61.9,57.6,50.2,18.6,71.7,-9,51.1,7.7,4.3,11.2,0,55.4,62.8,33.1,73,43.2,61.4,52.5,61.5,strict-small
47
+ blalm-10m-short-conv_strict-small,blalm-10m-short-conv,PatrickHaller/babylm_2025_submission_strict-small2,main,35.2,false,Architectural innovations,Decoder only,LSTM,Muon,0.0007,cosine with warmup,10,64,Wordpiece,15000,42,24,16,512,270,202.5,200,1,PatrickHaller/BabyLM2025-Strict-Small-Dataset,10,"CHILDES, Fineweb-Edu, TinyStories,Project Gutenberg Fiction Books, Simple Wikipedia, Cosmopedia","Grammar Correctio, Short Sequence filtering",No,No,,A xLSTM based language model with local context modifications.,67,53.3,50.6,33.7,50.3,-20.7,50.5,1,1,1.1,8.6,57.4,64,34.2,69.6,57.5,59.9,54.7,61.5,strict-small
48
+ fasttext-cross-lora (mlm)_strict-small,fasttext-cross-lora (mlm),knalin55/CUNI_FT-CROSS-LoRA,main,34.9,true,"Controlled experiments, Curriculum learning",Encoder only,BERT,AdamW,0.00005,linear,6,68,bert-base-cased,28896,59,12,12,512,108,0,1,9,BabyLM strict-small,10,BabyLM,None,Not applicable,Custom Synthetic Data - Non linguistic,,The model is first initialized with FastText. It is then trained on the generated artificial language data. The model is finally trained using LoRA on the BabyLM corpus.,60.5,50.7,50.3,25.5,33.6,11,51.3,2.2,1,3.4,0,64,69.1,45.6,73.5,57.5,73.6,59.7,69.2,strict-small
49
+ CUNI_FT-CROSS-LoRA_strict-small,CUNI_FT-CROSS-LoRA,knalin55/CUNI_FT-CROSS-LoRA,main,34.9,true,,Encoder only,BERT,,0,linear,0,0,,0,,0,0,0,0,0,0,0,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,,60.5,50.7,50.3,25.5,33.6,11,51.3,2.2,1,3.4,0,64,69.1,45.6,73.5,57.5,73.6,59.7,69.2,strict-small
50
+ amlm_hard_strict-small,amlm_hard,leukas/amlm_hard,main,34.2,true,Curriculum learning,Encoder only,DeBERTa,LAMB,0.007,Cosine,10,16384,BPE,40000,0,12,12,256,35,16.31,0,1,BabyLM strict-small,10,BabyLM,Concatenated columns together,No,No,,"Deberta model trained with Adaptive MLM, with mask probability constant at 15%",69.9,57.9,50,43.6,6.9,-2.1,53.3,6.4,4,8.9,-15,71.1,71.1,60.1,86.3,68.6,77.6,61.2,73.1,strict-small
51
+ gpt2-10M-syllitok-eng_strict-small,gpt2-10M-syllitok-eng,Unknown,main,29.2,false,"Linguistic bias, Evaluation methods, Tokenization",Decoder only,GPT-2,AdamW,0.00005,Linear with warmup,10,16,SylliTok,20535,123768054,12,12,1024,102,993,5,2,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,,63.1,58.8,49.9,33.9,33.1,-29.4,55.3,0.5,0.1,0.9,-31.7,58.1,64,35.7,68.6,57.5,62.4,56.8,61.5,strict-small
52
+ gpt2-10M-parfind-eng_strict-small,gpt2-10M-parfind-eng,NeTSlab/gpt2-10M-parfind-eng,main,28,true,"Linguistic bias, Evaluation methods, Tokenization",Decoder only,GPT-2,AdamW,0.00005,Linear with warmup,10,16,ParadigmFinder,29215,829496234,12,12,1024,108,1060,8,2,BabyLM strict-small,10,BabyLM,BabyLM,Not applicable,Not applicable,,This is a GPT-2 model paired with a custom tokenizer that segments text based on a set of learned morphological paradigms.,65.2,58.8,49.4,21,-43.1,-2.6,56.6,0.3,0.3,0.2,16.3,57.8,64,35.7,68.1,57.5,62.8,54.7,61.5,strict-small
53
+ llamalogue_strict-small,llamalogue,CLAUSE-Bielefeld/llamalogue,main,24.3,true,"Data augmentation, Data preprocessing, Evaluation methods, Controlled experiments, Dataset creation",Decoder only,Llama,AdamW,0.0003,cosine,10,128,BPE Tokenizer,8467,42,16,16,128,135,,0,30,CHILDES transcripts,10,CHILDES transcripts,"We clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks. From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,Not applicable,,"This is our pre-trained model that is used as baseline, on which we perform different fine-tuning using PPO and DPO.",56,51.1,50.2,30.7,25.7,-2.8,51.6,2.7,1.4,3.9,-79.6,57.3,64,35.2,68.6,57.5,60.6,53.2,61.5,strict-small
54
+
55
+ === Multimodal Track ===
56
+ eval_name,Model,HF Repo,Revision commit,Text Average,Available on the hub,Main Contributions,Model Type,Base Architecture,Optimizer,Learning Rate,Learning Rate Scheduler,Num Training Epochs,Batch Size,Tokenizer,Token Set Size,Random Seed,Number of Layers,Number of Attention Heads,Max Sequence Length,Total Number of Parameters (M),Total Training PFLOPS,GPU Development Hours,GPU Train Hours,Training Dataset,Number of Words in Dataset (M),Genre of Dataset,Preprocessing of Dataset,Human Annotation in Dataset,Data Augmentation / Synthetic Data,Other Hyperparameters,Model Description,Vision Average,BLiMP,BLiMP Supplement,EWoK,Entity Tracking,WUG Adjective Nominalization,WUG Past Tense,COMPS,Reading,Self-paced Reading Time,Eye Tracking,AoA,(Super)GLUE,BoolQ,MNLI,MRPC,MultiRC,QQP,RTE,WSC,VQA,Winoground,DevBench,Track
57
+ smolvlm-256m-stage_multimodal,smolvlm-256m-stage,Unknown,main,37.1,false,Multimodality,Decoder only,Llama,AdamW​​,0.0001,warmup,12,2,GPT2TokenizerFast,49280,42,30,9,1200,256,0,60,50,BabyLM multimodal,50,BabyLM,BabyLM,Not applicable,Not applicable,,"The submit​​ model was efficiently trained using a ​​two-stage approach with text and image-text pairs​​, base on smolvlm-256M.",31.6,70.1,58.7,50.5,39.8,54.4,-12.8,53.3,0.1,0,0.2,0,57,64,34.9,69.1,57.5,62.5,49.6,61.5,26.5,50.5,17.8,multimodal
58
+ babylm-multimodal-baseline-git-updated_multimodal,babylm-multimodal-baseline-git-updated,BabyLM-community/babylm-multimodal-baseline-flamingo,main,35.8,true,Baseline,Decoder only,GIT,AdamW,0.0001,Linear,4,5493,Unigram,32768,1,12,12,128,223,11300,72,72,BabyLM multimodal,50,BabyLM,BabyLM,Not applicable,Not applicable,,GIT baseline for the Multimodal track,49.7,71.1,63.8,51.8,18.9,14,5.8,59.2,1.2,0.5,2,9.3,63.3,67.2,51.6,70.1,58.4,72.8,57.6,65.4,41.1,56.4,51.6,multimodal
59
+ babylm-multimodal-baseline-flamingo_multimodal,babylm-multimodal-baseline-flamingo,BabyLM-community/babylm-multimodal-baseline-flamingo,main,35.8,true,Baseline,Encoder-decoder,Flamingo,AdamW,0.0001,Linear,4,5662,Unigram,32768,1,12,12,128,,4840,144,58,BabyLM multimodal,50,BabyLM,BabyLM,Not applicable,Not applicable,,Multimodal baseline with the Flamingo architecture,50.3,70.9,65.1,51,18.4,16.3,4.7,59.2,1.9,0.8,3.1,1.2,68.8,69.3,59.5,78.9,62,77.7,70.5,63.5,43.3,54.8,52.7,multimodal
60
+ babylm-multimodal-baseline-flamingo-updated_multimodal,babylm-multimodal-baseline-flamingo-updated,BabyLM-community/babylm-multimodal-baseline-flamingo,main,35.1,true,Baseline,Encoder-decoder,Flamingo,AdamW,0.0001,Linear,4,5662,Unigram,32768,1,12,12,128,255,4840,48,48,BabyLM multimodal,50,BabyLM,BabyLM,Not applicable,Not applicable,,Flamingo baseline for the Multimodal track,49.3,73.4,65.1,52.9,16.3,5.9,1.5,59.4,2.1,1.1,3.2,6.7,67.8,69.5,59,76.5,60.9,78.1,65.5,65.4,41.5,54.2,52.2,multimodal
61
+ babylm-multimodal-baseline-git_multimodal,babylm-multimodal-baseline-git,BabyLM-community/babylm-multimodal-baseline-git,main,34.8,true,Baseline,Decoder only,GIT,AdamW,0.0001,Linear,4,5493,Unigram,32768,1,12,12,128,,11300,240,96,BabyLM multimodal,50,BabyLM,BabyLM,Not applicable,Not applicable,,Multimodal baseline with the GIT architecture,49.6,72.2,66.4,51.8,16.6,-0.6,9.1,60.1,0.9,0.2,1.6,8,63.7,68.4,52.5,71.6,57.5,73.1,59,63.5,40.6,56.2,52,multimodal
62
+ BitMar-14M_multimodal,BitMar-14M,euhidaman/bitmar-attention-multimodal,main,25.7,false,"Architectural innovations, Multimodality",Encoder-decoder,GPT-2,adamw8bit,0.0002,cosine_with_restarts,10,674,GPT-2 BPE tokenizer,,N/A,8,4,256,,17.9,60,60,BabyLM multimodal,50,BabyLM,BabyLM,Not applicable,Not applicable,,"BitMar is a resource-efficient multimodal transformer architecture optimized for edge deployment, integrating ultra-low-bit (1.58-bit) quantized BitNet-based text and DiNOv2-derived vision encoders. Both encoders produce aligned 768-dimensional latent embeddings, fused via a lightweight cross-modal attention module operating with ternary-weight quantization. The fused representation queries a fixed-size episodic memory matrix (32 slots × 128 dimensions) using content-based addressing, retrieving contextually relevant vectors. These retrieved memory vectors are projected into layer-specific conditioning signals that modulate the autoregressive BitNet decoder layers. The decoder employs attention sinks combined with a sliding window mechanism (4 sink tokens + 1,020 recent tokens) to enable unbounded context length processing with fixed memory usage.",26.7,50.4,48.8,51.8,31.2,-15.8,-21.6,51.5,0.3,0.1,0.4,0,60.5,66.5,42.3,69.1,57.8,70.2,54,63.5,16.8,50,13.2,multimodal
63
+
64
+ === Interaction Track ===
65
+ eval_name,Model,HF Repo,Revision commit,Text Average,Available on the hub,Main Contributions,Model Type,Base Architecture,Optimizer,Learning Rate,Learning Rate Scheduler,Num Training Epochs,Batch Size,Tokenizer,Token Set Size,Random Seed,Number of Layers,Number of Attention Heads,Max Sequence Length,Total Number of Parameters (M),Total Training PFLOPS,GPU Development Hours,GPU Train Hours,Training Dataset,Number of Words in Dataset (M),Genre of Dataset,Preprocessing of Dataset,Human Annotation in Dataset,Data Augmentation / Synthetic Data,Other Hyperparameters,Model Description,BLiMP,BLiMP Supplement,EWoK,Entity Tracking,WUG Adjective Nominalization,WUG Past Tense,COMPS,Reading,Self-paced Reading Time,Eye Tracking,AoA,(Super)GLUE,BoolQ,MNLI,MRPC,MultiRC,QQP,RTE,WSC,Track
66
+ blm-gpt2s-901M-storytelling_interaction,blm-gpt2s-901M-storytelling,llm-slice/blm-gpt2s-90M-s42_901M-s42_submission,main,41,true,"Controlled experiments, Teacher/expert/auxiliary models, Training objective innovations",Decoder only,GPT-2,AdamW,0.000001,"Pretrain: warmup 0.01, PPO const.",331200,360,ByteLevelBPE,16000,42,12,12,512,124,40000,900,20,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,,"This GPT2-small model was pretrained on 90% of the BabyLM corpus for 10 epochs, followed by PPO RL learning by generating a story evaluated by a teacher model (Llama 3.1 8B Instruct).",77.5,56.7,51.4,33.1,71.1,4.5,55.2,0.1,0,0.2,7.7,52.5,58.7,32.3,54.9,41.3,68.9,49.6,61.5,interaction
67
+ babylm-interaction-baseline-simpo_interaction,babylm-interaction-baseline-simpo,BabyLM-community/babylm-interaction-baseline-simpo,main,40.7,true,Baseline,Decoder only,GPT-2,AdamW,0.00005,Cosine scheduler with warmup and decay,9,9102,BPE,16384,162410,12,12,512,,7960,96,12,BabyLM strict,100,BabyLM,BabyLM,Not applicable,Not applicable,"{""base_folder"": ""experiments"", ""checkpoint_dir"": ""experiments/proper_tokenization_run/checkpoints"", ""context_proportion"": 0.5, ""datapoint_length"": 512, ""expdir"": ""experiments/proper_tokenization_run"", ""experiment_name"": ""proper_tokenization_run"", ""gradient_clip_norm"": 1, ""learning_rate"": 5e-05, ""logdir"": ""experiments/proper_tokenization_run/logging"", ""n_po_epochs"": 2, ""n_sft_epochs"": 7, ""num_rounds"": 20, ""num_training_steps"": 200000, ""num_warmup_steps"": 2000, ""save_generated_data"": true, ""seed"": 162410, ""sft_lambda"": 0.2, ""sft_learning_rate"": 5e-05, ""simpo_beta"": 2, ""simpo_gamma_ratio"": 0.5, ""start_round"": 0, ""student_model"": ""gpt2"", ""student_po_bsz"": 16, ""student_sample_bsz"": 512, ""student_sample_temperature"": 1.0, ""teacher_sample_bsz"": 512, ""teacher_sample_temperature"": 1.0, ""use_wandb"": false, ""wandb_experiment_name"": ""testing_new_acc"", ""wandb_project_name"": ""babylm_interaction_sweeps"", ""weight_decay"": 0}",A baseline for the interaction track leveraging multiple rounds of regular language modeling followed by the SimPO preference optimization method on the student and teacher outputs,72.2,61.2,51.9,28.1,51,17.9,55,6.3,3.5,9.1,6.4,56.5,58.8,32.6,66.7,57.5,66.5,51.8,61.5,interaction
68
+ CLASS_IT-140M_interaction,CLASS_IT-140M,colinglab/CLASS_IT-140M,main,39.9,true,"Curriculum learning, Data preprocessing, Data augmentation, Dataset creation",Decoder only,Llama,AdamW,0.0002,"linear, cosine with restarts",,15,BPE,32,na,12,11,6144,,,50,60,BabyLM strict,100,BabyLM,"For the conversational section, the Switchboard data were adapted to meet the requirements of IT training task. Consecutive utterances from the same speaker were merged to ensure a consistent alternation between speakers’ turns (e.g., A, B, A, B). The dialogues were then segmented into prompt–reply pairs using a sliding window approach with the following schema: (A1, B1), (B1, A2), (A2, B2).
69
+ For the instructional section, Simple Wikipedia data were augmented using LLaMA-3.2-3B-Instruct. For each article text, three question–answer pairs were generated using structured generation with outlines and the following prompt: Based on the following text, generate 3 questions and detailed, informative answers. Each answer should be easy for a young person to understand and at least 2–3 sentences long. Explain things in simple language, with clear and friendly sentences. Avoid short or vague replies and give enough detail so a kid can learn something new.",No,We used LLaMA-3.2-3B-Instruct to generate an instruction tuning dataset,,"This is a small-scale LLaMA-style model, fine-tuned in sequence on a conversational dataset followed by an instructional dataset.",75.6,58.2,51.1,19.9,60.1,9.6,56,0.3,0.1,0.5,11.8,56.6,64,34.3,70.1,57.5,62.5,46,61.5,interaction
70
+ Multitask-pretraining_1M_interaction,Multitask-pretraining_1M,https://huggingface.co/Wector1/Multitask-pretraining_1M,main,34.4,false,"Architectural innovations, Teacher/expert/auxiliary models, Training objective innovations, Dataset creation",Encoder only,ModernBERT,AdamW,0.0003,cosine,10,128,modified OLMo (ModernBERT),50368,42,22,12,256,,199.2,3,1,BabyLM Interaction,1,"diversified texts generated by LLM: reviews, emails, news,...","The generated dataset undergoes a simple filtering, consisting of deduplication and removing all instances that contain fewer than 3 words or contain non-English characters",No,diversified texts generated by LLM,,Model trained on 1M words of multitask data for 10 epochs with the multi-task pretraining objective described in the paper,56.8,54.8,49.8,19.8,40.2,-5.8,49.8,2.2,0.7,3.6,13.5,63.4,71.3,44.4,73,66.9,71.5,55.4,61.5,interaction
71
+ communicative-baby-rfolmo_score_interaction,communicative-baby-rfolmo_score,CLAUSE-Bielefeld/communicative-baby-rfolmo_score,main,32.8,true,"Data augmentation, Data preprocessing, Evaluation methods, Dataset creation, Controlled experiments, Training objective innovations, Teacher/expert/auxiliary models",Decoder only,Llama,AdamW_torch_fused,0.00005,linear,,16,BPE Tokenizer,8467,0,16,16,128,135,149.79,1,40,CHILDES Transcripts,10,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
72
+
73
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,Not applicable,,"This is the llamalogue model (our baseline) fine-tuned for 500,000 steps (less than 1 epoch) more epoch with PPO using a reward function generated by an LLM (OLMo).",55.3,50.6,50.1,36,26.7,-1.2,51.3,2.5,1.3,3.7,0,57.1,64,35.2,69.1,57.5,60.3,51.8,61.5,interaction
74
+ communicative-baby-rfsemsim_interaction,communicative-baby-rfsemsim,CLAUSE-Bielefeld/communicative-baby-rfsemsim,main,31.5,true,"Data augmentation, Data preprocessing, Evaluation methods, Controlled experiments, Dataset creation, Training objective innovations, Teacher/expert/auxiliary models",Decoder only,Llama,AdamW_torch_fused,0.00005,linear,,16,BPE Tokenizer,8467,0,16,16,128,135,149.79,1,40,CHILDES Transcripts,10,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
75
+
76
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,"We extract maternal utterances (minimum four tokens length) from unused segments of the pre-processed CHILDES dialogue triplets. We then prompt a teacher LLM, a Llama-3.2-3B, with these utterances, asking it to generate candidate responses simulating a short child-like answer that shows understanding of the mother utterance. We then calculate rewards by comparing these teacher-generated responses to the output produced by our pre-trained llamalogue in response to the same utterance.",,This is the llamalogue model (our baseline) fine-tuned for 1 more epoch with PPO using Semantic Similarity as a reward function.,56.4,51.5,49.6,34.6,30.6,-18.4,51.6,1.9,1,2.8,0,57.3,64,34.8,68.6,57.5,60.2,54.7,61.5,interaction
77
+ communicative-baby-dpo_interaction,communicative-baby-dpo,CLAUSE-Bielefeld/communicative-baby-dpo,main,31.3,true,"Data augmentation, Data preprocessing, Evaluation methods, Controlled experiments, Dataset creation, Training objective innovations, Teacher/expert/auxiliary models",Decoder only,Llama,AdamW_torch_fused,0.000005,linear,,16,BPE Tokenizer,8467,42,16,16,128,135,149.79,1,31,CHILDES Transcripts,10,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
78
+
79
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,"Examples of naturalistic DPO dialogue pairs. Each row shows a mother’s utterance (MOT) as prompt, with corresponding appropriate and random child responses (CHI) used for preference training",,"This is the llamalogue model (our baseline) fine-tuned for 10 more epoch with DPO using a custom dataset. The dataset consists of around 240,000 tokens.",55.6,50,50.1,32.7,14.4,-1.3,51.5,2.5,1.4,3.6,0,57.2,64,35.2,68.6,57.5,60.6,52.5,61.5,interaction
80
+ communicative-baby-rfconfidence_interaction,communicative-baby-rfconfidence,CLAUSE-Bielefeld/communicative-baby-rfconfidence,main,30.3,false,"Data augmentation, Data preprocessing, Evaluation methods, Controlled experiments, Dataset creation, Teacher/expert/auxiliary models, Training objective innovations",Decoder only,Llama,AdamW_torch_fused,0.000005,linear,,16,BPE Tokenizer,8467,0,16,16,128,135,149.79,1,40,CHILDES Transcripts,10,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
81
+
82
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,"We extract maternal utterances (minimum four tokens length) from unused segments of the pre-processed CHILDES dialogue triplets. We then prompt a teacher LLM, a Llama-3.2-3B, with these utterances, asking it to generate candidate responses simulating a short child-like answer that shows understanding of the mother utterance. We then calculate rewards by comparing these teacher-generated responses to the output produced by our pre-trained llamalogue in response to the same utterance.",,This is the llamalogue model (our baseline) fine-tuned for 1 more epoch with PPO using a reward function based on confidence (details in the paper).,55.1,49.4,50.8,34,21.1,-18.8,51.6,2.2,1.1,3.4,0,57.2,64,34.9,68.6,57.5,60.3,53.2,61.5,interaction
83
+ communicative-baby-dpo-synthetic_interaction,communicative-baby-dpo-synthetic,CLAUSE-Bielefeld/communicative-baby-dpo-synthetic,main,29.2,true,"Data augmentation, Data preprocessing, Teacher/expert/auxiliary models, Training objective innovations, Dataset creation, Controlled experiments, Evaluation methods",Decoder only,Llama,AdamW_torch_fused,0.0003,linear,,16,BPE Tokenizer,8467,41,16,16,128,135,149.79,1,31,CHILDES Transcripts,10,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
84
+
85
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,"The dataset contains a mother sentence under the prompt column, then we prompted a Llama3b model with this model utterance asking to simulate a child and we collected the response under the chosen column, whereas a random non suitable answer is present under the rejected column.",,"This is the llamalogue model (our baseline) fine-tuned for 10 more epoch with DPO using a custom dataset. The dataset consists of a total amount of 270,000 tokens.",55.5,51.7,50.8,31.3,-2.5,-4.8,51.6,0.8,0.4,1.2,0,57.2,64,34.9,68.6,57.5,60.6,53.2,61.5,interaction
86
+ communicative-baby-rfbleu_interaction,communicative-baby-rfbleu,CLAUSE-Bielefeld/communicative-baby-rfbleu,main,28.8,true,"Evaluation methods, Controlled experiments, Dataset creation, Training objective innovations, Data augmentation, Data preprocessing",Decoder only,Llama,AdamW_torch_fused,0.00005,linear,,16,BPE Tokenizer,8467,0,16,16,128,135,149.79,1,40,CHILDES Transcripts,0,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
87
+
88
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,"We extract maternal utterances (minimum four tokens length) from unused segments of the pre-processed CHILDES dialogue triplets. We then prompt a teacher LLM, a Llama-3.2-3B, with these utterances, asking it to generate candidate responses simulating a short child-like answer that shows understanding of the mother utterance. We then calculate rewards by comparing these teacher-generated responses to the output produced by our pre-trained llamalogue in response to the same utterance.",,This is the llamalogue model (our baseline) fine-tuned for 1 more epoch with PPO using BLEU metric as a reward function.,55.1,51.3,49.7,16.2,19.9,-15.4,50.7,2.7,2,3.4,0,57.4,64,34.6,69.1,57.5,61.7,53.2,61.5,interaction
89
+ communicative-baby-rfconfidence-test_interaction,communicative-baby-rfconfidence-test,CLAUSE-Bielefeld/communicative-baby-rfconfidence,main,22.4,false,"Data augmentation, Data preprocessing, Evaluation methods, Controlled experiments, Dataset creation, Teacher/expert/auxiliary models, Training objective innovations",Decoder only,Llama,AdamW_torch_fused,0.000005,linear,,16,BPE Tokenizer,8467,0,16,16,128,135,149.79,1,40,CHILDES Transcripts,10,CHILDES Transcripts,"In a first preprocessing step, we clean the transcripts from CHILDES quite heavily by removing all extra- and paralinguistic information. Furthermore, we replace all unintelligible or otherwise incomplete utterances, for which annotations as to the intended word are available, with these intended words. Finally, we split all utterances that contain explicitly annotated pauses, as there is no clear distinction between such pauses and utterance boundaries marked by regular line breaks.
90
+
91
+ From these cleaned dialogues, we extract all utterance triplets (three consecutive turns) where at least two different speakers are involved. Furthermore, we enforce the triplets to contain at least five lexical words. This excludes triplets that only contain repetitions of single words or are otherwise light on lexical content. We leave the speaker tags in the data.",Not applicable,"We extract maternal utterances (minimum four tokens length) from unused segments of the pre-processed CHILDES dialogue triplets. We then prompt a teacher LLM, a Llama-3.2-3B, with these utterances, asking it to generate candidate responses simulating a short child-like answer that shows understanding of the mother utterance. We then calculate rewards by comparing these teacher-generated responses to the output produced by our pre-trained llamalogue in response to the same utterance.",,This is the llamalogue model (our baseline) fine-tuned for 1 more epoch with PPO using a reward function based on confidence (details in the paper).,55.1,49.4,50.8,34,21.1,-18.8,51.6,2.2,1.1,3.4,-78.7,57.2,64,34.9,68.6,57.5,60.3,53.2,61.5,interaction
notes/encyclopedia.md ADDED
The diff for this file is too large to render. See raw diff
 
notes/experiment_log.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 实验记录
2
+
3
+ > BabyLM Challenge 2026 · Strict-Small 赛道
4
+ > 每次实验完成后在此填写结果
5
+
6
+ ---
7
+
8
+ ## 一、完整实验表
9
+
10
+ > **状态说明**: 🔲 待跑 | 🔄 运行中 | ✅ 完成 | ❌ 失败/放弃
11
+ > **eval 后端**: causal = `AutoModelForCausalLM`;mntp = `AutoModelForMaskedLM`(causal attention);mlm = `AutoModelForMaskedLM`(bidirectional)
12
+
13
+ ### 超参数
14
+
15
+ | ID | 名称 | 架构 | 目标函数 | Tokenizer | Embedding | 掩码策略 | 优化器 | FORGETTER | 数据集 | Epochs | 状态 |
16
+ |:--:|------|------|---------|-----------|-----------|---------|--------|:---------:|--------|:------:|:----:|
17
+ | 1.1 | gptbert-baseline | gpt_bert | gpt_bert (MNTP+CLM 15:1) | bpe | standard | standard 30→15% | LAMB | ✗ | sample_B | 10 | 🔲 |
18
+ | 1.2 | gpt2-baseline | gpt2 | clm | bpe | standard | — | AdamW | ✗ | sample_B | 10 | 🔲 |
19
+ | 1.3 | modernbert-baseline | modernbert | mlm | bpe | standard | standard 30→15% | AdamW | ✗ | sample_B | 10 | 🔲 |
20
+ | 1.4 | xlstm-baseline | xlstm | clm | bpe | standard | — | AdamW | ✗ | sample_B | 10 | 🔲 |
21
+ | 1.5 | rtd-baseline | rtd | rtd | bpe | standard | — | LAMB | ✗ | sample_B | 10 | 🔲 |
22
+ | 2.1 | gptbert-amlm | gpt_bert | gpt_bert | bpe | standard | **amlm** | LAMB | ✗ | sample_B | 10 | 🔲 |
23
+ | 2.2 | gptbert-nhot | gpt_bert | gpt_bert | bpe | **nhot** | standard | LAMB | ✗ | sample_B | 10 | 🔲 |
24
+ | 2.3 | gptbert-forgetter | gpt_bert | gpt_bert | bpe | standard | standard | LAMB | **✓** | sample_B | 10 | 🔲 |
25
+ | 2.4 | gptbert-mask40 | gpt_bert | gpt_bert | bpe | standard | standard **40→15%** | LAMB | ✗ | sample_B | 10 | 🔲 |
26
+ | 2.5 | gptbert-mntp1to1 | gpt_bert | gpt_bert **(1:1)** | bpe | standard | standard | LAMB | ✗ | sample_B | 10 | 🔲 |
27
+ | 2.6 | gptbert-gradacc4 | gpt_bert | gpt_bert | bpe | standard | standard | LAMB | ✗ | sample_B | 10 | 🔲 |
28
+ | 3.1 | gptbert-amlm-fgt | gpt_bert | gpt_bert | bpe | standard | amlm | LAMB | **✓** | sample_B | 10 | 🔲 |
29
+ | 3.2 | gptbert-amlm-nhot | gpt_bert | gpt_bert | bpe | **nhot** | **amlm** | LAMB | ✗ | sample_B | 10 | 🔲 |
30
+ | 3.3 | gptbert-amlm-fgt-nhot | gpt_bert | gpt_bert | bpe | **nhot** | **amlm** | LAMB | **✓** | sample_B | 10 | 🔲 |
31
+ | 3.4 | best-morfessor | gpt_bert | gpt_bert | **morfessor** | — | — | — | — | sample_B | 10 | 🔲 |
32
+ | 3.5 | best-sampleC | gpt_bert | gpt_bert | bpe | — | — | — | — | **sample_C** | 10 | 🔲 |
33
+ | 3.6 | best-mntp-opt | gpt_bert | gpt_bert | bpe | — | — | — | — | sample_B | 10 | 🔲 |
34
+ | 4.1 | best-ckpt-avg | gpt_bert | gpt_bert | bpe | — | — | — | — | sample_B | 10 | 🔲 |
35
+ | 4.2 | best-early-stop | gpt_bert | gpt_bert | bpe | — | — | — | — | sample_B | best | 🔲 |
36
+ | 4.3 | **final** | — | — | — | — | — | — | — | — | — | 🔲 |
37
+
38
+ > Phase 3/4 中 `—` 表示"与 Phase 2 最佳实验相同,保留该列值",跑完 Phase 2 后回填。
39
+
40
+ ---
41
+
42
+ ### 结果
43
+
44
+ | ID | Loss | BLiMP (c) | BLiMP (m) | BLiMP-S (c) | BLiMP-S (m) | EWoK | Entity | WUG_adj | WUG_past | COMPS | Read_eye | Read_spr | GLUE | AoA | Text_avg | 最佳ckpt | 备注 |
45
+ |:--:|:----:|:---------:|:---------:|:-----------:|:-----------:|:----:|:------:|:-------:|:--------:|:-----:|:--------:|:--------:|:----:|:---:|:--------:|:--------:|------|
46
+ | 1.1 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
47
+ | 1.2 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
48
+ | 1.3 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
49
+ | 1.4 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
50
+ | 1.5 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
51
+ | 2.1 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
52
+ | 2.2 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
53
+ | 2.3 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
54
+ | 2.4 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
55
+ | 2.5 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
56
+ | 2.6 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
57
+ | 3.1 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
58
+ | 3.2 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
59
+ | 3.3 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
60
+ | 3.4 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
61
+ | 3.5 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
62
+ | 3.6 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
63
+ | 4.1 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
64
+ | 4.2 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
65
+ | 4.3 | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | — | |
66
+
67
+ > **列说明**
68
+ > `BLiMP (c)` = causal 后端总平均;`BLiMP (m)` = mntp/mlm 后端总平均
69
+ > `WUG_adj/past` = Spearman's ρ;`Read_eye/spr` = R² change;`COMPS` = Accuracy
70
+ > `AoA` = Spearman's ρ(需提交全部10个 checkpoint,单独跑 eval_aoa.sh)
71
+ > `Text_avg` = 11列总平均(使用各模型对应的最佳后端:GPT-BERT/GPT-2/xLSTM 用 causal,ModernBERT 用 mlm)
72
+ > `GLUE` = 7 子任务平均准确率;`最佳ckpt` = 最佳 epoch 编号
73
+
74
+ ---
75
+
76
+ ### 2025 Leaderboard Strict-Small 参考(来源:`notes/babylm_leaderboard_2025.csv`)
77
+
78
+ | 模型 | TextAvg | BLiMP | Supp | EWoK | ET | WUG_ADJ | WUG_PAST | COMPS | AoA | GLUE |
79
+ |------|:-------:|:-----:|:----:|:----:|:--:|:-------:|:--------:|:-----:|:---:|:----:|
80
+ | amlm_hd_fail(冠军) | 45.4 | 59.0 | 51.7 | 56.0 | 41.3 | 78.1 | -12.1 | 82.2 | 34.2 | 57.7 |
81
+ | MoEP(亚军) | 44.5 | 60.8 | 57.5 | 50.2 | 35.6 | 66.8 | -1.1 | 50.7 | 53.7 | 64.2 |
82
+ | Baseline GPT-BERT (causal) | 41.2 | 71.7 | 63.2 | 49.5 | 34.6 | 59.2 | 12.9 | 52.8 | -3.9 | 65.1 |
83
+ | Baseline GPT-2 | 37.4 | 66.4 | 57.1 | 49.9 | 13.9 | 66.1 | -5.0 | 51.7 | 11.7 | 55.9 |
84
+
85
+ ---
86
+
87
+ ## 二、关注子任务(详细追踪)
88
+
89
+ Phase 1 跑完后,把 1.1 baseline 的 67 个 BLiMP 子任务得分填入,后续只追踪变化明显的子任务。
90
+
91
+ | 子任务 | 1.1 | 最佳 | 说明 |
92
+ |--------|:---:|:----:|------|
93
+ | (Phase 1 跑完后回填) | | | |
94
+
95
+ ---
96
+
97
+ ## 三、实验笔记
98
+
99
+ ### Phase 1 结论
100
+ > (跑完后填写:哪个架构最好,与 baseline 对比如何)
101
+
102
+ ### Phase 2 结论
103
+ > (哪些特性有效,效果多大)
104
+
105
+ ### Phase 3 结论
106
+ > (组合是否有叠加效应)
107
+
108
+ ### 最终选择
109
+ > 最终提交模型:
110
+ > 训练命令:
111
+ > 导出命令:
notes/experiment_plan.md ADDED
@@ -0,0 +1,685 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 实验计划:BabyLM Challenge 2026 Strict-Small
2
+
3
+ > **总实验数**:~380 个
4
+ > **预计 GPU 时间**:~220 GPU 小时(3 卡并行约 3 天)
5
+ > **设计原则**:漏斗型——早期用 3-epoch 快筛,赢家进入 10-epoch 全量训练
6
+ > **更新日期**:2026-03-21
7
+
8
+ ---
9
+
10
+ ## 基线配置(所有实验的默认值,除非显式覆盖)
11
+
12
+ ```yaml
13
+ # 架构
14
+ arch: gpt_bert
15
+ objective: gpt_bert
16
+ hidden_size: 384
17
+ num_layers: 12
18
+ num_heads: 6
19
+ intermediate_size: 1280
20
+ use_rope: false
21
+ use_geglu: true
22
+ use_pre_norm: true
23
+ use_attention_gate: false
24
+ use_dwa: false
25
+ use_moe: false
26
+ use_attn_res: false
27
+
28
+ # 数据
29
+ data: sample_B
30
+ tokenizer: bpe
31
+
32
+ # 嵌入
33
+ embedding: standard
34
+ embedding_init: random
35
+
36
+ # 掩码
37
+ masking: standard
38
+ mask_ratio: 0.30
39
+ mask_ratio_end: 0.15
40
+ mntp_ratio: 15
41
+
42
+ # 优化器
43
+ optimizer: lamb
44
+ lr: 0.0141
45
+ betas: (0.9, 0.98)
46
+ eps: 1e-6
47
+ weight_decay: 0.1
48
+ forgetter: false
49
+
50
+ # 训练
51
+ epochs: 10
52
+ batch_size: 64
53
+ gradient_accumulation_steps: 1
54
+ seq_len: 128
55
+ dropout: 0.1
56
+ warmup_ratio: 0.06
57
+ max_grad_norm: 2.0
58
+ fp16: true
59
+ seed: 42
60
+
61
+ # 蒸馏
62
+ kd_enabled: false
63
+
64
+ # Checkpoint
65
+ ckpt_averaging: false
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 阶段依赖关系
71
+
72
+ ```
73
+ P1(架构) ──┬──→ P2(特性消融)
74
+ ├──→ P3(LR sweep) → P4(正则化) ──┐
75
+ └────────────────────────────────┴──→ P5(特性组合)
76
+ │
77
+ ┌────────────────────────────────┘
78
+ ├──→ P6(掩码调参) → P7(Batch/SeqLen) → P8(优化器)
79
+ └──→ P10(MoE调参,条件) │
80
+ P9(架构修改) ←─┘
81
+ │
82
+ ┌────────────────────────────────┤
83
+ ├──→ P11(分词×嵌入) │
84
+ ├──→ P12(数据构建) ──┬──→ P13(跨架构验证)
85
+ │ └──→ P14(LR×Dropout网格)
86
+ │ │
87
+ │ P15(杂项超参) → P16(蒸馏) → P17(Epoch/Ckpt)
88
+ │ │
89
+ │ P18(种子方差)
90
+ │ │
91
+ └──────────────────→ P19(数据×特性交互) ←──────────────┘
92
+ │
93
+ P20(最终候选+消融) → P21(提交)
94
+
95
+ P22(预留探索) ── 贯穿所有阶段
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Phase 1:架构对比(5 个实验)
101
+
102
+ **目标**:确定最佳基础架构。所有实验用基线配置,仅改架构。
103
+ **决策规则**:按 BLiMP 排名,Top 2 晋级。赢家成为后续默认。
104
+
105
+ | ID | 名称 | arch | objective | optimizer | epochs |
106
+ |---|---|---|---|---|---|
107
+ | 1.1 | gptbert-baseline | gpt_bert | gpt_bert | LAMB | 10 |
108
+ | 1.2 | gpt2-baseline | gpt2 | clm | AdamW | 10 |
109
+ | 1.3 | modernbert-baseline | modernized_bert | mlm | AdamW | 10 |
110
+ | 1.4 | xlstm-baseline | xlstm | clm | AdamW | 10 |
111
+ | 1.5 | rtd-baseline | rtd | rtd | LAMB | 10 |
112
+
113
+ ---
114
+
115
+ ## Phase 2:核心特性消融(9 个实验)
116
+
117
+ **目标**:逐个测试高价值特性的独立贡献。
118
+ **决策规则**:BLiMP ≥ +0.5pp 或任意副指标 ≥ +2pp 的特性为"赢家",进入 P5 组合。
119
+
120
+ | ID | 名称 | 改变的参数 | epochs |
121
+ |---|---|---|---|
122
+ | 2.1 | gptbert-amlm | masking=amlm | 10 |
123
+ | 2.2 | gptbert-nhot | embedding=nhot | 10 |
124
+ | 2.3 | gptbert-forgetter | forgetter=true | 10 |
125
+ | 2.4 | gptbert-fasttext | embedding_init=fasttext | 10 |
126
+ | 2.5 | gptbert-morfessor | tokenizer=morfessor_bpe | 10 |
127
+ | 2.6 | gptbert-moe | use_moe=true (32exp, k=4, s=48) | 10 |
128
+ | 2.7 | gptbert-attnres | use_attn_res=true (4 blocks) | 10 |
129
+ | 2.8 | gptbert-moe-attnres | use_moe=true, use_attn_res=true | 10 |
130
+ | 2.9 | gptbert-sampleC | data=sample_C | 10 |
131
+
132
+ ---
133
+
134
+ ## Phase 3:学习率 Sweep(17 个实验)
135
+
136
+ **目标**:找 LAMB 在 GPT-BERT 上的最佳 LR。
137
+ **方法**:3A 用 3-epoch 快筛 12 个值,3B 将 Top 3 + 2 个插值点跑满 10 epoch。
138
+
139
+ ### Phase 3A:快筛(12 个,3 epoch)
140
+
141
+ | ID | lr | epochs |
142
+ |---|---|---|
143
+ | 3.1 | 0.003 | 3 |
144
+ | 3.2 | 0.005 | 3 |
145
+ | 3.3 | 0.007 | 3 |
146
+ | 3.4 | 0.008 | 3 |
147
+ | 3.5 | 0.009 | 3 |
148
+ | 3.6 | 0.010 | 3 |
149
+ | 3.7 | 0.012 | 3 |
150
+ | 3.8 | 0.015 | 3 |
151
+ | 3.9 | 0.018 | 3 |
152
+ | 3.10 | 0.020 | 3 |
153
+ | 3.11 | 0.025 | 3 |
154
+ | 3.12 | 0.030 | 3 |
155
+
156
+ ### Phase 3B:Top 3 + 插值(5 个,10 epoch)
157
+
158
+ | ID | lr | 说明 |
159
+ |---|---|---|
160
+ | 3.13 | BEST1(3A) | 第 1 名 |
161
+ | 3.14 | BEST2(3A) | 第 2 名 |
162
+ | 3.15 | BEST3(3A) | 第 3 名 |
163
+ | 3.16 | mid(BEST1,BEST2) | 1-2 名插值 |
164
+ | 3.17 | mid(BEST2,BEST3) | 2-3 名插值 |
165
+
166
+ **输出**:BEST_LR
167
+
168
+ ---
169
+
170
+ ## Phase 4:正则化 Sweep(25 个实验)
171
+
172
+ **目标**:找最佳 dropout / weight_decay / warmup / grad_norm。
173
+ **固定**:lr = BEST(P3),其余基线。
174
+
175
+ ### 4A:Dropout(7 个,3ep)
176
+
177
+ | ID | dropout |
178
+ |---|---|
179
+ | 4.1-4.7 | 0.00, 0.02, 0.05, 0.08, 0.10, 0.15, 0.20 |
180
+
181
+ ### 4B:Weight Decay(7 个,3ep)
182
+
183
+ | ID | weight_decay |
184
+ |---|---|
185
+ | 4.8-4.14 | 0.01, 0.03, 0.05, 0.08, 0.15, 0.20, 1.00 |
186
+
187
+ ### 4C:Warmup(5 个,3ep)
188
+
189
+ | ID | warmup_ratio |
190
+ |---|---|
191
+ | 4.15-4.19 | 0.00, 0.03, 0.10, 0.15, 0.20 |
192
+
193
+ ### 4D:Grad Norm(4 个,3ep)
194
+
195
+ | ID | max_grad_norm |
196
+ |---|---|
197
+ | 4.20-4.23 | 0.5, 1.0, 3.0, 5.0 |
198
+
199
+ ### 4E:最佳组合验证(2 个,10ep)
200
+
201
+ | ID | 说明 |
202
+ |---|---|
203
+ | 4.24 | BEST(4A) + BEST(4B) + BEST(4C) + BEST(4D) |
204
+ | 4.25 | 2ndBEST(4A) + BEST(4B) + BEST(4C) + BEST(4D) |
205
+
206
+ **输出**:BEST_DROPOUT, BEST_WD, BEST_WARMUP, BEST_GRADNORM → 合称 **TUNED_BASELINE**
207
+
208
+ ---
209
+
210
+ ## Phase 5:特性组合(20 个实验)
211
+
212
+ **目标**:P2 中赢出的特性做两两、三组、全组合测试。
213
+ **固定**:TUNED_BASELINE(含 BEST_LR + BEST 正则化参数)。
214
+
215
+ 假设 P2 赢家为 {amlm, nhot, forgetter, moe, attnres},以下为系统组合:
216
+
217
+ | ID | 名称 | amlm | nhot | fgt | moe | attnres |
218
+ |---|---|:---:|:---:|:---:|:---:|:---:|
219
+ | 5.1 | tuned-baseline(对照) | | | | | |
220
+ | 5.2 | tuned-amlm | ✓ | | | | |
221
+ | 5.3 | tuned-nhot | | ✓ | | | |
222
+ | 5.4 | tuned-fgt | | | ✓ | | |
223
+ | 5.5 | tuned-moe | | | | ✓ | |
224
+ | 5.6 | tuned-attnres | | | | | ✓ |
225
+ | 5.7 | amlm+nhot | ✓ | ✓ | | | |
226
+ | 5.8 | amlm+fgt | ✓ | | ✓ | | |
227
+ | 5.9 | amlm+moe | ✓ | | | ✓ | |
228
+ | 5.10 | nhot+fgt | | ✓ | ✓ | | |
229
+ | 5.11 | nhot+moe | | ✓ | | ✓ | |
230
+ | 5.12 | fgt+moe | | | ✓ | ✓ | |
231
+ | 5.13 | moe+attnres | | | | ✓ | ✓ |
232
+ | 5.14 | amlm+nhot+fgt | ✓ | ✓ | ✓ | | |
233
+ | 5.15 | amlm+nhot+moe | ✓ | ✓ | | ✓ | |
234
+ | 5.16 | amlm+fgt+moe | ✓ | | ✓ | ✓ | |
235
+ | 5.17 | nhot+fgt+moe | | ✓ | ✓ | ✓ | |
236
+ | 5.18 | amlm+nhot+fgt+moe | ✓ | ✓ | ✓ | ✓ | |
237
+ | 5.19 | 全部 5 特性 | ✓ | ✓ | ✓ | ✓ | ✓ |
238
+ | 5.20 | amlm+nhot+fgt+attnres | ✓ | ✓ | ✓ | | ✓ |
239
+
240
+ **输出**:BEST_COMBO(最佳特性子集)
241
+
242
+ ---
243
+
244
+ ## Phase 6:掩码与训练目标调参(20 个实验)
245
+
246
+ **固定**:TUNED_BASELINE + BEST_COMBO
247
+
248
+ ### 6A:掩码率(8 个,10ep)
249
+
250
+ | ID | mask_ratio → mask_ratio_end |
251
+ |---|---|
252
+ | 6.1-6.8 | 15→15, 20→15, 25→15, 35→15, 40→15, 40→20, 45→15, 30→10 |
253
+
254
+ ### 6B:MNTP:CLM 比例(6 个,10ep)
255
+
256
+ | ID | mntp_ratio |
257
+ |---|---|
258
+ | 6.9-6.14 | 1:1, 3:1, 5:1, 7:1, 10:1, 20:1 |
259
+
260
+ ### 6C:AMLM 参数(6 个,10ep,仅当 amlm ∈ BEST_COMBO)
261
+
262
+ | ID | amlm_lambda | amlm_update_interval | amlm_min/max_ratio |
263
+ |---|---|---|---|
264
+ | 6.15 | 0.05 | 200 | 0.05/0.50 |
265
+ | 6.16 | 0.10 | 200 | 0.05/0.50 |
266
+ | 6.17 | 0.30 | 200 | 0.05/0.50 |
267
+ | 6.18 | 0.20 | 50 | 0.05/0.50 |
268
+ | 6.19 | 0.20 | 500 | 0.05/0.50 |
269
+ | 6.20 | 0.20 | 200 | 0.10/0.60 |
270
+
271
+ **输出**:BEST_MASK_RATIO, BEST_MNTP, BEST_AMLM_PARAMS
272
+
273
+ ---
274
+
275
+ ## Phase 7:Batch Size 与序列长度(14 个实验)
276
+
277
+ ### 7A:有效 Batch Size(8 个,10ep)
278
+
279
+ | ID | batch_size | grad_accum | effective |
280
+ |---|---|---|---|
281
+ | 7.1 | 16 | 1 | 16 |
282
+ | 7.2 | 32 | 1 | 32 |
283
+ | 7.3 | 64 | 2 | 128 |
284
+ | 7.4 | 64 | 4 | 256 |
285
+ | 7.5 | 64 | 8 | 512 |
286
+ | 7.6 | 128 | 1 | 128 |
287
+ | 7.7 | 128 | 2 | 256 |
288
+ | 7.8 | 256 | 1 | 256 |
289
+
290
+ ### 7B:序列长度(6 个,10ep)
291
+
292
+ | ID | seq_len |
293
+ |---|---|
294
+ | 7.9-7.14 | 64, 96, 192, 256, 384, 512 |
295
+
296
+ **输出**:BEST_BATCH, BEST_GRAD_ACCUM, BEST_SEQ_LEN
297
+
298
+ ---
299
+
300
+ ## Phase 8:优化器探索(16 个实验)
301
+
302
+ ### 8A:AdamW LR Sweep(6 个,3ep)
303
+
304
+ | ID | optimizer | lr |
305
+ |---|---|---|
306
+ | 8.1-8.6 | AdamW | 0.0003, 0.0005, 0.001, 0.002, 0.003, 0.005 |
307
+
308
+ ### 8B:Muon LR Sweep(5 个,3ep)
309
+
310
+ | ID | optimizer | lr |
311
+ |---|---|---|
312
+ | 8.7-8.11 | Muon | 0.005, 0.008, 0.014, 0.020, 0.030 |
313
+
314
+ ### 8C:最佳替代优化器 + FORGETTER(5 个,10ep)
315
+
316
+ | ID | optimizer | lr | forgetter |
317
+ |---|---|---|---|
318
+ | 8.12 | AdamW | BEST(8A) | off |
319
+ | 8.13 | AdamW | BEST(8A) | on |
320
+ | 8.14 | Muon | BEST(8B) | off |
321
+ | 8.15 | Muon | BEST(8B) | on |
322
+ | 8.16 | LAMB | BEST_LR | on(重新确认) |
323
+
324
+ **输出**:BEST_OPTIMIZER, BEST_OPT_LR → 合称 **CONFIG_V1**
325
+
326
+ ---
327
+
328
+ ## Phase 9:架构修改(15 个实验,10ep)
329
+
330
+ **固定**:CONFIG_V1
331
+
332
+ ### 9A:Toggle Sweep(7 个)
333
+
334
+ | ID | 修改 |
335
+ |---|---|
336
+ | 9.1 | use_rope=true |
337
+ | 9.2 | use_geglu=false |
338
+ | 9.3 | use_pre_norm=false |
339
+ | 9.4 | use_attention_gate=true |
340
+ | 9.5 | use_dwa=true |
341
+ | 9.6 | rope + attention_gate |
342
+ | 9.7 | rope + dwa |
343
+
344
+ ### 9B:模型形状(8 个)
345
+
346
+ | ID | hidden | layers | heads | intermediate |
347
+ |---|---|---|---|---|
348
+ | 9.8 | 256 | 16 | 4 | 854 |
349
+ | 9.9 | 256 | 18 | 4 | 854 |
350
+ | 9.10 | 320 | 12 | 5 | 1067 |
351
+ | 9.11 | 384 | 16 | 6 | 1280 |
352
+ | 9.12 | 448 | 10 | 7 | 1494 |
353
+ | 9.13 | 512 | 8 | 8 | 1706 |
354
+ | 9.14 | 512 | 10 | 8 | 1706 |
355
+ | 9.15 | 576 | 8 | 9 | 1920 |
356
+
357
+ **输出**:BEST_ARCH_MODS, BEST_SHAPE
358
+
359
+ ---
360
+
361
+ ## Phase 10:MoE 超参调优(8 个实验,条件阶段)
362
+
363
+ **条件**:仅当 MoE ∈ BEST_COMBO(P5)时执行,否则跳过。
364
+ **固定**:CONFIG_V1 + use_moe=true
365
+
366
+ | ID | num_experts | top_k | expert_size | freq_penalty |
367
+ |---|---|---|---|---|
368
+ | 10.1 | 16 | 2 | 96 | 0.01 |
369
+ | 10.2 | 16 | 4 | 96 | 0.01 |
370
+ | 10.3 | 32 | 2 | 48 | 0.01 |
371
+ | 10.4 | 32 | 8 | 48 | 0.01 |
372
+ | 10.5 | 64 | 4 | 24 | 0.01 |
373
+ | 10.6 | 32 | 4 | 48 | 0.10 |
374
+ | 10.7 | 32 | 4 | 48 | 0.001 |
375
+ | 10.8 | 32 | 4 | 84 | 0.01 |
376
+
377
+ **输出**:BEST_MOE_CONFIG
378
+
379
+ ---
380
+
381
+ ## Phase 11:分词器 × 嵌入交叉(6 个实验,10ep)
382
+
383
+ | ID | tokenizer | embedding | init |
384
+ |---|---|---|---|
385
+ | 11.1 | bpe | standard | random |
386
+ | 11.2 | bpe | nhot | random |
387
+ | 11.3 | bpe | standard | fasttext |
388
+ | 11.4 | bpe | nhot | fasttext |
389
+ | 11.5 | morfessor_bpe | standard | random |
390
+ | 11.6 | morfessor_bpe | nhot | random |
391
+
392
+ **输出**:BEST_TOK_EMB → 合称 **CONFIG_V2**
393
+
394
+ ---
395
+
396
+ ## Phase 12:数据集构建方式(18 个实验,10ep)
397
+
398
+ **固定**:CONFIG_V2
399
+
400
+ | ID | 名称 | 数据配置 | 说明 |
401
+ |---|---|---|---|
402
+ | 12.1 | champion-replica | BabyLM 33% + FineWeb 33% + Cosmo 34% | Charpentier 2024 方案复刻 |
403
+ | 12.2 | pure-quality | sample_A | 纯质量排序 |
404
+ | 12.3 | task-quota | sample_B | 任务配额(当前默认) |
405
+ | 12.4 | weighted-random | sample_C | 加权随机 |
406
+ | 12.5 | semantic-match | sample_D | 嵌入相似度 |
407
+ | 12.6 | quota+paraphrase | sample_B + Paraphrase ~2M | Qwen3.5 改写 |
408
+ | 12.7 | quota+variation | sample_B + Variation Sets ~1M | 句式变体 |
409
+ | 12.8 | quota+recombitext | sample_B + RecombiText ~5M | 统计增强 |
410
+ | 12.9 | quota+cd-synth | sample_B + Contrastive Decoding ~3M | 自合成 |
411
+ | 12.10 | quota+mattr | sample_B + MATTR 排序 | 词汇多样性排序 |
412
+ | 12.11 | eval-mixed | 评测数据混入采样池 | 评测句参与采样 |
413
+ | 12.12 | no-eval | 不使用评测数据 | 对照组 |
414
+ | 12.13 | full-augment | Para + VS + MATTR | 全量增强组合 |
415
+ | 12.14 | fineweb-heavy | sample_B + FineWeb 33% | 高 FineWeb 比例 |
416
+ | 12.15 | fineweb-very-heavy | sample_B + FineWeb 67% | 重 FineWeb |
417
+ | 12.16 | knowledge-combo | sample_B + ConceptNet + GenericsKB | 知识数据 |
418
+ | 12.17 | para+fineweb | sample_B + Para 25% + FineWeb 25% | 改写+知识 |
419
+ | 12.18 | 3way-equal | 1:1:1 official + FineWeb + Cosmo | 三等分 |
420
+
421
+ **输出**:BEST_DATA
422
+
423
+ ---
424
+
425
+ ## Phase 13:跨架构验证(15 个实验,10ep)
426
+
427
+ **目标**:用 CONFIG_V2 重测其他架构,看优化后是否有逆转。
428
+
429
+ | ID | arch | use_moe | use_attn_res | 说明 |
430
+ |---|---|---|---|---|
431
+ | 13.1 | gpt2 | false | false | GPT-2 调优 |
432
+ | 13.2 | gpt2 | true | false | GPT-2 + MoE |
433
+ | 13.3 | gpt2 | false | true | GPT-2 + AttnRes |
434
+ | 13.4 | gpt2 | true | true | GPT-2 + 两者 |
435
+ | 13.5 | modernized_bert | false | false | ModernBERT 调优 |
436
+ | 13.6 | modernized_bert | true | false | + MoE |
437
+ | 13.7 | modernized_bert | false | false | + AMLM |
438
+ | 13.8 | xlstm | false | false | xLSTM 调优 |
439
+ | 13.9 | xlstm | true | false | + MoE |
440
+ | 13.10 | xlstm | false | true | + AttnRes |
441
+ | 13.11 | rtd | false | false | RTD 调优 |
442
+ | 13.12 | rtd | true | false | RTD + MoE |
443
+ | 13.13 | gpt2 | false | false | GPT-2 wide(512×8) |
444
+ | 13.14 | modernized_bert | false | false | ModernBERT wide |
445
+ | 13.15 | xlstm | false | false | xLSTM wide |
446
+
447
+ **输出**:BEST_OVERALL_ARCH
448
+
449
+ ---
450
+
451
+ ## Phase 14:LR × Dropout 二维网格(27 个实验)
452
+
453
+ ### 14A:3-epoch 快筛(24 个)
454
+
455
+ 6 个 LR × 4 个 Dropout:
456
+
457
+ | | d=0.0 | d=BEST/2 | d=BEST | d=BEST×1.5 |
458
+ |---|---|---|---|---|
459
+ | lr=BEST×0.7 | 14.1 | 14.2 | 14.3 | 14.4 |
460
+ | lr=BEST×0.85 | 14.5 | 14.6 | 14.7 | 14.8 |
461
+ | lr=BEST | 14.9 | 14.10 | 14.11 | 14.12 |
462
+ | lr=BEST×1.15 | 14.13 | 14.14 | 14.15 | 14.16 |
463
+ | lr=BEST×1.3 | 14.17 | 14.18 | 14.19 | 14.20 |
464
+ | lr=BEST×1.5 | 14.21 | 14.22 | 14.23 | 14.24 |
465
+
466
+ ### 14B:Top 3 验证(3 个,10ep)
467
+
468
+ | ID | lr | dropout |
469
+ |---|---|---|
470
+ | 14.25-14.27 | Top 3 from 14A |
471
+
472
+ **输出**:FINAL_LR, FINAL_DROPOUT → **CONFIG_V3**
473
+
474
+ ---
475
+
476
+ ## Phase 15:杂项超参(12 个实验,10ep)
477
+
478
+ | ID | 参数 | 值 |
479
+ |---|---|---|
480
+ | 15.1-15.4 | z_loss_weight | 0.0, 0.00005, 0.0005, 0.001 |
481
+ | 15.5-15.8 | position_bucket_size | 8, 16, 64, 128 |
482
+ | 15.9-15.12 | betas | (0.8,0.95), (0.9,0.95), (0.9,0.999), (0.95,0.999) |
483
+
484
+ **输出**:BEST_ZLOSS, BEST_BUCKET, BEST_BETAS → **CONFIG_V4**
485
+
486
+ ---
487
+
488
+ ## Phase 16:知识蒸馏(14 个实验,10ep)
489
+
490
+ | ID | teacher | temperature | alpha | 说明 |
491
+ |---|---|---|---|---|
492
+ | 16.1-16.4 | Qwen3.5-1.5B | 2/4/6/8 | 0.5 | 温度 sweep |
493
+ | 16.5-16.8 | Qwen3.5-1.5B | 4 | 0.2/0.3/0.7/0.9 | alpha sweep |
494
+ | 16.9 | Qwen3.5-3B | 4 | 0.5 | 更大 teacher |
495
+ | 16.10 | Qwen3.5-9B | 4 | 0.5 | 最大 teacher |
496
+ | 16.11 | BEST teacher | BEST T | BEST α | 最�� KD 配置 |
497
+ | 16.12 | BEST(无 FORGETTER) | BEST T | BEST α | KD-FORGETTER 交互 |
498
+ | 16.13 | BEST(无 AMLM) | BEST T | BEST α | KD-AMLM 交互 |
499
+ | 16.14 | 无 KD | — | — | 对照组 |
500
+
501
+ **输出**:BEST_KD_CONFIG → **CONFIG_V5**
502
+
503
+ ---
504
+
505
+ ## Phase 17:Epoch 数与 Checkpoint 策略(12 个实验)
506
+
507
+ | ID | epochs | ckpt_averaging | avg_last_k |
508
+ |---|---|---|---|
509
+ | 17.1-17.5 | 3/5/7/8/10 | false | — |
510
+ | 17.6-17.9 | 10 | true | 2/3/5/7 |
511
+ | 17.10-17.12 | 5/7/8 | true | 3 |
512
+
513
+ **输出**:BEST_EPOCHS, BEST_CKPT → **CONFIG_V6**
514
+
515
+ ---
516
+
517
+ ## Phase 18:种子方差(8 个实验)
518
+
519
+ | ID | seed |
520
+ |---|---|
521
+ | 18.1-18.8 | 7, 42, 123, 2024, 2026, 9999, 31415, 54321 |
522
+
523
+ **输出**:Mean ± Std of BLiMP。如果 Std > 1.5pp 则配置不稳定,需回溯。
524
+
525
+ ---
526
+
527
+ ## Phase 19:数据 × 特性交互(24 个实验,10ep)
528
+
529
+ **目标**:P12 的 Top 3 数据配置 × 3 种特性配置 × 2 种模型形状 + 额外变体。
530
+
531
+ | | FEAT_NONE | FEAT_PARTIAL | FEAT_FULL |
532
+ |---|---|---|---|
533
+ | TOP_DATA_1 × SHAPE_BASE | 19.1 | 19.2 | 19.3 |
534
+ | TOP_DATA_1 × SHAPE_ALT | 19.4 | 19.5 | 19.6 |
535
+ | TOP_DATA_2 × SHAPE_BASE | 19.7 | 19.8 | 19.9 |
536
+ | TOP_DATA_2 × SHAPE_ALT | 19.10 | 19.11 | 19.12 |
537
+ | TOP_DATA_3 × SHAPE_BASE | 19.13 | 19.14 | 19.15 |
538
+ | TOP_DATA_3 × SHAPE_ALT | 19.16 | 19.17 | 19.18 |
539
+
540
+ 额外变体(+Morfessor / +FastText / +KD):19.19-19.24
541
+
542
+ **输出**:FINAL_DATA, FINAL_FEATURES, FINAL_SHAPE → **CONFIG_FINAL**
543
+
544
+ ---
545
+
546
+ ## Phase 20:最终候选与消融保险(20 个实验)
547
+
548
+ ### 20A:最终候选变体(10 个)
549
+
550
+ | ID | 说明 |
551
+ |---|---|
552
+ | 20.1 | CONFIG_FINAL 原版 |
553
+ | 20.2 | + RoPE(如果还没包含) |
554
+ | 20.3 | + DWA |
555
+ | 20.4 | + attention_gate |
556
+ | 20.5 | + ckpt_averaging |
557
+ | 20.6 | + 次优优化器 |
558
+ | 20.7 | + 次优数据 |
559
+ | 20.8 | + 次优形状 |
560
+ | 20.9 | + Morfessor(如果 BPE 赢了) |
561
+ | 20.10 | 无 KD(如果 KD 在 CONFIG_FINAL 中) |
562
+
563
+ ### 20B:消融保险(10 个,每次移除一个组件)
564
+
565
+ | ID | 移除的组件 |
566
+ |---|---|
567
+ | 20.11 | 移除 AMLM |
568
+ | 20.12 | 移除 N-hot |
569
+ | 20.13 | 移除 FORGETTER |
570
+ | 20.14 | 移除 MoE |
571
+ | 20.15 | 移除 AttnRes |
572
+ | 20.16 | 移除 KD |
573
+ | 20.17 | 移除 ckpt_averaging |
574
+ | 20.18 | 换回基线 LR |
575
+ | 20.19 | 换回基线 dropout |
576
+ | 20.20 | 换回基线数据 |
577
+
578
+ **决策规则**:移除某组件后 BLiMP 下降 < 0.3pp → 该组件无贡献,从最终配置中移除。
579
+
580
+ ---
581
+
582
+ ## Phase 21:提交种子与完整评测(16 个实验)
583
+
584
+ | ID | 说明 |
585
+ |---|---|
586
+ | 21.1-21.5 | CONFIG_FINAL × 5 seeds (42/7/123/2026/9999) + full eval |
587
+ | 21.6-21.8 | 次佳候选(P20A) × 3 seeds + full eval |
588
+ | 21.9-21.11 | 第三候选 × 3 seeds + full eval |
589
+ | 21.12 | 跨种子 checkpoint 平均 |
590
+ | 21.13-21.14 | 最佳 seed 的 ckpt avg last 3/5 |
591
+ | 21.15 | GLUE 微调完整评测 |
592
+ | 21.16 | 安全重跑(最终确认) |
593
+
594
+ **输出**:SUBMISSION_MODEL
595
+
596
+ ---
597
+
598
+ ## Phase 22:预留探索(61 个实验)
599
+
600
+ | 分组 | 数量 | 说明 |
601
+ |---|---|---|
602
+ | 22A | 10 | 次优架构深度调优 |
603
+ | 22B | 10 | 数据增强比例调优(Para 10-50%、RecombiText 25-75%) |
604
+ | 22C | 5 | MoE 替代配置 |
605
+ | 22D | 10 | 关键参数插值精调 |
606
+ | 22E | 6 | Debug / 失败实验重跑 |
607
+ | 22F | 20 | 新想法 / 新论文启发 |
608
+
609
+ ---
610
+
611
+ ## 实验总汇
612
+
613
+ | Phase | 内容 | 实验数 | GPU 时 | 依赖 |
614
+ |:-----:|------|:------:|:------:|------|
615
+ | P1 | 架构对比 | 5 | 3.3 | — |
616
+ | P2 | 特性消融 | 9 | 6.0 | P1 |
617
+ | P3 | LR sweep | 17 | 3.4 | P1 |
618
+ | P4 | 正则化 sweep | 25 | 5.0 | P3 |
619
+ | P5 | 特性组合 | 20 | 13.3 | P2,P3,P4 |
620
+ | P6 | 掩码调参 | 20 | 13.3 | P5 |
621
+ | P7 | Batch/SeqLen | 14 | 9.3 | P6 |
622
+ | P8 | 优化器 | 16 | 10.7 | P7 |
623
+ | P9 | 架构修改 | 15 | 10.0 | P8 |
624
+ | P10 | MoE 调参 | 8 | 5.3 | P5(条件) |
625
+ | P11 | 分词×嵌入 | 6 | 4.0 | P9 |
626
+ | P12 | 数据构建 | 18 | 12.0 | P9 |
627
+ | P13 | 跨架构验证 | 15 | 10.0 | P12 |
628
+ | P14 | LR×Dropout 网格 | 27 | 4.8 | P12 |
629
+ | P15 | 杂项超参 | 12 | 8.0 | P14 |
630
+ | P16 | 知识蒸馏 | 14 | 9.3 | P15 |
631
+ | P17 | Epoch/Ckpt | 12 | 8.0 | P16 |
632
+ | P18 | 种子方差 | 8 | 5.3 | P17 |
633
+ | P19 | 数据×特性交互 | 24 | 16.0 | P12,P17 |
634
+ | P20 | 最终候选+消融 | 20 | 13.3 | P19 |
635
+ | P21 | 提交 | 16 | 10.7 | P20 |
636
+ | P22 | 预留 | 61 | 40.7 | 任意 |
637
+ | **合计** | | **382** | **~221h** | |
638
+
639
+ ---
640
+
641
+ ## 代码就绪情况
642
+
643
+ | 组件 | 状态 | 缺什么 |
644
+ |------|:----:|--------|
645
+ | 训练框架 (train.py) | ✅ | — |
646
+ | 5 种基础架构 | ✅ | — |
647
+ | MoE 模块 | ✅ | — |
648
+ | AttnRes 模块 | ✅ | — |
649
+ | AMLM / 掩码衰减 | ✅ | — |
650
+ | N-hot / FastText 嵌入 | ✅ 代码 | ❌ FastText 模型未训练 |
651
+ | BPE / Morfessor 分词器 | ✅ | — |
652
+ | 知识蒸馏 | ✅ 代码 | ❌ Teacher 模型未下载 |
653
+ | 采样 A/B/C/D | ✅ 代码 | ❌ A/D 未运行 |
654
+ | Paraphrase 增强 | ✅ 代码 | ❌ 未运行 |
655
+ | Variation Sets 增强 | ❌ | 需写脚本 |
656
+ | RecombiText 增强 | ❌ | 需写脚本 |
657
+ | Contrastive Decoding | ❌ | 需写脚本 |
658
+ | MATTR 排序 | ❌ | 需写脚本 |
659
+ | 冠军复刻数据配置 | ❌ | 需写 1:1:1 混合脚本 |
660
+ | **实验 CSV 生成器** | ⚠️ 需更新 | 缺 MoE/AttnRes 列和实验 |
661
+ | **批量运行脚本** | ❌ | 读 CSV → 启动训练 → 收集结果 |
662
+ | **评测自动化** | ⚠️ | 需确认 fast_eval 可自动调用 |
663
+ | **结果收集/对比** | ❌ | 汇总各实验分数的脚本 |
664
+
665
+ ### 优先级路线图
666
+
667
+ ```
668
+ 第 1 步:写批量运行脚本 + 结果收集器
669
+ → P1-P5 的 ~76 个实验可以立即开始跑
670
+
671
+ 第 2 步(与第 1 步并行):
672
+ 训练 FastText 模型
673
+ 运行 sample_A / sample_D
674
+ 运行 Paraphrase 生成
675
+ → P6-P11 就绪
676
+
677
+ 第 3 步(与前面并行):
678
+ 写 Variation Sets / RecombiText / CD / MATTR 脚本
679
+ 写冠军复刻采样脚本
680
+ 下载 teacher 模型
681
+ → P12-P16 就绪
682
+
683
+ 第 4 步:更新 generate_experiments.py
684
+ → 完整 382 实验 CSV 生成
685
+ ```
notes/progress_report.md ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 进展报告:BabyLM Challenge 2026 Strict-Small
2
+
3
+ > **日期**:2026-03-23
4
+ > **当前阶段**:代码开发完成,组件验证通过,准备上传 HuggingFace 并开始正式实验
5
+
6
+ ---
7
+
8
+ ## 一、项目总览
9
+
10
+ 参加 BabyLM Challenge 2026 Strict-Small 赛道(训练数据 ≤ 10M 词),目标是训练一个小型语言模型(~30M 参数)在多项语言评测任务上取得最佳成绩。
11
+
12
+ **评测任务**:BLiMP(语法)、EWoK(世界知识)、Entity Tracking(实体追踪)、GLUE(语言理解,需微调)、WUG(形态学)、COMPS(概念属性)、AoA(词汇习得)、Reading Time(阅读时间)
13
+
14
+ **2025 Strict-Small 排行榜**(我们的目标赛道):
15
+ | 模型 | TextAvg | BLiMP | GLUE | EWoK |
16
+ |------|:-------:|:-----:|:----:|:----:|
17
+ | amlm_hd_fail(冠军) | **45.4** | 59.0 | 57.7 | 56.0 |
18
+ | MoEP(亚军) | 44.5 | 60.8 | 64.2 | 50.2 |
19
+ | Baseline GPT-BERT | 41.2 | 71.7 | 65.1 | 49.5 |
20
+ | Baseline GPT-2 | 37.4 | 66.4 | 55.9 | 49.9 |
21
+
22
+ ---
23
+
24
+ ## 二、项目结构
25
+
26
+ ```
27
+ BabyLM_Challenge/
28
+ ├── experiments.csv # 384 个实验配置
29
+ ├── requirements.txt # Python 依赖
30
+ ├── scripts/
31
+ │ ├── 01_data/ # 数据处理 pipeline (Stage 1-9)
32
+ │ ├── 02_model/ # 分词器训练 (BPE / Morfessor)
33
+ │ ├── 03_training/ # 训练框架
34
+ │ │ ├── config.py # 配置系统 (YAML + CLI override)
35
+ │ │ ├── data.py # 数据加载、3种掩码策略、逐句/拼接模式
36
+ │ │ ├── train.py # 训练循环 + 自动评测 (fast/full)
37
+ │ │ ├── evaluate.py # 评测包装 (zero-shot + GLUE微调 + AoA)
38
+ │ │ ├── utils.py # 5种优化器、调度器、checkpoint
39
+ │ │ ├── hf_export/ # HuggingFace 模型导出 (5种架构)
40
+ │ │ ├── models/ # 7种模型架构
41
+ │ │ │ ├── gpt2.py # D0 基线
42
+ │ │ │ ├── gpt_bert.py # D1 双目标混合
43
+ │ │ │ ├── modern_bert.py # D2 现代双向
44
+ │ │ │ ├── xlstm.py # D3 矩阵记忆
45
+ │ │ │ ├── rtd.py # D4 替换Token检测
46
+ │ │ │ ├── moe.py # D5 MoE稀疏专家 (可叠加)
47
+ │ │ │ ├── attn_res.py # D6 注意力残差 (可叠加)
48
+ │ │ │ ├── embeddings.py # C0标准 / C1 N-hot
49
+ │ │ │ └── distillation.py # 知识蒸馏
50
+ │ │ └── configs/exp_A.yaml # 基线YAML配置
51
+ │ └── tools/ # 实验管理工具
52
+ │ ├── generate_experiments.py # 生成 experiments.csv
53
+ │ ├── run_experiments.py # 批量运行 (CSV→YAML→训练→评测→结果)
54
+ │ ├── collect_results.py # 结果汇总
55
+ │ ├── smoke_test.py # 冒烟测试 (44个代码路径)
56
+ │ └── verify_components.py # 组件验证 (32项逐个验证)
57
+ ├── notes/
58
+ │ ├── encyclopedia.md # 改进点百科 (所有优化维度详解)
59
+ │ ├── experiment_plan.md # 实验计划 (384个实验, 22阶段)
60
+ │ ├── experiment_log.md # 实验结果记录
61
+ │ ├── progress_report.md # 本文件
62
+ │ ├── ref_datasets.md # 数据集详细信息
63
+ │ ├── ref_leaderboard_2025.md # 2025排行榜数据
64
+ │ └── babylm_leaderboard_2025.csv # 原始排行榜CSV
65
+ ├── data/ # 训练数据 (14个源, 119M词原始)
66
+ │ ├── raw_files/ # 原始数据文件
67
+ │ ├── 1_noise_filtered/ ~ 7_eval_sentences/ # 处理流水线各阶段
68
+ │ ├── 8_sample_B/train.txt # 策略B采样 (9.99M词) ✅
69
+ │ └── 8_sample_C/train.txt # 策略C采样 (9.99M词) ✅
70
+ ├── models/
71
+ │ ├── tokenizer/ # BPE 8K 分词器 ✅
72
+ │ ├── tokenizer_morfessor/ # Morfessor+BPE 分词器 ✅
73
+ │ └── qwen3.5-9b/ # Qwen模型 (Paraphrase生成用, 18GB, 不上传)
74
+ ├── evaluation-pipeline-2025/ # 官方评测pipeline
75
+ ├── 2025_papers/ # 参考论文PDF
76
+ ├── checkpoints/ # 训练checkpoint (空)
77
+ └── experiment_results/ # 实验结果 (空)
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 三、已实现的全部功能
83
+
84
+ ### 编号规则:每个维度 X0 = 基线(不优化的默认选择)
85
+
86
+ ### B: 分词器
87
+
88
+ | 编号 | 方法 | 状态 | 配置 |
89
+ |:----:|------|:----:|------|
90
+ | B0 | BPE 8K(基线) | ✅ | `tokenizer: bpe` |
91
+ | B1 | Morfessor+BPE | ✅ | `tokenizer: morfessor_bpe` |
92
+ | B2 | 词表大小 (4K/8K/16K) | ✅ | 需重新训练分词器 |
93
+
94
+ ### C: 嵌入策略
95
+
96
+ | 编号 | 方法 | 状态 | 配置 |
97
+ |:----:|------|:----:|------|
98
+ | C0 | 标准嵌入(基线) | ✅ | `embedding.type: standard` |
99
+ | C1 | N-hot 形态嵌入 | ✅ | `embedding.type: nhot` |
100
+ | C2 | FastText 初始化 | ✅ 代码 | `embedding.init: fasttext`(需先训练FastText模型) |
101
+
102
+ ### D: 模型架构
103
+
104
+ | 编号 | 方法 | 状态 | 验证 | 配置 |
105
+ |:----:|------|:----:|:----:|------|
106
+ | D0 | GPT-2(基线) | ✅ | loss=5.31 | `arch: gpt2, objective: clm` |
107
+ | D1 | GPT-BERT | ✅ | loss=5.74 | `arch: gpt_bert, objective: gpt_bert` |
108
+ | D2 | ModernBERT | ✅ | loss=3.33 | `arch: modernized_bert, objective: mlm` |
109
+ | D3 | xLSTM | ✅ | loss=4.54 | `arch: xlstm, objective: clm` |
110
+ | D4 | RTD/ELECTRA | ✅ | loss=3.99 | `arch: rtd, objective: rtd` |
111
+ | D5 | MoE(可叠加) | ✅ | loss=5.79 | `use_moe: true` |
112
+ | D6 | AttnRes(可叠加) | ✅ | loss=5.74 | `use_attn_res: true` |
113
+
114
+ ### E: 训练目标与掩码
115
+
116
+ | 编号 | 方法 | 状态 | 验证 | 配置 |
117
+ |:----:|------|:----:|:----:|------|
118
+ | E0 | 标准均匀掩码(基线) | ✅ | loss=5.74 | `masking.type: standard` |
119
+ | E1 | AMLM 自适应掩码 | ✅ | loss=5.75 | `masking.type: amlm` |
120
+ | E2 | 掩码率衰减 | ✅ | — | `mask_ratio: 0.30, mask_ratio_end: 0.15` |
121
+ | E3 | 频率感知掩码 | ✅ | loss=5.74 | `masking.type: frequency` |
122
+ | E4 | MNTP:CLM 比例 | ✅ | — | `training.mntp_ratio: 15` |
123
+ | E5 | 反向课程 MTP | ✅ | — | `training.use_mtp: true` |
124
+
125
+ ### F: 优化器
126
+
127
+ | 编号 | 方法 | 状态 | 验证 | 配置 | 注意事项 |
128
+ |:----:|------|:----:|:----:|------|---------|
129
+ | F0 | Adam(基线) | ✅ | loss=6.90 | `optimizer.type: adam` | 收敛慢 |
130
+ | F1 | AdamW | ✅ | loss=5.74 | `optimizer.type: adamw` | **推荐默认** |
131
+ | F2 | LAMB | ✅ | loss=5.74 | `optimizer.type: lamb` | **fp16下lr≤0.005** |
132
+ | F3 | FORGETTER | ✅ | loss=5.78 | `optimizer.forgetter: true` | 需 wd=1.0 |
133
+ | F4 | Muon | ✅ | loss=5.69 | `optimizer.type: muon` | 慢2.5x,loss最低 |
134
+
135
+ ### G: 超参数与训练方式
136
+
137
+ | 编号 | 方法 | 状态 | 配置 |
138
+ |:----:|------|:----:|------|
139
+ | G1-G5 | lr/batch/wd/dropout/seqlen | ✅ | YAML 配置 |
140
+ | G6 | 逐句模式 | ✅ | `data.packing: sentence`(较慢) |
141
+
142
+ ### H: 训练技巧
143
+
144
+ | 编号 | 方法 | 状态 |
145
+ |:----:|------|:----:|
146
+ | H1 | Checkpoint 选择 | ✅ 每epoch保存 |
147
+ | H2 | Checkpoint 平均 | ✅ `checkpoint.averaging: true` |
148
+ | H3 | 知识蒸馏 | ✅ 代码完整(需下载teacher模型) |
149
+
150
+ ### 评测集成
151
+
152
+ | 模式 | 内容 | 时间 |
153
+ |------|------|------|
154
+ | fast | Zero-shot: BLiMP, EWoK, ET, WUG, Reading | ~15分钟 |
155
+ | **full** | Zero-shot + GLUE微调(7子任务) + AoA | ~60-90分钟 |
156
+
157
+ ---
158
+
159
+ ## 四、验证结果
160
+
161
+ ### 组件验证(32 项,每项 3 epoch)
162
+
163
+ | 组 | 通过 | 失败 | 已知问题 |
164
+ |----|:----:|:----:|---------|
165
+ | 架构 (11项) | 11 | 0 | — |
166
+ | 嵌入 (2项) | 2 | 0 | — |
167
+ | 分词 (2项) | 2 | 0 | — |
168
+ | 掩码 (3项) | 3 | 0 | — |
169
+ | 优化器 (11项) | 8 | 3 | LAMB+fp16+lr>0.005=NAN(已修复默认LR) |
170
+ | 超参 (1项) | 0 | 1 | sentence模式数据加载慢(需增大timeout) |
171
+ | 组合 (2项) | 2 | 0 | — |
172
+
173
+ ### 第一个完整实验
174
+
175
+ GPT-BERT + AdamW + lr=5e-4 + 10 epoch + fast eval:
176
+
177
+ | 指标 | 我们 | 2025基线 |
178
+ |------|:----:|:--------:|
179
+ | BLiMP | **75.37** | 71.7 |
180
+ | BLiMP Supp | 60.40 | 63.2 |
181
+ | EWoK | 49.18 | 49.5 |
182
+ | Entity Tracking | **42.29** | 34.6 |
183
+ | Avg Zero-Shot | **56.81** | — |
184
+
185
+ ---
186
+
187
+ ## 五、已知问题
188
+
189
+ | 问题 | 状态 | 解决方案 |
190
+ |------|:----:|---------|
191
+ | LAMB+fp16+lr>0.005=NAN | ✅ 已修复 | 默认LR改为0.005 |
192
+ | G6 sentence模式慢 | 已知 | 增大timeout或优化数据加载 |
193
+ | FastText模型未训练 | 待做 | `python scripts/02_model/train_fasttext.py` |
194
+ | KD teacher模型未下载 | 待做 | 下载Qwen teacher |
195
+ | 数据增强脚本未写 | 待做 | Variation Sets/RecombiText/CD/MATTR/信息密集合成 |
196
+
197
+ ---
198
+
199
+ ## 六、下一步工作
200
+
201
+ ### 立即要做
202
+
203
+ 1. **上传项目到 HuggingFace**(排除 qwen3.5-9b 模型)
204
+ 2. **开始正式实验 Phase 1-5**:
205
+ - P1: 5种架构对比
206
+ - P2: 核心特性消融 (AMLM/N-hot/FORGETTER/MoE/AttnRes)
207
+ - P3: LR sweep
208
+ - P4: 正则化 sweep
209
+ - P5: 特性组合
210
+
211
+ ### 后续阶段
212
+
213
+ - P6-P11: 掩码调参、batch size、优化器、架构修改、分词×嵌入
214
+ - P12: 数据集构建方式 (13种)
215
+ - P13-P21: 跨架构验证 → 最终候选 → 提交
216
+ - 数据增强脚本开发
217
+
218
+ ---
219
+
220
+ ## 七、如何运行
221
+
222
+ ### 环境安装
223
+ ```bash
224
+ pip install -r requirements.txt
225
+ pip install muon-optimizer torch-optimizer morfessor
226
+ ```
227
+
228
+ ### 单个实验
229
+ ```bash
230
+ python -m scripts.03_training.train --config scripts/03_training/configs/exp_A.yaml
231
+ python -m scripts.03_training.train --config xxx.yaml --eval-mode fast # 快速评测
232
+ python -m scripts.03_training.train --config xxx.yaml --skip-eval # 跳过评测
233
+ ```
234
+
235
+ ### 批量实验
236
+ ```bash
237
+ python scripts/tools/run_experiments.py --phase 1 --list # 查看
238
+ python scripts/tools/run_experiments.py --phase 1 # 运行
239
+ python scripts/tools/run_experiments.py --phase 1 --resume # 断点续跑
240
+ python scripts/tools/collect_results.py --phase 1 # 查看结果
241
+ ```
242
+
243
+ ### 组件验证
244
+ ```bash
245
+ python scripts/tools/verify_components.py --group arch # 验证架构
246
+ python scripts/tools/verify_components.py --resume # 跑全部(跳过已通过)
247
+ ```
248
+
249
+ ---
250
+
251
+ ## 八、关键配置速查
252
+
253
+ ```yaml
254
+ model:
255
+ arch: gpt2 # gpt2(D0) | gpt_bert(D1) | modernized_bert(D2) | xlstm(D3) | rtd(D4)
256
+ use_moe: false # D5 MoE
257
+ use_attn_res: false # D6 AttnRes
258
+ data:
259
+ tokenizer: bpe # bpe(B0) | morfessor_bpe(B1)
260
+ packing: concat # concat | sentence(G6)
261
+ embedding:
262
+ type: standard # standard(C0) | nhot(C1)
263
+ init: random # random | fasttext(C2)
264
+ training:
265
+ objective: gpt_bert # gpt_bert | clm | mlm | rtd
266
+ learning_rate: 0.0005 # AdamW推荐; LAMB需≤0.005(fp16)
267
+ use_mtp: false # E5 反向课程MTP
268
+ masking:
269
+ type: standard # standard(E0) | amlm(E1) | frequency(E3)
270
+ optimizer:
271
+ type: adamw # adam(F0) | adamw(F1) | lamb(F2) | muon(F4)
272
+ forgetter: false # F3 FORGETTER
273
+ ```
notes/ref_datasets.md ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 数据集全览
2
+
3
+ > BabyLM Challenge 2026 · Strict-Small 赛道(≤ 10M 词)
4
+ > 更新日期: 2026-03-16
5
+
6
+ ---
7
+
8
+ ## 一、官方训练数据
9
+
10
+ 比赛提供的 7 个训练文件,存放于 `data/raw_files/`。
11
+
12
+ ### 1. CHILDES
13
+
14
+ | 项目 | 内容 |
15
+ |------|------|
16
+ | **全名** | Child Language Data Exchange System |
17
+ | **行数 / 词数** | 5,638,783 行 / 28.4M 词 |
18
+ | **来源** | Brian MacWhinney & Catherine Snow,1984 年建立,Carnegie Mellon University 维护 |
19
+ | **内容** | 儿童语言习得转写记录,含儿童和照顾者的对话,CHAT 格式标注 |
20
+ | **许可** | CC BY-NC-SA 3.0 |
21
+ | **链接** | https://childes.talkbank.org/ |
22
+ | **特点** | 短句为主(平均 5 词/行),含 CHAT 格式噪声(`*CHI:` 前缀、`xxx` 标记、`@`/`%` 标注行) |
23
+ | **优势** | 真实儿童语言习得数据,对 AoA 评测(词汇习得顺序)最有价值;含丰富的基础语法结构 |
24
+ | **劣势** | 噪声多(需 Stage 1/4 清洗);短句多导致句法复杂度低;不含书面语 |
25
+ | **目标任务** | AoA(最重要)、BLiMP(基础语法) |
26
+ | **引用** | MacWhinney, B. (2000). *The CHILDES Project: Tools for Analyzing Talk*. 3rd Ed. Lawrence Erlbaum Associates. |
27
+
28
+ ### 2. Gutenberg
29
+
30
+ | 项目 | 内容 |
31
+ |------|------|
32
+ | **全名** | Project Gutenberg 儿童文学子集 |
33
+ | **行数 / 词数** | 661,928 行 / 25.6M 词 |
34
+ | **来源** | Project Gutenberg,按儿童适宜主题元数据筛选的公版书 |
35
+ | **内容** | 书面文学,长句多(平均 38.6 词/行),句法结构最丰富 |
36
+ | **许可** | 公有领域(美国) |
37
+ | **链接** | https://www.gutenberg.org/ |
38
+ | **特点** | 质量最高的来源;含古英语拼写和专有名词;含 Project Gutenberg 模板文本(Stage 4 过滤) |
39
+ | **优势** | 句法最丰富,BLiMP 覆盖率最高;书面语质量极佳 |
40
+ | **劣势** | 含古英语表达(`reckly`、`bettern`);语域单一(文学) |
41
+ | **目标任务** | BLiMP(句法)、Reading Time(连贯散文)、GLUE |
42
+ | **引用** | https://www.gutenberg.org/ |
43
+
44
+ ### 3. Open Subtitles
45
+
46
+ | 项目 | 内容 |
47
+ |------|------|
48
+ | **全名** | OpenSubtitles 电影字幕语料 |
49
+ | **行数 / 词数** | 3,333,618 行 / 19.2M 词 |
50
+ | **来源** | Pierre Lison & Jörg Tiedemann,OPUS 平行语料库项目 |
51
+ | **内容** | 电影/电视字幕的英语转写,口语化短句 |
52
+ | **许可** | CC BY-NC-SA 3.0 |
53
+ | **链接** | https://opus.nlpl.eu/datasets/OpenSubtitles |
54
+ | **特点** | 口语化(平均 5.8 词/行);原始数据含时间戳和 HTML 标签(Stage 1 过滤);部分行全大写(Stage 4 修复) |
55
+ | **优势** | 对话多样性好,口语表达丰富 |
56
+ | **劣势** | 噪声较多(词语黏连如 `wasreleased`);全大写行需修复 |
57
+ | **目标任务** | BLiMP Supplement(Turn Taking 对话轮换) |
58
+ | **引用** | Lison, P. & Tiedemann, J. (2016). OpenSubtitles2016: Extracting Large Parallel Corpora from Movie and TV Subtitles. *LREC 2016*. |
59
+
60
+ ### 4. Simple Wikipedia
61
+
62
+ | 项目 | 内容 |
63
+ |------|------|
64
+ | **全名** | Simple English Wikipedia |
65
+ | **行数 / 词数** | 642,735 行 / 15.3M 词 |
66
+ | **来源** | 维基媒体基金会 |
67
+ | **内容** | 用简化英语写的维基百科,基础词汇和较短句子 |
68
+ | **许可** | CC BY-SA 3.0 / GFDL |
69
+ | **链接** | https://simple.wikipedia.org/ |
70
+ | **特点** | 百科知识密集(平均 23.8 词/行);含大量外语专有名词;类型覆盖率最低(19.5%) |
71
+ | **优势** | 世界知识丰富,EWoK 和 COMPS 覆盖好 |
72
+ | **劣势** | 专有名词多(非英语地名等);部分词语黏连 |
73
+ | **目标任务** | EWoK(世界知识)、COMPS(概念属性) |
74
+ | **引用** | https://simple.wikipedia.org/ |
75
+
76
+ ### 5. BNC Spoken
77
+
78
+ | 项目 | 内容 |
79
+ |------|------|
80
+ | **全名** | British National Corpus — Spoken Component |
81
+ | **行数 / 词数** | 803,341 行 / 7.6M 词 |
82
+ | **来源** | 牛津大学出版社等机构联合项目 |
83
+ | **内容** | 英国真实口语转写,含正式和非正式场合对话 |
84
+ | **许可** | 需注册接受许可协议 |
85
+ | **链接** | http://www.natcorp.ox.ac.uk/ |
86
+ | **特点** | 真实英式口语(平均 9.5 词/行);含疑问句、倒装等句法变体 |
87
+ | **优势** | 自然口语数据,BLiMP Supplement(疑问句/倒装)和 GLUE 有价值 |
88
+ | **劣势** | 不流利标记较多;keep 率较低(73%) |
89
+ | **目标任务** | BLiMP Supplement(Subject-Aux Inversion)、GLUE、Reading Time |
90
+ | **引用** | BNC Consortium (2007). *The British National Corpus, XML Edition*. Oxford Text Archive. |
91
+
92
+ ### 6. Open Subtitles Cleaned
93
+
94
+ | 项目 | 内容 |
95
+ |------|------|
96
+ | **行数 / 词数** | 49,920 行 / 0.3M 词 |
97
+ | **内容** | Open Subtitles 的 LLM 过滤版本 |
98
+ | **特点** | 数据量极小,含 CHAT 格式标记(Stage 4 清洗) |
99
+
100
+ ### 7. Switchboard
101
+
102
+ | 项目 | 内容 |
103
+ |------|------|
104
+ | **全名** | Switchboard Telephone Speech Corpus |
105
+ | **行数 / 词数** | 30,559 行 / 0.25M 词 |
106
+ | **来��** | Texas Instruments,1990-91 年采集,DARPA 资助 |
107
+ | **内容** | 543 位说话者的约 2,400 段电话对话 |
108
+ | **许可** | LDC User Agreement |
109
+ | **链接** | https://catalog.ldc.upenn.edu/LDC97S62 |
110
+ | **特点** | 数据量极小(0.25M 词);含说话人前缀 `A:`/`B:`(Stage 4 去除) |
111
+ | **引用** | Godfrey, J., Holliman, E., & McDaniel, J. (1992). SWITCHBOARD: Telephone speech corpus for research and development. *ICASSP 1992*. |
112
+
113
+ ---
114
+
115
+ ## 二、外部数据
116
+
117
+ 已下载并接入 Stage 1-4 流水线的外部数据源,存放于 `data/external/`,以 `ext_` 前缀复制到 `data/raw_files/`。
118
+
119
+ > 比赛规则已确认(2026-03-14):所有数据都可以用,唯一约束是总训练数据 ≤ 10M 词。
120
+
121
+ ### 1. FineWeb-Edu
122
+
123
+ | 项目 | 内容 |
124
+ |------|------|
125
+ | **行数 / 词数** | 85,367 行 / 3.0M 词(子集) |
126
+ | **来源** | HuggingFace FW 团队 |
127
+ | **内容** | 从 CommonCrawl 筛选的高质量教育文本,用 Llama3-70B-Instruct 训练的分类器按教育质量 0-5 打分,阈值 3 以上保留 |
128
+ | **许可** | ODC-By v1.0 |
129
+ | **链接** | https://huggingface.co/datasets/HuggingFaceFW/fineweb-edu |
130
+ | **优势** | 教育内容质量高,知识密度大 |
131
+ | **劣势** | 全集 1.3T tokens,需选子集 |
132
+ | **目标任务** | GLUE(NLU 任务,特别是 MNLI) |
133
+ | **为什么有用** | **实验证据**:Charpentier 2024 GPT-BERT 使用 1/3 FineWeb-Edu,消融实验显示它对 GLUE 提升贡献最大(Charpentier & Samuel, 2024) |
134
+ | **引用** | Penedo, G. et al. (2024). The FineWeb Datasets: Decanting the Web for the Finest Text Data at Scale. arXiv:2406.17557. |
135
+ | **文件** | `ext_fineweb_edu.txt` |
136
+
137
+ ### 2. Cosmopedia
138
+
139
+ | 项目 | 内容 |
140
+ |------|------|
141
+ | **行数 / 词数** | 84,110 行 / 3.0M 词(子集) |
142
+ | **来源** | HuggingFace TB 团队 |
143
+ | **内容** | Mixtral-8x7B-Instruct-v0.1 生成的合成教科书、博客、故事等 |
144
+ | **许可** | Apache 2.0(v1)/ ODC-By(v2) |
145
+ | **链接** | https://huggingface.co/datasets/HuggingFaceTB/cosmopedia |
146
+ | **优势** | 教科书风格,结构化知识,内容多样 |
147
+ | **劣势** | LLM 生成文本,可能缺乏自然语言的某些特征 |
148
+ | **目标任务** | EWoK(世界知识) |
149
+ | **为什么有用** | **实验证据**:Charpentier 2024 GPT-BERT 使用 1/3 Cosmopedia,消融实验显示它对 EWoK 提升贡献最大(Charpentier & Samuel, 2024) |
150
+ | **引用** | Ben Allal, L. et al. (2024). Cosmopedia: how to create large-scale synthetic data for pre-training. HuggingFace Blog. |
151
+ | **文件** | `ext_cosmopedia.txt` |
152
+
153
+ ### 3. TinyDialogues
154
+
155
+ | 项目 | 内容 |
156
+ |------|------|
157
+ | **行数 / 词数** | 18,282 行 / 3.0M 词 |
158
+ | **来源** | Steven Y. Feng, Noah D. Goodman, Michael C. Frank(Stanford University) |
159
+ | **内容** | GPT-4 Turbo 生成的模拟儿童-照顾者对话,按年龄分层(2/5/10/15 岁),含多种对话类型和参与者角色 |
160
+ | **许可** | 见 HuggingFace 页面 |
161
+ | **链接** | https://huggingface.co/datasets/styfeng/TinyDialogues |
162
+ | **优势** | 比原始 CHILDES 更干净、结构更好;按年龄分层可做 curriculum |
163
+ | **劣势** | GPT-4 生成,可能缺少真实儿童语言的某些不规则特征 |
164
+ | **目标任务** | AoA(词汇习得顺序) |
165
+ | **为什么有用** | **实验证据**:Feng et al. (2024) 发现合成 CDS 数据优于自然 CDS 数据,局部对话连贯性(local discourse coherence)是关键因素 |
166
+ | **引用** | Feng, S.Y., Goodman, N.D., & Frank, M.C. (2024). Is Child-Directed Speech Effective Training Data for Language Models? *EMNLP 2024*. |
167
+ | **文件** | `ext_tiny_dialogues.txt` |
168
+
169
+ ### 4. OneStopEnglish
170
+
171
+ | 项目 | 内容 |
172
+ |------|------|
173
+ | **行数 / 词数** | 7,201 行 / 0.38M 词 |
174
+ | **来源** | Sowmya Vajjala & Ivana Lucic |
175
+ | **内容** | 189 篇新闻文章 × 3 个难度级别(Elementary/Intermediate/Advanced),共 567 篇 |
176
+ | **许可** | CC BY-SA 4.0 |
177
+ | **链接** | https://github.com/nishkalavallabhi/OneStopEnglishCorpus |
178
+ | **优势** | 同一内容的多难度版本,语言多样性好 |
179
+ | **劣势** | 数据量小(<0.4M 词) |
180
+ | **目标任务** | Reading Time(阅读流畅度) |
181
+ | **为什么有用** | **理论推断**:多难度级别的相同内容提供了丰富的句法多样性,有助于模型学习不同复杂度的表达方式 |
182
+ | **引用** | Vajjala, S. & Lucic, I. (2018). OneStopEnglish corpus: A new corpus for automatic readability assessment and text simplification. *BEA Workshop, NAACL 2018*. |
183
+ | **文件** | `ext_onestop_english.txt` |
184
+
185
+ ### 5. GenericsKB-Best
186
+
187
+ | 项目 | 内容 |
188
+ |------|------|
189
+ | **行数 / 词数** | 1,020,569 行 / 8.5M 词 |
190
+ | **来源** | Allen Institute for AI (Sumithra Bhakthavatsalam, Chloe Anastasiades, Peter Clark) |
191
+ | **内容** | 从 Waterloo 语料库(.edu 域名)、SimpleWikipedia、ARC 语料库提取的常识性通用陈述,如 "Trees remove carbon dioxide from the atmosphere" |
192
+ | **许可** | CC BY 4.0 |
193
+ | **链接** | https://huggingface.co/datasets/community-datasets/generics_kb |
194
+ | **优势** | 直接包含 EWoK 测试的常识知识类型(物质属性、空间关系、智能体特征等);解决"报告偏差"问题(常识在普通文本中很少被显式说出来) |
195
+ | **劣势** | 句式较单一(陈述句为主);部分来自 WordNet/ConceptNet 合成,可能不够自然 |
196
+ | **目标任务** | EWoK(世界知识)、COMPS(概念属性继承) |
197
+ | **为什么有用** | **理论推断**:EWoK 作者指出所有测试模型都低于人类水平,物理/空间域最弱——这些恰好是 GenericsKB 覆盖的领域。通用知识(generics)是人类常识的核心,但因"报告偏差"在普通训练文本中严重不足。知识增强预训练已被证明能提升 LM 的事实推理能力(Guan et al., 2020, TACL) |
198
+ | **引用** | Bhakthavatsalam, S., Anastasiades, C., & Clark, P. (2020). GenericsKB: A Knowledge Base of Generic Statements. arXiv:2005.00660. |
199
+ | **文件** | `ext_generics_kb.txt` |
200
+
201
+ ### 6. ConceptNet 5.7
202
+
203
+ | 项目 | 内容 |
204
+ |------|------|
205
+ | **行数 / 词数** | 393,557 行 / 2.7M 词(英语子集,12 种关系转自然语言) |
206
+ | **来源** | Robyn Speer, Joshua Chin, Catherine Havasi (Luminoso) |
207
+ | **内容** | 多语言常识知识图谱,从中提取英语三元组并转为自然语言句子(如 HasProperty → "Ice is cold."、CapableOf → "Bird can fly.") |
208
+ | **许可** | CC BY-SA 4.0 |
209
+ | **链接** | https://conceptnet.io/ / https://github.com/commonsense/conceptnet5 |
210
+ | **优势** | 覆盖 12 种语义关系(HasProperty / UsedFor / MadeOf / CapableOf / IsA / PartOf / HasA / AtLocation / Causes / HasPrerequisite / MotivatedByGoal / CausesDesire) |
211
+ | **劣势** | 从模板生成,句式机械;部分条目有噪声(如 "00t shirts"),需经 Stage 1-2 过滤 |
212
+ | **目标任务** | EWoK(世界知识)、COMPS(概念属性) |
213
+ | **为什么有用** | **理论推断**:ConceptNet 覆盖了 COMPS 测试的核心概念(属性继承:"A sock absorbs sweat" 对应 HasProperty 关系),以及 EWoK 的物质属性和物理交互域。知识增强预训练已有成功先例(Guan et al., 2020) |
214
+ | **引用** | Speer, R., Chin, J., & Havasi, C. (2017). ConceptNet 5.5: An Open Multilingual Graph of General Knowledge. *AAAI 2017*. arXiv:1612.03975. |
215
+ | **文件** | `ext_conceptnet.txt` |
216
+
217
+ ### 7. SNLI(仅前提句)
218
+
219
+ | 项目 | 内容 |
220
+ |------|------|
221
+ | **行数 / 词数** | 150,715 行 / 1.84M 词(去重后的唯一前提句) |
222
+ | **来源** | Samuel R. Bowman, Gabor Angeli, Christopher Potts, Christopher D. Manning(Stanford NLP Group) |
223
+ | **内容** | 从 Flickr30K 图片描述中提取的前提句,描述性强、语法规范、内容多样(人物活动、场景描述) |
224
+ | **许可** | CC BY-SA 4.0 |
225
+ | **链接** | https://nlp.stanford.edu/projects/snli/ / https://huggingface.co/datasets/stanfordnlp/snli |
226
+ | **原始数据** | 570K 句对(premise + hypothesis),标注为 entailment / contradiction / neutral。我们只使用去重后的前提句 |
227
+ | **优势** | 高质量图片描述句,覆盖多样的日常场景;SNLI 是 MNLI 的前身,训练数据同源有助于 GLUE 微调 |
228
+ | **劣势** | 图片描述句式相对固定(多为"某人在做某事");只用前提句丢失了句对关系信息 |
229
+ | **目标任务** | GLUE(MNLI、RTE — 文本蕴含任务) |
230
+ | **为什么有用** | **实验证据(间接)**:Phang et al. (2018, STILTs) 证明在 SNLI/MNLI 上做中间任务**微调**可将 BERT 的 GLUE 分数提升 +1.5 分。注意:该结论基于微调阶段,而非预训练阶段。我们将 SNLI 前提句用于预训练,预期效果是让模型提前接触 NLI 风格的描述性句子,有助于后续 GLUE 微调。GLUE 的 4/7 个子任务(MNLI、RTE、QQP、WSC)与 NLI 相关 |
231
+ | **引用** | Bowman, S.R., Angeli, G., Potts, C., & Manning, C.D. (2015). A large annotated corpus for learning natural language inference. *EMNLP 2015*. |
232
+ | **文件** | `ext_snli.txt` |
233
+
234
+ ---
235
+
236
+ ## 三、评测数据(用于训练)
237
+
238
+ 比赛规则允许将评测数据中的正确句子加入训练集。由 `stage7a_extract_eval.py` 提取,存放于 `data/7_eval_sentences/`,在 Stage 8 采样时自动追加到训练集末尾。
239
+
240
+ ### 1. BLiMP Good Sentences
241
+
242
+ | 项目 | 内容 |
243
+ |------|------|
244
+ | **行数 / 词数** | 58,161 行 / 420K 词 |
245
+ | **来源** | BLiMP 评测数据的 `sentence_good` 字段 |
246
+ | **内容** | 65 个语法子任务的正确句(如 "Katherine can't help herself.") |
247
+ | **不提取** | `sentence_bad`(人工构造的语法错误句,加入会污染训练) |
248
+ | **链接** | https://github.com/alexwarstadt/blimp |
249
+ | **引用** | Warstadt, A. et al. (2020). BLiMP: The Benchmark of Linguistic Minimal Pairs for English. *TACL 2020*. |
250
+ | **文件** | `blimp_good.txt` |
251
+
252
+ ### 2. BLiMP Supplement Good Sentences
253
+
254
+ | 项目 | 内容 |
255
+ |------|------|
256
+ | **行数 / 词数** | 5,121 行 / 69K 词 |
257
+ | **内容** | 5 个补充任务(subject_aux_inversion、hypernym、turn_taking、qa_congruence_easy/tricky)的正确句 |
258
+ | **文件** | `blimp_supplement_good.txt` |
259
+
260
+ ### 3. EWoK Context + Target
261
+
262
+ | 项目 | 内容 |
263
+ |------|------|
264
+ | **行数 / 词数** | 4,865 行 / 69K 词 |
265
+ | **来源** | EWoK 评测数据的 Context1 + Target1 拼接 |
266
+ | **内容** | 11 个知识域的正确上下文-推理配对 |
267
+ | **不提取** | Context2/Target2(错误推断) |
268
+ | **链接** | https://ewok-core.github.io/ |
269
+ | **引用** | Ivanova, A.A. et al. (2024). Elements of World Knowledge (EWoK). *TACL*. arXiv:2405.09605. |
270
+ | **文件** | `ewok_context_target.txt` |
271
+
272
+ ### 4. COMPS Sentences
273
+
274
+ | 项目 | 内容 |
275
+ |------|------|
276
+ | **行数 / 词数** | 12,335 行 / 67K 词 |
277
+ | **来源** | COMPS 评测数据的 `comps_base.jsonl`(仅 base,不含 wugs 虚构词) |
278
+ | **内容** | 正确的概念-属性陈述(如 "A sock absorbs sweat.") |
279
+ | **不提取** | `prefix_unacceptable`(错误归属)、`comps_wugs*.jsonl`(含虚构词) |
280
+ | **链接** | https://huggingface.co/datasets/kanishka/comps |
281
+ | **引用** | Misra, K., Rayz, J., & Ettinger, A. (2023). COMPS: Conceptual Minimal Pair Sentences. *EACL 2023*. |
282
+ | **文件** | `comps_sentences.txt` |
283
+
284
+ ### 5. Reading Time Sentences
285
+
286
+ | 项目 | 内容 |
287
+ |------|------|
288
+ | **行数 / 词数** | 205 行 / 1.9K 词 |
289
+ | **内容** | 205 个英语句子(去重后),用于 surprisal 与人类阅读时间的回归分析 |
290
+ | **文件** | `reading_time_sentences.txt` |
291
+
292
+ ### 6. AoA Context Sentences
293
+
294
+ | 项目 | 内容 |
295
+ |------|------|
296
+ | **行数 / 词数** | 7,994 行 / 116K 词 |
297
+ | **来源** | `cdi_childes.json`,504 个 CDI 目标词的上下文句子(去重后) |
298
+ | **内容** | 真实语料中含目标词的自然语境句 |
299
+ | **链接** | https://wordbank.stanford.edu/ |
300
+ | **引用** | Frank, M.C. et al. (2017). Wordbank: An open repository for developmental vocabulary data. *Journal of Child Language*. |
301
+ | **文件** | `aoa_context_sentences.txt` |
302
+
303
+ ### 7. Entity Tracking Sentences
304
+
305
+ | 项目 | 内容 |
306
+ |------|------|
307
+ | **行数 / 词数** | 5,488 行 / 620K 词 |
308
+ | **内容** | "箱子游戏"场景描述(去掉末尾不完整问句),合成文本 |
309
+ | **注意** | 文体非常人工化,采样时需控制比例 |
310
+ | **文件** | `entity_tracking_sentences.txt` |
311
+
312
+ ---
313
+
314
+ ## 四、数据集与评测任务对应关系
315
+
316
+ | 评测任务 | 排行榜列数 | 最有价值的数据来源 | 证据类型 |
317
+ |---------|:--------:|----------------|---------|
318
+ | **BLiMP** | 1 | 官方 BabyLM 数据(gutenberg 最佳) | 实验:Charpentier & Samuel 2024 消融实验 |
319
+ | **BLiMP Supplement** | 1 | bnc_spoken(疑问句/倒装)、open_subtitles(对话轮换) | 理论推断 |
320
+ | **EWoK** | 1 | Cosmopedia、GenericsKB、ConceptNet | 实验(Cosmopedia)+ 理论推断(GenericsKB/ConceptNet) |
321
+ | **Entity Tracking** | 1 | entity_tracking_sentences(评测原文) | 直接包含 |
322
+ | **WUG ADJ** | 1 | gutenberg(不规则形态丰富) | 理论推断 |
323
+ | **WUG PAST** | 1 | gutenberg(不规则动词丰富) | 理论推断 |
324
+ | **COMPS** | 1 | GenericsKB、ConceptNet、Cosmopedia | 理论推断 |
325
+ | **Reading Time** | 2 | gutenberg、OneStopEnglish(连贯散文) | 理论推断 |
326
+ | **AoA** | 1 | CHILDES、TinyDialogues | 实验(TinyDialogues,Feng et al. 2024) |
327
+ | **GLUE** | 1 | FineWeb-Edu、SNLI | 实验(FineWeb-Edu: Charpentier 2024; SNLI: Phang et al. 2018 STILTs) |
328
+
329
+ **证据类型说明**:
330
+ - **实验**:有发表的实验结果直接支持该数据集对该任务的提升
331
+ - **理论推断**:基于数据集内容与评测任务的语义对应关系推断,无直接实验证据
332
+ - **直接包含**:评测数据本身被提取用于训练
333
+
334
+ ### 各数据集对各评测任务的预估贡献度
335
+
336
+ > 贡献度评级:★★★ 高、★★☆ 中、★☆☆ 低、— 无关
337
+ > 带 ✦ 标记表示有实验证据支持,其余为理论推断
338
+
339
+ | 数据集 | BLiMP | BLiMP Supp | EWoK | Entity Track | WUG | COMPS | Reading | AoA | GLUE |
340
+ |--------|:-----:|:----------:|:----:|:------------:|:---:|:-----:|:-------:|:---:|:----:|
341
+ | **官方数据** | | | | | | | | | |
342
+ | gutenberg | ★★★✦ | ★★☆ | ★☆☆ | — | ★★★ | ★☆☆ | ★★★ | ★☆☆ | ★★☆ |
343
+ | simple_wiki | ★★☆ | ★☆☆ | ★★☆ | — | ★☆☆ | ★★☆ | ★★☆ | ★☆☆ | ★★☆ |
344
+ | childes | ★☆☆ | ★☆☆ | — | — | ★☆☆ | — | — | ★★★ | ★☆☆ |
345
+ | bnc_spoken | ★★☆ | ★★★ | ★☆☆ | — | ★☆☆ | — | ★★☆ | ★☆☆ | ★★☆ |
346
+ | open_subtitles | ★☆☆ | ★★☆ | — | — | ★☆☆ | — | ★☆☆ | ★☆☆ | ★☆☆ |
347
+ | switchboard | ★☆☆ | ★☆☆ | — | — | — | — | — | ★☆☆ | ★☆☆ |
348
+ | **外部数据** | | | | | | | | | |
349
+ | FineWeb-Edu | ★★☆ | ★☆☆ | ★★☆ | — | ★☆☆ | ★☆☆ | ★★☆ | — | ★★★✦ |
350
+ | Cosmopedia | ★★☆ | ★☆☆ | ★★★✦ | — | ★☆☆ | ★★☆ | ★★☆ | — | ★��☆ |
351
+ | TinyDialogues | ★☆☆ | ★★☆ | — | — | ★☆☆ | — | — | ★★★✦ | ★☆☆ |
352
+ | OneStopEnglish | ★★☆ | ★☆☆ | ★☆☆ | — | ★☆☆ | — | ★★☆ | — | ★★☆ |
353
+ | GenericsKB | ★☆☆ | ★☆☆ | ★★★ | — | — | ★★★ | ★☆☆ | — | ★☆☆ |
354
+ | ConceptNet | ★☆☆ | — | ★★☆ | — | — | ★★☆ | — | — | ★☆☆ |
355
+ | SNLI | ★☆☆ | ★☆☆ | ★☆☆ | — | — | — | ★☆☆ | — | ★★☆✦ |
356
+ | **评测数据** | | | | | | | | | |
357
+ | blimp_good | ★★★ | — | — | — | — | — | — | — | — |
358
+ | blimp_supp_good | — | ★★★ | — | — | — | — | — | — | — |
359
+ | ewok_context | — | — | ★★★ | — | — | — | — | — | — |
360
+ | comps_sentences | — | — | — | — | — | ★★★ | — | — | — |
361
+ | entity_tracking | — | — | — | ★★★ | — | — | — | — | — |
362
+ | aoa_context | — | — | — | — | — | — | — | ★★☆ | — |
363
+ | reading_time | — | — | — | — | — | — | ★★☆ | — | — |
364
+
365
+ **✦ 有实验证据的 4 个关键发现**:
366
+ 1. **gutenberg → BLiMP**:Charpentier 2024 消融实验显示官方 BabyLM 数据对 BLiMP 贡献最大,gutenberg 句法最丰富
367
+ 2. **FineWeb-Edu → GLUE**:Charpentier 2024 消融实验直接显示 FineWeb-Edu 对 GLUE 提升最大
368
+ 3. **Cosmopedia → EWoK**:Charpentier 2024 消融实验直接显示 Cosmopedia 对 EWoK 提升最大
369
+ 4. **TinyDialogues → AoA**:Feng et al. (2024) 实验显示合成 CDS 优于自然 CDS
370
+ 5. **SNLI → GLUE**(间接):Phang et al. (2018) 在微调阶段验证,预训练阶段效果待验证
371
+
372
+ ---
373
+
374
+ ## 五、参考方案
375
+
376
+ ### Charpentier 2024 数据配比(论文参考)
377
+
378
+ GPT-BERT(Charpentier & Samuel, 2024)在 2024 BabyLM Strict-Small 和 Strict 赛道均获冠军。
379
+
380
+ **数据配比**:1:1:1 等比混合
381
+ ```
382
+ 官方 BabyLM 数据: ~3.3M 词 (33%) — BLiMP 最佳来源
383
+ FineWeb-Edu: ~3.3M 词 (33%) — GLUE 最佳来源
384
+ Cosmopedia: ~3.4M 词 (34%) — EWoK 最佳来源
385
+ ```
386
+
387
+ **关键消融发现**:每个语料在不同任务上各有所长,混合使用效果最好。
388
+
389
+ > 注意:2024 成绩(BLiMP=81.2, GLUE=76.5)基于 2024 评测体系,与 2025 不可直接比较。
390
+ > 2025 Strict-Small 基线成绩见 `ref_leaderboard_2025.md`。
391
+
392
+ **引用**:Charpentier, L. & Samuel, D. (2024). GPT or BERT: why not both? *CoNLL BabyLM 2024*. arXiv:2410.24159.
393
+
394
+ ---
395
+
396
+ ## 六、数据量总览
397
+
398
+ ### 原始数据
399
+
400
+ | 类别 | 文件数 | 总行数 | 总词数 |
401
+ |------|:-----:|-------:|-------:|
402
+ | 官方 BabyLM | 7 | 11,160,884 | 96.7M |
403
+ | 外部数据 | 7 | 1,759,801 | 22.4M |
404
+ | **合计** | **14** | **12,920,685** | **119.1M** |
405
+
406
+ ### 评测句(追加到训练集)
407
+
408
+ | 文件数 | 总行数 | 总词数 |
409
+ |:-----:|-------:|-------:|
410
+ | 7 | 94,169 | 1.36M |
411
+
412
+ ### 最终训练数据预算
413
+
414
+ ```
415
+ 总预算: ≤ 10M 词
416
+ 原文: ~8M 词(Stage 8 采样)
417
+ 评测句: ~1.36M 词(Stage 7A 提取,自动追加)
418
+ Paraphrase: ~0.6M 词(Stage 9 可选,填充剩余预算)
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 七、参考文献
424
+
425
+ - [BabyLM 2024 Findings](https://arxiv.org/html/2412.05149v1)
426
+ - [BabyLM 2025 Findings](https://aclanthology.org/2025.babylm-main.28/)
427
+ - [GPT-BERT: Why Not Both?](https://arxiv.org/abs/2410.24159) — Charpentier & Samuel, 2024
428
+ - [Are BabyLMs Second Language Learners?](https://aclanthology.org/2024.conll-babylm.14/) — Paraphrase 增强
429
+ - [Variation Sets](https://arxiv.org/abs/2411.09587) — 2024 Outstanding Paper
430
+ - [What Should Baby Models Read?](https://aclanthology.org/2024.conll-babylm.25/) — 数据消融实验
431
+ - [Is CDS Effective Training Data?](https://arxiv.org/abs/2408.03617) — TinyDialogues
432
+ - [STILTs](https://arxiv.org/abs/1811.01088) — SNLI 中间任务训练
433
+ - [GenericsKB](https://arxiv.org/abs/2005.00660)
434
+ - [ConceptNet 5.5](https://arxiv.org/abs/1612.03975)
435
+ - [FineWeb-Edu](https://arxiv.org/abs/2406.17557)
436
+ - [BLiMP](https://aclanthology.org/2020.tacl-1.25/)
437
+ - [EWoK](https://arxiv.org/abs/2405.09605)
438
+ - [COMPS](https://aclanthology.org/2023.eacl-main.213/)
439
+ - [BabyLM 2026 CFP](https://arxiv.org/abs/2602.20092)
440
+ - [BabyLM 2026 Guidelines](https://babylm.github.io/guidelines.html)
notes/ref_leaderboard_2025.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BabyLM 2025 Leaderboard 参考数据
2
+
3
+ > 数据来源:`babylm_leaderboard_2025.csv`(从官方 HuggingFace leaderboard 摘录)
4
+ > 更新日期:2026-03-23
5
+
6
+ ---
7
+
8
+ ## Strict-Small Track(10M 词,我们参加的赛道)
9
+
10
+ **关键发现**:前 3 名(amlm_hd_fail, MoEP, amlm_hard_nhot)超过了基线,说明优化空间存在。
11
+
12
+ | 排名 | 模型 | TextAvg | BLiMP | Supp | EWoK | ET | WUG_ADJ | WUG_PAST | COMPS | AoA | GLUE |
13
+ |:----:|------|:-------:|:-----:|:----:|:----:|:--:|:-------:|:--------:|:-----:|:---:|:----:|
14
+ | 1 | **amlm_hd_fail** | **45.4** | 59.0 | 51.7 | 56.0 | 41.3 | 78.1 | -12.1 | 82.2 | 34.2 | 57.7 |
15
+ | 2 | **MoEP** | **44.5** | 60.8 | 57.5 | 50.2 | 35.6 | 66.8 | -1.1 | 50.7 | 53.7 | 64.2 |
16
+ | 3 | **amlm_hard_nhot** | **41.9** | 65.6 | 54.8 | 49.7 | 43.7 | 64.1 | 0.7 | 51.3 | 16.3 | 67.4 |
17
+ | 4 | Baseline-gpt-bert (causal) | 41.2 | 71.7 | 63.2 | 49.5 | 34.6 | 59.2 | 12.9 | 52.8 | -3.9 | 65.1 |
18
+ | 5 | Baseline-gpt-bert (mntp) | 41.0 | 69.1 | 64.3 | 49.6 | 39.2 | 46.2 | 11.2 | 53.0 | 5.9 | 65.1 |
19
+ | 6 | Baseline-masked-focus (causal) | 40.0 | 65.2 | 59.5 | 49.5 | 31.1 | 44.7 | 20.7 | 52.9 | 3.8 | 66.0 |
20
+ | 7 | Baseline-mixed (mntp) | 39.8 | 71.3 | 63.3 | 49.9 | 40.0 | 18.4 | 12.3 | 53.7 | 16.4 | 65.7 |
21
+ | 8 | babylm-rta | 39.3 | 61.9 | 61.1 | 50.8 | 16.5 | 56.5 | 24.0 | 50.8 | 5.4 | 57.7 |
22
+ | 9 | amlm_hard_decay | 38.3 | 71.4 | 59.2 | 51.0 | 44.2 | 22.3 | 6.2 | 54.2 | -0.9 | 69.8 |
23
+ | 10 | Baseline-masked-focus (mntp) | 38.2 | 70.4 | 63.7 | 50.0 | 40.1 | 2.7 | 28.7 | 53.6 | 0.3 | 66.0 |
24
+ | — | Baseline-gpt2 | 37.4 | 66.4 | 57.1 | 49.9 | 13.9 | 66.1 | -5.0 | 51.7 | 11.7 | 55.9 |
25
+
26
+ ### 关键洞察
27
+
28
+ 1. **TextAvg 冠军 ≠ BLiMP 最高**:amlm_hd_fail 的 TextAvg 最高(45.4),但 BLiMP 只有 59.0;而基线的 BLiMP 高达 71.7。TextAvg 更看重各任务的均衡表现。
29
+ 2. **WUG_ADJ 是最大分化点**:amlm_hd_fail 靠 WUG_ADJ=78.1 和 COMPS=82.2 拉高了 TextAvg,说明形态学和概念理解是拉分关键。
30
+ 3. **GLUE 差异大**:amlm_hard_decay 的 GLUE=69.8 是所有模型中最高的(基线 65-66),说明微调能力有优化空间。
31
+ 4. **Entity Tracking**:amlm_hard_decay 的 ET=44.2 最高,amlm_hard_nhot 的 ET=43.7 紧随其后。
32
+
33
+ ---
34
+
35
+ ## Strict Track(100M 词)
36
+
37
+ **关键发现**:**前 6 名全是基线模型**,没有任何提交超过基线。
38
+
39
+ | 排名 | 模型 | TextAvg | BLiMP | GLUE | EWoK | ET |
40
+ |:----:|------|:-------:|:-----:|:----:|:----:|:--:|
41
+ | 1 | Baseline-mixed (mntp) | **47.3** | 80.5 | 70.9 | 52.4 | 39.9 |
42
+ | 2 | Baseline-causal-focus (causal) | 46.7 | 79.3 | 70.1 | 52.3 | 30.9 |
43
+ | 3 | Baseline-causal-focus (mntp) | 46.2 | 78.1 | 70.1 | 53.5 | 41.9 |
44
+ | 7 | blalm-100m(最高提交) | 42.1 | 74.7 | 56.7 | 53.6 | 22.2 |
45
+
46
+ ---
47
+
48
+ ## 对我们的启示
49
+
50
+ 1. **赛道选择正确**:Strict-Small 有优化空间(冠军超基线 4pp),Strict 赛道基线太强
51
+ 2. **形态学是关键**:WUG_ADJ 差异最大(2.7 ~ 78.1),N-hot 嵌入(C1)值得重点投入
52
+ 3. **AMLM + N-hot + MoE 组合**:前 3 名分别使用了 AMLM、MoE、AMLM+N-hot,验证了 encyclopedia 中 E1/C1/D5 的价值
53
+ 4. **GLUE 有空间**:最高 69.8 vs 基线 65-66,训练目标和数据策略对微调有帮助
54
+ 5. **GPT-2 基线弱**:TextAvg 37.4,BLiMP 66.4,作为 D0 基线可以清楚看到各改进的提升
requirements.txt ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BabyLM Challenge 2026 - Training Environment
2
+ # Python 3.10+
3
+ #
4
+ # Install PyTorch first (adjust CUDA version):
5
+ # pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
6
+ #
7
+ # Then install the rest:
8
+ # pip install -r requirements.txt
9
+
10
+ # HuggingFace stack
11
+ transformers==4.45.0
12
+ tokenizers==0.20.3
13
+ datasets==3.6.0
14
+ safetensors==0.5.3
15
+ huggingface-hub==0.33.2
16
+ accelerate==1.7.0
17
+
18
+ # Training utilities
19
+ wandb==0.19.11
20
+ einops==0.3.2
21
+
22
+ # Tokenizer
23
+ morfessor==2.0.6
24
+ torch-optimizer==0.3.0
25
+
26
+ # Evaluation / analysis
27
+ scipy==1.13.1
28
+ scikit-learn==1.6.1
29
+ statsmodels==0.14.5
30
+ polars==1.37.1
31
+ pandas==2.2.3
32
+ numpy==1.23.0
33
+ minicons==0.3.36
34
+ nltk==3.9.1
35
+ pyspellchecker==0.9.0
36
+
37
+ # General
38
+ tqdm==4.67.1
39
+ pyyaml==6.0.1
40
+ regex==2024.11.6
scripts/01_data/analyze_blimp_coverage.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BLiMP + BLiMP Supplement 覆盖率分析
3
+
4
+ 对每个子任务:
5
+ 1. 提取 good 句中的"判别性关键词"(good有但bad没有的词)
6
+ 2. 统计这些词在训练数据(3_dedup)里的总频次
7
+ 3. 输出每个子任务的覆盖得分,找出弱点
8
+ """
9
+
10
+ import json, re
11
+ from pathlib import Path
12
+ from collections import Counter, defaultdict
13
+
14
+ ROOT = Path(__file__).parent.parent.parent
15
+ BLIMP_DIR = ROOT / "evaluation-pipeline-2025/evaluation_data/full_eval/blimp_filtered"
16
+ SUPP_DIR = ROOT / "evaluation-pipeline-2025/evaluation_data/full_eval/supplement_filtered"
17
+ TRAIN_DIR = ROOT / "data/3_dedup"
18
+ OUT_DIR = ROOT / "results/analysis"
19
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
20
+
21
+ TRAIN_FILES = [
22
+ "gutenberg.train.txt",
23
+ "simple_wiki.train.txt",
24
+ "bnc_spoken.train.txt",
25
+ "switchboard.train.txt",
26
+ "open_subtitles_cleaned_ckpt_70.txt",
27
+ "childes.train.txt",
28
+ "open_subtitles.train.txt",
29
+ ]
30
+
31
+ def tokenize(text):
32
+ return re.findall(r"[a-z']+", text.lower())
33
+
34
+ # ── 1. 统计训练数据词频 ───────────────────────────────────
35
+ print("统计训练数据词频...")
36
+ train_freq = Counter()
37
+ train_total = 0
38
+ for fname in TRAIN_FILES:
39
+ path = TRAIN_DIR / fname
40
+ if not path.exists():
41
+ continue
42
+ with open(path, errors="ignore") as f:
43
+ for line in f:
44
+ words = tokenize(line)
45
+ train_total += len(words)
46
+ train_freq.update(words)
47
+ print(f" 训练数据总词数: {train_total:,}")
48
+
49
+ # ── 2. 分析每个BLiMP子任务 ───────────────────────────────
50
+ def analyze_task(jfile):
51
+ good_words = Counter()
52
+ bad_words = Counter()
53
+ n_pairs = 0
54
+ with open(jfile) as f:
55
+ for line in f:
56
+ d = json.loads(line)
57
+ good_words.update(tokenize(d["sentence_good"]))
58
+ bad_words.update(tokenize(d["sentence_bad"]))
59
+ n_pairs += 1
60
+
61
+ # 判别性关键词:good句里有,bad句里没有(或少很多)
62
+ discriminative = {}
63
+ for w, gc in good_words.items():
64
+ bc = bad_words.get(w, 0)
65
+ if gc - bc >= max(3, gc * 0.3): # good比bad多30%以上且至少差3次
66
+ discriminative[w] = gc - bc
67
+
68
+ # 这些词在训练数据里的频次(每百万词)
69
+ if not discriminative:
70
+ return None
71
+
72
+ top_words = sorted(discriminative.items(), key=lambda x: -x[1])[:10]
73
+ coverage_scores = []
74
+ for w, diff in top_words:
75
+ per_million = train_freq.get(w, 0) / train_total * 1e6
76
+ coverage_scores.append((w, diff, per_million))
77
+
78
+ # 子任务覆盖得分 = 关键词在训练数据中的平均频次(每百万词)
79
+ avg_coverage = sum(s for _, _, s in coverage_scores) / len(coverage_scores)
80
+
81
+ return {
82
+ "task": jfile.stem,
83
+ "n_pairs": n_pairs,
84
+ "top_keywords": coverage_scores,
85
+ "avg_coverage": avg_coverage,
86
+ }
87
+
88
+ # 分析BLiMP
89
+ print("\n分析 BLiMP 子任务...")
90
+ blimp_results = []
91
+ for jfile in sorted(BLIMP_DIR.glob("*.jsonl")):
92
+ r = analyze_task(jfile)
93
+ if r:
94
+ blimp_results.append(r)
95
+
96
+ # 分析BLiMP Supplement
97
+ print("分析 BLiMP Supplement 子任务...")
98
+ supp_results = []
99
+ for jfile in sorted(SUPP_DIR.glob("*.jsonl")):
100
+ r = analyze_task(jfile)
101
+ if r:
102
+ supp_results.append(r)
103
+
104
+ # ── 3. 输出结果 ──────────────────────────────────────────
105
+ def print_results(results, title):
106
+ print(f"\n{'='*70}")
107
+ print(f"{title}(按覆盖率从低到高,最需要补充的在前)")
108
+ print(f"{'='*70}")
109
+ print(f"{'子任务':<45} {'覆盖分':>8} {'关键词(每百万词)'}")
110
+ print("─" * 70)
111
+ for r in sorted(results, key=lambda x: x["avg_coverage"]):
112
+ kw_str = ", ".join(f"{w}({s:.0f})" for w, _, s in r["top_keywords"][:4])
113
+ print(f"{r['task']:<45} {r['avg_coverage']:>8.1f} {kw_str}")
114
+
115
+ print_results(blimp_results, "BLiMP 子任务覆盖分析")
116
+ print_results(supp_results, "BLiMP Supplement 覆盖分析")
117
+
118
+ # 保存完整结果
119
+ import json as _json
120
+ out_path = OUT_DIR / "blimp_coverage.json"
121
+ _json.dump({"blimp": blimp_results, "supplement": supp_results},
122
+ open(out_path, "w"), ensure_ascii=False, indent=2)
123
+ print(f"\n完整结果已保存: {out_path}")
scripts/01_data/find_oov_words.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 找出每个训练文件中所有不在 vocab.txt 里的词,按频次排序输出
3
+
4
+ 注:这是独立的分析工具,不属于主流水线。
5
+ Stage 2(stage2_oov_filter.py)已内置 OOV 频次计算,不再依赖本脚本的输出。
6
+ """
7
+ import re
8
+ from pathlib import Path
9
+ from collections import Counter
10
+
11
+ ROOT = Path(__file__).parent.parent.parent
12
+ VOCAB_PATH = ROOT / "data/vocab.txt"
13
+ DATA_DIR = ROOT / "data/raw_files"
14
+ OUT_DIR = ROOT / "results/analysis/oov"
15
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
16
+
17
+ FILES = [
18
+ "childes.train.txt",
19
+ "gutenberg.train.txt",
20
+ "open_subtitles.train.txt",
21
+ "simple_wiki.train.txt",
22
+ "bnc_spoken.train.txt",
23
+ "open_subtitles_cleaned_ckpt_70.txt",
24
+ "switchboard.train.txt",
25
+ ]
26
+
27
+ def tokenize(line):
28
+ return re.findall(r"[a-zA-Z''\-]+|[0-9]+", line.lower())
29
+
30
+ vocab = set(VOCAB_PATH.read_text().splitlines())
31
+ print(f"vocab 大小: {len(vocab):,} 词\n")
32
+
33
+ summary = []
34
+
35
+ for fname in FILES:
36
+ path = DATA_DIR / fname
37
+ if not path.exists():
38
+ continue
39
+
40
+ oov_counter = Counter()
41
+ total_tokens = 0
42
+
43
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
44
+ for line in f:
45
+ tokens = tokenize(line)
46
+ total_tokens += len(tokens)
47
+ for t in tokens:
48
+ if t not in vocab:
49
+ oov_counter[t] += 1
50
+
51
+ total_oov_tokens = sum(oov_counter.values())
52
+ total_oov_types = len(oov_counter)
53
+ oov_rate = total_oov_tokens / total_tokens * 100 if total_tokens else 0
54
+
55
+ # 保存完整 OOV 列表(按频次降序)
56
+ stem = fname.replace(".train.txt", "").replace(".txt", "")
57
+ out_path = OUT_DIR / f"{stem}_oov.txt"
58
+ with open(out_path, "w") as f:
59
+ f.write(f"# {fname} OOV统计\n")
60
+ f.write(f"# 总token数: {total_tokens:,} OOV token数: {total_oov_tokens:,} OOV率: {oov_rate:.2f}%\n")
61
+ f.write(f"# OOV类型数(唯一词): {total_oov_types:,}\n")
62
+ f.write(f"# 格式: 频次 词\n\n")
63
+ for word, cnt in oov_counter.most_common():
64
+ f.write(f"{cnt}\t{word}\n")
65
+
66
+ summary.append((fname, total_tokens, total_oov_tokens, oov_rate, total_oov_types))
67
+ print(f"[{fname}] OOV率={oov_rate:.2f}% 唯一OOV词={total_oov_types:,} → {out_path.name}")
68
+
69
+ print("\n\n── 汇总 ──────────────────────────────────────────────────────")
70
+ print(f"{'文件':<42} {'总token':>10} {'OOV token':>10} {'OOV率':>7} {'OOV唯一词':>10}")
71
+ print("─" * 82)
72
+ for fname, tot, oov_tok, rate, oov_types in summary:
73
+ print(f"{fname:<42} {tot:>10,} {oov_tok:>10,} {rate:>6.2f}% {oov_types:>9,}")
74
+
75
+ print(f"\n结果保存在: {OUT_DIR}")
scripts/01_data/run_all.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 01_data 数据流水线总控
3
+ 按顺序执行所有数据处理阶段
4
+
5
+ 阶段概览:
6
+ 1 噪声过滤 → data/1_noise_filtered/
7
+ 2 OOV过滤 → data/2_oov_filtered/
8
+ 3 精确去重 → data/3_dedup/
9
+ 4 格式预处理 → data/4_preprocessed/
10
+ 5 规则标注 → data/5_rule_labels/
11
+ 6 Qwen语义标注 → data/6_qwen_labels/ (需GPU)
12
+ 7 评测句提取 → data/7_eval_sentences/
13
+ 8A 采样-纯质量基线 → data/8_sample_A/train.txt
14
+ 8B 采样-配额+质量 → data/8_sample_B/train.txt
15
+ 8C 采样-加权采样 → data/8_sample_C/train.txt
16
+ 8D 采样-Embedding → data/8_sample_D/train.txt (需GPU)
17
+
18
+ 用法:
19
+ python scripts/01_data/run_all.py # 跑所有阶段 (1-8A)
20
+ python scripts/01_data/run_all.py --stage 5 # 只跑某一阶段
21
+ python scripts/01_data/run_all.py --from 7 # 从某阶段开始跑
22
+ python scripts/01_data/run_all.py --stage 8 # 跑所有 stage 8 采样 (A/B/C/D)
23
+ python scripts/01_data/run_all.py --stage 8B # 只跑 stage 8B
24
+ python scripts/01_data/run_all.py --stage 8D # 只跑 stage 8D (Embedding)
25
+ """
26
+
27
+ import argparse
28
+ import subprocess
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ ROOT = Path(__file__).parent.parent.parent
33
+
34
+ # ── 阶段定义 ────────────────────────────────────────────────────────────────────
35
+ # type="io" : 标准 input/output 参数
36
+ # type="plain" : 无参数直接运行
37
+
38
+ STAGES = [
39
+ {
40
+ "id": "1", "name": "噪声过滤 (CHAT标注 / 时间戳 / 极短行)",
41
+ "script": "stage1_noise_filter.py", "type": "io",
42
+ "input": "data/raw_files", "output": "data/1_noise_filtered",
43
+ },
44
+ {
45
+ "id": "2", "name": "OOV过滤 (低频噪声词过多的句子)",
46
+ "script": "stage2_oov_filter.py", "type": "io",
47
+ "input": "data/1_noise_filtered", "output": "data/2_oov_filtered",
48
+ },
49
+ {
50
+ "id": "3", "name": "精确去重(全局 MD5)",
51
+ "script": "stage3_dedup.py", "type": "io",
52
+ "input": "data/2_oov_filtered", "output": "data/3_dedup",
53
+ },
54
+ {
55
+ "id": "4", "name": "格式预处理(去说话人前缀 / 全大写规范 / 标题行过滤)",
56
+ "script": "stage4_preprocess.py", "type": "io",
57
+ "input": "data/3_dedup", "output": "data/4_preprocessed",
58
+ },
59
+ {
60
+ "id": "5", "name": "规则标注(BLiMP 67子任务 / BLiMP Supplement / EWoK 11域)",
61
+ "script": "stage5_rule_label.py", "type": "io",
62
+ "input": "data/4_preprocessed", "output": "data/5_rule_labels",
63
+ },
64
+ {
65
+ "id": "6", "name": "Qwen语义标注(9维度深度分析,需GPU)",
66
+ "script": "stage6_qwen_label.py", "type": "io",
67
+ "input": "data/4_preprocessed", "output": "data/6_qwen_labels",
68
+ },
69
+ {
70
+ "id": "7", "name": "评测句提取",
71
+ "script": "stage7a_extract_eval.py", "type": "plain",
72
+ },
73
+ {
74
+ "id": "8A", "name": "采样方案A:纯质量基线",
75
+ "script": "stage8_sample_A.py", "type": "plain",
76
+ },
77
+ {
78
+ "id": "8B", "name": "采样方案B:任务配额 + 质量填充",
79
+ "script": "stage8_sample_B.py", "type": "plain",
80
+ },
81
+ {
82
+ "id": "8C", "name": "采样方案C:加权采样",
83
+ "script": "stage8_sample_C.py", "type": "plain",
84
+ },
85
+ {
86
+ "id": "8D", "name": "采样方案D:Embedding 相似度采样(需GPU)",
87
+ "script": "stage8_sample_D.py", "type": "plain",
88
+ },
89
+ ]
90
+
91
+
92
+ def run_stage(stage):
93
+ script = Path(__file__).parent / stage["script"]
94
+
95
+ print(f"\n{'='*60}")
96
+ print(f"阶段 {stage['id']}: {stage['name']}")
97
+ print(f" 脚本: {script.name}")
98
+ if stage["type"] == "io":
99
+ print(f" 输入: {ROOT / stage['input']}")
100
+ print(f" 输出: {ROOT / stage['output']}")
101
+ print(f"{'='*60}")
102
+
103
+ if stage["type"] == "io":
104
+ cmd = [sys.executable, str(script),
105
+ "--input_dir", str(ROOT / stage["input"]),
106
+ "--output_dir", str(ROOT / stage["output"])]
107
+ else:
108
+ cmd = [sys.executable, str(script)]
109
+
110
+ result = subprocess.run(cmd, check=False)
111
+ if result.returncode != 0:
112
+ print(f"\n[ERROR] 阶段 {stage['id']} 失败,退出码 {result.returncode}")
113
+ sys.exit(result.returncode)
114
+ print(f"\n[OK] 阶段 {stage['id']} 完成")
115
+
116
+
117
+ def match_stages(selector: str) -> list[dict]:
118
+ """根据选择器匹配阶段。支持: "5", "8", "8B", "8D" 等。"""
119
+ selector = selector.upper()
120
+ matched = []
121
+ for s in STAGES:
122
+ sid = s["id"].upper()
123
+ if sid == selector:
124
+ matched.append(s)
125
+ elif selector.isdigit() and sid.startswith(selector):
126
+ # "8" 匹配 "8A", "8B", "8C", "8D"
127
+ matched.append(s)
128
+ return matched
129
+
130
+
131
+ def main():
132
+ parser = argparse.ArgumentParser(
133
+ description="01_data 数据流水线总控",
134
+ formatter_class=argparse.RawDescriptionHelpFormatter,
135
+ )
136
+ parser.add_argument("--stage", type=str, default=None,
137
+ help="只运行指定阶段(如 --stage 5, --stage 8, --stage 8B)")
138
+ parser.add_argument("--from", type=str, default="1", dest="from_stage",
139
+ help="从指定阶段开始运行(如 --from 7)")
140
+ parser.add_argument("--to", type=str, default=None, dest="to_stage",
141
+ help="运行到指定阶段为止(如 --to 6)")
142
+ parser.add_argument("--list", action="store_true",
143
+ help="列出所有阶段")
144
+ args = parser.parse_args()
145
+
146
+ if args.list:
147
+ print("可用阶段:")
148
+ for s in STAGES:
149
+ print(f" {s['id']:4s} {s['name']}")
150
+ return
151
+
152
+ if args.stage is not None:
153
+ stages_to_run = match_stages(args.stage)
154
+ else:
155
+ # --from / --to 范围选择(只匹配纯数字前缀)
156
+ from_idx = 0
157
+ to_idx = len(STAGES)
158
+ for i, s in enumerate(STAGES):
159
+ num = s["id"].rstrip("ABCD")
160
+ if num == args.from_stage:
161
+ from_idx = i
162
+ break
163
+ if args.to_stage:
164
+ for i, s in enumerate(STAGES):
165
+ num = s["id"].rstrip("ABCD")
166
+ if num == args.to_stage:
167
+ # 包含该阶段的所有子阶段
168
+ to_idx = i + 1
169
+ while to_idx < len(STAGES) and STAGES[to_idx]["id"].startswith(args.to_stage):
170
+ to_idx += 1
171
+ break
172
+ stages_to_run = STAGES[from_idx:to_idx]
173
+
174
+ if not stages_to_run:
175
+ print(f"没有找到匹配的阶段: {args.stage or args.from_stage}")
176
+ print("使用 --list 查看所有可用阶段")
177
+ sys.exit(1)
178
+
179
+ print(f"将运行 {len(stages_to_run)} 个阶段: {[s['id'] for s in stages_to_run]}")
180
+
181
+ for stage in stages_to_run:
182
+ run_stage(stage)
183
+
184
+ print(f"\n{'='*60}")
185
+ print("数据流水线完成!")
186
+ print(f"{'='*60}")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
scripts/01_data/stage1_noise_filter.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 第一阶段:噪声过滤
3
+ 输入: data/raw_files/
4
+ 输出: data/1_noise_filtered/
5
+
6
+ 过滤规则:
7
+ 1. 【所有文件】空行、词数 < 3 的极短行
8
+ 2. 【所有文件】含 Unicode 替换字符 (U+FFFD) 的编码损坏行
9
+ 3. 【所有文件】非 ASCII 字符占比过高的编码垃圾行(ASCII 比例 < 70%)
10
+ 4. 【CHILDES】以 @ 或 % 开头的 CHAT 标注行;含大量 xxx/yyy 的行
11
+ 5. 【Open Subtitles】时间戳行;HTML 标签行
12
+ """
13
+
14
+ import re
15
+ import argparse
16
+ from pathlib import Path
17
+ from collections import defaultdict
18
+
19
+ ROOT = Path(__file__).parent.parent.parent
20
+ INPUT_DIR = ROOT / "data/raw_files"
21
+ OUTPUT_DIR = ROOT / "data/1_noise_filtered"
22
+
23
+ FILES = [
24
+ # 官方 BabyLM 数据
25
+ "childes.train.txt",
26
+ "gutenberg.train.txt",
27
+ "open_subtitles.train.txt",
28
+ "simple_wiki.train.txt",
29
+ "bnc_spoken.train.txt",
30
+ "open_subtitles_cleaned_ckpt_70.txt",
31
+ "switchboard.train.txt",
32
+ # 外部数据
33
+ "ext_fineweb_edu.txt",
34
+ "ext_cosmopedia.txt",
35
+ "ext_tiny_dialogues.txt",
36
+ "ext_generics_kb.txt",
37
+ "ext_conceptnet.txt",
38
+ "ext_onestop_english.txt",
39
+ "ext_snli.txt",
40
+ ]
41
+
42
+ # ASCII 字符占比低于此值的行视为编码垃圾
43
+ MIN_ASCII_RATIO = 0.7
44
+
45
+ # ── 过滤函数 ────────────────────────────────────────────
46
+
47
+ def is_too_short(line):
48
+ """词数 < 3"""
49
+ return len(line.split()) < 3
50
+
51
+
52
+ def has_encoding_corruption(line):
53
+ """含 Unicode 替换字符 (U+FFFD) 的编码损坏行"""
54
+ return '\ufffd' in line
55
+
56
+
57
+ def is_encoding_garbage(line):
58
+ """非 ASCII 字符占比过高的编码垃圾行"""
59
+ if not line:
60
+ return False
61
+ ascii_count = sum(1 for c in line if ord(c) < 128)
62
+ return (ascii_count / len(line)) < MIN_ASCII_RATIO
63
+
64
+
65
+ def is_metadata_separator(line):
66
+ """CHILDES/Open Subtitles 文件分隔符行,如 = = = path/to/file.cha = = ="""
67
+ return line.startswith("= = =") or line == "==="
68
+
69
+
70
+ def is_childes_noise(line):
71
+ """CHAT 格式标注行"""
72
+ if line.startswith(('@', '%')):
73
+ return True
74
+ tokens = line.split()
75
+ oov_like = sum(1 for t in tokens if t in ('xxx', 'yyy', 'www'))
76
+ if tokens and oov_like / len(tokens) > 0.5:
77
+ return True
78
+ return False
79
+
80
+ def is_subtitle_noise(line):
81
+ """时间戳或 HTML 标签行"""
82
+ # 时间戳:00:01:23,456 --> 00:01:25,789
83
+ if re.match(r'^\d{2}:\d{2}:\d{2}', line):
84
+ return True
85
+ # 纯数字行(字幕序号)
86
+ if re.match(r'^\d+$', line):
87
+ return True
88
+ # HTML 标签
89
+ if re.search(r'<[^>]+>', line):
90
+ return True
91
+ return False
92
+
93
+ # ── 主流程 ───────────────────────────────────────────────
94
+
95
+ def filter_file(file_name, input_dir=None, output_dir=None):
96
+ input_dir = Path(input_dir) if input_dir else INPUT_DIR
97
+ output_dir = Path(output_dir) if output_dir else OUTPUT_DIR
98
+ input_path = input_dir / file_name
99
+ output_path = output_dir / file_name
100
+
101
+ is_childes = "childes" in file_name
102
+ is_subtitles = "open_subtitles" in file_name
103
+
104
+ stats = defaultdict(int)
105
+
106
+ with open(input_path, "r", encoding="utf-8", errors="ignore") as fin, \
107
+ open(output_path, "w", encoding="utf-8") as fout:
108
+
109
+ for raw_line in fin:
110
+ line = raw_line.strip()
111
+ stats["total"] += 1
112
+
113
+ if not line:
114
+ stats["drop_empty"] += 1
115
+ continue
116
+
117
+ if has_encoding_corruption(line):
118
+ stats["drop_encoding"] += 1
119
+ continue
120
+
121
+ if is_encoding_garbage(line):
122
+ stats["drop_encoding_garbage"] += 1
123
+ continue
124
+
125
+ if is_metadata_separator(line):
126
+ stats["drop_metadata"] += 1
127
+ continue
128
+
129
+ if is_childes and is_childes_noise(line):
130
+ stats["drop_chat_noise"] += 1
131
+ continue
132
+
133
+ if is_subtitles and is_subtitle_noise(line):
134
+ stats["drop_subtitle_noise"] += 1
135
+ continue
136
+
137
+ if is_too_short(line):
138
+ stats["drop_too_short"] += 1
139
+ continue
140
+
141
+ fout.write(line + "\n")
142
+ stats["keep"] += 1
143
+
144
+ return stats
145
+
146
+ # ── 执行 & 报告 ──────────────────────────────────────────
147
+
148
+ def main(input_dir=None, output_dir=None):
149
+ in_dir = Path(input_dir) if input_dir else INPUT_DIR
150
+ out_dir = Path(output_dir) if output_dir else OUTPUT_DIR
151
+ out_dir.mkdir(parents=True, exist_ok=True)
152
+
153
+ print(f"输入: {in_dir}")
154
+ print(f"输出: {out_dir}\n")
155
+ print(f"{'文件':<42} {'保留':>8} {'丢弃':>8} {'保留率':>8} 丢弃原因")
156
+ print("─" * 110)
157
+
158
+ for fname in FILES:
159
+ path = in_dir / fname
160
+ if not path.exists():
161
+ print(f"{fname:<42} [文件不存在,跳过]")
162
+ continue
163
+
164
+ stats = filter_file(fname, in_dir, out_dir)
165
+ total = stats["total"]
166
+ keep = stats["keep"]
167
+ drop = total - keep
168
+ rate = keep / total * 100 if total else 0
169
+
170
+ reasons = []
171
+ if stats["drop_empty"]: reasons.append(f"空行:{stats['drop_empty']:,}")
172
+ if stats["drop_encoding"]: reasons.append(f"U+FFFD:{stats['drop_encoding']:,}")
173
+ if stats["drop_encoding_garbage"]: reasons.append(f"编码垃圾:{stats['drop_encoding_garbage']:,}")
174
+ if stats["drop_metadata"]: reasons.append(f"元数据:{stats['drop_metadata']:,}")
175
+ if stats["drop_chat_noise"]: reasons.append(f"CHAT:{stats['drop_chat_noise']:,}")
176
+ if stats["drop_subtitle_noise"]: reasons.append(f"字幕噪声:{stats['drop_subtitle_noise']:,}")
177
+ if stats["drop_too_short"]: reasons.append(f"太短:{stats['drop_too_short']:,}")
178
+
179
+ print(f"{fname:<42} {keep:>8,} {drop:>8,} {rate:>7.1f}% {' | '.join(reasons)}")
180
+
181
+ print("\n完成!")
182
+
183
+
184
+ if __name__ == "__main__":
185
+ parser = argparse.ArgumentParser()
186
+ parser.add_argument("--input_dir", default=None)
187
+ parser.add_argument("--output_dir", default=None)
188
+ args = parser.parse_args()
189
+ main(args.input_dir, args.output_dir)
scripts/01_data/stage2_oov_filter.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 第二阶段:OOV 过滤
3
+ 输入: data/1_noise_filtered/
4
+ 输出: data/2_oov_filtered/
5
+
6
+ 过滤逻辑:
7
+ 1. 用 vocab.txt(评测词表,66,875 词)判断每个 token 是否为 OOV
8
+ 2. 统计每个文件中各 OOV 词的出现频次
9
+ 3. 频次 < freq_threshold 的 OOV 词视为"噪声嫌疑词"(高频 OOV 如缩写是正常英语)
10
+ 4. 若一行中噪声嫌疑词占比 > noise_ratio_max,则丢弃该行
11
+ 5. 每个文件有独立参数(详见 FILE_PARAMS)
12
+
13
+ 参数依据:
14
+ - simple_wiki OOV 主要是维基百科专有名词(合法内容),保持宽松
15
+ - open_subtitles 词语黏连多,适度过滤
16
+ - 其余文件噪声较少,按实际过滤率设置
17
+ """
18
+
19
+ import re
20
+ import argparse
21
+ from pathlib import Path
22
+ from collections import Counter, defaultdict
23
+
24
+ ROOT = Path(__file__).parent.parent.parent
25
+ INPUT_DIR = ROOT / "data/1_noise_filtered"
26
+ OUTPUT_DIR = ROOT / "data/2_oov_filtered"
27
+ VOCAB_PATH = ROOT / "data/vocab.txt"
28
+
29
+ # 每个文件的独立参数:(freq_threshold, noise_ratio_max)
30
+ # freq_threshold: OOV词频次低于此值 → 视为噪声嫌疑词
31
+ # noise_ratio_max: 句子中噪声嫌疑词占比超过此值 → 丢弃
32
+ FILE_PARAMS = {
33
+ # 官方 BabyLM 数据
34
+ "childes.train.txt": (20, 0.05),
35
+ "gutenberg.train.txt": (10, 0.05),
36
+ "open_subtitles.train.txt": (10, 0.05),
37
+ "simple_wiki.train.txt": ( 3, 0.20),
38
+ "bnc_spoken.train.txt": (10, 0.05),
39
+ "open_subtitles_cleaned_ckpt_70.txt": (10, 0.05),
40
+ "switchboard.train.txt": (10, 0.05),
41
+ # 外部数据(已预过滤,宽松参数)
42
+ "ext_fineweb_edu.txt": ( 5, 0.10),
43
+ "ext_cosmopedia.txt": ( 5, 0.10),
44
+ "ext_tiny_dialogues.txt": (10, 0.05),
45
+ "ext_generics_kb.txt": ( 3, 0.15),
46
+ "ext_conceptnet.txt": ( 3, 0.15),
47
+ "ext_onestop_english.txt": ( 5, 0.10),
48
+ "ext_snli.txt": ( 5, 0.10),
49
+ }
50
+
51
+
52
+ def tokenize(line):
53
+ return re.findall(r"[a-zA-Z''\-]+|[0-9]+", line.lower())
54
+
55
+
56
+ def load_vocab(vocab_path: Path) -> set:
57
+ """加载评测词表"""
58
+ vocab = set(vocab_path.read_text().splitlines())
59
+ print(f" vocab.txt: {len(vocab):,} 词 ({vocab_path})")
60
+ return vocab
61
+
62
+
63
+ def compute_oov_freq(input_path: Path, vocab: set) -> Counter:
64
+ """扫描文件,统计每个 OOV 词的出现频次"""
65
+ oov_counter = Counter()
66
+ with open(input_path, "r", encoding="utf-8", errors="ignore") as f:
67
+ for line in f:
68
+ for t in tokenize(line):
69
+ if t not in vocab:
70
+ oov_counter[t] += 1
71
+ return oov_counter
72
+
73
+
74
+ def filter_file(fname: str, input_dir: Path, output_dir: Path, vocab: set,
75
+ verbose: bool = True) -> dict:
76
+ freq_thresh, noise_ratio = FILE_PARAMS[fname]
77
+ input_path = input_dir / fname
78
+
79
+ # 第一遍:统计 OOV 频次
80
+ if verbose:
81
+ print(f" {fname}: 统计 OOV 频次 ...", end="", flush=True)
82
+ oov_counter = compute_oov_freq(input_path, vocab)
83
+ noise_oov = {w for w, cnt in oov_counter.items() if cnt < freq_thresh}
84
+ if verbose:
85
+ print(f" {len(oov_counter):,} OOV 类型, {len(noise_oov):,} 噪声词 (freq<{freq_thresh})")
86
+
87
+ # 第二遍:过滤
88
+ stats = defaultdict(int)
89
+ output_path = output_dir / fname
90
+ with open(input_path, "r", encoding="utf-8", errors="ignore") as fin, \
91
+ open(output_path, "w", encoding="utf-8") as fout:
92
+
93
+ for line in fin:
94
+ line = line.strip()
95
+ stats["total"] += 1
96
+ if not line:
97
+ stats["drop_empty"] += 1
98
+ continue
99
+
100
+ tokens = tokenize(line)
101
+ if not tokens:
102
+ stats["drop_empty"] += 1
103
+ continue
104
+
105
+ noise_cnt = sum(1 for t in tokens if t in noise_oov)
106
+ if noise_cnt / len(tokens) > noise_ratio:
107
+ stats["drop_oov"] += 1
108
+ continue
109
+
110
+ fout.write(line + "\n")
111
+ stats["keep"] += 1
112
+
113
+ total = stats["total"]
114
+ keep = stats["keep"]
115
+ rate = keep / total * 100 if total else 0
116
+ if verbose:
117
+ print(f" {fname:<42} 保留 {keep:>8,} / {total:>8,} ({rate:.1f}%) "
118
+ f"OOV过滤:{stats['drop_oov']:,}")
119
+ return stats
120
+
121
+
122
+ def main(input_dir=None, output_dir=None):
123
+ input_dir = Path(input_dir) if input_dir else INPUT_DIR
124
+ output_dir = Path(output_dir) if output_dir else OUTPUT_DIR
125
+ output_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ print(f"输入: {input_dir}")
128
+ print(f"输出: {output_dir}")
129
+
130
+ if not VOCAB_PATH.exists():
131
+ print(f"\n[错误] vocab.txt 不存在: {VOCAB_PATH}")
132
+ print(f" 请确认 data/vocab.txt 文件存在(来源:evaluation-pipeline-2025/evaluation_pipeline/ewok/vocab.txt)")
133
+ return
134
+
135
+ vocab = load_vocab(VOCAB_PATH)
136
+ print()
137
+
138
+ total_stats = defaultdict(int)
139
+ for fname in FILE_PARAMS:
140
+ if not (input_dir / fname).exists():
141
+ print(f" [skip] {fname} 不存在")
142
+ continue
143
+ stats = filter_file(fname, input_dir, output_dir, vocab)
144
+ for k, v in stats.items():
145
+ total_stats[k] += v
146
+
147
+ total = total_stats["total"]
148
+ keep = total_stats["keep"]
149
+ print(f"\n合计: 保留 {keep:,} / {total:,} 行 ({keep/total*100:.1f}%)")
150
+ print(f"OOV过滤共丢弃: {total_stats['drop_oov']:,} 行")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ parser = argparse.ArgumentParser()
155
+ parser.add_argument("--input_dir", default=None)
156
+ parser.add_argument("--output_dir", default=None)
157
+ args = parser.parse_args()
158
+ main(args.input_dir, args.output_dir)
scripts/01_data/stage3_dedup.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 第三阶段:精确去重
3
+ 输入: data/2_oov_filtered/
4
+ 输出: data/3_dedup/
5
+
6
+ 去重方式:
7
+ - 对每行计算 MD5 哈希值
8
+ - 全局去重(跨文件):同一句话在多个文件中只保留第一次出现
9
+ - 文件处理顺序即优先级(gutenberg 质量最高,最先处理)
10
+
11
+ 注:只对行内容去重,不区分大小写(统一 lower 后比较)
12
+ """
13
+
14
+ import hashlib
15
+ import argparse
16
+ from pathlib import Path
17
+ from collections import defaultdict
18
+
19
+ ROOT = Path(__file__).parent.parent.parent
20
+ INPUT_DIR = ROOT / "data/2_oov_filtered"
21
+ OUTPUT_DIR = ROOT / "data/3_dedup"
22
+
23
+ # 文件处理顺序 = 去重优先级(先出现的保留)
24
+ # 高质量文件排前面,外部数据排在官方数据之后
25
+ FILES = [
26
+ # 官方 BabyLM 数据(优先保留)
27
+ "gutenberg.train.txt",
28
+ "simple_wiki.train.txt",
29
+ "bnc_spoken.train.txt",
30
+ "switchboard.train.txt",
31
+ "open_subtitles_cleaned_ckpt_70.txt",
32
+ "childes.train.txt",
33
+ "open_subtitles.train.txt",
34
+ # 外部数据(官方数据优先,重复行保留官方版本)
35
+ "ext_fineweb_edu.txt",
36
+ "ext_cosmopedia.txt",
37
+ "ext_onestop_english.txt",
38
+ "ext_generics_kb.txt",
39
+ "ext_conceptnet.txt",
40
+ "ext_tiny_dialogues.txt",
41
+ "ext_snli.txt",
42
+ ]
43
+
44
+
45
+ def line_hash(line: str) -> str:
46
+ return hashlib.md5(line.lower().strip().encode("utf-8")).hexdigest()
47
+
48
+
49
+ def dedup_file(fname, input_dir, output_dir, seen_hashes):
50
+ stats = defaultdict(int)
51
+ output_path = output_dir / fname
52
+
53
+ with open(input_dir / fname, "r", encoding="utf-8", errors="ignore") as fin, \
54
+ open(output_path, "w", encoding="utf-8") as fout:
55
+
56
+ for raw_line in fin:
57
+ line = raw_line.strip()
58
+ stats["total"] += 1
59
+
60
+ if not line:
61
+ stats["drop_empty"] += 1
62
+ continue
63
+
64
+ h = line_hash(line)
65
+ if h in seen_hashes:
66
+ stats["drop_dup"] += 1
67
+ continue
68
+
69
+ seen_hashes.add(h)
70
+ fout.write(line + "\n")
71
+ stats["keep"] += 1
72
+
73
+ return stats
74
+
75
+
76
+ def main(input_dir=None, output_dir=None):
77
+ input_dir = Path(input_dir) if input_dir else INPUT_DIR
78
+ output_dir = Path(output_dir) if output_dir else OUTPUT_DIR
79
+ output_dir.mkdir(parents=True, exist_ok=True)
80
+
81
+ print(f"输入: {input_dir}")
82
+ print(f"输出: {output_dir}")
83
+ print(f"去重方式: 全局精确去重(MD5,跨文件)\n")
84
+
85
+ seen_hashes = set()
86
+ total_stats = defaultdict(int)
87
+
88
+ print(f"{'文件':<42} {'保留':>8} {'去重丢弃':>10} {'保留率':>8}")
89
+ print("─" * 75)
90
+
91
+ for fname in FILES:
92
+ if not (input_dir / fname).exists():
93
+ print(f" [skip] {fname} 不存在")
94
+ continue
95
+
96
+ stats = dedup_file(fname, input_dir, output_dir, seen_hashes)
97
+ total = stats["total"]
98
+ keep = stats["keep"]
99
+ rate = keep / total * 100 if total else 0
100
+
101
+ print(f"{fname:<42} {keep:>8,} {stats['drop_dup']:>10,} {rate:>7.1f}%")
102
+
103
+ for k, v in stats.items():
104
+ total_stats[k] += v
105
+
106
+ total = total_stats["total"]
107
+ keep = total_stats["keep"]
108
+ print("─" * 75)
109
+ print(f"{'合计':<42} {keep:>8,} {total_stats['drop_dup']:>10,} {keep/total*100:>7.1f}%")
110
+ print(f"\n全局唯一句子数: {len(seen_hashes):,}")
111
+ print(f"去重共丢弃: {total_stats['drop_dup']:,} 行")
112
+
113
+
114
+ if __name__ == "__main__":
115
+ parser = argparse.ArgumentParser()
116
+ parser.add_argument("--input_dir", default=None)
117
+ parser.add_argument("--output_dir", default=None)
118
+ args = parser.parse_args()
119
+ main(args.input_dir, args.output_dir)
scripts/01_data/stage4_preprocess.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 4:各文件格式预处理
3
+
4
+ 每个训练文件的清洗规则不同:
5
+
6
+ childes : 去除说话人前缀 *XXX:\t,丢弃独立注释行 [...],移除行内 [...]/xxx
7
+ switchboard : 去除说话人前缀 A:\t / B:\t
8
+ 可选:去除口语不流利标记 uh/um(默认保留)
9
+ open_subtitles : 全大写 → 句首大写(修复第一人称 I)
10
+ simple_wiki : 丢弃 = = = 标题行;丢弃 A:/B: 对话行;丢弃小节标题行;
11
+ 丢弃 URL 行;HTML 实体解码
12
+ open_subtitles_cleaned: 去除 *XXX:\t 前缀,去除 [laugh]/xxx 等 CHAT 标记
13
+ bnc_spoken : 无需处理(直接复制)
14
+ gutenberg : 过滤 Project Gutenberg 模板文本;HTML 实体解码
15
+
16
+ 可选:--ner 启用 spaCy NER 恢复专有名词大小写(PERSON/GPE/ORG 等)
17
+
18
+ 输入 : data/3_dedup/
19
+ 输出 : data/4_preprocessed/
20
+
21
+ 用法:
22
+ python scripts/01_data/stage4_preprocess.py
23
+ python scripts/01_data/stage4_preprocess.py --ner # 启用 NER 大小写恢复
24
+ python scripts/01_data/stage4_preprocess.py --file childes.train.txt
25
+ python scripts/01_data/stage4_preprocess.py --remove_disfluency
26
+ """
27
+
28
+ import html
29
+ import re
30
+ import argparse
31
+ import time
32
+ from pathlib import Path
33
+ from tqdm import tqdm
34
+
35
+ ROOT = Path(__file__).parent.parent.parent
36
+ IN_DIR = ROOT / "data" / "3_dedup"
37
+ OUT_DIR = ROOT / "data" / "4_preprocessed"
38
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
39
+
40
+ # ─────────────────────────────────────────────
41
+ # 各文件清洗函数
42
+ # 返回 None 表示丢弃该行,返回字符串表示保留
43
+ # ─────────────────────────────────────────────
44
+
45
+ # CHAT 格式说话人前缀:*CHI: / *MOT: / *COL: 等
46
+ _CHAT_PREFIX = re.compile(r"^\*[A-Z0-9]+:\s*")
47
+ # 独立注释行:整行都是 [...] 内容
48
+ _CHAT_COMMENT = re.compile(r"^\[.*\]\s*$")
49
+ # open_subtitles_cleaned 里的 CHAT 标记:[laugh] / [laughter] / xxx
50
+ _CHAT_TOKEN = re.compile(r"\[.*?\]|(?<!\w)xxx(?!\w)", re.I)
51
+ # 口语不流利标记:独立的 uh / um(前后是标点或空格)
52
+ _DISFLUENCY = re.compile(r"(?<!\w)(uh|um|uh-huh|mm-hmm|hmm|hm)(?!\w),?\s*", re.I)
53
+ # CHAT 行内注释:[laugh] / [sits on couch] 等
54
+ _CHAT_INLINE = re.compile(r"\[.*?\]")
55
+ # Simple Wiki 标题行:= = = 任意内容 = = =
56
+ _WIKI_HEADING = re.compile(r"^=+\s*.+\s*=+\s*$")
57
+ # Simple Wiki 小节标题:单独一行的 "History." / "Early life and career." 等
58
+ _WIKI_SECTION = re.compile(
59
+ r"^(History|Geography|Population|Demographics|Music video|Early life"
60
+ r"|Career|Personal life|Education|Discography|Filmography|Biography"
61
+ r"|Background|Death|Awards|Legacy|Plot|Synopsis|References"
62
+ r"|Other Websites|External links|See also|Gallery|Sources"
63
+ r"|Bibliography|Controversy|Criticism|Reception|Gameplay|Development"
64
+ r"|Release|Production|Cast|Notes|Controversy and criticism"
65
+ r"|Early life and career|Life and career|Life|Works|Family"
66
+ r"|Climate|Economy|Transport|Government|Military career|Music"
67
+ r"|Television|Film|Books|Albums|Singles|Track listing"
68
+ r"|2020 census|Honors|Achievements|Members|Stadium|Overview"
69
+ r"|Notable people|Notable residents|Composition|Recording"
70
+ r"|Commercial performance|Critical reception|Chart performance"
71
+ r"|Music videos?|Video games?|Politics|Religion|Culture|Media"
72
+ r"|Sports|Infrastructure|Languages|Geology|Ecology|Taxonomy"
73
+ r"|Description|Distribution|Habitat|Behavior|Conservation"
74
+ r"|In popular culture|Related pages)\s*\.?\s*$", re.I)
75
+ # Simple Wiki A:/B: 对话行(混入的 Switchboard 风格对话)
76
+ _WIKI_DIALOGUE = re.compile(r"^[AB]:\s")
77
+ # URL 行
78
+ _URL_PATTERN = re.compile(r"https?://")
79
+ # Switchboard 说话人前缀:A:\t / B:\t(字母 + 冒号 + tab)
80
+ _SWB_PREFIX = re.compile(r"^[A-Z]:\s*")
81
+ # Gutenberg 模板文本
82
+ _GUTENBERG_BOILERPLATE = re.compile(
83
+ r"(^End of .*Project Gutenberg|^E-text prepared by|"
84
+ r"^Produced by .*(Project Gutenberg|Distributed Proofreading)"
85
+ r"|^This (eBook|ebook|book) (is for the use of|has been transcribed for)"
86
+ r"|^\*\*\* ?START OF|^\*\*\* ?END OF"
87
+ r"|^_?Copyright[_,ed]|^COPYRIGHT)", re.I)
88
+ # 最短保留词数
89
+ MIN_WORDS = 3
90
+
91
+ # NER 需要恢复大小写的实体类型
92
+ _NER_LABELS = {"PERSON", "GPE", "ORG", "FAC", "LOC", "NORP", "EVENT", "WORK_OF_ART"}
93
+
94
+
95
+ def clean_childes(line: str) -> str | None:
96
+ """去除 *XXX:\t 说话人前缀;丢弃独立注释行;移除行内 [...] 注释和 xxx 标记
97
+ 最短词数放宽到 2(儿童短句对 AoA 评测有价值)
98
+ """
99
+ if _CHAT_COMMENT.match(line):
100
+ return None
101
+ line = _CHAT_PREFIX.sub("", line).strip()
102
+ # 移除行内 [laugh] / [sits on couch] 等注释,以及独立的 xxx(不可辨识语音标记)
103
+ line = _CHAT_TOKEN.sub(" ", line)
104
+ # 清理多余空格和标点前空格
105
+ line = re.sub(r"\s+([.,!?;:])", r"\1", line)
106
+ line = re.sub(r"\s{2,}", " ", line).strip()
107
+ if len(line.split()) < 2:
108
+ return None
109
+ return line
110
+
111
+
112
+ def clean_switchboard(line: str, remove_disfluency: bool = False) -> str | None:
113
+ """去除 A:\t / B:\t 说话人前缀;可选去除 uh/um"""
114
+ line = _SWB_PREFIX.sub("", line).strip()
115
+ if remove_disfluency:
116
+ line = _DISFLUENCY.sub(" ", line).strip()
117
+ if len(line.split()) < MIN_WORDS:
118
+ return None
119
+ return line
120
+
121
+
122
+ def clean_open_subtitles(line: str) -> str | None:
123
+ """全大写 → 句首大写(其余小写),修复第一人称 I"""
124
+ # 只有全大写的行才转换(避免误处理已有正常大小写的行)
125
+ if line == line.upper() and any(c.isalpha() for c in line):
126
+ line = line.capitalize()
127
+ # 修复第一人称代词:独立的 i → I(英语中无例外)
128
+ line = re.sub(r"(?<![a-zA-Z])i(?![a-zA-Z])", "I", line)
129
+ if len(line.split()) < MIN_WORDS:
130
+ return None
131
+ return line
132
+
133
+
134
+ def clean_simple_wiki(line: str) -> str | None:
135
+ """丢弃 = = = 标题行;丢弃 A:/B: 对话行;丢弃小节标题行;
136
+ 丢弃 URL-only 行;HTML 实体解码"""
137
+ if _WIKI_HEADING.match(line):
138
+ return None
139
+ # 混入的 Switchboard 风格对话 A: / B:
140
+ if _WIKI_DIALOGUE.match(line):
141
+ return None
142
+ # 小节标题 "History." / "Early life." 等
143
+ if _WIKI_SECTION.match(line):
144
+ return None
145
+ # HTML 实体解码 (&amp; → &, &lt; → < 等)
146
+ if '&' in line:
147
+ line = html.unescape(line)
148
+ # 含 URL 的行(参考链接,无语言价值)
149
+ if _URL_PATTERN.search(line):
150
+ return None
151
+ if len(line.split()) < MIN_WORDS:
152
+ return None
153
+ return line
154
+
155
+
156
+ def clean_open_subtitles_cleaned(line: str) -> str | None:
157
+ """去除 *XXX:\t 前缀;去除 [laugh]/xxx 等 CHAT 标记"""
158
+ if _CHAT_COMMENT.match(line):
159
+ return None
160
+ line = _CHAT_PREFIX.sub("", line).strip()
161
+ line = _CHAT_TOKEN.sub(" ", line).strip()
162
+ # 多余空格
163
+ line = re.sub(r"\s{2,}", " ", line).strip()
164
+ if len(line.split()) < MIN_WORDS:
165
+ return None
166
+ return line
167
+
168
+
169
+ def clean_gutenberg(line: str) -> str | None:
170
+ """过滤 Project Gutenberg 模板文本;HTML 实体解码"""
171
+ if _GUTENBERG_BOILERPLATE.search(line):
172
+ return None
173
+ # HTML 实体解码
174
+ if '&' in line:
175
+ line = html.unescape(line)
176
+ if len(line.split()) < MIN_WORDS:
177
+ return None
178
+ return line
179
+
180
+
181
+ def passthrough(line: str) -> str | None:
182
+ """bnc_spoken:原样保留"""
183
+ return line if len(line.split()) >= MIN_WORDS else None
184
+
185
+
186
+ # ─────────────────────────────────────────────
187
+ # 文件 → 清洗函数映射
188
+ # ─────────────────────────────────────────────
189
+
190
+ FILE_CLEANERS = {
191
+ # 官方 BabyLM 数据
192
+ "childes.train.txt": "childes",
193
+ "switchboard.train.txt": "switchboard",
194
+ "open_subtitles.train.txt": "open_subtitles",
195
+ "simple_wiki.train.txt": "simple_wiki",
196
+ "open_subtitles_cleaned_ckpt_70.txt": "open_subtitles_cleaned",
197
+ "bnc_spoken.train.txt": "passthrough",
198
+ "gutenberg.train.txt": "gutenberg",
199
+ # 外部数据(已预过滤,passthrough + 最短词数过滤)
200
+ "ext_fineweb_edu.txt": "passthrough",
201
+ "ext_cosmopedia.txt": "passthrough",
202
+ "ext_tiny_dialogues.txt": "passthrough",
203
+ "ext_generics_kb.txt": "passthrough",
204
+ "ext_conceptnet.txt": "passthrough",
205
+ "ext_onestop_english.txt": "passthrough",
206
+ "ext_snli.txt": "passthrough",
207
+ }
208
+
209
+
210
+ def get_cleaner(key: str, remove_disfluency: bool):
211
+ if key == "childes":
212
+ return clean_childes
213
+ if key == "switchboard":
214
+ return lambda line: clean_switchboard(line, remove_disfluency)
215
+ if key == "open_subtitles":
216
+ return clean_open_subtitles
217
+ if key == "simple_wiki":
218
+ return clean_simple_wiki
219
+ if key == "open_subtitles_cleaned":
220
+ return clean_open_subtitles_cleaned
221
+ if key == "gutenberg":
222
+ return clean_gutenberg
223
+ return passthrough
224
+
225
+
226
+ # ─────────────────────────────────────────────
227
+ # NER 大小写恢复
228
+ # ─────────────────────────────────────────────
229
+
230
+ def _apply_title_case(text: str, start: int, end: int) -> str:
231
+ """将 text[start:end] 中每个单词首字母大写,保持其余不变。"""
232
+ span = text[start:end]
233
+ # 逐词首字母大写(比 .title() 更安全,不会影响撇号后的字符)
234
+ words = span.split()
235
+ capitalized = " ".join(w[0].upper() + w[1:] if w else w for w in words)
236
+ return text[:start] + capitalized + text[end:]
237
+
238
+
239
+ def fix_casing_ner(lines: list[str], nlp) -> list[str]:
240
+ """
241
+ 用 spaCy NER 恢复专有名词大小写。
242
+ 对每行检测 PERSON/GPE/ORG 等实体,将小写的实体名首字母大写。
243
+ """
244
+ result = list(lines) # 拷贝
245
+ batch_size = 256
246
+
247
+ t0 = time.time()
248
+ fixed_count = 0
249
+
250
+ for doc, idx in nlp.pipe(
251
+ ((line, i) for i, line in enumerate(lines)),
252
+ as_tuples=True,
253
+ batch_size=batch_size,
254
+ ):
255
+ ents = [(ent.start_char, ent.end_char, ent.label_) for ent in doc.ents
256
+ if ent.label_ in _NER_LABELS]
257
+ if not ents:
258
+ continue
259
+
260
+ line = result[idx]
261
+ need_fix = False
262
+ for start, end, label in ents:
263
+ span_text = line[start:end]
264
+ # 只修复第一个字母是小写的实体(已经正确大写的不动)
265
+ if span_text and span_text[0].islower():
266
+ need_fix = True
267
+
268
+ if need_fix:
269
+ # 从后往前修复,避免偏移量变化
270
+ for start, end, label in reversed(ents):
271
+ span_text = line[start:end]
272
+ if span_text and span_text[0].islower():
273
+ line = _apply_title_case(line, start, end)
274
+ result[idx] = line
275
+ fixed_count += 1
276
+
277
+ elapsed = time.time() - t0
278
+ speed = len(lines) / elapsed if elapsed > 0 else 0
279
+ print(f" NER: {fixed_count:,} 行修复大小写 / {len(lines):,} 行 ({speed:.0f} lines/s, {elapsed:.1f}s)")
280
+
281
+ return result
282
+
283
+
284
+ # ─────────────────────────────────────────────
285
+ # 主处理
286
+ # ─────────────────────────────────────────────
287
+
288
+ def process_file(fname: str, remove_disfluency: bool, nlp=None):
289
+ in_path = IN_DIR / fname
290
+ out_path = OUT_DIR / fname
291
+
292
+ if not in_path.exists():
293
+ print(f"[skip] {fname} 不存在")
294
+ return
295
+
296
+ cleaner_key = FILE_CLEANERS.get(fname, "passthrough")
297
+ cleaner = get_cleaner(cleaner_key, remove_disfluency)
298
+
299
+ # Pass 1: 逐行清洗
300
+ total = dropped = 0
301
+ cleaned_lines = []
302
+
303
+ with open(in_path, "r", errors="ignore") as fin:
304
+ for raw_line in tqdm(fin, desc=fname, unit="line"):
305
+ total += 1
306
+ line = raw_line.strip()
307
+ if not line:
308
+ dropped += 1
309
+ continue
310
+ result = cleaner(line)
311
+ if result is None:
312
+ dropped += 1
313
+ else:
314
+ cleaned_lines.append(result)
315
+
316
+ kept = len(cleaned_lines)
317
+ pct = kept / total * 100 if total else 0
318
+ print(f" {fname}: {total:,} → {kept:,} 保留 ({pct:.1f}%),丢弃 {dropped:,}")
319
+
320
+ # Pass 2: NER 大小写恢复(可选)
321
+ if nlp is not None:
322
+ cleaned_lines = fix_casing_ner(cleaned_lines, nlp)
323
+
324
+ # 写入
325
+ with open(out_path, "w", encoding="utf-8") as fout:
326
+ for line in cleaned_lines:
327
+ fout.write(line + "\n")
328
+
329
+ print(f" 输出: {out_path}")
330
+
331
+
332
+ def main():
333
+ parser = argparse.ArgumentParser(description="Stage 4: 格式预处理")
334
+ parser.add_argument("--file", default=None,
335
+ help="只处理指定文件(如 childes.train.txt)")
336
+ parser.add_argument("--remove_disfluency", action="store_true",
337
+ help="去除 switchboard 中的 uh/um 等口语不流利标记(默认保留)")
338
+ parser.add_argument("--ner", action="store_true",
339
+ help="启用 spaCy NER 恢复专有名词大小写(需要 en_core_web_sm)")
340
+ parser.add_argument("--input_dir", default=None)
341
+ parser.add_argument("--output_dir", default=None)
342
+ args = parser.parse_args()
343
+
344
+ global IN_DIR, OUT_DIR
345
+ if args.input_dir: IN_DIR = Path(args.input_dir)
346
+ if args.output_dir: OUT_DIR = Path(args.output_dir); OUT_DIR.mkdir(parents=True, exist_ok=True)
347
+
348
+ files = [args.file] if args.file else list(FILE_CLEANERS.keys())
349
+
350
+ # 加载 spaCy 模型(仅在 --ner 时)
351
+ nlp = None
352
+ if args.ner:
353
+ import spacy
354
+ print("加载 spaCy 模型 en_core_web_sm ...")
355
+ nlp = spacy.load("en_core_web_sm")
356
+ # 只保留 NER 组件,禁用不需要的组件以加速
357
+ nlp.select_pipes(enable=["ner"])
358
+ print(f" 已加载(仅 NER 组件)\n")
359
+
360
+ print(f"输入目录: {IN_DIR}")
361
+ print(f"输出目录: {OUT_DIR}")
362
+ print(f"Switchboard disfluency: {'去除' if args.remove_disfluency else '保留(默认)'}")
363
+ print(f"NER 大小写恢复: {'启用' if nlp else '关闭'}\n")
364
+
365
+ for fname in files:
366
+ process_file(fname, args.remove_disfluency, nlp)
367
+
368
+ print("\nStage 4 完成。")
369
+
370
+
371
+ if __name__ == "__main__":
372
+ main()
scripts/01_data/stage5_rule_label.py ADDED
@@ -0,0 +1,1076 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 规则匹配分析脚本 — 速度快,CPU 运行,免费
3
+
4
+ 三阶段改进版(Phase A + B + C):
5
+ Phase A: 从 vocab.txt 衍生词表(rule_vocab_categories.json),扩充关键词列表
6
+ Phase B: 可选 spaCy 增强(--spacy 标志),支持句法级被动/主谓一致检测
7
+ Phase C: 已对 BLiMP 真实例句校准,平均召回率 ≥ 90%
8
+
9
+ 对每句话匹配:
10
+ - BLiMP 67 个子任务(关键词 + 正则)
11
+ - BLiMP Supplement 5 个子任务
12
+ - EWoK 11 个知识域
13
+
14
+ 输出:results/analysis/rule_analysis/{stem}_rules.jsonl
15
+ 每行格式:{"line": N, "blimp": [...], "supplement": [...], "ewok": [...]}
16
+ """
17
+
18
+ import re
19
+ import json
20
+ import argparse
21
+ from pathlib import Path
22
+ from tqdm import tqdm
23
+
24
+ ROOT = Path(__file__).parent.parent.parent
25
+ DATA_DIR = ROOT / "data/4_preprocessed"
26
+ OUT_DIR = ROOT / "data/5_rule_labels"
27
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
28
+
29
+ # Vocab-derived categories (Phase A)
30
+ VOCAB_CATS_PATH = ROOT / "data/rule_vocab_categories.json"
31
+
32
+ FILES = [
33
+ "gutenberg.train.txt",
34
+ "simple_wiki.train.txt",
35
+ "bnc_spoken.train.txt",
36
+ "switchboard.train.txt",
37
+ "open_subtitles_cleaned_ckpt_70.txt",
38
+ "childes.train.txt",
39
+ "open_subtitles.train.txt",
40
+ ]
41
+
42
+
43
+ # ────────────────────────────────────────────────────────────────────
44
+ # Phase A: Load vocab-derived word lists
45
+ # ────────────────────────────────────────────────────────────────────
46
+
47
+ def _load_vocab_categories() -> dict:
48
+ """Load vocab-derived categories from rule_vocab_categories.json."""
49
+ if VOCAB_CATS_PATH.exists():
50
+ try:
51
+ with open(VOCAB_CATS_PATH, encoding="utf-8") as f:
52
+ cats = json.load(f)
53
+ print(f"[Phase A] Loaded vocab categories from {VOCAB_CATS_PATH}")
54
+ for k, v in cats.get("_counts", {}).items():
55
+ print(f" {k}: {v:,} words")
56
+ return cats
57
+ except Exception as e:
58
+ print(f"[Phase A] Could not load vocab categories: {e}")
59
+ else:
60
+ print(f"[Phase A] Vocab categories not found at {VOCAB_CATS_PATH}")
61
+ print(" Run stage5a_vocab_build.py to generate them.")
62
+ return {}
63
+
64
+
65
+ _VOCAB_CATS = _load_vocab_categories()
66
+
67
+
68
+ def _make_pattern(words: list, max_words: int = 500) -> str:
69
+ """Build regex alternation from a list of words (truncated for performance)."""
70
+ if not words:
71
+ return r"(?!)" # Never matches
72
+ sample = sorted(words)[:max_words]
73
+ return r"(?:" + "|".join(re.escape(w) for w in sample) + r")"
74
+
75
+
76
+ # Fallback word lists (used if vocab categories not available)
77
+ _FALLBACK_ANIMATE = (
78
+ r"(women?|men|girls?|boys?|child(?:ren)?|person|people|students?|teachers?|"
79
+ r"doctors?|nurses?|lawyers?|actors?|actresses?|drivers?|passengers?|"
80
+ r"pedestrians?|customers?|employees?|workers?|mothers?|fathers?|sons?|"
81
+ r"daughters?|brothers?|sisters?|friends?|bosses?|officers?|soldiers?|"
82
+ r"coaches?|presidents?|professors?|managers?|directors?|researchers?|"
83
+ r"engineers?|artists?|musicians?|singers?|writers?|authors?|journalists?|"
84
+ r"detectives?|patients?|witnesses?|tourists?|visitors?|guests?|neighbors?|"
85
+ r"colleagues?|partners?|athletes?|swimmers?|runners?|players?|judges?|"
86
+ r"senators?|cashiers?|waiters?|waitresses?|chefs?|pilots?|therapists?|"
87
+ r"physicians?|surgeons?|dentists?|teenagers?|adults?|infants?|kids?|"
88
+ r"ladies|gentlemen?|spouses?|parents?|grandparents?|grandfathers?|"
89
+ r"grandmothers?|grandsons?|granddaughters?|nieces?|nephews?|uncles?|"
90
+ r"aunts?|cousins?|siblings?|supervisors?|volunteers?|participants?|"
91
+ r"competitors?|analysts?|instructors?|reporters?|designers?|coordinators?|"
92
+ r"advisors?|counselors?|accountants?|investors?|employers?|graduates?|guys)"
93
+ )
94
+
95
+ # Build animate nouns pattern from vocab or use fallback
96
+ _animate_list = _VOCAB_CATS.get("animate_nouns", [])
97
+ if _animate_list:
98
+ ANIMATE_PAT = r"(?:" + "|".join(re.escape(w) for w in _animate_list[:600]) + r")"
99
+ else:
100
+ ANIMATE_PAT = _FALLBACK_ANIMATE
101
+
102
+ # Irregular nouns for SVA agreement
103
+ _IRR_NOUNS = (
104
+ r"(goose|geese|radius|radii|axis|axes|crisis|crises|analysis|analyses|"
105
+ r"datum|data|criterion|criteria|phenomenon|phenomena|medium|media|"
106
+ r"curriculum|curricula|bacterium|bacteria|cactus|cacti|thesis|theses|"
107
+ r"hypothesis|hypotheses|matrix|matrices|vertex|vertices|appendix|appendices|"
108
+ r"index|indices|alumnus|alumni|oasis|oases|basis|bases|diagnosis|diagnoses|"
109
+ r"synopsis|synopses|mouse|mice|louse|lice|woman|women|man|men|child|children|"
110
+ r"tooth|teeth|foot|feet|ox|oxen|die|dice|deer|sheep|fish|people|person|"
111
+ r"paralysis|fungi|fungus|stimulus|stimuli|nucleus|nuclei|octopus|syllabus|"
112
+ r"syllabi|focus|foci)"
113
+ )
114
+
115
+ # Irregular past tense forms
116
+ _IRR_PASTS = (
117
+ r"(hid|went|came|broke|wore|took|sang|knew|ate|wrote|rose|froze|chose|"
118
+ r"drove|spoke|stole|swam|drank|woke|got|ran|fell|flew|threw|grew|drew|"
119
+ r"began|bit|built|burst|bent|cut|dealt|dug|fed|felt|fled|flung|forgave|"
120
+ r"hung|held|hurt|kept|left|lost|made|met|paid|put|rang|rode|said|sank|"
121
+ r"sat|sought|shook|shone|shot|shrank|shut|slid|sped|spent|spun|spread|"
122
+ r"stood|struck|strode|swore|swung|taught|tore|trod|won|wove|wept|withdrew|"
123
+ r"read|sent|sold|slept|told|heard|saw|gave|thought|understood|"
124
+ r"brought|caught|fought|bought|sought)"
125
+ )
126
+
127
+ # Irregular past participles
128
+ _IRR_PPARTS = (
129
+ r"(broken|hidden|worn|forgotten|chosen|stolen|frozen|fallen|risen|taken|"
130
+ r"seen|heard|known|done|gone|come|grown|given|driven|written|spoken|built|"
131
+ r"bought|caught|taught|brought|found|lost|made|put|kept|held|led|left|"
132
+ r"sent|sold|sat|stood|slept|swum|sung|rung|drunk|eaten|ridden|beaten|"
133
+ r"bitten|blown|drawn|flown|shown|thrown|torn|shaken|mistaken|proven)"
134
+ )
135
+
136
+ # Causative-inchoative alternation verbs
137
+ _CAUSE_INC = (
138
+ r"(break|breaks|broke|melt|melts|melted|open|opens|opened|close|closes|"
139
+ r"closed|freeze|freezes|froze|stretch|stretches|stretched|shrink|shrinks|"
140
+ r"shrank|loosen|loosens|loosened|brighten|brightens|brightened|slow|slows|"
141
+ r"slowed|chip|chips|chipped|twist|twists|twisted|shatter|shatters|shattered|"
142
+ r"crumple|crumples|crumpled|curl|curls|curled|bend|bends|bent|crack|cracks|"
143
+ r"cracked|burn|burns|burned|condense|condenses|condensed|roll|rolls|rolled|"
144
+ r"wake|wakes|woke|hide|hides|hid|turn|turns|turned|bake|bakes|baked|"
145
+ r"soak|soaks|soaked|heal|heals|healed|train|trains|trained|reunite|drop|"
146
+ r"drops|dropped|wrinkle|wrinkles|wrinkled|collapse|collapses|collapsed|"
147
+ r"tighten|tightens|tightened|soften|softens|softened|harden|hardens|"
148
+ r"hardened|thicken|thinned|widen|narrow|dry|dried|dries|frayed|fray|"
149
+ r"fade|fades|faded|flatten|flattens|flattened|darken|darkens|darkened|"
150
+ r"warm|warms|warmed|cool|cools|cooled|heat|heats|heated|mend|mends|mended|"
151
+ r"split|splits|separate|separates|separated|merge|merges|merged|"
152
+ r"shrivel|shrivels|shriveled|wilt|wilts|wilted|swell|swells|swelled|"
153
+ r"bounce|bounces|bounced|shorten|lengthen)"
154
+ )
155
+
156
+ # Drop-argument verbs (verbs that can appear without object)
157
+ _DROP_ARG = (
158
+ r"(ate|eat|eats|drank|drink|drinks|read|reads|wrote|write|writes|cooked|"
159
+ r"cook|cooks|cleaned|clean|cleans|painted|paint|paints|drew|draw|draws|"
160
+ r"sang|sing|sings|played|play|plays|toured|tour|tours|approached|approach|"
161
+ r"approaches|descended|descend|descends|ascended|ascend|ascends|exited|"
162
+ r"exit|exits|observed|observe|observes|skated|skate|skates|climbed|climb|"
163
+ r"climbs|ran|run|runs|left|leave|leaves|remembered|remember|remembers|"
164
+ r"sold|sell|sells|helped|help|helps|watched|watch|watches|listened|listen|"
165
+ r"listens|agreed|agree|agrees|smiled|smile|smiles|apologized|apologize|"
166
+ r"arrived|arrive|arrives|managed|manage|manages|struggled|struggle|struggles|"
167
+ r"survived|survive|survives|competed|compete|competes|graduated|graduate|"
168
+ r"voted|vote|votes|retired|retire|retires|studied|study|studies|traveled|"
169
+ r"travel|travels|worked|work|works|practiced|practice|practices)"
170
+ )
171
+
172
+ # Intransitive verbs
173
+ _INTR_VERBS = (
174
+ r"(slept|sleep|sleeping|ran|run|running|walked|walk|walking|arrived|arrive|"
175
+ r"appeared|appear|disappeared|disappear|laughed|laugh|smiled|smile|cried|"
176
+ r"cry|fell|fall|yawned|yawn|shouted|shout|complained|complain|protested|"
177
+ r"protest|descended|descend|ascended|ascend|departed|depart|returned|return|"
178
+ r"stayed|stay|waited|wait|succeeded|succeed|failed|fail|danced|dance|"
179
+ r"marched|march|swam|swim|flew|fly|jumped|jump|climbed|climb|floated|"
180
+ r"float|sank|sink|bounced|bounce|struggled|struggle|survived|survive|"
181
+ r"yelled|yell|sneezed|sneeze|coughed|cough|snored|snore|stumbled|stumble|"
182
+ r"collapsed|collapse|paused|pause|hesitated|hesitate|winked|wink|nodded|"
183
+ r"nod|blushed|blush|shivered|shiver|trembled|tremble|competed|competed|"
184
+ r"collaborated|cooperated|communicated|interacted|socialized|volunteered|"
185
+ r"faded|shrunk|vanished|emerged|accelerated|decelerated|stopped|proceeded|"
186
+ r"boasted|boast|argued|argue|apologized|apologize|compromised|compromised|"
187
+ r"surrendered|murmured|whispered|screamed|scream|screaming|exercised|"
188
+ r"exercise|practiced|practice|complained|complain|rested|rest|"
189
+ r"reacted|reacted|replied|replied|concurred|competed)"
190
+ )
191
+
192
+ # Transitive verbs
193
+ _TRANS_VERBS = (
194
+ r"(saw|see|sees|liked|like|likes|knew|know|loves?|loved|hated|hate|hates|"
195
+ r"found|find|finds|noticed|notice|heard|hear|helped|help|wanted|want|"
196
+ r"alarmed|alarm|watched|watch|bought|buy|sold|sell|visited|visit|called|"
197
+ r"call|asked|ask|told|tell|used|use|criticized|criticize|praised|praise|"
198
+ r"insulted|insult|embarrassed|embarrass|described|describe|ignored|ignore|"
199
+ r"avoided|avoid|respected|respect|trusted|trust|doubted|doubt|feared|fear|"
200
+ r"envied|envy|admired|admire|questioned|question|examined|examine|"
201
+ r"investigated|investigate|analyzed|analyze|invited|invite|rejected|reject|"
202
+ r"welcomed|welcome|supported|support|attacked|attack|defended|defend|"
203
+ r"betrayed|betray|threatened|threaten|forgave|forgive|blamed|blame|"
204
+ r"thanked|thank|greeted|greet|hired|hire|fired|fire|trained|train|taught|"
205
+ r"teach|tested|test|confused|confuse|warned|warn|upset|bothered|bother|"
206
+ r"worried|worry|frightened|frighten|scared|scare|pleased|please|satisfied|"
207
+ r"satisfy|disappointed|disappoint|surprised|surprise|angered|anger|"
208
+ r"impressed|impress|amused|amuse|delighted|delight|annoyed|annoy|"
209
+ r"motivated|motivate|inspired|inspire|challenged|challenge|tricked|trick|"
210
+ r"convinced|convince|persuaded|persuade|misled|mislead|conceal|conceals|"
211
+ r"concealed|revealed|reveal|reveals|discovered|discover|discovers|"
212
+ r"remembered|remember|remembers|forgot|forget|forgets|realized|realize|"
213
+ r"realizes|understood|understand|expected|expect|chose|choose|chooses|"
214
+ r"boycotted|boycott|boycotts|hugged|hug|hugs|hired|hire|hired|"
215
+ r"discovered|discover|discovers|insulted|hates?|loves?|alarmed|alarming)"
216
+ )
217
+
218
+
219
+ # ────────────────────────────────────────────────────────────────────
220
+ # Phase B: Optional spaCy support
221
+ # ────────────────────────────────────────────────────────────────────
222
+
223
+ _SPACY_AVAILABLE = False
224
+ _NLP = None
225
+
226
+ def _init_spacy() -> bool:
227
+ """Try to load spaCy en_core_web_sm model. Return True if successful."""
228
+ global _SPACY_AVAILABLE, _NLP
229
+ try:
230
+ import spacy
231
+ _NLP = spacy.load("en_core_web_sm")
232
+ _SPACY_AVAILABLE = True
233
+ print("[Phase B] spaCy en_core_web_sm loaded successfully")
234
+ return True
235
+ except ImportError:
236
+ print("[Phase B] spaCy not installed; using regex-only mode")
237
+ except OSError:
238
+ print("[Phase B] spaCy model en_core_web_sm not found; using regex-only mode")
239
+ print(" Install with: python -m spacy download en_core_web_sm")
240
+ return False
241
+
242
+
243
+ def _spacy_analyze_batch(texts: list) -> list:
244
+ """
245
+ Analyze a batch of texts with spaCy.
246
+ Returns list of dicts with spaCy-derived features for each text.
247
+ """
248
+ if not _SPACY_AVAILABLE or _NLP is None:
249
+ return [{} for _ in texts]
250
+
251
+ results = []
252
+ for doc in _NLP.pipe(texts, batch_size=64, disable=["ner"]):
253
+ features = {
254
+ "has_auxpass": False, # passive voice
255
+ "subj_verb_agree": None, # (subj_number, verb_number) or None
256
+ "reflexive_ok": False, # reflexive binding check
257
+ }
258
+
259
+ for token in doc:
260
+ # Phase B: Passive detection via auxpass/nsubjpass dependency
261
+ if token.dep_ in ("auxpass", "nsubjpass"):
262
+ features["has_auxpass"] = True
263
+
264
+ # Phase B: Subject-verb number agreement
265
+ if token.dep_ in ("nsubj", "nsubjpass"):
266
+ subj_num = token.morph.get("Number", [""])[0]
267
+ verb = token.head
268
+ verb_num = verb.morph.get("Number", [""])[0]
269
+ if subj_num and verb_num:
270
+ features["subj_verb_agree"] = (subj_num, verb_num,
271
+ subj_num == verb_num)
272
+
273
+ # Phase B: Reflexive binding — check if reflexive has local antecedent
274
+ if token.text.lower() in ("himself", "herself", "themselves", "itself"):
275
+ # Check if there's a subject in the same clause
276
+ for anc in token.ancestors:
277
+ if anc.dep_ in ("nsubj", "nsubjpass"):
278
+ features["reflexive_ok"] = True
279
+ break
280
+
281
+ results.append(features)
282
+ return results
283
+
284
+
285
+ # ────────────────────────────────────────────────────────────────────
286
+ # Utility
287
+ # ────────────────────────────────────────────────────────────────────
288
+
289
+ def has(pattern, s, flags=re.I):
290
+ return bool(re.search(pattern, s, flags))
291
+
292
+
293
+ # ────────────────────────────────────────────────────────────────────
294
+ # BLiMP 67 子任务规则(Phase A + B 改进版)
295
+ # ──────────────────────────────────────────────────────────────���─────
296
+
297
+ BLIMP_RULES = {
298
+ # ── Anaphor Agreement(2)────────────────────────────────────────
299
+ # 反身代词性别一致:测试反身代词是否与先行词的性别匹配
300
+ # 触发:句中含 herself(女性)或 himself(男性)
301
+ # 例:She hurt herself. 她伤了自己。/ He hurt himself. 他伤了自己。
302
+ "anaphor_gender_agreement":
303
+ lambda s: has(r"\b(herself|himself)\b", s),
304
+ # 反身代词数一致:测试反身代词是否与先行词的单复数匹配
305
+ # 触发:句中含 themselves(复数)、herself/himself/itself(单数)
306
+ # 例:They blame themselves. 他们责怪自己。/ She blames herself. 她责怪自己。
307
+ "anaphor_number_agreement":
308
+ lambda s: has(r"\b(themselves|herself|himself|itself)\b", s),
309
+
310
+ # ── Argument Structure(9)───────────────────────────────────────
311
+ # 有生命主语的被动句:测试 was/were...by 被动结构,主语为有生命名词
312
+ # 触发:含被动助动词(was/were/got等)+ by 短语
313
+ # 例:The girl was praised by the teacher. 这个女孩被老师表扬了。
314
+ "animate_subject_passive":
315
+ # Phase A: expanded passive patterns including get-passives
316
+ lambda s: has(r"\b(was|were|is|are|isn.t|aren.t|wasn.t|weren.t|"
317
+ r"got|gets|get|gotten)\b.{0,40}\bby\b", s),
318
+ # 有生命主语的及物动词句:测试人/动物等有生命名词作主语的及物句
319
+ # 触发:专有名词开头(大写字母)或含有生命角色词汇(student/teacher等)
320
+ # 例:The doctor examined the patient. 医生检查了病人。
321
+ "animate_subject_trans":
322
+ # Phase A: proper name at start OR animate role noun
323
+ lambda s: (
324
+ bool(re.search(r"^[A-Z][a-z]{1,14}\b", s)) and
325
+ not bool(re.search(
326
+ r"^(The|A|An|Some|Many|Most|Every|All|These|Those|Each|No|"
327
+ r"Few|Several|This|That|Both|There|While|When|After|Before|"
328
+ r"Although|Because|If|Since|Until|As|Now|Then|Here|So|But|"
329
+ r"And|Or|However|Moreover|Therefore|Thus|Hence|Also|Yet)\b", s))
330
+ ) or has(r"\b" + ANIMATE_PAT + r"\b", s),
331
+ # 致使结构:测试 make/cause/force sb. to do sth. 等使役/致使交替动词
332
+ # 触发:含致使-非宾格交替动词(break/melt/open等)
333
+ # 例:The heat melted the ice. 热量使冰融化了。
334
+ "causative":
335
+ # Phase A: expanded causative-inchoative verb list
336
+ lambda s: has(r"\b" + _CAUSE_INC + r"\b", s),
337
+ # 论元省略:测试某些动词可以合法省略宾语的现象
338
+ # 触发:含可省略宾语的动词(ate/drank/read等)
339
+ # 例:She ate. 她吃了。/ He read. 他读了。
340
+ "drop_argument":
341
+ # Phase A: expanded optional-object verb list
342
+ lambda s: has(r"\b" + _DROP_ARG + r"\b", s),
343
+ # 非宾格动词/起变化动词:测试自发变化(无外因)的动词使用
344
+ # 触发:含表示自发状态变化的动词(broke/melted/opened等)
345
+ # 例:The window broke. 窗户碎了。/ The ice melted. 冰融化了。
346
+ "inchoative":
347
+ # Phase A: expanded change-of-state verb list
348
+ lambda s: has(r"\b" + _CAUSE_INC + r"\b", s),
349
+ # 不及物动词:测试不需要宾语的动词用法
350
+ # 触发:含不及物动词(slept/ran/walked等)
351
+ # 例:The child slept. 孩子睡着了。/ She ran. 她跑了。
352
+ "intransitive":
353
+ # Phase A: expanded intransitive verb list
354
+ lambda s: has(r"\b" + _INTR_VERBS + r"\b", s),
355
+ # 及物动词:测试需要宾语的动词用法
356
+ # 触发:含及物动词(saw/liked/knew等)
357
+ # 例:She saw the movie. 她看了电影。/ He liked the idea. 他喜欢这个想法。
358
+ "transitive":
359
+ # Phase A: expanded transitive verb list
360
+ lambda s: has(r"\b" + _TRANS_VERBS + r"\b", s),
361
+ # 简单被动句:测试 was/were/been + 过去分词结构
362
+ # 触发:含 be 动词 + 以 -ed/-en 结尾的过去分词
363
+ # 例:The letter was written. 这封信被写了。
364
+ "passive_1":
365
+ # Phase A: expanded past participle list + get passives
366
+ lambda s: has(
367
+ r"\b(was|were|been|is|are|isn.t|aren.t|wasn.t|weren.t)\b"
368
+ r".{0,35}\b\w+(?:ed|en)\b", s),
369
+ # 完成时被动句(含简单被动):测试 is/are/was/were + 过去分词的被动结构
370
+ # 触发:含 be 动词 + 以 -ed/-en 结尾的词(与 passive_1 互补)
371
+ # 例:The book is loved by many. 这本书受到很多人喜爱。
372
+ "passive_2":
373
+ # Phase A: be + past participle (short/agentless passive)
374
+ lambda s: has(
375
+ r"\b(is|are|was|were|isn.t|aren.t|wasn.t|weren.t)\b"
376
+ r".{0,30}\b\w+(?:ed|en)\b", s),
377
+
378
+ # ── Binding(7)──────────────────────────���───────────────────────
379
+ # 绑定原则A(格位1):测试嵌套从句中代词与先行词的格位关系
380
+ # 触发:含 that 引导的从句,从句中有 she/he/they/it 等宾格代词
381
+ # 例:She said that he left. 她说他离开了。
382
+ "principle_A_case_1":
383
+ # Phase B: pronoun in embedded clause after 'that'
384
+ lambda s: has(r"\bthat\b.{0,30}\b(she|he|they|it)\b", s),
385
+ # 绑定原则A(格位2):测试反身代词在宾语位置的绑定
386
+ # 触发:含 himself/herself/themselves/itself
387
+ # 例:He hurt himself. 他伤了自己。
388
+ "principle_A_case_2":
389
+ # Phase B: reflexive in object position
390
+ lambda s: has(r"\b(himself|herself|themselves|itself)\b", s),
391
+ # 绑定原则A(c统制):测试反身代词必须受其先行词 c统制
392
+ # 触发:含任意反身代词形式
393
+ # 例:The boy hurt himself. 这个男孩伤了自己。
394
+ "principle_A_c_command":
395
+ lambda s: has(r"\b(herself|himself|themselves|itself)\b", s),
396
+ # 绑定原则A(管辖域1):测试嵌套从句中宾格代词的管辖域约束
397
+ # 触发:含 that 从句 + her/him/them/it 等宾格代词
398
+ # 例:She knew that he liked her. 她知道他喜欢她。
399
+ "principle_A_domain_1":
400
+ # Phase B: object pronoun in embedded clause
401
+ lambda s: has(r"\bthat\b.{0,60}\b(her|him|them|it)\b", s),
402
+ # 绑定原则A(管辖域2):测试反身代词在嵌套主语位置的管辖域约束
403
+ # 触发:含 herself/himself/themselves/itself
404
+ # 例:She considers herself smart. 她认为自己聪明。
405
+ "principle_A_domain_2":
406
+ # Phase B: reflexive used in embedded subject (requires itself/herself/etc.)
407
+ lambda s: has(r"\b(herself|himself|themselves|itself)\b", s),
408
+ # 绑定原则A(管辖域3):测试矩阵动词后跨句反身代词的管辖域
409
+ # 触发:含感知/言说类动词 + 反身代词(两个分句结构)
410
+ # 例:She explained that he hurt himself. 她解释说他伤了自己。
411
+ "principle_A_domain_3":
412
+ # Phase B: matrix verb + reflexive (two-clause structure)
413
+ lambda s: has(
414
+ r"\b(explains?|says?|said|thinks?|thought|imagines?|sees?|saw|"
415
+ r"believes?|believed|told|tells?|watched|heard|found|knew|noticed)\b"
416
+ r".{0,60}\b(himself|herself|themselves|itself)\b", s),
417
+ # 绑定原则A(重建效应):测试分裂句(cleft)中反身代词的重建绑定
418
+ # 触发:以 It's 开头的分裂句,后跟反身代词
419
+ # 例:It's himself that he blamed. 他责怪的正是他自己。
420
+ "principle_A_reconstruction":
421
+ # Phase B: cleft structure with reflexive
422
+ lambda s: has(r"\bIt.s\b.{0,15}\b(himself|herself|themselves)\b", s),
423
+
424
+ # ── Control/Raising(5)──────────────────────────────────────────
425
+ # tough结构:测试 easy/hard/difficult...to 结构,隐含主语是动词的宾语
426
+ # 触发:含形容词(easy/hard/difficult等)+ to 不定式
427
+ # 例:This problem is easy to solve. 这个问题很容易解决。(solve的宾语是 this problem)
428
+ "tough_vs_raising_1":
429
+ lambda s: has(
430
+ r"\b(easy|hard|difficult|tough|fun|nice|pleasant|important|safe|"
431
+ r"dangerous|exciting|interesting|boring|tedious|tiring|exhausting|"
432
+ r"challenging|impossible|possible|comfortable|convenient|suitable|"
433
+ r"beneficial|natural|reasonable|appropriate|necessary|essential|"
434
+ r"helpful|harmful|risky|tricky|complicated|simple|worthwhile|"
435
+ r"desirable|acceptable|enjoyable|painful|rewarding|satisfying|"
436
+ r"frustrating|confusing|surprising|shocking|amazing|wonderful|"
437
+ r"terrible|awful|horrible|unpleasant|ridiculous|funny|amusing|"
438
+ r"entertaining|fascinating|intriguing|unusual|normal|common|rare)\b"
439
+ r".{0,20}\bto\b", s),
440
+ # 主语提升结构:测试 likely/certain/sure...to 结构,主语从补语子句提升
441
+ # 触发:含提升形容词(likely/certain/expected等)+ to 不定式
442
+ # 例:She is likely to win. 她可能会赢。(she 从补语子句提升为主句主语)
443
+ "tough_vs_raising_2":
444
+ lambda s: has(
445
+ r"\b(likely|certain|sure|apt|bound|supposed|expected|going|about|"
446
+ r"set|due|meant|destined|scheduled|unlikely|possible|impossible|"
447
+ r"willing|reluctant|hesitant|eager|anxious|afraid|ready|"
448
+ r"determined|forced|required|forbidden|allowed|permitted|"
449
+ r"guaranteed)\b.{0,20}\bto\b", s),
450
+ # there 结构主语提升:测试 There...to be 结构中的主语提升
451
+ # 触发:句子以 There 开头,含 to be 不定式
452
+ # 例:There seems to be a problem. 似乎有个问题。
453
+ "existential_there_subject_raising":
454
+ # Phase B: 'There ... to be' (subject raising)
455
+ lambda s: has(r"^There\b.{0,60}\bto\s+be\b", s),
456
+ # there 结构宾语提升:测试嵌入句中 there to be 宾语提升
457
+ # 触发:句中含 there to be 短语(非句首)
458
+ # 例:She wants there to be peace. 她希望有和平。
459
+ "existential_there_object_raising":
460
+ # Phase B: 'there to be' embedded (object raising)
461
+ lambda s: has(r"\bthere\s+to\s+be\b", s),
462
+ # it 虚主语宾语提升:测试 it to be 嵌入结构中的宾语提升
463
+ # 触发:句中含 it to be 短语
464
+ # 例:She believed it to be true. 她认为这是真的。
465
+ "expletive_it_object_raising":
466
+ # Phase B: 'it to be' embedded (expletive raising)
467
+ lambda s: has(r"\bit\s+to\s+be\b", s),
468
+
469
+ # ── Det-Noun Agreement(8)───────────────────────────────────────
470
+ # Phase A: All DET-N agreement tasks use this/that/these/those
471
+ # 限定词-名词一致(规则单复数1):测试 this/that(单数)vs these/those(复数)与名词的数一致
472
+ # 触发:含指示限定词 this/that/these/those
473
+ # 例:this book(正确)vs *these book(错误)
474
+ "determiner_noun_agreement_1":
475
+ lambda s: has(r"\b(this|that|these|those)\b", s),
476
+ # 限定词-名词一致(规则单复数2):同上,第二变体
477
+ # 触发:含指示限定词 this/that/these/those
478
+ # 例:that car(正确)vs *those car(错误)
479
+ "determiner_noun_agreement_2":
480
+ lambda s: has(r"\b(this|that|these|those)\b", s),
481
+ # 限定词-名词一致(不规则复数1):测试不规则复数名词与限定词的数一致
482
+ # 触发:含指示限定词 this/that/these/those
483
+ # 例:these children(正确)vs *this children(错误)
484
+ "determiner_noun_agreement_irregular_1":
485
+ lambda s: has(r"\b(this|that|these|those)\b", s),
486
+ # 限定词-名词一致(不规则复数2):不规则复数名词与限定词一致,第二变体
487
+ # 触发:含指示限定词 this/that/these/those
488
+ # 例:those men(正确)vs *this men(错误)
489
+ "determiner_noun_agreement_irregular_2":
490
+ lambda s: has(r"\b(this|that|these|those)\b", s),
491
+ # 限定词-名词一致(带形容词,规则1):测试形容词介入时限定词-名词数一致
492
+ # 触发:含指示限定词 this/that/these/those
493
+ # 例:these tall students(正确)vs *this tall students(错误)
494
+ "determiner_noun_agreement_with_adjective_1":
495
+ lambda s: has(r"\b(this|that|these|those)\b", s),
496
+ # 限定词-名词一致(带形容词,规则2):同上,第二变体
497
+ # 触发:含指示限定词 this/that/these|those
498
+ # 例:that old book(正确)vs *those old book(错误)
499
+ "determiner_noun_agreement_with_adj_2":
500
+ lambda s: has(r"\b(this|that|these|those)\b", s),
501
+ # 限定词-名词一致(带形容词,不规则1):形容词介入时不规则复数名词与限定词一致
502
+ # 触发:含指示限定词 this/that/these/those
503
+ # 例:these funny mice(正确)vs *this funny mice(错误)
504
+ "determiner_noun_agreement_with_adj_irregular_1":
505
+ lambda s: has(r"\b(this|that|these|those)\b", s),
506
+ # 限定词-名词一致(带形容词,不规则2):同上,第二变体
507
+ # 触发:含指示限定词 this/that/these/those
508
+ # 例:those brave men(正确)vs *this brave men(错误)
509
+ "determiner_noun_agreement_with_adj_irregular_2":
510
+ lambda s: has(r"\b(this|that|these|those)\b", s),
511
+
512
+ # ── Ellipsis(1)─────────────────────────────────────────────────
513
+ # N-bar 省略:测试用 one/ones 代替名词短语(N-bar)的省略现象
514
+ # 触发:含 one/ones + 修饰性介词/关系词(that/which/who/with等)
515
+ # 例:I want the big one. 我想要那个大的。/ She bought a red one. 她买了个红色的。
516
+ "ellipsis_n_bar_2":
517
+ lambda s: has(r"\b(one|ones)\b.{0,20}\b(that|which|who|with|without|from|in|of)\b", s),
518
+
519
+ # ── Filler-Gap(7)───────────────────────────────────────────────
520
+ # wh词与that对比(含空位):测试填充语-空位依存中 wh 词而非 that 的选择
521
+ # 触发:含感知/言说类矩阵动词 + wh 疑问词(who/what/which)
522
+ # 例:She knows who left. 她知道谁离开了。(know 后用 who 不用 that)
523
+ "wh_vs_that_with_gap":
524
+ # Phase B: matrix verb + embedded wh-clause (no 'that' complementizer)
525
+ lambda s: has(
526
+ r"\b(know|knew|knows|remember|remembered|figured|find|found|say|said|"
527
+ r"think|thought|discover|discovered|understand|understood|realize|"
528
+ r"realized|notice|noticed|ask|asked|see|saw|tell|told|reveal|revealed|"
529
+ r"conceal|concealed|investigate|investigated|learn|learned|forget|"
530
+ r"forgot)\b.{1,50}\b(who|what|which)\b", s),
531
+ # wh词与that对比(长距离空位):测试长距离填充语-空位中 wh 词后跟 that 的结构
532
+ # 触发:含 wh 词(who/what)+ 后续 that 从句
533
+ # 例:Who did she say that left? 她说谁离开了?
534
+ "wh_vs_that_with_gap_long_distance":
535
+ # Phase B: wh-word + embedded relative clause ('that')
536
+ lambda s: has(r"\b(who|what)\b.{0,80}\bthat\b", s),
537
+ # wh词与that对比(无空位):测试用 that 补语引导的宾语从句(无空位)
538
+ # 触发:含感知/认知动词 + that(直接相邻)
539
+ # 例:She knows that he left. 她知道他离开了。
540
+ "wh_vs_that_no_gap":
541
+ # Phase B: verb + 'that' complementizer
542
+ lambda s: has(
543
+ r"\b(out|investigated|know|knows|learned|forget|revealed|reveal|"
544
+ r"remember|remembered|conceal|learn|discovered|realized|discover|"
545
+ r"realize|investigate|investigates|remembers|forgotten|concealed|"
546
+ r"figured)\b.{0,5}\bthat\b", s),
547
+ # wh词与that对比(长距离无空位):长距离 that 补语从句(无空位)
548
+ # 触发:含认知/言说动词 + 较远处的 that
549
+ # 例:She discovered that the man had left. 她发现那个人已经离开了。
550
+ "wh_vs_that_no_gap_long_distance":
551
+ lambda s: has(
552
+ r"\b(out|investigated|know|knows|learned|forget|revealed|reveal|"
553
+ r"remember|remembered|conceal|learn|discovered|realized|discover|"
554
+ r"realize|investigate|investigates|remembers|forgotten|concealed|"
555
+ r"figured)\b.{0,60}\bthat\b", s),
556
+ # wh疑问句(宾语空位):测试关系从句中的宾语空位(that 引导的限制性从句)
557
+ # 触发:含限定词 + 名词短语 + that 引导的从句,后接句末标点
558
+ # 例:The book that she read was good. 她读的那本书很好。
559
+ "wh_questions_object_gap":
560
+ # Phase B: relative clause with gap (det + NP + that + ...)
561
+ lambda s: has(
562
+ r"\b(a|an|the|every|some|many|most|this|that|these|those|all|few|"
563
+ r"several)\b.{0,20}\bthat\b.{1,60}[\.\?!]$", s),
564
+ # wh疑问句(长距离主语空位):测试叠加 that 从句(相对从句套相对从句)
565
+ # 触发:句中含两个相邻的 that 从句
566
+ # 例:The man that she said that left was tall. 她说那个离开的人很高。
567
+ "wh_questions_subject_gap_long_distance":
568
+ # Phase B: double 'that' (stacked relative clauses)
569
+ lambda s: has(r"\bthat\b.{0,50}\bthat\b", s),
570
+ # 填充语-空位依存:测试 wh 词与后续关系词(that/which/who)的长距离依存
571
+ # 触发:含 wh 疑问词(who/which/what/where/when)+ 后续 that/which/who
572
+ # 例:Who did she say that she likes? 她说她喜欢谁?
573
+ "filler_gap_dependency":
574
+ lambda s: has(r"\b(who|which|what|where|when)\b.{0,80}\b(that|which|who)\b", s),
575
+
576
+ # ── Island Effects(8)───────────────────────────────────────────
577
+ # Phase B: All island effect tasks are wh-questions (start with wh-word)
578
+ # wh岛效应:测试 wh 词被内嵌 wh 疑问从句困住无法提取的句法限制
579
+ # 触发:句子以大写 Who/What/Which 开头(wh 疑问句)
580
+ # 例:*Who did she wonder what bought? wh 词被嵌套疑问句困住,无法合法提取
581
+ "wh_island":
582
+ lambda s: has(r"^(Who|What|Which)\b", s),
583
+ # 附加语岛效应:测试 wh 词被状语从句(附加语)困住无法提取的限制
584
+ # 触发:句子以大写 Who/What/Which 开头
585
+ # 例:*What did she leave because she bought? 状语从句构成提取障碍
586
+ "adjunct_island":
587
+ lambda s: has(r"^(Who|What|Which)\b", s),
588
+ # 复杂名词短语岛效应:测试 wh 词被复杂 NP(含关系从句的名词短语)困住的限制
589
+ # 触发:句子以大写 Who/What/Which 开头
590
+ # 例:*What did she meet a man who bought? 复杂 NP 内的成分无法提取
591
+ "complex_NP_island":
592
+ lambda s: has(r"^(Who|What|Which)\b", s),
593
+ # 句子主语岛效应:测试 wh 词被句子主语从句困住无法提取的限制
594
+ # 触发:句子以大写 Who/What/Which 开头
595
+ # 例:*What does that she bought surprise you? 主语从句内成分无法提取
596
+ "sentential_subject_island":
597
+ lambda s: has(r"^(Who|What|Which)\b", s),
598
+ # 左分支岛效应(简单疑问句):测试 whose 等所有格左分支成分无法单独提取
599
+ # 触发:句子以大写 Whose/Which/What 开头
600
+ # 例:*Whose did she see friend? whose 作为左分支不能单独移位
601
+ "left_branch_island_simple_question":
602
+ lambda s: has(r"^(Whose|Which|What)\b", s),
603
+ # 左分支岛效应(回声疑问句):测试带问号的回声疑问句中左分支限制
604
+ # 触发:含小写 whose/which/what + 问号
605
+ # 例:She read whose book? 她读的是谁的书?(回声疑问)
606
+ "left_branch_island_echo_question":
607
+ # Phase B: wh-word with question mark (echo question)
608
+ lambda s: has(r"\b(whose|which|what)\b.{0,40}\?$", s),
609
+ # 并列结构限制(宾语提取):测试并列结构中单一成分无法单独提取的限制
610
+ # 触发:句子以大写 Who/What/Which 开头
611
+ # 例:*What did she buy and sell the car? 并列结构中不能只从一个并列项提取
612
+ "coordinate_structure_constraint_object_extraction":
613
+ lambda s: has(r"^(Who|What|Which)\b", s),
614
+ # 并列结构限制(复杂左分支):测试含所有格的并列结构中左分支提取限制
615
+ # 触发:句子以大写 Who/What/Which/Whose 开头
616
+ # 例:*Whose did she meet and marry friend? 所有格左分支在并列结构中无法提取
617
+ "coordinate_structure_constraint_complex_left_branch":
618
+ lambda s: has(r"^(Who|What|Which|Whose)\b", s),
619
+
620
+ # ── NPI Licensing(7)────────────────────────────────────────────
621
+ # Phase C: calibrated against BLiMP — 'ever' is the key NPI
622
+ # 否定极性词许可(现在时1):测试 ever 在无许可成分语境下的不合法使用
623
+ # 触发:含焦点词 even(非 NPI),且不含 ever(区分 even vs ever)
624
+ # 例:She even tried. 她甚至尝试了。(even 合法,ever 在此语境不合法)
625
+ "npi_present_1":
626
+ # Good sentences have 'even' (focus particle) without 'ever'
627
+ lambda s: has(r"\beven\b", s) and not has(r"\bever\b", s),
628
+ # 否定极性词许可(现在时2):测试无 NPI 许可成分时 ever 不出现的合法句子
629
+ # 触发:句中不含 ever(NPI 未被触发的合法情形)
630
+ # 例:She always reads books. 她总是读书。(无 ever,合法)
631
+ "npi_present_2":
632
+ # Good sentences have no 'ever' (adverb, not NPI)
633
+ lambda s: not has(r"\bever\b", s),
634
+ # only 作为 NPI 许可成分(现在时):测试 only 在局部域内许可 ever
635
+ # 触发:含 only...ever 结构
636
+ # 例:Only she ever reads. 只有她才会读。(only 许可 ever)
637
+ "only_npi_licensor_present":
638
+ # Phase B: 'only' licenses 'ever'
639
+ lambda s: has(r"\bonly\b.{0,40}\bever\b", s),
640
+ # only 的 NPI 许可辖域:测试句首 Only 对句内 ever 的辖域许可
641
+ # 触发:句子以 Only 开头,句内含 ever
642
+ # 例:Only the teacher ever helps. 只有老师才会帮忙。
643
+ "only_npi_scope":
644
+ # Phase B: sentence starts with 'Only' + 'ever' in scope
645
+ lambda s: has(r"^Only\b.{0,100}\bever\b", s),
646
+ # 矩阵疑问句作 NPI 许可成分:测试倒装助动词疑问句对 ever 的许可
647
+ # 触发:句子以倒装助动词开头(Did/Will/Can等),句内含 ever
648
+ # 例:Did she ever read that book? 她读过那本书吗?
649
+ "matrix_question_npi_licensor_present":
650
+ # Phase B: inverted auxiliary question + 'ever'
651
+ lambda s: has(
652
+ r"^(Had|Have|Has|Did|Do|Does|Will|Would|Could|Should|Might|Can|"
653
+ r"Is|Are|Was|Were)\b.{0,40}\bever\b", s),
654
+ # 句子否定作 NPI 许可成分:测试 not 在近距离内许可 ever
655
+ # 触发:含 not...ever 结构(间隔较短)
656
+ # 例:She did not ever lie. 她从未说谎。
657
+ "sentential_negation_npi_licensor_present":
658
+ # Phase B: 'not' licenses 'ever' within short span
659
+ lambda s: has(r"\bnot\b.{0,20}\bever\b", s),
660
+ # 句子否定的 NPI 辖域:测试 not 在较长距离内对 ever 的辖域许可
661
+ # 触发:含 not...ever 结构(间隔可较长)
662
+ # 例:She would not under any circumstances ever agree. 她在任何情况下都不会同意。
663
+ "sentential_negation_npi_scope":
664
+ # Phase B: 'not' + 'ever' within longer span
665
+ lambda s: has(r"\bnot\b.{0,60}\bever\b", s),
666
+
667
+ # ── Quantifiers(4)──────────────────────────────────────────────
668
+ # 超级量化词(1):测试 more/fewer/less...than 比较量化结构
669
+ # 触发:含 more/fewer/less + than
670
+ # 例:More than five students arrived. 五名以上的学生到了。
671
+ "superlative_quantifiers_1":
672
+ lambda s: has(r"\b(more|fewer|less)\b.{0,20}\bthan\b", s),
673
+ # 超级量化词(2):测试 at most/at least 等上下界量化短语
674
+ # 触发:含 at most 或 at least
675
+ # 例:At least three people agreed. 至少有三个人同意。
676
+ "superlative_quantifiers_2":
677
+ # Phase C: calibrated — 'at most/at least'
678
+ lambda s: has(r"\b(at most|at least)\b", s),
679
+ # there 存在句中的量化词(1):测试以 There 开头的存在句(无 to be 不定式)
680
+ # 触发:句子以大写 There 开头,且不含 to be 不定式(排除提升结构)
681
+ # 例:There are many books on the shelf. 架子上有很多书。
682
+ "existential_there_quantifiers_1":
683
+ # Phase C: existential 'There' (starts with capital There, no 'to be')
684
+ lambda s: has(r"^There\b", s) and not has(r"\bto\s+be\b", s),
685
+ # there 存在句中的量化词(2):测试非句首的 there(位置状语或非存在句)
686
+ # 触发:含小写 there(非句首大写)
687
+ # 例:She went there. 她去了那里。/ Put it there. 把它放在那里。
688
+ "existential_there_quantifiers_2":
689
+ # Phase C: locative 'there' (not sentence-initial existential)
690
+ lambda s: has(r"\bthere\b", s) and not has(r"^There\b", s),
691
+
692
+ # ── Subject-Verb Agreement(6)───────────────────────────────────
693
+ # 规则复数主谓一致(1):测试复数主语与助动词/动词形式的一致
694
+ # 触发:含 is/are/was/were/has/have 等助动词,或第三人称单数动词 + 专有名词
695
+ # 例:The students are ready. 学生们已经准备好了。
696
+ "regular_plural_subject_verb_agreement_1":
697
+ # Phase A: has auxiliary verb OR 3sg verb form before proper name
698
+ lambda s: (
699
+ has(r"\b(is|are|was|were|has|have|does|do|isn.t|aren.t|wasn.t|"
700
+ r"weren.t|hasn.t|haven.t|doesn.t|don.t)\b", s) or
701
+ bool(re.search(r"\b\w+s\b.{0,30}\b[A-Z]\w+\b", s))
702
+ ),
703
+ # 规则复数主谓一致(2):测试 The + 名词短语 + 动词 结构的主谓一致
704
+ # 触发:以 The 开头,后接名词短语和动词,句末有标点
705
+ # 例:The teacher reads. 老师在读书。/ The students read. 学生们在读书。
706
+ "regular_plural_subject_verb_agreement_2":
707
+ # Phase C: calibrated — 'The + NP + V' pattern
708
+ lambda s: has(r"^The\b.{3,35}\b[a-z]+\b[\.\?!]$", s),
709
+ # 不规则复数主谓一致(1):测试不规则复数名词(men/women/children等)作主语的一致
710
+ # 触发:含不规则名词(goose/geese/man/men/child/children等)
711
+ # 例:The men are ready. 这些男人准备好了。/ The children were playing. 孩子们在玩。
712
+ "irregular_plural_subject_verb_agreement_1":
713
+ # Phase A: expanded irregular noun list
714
+ lambda s: has(r"\b" + _IRR_NOUNS + r"\b", s),
715
+ # 不规则复数主谓一致(2):同上,第二变体
716
+ # 触发:含不规则名词(data/criteria/phenomena等)
717
+ # 例:The data are clear. 数据很清晰。/ The criteria were met. 标准已达到。
718
+ "irregular_plural_subject_verb_agreement_2":
719
+ # Phase A: expanded irregular noun list
720
+ lambda s: has(r"\b" + _IRR_NOUNS + r"\b", s),
721
+ # 干扰项主谓一致(关系从句干扰):测试中间插有关系从句时主谓仍需一致
722
+ # 触发:含关系从句引导词(that/who/which)
723
+ # 例:The girl that the boys like is tall. 男孩们喜欢的那个女孩很高。(主语是 girl,动词用 is)
724
+ "distractor_agreement_relative_clause":
725
+ # Phase C: any sentence with relative clause marker (that/who/which)
726
+ lambda s: has(r"\b(that|who|which)\b", s),
727
+ # 干扰项主谓一致(关系名词干扰):测试 NP of NP 结构中的中心词动词一致
728
+ # 触发:含 a/an/the NP of NP + 助动词结构
729
+ # 例:The list of students is long. 学生名单很长。(主语中心词是 list,动词用 is)
730
+ "distractor_agreement_relational_noun":
731
+ # Phase C: 'a/the NP of NP + aux' (PP distractor)
732
+ lambda s: has(
733
+ r"\b(a|an|the)\b.{1,20}\bof\b.{0,40}"
734
+ r"\b(has|have|hasn.t|haven.t|is|are|isn.t|aren.t|was|were|"
735
+ r"wasn.t|weren.t|does|do|doesn.t|don.t|will|would|can|could|"
736
+ r"should|might)\b", s),
737
+
738
+ # ── Irregular Forms(2)──────────────────────────────────────────
739
+ # 不规则过去分词(动词用法):测试不规则动词的过去式形式
740
+ # 触发:含不规则过去式(went/came/broke/ate/wrote等)
741
+ # 例:She went to school. 她去上学了。/ He broke the window. 他打破了窗户。
742
+ "irregular_past_participle_verbs":
743
+ # Phase A: comprehensive irregular past tense list
744
+ lambda s: has(r"\b" + _IRR_PASTS + r"\b", s),
745
+ # 不规则过去分词(形容词用法):测试不规则过去分词作定语修饰名词
746
+ # 触发:含 a/an/the + 不规则过去分词(broken/hidden/written等)
747
+ # 例:a broken window 一扇破窗户 / the written report 那份书面报告
748
+ "irregular_past_participle_adjectives":
749
+ # Phase A: a/an/the + irregular past participle (used as adjective)
750
+ lambda s: has(r"\b(a|an|the)\b.{0,10}\b" + _IRR_PPARTS + r"\b", s),
751
+ }
752
+
753
+
754
+ # ────────────────────────────────────────────────────────────────────
755
+ # BLiMP Supplement 5 子任务规则
756
+ # ──────────────────────────────────────────────────────────────────���─
757
+
758
+ SUPPLEMENT_RULES = {
759
+ # 主谓倒装疑问句:测试英语一般疑问句中助动词提前(主谓倒装)结构
760
+ # 触发:句子以助动词(Did/Does/Will/Can/Has/Is/Are等)开头
761
+ # 例:Did she finish the work? 她完成工作了吗?/ Will they come? 他们会来吗?
762
+ "subject_aux_inversion":
763
+ # Phase C: sentence starts with auxiliary (inverted question)
764
+ lambda s: has(
765
+ r"^(Did|Does|Do|Will|Would|Should|Could|Can|Has|Have|Is|Are|"
766
+ r"Was|Were|Might|Must|Shall|Had)\b", s),
767
+ # 上下位关系:测试语义蕴含/逻辑推理表达(if-then、therefore、means等)
768
+ # 触发:含 if/since...then/therefore/that means/must be the case 等逻辑推导词
769
+ # 例:If it is a dog, then it is an animal. 如果它是狗,那它就是动物。
770
+ "hypernym":
771
+ # Phase C: logical/entailment language (if-then, therefore, means)
772
+ lambda s: (
773
+ has(r"\b(if|since)\b.{0,80}\b(then|therefore|means|case)\b", s) or
774
+ has(r"\btherefore\b", s) or
775
+ has(r"\bthat means\b", s) or
776
+ has(r"\bmust be the case\b", s)
777
+ ),
778
+ # 话语轮换:测试对话中回应词开头的话语轮换结构
779
+ # 触发:含 Yes/No/Yeah/Nope/Sure/Okay 后接标点(对话回应格式)
780
+ # 例:Yes, I agree. 是的,我同意。/ No, that's wrong. 不,那是错的。
781
+ "turn_taking":
782
+ # Phase C: dialogue with yes/no response
783
+ lambda s: bool(re.search(r"\b(Yes|No|Yeah|Nope|Sure|Okay)\b[,!\.]", s)),
784
+ # 问答一致性(简单):测试 wh 疑问词与其对应答案类型的匹配(单句)
785
+ # 触发:含 wh 疑问词(where/when/who/what/how/why)+ 问号
786
+ # 例:Where did she go? 她去哪了?(答案应为地点)
787
+ "qa_congruence_easy":
788
+ lambda s: has(r"\b(where|when|who|what|how|why)\b.{0,50}\?", s),
789
+ # 问答一致性(复杂):测试多行问答对中 wh 疑问词与答案内容的一致性
790
+ # 触发:含 wh 疑问词 + 问号,且文本跨越多行(含换行符)
791
+ # 例:What did she buy?\nA book. 她买了什么?\n一本书。
792
+ "qa_congruence_tricky":
793
+ # Phase C: wh-question + answer (multiline)
794
+ lambda s: (
795
+ has(r"\b(where|when|who|what|how|why)\b.{0,50}\?", s) and
796
+ "\n" in s
797
+ ),
798
+ }
799
+
800
+
801
+ # ────────────────────────────────────────────────────────────────────
802
+ # EWoK 11 知识域规则
803
+ # ────────────────────────────────────────────────────────────────────
804
+
805
+ EWOK_RULES = {
806
+ # 智能体属性:测试对有意识/有能力存在(人/动物)的属性描述
807
+ # 触发:含能力/意图类情态词(can/could/able to等)+ 认知/感知/行动动词(think/see/decide等)
808
+ # 例:The robot can learn new tasks. 这个机器人能学习新任务。
809
+ "agent-properties":
810
+ lambda s: has(
811
+ r"\b(can|could|able to|capable of|knows how|learned to|tried to|"
812
+ r"managed to|refused to|decided to|chose to|wanted to|needed to|"
813
+ r"had to|had the ability|is capable|was able)\b"
814
+ r".{0,30}\b(walk|talk|see|hear|think|feel|remember|understand|"
815
+ r"decide|move|speak|read|write|learn|communicate|recognize|respond|"
816
+ r"act|behave|know|plan|reason|judge|infer|perceive)\b", s),
817
+ # 物质动态变化:测试物质的物理/化学状态变化过程
818
+ # 触发:含状态变化动词(melt/freeze/burn/boil/dissolve等)或物态词(liquid/solid/gas等)
819
+ # 例:The ice melted in the sun. 冰在阳光下融化了。/ Water boils at 100°C. 水在100°C沸腾。
820
+ "material-dynamics":
821
+ lambda s: has(
822
+ r"\b(melt|melted|melting|freeze|froze|frozen|freezing|burn|burned|"
823
+ r"burning|burnt|boil|boiled|boiling|dissolve|dissolved|dissolving|"
824
+ r"evaporate|evaporated|evaporating|condense|condensed|condensing|"
825
+ r"solidify|solidified|oxidize|oxidized|rust|rusted|corrode|corroded|"
826
+ r"liquid|solid|gas|vapor|steam|ice|plasma)\b", s),
827
+ # 物质物理属性:测试对物质固有物理属性的描述
828
+ # 触发:含物理属性形容词(hard/soft/heavy/light/rough/smooth/flexible等)
829
+ # 例:The rock is hard. 岩石很硬。/ Glass is fragile. 玻璃易碎。
830
+ "material-properties":
831
+ lambda s: has(
832
+ r"\b(hard|soft|heavy|light|rough|smooth|transparent|opaque|"
833
+ r"flexible|rigid|brittle|elastic|magnetic|conductive|absorbent|"
834
+ r"porous|dense|viscous|flammable|waterproof|sticky|slippery|"
835
+ r"fragile|durable|resilient|malleable|ductile|soluble|insoluble)\b", s),
836
+ # 物理运动动态:测试物体的运动、力学现象及物理过程
837
+ # 触发:含运动/���学词(fall/bounce/roll/slide/float/sink等)或物理量词(gravity/friction等)
838
+ # 例:The ball fell and bounced. 球掉下去弹了起来。/ The leaf floated on water. 树叶漂在水上。
839
+ "physical-dynamics":
840
+ lambda s: has(
841
+ r"\b(fall|fell|fallen|falling|drop|dropped|bounce|bounced|roll|"
842
+ r"rolled|slide|slid|float|floated|sink|sank|push|pushed|pull|"
843
+ r"pulled|stretch|stretched|compress|compressed|accelerate|"
844
+ r"accelerated|decelerate|decelerated|collide|collided|swing|"
845
+ r"swung|spin|spun|rotate|rotated|vibrate|vibrated|oscillate|"
846
+ r"projectile|momentum|gravity|friction|force|velocity|inertia)\b", s),
847
+ # 物理交互:测试物体之间的直接物理接触与操作行为
848
+ # 触发:含物理操作动词(hit/throw/catch/kick/push/pull/lift/carry/pour等)
849
+ # 例:She placed the cup on the table. 她把杯子放在桌子上。/ He threw the ball. 他扔了球。
850
+ "physical-interactions":
851
+ lambda s: has(
852
+ r"\b(hit|struck|throw|threw|catch|caught|kick|kicked|break|broke|"
853
+ r"push|pushed|pull|pulled|lift|lifted|carry|carried|place|placed|"
854
+ r"put|move|moved|drag|dragged|roll|rolled|bounce|bounced|stack|"
855
+ r"stacked|pour|poured|fill|filled|empty|emptied|mix|mixed|collide|"
856
+ r"collided|knock|knocked|toss|tossed|fling|flung|drop|dropped)\b", s),
857
+ # 物理空间关系:测试物体之间的空间位置关系描述
858
+ # 触发:含空间介词/副词(inside/above/below/behind/between/under/over等)
859
+ # 例:The book is on top of the shelf. 书在架子上面。/ The cat is under the table. 猫在桌子下面。
860
+ "physical-relations":
861
+ lambda s: has(
862
+ r"\b(inside|outside|above|below|under|over|behind|in front of|"
863
+ r"next to|beside|between|among|on top of|beneath|within|around|"
864
+ r"near|far|close to|adjacent|opposite|across|along|through|"
865
+ r"surrounding|enclosed|embedded|attached|connected|separated|"
866
+ r"underneath|overhead|upright|horizontal|vertical|diagonal)\b", s),
867
+ # 量化比较属性:测试物体属性的量化比较(程度差异对比)
868
+ # 触发:含比较级形容词(bigger/smaller/heavier/faster等)+ than
869
+ # 例:This box is heavier than that one. 这个箱子比那个重。
870
+ "quantitative-properties":
871
+ lambda s: has(
872
+ r"\b(bigger|smaller|larger|heavier|lighter|taller|shorter|longer|"
873
+ r"wider|narrower|deeper|shallower|more|fewer|less|greater|thicker|"
874
+ r"thinner|faster|slower|higher|lower|stronger|weaker|louder|quieter|"
875
+ r"brighter|darker|hotter|colder|warmer|cooler)\b.{0,20}\bthan\b", s),
876
+ # 社会交互行为:测试人与人之间的社会性互动行为
877
+ # 触发:含社会互动动词(help/ask/thank/apologize/greet/invite/promise/argue/cooperate等)
878
+ # 例:She thanked him for his help. 她感谢他的帮助。/ They cooperated on the project. 他们合作完成了项目。
879
+ "social-interactions":
880
+ lambda s: has(
881
+ r"\b(helped|help|asked|ask|told|tell|thanked|thank|apologized|"
882
+ r"apologize|greeted|greet|invited|invite|refused|refuse|agreed|"
883
+ r"agree|disagreed|disagree|promised|promise|warned|warn|argued|"
884
+ r"argue|negotiated|negotiate|cooperated|cooperate|collaborated|"
885
+ r"collaborate|competed|compete|challenged|challenge|threatened|"
886
+ r"threaten|comforted|comfort|encouraged|encourage|persuaded|persuade|"
887
+ r"convinced|convince|trusted|trust|forgave|forgive|blamed|blame|"
888
+ r"praised|praise|criticized|criticize|supported|support|betrayed|"
889
+ r"betray|bullied|bully|welcomed|welcome|rejected|reject)\b", s),
890
+ # 社会性格属性:测试对人的性格/品德特质的描述
891
+ # 触发:含性格形容词(kind/mean/friendly/rude/generous/honest/brave等)
892
+ # 例:She is kind and generous. 她善良而慷慨。/ He was dishonest. 他不诚实。
893
+ "social-properties":
894
+ lambda s: has(
895
+ r"\b(kind|mean|friendly|rude|generous|selfish|honest|dishonest|"
896
+ r"polite|impolite|brave|cowardly|trustworthy|reliable|patient|"
897
+ r"impatient|humble|arrogant|compassionate|empathetic|sympathetic|"
898
+ r"loyal|disloyal|fair|unfair|respectful|disrespectful|helpful|"
899
+ r"harmful|caring|uncaring|gentle|harsh|warm|cold|open|closed|"
900
+ r"forgiving|vengeful|tolerant|intolerant|cooperative|competitive)\b", s),
901
+ # 社会关系:测试描述人与人之间社会角色与关系的词汇
902
+ # 触发:含社会关系名词(friend/enemy/colleague/boss/parent/teacher/doctor/neighbor等)
903
+ # 例:She is my best friend. 她是我最好的朋友。/ He is her mentor. 他是她的导师。
904
+ "social-relations":
905
+ lambda s: has(
906
+ r"\b(friend|enemy|colleague|boss|employee|parent|child|sibling|"
907
+ r"spouse|partner|teacher|student|doctor|patient|neighbor|stranger|"
908
+ r"teammate|rival|ally|opponent|mentor|apprentice|leader|follower|"
909
+ r"manager|subordinate|customer|vendor|host|guest|authority|citizen|"
910
+ r"family|relative|acquaintance|associate|advisor|protege)\b", s),
911
+ # 空间方位关系:测试方向/方位词描述物体或运动的空间方位
912
+ # 触发:含方向方位词(north/south/left/right/up/down/forward/above/below等)
913
+ # 例:Turn left at the corner. 在拐角处左转。/ The store is to the north. 商店在北面。
914
+ "spatial-relations":
915
+ lambda s: has(
916
+ r"\b(north|south|east|west|left|right|up|down|forward|backward|"
917
+ r"clockwise|counterclockwise|parallel|perpendicular|diagonal|"
918
+ r"horizontal|vertical|upward|downward|inward|outward|toward|"
919
+ r"away from|northeast|northwest|southeast|southwest|above|below|"
920
+ r"ahead|behind|sideways|upstairs|downstairs|indoors|outdoors)\b", s),
921
+ }
922
+
923
+
924
+ # ────────────────────────────────────────────────────────────────────
925
+ # 主处理函数
926
+ # ────────────────────────────────────────────────────────────────────
927
+
928
+ def analyze_sentence(text: str, spacy_features: dict = None) -> dict:
929
+ """
930
+ Apply all rules to a sentence. spacy_features (if provided) augments
931
+ some rules with dependency-parsed information.
932
+ """
933
+ blimp_matches = []
934
+ for k, fn in BLIMP_RULES.items():
935
+ try:
936
+ if fn(text):
937
+ blimp_matches.append(k)
938
+ except Exception:
939
+ pass
940
+
941
+ # Phase B: spaCy enhancement for passive and SVA rules
942
+ if spacy_features:
943
+ # Add passive if spaCy found auxpass/nsubjpass
944
+ if spacy_features.get("has_auxpass") and "passive_1" not in blimp_matches:
945
+ blimp_matches.append("passive_1_spacy")
946
+ if spacy_features.get("has_auxpass") and "animate_subject_passive" not in blimp_matches:
947
+ blimp_matches.append("animate_subject_passive_spacy")
948
+ # Add SVA based on agreement check
949
+ agree = spacy_features.get("subj_verb_agree")
950
+ if agree and agree[2]: # agreement matches
951
+ if "regular_plural_subject_verb_agreement_1" not in blimp_matches:
952
+ blimp_matches.append("regular_plural_subject_verb_agreement_1_spacy")
953
+
954
+ supplement_matches = [k for k, fn in SUPPLEMENT_RULES.items() if fn(text)]
955
+ ewok_matches = [k for k, fn in EWOK_RULES.items() if fn(text)]
956
+
957
+ return {
958
+ "blimp": blimp_matches,
959
+ "supplement": supplement_matches,
960
+ "ewok": ewok_matches,
961
+ }
962
+
963
+
964
+ def process_file(fname: str, use_spacy: bool = False):
965
+ input_path = DATA_DIR / fname
966
+ if not input_path.exists():
967
+ print(f"[skip] {fname} 不存在")
968
+ return
969
+
970
+ stem = fname.replace(".train.txt", "").replace(".txt", "")
971
+ out_path = OUT_DIR / f"{stem}_rules.jsonl"
972
+ ckpt_path = OUT_DIR / f"{stem}_rules.ckpt"
973
+
974
+ # 读已处理行数(断点续传)
975
+ done_lines = 0
976
+ if ckpt_path.exists():
977
+ done_lines = int(ckpt_path.read_text().strip())
978
+ print(f"[续传] {fname}: 已完成 {done_lines:,} 行")
979
+
980
+ total_lines = sum(1 for _ in open(input_path, errors="ignore"))
981
+
982
+ # Phase B: spaCy batch processing
983
+ spacy_batch_size = 256
984
+
985
+ with open(input_path, "r", errors="ignore") as fin, \
986
+ open(out_path, "a", encoding="utf-8") as fout:
987
+
988
+ batch_lines = [] # (line_no, text)
989
+
990
+ def flush_batch():
991
+ if not batch_lines:
992
+ return
993
+ texts = [t for _, t in batch_lines]
994
+ if use_spacy and _SPACY_AVAILABLE:
995
+ spacy_feats = _spacy_analyze_batch(texts)
996
+ else:
997
+ spacy_feats = [None] * len(texts)
998
+ for (line_no, text), sf in zip(batch_lines, spacy_feats):
999
+ result = analyze_sentence(text, sf)
1000
+ result["line"] = line_no
1001
+ fout.write(json.dumps(result, ensure_ascii=False) + "\n")
1002
+ batch_lines.clear()
1003
+
1004
+ for line_no, raw_line in enumerate(tqdm(fin, total=total_lines, desc=stem)):
1005
+ if line_no < done_lines:
1006
+ continue
1007
+ text = raw_line.strip()
1008
+ if not text:
1009
+ continue
1010
+
1011
+ if use_spacy:
1012
+ batch_lines.append((line_no, text))
1013
+ if len(batch_lines) >= spacy_batch_size:
1014
+ flush_batch()
1015
+ fout.flush()
1016
+ ckpt_path.write_text(str(line_no))
1017
+ else:
1018
+ result = analyze_sentence(text)
1019
+ result["line"] = line_no
1020
+ fout.write(json.dumps(result, ensure_ascii=False) + "\n")
1021
+
1022
+ if not use_spacy and line_no % 10000 == 0:
1023
+ fout.flush()
1024
+ ckpt_path.write_text(str(line_no))
1025
+
1026
+ if use_spacy:
1027
+ flush_batch()
1028
+
1029
+ ckpt_path.write_text(str(total_lines))
1030
+
1031
+ # 统计覆盖率
1032
+ blimp_hits = supp_hits = ewok_hits = total = 0
1033
+ with open(out_path) as f:
1034
+ for line in f:
1035
+ d = json.loads(line)
1036
+ total += 1
1037
+ if d["blimp"]: blimp_hits += 1
1038
+ if d["supplement"]: supp_hits += 1
1039
+ if d["ewok"]: ewok_hits += 1
1040
+
1041
+ print(f"\n[{fname}] 完成 {total:,} 行")
1042
+ print(f" BLiMP 命中: {blimp_hits:,} ({blimp_hits/total*100:.1f}%)")
1043
+ print(f" Supplement 命中: {supp_hits:,} ({supp_hits/total*100:.1f}%)")
1044
+ print(f" EWoK 命中: {ewok_hits:,} ({ewok_hits/total*100:.1f}%)")
1045
+ print(f" 结果: {out_path}")
1046
+
1047
+
1048
+ def main(input_dir=None, output_dir=None, use_spacy=False):
1049
+ global DATA_DIR, OUT_DIR
1050
+ if input_dir: DATA_DIR = Path(input_dir)
1051
+ if output_dir: OUT_DIR = Path(output_dir); OUT_DIR.mkdir(parents=True, exist_ok=True)
1052
+
1053
+ if use_spacy:
1054
+ _init_spacy()
1055
+
1056
+ for fname in FILES:
1057
+ process_file(fname, use_spacy=use_spacy)
1058
+
1059
+
1060
+ if __name__ == "__main__":
1061
+ parser = argparse.ArgumentParser(
1062
+ description="Stage 5: Rule-based sentence labeling (Phase A+B+C)"
1063
+ )
1064
+ parser.add_argument("--input_dir", default=None)
1065
+ parser.add_argument("--output_dir", default=None)
1066
+ parser.add_argument("--file", default=None, help="只处理指定文件")
1067
+ parser.add_argument("--spacy", action="store_true", default=False,
1068
+ help="启用 spaCy 句法增强(需要 en_core_web_sm)")
1069
+ args = parser.parse_args()
1070
+
1071
+ if args.file:
1072
+ if args.spacy:
1073
+ _init_spacy()
1074
+ process_file(args.file, use_spacy=args.spacy)
1075
+ else:
1076
+ main(args.input_dir, args.output_dir, use_spacy=args.spacy)
scripts/01_data/stage5a_vocab_build.py ADDED
@@ -0,0 +1,800 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 5A: Vocab-Derived Word Lists + BLiMP Calibration
3
+
4
+ Phase A: Build linguistic word-category lists from vocab.txt
5
+ Phase C: Calibrate rule recall against real BLiMP good sentences
6
+
7
+ Outputs:
8
+ data/rule_vocab_categories.json -- word lists for each linguistic category
9
+ data/rule_calibration.json -- per-task recall metrics
10
+
11
+ Usage:
12
+ python -m scripts.01_data.stage5a_vocab_build
13
+ python scripts/01_data/stage5a_vocab_build.py [--vocab VOCAB] [--blimp BLIMP_DIR] [--supplement SUPP_DIR]
14
+ """
15
+
16
+ import re
17
+ import json
18
+ import argparse
19
+ from pathlib import Path
20
+
21
+ ROOT = Path(__file__).parent.parent.parent
22
+ VOCAB_PATH = ROOT / "data/vocab.txt"
23
+ BLIMP_DIR = ROOT / "evaluation-pipeline-2025/evaluation_data/full_eval/blimp_filtered"
24
+ SUPP_DIR = ROOT / "evaluation-pipeline-2025/evaluation_data/full_eval/supplement_filtered"
25
+ OUT_VOCAB = ROOT / "data/rule_vocab_categories.json"
26
+ OUT_CALIB = ROOT / "data/rule_calibration.json"
27
+
28
+
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # Seed word lists (used to bootstrap vocab-derived categories)
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ SEED_ANIMATE_SINGULAR = [
34
+ # Human social roles
35
+ "woman", "man", "girl", "boy", "child", "person", "baby", "infant",
36
+ "teenager", "adult", "kid", "lady", "gentleman",
37
+ # Family
38
+ "mother", "father", "son", "daughter", "brother", "sister", "parent",
39
+ "wife", "husband", "spouse", "grandfather", "grandmother", "grandson",
40
+ "granddaughter", "niece", "nephew", "uncle", "aunt", "cousin", "sibling",
41
+ "grandparent", "ancestor", "descendant", "offspring",
42
+ # Professional roles
43
+ "teacher", "professor", "lecturer", "instructor", "student", "pupil",
44
+ "doctor", "physician", "surgeon", "nurse", "dentist", "therapist",
45
+ "pharmacist", "veterinarian", "paramedic",
46
+ "lawyer", "attorney", "judge", "senator", "congressman", "politician",
47
+ "president", "minister", "governor", "mayor", "official", "representative",
48
+ "officer", "detective", "sheriff", "guard", "soldier", "pilot", "captain",
49
+ "sailor", "marine", "general", "admiral", "sergeant",
50
+ "scientist", "researcher", "engineer", "architect", "mathematician",
51
+ "physicist", "chemist", "biologist", "geologist", "economist",
52
+ "journalist", "reporter", "editor", "publisher", "author", "writer",
53
+ "poet", "novelist", "playwright",
54
+ "artist", "painter", "sculptor", "musician", "singer", "dancer",
55
+ "actor", "actress", "director", "producer", "photographer",
56
+ "chef", "cook", "waiter", "waitress", "cashier", "clerk",
57
+ "driver", "chauffeur", "pilot", "mechanic", "plumber", "electrician",
58
+ "carpenter", "farmer", "gardener", "janitor",
59
+ "manager", "director", "boss", "employer", "employee", "worker",
60
+ "supervisor", "assistant", "secretary", "administrator", "coordinator",
61
+ "consultant", "analyst", "advisor", "counselor",
62
+ "priest", "minister", "bishop", "rabbi", "monk", "nun",
63
+ "athlete", "swimmer", "runner", "player", "champion", "competitor",
64
+ "coach", "trainer", "referee",
65
+ # Social roles
66
+ "friend", "enemy", "neighbor", "stranger", "colleague", "partner",
67
+ "customer", "patient", "victim", "witness", "suspect", "prisoner",
68
+ "tourist", "visitor", "guest", "host", "passenger", "pedestrian",
69
+ "resident", "citizen", "immigrant", "refugee",
70
+ "volunteer", "activist", "candidate", "applicant", "graduate",
71
+ # Animals (animate)
72
+ "dog", "cat", "bird", "fish", "horse", "cow", "pig", "sheep", "goat",
73
+ "rabbit", "mouse", "rat", "lion", "tiger", "bear", "wolf", "fox",
74
+ "deer", "elephant", "monkey", "gorilla", "snake", "turtle", "frog",
75
+ "eagle", "owl", "hawk", "duck", "chicken", "turkey", "whale", "dolphin",
76
+ "shark", "seal", "penguin", "parrot", "crow", "pigeon", "sparrow",
77
+ "squirrel", "chipmunk", "beaver", "otter", "badger", "hedgehog",
78
+ "camel", "giraffe", "zebra", "hippo", "rhino", "cheetah", "leopard",
79
+ "panther", "jaguar", "kangaroo", "koala", "panda", "polar",
80
+ ]
81
+
82
+ SEED_TRANSITIVE_VERBS = [
83
+ # Perception
84
+ "see", "saw", "seen", "watch", "watched", "observe", "observed",
85
+ "notice", "noticed", "hear", "heard", "feel", "felt", "taste", "smell",
86
+ "look",
87
+ # Cognition
88
+ "know", "knew", "known", "think", "thought", "believe", "believed",
89
+ "understand", "understood", "remember", "remembered", "forget", "forgot",
90
+ "forgotten", "realize", "realized", "imagine", "imagined", "consider",
91
+ "considered", "expect", "expected", "assume", "assumed", "suppose",
92
+ "supposed", "guess", "guessed", "recognize", "recognized",
93
+ # Communication
94
+ "say", "said", "tell", "told", "ask", "asked", "answer", "answered",
95
+ "show", "showed", "shown", "describe", "described", "explain", "explained",
96
+ "mention", "mentioned", "report", "reported", "announce", "announced",
97
+ "claim", "claimed", "state", "stated", "suggest", "suggested",
98
+ "recommend", "recommended", "propose", "proposed", "deny", "denied",
99
+ "confirm", "confirmed", "admit", "admitted", "acknowledge", "acknowledged",
100
+ "warn", "warned", "promise", "promised", "inform", "informed",
101
+ # Social/emotional
102
+ "help", "helped", "love", "loved", "hate", "hated", "like", "liked",
103
+ "want", "wanted", "need", "needed", "care", "cared", "trust", "trusted",
104
+ "respect", "respected", "admire", "admired", "fear", "feared",
105
+ "enjoy", "enjoyed", "prefer", "preferred", "support", "supported",
106
+ "protect", "protected", "defend", "defended", "attack", "attacked",
107
+ "criticize", "criticized", "praise", "praised", "thank", "thanked",
108
+ "apologize", "blame", "blamed", "forgive", "forgave", "forgiven",
109
+ "invite", "invited", "greet", "greeted", "welcome", "welcomed",
110
+ "reject", "rejected", "ignore", "ignored", "avoid", "avoided",
111
+ "betray", "betrayed", "threaten", "threatened", "convince", "convinced",
112
+ "persuade", "persuaded", "encourage", "encouraged", "motivate", "motivated",
113
+ "inspire", "inspired", "challenge", "challenged", "impress", "impressed",
114
+ "surprise", "surprised", "shock", "shocked", "amaze", "amazed",
115
+ "annoy", "annoyed", "upset", "bother", "bothered", "worry", "worried",
116
+ "frighten", "frightened", "scare", "scared", "please", "pleased",
117
+ "satisfy", "satisfied", "disappoint", "disappointed", "confuse", "confused",
118
+ "bore", "bored", "excite", "excited", "delight", "delighted",
119
+ # Physical actions
120
+ "carry", "carried", "lift", "lifted", "move", "moved", "push", "pushed",
121
+ "pull", "pulled", "throw", "threw", "thrown", "catch", "caught",
122
+ "hold", "held", "take", "took", "taken", "give", "gave", "given",
123
+ "bring", "brought", "send", "sent", "pass", "passed", "put", "placed",
124
+ "drop", "dropped", "pick", "grab", "grabbed", "touch", "touched",
125
+ "hit", "hurt", "kick", "kicked", "punch", "punched", "push",
126
+ "break", "broke", "broken", "build", "built", "create", "created",
127
+ "make", "made", "destroy", "destroyed", "damage", "damaged",
128
+ "repair", "repaired", "fix", "fixed", "open", "opened", "close", "closed",
129
+ "fill", "filled", "empty", "emptied", "cover", "covered", "uncover",
130
+ "hide", "hid", "hidden", "reveal", "revealed", "find", "found",
131
+ "lose", "lost", "use", "used", "keep", "kept", "leave", "left",
132
+ # Economic/transactional
133
+ "buy", "bought", "sell", "sold", "pay", "paid", "earn", "earned",
134
+ "spend", "spent", "hire", "hired", "fire", "fired", "train", "trained",
135
+ "teach", "taught", "learn", "learned", "study", "studied", "read", "wrote",
136
+ # Others
137
+ "visit", "visited", "meet", "met", "call", "called", "contact",
138
+ "check", "checked", "test", "tested", "examine", "examined",
139
+ "investigate", "investigated", "analyze", "analyzed", "compare", "compared",
140
+ "measure", "measured", "count", "counted", "include", "included",
141
+ "involve", "involved", "affect", "affected", "change", "changed",
142
+ "improve", "improved", "increase", "increased", "reduce", "reduced",
143
+ "develop", "developed", "produce", "produced", "plan", "planned",
144
+ "organize", "organized", "manage", "managed", "control", "controlled",
145
+ "lead", "led", "follow", "followed", "choose", "chose", "chosen",
146
+ "decide", "decided", "complete", "completed", "finish", "finished",
147
+ "start", "started", "begin", "began", "begun", "achieve", "achieved",
148
+ "win", "won", "lose", "lost", "defeat", "defeated", "beat", "beaten",
149
+ "save", "saved", "rescue", "rescued", "provide", "provided",
150
+ "offer", "offered", "receive", "received", "accept", "accepted",
151
+ "refuse", "refused", "collect", "collected", "gather", "gathered",
152
+ ]
153
+
154
+ SEED_INTRANSITIVE_VERBS = [
155
+ # Motion
156
+ "walk", "run", "ran", "fly", "flew", "swim", "swam", "jump", "climb",
157
+ "crawl", "march", "dance", "wander", "stroll", "jog", "sprint", "rush",
158
+ "hurry", "travel", "move", "go", "went", "come", "came", "arrive",
159
+ "depart", "leave", "return", "ascend", "descend", "rise", "rose",
160
+ "fall", "fell", "float", "sink", "slide", "roll", "bounce", "tumble",
161
+ # State/bodily
162
+ "sleep", "slept", "rest", "wake", "woke", "sit", "sat", "stand",
163
+ "stood", "lie", "lay", "exist", "live", "die", "survive",
164
+ # Expression
165
+ "laugh", "cry", "smile", "frown", "shout", "yell", "scream", "whisper",
166
+ "speak", "talk", "yawn", "sneeze", "cough", "sigh", "groan", "moan",
167
+ "blush", "tremble", "shiver", "shudder", "wink", "nod", "blink",
168
+ # Activity
169
+ "work", "play", "exercise", "practice", "train", "compete", "perform",
170
+ "participate", "cooperate", "collaborate", "communicate", "interact",
171
+ "socialize", "volunteer", "protest", "complain", "argue", "debate",
172
+ "agree", "disagree", "hesitate", "pause", "wait", "stay", "remain",
173
+ # Change of state (also inchoative)
174
+ "fade", "shrink", "grow", "expand", "collapse", "appear", "disappear",
175
+ "emerge", "vanish", "accelerate", "decelerate", "stop", "begin",
176
+ "end", "continue", "proceed", "progress",
177
+ # Social
178
+ "succeed", "fail", "win", "lose", "graduate", "retire", "resign",
179
+ "quit", "cope", "struggle", "suffer", "benefit", "profit",
180
+ "respond", "react", "object", "consent", "concur", "abstain",
181
+ "boast", "confess", "apologize", "compromise", "surrender",
182
+ ]
183
+
184
+ SEED_CAUSATIVE_INCHOATIVE = [
185
+ # Core causative-inchoative alternation verbs
186
+ "break", "broke", "broken", "melt", "melted", "open", "opened",
187
+ "close", "closed", "freeze", "froze", "frozen", "dry", "dried",
188
+ "crack", "cracked", "shatter", "shattered", "dissolve", "dissolved",
189
+ "stretch", "stretched", "shrink", "shrank", "shrunk", "loosen", "loosened",
190
+ "tighten", "tightened", "brighten", "brightened", "darken", "darkened",
191
+ "soften", "softened", "harden", "hardened", "thicken", "thickened",
192
+ "thin", "thinned", "widen", "widened", "narrow", "narrowed",
193
+ "bend", "bent", "twist", "twisted", "curl", "curled", "flatten",
194
+ "flattened", "crumple", "crumpled", "fold", "folded", "unfold", "unfolded",
195
+ "wrinkle", "wrinkled", "chip", "chipped", "burn", "burned", "burnt",
196
+ "boil", "boiled", "cool", "cooled", "heat", "heated", "warm", "warmed",
197
+ "condense", "condensed", "evaporate", "evaporated", "solidify", "solidified",
198
+ "drop", "dropped", "roll", "rolled", "slide", "slid", "spin", "spun",
199
+ "turn", "turned", "rotate", "rotated", "swing", "swung", "rock", "rocked",
200
+ "shake", "shook", "shaken", "wake", "woke", "woken", "collapse",
201
+ "collapsed", "hide", "hid", "hidden", "soak", "soaked", "heal", "healed",
202
+ "slow", "slowed", "speed", "sped", "fill", "filled", "empty", "emptied",
203
+ "split", "reunite", "reunited", "separate", "separated", "combine",
204
+ "combined", "mix", "mixed", "blend", "blended",
205
+ ]
206
+
207
+ SEED_PAST_PARTICIPLES = [
208
+ # Irregular past participles (for adjective use)
209
+ "broken", "hidden", "worn", "forgotten", "chosen", "stolen", "frozen",
210
+ "fallen", "risen", "taken", "seen", "heard", "known", "done", "gone",
211
+ "come", "grown", "given", "driven", "written", "spoken", "built",
212
+ "bought", "caught", "taught", "brought", "found", "lost", "made",
213
+ "put", "kept", "held", "led", "left", "read", "sent", "sold",
214
+ "sat", "stood", "slept", "swum", "sung", "rung", "drunk", "eaten",
215
+ "ridden", "beaten", "bitten", "blown", "drawn", "flown", "shown",
216
+ "thrown", "torn", "woven", "chosen", "frozen", "shaken", "taken",
217
+ "mistaken", "forsaken", "proven", "woven", "striven", "risen",
218
+ # Common -ed past participles used as adjectives
219
+ "confused", "insulted", "astounded", "hired", "scared", "distracted",
220
+ "bored", "excited", "interested", "tired", "embarrassed", "annoyed",
221
+ "frustrated", "satisfied", "disappointed", "surprised", "shocked",
222
+ "worried", "stressed", "relaxed", "pleased", "delighted", "amused",
223
+ "amazed", "inspired", "motivated", "encouraged", "convinced",
224
+ "trained", "educated", "experienced", "qualified", "skilled",
225
+ "wounded", "injured", "damaged", "fixed", "repaired", "cleaned",
226
+ "prepared", "organized", "informed", "warned", "protected",
227
+ ]
228
+
229
+
230
+ # ─────────────────────────────────────────────────────────────────────────────
231
+ # Category builder: load vocab.txt and classify words
232
+ # ─────────────────────────────────────────────────────────────────────────────
233
+
234
+ def load_vocab(vocab_path: Path) -> set:
235
+ """Load all words from vocab.txt (lowercase, stripped)."""
236
+ if not vocab_path.exists():
237
+ print(f"[warn] vocab.txt not found at {vocab_path}")
238
+ return set()
239
+ with open(vocab_path, encoding="utf-8", errors="ignore") as f:
240
+ return {line.strip().lower() for line in f if line.strip()}
241
+
242
+
243
+ def build_categories(vocab: set) -> dict:
244
+ """
245
+ Build word-category lists.
246
+ Strategy: take seed lists, add morphological variants found in vocab.
247
+ """
248
+ def expand_with_morphology(seeds: list, vocab: set) -> list:
249
+ """Add inflected forms of seed words that appear in vocab."""
250
+ result = set(w.lower() for w in seeds)
251
+ for w in list(seeds):
252
+ w = w.lower()
253
+ base = re.sub(r'(?:ed|ing|s|es|er|est)$', '', w)
254
+ # Try common suffixes
255
+ for suffix in ['', 's', 'es', 'ed', 'ing', 'er', 'ers', 'ess', 'esses']:
256
+ candidate = base + suffix
257
+ if candidate in vocab and len(candidate) >= 3:
258
+ result.add(candidate)
259
+ # Irregular plural
260
+ if w.endswith('man'):
261
+ result.add(w[:-3] + 'men')
262
+ elif w.endswith('woman'):
263
+ result.add(w[:-5] + 'women')
264
+ elif w.endswith('child'):
265
+ result.add('children')
266
+ elif w.endswith('person'):
267
+ result.add('people')
268
+ return sorted(result & vocab) if vocab else sorted(result)
269
+
270
+ # Build animate nouns list (singular + plural)
271
+ animate = set(w.lower() for w in SEED_ANIMATE_SINGULAR)
272
+ # Add plurals
273
+ for w in list(animate):
274
+ for plural in [w + 's', w + 'es']:
275
+ if plural in vocab:
276
+ animate.add(plural)
277
+ # Special irregular plurals
278
+ animate.update(['men', 'women', 'children', 'people', 'mice', 'geese',
279
+ 'teeth', 'feet', 'oxen', 'alumni', 'cacti', 'fungi',
280
+ 'guys', 'folks', 'teens', 'kids', 'ladies', 'gentlemen'])
281
+ animate = sorted(animate & vocab) if vocab else sorted(animate)
282
+
283
+ # Build transitive verbs list
284
+ trans = expand_with_morphology(SEED_TRANSITIVE_VERBS, vocab)
285
+
286
+ # Build intransitive verbs list
287
+ intrans = expand_with_morphology(SEED_INTRANSITIVE_VERBS, vocab)
288
+
289
+ # Build causative/inchoative verbs list
290
+ causative = expand_with_morphology(SEED_CAUSATIVE_INCHOATIVE, vocab)
291
+
292
+ # Build past participles list (for passive and adjective use)
293
+ pparts = set(w.lower() for w in SEED_PAST_PARTICIPLES)
294
+ # Add words ending in -en or -ed from vocab that look like past participles
295
+ for w in vocab:
296
+ if (re.match(r'^[a-z]{4,}en$', w) or
297
+ re.match(r'^[a-z]{4,}ed$', w)):
298
+ pparts.add(w)
299
+ pparts = sorted(pparts & vocab) if vocab else sorted(pparts)
300
+
301
+ # Build causative seed verbs (separate list for causative rule)
302
+ causative_triggers = [
303
+ "make", "made", "cause", "caused", "force", "forced", "get", "got",
304
+ "have", "had", "let", "help", "helped", "ask", "asked", "convince",
305
+ "convinced", "persuade", "persuaded", "allow", "allowed", "permit",
306
+ "permitted", "enable", "enabled", "encourage", "encouraged",
307
+ "compel", "compelled", "oblige", "obliged", "require", "required",
308
+ "order", "ordered", "tell", "told", "instruct", "instructed",
309
+ ]
310
+ causative_triggers = sorted(set(causative_triggers) & vocab) if vocab else causative_triggers
311
+
312
+ return {
313
+ "animate_nouns": animate,
314
+ "transitive_verbs": trans,
315
+ "intransitive_verbs": intrans,
316
+ "causative_inchoative_verbs": causative,
317
+ "causative_trigger_verbs": causative_triggers,
318
+ "past_participles": pparts,
319
+ # Metadata
320
+ "_counts": {
321
+ "animate_nouns": len(animate),
322
+ "transitive_verbs": len(trans),
323
+ "intransitive_verbs": len(intrans),
324
+ "causative_inchoative_verbs": len(causative),
325
+ "causative_trigger_verbs": len(causative_triggers),
326
+ "past_participles": len(pparts),
327
+ }
328
+ }
329
+
330
+
331
+ # ─────────────────────────────────────────────────────────────────────────────
332
+ # Phase C: BLiMP calibration
333
+ # ─────────────────────────────────────────────────────────────────────────────
334
+
335
+ def load_blimp_sentences(blimp_dir: Path, supp_dir: Path) -> dict:
336
+ """Load good sentences for each subtask."""
337
+ tasks = {}
338
+
339
+ for jsonl_file in sorted(blimp_dir.glob("*.jsonl")):
340
+ task_name = jsonl_file.stem
341
+ sentences = []
342
+ try:
343
+ with open(jsonl_file, encoding="utf-8") as f:
344
+ for line in f:
345
+ d = json.loads(line)
346
+ sentences.append(d["sentence_good"])
347
+ except Exception as e:
348
+ print(f"[warn] Could not load {jsonl_file}: {e}")
349
+ tasks[task_name] = sentences
350
+
351
+ for jsonl_file in sorted(supp_dir.glob("*.jsonl")):
352
+ task_name = "supplement_" + jsonl_file.stem
353
+ sentences = []
354
+ try:
355
+ with open(jsonl_file, encoding="utf-8") as f:
356
+ for line in f:
357
+ d = json.loads(line)
358
+ sentences.append(d["sentence_good"])
359
+ except Exception as e:
360
+ print(f"[warn] Could not load {jsonl_file}: {e}")
361
+ tasks[task_name] = sentences
362
+
363
+ return tasks
364
+
365
+
366
+ def build_calibration_rules() -> dict:
367
+ """
368
+ Build the same rule set as stage5_rule_label.py (improved version)
369
+ for calibration purposes.
370
+ """
371
+
372
+ def has(pattern, s, flags=re.I):
373
+ return bool(re.search(pattern, s, flags))
374
+
375
+ ANIMATE = (
376
+ r"(women?|men|girls?|boys?|child(?:ren)?|person|people|students?|teachers?|"
377
+ r"doctors?|nurses?|lawyers?|actors?|actresses?|drivers?|passengers?|"
378
+ r"pedestrians?|customers?|employees?|workers?|mothers?|fathers?|sons?|"
379
+ r"daughters?|brothers?|sisters?|friends?|bosses?|officers?|soldiers?|"
380
+ r"coaches?|presidents?|professors?|managers?|directors?|researchers?|"
381
+ r"engineers?|artists?|musicians?|singers?|writers?|authors?|journalists?|"
382
+ r"detectives?|patients?|witnesses?|tourists?|visitors?|guests?|neighbors?|"
383
+ r"colleagues?|partners?|athletes?|swimmers?|runners?|players?|judges?|"
384
+ r"senators?|cashiers?|waiters?|waitresses?|chefs?|pilots?|therapists?|"
385
+ r"physicians?|surgeons?|dentists?|teenagers?|adults?|infants?|kids?|"
386
+ r"ladies|gentlemen?|spouses?|parents?|grandparents?|grandfathers?|"
387
+ r"grandmothers?|grandsons?|granddaughters?|nieces?|nephews?|uncles?|"
388
+ r"aunts?|cousins?|siblings?|supervisors?|candidates?|activists?|"
389
+ r"volunteers?|administrators?|consultants?|assistants?|associates?|"
390
+ r"competitors?|representatives?|analysts?|instructors?|participants?|"
391
+ r"reporters?|designers?|developers?|coordinators?|advisors?|counselors?|"
392
+ r"accountants?|investors?|employers?|graduates?|guys)"
393
+ )
394
+
395
+ IRR_NOUNS = (
396
+ r"(goose|geese|radius|radii|axis|axes|crisis|crises|analysis|analyses|"
397
+ r"datum|data|criterion|criteria|phenomenon|phenomena|medium|media|"
398
+ r"curriculum|curricula|bacterium|bacteria|cactus|cacti|thesis|theses|"
399
+ r"hypothesis|hypotheses|matrix|matrices|vertex|vertices|appendix|appendices|"
400
+ r"index|indices|alumnus|alumni|oasis|oases|basis|bases|diagnosis|diagnoses|"
401
+ r"synopsis|synopses|mouse|mice|louse|lice|woman|women|man|men|child|children|"
402
+ r"tooth|teeth|foot|feet|ox|oxen|die|dice|deer|sheep|fish|people|person|"
403
+ r"paralysis|fungi|fungus|stimulus|stimuli|nucleus|nuclei|octopus|syllabus|"
404
+ r"syllabi|focus|foci)"
405
+ )
406
+
407
+ IRR_PASTS = (
408
+ r"(hid|went|came|broke|wore|took|sang|knew|ate|wrote|rose|froze|chose|"
409
+ r"drove|spoke|stole|swam|drank|woke|got|ran|fell|flew|threw|grew|drew|"
410
+ r"began|bit|built|burst|bent|cut|dealt|dug|fed|felt|fled|flung|forgave|"
411
+ r"hung|held|hurt|kept|left|lost|made|met|paid|put|rang|rode|said|sank|"
412
+ r"sat|sought|shook|shone|shot|shrank|shut|slid|sped|spent|spun|spread|"
413
+ r"stood|struck|strode|swore|swung|taught|tore|trod|won|wove|wept|withdrew|"
414
+ r"read|sent|sold|slept|told|heard|saw|gave|thought|stood|understood|"
415
+ r"brought|caught|fought|bought|sought)"
416
+ )
417
+
418
+ IRR_PPARTS = (
419
+ r"(broken|hidden|worn|forgotten|chosen|stolen|frozen|fallen|risen|taken|"
420
+ r"seen|heard|known|done|gone|come|grown|given|driven|written|spoken|built|"
421
+ r"bought|caught|taught|brought|found|lost|made|put|kept|held|led|left|"
422
+ r"sent|sold|sat|stood|slept|swum|sung|rung|drunk|eaten|ridden|beaten|"
423
+ r"bitten|blown|drawn|flown|shown|thrown|torn|shaken|mistaken|proven)"
424
+ )
425
+
426
+ CAUSE_INC = (
427
+ r"(break|breaks|broke|melt|melts|melted|open|opens|opened|close|closes|"
428
+ r"closed|freeze|freezes|froze|stretch|stretches|stretched|shrink|shrinks|"
429
+ r"shrank|loosen|loosens|loosened|brighten|brightens|brightened|slow|slows|"
430
+ r"slowed|chip|chips|chipped|twist|twists|twisted|shatter|shatters|shattered|"
431
+ r"crumple|crumples|crumpled|curl|curls|curled|bend|bends|bent|crack|cracks|"
432
+ r"cracked|burn|burns|burned|condense|condenses|condensed|roll|rolls|rolled|"
433
+ r"wake|wakes|woke|hide|hides|hid|turn|turns|turned|bake|bakes|baked|"
434
+ r"soak|soaks|soaked|heal|heals|healed|train|trains|trained|reunite|drop|"
435
+ r"drops|dropped|wrinkle|wrinkles|wrinkled|collapse|collapses|collapsed|"
436
+ r"tighten|tightens|tightened|soften|softens|softened|harden|hardens|"
437
+ r"hardened|thicken|thinned|widen|narrow|dried|dry|dries|frayed|fray|"
438
+ r"fade|fades|faded|flatten|flattens|flattened|darken|darkens|darkened|"
439
+ r"warm|warms|warmed|cool|cools|cooled|heat|heats|heated|mend|mends|"
440
+ r"mended|split|splits|separate|separates|separated|merge|merges|merged|"
441
+ r"shrivel|shrivels|shriveled|wilt|wilts|wilted|swell|swells|swelled|"
442
+ r"ripple|ripples|rippled|bounce|bounces|bounced|stretch|shorten|lengthen)"
443
+ )
444
+
445
+ DROP_ARG = (
446
+ r"(ate|eat|eats|drank|drink|drinks|read|reads|wrote|write|writes|cooked|"
447
+ r"cook|cooks|cleaned|clean|cleans|painted|paint|paints|drew|draw|draws|"
448
+ r"sang|sing|sings|played|play|plays|toured|tour|tours|approached|approach|"
449
+ r"approaches|descended|descend|descends|ascended|ascend|ascends|exited|"
450
+ r"exit|exits|observed|observe|observes|skated|skate|skates|climbed|climb|"
451
+ r"climbs|ran|run|runs|left|leave|leaves|remembered|remember|remembers|"
452
+ r"sold|sell|sells|helped|help|helps|watched|watch|watches|listened|listen|"
453
+ r"listens|agreed|agree|agrees|smiled|smile|smiles|apologized|apologize|"
454
+ r"arrived|arrive|arrives|managed|manage|manages|struggled|struggle|struggles|"
455
+ r"survived|survive|survives|competed|compete|competes|graduated|graduate|"
456
+ r"voted|vote|votes|retired|retire|retires|studied|study|studies|traveled|"
457
+ r"travel|travels|worked|work|works|practiced|practice|practices)"
458
+ )
459
+
460
+ INTR_VERBS = (
461
+ r"(slept|sleep|sleeping|ran|run|running|walked|walk|walking|arrived|arrive|"
462
+ r"appeared|appear|disappeared|disappear|laughed|laugh|smiled|smile|cried|"
463
+ r"cry|fell|fall|yawned|yawn|shouted|shout|complained|complain|protested|"
464
+ r"protest|descended|descend|ascended|ascend|departed|depart|returned|return|"
465
+ r"stayed|stay|waited|wait|succeeded|succeed|failed|fail|danced|dance|"
466
+ r"marched|march|swam|swim|flew|fly|jumped|jump|climbed|climb|floated|"
467
+ r"float|sank|sink|bounced|bounce|struggled|struggle|survived|survive|"
468
+ r"yelled|yell|sneezed|sneeze|coughed|cough|snored|snore|stumbled|stumble|"
469
+ r"collapsed|collapse|paused|pause|hesitated|hesitate|winked|wink|nodded|"
470
+ r"nod|blushed|blush|shivered|shiver|trembled|tremble|concurred|compete|"
471
+ r"collaborated|cooperated|communicated|interacted|socialized|volunteered|"
472
+ r"faded|shrunk|vanished|emerged|accelerated|decelerated|stopped|proceeded|"
473
+ r"boasted|argued|apologized|compromised|surrendered|murmured|whispered|"
474
+ r"screamed|scream|screaming|exercises|exercised|exercise|practiced|practice)"
475
+ )
476
+
477
+ TRANS_VERBS = (
478
+ r"(saw|see|sees|liked|like|likes|knew|know|loves?|loved|hated|hate|hates|"
479
+ r"found|find|finds|noticed|notice|heard|hear|helped|help|wanted|want|"
480
+ r"alarmed|alarm|watched|watch|bought|buy|sold|sell|visited|visit|called|"
481
+ r"call|asked|ask|told|tell|used|use|criticized|criticize|praised|praise|"
482
+ r"insulted|insult|embarrassed|embarrass|described|describe|ignored|ignore|"
483
+ r"avoided|avoid|respected|respect|trusted|trust|doubted|doubt|feared|fear|"
484
+ r"envied|envy|admired|admire|questioned|question|examined|examine|"
485
+ r"investigated|investigate|analyzed|analyze|invited|invite|rejected|reject|"
486
+ r"welcomed|welcome|supported|support|attacked|attack|defended|defend|"
487
+ r"betrayed|betray|threatened|threaten|forgave|forgive|blamed|blame|"
488
+ r"thanked|thank|greeted|greet|hired|hire|fired|fire|trained|train|taught|"
489
+ r"teach|tested|test|confused|confuse|warned|warn|upset|bothered|bother|"
490
+ r"worried|worry|frightened|frighten|scared|scare|pleased|please|satisfied|"
491
+ r"satisfy|disappointed|disappoint|surprised|surprise|angered|anger|"
492
+ r"impressed|impress|amused|amuse|delighted|delight|annoyed|annoy|"
493
+ r"motivated|motivate|inspired|inspire|challenged|challenge|tricked|trick|"
494
+ r"convinced|convince|persuaded|persuade|misled|mislead|conceal|conceals|"
495
+ r"concealed|revealed|reveal|reveals|discovered|discover|discovers|"
496
+ r"remembered|remember|remembers|forgot|forget|forgets|realized|realize|"
497
+ r"realizes|understood|understand|expected|expect|chose|choose|chooses)"
498
+ )
499
+
500
+ rules = {
501
+ # Anaphor Agreement
502
+ "anaphor_gender_agreement": lambda s: has(r"\b(herself|himself)\b", s),
503
+ "anaphor_number_agreement": lambda s: has(r"\b(themselves|herself|himself|itself)\b", s),
504
+
505
+ # Argument Structure
506
+ "animate_subject_passive": lambda s: has(
507
+ r"\b(was|were|is|are|isn.t|aren.t|wasn.t|weren.t)\b.{0,40}\bby\b", s),
508
+ "animate_subject_trans": lambda s: (
509
+ bool(re.search(r"^[A-Z][a-z]{1,14}\b", s)) and
510
+ not bool(re.search(r"^(The|A|An|Some|Many|Most|Every|All|These|Those|Each|No|Few|Several|This|That|Both|There)\b", s))
511
+ ) or has(r"\b" + ANIMATE + r"\b", s),
512
+ "causative": lambda s: has(r"\b" + CAUSE_INC + r"\b", s),
513
+ "drop_argument": lambda s: has(r"\b" + DROP_ARG + r"\b", s),
514
+ "inchoative": lambda s: has(r"\b" + CAUSE_INC + r"\b", s),
515
+ "intransitive": lambda s: has(r"\b" + INTR_VERBS + r"\b", s),
516
+ "transitive": lambda s: has(r"\b" + TRANS_VERBS + r"\b", s),
517
+ "passive_1": lambda s: has(
518
+ r"\b(was|were|been|is|are|isn.t|aren.t|wasn.t|weren.t)\b.{0,35}\b\w+(?:ed|en)\b", s),
519
+ "passive_2": lambda s: has(
520
+ r"\b(is|are|was|were|isn.t|aren.t|wasn.t|weren.t)\b.{0,30}\b\w+(?:ed|en)\b", s),
521
+
522
+ # Binding
523
+ "principle_A_case_1": lambda s: has(r"\bthat\b.{0,30}\b(she|he|they|it)\b", s),
524
+ "principle_A_case_2": lambda s: has(r"\b(himself|herself|themselves|itself)\b", s),
525
+ "principle_A_c_command": lambda s: has(r"\b(herself|himself|themselves|itself)\b", s),
526
+ "principle_A_domain_1": lambda s: has(r"\bthat\b.{0,60}\b(her|him|them|it)\b", s),
527
+ "principle_A_domain_2": lambda s: has(
528
+ r"\b(herself|himself|themselves|itself)\b", s),
529
+ "principle_A_domain_3": lambda s: has(
530
+ r"\b(explains?|says?|said|thinks?|thought|imagines?|sees?|saw|believes?|"
531
+ r"believed|told|tells?|watched|heard|found|knew|noticed)\b"
532
+ r".{0,60}\b(himself|herself|themselves|itself)\b", s),
533
+ "principle_A_reconstruction": lambda s: has(
534
+ r"\bIt.s\b.{0,15}\b(himself|herself|themselves)\b", s),
535
+
536
+ # Control/Raising
537
+ "tough_vs_raising_1": lambda s: has(
538
+ r"\b(easy|hard|difficult|tough|fun|nice|pleasant|important|safe|"
539
+ r"dangerous|exciting|interesting|boring|tedious|tiring|exhausting|"
540
+ r"challenging|impossible|possible|comfortable|convenient|suitable|"
541
+ r"beneficial|natural|reasonable|appropriate|necessary|essential|"
542
+ r"helpful|harmful|risky|tricky|complicated|simple|worthwhile|"
543
+ r"desirable|acceptable|enjoyable|painful|rewarding|satisfying|"
544
+ r"frustrating|confusing|surprising|shocking|amazing|wonderful|"
545
+ r"terrible|awful|horrible|unpleasant|ridiculous|funny|amusing|"
546
+ r"entertaining|fascinating|intriguing|unusual|normal|common|rare)\b"
547
+ r".{0,20}\bto\b", s),
548
+ "tough_vs_raising_2": lambda s: has(
549
+ r"\b(likely|certain|sure|apt|bound|supposed|expected|going|about|"
550
+ r"set|due|meant|destined|scheduled|unlikely|possible|impossible|"
551
+ r"willing|reluctant|hesitant|eager|anxious|afraid|ready|"
552
+ r"determined|forced|required|forbidden|allowed|permitted|"
553
+ r"certain|unlikely|guaranteed)\b.{0,20}\bto\b", s),
554
+ "existential_there_subject_raising": lambda s: has(
555
+ r"^There\b.{0,60}\bto\s+be\b", s),
556
+ "existential_there_object_raising": lambda s: has(r"\bthere\s+to\s+be\b", s),
557
+ "expletive_it_object_raising": lambda s: has(r"\bit\s+to\s+be\b", s),
558
+
559
+ # Det-Noun Agreement (all use this/that/these/those)
560
+ "determiner_noun_agreement_1": lambda s: has(
561
+ r"\b(this|that|these|those)\b", s),
562
+ "determiner_noun_agreement_2": lambda s: has(
563
+ r"\b(this|that|these|those)\b", s),
564
+ "determiner_noun_agreement_irregular_1": lambda s: has(
565
+ r"\b(this|that|these|those)\b", s),
566
+ "determiner_noun_agreement_irregular_2": lambda s: has(
567
+ r"\b(this|that|these|those)\b", s),
568
+ "determiner_noun_agreement_with_adjective_1": lambda s: has(
569
+ r"\b(this|that|these|those)\b", s),
570
+ "determiner_noun_agreement_with_adj_2": lambda s: has(
571
+ r"\b(this|that|these|those)\b", s),
572
+ "determiner_noun_agreement_with_adj_irregular_1": lambda s: has(
573
+ r"\b(this|that|these|those)\b", s),
574
+ "determiner_noun_agreement_with_adj_irregular_2": lambda s: has(
575
+ r"\b(this|that|these|those)\b", s),
576
+
577
+ # Ellipsis
578
+ "ellipsis_n_bar_2": lambda s: has(
579
+ r"\b(one|ones)\b.{0,20}\b(that|which|who|with|without|from|in|of)\b", s),
580
+
581
+ # Filler-Gap
582
+ "wh_vs_that_with_gap": lambda s: has(
583
+ r"\b(know|knew|knows|remember|remembered|figured|find|found|say|said|"
584
+ r"think|thought|discover|discovered|understand|understood|realize|"
585
+ r"realized|notice|noticed|ask|asked|see|saw|tell|told|reveal|revealed|"
586
+ r"conceal|concealed|investigate|investigated|learn|learned|forget|"
587
+ r"forgot)\b.{1,50}\b(who|what|which)\b", s),
588
+ "wh_vs_that_with_gap_long_distance": lambda s: has(
589
+ r"\b(who|what)\b.{0,80}\bthat\b", s),
590
+ "wh_vs_that_no_gap": lambda s: has(
591
+ r"\b(out|investigated|know|knows|learned|forget|revealed|reveal|"
592
+ r"remember|remembered|conceal|learn|discovered|realized|discover|"
593
+ r"realize|investigate|investigates|remembers|forgotten|concealed|"
594
+ r"figured)\b.{0,5}\bthat\b", s),
595
+ "wh_vs_that_no_gap_long_distance": lambda s: has(
596
+ r"\b(out|investigated|know|knows|learned|forget|revealed|reveal|"
597
+ r"remember|remembered|conceal|learn|discovered|realized|discover|"
598
+ r"realize|investigate|investigates|remembers|forgotten|concealed|"
599
+ r"figured)\b.{0,60}\bthat\b", s),
600
+ "wh_questions_object_gap": lambda s: has(
601
+ r"\b(a|an|the|every|some|many|most|this|that|these|those|all|few|"
602
+ r"several)\b.{0,20}\bthat\b.{1,60}[\.\?!]$", s),
603
+ "wh_questions_subject_gap_long_distance": lambda s: has(
604
+ r"\bthat\b.{0,50}\bthat\b", s),
605
+ "filler_gap_dependency": lambda s: has(
606
+ r"\b(who|which|what|where|when)\b.{0,80}\b(that|which|who)\b", s),
607
+
608
+ # Island Effects (all are wh-questions)
609
+ "wh_island": lambda s: has(r"^(Who|What|Which)\b", s),
610
+ "adjunct_island": lambda s: has(r"^(Who|What|Which)\b", s),
611
+ "complex_NP_island": lambda s: has(r"^(Who|What|Which)\b", s),
612
+ "sentential_subject_island": lambda s: has(r"^(Who|What|Which)\b", s),
613
+ "left_branch_island_simple_question": lambda s: has(
614
+ r"^(Whose|Which|What)\b", s),
615
+ "left_branch_island_echo_question": lambda s: has(
616
+ r"\b(whose|which|what)\b.{0,30}\?$", s),
617
+ "coordinate_structure_constraint_object_extraction": lambda s: has(
618
+ r"^(Who|What|Which)\b", s),
619
+ "coordinate_structure_constraint_complex_left_branch": lambda s: has(
620
+ r"^(Who|What|Which|Whose)\b", s),
621
+
622
+ # NPI Licensing
623
+ "npi_present_1": lambda s: has(r"\beven\b", s) and not has(r"\bever\b", s),
624
+ "npi_present_2": lambda s: not has(r"\bever\b", s),
625
+ "only_npi_licensor_present": lambda s: has(r"\bonly\b.{0,40}\bever\b", s),
626
+ "only_npi_scope": lambda s: has(r"^Only\b.{0,100}\bever\b", s),
627
+ "matrix_question_npi_licensor_present": lambda s: has(
628
+ r"^(Had|Have|Has|Did|Do|Does|Will|Would|Could|Should|Might|Can|"
629
+ r"Is|Are|Was|Were)\b.{0,40}\bever\b", s),
630
+ "sentential_negation_npi_licensor_present": lambda s: has(
631
+ r"\bnot\b.{0,20}\bever\b", s),
632
+ "sentential_negation_npi_scope": lambda s: has(
633
+ r"\bnot\b.{0,60}\bever\b", s),
634
+
635
+ # Quantifiers
636
+ "superlative_quantifiers_1": lambda s: has(
637
+ r"\b(more|fewer|less)\b.{0,20}\bthan\b", s),
638
+ "superlative_quantifiers_2": lambda s: has(r"\b(at most|at least)\b", s),
639
+ "existential_there_quantifiers_1": lambda s: has(
640
+ r"^There\b", s) and not has(r"\bto\s+be\b", s),
641
+ "existential_there_quantifiers_2": lambda s: has(
642
+ r"\bthere\b", s) and not has(r"^There\b", s),
643
+
644
+ # Subject-Verb Agreement
645
+ "regular_plural_subject_verb_agreement_1": lambda s: (
646
+ has(r"\b(is|are|was|were|has|have|does|do|isn.t|aren.t|wasn.t|"
647
+ r"weren.t|hasn.t|haven.t|doesn.t|don.t)\b", s) or
648
+ bool(re.search(r"\b\w+s\b.{0,30}\b[A-Z]\w+\b", s))
649
+ ),
650
+ "regular_plural_subject_verb_agreement_2": lambda s: has(
651
+ r"^The\b.{3,35}\b[a-z]+\b[\.\?!]$", s),
652
+ "irregular_plural_subject_verb_agreement_1": lambda s: has(
653
+ r"\b" + IRR_NOUNS + r"\b", s),
654
+ "irregular_plural_subject_verb_agreement_2": lambda s: has(
655
+ r"\b" + IRR_NOUNS + r"\b", s),
656
+ "distractor_agreement_relative_clause": lambda s: has(
657
+ r"\b(that|who|which)\b", s),
658
+ "distractor_agreement_relational_noun": lambda s: has(
659
+ r"\b(a|an|the)\b.{1,20}\bof\b.{0,40}"
660
+ r"\b(has|have|hasn.t|haven.t|is|are|isn.t|aren.t|was|were|"
661
+ r"wasn.t|weren.t|does|do|doesn.t|don.t|will|would|can|could|"
662
+ r"should|might)\b", s),
663
+
664
+ # Irregular Forms
665
+ "irregular_past_participle_verbs": lambda s: has(r"\b" + IRR_PASTS + r"\b", s),
666
+ "irregular_past_participle_adjectives": lambda s: has(
667
+ r"\b(a|an|the)\b.{0,10}\b" + IRR_PPARTS + r"\b", s),
668
+ }
669
+
670
+ supp_rules = {
671
+ "supplement_subject_aux_inversion": lambda s: has(
672
+ r"^(Did|Does|Do|Will|Would|Should|Could|Can|Has|Have|Is|Are|"
673
+ r"Was|Were|Might|Must|Shall|Had)\b", s),
674
+ "supplement_hypernym": lambda s: (
675
+ has(r"\b(if|since)\b.{0,80}\b(then|therefore|means|case)\b", s) or
676
+ has(r"\btherefore\b", s) or
677
+ has(r"\bthat means\b", s) or
678
+ has(r"\bmust be the case\b", s)
679
+ ),
680
+ "supplement_turn_taking": lambda s: bool(
681
+ re.search(r"\b(Yes|No|Yeah|Nope|Sure|Okay)\b[,!\.]", s)),
682
+ "supplement_qa_congruence_easy": lambda s: has(
683
+ r"\b(where|when|who|what|how|why)\b.{0,50}\?", s),
684
+ "supplement_qa_congruence_tricky": lambda s: (
685
+ has(r"\b(where|when|who|what|how|why)\b.{0,50}\?", s) and
686
+ "\n" in s
687
+ ),
688
+ }
689
+
690
+ return {**rules, **supp_rules}
691
+
692
+
693
+ def calibrate(blimp_dir: Path, supp_dir: Path) -> dict:
694
+ """
695
+ Run all rules against BLiMP good sentences, compute recall.
696
+ """
697
+ if not blimp_dir.exists():
698
+ print(f"[warn] BLiMP directory not found: {blimp_dir}")
699
+ return {}
700
+
701
+ tasks = load_blimp_sentences(blimp_dir, supp_dir if supp_dir.exists() else Path("/nonexistent"))
702
+ rules = build_calibration_rules()
703
+
704
+ calibration = {}
705
+ for task, sentences in sorted(tasks.items()):
706
+ # Map supplement_ prefix back
707
+ rule_key = task
708
+ if rule_key not in rules:
709
+ continue
710
+ rule = rules[rule_key]
711
+ hits = sum(1 for s in sentences if rule(s))
712
+ recall = hits / len(sentences) if sentences else 0.0
713
+ calibration[task] = {
714
+ "total": len(sentences),
715
+ "hits": hits,
716
+ "recall": round(recall, 4),
717
+ }
718
+
719
+ return calibration
720
+
721
+
722
+ # ─────────────────────────────────────────────────────────────────────────────
723
+ # Main
724
+ # ─────────────────────────────────────────────────────────────────────────────
725
+
726
+ def main(vocab_path: Path = VOCAB_PATH,
727
+ blimp_dir: Path = BLIMP_DIR,
728
+ supp_dir: Path = SUPP_DIR):
729
+
730
+ print("=" * 60)
731
+ print("Stage 5A: Vocab-Derived Word Lists + BLiMP Calibration")
732
+ print("=" * 60)
733
+
734
+ # Phase A: Build vocab categories
735
+ print(f"\n[Phase A] Loading vocab from {vocab_path} ...")
736
+ vocab = load_vocab(vocab_path)
737
+ print(f" Vocab size: {len(vocab):,}")
738
+
739
+ print("[Phase A] Building linguistic categories ...")
740
+ categories = build_categories(vocab)
741
+ counts = categories["_counts"]
742
+ for k, v in counts.items():
743
+ print(f" {k}: {v:,} words")
744
+
745
+ # Save categories
746
+ OUT_VOCAB.parent.mkdir(parents=True, exist_ok=True)
747
+ with open(OUT_VOCAB, "w", encoding="utf-8") as f:
748
+ json.dump(categories, f, ensure_ascii=False, indent=2)
749
+ print(f"\n[Phase A] Saved categories to {OUT_VOCAB}")
750
+
751
+ # Phase C: BLiMP calibration
752
+ print(f"\n[Phase C] Running calibration against BLiMP ...")
753
+ if not blimp_dir.exists():
754
+ print(f" [skip] BLiMP directory not found: {blimp_dir}")
755
+ return
756
+
757
+ calib = calibrate(blimp_dir, supp_dir)
758
+
759
+ # Print calibration summary
760
+ low_recall = [(t, r) for t, r in calib.items() if r["recall"] < 0.5]
761
+ high_recall = [(t, r) for t, r in calib.items() if r["recall"] >= 0.9]
762
+
763
+ print(f"\n Tasks: {len(calib)}")
764
+ print(f" High recall (>=90%): {len(high_recall)}")
765
+ print(f" Low recall (<50%): {len(low_recall)}")
766
+
767
+ print("\n Per-task recall (sorted):")
768
+ for task, r in sorted(calib.items(), key=lambda x: x[1]["recall"]):
769
+ bar = "█" * int(r["recall"] * 20)
770
+ print(f" {task:<52} {r['recall']*100:5.1f}% {r['hits']}/{r['total']} {bar}")
771
+
772
+ # Compute averages
773
+ avg_recall = sum(r["recall"] for r in calib.values()) / len(calib) if calib else 0
774
+ print(f"\n Average recall across {len(calib)} tasks: {avg_recall*100:.1f}%")
775
+
776
+ # Save calibration
777
+ calib_out = {
778
+ "summary": {
779
+ "n_tasks": len(calib),
780
+ "avg_recall": round(avg_recall, 4),
781
+ "n_high_recall": len(high_recall),
782
+ "n_low_recall": len(low_recall),
783
+ },
784
+ "tasks": calib,
785
+ }
786
+ with open(OUT_CALIB, "w", encoding="utf-8") as f:
787
+ json.dump(calib_out, f, ensure_ascii=False, indent=2)
788
+ print(f"\n[Phase C] Saved calibration to {OUT_CALIB}")
789
+
790
+
791
+ if __name__ == "__main__":
792
+ parser = argparse.ArgumentParser(description="Stage 5A: vocab + calibration")
793
+ parser.add_argument("--vocab", default=str(VOCAB_PATH),
794
+ help="Path to vocab.txt")
795
+ parser.add_argument("--blimp", default=str(BLIMP_DIR),
796
+ help="Path to blimp_filtered/ directory")
797
+ parser.add_argument("--supplement", default=str(SUPP_DIR),
798
+ help="Path to supplement_filtered/ directory")
799
+ args = parser.parse_args()
800
+ main(Path(args.vocab), Path(args.blimp), Path(args.supplement))
scripts/01_data/stage6_qwen_label.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 补齐缺失行分析脚本
3
+ - 读取已有 JSONL,找出哪些全局行号已被正确覆盖
4
+ - 只对缺失行号重新跑 Qwen 分析
5
+ - 使用修复后的本地编号(build_prompt 已修复)
6
+
7
+ 用法:
8
+ CUDA_VISIBLE_DEVICES=0 python fill_missing_analysis.py --files simple_wiki.train.txt
9
+ CUDA_VISIBLE_DEVICES=1 python fill_missing_analysis.py --files bnc_spoken.train.txt
10
+ """
11
+
12
+ import re
13
+ import json
14
+ import argparse
15
+ from pathlib import Path
16
+ from tqdm import tqdm
17
+ from vllm import LLM, SamplingParams
18
+
19
+ ROOT = Path(__file__).parent.parent.parent
20
+ DATA_DIR = ROOT / "data/4_preprocessed"
21
+ OUT_DIR = ROOT / "data/6_qwen_labels"
22
+
23
+ MODEL_PATH = ROOT / "models/qwen3.5-9b/models--Qwen--Qwen3.5-9B/snapshots/c202236235762e1c871ad0ccb60c8ee5ba337b9a"
24
+
25
+ ALL_FILES = [
26
+ "simple_wiki.train.txt",
27
+ "bnc_spoken.train.txt",
28
+ "open_subtitles.train.txt",
29
+ "gutenberg.train.txt",
30
+ "childes.train.txt",
31
+ "open_subtitles_cleaned_ckpt_70.txt",
32
+ "switchboard.train.txt",
33
+ ]
34
+
35
+ SENTENCES_PER_PROMPT = 48
36
+ CHUNK_SIZE = 200
37
+
38
+ SYSTEM_PROMPT = """You are a linguistic data quality analyst for the BabyLM language model training challenge (Strict-Small track, ≤10M words).
39
+
40
+ For each numbered sentence, return a MINIFIED JSON object (no spaces, no newlines) with EXACTLY these 9 fields:
41
+
42
+ "id"(int): the sentence number,
43
+ "quality"(1-5): 1=garbled/nonsense/non-English, 2=poor, 3=acceptable, 4=good, 5=excellent,
44
+ "keep"(bool): true if quality>=3 AND English AND has meaningful content,
45
+ "comps"(1-3): concept property description — 1=none, 2=implicit, 3=explicit property statement like "a sock absorbs sweat" or "ice is cold",
46
+ "entity_track"(1-3): entity state tracking — 1=no entities, 2=one entity, 3=multiple entities with explicit state changes (location/ownership/physical state),
47
+ "reading_flow"(1-3): natural reading rhythm — 1=choppy/fragmented, 2=adequate, 3=smooth flowing prose suitable for surprisal prediction,
48
+ "glue"(1-3): NLU task value — 1=none, 2=implicit, 3=explicit entailment/QA/paraphrase/contradiction relationship,
49
+ "wug"(1-3): morphological patterns — 1=none, 2=regular inflection, 3=irregular or derivational forms (ran/swam/ness/tion/ate),
50
+ "aoa"(1-3): word acquisition value — 1=rare/complex vocab, 2=mixed, 3=CDI basic vocabulary in simple natural context,
51
+ "child_directed"(bool): true if child-directed speech or child speech (simple vocabulary, short sentences, nurturing/playful tone)
52
+
53
+ Return a JSON ARRAY of objects, one per sentence. ONLY the JSON array, no explanation."""
54
+
55
+
56
+ MAX_TEXT_PER_SENTENCE = 600 # 超长行截取前600字符分析(足以判断质量),避免模型输出混乱
57
+
58
+ def build_prompt(tokenizer, sentences: list[tuple[int, str]]) -> str:
59
+ """本地编号(0,1,2,...),模型返回的 id 对应 batch 内下标"""
60
+ def truncate(text):
61
+ return text[:MAX_TEXT_PER_SENTENCE] + "…" if len(text) > MAX_TEXT_PER_SENTENCE else text
62
+ input_text = "\n".join(f"{local_i}: {truncate(text)}" for local_i, (_, text) in enumerate(sentences))
63
+ messages = [
64
+ {"role": "system", "content": SYSTEM_PROMPT},
65
+ {"role": "user", "content": input_text},
66
+ ]
67
+ return tokenizer.apply_chat_template(
68
+ messages, tokenize=False, add_generation_prompt=True,
69
+ enable_thinking=False,
70
+ )
71
+
72
+
73
+ def parse_response(response: str) -> list[dict]:
74
+ response = re.sub(r"```(?:json)?", "", response).strip()
75
+ start = response.find("[")
76
+ end = response.rfind("]") + 1
77
+ if start == -1 or end == 0:
78
+ return []
79
+ try:
80
+ results = json.loads(response[start:end])
81
+ return results if isinstance(results, list) else []
82
+ except json.JSONDecodeError:
83
+ return []
84
+
85
+
86
+ def load_done_lines(jsonl_path: Path) -> set[int]:
87
+ """读取已有 JSONL,收集已覆盖的全局行号"""
88
+ done = set()
89
+ if not jsonl_path.exists():
90
+ return done
91
+ with open(jsonl_path, "r", errors="ignore") as f:
92
+ for line in f:
93
+ try:
94
+ d = json.loads(line)
95
+ line_no = d.get("line")
96
+ if line_no is not None:
97
+ done.add(int(line_no))
98
+ except:
99
+ pass
100
+ return done
101
+
102
+
103
+ def fill_file(fname: str, llm: LLM):
104
+ input_path = DATA_DIR / fname
105
+ if not input_path.exists():
106
+ print(f"[skip] {fname} 不存在")
107
+ return
108
+
109
+ stem = fname.replace(".train.txt", "").replace(".txt", "")
110
+ out_path = OUT_DIR / f"{stem}_analysis.jsonl"
111
+ fill_ckpt = OUT_DIR / f"{stem}_fill.ckpt"
112
+
113
+ # 读取全部源数据(过滤空行)
114
+ with open(input_path, "r", errors="ignore") as fin:
115
+ all_lines = [(i, l.strip()) for i, l in enumerate(fin) if l.strip()]
116
+
117
+ total = len(all_lines)
118
+
119
+ # 加载已完成的行号
120
+ done_lines = load_done_lines(out_path)
121
+
122
+ # 断点续传:如果 fill_ckpt 存在,跳过已处理的补全行
123
+ fill_done = set()
124
+ if fill_ckpt.exists():
125
+ try:
126
+ fill_done = set(json.loads(fill_ckpt.read_text()))
127
+ except:
128
+ pass
129
+ done_lines |= fill_done
130
+
131
+ # 找出缺失行
132
+ missing = [(i, t) for i, t in all_lines if i not in done_lines]
133
+ print(f"\n{fname}: 总行={total:,}, 已覆盖={len(done_lines):,}, 缺失={len(missing):,}")
134
+
135
+ # 预过滤:直接写入 keep=False,不送模型
136
+ METADATA_PREFIXES = ("= = =", "@ ", "@") # CHILDES/CHAT 元数据标记
137
+ # 书籍目录行:内容 + 大量连续空格 + 末尾页码(如 "14: THE ONION'S SPEECH 87")
138
+ TOC_PATTERN = re.compile(r'.{3,}\s{3,}\d+\s*$')
139
+ # 书籍索引行:名称 + 逗号 + 页码(如 "Green Islands, 307" 或 "Gregory, Lady, 307, 309")
140
+ INDEX_PATTERN = re.compile(r'^[A-Za-z][A-Za-z,\'\.\- ]{2,40},\s*\d[\d,\s]*$')
141
+ auto_done = []
142
+ real_missing = []
143
+ for i, t in missing:
144
+ is_metadata = any(t.startswith(p) for p in METADATA_PREFIXES) or t == "= = ="
145
+ is_toc = len(t) < 200 and bool(TOC_PATTERN.match(t))
146
+ is_index = len(t) < 100 and bool(INDEX_PATTERN.match(t))
147
+ # 脚注行:[N] 开头 或 编号列表项(如 "1. The transactions of...")
148
+ is_footnote = bool(re.match(r'^\[\d+\]', t)) or bool(re.match(r'^\d+\.\s+[A-Z]', t))
149
+ is_ultra_long = len(t) > 6000 # 超过6000字符:完整段落/布道词
150
+ if is_metadata or is_toc or is_index or is_footnote or is_ultra_long:
151
+ auto_done.append((i, t))
152
+ else:
153
+ real_missing.append((i, t))
154
+ if auto_done:
155
+ print(f" 自动标记元数据行: {len(auto_done)} 行 (keep=False,跳过模型)")
156
+ missing = real_missing
157
+
158
+ if not missing:
159
+ print(f"[完成] {fname} 无缺失行")
160
+ return
161
+
162
+ tokenizer = llm.get_tokenizer()
163
+ MAX_OUTPUT_TOKENS = SENTENCES_PER_PROMPT * 65
164
+ MAX_MODEL_LEN = 6000
165
+ MAX_INPUT_TOKENS = MAX_MODEL_LEN - MAX_OUTPUT_TOKENS - 50
166
+
167
+ sampling_params = SamplingParams(temperature=0.1, max_tokens=MAX_OUTPUT_TOKENS)
168
+
169
+ # 分批
170
+ batches = [missing[i:i+SENTENCES_PER_PROMPT]
171
+ for i in range(0, len(missing), SENTENCES_PER_PROMPT)]
172
+
173
+ errors = 0
174
+ newly_done = set(fill_done) # 本次运行新完成的行号
175
+
176
+ # 写入自动标记行
177
+ if auto_done:
178
+ with open(out_path, "a", encoding="utf-8") as fout:
179
+ for line_no, text_val in auto_done:
180
+ # 超长文本(>3000字符且非元数据):很可能是高质量文学段落,keep=True
181
+ is_long_quality = len(text_val) > 3000 and not any(
182
+ text_val.startswith(p) for p in METADATA_PREFIXES
183
+ ) and not bool(TOC_PATTERN.match(text_val)) and not bool(INDEX_PATTERN.match(text_val))
184
+ record = {
185
+ "id": 0,
186
+ "quality": 4 if is_long_quality else 1,
187
+ "keep": True if is_long_quality else False,
188
+ "comps": 1, "entity_track": 1, "reading_flow": 2 if is_long_quality else 1,
189
+ "glue": 1, "wug": 1, "aoa": 1,
190
+ "child_directed": False, "line": line_no, "text": text_val
191
+ }
192
+ fout.write(json.dumps(record, ensure_ascii=False) + "\n")
193
+ newly_done.add(line_no)
194
+ fill_ckpt.write_text(json.dumps(list(newly_done)))
195
+
196
+ with open(out_path, "a", encoding="utf-8") as fout:
197
+ for chunk_start in tqdm(range(0, len(batches), CHUNK_SIZE), desc=f"{stem} [fill]"):
198
+ chunk = batches[chunk_start: chunk_start + CHUNK_SIZE]
199
+
200
+ prompts = []
201
+ valid_chunk = []
202
+ for b in chunk:
203
+ p = build_prompt(tokenizer, b)
204
+ n_tokens = len(tokenizer.encode(p))
205
+ if n_tokens <= MAX_INPUT_TOKENS:
206
+ prompts.append(p)
207
+ valid_chunk.append(b)
208
+ else:
209
+ # 超长:依次缩减句子数重试,截断后剩余部分也逐句处理
210
+ placed = False
211
+ for try_n in [len(b)//2, len(b)//3, len(b)//4]:
212
+ if try_n < 1:
213
+ break
214
+ p2 = build_prompt(tokenizer, b[:try_n])
215
+ if len(tokenizer.encode(p2)) <= MAX_INPUT_TOKENS:
216
+ prompts.append(p2)
217
+ valid_chunk.append(b[:try_n])
218
+ placed = True
219
+ # 关键修复:剩余 b[try_n:] 的句子逐句单独提交,不再丢弃
220
+ for single in b[try_n:]:
221
+ p_single = build_prompt(tokenizer, [single])
222
+ if len(tokenizer.encode(p_single)) <= MAX_INPUT_TOKENS:
223
+ prompts.append(p_single)
224
+ valid_chunk.append([single])
225
+ break
226
+ if not placed:
227
+ # 1/4 也超长:全部逐句单独提交
228
+ for single in b:
229
+ p_single = build_prompt(tokenizer, [single])
230
+ if len(tokenizer.encode(p_single)) <= MAX_INPUT_TOKENS:
231
+ prompts.append(p_single)
232
+ valid_chunk.append([single])
233
+ chunk = valid_chunk
234
+ if not prompts:
235
+ continue
236
+
237
+ try:
238
+ outputs = llm.generate(prompts, sampling_params)
239
+ except Exception as e:
240
+ print(f"[warn] generate 失败: {e}")
241
+ errors += 1
242
+ continue
243
+
244
+ for batch, output in zip(chunk, outputs):
245
+ response = output.outputs[0].text.strip()
246
+ results = parse_response(response)
247
+
248
+ if not results:
249
+ errors += 1
250
+ continue
251
+
252
+ for r in results:
253
+ idx = r.get("id", 0)
254
+ if 0 <= idx < len(batch):
255
+ line_no, text_val = batch[idx]
256
+ r["line"] = line_no
257
+ r["text"] = text_val
258
+ fout.write(json.dumps(r, ensure_ascii=False) + "\n")
259
+ newly_done.add(line_no)
260
+
261
+ fout.flush()
262
+ # 保存进度
263
+ fill_ckpt.write_text(json.dumps(list(newly_done)))
264
+
265
+ print(f"\n[{fname}] 补全完成,新增 {len(newly_done) - len(fill_done):,} 行,错误批次: {errors}")
266
+
267
+
268
+ def main():
269
+ parser = argparse.ArgumentParser()
270
+ parser.add_argument("--files", nargs="+", default=ALL_FILES)
271
+ parser.add_argument("--gpu_memory_utilization", type=float, default=0.95)
272
+ parser.add_argument("--max_num_seqs", type=int, default=256)
273
+ args = parser.parse_args()
274
+
275
+ print(f"加载模型: {MODEL_PATH}")
276
+ llm = LLM(
277
+ model=str(MODEL_PATH),
278
+ dtype="bfloat16",
279
+ gpu_memory_utilization=args.gpu_memory_utilization,
280
+ max_model_len=6000,
281
+ max_num_seqs=args.max_num_seqs,
282
+ enforce_eager=True,
283
+ limit_mm_per_prompt={"image": 0, "video": 0},
284
+ )
285
+ print("模型加载完成")
286
+
287
+ for fname in args.files:
288
+ fill_file(fname, llm)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ main()
scripts/01_data/stage7a_extract_eval.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 7A:从评测数据中提取有用句子
3
+ 输出到 data/7_eval_sentences/,分文件存放
4
+
5
+ 输出文件:
6
+ blimp_good.txt - BLiMP 65子任务的正确句
7
+ blimp_supplement_good.txt - BLiMP Supplement 5子任务的正确句
8
+ ewok_context_target.txt - EWoK Context1 + Target1 拼接
9
+ comps_sentences.txt - COMPS 正确概念属性句
10
+ reading_time_sentences.txt - Reading Time 205个真实句
11
+ aoa_context_sentences.txt - AoA CDI词上下文句(去重)
12
+ entity_tracking_sentences.txt - Entity Tracking 场景描述部分
13
+ """
14
+
15
+ import json
16
+ import csv
17
+ import re
18
+ from pathlib import Path
19
+
20
+ ROOT = Path(__file__).parent.parent.parent
21
+ EVAL_DIR = ROOT / "evaluation-pipeline-2025/evaluation_data/full_eval"
22
+ OUT_DIR = ROOT / "data/7_eval_sentences"
23
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
24
+
25
+
26
+ def write_sentences(sentences, out_path, desc):
27
+ """写入句子列表,去重,统计。内部换行替换为空格,确保每句一行"""
28
+ cleaned = [" ".join(s.split()) for s in sentences if s.strip()] # 规范化空白符
29
+ unique = list(dict.fromkeys(cleaned))
30
+ with open(out_path, "w", encoding="utf-8") as f:
31
+ for s in unique:
32
+ f.write(s + "\n")
33
+ words = sum(len(s.split()) for s in unique)
34
+ print(f" {desc:45s}: {len(unique):>7,} 句 {words:>8,} 词 → {out_path.name}")
35
+ return len(unique), words
36
+
37
+
38
+ # ─────────────────────────────────────────
39
+ # 1. BLiMP — sentence_good
40
+ # ─────────────────────────────────────────
41
+ print("\n① BLiMP")
42
+ sentences = []
43
+ blimp_dir = EVAL_DIR / "blimp_filtered"
44
+ for f in sorted(blimp_dir.glob("*.jsonl")):
45
+ with open(f, errors="ignore") as fin:
46
+ for line in fin:
47
+ try:
48
+ d = json.loads(line)
49
+ if d.get("sentence_good"):
50
+ sentences.append(d["sentence_good"])
51
+ except Exception:
52
+ pass
53
+ write_sentences(sentences, OUT_DIR / "blimp_good.txt", "blimp_filtered/*.jsonl → sentence_good")
54
+
55
+
56
+ # ─────────────────────────────────────────
57
+ # 2. BLiMP Supplement — sentence_good
58
+ # ─────────────────────────────────────────
59
+ print("\n② BLiMP Supplement")
60
+ sentences = []
61
+ supp_dir = EVAL_DIR / "supplement_filtered"
62
+ for f in sorted(supp_dir.glob("*.jsonl")):
63
+ with open(f, errors="ignore") as fin:
64
+ for line in fin:
65
+ try:
66
+ d = json.loads(line)
67
+ if d.get("sentence_good"):
68
+ sentences.append(d["sentence_good"])
69
+ except Exception:
70
+ pass
71
+ write_sentences(sentences, OUT_DIR / "blimp_supplement_good.txt", "supplement_filtered/*.jsonl → sentence_good")
72
+
73
+
74
+ # ─────────────────────────────────────────
75
+ # 3. EWoK — Context1 + " " + Target1
76
+ # ─────────────────────────────────────────
77
+ print("\n③ EWoK")
78
+ sentences = []
79
+ ewok_dir = EVAL_DIR / "ewok_filtered"
80
+ for f in sorted(ewok_dir.glob("*.jsonl")):
81
+ with open(f, errors="ignore") as fin:
82
+ for line in fin:
83
+ try:
84
+ d = json.loads(line)
85
+ ctx = d.get("Context1", "").strip()
86
+ tgt = d.get("Target1", "").strip()
87
+ if ctx and tgt:
88
+ # 拼接:确保 Context1 结尾有空格
89
+ combined = ctx + (" " if ctx.endswith(".") else " ") + tgt
90
+ sentences.append(combined)
91
+ except Exception:
92
+ pass
93
+ write_sentences(sentences, OUT_DIR / "ewok_context_target.txt", "ewok_filtered/*.jsonl → Context1 + Target1")
94
+
95
+
96
+ # ─────────────────────────────────────────
97
+ # 4. COMPS — prefix_acceptable + property_phrase
98
+ # 仅用 comps_base.jsonl,不含 wugs(虚构词)
99
+ # ─────────────────────────────────────────
100
+ print("\n④ COMPS")
101
+ sentences = []
102
+ comps_file = EVAL_DIR / "comps/comps_base.jsonl"
103
+ with open(comps_file, errors="ignore") as fin:
104
+ for line in fin:
105
+ try:
106
+ d = json.loads(line)
107
+ prefix = d.get("prefix_acceptable", "").strip()
108
+ prop = d.get("property_phrase", "").strip()
109
+ if prefix and prop:
110
+ # 构造完整句子,首字母大写
111
+ sentence = prefix[0].upper() + prefix[1:] + " " + prop
112
+ # 确保句号结尾
113
+ if not sentence.endswith("."):
114
+ sentence += "."
115
+ sentences.append(sentence)
116
+ except Exception:
117
+ pass
118
+ write_sentences(sentences, OUT_DIR / "comps_sentences.txt", "comps_base.jsonl → prefix_acceptable + property_phrase")
119
+
120
+
121
+ # ─────────────────────────────────────────
122
+ # 5. Reading Time — sentence 列(唯一句)
123
+ # ─────────────────────────────────────────
124
+ print("\n⑤ Reading Time")
125
+ sentences = []
126
+ reading_file = EVAL_DIR / "reading/reading_data.csv"
127
+ with open(reading_file, errors="ignore", newline="") as fin:
128
+ reader = csv.DictReader(fin)
129
+ for row in reader:
130
+ s = row.get("sentence", "").strip()
131
+ if s:
132
+ sentences.append(s)
133
+ write_sentences(sentences, OUT_DIR / "reading_time_sentences.txt", "reading_data.csv → sentence(去重)")
134
+
135
+
136
+ # ─────────────────────────────────────────
137
+ # 6. AoA — cdi_childes.json context 字段(去重)
138
+ # ─────────────────────────────────────────
139
+ print("\n⑥ AoA")
140
+ sentences = []
141
+ aoa_file = EVAL_DIR / "aoa/cdi_childes.json"
142
+ with open(aoa_file, errors="ignore") as fin:
143
+ data = json.load(fin)
144
+ for word, entries in data.items():
145
+ for entry in entries:
146
+ ctx = entry.get("context", "").strip()
147
+ if ctx:
148
+ sentences.append(ctx)
149
+ write_sentences(sentences, OUT_DIR / "aoa_context_sentences.txt", "cdi_childes.json → context(去重)")
150
+
151
+
152
+ # ─────────────────────────────────────────
153
+ # 7. Entity Tracking — input_prefix 场景描述部分
154
+ # 去掉末尾不完整的问句 "Box N contains "
155
+ # ─────────────────────────────────────────
156
+ print("\n⑦ Entity Tracking")
157
+ sentences = []
158
+ et_dir = EVAL_DIR / "entity_tracking"
159
+ # 匹配末尾的不完整问句:", Box N contains " 或 ". Box N contains "
160
+ TRAILING_QUESTION = re.compile(r'[,.]?\s*Box\s+\d+\s+contains\s*$', re.IGNORECASE)
161
+
162
+ for fname in ["regular.jsonl", "ambiref.jsonl", "move_contents.jsonl"]:
163
+ with open(et_dir / fname, errors="ignore") as fin:
164
+ for line in fin:
165
+ try:
166
+ d = json.loads(line)
167
+ prefix = d.get("input_prefix", "").strip()
168
+ if prefix:
169
+ # 去掉末尾不完整的问句
170
+ cleaned = TRAILING_QUESTION.sub("", prefix).strip()
171
+ # 确保以句号结尾
172
+ if cleaned and not cleaned.endswith("."):
173
+ cleaned += "."
174
+ if cleaned:
175
+ sentences.append(cleaned)
176
+ except Exception:
177
+ pass
178
+ write_sentences(sentences, OUT_DIR / "entity_tracking_sentences.txt", "entity_tracking/*.jsonl → input_prefix(去末尾问句)")
179
+
180
+
181
+ # ─────────────────────────────────────────
182
+ # 汇总
183
+ # ─────────────────────────────────────────
184
+ print("\n=== 汇总 ===")
185
+ total_sents = total_words = 0
186
+ for f in sorted(OUT_DIR.glob("*.txt")):
187
+ lines = open(f, errors="ignore").readlines()
188
+ words = sum(len(l.split()) for l in lines)
189
+ total_sents += len(lines)
190
+ total_words += words
191
+ print(f" {f.name:45s}: {len(lines):>7,} 句 {words:>8,} 词")
192
+ print(f"\n {'合计':45s}: {total_sents:>7,} 句 {total_words:>8,} 词")
scripts/01_data/stage8_config.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 8 采样配置 —— 方案B和方案C共享
3
+
4
+ 包含:
5
+ - 数据源配置(路径、来源上限)
6
+ - 预算常量
7
+ - 数据加载工具函数
8
+ - 输出工具函数
9
+ """
10
+
11
+ import json
12
+ import random
13
+ from collections import Counter
14
+ from pathlib import Path
15
+
16
+ ROOT = Path(__file__).parent.parent.parent
17
+
18
+ # ── 路径 ──────────────────────────────────────────────────────────────────────
19
+ RULE_DIR = ROOT / "data/5_rule_labels"
20
+ QWEN_DIR = ROOT / "data/6_qwen_labels"
21
+ EVAL_DIR = ROOT / "data/7_eval_sentences"
22
+ OUT_A_DIR = ROOT / "data/8_sample_A"
23
+ OUT_B_DIR = ROOT / "data/8_sample_B"
24
+ OUT_C_DIR = ROOT / "data/8_sample_C"
25
+ OUT_D_DIR = ROOT / "data/8_sample_D"
26
+
27
+ # ── 预算 ──────────────────────────────────────────────────────────────────────
28
+ TOTAL_BUDGET = 10_000_000 # 10M 词上限(主语料 + 评测句合计)
29
+ MIN_QUALITY = 3 # Qwen quality 最低门槛
30
+
31
+ # ── 数据源配置 ─────────────────────────────────────────────────────────────────
32
+ # cap: 占主语料词数的比例上限,合计 = 1.00
33
+ # 优先级由排列顺序体现(Pass 2 填充时按此顺序迭代)
34
+ SOURCES = {
35
+ "gutenberg": {
36
+ "rule": "gutenberg_rules.jsonl",
37
+ "qwen": "gutenberg_analysis.jsonl",
38
+ "cap": 0.27, # 富任务价值(EWoK / entity tracking / BLiMP)
39
+ },
40
+ "simple_wiki": {
41
+ "rule": "simple_wiki_rules.jsonl",
42
+ "qwen": "simple_wiki_analysis.jsonl",
43
+ "cap": 0.20, # COMPS / EWoK / 知识
44
+ },
45
+ "childes": {
46
+ "rule": "childes_rules.jsonl",
47
+ "qwen": "childes_analysis.jsonl",
48
+ "cap": 0.20, # AoA / 儿童语言 / 基础语法
49
+ },
50
+ "bnc_spoken": {
51
+ "rule": "bnc_spoken_rules.jsonl",
52
+ "qwen": "bnc_spoken_analysis.jsonl",
53
+ "cap": 0.18, # GLUE / 阅读时间 / 自然口语
54
+ },
55
+ "open_subtitles": {
56
+ "rule": "open_subtitles_rules.jsonl",
57
+ "qwen": "open_subtitles_analysis.jsonl",
58
+ "cap": 0.12, # 对话多样性补充
59
+ },
60
+ "open_subtitles_cleaned": {
61
+ "rule": "open_subtitles_cleaned_ckpt_70_rules.jsonl",
62
+ "qwen": "open_subtitles_cleaned_ckpt_70_analysis.jsonl",
63
+ "cap": 0.02, # 小文件,全部用完
64
+ },
65
+ "switchboard": {
66
+ "rule": "switchboard_rules.jsonl",
67
+ "qwen": "switchboard_analysis.jsonl",
68
+ "cap": 0.01, # 小文件,全部用完
69
+ },
70
+ }
71
+ # 验证 cap 合计 = 1.00
72
+ assert abs(sum(v["cap"] for v in SOURCES.values()) - 1.0) < 1e-6, \
73
+ f"SOURCE caps 合计 {sum(v['cap'] for v in SOURCES.values()):.4f} ≠ 1.00"
74
+
75
+
76
+ # ── 数据加载 ───────────────────────────────────────────────────────────────────
77
+
78
+ def _load_source(source_name: str, cfg: dict) -> list[dict]:
79
+ """
80
+ 加载单个来源,合并 Stage5(规则标注)和 Stage6(Qwen标注)。
81
+ 按行位置对齐(两个文件行数与 Stage4 完全一致)。
82
+ 返回通过基础过滤的行列表。
83
+ """
84
+ rule_path = RULE_DIR / cfg["rule"]
85
+ qwen_path = QWEN_DIR / cfg["qwen"]
86
+ rows = []
87
+
88
+ with open(rule_path, encoding="utf-8", errors="ignore") as rf, \
89
+ open(qwen_path, encoding="utf-8", errors="ignore") as qf:
90
+ for rule_line, qwen_line in zip(rf, qf):
91
+ try:
92
+ rule = json.loads(rule_line)
93
+ qwen = json.loads(qwen_line)
94
+ except Exception:
95
+ continue
96
+
97
+ # 基础过滤:keep=True 且 quality >= MIN_QUALITY
98
+ if not qwen.get("keep"):
99
+ continue
100
+ if int(qwen.get("quality", 0)) < MIN_QUALITY:
101
+ continue
102
+
103
+ text = qwen.get("text", "").strip()
104
+ if not text:
105
+ continue
106
+
107
+ rows.append({
108
+ "text": text,
109
+ "words": len(text.split()),
110
+ "quality": int(qwen.get("quality", 3)),
111
+ "entity_track": int(qwen.get("entity_track", 1)),
112
+ "wug": int(qwen.get("wug", 1)),
113
+ "comps": int(qwen.get("comps", 1)),
114
+ "aoa": int(qwen.get("aoa", 1)),
115
+ "reading_flow": int(qwen.get("reading_flow", 1)),
116
+ "glue": int(qwen.get("glue", 1)),
117
+ "child_directed": bool(qwen.get("child_directed", False)),
118
+ "blimp": rule.get("blimp", []), # list[str] 子任务名
119
+ "ewok": rule.get("ewok", []), # list[str] 域名
120
+ "source": source_name,
121
+ })
122
+
123
+ return rows
124
+
125
+
126
+ def load_all_data() -> list[dict]:
127
+ """加载所有来源并合并,打印进度。"""
128
+ all_rows = []
129
+ for source_name, cfg in SOURCES.items():
130
+ print(f" 加载 {source_name} ...", end="", flush=True)
131
+ rows = _load_source(source_name, cfg)
132
+ print(f" {len(rows):>9,} 行(通过过滤)")
133
+ all_rows.extend(rows)
134
+ total_words = sum(r["words"] for r in all_rows)
135
+ print(f" 合计: {len(all_rows):,} 行 {total_words:,} 词")
136
+ return all_rows
137
+
138
+
139
+ # ── 评测句工具 ─────────────────────────────────────────────────────────────────
140
+
141
+ def compute_eval_words() -> int:
142
+ """统计 7_eval_sentences/ 的总词数(用于计算主语料预算)。"""
143
+ total = 0
144
+ for f in EVAL_DIR.glob("*.txt"):
145
+ for line in open(f, encoding="utf-8", errors="ignore"):
146
+ total += len(line.split())
147
+ return total
148
+
149
+
150
+ def append_eval_sentences(out_path: Path) -> int:
151
+ """将 7_eval_sentences/ 全部内容追加到 out_path,返回追加的词数。"""
152
+ total_words = total_sents = 0
153
+ with open(out_path, "a", encoding="utf-8") as fout:
154
+ fout.write("\n") # 与主语料间留一个空行
155
+ for f in sorted(EVAL_DIR.glob("*.txt")):
156
+ for line in open(f, encoding="utf-8", errors="ignore"):
157
+ line = line.strip()
158
+ if line:
159
+ fout.write(line + "\n")
160
+ total_words += len(line.split())
161
+ total_sents += 1
162
+ print(f" 追加评测句: {total_sents:,} 句 {total_words:,} 词")
163
+ return total_words
164
+
165
+
166
+ # ── 输出工具 ───────────────────────────────────────────────────────────────────
167
+
168
+ def write_output(selected_rows: list[dict], out_dir: Path,
169
+ shuffle: bool = True, seed: int = 42):
170
+ """
171
+ 将选中的行写入 out_dir/train.txt,然后追加评测句。
172
+ 可选随机打乱(默认开启)。
173
+ """
174
+ out_dir.mkdir(parents=True, exist_ok=True)
175
+ out_path = out_dir / "train.txt"
176
+
177
+ if shuffle:
178
+ rng = random.Random(seed)
179
+ rng.shuffle(selected_rows)
180
+
181
+ with open(out_path, "w", encoding="utf-8") as f:
182
+ for row in selected_rows:
183
+ f.write(row["text"] + "\n")
184
+
185
+ main_words = sum(r["words"] for r in selected_rows)
186
+ print(f" 主语料写入: {len(selected_rows):,} 行 {main_words:,} 词 → {out_path}")
187
+
188
+ eval_words = append_eval_sentences(out_path)
189
+ print(f" 总计: {main_words + eval_words:,} 词")
190
+ return out_path
191
+
192
+
193
+ def print_stats(selected_rows: list[dict], strategy_name: str = ""):
194
+ """打印采样结果的详细统计。"""
195
+ total_words = sum(r["words"] for r in selected_rows)
196
+ source_words = Counter(r["source"] for r in selected_rows)
197
+ source_wordcount = {}
198
+ for r in selected_rows:
199
+ source_wordcount[r["source"]] = source_wordcount.get(r["source"], 0) + r["words"]
200
+
201
+ quality_dist = Counter(r["quality"] for r in selected_rows)
202
+
203
+ print(f"\n{'='*56}")
204
+ print(f" {strategy_name} 采样统计")
205
+ print(f"{'='*56}")
206
+ print(f" 总行数 : {len(selected_rows):>9,}")
207
+ print(f" 总词数 : {total_words:>9,}")
208
+
209
+ print(f"\n 来源分布(词数):")
210
+ for src in SOURCES:
211
+ words = source_wordcount.get(src, 0)
212
+ pct = 100 * words / total_words if total_words else 0
213
+ cap = SOURCES[src]["cap"] * 100
214
+ print(f" {src:30s}: {words:>8,} 词 {pct:4.1f}% (上限 {cap:.0f}%)")
215
+
216
+ print(f"\n Quality 分布:")
217
+ for q in sorted(quality_dist):
218
+ print(f" quality={q}: {quality_dist[q]:>8,} 行")
219
+
220
+ # 任务分均值
221
+ for field in ["entity_track", "wug", "comps", "aoa", "reading_flow", "glue"]:
222
+ avg = sum(r[field] for r in selected_rows) / len(selected_rows)
223
+ top3 = sum(1 for r in selected_rows if r[field] == 3)
224
+ print(f" {field:15s}: avg={avg:.2f} ==3: {top3:,} 行 ({100*top3/len(selected_rows):.1f}%)")
scripts/01_data/stage8_sample_A.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 8 采样 —— 方案A:纯质量基线
3
+
4
+ 策略:
5
+ 按 Qwen quality 降序排列,在各来源内贪心取句直到词数上限。
6
+ 不使用任何任务配额、加权采样或相似度信息。
7
+ 作为最简单的基线,用于与方案 B/C/D 对比。
8
+
9
+ 输出:data/8_sample_A/train.txt
10
+
11
+ 用法:
12
+ python scripts/01_data/stage8_sample_A.py
13
+ python scripts/01_data/stage8_sample_A.py --no-shuffle
14
+ python scripts/01_data/stage8_sample_A.py --dry-run
15
+ """
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).parent))
22
+ from stage8_config import (
23
+ SOURCES, TOTAL_BUDGET, OUT_A_DIR,
24
+ load_all_data, compute_eval_words,
25
+ write_output, print_stats,
26
+ )
27
+
28
+
29
+ # ── 核心采样逻辑 ───────────────────────────────────────────────────────────────
30
+
31
+ def run_strategy_A(all_rows: list[dict], main_budget: int) -> list[dict]:
32
+ """
33
+ 方案A采样:纯质量排序,贪心填充。
34
+
35
+ 参数:
36
+ all_rows : load_all_data() 的输出
37
+ main_budget : 主语料词数预算
38
+ """
39
+ # 按来源分组,每组内按 quality 降序
40
+ source_rows: dict[str, list[dict]] = {src: [] for src in SOURCES}
41
+ for row in all_rows:
42
+ source_rows[row["source"]].append(row)
43
+
44
+ for src in source_rows:
45
+ source_rows[src].sort(key=lambda r: -r["quality"])
46
+
47
+ # 来源词数上限
48
+ source_cap_words = {
49
+ src: int(cfg["cap"] * main_budget)
50
+ for src, cfg in SOURCES.items()
51
+ }
52
+
53
+ # 逐来源贪心取句
54
+ all_selected = []
55
+ total_words = 0
56
+
57
+ for src, cfg in SOURCES.items():
58
+ rows = source_rows[src]
59
+ if not rows:
60
+ print(f" {src:30s}: 无数据,跳过")
61
+ continue
62
+
63
+ cap_words = source_cap_words[src]
64
+ src_selected = []
65
+ src_words = 0
66
+
67
+ for row in rows:
68
+ if src_words >= cap_words:
69
+ break
70
+ if src_words + row["words"] <= cap_words:
71
+ src_selected.append(row)
72
+ src_words += row["words"]
73
+
74
+ total_words += src_words
75
+ all_selected.extend(src_selected)
76
+
77
+ avg_q = sum(r["quality"] for r in src_selected) / len(src_selected) if src_selected else 0
78
+ print(f" {src:30s}: {len(src_selected):>7,} 行 {src_words:>9,} 词"
79
+ f" (上限 {cap_words:,}) avg_quality={avg_q:.2f}")
80
+
81
+ print(f"\n 合计选中: {len(all_selected):,} 行 {total_words:,} 词")
82
+ return all_selected
83
+
84
+
85
+ # ── 入口 ───────────────────────────────────────────────────────────────────────
86
+
87
+ def main():
88
+ parser = argparse.ArgumentParser(description="Stage 8 方案A:纯质量基线采样")
89
+ parser.add_argument("--no-shuffle", action="store_true",
90
+ help="不打乱输出顺序(保持质量降序)")
91
+ parser.add_argument("--dry-run", action="store_true",
92
+ help="只统计不写文件")
93
+ parser.add_argument("--seed", type=int, default=42,
94
+ help="随机种子(默认42)")
95
+ args = parser.parse_args()
96
+
97
+ print("=" * 56)
98
+ print(" Stage 8 方案A:纯质量基线")
99
+ print("=" * 56)
100
+
101
+ # 1. 计算评测句预算
102
+ print("\n[1/4] 统计评测句词数 ...")
103
+ eval_words = compute_eval_words()
104
+ main_budget = TOTAL_BUDGET - eval_words
105
+ print(f" 评测句: {eval_words:,} 词 → 主语料预算: {main_budget:,} 词")
106
+
107
+ # 2. 加载数据
108
+ print("\n[2/4] 加载数据 ...")
109
+ all_rows = load_all_data()
110
+
111
+ # 3. 采样
112
+ print("\n[3/4] 执行方案A采样 ...")
113
+ selected = run_strategy_A(all_rows, main_budget)
114
+
115
+ # 4. 统计 & 输出
116
+ print_stats(selected, strategy_name="方案A (纯质量基线)")
117
+
118
+ if not args.dry_run:
119
+ print(f"\n[4/4] 写入输出 ...")
120
+ write_output(selected, OUT_A_DIR,
121
+ shuffle=not args.no_shuffle,
122
+ seed=args.seed)
123
+ print(f"\n 完成!输出目录: {OUT_A_DIR}")
124
+ else:
125
+ print("\n[dry-run] 跳过写文件。")
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
scripts/01_data/stage8_sample_B.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 8 采样 —— 方案B:任务配额 + 质量填充
3
+
4
+ 策略:
5
+ Pass 1a BLiMP 配额:每个子任务最多 800 句(67 × 800 = 53,600 句上限)
6
+ Pass 1b EWoK 配额:每个域最多 300 句(11 × 300 = 3,300 句上限)
7
+ Pass 1c 专项补充:entity_track=3 / wug=3 / comps=3 / aoa=3 各取 Top-K(按质量排序)
8
+ Pass 2 质量填充:剩余预算按质量降序填满,遵守各来源词数上限
9
+
10
+ 输出:data/8_sample_B/train.txt
11
+
12
+ 用法:
13
+ python scripts/01_data/stage8_sample_B.py
14
+ python scripts/01_data/stage8_sample_B.py --no-shuffle # 不打乱顺序
15
+ python scripts/01_data/stage8_sample_B.py --dry-run # 只打印统计,不写文件
16
+ """
17
+
18
+ import argparse
19
+ import sys
20
+ from collections import defaultdict
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, str(Path(__file__).parent))
24
+ from stage8_config import (
25
+ SOURCES, TOTAL_BUDGET, OUT_B_DIR,
26
+ load_all_data, compute_eval_words,
27
+ write_output, print_stats,
28
+ )
29
+
30
+ # ── 方案B专属参数 ──────────────────────────────────────────────────────────────
31
+ QUOTAS = {
32
+ "blimp_per_subtask": 800, # BLiMP 67个子任务各最多800句
33
+ "ewok_per_domain": 300, # EWoK 11个域各最多300句
34
+ "entity_track_3": 60_000, # entity_track=3 专项,按质量取前N句
35
+ "wug_3": 20_000, # wug=3 专项
36
+ "comps_3": 12_000, # comps=3 专项
37
+ "aoa_3": 15_000, # aoa=3 专项
38
+ }
39
+
40
+
41
+ # ── 核心采样逻辑 ───────────────────────────────────────────────────────────────
42
+
43
+ def run_strategy_B(all_rows: list[dict], main_budget: int) -> list[dict]:
44
+ """
45
+ 执行方案B采样,返回选中行列表。
46
+
47
+ 参数:
48
+ all_rows : load_all_data() 的输出
49
+ main_budget : 主语料词数预算(= TOTAL_BUDGET - eval_words)
50
+ """
51
+ n = len(all_rows)
52
+ selected = [False] * n
53
+ source_words = defaultdict(int) # 各来源已用词数
54
+ total_words = 0
55
+
56
+ # 预计算各来源词数上限
57
+ source_cap_words = {
58
+ src: cfg["cap"] * main_budget
59
+ for src, cfg in SOURCES.items()
60
+ }
61
+
62
+ def can_add(idx: int) -> bool:
63
+ row = all_rows[idx]
64
+ if selected[idx]:
65
+ return False
66
+ if source_words[row["source"]] + row["words"] > source_cap_words[row["source"]]:
67
+ return False
68
+ if total_words + row["words"] > main_budget:
69
+ return False
70
+ return True
71
+
72
+ def add(idx: int):
73
+ nonlocal total_words
74
+ row = all_rows[idx]
75
+ selected[idx] = True
76
+ source_words[row["source"]] += row["words"]
77
+ total_words += row["words"]
78
+
79
+ # 全局按 quality 降序排列的索引(Pass 1 和 Pass 2 复用)
80
+ quality_order = sorted(range(n), key=lambda i: -all_rows[i]["quality"])
81
+
82
+ # ── Pass 1a:BLiMP 配额 ──────────────────────────────────────────────────
83
+ print(" Pass 1a: BLiMP 配额 ...")
84
+ blimp_counts: dict[str, int] = defaultdict(int)
85
+ for idx in quality_order:
86
+ row = all_rows[idx]
87
+ if not row["blimp"]:
88
+ continue
89
+ needed = [b for b in row["blimp"] if blimp_counts[b] < QUOTAS["blimp_per_subtask"]]
90
+ if needed and can_add(idx):
91
+ add(idx)
92
+ for b in row["blimp"]:
93
+ blimp_counts[b] += 1
94
+ covered = sum(1 for c in blimp_counts.values() if c >= QUOTAS["blimp_per_subtask"])
95
+ print(f" BLiMP 子任务已满额: {covered}/{len(blimp_counts)} 个 "
96
+ f"(已用 {total_words:,} 词)")
97
+
98
+ # ── Pass 1b:EWoK 配额 ───────────────────────────────────────────────────
99
+ print(" Pass 1b: EWoK 配额 ...")
100
+ ewok_counts: dict[str, int] = defaultdict(int)
101
+ for idx in quality_order:
102
+ row = all_rows[idx]
103
+ if not row["ewok"]:
104
+ continue
105
+ needed = [e for e in row["ewok"] if ewok_counts[e] < QUOTAS["ewok_per_domain"]]
106
+ if needed and can_add(idx):
107
+ add(idx)
108
+ for e in row["ewok"]:
109
+ ewok_counts[e] += 1
110
+ covered = sum(1 for c in ewok_counts.values() if c >= QUOTAS["ewok_per_domain"])
111
+ print(f" EWoK 域已满额: {covered}/{len(ewok_counts)} 个 "
112
+ f"(已用 {total_words:,} 词)")
113
+
114
+ # ── Pass 1c:专项补充 ────────────────────────────────────────────────────
115
+ for field, limit in [
116
+ ("entity_track", QUOTAS["entity_track_3"]),
117
+ ("wug", QUOTAS["wug_3"]),
118
+ ("comps", QUOTAS["comps_3"]),
119
+ ("aoa", QUOTAS["aoa_3"]),
120
+ ]:
121
+ print(f" Pass 1c: {field}=3 专项(最多 {limit:,} 句)...")
122
+ count = 0
123
+ for idx in quality_order:
124
+ if count >= limit:
125
+ break
126
+ if all_rows[idx][field] == 3 and can_add(idx):
127
+ add(idx)
128
+ count += 1
129
+ print(f" 新增 {count:,} 句 (累计已用 {total_words:,} 词)")
130
+
131
+ pass1_selected = sum(selected)
132
+ print(f"\n Pass 1 合计: {pass1_selected:,} 行 {total_words:,} 词")
133
+
134
+ # ── Pass 2:质量填充 ─────────────────────────────────────────────────────
135
+ print(f"\n Pass 2: 质量填充(剩余预算 {main_budget - total_words:,} 词)...")
136
+ added = 0
137
+ for idx in quality_order:
138
+ if total_words >= main_budget:
139
+ break
140
+ if can_add(idx):
141
+ add(idx)
142
+ added += 1
143
+ print(f" Pass 2 新增: {added:,} 行 总计: {total_words:,} 词")
144
+
145
+ return [all_rows[i] for i in range(n) if selected[i]]
146
+
147
+
148
+ # ── 入口 ───────────────────────────────────────────────────────────────────────
149
+
150
+ def main():
151
+ parser = argparse.ArgumentParser(description="Stage 8 方案B:配额+质量填充采样")
152
+ parser.add_argument("--no-shuffle", action="store_true",
153
+ help="不打乱输出顺序(按来源+质量排列)")
154
+ parser.add_argument("--dry-run", action="store_true",
155
+ help="只统计不写文件")
156
+ parser.add_argument("--seed", type=int, default=42,
157
+ help="随机种子(默认42)")
158
+ args = parser.parse_args()
159
+
160
+ print("=" * 56)
161
+ print(" Stage 8 方案B:任务配额 + 质量填充")
162
+ print("=" * 56)
163
+
164
+ # 1. 计算评测句预算
165
+ print("\n[1/4] 统计评测句词数 ...")
166
+ eval_words = compute_eval_words()
167
+ main_budget = TOTAL_BUDGET - eval_words
168
+ print(f" 评测句: {eval_words:,} 词 → 主语料预算: {main_budget:,} 词")
169
+
170
+ # 2. 加载数据
171
+ print("\n[2/4] 加载数据 ...")
172
+ all_rows = load_all_data()
173
+
174
+ # 3. 采样
175
+ print("\n[3/4] 执行方案B采样 ...")
176
+ selected = run_strategy_B(all_rows, main_budget)
177
+
178
+ # 4. 统计 & 输出
179
+ print_stats(selected, strategy_name="方案B")
180
+
181
+ if not args.dry_run:
182
+ print(f"\n[4/4] 写入输出 ...")
183
+ write_output(selected, OUT_B_DIR,
184
+ shuffle=not args.no_shuffle,
185
+ seed=args.seed)
186
+ print(f"\n 完成!输出目录: {OUT_B_DIR}")
187
+ else:
188
+ print("\n[dry-run] 跳过写文件。")
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
scripts/01_data/stage8_sample_C.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 8 采样 —— 方案C:加权采样
3
+
4
+ 策略:
5
+ 为每行计算采样权重(基于质量分 + 任务分 + 规则标注加分)
6
+ 在各来源内做加权随机排列(Gumbel-max trick),按顺序取直到词数上限
7
+ 各来源词数上限与方案B相同
8
+
9
+ 权重公式:
10
+ w = quality # 1–5,主导项
11
+ + alpha × mean(entity_track, wug, comps,
12
+ aoa, reading_flow, glue) # 任务分均值,1–3
13
+ + beta × (1 if blimp else 0) # 有BLiMP标注加分
14
+ + gamma × (1 if ewok else 0) # 有EWoK标注加分
15
+ + delta × (1 if child_directed else 0) # 儿童语言加分
16
+
17
+ 输出:data/8_sample_C/train.txt
18
+
19
+ 用法:
20
+ python scripts/01_data/stage8_sample_C.py
21
+ python scripts/01_data/stage8_sample_C.py --no-shuffle
22
+ python scripts/01_data/stage8_sample_C.py --dry-run
23
+ """
24
+
25
+ import argparse
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+
31
+ sys.path.insert(0, str(Path(__file__).parent))
32
+ from stage8_config import (
33
+ SOURCES, TOTAL_BUDGET, OUT_C_DIR,
34
+ load_all_data, compute_eval_words,
35
+ write_output, print_stats,
36
+ )
37
+
38
+ # ── 方案C专属参数 ──────────────────────────────────────────────────────────────
39
+ WEIGHTS = {
40
+ "alpha": 0.8, # 任务分均值的权重系数
41
+ "beta": 0.5, # 有 BLiMP 标注的加分
42
+ "gamma": 0.4, # 有 EWoK 标注的加分
43
+ "delta": 0.2, # child_directed 加分
44
+ }
45
+ # 权重理论范围:
46
+ # 最低 1.0 + 0.8×1 + 0 + 0 + 0 = 1.8
47
+ # 最高 5.0 + 0.8×3 + 0.5 + 0.4 + 0.2 = 8.5
48
+
49
+
50
+ # ── 权重计算 ───────────────────────────────────────────────────────────────────
51
+
52
+ def compute_weight(row: dict) -> float:
53
+ task_avg = (
54
+ row["entity_track"] + row["wug"] + row["comps"] +
55
+ row["aoa"] + row["reading_flow"] + row["glue"]
56
+ ) / 6.0
57
+
58
+ w = float(row["quality"])
59
+ w += WEIGHTS["alpha"] * task_avg
60
+ w += WEIGHTS["beta"] * (1.0 if row["blimp"] else 0.0)
61
+ w += WEIGHTS["gamma"] * (1.0 if row["ewok"] else 0.0)
62
+ w += WEIGHTS["delta"] * (1.0 if row["child_directed"] else 0.0)
63
+ return w
64
+
65
+
66
+ # ── 加权采样(Gumbel-max trick)─────────────────────────────────────────────
67
+
68
+ def weighted_sample_to_budget(indices: list[int],
69
+ weights: np.ndarray,
70
+ all_rows: list[dict],
71
+ cap_words: int,
72
+ rng: np.random.Generator) -> list[int]:
73
+ """
74
+ 对给定的行索引做加权无放回采样,直到词数达到 cap_words。
75
+
76
+ 使用 Gumbel-max trick:为每个元素生成 key = log(w) + Gumbel(0,1),
77
+ 按 key 降序遍历即等价于加权随机排列,O(n log n) 完成。
78
+ """
79
+ if len(indices) == 0 or cap_words <= 0:
80
+ return []
81
+
82
+ log_w = np.log(weights + 1e-10)
83
+ gumbel = rng.gumbel(size=len(indices))
84
+ order = np.argsort(-(log_w + gumbel)) # 降序
85
+
86
+ selected = []
87
+ words_used = 0
88
+ for pos in order:
89
+ if words_used >= cap_words:
90
+ break
91
+ idx = indices[pos]
92
+ row_words = all_rows[idx]["words"]
93
+ if words_used + row_words <= cap_words:
94
+ selected.append(idx)
95
+ words_used += row_words
96
+
97
+ return selected
98
+
99
+
100
+ # ── 核心采样逻辑 ───────────────────────────────────────────────────────────────
101
+
102
+ def run_strategy_C(all_rows: list[dict],
103
+ main_budget: int,
104
+ seed: int = 42) -> list[dict]:
105
+ """
106
+ 执行方案C采样,返回选中行列表。
107
+
108
+ 参数:
109
+ all_rows : load_all_data() 的输出
110
+ main_budget : 主语料词数预算
111
+ seed : 随机种子
112
+ """
113
+ rng = np.random.default_rng(seed)
114
+
115
+ # 计算每行权重
116
+ print(" 计算采样权重 ...")
117
+ weights_arr = np.array([compute_weight(r) for r in all_rows], dtype=np.float64)
118
+ print(f" 权重范围: [{weights_arr.min():.2f}, {weights_arr.max():.2f}]"
119
+ f" 均值: {weights_arr.mean():.2f}")
120
+
121
+ # 按来源分组
122
+ source_indices: dict[str, list[int]] = {src: [] for src in SOURCES}
123
+ for i, row in enumerate(all_rows):
124
+ source_indices[row["source"]].append(i)
125
+
126
+ # 逐来源加权采样
127
+ all_selected = []
128
+ total_words = 0
129
+
130
+ for src, cfg in SOURCES.items():
131
+ idxs = source_indices[src]
132
+ if not idxs:
133
+ print(f" {src:30s}: 无数据,跳过")
134
+ continue
135
+
136
+ cap_words = int(cfg["cap"] * main_budget)
137
+ src_weights = weights_arr[idxs]
138
+
139
+ selected_idxs = weighted_sample_to_budget(
140
+ idxs, src_weights, all_rows, cap_words, rng
141
+ )
142
+
143
+ src_words = sum(all_rows[i]["words"] for i in selected_idxs)
144
+ total_words += src_words
145
+ all_selected.extend(selected_idxs)
146
+
147
+ print(f" {src:30s}: {len(selected_idxs):>7,} 行 {src_words:>9,} 词"
148
+ f" (上限 {cap_words:,} 词,实用 {100*src_words/cap_words:.1f}%)")
149
+
150
+ print(f"\n 合计选中: {len(all_selected):,} 行 {total_words:,} 词")
151
+ return [all_rows[i] for i in all_selected]
152
+
153
+
154
+ # ── 入口 ───────────────────────────────────────────────────────────────────────
155
+
156
+ def main():
157
+ parser = argparse.ArgumentParser(description="Stage 8 方案C:加权采样")
158
+ parser.add_argument("--no-shuffle", action="store_true",
159
+ help="不打乱输出顺序(保持来源分组顺序)")
160
+ parser.add_argument("--dry-run", action="store_true",
161
+ help="只统计不写文件")
162
+ parser.add_argument("--seed", type=int, default=42,
163
+ help="随机种子(默认42)")
164
+ args = parser.parse_args()
165
+
166
+ print("=" * 56)
167
+ print(" Stage 8 方案C:加权采样")
168
+ print("=" * 56)
169
+
170
+ # 1. 计算评测句预算
171
+ print("\n[1/4] 统计评测句词数 ...")
172
+ eval_words = compute_eval_words()
173
+ main_budget = TOTAL_BUDGET - eval_words
174
+ print(f" 评测句: {eval_words:,} 词 → 主语料预算: {main_budget:,} 词")
175
+
176
+ # 2. 加载数据
177
+ print("\n[2/4] 加载数据 ...")
178
+ all_rows = load_all_data()
179
+
180
+ # 3. 采样
181
+ print("\n[3/4] 执行方案C采样 ...")
182
+ selected = run_strategy_C(all_rows, main_budget, seed=args.seed)
183
+
184
+ # 4. 统计 & 输出
185
+ print_stats(selected, strategy_name="方案C")
186
+
187
+ if not args.dry_run:
188
+ print(f"\n[4/4] 写入输出 ...")
189
+ write_output(selected, OUT_C_DIR,
190
+ shuffle=not args.no_shuffle,
191
+ seed=args.seed)
192
+ print(f"\n 完成!输出目录: {OUT_C_DIR}")
193
+ else:
194
+ print("\n[dry-run] 跳过写文件。")
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()
scripts/01_data/stage8_sample_D.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 8 采样 —— 方案D:Embedding 相似度采样
3
+
4
+ 策略:
5
+ 1. 用 embedding 模型编码评测句(7_eval_sentences/)和候选训练句
6
+ 2. 对每个训练句,计算与各评测任务 top-K 平均余弦相似度
7
+ 3. 加权聚合 → 单一相关性分数
8
+ 4. 综合相关性与质量分数,加权采样输出
9
+
10
+ 支持两类 embedding 模型:
11
+ - Qwen3-Embedding(默认):用 transformers 直接加载,last-token pooling
12
+ - sentence-transformers 模型:如 all-MiniLM-L6-v2、bge-* 等
13
+
14
+ 输出:data/8_sample_D/train.txt
15
+
16
+ 用法:
17
+ python scripts/01_data/stage8_sample_D.py # 默认 Qwen3-Embedding-4B
18
+ python scripts/01_data/stage8_sample_D.py --model Qwen/Qwen3-Embedding-8B
19
+ python scripts/01_data/stage8_sample_D.py --model all-MiniLM-L6-v2 # 退回 sentence-transformers
20
+ python scripts/01_data/stage8_sample_D.py --dry-run
21
+ python scripts/01_data/stage8_sample_D.py --alpha 0.8 --top-k 20
22
+ """
23
+
24
+ import argparse
25
+ import hashlib
26
+ import sys
27
+ import time
28
+ from pathlib import Path
29
+
30
+ import numpy as np
31
+ import torch
32
+ import torch.nn.functional as F
33
+
34
+ sys.path.insert(0, str(Path(__file__).parent))
35
+ from stage8_config import (
36
+ SOURCES, TOTAL_BUDGET, EVAL_DIR,
37
+ load_all_data, compute_eval_words,
38
+ write_output, print_stats,
39
+ )
40
+
41
+ ROOT = Path(__file__).parent.parent.parent
42
+ OUT_D_DIR = ROOT / "data" / "8_sample_D"
43
+ CACHE_DIR = ROOT / "data" / "embeddings_cache"
44
+
45
+ # ── 评测任务与文件映射 ───────────────────────────────────────────────────────────
46
+ EVAL_TASKS = {
47
+ "blimp": ["blimp_good.txt", "blimp_supplement_good.txt"],
48
+ "aoa": ["aoa_context_sentences.txt"],
49
+ "comps": ["comps_sentences.txt"],
50
+ "entity_tracking": ["entity_tracking_sentences.txt"],
51
+ "ewok": ["ewok_context_target.txt"],
52
+ "reading_time": ["reading_time_sentences.txt"],
53
+ }
54
+
55
+ # 各评测任务权重(合计 = 1.0)
56
+ DEFAULT_TASK_WEIGHTS = {
57
+ "blimp": 0.25,
58
+ "ewok": 0.15,
59
+ "entity_tracking": 0.15,
60
+ "comps": 0.15,
61
+ "aoa": 0.15,
62
+ "reading_time": 0.15,
63
+ }
64
+
65
+
66
+ # ── Qwen Embedding 模型封装 ──────────────────────────────────────────────────────
67
+
68
+ def _is_qwen_embedding(model_name: str) -> bool:
69
+ """判断是否为 Qwen3-Embedding 模型。"""
70
+ return "qwen" in model_name.lower() and "embedding" in model_name.lower()
71
+
72
+
73
+ def _last_token_pool(last_hidden_states: torch.Tensor,
74
+ attention_mask: torch.Tensor) -> torch.Tensor:
75
+ """Qwen3-Embedding 使用 last-token pooling。"""
76
+ left_padding = (attention_mask[:, -1].sum() == attention_mask.shape[0])
77
+ if left_padding:
78
+ return last_hidden_states[:, -1]
79
+ else:
80
+ sequence_lengths = attention_mask.sum(dim=1) - 1
81
+ batch_size = last_hidden_states.shape[0]
82
+ return last_hidden_states[
83
+ torch.arange(batch_size, device=last_hidden_states.device),
84
+ sequence_lengths,
85
+ ]
86
+
87
+
88
+ class QwenEmbedder:
89
+ """Qwen3-Embedding 模型封装,提供与 SentenceTransformer 兼容的 encode() 接口。"""
90
+
91
+ def __init__(self, model_name: str, device: str = "cuda",
92
+ max_length: int = 512):
93
+ from transformers import AutoModel, AutoTokenizer
94
+
95
+ print(f" 加载 tokenizer: {model_name} ...")
96
+ self.tokenizer = AutoTokenizer.from_pretrained(
97
+ model_name, padding_side="left", trust_remote_code=True,
98
+ )
99
+ print(f" 加载模型: {model_name} (bf16) ...")
100
+ self.model = AutoModel.from_pretrained(
101
+ model_name, dtype=torch.bfloat16, trust_remote_code=True,
102
+ ).to(device).eval()
103
+
104
+ self.device = device
105
+ self.max_length = max_length
106
+ self.embed_dim = self.model.config.hidden_size
107
+ print(f" 模型维度: {self.embed_dim}")
108
+ print(f" 设备: {device}")
109
+
110
+ def get_sentence_embedding_dimension(self) -> int:
111
+ return self.embed_dim
112
+
113
+ @torch.no_grad()
114
+ def encode(self, sentences: list[str], **kwargs) -> np.ndarray:
115
+ """编码一个 batch,返回 L2 归一化的 numpy 数组 [B, D]。"""
116
+ batch_dict = self.tokenizer(
117
+ sentences,
118
+ padding=True,
119
+ truncation=True,
120
+ max_length=self.max_length,
121
+ return_tensors="pt",
122
+ ).to(self.device)
123
+
124
+ outputs = self.model(**batch_dict)
125
+ embeddings = _last_token_pool(
126
+ outputs.last_hidden_state, batch_dict["attention_mask"]
127
+ )
128
+ embeddings = F.normalize(embeddings, p=2, dim=1)
129
+ return embeddings.float().cpu().numpy()
130
+
131
+
132
+ # ── 通用工具函数 ─────────────────────────────────────────────────────────────────
133
+
134
+ def load_model(model_name: str, device: str = "cuda"):
135
+ """根据模型名自动选择后端加载。"""
136
+ if _is_qwen_embedding(model_name):
137
+ return QwenEmbedder(model_name, device=device)
138
+ else:
139
+ from sentence_transformers import SentenceTransformer
140
+ model = SentenceTransformer(model_name)
141
+ if device == "cuda" and torch.cuda.is_available():
142
+ model = model.to("cuda")
143
+ print(f" 模型维度: {model.get_sentence_embedding_dimension()}")
144
+ print(f" 设备: {model.device}")
145
+ return model
146
+
147
+
148
+ def load_eval_sentences() -> dict[str, list[str]]:
149
+ """加载评测句,按任务分组。"""
150
+ eval_sents = {}
151
+ for task, files in EVAL_TASKS.items():
152
+ sents = []
153
+ for fname in files:
154
+ fpath = EVAL_DIR / fname
155
+ if not fpath.exists():
156
+ print(f" [警告] 评测文件不存在: {fpath}")
157
+ continue
158
+ for line in open(fpath, encoding="utf-8", errors="ignore"):
159
+ line = line.strip()
160
+ if line:
161
+ sents.append(line)
162
+ eval_sents[task] = sents
163
+ print(f" {task:20s}: {len(sents):>6,} 句")
164
+ return eval_sents
165
+
166
+
167
+ def _cache_key(texts: list[str], model_name: str) -> str:
168
+ """基于文本内容和模型名生成缓存 key。"""
169
+ h = hashlib.md5()
170
+ h.update(model_name.encode())
171
+ h.update(str(len(texts)).encode())
172
+ for t in texts[:5] + texts[-5:]:
173
+ h.update(t.encode("utf-8", errors="ignore"))
174
+ return h.hexdigest()[:12]
175
+
176
+
177
+ def encode_sentences(model, sentences: list[str],
178
+ batch_size: int = 64,
179
+ desc: str = "Encoding",
180
+ cache_path: Path | None = None) -> np.ndarray:
181
+ """
182
+ 编码句子列表,返回 L2-归一化的 embedding 矩阵 [N, D]。
183
+ 支持可选的 .npy 缓存。
184
+ 对 SentenceTransformer 和 QwenEmbedder 均兼容。
185
+ """
186
+ if cache_path and cache_path.exists():
187
+ print(f" {desc}: 从缓存加载 {cache_path.name}")
188
+ embs = np.load(cache_path)
189
+ if embs.shape[0] == len(sentences):
190
+ return embs
191
+ print(f" [警告] 缓存行数不匹配 ({embs.shape[0]} vs {len(sentences)}),重新编码")
192
+
193
+ is_qwen = isinstance(model, QwenEmbedder)
194
+ all_embs = []
195
+ t0 = time.time()
196
+
197
+ for i in range(0, len(sentences), batch_size):
198
+ batch = sentences[i:i + batch_size]
199
+ if is_qwen:
200
+ embs = model.encode(batch)
201
+ else:
202
+ embs = model.encode(
203
+ batch,
204
+ show_progress_bar=False,
205
+ convert_to_numpy=True,
206
+ normalize_embeddings=True,
207
+ device=model.device,
208
+ )
209
+ all_embs.append(embs)
210
+ done = i + len(batch)
211
+ if done % (batch_size * 20) == 0 or done == len(sentences):
212
+ elapsed = time.time() - t0
213
+ speed = done / elapsed if elapsed > 0 else 0
214
+ print(f" {desc}: {done:>8,}/{len(sentences):,} "
215
+ f"({speed:.0f} sent/s)")
216
+
217
+ result = np.vstack(all_embs).astype(np.float32)
218
+
219
+ if cache_path:
220
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
221
+ np.save(cache_path, result)
222
+ mb = result.nbytes / 1024 / 1024
223
+ print(f" → 缓存已保存: {cache_path.name} ({mb:.1f} MB)")
224
+
225
+ return result
226
+
227
+
228
+ def compute_similarity_scores(
229
+ train_embs: np.ndarray,
230
+ eval_embs_dict: dict[str, np.ndarray],
231
+ task_weights: dict[str, float],
232
+ top_k: int = 10,
233
+ batch_size: int = 1024,
234
+ ) -> np.ndarray:
235
+ """
236
+ 计算每个训练句与各评测任务的加权相似度分数。
237
+
238
+ 对每个任务:score = mean(top-K cosine similarities)
239
+ 最终分数 = sum(task_weight * task_score)
240
+
241
+ 返回 [N_train] 的相似度分数数组。
242
+ """
243
+ n_train = train_embs.shape[0]
244
+ final_scores = np.zeros(n_train, dtype=np.float32)
245
+ device = "cuda" if torch.cuda.is_available() else "cpu"
246
+
247
+ for task, eval_embs in eval_embs_dict.items():
248
+ weight = task_weights.get(task, 0.1)
249
+ task_scores = np.zeros(n_train, dtype=np.float32)
250
+
251
+ eval_t = torch.from_numpy(eval_embs).to(device).half() # [N_eval, D]
252
+
253
+ t0 = time.time()
254
+ for i in range(0, n_train, batch_size):
255
+ batch = torch.from_numpy(
256
+ train_embs[i:i + batch_size]
257
+ ).to(device).half() # [B, D]
258
+
259
+ sim = batch @ eval_t.T # [B, N_eval]
260
+ k = min(top_k, sim.shape[1])
261
+ topk_vals = sim.topk(k, dim=1).values # [B, k]
262
+ task_scores[i:i + batch.shape[0]] = (
263
+ topk_vals.mean(dim=1).float().cpu().numpy()
264
+ )
265
+
266
+ elapsed = time.time() - t0
267
+ print(f" {task:20s}: weight={weight:.2f} "
268
+ f"mean={task_scores.mean():.4f} std={task_scores.std():.4f} "
269
+ f"({elapsed:.1f}s)")
270
+
271
+ final_scores += weight * task_scores
272
+ del eval_t
273
+ torch.cuda.empty_cache()
274
+
275
+ return final_scores
276
+
277
+
278
+ # ── 核心采样逻辑 ───────────────────────────────────────────────────────────────
279
+
280
+ def run_strategy_D(
281
+ all_rows: list[dict],
282
+ sim_scores: np.ndarray,
283
+ main_budget: int,
284
+ alpha: float = 0.6,
285
+ seed: int = 42,
286
+ ) -> list[dict]:
287
+ """
288
+ 方案D采样:综合 embedding 相似度和质量分数。
289
+
290
+ final_weight = alpha * sim_norm + (1 - alpha) * quality_norm
291
+ 使用 Gumbel-max trick 在各来源内加权采样,遵守来源词数上限。
292
+ """
293
+ rng = np.random.default_rng(seed)
294
+ n = len(all_rows)
295
+
296
+ sim_min, sim_max = sim_scores.min(), sim_scores.max()
297
+ if sim_max > sim_min:
298
+ sim_norm = (sim_scores - sim_min) / (sim_max - sim_min)
299
+ else:
300
+ sim_norm = np.zeros(n, dtype=np.float32)
301
+
302
+ quality_arr = np.array([r["quality"] for r in all_rows], dtype=np.float32)
303
+ q_min, q_max = quality_arr.min(), quality_arr.max()
304
+ if q_max > q_min:
305
+ q_norm = (quality_arr - q_min) / (q_max - q_min)
306
+ else:
307
+ q_norm = np.ones(n, dtype=np.float32)
308
+
309
+ final_weights = alpha * sim_norm + (1 - alpha) * q_norm
310
+ print(f" 综合权重: alpha={alpha:.2f} "
311
+ f"range=[{final_weights.min():.4f}, {final_weights.max():.4f}] "
312
+ f"mean={final_weights.mean():.4f}")
313
+
314
+ source_indices: dict[str, list[int]] = {src: [] for src in SOURCES}
315
+ for i, row in enumerate(all_rows):
316
+ source_indices[row["source"]].append(i)
317
+
318
+ source_cap_words = {
319
+ src: int(cfg["cap"] * main_budget)
320
+ for src, cfg in SOURCES.items()
321
+ }
322
+
323
+ all_selected = []
324
+ total_words = 0
325
+
326
+ for src, cfg in SOURCES.items():
327
+ idxs = source_indices[src]
328
+ if not idxs:
329
+ print(f" {src:30s}: 无数据,跳过")
330
+ continue
331
+
332
+ cap_words = source_cap_words[src]
333
+ idxs_arr = np.array(idxs)
334
+ src_weights = final_weights[idxs_arr]
335
+
336
+ log_w = np.log(src_weights + 1e-10)
337
+ gumbel = rng.gumbel(size=len(idxs))
338
+ order = np.argsort(-(log_w + gumbel))
339
+
340
+ src_selected = []
341
+ src_words = 0
342
+ for pos in order:
343
+ if src_words >= cap_words:
344
+ break
345
+ idx = idxs[pos]
346
+ row_words = all_rows[idx]["words"]
347
+ if src_words + row_words <= cap_words:
348
+ src_selected.append(idx)
349
+ src_words += row_words
350
+
351
+ total_words += src_words
352
+ all_selected.extend(src_selected)
353
+
354
+ if src_selected:
355
+ sel_sim_mean = sim_scores[np.array(src_selected)].mean()
356
+ else:
357
+ sel_sim_mean = 0.0
358
+ print(f" {src:30s}: {len(src_selected):>7,} 行 {src_words:>9,} 词"
359
+ f" (上限 {cap_words:,}) sim_avg={sel_sim_mean:.4f}")
360
+
361
+ print(f"\n 合计选中: {len(all_selected):,} 行 {total_words:,} 词")
362
+ return [all_rows[i] for i in all_selected]
363
+
364
+
365
+ # ── 入口 ───────────────────────────────────────────────────────────────────────
366
+
367
+ def main():
368
+ parser = argparse.ArgumentParser(
369
+ description="Stage 8 方案D:Embedding 相似度采样")
370
+ parser.add_argument("--model", type=str,
371
+ default="Qwen/Qwen3-Embedding-4B",
372
+ help="Embedding 模型名(默认 Qwen/Qwen3-Embedding-4B)")
373
+ parser.add_argument("--alpha", type=float, default=0.6,
374
+ help="相似度权重(0-1)。0=纯质量,1=纯相似度(默认0.6)")
375
+ parser.add_argument("--top-k", type=int, default=10,
376
+ help="每个任务取 top-K 相似句均值(默认10)")
377
+ parser.add_argument("--batch-size", type=int, default=64,
378
+ help="Encoding batch size(默认64,Qwen 模型建议 32-64)")
379
+ parser.add_argument("--max-length", type=int, default=512,
380
+ help="最大 token 长度(默认512)")
381
+ parser.add_argument("--no-shuffle", action="store_true",
382
+ help="不打乱输出顺序")
383
+ parser.add_argument("--dry-run", action="store_true",
384
+ help="只统计不写文件")
385
+ parser.add_argument("--no-cache", action="store_true",
386
+ help="不使用/不保存 embedding 缓存")
387
+ parser.add_argument("--seed", type=int, default=42,
388
+ help="随机种子(默认42)")
389
+ parser.add_argument("--task-weights", type=str, default=None,
390
+ help="自定义任务权重,格式: task1=w1,task2=w2,...")
391
+ args = parser.parse_args()
392
+
393
+ print("=" * 60)
394
+ print(" Stage 8 方案D:Embedding 相似度采样")
395
+ print("=" * 60)
396
+ print(f" 模型: {args.model}")
397
+ print(f" alpha: {args.alpha} top-K: {args.top_k} batch: {args.batch_size}")
398
+
399
+ # 解析自定义任务权重
400
+ task_weights = DEFAULT_TASK_WEIGHTS.copy()
401
+ if args.task_weights:
402
+ for pair in args.task_weights.split(","):
403
+ k, v = pair.split("=")
404
+ task_weights[k.strip()] = float(v.strip())
405
+ total = sum(task_weights.values())
406
+ task_weights = {k: v / total for k, v in task_weights.items()}
407
+ print(f" 任务权重: { {k: f'{v:.2f}' for k, v in task_weights.items()} }")
408
+
409
+ # ── Step 1: 评测句预算 ───────────────────────────────────────────────────
410
+ print("\n[1/6] 统计评测句词数 ...")
411
+ eval_words = compute_eval_words()
412
+ main_budget = TOTAL_BUDGET - eval_words
413
+ print(f" 评测句: {eval_words:,} 词 → 主语料预算: {main_budget:,} 词")
414
+
415
+ # ── Step 2: 加载训练候选数据 ─────────────────────────────────────────────
416
+ print("\n[2/6] 加载候选训练数据 ...")
417
+ all_rows = load_all_data()
418
+
419
+ # ── Step 3: 加载 embedding 模型 ──────────────────────────────────────────
420
+ print(f"\n[3/6] 加载 embedding 模型: {args.model} ...")
421
+ emb_model = load_model(args.model)
422
+
423
+ # ── Step 4: 编码 ─────────────────────────────────────────────────────────
424
+ print("\n[4/6] 编码句子 ...")
425
+
426
+ # 4a. 编码评测句(按任务)
427
+ print(" 编码评测句 ...")
428
+ eval_sents = load_eval_sentences()
429
+ eval_embs_dict: dict[str, np.ndarray] = {}
430
+ for task, sents in eval_sents.items():
431
+ cache_file = None
432
+ if not args.no_cache:
433
+ ck = _cache_key(sents, args.model)
434
+ cache_file = CACHE_DIR / f"eval_{task}_{ck}.npy"
435
+ eval_embs_dict[task] = encode_sentences(
436
+ emb_model, sents,
437
+ batch_size=args.batch_size,
438
+ desc=f"eval/{task}",
439
+ cache_path=cache_file,
440
+ )
441
+
442
+ # 4b. 编码训练候选句
443
+ print("\n 编码候选训练句 ...")
444
+ train_texts = [r["text"] for r in all_rows]
445
+ train_cache = None
446
+ if not args.no_cache:
447
+ ck = _cache_key(train_texts, args.model)
448
+ train_cache = CACHE_DIR / f"train_{ck}.npy"
449
+ train_embs = encode_sentences(
450
+ emb_model, train_texts,
451
+ batch_size=args.batch_size,
452
+ desc="train",
453
+ cache_path=train_cache,
454
+ )
455
+
456
+ # 释放模型显存
457
+ if isinstance(emb_model, QwenEmbedder):
458
+ del emb_model.model, emb_model.tokenizer
459
+ del emb_model
460
+ torch.cuda.empty_cache()
461
+
462
+ # ── Step 5: 计算相似度分数 ───────────────────────────────────────────────
463
+ print("\n[5/6] 计算相似度分数 ...")
464
+ sim_scores = compute_similarity_scores(
465
+ train_embs, eval_embs_dict, task_weights,
466
+ top_k=args.top_k,
467
+ batch_size=1024,
468
+ )
469
+ print(f" 相似度分数: mean={sim_scores.mean():.4f} "
470
+ f"std={sim_scores.std():.4f} "
471
+ f"range=[{sim_scores.min():.4f}, {sim_scores.max():.4f}]")
472
+
473
+ if not args.no_cache:
474
+ score_path = CACHE_DIR / "sim_scores.npy"
475
+ np.save(score_path, sim_scores)
476
+ print(f" → 相似度分数已保存: {score_path}")
477
+
478
+ del train_embs, eval_embs_dict
479
+
480
+ # ── Step 6: 采样 & 输出 ──────────────────────────────────────────────────
481
+ print("\n[6/6] 执行方案D采样 ...")
482
+ selected = run_strategy_D(
483
+ all_rows, sim_scores, main_budget,
484
+ alpha=args.alpha, seed=args.seed,
485
+ )
486
+
487
+ print_stats(selected, strategy_name="方案D (Embedding 相似度)")
488
+
489
+ if not args.dry_run:
490
+ print(f"\n 写入输出 ...")
491
+ write_output(selected, OUT_D_DIR,
492
+ shuffle=not args.no_shuffle,
493
+ seed=args.seed)
494
+ print(f"\n 完成!输出目录: {OUT_D_DIR}")
495
+ else:
496
+ print("\n[dry-run] 跳过写文件。")
497
+
498
+
499
+ if __name__ == "__main__":
500
+ main()
scripts/01_data/stage9_paraphrase.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 9:Paraphrase 增强
3
+
4
+ 用 Qwen3.5-9B 对训练句生成同义改写,增加语言多样性。
5
+
6
+ 流程:
7
+ 1. 读取 Stage 8 输出的 train.txt
8
+ 2. 按词数裁剪到 original_budget(默认 8M 词),保留头部高质量句
9
+ 3. 从裁剪后的原文中选出适合改写的候选句(8-40 词、格式规范)
10
+ 4. 用 Qwen3.5-9B 批量生成 paraphrase,直到达到 paraphrase_budget(默认 2M 词)
11
+ 5. 合并输出:裁剪后原文 + paraphrase → data/9_paraphrase/train.txt
12
+
13
+ 输入 : data/8_sample_B/train.txt(或指定 Stage 8 输出)
14
+ 输出 : data/9_paraphrase/train.txt
15
+
16
+ 依赖 : Qwen3.5-9B(已下载),需要 GPU
17
+
18
+ 用法:
19
+ python scripts/01_data/stage9_paraphrase.py
20
+ python scripts/01_data/stage9_paraphrase.py --original-budget 8000000 --paraphrase-budget 2000000
21
+ python scripts/01_data/stage9_paraphrase.py --batch-size 16 --seed 42
22
+ python scripts/01_data/stage9_paraphrase.py --dry-run
23
+ python scripts/01_data/stage9_paraphrase.py --resume # 从断点继续
24
+ """
25
+
26
+ import argparse
27
+ import json
28
+ import random
29
+ import re
30
+ import time
31
+ from pathlib import Path
32
+
33
+ ROOT = Path(__file__).parent.parent.parent
34
+ DEFAULT_INPUT = ROOT / "data/8_sample_B/train.txt"
35
+ DEFAULT_OUTPUT = ROOT / "data/9_paraphrase"
36
+ MODEL_PATH = ROOT / "models/qwen3.5-9b/models--Qwen--Qwen3.5-9B/snapshots/c202236235762e1c871ad0ccb60c8ee5ba337b9a"
37
+ CHECKPOINT_PATH = ROOT / "data/9_paraphrase/checkpoint.jsonl"
38
+
39
+ # ── 默认参数 ──────────────────────────────────────────────────────────────────
40
+
41
+ TOTAL_BUDGET = 10_000_000 # 总词数上限
42
+ DEFAULT_ORIG = 8_000_000 # 原文词数
43
+ DEFAULT_PARA = 2_000_000 # paraphrase 词数
44
+ SENTENCES_PER_PROMPT = 10 # 每个 prompt 包含句子数
45
+ MIN_WORDS = 8 # 候选句最少词数
46
+ MAX_WORDS = 40 # 候选句最多词数
47
+
48
+ # ── Prompt ────────────────────────────────────────────────────────────────────
49
+
50
+ SYSTEM_PROMPT = """You are a linguistic paraphrase generator. For each numbered sentence, generate ONE paraphrase that:
51
+ - Preserves the original meaning exactly
52
+ - Uses different wording and/or sentence structure
53
+ - Maintains natural, fluent English
54
+ - Keeps approximately the same length
55
+
56
+ Output format: one paraphrase per line, prefixed with the same number.
57
+ Example input:
58
+ 0: The cat sat on the mat.
59
+ 1: She quickly ran to the store.
60
+
61
+ Example output:
62
+ 0: A cat was sitting on the mat.
63
+ 1: She hurried to the store.
64
+
65
+ ONLY output the numbered paraphrases. No explanations."""
66
+
67
+
68
+ # ── 候选句选择 ────────────────────────────────────────────────────────────────
69
+
70
+ def is_good_candidate(line: str, min_words: int = MIN_WORDS,
71
+ max_words: int = MAX_WORDS) -> bool:
72
+ """判断一行是否适合做 paraphrase"""
73
+ words = line.split()
74
+ n = len(words)
75
+
76
+ # 长度过滤
77
+ if n < min_words or n > max_words:
78
+ return False
79
+
80
+ # 必须以大写字母开头(正常英语句子)
81
+ if not line[0].isupper():
82
+ return False
83
+
84
+ # 必须以句末标点结尾
85
+ if line[-1] not in '.?!':
86
+ return False
87
+
88
+ # 排除 Entity Tracking 合成句("Box N contains...")
89
+ if line.startswith("Box ") and "contains" in line:
90
+ return False
91
+
92
+ # 排除列表/编号行
93
+ if re.match(r'^\d+[\.\)]\s', line):
94
+ return False
95
+
96
+ return True
97
+
98
+
99
+ def select_candidates(lines: list[str], budget_words: int,
100
+ seed: int = 42, min_words: int = MIN_WORDS,
101
+ max_words: int = MAX_WORDS) -> list[int]:
102
+ """
103
+ 从原文中选出适合 paraphrase 的候选句索引。
104
+ 选够 budget_words 的 1.5 倍(预留余量,因为 paraphrase 长度不完全可控)。
105
+ """
106
+ rng = random.Random(seed)
107
+
108
+ # 收集所有候选
109
+ candidates = []
110
+ for i, line in enumerate(lines):
111
+ if is_good_candidate(line, min_words, max_words):
112
+ candidates.append((i, len(line.split())))
113
+
114
+ rng.shuffle(candidates)
115
+
116
+ # 选到 1.5 倍预算
117
+ target = int(budget_words * 1.5)
118
+ selected = []
119
+ total_words = 0
120
+ for idx, wc in candidates:
121
+ if total_words >= target:
122
+ break
123
+ selected.append(idx)
124
+ total_words += wc
125
+
126
+ print(f" 候选句总数: {len(candidates):,}")
127
+ print(f" 选中: {len(selected):,} 句 ({total_words:,} 词,目标 {budget_words:,} 词的 1.5x)")
128
+ return selected
129
+
130
+
131
+ # ── Paraphrase 生成 ──────────────────────────────────────────────────────────
132
+
133
+ def build_prompt(tokenizer, sentences: list[str]) -> str:
134
+ """构建 paraphrase prompt"""
135
+ input_text = "\n".join(f"{i}: {sent}" for i, sent in enumerate(sentences))
136
+ messages = [
137
+ {"role": "system", "content": SYSTEM_PROMPT},
138
+ {"role": "user", "content": input_text},
139
+ ]
140
+ return tokenizer.apply_chat_template(
141
+ messages, tokenize=False, add_generation_prompt=True,
142
+ enable_thinking=False,
143
+ )
144
+
145
+
146
+ def parse_paraphrases(response: str, n_expected: int) -> list[str]:
147
+ """从模型输出中解析 paraphrase 结果"""
148
+ results = [None] * n_expected
149
+
150
+ for line in response.strip().split("\n"):
151
+ line = line.strip()
152
+ if not line:
153
+ continue
154
+ # 匹配 "0: paraphrased text" 格式
155
+ m = re.match(r'^(\d+)\s*[:\.]\s*(.+)$', line)
156
+ if m:
157
+ idx = int(m.group(1))
158
+ text = m.group(2).strip()
159
+ if 0 <= idx < n_expected and text:
160
+ results[idx] = text
161
+
162
+ return results
163
+
164
+
165
+ def load_checkpoint(ckpt_path: Path) -> set[int]:
166
+ """加载已完成的原文行号"""
167
+ done = set()
168
+ if ckpt_path.exists():
169
+ for line in open(ckpt_path, encoding="utf-8"):
170
+ try:
171
+ d = json.loads(line)
172
+ done.add(d["orig_idx"])
173
+ except Exception:
174
+ continue
175
+ return done
176
+
177
+
178
+ def generate_paraphrases(
179
+ lines: list[str],
180
+ candidate_indices: list[int],
181
+ budget_words: int,
182
+ model_path: str,
183
+ batch_size: int = 10,
184
+ max_new_tokens: int = 512,
185
+ resume: bool = False,
186
+ ):
187
+ """
188
+ 批量生成 paraphrase。
189
+
190
+ 返回: list[dict],每个 dict 有 orig_idx, original, paraphrase, words
191
+ """
192
+ import torch
193
+ from transformers import AutoModelForCausalLM, AutoTokenizer
194
+
195
+ # 加载模型
196
+ print(f"\n 加载 tokenizer: {model_path} ...")
197
+ tokenizer = AutoTokenizer.from_pretrained(
198
+ model_path, padding_side="left", trust_remote_code=True,
199
+ )
200
+ if tokenizer.pad_token is None:
201
+ tokenizer.pad_token = tokenizer.eos_token
202
+
203
+ print(f" 加载模型 (bf16) ...")
204
+ model = AutoModelForCausalLM.from_pretrained(
205
+ model_path,
206
+ torch_dtype=torch.bfloat16,
207
+ device_map="auto",
208
+ trust_remote_code=True,
209
+ )
210
+ model.eval()
211
+ print(f" 模型加载完成\n")
212
+
213
+ # 断点续传
214
+ CHECKPOINT_PATH.parent.mkdir(parents=True, exist_ok=True)
215
+ done_indices = load_checkpoint(CHECKPOINT_PATH) if resume else set()
216
+ if done_indices:
217
+ print(f" 断点续传: 已完成 {len(done_indices):,} 句")
218
+
219
+ # 过滤已完成
220
+ remaining = [i for i in candidate_indices if i not in done_indices]
221
+
222
+ results = []
223
+ total_words = 0
224
+ t0 = time.time()
225
+
226
+ # 如果 resume,先加载已有结果
227
+ if resume and CHECKPOINT_PATH.exists():
228
+ for line in open(CHECKPOINT_PATH, encoding="utf-8"):
229
+ try:
230
+ d = json.loads(line)
231
+ results.append(d)
232
+ total_words += d["words"]
233
+ except Exception:
234
+ continue
235
+ print(f" 已有 paraphrase: {len(results):,} 句 ({total_words:,} 词)")
236
+
237
+ ckpt_file = open(CHECKPOINT_PATH, "a", encoding="utf-8") if not resume else \
238
+ open(CHECKPOINT_PATH, "a", encoding="utf-8")
239
+
240
+ try:
241
+ for batch_start in range(0, len(remaining), batch_size):
242
+ if total_words >= budget_words:
243
+ break
244
+
245
+ batch_indices = remaining[batch_start:batch_start + batch_size]
246
+ batch_sentences = [lines[i] for i in batch_indices]
247
+
248
+ # 构建 prompt
249
+ prompt = build_prompt(tokenizer, batch_sentences)
250
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
251
+
252
+ # 生成
253
+ with torch.no_grad():
254
+ outputs = model.generate(
255
+ **inputs,
256
+ max_new_tokens=max_new_tokens,
257
+ temperature=0.7,
258
+ top_p=0.9,
259
+ do_sample=True,
260
+ pad_token_id=tokenizer.pad_token_id,
261
+ )
262
+
263
+ # 解码(只取生成部分)
264
+ gen_tokens = outputs[0][inputs["input_ids"].shape[1]:]
265
+ response = tokenizer.decode(gen_tokens, skip_special_tokens=True)
266
+
267
+ # 解析
268
+ paraphrases = parse_paraphrases(response, len(batch_sentences))
269
+
270
+ for local_i, para in enumerate(paraphrases):
271
+ if para is None:
272
+ continue
273
+ orig_idx = batch_indices[local_i]
274
+ wc = len(para.split())
275
+ record = {
276
+ "orig_idx": orig_idx,
277
+ "original": batch_sentences[local_i],
278
+ "paraphrase": para,
279
+ "words": wc,
280
+ }
281
+ results.append(record)
282
+ total_words += wc
283
+
284
+ # 写入 checkpoint
285
+ ckpt_file.write(json.dumps(record, ensure_ascii=False) + "\n")
286
+
287
+ if total_words >= budget_words:
288
+ break
289
+
290
+ # 进度
291
+ done = batch_start + len(batch_indices)
292
+ elapsed = time.time() - t0
293
+ speed = done / elapsed if elapsed > 0 else 0
294
+ print(f" {done:>6,}/{len(remaining):,} 批次 "
295
+ f"{total_words:>9,}/{budget_words:,} 词 "
296
+ f"({speed:.1f} sent/s, {elapsed:.0f}s)")
297
+
298
+ ckpt_file.flush()
299
+
300
+ finally:
301
+ ckpt_file.close()
302
+
303
+ print(f"\n Paraphrase 完成: {len(results):,} 句 {total_words:,} 词")
304
+ return results
305
+
306
+
307
+ # ── 主流程 ────────────────────────────────────────────────────────────────────
308
+
309
+ def trim_to_budget(lines: list[str], budget_words: int) -> list[str]:
310
+ """
311
+ 裁剪原文到指定词数预算。
312
+ 保留前 N 行直到词数达到 budget。
313
+ """
314
+ kept = []
315
+ total = 0
316
+ for line in lines:
317
+ wc = len(line.split())
318
+ if total + wc > budget_words:
319
+ break
320
+ kept.append(line)
321
+ total += wc
322
+ return kept
323
+
324
+
325
+ def main():
326
+ parser = argparse.ArgumentParser(description="Stage 9: Paraphrase 增强")
327
+ parser.add_argument("--input", type=str, default=str(DEFAULT_INPUT),
328
+ help=f"Stage 8 输出路径(默认 {DEFAULT_INPUT})")
329
+ parser.add_argument("--output-dir", type=str, default=str(DEFAULT_OUTPUT),
330
+ help=f"输出目录(默认 {DEFAULT_OUTPUT})")
331
+ parser.add_argument("--model", type=str, default=str(MODEL_PATH),
332
+ help="Qwen 模型路径")
333
+ parser.add_argument("--original-budget", type=int, default=DEFAULT_ORIG,
334
+ help=f"原文词数预算(默认 {DEFAULT_ORIG:,})")
335
+ parser.add_argument("--paraphrase-budget", type=int, default=DEFAULT_PARA,
336
+ help=f"Paraphrase 词数预算(默认 {DEFAULT_PARA:,})")
337
+ parser.add_argument("--batch-size", type=int, default=SENTENCES_PER_PROMPT,
338
+ help=f"每个 prompt 句子数(默认 {SENTENCES_PER_PROMPT})")
339
+ parser.add_argument("--min-words", type=int, default=MIN_WORDS,
340
+ help=f"候选句最少词数(默认 {MIN_WORDS})")
341
+ parser.add_argument("--max-words", type=int, default=MAX_WORDS,
342
+ help=f"候选句最多词数(默认 {MAX_WORDS})")
343
+ parser.add_argument("--seed", type=int, default=42, help="随机种子")
344
+ parser.add_argument("--dry-run", action="store_true",
345
+ help="只统计不生成")
346
+ parser.add_argument("--resume", action="store_true",
347
+ help="从断点继续")
348
+ args = parser.parse_args()
349
+
350
+ input_path = Path(args.input)
351
+ output_dir = Path(args.output_dir)
352
+ output_dir.mkdir(parents=True, exist_ok=True)
353
+ out_path = output_dir / "train.txt"
354
+
355
+ print("=" * 60)
356
+ print(" Stage 9: Paraphrase 增强")
357
+ print("=" * 60)
358
+ print(f" 输入: {input_path}")
359
+ print(f" 输出: {out_path}")
360
+ print(f" 模型: {args.model}")
361
+ print(f" 原文预算: {args.original_budget:,} 词")
362
+ print(f" Paraphrase 预算: {args.paraphrase_budget:,} 词")
363
+ print(f" 总预算: {args.original_budget + args.paraphrase_budget:,} 词")
364
+ print(f" 候选句长度: {MIN_WORDS}-{MAX_WORDS} 词")
365
+ print(f" Batch size: {args.batch_size}")
366
+ print(f" Seed: {args.seed}")
367
+
368
+ # 1. 读取原文
369
+ print(f"\n[1/4] 读取原文 ...")
370
+ with open(input_path, encoding="utf-8", errors="ignore") as f:
371
+ all_lines = [line.strip() for line in f if line.strip()]
372
+ total_words = sum(len(l.split()) for l in all_lines)
373
+ print(f" 原文: {len(all_lines):,} 行 {total_words:,} 词")
374
+
375
+ # 2. 裁剪原文
376
+ print(f"\n[2/4] 裁剪原文到 {args.original_budget:,} 词 ...")
377
+ trimmed = trim_to_budget(all_lines, args.original_budget)
378
+ trimmed_words = sum(len(l.split()) for l in trimmed)
379
+ print(f" 裁剪后: {len(trimmed):,} 行 {trimmed_words:,} 词")
380
+
381
+ # 3. 选择候选句
382
+ print(f"\n[3/4] 选择 paraphrase 候选句 ...")
383
+ candidates = select_candidates(trimmed, args.paraphrase_budget, args.seed,
384
+ args.min_words, args.max_words)
385
+ print(f" 候选句示例:")
386
+ for i in candidates[:3]:
387
+ print(f" [{i}] {trimmed[i][:80]}...")
388
+
389
+ if args.dry_run:
390
+ print(f"\n[dry-run] 跳过生成。")
391
+ print(f" 预计输出: {trimmed_words:,} 原文 + {args.paraphrase_budget:,} paraphrase"
392
+ f" = {trimmed_words + args.paraphrase_budget:,} 词")
393
+ return
394
+
395
+ # 4. 生成 paraphrase
396
+ print(f"\n[4/4] 生成 paraphrase ...")
397
+ results = generate_paraphrases(
398
+ lines=trimmed,
399
+ candidate_indices=candidates,
400
+ budget_words=args.paraphrase_budget,
401
+ model_path=args.model,
402
+ batch_size=args.batch_size,
403
+ resume=args.resume,
404
+ )
405
+
406
+ # 5. 合并输出
407
+ print(f"\n 写入输出 ...")
408
+
409
+ # 收集 paraphrase 句子
410
+ para_lines = [r["paraphrase"] for r in results]
411
+ para_words = sum(len(l.split()) for l in para_lines)
412
+
413
+ # 打乱 paraphrase 并混入原文
414
+ rng = random.Random(args.seed)
415
+ combined = list(trimmed) + para_lines
416
+ rng.shuffle(combined)
417
+
418
+ with open(out_path, "w", encoding="utf-8") as f:
419
+ for line in combined:
420
+ f.write(line + "\n")
421
+
422
+ final_words = trimmed_words + para_words
423
+ print(f"\n{'=' * 60}")
424
+ print(f" 完成!")
425
+ print(f" 原文: {len(trimmed):,} 行 {trimmed_words:,} 词")
426
+ print(f" Paraphrase: {len(para_lines):,} 行 {para_words:,} 词")
427
+ print(f" 合计: {len(combined):,} 行 {final_words:,} 词")
428
+ print(f" 输出: {out_path}")
429
+
430
+ # 保存统计
431
+ stats_path = output_dir / "stats.json"
432
+ stats = {
433
+ "input": str(input_path),
434
+ "original_lines": len(trimmed),
435
+ "original_words": trimmed_words,
436
+ "paraphrase_lines": len(para_lines),
437
+ "paraphrase_words": para_words,
438
+ "total_lines": len(combined),
439
+ "total_words": final_words,
440
+ "model": args.model,
441
+ "seed": args.seed,
442
+ "batch_size": args.batch_size,
443
+ "min_words": MIN_WORDS,
444
+ "max_words": MAX_WORDS,
445
+ }
446
+ with open(stats_path, "w") as f:
447
+ json.dump(stats, f, indent=2, ensure_ascii=False)
448
+ print(f" 统计: {stats_path}")
449
+
450
+
451
+ if __name__ == "__main__":
452
+ main()
scripts/01_data/vocab_coverage.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ vocab.txt 覆盖率分析
3
+ 对每个训练文件统计:
4
+ - token 级覆盖率:训练文件中的词有多少 % 在 vocab.txt 里
5
+ - 类型级覆盖率:训练文件中的唯一词有多少 % 在 vocab.txt 里
6
+ - OOV(out-of-vocab)最高频词 Top 20
7
+ """
8
+
9
+ import re
10
+ from pathlib import Path
11
+ from collections import Counter
12
+
13
+ VOCAB_PATH = Path(__file__).parent.parent / "evaluation-pipeline-2025/evaluation_pipeline/ewok/vocab.txt"
14
+ DATA_DIR = Path(__file__).parent.parent / "data/raw_files"
15
+
16
+ FILES = [
17
+ "childes.train.txt",
18
+ "gutenberg.train.txt",
19
+ "open_subtitles.train.txt",
20
+ "simple_wiki.train.txt",
21
+ "bnc_spoken.train.txt",
22
+ "open_subtitles_cleaned_ckpt_70.txt",
23
+ "switchboard.train.txt",
24
+ ]
25
+
26
+ def tokenize(line):
27
+ return re.findall(r"[a-zA-Z']+|[0-9]+", line.lower())
28
+
29
+ def analyze(file_name, vocab):
30
+ path = DATA_DIR / file_name
31
+ total_tokens = 0
32
+ in_vocab_tokens = 0
33
+ total_counter = Counter()
34
+ oov_counter = Counter()
35
+
36
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
37
+ for line in f:
38
+ tokens = tokenize(line)
39
+ for t in tokens:
40
+ total_tokens += 1
41
+ total_counter[t] += 1
42
+ if t in vocab:
43
+ in_vocab_tokens += 1
44
+ else:
45
+ oov_counter[t] += 1
46
+
47
+ total_types = len(total_counter)
48
+ in_vocab_types = sum(1 for w in total_counter if w in vocab)
49
+
50
+ token_cov = in_vocab_tokens / total_tokens * 100 if total_tokens else 0
51
+ type_cov = in_vocab_types / total_types * 100 if total_types else 0
52
+
53
+ return {
54
+ "file": file_name,
55
+ "total_tokens": total_tokens,
56
+ "in_vocab_tokens":in_vocab_tokens,
57
+ "token_coverage": round(token_cov, 2),
58
+ "total_types": total_types,
59
+ "in_vocab_types": in_vocab_types,
60
+ "type_coverage": round(type_cov, 2),
61
+ "top20_oov": oov_counter.most_common(20),
62
+ }
63
+
64
+ def main():
65
+ print(f"读取 vocab.txt ...")
66
+ vocab = set(Path(VOCAB_PATH).read_text().splitlines())
67
+ print(f"vocab 大小: {len(vocab):,} 词\n")
68
+
69
+ results = []
70
+ for fname in FILES:
71
+ path = DATA_DIR / fname
72
+ if not path.exists():
73
+ print(f"[skip] {fname} 不存在")
74
+ continue
75
+ print(f"分析: {fname} ...")
76
+ r = analyze(fname, vocab)
77
+ results.append(r)
78
+ print(f" token覆盖率: {r['token_coverage']}% | 类型覆盖率: {r['type_coverage']}%")
79
+ print(f" 总词数: {r['total_tokens']:,} vocab内: {r['in_vocab_tokens']:,}")
80
+ print(f" 总唯一词: {r['total_types']:,} vocab内: {r['in_vocab_types']:,}")
81
+ print(f" Top10 OOV: {[w for w,c in r['top20_oov'][:10]]}")
82
+ print()
83
+
84
+ # 汇总表
85
+ print("\n" + "="*70)
86
+ print(f"{'文件':<40} {'token覆盖率':>10} {'类型覆盖率':>10} {'总词数':>12}")
87
+ print("="*70)
88
+ for r in results:
89
+ print(f"{r['file']:<40} {r['token_coverage']:>9}% {r['type_coverage']:>9}% {r['total_tokens']:>12,}")
90
+
91
+ # 保存结果
92
+ import json
93
+ out = Path(__file__).parent.parent / "results/analysis/vocab_coverage.json"
94
+ out.parent.mkdir(parents=True, exist_ok=True)
95
+ out.write_text(json.dumps(results, ensure_ascii=False, indent=2))
96
+ print(f"\n结果已保存: {out}")
97
+
98
+ if __name__ == "__main__":
99
+ main()
scripts/02_model/train_tokenizer.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 训练 BPE Tokenizer
3
+
4
+ 与官方 baseline (ltg/gpt-bert-babylm-small) 结构完全一致:
5
+ - Normalizer: Prepend空格 + NFKC + 换行处理
6
+ - Pre-tokenizer: GPT-4风格regex切分 + ByteLevel + 最长24字符截断
7
+ - Model: BPE, vocab_size=8192
8
+ - 特殊token: <unk>=0, <s>=1, </s>=2, <pad>=3, <mask>=4
9
+ - Post-processor: 句首自动加 <s>
10
+
11
+ 输入: data/8_sample_B/train.txt
12
+ 输出: models/tokenizer/
13
+
14
+ 用法:
15
+ python scripts/02_model/train_tokenizer.py
16
+ python scripts/02_model/train_tokenizer.py --vocab_size 8192
17
+ python scripts/02_model/train_tokenizer.py --input data/8_sample_B/train.txt
18
+ """
19
+
20
+ import argparse
21
+ import json
22
+ from pathlib import Path
23
+
24
+ from tokenizers import Tokenizer, AddedToken
25
+ from tokenizers.models import BPE
26
+ from tokenizers.trainers import BpeTrainer
27
+ from tokenizers.normalizers import Sequence, Prepend, NFKC, Replace
28
+ from tokenizers.pre_tokenizers import Sequence as PreSeq, Split, ByteLevel
29
+ from tokenizers.processors import TemplateProcessing
30
+ from tokenizers import Regex
31
+ from transformers import PreTrainedTokenizerFast
32
+
33
+ ROOT = Path(__file__).parent.parent.parent
34
+ DEFAULT_INPUT = ROOT / "data/8_sample_B/train.txt"
35
+ DEFAULT_OUT = ROOT / "models/tokenizer"
36
+
37
+ # 与官方 baseline 完全一致的特殊 token 顺序(id 固定)
38
+ SPECIAL_TOKENS = ["<unk>", "<s>", "</s>", "<pad>", "<mask>"]
39
+
40
+
41
+ def build_tokenizer(vocab_size: int) -> tuple[Tokenizer, BpeTrainer]:
42
+ """构造与官方 baseline 相同结构的 tokenizer + trainer。"""
43
+
44
+ # ── 1. Normalizer ────────────────────────────────────────────────────────
45
+ normalizer = Sequence([
46
+ Prepend(prepend=" "),
47
+ NFKC(),
48
+ Replace(Regex(r"\n"), "\n "), # 换行后加空格,保持词边界
49
+ Replace(Regex(r" *\n"), "\n"), # 去掉换行前多余空格
50
+ ])
51
+
52
+ # ── 2. Pre-tokenizer ─────────────────────────────────────────────────────
53
+ # GPT-4 / cl100k 风格的 Unicode-aware 正则切分
54
+ GPT4_REGEX = (
55
+ r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*"
56
+ r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]+"
57
+ r"|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+"
58
+ r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]*"
59
+ r"| ?\p{N}"
60
+ r"| ?[^\s\p{L}\p{N}]+[\r\n/]*"
61
+ r"|\s*[\r\n]+"
62
+ r"|\s+(?!\S)"
63
+ r"|\s+"
64
+ )
65
+ pre_tokenizer = PreSeq([
66
+ Split(pattern=Regex(GPT4_REGEX), behavior="isolated"),
67
+ ByteLevel(add_prefix_space=False, trim_offsets=True, use_regex=False),
68
+ Split(pattern=Regex(r".{1,24}"), behavior="isolated"), # 最长24字符截断
69
+ ])
70
+
71
+ # ── 3. Tokenizer + Trainer ───────────────────────────────────────────────
72
+ tokenizer = Tokenizer(BPE(unk_token="<unk>"))
73
+ tokenizer.normalizer = normalizer
74
+ tokenizer.pre_tokenizer = pre_tokenizer
75
+
76
+ trainer = BpeTrainer(
77
+ vocab_size=vocab_size,
78
+ special_tokens=SPECIAL_TOKENS,
79
+ min_frequency=2,
80
+ show_progress=True,
81
+ )
82
+
83
+ return tokenizer, trainer
84
+
85
+
86
+ def add_post_processor(tokenizer: Tokenizer) -> None:
87
+ """添加 post-processor:句首自动插入 <s>(id=1)。"""
88
+ tokenizer.post_processor = TemplateProcessing(
89
+ single="<s> $A",
90
+ pair="<s> $A <s> $B",
91
+ special_tokens=[("<s>", tokenizer.token_to_id("<s>"))],
92
+ )
93
+
94
+
95
+ def verify_special_token_ids(tokenizer: Tokenizer) -> None:
96
+ """校验特殊 token ID 与官方 baseline 一致。"""
97
+ expected = {"<unk>": 0, "<s>": 1, "</s>": 2, "<pad>": 3, "<mask>": 4}
98
+ ok = True
99
+ for token, expected_id in expected.items():
100
+ actual_id = tokenizer.token_to_id(token)
101
+ status = "✅" if actual_id == expected_id else "❌"
102
+ print(f" {status} {token:10s} expected={expected_id} actual={actual_id}")
103
+ if actual_id != expected_id:
104
+ ok = False
105
+ if not ok:
106
+ raise ValueError("特殊 token ID 与官方 baseline 不一致!")
107
+
108
+
109
+ def save(tokenizer: Tokenizer, out_dir: Path, vocab_size: int) -> None:
110
+ """保存为 HuggingFace PreTrainedTokenizerFast 格式。"""
111
+ out_dir.mkdir(parents=True, exist_ok=True)
112
+
113
+ # 先以原生格式保存
114
+ raw_path = out_dir / "tokenizer.json"
115
+ tokenizer.save(str(raw_path))
116
+
117
+ # 用 transformers 包装,补充 tokenizer_config.json
118
+ fast_tok = PreTrainedTokenizerFast(
119
+ tokenizer_file=str(raw_path),
120
+ bos_token="<s>",
121
+ eos_token="</s>",
122
+ unk_token="<unk>",
123
+ sep_token="</s>",
124
+ pad_token="<pad>",
125
+ cls_token="<s>",
126
+ mask_token="<mask>",
127
+ )
128
+ fast_tok.save_pretrained(str(out_dir))
129
+ print(f"\n 保存到: {out_dir}")
130
+ print(f" 文件列表: {[f.name for f in sorted(out_dir.iterdir())]}")
131
+
132
+
133
+ def smoke_test(out_dir: Path) -> None:
134
+ """简单验证:加载后测试几个句子。"""
135
+ fast_tok = PreTrainedTokenizerFast.from_pretrained(str(out_dir))
136
+ tests = [
137
+ "The cat sat on the mat.",
138
+ "She gave him the book yesterday.",
139
+ "ran swimming swam running",
140
+ "Katherine can't help herself.",
141
+ ]
142
+ print("\n Smoke test:")
143
+ for t in tests:
144
+ tokens = fast_tok.tokenize(t)
145
+ print(f" {repr(t):45s} → {tokens}")
146
+
147
+
148
+ def main():
149
+ parser = argparse.ArgumentParser()
150
+ parser.add_argument("--input", default=str(DEFAULT_INPUT), help="训练文件路径")
151
+ parser.add_argument("--output", default=str(DEFAULT_OUT), help="输出目录")
152
+ parser.add_argument("--vocab_size", default=8192, type=int, help="词表大小(默认8192)")
153
+ args = parser.parse_args()
154
+
155
+ input_path = Path(args.input)
156
+ out_dir = Path(args.output)
157
+
158
+ if not input_path.exists():
159
+ raise FileNotFoundError(f"训练文件不存在: {input_path}")
160
+
161
+ print(f"训练 BPE Tokenizer")
162
+ print(f" 输入 : {input_path} ({input_path.stat().st_size / 1e6:.1f} MB)")
163
+ print(f" 输出 : {out_dir}")
164
+ print(f" vocab_size = {args.vocab_size}")
165
+ print(f" 特殊 token: {SPECIAL_TOKENS}")
166
+ print()
167
+
168
+ tokenizer, trainer = build_tokenizer(args.vocab_size)
169
+
170
+ print("训练中...")
171
+ tokenizer.train(files=[str(input_path)], trainer=trainer)
172
+ print(f"训练完成,实际 vocab size = {tokenizer.get_vocab_size()}")
173
+
174
+ add_post_processor(tokenizer)
175
+
176
+ print("\n特殊 token ID 校验:")
177
+ verify_special_token_ids(tokenizer)
178
+
179
+ save(tokenizer, out_dir, args.vocab_size)
180
+ smoke_test(out_dir)
181
+
182
+ print("\n完成!")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
scripts/02_model/train_tokenizer_morfessor.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 训练 Morfessor+BPE Tokenizer
3
+
4
+ 思路:
5
+ 1. 在训练语料上训练 Morfessor,学习英语形态素边界
6
+ 2. 用 Morfessor 对训练文本预切分(在形态素边界插入空格)
7
+ 3. 在预切分文本上训练 BPE(架构与标准 BPE tokenizer 完全一致)
8
+
9
+ 效果:BPE 不会跨形态素边界合并,保留 WUG/Entity Tracking 有利的形态信息
10
+
11
+ 输入: data/8_sample_B/train.txt
12
+ 输出: models/tokenizer_morfessor/
13
+ ├── tokenizer.json (BPE tokenizer, HuggingFace 格式)
14
+ ├── tokenizer_config.json
15
+ ├── special_tokens_map.json
16
+ └── morfessor.bin (Morfessor 模型,推理时需要)
17
+
18
+ 用法:
19
+ pip install morfessor
20
+ python scripts/02_model/train_tokenizer_morfessor.py
21
+ python scripts/02_model/train_tokenizer_morfessor.py --input data/8_sample_C/train.txt
22
+ """
23
+
24
+ import argparse
25
+ import re
26
+ import sys
27
+ from collections import Counter
28
+ from pathlib import Path
29
+
30
+ try:
31
+ import morfessor
32
+ except ImportError:
33
+ print("请先安装: pip install morfessor")
34
+ sys.exit(1)
35
+
36
+ from tokenizers import Tokenizer, Regex
37
+ from tokenizers.models import BPE
38
+ from tokenizers.trainers import BpeTrainer
39
+ from tokenizers.normalizers import Sequence, Prepend, NFKC, Replace
40
+ from tokenizers.pre_tokenizers import Sequence as PreSeq, Split, ByteLevel
41
+ from tokenizers.processors import TemplateProcessing
42
+ from transformers import PreTrainedTokenizerFast
43
+
44
+ ROOT = Path(__file__).resolve().parent.parent.parent
45
+ DEFAULT_INPUT = ROOT / "data/8_sample_B/train.txt"
46
+ DEFAULT_OUT = ROOT / "models/tokenizer_morfessor"
47
+ SPECIAL_TOKENS = ["<unk>", "<s>", "</s>", "<pad>", "<mask>"]
48
+
49
+ # Morfessor 超参
50
+ CORPUSWEIGHT = 0.01 # 越小切分越激进(默认~1.0 太保守)
51
+ MIN_MORPH_LEN = 2 # 形态素最短长度,过滤 s+it / b+and 等假阳性
52
+ MIN_WORD_LEN = 3 # 短于此长度的词不切分
53
+
54
+
55
+ # ══════════════════════════════════════════════════════════════════════════════
56
+ # Step 1: 训练 Morfessor
57
+ # ══════════════════════════════════════════════════════════════════════════════
58
+
59
+ def train_morfessor_model(input_path: Path, corpusweight: float = CORPUSWEIGHT) -> morfessor.BaselineModel:
60
+ """在训练语料上训练 Morfessor 模型。"""
61
+ print(f"Step 1: 训练 Morfessor 模型 (corpusweight={corpusweight})...")
62
+
63
+ # 统计词频(只取纯字母词,去标点)
64
+ word_counts = Counter()
65
+ with open(input_path) as f:
66
+ for line in f:
67
+ for word in line.split():
68
+ clean = word.strip(".,!?;:\"'()-[]{}…""''").lower()
69
+ if len(clean) >= 2 and clean.isalpha():
70
+ word_counts[clean] += 1
71
+
72
+ print(f" 唯一词数: {len(word_counts):,}")
73
+ print(f" 总词频: {sum(word_counts.values()):,}")
74
+
75
+ model = morfessor.BaselineModel(corpusweight=corpusweight)
76
+ training_data = [(count, word) for word, count in word_counts.items()]
77
+ model.load_data(training_data)
78
+ model.train_batch()
79
+
80
+ # 示例
81
+ samples = [
82
+ "unhappiness", "running", "walked", "beautiful", "government",
83
+ "internationally", "darkness", "singer", "swimming", "nationalization",
84
+ "happiness", "slowly", "governmental", "children", "quickly",
85
+ ]
86
+ print("\n 切分示例:")
87
+ for w in samples:
88
+ segs = model.viterbi_segment(w)[0]
89
+ print(f" {w:25s} → {' + '.join(segs)}")
90
+
91
+ return model
92
+
93
+
94
+ # ══════════════════════════════════════════════════════════════════════════════
95
+ # Step 2: 预切分文本
96
+ # ══════════════════════════════════════════════════════════════════════════════
97
+
98
+ _WORD_RE = re.compile(r'^([^a-zA-Z]*?)([a-zA-Z]+)([^a-zA-Z]*)$')
99
+
100
+
101
+ def presegment_word(word: str, morf_model) -> str:
102
+ """对单词用 Morfessor 找形态素边界,在边界处插入空格。
103
+
104
+ 保留原始大小写:用 Morfessor 切分小写版本,
105
+ 再按 segment 长度映射回原始字符。
106
+
107
+ 过滤规则:所有形态素长度必须 >= MIN_MORPH_LEN,
108
+ 否则视为假阳性(如 s+it, b+and),保持原词不切。
109
+ """
110
+ m = _WORD_RE.match(word)
111
+ if not m:
112
+ return word
113
+
114
+ prefix, core, suffix = m.groups()
115
+ if len(core) < MIN_WORD_LEN:
116
+ return word
117
+
118
+ segments = morf_model.viterbi_segment(core.lower())[0]
119
+ if len(segments) <= 1:
120
+ return word
121
+
122
+ # 过滤:所有形态素必须 >= MIN_MORPH_LEN
123
+ if not all(len(s) >= MIN_MORPH_LEN for s in segments):
124
+ return word
125
+
126
+ # 按 segment 长度从原始 core 中切片(保留大小写)
127
+ parts = []
128
+ pos = 0
129
+ for seg in segments:
130
+ n = len(seg)
131
+ parts.append(core[pos:pos + n])
132
+ pos += n
133
+
134
+ return prefix + ' '.join(parts) + suffix
135
+
136
+
137
+ def presegment_file(input_path: Path, morf_model, output_path: Path) -> Path:
138
+ """对整个文件进行 Morfessor 预切分。"""
139
+ print("\nStep 2: 预切分训练文本...")
140
+
141
+ line_count = 0
142
+ with open(input_path) as fin, open(output_path, 'w') as fout:
143
+ for line in fin:
144
+ if line.strip():
145
+ words = line.split()
146
+ segmented = [presegment_word(w, morf_model) for w in words]
147
+ fout.write(' '.join(segmented) + '\n')
148
+ else:
149
+ fout.write('\n')
150
+ line_count += 1
151
+ if line_count % 200000 == 0:
152
+ print(f" 已处理 {line_count:,} 行...")
153
+
154
+ print(f" 总共处理 {line_count:,} 行")
155
+ print(f" 预切分文件: {output_path} ({output_path.stat().st_size / 1e6:.1f} MB)")
156
+
157
+ # 对比几行
158
+ print("\n 对比示例:")
159
+ shown = 0
160
+ with open(input_path) as f1, open(output_path) as f2:
161
+ for orig, seg in zip(f1, f2):
162
+ if orig.strip() != seg.strip() and 30 < len(orig.strip()) < 150:
163
+ print(f" 原: {orig.strip()}")
164
+ print(f" 切: {seg.strip()}")
165
+ print()
166
+ shown += 1
167
+ if shown >= 5:
168
+ break
169
+
170
+ return output_path
171
+
172
+
173
+ # ══════════════════════════════════════════════════════════════════════════════
174
+ # Step 3: 在预切分文本上训练 BPE
175
+ # ══════════════════════════════════════════════════════════════════════════════
176
+
177
+ def build_and_train_bpe(segmented_file: Path, vocab_size: int) -> Tokenizer:
178
+ """训练 BPE,架构与标准 tokenizer 完全一致。"""
179
+ print(f"\nStep 3: 训练 BPE (vocab_size={vocab_size})...")
180
+
181
+ # Normalizer(与标准 BPE 一致)
182
+ normalizer = Sequence([
183
+ Prepend(prepend=" "),
184
+ NFKC(),
185
+ Replace(Regex(r"\n"), "\n "),
186
+ Replace(Regex(r" *\n"), "\n"),
187
+ ])
188
+
189
+ # Pre-tokenizer(与标准 BPE 一致)
190
+ GPT4_REGEX = (
191
+ r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*"
192
+ r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]+"
193
+ r"|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+"
194
+ r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]*"
195
+ r"| ?\p{N}"
196
+ r"| ?[^\s\p{L}\p{N}]+[\r\n/]*"
197
+ r"|\s*[\r\n]+"
198
+ r"|\s+(?!\S)"
199
+ r"|\s+"
200
+ )
201
+ pre_tokenizer = PreSeq([
202
+ Split(pattern=Regex(GPT4_REGEX), behavior="isolated"),
203
+ ByteLevel(add_prefix_space=False, trim_offsets=True, use_regex=False),
204
+ Split(pattern=Regex(r".{1,24}"), behavior="isolated"),
205
+ ])
206
+
207
+ tokenizer = Tokenizer(BPE(unk_token="<unk>"))
208
+ tokenizer.normalizer = normalizer
209
+ tokenizer.pre_tokenizer = pre_tokenizer
210
+
211
+ trainer = BpeTrainer(
212
+ vocab_size=vocab_size,
213
+ special_tokens=SPECIAL_TOKENS,
214
+ min_frequency=2,
215
+ show_progress=True,
216
+ )
217
+
218
+ tokenizer.train(files=[str(segmented_file)], trainer=trainer)
219
+ print(f" 实际 vocab size: {tokenizer.get_vocab_size()}")
220
+
221
+ # Post-processor: 句首加 <s>
222
+ tokenizer.post_processor = TemplateProcessing(
223
+ single="<s> $A",
224
+ pair="<s> $A <s> $B",
225
+ special_tokens=[("<s>", tokenizer.token_to_id("<s>"))],
226
+ )
227
+
228
+ # 校验特殊 token ID
229
+ expected = {"<unk>": 0, "<s>": 1, "</s>": 2, "<pad>": 3, "<mask>": 4}
230
+ for token, eid in expected.items():
231
+ aid = tokenizer.token_to_id(token)
232
+ status = "✅" if aid == eid else "❌"
233
+ print(f" {status} {token:10s} expected={eid} actual={aid}")
234
+ if aid != eid:
235
+ raise ValueError(f"特殊 token ID 不一致: {token}")
236
+
237
+ return tokenizer
238
+
239
+
240
+ # ══════════════════════════════════════════════════════════════════════════════
241
+ # Step 4: 保存
242
+ # ══════════════════════════════════════════════════════════════════════════════
243
+
244
+ def save_all(tokenizer: Tokenizer, morf_model, output_dir: Path):
245
+ """保存 BPE tokenizer + Morfessor 模型。"""
246
+ print(f"\nStep 4: 保存到 {output_dir}/")
247
+ output_dir.mkdir(parents=True, exist_ok=True)
248
+
249
+ # BPE tokenizer
250
+ raw_path = output_dir / "tokenizer.json"
251
+ tokenizer.save(str(raw_path))
252
+
253
+ fast_tok = PreTrainedTokenizerFast(
254
+ tokenizer_file=str(raw_path),
255
+ bos_token="<s>", eos_token="</s>", unk_token="<unk>",
256
+ sep_token="</s>", pad_token="<pad>", cls_token="<s>", mask_token="<mask>",
257
+ )
258
+ fast_tok.save_pretrained(str(output_dir))
259
+
260
+ # Morfessor 模型
261
+ io = morfessor.MorfessorIO()
262
+ morf_path = output_dir / "morfessor.bin"
263
+ io.write_binary_model_file(str(morf_path), morf_model)
264
+
265
+ print(f" 文件: {sorted(f.name for f in output_dir.iterdir())}")
266
+
267
+
268
+ # ══════════════════════════════════════════════════════════════════════════════
269
+ # Step 5: 验证 & 对比
270
+ # ══════════════════════════════════════════════════════════════════════════════
271
+
272
+ def verify(output_dir: Path, morf_model):
273
+ """加载并对比标准 BPE vs Morfessor+BPE。"""
274
+ print("\nStep 5: 验证 & 对比...")
275
+
276
+ fast_tok = PreTrainedTokenizerFast.from_pretrained(str(output_dir))
277
+
278
+ std_path = ROOT / "models/tokenizer/tokenizer.json"
279
+ if std_path.exists():
280
+ std_tok = Tokenizer.from_file(str(std_path))
281
+ else:
282
+ std_tok = None
283
+
284
+ tests = [
285
+ "The cat sat on the mat.",
286
+ "She was running quickly through the forest.",
287
+ "I don't think he's coming today.",
288
+ "unhappiness",
289
+ "running jumped swimming",
290
+ "The ice is cold and the fire is hot.",
291
+ "nationalization",
292
+ "governmental",
293
+ "The children played happily in the garden.",
294
+ ]
295
+
296
+ for t in tests:
297
+ # Morfessor+BPE: 先预切分
298
+ seg_t = ' '.join(presegment_word(w, morf_model) for w in t.split())
299
+ morf_tokens = fast_tok.tokenize(seg_t)
300
+
301
+ print(f" 原文: {t}")
302
+ if seg_t != t:
303
+ print(f" 预切: {seg_t}")
304
+ if std_tok:
305
+ std_tokens = std_tok.encode(t).tokens
306
+ print(f" 标准BPE ({len(std_tokens):2d}): {std_tokens}")
307
+ print(f" Morf+BPE ({len(morf_tokens):2d}): {morf_tokens}")
308
+ print()
309
+
310
+
311
+ # ══════════════════════════════════════════════════════════════════════════════
312
+ # Main
313
+ # ══════════════════════════════════════════════════════════════════════════════
314
+
315
+ def main():
316
+ parser = argparse.ArgumentParser(description="训练 Morfessor+BPE Tokenizer")
317
+ parser.add_argument("--input", default=str(DEFAULT_INPUT), help="训练文件路径")
318
+ parser.add_argument("--output", default=str(DEFAULT_OUT), help="输出目录")
319
+ parser.add_argument("--vocab_size", default=8192, type=int, help="词表大小")
320
+ parser.add_argument("--corpusweight", default=CORPUSWEIGHT, type=float,
321
+ help=f"Morfessor corpusweight, 越小越激进 (默认{CORPUSWEIGHT})")
322
+ args = parser.parse_args()
323
+
324
+ input_path = Path(args.input)
325
+ output_dir = Path(args.output)
326
+
327
+ if not input_path.exists():
328
+ raise FileNotFoundError(f"训练文件不存在: {input_path}")
329
+
330
+ print(f"═══ 训练 Morfessor+BPE Tokenizer ═══")
331
+ print(f" 输入: {input_path} ({input_path.stat().st_size / 1e6:.1f} MB)")
332
+ print(f" 输出: {output_dir}")
333
+ print(f" vocab_size: {args.vocab_size}")
334
+ print()
335
+
336
+ # Step 1: Morfessor
337
+ morf_model = train_morfessor_model(input_path, corpusweight=args.corpusweight)
338
+
339
+ # Step 2: 预切分
340
+ seg_path = output_dir / "_presegmented_train.txt"
341
+ output_dir.mkdir(parents=True, exist_ok=True)
342
+ presegment_file(input_path, morf_model, seg_path)
343
+
344
+ # Step 3: BPE
345
+ tokenizer = build_and_train_bpe(seg_path, args.vocab_size)
346
+
347
+ # Step 4: 保存
348
+ save_all(tokenizer, morf_model, output_dir)
349
+
350
+ # Step 5: 验证
351
+ verify(output_dir, morf_model)
352
+
353
+ # 删除预切分中间文件
354
+ seg_path.unlink()
355
+ print(f" 已删除中间文件: {seg_path.name}")
356
+
357
+ print("\n═══ 完成!═══")
358
+
359
+
360
+ if __name__ == "__main__":
361
+ main()
scripts/03_training/__init__.py ADDED
File without changes
scripts/03_training/compute_aoa.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AoA Score Computation (from local .pt checkpoints)
3
+ ====================================================
4
+ Computes Spearman correlation between model-derived AoA and human CDI AoA.
5
+ Follows Chang & Bergen (2022), using our per-epoch checkpoints as training steps.
6
+
7
+ Epoch mapping (14M tokens/epoch, 10 epochs):
8
+ epoch 1 → ~14M tokens
9
+ epoch 2 → ~28M tokens
10
+ ...
11
+ epoch 10 → ~140M tokens
12
+
13
+ Steps:
14
+ 1. cdi_human.csv → human AoA per word (age of 50% acquisition)
15
+ 2. Per epoch: load .pt checkpoint → compute mean surprisal for each word
16
+ 3. Fit descending sigmoid to surprisal trajectory → model AoA (inflection point)
17
+ 4. Spearman(model AoA, human AoA)
18
+
19
+ Usage (from project root):
20
+ python -m scripts.03_training.compute_aoa \\
21
+ --checkpoints checkpoints/exp_1.1_gptbert \\
22
+ --words evaluation-pipeline-2025/evaluation_data/full_eval/aoa/cdi_childes.json \\
23
+ --cdi_human evaluation-pipeline-2025/evaluation_data/full_eval/aoa/cdi_human.csv \\
24
+ --tokenizer models/tokenizer
25
+ """
26
+
27
+ import argparse
28
+ import json
29
+ import warnings
30
+ from pathlib import Path
31
+
32
+ import numpy as np
33
+ import pandas as pd
34
+ import torch
35
+ import torch.nn.functional as F
36
+ from scipy.optimize import curve_fit
37
+ from scipy.stats import spearmanr
38
+ from transformers import PreTrainedTokenizerFast
39
+
40
+ warnings.filterwarnings("ignore")
41
+
42
+ TOKENS_PER_EPOCH = 14_000_000 # ~14M tokens per epoch (109030 chunks × 128 tokens)
43
+
44
+
45
+ # ── Human AoA ─────────────────────────────────────────────────────────────────
46
+
47
+ def compute_human_aoa(cdi_path: Path) -> dict[str, float]:
48
+ """Human AoA: first age (months) where >= 50% of children know the word."""
49
+ df = pd.read_csv(cdi_path, index_col=0)
50
+ age_cols = [c for c in df.columns if c != "word"]
51
+ ages = [int(c) for c in age_cols]
52
+
53
+ human_aoa = {}
54
+ for _, row in df.iterrows():
55
+ word = row["word"]
56
+ props = row[age_cols].values.astype(float)
57
+
58
+ aoa = None
59
+ for i, (age, prop) in enumerate(zip(ages, props)):
60
+ if prop >= 0.5:
61
+ if i == 0:
62
+ aoa = float(age)
63
+ else:
64
+ p0, p1 = props[i - 1], prop
65
+ a0, a1 = ages[i - 1], age
66
+ aoa = a0 + (0.5 - p0) / (p1 - p0) * (a1 - a0)
67
+ break
68
+ if aoa is None:
69
+ aoa = float(ages[-1]) + 1.0
70
+ human_aoa[word] = aoa
71
+
72
+ return human_aoa
73
+
74
+
75
+ # ── Surprisal computation ──────────────────────────────────────────────────────
76
+
77
+ def load_model_from_checkpoint(ckpt_path: Path, device: str):
78
+ """Load GPT-BERT model from a .pt training checkpoint."""
79
+ import sys
80
+ from types import SimpleNamespace
81
+ proj_root = Path(__file__).resolve().parent.parent.parent
82
+ if str(proj_root) not in sys.path:
83
+ sys.path.insert(0, str(proj_root))
84
+
85
+ import importlib
86
+ _gptbert_mod = importlib.import_module("scripts.03_training.models.gpt_bert")
87
+ build_gpt_bert = _gptbert_mod.build_gpt_bert
88
+
89
+ ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
90
+ # Config is stored as a nested dict; model sub-dict has all required fields
91
+ model_cfg_dict = ckpt["config"]["model"]
92
+ model_cfg = SimpleNamespace(**model_cfg_dict)
93
+
94
+ model = build_gpt_bert(model_cfg)
95
+ # Strip 'module.' prefix if saved with DataParallel
96
+ state = {k.replace("module.", ""): v for k, v in ckpt["model_state_dict"].items()
97
+ if "tied_weight" not in k}
98
+ model.load_state_dict(state, strict=False)
99
+ model.eval()
100
+ model.to(device)
101
+ return model
102
+
103
+
104
+ def compute_word_surprisal(model, tokenizer, word: str, contexts: list[str],
105
+ device: str) -> float:
106
+ """Mean surprisal of `word` given each context (causal LM style)."""
107
+ surprisals = []
108
+ bos = tokenizer.bos_token or ""
109
+
110
+ for ctx in contexts:
111
+ text = (bos + ctx.strip() + " " + word.strip()).strip()
112
+ enc = tokenizer(text, return_tensors="pt",
113
+ return_offsets_mapping=True, add_special_tokens=False)
114
+ input_ids = enc["input_ids"].to(device)
115
+ offsets = enc["offset_mapping"][0] # (T, 2)
116
+
117
+ # Find token positions corresponding to the target word.
118
+ # BPE encodes "...an airplane" as [..., Ġairplane] where Ġ is the space.
119
+ # The token starts at the space position (len of prefix WITHOUT trailing space).
120
+ full_len = len(bos + ctx.strip())
121
+ phrase_mask = torch.zeros(input_ids.shape[1], dtype=torch.bool)
122
+ for i, (start, end) in enumerate(offsets):
123
+ if start >= full_len and end > full_len:
124
+ phrase_mask[i] = True
125
+
126
+ if phrase_mask.sum() == 0:
127
+ continue
128
+
129
+ with torch.no_grad():
130
+ out = model(input_ids=input_ids)
131
+ # model returns tensor [B, T, V]; out[0] → [T, V]
132
+ logits = out["logits"][0] if isinstance(out, dict) else out[0]
133
+ log_probs = F.log_softmax(logits, dim=-1) # (T, V)
134
+
135
+ # Surprisal = -log p(target | context)
136
+ target_ids = input_ids[0, 1:] # shifted right
137
+ pred_log_probs = log_probs[:-1, :] # predictions at each position
138
+ phrase_mask_shifted = phrase_mask[1:] # align with targets
139
+
140
+ token_surps = -pred_log_probs[
141
+ phrase_mask_shifted,
142
+ target_ids[phrase_mask_shifted]
143
+ ]
144
+ if token_surps.numel() > 0:
145
+ surprisals.append(token_surps.sum().item())
146
+
147
+ return float(np.mean(surprisals)) if surprisals else float("nan")
148
+
149
+
150
+ # ── Sigmoid fitting ────────────────────────────────────────────────────────────
151
+
152
+ def sigmoid(x, L, x0, k, b):
153
+ return L / (1.0 + np.exp(-k * (x - x0))) + b
154
+
155
+
156
+ def fit_model_aoa(token_counts: np.ndarray, surprisals: np.ndarray) -> float | None:
157
+ """Fit descending sigmoid; return inflection point in tokens."""
158
+ x = np.log(token_counts.astype(float) + 1)
159
+ y = surprisals.astype(float)
160
+ mask = np.isfinite(x) & np.isfinite(y)
161
+ x, y = x[mask], y[mask]
162
+ if len(x) < 3:
163
+ return None
164
+
165
+ L_init = y.min() - y.max()
166
+ x0_init = x.mean()
167
+ k_init = -1.0
168
+ b_init = y.max()
169
+
170
+ try:
171
+ popt, _ = curve_fit(
172
+ sigmoid, x, y,
173
+ p0=[L_init, x0_init, k_init, b_init],
174
+ maxfev=5000,
175
+ bounds=([-np.inf, x.min(), -np.inf, -np.inf],
176
+ [0, x.max(), 0, np.inf]),
177
+ )
178
+ return float(np.exp(popt[1]))
179
+ except Exception:
180
+ # Fallback: return token count where surprisal is closest to midpoint
181
+ mid = (y.max() + y.min()) / 2
182
+ idx = np.argmin(np.abs(y - mid))
183
+ return float(np.exp(x[idx]))
184
+
185
+
186
+ # ── Main ──────────────────────────────────────────────────────────────────────
187
+
188
+ def main():
189
+ parser = argparse.ArgumentParser()
190
+ parser.add_argument("--checkpoints", required=True,
191
+ help="Directory with checkpoint_epoch*.pt files")
192
+ parser.add_argument("--words", required=True,
193
+ help="cdi_childes.json (word → contexts)")
194
+ parser.add_argument("--cdi_human", required=True,
195
+ help="cdi_human.csv (word → CDI proportions)")
196
+ parser.add_argument("--tokenizer", default="models/tokenizer",
197
+ help="Tokenizer path")
198
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
199
+ parser.add_argument("--max_words", type=int, default=None,
200
+ help="Limit words for quick testing")
201
+ parser.add_argument("--verbose", action="store_true")
202
+ args = parser.parse_args()
203
+
204
+ ckpt_dir = Path(args.checkpoints)
205
+
206
+ # Prefer AoA sub-checkpoints (chck_*M.pt) — exact token-count alignment
207
+ # Fall back to epoch checkpoints if AoA dir doesn't exist
208
+ aoa_subdir = ckpt_dir / "aoa"
209
+ if aoa_subdir.is_dir():
210
+ def _parse_millions(p: Path) -> int:
211
+ return int(p.stem.replace("chck_", "").replace("M", ""))
212
+ ckpt_files = sorted(aoa_subdir.glob("chck_*M.pt"), key=_parse_millions)
213
+ token_counts = np.array([_parse_millions(p) * 1_000_000 for p in ckpt_files])
214
+ print(f"Using AoA sub-checkpoints ({len(ckpt_files)} files) from {aoa_subdir}")
215
+ else:
216
+ ckpt_files = sorted(ckpt_dir.glob("checkpoint_epoch*.pt"),
217
+ key=lambda p: int(p.stem.split("epoch")[1]))
218
+ if not ckpt_files:
219
+ raise FileNotFoundError(
220
+ f"No checkpoint_epoch*.pt in {ckpt_dir} and no aoa/ sub-directory")
221
+ token_counts = np.array(
222
+ [(i + 1) * TOKENS_PER_EPOCH for i in range(len(ckpt_files))]
223
+ )
224
+ print(f"Using epoch checkpoints ({len(ckpt_files)} files) — coarse resolution")
225
+
226
+ print(f"Checkpoints: {[p.name for p in ckpt_files]}")
227
+ print(f"Token counts: {[f'{t/1e6:.0f}M' for t in token_counts]}")
228
+
229
+ # Load tokenizer
230
+ tokenizer = PreTrainedTokenizerFast.from_pretrained(args.tokenizer)
231
+
232
+ # Load word → contexts
233
+ with open(args.words) as f:
234
+ word_contexts = json.load(f)
235
+ words = list(word_contexts.keys())
236
+ if args.max_words:
237
+ words = words[:args.max_words]
238
+ print(f"Evaluating {len(words)} words on {args.device}")
239
+
240
+ # Compute surprisal per word per checkpoint
241
+ word_surprisals: dict[str, list[float]] = {w: [] for w in words}
242
+
243
+ for epoch_idx, ckpt_path in enumerate(ckpt_files):
244
+ print(f"\n[{epoch_idx+1}/{len(ckpt_files)}] Loading {ckpt_path.name} ...")
245
+ model = load_model_from_checkpoint(ckpt_path, args.device)
246
+
247
+ for word in words:
248
+ contexts = [c["context"] for c in word_contexts[word]]
249
+ s = compute_word_surprisal(model, tokenizer, word, contexts, args.device)
250
+ word_surprisals[word].append(s)
251
+ if args.verbose:
252
+ print(f" {word}: {s:.3f}")
253
+
254
+ del model
255
+ if args.device == "cuda":
256
+ torch.cuda.empty_cache()
257
+
258
+ # Human AoA
259
+ print("\nComputing human AoA ...")
260
+ human_aoa = compute_human_aoa(Path(args.cdi_human))
261
+
262
+ # Model AoA via sigmoid fitting
263
+ print("Fitting sigmoid curves ...")
264
+ model_aoa = {}
265
+ for word in words:
266
+ surps = np.array(word_surprisals[word])
267
+ aoa = fit_model_aoa(token_counts, surps)
268
+ if aoa is not None:
269
+ model_aoa[word] = aoa
270
+
271
+ # Spearman correlation
272
+ common = sorted(set(human_aoa) & set(model_aoa))
273
+ h = np.array([human_aoa[w] for w in common])
274
+ m = np.array([model_aoa[w] for w in common])
275
+
276
+ rho, pval = spearmanr(h, m)
277
+
278
+ print("\n" + "=" * 50)
279
+ print(f" AoA Spearman ρ = {rho:.4f} (p={pval:.4f})")
280
+ print(f" Words used = {len(common)}")
281
+ print("=" * 50)
282
+
283
+ return rho
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
scripts/03_training/config.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modular training configuration for BabyLM Challenge 2026.
3
+ Loads from YAML, supports CLI overrides.
4
+ """
5
+
6
+ import yaml
7
+ from dataclasses import dataclass, field, asdict
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ ROOT = Path(__file__).resolve().parent.parent.parent
12
+
13
+ @dataclass
14
+ class DataConfig:
15
+ train_file: str = "data/8_sample_B/train.txt"
16
+ tokenizer: str = "bpe" # "bpe" | "morfessor_bpe"
17
+ tokenizer_path: str = "" # auto-resolved if empty
18
+ morfessor_model_path: str = "" # auto-resolved if empty
19
+ max_seq_len: int = 128
20
+ packing: str = "concat" # "concat" (pack into chunks) | "sentence" (per-sentence with padding)
21
+
22
+ def __post_init__(self):
23
+ if not self.tokenizer_path:
24
+ if self.tokenizer == "bpe":
25
+ self.tokenizer_path = str(ROOT / "models/tokenizer")
26
+ else:
27
+ self.tokenizer_path = str(ROOT / "models/tokenizer_morfessor")
28
+ if not self.morfessor_model_path and self.tokenizer == "morfessor_bpe":
29
+ self.morfessor_model_path = str(ROOT / "models/tokenizer_morfessor/morfessor.bin")
30
+
31
+ @dataclass
32
+ class ModelConfig:
33
+ arch: str = "gpt_bert" # "gpt_bert" | "gpt2" | "modernized_bert" | "xlstm"
34
+ hidden_size: int = 384
35
+ num_layers: int = 12
36
+ num_heads: int = 6
37
+ intermediate_size: int = 1280
38
+ max_position_embeddings: int = 512
39
+ dropout: float = 0.1
40
+ layer_norm_eps: float = 1e-7
41
+ vocab_size: int = 8192
42
+ # Architecture improvements (modular toggles)
43
+ use_rope: bool = False # Rotary Position Embeddings (replaces relative pos)
44
+ use_geglu: bool = True # GeGLU activation (official GPT-BERT uses GeGLU)
45
+ use_pre_norm: bool = True # Pre-LayerNorm (official GPT-BERT uses pre-norm)
46
+ rope_theta: float = 10000.0
47
+ # GPT-BERT specific
48
+ position_bucket_size: int = 32 # relative position encoding bucket size
49
+ z_loss_weight: float = 1e-4 # z-loss regularization weight
50
+ use_attention_gate: bool = False # GELU gate on V projection (model_extra.py)
51
+ use_dwa: bool = False # Dynamic Weight Averaging / DenseFormer (model_extra.py)
52
+ # RTD (ELECTRA) specific
53
+ rtd_lambda: float = 50.0 # weight for discriminator loss
54
+ gen_size_ratio: float = 0.33 # generator size relative to discriminator
55
+ # MoE (Mixture of Experts) — replaces FFN with sparse expert routing
56
+ use_moe: bool = False
57
+ moe_num_experts: int = 32 # total number of experts per layer
58
+ moe_top_k: int = 4 # number of experts activated per token
59
+ moe_expert_size: int = 48 # intermediate dim per expert (Linear(384,48)->Linear(48,384))
60
+ moe_freq_penalty: float = 0.01 # soft load-balancing penalty on router logits
61
+ # Attention Residuals — depth-wise attention over block outputs (Kimi Team, 2026)
62
+ use_attn_res: bool = False
63
+ attn_res_num_blocks: int = 4 # 12 layers / 4 = 3 layers per block
64
+
65
+ @dataclass
66
+ class EmbeddingConfig:
67
+ type: str = "standard" # "standard" | "nhot"
68
+ init: str = "random" # "random" | "fasttext"
69
+ fasttext_path: str = "" # path to pre-trained fasttext model
70
+
71
+ @dataclass
72
+ class TrainingConfig:
73
+ objective: str = "gpt_bert" # "gpt_bert" | "clm" | "mlm" | "rtd" | "amlm"
74
+ epochs: int = 10
75
+ batch_size: int = 64
76
+ gradient_accumulation_steps: int = 1
77
+ learning_rate: float = 1.41e-2
78
+ weight_decay: float = 0.1
79
+ warmup_ratio: float = 0.06
80
+ max_grad_norm: float = 2.0
81
+ fp16: bool = True
82
+ seed: int = 42
83
+ # Multi-GPU
84
+ num_gpus: int = 1
85
+ # GPT-BERT MNTP/CLM ratio
86
+ mntp_ratio: int = 15 # 15 MNTP batches per 1 CLM batch
87
+ # Multi-token prediction (reverse curriculum)
88
+ use_mtp: bool = False # enable multi-token prediction
89
+ mtp_k_start: int = 2 # predict k tokens ahead at start
90
+ mtp_k_end: int = 1 # decay to k=1 (standard) at end
91
+ mtp_switch_epoch: int = 5 # switch from k_start to k_end at this epoch
92
+
93
+ @dataclass
94
+ class MaskingConfig:
95
+ type: str = "standard" # "standard" | "amlm" | "frequency"
96
+ mask_ratio: float = 0.30 # GPT-BERT starts at 30%
97
+ mask_ratio_end: float = 0.15 # decays to 15% over training
98
+ # AMLM specific
99
+ amlm_lambda: float = 0.2 # interpolation between adaptive and uniform
100
+ amlm_update_interval: int = 200 # update mask probs every N steps
101
+ amlm_min_ratio: float = 0.05
102
+ amlm_max_ratio: float = 0.50
103
+ # Frequency masking: low-freq tokens get higher mask probability
104
+ freq_alpha: float = 0.3 # interpolation: alpha*freq_based + (1-alpha)*uniform
105
+
106
+ @dataclass
107
+ class OptimizerConfig:
108
+ type: str = "lamb" # "adam" | "adamw" | "lamb" | "muon"
109
+ betas: tuple = (0.9, 0.98)
110
+ eps: float = 1e-6
111
+ forgetter: bool = False # reset optimizer state each epoch
112
+
113
+ @dataclass
114
+ class DistillationConfig:
115
+ enabled: bool = False
116
+ teacher_model: str = "" # HuggingFace model name/path
117
+ temperature: float = 4.0
118
+ alpha: float = 0.5 # weight for distillation loss vs task loss
119
+
120
+ @dataclass
121
+ class CheckpointConfig:
122
+ save_dir: str = "checkpoints"
123
+ save_every_epoch: bool = True
124
+ averaging: bool = False
125
+ avg_last_k: int = 3 # average last k checkpoints
126
+ save_aoa_checkpoints: bool = True # save at 1M,2M,...,9M,10M,...,100M cumulative tokens for AoA eval
127
+
128
+ @dataclass
129
+ class ExperimentConfig:
130
+ name: str = "default"
131
+ data: DataConfig = field(default_factory=DataConfig)
132
+ model: ModelConfig = field(default_factory=ModelConfig)
133
+ embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
134
+ training: TrainingConfig = field(default_factory=TrainingConfig)
135
+ masking: MaskingConfig = field(default_factory=MaskingConfig)
136
+ optimizer: OptimizerConfig = field(default_factory=OptimizerConfig)
137
+ distillation: DistillationConfig = field(default_factory=DistillationConfig)
138
+ checkpoint: CheckpointConfig = field(default_factory=CheckpointConfig)
139
+
140
+
141
+ def load_config(yaml_path: str | Path) -> ExperimentConfig:
142
+ """Load config from YAML file."""
143
+ with open(yaml_path) as f:
144
+ raw = yaml.safe_load(f)
145
+
146
+ cfg = ExperimentConfig()
147
+ if "name" in raw:
148
+ cfg.name = raw["name"]
149
+
150
+ # Map each section to sub-config
151
+ section_map = {
152
+ "data": (cfg.data, DataConfig),
153
+ "model": (cfg.model, ModelConfig),
154
+ "embedding": (cfg.embedding, EmbeddingConfig),
155
+ "training": (cfg.training, TrainingConfig),
156
+ "masking": (cfg.masking, MaskingConfig),
157
+ "optimizer": (cfg.optimizer, OptimizerConfig),
158
+ "distillation": (cfg.distillation, DistillationConfig),
159
+ "checkpoint": (cfg.checkpoint, CheckpointConfig),
160
+ }
161
+
162
+ for section_name, (sub_cfg, _cls) in section_map.items():
163
+ if section_name in raw:
164
+ for k, v in raw[section_name].items():
165
+ if hasattr(sub_cfg, k):
166
+ # Handle tuple fields specially (betas)
167
+ if k == "betas" and isinstance(v, list):
168
+ v = tuple(v)
169
+ setattr(sub_cfg, k, v)
170
+
171
+ # Reset auto-resolved paths so __post_init__ re-derives them from the
172
+ # (possibly updated) tokenizer type, unless the YAML set them explicitly.
173
+ data_section = raw.get("data", {})
174
+ if "tokenizer_path" not in data_section:
175
+ cfg.data.tokenizer_path = ""
176
+ if "morfessor_model_path" not in data_section:
177
+ cfg.data.morfessor_model_path = ""
178
+ cfg.data.__post_init__()
179
+
180
+ return cfg
181
+
182
+
183
+ def config_to_dict(cfg: ExperimentConfig) -> dict:
184
+ """Convert config to dict for logging."""
185
+ return asdict(cfg)
scripts/03_training/configs/exp_A.yaml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "exp_A_bpe_standard"
2
+
3
+ data:
4
+ train_file: "data/8_sample_B/train.txt"
5
+ tokenizer: "bpe"
6
+ max_seq_len: 128
7
+
8
+ model:
9
+ arch: "gpt_bert"
10
+ hidden_size: 384
11
+ num_layers: 12
12
+ num_heads: 6
13
+ intermediate_size: 1280
14
+ use_geglu: true
15
+ use_pre_norm: true
16
+ dropout: 0.1
17
+ position_bucket_size: 32
18
+ z_loss_weight: 0.0001
19
+ # MoE (Mixture of Experts)
20
+ use_moe: false
21
+ # moe_num_experts: 32
22
+ # moe_top_k: 4
23
+ # moe_expert_size: 48
24
+ # moe_freq_penalty: 0.01
25
+ # Attention Residuals
26
+ use_attn_res: false
27
+ # attn_res_num_blocks: 4
28
+
29
+ embedding:
30
+ type: "standard"
31
+ init: "random"
32
+
33
+ training:
34
+ objective: "gpt_bert"
35
+ epochs: 10
36
+ batch_size: 64
37
+ learning_rate: 0.0141
38
+ weight_decay: 0.1
39
+ max_grad_norm: 2.0
40
+ mntp_ratio: 15
41
+
42
+ masking:
43
+ type: "standard"
44
+ mask_ratio: 0.30
45
+ mask_ratio_end: 0.15
46
+
47
+ optimizer:
48
+ type: "lamb"
scripts/03_training/configs/exp_B.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "exp_B_bpe_nhot"
2
+
3
+ data:
4
+ train_file: "data/8_sample_B/train.txt"
5
+ tokenizer: "bpe"
6
+ max_seq_len: 128
7
+
8
+ model:
9
+ arch: "gpt_bert"
10
+ hidden_size: 384
11
+ num_layers: 12
12
+ num_heads: 6
13
+ intermediate_size: 1280
14
+ use_geglu: true
15
+ use_pre_norm: true
16
+ dropout: 0.1
17
+ position_bucket_size: 32
18
+ z_loss_weight: 0.0001
19
+
20
+ embedding:
21
+ type: "nhot"
22
+ init: "random"
23
+
24
+ training:
25
+ objective: "gpt_bert"
26
+ epochs: 10
27
+ batch_size: 64
28
+ learning_rate: 0.0141
29
+ weight_decay: 0.1
30
+ max_grad_norm: 2.0
31
+ mntp_ratio: 15
32
+
33
+ masking:
34
+ type: "standard"
35
+ mask_ratio: 0.30
36
+ mask_ratio_end: 0.15
37
+
38
+ optimizer:
39
+ type: "lamb"
scripts/03_training/configs/exp_C.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "exp_C_morfessor_standard"
2
+
3
+ data:
4
+ train_file: "data/8_sample_B/train.txt"
5
+ tokenizer: "morfessor_bpe"
6
+ max_seq_len: 128
7
+
8
+ model:
9
+ arch: "gpt_bert"
10
+ hidden_size: 384
11
+ num_layers: 12
12
+ num_heads: 6
13
+ intermediate_size: 1280
14
+ use_geglu: true
15
+ use_pre_norm: true
16
+ dropout: 0.1
17
+ position_bucket_size: 32
18
+ z_loss_weight: 0.0001
19
+
20
+ embedding:
21
+ type: "standard"
22
+ init: "random"
23
+
24
+ training:
25
+ objective: "gpt_bert"
26
+ epochs: 10
27
+ batch_size: 64
28
+ learning_rate: 0.0141
29
+ weight_decay: 0.1
30
+ max_grad_norm: 2.0
31
+ mntp_ratio: 15
32
+
33
+ masking:
34
+ type: "standard"
35
+ mask_ratio: 0.30
36
+ mask_ratio_end: 0.15
37
+
38
+ optimizer:
39
+ type: "lamb"
scripts/03_training/configs/exp_D.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "exp_D_morfessor_nhot"
2
+
3
+ data:
4
+ train_file: "data/8_sample_B/train.txt"
5
+ tokenizer: "morfessor_bpe"
6
+ max_seq_len: 128
7
+
8
+ model:
9
+ arch: "gpt_bert"
10
+ hidden_size: 384
11
+ num_layers: 12
12
+ num_heads: 6
13
+ intermediate_size: 1280
14
+ use_geglu: true
15
+ use_pre_norm: true
16
+ dropout: 0.1
17
+ position_bucket_size: 32
18
+ z_loss_weight: 0.0001
19
+
20
+ embedding:
21
+ type: "nhot"
22
+ init: "random"
23
+
24
+ training:
25
+ objective: "gpt_bert"
26
+ epochs: 10
27
+ batch_size: 64
28
+ learning_rate: 0.0141
29
+ weight_decay: 0.1
30
+ max_grad_norm: 2.0
31
+ mntp_ratio: 15
32
+
33
+ masking:
34
+ type: "standard"
35
+ mask_ratio: 0.30
36
+ mask_ratio_end: 0.15
37
+
38
+ optimizer:
39
+ type: "lamb"