Vamos a ver otro algoritmo de clasificación llamado máquina de vectores soporte (SVM, Support Vector Machine).
Supongamos que tenemos los siguientes datos:
Como vemos, tenemos datos con dos clases diferentes. El objetivo es predecir a qué clase pertenecen las nuevas instancias.
La idea de SVM es crear un hiperplano que separe los datos según su clase. El problema es encontrar ese hiperplano. Por ejemplo, en nuestro caso, tenemos infinitas posibilidades para separar los datos:
¿Cuál es la mejor separación?. La solución a este problema consiste en seleccionar como clasificador óptimo el hiperplano que se encuentra más alejado de todas las observaciones de entrenamiento.
Las instancias que “soportan” el hiperplano, se conocen como vectores soporte.
En el ejemplo anterior, todas las muestras son linealmente separables. En este caso hablamos de clasificación de margen duro. Hay dos problemas principales con este tipo de clasificadores. Primero, las muestras tienen que ser linealmente separables (lo que no es habitual). Segundo, es sensible a los valores anómalos.
Para evitar estos problemas, podemos hacer un modelo más flexible. El objetivo es mantener la calle lo más ancha posible limitando las violaciones del margen (muestras que caen dentro de la calle, incluso en el lado equivocado).
Veamos el siguiente ejemplo:
En este caso, no podemos separar los datos de forma perfecta. Si aplicamos un clasificador de margen blando:
Como vemos, hay instancias que están dentro de la calle y algunas están mal clasificadas.
Cuando creamos un modelo SVM con Sklearn, podemos especificar un número de hiperparámetros. Uno de ellos es C. C controla el número y severidad de las violaciones del margen (y del hiperplano) que se toleran en el proceso de ajuste. Si C = ∞, no se permite ninguna violación del margen y por lo tanto, el resultado es un clasificador de margen duro (teniendo en cuenta que esta solución solo es posible si las clases son perfectamente separables). Cuando más se aproxima C a cero, menos se penalizan los errores y más observaciones pueden estar en el lado incorrecto del margen o incluso del hiperplano. En la práctica, su valor óptimo se identifica mediante validación cruzada.
Los modelos anteriores consigue buenos resultados cuando el límite de separación entre clases es aproximadamente lineal. Si no lo es, su capacidad decae drásticamente. Una estrategia para enfrentarse a escenarios en los que la separación de los grupos es de tipo no lineal consiste en expandir las dimensiones del espacio original.
El hecho de que los grupos no sean linealmente separables en el espacio original no significa que no lo sean en un espacio de mayores dimensiones.
Sklearn dispone de varios clasificadores SVM. Uno de ellos es LinearSVC. Vamos a probarlo con datos que son linealmente separables. Para ello, cogeremos los 100 primeros datos (las dos primeras clases) del conjunto de datos iris.
import pandas as pd import numpy as np from sklearn.svm import LinearSVC from sklearn.datasets import load_iris import matplotlib.pyplot as plt plt.rcParams['image.cmap'] = "bwr" # Configuración warnings # ============================================================================== import warnings warnings.filterwarnings('ignore')
iris = load_iris() df = pd.DataFrame(data = iris.data, columns = iris.feature_names) df['target'] = iris.target X = df.drop(columns = ['target', 'sepal length (cm)', 'sepal width (cm)'], axis = 1) X = X.iloc[:100, :] y = df['target'] y = y.iloc[:100]
figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() _ = axes.scatter(X['petal length (cm)'], X['petal width (cm)'], c = y, alpha = 1)
Como vemos, nuestros datos se pueden separar perfectamente. Usamos el modelo de Sklearn, y vemos el resultado:
svc_model = LinearSVC() svc_model.fit(X, y)
# obtain the support vectors through the decision function decision_function = svc_model.decision_function(X) # we can also calculate the decision function manually # decision_function = np.dot(X, clf.coef_[0]) + clf.intercept_[0] # The support vectors are the samples that lie within the margin # boundaries, whose size is conventionally constrained to 1 support_vector_indices = np.where(np.abs(decision_function) <= 1 + 1e-15)[0] support_vectors = X.iloc[support_vector_indices] figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() _ = axes.scatter(X['petal length (cm)'], X['petal width (cm)'], c=y, s=30) ax = plt.gca() xlim = ax.get_xlim() ylim = ax.get_ylim() xx, yy = np.meshgrid( np.linspace(xlim[0], xlim[1], 50), np.linspace(ylim[0], ylim[1], 50) ) Z = svc_model.decision_function(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) _ =axes.contour( xx, yy, Z, colors="k", levels=[-1, 0, 1], alpha=0.5, linestyles=["--", "-", "--"], ) _ = axes.scatter( support_vectors.iloc[:, 0], support_vectors.iloc[:, 1], s=100, linewidth=1, facecolors="none", edgecolors="k", )
¿Por qué nuestro clasificador funciona, aparentemente, mal? Como hemos dicho antes, uno de los hiperparámetros del modelo es C. Cuanto más pequeño sea su valor, más violaciones al margen permite. Vamos a probar con un C más grande:
svc_model = LinearSVC(C = 100) svc_model.fit(X, y)
Ahora ya no tenemos instancias mal clasificadas.
Vamos a hacer lo mismo, pero cogiendo las dos últimas clases del conjunto de datos iris. Si mostramos el gráfico, vemos que no se pueden separar los datos de forma perfecta:
Probamos primero con el hiperparámetro C por defecto (1):
svc_model = LinearSVC(C = 1) svc_model.fit(X, y)
Resultado:
En este caso, existen bastantes violaciones al margen, con lo que igual sería recomendable aumentar C. Probamos con 100:
Ahora nuestro clasificador tiene mejor pinta (aunque puede que esté sobreajustando los datos).
Como hemos visto, cuando los datos son linealmente separables (o casi) SVM se comporta bastante bien. Pero, ¿qué pasa si nuestros datos no son linealmente separables?. Por ejemplo:
En este caso, sólo tenemos una característica y no podemos separar nuestros datos con una sola línea recta. La solución es añadir una nueva característica ($x_{2} = x_{1}²$):
La pregunta es ¿cómo aumentamos la dimensión y cuál es la dimensión correcta?. Por suerte, SVM puede aplicar una técnica matemática llamada truco kernel. El truco kernel hace que sea posible conseguir el mismo resultado que si hubiésemos añadido muchas características polinomiales sin tener que añadirlas en realidad.
Existen multitud de kernels distintos, cada uno con sus propios hiperparámetros. Algunos de los más utilizados son:
Para utilizar SVM con diferentes tipos de kernels, vamos a usar SVC en lugar de LinearSVC. Vamos a probar el clasificador con el conjunto de datos moon.
import numpy as np from sklearn.svm import SVC from sklearn.datasets import make_moons import matplotlib.pyplot as plt plt.rcParams['image.cmap'] = "bwr" # Configuración warnings # ============================================================================== import warnings warnings.filterwarnings('ignore')
X, y = make_moons(n_samples = 100, noise=0.15) figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() axes.set_xlabel('X1', size = 'x-large') axes.set_ylabel('X2', size = 'x-large') _ = axes.scatter(X[:, 0], X[:, 1], c = y)
Si vemos la distribución de nuestros datos, tienen la forma de dos semicírculos intercalados. Vamos a probar primero con un kernel lineal:
svc_model = SVC(kernel="linear") svc_model.fit(X, y)
Para mostrar el resultado del clasificador y simplificar el código, creamos una función para dibujar el gráfico resultante:
def draw_model(X, y, model): figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() axes.set_xlabel('X1', size = 'x-large') axes.set_ylabel('X2', size = 'x-large') _ = axes.scatter(X[:, 0], X[:, 1], c = y) h = .02 # create a mesh to plot in x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) # Put the result into a color plot Z = Z.reshape(xx.shape) _ = axes.contourf(xx, yy, Z, cmap=plt.cm.coolwarm, alpha=0.3)
Mostramos el resultado:
draw_model(X, y, svc_model)
Vamos a probar con un kernel polinómico. En este caso tenemos 2 hiperparámetros (3, si tenemos en cuenta C): el grado del polinomio (degree) y coef0, que controla cómo de influido está el modelo por polinomios de grado alto frente a polinomios de grado bajo.
Probamos con varios valores de esos hiperparámetros:
hiper_params = np.array([ [3, 1], [3, 100], [10, 1], [10, 100] ]) figure=plt.figure(figsize = (20, 15), constrained_layout = True) for index, param in enumerate(hiper_params): svc_model = SVC(kernel="poly", degree=param[0], coef0=param[1], C = 5) svc_model.fit(X, y) axes = figure.add_subplot(2,2,index+1) title = "d = " + str(param[0]) + ", r = " + str(param[1]) + ", C = 5" axes.set_title(title, fontsize=15,pad=20,color="#003B80") draw_model(X, y, svc_model, axes)
Es el turno del kernel rbf. En este caso, sólo tenemos un hiperparámetro: $\gamma$, que controla el comportamiento del kernel. Cuando es muy pequeño, el modelo final es equivalente al obtenido con un kernel lineal, a medida que aumenta su valor, también lo hace la flexibilidad del modelo.
hiper_params = np.array([ [0.1, 0.001], [0.1, 1000], [5, 0.001], [5, 1000] ]) figure=plt.figure(figsize = (20, 15), constrained_layout = True) for index, param in enumerate(hiper_params): svc_model = SVC(kernel="rbf", gamma=param[0], C = param[1]) svc_model.fit(X, y) axes = figure.add_subplot(2,2,index+1) title = "gamma = " + str(param[0]) + ", C = " + str(param[1]) axes.set_title(title, fontsize=15,pad=20,color="#003B80") draw_model(X, y, svc_model, axes)