Training and Pre-Training
***Prerequisites: "Transformers - 1,2,3"; "PyTorch" chapters***
1.引言
大家好,我们又回来了。按照上期的既定计划,我们将讲解训练和预训练的过程。在transformers部分,我们预留了一个伏笔:权重(参数)W。
相信大家对参数/权重这个词语应该不陌生了。但是,它到底是怎样产生的呢?women这期来解答这个问题。
我们首先来区分一下几种主流的训练方式:
预训练(Pretraining): 模型权重从随机分布(通常按模型维度缩放的高斯分布)初始化。通过在海量文本上消耗巨量算力,权重学会了语法、事实、推理模式和语言结构。
微调(Fine-Tuning): 预训练得到的“基座”权重在高质量、精选的数据集(如指令遵循对或特定领域数据)上进一步调整,以改变模型的行为风格、语气或特定能力。
对齐(Alignment): 利用人类反馈强化学习(RLHF)或直接偏好优化(DPO)等算法进一步优化权重,使模型输出在安全性、诚实性和实用性方面符合人类偏好。
推理(Inference): 权重被完全冻结。生成过程通过前向传播逐步进行,利用现有上下文预测下一个 Token,而不更新任何参数。
相信这段话其实很难读懂,没关系,经过之后的详细讲解,这些方式就会变得更加明朗。
2.搭建训练流水线
1.寻找合适的原始数据集:在网络上搜寻适合当数据的文章,例如论文,新闻文章,代码库;启发式过滤器会将其中垃圾,错误的,无意义的信息去除掉。
2.去重(Deduplication)这个很好理解,也就是去掉数据中重复的信息而已。
3.分词(Tokennization)根据预选好的词表(Vocabulary)将数据标好序号,利用字节对编码(BPE)或者Wordpiece算法。
4.文档边界(Boundaries)我们在文档的结尾适当的插入休止符号,如 “<|endoftext|> 或者<s>。使得模型学会上下文在哪里终止。
5.上下文窗口和文档打包(Packing):单个文档的长度各不相同,于是我们为了最大化GPU的张量计算利用率,多个由边界 Token 分隔的文档会被拼接为一个固定序列长度(例如 $N = 2048$ 或 $N = 4096$)。
6.在做这些之前,务必将数据切分为训练集和验证集。这点我们在scikit learn那一集也教学过了。
3.自监督学习与因果掩码
我们终于可以开始训练了,我们首先要做的是引入一个机制,名字叫教师强制机制(Teacher Forcing)也就是,我们利用一些特殊的手段,使decoders在预测下一个token时,看到的一定是正确的提示token。
给定一个 Token ID 序列 \([t_1,t_2,t_3,t_4]\),我们向模型提供左移的输入和右移的目
标:
\(输入序列(x): [The, cat, sit]\)
\(目标序列(y): [cat, sit, down]\)
在训练期间,模型使用教师强制(Teacher Forcing)机制:在位置 \(i\),无论模型在前
一步 的预测是否正确,它都会接收真实的 Token \(x_i\) 作为输入,去预测目标
\(y_i = x_i+1\)。
接下来,我们通过因果掩码(这个概念我们教过,如果忘了可以去回看decoders那期),保证decoder在输出token t_i 时,严禁看到未来的内容,尽
管我们的上下文N需要被并行计算,我们的decoders依然只可以看到 t_i<1 的上下文
tokens。
4.交叉熵(Cross Entropy)与困惑度(Perplexity)
每一个序列位置,decoders都会生成对应的logits,之后使用softmax生成对应的概率。
我们利用下面这个公式来算损失:
\(mathcal{L}_i = -log P_i(y_i)\)
于是乎我们就得到了交叉熵损失的值了。
为了计算总批次损失,需要对所有有效目标位置的单 Token 损失 \(L\) 求平
均。用于对齐序列长度的填充 Token(Padding)会通过二值目标掩码 \(m\)
予以排除:
\(\mathcal{L}_{\text{mean}} =
\frac{\sum_{i=1}^{N} m_i \cdot \mathcal{L}_i}
{\sum_{i=1}^{N} m_i}\)
困惑度(Perplexity)
困惑度用来衡量预测下一个token的不确定性,一般我们用数学来说是交叉熵的指数形式:
\(PPL = exp(L_{mean})\)
如果某位置的困惑度为K,就代表模型会在总数为K的词汇中,随机选取一个token。
注意,这里我们的困惑由于各模型参数不一,不能直接用于比较衡量各模型能力。
5.权重更新&PyTorch实践
我们得知了loss function的算法后,在我们之前的PyTorch教程中,我们的把张量做正向&反向传播,得以做梯度下降。于是乎,我们的可以利用这个机制,应用在我们LLM的交叉熵公式上。
具体过程:
输入 (X) ──► [ 前向传播 ] ──► 损失 (L) ──► [ 反向传播 ] ──► [ 优化器更新 ] ──► 清空梯度优化机制:
1. AdamW优化器:使用一阶矩(\(m_t\),均值)和二阶矩(\(v_t\),未中心化的方差)的逐元素自适应估计,并结合解耦的权重衰减($\lambda w$)。
2.解耦权重衰减(Decoupled Weight Decay): 与标准的 \(L_2\) 正则化不同,AdamW 直接将权重衰减从参数中扣除,而不经过自适应梯度缩放,避免了梯度缩放对衰减率的扭曲:
\(w_{t+1} = w_t - \eta_t \left(
\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}
+ \lambda w_t
\right)\)
梯度裁剪:当梯度的全局\(L_2\)超过某一个阈值\(c\)时,我们把它按比例缩放,防止过大的梯度破坏权重。
\(\mathbf{g} \leftarrow \mathbf{g} \cdot
\min\left(1, \frac{c}{\lVert \mathbf{g} \rVert_2}\right)\)
Pytorch工作流:
import torch
# 1. 清空上一轮迭代累积的梯度
optimizer.zero_grad(set_to_none=True)
# 2. 前向传播:给定输入 X 计算预测的 logits
logits = model(input_ids) # 形状: (Batch, Sequence_Length, Vocab_Size)
# 3. 对齐目标 Y 并计算损失
# 展平形状为 (Batch * Sequence_Length, Vocab_Size) 与 (Batch * Sequence_Length)
loss = criterion(logits.view(-1, vocab_size), targets.view(-1))
# 4. 反向传播:通过自动求导计算梯度
loss.backward()
# 5. 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 6. 优化器更新:根据 AdamW 规则更新模型参数
optimizer.step()6.规模化训练&算力均衡
我们在训练大模型时,时常需要平衡超参数的值,找到一个相对协调的值。
一般有:
批次大小(Batch Size, \(B\)): 单个优化步骤中处理的总序列数(通常按Token 数 \(B*T\) 衡量,其中 \(T\) 为序列长度)。
步数(Steps, \(S\)): 完成权重更新参数循环的次数。
总 Token 数(\(N_{token}\)): 计算公式为 \(N_{token} = B * T * S\)。
轮次(Epochs): 遍历完整数据集的次数(为避免记忆化,预训练通常仅训练 \(1\) 到 \(2\) 个 Epoch)。
学习率均衡:
当我们在开始训练时,直接使用较大的学习率(Learning rate)会时结果很不稳定,于是乎我们使用一个动态调整学习率的策略:
学习率 ^
│ / \
│ / \ 余弦衰减 (Cosine Decay)
│ / \
│ / ` - . _
│ / ` - . _
└──┴───────────────────────────► 步数 (Steps)
预热 (Warmup)开始部分:逐渐上升至\(eta_max\)
衰减阶段:逐渐下降至最大值的10%左右
根据经验缩放律,最优算力分配需要在模型参数量 \(N\) 和预训练 Token 数 \(D\) 之间取得平衡:
7.泛化能力与训练动态
我们需要在模型室进行训练时动态监测其训练状态。
损失 ^
│ \
│ \ 验证损失 (开始上升 = 过拟合)
│ \ / ─────────---
│ \ /
│ \ /
│ * <-- 最佳检查点
│ \ ───────────────── 训练损失
└─────────────────────────────► 训练步数过拟合(Overfitting): 当训练损失持续下降而验证损失趋于平缓甚至上升时发生,这表明模型正在死记硬背训练序列,而非学习可泛化的语言规律。
检查点保存(Checkpointing): 定期将模型参数张量、优化器状态字典以及当前步数计数器保存到磁盘中。
可恢复性(Resumability): 能在硬件崩溃时恢复训练,并允许开发者根据最佳验证损失而非单纯根据总步数来挑选最佳模型版本。
8.预训练到指令微调
预训练只能把模型训练成一个接龙机器,我们要使用指令微调让它有了回答问题的能力。
预训练基座模型(“文本接龙器”)
│ 提示词: "写一篇关于光合作用的论文。"
└─► 输出: "《光合作用论文》是高中生物课常见的作业……" (纯文本接续)
│ [ 指令微调 / SFT ]
▼
指令对齐模型(“智能助手”)
│ 提示词: "写一篇关于光合作用的论文。"
└─► 输出: "光合作用是植物利用太阳能……" (回答问题)仅仅靠预先训练一个模型是不够的,这样只能让模型成为一个文字接话器,于是乎,要让模型真正有回答问题的能力,我们还需要:
-监督微调(SFT):教会模型以助手的身份解决问题
-偏好对齐(DPO/RLHF):利用偏好数据,使模型达到符合人给其设定的标准。
于是乎,熟悉本频道的观众就知道了,我们下一个Lecture就要讲到微调/对齐了。
感谢看到这的你!也感谢一直以来对本频道的阅读!我们下期见!不会很久的。