Token的三世:大模型如何把文字变成可计算的向量

发布时间:2026/10/26 6:21:34
Token的三世:大模型如何把文字变成可计算的向量 最近在啃 MiniMind一个把大模型训练和推理逻辑完整保留、但模型规模压到极小的学习项目。起因很简单我不想只会调 API想亲眼看看大模型内部到底是怎么干活的。第一次打开源码第一个让我卡住的概念就是 Token。网上都说“Token 是大模型理解文字的基本单位”可这个回答太笼统了——单位为什么同一个 Token 一会儿是字符串一会儿是整数一会儿又变成一个高维向量我后来想明白一件事Token 在模型里其实要经历三种完全不同的形态可以理解成“前世、今生、来世”。这篇笔记就把这趟旅程从头到尾走一遍从分词、编号、向量化到 Transformer 里的注意力计算再聊几个实际运行时最容易踩的坑。适合正在读 MiniMind 这类代码、或者刚开始系统学大模型原理的朋友参考也欢迎在评论区拿你自己的报错来对比。1. 先回答一个基础问题Token 到底是什么1.1 从一段中文文本说起先别管那些抽象定义。打开 Python随便用一个本地分词器把一句话拆开看看会发生什么。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your_minimind_tokenizer_dir) text MiniMind 是怎么理解中文的 tokens tokenizer.tokenize(text) ids tokenizer.encode(text) print(tokens) print(ids)我这边实际跑出来的结果大概是这样的[Mini, Mind, 是怎么, 理解, 中文, 的, ] [8352, 28466, 7441, 11412, 10392, 878, 10821]注意这个输出不唯一。不同模型的词表是在不同语料上用 BPE 之类的算法训练出来的同一个句子在 GPT 系、Qwen 系、LLaMA 系的分词器里会被切成不同的片段ID 也各不相同。但规律是一样的模型不会一个汉字一个字去读而是把这段文字切成若干“词元”每个词元对应一个编号。这就是 Token 的“前世”它脱胎于原始字符串是分词器从连续文本里切出来的一个个小片段。1.2 模型为什么不能直接“读”文字这里要回答一个很自然的疑问神经网络不也是程序吗为什么不直接把字符编码成 ASCII 或 UTF-8 塞进去原因有三层。第一字符编码只代表字节值不代表语义。比如“苹果”的 UTF-8 字节和“apple”没有任何数值上的相关性模型如果直接用字节值做输入学不出“这两种写法指的是同一个东西”。第二文本长度不固定。一个句子有 5 个字另一个有 500 个字如果按字输入模型要处理长度可变的序列而神经网络计算往往需要固定维度或者至少有一个明确的基本单位。第三大模型训练和推理的复杂度跟序列长度强相关如果直接按字节处理序列会变得非常长算力开销会爆炸。Token 恰好解决了这三个问题它把变长的文本切成粒度适中的片段把每个片段映射成离散编号再转成向量。大模型对外表现是“读文字”本质上是在处理这些编号化之后的 Token。1.3 三代身份刚好构成“三世之旅”按我自己的理解一个 Token 在 MiniMind 里会经历三种形态字符串形态分词器输出的字符片段比如[Mini, Mind]。整数编号形态每个 Token 在词表里的 ID类似[8352, 28466]。高维向量形态Embedding 层把 ID 变成稠密向量再送进 Transformer 层参与注意力计算。全文的“三世”就是按这个顺序展开的。第一世是“字符串切片”第二世是“编号映射”第三世是“向量感知与上下文交融”。等这三世走完模型就能基于这个 Token 的所有信息去预测下一个 Token 的概率进而生成完整句子。2. 第一世从字符串到 Token——BPE 分词2.1 三种主流分词思路分词算法决定了一个句子会被切成多碎。最常见的思路有三种按词切分英文可以直接用空格和标点切中文就得先做分词库否则“武汉市长江大桥”这种句子会切出鬼故事。而且真实世界的新词、专有名词、乱码文本根本切不完。按字/字节切分绝对不会出现“未知词”但序列长度成倍增加。一个短句变成几十个 token模型要花更多计算量去学习“哪些字常一起出现”。子词切分界于上面两者之间高频词整体保留低频词拆成子词或字符。BPE、WordPiece、SentencePiece 都属于这类。MiniMind 这类用 HuggingFace 生态的项目分词器最常用的是字节级 BPEByte-Level BPE。它把文本先转成 UTF-8 字节然后不断合并出现频率最高的相邻字节对最后生成一份字节级别的子词表。这样做的好处是任何文本都能编码不会遇到“这个词没在词表里”的尴尬。2.2 BPE 到底怎么合并的理解 BPE 不需要看复杂的论文记住一件事就行反复合并出现次数最多的相邻符号对。用一个经典例子说明。假设语料是low low low low low lowest lowest初始时每个字符都是一个 tokenl,o,w,e,s,t。统计相邻对出现次数l o出现了 7 次最高于是合并成lo。接着lo w也出现 7 次合并成low。再看lowest内部e s出现 2 次合并成eses t出现 2 次合并成est最后low est出现 2 次形成完整词lowest。这个过程的伪代码大概长这样while 词表大小 目标大小: pair 统计所有相邻 token 对的出现频率 best 出现频率最高的那一对 把 best 合并成一个新 token 语料里所有 best 出现的地方都替换成新 token所以英文里常见的learn、learning、learner可能会被拆成learn加后缀因为learn作为高频片段会被保留下来。中文里“怎么”“理解”“我们”这类常见词组如果频繁出现也可能整体保留下。但如果语料里中文不够丰富大部分双字组合的频率拼不过英文高频片段中文就常常退化成单字切分。2.3 实操在 MiniMind 里看一次分词我建议你把 MiniMind 仓库里的 tokenizer 单独提出来做个最基础的自测from transformers import AutoTokenizer tk AutoTokenizer.from_pretrained(./minimind_tokenizer) samples [ 大模型是怎么理解文字的, Transformer attention is all you need, MiniMind 2024, ] for s in samples: print(s) print(tk.tokenize(s)) print(tk.encode(s)) print(token 数量:, len(tk.encode(s))) print(- * 60)这个脚本能帮你直观看到英文单词是不是整体保留、中文是不是被切碎、数字和空格怎么处理。我实际跑的时候中文句子被切成七八个 token英文短句也被切成七八个 token两者的 token 量差距并没有想象中那么大。原因是英文单词间的空格、标点、常见后缀本身也会占 token中文每个字虽然单独算但信息密度高。2.4 中文经常被拆很碎这不是 bug很多新手第一次看到大模型是怎么理解文字的被拆成大模型是怎么理解文字的会觉得分词器坏了。其实不是。词表大小是固定的比如 MiniMind 的词表可能只有 3 万到 5 万。英文里the、is、ing这种高频片段会抢走大量词表名额中文双字词里只有出现频率极高的组合才可能留下。剩下的中文内容拆成单字模型的注意力机制照样可以自行学习“哪几个字关系紧密”。拆细一点反而能处理更多未登录组合只是计算量会涨。所以我给大家一个建议不要纠结分词结果“像不像正常人切词”。BPE 的目标不是语言学上正确的分词而是让模型能以有限词表覆盖无限文本同时让高频模式获得更稳定的表示。3. 第二世从 Token 到整数 ID——词表映射3.1 词表就是一部“编码字典”第一世结束后句子变成了 token 字符串列表比如[Mini, Mind, ...]。可神经网络算不了字符串所以还得给每个 token 分配一个整数 ID。这个映射关系存在vocab.json里。文件内容大概长这样{ !: 0, \: 1, #: 2, $: 3, Mini: 8352, Mind: 28466, ... }键是 token 的字符串形式值是它在词表中的唯一编号。MiniMind 加载这类文件后tokenizer.encode(MiniMind)就会返回[8352, 28466]。这一步不需要训练是分词器训练完就定死的映射。关键点在于**词表一旦固定训练和推理必须使用同一份词表。**如果你中途换了词表Embedding 层的维度就对不上了模型就等于失忆了。3.2 特殊 Token 是“系统保留位”除了正常汉字英文词表里还会有一批特殊 Token。最常见的有[UNK]/unk遇到无法识别的字符时用它兜底。s//s或[BOS]/[EOS]标记序列开始和结束。pad把 batch 里长短不一的样本补齐到同一长度。对话类模型还有|im_start|/|im_end|这样的角色分隔符。MiniMind 使用对话模板时|im_start|user和|im_end|这些 token 都占真实 ID它们不是辅助信息而是与普通文本一起参与模型计算的。所以调用tokenizer.encode时要注意add_special_tokens参数。默认True时会自动加 BOS/EOS如果你在训练时默认加、推理时又手动加了一遍就会凭空多出两个 token找半天都发现不了。3.3 实操写一个 encode-inspect-decode 自检函数我之前调试 MiniMind 时写了一个小工具每次发现模型输出乱掉就先用它验证分词器有没有正常工作def inspect_tokenizer(text, tokenizer): enc tokenizer(text, add_special_tokensTrue, return_tensorspt) ids enc[input_ids].tolist()[0] print(原文:, text) print(decode 回来:, tokenizer.decode(ids)) print(token 数:, len(ids)) print(逐 token 查看:) for pos, (tok, tid) in enumerate(zip( tokenizer.convert_ids_to_tokens(ids), ids )): print(f{pos:4} | {repr(tok):15} | {tid})正常时decode回来的文本应该和原文一致若不一致说明特殊 token 处理或字节解码出问题了。逐 token 查看也能帮你确认一个 token 到底对应几个字很多长度问题就是在这里发现的。3.4 上下文窗口说的其实是 Token 数MiniMind 配置里的max_seq_len、block_size指的是 Token 数不是字数、不是字符数。很多项目说明写“上下文长度 2048”意思是模型最多处理 2048 个 Token。英文里平均一个单词大约 1.2 到 1.5 个 Token所以 2048 个 Token 大概能装 1500 个英文单词。中文一个汉字经常就是一个 Token所以 2048 个 Token 大概能装 1800 到 2000 个汉字。如果直接按“字符数”配置很容易超长。MiniMind 在数据预处理阶段也会按max_seq_len截断样本。比如你有一批长文本先用 tokenizer 编码然后只保留前 1024 个 Token多余的直接丢掉。做微调时如果发现显存不够第一件事就是看max_seq_len是不是设太大了而不是急着换模型。4. 第三世从 ID 到高维向量——模型真正“理解”的地方4.1 Embedding给每个 Token 发一张“语义身份证”第二世结束时每个 Token 是一个整数 ID但这个整数本身没有任何语义。你说苹果 100香蕉 200模型不会根据 100 和 200 的数值差猜出它们都是水果。数字大小在这里毫无意义。于是要进入第三世的第一步Embedding也就是查表。PyTorch 里就是一行代码import torch import torch.nn as nn vocab_size 32000 hidden_size 768 embed nn.Embedding(vocab_size, hidden_size) ids torch.tensor([[8352, 28466, 7441]]) vectors embed(ids) print(vectors.shape) # torch.Size([1, 3, 768])这里每个 Token ID 会被换成 768 维的浮点向量。初期这些向量是随机的但随着模型训练Embedding 矩阵会不断调整让语义接近的 Token 在向量空间里距离更近。“国王”和“王后”、“喜欢”和“热爱”逐渐会落在相近位置。可以把这个过程想成一本巨大的语义词典查“MiniMind”得到一串语义特征查“Token”得到另一串。模型后续的所有计算都在这串特征上进行。4.2 为什么不用 one-hot 编码有人会问直接用 one-hot 向量不是也能区分 Token 吗用是能用但非常不划算。词表如果是 3 万一个 one-hot 向量就是 3 万维而且任何两个不同的 one-hot 内积都是 0意味着 Token 之间完全没有相关性。“猫”和“狗”这两个 ID 在这个空间里是正交的模型看不到它们之间的任何相似性。而密集的 Embedding 向量不同它在训练中学会了相似 Token 的向量方向接近这才是模型能“推理词义”的基础。所以 Embedding 层不仅是查表它是一场从离散符号到连续语义空间的投影。这一步做完Token 才真正从“死编号”变成“活向量”。4.3 在注意力机制里Token 开始互相沟通单个 Token 的向量再漂亮也只是静态特征。真正让大模型理解一句话的是 Token 与 Token 之间的交互这个交互主要由自注意力完成。我们可以用开会的场景来类比。假设会议室里有 5 个 Token每个人都先写好三个小纸条我要去哪找信息Query、我能提供什么信息Key、我实际想说的内容Value。接下来每个 Token 都拿自己的 Query 去跟所有人的 Key 做匹配。匹配度越高注意力权重越大最后把所有人的 Value 按这个权重加权求和作为自己的新向量。也就是说模型在处理“它”这个 Token 时会通过注意力机制去关注前文里的“苹果”把“苹果”的信息融合到“它”的向量里。这个过程在每个 Transformer 层都会做一次层次越深Token 能“看到”的上下文范围越广。注意力权重计算公式可以写成Attention(Q, K, V) softmax(Q K^T / sqrt(d_k)) V其中d_k是 Key 向量的维度除以sqrt(d_k)是为了防止点积过大把 softmax 梯度压没。对新手来说记住前面那张“开会互相对答案”的图就够了背后的矩阵乘法只是把这个过程规模化。4.4 MiniMind 里 Token 张量形状的变化我用的 MiniMind 版本默认配置大概是n_layer8、n_head8、n_embd512、block_size512。不同分支配置有差异但数据流转逻辑是一样的。输入一批文本后张量形状是这么变的输入 ids[batch, seq_len] → Embedding[batch, seq_len, hidden_size] → 8 层 TransformerBlock[batch, seq_len, hidden_size] → LM Head[batch, seq_len, vocab_size] → softmax每个位置下一个 Token 的概率分布seq_len就是这一批样本里最长的 Token 数所有样本会被 pad 到等长。hidden_size512表示每个 Token 最终用 512 维向量表示。vocab_size是词表大小LM Head 的输出维度必须和词表一致因为我们要给每个候选 Token 打分分数最高或按概率采样到的 ID就是模型生成的下一 Token。这也解释了一个现象为什么生成时是一个 Token 一个 Token 往外蹦。因为模型每前向一次只能根据当前上下文预测下一位。预测出来的 ID 再拼到输入末尾再前向一次才能预测再下一位。看起来模型在输出文字其实它是把这些 Token 一个个“续写”出来的。4.5 Token 的解码返场第三世走完后Token 还要变回能读的文字。这一步发生在 LM Head 输出之后next_token_id int(logits[0, -1, :].argmax()) next_token_text tokenizer.decode([next_token_id])decode并不只是查表它需要把子词 token 拼接起来还要处理字节级分词器留下的中间状态。比如某个 token 是半个汉字另一个 token 是后半截decode会先把它们拼成完整 UTF-8 字节再转成字符。如果直接拼接字符串往往会出现乱码。所以“模型理解了文字”这个说法的真相是文字先被切碎成 Token再变成 ID再变成向量向量经过一系列注意力交互后最终映射回另一个 Token 的概率分布再解码回字符串。整个过程都是可计算的统计过程不存在什么玄学理解。5. 常见问题与排查实录5.1 中文被拆得很碎影响模型效果吗我在 MiniMind 上用中文数据微调时最直观的感受是中文 Token 更碎序列更长同一条数据比英文多用大约 20% 到 50% 的 Token。这不一定影响语义理解因为注意力机制可以让单字 Token 自由组合出词义但会拖慢训练速度、增加显存占用。如果你是在别的模型基础上做中文增强不要轻易换词表。换词表后原模型的 Embedding 权重全部失效等于从零开始学中文很可能越调越差。更稳妥的做法是把新语料在现有分词器上跑一遍缺失的高频中文组合用额外 Embedding 增量训练做扩展这属于进阶玩法初学者慎碰。5.2 出现 [UNK] 或乱码先查分词器[UNK]出现的原因通常是三个词表里没有对应字符、加载的分词器文件不完整、特殊 Token 映射缺失。常见场景是拿一个只支持英文的 tokenizer 去怼中文结果中文全变成[UNK]。解决方案是先跑我前面写的inspect_tokenizer函数确认decode能不能还原原文。至于解码乱码问题往往出在字节级 BPE 上。字节级词表存的是 UTF-8 字节片段直接用字符串拼接会拆坏多字节字符。正确做法是始终用官方提供的tokenizer.decode方法不要自己拼接 token 文本。5.3 序列超过 max_seq_len 怎么办训练时如果样本太长模型会报维度错误或者悄无声息地截断。MiniMind 的数据加载里通常有max_seq_len参数超长样本会被截尾。但这会带来一个副作用样本的答案部分被切掉了模型学了个寂寞。我的习惯是在构造样本时就分层处理。普通段落超过限制就做滑动窗口切块每块留一点重叠如果是对话样本保证最后一条回复完整宁可把历史对话往前截也不截掉当前回复。代码里可以这样def truncate_to_fit(input_ids, max_len, keep_last_ratio0.6): if len(input_ids) max_len: return input_ids keep_last int(max_len * keep_last_ratio) keep_first max_len - keep_last return input_ids[:keep_first] input_ids[-keep_last:]这个函数不是标准做法是我自己实验下来效果比较稳的折中方案。核心思想是开头上下文可以牺牲一点结尾的答案尽量完整。5.4 “Token 失效”“403”跟模型里的 Token 没关系搜索 Token 相关问题时会撞到大量报错token expired token exchange failed token endpoint returned status 403这些报错发生在登录第三方服务、调用 API、刷新授权时说的是“访问令牌Access Token”失效了。它跟大模型里的 Token 完全是同名异物。访问令牌是一种身份凭证常见实现是 JWT。它的exp字段记录过期时间过期后继续使用就会返回 401 或 403。排查思路很直接检查系统时间是否准确、JWT 的密钥和签名算法是否一致、有没有用 refresh token 换新如果是 OAuth 流程里 token endpoint 报 403优先检查 client_id、client_secret、回调地址这些配置。而大模型里的 Token 报错则应该看分词器加载、词表路径、序列长度。两者别混在一起查。我见过不少人把“tokenizer 加载失败”和“登录 token 失效”当成一个问题搜半天最后发现根本没有关联。这个坑值得专门提醒。5.5 实用排查速查表现象属于哪类 Token常见原因排查方向打印出来全是[UNK]模型词元分词器与文本语言不匹配使用支持中文的词表decode 乱码模型词元字节级 BPE 拼接错误必须用官方 decode 方法显存爆掉模型词元max_seq_len太大缩短序列长度训练效果差模型词元截断截掉了答案保留尾部关键内容API 返回 token expired访问令牌JWT 过期刷新凭证登录时 403访问令牌回调地址或客户端配置错误检查 OAuth 配置6. 一点个人实操心得学 MiniMind 最值的一步是把 tokenizer 单独拆出来玩透。我强烈建议你找个半天时间把仓库里的 tokenizer 加载起来用它编码几段中文、英文、甚至带上特殊符号的文本逐行打印 token 和 ID再 decode 回去看看会不会还原。这个过程看起来简单但能帮你把“Token 三世”串起来之后再去看 Transformer 代码心里就有底了。我个人的一个小技巧在理解注意力之前先拿一个很短很短的输入比如我喜欢 MiniMind通过在 MiniMind 的 forward 里临时加几个调试打印观察它的张量是怎么从[1, 6]变成[1, 6, 512]又怎么变成[1, 6, vocab_size]的。每看一次你都会对“模型如何处理文字”多一层体感。如果把这一圈走完再回头看开头的问题“大模型怎么理解和处理文字”我的回答是它先把文字切成 Token把 Token 变成可计算的 ID 和向量再靠注意力机制让这些向量在上下文里互相融合最后从概率分布里采样下一个 Token。整个过程没有任何玄学只是逐步把文字这个符号系统翻译成了矩阵和浮点数的数学世界。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询