timorobrecht commited on
Commit
b187e25
·
verified ·
1 Parent(s): eebe8da

Add fast tokenizer and benchmark classification compatibility

Browse files
__pycache__/tokenization_pinyin_code.cpython-312.pyc CHANGED
Binary files a/__pycache__/tokenization_pinyin_code.cpython-312.pyc and b/__pycache__/tokenization_pinyin_code.cpython-312.pyc differ
 
config.json CHANGED
@@ -8,8 +8,9 @@
8
  "AutoModelForCausalLM": "modeling_pinyin_code.PinyinCodeForCausalLM",
9
  "AutoTokenizer": [
10
  "tokenization_pinyin_code.EncodedMandarinTokenizer",
11
- null
12
- ]
 
13
  },
14
  "block_size": 512,
15
  "bos_token_id": 2,
@@ -26,9 +27,9 @@
26
  "n_layer": 8,
27
  "num_attention_heads": 8,
28
  "num_hidden_layers": 8,
29
- "pad_token_id": 0,
30
- "patch_pathlib_utf8_open": true,
31
- "transformers_version": "5.10.2",
32
  "unk_token_id": 1,
33
  "use_cache": false,
34
  "vocab_size": 16000
 
8
  "AutoModelForCausalLM": "modeling_pinyin_code.PinyinCodeForCausalLM",
9
  "AutoTokenizer": [
10
  "tokenization_pinyin_code.EncodedMandarinTokenizer",
11
+ "tokenization_pinyin_code.EncodedMandarinTokenizerFast"
12
+ ],
13
+ "AutoModelForSequenceClassification": "modeling_pinyin_code.PinyinCodeForSequenceClassification"
14
  },
15
  "block_size": 512,
16
  "bos_token_id": 2,
 
27
  "n_layer": 8,
28
  "num_attention_heads": 8,
29
  "num_hidden_layers": 8,
30
+ "pad_token_id": 0,
31
+ "patch_pathlib_utf8_open": true,
32
+ "transformers_version": "5.10.2",
33
  "unk_token_id": 1,
34
  "use_cache": false,
35
  "vocab_size": 16000
configuration_pinyin_code.py CHANGED
@@ -1,56 +1,56 @@
1
- """Configuration for the Transformers-compatible pinyin-code causal LM."""
2
-
3
- from __future__ import annotations
4
-
5
- import functools
6
- import os
7
- import pathlib
8
-
9
- from transformers import PretrainedConfig
10
-
11
-
12
- _UTF8_PATH_OPEN_PATCH_MARKER = "_pinyin_code_utf8_path_open_patch"
13
-
14
-
15
- def install_utf8_path_open_patch() -> None:
16
- """Default text-mode ``Path.open`` calls to UTF-8 when encoding is omitted.
17
-
18
- Some external Windows evaluation pipelines call ``Path.open("r")`` on
19
- UTF-8 JSONL data before specifying an encoding. The model is loaded before
20
- those datasets, so this narrow compatibility shim lets such pipelines read
21
- Mandarin evaluation files without repository-side changes. Explicit
22
- encodings and binary modes are left untouched.
23
- """
24
- current_open = pathlib.Path.open
25
- if getattr(current_open, _UTF8_PATH_OPEN_PATCH_MARKER, False):
26
- return
27
-
28
- @functools.wraps(current_open)
29
- def utf8_default_open(
30
- self,
31
- mode: str = "r",
32
- buffering: int = -1,
33
- encoding: str | None = None,
34
- errors: str | None = None,
35
- newline: str | None = None,
36
- ):
37
- if encoding is None and "b" not in mode:
38
- encoding = "utf-8"
39
- return current_open(
40
- self,
41
- mode=mode,
42
- buffering=buffering,
43
- encoding=encoding,
44
- errors=errors,
45
- newline=newline,
46
- )
47
-
48
- setattr(utf8_default_open, _UTF8_PATH_OPEN_PATCH_MARKER, True)
49
- pathlib.Path.open = utf8_default_open
50
-
51
-
52
- class PinyinCodeConfig(PretrainedConfig):
53
- """Configuration for the compact GPT-style pinyin-code decoder."""
54
 
55
  model_type = "pinyin_code"
56
 
@@ -63,15 +63,15 @@ class PinyinCodeConfig(PretrainedConfig):
63
  n_embd: int = 256,
64
  dropout: float = 0.1,
65
  bos_token_id: int | None = None,
