ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零手写Transformer:从注意力机制到LoRA微调全指南

从零手写Transformer:从注意力机制到LoRA微调全指南 很多同学学 Transformer都会经历一个奇怪的过程视频看懂了论文刷完了脑内觉得自己已经掌握了注意力机制一打开代码编辑器手指悬在键盘上完全不知道第一行该写什么。这不是你的问题而是 Transformer 这门知识的结构性特点决定的。它和传统的机器学习模型不一样你没法靠“理解算法原理”就跳过“亲手调试张量形状”这一步。Transformer 的难点不在于数学有多高深而在于它的数据流动方式输入是什么形状经过注意力层之后变成什么形状多头拆分之后又是什么形状mask 到底在哪里生效——这些才是真正卡住新手的东西。所以这篇文章不打算再给你画一遍那张经典的 Attention is All You Need 架构图。外面画这张图的教程已经足够多了你需要的是另一条路径先把核心概念用最少的话讲清楚然后直接动手从零敲出一个能训练的 Mini Transformer再用 Hugging Face 跑一个真实文本分类任务最后用 LoRA 做一次高效微调。这套路线走完你会发现 Transformer 不再是“看过但没写过”的知识而变成了你手里真正能用的工具。1. 这篇文章真正要解决的问题先说实话Transformer 的学习材料已经严重过剩。B 站上有大量讲解视频GitHub 上有大量源码仓库知乎上有大量原理文章。但和学习者的反馈对照一下会发现一个很大的错位很多人收藏了几十个“保姆级教程”最后还是不会写代码。问题出在哪里我观察到的有三个典型困境。第一原理和代码脱节。原理文章讲 Q、K、V用“查询、键、值”这种词解释看起来很有道理但一到代码里你看到的是一个形状为[batch_size, seq_len, d_model]的张量被拆成多头transpose 来 transpose 去最后又 view 回去。教科书不会告诉你这些张量操作到底在干什么。第二教程的跳跃度太大。要么只讲原理不给可运行的完整代码要么直接让你用 Hugging Face 一行代码加载预训练模型结果你连模型内部发生了什么都不知道。对新手来说这两种方式中间缺了一座桥。第三实战训练成本高。很多新手想跑一个 Transformer 训练结果发现光是 GPU 显存就不够或者训练一个模型要几个小时。其实新手学习的正确路径应该是先跑极小规模的实验理解机制之后再去接触大模型。这篇文章专门解决这三件事。我会先讲清楚核心概念然后带着你从零写一个只有几百行的 Mini Transformer——它在 CPU 上几分钟就能训练完。再进入实战环节用 Hugging Face 完成一个标准 NLP 任务。最后讲高效微调让你明白为什么现在大家都在用 LoRA而不是去全量微调一个大模型。读完这篇文章你能得到三样东西一张可以照着理解任何 Transformer 变体的认知地图、一份可以复制到本地运行的最小实现代码以及一套从零到微调的完整动手路线。2. Transformer 核心概念与原理这一章不会展开所有公式推导只讲清楚四个真正影响你写代码的概念注意力机制、QKV 拆解、多头注意力和位置编码。2.1 注意力机制一次信息检索想象一个场景你在一个堆满文件的办公室里找一份重要合同。你手里有一张便签上面写着“2023年供应商合同”。你会怎么做你会扫一眼文件柜上的标签找到匹配的那些文件夹打开看内容然后带走最有用的那份。注意力机制做的事情完全一样。序列里的每一个 token 都是一份文件你的“查询”是一个问题所有 token 根据这个问题被“检索”出来最后根据相关程度加权汇总成输出。这个过程在 Transformer 里不是一次性完成的而是每个 token 同时发起查询同时检索整个序列——这就是“自注意力”里那个“自”的含义。这也是为什么 Transformer 能处理长距离依赖无论两个 token 在序列中隔多远注意力机制都能直接建立联系不需要像 RNN 那样一步步传递信息。2.2 Q、K、V一场三角关系Q、K、V 是注意力机制里最劝退人的三个字母。很多人背下了“Query 是查询Key 是键Value 是值”这句话但完全不知道这三个东西是怎么来的。简单理解每个 token 都会生成三个向量分别负责“我要找什么”、 “我是什么类别”、 “我携带什么内容”。然后查询向量和键向量做点积计算出这个 token 和其他所有 token 的匹配程度这个匹配程度经过 softmax 变成权重再用权重去加权求和所有的值向量。在代码里Q、K、V 就是三个线性层输入同一个张量输出三个新的张量。你可以认为原始输入被复制了三份分别经过不同的线性变换变成了三种角色的向量。2.3 多头注意力多个角度同时看为什么需要“多头”因为单次注意力只能捕捉一种匹配模式。但语言里的关系是多种多样的一个词可能因为语法关系和前面的词关联也可能因为语义关系和很远的词关联还可能因为指代关系和另一个词关联。多头注意力就是把注意力计算拆成多个子空间让每个头学到不同的关系模式。代码上怎么做把d_model维向量切成n_head份每份是一个维度为d_k的子向量然后这几个头并行计算注意力最后拼接回去。这里有一个新手最容易看懵的地方为什么代码里要对 Q、K、V 做view和transpose原因很简单——你把[batch_size, seq_len, d_model]的张量通过view变成[batch_size, seq_len, n_head, d_k]再用transpose把n_head换到第二个维度变成[batch_size, n_head, seq_len, d_k]这样才能实现多头并行计算。想清楚这一行代码你就已经战胜了 50% 的 Transformer 新手。2.4 位置编码给序列一个顺序感注意力机制本身是“无序”的。它对所有 token 一视同仁没有先后概念。这导致一个后果你把“我爱你”和“你爱我”输入 Transformer如果不加位置信息模型会认为这是同一个输入。位置编码就是为了解决这个问题。原始 Transformer 使用正弦余弦函数生成固定位置向量加到 token embedding 上。后来的模型则用可学习的位置 embedding或者像 RoPE 那样把位置信息编码进旋转矩阵里。对新手来说位置编码的核心认知是它的工作就是给每个 token 附上一个“第几位”的标记。至于具体用哪种方案不影响你对整体架构的理解。2.5 Encoder 与 Decoder两类架构方向Transformer 原论文有两种模块Encoder 负责编码输入序列每个位置都能看到整个序列Decoder 负责生输出序列生成时只能看到当前位置之前的内容这就是 mask 的作用。现在的生态里BERT 这类模型只用 Encoder适合理解类任务分类、标注。GPT 这类模型只用 Decoder适合生成类任务。理解这个区分很重要因为你之后无论接触哪种模型核心架构都不会变。模型结构适合任务代表Encoder-only双向注意力分类、抽取、匹配BERT、RoBERTaDecoder-only单向注意力 mask生成、对话、续写GPT、Qwen、LlamaEncoder-Decoder双向编码 单向解码翻译、摘要T5、BART这一章的小结论是Transformer 的四个核心机制注意力负责“看全”多头负责“多角度”位置编码负责“有序”mask 负责“守规矩”。后面的所有代码都是这四个机制的排列组合。3. 环境准备与前置条件在学习阶段不建议一上来就上大模型。你需要的是一个轻量、稳定的实验环境。这里给出一个通用建议具体版本以你实际安装到的为准。3.1 核心依赖清单最小环境需要四样东西依赖用途建议Python开发语言3.8 及以上3.10 更稳妥PyTorch深度学习框架2.x 版本CPU 版即可完成本文实验TransformersHugging Face 模型库4.x 最新稳定版Datasets数据集加载随 Transformers 一起安装安装命令很简单pip install torch transformers datasets accelerate peft如果你还没有 Python 环境推荐使用 Anaconda 创建独立环境避免把系统 Python 搞乱conda create -n transformer python3.10 conda activate transformer3.2 硬件说明这是新手最容易高估的一个环节。跑本文的 Mini Transformer 实验CPU 就够了几分钟能完成训练。Hugging Face 的 BERT 文本分类实战用 CPU 也能跑只是慢一些有 GPU 会更舒服。LoRA 微调一章我会选一个小模型GPU 显存在 8G 以上的机器都可以跑。一句话原则先跑通再上规模。不要一开始就想着微调 7B 模型那会让你花大量时间在环境配置上而不是学习本身。3.3 验证环境安装完成后在命令行执行python -c import torch; print(torch.__version__)能输出版本号就说明 PyTorch 装好了。4. 从零手写一个 Mini Transformer我坚持认为学习 Transformer 必须手写一次。哪怕你之后全部用 Hugging Face手写的过程会帮你建立对张量形状的敏感度这是调模型时最值钱的能力。接下来我们用 PyTorch 实现一个完整的 Decoder-only Mini Transformer用于一个非常简单的任务学习按字母表顺序输出字母序列。比如输入a b c输出d。这个任务足够简单CPU 上几分钟就能跑出明显效果同时又能完整检验 Transformer 的各个模块是否正确。4.1 多头注意力实现# 文件路径mini_transformer.py import math import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_head, dropout0.1): super().__init__() assert d_model % n_head 0 self.d_model d_model self.n_head n_head self.d_k d_model // n_head self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) self.w_o nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): batch_size, seq_len, _ x.size() # 生成 Q、K、V 并拆多头 Q self.w_q(x).view(batch_size, seq_len, self.n_head, self.d_k).transpose(1, 2) K self.w_k(x).view(batch_size, seq_len, self.n_head, self.d_k).transpose(1, 2) V self.w_v(x).view(batch_size, seq_len, self.n_head, self.d_k).transpose(1, 2) # 缩放点积注意力 scores Q K.transpose(-2, -1) / math.sqrt(self.d_k) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn F.softmax(scores, dim-1) attn self.dropout(attn) out attn V # 多头结果拼接回 d_model 维度 out out.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) return self.w_o(out)这段代码里有几个关键点需要看懂第一view(batch_size, seq_len, self.n_head, self.d_k)这一步是把一个维度为d_model的向量切成n_head个d_k维的子向量。因为d_model n_head * d_k所以总维度不变。第二transpose(1, 2)之后张量变成[batch_size, n_head, seq_len, d_k]这样在后续计算中每个头都是独立处理一整条序列的。第三scores Q K.transpose(-2, -1)计算的是每个查询对所有键的匹配度得到一个[batch_size, n_head, seq_len, seq_len]的注意力矩阵。除以sqrt(d_k)是为了防止点积结果过大导致梯度消失。第四mask 的处理方式是用masked_fill把不合法的位置填成负无穷这样 softmax 之后这些位置的权重会变成 0。这就是 Decoder 里“看不到未来”的实现方式。4.2 前馈网络与 Transformer BlockTransformer Block 里的第二个核心组件是前馈网络FFN。它本质上是两个全连接层对每个 token 的位置独立做非线性变换。# 文件路径mini_transformer.py续 class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout0.1): super().__init__() self.net nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model), nn.Dropout(dropout), ) def forward(self, x): return self.net(x) class TransformerBlock(nn.Module): def __init__(self, d_model, n_head, d_ff, dropout0.1): super().__init__() self.attn MultiHeadAttention(d_model, n_head, dropout) self.ffn FeedForward(d_model, d_ff, dropout) self.ln1 nn.LayerNorm(d_model) self.ln2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # Pre-Norm 残差连接 x x self.dropout(self.attn(self.ln1(x), mask)) x x self.dropout(self.ffn(self.ln2(x))) return x这里采用 Pre-Norm 结构先层归一化再进子层最后加残差。相比原始 Transformer 的 Post-NormPre-Norm 在训练时更稳定后来的 GPT 模型基本都采用了这种设计。4.3 完整模型封装# 文件路径mini_transformer.py续 class MiniTransformer(nn.Module): def __init__(self, vocab_size, d_model64, n_head4, d_ff128, num_layers3, max_seq_len32, dropout0.1): super().__init__() self.token_embedding nn.Embedding(vocab_size, d_model) self.pos_embedding nn.Embedding(max_seq_len, d_model) self.blocks nn.ModuleList([ TransformerBlock(d_model, n_head, d_ff, dropout) for _ in range(num_layers) ]) self.ln_f nn.LayerNorm(d_model) self.lm_head nn.Linear(d_model, vocab_size, biasFalse) self.max_seq_len max_seq_len def forward(self, tokens, maskNone): batch_size, seq_len tokens.size() positions torch.arange(seq_len, devicetokens.device).unsqueeze(0) x self.token_embedding(tokens) self.pos_embedding(positions) for block in self.blocks: x block(x, mask) x self.ln_f(x) logits self.lm_head(x) return logits这就是一个缩小版的 GPT。它包含四个关键部分token embedding把离散的 token ID 变成稠密向量。位置 embedding为每个位置分配一个可学习向量和 token embedding 相加。多个 Transformer Block堆叠起来逐层提取特征。输出层将最后一层表示映射回词表大小得到每个位置的预测分布。到这里你已经拥有一个可以在 CPU 上训练的最小 GPT。这段代码虽然不到一百行但完整覆盖了 Decoder-only Transformer 的全部核心机制。4.4 训练这个 Mini Transformer我们用一个极小的任务来验证模型给定一个字母序列预测下一个字母。比如输入a b c目标输出d。# 文件路径train_mini.py import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset import string from mini_transformer import MiniTransformer # 准备数据 letters string.ascii_lowercase # a-z vocab {ch: i for i, ch in enumerate(letters)} idx_to_ch {i: ch for i, ch in enumerate(letters)} vocab_size len(letters) class NextLetterDataset(Dataset): def __init__(self, seq_len4): self.seq_len seq_len self.data [] # 构造 (输入序列, 目标) 样本 for i in range(len(letters) - seq_len): seq letters[i:i seq_len] target letters[i seq_len] self.data.append((seq, target)) def __len__(self): return len(self.data) def __getitem__(self, idx): seq, target self.data[idx] seq_ids torch.tensor([vocab[ch] for ch in seq]) target_id torch.tensor(vocab[target]) return seq_ids, target_id def train(): device torch.device(cuda if torch.cuda.is_available() else cpu) model MiniTransformer(vocab_sizevocab_size).to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() dataset NextLetterDataset(seq_len4) loader DataLoader(dataset, batch_size16, shuffleTrue) for epoch in range(200): total_loss 0 for seq_ids, target_id in loader: seq_ids, target_id seq_ids.to(device), target_id.to(device) logits model(seq_ids) # [batch, seq_len, vocab] loss criterion(logits[:, -1, :], target_id) # 只取最后一个位置的预测 optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() if epoch % 20 0: print(fEpoch {epoch}, Loss: {total_loss / len(loader):.4f}) # 测试 model.eval() test_seq abcd seq_ids torch.tensor([[vocab[ch] for ch in test_seq]]).to(device) logits model(seq_ids) pred_idx logits[:, -1, :].argmax(dim-1).item() print(fInput: {test_seq}, Predict next char: {idx_to_ch[pred_idx]}) # 保存模型 torch.save(model.state_dict(), mini_transformer.pt) if __name__ __main__: train()这段代码里有一个很重要的设计训练时只取logits[:, -1, :]作为预测结果。因为我们预测的是最后一个位置的下一字母前面的位置虽然也会生成 logits但在语言建模中每个位置都对应“预测当前位置的下一个 token”。如果你想做标准的语言建模应该把所有位置的 logits 都和下一位置的 token 算 loss这里为了演示简单只关注最后一个位置。运行python train_mini.py你会看到 loss 稳步下降。训练完成后输入abcd模型应该能预测出e。这个实验虽然简单但验证了整条链路是通的。5. 实战演练用 Hugging Face 完成文本分类手写代码解决了“理解原理”的问题。但真实项目里你几乎不会从零训练一个 Transformer——成本太高、数据量不够、效果还不一定好。你要学的是另一个技能用预训练模型做迁移学习。这一节我们用 Hugging Face Transformers 库在一个真实数据集上完成情感分类任务。数据集就用经典的 IMDb 电影评论数据集任务是把评论分为正面和负面。5.1 加载数据集与分词# 文件路径train_classifier.py from datasets import load_dataset from transformers import AutoTokenizer # 加载 IMDb 数据集网络不好时首次下载会较慢 dataset load_dataset(imdb) # 使用 BERT 的分词器 model_name bert-base-uncased tokenizer AutoTokenizer.from_pretrained(model_name) def tokenize_fn(batch): return tokenizer(batch[text], paddingmax_length, truncationTrue, max_length128) dataset dataset.map(tokenize_fn, batchedTrue) dataset dataset.rename_column(label, labels) dataset.set_format(torch, columns[input_ids, attention_mask, labels]) print(dataset[train]) print(dataset[test])要点说明load_dataset(imdb)会自动下载并缓存数据。如果你的网络环境不太顺畅可以先手动下载数据集放到本地缓存目录。paddingmax_length把每条样本填充到 128 的长度truncationTrue把超长的评论截断。这两个参数保证一个 batch 内的样本形状一致。rename_column是因为 Transformers 的 Trainer 默认读取名为labels的标签列而 IMDb 数据集里叫label。5.2 加载预训练模型并配置训练器# 文件路径train_classifier.py续 from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer model AutoModelForSequenceClassification.from_pretrained( model_name, num_labels2, ) training_args TrainingArguments( output_dir./results, evaluation_strategyepoch, save_strategyepoch, num_train_epochs2, per_device_train_batch_size16, per_device_eval_batch_size32, logging_steps100, load_best_model_at_endTrue, metric_for_best_modelaccuracy, ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset[train], eval_datasetdataset[test], ) trainer.train()这里真正要理解的是AutoModelForSequenceClassification它在 BERT 的顶部加了一个分类头把最后一个位置的[CLS]向量映射到类别数。你不需要自己写分类头库已经帮你做完了。5.3 训练与保存训练命令python train_classifier.py如果用的是 CPU训练可能需要比较长时间。你可以先把num_train_epochs改成 1per_device_train_batch_size改成 8先验证流程能跑通再逐步加规模。训练结束后模型会保存到./results目录同时在./results/checkpoint-xxx里留下检查点。你可以用下面的代码加载最佳模型做推理# 文件路径predict.py from transformers import AutoModelForSequenceClassification, AutoTokenizer model_path ./results # 如果有 checkpoint改成具体路径 model AutoModelForSequenceClassification.from_pretrained(model_path) tokenizer AutoTokenizer.from_pretrained(bert-base-uncased) text This movie was absolutely brilliant! I loved every minute of it. inputs tokenizer(text, return_tensorspt, paddingmax_length, truncationTrue, max_length128) outputs model(**inputs) pred outputs.logits.argmax(dim-1).item() print(Positive if pred 1 else Negative)到这里你已经从手写模型跨到了真实项目场景加载预训练权重、适配下游任务、训练、保存、推理。这个流程覆盖了 90% 的 NLP 工程实践。6. 高效微调LoRA 实战最后一个模块也是 2026 年最值得掌握的能力高效微调。为什么要高效微调因为全量微调一个大语言模型的门槛太高了。以 Qwen 15B 模型为例全量微调时优化器状态、梯度、参数副本加起来显存消耗会达到模型本身的数倍普通开发者根本跑不动。而且全量微调会修改所有参数既慢又容易破坏预训练模型学到的通用知识。LoRALow-Rank Adaptation的思路是冻结原始权重只训练一小部分新增的低秩矩阵。这个设计背后的原理是——预训练模型已经学到了足够好的特征表示下游任务适配只需要在少数维度上做调整不需要推倒重来。6.1 用 PEFT 库实现 LoRA# 文件路径train_lora.py from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model, TaskType # 选择一个你能下载到的小模型Actuall 大小以你本地配置为准 # 示例Qwen2-1.5B也可以换成 GPT-2、Llama-2-7B 等 model_name Qwen/Qwen2-1.5B-Instruct model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypeauto, device_mapauto, ) tokenizer AutoTokenizer.from_pretrained(model_name) # 配置 LoRA lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, r8, # 低秩矩阵的秩 lora_alpha16, # 缩放系数 lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], ) model get_peft_model(model, lora_config) model.print_trainable_parameters()print_trainable_parameters()会输出类似这样的结果trainable params: 2,359,296 || all params: 1,592,037,888 || trainable%: 0.1482这个数字就是 LoRA 的核心价值你只需要训练全部参数的 0.15% 左右就能达到接近全量微调的效果。6.2 LoRA 的配置项怎么理解参数含义经验值r低秩矩阵的秩决定新增参数的容量8 ~ 64新手从 8 开始lora_alpha缩放系数控制 LoRA 分支的影响强度设置为 r 的 1~2 倍lora_dropout防止过拟合0.05 ~ 0.1target_modules要插入 LoRA 的模块注意力层的四个投影矩阵r不是越大越好。它越大新增参数越多模型越有可能在微调数据上过拟合。可以先从 8 跑起效果不够再加到 16、32。6.3 LoRA 训练流程# 文件路径train_lora.py续 from datasets import load_dataset from transformers import TrainingArguments, Trainer, DataCollatorForLanguageModeling # 构造一个简单的指令数据 dataset load_dataset(json, data_filessft_data.json) def format_instruction(example): messages [ {role: user, content: example[instruction]}, {role: assistant, content: example[output]}, ] text tokenizer.apply_chat_template(messages, tokenizeFalse) return {text: text} dataset dataset.map(format_instruction) def tokenize_fn(examples): tokenized tokenizer( examples[text], paddingmax_length, truncationTrue, max_length512, ) tokenized[labels] tokenized[input_ids] return tokenized tokenized_dataset dataset.map(tokenize_fn, batchedTrue).remove_columns([instruction, output, text]) data_collator DataCollatorForLanguageModeling(tokenizertokenizer, mlmFalse) training_args TrainingArguments( output_dir./lora_output, num_train_epochs3, per_device_train_batch_size4, gradient_accumulation_steps8, save_strategyepoch, logging_steps50, learning_rate2e-4, fp16True, ) trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_dataset[train], data_collatordata_collator, ) trainer.train() # 保存 LoRA 权重 model.save_pretrained(./lora_weights)这段代码的关键点在DataCollatorForLanguageModeling它会自动把输入序列的标签右移一位让模型学习“根据前面的词预测下一个词”。mlmFalse表示我们做的是因果语言建模而不是 BERT 那种掩码语言建模。6.4 加载 LoRA 权重做推理# 文件路径infer_lora.py from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base_model_name Qwen/Qwen2-1.5B-Instruct lora_model_path ./lora_weights # 先加载基础模型 model AutoModelForCausalLM.from_pretrained(base_model_name, device_mapauto) tokenizer AutoTokenizer.from_pretrained(base_model_name) # 再叠加 LoRA 权重 model PeftModel.from_pretrained(model, lora_model_path) messages [{role: user, content: 你好介绍一下你自己}] text tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) inputs tokenizer(text, return_tensorspt) outputs model.generate(**inputs, max_new_tokens128) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))到这里你已经完成了一条完整的高效微调链路加载基础模型、注入 LoRA、训练、保存、推理。这套方法论可以平移到任何对话模型上区别只在于target_modules要按模型的实现细节调整。7. 运行结果与效果验证很多人写完代码之后只盯着 loss 看其实验证效果有一套更完整的方法。7.1 Mini Transformer 的验证标准运行python train_mini.py你会看到类似这样的输出Epoch 0, Loss: 3.2951 Epoch 20, Loss: 2.0118 Epoch 40, Loss: 0.9563 Epoch 60, Loss: 0.4211 Epoch 80, Loss: 0.1983 Epoch 100, Loss: 0.1012 Epoch 120, Loss: 0.0615 Epoch 140, Loss: 0.0408 Epoch 160, Loss: 0.0291 Epoch 180, Loss: 0.0233 Input: abcd, Predict next char: e判断标准不是 loss 越低越好而是模型在测试输入上能否给出正确的预测。如果abcd预测出了e说明模型已经学到了字母之间的顺序关系。如果预测错误优先检查数据是否切分正确、mask 是否生效、学习率是否过高。7.2 文本分类的验证标准对于 BERT 文本分类除了看训练 loss更重要的是看 eval accuracy。你可以在 Trainer 里传入compute_metrics函数# 文件路径train_classifier.py补充 from evaluate import load import numpy as np accuracy load(accuracy) def compute_metrics(eval_pred): logits, labels eval_pred predictions np.argmax(logits, axis-1) return accuracy.compute(predictionspredictions, referenceslabels) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset[train], eval_datasetdataset[test], compute_metricscompute_metrics, )IMDb 上 BERT-base 的 accuracy 一般会在 90% 以上。如果明显低于这个水平大概率是训练轮次太少或学习率不合适。7.3 LoRA 微调的验证标准LoRA 微调后如果 loss 是下降的、生成结果在指令遵循上比基础模型有明显改善就说明微调有效。建议手动测试几条微调数据之外的样本观察模型的泛化能力而不是仅仅看训练集上的表现。如果训练集表现好但测试集表现差说明r偏大或训练轮次过多产生了过拟合。8. 常见问题与排查思路下面这些问题是新手最容易遇到的按出现频率排序。问题现象可能原因排查方式解决方案训练时 loss 不下降学习率过高打印前几个 batch 的 loss降低学习率或增加 warmup 步骤训练时 loss 直接变成 NaN数值溢出fp16检查日志中 loss 变化关闭 fp16或增加梯度裁剪加载模型时显存不足模型太大查看模型总参数量换更小的模型或用device_mapauto做层间卸载模型训练后生成结果是重复的温度参数太低检查生成配置调整 temperature 到 0.7~1.0 之间或调整 top_p形变错误size mismatch分词器与模型不匹配打印输入张量形状确保AutoTokenizer与AutoModel使用同一个model_name数据集加载失败网络不稳定查看错误信息和缓存目录手动下载数据集并指定data_dir参数LoRA 训练后模型没有变化目标模块设置错误打印可训练参数列表根据模型架构调整target_modules可以参考model.named_modules()还有一个常见问题是关于 PyTorch 张量形状的。看代码时用x.shape打印每一步的张量形状比盯着公式更有用。下面这段调试代码可以帮你快速定位形状问题# 调试工具打印张量形状 def debug_forward(model, batch): x batch print(fInput shape: {x.shape}) # [batch, seq_len] x model.token_embedding(x) print(fAfter embedding: {x.shape}) # [batch, seq_len, d_model] for i, block in enumerate(model.blocks): x block.attn.ln1(x) print(fBlock {i} attention output: {x.shape}) print(fFinal output: {model.lm_head(x).shape})9. 最佳实践与学习路线建议文章写到这里原理、代码、实战、微调都已经覆盖了。最后给你一套我认为值得遵循的实践原则。9.1 新手最重要的一条原则先缩小问题规模任何时候遇到一个复杂的 Transformer 实验第一件事就是在极小规模上验证可行性。比如把词表缩小到 100 个词把序列长度缩短到 32只训练 1 个 epoch。这能帮你快速发现代码逻辑问题而不是等到跑了 3 小时后才发现数据配错了。9.2 遇到问题先去查张量形状Transformer 代码 90% 的问题出在张量形状上。尤其是多头注意力里的view和transpose一定要自己画一遍维度变化图。建议在关键位置加print(x.shape)不要靠猜。9.3 学习顺序建议我的建议是走过这样一条链路手写一个 Mini Transformer完成本文第四章。用 Hugging Face 跑通一个基础任务分类、序列标注。用 LoRA 微调一个对话模型做出自己的小助手。再回头读原论文《Attention Is All You Need》和《The Illustrated Transformer》把细节补齐。很多人一上来就读论文读不懂就放弃了。先动手、再读论文效率会高很多。9.4 命名与配置管理微调实验很容易出现“调了一堆参数学最后忘了用的哪套”。强烈建议每次实验都建一个独立的输出目录并把 LoRA 的r、lora_alpha、学习率这些关键参数写死在配置文件名里例如lora_r8_alpha16_lr2e-4。这样后续回调模型时不至于两眼一抹黑。9.5 运行与迭代的节奏不要一次性把num_train_epochs设成 10。先设 1 个 epoch跑通之后再增加。对于 LoRA显存不足时优先考虑减小per_device_train_batch_size同时增大gradient_accumulation_steps这样可以保持有效 batch size 不变但显存压力更小。10. 总结与后续学习方向这篇文章真正想帮你跨过的门槛是“看懂原理”和“能写代码”之间的鸿沟。我们从注意力机制、QKV、多头、位置编码这些核心概念出发手写了一个完整的 Mini Transformer然后用 Hugging Face 完成了文本分类实战最后用 LoRA 实现了一次高效微调。现在你手上应该已经有三个可以跑的代码mini_transformer.py、train_classifier.py、train_lora.py。下一步你可以做三件事。第一打开MiniTransformer的代码把n_head改成 1 或者 8观察模型效果的变化这会帮你建立对多头的直觉。第二把文本分类的数据从 IMDb 换成中文的评论数据你会发现除了换分词器几乎不需要改动其他代码。第三把 LoRA 微调里的基础模型换成一个更大的模型感受显存占用和训练时长的变化。如果你准备继续深入建议按这个顺序先读 Transformer 原论文重点看公式和架构图再读 Hugging Face 官方文档里关于 Trainer 和 PEFT 的部分最后去 GitHub 找一个开源的 LoRA 微调项目读它的训练脚本看生产环境里的完整配置是什么样子。这篇文章里的三份代码可以作为你的起点建议收藏备用。从今天开始Transformer 不再是屏幕上那些大佬才能驾驭的东西——你自己已经能写它、训它、微调它了。剩下的就是在你自己的数据上把它用起来。
返回列表