Las máquinas de soporte vectorial (Support Vector Machines) son un modelo muy potente para tareas de clasificación.
La idea base es encontrar un hiperplano de separación entre dos clases con margen máximo.
La potencia de esta aproximación aumenta gracias a la introducción de lo que se conoce como el truco del kernel que consiste en utilizar un espacio de representación con más dimensiones que el número de características que inicialmente tiene el conjunto de datos.
El objetivo de las Máquinas de Soporte Vectorial es crear un modelo de clasificación que dada una muestra me prediga a cuál de entre dos clases pertenece.
Clasificación binaria.
Vamos a empezar con un conjunto de datos sencillo, en dos dimensiones, y separable mediante una recta (un hiperplano en dos dimensiones)
¿Cuál es la mejor recta que separa las muestras dejando a un lado las positivas y al otro las negativas? En principio, tenemos muchas opciones.
Vamos a definir como mejor solución aquella línea que deja un mayor margen entre los dos conjuntos de datos.
Ya tenemos definido nuestro objetivo, encontrar el hiperplano de separación que deja el mayor margen entre las muestras de los dos conjuntos de datos.
Los puntos de la recta que estamos buscando cumplen la siguiente ecuación:
\[ y(x) = \theta^T x + \theta_0 = 0 \]
Cuidado
\(\theta\) no es el vector director de la recta, de hecho es un vector perpendicular a la recta (Demostración).
Los puntos de la recta que estamos buscando cumplen la siguiente ecuación:
\[ y(x) = \theta^T x + \theta_0 = 0 \]
Cuidado
\(\theta_0\) no es el punto de corte con el eje de ordenadas.
Sí que es interesante que veamos cómo calcular la distancia de la recta al origen, que viene dada por la siguiente expresión (Demostración):
\[ d = \frac{\theta^T x}{\|\theta\|} = \frac{-\theta_0}{\| \theta \|} \]
Dónde \(x\) es cualquier punto que pertenezca a la recta.
Importante
Si multiplicamos el vector \(\theta\) y el escalar \(\theta_0\) por la misma constante:
El siguiente paso es establecer un criterio para decidir si una muestra la clasificamos como positiva o negativa.
La primera idea brillante es, ya que podemos ajustar \(\theta\) y \(\theta_0\) con una constante y seguir teniendo la misma recta, lo vamos a hacer de tal modo que:
Las muestras positivas cumplan:
\[ y(x_{+}) = \theta^T x_+ + \theta_0 \ge 1 \]
Y las negativas:
\[ y(x_{-}) = \theta^T x_- + \theta_0 \le -1 \]
Vamos intentar condensar ambas expresiones en una única expresión, para ello definimos:
\[ t_n = +1 \: si \: x_n: positiva \\ t_n = -1 \: si \: x_n: negativa \]
Ahora podemos escribir:
\[ t_n(y(x_n)) = t_n(\theta^T x_n + \theta_0) \ge 1 \\ \]
O lo que es lo mismo:
\[ \boxed{t_n(\theta^T x_n + \theta_0) - 1 \ge 0} \]
Donde la igualdad se cumple justo para los puntos que están en las líneas de margen.
La anchura del margen \(a\) es (Demostración):
\[ a = \frac{\theta^T}{\| \theta \|} (v_+ - v_-) = \frac{1}{\| \theta \|} (\theta^T v_+ - \theta^T v_-) \]
\[ t_n(\theta^T x_n + \theta_0) - 1 = 0 \]
\[ v_+ \rightarrow 1(\theta^T v_+ + \theta_0) - 1 = 0 \\ \theta^T v_+ = 1 - \theta_0 \\ v_- \rightarrow -1(\theta^T v_- + \theta_0) - 1 = 0 \\ \theta^T v_- = -1 - \theta_0 \]
Finalmente:
\[ a = \frac{1}{\| \theta \|} [1 - \theta_0 - (-\theta_0 - 1)] = \frac{2}{\| \theta \|} \]
Resumiendo, la anchura que debemos maximizar es:
\[ a = \frac{2}{\| \theta \|} \]
Pero es más interesante minimizar su inversa al cuadrado:
\[ h = \frac{1}{2} \| \theta \|^2 \]
Con las restricciones (una por cada elemento del conjunto):
\[ t_n(\theta^T x_n + \theta_0) - 1 \ge 0 \]
Que es un problema de optimización.
Para solucionarlo vamos a hacer uso de los multiplicadores de Lagrange (Ver Apéndice E de libro de Bishop) lo que implica definir la siguiente Lagrangiana (Ejemplo):
\[ L(\theta, \theta_0, \alpha_n) = \frac{1}{2} \| \theta \|^2 - \sum_{n=1}^N \alpha_n [t_n(\theta^T x_n + \theta_0) - 1] \]
Con todos los parámetros de Lagrange \(\alpha_n \ge 0\).
Fíjate en que la Lagrangiana depende de \((\theta, \theta_0, \alpha_n)\), y lo que conocemos son las \(x_n\) y \(t_n\). Es un problema de optimización difícil de resolver.
\[ L(\theta, \theta_0, \alpha_n) = \frac{1}{2} \| \theta \|^2 - \sum_{n=1}^N \alpha_n [t_n(\theta^T x_n + \theta_0) - 1] \]
Nota
Los signos en la Lagrangiana son opuestos porque estamos maximizando la primera expresión, pero minimizando la segunda.
El siguiente paso es encontrar los extremos de la Lagrangiana con respecto de \(\theta\) y \(\theta_0\). Para ello, derivamos la Lagrangiana con respecto a ellos, igualamos a cero y despejamos:
\[ \frac{\partial L}{\partial \theta} = \theta - \sum_{n=1}^N \alpha_n t_n x_n = 0 \rightarrow \boxed{\theta = \sum_{n=1}^N \alpha_n t_n x_n} \\ \frac{\partial L}{\partial \theta_0} = - \sum_{n=1}^N \alpha_n t_n = 0 \rightarrow \boxed{\sum_{n=1}^N \alpha_n t_n = 0} \]
Parece que ya tenemos parte del problema solucionado, pero aún no del todo, porque no conocemos el valor de las \(\alpha_n\) y, de momento, no tenemos una expresión para calcular \(\theta_0\).
La segunda idea brillante es sustituir la expresión para \(\theta\) de la anterior expresión en la Lagrangiana, y hacer uso de la segunda expresión, con lo que la Lagrangina nos queda como (Demostrar):
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m x_n^T x_m + \sum_{n=1}^N \alpha_n \]
Si comparamos las dos expresiones:
\[ L(\theta, \theta_0, \alpha_n) = \frac{1}{2} \| \theta \|^2 - \sum_{n=1}^N \alpha_n [t_n(\theta^T x_n + \theta_0) - 1] \]
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m x_n^T x_m + \sum_{n=1}^N \alpha_n \]
La segunda tiene menos parámetros desconocidos. De hecho, la nueva expresión de la Lagrangiana es un problema cuadrático de minimización que siempre tiene solución numérica.
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m x_n^T x_m + \sum_{n=1}^N \alpha_n \]
Se puede demostrar que este problema de optimización añade una nueva restricción a las dos que ya tenemos, con lo que tenemos esta serie de restricciones:
\[ \alpha_n \ge 0 \\ t_n y(x_n) - 1 \ge 0 \\ \alpha_n [t_n y(x_n) - 1] = 0 \]
La última restricción es la clave.
\[ \alpha_n [t_n y(x_n) - 1] = 0 \]
Para que se cumpla esta condición tenemos dos opciones:
\[ \alpha_n = 0 \\ t_n y(x_n) - 1 = 0 \]
La primera opción nos dice que el vector \(x_n\) (dato de entrenamiento) no contribuye a la solución para obtener el valor de \(\theta\).
\[ \theta = \sum_{n=1}^N \alpha_n t_n x_n \]
La segunda opción:
\[ t_n y(x_n) - 1 = 0 \]
Nos dice que el vector \(x_n\) (dato de entrenamiento) está sobre la recta del margen, y sí contribuye a la solución de \(\theta\).
\(x_n\) es un vector de soporte!!!.
Con toda esta información, ahora sí, podemos calcular el valor de \(\theta_0\) que lo tenemos aún pendiente.
Sustituyendo en la expresión de la condición que el vector esté sobre la recta del margen:
\[ t_n (\theta^T x_n + \theta_0) = 1 \]
La expresión de \(\theta\) (sumatorio sólo se extiende sobre los vectores de soporte):
\[ \theta = \sum_{x_m \in S} \alpha_m t_m x_m \]
Tenemos:
\[ t_n \left[ \left( \sum_{x_m \in S} \alpha_m t_m x_m^T x_n \right) + \theta_0 \right] = 1 \]
Que se puede modificar para facilitar el trabajo de los algoritmos de optimización (Bishop, 2006).
Si la anterior expresión la multiplicamos por \(t_n\) y sumamos sobre todos los vectores de soporte, podemos despejar \(\theta_0\) (Demostración):
\[ \theta_0 = \frac{1}{N_S} \left[ \sum_{x_n \in S} \left( t_n - \sum_{x_m \in S} \alpha_m t_m x_m^T x_n \right) \right] \]
Que junto a:
\[ \theta = \sum_{x_m \in S} \alpha_m t_m x_m \]
Resuelve el problema de clasificación.
En la solución sólo están implicadas las \(x_n \in S\), que hemos encontrado resolviendo un problema de optimización cuadrático, las que hemos llamado vectores de soporte.
Cuando tenemos que clasificar una nueva muestra, aplicamos:
\[ y(x) = \theta^T x + \theta_0 \]
Si \(y(x) > 0\) la muestra la clasificamos como positiva.
Si \(y(x) <0\) la muestra la clasificamos como negativa.
El algoritmo de clasificación es muy rápido.
Lo que buscamos es la recta (hiperplano) que separa las muestras y tiene mayor margen.
Las máquinas de soporte vectorial funcionan muy bien cuando nuestro conjunto de entrenamiento contiene unos cuantos miles de muestras. Si tenemos muchas muestras, el entrenamiento puede ser lento.
Después de la teoría, veamos cómo utilizar la implementación de scikit-learn (¿Qué es eso del kernel?)
from sklearn import svm
from sklearn.inspection import DecisionBoundaryDisplay
clasificador = svm.SVC(kernel="linear")
clasificador.fit(X, y);
plt.scatter(X[:5,0], X[:5,1])
plt.scatter(X[5:,0], X[5:,1])
plt.legend(["positivas", "negativas"])
ax = plt.gca()
DecisionBoundaryDisplay.from_estimator(
clasificador,
X,
plot_method = "contour",
colors = "k",
levels=[-1, 0, 1],
alpha=0.5,
linestyles=["--", "-", "--"],
ax=ax
)
ax.scatter(
clasificador.support_vectors_[:, 0],
clasificador.support_vectors_[:, 1],
s=100,
linewidth=1,
facecolors="none",
edgecolors="k",
);
Sólo dos de las muestras son vectores de soporte.
scikit-learn proporciona una versión optimizada cuando el problema es linealmente separable:
Esta clase está optimizada para problemas lineales, pero no da tanta información sobre la solución como la clase más genérica SVC.
SVM da mejores resultados si los datos están en la misma escala:
Para ello, resulta muy útil utilizar alguna de las clases que proporciona scikit-learn como MinMaxScaler o StandardScaler
O mejor aún, definir un pipeline con la secuencia de pasos:
Antes de pasar a ver qué es el kernel, vamos a analizar el caso en el que las muestras están solapadas, están en la región de la clase contraria.
No vamos a entrar en el detalle de la demostración (Bishop 2006).
La cantidad (relacionado con la distancia) que esta vez debemos minimizar es:
\[ C \sum_{n=1}^N \xi_n+ \frac{1}{2} \| \theta \|^2 \]
Esta vez en la solución tenemos vectores de soporte que pueden cumplir las condiciones 2, 3 ó 4.
En el código, el único cambio es que podemos especificar el valor de \(C\) que es un parámetro de regularización.
Observa el siguiente conjunto de datos:
Fíjate que cada muestra sólo tiene valor para una característica y la etiqueta de la clase.
Ahora, vamos a hacer un truco aumentando el número de características de cada muestra \((x) \rightarrow (x, x^2)\):
Ahora el problema sí que es linealmente separable:
Hemos ampliado el número de características en el conjunto de datos.
Vamos a ver un ejemplo más complejo:
Claramente no es linealmente separable.
Para cada punto \(p = (p_1,p_2)\) vamos a realizar la siguiente transformación:
\[ \phi: \mathbb{R^2} \rightarrow \mathbb{R^3} \\ (p_1, p_2) \rightarrow (p_1^2, p_2^2, \sqrt{2}p_1 p_2) \] Ahora sí que podemos separar las muestras con un hiperplano.
Por otro lado, esta es la lagrangiana que hemos minimizado:
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m x_n^T x_m + \sum_{n=1}^N \alpha_n \]
El punto importante es que aparecen términos del producto escalar de dos puntos del conjunto de entrenamiento \(x_n^T, x_m\), con:
\[ x_n, x_m \in \mathbb{R^D} \]
Donde \(D\) es la dimensión del espacio.
Quizás, la lagrangiana no se pueda minimizar en un espacio de dimensión \(D\), pero sí se puede minimizar en un espacio de dimensión superior:
\[ \phi: \mathbb{R^D} \rightarrow \mathbb{R^E} \\ x \in \mathbb{R^D} \rightarrow \phi(x) \in \mathbb{R^E} \]
Con \(E > D\).
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m \phi(x_n^T) \phi(x_m) + \sum_{n=1}^N \alpha_n \]
El punto importe es el producto de los vectores transformados:
\[ \phi(x_n^T) \phi(x_m) \]
Puede ocurrir que al transformar los vectores de partida lleguemos a un espacio con muchas más dimensiones que el original, y el producto de los vectores transformados sea impracticable (Ej: el kernel gaussiano transforma un vector de dimensión n a un espacio de infinitas dimensiones).
El truco consiste en elegir una transformación \(\phi(\cdot)\) de tal modo que cuando tenga que hacer el producto escalar \(\phi(x_n^T) \phi(x_m)\) no tenga que hacer las transformaciones \(\phi(\cdot)\) de cada vector porque sé como calcular ese producto sin tener que hacerlas.
Volvamos al ejemplo inicial:
\[ \phi: \mathbb{R^2} \rightarrow \mathbb{R^3} \\ x = (x_1, x_2) \rightarrow \phi(x) = (x_1^2, x_2^2, \sqrt{2}x_1 x_2) \\ x^{\prime} = (x_1^{\prime}, x_2^{\prime}) \rightarrow \phi(x^{\prime}) = (x_1^{\prime 2}, x_2^{\prime 2}, \sqrt{2}x_1^{\prime} x_2^{\prime}) \]
Multiplicamos escalarmente los vectores transformados:
\[ \phi(x) \phi(x^{\prime}) = (x_1^2, x_2^2, \sqrt{2}x_1 x_2) \cdot (x_1^{\prime 2}, x_2^{\prime 2}, \sqrt{2}x_1^{\prime} x_2^{\prime}) = \\ x_1^2 x_1^{\prime 2} + x_2^2 x_2^{\prime 2} + 2 x_1 x_1^{\prime} x_2 x_2^{\prime} = \\ [(x_1, x_2) \cdot (x_1^{\prime}, x_2^{\prime})]^2 = k(x, x^{\prime}) \]
El kernel \(k(x, x^{\prime})\) se calcula a partir de los vectores sin transformar, y el resultado es el mismo que el del producto escalar de los vectores transformados.
Luego:
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m \phi(x_n^T) \phi(x_m) + \sum_{n=1}^N \alpha_n \]
\[ L(\alpha_n) = -\frac{1}{2} \sum_{n=1}^N \sum_{m=1}^N \alpha_n \alpha_m t_n t_m k(x_n, x_m) + \sum_{n=1}^N \alpha_n \]
Lo complicado es encontrar los kernels.
La condición (fuerte) que debe cumplir el kernel es que debe ser igual al producto escalar de los vectores transformados.
Algunos kernels:
Resulta más sencillo construir nuevos kernels a partir de kernels conocidos usando las siguientes reglas:
Vamos a probar un kernel gaussiano:
El kernel gaussiano lleva el conjunto de datos a un espacio de dimensión infinita.
clasificador_no_lineal = svm.SVC(kernel="rbf", C=1000)
clasificador_no_lineal.fit(Xcir, ycir)
plt.scatter(Xcir[ycir==0][:,0], Xcir[ycir==0][:,1])
plt.scatter(Xcir[ycir==1][:,0], Xcir[ycir==1][:,1])
plt.legend(["positivas", "negativas"])
plt.xlim(-1.5, 1.5)
plt.ylim(-1.5, 1.5)
ax = plt.gca()
disp = DecisionBoundaryDisplay.from_estimator(
clasificador_no_lineal,
Xcir,
plot_method = "contour",
colors = "k",
levels=[-1, 0, 1],
alpha=0.5,
linestyles=["--", "-", "--"],
ax=ax
)
ax.scatter(
clasificador_no_lineal.support_vectors_[:, 0],
clasificador_no_lineal.support_vectors_[:, 1],
s=100,
linewidth=1,
facecolors="none",
edgecolors="k",
);
¿Se te ocurre alguna otra solución ad-hoc para solucionar el problema de clasificación de estos datos?
Vamos a aplicar SVM al caso de los datos de Howell. Vamos a utilizar, inicialmente un kernel lineal:
Ahora veamos los resultados con un kenel polinómico y otro gaussiano:
Veamos qué tal se comporta con los datos de prueba para el caso de un kernel lineal:
Precisión (accuracy): 0.8522727272727273
Veamos qué tal se comporta con los datos de prueba para el caso de un kernel polinómico:
Precisión (accuracy): 0.8409090909090909
Finalmente, veamos qué tal se comporta con los datos de prueba para el caso de un kernel gaussiano:
Precisión (accuracy): 0.8522727272727273
Si comparamos con la regresión logística, vemos que los resultados obtenidos son muy parecidos.
Aprendizaje Automático (IR2130) - Óscar Belmonte Fernández