前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >强化学习系列(十)--基于大语言模型的RLHF

强化学习系列(十)--基于大语言模型的RLHF

原创
作者头像
languageX
修改2024-11-27 09:14:37
修改2024-11-27 09:14:37
2360
举报
文章被收录于专栏:强化学习系列

推荐文章:《Linux本地部署开源项目OpenHands基于AI的软件开发代理平台及公网访问

本篇文章介绍如何在 Linux 本地部署开源项目 OpenHands 基于人工智能的软件开发代理平台,并结合 cpolar 实现公网访问,部署强大的开发助手提升你的工作效率。


这篇文章我们回顾了强化学习的理论知识和各种强化学习算法。

这篇文章我们介绍过大语言模型LLM。

本文就介绍将两个知识点结合,让LLM和人类对齐的技术--基于人类反馈的强化学习。

大语言模型训练

大模型训练业界普遍遵循的三部曲:预训练(Pre-training)、微调(Fine-tuning)和对齐(Alignment)。过去两年,行业主要聚焦在Pretraining和SFT上,而基于人类反馈的强化学习(Reinforcement Learning from Human Feedback, RLHF)的框架虽然被广泛讨论也有很多开源框架,但在实际落地应用场景并不算多。今年9月领军模型ChatGPT又推出了o1模型,在逻辑推理和慢思考问题上取得了显著的进展,这也使得强化学习逐渐成为业界的热门话题。最近几个月大家可以看到多个o1复现框架开源。

再次奉上经典pipline图:

descript
descript

先回顾通过pretrain和SFT后我们得到了什么。

pretraiing是给模型读万卷书,这个底座基础模型具有的功能就是续写,通过上文迭代输出下一个token。

SFT是使用问答对对pretraining进行微调,这个模型具有的功能就是可以进行对话。

但是单纯的SFT模型可能会生成语法或者技术上正确但与人类价值观、偏好不符的回答。所以需要第三部RLHF来帮助模型学习更符合人类意图和偏好的回复,更好地与人类的价值观保持一致,即生成更安全合规的回答。

RLHF(基于人类反馈的强化学习)

先套强化学习公式,看看在LLM中的几大元素什么含义:

智能体(Agent):与环境交互、并执行策略的主体。LLM领域那就是LLM本身。

环境(Environment):可以认为是用户提供的prompt和上下文信息。

状态(state):输入给模型的tokens

动作(Action):模型的输出token

奖励(Reward):人类对模型的输出的质量评估

那要进行RLHF是什么流程呢?

再次献上经典deepspeed的RLHF流程图:

descript
descript

众所知周,RLHF-PPO需要四个模型,那到底是哪四个模型,分别是干什么的,四个模型什么关系,如何将PPO应用到RLHF里面?不慌,一步一步看~

本文主要基于开源代码LLM-turning进行学习。

2.1 step1:SFT

这一步不做过多介绍,基于问答对训练一个对话模型。可以使用开源模型,比如Qwen-7B-Chat进行微调。

2.2 step2:RW

这一步就是使用pari goog/bad answers进行训练得到一个奖励模型,简单来说是训练一个分类模型来判断模型回复的好坏。

训练过程中我们需要准备的数据示例:

descript
descript

其中chosen代表较好的回复,rejected表示较差的回复。

核心实现代码:

代码语言:python
代码运行次数:0
复制
# 处理数据:tokenized_j好的回复,tokenized_k差的回复
for question, response_j, response_k in zip(examples[question_key], examples[good_key], examples[bad_key]):
        tokenized_j = tokenizer("问:" + question + "\n\n答:" + response_j, truncation=True)
        tokenized_k = tokenizer("问:" + question + "\n\n答:" + response_k, truncation=True)
...
# Define how to compute the reward loss. We use the InstructGPT pairwise logloss: https://arxiv.org/abs/2203.02155
    # 由于训练数据都是好坏成对出现,所以这里两个输入两个输出
    def compute_loss(self, model, inputs, return_outputs=False):
        rewards_j = model(input_ids=inputs["input_ids_j"], attention_mask=inputs["attention_mask_j"])[0]
        rewards_k = model(input_ids=inputs["input_ids_k"], attention_mask=inputs["attention_mask_k"])[0]
        loss = -nn.functional.logsigmoid(rewards_j - rewards_k).mean()
        if return_outputs:
            return loss, {"rewards_j": rewards_j, "rewards_k": rewards_k}
        return loss

2.3 step3: PPO

这一步比较复杂,通过上图结合前篇文章我们介绍的PPO算法思路,我们基于代码来分析整个流程和思路。

