آموزش شبکه های عصبی بازگشتی (Recurrent neural network) بخش سوم : Backpropagation

9 5,778

بسم الله الرحمن الرحیم

در بخش قبل(بخش دوم) با هم چگونگی پیاده سازی فاز پیشرو (forward pass) در یک شبکه عصبی بازگشتی (Recurrent Neural network) را به انجام رساندیم. در این پست به فاز دوم پیاده سازی یعنی پیاده سازی عملیات پس انتشار (Backpropagation) در یک شبکه عصبی بازگشتی خواهیم پرداخت.

در این بخش با مفاهیم “پس انتشار در زمان”  و “پس انتشار کوتاه” شده نیز آشنا خواهیم شد.

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

عملیاتهای صورت گرفته در بخش forward مشخص هستند اما برای اینکه قادر باشیم این فاز را بدرستی پیاده سازی کنیم نیاز به جزییات بیشتری داریم . میدانیم که در یک شبکه RNN ما بطور خاص با یک حلقه سرو کار داریم به همین جهت گرادیان هایی که به ازای عملیات انجام شده بدست می آوریم را بایستی با هم جمع کنیم تا در نهایت بدرستی تاثیرات صورت گرفته در فاز forward را لحاظ کرده باشیم .

همانطور که بالا دیدیم فاز forward ما متشکل از ۲ عملیات کلی بود که به قرار زیراند :

h_t = tanh(W_1 \cdot X_t +W_2 \cdot h_{t-1} + b_h)

 o_t = softmax(W_3 \cdot h_t + b_o)

برای انجام پس انتشار ما از گام زمانی اخر شروع کرده خطا را محاسبه میکنیم و سپس در یک حلقه محاسبات را به ازای تمامی گام های زمانی ادامه میدهیم .

فرض ما در اینجا این است که شما با عملیات پس انتشار(backpropagation) آشنایید بنابر این در اینجا با جزییات کامل پس انتشار(که پس انتشار چیست) آشنا نمیشویم انشاءالله در مورد عملیات پس انتشار و مبحث گرادیان ها در مطلبی جداگانه بطور مفصل صحبت خواهم کرد اما بطور کوتاه باید بدانیم که الگوریتم پس انتشار (خطا) یک الگوریتم credit assignment است. یعنی کار این الگوریتم منتسب کردن پاداش یا جزای هر پارامتر نسبت به نتیجه بدست امده نهایی است. اینکه هر پارامتر چقدر در نتیجه بدست امده دخیل بوده و در صورت نتیجه صحیح و یا غلط به چه میزان بایستی تغییر کند. این تمام چیزی است که این الگوریتم مسئولیت انجام آن را دارد. ستون فقرات این الگوریتم را هم گرادیان ها تشکیل میدهند. کاری که ما در اینجا بایستی انجام دهیم مشخص کردن میزان تاثیر هرکدام از متغییرها بر روی نتیجه بدست آمده نهایی است. برای اینکار ما از مشتق جزئی استفاده میکنیم همانطور که حتما میدانید مشتق جزئی در مورد توابع چند متغیره، به مشتق تابع نسبت به یکی از متغیرها با ثابت نگه‌داشتن سایر متغیرها گفته می‌شود. بعنوان مثال در عبارت f(x,y) = 3x^2 + 5xy - 2y^4 مشتق جزئی f نسبت به x برابر با 6x + 5y خواهد بود. و اگر از تابع f نسبت به y بخواهیم مشتق بگیریم نتیجه برابر با 5x - 8y^3 خواهد بود. زمانی که با مشتق جزئی سرو کار داریم از نماد \partial استفاده میکنیم. برای یادآوری این مبحث میتوانید به این نوشته کوتاه و بسیار خوب مراجعه کنید.

خب به بحث اصلی برگردیم. برای محاسبه گرادیان ها ما بصورت زیر بایستی عمل کنیم :

  • محاسبه خطا : output_t - label_t
  • محاسبه گرادیان عبارت خروجی :  o_t = softmax(W_3 \cdot h_t + b_o)
  • محاسبه گرادیان عبارت مربوط به حالت مخفی جدید : h_t = tanh(W_1 \cdot X_t +W_2 \cdot h_{t-1} + b_h)

