2013-11-14 15:45:21 +08:00
|
|
|
"""
|
|
|
|
|
==========================
|
|
|
|
|
Model Complexity Influence
|
|
|
|
|
==========================
|
|
|
|
|
|
|
|
|
|
Demonstrate how model complexity influences both prediction accuracy and
|
|
|
|
|
computational performance.
|
|
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
We will be using two datasets:
|
|
|
|
|
- :ref:`diabetes_dataset` for regression.
|
|
|
|
|
This dataset consists of 10 measurements taken from diabetes patients.
|
|
|
|
|
The task is to predict disease progression;
|
|
|
|
|
- :ref:`20newsgroups_dataset` for classification. This dataset consists of
|
|
|
|
|
newsgroup posts. The task is to predict on which topic (out of 20 topics)
|
|
|
|
|
the post is written about.
|
|
|
|
|
|
|
|
|
|
We will model the complexity influence on three different estimators:
|
|
|
|
|
- :class:`~sklearn.linear_model.SGDClassifier` (for classification data)
|
|
|
|
|
which implements stochastic gradient descent learning;
|
|
|
|
|
|
|
|
|
|
- :class:`~sklearn.svm.NuSVR` (for regression data) which implements
|
|
|
|
|
Nu support vector regression;
|
|
|
|
|
|
|
|
|
|
- :class:`~sklearn.ensemble.GradientBoostingRegressor` (for regression
|
|
|
|
|
data) which builds an additive model in a forward stage-wise fashion.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
We make the model complexity vary through the choice of relevant model
|
|
|
|
|
parameters in each of our selected models. Next, we will measure the influence
|
|
|
|
|
on both computational performance (latency) and predictive power (MSE or
|
|
|
|
|
Hamming Loss).
|
2013-11-14 15:45:21 +08:00
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
print(__doc__)
|
|
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
# Authors: Eustache Diemert <eustache@diemert.fr>
|
|
|
|
|
# Maria Telenczuk <https://github.com/maikia>
|
|
|
|
|
# Guillaume Lemaitre <g.lemaitre58@gmail.com>
|
2013-11-14 15:45:21 +08:00
|
|
|
# License: BSD 3 clause
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
import numpy as np
|
2014-05-16 11:09:30 +08:00
|
|
|
import matplotlib.pyplot as plt
|
2013-11-14 15:45:21 +08:00
|
|
|
|
|
|
|
|
from sklearn import datasets
|
|
|
|
|
from sklearn.utils import shuffle
|
|
|
|
|
from sklearn.metrics import mean_squared_error
|
2019-10-19 04:18:33 +08:00
|
|
|
from sklearn.svm import NuSVR
|
2019-10-21 09:56:55 +08:00
|
|
|
from sklearn.ensemble import GradientBoostingRegressor
|
2019-10-23 21:34:26 +08:00
|
|
|
from sklearn.linear_model import SGDClassifier
|
2015-03-03 01:50:53 +08:00
|
|
|
from sklearn.metrics import hamming_loss
|
2013-11-14 15:45:21 +08:00
|
|
|
|
2013-12-02 23:39:07 +08:00
|
|
|
|
2017-06-20 20:48:57 +08:00
|
|
|
# Initialize random generator
|
2013-11-14 16:09:47 +08:00
|
|
|
np.random.seed(0)
|
2013-11-15 18:51:40 +08:00
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
##############################################################################
|
|
|
|
|
# Load the data
|
|
|
|
|
# -------------
|
|
|
|
|
#
|
|
|
|
|
# First we load both datasets.
|
|
|
|
|
#
|
|
|
|
|
# .. note:: We are using
|
|
|
|
|
# :func:`~sklearn.datasets.fetch_20newsgroups_vectorized` to download 20
|
|
|
|
|
# newsgroups dataset. It returns ready-to-use features.
|
|
|
|
|
#
|
|
|
|
|
# .. note:: ``X`` of the 20 newsgroups dataset is a sparse matrix while ``X``
|
|
|
|
|
# of diabetes dataset is a numpy array.
|
|
|
|
|
#
|
2013-11-15 18:51:40 +08:00
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
|
|
|
|
|
def generate_data(case):
|
2013-12-02 23:39:07 +08:00
|
|
|
"""Generate regression/classification data."""
|
|
|
|
|
if case == 'regression':
|
2020-05-18 21:30:23 +08:00
|
|
|
X, y = datasets.load_diabetes(return_X_y=True)
|
2013-12-02 23:39:07 +08:00
|
|
|
elif case == 'classification':
|
2019-08-20 10:08:23 +08:00
|
|
|
X, y = datasets.fetch_20newsgroups_vectorized(subset='all',
|
|
|
|
|
return_X_y=True)
|
|
|
|
|
X, y = shuffle(X, y)
|
2013-11-15 18:51:40 +08:00
|
|
|
offset = int(X.shape[0] * 0.8)
|
|
|
|
|
X_train, y_train = X[:offset], y[:offset]
|
|
|
|
|
X_test, y_test = X[offset:], y[offset:]
|
2020-05-18 21:30:23 +08:00
|
|
|
|
2013-11-15 18:51:40 +08:00
|
|
|
data = {'X_train': X_train, 'X_test': X_test, 'y_train': y_train,
|
|
|
|
|
'y_test': y_test}
|
|
|
|
|
return data
|
|
|
|
|
|
2013-11-14 15:45:21 +08:00
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
regression_data = generate_data('regression')
|
|
|
|
|
classification_data = generate_data('classification')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
|
# Benchmark influence
|
|
|
|
|
# -------------------
|
|
|
|
|
# Next, we can calculate the influence of the parameters on the given
|
|
|
|
|
# estimator. In each round, we will set the estimator with the new value of
|
|
|
|
|
# ``changing_param`` and we will be collecting the prediction times, prediction
|
|
|
|
|
# performance and complexities to see how those changes affect the estimator.
|
|
|
|
|
# We will calculate the complexity using ``complexity_computer`` passed as a
|
|
|
|
|
# parameter.
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
2013-11-14 15:45:21 +08:00
|
|
|
def benchmark_influence(conf):
|
|
|
|
|
"""
|
2020-05-18 21:30:23 +08:00
|
|
|
Benchmark influence of `changing_param` on both MSE and latency.
|
2013-11-14 15:45:21 +08:00
|
|
|
"""
|
|
|
|
|
prediction_times = []
|
2013-12-02 23:39:07 +08:00
|
|
|
prediction_powers = []
|
2013-11-14 15:45:21 +08:00
|
|
|
complexities = []
|
|
|
|
|
for param_value in conf['changing_param_values']:
|
|
|
|
|
conf['tuned_params'][conf['changing_param']] = param_value
|
|
|
|
|
estimator = conf['estimator'](**conf['tuned_params'])
|
2020-05-18 21:30:23 +08:00
|
|
|
|
2013-11-14 15:45:21 +08:00
|
|
|
print("Benchmarking %s" % estimator)
|
2013-11-15 18:51:40 +08:00
|
|
|
estimator.fit(conf['data']['X_train'], conf['data']['y_train'])
|
2013-12-02 23:39:07 +08:00
|
|
|
conf['postfit_hook'](estimator)
|
2013-11-14 15:45:21 +08:00
|
|
|
complexity = conf['complexity_computer'](estimator)
|
|
|
|
|
complexities.append(complexity)
|
|
|
|
|
start_time = time.time()
|
2013-11-15 18:51:40 +08:00
|
|
|
for _ in range(conf['n_samples']):
|
|
|
|
|
y_pred = estimator.predict(conf['data']['X_test'])
|
|
|
|
|
elapsed_time = (time.time() - start_time) / float(conf['n_samples'])
|
2013-11-14 15:45:21 +08:00
|
|
|
prediction_times.append(elapsed_time)
|
2013-12-02 23:39:07 +08:00
|
|
|
pred_score = conf['prediction_performance_computer'](
|
|
|
|
|
conf['data']['y_test'], y_pred)
|
|
|
|
|
prediction_powers.append(pred_score)
|
|
|
|
|
print("Complexity: %d | %s: %.4f | Pred. Time: %fs\n" % (
|
|
|
|
|
complexity, conf['prediction_performance_label'], pred_score,
|
|
|
|
|
elapsed_time))
|
|
|
|
|
return prediction_powers, prediction_times, complexities
|
2013-11-14 15:45:21 +08:00
|
|
|
|
|
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
##############################################################################
|
|
|
|
|
# Choose parameters
|
|
|
|
|
# -----------------
|
|
|
|
|
#
|
|
|
|
|
# We choose the parameters for each of our estimators by making
|
|
|
|
|
# a dictionary with all the necessary values.
|
|
|
|
|
# ``changing_param`` is the name of the parameter which will vary in each
|
|
|
|
|
# estimator.
|
|
|
|
|
# Complexity will be defined by the ``complexity_label`` and calculated using
|
|
|
|
|
# `complexity_computer`.
|
|
|
|
|
# Also note that depending on the estimator type we are passing
|
|
|
|
|
# different data.
|
|
|
|
|
#
|
2013-12-02 23:39:07 +08:00
|
|
|
|
|
|
|
|
def _count_nonzero_coefficients(estimator):
|
2014-05-21 09:16:34 +08:00
|
|
|
a = estimator.coef_.toarray()
|
2014-01-06 21:45:40 +08:00
|
|
|
return np.count_nonzero(a)
|
2013-12-02 23:39:07 +08:00
|
|
|
|
2020-05-18 21:30:23 +08:00
|
|
|
|
2013-11-15 18:51:40 +08:00
|
|
|
configurations = [
|
2013-12-02 23:39:07 +08:00
|
|
|
{'estimator': SGDClassifier,
|
|
|
|
|
'tuned_params': {'penalty': 'elasticnet', 'alpha': 0.001, 'loss':
|
2017-10-04 23:28:32 +08:00
|
|
|
'modified_huber', 'fit_intercept': True, 'tol': 1e-3},
|
2013-12-02 23:39:07 +08:00
|
|
|
'changing_param': 'l1_ratio',
|
|
|
|
|
'changing_param_values': [0.25, 0.5, 0.75, 0.9],
|
|
|
|
|
'complexity_label': 'non_zero coefficients',
|
|
|
|
|
'complexity_computer': _count_nonzero_coefficients,
|
|
|
|
|
'prediction_performance_computer': hamming_loss,
|
|
|
|
|
'prediction_performance_label': 'Hamming Loss (Misclassification Ratio)',
|
|
|
|
|
'postfit_hook': lambda x: x.sparsify(),
|
|
|
|
|
'data': classification_data,
|
|
|
|
|
'n_samples': 30},
|
|
|
|
|
{'estimator': NuSVR,
|
2015-03-03 01:50:53 +08:00
|
|
|
'tuned_params': {'C': 1e3, 'gamma': 2 ** -15},
|
2013-12-02 23:39:07 +08:00
|
|
|
'changing_param': 'nu',
|
|
|
|
|
'changing_param_values': [0.1, 0.25, 0.5, 0.75, 0.9],
|
|
|
|
|
'complexity_label': 'n_support_vectors',
|
|
|
|
|
'complexity_computer': lambda x: len(x.support_vectors_),
|
|
|
|
|
'data': regression_data,
|
|
|
|
|
'postfit_hook': lambda x: x,
|
|
|
|
|
'prediction_performance_computer': mean_squared_error,
|
|
|
|
|
'prediction_performance_label': 'MSE',
|
|
|
|
|
'n_samples': 30},
|
|
|
|
|
{'estimator': GradientBoostingRegressor,
|
|
|
|
|
'tuned_params': {'loss': 'ls'},
|
|
|
|
|
'changing_param': 'n_estimators',
|
|
|
|
|
'changing_param_values': [10, 50, 100, 200, 500],
|
|
|
|
|
'complexity_label': 'n_trees',
|
|
|
|
|
'complexity_computer': lambda x: x.n_estimators,
|
|
|
|
|
'data': regression_data,
|
|
|
|
|
'postfit_hook': lambda x: x,
|
|
|
|
|
'prediction_performance_computer': mean_squared_error,
|
|
|
|
|
'prediction_performance_label': 'MSE',
|
|
|
|
|
'n_samples': 30},
|
|
|
|
|
]
|
2020-05-18 21:30:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
|
# Run the code and plot the results
|
|
|
|
|
# ---------------------------------
|
|
|
|
|
#
|
|
|
|
|
# We defined all the functions required to run our benchmark. Now, we will loop
|
|
|
|
|
# over the different configurations that we defined previously. Subsequently,
|
|
|
|
|
# we can analyze the plots obtained from the benchmark:
|
|
|
|
|
# Relaxing the `L1` penalty in the SGD classifier reduces the prediction error
|
|
|
|
|
# but leads to an increase in the training time.
|
|
|
|
|
# We can draw a similar analysis regarding the training time which increases
|
|
|
|
|
# with the number of support vectors with a Nu-SVR. However, we observed that
|
|
|
|
|
# there is an optimal number of support vectors which reduces the prediction
|
|
|
|
|
# error. Indeed, too few support vectors lead to an under-fitted model while
|
|
|
|
|
# too many support vectors lead to an over-fitted model.
|
|
|
|
|
# The exact same conclusion can be drawn for the gradient-boosting model. The
|
|
|
|
|
# only the difference with the Nu-SVR is that having too many trees in the
|
|
|
|
|
# ensemble is not as detrimental.
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
def plot_influence(conf, mse_values, prediction_times, complexities):
|
|
|
|
|
"""
|
|
|
|
|
Plot influence of model complexity on both accuracy and latency.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
fig = plt.figure()
|
|
|
|
|
fig.subplots_adjust(right=0.75)
|
|
|
|
|
|
|
|
|
|
# first axes (prediction error)
|
|
|
|
|
ax1 = fig.add_subplot(111)
|
|
|
|
|
line1 = ax1.plot(complexities, mse_values, c='tab:blue', ls='-')[0]
|
|
|
|
|
ax1.set_xlabel('Model Complexity (%s)' % conf['complexity_label'])
|
|
|
|
|
y1_label = conf['prediction_performance_label']
|
|
|
|
|
ax1.set_ylabel(y1_label)
|
|
|
|
|
|
|
|
|
|
ax1.spines['left'].set_color(line1.get_color())
|
|
|
|
|
ax1.yaxis.label.set_color(line1.get_color())
|
|
|
|
|
ax1.tick_params(axis='y', colors=line1.get_color())
|
|
|
|
|
|
|
|
|
|
# second axes (latency)
|
|
|
|
|
ax2 = fig.add_subplot(111, sharex=ax1, frameon=False)
|
|
|
|
|
line2 = ax2.plot(complexities, prediction_times, c='tab:orange', ls='-')[0]
|
|
|
|
|
ax2.yaxis.tick_right()
|
|
|
|
|
ax2.yaxis.set_label_position("right")
|
|
|
|
|
y2_label = "Time (s)"
|
|
|
|
|
ax2.set_ylabel(y2_label)
|
|
|
|
|
ax1.spines['right'].set_color(line2.get_color())
|
|
|
|
|
ax2.yaxis.label.set_color(line2.get_color())
|
|
|
|
|
ax2.tick_params(axis='y', colors=line2.get_color())
|
|
|
|
|
|
|
|
|
|
plt.legend((line1, line2), ("prediction error", "latency"),
|
|
|
|
|
loc='upper right')
|
|
|
|
|
|
|
|
|
|
plt.title("Influence of varying '%s' on %s" % (conf['changing_param'],
|
|
|
|
|
conf['estimator'].__name__))
|
|
|
|
|
|
|
|
|
|
|
2013-11-14 15:45:21 +08:00
|
|
|
for conf in configurations:
|
2013-12-02 23:39:07 +08:00
|
|
|
prediction_performances, prediction_times, complexities = \
|
|
|
|
|
benchmark_influence(conf)
|
|
|
|
|
plot_influence(conf, prediction_performances, prediction_times,
|
|
|
|
|
complexities)
|
2020-05-18 21:30:23 +08:00
|
|
|
plt.show()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
|
# Conclusion
|
|
|
|
|
# ----------
|
|
|
|
|
#
|
|
|
|
|
# As a conclusion, we can deduce the following insights:
|
|
|
|
|
#
|
|
|
|
|
# * a model which is more complex (or expressive) will require a larger
|
|
|
|
|
# training time;
|
|
|
|
|
# * a more complex model does not guarantee to reduce the prediction error.
|
|
|
|
|
#
|
|
|
|
|
# These aspects are related to model generalization and avoiding model
|
|
|
|
|
# under-fitting or over-fitting.
|