news 2026/4/15 18:53:59

奖励函数怎么写?verl自定义奖励实战教学

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
奖励函数怎么写?verl自定义奖励实战教学

奖励函数怎么写?verl自定义奖励实战教学

强化学习训练大语言模型,最关键的不是算法本身,而是——你给模型的反馈是否真实、合理、可执行。在RLHF(基于人类反馈的强化学习)中,奖励函数就是那个“裁判”,它不教模型怎么思考,但它清楚地告诉模型:这句话写得好不好、这个推理步骤对不对、这张图生成得像不像。写错奖励函数,再强的PPO也会学偏;写好奖励函数,哪怕用最基础的REINFORCE,也能训出靠谱结果。

但现实是:很多工程师卡在第一步——不知道奖励函数该怎么写。查文档只看到reward_fn参数,点进去发现是个空函数;看示例代码全是调用现成的HFRewardModel,可自己业务场景里没有现成模型怎么办?要不要从头训一个奖励模型?要的话数据怎么构造?不要的话纯规则又怕太死板……

这篇文章不讲理论推导,不堆公式,也不复刻论文。我们直接打开verl框架,用一个真实可运行的数学推理任务(GSM8K风格),手把手带你:

  • 从零写出一个可验证、可调试、可上线的自定义奖励函数
  • 理解verl中奖励函数的输入结构、调用时机和返回约束
  • 避开三个新手必踩的坑:token级reward错位、batch维度混乱、reward缩放失衡
  • 最后给出一套渐进式奖励设计方法论:从规则起步 → 加入轻量模型打分 → 引入可验证逻辑 → 对齐人工偏好

全程代码可复制、命令可粘贴、效果可验证。你不需要提前装好集群,一台带GPU的开发机就能跑通。

1. 先搞清verl里奖励函数到底长什么样

在verl中,奖励函数不是黑盒API,而是一个明确签名、严格契约的Python函数。它被设计成可插拔、可组合、可独立测试的单元。理解它的接口,是写好reward的第一步。

1.1 verl奖励函数的标准签名

verl要求所有自定义奖励函数必须符合以下签名:

def reward_fn( batch: Dict[str, torch.Tensor], model_outputs: Dict[str, torch.Tensor], tokenizer: PreTrainedTokenizerBase, **kwargs ) -> torch.Tensor: """ Args: batch: 包含原始prompt、chosen response等的字典,key通常为'prompt', 'chosen' model_outputs: 模型前向输出,含logits、attention_mask等 tokenizer: 用于解码、定位token位置 **kwargs: 额外配置,如reward_weight、debug_mode等 Returns: reward: shape = (batch_size,) 的一维Tensor,每个元素对应一条样本的标量reward """ pass

注意三个硬性要求:

  • 输入必须是字典,不能是list或tuple:verl内部按key取值,比如batch['prompt']必须存在
  • 返回必须是float32类型的一维Tensor:shape为(N,),不能是scalar、list、numpy array或二维Tensor
  • reward值必须是标量(每条样本一个数):不支持token-level reward(除非你手动聚合,如取mean/logprob)

这个签名看似简单,但90%的报错都源于违反其中一条。比如把return [1.2, 0.8]写成list,verl会在torch.stack()时崩溃;又比如忘了.to(torch.float32),混合精度训练会静默失败。

1.2 它在训练流程中何时被调用?

很多人以为reward函数只在PPO step里算一次,其实不然。在verl的HybridFlow架构中,reward函数被两次调用

调用阶段触发条件用途注意事项
第一阶段:rollout生成后Actor模型生成完chosenrejected响应后计算每条样本的reward,用于后续优势估计(advantage computation)此时model_outputs包含完整logits,可做token级分析
第二阶段:训练step中Critic模型更新时作为监督信号,与Critic预测值计算loss此时model_outputs可能被裁剪,仅保留必要字段

这意味着:你的reward函数必须无副作用、幂等、不依赖外部状态。不能在里面写日志文件、不能修改batch原地、不能调用随机数(除非固定seed)。

