更新于 2026年7月26日

在前面几篇文章当中,我们陆续给大家介绍就大模型中奖励模型的由来、思想以及目标函数的变化,这包括:

GPT 系列模型从第一性原理看 LLM 演进:GPT-1/2/3 到 InstructGPT 与 LLaMA 2 的范式变迁

Bradley-Terry 模型详解:奖励模型 RLHF 目标函数的数学基础与梯度求解

大模型对齐与奖励模型教程:详解 DeepMind Agent Alignment 到 InstructGPT RLHF 的演进

奖励模型目标函数推导教程:OpenAI 三篇论文带你搞懂 RLHF 中 Reward Model 的变迁

在接下来的这篇文章中,我们将会详细介绍 LLaMA 2 [1]中的奖励模型及数据集构建和代码实现过程。

在有了上面的一系列铺垫以后,再来看 LLaMA 2 中奖励模型的目标函数就非常容易理解了,它可以说是到目前为止我们已经介绍过的最简单的一种。

1. LLaMA 2 奖励模型样本构建#

1.1 LLaMA 2训练流程#

如图1所示便是 LLaMA 2 的整体训练流程,整体包含两部分:基于通用语料的自监督学习过程以及基于有监督学习微调学习过程,而在有监督的微调过程中又分为基于人工标注对话语料的有监督微调(Supervised Fine Tuning, SFT)过程和基于人类反馈的强化学习微调过程。

图 1. LLaMA 2 训练流程图
图 1. LLaMA 2 训练流程图 [1]

在图1中,奖励模型所处的训练流程便是对 “Llama-2-chat” 输出的内容进行评估得到一个分数,然后这个分数将会作为 RLHF 的输入,最后 RLHF 会根据输入的分数对 “Llama-2-chat” 进行优化,整个流程就是图3中过程①②③①的循环迭代。

同时,在文章「到底是谁提出了大模型中的对齐问题?」中我们提到,通常奖励模型并不是从零开始训练,而是直接使用一个或多个 SFT 过程中的 ChatModel 作为初始化 [1] [3]。这样做的好处是奖励模型可以和 ChatModel 共享在预训练阶段学习到的知识内容(语言理解、事实知识、常识等),从同样的“知识水平”、同一基线出发 [1]。

因此,奖励模型的网络结构整体同预训练模型的架构一致,只是将最后的分类层替换成了一个回归层用于得分的预测

这里值得一提的是,由于 LLaMA 2 训练策略的实际需要,所以整个主模型训练过程中一共使用到了两个奖励模型,一个用于让模型生成更符合人类偏好的回答,一个用于让模型输出更符合安全的回答,但是整个构建原理都是一样的。

1.2 标注样本构建#

为了让最后的 ChatModel 既能够做到输出更符合人类偏好的回答,同时还能够输出更符合安全规范的回答,在 LLaMa 2 的训练过程中 LLaMA 2 团队 一共训练了两个奖励模型。

对于每个奖励模型来说:① 首先让标注人员写出 Prompt;② 然后使用了两个不同版本的 ChatModel( SFT中不同的 Checkpoint 时间点,目的是为了增加输出结果的多样性)再以不同的温度(关于温度对模型的影响可参见文章「起底 DeepSeek 大模型中常用的模型蒸馏技术!从零实现附源代码!」)分别针对同一个 Prompt 生成一个回答;③ 其次,按照标准标注人员必须在两个答案中选择更好的一个;④ 最后人工再标注这个选择相对于另一个的优势程度(显著/中等/略微/几乎没区别)。

按照以上流程,LLaMA 2 团队一共标注了超过 100 万条条数据集,并且还发现这部分的训练数据越多,训练得到的奖励模型效果就越好,最后微调得到的 Llama 2-Chat 效果也就越好。

最后,得到的标注数据格式形如:

Prompt Response A Response B 标注结果
你好! 你好! 你是谁? A 中等好于B
如何制作炸药? 制作炸药首先需要购买…… 该问题涉及公共安全…… B 显著好于 A

介绍完 LLaMA 2 中奖励模型训练数据的形式以后,我们再来具体看如何构建目标函数。

2. LLaMA 2 奖励模型目标函数构建#

2.1 奖励模型目标函数构建#

设奖励模型为 $r_{\theta}(x,y)\in \mathbb{R}$ ,其中 $\theta$ 表示奖励模型对应的参数,$x$ 表示 Prompt ,$y$ 表示 Response,$(x,y)$ 共同作为奖励模型的输入。进一步,我们假设对于同一个 Prompt $x$,根据两个不同版本生成的 Response 分别为 $y_c$ 和 $y_r$,其中 $y_c$ 表示更受人类偏好选择的回答,$y_r$ 则表示被拒绝的回答。

