Читать книгу Python Machine Learning - Vahid Mirjalili - Страница 10
ОглавлениеUn recorrido por los clasificadores de aprendizaje automático con scikit-learn
En este capítulo, realizaremos un recorrido por una selección de algoritmos de aprendizaje automático potentes y conocidos que se utilizan habitualmente tanto en formación como en la industria. Al tiempo que aprendemos las diferencias entre varios algoritmos de aprendizaje automático para clasificación, también desarrollaremos una apreciación intuitiva de las fortalezas y debilidades de cada uno de ellos. Además, llevaremos a cabo nuestro primer paso con la librería scikit-learn, que proporciona una interfaz intuitiva para utilizar estos algoritmos de forma eficiente y productiva.
Los temas que aprenderemos en este capítulo son los siguientes:
•Introducción a algoritmos populares y robustos para clasificación, como regresión lógica, máquinas de vectores de soporte y árboles de decisión.
•Ejemplos y explicaciones mediante la librería de aprendizaje automático scikit-learn, que proporciona una amplia variedad de algoritmos de aprendizaje automático mediante una API de Python intuitiva.
•Discusiones acerca de las fortalezas y las debilidades de los clasificadores con límites de decisión lineales y no lineales.
Elegir un algoritmo de clasificación
La elección de un algoritmo de clasificación apropiado para una tarea problemática concreta requiere práctica; cada algoritmo posee sus propias peculiaridades y está basado en determinadas suposiciones. Volviendo a plantear el teorema de No hay almuerzo gratis de David H. Wolpert, ningún clasificador único funciona mejor en todos los escenarios (The Lack of A Priori Distinctions Between Learning Algorithms, Wolpert y David H, Neural Computation 8.7 (1996): 1341-1390). A la práctica, siempre se recomienda comparar el rendimiento de, como mínimo, un puñado de algoritmos de aprendizaje distintos para seleccionar el mejor modelo para un problema concreto. Estos deben ser distintos en cuanto al número de características o muestras, la cantidad de ruido en el conjunto de datos y las propiedades de las clases (si son linealmente separables o no).
Eventualmente, el rendimiento de un clasificador, tanto de rendimiento computacional como de poder predictivo, depende sobre todo de los datos subyacentes disponibles para el aprendizaje. Los cinco pasos principales que se dan en el entrenamiento de un algoritmo de aprendizaje automático se pueden resumir en los siguientes:
1.Seleccionar características y recopilar muestras de entrenamiento.
2.Elegir una medición del rendimiento.
3.Elegir un algoritmo clasificador y de optimización.
4.Evaluar el rendimiento del modelo.
5.Afinar el algoritmo.
Como el objetivo de este libro es generar un conocimiento del aprendizaje automático paso a paso, nos centraremos sobre todo en los conceptos principales de los distintos algoritmos que aparecen en este capítulo y revisaremos temas como la selección de características y el preprocesamiento, la medición del rendimiento y el ajuste de hiperparámetros para que podamos tratarlos con mayor detalle más adelante.
Primeros pasos con scikit-learn: entrenar un perceptrón
En el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación, has conocido dos algoritmos de aprendizaje para la clasificación relacionados, la regla del perceptrón y Adaline, que hemos implementado en Python por separado. Ahora, echaremos un vistazo a la API scikit-learn, que combina una interfaz intuitiva con una implementación altamente optimizada de varios algoritmos de clasificación. La librería scikit-learn ofrece no solo una amplia variedad de algoritmos de aprendizaje, sino también diferentes funciones sencillas para preprocesar datos y ajustar con precisión y evaluar nuestros modelos. Hablaremos de ello con más detalle, junto a los conceptos subyacentes, en el Capítulo 4, Generar buenos modelos de entrenamiento - Preprocesamiento de datos y en el Capítulo 5, Comprimir datos mediante la reducción de dimensionalidad.
Para empezar con la librería scikit-learn, vamos a entrenar un modelo de perceptrón parecido al que implementamos en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación. Para simplificar las cosas, en las siguientes secciones utilizaremos el ya conocido conjunto de datos Iris. El conjunto de datos Iris ya está disponible en scikit-learn, puesto que es un conjunto de datos popular y sencillo que se utiliza con frecuencia para probar y experimentar con algoritmos. Nosotros solo utilizaremos dos características del conjunto de datos Iris para finalidades de visualización.
Vamos a asignar la longitud del pétalo y la anchura del pétalo de 150 muestras de flores a la matriz de características X y las correspondientes etiquetas de clase de las especies de flor al vector y:
>>> from sklearn import datasets
>>> import numpy as np
>>> iris = datasets.load_iris()
>>> X = iris.data[:, [2, 3]]
>>> y = iris.target
>>> print('Class labels:', np.unique(y))
Class labels: [0 1 2]
La función np.unique(y) devuelve las tres únicas etiquetas de clase almacenadas en iris.target y, como podemos ver, los nombres de clase de la flor Iris Iris-setosa, Iris-versicolor e Iris-virginica ya se encuentran almacenados como enteros (aquí: 0, 1, 2). Aunque la mayoría de las funciones y métodos de clase de scikit-learn también funcionan con etiquetas de clase en formato de cadena, es recomendable utilizar etiquetas enteras para evitar fallos técnicos y mejorar el rendimiento computacional debido a un uso más pequeño de memoria. Además, codificar las etiquetas de clase como enteras es una convención común entre la mayoría de librerías de aprendizaje automático.
Para evaluar el buen funcionamiento de un modelo entrenado sobre datos no vistos, vamos a dividir el conjunto de datos en conjuntos de datos de prueba y entrenamiento independientes. Más adelante, en el Capítulo 6, Aprender las mejores prácticas para la evaluación de modelos y el ajuste de hiperparámetros, trataremos con más detalle las mejores prácticas en torno a la evaluación de modelos:
>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, test_size=0.3, random_state=1, stratify=y)
Mediante la función train_test_split del módulo model_selection de scikit-learn, dividimos de forma aleatoria las matrices X e y en un 30 % de datos de prueba (45 muestras) y un 70 % de datos de entrenamiento (105 muestras).
Observa que la función train_test_split ya mezcla los conjuntos de entrenamiento internamente antes de realizar la división; de no ser así, todas las muestras de clase 0 y clase 1 habrían terminado en el conjunto de entrenamiento, y el conjunto de prueba estaría compuesto por 45 muestras de la clase 2. Mediante el parámetro random_state, hemos proporcionado una semilla aleatoria fija (random_state=1) para el generador de números pseudoaleatorio interno que se utiliza para mezclar los conjuntos de datos antes de su división. Utilizar un random_state fijo como este garantiza que nuestros resultados sean reproducibles.
Por último, aprovechamos el soporte integrado para la estratificación mediante stratify=y. En este contexto, «estratificación» significa que el método train_test_split devuelve subconjuntos de prueba y de entrenamiento con las mismas proporciones que las etiquetas de clase de el conjunto de datos de entrada. Podemos utilizar la función bincount de NumPy, que cuenta los números de coincidencias de cada valor en una matriz, para verificar que este es realmente el caso:
>>> print('Labels counts in y:', np.bincount(y))
Labels counts in y: [50 50 50]
>>> print('Labels counts in y_train:', np.bincount(y_train))
Labels counts in y_train: [35 35 35]
>>> print('Labels counts in y_test:', np.bincount(y_test))
Labels counts in y_test: [15 15 15]
Muchos algoritmos de optimización y aprendizaje automático también requieren escalado de características para un rendimiento óptimo, como recordamos del ejemplo de descenso de gradiente en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación. En este caso, vamos a normalizar las características utilizando la clase StandardScaler del módulo preprocessing de scikit-learn:
>>> from sklearn.preprocessing import StandardScaler
>>> sc = StandardScaler()
>>> sc.fit(X_train)
>>> X_train_std = sc.transform(X_train)
>>> X_test_std = sc.transform(X_test)
Con el código anterior, hemos cargado la clase StandardScaler del módulo preprocessing y hemos inicializado un nuevo objeto StandardScaler, que hemos asignado a la variable sc. Con el método fit, StandardScaler ha estimado los parámetros μ (muestra media) y σ (desviación estándar) para cada dimensión de características de los datos de entrenamiento. Llamando al método transform, hemos normalizado los datos de entrenamiento mediante estos parámetros estimados de y . Observa que hemos utilizado los mismos parámetros de escalado para normalizar el conjunto de prueba, por lo que ambos valores en el conjunto de datos de prueba y de entrenamiento son comparables unos con otros.
Una vez normalizados los datos de entrenamiento, ya podemos entrenar un modelo de perceptrón. La mayoría de los algoritmos en scikit-learn soportan la clasificación multiclase por defecto mediante el método One-versus-Rest (OvR), que nos permite alimentar al perceptrón con las tres clases de flor a la vez. El código es el siguiente:
>>> from sklearn.linear_model import Perceptron
>>> ppn = Perceptron(n_iter=40, eta0=0.1, random_state=1)
>>> ppn.fit(X_train_std, y_train)
La interfaz de scikit-learn nos recuerda a la implementación de nuestro perceptrón en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación: después de cargar la clase Perceptron desde el módulo linear_model, iniciamos un nuevo objeto Perceptron y entrenamos el modelos mediante el método fit. En este caso, el parámetro de modelo eta0 es equivalente al rango de aprendizaje eta que utilizamos en la implementación de nuestro perceptrón, y el parámetro n_iter define el número de épocas (pasos en el conjunto de entrenamiento).
Como recordarás del Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación, encontrar un rango de aprendizaje apropiado requiere algo de experimentación. Si el rango de aprendizaje es demasiado amplio, el algoritmo superará el mínimo coste global. Si el rango de aprendizaje es demasiado pequeño, el algoritmo requerirá más épocas hasta la convergencia, hecho que puede provocar que el aprendizaje sea lento, especialmente en conjuntos de datos grandes. Además, también utilizamos el parámetro random_state para garantizar la reproducibilidad de la mezcla inicial del conjunto de datos de entrenamiento después de cada época.
Una vez entrenado un modelo en scikit-learn, ya podemos realizar predicciones mediante el método predict, exactamente como en la implementación de nuestro perceptrón en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación. El código es el siguiente:
>>> y_pred = ppn.predict(X_test_std)
>>> print('Misclassified samples: %d' % (y_test != y_pred).sum())
Misclassified samples: 3
Al ejecutar el código, vemos que el perceptrón clasifica erróneamente tres de las 45 muestras de flor. Por tanto, el error de clasificación en el conjunto de datos de prueba es aproximadamente el 0.067 o 6.7 % .
En lugar del error de clasificación, muchos de los que trabajan con el aprendizaje automático informan de la precisión de la clasificación de un modelo, que se calcula simplemente de la siguiente forma:1-error = 0.933 o 93.3 %. |
La librería también implementa una amplia variedad de mediciones de rendimiento distintas disponibles a través del módulo de medición. Por ejemplo, podemos calcular la precisión de clasificación del perceptrón en la prueba del modo siguiente:
>>> from sklearn.metrics import accuracy_score
>>> print('Accuracy: %.2f' % accuracy_score(y_test, y_pred))
Accuracy: 0.93
En este caso, y_test con las etiquetas de clase verdaderas e y_pred son las etiquetas de clase que habíamos predicho anteriormente. De forma alternativa, cada clasificador en scikit-learn tiene un método score, que calcula la precisión de la predicción de un clasificador combinando la llamada predict con accuracy_score, como se muestra a continuación:
>>> print('Accuracy: %.2f' % ppn.score(X_test_std, y_test))
Accuracy: 0.93
Observa que en este capítulo evaluamos el rendimiento de nuestros modelos en base al conjunto de prueba. En el Capítulo 5, Comprimir datos mediante la reducción de dimensionalidad, aprenderás unas útiles técnicas –incluyendo análisis gráficos como las curvas de aprendizaje– para detectar y prevenir el overfitting o sobreajuste. El sobreajuste significa que el modelo captura los patrones en los datos de entrenamiento, pero falla en la generalización de los datos no vistos. |
Por último, podemos utilizar nuestra función plot_decision_regions del Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación para mostrar gráficamente las regiones de decisión de nuestro modelo de perceptrón recién entrenado y visualizar cómo separa correctamente las diferentes muestras de flor. Sin embargo, vamos a añadir una pequeña modificación para destacar las muestras del conjunto de datos de prueba mediante unos pequeños círculos:
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
def plot_decision_regions(X, y, classifier, test_idx=None,
resolution=0.02):
# define generador de marcador y mapa de colores
markers = ('s', 'x', 'o', '^', 'v')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# representa la superficie de decisión
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
np.arange(x2_min, x2_max, resolution))
Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
Z = Z.reshape(xx1.shape)
plt.contourf(xx1, xx2, Z, alpha=0.3, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
alpha=0.8, c=colors[idx],
marker=markers[idx], label=cl,
edgecolor='black')
# destaca las muestras de prueba
if test_idx:
# representa todas las muestras
X_test, y_test = X[test_idx, :], y[test_idx]
plt.scatter(X_test[:, 0], X_test[:, 1],
c='', edgecolor='black', alpha=1.0,
linewidth=1, marker='o',
s=100, label='test set')
Con esta pequeña modificación en la función plot_decision_regions, ya podemos especificar los índices de las muestras que queremos marcar en los diagramas resultantes. El código es el siguiente:
>>> X_combined_std = np.vstack((X_train_std, X_test_std))
>>> y_combined = np.hstack((y_train, y_test))
>>> plot_decision_regions(X=X_combined_std,
... y=y_combined,
... classifier=ppn,
... test_idx=range(105, 150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
Como podemos ver en el diagrama resultante, las tres clases de flor no pueden ser separadas perfectamente por un límite de decisión lineal:
Recuerda que en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación dijimos que el algoritmo de perceptrón no converge nunca en conjuntos de datos que no son perfectamente separables linealmente, razón por la cual el uso del algoritmo de perceptrón no suele ser recomendable para esta práctica. En las siguientes secciones, trataremos unos clasificadores lineales más potentes que convergen en un mínimo coste, incluso si las clases no son perfectamente separables linealmente.
El Perceptron, así como otras funciones y clases de scikit-learn, con frecuencia tiene parámetros adicionales que nosotros omitimos para mayor claridad. Puedes leer más acerca de estos parámetros en la función help de Python (por ejemplo, help(Perceptron)) o accediendo a la excelente documentación online de scikit-learn en http://scikit-learn.org/stable/. |
Modelar probabilidades de clase mediante regresión logística
Aunque la regla de perceptrón ofrece una sencilla y agradable introducción a los algoritmos de aprendizaje automático para clasificación, su mayor inconveniente es que nunca converge si las clases no son perfectamente separables linealmente. La tarea de clasificación de la sección anterior sería un ejemplo de este caso. De forma intuitiva, podemos pensar que la razón por la que los pesos se actualizan continuamente es que siempre existe como mínimo una muestra clasificada erróneamente en cada época. Evidentemente, puedes cambiar el rango de aprendizaje y aumentar el número de épocas, pero ten en cuenta que el perceptrón no convergerá nunca en este conjunto de datos. Para aprovechar mejor el tiempo, veamos otro sencillo –a la vez que potente– algoritmo para problemas de clasificación binaria y lineal: la regresión logística. Ten en cuenta que, a pesar de su nombre, la regresión logística es un modelo para clasificación, no para regresión.
Intuición en regresión logística y probabilidades condicionales
La regresión logística es un modelo de clasificación muy sencillo de implementar y que funciona muy bien en clases separables lineales. Es uno de los algoritmos más utilizados para clasificación en la industria. Parecido al perceptrón y a Adaline, el modelo de regresión logística es también, en este caso, un modelo lineal para clasificación binaria que puede ampliarse a la clasificación multiclase, por ejemplo, mediante la técnica OvR.
Para explicar la idea que se esconde detrás de la regresión logística como modelo probabilístico, vamos a presentar primero la razón de probabilidades: las probabilidades de que ocurra un evento concreto. La razón de probabilidades se puede escribir como , donde significa la probabilidad del evento positivo. El término evento positivo no significa necesariamente bueno, sino que se refiere al evento que queremos predecir, por ejemplo, la probabilidad de que un paciente tenga una determinada enfermedad. Podemos pensar en el evento positivo como una etiqueta de clase . Seguidamente, también podemos definir la función logit, que es simplemente el logaritmo de la razón de probabilidades:
Ten en cuenta que log se refiere al logaritmo natural, puesto que es la convención común en informática. La función logit toma como entrada valores del rango de 0 a 1 y los transforma en valores de todo el rango de números reales, que podemos utilizar para expresar una relación lineal entre valores de características y logaritmos de la razón de probabilidades:
En este caso, es la probabilidad condicional de que una muestra en concreto pertenezca a la clase 1 dadas sus características x.
Ahora, nos interesa realmente predecir la probabilidad de que una determinada muestra pertenezca a una clase concreta, que es la forma inversa de la función logit. También se denomina función sigmoide logística, en ocasiones abreviada simplemente como función sigmoide, debido a su característica forma de S:
Aquí z es la entrada de red, la combinación lineal de pesos y las características de la muestra, .
Observa que, de forma similar a la convención que utilizamos en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación, se refiere al parámetro del sesgo y es un valor de entrada adicional que proporcionamos () que es igual a 1. |
Ahora vamos a mostrar gráficamente la función sigmoide para diferentes valores del rango de -7 a 7 para ver qué aspecto tiene:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> def sigmoid(z):
... return 1.0 / (1.0 + np.exp(-z))
>>> z = np.arange(-7, 7, 0.1)
>>> phi_z = sigmoid(z)
>>> plt.plot(z, phi_z)
>>> plt.axvline(0.0, color='k')
>>> plt.ylim(-0.1, 1.1)
>>> plt.xlabel('z')
>>> plt.ylabel('$\phi (z)$')
>>> # y axis ticks and gridline
>>> plt.yticks([0.0, 0.5, 1.0])
>>> ax = plt.gca()
>>> ax.yaxis.grid(True)
>>> plt.show()
Como resultado de la ejecución del código del ejemplo anterior, deberíamos ver la curva en forma de S (sigmoide):
Podemos ver que se acerca a 1 si z se dirige hacia el infinito (), puesto que pasa a ser muy pequeño para los amplios valores de z. De forma parecida, se dirige hacia 0 para como resultado de un denominador cada vez más grande. Así, llegamos a la conclusión de que la función sigmoide toma valores de números reales como entrada y los transforma en valores del rango [0, 1] con una intercepción en .
Para crear una intuición para el modelo de regresión lógica, podemos relacionarlo con el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación. En Adaline, utilizamos la función de identidad como función de activación. En regresión lógica, esta función de activación simplemente se convierte en la función sigmoide que hemos definido anteriormente. La diferencia entre Adaline y la regresión logística se muestra en la siguiente imagen:
La salida de la función sigmoide se interpreta como la probabilidad de que una muestra concreta pertenezca a la clase 1, , dadas sus características x parametrizadas por los pesos w. Por ejemplo, si calculamos para una muestra de flor en particular, significa que la oportunidad de que esta muestra sea una flor Iris-versicolor es del 0 %. Por lo tanto, la probabilidad de que esta flor sea una Iris-setosa se puede calcular como o 20 %. La probabilidad predicha se puede, simplemente, convertir después en un resultado binario mediante una función umbral:
Si miramos el gráfico anterior de la función sigmoide, este equivale a lo siguiente:
De hecho, existen muchas aplicaciones en las cuales no solo nos interesan las etiquetas de clase predichas, sino que también la estimación de la probabilidad de la pertenencia a una clase resulta particularmente útil (la salida de la función sigmoide antes de aplicar la función umbral). La regresión logística se utiliza en la previsión meteorológica, por ejemplo, no solo para predecir si lloverá un día en concreto sino también para informar de la posibilidad de lluvia. De forma parecida, la regresión logística se puede utilizar para predecir la posibilidad de que un paciente tenga una enfermedad concreta dados determinados síntomas, razón por la cual goza de una gran popularidad en el campo de la medicina.
Aprender los pesos de la función de coste logística
Ahora que ya has aprendido cómo podemos utilizar el modelo de regresión logística para predecir probabilidades y etiquetas de clase, vamos a hablar brevemente acerca de cómo debemos ajustar los parámetros del modelo, por ejemplo, los pesos w. En el capítulo anterior, definimos la suma de errores cuadráticos de la función de coste de la siguiente manera:
Minimizamos esta función para aprender los pesos w para nuestro modelo de clasificación Adaline. Para explicar cómo podemos derivar la función de coste para la regresión logística, en primer lugar debemos definir la probabilidad L que queremos maximizar al crear un modelo de regresión logística, asumiendo que las muestras individuales de nuestro conjunto de datos son independientes unas de otras. La fórmula es como esta:
A la práctica, es más fácil maximizar el logaritmo (natural) de esta ecuación, denominada función de probabilidad logarítmica:
En primer lugar, la aplicación de la función log reduce el potencial para el desbordamiento numérico, que puede ocurrir si las probabilidades son muy pequeñas. En segundo lugar, podemos convertir el producto de factores en una suma de factores, que hace más fácil obtener la derivada de esta función mediante el truco de la suma, como recordarás del cálculo.
Ahora, podríamos utilizar un algoritmo de optimización como el ascenso del gradiente para maximizar esta función de probabilidad logarítmica. De forma alternativa, vamos a volver a escribir la probabilidad logarítmica como una función de coste J que puede ser minimizada mediante el descenso del gradiente, como en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación:
Para entender mejor esta función de coste, echemos un vistazo al coste que calculamos para una instancia de entrenamiento de muestra única:
Si miramos la ecuación, podemos ver que el primer término es cero si , y que el segundo término es cero si :
Vamos a escribir un pequeño fragmento de código para crear un diagrama que ilustre el coste de la clasificación de una instancia de muestra única para diferentes valores de :
>>> def cost_1(z):
... return - np.log(sigmoid(z))
>>> def cost_0(z):
... return - np.log(1 - sigmoid(z))
>>> z = np.arange(-10, 10, 0.1)
>>> phi_z = sigmoid(z)
>>> c1 = [cost_1(x) for x in z]
>>> plt.plot(phi_z, c1, label='J(w) if y=1')
>>> c0 = [cost_0(x) for x in z]
>>> plt.plot(phi_z, c0, linestyle='--', label='J(w) if y=0')
>>> plt.ylim(0.0, 5.1)
>>> plt.xlim([0, 1])
>>> plt.xlabel('$\phi$(z)')
>>> plt.ylabel('J(w)')
>>> plt.legend(loc='best')
>>> plt.show()
El diagrama resultante muestra la activación sigmoide en el eje x en el rango de 0 a 1 (las entradas en la función sigmoide eran valores z en el rango de -10 a 10) y el coste logístico asociado en el eje y:
Podemos ver que el coste se acerca a 0 (línea continua) si predecimos correctamente que una muestra pertenece a la clase 1. De forma similar, podemos ver en el eje y que el coste también se acerca a 0 si predecimos correctamente (línea discontinua). Sin embargo, si la predicción es errónea, el coste se dirige hacia el infinito. La conclusión principal es que penalizamos las predicciones erróneas con un coste cada vez mayor.
Convertir una implementación Adaline en un algoritmo para regresión logística
Si debemos implementar nosotros mismos una regresión logística, sencillamente podemos sustituir la función de coste J en nuestra implementación Adaline del Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación con la nueva función de coste:
Utilizamos este proceso para calcular el coste de clasificar todas las muestras de entrenamiento por época. Además, tenemos que intercambiar la función de activación lineal por la activación sigmoide y cambiar la función umbral para devolver etiquetas de clase 0 y 1 en lugar de -1 y 1. Si realizamos estos tres cambios en el código de Adaline, conseguiremos una implementación de regresión logística que funciona, como se muestra a continuación:
class LogisticRegressionGD(object):
"""Logistic Regression Classifier using gradient descent.
Parameters
------------
eta : float
Learning rate (between 0.0 and 1.0)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight
initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
cost_ : list
Sum-of-squares cost function value in each epoch.
"""
def __init__(self, eta=0.05, n_iter=100, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self, X, y):
""" Fit training data.
Parameters
----------
X : {array-like}, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of
samples and
n_features is the number of features.
y : array-like, shape = [n_samples]
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01,
size=1 + X.shape[1])
self.cost_ = []
for i in range(self.n_iter):
net_input = self.net_input(X)
output = self.activation(net_input)
errors = (y - output)
self.w_[1:] += self.eta * X.T.dot(errors)
self.w_[0] += self.eta * errors.sum()
# note that we compute the logistic `cost` now
# instead of the sum of squared errors cost
cost = (-y.dot(np.log(output)) -
((1 - y).dot(np.log(1 - output))))
self.cost_.append(cost)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_[1:]) + self.w_[0]
def activation(self, z):
"""Compute logistic sigmoid activation"""
return 1. / (1. + np.exp(-np.clip(z, -250, 250)))
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.net_input(X) >= 0.0, 1, 0)
# equivalente a:
# return np.where(self.activation(self.net_input(X))
# >= 0.5, 1, 0)
Cuando configuramos un modelo de regresión logística, debemos tener en cuenta que este solo funciona para tareas de clasificación binaria. Así, vamos a considerar solo las flores Iris-setosa e Iris-versicolor (clases 0 y 1) y a comprobar que nuestra implementación de regresión logística funciona:
>>> X_train_01_subset = X_train[(y_train == 0) | (y_train == 1)]
>>> y_train_01_subset = y_train[(y_train == 0) | (y_train == 1)]
>>> lrgd = LogisticRegressionGD(eta=0.05,
... n_iter=1000,
... random_state=1)
>>> lrgd.fit(X_train_01_subset,
... y_train_01_subset)The
>>> plot_decision_regions(X=X_train_01_subset,
... y=y_train_01_subset,
... classifier=lrgd)
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
La gráfica de la región de decisión resultante tiene el aspecto siguiente:
El algoritmo de aprendizaje del descenso del gradiente para regresión logísticaCon el cálculo, podemos mostrar que la actualización de peso en regresión logística mediante el descenso del gradiente es igual a la ecuación que utilizamos en Adaline en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación. Sin embargo, debes tener en cuenta que la siguiente derivación de la regla de aprendizaje del descenso del gradiente va destinada a aquellos lectores que estén interesados en los conceptos matemáticos que hay detrás de la regla de aprendizaje del descenso del gradiente para regresión logística. No es esencial para seguir con el resto de este capítulo.Empezaremos calculando la derivada parcial de la función de probabilidad logarítmica con respecto al peso j:Antes de continuar, calcularemos también la derivada parcial de la función sigmoide: |
Ahora, podemos volver a sustituir en nuestra primera ecuación para obtener lo siguiente:Recuerda que el objetivo es encontrar los pesos que maximicen la probabilidad logarítmica, por lo que llevamos a cabo la actualización para cada peso del modo siguiente:Como actualizamos todos los pesos simultáneamente, podemos escribir la regla de actualización general así:Definimos así:Como maximizar la probabilidad logarítmica es igual que minimizar la función de coste J que definimos anteriormente, podemos escribir la regla de actualización del descenso del gradiente del siguiente modo:Esto es igual que la regla del descenso del gradiente para Adaline en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación. |
Entrenar un modelo de regresión logística con scikit-learn
En la sección anterior, acabamos de ver algunos ejercicios de matemáticas y de código útiles, que nos han ayudado a ilustrar las diferencias conceptuales entre Adaline y la regresión logística. A continuación, aprenderemos cómo utilizar la implementación más optimizada de regresión logística de scikit-learn, que también soporta ajustes multiclase fuera de la librería (por defecto, OvR). En el siguiente código de ejemplo, utilizaremos la clase sklearn.linear_model.LogisticRegression, así como el ya conocido método fit, para entrenar el modelo en las tres clases en el conjunto de datos de entrenamiento de flores normalizado:
>>> from sklearn.linear_model import LogisticRegression
>>> lr = LogisticRegression(C=100.0, random_state=1)
>>> lr.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined,
... classifier=lr,
... test_idx=range(105, 150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
Después de ajustar el modelo en los datos de entrenamiento, hemos mostrado gráficamente las regiones de decisión, las muestras de entrenamiento y las muestras de prueba, como puedes ver en la siguiente imagen:
Si piensas en el código anterior que utilizamos para entrenar el modelo LogisticRegression, debes estar preguntándote: «¿Qué es este parámetro misterioso C?». Trataremos este parámetro en la siguiente subsección donde, en primer lugar, introduciremos los conceptos de sobreajuste y regularización. Sin embargo, antes de pasar a estos temas, vamos a acabar nuestra discusión sobre las probabilidades de pertenencia a una clase.
La probabilidad de que los ejemplos de entrenamiento pertenezcan a una determinada clase puede ser calculada con el método predict_proba. Por ejemplo, podemos predecir las probabilidades de las tres primeras muestras en la prueba como sigue:
>>> lr.predict_proba(X_test_std[:3, :])
Este fragmento de código devuelve la matriz siguiente:
array([[ 3.20136878e-08, 1.46953648e-01, 8.53046320e-01],
[ 8.34428069e-01, 1.65571931e-01, 4.57896429e-12],
[ 8.49182775e-01, 1.50817225e-01, 4.65678779e-13]])
La primera fila corresponde a las probabilidades de pertenencia a una clase de la primera flor, la segunda fila corresponde a las probabilidades de pertenencia a una clase de la tercera flor, etc. Observa que las columnas suman todas más de uno, como esperábamos (puedes confirmar este hecho ejecutando lr.predict_proba(X_test_std[:3, :]).sum(axis=1)). El valor más alto de la primera fila es aproximadamente 0.853, lo cual significa que la primera muestra pertenece a la clase tres (Iris-virginica) con una predicción de la probabilidad del 85.7 %. Así, como ya habrás observado, podemos obtener las etiquetas de clase predichas identificando la columna más grande de cada fila; por ejemplo, mediante la función argmax de NumPy:
>>> lr.predict_proba(X_test_std[:3, :]).argmax(axis=1)
Los índices de clase devueltos se muestran a continuación (estos corresponden a Iris-virginica, Iris-setosa e Iris-setosa):
array([2, 0, 0])
Las etiquetas de clase que obtuvimos a partir de las probabilidades condicionales anteriores son, evidentemente, solo un enfoque manual para llamar directamente al método predict, que podemos verificar de la siguiente manera:
>>> lr.predict(X_test_std[:3, :])
array([2, 0, 0])
Por último, una advertencia si quieres predecir la etiqueta de clase de una única muestra de flor: scikit-learn espera una matriz bidimensional como entrada de datos; por tanto, debemos convertir primero una fila única a un formato de este tipo. Una manera de convertir una entrada de fila única en una matriz de datos bidimensional es utilizando el método reshape de NumPy para añadir una nueva dimensión, como se demuestra a continuación:
>>> lr.predict(X_test_std[0, :].reshape(1, -1))
array([2])
Abordar el sobreajuste con la regularización
El sobreajuste es un problema común en el aprendizaje automático, donde un modelo funciona bien en el entrenamiento de datos pero no generaliza bien con los datos no vistos (datos de prueba). Si un modelo sufre una situación de sobreajuste, también decimos que el modelo tiene una alta varianza, causada quizás por tener demasiados parámetros que conducen a un modelo demasiado complejo dados los datos subyacentes. De forma parecida, nuestro modelo también puede sufrir una situación de subajuste o underfitting (un sesgo elevado), que significa que nuestro modelo no es suficientemente complejo como para capturar correctamente el patrón en los datos de entrenamiento y, por lo tanto, sufre por el bajo rendimiento en los datos no vistos.
Aunque hasta ahora solo hemos encontrado modelos lineales para clasificación, el problema del sobreajuste y el subajuste se puede ilustrar mejor mediante la comparación de un límite de decisión lineal con otros límites de decisión no lineales y más complejos, como se muestra en la siguiente imagen:
La varianza mide la consistencia (o variabilidad) de la predicción del modelo para una instancia de muestra en particular en el caso de tener que entrenar el modelo varias veces, por ejemplo en diferentes subconjuntos del conjunto de datos de entrenamiento. Podemos decir que el modelo es sensible a la aleatoriedad en los datos de entrenamiento. Al contrario, el sesgo mide cómo estarían de lejos las predicciones de los valores correctos si volviéramos a crear el modelo varias veces en distintos conjuntos de datos de entrenamiento; el sesgo es la medida del error sistemático que no procede de la aleatoriedad. |
Una manera de encontrar una buena compensación entre el sesgo y la varianza es afinar la complejidad del modelo mediante la regularización. La regularización es un método muy útil para manejar la colinealidad (alta correlación entre características), filtra el ruido de los datos y puede prevenir el sobreajuste. El concepto que hay detrás de la regularización es presentar información adicional (sesgo) para penalizar valores (peso) de parámetros extremos. La forma más común de regularización también se denomina regularización L2 (conocida a veces como contracción L2 o penalización de pesos), que puede escribirse de la siguiente forma:
Aquí, también se denomina parámetro de regularización.
La regularización es otra de las razones por las que el escalado de características como la normalización es importante. Para que la regularización funcione adecuadamente, debemos asegurarnos de que todas nuestras características se encuentran en escalas comparables. |
La función de coste para la regresión logística se puede regularizar añadiendo un sencillo término de regularización, que contraerá los pesos durante el entrenamiento del modelo:
Mediante el parámetro de regularización , podemos controlar el ajuste de los datos de entrenamiento manteniendo pequeños los pesos. Si aumentamos el valor de , aumentamos la fuerza de regularización.
El parámetro C implementado para la clase LogisticRegression en scikit-learn procede de una convención entre las máquinas de vectores de soporte, tema que será tratado en la siguiente sección. El término C está directamente relacionado con el parámetro de regularización , que es su inverso. En consecuencia, reducir el valor del parámetro de regularización inverso C significa que estamos incrementando la fuerza de regularización, hecho que podemos visualizar mostrando gráficamente la ruta de regularización L2 para los dos coeficientes de peso:
>>> weights, params = [], []
>>> for c in np.arange(-5, 5):
... lr = LogisticRegression(C=10.**c, random_state=1)
... lr.fit(X_train_std, y_train)
... weights.append(lr.coef_[1])
... params.append(10.**c)
>>> weights = np.array(weights)
>>> plt.plot(params, weights[:, 0],
... label='petal length')
>>> plt.plot(params, weights[:, 1], linestyle='--',
... label='petal width')
>>> plt.ylabel('weight coefficient')
>>> plt.xlabel('C')
>>> plt.legend(loc='upper left')
>>> plt.xscale('log')
>>> plt.show()
Si ejecutamos este código, ajustaremos diez modelos de regresión logística con distintos valores para el parámetro de regularización inverso C. A efectos de ilustración, solo hemos recogido los coeficientes de peso de la clase 1 (en este caso, la segunda clase en el conjunto de datos, Iris-versicolor) frente a todos los clasificadores –recuerda que estamos utilizando la técnica OvR para clasificación multiclase–.
Como podemos ver en el diagrama resultante, los coeficientes de peso se contraen si reducimos el parámetro C, es decir, si aumentamos la fuerza de regularización:
Como un tratamiento más profundo de los algoritmos de clasificación individual supera el objetivo de este libro, recomendamos fervientemente Logistic Regression: From Introductory to Advanced Concepts and Applications, Dr. Scott Menard's, Sage Publications, 2009, para aquellos lectores que deseen aprender más acerca de la regresión logística. |
Margen de clasificación máximo con máquinas de vectores de soporte
Otro potente algoritmo de aprendizaje muy utilizado es la máquina de vectores de soporte (SVM, del inglés Support Vector Machine), que puede considerarse una extensión del perceptrón. Con el algoritmo del perceptrón, minimizamos errores de clasificación. Sin embargo, con las SVM nuestro objetivo de optimización es maximizar el margen. El margen se define como la distancia entre el hiperplano de separación (límite de decisión) y las muestras de entrenamiento que están más cerca de ese hiperplano, que también se denominan vectores de soporte. Así lo mostramos en la siguiente imagen:
Margen máximo de intuición
El razonamiento que hay detrás de tener límites de decisión con amplios márgenes es que estos tienden a tener más bajo error de generalización allí donde los modelos con márgenes pequeños son más propensos al sobreajuste. Para hacernos una idea de la maximización del margen, echaremos un vistazo a aquellos hiperplanos positivos y negativos que son paralelos al límite de decisión, que puede expresarse así:
Si restamos las dos ecuaciones lineales (1) y (2) entre ellas, obtenemos:
Podemos normalizar esta ecuación mediante la longitud del vector w, que se define del siguiente modo:
De este modo, llegamos a la siguiente ecuación:
La parte izquierda de la ecuación anterior puede ser interpretada como la distancia entre el hiperplano positivo y el negativo, también denominada margen, que queremos maximizar.
Ahora, la función objetiva de la SVM pasa a ser la maximización de este margen, maximizando bajo la restricción de que las muestras están clasificadas correctamente. Puede escribirse así:
En este caso, N es el número de muestra de nuestro conjunto de datos.
Estas dos ecuaciones dicen básicamente que todas las muestras negativas deben caer en un lado del hiperplano negativo, mientras que todas las muestras positivas deben caer detrás del hiperplano positivo. Puede escribirse de manera compacta del siguiente modo:
En la práctica, sin embargo, es más sencillo minimizar el término recíproco , que se puede resolver mediante programación cuadrática. Ahora bien, como el tratamiento más detallado de la programación cuadrática queda fuera del objetivo de este libro, puedes aprender más sobre las máquinas de vectores de soporte en The Nature of Statistical Learning Theory, Springer Science+Business Media, Vladimir Vapnik (2000), o bien en la excelente explicación de Chris J.C. Burges en A Tutorial on Support Vector Machines for Pattern Recognition (Data Mining and Knowledge Discovery, 2(2): 121-167, 1998).
Tratar un caso separable no lineal con variables flexibles
Aunque no queremos llegar mucho más lejos en los conceptos matemáticos que se esconden detrás del margen máximo de clasificación, sí que mencionaremos brevemente la variable flexible , que presentó Vladimir Vapnik en 1995 y dio lugar a la denominada clasificación de margen blando. La motivación para introducir la variable flexible es que las restricciones lineales deben ser relajadas para los datos separables no lineales para permitir la convergencia de la optimización cuando existen errores de clasificación, bajo la penalización de coste apropiada.
La variable flexible de valores positivos simplemente se añade a las restricciones lineales:
En este caso, N es el número de muestras en nuestro conjunto de datos. Así, el nuevo objetivo que se debe minimizar (sujeto a las restricciones) pasa a ser:
Mediante la variable C, podemos controlar la penalización por error de clasificación. Si C cuenta con valores amplios se producirán amplias penalizaciones de errores, mientras que si elegimos para C valores más pequeños seremos menos estrictos con los errores de clasificación. También podemos utilizar el parámetro C para controlar la anchura del margen y, así, afinar la compensación entre el sesgo y la varianza, como se muestra en la siguiente imagen:
Este concepto está relacionado con la regularización, que tratamos en la sección anterior cuando hablamos de que la regresión regularizada, al reducir el valor de C, aumenta el sesgo y disminuye la varianza del modelo.
Ahora que hemos aprendido los conceptos básicos de las SVM lineales, vamos a entrenar un modelo de SVM para clasificar las distintas flores en nuestro conjunto de datos Iris:
>>> from sklearn.svm import SVC
>>> svm = SVC(kernel='linear', C=1.0, random_state=1)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined,
... classifier=svm,
... test_idx=range(105, 150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
Las tres regiones de decisión de la SVM, que mostramos después de entrenar el clasificador en el conjunto de datos Iris mediante la ejecución del código de ejemplo, se muestran en el siguiente diagrama:
Regresión logística frente a las máquinas de vectores de soporteEn las tareas de clasificación prácticas, la regresión logística lineal y las SVM lineales a menudo proporcionan resultados muy parecidos. La regresión logística intenta maximizar las probabilidades condicionales de los datos de entrenamiento, y los hace más propensos a valores extremos o outliers que las SVM, que tienen en cuenta sobre todo los puntos más cercanos al límite de decisión (vectores de soporte). Por otro lado, la regresión logística tiene la ventaja de ser un modelo más simple y de poder implementarse más fácilmente. Además, los modelos de regresión logística pueden ser actualizados con facilidad, lo cual es un factor atractivo si se trabaja con transmisión de datos. |
Implementaciones alternativas en scikit-learn
La librería Perceptron y las clases LogisticRegression de scikit-learn, que hemos utilizado en la sección anterior, hacen uso de la librería LIBLINEAR, que es una librería C/C++ altamente optimizada desarrollada en la National Taiwan University (http://www.csie.ntu.edu.tw/~cjlin/liblinear/). De forma parecida, la clase SVC, que utilizamos para entrenar un SVM, hace uso de LIBSVM, que es una librería C/C++ equivalente especializada para SVM (http://www.csie.ntu.edu.tw/~cjlin/libsvm/).
La ventaja de utilizar LIBLINEAR y LIBSVM sobre implementaciones nativas de Python es que permiten un entrenamiento extremadamente rápido de grandes cantidades de clasificadores lineales. Sin embargo, a veces nuestros conjuntos de datos son demasiado grandes para encajarlos en las memorias de los ordenadores. Por esta razón, scikit-learn también ofrece implementaciones alternativas mediante la clase SGDClassifier, que también soporta aprendizaje online a través del método partial_fit. El concepto que se esconde detrás de la clase SGDClassifier es similar al algoritmo de gradiente estocástico que implementamos en el Capítulo 2, Entrenar algoritmos simples de aprendizaje automático para clasificación para Adaline. Podríamos inicializar la versión del descenso del gradiente estocástico, una regresión logística y una máquina de vectores de soporte con parámetros predeterminados de la siguiente forma:
>>> from sklearn.linear_model import SGDClassifier
>>> ppn = SGDClassifier(loss='perceptron')
>>> lr = SGDClassifier(loss='log')
>>> svm = SGDClassifier(loss='hinge')
Resolver problemas no lineales con una SVM kernelizada
Otra razón por la cual las SVM gozan de gran popularidad entre los que trabajan con el aprendizaje automático es que pueden ser fácilmente kernelizadas para resolver problemas de clasificación no lineal. Antes de tratar el concepto principal que se esconde detrás de una SVM kernelizada, vamos primero a crear un conjunto de datos de muestra para ver qué aspecto tendría un problema de clasificación no lineal.
Métodos kernel para datos inseparables lineales
Con el siguiente código, crearemos un sencillo conjunto de datos que tiene la forma de una puerta XOR mediante la función logical_or de NumPy, donde se asignarán 100 muestras a la etiqueta de clase 1 y otras 100 a la etiqueta de clase -1:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> np.random.seed(1)
>>> X_xor = np.random.randn(200, 2)
>>> y_xor = np.logical_xor(X_xor[:, 0] > 0,
... X_xor[:, 1] > 0)
>>> y_xor = np.where(y_xor, 1, -1)
>>> plt.scatter(X_xor[y_xor == 1, 0],
... X_xor[y_xor == 1, 1],
... c='b', marker='x',
... label='1')
>>> plt.scatter(X_xor[y_xor == -1, 0],
... X_xor[y_xor == -1, 1],
... c='r',
... marker='s',
... label='-1')
>>> plt.xlim([-3, 3])
>>> plt.ylim([-3, 3])
>>> plt.legend(loc='best')
>>> plt.show()
Después de ejecutar el código, tendremos un conjunto de datos XOR con ruido aleatorio, como se muestra en la siguiente imagen:
Evidentemente, no podríamos separar correctamente las muestras de las clases positivas y negativas utilizando un hiperplano lineal como límite de decisión a través de la regresión logística lineal o del modelo de SVM lineal que tratamos en secciones anteriores.
La idea fundamental que hay detrás de los métodos kernel para tratar datos inseparables lineales como estos es crear combinaciones no lineales de las características originales para proyectarlas hacia un espacio de dimensiones mayores, mediante una función de mapeo , donde pasen a ser separables lineales. Como se muestra en la siguiente imagen, podemos transformar, mediante la siguiente proyección, un conjunto de datos bidimensional en un nuevo espacio de características tridimensional donde las clases sean separables:
Esto nos permite separar las dos clases que aparecen en el gráfico mediante un hiperplano lineal que se convierte en un límite de decisión no lineal si lo volvemos a proyectar en el espacio de características original:
El truco de kernel para encontrar hiperplanos separados en un espacio de mayor dimensionalidad
Para resolver un problema no lineal utilizando una SVM, debemos transformar los datos de entrenamiento en un espacio de características de mayor dimensionalidad mediante una función de mapeo y entrenar un modelo de SVM lineal para clasificar los datos en este nuevo espacio de características. Después, podemos utilizar la misma función de mapeo para transformar nuevos datos no vistos y clasificarlos mediante el modelo de SVM lineal.
Sin embargo, un problema con este enfoque de mapeo es que la construcción de nuevas características es computacionalmente muy costosa, especialmente si tratamos con datos de mayor dimensionalidad. Y aquí es donde el denominado truco de kernel entra en juego. No entraremos mucho en detalle sobre cómo resolver la tarea de programación cuadrática para entrenar un SVM, ya que a la práctica todo cuanto necesitamos es sustituir el producto escalar por . Con el fin de ahorrarnos el costoso paso de calcular este producto escalar entre dos puntos explícitamente, definimos la denominada función kernel: .
Uno de los kernels más ampliamente utilizados es la Función de base radial (RBF), también denominada kernel Gaussiana:
Habitualmente se simplifica como:
En este caso, es un parámetro libre que debe ser optimizado.
Más o menos, el término kernel puede ser interpretado como una función de similitud entre un par de muestras. El signo menos invierte la medida de distancia de una puntuación de similitud y, debido al término exponencial, la puntuación de similitud resultante caerá en un rango entre 1 (para muestras exactamente similares) y 0 (para muestras muy diferentes).
Ahora que ya hemos definido a grandes rasgos cuanto hay detrás del truco de kernel, podemos entrenar una SVM kernelizada que sea capaz de dibujar un límite de decisión no lineal que separe correctamente los datos XOR. En este caso, simplemente utilizamos la clase SVC de scikit-learn que importamos anteriormente y sustituimos el parámetro kernel='linear' por kernel='rbf':
>>> svm = SVC(kernel='rbf', random_state=1, gamma=0.10, C=10.0)
>>> svm.fit(X_xor, y_xor)
>>> plot_decision_regions(X_xor, y_xor, classifier=svm)
>>> plt.legend(loc='upper left')
>>> plt.show()
Como podemos ver en el diagrama resultante, la SVM kernelizada separa los datos XOR relativamente bien:
El parámetro , que ajustamos en gamma=0.1, se puede entender como un parámetro de corte para la esfera Gaussiana. Si aumentamos el valor de , aumentamos la influencia o alcance de las muestras de entrenamiento, lo cual nos lleva a un límite de decisión más ajustado y lleno de baches. Para conseguir una intuición mejor para , vamos a aplicar una SVM kernelizada de RBF a nuestro conjunto de datos de flor Iris:
>>> svm = SVC(kernel='rbf', random_state=1, gamma=0.2, C=1.0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined, classifier=svm,
... test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
Como hemos elegido un valor para relativamente pequeño, el límite de decisión resultante del modelo SVM kernelizado RBF será relativamente suave, como se muestra en la siguiente figura:
Seguidamente, aumentaremos el valor de y observaremos el efecto en el límite de decisión:
>>> svm = SVC(kernel='rbf', random_state=1, gamma=100.0, C=1.0)
>>> svm.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std,
... y_combined, classifier=svm,
... test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
En el gráfico resultante podemos ver que el límite de decisión alrededor de las clases 0 y 1 está mucho más apretado si utilizamos un valor de relativamente grande:
Aunque el modelo ajusta muy bien el conjunto de datos de entrenamiento, dicho clasificador probablemente tendrá un error de generalización elevado sobre los datos no vistos. Esto demuestra que el parámetro también juega un papel importante en el control del sobreajuste.
Aprendizaje basado en árboles de decisión
Los árboles de decisión son atractivos modelos si nos preocupamos de la interpretabilidad. Como su nombre sugiere, podemos pensar en este modelo como en una descomposición de nuestros datos mediante la toma de decisiones basada en la formulación de una serie de preguntas.
Vamos a considerar el siguiente ejemplo en el cual utilizamos un árbol de decisión para decidir sobre la realización de una actividad en un día en concreto:
Si nos basamos en las características de nuestro conjunto de datos de entrenamiento, el modelo de árbol de decisión aprende una serie de preguntas para deducir las etiquetas de clase de las muestras. Aunque la imagen anterior muestra un concepto de árbol de decisión basado en variables categóricas, este mismo concepto se puede aplicar si nuestras características son números reales, como en el conjunto de datos Iris. Por ejemplo, podríamos simplemente definir un valor de corte a lo largo del eje de características anchura del sépalo y formular una pregunta binaria del tipo: «¿La anchura del sépalo es ≥ 2.8 cm?».
Utilizando el algoritmo de decisión, empezamos en la raíz del árbol y dividimos los datos en la característica que resulta en la mayor Ganancia de la información (IG, del inglés Information Gain), que explicaremos con más detalle en la siguiente sección. En un proceso iterativo, podemos repetir este procedimiento de división en cada nodo hijo hasta que las hojas sean puras. Esto significa que las muestras de cada nodo pertenecen todas a la misma clase. A la práctica, esto puede producir un árbol muy profundo con muchos nodos, que puede provocar fácilmente sobreajuste. Por lo tanto, una buena opción es podar el árbol ajustando un límite para su profundidad máxima.
Maximizar la ganancia de información: sacar el mayor partido de tu inversión
Con el fin de dividir los nodos en las características más informativas, debemos definir una función objetivo que deseamos optimizar mediante el algoritmo de aprendizaje del árbol. En este caso, nuestra función objetivo es maximizar la ganancia de información en cada división, que definimos de la siguiente forma:
En este caso, f es la característica para realizar la división; y son el conjunto de datos del nodo padre y del nodo hijo j; I es nuestra medida de impureza; es el número total de muestras en el nodo padre; y es el número de muestras en el nodo hijo j. Como podemos ver, la ganancia de información es simplemente la diferencia entre la impureza del nodo padre y la suma de las impurezas del nodo hijo: cuanto menor es la impureza de los nodos hijos, mayor es la ganancia de información. Sin embargo, por simplicidad y para reducir el espacio de búsqueda combinatoria, la mayoría de las librerías (incluida scikit-learn) implementan árboles de decisión binarios. Esto significa que cada nodo padre se divide en dos nodos hijos, y :
Ahora, las tres medidas de impurezas o criterios de división que normalmente se utilizan en los árboles de decisión binarios son impureza de Gini (), entropía () y error de clasificación (). Vamos a empezar con la definición de entropía para todas las clases no-vacías ():
En este caso, es la proporción de las muestras que pertenecen a la clase c para un determinado nodo t. La entropía es, por tanto, 0 si todas las muestras en un nodo pertenecen a la misma clase, y la entropía es máxima si tenemos una distribución de clases uniforme. Por ejemplo, en un ajuste de clases binario, la entropía es 0 si o . Si las clases están distribuidas uniformemente con y , la entropía es 1. Así, podemos decir que los criterios de la entropía intentan maximizar la información mutua en el árbol.
De forma intuitiva, la impureza de Gini se puede entender como un criterio para minimizar la probabilidad de clasificación errónea:
De forma similar a la entropía, la impureza de Gini es máxima si las clases están perfectamente mezcladas; por ejemplo, en un ajuste de clase binaria ():
Sin embargo, a la práctica, tanto la impureza de Gini como la entropía producen normalmente resultados muy similares, y a menudo no vale la pena perder el tiempo evaluando los árboles mediante diferentes criterios de impureza en lugar de experimentar con distintos cortes de poda.
Otra medida de impureza es el error de clasificación:
Se trata de un criterio muy útil para podar aunque no es recomendable para hacer crecer un árbol de decisión, puesto que es menos sensible a los cambios en las probabilidades de clase de los nodos. Podemos demostrarlo observando los dos posibles casos de división mostrados en la siguiente imagen:
Empezamos con un conjunto de datos en el nodo padre , que consiste en 40 muestras de clase 1 y 40 muestras de clase 2 que dividimos en dos conjuntos de datos, y . La ganancia de información mediante el error de clasificación como un criterio de división sería igual () en ambos casos, A y B:
Sin embargo, la impureza favorecería la división en el caso B () por encima del caso A () que, de hecho, es más puro:
De forma parecida, el criterio de entropía también favorecería el caso B () por encima del caso A ():
Para obtener una comparación más visual de los tres criterios distintos de impureza que acabamos de tratar, vamos a mostrar gráficamente los índices de impureza para el rango de probabilidad [0, 1] para la clase 1. Ten en cuenta que también añadiremos una versión escalada de la entropía (entropía / 2) para observar que la impureza de Gini es una medida intermedia entre la entropía y el error de clasificación. El código es el siguiente:
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> def gini(p):
... return (p)*(1 - (p)) + (1 - p)*(1 - (1-p))
>>> def entropy(p):
... return - p*np.log2(p) - (1 - p)*np.log2((1 - p))
>>> def error(p):
... return 1 - np.max([p, 1 - p])
>>> x = np.arange(0.0, 1.0, 0.01)
>>> ent = [entropy(p) if p != 0 else None for p in x]
>>> sc_ent = [e*0.5 if e else None for e in ent]
>>> err = [error(i) for i in x]
>>> fig = plt.figure()
>>> ax = plt.subplot(111)
>>> for i, lab, ls, c, in zip([ent, sc_ent, gini(x), err],
... ['Entropy', 'Entropy (scaled)',
... 'Gini Impurity',
... 'Misclassification Error'],
... ['-', '-', '--', '-.'],
... ['black', 'lightgray',
... 'red', 'green', 'cyan']):
... line = ax.plot(x, i, label=lab,
... linestyle=ls, lw=2, color=c)
>>> ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.15),
... ncol=5, fancybox=True, shadow=False)
>>> ax.axhline(y=0.5, linewidth=1, color='k', linestyle='--')
>>> ax.axhline(y=1.0, linewidth=1, color='k', linestyle='--')
>>> plt.ylim([0, 1.1])
>>> plt.xlabel('p(i=1)')
>>> plt.ylabel('Impurity Index')
>>> plt.show()
El gráfico obtenido a partir del código anterior es el siguiente:
Crear un árbol de decisión
Los árboles de decisión pueden generar límites de decisión complejos dividiendo el espacio de características en rectángulos. Sin embargo, debemos ir con cuidado puesto que cuanto más profundo es el árbol de decisión, más complejo es el límite de decisión, el cual puede caer fácilmente en el sobreajuste. Con scikit-learn, vamos a entrenar un árbol de decisión con una profundidad máxima de 3, utilizando la entropía como criterio para la impureza. Aunque podríamos desear un escalado de características por motivos de visualización, ten en cuenta que dicho escalado de características no es obligatorio para los algoritmos de árboles de decisión. El código es el siguiente:
>>> from sklearn.tree import DecisionTreeClassifier
>>> tree = DecisionTreeClassifier(criterion='gini',
... max_depth=4,
... random_state=1)
>>> tree.fit(X_train, y_train)
>>> X_combined = np.vstack((X_train, X_test))
>>> y_combined = np.hstack((y_train, y_test))
>>> plot_decision_regions(X_combined,
... y_combined,
... classifier=tree,
... test_idx=range(105, 150))
>>> plt.xlabel('petal length [cm]')
>>> plt.ylabel('petal width [cm]')
>>> plt.legend(loc='upper left')
>>> plt.show()
Después de ejecutar el código de ejemplo, obtenemos los límites de decisión típicos de eje paralelo del árbol de decisión:
Una buena característica de scikit-learn es que después del entrenamiento nos permite exportar el árbol de decisión como un archivo .dot, que podemos visualizar con el programa GraphViz, por ejemplo.
Este programa está disponible de forma gratuita en http://www.graphviz.org y es compatible con Linux, Windows y macOS. Además de GraphViz, utilizaremos una librería de Python denominada pydotplus, que tiene funciones similares a GraphViz y nos permite convertir archivos de datos .dot en un archivo de imagen de árbol de decisión. Después de instalar GraphViz (siguiendo las instrucciones que encontrarás en http://www.graphviz.org/Download.php), puedes instalar directamente pydotplus mediante el instalador pip, por ejemplo, ejecutando el siguiente comando en tu terminal:
> pip3 install pydotplus
Ten en cuenta que en algunos sistemas deberás instalar los requisitos pydotplus manualmente ejecutando los siguientes comandos:pip3 install graphvizpip3 install pyparsing |
El siguiente código creará una imagen de nuestro árbol de decisión en formato PNG en nuestro directorio local:
>>> from pydotplus import graph_from_dot_data
>>> from sklearn.tree import export_graphviz
>>> dot_data = export_graphviz(tree,
... filled=True,
... rounded=True,
... class_names=['Setosa',
... 'Versicolor',
... 'Virginica'],
... feature_names=['petal length',
... 'petal width'],
... out_file=None)
>>> graph = graph_from_dot_data(dot_data)
>>> graph.write_png('tree.png')
Mediante la opción out_file=None, asignamos directamente los datos dot a una variable dot_data, en lugar de escribir un archivo tree.dot intermedio en el disco. Los argumentos para filled, rounded, class_names y feature_names son opcionales pero hacen que el archivo de imagen resultante sea más atractivo visualmente al añadir color, redondear los bordes del cuadro, mostrar el nombre de la mayoría de las etiquetas de clase para cada nodo y mostrar los nombres de las características en el criterio de división. Estos ajustes dan como resultado la siguiente imagen de árbol de decisión:
Si observamos la imagen del árbol de decisión, podemos trazar fácilmente las divisiones que el árbol de decisión ha determinado a partir de nuestro conjunto de datos de entrenamiento. Hemos empezado con 105 muestras en la raíz y las hemos dividido en dos nodos hijo con 35 y 70 muestras, mediante el corte anchura del pétalo ≤ 0.75 cm. Tras la primera división, podemos ver que el nodo hijo de la izquierda ya es puro y solo contiene muestras de la clase Iris-setosa (impureza de Gini = 0). Las otras divisiones a la derecha se utilizan para separar las muestras de la clase Iris-versicolor y Iris-virginica class.
Si observamos este árbol, y el gráfico de la región de decisión del árbol, vemos que el árbol de decisión ha hecho un buen trabajo separando las clases de flor. Desafortunadamente, por ahora scikit-learn no implementa ninguna funcionalidad para podar posteriormente de forma manual un árbol de decisión. Sin embargo, podríamos retomar nuestro ejemplo anterior, cambiar la max_depth de nuestro árbol de decisión a 3 y compararlo con nuestro modelo actual. Pero dejaremos este ejercicio para aquellos lectores más interesados.
Combinar árboles de decisión múltiples mediante bosques aleatorios
Los bosques aleatorios, o random forests, han ganado una gran popularidad entre las aplicaciones de aprendizaje automático durante la última década debido a su excelente rendimiento de clasificación, su escalabilidad y su facilidad de uso. De forma intuitiva, un bosque aleatorio se puede considerar como un conjunto de árboles de decisión. La idea que hay detrás de un bosque aleatorio es promediar árboles de decisión múltiples (profundos) que individualmente sufren una elevada varianza para crear un modelo más robusto que tenga un mejor rendimiento de generalización y sea menos susceptible al sobreajuste. El algoritmo del bosque aleatorio se puede resumir en cuatro sencillos pasos:
1.Dibuja una muestra bootstrap aleatoria de tamaño n (elige al azar muestras n del conjunto de entrenamiento con reemplazo).
2.Crea un árbol de decisión a partir de la muestra bootstrap. Para cada nodo:
a.Selecciona al azar características d sin reemplazo.
b.Divide el nodo utilizando la característica que proporciona la mejor división según la función objetivo; por ejemplo, maximizando la ganancia de información.
3.Repite los pasos 1-2 k veces.
4.Añade la predicción para cada árbol para asignar la etiqueta de clase por mayoría de votos. La mayoría de votos será tratada con más detalle en el Capítulo 7, Combinar diferentes modelos para un aprendizaje conjunto.
Debemos tener en cuenta una ligera modificación en el paso 2 cuando estemos entrenando los árboles de decisión individuales: en lugar de evaluar todas las características para determinar la mejor división para cada nodo, solo consideraremos un subconjunto al azar de ellos.
Si no estás familiarizado con los términos de muestreo con y sin reemplazo, vamos a realizar un simple experimento mental. Supongamos que jugamos a un juego de lotería donde extraemos al azar números de una urna. Empezamos con una urna que contiene cinco únicos números (0, 1, 2, 3 y 4) y sacamos exactamente un número cada vez. En el primer turno, la probabilidad de extraer un número en concreto de la urna sería de 1/5. Ahora bien, en muestreo sin reemplazo no devolvemos el número a la urna después de cada turno. En consecuencia, la probabilidad de extraer un número en concreto del conjunto de los números restantes en la siguiente ronda depende de la ronda anterior. Por ejemplo, si tenemos un conjunto de números 0, 1, 2 y 4, la oportunidad de extraer el número 0 pasa a ser de 1/4 en la siguiente ronda.Sin embargo, en el muestreo aleatorio con reemplazo siempre devolvemos el número extraído a la urna, por lo que las probabilidades de extraer un número en concreto en cada turno no cambian; podemos extraer el mismo número más de una vez. En otras palabras, en el muestreo con reemplazo las muestras (números) son independientes y tienen una covarianza de cero. Por ejemplo, los resultados de cinco rondas de extracción de números al azar serían como los siguientes:•Muestreo aleatorio sin reemplazo: 2, 1, 3, 4, 0•Muestreo aleatorio con reemplazo: 1, 3, 3, 4, 1 |
Aunque los bosques aleatorios no ofrecen el mismo nivel de interpretabilidad que los árboles de decisión, sí poseen la gran ventaja de que no debemos preocuparnos demasiado de elegir unos buenos valores de hiperparámetro. Normalmente no es necesario podar el bosque aleatorio, puesto que el modelo conjunto es bastante robusto ante el ruido de los árboles de decisión individuales. El único parámetro que debemos tener en cuenta a la práctica es el número de árboles k (paso 3) que elegimos para el bosque aleatorio. Habitualmente, cuanto más alto es el número de árboles mejor es el rendimiento del bosque aleatorio a expensas de un mayor coste computacional.
Aunque es menos común, otros hiperparámetros del clasificador de bosque aleatorio que pueden ser optimizados –mediante técnicas que trataremos en el Capítulo 5, Comprimir datos mediante la reducción de dimensionalidad– son el tamaño n de la muestra bootstrap (paso 1) y el número de características d que se elige aleatoriamente para cada división (paso 2.1). Mediante el tamaño de muestra n de la muestra bootstrap controlamos la compensación entre varianza y sesgo del bosque aleatorio.
Reducir el tamaño de la muestra bootstrap aumenta la diversidad entre los árboles individuales, puesto que la probabilidad de que una muestra de entrenamiento en concreto esté incluida en la muestra bootstrap es más baja. Así, contraer el tamaño de las muestras bootstrap aumenta la aleatoriedad del bosque aleatorio y esto puede ayudar a reducir el efecto de sobreajuste. Sin embargo, las muestras de bootstrap más pequeñas normalmente tienen como resultado un rendimiento general más bajo del bosque aleatorio, una distancia más pequeña entre el entrenamiento y el rendimiento de prueba y, sobre todo, un rendimiento de prueba más bajo. Inversamente, aumentar el tamaño de la muestra bootstrap aumenta el grado de sobreajuste. Como las muestras bootstrap, y en consecuencia los árboles de decisión individuales, se parecen más entre ellas, aprenden a ajustar el conjunto de datos de entrenamiento original más de cerca.
En la mayoría de las implementaciones, incluyendo la implementación RandomForestClassifier en scikit-learn, el tamaño de la muestra bootstrap se elige para que sea igual al número de muestras del conjunto de entrenamiento original, que normalmente proporciona una buena compensación entre el sesgo y la varianza. Para el número de características d en cada división, deseamos elegir un valor que sea más pequeño que el número total de características en el conjunto de entrenamiento. Un parámetro por defecto razonable que se utiliza en scikit-learn y otras implementaciones es , donde m es el número de características en el conjunto de entrenamiento.
No hace falta que construyamos nosotros mismos el bosque aleatorio a partir de árboles de decisión individuales, puesto que ya existe una implementación en scikit-learn que podemos utilizar:
>>> from sklearn.ensemble import RandomForestClassifier
>>> forest = RandomForestClassifier(criterion='gini',
... n_estimators=25,
... random_state=1,
... n_jobs=2)
>>> forest.fit(X_train, y_train)
>>> plot_decision_regions(X_combined, y_combined,
... classifier=forest, test_idx=range(105,150))
>>> plt.xlabel('petal length')
>>> plt.ylabel('petal width')
>>> plt.legend(loc='upper left')
>>> plt.show()
Después de ejecutar el código anterior, podemos ver las regiones de decisión formadas por el conjunto de árboles en el bosque aleatorio, como se muestra en la siguiente imagen:
Con el código anterior, hemos entrenado un bosque aleatorio a partir de 25 árboles de decisión con el parámetro n_estimators y hemos utilizado el criterio de entropía como una medida de impureza para dividir los nodos. Aunque estamos cultivando un bosque aleatorio muy pequeño a partir de un conjunto de datos de entrenamiento muy pequeño, con la idea de demostrarlo hemos utilizado el parámetro n_jobs, el cual nos permite paralelizar el entrenamiento del modelo mediante múltiples núcleos de nuestro ordenador (en este caso, dos núcleos).
K-vecinos más cercanos: un algoritmo de aprendizaje vago
El último algoritmo de aprendizaje supervisado que queremos tratar en este capítulo es el clasificador k-vecinos más cercanos (KNN, del inglés k-nearest neighbours), que resulta especialmente interesante porque es fundamentalmente distinto de los algoritmos de aprendizaje que hemos tratado hasta ahora.
El KNN es un ejemplo típico de aprendizaje vago. Se denomina vago no por su aparente simplicidad, sino porque no obtiene ninguna función discriminitiva a partir de los datos de entrenamiento, sino que en su lugar memoriza el conjunto de datos de entrenamiento.
Modelos paramétricos frente a no paramétricosLos algoritmos de aprendizaje automático se pueden agrupar en modelos paramétricos y no paramétricos. Con los modelos paramétricos, estimamos parámetros a partir de conjuntos de datos de entrenamiento para aprender una función que pueda clasificar nuevos puntos de datos sin necesidad del conjunto de datos de entrenamiento original. Los ejemplos típicos de modelos paramétricos son el perceptrón, la regresión logística y las SVM lineales. Contrariamente, los modelos no paramétricos no se pueden caracterizar por un conjunto fijo de parámetros, sino por que el número de parámetros crece con los datos de entrenamiento. Dos ejemplos de modelos no paramétricos que ya hemos visto son los árboles de decisión y bosques aleatorios y las SVM kernelizadas.El KNN pertenece a una subcategoría de modelos no paramétricos que se describe como aprendizaje basado en instancias. Los modelos asentados en el aprendizaje basado en instancias se caracterizan por memorizar el conjunto de datos de entrenamiento. El aprendizaje vago es un caso especial de aprendizaje basado en instancias que está asociado con un coste nulo (cero) durante el proceso de aprendizaje. |
El algoritmo KNN en sí mismo es bastante sencillo y se puede resumir en los siguientes pasos:
1.Elige el número de k y una medida de distancia.
2.Encuentra los k-vecinos más cercanos de la muestra que quieres clasificar.
3.Asigna la etiqueta de clase por mayoría de votos.
La siguiente imagen muestra cómo se ha asignado un nuevo punto de datos (?) a la etiqueta de clase triángulo por mayoría de votos entre sus cinco vecinos más cercanos.
Basado en la medida de distancia seleccionada, el algoritmo KNN encuentra en el con-junto de datos de entrenamiento las muestras k que están más cerca (son más similares) del punto que queremos clasificar. A continuación, la etiqueta de clase del nuevo punto de datos se determinada por mayoría de votos entre sus k vecinos más cercanos.
La principal ventaja de un enfoque basado en memoria como este es que el clasificador se adapta inmediatamente cuando recogemos nuevos datos de entrenamiento. Sin embargo, la cara oculta es que, en el peor de los casos, la complejidad computacional para la clasificación de nuevas muestras crece linealmente con el número de muestras en el conjunto de datos de entrenamiento –a menos que el conjunto de datos tenga muy pocas dimensiones (características) y el algoritmo haya sido implementado mediante estructuras de datos eficientes, como los árboles kd–. An Algorithm for Finding Best Matches in Logarithmic Expected Time, J. H. Friedman, J. L. Bentley, y R.A. Finkel, ACM transactions on mathematical software (TOMS), 3(3): 209–226, 1977. Además, no podemos descartar muestras de entrenamiento puesto que no existe ningún paso de entrenamiento involucrado. Por tanto, el espacio de almacenamiento puede llegar a ser un desafío si estamos trabajando con grandes conjuntos de datos.
Con la ejecución del siguiente código, implementaremos un modelo KNN en scikit-learn mediante una distancia euclidiana:
>>> from sklearn.neighbors import KNeighborsClassifier
>>> knn = KNeighborsClassifier(n_neighbors=5, p=2,
... metric='minkowski')
>>> knn.fit(X_train_std, y_train)
>>> plot_decision_regions(X_combined_std, y_combined,
... classifier=knn, test_idx=range(105,150))
>>> plt.xlabel('petal length [standardized]')
>>> plt.ylabel('petal width [standardized]')
>>> plt.legend(loc='upper left')
>>> plt.show()
Al especificar cinco vecinos en el modelo KNN para este conjunto de datos, obtenemos un límite de decisión relativamente suave, como se muestra en la siguiente imagen:
En caso de empate, la implementación de scikit-learn del algoritmo KNN se decantará por los vecinos que estén más cerca de la muestra. Si las distancias de los vecinos son similares, el algoritmo elegirá la etiqueta de clase que aparezca primero en el conjunto de datos de entrenamiento. |
La elección correcta de k es crucial para encontrar un buen equilibrio entre sobreajuste y subajuste. Además, debemos asegurarnos de que elegimos una métrica de distancia apropiada para las características del conjunto de datos. A menudo, una simple distancia euclidiana se utiliza para muestras de valores reales; por ejemplo, las flores de nuestro conjunto de datos Iris, que tiene características medidas en centímetros. Sin embargo, si estamos utilizando una distancia euclidiana, también es importante normalizar los datos para que cada característica contribuya de forma equitativa en la distancia. La distancia minkowski que hemos utilizado en el código anterior es simplemente una generalización de la distancia euclidiana y de Manhattan, que puede escribirse de la siguiente forma:
Esto se convierte en la distancia euclidiana si ajustamos el parámetro p=2, o en la distancia de Manhattan en p=1. Scikit-learn dispone de muchas otras métricas de distancia que pueden proporcionarse al parámetro métrico. Estas métricas pueden encontrarse en http://scikit-learn.org/stable/modules/generated/sklearn.neighbors.DistanceMetric.html.
La maldición de la dimensionalidadEs importante mencionar que KNN es muy susceptible al sobreajuste debido a la maldición de la dimensionalidad. La maldición de la dimensionalidad describe el fenómeno donde el espacio de características se vuelve cada vez más escaso para un número cada vez mayor de dimensiones de un conjunto de datos de entrenamiento de tamaño fijo. Intuitivamente, podemos pensar que en un espacio de mayores dimensiones incluso los vecinos más cercanos están demasiado lejos para ofrecer una buena estimación.Ya hemos tratado el concepto de la regularización –en la sección dedicada a la regresión logística– como una manera de evitar el sobreajuste. Sin embargo, en modelos donde la regularización no es aplicable, como en los árboles de decisión podemos usar técnicas de selección de características y reducción de dimensionalidad para ayudarnos a evitar la maldición de la dimensionalidad. Trataremos este aspecto con más detalle en el siguiente capítulo. |
Resumen
En este capítulo, has aprendido diferentes algoritmos de aprendizaje automático que se utilizan para abordar problemas lineales y no lineales. Hemos visto que los árboles de decisión son particularmente atractivos si nos preocupamos por la interpretabilidad. La regresión logística no es solo un modelo útil para el aprendizaje online mediante el descenso del gradiente estocástico, sino que también nos permite predecir la probabilidad de un evento en concreto. A pesar de que las máquinas de vectores de soporte sean potentes modelos lineales que se pueden ampliar hasta problemas no lineales mediante el truco de kernel, tienen muchos parámetros que deben ajustarse para poder realizar buenas predicciones. Por el contrario, el conjunto de métodos de los bosques aleatorios no requiere demasiados ajustes de parámetros y no se sobreajusta tan fácilmente como los árboles de decisiones, lo que los convierte en modelos atractivos para distintos dominios de problemas prácticos. El clasificador KNN ofrece un enfoque alternativo a la clasificación a través del aprendizaje vago, que nos permite realizar predicciones sin entrenar ningún modelo pero con un paso de predicción computacionalmente más costoso.
Sin embargo, incluso más importante que la elección de un algoritmo de aprendizaje adecuado es la naturaleza de los datos disponibles en nuestro conjunto de datos de entrenamiento. Ningún algoritmo va a ser capaz de realizar buenas predicciones sin características informativas y discriminatorias.
En el siguiente capítulo, trataremos temas importantes relacionados con el preprocesamiento de datos, la selección de características y la reducción de la dimensionalidad, que necesitaremos para crear modelos potentes de aprendizaje automático. Más adelante, en el Capítulo 6, Aprender las mejores prácticas para la evaluación de modelos y el ajuste de hiperparámetros, veremos cómo podemos evaluar y comparar el rendimiento de nuestros modelos y aprender útiles trucos para ajustar con precisión los diferentes algoritmos.