如何赋予大语言模型以“灵魂”?深度解析增量预训练(Continual Pre-training)逻辑与实战代码

69 阅读15分钟

知求木之长者,必固其根本;欲流之远者,必浚其泉源。

大语言模型的出现,彻底改变了数据的宿命,原本只能躺在硬盘里吃灰的存档、记录,如今转化为了能够用于训练大模型、让大模型理解专业化领域的“智慧之果”。

所以怎么使用这些数据?是采用 RAG 技术增强检索?还是通过增量预训练注入知识?抑或是利用微调提升任务表现?到底该怎么选?我将开设一系列专栏记录我在学习过程中遇到的难题与思考,从核心概念到实践代码,希望能为每一个想法的落地提供助力。文中观点难免存在不足之处,欢迎各位技术大佬指正讨论。


什么是 增量预训练 Continual Pre-training ?

Continual Learning of Large Language Models: A Comprehensive Survey 根据这篇发布在24年的综述,若想要赋予大模型以灵魂,需要经过这三种增量预训练:

  1. 增量预训练 (Continual Pre-Training, CPT):

    • 概念:在供应商端,利用新收集的大规模无标签数据和现有数据对模型进行持续训练。

    • 目的:应对时间偏移。大模型不再是死板的百科全书,通过CPT,它能吃掉最新的时政新闻、科技进展和语言演变。这是模型保持“活性”的基础。

  2. 领域自适应预训练 (Domain-Adaptive Pre-training, DAP):

    • 概念:针对特定领域(如医疗、法律、代码)的无标签数据进行专项训练。

    • 目的:应对空间迁移。这是赋予大模型“灵魂”的关键一步。它不再只是“泛泛而谈”,而是开始理解行业黑话、逻辑范式和专业知识。

  3. 持续微调 (Continual Fine-Tuning, CFT):

    • 概念:在消费端(应用层),针对具体任务进行连续的指令微调(SFT)和对齐(RLHF)。

    • 目的:提高任务成功率和输出质量。如果说 CPT 和 DAP 是在给大模型‘灌注海量知识’,那么CFT就是在规范它的‘表达方式’,让它知道如何把脑子里的东西转化成用户想要的答案。

注意此处CFT是指微调技术的应用方法,与具体的SFT技术不同

虽然这三者相辅相成,构建了大模型从“清纯大学生”到“硬核博士生”再到“高效打工人”的垂直进阶路径。但在现实中,由于算力有限数据壁垒信息分散,很少有厂商能凭一己之力跑通全链路。


怎么选择适合自己的 增量预训练 Continual Pre-training?

场景一:应对知识过时或语言能力不足

当大模型因知识陈旧或对特定语言(如中文)支持较弱而无法满足业务需求时,增量预训练 (Continual Pre-Training)是有效的解决方案。通过输入大规模的新近语料(如新闻、百科),使模型在学习过程中不断吸收和内化新知识,从而保持其信息的时效性和语言适应性。

下图展示了由于 Llama 系列模型对中文的原生支持有限,HuggingFace 与 ModelScope 社区中的开发者们广泛采用增量预训练或微调,以增强其中文能力。

how-to-cpt-2.png

场景二:应对垂直领域专业知识缺失

当大模型因缺乏特定领域的深度知识、术语或专业逻辑而无法胜任任务时,应采用领域自适应预训练(Domain-Adaptive Pre-training)。通过使用该领域(如医学、法律、金融)的大规模专业语料进行训练,使模型构建起领域的核心知识框架与思维模式,从而获得精准的专业表达能力。

下图对比了通用模型 Pythia 6.9B 与经过金融领域增量预训练后的 FinPythia 6.9B 的表现。面对专业的金融领域问题,前者无法理解术语,而后者能生成准确、详细的答案。

how-to-cpt-2.png

转自Efficient continual pre-training LLMs for financial domains

在技术上,CPT 与 DAP 的实现路径相同,主要区别在于训练数据集的构建。在学术界,DAP通常被视为CPT针对特定垂直领域的具体实践。

场景三:优化任务执行与指令遵循能力

当模型已掌握知识,但在遵循指令、输出格式或满足安全规范方面存在不足时,则需要持续微调 (Continual Fine-Tuning),通过精心构造的指令数据与人类反馈或者标准化的输入输出,对模型进行反复、细致的校准与优化,让它学会将内在知识转化为清晰、有用、符合指令的答案。

此阶段通常侧重于在应用层,根据用户反馈和任务成功率,对模型进行持续、迭代、个性化的精细化调整。


选择 增量预训练需要付出什么,会带来什么收益呢?

