Introduccion a la evaluacion y seleccion de modelos

Metodologías aplicables en aprendizaje supervisado

Agenda

1- Introducción

  • Asuntos de la comunidad
  • Recursos en Github (temas vistos previamente)
    • Introducción al análisis exploratorio de datos
    • Introducción a la programación estadística
    • Introducción a la regresión

Agenda

2- Conceptos fundamentales (spanglish!)

  • Learners
  • Classifiers, regressors & models
  • Parameters & HYPERparameters
  • Generalization
    • Underfitting
    • Overfitting
  • Etc... etc...

Agenda

3- Protocolos de aprendizaje

  • Train & Test
  • Train, validate, test
  • Cross-validation
    • Nested cross-validation
  • Temas para una próxima ocasión
    • Repeated cross-validation (RxK)
    • Repeated nested cross-validation
    • Basta de CV: other resampling techniques
      • Bootstrap
      • ...

Conceptos fundamentales

Definiciones no rigurosas (a 35,000 pies de altura)

  • Modelo (primer significado):
        Mecanismo capaz de codificar conocimiento para solución de problemas. Tipicamente es una función compuesta de parametros, ej.: modelo de regresión linear ($y = \beta X + \epsilon$)

  • Learner (algoritmo):
        Algoritmo o metodo que toma como insumo datos y genera una función o programa capaz de emitir predicciones.

  • Aprendizaje (learning) o entrenamiento (training):
        La ejecución concreta o combinación de un learner con datos aplicables.

  • Classifiers, regressors & models (segundo significado):
        Implementación concreta de una función o programa que toma como insumo datos y genera predicciones. Aquí y en muchas ocasiones encontrarán que el término model se refiere a este concepto, el del resultado del aprendizaje o entrenamiento

Conceptos fundamentales

Definiciones no rigurosas (a 35,000 pies de altura)

  • Cost/Utility/Loss function:
        Función objetivo o de costo que juzga basado en los datos que tan bien se han ajustado los parameters al problema especifico.

  • Parameters:
        Conjunto de elementos configurables/variables que un learner ajusta durante su proceso de aprendizaje para producir un clasificador o regresor

  • HYPERparameters:
        Conjunto de elementos configurables del learner que no se ajustan durante el proceso de aprendizaje, sino que deben ser determinados "manualmente" por nosotros.

  • Predictores (features):
         Subconjunto vertical de los datos (columnas) que caracterizan cada ejemplo/registro y que son utilizados como insumo para predecir/clasificar.

  • Etiquetas (labels):
         Subconjunto vertical de los datos (columnas) que queremos predecir.

Conceptos fundamentales

Definiciones no rigurosas (a 35,000 pies de altura)

  • Generalization:
         El objetivo último de que el modelo aprendido tenga un buen rendimiento con datos desconocidos, es decir en la vida real. El "buen rendimiento" puede definirse con la misma función de utilidad que se uso durante aprendizaje o con otras metricas.

  • Metricas:
         Función que tipicamente toma como insumo los datos (predictores y etiquetas) y valora el rendimiento del modelo, es decir que tanto se acercan las predicciones a la realidad.

  • Ruído (Noise):
         Término genérico que agrupa varias fuentes de distorción no deseada en los datos. Puede ser error de medición (humano o del instrumento), error de recolección, y variabilidad en los datos debido a las simplificaciones que son parte del modelo (1era definicion) usado, entre otras posibles fuentes.

  • Señal:
         Todo lo que no es ruido. Estos dos términos se toman prestados del área de procesamiento de señales, pero su uso es menos estricto.

  • Outlier (valor atipico):
         Es una observación que es muy distinta a las demás. Podría ser ruido o señal, es dificil saber de antemano. Su presencia debe ser considerada puesto que puede tener impacto en cualquier análisis.

  • Underfitting:
        Cuando un modelo no aprende como esperado de los datos y su rendimiento con los mismos datos con los que fue entrenado es "decepcionante".

  • Overfitting:
        Cuando un modelo presenta muy buen rendimiento con los datos con los que fue entrenado (o validado) pero no así con datos no vistos previamente. Podrían escuchar decir que se aprendío del ruido (algo indeseable).

Overfitting curve

Protocolos de Aprendizaje

Pasos realizados para construir modelos usando learners y datos de forma que se garantice:

  • Buen rendimiento (según la métrica elegida)
  • Poder obtener un estimador no sesgado de la generalización. Es decir, una idea sincera de cómo funcionará nuestro modelo en la práctica.

Train & Test

El protocolo más sencillo de todos

Dividimos los datos en dos partes. Entrenamos con una y estimamos la generalizacion con la restante.

Es la base de todos los demás y nos enseña el principio FUNDAMENTAL:

    Debemos estimar la generalización siempre con datos que no hayan sido usados en lo absoluto


Cons:

  • Solo tenemos un estimador punto de la generalización (un único valor)
  • Solo debemos ajustar los parametros de un modelo. Por qué?

