timorobrecht commited on
Commit
eea02cb
·
verified ·
1 Parent(s): acc1d19

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ pipeline_tag: text-generation
4
+ tags:
5
+ - causal-lm
6
+ - trust-remote-code
7
+ - sentencepiece
8
+ ---
9
+
10
+ # Pinyin-Code Causal LM
11
+
12
+ This repository contains a custom Transformers causal language model.
13
+ External evaluation repositories should load it with
14
+ `trust_remote_code=True` and use the `causal` backend.
15
+
16
+ ## Dependencies
17
+
18
+ Install the runtime dependencies before loading the model:
19
+
20
+ ```bash
21
+ pip install torch transformers safetensors sentencepiece pypinyin jieba
22
+ ```
23
+
24
+ `sentencepiece` is required for `AutoTokenizer`. `pypinyin` is required
25
+ for raw Mandarin-to-pinyin tokenization. `jieba` is required when
26
+ `use_jieba` is true; this export was created with `use_jieba=true`.
27
+
28
+ ## Loading
29
+
30
+ ```python
31
+ from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoTokenizer
32
+
33
+ model_path = "PATH_OR_REPO_ID"
34
+
35
+ config = AutoConfig.from_pretrained(model_path, trust_remote_code=True)
36
+ tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
37
+ base_model = AutoModel.from_pretrained(model_path, trust_remote_code=True)
38
+ model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True)
39
+ ```
40
+
41
+ ## Evaluation
42
+
43
+ Configure external evaluators with:
44
+
45
+ - model path: this local folder or Hugging Face repo ID
46
+ - backend: `causal`
47
+ - trust remote code: enabled
48
+
49
+ The tokenizer accepts raw text through standard calls such as
50
+ `tokenizer(text)`, `tokenizer(text, add_special_tokens=False)`, and
51
+ `tokenizer(texts, padding=True, truncation=True, return_tensors="pt")`.
52
+ It also accepts `return_offsets_mapping=True` for compatibility with
53
+ completion-ranking evaluators that need suffix masks. The model supports
54
+ `output_hidden_states=True` for representation extraction tasks.
55
+
56
+ This export sets `patch_pathlib_utf8_open=true` in `config.json`.
57
+ When loaded with `trust_remote_code=True`, the config installs a narrow
58
+ Windows compatibility shim so later text-mode `Path.open("r")` calls
59
+ without an explicit encoding default to UTF-8. Set
60
+ `PINYIN_CODE_DISABLE_UTF8_OPEN_PATCH=1` before loading the model to
61
+ disable that shim.
62
+
63
+ Export metadata:
64
+
65
+ - transliteration: `pinyin-code`
66
+ - use_jieba: `true`
config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "PinyinCodeForCausalLM"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_pinyin_code.PinyinCodeConfig",
7
+ "AutoModel": "modeling_pinyin_code.PinyinCodeModel",
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,
16
+ "dropout": 0.1,
17
+ "dtype": "float32",
18
+ "eos_token_id": 3,
19
+ "evaluation_backend": "causal",
20
+ "hidden_size": 512,
21
+ "is_decoder": true,
22
+ "max_position_embeddings": 512,
23
+ "model_type": "pinyin_code",
24
+ "n_embd": 512,
25
+ "n_head": 8,
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
35
+ }
configuration_pinyin_code.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
57
+ def __init__(
58
+ self,
59
+ vocab_size: int = 8000,
60
+ block_size: int = 128,
61
+ n_layer: int = 6,
62
+ n_head: int = 8,
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,
78
+ )
79
+ self.vocab_size = vocab_size
80
+ self.block_size = block_size
81
+ self.n_layer = n_layer
82
+ self.n_head = n_head
83
+ self.n_embd = n_embd
84
+ self.dropout = dropout
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()
full_chinese_spm.vocab ADDED
The diff for this file is too large to render. See raw diff
 
