معرفی ماژول نامپای در پایتون (Numpy)

Numpy - Python

ماژول نامپای یک لایبری قابل افزودن به زبان برنامه‌نویسی پایتون است که کاربرد اصلی‌اش برای مقاصد علمی و کار با اعداد است. این ماژول دارای توابع آرایه‌ای ریاضیات و آمار در زبان برنامه‌نویسی پایتون دارا می‌باشد. ماژول نامپای در خیلی از کتابخانه‌های کُد و پروژه‌ها به کار گرفته شده و در این زمینه یک فناوری «پایه» است.

کاربرد نامپای در عمل

اشیاء نامپای از نوع ndarray (آرایه‌ی n بعدی) می‌باشند و روش‌های مختلفی برای ایجاد آنها وجود دارد. ما می‌توانیم توسط موارد زیر یک ndarray ایجاد کنیم:

  • تبدیل به یک لیست پایتون
  • استفاده از تابع factory که یک بردار جمعیتی را بازگردانی می‌کند
  • بازخوانی داده به صورت مستقیم از یک فایل به یک شیء نامپای

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

# Five different ways to create a vector…

import numpy as np

# From a Python list

vec1 = np.array( [ 0., 1., 2., 3., 4. ] )

# arange( start inclusive, stop exclusive, step size )

vec2 = np.arange( 0, 5, 1, dtype=float )

# linspace( start inclusive, stop inclusive, number of elements )

vec3 = np.linspace( 0, 4, 5 )

# zeros( n ) returns a vector filled with n zeros

vec4 = np.zeros( 5 )

for i in range( 5 ):

vec4[i] = i

# read from a text file, one number per row

vec5 = np.loadtxt( “data” )

در آخر هر پنج بردار دارای داده‌های یکسان خواهند بود. همانطور که می‌بینید مقادیر در لیست پایتون که برای مقداردهی اولیه vec1 استفاده شدند، مقادیر floating-point (شناور) هستند و باید در نظر داشته باشید که نوع مقادیر برای عناصر vec2 باید در هنگام استفاده تابع arrange () تعریف شود.

اکنون که این اشیاء را ایجاد کرده‌ایم، می‌توانیم با آنها کار کنیم (لیست‌ بعدی را مشاهده کنید). یکی از تسهیلات مهمی که نامپای ایجاد کرده، این است که می‌توانیم از اشیاء آن بهره ببریم حتی اگر از نوع پایین ترین سطح داده‌ای باشند: می‌توان بدون نیاز استفاده از حله (مانند for) آنها را جمع ببندیم، تفریق کنیم، ضرب کنیم (و غیره). اجتناب از ایجاد این گونه حلقه‌ها کدهای ما را تمیز تر می‌کند. همچنین باعث تسریع کدها می‌شود.

# … continuation from previous listing

# Add a vector to another

v1 = vec1 + vec2

# Unnecessary: adding two vectors using an explicit loop

v2 = np.zeros( 5 )

for i in range( 5 ):

v2[i] = vec1[i] + vec2[i]

# Adding a vector to another in place

vec1 += vec2

# Broadcasting: combining scalars and vectors

v3 = 2*vec3

v4 = vec4 + 3

# Ufuncs: applying a function to a vector, element by element

v5 = np.sin(vec5)

# Converting to Python list object again

lst = v5.tolist()

تمام عملیات به صورت جزء به جزء انجام می‌شود: اگر دو بردار را با هم جمع نماییم، آن گاه اجزای متناظر از هر بردار با هم جمع می‌شوند و بردار جدید را می‌سازند. به عبارت دیگر، عبارت vec1 + vec2 برای v1 معادل این است که به منظور محاسبه‌ی v2، حلقه‌ای با تعداد دفعات تکرار مشخص ایجاد کنیم. این قضیه در مورد ضرب هم صدق می‌کند: vec1 * vec2 برداری را ایجاد می‌کند که اجزای آن از ضرب اجزای متناظر هر دو بردار به دست آمده است. (اگر به دنبال یک ضرب برداری یا ضرب داخلی هستید، باید از تابع dot () استفاده نمایید). طبیعتاً، برای این نوع عملگرها نیاز است تا تعداد اجزاء یکسان باشند!

