یکی از بخشهای کلیدی در پیشآموزش مدلهای زبانی، طراحی پایپلاین دادهای است که بتواند متن طولانی را به دستههای آموزشیِ استاندارد تبدیل کند. اگر این پایپلاین بدون دقت به ساختار همپوشانی و شیفت به چپ ساخته شود، مدل در یادگیری وظیفهی پیشبینی توکن بعدی دچار افت کارایی یا خطای محاسباتی میشود.
در این درس یاد میگیرید که چگونه یک جریان طولانی از شناسههای توکن را با روش پنجرهی لغزان (sliding window) به نمونههای آموزشی با طول زمینه مشخص برش بزنید، کلاس سفارشی Dataset را در PyTorch بسازید و با استفاده از بُر زدن قطعی (deterministic shuffle)، مقایسهپذیری آزمایشهای مختلف را تضمین کنید.
از آنجا که دادهها در حافظهی رم پردازش میشوند و ساختار کد سبک است، پیادهسازی این درس به GPU ابری نیاز ندارد و بهسادگی روی پردازندهی استاندارد سیستم شخصی قابل اجرا و آزمایش است.
فرایند پیشآموزش (pretraining)، در واقع تابعی است که شناسههای توکن را میگیرد و گرادیانها را تحویل میدهد. در این درس، همان نوار نقالهای را میسازیم که این شناسهها را به مدل میرساند.
اهداف یادگیری
- تبدیل یک پیکرهی متنی خام (corpus) به جریانی از شناسههای توکن با یک بار فراخوانی توکنایزر.
- برش زدن جریان شناسهها به پنجرههایی با طول ثابت و با استراید (گام لغزش) دارای قابلیت همپوشانی و پیکربندی.
- ساخت یک Dataset در PyTorch که تنسورهای ورودی و هدف را برای پیشبینی توکن بعدی تولید کند.
- قرار دادن دیتاست درون یک DataLoader به همراه بُر زدن قطعی (deterministic shuffle) با سید مشخص در هر دور (epoch).
- تحلیل و موازنهی بین استراید، تکرار دادهها و اندازهی مؤثر دیتاست.
صورت مسئله
فرایند پیشآموزش در هر گام یک بچ (batch) از شناسههای توکن را میخواند و مدل را بهروزرسانی میکند. ابعاد هر بچ را قرارداد آموزش تعیین میکند. برای یک مدل زبانی علی (causal language model)، هر بچ شامل شناسههای ورودی با ابعاد (B, T) و شناسههای هدف با همان ابعاد (B, T) است؛ جایی که هدف، در واقع همان ورودی است که یک واحد به چپ شیفت پیدا کرده است. وظیفهی پایپلاین داده این است که چنین قراردادی را بر اساس تقاضا، بهشکلی قطعی و تکرارپذیر، از دل پیکرهای که شاید چندین گیگابایت متن خام باشد تولید کند.
در این درس این پایپلاین را میسازیم. توکنایزرِ درسِ قبل، متن را به یک لیست مسطح و طولانی از شناسهها تبدیل میکند. یک پنجرهی لغزان آن لیست را به نمونههای آموزشی برش میزند. یک Dataset سفارشی، این نمونهها را در قالب تنسورها ارائه میدهد. در نهایت، یک DataLoader آنها را دستهبندی (batch) کرده و با یک سید مشخص بُر میزند.
قرارداد ابعاد داده
یک مدل زبانی علی، شناسههایی با ابعاد (B, T) مصرف میکند که در آن B اندازهی بچ و T طول زمینه (context length) است. هدف در جایگاه t، همان ورودی در جایگاه t+1 است. این یعنی هر نمونهی آموزشی باید T+1 شناسهی خام را پوشش دهد. استرایدِ پنجره مشخص میکند که بین نمونههای متوالی چقدر همپوشانی وجود داشته باشد.
بخش برشدهنده هرگز با انتهای پیکرهی متنی همپوشانی برقرار نمیکند. اگر پنجرهی آخر شناسهی کافی برای پر کردن T+1 جایگاه نداشته باشد، برشدهنده آن را کنار میگذارد. پدینگ کردنِ بخش انتهایی با <|pad|> هم راهحل درستی است، اما ماسک تابع زیان (loss) را پیچیده میکند. در این درس، نمونههای ناقص انتهایی را نادیده میگیریم.
چرا پنجرهی لغزان؟
یک پیکرهی پیشآموزش، جریانی ممتد و طولانی از شناسهها است. اگر مدل فقط پنجرههای بدون همپوشانی را میدید، تکتک نمونههای آموزشی مرزهای Tگانهی یکسانی را به آن یاد میدادند. تغییر استراید این مرزها را جابهجا میکند تا مدل وظایف پیشبینی توکن بعدی متنوعتری را تجربه کند.
استرایدی برابر با T پنجرههایی بدون همپوشانی میسازد. استرایدی برابر با T // 2 همپوشانی پنجاه درصدی ایجاد کرده و اندازهی مؤثر دیتاست را دو برابر میکند. استرایدی معادل 1 بیشترین همپوشانی را تولید کرده و حجم دیتاست را به ضریب T میرساند. هزینهی این کار، بار پردازشی بیشتر در هر دور آموزش است؛ اما مزیتش تنوع بیشتر در مرزبندیهاست. اکثر فرایندهای پیشآموزش استرایدی برابر با طول زمینه انتخاب میکنند، چون پیکرهی متنی از ابتدا بسیار بزرگتر از چیزی است که مدل بتواند در یک دور تمامش کند؛ بنابراین استدلال تنوع مرزبندیها در آنجا کماهمیتتر میشود.
کلاس Dataset
یک Dataset در PyTorch به دو متد الزامی نیاز دارد. متد __len__ تعداد نمونهها را برمیگرداند و متد __getitem__ یک نمونه را بهشکل یک جفت تنسور تحویل میدهد. دیتاست ما، جریان شناسههای کدگذاریشده و مقدار استراید را نگهداری میکند. با اعمال ایندکس روی آن، نقطهی شروع پنجره در لحظه محاسبه میشود؛ در نتیجه، فارغ از اینکه استراید چه تعداد نمونه بسازد، هزینهی حافظه فقط به اندازهی نگهداری یک نسخه از جریان شناسهها خواهد بود.
عملیات یک واحد شیفت به چپ درون __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) است.