66
- eos_token_id: int | None = None,
67
- pad_token_id: int | None = None,
68
- unk_token_id: int | None = None,
69
- patch_pathlib_utf8_open: bool = False,
70
- **kwargs,
71
- ) -> None:
72
- super().__init__(
73
- bos_token_id=bos_token_id,
74
- eos_token_id=eos_token_id,
75
  pad_token_id=pad_token_id,
76
  unk_token_id=unk_token_id,
77
  **kwargs,
@@ -85,13 +85,13 @@ class PinyinCodeConfig(PretrainedConfig):
85
  self.num_hidden_layers = n_layer
86
  self.num_attention_heads = n_head
87
  self.hidden_size = n_embd
88
- self.max_position_embeddings = block_size
89
- self.is_decoder = True
90
- self.is_encoder_decoder = False
91
- self.use_cache = False
92
- self.patch_pathlib_utf8_open = patch_pathlib_utf8_open
93
- if (
94
- patch_pathlib_utf8_open
95
- and os.environ.get("PINYIN_CODE_DISABLE_UTF8_OPEN_PATCH") != "1"
96
- ):
97
- install_utf8_path_open_patch()
 
1
+ """Configuration for the Transformers-compatible pinyin-code causal LM."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import os
7
+ import pathlib
8
+
9
+ from transformers import PretrainedConfig
10
+
11
+
12
+ _UTF8_PATH_OPEN_PATCH_MARKER = "_pinyin_code_utf8_path_open_patch"
13
+
14
+
15
+ def install_utf8_path_open_patch() -> None:
16
+ """Default text-mode ``Path.open`` calls to UTF-8 when encoding is omitted.
17
+
18
+ Some external Windows evaluation pipelines call ``Path.open("r")`` on
19
+ UTF-8 JSONL data before specifying an encoding. The model is loaded before
20
+ those datasets, so this narrow compatibility shim lets such pipelines read
21
+ Mandarin evaluation files without repository-side changes. Explicit
22
+ encodings and binary modes are left untouched.
23
+ """
24
+ current_open = pathlib.Path.open
25
+ if getattr(current_open, _UTF8_PATH_OPEN_PATCH_MARKER, False):
26
+ return
27
+
28
+ @functools.wraps(current_open)
29
+ def utf8_default_open(
30
+ self,
31
+ mode: str = "r",
32
+ buffering: int = -1,
33
+ encoding: str | None = None,
34
+ errors: str | None = None,
35
+ newline: str | None = None,
36
+ ):
37
+ if encoding is None and "b" not in mode:
38
+ encoding = "utf-8"
39
+ return current_open(
40
+ self,
41
+ mode=mode,
42
+ buffering=buffering,
43
+ encoding=encoding,
44
+ errors=errors,
45
+ newline=newline,
46
+ )
47
+
48
+ setattr(utf8_default_open, _UTF8_PATH_OPEN_PATCH_MARKER, True)
49
+ pathlib.Path.open = utf8_default_open
50
+
51
+
52
+ class PinyinCodeConfig(PretrainedConfig):
53
+ """Configuration for the compact GPT-style pinyin-code decoder."""
54
 
55
  model_type = "pinyin_code"
56
 
 
63
  n_embd: int = 256,
64
  dropout: float = 0.1,
65
  bos_token_id: int | None = None,
66
+ eos_token_id: int | None = None,
67
+ pad_token_id: int | None = None,
68
+ unk_token_id: int | None = None,
69
+ patch_pathlib_utf8_open: bool = False,
70
+ **kwargs,
71
+ ) -> None:
72
+ super().__init__(
73
+ bos_token_id=bos_token_id,
74
+ eos_token_id=eos_token_id,
75
  pad_token_id=pad_token_id,
76
  unk_token_id=unk_token_id,
77
  **kwargs,
 
85
  self.num_hidden_layers = n_layer
86
  self.num_attention_heads = n_head
87
  self.hidden_size = n_embd
88
+ self.max_position_embeddings = block_size
89
+ self.is_decoder = True
90
+ self.is_encoder_decoder = False
91
+ self.use_cache = False
92
+ self.patch_pathlib_utf8_open = patch_pathlib_utf8_open
93
+ if (
94
+ patch_pathlib_utf8_open
95
+ and os.environ.get("PINYIN_CODE_DISABLE_UTF8_OPEN_PATCH") != "1"
96
+ ):
97
+ install_utf8_path_open_patch()
modeling_pinyin_code.py CHANGED
@@ -5,9 +5,13 @@ from __future__ import annotations
5
  import torch
6
  from torch import nn
7
  from torch.nn import functional as F
8
- from transformers import PreTrainedModel
9
- from transformers.generation import GenerationMixin
10
- from transformers.modeling_outputs import BaseModelOutput, CausalLMOutput
 
 
 
 
11
 
12
  from .configuration_pinyin_code import PinyinCodeConfig
13
 
@@ -98,7 +102,7 @@ class TransformerBlock(nn.Module):
98
  return x
99
 
100
 
101
- class PinyinCodePreTrainedModel(PreTrainedModel):
102
  """Base class for pinyin-code Transformers models."""
103
 
104
  config_class = PinyinCodeConfig
@@ -110,113 +114,113 @@ class PinyinCodePreTrainedModel(PreTrainedModel):
110
  nn.init.normal_(module.weight, mean=0.0, std=0.02)
111
  if module.bias is not None:
112
  nn.init.zeros_(module.bias)
113
- elif isinstance(module, nn.Embedding):
114
- nn.init.normal_(module.weight, mean=0.0, std=0.02)
115
-
116
-
117
- class PinyinCodeModel(PinyinCodePreTrainedModel):
118
- """Base decoder model returned by ``AutoModel``."""
119
-
120
- def __init__(self, config: PinyinCodeConfig, init_weights: bool = True) -> None:
121
- super().__init__(config)
122
- self.token_embedding = nn.Embedding(config.vocab_size, config.n_embd)
123
- self.position_embedding = nn.Embedding(config.block_size, config.n_embd)
124
- self.dropout = nn.Dropout(config.dropout)
125
- self.blocks = nn.ModuleList(TransformerBlock(config) for _ in range(config.n_layer))
126
- self.ln_f = nn.LayerNorm(config.n_embd)
127
- if init_weights:
128
- self.post_init()
129
-
130
- def get_input_embeddings(self) -> nn.Embedding:
131
- return self.token_embedding
132
-
133
- def set_input_embeddings(self, value: nn.Embedding) -> None:
134
- self.token_embedding = value
135
-
136
- def forward(
137
- self,
138
- input_ids: torch.Tensor | None = None,
139
- attention_mask: torch.Tensor | None = None,
140
- inputs_embeds: torch.Tensor | None = None,
141
- position_ids: torch.Tensor | None = None,
142
- output_hidden_states: bool | None = None,
143
- return_dict: bool | None = None,
144
- **kwargs,
145
- ) -> BaseModelOutput | tuple:
146
- return_dict = True if return_dict is None else return_dict
147
- output_hidden_states = (
148
- self.config.output_hidden_states
149
- if output_hidden_states is None
150
- else output_hidden_states
151
- )
152
-
153
- if input_ids is None and inputs_embeds is None:
154
- raise ValueError("You must provide either input_ids or inputs_embeds")
155
- if input_ids is not None and inputs_embeds is not None:
156
- raise ValueError("You cannot provide both input_ids and inputs_embeds")
157
-
158
- if inputs_embeds is None:
159
- _, seq_len = input_ids.shape
160
- if seq_len > self.config.block_size:
161
- raise ValueError(
162
- f"Sequence length {seq_len} exceeds block size {self.config.block_size}"
163
- )
164
- inputs_embeds = self.token_embedding(input_ids)
165
- else:
166
- seq_len = inputs_embeds.shape[1]
167
- if seq_len > self.config.block_size:
168
- raise ValueError(
169
- f"Sequence length {seq_len} exceeds block size {self.config.block_size}"
170
- )
171
-
172
- if position_ids is None:
173
- if attention_mask is not None:
174
- position_ids = attention_mask.long().cumsum(dim=-1) - 1
175
- position_ids = position_ids.clamp_min(0)
176
- else:
177
- position_ids = torch.arange(seq_len, device=inputs_embeds.device)
178
- position_ids = position_ids[:, -seq_len:] if position_ids.ndim == 2 else position_ids
179
-
180
- x = inputs_embeds + self.position_embedding(position_ids)
181
- x = self.dropout(x)
182
- all_hidden_states = (x,) if output_hidden_states else None
183
- for block in self.blocks:
184
- x = block(x, attention_mask=attention_mask)
185
- if output_hidden_states:
186
- all_hidden_states = all_hidden_states + (x,)
187
- hidden_states = self.ln_f(x)
188
- if output_hidden_states:
189
- all_hidden_states = all_hidden_states + (hidden_states,)
190
-
191
- if not return_dict:
192
- output = (hidden_states,)
193
- if output_hidden_states:
194
- output = output + (all_hidden_states,)
195
- return output
196
-
197
- return BaseModelOutput(
198
- last_hidden_state=hidden_states,
199
- hidden_states=all_hidden_states,
200
- )
201
-
202
-
203
  class PinyinCodeForCausalLM(PinyinCodeModel, GenerationMixin):
204
- """Compact GPT-style causal language model using the original architecture."""
205
-
206
- _tied_weights_keys = {"lm_head.weight": "token_embedding.weight"}
207
- _keys_to_ignore_on_load_missing = [r"lm_head\.weight"]
208
-
209
- def __init__(self, config: PinyinCodeConfig) -> None:
210
- super().__init__(config, init_weights=False)
211
- self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
212
- self.post_init()
213
- self.tie_weights()
214
-
215
- def get_output_embeddings(self) -> nn.Linear:
216
- return self.lm_head
217
-
218
- def set_output_embeddings(self, new_embeddings: nn.Linear) -> None:
219
- self.lm_head = new_embeddings
220
 
221
  def tie_weights(self, *args, **kwargs) -> None:
222
  self.lm_head.weight = self.token_embedding.weight
@@ -242,20 +246,82 @@ class PinyinCodeForCausalLM(PinyinCodeModel, GenerationMixin):
242
  "position_ids": position_ids,
243
  }
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  def forward(
246
  self,
247
  input_ids: torch.Tensor | None = None,
248
  attention_mask: torch.Tensor | None = None,
249
  labels: torch.Tensor | None = None,
250
- inputs_embeds: torch.Tensor | None = None,
251
  position_ids: torch.Tensor | None = None,
 
 
252
  output_hidden_states: bool | None = None,
253
  return_dict: bool | None = None,
254
  **kwargs,
255
- ) -> CausalLMOutput | tuple:
256
  return_dict = True if return_dict is None else return_dict
257
 
258
- decoder_outputs = PinyinCodeModel.forward(
259
  self,
260
  input_ids=input_ids,
261
  attention_mask=attention_mask,
@@ -263,25 +329,61 @@ class PinyinCodeForCausalLM(PinyinCodeModel, GenerationMixin):
263
  position_ids=position_ids,
264
  output_hidden_states=output_hidden_states,
265
  return_dict=True,
 
266
  )
267
- logits = self.lm_head(decoder_outputs.last_hidden_state)
268
-
269
- loss = None
270
- if labels is not None:
271
- loss = F.cross_entropy(
272
- logits[:, :-1, :].contiguous().view(-1, logits.size(-1)),
273
- labels[:, 1:].contiguous().view(-1),
274
- ignore_index=-100,
275
- )
276
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  if not return_dict:
278
  output = (logits,)
279
- if decoder_outputs.hidden_states is not None:
280
- output = output + (decoder_outputs.hidden_states,)
 
 
 
281
  return ((loss,) + output) if loss is not None else output
282
 
283
- return CausalLMOutput(
284
  loss=loss,
285
  logits=logits,
286
- hidden_states=decoder_outputs.hidden_states,
 
287
  )
 
5
  import torch
6
  from torch import nn
7
  from torch.nn import functional as F
8
+ from transformers import PreTrainedModel
9
+ from transformers.generation import GenerationMixin
10
+ from transformers.modeling_outputs import (
11
+ BaseModelOutput,
12
+ CausalLMOutput,
13
+ SequenceClassifierOutput,
14
+ )
15
 
16
  from .configuration_pinyin_code import PinyinCodeConfig
17
 
 
102
  return x
103
 
104
 
105
+ class PinyinCodePreTrainedModel(PreTrainedModel):
106
  """Base class for pinyin-code Transformers models."""
107
 
108
  config_class = PinyinCodeConfig
 
114
  nn.init.normal_(module.weight, mean=0.0, std=0.02)
115
  if module.bias is not None:
116
  nn.init.zeros_(module.bias)
117
+ elif isinstance(module, nn.Embedding):
118
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
119
+
120
+
121
+ class PinyinCodeModel(PinyinCodePreTrainedModel):
122
+ """Base decoder model returned by ``AutoModel``."""
123
+
124
+ def __init__(self, config: PinyinCodeConfig, init_weights: bool = True) -> None:
125
+ super().__init__(config)
126
+ self.token_embedding = nn.Embedding(config.vocab_size, config.n_embd)
127
+ self.position_embedding = nn.Embedding(config.block_size, config.n_embd)
128
+ self.dropout = nn.Dropout(config.dropout)
129
+ self.blocks = nn.ModuleList(TransformerBlock(config) for _ in range(config.n_layer))
130
+ self.ln_f = nn.LayerNorm(config.n_embd)
131
+ if init_weights:
132
+ self.post_init()
133
+
134
+ def get_input_embeddings(self) -> nn.Embedding:
135
+ return self.token_embedding
136
+
137
+ def set_input_embeddings(self, value: nn.Embedding) -> None:
138
+ self.token_embedding = value
139
+
140
+ def forward(
141
+ self,
142
+ input_ids: torch.Tensor | None = None,
143
+ attention_mask: torch.Tensor | None = None,
144
+ inputs_embeds: torch.Tensor | None = None,
145
+ position_ids: torch.Tensor | None = None,
146
+ output_hidden_states: bool | None = None,
147
+ return_dict: bool | None = None,
148
+ **kwargs,
149
+ ) -> BaseModelOutput | tuple:
150
+ return_dict = True if return_dict is None else return_dict
151
+ output_hidden_states = (
152
+ self.config.output_hidden_states
153
+ if output_hidden_states is None
154
+ else output_hidden_states
155
+ )
156
+
157
+ if input_ids is None and inputs_embeds is None:
158
+ raise ValueError("You must provide either input_ids or inputs_embeds")
159
+ if input_ids is not None and inputs_embeds is not None:
160
+ raise ValueError("You cannot provide both input_ids and inputs_embeds")
161
+
162
+ if inputs_embeds is None:
163
+ _, seq_len = input_ids.shape
164
+ if seq_len > self.config.block_size:
165
+ raise ValueError(
166
+ f"Sequence length {seq_len} exceeds block size {self.config.block_size}"
167
+ )
168
+ inputs_embeds = self.token_embedding(input_ids)
169
+ else:
170
+ seq_len = inputs_embeds.shape[1]
171
+ if seq_len > self.config.block_size:
172
+ raise ValueError(
173
+ f"Sequence length {seq_len} exceeds block size {self.config.block_size}"
174
+ )
175
+
176
+ if position_ids is None:
177
+ if attention_mask is not None:
178
+ position_ids = attention_mask.long().cumsum(dim=-1) - 1
179
+ position_ids = position_ids.clamp_min(0)
180
+ else:
181
+ position_ids = torch.arange(seq_len, device=inputs_embeds.device)
182
+ position_ids = position_ids[:, -seq_len:] if position_ids.ndim == 2 else position_ids
183
+
184
+ x = inputs_embeds + self.position_embedding(position_ids)
185
+ x = self.dropout(x)
186
+ all_hidden_states = (x,) if output_hidden_states else None
187
+ for block in self.blocks:
188
+ x = block(x, attention_mask=attention_mask)
189
+ if output_hidden_states:
190
+ all_hidden_states = all_hidden_states + (x,)
191
+ hidden_states = self.ln_f(x)
192
+ if output_hidden_states:
193
+ all_hidden_states = all_hidden_states + (hidden_states,)
194
+
195
+ if not return_dict:
196
+ output = (hidden_states,)
197
+ if output_hidden_states:
198
+ output = output + (all_hidden_states,)
199
+ return output
200
+
201
+ return BaseModelOutput(
202
+ last_hidden_state=hidden_states,
203
+ hidden_states=all_hidden_states,
204
+ )
205
+
206
+
207
  class PinyinCodeForCausalLM(PinyinCodeModel, GenerationMixin):
208
+ """Compact GPT-style causal language model using the original architecture."""
209
+
210
+ _tied_weights_keys = {"lm_head.weight": "token_embedding.weight"}
211
+ _keys_to_ignore_on_load_missing = [r"lm_head\.weight"]
212
+
213
+ def __init__(self, config: PinyinCodeConfig) -> None:
214
+ super().__init__(config, init_weights=False)
215
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
216
+ self.post_init()
217
+ self.tie_weights()
218
+
219
+ def get_output_embeddings(self) -> nn.Linear:
220
+ return self.lm_head
221
+
222
+ def set_output_embeddings(self, new_embeddings: nn.Linear) -> None:
223
+ self.lm_head = new_embeddings
224
 
225
  def tie_weights(self, *args, **kwargs) -> None:
226
  self.lm_head.weight = self.token_embedding.weight
 
246
  "position_ids": position_ids,
247
  }
248
 
249
+ def forward(
250
+ self,
251
+ input_ids: torch.Tensor | None = None,
252
+ attention_mask: torch.Tensor | None = None,
253
+ labels: torch.Tensor | None = None,
254
+ inputs_embeds: torch.Tensor | None = None,
255
+ position_ids: torch.Tensor | None = None,
256
+ output_hidden_states: bool | None = None,
257
+ return_dict: bool | None = None,
258
+ **kwargs,
259
+ ) -> CausalLMOutput | tuple:
260
+ return_dict = True if return_dict is None else return_dict
261
+
262
+ decoder_outputs = PinyinCodeModel.forward(
263
+ self,
264
+ input_ids=input_ids,
265
+ attention_mask=attention_mask,
266
+ inputs_embeds=inputs_embeds,
267
+ position_ids=position_ids,
268
+ output_hidden_states=output_hidden_states,
269
+ return_dict=True,
270
+ )
271
+ logits = self.lm_head(decoder_outputs.last_hidden_state)
272
+
273
+ loss = None
274
+ if labels is not None:
275
+ loss = F.cross_entropy(
276
+ logits[:, :-1, :].contiguous().view(-1, logits.size(-1)),
277
+ labels[:, 1:].contiguous().view(-1),
278
+ ignore_index=-100,
279
+ )
280
+
281
+ if not return_dict:
282
+ output = (logits,)
283
+ if decoder_outputs.hidden_states is not None:
284
+ output = output + (decoder_outputs.hidden_states,)
285
+ return ((loss,) + output) if loss is not None else output
286
+
287
+ return CausalLMOutput(
288
+ loss=loss,
289
+ logits=logits,
290
+ hidden_states=decoder_outputs.hidden_states,
291
+ )
292
+
293
+
294
+ class PinyinCodeForSequenceClassification(PinyinCodeModel):
295
+ """Sequence classifier using the pinyin-code decoder backbone."""
296
+
297
+ _keys_to_ignore_on_load_unexpected = [r"lm_head\.weight"]
298
+
299
+ def __init__(self, config: PinyinCodeConfig) -> None:
300
+ super().__init__(config, init_weights=False)
301
+ self.num_labels = config.num_labels
302
+ classifier_dropout = getattr(config, "classifier_dropout", None)
303
+ if classifier_dropout is None:
304
+ classifier_dropout = getattr(config, "hidden_dropout_prob", config.dropout)
305
+ self.score_dropout = nn.Dropout(classifier_dropout)
306
+ self.classifier = nn.Linear(config.hidden_size, config.num_labels)
307
+ self.post_init()
308
+
309
  def forward(
310
  self,
311
  input_ids: torch.Tensor | None = None,
312
  attention_mask: torch.Tensor | None = None,
313
  labels: torch.Tensor | None = None,
314
+ token_type_ids: torch.Tensor | None = None,
315
  position_ids: torch.Tensor | None = None,
316
+ inputs_embeds: torch.Tensor | None = None,
317
+ output_attentions: bool | None = None,
318
  output_hidden_states: bool | None = None,
319
  return_dict: bool | None = None,
320
  **kwargs,
321
+ ) -> SequenceClassifierOutput | tuple:
322
  return_dict = True if return_dict is None else return_dict
323
 
324
+ outputs = PinyinCodeModel.forward(
325
  self,
326
  input_ids=input_ids,
327
  attention_mask=attention_mask,
 
329
  position_ids=position_ids,
330
  output_hidden_states=output_hidden_states,
331
  return_dict=True,
332
+ **kwargs,
333
  )
334
+
335
+ hidden_states = outputs.last_hidden_state
336
+ batch_size, sequence_length, _ = hidden_states.shape
337
+ if attention_mask is not None:
338
+ sequence_indices = attention_mask.to(hidden_states.device).long().sum(dim=-1) - 1
339
+ else:
340
+ sequence_indices = torch.full(
341
+ (batch_size,),
342
+ sequence_length - 1,
343
+ dtype=torch.long,
344
+ device=hidden_states.device,
345
+ )
346
+ sequence_indices = sequence_indices.clamp(min=0, max=sequence_length - 1)
347
+ batch_indices = torch.arange(batch_size, device=hidden_states.device)
348
+ pooled_hidden_states = hidden_states[batch_indices, sequence_indices]
349
+ logits = self.classifier(self.score_dropout(pooled_hidden_states))
350
+
351
+ loss = None
352
+ if labels is not None:
353
+ labels = labels.to(logits.device)
354
+ if getattr(self.config, "problem_type", None) is None:
355
+ if self.num_labels == 1:
356
+ self.config.problem_type = "regression"
357
+ elif labels.dtype in (torch.long, torch.int):
358
+ self.config.problem_type = "single_label_classification"
359
+ else:
360
+ self.config.problem_type = "multi_label_classification"
361
+
362
+ if self.config.problem_type == "regression":
363
+ if self.num_labels == 1:
364
+ loss = F.mse_loss(logits.squeeze(), labels.squeeze())
365
+ else:
366
+ loss = F.mse_loss(logits, labels)
367
+ elif self.config.problem_type == "single_label_classification":
368
+ loss = F.cross_entropy(
369
+ logits.view(-1, self.num_labels),
370
+ labels.view(-1),
371
+ )
372
+ elif self.config.problem_type == "multi_label_classification":
373
+ loss = F.binary_cross_entropy_with_logits(logits, labels)
374
+
375
  if not return_dict:
376
  output = (logits,)
377
+ if outputs.hidden_states is not None:
378
+ output = output + (outputs.hidden_states,)
379
+ attentions = getattr(outputs, "attentions", None)
380
+ if attentions is not None:
381
+ output = output + (attentions,)
382
  return ((loss,) + output) if loss is not None else output
383
 
384
+ return SequenceClassifierOutput(
385
  loss=loss,
386
  logits=logits,
387
+ hidden_states=outputs.hidden_states,
388
+ attentions=getattr(outputs, "attentions", None),
389
  )
preprocessing/__pycache__/preprocess.cpython-312.pyc ADDED
Binary file (18.2 kB). View file
 
preprocessing/__pycache__/split_long_sentencepiece_lines.cpython-312.pyc ADDED
Binary file (6.42 kB). View file
 
preprocessing/extract_babylm_zho.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Extract BabyLM Chinese data from Hugging Face into JSONL.
2
+
3
+ The dataset is gated. Accept the terms at:
4
+ https://huggingface.co/datasets/BabyLM-community/babylm-zho
5
+
6
+ Then log in with `huggingface-cli login` or set `HF_TOKEN`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Any, Iterable
16
+
17
+
18
+ DATASET_ID = "BabyLM-community/babylm-zho"
19
+
20
+
21
+ def hf_token() -> str | None:
22
+ return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
23
+
24
+
25
+ def load_rows(split: str, streaming: bool) -> Iterable[dict[str, Any]]:
26
+ try:
27
+ from datasets import load_dataset
28
+ except ImportError as exc:
29
+ raise SystemExit(
30
+ "Missing dependency: install it with `py -m pip install datasets`."
31
+ ) from exc
32
+
33
+ kwargs: dict[str, Any] = {
34
+ "path": DATASET_ID,
35
+ "split": split,
36
+ "streaming": streaming,
37
+ }
38
+ token = hf_token()
39
+ if token:
40
+ kwargs["token"] = token
41
+
42
+ return load_dataset(**kwargs)
43
+
44
+
45
+ def keep_row(row: dict[str, Any], args: argparse.Namespace) -> bool:
46
+ if args.category and row.get("category") not in args.category:
47
+ return False
48
+ if args.script and row.get("script") not in args.script:
49
+ return False
50
+ if args.language and row.get("language") not in args.language:
51
+ return False
52
+ return True
53
+
54
+
55
+ def clean_row(row: dict[str, Any], text_only: bool) -> dict[str, Any]:
56
+ if text_only:
57
+ return {
58
+ "doc_id": row.get("doc-id"),
59
+ "text": row.get("text", ""),
60
+ }
61
+ return dict(row)
62
+
63
+
64
+ def extract(args: argparse.Namespace) -> int:
65
+ args.output.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ written = 0
68
+ with args.output.open("w", encoding="utf-8", newline="\n") as out:
69
+ for row in load_rows(args.split, args.streaming):
70
+ if args.max_docs is not None and written >= args.max_docs:
71
+ break
72
+ if not keep_row(row, args):
73
+ continue
74
+
75
+ out.write(json.dumps(clean_row(row, args.text_only), ensure_ascii=False))
76
+ out.write("\n")
77
+ written += 1
78
+
79
+ return written
80
+
81
+
82
+ def parse_args() -> argparse.Namespace:
83
+ parser = argparse.ArgumentParser(
84
+ description=f"Extract {DATASET_ID} from Hugging Face into JSONL."
85
+ )
86
+ parser.add_argument("--split", default="train")
87
+ parser.add_argument("--output", type=Path, default=Path("data/babylm_zho.jsonl"))
88
+ parser.add_argument("--max-docs", type=int, default=None)
89
+ parser.add_argument("--category", action="append")
90
+ parser.add_argument("--script", action="append")
91
+ parser.add_argument("--language", action="append")
92
+ parser.add_argument("--text-only", action="store_true")
93
+ parser.add_argument(
94
+ "--streaming",
95
+ action=argparse.BooleanOptionalAction,
96
+ default=True,
97
+ help="Stream by default so the full dataset is not downloaded first.",
98
+ )
99
+ return parser.parse_args()
100
+
101
+
102
+ def main() -> None:
103
+ args = parse_args()
104
+ count = extract(args)
105
+ print(f"Wrote {count:,} records to {args.output}")
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
preprocessing/preprocess.py CHANGED
@@ -1,333 +1,393 @@
1
- """Preprocess BabyLM Mandarin JSONL into word-preserving pinyin initial codes."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import logging
8
- import re
9
- import sys
10
- import unicodedata
11
- from pathlib import Path
12
- from typing import Any, Iterable, Literal
13
-
14
- import jieba
15
- from pypinyin import Style, pinyin
16
-
17
-
18
- LABELS = {
19
- "题干": "<QUESTION>",
20
- "选项": "<OPTIONS>",
21
- "答案": "<ANSWER>",
22
- "解析": "<EXPLANATION>",
23
- }
24
-
25
- PUNCTUATION = set("。,、?!:;.,?!:;()[]{}<>《》【】“”\"'‘’「」『』—-~…/\\")
26
- CHINESE_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
27
- Transliteration = Literal["pinyin-code", "pinyin-initial", "hanzi"]
28
- LATIN_LETTER = r"A-Za-zÀ-ÖØ-öø-ÿĀ-ſƀ-ɏɐ-ʯ"
29
- LATIN_ALNUM_PATTERN = (
30
- rf"(?:[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
31
- rf"(?:[-_][{LATIN_LETTER}0-9]+)*|"
32
- rf"[0-9]+[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
33
- rf"(?:[-_][{LATIN_LETTER}0-9]+)*)"
34
- )
35
- LATIN_ALNUM_RE = re.compile(LATIN_ALNUM_PATTERN)
36
- URL_RE = re.compile(r"\b(?:https?://\S*|www\.\S+)", flags=re.I)
37
- DISCARDED_UNICODE_CATEGORIES = {"Cc", "Cf", "Co", "Cs", "Cn"}
38
-
39
- # Match protected markers before ordinary words so tokens like <ANSWER> survive
40
- # the later English/punctuation handling as a single vocabulary item.
41
- TOKEN_RE = re.compile(
42
- r"<[A-Z_]+>|"
43
- r"[\u3400-\u4dbf\u4e00-\u9fff]+|"
44
- rf"{LATIN_ALNUM_PATTERN}|"
45
- r"\S"
46
- )
47
-
48
-
49
- def require_dependencies() -> None:
50
- """Fail early with a concise install hint and quiet jieba startup logging."""
51
- if jieba is None or Style is None or pinyin is None:
52
- raise SystemExit(
53
- "Missing dependency: install with `py -m pip install jieba pypinyin`."
54
- )
55
- jieba.setLogLevel(logging.WARNING)
56
-
57
-
58
- def normalize_text(text: str) -> str:
59
- """Replace task-specific surface forms with stable special tokens.
60
-
61
- This happens before tokenization so multi-character patterns such as
62
- ``$$...$$`` and empty brackets cannot be split into punctuation pieces.
63
- """
64
- text = unicodedata.normalize("NFKC", text)
65
- text = URL_RE.sub(" <URL> ", text)
66
- text = re.sub(r"\$\$.*?\$\$", " <MATH> ", text, flags=re.DOTALL)
67
- text = re.sub(r"[((]\s*[))]", " <BLANK> ", text)
68
-
69
- for label, marker in LABELS.items():
70
- text = re.sub(rf"{label}\s*[::]", f" {marker} ", text)
71
-
72
- text = re.sub(
73
- rf"(?<![{LATIN_LETTER}])yes(?![{LATIN_LETTER}])",
74
- " <YES> ",
75
- text,
76
- flags=re.I,
77
- )
78
- text = re.sub(
79
- rf"(?<![{LATIN_LETTER}])no(?![{LATIN_LETTER}])",
80
- " <NO> ",
81
- text,
82
- flags=re.I,
83
- )
84
- text = re.sub(
85
- rf"(?<![{LATIN_LETTER}])[ABCD](?=\s*[::..、\)])",
86
- r" \g<0> ",
87
- text,
88
- )
89
- text = re.sub(
90
- rf"(?<![{LATIN_LETTER}0-9])[-+]?\d+(?:[.,]\d+)*(?:%|%)?"
91
- rf"(?![{LATIN_LETTER}0-9])",
92
- " <NUM> ",
93
- text,
94
- )
95
-
96
- text = text.replace("(", "(").replace(")", ")")
97
- text = re.sub(r"\s+", " ", text)
98
- return text.strip()
99
-
100
-
101
- def latin_token_to_model_token(token: str) -> str:
102
- """Normalize non-Mandarin alphanumeric tokens without losing option labels."""
103
- upper = token.upper()
104
- return upper if upper in {"A", "B", "C", "D"} else token.lower()
105
-
106
-
107
- def should_preserve_fallback_token(token: str) -> bool:
108
- """Return true for visible non-Hanzi letters, punctuation, and symbols."""
109
- if token == "\ufffd":
110
- return False
111
- for char in token:
112
- category = unicodedata.category(char)
113
- if category in DISCARDED_UNICODE_CATEGORIES:
114
- return False
115
- if category[0] not in {"L", "P", "S"}:
116
- return False
117
- return True
118
-
119
-
120
- def split_tone3_syllable(syllable: str) -> tuple[str, int]:
121
- """Return the plain pinyin syllable and its tone number.
122
-
123
- ``Style.TONE3`` writes tones as final digits, but neutral tone syllables have
124
- no digit. Treat those digitless cases as fifth tone.
125
- """
126
- match = re.fullmatch(r"([a-züv]+)([1-5]?)", syllable.lower())
127
- if not match:
128
- return syllable, 5
129
-
130
- plain, tone = match.groups()
131
- return plain, int(tone or "5")
132
-
133
-
134
- def length_digit_offset(syllable: str) -> int:
135
- """Map pinyin syllable length to the requested 0-4 digit offset."""
136
- return min(max(len(syllable), 1), 5) - 1
137
-
138
-
139
- def syllable_to_initial_code(syllable: str) -> str:
140
- """Convert one pinyin syllable with tone into ``initial + digit``.
141
-
142
- Tone controls initial casing and whether the digit starts from 0 or 5:
143
- tones 1/3/5 use uppercase initials, while tones 2/4 use lowercase initials.
144
- Syllable length then adds the 0-4 offset that makes the final digit.
145
- """
146
- plain, tone = split_tone3_syllable(syllable)
147
- if not plain:
148
- return ""
149
-
150
- tone_offset = 5 if tone in {3, 4, 5} else 0
151
- digit = tone_offset + length_digit_offset(plain)
152
- initial = plain[0].upper() if tone in {1, 3, 5} else plain[0].lower()
153
- return f"{initial}{digit}"
154
-
155
-
156
- def syllable_to_initial_letter(syllable: str) -> str:
157
- """Convert one pinyin syllable with tone into its lowercase first letter."""
158
- plain, _ = split_tone3_syllable(syllable)
159
- return plain[:1].lower()
160
-
161
-
162
- def chinese_word_to_initial_codes(word: str) -> str:
163
- """Convert one already-segmented Chinese word to one compact code token.
164
-
165
- The important representation choice is preserved: jieba decides the word
166
- boundary, and all syllable codes inside that word are concatenated. For
167
- example, ``我们`` becomes ``W6M7`` rather than ``W6 M7``.
168
- """
169
- syllables = pinyin(word, style=Style.TONE3, heteronym=False, errors="ignore")
170
- codes = [syllable_to_initial_code(item[0]) for item in syllables if item and item[0]]
171
- return "".join(code for code in codes if code)
172
-
173
-
174
- def chinese_word_to_initial_letters(word: str) -> str:
175
- """Convert one already-segmented Chinese word to lowercase pinyin initials."""
176
- syllables = pinyin(word, style=Style.TONE3, heteronym=False, errors="ignore")
177
- initials = [
178
- syllable_to_initial_letter(item[0]) for item in syllables if item and item[0]
179
- ]
180
- return "".join(initial for initial in initials if initial)
181
-
182
-
183
- def chinese_word_to_transliteration(word: str, transliteration: Transliteration) -> str:
184
- """Convert one segmented Chinese word using the requested transliteration."""
185
- if transliteration == "pinyin-code":
186
- return chinese_word_to_initial_codes(word)
187
- if transliteration == "pinyin-initial":
188
- return chinese_word_to_initial_letters(word)
189
- if transliteration == "hanzi":
190
- return word
191
- raise ValueError(f"Unsupported transliteration: {transliteration}")
192
-
193
-
194
- def tokenize_chinese_span(
195
- text: str,
196
- transliteration: Transliteration = "pinyin-code",
197
- use_jieba: bool = True,
198
- ) -> Iterable[str]:
199
- """Emit one token per jieba word or per Hanzi character."""
200
- words = jieba.cut(text, cut_all=False) if use_jieba else text
201
- for word in words:
202
- word = word.strip()
203
- if not word:
204
- continue
205
- if CHINESE_RE.search(word):
206
- token = chinese_word_to_transliteration(word, transliteration)
207
- if token:
208
- yield token
209
-
210
-
211
- def process_text(
212
- text: str,
213
- transliteration: Transliteration = "pinyin-code",
214
- use_jieba: bool = True,
215
- ) -> str:
216
- """Convert one raw document string into the final space-separated token line."""
217
- tokens: list[str] = []
218
- for part in TOKEN_RE.findall(normalize_text(text)):
219
- if part.startswith("<") and part.endswith(">"):
220
- tokens.append(part)
221
- elif CHINESE_RE.fullmatch(part):
222
- tokens.extend(tokenize_chinese_span(part, transliteration, use_jieba))
223
- elif part in PUNCTUATION:
224
- tokens.append(part)
225
- elif LATIN_ALNUM_RE.fullmatch(part):
226
- tokens.append(latin_token_to_model_token(part))
227
- elif part.isdigit():
228
- tokens.append("<NUM>")
229
- elif should_preserve_fallback_token(part):
230
- tokens.append(part.lower())
231
-
232
- return " ".join(tokens)
233
-
234
-
235
- def hanzi_to_encoded(text: str, use_jieba: bool = True) -> str:
236
- """Convert normal Hanzi/Mandarin text to the compact initial+digit encoding."""
237
- require_dependencies()
238
- return process_text(text, "pinyin-code", use_jieba)
239
-
240
-
241
- def read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
242
- """Yield JSON objects from UTF-8 JSONL, tolerating a leading BOM if present."""
243
- with path.open("r", encoding="utf-8-sig") as handle:
244
- for line_number, line in enumerate(handle, start=1):
245
- line = line.strip()
246
- if not line:
247
- continue
248
- try:
249
- yield json.loads(line)
250
- except json.JSONDecodeError as exc:
251
- raise ValueError(f"Invalid JSON on line {line_number}: {exc}") from exc
252
-
253
-
254
- def preprocess_file(
255
- input_path: Path,
256
- output_path: Path,
257
- preview_count: int,
258
- transliteration: Transliteration = "pinyin-code",
259
- use_jieba: bool = True,
260
- ) -> int:
261
- """Stream input documents to output while retaining a small preview buffer."""
262
- output_path.parent.mkdir(parents=True, exist_ok=True)
263
- written = 0
264
- previews: list[tuple[str, str]] = []
265
-
266
- with output_path.open("w", encoding="utf-8", newline="\n") as out:
267
- for obj in read_jsonl(input_path):
268
- text = str(obj.get("text", ""))
269
- processed = process_text(text, transliteration, use_jieba)
270
- out.write(processed)
271
- out.write("\n")
272
- written += 1
273
-
274
- if len(previews) < preview_count:
275
- previews.append((text, processed))
276
-
277
- for original, processed in previews:
278
- print("ORIGINAL:")
279
- print(original)
280
- print("PROCESSED:")
281
- print(processed)
282
- print()
283
-
284
- return written
285
-
286
-
287
- def parse_args() -> argparse.Namespace:
288
- parser = argparse.ArgumentParser(
289
- description="Convert BabyLM Mandarin JSONL text fields to model-ready tokens."
290
- )
291
- parser.add_argument("--input", type=Path, required=True)
292
- parser.add_argument("--output", type=Path, required=True)
293
- parser.add_argument("--preview", type=int, default=3)
294
- parser.add_argument(
295
- "--transliteration",
296
- choices=("pinyin-code", "pinyin-initial", "hanzi"),
297
- default="pinyin-code",
298
- help=(
299
- "Mandarin transliteration to emit: 'pinyin-code' keeps the original "
300
- "tone/length code, while 'pinyin-initial' emits lowercase pinyin "
301
- "first letters only, and 'hanzi' keeps segmented Mandarin as Hanzi."
302
- ),
303
- )
304
- parser.add_argument(
305
- "--jieba",
306
- action=argparse.BooleanOptionalAction,
307
- default=True,
308
- help=(
309
- "Use jieba word segmentation for Chinese spans. Disable with "
310
- "--no-jieba to emit one token per Hanzi character before "
311
- "transliteration."
312
- ),
313
- )
314
- return parser.parse_args()
315
-
316
-
317
- def main() -> None:
318
- require_dependencies()
319
- if hasattr(sys.stdout, "reconfigure"):
320
- sys.stdout.reconfigure(encoding="utf-8")
321
- args = parse_args()
322
- count = preprocess_file(
323
- args.input,
324
- args.output,
325
- args.preview,
326
- args.transliteration,
327
- args.jieba,
328
- )
329
- print(f"Wrote {count:,} processed documents to {args.output}")
330
-
331
-
332
- if __name__ == "__main__":
333
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Preprocess BabyLM Mandarin JSONL into word-preserving pinyin initial codes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import logging
8
+ import re
9
+ import sys
10
+ import unicodedata
11
+ from multiprocessing import Pool
12
+ from pathlib import Path
13
+ from typing import Any, Iterable, Literal
14
+
15
+ import jieba
16
+ from pypinyin import Style, pinyin
17
+
18
+
19
+ LABELS = {
20
+ "题干": "<QUESTION>",
21
+ "选项": "<OPTIONS>",
22
+ "答案": "<ANSWER>",
23
+ "解析": "<EXPLANATION>",
24
+ }
25
+
26
+ PUNCTUATION = set("。,、?!:;.,?!:;()[]{}<>《》【】“”\"'‘’「」『』—-~…/\\")
27
+ CHINESE_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
28
+ Transliteration = Literal["pinyin-code", "pinyin-initial", "hanzi"]
29
+ LATIN_LETTER = r"A-Za-zÀ-ÖØ-öø-ÿĀ-ſƀ-ɏɐ-ʯ"
30
+ LATIN_ALNUM_PATTERN = (
31
+ rf"(?:[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
32
+ rf"(?:[-_][{LATIN_LETTER}0-9]+)*|"
33
+ rf"[0-9]+[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
34
+ rf"(?:[-_][{LATIN_LETTER}0-9]+)*)"
35
+ )
36
+ LATIN_ALNUM_RE = re.compile(LATIN_ALNUM_PATTERN)
37
+ URL_RE = re.compile(r"\b(?:https?://\S*|www\.\S+)", flags=re.I)
38
+ DISCARDED_UNICODE_CATEGORIES = {"Cc", "Cf", "Co", "Cs", "Cn"}
39
+
40
+ # Match protected markers before ordinary words so tokens like <ANSWER> survive
41
+ # the later English/punctuation handling as a single vocabulary item.
42
+ TOKEN_RE = re.compile(
43
+ r"<[A-Z_]+>|"
44
+ r"[\u3400-\u4dbf\u4e00-\u9fff]+|"
45
+ rf"{LATIN_ALNUM_PATTERN}|"
46
+ r"\S"
47
+ )
48
+
49
+
50
+ def require_dependencies() -> None:
51
+ """Fail early with a concise install hint and quiet jieba startup logging."""
52
+ if jieba is None or Style is None or pinyin is None:
53
+ raise SystemExit(
54
+ "Missing dependency: install with `py -m pip install jieba pypinyin`."
55
+ )
56
+ jieba.setLogLevel(logging.WARNING)
57
+
58
+
59
+ def normalize_text(text: str) -> str:
60
+ """Replace task-specific surface forms with stable special tokens.
61
+
62
+ This happens before tokenization so multi-character patterns such as
63
+ ``$$...$$`` and empty brackets cannot be split into punctuation pieces.
64
+ """
65
+ text = unicodedata.normalize("NFKC", text)
66
+ text = URL_RE.sub(" <URL> ", text)
67
+ text = re.sub(r"\$\$.*?\$\$", " <MATH> ", text, flags=re.DOTALL)
68
+ text = re.sub(r"[((]\s*[))]", " <BLANK> ", text)
69
+
70
+ for label, marker in LABELS.items():
71
+ text = re.sub(rf"{label}\s*[::]", f" {marker} ", text)
72
+
73
+ text = re.sub(
74
+ rf"(?<![{LATIN_LETTER}])yes(?![{LATIN_LETTER}])",
75
+ " <YES> ",
76
+ text,
77
+ flags=re.I,
78
+ )
79
+ text = re.sub(
80
+ rf"(?<![{LATIN_LETTER}])no(?![{LATIN_LETTER}])",
81
+ " <NO> ",
82
+ text,
83
+ flags=re.I,
84
+ )
85
+ text = re.sub(
86
+ rf"(?<![{LATIN_LETTER}])[ABCD](?=\s*[::..、\)])",
87
+ r" \g<0> ",
88
+ text,
89
+ )
90
+ text = re.sub(
91
+ rf"(?<![{LATIN_LETTER}0-9])[-+]?\d+(?:[.,]\d+)*(?:%|%)?"
92
+ rf"(?![{LATIN_LETTER}0-9])",
93
+ " <NUM> ",
94
+ text,
95
+ )
96
+
97
+ text = text.replace("", "(").replace(")", ")")
98
+ text = re.sub(r"\s+", " ", text)
99
+ return text.strip()
100
+
101
+
102
+ def latin_token_to_model_token(token: str) -> str:
103
+ """Normalize non-Mandarin alphanumeric tokens without losing option labels."""
104
+ upper = token.upper()
105
+ return upper if upper in {"A", "B", "C", "D"} else token.lower()
106
+
107
+
108
+ def should_preserve_fallback_token(token: str) -> bool:
109
+ """Return true for visible non-Hanzi letters, punctuation, and symbols."""
110
+ if token == "\ufffd":
111
+ return False
112
+ for char in token:
113
+ category = unicodedata.category(char)
114
+ if category in DISCARDED_UNICODE_CATEGORIES:
115
+ return False
116
+ if category[0] not in {"L", "P", "S"}:
117
+ return False
118
+ return True
119
+
120
+
121
+ def split_tone3_syllable(syllable: str) -> tuple[str, int]:
122
+ """Return the plain pinyin syllable and its tone number.
123
+
124
+ ``Style.TONE3`` writes tones as final digits, but neutral tone syllables have
125
+ no digit. Treat those digitless cases as fifth tone.
126
+ """
127
+ match = re.fullmatch(r"([a-züv]+)([1-5]?)", syllable.lower())
128
+ if not match:
129
+ return syllable, 5
130
+
131
+ plain, tone = match.groups()
132
+ return plain, int(tone or "5")
133
+
134
+
135
+ def length_digit_offset(syllable: str) -> int:
136
+ """Map pinyin syllable length to the requested 0-4 digit offset."""
137
+ return min(max(len(syllable), 1), 5) - 1
138
+
139
+
140
+ def syllable_to_initial_code(syllable: str) -> str:
141
+ """Convert one pinyin syllable with tone into ``initial + digit``.
142
+
143
+ Tone controls initial casing and whether the digit starts from 0 or 5:
144
+ tones 1/3/5 use uppercase initials, while tones 2/4 use lowercase initials.
145
+ Syllable length then adds the 0-4 offset that makes the final digit.
146
+ """
147
+ plain, tone = split_tone3_syllable(syllable)
148
+ if not plain:
149
+ return ""
150
+
151
+ tone_offset = 5 if tone in {3, 4, 5} else 0
152
+ digit = tone_offset + length_digit_offset(plain)
153
+ initial = plain[0].upper() if tone in {1, 3, 5} else plain[0].lower()
154
+ return f"{initial}{digit}"
155
+
156
+
157
+ def syllable_to_initial_letter(syllable: str) -> str:
158
+ """Convert one pinyin syllable with tone into its lowercase first letter."""
159
+ plain, _ = split_tone3_syllable(syllable)
160
+ return plain[:1].lower()
161
+
162
+
163
+ def chinese_word_to_initial_codes(word: str) -> str:
164
+ """Convert one already-segmented Chinese word to one compact code token.
165
+
166
+ The important representation choice is preserved: jieba decides the word
167
+ boundary, and all syllable codes inside that word are concatenated. For
168
+ example, ``我们`` becomes ``W6M7`` rather than ``W6 M7``.
169
+ """
170
+ syllables = pinyin(word, style=Style.TONE3, heteronym=False, errors="ignore")
171
+ codes = [syllable_to_initial_code(item[0]) for item in syllables if item and item[0]]
172
+ return "".join(code for code in codes if code)
173
+
174
+
175
+ def chinese_word_to_initial_letters(word: str) -> str:
176
+ """Convert one already-segmented Chinese word to lowercase pinyin initials."""
177
+ syllables = pinyin(word, style=Style.TONE3, heteronym=False, errors="ignore")
178
+ initials = [
179
+ syllable_to_initial_letter(item[0]) for item in syllables if item and item[0]
180
+ ]
181
+ return "".join(initial for initial in initials if initial)
182
+
183
+
184
+ def chinese_word_to_transliteration(word: str, transliteration: Transliteration) -> str:
185
+ """Convert one segmented Chinese word using the requested transliteration."""
186
+ if transliteration == "pinyin-code":
187
+ return chinese_word_to_initial_codes(word)
188
+ if transliteration == "pinyin-initial":
189
+ return chinese_word_to_initial_letters(word)
190
+ if transliteration == "hanzi":
191
+ return word
192
+ raise ValueError(f"Unsupported transliteration: {transliteration}")
193
+
194
+
195
+ def tokenize_chinese_span(
196
+ text: str,
197
+ transliteration: Transliteration = "pinyin-code",
198
+ use_jieba: bool = True,
199
+ ) -> Iterable[str]:
200
+ """Emit one token per jieba word or per Hanzi character."""
201
+ words = jieba.cut(text, cut_all=False) if use_jieba else text
202
+ for word in words:
203
+ word = word.strip()
204
+ if not word:
205
+ continue
206
+ if CHINESE_RE.search(word):
207
+ token = chinese_word_to_transliteration(word, transliteration)
208
+ if token:
209
+ yield token
210
+
211
+
212
+ def process_text(
213
+ text: str,
214
+ transliteration: Transliteration = "pinyin-code",
215
+ use_jieba: bool = True,
216
+ ) -> str:
217
+ """Convert one raw document string into the final space-separated token line."""
218
+ tokens: list[str] = []
219
+ for part in TOKEN_RE.findall(normalize_text(text)):
220
+ if part.startswith("<") and part.endswith(">"):
221
+ tokens.append(part)
222
+ elif CHINESE_RE.fullmatch(part):
223
+ tokens.extend(tokenize_chinese_span(part, transliteration, use_jieba))
224
+ elif part in PUNCTUATION:
225
+ tokens.append(part)
226
+ elif LATIN_ALNUM_RE.fullmatch(part):
227
+ tokens.append(latin_token_to_model_token(part))
228
+ elif part.isdigit():
229
+ tokens.append("<NUM>")
230
+ elif should_preserve_fallback_token(part):
231
+ tokens.append(part.lower())
232
+
233
+ return " ".join(tokens)
234
+
235
+
236
+ def hanzi_to_encoded(text: str, use_jieba: bool = True) -> str:
237
+ """Convert normal Hanzi/Mandarin text to the compact initial+digit encoding."""
238
+ require_dependencies()
239
+ return process_text(text, "pinyin-code", use_jieba)
240
+
241
+
242
+ def read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
243
+ """Yield JSON objects from UTF-8 JSONL, tolerating a leading BOM if present."""
244
+ with path.open("r", encoding="utf-8-sig") as handle:
245
+ for line_number, line in enumerate(handle, start=1):
246
+ line = line.strip()
247
+ if not line:
248
+ continue
249
+ try:
250
+ yield json.loads(line)
251
+ except json.JSONDecodeError as exc:
252
+ raise ValueError(f"Invalid JSON on line {line_number}: {exc}") from exc
253
+
254
+
255
+ def iter_processing_tasks(
256
+ input_path: Path,
257
+ transliteration: Transliteration,
258
+ use_jieba: bool,
259
+ ) -> Iterable[tuple[str, Transliteration, bool]]:
260
+ """Yield independent document tasks without loading the full corpus."""
261
+ for obj in read_jsonl(input_path):
262
+ yield str(obj.get("text", "")), transliteration, use_jieba
263
+
264
+
265
+ def process_task(
266
+ task: tuple[str, Transliteration, bool],
267
+ ) -> tuple[str, str]:
268
+ """Process one document and return its original and encoded text."""
269
+ text, transliteration, use_jieba = task
270
+ return text, process_text(text, transliteration, use_jieba)
271
+
272
+
273
+ def preprocess_file(
274
+ input_path: Path,
275
+ output_path: Path,
276
+ preview_count: int,
277
+ transliteration: Transliteration = "pinyin-code",
278
+ use_jieba: bool = True,
279
+ workers: int = 1,
280
+ chunksize: int = 32,
281
+ ) -> int:
282
+ """Stream documents to output, optionally using ordered multiprocessing."""
283
+ if workers < 1:
284
+ raise ValueError("workers must be at least 1")
285
+ if chunksize < 1:
286
+ raise ValueError("chunksize must be at least 1")
287
+
288
+ output_path.parent.mkdir(parents=True, exist_ok=True)
289
+ written = 0
290
+ previews: list[tuple[str, str]] = []
291
+ tasks = iter_processing_tasks(input_path, transliteration, use_jieba)
292
+
293
+ with output_path.open("w", encoding="utf-8", newline="\n") as out:
294
+ if workers == 1:
295
+ for text, processed in map(process_task, tasks):
296
+ out.write(processed)
297
+ out.write("\n")
298
+ written += 1
299
+
300
+ if len(previews) < preview_count:
301
+ previews.append((text, processed))
302
+ else:
303
+ with Pool(
304
+ processes=workers,
305
+ initializer=require_dependencies,
306
+ ) as pool:
307
+ results = pool.imap(
308
+ process_task,
309
+ tasks,
310
+ chunksize=chunksize,
311
+ )
312
+
313
+ for text, processed in results:
314
+ out.write(processed)
315
+ out.write("\n")
316
+ written += 1
317
+
318
+ if len(previews) < preview_count:
319
+ previews.append((text, processed))
320
+
321
+ for original, processed in previews:
322
+ print("ORIGINAL:")
323
+ print(original)
324
+ print("PROCESSED:")
325
+ print(processed)
326
+ print()
327
+
328
+ return written
329
+
330
+ def parse_args() -> argparse.Namespace:
331
+ parser = argparse.ArgumentParser(
332
+ description="Convert BabyLM Mandarin JSONL text fields to model-ready tokens."
333
+ )
334
+ parser.add_argument("--input", type=Path, required=True)
335
+ parser.add_argument("--output", type=Path, required=True)
336
+ parser.add_argument("--preview", type=int, default=3)
337
+ parser.add_argument(
338
+ "--transliteration",
339
+ choices=("pinyin-code", "pinyin-initial", "hanzi"),
340
+ default="pinyin-code",
341
+ help=(
342
+ "Mandarin transliteration to emit: 'pinyin-code' keeps the original "
343
+ "tone/length code, while 'pinyin-initial' emits lowercase pinyin "
344
+ "first letters only, and 'hanzi' keeps segmented Mandarin as Hanzi."
345
+ ),
346
+ )
347
+ parser.add_argument(
348
+ "--jieba",
349
+ action=argparse.BooleanOptionalAction,
350
+ default=True,
351
+ help=(
352
+ "Use jieba word segmentation for Chinese spans. Disable with "
353
+ "--no-jieba to emit one token per Hanzi character before "
354
+ "transliteration."
355
+ ),
356
+ )
357
+ parser.add_argument(
358
+ "--workers",
359
+ type=int,
360
+ default=1,
361
+ help=(
362
+ "Number of document-processing worker processes. "
363
+ "The default of 1 preserves sequential behavior."
364
+ ),
365
+ )
366
+ parser.add_argument(
367
+ "--chunksize",
368
+ type=int,
369
+ default=32,
370
+ help="Documents assigned to each worker per multiprocessing batch.",
371
+ )
372
+ return parser.parse_args()
373
+
374
+
375
+ def main() -> None:
376
+ require_dependencies()
377
+ if hasattr(sys.stdout, "reconfigure"):
378
+ sys.stdout.reconfigure(encoding="utf-8")
379
+ args = parse_args()
380
+ count = preprocess_file(
381
+ args.input,
382
+ args.output,
383
+ args.preview,
384
+ args.transliteration,
385
+ args.jieba,
386
+ args.workers,
387
+ args.chunksize,
388
+ )
389
+ print(f"Wrote {count:,} processed documents to {args.output}")
390
+
391
+
392
+ if __name__ == "__main__":
393
+ main()
preprocessing/probability_matrix.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build a bucketed tone/length probability matrix from tone statistics JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from collections import Counter
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ TONE_BUCKETS = {
13
+ "1": "tone_1",
14
+ "2": "tone_2",
15
+ "4": "tone_4",
16
+ "3": "tone_3_or_5",
17
+ "5": "tone_3_or_5",
18
+ "blank": "tone_3_or_5",
19
+ }
20
+ TONE_ORDER = ("tone_1", "tone_2", "tone_4", "tone_3_or_5")
21
+
22
+ LENGTH_ORDER = ("length_1_or_2", "length_3", "length_4_to_6")
23
+
24
+
25
+ def load_stats(path: Path) -> dict[str, Any]:
26
+ return json.loads(path.read_text(encoding="utf-8-sig"))
27
+
28
+
29
+ def length_bucket(length: int) -> str:
30
+ if length in {1, 2}:
31
+ return "length_1_or_2"
32
+ if length == 3:
33
+ return "length_3"
34
+ if length in {4, 5, 6}:
35
+ return "length_4_to_6"
36
+ raise ValueError(f"Unsupported pinyin length: {length}")
37
+
38
+
39
+ def bucketed_marginals(stats: dict[str, Any]) -> tuple[Counter[str], Counter[str], int]:
40
+ tone_counts: Counter[str] = Counter()
41
+ for tone, values in stats.get("tones", {}).items():
42
+ bucket = TONE_BUCKETS.get(str(tone))
43
+ if bucket is None:
44
+ raise ValueError(f"Unsupported tone bucket: {tone}")
45
+ tone_counts[bucket] += int(values["count"])
46
+
47
+ length_counts: Counter[str] = Counter()
48
+ for values in stats.get("lengths", []):
49
+ bucket = length_bucket(int(values["length"]))
50
+ length_counts[bucket] += int(values["count"])
51
+
52
+ total = int(stats.get("total_unique_cases") or sum(tone_counts.values()))
53
+ return tone_counts, length_counts, total
54
+
55
+
56
+ def exact_matrix_from_cases(stats: dict[str, Any]) -> tuple[dict[str, dict[str, int]], int]:
57
+ matrix = {tone: {length: 0 for length in LENGTH_ORDER} for tone in TONE_ORDER}
58
+ for case in stats["unique_cases"]:
59
+ tone_bucket = TONE_BUCKETS.get(str(case["tone"]))
60
+ if tone_bucket is None:
61
+ raise ValueError(f"Unsupported tone bucket: {case['tone']}")
62
+ len_bucket = length_bucket(int(case["length"]))
63
+ matrix[tone_bucket][len_bucket] += 1
64
+ total = sum(sum(row.values()) for row in matrix.values())
65
+ return matrix, total
66
+
67
+
68
+ def estimated_matrix_from_marginals(
69
+ tone_counts: Counter[str],
70
+ length_counts: Counter[str],
71
+ total: int,
72
+ ) -> dict[str, dict[str, float]]:
73
+ if total == 0:
74
+ return {tone: {length: 0.0 for length in LENGTH_ORDER} for tone in TONE_ORDER}
75
+
76
+ return {
77
+ tone: {
78
+ length: tone_counts[tone] * length_counts[length] / total
79
+ for length in LENGTH_ORDER
80
+ }
81
+ for tone in TONE_ORDER
82
+ }
83
+
84
+
85
+ def matrix_payload(stats: dict[str, Any]) -> dict[str, Any]:
86
+ if "unique_cases" in stats:
87
+ counts, total = exact_matrix_from_cases(stats)
88
+ method = "exact_from_unique_cases"
89
+ else:
90
+ tone_counts, length_counts, total = bucketed_marginals(stats)
91
+ counts = estimated_matrix_from_marginals(tone_counts, length_counts, total)
92
+ method = "estimated_from_marginals_assuming_independence"
93
+
94
+ probabilities = {
95
+ tone: {
96
+ length: round((counts[tone][length] / total) if total else 0.0, 6)
97
+ for length in LENGTH_ORDER
98
+ }
99
+ for tone in TONE_ORDER
100
+ }
101
+
102
+ percentages = {
103
+ tone: {
104
+ length: round(probabilities[tone][length] * 100, 4)
105
+ for length in LENGTH_ORDER
106
+ }
107
+ for tone in TONE_ORDER
108
+ }
109
+
110
+ return {
111
+ "method": method,
112
+ "total_unique_cases": total,
113
+ "tone_buckets": list(TONE_ORDER),
114
+ "length_buckets": list(LENGTH_ORDER),
115
+ "counts": counts,
116
+ "probabilities": probabilities,
117
+ "percentages": percentages,
118
+ }
119
+
120
+
121
+ def print_cli_matrix(payload: dict[str, Any]) -> None:
122
+ print(f"method: {payload['method']}")
123
+ print(f"total_unique_cases: {payload['total_unique_cases']}")
124
+ print()
125
+
126
+ headers = ["tone \\ length", *LENGTH_ORDER]
127
+ rows = []
128
+ for tone in TONE_ORDER:
129
+ row = [tone]
130
+ for length in LENGTH_ORDER:
131
+ row.append(f"{payload['percentages'][tone][length]:.4f}%")
132
+ rows.append(row)
133
+
134
+ widths = [
135
+ max(len(str(row[index])) for row in [headers, *rows])
136
+ for index in range(len(headers))
137
+ ]
138
+ print(" | ".join(value.ljust(widths[index]) for index, value in enumerate(headers)))
139
+ print("-+-".join("-" * width for width in widths))
140
+ for row in rows:
141
+ print(" | ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
142
+
143
+
144
+ def write_output(path: Path, payload: dict[str, Any]) -> None:
145
+ path.parent.mkdir(parents=True, exist_ok=True)
146
+ path.write_text(
147
+ json.dumps(payload, ensure_ascii=False, indent=2),
148
+ encoding="utf-8",
149
+ newline="\n",
150
+ )
151
+
152
+
153
+ def parse_args() -> argparse.Namespace:
154
+ parser = argparse.ArgumentParser(
155
+ description="Create a bucketed tone/length probability matrix."
156
+ )
157
+ parser.add_argument("--input", type=Path, default=Path("data/10k_statistics.jsonl"))
158
+ parser.add_argument("--output", type=Path)
159
+ return parser.parse_args()
160
+
161
+
162
+ def main() -> None:
163
+ args = parse_args()
164
+ payload = matrix_payload(load_stats(args.input))
165
+ print_cli_matrix(payload)
166
+
167
+ if args.output:
168
+ write_output(args.output, payload)
169
+ print(f"\nWrote matrix JSON to {args.output}")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()
preprocessing/split_long_sentencepiece_lines.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Split long processed-text lines for SentencePiece training.
2
+
3
+ SentencePiece BPE can fail on very long lines when whitespace splitting is
4
+ disabled. This utility writes a tokenizer-training copy of a processed corpus
5
+ where long documents are split at whitespace boundaries while preserving all
6
+ tokens.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Iterable
15
+
16
+
17
+ @dataclass
18
+ class SplitStats:
19
+ input_lines: int = 0
20
+ output_lines: int = 0
21
+ split_lines: int = 0
22
+ max_input_chars: int = 0
23
+ max_output_chars: int = 0
24
+
25
+
26
+ def split_line_at_whitespace(line: str, max_chars: int) -> list[str]:
27
+ """Return chunks no longer than max_chars, splitting only between tokens."""
28
+ line = line.strip()
29
+ if not line:
30
+ return []
31
+ if len(line) <= max_chars:
32
+ return [line]
33
+
34
+ chunks: list[str] = []
35
+ current: list[str] = []
36
+ current_len = 0
37
+
38
+ for token in line.split():
39
+ token_len = len(token)
40
+ if token_len > max_chars:
41
+ raise ValueError(
42
+ f"Token longer than --max-chars ({token_len} > {max_chars}): {token[:80]!r}"
43
+ )
44
+
45
+ next_len = token_len if not current else current_len + 1 + token_len
46
+ if current and next_len > max_chars:
47
+ chunks.append(" ".join(current))
48
+ current = [token]
49
+ current_len = token_len
50
+ else:
51
+ current.append(token)
52
+ current_len = next_len
53
+
54
+ if current:
55
+ chunks.append(" ".join(current))
56
+
57
+ return chunks
58
+
59
+
60
+ def iter_split_lines(input_path: Path, max_chars: int, stats: SplitStats) -> Iterable[str]:
61
+ """Yield split lines and update stats."""
62
+ with input_path.open("r", encoding="utf-8-sig") as handle:
63
+ for raw_line in handle:
64
+ line = raw_line.rstrip("\n\r")
65
+ stats.input_lines += 1
66
+ stats.max_input_chars = max(stats.max_input_chars, len(line))
67
+
68
+ chunks = split_line_at_whitespace(line, max_chars)
69
+ if len(chunks) > 1:
70
+ stats.split_lines += 1
71
+
72
+ for chunk in chunks:
73
+ stats.output_lines += 1
74
+ stats.max_output_chars = max(stats.max_output_chars, len(chunk))
75
+ yield chunk
76
+
77
+
78
+ def split_file(input_path: Path, output_path: Path, max_chars: int, dry_run: bool) -> SplitStats:
79
+ """Split input_path into output_path and return summary statistics."""
80
+ if max_chars <= 0:
81
+ raise ValueError("--max-chars must be greater than zero")
82
+
83
+ stats = SplitStats()
84
+ split_lines = iter_split_lines(input_path, max_chars, stats)
85
+
86
+ if dry_run:
87
+ for _ in split_lines:
88
+ pass
89
+ return stats
90
+
91
+ output_path.parent.mkdir(parents=True, exist_ok=True)
92
+ with output_path.open("w", encoding="utf-8", newline="\n") as out:
93
+ for line in split_lines:
94
+ out.write(line)
95
+ out.write("\n")
96
+
97
+ return stats
98
+
99
+
100
+ def parse_args() -> argparse.Namespace:
101
+ parser = argparse.ArgumentParser(
102
+ description=(
103
+ "Create a SentencePiece-training copy of processed text by splitting "
104
+ "long lines at whitespace boundaries."
105
+ )
106
+ )
107
+ parser.add_argument(
108
+ "--input",
109
+ type=Path,
110
+ required=True,
111
+ help="Processed .txt file with one document per line.",
112
+ )
113
+ parser.add_argument(
114
+ "--output",
115
+ type=Path,
116
+ required=True,
117
+ help="Output .txt file to use for SentencePiece training.",
118
+ )
119
+ parser.add_argument(
120
+ "--max-chars",
121
+ type=int,
122
+ default=60000,
123
+ help="Maximum characters per output line. Keep below 65535 for SentencePiece BPE.",
124
+ )
125
+ parser.add_argument(
126
+ "--dry-run",
127
+ action="store_true",
128
+ help="Report what would be written without creating the output file.",
129
+ )
130
+ return parser.parse_args()
131
+
132
+
133
+ def main() -> None:
134
+ args = parse_args()
135
+ stats = split_file(args.input, args.output, args.max_chars, args.dry_run)
136
+
137
+ action = "Would write" if args.dry_run else "Wrote"
138
+ print(f"{action} {stats.output_lines:,} lines from {stats.input_lines:,} input lines")
139
+ print(f"Split input lines: {stats.split_lines:,}")
140
+ print(f"Max input chars: {stats.max_input_chars:,}")
141
+ print(f"Max output chars: {stats.max_output_chars:,}")
142
+ if not args.dry_run:
143
+ print(f"Output: {args.output}")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ main()
preprocessing/statistics_to_latex.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render pinyin tone/length summary statistics as a publication-ready LaTeX table."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ TONE_LABELS = {
12
+ "1": "Tone 1",
13
+ "2": "Tone 2",
14
+ "3": "Tone 3",
15
+ "4": "Tone 4",
16
+ "blank": "Neutral/unmarked",
17
+ }
18
+ TONE_ORDER = ("1", "2", "3", "4", "blank")
19
+
20
+
21
+ def load_stats(path: Path) -> dict[str, Any]:
22
+ """Load the JSON summary payload written by ``tone_statistics.py``."""
23
+ try:
24
+ return json.loads(path.read_text(encoding="utf-8-sig"))
25
+ except json.JSONDecodeError as exc:
26
+ raise ValueError(f"Invalid statistics JSON in {path}: {exc}") from exc
27
+
28
+
29
+ def latex_escape(text: str) -> str:
30
+ """Escape free text inserted into LaTeX prose fields."""
31
+ replacements = {
32
+ "\\": r"\textbackslash{}",
33
+ "&": r"\&",
34
+ "%": r"\%",
35
+ "$": r"\$",
36
+ "#": r"\#",
37
+ "_": r"\_",
38
+ "{": r"\{",
39
+ "}": r"\}",
40
+ "~": r"\textasciitilde{}",
41
+ "^": r"\textasciicircum{}",
42
+ }
43
+ return "".join(replacements.get(char, char) for char in text)
44
+
45
+
46
+ def validate_stats(stats: dict[str, Any]) -> int:
47
+ """Validate required summary fields and return the number of unique cases."""
48
+ try:
49
+ total = int(stats["total_unique_cases"])
50
+ tones = stats["tones"]
51
+ lengths = stats["lengths"]
52
+ except (KeyError, TypeError, ValueError) as exc:
53
+ raise ValueError("Statistics payload is missing required summary fields.") from exc
54
+
55
+ missing_tones = [tone for tone in TONE_ORDER if tone not in tones]
56
+ if missing_tones:
57
+ raise ValueError(f"Missing tone categories: {', '.join(missing_tones)}")
58
+
59
+ tone_total = sum(int(tones[tone]["count"]) for tone in TONE_ORDER)
60
+ length_total = sum(int(item["count"]) for item in lengths)
61
+ if tone_total != total or length_total != total:
62
+ raise ValueError(
63
+ "Statistics totals do not agree: "
64
+ f"reported={total}, tones={tone_total}, lengths={length_total}."
65
+ )
66
+ return total
67
+
68
+
69
+ def format_row(
70
+ tone_label: str = "",
71
+ tone_count: str = "",
72
+ tone_percent: str = "",
73
+ length_label: str = "",
74
+ length_count: str = "",
75
+ length_percent: str = "",
76
+ ) -> str:
77
+ return (
78
+ f"{tone_label} & {tone_count} & {tone_percent} & "
79
+ f"{length_label} & {length_count} & {length_percent} \\\\"
80
+ )
81
+
82
+
83
+ def render_table(
84
+ stats: dict[str, Any],
85
+ caption: str,
86
+ label: str,
87
+ ) -> str:
88
+ """Return a standalone LaTeX table environment for summary statistics."""
89
+ total = validate_stats(stats)
90
+ count_format = f"{len(str(total))}.0"
91
+ tone_rows = [
92
+ (
93
+ TONE_LABELS[tone],
94
+ str(int(stats["tones"][tone]["count"])),
95
+ f'{float(stats["tones"][tone]["percentage"]):.2f}',
96
+ )
97
+ for tone in TONE_ORDER
98
+ ]
99
+ length_rows = [
100
+ (
101
+ str(int(item["length"])),
102
+ str(int(item["count"])),
103
+ f'{float(item["percentage"]):.2f}',
104
+ )
105
+ for item in sorted(stats["lengths"], key=lambda item: int(item["length"]))
106
+ ]
107
+
108
+ rows: list[str] = []
109
+ data_row_count = max(len(tone_rows), len(length_rows))
110
+ for index in range(data_row_count):
111
+ tone = tone_rows[index] if index < len(tone_rows) else ("", "", "")
112
+ length = length_rows[index] if index < len(length_rows) else ("", "", "")
113
+ rows.append(format_row(*tone, *length))
114
+ rows.append(r"\addlinespace")
115
+ rows.append(format_row("Total", str(total), "100.00", "Total", str(total), "100.00"))
116
+
117
+ body = "\n".join(rows)
118
+ return f"""% Requires \\usepackage{{booktabs}}
119
+ % Requires \\usepackage{{siunitx}}
120
+ \\begin{{table}}[t]
121
+ \\centering
122
+ \\caption{{{latex_escape(caption)}}}
123
+ \\label{{{label}}}
124
+ \\begin{{tabular}}{{
125
+ l
126
+ S[table-format={count_format}]
127
+ S[table-format=3.2]
128
+ @{{\\hspace{{1.75em}}}}
129
+ l
130
+ S[table-format={count_format}]
131
+ S[table-format=3.2]
132
+ }}
133
+ \\toprule
134
+ \\multicolumn{{3}}{{c}}{{Tone category}} &
135
+ \\multicolumn{{3}}{{c}}{{Syllable length (letters)}} \\\\
136
+ \\cmidrule(lr){{1-3}} \\cmidrule(lr){{4-6}}
137
+ {{Category}} & {{Count}} & {{Percent (\\%)}} &
138
+ {{Length}} & {{Count}} & {{Percent (\\%)}} \\\\
139
+ \\midrule
140
+ {body}
141
+ \\bottomrule
142
+ \\end{{tabular}}
143
+ \\vspace{{2pt}}
144
+
145
+ \\begin{{minipage}}{{0.95\\linewidth}}
146
+ \\footnotesize\\emph{{Note.}} Percentages are calculated over {total:,} unique
147
+ character--contextual-pinyin cases; syllable length excludes the final tone
148
+ digit. Neutral/unmarked denotes readings without an explicit tone digit.
149
+ \\end{{minipage}}
150
+ \\end{{table}}
151
+ """
152
+
153
+
154
+ def parse_args() -> argparse.Namespace:
155
+ parser = argparse.ArgumentParser(
156
+ description="Convert pinyin summary statistics JSON to a LaTeX table."
157
+ )
158
+ parser.add_argument(
159
+ "--input",
160
+ type=Path,
161
+ default=Path("data/10k_statistics.jsonl"),
162
+ help="Summary statistics JSON file.",
163
+ )
164
+ parser.add_argument(
165
+ "--output",
166
+ type=Path,
167
+ default=Path("tables/10k_statistics_table.tex"),
168
+ help="Path for the generated LaTeX table.",
169
+ )
170
+ parser.add_argument(
171
+ "--caption",
172
+ default=(
173
+ "Distribution of tone categories and syllable lengths among unique "
174
+ "contextual pinyin cases in the 10k BabyLM-Zho sample."
175
+ ),
176
+ )
177
+ parser.add_argument("--label", default="tab:10k-pinyin-statistics")
178
+ return parser.parse_args()
179
+
180
+
181
+ def main() -> None:
182
+ args = parse_args()
183
+ table = render_table(load_stats(args.input), args.caption, args.label)
184
+ args.output.parent.mkdir(parents=True, exist_ok=True)
185
+ args.output.write_text(table, encoding="utf-8", newline="\n")
186
+ print(f"Wrote LaTeX table to {args.output}")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
preprocessing/tone_statistics.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Count unique Mandarin character-to-pinyin tone and length cases in JSONL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import logging
8
+ import re
9
+ import sys
10
+ from collections import Counter
11
+ from pathlib import Path
12
+ from typing import Any, Iterable
13
+
14
+ import jieba
15
+ from pypinyin import Style, pinyin
16
+
17
+
18
+ CHINESE_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
19
+ TONE_RE = re.compile(r"[1-4]$")
20
+ TONE_ORDER = ("1", "2", "3", "4", "blank")
21
+
22
+
23
+ def require_dependencies() -> None:
24
+ """Fail early with a concise install hint and quiet jieba startup logging."""
25
+ if jieba is None or Style is None or pinyin is None:
26
+ raise SystemExit(
27
+ "Missing dependency: install with `py -m pip install jieba pypinyin`."
28
+ )
29
+ jieba.setLogLevel(logging.WARNING)
30
+
31
+
32
+ def read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
33
+ """Yield JSON objects from UTF-8 JSONL, tolerating a leading BOM if present."""
34
+ with path.open("r", encoding="utf-8-sig") as handle:
35
+ for line_number, line in enumerate(handle, start=1):
36
+ line = line.strip()
37
+ if not line:
38
+ continue
39
+ try:
40
+ yield json.loads(line)
41
+ except json.JSONDecodeError as exc:
42
+ raise ValueError(f"Invalid JSON on line {line_number}: {exc}") from exc
43
+
44
+
45
+ def chinese_spans(text: str) -> Iterable[str]:
46
+ """Yield contiguous Chinese spans from mixed text."""
47
+ yield from CHINESE_RE.findall(text)
48
+
49
+
50
+ def pinyin_tone_case(character: str, pinyin_with_tone: str) -> tuple[str, str, str]:
51
+ """Return the unique mapping key plus the tone bucket for one character."""
52
+ tone_match = TONE_RE.search(pinyin_with_tone)
53
+ tone = tone_match.group(0) if tone_match else "blank"
54
+ return character, pinyin_with_tone.lower(), tone
55
+
56
+
57
+ def pinyin_length(pinyin_with_tone: str) -> int:
58
+ """Return pinyin syllable length after removing a final tone digit."""
59
+ return len(TONE_RE.sub("", pinyin_with_tone))
60
+
61
+
62
+ def iter_contextual_cases(text: str) -> Iterable[tuple[str, str, str]]:
63
+ """Segment Chinese text with jieba and yield contextual pinyin cases.
64
+
65
+ pypinyin receives whole jieba words instead of isolated characters, which lets
66
+ it choose readings for common contextual forms such as polyphonic characters.
67
+ """
68
+ for span in chinese_spans(text):
69
+ for word in jieba.cut(span, cut_all=False):
70
+ word = word.strip()
71
+ if not word:
72
+ continue
73
+
74
+ pronunciations = pinyin(
75
+ word,
76
+ style=Style.TONE3,
77
+ heteronym=False,
78
+ neutral_tone_with_five=False,
79
+ errors="ignore",
80
+ )
81
+ chinese_chars = [char for char in word if CHINESE_RE.fullmatch(char)]
82
+
83
+ for character, syllable in zip(chinese_chars, pronunciations):
84
+ if not syllable:
85
+ continue
86
+ yield pinyin_tone_case(character, syllable[0])
87
+
88
+
89
+ def collect_unique_cases(input_path: Path, text_field: str) -> set[tuple[str, str, str]]:
90
+ """Collect unique character/pinyin/tone cases from a JSONL file."""
91
+ unique_cases: set[tuple[str, str, str]] = set()
92
+ for obj in read_jsonl(input_path):
93
+ text = str(obj.get(text_field, ""))
94
+ unique_cases.update(iter_contextual_cases(text))
95
+ return unique_cases
96
+
97
+
98
+ def summarize(unique_cases: set[tuple[str, str, str]]) -> dict[str, Any]:
99
+ """Build counts and percentages for unique cases grouped by tone and length."""
100
+ tone_counts = Counter(tone for _, _, tone in unique_cases)
101
+ length_counts = Counter(pinyin_length(syllable) for _, syllable, _ in unique_cases)
102
+ total = len(unique_cases)
103
+
104
+ tones = {}
105
+ for tone in TONE_ORDER:
106
+ count = tone_counts[tone]
107
+ percentage = (count / total * 100) if total else 0.0
108
+ tones[tone] = {
109
+ "count": count,
110
+ "percentage": round(percentage, 4),
111
+ }
112
+
113
+ lengths = [
114
+ {
115
+ "length": length,
116
+ "count": count,
117
+ "percentage": round((count / total * 100) if total else 0.0, 4),
118
+ }
119
+ for length, count in sorted(length_counts.items())
120
+ ]
121
+
122
+ return {
123
+ "total_unique_cases": total,
124
+ "tones": tones,
125
+ "lengths": lengths,
126
+ }
127
+
128
+
129
+ def write_case_list(
130
+ output_path: Path,
131
+ unique_cases: set[tuple[str, str, str]],
132
+ summary: dict[str, Any],
133
+ ) -> None:
134
+ """Write summary plus all unique mappings for later inspection."""
135
+ payload = {
136
+ **summary,
137
+ "unique_cases": [
138
+ {
139
+ "character": char,
140
+ "pinyin": syllable,
141
+ "tone": tone,
142
+ "length": pinyin_length(syllable),
143
+ }
144
+ for char, syllable, tone in sorted(unique_cases)
145
+ ],
146
+ }
147
+ output_path.parent.mkdir(parents=True, exist_ok=True)
148
+ output_path.write_text(
149
+ json.dumps(payload, ensure_ascii=False, indent=2),
150
+ encoding="utf-8",
151
+ newline="\n",
152
+ )
153
+
154
+
155
+ def parse_args() -> argparse.Namespace:
156
+ parser = argparse.ArgumentParser(
157
+ description=(
158
+ "Segment JSONL text with jieba, convert to contextual pinyin, and "
159
+ "count unique Chinese character/pinyin cases by tone and length."
160
+ )
161
+ )
162
+ parser.add_argument("--input", type=Path, required=True)
163
+ parser.add_argument("--output", type=Path)
164
+ parser.add_argument("--text-field", default="text")
165
+ parser.add_argument(
166
+ "--show-cases",
167
+ action="store_true",
168
+ help="Print every unique character/pinyin/tone case after the summary.",
169
+ )
170
+ return parser.parse_args()
171
+
172
+
173
+ def main() -> None:
174
+ require_dependencies()
175
+ if hasattr(sys.stdout, "reconfigure"):
176
+ sys.stdout.reconfigure(encoding="utf-8")
177
+
178
+ args = parse_args()
179
+ unique_cases = collect_unique_cases(args.input, args.text_field)
180
+ summary = summarize(unique_cases)
181
+
182
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
183
+
184
+ if args.show_cases:
185
+ for char, syllable, tone in sorted(unique_cases):
186
+ print(f"{char}\t{syllable}\t{tone}")
187
+
188
+ if args.output:
189
+ write_case_list(args.output, unique_cases, summary)
190
+ print(f"Wrote tone statistics to {args.output}")
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
195
+
196
+
197
+ """
198
+ 10k_babylm_zho.jsonl
199
+ {
200
+ "total_unique_cases": 5426,
201
+ "tones": {
202
+ "1": {
203
+ "count": 1370,
204
+ "percentage": 25.2488
205
+ },
206
+ "2": {
207
+ "count": 1352,
208
+ "percentage": 24.9171
209
+ },
210
+ "3": {
211
+ "count": 903,
212
+ "percentage": 16.6421
213
+ },
214
+ "4": {
215
+ "count": 1754,
216
+ "percentage": 32.3258
217
+ },
218
+ "blank": {
219
+ "count": 47,
220
+ "percentage": 0.8662
221
+ }
222
+ },
223
+ "lengths": [
224
+ {
225
+ "length": 1,
226
+ "count": 29,
227
+ "percentage": 0.5345
228
+ },
229
+ {
230
+ "length": 2,
231
+ "count": 1523,
232
+ "percentage": 28.0686
233
+ },
234
+ {
235
+ "length": 3,
236
+ "count": 2108,
237
+ "percentage": 38.85
238
+ },
239
+ {
240
+ "length": 4,
241
+ "count": 1449,
242
+ "percentage": 26.7048
243
+ },
244
+ {
245
+ "length": 5,
246
+ "count": 298,
247
+ "percentage": 5.4921
248
+ },
249
+ {
250
+ "length": 6,
251
+ "count": 19,
252
+ "percentage": 0.3502
253
+ }
254
+ ]
255
+ }
256
+ """
257
+
258
+ """
259
+ babylm_zho.jsonl
260
+ {
261
+ "total_unique_cases": 8231,
262
+ "tones": {
263
+ "1": {
264
+ "count": 2092,
265
+ "percentage": 25.4161
266
+ },
267
+ "2": {
268
+ "count": 2095,
269
+ "percentage": 25.4526
270
+ },
271
+ "3": {
272
+ "count": 1350,
273
+ "percentage": 16.4014
274
+ },
275
+ "4": {
276
+ "count": 2629,
277
+ "percentage": 31.9402
278
+ },
279
+ "blank": {
280
+ "count": 65,
281
+ "percentage": 0.7897
282
+ }
283
+ }
284
+ }
285
+ """
tokenization_pinyin_code.py CHANGED
@@ -2,16 +2,19 @@
2
 
3
  from __future__ import annotations
4
 
5
- import logging
6
- import math
7
- import re
8
- import shutil
9
- import unicodedata
10
- from pathlib import Path
11
- from typing import Any
12
 
13
- import sentencepiece as spm
14
- from transformers import PreTrainedTokenizer
 
 
 
15
 
16
 
17
  CHINESE_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]")