Domain Adaptation of Foundation LLMs for e-Commerce我们可以从这篇eBay团队发布于25年1月的论文看到,进行CPT训练,只需要达到训练原模型的GPU硬件资源的 5% 以下就能够进行(分别是 16000 块 H100 和 480 块 H100),所需要的数据集集也只需要原本的 5% 左右(分别是 15 万亿 toekns 和 1 万亿 tokens),这样我们只需要花费大约原模型不到 5% 的成本,就能得到一个在电商这种知识极其细碎、术语更新极快的领域,通用大模型(即使是 405B 参数量的 Llama 3.1)也无法完全替代的,经过 CPT 注入深度行业知识的专有模型。

目前还有人提出了CPT Scaling Laws,将 CPT 从“撞大运”的炼丹过程变成了可工程化、可预测收益的投资。

类似的还有Towards Effective and Efficient Continual Pre-training of Large Language Models给出了 Llama-3 8B 进行 CPT 后的数据:数学能力提升 12.00分,中文能力提升 8.81分。

而这些恰好证明了浅层的微调、外部的RAG难以像增量预训练一样赋予大模型灵魂。

所以怎么进行 增量预训练 Continual Pre-training ?

我们可以使用训练架构(如Unsloth)、算子优化(如flash-attentionLiger-Kernel)等各类优化,通过一张显存在16G及以上的N卡(或者通过一些算力平台)训练一个14B参数量级的模型来进行尝试。

这里我特别推荐大家去Unsloth Notebooks上找相应的案例,Unsloth官方提供了几乎目前所有开源模型的基于Unsloth框架训练的colab笔记本,而且Unsloth自身也是一个特别优秀的框架,能够显著地加速训练和降低显存占用。

接下来我们将以该笔记本为模板,以Qwen-3-4B,来讲解CPT的训练流程以及各个参数的作用。

1. 配置环境

pip install unsloth

2. 下载模型

pip install modelscope
modelscope download --model unsloth/Qwen3-4B --local_dir 你模型下载到的本地位置

3. 数据准备

我们选取一招金融数据集-sample中cn-sample.jsonl的作为训练集,并把训练集的文字内容处理为{"text": "### 标题: ... ### 内容: ... "}的格式,便于后续数据集的加载。

数据集很有可能已经涵盖在预训练过程中,这里选取该数据集是为了跑通代码

Towards Effective and Efficient Continual Pre-training of Large Language Models
这篇论文提到,他们对 Llama-3 的语言迁移的持续预训练训练中,将数据集配置为中文、英文和合成指令数据的比例调整为1:7:2,目的是提供训练复杂度的渐进和平滑过渡。

Domain Adaptation of Foundation LLMs for e-Commerce
这篇论文提到,eBay 团队在训练 e-Llama 时,所使用的数据集 50% 为电商特定数据,50% 为通用领域数据。

以上两篇论文对我们的 CPT 实践指导意义重大,说明我们在实际的 CPT 训练中,不宜只有相应领域的专业数据,同样需要包括一些通用领域数据或者一些合成数据,来防止模型发生灾难性遗忘。为了演示代码简洁性,下方代码仅加载了金融数据。在实际生产环境中,请务必按照上文提到的比例,将通用数据集与专业领域数据混合后再进行训练。

IBM-什么是灾难性遗忘?
当神经网络在使用新数据训练后或针对特定任务进行微调后忘记以前学过的任务时,就会发生灾难性遗忘。这种现象也被称为灾难性干扰,它导致训练有素的网络在连续学习过程中使用新数据训练时,丢失与旧任务相关的信息。
许多人工智能技术的部署需要机器学习模型不断适应新的用例。当新任务的训练过程干扰了模型对旧任务的理解时,就会出现灾难性遗忘现象。随着新知识取代过往习得的知识,模型就会丧失处理其原始任务的能力。

4. Talk is Cheap, Show me the code

4.1 加载模型

from unsloth import FastLanguageModel
import torch
max_seq_length = 2048 # 可以根据数据集的处理来决定序列的长度,更长的序列长度会导致显存占用呈指数级上涨
dtype = None # None 表示自动检测。Tesla T4/V100 建议使用 Float16,Ampere 架构及以上建议使用 Bfloat16,一般情况下保持 None 即可
load_in_4bit = True # 使用 4bit 量化以减少显存占用。也可以设置为 False。

# Unsloth支持的 4bit 预量化模型,模型大小小 4 倍且能防止显存溢出 (OOM)。
# 在ModelScope社区有Unsloth的镜像,可以预先下载到本地来跳过从HuggingFace上下载
fourbit_models = [
  "unsloth/Qwen3-4B",
  "unsloth/Phi-4-mini-instruct",
  "unsloth/gemma-3-12b-it",
]

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "你模型下载到的本地位置",
    max_seq_length = max_seq_length,
    dtype = dtype,
    load_in_4bit = load_in_4bit,
)

