<commit_before>.. _computational_performance:

=========================
Computational Performance
=========================

For some applications the performance (mainly latency and throughput) of
estimators is crucial. We will review here the orders of magnitude you can
expect from a number of scikit-learn estimators in different contexts and
provide some tips and tricks for overcoming performance bottlenecks.

Prediction latency is measured as the elapsed time necessary to make a
prediction (e.g. in micro-seconds). Latency is often viewed as a distribution
and operations engineers often focus on the latency at a given percentile of
this distribution (e.g. the 90 percentile).

Prediction throughput is defined as the number of predictions the software can
deliver in a given amount of time (e.g. in predictions per second).

Prediction Latency
==================

One of the most straight-forward concerns one may have when using/choosing a
machine learning toolkit is the latency at which predictions can be made in a
production environment.

The main factors that influence the prediction latency are
  1. Number of features
  2. Input data representation and sparsity
  3. Model complexity
  4. Feature extraction

A last major parameter is also the possibility to do predictions in bulk or
one-at-a-time mode.

Bulk versus Atomic mode
-----------------------

In general doing predictions in bulk (many instances at the same time) is
more efficient for a number of reasons (branching predictability, CPU cache,
linear algebra libraries optimizations etc.). Here we see on a setting
with few features that independently of estimator choice the bulk mode is
always faster by 2 orders of magnitude:

.. |atomic_prediction_latency| image::  ../auto_examples/applications/images/plot_prediction_latency_1.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |atomic_prediction_latency|

.. |bulk_prediction_latency| image::  ../auto_examples/applications/images/plot_prediction_latency_2.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |bulk_prediction_latency|

To benchmark different estimators for your case you can simply change the
``n_features`` parameter according to your case in this example:
:ref:`example_applications_plot_prediction_latency.py`. This should give you
an estimate of the order of magnitude of the prediction latency for your case.

Influence of the number of Features
-----------------------------------

Obviously when the number of features increases so does the memory
consumption of each example. Indeed, for a matrix of `M` instances with `N`
features, the space complexity is in `O(N.M)`. From a computing perspective
it also means that the number of basic operations (e.g. multiplications for
vector-matrix products in linear models) increases too. Here is a graph of
the evolution of the prediction latency with the number of features:

.. |influence_of_n_features_on_latency| image::  ../auto_examples/applications/images/plot_prediction_latency_3.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |influence_of_n_features_on_latency|

Overall you can expect the prediction time to increase at least linearly with
the number of features (non-linear cases can happen depending on the global
memory footprint and estimator).

Influence of the Input Data Representation
------------------------------------------

Numpy / Scipy support sparse matrix formats which are optimized for storing
sparse data. The main feature of sparse formats is that you don't store zeros
so if your data is sparse then you use much less memory. A non-zero value in
a sparse (`CSR or CSC <http://docs.scipy.org/doc/scipy/reference/sparse.html>`_)
representation will only take on average one 32bit integer position + the 64
bit floating point value + an additional 32bit per row or column in the matrix.
Using sparse input on a dense (or sparse) linear model can speedup prediction
by quite a bit as only the non zero valued features impact the dot product
and thus the model predictions. Hence if you have 100 non zeros in 1e6
dimensional space, you only need 100 multiply + add operation instead of 1e6.

Note that dense / dense operations benefit from both BLAS-provided SSE
vectorized operations and multithreading and lower CPU cache miss rates. Sparse
dot product is more hit or miss and does not leverage the optimized BLAS
benefit. So the sparsity should typically be quite high (10% non-zeros max,
to be checked depending on the hardware) for the sparse input representation
to be faster that the dense input representation on a machine with many CPU and
an optimized BLAS implementation.

Here is a sample code to test the sparsity of your input:

    >>> from sklearn.utils.fixes import count_nonzero
    >>> def sparsity_ratio(X):
    >>>     return 1.0 - count_nonzero(X) / float(X.shape[0] * X.shape[1])
    >>> print("input sparsity ratio:", sparsity_ratio(X))

As a rule of thumb you can consider that if the sparsity ratio is greater
than 90% you can probably benefit from sparse formats. Now if you want to try
to leverage sparsity for your input data you should either build your input
matrix in the CSR or CSC or call the ``to_csr()`` method or the ``csr_matrix()``
helper function from Scipy.

Prediction Throughput
=====================

Another important metric to care about when sizing production systems is the
throughput i.e. the number of predictions you can make in a given amount of
time. Here is a benchmark from the
:ref:`example_applications_plot_prediction_latency.py` example that measures
this quantity for a number of estimators on synthetic data:

.. |throughput_benchmark| image::  ../auto_examples/applications/images/plot_prediction_latency_4.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |throughput_benchmark|

These throughputs are achieved on a single process. An obvious way to
increase the throughput of your application is to spawn additional instances
(usually processes in Python because of the
`GIL <https://wiki.python.org/moin/GlobalInterpreterLock>`_) that share the
same model. One might also add machines to spread the load. A detailed
explanation on how to achieve this is beyond the scope of this documentation
though.
<commit_msg>added section on model complexity<commit_after>.. _computational_performance:

=========================
Computational Performance
=========================

For some applications the performance (mainly latency and throughput) of
estimators is crucial. We will review here the orders of magnitude you can
expect from a number of scikit-learn estimators in different contexts and
provide some tips and tricks for overcoming performance bottlenecks.

Prediction latency is measured as the elapsed time necessary to make a
prediction (e.g. in micro-seconds). Latency is often viewed as a distribution
and operations engineers often focus on the latency at a given percentile of
this distribution (e.g. the 90 percentile).

Prediction throughput is defined as the number of predictions the software can
deliver in a given amount of time (e.g. in predictions per second).

