ספריית NumPy - חלק 3


NumPy



מתי מערך מועתק ומתי לא

יש פעולות שמשכפלות את המערך ויש פעולות שמשנות את המערך עצמו ללא שכפול.
ישנם שלושה מקרים:


מקרים ללא שכפול

השמה של מערך a למערך b:
b = a
 לא משכפלת את מערך a. פשוט עכשיו לאותו מקום בזכרון שמחזיק את מערך a יש שם נוסף b. ולכן כל שינוי ב-a ישנה את b. וכמובן שהמשתנים של שני האובייקטים יהיו זהים. כמו למשל shape.

פייתון מעביר mutable objects לפונקציות כרפרנס ולכן גם בקריאה לפונקציה עם פרמטר שהוא אובייקט np.ndarray אין שכפול של הזיכרון.


מקרים עם שכפול חלקי

פונקציית view מאפשרת יצירה של מערך חדש שמסתכל על אותם נתונים של מערך אחר בלי להעתיק אותם.
המשתנה base של אובייקט ndarray מצביע על האובייקט שעל הזכרון שלו הוא מסתכל (אם הוא לא מסתכל על אובייקט אחר אלא על הזיכרון ששייך לו עצמו ה-base שלו יהיה None).
נסביר ע"י דוגמה:

>>> a = np.arange(1,12)
>>> a.resize((3,4))
>>> a
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11,  0]])

>>> b = a.view()

>>> b is a
False

>>> b.base is a
True

שינוי המבנה של b משנה רק את הצורה שבה b מסתכל על a אבל לא משנה את הזיכרון של a.


>>> b.shape
(3, 4)

>>> b.shape = 2,6
>>> b.shape
(2, 6)

>>> b
array([[ 1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11,  0]])

>>> a.shape
(3, 4)

שינוי אלמנטים של b משנה אלמנטים של a:


>>> b[1,2] = 99
>>> b
array([[ 1,  2,  3,  4,  5,  6],
       [ 7,  8, 99, 10, 11,  0]])

>>> a
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [99, 10, 11,  0]])

חיתוך מערך מחזיר view. לדוגמה:


>>> c = a[1,:]
>>> c
array([5, 6, 7, 8])

>>> c.base
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [99, 10, 11,  0]])

>>> c[0] = 12
>>> c
array([12,  6,  7,  8])

>>> a
array([[ 1,  2,  3,  4],
       [12,  6,  7,  8],
       [99, 10, 11,  0]])


מקרים עם שכפול מלא

הפונקציה copy משכפלת מערך ויוצרת מערך חדש עם זכרון נפרד.


>>> d = a.copy()
>>> d
array([[ 1,  2,  3,  4],
       [12,  6,  7,  8],
       [99, 10, 11,  0]])

>>> d is a
False

>>> d.base is a
False

>>> d[0,0] = 55
>>> d
array([[55,  2,  3,  4],
       [12,  6,  7,  8],
       [99, 10, 11,  0]])
>>> a
array([[ 1,  2,  3,  4],
       [12,  6,  7,  8],
       [99, 10, 11,  0]])

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


>>> a = np.arange(1e6)
>>> a
array([0.00000e+00, 1.00000e+00, 2.00000e+00, ..., 9.99997e+05,
       9.99998e+05, 9.99999e+05])

>>> a.shape
(1000000,)

>>> b = a[:100].copy()

>>> del a


אם היינו יוצרים את b ללא copy
b = a[:100]
אז b היה בעצם שם נוסף לזכרון של a וכך גם לאחר מחיקת a עדיין הזיכרון היה נשאר כי b מצביע עליו.


דוגמא שימושית - החלפת שורות/עמודות

כדי להראות את החשיבות של הבנת הנושאים האלו נראה דוגמה שימושית - החלפת שורות. נגיד למשל שיש לי תמונה בפורמט GRB, שורה ראשונה כל ערכי ה-G שורה שניה כל ערכי ה-R ושורה שלישית כל ערכי ה-B. ואני צריך להשתמש בספריה שדורשת ממני פורמט של RGB. אז יש צורך להחליף בין השורה הראשונה לשניה כך שבשורה הראשונה יהיו ערכי R ובשורה השניה ערכי G.

