بلاگ

چطوری حافظه و زمان اجرا را در پایتون و ژوپیتر نوت بوک حساب کنیم؟

چطوری حافظه و زمان اجرا را در پایتون و ژوپیتر نوت بوک حساب کنیم؟

قرار هستش در این اموزش با روش های ساده تا پیشرفته محاسبه زمان اجرا و وضعیت مصرف حافظه در پایتون رو با هم بررسی کنم پس ما من همراه باشید.

 

خیلی خلاصه بگم که توی ژوپیتر نوت بوک ابزار مورد علاقه من کلی ابزار هست که میتونیم از اونها استفاده کنیم و زمان اجرا رو محاسبه کنیم در کنارش توی پایتون خالص هم روش / روش هایی برای اینکار هست که در این مقاله با توجه به رویه یک دانشمند داده و مهندسی یادگیری ماشین بیشتر درباره ژوپیتر نوت بوک صحبت میکنم.

لینک ویدیویی روش های بررسی مدت زمان اجرای توابع و برنامه در پایتون و ژوپیتر نوت بوک در یوتوب

اگر که میخواهید میتونید راهنمای جامع ژوپیتر نوت بوک به صورت پست بلاگ رو مشاهده کنید و همچنین پلی لیست یوتوب ژوپیتر نوت بوک رو بررسی کنید.

پیش نیاز:

  • دانش اولیه پایتونی
  • دانش اولیه کار با ژوپیتر نوت بوک

جدول محتوایی

  • روش های محاسب زمان اجرا در پایتون (خالص)
  • روش های محاسبه زمان اجرا در ژوپیتر نوت بوک
  • محاسبه مصرف Ram در ژوپیتر نوت بوک

 

روش های محاسبه زمان اجرا در پایتون

 

برای محاسبه در قالب یک سری کد پایتونی ساده ترین روش استفاده از کتابخونه time و محاسبه قبل و بعد از اجرای یک یا چند خط کد میباشد.

قبل از اینکه کد اصلی رو بزنم باید یک کدی داشته باشیم که اجرا بشه برای شروه فاکتوریل ۱۰۰۰ که مودش رو بر ۲ بگیریم :

js
import math 
def my_code():
    print(f"factorial of 1000 % 2 is: {math.factorial(1000) % 2}")

حالا کد اصلی که با استفاده از time.clock() هست و میایم قبل و بعد از کد قرار میدیم:

python
import time

start_time = time.clock()
### codes
my_code()
###
print (time.clock() - start_time, "seconds")

این روش دو عیب داره برامون اینکه ما نمیدونیم هر سطر چقدر منابع مصرف میکنه و اینکه به صورت متوسط چقدر منابع مصرف میشه که بریم به بخش بعدی تا برای هر کدوم از این عیب ها بهتون بگم که jupyter notebook چه خوابی برامون دیده ( البته که Ipython باید گفت)

دو مشکل داریم:

۱. زمان اجرای متوسط رو نداریم

۲. زمان اجرای هر بخش رو نداریم

روش های محاسبه زمان اجرا در ژوپیتر نوت بوک

 

فعلا هنوز کد قبلیمون رو داریم و به نظرم به اندازه کافی خوب هستش برای اینجا بریم و بررسی کنیم

%time به خاطر اینکه ما دفعات زیادی کد رو اجرا میکنیم و دستور print خروجی رو شلوع میکنه و io رو درگیر من کد مرجع رو تغییر دادم به:

python
import math 
def my_code():
save_some_where = "factorial of 1000 % 2 is: {math.factorial(1000) % 2}"

از بخش توابع جادویی با یک تابع جادویی به اسم time  اشنا شدیم که برامون دقیقا مثل قسمت قبل مقدارش رو برمیگردونه برای مثال ببینید:

python
%time my_code()

البته یک قاعده ای رو داریم اگر دوتا % بذاریم پشت time برای کل اون سلول زمان رو حساب میکنه که زمانی که چند خط داریم از این رویه استفاده میکنیم ( یا خوانایی بهتر)

python
%%time 
my_code()
my_code()

این تا حدی خوبه که با جزییات ما میدونیم هر بخش چقدر زمان صرفش شده ولی بهمون هر دفعه یک رقم جدید میده و مشکل اول زمان اجرای متوسط رو نداریم رو اینجا قرار هست با دستور timeit حل کنیم!

python
%timeit

 

این دستور برای ما میاد و کدی / کدهایی که گفتیم رو چندین بار اجرا میکنه و بهمون یک خلاصه ای از عملکردش رو میده

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

python
%timeit my_code()

 

