Overfitting

Introducción

El overfitting ocurre cuando el algoritmo de machine learning captura el ruido de los datos. Intuitivamente, el overfitting ocurre cuando el modelo o el algoritmo se ajusta demasiado bien a los datos. Específicamente, el sobreajuste ocurre si el modelo o algoritmo muestra un sesgo bajo pero una varianza alta.

El overfitting a menudo es el resultado de un modelo excesivamente complicado, y puede evitarse ajustando múltiples modelos y utilizando validación o validación cruzada para comparar sus precisiones predictivas en los datos de prueba.

../../../../_images/over.png

El underfitting ocurre cuando un modelo estadístico o un algoritmo de machine learning no pueden capturar la tendencia subyacente de los datos. Intuitivamente, el underfitting ocurre cuando el modelo o el algoritmo no se ajustan suficientemente a los datos. Específicamente, el underfitting ocurre si el modelo o algoritmo muestra una varianza baja pero un sesgo alto.

El underfitting suele ser el resultado de un modelo excesivamente simple.

../../../../_images/under.png

¿Cómo escoger el mejor modelo?

../../../../_images/overfitting_1.png
  • El sobreajuste va a estar relacionado con la complejidad del modelo, mientras más complejidad le agreguemos, mayor va a ser la tendencia a sobreajuste a los datos.

  • No existe una regla general para establecer cual es el nivel ideal de complejidad que le podemos otorgar a nuestro modelo sin caer en el sobreajuste; pero podemos valernos de algunas herramientas analíticas para intentar entender como el modelo se ajusta a los datos y reconocer el sobreajuste.

Para entender esto, veamos un ejemplo con el método de árboles de decisiones. Los árboles de decisión (DT) son un método de aprendizaje supervisado no paramétrico utilizado para la clasificación y la regresión.

Ejemplo con Árboles de Decisión

../../../../_images/tree1.png

Los Árboles de Decisión pueden ser muchas veces una herramienta muy precisa, pero también con mucha tendencia al sobreajuste. Para construir estos modelos aplicamos un procedimiento recursivo para encontrar los atributos que nos proporcionan más información sobre distintos subconjuntos de datos, cada vez más pequeños.

Si aplicamos este procedimiento en forma reiterada, eventualmente podemos llegar a un árbol en el que cada hoja tenga una sola instancia de nuestra variable objetivo a clasificar.

En este caso extremo, el Árbol de Decisión va a tener una pobre generalización y estar bastante sobreajustado; ya que cada instancia de los datos de entrenamiento va a encontrar el camino que lo lleve eventualmente a la hoja que lo contiene, alcanzando así una precisión del 100% con los datos de entrenamiento.

Ejemplo función sinusoidal

Veamos un ejemplo sencillo con la ayuda de python, tratemos de ajustar un modelo de DT sobre una función senusoidal.

# librerias 

import pandas as pd
import numpy as np 
import matplotlib.pyplot as plt 
import seaborn as sns 
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_classification
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier,DecisionTreeRegressor
import random

random.seed(1982) # semilla

# graficos incrustados
%matplotlib inline

# parametros esteticos de seaborn
sns.set_palette("deep", desat=.6)
sns.set_context(rc={"figure.figsize": (12, 4)})
# Create a random dataset
rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - rng.rand(16))

# separ los datos en train y eval
x_train, x_eval, y_train, y_eval = train_test_split(X, y, test_size=0.35, 
                                                    train_size=0.65,
                                                    random_state=1982)
# Fit regression model
regr_1 = DecisionTreeRegressor(max_depth=2)
regr_2 = DecisionTreeRegressor(max_depth=10)

regr_1.fit(x_train,y_train)
regr_2.fit(x_train,y_train)

# Predict
X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
y_1 = regr_1.predict(X_test)
y_2 = regr_2.predict(X_test)
y_3 = regr_1.predict(X_test)

# Plot the results
fig, ax = plt.subplots(figsize=(11, 8.5))
plt.scatter(X, y, s=20, edgecolor="black",
            c="darkorange", label="data")
plt.plot(X_test, y_1, color="cornflowerblue",
         label="max_depth=2", linewidth=2)
plt.plot(X_test, y_2, color="yellowgreen", label="max_depth=10", linewidth=2)
plt.xlabel("data")
plt.ylabel("target")
plt.title("Decision Tree Regression")
plt.legend()
plt.show()
../../../../_images/04_overfitting_underfitting_7_0.png

Basado en los gráficos, el modelo de DT con profundidad 2, no se ajuste muy bien a los datos, mientras que el modelo DT con profundidad 10 se ajuste excesivamente demasiado a ellos.

