9. پایتون برای محاسبات علمی#
“ما باید کاراییهای کوچک را فراموش کنیم، بگوییم حدود 97 درصد از زمان: بهینهسازی زودرس ریشه همه بدیهاست.” – دونالد کنوت
9.1. مرور کلی#
احتمالاً میتوان با اطمینان گفت که پایتون محبوبترین زبان برای محاسبات علمی است.
این به دلیل موارد زیر است:
ماهیت قابل دسترس و گویا بودن خود زبان،
دامنه عظیم کتابخانههای علمی با کیفیت بالا،
این واقعیت که زبان و کتابخانهها متنباز هستند،
نقش محوری که پایتون در علم داده، یادگیری ماشین و هوش مصنوعی ایفا میکند.
در سخنرانیهای قبلی، از برخی کتابخانههای علمی پایتون، از جمله NumPy و Matplotlib استفاده کردیم.
با این حال، تمرکز اصلی ما بر زبان پایتون بود، نه کتابخانهها.
اکنون به کتابخانههای علمی میپردازیم و تمام توجه خود را به آنها معطوف میکنیم.
در این سخنرانی مقدماتی، موضوعات زیر را مورد بحث قرار خواهیم داد:
عناصر اصلی اکوسیستم علمی پایتون چیست؟
آنها چگونه با هم جور میشوند؟
وضعیت در طول زمان چگونه در حال تغییر است؟
علاوه بر آنچه در Anaconda موجود است، این سخنرانی به موارد زیر نیاز دارد:
!pip install quantecon
بیایید با برخی import ها شروع کنیم:
import numpy as np
import quantecon as qe
import matplotlib.pyplot as plt
import random
9.2. کتابخانههای علمی اصلی#
بیایید به طور خلاصه کتابخانههای علمی پایتون را مرور کنیم.
9.2.1. چرا به آنها نیاز داریم؟#
ما به کتابخانههای علمی پایتون به دو دلیل نیاز داریم:
پایتون کوچک است
پایتون کند است
پایتون کوچک است
هسته پایتون به طور طراحی کوچک است – این به بهینهسازی، دسترسیپذیری و نگهداری کمک میکند.
کتابخانههای علمی روالهایی را فراهم میکنند که نمیخواهیم – و احتمالاً نباید – خودمان بنویسیم.
انتگرالگیری عددی، درونیابی، جبر خطی، یافتن ریشه و غیره.
پایتون کند است
دلیل دیگری که به کتابخانههای علمی نیاز داریم این است که پایتون خالص نسبتاً کند است.
کتابخانههای علمی اجرا را با استفاده از سه استراتژی اصلی تسریع میکنند:
برداریسازی: ارائه کد ماشین کامپایلشده و رابطهایی که این کد را قابل دسترس میکنند
کامپایل JIT: کامپایلرهایی که دستورات شبیه پایتون را به کد ماشین سریع در زمان اجرا تبدیل میکنند
موازیسازی: توزیع وظایف در چندین رشته / CPU / GPU / TPU
ما این ایدهها را به طور عمیق در ادامه بحث خواهیم کرد.
9.2.2. اکوسیستم علمی پایتون#
در QuantEcon، کتابخانههای علمی که بیشترین استفاده را از آنها میکنیم عبارتند از:
اینگونه با هم جور میشوند:
NumPy با ارائه یک نوع داده آرایهای پایه (به بردارها و ماتریسها فکر کنید) و توابعی برای عمل بر روی این آرایهها (به عنوان مثال، ضرب ماتریسی)، پایهها را تشکیل میدهد.
SciPy بر روی NumPy با افزودن روشهای عددی که به طور معمول در علم استفاده میشوند (درونیابی، بهینهسازی، یافتن ریشه و غیره) ساخته میشود.
Matplotlib برای تولید شکلها، با تمرکز بر رسم دادههای ذخیره شده در آرایههای NumPy استفاده میشود.
JAX شامل عملیات پردازش آرایه مشابه NumPy، مشتقگیری خودکار، یک کامپایلر just-in-time با محوریت موازیسازی، و یکپارچهسازی خودکار با شتابدهندههای سختافزاری مانند GPUها است.
Pandas انواع و توابعی را برای دستکاری دادهها فراهم میکند.
Numba یک کامپایلر just-in-time فراهم میکند که با NumPy به خوبی کار میکند و به تسریع کد پایتون کمک میکند.
ما همه این کتابخانهها را به طور گسترده در این مجموعه سخنرانیها مورد بحث قرار خواهیم داد.
9.3. چرا Python خالص کند است؟#
همانطور که در بالا ذکر شد، کد عددی نوشتهشده در Python خالص نسبتاً کند است.
بیایید بکوشیم بفهمیم چه چیزی موجب سرعتهای اجرای کند میشود.
9.3.1. بررسی نوع#
یکی از منابع سربار در عملیات Python خالص، بررسی نوع است.
بیایید بکوشیم مسائل را درک کنیم.
9.3.1.1. تایپگذاری پویا#
این عملیات Python را در نظر بگیرید
a, b = 10, 10
a + b
20
حتی برای این عملیات ساده، مفسر Python کار قابلتوجهی باید انجام دهد.
برای مثال، در عبارت a + b، مفسر باید بداند کدام عملیات را فراخوانی کند.
اگر a و b رشته باشند، آنگاه a + b به الحاق رشتهها نیاز دارد
a, b = 'foo', 'bar'
a + b
'foobar'
اگر a و b لیست باشند، آنگاه a + b به الحاق لیستها نیاز دارد
a, b = ['foo'], ['bar']
a + b
['foo', 'bar']
در نتیجه، هنگام اجرای a + b، Python ابتدا باید نوع اشیاء را بررسی کند و سپس عملیات صحیح را فراخوانی کند.
این فرایند سربار به همراه دارد.
اگر این عبارت را بارها در یک حلقه فشرده اجرا کنیم، سربار بزرگ میشود.
9.3.1.2. انواع ایستا#
زبانهای کامپایلشده با استفاده از انواع صریح و ایستا از این سربارها اجتناب میکنند.
برای مثال، کد C زیر را در نظر بگیرید که اعداد صحیح ۱ تا ۱۰ را جمع میکند
#include <stdio.h>
int main(void) {
int i;
int sum = 0;
for (i = 1; i <= 10; i++) {
sum = sum + i;
}
printf("sum = %d\n", sum);
return 0;
}
متغیرهای i و sum به صراحت به عنوان اعداد صحیح اعلام شدهاند.
علاوه بر این، وقتی عبارتی مانند int i مینویسیم، به کامپایلر قول میدهیم که i در طول اجرای برنامه همواره یک عدد صحیح خواهد بود.
بدین ترتیب، معنای جمع در عبارت sum + i کاملاً بدون ابهام است.
نیازی به بررسی نوع وجود ندارد و از این رو هیچ سرباری وجود ندارد.
9.3.2. دسترسی به داده#
یکی دیگر از عوامل کاهش سرعت در زبانهای سطح بالا، دسترسی به داده است.
برای نشان دادن این موضوع، مسئله جمع کردن برخی دادهها — مثلاً مجموعهای از اعداد صحیح — را در نظر بگیرید.
9.3.2.1. جمعزدن با کد کامپایلشده#
در C یا Fortran، آرایهای از اعداد صحیح در یک بلوک پیوسته از حافظه ذخیره میشود
برای مثال، یک عدد صحیح ۶۴ بیتی در ۸ بایت از حافظه ذخیره میشود.
آرایهای از \(n\) عدد صحیح از این نوع، \(8n\) بایت متوالی را اشغال میکند.
علاوه بر این، نوع داده در زمان کامپایل مشخص است.
از این رو، میتوان به هر نقطه داده متوالی با جابهجایی به جلو در فضای حافظه به اندازهای معین و ثابت دسترسی یافت.
9.3.2.2. جمعزدن در Python خالص#
Python تلاش میکند تا حدی این ایدهها را بازتولید کند.
برای مثال، در پیادهسازی استاندارد Python (CPython)، عناصر لیست در مکانهای حافظهای قرار میگیرند که به نوعی پیوسته هستند.
با این حال، این عناصر لیست بیشتر شبیه اشارهگر به داده هستند تا دادههای واقعی.
از این رو، دسترسی به مقادیر داده هنوز هم سربار دارد.
چنین سرباری یکی از عوامل اصلی اجرای کند است.
9.3.3. خلاصه#
آیا بحث فوق به این معناست که باید همه چیز را به C یا Fortran تبدیل کنیم؟
پاسخ این است: قطعاً نه!
برای هر برنامهای، تنها تعداد نسبتاً کمی از خطوط از نظر زمانی بحرانی هستند.
از این رو بسیار کارآمدتر است که بیشتر کدهای خود را در یک زبان با بهرهوری بالا مانند Python بنویسیم.
علاوه بر این، حتی برای آن دسته از خطوط کدی که واقعاً از نظر زمانی بحرانی هستند، اکنون میتوانیم با استفاده از کتابخانههای علمی Python به عملکرد برنامههای باینری کامپایلشده از C یا Fortran برسیم یا از آنها پیشی بگیریم.
در این راستا، تأکید میکنیم که در چند سال اخیر، شتاببخشی به کد عملاً مترادف با موازیسازی شده است.
این وظیفه بهتر است به کامپایلرهای تخصصی سپرده شود!
9.4. تسریع پایتون#
در این بخش به سه تکنیک مرتبط برای تسریع کد پایتون نگاه میکنیم.
در اینجا بر ایدههای بنیادی تمرکز خواهیم کرد.
بعداً به کتابخانههای خاص و نحوه پیادهسازی این ایدهها توسط آنها نگاه خواهیم کرد.
9.4.1. برداریسازی#
یکی از روشها برای اجتناب از ترافیک حافظه و بررسی نوع، برنامهنویسی آرایهای است.
بسیاری از اقتصاددانان معمولاً به برنامهنویسی آرایهای به عنوان “برداریسازی” اشاره میکنند.
Note
در علوم کامپیوتر، این اصطلاح معنای کمی متفاوت دارد.
ایده کلیدی این است که عملیات پردازش آرایه را به صورت دستهای به کد ماشین بومی از پیش کامپایل شده و کارآمد ارسال کنیم.
خود کد ماشین معمولاً از C یا Fortran که به دقت بهینه شدهاند کامپایل میشود.
به عنوان مثال، هنگام کار در یک زبان سطح بالا، عملیات معکوس کردن یک ماتریس بزرگ میتواند به کد ماشین کارآمد که از پیش برای این منظور کامپایل شده و به عنوان بخشی از یک بسته به کاربران ارائه شده است، پیمانکاری شود.
مزایای اصلی عبارتند از:
بررسی نوع به ازای هر آرایه پرداخت میشود، نه به ازای هر عنصر، و
آرایههای حاوی عناصر با یک نوع داده از نظر دسترسی به حافظه کارآمد هستند.
ایده برداریسازی به MATLAB برمیگردد که به طور گسترده از برداریسازی استفاده میکند.
9.4.2. برداریسازی در مقابل حلقههای پایتون خالص#
بیایید یک مقایسه سریع سرعت را امتحان کنیم تا نشان دهیم چگونه برداریسازی میتواند کد را تسریع کند.
در اینجا مقداری کد غیر برداری شده وجود دارد که از یک حلقه بومی پایتون برای تولید، مجذور کردن و سپس جمع کردن تعداد زیادی متغیر تصادفی استفاده میکند:
n = 1_000_000
with qe.Timer():
y = 0 # Will accumulate and store sum
for i in range(n):
x = random.uniform(0, 1)
y += x**2
0.2348 seconds elapsed
کد برداری شده زیر از NumPy استفاده میکند که به زودی آن را به تفصیل بررسی خواهیم کرد، تا همان کار را انجام دهد.
rng = np.random.default_rng()
with qe.Timer():
x = rng.uniform(0, 1, n)
y = np.sum(x**2)
0.0068 seconds elapsed
همانطور که میبینید، بلوک کد دوم بسیار سریعتر اجرا میشود.
این حلقه را به سه عملیات اساسی تقسیم میکند:
nuniform را بکشیدآنها را مجذور کنید
آنها را جمع کنید
اینها به عنوان عملگرهای دستهای به کد ماشین بهینه شده ارسال میشوند.
9.4.3. کامپایلرهای JIT#
در بهترین حالت، برداریسازی کد سریع و ساده ارائه میدهد.
با این حال، بدون معایب نیست.
یکی از مسائل این است که میتواند بسیار فشرده از نظر حافظه باشد.
این به این دلیل است که برداریسازی تمایل به ایجاد آرایههای میانی زیادی قبل از تولید محاسبه نهایی دارد.
مسئله دیگر این است که همه الگوریتمها نمیتوانند برداری شوند.
به دلیل این مسائل، بیشتر محاسبات با کارایی بالا از برداریسازی سنتی دور میشوند و به سمت استفاده از کامپایلرهای just-in-time حرکت میکنند.
در سخنرانیهای بعدی در این مجموعه، در مورد چگونگی بهرهبرداری کتابخانههای مدرن پایتون از کامپایلرهای just-in-time برای تولید کد ماشین سریع، کارآمد و موازی یاد خواهیم گرفت.
9.5. موازیسازی#
رشد سرعت کلاک CPU (یعنی سرعتی که یک زنجیره منفرد منطقی میتواند اجرا شود) در سالهای اخیر به طور چشمگیری کند شده است.
طراحان تراشه و برنامهنویسان کامپیوتر با کندی با جستجوی مسیری متفاوت برای اجرای سریع پاسخ دادهاند: موازیسازی.
این امر شامل موارد زیر میشود:
افزایش تعداد CPUهای تعبیه شده در هر ماشین
اتصال شتابدهندههای سختافزاری مانند GPUها و TPUها
برای برنامهنویسان، چالش این بوده است که از این سختافزار با اجرای بسیاری از فرآیندها به صورت موازی بهرهبرداری کنند.
در زیر ما موازیسازی برای محاسبات علمی را با تمرکز بر موارد زیر بحث میکنیم:
ابزارها برای موازیسازی در پایتون و
چگونه این ابزارها میتوانند برای مسائل اقتصادی کمی به کار گرفته شوند.
9.5.1. موازیسازی بر روی CPUها#
بیایید دو نوع اصلی موازیسازی مبتنی بر CPU که معمولاً در محاسبات علمی استفاده میشود را مرور کنیم و مزایا و معایب آنها را بحث کنیم.
9.5.1.1. چندرشتهای#
چندرشتهای به معنای اجرای چندین رشته اجرایی در داخل یک فرآیند واحد است.
همه رشتهها فضای حافظه یکسانی را به اشتراک میگذارند، بنابراین میتوانند بدون کپی کردن دادهها از آرایههای یکسان بخوانند و در آنها بنویسند.
به عنوان مثال، وقتی یک عملیات عددی روی یک آرایه بزرگ روی یک لپتاپ مدرن اجرا میشود، بار کاری میتواند در میان هستههای چندگانه CPU ماشین تقسیم شود، با هر هسته که بخشی از آرایه را مدیریت میکند.
Note
پایتون بومی برای پیادهسازی چندرشتهای به دلیل برخی ویژگیهای طراحی قدیمی مشکل دارد. اما این یک محدودیت برای کتابخانههای علمی مانند NumPy و Numba نیست. توابع وارد شده از این کتابخانهها و کد JIT-compiled در محیطهای اجرای سطح پایین اجرا میشوند که محدودیتهای قدیمی پایتون اعمال نمیشود.
9.5.1.2. چندپردازشی#
چندپردازشی به معنای اجرای چندین فرآیند مستقل است که هر کدام فضای حافظه جداگانه خود را دارند.
از آنجا که حافظه مشترک نیست، فرآیندها با انتقال داده بین خود ارتباط برقرار میکنند.
چندپردازشی میتواند روی یک ماشین واحد یا در کلاسترهایی از ماشینهای متصل شده توسط یک شبکه توزیع شود.
9.5.1.3. کدام را باید استفاده کنیم؟#
برای کار عددی روی یک ماشین واحد، چندرشتهای معمولاً ترجیح داده میشود — سبک است و مدل حافظه مشترک بسیار راحت است.
چندپردازشی زمانی اهمیت پیدا میکند که مقیاسبندی فراتر از یک ماشین واحد ضروری باشد.
برای اکثریت قریب به اتفاق کاری که ما در این درسها انجام میدهیم، چندرشتهای کافی خواهد بود.
9.5.2. شتابدهندههای سختافزاری#
منبع چشمگیرتری از موازیسازی از شتابدهندههای سختافزاری تخصصی، به ویژه GPUها (واحدهای پردازش گرافیکی) به وجود میآید.
GPUها در ابتدا برای رندرینگ گرافیک طراحی شدند که نیاز به انجام همزمان یک عملیات روی بسیاری از پیکسلها دارد.
این معماری — هزاران هسته ساده که یک دستورالعمل یکسان را روی نقاط داده مختلف اجرا میکنند — برای محاسبات علمی ایدهآل است.
Note
یک هسته یک واحد پردازشی مستقل در داخل یک تراشه است — مداری که میتواند دستورالعملها را به تنهایی اجرا کند. یک CPU معمولاً تعداد کمی هسته قدرتمند دارد که هر کدام قادر به مدیریت دنبالههای پیچیده از عملیات هستند. یک GPU در عوض هزاران هسته کوچکتر و سادهتر را در خود جای داده است که هر کدام برای انجام عملیات حسابی پایه طراحی شدهاند. قدرت GPU از داشتن همه این هستهها که به طور همزمان روی قطعات مختلف یک مسئله کار میکنند ناشی میشود.
وقتی یک محاسبه میتواند به عنوان عملیات مستقل بر روی آرایههای بزرگ داده بیان شود، GPUها میتوانند درجههای بزرگی سریعتر از CPUها باشند.
TPUها (واحدهای پردازش تانسور)، که توسط گوگل برای یادگیری ماشین طراحی شدهاند، از فلسفه مشابهی پیروی میکنند و برای عملیات ماتریسی موازی عظیم بهینهسازی شدهاند.
9.5.3. دسترسی به منابع GPU#
بسیاری از ایستگاههای کاری و لپتاپها اکنون با GPUهای قابل استفاده ارائه میشوند و یک GPU مدرن تکی اغلب برای پروژههای تحقیقاتی فردی کافی است.
کتابخانههای مدرن پایتون مانند JAX که به طور گسترده در این مجموعه درسها مورد بحث قرار میگیرند، به طور خودکار GPUهای موجود را با تغییرات حداقلی در کد تشخیص داده و استفاده میکنند.
برای مسائل در مقیاس بزرگتر، سرورهای چند GPU (اغلب 4 تا 8 GPU در هر ماشین) به طور فزایندهای رایج هستند.
با نرمافزار مناسب، محاسبات میتوانند در چندین GPU توزیع شوند، یا در یک سرور واحد یا در یک کلاستر.
ما محاسبات GPU را با جزئیات بیشتری در درسهای بعدی بررسی خواهیم کرد و آن را در طیف وسیعی از کاربردهای اقتصادی به کار خواهیم برد.