由此可得,$r_{\theta}(x,y_c)$ 为奖励模型对回答 $y_c$ 的打分,$r_{\theta}(x,y_r)$ 为奖励模型对 $y_r$ 的打分。

现在光有了打分,我们如何来训练奖励模型呢?换句话说怎么来根据打分构建目标函数?

由于不同的标注人员对于分数尺度的把控不一样,很有可能同一个样本不同的人标注出的分数会相差很远,所以就需要换一个角度来考虑这个问题。

什么角度呢?

那就是 “我比你好” 这样一个角度。

此时,定义模型认为 $y_c$ 优于 $y_r$ 的概率为 [2] [3]

$$ P_{\theta}(y_c\succ y_r)=\sigma\left(r_{\theta}(x,y_c)-r_{\theta}(x,y_r)\right)\tag{1} $$

其中 $\sigma$ 为 sigmoid 函数,即

$$ \sigma(z)=\frac{1}{1+e^{-z}}\tag{2} $$

此时,根据式(1)可知

① 当 $r_{\theta}(x,y_c)\gg r_{\theta}(x,y_r)$ 时,$P_{\theta}(y_c\succ y_r)$ 接近于1;

② 当 $r_{\theta}(x,y_c)\approx r_{\theta}(x,y_r)$ 时,$P_{\theta}(y_c\succ y_r)$ 接近于0.5;

③ 当 $r_{\theta}(x,y_c)\ll r_{\theta}(x,y_r)$ 时,$P_{\theta}(y_c\succ y_r)$ 接近于0。

所以,我们希望人类偏好选择回答 $y_c$ 的打分比拒绝的回答 $y_r$ 要高,即为 $y_c$ 优于 $y_r$的概率 $P_{\theta}(y_c\succ y_r)$ 约接近于1越好。

进一步,因为 $\sigma(z)$ 的输出在 $(0,1)$ 之间,如果直接最大化这个概率那么将会因为差异太小而导致训练不稳定,同时 sigmoid 在两侧的梯度较小,所以进一步会去负对数,即 [1] [2] [3]

$$ \mathcal{L}_{\text{ranking}}=-\log(\sigma(r_{\theta}(x,y_c)-r_{\theta}(x,y_r)))\tag{3} $$

根据式(3)可知,因为 $P_{\theta}(y_c\succ y_r)$ 的值域为 $(0,1)$,因此概率值越接近于1,那么损失将会趋近于0,而最小化排序损失 $\mathcal{L}_{\text{ranking}}$ 就等价于最大化 $y_c$ 优于 $y_r$的概率,这其实也是 Bradley-Terry 模型。这样,便可以放大错误预测的惩罚,让模型更快学习。

因此,如果有 $m$ 个样本,则式(3)可改写为

$$ \mathcal{L}_{\text{ranking}}=-\sum_{i=1}^m\log(\sigma(r_{\theta}(x^{(i)},y^{(i)}_c)-r_{\theta}(x^{(i)},y^{(i)}_r)))\tag{4} $$

此时你会不会已经开始疑惑了?

明明说奖励模型是一个打分模型,怎么最后却是通过交叉熵来构建目标函数?

关于以上整个目标函数的由来的详细过程可以参见文章「OpenAI的这三篇论文告诉你大模型中奖励模型的变迁过程」

2.2 训练样本构建#

不过还有一点就是,大家有没有好奇在目标函数(4)中,标签是如何体现的?

为了回答这个问题我们先来看下样本的构建过程。

在第1节中我们介绍了样本标注的4个步骤,下面以三个样本为例进行说明。

  • Prompt 1: “请解释一下量子纠缠。”

    模型生成两个回答:A1 和 B1 ;人类标注: A1 显著优于B1。

  • Prompt 2: “请解释一下勾股定理。”

    模型生成两个回答:A2 和 B2 ; 人类标注: B2 略微优于 A2。

  • Prompt 3: “请解释一下大模型。”

    模型生成两个回答:A3 和 B3 ; 人类标注: A3 和 B3 几乎没区别。

在得到如上所示的三个标注样本以后,我们会直接去掉第三种”几乎没区别“这样的样本;其次,对于”显著“、”中等“和”略微“这样的情况会一视同仁,都直接看成一个答案比另一个号;最后,将好的答案定义为 $y_c$,将另一个定义为 $y_r$ 来构建样本,如下所示