4.2 添加 LoRA 适配器

现在添加 LoRA 适配器,这样我们只需要更新所有参数的 1% 到 10%,极大地降低了显存的需要。 还添加了 embed_tokens 和 lm_head,以允许模型学习预训练数据以外的的数据。

model = FastLanguageModel.get_peft_model(
    model,
    # target_modules 定义了我们要对哪些层添加 LoRA 适配器
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", # 注意力层
                      "gate_proj", "up_proj", "down_proj",    # 前馈网络层

                      "embed_tokens", "lm_head",],            # 嵌入层和输出层(目前很多模型将这两层指向了同一片区域,不过这样训练问题也不大)
    r = 32,           # 秩 (Rank):大于 0 的整数。建议值为 8, 16, 32, 64, 128
    lora_alpha = 32,  # 缩放系数:通常设为与 r 相同或 r 的 2 倍
    lora_dropout = 0, # 失活率:支持任意值,但设为 0 是经过 Unsloth 优化的(速度最快)
    bias = "none",    # 偏置:支持任意值,但 "none" 是经过优化的
    use_gradient_checkpointing = "unsloth", # 使用梯度检查点:设置为 "unsloth" 以支持极长上下文并节省显存
    random_state = 3407, # 随机种子(著名的 3407 种子,据称能提升表现)
    use_rslora = True,   # 使用 Rank Stabilized LoRA(秩稳定 LoRA)
    loftq_config = None, # 使用 LoftQ 量化初始化
)

4.3 加载并处理数据集

from datasets import load_dataset

dataset = load_dataset("json", data_files=你数据集的下载位置, split="train")

EOS_TOKEN = tokenizer.eos_token 

def formatting_prompts_func(examples):
  # 为每个文本块添加 EOS,这样在 Packing(打包)时模型能知道哪里是一句话的结束
    return { "text" : [example + EOS_TOKEN for example in examples["text"]] }

dataset = dataset.map(formatting_prompts_func, batched = True,) # 为数据集的每段文字的末尾添加 EOS_TOKEN, 以免模型进行无尽地输出

4.4 准备训练器

现在让我们配置 Unsloth 的 UnslothTrainer 训练器的参数

为了加快速度,我们目前只设置了 20 个步骤 (steps)。但你可以将 num_train_epochs 设置为 1 并同时将 max_steps 设为 None 来进行完整的训练。

此外,请务必将 embedding_learning_rate(嵌入层学习率)设置为比常规 learning_rate(学习率)小至少 2 倍或 10 倍的值,这样才能确保增量预训练有效!

为什么 embedding_learning_rate 要更小?
在增量预训练中,由于你开启了 embed_tokens 和 lm_head 的训练,这两个层直接影响模型对词汇的底层理解。
如果这两个层的学习率太高,模型会非常容易产生“灾难性遗忘”,导致原本学到的语言基础被破坏。因此,通常给它们一个更微弱的步长,让它们在保留旧知识的同时缓慢适应新数据。

from transformers import TrainingArguments
from unsloth import UnslothTrainer, UnslothTrainingArguments

trainer = UnslothTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    dataset_text_field = "text",         # 数据集中对应的文本列名
    max_seq_length = max_seq_length,
    dataset_num_proc = 4,                # 数据预处理使用的进程数(多核加速)
    packing = True,
    train_on_responses_only = False,

    args = UnslothTrainingArguments(
        per_device_train_batch_size = 2, # 每个显卡上的显存批次大小
        gradient_accumulation_steps = 8, # 梯度累积:相当于总 batch size 为 2*8=16

        # 对于较长时间的训练,请使用 warmup_ratio 和 num_train_epochs 而非 max_steps 和 warmup_steps
        max_steps = 120,                 # 训练总步数(快速测试用)
        warmup_steps = 10,               # 热身步数:学习率从 0 缓慢升至设定的初始阶段
        # warmup_ratio = 0.1,            # 热身比例(例如总步数的 10%)
        # num_train_epochs = 1,          # 训练轮数

        # 为嵌入矩阵(Embedding matrices)选择一个小 2 到 10 倍的学习率
        learning_rate = 5e-5,            # 主学习率
        embedding_learning_rate = 1e-5,  # 嵌入层专用学习率

        logging_steps = 1,               # 每一步都打印日志
        optim = "adamw_8bit",            # 使用 8-bit AdamW 优化器(省显存)
        weight_decay = 0.001,            # 权重衰减:防止过拟合
        lr_scheduler_type = "linear",    # 学习率调度器类型:线性下降
        seed = 3407,                     # 随机种子
        output_dir = "outputs",          # 模型保存路径
        report_to = "none",              # 不上报日志(可改为 "wandb" 或 "tensorboard")
    ),
)

