直接在浏览器里刷到 Transformer 相关的视频或文章,第一反应往往是“这是一个深度学习基础模型,很重要”,但真到自己动手跑代码时,就会发现网上资料要么只讲论文,要么只贴代码,很少有把“原理拆解→手写实现→实战应用→高效微调”串成一条线讲的。本文想解决的就是这个问题:用一套闭环的学习路径,把 Transformer 从注意力机制到位置编码、从手写编码器到 Hugging Face 实战,最后再到 LoRA 高效微调,完整走一遍。内容以代码为核心,穿插必要的原理说明,适合刚接触 Transformer 的初学者,也适合想快速落地 NLP 或视觉任务的开发者。
1. 背景与核心概念
1.1 Transformer 是什么
Transformer 最初由 Google 在 2017 年的论文《Attention Is All You Need》中提出,是一种基于注意力机制(Attention Mechanism)的深度学习模型架构。与传统的循环神经网络(RNN)和卷积神经网络(CNN)不同,Transformer 完全依赖自注意力(Self-Attention)来捕捉输入序列中各个元素之间的依赖关系,因此天然具备处理长序列的能力,并且可以高度并行化训练。
通俗地理解,Transformer 在做的事情是:给输入序列中的每个词计算一个“上下文相关”的向量表示。这个向量不再是固定词向量,而是根据它和其他词的关联程度动态生成的。比如“苹果”在“苹果很好吃”和“苹果公司发布了新手机”中,与其他词的关联权重完全不同,Transformer 能根据注意力权重自动区分这种语义差异。
当前主流的预训练模型,如 BERT、GPT、T5、ViT、Swin Transformer 等,底层架构都是 Transformer。无论你后续是做文本分类、命名实体识别、机器翻译、文本生成、时间序列预测,还是图像分类、目标检测,理解 Transformer 都是必须跨过的一道门槛。
1.2 Transformer 解决什么问题
在 Transformer 出现之前,处理序列数据主要依赖 RNN 及其变体(如 LSTM、GRU)。RNN 的核心思路是按时间步依次读取输入,把上一个时间步的隐状态传递到下一个时间步。这种结构存在两个明显问题:
第一,长距离依赖难以建模。当输入序列较长时,信息需要经过多个时间步逐步传递,较早的信息容易在传递过程中被“稀释”或遗忘。用术语说,就是容易发生梯度消失或梯度爆炸。
第二,不能并行计算。由于每个时间步必须等前一个时间步计算完成,RNN 的训练速度受限于序列长度。即使后来有了 Transformer-XL、LSTM 的各种变体,也没能彻底解决并行性的问题。
Transformer 换了一个思路:不再按顺序逐个处理词,而是把整个序列同时输入模型,通过注意力机制直接计算任意两个位置之间的关联。这样不仅解决了长距离依赖问题,也让训练可以大规模并行,从而支撑起后续像 GPT-3 这样千亿参数级别的大模型训练。
1.3 常见应用场景
Transformer 的应用场景早已超出自然语言处理范畴,下面列几类最常见的场景:
- 自然语言处理(NLP):文本分类、机器翻译、问答系统、命名实体识别、情感分析、文本摘要、对话生成等。
- 计算机视觉(CV):Vision Transformer(ViT)把图像切块后当作序列输入,用于图像分类、目标检测、语义分割。
- 语音处理:语音识别、语音合成、音频分类。
- 时序预测:把历史时间窗口作为序列输入 Transformer,预测未来值,常用于销量预测、流量预测、气象预测。这也是“Transformer 预测正弦函数”“Transformer 时序预测”这类热搜词背后代表的应用方向。
- 多模态:同时处理文本、图像、音频等多种模态输入,例如 CLIP、LLaVA。
1.4 为什么需要掌握 Transformer
从找工作、做项目、发论文三条线看,Transformer 都是基础能力。
面试中,Transformer 是深度学习岗位的必考内容,从“介绍一下 Self-Attention”到“为什么用 LayerNorm 而不是 BatchNorm”都有可能被问到。做项目时,绝大多数 NLP 任务可以直接用 Hugging Face 提供的预训练 Transformer 模型微调,不需要自己从零训练,掌握调用和微调方法就能完成业务需求。做研究时,无论是改进注意力机制、设计新的位置编码,还是把 Transformer 迁移到新领域,都需要先理解原始架构的每一处细节。
因此本文的路线是:先用通俗方式理解核心思想,再手写一个 mini Transformer 来加深印象,然后结合 Hugging Face 完成一个文本分类实战,最后介绍 LoRA 高效微调的方法和完整代码。
2. 环境准备与版本说明
2.1 推荐环境配置
下面以常见环境为例,版本需要根据你的项目实际情况调整。如果你使用的是 GPU 服务器,请先确认 CUDA 和显卡驱动版本;如果本机没有可用 GPU,也可以在 CPU 上运行,只是训练速度会慢一些。
建议环境如下:
- 操作系统:Windows 10/11、Ubuntu 20.04/22.04 均可。Windows 用户建议使用 WSL2 或 Anaconda Prompt 执行命令。
- Python:3.9 或 3.10。
- 深度学习框架:PyTorch 2.0 或以上。
- Transformers 库:4.30 以上版本。
- 其他依赖:datasets、accelerate、peft、numpy、pandas、scikit-learn。
- 硬件:CPU 可以运行全部示例;训练效率更高时建议 NVIDIA GPU(8GB 显存足够跑本文的 LoRA 示例)。
2.2 安装依赖
推荐使用 conda 创建虚拟环境,避免污染系统 Python。
conda create -n transformer-tutorial python=3.10 conda activate transformer-tutorial然后安装 PyTorch。CPU 版本和执行对应命令,GPU 版本请到 PyTorch 官网按 CUDA 版本选择对应命令。
# CPU 版本(适用于无 GPU 环境) pip install torch torchvision torchaudio接着安装 Hugging Face 相关库:
pip install transformers datasets accelerate peft如果希望使用 Jupyter Notebook 上课,可以额外安装 jupyter:
pip install jupyter2.3 验证环境
安装完成后,可以运行下面这段代码验证环境是否正常:
import torch import transformers print("PyTorch 版本:", torch.__version__) print("Transformers 版本:", transformers.__version__) if torch.cuda.is_available(): print("GPU:", torch.cuda.get_device_name(0)) print("显存:", torch.cuda.get_device_properties(0).total_memory / 1024**3, "GB") else: print("当前使用 CPU 模式")如果打印出对应版本号,说明环境已经准备好。下面进入核心原理部分。
3. Transformer 架构核心原理
3.1 整体架构图
Transformer 的原始结构由 Encoder(编码器)和 Decoder(解码器)两部分组成。Encoder 负责把输入序列转成一组上下文相关的向量表示,Decoder 负责根据 Encoder 的输出和已经生成的内容,逐步生成目标序列。
不过在实际开发中,很多任务只需要使用其中一个部分:BERT 只用 Encoder,适合理解类任务;GPT 只用 Decoder,适合生成类任务。
这里用文字描述一下整体流程:
- 输入文本经过分词器变成 token 序列。
- 每个 token 通过嵌入层(Embedding)变成向量。
- 向量加上位置编码(Positional Encoding),得到带有顺序信息的输入表示。
- 输入经过若干层 Encoder。每层包含多头自注意力(Multi-Head Attention)和前馈神经网络(Feed-Forward Network),并配合残差连接(Residual Connection)与层归一化(Layer Normalization)。
- 输出最后一层的结果,根据需要接分类头或解码器。
3.2 Self-Attention 的原理:从 QKV 说起
自注意力是 Transformer 最核心的部分。为了理解它,先引入三个概念:Query(查询)、Key(键)、Value(值)。
可以这样联想:你在图书馆找一本书。Query 是你想查的关键词(比如“Transformer”);Key 是每本书的标签或索引(比如“深度学习”“自然语言处理”);Value 是书本身的内容。
计算过程分为四步:
第一步,将输入向量乘以三个权重矩阵,分别得到 Q、K、V:
Q = X @ W_Q K = X @ W_K V = X @ W_V第二步,计算每个 Query 与所有 Key 的点积,再除以缩放因子,得到注意力分数:
scores = Q @ K.T / sqrt(d_k)第三步,用 Softmax 把分数转成总和为 1 的权重:
weights = softmax(scores)第四步,用权重对所有 Value 做加权求和,得到新的输出向量:
output = weights @ V整个过程用一句话概括:先计算“应该关注谁”,再按关注度把信息聚合起来。
这里有一个容易被新手忽略的细节:除以根号 d_k。d_k 是 Key 的维度,除以它可以防止点积结果过大导致 Softmax 进入饱和区,梯度变得极小。这也是论文中“Scaled”的含义。
3.3 Multi-Head Attention:让模型关注不同的子空间
如果只做一次自注意力,所有头共享同一套 QKV,表达能力的上限会比较有限。多头注意力做的事情是:把 Q、K、V 拆成多份,让每一份独立做注意力计算,最后把结果拼接起来。
例如 d_model=512,num_heads=8,则每个头的维度是 64。每一头可以关注序列中不同的关系类型:有的头关注相邻词的语法关系,有的头关注远距离的指代关系,有的头关注句法结构。多头机制提升了模型的表达能力。
下面是一个简单易懂的多头注意力示意图描述:
- 输入 X 分别经过三个线性层得到 Q、K、V。
- Q、K、V 被拆成 8 份,每份维度为 64。
- 每一份独立计算注意力,输出维度为 64。
- 8 个结果拼接成 512 维,再经过一个线性层输出。
3.4 Positional Encoding:Transformer 如何感知顺序
RNN 是按时间步依次读取输入的,所以天然有顺序概念。Transformer 是把整个序列并行输入的,如果不加额外处理,模型会把“我喜欢你”和“你喜欢我”看成完全相同的输入,因为它们的 token 集合和词嵌入完全相同。
为了解决这个问题,Transformer 引入位置编码,把位置信息加到输入嵌入中:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))其中 pos 表示位置,i 表示维度索引。这种做法使用正弦和余弦函数生成位置编码,好处是位置编码可以泛化到比训练时更长的序列。
在手写 Transformer 时,也可以使用可学习的绝对位置嵌入(Learnable Positional Embedding)。BERT、GPT 都使用这种方案。图像领域的 ViT 也采用可学习位置嵌入,但为了更好的平移等变性,目前也有一些相对位置编码的方案。
3.5 残差连接与 LayerNorm
在每层子层(注意力层、前馈网络)后,都有残差连接和层归一化:
x = x + sublayer(x) x = LayerNorm(x)残差连接解决深层网络梯度消失问题,让梯度可以直接从输出层传回输入层。LayerNorm 对每个样本的所有特征维度做归一化,稳定训练过程。这里要特别区分 LayerNorm 和 BatchNorm:BatchNorm 是跨样本、对每个特征通道归一化;LayerNorm 是单个样本内跨特征维度归一化。Transformer 选用 LayerNorm 的原因是它不依赖 batch size,并且在变长序列处理中表现更好。
3.6 前馈神经网络与激活函数
每个 Encoder 层包含一个两层的全连接网络:
FFN(x) = max(0, x @ W1 + b1) @ W2 + b2中间层维度一般比 d_model 大四倍,先升维再用 ReLU(或 GELU)激活,再降维回 d_model。这个设计的直觉是:注意力层负责在序列位置之间交换信息,前馈层负责在每个位置上做非线性变换和特征映射。
4. 手写一个 mini Transformer
4.1 项目结构
这一节我们用一个极简实现的 Transformer Encoder 来跑通一个小任务:对随机序列做特征提取。目的不是训练出多好的模型,而是把每一行代码和前面讲的原理对应起来。
创建项目目录:
transformer-from-scratch/ ├── mini_transformer.py └── run.py4.2 完整实现
下面是mini_transformer.py的核心代码,是一个可以运行的简化版 Transformer Encoder。
import math import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): """多头自注意力机制""" def __init__(self, d_model, num_heads): super().__init__() assert d_model % num_heads == 0 self.d_model = d_model self.num_heads = num_heads self.d_k = d_model // num_heads 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) def forward(self, x): batch_size, seq_len, _ = x.size() Q = self.W_Q(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) K = self.W_K(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) V = self.W_V(x).view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k) weights = F.softmax(scores, dim=-1) output = weights @ V output = output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) return self.W_O(output) class PositionalEncoding(nn.Module): """正弦余弦位置编码""" def __init__(self, d_model, max_len=5000): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): return x + self.pe[:, :x.size(1)] class EncoderLayer(nn.Module): """单层 Encoder""" def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super().__init__() self.attention = MultiHeadAttention(d_model, num_heads) self.ffn = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): # 子层1:多头注意力 + 残差 + LayerNorm attn_output = self.attention(x) x = self.norm1(x + self.dropout(attn_output)) # 子层2:前馈网络 + 残差 + LayerNorm ffn_output = self.ffn(x) x = self.norm2(x + self.dropout(ffn_output)) return x class MiniTransformer(nn.Module): """完整 Transformer Encoder""" def __init__(self, vocab_size, d_model, num_heads, num_layers, d_ff, max_len=128): super().__init__() self.embedding = nn.Embedding(vocab_size, d_model) self.positional_encoding = PositionalEncoding(d_model, max_len) self.layers = nn.ModuleList([ EncoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers) ]) def forward(self, x): x = self.embedding(x) x = self.positional_encoding(x) for layer in self.layers: x = layer(x) return x代码中的关键点:
MultiHeadAttention中先把 Q、K、V 拆成 num_heads 份,在transpose(1, 2)之后,每个 head 独立计算注意力,最后拼接后通过 W_O 输出。PositionalEncoding使用 register_buffer 注册位置编码,不参与训练,但会随模型一起迁移到 GPU 或 CPU。EncoderLayer严格按照“x + sublayer(x)”的顺序完成残差连接和 LayerNorm。MiniTransformer把嵌入层、位置编码、多层 Encoder 串起来。
4.3 运行示例
编写一个简单的run.py,验证模型能前向传播:
import torch from mini_transformer import MiniTransformer # 模拟一个 batch:batch_size=2, seq_len=10 x = torch.randint(0, 1000, (2, 10)) model = MiniTransformer(vocab_size=1000, d_model=128, num_heads=4, num_layers=3, d_ff=512) output = model(x) print("输出形状:", output.shape)运行命令:
python run.py预期输出:
输出形状: torch.Size([2, 10, 128])这说明模型成功把输入长度为 10 的 token 序列转换成了 shape 为 (batch_size, seq_len, d_model) 的向量序列。此时每个 token 的表示已经融合了序列中其他 token 的信息,这正是 Transformer 的优势所在。
4.4 手写代码的意义
很多同学觉得直接调 Hugging Face 就够了,没必要手写。这里有一个真实的课堂经验:面试时被问“多头注意力的蝴蝶矩阵怎么变换”“为什么 scores 要除以根号 d_k”,如果只用过model.forward(),很难完全答清楚。手写一遍之后,这些细节就成了肌肉记忆,后面看大模型源码、做模型结构改造都会顺畅很多。
5. 实战:基于 Hugging Face 的文本分类
5.1 任务说明
这一节用 Transformers 库完成一个中文情感二分类任务:判断一条评论是正面还是负面。为了环境友好,使用 uer/roberta-base-finetuned-jd-binary-chinese 这种较小规模的模型,或者使用 bert-base-chinese 也可以。如果显存不够,可以把 batch_size 调小,或使用 CPU 运行推理。
5.2 加载数据集和图谱
先创建一个text_classification.py:
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments from datasets import load_dataset # 使用在线数据集中带 sentiment 字段的中文评论数据 dataset = load_dataset("seamew/ChnSentiCorp") print(dataset)ChnSentiCorp 是开源的中文评论情感分类数据集,包含 1 万条左右带 label 的句子,0 表示负面,1 表示正面。
5.3 数据预处理
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") def preprocess_function(examples): return tokenizer( examples["text"], truncation=True, max_length=128, padding="max_length" ) encoded_dataset = dataset.map(preprocess_function, batched=True) encoded_dataset = encoded_dataset.rename_column("label", "labels") encoded_dataset.set_format("torch", columns=["input_ids", "attention_mask", "labels"])这里需要注意:padding="max_length"会把所有样本统一填充到 128 个 token。attention_mask用于告诉模型哪些位置是真实内容,哪些位置是 padding,模型在计算注意力时会忽略 padding 位置。
5.4 模型定义与训练
model = AutoModelForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=2 ) training_args = TrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=16, eval_strategy="epoch", save_strategy="epoch", logging_dir="./logs", logging_steps=100, ) trainer = Trainer( model=model, args=training_args, train_dataset=encoded_dataset["train"], eval_dataset=encoded_dataset["validation"], ) trainer.train()如果遇到显存不足(OOM),可以把 batch_size 调整为 8 或 4。如果希望训练更快,可以使用bert-base-chinese的蒸馏版本distilbert-base-chinese。
5.5 保存与推理
训练完成后保存模型:
model.save_pretrained("./sentiment_model") tokenizer.save_pretrained("./sentiment_model")加载模型做预测:
from transformers import pipeline pipe = pipeline("text-classification", model="./sentiment_model", tokenizer="./sentiment_model") texts = [ "发货速度很快,商品质量非常好", "颜色款式还行,可惜客服态度太差" ] for text in texts: result = pipe(text) print(text, "=>", result)输出类似:
发货速度很快,商品质量非常好 => [{'label': 'LABEL_1', 'score': 0.99}] 颜色款式还行,可惜客服态度太差 => [{'label': 'LABEL_0', 'score': 0.82}]其中LABEL_1对应正面,LABEL_0对应负面。
6. 高效微调:LoRA 实战
6.1 为什么需要高效微调
大模型的参数量动辄几十亿甚至上千亿。如果直接微调全部参数,每个任务都需要保存一份完整的模型副本,显存和存储压力都很大。例如一个 7B 参数的模型,以 FP16 精度存储也需要约 14GB 显存,加上梯度、优化器状态,训练显存会更夸张。
高效微调(Parameter-Efficient Fine-Tuning,PEFT)的核心思想是:冻结预训练模型的绝大多数参数,只训练少量新增参数,同时尽量保持模型效果。常见方法包括:
- LoRA(Low-Rank Adaptation):在权重矩阵旁加入低秩分解矩阵。
- Prefix Tuning:在 attention 的 Key 和 Value 前添加可学习的提示向量。
- P-Tuning:在输入端添加可学习的 prompt embedding。
- Adapter:在 Transformer 层之间插入小型全连接网络。
其中 LoRA 是目前应用最广泛、效果最稳定的方法,也是本文的重点。
6.2 LoRA 核心原理
LoRA 的思想并不复杂。它假设模型微调时的权重更新量 ΔW 是低秩的。对于预训练权重矩阵 W ∈ R^(d×d),LoRA 不直接更新 W,而是学习两个小矩阵:
ΔW = A @ B其中 A ∈ R^(d×r),B ∈ R^(r×d),r 远小于 d。训练时冻结原始 W,只更新 A 和 B。推理时可以把 A@B 合并回 W,因此不会增加推理延迟。
LoRA 的典型配置参数:
- r:低秩维度,常用 8、16、32。r 越大,可学习的参数量越多,表达能力越强,但也不能过大。
- alpha:缩放系数,一般设置为 r 的 1 倍或 2 倍。实际使用时常设 lora_alpha=16、32。
- dropout:LoRA 层的 dropout 概率,减少过拟合。
- target_modules:需要加 LoRA 的模块名,例如 q_proj、v_proj。
6.3 使用 PEFT 库实现 LoRA
这里以bert-base-chinese微调为例,演示完整流程:
from transformers import AutoModelForSequenceClassification, AutoTokenizer, Trainer, TrainingArguments from datasets import load_dataset from peft import get_peft_model, LoraConfig, TaskType # 1. 加载基础模型与分词器 model_name = "bert-base-chinese" model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) tokenizer = AutoTokenizer.from_pretrained(model_name) # 2. 配置 LoRA lora_config = LoraConfig( task_type=TaskType.SEQ_CLS, r=8, lora_alpha=16, lora_dropout=0.1, target_modules=["query", "value"], ) # 3. 用 peft 包装模型 peft_model = get_peft_model(model, lora_config) # 4. 打印可训练参数数量 peft_model.print_trainable_parameters()运行后会输出类似:
trainable params: 294912 || all params: 102466565 || trainable%: 0.2878也就是说,在 BERT-base 上只训练约 0.28% 的参数,就能达到接近全参数微调的效果。
接着按照前面相同的 Trainer 流程进行训练,只是把 model 换成peft_model。
# 继续使用前面的 encoded_dataset training_args = TrainingArguments( output_dir="./lora_results", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=16, eval_strategy="epoch", ) trainer = Trainer( model=peft_model, args=training_args, train_dataset=encoded_dataset["train"], eval_dataset=encoded_dataset["validation"], ) trainer.train() # 保存 LoRA 权重(只有几百 MB) peft_model.save_pretrained("./sentiment_lora")6.4 加载 LoRA 模型做推理
推理时可以使用 PeftModel.from_pretrained 加载:
from peft import PeftModel base_model = AutoModelForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=2) lora_model = PeftModel.from_pretrained(base_model, "./sentiment_lora") pipe = pipeline("text-classification", model=lora_model, tokenizer=tokenizer) print(pipe("这个产品质量出乎意料地好"))这里有一个重要区别:LoRA 保存的是低秩矩阵 A 和 B,不是完整模型。推理时必须先加载原始基础模型,再加载 LoRA 适配器。这也是 LoRA 可以“一个基础模型,多个任务适配器”的原因:换任务时只需更换适配器文件,不需要复制完整模型权重。
6.5 LoRA 在不同领域的扩展
LoRA 不仅适用于文本分类,也适用于:
- 大语言模型指令微调:比如在 Llama、ChatGLM 上使用 LoRA 微调,让模型学会特定领域的问答格式。
- 文生图模型微调:Stable Diffusion 的 LoRA 训练可以生成特定风格或特定人物的图像。
- 多模态模型:在视觉语言模型上应用 LoRA,减少跨模态微调成本。
如果你以后看到“LoRA 训练”“lora 微调”“PEFT”这些词,应该能明白它们指的是同一类技术:冻结大部分参数,只训练一小部分低秩增量。
7. 常见问题与排查清单
7.1 显存不足(OOM)
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练时报 CUDA out of memory | batch_size 过大、序列过长 | 调小 batch_size;限制 max_length 为 128 或 64;使用 gradient_accumulation_steps 模拟大 batch |
| 推理时报 OOM | 模型和输入同时占满显存 | 使用 batch_size=1;开启 FP16 推理;使用 CPU 推理(速度略慢) |
| LoRA 训练仍显存不足 | 基础模型太大 | 选择更小的基础模型;使用 8-bit 量化加载(load_in_8bit) |
7.2 模型效果差
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练 loss 不下降 | 学习率过大或过小 | 尝试 2e-5、3e-5、5e-5 之间的学习率;检查 label 是否对应正确 |
| 验证集准确率低 | 数据量不足或类别不均衡 | 增加数据增强;检查数据集标签分布;尝试用更大的预训练模型 |
| 过拟合明显 | LoRA r 值过大、epoch 过多 | 减小 r;设置 lora_dropout=0.1;增加 weight_decay;减少 epoch |
| 中文任务效果差 | 使用了不合适的 tokenizer | 中文任务优先使用 bert-base-chinese 或 chinese-roberta 系列 |
7.3 代码运行常见报错
| 报错信息 | 常见原因 | 解决思路 |
|---|---|---|
| AssertionError: d_model % num_heads must be 0 | d_model 不能被 num_heads 整除 | 调整 num_heads 为 d_model 的因数,比如 d_model=128 时 heads 可以是 8 |
| IndexError: 位置编码长度不够 | 输入序列长度超过 max_len | 增大 max_len,或使用可学习位置编码替代正弦编码 |
| RuntimeError: shape mismatch | QKV 维度转换错误 | 检查 view 和 transpose 后的 shape,建议打印中间张量 shape 调试 |
| AttributeError: 'Model' object has no attribute 'query' | target_modules 配置不正确 | 先打印模型结构,确认注意力层模块名,再设置 target_modules |
7.4 关于“编码器有多少层”的疑问
Transformer 原文用了 6 层 Encoder 和 6 层 Decoder。BERT-base 用了 12 层,BERT-large 用了 24 层。实际使用中,层数由模型决定,不需要手动配置。如果你想做“手写 Transformer”,只需要把num_layers设置成任意整数,模型会自动叠加对应数量的 Encoder 层。
8. 最佳实践与工程建议
8.1 数据与配置
数据集划分不要只看训练集和测试集整体准确率。建议固定随机种子,把原始数据按 8:1:1 划分为训练、验证、测试三段。验证集用于调参,测试集只在最终评估时使用一次,防止“对着测试集调参”带来的数据泄漏。
分词参数建议统一固定:max_length、truncation、padding在训练和推理阶段保持一致。否则训练长度合理但推理被截断,效果会有偏差。
8.2 训练技巧
- 学习率:BERT 微调常用 2e-5 到 5e-5,LoRA 微调时可以稍大一些,例如 1e-4,但需要配合 warmup steps。
- 优化器:Hugging Face Trainer 默认使用 AdamW,对大多数任务够用。如果使用更大模型,可以考虑 AdaFactor 节省显存。
- 混合精度:在 GPU 上训练时开启 fp16,能显著减少显存占用并加速训练。在 CPU 上不要开启 fp16。
- 日志与保存:TrainingArguments 中开启
logging_strategy="steps"、logging_steps=50,可以观察训练过程中的 loss 变化;save_strategy="epoch"可以保留每个 epoch 的 checkpoint,方便中断恢复。
8.3 安全与权限边界
如果是公司项目,请关注以下几点:
- 预训练模型权重有各自的开源许可,商用前确认许可是否符合要求。
- 微调数据中包含用户个人信息时,要提前脱敏。
- 在正式环境部署模型前,业务方需要明确模型的适用边界,比如哪些输入不能接受、哪些输出需要人工复核。
- 涉及模型更新时,应先在测试环境做 A/B 对比,确认不劣化后再全量发布。
8.4 可复现性
设置随机种子是深度学习实验里最容易忽略的步骤:
def set_seed(seed: int): import random import numpy as np import torch random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) set_seed(42)同时把环境依赖固化成requirements.txt:
torch>=2.0 transformers>=4.30 datasets>=2.10 peft>=0.5 accelerate>=0.20这样换机器重新训练时结果可以基本保持一致。
9. 扩展学习路线
到这里,你已经完成了一条 Transformer 从理论到实践的入门路径。总结一下掌握的重点:
- Transformer 解决的核心问题:长距离依赖与并行训练。
- Self-Attention 的四步流程:生成 QKV、计算缩放点积、Softmax、加权求和。
- Multi-Head Attention 与位置编码的实现思路。
- 手写 mini Transformer Encoder 并完成前向传播。
- 使用 Hugging Face 加载预训练模型完成文本分类训练。
- 使用 LoRA 高效微调,只训练约 0.28% 的参数达到实际可用效果。
如果想继续深入,可以按下面路线走:
- 精读《Attention Is All You Need》原论文,建议边读边对照手写代码理解每一步。
- 阅读 BERT 论文和原始实现源码,弄清预训练与微调的关系。
- 研究 GPT 系列模型的 Decoder 结构和生成逻辑。
- 学习如何把 Transformer 应用到图像领域,从 ViT 开始,再到 Swin Transformer 的窗口注意力。
- 尝试用 LoRA 微调一个更大规模的大语言模型,比如 ChatGLM 系列或 Llama 系列,跑一个垂直领域问答任务。
最后提醒一句:看再多视频和文章,都不如自己把示例代码跑一遍。先跑通手写 Transformer,再跑 Hugging Face 微调,最后试一次 LoRA,整个过程可能只需要一个晚上。如果你在跑代码时遇到本文没有覆盖的问题,建议先打印中间张量的 shape,再逐段定位,这是排查深度学习代码最实用的方法。
如果这篇文章对你有帮助,可以收藏备用,后续我会继续整理 Transformer 在图像、大模型微调场景下的实战经验。