使用SPIN技術(shù)對LLM進(jìn)行自我博弈微調(diào)訓(xùn)練
2024年是大型語言模型(llm)的快速發(fā)展的一年,對于大語言模型的訓(xùn)練一個(gè)重要的方法是對齊方法,它包括使用人類樣本的監(jiān)督微調(diào)(SFT)和依賴人類偏好的人類反饋強(qiáng)化學(xué)習(xí)(RLHF)。這些方法在llm中發(fā)揮了至關(guān)重要的作用,但是對齊方法對人工注釋數(shù)據(jù)有的大量需求。這一挑戰(zhàn)使得微調(diào)成為一個(gè)充滿活力的研究領(lǐng)域,研究人員積極致力于開發(fā)能夠有效利用人類數(shù)據(jù)的方法。
加州大學(xué)最近的一項(xiàng)研究介紹了一種名為SPIN(Self Play fIne tuNing)的新技術(shù)。SPIN從AlphaGo Zero和AlphaZero等游戲中成功的自我對弈機(jī)制中汲取靈感。它能夠使LLM參與自我游戲的能力。這消除了對專業(yè)注釋者的需求,無論是人類還是更高級(jí)的模型(如GPT-4)。SPIN涉及訓(xùn)練一個(gè)新的語言模型,并通過一系列迭代來區(qū)分它自己生成的響應(yīng)和人類生成的響應(yīng)。最終目標(biāo)是開發(fā)得到一種語言模型,使其產(chǎn)生的反應(yīng)與人類產(chǎn)生的反應(yīng)沒有區(qū)別。
自我博弈
自我博弈是一種算法通過對抗自身副本來學(xué)習(xí)的技術(shù)。這種方法增加了學(xué)習(xí)環(huán)境的挑戰(zhàn)性和復(fù)雜性,允許代理與自己的不同版本進(jìn)行交互。例如AlphaGo Zero,就是一個(gè)自我博弈的案例。
自我博弈在MARL中的有效性已經(jīng)得到證實(shí),但將其應(yīng)用于大型語言模型(llm)的增強(qiáng)是一種新的方法。在大型語言模型中應(yīng)用自我博弈有可能進(jìn)一步提高他們的能力,使他們能夠生成更連貫、信息豐富的文本。
自我游戲既可以用于競爭環(huán)境,也可以用于合作環(huán)境。在競爭環(huán)境中,算法的副本相互競爭以達(dá)到特定的目標(biāo)。在協(xié)作設(shè)置中,算法的副本一起工作以實(shí)現(xiàn)共同的目標(biāo)。它還可以與其他學(xué)習(xí)技術(shù)相結(jié)合,如監(jiān)督學(xué)習(xí)和強(qiáng)化學(xué)習(xí),以進(jìn)一步提高算法的性能。
SPIN
SPIN就像一個(gè)雙人游戲。在這個(gè)游戲中:
主模型(新LLM) -這個(gè)代理的角色是學(xué)習(xí)如何區(qū)分由語言模型(LLM)生成的響應(yīng)和由人類創(chuàng)建的響應(yīng)。在每個(gè)迭代中,主模型是正在積極訓(xùn)練的LLM。其目標(biāo)是提高其識(shí)別和區(qū)分反應(yīng)的能力。
對手模型(舊LLM) -對手模型的任務(wù)是生成與人類產(chǎn)生的反應(yīng)沒有區(qū)別的結(jié)果。對手模型是來自前一個(gè)迭代(輪)的LLM。它使用自我博弈機(jī)制,根據(jù)過去的知識(shí)產(chǎn)生結(jié)果。對手模型的目標(biāo)是創(chuàng)造逼真的反應(yīng),讓新的LLM無法判斷他是否是機(jī)器生成的。
這個(gè)流程是不是很像GAN,但是還是不太一樣
SPIN的動(dòng)態(tài)涉及使用監(jiān)督微調(diào)(SFT)數(shù)據(jù)集,該數(shù)據(jù)集由輸入(x)和輸出(y)對組成。這些示例由人工注釋,并作為訓(xùn)練主模型識(shí)別類人響應(yīng)的基礎(chǔ)。一些公開的SFT數(shù)據(jù)集包括Dolly15K、Baize、Ultrachat等。
主模型的訓(xùn)練
為了訓(xùn)練主模型區(qū)分語言模型(LLM)和人類反應(yīng),SPIN使用了一個(gè)目標(biāo)函數(shù)。這個(gè)函數(shù)測量真實(shí)數(shù)據(jù)和對手模型產(chǎn)生的反應(yīng)之間的預(yù)期值差距。主模型的目標(biāo)是最大化這一期望值差距。這包括將高值分配給與真實(shí)數(shù)據(jù)的響應(yīng)配對的提示,并將低值分配給由對手模型生成的響應(yīng)配對。這個(gè)目標(biāo)函數(shù)被表述為最小化問題。
主模型的工作是最小化損失函數(shù),即衡量來自真實(shí)數(shù)據(jù)的配對分配值與來自對手模型反應(yīng)的配對分配值之間的差異。在整個(gè)訓(xùn)練過程中,主模型調(diào)整其參數(shù)以最小化該損失函數(shù)。這個(gè)迭代過程一直持續(xù)下去,直到主模型能夠熟練地有效區(qū)分LLM的反應(yīng)和人類的反應(yīng)。
對手模型的更新
更新對手模型涉及改進(jìn)主模型的能力,他們在訓(xùn)練時(shí)已經(jīng)學(xué)會(huì)區(qū)分真實(shí)數(shù)據(jù)和語言模型反應(yīng)。隨著主模型的改進(jìn)及其對特定函數(shù)類的理解,我們還需要更新如對手模型的參數(shù)。當(dāng)主玩家面對相同的提示時(shí),它便會(huì)使用學(xué)習(xí)得到的辨別能力去評估它們的價(jià)值。
對手模型玩家的目標(biāo)是增強(qiáng)語言模型,使其響應(yīng)與主玩家的真實(shí)數(shù)據(jù)無法區(qū)分。這就需要設(shè)置一個(gè)流程來調(diào)整語言模型的參數(shù)。目的是在保持穩(wěn)定性的同時(shí),最大限度地提高主模型對語言模型反應(yīng)的評價(jià)。這涉及到一種平衡行為,確保改進(jìn)不會(huì)偏離原始語言模型太遠(yuǎn)。
聽著有點(diǎn)亂,我們簡單總結(jié)下:
訓(xùn)練的時(shí)候只有一個(gè)模型,但是將模型分為前一輪的模型(舊LLM/對手模型)和主模型(正在訓(xùn)練的),使用正在訓(xùn)練的模型的輸出與上一輪模型的輸出作為對比,來優(yōu)化當(dāng)前模型的訓(xùn)練。但是這里就要求我們必須要有一個(gè)訓(xùn)練好的模型作為對手模型,所以SPIN算法只適合在訓(xùn)練結(jié)果上進(jìn)行微調(diào)。
SPIN算法
SPIN從預(yù)訓(xùn)練的模型生成合成數(shù)據(jù)。然后使用這些合成數(shù)據(jù)對新任務(wù)上的模型進(jìn)行微調(diào)。
上面時(shí)原始論文中Spin算法的偽代碼,看著有點(diǎn)難理解,我們通過Python來復(fù)現(xiàn)更好地解釋它是如何工作的。
1、初始化參數(shù)和SFT數(shù)據(jù)集
原論文采用Zephyr-7B-SFT-Full作為基本模型。對于數(shù)據(jù)集,他們使用了更大的Ultrachat200k語料庫的子集,該語料庫由使用OpenAI的Turbo api生成的大約140萬個(gè)對話組成。他們隨機(jī)抽取了50k個(gè)提示,并使用基本模型來生成合成響應(yīng)。
# Import necessary libraries
from datasets import load_dataset
import pandas as pd
# Load the Ultrachat 200k dataset
ultrachat_dataset = load_dataset("HuggingFaceH4/ultrachat_200k")
# Initialize an empty DataFrame
combined_df = pd.DataFrame()
# Loop through all the keys in the Ultrachat dataset
for key in ultrachat_dataset.keys():
# Convert each dataset key to a pandas DataFrame and concatenate it with the existing DataFrame
combined_df = pd.concat([combined_df, pd.DataFrame(ultrachat_dataset[key])])
# Shuffle the combined DataFrame and reset the index
combined_df = combined_df.sample(frac=1, random_state=123).reset_index(drop=True)
# Select the first 50,000 rows from the shuffled DataFrame
ultrachat_50k_sample = combined_df.head(50000)
作者的提示模板“### Instruction: {prompt}\n\n### Response:”
# for storing each template in a list
templates_data = []
for index, row in ultrachat_50k_sample.iterrows():
messages = row['messages']
# Check if there are at least two messages (user and assistant)
if len(messages) >= 2:
user_message = messages[0]['content']
assistant_message = messages[1]['content']
# Create the template
instruction_response_template = f"### Instruction: {user_message}\n\n### Response: {assistant_message}"
# Append the template to the list
templates_data.append({'Template': instruction_response_template})
# Create a new DataFrame with the generated templates (ground truth)
ground_truth_df = pd.DataFrame(templates_data)
然后得到了類似下面的數(shù)據(jù):
SPIN算法通過迭代更新語言模型(LLM)的參數(shù)使其與地面真實(shí)響應(yīng)保持一致。這個(gè)過程一直持續(xù)下去,直到很難區(qū)分生成的響應(yīng)和真實(shí)情況,從而實(shí)現(xiàn)高水平的相似性(降低損失)。
SPIN算法有兩個(gè)循環(huán)。內(nèi)部循環(huán)基于我們正在使用的樣本數(shù)量運(yùn)行,外部循環(huán)總共運(yùn)行了3次迭代,因?yàn)樽髡甙l(fā)現(xiàn)模型的性能在此之后沒有變化。采用Alignment Handbook庫作為微調(diào)方法的代碼庫,結(jié)合DeepSpeed模塊,降低了訓(xùn)練成本。他們用RMSProp優(yōu)化器訓(xùn)練Zephyr-7B-SFT-Full,所有迭代都沒有權(quán)重衰減,就像通常用于微調(diào)llm一樣。全局批大小設(shè)置為64,使用bfloat16精度。迭代0和1的峰值學(xué)習(xí)率設(shè)置為5e-7,迭代2和3的峰值學(xué)習(xí)率隨著循環(huán)接近自播放微調(diào)的結(jié)束而衰減為1e-7。最后選擇β = 0.1,最大序列長度設(shè)置為2048個(gè)標(biāo)記。下面就是這些參數(shù)
# Importing the PyTorch library
import torch
# Importing the neural network module from PyTorch
import torch.nn as nn
# Importing the DeepSpeed library for distributed training
import deepspeed
# Importing the AutoTokenizer and AutoModelForCausalLM classes from the transformers library
from transformers import AutoTokenizer, AutoModelForCausalLM
# Loading the zephyr-7b-sft-full model from HuggingFace
tokenizer = AutoTokenizer.from_pretrained("alignment-handbook/zephyr-7b-sft-full")
model = AutoModelForCausalLM.from_pretrained("alignment-handbook/zephyr-7b-sft-full")
# Initializing DeepSpeed Zero with specific configuration settings
deepspeed_config = deepspeed.config.Config(train_batch_size=64, train_micro_batch_size_per_gpu=4)
model, optimizer, _, _ = deepspeed.initialize(model=model, config=deepspeed_config, model_parameters=model.parameters())
# Defining the optimizer and setting the learning rate using RMSprop
optimizer = deepspeed.optim.RMSprop(optimizer, lr=5e-7)
# Setting up a learning rate scheduler using LambdaLR from PyTorch
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lambda epoch: 0.2 ** epoch)
# Setting hyperparameters for training
num_epochs = 3
max_seq_length = 2048
beta = 0.1
2、生成合成數(shù)據(jù)(SPIN算法內(nèi)循環(huán))
這個(gè)內(nèi)部循環(huán)負(fù)責(zé)生成需要與真實(shí)數(shù)據(jù)保持一致的響應(yīng),也就是一個(gè)訓(xùn)練批次的代碼
# zephyr-sft-dataframe (that contains output that will be improved while training)
zephyr_sft_output = pd.DataFrame(columns=['prompt', 'generated_output'])
# Looping through each row in the 'ultrachat_50k_sample' dataframe
for index, row in ultrachat_50k_sample.iterrows():
# Extracting the 'prompt' column value from the current row
prompt = row['prompt']
# Generating output for the current prompt using the Zephyr model
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
output = model.generate(input_ids, max_length=200, num_beams=5, no_repeat_ngram_size=2, top_k=50, top_p=0.95)
# Decoding the generated output to human-readable text
generated_text = tokenizer.decode(output[0], skip_special_tokens=True)
# Appending the current prompt and its generated output to the new dataframe 'zephyr_sft_output'
zephyr_sft_output = zephyr_sft_output.append({'prompt': prompt, 'generated_output': generated_text}, ignore_index=True)
這就是一個(gè)提示的真實(shí)值和模型輸出的樣例。
新的df zephyr_sft_output,其中包含提示及其通過基本模型Zephyr-7B-SFT-Full生成的相應(yīng)輸出。
3、更新規(guī)則
在編碼最小化問題之前,理解如何計(jì)算llm生成的輸出的條件概率分布是至關(guān)重要的。原論文使用馬爾可夫過程,其中條件概率分布pθ (y∣x)可通過分解表示為:
這種分解意味著給定輸入序列的輸出序列的概率可以通過將給定輸入序列的每個(gè)輸出標(biāo)記與前一個(gè)輸出標(biāo)記的概率相乘來計(jì)算。例如輸出序列為“I enjoy reading books”,輸入序列為“I enjoy”,則在給定輸入序列的情況下,輸出序列的條件概率可以計(jì)算為:
馬爾可夫過程條件概率將用于計(jì)算真值和Zephyr LLM響應(yīng)的概率分布,然后用于計(jì)算損失函數(shù)。但首先我們需要對條件概率函數(shù)進(jìn)行編碼。
# Conditional Probability Function of input text
def compute_conditional_probability(tokenizer, model, input_text):
# Tokenize the input text and convert it to PyTorch tensors
inputs = tokenizer([input_text], return_tensors="pt")
# Generate text using the model, specifying additional parameters
outputs = model.generate(**inputs, return_dict_in_generate=True, output_scores=True)
# Assuming 'transition_scores' is the logits for the generated tokens
transition_scores = model.compute_transition_scores(outputs.sequences, outputs.scores, normalize_logits=True)
# Get the length of the input sequence
input_length = inputs.input_ids.shape[1]
# Assuming 'transition_scores' is the logits for the generated tokens
logits = torch.tensor(transition_scores)
# Apply softmax to obtain probabilities
probs = torch.nn.functional.softmax(logits, dim=-1)
# Extract the generated tokens from the output
generated_tokens = outputs.sequences[:, input_length:]
# Compute conditional probability
conditional_probability = 1.0
for prob in probs[0]:
token_probability = prob.item()
conditional_probability *= token_probability
return conditional_probability
損失函數(shù)它包含四個(gè)重要的條件概率變量。這些變量中的每一個(gè)都取決于基礎(chǔ)真實(shí)數(shù)據(jù)或先前創(chuàng)建的合成數(shù)據(jù)。
而lambda是一個(gè)正則化參數(shù),用于控制偏差。在KL正則化項(xiàng)中使用它來懲罰對手模型的分布與目標(biāo)數(shù)據(jù)分布之間的差異。論文中沒有明確提到lambda的具體值,因?yàn)樗赡軙?huì)根據(jù)所使用的特定任務(wù)和數(shù)據(jù)集進(jìn)行調(diào)優(yōu)。
def LSPIN_loss(model, updated_model, tokenizer, input_text, lambda_val=0.01):
# Initialize conditional probability using the original model and input text
cp = compute_conditional_probability(tokenizer, model, input_text)
# Update conditional probability using the updated model and input text
cp_updated = compute_conditional_probability(tokenizer, updated_model, input_text)
# Calculate conditional probabilities for ground truth data
p_theta_ground_truth = cp(tokenizer, model, input_text)
p_theta_t_ground_truth = cp(tokenizer, model, input_text)
# Calculate conditional probabilities for synthetic data
p_theta_synthetic = cp_updated(tokenizer, updated_model, input_text)
p_theta_t_synthetic = cp_updated(tokenizer, updated_model, input_text)
# Calculate likelihood ratios
lr_ground_truth = p_theta_ground_truth / p_theta_t_ground_truth
lr_synthetic = p_theta_synthetic / p_theta_t_synthetic
# Compute the LSPIN loss
loss = lambda_val * torch.log(lr_ground_truth) - lambda_val * torch.log(lr_synthetic)
return loss
如果你有一個(gè)大的數(shù)據(jù)集,可以使用一個(gè)較小的lambda值,或者如果你有一個(gè)小的數(shù)據(jù)集,則可能需要使用一個(gè)較大的lambda值來防止過擬合。由于我們數(shù)據(jù)集大小為50k,所以可以使用0.01作為lambda的值。
4、訓(xùn)練(SPIN算法外循環(huán))
這就是Pytorch訓(xùn)練的一個(gè)基本流程,就不詳細(xì)解釋了:
# Training loop
for epoch in range(num_epochs):
# Model with initial parameters
initial_model = AutoModelForCausalLM.from_pretrained("alignment-handbook/zephyr-7b-sft-full")
# Update the learning rate
scheduler.step()
# Initialize total loss for the epoch
total_loss = 0.0
# Generating Synthetic Data (Inner loop)
for index, row in ultrachat_50k_sample.iterrows():
# Rest of the code
...
# Output == prompt response dataframe
zephyr_sft_output
# Computing loss using LSPIN function
for (index1, row1), (index2, row2) in zip(ultrachat_50k_sample.iterrows(), zephyr_sft_output.iterrows()):
# Assuming 'prompt' and 'generated_output' are the relevant columns in zephyr_sft_output
prompt = row1['prompt']
generated_output = row2['generated_output']
# Compute LSPIN loss
updated_model = model # It will be replacing with updated model
loss = LSPIN_loss(initial_model, updated_model, tokenizer, prompt)
# Accumulate the loss
total_loss += loss.item()
# Backward pass
loss.backward()
# Update the parameters
optimizer.step()
# Update the value of beta
if epoch == 2:
beta = 5.0
我們運(yùn)行3個(gè)epoch,它將進(jìn)行訓(xùn)練并生成最終的Zephyr SFT LLM版本。官方實(shí)現(xiàn)還沒有在GitHub上開源,這個(gè)版本將能夠在某種程度上產(chǎn)生類似于人類反應(yīng)的輸出。我們看看他的運(yùn)行流程
表現(xiàn)及結(jié)果
SPIN可以顯著提高LLM在各種基準(zhǔn)測試中的性能,甚至超過通過直接偏好優(yōu)化(DPO)補(bǔ)充額外的GPT-4偏好數(shù)據(jù)訓(xùn)練的模型。
當(dāng)我們繼續(xù)訓(xùn)練時(shí),隨著時(shí)間的推移,進(jìn)步會(huì)變得越來越小。這表明模型達(dá)到了一個(gè)閾值,進(jìn)一步的迭代不會(huì)帶來顯著的收益。這是我們訓(xùn)練數(shù)據(jù)中樣本提示符每次迭代后的響應(yīng)。