راهنمای جامع کار با کتابخانه Numpy - شروع به کار

در این سری از مطالب سعی شده نگاه جامعی بر آخرین نسخه از کتابخانه کاربردی Numpy داشته باشم و ابزارهای موجود در آن را بررسی کنیم. با توجه به آن که دوره به مرور تکمیل می‌شود؛ با ذخیره کردن مقاله زیر، نسبت به مطالب جدید یک دسترسی سریع‌تر برای خود ایجاد کنید:


کتابخانه Numpy حول یک شیء آرایه چند بعدی متمرکز شده است. این کتابخانه ساختار داده‎‌ای قدرتمندی به پایتون اضافه کرده است که نسبت به لیست‌ها در پایتون محاسبات کارآمد و سریع‌تر را تضمین می‌کند. Numpy مجموعه‌ای عظیم از توابع ریاضی مانند توابع جبرخطی، تبدیل فوریه، اعداد تصافی و غیره را فراهم کرده است.

شروع کار با Numpy

آخرین نسخه از کتابخانه Numpy در زمان نوشتن این آموزش 1.20.2 می‌باشد. برای نصب آخرین نسخه این کتابخانه براحتی می‌توانید با کمک دستور زیر بسته به اینکه از چه توزیعی استفاده می‌کنید، این کار را انجام دهید:

pip install numpy

// or if using Anaconda
conda install numpy

همانطور که پیش‌تر گفته شد هر زمان که بخواهید در پایتون از پکیجی استفاده ببرید لازم است دسترسی آن را در کد خود فراهم کنید، بنابراین برای استفاده از کتابخانه Numpy و تمام توابع موجود در آن تنها کافیست از دستور زیر استفاده کنید:

import numpy as np

غالباً این کتابخانه را با نام np در کدها وارد می‌کنند.


اجازه دهید اولین شی آرایه Numpy را ایجاد کنیم. برای اینکار فقط لازم دارید تا تابع array را صدا بزنید، به مثال زیر توجه کنید:

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

>> output:

[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

در مثال فوق ما یک ماتریس 2-بعدی ایجاد کردیم. در Numpy به هر بعد ماتریس axis یا محور گفته می‌شود و به تعداد محورها در یک ماتریس rank یا رتبه گفته می‌شود. برای نمونه ماتریس 4×3 فوق یک آرایه با رتبه 2 است. اولین محور آن به طول 3 و دومین محور 4 است. به لیست طول محورها در Numpy اصطلاحا shape گفته می‌شود. برای نمونه در ماتریس فوق shape برابر (3,4) است. بعبارتی رتبه ماتریس همان طول آرایه shape آن هم هست. نهایتاً اندازه یا size یک ماتریس نیز تعداد کل عناصر در ماتریس می‌باشد که در اینجا برای مثال فوق برابر 12 = 4 * 3 است.

a.shape
>> (3, 4)

a.ndim  # equal to len(a.shape)
>> 2

a.size
>> 12

همانطور که گفته شد هر آرایه در Numpy، یک شیء به نام ndarray می‌باشد که در واقع یک آرایه N-بعدی همگن است:

type(a)
>> <class 'numpy.ndarray'>

در مثال فوق یکی از توابعی که با آن می‌توان این آرایه را ایجاد کرد گفته شد بطور کلی توابعی که می‌توان با کمک آن‌ها در Numpy آرایه ایجاد کرد بصورت زیر است:

np.array()
np.zeros()
np.ones()
np.empty()
np.arange()
np.linspace()

در ادامه به بررسی هر یک از این توابع می‌پردازیم:

np.zeros

این تابع یک آرایه با عناصر صفر ایجاد می‌کند:

np.zeros(5)
>> array([0., 0., 0., 0., 0.])

np.zeros((3,4))
>> array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])

شما همچنین می‌توانید یک آرایه N بعدی از رتبه دلخواه ایجاد کنید. به عنوان مثال، در اینجا یک آرایه 3بعدی (رتبه = 3)، با شکل (2،3،4) وجود دارد:

np.zeros((2,3,4))
>> array([[[0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [0., 0., 0., 0.]],
           
         [[0., 0., 0., 0.],
          [0., 0., 0., 0.],
          [0., 0., 0., 0.]]])


np.ones

این تابع یک آرایه با عناصر یک ایجاد می‌کند:

np.ones((3,4))
>> 
array([[ 1.,  1.,  1.,  1.],
          [ 1.,  1.,  1.,  1.],
          [ 1.,  1.,  1.,  1.]])

np.full

آرایه‌ای را با توجه به ابعاد و با مقدار داده شده ایجاد می‌کند. در اینجا یک ماتریس 3x4 با مقدار π ساخته‌ایم:

np.full((3,4), np.pi)
>> array([[ 3.14159265,  3.14159265,  3.14159265,  3.14159265],
          [ 3.14159265,  3.14159265,  3.14159265,  3.14159265],
          [ 3.14159265,  3.14159265,  3.14159265,  3.14159265]])

همانطور که در مثال فوق هم مشاهده می‌کنید مقادیر ثابتی (مانند np.pi برای عدد پی π) در Numpy وجود دارد که در اینجا می‌توانید لیست کاملی از آنها را مشاهده کنید.

np.empty

این تابع تنها یک آرایه بدون مقداردهی اولیه می‌سازد و مقادیر آن قابل پیش‌بینی نیست، زیرا از هر آنچه که در آن لحظه درون حافظه سیستم وجود دارد برای مقداردهی آرایه استفاده می‌کند. 

np.empty((2, 3))
>> array([[1.e-323 0.e+000 0.e+000]
 	      [0.e+000 0.e+000 0.e+000]]

np.array

پیش از این با این تابع آشنا شدید. با کمک این تابع براحتی می‌توانید یک شیء آرایه ndarray از Numpy را با استفاده از آرایه‌های معمولی پایتون مقداردهی کنید.

np.arange

شما می توانید با استفاده از این تابع، که مشابه عملکرد تابع داخلی range پایتون است، یک آرایه ndarray برای یک محدوده مشخص ایجاد کنید:

np.arange(1, 10)
>> array([1, 2, 3, 4, 5, 6, 7, 8, 9])

این تابع همچنین با مقادیر اعشاری نیز کار می‌کند:

np.arange(1.0, 10.0)
>> array([1., 2., 3., 4., 5., 6., 7., 8., 9.])

البته می توانید پارامتر step را نیز  تعریف کنید:

np.arange(1, 5, step=0.5)
>> array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5])

np.linspace

تابع arange همانطور که در بالا گفته شد برای گام‌های با مقادیر اعشاری بین دو محدوده نیز یک آرایه ایجاد می‌کند. اما گاهی دممکن است تعداد عناصر در این آرایه به دلیل محدودیت حافظه مشخص نباشد. برای روشن‌تر شدن این موضوع به مثال زیر توجه بفرمایید:

np.arange(0, 5/3, 0.333333333)
>> array([0., 0.33333333, 0.66666667, 1., 1.33333333, 1.66666667])

np.arange(0, 5/3, 0.333333334)
>> array([0., 0.33333333, 0.66666667, 1., 1.33333334]

به همین دلیل عموماً ترجیح داده می‌شود از تابع linspace بجای arange برای ساخت آرایه محدود درون یک بازه خصوصا زمانیکه گام‌ها یک عدد اعشاریست، استفاده شود:

np.linspace(0, 5/3, 6)
>> array([0., 0.33333333, 0.66666667, 1., 1.33333333, 1.66666667])

 

اعداد تصادفی با np.random 

در ماژول random از کتابخانه Numpy نیز تعدادی تابع برای ایجاد آرایه‌های ndarray که با اعداد تصادفی مقداردهی می‌شوند، وجود دارد. می‌توانید لیست این توابع در ماژول random را در اینجا مشاهده کنید. برای نمونه در زیر با کمک تابع rand یک ماتریس 4×3 با اعداد تصادفی اعشاری بین 0 و 1 مقداردهی شده است.

np.random.rand(3,4)
>> array([[0.02246299, 0.17706986, 0.2514137 , 0.07247134],
          [0.49027483, 0.90593102, 0.70472145, 0.41700889],
          [0.84201564, 0.41875008, 0.12580629, 0.5322442 ]])

ماتریس فوق را نیز می‌توان با تابع دیگری از ماژول random مجددا ایجاد کرد. با این تفاوت که در آن اعداد اعشاری از یک توزیع نرمال یک متغیره (توزیع گوسی) با میانگین 0 و واریانس 1 آمده‌اند:

np.random.randn(3,4)
>> array([[ 0.19335153, -0.77536899,  1.41071992, -0.49698281],
          [-0.29661267, -0.19211462, -1.42000361,  0.32262415],
          [ 0.34363616,  0.12019296,  1.15223647,  2.28839286]])

اجازه دهید برای اینکه تفاوت این دو تابع مشخص شود با کمک matplotlib یک مثال را تصویر کنم:

import numpy as np
import matplotlib.pyplot as plt

plt.subplot(1, 2, 1)
plt.hist(np.random.rand(100000), bins=100, histtype="step", color="blue")
plt.title("np.random.rand")

plt.subplot(1, 2, 2)
plt.hist(np.random.randn(100000), bins=100, histtype="step", color="red")
plt.title("np.random.randn")

plt.show()
rand vs randn
rand vs randn
rand vs randn

تابع دیگری که با کمک آن می‌توانید آرایه‌های خود را با اعداد تصادفی صحیح مقداردهی کنید، تابع randint است:

np.random.randint(2, size=10)
>> array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0])