1.3 verl内置奖励函数长啥样?(以HFRewardModel为例)

verl官方示例中常用HFRewardModel,它本质是加载一个预训练的奖励模型(如OpenAssistant/reward-model-deberta-v3-large)。我们拆解它的核心逻辑,就能反推出自定义函数该怎么做:

# 简化版HFRewardModel.reward_fn逻辑 def hf_reward_fn(batch, model_outputs, tokenizer, reward_model, reward_tokenizer): # 1. 将prompt+response拼成文本 texts = [] for i in range(len(batch['prompt'])): prompt = tokenizer.decode(batch['prompt'][i], skip_special_tokens=True) response = tokenizer.decode(model_outputs['response_ids'][i], skip_special_tokens=True) texts.append(prompt + response) # 2. 用reward tokenizer编码,送入reward model inputs = reward_tokenizer(texts, padding=True, truncation=True, return_tensors="pt") with torch.no_grad(): reward_logits = reward_model(**inputs).logits # shape: (B, 1) # 3. 返回reward值(通常取logits[0]) return reward_logits.squeeze(-1).float() # shape: (B,)

关键启示有三点:

  • 它不碰原始logits:只用response_ids解码成文本,再喂给另一个模型——说明reward可以完全脱离Actor的内部表示
  • 它做的是“端到端打分”:输入是字符串,输出是标量,中间过程对Actor透明
  • 它依赖tokenizer一致性:prompt和response必须用同一个tokenizer解码,否则拼接错位

这给了我们极大自由:你可以用正则匹配关键词、调用外部API打分、运行本地小模型、甚至读Excel查表——只要最终能输出(B,)的Tensor。

2. 动手写第一个自定义奖励函数:数学答案校验器

我们以GSM8K数据集为背景:模型接收一道小学数学题(如“莉莉有5个苹果,吃了2个,还剩几个?”),需输出完整推理链并给出最终答案。理想reward应鼓励:
推理逻辑连贯
最终答案正确(数字匹配)
❌ 乱编答案(如“剩7个”)
❌ 答案藏在中间没标出(如不写“所以答案是3”)

下面这个函数,就是verl生产环境中真实可用的轻量级reward方案:

2.1 代码实现:基于正则的答案提取+数值比对

