فاز 19 · درس 59

پیاده‌سازی انکودر ترنسفورمر بینایی (ViT): از توکن‌های تصویر تا تجمیع با CLS

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
طرح مفهومی از یک آرایه شبکه‌ای شیشه‌ای لایه‌بندی شده که نور از دوازده سطح متوالی عبور کرده و روی یک منشور مرکزی متمرکز می‌شود
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. مقایسه‌ی Pre-LN و Post-LN
  5. خوداتنشن چندسر
  6. دلیل انبساط 4 برابری لایه‌ی پیش‌خور
  7. استفاده از ماسک علّی یا بدون ماسک؟
  8. توکن CLS چه چیزی یاد می‌گیرد؟
  9. پیاده‌سازی از صفر
  10. استفاده از ابزارهای واقعی
  11. تست‌ها
  12. تمرین‌ها
  13. اصطلاحات کلیدی
  14. برای مطالعه‌ی بیشتر

در سیستم‌های بینایی ماشین نوین، صرفاً خرد کردن تصویر به تکه‌ها و تبدیل آن‌ها به بردار کمکی به درک محتوا نمی‌کند؛ این بردارها در ابتدا هیچ دیدی نسبت به ارتباط فضایی و معنایی با یکدیگر ندارند. برای اینکه یک مدل چندحالته تفاوت بین پس‌زمینه و سوژه‌ی اصلی را تشخیص دهد، به سازوکاری نیاز است که اطلاعات تکه‌ها را با یکدیگر ترکیب کرده و رابطه‌ی بین آن‌ها را کشف کند.

انکودر ترنسفورمر بینایی (Vision Transformer Encoder) دقیقاً همان بستری است که قلب تپنده‌ی مدل‌های پیشرفته‌ای چون CLIP، SigLIP و DINOv2 را شکل می‌دهد. در این درس یاد می‌گیرید که چگونه با در کنار هم قرار دادن لایه‌های خوداتنشن چندسر، شبکه‌ی پیش‌خور با انبساط 4 برابری و نرمال‌سازی Pre-LN، یک انکودر ViT-Base کامل را از صفر تا صد پیاده‌سازی و ارزیابی کنید.

کدها و محاسبات این معماری در مقیاس پایه به‌گونه‌ای طراحی شده‌اند که بدون نیاز به GPUهای گران‌قیمت یا سرویس‌های ابری خارجی، مستقیماً روی پردازنده‌ی سیستم شخصی یا تراشه‌های Apple Silicon قابل اجرا و آزمایش باشند.

تکه‌ها به‌تنهایی چیزی را «نمی‌بینند». یک ترنسفورمر (Transformer) دوازده‌لایه با معماری Pre-LN و 12 سرِ اتنشن، توالیِ توکن‌های وصله‌ای را به توالیِ توکن‌های زمینه‌مند (Contextual) تبدیل می‌کند؛ توکن CLS نیز در آخرین وضعیت پنهان خود، ویژگی‌های کل تصویر را یک‌جا جمع (Pool) می‌کند. این درس حکم موتورخانه‌ی تمام مدل‌های بینایی-زبانی مدرن را دارد.

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

  • پیاده‌سازی یک بلوک ترنسفورمر با معماری Pre-LN، خوداتنشن چندسر (Multi-Head Self-Attention) و زیرلایه‌ی پیش‌خور (Feed-Forward).
  • چیدن 12 بلوک با 12 سر روی هم برای ساخت انکودر ViT-Base.
  • اتصال فرانت‌اند تکه‌ها از درس 58 به این انکودر و اجرای یک گذر رو به جلو (Forward pass).
  • راستی‌آزمایی اینکه توکن CLS اطلاعات را از تک‌تک تکه‌ها جمع‌آوری می‌کند.

صورت مسئله

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

