06-LLM训练揭秘:预训练、微调、对齐全流程

53 阅读10分钟

LLM训练揭秘:预训练、微调、对齐全流程

深入了解大语言模型的完整训练流程,从预训练到对齐的每个环节。

前言

一个强大的大语言模型是如何诞生的?它经历了哪些训练阶段?每个阶段的目的和方法是什么?

今天,我们将揭开LLM训练的神秘面纱,带你了解从预训练到微调再到对齐的完整流程。


一、训练流程概览

三阶段训练范式

┌─────────────────────────────────────────────────────────────┐
│                    LLM训练全景图                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  阶段一:预训练(Pre-training)                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  海量文本 → 语言模型 → 基础模型(Base Model)         │   │
│  │  目标:学习语言知识和世界知识                          │   │
│  │  数据:TB级别文本                                     │   │
│  │  计算:数千张GPU,数周时间                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                          ↓                                  │
│  阶段二:微调(Fine-tuning)                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  任务数据 → 有监督学习 → 微调模型                      │   │
│  │  目标:适应特定任务或领域                              │   │
│  │  数据:数千到数百万条高质量数据                        │   │
│  │  计算:数十张GPU,数小时到数天                         │   │
│  └─────────────────────────────────────────────────────┘   │
│                          ↓                                  │
│  阶段三:对齐(Alignment)                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  人类反馈 → 强化学习 → 对齐模型                        │   │
│  │  目标:与人类价值观对齐                                │   │
│  │  数据:人类偏好标注                                    │   │
│  │  计算:数百张GPU,数天时间                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、预训练:构建语言知识的基石

核心目标

预训练的目标是让模型学习通用的语言知识和世界知识

预训练学到的能力:

语言层面:
├── 语法规则
├── 词义理解
├── 句子结构
└── 上下文关系

知识层面:
├── 事实知识(巴黎是法国首都)
├── 常识推理(水往低处流)
├── 专业知识(编程、医学、法律)
└── 文化知识(历史、艺术)

数据准备

# 预训练数据的来源和比例

training_data = {
    "网页数据": {
        "比例": "40%",
        "来源": "Common Crawl",
        "处理": "去重、清洗、过滤低质量内容"
    },
    "书籍": {
        "比例": "20%",
        "来源": "BookCorpus、Gutenberg",
        "处理": "提取文本、清理格式"
    },
    "代码": {
        "比例": "15%",
        "来源": "GitHub",
        "处理": "许可证过滤、去重"
    },
    "维基百科": {
        "比例": "10%",
        "来源": "Wikipedia",
        "处理": "提取正文、清理标记"
    },
    "学术论文": {
        "比例": "8%",
        "来源": "arXiv、PubMed",
        "处理": "提取摘要和正文"
    },
    "其他": {
        "比例": "7%",
        "来源": "Reddit、Stack Exchange等",
        "处理": "质量过滤、去重"
    }
}

数据处理流程

def preprocess_training_data(raw_text):
    """预训练数据处理流程"""

    # 1. 文本清洗
    text = clean_text(raw_text)
    # - 去除HTML标签
    # - 统一编码
    # - 处理特殊字符

    # 2. 质量过滤
    if not is_high_quality(text):
        return None
    # - 语言检测
    # - 困惑度过滤
    # - 长度过滤

    # 3. 去重
    if is_duplicate(text, existing_texts):
        return None

    # 4. 分词
    tokens = tokenizer.encode(text)

    # 5. 构建训练样本
    samples = create_samples(tokens, max_length=2048)

    return samples

def is_high_quality(text):
    """质量过滤规则"""
    # 长度检查
    if len(text) < 100:
        return False

    # 语言检查
    if not is_target_language(text):
        return False

    # 困惑度检查(用小模型计算)
    perplexity = calculate_perplexity(text)
    if perplexity > threshold:
        return False

    return True

训练目标

预训练的核心目标是下一个词预测(Next Token Prediction)

import torch
import torch.nn as nn