An important aspect of performance optimization is also that it can hurt
prediction accuracy. Indeed, simpler models (e.g. linear instead of
non-linear, or with fewer parameters) often run faster but are not always able
to take into account the same exact properties of the data as more complex ones.

Prediction Latency
==================

One of the most straight-forward concerns one may have when using/choosing a
machine learning toolkit is the latency at which predictions can be made in a
production environment.

The main factors that influence the prediction latency are
  1. Number of features
  2. Input data representation and sparsity
  3. Model complexity
  4. Feature extraction

A last major parameter is also the possibility to do predictions in bulk or
one-at-a-time mode.

Bulk versus Atomic mode
-----------------------

In general doing predictions in bulk (many instances at the same time) is
more efficient for a number of reasons (branching predictability, CPU cache,
linear algebra libraries optimizations etc.). Here we see on a setting
with few features that independently of estimator choice the bulk mode is
always faster by 2 orders of magnitude:

.. |atomic_prediction_latency| image::  ../auto_examples/applications/images/plot_prediction_latency_1.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |atomic_prediction_latency|

.. |bulk_prediction_latency| image::  ../auto_examples/applications/images/plot_prediction_latency_2.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |bulk_prediction_latency|

To benchmark different estimators for your case you can simply change the
``n_features`` parameter according to your case in this example:
:ref:`example_applications_plot_prediction_latency.py`. This should give you
an estimate of the order of magnitude of the prediction latency for your case.

Influence of the number of Features
-----------------------------------

Obviously when the number of features increases so does the memory
consumption of each example. Indeed, for a matrix of `M` instances with `N`
features, the space complexity is in `O(N.M)`. From a computing perspective
it also means that the number of basic operations (e.g. multiplications for
vector-matrix products in linear models) increases too. Here is a graph of
the evolution of the prediction latency with the number of features:

.. |influence_of_n_features_on_latency| image::  ../auto_examples/applications/images/plot_prediction_latency_3.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |influence_of_n_features_on_latency|

Overall you can expect the prediction time to increase at least linearly with
the number of features (non-linear cases can happen depending on the global
memory footprint and estimator).

Influence of the Input Data Representation
------------------------------------------

Numpy / Scipy support sparse matrix formats which are optimized for storing
sparse data. The main feature of sparse formats is that you don't store zeros
so if your data is sparse then you use much less memory. A non-zero value in
a sparse (`CSR or CSC <http://docs.scipy.org/doc/scipy/reference/sparse.html>`_)
representation will only take on average one 32bit integer position + the 64
bit floating point value + an additional 32bit per row or column in the matrix.
Using sparse input on a dense (or sparse) linear model can speedup prediction
by quite a bit as only the non zero valued features impact the dot product
and thus the model predictions. Hence if you have 100 non zeros in 1e6
dimensional space, you only need 100 multiply + add operation instead of 1e6.

Note that dense / dense operations benefit from both BLAS-provided SSE
vectorized operations and multithreading and lower CPU cache miss rates. Sparse
dot product is more hit or miss and does not leverage the optimized BLAS
benefit. So the sparsity should typically be quite high (10% non-zeros max,
to be checked depending on the hardware) for the sparse input representation
to be faster that the dense input representation on a machine with many CPU and
an optimized BLAS implementation.

Here is a sample code to test the sparsity of your input:

    >>> from sklearn.utils.fixes import count_nonzero
    >>> def sparsity_ratio(X):
    >>>     return 1.0 - count_nonzero(X) / float(X.shape[0] * X.shape[1])
    >>> print("input sparsity ratio:", sparsity_ratio(X))

As a rule of thumb you can consider that if the sparsity ratio is greater
than 90% you can probably benefit from sparse formats. Now if you want to try
to leverage sparsity for your input data you should either build your input
matrix in the CSR or CSC or call the ``to_csr()`` method or the ``csr_matrix()``
helper function from Scipy.

Model Complexity
----------------

For linear models (e.g. Lasso, ElasticNet, SGDClassifier/Regressor,
Ridge & RidgeClassifier, PassiveAgressiveClassifier/Regressor, LinearSVC,
LogisticRegression...) the decision function that is applied at prediction
time is the same, so latency should be equivalent. Of course the particular
values (and sparsity) will change depending on how the model was trained but
the type of operation is the same (a dot product).

For the SVM family of algorithms the latency is tied to the number of support
vectors (the fewer the faster).

For tree or forest algorithms (e.g. RandomForest, GBT, ExternalTrees etc) the
number of trees and their depth play the most important role.

In any case be warned that playing with model complexity can hurt accuracy as
mentionned above. For instance a non-linearly separable problem can be dealt
with a speedy linear model but prediction power will very likely suffer in
the process.

Prediction Throughput
=====================

Another important metric to care about when sizing production systems is the
throughput i.e. the number of predictions you can make in a given amount of
time. Here is a benchmark from the
:ref:`example_applications_plot_prediction_latency.py` example that measures
this quantity for a number of estimators on synthetic data:

.. |throughput_benchmark| image::  ../auto_examples/applications/images/plot_prediction_latency_4.png
    :target: ../auto_examples/applications/plot_prediction_latency.html
    :scale: 80

.. centered:: |throughput_benchmark|

These throughputs are achieved on a single process. An obvious way to
increase the throughput of your application is to spawn additional instances
(usually processes in Python because of the
`GIL <https://wiki.python.org/moin/GlobalInterpreterLock>`_) that share the
same model. One might also add machines to spread the load. A detailed
explanation on how to achieve this is beyond the scope of this documentation
though.

{'fraction_non_alphanumeric': 0.076238253373843, 'fraction_numerical': 0.006076450222567653, 'mean_word_length': 4.331073446327683, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 31, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3004519', 'n_tokens_mistral': 3726, 'n_tokens_neox': 3473, 'n_words': 1859}