منبع: تصویر تولید شده با استفاده از GPT-4o
منبع: تصویر تولید شده با استفاده از GPT-4o

از اصول اولیه: ساخت فراخوانی تابع با تنظیم دقیق NanoGPT

آنچه در این راهنما خواهید آموخت

در این آموزش عملی، خواهید آموخت:

  • چگونه فراخوانی تابع را از ابتدا بسازید - بدون جعبه‌های سیاه، بدون جادو، فقط PyTorch و Tiktoken خالص
  • راز پشت فراخوانی تابع کارآمد - آموزش مدل‌ها برای تولید خروجی‌های ساختاریافته بدون بزرگ کردن درخواست‌ها
  • یک رویکرد تنظیم دقیق ساده که حتی برای مدل‌های زبانی کوچکتر نیز کار می‌کند
  • تکنیک‌های عملی برای بهینه‌سازی حلقه آموزش شامل پوشش‌دهی تلفات سفارشی و آماده‌سازی استراتژیک داده‌ها

چه یک مهندس ML باشید که به دنبال سفارشی کردن قابلیت‌های LLM است، چه یک محقق که به دنبال تنظیم دقیق مدل است، یا یک توسعه‌دهنده که می‌خواهد فراخوانی تابع را به برنامه‌های خود اضافه کند، این راهنما طرح کامل پیاده‌سازی با حداقل وابستگی‌ها را ارائه می‌دهد.

سفر ما در این پست وبلاگ در مورد رمزگشایی این فناوری است.

آنچه این رویکرد را واقعاً متمایز می‌کند این است که ما مدل را آموزش می‌دهیم تا فراخوانی‌های تابع ساختاریافته را بدون جاسازی تعاریف تابع در هر درخواست تولید کند.

در این پست وبلاگ، ما اجرای قابلیت‌های فراخوانی تابع را با تنظیم دقیق یک مدل NanoGPT-مانند با استفاده از یک رویکرد خالص و از ابتدا بررسی خواهیم کرد.

1. کنترل معماری کامل: با ساختن از پایه، انعطاف‌پذیری بی‌سابقه‌ای برای اصلاح و بهینه‌سازی هر جزء از مدل بدون محدود شدن توسط APIهای کتابخانه خارجی به دست می‌آوریم.

2. درک فنی عمیق: فرآیند اجرای دستی هر جزء، درک عمیق و دقیق از عملکرد داخلی مدل را اجباری می‌کند، و بینش‌هایی را آشکار می‌کند که کتابخانه‌های انتزاعی اغلب پنهان می‌کنند.

3. عملکرد و کارایی: با کنترل کامل بر پیاده‌سازی، می‌توانیم کد را به طور دقیق برای مورد استفاده خاص خود بهینه کنیم، سربار غیرضروری را حذف کرده و معماری را دقیقاً مطابق با نیازهای خود تنظیم کنیم.

4. حداقل ردپای وابستگی: این رویکرد منجر به یک پایگاه کد سبک و کم‌وابستگی می‌شود که نگهداری، استقرار و درک آن آسان‌تر است.

کد کامل برای این پیاده‌سازی در GitHub موجود است: https://github.com/suyashh94/finetune-function-calling-from-scratch

ما عمیقاً در کد غواصی خواهیم کرد، هر جزء را توضیح می‌دهیم و نشان می‌دهیم که چگونه یک معماری نسبتاً ساده می‌تواند برای پشتیبانی از فراخوانی تابع ساختاریافته پیچیده گسترش یابد.

درک تنظیم دقیق نظارت‌شده و فراخوانی تابع

بیایید با اصول اولیه شروع کنیم. تنظیم دقیق نظارت‌شده (SFT) تکنیکی است که در آن یک مدل زبانی از پیش آموزش‌دیده بر روی مثال‌های خاصی بیشتر آموزش داده می‌شود تا رفتار آن را به سمت یک سبک یا قابلیت خاص هدایت کند.

