大模型应用开发之预训练
预训练是研发大语言模型的第一个训练阶段,通过在大规模语料上进行预训练,大语言模型可以获得通用的语言理解与生成能力,掌握较为广泛的世界知识,具备解决众多下游任务的性能潜力
一、数据预处理
1. 数据的收集
1)通用文本数据(“主食”)
- 来源:网页(C4 、RefinedWeb、CC-Stories 等);书籍(Books3 、Bookcorpus2等);
- 特点:量大;多样;需要清洗;注意搭配
2)专用文本数据(“特色”)
- 多语言文本:加入大量非英语的文本数据,加强多语言任务的同时,还能促进不同语言的知识迁移,提升模型泛化能力(BLOOM 和 PaLM 模型使用了百种语言进行训练)
- 科学文本:加入大量科学文献、论文(比如 ArXiv 数据集)、教科书等,提升模型在专业领域的问答、推理和信息抽取能力(注意公式等符号需要采用特定的分词和预处理技术)
- 代码:加入海量来自Stack Exchange、GitHub 等的代码数据,提升编程能力
2. 数据预处理
1) 质量过滤--去除低质量
- 基于启发式规则:以通过精心设计的规则(基于语种、统计指标、关键词)来针对地识别和剔除低质量的文本数据
- 基于分类器:先人工标注一批高质量和低质量数据作为“教学样本”训练一个模型,让它学习判断文本质量的好坏(目前常用来实现分类器的方法包括轻量级模型(如 FastText 等)、可微调的预训练语言模型(如 BERT、BART 或者 LLaMA 等)以及闭源大语言模型 API(如 GPT-4)
2) 敏感内容过滤--去除敏感
- 有毒内容:采用基于分类器的过滤方法(Jigsaw 评论数据集提供了用于训练毒性分类器的数据)通过设置合理(精确度和召回率的平衡)的分类阈值,训练完成的分类器将能够有效识别并过滤掉含有有毒内容的信息
- 隐私内容:直接且有效的方法是使用启发式方法,如关键字识别来检测和删除私人信息
3)去重
- 基于计算粒度:去重可以在句子级别、文档级别和数据集级别等多种粒度上进行,现有的数据集往往采用多阶段、多粒度的方式来实现高效的去重,针对数据集和文档级别进行去重,旨在去除那些具高度相似甚至完全一致内容的文档,随后,进一步在句子级别实现更为精细的去重
- 基于匹配算法:精确匹配算法(使用后缀数组匹配最小长度的完全相同子串)&近似匹配算法(局部敏感哈希:minhash)--文档层面可以使用开销较小的近似匹配,句子层面可以使用精确匹配
4)数据词元化(Tokenization)
分词是数据预处理中的一个关键步骤,旨在将原始文本分词成模型可识别和建模的词元(token)序列,作为大语言模型的输入数据,代表性的分词方法:
- BPE: 统计文本里最常相邻出现的“零件”组合,把它们合并成一个新的、更长的“零件”,不断重复这个过程,直到达到预设的“零件库”大小;基于此扩展出字节级 BPE,无论什么语言、什么奇怪的符号,都能被表示(因为任何字符都能分解成字节)
- WordPiece:BPE 是看谁出现次数多就合并谁。WordPiece 更“功利”一点,它看合并哪个组合能让整个文本的可能性(语言模型的似然性)提升最大。在合并前,WordPiece 分词算法会首先训练一个语言模型,并用这个语言模型对所有可能的词元对进行评分=词对出现的频率/第一个词出现的频率x第二个词出现的频率。然后,在每次合并时,它都会选择使得训练数据的似然性增加最多的词元对
- Unigram:Unigram 相反,从语料库的一组足够大的字符串或词元初始集合开始,迭代地删除其中的词元,直到达到预期的词表大小。它采用期望最大化 EM算法,不断评估和移除那些“不怎么好用”或者“可以被其他零件组合替代”的零件,逐步缩减零件库,直到达到目标大小。它会计算每个零件被移除后对整体表达能力的影响,优先移除影响小的
- 分词器Tokenizer:对于混合了多领域多种格式的语料,制定专门的分词器会更加有效,而制定高效的分词器,需要具备无损重构的特性;有高压缩率;高适应性;
流行的分词库SentencePiece: Google 开源的库,同时支持 BPE 和 Unigram,很多大模型都在用它来定制分词器
3. 数据调度Data Scheduling
仅仅有好数据还不够,如何组合这些数据以及按照什么顺序喂给模型,对模型的最终能力和训练效率有着重要影响
1) 数据混合配比
确定不同来源的数据(网页、书籍、代码等)应该各自占多少比例。不同类型的数据对模型学习特定能力有不同贡献(如代码数据提升编程能力,书籍数据提升长文本理解能力)。合适的混合比例是模型全面发展或专精某项技能的关键。 常见做法有:
- 设定整体比例: 在整个预训练开始前,就定好各种数据源的总量占比。训练时按照这个比例随机采样数据。
- 分阶段调整比例: 可能在训练的不同阶段使用不同的混合比例。
- 上/下采样: 为了达到目标比例,可能需要对某些数据源进行“上采样”(重复使用)或“下采样”(只用一部分)。
怎么定比例?
经验为主 + 增加多样性: 目前很多比例是基于经验设定的。普遍认同的原则是增加数据源的多样性,让模型见多识广,有助于提升综合能力和下游任务表现。可以通过“消融实验”(依次去掉某个数据源看效果)来研究不同数据源的重要性。
可学习的优化方法 (如 DoReMi): 试图让模型自动学习最优的数据混合比例。先用一个初始比例训练一个小模型,看它在哪些数据域上学得不好(损失高),然后就增加这些“难学”数据域的采样权重,再训练一个小模型,反复迭代。最终得到一个优化的权重比例,用于训练大模型。
针对特定能力优化: 想让模型数学好,就多喂数学题和代码;想让模型懂长文本,就多喂书。这通常结合多阶段训练(数据课程)来实现。
LLaMA 的配比:网页数据占大头 (超 80%) 提供广泛的世界知识,辅以一定比例的代码 (6.5%)、书籍 (4.5%) 和科学文献 (2.5%)。
专业模型如 CodeGen 会大幅增加代码比例,但通常仍会保留一些通用网页数据,以维持基础的语义理解能力,防止模型变得过于“偏科”。
2) 数据安排Data Curriculum
不仅仅是数据比例,喂给模型的数据顺序也很重要。 核心思想 : 先易后难,先广后专。让模型先学习基础、通用的知识,再逐渐接触更复杂、更专业的内容。 广泛定义: 也可以指在训练的不同阶段使用不同的数据混合比例。
实践方法: 监控与调整,通过评测基准持续监控模型在各项关键能力上的学习进展,然后动态调整数据混合比例或引入新的数据类型。
主要应用场景:继续预训练 (Continual Pre-training):,由于从头预训练代价高昂,数据课程的研究更多集中在如何在一个已经训练好的通用大模型基础上,通过继续喂食特定数据来增强特定能力或适应新任务。
应用实例 - 如何通过数据课程提升特定能力?
提升代码能力 (CodeLLaMA): 先用海量通用数据 (2T tokens) 训练一个基础模型 → 然后用大量代码密集型数据 (500B tokens) 继续训练。 针对特定语言 (Python): 通用数据 → 代码数据 → 大量 Python 相关代码数据 (100B tokens)。
提升数学能力 (Llemma): 基于一个代码能力已经不错的模型 (CodeLLaMA) → 再用包含科学论文、数学、代码的混合数据继续训练 (50-200B tokens)。 注意“正则化”: 在这个专业训练阶段,仍然保留少量 (5%) 通用数据,防止模型在提升数学能力的同时,忘记了原有的通用语言能力。
提升长文本能力 (CodeLLaMA / LongLLaMA): 通常需要修改位置编码(如 RoPE 的参数)来适应更长的上下文窗口。 先用短上下文窗口 (如 4K) 训练 → 然后用长序列数据和更长的上下文窗口 (如 16K, 甚至 100K) 继续训练少量数据 (如 20B tokens)。 好处: 这种从短到长的训练方式,既能让模型学会处理长文本,又能节省大量训练时间(因为大部分训练还是在短序列上完成的)。
二、预训练过程
1、预训练任务
在大规模预训练时,需要设计合适的自监督预训练任务,使得模型能够从海量无标注数据中学习到广泛的语义知识与世界知识。目前,常用的预训练任务主要分为三类,包括语言建模(Language Modeling, LM)、去噪自编码(Denoising Autoencoding, DAE)以及混合去噪器(Mixture-of-Denoisers, MoD)。
1)语言建模--文本预测
任务核心:预测序列中的下一个词元 (token)
方法:利用自回归,在预测第 t 个词元 ut 时,仅使用前 t−1 个词元 u<t 作为输入,并在训练中根据似然函数最大化正确猜中每个词ut的概率的总和:
扩展:
· 前缀语言建模(常用语对话,续写):专门为采用前缀编码器架构的模型设计,基于前缀(prefix)信息预测后缀(suffix)的词元,将输入序列 u 随机切分为前缀 uprefix={u1,…,uk} 和后缀 usuffix={uk+1,…,uT},仅计算后缀部分的损失。训练时的目标函数为:
· 中间任务填充(常用语代码补全):训练模型填充序列中间缺失的部分,将输入序列分为前缀 uprefix、中间 umiddle、后缀 usuffix,并将中间部分移至末尾,形成新序列 unew=[uprefix,usuffix,umiddle],模型需自回归预测新序列,同时恢复中间缺失内容
2)去噪自编码--文本修复
输入文本经过一系列随机替换或删除操作,形成损坏的文本𝒖\ 。模型的目标是根据这些损坏的文本恢复出被替换或删除的词元片段:
去噪自编码的实际实现通常更复杂,因为需要精心设计“损坏策略”(比如遮盖多少比例的词、遮盖多长的片段等),这些策略会直接影响模型的学习效果
3)混合去噪器(UL2)--全能去噪
UL2 框架让模型接受多种不同难度和类型的“损坏-修复”考验:
- S-denoiser (序列去噪 / 前缀挑战): 与之前说的“前缀语言建模”目标相同。给模型一段开头的文本,让它把后续的内容补充完整。
- R-denoiser (常规去噪 / 跨度修复): 与去噪自编码任务的优化目标相似。二者仅在被掩盖片段的跨度和损坏比例上有所区别。R-去噪器屏蔽序列中约 15% 的词元,且每个被屏蔽的片段仅包含 3 到 5 个词元。
- X-denoiser (极限去噪 / 深度修复): X-去噪器采用更长的片段(12 个词元以上)或更高的损坏比例(约 50%),进而要求模型能够精准还原原始信息。这种设置增加了任务难度,迫使模型学习到更全面的文本表示。
2、优化参数设置
核心目标: 确保训练过程稳定、高效,并且模型能够充分学习数据中的知识,最终达到良好的泛化能力(在没见过的数据上也能表现好),同时避免过拟合(在训练数据上表现完美,但在新数据上表现糟糕),主要调校手段:
1)批次数据训练
-
批次大小 (Batch Size) - 一次“喂”多少数据给模型学习?通常设置非常大的批次大小,比如 1M 到 4M 个词元 (tokens)。像 GPT-3, PaLM, Gopher 这些巨型模型,它们的批次大小都是百万级别的。即使是相对小一些的 LLaMA-2 (7B),批次大小也达到了 4M。
- 原因:
-
提高训练稳定性: 大批次数据计算出的梯度更稳定,抖动更小。
-
提高吞吐量 (Throughput): 在并行计算硬件(如 GPU/TPU)上,大批次能更充分地利用计算资源,加快训练速度。
- 动态调整批次大小 (如 PaLM-540B): 有些模型在训练初期使用较小的批次,然后逐渐增大。这可能与训练不同阶段对梯度稳定性和计算效率的需求不同有关。
2)学习率
学习率是训练中最关键的超参数之一,直接影响模型能否收敛以及收敛的速度和质量。固定的学习率往往不是最优的。训练初期,我们希望模型学得快一些(大学习率);训练后期,接近最优解时,我们希望模型学得稳一些(小学习率),避免“步子迈大了扯着蛋”,错过最优解。
-
学习率调度策略 (Learning Rate Scheduling) - “学习步伐”的动态调整:预热 (Warmup) + 衰减 (Decay)
-
衰减阶段 (Decay): 达到峰值学习率后,随着训练的进行,学习率会按照某种策略(比如余弦衰减 Cosine Decay,这是非常常用的一种)逐渐降低,直到训练结束时接近于零。余弦衰减的特点是下降平滑,前期下降慢,后期下降快。
-
预热阶段 (Warmup): 训练刚开始时,模型参数是随机初始化的,很不稳定。此时如果学习率太大,容易导致训练发散。所以,先用一个非常小的学习率开始,然后线性地逐渐增加到一个预设的峰值学习率(通常这个峰值学习率在预训练阶段的整体学习步数中只占很小一部分,比如前几个百分点的步数)。
-
3)优化器
根据计算出的梯度,决定如何更新模型的参数。常选择:AdamW 和 Adafactor
-
Adam (Adaptive Moment Estimation): 一种非常流行的自适应学习率优化算法。它会为每个参数维护一个独立的学习率,并根据梯度的一阶矩(均值)和二阶矩(未中心化的方差)来动态调整这个学习率。
-
“动量” (Momentum): Adam 借鉴了动量的思想,使得参数更新方向在一定程度上保持历史趋势,有助于加速收敛并越过一些小的局部最优。
-
自适应学习率: 对不同参数使用不同的学习率,对梯度大的参数学习率小一些,对梯度小的参数学习率大一些。
-
常见超参数设置 (β1, β2, ε): β1 (通常 0.9) 控制一阶矩的衰减率,β2 (通常 0.95 或 0.999) 控制二阶矩的衰减率,ε (通常 10^-8) 是一个防止除零的小常数。
-
-
AdamW: 是 Adam 的一个改进版本,它将权重衰减 (Weight Decay) 从梯度更新中解耦出来,直接作用于参数本身,被认为在泛化性能上通常优于原始的 Adam。这是目前大模型预训练中最常用的优化器。
-
Adafactor: Adam 的另一个变种,主要目的是减少内存占用。它通过一些技巧(如不存储完整的二阶矩,或者对二阶矩进行低秩分解)来降低优化器状态的存储需求,这对于训练参数量巨大的模型非常有帮助。T5 和 PaLM 等模型就使用了 Adafactor。
-
Adafactor 的超参数设置: 通常也需要设置 β1 和 β2,但其更新规则和内存管理方式与 Adam/AdamW 不同
-
3、可扩展的高效训练技术
如何在有限的计算资源下高效地训练模型已经成为制约大语言模型研发的关键技术挑战。其中,主要面临着两个技术问题:一是如何提高训练效率;二是如何将庞大的模型有效地加载到不同的处理器中
1) 3D 并行训练--三维协同作战
3D 并行并不是指三维空间,而是指三种并行策略的组合,它们从不同维度分解训练任务,分配给多个计算单元(通常是 GPU)协同完成。这三种策略是:
- 数据并行 (Data Parallelism, DP) - “大家分头处理不同作业,最后汇总答案”
- 模型复制: 把完整的模型参数和优化器状态复制到每一个 GPU 上。
- 数据分片: 把一个大的训练批次 (Batch) 的数据平均分配给这些 GPU。
- 独立计算梯度: 每个 GPU 只处理自己分到的那部分数据,独立地进行前向传播和反向传播,计算出梯度。
- 梯度同步与更新: 所有 GPU 计算完梯度后,将各自的梯度进行聚合(通常是求平均),然后用这个聚合后的梯度去更新每一个 GPU 上的模型参数。这样保证了所有 GPU 上的模型始终保持一致。
优点: 实现相对简单,很多深度学习框架都内置支持。可以有效提高训练的吞吐量(单位时间处理的数据量)。
缺点: 每个 GPU 都需要存储完整的模型,当模型非常大时,单个 GPU 可能还是放不下。
- 流水线并行 (Pipeline Parallelism, PP) - “生产线模式,各 GPU 负责不同工序”
将模型的不同层(或者说计算图的不同阶段)分配到不同的 GPU 上。数据像在流水线上一样,依次流过这些 GPU,每个 GPU 只负责自己那一部分“工序”的计算。
-
流水线气泡 (Pipeline Bubble): 简单的流水线会导致很多 GPU 处于空闲等待状态(比如 GPU2 要等 GPU1 处理完才能开始)。这就像生产线上,如果前一个工序慢了,后面的工序就得等着。
-
GPipe / Micro-batching: 为了减少气泡,通常会将一个大的批次数据再切分成更小的“微批次 (Micro-batches)”。让这些微批次尽可能连续地流过流水线,使得不同 GPU 可以更充分地并行工作,提高设备利用率。
-
梯度累积 (Gradient Accumulation): 由于数据被切分成了微批次,每个微批次计算的梯度可能不够稳定。通常会在处理完一个完整的大批次的所有微批次后,才累积梯度并进行一次模型更新。
优点: 可以将非常大的模型(其层数很多)分散到多个 GPU 上,解决单个 GPU 存不下的问题。
缺点: 实现比数据并行复杂,需要仔细设计层切分和调度策略以减少气泡。通信开销可能较大。
- 张量并行 (Tensor Parallelism, TP) / 模型并行的一种 - “一个大矩阵运算,大家分块计算”
对于模型中的单个大操作(比如一个巨大的矩阵乘法,这在 Transformer 的 FFN 层和 Attention 层的权重矩阵中非常常见),将其在张量(多维数组)的维度上进行切分,分配给不同的 GPU 并行计算,最后再将结果合并。流水线并行是按层切分模型(纵向切);张量并行是对层内的单个大运算进行切分(横向切或更细致的切)。实现方式 (如 Megatron-LM):
-
按列切分 (Column Parallelism): 比如一个权重矩阵 W,可以将其按列切分成 W = [W_A, W_B]。那么 Y = XW 就变成了 Y = X[W_A, W_B] = [XW_A, XW_B]。可以让 GPU0 计算 XW_A,GPU1 计算 XW_B,然后拼接结果。
-
按行切分 (Row Parallelism): 同样,也可以按行切分。
-
在 Transformer 中,通常会将 FFN 层的第一个权重矩阵按列切分,第二个权重矩阵按行切分,并通过巧妙的 AllReduce 通信操作来保证计算的正确性。Attention 层的 Q, K, V 投影和输出投影也可以类似地进行张量并行。
优点: 可以将单个非常大的层(其权重矩阵单个 GPU 放不下)分散到多个 GPU 上,解决了层内参数过大的问题。可以有效减少模型参数在单个 GPU 上的存储压力。
缺点: 实现非常复杂,需要对模型的具体运算进行深入分析和改造。通信开销也比较大,因为需要在不同 GPU 之间传递中间计算结果。
2)零冗余优化器ZeRO
在数据并行中,虽然模型参数在每个 GPU 上都有副本,但更占显存的其实是优化器状态(比如 Adam 优化器的动量和方差)、梯度以及模型参数本身。ZeRO 的目标就是消除这些状态在数据并行时的冗余存储。将优化器状态、梯度和模型参数也进行分片,每个 GPU 只负责存储和更新完整状态的一部分。
-
ZeRO 的不同阶段 (Stage 1, 2, 3):
-
Stage 1: 只对优化器状态进行分片。
-
Stage 2: 对优化器状态和梯度进行分片。
-
Stage 3: 对优化器状态、梯度和模型参数本身都进行分片。这是最极致的节省显存的方式。
-
-
PyTorch FSDP (Fully Sharded Data Parallel): PyTorch 提供的功能,与 ZeRO Stage 3 的思想类似。
优点: 极大地降低了数据并行时的显存占用,使得可以在同等硬件条件下训练更大的模型,或者使用更大的批次大小。
缺点: 增加了通信开销,因为需要在不同 GPU 之间传递分片后的状态。
3)激活重计算
在前向传播过程中,为了计算反向传播时的梯度,需要存储每一层的激活值 (Activation)。对于层数很深的模型,这些激活值会占用大量的显存。
- 核心思想: 不存储所有中间层的激活值,而是在反向传播需要用到某个激活值时,重新从前一个检查点 (Checkpoint) 开始进行一次局部的前向计算来得到它。
-
做法:
-
在前向传播时,只保存少量关键节点(检查点)的激活值。
-
在反向传播计算到某个需要中间激活值的层时,如果这个激活值没有被保存,就找到离它最近的前一个检查点,然后从这个检查点开始重新执行一小段前向传播,得到所需的激活值。
-
优点: 显著减少显存占用,使得可以在有限的显存下训练更深或更大的模型。
缺点: 增加了计算时间,因为需要进行额外的局部前向传播。这是一种典型的“时间换空间”的策略。
4)混合精度训练
传统的神经网络训练通常使用 32 位浮点数 (FP32) 进行计算。但现代 GPU 对 16 位浮点数 (FP16 或 BF16) 的计算速度更快,显存占用也更小。
- 核心思想: 在训练过程中,同时使用高精度 (FP32) 和低精度 (FP16/BF16) 的浮点数
-
做法:
-
模型权重和激活值用低精度 (FP16/BF16) 存储和计算: 这可以加快计算速度,减少显存占用。
-
梯度计算和参数更新用高精度 (FP32) 进行: 为了保持数值的稳定性和精度,在反向传播计算梯度以及优化器更新模型权重时,通常会将梯度和权重转换回 FP32 进行。
-
动态损失缩放 (Dynamic Loss Scaling): 由于 FP16 的表示范围较小,在反向传播时梯度值可能变得非常小(下溢)而变成零,导致无法更新参数。动态损失缩放通过将损失值乘以一个缩放因子,使得梯度值相应增大,避免下溢。如果梯度值过大(上溢),则减小缩放因子。
-
优点:训练速度提升: GPU 对低精度运算有加速;显存占用减少: 低精度数据占用显存更少
缺点: 需要仔细处理数值精度问题,防止训练不稳定。