昨天 Day19 我們用 Transformers + DeepSpeed 完成一個模型的預訓練,接下來要讓模型理解自然語言,所以需要SFT。
我們先複習一下之前有介紹過的,大型語言模型(LLM)的三個階段訓練流程
Pretrain(預訓練):
使用海量的無標註文本,進行自監督的 Causal Language Modeling(CLM),即用於訓練預測下一個 token,模型在此階段學習語言結構、語義關係與世界知識。
SFT(有監督微調):
使用「指令:回應」成對的資料進行訓練,讓模型能夠依據人類指令進行規劃與輸出,核心任務仍為 CLM,但 Loss 僅對回應(Assistant 輸出)計算,不計算指令部分。
因此,SFT 與 Pretrain 的主要區別不在於建模方式,而在於 資料格式與 Loss 計算區域。
要做 SFT,我們需要設計一個 Chat Template,把多輪對話拼成一個文字序列,例如在 Qwen 模型中,對話會被標註成三種角色:
Chat Template 範例:
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>human
請翻譯:今天天氣真好<|im_end|>
<|im_start|>assistant
The weather is nice today.<|im_end|>
在 SFT 中:
我們用 BelleGroup 指令數據集來做範例
# BOS / EOS / PAD
im_start = tokenizer("<|im_start|>").input_ids
im_end = tokenizer("<|im_end|>").input_ids
IGNORE_TOKEN_ID = -100 # 不計算 loss 的 token
# 角色標籤
_system = tokenizer("system").input_ids
_user = tokenizer("human").input_ids
_assistant = tokenizer("assistant").input_ids
def preprocess(sources, tokenizer, max_len=2048):
input_ids, targets, attention_masks = [], [], []
for source in sources: # 每個 source 是一段多回合對話
# system prompt
system = im_start + _system + tokenizer("You are a helpful assistant.").input_ids + im_end
input_id = system
target = [IGNORE_TOKEN_ID] * len(system)
for sentence in source:
role = sentence["from"] # "human" 或 "assistant"
content = tokenizer(sentence["value"]).input_ids + im_end
if role == "human":
# User → 不計算 loss
input_id += _user + content
target += [IGNORE_TOKEN_ID] * (len(_user) + len(content))
elif role == "assistant":
# Assistant → 要學習
input_id += _assistant + content
target += [IGNORE_TOKEN_ID] * len(_assistant) + content
# padding & mask
input_id = input_id[:max_len]
target = target[:max_len]
attention_mask = [1 if t != tokenizer.pad_token_id else 0 for t in input_id]
input_ids.append(input_id)
targets.append(target)
attention_masks.append(attention_mask)
return {
"input_ids": torch.tensor(input_ids),
"labels": torch.tensor(targets),
"attention_mask": torch.tensor(attention_mask)
}
from torch.utils.data import Dataset
class SupervisedDataset(Dataset):
def __init__(self, raw_data, tokenizer, max_len=2048):
self.data = preprocess(raw_data, tokenizer, max_len)
def __len__(self):
return len(self.data["input_ids"])
def __getitem__(self, i):
return dict(
input_ids=self.data["input_ids"][i],
labels=self.data["labels"][i],
attention_mask=self.data["attention_mask"][i],
)
這樣 SFT 的 Dataset 就準備好了!
接下來的步驟幾乎和 Pretrain 一模一樣!
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
import torch, json
# 1. 載入模型與 tokenizer
model_name = "Qwen/Qwen1.5-1.5B"
model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
# 2. 載入資料
with open("belle_train.json", "r", encoding="utf-8") as f:
raw_data = [json.loads(line) for line in f]
train_dataset = SupervisedDataset(raw_data, tokenizer)
# 3. 訓練設定
training_args = TrainingArguments(
output_dir="./sft-output",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
num_train_epochs=1,
logging_steps=10,
save_steps=500,
fp16=True,
)
# 4. 建立 Trainer 並開始訓練
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
tokenizer=tokenizer
)
trainer.train()
參考連結:
https://datawhalechina.github.io/happy-llm/#/