先看PPO训练过程中需要的四个模型分别是什么:

descript
descript

Reference model(参考模型): 其实就是step1中的SFT模型冻结了参数作为refModel,主要用于指导Actor model,让策略模型和ref模型在分布上差距不要太大,防止Actor model训歪。

Reward Model(奖励模型):其实就是step2中的RM模型冻结了参数作,用于计算策略生成token的即时奖励。

Actor Model(演员模型):就是我们要训练的目标策略模型,一般使用step1的SFT作为初始化。

Critic Model(评论家模型):用于预测策略生成token的累计回报。因为critic和actor的输入一致,所以可以和Actor Model共享参数,然后增加一层value head作为预测累计回报值。

2.3.1 加载模型

在LLM-Tuning项目中,Actor Model和 Critic Model是共享参数,然后新增value head层来预测回报V。

AutoModelForCausalLMWithValueHead核心代码:

代码语言:python
代码运行次数:0
复制
...
lm_logits = base_model_output.logits
loss = base_model_output.loss
...
# 增加一层v_head
value = self.v_head(last_hidden_state).squeeze(-1)
...
return (lm_logits, loss, value)

所以在项目中加载实际是需要3个模型,冻结的Reference Model, Reward Model,需要训练的PPO Model。只是这个Model有两个分支任务:Actor 和 Critic。

加载Reference_model:

代码语言:python
代码运行次数:0
复制
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(
    script_args.merged_sft_model_path,
    trust_remote_code=True
)

加载Rewar_model:

代码语言:python
代码运行次数:0
复制
base_model_for_RM = BaichuanForSequenceClassification.from_pretrained(
    script_args.base_model_name, num_labels=1, 
    trust_remote_code=True, 
    torch_dtype=torch.bfloat16, 
    device_map="auto",
    # device_map={"": current_device},
)
reward_model = PeftModel.from_pretrained(base_model_for_RM, script_args.reward_model_lora_path)

加载ppo_model代码也比较简单,1加载基础模型,2加载sft后 lora模型,3适配了ValueHead的ppo_model:

代码语言:python
代码运行次数:0
复制
# load the base model
base_model_for_PPO = AutoModelForCausalLM.from_pretrained(
    script_args.base_model_name,
    trust_remote_code=True,
    torch_dtype=torch.bfloat16, 
    device_map='auto'
    )
# install the lora modules
base_model_for_PPO_with_sft_lora = PeftModel.from_pretrained(
    base_model_for_PPO, 
    script_args.sft_model_lora_path
    )
# wrap with the AutoModelForCausalLMWithValueHead wrapper
ppo_model = AutoModelForCausalLMWithValueHead.from_pretrained(
    base_model_for_PPO_with_sft_lora
)

2.3.2 训练流程

我们可以,直接使用trl库,训练脚本其实比较简单:

代码语言:python
代码运行次数:0
复制
...
response_tensors = ppo_trainer.generate(
            question_tensors,
            return_prompt=False,
            **generation_kwargs,
        )
        batch["response"] = tokenizer.batch_decode(response_tensors, skip_special_tokens=True)

        # Compute sentiment score
        texts = [q + r for q, r in zip(batch["query"], batch["response"])]
        scores = get_reward_value(texts)
        rewards = [torch.tensor(score - script_args.reward_baseline) for score in scores]
        for q, r, s in zip(batch["query"], batch["response"], scores):
            print(epoch,'query:',q)
            print('response:',r)
            print('score:',s)
        # Run PPO step
        stats = ppo_trainer.step(question_tensors, response_tensors, rewards)

如果不分析其中实现逻辑,准备好数据,直接使用trl库就可以开始训练。

训练代码实现就需要去trl里面查看源码, 下面我们结合核心代码理解流程。

首先使用策略网络生成一批数据,这里的unwrapped_model.policy就是Actor_model:

代码语言:python
代码运行次数:0
复制
 # actor网络预测responses和动作logits
 query_responses, logitss = batch_generation(
                        unwrapped_model.policy,
                        queries,
                        args.local_rollout_forward_batch_size,
                        processing_class.pad_token_id,
                        generation_config,
                    )