فراخوانی تابع، علی‌رغم ماهیت به ظاهر پیچیده آن، اساساً فقط یک شکل تخصصی از تنظیم دقیق نظارت‌شده است.

ما می‌خواهیم:

User: What's the weather like in San Francisco? Assistant: <functioncall> {"name": "get_weather", "arguments": "{'location': 'San Francisco'}"} </functioncall>

در اینجا، get_weather یک تابع از پیش تعریف شده در سیستم ما است که یک پارامتر مکان را می‌پذیرد و اطلاعات آب و هوا را برمی‌گرداند.

تفاوت اساسی در این رویکرد فراخوانی تابع در اجتناب از توضیحات تابع جاسازی‌شده در داخل پنجره زمینه است.

بسیار مهم است که روشن کنیم که "فراخوانی تابع" یک اسم نادرست است - مدل در واقع توابع را اجرا نمی‌کند، بلکه دستورالعمل‌های فراخوانی تابع ساختاریافته را با استفاده از توکن‌های تخصصی تولید می‌کند.

در اکثر سیستم‌های فراخوانی تابع - مانند سیستم‌های ساخته شده توسط OpenAI، Claude یا Mistral - مدل از قبل نمی‌داند که کدام توابع را می‌تواند استفاده کند. بنابراین هر بار که از مدل درخواست می‌کنید، باید به آن بگویید که چه توابعی در دسترس هستند، چه نام دارند، چه پارامترهایی می‌گیرند و چگونه از آنها استفاده کنید.

بیایید بگوییم شما 5 تابع دارید:

  • get_weather(location)
  • set_temperature(temperature, unit)
  • adjust_fan_speed(speed)
  • play_music(song_name)
  • turn_on_lights(location)

?? سنتی (فراخوانی تابع در متن)

شما باید چیزی شبیه به این را در هر درخواست وارد کنید:

{   "functions": [     {       "name": "get_weather",       "description": "Get current weather for a location",       "parameters": {         "type": "object",         "properties": {           "location": { "type": "string" }         },         "required": ["location"]       }     },     {       "name": "set_temperature",       "description": "Set the car temperature",       "parameters": {         "type": "object",         "properties": {           "temperature": { "type": "number" },           "unit": { "type": "string" }         },         "required": ["temperature", "unit"]       }     },     {       "name": "adjust_fan_speed",       "description": "Adjust fan speed",       "parameters": {         "type": "object",         "properties": {           "speed": { "type": "string", "enum": ["low", "medium", "high"] }         },         "required": ["speed"]       }     },     {       "name": "play_music",       "description": "Play a specific song",       "parameters": {         "type": "object",         "properties": {           "song_name": { "type": "string" }         },         "required": ["song_name"]       }     },     {       "name": "turn_on_lights",       "description": "Turn on lights in a location",       "parameters": {         "type": "object",         "properties": {           "location": { "type": "string" }         },         "required": ["location"]       }     }   ],   "user": "Can you turn on the lights in the kitchen?" }

مدل اکنون باید همه اینها را بخواند، همه توابع را بفهمد، درخواست شما را مطابقت دهد، و سپس یک تماس صحیح را فرمت کند.

?? تنظیم دقیق (بدون تعاریف در متن)

در مورد ما، همان درخواست به این شکل است:

{   "user": "Can you turn on the lights in the kitchen?" }

و مدل به سادگی پاسخ می‌دهد:

<functioncall>{"name": "turn_on_lights", "arguments": "{'location': 'kitchen'}"}</functioncall>

همین.
هیچ لیست تابعی، هیچ طرحی، هیچ فراداده‌ای، هیچ تورم JSON وجود ندارد. با پختن تعاریف تابع مستقیماً در وزنه‌های مدل، ما فراخوانی تابع را به یک وظیفه تولید تبدیل می‌کنیم، نه یک وظیفه تجزیه طرح.

