فاز 19 · درس 61

پیاده‌سازی کراس‌اتنشن و بلوک دیکودر تصویر-زبان

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

در مدل‌های چندوجهی تصویر و زبان، یکی از مهم‌ترین چالش‌ها اتصال معنایی کلمات به نواحی متناظر در تصویر است. اتصال ساده‌ی بردارهای ویژگی معمولاً برای وظایف دقیقی مثل تولید کپشن پاسخ‌گو نیست؛ چرا که هر کلمه در حین تولید باید بتواند به‌صورت پویا به بخش‌های متفاوتی از تصویر نگاه کند. این دقیقاً همان نقشی است که مکانیزم کراس‌اتنشن (Cross-Attention) بر عهده دارد.

در این درس معماری ادغام دیرهنگام (Late fusion) را بررسی می‌کنیم؛ راهکاری که مدل‌های شاخصی مثل Flamingo و BLIP-2 استفاده می‌کنند تا ضمن حفظ توانایی پردازش متن خالص، تصویر را یک بار پردازش کرده و در تمام مراحل تولید کلمات با کمترین هزینه‌ی محاسباتی فراخوانی کنند. تنظیم دقیق ابعاد ماتریس‌ها و نحوه‌ی اعمال ماسک علّی (Causal mask) روی متن بدون تداخل با ویژگی‌های تصویر، از مباحث عملی این پیاده‌سازی است.

لایه‌ی تصویر کردن (Projection layer)، یک بردار تصویر را با یک بردار کپشن هم‌تراز می‌کند؛ اما یک دیکودر تصویر-زبان واقعی به این نیاز دارد که تک‌تک توکن‌های متن به تمام توکن‌های تکه (Patch) تصویر اتنشن داشته باشند تا مدل بتواند هر کلمه را به ناحیه‌ای مشخص ارجاع دهد (Grounding). این ارجاع‌دهی دقیقاً با کراس‌اتنشن (Cross-Attention) اتفاق می‌افتد: متن کوئری می‌زند و کلیدها و مقادیر تصویر پاسخ می‌دهند. در این درس، بلوک کراس‌اتنشن، خوداتنشن علّی متن و ابعاد ماسک‌هایی را که برای کارکرد درست هر دو لازم است، می‌سازیم.

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

  • پیاده‌سازی کراس‌اتنشن چندسر به‌طوری که جریان کوئری، متن و جریان کلید/مقدار، تصویر باشد.
  • ساخت یک بلوک دیکودر: خوداتنشن علّی + کراس‌اتنشن + شبکه‌ی پیش‌خور (Feed-forward).
  • تنظیم درست ابعاد ماسک‌ها: ماسک علّی (Causal mask) برای خوداتنشن و بدون ماسک برای کراس‌اتنشن.
  • اجرای یک گذر رو به جلو (Forward pass) با دسته‌ای از توکن‌های متنی و مجموعه‌ای ثابت از توکن‌های تصویر.

صورت مسئله

پیوستن توکن‌های تصویر و توکن‌های متن در قالب یک توالی واحد، یکی از گزینه‌های ادغام است (ادغام زودهنگام یا Early fusion؛ همان مسیری که Chameleon و Emu3 رفته‌اند). کراس‌اتنشن گزینه‌ی دیگر است (ادغام دیرهنگام یا Late fusion؛ راهکاری که Flamingo معرفی کرد و از آن زمان تمام دیکودرهای با معماری مشابه Flamingo از آن الگو گرفته‌اند). در ادغام دیرهنگام، دیکودر متن فقط روی توکن‌های متنی اجرا می‌شود و در هر لایه از طریق کراس‌اتنشن به سراغ جریان تصویر می‌رود.

ادغام دیرهنگام دو مزیت دارد: نخست، جریان متن دست‌نخورده باقی می‌ماند و مدل قابلیت‌های پردازش متنی صرف خود را حفظ می‌کند. دوم، جریان تصویر برای هر تصویر تنها یک بار محاسبه شده و برای تمام مراحل رمزگشایی (Decode) دوباره استفاده می‌شود؛ بنابراین تولید متن حتی برای کپشن‌های طولانی هم ارزان است. بهای این مزایا، یک زیرلایه‌ی اتنشن اضافه در هر بلوک است.

مفاهیم پایه

