大模型继续预训练是提升模型领域知识的重要手段,本文从概念、案例到代码实战,详细解析了其与大模型训练体系的关系及实现路径。SFT微调更偏向行为模式调整,而继续预训练则通过大量领域文本让模型学习语言分布、术语体系和知识组织方式。GLM 5 和 MEDITRON-70B 案例展示了继续预训练在工业和研究中的应用,强调了高质量、领域性强的原始语料的重要性。代码实战部分通过小规模教学案例,展示了继续预训练的数据准备、分词分块、全量模型训练和4bit量化+LoRA训练的实现过程,帮助读者理解继续预训练的完整流程。本文适合想要学习大模型领域知识增强的程序员阅读。
前言
课程回顾
在前面的课程中,我们已经系统讲解了 SFT 微调的完整过程,包括不同的微调方法,如全量微调、LoRA 微调和 QLoRA 微调,以及配套的数据生成思路。通过这部分内容,我们其实也能够看到 SFT 微调的一些典型特点与局限。
例如,SFT 更擅长学习的是一种输出模式上的变化。当我们提供的数据中,非思考模式下的回复整体较短,而思考模式下的回复整体较长时,模型往往就会学习到这样一种模式:在非思考模式下倾向于给出较短回答,而在思考模式下倾向于输出较长内容。也就是说,SFT 更容易学到的是“应该怎样回答”,而不一定是“真正新增了多少领域知识”。
从这个角度来看,SFT 的核心作用更偏向于行为模式调整,而不是向模型内部真正注入大量新的垂类知识。如果我们的目标是让模型补充某一垂直领域中的知识储备,使其在参数层面更熟悉该领域的术语、表达方式和知识分布,那么所需要做的事情就不是停留在 SFT 阶段,而是要回到预训练这一层面,为模型补充大量高质量的垂类语料。
继续预训练的概念
为了实现这一目标,通常有两种思路。第一种是从头开始重新训练一个基座模型(Base Model)。理论上讲,如果我们手中拥有海量的垂类语料,并且具备充足的算力资源,那么完全可以像大模型厂商一样,从零训练一个属于自己的领域基座模型。但这种情况毕竟只是少数,因为训练一个基座模型往往需要极其庞大的算力、数据和工程投入,成本非常高。
因此,对于大多数垂直领域厂商或研究团队而言,更现实、也更经济的方案,通常是在大模型厂商已经开源的 Base Model 基础上进行继续预训练。在这种情况下,无论是对算力资源的要求,还是对数据规模的要求,都会比从零训练一个模型低得多,因此也更具可行性。
不过,这里有一个非常重要的点需要特别区分:我们这里所说的继续预训练,所使用的模型并不是前面课程中经常使用的 qwen3-0.6B,而是第二节课中曾经尝试过的 qwen3-0.6B-Base。原因在于,前者已经经历了后训练过程,不再只是一个单纯的语言模型,而更像是一个经过对齐的“助手模型”;它已经学会了更倾向于回答问题、遵循指令以及按照特定方式组织输出。
而继续预训练的目标,本质上仍然是预训练目标,也就是让模型继续通过大量原始文本去适应某个领域的语言分布与知识结构。比如我们持续给模型喂法律、医疗、金融、教育、代码等领域的文本,它会逐渐更熟悉该领域常见专业术语、术语之间的搭配关系以及某些概念在这个领域里的固定说法。
但如果此时我们直接在一个已经完成 SFT 或后训练的模型上,再用大量原始领域文本进行继续预训练,就有可能削弱模型原本已经学到的助手行为,使其指令跟随能力、对话风格乃至整体输出表现受到影响,最终变成一个既不像基础语言模型、也不像成熟助手模型的“中间态”模型。也正因如此,在继续预训练这一环节中,我们通常会选择 Base Model 作为起点,而不是已经完成后训练的指令模型。
当然,这样做也会带来一个现实问题,也就是在继续预训练完成之后,模型虽然在领域知识层面得到了增强,但后续往往还需要我们自己继续完成 SFT 微调以及更进一步的对齐工作。这对于很多垂直领域团队来说,其实本身就是一件门槛较高、成本较大的事情。也正因为如此,在实际业务中,很多团队并不会轻易选择继续预训练这条路线,而是更常见地采用另一种折中方案,即直接在现有指令模型的基础上,通过微调调整其对话风格和输出模式,再结合提示词工程、RAG,甚至 Agent 系统来补齐领域知识方面的短板,从而满足实际应用需求。
因此,继续预训练并不是默认选项。通常只有在以下条件同时具备时,它才真正值得考虑,比如模型对领域术语明显不熟悉、现有方法难以弥补这一短板,并且手中确实拥有大量高质量、非公开的原始领域数据。只有在这种情况下,继续预训练才可能成为一条具有投入价值的路线。
GLM 5 预训练过程解析
其实,继续预训练在大模型厂商的训练流程中是非常常见的。以最新推出的 GLM 5 为例,我们可以很清楚地看到这一点。其整体训练流程大致可以分为两个部分:一部分是基座模型(Base Model)训练,另一部分是后训练(Post-Training)。