البته باز هم نسخه چند خطی اون با %% در دسترس هست

python
%%timeit 
my_code()
my_code()

قبل از اینکه جلو تر بریم بیاییم ببینیم چه چیز هایی رو بهمون میگه:

143 ns ± 3.93 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

به طور متوسط ۱۴۳ نانو ثانیه زمان اجرا دستورات ماست و  std ما ( انحراف معیار) ما حدود ۴ نانو ثانیه هست در کنارش دو چیز رو مطرح میکنه که این بر اساس یک تعداد run و یک تعداد loop هست

loop میشه گفت اجرای از اول کد هستش

run اجرای اون کد هستش

 

البته که میتونیم با دادن پارامتر هایی این رو تعیین کنیم

%timeit -n10 -r5 my_code()

که n  تعداد اجراست و r تعداد حلقه هایی باید اجرا بشه

دیدن لیست کامل پارامتر ها و توضیحات این تابع

برای گرفتن خروجی این تابع مجیک میتوینم با فلگ o خروجیش رو در یک متغییر ذخیره کنیم

output_of_test = %timeit -n10 -r5 -o my_code()

که این خروجیش میتونیم ازش مورادی مثل بهترین ها و بدترین زمان های اجرا رو بگیریم

python
# best time
times.best
# worst time
times.worst
#all times
times.timings

تا اینجا یکی از مشکلاتی که گفتم یعنی متوسط زمان اجرا رو حل کردیم ولی هنوز بهمون میزانی که هر خط کد طول میکشه اجرا بشه رو نمیگه بریم به بخش استفاده از ماژول های line_profiler و memory_profiler در ژوپیتر برای تشخیص میزان مصرف منابع و حافظه خط به خط کد پایتون:

 

نصب و استفاده از ماژول  line_profiler برای تشخیص زمان اجرای خط به خط کد

با استفاده از ماژول line_profiler در ژوپیتر و Ipython میتونیم زمان اجرای خط به خط پایتون رو در بیاریم و بعدا بتونیم با این اطلاعات بهبود بدیم اون رو

ابتدا اون رو نصب کنیم با کمک دستور pip

bash
pip install line_profiler

بعد از نصب میتونید با کمک load_ext اون رو توی ژوپیتر لود کنید

python
%load_ext line_profiler

و قبل از اینکه بریم و یک کدی بنویسیم که چندین قسمت داره تا درک بهتری داشته باشیم به کار.

python
def my_code():
    factorial_5 = math.factorial(5)
    factorial_100 = math.factorial(100)
    factorial_250 = math.factorial(250)
    factorial_50 = math.factorial(50)

چیزی که میدونیم اینکه فاکتوریل اعداد بزرگتر بیشتر طول میکشه و میخواهیم بررسی کنیم 250 چقدر بیشتر از بقیه طول میکشه. برای اینجا بعد از لود باید از lprun استفاده کنیم به این شکل که با فلگ f اسم تابع و خود تابع رو بفرستیم.

python
%lprun -f my_code my_code()

که بهمون این خروجی رو میده:

bash
Timer unit: 1e-06 s

Total time: 5.3e-05 s
File: 
Function: my_code at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def my_code():
     2         1         11.0     11.0     20.8      factorial_5 = math.factorial(5)
     3         1         15.0     15.0     28.3      factorial_100 = math.factorial(100)
     4         1         22.0     22.0     41.5      factorial_250 = math.factorial(250)
     5         1          5.0      5.0      9.4      factorial_50 = math.factorial(50)

که الان میدونیم هر خط چقدر منابع مصرف میکنه حالا بیایم با یک کد دیگه هم بررسیش کنیم

python
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.datasets import load_iris
X , y = load_iris(return_X_y =True)
%lprun -f LinearRegression().fit LinearRegression().fit(X, y)

برای اینکه نشون بدم میشه از توابعی که حتی خودمون نساختیم هم استفاده کنیم اینجا یک مدل خیلی ساده با استفاده از LinearRegression و دیتاست ایریس ساختم و اجراش کردم ببینم چطوری اجرا میشه که نتیجه به این شکل شد:

text
Timer unit: 1e-06 s

