فاز 19 · درس 31

ساخت دیتاست با پنجره‌ی لغزان برای پیش‌آموزش در PyTorch

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
پیش‌نیاز
درس‌های فاز 04، درس‌های ترنسفورمر فاز 07، درس 30 همین فاز
یک نوار فلزی مشبک افقی که فریم‌های مستطیلی شفاف با گام‌های مشخص و متوالی روی آن حرکت می‌کنند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. قرارداد ابعاد داده
  4. چرا پنجره‌ی لغزان؟
  5. کلاس Dataset
  6. بُر زدن قطعی داده‌ها
  7. نمونه‌گیر بچ
  8. شمارش نمونه‌ها
  9. مواردی که در این درس بررسی نمی‌شوند
  10. راهنمای خواندن کد

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

در این درس یاد می‌گیرید که چگونه یک جریان طولانی از شناسه‌های توکن را با روش پنجره‌ی لغزان (sliding window) به نمونه‌های آموزشی با طول زمینه مشخص برش بزنید، کلاس سفارشی Dataset را در PyTorch بسازید و با استفاده از بُر زدن قطعی (deterministic shuffle)، مقایسه‌پذیری آزمایش‌های مختلف را تضمین کنید.

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

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

اهداف یادگیری

  • تبدیل یک پیکره‌ی متنی خام (corpus) به جریانی از شناسه‌های توکن با یک بار فراخوانی توکنایزر.
  • برش زدن جریان شناسه‌ها به پنجره‌هایی با طول ثابت و با استراید (گام لغزش) دارای قابلیت همپوشانی و پیکربندی.
  • ساخت یک Dataset در PyTorch که تنسورهای ورودی و هدف را برای پیش‌بینی توکن بعدی تولید کند.
  • قرار دادن دیتاست درون یک DataLoader به همراه بُر زدن قطعی (deterministic shuffle) با سید مشخص در هر دور (epoch).
  • تحلیل و موازنه‌ی بین استراید، تکرار داده‌ها و اندازه‌ی مؤثر دیتاست.
پنجره‌ی لغزانلغزش پنجره · استراید
SLIDING WINDOWThe tokenizer flattens the corpus into one long id stream. A fixed-length window of T ids slides along it by a stride, and the target is the same window shifted left by one for next-token prediction. Smaller strides overlap more and inflate the example count.جریان شناسه‌های توکن1279345823197764015861322ورودی (B, T)target = input shifted by one
توکنایزر کل پیکره‌ی متنی را به یک جریان پیوسته از شناسه‌ها تبدیل می‌کند. یک پنجره با طول ثابت T به اندازه‌ی استراید در طول آن می‌لغزد و هدف (target) همان پنجره است که برای پیش‌بینی توکن بعدی، یک واحد به چپ شیفت پیدا کرده است. استرایدهای کوچک‌تر هم‌پوشانی بیشتری ایجاد می‌کنند و تعداد نمونه‌ها را افزایش می‌دهند.

صورت مسئله

فرایند پیش‌آموزش در هر گام یک بچ (batch) از شناسه‌های توکن را می‌خواند و مدل را به‌روزرسانی می‌کند. ابعاد هر بچ را قرارداد آموزش تعیین می‌کند. برای یک مدل زبانی علی (causal language model)، هر بچ شامل شناسه‌های ورودی با ابعاد (B, T) و شناسه‌های هدف با همان ابعاد (B, T) است؛ جایی که هدف، در واقع همان ورودی است که یک واحد به چپ شیفت پیدا کرده است. وظیفه‌ی پایپ‌لاین داده این است که چنین قراردادی را بر اساس تقاضا، به‌شکلی قطعی و تکرارپذیر، از دل پیکره‌ای که شاید چندین گیگابایت متن خام باشد تولید کند.

در این درس این پایپ‌لاین را می‌سازیم. توکنایزرِ درسِ قبل، متن را به یک لیست مسطح و طولانی از شناسه‌ها تبدیل می‌کند. یک پنجره‌ی لغزان آن لیست را به نمونه‌های آموزشی برش می‌زند. یک Dataset سفارشی، این نمونه‌ها را در قالب تنسورها ارائه می‌دهد. در نهایت، یک DataLoader آن‌ها را دسته‌بندی (batch) کرده و با یک سید مشخص بُر می‌زند.

قرارداد ابعاد داده

یک مدل زبانی علی، شناسه‌هایی با ابعاد (B, T) مصرف می‌کند که در آن B اندازه‌ی بچ و T طول زمینه (context length) است. هدف در جایگاه t، همان ورودی در جایگاه t+1 است. این یعنی هر نمونه‌ی آموزشی باید T+1 شناسه‌ی خام را پوشش دهد. استرایدِ پنجره مشخص می‌کند که بین نمونه‌های متوالی چقدر همپوشانی وجود داشته باشد.