این امر سربار ثابت شامل طرح‌های JSON طولانی را از بین می‌برد، استنتاج را سریع‌تر و لاغرتر می‌کند و قابلیت استفاده را برای مدل‌های کوچکتر یا جاسازی‌شده به شدت بهبود می‌بخشد.

الزامات مجموعه داده برای فراخوانی تابع

قبل از غواصی در پیاده‌سازی، ارزش دارد که به طور خلاصه در مورد الزامات مجموعه داده بحث کنیم. برای اینکه فراخوانی تابع به طور مؤثر کار کند، داده‌های آموزشی شما باید فضای پارامتر توابع شما را به اندازه کافی پوشش دهد.

این مثال را از مجموعه داده ما در نظر بگیرید:

{ "system": "<|im_start|>system You are a helpful assistant. You have to either provide a way to answer user's request or answer user's query. <|im_end|> ", "user": "<|im_start|>user Set the temperature to 22 degrees Celsius.<|im_end|> ", "assistant": "<|im_start|>assistant <functioncall> {\"name\": \"set_temperature\", \"arguments\": \"{'temperature': 22, 'unit': 'celsius'}\"} <|im_end|><|endoftext|>" }

اکنون آن را با مثال دیگری مقایسه کنید:

{ "system": "<|im_start|>system You are a helpful assistant. You have to either provide a way to answer user's request or answer user's query. <|im_end|> ", "user": "<|im_start|>user Can you set it to 65 degrees Fahrenheit?<|im_end|> ", "assistant": "<|im_start|>assistant <functioncall> {\"name\": \"set_temperature\", \"arguments\": \"{'temperature': 65, 'unit': 'fahrenheit'}\"} <|im_end|><|endoftext|>" }

توجه داشته باشید که چگونه ما مثال‌هایی با مقادیر مختلف دما و سیستم‌های واحد را وارد کرده‌ایم. برای توابعی با پارامترهای دسته‌بندی (مانند "بالا"، "متوسط"، "پایین")، شما مثال‌هایی می‌خواهید که هر مقدار ممکن را پوشش دهند:

{ "system": "<|im_start|>system You are a helpful assistant. You have to either provide a way to answer user's request or answer user's query. <|im_end|> ", "user": "<|im_start|>user Set the fan speed to high.<|im_end|> ", "assistant": "<|im_start|>assistant <functioncall> {\"name\": \"set_fan_speed\", \"arguments\": \"{'speed': 'high'}\"} <|im_end|><|endoftext|>" }

سنگ بنای فراخوانی تابع مؤثر در غنا و تنوع داده‌های آموزشی شما نهفته است. این رویکرد مستلزم پوشش جامع تغییرات زبانی و ترکیبات پارامتر است. برای هر تابع، ما به طور دقیق مثال‌های آموزشی تولید می‌کنیم که هر جایگشت ممکن از پارامترها را بررسی می‌کند، و اطمینان حاصل می‌کند که مدل می‌تواند طیف گسترده‌ای از تعاملات کاربر را مدیریت کند.

  • تنوع زبانی
  • بررسی جامع پارامتر
  • پیچیدگی تعامل

هدف ایجاد یک مجموعه داده آنقدر جامع است که مدل بتواند تقریباً هر درخواست کاربر را بدون توجه به نحوه بیان یا ناقص بودن درخواست اولیه، به فراخوانی تابع صحیح ترجمه کند.

معماری اصلاح شده NanoGPT

این پیاده‌سازی بر اساس NanoGPT، پیاده‌سازی GPT مینیمالیستی آندری کارپاتی ساخته شده است. با این حال، ما چندین اصلاح کلیدی برای پشتیبانی از قابلیت‌های فراخوانی تابع ایجاد کرده‌ایم.

اولین تغییر مهم در توکن‌ساز است. ما توکن‌ساز پایه GPT-2 را با توکن‌های ویژه گسترش داده‌ایم که به تفکیک بخش‌های مختلف ورودی و خروجی کمک می‌کنند:

