ג.3 טנסורים ב־PyTorch

בפרק המבוא ראינו שרשת נוירונים היא בסופו של דבר אוסף גדול של מספרים: הקלטים X, המשקלים W והפלטים Y. תמונה של חתול היא טבלה של ערכי פיקסלים; המשקלים של שכבה ברשת הם טבלה של מספרים, אחד לכל חיבור; ואוסף של אלף תמונות אימון הוא כבר "טבלה של טבלאות". כל החישוב שהרשת מבצעת — הכפלת קלטים במשקלים, חיבור, הפעלת אקטיבציה — הוא פעולות חשבון על המערכים האלה, ובאימון גם המשקלים עצמם מתעדכנים שוב ושוב.

לכן כלי הבסיס שלנו צריך להיות מבנה נתונים שמחזיק מערכים רב־ממדיים של מספרים ומבצע עליהם חישובים במהירות. בחלק א הכרנו מערכים כאלה ב־NumPy. ב־PyTorch המבנה המקביל נקרא טנסור, והוא דומה מאוד למערך NumPy אך מוסיף שתי יכולות שחסרות לו ושחיוניות לאימון רשתות: אפשרות להריץ את החישוב על GPU, ומעקב אוטומטי אחרי החישוב לצורך חישוב נגזרות. את המשמעות המלאה של הנגזרות נבין בפרקים הבאים; כאן נלמד קודם כל להחזיק את המספרים ולעבוד איתם.

ספריית PyTorch מבוססת על טנסורים — Tensors, מבני נתונים המיועדים ללמידת מכונה. טנסור הוא מערך בעל מספר ממדים, הכולל יכולות נוספות המיוחדות ללמידת מכונה:

  • חישובים מתמטיים מהירים על מערכים בעלי מספר ממדים.
  • שימוש ב־GPU לביצוע חישובים במקביל ולקיצור זמני החישוב.
  • חישוב נגזרות — Gradients — באופן אוטומטי באמצעות autograd.
  • שימוש בטנסורים לבניית רשתות נוירונים עמוקות באמצעות torch.nn.

בפרק זה נלמד את יסודות השימוש בטנסורים: כיצד יוצרים אותם, מהם המאפיינים שלהם, איך ניגשים לאיברים ומבצעים חישובים, ואיך משנים את צורתם. רבות מהפעולות יזכירו את NumPy, וזה לא מקרה; מי שמכיר את NumPy ירגיש כאן בבית.

השיעור וההרצאות באתר של גלעד מרקמן

חומרי הליווי: 2. PyTorch Tensor (עותק מקומי) · מחברת הטנסורים (עותק מקומי)

נייבא את PyTorch ואת NumPy. את NumPy נצטרך כדי להראות את המעבר בין שני סוגי המערכים:

import torch
import numpy as np

יצירת טנסורים

הצעד הראשון הוא להכניס מספרים לטנסור. בעבודה אמיתית הנתונים מגיעים בדרך כלל מבחוץ, למשל מקובץ או מתמונה, ואילו המשקלים של הרשת מתחילים בערכים אקראיים. לכן נלמד שתי דרכים: ניצור טנסורים מתוך נתונים קיימים או באמצעות ערכים שנקבע מראש. מרשימה: נגדיר רשימה של שתי שורות ושתי עמודות ונעביר אותה ל־torch.Tensor.

data = [[1,2], [3,4]]
x = torch.Tensor(data)
print (x)

פלט

tensor([[1., 2.],
        [3., 4.]])

האות T ב־Tensor גדולה. בדרך יצירה זו מתקבלים כאן מספרים בנקודה צפה, ולכן מופיעה נקודה אחרי כל מספר. מספרים בנקודה צפה הם ברירת המחדל הטבעית בלמידת מכונה, מפני שמשקלים ותוצאות ביניים כמעט אף פעם אינם שלמים.

ממערך NumPy: ניצור מערך מאותם נתונים ונמיר אותו לטנסור באמצעות torch.from_numpy.

