找回密码
 立即注册
首页 业界区 业界 C#AI系列(7):从零开始LLM之Tokenizer实现

C#AI系列(7):从零开始LLM之Tokenizer实现

郜庄静 4 小时前
一、前言: token是什么

LLM只做一个事情,就是吃掉token吐出token,token是LLM(大语言模型)的基本元素。token与LLM的关系,相当于乐高积木与乐高工厂,我的世界方块与我的世界游戏。那么token到底是什么呢?有人翻译成令牌,有人翻译成词源。我们不妨换个概念理解,token就是最小操作、最小信息单元的意思。这个最小是相对于LLM要处理的原始文本来说的。
举个栗子,当一个句子文本输入到电脑中,天然就就具有字符级别的切分。如果不打算继续拆分或组合,我们可以通过一个映射关系,将现有这些字符转换为整数数组,称为编码过程。编码后数组内的元素就是token,元素取值就等于token取值。LLM可以吃掉这个token数组,并吐出新数组。对这个新数组按前前述的映射进行逆转换,称为解码过程。解码后我们就能得到人类可以理解的文本了。
  1. // 原句子
  2. "我有一个 apple."
  3. // 句子拆分
  4. ["我","有","一","个"," ","a","p","p","l","e",".","\0"]
  5. // 编码为整数数组
  6. [1,2,3,4,5,6,7,7,8,9,10,11]
复制代码
从实际应用看,主流LLM几乎不用纯字符级级别切分,而是为了更好效果,使用BPE/WordPiece/SentencePiece等子词(sub-word)算法。此时"hello"大概率是1个或2个token,而不是5个。对于中文来说,"我有一个" 可能切成了 "我/有/一/个",也可能是"我有/一个",取决于词表。在字词算法中,单个token拎出来会存在不可解释性,因为是打散的词根。
但是无论怎么处理,LLM传入传出的都是一个整数数组,数组元素的数量,就是token数量(也是LLM服务的计费标准)。
再从实际应用看,主流LLM几乎都采用BPE或BBPE方式进行Tokenizer。我们接下来继续了解BPE。
二、BPE(字节对编码)

字节对编码 是一种简单的数据压缩形式,这种方法用数据中不存的一个字节表示最常出现的连续字节数据。这样的替换需要重建全部原始数据。编码过程如下:
  1. // wiki的BPE案例
  2. "aaabdaaabac": "aa"=>"Z" //“aa”出现次数最多,用中没有出现的“Z”替换
  3. "ZabdZabac": "aa"=>"Z", "Za"=>"Y" //同上,更新替换表
  4. "YbdYbac": "aa"=>"Z", "Za"=>"Y", "Yb"=>"X" //同上,更新替换表
  5. "XdXac":"aa"=>"Z", "Za"=>"Y", "Yb"=>"X" // 无可用替换
复制代码
我们将"aaabdaaabac"通过BPE方式编码成了"XdXac"。解码时只需要对附带的 替换表("aa"=>"Z", "Za"=>"Y", "Yb"=>"X")按顺序逆向操作,就能得到原信息。
BPE 用“比字符大、比单词小”的子词当积木,之所以能流行主要是因为其编码后的token数量适中,处于单字符切分,全词切分之间。相对与全词切分,BPE是子词切分,不仅可以控制上限避免词库膨胀,还能最小可退到字节/字符,最大可保留整词,粒度随频率动态伸缩。就算预见新的词组也无所谓,不存在未登录词的问题。而且一套算法与英语、阿拉伯语语言无关,都是一套处理方式。还具有词表可读性好,在一定效果下计算成本低等特点。
三、BPE Tokenizer

一个BPE Tokenizer,主要功能可分为1.训练处理得到词表;2.编解码。词表的训练上面已经做了示意,接下来我们主要针对编解码部分。
训练好的BPE的数据主要包括三个部分:

  • vocab.json:符号 → id 的字典;
  • merges.txt:按合并顺序排列的“信息对”;
  • tokenizer_config.json:预处理规则(regex文本)、特殊标记。