حالا باید دو ویژگی دیگری که در مستندات نامپای با نام‌های broadcasting و ufuncs به آنها اشاره می‌شود را نشان دهیم. عبارت broadcasting یا همه فرستی در اینجا هیچ ارتباطی به پیام رسانی ندارد. در عوض، به این معنی است که اگر شما دو آرگومان با دو شکل متفاوت را بخواهید ترکیب کنید، آرگومان کوچک‌تر بسط پیدا کرده تا با آرگومان بزرگ‌تر مطابق شود. به خصوص این امر هنگام ترکیب اسکالرها با بردارها مفید می‌باشد: کمیت اسکالر به برداری با اندازه‌ی مناسب بسط یافته و تمام اجزای آن مقادیر اسکالر می‌باشند؛ سپس عملیات به مانند قبل به صورت جزء به جزء پیش می‌رود. عبارت «ufunc» به تابع اسکالری برمی‌گردد که می‌توان آن را بر روی تمام اشیاء پروژه نامپای اعمال کرد. تابع به صورت جزء به جزء بر روی تمام اعضای ورودی‌ به اشیاء نامپای اعمال می‌گردد، و در نتیجه یک شیء نامپای جدید که شکلی یکسان با شئی اولیه دارد، ایجاد می‌شود.

با استفاده‌ی مناسب از این ویژگی‌ها، می‌توانیم تابعی برای محاسبه‌ی برآورد چگالی، تنها در یک خط کد بنویسیم:

from numpy import *

# z: position, w: bandwidth, xv: vector of points

def kde( z, w, xv ):

return sum( exp(-0.5*((z-xv)/w)**2)/sqrt(2*pi*w**2) )

d = loadtxt( “presidents”, usecols=(2,) )

w = 2.5

for x in linspace( min(d)-w, max(d)+w, 1000 ):

print x, kde( x, w, d )

بیشتر لیست‌ها، از قبیل فایل‌های خواندنی و نوشتنی، از نوع کدهای استاندارد (کدهایی که بدون تغییر یا با تغییر بسیار کم در همه جا استفاده می‌شوند) هستند. تمام کار اصلی در یک دستور یک خطی kde(z, w, xv) انجام شد. این دستور از هر دو ویژگی که در بالا شرح دادیم استفاده کرده و مثال خوبی برای سبک برنامه‌نویسی نامپای می‌باشد. بیایید آن را تشریح کنیم.

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

در خط بعدی، عبارت z-xv برداری از فواصل بین نقطه‌ی z با تمام نقاط موجود در xv تولید را شامل. سپس هر عضو را بر مقدار عرض توزیع (پارامتر W) تقسیم می‌کنیم، در ۰.۵ ضرب می‌کنیم و به توان دو می‌رسانیم. در نهایت، تابع نمایی exp () را روی این بردار اعمال می‌نماییم (این تابع یک ufunc است). در نتیجه برداری داریم که در آن فواصل بین نقاط موجود در مجموعه‌ی داده‌ها و نقطه‌ی z، تابع نمایی برآورد شده است. حالا فقط باید تمام اجزای بردار را با هم جمع نماییم (آنچه دستور sum() انجام می‌دهد)، و با انجام اینکار تابع چگالی را در موقعیت z محاسبه کردیم. اگر قصد داریم که تابع چگالی را به شکل یک منحنی رسم نماییم، باید این فرآیند را برای هر مکانی که مد نظر ما است تکرار کنیم- دلیل وجود حلقه‌ی پایانی در لیست کد همین است.

جزئیات نامپای

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

