Influencia de la complejidad del modelo

Demostrar cómo la complejidad del modelo influye tanto en la exactitud de la predicción como en el rendimiento computacional.

Utilizaremos dos conjuntos de datos:
Vamos a modelar la influencia de la complejidad en tres estimadores diferentes:
  • SGDClassifier (para datos de clasificación) que implementa el aprendizaje por descenso de gradiente estocástico;

  • NuSVR (para datos de regresión) que implementa la regresión de vectores de soporte Nu;

  • GradientBoostingRegressor (para datos de regresión) que construye un modelo aditivo de forma progresiva por etapas.

Hacemos variar la complejidad del modelo mediante la elección de los parámetros relevantes del modelo en cada uno de nuestros modelos seleccionados. A continuación, mediremos la influencia tanto en el rendimiento computacional (latencia) como en la capacidad de predicción (MSE o pérdida de Hamming).

print(__doc__)

# Authors: Eustache Diemert <eustache@diemert.fr>
#          Maria Telenczuk <https://github.com/maikia>
#          Guillaume Lemaitre <g.lemaitre58@gmail.com>
# License: BSD 3 clause

import time
import numpy as np
import matplotlib.pyplot as plt

from sklearn import datasets
from sklearn.utils import shuffle
from sklearn.metrics import mean_squared_error
from sklearn.svm import NuSVR
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import hamming_loss


# Initialize random generator
np.random.seed(0)

Cargar los datos

Primero cargamos ambos conjuntos de datos.

Nota

Utilizamos fetch_20newsgroups_vectorized para descargar el conjunto de datos de 20 grupos de noticias. Devuelve características listas para usar.

Nota

La «X» del conjunto de datos de los 20 grupos de noticias es una matriz dispersa, mientras que la «X» del conjunto de datos de la diabetes es una matriz numpy.

def generate_data(case):
    """Generate regression/classification data."""
    if case == 'regression':
        X, y = datasets.load_diabetes(return_X_y=True)
    elif case == 'classification':
        X, y = datasets.fetch_20newsgroups_vectorized(subset='all',
                                                      return_X_y=True)
    X, y = shuffle(X, y)
    offset = int(X.shape[0] * 0.8)
    X_train, y_train = X[:offset], y[:offset]
    X_test, y_test = X[offset:], y[offset:]

    data = {'X_train': X_train, 'X_test': X_test, 'y_train': y_train,
            'y_test': y_test}
    return data


regression_data = generate_data('regression')
classification_data = generate_data('classification')

Influencia de la referencia

A continuación, podemos calcular la influencia de los parámetros en el estimador dado. En cada ronda, pondremos el estimador con el nuevo valor de changing_param e iremos recogiendo los tiempos de predicción, el rendimiento de la predicción y las complejidades para ver cómo afectan esos cambios al estimador. Calcularemos la complejidad utilizando complexity_computer pasado como parámetro.

def benchmark_influence(conf):
    """
    Benchmark influence of `changing_param` on both MSE and latency.
    """
    prediction_times = []
    prediction_powers = []
    complexities = []
    for param_value in conf['changing_param_values']:
        conf['tuned_params'][conf['changing_param']] = param_value
        estimator = conf['estimator'](**conf['tuned_params'])

        print("Benchmarking %s" % estimator)
        estimator.fit(conf['data']['X_train'], conf['data']['y_train'])
        conf['postfit_hook'](estimator)
        complexity = conf['complexity_computer'](estimator)
        complexities.append(complexity)
        start_time = time.time()
        for _ in range(conf['n_samples']):
            y_pred = estimator.predict(conf['data']['X_test'])
        elapsed_time = (time.time() - start_time) / float(conf['n_samples'])
        prediction_times.append(elapsed_time)
        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

Seleccionar parámetros

Elegimos los parámetros para cada uno de nuestros estimadores haciendo un diccionario con todos los valores necesarios. param_cambiante es el nombre del parámetro que variará en cada estimador. La complejidad se definirá con la etiqueta complexity_label y se calculará con complexity_computer. También hay que tener en cuenta que dependiendo del tipo de estimador estamos pasando datos diferentes.

def _count_nonzero_coefficients(estimator):
    a = estimator.coef_.toarray()
    return np.count_nonzero(a)