np_array = np.array(data)
x_t = torch.from_numpy(np_array)
print(x_t)

פלט

tensor([[1, 2],
        [3, 4]])

כאן הנתונים נשארים מספרים שלמים. בהמשך הפרק נראה גם את הקשר בין הזיכרון של מערך NumPy לזה של הטנסור.

יצירה עם ערכים: לעיתים קרובות נרצה טנסור בגודל נתון בלי להקליד את כל האיברים, למשל טנסור משקלים אקראיים או טנסור אפסים שיאסוף תוצאות. המשתנה shape קובע את הצורה — שתי שורות ושלוש עמודות — וכל אחת מהפונקציות הבאות ממלאת צורה כזו בדרך אחרת.

  • rand יוצרת ערכים אקראיים בין 0 ל־1, ללא 1.
  • randint יוצרת שלמים אקראיים; בדוגמה מ־0 עד 8.
  • ones ו־zeros ממלאות את הטנסור באחדות ובאפסים.
  • full ממלאת אותו בערך שנבחר.
  • arange(10) יוצרת את הרצף מ־0 עד 9.
shape = (2,3)
rand_tensor = torch.rand(shape)
rand_int_tensor = torch.randint(0,9,shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
full_tensor = torch.full((2, 3), 0.5)
range_tensor = torch.arange(10)
print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Random int Tensor: \n {rand_int_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor} \n")
print(f"full_tensor: \n {full_tensor} \n")
print(f"Range tensor: \n {range_tensor} \n")

הערכים האקראיים בפלט הם דוגמה; הם עשויים להשתנות בכל הרצה.

פלט

Random Tensor: 
 tensor([[0.1126, 0.8180, 0.6814],
        [0.3566, 0.8625, 0.2720]]) 

Random int Tensor: 
 tensor([[1, 2, 5],
        [2, 0, 2]]) 

Ones Tensor: 
 tensor([[1., 1., 1.],
        [1., 1., 1.]]) 

Zeros Tensor: 
 tensor([[0., 0., 0.],
        [0., 0., 0.]]) 

full_tensor: 
 tensor([[0.5000, 0.5000, 0.5000],
        [0.5000, 0.5000, 0.5000]]) 

Range tensor: 
 tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

טנסור ללא אתחול: כשמתכוונים למלא את כל האיברים מיד לאחר היצירה, אין טעם לבזבז זמן על מילוי באפסים. empty מקצה מקום לטנסור, אך אינה קובעת את ערכי האיברים. בדוגמה הטנסור תלת־ממדי: שני "לוחות", בכל אחד שתי שורות ושלוש עמודות.

empty_tensor = torch.empty(2,2,3)
print(f"empty_tensor: \n {empty_tensor} \n")

זהו פלט אפשרי בלבד. גם אם מופיעים אפסים, אין להסתמך עליהם: הערכים בזיכרון לא אותחלו.

פלט

empty_tensor: 
 tensor([[[0., 0., 0.],
         [0., 0., 0.]],

        [[0., 0., 0.],
         [0., 0., 0.]]])

המאפיינים של טנסור

כשמקבלים טנסור שלא אנחנו יצרנו, למשל פלט של שכבה ברשת, השאלות הראשונות הן: מה הצורה שלו, מאיזה סוג המספרים, והיכן הוא נמצא. שלושה מאפיינים מתארים את הטנסור:

  • shape — המבנה שלו: מספר האיברים בכל ממד.
  • dtype — סוג המספרים שהוא מכיל.
  • device — ההתקן שבזיכרון שלו הנתונים נמצאים: CPU או GPU.

שגיאות רבות בעבודה עם רשתות נובעות מאי־התאמה באחד משלושת המאפיינים האלה, למשל חיבור של טנסורים בצורות שונות או של טנסור על CPU עם טנסור על GPU, ולכן כדאי להתרגל לבדוק אותם.

tensor = torch.rand(3,4)
print(tensor)
print(tensor.shape)
print(tensor.dtype)
print(tensor.device)

ערכי הטנסור אקראיים; הצורה, סוג הנתונים וההתקן המוצגים כאן קבועים. בפלט נראה ש־torch.rand יוצרת כברירת מחדל מספרים מסוג float32 על ה־CPU.

פלט

tensor([[0.5128, 0.5309, 0.0430, 0.2308],
        [0.4617, 0.0351, 0.7457, 0.3395],
        [0.4427, 0.3195, 0.4810, 0.2737]])
torch.Size([3, 4])
torch.float32
cpu

קביעת מאפייני טנסור

עד כה PyTorch בחרה עבורנו את סוג המספרים ואת ההתקן. בעת היצירה אפשר לבחור את סוג הנתונים ואת מיקום האחסון. float32 ו־float16 הם מספרים בנקודה צפה ברוחב 32 ו־16 סיביות; ככל שהרוחב קטן יותר, המספר תופס פחות זיכרון והחישוב מהיר יותר, אך הדיוק נמוך יותר. "cpu" מציין את המעבד המרכזי, ו־"cuda" מציין כאן GPU של NVIDIA. CUDA היא הטכנולוגיה של NVIDIA להרצת חישובים כלליים על המעבד הגרפי, וזהו השם שבו PyTorch מתייחסת ל־GPU.

ניצור טנסור מסוג float32 על GPU, אם הוא זמין, וטנסור מסוג float16 על CPU. הבדיקה torch.cuda.is_available() מחזירה True כאשר אפשר להשתמש ב־CUDA.

if torch.cuda.is_available():
    tensor_float32 = torch.tensor(
        data, dtype=torch.float32, device="cuda"
    )
    print("tensor_float32:")
    print(tensor_float32.dtype, tensor_float32.device)
tensor_float16 = torch.rand(
    size=(3, 3), dtype=torch.float16, device="cpu"
)
print("tensor_float16:")
print(tensor_float16.dtype, tensor_float16.device)

בטנסור שעל GPU יופיע ההתקן cuda:0; הספרה 0 מציינת את ה־GPU הראשון. אם אין GPU זמין, ייווצר רק הטנסור שעל CPU.

טנסורים ו־GPU

ל־GPU יש זיכרון משלו, נפרד מזיכרון המחשב, וכדי שה־GPU יבצע חישוב על טנסור, הטנסור חייב להימצא בזיכרון הזה. בפרק הקודם למדנו לבחור GPU ב־Colab; כעת נראה כיצד מעבירים אליו נתונים. אפשר להעביר טנסור מהזיכרון המשמש את ה־CPU לזיכרון ה־GPU ולהפך. משתמשים בפעולה to ושומרים את הטנסור שהיא מחזירה.

if torch.cuda.is_available():
    tensor_cuda = tensor_float16.to("cuda")
    tensor_cpu = tensor_cuda.to("cpu")
    print("tensor_cuda:", tensor_cuda.device)
    print("tensor_cpu:", tensor_cpu.device)

אם GPU זמין, tensor_cuda מכיל עותק על ה־GPU, ו־tensor_cpu מכיל עותק שהועבר חזרה ל־CPU. שימו לב ש־to אינה משנה את הטנסור המקורי אלא מחזירה טנסור חדש; לכן חייבים לשמור את התוצאה במשתנה. חישוב יכול להתבצע רק בין טנסורים שנמצאים על אותו התקן.

כדי לבצע חישוב של רשת נוירונים על GPU, צריך להעביר לזיכרון שלו גם את רשת הנוירונים, ובכלל זה המשקלים (W), וגם את הנתונים — הטנסורים שעליהם הרשת תבצע את החישוב. באימון מעבירים גם את טנסור התשובות הרצויות, כך שהרשת, הקלט והתשובות נמצאים על אותו התקן.

ההעברה נעשית באמצעות to(device). הקטע הבא מניח שכבר נוצרו רשת בשם model, טנסור קלט בשם inputs וטנסור תשובות בשם targets; את יצירת הרשת נלמד בהמשך:

device = "cuda" if torch.cuda.is_available() else "cpu"

model = model.to(device)
inputs = inputs.to(device)
targets = targets.to(device)

השורה הראשונה בוחרת GPU אם הוא זמין, ואחרת CPU. model.to(device) מעבירה את הרשת להתקן שנבחר; inputs.to(device) ו־targets.to(device) מעבירות אליו את טנסורי הקלט והתשובות, וההשמות שומרות את הטנסורים המוחזרים. בחירת GPU בתפריט של Colab לבדה אינה מעבירה אליו את הרשת או את הטנסורים.

אינדקסים וחיתוך

לעיתים קרובות נצטרך לעבוד עם חלק מהטנסור: לשלוף דוגמה אחת מתוך אוסף הנתונים, לבחור עמודה אחת של תכונה, או לקבוע ערכים באזור מסוים. הגישה לאיברים עובדת כמו ברשימות וב־NumPy. אפשר לגשת לאיבר בטנסור באמצעות האינדקס שלו. במטריצה האינדקס הראשון בוחר שורה והשני בוחר עמודה; הספירה מתחילה ב־0.

ניצור טנסור של אחדות בגודל 4×4. נשנה את האיבר בשורה הרביעית ובעמודה הרביעית ל־5, נאפס את העמודה השנייה, ואז נקבע את כל השורה השלישית ל־0.1.

tensor = torch.ones(4, 4)
tensor[3,3] = 5
tensor[:,1] = 0  # zero second column
tensor[2] = 0.1
print (tensor)

פלט

tensor([[1.0000, 0.0000, 1.0000, 1.0000],
        [1.0000, 0.0000, 1.0000, 1.0000],
        [0.1000, 0.1000, 0.1000, 0.1000],
        [1.0000, 0.0000, 1.0000, 5.0000]])

ההשמה לשורה השלישית משנה גם את האפס שהיה בה בעקבות ההשמה לעמודה.

כעת נבחר שורות ועמודות. : בוחר את כל האיברים בממד, ו־... משלים את הממדים הקודמים. האינדקס -2 בוחר את העמודה הלפני־אחרונה. רשימת אינדקסים כמו [0, 1, 3] מאפשרת לבחור כמה שורות או עמודות.

print(f"second row: {tensor[1]}")
print(f"Third column: {tensor[:, 2]}")
print(f"Second-last column: {tensor[..., -2]}") # use ... when there are more then two dimentions
print(f"1st, 2nd & 4th row: \n {tensor[[0, 1, 3]]}")
print(f"1st, 2nd & 4th column: \n {tensor[:,[0, 1, 3]]}")

פלט

second row: tensor([1., 0., 1., 1.])
Third column: tensor([1.0000, 1.0000, 0.1000, 1.0000])
Second-last column: tensor([1.0000, 1.0000, 0.1000, 1.0000])
1st, 2nd & 4th row: 
 tensor([[1., 0., 1., 1.],
        [1., 0., 1., 1.],
        [1., 0., 1., 5.]])
1st, 2nd & 4th column: 
 tensor([[1.0000, 0.0000, 1.0000],
        [1.0000, 0.0000, 1.0000],
        [0.1000, 0.1000, 0.1000],
        [1.0000, 0.0000, 5.0000]])

פעולות מתמטיות

כאן נמצא הכוח האמיתי של טנסורים. במקום לכתוב לולאה שעוברת על כל איבר, כותבים פעולה אחת על הטנסור כולו, ו־PyTorch מבצעת אותה על כל האיברים, במקביל אם הטנסור על GPU. כך נראה בהמשך חישוב של שכבה שלמה ברשת בשורה אחת. אפשר לבצע פעולות מתמטיות על טנסורים שלמים. בדוגמאות הבאות כל פעולה מתבצעת בין איברים במקומות המתאימים.

ניצור טנסור של אחדות וטנסור של שתיים, שניהם בגודל 3×3, ונחבר אותם:

x = torch.ones(3,3)
y = torch.full((3,3), 2)
print (x,"\n", y)
z1 = x + y
print (z1)

פלט

tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]]) 
 tensor([[2, 2, 2],
        [2, 2, 2],
        [2, 2, 2]])