首先来看左侧的 Base Model 训练部分。基座模型并不是一次性训练完成的,而是经历了多个阶段。其首先使用 18T(18 万亿 tokens)的通用语料进行预训练,在此基础上,又继续引入 9T 的代码与推理语料进行强化。从这个角度来看,这一阶段其实就已经体现出了“继续预训练”的思想,因为模型并不是只依赖单一通用语料完成训练,而是在完成基础能力构建后,进一步通过代码与推理相关语料补充特定能力。
图中下方标注的 4K(1K ≈ 1024),可以理解为这一阶段模型训练时所使用的上下文长度,也就是训练时的最大序列长度大约在 4096 token 左右。
除了 Pre-training 之外,GLM 5 团队还加入了一个 Mid-training 阶段,也可以理解为是新一轮的继续预训练。在这一阶段中,模型继续补充代码、推理以及长上下文相关数据,同时进一步扩展上下文长度,使模型具备处理更长文本输入的能力。其目标非常明确:不仅要让模型具备短链路推理能力,还希望它能够处理更长的代码文件、更复杂的推理过程以及更长上下文下的任务场景。也正因如此,在这一阶段之后,模型的长上下文能力会得到进一步增强。
在最下方,我们还可以看到一个 Sparse Attention Adaptation 模块。它可以理解为:为了将模型上下文窗口进一步扩展到 200K 这样的超长范围,团队专门进行了一轮稀疏注意力适配。其原因在于,标准 Transformer 的全注意力机制在超长上下文场景下计算成本非常高,因此如果希望模型稳定支持 128K 甚至 200K 的上下文长度,通常就需要引入一些结构层面的优化,例如稀疏注意力、分块注意力等方法。不过需要注意的是,虽然模型在结构设计上具备了超长上下文支持能力,但在实际使用中,当上下文长度过长时,模型的回复质量仍可能出现明显下降,这也是当前长上下文模型中较为常见的现象。
总的来看,左侧的基座模型训练过程,本质上就是先通过大规模通用语料与代码推理语料完成基础能力构建,再通过 mid-training 与稀疏注意力适配,进一步将模型推向长上下文、长链路推理以及 Agent 场景适应的方向。从这也可以看出,继续预训练的目的就是为了在通用的基础上强化部分垂类的知识内容,使其模型后续能够掌握部分特定的能力。
而右侧的 Post-Training,则是在基座模型的基础上,先通过一轮 SFT(监督微调) 让模型具备更好的指令跟随与基础助手行为能力,随后再借助强化学习进一步提升其推理能力、智能体任务执行能力以及工具调用能力,并通过蒸馏等方式尽可能保留这些阶段性训练成果。至于这一部分所涉及的强化学习训练机制,我们将在下一节课中再进行详细讲解。
继续预训练的训练流程
为了更具体地理解继续预训练的实际训练过程,这里我们可以结合医疗垂直领域的一篇代表性论文——MEDITRON-70B: Scaling Medical Pretraining for Large Language Models——来看,大模型究竟是如何基于一个通用基座模型继续注入垂类知识的。该工作并不是从零开始训练医疗模型,而是在 Llama-2 的 7B 和 70B 模型基础上,进一步进行医疗领域的 continued pretraining。
从数据角度来看,MEDITRON 的继续预训练语料并不是随意拼接而成的,而是经过筛选和消融实验后逐步确定下来的。论文中给出的 GAP-REPLAY 语料由四部分组成:
- PubMed / PMC 全文论文:约 40.7B tokens
- PubMed 摘要:约 5.48B tokens
- Clinical Guidelines(临床指南):约 107M tokens
- Experience Replay:约 420M tokens 的通用域回放数据
其中训练集与验证集合计约为 48.1B tokens;其中训练部分约 46.7B,验证部分约 1.4B。

更重要的是,这套语料并不是一开始就固定下来的。论文专门做了多种数据混合方案的对比实验,例如只使用 PMC、加入 replay、加入代码数据、以及最终的 GAP + Replay 等。实验结果表明,仅依赖公开医学论文并不是最优方案;在加入 PubMed 摘要与临床指南之后,模型的下游医疗推理表现更好。同时,作者也尝试过向继续预训练语料中加入代码数据,但在他们这套医疗任务设定下,并没有带来更优的下游结果,因此最终没有被选入正式训练方案。这一点也说明,继续预训练的语料设计并不是“数据越杂越好”,而是要围绕具体领域目标进行验证和筛选。
从这个案例也能看出,继续预训练的数据往往具有两个典型特点。第一,领域性要足够强。对于医疗场景而言,模型不仅需要接触医学论文中的知识,还需要接触面向真实临床实践的指南类文本,因为后者更贴近“如何做决策”和“如何进行规范处置”。第二,仍然需要保留少量通用域数据。MEDITRON 在训练混合中加入了约 420M tokens 的 Experience Replay,并明确指出这样做是为了缓解继续预训练过程中可能出现的 catastrophic forgetting(灾难性遗忘),尽量避免模型在吸收大量医疗文本后明显损失原有的通用能力。
在完成数据准备之后,接下来便是在 Llama-2 的基础上进行继续预训练。MEDITRON 在这一阶段并没有引入特别复杂的新架构,而是基本沿用了 Llama-2 的核心设计,包括标准 Transformer、RMSNorm、SwiGLU、RoPE 和 GQA。
需要注意的是,虽然这条路线不是“从零训练一个 base model”,但它绝不是轻量级的小实验。论文在附录中给出的统计是 70B 模型的继续预训练共运行了 332 小时,使用了 128 张 A100 GPU,总计约 42,496 GPU-hours。这说明,哪怕是建立在现有开源模型上的继续预训练,整体成本仍然非常高,对于一般的开发者而言是很难以实现的。
在完成继续预训练之后,模型并不会直接停止,而是还会进入 SFT(监督微调) 阶段。不过,MEDITRON 这里的 SFT 并不是先去训练一个统一的“医疗问答助手”,让模型先学会如何扮演一个通用医生角色,而是直接进入 task-specific supervised finetuning。也就是说,作者会把已经完成继续预训练的 MEDITRON,分别拿到不同医疗 benchmark 的训练集上单独微调,例如 PubMedQA、MedMCQA、MedQA 等。
这样做的目的,并不是再次补充医学知识,因为医学领域知识主要已经在前面的继续预训练阶段注入到模型内部;这一阶段更重要的作用,是让模型进一步适应具体任务的输入输出格式、答题方式以及评价标准。换句话说,前面的继续预训练解决的是“让模型更懂医疗”,而这里的 task-specific SFT 解决的则是“让模型更会完成具体的医疗推理任务”。因此,这一步本质上更像是在让模型针对不同 benchmark 进行专项适配,从而把前面获得的领域知识,真正转化为可衡量的任务表现。
在评测阶段,作者不仅测试了继续预训练后的 raw model,也测试了经过 task-specific finetuning 之后的模型,并进一步比较了不同推理策略下的表现,包括普通 top-token 选择、Chain-of-Thought(CoT) 以及 Self-consistency CoT。论文中显示,在不做额外监督微调的情况下,MEDITRON-70B 的 few-shot 平均成绩从 Llama-2-70B 的 60.8 提升到了 63.3,说明仅靠 continued pretraining,本身就已经让模型在医疗任务上表现出更强的领域适应性。
而在经过 task-specific finetuning 之后,模型的表现还会进一步提升。论文中显示,在 self-consistency CoT 设置下,MEDITRON-70B 的平均成绩达到了 72.0,高于同样流程下 Llama-2-70B 的 69.2。同时,论文还引用了 MedQA 的人类通过线 60.0,而 MEDITRON-70B 在多种推理设置下都超过了这一水平。

