InstructGPT:让人工智能更听话的技术
InstructGPT 是一种训练大型语言模型(就像 GPT-3)的技术,让它们更好地理解人类的指令,并给出更符合我们期望的回答。这种技术的核心是基于人类反馈的强化学习 (RLHF)。简单来说,就是让人来告诉模型,什么样的回答是好的,然后模型通过学习不断改进自己。
InstructGPT 的训练过程可以分为三个主要步骤:
-
监督微调 (SFT)
-
原理:先准备一批高质量的"问答"数据,这些"问答"不是随便写的,而是人根据特定的指令认真编写的。然后,用这些数据来"教"预训练好的 GPT-3 模型,让它学会理解各种指令,并给出对应的回答。
-
实际应用例子: 假设我们要训练一个可以生成菜谱的模型。
-
指令:"写一个西红柿炒鸡蛋的菜谱"。
-
人工编写的期望输出:
markdown菜名:西红柿炒鸡蛋 材料:西红柿2个,鸡蛋3个,葱少许,盐适量,糖少许,食用油适量 步骤: 1. 西红柿洗净切块,鸡蛋打散加少许盐。 2. 葱切葱花。 3. 热锅冷油,倒入鸡蛋液炒至凝固盛出。 4. 锅中再放少许油,放入西红柿块翻炒至软烂。 5. 倒入炒好的鸡蛋,加入盐和糖调味,翻炒均匀。 6. 撒上葱花即可出锅。
-
然后用大量类似的"指令-菜谱"数据来微调 GPT-3 模型。
-
-
Demo 代码 :虽然直接微调 GPT-3 这样的模型需要大量的计算资源,但我们可以用更小的模型和数据集来演示这个过程。比如,使用 Hugging Face 的
transformers
库:pythonfrom transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments # 加载预训练模型和 tokenizer model_name = "distilgpt2" # 使用一个较小的 GPT-2 模型作为例子 model = AutoModelForCausalLM.from_pretrained(model_name) tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token # 准备训练数据 train_data = [ {"instruction": "写一个西红柿炒鸡蛋的菜谱", "output": "菜名:西红柿炒鸡蛋\n材料:西红柿2个,鸡蛋3个...\n步骤:1. 西红柿洗净切块...\n"}, {"instruction": "写一个宫保鸡丁的菜谱", "output": "菜名:宫保鸡丁\n材料:鸡胸肉200克,花生米...\n步骤:1. 鸡胸肉切丁...\n"} ] # 格式化数据 def format_data(data_point): text = "Instruction: " + data_point["instruction"] + "\nOutput: " + data_point["output"] return text train_encodings = tokenizer([format_data(item) for item in train_data], truncation=True, padding=True, return_tensors="pt") # 定义 Trainer training_args = TrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=1, logging_dir="./logs", ) class RecipeDataset: def __init__(self, encodings): self.encodings = encodings def __getitem__(self, idx): return {key: val[idx].clone().detach() for key, val in self.encodings.items()} def __len__(self): return len(self.encodings.input_ids) train_dataset = RecipeDataset(train_encodings) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, tokenizer=tokenizer, ) # 开始训练 trainer.train() # 使用模型生成菜谱 def generate_recipe(instruction): input_text = "Instruction: " + instruction + "\nOutput: " input_ids = tokenizer.encode(input_text, return_tensors="pt") output = model.generate(input_ids, max_length=200, num_return_sequences=1, no_repeat_ngram_size=2) recipe = tokenizer.decode(output[0], skip_special_tokens=True) return recipe instruction = "写一个麻婆豆腐的菜谱" recipe = generate_recipe(instruction) print(recipe)
- 代码解释 :
- 首先,我们加载了一个预训练的
distilgpt2
模型和一个 tokenizer。 - 然后,我们准备了一些训练数据,包括指令和对应的输出。
- 我们定义了一个
format_data
函数,将指令和输出拼接成一个字符串,作为模型的输入。 - 我们使用 tokenizer 将训练数据编码成模型可以理解的格式。
- 我们定义了一个
RecipeDataset
类,用于加载和处理训练数据。 - 我们使用
Trainer
类来训练模型。 - 最后,我们定义了一个
generate_recipe
函数,用于使用模型生成菜谱。
- 首先,我们加载了一个预训练的
- 代码解释 :
-
-
训练奖励模型 (RM)
-
原理:让模型针对同一个指令生成多个不同的回答,然后让人对这些回答进行排序,选出最好的。奖励模型通过学习人类对不同模型输出的偏好来进行训练。
-
实际应用例子: 仍然以菜谱生成为例。
- 指令:"写一个西红柿炒鸡蛋的菜谱"。
- 模型生成了三个不同的菜谱:
- 菜谱 1:步骤比较简单,但描述不够详细。
- 菜谱 2:步骤非常详细,但有些步骤过于繁琐。
- 菜谱 3:步骤适中,描述也比较清晰。
- 人工对这三个菜谱进行排序:菜谱 3 > 菜谱 2 > 菜谱 1。
- 然后用大量类似的排序数据来训练奖励模型,让它学会判断哪个菜谱更好。
-
Demo 代码:训练奖励模型也需要大量的数据和计算资源。我们可以用一个简单的例子来演示这个过程。
pythonimport torch import torch.nn as nn import torch.optim as optim # 定义奖励模型 class RewardModel(nn.Module): def __init__(self, input_size): super(RewardModel, self).__init__() self.linear1 = nn.Linear(input_size, 64) self.linear2 = nn.Linear(64, 1) def forward(self, x): x = torch.relu(self.linear1(x)) x = self.linear2(x) return x # 准备训练数据 # 假设我们已经有了模型生成的多个菜谱的 embedding,以及人工对这些菜谱的排序 recipe1_embedding = torch.randn(1, 10) # 假设 embedding 的维度是 10 recipe2_embedding = torch.randn(1, 10) recipe3_embedding = torch.randn(1, 10) # 假设人工排序是 recipe3 > recipe2 > recipe1 # 我们需要将排序转换成奖励值 reward1 = 1.0 # 最差的菜谱的奖励值 reward2 = 2.0 reward3 = 3.0 # 最好的菜谱的奖励值 # 定义损失函数 loss_fn = nn.MarginRankingLoss(margin=1.0) # 使用 MarginRankingLoss 来学习排序 # 定义模型和优化器 model = RewardModel(10) optimizer = optim.Adam(model.parameters(), lr=0.001) # 训练模型 for i in range(100): optimizer.zero_grad() # 计算每个菜谱的奖励值 score1 = model(recipe1_embedding) score2 = model(recipe2_embedding) score3 = model(recipe3_embedding) # 使用 MarginRankingLoss 计算损失 loss = loss_fn(score3, score2, torch.tensor([1])) + loss_fn(score2, score1, torch.tensor([1])) # 反向传播和更新参数 loss.backward() optimizer.step() if i % 10 == 0: print(f"Epoch {i}, Loss: {loss.item()}") # 使用奖励模型预测菜谱的奖励值 print(f"Recipe 1 Reward: {model(recipe1_embedding).item()}") print(f"Recipe 2 Reward: {model(recipe2_embedding).item()}") print(f"Recipe 3 Reward: {model(recipe3_embedding).item()}")
- 代码解释 :
- 首先,我们定义了一个简单的奖励模型,它接受一个菜谱的 embedding 作为输入,并输出一个奖励值。
- 然后,我们准备了一些训练数据,包括模型生成的多个菜谱的 embedding,以及人工对这些菜谱的排序。
- 我们将人工排序转换成奖励值,最好的菜谱的奖励值最高,最差的菜谱的奖励值最低。
- 我们使用
MarginRankingLoss
作为损失函数,它可以学习排序关系。 - 我们使用 Adam 优化器来训练模型。
- 最后,我们使用训练好的奖励模型来预测菜谱的奖励值。
- 代码解释 :
-
-
强化学习 (RL)
-
原理:使用奖励模型作为"裁判",让模型不断尝试生成新的回答。如果生成的回答能够获得更高的奖励,就说明这个回答更好,模型就会学习并记住这种回答方式。同时,为了防止模型"跑偏",我们会限制它不要偏离原始预训练模型的预测太远。
-
实际应用例子: 仍然以菜谱生成为例。
- 模型生成一个新的菜谱。
- 奖励模型对这个菜谱进行评分。
- 如果奖励很高,就说明这个菜谱很好,模型就会学习并记住这种生成菜谱的方式。
- 如果奖励很低,就说明这个菜谱不好,模型就会尝试生成新的菜谱。
- 同时,为了防止模型生成一些不靠谱的菜谱(比如用奇怪的食材),我们会限制它不要偏离原始预训练模型的预测太远。
-
Demo 代码:强化学习的训练过程非常复杂,需要大量的计算资源。我们可以用一个简单的例子来演示这个过程。
pythonimport torch import torch.nn as nn import torch.optim as optim from torch.distributions import Categorical # 定义策略模型(Policy Model),这里简化为一个简单的线性模型 class PolicyModel(nn.Module): def __init__(self, input_size, output_size): super(PolicyModel, self).__init__() self.linear = nn.Linear(input_size, output_size) def forward(self, x): x = torch.softmax(self.linear(x), dim=-1) # 输出概率分布 return x # 模拟奖励模型(Reward Model),这里简化为一个函数 def reward_model(action): # 假设 action 是一个 one-hot 向量,代表选择的菜谱 # 这里简单地给不同的菜谱不同的奖励 if action == 0: # 假设 action 0 是一个好的菜谱 return 1.0 elif action == 1: return 0.5 else: return 0.0 # 超参数 input_size = 10 # 假设输入状态的维度是 10 output_size = 3 # 假设有 3 个可能的动作(菜谱) learning_rate = 0.01 gamma = 0.99 # 折扣因子 # 初始化模型和优化器 policy_model = PolicyModel(input_size, output_size) optimizer = optim.Adam(policy_model.parameters(), lr=learning_rate) # 训练循环 num_episodes = 100 for episode in range(num_episodes): # 模拟一个 episode state = torch.randn(1, input_size) # 初始状态 log_probs = [] rewards = [] actions = [] for t in range(10): # 每个 episode 运行 10 步 # 1. 策略模型根据当前状态输出一个动作的概率分布 probs = policy_model(state) m = Categorical(probs) # 创建一个类别分布 action = m.sample() # 根据概率分布采样一个动作 log_prob = m.log_prob(action) # 计算这个动作的对数概率 # 2. 执行动作,获得奖励 reward = reward_model(action.item()) # 3. 记录 log_prob 和 reward log_probs.append(log_prob) rewards.append(reward) actions.append(action) # 4. 更新状态(这里简化为随机更新) state = torch.randn(1, input_size) # 计算 discounted rewards discounted_rewards = [] R = 0 for r in reversed(rewards): R = r + gamma * R discounted_rewards.insert(0, R) discounted_rewards = torch.tensor(discounted_rewards) # discounted_rewards = (discounted_rewards - discounted_rewards.mean()) / (discounted_rewards.std() + 1e-8) # 标准化 # 计算 loss policy_loss = [] for log_prob, R in zip(log_probs, discounted_rewards): policy_loss.append(-log_prob * R) policy_loss = torch.cat(policy_loss).sum() # 反向传播和优化 optimizer.zero_grad() policy_loss.backward() optimizer.step() # 打印 episode 信息 print(f"Episode {episode}, Total Reward: {sum(rewards)}, Policy Loss: {policy_loss.item()}")
- 代码解释 :
- 首先,我们定义了一个简单的策略模型
PolicyModel
,它根据当前状态(state)输出一个动作(action)的概率分布。 - 我们定义了一个模拟的奖励模型
reward_model
,它根据选择的动作(菜谱)给出奖励。 - 在训练循环中,我们模拟多个 episodes。
- 在每个 episode 中,策略模型根据当前状态输出一个动作的概率分布,然后我们根据这个概率分布采样一个动作。
- 我们执行这个动作,获得奖励,并记录 log_prob 和 reward。
- 然后,我们计算 discounted rewards,也就是考虑了折扣因子的未来奖励。
- 我们使用 discounted rewards 来计算 policy loss,并使用反向传播来更新策略模型的参数。
- 首先,我们定义了一个简单的策略模型
- 代码解释 :
-
通过这三个步骤,InstructGPT 能够更好地理解用户意图,生成更符合人类偏好的输出。虽然它还不能做到完美,但它代表了人工智能发展的一个重要方向:让人工智能真正为人类服务。