tensor([[3., 3., 3.],
        [3., 3., 3.],
        [3., 3., 3.]])

נכפול את האיברים המתאימים:

z2 = x * y
print (z2)

פלט

tensor([[2., 2., 2.],
        [2., 2., 2.],
        [2., 2., 2.]])

אפשר לשלב גם פעולה בין טנסור למספר בודד: המספר מופעל על כל איבר בנפרד. נעלה כל איבר ב־y בריבוע ונוסיף לו 5:

z3 = y**2 + 5
print(z3, "\n")

פלט

tensor([[9, 9, 9],
        [9, 9, 9],
        [9, 9, 9]])

פעולות המחזירות ערך בודד

באימון נרצה לעיתים קרובות לכווץ טנסור שלם למספר אחד, למשל לחשב את ממוצע הטעות על כל הדוגמאות כדי להדפיס אותו או לצייר גרף. פעולות כמו סכום וממוצע מחזירות טנסור בעל איבר יחיד. כדי לקבל ממנו מספר פייתון רגיל משתמשים ב־item().

data = torch.tensor([[1,2,3],[4,5,6]], dtype=torch.float32)
sum = data.sum()
mean = data.mean()

print("sum: ", sum)
print("mean: ", mean.item())
n = mean.item()

פלט

sum:  tensor(21.)
mean:  3.5

