tf-bao commited on
Commit
3075644
·
verified ·
1 Parent(s): a5fdccd

Update inference code and model card

Browse files
Files changed (6) hide show
  1. README.md +41 -39
  2. csc_config.json +6 -29
  3. csc_model.py +48 -58
  4. example_correct.py +4 -3
  5. model.py +11 -114
  6. tokenizer.py +64 -0
README.md CHANGED
@@ -3,62 +3,64 @@ license: apache-2.0
3
  language:
4
  - zh
5
  tags:
 
6
  - chinese
 
7
  - spelling-correction
8
- - csc
9
- - bert
10
- - text-correction
11
  library_name: pytorch
12
  ---
13
 
14
  # BERTc-165M-CSC
15
 
16
- BERTc-165M-CSC is a Chinese spelling correction model fine-tuned from
17
- `Ismantic/BERTc-165M`. It uses a Modern BERTc encoder with two heads:
18
 
19
- - correction head: tied to the input embedding matrix
20
- - detection head: binary error detection
21
 
22
- ## Metrics
23
 
24
- SIGHAN-15 sentence-level evaluation using the pycorrector-style 707-sample protocol:
 
 
25
 
26
- - Sentence F1: **0.8308**
27
- - Accuracy: **0.8416**
28
- - Precision: **0.9516**
29
- - Recall: **0.7373**
30
- - TP/FP/FN/TN: 275 / 14 / 98 / 320
31
 
32
- Training recipe:
 
33
 
34
- - backbone: `Ismantic/BERTc-165M`
35
- - epochs: 5
36
- - batch size: 64
37
- - learning rate: 5e-5
38
- - warmup ratio: 0.1
39
- - detection loss weight: 0.3
40
- - inference threshold: 0.7
41
- - max length: 128
42
 
43
- ## Files
 
44
 
45
- - `model.safetensors`: CSC state dict. `cor_head.weight` is intentionally omitted and tied to `bert.embed.weight` by `csc_model.py`.
46
- - `config.json`: BERTc backbone architecture.
47
- - `csc_config.json`: task and metric metadata.
48
- - `model.py`: Modern BERTc implementation.
49
- - `csc_model.py`: CSC wrapper and batch correction helper.
50
- - `piece.model`: tokenizer model; load with `piece_tokenizer` using `cn_dict="no"`.
51
 
52
- ## Usage
53
 
54
- ```python
55
- from csc_model import BERTcForCSC, PieceCharTokenizer
56
 
57
- tok = PieceCharTokenizer(".")
58
- model = BERTcForCSC.from_pretrained(".")
59
- texts = ["少先队员因该为老人让坐。"]
60
- print(model.correct(texts, tok, threshold=0.7))
 
 
 
61
  ```
62
 
63
- This is not a generative model. It performs same-length character replacement for
64
- Chinese spelling correction.
 
 
 
 
 
 
 
 
 
 
 
 
3
  language:
4
  - zh
5
  tags:
6
+ - bert
7
  - chinese
8
+ - text2text-generation
9
  - spelling-correction
10
+ pipeline_tag: fill-mask
 
 
11
  library_name: pytorch
12
  ---
13
 
14
  # BERTc-165M-CSC
15
 