Train, validate & Test

Si nuestro learner tiene hyperparameters y queremos optimizarlos debemos explorar diferentes valores/combinaciones.

Dividir los datos en tres partes:

  • Train, para construir modelos con las diferentes configuraciónes de hyperparametros
  • Validate, para elegir el la configuración de hyperparametros que tiene mejor rendimiento
  • Test, para estimar la generalización del modelo elegido

Cross-validation (CV)

Empezamos a complicarnos para atacar la primera debilidad del protocolo Train & Test: que sólo tenemos un punto/valor estimado de la generalización (ej.: 95% casos bien clasificados). No sabemos qué tan preciso podría ser ese estimado.

Quisieramos poder decir algo como: "Clasificaremos correctamente entre 92% y 98% de los casos"

Para eso necesitamos una distribución para el estimado de la generalización, es decir varios valores con los cuales podamos calcular promedio, desviación estándar, etc.

Así que:

  • dividimos los datos en K (2, 3, 4,...,K) partes
  • entrenamos con todas las partes excepto una
  • esa la reservamos para estimar la generalización
  • repetimos el proceso K veces, cambiando la parte con la cual estimamos
  • acumulamos todos los estimados en una una muestra de la distribución de la generalización
  • tomamos el promedio como estimado final y lo calificamos con la desviación estandar de la muestra (como minimo)

Cross-validation

Y qué de los hyperparameters

Podemos usar Cross-Validation tal cual lo acabamos de describir para de una misma vez:

  • escoger la mejor combinación de hyperparameters de nuestro learner
  • estimar la generalización del mejor modelo

Nested Cross-validation

Nested Cross-validation (CV)

Después de reenfocar la vista del mareo anterior, recordemos lo que estamos buscando:

Una estimación honesta de la generalización del modelo que finalmente usaremos en la práctica

Con lo que hemos considerado hasta ahora, para lograrlo debemos:

  1. Probar multiples combinaciones de hyperparameters para los datos que tenemos
  2. Elegir el "mejor" de los que vimos
  3. Estimar la generalización de este "mejor" modelo

El nivel exterior de este protocolo nos dará una muestra de la distribución de la generalización, lo mismo que teníamos con el protocolo anterior.

El nivel interior de CV en este protocolo nos permitirá probar múltiples combinaciones de hyperparameters y elegir la que tenga el mejor rendimiento.

Un ejemplo

con multiples oportunidades de mejora...


In [6]:
import pandas as pd
import numpy as np
from scipy import stats
from scipy.stats import norm, skew
from sklearn.preprocessing import LabelEncoder
from scipy.special import boxcox1p