@@ -78,7 +81,7 @@ def should_preserve_fallback_token(token: str) -> bool:
78
  return True
79
 
80
 
81
- class PinyinCodeTokenizer(PreTrainedTokenizer):
82
  """Slow tokenizer that preserves the existing SentencePiece model."""
83
 
84
  vocab_files_names = {"vocab_file": "tokenizer.model"}
@@ -277,150 +280,150 @@ class PinyinCodeTokenizer(PreTrainedTokenizer):
277
 
278
  return " ".join(tokens)
279
 
280
- def _preprocess_tokenizer_input(self, value: Any) -> Any:
281
- if value is None:
282
- return None
283
- if isinstance(value, str):
284
- return self._preprocess_raw_text(value)
285
  if isinstance(value, tuple):
286
  return tuple(self._preprocess_tokenizer_input(item) for item in value)
287
  if isinstance(value, list):
288
- return [self._preprocess_tokenizer_input(item) for item in value]
289
- return value
290
-
291
- def _non_content_token_ids(self) -> set[int]:
292
- return {
293
- token_id
294
- for token_id in (
295
- self.pad_token_id,
296
- self.bos_token_id,
297
- self.eos_token_id,
298
- self.cls_token_id,
299
- self.sep_token_id,
300
- self.mask_token_id,
301
- )
302
- if token_id is not None
303
- }
304
-
305
- def _offset_source_text(self, value: Any, is_split_into_words: bool = False) -> str:
306
- if value is None:
307
- return ""
308
- if isinstance(value, str):
309
- return value
310
- if isinstance(value, tuple):
311
- return " ".join(self._offset_source_text(item) for item in value)
312
- if isinstance(value, list):
313
- separator = " " if is_split_into_words else ""
314
- return separator.join(self._offset_source_text(item) for item in value)
315
- return str(value)
316
-
317
- def _synthetic_offset_mapping(self, text: Any, input_ids: Any, is_split_into_words: bool = False) -> list[tuple[int, int]]:
318
- """Return slow-tokenizer-compatible offsets for evaluators that require them.
319
-
320
- SentencePiece offsets are not available for this Python tokenizer because
321
- raw Mandarin text is preprocessed into pinyin-code before encoding. These
322
- spans conservatively distribute non-special tokens across the original
323
- text so suffix/completion masking code can run without requiring a fast
324
- tokenizer.
325
- """
326
- ids = input_ids.tolist() if hasattr(input_ids, "tolist") else list(input_ids)
327
- source = self._offset_source_text(text, is_split_into_words=is_split_into_words)
328
- source_length = len(source)
329
- if not ids:
330
- return []
331
- if source_length == 0:
332
- return [(0, 0) for _ in ids]
333
-
334
- non_content_ids = self._non_content_token_ids()
335
- content_positions = [
336
- index for index, token_id in enumerate(ids) if int(token_id) not in non_content_ids
337
- ]
338
- if not content_positions:
339
- return [(0, 0) for _ in ids]
340
-
341
- offsets = [(0, 0) for _ in ids]
342
- count = len(content_positions)
343
- for ordinal, position in enumerate(content_positions):
344
- start = math.floor(ordinal * source_length / count)
345
- end = math.ceil((ordinal + 1) * source_length / count)
346
- if end <= start:
347
- end = min(source_length, start + 1)
348
- offsets[position] = (start, end)
349
- return offsets
350
-
351
- def _with_optional_offsets(
352
- self,
353
- encoding,
354
- original_text: Any,
355
- return_offsets_mapping: bool,
356
- is_split_into_words: bool = False,
357
- return_tensors: str | None = None,
358
- ):
359
- if not return_offsets_mapping:
360
- return encoding
361
-
362
- input_ids = encoding["input_ids"]
363
- tensor_input = hasattr(input_ids, "ndim")
364
- input_ids_list = input_ids.tolist() if tensor_input else input_ids
365
-
366
- is_batched = False
367
- if tensor_input:
368
- is_batched = input_ids.ndim > 1
369
- elif input_ids_list and isinstance(input_ids_list[0], list):
370
- is_batched = True
371
-
372
- if is_batched:
373
- if isinstance(original_text, list) and not is_split_into_words:
374
- texts = original_text
375
- else:
376
- texts = [original_text] * len(input_ids_list)
377
- offsets = [
378
- self._synthetic_offset_mapping(text, ids, is_split_into_words=is_split_into_words)
379
- for text, ids in zip(texts, input_ids_list)
380
- ]
381
- else:
382
- offsets = self._synthetic_offset_mapping(
383
- original_text,
384
- input_ids_list,
385
- is_split_into_words=is_split_into_words,
386
- )
387
-
388
- if return_tensors == "pt" or tensor_input:
389
- try:
390
- import torch
391
-
392
- offsets = torch.tensor(offsets, dtype=torch.long)
393
- except ImportError:
394
- pass
395
- encoding["offset_mapping"] = offsets
396
- return encoding
397
-
398
- def __call__(self, text=None, text_pair=None, *args, **kwargs):
399
- original_text = text
400
- return_offsets_mapping = bool(kwargs.pop("return_offsets_mapping", False))
401
- is_split_into_words = bool(kwargs.get("is_split_into_words", False))
402
- return_tensors = kwargs.get("return_tensors")
403
-
404
- if "text_target" in kwargs:
405
- kwargs["text_target"] = self._preprocess_tokenizer_input(kwargs["text_target"])
406
- if "text_pair_target" in kwargs:
407
- kwargs["text_pair_target"] = self._preprocess_tokenizer_input(
408
- kwargs["text_pair_target"]
409
  )