configurations = [
    {'estimator': SGDClassifier,
     'tuned_params': {'penalty': 'elasticnet', 'alpha': 0.001, 'loss':
                      'modified_huber', 'fit_intercept': True, 'tol': 1e-3},
     '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,
     'tuned_params': {'C': 1e3, 'gamma': 2 ** -15},
     '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},
]

Ejecutar el código y graficar los resultados

Hemos definido todas las funciones necesarias para ejecutar nuestro benchmark. Ahora, haremos un bucle sobre las diferentes configuraciones que hemos definido anteriormente. A continuación, podemos analizar los gráficos obtenidos en la prueba comparativa: La relajación de la penalización L1 en el clasificador SGD reduce el error de predicción pero conduce a un aumento del tiempo de entrenamiento. Podemos extraer un análisis similar en relación con el tiempo de entrenamiento, que aumenta con el número de vectores de soporte con un Nu-SVR. Sin embargo, observamos que existe un número óptimo de vectores de soporte que reduce el error de predicción. De hecho, muy pocos vectores de soporte conducen a un modelo infraajustado, mientras que demasiados vectores de soporte conducen a un modelo sobreajustado. Se puede llegar a la misma conclusión para el modelo de gradiente-boosting. La única diferencia con el Nu-SVR es que tener demasiados árboles en el conjunto no es tan perjudicial.

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__))


for conf in configurations:
    prediction_performances, prediction_times, complexities = \
        benchmark_influence(conf)
    plot_influence(conf, prediction_performances, prediction_times,
                   complexities)
plt.show()
  • Influence of varying 'l1_ratio' on SGDClassifier
  • Influence of varying 'nu' on NuSVR
  • Influence of varying 'n_estimators' on GradientBoostingRegressor

Out:

Benchmarking SGDClassifier(alpha=0.001, l1_ratio=0.25, loss='modified_huber',
              penalty='elasticnet')
Complexity: 4482 | Hamming Loss (Misclassification Ratio): 0.2541 | Pred. Time: 0.023992s

Benchmarking SGDClassifier(alpha=0.001, l1_ratio=0.5, loss='modified_huber',
              penalty='elasticnet')
Complexity: 1668 | Hamming Loss (Misclassification Ratio): 0.2854 | Pred. Time: 0.017126s

Benchmarking SGDClassifier(alpha=0.001, l1_ratio=0.75, loss='modified_huber',
              penalty='elasticnet')
Complexity: 874 | Hamming Loss (Misclassification Ratio): 0.3143 | Pred. Time: 0.012552s

Benchmarking SGDClassifier(alpha=0.001, l1_ratio=0.9, loss='modified_huber',
              penalty='elasticnet')
Complexity: 663 | Hamming Loss (Misclassification Ratio): 0.3268 | Pred. Time: 0.014147s

Benchmarking NuSVR(C=1000.0, gamma=3.0517578125e-05, nu=0.1)
Complexity: 36 | MSE: 7004.5333 | Pred. Time: 0.000534s

Benchmarking NuSVR(C=1000.0, gamma=3.0517578125e-05, nu=0.25)
Complexity: 90 | MSE: 6918.2577 | Pred. Time: 0.000722s

Benchmarking NuSVR(C=1000.0, gamma=3.0517578125e-05)
Complexity: 178 | MSE: 6840.2763 | Pred. Time: 0.001507s

Benchmarking NuSVR(C=1000.0, gamma=3.0517578125e-05, nu=0.75)
Complexity: 266 | MSE: 6918.2492 | Pred. Time: 0.002023s

Benchmarking NuSVR(C=1000.0, gamma=3.0517578125e-05, nu=0.9)
Complexity: 318 | MSE: 6940.2899 | Pred. Time: 0.002330s

Benchmarking GradientBoostingRegressor(n_estimators=10)
Complexity: 10 | MSE: 4062.4219 | Pred. Time: 0.000183s

Benchmarking GradientBoostingRegressor(n_estimators=50)
Complexity: 50 | MSE: 3156.4420 | Pred. Time: 0.000260s

Benchmarking GradientBoostingRegressor()
Complexity: 100 | MSE: 3301.5938 | Pred. Time: 0.000340s

Benchmarking GradientBoostingRegressor(n_estimators=200)
Complexity: 200 | MSE: 3235.9376 | Pred. Time: 0.000434s

Benchmarking GradientBoostingRegressor(n_estimators=500)
Complexity: 500 | MSE: 3473.6361 | Pred. Time: 0.000832s

Conclusión

Como conclusión, podemos deducir las siguientes ideas:

  • un modelo más complejo (o expresivo) requerirá un mayor tiempo de entrenamiento;

  • un modelo más complejo no garantiza la reducción del error de predicción.

Estos aspectos están relacionados con la generalización del modelo y con evitar el infra o sobreajuste del mismo.

Tiempo total de ejecución del script: (0 minutos 41.995 segundos)

Galería generada por Sphinx-Gallery