Se trata de la librería de machine learning por excelencia en Python. Dispone de una API muy elegante, con una lógica consistente que hace muy sencillo aplicar distintos tipos de algoritmos para la predicción, la clasificación, la reducción de dimensión, el preprocesamiento de los datos así como la búsqueda de grupos (clustering).
Un concepto básico en scikit-learn
es el de "estimator" y de "transformer".
Un "transformer" es un procedimiento de procesado de los datos (para estanderizarlos por ejemplo, o imputar valores faltantes), mientras que un algoritmo que se ajuste a los datos es un "estimator". Una combinación de ellos pertenece a ambas clases a la vez.
Por ejemplo, consideremos el algoritmo de regresion lineal
from sklearn.linear_model import LinearRegression
o el procedimiento de estanderización:
from sklearn.preprocessing import StandardScaler
El primero es un estimator
mientras que el segundo es un transformer
. Les aplicamos varios pasos:
Varios pasos se siguen en orden para aplicar (un procedimiento o un algoritmo o una combinación de ambos)
X
e posiblemente 'y'). Se hace con el método fit
. Se hace inplace
.transform
y devuelve los datos procesados.predict
.Veamos un ejemplo con los datos de notas DURM y ETSIT que vimos en la práctica anterior
Empezamos por importar los datos en un DataFrame
que llamaremos grados
import numpy as np
import pandas as pd
grados = pd.read_csv(
'https://multimediarepository.blob.core.windows.net/imagecontainer/51b97ec8969047fa934d4f9bdeeb1297.csv'
)
Nos interesa, para empezar, predecir MEDIA
, la nota media de los alumnos en su grado a partir de la calificación media obtenida en las pruebas de acceso a la universidad NOTA_PAU_CALIFICACION
.
Definimos la matriz X
de diseño y el vector y
de respuestas.
grados_limpio = grados.dropna(subset=['MEDIA', 'NOTA_PAU_CALIFICACION'])
y = grados_limpio['MEDIA'].values
X = grados_limpio['NOTA_PAU_CALIFICACION'].values.reshape(-1,1)
Vamos a usar el estimator LinearRegression
del submodulo linear_model
from sklearn.linear_model import LinearRegression
Instanciamos el estimador
lin_reg = LinearRegression()
Actualizamos el estimador, ajustando los valores de sus parámetros usando los datos.
lin_reg.fit(X, y) # Se hace 'inplace'
LinearRegression()
Si tenemos nuevos individuos con sus características, (X) podemos predecir la respuesta. Por ejemplo para un alumno que haya obtenido un 8 en las pruebas de acceso:
lin_reg.predict(np.array([8]).reshape(-1, 1))
array([7.36303681])
Veamos ahora cómo podemos estanderizar características usando el estimador StandardScaler
del submódulo preprocessing
from sklearn.preprocessing import StandardScaler
Instanciamos el transformador
scaler = StandardScaler()
Actualizamos el transformador, ajustando los valores de sus parámetros usando los datos.
scaler.fit(X)
StandardScaler()
Podemos ahora normalizar las características y guardarlas en una matriz usando transform
.
columnas_normalizadas = scaler.transform(X)
También podemos aplicar el transformador sobre las características de un nuevo individuo
scaler.transform(np.array([[8]]))
array([[0.49856921]])
Los estimadores o transformadores tienen atributos que contienen información relevante sobre su trabajo.
Por ejemplo, en el caso de lin_reg
, podemos consultar los coeficientes del ajuste:
lin_reg.coef_
array([0.35175585])
lin_reg.intercept_
4.5489899950369574
En la práctica anterior, implementando el algoritmo del gradiente, habíamos obtenido:
Para ello, usamos las "pipelines" de scikit-learn
.
from sklearn.pipeline import Pipeline
Formamos un flujo de trabajo, especificando los pasos con tuplas (nombre, objeto). Supongamos por ejemplo, que queremos imputar valores faltantes y luego normalizar las características.
scaler = StandardScaler()
lin_reg = LinearRegression()
regresor = Pipeline([('estanderizacion', scaler), ('regresion', lin_reg)])
Ahora podemos aplicar el nuevo objeto combinado a los datos
regresor.fit(X, y)
Pipeline(steps=[('estanderizacion', StandardScaler()), ('regresion', LinearRegression())])
Y predecir la respuesta para nuevos individuos
regresor.predict(np.array([8, 7]).reshape(-1, 1))
array([7.36303681, 7.01128095])
Una vez que tenemos ajustado un modelo, queremos comprobar la calidad del ajuste. Para ello, usamos indicadores que midan lo cerca que están los datos predichos de los datos observados.
En el submódulo metrics
de scikit-learn
, están implementados muchos indicadores sobre la calidad del ajuste, apropiados según el tipo de problema que consideremos.
En nuestro caso de predicción de una respuesta, podemos usar el error cuadrático medio residual (Residual Mean Square Error, RMSE), que coincide con el valor final de la función de coste optimizada:
donde $y_i^{pred}$ o $\hat{y}_i$ representan el valor predicho por el modelo para el individuo $i$.
Otra opción sería el error absoluto medio residual (Mean Absolute Deviation, MAD):
En nuestro caso de la predicción de la nota media:
from sklearn.metrics import mean_squared_error
# Calculamos los valores predichos de nuestro modelo sobre el conjunto de características:
y_pred = lin_reg.predict(X)
# Podemos calcular el RMSE:
mean_squared_error(y_pred, y)
8.437530500244971
Si preferimos usar el error absoluto medio:
from sklearn.metrics import mean_absolute_error
mean_absolute_error(y_pred, y)
2.8569714899297183
En las transparencias anteriores, hemos medido la calidad del modelo sobre los mismos datos (X, y) que hemos usado para ajustarlo.
Puede dar una impresión falsa de bondad en el ajuste.
Consideremos estos datos:
Podemos usar un polinomio de orden 2 o un polinomio de orden 5 como hipótesis:
El modelo de grado 5 parece mejor, pero qué pasa si queremos usar los modelos para predecir un nuevo valor?
La predicción es mucho peor para el nuevo individuo en el modelo más complejo
Es el problema que se conoce como "overfitting" (sobre ajuste) en el que el modelo es demasiado complejo y consigue un ajuste bueno de manera artificial.
Para detectar el overfitting, debemos evaluar el modelo sobre unos datos diferentes de los que han servido para entrenarlo.
Separamos el conjunto entero en "training set" y en "test set".
Attribution: EpochFail, CC BY-SA 4.0 via Wikimedia Commons https://commons.wikimedia.org/wiki/File:Supervised_machine_learning_in_a_nutshell.svg
sckit-learn
nos proporciona funciones para realizar esta separacion de manera sencilla.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.33, random_state=314)
print(f'Tamaños: X: {X.shape}, X_train: {X_train.shape}, X_test: {X_test.shape}')
print(f'Tamaños: y: {y.shape}, y_train: {y_train.shape}, y_test: {y_test.shape}')
Tamaños: X: (313, 1), X_train: (209, 1), X_test: (104, 1) Tamaños: y: (313,), y_train: (209,), y_test: (104,)
Podríamos incluso haber realizado esta partición de manera stratificada, respetando la proporción de los dos grados en el conjunto. Se hace con la función
StratifiedShuffleSplit
del mismo submódulomodel_selection
.
A la hora de calcular una medida de la calidad del ajuste de nuestra hipótesis, se podría dar el caso de que la parte del conjunto que hemos escogido al azar y usado para ajustar es particularmente propicio, por casualidad al modelo.
Para asegurarnos de que no se debe a la suerte, usamos el procedimiento de validación cruzada (aquí con 10 partes):
En inglés, se llama k-fold cross validation, $k$ es el número de "pliegues", es decir partes que usamos.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(
lin_reg,
X_train,
y_train,
scoring='neg_mean_squared_error',
cv=10
)
print(f'Media de las puntuaciones: {- np.mean(scores)}')
print(f'Desv. Típica de las puntuaciones: {np.std(-scores)}')
Media de las puntuaciones: 0.30167625217975447 Desv. Típica de las puntuaciones: 0.09854748002193772
scikit-learn utiliza función de utilidad (número mayor es mejor) en lugar de función de coste. Especificamos el opuesto de RSME, con
scoring='neg_mean_squared_error'
y calculamos el opuesto descores
.
El funcionamiento de muchos de los algoritmos dependen de los valores de parámetros que hay que fijar. En el caso del algoritmo del gradiente por ejemplo, está el número de iteraciones así como la taza de aprendizaje. No son parámetros del modelo, sino del algoritmo que busca encontrar el modelo, por lo que se llaman hiperparámetros.
Para ilustrarlo, supongamos que queremos aplicar otro tipo de regresión: la regresión "Nearest Neighbors". Su principio es muy sencillo:
Fuente: Scikit-learn User Guide: https://scikit-learn.org/stable/modules/neighbors.html#regression
Está claro que el valor que escojamos de $K$ condiciona mucho el resultado del algoritmo:
En nuestro ejemplo de la predicción de la nota media:
$K$ es un hiperparámetro que tendremos que fijar. Para ello, es buena práctica ir probando varios valores para ver cómo se comporta el algoritmo. Por ejemplo, para $K=5, 10, 20$
Seguiremos el procedimiento:
scikit-learn
hace muy fácil este procedimiento con GridSearchCV
from sklearn.model_selection import GridSearchCV
param_grid = {'n_neighbors': [5, 10, 20]}
knr = KNeighborsRegressor()
grid_search = GridSearchCV(knr, param_grid, cv=5, scoring='neg_mean_squared_error')
Con esta última línea hemos instanciado el estimador, ahora toca los pasos de ajuste y predicción
grid_search.fit(X_train, y_train)
GridSearchCV(cv=5, estimator=KNeighborsRegressor(), param_grid={'n_neighbors': [5, 10, 20]}, scoring='neg_mean_squared_error')
El resultado de grid_search es el mejor estimador, puedo usarlo directamente para la predicción sobre el conjunto test:
y_test_pred = grid_search.predict(X_test)
from sklearn.metrics import mean_squared_error
mean_squared_error(y_test, y_test_pred)
0.2303573679142884
Podríamos especificar más de un parámetro en param_grid
, anadiendo entradas al diccionario.
param_grid = {'n_neighbors': [5, 10, 20], 'weights': 'distance'} # sólo un valor para weights
param_grid = {'n_neighbors': [5, 10, 20], 'weights': ['uniform', 'distance']} # probará todas las combinaciones
Puedo comprobar qué valor o combinación de los hiperparámetros ha dado el mejor resultado
grid_search.best_params_
{'n_neighbors': 20}
O directamente el mejor estimador
grid_search.best_estimator_
KNeighborsRegressor(n_neighbors=20)
y puedo recorrer todos los algoritmos que se probaron, imprimiendo su puntuación:
resultados = grid_search.cv_results_
for mean_score, params in zip(resultados['mean_test_score'], resultados['params']):
print(np.sqrt(-mean_score), params)
0.5865605225144983 {'n_neighbors': 5} 0.5509375406309729 {'n_neighbors': 10} 0.5491018587481785 {'n_neighbors': 20}
Finalmente, en el caso en que hayamos construido un flujo de transformador(es) y estimador(es), hay que tener en cuenta que debemos, para especificar la lista de valores del hiperparámetro que queremos usar, añadir de prefijo el nombre que le hemos dado al paso asociado, con dos guiones bajos.
scaler = StandardScaler()
knr = KNeighborsRegressor()
regresor = Pipeline([('estanderizacion', scaler), ('knregression', knr)])
param_grid = {'knregression__n_neighbors': [5, 10, 20]}
grid_search = GridSearchCV(regresor, param_grid, cv=5, scoring='neg_mean_squared_error')
Imagen original: EpochFail, CC BY-SA 4.0 via Wikimedia Commons https://commons.wikimedia.org/wiki/File:Supervised_machine_learning_in_a_nutshell.svg