ג.4 נגזרות ו־Autograd
בפרק המבוא ראינו שאימון רשת פירושו לשנות את המשקלים כדי להקטין את הטעות, ונשארנו עם שאלה פתוחה: לאיזה כיוון לשנות כל משקל? נדמיין שאנו מסובבים כפתור ומודדים את הטעות. אם סיבוב קטן ימינה מגדיל את הטעות, כדאי לסובב שמאלה; אם הוא מקטין אותה, כדאי להמשיך ימינה. מה שאנו צריכים הוא מדד לכך: כמה הטעות משתנה כשמשנים מעט את המשקל. במתמטיקה למדד הזה קוראים נגזרת.
הנגזרת מתארת כיצד ערך הפונקציה משתנה בסביבת נקודה, ולכן יכולה להנחות אותנו בחיפוש מינימום. אם נדע לחשב את נגזרת הטעות לפי כל משקל, נדע לאיזה כיוון להזיז כל אחד מהם, וזה בדיוק מה שנעשה בפרקים הבאים כשנרד בצעדים קטנים אל המינימום של פונקציית הטעות. ברשת אמיתית יש אלפי משקלים ואלפי נגזרות לחשב, ולכן איננו רוצים לגזור ביד; PyTorch עושה זאת בשבילנו במנגנון הנקרא Autograd.
הפרק בנוי בשני חלקים. תחילה נרענן את המתמטיקה: נלמד כללי גזירה, נגזרות חלקיות וכלל השרשרת, ונבין כיצד פירוק חישוב לצעדים קטנים מאפשר לגזור פונקציה מורכבת. לאחר מכן נראה כיצד PyTorch מחשבת נגזרות באמצעות גרף חישוב, ונלמד את כללי השימוש במנגנון: איך מפעילים אותו, איך קוראים את התוצאה, ומתי צריך לכבות אותו או לאפס אותו.
השיעור וההרצאות באתר של גלעד מרקמן
חומרי הליווי: 3. PyTorch Autograd (עותק מקומי) · מחברת Autograd (עותק מקומי)
מהי נגזרת?
נתחיל בפונקציה של משתנה אחד, כמו אלה שמכירים מבית הספר, ונבין מה הנגזרת אומרת לנו על הגרף. הנגזרת בנקודה מתארת את שיפוע הישר המשיק לגרף באותה נקודה. נגזרת חיובית מצביעה על עלייה, ונגזרת שלילית על ירידה, וככל שהערך המוחלט שלה גדול יותר, הגרף תלול יותר. בנקודת מינימום או מקסימום פנימית שבה הפונקציה גזירה, הנגזרת מתאפסת; נגזרת אפס לבדה אינה מבטיחה מינימום. שלוש העובדות האלה — סימן הנגזרת, גודלה והתאפסותה בנקודת קיצון — הן כל מה שנצטרך כדי לחפש מינימום בפרק הבא.
השתנות הנגזרת בהתאם לשיפוע הגרף
חשוב להבין שהנגזרת אינה מספר אחד לפונקציה כולה, אלא פונקציה בפני עצמה. השיפוע משתנה לאורך הגרף, ולכן גם ערך הנגזרת משתנה. למשל, עבור הפונקציה f(x) = x·sin(x²) + 1 מוצגת הנגזרת f′(x) = sin(x²) + 2x²·cos(x²). בכל נקודה ערכה קובע את שיפוע המשיק שם. לכן כשנחפש מינימום נצטרך לחשב את הנגזרת מחדש בכל נקודה שאליה נגיע.
חישוב נגזרת
איך מוצאים את הנגזרת? אין צורך לחשב שיפועים של משיקים בעין. כללי גזירה מאפשרים לקבל נוסחה לנגזרת מתוך נוסחת הפונקציה. נסתפק כאן בכללים הבסיסיים, שמספיקים לפולינומים; אלה גם הכללים ש־Autograd מפעיל מאחורי הקלעים.
נסמן את הנגזרת של f(x) ב־f′(x). נשתמש בכללים הבאים:
| פונקציה | נגזרת |
|---|---|
| קבוע, למשל f(x)=5 | f′(x)=0 |
| f(x)=x | f′(x)=1 |
| כפל בקבוע: f(x)=k·g(x) | f′(x)=k·g′(x) |
| סכום: f(x)=g(x)+h(x) | f′(x)=g′(x)+h′(x) |
| חזקה: f(x)=xⁿ | f′(x)=n·xⁿ⁻¹ |
לדוגמה, הנגזרת של x⁵ היא 5x⁴. בשילוב הכללים אפשר לגזור כל פולינום איבר־איבר: גוזרים כל חזקה בנפרד, כופלים במקדם ומחברים.
תרגיל
נתרגל את הכללים על פולינום קצר, ונשים לב במיוחד לסעיף האחרון, שבו הנגזרת משמשת למציאת מינימום. נתונה הפונקציה f(x)=3x²+6x.
- חשבו את נוסחת הנגזרת.
- חשבו את הנגזרת בנקודות x=0, x=2, x=−2.
- קבעו בכל אחת מהנקודות אם הפונקציה עולה או יורדת.
- מצאו את נקודת המינימום של הפונקציה.
נגזרת חלקית — פונקציה במספר משתנים
הטעות של רשת נוירונים אינה תלויה במשקל אחד אלא בכולם יחד, ולכן היא פונקציה של משתנים רבים. איך גוזרים פונקציה כזו? מסובבים כפתור אחד בלבד ומחזיקים את כל האחרים במקומם. בפונקציה בעלת כמה משתנים אפשר לגזור ביחס לכל אחד מהם. בנגזרת חלקית מתייחסים לשאר המשתנים כאל קבועים. התוצאה היא נגזרת אחת לכל משתנה, וכל אחת מהן אומרת לנו כמה הפונקציה משתנה כשמזיזים רק את המשתנה הזה.
עבור f(x,y)=3x²+2y³+4xy נקבל:
- לפי x: ∂f/∂x=6x+4y. האיבר 2y³ נחשב קבוע ולכן נגזרתו אפס.
- לפי y: ∂f/∂y=6y²+4x. הפעם 3x² נחשב קבוע.
אוסף הנגזרות החלקיות של פונקציה, אחת לכל משתנה, נקרא גרדיאנט — Gradient. זהו המונח שממנו נגזר שמו של Autograd, ושל האלגוריתם שנלמד בפרק הבא.
סימונים לנגזרת
בספרים ובתיעוד של PyTorch נפגוש כמה כתיבים לאותו רעיון, וכדאי להכיר את כולם. בפונקציה של משתנה אחד אפשר לכתוב f′(x) או df/dx. בפונקציה של כמה משתנים נכתוב ∂f/∂x או ∂f/∂y כדי להבהיר לפי איזה משתנה גוזרים. המנה Δf/Δx מתארת שינוי על פני קטע; הנגזרת מתקבלת בגבול כאשר השינוי בקלט שואף לאפס.
כלל השרשרת
רשת נוירונים היא פונקציה מורכבת: הפלט של שכבה אחת נכנס לשכבה הבאה, וזו שוב לבאה אחריה, ובסוף מחשבים מהכול את הטעות. כדי לגזור שרשרת כזו של פונקציות צריך כלל אחד נוסף, והוא הבסיס לכל מנגנון Autograd. כלל השרשרת מאפשר לגזור פונקציה מורכבת. נפרק את y=(3x²+2)³ לשני שלבים:
- v=3x²+2, ולכן dv/dx=6x.
- y=v³, ולכן dy/dv=3v².
נכפול את הנגזרות:
dy/dx = (dy/dv)·(dv/dx) = 3v²·6x = 18x(3x²+2)²
כלומר, גוזרים את הפונקציה החיצונית וכופלים בנגזרת הפנימית. הכתיב (dy/dv)·(dv/dx) מזכיר צמצום של שברים, אך הוא מבטא את כלל השרשרת.
גרף חישוב
כלל השרשרת נותן לנו שיטה מכנית: כל חישוב, מסובך ככל שיהיה, אפשר לפרק לפעולות בסיסיות שאת הנגזרת של כל אחת מהן קל לחשב, ואז לכפול את הנגזרות לאורך הדרך. את הפירוק הזה נוח לצייר כתרשים שבו כל צומת הוא פעולה אחת; לתרשים כזה קוראים גרף חישוב — Computational Graph, וזה בדיוק מה ש־PyTorch בונה מאחורי הקלעים. אפשר לפרק חישוב לפונקציות קטנות ולהשתמש בכלל השרשרת כדי לחשב את הנגזרת. עבור L=(4x−1)² נחשב תחילה ŷ=4x, ואחר כך L=(ŷ−1)².
הסימונים כאן אינם מקריים: ŷ הוא הסימון המקובל לפלט של מודל, ו־L הוא ההפסד, כלומר הטעות. הדוגמה הקטנה הזו היא למעשה תבנית של אימון: מחשבים פלט, מודדים את הטעות, וגוזרים אותה לפי הפרמטר. בחישוב קדימה — Forward — נציב x=1: נקבל ŷ=4 ולבסוף L=9. בחישוב לאחור — Backward — נחשב את הנגזרות המקומיות: dL/dŷ=6 ו־dŷ/dx=4. מכפלתן נותנת dL/dx=24. החישוב לאחור מתחיל מהתוצאה הסופית וחוזר צעד־צעד אל הקלט, כשבכל צעד כופלים בנגזרת המקומית של אותו צומת; מכאן השם.
תרגילים
בתרגילים הבאים חזרו על חישוב הגרף ביד, ובכל פעם שאלו את עצמכם מה סימן הנגזרת וגודלה אומרים על הכיוון שבו כדאי להזיז את x כדי להקטין את L.
- מה אפשר ללמוד מהנגזרת שחישבנו על הפונקציה L בנקודה x=1?
- בנו גרף חישוב עבור L=(3x−1)², חשבו את הנגזרת ב־x=1 ופרשו אותה.
- חזרו על הפעולה עבור L=(x−1)² בנקודה x=1.
הפעלת Autograd
עברנו לחלק המעשי. את כל מה שעשינו ביד בסעיף הקודם — פירוק לפעולות, חישוב קדימה, חישוב לאחור עם כלל השרשרת — PyTorch יודעת לעשות בעצמה על כל חישוב שנכתוב בטנסורים. ל־PyTorch יש מנגנון לחישוב נגזרות אוטומטי. נייבא את הספריות:
import torch
import numpy as np
המעקב אחרי החישוב עולה בזיכרון ובזמן, ולכן PyTorch אינה עוקבת אחרי כל טנסור אלא רק אחרי אלה שסימנו. באימון אלה יהיו המשקלים; כאן זהו x. כדי לעקוב אחר המשתנה שלפיו נגזור, נגדיר requires_grad=True.
x = torch.tensor([1], dtype=torch.float, requires_grad=True)
print (x)
פלט
tensor([1.], requires_grad=True)
מרגע הסימון, כל פעולה שנבצע על x, ועל התוצאות שנובעות ממנו, נרשמת. נבנה את אותו חישוב (4x−1)² מהסעיף על גרף החישוב, בשלושה צעדים נפרדים. הפעולות על הטנסור נרשמות בגרף חישוב:
v = 4 * x
z = v - 1
w = z ** 2
print (v)
print (z)
print (w)
פלט
tensor([4.], grad_fn=<MulBackward0>)
tensor([3.], grad_fn=<SubBackward0>)
tensor([9.], grad_fn=<PowBackward0>)
המאפיין grad_fn מציין את הפעולה שיצרה את התוצאה: MulBackward0 לכפל, SubBackward0 לחיסור ו־PowBackward0 לחזקה. אלה בדיוק הצמתים של גרף החישוב, וכל אחד מהם יודע לחשב את הנגזרת המקומית שלו. בשלב זה נבנה הגרף; הנגזרת המספרית תחושב בחישוב לאחור.
חישוב נגזרת באמצעות Autograd
כעת נסגור את המעגל ונקבל מ־PyTorch את המספר עצמו. כמו בגרף החישוב שציירנו, החישוב נעשה בשני שלבים: תחילה מחשבים את ערך הפונקציה, ולאחר מכן מפעילים backward() לחישוב הנגזרת. התוצאה אינה מוחזרת מהפונקציה אלא נכתבת לשדה grad של הטנסור שסומן למעקב. הפעם נציב x=2, ובהערה שבקוד מופיעה נוסחת הנגזרת שחישבנו ביד, כדי שנוכל להשוות.
x = torch.tensor([2.0], requires_grad=True)
#forward
l = (4*x-1)**2 # l' = 2(4x-1)4 = 8(4x-1)
print (l)
print(x.grad)
# backwrad
l.backward()
print (x.grad)
פלט
tensor([49.], grad_fn=<PowBackward0>)
None
tensor([56.])
לפני backward() מופיע None בשדה x.grad; לאחר הקריאה נשמרת בו הנגזרת. הערך 56 מתאים לנוסחה: 8(4·2−1)=56. אפשר גם להגדיר את החישוב בתוך פונקציה, ואז נחזור לנקודה x=1 מגרף החישוב ונצפה לקבל 24:
def F (x):
return (4*x-1)**2
x = torch.tensor([1.0], requires_grad=True)
#forward
f = F(x)
print(f)
#backward
f.backward()
print(x.grad)
פלט
tensor([9.], grad_fn=<PowBackward0>)
tensor([24.])
חישוב נגזרת חלקית במספר משתנים
כשהחישוב תלוי בכמה משתנים, נוכל לבקש נגזרת ביחס לכל אחד מהם. נחליף את הקבועים בפונקציה במשתנים ונחשב את Loss=(wx−y)². השמות נבחרו בכוונה: x הוא הקלט, w הוא המשקל, y היא התשובה הרצויה, ו־wx הוא הפלט של "רשת" בת נוירון יחיד ללא אקטיבציה. ריבוע ההפרש בין הפלט לתשובה הרצויה הוא הטעות. מבין שלושת המשתנים, המשקל w הוא זה שנרצה לשנות באימון, ולכן הנגזרת לפיו היא המעניינת. את x נגדיר ללא מעקב, כמו שנעשה עם נתוני קלט אמיתיים.
x = torch.tensor([1.0], requires_grad=False)
w = torch.tensor([3.0], requires_grad=True)
y = torch.tensor([4.0], requires_grad=True)
def Loss(w, x, y):
return (w * x - y) ** 2 #torch.sin
#forward
loss = Loss(w, x, y)
print(f"loss: {loss}")
פלט
loss: tensor([1.], grad_fn=<PowBackward0>)
הפלט של הרשת הוא 3 והתשובה הרצויה 4, ולכן הטעות היא 1. הנגזרת לפי w היא 2(wx−y)x, ובנקודה הנתונה ערכה −2. הנגזרת שלילית, כלומר הגדלת w תקטין את הטעות, וזה הגיוני: הפלט 3 קטן מהתשובה 4.
#backward
loss.backward()
print (f"loss'(x) (1,3,4) = {x.grad}")
print (f"loss'(w) (1,3,4) = {w.grad}")
print (f"loss'(y) (1,3,4) = {y.grad}")
פלט
loss'(x) (1,3,4) = None
loss'(w) (1,3,4) = tensor([-2.])
loss'(y) (1,3,4) = tensor([2.])
עבור x מתקבל None מפני שהוגדר ללא מעקב; אין פירוש הדבר שהנגזרת המתמטית שלו היא אפס. קריאה אחת ל־backward() חישבה את הנגזרות לפי כל המשתנים המסומנים בבת אחת; כך יהיה גם ברשת עם אלפי משקלים.
נגזרת במספר נקודות בפונקציה המחזירה סקלר
באימון אמיתי איננו מודדים את הטעות על דוגמה אחת אלא על אוסף של דוגמאות, ומסכמים את הטעויות למספר אחד. לכן x, w ו־y יהיו עכשיו טנסורים בני כמה איברים, וההפסד יהיה סכום. פונקציה יכולה לקבל טנסור של ערכים ולהחזיר מספר יחיד, למשל סכום או ממוצע. בדוגמה הבאה נסכום את ריבועי ההפרשים:
x = torch.tensor([1,2,3,4], dtype=torch.float32, requires_grad=True)
w = torch.tensor([4.0, 4.5, 2.0, 3.0], requires_grad=True)
y = torch.tensor([1.5,3,4.5,6])
L = ((w * x - y) ** 2).sum()
print (L)
פלט
tensor(80.5000, grad_fn=<SumBackward0>)
נחשב את הנגזרות לפי רכיבי x ולפי רכיבי w:
L.backward()
print(x, w, y, L)
print (f"loss'(x) = {x.grad}")
print (f"loss'(w) = {w.grad}")
פלט
tensor([1., 2., 3., 4.], requires_grad=True) tensor([4.0000, 4.5000, 2.0000, 3.0000], requires_grad=True) tensor([1.5000, 3.0000, 4.5000, 6.0000]) tensor(80.5000, grad_fn=<SumBackward0>)
loss'(x) = tensor([20., 54., 6., 36.])
loss'(w) = tensor([ 5., 24., 9., 48.])
הנגזרת של טנסור היא טנסור באותה צורה: לכל איבר ב־x ולכל איבר ב־w מתקבלת נגזרת חלקית משלו, המתארת כמה ההפסד הכולל L משתנה כשמזיזים רק את האיבר הזה. לרכיב הראשון מתקבלים ∂L/∂x₁=20 ו־∂L/∂w₁=5.
נגזרת במספר נקודות בפונקציה המחזירה ערכים מרובים
backward() יודעת לפעול ישירות רק על תוצאה שהיא מספר יחיד, מפני שרק אז ברור מה גוזרים. מה עושים כשהתוצאה היא וקטור? אפשר לחשב נגזרות גם כאשר מתקבלות כמה תוצאות נפרדות. כעת נשאיר את ריבועי ההפרשים כווקטור, ללא סכימה:
x = torch.tensor([1,2,3,4], dtype=torch.float32, requires_grad=True)
w = torch.tensor([4.0, 4.0, 4.0, 4.0], requires_grad=True)
y = torch.tensor([1.5,3,4.5,6])
#forward
l = (w * x - y) ** 2
print (l)
פלט
tensor([ 6.2500, 25.0000, 56.2500, 100.0000], grad_fn=<PowBackward0>)
כאשר הפלט מכיל כמה איברים, נעביר ל־backward וקטור באותה צורה כמו הפלט. הווקטור אומר ל־PyTorch באיזה משקל לצרף כל רכיב של הפלט לחישוב. כאן נבחר וקטור אחדות, השקול לחישוב נגזרת סכום הרכיבים; זו הבחירה הנפוצה, וכך נשתמש בה גם בתרגילים בסוף הפרק.
v = torch.tensor([1.0,1.0,1.0,1.0])
# backward
l.backward(v)
print (f"loss'(x) = {x.grad}")
print (f"loss'(w) = {w.grad}")
פלט
loss'(x) = tensor([20., 40., 60., 80.])
loss'(w) = tensor([ 5., 20., 45., 80.])
ברכיב הראשון, עבור w=4 ו־y=1.5, הנגזרת לפי x היא 8(4x−1.5), ולכן ב־x=1 מתקבל 20. עבור x=1 ו־y=1.5, הנגזרת לפי w היא 2(w−1.5), ולכן ב־w=4 מתקבל 5.
ביטול Autograd
למדנו להפעיל את המנגנון; עכשיו נלמד שלושה כללי זהירות שבלעדיהם קוד האימון ייתן תוצאות שגויות: הנגזרות נצברות, טנסור במעקב אינו ניתן להמרה ישירה ל־NumPy, ולעיתים צריך לבצע חישוב בלי שיירשם בגרף. נתחיל בצבירה. בכל קריאה ל־backward() הנגזרות מתווספות לערכים שכבר נצברו בשדה grad, במקום להחליף אותם.
x = torch.tensor([1.0], requires_grad=True)
l = (4*x-1)**2
print (l)
l.backward()
print ('dL/dx(1) = ', x.grad)
y = x**2
print (y)
y.backward()
print ('Accumulated x.grad = ', x.grad) # Y'=2X , Y'(1) = 2
פלט
tensor([9.], grad_fn=<PowBackward0>)
dL/dx(1) = tensor([24.])
tensor([1.], grad_fn=<PowBackward0>)
Accumulated x.grad = tensor([26.])
אחרי החישוב הראשון נצבר 24. הנגזרת של y=x² ב־x=1 היא 2, ולכן לאחר החישוב השני השדה מכיל 26, ולא 2. באימון, שבו קוראים ל־backward() שוב ושוב, צבירה כזו תשבש את הכיוון של כל צעד, ולכן נצטרך לאפס את השדה; נראה זאת בסעיף "איפוס הגרדיאנטים".
הכלל השני נוגע להמרה ל־NumPy, שלמדנו בפרק הקודם. אי אפשר להמיר ישירות ל־NumPy טנסור שדורש מעקב נגזרות, מפני שמערך NumPy אינו יכול להשתתף בגרף החישוב. השורה הבאה מדגימה פעולה שתגרום לשגיאה:
x_np = x.numpy()
יש שתי דרכים לקבל מטנסור כזה טנסור רגיל שאפשר להמיר. אפשר לבטל את המעקב בטנסור עצמו:
x = torch.tensor([1.0], requires_grad=True)
print(x)
x.requires_grad_(False)
print(x)
פלט
tensor([1.], requires_grad=True)
tensor([1.])
אפשר גם לקבל טנסור מנותק באמצעות detach():
x = torch.tensor([1.0], requires_grad=True)
print(x)
y = x.detach()
print(y)
print(x)
פלט
tensor([1.], requires_grad=True)
tensor([1.])
tensor([1.], requires_grad=True)
detach() מנתקת את המעקב, אך הטנסור המוחזר חולק את הנתונים בזיכרון עם המקור. ההבדל בין השתיים: requires_grad_(False) משנה את הטנסור עצמו ומפסיקה את המעקב אחריו לתמיד, ואילו detach() משאירה את המקור במעקב ונותנת עותק מנותק לשימוש צדדי, למשל לציור גרף.
השהיית Autograd
הכלל השלישי: לפעמים נרצה להשאיר את הטנסור במעקב, אך לבצע עליו פעולה מסוימת בלי שתירשם בגרף. הדוגמה החשובה ביותר תגיע בפרק הבא: עדכון המשקלים בעצמו אינו חלק מהחישוב שגוזרים, ולכן מבצעים אותו מחוץ למעקב. ההקשר with torch.no_grad(): משהה את המעקב אחר חישובים שבתוכו. בדוגמה הבאה אפשר לבצע את ההמרה ל־NumPy בתוך ההקשר:
x = torch.tensor([1.0], requires_grad=True)
with torch.no_grad():
x_np = x.numpy()
print (x_np)
print(x)
# np = np.array([1.0])
# print(np)
פלט
[1.]
tensor([1.], requires_grad=True)
לאחר היציאה מההקשר, הדגל requires_grad של x נשאר פעיל.
איפוס הגרדיאנטים
נחזור לבעיית הצבירה ונראה את הפתרון בתוך לולאה, בדומה ללולאת האימון שנכתוב בפרק הבא. כדי לחשב נגזרת חדשה בלי לצבור את הקודמת, נאפס את השדה בעזרת zero_(). בדוגמה W הוא טנסור של ארבעה "משקלים", ובכל חזרה של הלולאה מחשבים קדימה, גוזרים לאחור, מדפיסים את הגרדיאנט ומאפסים אותו. המשתנה epoch הוא השם המקובל למספר החזרה; אפוק — epoch הוא מעבר אחד על הנתונים בזמן אימון, ובפרקים הבאים נשתמש בשם הזה שוב ושוב.
W = torch.tensor([2.0, 3, 5, 8], requires_grad=True)
for epoch in range(3):
#forward
model = (W * 3).mean()
print(model)
#backward
model.backward()
print (W.grad)
W.grad.zero_()
פלט
tensor(13.5000, grad_fn=<MeanBackward0>)
tensor([0.7500, 0.7500, 0.7500, 0.7500])
tensor(13.5000, grad_fn=<MeanBackward0>)
tensor([0.7500, 0.7500, 0.7500, 0.7500])
tensor(13.5000, grad_fn=<MeanBackward0>)
tensor([0.7500, 0.7500, 0.7500, 0.7500])
הממוצע של ארבעה איברים שכל אחד מהם הוא 3W נגזר לפי כל רכיב ל־3/4=0.75. בלי שורת האיפוס היו מתקבלים ברכיבי הגרדיאנט 0.75, אחר כך 1.5 ולבסוף 2.25. עם האיפוס מתקבל בכל חזרה 0.75, כפי שרואים בפלט. שימו לב שהאיפוס נעשה במקום, בפעולת in-place עם קו תחתון, כפי שלמדנו בפרק הקודם.
תרגילים
בתרגילים הבאים נחשב נגזרות של פונקציות בכמה נקודות בבת אחת, בעזרת הטכניקה של וקטור האחדות שלמדנו. חשבו באמצעות PyTorch את הנגזרות הבאות, לפי הסדר:
- y=3x²+0.5x+5, בנקודות −3, 1, 4, 9.
- הנגזרות לפי W של loss=mean((W·X−Y)²), כאשר W=[−0.5,1,2,3], X=[1,2,3,4], Y=[2,4,6,8].
- f=2x³−3x², בנקודות −2, 0, 0.5, 1, 2. מה אפשר ללמוד מהנגזרות?
התרגיל הראשון: ארבע הנקודות מוכנסות לטנסור אחד, ולכן החישוב קדימה מחזיר ארבעה ערכים, וכדי לגזור נעביר ל־backward וקטור אחדות באורך 4.
x = torch.tensor([-3,1,4,9], dtype=torch.float32, requires_grad=True)
y = 3 * x**2 + 0.5 * x + 5 # forward
print(y)
פלט
tensor([ 30.5000, 8.5000, 55.0000, 252.5000], grad_fn=<AddBackward0>)
v = torch.ones(4)
y.backward(v)
print(x.grad )
פלט
tensor([-17.5000, 6.5000, 24.5000, 54.5000])
הנגזרת של הפונקציה היא 6x+0.5; בנקודה −3 היא שלילית, כלומר הפונקציה יורדת שם, ובשאר הנקודות היא חיובית.
התרגיל השני: כאן ההפסד הוא ממוצע של ארבעה ריבועי הפרשים, כלומר מספר יחיד, ולכן backward() נקראת ללא וקטור. שימו לב שהתשובה הרצויה Y היא בדיוק 2X.
W = torch.tensor ([-0.5,1,2,3], dtype=torch.float32, requires_grad=True)
X = torch.tensor ([1,2,3,4])
Y = torch.tensor ([2,4,6,8])
loss = ((W*X-Y)**2).mean() # forward
print(loss)
loss.backward()
print(W.grad)
פלט
tensor(6.5625, grad_fn=<MeanBackward0>)
tensor([-1.2500, -2.0000, 0.0000, 8.0000])
הרכיב השלישי של W הוא 2, בדיוק הערך שמביא את W·X להיות שווה ל־Y, ולכן הנגזרת לפיו היא 0: אין טעם להזיז אותו. הרכיבים הראשון והשני קטנים מ־2 ונגזרתם שלילית, כלומר צריך להגדיל אותם; הרביעי גדול מ־2 ונגזרתו חיובית, כלומר צריך להקטין אותו. זהו בדיוק המידע שאלגוריתם האימון יזדקק לו.
התרגיל השלישי:
x = torch.tensor([-2,0,0.5,1,2], dtype=torch.float32, requires_grad=True)
f = 2*x**3 - 3 * x **2 # forward
v = torch.ones(5)
print (f)
f.backward(v)
print (x.grad)
פלט
tensor([-28.0000, 0.0000, -0.5000, -1.0000, 4.0000],
grad_fn=<SubBackward0>)
tensor([36.0000, 0.0000, -1.5000, 0.0000, 12.0000])
הנגזרת מתאפסת בשתי נקודות, 0 ו־1. בין הנקודות האלה, ב־0.5, הנגזרת שלילית: הפונקציה יורדת. מחוץ להן, ב־−2 וב־2, הנגזרת חיובית: הפונקציה עולה. מכאן ש־0 היא נקודת מקסימום מקומי ו־1 היא נקודת מינימום מקומי. זו דוגמה לכך שנגזרת אפס לבדה אינה אומרת לנו אם הגענו למינימום, כפי שציינו בתחילת הפרק.
נגזרות חלקיות — דוגמה נוספת: נסיים בשתי דוגמאות של נגזרות חלקיות, שבהן כל משתנה הוא טנסור נפרד במעקב ו־backward() ממלאת את השדה grad של כל אחד מהם. f(x₁,x₂)=3x₁²+2(x₂+2)³, בנקודה (1,2).
x1 = torch.tensor([1.0], requires_grad=True)
x2 = torch.tensor([2.0], requires_grad=True)
#forward
f = 3*x1**2 + 2*(x2 +2)**3
print(f)
#backward
f.backward()
print(x1.grad, x2.grad)
פלט
tensor([131.], grad_fn=<AddBackward0>)
tensor([6.]) tensor([96.])
אפשר לבדוק ביד: לפי x₁ הנגזרת היא 6x₁, ובנקודה 1 מתקבל 6; לפי x₂ הנגזרת היא 6(x₂+2)², ובנקודה 2 מתקבל 96.
נגזרות חלקיות בשלושה משתנים: f(x₁,x₂,x₃)=(3x₁+x₂)²(x₃+5), בנקודה (2,1,3).
def F (x1, x2, x3):
return (3*x1 + x2)**2*(x3 + 5)
#forward
x1 = torch.tensor([2.0], requires_grad=True)
x2 = torch.tensor([1.0], requires_grad=True)
x3 = torch.tensor([3.0], requires_grad=True)
f = F(x1, x2, x3)
print(f)
#backward
f.backward()
print(x1.grad, x2.grad, x3.grad)
פלט
tensor([392.], grad_fn=<MulBackward0>)
tensor([336.]) tensor([112.]) tensor([49.])
יש לנו עתה את שני הכלים הדרושים לאימון: טנסורים שמחזיקים את הנתונים והמשקלים, ו־Autograd שמחשב לכל משקל את נגזרת הטעות לפיו. בפרק הבא נחבר אותם לאלגוריתם שמשתמש בנגזרות כדי לרדת, צעד אחר צעד, אל המינימום של פונקציית ההפסד.