import tiktoken  class Encoder:     def __init__(self):         gpt2_base = tiktoken.get_encoding("gpt2")          # In production, load the arguments directly instead of accessing private attributes         # See openai_public.py for examples of arguments for specific encodings         enc = tiktoken.Encoding(             # If you're changing the set of special tokens, make sure to use a different name             # It should be clear from the name what behaviour to expect.             name="gpt_instruct",             pat_str=gpt2_base._pat_str,             mergeable_ranks=gpt2_base._mergeable_ranks,             special_tokens={                 **gpt2_base._special_tokens,                 "<|pad_token|>": 50257,                 "<|eop_token|>": 50258,             },         )         enc.pad_token = 50257         enc.eop_token = 50258          self.encoder = enc   if __name__ == "__main__":     encoder = Encoder()     instruction = "Write a summary of the given text."     print("Instruction:", instruction)      encoded_instr = encoder.encoder.encode_ordinary(instruction)     print("Encoded instruction:", encoded_instr)      decoded_instr = encoder.encoder.decode(encoded_instr)     print("Decoded instruction:", decoded_instr)

این رمزگذار دو توکن ویژه فراتر از واژگان پایه GPT-2 اضافه می‌کند:

· <|pad_token|> (شناسه توکن 50257): برای پر کردن توالی‌ها به طول ثابت استفاده می‌شود

· <|eop_token|> (شناسه توکن 50258): توکن پایان درخواست که مرز بین درخواست و پاسخ را مشخص می‌کند

توجه: در یک سناریوی ایده‌آل، ما همچنین <functioncall> و </functioncall> را به عنوان توکن‌های ویژه در رمزگذار خود اضافه می‌کنیم، زیرا آنها برای پیاده‌سازی فراخوانی تابع ما اساسی هستند.

معماری مدل پایه تا حد زیادی از NanoGPT بدون تغییر باقی می‌ماند، با یک معماری ترانسفورماتور استاندارد شامل بلوک‌های خودتوجهی و یک MLP.

پردازش مجموعه داده برای فراخوانی تابع

قلب این پیاده‌سازی در نحوه پردازش مجموعه داده نهفته است. بیایید تابع کلیدی را بررسی کنیم:

def process_dataset(dataset, enc, input_len=1024):     data = json.loads(dataset["text"])     system_prompt = "system: " + data["system"] + "\n"     user_prompt = "user: " + data["user"] + "\n"     response = "assistant: " + data["assistant"]      prompt = system_prompt + user_prompt     prompt_ids = enc.encode_ordinary(prompt)     prompt_id_len = len(prompt_ids)     prompt_ids.append(enc.eop_token)      response_ids = enc.encode_ordinary(response)     response_ids.append(enc.eot_token)     prompt_ids = prompt_ids + response_ids     prompt_response_len = len(prompt_ids)      prompt_ids = prompt_ids + [enc.pad_token] * (input_len - len(prompt_ids) + 1)     prompt_ids = np.array(prompt_ids, dtype=np.uint16)      prompt_mask = np.array([1] * prompt_id_len + [0] * (input_len - prompt_id_len))     prompt_mask = np.array(prompt_mask, dtype=np.uint8)      pad_mask = np.array([0] * input_len)     pad_mask[prompt_response_len - 1 :] = 1     pad_mask = np.array(pad_mask, dtype=np.uint8)      out = {         "output_ids": prompt_ids,         "length": prompt_response_len,         "prompt_mask": prompt_mask,         "pad_mask": pad_mask,     }      return out

بیایید آنچه در اینجا اتفاق می‌افتد را بررسی کنیم:

1. ما پیام سیستم و ورودی کاربر را برای تشکیل درخواست کامل ترکیب می‌کنیم

2. ما این درخواست را به توکن‌ها رمزگذاری می‌کنیم و طول آن را ثبت می‌کنیم