Para ver el ajuste de cada modelo, estudiaremos su precisión (score) sobre los conjunto de entrenamiento y de testeo.

result  = pd.DataFrame({
    
    'model': ['dt_depth_2','dt_depth_10'],
    'train_score': [ regr_1.score(x_train, y_train), regr_2.score(x_train, y_train)],
    'test_score': [ regr_1.score(x_eval, y_eval), regr_2.score(x_eval, y_eval)]
})
result
model train_score test_score
0 dt_depth_2 0.766363 0.719137
1 dt_depth_10 1.000000 0.661186

Como es de esperar, para el modelo DT con profundidad 10, la precisión sobre el conjunto de entrenamiento es perfecta (igual a 1), no obstante, esta disminuye considerablemente al obtener la presición sobre los datos de testeo (igual a 0.66), por lo que esto es una evidencia para decir que el modelo tiene overfitting.

Caso contrario es el modelo DT con profundidad 2, puesto que es un caso típico de underfitting. Cabe destacar que el modelo de underfitting tiene una presición similar tanto para el conjunto de entrenamiento como para el conjunto de testo.

Conclusiones del caso

Ambos modelos no ajuste de la mejor manera, pero lo hacen de distintas perspectivas. Se debe poner mucho énfasis al momento de separar el conjunto de entrenamiento y de testeo, puesto que los resultados se pueden ver altamente sesgado (caso del overfitting). Particularmente para este caso, el ajuste era complejo de realizar puesto que eliminabamos un monto de datos “significativos”, que hacian que los modelos no captarán la continuidad de la función sinusoidal.

Equilibrio en el ajuste de modelos

A continuación ocuparemos otro conjunto de entrenamientos (make_classification) para mostrar una forma de encoentrar un un equilibrio en la complejidad del modelo y su ajuste a los datos.

Siguiendo con el ejemplo de los modelos de árbol de decisión, analizaremos la presición (score) para distintas profundidades sobre los distintos conjuntos (entrenamiento y testeo).

# Ejemplo en python - árboles de decisión
# dummy data con 100 atributos y 2 clases
X, y = make_classification(10000, 100, n_informative=3, n_classes=2,
                          random_state=1982)

# separ los datos en train y eval
x_train, x_eval, y_train, y_eval = train_test_split(X, y, test_size=0.35, 
                                                    train_size=0.65,
                                                    random_state=1982)

# Grafico de ajuste del árbol de decisión
train_prec =  []
eval_prec = []
max_deep_list = list(range(2, 20))

# Entrenar con arboles de distinta profundidad
for deep in max_deep_list:
    model = DecisionTreeClassifier( max_depth=deep)
    model.fit(x_train, y_train)
    train_prec.append(model.score(x_train, y_train))
    eval_prec.append(model.score(x_eval, y_eval))
# graficar los resultados.

sns.set(rc={'figure.figsize':(12,9)})

df1 = pd.DataFrame({'numero_nodos':max_deep_list,
                   'precision':train_prec,
                   'datos':'entrenamiento'})

df2 = pd.DataFrame({'numero_nodos':max_deep_list,
                   'precision':eval_prec,
                   'datos':'evaluacion'})

df_graph = pd.concat([df1,df2])

sns.lineplot(data=df_graph,
             x='numero_nodos',
             y='precision',
             hue='datos',
             palette="Set1")
<AxesSubplot:xlabel='numero_nodos', ylabel='precision'>
../../../../_images/04_overfitting_underfitting_12_1.png

El gráfico que acabamos de construir se llama gráfico de ajuste y muestra la precisión del modelo en función de su complejidad.

El punto con mayor precisión, en los datos de evaluación, lo obtenemos con un nivel de profundidad de aproximadamente 6 nodos; a partir de allí el modelo pierde en generalización y comienza a estar sobreajustado.

También podemos crear un gráfico similar con la ayuda de Scikit-learn, utilizando validation_curve.

# utilizando validation curve de sklearn
from sklearn.model_selection import validation_curve

train_prec, eval_prec = validation_curve(estimator=model, X=x_train,
                                        y=y_train, param_name='max_depth',
                                        param_range=max_deep_list, cv=5)

train_mean = np.mean(train_prec, axis=1)
train_std = np.std(train_prec, axis=1)
test_mean = np.mean(eval_prec, axis=1)
test_std = np.std(eval_prec, axis=1)
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-7-f665845d2192> in <module>
      2 from sklearn.model_selection import validation_curve
      3 
