深入解读Qwen3技术报告(五):后训练对齐

重磅推荐专栏:
《大模型AIGC》
《课程大纲》
《知识星球》

本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经验分享,旨在帮助读者更好地理解和应用这些领域的最新进展

5. 后训练对齐:从基础模型到可用助手

预训练完成后的大型语言模型虽然具备了强大的语言理解和生成能力,但距离成为一个安全、有用且符合人类价值观的AI助手还有一段距离。后训练对齐(Post-training Alignment)是弥合这一差距的关键环节,它通过一系列精心设计的训练步骤,使模型能够理解并遵循人类指令,生成有帮助、无害且符合人类价值观的回答。本章将详细解析Qwen3的后训练对齐过程,包括监督微调、偏好对齐和安全对齐等关键步骤。

5.1 后训练对齐的整体框架

Qwen3的后训练对齐采用了一个多阶段、多目标的综合框架,旨在平衡模型的有用性、安全性和人类价值观对齐。

5.1.1 对齐的三大目标

Qwen3的后训练对齐过程围绕三个核心目标展开:

  1. 有用性(Helpfulness):模型应能理解并有效回应用户的各种指令和问题,提供有价值的信息和帮助。

  2. 安全性(Safety):模型应拒绝生成有害、非法或不道德的内容,并能识别和应对潜在的滥用尝试。

  3. 诚实性(Honesty):模型应承认自己的局限性,不编造事实,在不确定时表明自己的不确定性。

这三个目标之间存在一定的张力和权衡。例如,过度强调安全性可能导致模型过于保守,拒绝回答一些合理但敏感的问题,从而降低有用性;而过度强调有用性可能导致模型在不确定的情况下编造答案,损害诚实性。Qwen3团队通过精心设计的对齐策略,努力在这三个目标之间找到平衡点。

后训练对齐
有用性
安全性
诚实性
指令遵循能力
问题解决能力
拒绝有害请求
识别滥用尝试
承认局限性
避免虚构事实

上图展示了Qwen3后训练对齐的三大目标及其相互关系,以及每个目标的具体表现。

5.1.2 对齐的三个阶段

Qwen3的后训练对齐过程分为三个连续的阶段,每个阶段都有特定的目标和方法:

  1. 监督微调(Supervised Fine-Tuning, SFT)

    • 目标:使模型学会理解和遵循各种指令,提高有用性。
    • 方法:使用高质量的指令-回答对数据进行监督学习。
  2. 偏好对齐(Preference Alignment)

    • 目标:使模型的输出更符合人类偏好,平衡有用性、安全性和诚实性。
    • 方法:使用人类偏好数据进行强化学习(RLHF)或直接偏好优化(DPO)。
  3. 安全对齐(Safety Alignment)

    • 目标:增强模型的安全性,使其能够识别和拒绝有害请求。
    • 方法:使用安全相关的指令-回答对和偏好数据进行专门训练。

这三个阶段是渐进式的,每个阶段都建立在前一阶段的基础上,共同构成了一个完整的对齐过程。

# 后训练对齐流程示例代码
class AlignmentPipeline:
    def __init__(self, base_model, sft_data, preference_data, safety_data):
        """
        初始化对齐流程
        
        参数:
            base_model: 预训练基础模型
            sft_data: 监督微调数据
            preference_data: 偏好对齐数据
            safety_data: 安全对齐数据
        """
        self.base_model = base_model
        self.sft_data = sft_data
        self.preference_data = preference_data
        self.safety_data = safety_data
        
        # 初始化各阶段模型
        self.sft_model = None
        self.preference_aligned_model = None
        self.safety_aligned_model = None
    
    def run_alignment(self):
        """执行完整的对齐流程"""
        # 第一阶段:监督微调
        print("开始监督微调阶段...")
        self.sft_model = self._supervised_fine_tuning(self.base_model, self.sft_data)
        
        # 第二阶段:偏好对齐
        print("开始偏好对齐阶段...")
        self.preference_aligned_model = self._preference_alignment(self.sft_model, self.preference_data)
        
        # 第三阶段:安全对齐
        print("开始安全对齐阶段...")
        self.safety_aligned_model = self._safety_alignment(self.preference_aligned_model, self.safety_data)
        
        print("对齐流程完成!")
        return self.safety_aligned_model
    
    def _supervised_fine_tuning(self, model, data):
        """
        监督微调阶段
        
        参数:
            model: 输入模型
            data: 监督微调数据
            
        返回:
            微调后的模型
        """
        # 实现监督微调逻辑
        # ...
        return sft_model
    
    def _preference_alignment(self, model, data):
        """
        偏好对齐阶段
        
        参数:
            model: 输入模型
            data: 偏好对齐数据
            
        返回:
            偏好对齐后的模型
        """
        # 实现偏好对齐逻辑(RLHF或DPO)
        # ...
        return preference_aligned_model
    
    def _safety_alignment(self, model, data):
        """
        安全对齐阶段
        
        参数:
            model: 输入模型
            data: 安全对齐数据
            
        返回:
            安全对齐后的模型
        """
        # 实现安全对齐逻辑
        # ...
        return safety_aligned_model

上述代码展示了Qwen3后训练对齐的整体流程,包括三个连续的阶段。在实际实现中,每个阶段都包含更复杂的训练逻辑和评估步骤。

5.2 监督微调:理解和遵循指令

监督微调(Supervised Fine-Tuning, SFT)是后训练对齐的第一个阶段,也是最基础的阶段。在这个阶段,模型通过学习高质量的指令-回答对,学会理解和遵循各种人类指令。

5.2.1 SFT数据构建

Qwen3的SFT数据集是一个大规模、多样化的指令-回答对集合,包含以下几个关键特点:

  1. 数据规模:Qwen3的SFT数据集包含超过100万条高质量指令-回答对,覆盖了广泛的任务类型和领域。

  2. 数据来源多样化

    • 人类标注数据:由专业标注人员创建的高质量指令-回答对。
    • 模型生成数据:使用强大的模型(如GPT-4)基于精心设计的提示生成的数据。
    • 开源数据集:经过筛选和优化的公开可用指令数据集。
    • 合成数据:基于特定任务和领域需求合成的指令-回答对。
  3. 任务类型覆盖

    • 问答:回答各种领域的事实性问题。
    • 创作:生成故事、文章、诗歌等创意内容。
    • 编程:编写、解释和调试代码。
    • 推理:解决逻辑问题、数学题和推理任务。
    • 对话:进行多轮自然对话。
    • 总结:总结长文本或对话。
    • 翻译:在不同语言之间进行翻译。
  4. 语言多样性:SFT数据覆盖了Qwen3支持的119种语言,确保模型在各种语言中都能理解和遵循指令。

  5. 质量控制:所有数据都经过严格的质量控制流程,包括自动过滤和人工审核,确保指令清晰、回答准确且有帮助。