הסכום מוצג כטנסור, ואילו הממוצע מוצג כמספר לאחר item(). קריאה ל־item() על טנסור בעל כמה איברים תגרום לשגיאה.

Transpose — שחלוף

בפרק המבוא כתבנו את הסכום המשוקלל של נוירון כ־WᵀX. האות T הקטנה מסמנת שחלוף — Transpose, פעולה נפוצה מאוד בחישובי רשתות, ולכן כדאי להכיר אותה כבר עכשיו. שחלוף הופך את שורות המטריצה לעמודות: שורה 0 הופכת לעמודה 0, ושורה 1 לעמודה 1. זו החלפה בין שורות לעמודות, ולא סיבוב של המטריצה.

x = torch.tensor([[1,2,3],[4,5,6]])
print (x)
x_trans = x.T
print (x.T)
print(x)

פלט

tensor([[1, 2, 3],
        [4, 5, 6]])
tensor([[1, 4],
        [2, 5],
        [3, 6]])
tensor([[1, 2, 3],
        [4, 5, 6]])

x.T מחזירה את המטריצה המשוחלפת. כפי שרואים בהדפסה האחרונה, הצורה של x עצמו נשארה שתי שורות ושלוש עמודות.

פעולות in-place

עד כה כל פעולה יצרה טנסור חדש והשאירה את המקורי ללא שינוי. לפעמים נרצה דווקא לשנות את הטנסור הקיים, למשל כדי לעדכן את המשקלים במקום בזמן האימון בלי להקצות זיכרון חדש בכל צעד. פעולות המשנות את הטנסור המקורי נקראות in-place. בדרך כלל מסמנים אותן בקו תחתון בסוף שם הפעולה.

