10. Aprendizaje Automático con Scikit-learn#
En este capítulo nos vamos a concentrar en la librería de código abierto
scikit-learn, una de las herramientas más utilizadas en Python para la
implementación de algoritmos de Aprendizaje Automático
(machine learning, ML), uno de los campos de las ciencias computacionales
con mayor impacto en la actualidad.
El aprendizaje automático es un área amplia y activa de investigación que, por su profundidad teórica y variedad de enfoques, normalmente requiere uno o varios cursos especializados. En este capítulo no se pretende cubrir los fundamentos matemáticos ni teóricos del área, sino que se asume que el lector cuenta con conocimientos básicos previos en temas como Minería de Datos, Aprendizaje Automático o Inteligencia Artificial.
El enfoque principal será la aplicación práctica de la librería ``scikit-learn`` en Python, mostrando cómo utilizar sus componentes para preprocesar los datos, entrenar, evaluar y utilizar modelos de aprendizaje automático de manera eficiente. Pondremos especial atención en el flujo de trabajo típico que sigue esta librería, así como en su integración con herramientas vistas en capítulos anteriores, como NumPy y Pandas.
A lo largo del capítulo se presentarán ejemplos claros y reproducibles que permitan al lector comprender cómo llevar modelos de aprendizaje automático desde los datos hasta su uso en aplicaciones reales, haciendo énfasis en la interpretación de resultados y en las buenas prácticas de uso de la librería.
10.1. El flujo de trabajo del Aprendizaje Automático#
Los algoritmos de aprendizaje automático forman parte de un proceso más amplio que requiere un flujo de trabajo cuyo objetivo general es extraer conocimiento a partir de los datos. En este proceso pueden intervenir muchas técnicas, como aprendizaje automático, reconocimiento de patrones, computación inteligente, estadística, procesamiento de lenguaje natural, visualización de datos e ingeniería de software, entre otras.
Este flujo de trabajo tiene su origen en el proceso de Extracción de Conocimiento de Bases de Datos (Knowledge Discovery in Databases, KDD).
Aunque el término KDD se utiliza con menor frecuencia en la literatura
industrial actual, el flujo de trabajo que propone sigue siendo la base
conceptual de los procesos modernos de ciencia de datos y aprendizaje
automático, incluidos los flujos de trabajo implementados con librerías
como scikit-learn.
Veamos en qué consiste este proceso, según el esquema propuesto por Brachman y Anand:
Como primer paso se debe identificar el objetivo del proceso de KDD. Por ejemplo, un proveedor de telefonía móvil podría estar interesado en identificar a aquellos clientes que no renovarán su contrato y se irán con la competencia. A esto se le conoce como la tasa de cancelación de clientes (en inglés churn rate o attrition rate), la cual es crucial para estimar el desempeño de la empresa.
El siguiente paso es seleccionar y recolectar los datos necesarios para el proceso. En nuestro ejemplo, podríamos requerir el historial de pagos de los clientes, datos sobre quejas y llamadas a soporte, servicios adicionales contratados o cancelados, entre otros. Esta información puede estar distribuida en diferentes bases de datos. También se pueden incluir datos recolectados por medio de sensores o sistemas externos, como lecturas de GPS, caídas de conexión o el número de aplicaciones instaladas por el cliente.
Es necesario preprocesar los datos para eliminar valores erróneos, datos faltantes, inconsistencias, cambios de formato, entre otros problemas. Este suele ser un proceso complejo y que puede demandar una cantidad considerable de recursos.
Dependiendo de los objetivos, los datos deben transformarse para facilitar su procesamiento. Por ejemplo, un documento de texto debe transformarse en una representación vectorial para permitir su análisis. De manera similar, una imagen puede convertirse en una representación simplificada que conserve sus características esenciales. En muchos casos también es necesario eliminar atributos que no aportan información relevante. Siguiendo nuestro ejemplo, podríamos descubrir que el número telefónico no es útil para distinguir el comportamiento del cliente, mientras que la marca y el modelo del dispositivo sí lo son.
En este paso se selecciona la tarea de minería de datos adecuada de acuerdo con el objetivo del proceso de KDD, por ejemplo clasificación, regresión o agrupamiento.
Se realiza un análisis exploratorio, en el cual se experimenta con distintos algoritmos de minería de datos o aprendizaje automático. Al seleccionar los algoritmos se deben considerar los tipos de datos disponibles, ya que algunos modelos no son adecuados para variables categóricas. También es necesario ajustar parámetros, evaluar el desempeño y comparar distintos enfoques.
En este paso se lleva a cabo el aprendizaje automático propiamente dicho, utilizando el o los algoritmos seleccionados anteriormente.
Este proceso produce los llamados patrones ocultos, los cuales describen la estructura subyacente de los datos. Siguiendo nuestro ejemplo, el resultado podría ser un conjunto de reglas que permitan decidir si un cliente cancelará su suscripción. Una regla podría ser:
SI el cliente tiene un promedio mayor a 7 días de retraso
AND su promedio mensual de llamadas es menor que 10
ENTONCES:
el cliente cancelará el servicio
Los patrones son, en esencia, modelos ajustados a los datos. Estos modelos no siempre se expresan en una forma directamente interpretable por los humanos. Por ejemplo, el resultado de un algoritmo de agrupamiento puede ser simplemente un conjunto de grupos de clientes que posteriormente deben ser analizados e interpretados.
10.2. ¿Qué es scikit-learn y para qué sirve?#
Precisamente scikit-learn incluye herramientas para cada uno de los pasos
descritos anteriormente, lo que la convierte en un ecosistema completo
para implementar el flujo de trabajo del aprendizaje automático en Python.
Aunque es muy útil para proyectos de tamaño mediano, no incluye capacidades
para el procesamiento de datos masivos como las utilizadas en entornos de
Big Data, ni librerías para deep learning con aceleración en múltiples GPUs,
como PyTorch o TensorFlow. Sin embargo, resulta muy atractiva para
abordar la mayoría de los problemas de aprendizaje automático convencional.
Sobre todo, scikit-learn es ideal para aprender los principios del
aprendizaje automático, ya que permite probar conceptos de manera rápida
utilizando una gran variedad de algoritmos listos para usarse. Además, la
librería está diseñada para ser extensible: los modelos y componentes que
se utilizan a lo largo del capítulo siguen una estructura bien definida, lo
que permite al usuario crear sus propios modelos y componentes de
preprocesamiento cuando las necesidades del problema así lo requieran.
En capítulos anteriores ya hemos trabajado con herramientas fundamentales como
NumPy, programación funcional y estructuras de datos, las cuales forman parte
del ecosistema sobre el que se construye scikit-learn.
10.3. El flujo de trabajo básico en scikit-learn#
En este capítulo haremos un recorrido por la librería siguiendo el flujo de trabajo del aprendizaje automático, utilizando ejemplos con distintos datasets, con el objetivo de mostrar de manera práctica cómo integrar las diferentes etapas del proceso.
Comenzaremos por el núcleo del proceso, asumiendo un escenario ideal en el que ya se ha realizado gran parte del trabajo previo. En este punto contamos con datos preprocesados y limpios, listos para ser utilizados en el proceso de aprendizaje automático. Bajo estas condiciones, el flujo de trabajo puede simplificarse a los siguientes pasos básicos:
Cargar los datos.
Entrenar un modelo utilizando parámetros básicos.
Evaluar qué tan bien funciona el modelo antes de utilizarlo en un problema real.
Este es el caso más simple y directo. Más adelante iremos incorporando pasos adicionales y desglosando con mayor detalle los componentes internos del proceso, tal como ocurre en entornos reales de desarrollo y de investigación.
10.4. Clasificando pingüinos#
Para este primer ejemplo vamos a utilizar el dataset de los pingüinos, un conjunto de datos sencillo y ampliamente utilizado con fines educativos en aprendizaje automático.
El objetivo será clasificar distintas especies de pingüinos a partir de características físicas medidas en cada individuo, como el tamaño del pico, la longitud de las aletas y el peso corporal. Este tipo de problema es un ejemplo clásico de clasificación supervisada, donde contamos con ejemplos etiquetados que nos permiten entrenar y evaluar un modelo.
Este dataset es ideal para comenzar porque:
Tiene un tamaño manejable.
Contiene variables numéricas fáciles de interpretar.
Permite visualizar claramente el flujo completo de trabajo del aprendizaje automático sin distraernos con detalles innecesarios.
A lo largo de esta sección seguiremos los pasos básicos descritos anteriormente: cargar los datos, entrenar un modelo sencillo y evaluar su desempeño antes de utilizarlo en un escenario real.
Nota
Este dataset es conceptualmente muy similar al clásico dataset de Iris, ampliamente utilizado en ejemplos introductorios de aprendizaje automático. En ambos casos se trata de un problema de clasificación supervisada con un número reducido de características numéricas y clases bien definidas. La principal diferencia es que el dataset de pingüinos resulta más cercano a problemas reales y evita algunas de las limitaciones conocidas del dataset de Iris.
Cargamos el dataset#
El dataset se encuentra en el repositorio público de GitHub de Allison Horst y consiste en dos archivos que contienen datos recolectados de 344 pingüinos encontrados en tres islas del Archipiélago Palmer, en la Antártida.
Los datos están disponibles bajo la licencia CC-0, de acuerdo con el Palmer Station LTER Data Policy y el LTER Data Access Policy for Type I data [5].
El primer archivo, llamado penguins, es una versión simplificada de los
datos originales. El segundo archivo, penguins_raw, contiene los datos
crudos tal como fueron capturados originalmente. En el repositorio original
los archivos se encuentran en formato del lenguaje R; para facilitar su
lectura en Python, podemos descargar los archivos en formato CSV desde el
sitio Kaggle
(requiere registro). Estos archivos también estarán disponibles en el
repositorio del libro.
Los archivos CSV que utilizaremos se llaman:
penguins_size.csv(versión simplificada)penguins_iter.csv(versión cruda)
Para leer los archivos vamos a suponer que se encuentran en el mismo directorio
desde donde ejecutamos el intérprete de Python. Comenzaremos leyendo la versión
simplificada utilizando la librería pandas:
>>> import pandas as pd
>>> df = pd.read_csv('penguins_size.csv')
>>> df.head()
species island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 MALE
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 FEMALE
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 FEMALE
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 FEMALE
Observamos que algunos registros contienen valores NaN, los cuales
interpretaremos como datos faltantes. Este es un escenario común en
conjuntos de datos reales y lo abordaremos más adelante.
Ahora imprimimos información general sobre el dataset y los tipos de datos de sus columnas:
>>> df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 344 entries, 0 to 343
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 species 344 non-null object
1 island 344 non-null object
2 culmen_length_mm 342 non-null float64
3 culmen_depth_mm 342 non-null float64
4 flipper_length_mm 342 non-null float64
5 body_mass_g 342 non-null float64
6 sex 334 non-null object
dtypes: float64(4), object(3)
memory usage: 18.9+ KB
Las columnas del dataset incluyen los siguientes atributos:
species: especie del pingüino (Chinstrap, Adélie o Gentoo)culmen_length_mm: longitud del culmen (mm)culmen_depth_mm: profundidad del culmen (mm)flipper_length_mm: longitud de la aleta (mm)body_mass_g: masa corporal (g)island: nombre de la isla (Dream, Torgersen o Biscoe)sex: sexo del pingüino
Como hay varios casos de datos categóricos a los que no se le asigna el tipo de dato correcto, cargaremos de nuevo los datos con el dtype correspondiente:
>>> df = pd.read_csv(
... 'penguins_size.csv',
... dtype={
... 'species': 'category',
... 'island': 'category',
... 'culmen_length_mm': 'float64',
... 'culmen_depth_mm': 'float64',
... 'flipper_length_mm': 'float64',
... 'body_mass_g': 'float64',
... 'sex': 'category'
... }
... )
Eliminamos registros con valores nulos#
Más adelante veremos las herramientas de scikit-learn para tratar los casos de valores nulos. En este primer ejercicio simplemente vamos a eliminar estos registros. Primero vemos cuales renglones incluyen valores nulos en alguna de sus columnas:
>>> df.isnull().any(axis=1)
0 False
1 False
2 False
3 True
4 False
...
339 True
340 False
341 False
342 False
343 False
Length: 344, dtype: bool
Podemos utilizar este vector de booleanos (máscara booleana) para filtrar aquellos que no tienen valores nulos y copiarlos en un nuevo DataFrame. Como esta es la idea, mejor vamos a utilizar directamente notnull() para quedarnos con los que cumplen con la condición (True). Es importante observar que ahora tenemos que utilizar el cuantificador all(axis=1) porque queremos no nulo en todas las columnas.
>>> df = df[df.notnull().all(axis=1)]
>>> df.isnull().all(axis=1).sum()
0
Preprocesamiento mínimo#
Los algoritmos de clasificación en scikit-learn requieren que tanto las
características como la clase objetivo estén representadas mediante
valores numéricos.
En nuestro dataset, la clase corresponde a la especie del pingüino, la
cual está representada como texto. Además, como vimos anteriormente, la columna
sex es una variable categórica. Para poder entrenar un modelo, es necesario
codificar este tipo de datos.
Para realizar esta codificación utilizaremos la librería
sklearn.preprocessing, en particular el encoder OrdinalEncoder, el
cual transforma variables categóricas asignando un valor numérico ordinal a
cada categoría:
>>> from sklearn.preprocessing import OrdinalEncoder
>>> encoder = OrdinalEncoder()
>>> df[['species', 'island', 'sex']] = encoder.fit_transform(
... df[['species', 'island', 'sex']]
... )
>>> df.head()
species island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex
0 0.0 2.0 39.1 18.7 181.0 3750.0 2.0
1 0.0 2.0 39.5 17.4 186.0 3800.0 1.0
2 0.0 2.0 40.3 18.0 195.0 3250.0 1.0
4 0.0 2.0 36.7 19.3 193.0 3450.0 1.0
5 0.0 2.0 39.3 20.6 190.0 3650.0 2.0
Advertencia
Es importante recordar que los datos ordinales implican la existencia de una
secuencia u orden inherente entre las categorías, lo cual no ocurre en
este caso. Aunque scikit-learn puede trabajar con este tipo de
codificación, algunos algoritmos de aprendizaje automático pueden interpretar
erróneamente estos valores como si existiera una relación de orden o
magnitud entre ellos.
En la mayoría de los casos, especialmente en problemas reales, es preferible
utilizar encoders alternativos como OneHotEncoder o TargetEncoder,
los cuales evitan introducir supuestos de orden que no están presentes en los
datos originales.
Nota
Como vimos anteriormente, estas transformaciones también pueden realizarse con pandas; aquí se
presentan utilizando scikit-learn para mantener un flujo de trabajo
coherente con el entrenamiento de modelos.
Preparar los datos para el algoritmo de clasificación#
Un clasificador es un algoritmo cuyo objetivo es aprender un modelo a partir de un conjunto de datos de entrenamiento [14]. Los datos son objetos representados mediante un vector de características y la categoría a la que pertenece cada objeto. Podemos representar a cada objeto como una tupla \((\vec{x}, y)\), donde \(\vec{x}\) es el vector de características y \(y\) es la categoría asociada, también llamada etiqueta o clase.
En el caso de scikit-learn (y de la mayoría de las librerías de machine
learning) el conjunto de vectores de características se representa como una
matriz X, mientras que las categorías correspondientes se representan como
un vector y. Los métodos que implementan los algoritmos de clasificación
esperan, de manera estándar, recibir estos dos objetos como parámetros.
Por esta razón, una tarea común al preparar los datos consiste en separar el
dataset en X y y. En este ejemplo, utilizaremos como clase objetivo
la columna species y el resto de las columnas como características
predictoras.
Vamos al código.
>>> X = df.drop(columns='species')
>>> y = df['species']
Podemos verificar las dimensiones de ambos objetos:
>>> X.shape
(333, 6)
>>> y.shape
(333,)
La matriz X contiene una fila por cada pingüino y una columna por cada
característica, mientras que el vector y contiene la clase asociada a cada
observación. Con estos datos ya estamos listos para entrenar nuestro primer
modelo de clasificación.
Como primer modelo de clasificación utilizaremos un árbol de decisión.
Este tipo de modelos es especialmente útil para comenzar, ya que su
funcionamiento es intuitivo y permite inspeccionar fácilmente las reglas que
aprende a partir de los datos.
En scikit-learn, los árboles de decisión se encuentran en el módulo
sklearn.tree:
>>> from sklearn import tree
>>> clf = tree.DecisionTreeClassifier()
>>> clf = clf.fit(X, y)
¡Listo! En clf ya tenemos un modelo de clasificación entrenado y listo para
ser utilizado. Una de las principales ventajas de los árboles de decisión
es que permiten inspeccionar el modelo aprendido y, en muchos casos,
interpretar sus decisiones.
Para ello, podemos imprimir el árbol en formato de texto utilizando la función
export_text:
>>> from sklearn.tree import export_text
>>> feature_names = X.columns.tolist()
>>> r = export_text(clf, feature_names=feature_names)
>>> print(r)
Advertencia
En este ejemplo entrenamos el modelo utilizando todos los datos disponibles, lo cual puede conducir a sobreentrenamiento (overfitting). Más adelante veremos cómo dividir los datos en conjuntos de entrenamiento y prueba para evaluar correctamente el desempeño del modelo.
Evaluación del modelo#
Dado que entrenamos el modelo con todo el dataset, no tendría sentido pedirle que clasifique un pingüino que ya ha visto durante el entrenamiento. Por esta razón, en la práctica es habitual separar el conjunto de datos en dos partes: un conjunto de entrenamiento y un conjunto de prueba.
Esta separación nos permite evaluar qué tan bien generaliza el modelo a datos no vistos. Más adelante veremos cómo extender esta idea mediante técnicas más robustas, como la validación cruzada, para reducir el riesgo de sobreentrenamiento.
Datos de entrenamiento y prueba#
Vamos a volver a crear el modelo, pero ahora separando el dataset en dos
conjuntos aleatorios: entrenamiento y prueba. El parámetro
test_size indica el porcentaje de datos que se reservarán para la prueba. El
tamaño del conjunto de entrenamiento será el complemento.
Aunque la partición es aleatoria, con el parámetro random_state definimos
la semilla para obtener experimentos repetibles:
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, test_size=0.20, random_state=12
... )
Ahora entrenamos de nuevo con los datos de entrenamiento X_train y
y_train:
>>> from sklearn import tree
>>> clf = tree.DecisionTreeClassifier(random_state=12)
>>> clf = clf.fit(X_train, y_train)
Podemos probar clasificando alguno de los pingüinos de prueba. Cuando el modelo
se entrena utilizando un DataFrame de pandas, es recomendable realizar
las predicciones utilizando el mismo tipo de estructura.
Como vimos anteriormente, aunque seleccionemos solo una fila podemos conservar el
DataFrame utilizando iloc[[i]]:
>>> X_test.iloc[[0]]
island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex
56 0.0 39.0 17.5 186.0 3550.0 1.0
>>> clf.predict(X_test.iloc[[0]])
array([0.])
Veamos si la predicción es correcta:
>>> y_test.iloc[0]
2.0
En este caso el clasificador no funcionó correctamente. Sin embargo, para evaluar el desempeño del modelo es necesario repetir este proceso para todos los registros del conjunto de prueba, utilizando una métrica apropiada.
Evaluación con una métrica#
La forma más directa de evaluar un clasificador consiste en predecir todas las etiquetas del conjunto de prueba y compararlas contra las etiquetas reales.
Comenzamos obteniendo las predicciones del modelo para los datos de prueba:
>>> y_pred = clf.predict(X_test)
>>> y_pred
array([2., 0., 2., 0., 2., 1., 2., 0., 1., 2., 0., 0., 0., 2., 0., 2., 0.,
2., 1., 0., 2., 0., 2., 2., 0., 0., 1., 2., 2., 1., 0., 0., 0., 2.,
2., 1., 2., 0., 2., 1., 2., 2., 2., 2., 2., 0., 0., 2., 2., 0., 2.,
1., 1., 0., 0., 1., 1., 0., 1., 2., 2., 0., 2., 1., 2., 2., 0.])
Exactitud (accuracy)#
Vamos a evaluar nuestro modelo de clasificación utilizando la métrica de exactitud (accuracy), la cual mide el porcentaje de ejemplos que fueron clasificados correctamente:
En scikit-learn esta métrica se encuentra disponible en el módulo
sklearn.metrics:
>>> from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred)
0.9850746268656716
El modelo alcanza una exactitud cercana al 98 %, lo cual indica que clasifica correctamente la gran mayoría de los ejemplos del conjunto de prueba.
Preprocesamiento y modelos con pipelines#
Hasta ahora hemos aplicado el preprocesamiento de los datos y el entrenamiento del modelo como pasos separados. En problemas reales, este enfoque puede volverse difícil de mantener, especialmente cuando el preprocesamiento es más complejo o cuando se prueban distintos modelos.
Para resolver este problema, scikit-learn proporciona el concepto de
pipeline, que permite encadenar varias etapas del flujo de trabajo en un
solo objeto. De esta manera, el preprocesamiento y el modelo se tratan como una
unidad coherente.
Un pipeline típico incluye:
una o más etapas de transformación de datos, y
una etapa final de aprendizaje automático.
Veamos cómo utilizar un pipeline sencillo con el mismo dataset de los pingüinos, incorporando ahora una transformación adicional para el tratamiento de valores faltantes.
Hasta este punto eliminamos manualmente los registros con valores nulos para simplificar los ejemplos. Sin embargo, en problemas reales esta estrategia no siempre es adecuada, ya que puede implicar la pérdida de información valiosa. Una de las principales ventajas de los pipelines es que el tratamiento de valores faltantes puede integrarse directamente como una etapa más del flujo de trabajo, garantizando que el mismo preprocesamiento se aplique de forma consistente durante el entrenamiento y la predicción.
Para ello utilizaremos el objeto SimpleImputer, el cual permite reemplazar
valores faltantes siguiendo una estrategia definida, como el promedio, la
mediana o el valor más frecuente.
En este ejemplo:
imputaremos las variables numéricas utilizando la mediana, y
codificaremos las variables categóricas mediante
OrdinalEncoder.
Primero definimos las columnas numéricas y categóricas:
>>> numeric_features = [
... 'culmen_length_mm',
... 'culmen_depth_mm',
... 'flipper_length_mm',
... 'body_mass_g'
... ]
>>> categorical_features = ['island', 'sex']
Es importante notar que los pipelines en scikit-learn se aplican
únicamente a las variables de entrada X. La clase objetivo y no forma
parte del pipeline y se proporciona directamente al método fit.
Esta separación refleja el flujo conceptual del aprendizaje automático: el preprocesamiento se aplica a los datos observables, mientras que las etiquetas se utilizan como referencia para el aprendizaje del modelo.
Ahora construimos los transformadores correspondientes:
>>> from sklearn.impute import SimpleImputer
>>> from sklearn.preprocessing import OrdinalEncoder
>>> from sklearn.compose import ColumnTransformer
>>> from sklearn.pipeline import Pipeline
>>> numeric_transformer = SimpleImputer(strategy='median')
>>> categorical_transformer = Pipeline(steps=[
... ('imputer', SimpleImputer(strategy='most_frequent')),
... ('encoder', OrdinalEncoder())
... ])
A continuación combinamos ambos tipos de variables utilizando un
ColumnTransformer:
>>> preprocessor = ColumnTransformer(
... transformers=[
... ('num', numeric_transformer, numeric_features),
... ('cat', categorical_transformer, categorical_features)
... ]
... )
Finalmente, integramos el preprocesamiento y el modelo de clasificación en un solo pipeline:
>>> from sklearn.tree import DecisionTreeClassifier
>>> pipeline = Pipeline(steps=[
... ('preprocess', preprocessor),
... ('classifier', DecisionTreeClassifier(random_state=0))
... ])
Entrenamos el pipeline utilizando los datos de entrenamiento originales, incluyendo valores nulos:
>>> pipeline.fit(X_train, y_train)
Finalmente calculamos la exactitud del modelo creado a partir del pipeline:
>>> y_pred = pipeline.predict(X_test)
>>> from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred)
0.9855072463768116
Aunque este valor de exactitud es alto, es importante recordar que el dataset es relativamente pequeño y que el modelo utilizado es capaz de ajustarse con facilidad a los datos. En problemas reales, es recomendable complementar esta evaluación con otras métricas y técnicas, como la validación cruzada, para obtener una estimación más robusta del desempeño del modelo.
Un ejemplo breve con redes neuronales#
scikit-learn también incluye implementaciones básicas de redes neuronales,
útiles con fines educativos y para problemas de tamaño pequeño. En particular,
podemos utilizar MLPClassifier (Perceptrón Multicapa) como clasificador.
Nota
Algunos modelos, como las redes neuronales, son sensibles a la escala de las
variables numéricas. En estos casos suele ser necesario aplicar técnicas de
normalización o estandarización, como StandardScaler.
La idea es exactamente la misma que en el ejemplo anterior: reutilizamos el mismo preprocesamiento (imputación + codificación) y sustituimos el modelo final por una red neuronal.
>>> from sklearn.neural_network import MLPClassifier
>>> from sklearn.pipeline import Pipeline
>>> nn_pipeline = Pipeline(steps=[
... ('preprocess', preprocessor),
... ('classifier', MLPClassifier(hidden_layer_sizes=(20,),
... max_iter=2000,
... random_state=0))
... ])
Entrenamos el modelo con los datos de entrenamiento:
>>> nn_pipeline.fit(X_train, y_train)
Y evaluamos su desempeño en el conjunto de prueba usando exactitud (accuracy):
>>> y_pred = nn_pipeline.predict(X_test)
>>> from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred)
0.6811594202898551
Este resultado nos puede sorprender por ser tan bajo, pero muestra una realidad del aprendizaje automático: no todo se resuelve utilizando el mejor algoritmo. De hecho, según el principio No Free Lunch Theorem no hay un algoritmo que sea el mejor en todos los casos. En este caso, el resultado se puede explicar por varias razones:
El dataset tiene muy pocos registros y las clases están bien separadas. La red no alcanza a ajustarse a los datos, en el caso de los árboles de decisión que hacen «cortes» rectos aquí es más fácil.
El preprocesamiento no fue adecuado (este normalmente es el caso)
Los parámetros (hiperparámetros) de la red no son los adecuados.
Veamos el caso del preprocesamiento.
Es importante notar que, en este ejemplo, utilizamos OrdinalEncoder para
codificar las variables categóricas. Esta codificación introduce una relación
numérica artificial entre categorías que no tienen un orden inherente.
Mientras que modelos como los árboles de decisión son poco sensibles a esta
representación, las redes neuronales sí se ven fuertemente afectadas por la
forma en que se codifican las variables categóricas. En estos casos, suele ser
preferible utilizar encoders como OneHotEncoder, que evitan introducir
datos ordinales.
El menor desempeño observado en este ejemplo resalta la importancia de realizar un preprocesamiento adecuado en función del algoritmo utilizado. Mejorar este resultado quedará como ejercicio para el lector.
Ejercicio
En el ejemplo con redes neuronales utilizamos el mismo pipeline que en el
caso del árbol de decisión, incluyendo la codificación de variables categóricas
mediante OrdinalEncoder. Como observamos, el desempeño del modelo fue
considerablemente menor.
Modifica el pipeline para utilizar
OneHotEncoderen lugar deOrdinalEncoderpara las variables categóricas.Entrena nuevamente el modelo de red neuronal utilizando
MLPClassifiery evalúa su desempeño en el conjunto de prueba.Compara los resultados obtenidos con los del árbol de decisión y reflexiona sobre cómo la representación de los datos influye en el desempeño del modelo.
Este ejercicio ilustra la importancia del preprocesamiento y muestra que distintos modelos pueden requerir distintas representaciones de los datos para obtener buenos resultados.
Resumen del capítulo#
En este capítulo presentamos una introducción práctica al aprendizaje automático con ``scikit-learn``, enfocándonos en el flujo de trabajo completo más que en los detalles teóricos de los algoritmos.
A lo largo del capítulo:
Revisamos el flujo general del aprendizaje automático y su relación con el proceso de Knowledge Discovery in Databases (KDD).
Trabajamos con un dataset real de pingüinos, utilizando
pandaspara su exploración y preparación inicial.Construimos y entrenamos un primer modelo de clasificación utilizando árboles de decisión.
Introdujimos buenas prácticas como la separación de datos en conjuntos de entrenamiento y prueba, así como la evaluación del modelo mediante una métrica sencilla.
Mostramos cómo integrar preprocesamiento y modelos mediante pipelines, incluyendo el tratamiento de valores faltantes.
Exploramos de manera breve el uso de redes neuronales dentro del mismo flujo de trabajo, destacando la importancia del preprocesamiento y de la representación de los datos.
Discutimos que la elección del modelo y del preprocesamiento depende del problema y de los datos disponibles, y que modelos más complejos no garantizan mejores resultados.
El objetivo principal de este capítulo no fue mostrar todas las capacidades de
scikit-learn, sino proporcionar una visión clara del flujo de
trabajo básico del aprendizaje automático en Python.