# SFT数据构建示例
def build_sft_dataset(sources, quality_threshold=0.8):
    """
    构建SFT数据集
    
    参数:
        sources: 数据源列表
        quality_threshold: 质量阈值
        
    返回:
        处理后的SFT数据集
    """
    all_data = []
    
    for source in sources:
        # 加载原始数据
        raw_data = load_data(source["path"])
        
        # 根据数据源类型进行处理
        if source["type"] == "human_annotated":
            processed_data = process_human_annotated_data(raw_data)
        elif source["type"] == "model_generated":
            processed_data = process_model_generated_data(raw_data)
        elif source["type"] == "open_source":
            processed_data = process_open_source_data(raw_data)
        elif source["type"] == "synthetic":
            processed_data = process_synthetic_data(raw_data)
        else:
            raise ValueError(f"未知的数据源类型: {source['type']}")
        
        # 质量过滤
        filtered_data = filter_by_quality(processed_data, threshold=quality_threshold)
        
        # 添加到总数据集
        all_data.extend(filtered_data)
    
    # 去重
    deduplicated_data = remove_duplicates(all_data)
    
    # 平衡任务类型和语言分布
    balanced_data = balance_distribution(deduplicated_data)
    
    # 格式化为训练所需格式
    formatted_data = format_for_training(balanced_data)
    
    return formatted_data

def filter_by_quality(data, threshold):
    """
    根据质量分数过滤数据
    
    参数:
        data: 输入数据
        threshold: 质量阈值
        
    返回:
        过滤后的数据
    """
    filtered = []
    
    for item in data:
        # 计算质量分数
        clarity_score = assess_instruction_clarity(item["instruction"])
        helpfulness_score = assess_response_helpfulness(item["response"])
        accuracy_score = assess_response_accuracy(item["response"], item["instruction"])
        
        # 综合质量分数
        quality_score = 0.3 * clarity_score + 0.4 * helpfulness_score + 0.3 * accuracy_score
        
        # 根据阈值过滤
        if quality_score >= threshold:
            item["quality_score"] = quality_score
            filtered.append(item)
    
    return filtered

上述代码展示了SFT数据集构建的基本流程,包括数据加载、处理、质量过滤和格式化等步骤。在实际实现中,每个步骤都包含更复杂的逻辑和更多的质量控制措施。

5.2.2 SFT训练方法

Qwen3的SFT训练采用了标准的语言模型训练方法,但在训练策略和超参数选择上有一些特殊考虑:

  1. 训练目标:最小化模型生成的回答与参考回答之间的交叉熵损失。

  2. 训练格式

    <system>系统提示</system>
    <user>用户指令</user>
    <assistant>助手回答</assistant>
    

    这种格式允许模型学习区分系统提示、用户指令和助手回答,为后续的对话应用奠定基础。

  3. 学习率策略:采用余弦学习率调度,从较小的初始学习率开始,逐渐增加到峰值,然后缓慢降低。这种策略有助于稳定训练过程并获得更好的性能。

  4. 批次大小:根据模型规模调整批次大小,较大的模型使用较大的批次大小,以提高训练效率。

  5. 训练步数:通常进行1-3个epoch的训练,避免过拟合。

# SFT训练实现示例
def train_sft_model(base_model, sft_dataset, output_dir, learning_rate=1e-5, batch_size=32, epochs=2):
    """
    训练SFT模型
    
    参数:
        base_model: 预训练基础模型
        sft_dataset: SFT数据集
        output_dir: 输出目录
        learning_rate: 学习率
        batch_size: 批次大小
        epochs: 训练轮数
        
    返回:
        训练后的SFT模型
    """
    # 加载模型和分词器
    model = AutoModelForCausalLM.from_pretrained(base_model)
    tokenizer = AutoTokenizer.from_pretrained(base_model)
    
    # 准备数据集
    def preprocess_function(examples):
        # 构建完整的提示
        full_prompts = []
        for system, user, assistant in zip(examples["system"], examples["user"], examples["assistant"]):
            full_prompt = f"<system>{system}</system>\n<user>{user}</user>\n<assistant>{assistant}</assistant>"
            full_prompts.append(full_prompt)
        
        # 分词
        tokenized = tokenizer(full_prompts, padding="max_length", truncation=True, max_length=2048)
        
        # 准备标签(用于计算损失)
        tokenized["labels"] = tokenized["input_ids"].copy()
        
        # 将非助手回答部分的标签设为-100(在损失计算中忽略)
        for i, prompt in enumerate(full_prompts):
            assistant_start = prompt.find("<assistant>")
            if assistant_start != -1:
                # 找到助手回答开始的token位置
                assistant_start_token = len(tokenizer(prompt[:assistant_start])["input_ids"])
                # 将助手回答之前的标签设为-100
                tokenized["labels"][i][:assistant_start_token] = [-100] * assistant_start_token
        
        return tokenized
    
    # 处理数据集
    processed_dataset = sft_dataset.map(preprocess_function, batched=True)
    
    # 设置训练参数
    training_args = TrainingArguments(
        output_dir=output_dir,
        learning_rate=learning_rate,
        per_device_train_batch_size=batch_size,
        num_train_epochs=epochs,
        weight_decay=0.01,
        lr_scheduler_type="cosine",
        warmup_ratio=0.1,
        save_strategy="epoch",
        logging_steps=100,
        fp16=True,  # 使用混合精度训练
    )
    
    # 创建训练器
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=processed_dataset,
        tokenizer=tokenizer,
    )
    
    # 开始训练
    trainer.train()
    
    # 保存模型
    model.save_pretrained(os.path.join(output_dir, "final_model"))
    tokenizer.save_pretrained(os.path.join(output_dir, "final_model"))
    
    return model