دستورالعمل استاندارد شامل عمق 12 بلوک، عرض 12 سر اتنشن، چینش Pre-LayerNorm، تابع فعال‌سازی GELU و ضریب انبساط 4 برابری در لایه‌ی پیش‌خور است. این فرمول، ستون فقرات مدل‌های CLIP ViT-L، SigLIP، DINOv2، خانواده‌ی Qwen-VL، InternVL و تمامی انکودرهای تصویر با وزن‌های باز (Open-weight) در سال‌های 2025 تا 2026 محسوب می‌شود. این ساختار آن‌قدر تثبیت‌شده است که در مطالعه‌ی هر کدام از این مقاله‌ها می‌توانید چنین معماری بلوکی را پیش‌فرض بگیرید، مگر اینکه خلاف آن صراحتاً ذکر شده باشد.

مفاهیم پایه

دنباله توکن B x 197 x 768بلوک 1بلوک 2...بلوک 12LayerNorm نهاییتوکن‌های زمینه‌ای B x 197 x 768
توکن xLayerNormخوداتنشن چندسر+ باقی‌ماندهLayerNormFFN: خطی - GELU - خطی+ باقی‌ماندهتوکن 'x

مقایسه‌ی Pre-LN و Post-LN

ترنسفورمر اولیه، لایه‌ی LayerNorm را بعد از اتصال باقیمانده (Residual) قرار می‌داد. معماری Pre-LN (قرار گرفتن LayerNorm پیش از هر زیرلایه) نسخه‌ای است که همه‌ی مدل‌های مدرن بینایی-زبانی به کار می‌برند؛ چون بدون نیاز به ترفندهای گرم‌کردن نرخ یادگیری (LR warm-up)، آموزش پایداری ارائه می‌دهد. تفاوت در کد گذر رو به جلو تنها در حد یک خط است، اما رفتار جریان گرادیان در عمق‌های بالاتر از 12 اصلاً قابل مقایسه نیست.

خوداتنشن چندسر

هر سر اتنشن، بردار توکن را به سه‌گانه‌ی (query, key, value) اختصاصی خود با بعد head_dim = hidden / num_heads تصویر می‌کند. با داشتن hidden = 768 و heads = 12، بعد هر سر برابر با dim = 64 می‌شود. این 12 سر به شکل موازی اتنشن را اعمال می‌کنند، سپس خروجی‌های آن‌ها دوباره به بعد 768 به هم متصل (Concat) شده و از یک لایه‌ی تصویر کردنِ خروجی عبور می‌کنند. مزیت چندسر بودن در این است که یک سر می‌تواند «اتنشن روی چشم گربه» را یاد بگیرد، در حالی که سر دیگر بدون هیچ تداخلی، «اتنشن روی گرادیان رنگ پس‌زمینه» را یاد می‌گیرد.

دلیل انبساط 4 برابری لایه‌ی پیش‌خور

شبکه‌ی پیش‌خور (FFN) مسیر hidden -> 4 * hidden -> hidden را با فعال‌ساز GELU در میانه‌ی آن طی می‌کند. ضریب 4 یک مقدار تجربی است که از سال 2017 در تمام ترنسفورمرهای متن و تصویر ثابت مانده است. مقادیر کوچک‌تر (مثل 2 برابر) دچار کم‌برازش (Underfitting) می‌شوند و مقادیر بزرگ‌تر (مثل 8 برابر) با بودجه‌ی داده‌ی ثابت، دچار بیش‌برازش (Overfitting) خواهند شد. شبکه‌ی پرسپترون چندلایه (MLP) جایی است که مدل بیشترین بخش از فکت‌های آموخته‌شده‌ی خود را ذخیره می‌کند و بخش عریض‌تر میانی، محل قرارگیری همین اطلاعات است.

