首页 / 资讯中心 / 文章详情

Transformer到LLM实战指南:从自注意力到LoRA微调

Transformer到LLM实战指南:从自注意力到LoRA微调 ★ FEATURED ARTICLE
1. 先搞清楚从Attention到Transformer到底解决了什么问题第一次接触Transformer的人多少都会带着一个困惑RNN、LSTM已经能让模型“记住”序列信息了为什么还要搞一套完全不同的架构我当初也有同样的疑问直到我把序列建模的痛点彻底捋了一遍才意识到Transformer的出现不是一次简单的升级而是一次底层思维方式的转变。RNN家族处理序列的方法是“逐步扫描”——每读一个词就更新一次隐藏状态把前面所有的信息压缩成一个向量传给下一步。这种做法有两个先天的问题第一个是并行度极低当前步的计算必须等前一步算完GPU再强也只能看着流水线干等训练效率上不去第二个是长距离依赖虽然LSTM加了门控机制但从某个词传递到很远位置的词信息经过多步衰减之后基本就面目全非了所以大家常说RNN在长文本上会“失忆”。Transformer的思路完全不同。它抛弃了“逐步扫描”的套路直接把整句话的所有词一次性扔进模型让词和词之间两两计算相关性。这个就是自注意力机制Self-Attention。它把“我应该从哪里获取信息”这个决策权完全交给了模型自己任何一个词都能直接看到序列中的任何位置不管相隔多远信息路径的长度恒定为1。这才是Transformer能真正解决长距离依赖问题的根本原因而不是像LSTM那样靠门控去缓解。而且因为不再存在步骤之间的顺序依赖整句话可以同时在GPU上并行计算。同样一批数据Transformer的训练效率比RNN高出一个数量级。当年BERT、GPT那一波爆发背后真正的推动力就是Transformer的并行能力让“大力出奇迹”变成了可能。另外还有一个很多人忽略的点RNN对序列的顺序本身是隐式建模的它靠“一步一步走”来自然感知位置先后。而Transformer输入是并行的集合没有一个东西告诉模型“这个词在第几个位置”。所以作者加了一个不显眼但极其关键的设计——位置编码Positional Encoding。加上它模型才第一次知道“听”和“说”不是同一张牌。我经常用一句话总结Transformer的核心思路全连接地看并行地算用矩阵乘法换掉循环步骤。这三件事合在一起本质上就是把“序列建模”从一个顺序问题变成了一个集合计算问题。想真正理解Transformer的实战行为这些底层的设计动机必须建立起来因为后面LLM中的很多工程决策——比如上下文窗口怎么扩、推理速度为什么慢、kv cache是什么——都源自这里。2. 图解核心模块自注意力、多头机制、位置编码的来龙去脉2.1 自注意力到底在算什么自注意力这一步是整个Transformer的发动机值得彻底拆开来看。一句话描述它做的事情对于序列里的每一个词都算出它跟所有其他词的相关性分数然后按相关性加权汇总别人的信息得到这个词的新表示。具体分三步。第一步每个词都同时生成三个向量Query、Key、Value。这三者的关系你可以粗暴类比成一个“检索过程”Query是“我想找什么”Key是“我有什么可以被匹配的标签”Value是“匹配上了之后实际要取用的内容”。这个类比不是完美的但足够帮你建立直觉模型通过Query跟所有Key做点乘得到匹配分数再用这个分数去加权Value得到最终的输出。第二步计算注意力分数。对词 i 和词 j分数等于 Query_i 和 Key_j 点乘再除以根号下 d_kKey向量的维度。为什么除因为维度越高点乘结果的方差越大分数会容易落到softmax的饱和区梯度变得很小不好训练。除以根号d_k是简单有效的缩放手段。第三步对所有分数做softmax归一化得到一组和为1的权重然后去加权Value向量。这一步加权求和就是词语之间“信息交换”的过程。这里有一个非常重要的直觉注意力权重本身是个动态计算结果它完全依赖于当前的输入。同样的两个词在不同句子、不同语境下相关性分数会完全不同。所以自注意力不是硬编码的语法规则而是模型自己学出来的“动态路由”。2.2 多头注意力为什么一个头不够自注意力只用一组Q、K、V意味着整个模型只能在一种“关系视角”下观察词语。但语言关系是多维的——句法关系、指代关系、语义相似性、某种修辞呼应这些可能同时存在。一个头只能捕捉一小部分。多头注意力的做法很简单把Q、K、V分别切成h份每份独立做一次注意力计算最后把h个结果拼接起来再过一层线性变换。每个头在训练中会自然分化出不同的分工有的头专注局部相邻词的语法关系有的头专注长距离指代有的头关注动词和宾语的搭配。你观察训练好的模型注意力头之间的行为模式往往有明显差异这就是“多头”的价值用多组独立的视角覆盖不同类型的依赖关系。但这里我提醒一句多头不是越多越好。头数太多每个头分到的维度太少表达能力反而受限而且计算量线性上升。常见的配置是8头、12头、16头每头维度64。实际做项目时如果你正在调一个模型头数不要随便加先看当前模型的注意力行为如果各头分化不明显再去考虑调整。2.3 位置编码给并行模型注入序列感Transformer输入的是一整排词向量如果不加任何位置信息模型看到的这组词和它们的任意排列是完全等价的。你可以做个实验把一句话的词随便打乱顺序输入Transformer如果模型在没有任何位置信息的情况下预测结果完全一样——因为它根本感知不到顺序。原始Transformer用的是正弦位置编码位置为pos、维度为i的位置嵌入在偶数维用sin(pos / 10000^(2i/d))奇数维用cos。这组公式的妙处在于相邻的位置编码向量之间存在规律的线性关系某个位置加上一个固定偏移可以通过线性变换近似得到另一个位置的编码这有利于模型学到相对位置关系。但后来大家发现这个固定编码学起来不够灵活于是出现了两个主流的进化方向可学习位置编码把位置编码当成一组可训练参数让模型自己适应数据分布。BERT用的是这个。旋转位置编码RoPE用旋转矩阵把位置信息以角度形式注入词向量在相对位置上表现很好而且天然支持外推。现在的主流LLM基本都在用RoPE。2.4 残差与层归一化让深层网络真正train得起来Transformer很多层堆叠前向传播和反向传播的信号都会经过非常长的路径。残差连接Residual Connection保证信号可以“绕行”直达底层缓解梯度消失层归一化LayerNorm让每一层的输入分布稳定避免训练过程中的内部协变量偏移。这两个机制是Transformer能深度堆叠的基石。实战中有个细节值得注意原始Transformer用的是Post-LN每个子层输出先过残差再过LayerNorm而目前主流LLM倾向于Pre-LN先LayerNorm再进入子层计算最后残差。Pre-LN在训练初期更稳定可以不用学习率预热也训得起来但表达能力相比Post-LN略打折扣。这个问题在训练大型模型时非常关键如果你在用某个开源底座做二次训练一看结构就知道它用的是哪种方案。3. 从Transformer到LLM架构的演进才是实战的入场券3.1 Encoder-Decoder到Decoder-only的路线选择最初Transformer论文里是Encoder-Decoder结构——左边encoder处理完整输入文本右边decoder逐步生成目标文本用于翻译任务。这个结构把“理解”和“生成”拆成了两个模块非常符合翻译的直觉。但GPT系列的路线完全不一样它只保留Decoder部分做成了Decoder-only的架构。输入输出都塞进同一个自回归结构中每一次预测都依赖前面已经生成的token。事实证明这条路更适合“大语言模型”一个纯Decoder模型结构更简单更容易扩展到海量参数对训练数据的格式要求也低通用性极强。后来的LLM军备竞赛基本都遵循Decoder-only路线。为什么实战中你跑一次就明白了——Encoder-Decoder结构在生成任务里需要维护两个模块的状态扩展难、调度复杂、工程化成本高而Decoder-only一条道走到底训练和推理的模式都是“下一个词预测”所有问题都被统一成一个范式大规模算力怎么堆都方便。3.2 归一化位置的变化与隐性影响不少新入门同学会忽略一个细节从原始Transformer到主流LLMLayerNorm的位置悄悄从post-LN变成了pre-LN而且很多模型还用RMSNorm替换了LayerNorm。RMSNorm相比LayerNorm去掉了均值中心化这一步因为ReLU类的激活函数本身对均值不敏感计算量更小训练却意外的更稳定。别看这只是个小改动在大规模训练中每层省一点点计算几十层堆叠下来训练吞吐就能拉开明显差距。而且RMSNorm省下来的方差缩放处理在某些极端梯度场景下反而给了模型更强的鲁棒性。你去看Llama、Mistral这些开源模型的配置表 normalization都用的是RMSNorm这就是工程优化经过大规模验证之后形成的共识。3.3 RoPE、SwiGLU等关键trickLLM的架构迭代过程中有几个不可忽视的组件升级RoPE旋转位置编码我之前已经提过。它不止是位置编码方案还给长文本外推提供了操作空间。很多LLM宣称支持很长的上下文窗口背后就是通过修改RoPE的基频或者插值方案来实现的。SwiGLU激活函数这个我特别推荐关注。GELU、ReLU这类激活函数在各层FFN里用得很好但SwiGLU的思想是“用门控的方式决定信息有多少流向下层”它有更强的表达能力在很多公开测试里都能看到更好的效果。代价是多了一组参数但这一点点参数开销跟带来的效果提升相比非常划算。GQA分组查询注意力传统多头注意力里每个头都有独立的Q和K、V这意味着推理时每个token都要缓存一份完整的键值向量占用大量显存。GQA的做法是多组Query共享一组Key和Value在不明显牺牲效果的情况下大幅降低推理时的KV Cache占用和带宽消耗。这也是很多LLM能把推理速度做上去的秘密武器之一。3.4 上下文长度的工程挑战当前LLM的战场除了模型质量就是上下文长度。把上下文从2K扩展到32K、128K甚至1M短文本上效果不变长文本上能准确引用——这个目标非常难因为自注意力计算复杂度是输入长度的平方。实战中公司都会用多阶段训练先在短序列上训练到收敛再逐步拉长序列长度做续训。RoPE插值、滑动窗口注意力、稀疏注意力都是围绕“长序列”做工程改造的手段。这些内容如果你只看论文会觉得花里胡哨但放到实战里每一项都对应着非常具体的显存和速度压力。4. LLM实战从部署到微调的完整路径4.1 推理阶段的参数选型进入LLM实战第一步通常不是训练而是部署。拿一个开源模型做推理时你得面临几个关键配置。以HuggingFace的transformers库为例模型加载时几个参数直接影响资源占用和速度load_in_8bit和load_in_4bit这两个量化选项可以把模型权重压到8bit或4bit显存需求直接减半或减到四分之一。很多人以为量化一定会让效果崩掉但实测下来用GPTQ或AWQ做4bit量化模型的智力损伤很小只有极长文本或复杂推理任务里能感到轻微差距。device_map设为“auto”可以自动把所有层分配到所有可用GPU上不用自己手动切层。对小规模实验来说特别方便但多卡推理时层间通信开销会拖慢速度性能要求高的项目还是得手动做并行方案。trust_remote_code这个参数很隐蔽但很重要。不少模型自定义了新的模型结构代码没有合并到transformers主干这时候得指定trust_remote_codeTrue。不过用了它就要承担一定风险你加载的是远端代码建议先审查过代码再跑。如果你的公司或项目对推理性能要求高vLLM可能是更好的选择。它引入了PagedAttention把KV Cache像操作系统分页一样拆成小块管理能极大优化显存利用率。实测效果很直观同样的模型vLLM的吞吐可以做到比原生transformers高出很多倍。我的建议是快速验证用transformers生产服务直接用vLLM。4.2 微调实战LoRA究竟省了什么全参微调Full Fine-tuning会更新所有层的全部参数效果好但显存和训练开销大得很。于是出现了PEFTParameter Efficient Fine-tuning的思路LoRA是其中最出圈的一个。LoRA的核心假设是预训练模型权重更新时的“变化量”其实处在一个低秩空间里。也就是说权重更新的矩阵不必是完整的参数矩阵它可以被分解成两个低秩矩阵的乘积从而大大减少训练参数。简单说原本模型的W矩阵是d×d维度LoRA给它加了一个旁路分支这个分支是A×BA是d×rB是r×dr远小于d。训练时只更新A和B原来的W完全冻结不动。最后模型的推理结果等于原模型输出加上这个低秩分支带来的偏移。我自己做微调时的实际数字一个7B模型全参微调单卡A100的显存压力很大通常要多卡并行而且优化器状态、梯度都要占额外显存。而LoRA训练只更新几千到几百万个参数同一张显卡就能轻松跑起来而且效果在全参微调的90%以上。对绝大部分业务场景来说LoRA足够。4.3 数据准备与微调策略微调效果的上限不是由模型决定的而是由数据决定的。我踩过最大的坑就是把所有数据一次性混进去训练结果模型学会了“数据间冲突”的折中行为效果一团糟。比较稳妥的做法是分阶段混合。以做一个客服小微调为例我会分两步第一阶段用通用指令数据训练先让模型学会“听指令”的基本姿态第二阶段用特定业务的问答对做精修让模型在业务问题上给出正确的范式。每阶段都做验证集效果对比防止遗忘灾难。数据量上不是越多越好质量远胜数量。几条高质量、能覆盖典型难点的样本比几万条低质量样本更有价值。另外还有一个需要注意的点如果你要做多轮对话场景数据格式要能在训练时保留多轮上下文结构如果只是单轮QA就别把多轮历史带进去否则推理时模型会对上下文长短产生错误的预期。4.4 评估别只看loss很多人训练完只看loss降了没降这是非常危险的做法。loss下降只能说明模型拟合了训练集的分布不能说明它在目标任务上的真实表现。我建议每个项目搭配三层评估体系通用能力抽查拿一组长尾问题实际测试模型的回答看语言流畅性和常识正确性有没有被破坏业务场景指标对你的核心任务建一套自动化评估脚本比如分类任务的准确率抽取任务的关键字段命中率对抗性测试专门准备一些边界case、陷阱问题看模型会不会跑偏这些往往最能暴露微调带来的副作用。这三层跑完你才算真正知道这次微调是正向还是负向。5. 常见问题与排查思路实战中反复踩过的坑5.1 训练显存爆了怎么办显存溢出是初学者最常撞的墙但实际上有非常成熟的解决方案。从性价比来排先检查batch size是不是设得太大了。减小batch size是零成本缓解显存压力的第一步梯度累积gradient accumulation等于是把一个大batch拆成几个小batch依次计算每步计算Loss并保存梯度攒够了再统一更新参数。它能复现大batch的效果代价只是训练时间略长打开混合精度AMPFP16训练显存占用减半。现在主流框架都默认支持用LoRA或者QLoRA做微调而不是全参如果还是爆就需要换更大显存的卡或者用多卡切片。显存不是一切还有优化器状态占的显存不亚于模型本身。用AdamW训练时每个参数要保存两个动量这就是2倍参数量。所以很多大规模训练用8bit优化器或Adafactor来省显存。5.2 推理速度太慢怎么破如果你在本地部署一个7B模型做对话感觉很慢可能有几个原因模型没有被量化改成4bit量化之后速度提升非常明显没有启用KV Cache现在主流推理框架默认都会开但如果你自己写推理循环一定要手动维护缓存用了自回归解码的一次一个token的生成方式这是生成模型的天性但你可以用“批处理并行生成多个候选序列”或者考虑投机采样、推测解码这类方法让小模型先草稿大模型做校验这样显存消耗不变速度能快不少检查一下是不是CPU在做推理而不是GPU很多人搭环境的时候CUDA没配好模型跑在CPU上还浑然不知。跑一次推理任务看一下GPU利用率如果一直很低就要怀疑到底走没走GPU。5.3 效果不好模型胡说八道LLM热爱编造事实这是行业公认的问题。原因分两类一类是模型本身知识不足因为你微调的数据集里没有正确答案另一类是模型训练目标就是预测下一个词并不区分事实和虚构。实战中最有效的缓解手段提供充足的上下文参考用检索增强RAG把相关知识库结果拼进prompt让模型基于给定材料回答大幅降低幻觉概率限制解码策略temperature调低0.1~0.3top_p调低减少随机性用Prompt约束模型告诉它“不知道的时候就说不知道”这个表面对齐很简单但实测非常有效如果以上都没用那就说明你的微调数据里确实有噪声回去清洗数据。5.4 模型越训越差泛化崩了某次微调之后测试时发现它开会写代码的人设崩了简单问答也回答得乱七八糟。排查下来原因是训练样本太单一模型把所有问题都往同一个模式上套丢失了通用能力。这类问题叫灾难性遗忘常见原因训练数据分布严重偏移全是有标记样本缺少通用样本学习率过高优化过程把预训练学到的知识覆盖得太快训练步数太长过了最佳训练点还在跑。解决办法微调数据里掺入10%~20%的通用语料或者通用指令数据学习率压低一个数量级并且用验证集做early stopping防止过拟合。6. 总结一下我对这套技术栈的实操心得Transformer到LLM这条线是我这几年见过的“从论文到工程”转化最丝滑的技术路径但这个转化过程也最讲求细节写作、部署、微调每一步都会遇到让人崩溃的玄学问题。我个人最大的体会是与其追求一次性搞懂所有细节不如直接动手跑一个最小案例比如用一个小模型做一次LoRA微调把整个流程完整走一遍——模型加载、数据构造、训练脚本、评估脚本、推理解码全跑通之后你前面的理论认知才会真正落地成肌肉记忆。如果你后续要扩展我建议优先试这四件事一是做一个RAG系统把知识库接进模型这是一个极常用且改动小的增强方案二是用vLLM部署一个线上推理服务感受一下吞吐量差异三是替换RoPE的基频参数观察它如何影响长文本外推能力四是系统性的评估你的LoRA效果铺开测试集而不是只看几个例子。四条路随便选一条都能明显推进你对LLM工程的理解。用这个收藏版的内容作为起点把每个章节标注的细节变成你下一轮实验的清单比自己来回翻论文要高效得多。
阅读完成 · 觉得有帮助?
咨询建站