در نظر گرفتن نامپای به عنوان «پکیج ماتریس برای پایتون» اشتباه است (هرچند خیلی‌ها برای این منظور هم استفاده می‌کنند). فکر می‌کنم اگر نامپای را به عنوان یک لایه‌ی پوششی و دسترسی برای بافرهای اساسی زبان C در نظر بگیریم، مفیدتر باشد. این بافرها بلوک‌های مجاور حافظه‌ی C می‌باشند، که- بنابر ماهیت‌شان- دارای ساختارهای یک بعدی هستند. تمام عناصر در آن ساختار داده‌ها باید اندازه‌ی یکسان داشته باشند، تا بتوانیم هر نوع C اصلی را (که شامل ساختارهای زبان C هم می‌شود) به عنوان اجزای منحصر به فرد تشخیص دهیم. نوع پیش فرض با یک C double متناظر است و این همانی است که ما در مثال‌ها به کار بردیم، اما به یاد داشته باشید که انتخاب‌های دیگر هم ممکن است. تمام عملیاتی که بر روی داده‌ها اعمال شد در زبان برنامه‌نویسی C بود و به همین دلیل سریع انجام شدند.

برای این که داده‌ها را به شکل یک ماتریس یا سایر ساختارهای چند بعدی تعریف کنیم، باید شکل یا لایه را بر اساس اجزا آن در هنگام دسترسی به عناصر مشخص می‌شود. بنابراین یک ساختار داده‌ای ۱۲ عضوی را می‌توان به صورت یک بردار ۱۲ عضوی یا یک ماتریس ۴×۳ یا یک تانسور ۳×۲×۲ (tensor) تعبیر کرد- شکل مورد نظر فقط از طریقی که ما به اجزای منحصر به فرد دسترسی پیدا کنیم، به دست می‌آید (توجه داشته باشید که اگرچه تغییر شکل یک ساختار داده‌ای کار ساده‌ای است، ولی تغییر اندازه مشکل می‌باشد).

کپسوله‌سازی ساختارهای داده‌های اساسی زبان C خیلی خوب نیست: هنگامی که کوچکترین نوع داده را انتخاب می‌کنیم، انواع داده‌های زبان C را، و نه پایتون را، مشخص می‌کنیم. به طور مشابه، بعضی ویژگی‌هایی که توسط نامپای ارائه شده این اجازه را به ما می‌دهد که حافظه را به صورت دستی مدیریت کنیم، بجای اینکه مشخصاً توسط پایتون مدیریت شود. آن را این گونه طراحی کرده‌اند، زیرا نامپای باید به گونه‌ای باشد که با ساختارهای داده‌های بزرگ تطابق یابد- به اندازه‌ای بزرگ که شاید شما بخواهید (یا نیاز داشته باشید) که کنترل شدیدتری بر نحوه‌ی مدیریت حافظه اعمال کنید. به این دلیل، شما قادر خواهید بود انواع عناصری که فضای کمتری را اشغال می‌کنند، انتخاب نمایید (به عنوان مثال عناصر C float نسبت به double پیش فرض). به همین دلیل، تمام توابع جهانی یک آرگومان اختیاری را که نشان‌گر مکانی است که نتایج در آنجا قرار می‌گیرند (که در حال حاضر اختصاص داده شده)، می‌پذیرند و بدین وسیله از درخواست حافظه‌ی اضافه اجتناب می‌کنند. در نهایت، رویه‌های دسترسی و ساختار یک بازتاب (نه یک کپی!) از داده‌های اساسی مشابه را ارجاع می‌دهد. در اینجا یک مشکل دیگر مطرح می‌شود که باید آن را بررسی کنید.

در کد بعدی فوراً مفاهیم شکل و دید نشان داده شده است. در اینجا، فرض می‌کنیم که دستورات به شکل پرامت واسط پایتون وارد می‌شوند (که به شکل <<< در لیست نشان داده می‌شود). خروجی که توسط پایتون ایجاد می‌شود بدون پرامت نمایش داده می‌شود:

