Hugging Face預訓練GPT微調ChatGPT(微調入門!新手友好!)
在實戰中,?多數情況下都不需要從0開始訓練模型,?是使?“??”或者其他研究者開源的已經訓練好的?模型。
在各種?模型開源庫中,最具代表性的就是Hugging Face
。Hugging Face
是?家專注于NLP領域的AI公司,開發了?個名為Transformers
的開源庫,該開源庫擁有許多預訓練后的深度學習模型,如BERT、GPT-2、T5等。Hugging Face
的Transformers
開源庫使研究?員和開發?員能夠更輕松地使?這些模型進?各種NLP任務,例如?本分類、問答、?本?成等。這個庫也提供了簡潔、?效的API,有助于快速實現?然語?處理應?。
從Hugging Face下載?個GPT-2并微調成ChatGPT,需要遵循的步驟如下。

1.安裝Hugging Face Transformers庫
pip install transformers
2.載入預訓練GPT-2模型和分詞器
import torch # 導?torch
from transformers import GPT2Tokenizer # 導?GPT-2分詞器
from transformers import GPT2LMHeadModel # 導?GPT-2語?模型
model_name = "gpt2" # 也可以選擇其他模型,如"gpt2-medium" "gpt2-large"等
tokenizer = GPT2Tokenizer.from_pretrained(model_name) # 加載分詞器
tokenizer.pad_token = '' # 為分詞器添加pad token
tokenizer.pad_token_id = tokenizer.convert_tokens_to_ids('')
device = "cuda" if torch.cuda.is_available() else "cpu" # 判斷是否有可?的GPU
model = GPT2LMHeadModel.from_pretrained(model_name).to(device) # 將模型加載到設備上(CPU或GPU)
vocab = tokenizer.get_vocab() # 獲取詞匯表
print("模型信息:", model)
print("分詞器信息:",tokenizer)
print("詞匯表??:", len(vocab))
print("部分詞匯示例:", (list(vocab.keys())[8000:8005]))
3.準備微調數據集
from torch.utils.data import Dataset # 導入PyTorch的Dataset# 自定義ChatDataset類,繼承自PyTorch的Dataset類
class ChatDataset(Dataset):def __init__(self, file_path, tokenizer, vocab):self.tokenizer = tokenizer # 分詞器self.vocab = vocab # 詞匯表# 加載數據并處理,將處理后的輸入數據和目標數據賦值給input_data和target_dataself.input_data, self.target_data = self.load_and_process_data(file_path)# 定義加載和處理數據的方法def load_and_process_data(self, file_path):with open(file_path, "r") as f: # 讀取文件內容lines = f.readlines()input_data, target_data = [], []for i, line in enumerate(lines): # 遍歷文件的每一行if line.startswith("User:"): # 如以"User:"開頭,移除"User: "前綴,并將張量轉換為列表tokens = self.tokenizer(line.strip()[6:], return_tensors="pt")["input_ids"].tolist()[0]tokens = tokens + [self.tokenizer.eos_token_id] # 添加結束符input_data.append(torch.tensor(tokens, dtype=torch.long)) # 添加到input_dataelif line.startswith("AI:"): # 如以"AI:"開頭,移除"AI: "前綴,并將張量轉換為列表tokens = self.tokenizer(line.strip()[4:], return_tensors="pt")["input_ids"].tolist()[0]tokens = tokens + [self.tokenizer.eos_token_id] # 添加結束符target_data.append(torch.tensor(tokens, dtype=torch.long)) # 添加到target_datareturn input_data, target_data# 定義數據集的長度,即input_data的長度def __len__(self):return len(self.input_data)# 定義獲取數據集中指定索引的數據的方法def __getitem__(self, idx):return self.input_data[idx], self.target_data[idx]file_path = "/kaggle/input/hugging-face-chatgpt-chat-data/chat.txt" # 加載chat.txt數據集
chat_dataset = ChatDataset(file_path, tokenizer, vocab) # 創建ChatDataset對象,傳入文件、分詞器和詞匯表# 打印數據集中前2個數據示例
for i in range(2):input_example, target_example = chat_dataset[i]print(f"示例 {i + 1}:")print("輸入:", tokenizer.decode(input_example))print("輸出:", tokenizer.decode(target_example))
4.準備微調數據加載器
from torch.utils.data import DataLoader # 導入DataLoadertokenizer.pad_token = '' # 為分詞器添加pad token
tokenizer.pad_token_id = tokenizer.convert_tokens_to_ids('')# 定義pad_sequence函數,用于將一批序列補齊到相同長度
def pad_sequence(sequences, padding_value=0, length=None):# 計算最大序列長度,如果length參數未提供,則使用輸入序列中的最大長度max_length = max(len(seq) for seq in sequences) if length is None else length# 創建一個具有適當形狀的全零張量,用于存儲補齊后的序列result = torch.full((len(sequences), max_length), padding_value, dtype=torch.long)# 遍歷序列,將每個序列的內容復制到張量result中for i, seq in enumerate(sequences):end = len(seq)result[i, :end] = seq[:end]return result# 定義collate_fn函數,用于將一個批次的數據整理成適當的形狀
def collate_fn(batch):# 從批次中分離源序列和目標序列sources, targets = zip(*batch)# 計算批次中的最大序列長度max_length = max(max(len(s) for s in sources), max(len(t) for t in targets))# 使用pad_sequence函數補齊源序列和目標序列sources = pad_sequence(sources, padding_value=tokenizer.pad_token_id, length=max_length)targets = pad_sequence(targets, padding_value=tokenizer.pad_token_id, length=max_length)# 返回補齊后的源序列和目標序列return sources, targets# 創建DataLoader
chat_dataloader = DataLoader(chat_dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)# 檢查Dataloader輸出
for input_batch, target_batch in chat_dataloader:print("Input batch tensor size:", input_batch.size())print("Target batch tensor size:", target_batch.size())breakfor input_batch, target_batch in chat_dataloader:print("Input batch tensor:")print(input_batch)print("Target batch tensor:")print(target_batch)break
5.對GPT-2進行微調
import torch.nn as nn
import torch.optim as optim# 定義損失函數,忽略pad_token_id對應的損失值
criterion = nn.CrossEntropyLoss(ignore_index=tokenizer.pad_token_id)# 定義優化器
optimizer = optim.Adam(model.parameters(), lr=0.0001)# 進行500個epoch的訓練
for epoch in range(500):for batch_idx, (input_batch, target_batch) in enumerate(chat_dataloader): # 遍歷數據加載器中的批次optimizer.zero_grad() # 梯度清零input_batch, target_batch = input_batch.to(device), target_batch.to(device) # 輸入和目標批次移至設備outputs = model(input_batch) # 前向傳播logits = outputs.logits # 獲取logits# 計算損失loss = criterion(logits.view(-1, len(vocab)), target_batch.view(-1))loss.backward() # 反向傳播optimizer.step() # 更新參數if (epoch + 1) % 100 == 0: # 每100個epoch打印一次損失值print(f'Epoch: {epoch + 1:04d}, cost = {loss:.6f}')
6.用約束解碼函數生成回答
# 定義集束解碼函數
def generate_text_beam_search(model, input_str, max_len=50, beam_width=5):model.eval() # 將模型設置為評估模式(不計算梯度)# 對輸入字符串進行編碼,并將其轉換為張量,然后將其移動到相應的設備上input_tokens = tokenizer.encode(input_str, return_tensors="pt").to(device)# 初始化候選序列列表,包含當前輸入序列和其對數概率得分(我們從0開始)candidates = [(input_tokens, 0.0)]# 禁用梯度計算,以加速預測過程with torch.no_grad():# 迭代生成最大長度的序列for _ in range(max_len):new_candidates = []# 對于每個候選序列for candidate, candidate_score in candidates:# 使用模型進行預測outputs = model(candidate)# 獲取輸出logitslogits = outputs.logits[:, -1, :]# 獲取對數概率得分的top-k值(即beam_width)及其對應的tokenscores, next_tokens = torch.topk(logits, beam_width, dim=-1)final_results = []# 遍歷top-k token及其對應的得分for score, next_token in zip(scores.squeeze(), next_tokens.squeeze()):# 在當前候選序列中添加新的tokennew_candidate = torch.cat((candidate, next_token.unsqueeze(0).unsqueeze(0)), dim=-1)# 更新候選序列的得分new_score = candidate_score - score.item()# 如果新的token是結束符(eos_token),則將該候選序列添加到最終結果中if next_token.item() == tokenizer.eos_token_id:final_results.append((new_candidate, new_score))# 否則,將新的候選序列添加到新候選序列列表中else:new_candidates.append((new_candidate, new_score))# 從新候選序列列表中選擇得分最?的top-k個序列candidates = sorted(new_candidates, key=lambda x: x[1])[:beam_width]# 選擇得分最?的候選序列best_candidate, _ = sorted(candidates, key=lambda x: x[1])[0]# 將輸出token轉換回文本字符串output_str = tokenizer.decode(best_candidate[0])# 移除輸入字符串并修復空格問題input_len = len(tokenizer.encode(input_str))output_str = tokenizer.decode(best_candidate.squeeze()[input_len:])return output_str# 測試模型
test_inputs = ["what is the weather like today?","can you recommend a good book?"
]# 輸出測試結果
for i, input_str in enumerate(test_inputs, start=1):generated_text = generate_text_beam_search(model, input_str)print(f"測試 {i}:")print(f"User: {input_str}")print(f"AI: {generated_text}")
測試1:
User: what is the weather like today?<|endoftext|>
AI: you need an current time for now app with app app app app
測試2:
User: Can you recommend a good book?<|endoftext|>
AI: ockingbird Lee Harper Harper Taylor
模型的回答雖然稱不上完美,但是,我們?少能夠看出,微調數據集中的信息起到了?定的作?。第?個問題問及天?,模型敏銳地指向“app”(應?)這個存在于訓練語料庫中的信息,?查看“應?”確實是我們希望模型給出的答案。回答第?個問題時,模型給出了語料庫中所推薦圖書的作者的名字“Lee Harper”,?書名“To kill a Mockingbird”中的mockingbird是?個未知token,模型把它拆解成了三個token。具體信息如下。
tokenizer.encode('Mockingbird'):[44/76, 8629, 16944]
tokenizer.decode(44):'M'
tokenizer.decode(8629):'ocking'
tokenizer.decode(16944):'bird'
因此,在解碼時,出現了ockingbird這樣的不完整信息,但是其中也的確包含了?定的語料庫內部的知識。
?微調則針對特定任務進?優化。這?模式的優勢在于,微調過程通常需要較少的訓練數據和計算資源,同時仍能獲得良好的性能。