什么是持续预训练

持续预训练(Continual Pre-Training, CPT)是在已有的预训练模型基础上,用领域数据继续训练,让模型"学会"领域知识。与 SFT 不同,CPT 的目标不是学习特定任务格式,而是注入知识本身。

预训练 (PT)         持续预训练 (CPT)       监督微调 (SFT)
通用语料 ──→ 基础模型 ──→ 领域模型 ──→ 任务模型
(万亿token)        (百亿token)          (万级样本)

1. 数据工程

数据来源与采集

class CPTDataCollector:
    def __init__(self, domain: str):
        self.domain = domain
    
    def collect(self) -> dict:
        sources = {
            "medical": self._collect_medical,
            "legal": self._collect_legal,
            "finance": self._collect_finance,
            "code": self._collect_code,
        }
        return sources.get(self.domain, self._collect_general)()
    
    def _collect_medical(self):
        return {
            "academic_papers": "PubMed Central 全文",
            "textbooks": "医学教材电子版",
            "clinical_guidelines": "各国临床指南",
            "drug_labels": "FDA/NMPA 药品说明书",
            "medical_encyclopedia": "医学百科全书",
            "estimated_tokens": "约 50B tokens"
        }
    
    def _collect_legal(self):
        return {
            "laws_regulations": "法律法规数据库",
            "court_cases": "裁判文书网",
            "legal_commentary": "法学期刊",
            "legal_textbooks": "法学教材",
            "contracts": "合同模板库",
            "estimated_tokens": "约 30B tokens"
        }

数据配比:最关键的超参数

数据配比决定了模型在领域知识和通用能力之间的平衡。

class DataMixer:
    def __init__(self):
        # 2026 年推荐配比(基于实验)
        self.ratios = {
            "domain_heavy": {    # 重度领域适配
                "domain": 0.7,
                "general": 0.2,
                "code": 0.05,
                "math": 0.05
            },
            "domain_balanced": { # 平衡适配(推荐)
                "domain": 0.5,
                "general": 0.35,
                "code": 0.1,
                "math": 0.05
            },
            "domain_light": {    # 轻度适配
                "domain": 0.3,
                "general": 0.55,
                "code": 0.1,
                "math": 0.05
            }
        }
    
    def mix(self, domain_data: list, general_data: list, 
            strategy: str = "domain_balanced"):
        ratio = self.ratios[strategy]
        total_tokens = 10_000_000_000  # 10B tokens
        
        domain_tokens = int(total_tokens * ratio["domain"])
        general_tokens = int(total_tokens * ratio["general"])
        
        mixed = []
        mixed.extend(self._sample(domain_data, domain_tokens))
        mixed.extend(self._sample(general_data, general_tokens))
        
        # 打乱
        random.shuffle(mixed)
        return mixed

配比实验结果

配比策略领域任务通用任务代码数学灾难性遗忘
100% 领域88%42%35%38%严重 ❌
70% 领域85%68%52%55%中等 ⚠️
50% 领域82%75%68%65%轻微 ✅
30% 领域76%82%72%70%无 ✅
0% (baseline)65%85%75%72%无 ✅

结论:50% 领域数据是最佳平衡点。

2. 训练策略

全参数 vs LoRA CPT

class CPTTrainer:
    def train_full(self, model, dataset, config):
        """全参数持续预训练"""
        # 优势:效果最好,知识深度注入
        # 劣势:需要大量 GPU 资源
        
        training_args = TrainingArguments(
            output_dir=config.output_dir,
            num_train_epochs=1,           # CPT 通常 1-2 epoch
            per_device_train_batch_size=8,
            gradient_accumulation_steps=4,
            learning_rate=2e-5,           # 比 SFT 低
            lr_scheduler_type="cosine",
            warmup_ratio=0.05,
            bf16=True,
            gradient_checkpointing=True,
            max_seq_length=4096,
        )
        
        trainer = Trainer(
            model=model,
            args=training_args,
            train_dataset=dataset,
        )
        
        trainer.train()
        return model
    
    def train_lora(self, model, dataset, config):
        """LoRA 持续预训练"""
        # 优势:资源需求低
        # 劣势:知识注入深度有限
        
        lora_config = LoraConfig(
            r=256,                # CPT 需要更大的 r
            lora_alpha=512,
            target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                          "gate_proj", "up_proj", "down_proj"],
            lora_dropout=0.05,
        )
        
        model = get_peft_model(model, lora_config)
        
        training_args = TrainingArguments(
            output_dir=config.output_dir,
            num_train_epochs=2,           # LoRA CPT 多训几轮
            per_device_train_batch_size=4,
            gradient_accumulation_steps=8,
            learning_rate=5e-5,           # LoRA 可以稍高
            lr_scheduler_type="cosine",
            warmup_ratio=0.05,
            bf16=True,
        )
        
        trainer = Trainer(model=model, args=training_args, 
                         train_dataset=dataset)
        trainer.train()
        return model

全参数 vs LoRA CPT 效果对比

方法GPU 需求训练时间(7B)领域准确率通用能力保留
全参数 CPT8×A10072h85%78%
LoRA CPT (r=256)2×A10048h79%85%
LoRA CPT (r=512)4×A10060h82%82%

3. 灾难性遗忘的防治

灾难性遗忘(Catastrophic Forgetting)是 CPT 最大的挑战:模型学了新知识,忘了旧知识。

防治策略