16
+ 中文拼写纠错。基于 [BERTc-165M](https://huggingface.co/Ismantic/BERTc-165M) 微调。
 
17
 
18
+ 双头:cor 逐位置预测正确的字(权重与词嵌入绑定),det 判断该位置有没有错(focal loss)。**只做等长替换**,不处理多字少字。
 
19
 
20
+ ## 指标
21
 
22
+ | 指标 | |
23
+ |---|---|
24
+ | 句级 F1 | 0.8308 |
25
 
26
+ ## 训练
 
 
 
 
27
 
28
+ - 配方:5 epoch,batch 64,lr 5e-5,其余同 315M-CSC
29
+ - 数据:同 315M-CSC
30
 
31
+ ## 用法
 
 
 
 
 
 
 
32
 
33
+ ```python
34
+ from csc_model import BERTcForCSC
35
 
36
+ model = BERTcForCSC.from_pretrained(".")
37
+ print(model.correct("我今天很稿兴")) # 我今天很高兴
38
+ ```
 
 
 
39
 
40
+ ## 阈值
41
 
42
+ `correct(..., threshold=0.7)`:纠错置信度低于阈值就保留原字。调低提召回、
43
+ 调高提精确率。0.7 是与 MacBERT4CSC 对齐的默认值,报告的指标都基于它。
44
 
45
+ ## Tokenizer
46
+
47
+ 字级 SentencePiece,词表 12536(pad=12531,mask=12535)。**必须用 `dict="no"`
48
+ 加载**(字模式,不挂分词词典)——挂了词典编码结果会跟训练时不一致,而且不报错。
49
+
50
+ ```bash
51
+ pip install git+https://github.com/Ismantic/PieceTokenizer
52
  ```
53
 
54
+ ## 文件
55
+
56
+ | 文件 | 说明 |
57
+ |---|---|
58
+ | `model.safetensors` | 骨干 + 双头 |
59
+ | `csc_model.py` | 推理入口 `BERTcForCSC` |
60
+ | `model.py` | 骨干定义 |
61
+ | `tokenizer.py` | 字级 tokenizer |
62
+ | `example_correct.py` | 示例 |
63
+
64
+ ## 许可
65
+
66
+ Apache-2.0。训练语料各自的许可见对应数据集卡。
csc_config.json CHANGED
@@ -1,34 +1,11 @@
1
  {
 
2
  "base_model": "Ismantic/BERTc-165M",
3
- "task": "Chinese Spelling Correction",
4
- "threshold": 0.7,
5
- "max_len": 128,
6
- "epoch": 5,
7
  "metrics": {
8
- "acc": 0.8415841584158416,
9
- "precision": 0.9515570934256056,
10
- "recall": 0.7372654155495979,
11
- "f1": 0.8308157099697886,
12
- "TP": 275,
13
- "FP": 14,
14
- "FN": 98,
15
- "TN": 320,
16
- "n": 707
17
  },
18
- "args": {
19
- "backbone_path": "/home/tfbao/Shiyu/BERTc/pretrain/modern_bertc/output_v4_mid/checkpoint-8500",
20
- "tokenizer_dir": "/home/tfbao/Shiyu/BERTc/pretrain/modern_bertc/tokenizer",
21
- "train_pkl": "/home/tfbao/Shiyu/BERTc/csc/data/sighan_wang271k_pairs.pkl",
22
- "test_tsv": "/home/tfbao/Shiyu/BERTc/csc/data/test/sighan2015_test_official.tsv",
23
- "output_dir": "/home/tfbao/Shiyu/BERTc/csc/output_v4_mid_csc_v3_tied",
24
- "epochs": 5,
25
- "batch_size": 64,
26
- "lr": 5e-05,
27
- "warmup_ratio": 0.1,
28
- "max_len": 128,
29
- "det_weight": 0.3,
30
- "threshold": 0.7,
31
- "log_every": 200
32
- },
33
- "source_checkpoint": "finetune/sota/sota_csc_v4mid_5ep_best.pt"
34
  }
 
1
  {
2
+ "task": "csc",
3
  "base_model": "Ismantic/BERTc-165M",
 
 
 
 
4
  "metrics": {
5
+ "句级 F1": "0.8308"
 
 
 
 
 
 
 
 
6
  },
7
+ "recipe": "5 epoch,batch 64,lr 5e-5,其余同 315M-CSC",
8
+ "data": "同 315M-CSC",
9
+ "source_checkpoint": "finetune/sota/sota_csc_v4mid_5ep_best.pt",
10
+ "epoch": 5
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
csc_model.py CHANGED
@@ -1,39 +1,30 @@
 
 
 
 
 
 
 
 
 
1
  import json
2
  from pathlib import Path
3
 
4
  import torch
5
  import torch.nn as nn
6
- import torch.nn.functional as F
7
  from safetensors.torch import load_file
8
 
9
  from model import ModernBertConfig, ModernBertModel
 
10
 
11
 
12
- class PieceCharTokenizer:
13
- def __init__(self, model_dir):
14
- import piece_tokenizer as pt
15
- model_dir = Path(model_dir)
16
- self._tok = pt.Tokenizer()
17
- self._tok.load(str(model_dir / "piece.model"), cn_dict="no")
18
- mask_path = model_dir / "mask_token_id.txt"
19
- self.mask_token_id = int(mask_path.read_text().strip()) if mask_path.exists() else self._tok.vocab_size()
20
- self.vocab_size = self._tok.vocab_size() + 1
21
- self.pad_token_id = self._tok.piece_to_id("<pad>")
22
- self.unk_token_id = 0
23
- self.cache = {}
24
- self.inv_cache = {}
25
-
26
- def char_to_id(self, char):
27
- if char in self.cache:
28
- return self.cache[char]
29
- ids = self._tok.encode_as_ids(char)
30
- tid = ids[0] if ids else self.unk_token_id
31
- self.cache[char] = tid
32
- self.inv_cache.setdefault(tid, char)
33
- return tid
34
 
 
 
 
35
 
36
- class BERTcForCSC(nn.Module):
37
  def __init__(self, config):
38
  super().__init__()
39
  self.config = config
@@ -43,55 +34,54 @@ class BERTcForCSC(nn.Module):
43
  self.det_head = nn.Linear(config.hidden_size, 1)
44
 
45
  @classmethod
46
- def from_pretrained(cls, model_dir, map_location="cpu"):
47
  model_dir = Path(model_dir)
48
  cfg = ModernBertConfig(**json.loads((model_dir / "config.json").read_text()))
49
  model = cls(cfg)
50
- state = load_file(str(model_dir / "model.safetensors"), device=str(map_location))
 
51
  missing, unexpected = model.load_state_dict(state, strict=False)
52
- allowed_missing = {"cor_head.weight"}
53
- if set(missing) != allowed_missing or unexpected:
54
- raise RuntimeError(f"Bad state dict: missing={missing}, unexpected={unexpected}")
55
  model.cor_head.weight = model.bert.embed.weight
56
  model.eval()
57
  return model
58
 
59
  def forward(self, input_ids, attention_mask=None):
60
  h = self.bert(input_ids=input_ids, attention_mask=attention_mask)
61
- cor_logits = self.cor_head(h)
62
- det_logits = self.det_head(h).squeeze(-1)
63
- return cor_logits, det_logits
64
 
65
  @torch.no_grad()
66
- def correct(self, texts, tokenizer, threshold=0.7, max_len=128, device=None):
67
- if isinstance(texts, str):
68
- single = True
69
- texts = [texts]
70
- else:
71
- single = False
 
 
 
 
 
 
72
  device = device or next(self.parameters()).device
73
  self.eval()
74
- lengths = [min(len(t), max_len) for t in texts]
75
- max_l = max(lengths) if lengths else 0
76
- input_ids = torch.full((len(texts), max_l), tokenizer.pad_token_id, dtype=torch.long, device=device)
77
- attn = torch.zeros((len(texts), max_l), dtype=torch.long, device=device)
78
- for i, text in enumerate(texts):
79
- ids = [tokenizer.char_to_id(c) for c in text[:lengths[i]]]
80
- if ids:
81
- input_ids[i, :len(ids)] = torch.tensor(ids, dtype=torch.long, device=device)
82
- attn[i, :len(ids)] = 1
83
  cor_logits, _ = self(input_ids, attn)
84
- probs = F.softmax(cor_logits, dim=-1)
85
- top_probs, top_ids = probs.max(dim=-1)
 
86
  out = []
87
  for i, text in enumerate(texts):
88
- chars = list(text[:lengths[i]])
89
- pred = []
90
- for j, orig in enumerate(chars):
91
- tid = int(top_ids[i, j].item())
92
- prob = float(top_probs[i, j].item())
93
- pred.append(tokenizer.inv_cache.get(tid, orig) if prob >= threshold else orig)
94
- if len(text) > lengths[i]:
95
- pred.extend(list(text[lengths[i]:]))
96
- out.append("".join(pred))
97
  return out[0] if single else out
 
1
+ """BERTc-CSC 推理:中文拼写纠错。
2
+
3
+ 这份代码随模型一起发到 HF,只依赖同目录的 model.py / tokenizer.py
4
+ 和 torch + safetensors。
5
+
6
+ from csc_model import BERTcForCSC
7
+ model = BERTcForCSC.from_pretrained(".")
8
+ print(model.correct("我今天很稿兴")) # 我今天很高兴
9
+ """
10
  import json
11
  from pathlib import Path
12
 
13
  import torch
14
  import torch.nn as nn
 
15
  from safetensors.torch import load_file
16
 
17
  from model import ModernBertConfig, ModernBertModel
18
+ from tokenizer import PieceCharTokenizer
19
 
20
 
21
+ class BERTcForCSC(nn.Module):
22
+ """骨干 + 双头:cor 逐位置预测正确的字,det 判断该位置有没有错。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ cor_head 的权重与词嵌入绑定 —— 预训练的 MLM 头就是 logits = h @ embed.weightᵀ,
25
+ 换成独立的 Linear 会废掉这层对齐,纠错效果掉一大截。
26
+ """
27
 
 
28
  def __init__(self, config):
29
  super().__init__()
30
  self.config = config
 
34
  self.det_head = nn.Linear(config.hidden_size, 1)
35
 
36
  @classmethod
37
+ def from_pretrained(cls, model_dir=".", map_location="cpu"):
38
  model_dir = Path(model_dir)
39
  cfg = ModernBertConfig(**json.loads((model_dir / "config.json").read_text()))
40
  model = cls(cfg)
41
+ state = load_file(str(model_dir / "model.safetensors"),
42
+ device=str(map_location))
43
  missing, unexpected = model.load_state_dict(state, strict=False)
44
+ # cor_head.weight 跟 embed 共享,safetensors 不重复存,所以它一定"缺"
45
+ if set(missing) != {"cor_head.weight"} or unexpected:
46
+ raise RuntimeError(f"权重不匹配: {missing}, {unexpected}")
47
  model.cor_head.weight = model.bert.embed.weight
48
  model.eval()
49
  return model
50
 
51
  def forward(self, input_ids, attention_mask=None):
52
  h = self.bert(input_ids=input_ids, attention_mask=attention_mask)
53
+ return self.cor_head(h), self.det_head(h).squeeze(-1)
 
 
54
 
55
  @torch.no_grad()
56
+ def correct(self, texts, tokenizer=None, threshold=0.7, max_len=128,
57
+ device=None):
58
+ """纠错。返回修正后的句子(输入是单句就返回单句)。
59
+
60
+ threshold 是纠错置信度下限:softmax 概率低于它就保留原字。
61
+ 低置信度的"纠正"绝大多数是误伤,这个阈值是精确率的主要来源。
62
+ """
63
+ if tokenizer is None:
64
+ tokenizer = PieceCharTokenizer(Path(__file__).resolve().parent)
65
+ single = isinstance(texts, str)
66
+ texts = [texts] if single else list(texts)
67
+
68
  device = device or next(self.parameters()).device
69
  self.eval()
70
+ input_ids, attn, lengths = tokenizer.batch(texts, max_len, device)
71
+
 
 
 
 
 
 
 
72
  cor_logits, _ = self(input_ids, attn)
73
+ probs = torch.softmax(cor_logits.float(), dim=-1)
74
+ top_prob, top_id = probs.max(dim=-1)
75
+
76
  out = []
77
  for i, text in enumerate(texts):
78
+ n = lengths[i]
79
+ chars = list(text[:n])
80
+ for j in range(n):
81
+ if float(top_prob[i, j]) < threshold:
82
+ continue
83
+ c = tokenizer.id_to_char(int(top_id[i, j]))
84
+ if len(c) == 1: # 只接受单字替换,保证不改变长度
85
+ chars[j] = c
86
+ out.append("".join(chars) + text[n:]) # 超长部分原样接回
87
  return out[0] if single else out
example_correct.py CHANGED
@@ -1,5 +1,6 @@
1
- from csc_model import BERTcForCSC, PieceCharTokenizer
 
2
 
3
- tok = PieceCharTokenizer(".")
4
  model = BERTcForCSC.from_pretrained(".")
5
- print(model.correct(["少先队员因该为老人让坐。"], tok, threshold=0.7))
 
 
1
+ """BERTc-CSC:中文拼写纠错。"""
2
+ from csc_model import BERTcForCSC
3
 
 
4
  model = BERTcForCSC.from_pretrained(".")
5
+ for s in ["我今天很稿兴", "他的身体健康状况很不错,平时喜欢锻练"]:
6
+ print(f"{s} → {model.correct(s)}")
model.py CHANGED
@@ -1,7 +1,15 @@
1
- """Modern BERTc v3 — ModernBERT release-aligned + Cramming-style ScaledSinusoidal PE.
 
 
 
 
 
 
 
 
2
 
3
  主要按 release `modernbert-base-pretrain.yaml` 对齐(除 Alt Attn 和 PE):
4
- - 架构: 22L / 768H / 1152I (GLU) / 12 heads,head_dim=64
5
  - **ScaledSinusoidal 位置编码**(Hua et al. 2022 FLASH;Cramming 实测短 seq 比
6
  RoPE 更值:计算几乎免费,RoPE 收益被 5-10% 速度损失抵消)
7
  - GeGLU FFN(glu + gelu)
@@ -33,7 +41,7 @@
33
  head_bias (V,) : 12.5K
34
  total ≈ 130M
35
  """
36
- from dataclasses import dataclass, field
37
  from typing import Optional
38
  import math
39
 
@@ -334,114 +342,3 @@ class ModernBertForMLM(nn.Module):
334
  return sum(p.numel() for p in self.parameters() if p.requires_grad)
335
 
336
 
337
- # ============ Quick sanity test ============
338
-
339
- if __name__ == "__main__":
340
- print("=== Test 1: default config (22L/768H release-aligned) ===")
341
- cfg = ModernBertConfig()
342
- print(f"Config: V={cfg.vocab_size} H={cfg.hidden_size} L={cfg.num_hidden_layers} "
343
- f"head={cfg.num_attention_heads} d={cfg.head_dim} I={cfg.intermediate_size} "
344
- f"pe_theta={cfg.pe_theta} ln_eps={cfg.layer_norm_eps}")
345
- print(f"embed_norm={cfg.embed_norm} skip_first_prenorm={cfg.skip_first_prenorm} "
346
- f"final_norm={cfg.final_norm} init={cfg.init_method}")
347
- print(f"dropout: embed={cfg.embed_dropout} mlp={cfg.mlp_dropout} "
348
- f"attn_out={cfg.attn_out_dropout} attn_probs={cfg.attn_probs_dropout}")
349
- model = ModernBertForMLM(cfg)
350
- n = model.num_parameters()
351
- print(f"params: {n:,} = {n / 1e6:.1f}M")
352
-
353
- # forward smoke
354
- B, L = 2, 128
355
- ids = torch.randint(0, cfg.vocab_size, (B, L))
356
- mask = torch.ones(B, L, dtype=torch.long)
357
- labels = torch.randint(0, cfg.vocab_size, (B, L))
358
- out = model(ids, attention_mask=mask, labels=labels)
359
- print(f"logits: {out['logits'].shape} loss: {out['loss'].item():.4f}")
360
-
361
- # backward smoke
362
- out["loss"].backward()
363
- # 检查所有 trainable param 都有 grad
364
- no_grad = [n for n, p in model.named_parameters() if p.requires_grad and p.grad is None]
365
- if no_grad:
366
- print(f"ERROR: 缺 grad 的参数: {no_grad}")
367
- else:
368
- print("backward OK (所有参数都有 grad)")
369
-
370
- # 验证 skip_first_prenorm 生效
371
- assert model.bert.layers[0].skip_norm1, "layer[0].skip_norm1 应为 True"
372
- assert not model.bert.layers[1].skip_norm1, "layer[1].skip_norm1 应为 False"
373
- assert isinstance(model.bert.layers[0].norm1, nn.Identity), "layer[0].norm1 应为 Identity"
374
- assert isinstance(model.bert.layers[1].norm1, LayerNormNoBias), "layer[1].norm1 应为 LayerNormNoBias"
375
- print("skip_first_prenorm 配置正确")
376
-
377
- # 验证 embed_norm + final_norm
378
- assert isinstance(model.bert.embed_norm, LayerNormNoBias), "embed_norm 应为 LayerNormNoBias"
379
- assert isinstance(model.bert.final_norm, LayerNormNoBias), "final_norm 应为 LayerNormNoBias"
380
- print("embed_norm + final_norm 配置正确")
381
-
382
- # 验证 Megatron init
383
- L_layers = cfg.num_hidden_layers
384
- expected_scale = (2.0 * L_layers) ** -0.5
385
- # 一个未缩的初始值 std ≈ 0.02,缩后 std ≈ 0.02 * expected_scale
386
- o_std = model.bert.layers[5].attn.o.weight.std().item()
387
- expected_std = cfg.initializer_range * expected_scale
388
- # 允许 ±50% 容差(单 tensor std 估计有 noise)
389
- assert 0.5 * expected_std < o_std < 1.5 * expected_std, \
390
- f"Megatron init: attn.o.weight.std={o_std:.6f}, expected≈{expected_std:.6f}"
391
- print(f"Megatron init OK: attn.o.weight.std={o_std:.6f} ≈ {expected_std:.6f}")
392
-
393
- # 验证 no bias
394
- for name, p in model.named_parameters():
395
- if "bias" in name and name != "head_bias":
396
- print(f"ERROR: 不该有的 bias: {name}")
397
- print("Linear/Norm no-bias 配置正确")
398
-
399
- print("\n=== Test 1b: seg_ids → flex_attention 路径(cross-doc 隔离)===")
400
- if torch.cuda.is_available():
401
- # flex_attention 需要 CUDA
402
- model_cuda = ModernBertForMLM(cfg).cuda().to(torch.bfloat16)
403
- ids_c = ids.cuda()
404
- labels_c = labels.cuda()
405
- # 构造 seg_ids:chunk 内 0 0 0 ... 0 | 1 1 ... 1 (前半 doc0, 后半 doc1)
406
- seg_ids = torch.zeros(B, L, dtype=torch.int32, device="cuda")
407
- seg_ids[:, L // 2:] = 1
408
- out_flex = model_cuda(ids_c, seg_ids=seg_ids, labels=labels_c)
409
- print(f" flex_attention forward OK loss={out_flex['loss'].item():.4f} "
410
- f"logits={out_flex['logits'].shape} dtype={out_flex['logits'].dtype}")
411
- out_flex["loss"].backward()
412
- no_grad = [n for n, p in model_cuda.named_parameters() if p.requires_grad and p.grad is None]
413
- if no_grad:
414
- print(f" ERROR: 缺 grad: {no_grad[:5]}")
415
- else:
416
- print(f" flex_attention backward OK")
417
-
418
- # 验证 attention 真正隔离了:用相同 ids 但 seg_ids 不同,前半 token 应该输出不同
419
- # (因为 doc0 token 在 seg=0 时只 attend 同 seg=0;改成 seg=2 后 attend 不同的 kv 集)
420
- with torch.no_grad():
421
- seg_v1 = torch.zeros(B, L, dtype=torch.int32, device="cuda")
422
- seg_v2 = torch.zeros(B, L, dtype=torch.int32, device="cuda")
423
- seg_v2[:, :L // 2] = 0
424
- seg_v2[:, L // 2:] = 1 # 前半 vs 后半 隔离
425
- h1 = model_cuda.bert(ids_c, seg_ids=seg_v1) # 全 attend(单 doc)
426
- h2 = model_cuda.bert(ids_c, seg_ids=seg_v2) # 前后半隔离
427
- # 前半 token 在 v1 attend 全 chunk,在 v2 只 attend 前半 → 输出应不同
428
- front_diff = (h1[:, :L // 2] - h2[:, :L // 2]).abs().mean().item()
429
- print(f" 前半 token 输出差异(应 > 0,验证隔离生效):{front_diff:.6f}")
430
- assert front_diff > 1e-3, f"隔离未生效:front_diff={front_diff}"
431
- print(f" 跨 doc 隔离工作正常 ✓")
432
- else:
433
- print(" 跳过(无 CUDA)")
434
-
435
- print("\n=== Test 2: legacy v1 config (12L/1024H) — 验证向后兼容 ===")
436
- cfg_v1 = ModernBertConfig(
437
- hidden_size=1024, num_hidden_layers=12, num_attention_heads=16,
438
- intermediate_size=2752,
439
- embed_norm=False, skip_first_prenorm=False, init_method="normal",
440
- )
441
- model_v1 = ModernBertForMLM(cfg_v1)
442
- n_v1 = model_v1.num_parameters()
443
- print(f"v1-style params: {n_v1 / 1e6:.1f}M")
444
- out_v1 = model_v1(ids, attention_mask=mask, labels=labels)
445
- print(f"v1 forward OK loss={out_v1['loss'].item():.4f}")
446
-
447
- print("\n=== All smoke tests passed ===")
 
1
+ """Modern BERTc 骨干网络。ModernBERT release 对齐 + Cramming ScaledSinusoidal PE
2
+
3
+ 只依赖 torch。**state_dict 的 key 不能动** —— 改任何模块名或嵌套层级都会让
4
+ HF 上已发布的六个模型权重全部失配,而模型照样能随机初始化跑起来、不报错。
5
+ 改动后跑 test/test_reproduce_sota.py 验证。
6
+
7
+ 两个已发布规格(都用同一份代码,只是 config 不同):
8
+ BERTc-165M (v4-Mid) 12L / 1024H / 2752I / 16 heads
9
+ BERTc-315M (v4-Large) 24L / 1024H / 2752I / 16 heads
10
 
11
  主要按 release `modernbert-base-pretrain.yaml` 对齐(除 Alt Attn 和 PE):
12
+ - 默认 config: 22L / 768H / 1152I (GLU) / 12 heads,head_dim=64
13
  - **ScaledSinusoidal 位置编码**(Hua et al. 2022 FLASH;Cramming 实测短 seq 比
14
  RoPE 更值:计算几乎免费,RoPE 收益被 5-10% 速度损失抵消)
15
  - GeGLU FFN(glu + gelu)
 
41
  head_bias (V,) : 12.5K
42
  total ≈ 130M
43
  """
44
+ from dataclasses import dataclass
45
  from typing import Optional
46
  import math
47
 
 
342
  return sum(p.numel() for p in self.parameters() if p.requires_grad)
343
 
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tokenizer.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BERTc 发布包自带的字级 tokenizer。
2
+
3
+ 这份代码会**随模型一起发到 HF**,所以只能依赖 piece_tokenizer 本身,
4
+ 不能 import 仓库里的任何东西。
5
+
6
+ 装 tokenizer:
7
+ pip install git+https://github.com/Ismantic/PieceTokenizer
8
+ """
9
+ from pathlib import Path
10
+
11
+ import piece_tokenizer as _pt
12
+
13
+
14
+ class PieceCharTokenizer:
15
+ """字级 tokenizer。
16
+
17
+ 必须用 dict="no" 加载(字模式,不挂分词词典)—— 挂了词典编码结果会跟
18
+ 训练时不一致,而且不会报错。
19
+ """
20
+
21
+ def __init__(self, model_dir="."):
22
+ model_dir = Path(model_dir)
23
+ self._tok = _pt.Tokenizer()
24
+ self._tok.load(str(model_dir / "piece.model"), dict="no")
25
+
26
+ self.pad_token_id = self._tok.piece_to_id("<pad>")
27
+ self.unk_token_id = 0
28
+ mask_path = model_dir / "mask_token_id.txt"
29
+ self.mask_token_id = (int(mask_path.read_text().strip())
30
+ if mask_path.exists() else self._tok.vocab_size())
31
+ self.vocab_size = self._tok.vocab_size() + 1
32
+ self._cache = {}
33
+
34
+ def char_to_id(self, char: str) -> int:
35
+ tid = self._cache.get(char)
36
+ if tid is None:
37
+ ids = self._tok.encode_as_ids(char)
38
+ tid = ids[0] if ids else self.unk_token_id
39
+ self._cache[char] = tid
40
+ return tid
41
+
42
+ def id_to_char(self, tid: int) -> str:
43
+ piece = self._tok.id_to_piece(int(tid))
44
+ return piece.replace("▁", "")
45
+
46
+ def encode(self, text: str) -> list:
47
+ return [self.char_to_id(c) for c in text]
48
+
49
+ def batch(self, texts, max_len, device=None):
50
+ """一批文本 → (input_ids, attention_mask, 每条的有效长度)。"""
51
+ import torch
52
+
53
+ lengths = [min(len(t), max_len) for t in texts]
54
+ width = max(lengths) if lengths else 0
55
+ input_ids = torch.full((len(texts), width), self.pad_token_id,
56
+ dtype=torch.long, device=device)
57
+ attn = torch.zeros((len(texts), width), dtype=torch.long, device=device)
58
+ for i, t in enumerate(texts):
59
+ ids = self.encode(t[:lengths[i]])
60
+ if ids:
61
+ input_ids[i, :len(ids)] = torch.tensor(ids, dtype=torch.long,
62
+ device=device)
63
+ attn[i, :len(ids)] = 1
64
+ return input_ids, attn, lengths