پس انتشار
اکنون که ایده ای در مورد نحوه اندازه گیری تأثیر متغیرها بر خروجی یک تابع داریم، می توانیم شروع به نوشتن کد برای محاسبه این مشتقات جزئی کنیم تا نقش آنها را در به حداقل رساندن تلفات مدل ببینیم. قبل از اعمال این در یک شبکه عصبی کامل، بیایید با یک پاس رو به جلو ساده شده تنها با یک نورون شروع کنیم. به جای پس انتشار از تابع از دست دادن برای یک شبکه عصبی کامل، بیایید تابع ReLU را برای یک نورون واحد پس انتشار کنیم و طوری عمل کنیم که گویی قصد داریم خروجی این نورون را به حداقل برسانیم. ما ابتدا این کار را فقط به عنوان نمایشی برای ساده کردن توضیح انجام می دهیم، زیرا به حداقل رساندن خروجی از یک نورون فعال شده ReLU هیچ هدفی جز یک تمرین ندارد. به حداقل رساندن مقدار ضرر هدف نهایی ما است، اما در این مورد، ما با نشان دادن اینکه چگونه می توانیم از قانون زنجیره با مشتقات و مشتقات جزئی برای محاسبه تأثیر هر متغیر بر خروجی فعال شده ReLU استفاده کنیم، شروع می کنیم. ما همچنین با به حداقل رساندن این خروجی اساسی تر قبل از پرش به شبکه کامل و از دست دادن کلی شروع خواهیم کرد.
بیایید به سرعت عملیات عبور رو به جلو و اتمی را که باید برای فعال سازی این نورون و ReLU انجام دهیم، به یاد بیاوریم. ما از یک نورون نمونه با 3 ورودی استفاده خواهیم کرد، به این معنی که دارای 3 وزن و یک بایاس نیز است:
x = [1.0, -2.0, 3.0] # input values
w = [-3.0, -1.0, 2.0] # weights
b = 1.0 # bias
سپس با اولین ورودی، x[0]، و وزن مربوطه، w[0]:
شکل 9.01: شروع یک پاس رو به جلو با اولین ورودی و وزن.
ما باید ورودی را در وزن ضرب کنیم:
x = [1.0, -2.0, 3.0] # input values
w = [-3.0, -1.0, 2.0] # weights
b = 1.0 # bias
xw0 = x[0] * w[0]
print(xw0)
>>>
-3.0
Visually:
شکل 9.02: اولین ورودی و ضرب وزن.
ما این عملیات را برای جفت های x1 ، w1 و x2 ، w2 تکرار می کنیم:
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
print(xw1, xw2)
>>>
2.0 6.0
Visually:
شکل 9.03: ضرب ورودی و وزن همه ورودی ها.
کدنویسی همه با هم:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن ها
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
print(xw0, xw1, xw2)
>>>
-3.0 2.0 6.0
عملیات بعدی که باید انجام شود مجموع تمام ورودی های وزنی با بایاس است:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
print(xw0, xw1, xw2, b)
# افزودن ورودی های وزنی و تعصب
z = xw0 + xw1 + xw2 + b
print(z)
>>>
-3.0 2.0 6.0 1.0
6.0
شکل 9.04: ورودی های وزنی و افزودن بایاس.
این خروجی نورون را تشکیل می دهد. آخرین مرحله اعمال تابع فعال سازی ReLU بر روی این خروجی است:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن ها
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
print(xw0, xw1, xw2, b)
# افزودن ورودی های وزنی و bias
z = xw0 + xw1 + xw2 + b
print(z)
# عملکرد فعال سازی ReLU
y = max(z, 0)
print(y)
>>>
-3.0 2.0 6.0 1.0
6.0
6.0
شکل 9.05: فعال سازی ReLU به خروجی نورون اعمال می شود.
این عبور کامل به جلو از یک نورون و یک عملکرد فعال سازی ReLU است. بیایید همه این توابع زنجیره ای را به عنوان یک تابع بزرگ در نظر بگیریم که مقادیر ورودی (x)، وزن ها (w) و bias (b), به عنوان ورودی ها و خروجی های Y. این تابع بزرگ شامل چندین تابع ساده تر است – ضرب مقادیر و وزن های ورودی، مجموع این مقادیر و بایاس و همچنین یک تابع حداکثر به عنوان فعال سازی ReLU وجود دارد – در مجموع 3 تابع زنجیره ای:
اولین قدم این است که گرادیان های خود را با محاسبه مشتقات و مشتقات جزئی با توجه به هر یک از پارامترها و ورودی های خود پس انتشار کنیم. برای انجام این کار، ما می خواهیم از قانون زنجیره استفاده کنیم. به یاد بیاورید که قانون زنجیره ای برای یک تابع تصریح می کند که مشتق توابع تو در تو مانند f(g(x)) حل می شود:
این عملکرد بزرگی که به آن اشاره کردیم می تواند در زمینه شبکه عصبی ما به صورت آزاد تفسیر شود:
یا به شکلی که با کد با دقت بیشتری مطابقت دارد:
وظیفه فعلی ما محاسبه این است که هر یک از ورودی ها، وزن ها و سوگیری چقدر بر خروجی تأثیر می گذارد. ما با در نظر گرفتن آنچه باید برای مشتق جزئی w0 محاسبه کنیم، شروع می کنیم. اما ابتدا ، بیایید معادله خود را به شکلی بازنویسی کنیم که به ما امکان می دهد نحوه محاسبه مشتقات را راحت تر تعیین کنیم:
y = ReLU(sum(mul(x0, w0), mul(x1, w1), mul(x2, w2), b))
معادله فوق شامل 3 تابع تو در تو است: ReLU، مجموع ورودی های وزنی و بایاس و ضرب ورودی ها و وزن ها. برای محاسبه تأثیر وزن مثال، w0، بر خروجی، قانون زنجیره به ما می گوید که مشتق [1] ReLU را با توجه به پارامتر آن، که مجموع است، محاسبه کنیم، سپس آن را با مشتق جزئی عملیات مجموع با توجه به mul آن ضرب کنیم.
ورودی (x0, w0) ، زیرا این ورودی شامل پارامتر مورد نظر است. سپس، این را با مشتق جزئی عملیات ضرب با توجه به ورودی x0 ضرب کنید. بیایید این را در یک معادله ساده ببینیم:
For legibility, we did not denote the ReLU() parameter, which is the full sum, and the sum parameters, which are all of the multiplications of inputs and weights. We excluded this because the equation would be longer and harder to read. This equation shows that we have to calculate the derivatives and partial derivatives of all of the atomic operations and multiply them to acquire تاثیری که x0 بر خروجی می گذارد. سپس می توانیم این را تکرار کنیم تا تمام تأثیرات باقی مانده را محاسبه کنیم. مشتقات با توجه به وزن ها و سوگیری ما را در مورد تأثیر آنها مطلع می کنند و برای به روز رسانی این وزن ها و سوگیری ها استفاده می شوند. مشتقات با توجه به ورودی ها برای زنجیره بندی لایه های بیشتر با انتقال آنها به تابع قبلی در زنجیره استفاده می شوند.
ما چندین لایه زنجیره ای از نورون ها را در مدل شبکه عصبی خواهیم داشت و به دنبال آن تابع از دست دادن وجود دارد. ما می خواهیم تاثیر یک وزن یا سوگیری معین را بر کاهش بدانیم. این بدان معناست که ما باید مشتق تابع ضرر را محاسبه کنیم (که بعدا در این فصل انجام خواهیم داد) و قانون زنجیره را با مشتقات تمام توابع فعال سازی و نورون ها در تمام لایه های متوالی اعمال کنیم. مشتق با توجه به ورودی های لایه، در مقابل مشتق با توجه به وزن ها و اریب، برای به روز رسانی هیچ پارامتری استفاده نمی شود. در عوض، برای زنجیر کردن به لایه دیگری استفاده می شود (به همین دلیل است که ما به لایه قبلی در یک زنجیره بازنشر می کنیم).
در طول پاس به عقب، مشتق تابع ضرر را محاسبه می کنیم و از آن برای ضرب با مشتق تابع فعال سازی لایه خروجی استفاده می کنیم، سپس از این نتیجه برای ضرب در مشتق لایه خروجی و غیره استفاده می کنیم، از طریق تمام لایه های پنهان و توابع فعال سازی. در داخل این لایه ها، مشتق با توجه به وزن ها و بایاس ها گرادیان هایی را تشکیل می دهد که برای به روز رسانی وزن ها و بایاس ها استفاده خواهیم کرد. مشتقات با توجه به ورودی ها گرادیان زنجیره ای را با لایه قبلی تشکیل می دهند., این لایه می تواند تأثیر وزن ها و بایاس های آن را بر افت و گرادیان های پس انتشار بر ورودی ها بیشتر محاسبه کند.
برای این مثال، بیایید فرض کنیم که نورون ما شیب 1 را از لایه بعدی دریافت می کند. ما این مقدار را برای اهداف نمایشی می سازیم و مقدار 1 مقادیر را تغییر نمی دهد، به این معنی که ما می توانیم راحت تر همه فرآیندها را نشان دهیم. ما قصد داریم از رنگ قرمز برای مشتقات استفاده کنیم:
شکل 9.06: گرادیان اولیه (دریافت شده در طول پس انتشار).
به یاد بیاورید که مشتق ReLU() با توجه به ورودی آن 1 است، اگر ورودی بزرگتر از 0 باشد، و در غیر این صورت 0 است:
ما می توانیم آن را در پایتون به صورت زیر بنویسیم:
relu_dz = (1. if z > 0 else 0.)
جایی که drelu_dz به معنای مشتق تابع ReLU با توجه به z است – ما از z به جای x از معادله استفاده کردیم زیرا معادله به طور کلی تابع حداکثر را نشان می دهد و ما آن را برای خروجی نورون که z است اعمال می کنیم.
مقدار ورودی تابع ReLU 6 است، بنابراین مشتق برابر با 1 است. ما باید از قانون زنجیره استفاده کنیم و این مشتق را با مشتق دریافتی از لایه بعدی ضرب کنیم ، که برای هدف این مثال 1 است:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن ها
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
# افزودن ورودی های وزنی و bias
z = xw0 + xw1 + xw2 + b
# عملکرد فعال سازی ReLU
y = max(z, 0)
# Backward pass
# مشتق از لایه بعدی
dvalue = 1.0
# مشتق ReLU و قانون زنجیره ای
drelu_dz = dvalue * (1. if z > 0 else 0.)
print(drelu_dz)
>>>
1.0
شکل 9.07: مشتق تابع ReLU و قانون زنجیره ای.
این نتیجه با مشتق 1 است:
شکل 9.08: ReLU و شیب قانون زنجیره ای.
با حرکت به عقب از طریق شبکه عصبی ما، عملکردی که بلافاصله قبل از انجام تابع فعال سازی ایجاد می شود چیست؟
این مجموع ورودی های وزنی و تعصب است. این بدان معنی است که ما می خواهیم مشتق جزئی تابع جمع را محاسبه کنیم و سپس با استفاده از قانون زنجیره ای آن را در مشتق جزئی تابع بعدی و بیرونی که ReLU است ضرب کنیم. ما این نتایج را موارد زیر می نامیم:
drelu_dxw0 – مشتق جزئی ReLU w.r.t. اولین ورودی توزین، w0x0,
drelu_dxw1 – مشتق جزئی ReLU w.r.t. ورودی وزن دوم، w1x1,
drelu_dxw2 – مشتق جزئی ReLU w.r.t. سومین ورودی توزین شده، w2x2,
drelu_db – مشتق جزئی ReLU با توجه به bias b،
مشتق جزئی عملیات مجموع همیشه 1 است، بدون توجه به ورودی ها:
ورودی های وزنی و سوگیری در این مرحله جمع بندی می شوند. بنابراین مشتقات جزئی عملیات مجموع را با توجه به هر یک از اینها محاسبه خواهیم کرد، ضرب در مشتق جزئی برای تابع بعدی (با استفاده از قانون زنجیره)، که تابع ReLU است که با drelu_dz نشان داده می شود.
برای اولین مشتق جزئی:
dsum_dxw0 = 1
drelu_dxw0 = drelu_dz * dsum_dxw0
برای روشن بودن، dsum_dxw0 بالا به معنای مشتق جزئی مجموع با توجه به x (ورودی)، وزن دار، برای جفت ورودی ها و وزن های 0 است. 1 مقدار این مشتق جزئی است که با استفاده از قانون زنجیره ای با مشتق تابع بعدی که تابع ReLU است ضرب می کنیم.
باز هم ، ما باید قانون زنجیره را اعمال کنیم و مشتق تابع ReLU را با مشتق جزئی جمع ، با توجه به اولین ورودی وزنی ضرب کنیم:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن ها
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
# افزودن ورودی های وزنی و bias
z = xw0 + xw1 + xw2 + b
# عملکرد فعال سازی ReLU
y = max(z, 0)
# Backward pass
# مشتق از لایه بعدی
dvalue = 1.0
# مشتق ReLU و قانون زنجیره ای
drelu_dz = dvalue * (1. if z > 0 else 0.)
print(drelu_dz)
# مشتقات جزئی ضرب، قانون زنجیره ای
dsum_dxw0 = 1
drelu_dxw0 = drelu_dz * dsum_dxw0
print(drelu_dxw0)
>>>
1.0
1.0
شکل 9.09: مشتق جزئی تابع مجموع با اولین ورودی وزنی ؛ قانون زنجیره ای.
این نتیجه با مشتق جزئی 1 دوباره:
شکل 9.10: گرادیان مجموع و قانون زنجیره ای (برای اولین ورودی وزنی شده).
سپس می توانیم همان عملیات را با ورودی توزین شده بعدی انجام دهیم:
dsum_dxw1 = 1
drelu_dxw1 = drelu_dz * dsum_dxw1
شکل 9.11: مشتق جزئی تابع مجموع با ورودی وزنی دوم؛ قانون زنجیره ای.
که با مشتق جزئی محاسبه شده بعدی نتیجه می گیرد:
شکل 9.12: گرادیان مجموع و قانون زنجیره ای (برای ورودی وزنی دوم).
و آخرین ورودی وزن:
dsum_dxw2 = 1
drelu_dxw2 = drelu_dz * dsum_dxw2
شکل 9.13: مشتق جزئی تابع مجموع با سومین ورودی وزنی ؛ قانون زنجیره ای.
شکل 9.14: گرادیان مجموع و قانون زنجیره (برای سومین ورودی وزنی شده).
سپس bias :
dsum_db = 1
drelu_db = drelu_dz * dsum_db
شکل 9.15: مشتق جزئی تابع مجموع w.r.t. بایاس؛ قانون زنجیره ای.
شکل 9.16: گرادیان مجموع و قانون زنجیره برای bias
بیایید این مشتقات جزئی را با قانون زنجیره اعمال شده به کد خود اضافه کنیم:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن ها
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
# افزودن ورودی های وزنی و bias
z = xw0 + xw1 + xw2 + b
# عملکرد فعال سازی ReLU
y = max(z, 0)
# Backward pass
# مشتق از لایه بعدی
dvalue = 1.0
# مشتق ReLU و قانون زنجیره ای
drelu_dz = dvalue * (1. if z > 0 else 0.)
print(drelu_dz)
# مشتقات جزئی ضرب، قانون زنجیره ای
dsum_dxw0 = 1
dsum_dxw1 = 1
dsum_dxw2 = 1
dsum_db = 1
drelu_dxw0 = drelu_dz * dsum_dxw0
drelu_dxw1 = drelu_dz * dsum_dxw1
drelu_dxw2 = drelu_dz * dsum_dxw2
drelu_db = drelu_dz * dsum_db
print(drelu_dxw0, drelu_dxw1, drelu_dxw2, drelu_db)
>>>
1.0
1.0 1.0 1.0 1.0
در ادامه به عقب، تابعی که قبل از مجموع می آید، ضرب وزن ها و ورودی ها است. مشتق یک محصول هر چیزی است که ورودی در آن ضرب می شود. فراخوان:
مشتق جزئی f نسبت به x برابر با y است. مشتق جزئی f با توجه به y برابر با x است. با پیروی از این قانون، مشتق جزئی اولین ورودی وزنی با توجه به ورودی برابر با وزن است (ورودی دیگر این تابع). سپس، ما باید قانون زنجیره ای را اعمال کنیم و این مشتق جزئی را با مشتق جزئی تابع بعدی ضرب کنیم، که مجموع است (ما مشتق جزئی آن را قبلا در این فصل محاسبه کردیم):
dmul_dx0 = w[0]
drelu_dx0 = drelu_dxw0 * dmul_dx0
این بدان معنی است که ما مشتق جزئی را با توجه به ورودی x0 محاسبه می کنیم که مقدار آن w0 است و قانون زنجیره ای را با مشتق تابع بعدی که drelu_dxw0 است اعمال می کنیم.
این زمان خوبی است که به این نکته اشاره کنیم که همانطور که قانون زنجیره را به این روش اعمال می کنیم – با گرفتن مشتق ReLU()، گرفتن مشتق عملیات جمع، ضرب هر دو و غیره، این فرآیندی است که با استفاده از قانون زنجیره ای به آن پس انتشار می گویند. همانطور که از نام آن پیداست، شیب های تابع خروجی حاصل از طریق شبکه عصبی با استفاده از ضرب گرادیان توابع بعدی از لایه های بعدی با لایه فعلی منتقل می شوند. بیایید این مشتق جزئی را به کد اضافه کرده و در نمودار نشان دهیم:
# Forward pass
x = [1.0, -2.0, 3.0] # input values
w = [-3.0, -1.0, 2.0] # weights
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
# افزودن ورودی های وزنی و bias
z = xw0 + xw1 + xw2 + b
# عملکرد فعال سازی ReLU
y = max(z, 0)
# Backward pass
# مشتق از لایه بعدی
dvalue = 1.0
# مشتق ReLU و قانون زنجیره ای
drelu_dz = dvalue * (1. if z > 0 else 0.)
print(drelu_dz)
# مشتقات جزئی ضرب، قانون زنجیره ای
dsum_dxw0 = 1
dsum_dxw1 = 1
dsum_dxw2 = 1
dsum_db = 1
drelu_dxw0 = drelu_dz * dsum_dxw0
drelu_dxw1 = drelu_dz * dsum_dxw1
drelu_dxw2 = drelu_dz * dsum_dxw2
drelu_db = drelu_dz * dsum_db
print(drelu_dxw0, drelu_dxw1, drelu_dxw2, drelu_db)
# مشتقات جزئی ضرب، قانون زنجیره ای
dmul_dx0 = w[0]
drelu_dx0 = drelu_dxw0 * dmul_dx0
print(drelu_dx0)
>>>
1.0
1.0 1.0 1.0 1.0
-3.0
شکل 9.17: مشتق جزئی تابع ضرب با اولین ورودی؛ قانون زنجیره ای.
شکل 9.18: شیب ضرب و قانون زنجیره (برای اولین ورودی).
ما همین عملیات را برای سایر ورودی ها و وزن ها انجام می دهیم:
# Forward pass
x = [1.0, -2.0, 3.0] # مقادیر ورودی
w = [-3.0, -1.0, 2.0] # وزن
b = 1.0 # bias
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
# افزودن ورودی های وزنی و bias
z = xw0 + xw1 + xw2 + b
# عملکرد فعال سازی ReLU
y = max(z, 0)
# Backward pass
# مشتق از لایه بعدی
dvalue = 1.0
# مشتق ReLU و قانون زنجیره ای
drelu_dz = dvalue * (1. if z > 0 else 0.)
print(drelu_dz)
# مشتقات جزئی ضرب، قانون زنجیره ای
dsum_dxw0 = 1
dsum_dxw1 = 1
dsum_dxw2 = 1
dsum_db = 1
drelu_dxw0 = drelu_dz * dsum_dxw0
drelu_dxw1 = drelu_dz * dsum_dxw1
drelu_dxw2 = drelu_dz * dsum_dxw2
drelu_db = drelu_dz * dsum_db
print(drelu_dxw0, drelu_dxw1, drelu_dxw2, drelu_db)
# مشتقات جزئی ضرب، قانون زنجیره ای
dmul_dx0 = w[0]
dmul_dx1 = w[1]
dmul_dx2 = w[2]
dmul_dw0 = x[0]
dmul_dw1 = x[1]
dmul_dw2 = x[2]
drelu_dx0 = drelu_dxw0 * dmul_dx0
drelu_dw0 = drelu_dxw0 * dmul_dw0
drelu_dx1 = drelu_dxw1 * dmul_dx1
drelu_dw1 = drelu_dxw1 * dmul_dw1
drelu_dx2 = drelu_dxw2 * dmul_dx2
drelu_dw2 = drelu_dxw2 * dmul_dw2
print(drelu_dx0, drelu_dw0, drelu_dx1, drelu_dw1, drelu_dx2, drelu_dw2)
>>>
1.0
1.0 1.0 1.0 1.0
-3.0 1.0 -1.0 -2.0 2.0 3.0
شکل 9.19: نمودار پس انتشار کامل.
Anim 9.01-9.19: https://nnfs.io/pro
این مجموعه کاملی از مشتقات جزئی نورون فعال شده با توجه به ورودی ها، وزن ها و تعصب است.
معادله ابتدای این فصل را به یاد بیاورید:
از آنجایی که ما کد کاملی داریم و قانون زنجیره ای را از این معادله اعمال می کنیم، بیایید ببینیم چه چیزی را می توانیم در این محاسبات بهینه کنیم. ما قانون زنجیره ای را برای محاسبه مشتق جزئی تابع فعال سازی ReLU با توجه به اولین ورودی، x0 اعمال کردیم. در کد ما ، بیایید خطوط مربوط به کد را بگیریم و آنها را ساده کنیم:
drelu_dx0 = drelu_dxw0 * dmul_dx0
where:
dmul_dx0 = w[0]
then:
drelu_dx0 = drelu_dxw0 * w[0]
where:
drelu_dxw0 = drelu_dz * dsum_dxw0
then:
drelu_dx0 = drelu_dz * dsum_dxw0 * w[0]
where:
dsum_dxw0 = 1
then:
drelu_dx0 = drelu_dz * 1 * w[0] = drelu_dz * w[0]
where:
drelu_dz = dvalue * (1. if z > 0 else 0.)
then:
drelu_dx0 = dvalue * (1. if z > 0 else 0.) * w[0]
شکل 9.20: نحوه اعمال قانون زنجیره برای مشتق جزئی ورودی اول ReLU w.r.t.
شکل 9.21: قانون زنجیره ای برای مشتق جزئی ReLU w.r.t. اولین ورودی اعمال می شود
Anim 9.20-9.21: https://nnfs.io/com
در این معادله، از سمت چپ شروع می شود، مشتقی است که در لایه بعدی محاسبه می شود، با توجه به ورودی های آن – این گرادیان پس انتشار به لایه فعلی است، که مشتق تابع ReLU است، و مشتق جزئی تابع نورون با توجه به ورودی x0. همه اینها با استفاده از قانون زنجیره ای برای محاسبه تأثیر ورودی به نورون بر خروجی کل تابع ضرب می شود.
مشتق جزئی عملکرد نورون ، با توجه به وزن ، ورودی مربوط به این وزن است و با توجه به ورودی ، وزن مربوطه است. مشتق جزئی عملکرد نورون با توجه به سوگیری همیشه 1 است. ما آنها را با مشتق تابع بعدی (که در این مثال 1 بود) ضرب می کنیم تا مشتقات نهایی را بدست آوریم. ما قصد داریم همه این مشتقات را در کلاس لایه Dense و کلاس فعال سازی ReLU را برای مرحله پس انتشار کدنویسی کنیم.
در مجموع ، مشتقات جزئی بالا ، با هم ترکیب شده در یک بردار ، شیب های ما را تشکیل می دهند. شیب های ما را می توان به صورت زیر نشان داد:
dx = [drelu_dx0, drelu_dx1, drelu_dx2] # شیب روِ ورودی ها
dw = [drelu_dw0, drelu_dw1, drelu_dw2] # شیب روی وزن ها
db = drelu_db # bias گرادیا روی
برای این مثال تک نورون ، ما نیز به dx خود نیاز نخواهیم داشت. با بسیاری از لایه ها، ما به پس انتشار به لایه های قبلی با مشتق جزئی با توجه به ورودی های خود ادامه خواهیم داد.در ادامه مثال تک نورون، اکنون می توانیم این شیب ها را روی وزن ها اعمال کنیم تا امیدواریم خروجی را به حداقل برسانیم. این معمولا هدف بهینه ساز است (در فصل بعدی مورد بحث قرار می گیرد)، اما می توانیم با اعمال مستقیم کسر منفی گرادیان به وزن های خود، نسخه ساده شده ای از این کار را نشان دهیم. ما یک کسر منفی را به این گرادیان اعمال می کنیم زیرا می خواهیم مقدار خروجی نهایی را کاهش دهیم و گرادیان جهت تندترین صعود را نشان می دهد. به عنوان مثال، وزن ها و biases فعلی ما عبارتند از:
print(w, b)
>>>
[-3.0, -1.0, 2.0] 1.0
سپس می توانیم کسری از گرادیان ها را برای این مقادیر اعمال کنیم:
w[0] += -0.001 * dw[0]
w[1] += -0.001 * dw[1]
w[2] += -0.001 * dw[2]
b += -0.001 * db
print(w, b)
>>>
[-3.001, -0.998, 1.997] 0.999
اکنون، ما وزن ها و بایاس ها را کمی تغییر داده ایم به گونه ای که خروجی را تا حدودی هوشمندانه کاهش دهیم. ما می توانیم با انجام یک پاس رو به جلو دیگر اثرات ترفندهای خود را بر روی خروجی ببینیم:
# ضرب ورودی ها در وزن
xw0 = x[0] * w[0]
xw1 = x[1] * w[1]
xw2 = x[2] * w[2]
# اضافه کردن
z = xw0 + xw1 + xw2 + b
# عملکرد فعال سازی ReLU
y = max(z, 0)
print(y)
>>>
5.985
ما با موفقیت خروجی این نورون را از 6.000 به 5.985 کاهش داده ایم. توجه داشته باشید که کاهش خروجی نورون در یک شبکه عصبی واقعی منطقی نیست. ما این کار را صرفا به عنوان یک تمرین ساده تر از شبکه کامل انجام می دادیم. ما می خواهیم مقدار ضرر را کاهش دهیم، که آخرین محاسبه در زنجیره محاسبات در طول پاس رو به جلو است، و اولین محاسبه ای است که گرادیان را در طول پس انتشار محاسبه می کند. ما خروجی ReLU یک نورون را فقط برای هدف این مثال به حداقل رسانده ایم تا نشان دهیم که در واقع توانسته ایم ارزش توابع زنجیره ای را به طور هوشمندانه با استفاده از مشتقات، مشتقات جزئی و قانون زنجیره کاهش دهیم. اکنون، مثال یک نورون را در لیست نمونه ها اعمال می کنیم و آن را به یک لایه کامل از نورون ها گسترش می دهیم. برای شروع، بیایید لیستی از 3 نمونه را برای ورودی تنظیم کنیم، که در آن هر نمونه از 4 ویژگی تشکیل شده است. برای این مثال، شبکه ما از یک لایه پنهان تشکیل شده است که شامل 3 نورون (لیست 3 مجموعه وزن و 3 سوگیری) است. ما قصد نداریم دوباره پاس رو به جلو را توصیف کنیم، اما پاس عقب، در این مورد، نیاز به توضیح بیشتری دارد.
تا کنون، ما یک مثال عبور به عقب را با یک نورون واحد انجام داده ایم که یک مشتق منفرد برای اعمال قانون زنجیره دریافت کرده است. بیایید چندین نورون را در لایه زیر در نظر بگیریم. یک نورون از لایه فعلی به همه آنها متصل می شود – همه آنها خروجی این نورون را دریافت می کنند. در طول پس انتشار چه اتفاقی خواهد افتاد؟ هر نورون از لایه بعدی مشتق جزئی از عملکرد خود را با توجه به این ورودی برمی گرداند. نورون در لایه فعلی بردار متشکل از این مشتقات را دریافت می کند. ما به این نیاز داریم که یک مقدار منحصر به فرد برای یک نورون منفرد باشد. برای ادامه پس انتشار، باید این بردار را جمع کنیم.
حال ، بیایید نورون مفرد فعلی را با لایه ای از نورون ها جایگزین کنیم. برخلاف یک نورون واحد، یک لایه به جای یک مقدار مفرد، بردار مقادیر را خروجی می دهد. هر نورون در یک لایه به تمام نورون های لایه بعدی متصل می شود. در طول پس انتشار ، هر نورون از لایه فعلی بردار مشتقات جزئی را به همان روشی که برای یک نورون توصیف کردیم ، دریافت می کند. با لایه ای از نورون ها، به شکل لیستی از این بردارها یا یک آرایه دو بعدی خواهد بود. ما می دانیم که باید یک جمع را انجام دهیم، اما چه چیزی را باید جمع کنیم و نتیجه آن چه خواهد بود؟ هر نورون قصد دارد یک شیب از مشتقات جزئی را با توجه به تمام ورودی های خود خروجی دهد و همه نورون ها لیستی از این بردارها را تشکیل می دهند. ما باید ورودی ها را جمع کنیم – اولین ورودی به همه نورون ها، ورودی دوم و غیره. ما باید ستون ها را جمع کنیم.
برای محاسبه مشتقات جزئی با توجه به ورودی ها، به وزن ها نیاز داریم – مشتق جزئی با توجه به ورودی برابر با وزن مربوطه است. این بدان معنی است که آرایه مشتقات جزئی با توجه به همه ورودی ها برابر با آرایه وزن ها است. از آنجایی که این آرایه جابجا شده است، باید ردیف های آن را به جای ستون ها جمع کنیم. برای اعمال قانون زنجیره ای، باید آنها را در گرادیان تابع بعدی ضرب کنیم.
در کد برای نشان دادن این موضوع، وزن های جابجا شده را که آرایه انتقال یافته مشتقات با توجه به ورودی ها هستند، می گیریم و آنها را در شیب های مربوطه (مربوط به نورون های داده شده) ضرب می کنیم تا قانون زنجیره اعمال شود. سپس همراه با ورودی ها جمع می کنیم. سپس گرادیان لایه بعدی را در پس انتشار محاسبه می کنیم. لایه “بعدی” در پس انتشار لایه قبلی به ترتیب ایجاد مدل است:
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# بردار 1 ثانیه
dvalues = np.array([[1., 1., 1.]])
# ما 3 مجموعه وزن داریم – یک مجموعه برای هر نورون
# ما 4 ورودی داریم، بنابراین 4 وزن
# به یاد بیاورید که ما وزنه ها را جابجا می کنیم
weights = np.array([[0.2, 0.8, -0.5, 1],
[0.5, -0.91, 0.26, -0.5],
[-0.26, -0.27, 0.17, 0.87]]).T
# مجموع وزن ورودی داده شده
# و در شیب عبور شده برای این نورون ضرب کنید
dx0 = sum(weights[0])*dvalues[0]
dx1 = sum(weights[1])*dvalues[0]
dx2 = sum(weights[2])*dvalues[0]
dx3 = sum(weights[3])*dvalues[0]
dinputs = np.array([dx0, dx1, dx2, dx3])
print(dinputs)
>>>
[ 0.44 -0.38 -0.07 1.37]
DINPUT یک شیب از عملکرد نورون با توجه به ورودی ها است.
ما گرادیان تابع بعدی (dvalues) را به عنوان یک بردار ردیف تعریف کردیم که به زودی توضیح خواهیم داد. از دیدگاه NumPy و از آنجایی که هر دو وزن و dvalues آرایه های NumPy هستند، می توانیم محاسبه dx0 تا dx3 را ساده کنیم. از آنجا که آرایه وزن ها به گونه ای قالب بندی شده اند که ردیف ها حاوی وزن های مربوط به هر ورودی هستند (وزن برای همه نورون ها برای ورودی داده شده) ، می توانیم آنها را مستقیما در بردار گرادیان ضرب کنیم:
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# بردار 1 ثانیه
dvalues = np.array([[1., 1., 1.]])
# ما 3 مجموعه وزن داریم – یک مجموعه برای هر نورون
# ما 4 ورودی داریم، بنابراین 4 وزن
# به یاد بیاورید که ما وزنه ها را جابجا می کنیم
weights = np.array([[0.2, 0.8, -0.5, 1],
[0.5, -0.91, 0.26, -0.5],
[-0.26, -0.27, 0.17, 0.87]]).T
# مجموع وزن ورودی داده شده
# و در شیب عبور شده برای این نورون ضرب کنید
dx0 = sum(weights[0]*dvalues[0])
dx1 = sum(weights[1]*dvalues[0])
dx2 = sum(weights[2]*dvalues[0])
dx3 = sum(weights[3]*dvalues[0])
dinputs = np.array([dx0, dx1, dx2, dx3])
print(dinputs)
>>>
[ 0.44 -0.38 -0.07 1.37]
ممکن است قبلا ببینید که ما با این کار به کجا می رویم – مجموع ضرب عناصر، حاصل ضرب نقطه است. ما می توانیم با استفاده از تابع np.dot به همان نتیجه برسیم. برای اینکه این امکان وجود داشته باشد، باید اشکال “درونی” را مطابقت دهیم و اولین بعد نتیجه را که اولین بعد پارامتر اول است، تعیین کنیم. ما می خواهیم خروجی این محاسبه به شکل گرادیان از تابع بعدی باشد – به یاد بیاورید که ما یک مشتق جزئی برای هر نورون داریم و آن را در مشتق جزئی نورون با توجه به ورودی آن ضرب می کنیم. سپس می خواهیم هر یک از این شیب ها را با هر یک از مشتقات جزئی مربوط به ورودی های این نورون ضرب کنیم و قبلا متوجه شدیم که آنها ردیف هستند. حاصل ضرب نقطه ای ردیف هایی را از آرگومان اول و ستون ها را از آرگومان دوم می گیرد تا ضرب و جمع را انجام دهد. بنابراین، باید وزن ها را برای این محاسبه جابجا کنیم:
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# بردار 1 ثانیه
dvalues = np.array([[1., 1., 1.]])
# ما 3 مجموعه وزن داریم – یک مجموعه برای هر نورون
# ما 4 ورودی داریم، بنابراین 4 وزن
# به یاد بیاورید که ما وزنه ها را جابجا می کنیم
weights = np.array([[0.2, 0.8, -0.5, 1],
[0.5, -0.91, 0.26, -0.5],
[-0.26, -0.27, 0.17, 0.87]]).T
# مجموع وزن ورودی داده شده
# و در شیب عبور شده برای این نورون ضرب کنید
dinputs = np.dot(dvalues[0], weights.T)
print(dinputs)
>>>
[ 0.44 -0.38 -0.07 1.37]
ما باید یک چیز دیگر را در نظر بگیریم – دسته ای از نمونه ها. تا کنون، ما از یک نمونه واحد استفاده کرده ایم که مسئول یک بردار گرادیان منفرد است که بین لایه ها پس انتشار می شود. بردار ردیفی که ما برای dvalues ایجاد کردیم در حال آماده سازی برای دسته ای از داده ها است. با نمونه های بیشتر، لایه لیستی از گرادیان ها را برمی گرداند که ما تقریبا به درستی از آنها استفاده می کنیم. بیایید گرادیان مفرد dvalues [0] را با لیست کاملی از گرادیان ها، dvalues جایگزین کنیم و گرادیان های نمونه بیشتری را به این لیست اضافه کنیم:
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# آرایه ای از مقادیر گرادیان افزایشی
dvalues = np.array([[1., 1., 1.],
[2., 2., 2.],
[3., 3., 3.]])
# ما 3 مجموعه وزن داریم – یک مجموعه برای هر نورون
# ما 4 ورودی داریم، بنابراین 4 وزن
# به یاد بیاورید که ما وزنه ها را جابجا می کنیم
weights = np.array([[0.2, 0.8, -0.5, 1],
[0.5, -0.91, 0.26, -0.5],
[-0.26, -0.27, 0.17, 0.87]]).T
# مجموع وزن ورودی داده شده
# و در شیب عبور شده برای این نورون ضرب کنید
dinputs = np.dot(dvalues, weights.T)
print(dinputs)
>>>
[[ 0.44 -0.38 -0.07 1.37]
[ 0.88 -0.76 -0.14 2.74]
[ 1.32 -1.14 -0.21 4.11]]
محاسبه گرادیان ها با توجه به وزن ها بسیار شبیه به هم است، اما در این مورد، ما از گرادیان ها برای به روز رسانی وزن ها استفاده می کنیم، بنابراین باید شکل وزن ها را مطابقت دهیم، نه ورودی ها. از آنجا که مشتق با توجه به وزن ها برابر با ورودی ها است ، وزن ها جابجا می شوند ، بنابراین برای دریافت مشتق نورون با توجه به وزن ها باید ورودی ها را جابجا کنیم. سپس از این ورودی های جابجا شده به عنوان اولین پارامتر حاصل ضرب نقطه استفاده می کنیم – حاصل ضرب نقطه ای قرار است ردیف ها را در ورودی ها ضرب کند، جایی که هر ردیف، همانطور که جابجا می شود، حاوی داده هایی برای یک ورودی معین برای همه نمونه ها، با ستون های dvalues است. این ستون ها مربوط به خروجی های نورون های منفرد برای همه نمونه ها هستند، بنابراین نتیجه شامل آرایه ای با شکل وزن ها خواهد بود که حاوی شیب ها نسبت به ورودی ها است که با گرادیان ورودی برای همه نمونه های دسته ضرب می شود:
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# آرایه ای از مقادیر گرادیان افزایشی
dvalues = np.array([[1., 1., 1.],
[2., 2., 2.],
[3., 3., 3.]])
# ما 3 مجموعه ورودی – نمونه داریم
inputs = np.array([[1, 2, 3, 2.5],
[2., 5., -1., 2],
[-1.5, 2.7, 3.3, -0.8]])
# مجموع وزن ورودی داده شده
# و در شیب عبور شده برای این نورون ضرب کنید
dweights = np.dot(inputs.T, dvalues)
print(dweights)
>>>
[[ 0.5 0.5 0.5]
[20.1 20.1 20.1]
[10.9 10.9 10.9]
[ 4.1 4.1 4.1]]
شکل این خروجی با شکل وزنه ها مطابقت دارد زیرا ورودی های هر وزن را جمع می کنیم و سپس آنها را در گرادیان ورودی ضرب می کنیم. Dweights یک گرادیان از عملکرد نورون با توجه به وزن است.
برای سوگیری ها و مشتقات با توجه به آنها ، مشتقات از عملیات جمع می آیند و همیشه برابر با 1 هستند ، ضرب در شیب های ورودی برای اعمال قانون زنجیره ای از آنجا که گرادیان ها لیستی از گرادیان ها هستند (بردار شیب برای هر نورون برای همه نمونه ها) ، فقط باید آنها را با نورون ها ، از نظر ستون ، در امتداد محور 0 جمع کنیم.
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# آرایه ای از مقادیر گرادیان افزایشی
dvalues = np.array([[1., 1., 1.],
[2., 2., 2.],
[3., 3., 3.]])
# یک تعصب برای هر نورون
# بایاس ها بردار ردیف با شکل (1 ، نورون ها) هستند
biases = np.array([[2, 3, 0.5]])
# dbiases – مقادیر مجموع، این کار را روی نمونه ها (محور اول)، keepdims انجام دهید
# از آنجایی که این به طور پیش فرض یک لیست ساده تولید می کند –
# ما این را در فصل 4 توضیح دادیم
dbiases = np.sum(dvalues, axis=0, keepdims=True)
print(dbiases)
>>>
[[6. 6. 6.]]
KeepDims به ما امکان می دهد گرادیان را به عنوان یک بردار ردیف نگه داریم – آرایه شکل بایاس ها را به یاد بیاوریم.
آخرین چیزی که در اینجا باید به آن پرداخت، مشتق تابع ReLU است. اگر ورودی بزرگتر از 0 باشد برابر با 1 و در غیر این صورت 0 است. لایه خروجی های خود را از طریق فعال سازی ReLU() در طول عبور رو به جلو منتقل می کند. برای پاس به عقب، ReLU() یک گرادیان از همان شکل دریافت می کند. مشتق تابع ReLU آرایه ای به همان شکل را تشکیل می دهد که وقتی ورودی مربوطه بزرگتر از 0 باشد با 1 پر می شود و در غیر این صورت 0 پر می شود. برای اعمال قانون زنجیره ، باید این آرایه را با گرادیان های تابع زیر ضرب کنیم:
import numpy as np
# خروجی لایه نمونه
z = np.array([[1, 2, -3, -4],
[2, -7, -1, 3],
[-1, 2, 5, -1]])
dvalues = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# مشتق فعال سازی ReLU
drelu = np.zeros_like(z)
drelu[z > 0] = 1
print(drelu)
# قانون زنجیره ای
drelu *= dvalues
print(drelu)
>>>
[[1 1 0 0]
[1 0 0 1]
[0 1 1 0]]
[[ 1 2 0 0]
[ 5 0 0 8]
[ 0 10 11 0]]
برای محاسبه مشتق ReLU، آرایه ای پر از صفر ایجاد کردیم. np.zeros_like یک تابع NumPy است که آرایه ای پر از صفر ایجاد می کند، با شکل آرایه از پارامتر آن، آرایه z در مورد ما، که نمونه ای از خروجی نورون است. پس از مشتق () ReLU ، مقادیر مربوط به ورودی های بزرگتر از 0 را به صورت 1 تنظیم می کنیم. سپس این جدول را چاپ می کنیم تا آن را با گرادیان ها ببینیم و مقایسه کنیم. در پایان این آرایه را با گرادیان تابع بعدی ضرب می کنیم و نتیجه را چاپ می کنیم.
اکنون می توانیم این عملیات را ساده کنیم. از آنجا که آرایه مشتق شده () ReLU با 1s پر شده است ، که مقادیر ضرب شده در آنها را تغییر نمی دهد ، و 0 که مقدار ضرب را صفر می کند ، این بدان معنی است که می توانیم شیب های تابع بعدی را بگیریم و تمام مقادیری را که با ورودی() ReLU مطابقت دارند و برابر یا کمتر از 0 هستند ، روی 0 تنظیم کنیم:
import numpy as np
# خروجی لایه نمونه
z = np.array([[1, 2, -3, -4],
[2, -7, -1, 3],
[-1, 2, 5, -1]])
dvalues = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# مشتق فعال سازی ReLU
# با اعمال قانون زنجیره ای
drelu = dvalues.copy()
drelu[z <= 0] = 0
print(drelu)
>>>
[[ 1 2 0 0]
[ 5 0 0 8]
[ 0 10 11 0]]
کپی dvalues تضمین می کند که ما آن را در طول محاسبه مشتق ReLU تغییر نمی دهیم.
بیایید عبور رو به جلو و عقب یک نورون را با یک لایه کامل و مشتقات جزئی مبتنی بر دسته ترکیب کنیم. ما خروجی ReLU را یک بار دیگر به حداقل می رسانیم، فقط برای این مثال:
import numpy as np
# در گرادیان از لایه بعدی منتقل شد
# برای هدف این مثال می خواهیم از
# آرایه ای از مقادیر گرادیان افزایشی
dvalues = np.array([[1., 1., 1.],
[2., 2., 2.],
[3., 3., 3.]])
# ما 3 مجموعه ورودی – نمونه داریم
inputs = np.array([[1, 2, 3, 2.5],
[2., 5., -1., 2],
[-1.5, 2.7, 3.3, -0.8]])
# ما 3 مجموعه وزن داریم – یک مجموعه برای هر نورون
# ما 4 ورودی داریم، بنابراین 4 وزن
# به یاد بیاورید که ما وزنه ها را جابجا می کنیم
weights = np.array([[0.2, 0.8, -0.5, 1],
[0.5, -0.91, 0.26, -0.5],
[-0.26, -0.27, 0.17, 0.87]]).T
# یک تعصب برای هر نورون
# biases بردار ردیف با شکل (1 ، نورون ها) هستند
biases = np.array([[2, 3, 0.5]])
# Forward pass
layer_outputs = np.dot(inputs, weights) + biases # لایه متراکم
relu_outputs = np.maximum(0, layer_outputs) # فعال سازی ReLU
# بیایید پس انتشار را در اینجا بهینه و آزمایش کنیم
# فعال سازی ReLU – مشتق را با توجه به مقادیر ورودی شبیه سازی می کند
# از لایه بعدی که در طول پس انتشار به لایه فعلی منتقل می شود
drelu = relu_outputs.copy()
drelu[layer_outputs <= 0] = 0
# لایه متراکم
# dinputs – ضرب در وزن
dinputs = np.dot(drelu, weights.T)
# dweights – ضرب در ورودی ها
dweights = np.dot(inputs.T, drelu)
# dbiases – مقادیر مجموع، این کار را روی نمونه ها (محور اول)، keepdims انجام دهید
# از آنجایی که این به طور پیش فرض یک لیست ساده تولید می کند –
# ما این را در فصل 4 توضیح دادیم
dbiases = np.sum(drelu, axis=0, keepdims=True)
# پارامترهای به روز رسانی
weights += -0.001 * dweights
biases += -0.001 * dbiases
print(weights)
print(biases)
>>>
[[ 0.179515 0.5003665 -0.262746 ]
[ 0.742093 -0.9152577 -0.2758402]
[-0.510153 0.2529017 0.1629592]
[ 0.971328 -0.5021842 0.8636583]]
[[1.98489 2.997739 0.497389]]
در این کد، توابع ساده پایتون را با انواع NumPy جایگزین کردیم، داده های نمونه ایجاد کردیم، پاس های رو به جلو و عقب را محاسبه کردیم و پارامترها را به روز کردیم. اکنون لایه متراکم و کد فعال سازی ReLU را با یک روش عقب (برای پس انتشار) به روز می کنیم که در مرحله پس انتشار مدل خود آن را فراخوانی خواهیم کرد.
# لایه متراکم
class Layer_Dense:
# مقداردهی اولیه لایه
def __init__(self, inputs, neurons):
self.weights = 0.01 * np.random.randn(inputs, neurons)
self.biases = np.zeros((1, neurons))
# Forward pass
def forward(self, inputs):
self.output = np.dot(inputs, self.weights) + self.biases
# فعال سازی ReLU
class Activation_ReLU:
# Forward pass
def forward(self, inputs):
self.output = np.maximum(0, inputs)
در طول متد فوروارد برای کلاس Layer_Dense خود، می خواهیم به یاد داشته باشیم که ورودی ها چه بوده اند (به یاد بیاورید که هنگام محاسبه مشتق جزئی با توجه به وزن ها در طول پس انتشار به آنها نیاز داریم)، که می تواند به راحتی با استفاده از یک ویژگی شی (self.inputs) پیاده سازی شود:
# لایه متراکم
class Layer_Dense:
…
# Forward pass
def forward(self, inputs):
…
self.inputs = inputs
در مرحله بعد، کد Backward pass (backpropagation) خود را که قبلا توسعه داده بودیم به یک متد جدید در کلاس لایه اضافه می کنیم که آن را به عقب می نامیم:
class Layer_Dense:
…
# Backward pass
def backward(self, dvalues):
# گرادیان در پارامترها
self.dweights = np.dot(self.inputs.T, dvalues)
self.dbiases = np.sum(dvalues, axis=0, keepdims=True)
# گرادیان در مقادیر
self.dinputs = np.dot(dvalues, self.weights.T)
سپس همین کار را برای کلاس ReLU خود انجام می دهیم:
# فعال سازی ReLU
class Activation_ReLU:
# Forward pass
def forward(self, inputs):
# مقادیر ورودی را به خاطر بسپارید
self.inputs = inputs
self.output = np.maximum(0, inputs)
# Backward pass
def backward(self, dvalues):
# از آنجایی که باید متغیر اصلی را تغییر دهیم،
# بیایید ابتدا یک کپی از مقادیر بسازیم
self.dinputs = dvalues.copy()
# گرادیان صفر که در آن مقادیر ورودی منفی بودند
self.dinputs[self.inputs <= 0] = 0
تا این مرحله، ما همه چیزهایی را که برای انجام پس انتشار نیاز داریم، به جز مشتق تابع فعال سازی Softmax و مشتق تابع از دست دادن آنتروپی متقاطع، پوشش داده ایم.
مشتق از دست دادن متقاطع آنتروپی طبقه بندی شده
اگر علاقه ای به مشتق ریاضی از دست دادن متقابل آنتروپی طبقه ای ندارید، با خیال راحت به پیاده سازی کد بروید، زیرا مشتقات برای توابع ضرر رایج شناخته شده اند و لزوما نیازی به دانستن نحوه حل آنها نخواهید داشت. با این حال، اگر قصد دارید توابع از دست دادن سفارشی ایجاد کنید، تمرین خوبی است.
همانطور که در فصل 5 آموختیم، فرمول تابع از دست دادن آنتروپی متقابل طبقه ای به شرح زیر است:
جایی که Li مقدار از دست دادن نمونه را نشان می دهد، i – نمونه i-th در یک مجموعه، k – شاخص برچسب هدف (برچسب زمینی واقعی)، y – مقادیر هدف و y-hat – مقادیر پیش بینی شده.
این فرمول هنگام محاسبه مقدار ضرر مناسب است، زیرا تنها چیزی که نیاز داریم خروجی تابع فعال سازی Softmax در شاخص کلاس صحیح است. به منظور محاسبه مشتق، از معادله کامل ذکر شده در فصل 5 استفاده خواهیم کرد:
Where Li denotes sample loss value, i — i-th sample in a set, j — label/output index, y — target values and y-hat — predicted values.
ما از این تابع کامل استفاده خواهیم کرد زیرا هدف فعلی ما محاسبه گرادیان است که از مشتقات جزئی تابع ضرر با توجه به هر یک از ورودی های آن (که خروجی های تابع فعال سازی Softmax هستند) تشکیل شده است. این بدان معنی است که ما نمی توانیم از معادله استفاده کنیم، که فقط مقدار را در شاخص کلاس صحیح (اولین معادله بالا) می گیرد. برای محاسبه مشتقات جزئی با توجه به هر یک از ورودی ها، به معادله ای نیاز داریم که همه آنها را به عنوان پارامتر در نظر بگیرد، بنابراین انتخاب استفاده از معادله کامل است.
ابتدا بیایید معادله گرادیان را تعریف کنیم:
جایی که Li مقدار از دست دادن نمونه را نشان می دهد، i – نمونه i-th در یک مجموعه، j – شاخص برچسب/خروجی، y – مقادیر هدف و y-hat – مقادیر پیش بینی شده.
اکنون باید مشتق تابع لگاریتمی را که متقابل پارامتر آن است ، ضرب (با استفاده از قانون زنجیره ای) در مشتق جزئی این پارامتر – با استفاده از نماد اول (که لاگرانژ نیز نامیده می شود) حل کنیم:
می توانیم آن را بیشتر حل کنیم (با استفاده از نماد لایب نیتس در این مورد):
بیایید این مشتق را اعمال کنیم:
مشتق جزئی یک مقدار با توجه به این مقدار برابر با 1 است:
از آنجا که ما مشتق جزئی را با توجه به y داده شده j محاسبه می کنیم ، مجموع روی یک عنصر واحد انجام می شود و می توان آن را حذف کرد:
راه حل کامل:
مشتق این تابع ضرر با توجه به ورودی های آن (مقادیر پیش بینی شده در نمونه i-th، از آنجایی که ما به یک گرادیان با توجه به مقادیر پیش بینی شده علاقه مند هستیم) برابر است با بردار منفی حقیقت زمینی، تقسیم بر بردار مقادیر پیش بینی شده (که بردار خروجی تابع softmax نیز هست).
پیاده سازی کد مشتق از ضرر متقابل آنتروپی طبقه بندی شده
از آنجایی که ما این معادله را استخراج کردیم و متوجه شدیم که به یک عملیات تقسیم ساده 2 مقدار حل می شود، می دانیم که با NumPy می توانیم این عملیات را به بردارهای نمونه ای حقیقت زمینی و مقادیر پیش بینی شده و بیشتر به آرایه های دسته ای آنها گسترش دهیم. از دیدگاه کدنویسی، باید یک روش عقب مانده به کلاس Loss_CategoricalCrossentropy اضافه کنیم. ما باید آرایه ای از پیش بینی ها و آرایه مقادیر واقعی را در آن منتقل کنیم و تقسیم نفی شده آنها را محاسبه کنیم:
# از دست دادن آنتروپی متقاطع
class Loss_CategoricalCrossentropy(Loss):
…
# Backward pass
def backward(self, dvalues, y_true):
# تعداد نمونه ها
samples = len(dvalues)
# تعداد برچسب ها در هر نمونه
# ما از اولین نمونه برای شمارش آنها استفاده خواهیم کرد
labels = len(dvalues[0])
# اگر برچسب ها پراکنده هستند، آنها را به یک بردار داغ تبدیل کنید
if len(y_true.shape) == 1:
y_true = np.eye(labels)[y_true]
# شیب را محاسبه کنید
self.dinputs = -y_true / dvalues
# شیب را عادی کنید
self.dinputs = self.dinputs / samples
همراه با محاسبه مشتق جزئی، ما در حال انجام دو عملیات اضافی هستیم. ابتدا، ما برچسب های عددی را به بردارهای رمزگذاری شده یک داغ تبدیل می کنیم – قبل از این، باید بررسی کنیم که y_true از چند بعد تشکیل شده است. اگر شکل برچسب ها یک بعد واحد را برمی گرداند
(به این معنی که آنها به شکل یک لیست هستند و نه مانند یک آرایه)، آنها از اعداد گسسته تشکیل شده اند و باید به لیستی از بردارهای رمزگذاری شده یک داغ تبدیل شوند – یک آرایه دو بعدی. اگر اینطور است، باید آنها را به بردارهای رمزگذاری شده یک داغ تبدیل کنیم. ما از متد np.eye استفاده می کنیم که با توجه به یک عدد، n، یک آرایه nxn پر از یک در مورب و صفر در هر جای دیگر را برمی گرداند . مثلا:
import numpy as np
np.eye(5)
>>>
array([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
سپس می توانیم این جدول را با برچسب عددی فهرست کنیم تا بردار رمزگذاری شده یک داغ را که آن را نشان می دهد بدست آوریم:
np.eye(5)[1]
>>>
array([0., 1., 0., 0., 0.])
np.eye(5)[4]
>>>
array([0., 0., 0., 0., 1.])
اگر y_true از قبل یک داغ کدگذاری شده باشد، این مرحله را انجام نمی دهیم.
عملیات دوم نرمال سازی گرادیان است. همانطور که در فصل بعدی یاد خواهیم گرفت، بهینه سازها تمام گرادیان های مربوط به هر وزن و بایاس را قبل از ضرب آنها در نرخ یادگیری (یا عامل دیگری) جمع می کنند. این بدان معناست که در مورد ما این است که هرچه نمونه های بیشتری در یک مجموعه داده داشته باشیم، مجموعه های گرادیان بیشتری در این مرحله دریافت خواهیم کرد و این مجموع بزرگتر خواهد شد. در نتیجه، ما باید نرخ یادگیری را با توجه به هر مجموعه از نمونه ها تنظیم کنیم. برای حل این مسئله، می توانیم تمام گرادیان ها را بر تعداد نمونه ها تقسیم کنیم. مجموع عناصر تقسیم بر تعداد آنها مقدار متوسط آنها است (و همانطور که اشاره کردیم، بهینه ساز مجموع را انجام می دهد) – به این ترتیب، ما به طور موثر گرادیان ها را نرمال می کنیم و مقدار مجموع آنها را با تعداد نمونه ها ثابت می کنیم.
مشتق فعال سازی Softmax
محاسبه بعدی که باید انجام دهیم مشتق جزئی تابع Softmax است که کمی پیچیده تر از مشتق از دست دادن آنتروپی متقاطع طبقه بندی شده است. بیایید معادله تابع فعال سازی Softmax را به خود یادآوری کنیم و مشتق آن را تعریف کنیم:
جایی که Si,j نشان دهنده خروجی j-th Softmax از نمونه i-th است، z – آرایه ورودی که لیستی از بردارهای ورودی (بردارهای خروجی از لایه قبلی) است، zi,j – ورودی j-th Softmax از نمونه i-th، L – تعداد ورودی ها، zi,k – k-th ورودی Softmax از نمونه i-th.
همانطور که در فصل 4 توضیح دادیم، تابع Softmax برابر با ورودی نمایی تقسیم بر مجموع تمام ورودی های نمایی است. به عبارت دیگر، ابتدا باید همه مقادیر را نمایی کنیم، سپس هر یک از آنها را بر مجموع همه آنها تقسیم کنیم تا نرمال سازی انجام شود. هر ورودی به Softmax بر هر یک از خروجی ها تأثیر می گذارد و ما باید مشتق جزئی هر خروجی را با توجه به هر ورودی محاسبه کنیم. از جنبه برنامه نویسی چیزها، اگر تأثیر یک لیست را بر لیست دیگر محاسبه کنیم، در نتیجه ماتریسی از مقادیر دریافت خواهیم کرد. این دقیقا همان چیزی است که ما در اینجا محاسبه خواهیم کرد – ما ماتریس Jacobian بردارها را محاسبه می کنیم (که بعدا توضیح خواهیم داد) که به زودی عمیق تر به آن خواهیم پرداخت.
برای محاسبه این مشتق ابتدا باید مشتق عملیات تقسیم را تعریف کنیم:
برای محاسبه مشتق عملیات تقسیم، باید مشتق عدد ضرب در مخرج را بگیریم، عدد ضرب شده در مشتق مخرج را از آن کم کنیم و سپس نتیجه را بر مخرج مربع تقسیم کنیم.
اکنون می توانیم شروع به حل مشتق کنیم:
بیایید مشتق عملیات تقسیم را اعمال کنیم:
در این مرحله، ما دو مشتق جزئی در معادله داریم. برای یکی در سمت راست عدد (سمت راست عملگر تفریق):
ما باید مشتق مجموع ثابت,e (عدد اویلر) را محاسبه کنیم که به توان zi,l افزایش یافته است (جایی که l شاخص های متوالی را از 1 به تعداد خروجی های Softmax – L نشان می دهد) با توجه به zi,k.. مشتق عملیات جمع مجموع مشتقات است و مشتق ثابت e با توجه به n برابر با n برابر با enاست:
این یک مورد خاص است که مشتق یک تابع نمایی برابر با خود این تابع نمایی باشد، زیرا توان آن دقیقا همان چیزی است که ما با توجه به آن به دست می آوریم، بنابراین مشتق آن برابر با 1 است. ما همچنین می دانیم که محدوده 1…L شامل k است (k یکی از شاخص های این محدوده است) دقیقا یک بار و سپس، در این مورد، مشتق برابر با e با توان zi,j (همانطور که j برابر با k است) و 0 در غیر این صورت (زمانی که j برابر با k نیست، l شامل نمی شود. zi,j و به عنوان یک ثابت در نظر گرفته می شود – مشتق ثابت برابر با 0 است:
مشتق سمت چپ عملگر تفریق در مخرج حالت کمی متفاوت است:
این شامل مجموع تمام عناصر مانند مشتقی که لحظاتی پیش حل کردیم نیست، بنابراین می تواند 0 شود اگر j≠k یا e به توان zi,j اگر j=k باشد. این بدان معناست که با شروع از این مرحله، باید مشتقات را برای هر دو حالت جداگانه محاسبه کنیم. بیایید با j=k شروع کنیم.
در مورد j=k، مشتق سمت چپ برابر است با e با توان zi,j و مشتق سمت راست در هر دو مورد به یک مقدار حل می شود. بیایید آنها را جایگزین کنیم:
صورت شامل ثابت e به توان zi,j در هر دو minuend (مقداری که از آن کم می کنیم) و subtrahend (مقداری که از minuend کم می کنیم) عملیات تفریق است. به همین دلیل ، می توانیم صورت را دوباره گروه بندی کنیم تا این مقدار ضرب در تفریق ضریب فعلی آنها را داشته باشد. همچنین می توانیم مخرج را به جای استفاده از توان 2 به عنوان ضرب مقدار بنویسیم:
سپس بیایید کل معادله را به 2 قسمت تقسیم کنیم:
ما e را از صورت و مجموع را از مخرج به کسر خود منتقل کردیم ، و محتوای پرانتز در صورت ، و مجموع دیگر از مخرج به عنوان کسر دیگر ، هر دو با عملیات ضرب به هم متصل شدند. اکنون می توانیم کسر “راست” را به دو کسر جداگانه تقسیم کنیم:
در این مورد، از آنجایی که این یک عملیات تفریق است، هر دو مقدار را از صورت جدا کردیم، هر دو را بر مخرج تقسیم کردیم و عملیات تفریق را بین کسرهای جدید اعمال کردیم. اگر دقیق نگاه کنیم، کسر “چپ” به معادله تابع Softmax و همچنین یکی “راست” تبدیل می شود، با کسر میانی که به 1 به عنوان عدد و مخرج حل می شود، مقادیر یکسانی هستند:
توجه داشته باشید که تابع Softmax “چپ” دارای پارامتر j و “راست” یک k است – هر دو به ترتیب از اعداد خود آمده اند.
راه حل کامل:
حالا باید به عقب برگردیم و مشتق را در مورد j≠k حل کنیم. در این حالت ، مشتق “چپ” معادله اصلی به 0 حل می شود زیرا کل عبارت به عنوان یک ثابت در نظر گرفته می شود:
تفاوت در این است که اکنون کل subtrahend به 0 حل می شود و فقط minuend را در صورت برای ما باقی می گذارد:
اکنون، دقیقا مانند قبل، می توانیم مخرج را به جای استفاده از توان 2 به عنوان ضرب مقادیر بنویسیم:
این به ما امکان می دهد این کسر را با استفاده از عملیات ضرب به 2 کسر تقسیم کنیم:
اکنون هر دو کسر تابع Softmax را نشان می دهند:
توجه داشته باشید که تابع سمت چپ Softmax دارای پارامتر j است و تابع “راست” دارای k است – هر دو به ترتیب از اعداد خود آمده اند.
راه حل کامل:
به عنوان خلاصه، راه حل مشتق تابع Softmax با توجه به ورودی های آن به شرح زیر است:
این پایان محاسباتی نیست که می توانیم در اینجا انجام دهیم. هنگامی که در این فرم باقی می ماند، 2 معادله جداگانه برای کدگذاری و استفاده در موارد مختلف خواهیم داشت، که برای سرعت محاسبات چندان راحت نیست. با این حال، ما می توانیم نتیجه مورد دوم مشتق را بیشتر تغییر دهیم:
در مرحله اول، سافت مکس دوم را در امتداد علامت منفی به داخل براکت ها منتقل کردیم تا بتوانیم یک صفر در داخل آنها و درست قبل از این مقدار اضافه کنیم. این راه حل را تغییر نمی دهد، اما اکنون:
هر دو راه حل بسیار شبیه به هم هستند ، آنها فقط در یک مقدار واحد متفاوت هستند. به راحتی، تابع Kronecker delta وجود دارد (که به زودی توضیح خواهیم داد) که معادله آن به شرح زیر است:
ما می توانیم آن را در اینجا اعمال کنیم و معادله خود را بیشتر به موارد زیر ساده کنیم:
این راه حل نهایی ریاضی برای مشتق خروجی های تابع Softmax با توجه به هر یک از ورودی های آن است. برای اینکه پیاده سازی آن در پایتون با استفاده از NumPy کمی آسان تر شود، بیایید معادله را برای آخرین بار تبدیل کنیم:
ما اساسا Si,j را در هر دو طرف عملیات تفریق از پرانتز ضرب کردیم.
پیاده سازی کد مشتق فعال سازی Softmax
این به ما اجازه می دهد تا راه حل را تنها با استفاده از دو تابع NumPy کدنویسی کنیم، که اکنون گام به گام توضیح خواهیم داد:
بیایید یک نمونه واحد بسازیم: softmax_output = [0.7, 0.1, 0.2]
و آن را به عنوان لیستی از نمونه ها شکل دهید:
import numpy as np
softmax_output = np.array(softmax_output).reshape(-1, 1)
print(softmax_output)
>>>
array([[0.7],
[0.1],
[0.2]])
سمت چپ معادله، خروجی Softmax ضرب در دلتای کرونکر است. دلتای کرونکر زمانی که هر دو ورودی برابر باشند برابر با 1 و در غیر این صورت برابر با 0 است. اگر این را به عنوان یک آرایه تجسم کنیم، آرایه ای از صفرها با یک ها در مورب خواهیم داشت – ممکن است به یاد داشته باشید که ما قبلا چنین راه حلی را با استفاده از روش np.eye پیاده سازی کرده ایم:
print(np.eye(softmax_output.shape[0]))
>>>
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
اکنون ضرب هر دو مقدار را از قسمت معادله انجام می دهیم:
print(softmax_output * np.eye(softmax_output.shape[0]))
>>>
array([[0.7, 0. , 0. ],
[0. , 0.1, 0. ],
[0. , 0. , 0.2]])
به نظر می رسد که می توانیم با جایگزینی آن با فراخوانی متد np.diagflat، که همان راه حل را محاسبه می کند، سرعت به دست آوریم – روش diagflat با استفاده از یک بردار ورودی به عنوان مورب یک آرایه ایجاد می کند:
print(np.diagflat(softmax_output))
>>>
array([[0.7, 0. , 0. ],
[0. , 0.1, 0. ],
[0. , 0. , 0.2]])
بخش دیگر معادله Si,jSi,k است – ضرب خروجی های Softmax، به ترتیب روی شاخص های j و k تکرار می شود. از آنجایی که برای هر نمونه ( شاخص i)، باید مقادیر خروجی تابع Softmax (در تمام ترکیبات) را ضرب کنیم، می توانیم از عملیات حاصل ضرب نقطه استفاده کنیم. برای این کار، فقط باید آرگومان دوم را جابجا کنیم تا فرم بردار ردیف آن را بدست آوریم (همانطور که در فصل 2 توضیح داده شده است):
print(np.dot(softmax_output, softmax_output.T))
>>>
array([[0.49, 0.07, 0.14],
[0.07, 0.01, 0.02],
[0.14, 0.02, 0.04]])
سرانجام ، می توانیم تفریق هر دو آرایه را انجام دهیم (به دنبال معادله):
print(np.diagflat(softmax_output) –
np.dot(softmax_output, softmax_output.T))
>>>
array([[ 0.21, -0.07, -0.14],
[-0.07, 0.09, -0.02],
[-0.14, -0.02, 0.16]])
نتیجه ماتریس معادله و راه حل آرایه ارائه شده توسط کد ، ماتریس ژاکوبین نامیده می شود. در مورد ما، ماتریس ژاکوبین آرایه ای از مشتقات جزئی در تمام ترکیبات هر دو بردار ورودی است. به یاد داشته باشید، ما مشتقات جزئی هر خروجی تابع Softmax را با توجه به هر ورودی به طور جداگانه محاسبه می کنیم. ما این کار را به این دلیل انجام می دهیم که هر ورودی به دلیل فرآیند نرمال سازی، که مجموع تمام ورودی های نمایی را می گیرد، بر هر خروجی تأثیر می گذارد. نتیجه این عملیات، که بر روی دسته ای از نمونه ها انجام می شود، فهرستی از ماتریس های ژاکوبی است که به طور موثر یک ماتریس سه بعدی را تشکیل می دهد – شما می توانید آن را به عنوان ستونی تجسم کنید که سطوح آن ماتریس های ژاکوبی هستند که گرادیان نمونه ای تابع Softmax هستند.
این یک سوال را ایجاد می کند – اگر گرادیان های نمونه ماتریس های ژاکوبی باشند، چگونه می توانیم قانون زنجیره را با گرادیان برگشتی که از تابع از دست دادن منتشر می شود، انجام دهیم، زیرا یک بردار برای هر نمونه است؟ همچنین، با این واقعیت که لایه قبلی، که لایه Dense است، انتظار دارد گرادیان ها یک آرایه دو بعدی باشند، چه کنیم؟ در حال حاضر، ما یک آرایه سه بعدی از مشتقات جزئی داریم – فهرستی از ماتریس های ژاکوبی. مشتق تابع Softmax با توجه به هر یک از ورودی های آن بردار مشتقات جزئی (یک ردیف از ماتریس ژاکوبین) را برمی گرداند، زیرا این ورودی بر تمام خروجی ها تأثیر می گذارد، بنابراین بر مشتق جزئی برای هر یک از آنها نیز تأثیر می گذارد. ما باید مقادیر این بردارها را جمع کنیم تا هر یک از ورودی های هر یک از نمونه ها به جای آن یک مقدار مشتق جزئی را برگردانند. از آنجایی که هر ورودی بر تمام خروجی ها تأثیر می گذارد، بردار برگشتی مشتقات جزئی باید برای مشتق جزئی نهایی با توجه به این ورودی خلاصه شود. ما می توانیم این عملیات را مستقیما روی هر یک از ماتریس های ژاکوبین انجام دهیم و قانون زنجیره را به طور همزمان اعمال کنیم (اعمال گرادیان از تابع ضرر) با استفاده از np.dot() – برای هر نمونه، ردیف را از ماتریس ژاکوبین می گیرد و آن را در مقدار مربوطه از گرادیان تابع ضرر ضرب می کند. در نتیجه، حاصل ضرب نقطه ای هر یک از این بردارها و مقادیر یک مقدار مفرد را برمی گرداند و بردار مشتقات جزئی را به صورت نمونه و یک آرایه دو بعدی (لیستی از بردارهای حاصل) را به صورت دسته ای تشکیل می دهد.
بیایید راه حل را کدنویسی کنیم:
# فعال سازی Softmax
class Activation_Softmax:
…
# Backward pass
def backward(self, dvalues):
# آرایه راه اندازی نشده ایجاد کنید
self.dinputs = np.empty_like(dvalues)
# خروجی ها و گرادیان ها را برشمارید
for index, (single_output, single_dvalues) in \
enumerate(zip(self.output, dvalues)):
# آرایه خروجی را صاف کنید
single_output = single_output.reshape(-1, 1)
# ماتریس Jacobian خروجی را محاسبه کنید و
jacobian_matrix = np.diagflat(single_output) – \
np.dot(single_output, single_output.T)
# گرادیان نمونه را محاسبه کنید
# و آن را به آرایه گرادیان های نمونه اضافه کنید
self.dinputs[index] = np.dot(jacobian_matrix,
single_dvalues)
ابتدا، ما یک آرایه خالی ایجاد کردیم (که به آرایه گرادیان حاصل تبدیل می شود) با همان شکل گرادیان هایی که برای اعمال قانون زنجیره دریافت می کنیم. متد np.empty_like یک آرایه خالی و مقداردهی اولیه ایجاد می کند. Uninitialized به این معنی است که می توانیم انتظار داشته باشیم که حاوی مقادیر بی معنی باشد، اما به هر حال همه آنها را به زودی تنظیم خواهیم کرد، بنابراین نیازی به مقداردهی اولیه نیست (به عنوان مثال، با صفر هایی که به جای آن np.zeros() استفاده می کنند). در مرحله بعد، ما قصد داریم نمونه را بر روی جفت خروجی ها و گرادیان ها تکرار کنیم، مشتقات جزئی را همانطور که قبلا توضیح داده شد محاسبه کنیم و حاصلضرب نهایی (اعمال قانون زنجیره ای) ماتریس ژاکوبین و بردار گرادیان (از آرایه گرادیان عبور) را محاسبه کنیم، بردار حاصل را به عنوان یک ردیف در آرایه dinput ذخیره کنیم. ما قصد داریم هر بردار را در هر ردیف ذخیره کنیم و آرایه خروجی را تشکیل دهیم.
از دست دادن آنتروپی متقابل طبقه بندی مشترک و مشتق فعال سازی Softmax
در حال حاضر، ما مشتقات جزئی توابع فعال سازی متقابل آنتروپی طبقه بندی و Softmax را محاسبه کرده ایم و در نهایت می توانیم از آنها استفاده کنیم، اما هنوز یک مرحله دیگر وجود دارد که می توانیم برای سرعت بخشیدن به محاسبات انجام دهیم. کتاب ها و آموزش های مختلف معمولا مشتق تابع loss را با توجه به ورودی های Softmax یا حتی وزن و بایاس لایه خروجی به طور مستقیم ذکر می کنند و به جزئیات مشتقات جزئی این توابع جداگانه نمی پردازند. این تا حدی به این دلیل است که مشتقات هر دو تابع برای حل یک معادله ساده ترکیب می شوند – اجرای کل کد ساده تر و سریع تر است. وقتی به کد فعلی خود نگاه می کنیم، چندین عملیات را برای محاسبه گرادیان ها انجام می دهیم و حتی یک حلقه را در مرحله عقب تابع فعال سازی قرار می دهیم.
بیایید قانون زنجیره را برای محاسبه مشتق جزئی تابع ضرر متقاطع آنتروپی طبقه ای با توجه به ورودی های تابع Softmax اعمال کنیم. ابتدا بیایید این مشتق را با استفاده از قانون زنجیره تعریف کنیم:
این مشتق جزئی برابر با مشتق جزئی تابع ضرر با توجه به ورودی های آن است که (با استفاده از قانون زنجیره ای) در مشتق جزئی تابع فعال سازی با توجه به ورودی های آن ضرب می شود. اکنون باید معناشناسی را سیستماتیک کنیم – می دانیم که ورودی های تابع از دست دادن y-hati,j, ، خروجی های تابع فعال سازی، Si,j: هستند:
این بدان معناست که ما می توانیم معادله را به شکل زیر به روز کنیم:
اکنون می توانیم معادله را جایگزین مشتق جزئی تابع آنتروپی متقاطع طبقه بندی کنیم، اما، از آنجایی که ما مشتق جزئی را با توجه به ورودی های Softmax محاسبه می کنیم، از عملگر حاوی عملگر مجموع روی همه خروجی ها استفاده می کنیم – به زودی مشخص خواهد شد که چرا. مشتق:
پس از جایگزینی معادله مشتق ترکیبی:
اکنون، همانطور که قبلا محاسبه کردیم، مشتق جزئی فعال سازی Softmax، قبل از اعمال دلتای کرونکر روی آن:
بیایید در واقع جایگزینی Si,j را با y-hati,j در اینجا نیز انجام دهیم:
راه حل بسته به اینکه j=k یا j≠k متفاوت است. برای رسیدگی به این وضعیت، باید مشتق جزئی فعلی را به دنبال این موارد تقسیم کنیم – زمانی که هر دو مطابقت دارند و چه زمانی مطابقت ندارند:
برای مورد j≠k، ما فقط عملگر sum را به روز کردیم تا k را حذف کنیم و این تنها تغییر است:
برای مورد j=k، ما نیازی به عملگر مجموع نداریم زیرا فقط یک عنصر از شاخص k را جمع می کند. برای
به همین دلیل، ما همچنین jindices را با k جایگزین می کنیم:
بازگشت به معادله اصلی:
اکنون می توانیم مشتقات جزئی تابع فعال سازی را برای هر دو حالت با راه حل های تازه تعریف شده جایگزین کنیم:
ما می توانیم y-hati,k را از هر دو طرف تفریق در معادله حذف کنیم – هر دو آن را به عنوان بخشی از عملیات ضرب و در مخرج خود دارند. سپس در سمت راست معادله ، می توانیم علامت 2 منفی را با مثبت یک جایگزین کنیم و پرانتز را حذف کنیم:
حالا بیایید -yi,k را در محتوای پرانتز سمت چپ معادله ضرب کنیم:
حالا بیایید به عملیات جمع نگاه کنیم yi,jy-hati,k را در تمام مقادیر ممکن شاخص j جمع می کند به جز زمانی که برابر با k باشد. سپس، در سمت چپ این قسمت از معادله، ما yi,ky-hati,k, را داریم که شامل yi,k است – عنصر دقیقی که از مجموع حذف شده است. سپس می توانیم هر دو عبارت را به هم بپیوندیم:
اکنون عملگر جمع روی تمام مقادیر ممکن j تکرار می شود و از آنجایی که می دانیم yi,j برای هر i بردار رمزگذاری شده یک داغ از مقادیر حقیقت زمینی است، مجموع تمام عناصر آن برابر با 1 است. به عبارت دیگر، به دنبال توضیح قبلی در این فصل – این مجموع 0 را در y-hati,k به جز یک موقعیت واحد، برچسب واقعی، که در آن 1 را در این مقدار ضرب می کند. سپس می توانیم آن را بیشتر به موارد زیر ساده کنیم:
راه حل کامل:
همانطور که می بینیم، وقتی قانون زنجیره ای را برای هر دو مشتق جزئی اعمال می کنیم، کل معادله به طور قابل توجهی برای تفریق مقادیر پیش بینی شده و حقیقت زمینی ساده می شود. همچنین محاسبه آن چندین برابر سریعتر است.
از دست دادن متقابل آنتروپی طبقه بندی رایج و مشتق فعال سازی Softmax – پیاده سازی کد
برای کدگذاری این راه حل، هیچ چیز در پاس فوروارد تغییر نمی کند – ما هنوز باید آن را روی تابع فعال سازی برای دریافت خروجی ها و سپس روی تابع ضرر برای محاسبه مقدار ضرر انجام دهیم. برای پس انتشار، مرحله عقب را ایجاد می کنیم که حاوی پیاده سازی معادله جدید است که گرادیان ترکیبی توابع تلفات و فعال سازی را محاسبه می کند. ما راه حل را به عنوان یک کلاس جداگانه کدنویسی می کنیم، که هم فعال سازی Softmax و هم آبجکت های متقاطع آنتروپی طبقه بندی شده را مقداردهی اولیه می کند و متدهای فوروارد آنها را به ترتیب در طول پاس رو به جلو فراخوانی می کند. سپس پاس عقب جدید حاوی کد جدید است:
# طبقه بندی کننده Softmax – فعال سازی ترکیبی Softmax
# و از دست دادن آنتروپی متقاطع برای گام عقب سریعتر
class Activation_Softmax_Loss_CategoricalCrossentropy():
# اشیاء عملکرد فعال سازی و از دست دادن را ایجاد می کند
def __init__(self):
self.activation = Activation_Softmax()
self.loss = Loss_CategoricalCrossentropy()
# Forward pass
def forward(self, inputs, y_true):
# عملکرد فعال سازی لایه خروجی
self.activation.forward(inputs)
# خروجی را تنظیم کنید
self.output = self.activation.output
# مقدار ضرر را محاسبه و برگردانید
return self.loss.calculate(self.output, y_true)
# Backward pass
def backward(self, dvalues, y_true):
# تعداد نمونه ها
samples = len(dvalues)
# اگر برچسب ها یک داغ کدگذاری شده باشند،
# آنها را به مقادیر گسسته تبدیل کنید
if len(y_true.shape) == 2:
y_true = np.argmax(y_true, axis=1)
# کپی کنید تا بتوانیم با خیال راحت تغییر دهیم
self.dinputs = dvalues.copy()
# شیب را محاسبه کنید
self.dinputs[range(samples), y_true] -= 1
# شیب را عادی کنید
self.dinputs = self.dinputs / samples
برای پیاده سازی راه حل y-hati,k-yi,k، به جای انجام تفریق آرایه های کامل، از این واقعیت استفاده می کنیم که y موجود در کد y_true از بردارهای کدگذاری شده یک داغ تشکیل شده است، به این معنی که برای هر نمونه، فقط یک مقدار مفرد 1 در این بردارها وجود دارد و موقعیت های باقی مانده با صفر پر می شوند.
این بدان معنی است که ما می توانیم از NumPy برای فهرست بندی آرایه پیش بینی با شماره نمونه و شاخص مقدار واقعی آن استفاده کنیم و 1 را از این مقادیر کم کنیم. این عملیات به جای برچسب های رمزگذاری شده یک داغ به برچسب های واقعی گسسته نیاز دارد، بنابراین کد اضافی که در صورت نیاز تبدیل را انجام می دهد – اگر تعداد ابعاد در آرایه حقیقت زمینی برابر با 2 باشد، به این معنی است که ماتریسی متشکل از بردارهای رمزگذاری شده یک داغ است. ما می توانیم از np.argmax() استفاده کنیم، که شاخص حداکثر مقدار را برمی گرداند (شاخص برای 1 در این مورد)، اما برای به دست آوردن بردار شاخص ها باید این عملیات را به صورت نمونه انجام دهیم:
import numpy as np
y_true = np.array([[1,0,0],[0,0,1],[0,1,0]])
np.argmax(y_true)
>>>
0
print(np.argmax(y_true, axis=1))
>>>
[0, 2, 1]
برای مرحله آخر، گرادیان را دقیقا به همان روش و به همان دلیلی که همراه با نرمال سازی گرادیان متقاطع آنتروپی طبقه بندی شده توضیح داده شد، نرمال می کنیم.
بیایید کد هر یک از کلاس هایی را که به روز کرده ایم خلاصه کنیم:
# فعال سازی Softmax
class Activation_Softmax:
# Forward pass
def forward(self, inputs):
# مقادیر ورودی را به خاطر بسپارید
self.inputs = inputs
# احتمالات غیرعادی را دریافت کنید
exp_values = np.exp(inputs – np.max(inputs, axis=1,
keepdims=True))
# آنها را برای هر نمونه عادی کنید
probabilities = exp_values / np.sum(exp_values, axis=1,
keepdims=True)
self.output = probabilities
# Backward pass
def backward(self, dvalues):
# آرایه راه اندازی نشده ایجاد کنید
self.dinputs = np.empty_like(dvalues)
# خروجی ها و گرادیان ها را برشمارید
for index, (single_output, single_dvalues) in \
enumerate(zip(self.output, dvalues)):
# آرایه خروجی را صاف کنید
single_output = single_output.reshape(-1, 1)
# ماتریس ژاکوبی خروجی را محاسبه کنید و
jacobian_matrix = np.diagflat(single_output) – \
np.dot(single_output, single_output.T)
# گرادیان نمونه را محاسبه کنید
# و آن را به آرایه گرادیان های نمونه اضافه کنید
self.dinputs[index] = np.dot(jacobian_matrix,
single_dvalues)
# از دست دادن آنتروپی متقاطع
class Loss_CategoricalCrossentropy(Loss):
# Forward pass
def forward(self, y_pred, y_true):
# تعداد نمونه ها در یک دسته
samples = len(y_pred)
# کلیپ داده ها برای جلوگیری از تقسیم بر 0
# هر دو طرف را کلیپ کنید تا میانگین را به سمت هیچ مقداری نکشید
y_pred_clipped = np.clip(y_pred, 1e-7, 1 – 1e-7)
# احتمالات برای مقادیر هدف –
# فقط اگر برچسب های طبقه بندی شده باشد
if len(y_true.shape) == 1:
correct_confidences = y_pred_clipped[
range(samples),
y_true
]
# مقادیر ماسک – فقط برای برچسب های رمزگذاری شده با یک داغ
elif len(y_true.shape) == 2:
correct_confidences = np.sum(
y_pred_clipped * y_true,
axis=1
)
# ضرر و زیان
negative_log_likelihoods = -np.log(correct_confidences)
return negative_log_likelihoods
# Backward pass
def backward(self, dvalues, y_true):
# تعداد نمونه ها
samples = len(dvalues)
# تعداد برچسب ها در هر نمونه
# ما از اولین نمونه برای شمارش آنها استفاده خواهیم کرد
labels = len(dvalues[0])
# اگر برچسب ها پراکنده هستند، آنها را به یک بردار داغ تبدیل کنید
if len(y_true.shape) == 1:
y_true = np.eye(labels)[y_true]
# شیب را محاسبه کنید
self.dinputs = -y_true / dvalues
# شیب را عادی کنید
self.dinputs = self.dinputs / samples
# طبقه بندی کننده Softmax – فعال سازی ترکیبی Softmax
# و از دست دادن آنتروپی متقاطع برای گام عقب سریعتر
class Activation_Softmax_Loss_CategoricalCrossentropy():
# اشیاء عملکرد فعال سازی و از دست دادن را ایجاد می کند
def __init__(self):
self.activation = Activation_Softmax()
self.loss = Loss_CategoricalCrossentropy()
# Forward pass
def forward(self, inputs, y_true):
# عملکرد فعال سازی لایه خروجی
self.activation.forward(inputs)
# خروجی را تنظیم کنید
self.output = self.activation.output
# مقدار ضرر را محاسبه و برگردانید
return self.loss.calculate(self.output, y_true)
# Backward pass
def backward(self, dvalues, y_true):
# تعداد نمونه ها
samples = len(dvalues)
# اگر برچسب ها یک داغ کدگذاری شده باشند،
# آنها را به مقادیر گسسته تبدیل کنید
if len(y_true.shape) == 2:
y_true = np.argmax(y_true, axis=1)
# کپی کنید تا بتوانیم با خیال راحت تغییر دهیم
self.dinputs = dvalues.copy()
# شیب را محاسبه کنید
self.dinputs[range(samples), y_true] -= 1
# شیب را عادی کنید
self.dinputs = self.dinputs / samples
اکنون می توانیم آزمایش کنیم که آیا گام رو به عقب ترکیبی مقادیر یکسانی را در مقایسه با زمانی که گرادیان ها را از طریق هر دو تابع به طور جداگانه پس انتشار می دهیم، برمی گرداند یا خیر. برای این مثال، بیایید خروجی تابع Softmax و برخی از مقادیر هدف را بسازیم. در مرحله بعد، بیایید آنها را با استفاده از هر دو راه حل پس انتشار کنیم:
import numpy as np
import nnfs
nnfs.init()
softmax_outputs = np.array([[0.7, 0.1, 0.2],
[0.1, 0.5, 0.4],
[0.02, 0.9, 0.08]])
class_targets = np.array([0, 1, 1])
softmax_loss = Activation_Softmax_Loss_CategoricalCrossentropy()
softmax_loss.backward(softmax_outputs, class_targets)
dvalues1 = softmax_loss.dinputs
activation = Activation_Softmax()
activation.output = softmax_outputs
loss = Loss_CategoricalCrossentropy()
loss.backward(softmax_outputs, class_targets)
activation.backward(loss.dinputs)
dvalues2 = activation.dinputs
print(‘Gradients: combined loss and activation:’)
print(dvalues1)
print(‘Gradients: separate loss and activation:’)
print(dvalues2)
>>>
Gradients: combined loss and activation:
[[-0.1 0.03333333 0.06666667]
[ 0.03333333 -0.16666667 0.13333333]
[ 0.00666667 -0.03333333 0.02666667]]
Gradients: separate loss and activation:
[[-0.09999999 0.03333334 0.06666667]
[ 0.03333334 -0.16666667 0.13333334]
[ 0.00666667 -0.03333333 0.02666667]]
نتایج یکسان است. تفاوت کوچک بین مقادیر در هر دو آرایه ناشی از دقت مقادیر ممیز شناور در پایتون و NumPy خام است. برای پاسخ به این سوال که این راه حل چند برابر سریعتر است، می توانیم از ماژول timeit پایتون استفاده کنیم، هر دو راه حل را چندین بار اجرا کنیم و زمان اجرا را ترکیب کنیم. شرح کامل ماژول timeit و کد استفاده شده در اینجا خارج از محدوده این کتاب است، اما ما این کد را صرفا برای نشان دادن دلتاهای سرعت قرار می دهیم:
import numpy as np
from timeit import timeit
import nnfs
nnfs.init()
softmax_outputs = np.array([[0.7, 0.1, 0.2],
[0.1, 0.5, 0.4],
[0.02, 0.9, 0.08]])
class_targets = np.array([0, 1, 1])
def f1():
softmax_loss = Activation_Softmax_Loss_CategoricalCrossentropy()
softmax_loss.backward(softmax_outputs, class_targets)
dvalues1 = softmax_loss.dinputs
def f2():
activation = Activation_Softmax()
activation.output = softmax_outputs
loss = Loss_CategoricalCrossentropy()
loss.backward(softmax_outputs, class_targets)
activation.backward(loss.dinputs)
dvalues2 = activation.dinputs
t1 = timeit(lambda: f1(), number=10000)
t2 = timeit(lambda: f2(), number=10000)
print(t2/t1)
>>>
6.922146504409747
محاسبه شیب ها به طور جداگانه حدود 7 برابر کندتر است. این عامل می تواند از ماشینی به ماشین دیگر متفاوت باشد، اما به وضوح نشان می دهد که ارزش تلاش بیشتری برای محاسبه و کدگذاری راه حل بهینه شده مشتق تابع تلفات و فعال سازی ترکیبی را دارد..
بیایید کد مدل را در نظر بگیریم و کلاس جدید شیء ترکیبی دقت و ضرر را مقداردهی اولیه کنیم:
# از دست دادن و فعال سازی ترکیبی طبقه بندی کننده Softmax را ایجاد کنید
loss_activation = Activation_Softmax_Loss_CategoricalCrossentropy()
Instead of the previous:
# ایجاد فعال سازی Softmax (برای استفاده با لایه Dense):
activation2 = Activation_Softmax()
# ایجاد عملکرد از دست دادن
loss_function = Loss_CategoricalCrossentropy()
Then replace the forward pass calls over these objects:
# عملکرد فعال سازی را از طریق عبور رو به جلو انجام دهید
# خروجی لایه متراکم دوم را در اینجا می گیرد
activation2.forward(dense2.output)
…
# محاسبه تلفات نمونه از خروجی فعال سازی2 (فعال سازی softmax)
loss = loss_function.forward(activation2.output, y)
با فراخوانی پاس رو به جلو روی شی جدید:
# یک عبور رو به جلو از طریق عملکرد فعال سازی/از دست دادن انجام دهید
# خروجی لایه متراکم دوم را در اینجا می گیرد و ضرر را برمی گرداند
loss = loss_activation.forward(dense2.output, y)
و در نهایت مرحله عقب و گرادیان های چاپ را اضافه کنید:
# Backward pass
loss_activation.backward(loss_activation.output, y)
dense2.backward(loss_activation.dinputs)
activation1.backward(dense2.dinputs)
dense1.backward(activation1.dinputs)
# شیب چاپ
print(dense1.dweights)
print(dense1.dbiases)
print(dense2.dweights)
print(dense2.dbiases)
کد مدل کامل:
# مجموعه داده ایجاد کنید
X, y = spiral_data(samples=100, classes=3)
# ایجاد لایه متراکم با 2 ویژگی ورودی و 3 مقدار خروجی
dense1 = Layer_Dense(2, 3)
# ایجاد فعال سازی ReLU (برای استفاده با لایه متراکم):
activation1 = Activation_ReLU()
# ایجاد لایه دوم Dense با 3 ویژگی ورودی (همانطور که خروجی می گیریم)
# لایه قبلی در اینجا) و 3 مقدار خروجی (مقادیر خروجی)
dense2 = Layer_Dense(3, 3)
# از دست دادن و فعال سازی ترکیبی طبقه بندی کننده Softmax را ایجاد کنید
loss_activation = Activation_Softmax_Loss_CategoricalCrossentropy()
# یک پاس رو به جلو از داده های آموزشی خود را از طریق این لایه انجام دهید
dense1.forward(X)
# عملکرد فعال سازی را از طریق عبور رو به جلو انجام دهید
# خروجی اولین لایه متراکم را در اینجا می گیرد
activation1.forward(dense1.output)
# یک عبور رو به جلو از لایه دوم Dense انجام دهید
# خروجی های تابع فعال سازی لایه اول را به عنوان ورودی می گیرد
dense2.forward(activation1.output)
# یک عبور رو به جلو از طریق عملکرد فعال سازی/از دست دادن انجام دهید
# خروجی لایه متراکم دوم را در اینجا می گیرد و ضرر را برمی گرداند
loss = loss_activation.forward(dense2.output, y)
# بیایید خروجی چند نمونه اول را ببینیم:
print(loss_activation.output[:5])
# مقدار از دست دادن چاپ
print(‘loss:’, loss)
# محاسبه دقت از خروجی فعال سازی2 و اهداف
# مقادیر را در امتداد محور اول محاسبه کنید
predictions = np.argmax(loss_activation.output, axis=1)
if len(y.shape) == 2:
y = np.argmax(y, axis=1)
accuracy = np.mean(predictions==y)
# دقت چاپ
print(‘acc:’, accuracy)
# Backward pass
loss_activation.backward(loss_activation.output, y)
dense2.backward(loss_activation.dinputs)
activation1.backward(dense2.dinputs)
dense1.backward(activation1.dinputs)
# شیب چاب
print(dense1.dweights)
print(dense1.dbiases)
print(dense2.dweights)
print(dense2.dbiases)
>>>
[[0.33333334 0.33333334 0.33333334]
[0.33333316 0.3333332 0.33333364]
[0.33333287 0.3333329 0.33333418]
[0.3333326 0.33333263 0.33333477]
[0.33333233 0.3333324 0.33333528]]
loss: 1.0986104
acc: 0.34
[[ 1.5766358e-04 7.8368575e-05 4.7324404e-05]
[ 1.8161036e-04 1.1045571e-05 -3.3096316e-05]]
[[-3.6055347e-04 9.6611722e-05 -1.0367142e-04]]
[[ 5.4410957e-05 1.0741142e-04 -1.6182236e-04]
[-4.0791339e-05 -7.1678100e-05 1.1246944e-04]
[-5.3011299e-05 8.5817286e-05 -3.2805994e-05]]
[[-1.0732794e-05 -9.4590941e-06 2.0027626e-05]]
کد کامل تا این مرحله:
import numpy as np
import nnfs
from nnfs.datasets import spiral_data
nnfs.init()
# لایه متراکم
class Layer_Dense:
# مقداردهی اولیه لایه
def __init__(self, n_inputs, n_neurons):
# وزن ها و biases را مقداردهی اولیه کنید
self.weights = 0.01 * np.random.randn(n_inputs, n_neurons)
self.biases = np.zeros((1, n_neurons))
# Forward pass
def forward(self, inputs):
# مقادیر ورودی را به خاطر بسپارید
self.inputs = inputs
# مقادیر خروجی را از ورودی ها، وزن ها و بایاس ها محاسبه کنید
self.output = np.dot(inputs, self.weights) + self.biases
# Backward pass
def backward(self, dvalues):
# گرادیان در پارامترها
self.dweights = np.dot(self.inputs.T, dvalues)
self.dbiases = np.sum(dvalues, axis=0, keepdims=True)
# گرادیان در مقادیر
self.dinputs = np.dot(dvalues, self.weights.T)
# فعال سازی ReLU
class Activation_ReLU:
# Forward pass
def forward(self, inputs):
# مقادیر ورودی را به خاطر بسپارید
self.inputs = inputs
# مقادیر خروجی را از ورودی ها محاسبه کنید
self.output = np.maximum(0, inputs)
# Backward pass
def backward(self, dvalues):
# از آنجایی که ما نیاز به تغییر متغیر اصلی داریم،
# بیایید ابتدا یک کپی از مقادیر بسازیم
self.dinputs = dvalues.copy()
# گرادیان صفر که در آن مقادیر ورودی منفی بودند
self.dinputs[self.inputs <= 0] = 0
# فعال سازی Softmax
class Activation_Softmax:
# Forward pass
def forward(self, inputs):
# مقادیر ورودی را به خاطر بسپارید
self.inputs = inputs
# احتمالات غیرعادی را دریافت کنید
exp_values = np.exp(inputs – np.max(inputs, axis=1,
keepdims=True))
# آنها را برای هر نمونه عادی کنید
probabilities = exp_values / np.sum(exp_values, axis=1,
keepdims=True)
self.output = probabilities
# Backward pass
def backward(self, dvalues):
# آرایه راه اندازی نشده ایجاد کنید
self.dinputs = np.empty_like(dvalues)
# خروجی ها و گرادیان ها را برشمارید
for index, (single_output, single_dvalues) in \
enumerate(zip(self.output, dvalues)):
# آرایه خروجی را صاف کنید
single_output = single_output.reshape(-1, 1)
# ماتریس Jacobian خروجی را محاسبه کنید و
jacobian_matrix = np.diagflat(single_output) – \
np.dot(single_output, single_output.T)
# گرادیان نمونه را محاسبه کنید
# و آن را به آرایه گرادیان های نمونه اضافه کنید
self.dinputs[index] = np.dot(jacobian_matrix,
single_dvalues)
# کلاس ضرر مشترک
class Loss:
# داده ها و تلفات منظم سازی را محاسبه می کند
# given model output and ground truth values
def calculate(self, output, y):
# تلفات نمونه را محاسبه کنید
sample_losses = self.forward(output, y)
# میانگین ضرر را محاسبه کنید
data_loss = np.mean(sample_losses)
# ضرر برگشتی
return data_loss
# از دست دادن آنتروپی متقاطع
class Loss_CategoricalCrossentropy(Loss):
# Forward pass
def forward(self, y_pred, y_true):
# تعداد نمونه ها در یک دسته
samples = len(y_pred)
# کلیپ داده ها برای جلوگیری از تقسیم بر 0
# هر دو طرف را کلیپ کنید تا میانگین را به سمت هیچ مقداری نکشید
y_pred_clipped = np.clip(y_pred, 1e-7, 1 – 1e-7)
# احتمالات برای مقادیر هدف –
# فقط اگر برچسب های طبقه بندی شده باشد
if len(y_true.shape) == 1:
correct_confidences = y_pred_clipped[
range(samples),
y_true
]
# مقادیر ماسک – فقط برای برچسب های رمزگذاری شده با یک داغ
elif len(y_true.shape) == 2:
correct_confidences = np.sum(
y_pred_clipped * y_true,
axis=1
)
# ضرر و زیان
negative_log_likelihoods = -np.log(correct_confidences)
return negative_log_likelihoods
# Backward pass
def backward(self, dvalues, y_true):
# Number of samples
samples = len(dvalues)
# تعداد برچسب ها در هر نمونه
# ما از اولین نمونه برای شمارش آنها استفاده خواهیم کرد
labels = len(dvalues[0])
# اگر برچسب ها پراکنده هستند، آنها را به یک بردار داغ تبدیل کنید
if len(y_true.shape) == 1:
y_true = np.eye(labels)[y_true]
# شیب را محاسبه کنید
self.dinputs = -y_true / dvalues
# Normalize gradient
self.dinputs = self.dinputs / samples
# طبقه بندی کننده Softmax – فعال سازی ترکیبی Softmax
# و از دست دادن آنتروپی متقاطع برای گام عقب سریعتر
class Activation_Softmax_Loss_CategoricalCrossentropy():
# اشیاء عملکرد فعال سازی و از دست دادن را ایجاد می کند
def __init__(self):
self.activation = Activation_Softmax()
self.loss = Loss_CategoricalCrossentropy()
# Forward pass
def forward(self, inputs, y_true):
# عملکرد فعال سازی لایه خروجی
self.activation.forward(inputs)
# Set the output
self.output = self.activation.output
# مقدار ضرر را محاسبه و برگردانید
return self.loss.calculate(self.output, y_true)
# Backward pass
def backward(self, dvalues, y_true):
# تعداد نمونه ها
samples = len(dvalues)
# اگر برچسب ها یک داغ کدگذاری شده باشند،
# آنها را به مقادیر گسسته تبدیل کنید
if len(y_true.shape) == 2:
y_true = np.argmax(y_true, axis=1)
# کپی کنید تا بتوانیم با خیال راحت تغییر دهیم
self.dinputs = dvalues.copy()
# Calculate gradient
self.dinputs[range(samples), y_true] -= 1
# شیب را عادی کنید
self.dinputs = self.dinputs / samples
# مجموعه داده ایجاد کنید
X, y = spiral_data(samples=100, classes=3)
# ایجاد لایه متراکم با 2 ویژگی ورودی و 3 مقدار خروجی
dense1 = Layer_Dense(2, 3)
# ایجاد فعال سازی ReLU (برای استفاده با لایه متراکم):
activation1 = Activation_ReLU()
# ایجاد لایه دوم Dense با 3 ویژگی ورودی (همانطور که خروجی می گیریم)
# لایه قبلی در اینجا) و 3 مقدار خروجی (مقادیر خروجی)
dense2 = Layer_Dense(3, 3)
# از دست دادن و فعال سازی ترکیبی طبقه بندی کننده Softmax را ایجاد کنید
loss_activation = Activation_Softmax_Loss_CategoricalCrossentropy()
# Perform a forward pass of our training data through this layer
dense1.forward(X)
# عملکرد فعال سازی را از طریق عبور رو به جلو انجام دهید
# خروجی اولین لایه متراکم را در اینجا می گیرد
activation1.forward(dense1.output)
# یک عبور رو به جلو از لایه دوم Dense انجام دهید
# خروجی های تابع فعال سازی لایه اول را به عنوان ورودی می گیرد
dense2.forward(activation1.output)
# یک عبور رو به جلو از طریق عملکرد فعال سازی/از دست دادن انجام دهید
# خروجی لایه متراکم دوم را در اینجا می گیرد و ضرر را برمی گرداند
loss = loss_activation.forward(dense2.output, y)
# بیایید خروجی چند نمونه اول را ببینیم:
print(loss_activation.output[:5])
# مقدار از دست دادن چاپ
print(‘loss:’, loss)
# محاسبه دقت از خروجی فعال سازی2 و اهداف
# مقادیر را در امتداد محور اول محاسبه کنید
predictions = np.argmax(loss_activation.output, axis=1)
if len(y.shape) == 2:
y = np.argmax(y, axis=1)
accuracy = np.mean(predictions==y)
# دقت چاپ
print(‘acc:’, accuracy)
# Backward pass
loss_activation.backward(loss_activation.output, y)
dense2.backward(loss_activation.dinputs)
activation1.backward(dense2.dinputs)
dense1.backward(activation1.dinputs)
# Print gradients
print(dense1.dweights)
print(dense1.dbiases)
print(dense2.dweights)
print(dense2.dbiases)
در این مرحله، به لطف گرادیان ها و پس انتشار با استفاده از قانون زنجیره ای، ما می توانیم وزن ها و بایاس ها را با هدف کاهش تلفات تنظیم کنیم، اما این کار را به روشی بسیار ابتدایی انجام می دهیم. این فرآیند تنظیم وزن ها و بایاس ها با استفاده از شیب ها برای کاهش تلفات وظیفه بهینه ساز است که موضوع فصل بعدی است.
مواد تکمیلی: https://nnfs.io/ch9
کد فصل، منابع بیشتر و اشتباهات این فصل.
درک مشتقات برایم دشوار است. آیا می توانید توضیح دهید که چرا ما مشتق را در اینجا و مشتق جزئی را در عملیات جمع و ضرب می گیریم؟