3. ما توکن پایان درخواست (EOP) را برای علامت‌گذاری پایان درخواست اضافه می‌کنیم

4. ما پاسخ دستیار (که حاوی فراخوانی تابع است) را رمزگذاری می‌کنیم

5. ما توکن پایان متن (EOT) را برای علامت‌گذاری پایان پاسخ اضافه می‌کنیم

6. ما این توالی‌ها را ترکیب می‌کنیم و آنها را به طول مورد نیاز پر می‌کنیم

7. ما دو آرایه ماسک ایجاد می‌کنیم:

· prompt_mask: نشان می‌دهد که کدام توکن‌ها به درخواست اصلی تعلق دارند

· pad_mask: نشان می‌دهد که کدام توکن‌ها پرکننده هستند

این پردازش به ما امکان می‌دهد تا در طول آموزش بین بخش‌های مختلف توالی ورودی تمایز قائل شویم، که برای نحوه محاسبه تلفات بسیار مهم است.

محاسبه تلفات و فرآیند آموزش

اکنون، بیایید بررسی کنیم که چگونه فرآیند آموزش کار می‌کند. در آموزش مدل زبانی معمولی، ما تلفات را روی تمام توکن‌ها با پیش‌بینی هر توکن بر اساس توکن‌های قبلی محاسبه می‌کنیم.

در این پیاده‌سازی، اجزای کلیدی در روش forward مدل GPT و حلقه آموزش قرار دارند:

def forward(self, idx, targets=None, prompt_idx=None, answer_idx=None, return_losses_separately=False):     device = idx.device     b, t = idx.size()          # Standard positioning and embedding     pos = torch.arange(0, t, dtype=torch.long, device=device)     tok_emb = self.transformer.wte(idx)     pos_emb = self.transformer.wpe(pos)     x = self.transformer.drop(tok_emb + pos_emb)          # Process through transformer blocks     for block in self.transformer.h:         x = block(x)     x = self.transformer.ln_f(x)          # Calculate loss if targets provided     if targets is not None:         logits = self.lm_head(x)                  if not return_losses_separately:             loss = F.cross_entropy(                 logits.view(-1, logits.size(-1)),                  targets.view(-1),                  ignore_index=-1  # Ignore tokens marked with -1             )         else:             # For separate loss tracking (prompt vs answer)             loss = F.cross_entropy(                 logits.view(-1, logits.size(-1)),                 targets.view(-1),                 ignore_index=-1,                 reduction="none",             )                          prompt_idx_flatten = prompt_idx.view(-1)             answer_idx_flatten = answer_idx.view(-1)             prompt_loss = loss[prompt_idx_flatten == 1].mean()             answer_loss = loss[answer_idx_flatten == 1].mean()             loss = loss.mean()                          lossDict = {                 "prompt_loss": prompt_loss,                  "answer_loss": answer_loss,                  "loss": loss             }     else:         # Inference time optimization         logits = self.lm_head(x[:, [-1], :])         loss = None          if return_losses_separately:         return logits, loss, lossDict          return logits, loss

و در حلقه آموزش:

def train(self):   # … (training setup code)   for batch in self.train_loader:     # Unpack the batch     input_ids = batch["input_ids"]     padding_mask = batch["padding_mask"]     prompt_mask = batch["prompt_mask"]          # Prepare input and target sequences     X = input_ids[:, :-1].to(self.device)     Y = input_ids[:, 1:].to(self.device)          # Mask out tokens we don't want to calculate loss for     Y[padding_mask == 1] = -1  # Don't calculate loss for padding tokens          # Decide whether to calculate loss on prompt tokens     if hasattr(self.config, "loss_on_prompt") and self.config.loss_on_prompt:         Y[prompt_mask == 1] = 1  # Include prompt tokens in loss     else:         Y[prompt_mask == 1] = -1  # Exclude prompt tokens from loss     # … (forward pass and optimization code)