410
-
411
- text = self._preprocess_tokenizer_input(text)
412
- text_pair = self._preprocess_tokenizer_input(text_pair)
413
- if text_pair is None:
414
- encoding = super().__call__(text, *args, **kwargs)
415
- else:
416
- encoding = super().__call__(text, text_pair, *args, **kwargs)
417
- return self._with_optional_offsets(
418
- encoding,
419
- original_text,
420
- return_offsets_mapping,
421
- is_split_into_words=is_split_into_words,
422
- return_tensors=return_tensors,
423
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
  def encode(self, text, text_pair=None, add_special_tokens=True, *args, **kwargs):
426
  kwargs["add_special_tokens"] = add_special_tokens
@@ -430,43 +433,43 @@ class PinyinCodeTokenizer(PreTrainedTokenizer):
430
  return super().encode(text, *args, **kwargs)
431
  return super().encode(text, text_pair, *args, **kwargs)
432
 
433
- def encode_plus(self, text, text_pair=None, *args, **kwargs):
434
- original_text = text
435
- return_offsets_mapping = bool(kwargs.pop("return_offsets_mapping", False))
436
- is_split_into_words = bool(kwargs.get("is_split_into_words", False))
437
- return_tensors = kwargs.get("return_tensors")
438
-
439
- text = self._preprocess_tokenizer_input(text)
440
- text_pair = self._preprocess_tokenizer_input(text_pair)
441
- if text_pair is None:
442
- encoding = super().encode_plus(text, *args, **kwargs)
443
- else:
444
- encoding = super().encode_plus(text, text_pair, *args, **kwargs)
445
- return self._with_optional_offsets(
446
- encoding,
447
- original_text,
448
- return_offsets_mapping,
449
- is_split_into_words=is_split_into_words,
450
- return_tensors=return_tensors,
451
- )
452
-
453
- def batch_encode_plus(self, batch_text_or_text_pairs, *args, **kwargs):
454
- original_batch = batch_text_or_text_pairs
455
- return_offsets_mapping = bool(kwargs.pop("return_offsets_mapping", False))
456
- is_split_into_words = bool(kwargs.get("is_split_into_words", False))
457
- return_tensors = kwargs.get("return_tensors")
458
-
459
- batch_text_or_text_pairs = self._preprocess_tokenizer_input(
460
- batch_text_or_text_pairs
461
- )
462
- encoding = super().batch_encode_plus(batch_text_or_text_pairs, *args, **kwargs)
463
- return self._with_optional_offsets(
464
- encoding,
465
- original_batch,
466
- return_offsets_mapping,
467
- is_split_into_words=is_split_into_words,
468
- return_tensors=return_tensors,
469
- )
470
 
471
  @property
472
  def vocab_size(self) -> int:
@@ -544,5 +547,160 @@ class PinyinCodeTokenizer(PreTrainedTokenizer):
544
  return (str(output_path),)
545
 
546
 
547
- class EncodedMandarinTokenizer(PinyinCodeTokenizer):
548
- """Tokenizer wrapper that hides Hanzi-to-encoded-Mandarin preprocessing."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ import logging
6
+ import math
7
+ import re
8
+ import shutil
9
+ import unicodedata
10
+ from pathlib import Path
11
+ from typing import Any
12
 
13
+ import sentencepiece as spm
14
+ from tokenizers import Tokenizer, decoders, normalizers
15
+ from tokenizers.models import BPE
16
+ from transformers import PreTrainedTokenizer, PreTrainedTokenizerFast
17
+ from transformers.tokenization_utils_base import generate_merges
18
 
19
 
20
  CHINESE_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]")
 
81
  return True
82
 
83
 
84
+ class PinyinCodeTokenizer(PreTrainedTokenizer):
85
  """Slow tokenizer that preserves the existing SentencePiece model."""
86
 
87
  vocab_files_names = {"vocab_file": "tokenizer.model"}
 
280
 
281
  return " ".join(tokens)
282
 
283
+ def _preprocess_tokenizer_input(self, value: Any) -> Any:
284
+ if value is None:
285
+ return None
286
+ if isinstance(value, str):
287
+ return self._preprocess_raw_text(value)
288
  if isinstance(value, tuple):
289
  return tuple(self._preprocess_tokenizer_input(item) for item in value)
290
  if isinstance(value, list):
291
+ return [self._preprocess_tokenizer_input(item) for item in value]
292
+ return value
293
+
294
+ def _non_content_token_ids(self) -> set[int]:
295
+ return {
296
+ token_id
297
+ for token_id in (
298
+ self.pad_token_id,
299
+ self.bos_token_id,
300
+ self.eos_token_id,
301
+ self.cls_token_id,
302
+ self.sep_token_id,
303
+ self.mask_token_id,
304
+ )
305
+ if token_id is not None
306
+ }
307
+
308
+ def _offset_source_text(self, value: Any, is_split_into_words: bool = False) -> str:
309
+ if value is None:
310
+ return ""
311
+ if isinstance(value, str):
312
+ return value
313
+ if isinstance(value, tuple):
314
+ return " ".join(self._offset_source_text(item) for item in value)
315
+ if isinstance(value, list):
316
+ separator = " " if is_split_into_words else ""
317
+ return separator.join(self._offset_source_text(item) for item in value)
318
+ return str(value)
319
+
320
+ def _synthetic_offset_mapping(self, text: Any, input_ids: Any, is_split_into_words: bool = False) -> list[tuple[int, int]]:
321
+ """Return slow-tokenizer-compatible offsets for evaluators that require them.
322
+
323
+ SentencePiece offsets are not available for this Python tokenizer because
324
+ raw Mandarin text is preprocessed into pinyin-code before encoding. These
325
+ spans conservatively distribute non-special tokens across the original
326
+ text so suffix/completion masking code can run without requiring a fast
327
+ tokenizer.
328
+ """
329
+ ids = input_ids.tolist() if hasattr(input_ids, "tolist") else list(input_ids)
330
+ source = self._offset_source_text(text, is_split_into_words=is_split_into_words)
331
+ source_length = len(source)
332
+ if not ids:
333
+ return []
334
+ if source_length == 0:
335
+ return [(0, 0) for _ in ids]
336
+
337
+ non_content_ids = self._non_content_token_ids()
338
+ content_positions = [
339
+ index for index, token_id in enumerate(ids) if int(token_id) not in non_content_ids
340
+ ]
341
+ if not content_positions:
342
+ return [(0, 0) for _ in ids]
343
+
344
+ offsets = [(0, 0) for _ in ids]
345
+ count = len(content_positions)
346
+ for ordinal, position in enumerate(content_positions):
347
+ start = math.floor(ordinal * source_length / count)
348
+ end = math.ceil((ordinal + 1) * source_length / count)
349
+ if end <= start:
350
+ end = min(source_length, start + 1)
351
+ offsets[position] = (start, end)
352
+ return offsets
353
+
354
+ def _with_optional_offsets(
355
+ self,
356
+ encoding,
357
+ original_text: Any,
358
+ return_offsets_mapping: bool,
359
+ is_split_into_words: bool = False,
360
+ return_tensors: str | None = None,
361
+ ):
362
+ if not return_offsets_mapping:
363
+ return encoding
364
+
365
+ input_ids = encoding["input_ids"]
366
+ tensor_input = hasattr(input_ids, "ndim")
367
+ input_ids_list = input_ids.tolist() if tensor_input else input_ids
368
+
369
+ is_batched = False
370
+ if tensor_input:
371
+ is_batched = input_ids.ndim > 1
372
+ elif input_ids_list and isinstance(input_ids_list[0], list):
373
+ is_batched = True
374
+
375
+ if is_batched:
376
+ if isinstance(original_text, list) and not is_split_into_words:
377
+ texts = original_text
378
+ else:
379
+ texts = [original_text] * len(input_ids_list)
380
+ offsets = [
381
+ self._synthetic_offset_mapping(text, ids, is_split_into_words=is_split_into_words)
382
+ for text, ids in zip(texts, input_ids_list)
383
+ ]
384
+ else:
385
+ offsets = self._synthetic_offset_mapping(
386
+ original_text,
387
+ input_ids_list,
388
+ is_split_into_words=is_split_into_words,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  )
390
+
391
+ if return_tensors == "pt" or tensor_input:
392
+ try:
393
+ import torch
394
+
395
+ offsets = torch.tensor(offsets, dtype=torch.long)
396
+ except ImportError:
397
+ pass
398
+ encoding["offset_mapping"] = offsets
399
+ return encoding
400
+
401
+ def __call__(self, text=None, text_pair=None, *args, **kwargs):
402
+ original_text = text
403
+ return_offsets_mapping = bool(kwargs.pop("return_offsets_mapping", False))
404
+ is_split_into_words = bool(kwargs.get("is_split_into_words", False))
405
+ return_tensors = kwargs.get("return_tensors")
406
+
407
+ if "text_target" in kwargs:
408
+ kwargs["text_target"] = self._preprocess_tokenizer_input(kwargs["text_target"])
409
+ if "text_pair_target" in kwargs:
410
+ kwargs["text_pair_target"] = self._preprocess_tokenizer_input(
411
+ kwargs["text_pair_target"]
412
+ )
413
+
414
+ text = self._preprocess_tokenizer_input(text)
415
+ text_pair = self._preprocess_tokenizer_input(text_pair)
416
+ if text_pair is None:
417
+ encoding = super().__call__(text, *args, **kwargs)
418
+ else:
419
+ encoding = super().__call__(text, text_pair, *args, **kwargs)
420
+ return self._with_optional_offsets(
421
+ encoding,
422
+ original_text,
423
+ return_offsets_mapping,
424
+ is_split_into_words=is_split_into_words,
425
+ return_tensors=return_tensors,
426
+ )
427
 
428
  def encode(self, text, text_pair=None, add_special_tokens=True, *args, **kwargs):
429
  kwargs["add_special_tokens"] = add_special_tokens
 
433
  return super().encode(text, *args, **kwargs)
434
  return super().encode(text, text_pair, *args, **kwargs)
435
 
436
+ def encode_plus(self, text, text_pair=None, *args, **kwargs):
437
+ original_text = text
438
+ return_offsets_mapping = bool(kwargs.pop("return_offsets_mapping", False))
439
+ is_split_into_words = bool(kwargs.get("is_split_into_words", False))
440
+ return_tensors = kwargs.get("return_tensors")
441
+
442
+ text = self._preprocess_tokenizer_input(text)
443
+ text_pair = self._preprocess_tokenizer_input(text_pair)
444
+ if text_pair is None:
445
+ encoding = super().encode_plus(text, *args, **kwargs)
446
+ else:
447
+ encoding = super().encode_plus(text, text_pair, *args, **kwargs)
448
+ return self._with_optional_offsets(
449
+ encoding,
450
+ original_text,
451
+ return_offsets_mapping,
452
+ is_split_into_words=is_split_into_words,
453
+ return_tensors=return_tensors,
454
+ )
455
+
456
+ def batch_encode_plus(self, batch_text_or_text_pairs, *args, **kwargs):
457
+ original_batch = batch_text_or_text_pairs
458
+ return_offsets_mapping = bool(kwargs.pop("return_offsets_mapping", False))
459
+ is_split_into_words = bool(kwargs.get("is_split_into_words", False))
460
+ return_tensors = kwargs.get("return_tensors")
461
+
462
+ batch_text_or_text_pairs = self._preprocess_tokenizer_input(
463
+ batch_text_or_text_pairs
464
+ )
465
+ encoding = super().batch_encode_plus(batch_text_or_text_pairs, *args, **kwargs)
466
+ return self._with_optional_offsets(
467
+ encoding,
468
+ original_batch,
469
+ return_offsets_mapping,
470
+ is_split_into_words=is_split_into_words,
471
+ return_tensors=return_tensors,
472
+ )
473
 
474
  @property
475
  def vocab_size(self) -> int:
 
547
  return (str(output_path),)
548
 
549
 
550
+ class EncodedMandarinTokenizer(PinyinCodeTokenizer):
551
+ """Tokenizer wrapper that hides Hanzi-to-encoded-Mandarin preprocessing."""
552
+
553
+
554
+ def build_sentencepiece_bpe_backend(vocab_file: str) -> Tokenizer:
555
+ """Build a tokenizers backend equivalent to the trained SentencePiece BPE."""
556
+ processor = spm.SentencePieceProcessor(model_file=vocab_file)
557
+ vocab = {
558
+ processor.id_to_piece(index): index
559
+ for index in range(processor.get_piece_size())
560
+ }
561
+ tokenizer = Tokenizer(
562
+ BPE(
563
+ vocab=vocab,
564
+ merges=generate_merges(vocab),
565
+ unk_token=processor.id_to_piece(processor.unk_id()),
566
+ fuse_unk=False,
567
+ )
568
+ )
569
+ tokenizer.normalizer = normalizers.Sequence(
570
+ [normalizers.Prepend("▁"), normalizers.Replace(" ", "▁")]
571
+ )
572
+ tokenizer.decoder = decoders.Sequence([decoders.Replace("▁", " ")])
573
+ return tokenizer
574
+
575
+
576
+ class EncodedMandarinTokenizerFast(PreTrainedTokenizerFast):
577
+ """Fast tokenizer preserving the raw-Hanzi pinyin-code preprocessing path."""
578
+
579
+ vocab_files_names = {
580
+ "vocab_file": "tokenizer.model",
581
+ "tokenizer_file": "tokenizer.json",
582
+ }
583
+ slow_tokenizer_class = EncodedMandarinTokenizer
584
+ model_input_names = ["input_ids", "attention_mask"]
585
+
586
+ def __init__(
587
+ self,
588
+ vocab_file: str | None = None,
589
+ tokenizer_file: str | None = None,
590
+ add_bos_token: bool = False,
591
+ add_eos_token: bool = False,
592
+ transliteration: str = "pinyin-code",
593
+ pinyin_format: str | None = None,
594
+ use_jieba: bool = True,
595
+ jieba: bool | None = None,
596
+ **kwargs,
597
+ ) -> None:
598
+ if vocab_file is None:
599
+ raise ValueError("EncodedMandarinTokenizerFast requires tokenizer.model")
600
+ self.vocab_file = vocab_file
601
+ self.sp_model = spm.SentencePieceProcessor(model_file=vocab_file)
602
+ self.transliteration = PinyinCodeTokenizer._normalize_transliteration(
603
+ self,
604
+ pinyin_format or transliteration,
605
+ )
606
+ self.use_jieba = use_jieba if jieba is None else jieba
607
+
608
+ kwargs.setdefault("unk_token", self._piece_or_none(self.sp_model.unk_id()))
609
+ kwargs.setdefault("bos_token", self._piece_or_none(self.sp_model.bos_id()))
610
+ kwargs.setdefault("eos_token", self._piece_or_none(self.sp_model.eos_id()))
611
+ kwargs.setdefault("pad_token", self._piece_or_none(self.sp_model.pad_id()))
612
+ kwargs.setdefault("transliteration", self.transliteration)
613
+ kwargs.setdefault("pinyin_format", self.transliteration)
614
+ kwargs.setdefault("use_jieba", self.use_jieba)
615
+ kwargs.setdefault("jieba", self.use_jieba)
616
+
617
+ if tokenizer_file is None:
618
+ kwargs["tokenizer_object"] = build_sentencepiece_bpe_backend(vocab_file)
619
+ super().__init__(
620
+ vocab_file=vocab_file,
621
+ tokenizer_file=tokenizer_file,
622
+ add_bos_token=add_bos_token,
623
+ add_eos_token=add_eos_token,
624
+ **kwargs,
625
+ )
626
+
627
+ _normalize_transliteration = PinyinCodeTokenizer._normalize_transliteration
628
+ _piece_or_none = PinyinCodeTokenizer._piece_or_none
629
+ _looks_preprocessed = PinyinCodeTokenizer._looks_preprocessed
630
+ _preprocess_raw_text = PinyinCodeTokenizer._preprocess_raw_text
631
+ _fallback_process_text = PinyinCodeTokenizer._fallback_process_text
632
+
633
+ def _preprocess_tokenizer_input(self, value: Any) -> Any:
634
+ if value is None:
635
+ return None
636
+ if isinstance(value, str):
637
+ return self._preprocess_raw_text(value)
638
+ if isinstance(value, tuple):
639
+ return tuple(self._preprocess_tokenizer_input(item) for item in value)
640
+ if isinstance(value, list):
641
+ return [self._preprocess_tokenizer_input(item) for item in value]
642
+ return value
643
+
644
+ def __call__(self, text=None, text_pair=None, *args, **kwargs):
645
+ if "text_target" in kwargs:
646
+ kwargs["text_target"] = self._preprocess_tokenizer_input(kwargs["text_target"])
647
+ if "text_pair_target" in kwargs:
648
+ kwargs["text_pair_target"] = self._preprocess_tokenizer_input(
649
+ kwargs["text_pair_target"]
650
+ )
651
+
652
+ text = self._preprocess_tokenizer_input(text)
653
+ text_pair = self._preprocess_tokenizer_input(text_pair)
654
+ if text_pair is None:
655
+ return super().__call__(text, *args, **kwargs)
656
+ return super().__call__(text, text_pair, *args, **kwargs)
657
+
658
+ def encode(self, text, text_pair=None, add_special_tokens=True, *args, **kwargs):
659
+ kwargs["add_special_tokens"] = add_special_tokens
660
+ text = self._preprocess_tokenizer_input(text)
661
+ text_pair = self._preprocess_tokenizer_input(text_pair)
662
+ if text_pair is None:
663
+ return super().encode(text, *args, **kwargs)
664
+ return super().encode(text, text_pair, *args, **kwargs)
665
+
666
+ def encode_plus(self, text, text_pair=None, *args, **kwargs):
667
+ text = self._preprocess_tokenizer_input(text)
668
+ text_pair = self._preprocess_tokenizer_input(text_pair)
669
+ if text_pair is None:
670
+ return super().encode_plus(text, *args, **kwargs)
671
+ return super().encode_plus(text, text_pair, *args, **kwargs)
672
+
673
+ def batch_encode_plus(self, batch_text_or_text_pairs, *args, **kwargs):
674
+ batch_text_or_text_pairs = self._preprocess_tokenizer_input(
675
+ batch_text_or_text_pairs
676
+ )
677
+ return super().batch_encode_plus(batch_text_or_text_pairs, *args, **kwargs)
678
+
679
+ def build_inputs_with_special_tokens(
680
+ self,
681
+ token_ids_0: list[int],
682
+ token_ids_1: list[int] | None = None,
683
+ ) -> list[int]:
684
+ output = list(token_ids_0)
685
+ if self.add_bos_token and self.bos_token_id is not None:
686
+ output = [self.bos_token_id] + output
687
+ if self.add_eos_token and self.eos_token_id is not None:
688
+ output = output + [self.eos_token_id]
689
+ if token_ids_1 is not None:
690
+ output += list(token_ids_1)
691
+ if self.add_eos_token and self.eos_token_id is not None:
692
+ output.append(self.eos_token_id)
693
+ return output
694
+
695
+ def save_vocabulary(
696
+ self,
697
+ save_directory: str,
698
+ filename_prefix: str | None = None,
699
+ ) -> tuple[str]:
700
+ output_name = "tokenizer.model"
701
+ if filename_prefix:
702
+ output_name = f"{filename_prefix}-{output_name}"
703
+ output_path = Path(save_directory) / output_name
704
+ if Path(self.vocab_file).resolve() != output_path.resolve():
705
+ shutil.copyfile(self.vocab_file, output_path)
706
+ return (str(output_path),)
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json CHANGED
@@ -1,53 +1,20 @@
1
  {
2
- "added_tokens_decoder": {
3
- "0": {
4
- "content": "<pad>",
5
- "lstrip": false,
6
- "normalized": false,
7
- "rstrip": false,
8
- "single_word": false,
9
- "special": true
10
- },
11
- "1": {
12
- "content": "<unk>",
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": "</s>",
29
- "lstrip": false,
30
- "normalized": false,
31
- "rstrip": false,
32
- "single_word": false,
33
- "special": true
34
- }
35
- },
36
- "auto_map": {
37
- "AutoTokenizer": [
38
- "tokenization_pinyin_code.EncodedMandarinTokenizer",
39
- null
40
- ]
41
- },
42
- "backend": "custom",
43
  "bos_token": "<s>",
44
  "eos_token": "</s>",
45
  "jieba": true,
46
  "model_max_length": 512,
47
  "pad_token": "<pad>",
48
  "pinyin_format": "pinyin-code",
49
- "tokenizer_class": "EncodedMandarinTokenizer",
50
  "transliteration": "pinyin-code",
51
  "unk_token": "<unk>",
52
- "use_jieba": true
 
 
 
 
 
 
 
53
  }
 
1
  {
2
+ "backend": "tokenizers",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  "bos_token": "<s>",
4
  "eos_token": "</s>",
5
  "jieba": true,
6
  "model_max_length": 512,
7
  "pad_token": "<pad>",
8
  "pinyin_format": "pinyin-code",
9
+ "tokenizer_class": "EncodedMandarinTokenizerFast",
10
  "transliteration": "pinyin-code",
11
  "unk_token": "<unk>",
12
+ "use_jieba": true,
13
+ "auto_map": {
14
+ "AutoTokenizer": [
15
+ "tokenization_pinyin_code.EncodedMandarinTokenizer",
16
+ "tokenization_pinyin_code.EncodedMandarinTokenizerFast"
17
+ ]
18
+ },
19
+ "tokenizer_kind": "sentencepiece"
20
  }