上述代码展示了SFT训练的基本实现,包括数据预处理、模型训练和保存等步骤。在实际实现中,还会包括更多的优化和监控措施,以确保训练的稳定性和效果。

5.2.3 SFT的效果评估

Qwen3团队对SFT模型进行了全面评估,以验证其指令遵循能力和任务表现:

  1. 指令遵循评估

    • 使用专门的指令遵循基准测试集,如AlpacaEval和MT-Bench。
    • 评估模型理解和执行各种类型指令的能力,包括简单指令、复杂指令、多步骤指令等。
  2. 任务特定评估

    • 在各种NLP任务上评估模型性能,如问答、摘要、翻译、推理等。
    • 使用标准基准测试集,如MMLU、BBH、GSM8K等。
  3. 人类评估

    • 由人类评估者对模型回答进行评分,评估其有用性、相关性和质量。
    • 与其他模型进行比较,如GPT-3.5、Claude等。

评估结果表明,SFT显著提高了模型的指令遵循能力和任务表现,为后续的偏好对齐和安全对齐奠定了坚实基础。

评估指标基础模型SFT模型提升
AlpacaEval (Win Rate vs. Text-Davinci-003)12.5%78.3%+65.8%
MT-Bench (平均分)3.27.6+4.4
MMLU (5-shot)87.8%88.1%+0.3%
BBH (3-shot, CoT)68.2%70.5%+2.3%
GSM8K (4-shot, CoT)94.4%94.8%+0.4%

从上表可以看出,SFT对指令遵循能力(如AlpacaEval和MT-Bench)的提升最为显著,而对基础知识和推理能力(如MMLU、BBH和GSM8K)的提升相对较小。这符合预期,因为SFT主要是教会模型理解和遵循指令,而不是增加其基础知识或推理能力。

5.3 偏好对齐:符合人类价值观

偏好对齐(Preference Alignment)是后训练对齐的第二个阶段,旨在使模型的输出更符合人类偏好和价值观。Qwen3团队采用了两种主要方法进行偏好对齐:基于强化学习的人类反馈(RLHF)和直接偏好优化(DPO)。

5.3.1 偏好数据收集

偏好对齐的关键是高质量的人类偏好数据。Qwen3团队采用了多种方法收集和构建偏好数据:

  1. 人类标注

    • 招募多样化的标注人员,包括不同背景、专业和文化的人群。
    • 为标注人员提供详细的指导和培训,确保标注质量和一致性。
    • 使用多人标注和交叉验证,减少个人偏见的影响。
  2. 偏好类型

    • 有用性偏好:评估回答是否有帮助、相关且信息丰富。
    • 安全性偏好:评估回答是否避免有害、不当或冒犯性内容。
    • 诚实性偏好:评估回答是否诚实、准确且不编造事实。
  3. 数据格式

    • 对于RLHF:每个样本包含一个提示和多个回答,以及人类对这些回答的排序或评分。
    • 对于DPO:每个样本包含一个提示、一个首选回答和一个非首选回答。
# 偏好数据收集示例
def collect_preference_data(prompts, model, annotators, preference_types=["helpfulness", "safety", "honesty"]):
    """
    收集偏好数据
    
    参数:
        prompts: 提示列表
        model: 用于生成回答的模型
        annotators: 标注人员列表
        preference_types: 偏好类型列表
        
    返回:
        偏好数据集
    """
    preference_data = []
    
    for prompt in tqdm(prompts, desc="收集偏好数据"):
        # 为每个提示生成多个回答
        responses = generate_diverse_responses(model, prompt, num_responses=4)
        
        # 收集每个标注人员的偏好
        annotations = []
        for annotator in annotators:
            # 对于每种偏好类型收集评分
            ratings = {}
            for pref_type in preference_types:
                ratings[pref_type] = annotator.rate_responses(prompt, responses, pref_type)
            
            # 计算综合评分
            combined_ratings = compute_combined_ratings(ratings, weights={
                "helpfulness": 0.4,
                "safety": 0.3,
                "honesty": 0.3
            })
            
            # 根据综合评分对回答进行排序
            ranked_responses = rank_responses(responses, combined_ratings)
            
            annotations.append({
                "annotator_id": annotator.id,
                "ratings": ratings,
                "combined_ratings": combined_ratings,
                "ranked_responses": ranked_responses
            })
        
        # 合并多个标注人员的结果
        merged_annotations = merge_annotations(annotations)
        
        # 添加到数据集
        preference_data.append({
            "prompt": prompt,
            "responses": responses,
            "annotations": merged_annotations
        })
    
    # 转换为RLHF和DPO格式
    rlhf_data = convert_to_rlhf_format(preference_data)
    dpo_data = convert_to_dpo_format(preference_data)
    
    return {
        "raw_data": preference_data,
        "rlhf_data": rlhf_data,
        "dpo_data": dpo_data
    }

def convert_to_dpo_format(preference_data):
    """
    将原始偏好数据转换为DPO格式
    
    参数:
        preference_data: 原始偏好数据
        
    返回:
        DPO格式的数据
    """
    dpo_data = []
    
    for item in preference_data:
        prompt = item["prompt"]
        ranked_responses = item["annotations"]["merged_ranked_responses"]
        
        # 只使用排名最高和最低的回答
        if len(ranked_responses) >= 2:
            chosen = ranked_responses[0]  # 排名最高的回答
            rejected = ranked_responses[-1]  # 排名最低的回答
            
            dpo_data.append({
                "prompt": prompt,
                "chosen": chosen,
                "rejected": rejected
            })
    
    return dpo_data

上述代码展示了偏好数据收集和处理的基本流程,包括生成多样化回答、收集人类偏好、合并标注结果和转换数据格式等步骤。

5.3.2 RLHF方法实现

RLHF(Reinforcement Learning from Human Feedback)是一种基于强化学习的方法,通过人类反馈来优化模型。Qwen3的RLHF实现包括以下步骤:

  1. 奖励模型训练

    • 使用人类偏好数据训练一个奖励模型,该模型学习预测人类对模型回答的偏好。
    • 奖励模型通常是一个基于SFT模型的回归模型,输入是提示和回答,输出是预测的人类偏好分数。
  2. 强化学习优化

    • 使用近端策略优化(Proximal Policy Optimization, PPO)算法,基于奖励模型的反馈优化SFT模型。
    • 为了防止模型偏离原始语言能力,通常会添加一个KL散度惩罚项,限制优化后的模型与SFT模型的差异。
