news 2026/4/15 12:17:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#AI系列(7):从零开始LLM之Tokenizer实现

一、前言: token是什么

LLM只做一个事情,就是吃掉token吐出token,token是LLM(大语言模型)的基本元素。token与LLM的关系,相当于乐高积木与乐高工厂,我的世界方块与我的世界游戏。那么token到底是什么呢?有人翻译成令牌,有人翻译成词源。我们不妨换个概念理解,token就是最小操作、最小信息单元的意思。这个最小是相对于LLM要处理的原始文本来说的。

举个栗子,当一个句子文本输入到电脑中,天然就就具有字符级别的切分。如果不打算继续拆分或组合,我们可以通过一个映射关系,将现有这些字符转换为整数数组,称为编码过程。编码后数组内的元素就是token,元素取值就等于token取值。LLM可以吃掉这个token数组,并吐出新数组。对这个新数组按前前述的映射进行逆转换,称为解码过程。解码后我们就能得到人类可以理解的文本了。

/* by 01022.hk - online tools website : 01022.hk/zh/calorie.html */ // 原句子 "我有一个 apple." // 句子拆分 ["我","有","一","个"," ","a","p","p","l","e",".","\0"] // 编码为整数数组 [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(字节对编码)

字节对编码 是一种简单的数据压缩形式,这种方法用数据中不存的一个字节表示最常出现的连续字节数据。这样的替换需要重建全部原始数据。编码过程如下:

/* by 01022.hk - online tools website : 01022.hk/zh/calorie.html */ // wiki的BPE案例 "aaabdaaabac": "aa"=>"Z" //“aa”出现次数最多,用中没有出现的“Z”替换 "ZabdZabac": "aa"=>"Z", "Za"=>"Y" //同上,更新替换表 "YbdYbac": "aa"=>"Z", "Za"=>"Y", "Yb"=>"X" //同上,更新替换表 "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可能会有字符串和数组两种对象存储方式,解析时候需要注意):

// cl100k_base { "version": "1.0", "truncation": null, "padding": null, "added_tokens": [ //特殊token { "id": 100257, "content": "<|endoftext|>", "single_word": false, "lstrip": false, "rstrip": false, "normalized": false, "special": true } ], "normalizer": null, "pre_tokenizer": { // 有的有,有的没有,因此regex需要预先硬编码 "type": "Sequence", "pretokenizers": [ { "type": "Split", // 预处理分割,“防呆尺” "pattern": { "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+" }, "behavior": "Removed", //一般默认写死,命中正则的片段保留,没命中的扔掉(与invert 配合)。 "invert": true //一般默认写死,把“命中/没命中”反转——最终只保留上面正则抓到的那些片段,其余全部丢弃。 }, { "type": "ByteLevel", "add_prefix_space": false, "trim_offsets": true, "use_regex": false } ] }, "post_processor": null, "decoder": { "type": "ByteLevel", "add_prefix_space": true, "trim_offsets": true, "use_regex": true }, "model": { "type": "BPE", "dropout": null, "unk_token": null, "continuing_subword_prefix": "", "end_of_word_suffix": "", "fuse_unk": false, "byte_fallback": false, "vocab": { "!": 0, "<|endofprompt|>": 100276 }, "merges": [ "ĠCon veyor" // 或 ["Ġp","ain"] ] } }

通过读取预先的数据,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. 读取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 词表
  2. 高效的特殊token切分:如果是模型训练用,tokenizer需要单独高效处理特殊token。因为特殊token的目的是正文出现越少越好,因此一般不会出现在词表中,需要通过单独切分的机制进行识别和切分。
  3. 高效的缓存机制:LumTokenizer 在分词阶段,订制了一套SpanDictionary, 为了实现高效的切片搜索,也就是说一个stirng可以基于NET的Span特性切成多个Slice,而SpanDictionary可以直接基于Span 执行Key的匹配(Span无法作为传统Dictionary的泛型),极大节省了子串string转换的开销。

Benchmark测试如下:在含有中文这种多字节字符的长文(500字符左右)处理时,具有很好的性能。

MethodtextMeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
SharpToken_cl100k_baseChinese122.99 us2.314 us2.273 us5.710.120.73249.1 KB1.19
TiktokenSharp_cl100k_baseChinese96.00 us1.829 us2.106 us4.450.110.48836.34 KB0.83
LumTokenizer_cl100k_baseChinese21.56 us0.268 us0.251 us1.000.020.61047.63 KB1.00
SharpToken_cl100k_baseEnglish26.77 us0.520 us0.639 us1.020.030.67148.38 KB0.74
TiktokenSharp_cl100k_baseEnglish20.21 us0.383 us0.376 us0.770.020.42725.51 KB0.49
LumTokenizer_cl100k_baseEnglish26.13 us0.495 us0.509 us1.000.030.915511.31 KB1.00
SharpToken_cl100k_baseMixed90.97 us1.580 us1.478 us3.780.090.854510.9 KB1.23
TiktokenSharp_cl100k_baseMixed63.85 us1.274 us1.564 us2.650.080.48836.74 KB0.76
LumTokenizer_cl100k_baseMixed24.08 us0.465 us0.435 us1.000.030.70198.83 KB1.00

具体可以去仓库看详细代码。

