.. _performance-howto: ============================== كيفية التحسين من أجل السرعة ============================== يقدم ما يلي بعض الإرشادات العملية لمساعدتك في كتابة تعليمات برمجية فعالة لمشروع scikit-learn. .. note:: على الرغم من أنه من المفيد دائمًا تنميط التعليمات البرمجية الخاصة بك للتحقق من **افتراضات الأداء**، يُوصى بشدة أيضًا بـ **مراجعة الأدبيات** للتأكد من أن الخوارزمية المُنفذة هي أحدث ما توصلت إليه التكنولوجيا للمهمة قبل الاستثمار في تحسين التنفيذ المُكلف. مرارًا وتكرارًا، أصبحت ساعات الجهود المبذولة في تحسين تفاصيل التنفيذ المعقدة غير ذات صلة باكتشاف **حيل خوارزمية** بسيطة لاحقًا، أو باستخدام خوارزمية أخرى أكثر ملاءمة للمشكلة تمامًا. يقدم القسم :ref:`warm-restarts` مثالاً على إحدى هذه الحيل. Python أو Cython أو C/C++؟ =========================== .. currentmodule:: sklearn بشكل عام، يؤكد مشروع scikit-learn على **قابلية قراءة** شفرة المصدر لتسهيل على مستخدمي المشروع الغوص في شفرة المصدر لفهم كيفية تصرف الخوارزمية على بياناتهم ولكن أيضًا لسهولة الصيانة (من قبل المطورين). عند تنفيذ خوارزمية جديدة، يُوصى **بالبدء بتنفيذها في Python باستخدام Numpy و Scipy** عن طريق الحرص على تجنب كود التكرار باستخدام التعابير الاصطلاحية المتجهة لهذه المكتبات. من الناحية العملية، هذا يعني محاولة **استبدال أي حلقات for متداخلة باستدعاءات لأساليب مصفوفة Numpy المكافئة**. الهدف هو تجنب إضاعة وحدة المعالجة المركزية للوقت في مترجم Python بدلاً من معالجة الأرقام لتناسب نموذجك الإحصائي. من الجيد عمومًا مراعاة نصائح أداء NumPy و SciPy: https://scipy.github.io/old-wiki/pages/PerformanceTips ومع ذلك، في بعض الأحيان لا يمكن التعبير عن الخوارزمية بكفاءة في كود Numpy متجه بسيط. في هذه الحالة، فإن الاستراتيجية المُوصى بها هي التالي: 1. **نمِّط** تنفيذ Python للعثور على الاختناق الرئيسي و اعزله في **دالة مستوى وحدة مخصصة**. سيتم إعادة تنفيذ هذه الدالة كوحدة نمطية ملحق مُجمَّعة. 2. إذا كان هناك تنفيذ **C/C++** جيد الصيانة BSD أو MIT لنفس الخوارزمية التي ليست كبيرة جدًا، فيمكنك كتابة **مغلِّف Cython** لها وتضمين نسخة من شفرة المصدر للمكتبة في شجرة مصدر scikit-learn: يتم استخدام هذه الاستراتيجية للفئات :class:`svm.LinearSVC` و :class:`svm.SVC` و :class:`linear_model.LogisticRegression` (مغلِّفات لـ liblinear و libsvm). 3. خلاف ذلك، اكتب إصدارًا مُحسَّنًا من دالة Python باستخدام **Cython** مباشرةً. تُستخدم هذه الاستراتيجية للفئات :class:`linear_model.ElasticNet` و :class:`linear_model.SGDClassifier` على سبيل المثال. 4. **انقل إصدار Python للدالة في الاختبارات** و استخدمه للتحقق من أن نتائج الملحق المُجمَّع تتوافق مع المعيار الذهبي، إصدار Python سهل التصحيح. 5. بمجرد تحسين الشفرة (وليس مجرد نقطة اختناق يمكن اكتشافها بواسطة التنميط)، تحقق مما إذا كان من الممكن الحصول على **توازي خشن الحبيبات** مناسب لـ **التعددية المعالجة** باستخدام فئة ``joblib.Parallel``. .. _profiling-python-code: تنميط كود Python ================== من أجل تنميط كود Python، نوصي بكتابة برنامج نصي يقوم بتحميل بياناتك وإعدادها ثم استخدام مُنمِّط IPython المُدمج لاستكشاف الجزء ذي الصلة من الشفرة بشكل تفاعلي. لنفترض أننا نريد تنميط وحدة عامل المصفوفة غير السالب لـ scikit-learn. لنقم بإعداد جلسة IPython جديدة وتحميل مجموعة بيانات الأرقام كما في المثال :ref:`sphx_glr_auto_examples_classification_plot_digits_classification.py`:: In [1]: from sklearn.decomposition import NMF In [2]: from sklearn.datasets import load_digits In [3]: X, _ = load_digits(return_X_y=True) قبل بدء جلسة التنميط والمشاركة في تكرارات التحسين المؤقتة، من المهم قياس إجمالي وقت التنفيذ للدالة التي نريد تحسينها دون أي نوع من النفقات العامة للمُنمِّط وحفظها في مكان ما للرجوع إليها لاحقًا:: In [4]: %timeit NMF(n_components=16, tol=1e-2).fit(X) 1 loops, best of 3: 1.7 s per loop لإلقاء نظرة على ملف تعريف الأداء العام باستخدام أمر ``%prun`` السحري:: In [5]: %prun -l nmf.py NMF(n_components=16, tol=1e-2).fit(X) 14496 استدعاء دالة في 1.682 ثانية لوحدة المعالجة المركزية مرتبة حسب: الوقت الداخلي تم تقليص القائمة من 90 إلى 9 بسبب التقييد <'nmf.py'> ncalls tottime percall cumtime percall filename:lineno(function) 36 0.609 0.017 1.499 0.042 nmf.py:151(_nls_subproblem) 1263 0.157 0.000 0.157 0.000 nmf.py:18(_pos) 1 0.053 0.053 1.681 1.681 nmf.py:352(fit_transform) 673 0.008 0.000 0.057 0.000 nmf.py:28(norm) 1 0.006 0.006 0.047 0.047 nmf.py:42(_initialize_nmf) 36 0.001 0.000 0.010 0.000 nmf.py:36(_sparseness) 30 0.001 0.000 0.001 0.000 nmf.py:23(_neg) 1 0.000 0.000 0.000 0.000 nmf.py:337(__init__) 1 0.000 0.000 1.681 1.681 nmf.py:461(fit) عمود ``tottime`` هو الأكثر إثارة للاهتمام: فهو يعطي إجمالي الوقت الذي تم قضاؤه في تنفيذ شفرة دالة معينة مع تجاهل الوقت الذي تم قضاؤه في تنفيذ الدوال الفرعية. يتم إعطاء إجمالي الوقت الحقيقي (الشفرة المحلية + استدعاءات الدوال الفرعية) بواسطة عمود ``cumtime``. لاحظ استخدام ``-l nmf.py`` الذي يقصر الإخراج على الأسطر التي تحتوي على سلسلة "nmf.py". هذا مفيد لإلقاء نظرة سريعة على النقطة الساخنة لوحدة nmf Python نفسها مع تجاهل أي شيء آخر. فيما يلي بداية ناتج نفس الأمر بدون عامل تصفية ``-l nmf.py``:: In [5] %prun NMF(n_components=16, tol=1e-2).fit(X) 16159 استدعاء دالة في 1.840 ثانية لوحدة المعالجة المركزية مرتبة حسب: الوقت الداخلي ncalls tottime percall cumtime percall filename:lineno(function) 2833 0.653 0.000 0.653 0.000 {numpy.core._dotblas.dot} 46 0.651 0.014 1.636 0.036 nmf.py:151(_nls_subproblem) 1397 0.171 0.000 0.171 0.000 nmf.py:18(_pos) 2780 0.167 0.000 0.167 0.000 {method 'sum' of 'numpy.ndarray' objects} 1 0.064 0.064 1.840 1.840 nmf.py:352(fit_transform) 1542 0.043 0.000 0.043 0.000 {method 'flatten' of 'numpy.ndarray' objects} 337 0.019 0.000 0.019 0.000 {method 'all' of 'numpy.ndarray' objects} 2734 0.011 0.000 0.181 0.000 fromnumeric.py:1185(sum) 2 0.010 0.005 0.010 0.005 {numpy.linalg.lapack_lite.dgesdd} 748 0.009 0.000 0.065 0.000 nmf.py:28(norm) ... تُظهر النتائج أعلاه أن التنفيذ يهيمن عليه إلى حد كبير عمليات حاصل الضرب النقطي (المفوضة إلى blas). ومن ثم، ربما لا يوجد مكسب كبير متوقع من خلال إعادة كتابة هذه الشفرة في Cython أو C/C++: في هذه الحالة، من إجمالي وقت التنفيذ البالغ 1.7 ثانية، يتم قضاء ما يقرب من 0.7 ثانية في التعليمات البرمجية المُجمَّعة التي يمكننا اعتبارها مثالية. من خلال إعادة كتابة بقية كود Python بافتراض أنه يمكننا تحقيق زيادة بنسبة 1000٪ في هذا الجزء (وهو أمر غير مرجح للغاية نظرًا لضحالة حلقات Python)، لن نحصل على أكثر من 2.4x تسريع عالميًا. ومن ثم، لا يمكن تحقيق تحسينات كبيرة إلا من خلال **التحسينات الخوارزمية** في هذا المثال المعين (على سبيل المثال، محاولة العثور على عملية مُكلفة وغير مجدية لتجنب حسابها بدلاً من محاولة تحسين تنفيذها). مع ذلك، لا يزال من المثير للاهتمام التحقق مما يحدث داخل دالة ``_nls_subproblem`` التي تُعد النقطة الساخنة إذا أخذنا في الاعتبار كود Python فقط: فهي تستغرق حوالي 100٪ من الوقت المُتراكم للوحدة. من أجل فهم ملف تعريف هذه الدالة المحددة بشكل أفضل، دعنا نُثبِّت ``line_profiler`` ونوصله بـ IPython: .. prompt:: bash $ pip install line_profiler **ضمن IPython 0.13+**، قم أولاً بإنشاء ملف تعريف تكوين: .. prompt:: bash $ ipython profile create ثم سجِّل امتداد line_profiler في ``~/.ipython/profile_default/ipython_config.py``:: c.TerminalIPythonApp.extensions.append('line_profiler') c.InteractiveShellApp.extensions.append('line_profiler') سيؤدي هذا إلى تسجيل الأمر السحري ``%lprun`` في تطبيق محطة IPython والواجهات الأمامية الأخرى مثل qtconsole ودفتر الملاحظات. الآن أعد تشغيل IPython ودعنا نستخدم هذه الأداة الجديدة:: In [1]: from sklearn.datasets import load_digits In [2]: from sklearn.decomposition import NMF ... : from sklearn.decomposition._nmf import _nls_subproblem In [3]: X, _ = load_digits(return_X_y=True) In [4]: %lprun -f _nls_subproblem NMF(n_components=16, tol=1e-2).fit(X) وحدة المؤقت: 1e-06 ثانية الملف: sklearn/decomposition/nmf.py الدالة: _nls_subproblem في السطر 137 إجمالي الوقت: 1.73153 ثانية رقم السطر الزيارات الوقت لكل زيارة ٪ الوقت محتويات السطر =============================================================================== 137 def _nls_subproblem(V, W, H_init, tol, max_iter): 138 """عامل المربعات الصغرى غير السالب ... 170 """ 171 48 5863 122.1 0.3 if (H_init < 0).any(): 172 raise ValueError("Negative values in H_init passed to NLS solver.") 173 174 48 139 2.9 0.0 H = H_init 175 48 112141 2336.3 5.8 WtV = np.dot(W.T, V) 176 48 16144 336.3 0.8 WtW = np.dot(W.T, W) 177 178 # القيم مُبررة في الورقة 179 48 144 3.0 0.0 alpha = 1 180 48 113 2.4 0.0 beta = 0.1 181 638 1880 2.9 0.1 for n_iter in range(1, max_iter + 1): 182 638 195133 305.9 10.2 grad = np.dot(WtW, H) - WtV 183 638 495761 777.1 25.9 proj_gradient = norm(grad[np.logical_or(grad < 0, H > 0)]) 184 638 2449 3.8 0.1 if proj_gradient < tol: 185 48 130 2.7 0.0 break 186 187 1474 4474 3.0 0.2 for inner_iter in range(1, 20): 188 1474 83833 56.9 4.4 Hn = H - alpha * grad 189 # Hn = np.where(Hn > 0, Hn, 0) 190 1474 194239 131.8 10.1 Hn = _pos(Hn) 191 1474 48858 33.1 2.5 d = Hn - H 192 1474 150407 102.0 7.8 gradd = np.sum(grad * d) 193 1474 515390 349.7 26.9 dQd = np.sum(np.dot(WtW, d) * d) ... من خلال النظر إلى القيم العليا لعمود ``٪ Time``، من السهل حقًا تحديد التعبيرات الأكثر تكلفة التي تستحق عناية إضافية. تنميط استخدام الذاكرة ======================== يمكنك تحليل استخدام الذاكرة لأي كود Python بالتفصيل بمساعدة `memory_profiler `_. أولاً، قم بتثبيت أحدث إصدار: .. prompt:: bash $ pip install -U memory_profiler ثم قم بإعداد السحر بطريقة مشابهة لـ ``line_profiler``. **ضمن IPython 0.11+**، قم أولاً بإنشاء ملف تعريف تكوين: .. prompt:: bash $ ipython profile create ثم سجِّل الامتداد في ``~/.ipython/profile_default/ipython_config.py`` جنبًا إلى جنب مع مُنمِّط الخط:: c.TerminalIPythonApp.extensions.append('memory_profiler') c.InteractiveShellApp.extensions.append('memory_profiler') سيؤدي هذا إلى تسجيل الأوامر السحرية ``%memit`` و ``%mprun`` في تطبيق محطة IPython والواجهات الأمامية الأخرى مثل qtconsole و دفتر الملاحظات. ``%mprun`` مفيد لفحص استخدام الذاكرة للدوال الرئيسية في برنامجك سطرًا بسطر. إنه مشابه جدًا لـ ``%lprun``، الذي تمت مناقشته في القسم السابق. على سبيل المثال، من دليل ``memory_profiler`` ``examples``:: In [1] from example import my_func In [2] %mprun -f my_func my_func() اسم الملف: example.py رقم السطر استخدام الذاكرة الزيادة محتويات السطر ========================================================== 3 @profile 4 5.97 MB 0.00 MB def my_func(): 5 13.61 MB 7.64 MB a = [1] * (10 ** 6) 6 166.20 MB 152.59 MB b = [2] * (2 * 10 ** 7) 7 13.61 MB -152.59 MB del b 8 13.61 MB 0.00 MB return a أمر سحري مفيد آخر يُحدده ``memory_profiler`` هو ``%memit``، وهو مُشابه لـ ``%timeit``. يمكن استخدامه على النحو التالي:: In [1]: import numpy as np In [2]: %memit np.zeros(1e7) الحد الأقصى من 3: 76.402344 ميغابايت لكل حلقة لمزيد من التفاصيل، انظر سلاسل الوثائق الخاصة بالسحر، باستخدام ``%memit؟`` و ``%mprun؟``. استخدام Cython ================ إذا كشف تنميط كود Python أن النفقات العامة لمترجم Python أكبر بمرتبة واحدة من حيث الحجم أو أكثر من تكلفة الحساب العددي الفعلي (على سبيل المثال، حلقات ``for`` على مكونات المتجه، التقييم المتداخل للتعبير الشرطي، الحساب القياسي...)، فمن المناسب استخراج جزء النقطة الساخنة من الشفرة كـ دالة مستقلة في ملف ``.pyx``، وإضافة إعلانات النوع الثابت ثم استخدام Cython لإنشاء برنامج C مناسب ليتم تجميعه كـ وحدة نمطية ملحق Python. تحتوي `وثائق Cython `_ على برنامج تعليمي ودليل مرجعي لتطوير مثل هذه الوحدة. لمزيد من المعلومات حول التطوير في Cython لـ scikit-learn، انظر :ref:`cython`. .. _profiling-compiled-extension: تنميط الملحقات المُجمَّعة ========================= عند العمل مع الملحقات المُجمَّعة (مكتوبة بلغة C/C++ مع غلاف أو مباشرة كملحق Cython)، يكون مُنمِّط Python الافتراضي عديم الفائدة: نحن بحاجة إلى أداة مخصصة لاستقصاء ما يحدث داخل الملحق المُجمَّع نفسه. استخدام yep و gperftools -------------------------- تنميط سهل بدون خيارات تجميع خاصة استخدم yep: - https://pypi.org/project/yep/ - https://fa.bianp.net/blog/2011/a-profiler-for-python-extensions استخدام مُصحِّح أخطاء، gdb -------------------------- * من المفيد استخدام ``gdb`` لتصحيح الأخطاء. للقيام بذلك، يجب على المرء استخدام مترجم Python تم تصميمه مع دعم التصحيح (رموز التصحيح والتحسين المناسب). لإنشاء بيئة conda جديدة (التي قد تحتاج إلى إلغاء تنشيطها وإعادة تنشيطها بعد البناء/التثبيت) باستخدام مترجم CPython مبني من المصدر: .. code-block:: bash git clone https://github.com/python/cpython.git conda create -n debug-scikit-dev conda activate debug-scikit-dev cd cpython mkdir debug cd debug ../configure --prefix=$CONDA_PREFIX --with-pydebug make EXTRA_CFLAGS='-DPy_DEBUG' -j make install استخدام gprof --------------- من أجل تنميط ملحقات Python المُجمَّعة، يمكن للمرء استخدام ``gprof`` بعد إعادة تجميع المشروع باستخدام ``gcc -pg`` واستخدام متغير ``python-dbg`` للمترجم على debian / ubuntu: ومع ذلك يتطلب هذا النهج أيضًا إعادة تجميع ``numpy`` و ``scipy`` باستخدام ``-pg`` وهو أمر معقد إلى حد ما لجعله يعمل. لحسن الحظ، يوجد مُنمِّطان بديلان لا يتطلبان منك إعادة تجميع كل شيء. استخدام valgrind / callgrind / kcachegrind -------------------------------------------- kcachegrind ~~~~~~~~~~~ يمكن استخدام ``yep`` لإنشاء تقرير تنميط. يوفر ``kcachegrind`` بيئة رسومية لتصور هذا التقرير: .. prompt:: bash $ # تشغيل yep لتنميط بعض البرامج النصية python python -m yep -c my_file.py .. prompt:: bash $ # افتح my_file.py.callgrin باستخدام kcachegrind kcachegrind my_file.py.prof .. note:: يمكن تنفيذ ``yep`` مع الوسيطة ``--lines`` أو ``-l`` لتجميع تقرير تنميط "سطرًا بسطر". التوازي متعدد النواة باستخدام ``joblib.Parallel`` ==================================================== انظر `وثائق joblib `_ .. _warm-restarts: خدعة خوارزمية بسيطة: عمليات إعادة التشغيل الدافئة ====================================================== انظر إدخال المُصطلحات لـ :term:`warm_start`