4.5 开始炼丹!

# 输出GPU简要情况
gpu_stats = torch.cuda.get_device_properties(0)
start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)
print(f"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.")
print(f"{start_gpu_memory} GB of memory reserved.")

output_path = "CPT LoRA输出路径"

trainer_stats = trainer.train()

# 简要总结训练的GPU使用情况
used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
used_memory_for_lora = round(used_memory - start_gpu_memory, 3)
used_percentage = round(used_memory / max_memory * 100, 3)
lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)
print(f"{trainer_stats.metrics['train_runtime']} seconds used for training.")
print(
    f"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training."
)
print(f"Peak reserved memory = {used_memory} GB.")
print(f"Peak reserved memory for training = {used_memory_for_lora} GB.")
print(f"Peak reserved memory % of max memory = {used_percentage} %.")
print(f"Peak reserved memory for training % of max memory = {lora_percentage} %.")

trainer.model.save_pretrained(output_path)
trainer.tokenizer.save_pretrained(output_path)

print(f"模型已保存到: {output_path}")

Over!

恭喜你完成了你的第一个CPT训练,你可以通过train_state.json来观察训练的过程。

5. 评估模型的训练结果

我们可以简要地通过 Loss 与 Gradient Norm 来观察模型的训练情况

一个CPT曲线

大概率你的曲线不会这样完整,因为我们直接截取了 120 步,但我们能从这张图看到,

  • Loss 总体呈下降趋势,伴随着震荡:这是训练的最基本目标。优化器( AdamW)在不断调整模型参数,试图最小化损失函数。震荡是因为每个批次的数据分布略有不同,导致 Loss 在下降的大趋势中会有波动。

  • Gradient Norm 梯度范数从一个比较高的值短暂回落到一个比较低的值,再升到一个较高的值并不断震荡。

Loss 下降就证明还在学习东西(我们还能通过添加评估数据集来评估当前是否过拟合或者发生灾难性遗忘),梯度没有爆炸(指数级上升)即证明训练时稳定,梯度没有趋近于 0 即证明陷入完全的过拟合。

如要严谨地评估CPT训练效果,应当设计合适的问答对,分别对比原始模型和 CPT 后的模型。


结语

至此,我们已经跑通了增量预训练(CPT)的全流程。

虽然 120 步的训练只能让 Loss 曲线微微低头,尚不足以让模型脱胎换骨,但我们已经掌握了通往“领域模型”的钥匙。

真正的“灵魂”注入,不在于这几行代码,而在于:

  • 高质量、高密度的领域数据清洗(Data Engineering)。
  • 通用数据与专业数据的科学配比(防止遗忘)。
  • 算力与耐心的投入(Scaling)。

祝大家炼丹成功 :-)

参考文献 References

  1. 综述:大模型持续学习
    Tongtong Wu, et al. Continual Learning of Large Language Models: A Comprehensive Survey. (2024). arXiv.
    arxiv.org/abs/2404.16…

  2. eBay 电商领域 CPT 实践
    eBay Team. Domain Adaptation of Foundation LLMs for e-Commerce. (Jan 2025). arXiv.
    arxiv.org/abs/2501.09…

  3. CPT 扩展定律 (Scaling Laws)
    Scaling Laws for Continual Pre-training LLMs. OpenReview.
    openreview.net/forum?id=Vk…

  4. Llama-3 高效增量预训练
    Chen, et al. Towards Effective and Efficient Continual Pre-training of Large Language Models. (2025). ACL Anthology.
    aclanthology.org/2025.acl-lo…

  5. 金融领域高效 CPT 案例
    AWS Machine Learning Blog. Efficient continual pre-training LLMs for financial domains.
    aws.amazon.com/cn/blogs/ma…

  6. 概念解释:灾难性遗忘
    IBM Topics. What is catastrophic forgetting?
    www.ibm.com/cn-zh/think…

  7. Unsloth 官方指南:增量预训练
    Unsloth Team. Continual Pretraining with Unsloth - A practical guide.
    unsloth.ai/blog/contpr…

  8. 深度解析:如何通过 CPT 打造领域专家模型
    Towards AI. Mastering Continual Pretraining: How to Transform Generalist LLMs into Domain Experts.
    pub.towardsai.net/mastering-c…

  9. Unsloth 官方实战代码仓库
    Unsloth Notebooks: Fine-tuning & Continual Pre-training examples.
    github.com/unslothai/n…

  10. Qwen3-4B 基座模型 (ModelScope) Qwen/Qwen3-4B - ModelScope. modelscope.cn/models/Qwen…

  11. 一招金融数据集 (YiZhao-FinDataSet) CMB AI Lab. Financial Dataset for LLM SFT & Pre-training. www.modelscope.cn/datasets/CM…