Total time: 0.002846 s
File: /home/amin/.local/lib/python3.6/site-packages/sklearn/linear_model/_base.py
Function: fit at line 481

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   481                                               def fit(self, X, y, sample_weight=None):
   482                                                   """
   483                                                   Fit linear model.
   484                                           
   485                                                   Parameters
   486                                                   ----------
   487                                                   X : {array-like, sparse matrix} of shape (n_samples, n_features)
   488                                                       Training data
   489                                           
   490                                                   y : array-like of shape (n_samples,) or (n_samples, n_targets)
   491                                                       Target values. Will be cast to X's dtype if necessary
   492                                           
   493                                                   sample_weight : array-like of shape (n_samples,), default=None
   494                                                       Individual weights for each sample
   495                                           
   496                                                       .. versionadded:: 0.17
   497                                                          parameter \*sample_weight\* support to LinearRegression.
   498                                           
   499                                                   Returns
   500                                                   -------
   501                                                   self : returns an instance of self.
   502                                                   """
   503                                           
   504         1          9.0      9.0      0.3          n_jobs_ = self.n_jobs
   505         1          8.0      8.0      0.3          X, y = self._validate_data(X, y, accept_sparse=\['csr', 'csc', 'coo'\],
   506         1       1320.0   1320.0     46.4                                     y_numeric=True, multi_output=True)
   507                                           
   508         1          8.0      8.0      0.3          if sample_weight is not None:
   509                                                       sample_weight = _check_sample_weight(sample_weight, X,
   510                                                                                            dtype=X.dtype)
   511                                           
   512         1          7.0      7.0      0.2          X, y, X_offset, y_offset, X_scale = self._preprocess_data(
   513         1          6.0      6.0      0.2              X, y, fit_intercept=self.fit_intercept, normalize=self.normalize,
   514         1          6.0      6.0      0.2              copy=self.copy_X, sample_weight=sample_weight,
   515         1        999.0    999.0     35.1              return_mean=True)
   516                                           
   517         1          8.0      8.0      0.3          if sample_weight is not None:
   518                                                       # Sample weight can be implemented via a simple rescaling.
   519                                                       X, y = _rescale_data(X, y, sample_weight)
   520                                           
   521         1         10.0     10.0      0.4          if sp.issparse(X):
   522                                                       X_offset_scale = X_offset / X_scale
   523                                           
   524                                                       def matvec(b):
   525                                                           return X.dot(b) - b.dot(X_offset_scale)
   526                                           
   527                                                       def rmatvec(b):
   528                                                           return X.T.dot(b) - X_offset_scale \* np.sum(b)
   529                                           
   530                                                       X_centered = sparse.linalg.LinearOperator(shape=X.shape,
   531                                                                                                 matvec=matvec,
   532                                                                                                 rmatvec=rmatvec)
   533                                           
   534                                                       if y.ndim < 2:
   535                                                           out = sparse_lsqr(X_centered, y)
   536                                                           self.coef_ = out\[0\]
   537                                                           self._residues = out\[3\]
   538                                                       else:
   539                                                           # sparse_lstsq cannot handle y with shape (M, K)
   540                                                           outs = Parallel(n_jobs=n_jobs_)(
   541                                                               delayed(sparse_lsqr)(X_centered, y\[:, j\].ravel())
   542                                                               for j in range(y.shape\[1\]))
   543                                                           self.coef_ = np.vstack(\[out\[0\] for out in outs\])
   544                                                           self._residues = np.vstack(\[out\[3\] for out in outs\])
   545                                                   else:
   546                                                       self.coef_, self._residues, self.rank_, self.singular_ = \\
   547         1        412.0    412.0     14.5                  linalg.lstsq(X, y)
   548         1          6.0      6.0      0.2              self.coef_ = self.coef_.T
   549                                           
   550         1          3.0      3.0      0.1          if y.ndim == 1:
   551         1         15.0     15.0      0.5              self.coef_ = np.ravel(self.coef_)
   552         1         27.0     27.0      0.9          self._set_intercept(X_offset, y_offset, X_scale)
   553         1          2.0      2.0      0.1          return self

نصب و استفاده از ماژول  memory_profiler برای تشخیص میزان رم هر خط

ماژول بعدی که میخوام دربارش صحبت کنم memory_profiler هست که با استفاده از اون میتونیم خط به خط میزان استفاده رو توی پایتون مشخص کنیم.

برای شروع با دستور زیر اون رو نصب میکنیم

bash
pip install memory_profiler

بعد از نصب میتونید با کمک load_ext اون رو توی ژوپیتر لود کنید

python
%load_ext memory_profiler

دقیقا مثل دستور قبلی و اینجا از mprun استفاده میکنیم برای نمونه برای تابعی که فاکتوریل های مختلفی اجرا میکرد:

python
%mprun -f LinearRegression().fit LinearRegression().fit(X, y)

همانطور که میبینید در خط اول مکان دقیق اون تابع رو برای ما مشخص کردم و از طرفی طبق پیامی که من گرفتم اجازه اجرا