جزء سازنده تعداد پارامتر در مقیاس ViT-Base
تصویر کردن qkv در هر بلوک 3 * 768 * 768 = 1.77M
تصویر کردن خروجی در هر بلوک 768 * 768 = 590K
لایه‌ی FFN در هر بلوک (انبساط 4 برابری) 2 * 768 * 4 * 768 = 4.72M
لایه‌ی LayerNorm در هر بلوک 4 * 768 = 3K
مجموع در هر بلوک حدود 7.1M
12 بلوک حدود 85M
به اضافه‌ی فرانت‌اند در مجموع حدود 86M

مدل ViT-Base یک انکودر 86 میلیون پارامتری است. این ابعاد با استانداردهای سال 2026 کوچک شمرده می‌شود (مدل SigLIP-So400M دارای 400 میلیون و ViT مدل Qwen-VL دارای 675 میلیون پارامتر است)، اما معماری آن‌ها به جز در پهنا و عمق، کاملاً یکسان است.

استفاده از ماسک علّی یا بدون ماسک؟

ترنسفورمرهای بینایی فقط-انکودر (Encoder-only) و دوطرفه (Bidirectional) هستند: توکن i می‌تواند به توکن j برای هر جفتی توجه کند؛ بنابراین نیازی به ماسک نیست. خوداتنشن متن در دیکودر درس 61 از ماسک علّی استفاده خواهد کرد (البته کراس‌اتنشن آن روی تکه‌های تصویر بدون ماسک است)، ولی درون انکودر تصویر، اتنشن به شکل کاملاً متصل عمل می‌کند.

توکن CLS چه چیزی یاد می‌گیرد؟

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

انکودر ViTاتنشن آگاهی ایجاد می‌کند
ViT ENCODERPatch tokens enter with no awareness of each other. Twelve pre-LN blocks of multi-head self-attention let every patch exchange information with every other, and the CLS token accumulates whole-image features layer by layer; its final hidden state is the pooled summary the rest of the stack reads. This block recipe is the spine of CLIP, SigLIP, and DINOv2.بلوک 1...بلوک 12CLSCLS کل تصویر را تجمیع می‌کند12 بلوک pre-LN با 12 سر، GELU و 4 برابر FFN
توکن‌های وصله‌ای بدون هیچ شناختی از یکدیگر وارد می‌شوند. دوازده بلوک pre-LN از خوداتنشن چندسر به هر تکه اجازه می‌دهند با سایر تکه‌ها تبادل اطلاعات کند، و توکن CLS لایه به لایه ویژگی‌های کل تصویر را تجمیع می‌نماید؛ وضعیت پنهان نهایی آن خلاصه یکپارچه‌ای است که بقیه پشته می‌خوانند. این ساختار بلوک ستون فقرات CLIP، SigLIP و DINOv2 است.

پیاده‌سازی از صفر

فایل code/main.py این بخش‌ها را پیاده‌سازی می‌کند:

  • کلاس MultiHeadSelfAttention همراه با لایه‌های تصویر کردن qkv و خروجی، محاسبات ریاضی اتنشن ضرب نقطه‌ای مقیاس‌شده (Scaled dot-product attention) و بررسی‌های انطباق ابعاد (Shape assertions).
  • کلاس FeedForward، همان پرسپترون چندلایه‌ی GELU با انبساط 4 برابری.
  • کلاس Block، یک بلوک Pre-LN که زیرلایه‌های اتنشن و پیش‌خور را به همراه اتصالات باقیمانده ترکیب می‌کند.
  • کلاس ViT، دسته‌ای از 12 بلوک روی هم به همراه لایه‌ی نهایی LayerNorm.
  • کلاس VisionEncoder، که VisionFrontEnd از درس 58 را به بلوک‌های ViT متصل می‌کند و متد forward() را با ارائه‌ی توالی زمینه‌مند و بردار تجمیع‌شده‌ی CLS در اختیار می‌گذارد.
  • یک نمونه‌ی نمایشی که تصویر ساختگی 224x224 را وارد کل انکودر می‌کند و ابعاد ورودی، ابعاد خروجی، تعداد پارامترها و نُرم (Norm) توکن CLS را یک‌لایه-در-میان چاپ می‌کند.