متن خام پیکرهtokenizer.encodeفهرست تخت شناسه‌هابرش‌دهنده پنجره لغزانid_window_0id_window_1id_window_nدیتاست PyTorchDataLoader با بُر زدن تصادفی (seed)دسته‌های B × T+1 شناسهتقسیم به ورودی و هدف

بخش برش‌دهنده هرگز با انتهای پیکره‌ی متنی همپوشانی برقرار نمی‌کند. اگر پنجره‌ی آخر شناسه‌ی کافی برای پر کردن T+1 جایگاه نداشته باشد، برش‌دهنده آن را کنار می‌گذارد. پدینگ کردنِ بخش انتهایی با <|pad|> هم راه‌حل درستی است، اما ماسک تابع زیان (loss) را پیچیده می‌کند. در این درس، نمونه‌های ناقص انتهایی را نادیده می‌گیریم.

چرا پنجره‌ی لغزان؟

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

استرایدی برابر با T پنجره‌هایی بدون همپوشانی می‌سازد. استرایدی برابر با T // 2 همپوشانی پنجاه درصدی ایجاد کرده و اندازه‌ی مؤثر دیتاست را دو برابر می‌کند. استرایدی معادل 1 بیشترین همپوشانی را تولید کرده و حجم دیتاست را به ضریب T می‌رساند. هزینه‌ی این کار، بار پردازشی بیشتر در هر دور آموزش است؛ اما مزیتش تنوع بیشتر در مرزبندی‌هاست. اکثر فرایندهای پیش‌آموزش استرایدی برابر با طول زمینه انتخاب می‌کنند، چون پیکره‌ی متنی از ابتدا بسیار بزرگ‌تر از چیزی است که مدل بتواند در یک دور تمامش کند؛ بنابراین استدلال تنوع مرزبندی‌ها در آنجا کم‌اهمیت‌تر می‌شود.

کلاس Dataset

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

TokenizerDatasetDataLoaderTrainerTokenizerDatasetDataLoaderTrainertokenizer.encode یک‌بار در زمان ساخت اجرا می‌شودiter(dataloader)__len____getitem__(i)window = ids[start:start+T+1](input_ids, target_ids)"دسته ورودی (B,T)، هدف (B,T)"

عملیات یک واحد شیفت به چپ درون __getitem__ اتفاق می‌افتد. دیتاست خروجی را به‌شکل (input, target) برمی‌گرداند که در آن input = window[:-1] و target = window[1:] است. هر دو از نوع تنسورهای اعداد صحیح بزرگ (long tensors) در PyTorch هستند و حلقه‌ی آموزش آن‌ها را داده‌ی واقعی در نظر می‌گیرد.

بُر زدن قطعی داده‌ها

دیتا لودری که با shuffle=True تنظیم شده باشد، داده‌ها را با استفاده از یک مولد تصادفی در PyTorch می‌خواند. اگر یک torch.Generator با سید مشخص را به‌ازای هر دور به آن پاس بدهیم، در هر بار اجرای مجدد، چیدمان دقیقاً یکسانی به دست می‌آوریم. این ویژگی زمانی حیاتی می‌شود که بخواهید دو اجرای مختلف را مقایسه کنید که فقط در یک هایپرپارامتر تفاوت دارند. بدون تعیین سید، دو اجرا داده‌ها را با ترتیب متفاوتی می‌بینند و نمودارهای تابع زیان به دلایلی که هیچ ارتباطی با تغییر اعمال‌شده ندارد، از هم فاصله می‌گیرند.

قرارداد سید در این درس بسیار ساده است: epoch_seed = base_seed + epoch_index. سید پایه در زمان ساخت شیء داده می‌شود. ایندکس دور هم در آغاز هر دور توسط بخش آموزش یک واحد افزایش می‌یابد. بدین ترتیب، اجرای مجدد با همان سید پایه، در تک‌تک دورها همیشه ترتیبی یکسان خواهد داشت.

نمونه‌گیر بچ

نمونه‌گیرِ پیش‌فرض در PyTorch، ایندکس‌ها را به‌صورت یکنواخت و تصادفی بدون جایگذاری انتخاب می‌کند. این دقیقاً همان چیزی است که برای پیش‌آموزش نیاز داریم. برای فاین‌تیون کردن روی یک دیتاست کوچک نیز همین روال برقرار است. DataLoader با فراخوانی B بارِ __getitem__ و روی‌هم‌چینی (stacking) نتایج، یک بچ می‌سازد. از آنجا که تک‌تک نمونه‌ها بنا بر ساختارشان طول یکسانی دارند، به هیچ منطقی برای پدینگ نیازی نیست.