توکن‌های تصویر B x Nv x Dانکودر بینایی منجمدتوکن‌های حافظه B x Nv x Dشناسه‌های توکن متنامبدینگ متنخوداتنشن ماسک‌شدهکراس‌اتنشن پرسش‌ها=متنکلیدها/ارزش‌ها=حافظهپیش‌خورلاجیت‌های توکن بعدی
متن Q B x H x Nt x dQ K^T / sqrt dتصویر K B x H x Nv x dسافت‌مکس روی Nvتصویر V B x H x Nv x dخروجی B x H x Nt x d

ابعاد ماسک‌ها

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

اتنشن طول کوئری طول کلید ماسک دلیل
خوداتنشن Nt (متن) Nt (متن) علّی: مثلثی پایینی (Nt, Nt) توکن‌های متن هنگام تولید خودبازگشتی نباید به جلو نگاه کنند
کراس‌اتنشن Nt (متن) Nv (تصویر) بدون ماسک تمام تصویر برای هر موقعیت متنی قابل مشاهده است

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

چرا کراس‌اتنشن به ماسک نیاز ندارد؟

پیش از تولید هر کلمه‌ای از متن، تصویر به‌طور کامل مشاهده می‌شود. توکن t از کپشن می‌تواند به هر تکه‌ای از تصویر اتنشن داشته باشد؛ چرا که ترتیبی زمانی بین تکه‌های تصویر وجود ندارد. برخی نسخه‌های Flamingo هنگام ترکیب متناوب چند تصویر و بخش‌های متنی، الگوی ماسک‌گذاری اختصاصی برای هر نمونه اضافه می‌کنند؛ اما برای حالت تک‌تصویر به همراه یک کپشن، کراس‌اتنشن همه‌چیز را کامل می‌بیند.

کش کلید/مقدار

کلیدها و مقادیر تصویر در شروع مرحله‌ی رمزگشایی یک بار محاسبه و در یک کش نگه‌داری می‌شوند. هر توکن جدید متن، بدون محاسبه‌ی مجدد، از همان داده‌های کش استفاده می‌کند. همین ویژگی سرعت بالای تولید کپشن در زمان استنتاج را تضمین می‌کند: مدل سنگین ViT یک بار اجرا می‌شود و کراس‌اتنشن در هر گام کلیدها و مقادیر آن را دوباره به کار می‌گیرد. این درس ساختار کش را پیاده کرده و مسیر مبتنی بر استفاده از کش (Cache-hit) را آزمایش می‌کند.

ساختار بلوک دیکودر

ترتیب اجرای یک بلوک دیکودر به این صورت است: پیش‌نرمال‌سازی (Pre-LN) -> خوداتنشن -> اتصال باقی‌مانده (Residual) -> پیش‌نرمال‌سازی -> کراس‌اتنشن -> اتصال باقی‌مانده -> پیش‌نرمال‌سازی -> شبکه‌ی پیش‌خور -> اتصال باقی‌مانده؛ یعنی سه زیرلایه که هر کدام لایه‌ی LayerNorm خود را دارند. مقاله‌ی Flamingo یک گیت تانژانت هذلولوی (tanh) یادگیرنده با مقدار اولیه‌ی صفر به کراس‌اتنشن اضافه کرد تا مدل زبانی منجمد کارش را بدون تغییر آغاز کند و پایداری آموزش حفظ شود؛ ساختار استاندارد و پایه‌ای که اینجا استفاده می‌شود (Baseline)، فاقد گیت است.

class DecoderBlock:
  def forward(self, text_tokens, image_tokens, text_mask, cross_mask):
      text_tokens = text_tokens + self.self_attn(self.ln1(text_tokens),
                                                 mask=text_mask)
      text_tokens = text_tokens + self.cross_attn(self.ln2(text_tokens),
                                                  image_tokens,
                                                  mask=cross_mask)
      text_tokens = text_tokens + self.ffn(self.ln3(text_tokens))
      return text_tokens
کراس‌اتنشنمتن می‌پرسد، تصویر پاسخ می‌دهد
CROSS-ATTENTIONIn late fusion the decoder runs on text-only tokens and reaches into the image stream at every layer: the highlighted token sends a query to every memory token, unmasked, and the weighted answer flows back into its residual stream. Self-attention along the bottom row stays causal. The image side is encoded once and reused for every decode step, so long captions stay cheap.توکن‌های حافظه تصویر (یک‌بار برای هر تصویر)acatonthematخوداتنشن علّی: فقط چپ به راستکوئری‌ها: بدون ماسک
در ادغام دیرهنگام، رمزگشا روی توکن‌های متنی اجرا می‌شود و در هر لایه به جریان تصویر متصل می‌گردد: توکن مشخص‌شده بدون ماسک به تمام توکن‌های حافظه کوئری می‌فرستد و پاسخ وزن‌دار به جریان پسماند آن برمی‌گردد. خوداتنشن در سطر پایین علّی باقی می‌ماند. سمت تصویر یک بار کدگذاری شده و برای هر گام رمزگشایی دوباره استفاده می‌شود، بنابراین تولید کپشن‌های طولانی کم‌هزینه باقی می‌ماند.