class PretrainingObjective(nn.Module):
    def __init__(self, model):
        super().__init__()
        self.model = model
        self.loss_fn = nn.CrossEntropyLoss()

    def forward(self, input_ids, labels):
        """
        input_ids: [batch, seq_len] 输入序列
        labels: [batch, seq_len] 目标序列(input_ids左移一位)
        """
        # 模型前向传播
        logits = self.model(input_ids)  # [batch, seq_len, vocab_size]

        # 计算损失
        loss = self.loss_fn(
            logits.view(-1, logits.size(-1)),  # [batch*seq_len, vocab_size]
            labels.view(-1)  # [batch*seq_len]
        )

        return loss

# 训练示例
def pretrain_step(model, batch, optimizer):
    input_ids = batch['input_ids']
    # 目标是预测下一个词
    labels = torch.roll(input_ids, shifts=-1, dims=1)

    # 前向传播
    loss = model(input_ids, labels)

    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    return loss.item()

Scaling Laws

模型的性能与三个因素相关:模型规模、数据量、计算量

性能提升规律:

Loss
  │
  │╲
  │ ╲
  │  ╲
  │   ╲________
  │            ╲______
  │                   ╲________
  └────────────────────────────→ 计算量

关键发现:
1. 性能随规模平滑提升
2. 没有明显的饱和迹象
3. 存在最优的训练计算分配

预训练的计算资源

# 预训练资源估算

def estimate_training_resources(params_count, tokens_count):
    """
    估算训练所需资源

    params_count: 参数量(如70B)
    tokens_count: 训练token数(如2T)
    """
    # 经验法则:每参数需要约20个训练token
    optimal_tokens = params_count * 20

    # FLOPs估算
    # 训练一个token约需要 6 * 参数量 FLOPs
    total_flops = 6 * params_count * tokens_count

    # GPU计算能力(A100: 312 TFLOPS for BF16)
    a100_flops = 312e12

    # 理论计算时间(单卡)
    theoretical_time = total_flops / a100_flops

    # 实际时间(考虑利用率约40%)
    actual_time = theoretical_time / 0.4

    return {
        "总FLOPs": f"{total_flops:.2e}",
        "理论时间(单卡)": f"{theoretical_time/3600:.0f}小时",
        "实际时间(单卡)": f"{actual_time/3600:.0f}小时",
        "建议GPU数量": f"约{int(actual_time / (3600 * 24 * 30))}张A100·月"
    }

# LLaMA-65B的估算
estimate_training_resources(65e9, 1.4e12)

三、微调:适应特定任务

微调的意义

预训练模型虽然学到了丰富的知识,但需要微调来:

  1. 适应特定任务:分类、生成、问答等
  2. 学习特定格式:指令格式、输出格式
  3. 注入领域知识:医疗、法律、金融等

微调方法分类

微调方法:

全量微调(Full Fine-tuning)
├── 更新所有参数
├── 效果最好
└── 资源需求最大

参数高效微调(PEFT)
├── LoRA
│   ├── 只训练低秩分解矩阵
│   └── 原始参数冻结
├── Adapter
│   ├── 在层间插入小模块
│   └── 只训练适配器
├── Prefix Tuning
│   ├── 添加可学习前缀
│   └── 只训练前缀向量
└── QLoRA
    ├── 量化 + LoRA
    └── 进一步降低显存

LoRA详解

LoRA(Low-Rank Adaptation)是目前最流行的PEFT方法:

import torch
import torch.nn as nn

class LoRALayer(nn.Module):
    """
    LoRA: 用低秩分解模拟权重更新

    原始:W = W₀
    LoRA:W = W₀ + BA
    其中 B ∈ R^(d×r), A ∈ R^(r×k), r << min(d, k)
    """
    def __init__(self, in_features, out_features, rank=8, alpha=16):
        super().__init__()

        # 原始权重(冻结)
        self.weight = nn.Parameter(torch.zeros(out_features, in_features))

        # LoRA权重(可训练)
        self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))

        self.scaling = alpha / rank

    def forward(self, x):
        # 原始变换
        result = nn.functional.linear(x, self.weight)

        # LoRA增量
        lora_output = (x @ self.lora_A.T) @ self.lora_B.T * self.scaling

        return result + lora_output

# 应用LoRA到Transformer
def apply_lora_to_model(model, rank=8, target_modules=["q_proj", "v_proj"]):
    """将LoRA应用到指定模块"""
    for name, module in model.named_modules():
        if any(target in name for target in target_modules):
            # 替换为LoRA层
            # 实际实现更复杂,需要保持原始权重
            pass
    return model

指令微调