>>> import numpy as np

>>> # Generate two vectors with 12 elements each

>>> d1 = np.linspace( 0, 11, 12 )

>>> d2 = np.linspace( 0, 11, 12 )

>>> # Reshape the first vector to a 3×4 (row x col) matrix

>>> d1.shape = ( 3, 4 )

>>> print d1

 حالا بیایید هر یک از مراحل این کد را بیشتر ببینیم. ما دو بردار ۱۲ عضوی ایجاد می‌کنیم. سپس اولی را به یک ماتریس ۴×۳ تغییر شکل (reshape) می‌دهیم. توجه داشته باشید که مشخصه‌ی شکل (shape) یک عضو داده‌ محسوب می‌شود- نه یک تابع accessor. برای بردار دوم، یک نما (view) به شکل یک ماتریس ۴×۳ ایجاد می‌کنیم. حالا d1 و نمای جدید d2 که اکنون ساخته شده، شکل یکسانی دارند، بنابراین می‌توانیم آنها را با هم ترکیب کنیم (با استفاده از جمع کردن آنها). توجه کنید که اگرچه تابع reshape () یک عضو از توابع می‌باشد، اما تغییر شکلی را ایجاد نمی‌کند و در عوض یک نمای جدید از شیء را برمی‌گرداند: d2 هنوز یک بردار یک بعدی بوده و تغییر نکرده است (همچنین یک نسخه‌ی مستقل از این دستور وجود دارد، به طوری که می‌توانیم بنویسیم view= np.reshape ( d2, (3,4)). وجود یک چنین قابلیت مازادی به دلیل تمایل به حفظ سازگاری با هر دو نسخه نامپای می‌باشد).

حالا می‌توانیم به اجزای منحصر به فرد ساختارهای داده، بسته به شکل آنها دسترسی داشته باشیم. از آنجایی که d1 و vie هر دو ماتریس هستند، با استفاده از جفت شاخص، اندیس‌گذاری شده‌اند (به ترتیب [row,col]). با این وجود، d2 هنوز یک بردار یک بعدی است و یک اندیس تکی می‌گیرد.

در پایان، برخی از دیگر موارد مربوط به شکل ساختارهای داده را بررسی می‌کنیم. shape تعداد عناصر در هر بُعد را به ما می‌گوید. تابع size تعداد کل عناصر را برگردانده و متناظر با مقداری است که تابع len () برای کل ساختار داده برمی‌گرداند. سرانجام، تابع ndim تعداد ابعاد را به ما می‌گوید (یعنی d.ndim==len(d.shape)) و معادل «مرتبه‌ی» کل ساختار داده می‌باشد. (دوباره می‌گویم که دلیل وجود قابلیت اضافه، حفظ سازگاری می‌باشد).

نهایتاً، بیایید به شکل دقیق‌تری روش‌هایی را که  توسط آنها می‌توانیم به اجزا و یا زیرمجموعه‌های بزرگ‌تر از یک آرایه‌ی n بعدی بر اساس ndarr دسترسی داشته باشیم را مورد ملاحظه قرار دهیم. در لیست قبلی دیدیم که چگونه با تخصیص یک اندیس به هر بعد،  به یک جزء منحصر به فرد دسترسی پیدا کردیم. همچنین می‌توانیم آرایه‌های فرعی بزرگ‌تری از یک ساختار داده را با به کارگیری دو روش دیگر، با نام‌های برش و اندیس‌گذاری پیشرفته، تعیین کنیم. کدهای زیر مثال‌هایی از این قبیل را نشان می‌دهد. (دوباره، این را به عنوان بخش پرامپ واسطه پایتون استفاده کنید).

>>> import numpy as np

>>> # Create a 12-element vector and reshape into 3×4 matrix

>>> d = np.linspace( 0, 11, 12 )

>>> d.shape = ( 3,4 )

>>> print d