پیاده‌سازی

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

  • CrossAttention(hidden, heads): کراس‌اتنشن چندسر با تصویر کردن‌های مجزا برای q و kv.
  • CausalSelfAttention(hidden, heads): خوداتنشن با ماسک علّی برگرفته از یک دیکودر استاندارد.
  • DecoderBlock: ترکیب سه زیرلایه با اتصالات باقی‌مانده و پیش‌نرمال‌سازی.
  • VisionLanguageDecoder: دیکودر چهارلایه‌ای که خروجی فرضی یک انکودر تصویر و جدول کوچک امبدینگ متن به آن داده می‌شود.
  • causal_mask(length): تابعی که یک تنسور بولی مثلثی پایینی با ابعاد (length, length) برمی‌گرداند.
  • یک نسخه‌ی نمایشی که دسته‌ای شامل دو توالی متنی به طول 10 را همراه با حافظه‌ی تصویری به طول 197 دریافت کرده و ابعاد خروجی، ابعاد ماسک خوداتنشن و نُرم خروجی کراس‌اتنشن را در هر موقعیت چاپ می‌کند.

اجرای آن:

python3 code/main.py

خروجی: دیکودر تنسور لاجیت‌هایی با ابعاد (2, 10, text_vocab) تولید می‌کند. ابعاد ماسک (10, 10) است. بررسی استفاده‌ی مجدد از کش KV نیز یکسان بودن لاجیت‌ها را میان مسیر کش‌شده و مسیر بدون کش تأیید می‌کند.

کاربرد در عمل

کراس‌اتنشن در دو خانواده‌ی مطرح در محیط عملیاتی (پروداکشن) حضور دارد:

  • Flamingo و IDEFICS: اضافه کردن یک زیرلایه‌ی کراس‌اتنشن در میان هر K بلوک از یک مدل زبانیِ منجمد. تطبیق‌دهنده‌ی تصویر-زبان (Adapter) در اینجا، همان بلوک کراس‌اتنشن به همراه گیت آن است.
  • BLIP-2: بخش Q-Former از کراس‌اتنشن برای ارتباط میان مجموعه‌ای ثابت از 32 توکن کوئری با ویژگی‌های تصویر استفاده می‌کند و سپس این کوئری‌ها را به فضای امبدینگ مدل زبانی تصویر می‌کند.

ساختار بلوک این درس مستقیماً روی هر دو معماری پیاده می‌شود و منطق اعمال ماسک (ماسک علّی برای خوداتنشن، بدون ماسک برای کراس‌اتنشن) در آن‌ها کاملاً یکسان است.

تست‌ها

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

  • ماسک علّی مثلثی پایینی است و با ابعاد بولی مورد انتظار همخوانی دارد.
  • خروجی کراس‌اتنشن صرف‌نظر از طول کلید، همیشه ابعاد (B, Nt, hidden) دارد.
  • خروجی مسیر مبتنی بر کش KV تا میزان تلورانس ممیز شناور با مسیر بدون کش منطبق است.
  • عدم تطابق ابعاد بین جریان‌های متن و تصویر، خطای واضح ValueError را صادر می‌کند.
  • یک گذر کامل رو به جلوی دیکودر، خروجی با ابعاد درست دسته‌ای و توالی تولید می‌کند.

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

python3 -m unittest code/test_main.py