指令微调(Instruction Tuning)让模型学会遵循指令:

# 指令微调数据格式

instruction_data = [
    {
        "instruction": "将下面的句子翻译成英文",
        "input": "我喜欢学习人工智能",
        "output": "I like learning artificial intelligence"
    },
    {
        "instruction": "总结下面的文章",
        "input": "人工智能是计算机科学的一个分支...",
        "output": "人工智能是研究如何让计算机模拟人类智能的学科。"
    },
    {
        "instruction": "回答问题",
        "input": "中国的首都是哪里?",
        "output": "中国的首都是北京。"
    }
]

# 格式化为模型输入
def format_instruction(sample):
    if sample['input']:
        prompt = f"""### 指令:
{sample['instruction']}

### 输入:
{sample['input']}

### 回答:
{sample['output']}"""
    else:
        prompt = f"""### 指令:
{sample['instruction']}

### 回答:
{sample['output']}"""
    return prompt

微调代码示例

from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model

# 加载模型
model = AutoModelForCausalLM.from_pretrained("base-model")
tokenizer = AutoTokenizer.from_pretrained("base-model")

# LoRA配置
lora_config = LoraConfig(
    r=8,  # 秩
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

# 应用LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.06%

# 训练配置
training_args = TrainingArguments(
    output_dir="./output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    learning_rate=1e-4,
    logging_steps=100,
    save_steps=500,
)

# 训练
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
)

trainer.train()

四、对齐:与人类价值观一致

为什么需要对齐?

预训练和微调后的模型可能存在:

对齐问题:

1. 有害输出
   用户:如何制作炸弹?
   模型:[详细步骤...] ❌

2. 虚假信息
   用户:第一个登上月球的人是谁?
   模型:尼尔·阿姆斯特朗于1969年... ✓
   用户:第一个登上火星的人是谁?
   模型:埃隆·马斯克于2025年... ❌(幻觉)

3. 偏见问题
   模型可能反映训练数据中的偏见

4. 不遵循指令
   用户:用一句话总结
   模型:[写了三段话] ❌

RLHF:人类反馈强化学习

RLHF(Reinforcement Learning from Human Feedback)是主流对齐方法:

RLHF流程:

阶段1:有监督微调(SFT)
┌────────────────────────────────────┐
│  人类编写的高质量指令-回答对         │
│  ↓                                 │
│  有监督训练                         │
└────────────────────────────────────┘

阶段2:奖励模型训练(RM)
┌────────────────────────────────────┐
│  模型对同一指令生成多个回答          │
│  ↓                                 │
│  人类对回答进行排序                  │
│  ↓                                 │
│  训练奖励模型预测人类偏好            │
└────────────────────────────────────┘

阶段3:强化学习优化(PPO)
┌────────────────────────────────────┐
│  用PPO算法优化语言模型              │
│  奖励 = 奖励模型评分 - KL散度惩罚   │
└────────────────────────────────────┘

奖励模型训练

import torch
import torch.nn as nn

class RewardModel(nn.Module):
    def __init__(self, base_model, hidden_size):
        super().__init__()
        self.base_model = base_model
        # 奖励头:将隐藏状态映射到标量
        self.reward_head = nn.Linear(hidden_size, 1)

    def forward(self, input_ids, attention_mask):
        # 获取隐藏状态
        outputs = self.base_model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )
        hidden_states = outputs.last_hidden_state

        # 使用最后一个token的隐藏状态
        last_hidden = hidden_states[:, -1, :]

        # 计算奖励
        reward = self.reward_head(last_hidden)
        return reward

def train_reward_model(reward_model, comparisons, optimizer):
    """
    使用比较数据训练奖励模型

    comparisons: [(prompt, chosen_response, rejected_response), ...]
    """
    total_loss = 0

    for prompt, chosen, rejected in comparisons:
        # 构建输入
        chosen_input = tokenize(prompt + chosen)
        rejected_input = tokenize(prompt + rejected)

        # 计算奖励
        chosen_reward = reward_model(**chosen_input)
        rejected_reward = reward_model(**rejected_input)

        # Bradley-Terry损失
        # 鼓励chosen的奖励高于rejected
        loss = -torch.log(
            torch.sigmoid(chosen_reward - rejected_reward)
        ).mean()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    return total_loss

PPO训练

