同一个句子在云端API里回答精准,在本地设备上却牛头不对马嘴。罪魁祸首往往不是模型本身,而是最不起眼的分词器。你用一个纯Dart手写的近似实现做编码,从第一枚词元开始,结果就已经偏了——这个偏差不会报错,只会让模型输出悄然退化,直到无法追踪。
避免这种漂移的办法,不是重写算法,而是直接绑定。新开源的hf_tokenizers没有重新实现任何逻辑,它通过一层C ABI和dart:ffi,把HuggingFace维护的Rust分词核心完整接入Dart。这意味着tokenizer.json里定义的规范化器、预分词器、模型(BPE、字节级BPE、WordPiece、Unigram)和后处理器,都会原封不动地运行,产生的每一个ID都和Python生态里的一模一样。
在pub.dev上,每个纯Dart实现都只抄了一种算法,且只抄了个大概。它们会在标点、大小写、罕见字符这些角落逐渐偏离参考词表。而你在设备上跑模型、调用前计算长度、或为检索切块时,需要的是和模型一起出厂的那个原始分词器,不是一个“看着像”的替代品。hf_tokenizers的绑定策略让这一需求成为可能:只需一行dart pub add hf_tokenizers,两行代码就能加载并编码,返回的ID序列与HuggingFace基准完全一致。
具体来说,Tokenizer.fromFile载入资产中的tokenizer.json,tk.encode('hello world')生成[101, 7592, 2088, 102],这恰好对应BERT的[CLS] hello world [SEP]。如果不需要特殊标记,传入addSpecialTokens: false即可。Tokenizer.fromBytes更支持直接从字节数据加载,适合运行时动态场景。无论加载BERT、GPT-2还是ALBERT的词表,所有细节都由底层Rust库处理,Dart代码无需理解词汇表内部结构。
整个包的立足之本就是“与参考实现完全匹配”。测试套件加载真实的bert-base-uncased分词配置,直接断言编码、解码以及WordPiece切分结果与HuggingFace已知正确值完全一致,decode往返也不出差错。这些ID的对齐不是“很可能对”,而是必须对——因为这是使用它的唯一理由。
为了量化偏差的代价,作者还附上了一个小工具:可遍历代码仓库,计算所有文件送入模型会消耗多少词元。当它扫过Dart生态里的image包时,报告显示479个Dart文件共684,054个单元,其中单一个arial_48.dart嵌入式字体文件就占去37,936个——接近一个完整上下文窗口的五分之一。这些数字让“静默漂移”的后果变得触手可及,也凸显了一个精确分词器在实际工具链中的关键位置。
热门跟贴