☰
Transformer 从原理到实战:手写实现与 LoRA 高效微调
2026/9/26 11:32:13 网站建设 项目流程

直接在浏览器里刷到 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 jupyter

2.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,适合生成类任务。

这里用文字描述一下整体流程:

  1. 输入文本经过分词器变成 token 序列。
  2. 每个 token 通过嵌入层(Embedding)变成向量。
  3. 向量加上位置编码(Positional Encoding),得到带有顺序信息的输入表示。
  4. 输入经过若干层 Encoder。每层包含多头自注意力(Multi-Head Attention)和前馈神经网络(Feed-Forward Network),并配合残差连接(Residual Connection)与层归一化(Layer Normalization)。
  5. 输出最后一层的结果,根据需要接分类头或解码器。

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.py

4.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 memorybatch_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 0d_model 不能被 num_heads 整除调整 num_heads 为 d_model 的因数,比如 d_model=128 时 heads 可以是 8
IndexError: 位置编码长度不够输入序列长度超过 max_len增大 max_len,或使用可学习位置编码替代正弦编码
RuntimeError: shape mismatchQKV 维度转换错误检查 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% 的参数达到实际可用效果。

如果想继续深入,可以按下面路线走:

  1. 精读《Attention Is All You Need》原论文,建议边读边对照手写代码理解每一步。
  2. 阅读 BERT 论文和原始实现源码,弄清预训练与微调的关系。
  3. 研究 GPT 系列模型的 Decoder 结构和生成逻辑。
  4. 学习如何把 Transformer 应用到图像领域,从 ViT 开始,再到 Swin Transformer 的窗口注意力。
  5. 尝试用 LoRA 微调一个更大规模的大语言模型,比如 ChatGLM 系列或 Llama 系列,跑一个垂直领域问答任务。

最后提醒一句:看再多视频和文章,都不如自己把示例代码跑一遍。先跑通手写 Transformer,再跑 Hugging Face 微调,最后试一次 LoRA,整个过程可能只需要一个晚上。如果你在跑代码时遇到本文没有覆盖的问题,建议先打印中间张量的 shape,再逐段定位,这是排查深度学习代码最实用的方法。

如果这篇文章对你有帮助,可以收藏备用,后续我会继续整理 Transformer 在图像、大模型微调场景下的实战经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询