תחילה נוסיף 5 באמצעות ביטוי והשמה:

x = torch.tensor([[1,2,3],[4,5,6]])
print (x)
x = x + 5
print (x)

פלט

tensor([[1, 2, 3],
        [4, 5, 6]])
tensor([[ 6,  7,  8],
        [ 9, 10, 11]])

הביטוי x + 5 יוצר תוצאה חדשה, וההשמה קושרת אליה את השם x. זו אינה פעולת in-place.

כעת נוסיף עוד 5 באמצעות add_, ולאחר מכן נשחלף את הטנסור עצמו באמצעות t_:

x.add_(5)
x.t_()
print (x)

פלט

tensor([[11, 14],
        [12, 15],
        [13, 16]])

שתי הפעולות משנות את x עצמו. הערכים בפלט כוללים את שתי ההוספות של 5: אחת בכל קטע קוד.

Reshape — שינוי צורה

הרשת מצפה לקלט בצורה מסוימת, והנתונים לא תמיד מגיעים בצורה הזו. תמונה, למשל, מגיעה כטבלה של שורות ועמודות של פיקסלים, אך לעיתים נצטרך "לשטח" אותה לשורה אחת ארוכה של מספרים לפני שנזין אותה לרשת. שינוי צורה מארגן את אותם נתונים בממדים אחרים. הפעולה reshape עושה זאת תוך שמירה על מספר האיברים. אם מציינים ממד אחד כ־-1, גודלו מחושב אוטומטית מתוך מספר האיברים והממדים האחרים.