np.random.randint(1, size=10)
>> array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

همانطور که مشاهده می‌کنید آرگومان اول حد بالا را برای ایجاد اعداد تصادفی تعریف می‌کند. به اینصورت که برای عدد m، اعداد تصادفی از 0 تا m - 1 انتخاب می‌شوند. آرگومان size نیز بعد آرایه را تعیین می‌کند. شما می‌توانید یک آرایه 2-بعدی نیز ایجاد کنید:

np.random.randint(5, size=(2, 4))
>> array([[4, 0, 2, 1],
          [3, 2, 2, 0]])

np.fromfunction

با این تابع می‌توانید کنترل مقداردهی آرایه‌های خود را بدست بگیرید:

def my_generator(x, y):
    return x + y

np.fromfunction(my_generator, (2, 4))
>> array([[0. 1. 2. 3.]
		  [1. 2. 3. 4.]])

dtype

با کمک این پارامتر می‌توانید نوع داده را بررسی کنید.


c = np.arange(1, 5)
print(c.dtype, c)
>> int64 [1 2 3 4]

c = np.arange(1.0, 5.0)
print(c.dtype, c)
>> 
float64 [ 1.  2.  3.  4.]

علاوه براین می‌توانید هنگام تعریف آرایه در Numpy با تنظیم پارامتر dtype نوع عناصر آرایه را تعیین کنید:


d = np.arange(1, 5, dtype=np.complex64)
print(d.dtype, d)
>> complex64 [ 1.+0.j  2.+0.j  3.+0.j  4.+0.j]

در اینجا می‌توانید لیست کاملی از انواع داده‌ها در Numpy مشاهده کنید.

تغییر شکل (Reshape) یک آرایه

برای تغییر شکل ابعاد یک آرایه همواره می‌توانید با کمک پارامتر shape همان شیء ndarray را به آرایه‌ای با شکلی متفاوت تبدیل کنید. به این معنی که هر تغییر دیگری بر آرایه تغییر شکل یافته، آرایه اولیه را تغییر می‌دهد:

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

a.shape = (3, 4)
print(a)
>> [[ 0  1  2  3]
 	[ 4  5  6  7]
 	[ 8  9 10 11]]
 	
a.shape = (2, 3, 2)
print(a)	
 >> [[[ 0  1]
  	  [ 2  3]
  	  [ 4  5]]
 	 [[ 6  7]
  	  [ 8  9]
  	  [10 11]]]

تنها نکته‌ای که باید به آن توجه داشته باشید size جدید از اندازه آرایه اصلی متفاوت نباشد:

a.shape = (3, 5)
>> ValueError: cannot reshape array of size 12 into shape (3,5)

تابع دیگری که با کمک آن می‌توانید اندازه یک آرایه را تغییر دهید تابع reshape است. تفاوت این تابع در آن است که یک شی ndarray دیگر را خروجی می‌دهد. به این معنی که هر تغییر در آرایه جدید تاثیری بر آرایه اولیه ندارد:

a = np.arange(12)
a.shape = (3, 4)

print(a.reshape(2, 6))
print(a)

که خروجی آن بصورت زیر است:

[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]]
 
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

در نهایت دوتابع دیگر به نام ravel و flatten وجود دارند که یک آرایه یک بعدی بازمی‌گرداند. تنها تفاوت در آن است که ravel آرایه اصلی را تغییر می‌دهد ولی flatten یک کپی از آرایه اصلی را تغییر می‌دهد:

a = np.arange(12).reshape(3, 4)
x = a.ravel()
x[0] = 31
print(a)
print(x)

>> [[31  1  2  3]
	[ 4  5  6  7]
    [ 8  9 10 11]]
	[31  1  2  3  4  5  6  7  8  9 10 11]


a = np.arange(12).reshape(3, 4)
y = a.flatten()
y[0] = 31
print(a)
print(y)

>> [[ 0  1  2  3]
 	[ 4  5  6  7]
 	[ 8  9 10 11]]
	[31  1  2  3  4  5  6  7  8  9 10 11]

عملیات حسابی 

تمامی عملگرهای حسابی معمول مانند + ، - ، * ، / ، // ، ** و غیره را می‌توان بر روی ndarray اعمال کرد. این عملیات بصورت عنصر به عنصر اعمال می‌شود توجه داشته باشید که ضرب و تقسیم در این حالت با ضرب و تقسیم ماتریسی متفاوت است (در این باره در مقالات بعدی بیشتر توضیح خواهم داد):

