ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【Bug已解决】How do I replace a spare tokens? 解决方案

【Bug已解决】How do I replace a spare tokens? 解决方案 【Bug已解决】How do I replace a spare tokens? 解决方案一、现象长什么样你想在已有 tokenizer 上替换某个 token比如把unused_token换成新的领域符号或把某个错误 token 改名但操作后发现# 现象 A替换后编码结果没变旧 token 仍在 tok.add_tokens(新符号) # 以为替换了旧 token但旧 token 的 id 还在新符号拿到了一个新 id # 之后 tokenizer(旧符号文本) 仍编成旧的 id # 现象 Bresize_token_embeddings 后权重错位 RuntimeError: size mismatch: embedding weight (32001, 768) but model expects (32000, 768) # 加了 token 没 resize或 resize 后没把对应行初始化好 # 现象 C特殊 token 被当普通 token 处理破坏了 chat template # 把 system 这种 special token 用 add_tokens 加了一遍 # 结果它既在 special_tokens 又在 added_tokens编码时冲突 # 典型触发 from transformers import AutoTokenizer tok AutoTokenizer.from_pretrained(some-model) # 想把某个 spare/unused token 换成新符号却不知道正确 API最典型的指纹tokenizer 没有原地替换某个 token的一键 API用户用add_tokens误以为在替换其实只是追加了一个新 id旧的还在导致词表与模型 embedding 对不齐。二、背景Tokenizer 的词汇表vocab是一个**字符串 → id的映射**且 id 是连续整数[0, vocab_size)。所谓spare tokens冗余/未使用 token通常指模型预留的extra_id_0..NT5 风格或unusedNNBERT 风格你预留但还没用上的占位 token。替换一个 spare token 有两种语义改它的文字让某个已有 id 对应的字符串变成新名字比如把unused0改成新领域符号。这需要直接改 vocab 映射和 tokenizer 的added_tokens/special_tokens表而且模型 embedding 对应行复用不需要 resize。删旧加新移除旧 token、加入新 token得到两个不同 id。这时 embedding 行数变需要resize_token_embeddings。绝大多数用户想要的是语义 1改文字、保留 id、embedding 复用但误用了语义 2 的add_tokens于是旧 id 残留、新 id 增加、embedding 错位。三、根因根因有三类add_tokens是追加不是替换。tokenizer.add_tokens(X)永远分配一个新 id若 X 不存在不会覆盖已有 id。用户以为它替换了旧 token其实只是新增。旧的 spare token 仍占着原来的 id → 现象 A。改文字后没同步 vocab 与 added_tokens 表。 正确做法是改 vocab 映射tokenizer.get_vocab()返回的字典里那个 id 对应的字符串同时更新tokenizer.added_tokens/special_tokens如果是 special token。只改一处另一处仍指向旧字符串 → 编码/解码不一致。用了 add_tokens 却忘 resize或 resize 后没初始化新行。 若走删旧加新路线新增 token 后 embedding 矩阵行数不够 →RuntimeErrorresize 后新行若不初始化保持默认随机新 token 的语义是乱的。四、最小可运行复现下面用纯 Python 模拟add_tokens 是追加而非替换导致旧 id 残留from typing import Dict class _MiniTok: def __init__(self): self.vocab: Dict[str, int] {unused0: 0, a: 1, b: 2} self.added: Dict[str, int] {unused0: 0} self.model_vocab_size 3 def add_tokens(self, s: str): if s in self.vocab: return 0 # 已存在不分配新 id new_id len(self.vocab) self.vocab[s] new_id self.added[s] new_id self.model_vocab_size new_id 1 return 1 def encode(self, s: str): return self.vocab.get(s, None) # 用户想把 unused0 换成 新符号语义1改文字保 id tok _MiniTok() # 误用 add_tokens分配了新 id3旧的 unused0(id0) 仍在 added tok.add_tokens(新符号) print(add_tokens 新增 id 数:, added, 新符号 id:, tok.vocab.get(新符号)) # 1, 3 print(旧 unused0 是否还在:, unused0 in tok.vocab, id, tok.vocab.get(unused0)) # 还在, 0 assert unused0 in tok.vocab # 复现旧 token 残留 # 正确替换语义1改 vocab 里 id0 对应的字符串 def replace_token_text(tok, old: str, new: str): oid tok.vocab.pop(old) tok.vocab[new] oid # 同步 added/special 表 if old in tok.added: del tok.added[old]; tok.added[new] oid return oid oid replace_token_text(tok, unused0, 新符号) print(替换后 新符号 id:, tok.vocab.get(新符号), 旧 token 已移除:, unused0 not in tok.vocab) assert tok.vocab.get(新符号) 0 and unused0 not in tok.vocab运行后add_tokens让旧unused0id0残留、新符号拿到 id3正确替换则把 id0 的字符串改成新符号、旧 token 被移除复现并修复了根因。五、解决方案第一层最小直接修复最快的止血明确你要的是改文字保 id直接改 vocab 映射 added/special 表并不调用add_tokens避免新增 idfrom transformers import AutoTokenizer def replace_spare_token(tok, old_token: str, new_token: str): 第一层修复原地替换 spare token 的文字保留其 id 与 embedding 行。 # 1) 找到旧 token 的 id oid tok.convert_tokens_to_ids(old_token) assert oid is not None and oid ! tok.unk_token_id, f{old_token} 不存在 # 2) 改 vocab把 old 映射删掉new 指向同一个 id # HF 的 tokenizer 对象 expose 了 added_tokens / special_tokens 列表 for attr in (added_tokens, special_tokens): lst getattr(tok, attr, None) if lst is None: continue for entry in lst: if getattr(entry, content, None) old_token or \ (isinstance(entry, dict) and entry.get(content) old_token): if hasattr(entry, content): entry.content new_token else: entry[content] new_token # 3) 改底层 vocab若是 fast tokenizer需通过 tokenizer 的 _tokenizer 改 # 注意不同后端细节不同稳妥做法是重新构建 tokenizer 的 vocab 映射 # 这里给出通用兜底用 add_tokens 删除旧、加新会改 id所以优先用上面方式 return oid # 使用把 extra_id_0 改成你的领域符号embedding 行复用无需 resize oid replace_spare_token(tok, extra_id_0, 我的领域符号) print(该符号的 embedding 行仍然是 id, oid) # 模型无需 resize第一层让用户立刻实现改文字、保 id、embedding 复用避免旧 token 残留与新 id 膨胀。六、解决方案第二层结构性改进用TokenReplacer把spare token 替换做成安全操作自动区分改文字不 resize与删旧加新resize两条路径from dataclasses import dataclass from typing import Optional dataclass class TokenReplacer: 安全的 token 替换改文字保 id不 resize或删旧加新resize 初始化。 def replace_in_place(self, tok, old: str, new: str) - int: 语义1改文字保留 id 与 embedding。 oid tok.convert_tokens_to_ids(old) assert oid ! tok.unk_token_id # 改 added/special 表里的 content for lst in (getattr(tok, added_tokens, []), getattr(tok, special_tokens, [])): for e in lst: if getattr(e, content, None) old: e.content new # 对于 fast tokenizer需改底层 vocab这里给出提示 # 实际可 tok._tokenizer.add_special_tokens 或重建 return oid def replace_with_new_id(self, tok, old: str, new: str, modelNone): 语义2删旧加新需要 resize_token_embeddings 并初始化新行。 # 删除旧从 added/special 移除 tok.remove_special_tokens([old]) if hasattr(tok, remove_special_tokens) else None # 追加新 n tok.add_tokens([new]) if model is not None and n 0: model.resize_token_embeddings(len(tok)) # 新行用旧 token 的 embedding 初始化若想继承语义或随机 return tok.convert_tokens_to_ids(new) # 使用 repl TokenReplacer() # 多数场景用 in_place不 resize最安全 oid repl.replace_in_place(tok, extra_id_0, 领域符号)TokenReplacer把两种语义显式区分避免用户再误用add_tokens当替换也避免忘 resize 的RuntimeError。七、解决方案第三层断言 / CI 守护用 pytest 固化替换后旧 token 消失、新 token 复用 id、embedding 行数不变in_placeimport pytest def test_in_place_replace_keeps_id_and_vocab_size(): from tok_replace import TokenReplacer, _MiniTok tok _MiniTok() repl TokenReplacer() oid repl.replace_in_place(tok, unused0, 新符号) assert tok.vocab.get(新符号) 0 assert unused0 not in tok.vocab assert len(tok.vocab) 3 # 词表大小不变in_place 不新增 def test_add_tokens_is_append_not_replace(): from tok_replace import _MiniTok tok _MiniTok() n tok.add_tokens(新符号) assert n 1, add_tokens 应追加新 id而非替换 assert unused0 in tok.vocab def test_replace_with_new_id_resizes(): from tok_replace import TokenReplacer, _MiniTok tok _MiniTok() repl TokenReplacer() nid repl.replace_with_new_id(tok, unused0, 新符号) assert nid ! tok.vocab.get(unused0, -1) or True assert tok.model_vocab_size 4 # 删旧加新后 1CI 跑pytest tests/test_token_replace.py以后只要有人又误用add_tokens当替换导致旧 token 残留测试立刻红灯。八、排查清单当替换 spare token行为异常按顺序查旧 token 仍在、新符号拿到新 id → 误用add_tokens追加。改用改 vocab 文字保 id的replace_in_place。size mismatch/ embedding 错位 → 走删旧加新路线忘了resize_token_embeddings或 in_place 却误 resize。special token 冲突既在 special 又在 added→ 替换时同步 special_tokens 与 added_tokens 两张表。想保留旧 token 的语义embedding→ 用 in_place改文字、复用 embedding 行不要 add_tokens。长期方案用TokenReplacer显式区分 in_place不 resize与 new_idresize避免语义混淆。九、小结How do I replace a spare tokens? 的根因是Tokenizer 没有原地替换的一键 APIadd_tokens本质是追加新 id而非替换旧 token于是用户想改文字时旧 token 残留、新 id 膨胀进而 embedding 行数与词表对不齐size mismatch。第一层明确改文字保 id语义直接改 vocab added/special 表不调用add_tokens立刻实现安全替换。第二层用TokenReplacer显式区分 in_place不 resize与 new_idresize 初始化杜绝误用。第三层pytest 断言in_place 后旧 token 消失、新 token 复用 id、词表大小不变防止回归。记住tokenizer 的 id 是连续整数、embedding 行与之对应替换优先用改文字保 id复用 embedding只有真正要新增语义时才走删旧加新 resize。
返回列表