MoxhiMT-30-Tag (zh→vi translation with entity tags)

Web demo HF Space

A Chinese → Vietnamese translation model (37M parameters) that marks proper names in its own output. One model and one decoding pass give the translation and the names. You do not need a second NER model.

苏夜祭出赤霄剑,剑光一闪,将对面那面玄冥盾劈成两半。
<PER>Tô Dạ</PER> tế ra <ITEM>Xích Tiêu Kiếm</ITEM>, kiếm quang lóe lên, chém tấm <ITEM>Huyền Minh Thuẫn</ITEM> đối diện thành hai nửa.
Tag Meaning Examples
PER person Hàn Lập, Mộ Vân Yên
LOC place Thiên Nam Thành, Hoàng Phong Cốc
ORG organization: sect, clan, guild, country Lạc Vân Tông, Thiên Cơ Các
ITEM named item: treasure, weapon, pill, technique, formation, scripture Xích Tiêu Kiếm, Cửu Chuyển Hoàn Hồn Đan

Generic words ("kiếm", "đan dược") do not get an ITEM tag; only names do.

Status: experimental. This is a variant of MoxhiMT-30 v4.0. It is not a drop-in replacement: the vocabulary has 24,008 entries (24,000 + 8 tag tokens), and the raw output contains tags that you must post-process (see below). ITEM is the newest and weakest type. Nobody has done a blind human review of this model yet.

Quick start

# pip install transformers sentencepiece torch huggingface_hub
import sys
from pathlib import Path
from huggingface_hub import hf_hub_download
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

repo = "DanVP/MoxhiMT-30-Tag"
tok = AutoTokenizer.from_pretrained(repo)
model = AutoModelForSeq2SeqLM.from_pretrained(repo)

# moxhi_tag.py: plain-Python tag post-processing (about 200 lines, no dependencies).
sys.path.insert(0, str(Path(hf_hub_download(repo, "moxhi_tag.py")).parent))
import moxhi_tag as mt

tagged, plain, names = mt.translate(model, tok, "慕云烟是落云宗的长老,她来自天南城。")
print(tagged)  # <PER>Mộ Vân Yên</PER> là trưởng lão của <ORG>Lạc Vân Tông</ORG>, nàng đến từ <LOC>Thiên Nam Thành</LOC>.
print(plain)   # Mộ Vân Yên là trưởng lão của Lạc Vân Tông, nàng đến từ Thiên Nam Thành.
print(names)   # [('PER', 'Mộ Vân Yên'), ('ORG', 'Lạc Vân Tông'), ('LOC', 'Thiên Nam Thành')]

mt.translate uses beam search with 4 beams and max_length=512, the setting of all scores below. Translate long text one sentence or one short paragraph at a time.

Post-processing

The tags are 8 special tokens (ids 24000–24007). Remove the <pad> and </s> ids, then decode with skip_special_tokens=False (mt.translate also removes <s> and <unk>). The raw string has extra spaces around the tags:

text = "韩立呈上青冥剑和一枚记载着《太玄丹经》的玉简,殿中众人无不动容。"
out = model.generate(**tok(text, return_tensors="pt"), num_beams=4, max_length=512)
ids = [i for i in out[0].tolist() if i not in {tok.pad_token_id, tok.eos_token_id}]
raw = tok.decode(ids, skip_special_tokens=False)
# '<PER> Hàn Lập</PER> dâng lên<ITEM> Thanh Minh kiếm</ITEM> cùng một ngọc giản ghi chép "<ITEM> Thái Huyền Đan Kinh</ITEM> " , ...'
tagged = mt.postprocess(raw)
# '<PER>Hàn Lập</PER> dâng lên <ITEM>Thanh Minh kiếm</ITEM> cùng một ngọc giản ghi chép “<ITEM>Thái Huyền Đan Kinh</ITEM>”, ...'