代码语言:python
代码运行次数:0
复制
all_logprob = F.log_softmax(logits, dim=-1)
# logporb:actor网络预测response的概率分布
logprob = torch.gather(all_logprob, 2, response.unsqueeze(-1)).squeeze(-1)
...
ref_output = forward(ref_policy, query_response, processing_class.pad_token_id)
...
ref_all_logprob = F.log_softmax(ref_logits, dim=-1)
# ref_logprob:ref网络预测response的概率分布
ref_logprob = torch.gather(ref_all_logprob, 2, response.unsqueeze(-1)).squeeze(-1)
代码语言:python
代码运行次数:0
复制
# critic网络预测的状态值value(累计rewards)
full_value, _, _ = get_reward(unwrapped_value_model, query_response, processing_class.pad_token_id, context_length)
value = full_value[:, context_length - 1 : -1].squeeze(-1)

# reward网络计算的score(T时刻即刻回报reward)
_, score, _ = get_reward(reward_model, postprocessed_query_response, processing_class.pad_token_id, context_length )

通过以上步骤(其中很多mask,filter等细节被省略),我们得到一批数据:

代码语言:python
代码运行次数:0
复制
# 决策模型的回复
responses = torch.cat(responses, 0)
# 处理response,哪些参与loss计算
postprocessed_responses = torch.cat(postprocessed_responses, 0)
# actor模型预测的动作概率分布
logprobs = torch.cat(logprobs, 0)
# ref模型预测的动作概率分布
ref_logprobs = torch.cat(ref_logprobs, 0)
# 序列长度
sequence_lengths = torch.cat(sequence_lengths, 0)
# reward模型计算的即刻reward
scores = torch.cat(scores, 0)
# critic模型预测的累计V
values = torch.cat(values, 0)

结合之前学习的知识点,构建优势函数:

代码语言:python
代码运行次数:0
复制
# 4. compute rewards
# actor和 ref输出分布的KL散度 * 非负权重
kl = logprobs - ref_logprobs
non_score_reward = -args.kl_coef * kl
# 每个时刻的reward使用kl散度表示。
# 这里解释下,由于rewardmodel训练是基于input+response一起的分类模型,所以分类结果最为最后一个时刻的奖励R,其他时刻是没有R。这里是使用KL散度,让决策和ref分布靠近来作为即可奖励。
rewards = non_score_reward.clone()
actual_start = torch.arange(rewards.size(0), device=rewards.device)
actual_end = torch.where(sequence_lengths_p1 < rewards.size(1), sequence_lengths_p1, sequence_lengths)
# 最后时刻的R就是reward model的score
rewards[[actual_start, actual_end]] += scores

....

# 6. compute advantages and returns
lastgaelam = 0
advantages_reversed = []
gen_length = responses.shape[1]
# 这里是使用了泛优势函数GAE,从最后时刻逆序计算
for t in reversed(range(gen_length)):
        nextvalues = values[:, t + 1] if t < gen_length - 1 else 0.0
        # TD:t时刻即可回报+gamma*下时刻的累计回报- t时刻累计回报
        delta = rewards[:, t] + args.gamma * nextvalues - values[:, t]
        # GAE的更新公式
        lastgaelam = delta + args.gamma * args.lam * lastgaelam
        advantages_reversed.append(lastgaelam)
advantages = torch.stack(advantages_reversed[::-1], axis=1)
# 优势+状态值估计
returns = advantages + values
advantages = masked_whiten(advantages, ~padding_mask)
advantages = torch.masked_fill(advantages, padding_mask, 0)

通过以上步骤,我们生成了一个batch的经验值数据,基于在线学习方法,就可以对数据重复利用,基于这一批数据训练进行多次ppo训练,训练过程中的loss核心代码如下:

代码语言:python
代码运行次数:0
复制
logprobs_diff = new_logprobs - mb_logprobs
ratio = torch.exp(logprobs_diff)
pg_losses = -mb_advantage * ratio
pg_losses2 = -mb_advantage * torch.clamp(ratio, 1.0 - args.cliprange, 1.0 + args.cliprange)
pg_loss_max = torch.max(pg_losses, pg_losses2)
pg_loss = masked_mean(pg_loss_max, ~padding_mask[micro_batch_inds])
loss = pg_loss + args.vf_coef * vf_loss

这里使用的重要知识点包括重要性采样,KL散度,优势函数,clip操作等之前的系列文章已经讲解非常清楚,本文就不再过多介绍。

以上就是基于人类反馈的强化学习PPO算法的核心思路,相信通过代码的理解,希望本文能让大家对PPO在LLM中的使用有更深刻的理解~

也欢迎大家留言讨论~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 大语言模型训练
  • RLHF(基于人类反馈的强化学习)
    • 2.1 step1:SFT
      • 2.2 step2:RW
        • 2.3 step3: PPO
          • 2.3.1 加载模型
          • 2.3.2 训练流程
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档