generation_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 2,
3
+ "eos_token_id": 3,
4
+ "pad_token_id": 0,
5
+ "transformers_version": "5.10.2"
6
+ }
hf/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Transformers integration for the pinyin-code language model."""
2
+
3
+ from .configuration_pinyin_code import PinyinCodeConfig
4
+ from .modeling_pinyin_code import PinyinCodeForCausalLM, PinyinCodeModel
5
+ from .tokenization_pinyin_code import EncodedMandarinTokenizer, PinyinCodeTokenizer
6
+
7
+ __all__ = [
8
+ "EncodedMandarinTokenizer",
9
+ "PinyinCodeConfig",
10
+ "PinyinCodeForCausalLM",
11
+ "PinyinCodeModel",
12
+ "PinyinCodeTokenizer",
13
+ ]
hf/configuration_pinyin_code.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
57
+ def __init__(
58
+ self,
59
+ vocab_size: int = 8000,
60
+ block_size: int = 128,
61
+ n_layer: int = 6,
62
+ n_head: int = 8,
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,
78
+ )
79
+ self.vocab_size = vocab_size
80
+ self.block_size = block_size
81
+ self.n_layer = n_layer
82
+ self.n_head = n_head
83
+ self.n_embd = n_embd
84
+ self.dropout = dropout
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()
hf/modeling_pinyin_code.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transformers-compatible implementation of the pinyin-code causal LM."""
2
+
3
+ from __future__ import annotations
4
+
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
+
14
+
15
+ class CausalSelfAttention(nn.Module):
16
+ """Multi-head masked self-attention matching the original training module."""
17
+
18
+ def __init__(self, config: PinyinCodeConfig) -> None:
19
+ super().__init__()
20
+ if config.n_embd % config.n_head != 0:
21
+ raise ValueError("n_embd must be divisible by n_head")
22
+
23
+ self.n_head = config.n_head
24
+ self.head_dim = config.n_embd // config.n_head
25
+ self.dropout_p = config.dropout
26
+ self.qkv = nn.Linear(config.n_embd, 3 * config.n_embd)
27
+ self.proj = nn.Linear(config.n_embd, config.n_embd)
28
+ self.resid_dropout = nn.Dropout(config.dropout)
29
+
30
+ def forward(self, x: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
31
+ batch_size, seq_len, embd = x.shape
32
+ q, k, v = self.qkv(x).split(embd, dim=2)
33
+
34
+ q = q.view(batch_size, seq_len, self.n_head, self.head_dim).transpose(1, 2)
35
+ k = k.view(batch_size, seq_len, self.n_head, self.head_dim).transpose(1, 2)
36
+ v = v.view(batch_size, seq_len, self.n_head, self.head_dim).transpose(1, 2)
37
+
38
+ dropout_p = self.dropout_p if self.training else 0.0
39
+ if attention_mask is not None:
40
+ causal_mask = torch.ones(
41
+ seq_len,
42
+ seq_len,
43
+ device=x.device,
44
+ dtype=torch.bool,
45
+ ).tril()
46
+ key_mask = attention_mask[:, None, None, :seq_len].to(dtype=torch.bool)
47
+ attn_mask = causal_mask.view(1, 1, seq_len, seq_len) & key_mask
48
+ y = F.scaled_dot_product_attention(
49
+ q,
50
+ k,
51
+ v,
52
+ attn_mask=attn_mask,
53
+ dropout_p=dropout_p,
54
+ is_causal=False,
55
+ )
56
+ else:
57
+ y = F.scaled_dot_product_attention(
58
+ q,
59
+ k,
60
+ v,
61
+ dropout_p=dropout_p,
62
+ is_causal=True,
63
+ )
64
+
65
+ y = y.transpose(1, 2).contiguous().view(batch_size, seq_len, embd)
66
+ return self.resid_dropout(self.proj(y))
67
+
68
+
69
+ class FeedForward(nn.Module):
70
+ """Transformer MLP block."""
71
+
72
+ def __init__(self, config: PinyinCodeConfig) -> None:
73
+ super().__init__()
74
+ self.net = nn.Sequential(
75
+ nn.Linear(config.n_embd, 4 * config.n_embd),
76
+ nn.GELU(),
77
+ nn.Linear(4 * config.n_embd, config.n_embd),
78
+ nn.Dropout(config.dropout),
79
+ )
80
+
81
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
82
+ return self.net(x)
83
+
84
+
85
+ class TransformerBlock(nn.Module):
86
+ """Pre-norm Transformer block."""
87
+
88
+ def __init__(self, config: PinyinCodeConfig) -> None:
89
+ super().__init__()
90
+ self.ln_1 = nn.LayerNorm(config.n_embd)
91
+ self.attn = CausalSelfAttention(config)
92
+ self.ln_2 = nn.LayerNorm(config.n_embd)
93
+ self.mlp = FeedForward(config)
94
+
95
+ def forward(self, x: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
96
+ x = x + self.attn(self.ln_1(x), attention_mask=attention_mask)
97
+ x = x + self.mlp(self.ln_2(x))
98
+ return x
99
+
100
+
101
+ class PinyinCodePreTrainedModel(PreTrainedModel):
102
+ """Base class for pinyin-code Transformers models."""
103
+
104
+ config_class = PinyinCodeConfig
105
+ base_model_prefix = "pinyin_code"
106
+ supports_gradient_checkpointing = False
107
+
108
+ def _init_weights(self, module: nn.Module) -> None:
109
+ if isinstance(module, nn.Linear):
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
223
+
224
+ def prepare_inputs_for_generation(
225
+ self,
226
+ input_ids: torch.Tensor,
227
+ past_key_values=None,
228
+ attention_mask: torch.Tensor | None = None,
229
+ **kwargs,
230
+ ) -> dict:
231
+ if input_ids.shape[1] > self.config.block_size:
232
+ input_ids = input_ids[:, -self.config.block_size :]
233
+ if attention_mask is not None:
234
+ attention_mask = attention_mask[:, -self.config.block_size :]
235
+ position_ids = None
236
+ if attention_mask is not None:
237
+ position_ids = attention_mask.long().cumsum(dim=-1) - 1
238
+ position_ids = position_ids.clamp_min(0)
239
+ return {
240
+ "input_ids": input_ids,
241
+ "attention_mask": attention_mask,
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,
262
+ inputs_embeds=inputs_embeds,
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
+ )
hf/tokenization_pinyin_code.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SentencePiece tokenizer wrapper for pinyin-code Transformers models."""
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]")
18
+ CHINESE_SPAN_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
19
+ PINYIN_CODE_TOKEN_RE = re.compile(
20
+ r"(?<![A-Za-z0-9])[A-Za-z]\d(?:[A-Za-z]\d)*(?![A-Za-z0-9])"
21
+ )
22
+ SPECIAL_MARKER_RE = re.compile(r"<[A-Z_]+>")
23
+ PUNCTUATION = set(
24
+ "\u3002\uff0c\u3001\uff1f\uff01\uff1a\uff1b.,?!:;()[]{}<>\u300a\u300b"
25
+ "\u3010\u3011\u201c\u201d\"'\u2018\u2019\u300c\u300d\u300e\u300f"
26
+ "\u2014-~\u2026/\\"
27
+ )
28
+ LATIN_LETTER = (
29
+ r"A-Za-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff"
30
+ r"\u0100-\u017f\u0180-\u024f\u0250-\u02af"
31
+ )
32
+ LATIN_ALNUM_PATTERN = (
33
+ rf"(?:[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
34
+ rf"(?:[-_][{LATIN_LETTER}0-9]+)*|"
35
+ rf"[0-9]+[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
36
+ rf"(?:[-_][{LATIN_LETTER}0-9]+)*)"
37
+ )
38
+ LATIN_ALNUM_RE = re.compile(LATIN_ALNUM_PATTERN)
39
+ URL_RE = re.compile(r"\b(?:https?://\S*|www\.\S+)", flags=re.I)
40
+ DISCARDED_UNICODE_CATEGORIES = {"Cc", "Cf", "Co", "Cs", "Cn"}
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
+ LABELS = {
48
+ "\u9898\u5e72": "<QUESTION>",
49
+ "\u9009\u9879": "<OPTIONS>",
50
+ "\u7b54\u6848": "<ANSWER>",
51
+ "\u89e3\u6790": "<EXPLANATION>",
52
+ }
53
+ PINYIN_FORMAT_ALIASES = {
54
+ "code": "pinyin-code",
55
+ "codes": "pinyin-code",
56
+ "pinyin-code": "pinyin-code",
57
+ "initial": "pinyin-initial",
58
+ "initials": "pinyin-initial",
59
+ "pinyin-initial": "pinyin-initial",
60
+ "hanzi": "hanzi",
61
+ }
62
+
63
+
64
+ def latin_token_to_model_token(token: str) -> str:
65
+ upper = token.upper()
66
+ return upper if upper in {"A", "B", "C", "D"} else token.lower()
67
+
68
+
69
+ def should_preserve_fallback_token(token: str) -> bool:
70
+ if token == "\ufffd":
71
+ return False
72
+ for char in token:
73
+ category = unicodedata.category(char)
74
+ if category in DISCARDED_UNICODE_CATEGORIES:
75
+ return False
76
+ if category[0] not in {"L", "P", "S"}:
77
+ return False
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"}
85
+ model_input_names = ["input_ids", "attention_mask"]
86
+
87
+ def __init__(
88
+ self,
89
+ vocab_file: str,
90
+ add_bos_token: bool = False,
91
+ add_eos_token: bool = False,
92
+ transliteration: str = "pinyin-code",
93
+ pinyin_format: str | None = None,
94
+ use_jieba: bool = True,
95
+ jieba: bool | None = None,
96
+ **kwargs,
97
+ ) -> None:
98
+ self.vocab_file = vocab_file
99
+ self.sp_model = spm.SentencePieceProcessor(model_file=vocab_file)
100
+ self.add_bos_token = add_bos_token
101
+ self.add_eos_token = add_eos_token
102
+ self.transliteration = self._normalize_transliteration(
103
+ pinyin_format or transliteration
104
+ )
105
+ self.use_jieba = use_jieba if jieba is None else jieba
106
+
107
+ kwargs.setdefault("unk_token", self._piece_or_none(self.sp_model.unk_id()))
108
+ kwargs.setdefault("bos_token", self._piece_or_none(self.sp_model.bos_id()))
109
+ kwargs.setdefault("eos_token", self._piece_or_none(self.sp_model.eos_id()))
110
+ kwargs.setdefault("pad_token", self._piece_or_none(self.sp_model.pad_id()))
111
+ kwargs.setdefault("transliteration", self.transliteration)
112
+ kwargs.setdefault("pinyin_format", self.transliteration)
113
+ kwargs.setdefault("use_jieba", self.use_jieba)
114
+ kwargs.setdefault("jieba", self.use_jieba)
115
+ super().__init__(**kwargs)
116
+
117
+ def _normalize_transliteration(self, value: str) -> str:
118
+ normalized = PINYIN_FORMAT_ALIASES.get(value.lower())
119
+ if normalized is None:
120
+ allowed = ", ".join(sorted(set(PINYIN_FORMAT_ALIASES.values())))
121
+ raise ValueError(f"Unsupported transliteration {value!r}; choose from {allowed}")
122
+ return normalized
123
+
124
+ def _piece_or_none(self, token_id: int) -> str | None:
125
+ if token_id is None or token_id < 0:
126
+ return None
127
+ return self.sp_model.id_to_piece(token_id)
128
+
129
+ def _looks_preprocessed(self, text: str) -> bool:
130
+ if SPECIAL_MARKER_RE.search(text):
131
+ return True
132
+ if self.transliteration == "pinyin-code" and PINYIN_CODE_TOKEN_RE.search(text):
133
+ return True
134
+ return False
135
+
136
+ def _preprocess_raw_text(self, text: str) -> str:
137
+ if not CHINESE_RE.search(text) and self._looks_preprocessed(text):
138
+ return text
139
+ try:
140
+ from preprocessing.preprocess import (
141
+ hanzi_to_encoded,
142
+ process_text,
143
+ require_dependencies,
144
+ )
145
+ except ImportError:
146
+ return self._fallback_process_text(text)
147
+
148
+ require_dependencies()
149
+ if self.transliteration == "pinyin-code":
150
+ return hanzi_to_encoded(text, self.use_jieba)
151
+ return process_text(text, self.transliteration, self.use_jieba)
152
+
153
+ def _fallback_process_text(self, text: str) -> str:
154
+ if self.use_jieba:
155
+ try:
156
+ import jieba
157
+ except ImportError as exc:
158
+ raise ImportError(
159
+ "Tokenizing raw Mandarin benchmark text with jieba segmentation "
160
+ "requires jieba. Install the model dependencies before running "
161
+ "lm_eval."
162
+ ) from exc
163
+
164
+ jieba.setLogLevel(logging.WARNING)
165
+ else:
166
+ jieba = None
167
+
168
+ if self.transliteration != "hanzi":
169
+ try:
170
+ from pypinyin import Style, pinyin
171
+ except ImportError as exc:
172
+ raise ImportError(
173
+ "Tokenizing raw Mandarin benchmark text as pinyin requires pypinyin. "
174
+ "Install the model dependencies before running lm_eval."
175
+ ) from exc
176
+
177
+
178
+ def normalize_text(value: str) -> str:
179
+ value = unicodedata.normalize("NFKC", value)
180
+ value = URL_RE.sub(" <URL> ", value)
181
+ value = re.sub(r"\$\$.*?\$\$", " <MATH> ", value, flags=re.DOTALL)
182
+ value = re.sub(r"[\uff08(]\s*[\uff09)]", " <BLANK> ", value)
183
+ for label, marker in LABELS.items():
184
+ value = re.sub(rf"{label}\s*[:\uff1a]", f" {marker} ", value)
185
+ value = re.sub(
186
+ rf"(?<![{LATIN_LETTER}])yes(?![{LATIN_LETTER}])",
187
+ " <YES> ",
188
+ value,
189
+ flags=re.I,
190
+ )
191
+ value = re.sub(
192
+ rf"(?<![{LATIN_LETTER}])no(?![{LATIN_LETTER}])",
193
+ " <NO> ",
194
+ value,
195
+ flags=re.I,
196
+ )
197
+ value = re.sub(
198
+ rf"(?<![{LATIN_LETTER}])[ABCD](?=\s*[:\uff1a.\uff0e\u3001\)])",
199
+ r" \g<0> ",
200
+ value,
201
+ )
202
+ value = re.sub(
203
+ rf"(?<![{LATIN_LETTER}0-9])[-+]?\d+(?:[.,]\d+)*(?:%|\uff05)?"
204
+ rf"(?![{LATIN_LETTER}0-9])",
205
+ " <NUM> ",
206
+ value,
207
+ )
208
+ value = value.replace("\uff08", "(").replace("\uff09", ")")
209
+ return re.sub(r"\s+", " ", value).strip()
210
+
211
+ def split_tone3_syllable(syllable: str) -> tuple[str, int]:
212
+ match = re.fullmatch(r"([a-z\u00fcv]+)([1-5]?)", syllable.lower())
213
+ if not match:
214
+ return syllable, 5
215
+ plain, tone = match.groups()
216
+ return plain, int(tone or "5")
217
+
218
+ def length_digit_offset(syllable: str) -> int:
219
+ return min(max(len(syllable), 1), 5) - 1
220
+
221
+ def syllable_to_initial_code(syllable: str) -> str:
222
+ plain, tone = split_tone3_syllable(syllable)
223
+ if not plain:
224
+ return ""
225
+ tone_offset = 5 if tone in {3, 4, 5} else 0
226
+ digit = tone_offset + length_digit_offset(plain)
227
+ initial = plain[0].upper() if tone in {1, 3, 5} else plain[0].lower()
228
+ return f"{initial}{digit}"
229
+
230
+ def syllable_to_initial_letter(syllable: str) -> str:
231
+ plain, _ = split_tone3_syllable(syllable)
232
+ return plain[:1].lower()
233
+
234
+ def convert_word(word: str) -> str:
235
+ if self.transliteration == "hanzi":
236
+ return word
237
+ syllables = pinyin(word, style=Style.TONE3, heteronym=False, errors="ignore")
238
+ if self.transliteration == "pinyin-code":
239
+ codes = [
240
+ syllable_to_initial_code(item[0])
241
+ for item in syllables
242
+ if item and item[0]
243
+ ]
244
+ return "".join(code for code in codes if code)
245
+ initials = [
246
+ syllable_to_initial_letter(item[0])
247
+ for item in syllables
248
+ if item and item[0]
249
+ ]
250
+ return "".join(initial for initial in initials if initial)
251
+
252
+ def tokenize_chinese_span(value: str) -> list[str]:
253
+ tokens = []
254
+ words = jieba.cut(value, cut_all=False) if self.use_jieba else value
255
+ for word in words:
256
+ word = word.strip()
257
+ if word and CHINESE_SPAN_RE.search(word):
258
+ token = convert_word(word)
259
+ if token:
260
+ tokens.append(token)
261
+ return tokens
262
+
263
+ tokens = []
264
+ for part in TOKEN_RE.findall(normalize_text(text)):
265
+ if part.startswith("<") and part.endswith(">"):
266
+ tokens.append(part)
267
+ elif CHINESE_SPAN_RE.fullmatch(part):
268
+ tokens.extend(tokenize_chinese_span(part))
269
+ elif part in PUNCTUATION:
270
+ tokens.append(part)
271
+ elif LATIN_ALNUM_RE.fullmatch(part):
272
+ tokens.append(latin_token_to_model_token(part))
273
+ elif part.isdigit():
274
+ tokens.append("<NUM>")
275
+ elif should_preserve_fallback_token(part):
276
+ tokens.append(part.lower())
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
427
+ text = self._preprocess_tokenizer_input(text)
428
+ text_pair = self._preprocess_tokenizer_input(text_pair)
429
+ if text_pair is None:
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:
473
+ return self.sp_model.get_piece_size()
474
+
475
+ def get_vocab(self) -> dict[str, int]:
476
+ vocab = {self.sp_model.id_to_piece(i): i for i in range(self.vocab_size)}
477
+ vocab.update(self.added_tokens_encoder)
478
+ return vocab
479
+
480
+ def _tokenize(self, text: str) -> list[str]:
481
+ text = self._preprocess_raw_text(text)
482
+ return self.sp_model.encode(text, out_type=str)
483
+
484
+ def _convert_token_to_id(self, token: str) -> int:
485
+ return self.sp_model.piece_to_id(token)
486
+
487
+ def _convert_id_to_token(self, index: int) -> str:
488
+ return self.sp_model.id_to_piece(index)
489
+
490
+ def convert_tokens_to_string(self, tokens: list[str]) -> str:
491
+ return self.sp_model.decode(tokens)
492
+
493
+ def build_inputs_with_special_tokens(
494
+ self,
495
+ token_ids_0: list[int],
496
+ token_ids_1: list[int] | None = None,
497
+ ) -> list[int]:
498
+ output = list(token_ids_0)
499
+ if self.add_bos_token and self.bos_token_id is not None:
500
+ output = [self.bos_token_id] + output
501
+ if self.add_eos_token and self.eos_token_id is not None:
502
+ output = output + [self.eos_token_id]
503
+ if token_ids_1 is not None:
504
+ output += list(token_ids_1)
505
+ if self.add_eos_token and self.eos_token_id is not None:
506
+ output.append(self.eos_token_id)
507
+ return output
508
+
509
+ def get_special_tokens_mask(
510
+ self,
511
+ token_ids_0: list[int],
512
+ token_ids_1: list[int] | None = None,
513
+ already_has_special_tokens: bool = False,
514
+ ) -> list[int]:
515
+ if already_has_special_tokens:
516
+ special_ids = set(self.all_special_ids)
517
+ return [1 if token_id in special_ids else 0 for token_id in token_ids_0]
518
+
519
+ mask = [0] * len(token_ids_0)
520
+ if self.add_bos_token and self.bos_token_id is not None:
521
+ mask = [1] + mask
522
+ if self.add_eos_token and self.eos_token_id is not None:
523
+ mask = mask + [1]
524
+ if token_ids_1 is not None:
525
+ mask += [0] * len(token_ids_1)
526
+ if self.add_eos_token and self.eos_token_id is not None:
527
+ mask.append(1)
528
+ return mask
529
+
530
+ def create_token_type_ids_from_sequences(
531
+ self,
532
+ token_ids_0: list[int],
533
+ token_ids_1: list[int] | None = None,
534
+ ) -> list[int]:
535
+ return [0] * len(self.build_inputs_with_special_tokens(token_ids_0, token_ids_1))
536
+
537
+ def save_vocabulary(self, save_directory: str, filename_prefix: str | None = None) -> tuple[str]:
538
+ output_name = "tokenizer.model"
539
+ if filename_prefix:
540
+ output_name = f"{filename_prefix}-{output_name}"
541
+ output_path = Path(save_directory) / output_name
542
+ if Path(self.vocab_file).resolve() != output_path.resolve():
543
+ shutil.copyfile(self.vocab_file, output_path)
544
+ return (str(output_path),)
545
+
546
+
547
+ class EncodedMandarinTokenizer(PinyinCodeTokenizer):
548
+ """Tokenizer wrapper that hides Hanzi-to-encoded-Mandarin preprocessing."""
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:403f1d74ffa4294902d2c857b80eae0d8a66411acd4ff0c66d54ad20be6ab0e2
3
+ size 134706152
modeling_pinyin_code.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transformers-compatible implementation of the pinyin-code causal LM."""
2
+
3
+ from __future__ import annotations
4
+
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
+
14
+
15
+ class CausalSelfAttention(nn.Module):
16
+ """Multi-head masked self-attention matching the original training module."""
17
+
18
+ def __init__(self, config: PinyinCodeConfig) -> None:
19
+ super().__init__()
20
+ if config.n_embd % config.n_head != 0:
21
+ raise ValueError("n_embd must be divisible by n_head")
22
+
23
+ self.n_head = config.n_head
24
+ self.head_dim = config.n_embd // config.n_head
25
+ self.dropout_p = config.dropout
26
+ self.qkv = nn.Linear(config.n_embd, 3 * config.n_embd)
27
+ self.proj = nn.Linear(config.n_embd, config.n_embd)
28
+ self.resid_dropout = nn.Dropout(config.dropout)
29
+
30
+ def forward(self, x: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
31
+ batch_size, seq_len, embd = x.shape
32
+ q, k, v = self.qkv(x).split(embd, dim=2)
33
+
34
+ q = q.view(batch_size, seq_len, self.n_head, self.head_dim).transpose(1, 2)
35
+ k = k.view(batch_size, seq_len, self.n_head, self.head_dim).transpose(1, 2)
36
+ v = v.view(batch_size, seq_len, self.n_head, self.head_dim).transpose(1, 2)
37
+
38
+ dropout_p = self.dropout_p if self.training else 0.0
39
+ if attention_mask is not None:
40
+ causal_mask = torch.ones(
41
+ seq_len,
42
+ seq_len,
43
+ device=x.device,
44
+ dtype=torch.bool,
45
+ ).tril()
46
+ key_mask = attention_mask[:, None, None, :seq_len].to(dtype=torch.bool)
47
+ attn_mask = causal_mask.view(1, 1, seq_len, seq_len) & key_mask
48
+ y = F.scaled_dot_product_attention(
49
+ q,
50
+ k,
51
+ v,
52
+ attn_mask=attn_mask,
53
+ dropout_p=dropout_p,
54
+ is_causal=False,
55
+ )
56
+ else:
57
+ y = F.scaled_dot_product_attention(
58
+ q,
59
+ k,
60
+ v,
61
+ dropout_p=dropout_p,
62
+ is_causal=True,
63
+ )
64
+
65
+ y = y.transpose(1, 2).contiguous().view(batch_size, seq_len, embd)
66
+ return self.resid_dropout(self.proj(y))
67
+
68
+
69
+ class FeedForward(nn.Module):
70
+ """Transformer MLP block."""
71
+
72
+ def __init__(self, config: PinyinCodeConfig) -> None:
73
+ super().__init__()
74
+ self.net = nn.Sequential(
75
+ nn.Linear(config.n_embd, 4 * config.n_embd),
76
+ nn.GELU(),
77
+ nn.Linear(4 * config.n_embd, config.n_embd),
78
+ nn.Dropout(config.dropout),
79
+ )
80
+
81
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
82
+ return self.net(x)
83
+
84
+
85
+ class TransformerBlock(nn.Module):
86
+ """Pre-norm Transformer block."""
87
+
88
+ def __init__(self, config: PinyinCodeConfig) -> None:
89
+ super().__init__()
90
+ self.ln_1 = nn.LayerNorm(config.n_embd)
91
+ self.attn = CausalSelfAttention(config)
92
+ self.ln_2 = nn.LayerNorm(config.n_embd)
93
+ self.mlp = FeedForward(config)
94
+
95
+ def forward(self, x: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
96
+ x = x + self.attn(self.ln_1(x), attention_mask=attention_mask)
97
+ x = x + self.mlp(self.ln_2(x))
98
+ return x
99
+
100
+
101
+ class PinyinCodePreTrainedModel(PreTrainedModel):
102
+ """Base class for pinyin-code Transformers models."""
103
+
104
+ config_class = PinyinCodeConfig
105
+ base_model_prefix = "pinyin_code"
106
+ supports_gradient_checkpointing = False
107
+
108
+ def _init_weights(self, module: nn.Module) -> None:
109
+ if isinstance(module, nn.Linear):
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
223
+
224
+ def prepare_inputs_for_generation(
225
+ self,
226
+ input_ids: torch.Tensor,
227
+ past_key_values=None,
228
+ attention_mask: torch.Tensor | None = None,
229
+ **kwargs,
230
+ ) -> dict:
231
+ if input_ids.shape[1] > self.config.block_size:
232
+ input_ids = input_ids[:, -self.config.block_size :]
233
+ if attention_mask is not None:
234
+ attention_mask = attention_mask[:, -self.config.block_size :]
235
+ position_ids = None
236
+ if attention_mask is not None:
237
+ position_ids = attention_mask.long().cumsum(dim=-1) - 1
238
+ position_ids = position_ids.clamp_min(0)
239
+ return {
240
+ "input_ids": input_ids,
241
+ "attention_mask": attention_mask,
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,
262
+ inputs_embeds=inputs_embeds,
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
+ )
preprocessing/__init__.py ADDED
File without changes
preprocessing/preprocess.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
special_tokens_map.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<s>",
3
+ "eos_token": "</s>",
4
+ "pad_token": "<pad>",
5
+ "unk_token": "<unk>"
6
+ }
tokenization_pinyin_code.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SentencePiece tokenizer wrapper for pinyin-code Transformers models."""
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]")
18
+ CHINESE_SPAN_RE = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
19
+ PINYIN_CODE_TOKEN_RE = re.compile(
20
+ r"(?<![A-Za-z0-9])[A-Za-z]\d(?:[A-Za-z]\d)*(?![A-Za-z0-9])"
21
+ )
22
+ SPECIAL_MARKER_RE = re.compile(r"<[A-Z_]+>")
23
+ PUNCTUATION = set(
24
+ "\u3002\uff0c\u3001\uff1f\uff01\uff1a\uff1b.,?!:;()[]{}<>\u300a\u300b"
25
+ "\u3010\u3011\u201c\u201d\"'\u2018\u2019\u300c\u300d\u300e\u300f"
26
+ "\u2014-~\u2026/\\"
27
+ )
28
+ LATIN_LETTER = (
29
+ r"A-Za-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff"
30
+ r"\u0100-\u017f\u0180-\u024f\u0250-\u02af"
31
+ )
32
+ LATIN_ALNUM_PATTERN = (
33
+ rf"(?:[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
34
+ rf"(?:[-_][{LATIN_LETTER}0-9]+)*|"
35
+ rf"[0-9]+[{LATIN_LETTER}][{LATIN_LETTER}0-9]*"
36
+ rf"(?:[-_][{LATIN_LETTER}0-9]+)*)"
37
+ )
38
+ LATIN_ALNUM_RE = re.compile(LATIN_ALNUM_PATTERN)
39
+ URL_RE = re.compile(r"\b(?:https?://\S*|www\.\S+)", flags=re.I)
40
+ DISCARDED_UNICODE_CATEGORIES = {"Cc", "Cf", "Co", "Cs", "Cn"}
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
+ LABELS = {
48
+ "\u9898\u5e72": "<QUESTION>",
49
+ "\u9009\u9879": "<OPTIONS>",
50
+ "\u7b54\u6848": "<ANSWER>",
51
+ "\u89e3\u6790": "<EXPLANATION>",
52
+ }
53
+ PINYIN_FORMAT_ALIASES = {
54
+ "code": "pinyin-code",
55
+ "codes": "pinyin-code",
56
+ "pinyin-code": "pinyin-code",
57
+ "initial": "pinyin-initial",
58
+ "initials": "pinyin-initial",
59
+ "pinyin-initial": "pinyin-initial",
60
+ "hanzi": "hanzi",
61
+ }
62
+
63
+
64
+ def latin_token_to_model_token(token: str) -> str:
65
+ upper = token.upper()
66
+ return upper if upper in {"A", "B", "C", "D"} else token.lower()
67
+
68
+
69
+ def should_preserve_fallback_token(token: str) -> bool:
70
+ if token == "\ufffd":
71
+ return False
72
+ for char in token:
73
+ category = unicodedata.category(char)
74
+ if category in DISCARDED_UNICODE_CATEGORIES:
75
+ return False
76
+ if category[0] not in {"L", "P", "S"}:
77
+ return False
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"}
85
+ model_input_names = ["input_ids", "attention_mask"]
86
+
87
+ def __init__(
88
+ self,
89
+ vocab_file: str,
90
+ add_bos_token: bool = False,
91
+ add_eos_token: bool = False,
92
+ transliteration: str = "pinyin-code",
93
+ pinyin_format: str | None = None,
94
+ use_jieba: bool = True,
95
+ jieba: bool | None = None,
96
+ **kwargs,
97
+ ) -> None:
98
+ self.vocab_file = vocab_file
99
+ self.sp_model = spm.SentencePieceProcessor(model_file=vocab_file)
100
+ self.add_bos_token = add_bos_token
101
+ self.add_eos_token = add_eos_token
102
+ self.transliteration = self._normalize_transliteration(
103
+ pinyin_format or transliteration
104
+ )
105
+ self.use_jieba = use_jieba if jieba is None else jieba
106
+
107
+ kwargs.setdefault("unk_token", self._piece_or_none(self.sp_model.unk_id()))
108
+ kwargs.setdefault("bos_token", self._piece_or_none(self.sp_model.bos_id()))
109
+ kwargs.setdefault("eos_token", self._piece_or_none(self.sp_model.eos_id()))
110
+ kwargs.setdefault("pad_token", self._piece_or_none(self.sp_model.pad_id()))
111
+ kwargs.setdefault("transliteration", self.transliteration)
112
+ kwargs.setdefault("pinyin_format", self.transliteration)
113
+ kwargs.setdefault("use_jieba", self.use_jieba)
114
+ kwargs.setdefault("jieba", self.use_jieba)
115
+ super().__init__(**kwargs)
116
+
117
+ def _normalize_transliteration(self, value: str) -> str:
118
+ normalized = PINYIN_FORMAT_ALIASES.get(value.lower())
119
+ if normalized is None:
120
+ allowed = ", ".join(sorted(set(PINYIN_FORMAT_ALIASES.values())))
121
+ raise ValueError(f"Unsupported transliteration {value!r}; choose from {allowed}")
122
+ return normalized
123
+
124
+ def _piece_or_none(self, token_id: int) -> str | None:
125
+ if token_id is None or token_id < 0:
126
+ return None
127
+ return self.sp_model.id_to_piece(token_id)
128
+
129
+ def _looks_preprocessed(self, text: str) -> bool:
130
+ if SPECIAL_MARKER_RE.search(text):
131
+ return True
132
+ if self.transliteration == "pinyin-code" and PINYIN_CODE_TOKEN_RE.search(text):
133
+ return True
134
+ return False
135
+
136
+ def _preprocess_raw_text(self, text: str) -> str:
137
+ if not CHINESE_RE.search(text) and self._looks_preprocessed(text):
138
+ return text
139
+ try:
140
+ from preprocessing.preprocess import (
141
+ hanzi_to_encoded,
142
+ process_text,
143
+ require_dependencies,
144
+ )
145
+ except ImportError:
146
+ return self._fallback_process_text(text)
147
+
148
+ require_dependencies()
149
+ if self.transliteration == "pinyin-code":
150
+ return hanzi_to_encoded(text, self.use_jieba)
151
+ return process_text(text, self.transliteration, self.use_jieba)
152
+
153
+ def _fallback_process_text(self, text: str) -> str:
154
+ if self.use_jieba:
155
+ try:
156
+ import jieba
157
+ except ImportError as exc:
158
+ raise ImportError(
159
+ "Tokenizing raw Mandarin benchmark text with jieba segmentation "
160
+ "requires jieba. Install the model dependencies before running "
161
+ "lm_eval."
162
+ ) from exc
163
+
164
+ jieba.setLogLevel(logging.WARNING)
165
+ else:
166
+ jieba = None
167
+
168
+ if self.transliteration != "hanzi":
169
+ try:
170
+ from pypinyin import Style, pinyin
171
+ except ImportError as exc:
172
+ raise ImportError(
173
+ "Tokenizing raw Mandarin benchmark text as pinyin requires pypinyin. "
174
+ "Install the model dependencies before running lm_eval."
175
+ ) from exc
176
+
177
+
178
+ def normalize_text(value: str) -> str:
179
+ value = unicodedata.normalize("NFKC", value)
180
+ value = URL_RE.sub(" <URL> ", value)
181
+ value = re.sub(r"\$\$.*?\$\$", " <MATH> ", value, flags=re.DOTALL)
182
+ value = re.sub(r"[\uff08(]\s*[\uff09)]", " <BLANK> ", value)
183
+ for label, marker in LABELS.items():
184
+ value = re.sub(rf"{label}\s*[:\uff1a]", f" {marker} ", value)
185
+ value = re.sub(
186
+ rf"(?<![{LATIN_LETTER}])yes(?![{LATIN_LETTER}])",
187
+ " <YES> ",
188
+ value,
189
+ flags=re.I,
190
+ )
191
+ value = re.sub(
192
+ rf"(?<![{LATIN_LETTER}])no(?![{LATIN_LETTER}])",
193
+ " <NO> ",
194
+ value,
195
+ flags=re.I,
196
+ )
197
+ value = re.sub(
198
+ rf"(?<![{LATIN_LETTER}])[ABCD](?=\s*[:\uff1a.\uff0e\u3001\)])",
199
+ r" \g<0> ",
200
+ value,
201
+ )
202
+ value = re.sub(
203
+ rf"(?<![{LATIN_LETTER}0-9])[-+]?\d+(?:[.,]\d+)*(?:%|\uff05)?"
204
+ rf"(?![{LATIN_LETTER}0-9])",
205
+ " <NUM> ",
206
+ value,
207
+ )
208
+ value = value.replace("\uff08", "(").replace("\uff09", ")")
209
+ return re.sub(r"\s+", " ", value).strip()
210
+
211
+ def split_tone3_syllable(syllable: str) -> tuple[str, int]:
212
+ match = re.fullmatch(r"([a-z\u00fcv]+)([1-5]?)", syllable.lower())
213
+ if not match:
214
+ return syllable, 5
215
+ plain, tone = match.groups()
216
+ return plain, int(tone or "5")
217
+
218
+ def length_digit_offset(syllable: str) -> int:
219
+ return min(max(len(syllable), 1), 5) - 1
220
+
221
+ def syllable_to_initial_code(syllable: str) -> str:
222
+ plain, tone = split_tone3_syllable(syllable)
223
+ if not plain:
224
+ return ""
225
+ tone_offset = 5 if tone in {3, 4, 5} else 0
226
+ digit = tone_offset + length_digit_offset(plain)
227
+ initial = plain[0].upper() if tone in {1, 3, 5} else plain[0].lower()
228
+ return f"{initial}{digit}"
229
+
230
+ def syllable_to_initial_letter(syllable: str) -> str:
231
+ plain, _ = split_tone3_syllable(syllable)
232
+ return plain[:1].lower()
233
+
234
+ def convert_word(word: str) -> str:
235
+ if self.transliteration == "hanzi":
236
+ return word
237
+ syllables = pinyin(word, style=Style.TONE3, heteronym=False, errors="ignore")
238
+ if self.transliteration == "pinyin-code":
239
+ codes = [
240
+ syllable_to_initial_code(item[0])
241
+ for item in syllables
242
+ if item and item[0]
243
+ ]
244
+ return "".join(code for code in codes if code)
245
+ initials = [
246
+ syllable_to_initial_letter(item[0])
247
+ for item in syllables
248
+ if item and item[0]
249
+ ]
250
+ return "".join(initial for initial in initials if initial)
251
+
252
+ def tokenize_chinese_span(value: str) -> list[str]:
253
+ tokens = []
254
+ words = jieba.cut(value, cut_all=False) if self.use_jieba else value
255
+ for word in words:
256
+ word = word.strip()
257
+ if word and CHINESE_SPAN_RE.search(word):
258
+ token = convert_word(word)
259
+ if token:
260
+ tokens.append(token)
261
+ return tokens
262
+
263
+ tokens = []
264
+ for part in TOKEN_RE.findall(normalize_text(text)):
265
+ if part.startswith("<") and part.endswith(">"):
266
+ tokens.append(part)
267
+ elif CHINESE_SPAN_RE.fullmatch(part):
268
+ tokens.extend(tokenize_chinese_span(part))
269
+ elif part in PUNCTUATION:
270
+ tokens.append(part)
271
+ elif LATIN_ALNUM_RE.fullmatch(part):
272
+ tokens.append(latin_token_to_model_token(part))
273
+ elif part.isdigit():
274
+ tokens.append("<NUM>")
275
+ elif should_preserve_fallback_token(part):
276
+ tokens.append(part.lower())
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
427
+ text = self._preprocess_tokenizer_input(text)
428
+ text_pair = self._preprocess_tokenizer_input(text_pair)
429
+ if text_pair is None:
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:
473
+ return self.sp_model.get_piece_size()
474
+
475
+ def get_vocab(self) -> dict[str, int]:
476
+ vocab = {self.sp_model.id_to_piece(i): i for i in range(self.vocab_size)}
477
+ vocab.update(self.added_tokens_encoder)
478
+ return vocab
479
+
480
+ def _tokenize(self, text: str) -> list[str]:
481
+ text = self._preprocess_raw_text(text)
482
+ return self.sp_model.encode(text, out_type=str)
483
+
484
+ def _convert_token_to_id(self, token: str) -> int:
485
+ return self.sp_model.piece_to_id(token)
486
+
487
+ def _convert_id_to_token(self, index: int) -> str:
488
+ return self.sp_model.id_to_piece(index)
489
+
490
+ def convert_tokens_to_string(self, tokens: list[str]) -> str:
491
+ return self.sp_model.decode(tokens)
492
+
493
+ def build_inputs_with_special_tokens(
494
+ self,
495
+ token_ids_0: list[int],
496
+ token_ids_1: list[int] | None = None,
497
+ ) -> list[int]:
498
+ output = list(token_ids_0)
499
+ if self.add_bos_token and self.bos_token_id is not None:
500
+ output = [self.bos_token_id] + output
501
+ if self.add_eos_token and self.eos_token_id is not None:
502
+ output = output + [self.eos_token_id]
503
+ if token_ids_1 is not None:
504
+ output += list(token_ids_1)
505
+ if self.add_eos_token and self.eos_token_id is not None:
506
+ output.append(self.eos_token_id)
507
+ return output
508
+
509
+ def get_special_tokens_mask(
510
+ self,
511
+ token_ids_0: list[int],
512
+ token_ids_1: list[int] | None = None,
513
+ already_has_special_tokens: bool = False,
514
+ ) -> list[int]:
515
+ if already_has_special_tokens:
516
+ special_ids = set(self.all_special_ids)
517
+ return [1 if token_id in special_ids else 0 for token_id in token_ids_0]
518
+
519
+ mask = [0] * len(token_ids_0)
520
+ if self.add_bos_token and self.bos_token_id is not None:
521
+ mask = [1] + mask
522
+ if self.add_eos_token and self.eos_token_id is not None:
523
+ mask = mask + [1]
524
+ if token_ids_1 is not None:
525
+ mask += [0] * len(token_ids_1)
526
+ if self.add_eos_token and self.eos_token_id is not None:
527
+ mask.append(1)
528
+ return mask
529
+
530
+ def create_token_type_ids_from_sequences(
531
+ self,
532
+ token_ids_0: list[int],
533
+ token_ids_1: list[int] | None = None,
534
+ ) -> list[int]:
535
+ return [0] * len(self.build_inputs_with_special_tokens(token_ids_0, token_ids_1))
536
+
537
+ def save_vocabulary(self, save_directory: str, filename_prefix: str | None = None) -> tuple[str]:
538
+ output_name = "tokenizer.model"
539
+ if filename_prefix:
540
+ output_name = f"{filename_prefix}-{output_name}"
541
+ output_path = Path(save_directory) / output_name
542
+ if Path(self.vocab_file).resolve() != output_path.resolve():
543
+ shutil.copyfile(self.vocab_file, output_path)
544
+ return (str(output_path),)
545
+
546
+
547
+ class EncodedMandarinTokenizer(PinyinCodeTokenizer):
548
+ """Tokenizer wrapper that hides Hanzi-to-encoded-Mandarin preprocessing."""
tokenizer.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:60cf469adc184103e3c73309b971a667d4ec04211cf33527e0341716ccd3f5ee
3
+ size 285315
tokenizer_config.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
training_metadata.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "epoch": 1,
3
+ "evaluation_backend": "causal",
4
+ "global_step": 119,
5
+ "jieba": true,
6
+ "source_checkpoint": "models\\full_chinese_gpu3.2-dpo\\best.pt",
7
+ "transliteration": "pinyin-code",
8
+ "use_jieba": true,
9
+ "validation_loss": 0.016063788817564033
10
+ }