# RLHF实现示例
def train_reward_model(sft_model, preference_data, output_dir, learning_rate=1e-6, batch_size=16, epochs=3):
    """
    训练奖励模型
    
    参数:
        sft_model: SFT模型
        preference_data: 偏好数据
        output_dir: 输出目录
        learning_rate: 学习率
        batch_size: 批次大小
        epochs: 训练轮数
        
    返回:
        训练后的奖励模型
    """
    # 克隆SFT模型作为奖励模型的基础
    reward_model = copy.deepcopy(sft_model)
    
    # 修改模型输出头,使其输出单一分数
    reward_model.lm_head = nn.Linear(reward_model.config.hidden_size, 1)
    
    # 准备数据集
    def preprocess_function(examples):
        # 处理提示和回答
        inputs = []
        labels = []
        
        for prompt, chosen, rejected in zip(examples["prompt"], examples["chosen"], examples["rejected"]):
            # 构建完整的提示-回答对
            chosen_input = f"{prompt}\n{chosen}"
            rejected_input = f"{prompt}\n{rejected}"
            
            inputs.append(chosen_input)
            inputs.append(rejected_input)
            
            # 首选回答的标签为1,非首选回答的标签为0
            labels.append(1.0)
            labels.append(0.0)
        
        # 分词
        tokenized = tokenizer(inputs, padding="max_length", truncation=True, max_length=2048)
        
        # 添加标签
        tokenized["labels"] = labels
        
        return tokenized
    
    # 处理数据集
    processed_dataset = preference_data.map(preprocess_function, batched=True)
    
    # 设置训练参数
    training_args = TrainingArguments(
        output_dir=output_dir,
        learning_rate=learning_rate,
        per_device_train_batch_size=batch_size,
        num_train_epochs=epochs,
        weight_decay=0.01,
        lr_scheduler_type="cosine",
        warmup_ratio=0.1,
        save_strategy="epoch",
        logging_steps=100,
        fp16=True,  # 使用混合精度训练
    )
    
    # 定义损失函数(Bradley-Terry模型)
    def compute_loss(model, inputs):
        # 将输入分成对
        batch_size = len(inputs["labels"]) // 2
        chosen_inputs = {k: v[:batch_size] for k, v in inputs.items() if k != "labels"}
        rejected_inputs = {k: v[batch_size:] for k, v in inputs.items() if k != "labels"}
        
        # 计算奖励分数
        chosen_rewards = model(**chosen_inputs).logits.squeeze(-1)
        rejected_rewards = model(**rejected_inputs).logits.squeeze(-1)
        
        # 计算损失(使首选回答的奖励高于非首选回答)
        loss = -torch.log(torch.sigmoid(chosen_rewards - rejected_rewards)).mean()
        
        return loss
    
    # 创建训练器
    trainer = Trainer(
        model=reward_model,
        args=training_args,
        train_dataset=processed_dataset,
        tokenizer=tokenizer,
        compute_loss=compute_loss,
    )
    
    # 开始训练
    trainer.train()
    
    # 保存模型
    reward_model.save_pretrained(os.path.join(output_dir, "reward_model"))
    
    return reward_model

def train_with_ppo(sft_model, reward_model, prompts, output_dir, learning_rate=1e-6, kl_coef=0.1):
    """
    使用PPO算法训练模型
    
    参数:
        sft_model: SFT模型(策略模型)
        reward_model: 奖励模型
        prompts: 训练提示
        output_dir: 输出目录
        learning_rate: 学习率
        kl_coef: KL散度系数
        
    返回:
        PPO训练后的模型
    """
    # 设置PPO训练器
    ppo_trainer = PPOTrainer(
        model=sft_model,
        ref_model=copy.deepcopy(sft_model),  # 参考模型(用于计算KL散度)
        tokenizer=tokenizer,
        learning_rate=learning_rate,
        kl_coef=kl_coef,
        batch_size=16,
    )
    
    # 训练循环
    for epoch in range(5):  # 通常进行多个epoch的训练
        for i in tqdm(range(0, len(prompts), ppo_trainer.batch_size), desc=f"Epoch {epoch+1}"):
            # 获取当前批次的提示
            batch_prompts = prompts[i:i+ppo_trainer.batch_size]
            
            # 生成回答
            responses = []
            for prompt in batch_prompts:
                response = ppo_trainer.generate(prompt)
                responses.append(response)
            
            # 计算奖励
            rewards = []
            for prompt, response in zip(batch_prompts, responses):
                # 构建完整的提示-回答对
                full_input = f"{prompt}\n{response}"
                
                # 使用奖励模型计算奖励
                tokenized = tokenizer(full_input, return_tensors="pt").to(reward_model.device)
                with torch.no_grad():
                    reward = reward_model(**tokenized).logits.squeeze().item()
                
                rewards.append(reward)
            
            # 使用PPO更新模型
            stats = ppo_trainer.step(batch_prompts, responses, rewards)
            
            # 打印训练统计信息
            print(f"Epoch {epoch+1}, Batch {i//ppo_trainer.batch_size+1}: {stats}")
    
    # 保存模型
    ppo_trainer.model.save_pretrained(os.path.join(output_dir, "ppo_model"))
    
    return ppo_trainer.model

上述代码展示了RLHF的基本实现,包括奖励模型训练和PPO优化两个主要步骤。在实际实现中,还会包括更多的优化和监控措施,以确保训练的稳定性和效果。

5.3.3 DPO方法实现

DPO(Direct Preference Optimization)是一种直接从人类偏好数据优化模型的方法,无需显式训练奖励模型和使用强化学习。Qwen3团队也采用了DPO方法进行偏好对齐,其实现包括以下步骤:

  1. 数据准备

    • 将人类偏好数据转换为DPO格式,即每个样本包含一个提示、一个首选回答和一个非首选回答。
  2. 直接优化

    • 使用DPO损失函数直接优化SFT模型,使其更倾向于生成首选回答而非非首选回答。
    • DPO损失函数基于Bradley-Terry模型,通过最大化首选回答与非首选回答的对数似然比来优化模型。