تمرین‌ها

  1. یک گیت یادگیرنده‌ی tanh به اتصال باقی‌مانده‌ی کراس‌اتنشن اضافه کنید (ترفند Flamingo) و بررسی کنید که آموزش چگونه از گیت با مقدار اولیه‌ی نزدیک به صفر همگرا می‌شود. گیت از 0 شروع می‌شود؛ مدل پیش از ترکیب اطلاعات جریان تصویر، رفتار متنی خالص خود را بازیابی می‌کند.

  2. اتنشن متناوب را پیاده‌سازی کنید؛ حالتی که همان دیکودر، هم‌زمان چند تصویر به همراه چند قطعه‌ی متنی را دریافت می‌کند. ماسک کراس‌اتنشن اختصاصیِ نمونه را بسازید تا مانع از اتنشن داشتن قطعه‌ی متنی 2 به تصویر 1 شود.

  3. بازدهی و زمان اجرای لایه‌ی کراس‌اتنشن را در مقایسه با خوداتنشن در ابعاد Nt=64, Nv=576 (مشبک 24x24 با رزولوشن بالاتر) ارزیابی کنید. هزینه‌ی کراس‌اتنشن Nt * Nv است و در رزولوشن‌های بالای تصویر، بخش عمده‌ی محاسبات را به خود اختصاص می‌دهد.

  4. روی نگاشت اتنشن در سمت کوئری‌های کراس‌اتنشن دراپ‌اوت بگذارید و تنوع کپشن‌های تولیدی را در نسخه‌ی نمایشی اندازه بگیرید (با دراپ‌اوت در نگاشت کراس، واریانس نمونه‌های کپشن بالا می‌رود).

  5. لایه‌ی کراس‌اتنشن را با یک بلوک اتنشن مشابه Q-Former جایگزین کنید؛ حالتی که در آن مجموعه‌ای ثابت از 32 توکن کوئری در هر لایه یک بار به ویژگی‌های تصویر اتنشن می‌کنند.

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

اصطلاح مفهوم
ادغام دیرهنگام (Late fusion) جریان‌های متن و تصویر در مسیرهای جداگانه می‌مانند؛ کراس‌اتنشن در هر بلوک نقش رابط (Bridge) را بین آن‌ها ایفا می‌کند
کراس‌اتنشن (Cross-attention) حالتی که کوئری (Q) از یک جریان و کلیدها (K) و مقادیر (V) از جریانی دیگر می‌آیند
ماسک علّی (Causal mask) ماسک بولی مثلثی پایینی که مانع دیدن آینده در فرآیند خودبازگشتی می‌شود
کش کلید-مقدار (KV cache) ذخیره‌ی یک‌باره‌ی کلیدها و مقادیر تصویر و استفاده‌ی مجدد از آن‌ها در تمام گام‌های رمزگشایی
توکن‌های حافظه (Memory tokens) توکن‌های منجمد تصویر که دیکودر برای دریافت اطلاعات به سراغشان می‌رود

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

  • مقاله‌ی Flamingo (2022) برای دیدن طراحی استاندارد ادغام دیرهنگام با کراس‌اتنشن گیت‌دار.
  • مقاله‌ی BLIP-2 (2023) برای آشنایی با Q-Former؛ بلوک کراس‌اتنشنی که در قالب مجموعه‌ای از کوئری‌های یادگیرنده پیاده شده است.
  • مقاله‌ی IDEFICS (2023) برای مشاهده‌ی بازتولید متن‌باز روش Flamingo.

خودآزمایی

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

  1. پیش از درستفاوت عملیاتی بین ادغام زودهنگام (early fusion) و ادغام دیرهنگام (late fusion) در مدل‌سازی بینایی-زبان چیست؟
  2. پیش از درسدر یک بلوک رمزگشا که هر دو لایه خوداتنشن و کراس‌اتنشن را دارد، کدام‌یک از ماسک علّی استفاده می‌کند؟
  3. حین درسچرا کش کلید-مقدار (KV cache) کراس‌اتنشن یک بار به ازای هر تصویر ساخته می‌شود؟
  4. حین درسابعاد خروجی کراس‌اتنشن زمانی که طول متن Nt=10 و طول تصویر Nv=197 با hidden=256 باشد چیست؟
  5. حین درسچرا در کراس‌اتنشن از ماسک استفاده نمی‌شود؟
  6. پس از درسکدام افزونه را اضافه می‌کنید تا پایداری به سبک Flamingo را در طول آموزش بازیابی کنید؟

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

چرا در لایه‌ی کراس‌اتنشن نیازی به ماسک علّی نیست؟

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

مزیت ادغام دیرهنگام (Late Fusion) بر ادغام زودهنگام چیست؟

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

چرا در کراس‌اتنشن ابعاد کوئری و کلید/مقدار متفاوت است؟

کوئری از توکن‌های متن با طول توالی متن (Nt) می‌آید، در حالی که کلید و مقدار از توکن‌های انکودر تصویر با طول توالی تصویر (Nv) تولید می‌شوند.

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

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