a = torch.arange(12)
a1 = a.reshape(-1,1) # (12,1)
a2 = a.reshape(4,3)
a3 = torch.reshape(a1, (2,2,3))
a4 = a.reshape(2, 2, -1)
print("a", a, a.shape)
print ("a1", a1, a1.shape)
print ("a2", a2, a2.shape)
print ("a3", a3, a3.shape)
print ("a4", a4, a4.shape)

פלט

a tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]) torch.Size([12])
a1 tensor([[ 0],
        [ 1],
        [ 2],
        [ 3],
        [ 4],
        [ 5],
        [ 6],
        [ 7],
        [ 8],
        [ 9],
        [10],
        [11]]) torch.Size([12, 1])
a2 tensor([[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8],
        [ 9, 10, 11]]) torch.Size([4, 3])
a3 tensor([[[ 0,  1,  2],
         [ 3,  4,  5]],

        [[ 6,  7,  8],
         [ 9, 10, 11]]]) torch.Size([2, 2, 3])
a4 tensor([[[ 0,  1,  2],
         [ 3,  4,  5]],

        [[ 6,  7,  8],
         [ 9, 10, 11]]]) torch.Size([2, 2, 3])

בכל התוצאות נשארו אותם 12 איברים. ב־a1 הם מסודרים בעמודה, ב־a2 בארבע שורות ושלוש עמודות, וב־a3 וב־a4 בשלושה ממדים שגודלם 2×2×3.

View

בקוד של PyTorch נפגוש לעיתים קרובות גם את view, שנראית דומה ל־reshape, ולכן כדאי להבין את ההבדל. גם view מאפשרת להציג נתונים בצורה אחרת. בשינוי צורה היא חולקת את אותו זיכרון עם הטנסור המקורי, ודורשת שסידור הנתונים בזיכרון יתאים לצורה המבוקשת. reshape יכולה להעתיק את הנתונים כאשר נדרש.

v = torch.arange(12)
v1 = v.view(-1,1)
v2 = v.view(4,3)
v3 = v.view(torch.float32)
v4 = v.view(-1, 1, 3)
print ("v", v.shape, v.dtype)
print ("v1", v1.shape)
print ("v2", v2.shape)
print ("v3", v3.shape, v3.dtype)
print ("v4", v4.shape)

פלט

v torch.Size([12]) torch.int64
v1 torch.Size([12, 1])
v2 torch.Size([4, 3])
v3 torch.Size([24]) torch.float32
v4 torch.Size([4, 1, 3])

v1, v2 ו־v4 משנות את הצורה. לעומתן, v.view(torch.float32) מפרשת את אותם בתים כ־float32; זו אינה המרה מספרית של הערכים. לכן מתקבלים 24 איברים של 32 סיביות במקום 12 איברים של 64 סיביות.

Permute — שינוי סדר הממדים

לפעמים הנתונים מסודרים בממדים הנכונים אך בסדר לא נכון. תמונה צבעונית, למשל, יכולה להישמר כשורות, עמודות וערוצי צבע, בעוד שהרשת מצפה לקבל תחילה את ערוצי הצבע. במקרה כזה לא רוצים לחלק את האיברים מחדש אלא רק להחליף את סדר הממדים. הפעולה torch.permute מחליפה את סדר הממדים בטנסור. בסדר (2, 0, 1), הממד שהיה שלישי עובר למקום הראשון, הראשון לשני והשני לשלישי.