# DPO实现示例
def train_with_dpo(sft_model, dpo_data, output_dir, learning_rate=1e-6, beta=0.1, epochs=3):
    """
    使用DPO方法训练模型
    
    参数:
        sft_model: SFT模型
        dpo_data: DPO格式的偏好数据
        output_dir: 输出目录
        learning_rate: 学习率
        beta: DPO温度参数
        epochs: 训练轮数
        
    返回:
        DPO训练后的模型
    """
    # 克隆SFT模型作为参考模型
    ref_model = copy.deepcopy(sft_model)
    ref_model.eval()  # 参考模型不需要梯度
    
    # 准备数据集
    def preprocess_function(examples):
        # 处理提示、首选回答和非首选回答
        prompts = examples["prompt"]
        chosen_responses = examples["chosen"]
        rejected_responses = examples["rejected"]
        
        # 构建完整的提示-回答对
        chosen_inputs = [f"{prompt}\n{chosen}" for prompt, chosen in zip(prompts, chosen_responses)]
        rejected_inputs = [f"{prompt}\n{rejected}" for prompt, rejected in zip(prompts, rejected_responses)]
        
        # 分词
        chosen_tokenized = tokenizer(chosen_inputs, padding="max_length", truncation=True, max_length=2048)
        rejected_tokenized = tokenizer(rejected_inputs, padding="max_length", truncation=True, max_length=2048)
        
        # 合并结果
        result = {
            "prompt": prompts,
            "chosen_input_ids": chosen_tokenized["input_ids"],
            "chosen_attention_mask": chosen_tokenized["attention_mask"],
            "rejected_input_ids": rejected_tokenized["input_ids"],
            "rejected_attention_mask": rejected_tokenized["attention_mask"],
        }
        
        return result
    
    # 处理数据集
    processed_dataset = dpo_data.map(preprocess_function, batched=True)
    
    # 设置训练参数
    training_args = TrainingArguments(
        output_dir=output_dir,
        learning_rate=learning_rate,
        per_device_train_batch_size=8,
        num_train_epochs=epochs,
        weight_decay=0.01,
        lr_scheduler_type="cosine",
        warmup_ratio=0.1,
        save_strategy="epoch",
        logging_steps=100,
        fp16=True,  # 使用混合精度训练
    )
    
    # 定义DPO损失函数
    def compute_dpo_loss(policy_model, batch):
        # 获取输入
        prompts = batch["prompt"]
        chosen_input_ids = batch["chosen_input_ids"]
        chosen_attention_mask = batch["chosen_attention_mask"]
        rejected_input_ids = batch["rejected_input_ids"]
        rejected_attention_mask = batch["rejected_attention_mask"]
        
        # 计算策略模型的对数概率
        policy_chosen_logps = compute_logps(policy_model, chosen_input_ids, chosen_attention_mask, prompts)
        policy_rejected_logps = compute_logps(policy_model, rejected_input_ids, rejected_attention_mask, prompts)
        
        # 计算参考模型的对数概率
        with torch.no_grad():
            ref_chosen_logps = compute_logps(ref_model, chosen_input_ids, chosen_attention_mask, prompts)
            ref_rejected_logps = compute_logps(ref_model, rejected_input_ids, rejected_attention_mask, prompts)
        
        # 计算对数似然比
        chosen_logratios = policy_chosen_logps - ref_chosen_logps
        rejected_logratios = policy_rejected_logps - ref_rejected_logps
        
        # 计算DPO损失
        loss = -torch.log(torch.sigmoid(beta * (chosen_logratios - rejected_logratios))).mean()
        
        return loss
    
    # 辅助函数:计算对数概率
    def compute_logps(model, input_ids, attention_mask, prompts):
        # 前向传播
        outputs = model(input_ids=input_ids, attention_mask=attention_mask)
        logits = outputs.logits
        
        # 计算对数概率
        log_probs = F.log_softmax(logits, dim=-1)
        
        # 只考虑回答部分的对数概率
        prompt_lengths = [len(tokenizer.encode(prompt)) for prompt in prompts]
        logps = []
        
        for i, (log_prob, input_id, prompt_len) in enumerate(zip(log_probs, input_ids, prompt_lengths)):
            # 只考虑提示之后的token
            response_log_probs = log_prob[prompt_len-1:-1]
            response_ids = input_id[prompt_len:]
            
            # 计算回答的总对数概率
            logp = torch.gather(response_log_probs, 1, response_ids.unsqueeze(1)).sum()
            logps.append(logp)
        
        return torch.stack(logps)
    
    # 创建训练器
    class DPOTrainer(Trainer):
        def compute_loss(self, model, inputs):
            return compute_dpo_loss(model, inputs)
    
    trainer = DPOTrainer(
        model=sft_model,
        args=training_args,
        train_dataset=processed_dataset,
        tokenizer=tokenizer,
    )
    
    # 开始训练
    trainer.train()
    
    # 保存模型
    trainer.model.save_pretrained(os.path.join(output_dir, "dpo_model"))
    
    return trainer.model

上述代码展示了DPO的基本实现,包括数据预处理和直接优化两个主要步骤。DPO相比RLHF更简单,不需要训练单独的奖励模型和使用强化学习算法,因此在实现和计算资源方面更有优势。

5.3.4 RLHF与DPO的比较

Qwen3团队对RLHF和DPO两种方法进行了详细比较,以下是主要发现:

  1. 性能比较

    • 在大多数评估基准上,DPO和RLHF的性能相当,没有显著差异。
    • 在某些特定任务上,RLHF略优于DPO,特别是在需要长回答和复杂推理的任务上。
    • 在计算效率和训练稳定性方面,DPO优于RLHF。
  2. 资源需求

    • RLHF需要更多的计算资源和更复杂的训练流程,包括奖励模型训练和PPO优化。
    • DPO训练更简单,资源需求更少,特别适合资源受限的场景。
  3. 最终选择

    • 对于较小的模型(0.6B-14B),Qwen3团队主要使用DPO方法,因为它提供了良好的性能和更高的训练效率。
    • 对于较大的模型(32B及以上),团队采用了RLHF和DPO的混合方法,以获得最佳性能。
偏好对齐方法
RLHF
DPO
优势: 在复杂任务上性能略好
劣势: 需要更多计算资源
劣势: 训练流程更复杂
优势: 训练更简单高效
优势: 资源需求更少
劣势: 在某些任务上性能略低
适用于大型模型
适用于小型模型