بینش اساسی در اینجا این است که چگونه از ماسک‌ها استفاده می‌کنیم:

1. ما توکن‌های هدف را که مربوط به پر کردن هستند (padding_mask == 1) را روی -1 تنظیم می‌کنیم

2. ما توکن‌های هدف را که مربوط به درخواست هستند (prompt_mask == 1) را روی -1 تنظیم می‌کنیم

3. تابع تلفات آنتروپی متقابل توکن‌ها را با مقدار هدف -1 نادیده می‌گیرد (ignore_index=-1)

این بدان معناست که مدل فقط برای پیش‌بینی توکن‌ها در پاسخ دستیار آموزش داده می‌شود - به طور خاص، فراخوانی تابع.

بیایید تجسم کنیم که این برای یک مثال چگونه به نظر می‌رسد ("توکن‌سازی عبارت عاقلانه" برای ساده‌سازی فرض می‌شود):

توالی ورودی:

[“You are a helpful assistant”, “Make the fans blow harder”, “<EOP>”, “<functioncall>”, “adjust_fan_speed”, “increase”, … <PAD> <PAD> <PAD>]

توالی هدف (یک موقعیت جابجا شده):

[“Make the fans blow harder”, “<EOP>”, “<functioncall>”, “adjust_fan_speed”, “increase”, …, <PAD> <PAD> <PAD> <PAD>]

با اعمال پوشش (جایی که -1 به معنای "تلفات را برای این توکن محاسبه نکن" است):

[-1, -1, -1, “<functioncall>”, “adjust_fan_speed”, “increase”, …, -1, -1, -1, -1]

مدل فقط برای پیش‌بینی نادرست توکن‌های فراخوانی تابع جریمه می‌شود، و آن را تشویق می‌کند تا نگاشت بین درخواست‌های کاربر و فرمت فراخوانی تابع مناسب را بیاموزد.

ویژگی پیشرفته: تلفات در درخواست

این پیاده‌سازی شامل یک ویژگی منحصر به فرد است: توانایی محاسبه تلفات در توکن‌های درخواست نیز. این توسط پارامتر پیکربندی loss_on_prompt کنترل می‌شود:

if hasattr(self.config, “loss_on_prompt”) and self.config.loss_on_prompt:    Y[prompt_mask == 1] = 1 else:   Y[prompt_mask == 1] = -1

هنگامی که loss_on_prompt فعال است، مدل برای پیش‌بینی توکن‌های درخواست نیز آموزش داده می‌شود. در حالی که این ممکن است غیرمنطقی به نظر برسد (چرا مدل را برای پیش‌بینی چیزی که از قبل می‌داند آموزش دهیم؟)، می‌تواند در سناریوهای خاصی کمک کند:

1. می‌تواند به عنوان نوعی منظم‌سازی عمل کند و از "فراموش کردن" مدل از پیش آموزش خود جلوگیری کند

2. می‌تواند به حفظ قابلیت‌های زبانی عمومی مدل کمک کند در حالی که در فراخوانی تابع تخصص پیدا می‌کند

3. یک سیگنال یادگیری اضافی را فراهم می‌کند که ممکن است عملکرد کلی را بهبود بخشد

در اکثر سناریوهای فراخوانی تابع، ما این را غیرفعال نگه می‌داریم و بر یادگیری برای تولید فراخوانی‌های تابع صحیح به جای پیش‌بینی درخواست تمرکز می‌کنیم.

پیاده‌سازی بدون HuggingFace

معماری مدل اصلی در فایل model.py تعریف شده است، که یک کپی دقیق از nanoGPT است، که اجزای استاندارد GPT را پیاده‌سازی می‌کند:

· LayerNorm: نرمال‌سازی لایه برای تثبیت آموزش

· CausalSelfAttention: مکانیسم خودتوجهی چند سر با پوشش علّی