To train the reward model, we convert our collected pairwise human preference data into a binary ranking label format (i.e., chosen & rejected) and enforce the chosen response to have a higher score than its counterpart [1].

① $x$ 为”请解释一下量子纠缠。“,$y_c$ 为 A1,$y_r$ 为 B1;

② $x$ 为”请解释一下勾股定理。“,$y_c$ 为 B2,$y_r$ 为 A2;

也就是说,我们在对 $y_c$ 和 $y_r$ 进行定义排序的时候,这就是标注正确标签的过程,所以它不是数值,而是一个偏好顺序 (Preference)。

当然,我们也可以在构建目标函数的时候充分利用答案之间的差别来更好的训练模型。

2.3 目标函数优化改进#

为了在训练奖励模型的过程中得到一个更好的效果,让它更好的区分 $y_c$ 和 $y_r$ ,在式(3)的基础上通常还会再加上一个边际(margin)值,即

$$ \mathcal{L}_{\text{ranking}}=-\log(\sigma(r_{\theta}(x,y_c)-r_{\theta}(x,y_r)-m(r)))\tag{5} $$

其中 $m(r)$ 是一个离散的状态函数,随状态 $r$ 的变化而变化,表示边际值(margin)。

从式子(5)可以看出,在获得同样的置信概率 $P_{\theta}(y_c\succ y_r)$的时候,相较于没有施加边际值的情况,加入边际值以后 $r_{\theta}(x,y_c)$ 和 $r_{\theta}(x,y_r)$ 将会有更大的区分度。

下面我们简单说一下 $m(r)$ 。

直白说就是 $m(r)$ 不是一个固定值,而是对于不同的情况(显著/中等/略微/几乎没区别)给出不同的边际值。例如

$$ m(r)=m_0\alpha(r), \alpha\in\{1.5,1.0,0.5,0\}\tag{6} $$

其中 $m_0$ 是一个基础的边际值,可以理解为“默认的间隔”;$\alpha(r)$是一个调节因子,决定在不同的状态下边际值要不要放大、保持不变、缩小,甚至去掉。例如它是离散取值可以是 $\{1.5,1.0,0.5,0\}$,这意味着边际值为

  • A 显著好于 B 时,放大边际,$m(r)=1.5m_0$;

  • A 中等好于 B 时,保持边际,$m(r)=1.0m_0$;

  • A 略微好于 B 时,减半边际,$m(r)=0.5m_0$;

  • A 几乎没区别于 B 时,无边际,$m(r)=0.0m_0$。

那为什么要这么设定呢?

因为对于不同的样本难度或者类别之间的关系不同,需要动态边际才能更稳定地收敛,即

  • 对难样本 $\implies$ 给大的边际值,强制拉开更大距离。

  • 对简单样本 $\implies$ 给小的边际值,避免过拟合。

所以 $m(s)$ 其实是“自适应的间隔”,不是一刀切。

当然,$m(r)$ 也可以是一个固定的值,保持所有情况下使用同一个边际值。

说到这里大家有没有发现式(5)中的边际值和我们之前介绍过的哪个式子中的值很类似?

想起来了吗?

对,就是「支持向量机 SVM 软间隔当中的松弛变量」 $\xi$ ,即

$$ {{y}^{(i)}}({{w}^{T}}{{x}^{(i)}}+b)\ge 1-{{\xi }_{i}} $$

3. 奖励模型实现#

在介绍完奖励模型的基本原理以后,我们再来简单看一下如何实现这部分内容。

首先,正如我们上面所说奖励模型的网络结构整体同基座模型的架构一致,只是将最后的分类层替换成了一个回归层用于得分的预测,因此我们在训练奖励模型的时候只需要修改最后一个网络层并在载入预训练模型的时候过滤掉最后一层参数即可。

关于模型参数解析和加载这部分内容我们这里就不再赘述,可以参见「模型的迁移学习」这篇文章中的简单示例。

下面,我们来看一下奖励模型的实现过程。

3.1 数据样本构建#

首先,我们先来看一下数据样本的构建过程,以便更好的理解后续实现的代码。

根据第2.2节介绍可知,现在假设有两个样本,如下所示:

texts_c = ["Hello, how are you? I am OK. ",
           "Transformers library makes NLP easier. Yes, ofcourse. "]
texts_r = ["Hello, how are you? Who are you?",
           "Transformers library makes NLP easier. It's a stupid design! "]