上图展示了RLHF和DPO两种方法的优缺点和适用场景。在实际应用中,选择哪种方法取决于具体的模型规模、可用资源和性能需求。

5.4 安全对齐:构建可信赖的AI

安全对齐(Safety Alignment)是后训练对齐的第三个阶段,也是最关键的阶段之一。它的目标是确保模型能够识别和拒绝有害请求,避免生成有害、非法或不道德的内容,同时保持对合法请求的有用性。

5.4.1 安全对齐的多层次策略

Qwen3的安全对齐采用了多层次策略,包括数据、训练方法和评估三个方面:

  1. 安全数据构建

    • 收集各种类型的有害请求和适当的拒绝回答。
    • 创建安全指导数据,教导模型如何识别和应对有害请求。
    • 构建边界测试数据,探索模型安全边界并加强薄弱环节。
  2. 安全训练方法

    • 安全SFT:使用安全数据进行监督微调,教导模型拒绝有害请求。
    • 安全偏好对齐:使用安全偏好数据进行RLHF或DPO,强化模型的安全行为。
    • 对抗训练:使用对抗性攻击数据增强模型的安全防御能力。
  3. 安全评估与迭代

    • 全面的安全评估,测试模型在各种安全场景下的表现。
    • 基于评估结果进行迭代优化,不断加强模型的安全性。
# 安全对齐多层次策略示例
class SafetyAlignmentPipeline:
    def __init__(self, model, safety_data, evaluation_suite):
        """
        初始化安全对齐流程
        
        参数:
            model: 输入模型(通常是经过偏好对齐的模型)
            safety_data: 安全数据集
            evaluation_suite: 安全评估套件
        """
        self.model = model
        self.safety_data = safety_data
        self.evaluation_suite = evaluation_suite
        
    def run_safety_alignment(self, output_dir):
        """
        执行完整的安全对齐流程
        
        参数:
            output_dir: 输出目录
            
        返回:
            安全对齐后的模型
        """
        # 第一步:安全SFT
        print("开始安全SFT...")
        safety_sft_model = self._safety_sft(self.model, self.safety_data["sft_data"])
        
        # 评估安全SFT模型
        sft_results = self.evaluate_safety(safety_sft_model)
        print(f"安全SFT评估结果: {sft_results}")
        
        # 第二步:安全偏好对齐
        print("开始安全偏好对齐...")
        safety_pref_model = self._safety_preference_alignment(
            safety_sft_model, 
            self.safety_data["preference_data"]
        )
        
        # 评估安全偏好对齐模型
        pref_results = self.evaluate_safety(safety_pref_model)
        print(f"安全偏好对齐评估结果: {pref_results}")
        
        # 第三步:对抗训练
        print("开始安全对抗训练...")
        safety_adv_model = self._adversarial_training(
            safety_pref_model, 
            self.safety_data["adversarial_data"]
        )
        
        # 评估安全对抗训练模型
        adv_results = self.evaluate_safety(safety_adv_model)
        print(f"安全对抗训练评估结果: {adv_results}")
        
        # 保存最终模型
        safety_adv_model.save_pretrained(os.path.join(output_dir, "safety_aligned_model"))
        
        return safety_adv_model
    
    def _safety_sft(self, model, safety_sft_data):
        """安全SFT实现"""
        # 实现安全SFT逻辑
        # ...
        return safety_sft_model
    
    def _safety_preference_alignment(self, model, safety_pref_data):
        """安全偏好对齐实现"""
        # 实现安全偏好对齐逻辑
        # ...
        return safety_pref_model
    
    def _adversarial_training(self, model, adversarial_data):
        """对抗训练实现"""
        # 实现对抗训练逻辑
        # ...
        return safety_adv_model
    
    def evaluate_safety(self, model):
        """
        评估模型的安全性
        
        参数:
            model: 待评估的模型
            
        返回:
            安全评估结果
        """
        results = {}
        
        # 运行安全评估套件中的所有测试
        for test_name, test_func in self.evaluation_suite.items():
            test_result = test_func(model)
            results[test_name] = test_result
            
            print(f"测试 {test_name}: {test_result}")
        
        # 计算综合安全分数
        safety_score = sum(results.values()) / len(results)
        results["overall_safety_score"] = safety_score
        
        return results

上述代码展示了安全对齐的多层次策略实现,包括安全SFT、安全偏好对齐和对抗训练三个主要步骤,以及安全评估流程。

5.4.2 安全类别与防护机制

Qwen3的安全对齐覆盖了多种安全类别,每种类别都有专门的防护机制:

  1. 有害内容

    • 暴力和极端内容
    • 仇恨言论和歧视
    • 自残和自杀相关内容
    • 成人和色情内容
  2. 非法活动

    • 黑客和网络攻击
    • 欺诈和金融犯罪
    • 非法药物和武器
    • 侵犯隐私和个人信息
  3. 误导信息

    • 虚假新闻和谣言
    • 医疗和健康误导
    • 科学误导
    • 阴谋论
  4. 操纵和滥用

    • 社会工程和操纵
    • 政治宣传
    • 商业滥用
    • 身份冒充

对于每种安全类别,Qwen3都实现了专门的防护机制,包括:

  1. 内容过滤:识别和过滤有害请求和内容。
  2. 安全回应:对有害请求提供适当的拒绝回应,解释拒绝原因。
  3. 边界检测:识别试图绕过安全措施的边界情况。
  4. 上下文理解:根据上下文判断内容的安全性,避免过度拒绝。
# 安全类别与防护机制示例
def safety_filter(prompt, model_response, safety_categories):
    """
    安全过滤器
    
    参数:
        prompt: 用户提示
        model_response: 模型原始回答
        safety_categories: 安全类别配置
        
    返回:
        过滤后的回答
    """
    # 检查提示是否包含有害内容
    prompt_safety_check = check_safety(prompt, safety_categories)
    
    # 如果提示包含有害内容,返回拒绝回答
    if not prompt_safety_check["is_safe"]:
        category = prompt_safety_check["violated_category"]
        return generate_refusal_response(category)
    
    # 检查模型回答是否包含有害内容
    response_safety_check = check_safety(model_response, safety_categories)
    
    # 如果回答包含有害内容,返回拒绝回答
    if not response_safety_check["is_safe"]:
        category = response_safety_check["violated_category"]
        return generate_refusal_response(category)
    
    # 如果都安全,返回原始回答
    return model_response