另外常见的还有tokenizer.json文件,他是Hugging Face 生态把“原本分散的三份文件”压进一个JSON文件。典型的结构如下(在不同版本中,merges可能会有字符串和数组两种对象存储方式,解析时候需要注意):
  1. // cl100k_base
  2. {
  3.   "version": "1.0",
  4.   "truncation": null,
  5.   "padding": null,
  6.   "added_tokens": [ //特殊token
  7.     {
  8.       "id": 100257,
  9.       "content": "<|endoftext|>",
  10.       "single_word": false,
  11.       "lstrip": false,
  12.       "rstrip": false,
  13.       "normalized": false,
  14.       "special": true
  15.     }
  16.   ],
  17.   "normalizer": null,
  18.   "pre_tokenizer": {  // 有的有,有的没有,因此regex需要预先硬编码
  19.     "type": "Sequence",
  20.     "pretokenizers": [
  21.       {
  22.         "type": "Split",  // 预处理分割,“防呆尺”
  23.         "pattern": {
  24.           "Regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+"
  25.         },
  26.         "behavior": "Removed",   //一般默认写死,命中正则的片段保留,没命中的扔掉(与invert 配合)。
  27.         "invert": true //一般默认写死,把“命中/没命中”反转——最终只保留上面正则抓到的那些片段,其余全部丢弃。
  28.       },
  29.       {
  30.         "type": "ByteLevel",
  31.         "add_prefix_space": false,
  32.         "trim_offsets": true,
  33.         "use_regex": false
  34.       }
  35.     ]
  36.   },
  37.   "post_processor": null,
  38.   "decoder": {
  39.     "type": "ByteLevel",
  40.     "add_prefix_space": true,
  41.     "trim_offsets": true,
  42.     "use_regex": true
  43.   },
  44.   "model": {
  45.     "type": "BPE",
  46.     "dropout": null,
  47.     "unk_token": null,
  48.     "continuing_subword_prefix": "",
  49.     "end_of_word_suffix": "",
  50.     "fuse_unk": false,
  51.     "byte_fallback": false,
  52.     "vocab": {
  53.       "!": 0,
  54.        "<|endofprompt|>": 100276
  55.     },
  56.     "merges": [
  57.       "ĠCon veyor"                // 或 ["Ġp","ain"]
  58.     ]
  59.   }
  60. }
复制代码
通过读取预先的数据,BPE Tokenizer就可以用了,其核心的功能就是编码和解码,即Encode和Decode。
四、Tokenizer的C#实现

在python中,可以直接用HuggingFace的AutoTokenizer载入本地权重。在C#中我们可以拉取SharpToken (2.0.4)和 TiktokenSharp(1.2.0)计算Token。
但是,如果我们要自己在C# 开发LLM(尽管很少有人这么干),一个好的Tokenizer就很重要了,需要更多自定义的功能,如支持huggingFac的tokenizer.json数据,并灵活的处理special token,充分优化。
于是就有了LumTokenizer这个项目。
1.png

主要功能实现如下:

  • 读取tokenizer.json数据,如果没有regex,内置了3种pretoken的regex,Regex50KBase:≈GPT-2 的 5 万级别基础词表;RegexCl100KBase:≈OpenAI CLIP / GPT-3.5 / GPT-4 使用的 10 万级别 CL-100K 词表;
    RegexO200KBase:≈Meta LLaMA、Mistral 等开源模型偏好的 20 万级别 O-200K 词表
  • 高效的特殊token切分:如果是模型训练用,tokenizer需要单独高效处理特殊token。因为特殊token的目的是正文出现越少越好,因此一般不会出现在词表中,需要通过单独切分的机制进行识别和切分。
  • 高效的缓存机制:LumTokenizer 在分词阶段,订制了一套SpanDictionary, 为了实现高效的切片搜索,也就是说一个stirng可以基于NET的Span特性切成多个Slice,而SpanDictionary可以直接基于Span执行Key的匹配(Span无法作为传统Dictionary的泛型),极大节省了子串string转换的开销。