[[ ۰. ۱. ۲. ۳.]

[ ۴. ۵. ۶. ۷.]

[ ۸. ۹. ۱۰. ۱۱.]]

>>> # Slicing…

>>> # First row

>>> print d[0,:]

[ ۰. ۱. ۲. ۳.]

>>> # Second col

>>> print d[:,1]

[ ۱. ۵. ۹.]

>>> # Individual element: scalar

>>> print d[0,1]

۱.۰

>>> # Subvector of shape 1

>>> print d[0:1,1]

[ ۱.]

>>> # Subarray of shape 1×1

>>> print d[0:1,1:2]

[[ ۱.]]

>>> # Indexing…

>>> # Integer indexing: third and first column

>>> print d[ :, [2,0] ]

[[ ۲. ۰.]

[ ۶. ۴.]

[ ۱۰. ۸.]]

>>> # Boolean indexing: second and third column

>>> k = np.array( [False, True, True] )

>>> print d[ k, : ]

[[ ۴. ۵. ۶. ۷.]

[ ۸. ۹. ۱۰. ۱۱.]]

 ابتدا یک بردار ۱۲ عضوی ایجاد می‌کنیم و مانند قبل آن را به یک ماتریس ۴×۳ تغییر شکل می‌دهیم. روش برش از یک ترکیب برش پایتون استاندارد start:stop:step استفاده می‌کند، به طوری که موقعیت شروع (start) جامع و کلی می‌باشد اما وضعیت توقف (stopping) منحصر به فرد است (در لیست، من فقط از ساده‌ترین شکل برش، یعنی انتخاب تمام اعضای قابل دسترسی، استفاده کردم).

دو شکل بالقوه برای برش وجود دارد. مورد اول، یک شاخص برنامه‌نویسی صریح و روشن (نه یک برش!) را تعیین می‌کند که بعد متناظر را به یک اسکالر کاهش می‌دهد. هرچند، برش از نظر ابعادی ساختار داده را کاهش نمی‌دهد. دو حالت متفاوت را در نظر بگیرید: در عبارت d[0,1]، اندیس‌های هر دو بعد کاملاً مشخص شده‌اند، و در نتیجه فقط یک اسکالر برای ما می‌ماند. در مقابل، d[0:1,1:2] در دو بعد برش خورده است. هیچ یک از ابعاد حذف نشدند، و شیء حاصل شده کماکان یک ماتریس (دو بعدی) اما با سایز کوچک‌تر است: به صورت ۱×۱ می‌باشد.

موضوع دومی که باید مورد توجه قرار گیرد، این است که برش‌ها، view را برگردان می‌کنند، نه کپی را.

علاوه بر برش، ما می‌توانیم یک آرایه‌ی n بعدی را با برداری از اندیس‌ها و توسط عملیاتی که به آن «اندیس‌گذاری پیشرفته» می‌گویند، شاخص گذاری نماییم. در قبل با استفاده از عملیات لیست کردن دو مثال ساده را نشان دادیم. در اولی از یک «شیء لیست پایتون» که شامل اندیس‌های عدد صحیح (یعنی موقعیت‌ها) برای ستون‌های مدنظر و ترتیب مناسب آنها بود، استفاده کردیم تا زیرمجموعه‌ای از ستون‌ها را انتخاب کنیم. در مثال دوم، یک آرایه‌ی n بعدی از ورودی‌های برنولی را ایجاد کردیم تا فقط آن ردیف‌هایی را انتخاب کنیم که داده‌ی بولی مقدار درست می‌گیرد. برخلاف برش، اندیس‌گذاری پیشرفته کپی‌ها را برگردان می‌کند، نه نماها را.

 

۰ پاسخ به "معرفی ماژول نامپای در پایتون (Numpy)"

ارسال یک پیغام

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

© دیاکو دانش افزار.

@DeyakoLTD

ما را در تلگرام دنبال کنید.

مشاهده کانال
بستن
X