def check_safety(text, safety_categories):
    """
    检查文本的安全性
    
    参数:
        text: 待检查的文本
        safety_categories: 安全类别配置
        
    返回:
        安全检查结果
    """
    result = {
        "is_safe": True,
        "violated_category": None,
        "confidence": 0.0
    }
    
    # 检查每个安全类别
    for category, config in safety_categories.items():
        # 使用关键词匹配
        if config["use_keywords"]:
            for keyword in config["keywords"]:
                if keyword.lower() in text.lower():
                    result["is_safe"] = False
                    result["violated_category"] = category
                    result["confidence"] = 0.8
                    return result
        
        # 使用分类器
        if config["use_classifier"]:
            classifier = config["classifier"]
            classification = classifier(text)
            
            if classification["label"] == "unsafe" and classification["score"] > config["threshold"]:
                result["is_safe"] = False
                result["violated_category"] = category
                result["confidence"] = classification["score"]
                return result
    
    return result

def generate_refusal_response(category):
    """
    生成拒绝回答
    
    参数:
        category: 违反的安全类别
        
    返回:
        拒绝回答
    """
    refusal_templates = {
        "violence": "我不能提供关于暴力或极端内容的信息。如果您需要帮助,请考虑寻求专业支持。",
        "hate_speech": "我不能生成包含仇恨言论或歧视内容的回答。我致力于尊重所有人群。",
        "self_harm": "我注意到您的请求涉及自残相关内容。如果您正在经历困难,请寻求专业心理健康支持。",
        "adult": "我不能生成成人或色情内容。请提出其他我能帮助的问题。",
        "illegal_activities": "我不能提供关于非法活动的指导或建议。请遵守法律法规。",
        "misinformation": "我不能生成可能包含误导信息的内容。我致力于提供准确可靠的信息。",
        "manipulation": "我不能生成旨在操纵或滥用他人的内容。我致力于促进积极健康的交流。"
    }
    
    # 获取对应类别的拒绝模板
    if category in refusal_templates:
        return refusal_templates[category]
    else:
        # 默认拒绝回答
        return "我不能回答这个问题,因为它可能违反我的使用指南。请提出其他我能帮助的问题。"

上述代码展示了安全过滤器的基本实现,包括提示和回答的安全检查,以及根据违反的安全类别生成适当的拒绝回答。

5.4.3 安全评估与红队测试

为了确保Qwen3的安全性,团队进行了全面的安全评估和红队测试:

  1. 安全评估基准

    • ToxicChat:测试模型对有毒和有害内容的抵抗力。
    • AdvBench:测试模型对对抗性攻击的抵抗力。
    • DecodingTrust:全面评估模型在多个安全维度上的表现。
    • SafetyBench:测试模型在各种安全场景下的表现。
  2. 红队测试

    • 招募专业的"红队"成员,尝试绕过模型的安全措施。
    • 使用各种技术,如提示注入、间接请求、上下文操纵等。
    • 记录成功的攻击并用于改进模型的安全性。
  3. 持续监控与更新

    • 建立持续的安全监控系统,收集和分析用户交互数据。
    • 定期更新安全措施,应对新出现的威胁和攻击方法。
# 安全评估与红队测试示例
def run_safety_evaluation(model, evaluation_suites):
    """
    运行安全评估
    
    参数:
        model: 待评估的模型
        evaluation_suites: 评估套件列表
        
    返回:
        评估结果
    """
    results = {}
    
    for suite_name, suite in evaluation_suites.items():
        print(f"运行评估套件: {suite_name}")
        suite_results = suite.evaluate(model)
        results[suite_name] = suite_results
        
        # 打印主要结果
        print(f"  总体通过率: {suite_results['overall_pass_rate']:.2f}")
        for category, score in suite_results["category_scores"].items():
            print(f"  {category}: {score:.2f}")
    
    return results

def run_red_team_testing(model, red_team_attacks):
    """
    运行红队测试
    
    参数:
        model: 待测试的模型
        red_team_attacks: 红队攻击列表
        
    返回:
        测试结果
    """
    results = {
        "total_attacks": len(red_team_attacks),
        "successful_attacks": 0,
        "attack_success_rate": 0.0,
        "attack_details": []
    }
    
    for attack in red_team_attacks:
        print(f"测试攻击: {attack['name']}")
        
        # 运行攻击
        attack_result = run_attack(model, attack)
        
        # 记录结果
        results["attack_details"].append({
            "attack_name": attack["name"],
            "attack_type": attack["type"],
            "success": attack_result["success"],
            "prompt": attack["prompt"],
            "model_response": attack_result["response"],
            "notes": attack_result["notes"]
        })
        
        # 更新成功攻击计数
        if attack_result["success"]:
            results["successful_attacks"] += 1
    
    # 计算攻击成功率
    results["attack_success_rate"] = results["successful_attacks"] / results["total_attacks"]
    
    print(f"总攻击数: {results['total_attacks']}")
    print(f"成功攻击数: {results['successful_attacks']}")
    print(f"攻击成功率: {results['attack_success_rate']:.2f}")
    
    return results

def run_attack(model, attack):
    """
    运行单个攻击
    
    参数:
        model: 待测试的模型
        attack: 攻击配置
        
    返回:
        攻击结果
    """
    # 获取模型回答
    response = model.generate(attack["prompt"])
    
    # 检查攻击是否成功
    success = False
    notes = ""
    
    if attack["type"] == "jailbreak":
        # 检查模型是否生成了被禁内容
        if contains_prohibited_content(response, attack["prohibited_content"]):
            success = True
            notes = "模型生成了被禁内容"
    
    elif attack["type"] == "prompt_injection":
        # 检查模型是否执行了注入的指令
        if follows_injected_instruction(response, attack["injected_instruction"]):
            success = True
            notes = "模型执行了注入的指令"
    
    elif attack["type"] == "data_extraction":
        # 检查模型是否泄露了敏感信息
        if contains_sensitive_information(response, attack["sensitive_information"]):
            success = True
            notes = "模型泄露了敏感信息"
    
    return {
        "success": success,
        "response": response,
        "notes": notes
    }