因此,MEDITRON 这篇论文很好地说明了 continued pretraining 的实际价值,即在高质量垂类语料上进行继续预训练,可以先把一个通用的 Llama-2 基座模型拉向医疗领域;随后再通过 task-specific SFT 让模型适应具体 benchmark 的任务形式,并配合 CoT 或 self-consistency 等推理策略,进一步释放其下游任务性能。 这也恰好说明,继续预训练与 SFT 并不是替代关系,而是分别作用于“领域适应”和“任务适应”这两个不同层面。
实战案例
在前面的内容中,我们已经从概念、产业案例和论文实践三个层面,逐步理解了继续预训练到底是什么、它与 SFT 的核心区别在哪里,以及为什么它更适合承担“领域知识注入”而不是“行为模式调整”这一任务。无论是前面提到的 GLM 5 在基座模型阶段通过多轮预训练与 mid-training 持续强化代码、推理和长上下文能力,还是 MEDITRON 基于 Llama-2 继续注入医疗知识后再结合 task-specific SFT 提升下游任务表现,都说明了一点:继续预训练的价值,主要体现在让模型先在参数层面更熟悉某一类领域语料,再为后续任务适配打下基础。
不过,也正如前文所分析的那样,真正工业级的继续预训练往往依赖海量高质量语料、复杂的数据配比以及昂贵的训练资源。对于一般教学或个人实验场景而言,我们很难完整复现这类大规模训练过程。因此,接下来的这部分内容,并不是要带大家真的训练出一个成熟可用的垂类基座模型,而是希望通过一个小规模、可落地、可复现的教学演示案例,把继续预训练最核心的工程流程完整走一遍。
在这个案例中,我们仍然沿用前面课程中使用过的《动手学深度学习》相关内容作为示例语料,以其中“过拟合与欠拟合”这一节为例,来模拟构造一个小型的领域文本数据集。之所以这样设计,一方面是因为这部分内容本身具有明确的知识边界和较强的专业表达特征,适合拿来演示“如何将连续文本整理为继续预训练所需语料”;另一方面也是因为它和前面课程中已经做过的 RAG、知识库构建等内容相互衔接,便于大家从已有材料出发理解继续预训练与其他训练方式之间的差异。
从实践目标来看,这里的重点并不在于最终一定要把模型训练到多强,而在于帮助大家真正看清楚继续预训练到底是拿什么样的数据来训练的,这些数据应该如何整理、如何分词、如何组织成可直接用于语言模型训练的样本,以及在代码实现上它与前面 SFT 微调究竟有哪些不同。 换句话说,这部分更关注的是“训练链路本身”,而不是“单次实验结果有多亮眼”。
具体来说,在接下来的代码实战中,我们会依次完成这样几个环节:首先,对原始文本进行清洗与整理,并构造出适合继续预训练使用的基础语料;接着,基于分词器将文本转换为 token 序列,并进一步组织成固定长度的训练样本;然后,在基座模型上分别尝试全量继续预训练与4bit 量化 + LoRA 两种训练方式,观察它们在实现路径与资源占用上的差异;最后,再结合训练结果,简单分析这类小规模继续预训练实验能够说明什么、又不能说明什么。
通过这一整套流程,大家就能够更直观地把前面讲过的那些概念真正落到代码上:继续预训练不是“换一种格式做 SFT”,也不是“再给助手模型喂一点文本”这么简单,而是要围绕Base Model、连续语料、语言建模目标以及后续任务适配来理解它的完整位置。这样一来,后面无论大家是要继续研究垂类知识增强,还是思考“究竟什么时候该做继续预训练、什么时候只做 SFT 或 RAG 就够了”,都会有一个更加清晰的判断基础。
代码实战
数据准备
数据载入
前面已经提到,对于继续预训练而言,它所需要的数据格式通常要比 SFT 更简单一些。因为继续预训练的目标不是去构造“问题—答案”式的监督样本,而是让模型基于大量连续文本继续进行语言建模训练。因此,在最初整理数据时,我们只需要把原始文本写成如下形式即可:
{"text": "......"}
{"text": "......"}
{"text": "......"}
也就是说,每一行只保留一段连续文本,并统一使用 text 作为字段名。模型在训练时看到的也不再是对话结构,而是这些原始语料本身,从而继续学习该领域中的表达方式、术语分布以及知识组织形式。
在本节的示例中,我们仍然沿用之前 RAG 部分里使用过的《动手学深度学习》中“过拟合与欠拟合”相关网页内容,来演示继续预训练的数据准备流程。不过,与前面那种“先进行文本切块再写入文件”
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(json.dumps({"text": text}, ensure_ascii=False) + "/n")
print("数据写入完成。")
在这段代码中,我们首先通过 WebBaseLoader 将目标网页正文提取出来,然后对文本进行简单清洗。包括统一换行符和压缩连续空行等内容。
完成清洗之后,我们并没有像之前那样立刻使用文本切分器把内容拆成很多小块,而是直接将整篇文本作为一个 text 字段写入 d2l_cpt.jsonl 文件中。这样做的好处在于:前面的数据准备过程会更简单,而真正面向模型训练所需的定长分块,可以统一放到后面的分词阶段之后再完成。
最终生成的文件内容大致如下所示:
{"text": "4.4. 模型选择、欠拟合和过拟合 ......"}
至此,我们就完成了继续预训练原始文本的准备工作。
数据集载入及分词
当我们将网页内容写入 jsonl 文件后,下一步还不能直接训练,因为当前数据仍然是原始字符串形式,而模型真正接收的是经过分词器编码后的 token 序列。因此,接下来我们需要先载入数据集,再使用分词器将文本转换成模型能够处理的数字编号。
该部分对应代码如下:
from datasets import load_dataset
from transformers import AutoTokenizer
model_name = r"D:/微调与部署/qwen-base"
data_file = "./data/d2l_cpt.jsonl"
dataset = load_dataset("json", data_files={"train": data_file})
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
def tokenize_function(examples):
texts = [text + tokenizer.eos_token for text in examples["text"]]
return tokenizer(texts, add_special_tokens=False)
tokenized_dataset = dataset.map(
tokenize_function,
batched=True,
remove_columns=["text"]
)
print(tokenized_dataset)
这里首先通过将刚刚保存好的 d2l_cpt.jsonl 读入为一个 Hugging Face 数据集对象。由于当前示例中只准备了一份原始语料文件,因此这里只构造了一个 train 划分:
dataset = load_dataset("json", data_files={"train": data_file})
随后,我们加载与目标模型对应的分词器。这里的 model_name 指向的是本地的 Qwen 基础模型目录,因此后续分词规则会与模型本身保持一致:
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
紧接着,这段代码是为了防止某些因果语言模型默认没有设置 pad_token,从而在后续处理或训练过程中报错。对于这类模型来说,通常会将 eos_token 兼作 pad_token 使用:
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
然后我们定义了一个专门用于分词的函数,将原始文本转换成 token 编号序列。同时我们在每条文本末尾都手动补上了一个 eos_token:
def tokenize_function(examples):
texts = [text + tokenizer.eos_token for text in examples["text"]]
return tokenizer(texts, add_special_tokens=False)
这样做的目的,是明确告诉模型这段文本已经结束,从而帮助模型在继续预训练时更清晰地学习文本边界。同时调用 tokenizer 的时候设置了 add_special_tokens=False 也是因为我们已经手动添加了结束标记,因此不再需要tokenizer 自动补充其他特殊符号。
这里添加
eos_token的方式其实相对比较直接,主要只是为了演示“如何在样本结尾补上结束标记”这一基本做法,并没有对网页文本本身进行更细致的样本划分。 可是在真实的继续预训练场景中,我们通常不会直接把整篇网页作为一条完整样本后只在末尾补一个EOS,因为这样做会让单条样本过长,模型也更容易把整篇网页视作一段需要持续生成的连续文本,从而弱化对正常文本边界的学习。
更合理的做法通常是先根据文本结构按段落、小节进行拆分;如果网页内容本身较长,也可以按照合适的长度切分为多个 chunk,再为每个 chunk 单独补上
eos_token。这样做不仅能够让训练样本的边界更加清晰,也有助于模型学习到“在一个相对完整的语义单元结束后停止”的模式,从而减少生成时出现无边界延续输出的问题。
最后,通过 dataset.map() 对整个数据集批量执行分词操作(当然这里就一条的话其实没关系):
tokenized_dataset = dataset.map(
tokenize_function,
batched=True,
remove_columns=["text"]
)
其中,batched=True 表示按批处理样本,可以提高处理效率;remove_columns=["text"] 则表示在分词完成后删除原始文本字段,只保留模型真正需要的编码结果,例如 input_ids 和 attention_mask 。
执行完成后,数据集会从原来的字符串形式,转变成类似下面这样的结构:
DatasetDict({
train: Dataset({
features: ['input_ids', 'attention_mask'],
num_rows: 1
})
})
这里之所以仍然只有 1 条样本,是因为我们前面写入 jsonl 时,本来就只保存了整篇文本这一条记录。真正的多条训练样本切分,还要在下一步中完成。
固定长度分块
在完成分词之后,虽然文本已经变成了 token 序列,但由于单个样本序列越长,所需的显存资源越多,因此一般不会直接拿一整篇超长文本作为单个样本,而是会把序列再切分成若干段固定长度的小块,让模型逐块学习。因此,接下来我们还需要对分词后的结果进行重新组织。具体代码如下:
block_size = 256
def group_texts(examples):
concatenated = {k: sum(examples[k], []) for k in examples.keys()}
total_length = len(concatenated["input_ids"])
total_length = (total_length // block_size) * block_size
result = {
k: [t[i:i + block_size] for i in range(0, total_length, block_size)]
for k, t in concatenated.items()
}
result["labels"] = result["input_ids"].copy()
return result
lm_dataset = tokenized_dataset.map(group_texts, batched=True)
print(lm_dataset)
tokenized_dataset.save_to_disk("./data/tokenized_dataset")
这里首先定义了 block_size 的值,表示后续每个训练样本都由 256 个 token 组成。当然这个值其实比较小,正常来说训练时候会设置得大一些,但我们这里由于显存有限,因此就选择一个比较小的值进行演示。
接着来看 group_texts() 函数。它的第一步是把当前批次中的所有 token 序列拼接在一起。也就是说,它并不是保留“每条原始文本各自独立”的边界,而是把它们视为一条连续 token 流来处理。这种做法在继续预训练中非常常见,因为目标本质上就是做连续语言建模:
concatenated = {k: sum(examples[k], []) for k in examples.keys()}
然后,通过计算拼接后总 token 长度,并将其截断为 block_size 的整数倍。这样做是为了确保后面切出来的每一块长度都恰好一致,避免末尾剩下一小段不足 256 个 token 的零碎数据:
total_length = len(concatenated["input_ids"])
total_length = (total_length // block_size) * block_size
随后这段代码会把拼接后的长序列,按照每块 256 个 token 的方式切分成多个固定长度的小块。这样处理之后,原本的一条长文本就会被拆成多条适合训练的小样本:
result = {
k: [t[i:i + block_size] for i in range(0, total_length, block_size)]
for k, t in concatenated.items()
}
最后由于继续预训练本质上仍然属于因果语言模型训练,它的目标就是根据前面的 token 去预测后面的 token。因此,训练时的 labels 通常就是 input_ids 本身的拷贝,后续由模型内部自动完成移位计算 loss:
result["labels"] = result["input_ids"].copy()
执行完这一步后,就可以得到最终可用于语言模型训练的数据集:
lm_dataset = tokenized_dataset.map(group_texts, batched=True)
print(lm_dataset)
此时的数据结构通常会变成类似下面这种形式:
DatasetDict({
train: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 50
})
})
这说明原来的整篇文本,已经被转换成了 50 条固定长度的 token 训练样本,并额外补充了 labels 字段,能够直接用于后续的继续预训练流程。
最后我们可以把这部分切分好的数据进行保存,准备后续进一步进行使用:
save_path = "./data/lm_dataset"
lm_dataset.save_to_disk(save_path)
print(f"已保存到:{save_path}")
然后我们可以代码查看一下是否正确保存该部分数据:
from datasets import load_from_disk
lm_dataset = load_from_disk("./data/lm_dataset")
print(lm_dataset)
print("train 条数:", len(lm_dataset["train"]))
print("字段:", lm_dataset["train"].column_names)
print("第一条样本:", lm_dataset["train"][0].keys())
可以看到结果为:
DatasetDict({
train: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 50
})
})
train 条数: 50
字段: ['input_ids', 'attention_mask', 'labels']
第一条样本: dict_keys(['input_ids', 'attention_mask', 'labels'])
数据集划分
在准备好了数据集后,一般情况下我们还需要将该数据集进行训练集和验证集的划分,从而方便后续查看具体的训练情况:
from datasets import load_from_disk, DatasetDict
lm_dataset = load_from_disk("./data/lm_dataset")
# 如果当前只有 train,就再切一个 validation 出来
if"validation"not in lm_dataset:
split = lm_dataset["train"].train_test_split(test_size=0.1, seed=42)
lm_dataset = DatasetDict({
"train": split["train"],
"validation": split["test"]
})
print(lm_dataset)
print("训练集大小:", len(lm_dataset["train"]))
print("验证集大小:", len(lm_dataset["validation"]))
此时就能看到数据集被划分为训练集和验证集了:
DatasetDict({
train: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 45
})
validation: Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 5
})
})
训练集大小: 45
验证集大小: 5
然后我们再把这部分数据集进行保存,后续就可以直接使用该数据集进行训练及验证了:
lm_dataset.save_to_disk("./data/lm_dataset_split")
最终的训练数据如下所示:

模型训练
全量模型训练
在完成数据集准备之后,接下来就可以正式编写训练代码,并基于这份处理好的语言模型数据集来进行继续预训练了。
整体来看,这部分训练流程与之前 SFT 微调时的基本思路是比较接近的,都是围绕“数据集准备—模型加载—训练参数配置—训练与评估”这一主线展开。不过,两者在具体工具的使用上还是存在明显区别。此前进行 SFT 微调时,我们使用的是 SFTTrainer 与 SFTConfig,它们更适合处理监督微调场景下的对话式或指令式数据;而在当前的继续预训练任务中,我们改为使用更底层的 Trainer 与 TrainingArguments。主要原因是这里输入给模型的已经不是“问题—答案”结构的数据,而是预先整理好的连续文本 token 序列,因此训练目标也不再是监督式学习,而是标准的因果语言建模。
该部分完整训练代码如下所示:
import math
import os
import torch
from datasets import load_from_disk
from transformers import (
AutoModelForCausalLM,
Trainer,
TrainingArguments,
default_data_collator,
AutoTokenizer
)
model_path = r"D:/微调与部署/qwen-base"
data_path = r"D:/微调与部署/continue-pre-training/data/lm_dataset_split"
lm_dataset = load_from_disk(data_path)
model = AutoModelForCausalLM.from_pretrained(model_path,trust_remote_code=True,torch_dtype=torch.bfloat16)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
os.environ["TENSORBOARD_LOGGING_DIR"] = "./outputs/qwen_cpt_full/runs"
training_args = TrainingArguments(
#
**一、输出与保存**
output_dir="./outputs/qwen_cpt_full", # 模型、checkpoint、日志等输出目录
#
**二、学习率与调度器**
learning_rate=1e-5, # 初始学习率;继续预训练通常会设得相对保守一些
warmup_steps=0.05, # warmup 占总训练步数的 5%
lr_scheduler_type="cosine", # 学习率调度器使用 cosine 衰减
#
**三、批次与梯度**
per_device_train_batch_size=1, # 每张设备卡每一步只喂 1 条样本(micro-batch)
per_device_eval_batch_size=1, # 评估时每张设备卡每一步也处理 1 条样本
gradient_accumulation_steps=8, # 累积 8 步梯度后再执行一次参数更新
gradient_checkpointing=True, # 开启梯度检查点:节省显存,但训练速度会变慢
gradient_checkpointing_kwargs={"use_reentrant": False},
#
**四、训练轮数**
num_train_epochs=3, # 总共训练 3 个 epoch
#
**五、日志**
logging_strategy="steps", # 按 step 打印日志
logging_steps=1, # 每 1 个 update step 打印一次日志
report_to="tensorboard", # 将训练日志写入 TensorBoard
#
**六、评估**
eval_strategy="steps", # 按 step 做评估,而不是每个 epoch 评估
eval_steps=5, # 每 5 个 update step 做一次验证
#
**七、checkpoint 保存**
save_strategy="steps", # 按 step 保存 checkpoint
save_steps=5, # 每 5 个 update step 保存一次 checkpoint
save_total_limit=2, # 最多保留 2 个 checkpoint,超过会删除较旧的
#
**八、最佳模型回载**
load_best_model_at_end=True, # 训练结束后自动加载验证集上表现最好的 checkpoint
metric_for_best_model="eval_loss", # 以验证损失作为“最好模型”的判断指标
greater_is_better=False, # eval_loss 越小越好
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=lm_dataset["train"],
eval_dataset=lm_dataset["validation"],
data_collator=default_data_collator,
)
trainer.train()
eval_result = trainer.evaluate()
print("eval_loss =", eval_result["eval_loss"])
不过需要注意的是,这里并不需要再额外设置 max_length 之类的参数,因为我们在前面的数据预处理阶段,已经通过固定 block_size 的方式将文本切分成了长度一致的训练样本。因此,当前读入的数据本身就已经可以直接用于语言模型训练。
与此同时,这里我们还显式传入了验证集,并在训练结束后额外调用了一次 trainer.evaluate() 来查看最终的验证损失。这样做的好处在于,我们不仅可以观察训练过程中的 loss 变化,还能够在训练完成后更直观地确认模型在验证集上的整体表现是否正常。
在当前这个教学演示案例中,由于数据规模非常小,因此整个训练过程会很快结束。最终得到的训练结果如下所示:
{'train_runtime': '123.3', 'train_samples_per_second': '1.095', 'train_steps_per_second': '0.146', 'train_loss': '1.896', 'epoch': '3'}
eval_loss = 1.8926546573638916
可以看到,这里的训练损失和验证损失非常接近,说明在这次小规模实验中,模型至少已经顺利完成了继续预训练流程,并且没有出现明显的训练异常。从结果上来说,这更像是一次“流程验证型”的继续预训练实验:它的重点并不在于依靠这几十条样本让模型能力发生显著跃迁,而是在于帮助我们完整理解继续预训练从数据准备、分词分块到正式训练与评估的整个实现过程。
当然,在真实场景中,如果希望继续预训练真正对模型的知识掌握、表达风格或领域适应能力带来较明显的提升,通常还需要规模更大、质量更高、分布更集中的领域语料作为支撑。当前这个例子更适合作为一个入门级演示,帮助大家先把完整流程跑通,再逐步扩展到更真实、更复杂的训练任务中去。
4bit 量化训练
如果希望在继续预训练时进一步降低显存占用,那么相比直接进行全量参数训练,更常见的一种做法是使用 4bit 量化 + LoRA 的方式来完成训练。这种思路其实就是前面提到的 QLoRA。它的核心做法是先把基础模型以 4bit 的形式加载到显存中,尽可能压缩模型本体的显存开销;然后再在模型之上挂载少量可训练的 LoRA 参数,训练时只更新这部分新增参数,而不去直接更新整个量化后的模型权重。
也正因为如此,这种方式更准确的理解并不是“对 4bit 模型做全量训练”,而是在 4bit 量化基座模型上进行 LoRA 式的参数高效训练。它最大的优势就在于,能够大幅降低训练时的显存消耗,因此在单卡或消费级显卡环境下,往往会比全量训练更容易落地。
完整代码如下所示:
import os
import math
import torch
from datasets import load_from_disk
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
Trainer,
TrainingArguments,
default_data_collator,
BitsAndBytesConfig
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training
)
model_path = r"D:/微调与部署/qwen-base"
data_path = r"D:/微调与部署/continue-pre-training/data/lm_dataset_split"
# 1. 读取已经处理好的继续预训练数据集
lm_dataset = load_from_disk(data_path)
# 2. 配置 4bit 量化参数
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
# 3. 加载 4bit 量化模型
model = AutoModelForCausalLM.from_pretrained(
model_path,
trust_remote_code=True,
quantization_config=bnb_config
)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# 4. 让量化模型进入可训练准备状态
model = prepare_model_for_kbit_training(model)
# 5. 配置 LoRA
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear"
)
# 6. 将 LoRA 挂载到量化模型上
model = get_peft_model(model, peft_config)
# 打印可训练参数占比
model.print_trainable_parameters()
os.environ["TENSORBOARD_LOGGING_DIR"] = "./outputs/qwen_cpt_4bit/runs"
training_args = TrainingArguments(
#
**一、输出与保存**
output_dir="./outputs/qwen_cpt_4bit",
#
**二、学习率与调度器**
learning_rate=1e-4,
warmup_steps=0.05,
lr_scheduler_type="cosine",
#
**三、批次与梯度**
per_device_train_batch_size=1,
per_device_eval_batch_size=1,
gradient_accumulation_steps=8,
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
#
**四、训练轮数**
num_train_epochs=3,
#
**五、日志**
logging_strategy="steps",
logging_steps=1,
report_to="tensorboard",
#
**六、评估**
eval_strategy="steps",
eval_steps=5,
#
**七、checkpoint 保存**
save_strategy="steps",
save_steps=5,
save_total_limit=2,
#
**八、最佳模型回载**
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=lm_dataset["train"],
eval_dataset=lm_dataset["validation"],
data_collator=default_data_collator,
)
trainer.train()
eval_result = trainer.evaluate()
print("eval_loss =", eval_result["eval_loss"])
和前面的全量模型训练相比,这段代码最核心的变化主要体现在模型加载与训练对象上。前面我们是直接以较高精度加载基础模型,并对其参数整体进行更新;而在这里,我们首先通过 BitsAndBytesConfig 将基础模型压缩为 4bit 形式,再调用 prepare_model_for_kbit_training() 对量化模型做训练准备,最后只在其上挂载并训练 LoRA 适配器。也就是说,此时真正被优化的并不是整个量化模型,而只是新增的那一小部分低秩参数。PEFT 文档明确将这一步作为 k-bit 训练前的标准准备流程,而 QLoRA 风格训练则进一步建议尽可能将 LoRA 作用到更多线性层上,因此这里使用了 target_modules="all-linear" 的写法,以避免不同模型架构下线性层命名不一致所带来的适配问题。
在训练参数上,这里与全量训练版本也有一个比较明显的区别,就是学习率通常会设得更高一些。TRL 的 PEFT 集成文档专门提到,在使用 LoRA 这类参数高效训练方法时,学习率往往需要比全量微调高一个量级左右,因为此时参与训练的参数数量已经大幅减少,需要更大的学习率来推动这些适配器参数有效更新。因此,这里将学习率设置为 1e-4,而不是前面全量训练中更保守的 1e-5。
当然,需要注意的是,4bit 量化训练虽然在显存占用上会更加友好,但它本身也引入了额外的复杂性。首先,你的环境中需要正确安装并配置好 bitsandbytes;其次,相关硬件也需要满足一定条件。按照 Hugging Face 当前的 bitsandbytes 文档,Windows 平台本身是支持的,而在 NVIDIA GPU 场景下,NF4/FP4 量化至少需要 Pascal 及以上架构的显卡。也就是说,这种方式并不是“任何环境下都能直接替换全量训练代码就跑”的方案,而是建立在量化后端正常可用的前提之上的。
从实际使用角度来看,如果你的目标是先快速跑通继续预训练流程、观察模型在某个领域语料上的适应效果,那么全量训练版本会更直观;而如果你更关注显存开销,希望在有限硬件条件下完成更大模型的训练,那么 4bit 量化 + LoRA 的方式通常会更有实用价值。两者并不是彼此替代关系,而更像是同一条训练思路在不同资源条件下的两种实现路径。
总结
通过本节内容,我们围绕继续预训练(Continued Pretraining)这一主题,从概念理解、典型案例到代码实战,较为完整地梳理了它在大模型训练体系中的位置与基本实现路径。
首先,我们回顾了前面课程中已经讲过的 SFT 微调,并进一步明确了它与继续预训练之间的核心区别。SFT 更擅长调整模型的输出方式、回答风格和行为模式,本质上更偏向于回答“模型应该怎样说”的问题;而继续预训练则更关注在参数层面让模型进一步适应某一类领域文本的语言分布、术语体系和知识组织方式,它要解决的更像是“模型应该更懂什么”的问题。也正因为如此,当我们的目标不只是让模型学会某种回答格式,而是希望它真正补充某一垂直领域中的知识储备时,继续预训练才会成为一个值得考虑的方向。
随后,我们又结合 GLM 5 的训练流程和 MEDITRON 的医疗继续预训练案例,进一步理解了继续预训练在真实工业与研究场景中的实际意义。无论是大模型厂商在基座模型阶段通过多轮预训练、mid-training 不断强化模型能力,还是垂直领域研究工作基于通用基座模型继续注入专业知识,再结合后续的任务微调释放效果,都说明继续预训练并不是一个孤立的训练技巧,而是大模型能力演化过程中非常重要的一环。不过,这些案例也提醒我们,真正有价值的继续预训练,往往建立在大量高质量、领域性强、分布合理的原始语料之上,并且通常还伴随着较高的算力与工程成本。
在代码实战部分,我们则通过一个小规模教学案例,把继续预训练最核心的实现流程完整走了一遍。我们从原始网页文本出发,先整理出适合训练的连续文本语料,再通过分词、拼接和固定长度切块,将其组织成可直接用于因果语言模型训练的数据集;随后又分别演示了全量模型训练与4bit 量化 + LoRA 训练两种实现方式。通过这一过程,可以更清楚地看到:继续预训练在工程上的关键点,不在于数据格式多复杂,而在于是否能够将原始领域文本正确地转换为符合语言建模目标的训练样本,并在合适的模型与资源条件下组织起完整训练链路。
当然,也需要再次强调的是,本节的实战案例更多是一个流程演示型实验。它的重点并不在于依靠少量样本真正训练出一个显著增强领域能力的新模型,而在于帮助大家建立对继续预训练的正确认识:它到底适用于什么场景,输入数据应该是什么形式,训练目标与 SFT 有什么不同,代码实现上又该如何落地。真正想要让继续预训练带来明显、稳定、可验证的能力提升,仍然需要更大规模、更高质量、分布更合理的领域语料,以及后续更加系统的评测与任务适配过程来支撑。
总的来说,继续预训练并不是默认必须采用的一条路线。对于很多实际业务而言,SFT + 提示词工程 + RAG + Agent 往往已经能够解决大部分问题;只有当模型在某一垂类知识上确实存在明显短板,现有手段又难以弥补,并且手中确实拥有足够优质的原始领域数据时,继续预训练才真正具有投入价值。而一旦决定走这条路线,我们也应当清楚它通常并不是训练流程的终点,而更像是为后续 SFT、对齐和任务适配打基础的起点。
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线科技企业深耕十二载,见证过太多因技术卡位而跃迁的案例。那些率先拥抱 AI 的同事,早已在效率与薪资上形成代际优势,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在大模型的学习中的很多困惑。我们整理出这套 AI 大模型突围资料包:
- ✅ 从零到一的 AI 学习路径图
- ✅ 大模型调优实战手册(附医疗/金融等大厂真实案例)
- ✅ 百度/阿里专家闭门录播课
- ✅ 大模型当下最新行业报告
- ✅ 真实大厂面试真题
- ✅ 2026 最新岗位需求图谱
所有资料 ⚡️ ,朋友们如果有需要 《AI大模型入门+进阶学习资源包》,下方扫码获取~