این درس برای حفظ سادگی، مقدار num_workers=0 را نگه می‌دارد. در یک محیط عملیاتی، ورکرها فراخوانی‌های __getitem__ را موازی می‌کنند. در پایپ‌لاین ما، این کار عملاً تفاوت چندانی ایجاد نمی‌کند چون تمام کار فقط برش زدن یک تنسور درون حافظه است؛ با این حال، همین رابط Dataset به‌زیبایی از ورکرها پشتیبانی می‌کند.

شمارش نمونه‌ها

برای جریان شناسه‌ای به طول N، طول زمینه‌ی T و استراید S، تعداد کل نمونه‌ها برابر است با max(0, 1 + (N - (T + 1)) // S). این درس این فرمول را در قالب یک متد استاتیک روی Dataset قرار می‌دهد تا بخش آموزش بتواند بدون نیاز به پیمایش کل داده‌ها، تعداد کل گام‌ها در هر دور را حساب کند.

مواردی که در این درس بررسی نمی‌شوند

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

همچنین مدیریت چندین سند مختلف بررسی نمی‌شود. پیکره‌ی متنی مانند یک جریان ممتد و واحد از شناسه‌ها در نظر گرفته می‌شود. مرز رسیدن به سند بعدی، هنگام ساخت پیکره از چند سند، با درج شناسه‌های <|endoftext|> مشخص می‌شود و مدل یاد می‌گیرد که پیش‌بینی‌ها را با در نظر گرفتن این مرز انجام دهد.

راهنمای خواندن کد

فایل main.py شامل دو کلاس و یک تابع کمکی است. کلاس SlidingWindowDataset همان دیتاست PyTorch است. تابع make_dataloader یک دیتا لودر تنظیم‌شده به همراه مولد تصادفیِ سیددار تحویل می‌دهد. تابع _encode_corpus_to_ids نیز فراخوانی یک‌باره‌ی توکنایزر را انجام می‌دهد. بخش دمو در انتهای فایل، یک توکنایزر کوچک را در لحظه می‌سازد، پیکره‌ی متنی داخلی را کدگذاری می‌کند، دیتاست و دیتا لودر را تشکیل می‌دهد، یک بچ را چاپ می‌کند و قرارداد ابعاد را با اعتبارسنجی (assert) می‌سنجد. تست‌های موجود در code/tests/test_dataset.py فرمول شمارش پنجره‌ها، ویژگی شیفت به چپ، بُر زدن قطعی داده‌ها و موازنه‌ی استراید را ارزیابی می‌کنند.

دمو را اجرا کنید. سپس طول زمینه را از 16 به 32 تغییر دهید و افت تعداد نمونه‌ها در هر دور را ببینید. این عدد دقیقاً همان بودجه‌ی گام‌های شما در هر دور آموزش (steps-per-epoch budget) است.

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسقرارداد ابعاد (shape contract) برای یک بچ آموزش مدل زبانی علّی (Causal LM) چیست؟
  2. حین درسچه تعداد پنجره به طول T+1 در یک جریان شناسه به طول N با استراید S جا می‌گیرد؟
  3. حین درسنصف کردن مقدار استراید چه تأثیری روی مجموعه‌داده می‌گذارد؟
  4. حین درسچرا یک torch.Generator صریح به DataLoader پاس داده می‌شود؟
  5. پس از درسچرا مجموعه‌داده از هر پنجره به اندازه‌ی T+1 خروجی (input[:-1], input[1:]) برمی‌گرداند؟
  6. پس از درسچرا در این درس پنجره‌های ناقص انتهایی به جای پدینگ دور ریخته می‌شوند؟

سؤالات متداول

چرا در پیش‌آموزش مدل‌های علّی هر نمونه باید T+1 شناسه داشته باشد؟

چون در مدل زبانی علّی ورودی و برچسب ابعاد (B, T) دارند و برچسب همان ورودی با یک گام شیفت به چپ است؛ بنابراین هر نمونه باید T+1 شناسه داشته باشد تا ورودی از صفر تا T و برچسب از یک تا T+1 برش بخورد.

تفاوت استراید برابر با T با استراید کوچکتر چیست؟

استراید برابر با T پنجره‌هایی بدون همپوشانی ایجاد می‌کند، در حالی که استرایدهای کمتر (مانند T // 2 یا 1) با ایجاد همپوشانی میان پنجره‌ها، تنوع مرزهای پیش‌بینی و حجم مؤثر دیتاست را افزایش می‌دهند.

چگونه ترتیب بُر خوردن داده‌ها در DataLoader را کاملاً قطعی و تکرارپذیر کنیم؟

با ساخت یک شیء torch.Generator اختصاصی و مقداردهی سید آن در هر دور بر اساس فرمول epoch_seed = base_seed + epoch_index، ترتیب داده‌ها در تمام اجراها کاملاً یکسان و تکرارپذیر باقی می‌ماند.

ما رو توی گوگل بیشتر ببین

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