· MLP: پرسشگر چند لایه برای پردازش پیش‌خور

· Block: توجه و MLP را با اتصالات باقیمانده ترکیب می‌کند

· GPT: همه چیز را در یک مدل زبانی کامل به هم متصل می‌کند

مدل می‌تواند از ابتدا مقداردهی اولیه شود یا از یک چک‌پوینت GPT-2 از پیش آموزش‌دیده از فایلی که از مدل از پیش آموزش‌دیده nanoGPT ذخیره شده است، بارگیری شود.

قرار دادن همه چیز در کنار هم: اجرای آموزش

بیایید بررسی کنیم که چگونه فرآیند آموزش واقعاً اجرا می‌شود. کلاس GPTTrainer در finetune.py کل خط لوله آموزش را مدیریت می‌کند:

def train(self):         t0 = time.time()  raw_model = self.model.module if self.ddp else self.model         running_mfu = -1.0   while True:             if self.iter_num >= self.config.max_iters:                 break   # Set epoch for samplers             if self.ddp:                 self.train_loader.sampler.set_epoch(self.iter_num // len(self.train_loader))  # type: ignore                 self.val_loader.sampler.set_epoch(self.iter_num // len(self.train_loader))  # type: ignore   for batch in self.train_loader:                 # Correctly unpack the batch                 input_ids = batch["input_ids"]                 padding_mask = batch["padding_mask"]                 prompt_mask = batch["prompt_mask"]   X = input_ids[:, :-1].to(self.device)                 Y = input_ids[:, 1:].to(self.device)                 Y[padding_mask == 1] = -1                 if hasattr(self.config, "loss_on_prompt") and self.config.loss_on_prompt:                     Y[prompt_mask == 1] = 1                 else:                     Y[prompt_mask == 1] = -1   lr = (                     self.get_lr(self.iter_num)                     if self.config.decay_lr  else self.config.learning_rate                 )                 for param_group in self.optimizer.param_groups:                     param_group["lr"] = lr   logits, loss = self.model(X, Y, prompt_mask[:, :-1], padding_mask[:, 1:])   if self.ddp:                     self.model.module.zero_grad(set_to_none=True)                 else:                     self.model.zero_grad(set_to_none=True)                 loss.backward()                 torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.config.grad_clip)                 self.optimizer.step()   if self.iter_num % self.config.eval_interval == 0:                     losses = self.estimate_loss()                     print(f"step {self.iter_num}: train loss {losses['train']:.

4f}, val loss {losses['val']:.4f}")                 if self.iter_num % self.config.log_interval == 0:                     t1 = time.time()                     dt = t1 - t0                     t0 = t1                     if self.wandb:                         log = {                             "lr": lr,                             "iter": self.iter_num,                             "train/loss": losses["train"],                             "val/loss": losses["val"],                             "mfu": running_mfu,                             "dt": dt,                         }                          self.wandb.log(log)                 self.iter_num += 1                 t0 = time.time()         if self.wandb:             self.wandb.finish()

· این آموزش برای تعداد مشخصی از تکرارها ادامه می‌یابد و از یک رویکرد رگرسیون برای نرخ یادگیری استفاده می‌کند. این یک روش رایج برای مدل‌های زبانی ترانسفورماتور است. در هر مرحله، آن دسته‌ای از داده‌ها را بازیابی می‌کند، درخواست و توالی پاسخ را آماده می‌کند، پیشروی را انجام می‌دهد و تلفات را محاسبه می‌کند، گرادیان‌ها را محاسبه می‌کند و وزنه‌های مدل را به‌روزرسانی می‌کند.

· به‌طور دوره‌ای، تلفات را در مجموعه اعتبار تخمین می‌زند و پیشرفت آموزش را ثبت می‌کند.

استنتاج و استقرار