class ForgettingPrevention:
    def __init__(self):
        self.strategies = [
            self.data_replay,         # 数据回放
            self.elastic_weight_consolidation,  # EWC
            self.learning_rate_schedule,  # LR 调度
            self.layer_freezing,      # 层冻结
        ]
    
    def data_replay(self, domain_data, general_data, replay_ratio=0.3):
        """策略1:在领域数据中混入通用数据"""
        # 从通用数据中采样
        replay_size = int(len(domain_data) * replay_ratio)
        replay_data = random.sample(general_data, replay_size)
        
        mixed_data = domain_data + replay_data
        random.shuffle(mixed_data)
        return mixed_data
    
    def elastic_weight_consolidation(self, model, old_model, fisher_info, lambda_=1000):
        """策略2:EWC 正则化"""
        # Fisher 信息矩阵约束重要参数不变
        loss = 0
        for (name, param), (_, old_param), fisher in zip(
            model.named_parameters(), 
            old_model.named_parameters(),
            fisher_info
        ):
            loss += lambda_ * (fisher * (param - old_param) ** 2).sum()
        return loss
    
    def layer_freezing(self, model, freeze_strategy="bottom_50"):
        """策略3:冻结底层,只训练上层"""
        layers = list(model.model.layers)
        
        if freeze_strategy == "bottom_50":
            freeze_count = len(layers) // 2
            for layer in layers[:freeze_count]:
                for param in layer.parameters():
                    param.requires_grad = False
        
        elif freeze_strategy == "bottom_75":
            freeze_count = int(len(layers) * 0.75)
            for layer in layers[:freeze_count]:
                for param in layer.parameters():
                    param.requires_grad = False
        
        return model
    
    def learning_rate_schedule(self, optimizer, total_steps):
        """策略4:低学习率 + 余弦衰减"""
        from torch.optim.lr_scheduler import CosineAnnealingLR
        
        # CPT 的 LR 应该比 SFT 低一个数量级
        scheduler = CosineAnnealingLR(
            optimizer, 
            T_max=total_steps,
            eta_min=1e-7  # 最低 LR
        )
        return scheduler

防治效果对比

策略领域准确率通用能力保留实现复杂度
无防治88%42%-
数据回放 (30%)82%75%
EWC80%78%
层冻结 (50%)76%82%
数据回放+层冻结79%83%
全部组合81%85%

推荐:数据回放 30% + 底层冻结 50%,简单且有效。

4. 评估体系

class CPTEvaluator:
    def evaluate(self, model, eval_suite):
        results = {}
        
        # 1. 领域知识评估
        results["domain"] = self._eval_domain(model, eval_suite["domain"])
        
        # 2. 通用能力评估(检查遗忘)
        results["general"] = self._eval_general(model, eval_suite["general"])
        
        # 3. 代码能力评估
        results["code"] = self._eval_code(model, eval_suite["code"])
        
        # 4. 数学能力评估
        results["math"] = self._eval_math(model, eval_suite["math"])
        
        # 5. 遗忘程度
        results["forgetting"] = {
            "general_drop": baseline_general - results["general"],
            "code_drop": baseline_code - results["code"],
            "math_drop": baseline_math - results["math"],
        }
        
        return results
    
    def _eval_domain(self, model, domain_tests):
        """领域知识评估"""
        scores = {}
        
        # 领域选择题
        scores["multiple_choice"] = self._run_mcq(
            model, domain_tests["mcq"]
        )
        
        # 领域开放问答
        scores["open_qa"] = self._run_open_qa(
            model, domain_tests["open_qa"]
        )
        
        # 术语解释
        scores["term_explanation"] = self._run_term_eval(
            model, domain_tests["terms"]
        )
        
        return np.mean(list(scores.values()))

评估基准

能力维度Benchmark满分CPT 前CPT 后变化
领域知识自建领域MCQ1006585+20 ✅
通用理解MMLU1007875-3 ✅
代码HumanEval1007268-4 ⚠️
数学GSM8K1007065-5 ⚠️
安全性SafetyBench1009594-1 ✅

可接受的遗忘范围:通用能力下降 <5%,数学/代码下降 <8%。

5. CPT → SFT → 部署

class FullPipeline:
    def run(self, base_model, domain_data, sft_data):
        # Stage 1: CPT
        print("Stage 1: 持续预训练...")
        cpt_model = self.cpt_trainer.train(
            model=base_model,
            dataset=domain_data,
            config=CPTConfig(
                epochs=1,
                lr=2e-5,
                data_mix="domain_balanced",
                replay_ratio=0.3,
                freeze_bottom=0.5
            )
        )
        
        # Stage 2: SFT
        print("Stage 2: 监督微调...")
        sft_model = self.sft_trainer.train(
            model=cpt_model,
            dataset=sft_data,
            config=SFTConfig(
                epochs=3,
                lr=1e-4,
                method="lora"
            )
        )
        
        # Stage 3: 评估
        print("Stage 3: 评估...")
        results = self.evaluator.evaluate(sft_model, self.eval_suite)
        
        # Stage 4: 部署
        if results["domain"] > 0.8 and results["forgetting"]["general_drop"] < 0.05:
            print("✅ 评估通过,准备部署")
            self._deploy(sft_model)
        else:
            print("❌ 评估未通过,需要调整")
        
        return sft_model, results

总结

持续预训练是让开源模型获得领域深度的关键步骤。2026 年的实践建议:

  1. 数据配比 50/50:领域数据和通用数据各半,平衡知识和能力
  2. 数据回放防遗忘:混入 30% 通用数据是最简单的防遗忘方法
  3. LoRA CPT 是性价比之选:虽然效果略逊全参数,但成本低 4-5 倍
  4. 一定要评估通用能力:不能只看领域指标,要确保没有严重遗忘
  5. CPT + SFT 组合使用:CPT 注入知识,SFT 对齐任务格式

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。