Benchmark测试如下:在含有中文这种多字节字符的长文(500字符左右)处理时,具有很好的性能。
MethodtextMeanErrorStdDevRatioRatioSDGen0AllocatedAlloc RatioSharpToken_cl100k_baseChinese122.99 us2.314 us2.273 us5.710.120.73249.1 KB1.19TiktokenSharp_cl100k_baseChinese96.00 us1.829 us2.106 us4.450.110.48836.34 KB0.83LumTokenizer_cl100k_baseChinese21.56 us0.268 us0.251 us1.000.020.61047.63 KB1.00SharpToken_cl100k_baseEnglish26.77 us0.520 us0.639 us1.020.030.67148.38 KB0.74TiktokenSharp_cl100k_baseEnglish20.21 us0.383 us0.376 us0.770.020.42725.51 KB0.49LumTokenizer_cl100k_baseEnglish26.13 us0.495 us0.509 us1.000.030.915511.31 KB1.00SharpToken_cl100k_baseMixed90.97 us1.580 us1.478 us3.780.090.854510.9 KB1.23TiktokenSharp_cl100k_baseMixed63.85 us1.274 us1.564 us2.650.080.48836.74 KB0.76LumTokenizer_cl100k_baseMixed24.08 us0.465 us0.435 us1.000.030.70198.83 KB1.00具体可以去仓库看详细代码。
  1.   [MemoryDiagnoser]
  2.   public class CompareBenchmark
  3.   {
  4.       internal GptEncoding _sharpToken;
  5.       internal TikToken _tikToken;
  6.       internal BPETokenizer _tokenizer1;
  7.       internal BPETokenizer _tokenizer2;
  8.       [GlobalSetup]
  9.       public void Setup()
  10.       {
  11.           _sharpToken = GptEncoding.GetEncoding("cl100k_base");
  12.           _tikToken = TikToken.GetEncodingAsync("cl100k_base").ConfigureAwait(false).GetAwaiter().GetResult();
  13.           _tokenizer1 = BPETokenizer.CreateTokenizer(
  14.               @"D:\Data\Personal\AI\llm\tokenizer\cl100k.txt", true, RegexType.RegexCl100KBase);
  15.           _tokenizer2 = BPETokenizer.CreateTokenizer(
  16.               @"D:\Data\Personal\AI\llm\tokenizer\qw_tokenizer.json", false, RegexType.RegexCl100KBase);
  17.       }
  18.       // ====== 1. 声明参数源 ======
  19.       public IEnumerable<string> TextSamples()
  20.       {
  21.           yield return TextCatalog.English;
  22.           yield return TextCatalog.Chinese;
  23.           yield return TextCatalog.Mixed;
  24.       }
  25.       // ====== 2. 每个方法改成带参数 ======
  26.       [Benchmark]
  27.       [ArgumentsSource(nameof(TextSamples))]
  28.       public int SharpToken_cl100k_base(string text)
  29.       {
  30.           var encoded = _sharpToken.Encode(text);
  31.           var decoded = _sharpToken.Decode(encoded);
  32.           return encoded.Count;
  33.       }
  34.       [Benchmark]
  35.       [ArgumentsSource(nameof(TextSamples))]
  36.       public int TiktokenSharp_cl100k_base(string text)
  37.       {
  38.           var encoded = _tikToken.Encode(text);
  39.           var decoded = _tikToken.Decode(encoded);
  40.           return encoded.Count;
  41.       }
  42.       [Benchmark(Baseline =true)]
  43.       [ArgumentsSource(nameof(TextSamples))]
  44.       public int LumTokenizer_cl100k_base(string text)
  45.       {
  46.           var encoded = _tokenizer1.Encode(text, false);
  47.           var decoded = _tokenizer1.Decode(encoded, false);
  48.           return encoded.Count;
  49.       }
  50.             
  51.       public int LumTokenizer_qwen150k(string text)
  52.       {
  53.           var encoded = _tokenizer2.Encode(text, false);
  54.           var decoded = _tokenizer2.Decode(encoded, false);
  55.           return encoded.Count;
  56.       }
  57.   }
复制代码
五、单元测试

现在单元测试可以说是越来越重要了,因为只有具有了完善的单元测试,才能放心的让ai去优化修改已有代码。
本文这个BPE Tokenizer项目单元测试分了5类。

  • P0_BasicTest:基础测试,测试编解码,数据读取,词表完善性等主要功能;
  • P1_RobustnessTests:鲁棒性测试,针对边缘条件,如仅空字符、仅特殊字符、超长文本、越界id等情况;
  • P2_VocabBpeTests:编解码准确性,要求正确的对原文进行分割,并准确编码,通过几种特定情况下的案例进行兜底。
  • P3_ChineseSubwordTests:中文字符测试,其中也包含了token压缩率的检验。主要是考虑在代码编写过程中,可能导致部分尾字节或特殊混编情况下不能准确字节合并的bug。
  • P4_EnglishSubwordTests:英文字符测试,目的同上,部分bug出现时,尽管decode正常,但encode编码也可能未达到预期(忽略了某些合并环节导致压缩率过高)。