برای اجرا:

python3 code/main.py

خروجی: تصویر نمونه به یک تنسور با ابعاد (1, 197, 768) رمزگذاری می‌شود. نُرم توکن CLS با پیشروی در لایه‌ها افزایش می‌یابد و سپس در LayerNorm نهایی تثبیت می‌شود. مجموع پارامترها نیز حدود 86 میلیون گزارش می‌شود.

استفاده از ابزارهای واقعی

انکودری که اینجا ساختید، به جز تغییرات جزئی در پهنا و عمق، دقیقاً همان مجموعه‌بلوک‌هایی است که در دل تمام مدل‌های بینایی-زبانی متن‌باز سال‌های 2025 تا 2026 قرار دارد. تفاوت‌ها فقط در این بخش‌هاست:

  • پهنا و عمق. در مدل ViT-Large مقادیر hidden=1024, depth=24, heads=16 است؛ در مدل SigLIP So400M این مقادیر hidden=1152, depth=27, heads=16 هستند. بلوک اصلی همان است.
  • سر تجمیع‌کننده (Pooling head). تجمیع با CLS (این درس) در برابر تجمیع میانگین (Average pooling در SigLIP) یا تجمیع بر پایه‌ی اتنشن (در مدل‌های بینایی-زبانی جدیدتر).
  • مدیریت موقعیت (Position handling). توابع سینوسی ثابت (درس 58) در برابر موقعیت یک‌بعدی یادگرفتنی، ALiBi یا RoPE دوبعدی. محاسبات پایه‌ی بلوک تغییری نمی‌کند.
  • توکن‌های رجیستر. مدل DINOv2 تعداد 4 توکن رجیستر یادگرفتنی به ابتدای دنباله اضافه می‌کند که پیاده‌سازی آن فقط به یک خط کد نیاز دارد.

این معماریِ بلوکی، پایه‌ی اصلی کار است. درس‌های بعدی (60 تا 63) روی همین فونداسیون بنا می‌شوند.

تست‌ها

فایل code/test_main.py موارد زیر را پوشش می‌دهد:

  • یک تک‌بلوک، ابعاد را حفظ می‌کند و نسبت به اندازه‌ی دسته‌ی ورودی پایدار است.
  • مجموع امتیازهای اتنشن در امتداد محور کلیدها برابر با یک است (راستی‌آزمایی سافت‌مکس).
  • مسیرهای باقیمانده به درستی متصل هستند (ورودیِ صفر همچنان از طریق توکن CLS خروجی غیرصفر تولید می‌کند).
  • گذر رو به جلو در 4 لایه‌ی چیده‌شده روی هم، ابعاد درستی می‌دهد.
  • گرادیان‌ها از خروجی CLS به لایه‌ی تصویر کردنِ تکه‌ها جریان پیدا می‌کنند.

برای اجرای تست‌ها:

python3 -m unittest code/test_main.py

تمرین‌ها

  1. توکن‌های رجیستر (4 بردار یادگرفتنی که بعد از CLS درج می‌شوند) را اضافه کرده و کد را دوباره اجرا کنید. میزان یکدستی نقشه‌ی اتنشن را از طریق آنتروپی توزیع سافت‌مکس در آخرین لایه مقایسه کنید.

  2. جای Pre-LN و Post-LN را عوض کنید و یک مدل را برای یک دور (Epoch) روی یک دسته‌بند اشکال ساختگی آموزش دهید. بررسی کنید کدام‌یک بدون گرم‌کردن نرخ یادگیری (LR warm-up)، آموزش پایدارتری دارد.

  3. ماسک علّی را در قالب ورودی attn_mask پیاده‌سازی کنید تا بتوان از همین بلوک به عنوان بلوک دیکودر هم استفاده کرد. ابعاد ماسک باید مثلثیِ پایینی و به شکل (seq, seq) باشد.

  4. یک گذر رو به جلو را با استفاده از torch.profiler در اندازه‌های دسته‌ی 1، 8 و 64 پروفایل کنید. خواهید دید که لایه‌ی MLP بیشترین زمان اجرا را به خود اختصاص می‌دهد، نه لایه‌ی اتنشن.

  5. لایه‌های تصویر کردن q-k-v را در یکی از سرهای اتنشن با یک تطبیق‌دهنده LoRA کم‌رتبه جایگزین کنید؛ بقیه‌ی بخش‌ها را فریز (Freeze) کرده و مطمئن شوید گرادیان فقط در مسیرهای مورد نظر جریان می‌یابد.