In [7]:
def transform(X):
    """Encapsular las transformaciones y limpiezas realizadas en charla previa

    Asumiendo que X es un DataFrame y sin validación alguna

    """
    X.drop("Id", axis = 1, inplace = True)
    # Eliminamos los outliers
    X = X.drop(X[(X['GrLivArea']>4000) & (X['SalePrice']<300000)].index)
    # Empleamos la función log1p de numpy para obtener el log(1+x) de todos los elementos de la variable objetivo
    y = np.log1p(X["SalePrice"])
    # Eliminamos la variable a predecir
    X.drop(['SalePrice'], axis=1, inplace=True)
    #
    X["PoolQC"] = X["PoolQC"].fillna("None")
    #
    X["MiscFeature"] = X["MiscFeature"].fillna("None")
    X["Alley"] = X["Alley"].fillna("None")
    X["Fence"] = X["Fence"].fillna("None")
    X["FireplaceQu"] = X["FireplaceQu"].fillna("None")
    #
    for col in ('GarageType', 'GarageFinish', 'GarageQual', 'GarageCond'):
        X[col] = X[col].fillna('None')
    # 
    # Agrupamos por vecindario y completamos los valores perdidos con la mediana de LotFrontage para todos los vecindarios
    X["LotFrontage"] = X.groupby("Neighborhood")["LotFrontage"].transform(
        lambda x: x.fillna(x.median()))
    #
    for col in ('GarageYrBlt', 'GarageArea', 'GarageCars'):
        X[col] = X[col].fillna(0)
    #    
    for col in ('BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF','TotalBsmtSF', 'BsmtFullBath', 'BsmtHalfBath'):
        X[col] = X[col].fillna(0)
    #
    for col in ('BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2'):
        X[col] = X[col].fillna('None')
    #
    X["MasVnrType"] = X["MasVnrType"].fillna("None")
    X["MasVnrArea"] = X["MasVnrArea"].fillna(0)
    #
    X['MSZoning'] = X['MSZoning'].fillna(X['MSZoning'].mode()[0])
    X = X.drop(['Utilities'], axis=1)
    X["Functional"] = X["Functional"].fillna("Typ")
    X['Electrical'] = X['Electrical'].fillna(X['Electrical'].mode()[0])
    X['KitchenQual'] = X['KitchenQual'].fillna(X['KitchenQual'].mode()[0])
    X['Exterior1st'] = X['Exterior1st'].fillna(X['Exterior1st'].mode()[0])
    X['Exterior2nd'] = X['Exterior2nd'].fillna(X['Exterior2nd'].mode()[0])
    X['SaleType'] = X['SaleType'].fillna(X['SaleType'].mode()[0])
    X['MSSubClass'] = X['MSSubClass'].fillna("None")
    #
    # MSSubClass = El tipo de edificios
    X['MSSubClass'] = X['MSSubClass'].apply(str)
    # OverallCond
    X['OverallCond'] = X['OverallCond'].astype(str)
    # Año y mes de venta.
    X['YrSold'] = X['YrSold'].astype(str)
    X['MoSold'] = X['MoSold'].astype(str)
    #
    cols = ('FireplaceQu', 'BsmtQual', 'BsmtCond', 'GarageQual', 'GarageCond', 
            'ExterQual', 'ExterCond','HeatingQC', 'PoolQC', 'KitchenQual', 'BsmtFinType1', 
            'BsmtFinType2', 'Functional', 'Fence', 'BsmtExposure', 'GarageFinish', 'LandSlope',
            'LotShape', 'PavedDrive', 'Street', 'Alley', 'CentralAir', 'MSSubClass', 'OverallCond', 
            'YrSold', 'MoSold')
    # procesa columnas, applicando LabelEncoder a los atributos categóricos
    for c in cols:
        lbl = LabelEncoder() 
        lbl.fit(list(X[c].values)) 
        X[c] = lbl.transform(list(X[c].values))
    #
    # Adicionamos el total de pies cuadrados (TotalSF) de la vivienda
    X['TotalSF'] = X['TotalBsmtSF'] + X['1stFlrSF'] + X['2ndFlrSF']
    #
    numeric_feats = X.dtypes[X.dtypes != "object"].index
    # Verificamos el sesgo de todos los atributos numéricos
    skewed_feats = X[numeric_feats].apply(lambda x: skew(x.dropna())).sort_values(ascending=False)
    skewness = pd.DataFrame({'Skew' :skewed_feats})
    #
    skewness = skewness[abs(skewness) > 0.75]
    skewed_features = skewness.index
    lam = 0.15
    for feat in skewed_features:
        X[feat] = boxcox1p(X[feat], lam)
    #
    X = pd.get_dummies(X)
    #
    return X.values, y.values

In [8]:
train = pd.read_csv("../data/train.csv")
train.head()


Out[8]:
Id MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape LandContour Utilities ... PoolArea PoolQC Fence MiscFeature MiscVal MoSold YrSold SaleType SaleCondition SalePrice
0 1 60 RL 65.0 8450 Pave NaN Reg Lvl AllPub ... 0 NaN NaN NaN 0 2 2008 WD Normal 208500
1 2 20 RL 80.0 9600 Pave NaN Reg Lvl AllPub ... 0 NaN NaN NaN 0 5 2007 WD Normal 181500
2 3 60 RL 68.0 11250 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 9 2008 WD Normal 223500
3 4 70 RL 60.0 9550 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 2 2006 WD Abnorml 140000
4 5 60 RL 84.0 14260 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 12 2008 WD Normal 250000

5 rows × 81 columns


In [9]:
X, y = transform(train.copy())

In [10]:
from sklearn.model_selection import GridSearchCV, cross_val_score, KFold
from sklearn.linear_model import ElasticNet
#
np.random.seed(42)
#
enet = ElasticNet()
p_grid = {"alpha": np.logspace(-3,1,10),
          "l1_ratio": [0.001, .01, .1, 0.25, 0.5],
         "max_iter": [1000,5000],
         "normalize": [True, False]}
#
inner_cv = KFold(n_splits=4, shuffle=True)
outer_cv = KFold(n_splits=4, shuffle=True)
# 
clf = GridSearchCV(estimator=enet, param_grid=p_grid, cv=inner_cv, n_jobs=-1)
#
nested_score = cross_val_score(clf, X=X, y=y, cv=outer_cv)
#
print('Avg Score {:4f} +/- {:4f}'.format(nested_score.mean(),nested_score.std()*2))


Avg Score 0.914551 +/- 0.025956

Consideraciones/dudas adicionales para una próxima

  • Cómo elegir entre diferentes algoritmos (y sus respectivos hyperparameters)?
  • Cuál es el impacto de las manipulaciones previas que se hicieron a los datos? (función transform)?
  • Cuál es el impacto de ligeros cambios en los datos usados?
    • Qué pasa si barajamos los datos y realizamos el experimento nuevamente? Qué tanto cambia la métrica?
  • Sobre el código como tal: cómo sé cuales fueron los hyperparameters exactos que me dieron el mejor modelo?