ابتدا سعی میکنیم خطا را محاسبه کرده و سپس مشتق جزئی برای رابطه  o_t = softmax(W_3 \cdot h_t + b_o) را بدست بیاوریم .

برای محاسبه مشتق عبارت فوق بهتر است از فرم ساده شده آن استفاده کنیم تا فرآیند مشتق گیری آسان تر باشد. ما میتوانیم تابع فوق را بصورت o = f(W_3.h + b_o)  در نظر بگیریم. بعد از اینکار مشتق ما بصورت زیر خواهد بود :

dW_3 = h.du

dh = W_3.du

db_o = 1.du

از انجایی که در این بخش ما بصورت مستقیم با خطا در ارتباط هستیم در نتیجه خطا را بصورت مستقیم میتوانیم لحاظ کنیم بنابر این خواهیم داشت :

dW_3 = h.error

dh = W_3.error

db_o = 1.error

پیاده سازی این عبارات در پایتون بصورت زیر خواهد بود :

 

نکته:

در تعاریف بالا یک مشکل وجود دارد و آن بحث لحاظ کردن حالات مخفی و تاثیرات آنها بر یکدیگر است . همانطور که در فاز forward حالت قبلی در گام زمانی بعدی مورد استفاده قرار میگرفت به همین صورت نیز مشتق حالت مخفی از گام زمانی جدیدتر به گام زمانی قدیمی تر بایستی منتقل شود. از آنجایی که در فاز پس انتشار ما از اخرین گام زمانی شروع کرده و بعد از این گام زمانی دیگری وجود ندارد در نتیجه در شروع کار \partial h = ۰ خواهد بود. اما این مقدار هر بار بروز شده و در گام زمانی قبل تر مورد استفاده قرار میگیرد.

همانطور که در تصویر زیر میبینیم هر سلول h فعلی را به گام زمانی بعدی ارسال میکند. به همین صورت در زمان بک پراپگیشن هم \partial h علاوه بر محاسبه گرادیان مبتنی بر خروجی ، گرادیان حاصل از گام زمانی بعدی نیز بایستی لحاظ شود.

در تصویر زیر این مهم مشخص شده است, نکته قابل اهمیت dh است به همین منظور تنها جهت جریان forward و همینطور گرادیان متناظر با آن مشخص شده است .

به همین خاطرمحاسبه dhبصورت زیر خواهد بود :

*** QuickLaTeX cannot compile formula:
dh = W_3.error + dh_{from\_next\_timestep}}

*** Error message:
Extra }, or forgotten $.
leading text: $dh = W_3.error + dh_{from\_next\_timestep}}

و از انجایی که شروع عملیات پس انتشار از گام زمانی اخر است و گام زمانی بعدی وجود ندارد مقدار dh_{from\_next\_timestep} در ابتدا صفر میباشد.

به همین صورت پیاده سازی عبارات فوق در زبان پایتون بصورت زیر خواهند بود :

 

حالا نیازمند محاسبه مشتقات جزئی برای عبارت دوم هستیم . همانطور که تازه دیدیم میتوانیم این عبارت را نیز بصورت h = f(u) ساده سازی کنیم. از انجایی که تابع f ما در اینجا در اصل tanh است و مشتق tanh(u) برابر با

*** QuickLaTeX cannot compile formula:
1-tan(u)^2\partialu

*** Error message:
Undefined control sequence \partialu.
leading text: $1-tan(u)^2\partialu

است. بنابر این خواهیم داشت h = tanh(u) و با جایگزین کردن خواهیم داشت:

dtanh = (1-h^2)\partial h

چرا که

h = tanh(u)

*** QuickLaTeX cannot compile formula:
dtanh = (1-tanh(u)^2\partialu)

*** Error message:
Undefined control sequence \partialu.
leading text: $dtanh = (1-tanh(u)^2\partialu

پس

 ( 1 - tanh(u)^2.du ) == ( 1 - h^2.dh )

به همین صورت مشتق سایر عبارتها را بصورت زیر بدست می آوریم :

dX_t = W_1.dtanh
dW_1 = X_t.dtanh
dW_2 = h_{prev}.dtanh
db_h = 1.dtanh

حالا اگر بخواهیم معادل این دستورات را در پایتون داشته باشیم بصورت زیر عمل میکنیم.

توجه :

دقت کنید که ما در اینجا ضرب بین بردارها و ماتریس ها را داریم و ابعاد مشتقات برابر با اندازه بردار یا ماتریس متناظر آن است(همانطور که قبلا گفتیم در مشتق ما بدنبال میزان تاثیر هر “پارامتر” بر روی نتیجه نهایی هستیم. پس به ازای هر پارامتر قابل یادگیری (هر دارایه ماتریس یا بردار) ما یک مقدار خواهیم داشت. همینطور توجه کنید که ما تنها به پارامترهای قابل ترین اهمیت میدهیم.(کد زیر را ببینید)

 

بعد از مشخص کردن موارد بالا پیاده سازی کامل این فاز براحتی قابل انجام خواهد بود. دو تابع زیر وظیفه پیاده سازی موارد مطرحی بالا را بعهده دارند :

 

به منظور تست توابع پیاده سازی شده نیز میتوان بصورت زیر عمل کرد :

 

خروجی :

پیاده سازی کامل :

در نهایت تمام پیاده سازی ما بصورت زیر خواهد بود :

 

در پیاده سازی فوق ما مفاهیم پایه را در نظر گرفتیم . مفاهیمی همانند truncated backpropagation و مباحث مرتبط با مقداردهی اولیه و بحث محوشدگی گرادیان(روشهای مقابله با آن همانند Gradient clipping و…) بعد از بیان مفاهیم پایه بعدی در پستی جداگانه بیان میشوند.

نکته پیاده سازی(پس انتشار در زمان و پس انتشار کوتاه شده در زمان)

در زیر مروری بر این مفاهیم داریم و انشاءالله در مطلبی جداگانه بصورت کامل به این مباحث خواهیم پرداخت.

پس انتشار در زمان (Backpropagation Through Time )  :

عملیات پس انتشار در یک شبکه عصبی بازگشتی تا حد بسیار زیادی همانند آنچه در شبکه های عصبی سنتی رخ میدهد است. اما در این نوع شبکه ها ما نیازمند آن هستیم که خطاها را از سلول فعلی به سلول های قبلی نیز منتقل کنیم. این همان قابلیتی است به شبکه عصبی بازگشتی اجازه حفظ و ثبت وابستگی های زمانی بلند مدت را میدهد. از آنجایی که ما درگیر گام های زمانی مختلف هستیم به این انتقال خطا از سلول فعلی به سلولهای قبلی (در طول زمان) پس انتشار در زمان (بک پراپگیشن در زمان) گفته میشود. از انجایی که پارامترهای مدل ما در این نوع شبکه ها در بین سلولهای مختلف بصورت مشترک اند (هر سلول RNN دقیقا از یک مجموعه وزن و بایاس بهره میبرد)، ما باید گرادایان ها را با توجه به هر گام زمانی بصورت جداگانه محاسبه کرده و سپس آن ها را با یکدیگر جمع کنیم. مشابه این روش را در شبکه های عصبی دیگری که دارای اشتراک پارامتر هستند (همانند شبکه های عصبی کانولوشن) نیز مشاهده میکنیم.

پس انتشار در زمان یا اصطلاحا Backpropagation through time (یا بصورت اختصار BPTT ) شیوه ای است که برای آموزش شبکه های عصبی بازگشتی مورد استفاده قرار میگیرد. این روش اما در زمان استفاده از دنباله های بسیار طولانی با مشکلاتی مواجه است. یادگیری یک شبکه عصبی بازگشتی با الگوریتم پس انتشار در زمان نیازمند باز کردن(unfold) شبکه در زمان به تعداد تمامی گام های زمانی که در دنباله ورودی وجود دارد است. این مساله در دنباله های بسیار طولانی سبب تحمیل محاسبات بسیار سنگین و مصرف حافظه بسیاری میشود چرا که نیازمند پردازش تمامی دنباله ورودی هستیم تا نهایتا یک گرادیان بدست آید این کاستی در اغلب اوقات(زمانی که خروجی از نوع سری باشد) با کوتاه سازی دنباله ورودی به چند دنباله کوچکتر و اعمال پس انتشار در این زیردنباله ها برطرف میشود. این الگوریتم به پس انتشار کوتاه شده در زمان یا اصطلاحا turncated backpropagation through time یا به اختصار TBPTT معروف است. هرچند این روش مشکل سربار محاسباتی و حافظه را در الگوریتم اصلی برطرف میسازد اما باعث از دست دادن وابستگی های زمانی طولانی مدت نیز میشود.

الگوریتم پس انتشار در زمان و معادل کوتاه شده آن تقریبا در حوزه شبکه های عصبی بازگشتی بی رقیب هسند. “پس انتشار در زمان” تقریبا بر روی دنباله های بسیار طولانی بسختی قابل استفاده است چرا که نیازمند ذخیره سازی و پس انتشار در یک شبکه با تعداد بسیار زیاد گام زمانی است (که هرکدام را میتوانید در قالب یک لایه متصور شوید) که این مساله باعث مصرف حجم قابل توجهی از حافظه و تحمیل سربار پردازشی بسیار زیادی است. مصرف حافظه را میتوان بصورت جزئی همانطور که در مقاله Memory-efficient backpropagation throughtime آمده است با افزایش سربار پردازشی برطرف کرد. پس انتشار در دنباله های بسیار طولانی همچنین به معنای اعمال گام های گرادیان کاهشی(gradient descent) کمتری است که بطور قابل توجهی آموزش را کند میکند. الگوریتم پس انتشار کوتاه شده در زمان کاستی های الگوریتم “پس انتشار در زمان” را با تقسیم دنباله اولیه به چند زیر دنباله با اندازه مساوی برطرف میکند. “پس انتشار کوتاه شده در زمان” جریان گرادیان در بین زیر دنباله ها را کوتاه میکند اما حالت مخفی در شبکه را حفظ میکند. عمل کوتاه سازی باعث ایجاد گرایش(بایاس) در گرادیان ها شده و با اینکار هرگونه ضمانت همگرایی با زیربنای نظری را از بین میبرد.  بطور شهودی پس انتشار کوتاه شده در زمان در یادگیری وابستگی هایی که بیش از اندازه کوتاه سازی است با مشکل مواجه است. روش هایی همانند NoBackTrack و Unbiased Online Recurrent Optimization(UORO) هر دو الگوریتم های یادگیری بازگشتی برخط بدون گرایش قابل تطبیقی را فراهم میکنند. این روشها نقطه نظر مقابل که نیازمند بدون حافظگی است را در پیش میگیرند بنابر این روشهای کوتاه سازی و هرگونه ذخیرهسازی حالات قبلی را غیرمجاز در نظر میگیرند. این روشها تماما برخط هستند و ساختار جریانی آنها باعث ایجاد تزریق نویز در تقریب گرادیان ها میشود.

 

منابع جهت مطالعه بیشتر :

https://www.cs.utoronto.ca/~ilya/pubs/ilya_sutskever_phd_thesis.pdf
https://arxiv.org/pdf/1705.08209.pdf
https://r2rt.com/styles-of-truncated-backpropagation.html
http://ir.hit.edu.cn/~jguo/docs/notes/bptt.pdf
https://r2rt.com/written-memories-understanding-deriving-and-extending-the-lstm.html#backpropagation-through-time-and-vanishing-sensitivity

در بخش بعد به معرفی GRU و بعد از آن LSTM خواهیم پرداخت.

9 نظرات
  1. […] آموزش شبکه های عصبی بازگشتی (Recurrent neural network) بخش سوم : Backpr… […]

  2. […] آموزش شبکه های عصبی بازگشتی (Recurrent neural network) بخش سوم : Backpr… […]

  3. مهسا می گوید

    e[np.arange(e.shape[0]), np.argmax(true_label, axis=1)] -=1
    همانطور که توضیح داده اید خطا همان اختلاف خروجی شبکه در هر گام از مقدار واقعی همان گام است
    اما من پیاده سازی که به صورت بالا گفته اید را متوجه نمیشوم
    اگر امکان دارد این فرمول را بیشتر توضیح دهید
    باتشکر

    1. سید حسین حسن پور متی کلایی می گوید

      کدوم بخشش رو متوجه نمیشید؟
      بخش مرتبط با پایتون (این دستور چکار میکنه) یا اینکه کلا مفهوم این قطعه کد رو متوجه نمیشید؟

  4. مهسا می گوید

    اگر true_lable=[[1,0,0],[0,0,1]] حال np.argmax(true_label, axis=1)=[0,2]
    و اگر خروجی شبکه به صورت e=[[0,1,0],[1,0 ,0]] (در واقع هنوز اموزش ندیده است) حال e.shape[0]=2 و [
    [۰,۱]=np.arange(e.shape[0])
    حال من متوجه نمیشوم
    e[[0,1],[0,2]]یعنی چه ؟ و پس از آن e[[0,1],[0,2]]-=1چطور اختلاف مقادیر خروجی و واقعی را محاسبه میکند؟

    1. سید حسین حسن پور متی کلایی می گوید

      سلام
      کامنت های پیاده سازی نهایی که نوشتم رو بخونید متوجه میشید . این پیاده سازی vectorized هست . نمونه غیر برداری اون رو هم بصورت کامنت تو خط ۳۷ نوشتم که ببینید احتمالا کاملا متوجه میشید.
      بخش اول سطرها رو مشخص میکنه و بخش دوم اندیس جاهایی که لیبل ۱ هست . مثلا سطر ۰ اندیس ۰ ( و سطر ۱ اندیس ۲ )
      دقت بکنید میبینید سطر ۰ اندیس ۰ داره اطلاعات لیبل صحیح شما رو میده که برای نمونه اول (سطر اول) کدوم کلاس صحیح هست :
      true_lable=[[1,0,0],
      [۰,۰,۱]]

      لیبل مربوط به نمونه اول (سطر ۰) اندیس ۰ نشانگر کلاس صحیح هست .
      و در نمونه دوم (سطر ۱) هم اندیس ۲ نشانگر کلاس صحیح هست.
      حالا همین درایه ها از ۱ کسر میشن تا خطا محاسبه بشه. (در هر گام زمانی ما فقط دنبال این هستیم که ببینیم در اون گام کلاس صحیح انتخاب شده یا نه و این رو از طریق قیاس خروجی پیش بینی شده – لیبل (که در اصل همون ۱ هست) بدست میاریم.

  5. مهسا می گوید

    یعنی اینجوری که شما میفرمایید اگر من درست متوجه شده باشم
    داخل ماتریس eفقط طبق مثال درایه های (۰,۰)و (۱,۲) منهی یک میشوند اگر مقدار این درایه ها برابر یک شده باشند پس با کم کردن از یک میشه صفر و این یعنی ما کاملا شبکمون اموزش دیده و به همون لیبل ها رسیده درسته؟ ودیگه مقادیر پارامترها اپدیت نمیشوند (چون مشتقات صفر خواهند شد ) .
    و خطا per_ts_loss به چه علت استفاده شده است؟

    1. سید حسین حسن پور متی کلایی می گوید

      ما داریم خطای خروجی رو محاسبه میکنیم برای یک گام! اگر جمله شما ۵ کلمه باشه باید مشخص بشه ایا تمام کلمات درست تشخیص داده شدند یا نه. پس شما هر بار یک کلمه رو میبینید آیا درست بوده تشخیص یا نه. صرف این که یک گام زمانی خروجی صحیحی تولید کرده به معنای تنظیم بودن شبکه نیست. انقدر این مساله تکرار میشه تا خطای کلی نهایتا کمینه بشه.
      اون per_ts_loss هم برای محاسبه کراس انتروپی هست. کد رو ببینید مشخصه.

  6. مهسا می گوید

    خیلی ممنون از توضیحاتتون

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.

این سایت از اکیسمت برای کاهش هرزنامه استفاده می کند. بیاموزید که چگونه اطلاعات دیدگاه های شما پردازش می‌شوند.