BERTc-165M-MT / mt_model.py
tf-bao's picture
Upload BERTc-165M-MT release
866611a verified
Raw
History Blame Contribute Delete
7.72 kB
import json
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from safetensors.torch import load_file
from torchcrf import CRF
from model import ModernBertConfig, ModernBertModel
CWS_ID2TAG = {0: "B", 1: "I", 2: "E", 3: "S"}
LTP_POS_TAGS = [
"a", "b", "c", "d", "e", "h", "i", "j", "k", "m",
"n", "nd", "nh", "ni", "nl", "ns", "nt", "nz", "o", "p",
"q", "r", "u", "v", "wp", "x", "z",
]
LTP_ID2POS = {i: t for i, t in enumerate(LTP_POS_TAGS)}
LTP_NER_TAGS = [
"O",
"B-Nh", "I-Nh", "E-Nh", "S-Nh",
"B-Ns", "I-Ns", "E-Ns", "S-Ns",
"B-Ni", "I-Ni", "E-Ni", "S-Ni",
]
LTP_ID2NER = {i: t for i, t in enumerate(LTP_NER_TAGS)}
def bies_to_words(chars, tag_ids):
words = []
buf = []
for ch, tid in zip(chars, tag_ids):
tag = CWS_ID2TAG.get(int(tid), "S")
if tag == "S":
if buf:
words.append("".join(buf))
buf = []
words.append(ch)
elif tag == "B":
if buf:
words.append("".join(buf))
buf = [ch]
elif tag == "I":
buf.append(ch)
elif tag == "E":
buf.append(ch)
words.append("".join(buf))
buf = []
else:
words.append(ch)
if buf:
words.append("".join(buf))
return words
def bies_tags_to_spans(tag_ids):
spans = []
i = 0
n = len(tag_ids)
while i < n:
tag = LTP_ID2NER.get(int(tag_ids[i]), "O")
if tag.startswith("S-"):
spans.append({"type": tag[2:], "start": i, "end": i + 1})
i += 1
continue
if tag.startswith("B-"):
ent = tag[2:]
j = i + 1
while j < n:
nxt = LTP_ID2NER.get(int(tag_ids[j]), "O")
if nxt == f"I-{ent}":
j += 1
continue
if nxt == f"E-{ent}":
spans.append({"type": ent, "start": i, "end": j + 1})
j += 1
break
i = j
continue
i += 1
return spans
class PieceCharTokenizer:
def __init__(self, model_dir):
import piece_tokenizer as pt
model_dir = Path(model_dir)
self._tok = pt.Tokenizer()
self._tok.load(str(model_dir / "piece.model"), cn_dict="no")
self.pad_token_id = self._tok.piece_to_id("<pad>")
self.unk_token_id = 0
mask_path = model_dir / "mask_token_id.txt"
self.mask_token_id = int(mask_path.read_text().strip()) if mask_path.exists() else self._tok.vocab_size()
self.vocab_size = self._tok.vocab_size() + 1
self._cache = {}
def char_to_id(self, char):
if char in self._cache:
return self._cache[char]
ids = self._tok.encode_as_ids(char)
tid = ids[0] if ids else self.unk_token_id
self._cache[char] = tid
return tid
class BERTcForMT(nn.Module):
def __init__(self, config, num_pos=27, num_cws=4, num_ner=13, dropout=0.1):
super().__init__()
self.config = config
self.bert = ModernBertModel(config)
hidden = config.hidden_size
self.dropout = nn.Dropout(dropout)
self.cws_classifier = nn.Linear(hidden, num_cws)
self.cws_crf = CRF(num_cws, batch_first=True)
self.pos_classifier = nn.Linear(hidden, num_pos)
self.ner_classifier = nn.Linear(hidden, num_ner)
self.ner_crf = CRF(num_ner, batch_first=True)
@classmethod
def from_pretrained(cls, model_dir, map_location="cpu"):
model_dir = Path(model_dir)
cfg = ModernBertConfig(**json.loads((model_dir / "config.json").read_text()))
model = cls(cfg)
state = load_file(str(model_dir / "model.safetensors"), device=str(map_location))
missing, unexpected = model.load_state_dict(state, strict=True)
if missing or unexpected:
raise RuntimeError(f"Bad state dict: missing={missing}, unexpected={unexpected}")
model.eval()
return model
def forward(self, input_ids, attention_mask, cws_labels=None, pos_labels=None, ner_labels=None):
hs = self.bert(input_ids, attention_mask=attention_mask)
hs = self.dropout(hs)
cws_emi = self.cws_classifier(hs)
pos_logits = self.pos_classifier(hs)
ner_emi = self.ner_classifier(hs)
mask = attention_mask.bool()
losses = {}
if cws_labels is not None:
losses["cws"] = -self.cws_crf(cws_emi, cws_labels, mask=mask, reduction="mean")
if pos_labels is not None:
losses["pos"] = F.cross_entropy(
pos_logits.view(-1, pos_logits.size(-1)),
pos_labels.view(-1),
ignore_index=-100,
)
if ner_labels is not None:
losses["ner"] = -self.ner_crf(ner_emi, ner_labels, mask=mask, reduction="mean")
return losses, (cws_emi, pos_logits, ner_emi)
@torch.no_grad()
def decode_cws(self, input_ids, attention_mask):
hs = self.bert(input_ids, attention_mask=attention_mask)
emi = self.cws_classifier(self.dropout(hs)).float()
return self.cws_crf.decode(emi, mask=attention_mask.bool())
@torch.no_grad()
def decode_ner(self, input_ids, attention_mask):
hs = self.bert(input_ids, attention_mask=attention_mask)
emi = self.ner_classifier(self.dropout(hs)).float()
return self.ner_crf.decode(emi, mask=attention_mask.bool())
@torch.no_grad()
def predict_pos(self, input_ids, attention_mask):
hs = self.bert(input_ids, attention_mask=attention_mask)
logits = self.pos_classifier(self.dropout(hs))
return logits.argmax(-1)
@torch.no_grad()
def predict(self, text, tokenizer=None, max_len=254, device=None):
if tokenizer is None:
tokenizer = PieceCharTokenizer(Path(__file__).resolve().parent)
if isinstance(text, str):
single = True
texts = [text]
else:
single = False
texts = list(text)
device = device or next(self.parameters()).device
self.eval()
lengths = [min(len(t), max_len) for t in texts]
max_l = max(lengths) if lengths else 0
input_ids = torch.full((len(texts), max_l), tokenizer.pad_token_id, dtype=torch.long, device=device)
attn = torch.zeros((len(texts), max_l), dtype=torch.long, device=device)
for i, s in enumerate(texts):
ids = [tokenizer.char_to_id(c) for c in s[:lengths[i]]]
if ids:
input_ids[i, :len(ids)] = torch.tensor(ids, dtype=torch.long, device=device)
attn[i, :len(ids)] = 1
cws_preds = self.decode_cws(input_ids, attn)
pos_pred = self.predict_pos(input_ids, attn).cpu().tolist()
ner_preds = self.decode_ner(input_ids, attn)
out = []
for i, s in enumerate(texts):
chars = list(s[:lengths[i]])
cws_ids = cws_preds[i][:len(chars)]
pos_ids = pos_pred[i][:len(chars)]
ner_ids = ner_preds[i][:len(chars)]
words = bies_to_words(chars, cws_ids)
pos = []
offset = 0
for w in words:
if offset < len(pos_ids):
pos.append(LTP_ID2POS.get(int(pos_ids[offset]), "x"))
else:
pos.append("x")
offset += len(w)
out.append({
"text": s,
"words": words,
"pos": pos,
"ner": bies_tags_to_spans(ner_ids),
})
return out[0] if single else out