大模型預(yù)訓(xùn)練代碼實戰(zhàn)教程
任務(wù)介紹
本文使用一個簡單的數(shù)據(jù)集,展示大模型預(yù)訓(xùn)練與有監(jiān)督微調(diào)過程。無論是大模型的預(yù)訓(xùn)練還是有監(jiān)督微調(diào),其損失值的計算過程都是與下一個要預(yù)測的詞計算損失。
預(yù)訓(xùn)練損失值的計算,即從第一個字開始每個字都與下一個字計算損失;
有監(jiān)督微調(diào)與預(yù)訓(xùn)練唯一不同的點,便是不對指令與用戶的輸入文本計算損失,實際操作就是把用戶輸入文本在訓(xùn)練過程中遮罩掉,把對應(yīng)的 label 的值設(shè)置為-100。這是因為不希望大模型學(xué)會,如何生成的用戶的問題。
當(dāng)前文章介紹預(yù)訓(xùn)練,下篇文章介紹有監(jiān)督微調(diào)
本文不使用 llamafactory 等,大模型微調(diào)工具,上述工具把大模型微調(diào)的過程都封裝到底層了。只使用 transformers庫的AutoTrain實現(xiàn)大模型的微調(diào)。
開源地址:
??https://github.com/JieShenAI/csdn/tree/main/25/02/pre_train??
原始數(shù)據(jù)集
將使用下述5條數(shù)據(jù)微調(diào)大模型,對比一下,預(yù)訓(xùn)練與有監(jiān)督微調(diào)的區(qū)別。
[
{
"instruct": "請你給哪吒寫一首詩:",
"input": "哪吒降世,意氣飛揚(yáng)。\n逆天改命,破障沖霄。",
"label": "紅綾纏腕,風(fēng)火踏浪。\n不屈不悔,笑傲蒼茫。"
},
{
"instruct": "請你給敖丙寫一首詩:",
"input": "碧海生龍子,云中舞雪霜。",
"label": "恩仇難兩忘,何處是家鄉(xiāng)?"
},
{
"instruct": "請你給殷夫人寫一首詩:",
"input": "十月懷胎盼子生,柔心鐵骨兩相承。",
"label": "甘將慈愛護(hù)天地,不懼風(fēng)雷不懼征。"
},
{
"instruct": "請你給太乙真人寫一首詩:",
"input": "仙風(fēng)道骨,騎獸遨游。",
"label": "爐中煉術(shù),指點神童。"
},
{
"instruct": "請你給申公豹寫一首詩:",
"input": "陰謀藏心,步步為營。\n狂傲不羈,志向高冥。",
"label": "欲翻天命,終難遂行。\n困局自招,悔恨難平。"
}
]
下述是標(biāo)準(zhǔn)的有監(jiān)督微調(diào)的數(shù)據(jù)格式,使用 ??apply_chat_template?
? 方法,告知模型哪些是系統(tǒng)提示詞、用戶問題、模型的回答。
d = {
"instruct": "請你給哪吒寫一首詩:",
"input": "哪吒降世,意氣飛揚(yáng)。\n逆天改命,破障沖霄。",
"label": "紅綾纏腕,風(fēng)火踏浪。\n不屈不悔,笑傲蒼茫。",
}
messages = [
{
"role": "system",
"content": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
},
{
"role": "user",
"content": d["instruct"] + d["input"],
},
{
"role": "assistant",
"content": d["label"],
},
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
# add_generation_prompt=True
)
print(text)
輸出:
<|im_start|>system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>
<|im_start|>user
請你給哪吒寫一首詩:哪吒降世,意氣飛揚(yáng)。
逆天改命,破障沖霄。<|im_end|>
<|im_start|>assistant
紅綾纏腕,風(fēng)火踏浪。
不屈不悔,笑傲蒼茫。<|im_end|>
上述是數(shù)據(jù) template的構(gòu)造,每個大模型的template不一樣,但很多大模型微調(diào)工具(llamafactory等)都會自動構(gòu)造template,無需太擔(dān)心。
本文是大模型預(yù)訓(xùn)練與有監(jiān)督微調(diào)的手搓簡化版本,數(shù)據(jù)構(gòu)造不使用template,設(shè)置預(yù)訓(xùn)練和有監(jiān)督微調(diào)的輸入文本一樣,都是把 ??instruct + input + label?
? 拼接起來,在結(jié)尾添加一個結(jié)束符號。
instruct + input + label + tokenizer.eos_token
在結(jié)尾需要添加 ??tokenizer.eos_token?
? 停止符號,這是為了讓大模型學(xué)會停止文本生成。不然在大模型推理的時候,大模型就會一直往后生成文本,直到達(dá)到模型最大的生成的長度才會停止。
預(yù)訓(xùn)練代碼實戰(zhàn)
from typing import List, Dict, Sequence
import torch
import transformers
from transformers import TrainingArguments, Trainer
from torch.utils.data import Dataset
from dataclasses import dataclass
IGNORE_INDEX = -100
device = "cuda:0" if torch.cuda.is_available() else "cpu"
??IGNORE_INDEX?
?? -100, 在 ??label?
? 中被標(biāo)注為-100表示不參與 loss 計算。
from transformers import AutoModelForCausalLM, AutoTokenizer
model_dir = r"Qwen/Qwen2.5-0.5B"
model = AutoModelForCausalLM.from_pretrained(model_dir)
model = model.to("cuda:0")
tokenizer = AutoTokenizer.from_pretrained(model_dir, padding_side="right")
據(jù)上圖所示,發(fā)現(xiàn) Qwen 模型 文本填充與文本結(jié)束符 是同一個符號。這給后續(xù)計算文本停止符號的 loss計算 帶來了麻煩。
這里的討論可以忽略,如果想加深對 填充符號、文本停止符號、generate停止符的理解,可以閱讀下述文本:
如果 文本填充與文本結(jié)束符 是同一個符號,那么在 label 中,就不能把全部的填充符號都設(shè)置為-100,因為模型的填充符號與文本生成的停止符號是同一個字符。如果全部設(shè)置為-100,都不計算 loss,會導(dǎo)致模型學(xué)不會生成文本結(jié)束符號。當(dāng)然也可以選擇對所有的文本填充符號都計算 loss,這會導(dǎo)致模型學(xué)會在生成填充符號之后,下一個字符繼續(xù)生成填充符號。
踩坑經(jīng)歷:我曾經(jīng)在微調(diào)模型的時候,遇到一種情況,大模型在經(jīng)過微調(diào)后,文本生成結(jié)束了還在一直輸出??[PAD]?
??符號。這個原因就是沒有把填充符號??[PAD]?
??的 label 設(shè)置為-100,導(dǎo)致大模型學(xué)會了在遇到[PAD]之后,下一個詞依然輸出[PAD]。同時也沒有把??[PAD]?
?,作為停止符號,添加到generate方法的停止詞中,這才導(dǎo)致了一直生成[PAD]的情況出現(xiàn)。
總而言之,Qwen的填充符與停止符是同一個符號沒有問題。在模型調(diào)用generate方法生成文本時,雖然模型會一直生成填充符號,但是填充符號同時也是停止符號,模型也會停止文本生成。
由于本文不使用框架訓(xùn)練模型,可以更自由一點,故自定義填充符為??[PAD]?
?:
tokenizer.add_special_tokens({
"pad_token": "[PAD]"
})
tokenizer.pad_token, tokenizer.pad_token_id
輸出:
('[PAD]', 151665)
自定義數(shù)據(jù)集
class PreTrainDataset(Dataset):
def __init__(self, data: List):
super().__init__()
self.data = data
def __len__(self):
return len(self.data)
def __getitem__(self, idx) -> List[Dict]:
item = self.data[idx]
text = item["instruct"] + item["input"] + item["label"] + tokenizer.eos_token
return text
dataset = PreTrainDataset(data)
dataset[0]
輸出:
'請你給哪吒寫一首詩:哪吒降世,意氣飛揚(yáng)。\n逆天改命,破障沖霄。紅綾纏腕,風(fēng)火踏浪。\n不屈不悔,笑傲蒼茫。<|endoftext|>'
很多人都喜歡在自定義數(shù)據(jù)集里面完成 tokenizer,但我把這個操作留到了 ??DataCollator?
? 中。
- 如果在數(shù)據(jù)集中完成tokenizer,那么就需要在 ?
?DataCollator?
?? 對 ??input_ids?
?? 和 ??attention_mask?
? 進(jìn)行手動填充。 - 如果在 ?
?DataCollator?
?? 完成 tokenizer,便無需再對 ??input_ids?
?? 和 ??attention_mask?
? 手動填充。tokenizer 會默認(rèn)把這個batch的數(shù)據(jù)處理完成。只需要手動處理 label。
@dataclass
class DataCollatorForPretrainDataset(object):
tokenizer: transformers.PreTrainedTokenizer
def __call__(self, items: Sequence[Dict]) -> Dict[str, torch.Tensor]:
prompt = [item for item in items]
prompt_tokenizer = tokenizer(
prompt,
return_tensors="pt",
padding="longest",
max_length=tokenizer.model_max_length,
truncatinotallow=True,
)
labels = prompt_tokenizer["input_ids"].clone()
# 不對 pad 計算 loss
pad_idx = labels.eq(tokenizer.pad_token_id)
labels[pad_idx] = -100
prompt_tokenizer["labels"] = labels
return prompt_tokenizer
- ?
?padding="longest"?
? 把數(shù)據(jù)填充到這個 batch中數(shù)據(jù)的最大長度; - ?
?max_length=tokenizer.model_max_length?
? 最大長度是 tokenizer中模型是最大長度
大模型預(yù)訓(xùn)練的 ??label?
?很簡單,就是input_ids,做一個復(fù)制操作就行。
在進(jìn)行模型訓(xùn)練之前,測試一下, DataCollatorForPretrainDataset 處理數(shù)據(jù):
tokenizer.eos_token_id, tokenizer.pad_token_id,
輸出:
(151643, 151665)
data_collator = DataCollatorForPretrainDataset(tokenizer=tokenizer)
prompt_tokenizer = data_collator([dataset[0], dataset[1]])
prompt_tokenizer
輸出:
{'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]),
'input_ids': tensor([[112720, 89012, 99459, 122157, 61443, 108462, 100045, 5122, 99459,
122157, 99457, 99244, 3837, 36589, 99180, 115449, 8997, 100531,
35727, 22418, 50509, 3837, 99577, 99884, 99907, 109564, 1773,
99425, 120827, 103073, 103610, 3837, 99208, 79599, 100875, 99964,
8997, 16530, 102683, 16530, 103020, 3837, 48738, 102744, 102635,
100619, 1773, 151643],
[112720, 89012, 113735, 106980, 61443, 108462, 100045, 5122, 102461,
55135, 21287, 99465, 44729, 3837, 99718, 15946, 100066, 100167,
105401, 1773, 100697, 100956, 99349, 77540, 99980, 3837, 114216,
20412, 105686, 11319, 151643, 151665, 151665, 151665, 151665, 151665,
151665, 151665, 151665, 151665, 151665, 151665, 151665, 151665, 151665,
151665, 151665, 151665]]),
'labels': tensor([[112720, 89012, 99459, 122157, 61443, 108462, 100045, 5122, 99459,
122157, 99457, 99244, 3837, 36589, 99180, 115449, 8997, 100531,
35727, 22418, 50509, 3837, 99577, 99884, 99907, 109564, 1773,
99425, 120827, 103073, 103610, 3837, 99208, 79599, 100875, 99964,
8997, 16530, 102683, 16530, 103020, 3837, 48738, 102744, 102635,
100619, 1773, 151643],
[112720, 89012, 113735, 106980, 61443, 108462, 100045, 5122, 102461,
55135, 21287, 99465, 44729, 3837, 99718, 15946, 100066, 100167,
105401, 1773, 100697, 100956, 99349, 77540, 99980, 3837, 114216,
20412, 105686, 11319, 151643, -100, -100, -100, -100, -100,
-100, -100, -100, -100, -100, -100, -100, -100, -100,
-100, -100, -100]])}
??151643?
?? 是文本結(jié)束符號,??151665?
? 是文本填充符號。
attention_mask 為1的代表有意義的文本,需要參與到向量嵌入計算中。attention_mask 為 0的一般都是填充的符號。
在 decode 模型中, labels 的shape乃至內(nèi)容,一般都與input_ids 一樣。-100代表該位置的值不參與 loss 計算。(眾所周知 decode 模型與下一個詞計算loss。labels 需要左移一位并在尾部填充-100,這個操作用戶無需關(guān)心,此操作由transformers包根據(jù)數(shù)據(jù)集中的labels自動轉(zhuǎn)換)
模型訓(xùn)練
args = TrainingArguments(
output_dir=r"C:\Users\username\Desktop\train_model_output\Qwen2.5-0.5B\CLM_output",
num_train_epochs=10,
per_device_train_batch_size=2,
save_safetensors=True,
logging_strategy="epoch",
# fp16=True,
)
utput_dir:模型的保存地址,我的C盤是固態(tài)硬盤,加載訓(xùn)練完成后的模型會快一點。
trainer = Trainer(
model=model,
processing_class=tokenizer,
args=args,
train_dataset=dataset,
eval_dataset=None,
data_collator=DataCollatorForSupervisedDataset(tokenizer=tokenizer),
)
參數(shù)量估算
我選擇 ??Qwen/Qwen2.5-0.5B?
? 這個模型,因為這個模型參數(shù)少,可以更快看到結(jié)果。
上述模型微調(diào)是全參數(shù)微調(diào),沒有使用LoRA,會導(dǎo)致顯存占用很大。
下述是顯存占用的粗略估算的過程:
1.全精度,fp32:
1B = 10^9個參數(shù) = 10^9 x 4Byte = 4GB
由于我們是全參數(shù)微調(diào),那么最終占用的顯存是: (模型參數(shù) x1 + 梯度 x1 + Adam優(yōu)化器 x2)
0.5 x 4GB x (4) = 8GB
8 GB + batch的中間變量內(nèi)存
2.半精度, fp161B = 10^9個參數(shù) = 10^9 x 2Byte = 2GB
由于我們是全參數(shù)微調(diào),那么最終占用的顯存是: (模型參數(shù) x1 + 梯度 x1 + Adam優(yōu)化器 x2)
0.5 x 2GB x (4) = 4GB
4 GB + batch的中間變量內(nèi)存
模型推理
使用上述訓(xùn)練完成的模型,在訓(xùn)練集的數(shù)據(jù)上進(jìn)行推理。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
device = "cuda:0"if torch.cuda.is_available() else"cpu"
train_model = r"C:\Users\1\Desktop\train_model_output\Qwen2.5-0.5B\CLM_output"
model = AutoModelForCausalLM.from_pretrained(train_model)
model = model.to(device)
tokenizer = AutoTokenizer.from_pretrained(train_model, padding_side="right")
def infer(text):
input_ids = tokenizer(text, return_tensors="pt").to(model.device)
generated_ids = model.generate(**input_ids)
generated_ids = [
output_ids[len(input_ids) :]
for input_ids, output_ids in zip(input_ids.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
return response
text = "請你給哪吒寫一首詩:"
infer(text)
輸出:
'哪吒降世,意氣飛揚(yáng)。\n逆天改命,破障沖霄。紅綾纏腕,風(fēng)火踏浪。\n不屈不悔,笑傲蒼茫。'
通過模型的推理結(jié)果,驗證了大模型的預(yù)訓(xùn)練是有效果的。
本文轉(zhuǎn)載自??AI悠閑區(qū)??,作者:AI悠閑區(qū)
