ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

NLP基础:从分词、嵌入到注意力机制,构建大模型的坚实基石

2026/8/11 2:31:24 拓冰建站 浏览量
NLP基础:从分词、嵌入到注意力机制,构建大模型的坚实基石 1. 项目概述为什么我们总在“地基”上栽跟头最近和几个刚入行大模型的朋友聊天发现一个挺有意思的现象。他们拿到一个开源LLM第一件事就是去跑几个对话看看效果酷不酷第二件事可能就是去微调想让它干点私活。但当问到“这个模型的词表是怎么构建的”或者“为什么你的输入文本要先做分词再做嵌入”时很多人就卡壳了回答往往是“框架自动处理的”、“照着教程做的”。这让我想起自己刚入门时踩的那些坑精心调参但效果死活上不去最后发现是数据预处理时标点符号全被删了导致模型根本学不会句子的边界信息又或者在尝试做领域适配时盲目往词表里加新词结果不仅没提升效果反而让模型产生了严重的退化。所以当我把这个学习笔记系列命名为“Happy-LLM”并决定从“NLP基础”开始时我是认真的。这不是在炒冷饭也不是在说前置废话。恰恰相反我认为跳过这些“基础”去直接摆弄大模型就像在沙地上盖高楼。你可能很快能搭出一个看起来不错的架子但任何一点风吹草动比如数据分布稍有变化、任务稍微复杂一点整个系统就可能摇摇欲坠。理解分词Tokenization、词向量Embedding、注意力机制Attention这些“老生常谈”的概念不是为了应付考试而是为了让你真正拥有“拆解”和“构建”LLM的能力。当模型输出一堆乱码时你能想到可能是BPE分词在罕见词上出了问题当微调效果不佳时你会去检查输入表示的维度是否对齐。这份笔记就是希望能和你一起把这些看似枯燥的地基一寸一寸打扎实。2. 核心基石一从“词”到“Token”——分词的艺术与陷阱当我们把一句话“Hello, happy learning!”喂给模型时它看到的并不是我们眼中的单词序列。这个转换过程就是分词Tokenization它是所有文本处理的第一步也是决定模型“世界观”的关键。2.1 主流分词策略的底层逻辑与选择目前LLM领域主要有三大分词器流派Word-based词级、Character-based字符级和Subword-based子词级。Word-based分词最简单但面对未登录词OOV直接傻眼且词表会膨胀到无法管理。Character-based最彻底把一切拆成字母词表极小但序列长度爆炸模型难以捕捉语义单元。因此Subword子词分词成为了绝对的主流它完美地在“词义”和“灵活性”之间取得了平衡。Subword分词的王者是Byte Pair Encoding (BPE)及其变种如GPT系列用的BPEBERT用的WordPiece。它们的核心思想是从字符开始通过不断合并最高频的相邻符号对来逐步构建词表。举个例子假设我们的语料里“happy”和“learning”出现很多。BPE算法会初始词表就是所有字符h, a, p, p, y, l, e, a, r, n, i, n, g。统计所有相邻符号对频率发现p和p经常连在一起于是合并成pp加入词表。继续统计ha可能合并成happ和y合并成ppy。最终“happy”可能被编码为h app y或ha ppy等子词单元。这里的关键不是算法本身而是其带来的深刻影响词表是数据分布的浓缩你的分词器在《华尔街日报》语料上训练和在网络小说语料上训练得到的词表天差地别。前者会有大量金融术语子词后者则可能包含“yyds”、“蚌埠住了”这样的网络流行语编码。用错词表相当于让一个只学过文言文的人去理解现代白话文。分词结果直接影响模型效率一个长词被切分成多个子词意味着模型需要处理更长的序列计算量和内存消耗随之增加。同时碎片化的分词也会增加模型理解整体语义的难度。实操心得分词器的“冷启动”问题很多新手会直接使用预训练模型如bert-base-uncased自带的分词器来处理自己的数据这通常没问题。但当你处理极度垂直的领域如医学论文、法律条文、程序代码时预训练词表可能严重不够用。这时你需要考虑在自己的领域语料上重新训练一个BPE分词器。工具如Hugging Face的tokenizers库很简单但关键在于语料的选择要有代表性并且要仔细评估新词表的大小通常在3万到10万之间过大会浪费过小则覆盖不足。2.2 中英文分词的巨大差异与实战处理英文分词相对简单空格是天然的分隔符。但中文完全不同句子是连续的字符串“今天天气很好”。应该分成“今天/天气/很/好”还是“今天/天/气/很好”这对模型来说是根本性的差异。主流的中文LLM如ChatGLM、Qwen、Baichuan通常采用基于字的分词或混合分词。字分词将每个汉字作为一个独立的Token。优点是完全避免分词错误词表稳定约7000常用字能很好地处理新词和专名。缺点是序列长度长且模型需要从零学习词语、短语的语义组合对算力和数据要求更高。词分词/混合分词先用传统分词工具如jieba切词再对长词应用BPE。优点是能利用词汇的先验知识序列更短。但严重依赖分词工具的质量分词错误会直接传导给模型且对未登录词不友好。在我们的实践中对于通用中文大模型更倾向于从“字”级别开始让模型自己学习组合规律。但对于某些特定任务例如专业文献处理可以引入领域词典来辅助生成更优的子词单元。# 一个简单的对比示例使用Hugging Face Transformers库 from transformers import AutoTokenizer # 加载不同的中文分词器 tokenizer_char AutoTokenizer.from_pretrained(“bert-base-chinese”) # 基本是字分词 tokenizer_word AutoTokenizer.from_pretrained(“hfl/chinese-bert-wwm-ext”) # 使用全词掩码分词粒度更粗 text “大语言模型正在改变世界。” tokens_char tokenizer_char.tokenize(text) # 输出[‘大’ ‘语’ ‘言’ ‘模’ ‘型’ ‘正’ ‘在’ ‘改’ ‘变’ ‘世’ ‘界’ ‘。’] print(“字分词:”, tokens_char) tokens_word tokenizer_word.tokenize(text) # 输出[‘大’ ‘语言’ ‘模型’ ‘正在’ ‘改变’ ‘世界’ ‘。’] print(“词/混合分词:”, tokens_word)可以看到不同的分词策略直接导致了不同的Token序列这必将影响模型对句子结构的理解。3. 核心基石二让Token拥有意义——嵌入Embedding的维度世界分词之后我们得到了一串Token ID数字。但计算机无法直接理解数字的“含义”。Embedding层的作用就是为每个Token ID赋予一个高维空间中的向量表示这个向量能够编码该Token的语义和语法信息。3.1 嵌入层的本质一个可查找的语义数据库你可以把Embedding层想象成一个巨大的表格矩阵。矩阵的行数就是词表大小V列数就是嵌入维度D。当输入Token ID为k时Embedding层就简单地“查找”这个表格的第k行取出那个D维的向量。import torch import torch.nn as nn vocab_size 50000 # 词表大小 embedding_dim 768 # 嵌入维度也是Transformer的隐藏层大小 embedding_layer nn.Embedding(vocab_size, embedding_dim) # 假设输入一个批次的两个句子每个句子3个token input_ids torch.LongTensor([[101, 2054, 102], [101, 1996, 102]]) # [batch_size, seq_len] embedded_output embedding_layer(input_ids) # [batch_size, seq_len, embedding_dim] print(embedded_output.shape) # torch.Size([2, 3, 768])这个查找操作本身没有计算量但这个巨大的矩阵50000*768 ≈ 3800万参数里的每一个数值都是在预训练过程中从海量文本里学出来的。语义相近的Token如“猫”和“狗”它们的向量在空间中的距离会更近。3.2 维度选择768、1024还是4096嵌入维度D是一个超参数它直接决定了模型表示能力的上限。为什么常见的是768、1024、4096这些数字模型容量与计算成本的权衡D越大每个Token携带的信息越丰富模型理论表达能力越强。但随之而来的是后续所有全连接层、注意力层的参数都会成平方级增长特别是注意力机制计算复杂度与D相关对显存和算力要求急剧上升。经验值BERT-base选择768作为一个在效果和效率上较好的平衡点。GPT-3等超大模型为了容纳更复杂的知识使用了4096甚至更高的维度。对于我们自己的项目如果资源有限从256或384开始尝试是合理的如果追求更好效果768或1024是更稳妥的起点。与后续层对齐嵌入维度通常与Transformer的隐藏层维度保持一致以确保信息流在通过网络时维度匹配。注意事项嵌入层的“冷”与“热”冷启动随机初始化当你从头训练一个模型时嵌入矩阵是随机初始化的。这意味着模型需要从零开始学习每个词的语义需要大量的数据和迭代。热启动加载预训练权重这是最常见的做法。使用在通用语料上预训练好的嵌入矩阵你的模型已经拥有了一个相对成熟的“语义世界”。即使在你的垂直领域数据上微调这个起点也远比随机初始化要高得多。永远不要低估一个高质量预训练嵌入层的力量。4. 核心基石三注意力机制——模型如何“聚焦”有了Token的向量表示模型如何理解它们之间的关系呢比如在“猫追老鼠”这句话里模型需要知道“追”这个动作的主体是“猫”客体是“老鼠”。这就是注意力机制Attention要解决的核心问题动态地、有区分地整合序列中所有位置的信息。4.1 自注意力Self-Attention的三步拆解自注意力允许序列中的任意一个位置直接“看到”并“吸收”其他所有位置的信息。其计算过程可以拆解为三个直观的步骤生成问题、答案和身份牌Q, K, V对于每个Token的输入向量我们通过三个不同的线性变换生成三个新的向量Query查询向量、Key键向量、Value值向量。你可以理解为每个Token都提出了自己的“问题”Q亮出了自己的“身份牌”K并准备好了自己要提供的“信息内容”V。计算亲疏关系注意力分数用当前Token的Q去和序列中所有Token包括自己的K做点积。点积值越高说明两者的“相关性”或“匹配度”越高。例如“追”的Q与“猫”的K点积可能很高因为“谁在追”这个问题“猫”这个身份牌给出了强烈回应。然后将所有分数通过Softmax归一化得到一组权重和为1这组权重就代表了当前Token应该“关注”其他Token的程度。加权求和得到新表示用上一步得到的权重对所有Token的V进行加权求和。这样“追”这个Token的新向量表示就包含了大量来自“猫”的信息以及少量来自“老鼠”和其他词的信息。它不再是孤立的“追”字而是“猫追某物”这个语义单元的一部分。# 一个极简的自注意力代码示意忽略批次和头维度 import torch import torch.nn.functional as F def self_attention(x, W_q, W_k, W_v): # x: [seq_len, dim] Q torch.matmul(x, W_q) # [seq_len, dim] K torch.matmul(x, W_k) # [seq_len, dim] V torch.matmul(x, W_v) # [seq_len, dim] attn_scores torch.matmul(Q, K.T) # [seq_len, seq_len] attn_weights F.softmax(attn_scores / (x.size(-1) ** 0.5), dim-1) # 缩放点积注意力 output torch.matmul(attn_weights, V) # [seq_len, dim] return output, attn_weights # 假设输入序列长度为3维度为4 x torch.randn(3, 4) W_q torch.randn(4, 4) W_k torch.randn(4, 4) W_v torch.randn(4, 4) output, weights self_attention(x, W_q, W_k, W_v) print(“注意力权重矩阵显示每个词对其它词的关注度:”) print(weights)4.2 多头注意力多视角的智慧为什么需要“多头”Multi-Head单一套Q、K、V只能学习到一种类型的依赖关系比如主谓关系。多头注意力相当于让模型拥有多套并行的“注意力感官”。每一套都有自己的W_q, W_k, W_v参数可以独立地学习不同子空间中的关系。例如一个头可能专门学习语法结构主谓宾另一个头可能专门学习指代关系“它”指代谁还有一个头可能捕捉语义搭配“剧烈运动”。最后所有头的输出被拼接起来再经过一个线性变换融合成最终的表示。这种设计极大地增强了模型的表征能力。5. 从基石到建筑Transformer架构的宏观视角理解了Tokenization、Embedding和Attention我们就能像搭积木一样理解Transformer的编码器Encoder和解码器Decoder结构了。5.1 编码器理解输入的“阅读理解专家”以BERT为代表的编码器模型其核心任务是为输入的每个Token生成一个融合了全局上下文信息的“深度表示”。它的工作流程是一个多层堆叠的过程输入嵌入Token经过分词、嵌入并加上位置编码告诉模型每个词的位置信息。编码器层堆叠数据通过N个例如BERT-base是12个相同的编码器层。每一层都包含多头自注意力子层让每个Token纵观全局与其他所有Token进行信息交互。前馈神经网络子层一个简单的全连接网络对每个Token的表示进行独立、非线性的变换增强其表达能力。残差连接与层归一化每个子层周围都包裹着残差连接和层归一化。这是训练深层网络的关键技巧能有效缓解梯度消失让模型可以堆叠得很深。输出经过所有层后每个输入Token都对应一个富含上下文信息的向量。这个向量可以用于各种下游任务比如文本分类取[CLS]位的向量、问答用问题与文本所有Token的向量进行匹配等。编码器的特点是“双向”的在自注意力中每个词都能同时看到前后文因此它非常擅长“理解”任务。5.2 解码器生成文本的“写作大师”以GPT系列为代表的解码器模型其核心任务是自回归地生成文本即根据已经生成的所有词预测下一个词。它的结构与编码器类似但有关键区别掩码自注意力这是与编码器最核心的区别。在解码器的自注意力层中每个Token只能“看到”它自己以及它左边的Token过去的Token而不能“看到”右边的Token未来的Token。这通过一个注意力掩码矩阵来实现确保了生成过程在时间上的因果性——模型在预测下一个词时无法“作弊”看到答案。编码器-解码器注意力在像T5、BART这样的Seq2Seq架构中解码器还会有第二层注意力。这一层的Query来自解码器自身而Key和Value则来自编码器的最终输出。这允许解码器在生成每一个词时有选择地“回顾”输入序列的信息非常适合翻译、摘要等需要理解源文本的任务。输出与循环解码器最后一个Token的输出向量会通过一个线性层映射到词表大小的向量上再经过Softmax得到下一个词的概率分布。我们根据这个分布采样或取最大概率得到下一个词然后将这个词作为输入的一部分再次送入解码器循环往复直到生成结束标记。解码器的特点是“单向”和“自回归”的它像我们人类写作一样一个字一个字地构思和创作。6. 实战避坑NLP基础不牢引发的“血案”理论懂了代码会写了但一到实际项目还是处处碰壁。下面分享几个我亲身经历或见同行踩过的坑它们都根源于对基础环节的忽视。6.1 分词不一致导致的“语义撕裂”场景你训练了一个客服机器人使用SentencePiece在客服对话数据上训练了BPE分词器。上线后用户输入“我想了解一下iPhone14ProMax的以旧换新政策”。你的分词器可能将其切分为[‘我’ ‘想’ ‘了解’ ‘一下’ ‘iPhone’ ‘14’ ‘Pro’ ‘Max’ ‘的’ ‘以旧换新’ ‘政策’]。看起来不错。问题但你的知识库里的相关条目标题是“iPhone 14 Pro Max 以旧换新细则”。如果知识库的索引或检索系统使用了另一个不同的分词器比如一个简单的空格分词那么它可能被切分为[‘iPhone 14 Pro Max’ ‘以旧换新’ ‘细则’]。尽管人类看来两者完全一样但在向量空间里由于Token序列的差异两者的相似度计算可能会严重失真导致检索失败。解决方案全链路分词统一确保从数据预处理、模型训练、到线上服务检索、匹配的整个流水线使用完全相同的分词器。最好将分词器对象和词表文件作为项目资产一起保存和部署。对输入进行规范化在分词前增加文本清洗和规范化步骤例如统一全半角、繁体转简体、纠正拼写错误等减少不必要的分词差异。6.2 嵌入维度不匹配的“维度灾难”场景你找到了一个在相似任务上表现很好的预训练模型比如一个文本分类模型想把它作为特征提取器用到你的新任务中。该模型的嵌入维度是768。但你的新模型架构中出于某些原因比如想和其他模块对齐隐藏层维度设计为512。问题如果你直接加载预训练模型的嵌入层权重会发现形状是[V, 768]无法放入你维度为512的新嵌入层。强行截断或填充都会严重破坏预训练学到的语义空间导致效果大幅下降。解决方案优先调整新模型架构最推荐的做法是修改你的新模型使其嵌入层和后续层的维度与预训练模型保持一致768。这样可以完整利用预训练知识。使用投影层如果确实无法修改维度可以加载预训练嵌入层后在其后添加一个可训练的线性投影层nn.Linear(768, 512)将768维向量映射到512维。在微调时这个投影层和模型其他部分一起学习。虽然会损失一些信息但比随机初始化要好。部分初始化如果词表有很大不同可以只加载公共词汇部分的嵌入向量新词的嵌入随机初始化。6.3 忽略位置编码的“顺序迷失”场景你正在实现一个简易的Transformer模型。为了省事你觉得注意力机制已经让词与词之间能互相看到位置信息可能没那么重要于是跳过了位置编码Positional Encoding。问题Transformer的自注意力本质上是置换不变的Permutation Invariant。也就是说打乱输入句子的词序得到的输出不考虑位置编码在向量空间中的集合是相同的只是顺序变了。模型完全丧失了理解“词序”的能力。“猫追老鼠”和“老鼠追猫”对它来说可能没有区别这显然是灾难性的。解决方案必须添加位置信息绝对不要省略位置编码。最常用的是正弦余弦位置编码Sinusoidal Positional Encoding它为每个位置生成一个独特的、固定的向量与词嵌入相加。现在也有可学习的位置嵌入Learned Positional Embedding效果类似。注意长度外推正弦编码理论上可以处理比训练时更长的序列但效果会下降。可学习的位置嵌入则完全无法处理超长序列。对于需要处理超长文本的任务需要关注ALiBi、RoPE等更先进的位置编码方案它们能更好地外推。6.4 注意力掩码使用不当的“信息泄露”场景在训练一个因果语言模型如GPT时你需要构造注意力掩码确保解码时看不到未来信息。你正确地处理了训练数据。但在模型部署后进行序列生成推理时你一次性将整个生成的序列输入模型来计算下一个词的概率。问题这造成了推理阶段的信息泄露因为在生成第t个词时你的输入已经包含了第t-1步及之前生成的所有词。如果你没有在推理时动态地应用因果掩码模型在计算第t个位置的注意力时可能会“看到”它自己在自回归生成中这通常意味着看到了未来的信息因为当前步的输出会成为下一步的输入的一部分导致生成结果出现重复、退化或逻辑错误。解决方案推理时使用自回归解码在生成每一个新Token时只将历史已生成的序列输入模型并确保注意力掩码是严格下三角的因果掩码。利用KV缓存加速为了避免重复计算可以使用KV缓存技术将之前步骤中计算好的Key和Value向量缓存起来在生成新Token时只计算当前步的Q与缓存的所有K、V的注意力。但即使使用缓存注意力掩码的逻辑也必须正确维护确保每一步只能看到历史信息。7. 延伸思考基础如何影响大模型的进阶能力当我们扎实地理解了这些NLP基础再回头看大模型的许多高级特性和问题就会有一种豁然开朗的感觉。长文本建模的瓶颈为什么模型处理长文本困难核心之一在于注意力机制的计算复杂度是序列长度的平方。当序列长度从512增加到8192时计算量和显存占用会增长256倍这就是为什么需要Flash Attention、环形注意力等优化技术。而这一切的起点就是你对注意力计算过程有清晰的认识。上下文学习In-Context Learning为什么GPT-3给几个例子就能学会新任务这依赖于模型强大的注意力机制能够在前向传播中动态地建立示例与问题之间的关联。模型并没有更新参数而是通过注意力权重在给定的上下文示例中“即时”找到了解决问题的模式。如果你不理解注意力是如何工作的就很难理解这种“魔法”般的现象。灾难性遗忘与持续学习当我们用新数据微调一个大模型时它经常会在新任务上表现很好却忘了旧任务。从嵌入和注意力的角度看微调过程是在调整整个模型的参数空间包括那个巨大的嵌入矩阵和注意力权重。新数据的梯度会覆盖掉参数中存储的旧知识。理解这一点你就会明白为什么需要像LoRA低秩适配这样的技术——它通过冻结原模型、只训练少量新增参数来避免直接扰动宝贵的预训练知识。地基的深度决定了建筑的高度和稳固性。在追逐更酷的模型架构、更炫的应用场景时时不时回头审视一下这些基础概念总能带来新的收获。在后续的“Happy-LLM”笔记中我们会带着对地基的深刻理解继续向上探索拆解大模型的训练、微调、评估与应用。希望这篇笔记能帮你把脚下的路踩得更实一些。