اصطلاحات کلیدی

اصطلاح تعریف
Pre-LN اعمال LayerNorm قبل از هر زیرلایه، به جای اعمال آن بعد از زیرلایه
خوداتنشن (Self-attention) توجه هر توکن به تمام توکن‌های دیگر در همان توالی
چندسر (Multi-head) تقسیم بعد وضعیت پنهان بین H سرِ اتنشنِ مستقل از هم
انبساط FFN عریض‌تر شدن لایه‌ی پیش‌خور تا 4 * hidden قبل از جمع‌شدن دوباره
تجمیع با CLS (CLS pooling) استفاده از وضعیت پنهان نهاییِ نخستین توکن به عنوان خلاصه‌ی کل تصویر

برای مطالعه‌ی بیشتر

  • مقاله‌ی An Image is Worth 16x16 Words (معرفی ViT، سال 2021) برای ساختار پایه‌ی انکودر.
  • مقاله‌ی DINOv2 (سال 2023) برای آشنایی با توکن‌های رجیستر و تابع زیان پیش‌آموزش خودنظارتی (Self-supervised).
  • مقاله‌ی SigLIP (سال 2023) برای مشاهده‌ی روش تجمیع میانگین و تابع زیان کنتراستیو بر پایه‌ی سیگموئید که در درس 62 استفاده می‌شود.

خودآزمایی

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

  1. پیش از درسچرا بلوک استاندارد ViT به جای post-LayerNorm از pre-LayerNorm استفاده می‌کند؟
  2. پیش از درسبا hidden=768 و heads=12، بعد هر سر چقدر است و چرا اهمیت دارد؟
  3. حین درسآیا انکودر ViT به ماسک علّی اتنشن نیاز دارد؟
  4. حین درسوضعیت پنهان نهایی توکن CLS پس از طی 12 بلوک نشان‌دهنده چیست؟
  5. حین درسچرا لایه پیش‌خور (FFN) به اندازه 4 برابر بعد پنهان گسترش می‌یابد؟
  6. پس از درسکدام افزونه، محاسبات بلوک را دست‌نخورده نگه می‌دارد اما توکن‌های یادگرفتنی را به ابتدا اضافه می‌کند؟

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

چرا در انکودرهای مدرن ViT به جای Post-LN از معماری Pre-LN استفاده می‌شود؟

در معماری Pre-LN، لایه‌ی LayerNorm پیش از اعمال هر زیرلایه قرار می‌گیرد؛ این تغییر باعث بهبود جریان گرادیان در لایه‌های عمیق شده و امکان آموزش پایدار شبکه را بدون نیاز به ترفندهای پیچیده‌ی گرم‌کردن نرخ یادگیری (LR warm-up) فراهم می‌کند.

نقش توکن CLS در انکودر Vision Transformer چیست؟

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

چرا در لایه‌های اتنشن انکودر ViT از ماسک علّی (Causal Mask) استفاده نمی‌شود؟

انکودر بینایی بر خلاف دیکودرهای زبانی ساختاری کاملاً دوطرفه دارد؛ یعنی هر تکه از تصویر باید بتواند آزادانه به تمامی تکه‌های دیگر در سراسر تصویر توجه کند تا ارتباطات فضایی به درستی شکل بگیرند.

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

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