① 全套AI大模型应用开发视频教程
(包含提示工程、RAG、LangChain、Agent、模型微调与部署、DeepSeek等技术点)

② 大模型系统化学习路线
作为学习AI大模型技术的新手,方向至关重要。 正确的学习路线可以为你节省时间,少走弯路;方向不对,努力白费。这里我给大家准备了一份最科学最系统的学习成长路线图和学习规划,带你从零基础入门到精通!

③ 大模型学习书籍&文档
学习AI大模型离不开书籍文档,我精选了一系列大模型技术的书籍和学习文档(电子版),它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础。

④ AI大模型最新行业报告
2025最新行业报告,针对不同行业的现状、趋势、问题、机会等进行系统地调研和评估,以了解哪些行业更适合引入大模型的技术和应用,以及在哪些方面可以发挥大模型的优势。

⑤ 大模型项目实战&配套源码
学以致用,在项目实战中检验和巩固你所学到的知识,同时为你找工作就业和职业发展打下坚实的基础。

⑥ 大模型大厂面试真题
面试不仅是技术的较量,更需要充分的准备。在你已经掌握了大模型技术之后,就需要开始准备面试,我精心整理了一份大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余。

以上资料如何领取?

为什么大家都在学大模型?
最近科技巨头英特尔宣布裁员2万人,传统岗位不断缩减,但AI相关技术岗疯狂扩招,有3-5年经验,大厂薪资就能给到50K*20薪!

不出1年,“有AI项目经验”将成为投递简历的门槛。
风口之下,与其像“温水煮青蛙”一样坐等被行业淘汰,不如先人一步,掌握AI大模型原理+应用技术+项目实操经验,“顺风”翻盘!


这些资料真的有用吗?
这份资料由我和鲁为民博士(北京清华大学学士和美国加州理工学院博士)共同整理,现任上海殷泊信息科技CEO,其创立的MoPaaS云平台获Forrester全球’强劲表现者’认证,服务航天科工、国家电网等1000+企业,以第一作者在IEEE Transactions发表论文50+篇,获NASA JPL火星探测系统强化学习专利等35项中美专利。本套AI大模型课程由清华大学-加州理工双料博士、吴文俊人工智能奖得主鲁为民教授领衔研发。
资料内容涵盖了从入门到进阶的各类视频教程和实战项目,无论你是小白还是有些技术基础的技术人员,这份资料都绝对能帮助你提升薪资待遇,转行大模型岗位。


以上全套大模型资料如何领取?


784

被折叠的 条评论
为什么被折叠?