mt.postprocess runs three deterministic steps:

  1. normalize_tags: one space before an opening tag, no space inside the tags, no space before closing punctuation.
  2. repair_tags: about 2 % of lines have broken tags, for example <LOC>…</ORG>. A closing tag takes the type of the open tag. A closing tag with no open tag is removed. When two tags open in a row, the first opening tag is removed. An opening tag that never closes is removed. The step never changes the text and never creates a new name.
  3. tidy_punct: after a closing tag the model writes punctuation as a separate word (</ITEM> "). This step removes the spaces inside 《 》 and turns an even number of straight quotes into “…” pairs.

Then mt.strip_tags(tagged) gives the plain translation and mt.entities(tagged) gives the list of (type, name) pairs.

Decoding notes:

  • Do not set no_repeat_ngram_size. It blocks repeated pairs such as </PER> , and breaks the tags.
  • Greedy decoding with repetition_penalty=1.05 (the setting of the web demo) also works. The tag scores below were measured with beam 4 only.
  • Output is about 5 % longer in tokens than MoxhiMT-30 output because of the tags.

Names in the Chinese source (optional)

ner_head/ner_head_l6_linear.json is a small linear classifier (9 BIO labels, 4,041 weights) that reads the encoder state after 6 of the 8 encoder layers and finds the names in the Chinese source. The encoder runs anyway during translation, so this costs almost nothing.

import json
head = json.load(open(hf_hub_download(repo, "ner_head/ner_head_l6_linear.json"), encoding="utf-8"))
ner = mt.SourceNER(model, tok, head, hf_hub_download(repo, "source.spm"))
print(ner(text))  # [(0, 2, 'PER', '韩立'), (4, 7, 'ITEM', '青冥剑'), (14, 18, 'ITEM', '太玄丹经')]

Offsets are character offsets into the source text. The web demo uses these spans to link each Vietnamese name to its Chinese name (by Sino-Vietnamese reading first, then by order).

Evaluation

Tags in the translation (beam 4, after repair_tags):

Type F1 Precision Recall Test set
PER 0.88 0.88 0.89 500 Chinese sentences from 11 novels, 607 gold names
LOC 0.54 0.53 0.54 same
ORG 0.68 0.70 0.67 same
ITEM 0.68 0.69 0.67 300 other sentences, 250 gold items (95 % CI of F1: 0.62–0.74)

A tag counts as correct when its type is correct and its Vietnamese text matches the gold name, checked against the Sino-Vietnamese reading of the Chinese name or against a reference rendering (for names that are not read in Sino-Vietnamese).

Names in the source (SourceNER, same test sets): F1 0.84 for PER/LOC/ORG together (PER 0.90, LOC 0.54, ORG 0.67) and 0.77 for ITEM (precision 0.75, recall 0.80).

Other measurements:

  • Well-formed tags before repair: 98.1 % of lines (1,392-line clean test suite). After repair: 100 %.
  • Over-tagging: on the 179 test sentences that have no gold PER/LOC/ORG name, 11.7 % get a tag (some of these are names that the gold labels missed). On item-free sentences, 6.8 % get an ITEM tag.
  • Translation (tags removed) against MoxhiMT-30 v4.0 on the 1,392-line clean suite: BLEU −0.13, repetition rate 1.36 % (v4.0: 1.51 %). Only 22 % of lines are word-for-word the same as v4.0; most other lines use different wording.

Caveats:

  • The 500-sentence set was also used to choose between training variants, so the PER/LOC/ORG numbers are slightly optimistic. Some of its source sentences are in the training data of the base translation model (without tags).
  • The ITEM gold labels come from a majority vote of three LLMs, followed by a review pass. A human annotator did not label them.
  • LOC and ORG are much weaker than PER. The same name can get two different readings in two sentences (for example "Mặc Thái Hoàn" and "Mặc Thải Hoàn").

Training

  • Start: MoxhiMT-30 v4.0 (Marian, 8 encoder + 2 decoder layers, d_model 448) with tokenizer v4.0.3, plus 8 new special tokens. The new embedding rows start from the 【 / 】 pieces.
  • Data: 86,573 different Chinese web-novel sentences, with all test sentences removed. The target of each sentence is the base model's own translation; tags were added from labels on the source side. PER/LOC/ORG labels come from a fine-tuned Chinese NER model. ITEM labels come from two LLMs that had to agree; a third LLM decided the cases where they did not. 28,599 sentences have no name and stay unchanged. In the training file, sentences with LOC/ORG appear 4 times and sentences with ITEM get one more copy (128,467 lines in total).
  • 1,000 updates, batch 128, learning rate 3e-5 (linear decay, 50 warm-up steps), label smoothing 0.1, bf16, one RTX 4090, about 2 minutes.

Files

File Content
model.safetensors, config.json, generation_config.json Model weights (fp32) and config
source.spm, target.spm, vocab.json, tokenizer_config.json, added_tokens.json, special_tokens_map.json Tokenizer (MoxhiMT-30 v4.0.3 + 8 tag tokens); loads with transformers 4.57 and 5.x
moxhi_tag.py Tag post-processing and the SourceNER helper
ner_head/ner_head_l6_linear.json Linear head for names in the source (layer 6)

Tag token ids: <PER> 24000, <LOC> 24001, <ORG> 24002, <ITEM> 24003, </PER> 24004, </LOC> 24005, </ORG> 24006, </ITEM> 24007.

Web demo

moxhi-tag.pages.dev (mirror: Space) runs this model in the browser with WebGPU, with 8-bit weights (37 MB). Text does not leave your computer. It needs Chrome or Edge 113+ on a computer, or Chrome on Android 12+. With 8-bit weights, about 83 % of sentences are word-for-word the same as the fp32 model output.

License

CC-BY-NC-4.0 (research / non-commercial use), the same as MoxhiMT-30.

Downloads last month
-
Safetensors
Model size
36.5M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for DanVP/MoxhiMT-30-Tag

Base model

DanVP/MoxhiMT-30
Finetuned
(1)
this model

Space using DanVP/MoxhiMT-30-Tag 1