import re import torch from typing import Dict, Any, Optional from transformers import PreTrainedTokenizerBase def math_answer_reward_fn( batch: Dict[str, torch.Tensor], model_outputs: Dict[str, torch.Tensor], tokenizer: PreTrainedTokenizerBase, answer_key: str = "answer", # 从batch中取真实答案的key debug_mode: bool = False, reward_scale: float = 1.0, wrong_penalty: float = -2.0 ) -> torch.Tensor: """ 数学题答案校验奖励函数 - 提取模型输出末尾的数字(支持整数/小数/分数格式) - 与batch中提供的标准答案比对 - 正确给+1.0,错误给-2.0,未提取到给0.0 """ device = model_outputs['response_ids'].device batch_size = len(batch[answer_key]) rewards = torch.zeros(batch_size, dtype=torch.float32, device=device) # 逐条处理 for i in range(batch_size): try: # 1. 解码模型输出 response_ids = model_outputs['response_ids'][i] # 过滤掉padding和special tokens mask = response_ids != tokenizer.pad_token_id clean_ids = response_ids[mask] response_text = tokenizer.decode(clean_ids, skip_special_tokens=True).strip() # 2. 提取标准答案(来自batch,通常是数字字符串) true_answer_str = str(batch[answer_key][i]).strip() # 3. 从response_text中提取最后一个数字(支持多种格式) # 匹配:整数(123)、小数(3.14)、分数(1/2)、带单位(5个) number_pattern = r'[-+]?\d*\.?\d+(?:/\d+)?(?=\D*$|\s*$)' matches = re.findall(number_pattern, response_text) pred_answer_str = matches[-1] if matches else "" # 4. 数值标准化与比对(处理分数、小数等) def normalize_number(s: str) -> Optional[float]: s = s.strip() if not s: return None # 处理分数:'3/4' -> 0.75 if '/' in s and s.count('/') == 1: try: a, b = s.split('/') return float(a.strip()) / float(b.strip()) except: return None # 处理普通数字 try: return float(s) except: return None true_num = normalize_number(true_answer_str) pred_num = normalize_number(pred_answer_str) # 5. 计算reward if true_num is not None and pred_num is not None: # 允许小浮点误差(如3.0 vs 3) if abs(true_num - pred_num) < 1e-6: rewards[i] = 1.0 * reward_scale else: rewards[i] = wrong_penalty else: # 无法解析答案,给中性分 rewards[i] = 0.0 except Exception as e: if debug_mode: print(f"[Reward Debug] Error on sample {i}: {e}") rewards[i] = 0.0 return rewards

2.2 关键设计点解析

  • 为什么用正则而不是LLM解析?
    简单任务(如数学答案)用规则更稳定、更快、零成本。LLM解析可能把“答案是3”误判为“3.0”,而正则明确锚定结尾数字。

  • 为什么只取最后一个数字?
    GSM8K标准格式要求模型在结尾写So the answer is 3.,我们信任这个约定,避免过度解读中间步骤。

  • 为什么用normalize_number而不直接float()
    真实数据含分数(1/2)、科学计数(1e2)、负数(-5),统一转换防报错。

  • reward_scalewrong_penalty为何要参数化?
    后期可调优:若模型总不敢答,调高reward_scale;若胡编答案多,调低wrong_penalty(如-0.5)降低惩罚力度。

2.3 如何集成到verl训练脚本?

只需在PPO配置中替换reward函数:

# config/ppo_gsm8k.yaml algorithm: name: "ppo" reward_fn: "math_answer_reward_fn" # 指向你定义的函数 reward_kwargs: answer_key: "answer" # batch中真实答案的key名 reward_scale: 2.0 wrong_penalty: -1.5 # 在训练启动脚本中注册函数 # train.py from my_reward_module import math_answer_reward_fn # verl会自动通过字符串名找到该函数

验证方式:在训练日志中搜索reward_mean,初期应在[-1.5, 2.0]间波动;几轮后若稳定在1.2+,说明模型开始学会输出正确答案。

3. 进阶技巧:让奖励更鲁棒、更智能

纯规则奖励虽快,但面对复杂任务(如代码生成、多跳推理)易失效。下面三个技巧,帮你平滑过渡到更强大的reward设计。

3.1 技巧一:加入“置信度加权”,避免过拟合表面模式

问题:规则奖励可能让模型学会“凑数字”。例如题干问“多少人”,它不管逻辑,硬在结尾写100。解决方案:用Actor模型自身的logprobs评估“答案位置”的置信度。

def confidence_weighted_reward_fn(...): # ... 前面提取答案逻辑不变 ... # 新增:计算答案token的平均logprob if 'logits' in model_outputs: logits = model_outputs['logits'][i] # shape: (seq_len, vocab_size) response_ids = model_outputs['response_ids'][i] # 找到答案token在response中的起始位置(需结合tokenizer) answer_tokens = tokenizer.encode(pred_answer_str, add_special_tokens=False) if len(answer_tokens) > 0: # 简单起见,取答案第一个token的logprob first_ans_token = answer_tokens[0] pos = (response_ids == first_ans_token).nonzero() if len(pos) > 0: logprob = torch.log_softmax(logits[pos[0][0]], dim=-1)[first_ans_token] # 将reward乘以exp(logprob)作为置信权重 rewards[i] *= torch.exp(logprob).item()

效果:模型不仅答对,还要“自信地答对”,抑制胡编。

3.2 技巧二:组合多个reward信号,用加权和

单一reward易片面。verl支持CompositeRewardFn,轻松组合:

from verl.utils.reward import CompositeRewardFn # 组合:答案正确性(主)+ 推理长度合理性(辅)+ 无有害内容(兜底) composite_reward = CompositeRewardFn( reward_fns=[ ("answer_correct", math_answer_reward_fn, {"weight": 0.6}), ("length_bonus", length_reward_fn, {"weight": 0.2, "target_len": 120}), ("safety_penalty", safety_reward_fn, {"weight": -0.2}), ] )

注意:权重和必须为1,且负权重用于惩罚项。verl会自动归一化。

3.3 技巧三:引入“可验证逻辑”,超越字符串匹配

对需要多步推理的任务(如“如果A>B且B>C,则A>C吗?”),纯答案比对不够。我们可以嵌入轻量逻辑检查器:

def logic_verification_reward_fn(...): # 1. 提取模型的推理步骤(用句号分割) steps = [s.strip() for s in response_text.split('。') if s.strip()] # 2. 构建符号逻辑图(简化版) facts = set() for step in steps[:3]: # 只看前3步,防过长 if "因为" in step or "由于" in step: # 提取"因为X,所以Y" -> 添加事实X pass # 3. 检查结论是否被前提逻辑蕴含(调用z3或简易规则引擎) if is_logically_entailed(facts, conclusion): return 1.0 else: return -0.5 # 比乱答惩罚轻,鼓励尝试

这种reward已接近“过程监督”,是迈向高质量RLHF的关键一步。

4. 避坑指南:新手常犯的5个致命错误

写reward函数不是写Python脚本,它运行在分布式、混合精度、动态batch的verl引擎中。这些错误不会立刻报错,但会让训练悄无声息地失败。

4.1 错误1:在reward函数里调用.cpu().item()

# ❌ 危险!强制同步,拖慢整个pipeline reward_val = rewards[i].item() # 同步等待GPU完成 # 正确:全程保持Tensor在GPU上 rewards[i] = torch.tensor(1.0, device=device, dtype=torch.float32)

4.2 错误2:忽略batch内padding导致的长度不一致

# ❌ 错误:假设所有response等长 response_ids = model_outputs['response_ids'] # shape: (B, L) last_token = response_ids[:, -1] # 取最后一列 —— 但padding位置可能是pad_id! # 正确:用attention_mask定位真实结尾 attention_mask = model_outputs.get('attention_mask', torch.ones_like(response_ids, dtype=torch.bool)) seq_len = attention_mask.sum(dim=1) # 每条的真实长度 last_token = torch.gather(response_ids, 1, (seq_len-1).unsqueeze(1))

4.3 错误3:reward值域过大,导致Critic爆炸

# ❌ 危险:reward范围[-100, +100],Critic梯度爆炸 rewards[i] = float(score * 100) # 来自外部API的原始分 # 正确:归一化到[-2, 2]或使用tanh压缩 rewards[i] = torch.tanh(torch.tensor(score * 0.1)).item()

4.4 错误4:修改了batch或model_outputs字典

# ❌ 危险:破坏verl内部状态 batch['prompt'] = batch['prompt'] + " [REWARD]" # verl后续会出错 # 正确:只读,不写 prompt_text = tokenizer.decode(batch['prompt'][i], skip_special_tokens=True)

4.5 错误5:没设device,导致CPU/GPU混用

# ❌ 错误:在GPU模型上返回CPU tensor return torch.tensor([1.0, 0.5]) # 默认在CPU # 正确:显式指定device device = model_outputs['response_ids'].device return torch.tensor([1.0, 0.5], device=device, dtype=torch.float32)

5. 总结:构建你自己的奖励工程方法论

写好reward函数,不是一次编码任务,而是一套持续迭代的工程实践。根据verl在真实项目中的经验,我们提炼出四步法:

5.1 第一步:从“可验证规则”起步(1天)

  • 目标:快速获得baseline reward,验证训练流程
  • 做法:用正则、关键词、数值比对等确定性逻辑
  • 指标:reward_mean是否收敛?reward_std是否下降?

5.2 第二步:加入“轻量模型信号”(3天)

  • 目标:提升reward的语义理解能力
  • 做法:接入tiny-BERT、distilRoBERTa等<100M参数模型,做二分类(好/坏)
  • 关键:用verl的HFRewardModel封装,避免重写IO

5.3 第三步:设计“可解释过程reward”(1周)

  • 目标:让模型学会正确推理路径,不止答案对
  • 做法:拆解任务为子步骤(如“提取实体→判断关系→得出结论”),每步给分
  • 工具:用spaCy/NLTK做实体识别,NetworkX建模逻辑图

5.4 第四步:对齐“人工偏好分布”(持续)

  • 目标:让reward函数逼近人类评委打分
  • 做法:收集人工标注的pairwise preference(A>B),训练reward model
  • verl支持:verl.data.preference_dataset可直接加载偏好数据

记住:最好的reward函数,是那个让你敢把模型交给客户的函数。它不一定最复杂,但一定最可靠、最透明、最容易调试。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/11 7:12:53

零基础理解eSPI物理接口电气特性

以下是对您提供的博文内容进行 深度润色与工程化重构后的版本 。我以一位深耕x86平台硬件设计十年、常年与EC/TPM/Flash打交道的嵌入式系统工程师身份&#xff0c;用更自然、更具实操感的语言重写全文—— 去掉所有AI腔调、模板化结构和空泛术语堆砌&#xff0c;代之以真实调…

作者头像 李华
网站建设 2026/4/12 6:20:24

小白也能用!Open-AutoGLM手机AI代理实战入门指南

小白也能用&#xff01;Open-AutoGLM手机AI代理实战入门指南 1. 这不是科幻&#xff0c;是今天就能上手的手机AI助手 你有没有过这样的时刻&#xff1a; 想在小红书搜“最近爆火的咖啡店”&#xff0c;但手指划了三页还没找到&#xff1b;点外卖时反复对比五家店的满减规则&…

作者头像 李华
网站建设 2026/4/11 10:33:42

多模态检索前置:Qwen3-Embedding-4B文本编码实战

多模态检索前置&#xff1a;Qwen3-Embedding-4B文本编码实战 1. 为什么你需要一个真正好用的文本编码器 在构建多模态检索系统时&#xff0c;很多人把注意力全放在图像、视频或语音模型上&#xff0c;却忽略了最底层也最关键的一步——文本怎么被准确“翻译”成向量。如果文本…

作者头像 李华
网站建设 2026/4/11 20:51:59

快速理解LVGL教程工作原理:基于LittlevGL的UI设计

以下是对您提供的博文内容进行 深度润色与专业重构后的版本 。本次优化严格遵循您的全部要求: ✅ 彻底去除AI腔调与模板化结构(如“引言”“总结”等标题) ✅ 所有技术点以真实开发视角展开,穿插工程经验、调试陷阱、性能权衡与底层逻辑洞察 ✅ 语言自然流畅,像一位资…

作者头像 李华
网站建设 2026/4/14 5:48:21

Qwen3-14B工业质检应用:知识库问答系统部署实战

Qwen3-14B工业质检应用&#xff1a;知识库问答系统部署实战 1. 为什么工业质检需要专属知识库问答系统&#xff1f; 在电子元器件、汽车零部件、光伏板等制造产线&#xff0c;每天产生海量检测报告、设备手册、缺陷图谱、SOP作业指导书和历史维修记录。这些资料往往分散在PDF…

作者头像 李华
网站建设 2026/4/3 4:50:50

YOLO11分类任务教程:yolo11-cls模型使用指南

YOLO11分类任务教程&#xff1a;yolo11-cls模型使用指南 1. 为什么选择YOLO11-cls做图像分类 你是不是也遇到过这些情况&#xff1a; 想快速验证一张图属于什么类别&#xff0c;但加载ResNet或ViT模型要配环境、写数据加载器、调预处理参数&#xff0c;半天跑不起来&#xf…

作者头像 李华