c = torch.randn(1,3,2)
c1 = torch.permute(c, (2, 0, 1))  # סדר המימדים
print(c.size())
print(c1.size())
print(c)
print(c1)

הערכים האקראיים הם דוגמה; שינוי הצורה הוא מ־1×3×2 ל־2×1×3.

פלט

torch.Size([1, 3, 2])
torch.Size([2, 1, 3])
tensor([[[-0.2273, -2.1568],
         [ 0.7375,  1.3695],
         [-0.3904, -1.8550]]])
tensor([[[-0.2273,  0.7375, -0.3904]],

        [[-2.1568,  1.3695, -1.8550]]])

בניגוד לחלוקה מחדש של האיברים באמצעות reshape, כאן משנים את סדר הממדים.

Cat — שרשור טנסורים

כשנתונים מגיעים בחלקים, למשל דוגמאות משני קבצים שונים, נרצה לאחד אותם לטנסור אחד. הפעולה cat מחברת טנסורים לאורך ממד שנבחר. כך אפשר להוסיף שורות או עמודות; הגדלים בשאר הממדים צריכים להתאים.

x1 = torch.ones(1,4)
x2 = torch.full((1,4) , 2)
print (x1, x2)
x3 = torch.cat((x1,x2),0)
x4 = torch.cat((x1,x2),1)
print (x3)
print (x4)

פלט

tensor([[1., 1., 1., 1.]]) tensor([[2, 2, 2, 2]])
tensor([[1., 1., 1., 1.],
        [2., 2., 2., 2.]])
tensor([[1., 1., 1., 1., 2., 2., 2., 2.]])

בחיבור לאורך ממד 0 מתקבלות שתי שורות וארבע עמודות. בחיבור לאורך ממד 1 מתקבלת שורה אחת ובה שמונה עמודות.

המרה בין טנסור למערך NumPy

ספריות רבות שהכרנו, כמו PyPlot לציור גרפים, עובדות עם מערכי NumPy ולא עם טנסורים. לכן נצטרך לא פעם להמיר טנסור למערך NumPy ולהפך, ובתחילת הפרק הבטחנו להסביר מה קורה אז לזיכרון. טנסור CPU מתאים ומערך NumPy יכולים לשתף את אותם נתונים בזיכרון. בדוגמה הבאה ההמרה אינה מעתיקה את הנתונים, ולכן שינוי באחד מהם נראה גם באחר.

ניצור טנסור של חמישה ערכים:

t = torch.full ((5,), 2.25)
print(t)

פלט

tensor([2.2500, 2.2500, 2.2500, 2.2500, 2.2500])

נמיר אותו למערך NumPy, נכפיל את הטנסור המקורי ב־5 במקום ונדפיס את המערך:

n = t.numpy()
t.mul_(5)
print(n)

פלט

[11.25 11.25 11.25 11.25 11.25]

גם ערכי n השתנו, משום שהוא חולק זיכרון עם t. אפשר ליצור ממנו שוב טנסור:

t1 = torch.from_numpy(n)
print(t1)

פלט

tensor([11.2500, 11.2500, 11.2500, 11.2500, 11.2500])

שיתוף הזיכרון חוסך זמן והעתקות, אך דורש זהירות: שינוי במקום באחד מהם משנה גם את השני, לעיתים בלי שהתכוונו לכך.

יש לנו עתה כלי להחזיק את הנתונים ואת המשקלים ולבצע עליהם חישובים. אך כדי לאמן רשת לא די בחישוב הפלט; צריך גם לדעת כיצד לשנות את המשקלים כדי להקטין את הטעות, ולשם כך נחוצות נגזרות. בפרק הבא נכיר את autograd ונלמד כיצד מחשבים נגזרות באמצעות טנסורים.