----> 4 train_prec, eval_prec = validation_curve(estimator=model, X=x_train,
      5                                         y=y_train, param_name='max_depth',
      6                                         param_range=max_deep_list, cv=5)

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/sklearn/utils/validation.py in inner_f(*args, **kwargs)
     61             extra_args = len(args) - len(all_args)
     62             if extra_args <= 0:
---> 63                 return f(*args, **kwargs)
     64 
     65             # extra_args > 0

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/sklearn/model_selection/_validation.py in validation_curve(estimator, X, y, param_name, param_range, groups, cv, scoring, n_jobs, pre_dispatch, verbose, error_score, fit_params)
   1647     parallel = Parallel(n_jobs=n_jobs, pre_dispatch=pre_dispatch,
   1648                         verbose=verbose)
-> 1649     results = parallel(delayed(_fit_and_score)(
   1650         clone(estimator), X, y, scorer, train, test, verbose,
   1651         parameters={param_name: v}, fit_params=fit_params,

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/parallel.py in __call__(self, iterable)
   1042                 self._iterating = self._original_iterator is not None
   1043 
-> 1044             while self.dispatch_one_batch(iterator):
   1045                 pass
   1046 

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/parallel.py in dispatch_one_batch(self, iterator)
    857                 return False
    858             else:
--> 859                 self._dispatch(tasks)
    860                 return True
    861 

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/parallel.py in _dispatch(self, batch)
    775         with self._lock:
    776             job_idx = len(self._jobs)
--> 777             job = self._backend.apply_async(batch, callback=cb)
    778             # A job can complete so quickly than its callback is
    779             # called before we get here, causing self._jobs to

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/_parallel_backends.py in apply_async(self, func, callback)
    206     def apply_async(self, func, callback=None):
    207         """Schedule a func to be run"""
--> 208         result = ImmediateResult(func)
    209         if callback:
    210             callback(result)

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/_parallel_backends.py in __init__(self, batch)
    570         # Don't delay the application, to avoid keeping the input
    571         # arguments in memory
--> 572         self.results = batch()
    573 
    574     def get(self):

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/parallel.py in __call__(self)
    260         # change the default number of processes to -1
    261         with parallel_backend(self._backend, n_jobs=self._n_jobs):
--> 262             return [func(*args, **kwargs)
    263                     for func, args, kwargs in self.items]
    264 

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/joblib/parallel.py in <listcomp>(.0)
    260         # change the default number of processes to -1
    261         with parallel_backend(self._backend, n_jobs=self._n_jobs):
--> 262             return [func(*args, **kwargs)
    263                     for func, args, kwargs in self.items]
    264 

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/sklearn/utils/fixes.py in __call__(self, *args, **kwargs)
    220     def __call__(self, *args, **kwargs):
    221         with config_context(**self.config):
--> 222             return self.function(*args, **kwargs)

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/sklearn/model_selection/_validation.py in _fit_and_score(estimator, X, y, scorer, train, test, verbose, parameters, fit_params, return_train_score, return_parameters, return_n_test_samples, return_times, return_estimator, split_progress, candidate_progress, error_score)
    596             estimator.fit(X_train, **fit_params)
    597         else:
--> 598             estimator.fit(X_train, y_train, **fit_params)
    599 
    600     except Exception as e:

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/sklearn/tree/_classes.py in fit(self, X, y, sample_weight, check_input, X_idx_sorted)
    901         """
    902 
--> 903         super().fit(
    904             X, y,
    905             sample_weight=sample_weight,

~/.cache/pypoetry/virtualenvs/mat281-2021-V7B8LTfe-py3.8/lib/python3.8/site-packages/sklearn/tree/_classes.py in fit(self, X, y, sample_weight, check_input, X_idx_sorted)
    392                                            min_impurity_split)
    393 
--> 394         builder.build(self.tree_, X, y, sample_weight)
    395 
    396         if self.n_outputs_ == 1 and is_classifier(self):

KeyboardInterrupt: 
# graficando las curvas
plt.plot(max_deep_list, train_mean, color='r', marker='o', markersize=5,
         label='entrenamiento')
plt.fill_between(max_deep_list, train_mean + train_std, 
                 train_mean - train_std, alpha=0.15, color='r')
plt.plot(max_deep_list, test_mean, color='b', linestyle='--', 
         marker='s', markersize=5, label='evaluacion')
plt.fill_between(max_deep_list, test_mean + test_std, 
                 test_mean - test_std, alpha=0.15, color='b')
plt.legend(loc='center right')
plt.xlabel('numero_nodos')
plt.ylabel('precision')
plt.show()
../../../../_images/04_overfitting_underfitting_15_0.png