在上面的示例中,texts_c 中的元素分别表示两个样本中由同一个 Prompt 生成的更好的回答,texts_r 中的元素则与之相反。

进一步,我们需要对其进行 tokenize、padding 等操作。这里我们可以直接借助 transformers==4.56.2 库中的相关模块来进行实现,示例代码如下:

 1 def make_data(texts_c=None, texts_r=None, margin=None):
 2     tokenizer = GPT2Tokenizer.from_pretrained("./gpt2")
 3     tokenizer.pad_token = tokenizer.eos_token
 4     encodings_c = tokenizer(texts_c, padding=True, truncation=True, return_tensors="pt")
 5     encodings_r = tokenizer(texts_r, padding=True, truncation=True, return_tensors="pt")
 6     inputs_train = {'input_ids_c': encodings_c['input_ids'],
 7                     'attention_mask_c': encodings_c['attention_mask'],
 8                     'input_ids_r': encodings_r['input_ids'],
 9                     'attention_mask_r': encodings_r['attention_mask'],
10                     'margin': margin}
11     return inputs_train, encodings_c

在上述代码中,第2~3行是实例化一个 tokenizer 并指定对应的 pad_token。因为 GPT2Tokenizer 本身没有pad_token,所以通常情况下可以使用 eos_token 进行充当。第4~5行是分别对 texts_ctexts_r 中的样本进行填充和截断。假如模型允许的最大长度为model_max_length = 1024 ,此时:

情况一:现在有两个样本长度分别为10和20,那么就会将长度为10的样本 padding 为20;

情况二:现在有两个样本长度分别为100和1026,那么就会将长度为100的样本 padding 为1026,但同时还会将两个样本都截取为1024的最大长度。

第6~10行是构造整个输入部分,其中attention_mask 这个字段准确来说应该是指 key_padding_mask,即 padding 部分的掩码。第11行将返回处理完成的结果,其中 encodings_c 用于后面我们测试推理时的代码,此时将仅作为打分使用。

此时,可以通过如下方式来运行上述代码:

 1 if __name__ == '__main__':
 2     texts_c = ["Hello, how are you? I am OK. ",
 3                "Transformers library makes NLP easier. Yes, ofcourse. "]
 4     texts_r = ["Hello, how are you? Who are you?",
 5                "Transformers library makes NLP easier. It's a stupid design! "]
 6     margin = torch.tensor([0.1, 0.15], dtype=torch.float)
 7     inputs_train, inputs_infe = make_data(texts_c=texts_c, 
 8     						    texts_r=texts_r, margin=margin)
 9     print(inputs_train)
10     print(inputs_infe)

运行上述代码将会得到如下结果:

{'input_ids_c':
     tensor([[15496, 11, 703, 389, 345, 30, 314, 716, 7477, 13, 220, 50256, 50256, 50256],
             [41762, 364, 5888, 1838, 399, 19930, 4577, 13, 3363, 11, 286, 17319, 13, 220]]),
 'attention_mask_c':
     tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
             [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]),
 'input_ids_r':
     tensor([[15496, 11, 703, 389, 345, 30, 5338, 389, 345, 30, 50256, 50256, 50256, 50256, 50256],
             [41762, 364, 5888, 1838, 399, 19930, 4577, 13, 632, 338, 257, 8531, 1486, 0, 220]]),
 'attention_mask_r':
     tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
             [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'margin': tensor([0.1000, 0.1500])}
{'input_ids':
     tensor([[15496, 11, 703, 389, 345, 30, 314, 716, 7477, 13, 220, 50256, 50256, 50256],
             [41762, 364, 5888, 1838, 399, 19930, 4577, 13, 3363, 11, 286, 17319, 13, 220]]),
 'attention_mask':
     tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
             [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}

3.2 目标函数实现#

进一步,对于目标函数来说它的实现很简单,根据式(5)可知,实现过程如下

 1 def compute_loss(model, inputs):
 2     rewards_c = model(input_ids=inputs["input_ids_c"],
 3                       attention_mask=inputs["attention_mask_c"])["logits"]
 4     rewards_r = model(input_ids=inputs["input_ids_r"],
 5                       attention_mask=inputs["attention_mask_r"])["logits"]
 6     if "margin" in inputs:
 7         loss = -F.logsigmoid(rewards_c - rewards_r - inputs["margin"]).mean()
 8     else:
 9         loss = -F.logsigmoid(rewards_c - rewards_r).mean()
10     return loss

在上述代码中,第1行 model 便是对应的奖励模型,inputs 表示对应的输入部分,不同情况下有所不同后面会举例介绍。同时,因为我们后续使用到的是transformers 库中的相关模型,所以第2、4行 model 返回的结果我们只取 logits 部分即可。第6~9行则是根据式(4)和式(5)来分别构建不同的损失函数。

3.3 奖励模型简洁实现#

最后,来实现奖励模型,这里 GPT2 为例作为基座模型,示例代码如下:

 1 class RewardModel(nn.Module):
 2     def __init__(self, config):
 3         super(RewardModel, self).__init__()
 4         self.rw = GPT2ForSequenceClassification(config)
 5 
 6     def forward(self, inputs=None, ):
 7         if 'input_ids_r' in inputs:  # 训练阶段
 8             loss = compute_loss(self.rw, inputs)
 9             return loss
10         else:
11             score = self.rw(**inputs)
12             return score

在上述代码中,第4行便是定义奖励模型。第7~12行是根据不同的情况返回不同的结果。

看到这里大家是不是会觉得奇怪,为什么第4行中奖励模型可以这样定义,怎么变成了一个分类模型?

这里我们可以简单看一下 GPT2ForSequenceClassification 内部的实现,如下所示:

1 class GPT2ForSequenceClassification(GPT2PreTrainedModel):
2     def __init__(self, config):
3         super().__init__(config)
4         self.num_labels = config.num_labels
5         self.transformer = GPT2Model(config)
6         self.score = nn.Linear(config.n_embd, self.num_labels, bias=False)               

在上述代码中,第4行指定分类的数量。第5行是 GPT2 模型的实现,但是不包含最后一个用于词表分类的分类层。因此,只需要将 config.num_labels 设置为1则就是一个回归层。

当然,此时你一定立马就会返回过来,如过是基于 GPT2 的基座模型,那么其网络结构实现为:

1 class GPT2LMHeadModel(GPT2PreTrainedModel, GenerationMixin):
2     def __init__(self, config):
3         super().__init__(config)
4         self.transformer = GPT2Model(config)
5         self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)

3.4 奖励模型使用示例#

最后,在完成上述代码实现以后,我们可以通过如下方式来进行使用:

 1 if __name__ == '__main__':
 2     batch_size = 3
 3     seq_len = 4
 4     embed_dim = 16
 5     num_heads = 2
 6     config = GPT2Config(n_embd=embed_dim, n_head=num_heads, num_labels=1)
 7     config.pad_token_id = 50526
 8     model = RewardModel(config)
 9     texts_c = ["Hello, how are you? I am OK. ",
10                "Transformers library makes NLP easier. Yes, ofcourse. "]
11     texts_r = ["Hello, how are you? Who are you?",
12                "Transformers library makes NLP easier. It's a stupid design! "]
13     margin = torch.tensor([0.1, 0.15], dtype=torch.float)
14     inputs_train, inputs_infe = make_data(texts_c=texts_c, texts_r=texts_r, margin=margin)
15     reward_model = RewardModel(config)
16     loss = reward_model(inputs_train)
17     loss.backward()
18     score = reward_model(inputs_infe)['logits']
19     print(loss)
20     print(score)

以上就是本次文章的全部内容,感谢阅读,完整示例代码可参见 Code/C09_RM/C03_reward.py 文件。

引用#

[1] Touvron H, Martin L, Stone K, et al. Llama 2: Open foundation and fine-tuned chat models[J]. arXiv preprint arXiv: 2307.09288, 2023.

[2] Ouyang L, Wu J, Jiang X, et al. InstructGPT: Training language models to follow instructions with human feedback[J]. Advances in neural information processing systems, 2022, 35: 27730-27744 .

[3] Nisan Stiennon, Long Ouyang, Jeff Wu, et al. Learning to summarize from human feedback. NIPS ‘20. Curran Associates Inc., Red Hook, NY, USA, Article 253, 3008–3021.

[4] Leandro von Werra and Younes Belkada, et al. TRL: Transformer Reinforcement Learning. 2020, GitHub repository, https://github.com/huggingface/trl

阅读 --

奖励模型中的核心思想 Bradley-Terry 模型

本文是奖励模型与 RLHF 系列的数学前置篇,系统讲清 InstructGPT、LLaMA 2/3、GPT-4 奖励模型目标函数的数学来源 Bradley-Terry 模型。从「成对比较无法直接打分」的直觉问题出发,推导 …