پس از اتمام آموزش، ما باید ارزیابی کنیم که چگونه مدل خوب کار می‌کند. ما مجموعه تست را داریم که برای این منظور استفاده می‌کنیم. مجموعه تست مجموعه دیگری از سوالات و پاسخ‌ها دارد که مدل در طول آموزش ندیده است. این را می‌توان برای اندازه‌گیری این که مدل تا چه حد به خوبی قادر است به درخواست‌های جدید که قبلا ندیده است تعمیم دهد استفاده شود.

برای استفاده از این مدل که به خوبی تنظیم شده‌اید، به چیزی شبیه به این نیاز دارید:

text = "write a function call to the below. User: Can you turn on the lights in the kitchen?"  model.eval() with torch.no_grad():   input_ids = torch.tensor(enc.encode(text), dtype=torch.long, device=device).unsqueeze(0)   # Crop input if the input is too long   if input_ids.shape[1] > model.config.block_size:    input_ids = input_ids[:, -model.config.block_size:]          # Pass in the index of the prompt (with prompt_idx_override)   logits, loss, lossDict = model.forward(input_ids, None, None, None)   # Take only the last token for inference (to see what the last token predicts)   logits = logits[:, -1, :]  # Shape: [1, vocab_size]   # Apply softmax to convert logits to probabilities   probabilities = torch.softmax(logits, dim=-1)   # Get the predicted token (highest probability)   predicted_token_id = torch.argmax(probabilities, dim=-1).item()   # Decode the predicted token to string (e.g., 'hello')   predicted_token_str = enc.decode([predicted_token_id]) # [0].tolist())          print(f"Text: {text}")   print(f"Predicted: {predicted_token_str}")   break

در این قطعه کد، ما ابتدا بررسی می‌کنیم که آیا ورودی بسیار طولانی است، و در این صورت آن را به حداکثر اندازه بلوک ترانسفورماتور کوچک می‌کنیم. این برای جلوگیری از سرریز شدن مدل مهم است.

ما همچنین می‌توانیم فهرست ماسک را برای مشخص کردن بخش‌های مختلف تولید در کد استفاده کنیم.

در استنتاج، مدل از یک حلقه رمزگشایی خودکار استفاده می‌کند تا توالی توکن‌ها را یکی یکی تولید کند تا توکن پایان متن (EOT) تولید شود.

نتیجه‌گیری

ما در درک چگونگی ایجاد یک مجموعه داده متنوع و جامع، اصلاح معماری NanoGPT، و مدیریت فرآیند آموزش عمیق‌تر شدیم. این تلاش ما را قادر ساخت تا مدل را به سمت پیش‌بینی فراخوانی‌های تابع ساختاریافته بر اساس ورودی کاربر، بدون نیاز به تعاریف فراخوانی تابع explicit در ورودی‌ها هدایت کنیم.

رویکردی که ارائه کردیم، نه‌تنها قابلیت‌های جدیدی را باز می‌کند، بلکه عملکرد و کارایی را برای مدل‌های استدلالی که اغلب در تنظیمات محدود از نظر منابع مستقر می‌شوند، بهینه می‌کند.

این رویکرد منبع‌محور امکان سفارشی‌سازی، کنترل و کارایی بیشتر، یک جایگزین ارزشمند برای APIهای انتزاعی ارائه می‌دهد.

در این پست وبلاگ، ما یک روش خالص و از ابتدا برای اجرای قابلیت‌های فراخوانی تابع با تنظیم دقیق یک مدل NanoGPT-مانند بررسی کرده‌ایم. با پختن تعاریف تابع مستقیماً در وزنه‌های مدل، ما دیگر به درج مداوم توابع در پنجره متن نیاز نداریم.

با تشکر برای همراهی در این اکتشاف! از شما دعوت می‌کنم تا این مفاهیم را بیشتر بررسی کنید، با این ابزارها بازی کنید و از پتانسیل قدرتمندی که فراخوانی تابع در هوش مصنوعی در آستین دارد، استفاده کنید.