上述代码展示了安全评估和红队测试的基本实现,包括运行评估套件和模拟各种攻击场景。通过这些测试,Qwen3团队能够全面评估模型的安全性,并针对发现的问题进行改进。

5.5 对齐效果评估:平衡有用性与安全性

后训练对齐的最终目标是在保持模型有用性的同时,确保其安全性和符合人类价值观。Qwen3团队对对齐后的模型进行了全面评估,以验证对齐效果。

5.5.1 有用性评估

对齐后的Qwen3模型在有用性方面表现出色:

  1. 指令遵循能力

    • 在AlpacaEval 2.0上,Qwen3 达到了92.5%的胜率(相对于Claude 2),超过了许多同类模型。
    • 在MT-Bench上,Qwen3 获得了8.6分(满分10分),接近闭源模型GPT-4的水平。
  2. 任务表现

    • 在MMLU、BBH、GSM8K等基准测试上,对齐后的模型保持了与基础模型相当的性能,有些甚至略有提升。
    • 在实际应用场景中,如问答、创作、编程等任务上,模型展示了强大的能力。
  3. 多语言能力

    • 在多语言指令遵循和任务表现上,Qwen3保持了良好的性能,特别是在中文、英文、日文等主要语言上。
5.5.2 安全性评估

在安全性方面,对齐后的Qwen3模型也取得了显著进步:

  1. 有害内容抵抗力

    • 在ToxicChat测试中,Qwen3 的有害内容生成率低于5%,远低于未对齐的基础模型。
    • 在AdvBench上,Qwen3 的防御成功率达到95.8%,表现优于多数开源模型。
  2. 红队测试结果

    • 在专业红队的测试中,Qwen3 成功抵御了超过90%的攻击尝试。
    • 对于成功的攻击,团队进行了分析并在后续版本中进行了修复。
  3. 安全与有用性平衡

    • Qwen3在保持安全性的同时,避免了过度拒绝合法请求的问题。
    • 在敏感但合法的话题上,模型能够提供有用的信息,同时保持适当的界限。
对齐效果
有用性
安全性
指令遵循: 92.5%
任务表现: 85.3%
多语言能力: 优秀
有害内容抵抗: 95%+
红队防御: 90%+
安全与有用性平衡: 良好

上图展示了Qwen3 在有用性和安全性方面的对齐效果。总体而言,Qwen3成功地在这两个方面之间找到了良好的平衡点,既保持了强大的有用性,又确保了必要的安全性。

5.5.3 人类价值观对齐

除了有用性和安全性外,Qwen3还在人类价值观对齐方面取得了进展:

  1. 诚实性

    • 模型能够承认自己的局限性,在不确定时表明自己的不确定性。
    • 避免编造事实,对于不知道的问题会明确表示不知道。
  2. 公平性

    • 模型在处理涉及不同群体的问题时表现出公平和中立。
    • 避免偏见和刻板印象,尊重多元观点。
  3. 透明度

    • 模型清晰地表明自己是一个AI助手,不会尝试冒充人类。
    • 在适当的情况下,解释自己的推理过程和决策依据。

这些方面的对齐使得Qwen3不仅是一个有用且安全的AI助手,还是一个符合人类价值观和伦理标准的助手。

5.6 对齐的挑战与未来方向

尽管Qwen3在后训练对齐方面取得了显著进展,但仍然面临一些挑战,这些也是未来研究的重要方向:

  1. 对齐税(Alignment Tax)

    • 对齐过程可能导致模型在某些任务上的性能下降,特别是需要创造性和边界思考的任务。
    • 未来研究需要探索如何减少对齐税,在保持安全性的同时不损害模型的能力。
  2. 文化差异

    • 不同文化和地区对什么是"有害"或"不当"的定义可能不同。
    • 需要更多研究来理解和适应这些文化差异,创建能够尊重多元文化价值观的模型。
  3. 对齐稳定性

    • 随着模型规模的增加和能力的提升,保持对齐的稳定性变得更加困难。
    • 需要开发更强大的对齐方法,确保模型在能力提升的同时保持对齐。
  4. 评估方法

    • 现有的评估方法可能不足以全面衡量模型的对齐程度。
    • 需要开发更全面、更细致的评估框架,特别是对人类价值观对齐的评估。

Qwen3团队正在积极研究这些挑战,并探索新的对齐方法和评估框架,以在未来版本中进一步提升模型的对齐效果。

总的来说,后训练对齐是将预训练基础模型转变为可用AI助手的关键环节。通过监督微调、偏好对齐和安全对齐三个阶段,Qwen3成功地平衡了有用性、安全性和人类价值观对齐,成为一个既强大又可信赖的AI助手。这些对齐技术和经验不仅对Qwen3本身重要,也为整个AI领域的安全和负责任发展提供了有价值的参考。

### 关于Qwen2.5 14B 参数模型的训练方法 对于拥有140亿参数的大规模语言模型 Qwen2.5 的训练,通常涉及分布式计算资源以及优化算法来处理庞大的数据集和复杂的网络结构[^1]。 #### 训练环境配置 为了支持如此大规模的模型训练,硬件方面推荐使用多台配备高性能GPU(如NVIDIA A100)的工作站组成集群。软件环境中需安装PyTorch框架及其依赖项,并设置好相应的通信库以便实现跨节点的数据交换与同步操作。 #### 数据准备 高质量且多样化的语料库是成功训练大型预训练模型的关键因素之一。应收集来自不同领域、风格各异的文字材料作为输入源,经过清洗、分词等一系列预处理流程后形成适合喂给神经网络的形式。 #### 模型架构设计 采用Transformer为基础构建深层编码器解码器结构,在此基础上引入稀疏激活机制和其他改进措施以提高效率并减少内存占用。针对特定应用场景还可以考虑加入额外的任务导向模块或自定义层。 #### 超参调整策略 合理设定初始学习率、批尺寸(batch size)等超参数至关重要。实践中往往通过网格搜索(Grid Search)或者贝叶斯优化(Bayesian Optimization)等方式寻找最优组合方案;同时利用梯度累积(Gradient Accumulation)技术缓解单步更新时可能出现的信息丢失现象。 ```python import torch from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir=&#39;./results&#39;, num_train_epochs=3, per_device_train_batch_size=8, gradient_accumulation_steps=4, learning_rate=5e-5, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, ) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小爷毛毛(卓寿杰)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值