ARTICLE DETAIL

资讯详情

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

Transformer原理与PyTorch手写实战:从自注意力到LoRA微调

Transformer原理与PyTorch手写实战:从自注意力到LoRA微调 很多朋友刷到过类似的视频封面写着“Transformer 从入门到天花板”“保姆级精讲”点进去弹幕齐刷刷“学会了”可一关屏幕连 Positional Encoding 的代码都写不出来。原因不是你不聪明而是视频节奏太快、信息密度太高缺少一份能随时翻、照着写的文字版笔记。这篇文章就把 Transformer 从原理到实战完整拆一遍先用通俗的方式讲清 Self-Attention、多头注意力、位置编码这些核心概念然后带你用 PyTorch 从零手写一个可运行的 Transformer再介绍如何用 LoRA 做高效微调把预训练模型真正用到业务场景里。不追求“一小时速成”只追求看完能动手、写完能跑、跑完能懂。阅读本文你不需要提前掌握复杂数学只要有一点 Python 基础和基本的深度学习概念即可。如果你是第一次接触 Transformer建议按章节顺序阅读如果你已经看过一些视频但没写过代码可以直接跳到第 4 节对照代码把整个流程串起来。1. 一文读懂 Transformer它是什么解决什么问题1.1 Transformer 解决了什么问题在 Transformer 出现之前NLP 领域最常用的序列模型是 RNN循环神经网络比如 LSTM、GRU。这类模型的特点是“串行”处理输入一个词一个词往后读当前时间步的输出依赖上一步的隐藏状态。这种串行方式有两个明显问题计算无法并行训练速度慢长序列上的效率更差。距离较远的信息需要通过多个时间步传递容易丢失或衰减这就是“长距离依赖”问题。虽然 LSTM 通过门控机制缓解了一部分但本质上没有完全解决。2017 年Google 在论文Attention Is All You Need中提出了 Transformer。它完全抛弃了循环结构只靠“注意力机制”来建模序列中任意两个位置之间的关系。因为每个词都可以直接和其他所有词计算相关度Transformer 天然擅长捕捉长距离依赖并且可以大规模并行计算。后来大家都把这篇论文当作“大模型时代”的开端。基于 Transformer 的模型在翻译、文本生成、问答、图像分类、目标检测、时序预测等领域全面开花。1.2 Transformer 的核心思想Transformer 的核心思想可以概括成一句话让序列中的每个元素通过注意力机制动态地聚合其他元素的信息。举个例子。在句子“小明把篮球递给小红因为它很重”中“它”指代的是“篮球”而不是“小明”。RNN 要理解这个指代关系需要从句尾一步步向前回溯信息传递路径很长而 Transformer 中的“它”可以直接和高注意力权重的“篮球”建立连接一步到位。这种“每个元素和所有元素交互”的操作就是自注意力机制Self-Attention。自注意力是 Transformer 的基本组成单元也是理解整个模型的关键。1.3 应用场景Transformer 不是一个具体的“模型”而是一种通用架构。围绕这个架构衍生出了大量模型和场景NLP 领域BERT 用于文本分类、命名实体识别、阅读理解GPT 系列用于文本生成、对话、代码生成T5 用于翻译和摘要。图像领域Vision TransformerViT把图像切块后当作序列输入 TransformerSwin Transformer 进一步引入层级结构大幅提升了视觉任务的性能。目标检测DETR 将目标检测建模为集合预测问题YOLO 系列也在后期版本中引入了 Transformer 模块来增强特征表达。时序预测Transformer 可以处理多变量时间序列很多金融、能源、气象预测方案都基于 Transformer 改造。多模态CLIP、Flamingo 等模型用 Transformer 联合建模文本和图像。可以说无论你想做 NLP、CV 还是多模态Transformer 都是绕不开的基本功。这也是为什么入门阶段就要把它吃透。2. 环境准备与版本说明2.1 运行环境本文的代码以 Python 和 PyTorch 为基础。为了让你在任何机器上都能快速实践代码没有依赖高显存CPU 也能跑通小型任务。建议环境如下Python 3.8 及以上PyTorch 2.xCUDA可选有 GPU 训练更快没有也能跑transformers、datasets、peft 库如果你用的是 Windows推荐安装 Anaconda管理 Python 环境更方便。如果是 Linux 服务器同样建议用 conda 或 venv 创建独立环境避免系统 Python 环境被污染。2.2 依赖库安装创建并激活虚拟环境后执行下面命令安装依赖pip install torch pip install transformers datasets peft如果你的机器已经安装了 CUDA 驱动可以用 PyTorch 官网对应的命令安装 GPU 版。版本需要根据你的实际环境调整本文以常见版本为例重点演示思路。2.3 示例项目结构为了让代码清晰我建议按下面的结构组织文件transformer-tutorial/ ├── model.py # 从零手写的 Transformer 模型 ├── train.py # 训练和推理脚本 └── finetune.py # 基于 Hugging Face 的 LoRA 微调示例后面章节的代码会按照这个文件路径给出。如果你是新手不需要一开始就理解每一行边写边运行边改效果最好。3. Transformer 核心原理拆解3.1 从 RNN 到 Attention 的演进先回顾一下 RNN 的公式方便对比h_t tanh(W_h * h_{t-1} W_x * x_t) y_t W_y * h_t其中h_t是当前时刻的隐藏状态它把所有历史信息压缩成一个固定向量。输入越长这个“压缩瓶”的负担越大后面的信息很容易把前面的信息“挤掉”。Attention 的思路是不再强制压缩历史信息而是把历史信息全部保留在预测当前词时动态地去“查”最相关的历史信息。这和人在阅读时的行为很像不是把整篇文章背下来而是需要某处信息时去定位到对应位置。Transformer 把这个思路做到了极致每一层都让序列元素两两交互并且这个交互是并行的。3.2 Self-Attention 的计算过程自注意力的输入通常记为X形状是(seq_len, d_model)。其中seq_len是序列长度d_model是每个词的向量维度。注意力计算的三个关键角色是 Query、Key、ValueQuery当前词“主动查询”的向量表示“我要找什么”。Key其他词被查询时提供的“标签”向量表示“我是什么”。Value其他词真正贡献的“内容”向量。它们的计算方式很简单Q X * W_Q K X * W_K V X * W_V这里的W_Q、W_K、W_V都是可学习的权重矩阵。注意力分数通过 Query 和 Key 的点积得到再经过 softmax 归一化最后对 Value 做加权求和。标准公式如下Attention(Q, K, V) softmax(Q * K^T / sqrt(d_k)) * V为什么要除以sqrt(d_k)因为当维度d_k变大时点积数值也会变大softmax 容易进入饱和区梯度会非常小。除以根号维度后点积的方差被压在 1 附近训练更稳定。用一句话描述自注意力就是把当前词和其他所有词的相似度算出来再按相似度权重聚合它们的值。3.3 多头注意力机制原始 Transformer 没有只用一个注意力头而是把d_model切成h份每个头在维度d_k d_model / h上独立做自注意力最后拼接起来再经过一个线性层。这就是多头注意力Multi-Head Attention。多头的好处是每个头可以关注不同类型的信息。比如一个头可能关注语法依赖一个头可能关注邻近词另一个头可能关注远程指代。不同头相当于不同的“视角”组合起来表达能力更强。代码层面的本质就是把 Q、K、V 从(B, L, d_model)变成(B, h, L, d_k)。在最后一维上分别做注意力计算。再把结果拼接回(B, L, d_model)。3.4 位置编码自注意力机制本身对位置不敏感。你把“我打你”和“你打我”输入模型如果不加位置信息模型看到的词集合完全一样无法区分语序。原始 Transformer 使用正弦余弦函数生成位置编码PE(pos, 2i) sin(pos / 10000^(2i / d_model)) PE(pos, 2i 1) cos(pos / 10000^(2i / d_model))其中pos是位置索引i是维度索引。不同维度的正弦周期不同这样每个位置都拥有一串“独一无二的指纹”并且模型可以通过三角恒等式感知相对位置。后来很多模型直接用可学习的位置嵌入比如 BERT 就是学习一个固定大小的位置向量矩阵。两者本质上都是把位置信息注入到输入中让注意力能够区分“第 3 个词”和“第 8 个词”。3.5 编码器与解码器结构原始 Transformer 是一个 Encoder-Decoder 架构编码器Encoder读取输入序列输出一组上下文表示内部由多个 Encoder Layer 堆叠而成。解码器Decoder根据编码器输出和已生成的目标序列逐步生成输出。编码器层包含两个子层多头自注意力层。前馈全连接层Feed-Forward Network。每一层都使用残差连接Residual Connection和层归一化Layer Normalization。解码器层比编码器层多一个子层带掩码的多头自注意力层。编码器-解码器交叉注意力层。前馈全连接层。带掩码是因为自回归生成时不能看未来的词所以需要用掩码矩阵把未来位置遮住。很多入门视频会问Transformer 的编码部分有多少个编码器原始论文用的是 N6也就是 6 个编码器层和 6 个解码器层。工程中 N 是可调的比如 BERT-base 用了 12 层编码器。4. 手写一个可运行的 Transformer代码实战纸上谈兵终觉浅。下面我们用 PyTorch 从零手写一个完整可运行的 Transformer 模型。代码会保证“麻雀虽小五脏俱全”重点是让你看清每个模块的输入输出形状。4.1 位置编码实现# 文件路径model.py import math import torch import torch.nn as nn import torch.nn.functional as F class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.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) # (1, max_len, d_model) self.register_buffer(pe, pe) def forward(self, x): # x: (B, L, d_model) return x self.pe[:, : x.size(1), :]这里用register_buffer注册位置编码表示它是一个不参与梯度更新的 Tensor但会随模型一起在 GPU/CPU 之间移动。forward里直接把位置编码加到词嵌入上。4.2 多头注意力实现class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout0.1): super().__init__() assert d_model % n_heads 0, d_model 必须能被 n_heads 整除 self.d_model d_model self.n_heads n_heads self.d_k d_model // n_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) self.dropout nn.Dropout(dropout) def forward(self, query, key, value, maskNone): batch_size query.size(0) # 线性变换后拆成多头 Q self.w_q(query).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) K self.w_k(key).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) V self.w_v(value).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) # 注意力分数 scores torch.matmul(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) context torch.matmul(attn, V) # 头部拼接 context context.transpose(1, 2).contiguous().view( batch_size, -1, self.d_model ) return self.w_o(context)核心维度变化我再说一遍输入形状是(B, L, d_model)经过view transpose变成(B, n_heads, L, d_k)。scores的形状是(B, n_heads, L_q, L_k)。最后再还原回(B, L, d_model)。很多新手在这里容易搞混view和transpose的区别。简单记忆view是“重新划分形状”transpose是“交换两个维度”两者连用后再contiguous()是标准写法。4.3 编码器层与解码器层前馈网络实现class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout0.1): super().__init__() self.linear1 nn.Linear(d_model, d_ff) self.linear2 nn.Linear(d_ff, d_model) self.dropout nn.Dropout(dropout) def forward(self, x): return self.linear2(self.dropout(F.relu(self.linear1(x))))编码器层class EncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, n_heads, dropout) self.feed_forward FeedForward(d_model, d_ff, dropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # 自注意力 残差 LayerNorm x x self.dropout(self.self_attn(x, x, x, mask)) x self.norm1(x) # 前馈网络 残差 LayerNorm x x self.dropout(self.feed_forward(x)) x self.norm2(x) return x解码器层class DecoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, n_heads, dropout) self.cross_attn MultiHeadAttention(d_model, n_heads, dropout) self.feed_forward FeedForward(d_model, d_ff, dropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.norm3 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, enc_output, src_maskNone, tgt_maskNone): # 第一层带掩码的自注意力 x x self.dropout(self.self_attn(x, x, x, tgt_mask)) x self.norm1(x) # 第二层交叉注意力K、V 来自编码器输出 x x self.dropout(self.cross_attn(x, enc_output, enc_output, src_mask)) x self.norm2(x) # 第三层前馈网络 x x self.dropout(self.feed_forward(x)) x self.norm3(x) return x残差连接的作用是让梯度可以在深层网络中顺利回传。LayerNorm 让每一层的数据分布保持稳定加速训练。4.4 组装完整 Transformer还需要两个 mask 生成函数。一个是 padding mask用于忽略补齐位置的无效 token另一个是 subsequent mask用于遮住未来信息。def make_pad_mask(seq, pad_idx0): # seq: (B, L)返回 (B, 1, 1, L) return (seq ! pad_idx).unsqueeze(1).unsqueeze(2).float() def make_subsequent_mask(size): # 返回下三角矩阵上三角为 0 return torch.tril(torch.ones(size, size, dtypetorch.float32))完整模型class Transformer(nn.Module): def __init__( self, src_vocab, tgt_vocab, d_model64, n_heads4, d_ff128, n_layers2, dropout0.1, max_len50, pad_idx0, ): super().__init__() self.src_embedding nn.Embedding(src_vocab, d_model) self.tgt_embedding nn.Embedding(tgt_vocab, d_model) self.positional_encoding PositionalEncoding(d_model, max_len) self.encoder_layers nn.ModuleList( [EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)] ) self.decoder_layers nn.ModuleList( [DecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)] ) self.fc_out nn.Linear(d_model, tgt_vocab) self.dropout nn.Dropout(dropout) self.d_model d_model self.pad_idx pad_idx def forward(self, src, tgt): src_mask make_pad_mask(src, self.pad_idx) tgt_mask make_pad_mask(tgt, self.pad_idx) * make_subsequent_mask(tgt.size(1)) # 词嵌入 位置编码 src_emb self.dropout( self.positional_encoding(self.src_embedding(src) * math.sqrt(self.d_model)) ) tgt_emb self.dropout( self.positional_encoding(self.tgt_embedding(tgt) * math.sqrt(self.d_model)) ) # 编码器 enc_output src_emb for layer in self.encoder_layers: enc_output layer(enc_output, src_mask) # 解码器 dec_output tgt_emb for layer in self.decoder_layers: dec_output layer(dec_output, enc_output, src_mask, tgt_mask) logits self.fc_out(dec_output) return logits论文里对嵌入层乘以了sqrt(d_model)目的是把嵌入向量的方差放大到和位置编码同一量级。这是一个细节但了解它有助于理解为什么原版代码中会看到类似embed * math.sqrt(d_model)的写法。4.5 训练一个最小反转序列任务为了验证模型真的能学习我们来做一个非常小的任务输入一串数字目标输出它的逆序序列。比如输入[1, 2, 3, 4, 5]目标输出[5, 4, 3, 2, 1]。这个任务虽然简单但足以检验 Transformer 的编码器、解码器、注意力、位置编码是否都正常工作。# 文件路径train.py import torch import torch.nn as nn from model import Transformer PAD_IDX 0 BOS_IDX 10 VOCAB_SIZE 16 SEQ_LEN 5 def make_batch(batch_size, seq_len): # 输入数字范围为 1~9BOS10 src torch.randint(1, 10, (batch_size, seq_len)) tgt torch.flip(src, dims[1]) # 解码器输入在目标序列前面加 BOS并去掉最后一个 token tgt_input torch.cat( [torch.full((batch_size, 1), BOS_IDX), tgt[:, :-1]], dim1 ) return src, tgt_input, tgt def main(): torch.manual_seed(42) model Transformer( src_vocabVOCAB_SIZE, tgt_vocabVOCAB_SIZE, d_model64, n_heads4, d_ff128, n_layers2, dropout0.1, max_len50, pad_idxPAD_IDX, ) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss(ignore_indexPAD_IDX) for epoch in range(30): model.train() src, tgt_input, tgt make_batch(64, SEQ_LEN) logits model(src, tgt_input) # (B, SEQ_LEN, VOCAB_SIZE) loss criterion(logits.view(-1, VOCAB_SIZE), tgt.view(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() if (epoch 1) % 5 0: print(fepoch {epoch 1:2d}, loss {loss.item():.4f}) if __name__ __main__: main()这里的关键点在于tgt_input和tgt的错位关系解码器输入是“BOS 目标序列去掉最后一个”预测目标则是完整目标序列。这样模型学习的是“根据已生成的词预测下一个词”。梯度裁剪clip_grad_norm_是很常见的训练技巧它把梯度模长限制在 1.0防止梯度爆炸。4.6 推理与结果说明训练完成后需要自回归式生成。每次只输入已经生成的 token取最后一个位置的预测结果作为下一个 token然后拼接继续。def infer(model, src): model.eval() with torch.no_grad(): tgt torch.full((1, 1), BOS_IDX, dtypetorch.long) for _ in range(SEQ_LEN): logits model(src, tgt) next_token logits[:, -1, :].argmax(dim-1, keepdimTrue) tgt torch.cat([tgt, next_token], dim1) return tgt[:, 1:].squeeze(0).tolist()运行后模型输出的 loss 会随着训练逐步下降。由于随机种子、硬件环境不同具体数值会有差异但整体趋势应该是一致的。如果 loss 能稳定下降说明自注意力、位置编码、解码器都正常工作了。这个任务只有几十秒到几分钟的训练量重点不是“效果”而是让你确认Transformer 的每个组件都真实参与了计算并且真的能学到序列变换规律。5. 高效微调把预训练 Transformer 用到业务里5.1 为什么不用“从零训练”路线手写 Transformer 能帮你理解原理但实际业务中几乎不会有人从零训练一个大模型。原因很简单数据量和算力都不够。预训练模型已经在海量文本上学习到了通用语义知识我们只需要在它的基础上针对特定任务做“微调”。比如情感分类、命名实体识别、文本相似度等都只需要少量标注数据就能取得不错的效果。但全量微调也有痛点。以 BERT-base 为例参数量过亿不仅要更新全部参数显存开销巨大而且每个下游任务都要保存一个完整模型副本。对于公司里多个小任务并存的场景既不经济也不灵活。5.2 LoRA 微调的核心思路LoRALow-Rank Adaptation是目前最流行的参数高效微调方法之一。它的核心思路是冻结预训练模型的原始权重在模型层旁边插入了两个低秩矩阵 A 和 B。改造后的权重变化可以写成W_new W_old B * A其中 A 的形状是(d, r)B 的形状是(r, d)秩r远小于d。训练时只更新 A 和 B原始权重保持不变。这样做有几个明显好处训练参数量大幅减少显存占用低。每个任务只需要保存一份很小的 LoRA 参数文件原始模型可以复用。切换任务时只需切换对应的 LoRA 权重非常灵活。5.3 基于 Hugging Face PEFT 的最小示例下面用 Hugging Face 的transformers和peft库演示一个文本二分类的 LoRA 微调流程。# 文件路径finetune.py import torch from torch.optim import AdamW from transformers import AutoTokenizer, AutoModelForSequenceClassification from peft import LoraConfig, get_peft_model, TaskType from datasets import Dataset # 1. 加载预训练模型 model_name bert-base-uncased tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels2) # 2. 配置 LoRA lora_config LoraConfig( task_typeTaskType.SEQ_CLS, r8, lora_alpha16, lora_dropout0.1, target_modules[query, value], ) peft_model get_peft_model(model, lora_config) peft_model.print_trainable_parameters()print_trainable_parameters()会打印当前可训练参数量这时你会看到只需要训练很少一部分参数其他参数全部冻结。target_modules可以指定需要施加 LoRA 的层名。不同模型的层名不同BERT 中通常是query和value如果导入模型时报错提示找不到目标模块可以用model.named_modules()查看实际层名并调整。5.4 微调训练流程与保存加载构造一个极小的示例数据方便说明流程texts [ this movie is really great, i like it, this movie is so bad, i do not recommend it, ] labels [1, 0] dataset Dataset.from_dict({text: texts, label: labels}) def tokenize(batch): return tokenizer(batch[text], truncationTrue, paddingTrue) dataset dataset.map(tokenize, batchedTrue) dataset.set_format(typetorch, columns[input_ids, attention_mask, label]) loader torch.utils.data.DataLoader(dataset, batch_size2) optimizer AdamW(peft_model.parameters(), lr5e-5) loss_fn torch.nn.CrossEntropyLoss() for epoch in range(3): for batch in loader: outputs peft_model( input_idsbatch[input_ids], attention_maskbatch[attention_mask], labelsbatch[label], ) loss outputs.loss optimizer.zero_grad() loss.backward() optimizer.step() print(floss: {loss.item():.4f})在真实项目中texts和labels应该替换成你自己的业务数据样本量通常需要几百到几千条。训练轮数也不要太长否则容易过拟合。保存 LoRA 权重peft_model.save_pretrained(./lora_cls_model) tokenizer.save_pretrained(./lora_cls_model)加载 LoRA 权重时必须先用同一个预训练模型作为基座再加载 LoRAfrom peft import PeftModel base_model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels2) loaded_model PeftModel.from_pretrained(base_model, ./lora_cls_model) loaded_model.eval()这里需要特别注意的是不能只保存 LoRA 文件就丢掉 base model。真正部署时base model 是所有任务共享的LoRA 文件只负责记录增量改动。6. 常见问题与排查思路6.1 常见报错速查问题现象常见原因解决思路显存不足 OOM序列过长、batch 过大或模型过大减小 batch、缩短序列长度、使用梯度累积、启用混合精度训练 loss 为 NaN学习率过大、数据包含异常值降低学习率检查数据是否有 NaN检查 mask 是否正确损失不下降学习率太低/太高、标签错位、模型结构错误从 1e-3 左右调起检查解码器输入输出是否错位维度不匹配d_model不能被n_heads整除确保d_model % n_heads 0LoRA 加载时找不到模块target_modules写错用model.named_modules()查看实际层名推理结果全一样模型过拟合到固定输出或没有 mask降低训练轮数检查 self-attention mask6.2 训练不收敛怎么排查如果手写 Transformer 的 loss 不下降按下面顺序排查第一步看数据。输入和目标是否构成正确的映射关系tgt_input和tgt是否错位一位加一个 BOS token 后输出长度是否和 target 对齐第二步看模型。d_model、n_heads、d_ff是否合理太小会导致表达力不足太大在 CPU 上训练会慢。第三步看输出。直接打印logits的形状确认是(B, L, vocab_size)。如果形状不对说明某个模块的维度变换写错了。第四步看损失。如果 loss 在初始阶段小幅波动但不下降通常不是模型坏了而是学习率不合适。小型任务从1e-3开始预训练微调从2e-5到5e-5开始。7. 最佳实践与工程建议7.1 模型选型与规模手写模型可以帮助学习但生产环境要尽可能复用成熟的预训练模型库。不同任务有不同选型思路文本分类、实体识别、语义匹配优先选 Encoder-only 模型如 BERT、RoBERTa。文本生成、对话、代码生成优先选 Decoder-only 模型如 GPT、LLaMA。机器翻译、文本摘要优先选 Encoder-Decoder 模型如 T5、BART。不要一上来就追求大模型。先在新任务上用 Base 规模模型跑通链路再根据效果逐步扩大到 Large 或更大规模。7.2 数据处理与训练策略数据质量决定效果上限。采集数据后至少要做这几件事清洗去掉重复样本、无效字符。标签校验找专人或基础规则核对 label尤其是分类任务。分布检查训练集和验证集的数据分布要一致。长度统计查看序列长度分布合理设置max_length。训练时建议统一设置随机种子保证实验可复现import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)7.3 微调与部署建议微调阶段优先考虑 LoRA 这类高效微调方法而不是直接全量微调。这样做既节省显存也便于多任务并行维护。部署阶段有几个常见优化方向使用 ONNX 或 TensorRT 转换模型加速推理。对模型做量化比如从 FP16 到 INT8减少显存占用。如果服务 QPS 高可以加缓存或者把模型放入多卡服务集群。每次上线前先用验证集和后端回归用例做冒烟测试确认效果没有回退。这里特别提醒任何对生产模型、数据或配置的变更都必须在测试环境验证通过后再按照可回滚的方式发布。8. 总结与下一步学习路线这篇文章从 Transformer 要解决的问题讲起分析了自注意力、多头注意力、位置编码、编码器解码器结构然后用 PyTorch 手写了一个完整可运行的 Transformer并最终演示了 LoRA 高效微调的实际用法。读完并且跑完代码后你应该已经理解了自注意力是如何计算并聚合信息的。多头注意力为什么比单头更强。位置编码为什么必不可少。Transformer 训练和推理在输入输出上的差异。高效微调和全量微调的区别。下一步建议按这样顺序继续学习。第一精读《Attention Is All You Need》原论文注意其中公式和超参数细节。第二看 The Illustrated Transformer 这类图解资料把注意力可视化画面和公式对照起来。第三把本文的模型改用英文翻译或文本分类任务加深对 Encoder-Decoder 的理解。第四系统学习 Hugging Face Transformers 库掌握Trainer和Datasets的使用。第五如果要做大模型应用继续深入学习 LoRA、QLoRA、Prefix Tuning 等高效微调技术以及推理优化、量化部署。先把今天的手写版跑通再进入下一步。中间遇到任何报错欢迎把问题和完整堆栈发在评论区一起讨论。
返回列表