简略来说,ChatGPT 是自然语言解决(NLP)和强化学习(RL)的一次胜利联合,思考到读者可能只相熟其中一个方向或者两个方向都不太熟悉,本文会将 ChatGPT 波及到的所有知识点尽可能通俗易懂的形式展示进去,有根底的同学能够选择性跳过一些内容。
一、GPT 的进化史
本节的次要目标是介绍自然语言解决中语言模型的一些基础知识,了解语言模型到底在做什么。
1.1 GPT
所谓的 GPT(Generative Pre-trained Transformer),其实是 Generative Pre Training of a language model(语言模型)。那什么是语言模型呢?能够简略地把语言模型了解为“给定一些字或者词,预测下一个字或者词的模型”,这里的字或者词在 NLP 畛域通常也被称为 token,即给定已有 token,预测下一个 token 的模型,这里举个例子,咱们在搜索引擎里进行搜寻时,主动会往后联想就是种语言模型的体现。
那么训练语言模型有什么劣势呢?答案就是它 不须要人工标注数据! 比方以“today is a good day”为例,它能够被拆解为:
接下来,让咱们来数学化地形容一下,给定一个句子,比方,语言模型其实就是想最大化:
【其中 k 是思考的窗口长度,条件概率 p 通过一个参数为 O 的神经网络来形容。
GPT 的神经网络采纳了一个多层的 Transformer decoder,输出通过 embedding 层(token embedding 叠加 position embedding),而后过多层解码器,最初通过一个 position-wise 的前向网络失去输入的散布:
有了模型构造,有了指标函数,曾经能够预训练一个大容量的语言模型了,这也就是 GPT 的第一阶段,在 GPT 的训练流程里还有第二个阶段,利用一些有标签数据进行微调。假如输出为,标签为【,】能够将输出喂入模型,模型的输入再叠加一个线性层作为最终的输入:
指标函数也就是:
然而作者在微调时还发现,同时思考语言模型的自回归指标函数成果更好,也就是:
在微调阶段,能够优化的参数只有顶部的线性层曾经用作分隔符的 token embedding。下图展现的就是 GPT 做微调时对文本的一些常见做法,其实就是 拼接和加宰割符 之类的操作,如下图。
1.2 GPT2
GPT1 须要对特定工作再进行精调(依赖有标签数据进行监督学习),而 GPT2 则是思考在预训练时思考各种不同的工作,也就更加通用化。因而,GPT2 的模型从本来 GPT1 的:
改为 task conditioning 的模式。
也就是把工作也作为模型的输出,具体的做法是引入一些示意工作的 token,举几个例子。
- 自回归工作 input:Today is a output:good
- 翻译工作 input:Today is a [翻译为中文] output:明天是一个
- 问答工作 input:我是小明 [问题] 我是谁 [答案] output:小明下面例子中 [翻译为中文]、[问题]、[答案] 这些就是用于通知模型执行什么工作的 token。
通过这样的形式,各种工作都能塞进预训练里进行了,想学的越多,模型的容量天然也须要更大,GPT2 的参数量达到了 1.5 Billions(GPT1 仅 117 Millions)。
1.3 GPT3
GPT3 能够了解为 GPT2 的升级版,应用了 45TB 的训练数据,领有 175B 的参数量,真正诠释了什么叫暴力出奇观。
GPT3 次要提出了两个概念:
- 情景(in-context)学习:就是对模型进行疏导,教会它该当输入什么内容,比方翻译工作能够采纳输出:请把以下英文翻译为中文:Today is a good day。这样模型就可能基于这一场景做出答复了,其实跟 GPT2 中不同工作的 token 有殊途同归之妙,只是表白更加欠缺、更加丰盛了。
- Zero-shot, one-shot and few-shot:GPT3 打出的口号就是“辞别微调的 GPT3”,它能够通过不应用一条样例的 Zero-shot、仅应用一条样例的 One-shot 和应用大量样例的 Few-shot 来实现推理工作。上面是比照微调模型和 GPT3 三种不同的样本推理模式图。
二、ChatGPT
ChatGPT 应用了相似 InstructGPT 的形式来训练模型,该办法也叫做 Learning from Human Feedback。次要分为三个步骤:
- 用有监督数据精调 GPT-3.5;
- 对于模型输入的候选后果(因为采样会导致同一输出有不同输入)进行打分,从而训练失去一个处分模型;
- 应用这个处分模型,用 PPO 算法来进一步对模型进行训练。
2.1 GPT 是如何训练的
接下来咱们来入手实际一下如何训练一个 GPT 模型进去,以从头训练一个代码补全的 GPT 模型为例。比方咱们给模型一个提醒,而后就能输入提醒。
from transformers import AutoTokenizer, AutoModelForSequenceClassification
# build a BERT classifier
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
model = AutoModelForSequenceClassification.from_pretrained('bert-base-uncased')
为了训练这样一个模型,首先咱们须要筹备用于训练的数据,常见的代码补全的数据为 codeparrot,下载链接:https://huggingface.co/codepa…。这里轻易打印一条数据,如下图。
然而,模型是不能间接接管这样的“文本”信息的,所以训练 NLP 模型前通常须要对其进行“分词”,转化为由一串数字示意,能够创立一个分词器:
tokenizer = AutoTokenizer.from_pretrained("./code-search-net-tokenizer")
对下面的代码进行分词转化,就能够失去如下的一串 id:
[3, 41082, 17023, 26, 11334, 13, 24, 41082, 173, 2745, 756, 173, 2745, 4397, 173, 2745, 1893, 173, 2745, 3857, 442, 2604, 173, 973, 7880, 978, 3399, 173, 973, 10888, 978, 4582, 173, 173, 973, 309, 65, 552, 978, 6336, 4391, 173, 295, 6472, 8, ...
下面的例子展现了对单条样本进行分词的后果;通常咱们会把分词函数定义好,而后间接对整个数据集进行 map 就能够对整个数据集进行分词了。
def tokenize(element):
outputs = tokenizer(element["content"],
truncation=True,
max_length=context_length,
return_overflowing_tokens=True,
return_length=True,
)
input_batch = []
for length, input_ids in zip(outputs["length"], outputs["input_ids"]):
if length == context_length:
input_batch.append(input_ids)
return {"input_ids": input_batch}
tokenized_datasets = raw_datasets.map(tokenize, batched=True, remove_columns=raw_datasets["train"].column_names
)
搞定数据后,接下来就须要创立(初始化)一个模型了,GPT 的构造其实就是由 transformer 组成的,网上的轮子曾经很多了,这里就不从新造轮子了,最常见的间接用的 transformers 库,通过配置的形式就可能疾速定义一个模型进去了。
from transformers import AutoTokenizer, GPT2LMHeadModel, AutoConfig
config = AutoConfig.from_pretrained(
"gpt2",
vocab_size=len(tokenizer),
n_ctx=context_length,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
)
model = GPT2LMHeadModel(config)
model_size = sum(t.numel() for t in model.parameters())
print(f"GPT-2 size: {model_size/1000**2:.1f}M parameters")
这是一个 124.2M 模型参数的 GPT2,模型的代码构造打给大家看看(具体的代码实现能够浏览 transformers 库的源码),其实次要就是后面有个 embedding 层,两头 12 个 transformer block,最初有个线性层。
GPT2LMHeadModel((transformer): GPT2Model((wte): Embedding(50000, 768)
(wpe): Embedding(1024, 768)
(drop): Dropout(p=0.1, inplace=False)
(h): ModuleList((0): GPT2Block((ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(attn): GPT2Attention((c_attn): Conv1D()
(c_proj): Conv1D()
(attn_dropout): Dropout(p=0.1, inplace=False)
(resid_dropout): Dropout(p=0.1, inplace=False)
)
(ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(mlp): GPT2MLP((c_fc): Conv1D()
(c_proj): Conv1D()
(act): NewGELUActivation()
(dropout): Dropout(p=0.1, inplace=False)
)
)
#################
两头省略反复的 10 层 Block
#################
(11): GPT2Block((ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(attn): GPT2Attention((c_attn): Conv1D()
(c_proj): Conv1D()
(attn_dropout): Dropout(p=0.1, inplace=False)
(resid_dropout): Dropout(p=0.1, inplace=False)
)
(ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(mlp): GPT2MLP((c_fc): Conv1D()
(c_proj): Conv1D()
(act): NewGELUActivation()
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
(ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
)
(lm_head): Linear(in_features=768, out_features=50000, bias=False)
)
这里也给出 GPT2 和 GPT 的模型结构图,感兴趣的同学能够认真看看,能够发现,GPT2 的模型构造 (右) 较 GPT 的模型构造 (左) 有所改变。在 GPT2 中的一个 Transformer Block 层中,第一个 LayerNormalization 模块被移到了
Msaked-Multi-Self-Attention 模块之前, 第二个 LayerNormalization 模块也被移到了 Feed-Forward 模块之前;同时 Residual-connection 的地位也调整到了 Msaked-Multi-Self-Attention 模块与 Feed-Forward 模块之后。
数据和模型构造都确定下来后,接下来咱们须要有一个训练的流程或者框架,最简便的那就是间接调用 transformers 提供的训练器,给定一些配置,模型、分词器、数据集。
from transformers import Trainer, TrainingArguments
args = TrainingArguments(
output_dir="codeparrot-ds",
per_device_train_batch_size=32,
per_device_eval_batch_size=32,
evaluation_strategy="steps",
eval_steps=5_000,
logging_steps=5_000,
gradient_accumulation_steps=8,
num_train_epochs=1,
weight_decay=0.1,
warmup_steps=1_000,
lr_scheduler_type="cosine",
learning_rate=5e-4,
save_steps=5_000,
fp16=True,
push_to_hub=True,
)
trainer = Trainer(
model=model,
tokenizer=tokenizer,
args=args,
data_collator=data_collator,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["valid"],
)
接下来,就能够开始训练了:
trainer.train()
自由度高一点的训练形式也能够自行打造,顺次拿到每个 batch 数据、送入模型、计算 loss、反向流传;对于大模型来说,常见的用 accelerate 库来进行减速,比方混合精度、梯度累积等操作。
上述的这些代码(应用训练器或者 accelerate 库进行训练)在 transformers 的官网教程里都有,代码链接:https://huggingface.co/course…。
训练完模型后咱们能够来看一下它的代码生成能力,那就先来跟大家 hello world 一下。比方给定 prompt:
def print_hello_world():
"""Print'Hello World!'."""
失去的后果如下:
def print_hello_world():
"""Print'Hello World!'."""
print('Hello World!')
接下来,咱们给定 prompt:
import numpy as np
from sklearn.ensemble import RandomForestClassifier
# create training data
X = np.random.randn(100, 100)
y = np.random.randint(0, 1, 100)
# setup train test split
它可能帮咱们划分训练和测试数据集:
import numpy as np
from sklearn.ensemble import RandomForestClassifier
# create training data
X = np.random.randn(100, 100)
y = np.random.randint(0, 1, 100)
# setup train test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
这一节中咱们走了一遍训练 GPT 的一个流程,这里和训练 ChatGPT 的第一步的差异在于:ChatGPT 第一步采纳人工写答案的形式失去的语料对预训练好的 GPT 进行了精调,而本节只是对一个小语料进行了一波预训练。
2.2 训练一个处分模型
接下来,咱们看一下训练 ChatGPT 的第二步:处分模型。
事实上,处分模型的学习实质就是一个句子分类或者回归的工作。这里找一个数据集来展现一下整个流程,比方 IMDB 影评数据集,罕用于情感剖析,也就是给文本的情感打分,输出就是影评,输入就是得分(踊跃或消极对应 1 或 0),那就能训练一个处分模型了(比方能够用来领导 GPT 生成得分越高,也就是更加踊跃的文本)。
首先,导入一下数据集,下载链接:https://ai.stanford.edu/~amaa…。
def read_imdb_split(split_dir):
split_dir = Path(split_dir)
texts = []
labels = []
for label_dir in ["pos", "neg"]:
for text_file in (split_dir/label_dir).iterdir():
texts.append(text_file.read_text(encoding='utf-8'))
labels.append(0 if label_dir is "neg" else 1)
return texts, labels
train_texts, train_labels = read_imdb_split("./DataSmall/aclImdb/train")
test_texts, test_labels = read_imdb_split("./DataSmall/aclImdb/test")
失去如下的文本,一段文本对应一个标签:
![]()
同样的咱们须要对其进行分词,转为一串 id,定义分词器,并结构数据集:
print("Tokenizing train, validate, test text")
tokenizer = DistilBertTokenizerFast.from_pretrained('distilbert-base-uncased')
train_encodings = tokenizer(train_texts, truncation=True, padding=True)
test_encodings = tokenizer(test_texts, truncation=True, padding=True)
class IMDbDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item['labels'] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
print("Loading tokenized text into Pytorch Datasets")
train_dataset = IMDbDataset(train_encodings, train_labels)
test_dataset = IMDbDataset(test_encodings, test_labels)
train_loader = DataLoader(train_dataset, batch_size=10, shuffle=True)
接下来,定义一个用于预测得分的模型以及优化器。
print("Loading pre-trained DistilBERT model")
model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased')
model.to(device)
model.train()
optim = AdamW(model.parameters(), lr=5e-5)
而后,开始模型的训练:
for epoch in range(3):
for (b_ix, batch) in enumerate(train_loader):
optim.zero_grad()
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs[0]
loss.backward()
optim.step()
训练完模型后,咱们就能够对任意文原本进行情感的打分了,比方:
I like you,I love you
显然这是一句很 positive 的话,模型的打分为:
具体的细节,能够参考 James D. McCaffrey 的博客:https://jamesmccaffrey.wordpr…。
模型的抉择问题,这里作为演示采样了一个 distilbert 作为处分模型,但实际上 ChatGPT 用的处分模型的规模应该是和生成模型差不多大小的模型。
到目前为止,咱们曾经可能训练一个可能生成文本的 GPT,一个可能对文本打分的处分模型,接下来就须要思考如果用处分模型来教会 GPT 生成高分文本了。思考到很多同学可能没有强化学习的根底,这里将插入一章强化学习的介绍。
三、强化学习
强化学习的内容其实很多,背地波及的数学也挺多,身边很多人在入门过程就被劝退,为了让大家更易于承受,这里次要介绍一些必备的基础知识,以及从策略梯度办法到 PPO 算法的演进。
3.1 强化术语
在强化学习中,智能体和环境进行交互并基于肯定的处分机制来进步本身的程度;智能体的决策,个别称为策略(policy)决定了在以后环境状态下智能体应该去施行什么动作;施行完动作后,智能领会随着环境进入下一个状态,并取得相应的处分或惩办。这里附上两页 slides 帮忙大家了解。
强化学习的最终目标就是要学会一个使得智能体可能最大化冀望回报的 policy,其中的回报就是对处分进行衰减求和:
和冀望回报严密相干的还有两个概念,一个是动作状态价值函数(观测到状态,做完决策,选中动作![]()):
另一个是状态价值函数(能够了解为比方下围棋时评估以后状态 ![]()的胜率)
理解了下面的定义后咱们能够停下来思考一下,强化学习学什么?
次要想学的必定就是策略 policy 函数,也就是从状态到动作的一个映射,如果间接学习它,那就可能拿来应用了,这类办法也叫做基于 policy 的办法;如果采纳间接点的形式,也能够学习值函数,而后依据值的大小来抉择动作,这类办法也叫做基于 value 的办法。当然,通常基于 policy 的办法也会波及到值函数的近似。
3.2 从策略梯度到 PPO
咱们要学一个策略函数,然而并不知道策略函数长什样,怎么去定义它才是适合的。好在有了深度学习这一工具,咱们能够无脑用一个神经网络来近似策略函数,而后通过优化神经网络参数的形式来学习失去一个策略函数。
优化神经网络的参数须要有个指标函数,如果一个策略很好,那么状态价值的均值该当很大,因而咱们定义指标函数:
这个指标函数排除掉了状态 S 的因素,只依赖于策略网络 π 的参数 Θ;策略越好,则 ![]()越大。所以策略学习能够形容为这样一个优化问题:
咱们心愿通过对策略网络参数 Θ 的更新,使得指标函数![]()越来越大,也就意味着策略 网络越来越强。想要求解最大化问题,显然能够用梯度回升更新模型的参数。值得庆幸的是,策略函数的梯度还能被推导进去:
策略梯度定理的具体推导这里就不开展了,咱们须要记住的是能计算出指标函数对于参数的梯度,那就能用来更新参数,也就能学习出策略函数了。
当然这外面还波及动作价值函数 Q 的预计,如果用理论观测的回报来近似,那就是 REINFORE 算法,如果再用一个神经网络来近似这个价值函数,那就是演员 - 评论家算法。PS:在理论应用中,策略梯度中的 Q 有多种不同的代替模式,常见成果比拟好的模式是采纳劣势函数 A(状态动作值函数 Q 减去状态值函数 V)来代替。
传统的策略梯度算法的局限性在于它是 sample-inefficient 的,也就是说每次获取的训练数据只被用来更新一次模型的参数后就丢掉了,因而 PPO 算法的次要改良在于结构了新的指标函数(防止较大的参数变动),使得每次获取的训练数据可能被用于屡次的参数更新。
其中,比值函数为以后策略和历史策略在状态 St 下施行动作 At 的概率的比值:
通过这一比值也就可能评估新旧策略的差异性,从而可能保障策略函数在更新参数时不会跟旧策略的差别太大。有工夫的同学也能够对比值在不同区间时指标函数的状况进行思考,也就是如下表的状况。
此外,咱们晓得在强化学习中通常还须要去让智能体可能具备 exploratory 的体现,这样能力挖掘出更多具备高价值的行为,所以 PPO 算法在训练策略网络时还在指标函数中加上了和熵相干的一项处分 H:
这里以离散动作为例,能够看到如果施行动作的概率扩散到不同的动作上将具备更大的熵。
后面介绍策略梯度时晓得策略梯度中还波及价值函数 / 劣势函数的预计,在 PPO 算法中也是采纳神经网络来预计状态价值函数,训练价值网络的指标函数通常仅须要最小化价值网络的预测和指标的平方误差就能够了:
综上所述,PPO 算法残缺的优化指标函数由三局部组成:
3.3 PPO 算法代码解读
理解完原理后,咱们来看一下强化的代码个别怎么写的。训练智能体前个别须要定义一个环境:
envs = xxxx
环境须要具备两个次要的性能函数,一个是 step,它的输出是动作,输入是下一步的观测、处分、以及示意环境是否完结等额定信息:
next_obs, reward, done, info = envs.step(action)
另一个是 reset,次要用来重置环境。
而后须要定义一个智能体,智能体蕴含策略网络和价值网络两局部(也就是演员和评论家),get_value 函数应用价值网络评估状态的价值,get_action_and_value 函数应用策略网络给出了某个状态下动作的概率分布(以及对数概率)、概率分布采样失去的动作,概率分布的熵、以及状态的价值。
class Agent(nn.Module):
def __init__(self, envs):
super().__init__()
self.critic = nn.Sequential(layer_init(nn.Linear(np.array(envs.single_observation_space.shape).prod(), 64)),
nn.Tanh(),
layer_init(nn.Linear(64, 64)),
nn.Tanh(),
layer_init(nn.Linear(64, 1), std=1.0),
)
self.actor = nn.Sequential(layer_init(nn.Linear(np.array(envs.single_observation_space.shape).prod(), 64)),
nn.Tanh(),
layer_init(nn.Linear(64, 64)),
nn.Tanh(),
layer_init(nn.Linear(64, envs.single_action_space.n), std=0.01),
)
def get_value(self, x):
return self.critic(x)
def get_action_and_value(self, x, action=None):
logits = self.actor(x)
probs = Categorical(logits=logits)
if action is None:
action = probs.sample()
return action, probs.log_prob(action), probs.entropy(), self.critic(x)
接下来就只须要思考如何收集数据和训练网络了。收集数据阶段次要包含两个局部,一部分是用智能体去和环境做交互,并保留相应的状态、动作等信息,另一部分次要是依据每一步的处分来计算每一步的回报,从而计算用于评估动作好坏的劣势函数值。
for step in range(0, args.num_steps):
obs[step] = next_obs
dones[step] = next_done
with torch.no_grad():
action, logprob, _, value = agent.get_action_and_value(next_obs)
values[step] = value.flatten()
actions[step] = action
logprobs[step] = logprob
next_obs, reward, done, info = envs.step(action.cpu().numpy())
rewards[step] = torch.tensor(reward).to(device).view(-1)
with torch.no_grad():
next_value = agent.get_value(next_obs).reshape(1, -1)
returns = torch.zeros_like(rewards).to(device)
for t in reversed(range(args.num_steps)):
if t == args.num_steps - 1:
nextnonterminal = 1.0 - next_done
next_return = next_value
else:
nextnonterminal = 1.0 - dones[t + 1]
next_return = returns[t + 1]
returns[t] = rewards[t] + args.gamma * nextnonterminal * next_return
advantages = returns - values
训练网络局部次要就是依据之前提到的三局部指标函数,顺次计算新旧策略的差别从而计算策略网络的损失、价值网络的损失 以及 对于动作多样性的熵处分;累加后再进行反向流传就能够更新网络的参数了。
for epoch in range(args.update_epochs):
np.random.shuffle(b_inds)
for start in range(0, args.batch_size, args.minibatch_size):
end = start + args.minibatch_size
mb_inds = b_inds[start:end]
# 计算新旧策略的差别
_, newlogprob, entropy, newvalue = agent.get_action_and_value(b_obs[mb_inds], b_actions.long()[mb_inds])
logratio = newlogprob - b_logprobs[mb_inds]
ratio = logratio.exp()
mb_advantages = b_advantages[mb_inds]
# 策略网络损失
pg_loss1 = -mb_advantages * ratio
pg_loss2 = -mb_advantages * torch.clamp(ratio, 1 - args.clip_coef, 1 + args.clip_coef)
pg_loss = torch.max(pg_loss1, pg_loss2).mean()
# 价值网络损失
newvalue = newvalue.view(-1)
v_loss = 0.5 * ((newvalue - b_returns[mb_inds]) ** 2).mean()
entropy_loss = entropy.mean()
loss = pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(agent.parameters(), args.max_grad_norm)
optimizer.step()
PPO 的残缺代码能够参考 https://github.com/huggingfac…
也倡议大家有空能够浏览一下这篇文章,有助于增强对强化的了解:https://fse.studenttheses.ub….
四、利用处分模型强化 GPT
看到了这里,咱们曾经学会啦如何训练一个 GPT,训练一个处分模型,以及强化学习算法 PPO 的训练过程,将它们组合起来,就可能做出一个 ChatGPT 了!
这里咱们先定义两个 GPT 模型,一个用于强化,一个用于参考(因为咱们通常也不心愿强化的模型齐全朝着更高的处分去优化,所以能够束缚一下强化的模型和原始模型依然具备肯定的“相似性”),参考模型不用于优化,所以设为 eval 模式即可。
model = GPT()
ref_model = GPT()
ref_model.eval()
须要留神的是为了适应强化这个框架,咱们还须要对原来的 GPT 模型进行肯定的封装,其实次要就是加一层 value head(线性层),让它预测每一个 token 的价值(了解为值函数,将 token 的暗藏状态转化为一个标量值)。
为了让大家更加分明地理解训练的过程,咱们以一条样本为例来展现阐明数据的传递过程。假如咱们有一个 query(就是送入 gpt 的输出),后面说过模型的输出个别通过分词器转化为一串 id,比方:
![]()
送入 GPT 后,生成模型会接着输出进行“续写”,失去一条答复 response:
![]()
而后咱们能够把这 query 和 response 拼接起来并解码为文本后送入处分模型,失去一个 rewards(能够了解 chatgpt 的答复好,就象征这把发问和模型的答复拼在一起看是正当的),接下来就能够思考应用 PPO 算法来对 GPT 进行强化了。
首先,第一步咱们就是把“发问”和“答复”拼接起来,送给处分模型,让它给 GPT 的答复打个分:
texts = [q + r for q, r in zip(query, response)]
score = reward_model(texts)[1]["score"]
有了这个得分后,就能够让 GPT 模型朝着尽可能高分的方向去优化了。
把拼接后的文本送入模型,并取出对应 token 的(对数)概率,这里并不需要梯度的流传,次要是取得“旧”的动作(模型的答复),以及用于计算每一个 token 的处分:
with torch.no_grad():
logits, _, v = self.model(**input_kwargs)
ref_logits, _, _ = self.ref_model(**input_kwargs)
old_logprobs = logprobs_from_logits(logits[:, :-1, :], input_ids[:, 1:])
ref_logprobs = logprobs_from_logits(ref_logits[:, :-1, :], input_ids[:, 1:])
计算处分这里思考两个局部,一部分是用于强化的 GPT 模型的答复和参考模型答复的 KL 散度(如后面所说,不能让模型一味朝着处分高的方向优化),另一部分就是处分模型给出的评分:
kl = old_logprobs - ref_logprob
reward = -kl
reward[-1] += score
接下来进行模型的前向流传(这里是须要梯度的),并索引出 response 局部的动作概率和值函数:
logits, _, vpred = self.model(**input_kwargs)
logprob = logprobs_from_logits(logits[:, :-1, :], model_input[:, 1:])
logprob, vpred = logprob[:, -gen_len:], vpred[:, -gen_len:]
计算 loss 之前咱们须要预计一下劣势函数和回报,劣势函数用于更新策略网络,回报用于更新价值网络,这里采纳经典的 GAE 办法(能够无效升高策略梯度的预计方差)来预计劣势函数:
lastgaelam = 0
advantages_reversed = []
for t in reversed(range(gen_len)):
nextvalues = values[:, t + 1] if t < gen_len - 1 else 0.0
delta = rewards[:, t] + self.config.gamma * nextvalues - values[:, t]
lastgaelam = delta + self.config.gamma * self.config.lam * lastgaelam
advantages_reversed.append(lastgaelam)
advantages = torch.stack(advantages_reversed[::-1]).transpose(0, 1)
returns = advantages + values
计算策略网络的损失:
ratio = torch.exp(logprob - old_logprobs)
pg_losses = -advantages * torch.clamp(ratio, 1.0 - self.config.cliprange, 1.0 + self.config.cliprange)
pg_loss = torch.mean(pg_losses)
价值函数的损失:
vf_losses = (vpred - returns) ** 2
vf_loss = 0.5 * torch.mean(vf_losses)
整体误差反向流传:
loss = pg_loss + self.config.vf_coef * vf_loss
optimizer.zero_grad()
accelerator.backward(loss)
optimizer.step()
这样就实现了应用 PPO 来强化 GPT 的一个 step 了,也就是 ChatGPT 实现的核心思想。
能够看下面的通过强化 GPT 再用于生成的一个成果:
以及将后果再用处分模型打分的前后比照:
mean:
rewards (before) 0.156629
rewards (after) 1.686487
median:
rewards (before) -0.547091
rewards (after) 2.479868
能够看到 GPT 生成后果的整体得分都有比拟大的晋升,这也阐明了如果处分模型训练得好,那用来做 ChatGPT 的成果天然也就可能大幅度提高。
本章的残缺代码能够参考开源我的项目:https://github.com/lvwerra/trl
参考:
- https://d4mucfpksywv.cloudfro…
- https://s3-us-west-2.amazonaw…
- https://arxiv.org/pdf/2005.14…
- https://openai.com/blog/chatgpt/
- https://huggingface.co/docs/t…
- https://huggingface.co/blog/rlhf
- https://huggingface.co/blog/d…
- https://fse.studenttheses.ub….
- https://github.com/lvwerra/trl
- https://github.com/wangshusen…