ARTICLE DETAIL

资讯详情

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

从零构建大语言模型:PyTorch手写Transformer与训练推理实战

从零构建大语言模型:PyTorch手写Transformer与训练推理实战 之前分享过深度学习系列的前面章节今天这篇继续围绕 Python 深度学习实战完整拆解从零构建大语言模型LLM的过程。本文定位是教程型长文对应系列里的第 15 章“LLM 核心模块实现”与第 16 章“训练、精度优化与推理实战”。无论你是刚入门深度学习的新手还是已经会用 Transformer 但没亲手实现过的开发者都可以通过这篇文章把“从论文到代码”的最后一公里打通。我们会从一个只有几千行字符的微型语料开始用 PyTorch 手写 Tokenizer、多头注意力、前馈网络、层归一化组合出完整的自回归语言模型并完成训练、推理、精度对比与常见问题排查。读完本文你能掌握 LLM 的最小可运行骨架也能理解 FP32、FP16、BF16、TF32 在训练和部署中的真正区别。1. 为什么需要从零构建 LLM1.1 LLM 的本质是什么大语言模型本质上是一个基于 Transformer 架构的自回归模型。它接收一段 token 序列预测下一个 token 的概率分布。训练时不断调整模型参数让预测结果越来越接近真实文本推理时把预测出的 token 拼接到输入后面再继续预测下一个 token形成流畅的文本生成过程。很多人第一次接触 LLM 是直接调用 OpenAI API 或 HuggingFace Transformers使用门槛很低但内部细节像一个“黑盒”。从零构建 LLM并不是要你重新发明轮子而是为了拆掉黑盒理解 Embedding、多头注意力、因果掩码、残差连接、层归一化、Softmax 交叉熵这些核心组件之间是如何协作的。1.2 从零构建和直接使用 Transformers 库的区别直接使用 Transformers 库时一行AutoModelForCausalLM.from_pretrained(gpt2)就能加载一个训练好的模型。这种方式的优点是快、稳定、生态成熟缺点是很多细节被封装在库内部。当你需要自定义模型结构、做低层性能优化、或者研究新的注意力机制时只停留在 API 层是不够的。从零构建则能让你拥有完整控制权。你能逐行看到 attention 分数怎么计算、causal mask 如何防止未来信息泄漏、残差连接为什么能让几十层 Transformer 稳定训练。本文采取“从零手写 PyTorch 张量操作”的方式不依赖高层模型库帮你建立扎实的底层认知。1.3 第 15-16 章的学习地图第 15 章聚焦模型本身包括字符级 Tokenizer、可学习位置编码、多头注意力、前馈网络、RMSNorm以及最终把模块组装成可推理的 Transformer 解码器。第 16 章聚焦训练与优化包括数据批次构造、交叉熵损失、AdamW 优化器、梯度裁剪、混合精度训练FP16/BF16并对比 FP32、FP16、BF16、TF32 四种数值格式的适用场景。接下来我们就按这条主线从环境准备开始实操。2. 环境准备与项目结构2.1 运行环境说明本文示例代码基于 Python 3.10 编写深度学习框架使用 PyTorch 2.x。PyTorch 2 对torch.autocast、torch.compile等 API 做了大量优化下面的代码在 PyTorch 2.0 以上版本均可直接运行。硬件方面CPU 环境也可以完成训练和推理只是速度较慢。建议使用 NVIDIA GPU并安装对应版本的 CUDA 版 PyTorch。如果你使用的是 Apple SiliconM1/M2/M3芯片Mac 版 PyTorch 也能运行本示例但不建议开启 BF16 混合精度因为部分算子不支持。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.2 安装依赖创建一个虚拟环境并安装依赖python -m venv llm_env source llm_env/bin/activate # Windows 下使用 llm_env\Scripts\activate pip install torch numpy tqdm如果你的机器有 NVIDIA GPU建议到 PyTorch 官网选择对应 CUDA 版本安装例如pip install torch --index-url https://download.pytorch.org/whl/cu121本示例不需要安装transformers、datasets等大型依赖全部代码用标准库和 PyTorch 即可跑通。2.3 项目文件结构为了便于阅读我们把代码拆成几个模块llm_from_scratch/ ├── tokenizer.py # 字符级Tokenizer ├── model.py # MiniLLM模型定义 ├── train.py # 训练与混合精度逻辑 ├── generate.py # 推理生成与采样策略 └── data.txt # 训练语料纯文本如果你习惯在 Jupyter Notebook 中学习也可以直接把代码按顺序写在 cell 里。下面我会按文件逐一说明实现思路。3. 第15章从零实现LLM核心模块3.1 字符级 TokenizerTokenizer 负责把原始文本转成数字 id。当前主流大模型大多使用 BPEByte Pair Encoding或 SentencePiece但为了把核心原理讲清楚本文先实现一个最简洁的字符级 Tokenizer。字符级意味着每个字符包括字母、标点、空格、换行对应一个独立 token。优点是实现简单、词表小缺点是长文本会被拆成很长的序列。对于微型演示模型来说这个缺点完全可接受。# 文件路径llm_from_scratch/tokenizer.py class CharTokenizer: def __init__(self, text): chars sorted(list(set(text))) self.vocab_size len(chars) self.stoi {ch: i for i, ch in enumerate(chars)} self.itos {i: ch for i, ch in enumerate(chars)} def encode(self, text): return [self.stoi[c] for c in text] def decode(self, ids): return .join([self.itos[i] for i in ids])使用方式很简单tokenizer CharTokenizer(hello world!) print(tokenizer.vocab_size) print(tokenizer.encode(hello)) print(tokenizer.decode([1, 2, 3]))在这个设计里stoi建立字符到索引的映射itos建立索引到字符的映射。encode把字符串变成 id 列表decode把 id 列表还原成字符串。实际项目中 Tokenizer 的选择会对训练效果产生明显影响。字符级 Tokenizer 容易导致序列过长BPE 或 WordPiece 则能有效压缩序列长度。当你完成本文的演示模型后建议继续学习 BPE 的实现。3.2 Embedding 与位置编码模型不能直接处理 token id需要先通过 Embedding 层映射为稠密向量。同时注意力机制本身不包含顺序信息因此必须额外注入位置编码。这里我选择“可学习位置编码”也就是为每个可能的位置维护一个可训练向量而不是使用固定的正弦位置编码。对于固定长度block_size的小型模型可学习位置编码实现更直观效果也足够好。# 文件路径llm_from_scratch/model.py部分代码 import math import torch import torch.nn as nn import torch.nn.functional as F class MiniLLM(nn.Module): def __init__(self, vocab_size, d_model128, n_head4, n_layer4, d_ff512, block_size128, dropout0.1): super().__init__() self.token_embedding nn.Embedding(vocab_size, d_model) self.pos_embedding nn.Embedding(block_size, d_model) self.blocks nn.ModuleList([ Block(d_model, n_head, d_ff, dropout) for _ in range(n_layer) ]) self.ln_f RMSNorm(d_model) self.lm_head nn.Linear(d_model, vocab_size, biasFalse) self.block_size block_size # 因果掩码保证当前位置只能看到当前位置及之前的位置 mask torch.tril(torch.ones(block_size, block_size)).view(1, 1, block_size, block_size) self.register_buffer(mask, mask) def forward(self, idx, targetsNone): B, T idx.shape assert T self.block_size, 输入序列长度超过模型最大长度 tok_emb self.token_embedding(idx) pos torch.arange(0, T, deviceidx.device).unsqueeze(0) pos_emb self.pos_embedding(pos) x tok_emb pos_emb for block in self.blocks: x block(x, self.mask[:, :, :T, :T]) logits self.lm_head(self.ln_f(x)) loss None if targets is not None: B, T, C logits.shape loss F.cross_entropy(logits.view(B * T, C), targets.view(B * T)) return logits, loss这里需要注意两个关键细节Embedding 层只是查表操作输出形状为(B, T, d_model)。位置 id 从 0 到T-1pos_embedding会自动补齐整个序列的位置向量。3.3 多头注意力机制注意力机制是 Transformer 的核心。它的作用是让序列中的每个 token 都能从其他 token 中聚合信息。多头注意力Multi-Head Attention把模型拆成多个子空间每个头关注不同的依赖关系。实现时我们一次性计算 Q、K、V然后拆分到多个头。为保证自回归性质我们用上三角掩码把未来位置置为-inf。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.head_dim d_model // n_head self.qkv nn.Linear(d_model, 3 * d_model) self.proj nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): B, T, C x.shape qkv self.qkv(x).reshape(B, T, 3, self.n_head, self.head_dim) q, k, v qkv.unbind(2) q q.transpose(1, 2) # (B, n_head, T, head_dim) k k.transpose(1, 2) v v.transpose(1, 2) att (q k.transpose(-2, -1)) * (self.head_dim ** -0.5) if mask is not None: att att.masked_fill(mask 0, float(-inf)) att torch.softmax(att, dim-1) att self.dropout(att) y att v y y.transpose(1, 2).contiguous().view(B, T, C) return self.proj(y)几个容易踩坑的地方reshape和transpose的区别先 reshape 成五维张量再通过 unbind 拆分出 QKV最后用 transpose 把头维度挪到 batch 之后方便矩阵乘法。缩放因子head_dim ** -0.5防止点积结果过大导致 softmax 梯度消失。masked_fill(mask 0, float(-inf))掩码矩阵中 0 表示未来位置把它设成负无穷softmax 后概率接近 0。3.4 前馈网络与 RMSNorm每个 Transformer Block 在注意力后都会接一个前馈网络Feed-Forward Network用于做逐位置的非线性变换。GPT 系列大多使用 GELU 激活函数。RMSNorm 是 LayerNorm 的一种简化版本。它省去了均值中心化只对均方根做归一化在训练大模型时更稳定、计算开销更低。class RMSNorm(nn.Module): def __init__(self, d_model, eps1e-8): super().__init__() self.eps eps self.weight nn.Parameter(torch.ones(d_model)) def forward(self, x): rms torch.sqrt(x.pow(2).mean(-1, keepdimTrue) self.eps) return x / rms * self.weight class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout0.1): super().__init__() self.seq nn.Sequential( nn.Linear(d_model, d_ff), nn.GELU(), nn.Dropout(dropout), nn.Linear(d_ff, d_model), nn.Dropout(dropout) ) def forward(self, x): return self.seq(x)残差连接是稳定深层网络的关键否则 4 层以上的 Transformer 很难训练。完整的 Block 如下class Block(nn.Module): def __init__(self, d_model, n_head, d_ff, dropout0.1): super().__init__() self.ln1 RMSNorm(d_model) self.attn MultiHeadAttention(d_model, n_head, dropout) self.ln2 RMSNorm(d_model) self.ffn FeedForward(d_model, d_ff, dropout) def forward(self, x, maskNone): x x self.attn(self.ln1(x), mask) x x self.ffn(self.ln2(x)) return x“先归一化再注意力”的 Pre-LN 结构和最早 Transformer 论文中的 Post-LN 不太一样。Pre-LN 在实际训练中更稳定也是 GPT 系列常用的结构所以这里直接采用这种方式。3.5 组装 Transformer 解码器把所有模块拼在一起后MiniLLM 的前向流程是token id 经过 Embedding 得到 token 向量。位置 id 经过位置 Embedding 得到位置向量。两者相加经过多层 Transformer Block。最后经过 RMSNorm 和线性输出层得到每个位置的词表 logits。如果提供 targets则计算交叉熵损失。相比原始 Transformer这里的模型只有 Decoder没有 Encoder。因为在自回归语言模型中输入和输出是同一个文本序列不需要 Encoder-Decoder 的跨序列注意力。4. 第16章训练一个微型LLM4.1 准备训练数据为了快速演示训练流程我们准备一小段文本。你可以把data.txt换成任何纯文本例如小说、代码、论文摘要效果会随之变化。# 文件路径llm_from_scratch/data.txt A tiny language model is a deep learning model that learns to predict the next token. It can generate new text by sampling from the predicted probability distribution. In this tutorial, we build a mini llm from scratch with Python and PyTorch.读取文本并创建 Tokenizertext open(data.txt, encodingutf-8).read() tokenizer CharTokenizer(text) data torch.tensor(tokenizer.encode(text), dtypetorch.long)训练时我们需要从长文本中随机截取连续block_size长度的片段。输入x是前block_size个 token目标y是后移一位的block_size个 token。这样模型在每个位置都能学习“看到前文预测下一个 token”。def get_batch(data, block_size, batch_size, device): ix torch.randint(len(data) - block_size, (batch_size,)) x torch.stack([data[i:i block_size] for i in ix]).to(device) y torch.stack([data[i 1:i block_size 1] for i in ix]).to(device) return x, y这里需要注意的是torch.randint的上限要减去block_size避免索引越界。4.2 损失函数与优化器语言模型使用交叉熵损失。因为模型输出形状是(B, T, vocab_size)目标形状是(B, T)需要先展平再计算交叉熵。优化器选用 AdamW。AdamW 在 Adam 的基础上把权重衰减和梯度更新解耦是大模型训练的事实标准。训练时还需要做梯度裁剪防止梯度范数过大导致训练不稳定。optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay0.01)4.3 训练循环与混合精度训练循环的流程如下从数据中随机采样一个 batch。前向传播得到 logits 和 loss。反向传播计算梯度。梯度裁剪。优化器更新参数。为了提升训练速度我们要用 PyTorch 的自动混合精度功能。torch.autocast可以在前向和反向过程中自动选择 FP16 或 BF16 计算从而降低显存占用并加快 Tensor Core 计算。这里给出一个支持 FP16/BF16 的训练脚本# 文件路径llm_from_scratch/train.py import torch import torch.nn.functional as F from model import MiniLLM from tokenizer import CharTokenizer def get_batch(data, block_size, batch_size, device): ix torch.randint(len(data) - block_size, (batch_size,)) x torch.stack([data[i:i block_size] for i in ix]).to(device) y torch.stack([data[i 1:i block_size 1] for i in ix]).to(device) return x, y def train(model, data, tokenizer, device, max_iters5000, batch_size16, block_size64, lr3e-4, use_ampTrue, amp_dtypetorch.bfloat16): optimizer torch.optim.AdamW(model.parameters(), lrlr, weight_decay0.01) scaler torch.cuda.amp.GradScaler(enabled(amp_dtype torch.float16)) model.train() for step in range(max_iters): x, y get_batch(data, block_size, batch_size, device) optimizer.zero_grad() with torch.autocast(device_typedevice.type, dtypeamp_dtype, enableduse_amp): _, loss model(x, y) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) scaler.step(optimizer) scaler.update() if step % 500 0: print(fstep {step:5d} | loss {loss.item():.4f}) torch.save(model.state_dict(), mini_llm.pt) print(训练完成模型已保存到 mini_llm.pt)这里有几个设计细节GradScaler只在 FP16 训练时需要。BF16 动态范围与 FP32 相近一般不依赖 loss scaling。使用torch.autocast时模型内部不需要手动转换 dtypePyTorch 会自动选择算子实现。如果机器只支持 CPU应该把use_amp设为False否则某些算子会警告或不支持。4.4 完整训练脚本项目根目录下创建main.py把训练流程串起来# 文件路径llm_from_scratch/main.py import torch from model import MiniLLM from tokenizer import CharTokenizer from train import train def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(using device:, device) text open(data.txt, encodingutf-8).read() tokenizer CharTokenizer(text) data torch.tensor(tokenizer.encode(text), dtypetorch.long) model MiniLLM( vocab_sizetokenizer.vocab_size, d_model128, n_head4, n_layer4, d_ff512, block_size64, dropout0.1, ).to(device) use_amp torch.cuda.is_available() train( modelmodel, datadata, tokenizertokenizer, devicedevice, max_iters3000, batch_size16, block_size64, lr3e-4, use_ampuse_amp, amp_dtypetorch.bfloat16 if device.type cuda else torch.float32, ) if __name__ __main__: main()运行命令python main.py在数据集非常小的情况下loss 会快速下降。如果使用 GPU 并开启 BF16单次迭代只需要几十毫秒。CPU 环境训练 3000 步可能需要几分钟请耐心等待。4.5 运行与保存模型训练到后期loss 通常会降到很低。由于语料只有几十个字符模型实际上会“背下”训练文本。这种情况在真实大模型训练中叫过拟合但对于理解 LLM 训练流程是正常的。保存的模型权重是mini_llm.pt。你也可以同时保存优化器状态方便断点续训torch.save({ model: model.state_dict(), optimizer: optimizer.state_dict(), step: step, }, checkpoint.pt)5. 推理生成文本5.1 贪婪解码与自回归生成训练完成后就可以用模型生成文本。最简单的方法是贪婪解码每次选择概率最高的 token拼接到输入末尾继续预测下一个 token。torch.no_grad() def generate(model, idx, max_new_tokens200, temperature1.0, top_kNone): model.eval() for _ in range(max_new_tokens): idx_cond idx[:, -model.block_size:] logits, _ model(idx_cond) logits logits[:, -1, :] / temperature if top_k is not None: v, _ torch.topk(logits, min(top_k, logits.size(-1))) logits[logits v[:, -1]] float(-inf) probs F.softmax(logits, dim-1) next_token torch.multinomial(probs, num_samples1) idx torch.cat([idx, next_token], dim1) return idx这里加入了 temperature 和 top-k 采样。Temperature 控制概率分布的平滑程度temperature 越小分布越尖锐temperature 越大输出越随机。5.2 Temperature、Top-K 与 Top-P 采样Temperature 1.0保持原始概率分布。Temperature 1.0更保守输出更确定。Temperature 1.0更发散创造性强。Top-K只保留概率最高的 K 个 token其余概率置零然后重新归一化。Top-P按概率从高到低累加直到累计概率达到 P再过滤掉剩余 token。Top-K 和 Top-P 能有效避免模型输出那些概率低但很奇怪的 token。实际项目中经常同时使用两者。你可以在generate中继续扩展 Top-P 逻辑逻辑很简单排序后计算累计概率低于阈值的 token 置为-inf。5.3 推理脚本与效果展示# 文件路径llm_from_scratch/generate.py import torch from model import MiniLLM from tokenizer import CharTokenizer import torch.nn.functional as F torch.no_grad() def generate(model, idx, max_new_tokens, temperature1.0, top_kNone): model.eval() for _ in range(max_new_tokens): idx_cond idx[:, -model.block_size:] logits, _ model(idx_cond) logits logits[:, -1, :] / temperature if top_k is not None: v, _ torch.topk(logits, min(top_k, logits.size(-1))) logits[logits v[:, -1]] float(-inf) probs F.softmax(logits, dim-1) next_token torch.multinomial(probs, num_samples1) idx torch.cat([idx, next_token], dim1) return idx def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) text open(data.txt, encodingutf-8).read() tokenizer CharTokenizer(text) model MiniLLM( vocab_sizetokenizer.vocab_size, d_model128, n_head4, n_layer4, d_ff512, block_size64, dropout0.0, ).to(device) model.load_state_dict(torch.load(mini_llm.pt, map_locationdevice)) prompt A tiny idx torch.tensor([tokenizer.encode(prompt)], dtypetorch.long, devicedevice) out generate(model, idx, max_new_tokens50, temperature0.8, top_k40) print(tokenizer.decode(out[0].tolist())) if __name__ __main__: main()运行python generate.py由于训练语料很小生成结果可能并不通顺但你已经跑通了一个微型 LLM 的完整闭环。接下来可以使用更大、更丰富的语料或者增加模型维度观察生成质量的提升。6. 精度选型FP32、FP16、BF16、TF326.1 四种浮点数格式对比在模型训练和部署中浮点精度直接影响显存占用、计算速度和数值稳定性。很多同学只听说过 FP16 比 FP32 快却被 BF16、TF32 搞糊涂这里统一对比格式符号位指数位尾数位主要特点典型场景FP321823精度高范围大速度较慢默认精度基准、小模型训练FP161510显存减半计算快但范围小易溢出老显卡混合精度训练BF16187指数范围与FP32一致尾数精度较低大模型训练首选精度TF321810不是存储格式是Tensor Core计算模式Ampere架构GPU矩阵加速FP16 的指数位只有 5 位最大值约 65504。如果梯度或中间结果过大很容易溢出变成 NaN所以需要 GradScaler 动态缩放梯度。BF16 的指数位和 FP32 一样都是 8 位动态范围极大基本不会溢出只是尾数精度少牺牲了一些精度换稳定性。TF32 不是一种存储类型而是 NVIDIA Ampere 架构中 Tensor Core 提供的计算模式。它在矩阵乘法时用 19 位精度计算速度比 FP32 快很多同时不需要像 FP16 那样担心溢出。PyTorch 中可以用下面的方式开启torch.backends.cuda.matmul.allow_tf32 True torch.backends.cudnn.allow_tf32 True6.2 训练场景如何选择如果你的 GPU 是 V100、A100、H100 等支持 BF16 的 Ampere 及以上架构优先使用 BF16。大模型训练中 BF16 几乎成为默认配置。如果你在较早的 GPU如 GTX 1080 Ti、V100上训练V100 支持 FP16 但不支持 BF16此时使用 FP16 GradScaler。如果模型较小、追求稳定直接用 FP32 也可以但训练速度会慢不少。显存不足时FP16/BF16 能直接把模型权重和激活显存减半是比减小 batch size 更优的方案。6.3 推理场景如何选择推理阶段对精度要求比训练低一些更关注吞吐量和显存占用。服务端 GPU 推理可以使用 FP16、BF16或进一步量化到 INT8。消费级显卡部署如果显存紧张量化到 INT8 是更常见的选择。当你发现 FP16/BF16 推理结果和 FP32 差异较大时优先检查是否存在溢出或采样种子固定问题。精度选型的一个核心经验是训练时更看重稳定性和动态范围推理时更看重速度和显存。不要在训练阶段盲目追求“精度越高越好”也不要在推理阶段为了速度忽视数值稳定性。7. 常见问题排查7.1 问题排查表问题现象常见原因解决思路Loss 一直是 NaN学习率过高 / FP16 梯度溢出降低学习率开启梯度裁剪FP16训练时使用GradScaler或换BF16显存不足 OOMbatch_size、block_size 过大减小 batch_size使用梯度累积改用 FP16/BF16训练不收敛loss 波动大数据预处理错误 / 掩码错误 / lr 过高检查 Tokenizer 和 batch 的对应关系可视化 attention mask生成文本全是重复temperature 过低 / 模型过小 / 语料太少调高 temperature使用 top-p 或 top-k增加训练数据CPU 训练极慢模型太大 / 未用 GPU减小 d_model、n_layer或者切换到 GPUBF16 推理结果与 FP32 有差异精度损失导致评估误差是否可接受必要时回退 FP327.2 训练不收敛的排查顺序如果 loss 下降缓慢甚至上升按以下顺序检查先确认数据流正确。打印一个 batch 的x和y并手动确认y[i][t]是否等于x[i][t1]。确认 causal mask 正确。在大规模模型里mask 错误会导致模型“偷看未来”训练 loss 并不会立刻崩但生成效果极差。检查学习率。Transformer 对学习率比较敏感建议从3e-4开始。确认损失函数输入输出形状。logits.view(B*T, C)与targets.view(B*T)必须对应。确认是否使用梯度裁剪。没有梯度裁剪时偶尔的大梯度会破坏训练稳定性。7.3 混合精度训练常见坑在 CPU 上开启torch.autocast会警告或不生效需要判断环境。FP16 训练必须搭配 GradScaler否则小梯度会被下溢为 0。BF16 在 H100/A100 上性能最好在 V100 上不支持只能用 FP16。混合精度下某些自定义算子里手动.half()和.float()混用会导致 dtype 不匹配建议统一交给 autocast 管理。8. 最佳实践与工程建议8.1 配置与实验管理训练脚本里的超参数不应该写死在代码里。推荐使用配置文件或命令行参数管理。即使只是微型项目也建议把所有关键参数输入到日志中方便复现模型结构参数n_layer、n_head、d_model、d_ff。训练参数lr、batch_size、max_iters、block_size。精度参数amp_dtype、allow_tf32。Python 的argparse、yaml都可以。深度学习项目里参数管理混乱是“跑不出论文效果”的重要原因之一。8.2 模型保存与恢复最佳实践是保存三样东西模型参数。优化器状态。当前 step 数。恢复训练时需要能回到任意中断点。如果只保存模型不保存优化器学习率动量信息会丢失训练效果可能受影响。保存格式可以统一用torch.save打包成字典。8.3 训练稳定性训练大模型时梯度裁剪是标配。统一做法是torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)同时要配合学习率预热。Transformer 训练初期梯度不稳定预热可以让学习率从很小逐渐升到目标值。torch.optim.lr_scheduler中的LambdaLR或CosineAnnealingLR都可以实现。8.4 数据与安全LLM 的数据质量决定了模型能力上限。如果数据中有大量重复、错误或低质量文本再好的模型结构也救不回来。真实项目中要重点关注数据去重与清洗。敏感信息过滤。来源授权与合规性。模型生成结果的审核与降级策略。即使你用开源权重做二次微调也必须了解数据来源和许可证。生成式模型可能输出不准确甚至有害内容工程落地时需要在产品层面加入安全和人工兜底机制。8.5 评估与观测不要只盯着训练 loss。语言模型的 loss 下降并不等于生成质量好。建议在训练过程中定期保存生成样本人工观察输出是否越来越连贯。同时记录训练 loss。验证 loss如果有验证集。生成样例。显存占用。训练时间。这些观测能帮你尽早发现模型退化、过拟合和训练异常。9. 总结与下一步学习路线本文从零构建了一个微型 LLM。我们手写了字符级 Tokenizer、位置编码、多头注意力、RMSNorm、前馈网络组合成完整的 Transformer Decoder并用一个非常小的语料完成了训练。随后完整实现了自回归推理加入了 temperature、top-k 采样并重点对比了 FP32、FP16、BF16、TF32 四种数值格式。到此为止你已经走通了大语言模型的完整最小闭环。下一步的学习方向可以围绕四个方向展开分词器升级从字符级 Tokenizer 切换到 BPE了解 GPT-2 论文中的 BPE 实现。模型结构升级加入 RoPE 旋转位置编码、Grouped Query Attention、SwiGLU 激活函数这些都是现代大模型的主流设计。训练效率升级实现张量并行、数据并行、梯度累积学习 DeepSpeed 或 FSDP。应用层扩展在自建模型基础上叠加 RAG、Agent、微调让模型真正服务业务。建议你做的第一个实验是把data.txt换成一份更长的英文小说或代码文件把d_model从 128 调到 256n_layer从 4 调到 6观察 loss 曲线和生成文本的变化。动手改一行代码比读完十篇文章都更有价值。
返回列表