import torch
from trl import PPOTrainer, PPOConfig

def ppo_training_step(policy_model, reward_model, tokenizer, prompt, ppo_trainer):
    """PPO训练步骤"""

    # 编码输入
    query_tensor = tokenizer.encode(prompt, return_tensors="pt")

    # 生成回答
    response_tensor = policy_model.generate(query_tensor)
    response = tokenizer.decode(response_tensor[0])

    # 计算奖励
    full_text = prompt + response
    reward = reward_model(tokenizer.encode(full_text, return_tensors="pt"))

    # PPO更新
    stats = ppo_trainer.step(
        query_tensor,
        response_tensor,
        reward
    )

    return stats

DPO:直接偏好优化

DPO(Direct Preference Optimization)是RLHF的简化替代方案:

def dpo_loss(policy_model, reference_model, prompt, chosen, rejected, beta=0.1):
    """
    DPO:直接优化偏好,无需训练奖励模型

    核心:增大chosen的概率,减小rejected的概率
    """
    # 计算log概率
    def get_log_prob(model, text):
        inputs = tokenizer(prompt + text, return_tensors="pt")
        outputs = model(**inputs, labels=inputs["input_ids"])
        return -outputs.loss

    # 政策模型的log概率
    policy_chosen_logp = get_log_prob(policy_model, chosen)
    policy_rejected_logp = get_log_prob(policy_model, rejected)

    # 参考模型的log概率(固定)
    with torch.no_grad():
        ref_chosen_logp = get_log_prob(reference_model, chosen)
        ref_rejected_logp = get_log_prob(reference_model, rejected)

    # DPO损失
    chosen_reward = beta * (policy_chosen_logp - ref_chosen_logp)
    rejected_reward = beta * (policy_rejected_logp - ref_rejected_logp)

    loss = -torch.log(
        torch.sigmoid(chosen_reward - rejected_reward)
    ).mean()

    return loss

五、训练资源与成本

各阶段资源对比

训练阶段资源需求对比:

预训练:
├── 数据:TB级别
├── 计算资源:数千张GPU
├── 时间:数周到数月
└── 成本:数百万美元

微调:
├── 数据:数千到数百万条
├── 计算资源:数张到数十张GPU
├── 时间:数小时到数天
└── 成本:数十到数千美元

对齐:
├── 数据:数万条人类偏好
├── 计算资源:数十张GPU
├── 时间:数天
└── 成本:数千到数万美元

训练技巧

# 训练优化技巧

training_tricks = {
    "混合精度训练": {
        "方法": "BF16/FP16",
        "效果": "减少显存,加速训练",
        "代码": "model.to(torch.bfloat16)"
    },

    "梯度累积": {
        "方法": "accumulate_grad_batches",
        "效果": "模拟大batch训练",
        "代码": "gradient_accumulation_steps=4"
    },

    "DeepSpeed/ZeRO": {
        "方法": "分片优化器状态",
        "效果": "降低显存占用",
        "配置": "zero_stage=2"
    },

    "Flash Attention": {
        "方法": "高效注意力计算",
        "效果": "加速2-4倍",
        "使用": "attn_implementation='flash_attention_2'"
    },

    "梯度检查点": {
        "方法": "用计算换显存",
        "效果": "降低30-50%显存",
        "代码": "gradient_checkpointing=True"
    }
}

小结

阶段目标数据方法资源需求
预训练学习语言和世界知识TB级文本Next Token Prediction极高
微调适应特定任务高质量指令数据SFT/PEFT中等
对齐与人类价值观一致人类偏好数据RLHF/DPO中等

思考与练习

  1. 思考题

    • 为什么预训练数据需要去重?
    • LoRA为什么能有效减少训练参数?
  2. 动手练习

    • 使用LoRA微调一个开源模型
    • 实现一个简单的DPO训练流程
  3. 延伸阅读


下期预告

下一篇文章,我们将深入探讨:Prompt Engineering:与大模型对话的艺术

会解答这些问题:

  • 什么是好的Prompt?
  • 有哪些高级的Prompt技巧?
  • 如何系统地优化Prompt?

关注专栏,不错过后续更新!


作者:ECH00O00 本文首发于掘金专栏《AI科普实验室》 欢迎评论区交流讨论,点赞收藏就是最大的鼓励 ❤️