尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

选择性上下文偏好优化:让大模型学会信任可信信息

选择性上下文偏好优化:让大模型学会信任可信信息 最近在做 RAG 问答的迭代时被一个现象困扰了很久模型对检索结果几乎是“全盘接受”哪怕检索文档和问题只有表面相关它也能一本正经地引用错误信息。一开始以为是检索精度不够但调高相关性阈值之后召回又明显下降。后来意识到问题不止在检索端更在模型对上下文的信任机制上。这个场景放在大模型应用里非常普遍长上下文窗口给了模型更多信息但模型并不知道哪些信息是可信的、哪些是噪声。如果只是把更多文档塞进上下文往往不是“增强”而是“污染”。Selective Context Preference Optimization选择性上下文偏好优化后文简称 SCPO正是为了解决“模型何时该信任上下文”这个问题。本文会从概念、原理、数据构造到训练代码逐个拆开讲希望给在 RAG、Agent 或长文本场景里被上下文噪声折磨的读者一条可以落地的思路。1. 背景与核心概念1.1 上下文越长模型越容易被误导很多人对大模型上下文的第一反应是“越长越好”。模型支持 128K、1M 上下文那就把所有资料都塞进去让模型自己找答案。但在实际业务里长上下文带来的问题非常明显检索到的大量文档只有一小部分是相关的其余都是噪声不同文档之间的信息存在冲突模型不知道该信哪个多轮对话中用户随口说的纠正词或闲聊内容会被模型当成新的指令依据Agent 场景下模型拿到几十个工具说明容易选错工具或填错参数。这些问题本质上不是“模型看不懂上下文”而是“模型无法对上下文做可信度区分”。在训练阶段模型见过的样本通常假设输入信息都是有效的但推理阶段我们却把一堆来源不同、质量参差不齐的内容塞给它。这种分布不一致才是上下文被滥用的根源。1.2 什么是选择性上下文偏好优化Selective Context Preference Optimization 是一种针对上下文可信度的偏好优化训练思路核心是让模型在学习过程中区分“高质量上下文”和“噪声上下文”并在生成答案时倾向于依赖高质量上下文。它可以拆成两个关键词来理解Selective Context选择性上下文不是所有上下文都值得被使用模型需要选择性地关注对当前问题有帮助的片段。这和传统的 attention 机制不同attention 解决的是“关注哪些 token”而这里解决的是“对输入中整段外部信息是否信任”的问题。Preference Optimization偏好优化使用偏好对preferred vs rejected来微调模型使得模型对“好答案/坏答案”或“好上下文/坏上下文”之间的差异有清晰建模。简单来说SCPO 的思想是既然模型分不清哪些上下文可信那就在训练数据里明确告诉它——在可信上下文下生成的答案应该被增强在噪声上下文下生成的答案应该被抑制。模型通过这种对比信号逐步学会“选择性信任”。1.3 SCPO 与 DPO 的关系要理解 SCPO必须先理解 DPODirect Preference Optimization直接偏好优化。DPO 是 2023 年被提出的一种训练对齐方法用来替代传统的 RLHF 流程。传统 RLHF 需要训练奖励模型再用强化学习 PPO 算法更新策略流程复杂、超参数多、训练不稳定。DPO 的最大贡献是发现偏好优化可以直接通过一个简单的二元分类损失完成不需要显式训练奖励模型。DPO 的目标是让模型对偏好回答chosen的输出概率上升对拒绝回答rejected的输出概率下降同时用参考模型约束模型不要偏离原始分布太远。SCPO 可以理解为 DPO 在“上下文维度”上的延伸。DPO 的偏好对通常是“同一个问题、两个不同回答”而 SCPO 的偏好对则同时包含上下文差异和回答差异。从训练目标上看SCPO 在 DPO 的公式基础上增加了上下文变量让模型在优化回答分布的同时也优化“对上下文的依赖策略”。1.4 典型应用场景SCPO 适合所有“外部上下文质量波动较大”的场景场景问题表现SCPO 的作用RAG 问答检索结果混入无关文档模型误引用错误信息让模型忽略低相关度上下文避免错误引用长文档问答文档存在重复、矛盾或过时内容让模型优先采用可信段落多轮对话用户随口一句不准确的信息改变模型判断减少对低置信用户输入的依赖Agent 工具调用工具说明很长模型选错工具让模型只关注必要的参数说明内容摘要输入文档有大量广告、模板噪声模型学会跳过噪声段落如果你的应用正好是这些类型那 SCPO 就值得你认真尝试。2. 核心原理拆解2.1 DPO 目标函数回顾先回顾 DPO 的核心公式。给定一个提示 (x)模型策略为 (\pi_\theta)参考模型为 (\pi_{\text{ref}})偏好回答为 (y_w)拒绝回答为 (y_l)DPO 的损失函数为[ L_{\text{DPO}}(\pi_\theta; \pi_{\text{ref}}) -\mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[ \log \sigma\left( \beta \log \frac{\pi\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)}\beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)} \right) \right] ]其中 (\sigma) 是 sigmoid 函数(\beta) 是温度超参数控制偏差的强度。这个公式表达的含义非常直接如果策略模型在偏好回答 (y_w) 上的相对概率高于拒绝回答 (y_l)那么 loss 会变小反之 loss 会变大。(\pi_{\text{ref}}) 的作用是防止模型在优化过程中偏离原始模型太远从而保持生成能力。2.2 SCPO 的选择性机制SCPO 把“上下文”作为影响偏好的一个关键变量引入。在 SCPO 的设置中每个训练样本包含用户问题 (q)高质量/可信上下文 (c^)低质量/噪声上下文 (c^-)基于可信上下文生成的好回答 (y^)基于噪声上下文生成的坏回答 (y^-)。这样模型就能同时学习两个层面的信号回答层面的信号(y^) 比 (y^-) 更值得生成上下文层面的信号(c^) 比 (c^-) 更值得依赖。训练目标可以写成类似 DPO 的扩展形式[ L_{\text{SCPO}} -\mathbb{E}{(q, c^, c^-, y^, y^-) \sim \mathcal{D}} \left[ \log \sigma\left( \beta \log \frac{\pi\theta(y^|q,c^)}{\pi_{\text{ref}}(y^|q,c^)}\beta \log \frac{\pi_\theta(y^-|q,c^-)}{\pi_{\text{ref}}(y^-|q,c^-)} \right) \right] ]这里需要说明的是不同论文或工程实现中对 SCPO 的具体定义会有差异但思想是一致的让模型偏好“可信上下文 优质回答”的组合抑制“噪声上下文 劣质回答”的组合。2.3 上下文可信度从哪里来SCPO 的训练效果很大程度上取决于如何构造“可信”与“不可信”上下文。常见的方式有三种。第一种基于检索信号构造。在 RAG 场景中检索系统会为每个 query 返回若干候选文档并且带有相关性分数。可以直接把相关性排序靠前的文档当作可信上下文把靠后但仍有部分关键词匹配的文档当作噪声上下文。这种方式的优点是构造成本低缺点是检索分数并不完全等于真实质量。第二种基于模型自评估构造。让大模型给上下文打分例如让模型判断“这份文档是否包含回答用户问题所需的足够信息”。通常可以使用一个更强大的模型来打标人工只负责抽样质检。这种方式更接近真实语义质量但成本较高也存在模型偏好偏差。第三种基于可学习的选择器。训练一个 Context Selector 模块输入上下文和问题输出一个可信度分数。选择器可以与主模型联合训练也可以先单独训练再冻结。这种方法工程上最灵活但训练难度也会上升。在实际工程中我建议先用前两种方式构造离线数据把 SCPO 主模型训练跑通再考虑引入可学习选择器。2.4 为什么 SCPO 能改善模型的信任策略从梯度更新的视角来看DPO 类方法本质上是让模型在“偏好样本”和“拒绝样本”之间做对比学习。SCPO 把上下文差异也纳入对比范围后模型会观察到当上下文可信时答案被奖励当上下文噪声时答案被抑制。慢慢地模型生成时会倾向于从上下文中寻找那些“类似可信样本”的特征而不是对所有上下文一视同仁。这种能力不是靠 prompt 里写“请忽略无关内容”实现的而是直接改变模型对上下文条件分布的建模。所以 SCPO 更适合作为训练阶段的对齐手段而不是推理阶段的补救策略。3. 环境准备与实验设计3.1 实验环境说明SCPO 训练需要以下基础环境操作系统Linux / macOS / Windows WSL2 均可编程语言Python 3.10深度学习框架PyTorch 2.0推理与训练库transformers、datasets、peft、accelerateGPU至少一张显存 16GB 以上的显卡建议 24GB模型本文示例以小规模模型为主方便本地验证。版本号建议根据实际环境调整。很多情况下transformers和peft的 API 会随版本变化如果你的环境版本较低需要以对应版本文档为准。3.2 依赖安装pip install torch transformers datasets accelerate peft推荐使用虚拟环境安装避免依赖版本互相冲突。3.3 项目结构为了便于理解我把实验代码组织成如下目录结构scpo_demo/ ├── data/ │ └── train.jsonl ├── src/ │ ├── dataset.py │ ├── scorer.py │ └── loss.py ├── train_scpo.py └── README.md其中data/train.jsonl训练数据每条记录是一个样本src/dataset.py数据加载和预处理逻辑src/scorer.py上下文选择器的简化实现src/loss.pySCPO 损失函数实现train_scpo.py训练主脚本。4. 完整实战案例4.1 构造示例数据集SCPO 的训练数据是一组偏好对。我构造一个 RAG 问答场景的示例data/train.jsonl{query: 什么是注意力机制, good_context: 注意力机制是深度学习中的一种技术它允许模型在处理序列数据时动态地关注输入中的不同部分核心思想是计算查询与键的相似度并用相似度加权求和得到输出。, bad_context: 注意力机制是一种营销策略指通过内容创作吸引用户关注品牌从而提升品牌曝光和转化率。, good_response: 注意力机制是一种让模型在处理序列数据时动态聚焦重要信息的技术核心是查询、键和值的加权组合。, bad_response: 注意力机制是营销中吸引用户注意力的方法。, category: rag} {query: 什么是梯度消失, good_context: 梯度消失是指在深层神经网络反向传播过程中梯度逐层相乘后变得极小导致浅层参数无法更新常见于使用sigmoid激活函数的深度网络。, bad_context: 梯度消失是指模型训练过程中学习率过高导致损失函数快速下降的现象。, good_response: 梯度消失是深层网络训练中的常见问题根源是反向传播时梯度连乘导致数值趋近于零。, bad_response: 梯度消失是学习率过高引起的训练发散问题。, category: rag} {query: 什么是CAP定理, good_context: CAP定理指出分布式系统在一致性、可用性和分区容错性三者中最多只能同时满足两项。, bad_context: CAP定理是指软件开发中成本、进度和质量三者之间需要平衡的经验法则。, good_response: CAP定理由Eric Brewer提出描述分布式系统在一致性、可用性和分区容错性之间的权衡。, bad_response: CAP定理是项目管理中成本、进度、质量三角约束。, category: rag}在构造数据时要注意bad_context不能是毫无关联的文本否则模型很容易通过表面文本差异来判断而不是真正学习语义可信度。bad_context应当与good_context有相同主题但包含错误定义或过时信息。4.2 数据加载与预处理创建src/dataset.pyimport json import torch from torch.utils.data import Dataset from transformers import AutoTokenizer class SCPODataset(Dataset): 加载 SCPO 训练数据并转换为模型输入格式。 def __init__(self, data_path: str, tokenizer: AutoTokenizer, max_length: int 1024): self.samples [] self.tokenizer tokenizer self.max_length max_length with open(data_path, r, encodingutf-8) as f: for line in f: sample json.loads(line) self.samples.append(sample) def _build_input(self, query: str, context: str, response: str): # 把 query、context、response 拼接成完整文本 text f问题{query}\n资料{context}\n回答{response} enc self.tokenizer( text, truncationTrue, max_lengthself.max_length, return_tensorspt, ) return { input_ids: enc[input_ids][0], attention_mask: enc[attention_mask][0], } def __len__(self): return len(self.samples) def __getitem__(self, idx: int): item self.samples[idx] query item[query] good_context item[good_context] bad_context item[bad_context] good_response item[good_response] bad_response item[bad_response] good_input self._build_input(query, good_context, good_response) bad_input self._build_input(query, bad_context, bad_response) return { good_input_ids: good_input[input_ids], good_attention_mask: good_input[attention_mask], bad_input_ids: bad_input[input_ids], bad_attention_mask: bad_input[attention_mask], }这里的关键是每个样本都包含“可信上下文 好回答”和“噪声上下文 坏回答”两组输入。模型需要在这两组输入之间形成对比。4.3 定义上下文选择器可学习的上下文选择器用于给上下文片段打分这里给出一个简化实现。它接收输入 token 的 hidden state通过平均池化之后映射为一个可信度分数。创建src/scorer.pyimport torch import torch.nn as nn class ContextScorer(nn.Module): 基于 hidden state 的上下文可信度打分器。 说明这是一个简化版本真实场景可以换成更复杂的结构 例如跨注意力、句子级编码器或对比学习训练的打分模型。 def __init__(self, hidden_size: int, hidden_dim: int 128): super().__init__() self.fc1 nn.Linear(hidden_size, hidden_dim) self.fc2 nn.Linear(hidden_dim, 1) self.dropout nn.Dropout(0.1) def forward(self, hidden_states: torch.Tensor) - torch.Tensor: # hidden_states: [batch_size, seq_len, hidden_size] pooled hidden_states.mean(dim1) pooled self.dropout(torch.relu(self.fc1(pooled))) score self.fc2(pooled).squeeze(-1) return score这个选择器不是训练主流程中的必需组件。如果你的数据是通过检索分数或人工标注构造的可以先不训练选择器直接使用 SCPO 主损失。只有当你希望模型自己学会对上下文打分时才需要联合训练这个模块。4.4 SCPO 损失函数实现创建src/loss.pyimport torch import torch.nn.functional as F def scpo_loss( policy_good_logp: torch.Tensor, policy_bad_logp: torch.Tensor, ref_good_logp: torch.Tensor, ref_bad_logp: torch.Tensor, beta: float 0.1, ) - torch.Tensor: 计算 SCPO 损失。 参数说明 - policy_good_logp: 策略模型在可信上下文好回答上的 log 概率 - policy_bad_logp: 策略模型在噪声上下文坏回答上的 log 概率 - ref_good_logp: 参考模型在可信上下文好回答上的 log 概率 - ref_bad_logp: 参考模型在噪声上下文坏回答上的 log 概率 - beta: 温度系数控制优化强度 good_log_ratio policy_good_logp - ref_good_logp bad_log_ratio policy_bad_logp - ref_bad_logp logits beta * (good_log_ratio - bad_log_ratio) loss -F.logsigmoid(logits).mean() return loss这个损失函数与 DPO 的损失在形式上一致区别在于数据组织方式这里的 good/bad 样本在上下文和回答两个维度上都不同。如果你需要让模型额外学习“上下文选择”可以对选择器打分的部分增加一个辅助损失例如二分类交叉熵。不过这会增加训练复杂度建议先跑通基础版再说。4.5 训练主流程创建train_scpo.pyimport torch import torch.nn.functional as F from torch.utils.data import DataLoader from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model from src.dataset import SCPODataset from src.loss import scpo_loss def get_sequence_logp(model, input_ids, attention_mask): 计算整个输入序列 token 级别的 log 概率之和。 注意这里为了简化没有区分题干部分和答案部分 真实训练中通常只计算 response 部分 token 的 log 概率避免模型把问题内容也背下来。 outputs model( input_idsinput_ids, attention_maskattention_mask, ) logits outputs.logits # [batch, seq_len, vocab_size] log_probs F.log_softmax(logits, dim-1) token_logp log_probs.gather(-1, input_ids.unsqueeze(-1)).squeeze(-1) # 去掉最后一个位置的预测没有真实下一个 token token_logp token_logp[:, :-1] return token_logp.sum(dim-1) def main(): model_name Qwen/Qwen2-0.5B # 示例使用小模型可按需替换 tokenizer AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token tokenizer.eos_token base_model AutoModelForCausalLM.from_pretrained(model_name) # 使用 LoRA 降低显存占用方便实验 peft_config LoraConfig( r8, lora_alpha16, target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) policy_model get_peft_model(base_model, peft_config) # 参考模型保持冻结不更新梯度 ref_model AutoModelForCausalLM.from_pretrained(model_name) for param in ref_model.parameters(): param.requires_grad False dataset SCPODataset(data/train.jsonl, tokenizer, max_length512) dataloader DataLoader(dataset, batch_size2, shuffleTrue) optimizer torch.optim.AdamW(policy_model.parameters(), lr1e-5) beta 0.1 policy_model.train() device cuda if torch.cuda.is_available() else cpu policy_model.to(device) ref_model.to(device) for epoch in range(3): total_loss 0.0 for batch in dataloader: good_input_ids batch[good_input_ids].to(device) good_attn batch[good_attention_mask].to(device) bad_input_ids batch[bad_input_ids].to(device) bad_attn batch[bad_attention_mask].to(device) # 策略模型前向 policy_good_logp get_sequence_logp( policy_model, good_input_ids, good_attn ) policy_bad_logp get_sequence_logp( policy_model, bad_input_ids, bad_attn ) # 参考模型前向不计算梯度 with torch.no_grad(): ref_good_logp get_sequence_logp( ref_model, good_input_ids, good_attn ) ref_bad_logp get_sequence_logp( ref_model, bad_input_ids, bad_attn ) loss scpo_loss( policy_good_logp, policy_bad_logp, ref_good_logp, ref_bad_logp, beta, ) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch 1} / 3, avg loss: {total_loss / len(dataloader):.4f}) # 保存 LoRA 权重 policy_model.save_pretrained(output/scpo_lora) tokenizer.save_pretrained(output/scpo_lora) if __name__ __main__: main()运行脚本python train_scpo.py4.6 结果说明在几轮训练后你会观察到 loss 呈下降趋势。但这只是“能跑通”的标志并不代表模型已经学会了选择性信任上下文。要验证 SCPO 的效果推荐做两件事**第一写一个简单的评估脚本。**分别给同一个问题拼上可信上下文和噪声上下文观察模型在两种输入下的回答质量。**第二统计回答引用率。**在 RAG 场景中可以统计模型回答中是否出现了噪声上下文里的错误实体或错误结论。如果训练有效噪声上下文导致的错误引用次数应当下降。5. 常见问题与排查思路问题现象常见原因解决思路训练 loss 不下降学习率过高、数据噪声大、beta 参数不合理调低学习率到 1e-5 左右检查 bad context 是否过于离谱Loss 直接变成负数模型对 good/bad 区分已经太好或数据存在泄漏增加训练数据难度保证 bad context 与 good context 主题接近训练后生成能力变差在答案部分之外也计算了 loss导致模型背诵输入只对 response 部分 token 计算 log 概率显存不足模型太大或 batch_size 太大使用 LoRA、梯度累积、更小的基础模型上下文选择器效果差打分器训练数据太少或选择了过强的主模型先用启发式分数构造偏好数据后续再训练选择器推理时模型仍然乱引用数据构造里噪声上下文和可信上下文区分度不够引入人工质检删除质量太差的 bad context其中最值得注意的坑是“把整个序列都算进 loss”。如果不对答案部分做掩码模型可能会把输入的“问题 资料”当成本来就应该生成的文本导致 not only 无法学习选择性还会让生成能力退化。6. 最佳实践与工程建议6.1 数据质量决定上限SCPO 对数据的依赖程度非常高。如果 bad context 和 good context 之间的差异一眼就能看出来模型只会学会简单的表面规则无法真正泛化到复杂场景。建议构造数据时遵循以下原则bad context 要与 good context 属于同一主题bad context 不能是乱码、空文本或完全不相关的内容bad context 的错误应当是“看似合理实则错误”good response 和 bad response 要与对应上下文逻辑一致。在内容安全方面如果你把 SCPO 用于生产环境数据里会包含大量业务文档。务必在标注和训练前完成合规审查不采集、不存储敏感个人信息训练数据要经过脱敏处理。6.2 先冻结选择器再联合训练如果你决定使用可学习的 Context Scorer我的建议是分两个阶段第一阶段用检索分数或模型自评估分数作为监督单独训练选择器第二阶段冻结选择器只训练主模型的 SCPO 损失。等模型表现稳定后再把选择器与主模型联合微调。直接从头联合训练很容易出现选择器给出错误打分而主模型跟着走偏的情况。6.3 训练稳定性控制DPO 系列方法对参考模型的选择比较敏感。参考模型必须与策略模型的初始状态一致否则 log ratio 会出现较大的数值偏移。另一个稳定训练的技巧是使用 LoRA。因为 LoRA 只更新一小部分参数训练过程中模型偏离参考分布的速度更慢不容易出现单次更新后生成质量骤降的问题。beta 参数的取值建议在 0.01 到 0.5 之间尝试。beta 过小模型很难学习到偏好差异beta 过大训练不稳定容易出现生成退化。6.4 上线前评估不能只看指标SCPO 的最终效果需要回到实际场景来验证。在 RAG 场景中建议增加“错误引用率”指标即模型回答中包含噪声上下文错误信息的比例。这个指标比回复的自动打分更能反映模型是否真正学会了选择性信任。另外SCPO 训练后的模型建议做一次通用能力回归测试。很多时候针对上下文任务的训练可能会轻微影响模型原有能力尤其是长文本生成稳定性。如果通用能力下降明显可以考虑降低 beta 或减少训练步数。6.5 最小权限与变更管理如果你把 SCPO 应用在公司内部推理系统建议遵循最小权限原则训练数据只能通过受控的标注平台导入模型权重文件存放在私有存储桶中训练脚本在独立环境中运行不允许直接在生产服务器上执行训练命令。模型更新时要先在测试集和灰度环境中评估再全量发布。涉及数据库、配置中心或线上文件的操作务必先备份再执行变更。这里涉及的是模型训练流程虽然没有数据库操作但同样需要规范版本管理。7. 总结Selective Context Preference Optimization 给我最大的启发是模型对上下文的使用策略是可以通过训练来调整的而不是只能靠推理时的 prompt 去“提醒”它。整篇内容的核心包括上下文质量波动是导致 RAG 和 Agent 应用效果翻车的常见原因SCPO 在 DPO 基础上引入上下文可信度差异让模型同时学习“回答偏好”和“上下文信任偏好”训练数据以“可信上下文 好回答”和“噪声上下文 坏回答”的偏好对形式组织工程实现上可以先冻结参考模型、用 LoRA 训练策略模型再逐步引入可学习的选择器数据质量和评估指标设计是决定 SCPO 成败的关键因素。下一步你可以从更细的上下文粒度入手例如段落级选择、句子级选择甚至把选择器输出作为约束信号放进生成过程。也可以尝试把 SCPO 与 RAG 检索排序模型联合优化实现“检索更准、引用更强”的闭环。如果本文对你有帮助可以收藏备用。也欢迎在调试过程中记录训练日志和评估结果这类数据往往比论文里的结论更有参考价值。
返回列表