ضمیمه B: Autodiff (مشتقگیری خودکار)
این ضمیمه توضیح میدهد قابلیت Autodiff (مشتقگیری خودکار) در TensorFlow چگونه کار میکند و با راهحلهای دیگر چه تفاوتی دارد.
فرض کنید تابع f(x, y) = x²y + y + 2 را تعریف کردهاید و به مشتقهای جزئی ∂f/∂x و ∂f/∂y نیاز دارید؛ معمولاً برای اجرای گرادیان نزولی یا الگوریتم بهینهسازی دیگری. گزینههای اصلی شما عبارتاند از مشتقگیری دستی، تقریب تفاضل محدود، مشتقگیری خودکار حالت روبهجلو و مشتقگیری خودکار حالت معکوس. TensorFlow حالت معکوس را پیادهسازی میکند، اما برای درک آن بهتر است ابتدا گزینههای دیگر را بررسی کنیم.
مشتقگیری دستی
نخستین روش محاسبه مشتقها این است که با قلم و کاغذ از قواعد حساب دیفرانسیل برای استخراج معادله مناسب استفاده کنید. برای تابع بالا کار چندان دشواری نیست و کافی است پنج قاعده را بهکار ببرید:
- مشتق یک ثابت برابر ۰ است.
- مشتق
λx برابر λ است، اگر λ ثابت باشد. - مشتق
x^λ برابر λx^(λ−1) است؛ بنابراین مشتق x² برابر 2x است. - مشتق مجموع چند تابع، مجموع مشتقهای آن توابع است.
- مشتق حاصلضرب یک ثابت
λ در یک تابع، برابر همان ثابت ضربدر مشتق تابع است.
از این قواعد میتوان معادله B-1 را بهدست آورد.
Equation B-1 — Partial derivatives of f(x, y)
∂f/∂x = ∂(x²y)/∂x + ∂y/∂x + ∂2/∂x = y·∂(x²)/∂x + 0 + 0 = 2xy
∂f/∂y = ∂(x²y)/∂y + ∂y/∂y + ∂2/∂y = x² + 1 + 0 = x² + 1
برای توابع پیچیدهتر، این روش میتواند بسیار خستهکننده شود و احتمال خطا نیز وجود دارد. خوشبختانه گزینههای دیگری داریم؛ در ادامه تقریب تفاضل محدود را بررسی میکنیم.
تقریب تفاضل محدود
به یاد بیاورید مشتق h′(x₀) تابع h(x) در نقطه x₀ همان شیب تابع در آن نقطه است. دقیقتر، مشتق بهصورت حد شیب خط مستقیمی تعریف میشود که از نقطه x₀ و نقطه دیگری x روی تابع میگذرد، هنگامی که x بینهایت به x₀ نزدیک میشود.
Equation B-2 — Definition of the derivative of h(x) at x₀
h′(x₀) = limx→x₀ [h(x) − h(x₀)] / (x − x₀) = limε→0 [h(x₀ + ε) − h(x₀)] / ε
برای محاسبه مشتق جزئی f(x,y) نسبت به x در x=3 و y=4، میتوان f(3+ε,4)−f(3,4) را با مقدار بسیار کوچکی برای ε محاسبه و نتیجه را بر ε تقسیم کرد. این نوع تقریب عددی مشتق «تقریب تفاضل محدود» نام دارد و این معادله خاص «خارجقسمت تفاضل نیوتن» است. کد زیر دقیقاً همین کار را انجام میدهد:
def f(x, y):
return x**2*y + y + 2
def derivative(f, x, y, x_eps, y_eps):
return (f(x + x_eps, y + y_eps) - f(x, y)) / (x_eps + y_eps)
df_dx = derivative(f, 3, 4, 0.00001, 0)
df_dy = derivative(f, 3, 4, 0, 0.00001)
نتیجه دقیق نیست و برای توابع پیچیدهتر وضعیت بدتر میشود. پاسخ درست بهترتیب ۲۴ و ۱۰ است، اما خروجی بهشکل زیر است:
>>> df_dx
24.000039999805264
>>> df_dy
10.000000000331966
برای محاسبه هر دو مشتق جزئی، باید تابع f() را دستکم سه بار فراخوانی کنیم؛ در کد بالا چهار بار فراخوانی شده، هرچند قابل بهینهسازی است. اگر ۱۰۰۰ پارامتر داشته باشیم، دستکم ۱۰۰۱ بار فراخوانی تابع لازم است. برای شبکههای عصبی بزرگ، این موضوع تقریب تفاضل محدود را بسیار ناکارآمد میکند.
بااینحال، چون پیادهسازی این روش بسیار ساده است، ابزار خوبی برای کنترل درستی روشهای دیگر بهشمار میرود. برای نمونه، اگر نتیجه آن با تابعی که دستی مشتق گرفتهاید ناسازگار باشد، احتمالاً تابع مشتقگیری دستی شما خطا دارد.
تا اینجا دو روش محاسبه گرادیان را دیدیم: مشتقگیری دستی و تقریب تفاضل محدود. هر دو برای آموزش شبکه عصبی در مقیاس بزرگ نقص بنیادی دارند. اکنون به Autodiff میرسیم و از حالت روبهجلو آغاز میکنیم.
Forward-Mode Autodiff (مشتقگیری خودکار حالت روبهجلو)
شکل B-1 نشان میدهد مشتقگیری خودکار حالت روبهجلو روی تابع سادهتر g(x,y)=5+xy چگونه کار میکند. سمت چپ گراف تابع و سمت راست گراف مشتق جزئی ∂g/∂x = 0 + (0×x + y×1) = y را نشان میدهد. به همین ترتیب میتوان مشتق نسبت به y را نیز بهدست آورد.
الگوریتم گراف محاسباتی را از ورودیها به سمت خروجیها طی میکند؛ از همین رو «حالت روبهجلو» نامیده میشود. ابتدا مشتقهای جزئی گرههای برگ محاسبه میشوند. گره ثابت ۵ مقدار ۰ بازمیگرداند، چون مشتق ثابت همیشه صفر است. متغیر x مقدار ۱ بازمیگرداند، زیرا ∂x/∂x=1، و متغیر y مقدار ۰ بازمیگرداند، زیرا ∂y/∂x=0. اگر مشتق نسبت به y را میخواستیم، وضعیت این دو متغیر برعکس میشد.
اکنون میتوانیم در گراف به گره ضرب تابع g برسیم. حساب دیفرانسیل میگوید مشتق حاصلضرب دو تابع u و v برابر ∂(u×v)/∂x = ∂v/∂x×u + v×∂u/∂x است. بنابراین بخش بزرگی از گراف سمت راست، یعنی 0×x + y×1، ساخته میشود.
در پایان به گره جمع میرسیم. مشتق مجموع توابع، مجموع مشتقهای آنهاست؛ پس با افزودن یک گره جمع به بخشهای محاسبهشده، مشتق درست ∂g/∂x = 0 + (0×x + y×1) حاصل میشود.
شکل B-1. مشتقگیری خودکار حالت روبهجلو
این معادله را میتوان بسیار ساده کرد. با هرس چند عمل غیرضروری از گراف محاسباتی، گراف بسیار کوچکی با فقط یک گره میماند: ∂g/∂x = y. در این مثال سادهسازی آسان است، اما برای توابع پیچیدهتر، مشتقگیری خودکار حالت روبهجلو ممکن است گرافی بسیار بزرگ تولید کند که سادهسازی آن دشوار و کارایی آن نامطلوب باشد.
از یک گراف محاسباتی شروع کردیم و مشتقگیری خودکار حالت روبهجلو گراف محاسباتی دیگری تولید کرد. این روش «مشتقگیری نمادین» نام دارد و دو مزیت مهم دارد: پس از تولید گراف مشتق میتوان آن را بارها برای مقادیر مختلف x و y اجرا کرد؛ همچنین میتوان دوباره مشتقگیری خودکار را روی همان گراف اجرا کرد تا مشتق مرتبه دوم، سوم و بالاتر بهدست آید.
البته حالت روبهجلو را میتوان بدون ساخت گراف، یعنی بهصورت عددی، نیز اجرا کرد و نتایج میانی را در لحظه محاسبه نمود. یکی از راهها استفاده از «اعداد دوگان» است: اعداد جالبی بهشکل a+bε که در آن a و b حقیقیاند و ε عددی بینهایت کوچک با ویژگی ε²=0 و درعینحال ε≠0 است. عدد دوگان 42+24ε را میتوان برای تقریب ذهنی شبیه 42.0000…000024 با بینهایت صفر تصور کرد، هرچند این فقط برای ایجاد شهود است.
در حافظه، عدد دوگان بهصورت یک جفت عدد اعشاری ذخیره میشود؛ برای نمونه 42+24ε با جفت (42.0, 24.0) نمایش داده میشود. اعداد دوگان را میتوان جمع و ضرب کرد:
Equation B-3 — Operations with dual numbers
λ(a+bε)=λa+λbε
(a+bε)+(c+dε)=(a+c)+(b+d)ε
(a+bε)(c+dε)=ac+(ad+bc)ε+bdε²=ac+(ad+bc)ε
مهمتر از همه، میتوان نشان داد h(a+bε)=h(a)+b×h′(a)ε؛ بنابراین محاسبه h(a+ε) در یک مرحله هم h(a) و هم مشتق h′(a) را به ما میدهد. شکل B-2 نشان میدهد مشتق جزئی ∂f/∂x(3,4) با اعداد دوگان چگونه محاسبه میشود. کافی است f(3+ε,4) را حساب کنیم؛ خروجی یک عدد دوگان است که مؤلفه نخست آن f(3,4) و مؤلفه دوم آن ∂f/∂x(3,4) است.
شکل B-2. مشتقگیری خودکار حالت روبهجلو با استفاده از اعداد دوگان
برای محاسبه ∂f/∂y(3,4) باید دوباره گراف را طی کنیم، اما اینبار با x=3 و y=4+ε.
پس حالت روبهجلو از تقریب تفاضل محدود بسیار دقیقتر است، اما وقتی تعداد ورودیها زیاد و تعداد خروجیها کم باشد، همان ضعف اصلی را دارد. اگر ۱۰۰۰ پارامتر وجود داشته باشد، برای محاسبه همه مشتقهای جزئی باید ۱۰۰۰ بار گراف طی شود. اینجاست که حالت معکوس برتری خود را نشان میدهد: همه آنها را تنها با دو بار پیمایش گراف محاسبه میکند.
Reverse-Mode Autodiff (مشتقگیری خودکار حالت معکوس)
حالت معکوس همان راهحلی است که TensorFlow پیادهسازی میکند. ابتدا گراف را در جهت روبهجلو، از ورودیها به خروجی، طی میکند تا مقدار هر گره محاسبه شود. سپس بار دوم گراف را در جهت معکوس، از خروجی به ورودیها، طی میکند تا همه مشتقهای جزئی بهدست آیند. نام «حالت معکوس» از همین پیمایش دوم گرفته شده است که در آن گرادیانها در جهت معکوس جریان مییابند.
شکل B-3 پیمایش دوم را نشان میدهد. در پیمایش نخست همه مقادیر گرهها با شروع از x=3 و y=4 محاسبه شدهاند؛ این مقادیر در پایین سمت راست هر گره دیده میشوند، برای نمونه x×x=9. برای وضوح، گرهها از n1 تا n7 نامگذاری شدهاند. گره خروجی n7 است و f(3,4)=n7=42.
شکل B-3. مشتقگیری خودکار حالت معکوس
ایده این است که بهتدریج در گراف پایین برویم و مشتق جزئی f(x,y) را نسبت به هر گره پیاپی محاسبه کنیم تا به گرههای متغیر برسیم. حالت معکوس برای این کار بهشدت به «قاعده زنجیرهای» متکی است.
Equation B-4 — Chain rule
∂f/∂x = (∂f/∂nᵢ) × (∂nᵢ/∂x)
چون n7 گره خروجی است و f=n7، پس ∂f/∂n7=1. اکنون به n5 میرویم. میزان تغییر f در برابر تغییر n5 برابر ∂f/∂n5 = ∂f/∂n7 × ∂n7/∂n5 است. میدانیم ∂f/∂n7=1 و چون n7=n5+n6، داریم ∂n7/∂n5=1؛ بنابراین ∂f/∂n5=1×1=1.
سپس به n4 میرسیم: ∂f/∂n4 = ∂f/∂n5 × ∂n5/∂n4. چون n5=n4×n2، داریم ∂n5/∂n4=n2، پس ∂f/∂n4 = 1×n2 = 4.
این فرایند تا پایین گراف ادامه مییابد. در پایان همه مشتقهای جزئی f(x,y) را در نقطه x=3 و y=4 محاسبه کردهایم: ∂f/∂x=24 و ∂f/∂y=10.
حالت معکوس روشی بسیار قدرتمند و دقیق است، بهویژه وقتی ورودیها بسیار و خروجیها کماند. برای هر خروجی فقط یک پیمایش روبهجلو و یک پیمایش معکوس لازم است تا همه مشتقهای جزئی نسبت به همه ورودیها محاسبه شوند. هنگام آموزش شبکه عصبی معمولاً تنها یک خروجی، یعنی مقدار تابع زیان، را کمینه میکنیم؛ در نتیجه برای محاسبه گرادیانها فقط دو پیمایش گراف لازم است. این روش همچنین میتواند با توابعی که همهجا مشتقپذیر نیستند کار کند، مشروط بر اینکه مشتق را در نقاط مشتقپذیر بخواهیم.
در شکل B-3 نتایج عددی در هر گره و در لحظه محاسبه شدهاند؛ اما TensorFlow دقیقاً این کار را انجام نمیدهد. در عوض یک گراف محاسباتی جدید میسازد؛ یعنی «مشتقگیری نمادین حالت معکوس» را پیادهسازی میکند. در نتیجه گراف محاسبه گرادیان زیان نسبت به همه پارامترهای شبکه فقط یکبار ساخته میشود و سپس هر بار که بهینهساز به گرادیانها نیاز دارد، بارها اجرا میشود. این روش امکان محاسبه مشتقهای مرتبه بالاتر را نیز فراهم میکند.