[MemoryDiagnoser] public class CompareBenchmark { internal GptEncoding _sharpToken; internal TikToken _tikToken; internal BPETokenizer _tokenizer1; internal BPETokenizer _tokenizer2; [GlobalSetup] public void Setup() { _sharpToken = GptEncoding.GetEncoding("cl100k_base"); _tikToken = TikToken.GetEncodingAsync("cl100k_base").ConfigureAwait(false).GetAwaiter().GetResult(); _tokenizer1 = BPETokenizer.CreateTokenizer( @"D:\Data\Personal\AI\llm\tokenizer\cl100k.txt", true, RegexType.RegexCl100KBase); _tokenizer2 = BPETokenizer.CreateTokenizer( @"D:\Data\Personal\AI\llm\tokenizer\qw_tokenizer.json", false, RegexType.RegexCl100KBase); } // ====== 1. 声明参数源 ====== public IEnumerable<string> TextSamples() { yield return TextCatalog.English; yield return TextCatalog.Chinese; yield return TextCatalog.Mixed; } // ====== 2. 每个方法改成带参数 ====== [Benchmark] [ArgumentsSource(nameof(TextSamples))] public int SharpToken_cl100k_base(string text) { var encoded = _sharpToken.Encode(text); var decoded = _sharpToken.Decode(encoded); return encoded.Count; } [Benchmark] [ArgumentsSource(nameof(TextSamples))] public int TiktokenSharp_cl100k_base(string text) { var encoded = _tikToken.Encode(text); var decoded = _tikToken.Decode(encoded); return encoded.Count; } [Benchmark(Baseline =true)] [ArgumentsSource(nameof(TextSamples))] public int LumTokenizer_cl100k_base(string text) { var encoded = _tokenizer1.Encode(text, false); var decoded = _tokenizer1.Decode(encoded, false); return encoded.Count; } public int LumTokenizer_qwen150k(string text) { var encoded = _tokenizer2.Encode(text, false); var decoded = _tokenizer2.Decode(encoded, false); return encoded.Count; } }

五、单元测试

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

  • P0_BasicTest:基础测试,测试编解码,数据读取,词表完善性等主要功能;
  • P1_RobustnessTests:鲁棒性测试,针对边缘条件,如仅空字符、仅特殊字符、超长文本、越界id等情况;
  • P2_VocabBpeTests:编解码准确性,要求正确的对原文进行分割,并准确编码,通过几种特定情况下的案例进行兜底。
  • P3_ChineseSubwordTests:中文字符测试,其中也包含了token压缩率的检验。主要是考虑在代码编写过程中,可能导致部分尾字节或特殊混编情况下不能准确字节合并的bug。
  • P4_EnglishSubwordTests:英文字符测试,目的同上,部分bug出现时,尽管decode正常,但encode编码也可能未达到预期(忽略了某些合并环节导致压缩率过高)。

编解码准确度与常用库比较:

LumTokenizer_cl100k_base 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 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|> SharpToken_cl100k_base 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 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|> TikTokenr_cl100k_base 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 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_qwen150k 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 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查看仓库地址,获取全部完整代码实现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/8 0:02:19

12.1 图像生成革命:CV算法与AIGC工具应用场景分析

12.1 图像生成革命:CV算法与AIGC工具应用场景分析 在前面的章节中,我们深入探讨了Agent技术及其在各种应用场景中的实现。从本章开始,我们将转向另一个重要的AIGC领域——图像生成技术。图像生成作为AIGC的重要分支,正在深刻改变创意产业和多个垂直领域。 今天,我们将首…

作者头像 李华
网站建设 2026/4/10 4:42:09

Excalidraw支持物联网设备组网图

Excalidraw&#xff1a;用一句话画出物联网组网图 在一次智能农业项目的远程会议中&#xff0c;产品经理刚说完“每个大棚有三个温湿度传感器&#xff0c;通过LoRa网关传到云端”&#xff0c;工程师已经在共享白板上点下回车——几秒钟后&#xff0c;一张包含传感器、网关和云服…

作者头像 李华
网站建设 2026/4/14 21:00:48

Open-AutoGLM性能优化秘籍:响应速度提升300%的底层逻辑

第一章&#xff1a;Open-AutoGLM消息智能回复的性能革命 Open-AutoGLM作为新一代开源智能消息回复系统&#xff0c;通过融合大语言模型推理优化与动态上下文感知技术&#xff0c;在响应速度、准确率和资源利用率三大维度实现了显著突破。其核心架构采用异步流式处理机制&#x…

作者头像 李华
网站建设 2026/4/3 7:23:52

Excalidraw支持二维码嵌入生成

Excalidraw 支持二维码嵌入生成 在数字协作的浪潮中&#xff0c;一张草图早已不再只是静态表达。当团队围坐在虚拟白板前讨论架构、梳理流程或设计原型时&#xff0c;真正高效的工具不仅要“画得清楚”&#xff0c;更要“连得上上下文”。Excalidraw 作为近年来广受开发者青睐的…

作者头像 李华
网站建设 2026/4/13 5:19:54

11.1 开发平台先行:Agent Studio大模型开发环境配置

11.1 开发平台先行:Agent Studio大模型开发环境配置 在前面的章节中,我们深入探讨了RAG技术及其在各种业务场景中的应用。从本章开始,我们将进入另一个重要的AIGC技术领域——Agent技术。Agent技术代表了AI应用的一个重要发展方向,它使得AI系统能够更加自主地执行复杂任务…

作者头像 李华
网站建设 2026/4/15 7:18:19

Excalidraw多人光标显示优化

Excalidraw 多人光标显示优化 在远程协作日益成为常态的今天&#xff0c;一个看似微小却至关重要的设计细节&#xff0c;往往决定了团队协作的流畅度——那就是“别人此刻正在做什么”。当多人同时编辑同一块白板时&#xff0c;如果无法实时感知彼此的操作位置和意图&#xff…

作者头像 李华