בצורה פשוטה היינו יכולים לנסות לעשות את זה כך:

>>> a = np.arange(1,13)
>>> a.resize((3,4))
>>> a
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])

>>> temp = a[0,:]

>>> a[0,:] = a[1,:]

>>> a[1,:] = temp

>>> a
array([[ 5,  6,  7,  8],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])

לא קיבלנו את מה שרצינו, וזה מכיון שבפקודה הרביעית כשהעתקנו את השורה הראשונה ל-temp קיבלנו בעצם view ולא נוצר משתנה חדש ונפרד מ-a. ולכן בשורה הבאה כשהעתקנו את השורה השניה של a לשורה הראשונה של a בעצם גם שינינו את temp.

הצורה הנכונה לעשות את זה היא ע"י שימוש ב-copy:


>>> temp = np.copy(a[0,:])
>>> a[0,:] = a[1,:]
>>> a[1,:] = temp

>>> a
array([[ 5,  6,  7,  8],
       [ 1,  2,  3,  4],
       [ 9, 10, 11, 12]])


כללי Broadcasting

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

נסביר ע"י דוגמה פשוטה:


>>> a = np.array([[1,2,3],[4,5,6]])
>>> a
array([[1, 2, 3],
       [4, 5, 6]])

>>> b = np.array([10,20,30])
>>> b
array([10, 20, 30])

>>> a+b
array([[11, 22, 33],
       [14, 25, 36]])

למערך a יש 2 שורות ו-3 עמודות. למערך b יש שורה אחת ו-3 עמודות. כדי שיהיה ניתן לחבר ביניהם NumPy שיכפל את b והפך אותו ל-2 שורות ו-3 עמודות. וכך חיבר בין a ל-b. השורה החדשה של b מכילה את אותם ערכים של השורה הראשונה.

הכלל הראשון של broadcasting הוא שהמערך שיש לו פחות מימדים מוסיפים לו את המימדים החסרים עם המספר 1. למשל אם מערך a הוא בגודל (2,3) ואילו מערך b הוא חד ממדי עם 2 אלמנטים (,2) - נתרגם את מערך b למערך דו-ממדי כשהמימד השני הוא בגודל 1 (2,1).



הכלל השני הוא שהמימדים שהם בגודל 1 יגדלו לאותו גודל של המימד המקביל במערך השני כאשר הנתונים שיתווספו במקומות החדשים יהיו שכפול של הנתונים מהמימד השני. למשל אם נמשיך את הדוגמה הקודמת אז עכשיו מערך b יהפוך מ-(2,1) ל-(2,3) כדי להשוות את גודל המימד השני למימד שבמערך a. והנתונים שיכנסו לעמודות החדשות יהיו זהים לנתונים שהיו בעמודה הראשונה.



במקרים שאין אפשרות לבצע כללים אלו, ה-broadcasting יכשל ונקבל הודעת שגיאה.
לדוגמה:


>>> a = np.array([[1,2,3],[4,5,6]])
>>> b = np.array([1,2])
>>> a + b
Traceback (most recent call last):

  File "<ipython-input-19-bd58363a63fc>", line 1, in <module>
    a + b

ValueError: operands could not be broadcast together with shapes (2,3) (2,) 



אינדוקס מתקדם

ספריית NumPy מציעה אפשרויות אינדוקס מתקדמות שלא קיימות ב-python רגיל.
ניתן להשתמש במערך של שלמים לטובת אינדקסים. לדוגמה:


>>> a = np.arange(10)**2
>>> i = np.array([1,4,8,2])
>>> a[i]
array([ 1, 16, 64,  4], dtype=int32)

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


>>> j = np.array([[1,3],[9,8]])
>>> a[j]
array([[ 1,  9],
       [81, 64]], dtype=int32)

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


>>> a = np.arange(25)
>>> a.resize(5,5)
>>> a
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24]])

>>> i = np.array([1,2,3])

>>> a[i]
array([[ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]])

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


>>> j = np.array([[2,1],
                  [1,3]])

>>> k = np.array([[0,1],
                  [1,0]])

>>> a[j,k]
array([[10,  6],
       [ 6, 15]])

ניתן להכניס את j,k ל-list ולהירשם ב-list כאינדקס.
l = [j,k]
a[l]

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


>>> a
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24]])
>>> a[[1,3]] = 99
>>> a
array([[ 0,  1,  2,  3,  4],
       [99, 99, 99, 99, 99],
       [10, 11, 12, 13, 14],
       [99, 99, 99, 99, 99],
       [20, 21, 22, 23, 24]])

ההשמה היתה לתוך שורה 1 ושורה 3.

שים לב להבדל של הדוגמה לעיל לעומת הדוגמה הבאה:


>>> a
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24]])
>>> a[1,3] = 99
>>> a
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7, 99,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24]])

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

אינדוקס בוליאני

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


>>> a = np.arange(12).reshape(4,3)
>>> a
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

>>> b = a>7
>>> b
array([[False, False, False],
       [False, False, False],
       [False, False,  True],
       [ True,  True,  True]])

>>> a[b] = 99
>>> a
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7, 99],
       [99, 99, 99]])

הצורה השניה לשימוש של אינדקס בוליאני הוא ע"י מערך חד-ממדי לכל מימד של המערך. לדוגמה:


>>> a = np.arange(12).reshape(4,3)
>>> a
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

>>> b1 = np.array([False, True, False, True])
>>> b2 = np.array([True, True, False])
>>> a[b1] #selecting rows 1 and 3. Same as a[b1,:]
array([[ 3,  4,  5],
       [ 9, 10, 11]])
>>> a[:,b2] # selecting columns 0 and 1
array([[ 0,  1],
       [ 3,  4],
       [ 6,  7],
       [ 9, 10]])


פונקציית ()_ix

נניח שיש לנו שלוש מערכים חד-ממדיים. a באורך 3, b באורך 4 ו-c באורך 5. ואנחנו רוצים לקבל מערך שמכיל את החישוב a+b*c עבור כל הקומביניות של איברי המערכים. אם ננסה פשוט לחשב את זה בצורה ישירה נקבל שגיאה, כיון שלא ניתן לבצע broadcasting במקרה הזה:


>>> a = np.array([1,2,3])
>>> b = np.array([1,2,3,4])
>>> c = np.array([1,2,3,4,5])
>>> a+b*c
Traceback (most recent call last):

  File "<ipython-input-62-9ff10b3e6b17>", line 1, in <module>
    a+b*c

ValueError: operands could not be broadcast together with shapes (4,) (5,) 

במקרה הזה אפשר להשתמש בפונקציית ()_ix שתשנה לנו את מבנה המערכים כך שנוכל לבצע פעולות אריתמטיות ביניהם:


>>> ax,bx,cx = np.ix_(a,b,c)
>>> ax
array([[[1]],

       [[2]],

       [[3]]])

>>> bx
array([[[1],
        [2],
        [3],
        [4]]])

>>> cx
array([[[1, 2, 3, 4, 5]]])

>>> ax.shape, bx.shape, cx.shape
((3, 1, 1), (1, 4, 1), (1, 1, 5))

>>> ax+bx*cx
array([[[ 2,  3,  4,  5,  6],
        [ 3,  5,  7,  9, 11],
        [ 4,  7, 10, 13, 16],
        [ 5,  9, 13, 17, 21]],

       [[ 3,  4,  5,  6,  7],
        [ 4,  6,  8, 10, 12],
        [ 5,  8, 11, 14, 17],
        [ 6, 10, 14, 18, 22]],

       [[ 4,  5,  6,  7,  8],
        [ 5,  7,  9, 11, 13],
        [ 6,  9, 12, 15, 18],
        [ 7, 11, 15, 19, 23]]])






























אם אהבתם, תכתבו משהו...

אין תגובות:

הוסף רשומת תגובה