a = np.array([14, 23, 32, 41])
b = np.array([5,  4,  3,  2])
print("a + b  =", a + b)
print("a - b  =", a - b)
print("a * b  =", a * b)
print("a / b  =", a / b)
print("a // b  =", a // b)
print("a % b  =", a % b)
print("a ** b =", a ** b)

خروجی:

a + b  = [19 27 35 43]
a - b  = [ 9 19 29 39]
a * b  = [70 92 96 82]
a / b  = [2.8  5.75 10.66666667 20.5]
a // b  = [2  5 10 20]
a % b  = [4 3 2 1]
a ** b = [537824 279841  32768   1681]

نکته مهم در اینجا این است که آرایه‌ها باید به یک شکل باشند. در غیر اینصورت، NumPy قوانین پخش (broadcasting rules) را اعمال می‌کند.

a = np.array([14, 23, 32, 41])
b = np.array([5, 4, 3, 2, 0])
print("a + b  =", a + b)

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

قوانین پخش یا Broadcasting

به طور کلی، وقتی NumPy انتظار دارد آرایه‌ها هم اندازه باشند، اما متوجه می‌شود که چنین نیست، در اینصورت اصطلاحاً قوانین پخش (broadcasting rules) را اعمال می کند:

قانون اول

اگر آرایه‌ها رتبه برابری نداشته باشند. آنقدر بعد به ابتدای آرایه کوچکتر اضافه می‌شود تا هر دو آرایه ابعاد یکسانی داشته باشند:

a = np.arange(5).reshape(1, 1, 5)
a = a + [10, 20, 30, 40, 50] 
# برابر با : a + [[[10, 20, 30, 40, 50]]]

print(a)

>> [[[10 21 32 43 54]]]

همانطور که مشاهده می‌کنید زمانیکه یک آرایه یک بعدی با اندازه (,5) را با یک آرایه 3-بعدی با اندازه (1,1,5) جمع کردیم، قانون اول پخش اعمال شد.

قانون دوم

آرایه کوچکتر با بعد 1 به گونه‌ای عمل می‌کند که گویی اندازه آرایه با بیشترین شکل را در طول آن بعد دارد. در اینصورت مقدار عنصر آرایه در طول آن بعد تکرار می شود. برای نمونه فرض کنید یک آرایه 2-بعدی با اندازه (2,1) را به یک آرایه با اندازه (2,3) اضافه کنیم. در اینصورت قانون دوم پخش بر روی ستون‌های (بعد دوم) آرایه با اندازه (2,1) بصورت زیر عمل می‌کند:

a = np.arange(6).reshape(2, 3)
a = a + [[100], [200]]

# برابر با : a +  [[100, 100, 100], [200, 200, 200]]

print(a)

>> [[100 101 102]
 	[203 204 205]]

قوانین 1 و 2 را نیز می‌توانند باهم ترکیب شوند:

a = a + [100, 200, 300]

# بعد از قانون اول : a + [[100, 200, 300]]

# بعد از قانون دوم: a + [[100, 200, 300], [100, 200, 300]]

print(a)

>> [[100, 201, 302],
    [103, 204, 305]]


a + 1000

# برابر با : a + [[1000, 1000, 1000], [1000, 1000, 1000]]

>> [[1000, 1001, 1002],
    [1003, 1004, 1005]]

قانون سوم

اگر اندازه دو آرایه در عملیات برابر نباشد و هیچ یک از قوانین اول و دوم اعمال نگردد، خطای عدم امکان پخش بالا می‌آید:

try:
    a + [33, 44]
except ValueError as e:
    print(e)
    
>> operands could not be broadcast 
together with shapes (2,3) (2,) 

Upcasting

اصطلاح دیگری به نام upcast در عملیات روی آرایه Numpy وجود دارد. این قانون نیز هنگامی که می‌خواهید چند آرایه‌ با انواع مختلف داده را با یکدیگر ترکیب کنید، اعمال می‌شود. NumPy به نوعی قابلیت مدیریت همه مقادیر ممکن را می دهد:

k1 = np.arange(0, 5, dtype=np.uint8)
k2 = k1 + np.array([5, 6, 7, 8, 9], dtype=np.int8)
print(k2.dtype, k2)

>> int16 [ 5  7  9 11 13]

توجه داشته باشید که int16 برای نشان دادن تمام مقادیر ممکن int8 و uint8 (از 128- تا 255) لازم است.

k3 = k1 + 1.5
print(k3.dtype, k3)

>> float64 [ 1.5  2.5  3.5  4.5  5.5]

در این مقاله با مفاهیم اولیه مورد نیاز همچون تعریف یک شی آرایه ndarray، تغییر اندازه، عملیات حسابی و قوانین پخش در کتابخانه Numpy آشنا شدید. در مقاله بعدی با عملیات ریاضی و آماری بر روی آرایه‌ها و ترکیب چند آرایه آشنا خواهید شد.