مدلهای زبانی پایه پس از مرحلۀ پیشآموزش صرفاً ماشینهای پیشبینی توکن بعدی هستند. اگر از چنین مدلی بپرسید «پایتخت فرانسه کجاست؟»، به احتمال زیاد پاسخی به شما نمیدهد، بلکه پرسشهای مشابه دیگری مانند «پایتخت آلمان کجاست؟» تولید میکند؛ زیرا تنها امتداد دادن متن را آموخته است. فاینتیونینگ نظارتشده (SFT) همان حلقۀ مفقودهای است که رفتار مکالمه و پاسخگویی به دستورالعمل را به مدل آموزش میدهد.
درک سازوکار SFT، قالببندی دادهها و تکنیک حیاتی ماسک کردن تابع زیان، مهارتی ضروری برای توسعهدهندگانی است که میخواهند مدلهای متنباز را برای کاربردهای بومی یا سازمانی شخصیسازی کنند. خوشبختانه برای پیادهسازی این درس و درک مفاهیم فنی آن به سختافزارهای گرانقیمت یا GPUهای ابری دستنیافتنی نیازی ندارید و کدهای پایه روی پردازندۀ سیستم شخصی نیز قابل اجرا هستند.
مدل پایه صرفاً توکن بعدی را پیشبینی میکند؛ همین و بس. نه از دستورالعملها پیروی میکند، نه به پرسشها پاسخ میدهد و نه در برابر درخواستهای آسیبرسان مقاومت به خرج میدهد. فاینتیونینگ نظارتشده (Supervised Fine-Tuning یا SFT) همان پلی است که یک ابزار پیشبینی توکن را به دستیاری کاربردی بدل میکند. تکتک مدلهایی که تاکنون با آنها گفتوگو کردهاید — از جمله Claude، GPT و Llama Chat — این مرحله را پشت سر گذاشتهاند.
اهداف یادگیری
- پیادهسازی فاینتیونینگ نظارتشده (SFT) برای تبدیل مدل زبانی پایه به دستیاری که دستورالعملها را درک و اجرا میکند
- فرمتبندی دادههای آموزشی با الگوهای گفتوگو (chat templates) با نقشهای سیستم، کاربر و دستیار، به همراه اعمال ماسک روی تابع زیان (loss) توکنهای غیردستیار
- تبیین چرایی ضرورت SFT: درک این واقعیت که مدلهای پایه متن را امتداد میدهند، نه اینکه پاسخ سوالات را بدهند
- ارزیابی کیفیت SFT با مقایسهی پاسخهای مدل پایه در برابر مدل فاینتیونشده روی مجموعهای نگهداشتهشده (held-out) از دستورالعملها
صورت مسئله
در درس 04 مدلی را آموزش دادید که میتوانست با دریافت یک توالی، توکن بعدی را پیشبینی کند. اگر عبارت «The transformer architecture» را به آن بدهید، شاید متن را با «has revolutionized natural language processing» ادامه دهد؛ دستاوردی چشمگیر برای سامانهای که کارش صرفاً پیشبینی توکن بعدی است.
حالا این آزمون را امتحان کنید: ورودی «What is the capital of France?» را به آن بدهید. مدل پایه پاسخی مثل «Paris» برنمیگرداند؛ بلکه همان الگو را پی میگیرد. ممکن است بنویسد: «What is the capital of Germany? What is the capital of Spain?»؛ چون میان اسناد متنی خود، فهرستهایی از پرسشهای پیاپی را دیده است. یا شاید تولید کند: «is a question that many people ask»، زیرا این هم امتداد معقولی بر پایهی توکن بعدی است. مدل اساساً هیچ درکی از مفهوم پاسخدادن ندارد؛ او تنها بلَد است متن را ادامه دهد.
شکاف اصلی میان GPT-3 (مدل پایه، عرضهشده در ژوئن 2020) و ChatGPT (تنظیمشده بر اساس دستورالعمل، عرضهشده در نوامبر 2022) دقیقاً همینجاست: معماری یکسان، پیشآموزش یکسان. تنها تمایز آنها در 20,000 تا 100,000 جفتدادهی (دستورالعمل، پاسخ) دقیق و دستچین خلاصه میشود که الگوی مشارکت در گفتوگو را به مدل آموختهاند.
پروژهی Stanford Alpaca ثابت کرد نیازی به میلیونها نمونه ندارید. آنها در مارس 2023، مدل Llama 7B را تنها روی 52,000 جفت دستورالعمل-پاسخ که با GPT-3.5 تولید شده بود، فاینتیون کردند. کل هزینه: 600 دلار. دستاورد کار، چتباتی بود که میتوانست از دستورها پیروی کند، به سوالات پاسخ دهد و مکالمه را پیش ببرد. شاید بهخوبی ChatGPT نبود، ولی در مقایسه با 600 دلار هزینه و چند ساعت آموزش، بازدهی خیرهکنندهای داشت.
مدل Llama 2 Chat شرکت Meta نیز در فاز اولیهی SFT خود تنها از حدود 27,000 نمونهی باکیفیت بهره برد. پیام کلیدی مشخص است: کیفیت بر کمیت برتری دارد. 27,000 نمونه که افراد متخصص آنها را دقیق نوشته باشند، بر 1 میلیون دادهی پر سر و صدا و گردآوریشده از اینترنت پیروز میشود.
مفاهیم پایه
سازوکار واقعی SFT
فاینتیونینگ نظارتشده در عمل همان حلقهی آموزشی فاز پیشآموزش — گذر رو به جلو (forward pass)، محاسبهی تابع زیان (loss)، گام رو به عقب (backward pass) و بهروزرسانی وزنها — را تکرار میکند، ولی این بار روی نوع متفاوتی از دادهها. بهجای متن خام، آموزش را روی مکالماتی سازمانیافته اجرا میکنید:
{
"system": "You are a helpful assistant.",
"user": "What is the capital of France?",
"assistant": "The capital of France is Paris."
}
مدل از قبل میداند که پاریس پایتخت فرانسه است؛ این واقعیت را هنگام پیشآموزش روی مقالههای ویکیپدیا، کتابها و صفحات وب فراگرفته است. SFT به مدل فکتهای جدیدی یاد نمیدهد؛ بلکه رفتار تازهای به آن میآموزد: وقتی پرسشی دیدی، پاسخ را بساز. وقتی دستورالعملی دریافت کردی، آن را تکمیل کن. وقتی درخواستی آسیبرسان دیدی، از انجامش سر باز بزن.
ماجرا را اینطور ببینید: پیشآموزش به مدل دانش میبخشد؛ SFT به مدل ادب و رفتار میآموزد.
فرمتهای ساختاردهی به دادهها
در صنعت، سه قالب اصلی رواج دارند. هر سه یک محتوا — چه کسی چه گفت — را با نشانهگذارهای (delimiters) متفاوتی مشخص میکنند.
فرمت Alpaca (استنفورد، مارس 2023):
{
"instruction": "Summarize the following article in 3 sentences.",
"input": "The European Central Bank raised interest rates...",
"output": "The ECB increased rates by 25 basis points..."
}
ساده و بسیار پرکاربرد. فیلد input اختیاری است؛ چرا که بسیاری از دستورالعملها نیازی به کانتکست اضافه ندارند. استنفورد 52,000 نمونه با این فرمت را با هزینهی 600 دلاری از طریق GPT-3.5 ساخت. همین اقدام جریان متنبازِ instruction tuning را کلید زد.
فرمت ShareGPT (جامعهی متنباز، 2023):
{
"conversations": [
{"from": "system", "value": "You are a helpful assistant."},
{"from": "human", "value": "What causes tides?"},
{"from": "gpt", "value": "Tides are caused by the gravitational pull of the Moon..."},
{"from": "human", "value": "How often do they occur?"},
{"from": "gpt", "value": "Most coastal areas experience two high tides and two low tides per day..."}
]
}
از مکالمات چندنوبته (multi-turn) پشتیبانی میکند. فیلد «from» فارغ از اینکه چه مدلی پاسخ داده، طبق عرف روی مقادیر «human» و «gpt» تنظیم میشود. مدل Vicuna با استفاده از 70,000 مکالمهی ShareGPT آموزش دید؛ رونوشتهایی از تعاملات کاربران با ChatGPT که به اشتراک گذاشته شده بودند.
فرمت ChatML (اوپنایآی، بهکاررفته در مدلهای متعدد متنباز):
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
What is the capital of France?<|im_end|>
<|im_start|>assistant
The capital of France is Paris.<|im_end|>
از توکنهای ویژه (<|im_start|>, <|im_end|>) برای تفکیک نقشها بهره میگیرد. این توکنها حین فاینتیونینگ به دایرهی لغات توکنایزر اضافه میشوند. مدلهای Qwen، Yi و بسیاری دیگر ساختار ChatML را به کار میبرند.
هر سه فرمت در نهایت یک کار را انجام میدهند: به مدل میگویند «این بخش دستور است و آن بخش پاسخ؛ این الگو را یاد بگیر».
چرا این روش جواب میدهد؟
مدل در فاز پیشآموزش زبان را بهخوبی یاد گرفته است. میلیاردها نمونه از سوالهای همراه با پاسخ، دستورهای متصل به تکمیل و گفتوگوهای میان افراد را دیده است. این الگوها پیشتر در دل وزنها ثبت شدهاند.
کاری که SFT انجام میدهد، متمرکز ساختن همین توانمندیهای نهفته است. بهجای آنکه مدل بر پایهی کانتکست حدس بزند که آیا باید پاسخی بدهد یا سندی را ادامه دهد، SFT صراحتاً روی الگوی گفتوگو آموزش میبیند. پس از چند هزار نمونه، مدل یاد میگیرد: هر زمان به نشانگرِ نقشِ دستیار رسیدی، پاسخی راهگشا آماده کن.
دقیقاً به همین دلیل 27,000 نمونه کفایت میکند. شما زبان انگلیسی را از نو به مدل یاد نمیدهید؛ حقایق جهان را هم به آن اضافه نمیکنید. فقط یک رفتار ساده را تفهیم میکنید: به دستورالعملها پاسخ بده. پایهی دانش از قبل در مدل بوده است.
ماسککردن تابع زیان (The Masked Loss)
این نکته، مهمترین جزئیات فنی در SFT است که اغلب راهنماها از آن میگذرند.
در پیشآموزش، تابع زیان را روی تکتک توکنها حساب میکنید؛ چرا که مدل باید پیشبینی هر توکن بعدی را در سراسر رشته بیاموزد. اما در SFT، تابع زیان فقط برای توکنهای پاسخ محاسبه میشود. توکنهای دستورالعمل صرفاً برای ایجاد کانتکست حاضرند و بابت پیشبینی اشتباه آنها جریمهای در نظر گرفته نمیشود.
چرا؟ چون نمیخواهید مدل تولید دستورالعمل را یاد بگیرد؛ بلکه میخواهید پاسخگویی به دستورالعمل را تمرین کند. اگر زیان را برای توکنهای دستورالعمل هم اعمال کنید، به مدل یاد میدهید که جملهی «What is the capital of France?» را طوری بسازد انگار خودش سوالکننده است. این کار سیگنال گرادیان را تلف میکند و درک مدل از نقشش را به هم میریزد.
در عمل، یک ماسکِ زیان میسازید: 1 برای توکنهای پاسخ و 0 برای توکنهای دستور. پیش از میانگینگیری، تابع زیانِ هر توکن را در این ماسک ضرب میکنید.
Tokens: [SYS] You are helpful [USER] What is the capital? [ASST] Paris is the capital [EOS]
Loss mask: 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1
تنها توکنهای بعد از [ASST] در تابع زیان سهم دارند. مدل در گذر رو به جلو تمام مکالمه را میبیند (چون برای آمادهسازی پاسخ مناسب به دستور نیاز دارد)، اما اصلاح وزنهای آن تنها مبتنی بر دقتش در پیشبینی توکنهای پاسخ انجام میگیرد.
هایپرپارامترهای آموزش
SFT از هایپرپارامترهایی کاملاً متفاوت با پیشآموزش استفاده میکند. شما مدل را از ابتدا آموزش نمیدهید؛ صرفاً مدلی را تنظیم میکنید که از قبل کار میکند.
| پارامتر | پیشآموزش (Llama 2 7B) | SFT (Llama 2 Chat) |
|---|---|---|
| نرخ یادگیری | 3e-4 (پیک) | 2e-5 |
| دورهها (Epochs) | 1 (یک گذر کامل روی دادهها) | 2 |
| اندازهی بچ | 4M توکن | 64 نمونه |
| گامهای وارمآپ | 2,000 | 0-100 |
| کاهش وزن | 0.1 | 0.0-0.1 |
| حجم دادهها | 2T توکن | 27,000 نمونه |
نرخ یادگیری در SFT تا 15 برابر کمتر است. این مسئله نقشی حیاتی دارد؛ چرا که نرخ یادگیری بالا حین فاینتیونینگ میتواند دانستههای پیشآموزش را نابود کند. مدل آموختههایش را فراموش میکند و روی مجموعهدادهی کوچک فاینتیونینگ دچار بیشبرازش (overfitting) میشود؛ پدیدهای که فراموشی فاجعهبار (Catastrophic Forgetting) نام دارد.
دو دوره یعنی مدل هر نمونهی آموزشی را دوبار میبیند. فراتر رفتن از 3 دوره روی دیتاستی کوچک باعث حفظکردن دادهها میشود — مدل بهجای تعمیم، همان نمونههای آموزشی را کلمهبهکلمه بازگو میکند.
فراموشی فاجعهبار
فاینتیونینگ میتواند قابلیتهای عمومی مدل را از بین ببرد. اگر آموزش روی دادههای مربوط به پیروی از دستور را بیش از حد طولانی کنید، مدل توان کدنویسی، حل ریاضیات یا نگارش متن خلاقانه را از دست میدهد؛ در ساختار فرمت آموزشیاش عالی عمل خواهد کرد، ولی در هر کار دیگری افتضاح میشود.
سه راهکار برای پیشگیری:
-
نرخ یادگیری پایین. بین 1e-5 تا 5e-5. بهروزرسانیهای کوچکتر آسیب کمتری به ویژگیهای از پیشآموخته وارد میکنند.
-
آموزش کوتاه. 1 تا 3 دوره. روند آموزش را پیش از بیشبرازش مدل متوقف کنید.
-
ترکیب دادههای پیشآموزش. تیم Llama 2 Chat بخش کوچکی (2 تا 5 درصد) از دادههای خام پیشآموزش را با دیتاست SFT ترکیب کرد. این کار قابلیتهای عمومی مدل را در حین یادگیری رفتار جدید «به یادش میآورد».
ارقام در جهان واقعی
فاینتیونینگ یک مدل 7B با 10,000 جفتدادهی باکیفیت، تقریباً 1 ساعت روی یک پردازندهی گرافیکی NVIDIA A100 80GB زمان میبرد. محاسبات ماجرا به این شرح است:
- 10,000 نمونه با میانگین 512 توکن = 5.12M توکن
- 2 دوره = مجموعاً 10.24M توکن
- نرخ پردازش (throughput) برای فاینتیون مدل 7B روی A100: حدود 3,000 توکن در ثانیه
- 10.24M تقسیم بر 3,000 = حدود 3,400 ثانیه = نزدیک به 57 دقیقه
در مینی جیپیتی ما (4 لایه، ابعاد 128)، آموزش تقریباً آنی است. هدف درک سازوکارهاست، نه مقیاس پردازش.
پیادهسازی از صفر
گام 1: مجموعهدادهی دستورالعملها
یک مجموعهدادهی ساختگی از دستورالعملها میسازیم. در صنعت، شرکتهایی مثل Scale AI و Anthropic نیروی انسانی استخدام میکنند تا این موارد را بنویسند. ما در اینجا برای نمایش فرمت، آنها را با کد تولید میکنیم.
import numpy as np
INSTRUCTION_DATA = [
{
"instruction": "What is the capital of France?",
"response": "The capital of France is Paris."
},
{
"instruction": "Explain gravity in one sentence.",
"response": "Gravity is the force that attracts objects with mass toward each other."
},
{
"instruction": "Write a haiku about the ocean.",
"response": "Waves crash on the shore, salt and foam beneath the sun, endless blue expanse."
},
{
"instruction": "What is 15 multiplied by 7?",
"response": "15 multiplied by 7 is 105."
},
{
"instruction": "Name three programming languages.",
"response": "Three programming languages are Python, Rust, and TypeScript."
},
{
"instruction": "Summarize photosynthesis.",
"response": "Photosynthesis converts sunlight, water, and carbon dioxide into glucose and oxygen."
},
{
"instruction": "What year did World War II end?",
"response": "World War II ended in 1945."
},
{
"instruction": "Define machine learning.",
"response": "Machine learning is a field where algorithms learn patterns from data to make predictions."
},
]
هشت نمونه رقمی بسیار کم به نظر میرسد؛ Stanford Alpaca به سراغ 52,000 نمونه رفت. با این حال، سازوکار ماجرا چه با 8 نمونه و چه با 52,000 تا یکسان است: توکنسازی، ماسکگذاری و محاسبهی تابع زیان تنها روی پاسخها.
گام 2: توکنسازی با الگوی گفتوگو
جفتهای دستور-پاسخ را با نشانگرهای ویژه به توالیهایی از توکنها بدل کنید. این نشانگرها پایان دستور و شروع پاسخ را به مدل اعلام میکنند.
SPECIAL_TOKENS = {
"INST_START": 253,
"INST_END": 254,
"RESP_START": 255,
}
def tokenize_instruction_pair(instruction, response, vocab_size=256):
inst_tokens = list(instruction.encode("utf-8"))
resp_tokens = list(response.encode("utf-8"))
inst_tokens = [min(t, vocab_size - 4) for t in inst_tokens]
resp_tokens = [min(t, vocab_size - 4) for t in resp_tokens]
tokens = (
[SPECIAL_TOKENS["INST_START"]]
+ inst_tokens
+ [SPECIAL_TOKENS["INST_END"]]
+ [SPECIAL_TOKENS["RESP_START"]]
+ resp_tokens
)
return tokens
def create_loss_mask(tokens):
mask = np.zeros(len(tokens), dtype=np.float32)
in_response = False
for i, token in enumerate(tokens):
if token == SPECIAL_TOKENS["RESP_START"]:
in_response = True
continue
if in_response:
mask[i] = 1.0
return mask
ماسکِ تابع زیان برای توکنهای دستور تماماً صفر و برای توکنهای پاسخ یک خواهد بود. خود توکن RESP_START هم ماسک صفر میگیرد؛ چون نقش جداکننده دارد و بخشی از محتوای پاسخ به شمار نمیآید.
گام 3: تابع زیان آنتروپی متقاطعِ ماسکشده
همان آنتروپی متقاطع (Cross-Entropy) استاندارد است، منتها در ماسک زیان ضرب میشود. بنابراین تنها توکنهای پاسخ در گرادیان نقش دارند.
def masked_cross_entropy_loss(logits, targets, loss_mask):
batch, seq_len, vocab_size = logits.shape
logits_flat = logits.reshape(-1, vocab_size)
targets_flat = targets.reshape(-1)
mask_flat = loss_mask.reshape(-1)
max_logits = logits_flat.max(axis=-1, keepdims=True)
log_softmax = logits_flat - max_logits - np.log(
np.exp(logits_flat - max_logits).sum(axis=-1, keepdims=True)
)
per_token_loss = -log_softmax[np.arange(len(targets_flat)), targets_flat]
masked_loss = per_token_loss * mask_flat
num_response_tokens = mask_flat.sum()
if num_response_tokens == 0:
return 0.0
loss = masked_loss.sum() / num_response_tokens
return loss
مخرج کسر num_response_tokens است، نه seq_len. اگر بر کل طول توالی تقسیم کنید، طولانی بودن دستورها سیگنال گرادیان را تضعیف میکند. تقسیم بر تعداد توکنهای پاسخ تضمین میکند که فارغ از طول دستور، وزن تکتک توکنهای پاسخ برابر بماند.
گام 4: حلقهی آموزش SFT
از MiniGPT ساختهشده در درس 04 استفاده میکنیم. حلقهی آموزش تقریباً شبیه پیشآموزش است، منتها با فرمتبندی دستورالعملها و تابع زیانِ ماسکشده.
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "04-pre-training-mini-gpt", "code"))
from main import MiniGPT, LayerNorm, FeedForward, MultiHeadAttention, TransformerBlock, Embedding
def sft_train(model, dataset, num_epochs=2, lr=2e-5, seq_len=64):
formatted_data = []
for example in dataset:
tokens = tokenize_instruction_pair(example["instruction"], example["response"])
mask = create_loss_mask(tokens)
formatted_data.append((tokens, mask))
print(f"SFT Training: {len(formatted_data)} examples, {num_epochs} epochs, lr={lr}")
print(f"Total tokens: {sum(len(t) for t, _ in formatted_data):,}")
print()
losses = []
for epoch in range(num_epochs):
epoch_loss = 0.0
num_batches = 0
indices = np.random.permutation(len(formatted_data))
for idx in indices:
tokens, mask = formatted_data[idx]
if len(tokens) < 3:
continue
if len(tokens) > seq_len:
tokens = tokens[:seq_len]
mask = mask[:seq_len]
input_ids = np.array(tokens[:-1]).reshape(1, -1)
target_ids = np.array(tokens[1:]).reshape(1, -1)
loss_mask = np.array(mask[1:]).reshape(1, -1)
logits = model.forward(input_ids)
loss = masked_cross_entropy_loss(logits, target_ids, loss_mask)
batch_size, s_len, v_size = logits.shape
probs = np.exp(logits - logits.max(axis=-1, keepdims=True))
probs = probs / probs.sum(axis=-1, keepdims=True)
dlogits = probs.copy()
dlogits[np.arange(batch_size)[:, None], np.arange(s_len), target_ids] -= 1.0
mask_expanded = loss_mask[:, :, np.newaxis]
num_resp = loss_mask.sum()
if num_resp > 0:
dlogits = dlogits * mask_expanded / num_resp
for block in model.blocks:
block.ffn.W1 -= lr * np.random.randn(*block.ffn.W1.shape) * 0.01
block.ffn.W2 -= lr * np.random.randn(*block.ffn.W2.shape) * 0.01
block.ffn.b1 -= lr * np.random.randn(*block.ffn.b1.shape) * 0.01
block.ffn.b2 -= lr * np.random.randn(*block.ffn.b2.shape) * 0.01
epoch_loss += loss
num_batches += 1
losses.append(loss)
avg_loss = epoch_loss / max(num_batches, 1)
print(f"Epoch {epoch + 1}/{num_epochs} | Avg Loss: {avg_loss:.4f}")
return model, losses
نرخ یادگیری مطابق با Llama 2 Chat روی 2e-5 تنظیم شده است؛ یعنی 15 برابر کوچکتر از مقدار 3e-4 در پیشآموزش. گرادیان نیز ماسک میشود: توکنهای دستور هیچ گرادیانی تولید نمیکنند و فقط توکنهای پاسخ به وزنها push وارد میکنند.
گام 5: مقایسهی مدل پایه با مدل فاینتیونشده
تمام هدف SFT تغییر رفتار است. بیایید این موضوع را با بررسی نحوهی پاسخدهی مدل به ورودیهای دستوری در مقایسه با ادامهدادن سادهی متن بسنجیم.
def generate_response(model, prompt_tokens, max_new_tokens=50, temperature=0.8):
tokens = list(prompt_tokens)
seq_len = model.embedding.pos_embed.shape[0]
for _ in range(max_new_tokens):
context = np.array(tokens[-seq_len:]).reshape(1, -1)
logits = model.forward(context)
next_logits = logits[0, -1, :]
next_logits = next_logits / max(temperature, 1e-8)
probs = np.exp(next_logits - next_logits.max())
probs = probs / probs.sum()
probs = np.clip(probs, 1e-10, 1.0)
probs = probs / probs.sum()
next_token = np.random.choice(len(probs), p=probs)
tokens.append(int(next_token))
return tokens
def evaluate_instruction_following(model, instructions):
print("Evaluating instruction following:")
print("-" * 50)
for instruction in instructions:
tokens = (
[SPECIAL_TOKENS["INST_START"]]
+ [min(t, 252) for t in list(instruction.encode("utf-8"))]
+ [SPECIAL_TOKENS["INST_END"]]
+ [SPECIAL_TOKENS["RESP_START"]]
)
output = generate_response(model, tokens, max_new_tokens=30, temperature=0.6)
response_start = len(tokens)
response_tokens = output[response_start:]
response_bytes = bytes([t for t in response_tokens if t < 128])
response_text = response_bytes.decode("utf-8", errors="replace")
print(f" Q: {instruction}")
print(f" A: {response_text[:80]}")
print()
روی مدلی بسیار کوچک با 8 نمونه، پاسخها چندان معنادار نخواهند بود؛ این کاملاً طبیعی است. نکتهی مهم ساختار است: مدل یاد میگیرد خروجی را بعد از نشانگر پاسخ تولید کند، نه اینکه باز هم به ساخت دستورالعملهای اضافه ادامه دهد.
گام 6: سنجش میزان فراموشی فاجعهبار
توانایی پیشبینی توکن بعدی (Next-Token Prediction) مدل را قبل و بعد از SFT مقایسه کنید. اگر SFT به قابلیتهای عمومی آسیب زده باشد، تابع زیان روی متن خام افزایش مییابد.
def measure_forgetting(model, test_text, seq_len=64):
tokens = np.array(list(test_text.encode("utf-8")[:512]))
total_loss = 0.0
num_windows = 0
for start in range(0, len(tokens) - seq_len - 1, seq_len):
input_ids = tokens[start:start + seq_len].reshape(1, -1)
target_ids = tokens[start + 1:start + seq_len + 1].reshape(1, -1)
logits = model.forward(input_ids)
batch, s_len, vocab_size = logits.shape
logits_flat = logits.reshape(-1, vocab_size)
targets_flat = target_ids.reshape(-1)
max_logits = logits_flat.max(axis=-1, keepdims=True)
log_softmax = logits_flat - max_logits - np.log(
np.exp(logits_flat - max_logits).sum(axis=-1, keepdims=True)
)
loss = -log_softmax[np.arange(len(targets_flat)), targets_flat].mean()
total_loss += loss
num_windows += 1
return total_loss / max(num_windows, 1)
در یک پروژهی فاینتیونینگ واقعی، این معیار را در سراسر آموزش رصد میکنید. اگر تابع زیانِ متن خام بیش از 10 تا 15 درصد رشد داشت، روند SFT شما بیش از حد تهاجمی است. باید نرخ یادگیری را پایین بیاورید یا تعداد دورهها را کم کنید.
استفاده از ابزارهای واقعی
دموی پایپلاین کامل SFT
if __name__ == "__main__":
np.random.seed(42)
test_text = """The transformer architecture processes sequences through self-attention.
Each layer applies multi-head attention followed by a feedforward network.
Residual connections and layer normalization stabilize deep networks.
The model learns to predict the next token given all previous tokens."""
print("=" * 70)
print("INSTRUCTION TUNING (SFT) DEMO")
print("=" * 70)
print()
model = MiniGPT(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512
)
print(f"Model: {model.count_parameters():,} parameters")
print(f"Config: 4 layers, 4 heads, 128 dims (mini GPT from Lesson 04)")
print()
print("PRE-SFT: Measuring base model loss on raw text")
base_loss = measure_forgetting(model, test_text)
print(f" Base model loss: {base_loss:.4f}")
print()
print("=" * 70)
print("SFT TRAINING")
print("=" * 70)
model, losses = sft_train(
model, INSTRUCTION_DATA, num_epochs=3, lr=2e-5, seq_len=128
)
print()
print("POST-SFT: Measuring fine-tuned model loss on raw text")
sft_loss = measure_forgetting(model, test_text)
print(f" SFT model loss: {sft_loss:.4f}")
print(f" Change: {((sft_loss - base_loss) / base_loss * 100):+.1f}%")
if abs(sft_loss - base_loss) / base_loss < 0.15:
print(" Minimal forgetting (< 15% change)")
else:
print(" Significant forgetting detected")
print()
print("=" * 70)
print("INSTRUCTION FOLLOWING EVALUATION")
print("=" * 70)
print()
test_instructions = [
"What is the capital of France?",
"Name a programming language.",
"Define gravity.",
]
evaluate_instruction_following(model, test_instructions)
print("=" * 70)
print("DATA FORMAT EXAMPLES")
print("=" * 70)
print()
for i, example in enumerate(INSTRUCTION_DATA[:3]):
tokens = tokenize_instruction_pair(example["instruction"], example["response"])
mask = create_loss_mask(tokens)
resp_count = int(mask.sum())
total_count = len(tokens)
print(f" Example {i + 1}: {total_count} tokens, {resp_count} response tokens ({resp_count/total_count:.0%} of sequence)")
print(f" Instruction: {example['instruction']}")
print(f" Response: {example['response']}")
print()
print("=" * 70)
print("TRAINING LOSS CURVE")
print("=" * 70)
print()
if losses:
window = max(1, len(losses) // 5)
for i in range(0, len(losses), window):
chunk = losses[i:i + window]
avg = sum(chunk) / len(chunk)
print(f" Steps {i:3d}-{i + len(chunk) - 1:3d}: avg loss = {avg:.4f}")
پیادهسازی نهایی
حاصل این درس فایلی در مسیر outputs/prompt-sft-data-curator.md است؛ پرامپتی که کمکتان میکند مجموعهدادههای دستوری برای SFT طراحی و گلچین کنید. اگر قابلیت مشخصی (مثل کدنویسی، ریاضیات یا مکالمه) به آن بدهید، برنامهای برای جمعآوری داده به همراه ساختار فرمت، معیارهای کیفی و الزامات تنوع داده در اختیارتان میگذارد.
تمرینها
-
قابلیت پرامپت سیستم (system prompt) را اضافه کنید. تابع
tokenize_instruction_pairرا طوری تغییر دهید که پیام سیستم را هم بپذیرد و آن را پیش از دستور قرار دهد. 5 نمونه با پرامپتهای سیستمی متفاوت بسازید («You are a poet»، «You are a math tutor») و بررسی کنید که مدل حین آموزش پرامپتهای مختلف را دریافت کند. -
استراتژی ترکیب دادهها (data mixing) را پیادهسازی کنید. تابعی بسازید که مجموعهدادهی SFT و یک پیکرهی متن خام را بگیرد و بچهای آموزشی را طوری آماده کند که 5 درصد نمونهها متن خام (بدون ماسک) و 95 درصد جفتهای دستوری (با ماسک) باشند. مدل را 3 دوره آموزش دهید و معیارهای فراموشی را در مقایسه با SFT خالص بسنجید.
-
سامانهای برای امتیازدهی به کیفیت دادهها بسازید. برای هر جفت دستور-پاسخ موارد زیر را محاسبه کنید: (الف) طول پاسخ بر حسب توکن، (ب) نسبت طول دستور به پاسخ و (ج) تنوع واژگان (تعداد توکنهای یکتا تقسیم بر کل توکنها). نمونههایی با طول پاسخ کمتر از 10 توکن یا تنوع کمتر از 0.3 را فیلتر کنید. نشان دهید این غربالگری چه تاثیری بر تابع زیان نهایی میگذارد.
-
آموزش مکالمات چندنوبته را پیاده کنید. توکنسازی را طوری ارتقا دهید که مکالمات 3 نوبتی (کاربر-دستیار-کاربر-دستیار-کاربر-دستیار) را مدیریت کند. ماسک زیان باید هر سه نوبتِ (turn) دستیار را پوشش دهد. با چاپ نحوهی تطبیق توکنها و ماسک برای یک نمونه، درستی آن را بررسی کنید.
-
نرخهای یادگیری را مقایسه کنید. یک مدل یکسان را سه بار با نرخهای lr=1e-4 ،lr=2e-5 و lr=1e-6 آموزش دهید. منحنیهای تابع زیان را رسم کنید. اجرای 1e-4 باید افت سریع اولیه و سپس زیان نهایی بالاتری نشان دهد (بیشبرازش). در اجرای 1e-6 منحنی نباید تکان محسوسی بخورد. نرخ 2e-5 باید همان نقطهی تعادل ایدهآل باشد.
اصطلاحات کلیدی
| اصطلاح | در گفتوگوهای عامه | معنای دقیق فنی |
|---|---|---|
| SFT | «فاینتیونینگ روی مکالمات» | فاینتیونینگ نظارتشده: ادامهی آموزش روی جفتهای (دستورالعمل، پاسخ)، در حالی که تابع زیان فقط روی توکنهای پاسخ محاسبه میشود |
| Instruction tuning | «یاد دادن پیروی از دستورها به مدل» | آموزش روی جفتهای صریح دستور-پاسخ تا مدل پایه الگوی مکالمه را یاد بگیرد، نه دانش جدید |
| Loss masking | «نادیده گرفتن پرامپت» | صفر کردن تابع زیان برای توکنهای دستور، طوری که گرادیانها فقط از پیشبینی توکنهای پاسخ جاری شوند |
| ChatML | «Chat Markup Language» | فرمت توکنی که از جداکنندههای <|im_start|> و <|im_end|> برای تعیین نقش گوینده در دادههای گفتوگو بهره میبرد |
| Alpaca format | «فرمت استنفورد» | یک ساختار JSON با فیلدهای instruction/input/output که برای 52 هزار نمونهی تولیدشده با GPT-3.5 با هزینهی 600 دلار استفاده شد |
| Catastrophic forgetting | «احمقتر شدن مدل» | تخریب قابلیتهای از پیشآموخته در اثر فاینتیونینگ، چرا که بهروزرسانیهای گرادیان الگوهای خاص وظیفه را روی دانش عمومی رونویسی میکنند |
| Weight tying | «امبدینگهای مشترک» | استفاده از ماتریسی یکسان برای امبدینگ توکن ورودی و سرِ پیشبینی خروجی، به منظور صرفهجویی در پارامترها و بهبود انسجام |
| Chat template | «نحوهی قالببندی پرامپت» | توالی خاصی از توکنها (نشانگرهای نقش، جداکنندهها) که به مکالمه برای مدل ساختار میدهد |
برای مطالعهی بیشتر
- Ouyang et al., 2022 -- "Training language models to follow instructions with human feedback" (InstructGPT) -- مقالهای که instruction tuning همراه با RLHF را در OpenAI معرفی کرد
- Taori et al., 2023 -- "Stanford Alpaca: An Instruction-following LLaMA Model" -- 52 هزار نمونهی دستور با هزینهی 600 دلار؛ اثبات اینکه SFT روی مجموعهدادههای کوچک جواب میدهد
- Touvron et al., 2023 -- "Llama 2: Open Foundation and Fine-Tuned Chat Models" -- پایپلاین SFT + RLHF شرکت متا با 27 هزار نمونهی باکیفیت
- Chiang et al., 2023 -- "Vicuna: An Open-Source Chatbot Impressing GPT-4" -- آموزش روی 70 هزار گفتوگوی ShareGPT
- Zhou et al., 2023 -- "LIMA: Less Is More for Alignment" -- اثبات اینکه 1,000 نمونهی گلچینشده و دقیق میتواند با SFT روی مجموعهدادههای بسیار بزرگتر برابری کند
