ترانسفورمر به عنوان یکی از معماریهای اصلی در زمینه پردازش زبان طبیعی و بینایی ماشین محسوب میشود و در مسائل مختلفی از ترجمه ماشینی گرفته تا تولید متن و تشخیص تصاویر مورد استفاده قرار میگیرد. این معماری با انعطاف بالا و قابلیت آموزش بیشتر در مقایسه با معماریهای سنتی شبکه عصبی عمیق (Deep Neural Network) مورد توجه قرار گرفته است.در این پست، ما به ترانسفورمرها نگاهی خواهیم کرد - مدلی که از توجه (attention) برای افزایش سرعت آموزش استفاده می کند. این معماری ابتدا در مقالهای به نام "Attention Is All You Need" توسط Vaswani و همکارانش معرفی شد و بسیاری از مسائل پردازش زبان طبیعی را بهبود بخشید.
پس بیایید سعی کنیم مدل را از هم جدا کنیم و به نحوه عملکرد آن نگاه کنیم.
بیایید به مدل به عنوان یک جعبه سیاه نگاه کنیم. در یک برنامه ترجمه، یک جمله را از یک زبان می گیرد و خروجی ترجمه آن را به زبان دیگر می دهد.
با باز کردن این جعبه، یک بخش رمزگذاری (encoding)، یک بخش رمزگشایی (decoding) و اتصالات بین آنها را می بینیم.
بخش رمزگذاری پشته ای از رمزگذارها (encoder) است (در مقاله، شش عدد از آنها را روی هم قرار می دهد - هیچ جادویی در مورد عدد شش نیست، قطعاً می توان با اعداد دیگری هم آزمایش کرد). بخش رمزگشایی پشتهای از رمزگشاها (decoder) به همان تعداد است.
همه encoderها از نظر ساختار یکسان هستند (ولی وزنهای یکسان ندارند). هر کدام به دو زیر لایه تقسیم می شوند:
ورودیهای انکودر ابتدا وارد یک لایه توجه-به-خود (self-attention) میشوند - لایهای که به انکودر امکان میدهد تا با توجه به کلمات دیگر جمله ورودی، کلمات آن را رمزگذاری کند. در ادامه پست بیشتر به self-attention خواهیم پرداخت.
خروجی های لایه self-attention به یک شبکه عصبی feed-forward تغذیه می شود.
دیکودر دارای هر دو لایه است، اما بین آنها یک لایه attention وجود دارد که به دیکودر کمک می کند تا روی کلمات مرتبط در جمله ورودی تمرکز کند (همان چیزی که attention در مدل های seq2seq انجام می دهد).
اکنون که اجزای اصلی مدل را دیدیم، بیایید به بررسی بردارها/تنسورهای مختلف و چگونگی ارتباط آنها در بین این مؤلفه ها، برای تبدیل ورودی یک مدل آموزش دیده به خروجی بپردازیم.
همانطور که در برنامه های NLP به طور کلی وجود دارد، ما با تبدیل هر کلمه ورودی به یک بردار با استفاده از یک الگوریتم امبدینگ embedding شروع می کنیم.
امبدینگ در اولین انکودر انجام می شود. بنابراین ورودی اولین انکودر، امبدینگها خواهند بود، اما در انکودرهای دیگر، خروجی انکودر است که مستقیماً به عنوان ورودی است. سایز لیست امبدینگها، هایپرپارامتری است که میتوانیم تنظیم کنیم - اساساً این عدد برابر طول طولانیترین جمله در مجموعه داده آموزشی ما خواهد بود.
پس از امبدینگ کلمات جمله ورودی، همه بردارهای امبدینگ در هر دو لایه انکودر جریان می یابد.
حالا یک ویژگی کلیدی ترانسفورمر را میبینیم و آن این است که هر کلمه از مسیر خود در انکودر عبور می کند. بین این مسیرها در لایه self-attention وابستگی هایی وجود دارد. با این حال، لایه feed-forward آن وابستگیها را ندارد و بنابراین مسیرهای مختلف میتوانند به صورت موازی در لایه feed-forward اجرا کرد.
در مرحله بعد، مثال را به یک جمله کوتاه تر تغییر می دهیم و به آنچه در هر زیر لایه انکودر اتفاق می افتد نگاه می کنیم.
همانطور که قبلاً اشاره کردیم، یک انکودر لیستی از بردارها را به عنوان ورودی دریافت می کند. این لیست را با انتقال این بردارها به یک لایه self-attention، سپس به یک شبکه عصبی feed-forward، پردازش میکند، سپس خروجی را به سمت بالا به انکودر بعدی ارسال میکند.
مفهوم 'self-attention' مفهومی است که همه باید با آن آشنا باشند. بیایید با نحوه عملکرد آن آشنا شویم.
جمله زیر یک جمله ورودی است که می خواهیم ترجمه کنیم:
”The animal didn't cross the street because it was too tired
”
“it” در این جمله به چه چیزی اشاره دارد؟ منظور street
است یا animal
؟ این یک سوال ساده برای انسان است، اما برای یک الگوریتم ساده نیست.
هنگامی که مدل در حال پردازش کلمه 'it
' است، self-attention به او اجازه می دهد 'it
' را با 'animal
' مرتبط کند.
همانطور که مدل هر کلمه را پردازش می کند (هر موقعیت در دنباله ورودی)، self-attention به آن اجازه می دهد تا به موقعیت های دیگر در دنباله ورودی برای سرنخ هایی نگاه کند که می تواند به رمزگذاری بهتر این کلمه کمک کند.
اگر با RNN ها آشنایی دارید، به این فکر کنید که چگونه حفظ یک حالت پنهان به یک RNN اجازه می دهد تا نمایش خود را از کلمات/بردارهای قبلی که پردازش کرده است را با حالت فعلی که پردازش می کند ترکیب کند. self-attention روشی است که ترانسفورمر برای ایجاد 'درک' سایر کلمات مرتبط به واژه ای که در حال حاضر در حال پردازش آن هستیم، استفاده می کند.
بیایید ابتدا نحوه محاسبه self-attention را با استفاده از بردارها بررسی کنیم، سپس به نحوه پیاده سازی آن با استفاده از ماتریس ها ادامه دهیم.
اولین مرحله در محاسبه self-attention، ایجاد سه بردار از هر یک از بردارهای ورودی انکودر (در این مورد، امبدینگ هر کلمه) است. بنابراین برای هر کلمه، یک بردار Query، یک بردار Key و یک بردار value ایجاد می کنیم. این بردارها با ضرب امبدینگ در سه ماتریس که در طول فرآیند آموزش، آموزش داده ایم ایجاد می شوند.
توجه داشته باشید که این بردارهای جدید از نظر ابعاد کوچکتر از بردار تعبیه شده هستند. ابعاد آنها 64 است، در حالی که بردارهای ورودی/خروجی امبدینگ و انکودر دارای ابعاد 512 هستند. البته الزامی برای اینکه کوچکتر باشند نیست، این یک انتخاب معماری برای محاسبات multiheaded attention است.
بردارهای «query»، «key» و «value» چیست؟
آنها انتزاعاتی هستند که برای محاسبات و تفکر در مورد توجه لازم هستند. هنگامی که نحوه محاسبه توجه در زیر ادامه دهید، تقریباً تمام آنچه را که باید در مورد نقش هر یک از این بردارها بدانید، خواهید دانست.
مرحله دوم در محاسبه self-attention، محاسبه امتیاز (score) است. فرض کنید ما در حال محاسبه self-attention برای اولین کلمه در این مثال، 'Thinking' هستیم. ما باید هر کلمه از جمله ورودی را در مقابل این کلمه امتیاز دهیم. امتیاز تعیین می کند که وقتی کلمه ای را در یک موقعیت خاص خود در جمله رمزگذاری می کنیم، چه مقدار تمرکز روی سایر قسمت های جمله ورودی قرار دهیم.
امتیاز با گرفتن حاصل ضرب نقطه بردار query با بردار key کلمه مربوطه که به آن امتیاز می دهیم محاسبه میشود. بنابراین اگر self-attention را برای کلمه در موقعیت 1# پردازش کنیم، اولین امتیاز حاصل ضرب نقطهای q1 و k1 خواهد بود. امتیاز دوم حاصل ضرب نقطه ای q1 و k2 خواهد بود.
مرحله سوم و چهارم تقسیم امتیازها بر 8 است (ریشه مربع بردارهای کلیدی استفاده شده در مقاله (64) این منجر به داشتن گرادیان های پایدارتر می شود. مقادیر دیگر هم ممکن است، اما پیش فرض این مقدار است)، سپس نتیجه را از یک عملیات softmax عبور دهید. Softmax امتیازات را نرمال می کند تا همه آنها مثبت باشند و جمع آنها 1 شود.
امتیاز سافتمکس تعیین می کند که هر کلمه چقدر در این موقعیت، شاخص می شود. واضح است که خود کلمه در این موقعیت بالاترین امتیاز softmax را خواهد داشت، اما گاهی اوقات تمرکز روی کلمه دیگری که با کلمه فعلی ارتباط دارد مفید است.
مرحله پنجم ضرب هر بردار value در امتیاز سافت مکس است. دلیل آن، این است که مقادیر کلمه(هایی) را که می خواهیم روی آنها تمرکز کنیم، دست نخورده نگه داریم و کلمات نامربوط را حذف کنیم (مثلاً با ضرب آنها در اعداد کوچکی مانند 0.001).
مرحله ششم، جمع بردارهای value وزن داده شده است. این خروجی لایه self-attention را در این موقعیت (برای کلمه اول) تولید می کند.
این خروجی محاسبات self-attention است. بردار به دست آمده برداری است که می توانیم به شبکه عصبی feed-forward ارسال کنیم. اما در پیاده سازی واقعی، این محاسبات به صورت ماتریسی جهت پردازش سریعتر انجام می شود. پس بیایید اکنون که نحوه محاسبات را در سطح کلمه دیدیم، به آن نگاه کنیم.
اولین مرحله، محاسبه ماتریس Query، Key و Value است. ما این کار را با قرار دادن امبدینگهای خود در ماتریس X و ضرب آن در ماتریسهای وزنی که آموزش دادهایم (WQ، WK، WV) انجام میدهیم.
در نهایت، از آنجایی که ما با ماتریسها سر و کار داریم، میتوانیم مراحل دو تا شش را در یک فرمول فشرده کنیم تا خروجیهای لایه self-attention را محاسبه کنیم.
مقاله با افزودن مکانیزمی به نام 'multi-headed attention' لایه self-attention را اصلاح کرد. این امر عملکرد لایه توجه را از دو طریق بهبود می بخشد:
«“The animal didn’t cross the street because it was too tired”»
را ترجمه می کنیم، لازم است که بدانیم «it» به کدام کلمه اشاره دارد.
اگر همان محاسبه self-attention را که در بالا ذکر کردیم انجام دهیم، فقط هشت بار مختلف با ماتریس های وزنی متفاوت، در نهایت به هشت ماتریس Z متفاوت می رسیم.
این ما را با کمی چالش روبرو می کند. لایه feed-forward انتظار هشت ماتریس را ندارد - انتظار یک ماتریس واحد (یک بردار برای هر کلمه) را دارد. بنابراین ما به راهی برای متراکم کردن این هشت در یک ماتریس نیاز داریم.
چگونه ما آن را انجام دهیم؟ ماتریس ها را به هم متصل می کنیم سپس آنها را در یک ماتریس وزن اضافی WO ضرب می کنیم.
این تقریباً تمام چیزی است که در مورد multi-headed self-attention وجود دارد. بیایید همه ماتریسها را در یک تصویر قرار دهیم تا بتوانیم آنها را یکجا نگاه کنیم
اکنون که attention heads را درک کردهایم، بیایید مثال قبلی خود را دوباره بررسی کنیم تا ببینیم attention heads مختلف در کجا تمرکز میکنند، همانطور که کلمه 'it' را در جمله مثال خود رمزگذاری میکنیم:
با این حال، اگر تمام attention heads را به تصویر اضافه کنیم، تفسیر چیزها دشوارتر می شود:
یکی از مواردی که در مدل گم شده است، روشی برای محاسبه ترتیب کلمات در دنباله ورودی است.
برای رفع این مشکل، ترانسفورمر یک بردار به هر تعبیه ورودی اضافه می کند. این بردارها از الگوی خاصی پیروی می کنند که به مدل کمک می کند موقعیت هر کلمه یا فاصله بین کلمات مختلف در دنباله را تعیین کند. علت این است که افزودن این مقادیر به امبدینگها، فواصل معنیداری را بین بردارهای امبدینگ پس از نمایش بردارهای Q/K/V و در هنگام ، فراهم میکند.
اگر فرض کنیم امبدینگ دارای ابعاد 4 است، positional encodings به این صورت خواهند بود:
یکی از جزئیات در معماری encoder که قبل از ادامه باید به آن اشاره کنیم، این است که هر لایه فرعی (self-attention، ffnn) در هر انکودر یک اتصال Residual دارد و سپس در ادامه یک مرحله لایه نرمال سازی دارد.
اگر بخواهیم بردارها و عملیات layer-norm مرتبط با self attention را تجسم کنیم، به شکل زیر خواهد بود:
این برای sub-layers در decoder نیز صدق می کند. اگر بخواهیم یک ترانسفورمر دو پشتهای encoders و decoders داشته باشیم چیزی شبیه به این خواهد بود:
اکنون که بیشتر مفاهیم سمت encoder را پوشش داده ایم، می دانیم که اجزای decoderها چگونه کار می کنند. اما بیایید نگاهی به نحوه کار آنها با یکدیگر بیندازیم.
کار encoder با پردازش دنباله ورودی شروع می شود. سپس خروجی آخرین انکودر به مجموعه بردارهای توجه K و V تبدیل میشود. این بردارها باید توسط هر decoder در لایه «encoder-decoder attention» استفاده شود که به decoder کمک میکند تا روی مکانهای مناسب در دنباله ورودی تمرکز کند:
مراحل فرآیند تکرار میشوند تا زمانی که به نماد خاصی برسد که نشان می دهد decoder خروجی خود را کامل کرده است. خروجی هر مرحله در مرحله بعدی به decoder پایینی داده میشود و decoderها نتایج رمزگشایی خود را درست مانند encoderها به بالا میفرستند. همانند کاری که با ورودیهای encoder انجام دادیم، برای نشان دادن موقعیت هر کلمه، positional encoding را به ورودیهای decoder امبد کرده و اضافه میکنیم.
لایه های self attention در decoder به روشی کمی متفاوت از لایه encoder عمل می کنند:
در decoder، لایه self attention فقط مجاز است به موقعیت های قبلی در دنباله خروجی توجه کند. این کار با ماسک کردن موقعیت های آینده (تنظیم آنها به -inf
) قبل از مرحله softmax در محاسبه self-attention انجام می شود.
لایه 'Encoder-Decoder Attention' درست مانند multiheaded self-attention کار می کند، با این تفاوت که ماتریس کوئری خود را از لایه زیر آن ایجاد می کند و ماتریس کلیدها و Values را از خروجی پشته رمزگذار می گیرد.
پشته decoder یک بردار از اعداد اعشار (floats) را خروجی می دهد. چگونه آن را به یک کلمه تبدیل کنیم؟ این کار آخرین لایه Linear است که یک لایه Softmax بعد از آن میآید.
لایه Linear یک شبکه عصبی fully connected ساده است که بردار تولید شده توسط پشته decoderها را به بردار بسیار بسیار بزرگتری به نام بردار logits تبدیل میکند.
بیایید فرض کنیم که مدل ما 10000 کلمه انگلیسی منحصربهفرد را که از مجموعه دادههای آموزشی خود آموخته است. این باعث می شود که بردار لاجیت 10000 سلول عرض داشته باشد - هر سلول شامل امتیاز یک کلمه منحصر به فرد است. اینگونه است که ما خروجی مدل را با لایه Linear تفسیر می کنیم.
سپس لایه softmax آن امتیازات را به احتمالات تبدیل میکند (همه مثبت، مجموع همه 1.0 میشود). سلول با بیشترین احتمال انتخاب می شود و کلمه مرتبط با آن به عنوان خروجی این مرحله زمانی تولید می شود.
اکنون که کل فرآیند forward-pass از طریق یک ترانسفورمر آموزش دیده را پوشش داده ایم، نگاهی به نحوه آموزش مدل مفید خواهد بود.
در طول آموزش، یک مدل آموزش ندیده دقیقاً همان forward-pass را پشت سر می گذارد. اما از آنجایی که ما آن را روی یک مجموعه داده آموزشی لیبلگذاری شده آموزش میدهیم، میتوانیم خروجی آن را با خروجی صحیح واقعی مقایسه کنیم.
برای تجسم این موضوع، فرض کنیم واژگان خروجی ما فقط شامل شش کلمه است.
(“a”, “am”, “i”, “thanks”, “student”, and “<eos>” (مخفف 'پایان جمله')
هنگامی که واژگان خروجی (output vocabulary) را تعریف می کنیم، می توانیم از بردار با همان بعد برای نشان دادن هر کلمه در واژگان خود استفاده کنیم که به عنوان one-hot encoding شناخته می شود. به عنوان مثال، می توانیم کلمه 'am' را با استفاده از بردار زیر نشان دهیم:
پس از این خلاصه، بیایید تابع loss مدل را مورد بحث قرار دهیم - معیاری که ما در مرحله آموزش بهینه سازی می کنیم تا به یک مدل آموزش دیده و امیدواریم به طرز شگفت انگیزی دقیق برسیم.
فرض کنید ما داریم مدل خود را آموزش می دهیم. فرض کنید اولین مرحله در فرآیند آموزش است و ما آن را با یک مثال ساده آموزش می دهیم - ترجمه 'merci' به 'متشکرم'.
این به این معنی است که ما می خواهیم خروجی یک توزیع احتمال باشد که کلمه 'متشکرم' را نشان می دهد. اما از آنجایی که این مدل هنوز آموزش ندیده است، بعید است که این اتفاق هنوز بیفتد.
چگونه دو توزیع احتمال را با هم مقایسه می کنید؟ به سادگی یکی را از دیگری کم می کنیم. برای جزئیات بیشتر، به cross-entropy و Kullback–Leibler divergence نگاه کنید.
اما توجه داشته باشید که این یک مثال بیش از حد ساده شده است. در شرایط واقع بینانه تر، از جمله ای طولانی تر از یک کلمه استفاده خواهیم کرد. به عنوان مثال - ورودی: 'je suis étudiant' و خروجی مورد انتظار: 'من یک دانشجو هستم'. معنای واقعی آن این است که ما می خواهیم مدل ما به طور متوالی توزیع های احتمال را در جایی که:
<end of sentence>
را نشان دهد که همچنین دارای یک سلول مرتبط با آن از واژگان 10000 عنصر است.حال، از آنجایی که مدل خروجی ها را یکی یکی تولید می کند، می توانیم فرض کنیم که مدل کلمه با بیشترین احتمال را از آن توزیع احتمال انتخاب می کند و بقیه را دور می اندازد. این یکی از راههای انجام آن است (به نام greedy decoding). راه دیگر برای انجام این کار این است که مثلاً دو کلمه بالا را نگه دارید (مثلاً «I» و «a»)، سپس در مرحله بعد، مدل را دو بار اجرا کنید: یک بار با فرض اینکه اولین موقعیت خروجی بود. کلمه 'I'، و بار دیگر با فرض اولین موقعیت خروجی کلمه 'a' بود، و هر نسخه ای که با در نظر گرفتن هر دو موقعیت #1 و #2 خطای کمتری ایجاد کرد، حفظ می شود. ما این کار را برای موقعیت های #2 و #3 ... و غیره تکرار می کنیم. این روش 'beam search' نامیده می شود، جایی که در مثال ما، beam_size دو بود (به این معنی که در هر زمان، دو فرضیه جزئی (ترجمه های ناتمام) در حافظه نگهداری می شود)، و top_beams نیز دو است (به این معنی که دو ترجمه را برمی گردانیم. ). این هر دو فراپارامترهایی هستند که می توانید با آنها آموزش دهید.
امیدوارم این شروعی برای آشنایی با مفاهیم اصلی ترانسفورمر بوده باشد. اگر می خواهید عمیق تر شوید، این مراحل را پیشنهاد می کنم: