لایه ی Batch Normalization (یا همون BN)، اثرات خیلی مفیدی داره که باعث شده طراحای شبکههای عصبی، در بیشتر مدلهایی که طراحی میکنن ازش استفاده کنن. یکی از این دلایل سرعت بالای آموزش (یا به اصطلاح دقیق تر، Converge کردن وزنهای مدل) هست.
توی این پست، اول توضیح میدم که BN دقیقا چیکار می کنه و بعدش اثری که روی فاز Training شبکههای عصبی میذاره رو بررسی میکنم. در آخر هم بحث میکنم که بهترین محل برای استفاده از BN بعد از کدوم لایهها توی شبکهی عصبی هست.
به صورت خلاصه، کاری که توی الگوریتم BN انجام میشه، نرمال کردن توزیع دیتا هست. (نرمال کردن اینجا کلمهی دقیقی نیست، ولی جایگزینی به ذهنم نرسید) دیتا بعد از این نرمال شدن، به شکلی در میاد که میانگینش حتما صفر و واریانسش حتما 1 باشه. (توی آمار کلاسیک، فرض میشه اکثر متغیرهای تصادفی که در طبیعت دیده میشن توزیع گاوسی یا شبه گاوسی دارن. در نتیجه برای اینکه بهتر بشه مقایسشون کرد، هرکدوم رو به شکلی Transform میکنن که میانگینشون صفر و واریانسشون 1 بشه.) توزیع مهمی که میشناسیم و این خاصیت رو داره، توزیع نرمال هست. توزیع نرمال همون توزیع گاوسی، با میانگین صفر و واریانس 1 هست.
توی BN، با این که میدونیم توزیع دادههامون گاوسی نیست، (در اصل توزیع دادههای ما همیشه گسسته هست در حالی که توزیعهای نرمال و حالت کلی ترش گاوسی، توزیعهای احتمال پیوسته هستن.) سعی میکنیم مثل توزیع گاوسی باهاش رفتار کنیم. اگه یه متغیر تصادفی گاوسی با میانگین u و واریانس s داشته باشیم:
X ~ Gaussian(u, s)
میتونیم یه متغیر تصادفی جدید از روش بسازیم که میانگینش صفر و واریانسش 1 باشه:
Y := (X - u) / s ~ Gaussian(0, 1) = Normal()
حالا که این متغیر تصادفی جدید رو ساختیم، میتونیم به جای متغیر تصادفی قبلی ازش استفاده کنیم و اینبار مطمئن باشیم که خیلی از خواص زیبای ریاضیاتی که برای توزیع نرمال اثبات شدن، برای متغیر جدید ما هم برقرار هستن. به عنوان مثال، میتونیم از روی محاسبهی فاصلهی یه نمونه از متغیر تصادفی جدید از صفر، میزان احتمال outlier بودن این نمونه رو مشخص کنیم. (متغیر تصادفی نرمال، احتمال کمی داره که از بازهی (-1 تا 1) بیرون بیوفته. احتمال خیلی کمی داره که از بازهی (-2 تا 2) بیرون بیوفته، احتمال خیلی خیلی کمی داره که از بازهی (-3 تا 3) بیرون بیوفته و ... پس اگه مقدارش مثلا 5 باشه، یعنی احتمالا باید یه outlier باشه. (یعنی مثلا موقع ثبت اطلاعاتش، یه اشتباه رخ داده و نویزی شده))
حالا توی شبکههای عصبی، چطوری از این نرمالیزیشن استفاده میکنیم؟ ساده است! وقتی یه دسته (Batch) از داده رو به شبکهی عصبی مون دادیم، هر جا که قرار باشه BN انجام بشه، یه دور از دادهها میانگین میگیریم (میانگین دادهها رو در راستای Batch میگیریم. یعنی اگه یه ماتریس 128 در 100 داشته باشیم که 128 اندازهی Batch و 100 تعداد نورونهای لایهی قبلی باشه، میانگینی که به دست میاریم باید یه بردار 100 تایی باشه.) و این میانگین به دست اومده رو از دادهها کم میکنیم. بعدش واریانس همین دادهها رو محاسبه میکنیم (واریانس رو هم باید دقیقا مثل میانگین به ازای هر نورون به صورت مجزا محاسبه کنیم. 100 تا نورون خروجی = 100 تا واریانس محاسبه شده.) حالا دادهها رو بر بردار واریانسها تقسیم میکنیم. (یعنی همهی 128 تا خروجی که برای نورون شمارهی 7 محاسبه شدن، اول میانگینشون ازشون کم میشه و بعد بر واریانسشون تقسیم میشن.) نتیجهی حاصل، یه ماتریس 128 در 100 جدیده، که میانگین هر ستونش برابر صفر و واریانس هرکدوم از ستوناش هم 1 هست.
گام بعدی توی اجرای BN، ضرب کردنش در بردار گاما و بعد جمع کردن سطرهای این ماتریس با بردار بتا هست. عملا با این کار، میانگین توزیع جدیدمون رو برابر بتا و واریانسش رو برابر گاما تعیین میکنیم. بردارهای بتا و گاما از کجا میان؟ خیلی ساده است، اجازه میدیم به کمک الگوریتم Back Propagation مثل بقیهی پارامترهای مدل آموزش داده بشن. اینطوری مدل میتونه در صورتی که لازم باشه، توزیع رو از حالت نرمال خارج کنه.
تا اینجا توضیح دادم که الگوریتم BN چیکارا انجام میده، اما این که چرا این کارارو انجام میده و با انجامشون، چه تاثیری روی عملکرد شبکهی عصبی میذاره، توی بخش بعدی در موردش صحبت میکنم.
آموزش شبکههای عصبی، بعضی وقتا خیلی کند و اعصاب خورد کن میشه. خصوصا وقتی شبکه مون خیلی سنگین باشه و برای هر epoch از آموزشش، چندین ساعت زمان لازم باشه. اما چرا؟ دلیلش تغییر مکرر توزیع خروجی نورونهای مخفی شبکه مون هست. فرض کنید نورون N قراره یاد بگیره که یه ویژگی به خصوص توی داده رو تشخیص بده و برای این کار، از خروجی نورونهای A و B استفاده میکنه. حالا همونطور که توی روند آموزش، همهی وزنها همزمان آموزش داده بشن، با یه ذره تغییر کردن وزنهای نورونهای A و B، خروجیهاشون یکم شیفت پیدا میکنن. با این شیفت جزئی، ممکنه نورون N (که قبلا داشت کارش رو نسبتا خوب انجام میداد) دوباره نیاز داشته باشه که مقادیر وزنهاشو تغییر بده تا با ورودیهای جدیدش هماهنگ باشه. در نتیجه، فرصت نمیکنه خروجی خودش رو هم بهبود بده و به دنبال هر تغییر کوچیک توی وزنهای نورونهای لایهی قبلی، فرآیند یادگیری نسبتا ساده اش رو تکرار کنه. (حالا اتفاقی که توی شبکههای عصبی خیلی عمیق میافته رو تصور کنین... هر لایه هر بار باید یاد بگیره با ورودیهایی با توزیع جدید کار کنه... و این توزیعها همش دارن تغییر میکنن...) حالا اگه توی همین سناریو، BN انجام بشه، با وجود تغییر مداوم توزیع خروجیهای A و B، نورون N به خاطر اعمال BN همیشه با توزیع یکسان مواجهه (میانگین همیشه صفره و واریانس همیشه مقدار 1 داره) پس تغییرات جزئی یا حتی شدید توی مقادیر وزنهای لایههای قبل، اثری روی آموزش نورون N نمیذاره و میتونه به کمک گرادیانهایی که دریافت میکنه، کار تشخیص اون ویژگی خاص توی دادهها رو مرتب بهبود بده.
همچنین در یه سناریوی دیگه، فرض کنید در ابتدای فرآیند آموزش، خروجی نورونهای A و B قبل از اعمال تابع فعالساز ReLU، مقادیر منفی داشته باشن. پس خروجی نهایی شون حتما صفر میشه. نورون N برای این که بتونه هرگونه گرادیان رو به این نورونها منتقل کنه، باید صبر کنه تا خروجی این نورونها تغییر کنه و به ناحیهی مثبت وارد بشه. پس عمل آموزش وزنهای نورون N و در نتیجه A و B، شدیدا کند میشه. در حالی که اگه از BN استفاده کنیم، این وزنها در وضعیتی خواهند بود که حدود نصفشون کمتر از صفر و بقیه بیشتر از صفر باشن. اینجوری میشه تضمین کرد که فرآیند آموزش هیچوقت متوقف نمیشه.
یه اتفاق هست که وقتی از توابع loss خیلی خاص استفاده میکنیم ممکنه پیش بیاد، اون هم زمانیه که تابع loss خروجی به شکلیه که ممکنه در یه batch خاص، مقدار گرادیانش خیلی زیاد باشه (نزدیک به بینهایت) و در نتیجه، مقادیر وزنهای شبکه رو با شدت زیادی تغییر بده. این کار بعضی وقتا باعث میشه همه یا بخشی از نورونها به نورونهای مرده تبدیل بشن. در نتیجه خروجی مدل مستقل از ورودیهاش، همیشه ثابت بمونه و دیگه آموزشی انجام نشه. اما اگه BN اعمال بشه، فرآیند آموزش دوباره میتونه از سر گرفته بشه چون هیچ نورونی بعد از اعمال BN نمیتونه نورون مردهی ReLU باشه. (میانگین صفر داشتن دادههای Batch باعث میشه همیشه یه تعداد از دادهها در سمت فعال تابع ReLU قرار بگیرن.)
این بخشو به صورت خلاصه میگم: اگه شبکهی عصبی تون عمق چندان زیادی نداره، برای پیاده سازی BN خودتونو به زحمت نندازین، احتمالا تاثیر زیادی روی فرآیند آموزش مدلتون نمیذاره. اما اگه دارین یه شبکهی عصبی ژرف آموزش میدین، که بیشتر از 5-6 لایه داره، بهتره بعد از اعمال activation function های هر لایه از BN استفاده کنین.
یه نکتهی قابل بحث دیگه هم اینه که بعضی از محققین معتقد هستن باید BN رو قبل از activation function هر نورون اعمال کنیم نه بعدش. اما منم مثل آقای فرانسوا شوله فکر میکنم اعمال BN بعد از تابع فعالساز، کار عاقلانهتری محسوب میشه. اگه قبل از اعمال ReLU از BN استفاده کنیم، باعث میشیم همهی نورونهای شبکهمون برای حدود نصف دادههای ورودی مقدار صفر به خودشون بگیرن. یعنی هر نورون برای نصف دادهها فعال و برای نصف دیگه غیرفعال میشه. در حالی که مثلا نورونی که میخواد یه ویژگی رو تشخیص بده، که احتمالش 10 درصد باشه، نیاز داره 90 درصد دادهها رو در نیمهی غیرفعالش قرار بده و فقط برای 10 درصد باقی مونده از دادهها فعال بشه. ولی اگه BN رو بعد از activation اعمال کنیم، وزنهای لایهی بعدی خودشون میتونن دادهها رو به قدر نیاز شیفت بدن و این تفاوت رو اعمال کنن.
یه جایی توی استکاوورفلو یه کامنت جالب دیدم، که [به درستی] اشاره میکرد اگه بعد از یه لایهی کانوولوشنی، از pooling استفاده میکنین، بهتره BN لایهی کانوولوشنی رو حذف کنین و بعد از pooling نرمالیزیشن رو اضافه کنین. دلیلش هم محاسبات کمتره. اگه مثلا pooling با کرنلهای 2 در 2 استفاده میکنید، محاسبات و همچنین پارامترهایی که برای BN استفاده می کردین، با انتقال BN به بعد از لایهی pooling، میتونین محاسبات انجام شده برای BN رو چهار برابر سریعتر کنین.
توی این نوشته در مورد BN صحبت کردم، و توضیح دادم چطور کار میکنه. همینطور در مورد علت استفاده از این تکنیک و خواصی که در فرآیند آموزش شبکههای عصبی ایجاد میکنه حرف زدم. بعد از اون، چند تا توصیه در مورد نحوهی استفاده از BN و بایدها و نبایدهاش گفتم. امیدوارم مفید باشه.
منابع:
https://en.wikipedia.org/wiki/Normalization_(statistics)
https://github.com/keras-team/keras/issues/1802#issuecomment-187966878