编解码准确度与常用库比较:
  1. LumTokenizer_cl100k_base
  2. 34655,61078,11,832,315,42482,596,77069,323,1455,73135,11335,11,10975,279,3446,315,279,46337,323,12280,12970,61078,11,889,65928,813,26135,11,439,568,1587,813,3611,38705,11,4184,311,52671,323,74571,13,61078,753,8060,439,264,7126,2995,360,3933,5678,323,813,1917,304,63355,323,31926,16134,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,29
  3. King Lear, one of Shakespeare's darkest and most savage plays, tells the story of the foolish and Job-like Lear, who divides his kingdom, as he does his affections, according to vanity and whim. Lear’s failure as a father engulfs himself and his world in turmoil and tragedy.<|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|>
  4. SharpToken_cl100k_base
  5. 34655,61078,11,832,315,42482,596,77069,323,1455,73135,11335,11,10975,279,3446,315,279,46337,323,12280,12970,61078,11,889,65928,813,26135,11,439,568,1587,813,3611,38705,11,4184,311,52671,323,74571,13,61078,753,8060,439,264,7126,2995,360,3933,5678,323,813,1917,304,63355,323,31926,16134,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,29
  6. King Lear, one of Shakespeare's darkest and most savage plays, tells the story of the foolish and Job-like Lear, who divides his kingdom, as he does his affections, according to vanity and whim. Lear’s failure as a father engulfs himself and his world in turmoil and tragedy.<|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|>
  7. TikTokenr_cl100k_base
  8. 34655,61078,11,832,315,42482,596,77069,323,1455,73135,11335,11,10975,279,3446,315,279,46337,323,12280,12970,61078,11,889,65928,813,26135,11,439,568,1587,813,3611,38705,11,4184,311,52671,323,74571,13,61078,753,8060,439,264,7126,2995,360,3933,5678,323,813,1917,304,63355,323,31926,16134,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,1822,91,318,5011,91,29,15339,220,220,57668,53901,27,91,318,6345,91,29
  9. King Lear, one of Shakespeare's darkest and most savage plays, tells the story of the foolish and Job-like Lear, who divides his kingdom, as he does his affections, according to vanity and whim. Lear’s failure as a father engulfs himself and his world in turmoil and tragedy.<|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|>
  10. LumTokenizer_qwen150k
  11. 33555,59978,11,825,315,41382,594,75969,323,1429,72035,11088,11,10742,279,3364,315,279,45237,323,12011,12681,59978,11,879,64828,806,25079,11,438,566,1558,806,3527,37605,11,4092,311,51571,323,73471,13,59978,748,7901,438,264,6981,2922,360,3848,5561,323,806,1879,304,62255,323,30826,13,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645,151644,14990,220,220,108386,151645
  12. King Lear, one of Shakespeare's darkest and most savage plays, tells the story of the foolish and Job-like Lear, who divides his kingdom, as he does his affections, according to vanity and whim. Lear’s failure as a father engulfs himself and his world in turmoil and tragedy.<|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|><|im_start|>hello  你好<|im_end|>
复制代码
六、最后

LumTokenizer这个项目现在版本是1.0.6.1,整体效果较好,很快速稳定,现在自己训练模型就在用它,尽管目前某些常用习惯写死了,但大家需要的可自行适配和扩展。MiniGPT和MiniMind都是很好的LLM学习入门python项目,但C#基本没有。Tokenier是C#开发LLM的重要环节,奈何.Net生态还是差很多,资料也少,现在AI生成的内容都千篇一律,很多现有库更新的又很慢。真要用C#来干LLM真是难上加难(估计也没人这么干)。
如果您觉得有收获的话,请多多支持本系列。再次感谢您的阅读,本案例及更加完整丰富的机器学习模型案例的代码已全部开源,新朋友们可以关注公众号回复Tokenizer查看仓库地址,获取全部完整代码实现。


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册