文本是怎么变成一串数字、再变成计算机能理解的东西的这个问题几乎每个刚接触大模型的人都会撞上。不少人看完架构图、注意力机制之后回头一看“分词”和“嵌入”这两个最底层的环节反而觉得有点飘分词就是把句子切开吗嵌入不就是查表吗听起来简单但里面坑不少而且这两步直接决定了模型能“看懂”什么、学得怎么样。这篇内容是大模型语言基础系列的第二章专门拆解文本数字化的完整链路先讲分词怎么把文本切成token再讲嵌入怎么把这些离散符号变成稠密向量最后带你把整条链路在代码里跑一遍。无论你是想理解模型原理、准备微调还是只是好奇大模型为什么“懂”语义这部分都是绕不开的地基。1. 为什么不能直接拿字符编码当输入很多人的第一反应是计算机里文本本来就是数字ASCII码、Unicode码点都是数字为什么不直接拿这些数字喂给模型这个问题问得特别好想明白它你就能理解分词和嵌入存在的意义。1.1 字符编码解决的是“存储”问题不是“语义”问题ASCII码告诉你字母a是97Unicode告诉你汉字“中”的码点是20013。这个数字只负责唯一标识一个字符它不携带任何含义。你拿97和20013去计算相似度得到的结论毫无意义——a和b在ASCII里差1a和Z差25这种数值关系跟语言本身一点关系都没有。更要命的是字符编码的分布是极其稀疏且无规律的。全世界的文字有几万个常用字符如果直接用码点当特征那等于在一个几万维的空间里每个字符只有一个维度有值其他全是0。这种表达方式在机器学习里有个专门的名字one-hot编码。它不是不能用而是一用就废——维度爆炸、数据稀疏、没有任何相似性可言模型根本学不动。1.2 语义相似必须靠“学习出来的向量”而不是“查出来的编码”真正可行的方案是让模型自己学一套映射把每个词或子词映射到一个固定维度的稠密向量。这个向量的每一个维度不是人为指定的而是模型在海量语料里通过预测任务一点一点调整出来的。于是“猫”和“狗”的向量距离近“猫”和“汽车”的向量距离远这种语义关系是数据驱动出来的不是拍脑袋定出来的。这个思路听起来直接但它要求你先回答一个问题模型处理的“基本单位”到底是什么是一个字一个词还是半个词这就回到了分词。注意分词和嵌入不是两个孤立步骤。分词决定了词表长什么样、模型能表示哪些文本单元嵌入决定这些单元怎么映射成向量。词表没切好后面的嵌入学得再好也白搭嵌入没学透分词再合理也表达不出语义。2. 分词到底在切什么从整句到Token分词Tokenization是把原始文本切分成一个个Token的过程。Token不是一个天然存在的语言单位它是你为了喂给模型而人为定义的文本片段可以是一个完整的词如“模型”、半个词如“learning”切出的“learn”和“ing”甚至是一个单字符。2.1 词表大小与覆盖率的博弈如果你按整词切好处是每个Token语义完整坏处是词表会失控。中文少说几十万个词英文的变形、复合词、专有名词更是无穷无尽。词表太大模型的嵌入层和输出层参数会爆炸——嵌入矩阵的大小是“词表大小×隐藏维度”词表100万、隐藏维度4096光这一层就是40多亿参数训练和推理都扛不住。如果按单字或单字符切词表是控制了但每个Token承载的信息太少模型需要很深的层数才能把相邻Token组合出词义训练效率直线下降。而且英文里一个词被切成好几个字符序列长度急剧拉长Transformer的自注意力计算量是序列长度的平方序列太长直接算不动。2.2 BPE算法字节对编码的合并逻辑目前最主流的分词方案是子词切分核心思想是常用词保持完整生僻词拆成更小的片段词表里放的是高频子词单元。这里必须讲清楚BPEByte Pair Encoding的合并逻辑因为它是GPT系列模型分词器的基础。BPE的流程说起来不复杂先把语料里每个词拆成一个个字符并在词尾加一个特殊结束符防止不同词的相同字符序列被错误合并。统计所有相邻字符对的出现频率。选出频率最高的一对合并成一个新的子词单元加入词表。重复第2、3步直到词表达到预设大小。举个例子“低”和“成本”如果经常相邻出现算法会先把“低”和“成”合并成“低成”如果这个组合出现频率够高再合并出“低成本”最终“低成本”作为一个整体进入词表。合并的次数由词表大小决定你可以把BPE理解成用有限次合并把语料里最常用的组合尽量收进词表剩下的就退化成更短的片段甚至单字符。实操心得BPE训练完以后真正做推理切分时是“按词表最长匹配优先”切还是“按合并规则逐步还原”不同分词器实现不一样。用Hugging Face的tokenizer时不需要关心这个细节但如果你自己实现分词器一定要理清训练和推理两套逻辑这是最容易出错的地方。2.3 WordPiece和Unigram另两条技术路线BPE是GPT系列的选择WordPiece是BERT系列的选择。两者非常像唯一的区别是合并的评判标准不同BPE看“相邻字符对出现的频率”WordPiece看“合并后能多大程度降低语料的不确定性”也就是用互信息类的指标选合并项。效果上两者差别不大WordPiece对某些语言的词根切分更精细一些。Unigram则是另一条思路它不是从字符开始往上合并而是从一个超大的候选词表开始不断删除那些对整体损失贡献最小的子词单元直到词表收缩到目标大小。它天生支持“一个词有多种切分方式”推理时选概率最高的一种。SentencePiece这个库把BPE、Unigram、WordPiece都实现了还额外解决了“原始文本没有空格”的语言比如中文、日文的预处理问题直接在原始文本上训练不需要预分词。2.4 中英文分词的显著差异英文天然有空格分词BPE处理英文相对省事。中文没有空格分词难度大得多这也是中文NLP长期以来的经典难题。大模型里处理中文通常两种做法一种是按单个汉字切分配合BPE合并常见字组另一种是先按字切再统计合并“字”、“词”、“词组”层次不够分明时就退回到单字。这带来一个实际影响同一个中文句子在不同分词器下切出的Token数量和边界完全不同。中文里一个汉字大概对应0.6到1个Token取决于分词器和词表英文一个单词平均对应1.3个Token左右。这意味着API计费时同样一段意思的中英文内容Token消耗是不同的做成本估算时要注意这个差异。3. 嵌入把Token变成有语义的向量分词产出的是一串整数ID每个ID对应词表里的一个Token。比如“我”是120、“爱”是531、“编程”是8962。但前面说了这些整数本身没有语义意义ID大小不表示任何关系。嵌入层要做的就是把这些离散ID映射成稠密向量。3.1 从one-hot到嵌入矩阵模型里实际做的是查表操作。假设词表大小是V嵌入维度是D那么嵌入层就是一个形状为(V, D)的矩阵。输入Token的ID是3就从矩阵第3行取出一个D维向量作为这个Token的初始表示。整个过程跟字典查询一样所以也叫查表Lookup。这个矩阵不是预先设计好的初始化时是随机数然后在训练过程中不断更新。更新信号来自模型的语言建模任务——预测下一个词时猜错了就根据误差调整嵌入矩阵里的对应行向量让这个词的向量逐渐向“它经常出现的上下文方向”靠拢。这就是分布式表示的核心思想一个词的语义不是靠规则定义的而是靠它在海量上下文中的分布模式定义出来的。3.2 嵌入空间的几何性质当嵌入训练到位后高维向量空间里会浮现出清晰的语义结构。经典例子是“国王-男女≈王后”通过在向量空间里做加减法能近似得到有意义的类比结果。这个现象说明嵌入向量不仅仅编码了词义还在维度方向上编码了一些语义属性。实际操作中怎么度量两个Token的语义接近程度通常用余弦相似度也就是两个向量夹角的余弦值值越接近1说明方向越一致。欧氏距离也能用但受向量模长影响语义相似但模长差异大的两个词可能距离很远所以实践中余弦相似度更常用。这里有个容易混淆的点嵌入向量表征的是“静态语义”也就是词在训练语料中的平均用法而不考虑它在当前句子里的具体含义。同一个词在不同语境下的不同含义静态嵌入是区分不开的这是它和上下文表示的界限。3.3 嵌入维度怎么选小了欠拟合大了浪费嵌入维度是模型设计的一个超参数BERT-base是768维GPT-2是768维GPT-3是12288维不同规模模型差异很大。维度太小表达能力不够语义差异压不进这么小的空间维度太大参数暴涨优化难度增加训练数据不够时还容易过拟合。业界经验上有一个大致的数量级对应关系但具体取值主要靠实验和算力预算没有绝对最优。提示嵌入维度是不少模型的“隐藏参数瓶颈”。有些模型的核心参数看着很大其实大头就在词表×嵌入维度这个矩阵上。如果词表特别大嵌入层可能占总参数的一半以上。这也是为什么大模型里开始出现“权重共享”和“低秩分解”这类压缩嵌入层的技术——嵌入层太肥了。4. 位置信息为什么嵌入之后还要再加位置编码现在你已经有了每个Token的向量表示但如果直接把这一串向量送进Transformer模型会分不清“我打你”和“你打我”的差别——自注意力机制本身对输入顺序是不敏感的它看到的是一个集合不是序列。这就是位置编码存在的根本原因。4.1 绝对位置编码BERT时代的老办法最早的做法是给每个位置分配一个固定的或可学习的向量然后加到Token嵌入上。BERT用的是可学习的绝对位置编码位置0、位置1、位置2……各有一个独立的嵌入向量。这种方案简单直接但有两个短板一是模型没见过比训练时长更长的序列时表现不稳定外推能力差二是它只告诉模型“第几个位置”没有显式建模“两个Token之间隔了多远”而距离信息对捕捉局部依赖很重要。4.2 相对位置编码与旋转位置编码新模型的选择后来出现的相对位置编码不再关心绝对位置而是把“两个Token之间的相对距离”直接注入注意力计算。这样模型天然知道相邻Token关系近、隔得远的Token关系远外推性也比绝对位置好。现在大模型里最流行的是旋转位置编码RoPERotary Position Embedding。它的做法非常巧妙把每个Token的向量按位置旋转一个角度两个Token的注意力分数就自动依赖它们的相对距离。这个方案同时具备了绝对位置的实现简单和相对位置的语义优势而且对长文本的外推性特别好。当前很多主流大模型都在用这个方案。4.3 长文本场景下位置编码是瓶颈位置编码直接决定模型能处理多长的序列。早期BERT是512个Token现在的模型动辄支持几万甚至几十万Token的上下文窗口绝大部分靠的是位置编码方案的改进而不是简单堆算力。如果你要做长文本应用比如读长文档、分析多轮对话务必要确认模型的位置编码方式支持什么长度有些模型强行超过训练长度后效果会断崖式下跌不是显存不够而是位置编码没见过那么长的位置。5. 完整链路实操从文本到向量的代码走读理论说再多不如亲手跑一遍。下面我用Hugging Face的Transformers库把一个句子从原始文本变成模型能用的输入再到嵌入向量完整走一遍。5.1 环境准备与模型加载pip install transformers torch然后加载一个较小的模型来验证链路。这里用一个小型模型做示例方便观察中间结果from transformers import AutoTokenizer, AutoModel import torch model_name bert-base-uncased tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name)5.2 分词与ID化看看文本变成什么text Tokenization is the first step to make text readable for machines. encoded tokenizer(text) print(encoded.input_ids) print(encoded.attention_mask) print(tokenizer.convert_ids_to_tokens(encoded.input_ids))你会看到类似这样的输出input_ids是一串整数开头是101[CLS]结尾是102[SEP]中间每个数字对应一个Token。convert_ids_to_tokens能让你把它们还原成可读的Token观察一下“Tokenization”被切成了几个片段“readable”又是怎么切的。这一步是理解整条链路的起点分词器负责把文本切Token并映射IDattention_mask负责标记哪些位置是真实文本、哪些是填充位。注意同一个句子用不同分词器Token切分结果完全不同。你可以把model_name换成GPT系列模型再跑一遍对比input_ids的变化。这是理解“分词器决定模型视角”最直观的实验。5.3 获取嵌入向量inputs tokenizer(text, return_tensorspt) with torch.no_grad(): outputs model(**inputs) token_embeddings outputs.last_hidden_state print(token_embeddings.shape)输出形状是(1, 序列长度, 768)这就是模型对这段文本的完整表示每个Token经过嵌入层之后又经过多层Transformer编码变成了一个768维的向量。这里的向量已经不是静态嵌入而是“上下文相关”的向量——同一个词在“bank”和“river”旁边时向量完全不同。如果你想单独看静态嵌入层的输出可以绕过Transformer层直接取嵌入embedding_layer model.embeddings.word_embeddings word_embeddings embedding_layer(torch.tensor(encoded.input_ids)) print(word_embeddings.shape)对比一下word_embeddings和last_hidden_state的差异前者的每个Token向量是固定的不随上下文变化后者已经经过多层语义融合随上下文变化。这个对比能帮你直观理解“静态嵌入”和“上下文表示”的区别。5.4 自己算一遍余弦相似度为了让你对“语义距离”有体感我建议做个简单的相似度实验token_embed model.embeddings.word_embeddings cat_id tokenizer.convert_tokens_to_ids(cat) dog_id tokenizer.convert_tokens_to_ids(dog) car_id tokenizer.convert_tokens_to_ids(car) cat_vec token_embed(torch.tensor(cat_id)) dog_vec token_embed(torch.tensor(dog_id)) car_vec token_embed(torch.tensor(car_id)) def cos_sim(a, b): return torch.dot(a, b) / (torch.norm(a) * torch.norm(b)) print(cos_sim(cat_vec, dog_vec)) print(cos_sim(cat_vec, car_vec))正常情况下“cat”和“dog”的相似度明显高于“cat”和“car”。这就是嵌入层从语料中学到的语义结构在实际向量空间中的体现。6. 常见问题与排查技巧实录这部分是我在实际使用中被问得最多的问题也踩过不少坑整理出来给你排雷。6.1 词表外词OOV怎么处理再大的词表也有覆盖不到的词尤其是专有名词、网络新词、代码变量名。BPE类分词器对OOV词的处理方式是不断切分直到所有子片段都在词表里实在不行就退化到单字符或字节级。所以你在使用中会发现一个很长的生僻词可能被切成七八个Token导致Token数暴涨、计费增加、模型对这个词的理解变差。排查方法如果发现某个句子的Token数量异常多先检查是不是被切碎了。用tokenizer.convert_ids_to_tokens打印出来看看大多数时候你会发现生僻词被拆得到处都是。缓解方式要么换用词表更大的模型要么在微调时让模型多见识这些词。6.2 padding方向和attention_mask的作用batch训练时不同句子长度不一样需要padding到相同长度。这里有个关键细节padding方向右侧还是左侧会影响模型效果。对BERT这样的双向模型padding在哪边问题不大对GPT这样的自回归模型padding必须在左侧因为模型只能看到当前位置左侧的内容右侧padding会让模型被迫预测“填充符”浪费训练信号。attention_mask的作用就是告诉模型哪些Token是真实文本、哪些是填充符计算损失时要忽略填充位的预测。如果忘了设置attention_mask模型会把padding当真实文本一起建模轻则浪费算力重则污染表示。6.3 “嵌入维度越大越好”是误区直观上觉得维度大信息多但实际上维度大到一定程度后收益急剧下降。GPT-3的嵌入维度是12288但这是和超大规模训练数据、超大词表配套的。小模型强行上大维度往往表现为训练不稳定、过拟合、推理变慢。选型时更合理的思路是模型越大、数据越多嵌入维度才应该越大它不是一个独立调节的旋钮。6.4 Token数量与字符数量的换算做成本估算的时候这件事很实用。英文里1个词约等于1.3个Token中文1个字符约等于0.6到1个Token具体取决于分词器。不同模型之间的差异也不小同一个句子在不同模型的分词器下Token数量可能差出20%。所以跨模型对比API价格时不要只看单价要看Token消耗速度。6.5 一个容易忽视的细节Special Token对嵌入的影响[CLS]、[SEP]、[PAD]这些特殊Token也会参与嵌入训练它们占用词表名额也可能干扰语义。比如[PAD]向量如果训练得不够好注意力计算时模型可能会在某些位置上对padding产生不必要的关注。新版本模型越来越倾向于去掉[CLS]位置的特殊用途但很多老模型的微调流程仍然依赖它。如果你自己微调注意不要把Special Token的嵌入向量冻结掉否则模型对任务格式的适应会变差。我个人在实际操作中体会最深的一件事是分词和嵌入虽然看起来是“预处理步骤”但它们的质量对最终效果的影响往往被严重低估。同一个任务换个分词器可能就带来好几个点的效果差异嵌入层训练不好后面整个Transformer都在一个歪地基上盖楼。所以无论你是做应用还是做研究都值得花点时间把这两步彻底弄透。下一步你可以试着把代码里的模型换成不同架构的版本跑一遍完整链路对比它们的Token切分差异、嵌入维度和语义相似度表现。数据比文字更能说明问题亲手调一遍你对“文本如何变成数字”的理解会彻底落地。