NOTE: %mprun can only be used on functions defined in physical files, and not in the IPython environment.

رو به ما میده که متاسفانه نمیتونم از فانکشن هایی که توی ژوپیتر تعریف میکنیم این تابع رو استفاده کنیم.

خب اینجا هم خروجی رو بینید که برای اون تابع fit هست:

text
Filename: /home/amin/.local/lib/python3.6/site-packages/sklearn/linear_model/_base.py

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
   481     99.5 MiB     99.5 MiB           1       def fit(self, X, y, sample_weight=None):
   482                                                 """
   483                                                 Fit linear model.
   484                                         
   485                                                 Parameters
   486                                                 ----------
   487                                                 X : {array-like, sparse matrix} of shape (n_samples, n_features)
   488                                                     Training data
   489                                         
   490                                                 y : array-like of shape (n_samples,) or (n_samples, n_targets)
   491                                                     Target values. Will be cast to X's dtype if necessary
   492                                         
   493                                                 sample_weight : array-like of shape (n_samples,), default=None
   494                                                     Individual weights for each sample
   495                                         
   496                                                     .. versionadded:: 0.17
   497                                                        parameter \*sample_weight\* support to LinearRegression.
   498                                         
   499                                                 Returns
   500                                                 -------
   501                                                 self : returns an instance of self.
   502                                                 """
   503                                         
   504     99.5 MiB      0.0 MiB           1           n_jobs_ = self.n_jobs
   505     99.5 MiB      0.0 MiB           1           X, y = self._validate_data(X, y, accept_sparse=\['csr', 'csc', 'coo'\],
   506     99.5 MiB      0.0 MiB           1                                      y_numeric=True, multi_output=True)
   507                                         
   508     99.5 MiB      0.0 MiB           1           if sample_weight is not None:
   509                                                     sample_weight = _check_sample_weight(sample_weight, X,
   510                                                                                          dtype=X.dtype)
   511                                         
   512     99.5 MiB      0.0 MiB           1           X, y, X_offset, y_offset, X_scale = self._preprocess_data(
   513     99.5 MiB      0.0 MiB           1               X, y, fit_intercept=self.fit_intercept, normalize=self.normalize,
   514     99.5 MiB      0.0 MiB           1               copy=self.copy_X, sample_weight=sample_weight,
   515     99.5 MiB      0.0 MiB           1               return_mean=True)
   516                                         
   517     99.5 MiB      0.0 MiB           1           if sample_weight is not None:
   518                                                     # Sample weight can be implemented via a simple rescaling.
   519                                                     X, y = _rescale_data(X, y, sample_weight)
   520                                         
   521     99.5 MiB      0.0 MiB           1           if sp.issparse(X):
   522                                                     X_offset_scale = X_offset / X_scale
   523                                         
   524                                                     def matvec(b):
   525                                                         return X.dot(b) - b.dot(X_offset_scale)
   526                                         
   527                                                     def rmatvec(b):
   528                                                         return X.T.dot(b) - X_offset_scale \* np.sum(b)
   529                                         
   530                                                     X_centered = sparse.linalg.LinearOperator(shape=X.shape,
   531                                                                                               matvec=matvec,
   532                                                                                               rmatvec=rmatvec)
   533                                         
   534                                                     if y.ndim < 2:
   535                                                         out = sparse_lsqr(X_centered, y)
   536                                                         self.coef_ = out\[0\]
   537                                                         self._residues = out\[3\]
   538                                                     else:
   539                                                         # sparse_lstsq cannot handle y with shape (M, K)
   540                                                         outs = Parallel(n_jobs=n_jobs_)(
   541                                                             delayed(sparse_lsqr)(X_centered, y\[:, j\].ravel())
   542                                                             for j in range(y.shape\[1\]))
   543                                                         self.coef_ = np.vstack(\[out\[0\] for out in outs\])
   544                                                         self._residues = np.vstack(\[out\[3\] for out in outs\])
   545                                                 else:
   546                                                     self.coef_, self._residues, self.rank_, self.singular_ = \\
   547     99.5 MiB      0.0 MiB           1                   linalg.lstsq(X, y)
   548     99.5 MiB      0.0 MiB           1               self.coef_ = self.coef_.T
   549                                         
   550     99.5 MiB      0.0 MiB           1           if y.ndim == 1:
   551     99.5 MiB      0.0 MiB           1               self.coef_ = np.ravel(self.coef_)
   552     99.5 MiB      0.0 MiB           1           self._set_intercept(X_offset, y_offset, X_scale)
   553     99.5 MiB      0.0 MiB           1           return self