Soporte Vector Machine, 30 líneas

En este artículo, le mostraré cómo escribir su máquina de vectores de soporte muy simple sin scikit-learn u otras bibliotecas con una implementación lista para usar en solo 30 líneas en Python. Si desea comprender el algoritmo SMO, pero le parece demasiado complicado, este artículo puede resultarle útil.



Es imposible explicar qué es la máquina de vectores de soporte de Matrix ... Tienes que verla tú mismo.





Habiendo aprendido que Habré tiene la capacidad de insertar elementos multimedia, creé una pequeña demostración (si de repente no funciona, aún puedes probar suerte con la versión en github [1] ). Colocar en un plano (espacio de dos entidades X y Y ) varios puntos rojos y azules (este es nuestro conjunto de datos) y la máquina realizará la clasificación (se pinta cada punto del fondo según donde se clasifique la solicitud correspondiente). Mueve los puntos, cambia el kernel (te aconsejo que pruebes Radial Basis Functions) y la dureza del borde (constante C ). Mis disculpas por el terrible código en JS: escribí en él solo unas pocas veces en mi vida, para comprender el algoritmo, use el código Python más adelante en el artículo.



Contenido









Support Vector Machine es un método de aprendizaje automático (aprendizaje supervisado) para la resolución de problemas de clasificación, regresión, detección de anomalías, etc. Lo consideraremos usando el ejemplo de un problema de clasificación binaria. Nuestro ejemplo de formación es un conjunto de vectores de características xi clasificado en una de dos clases yi=±1 ... Solicitud de clasificación - Vector x al cual debemos asignar la clase +1 o 1 ...



En el caso más simple, las clases de la muestra de entrenamiento se pueden dividir dibujando solo una línea recta como en la figura (para un mayor número de características, esto sería un hiperplano). Ahora, cuando llega una solicitud para clasificar algún punto x , es razonable colocarlo en la clase de qué lado estará.



¿Cómo elegir la mejor línea recta? Intuitivamente, quiero que la línea recta pase por el medio entre las clases. Para hacer esto, escribe la ecuación de la línea recta como xw+b=0 y escalar los parámetros para que el punto del conjunto de datos más cercano a la línea recta satisfaga xw+b=±1 (más o menos, según la clase): estos puntos se denominan vectores de soporte .



En este caso, la distancia entre los puntos límite de las clases es 2/|w| ... Obviamente, queremos maximizar este valor para separar las clases lo mejor posible. Esto último equivale a minimizar 12|w|2 , todo el problema de optimización está escrito





min12|w|2subject to: yi(xiw+b)10.





Si lo resuelve, entonces la clasificación a pedido. x producido así





class(x)=sign(xw+b).





Esta es la máquina de vectores de soporte más simple.



¿Y qué hacer en el caso de que puntos de diferentes clases se penetren mutuamente como en la figura?



Ya no podemos resolver el problema de optimización anterior, no hay parámetros que satisfagan esas condiciones. Entonces es posible permitir que los puntos violen el límite por la cantidad ξi0 , pero también es deseable que tales infractores sean el menor número posible. Esto se puede lograr modificando la función objetivo con un término adicional (regularización L1 ):





min(12|w|2+Ciξi)subject to: ξi+yi(xiw+b)10,ξi0,





y el procedimiento de clasificación continuará como antes. Aquí el hiperparámetro C es responsable de la fuerza de la regularización, es decir, determina cuán estrictamente requerimos puntos para respetar el límite: cuanto más C - cuanto más ξi desaparecerá y menos puntos violarán el límite. Los vectores de soporte en este caso son los puntos para los cuales ξi>0 ...



Pero, ¿qué pasa si el conjunto de entrenamiento se parece al logotipo de The Who y los puntos nunca pueden separarse con una línea recta?



Aquí nos ayudará una técnica ingeniosa: el truco del kernel [4] . Sin embargo, para aplicarlo hay que pasar al llamado problema de Lagrange dual (o dual ). Se puede encontrar una descripción detallada del mismo en Wikipedia [5] o en la sexta conferencia del curso [3] . Las variables en las que se resuelve el nuevo problema se denominan multiplicadores duales o de Lagrange.... El problema dual es a menudo más simple que el original y tiene buenas propiedades adicionales, por ejemplo, es cóncavo incluso si el problema original no es convexo. Si bien su solución no siempre coincide con la solución del problema original (ruptura de dualidad), existen una serie de teoremas que, en determinadas condiciones, garantizan dicha coincidencia (dualidad fuerte). Y este es solo nuestro caso, por lo que puede pasar con seguridad al problema dual





maxλni=1λi12ni=1nj=1yiyj(xixj)λiλj,subject to: 0λiC, for i=1,2,,n,ni=1yiλi=0,





Dónde λi - variables duales. Después de resolver el problema de maximización, también es necesario calcular el parámetro b , que no está incluido en el problema dual, pero es necesario para el clasificador





b=Ek,ξk0[ykiλiyi(xixk)].





El clasificador puede (y debe) reescribirse en términos de variables duales





class(x)=sign(f(x)),f(x)=iλiyi(xix)+b.





¿Cuál es la ventaja de esta grabación? Tenga en cuenta que todos los vectores del conjunto de formación se incluyen aquí exclusivamente en forma de productos punto (xixj) ... Primero puede mapear puntos a una superficie en un espacio de mayor dimensión y solo luego calcular el producto escalar de las imágenes en el nuevo espacio. Por qué hacer esto se puede ver en la figura.



Si el mapeo es exitoso, las imágenes de los puntos están separadas por un hiperplano. De hecho, todo es aún mejor: no hay necesidad de mostrar, porque solo nos interesa el producto escalar y no las coordenadas específicas de los puntos. Entonces, todo el procedimiento se puede emular reemplazando el producto escalar con la función K(xi;xj) , que se llama núcleo . Por supuesto, no todas las funciones pueden ser un kernel; debe existir al menos hipotéticamente un mapeo φ tal que K(xi;xj)=(φ(xi)φ(xj)) ... Las condiciones necesarias están determinadas por el teorema de Mercer [6] . La implementación de Python representará lineal ( K(xi;xj)=xTixj ), polinomio ( K(xi;xj)=(xTixj)d ) el núcleo y el núcleo de las funciones de base radial ( K(xi;xj)=eγ|xixj|2 ). Como puede ver en los ejemplos, los núcleos pueden introducir sus hiperparámetros específicos en el algoritmo, lo que también afectará su funcionamiento.



Es posible que hayas visto un video donde se explica la acción de la gravedad usando el ejemplo de una película de goma estirada en forma de embudo [7] . Esto funciona, ya que el movimiento de un punto a lo largo de una superficie curva en un espacio de alta dimensión es equivalente al movimiento de su imagen en un espacio de menor dimensión, si se proporciona una métrica no trivial. De hecho, el núcleo dobla el espacio.





Algoritmo SMO



Entonces, estamos en la meta, queda por resolver el problema dual planteado en el apartado anterior





maxλni=1λi12ni=1nj=1yiyjK(xi;xj)λiλj,subject to: 0λiC, for i=1,2,,n,ni=1yiλi=0,





luego encuentra el parámetro





b=Ek,ξk0[ykiλiyiK(xi;xk)],





y el clasificador tomará la siguiente forma





class(x)=sign(f(x)),f(x)=iλiyiK(xi;x)+b.





El algoritmo SMO (Optimización mínima secuencial, [8] ) para resolver el problema dual es el siguiente. En el ciclo, utilizando una heurística compleja ( [9] ), se selecciona un par de variables duales λM y λL , y luego la función objetivo se minimiza sobre ellos, con la condición de la constancia de la suma yMλM+yLλL y restricciones 0λMC , 0λLC (ajuste de la dureza del borde). La condición de suma almacena la suma de todos yiλi sin cambios (después de todo, no tocamos el resto de las lambdas). El algoritmo se detiene cuando detecta una observancia suficientemente buena de las llamadas condiciones KKT (Karush-Kuna-Tucker [10] ).



Voy a hacer algunas simplificaciones.



  • Abandonaré las complejas heurísticas de la selección de índices (esto se hace en el curso de la Universidad de Stanford [11] ), iteraré sobre un índice y seleccionaré el segundo al azar.
  • Me negaré a verificar el PCC y realizaré un número predeterminado de iteraciones por adelantado.
  • En el procedimiento de optimización en sí, en contraste con el trabajo clásico [9] o el enfoque de Stanford [11] , usaré la ecuación vectorial en línea recta. Esto simplificará enormemente los cálculos (compare el volumen de [12] y [13] ).


Ahora para los detalles. La información de la muestra de formación se puede escribir en forma de matriz





K=(y1y1K(x1;x1)y1y2K(x1;x2)y1yNK(x1;xN)y2y1K(x2;x1)y2y2K(x2;x2)y2yNK(x2;xN)yNy1K(xN;x1)yNy2K(xN;x2)yNyNK(xN;xN)).





En lo que sigue, usaré la notación con dos índices ( Ki,j ) para hacer referencia a un elemento de la matriz y con un índice ( Kk ) para denotar el vector columna de la matriz. Recopile las variables duales en un vector de columna λ ... Estamos interesados ​​en





maxλni=1λi12λTKλL.





Supongamos que, en la iteración actual, queremos maximizar la función objetivo mediante índices L y M ... Tomaremos derivados, por lo que conviene seleccionar los términos que contienen los índices L y M ... Es fácil de hacer en la parte con la cantidad. λi , pero la forma cuadrática requerirá varias transformaciones.



Al calcular λTKλ la suma se realiza sobre dos índices, sea i y j ... Resalte los pares de índices que contienen L o M ...





Reescribamos el problema combinando todo lo que no contiene λL o λM ... Para facilitar el seguimiento de los índices, preste atención a K en la imagen.

L=λM+λLjλMλjKM,jiλLλiKL,i+const+ +12λ2MKM,M+λMλLKM,L+12λ2LKL,L==λM(1jλjKM,j)+λL(1iλiKL,i)++12(λ2MKM,M+2λMλLKM,L+λ2LKL,L)+const==kT0v0+12vT0Qv0+const,





Dónde const denota términos independientes de λL o λM ... En la última línea, usé la notación

v0=(λM,λL)T,k0=(1λTKM,1λTKL)T,Q=(KM,MKM,LKL,MKL,L),u=(yL,yM)T.





tenga en cuenta que k0+Qv0 no depende de λL ni de λM

k0=(1λMKM,MλLKM,LiM,LλiKM,i1λMKL,MλLKL,LiM,LλiKL,i)=(1iM,LλiKM,i1iM,LλiKL,i)Qv0.





El kernel es simétrico, por lo tanto QT=Q y tu puedes escribir





L=(k0+Qv0Qv0)Tv0+12vT0Qv0+const=(k0+Qv0)Tv012vT0Qv0+const





Queremos realizar la maximización para que yLλL+yMλM permaneció constante. Para ello, los nuevos valores deben estar en línea recta.

(λnewM,λnewL)T=v(t)=v0+tu.





Es fácil asegurarse de que para cualquier t

yMλnewM+yLλnewL=yMλM+yLλL+t(yMyL+yLyM)=yMλM+yLλL.





En este caso, debemos maximizar





L(t)=(k0+Qv0)Tv(t)12vT(t)Qv(t)+const,





que es fácil de hacer tomando la derivada





dL(t)dt=(k0+Qv0)Tdvdt12(d(vTQv)dv)Tdvdt==kT0u+vT0QTuvTQTu(vT0vT)Qu=kT0utuTQu.





Igualando la derivada a cero, obtenemos





t=kT0uuTQu.





Y una cosa más: quizás trepemos más de lo necesario y nos encontremos fuera de la plaza como en la imagen. Entonces necesitas dar un paso atrás y regresar a su borde.

(λnewM,λnewL)=v0+trestru.





Esto completa la iteración y selecciona nuevos índices.





Implementación





El diagrama esquemático del entrenamiento de una máquina de vectores de soporte simplificada se puede escribir como







Echemos un vistazo al código en un lenguaje de programación real. Si no le gusta el código de los artículos, puede estudiarlo en github [14] .



Código fuente de la máquina de vectores de soporte simplificado
import numpy as np

class SVM:
  def __init__(self, kernel='linear', C=10000.0, max_iter=100000, degree=3, gamma=1):
    self.kernel = {'poly'  : lambda x,y: np.dot(x, y.T)**degree,
         'rbf': lambda x,y: np.exp(-gamma*np.sum((y-x[:,np.newaxis])**2,axis=-1)),
         'linear': lambda x,y: np.dot(x, y.T)}[kernel]
    self.C = C
    self.max_iter = max_iter

  #   t,       
  def restrict_to_square(self, t, v0, u): 
    t = (np.clip(v0 + t*u, 0, self.C) - v0)[1]/u[1]
    return (np.clip(v0 + t*u, 0, self.C) - v0)[0]/u[0]

  def fit(self, X, y):
    self.X = X.copy()
    #   0,1  -1,+1;     sklearn
    self.y = y * 2 - 1 
    self.lambdas = np.zeros_like(self.y, dtype=float)
    #  (3)
    self.K = self.kernel(self.X, self.X) * self.y[:,np.newaxis] * self.y
    
    #  self.max_iter 
    for _ in range(self.max_iter):
      #     
      for idxM in range(len(self.lambdas)):                                    
        # idxL  
        idxL = np.random.randint(0, len(self.lambdas))                         
        #  (4)
        Q = self.K[[[idxM, idxM], [idxL, idxL]], [[idxM, idxL], [idxM, idxL]]] 
        #  (4a)
        v0 = self.lambdas[[idxM, idxL]]                                        
        #  (4b)
        k0 = 1 - np.sum(self.lambdas * self.K[[idxM, idxL]], axis=1)           
        #  (4d)
        u = np.array([-self.y[idxL], self.y[idxM]])                            
        #   (5),    idxM = idxL
        t_max = np.dot(k0, u) / (np.dot(np.dot(Q, u), u) + 1E-15) 
        self.lambdas[[idxM, idxL]] = v0 + u * self.restrict_to_square(t_max, v0, u)
    
    #    
    idx, = np.nonzero(self.lambdas > 1E-15) 
    #  (1)
    self.b = np.mean((1.0-np.sum(self.K[idx]*self.lambdas, axis=1))*self.y[idx]) 
  
  def decision_function(self, X):
    return np.sum(self.kernel(X, self.X) * self.y * self.lambdas, axis=1) + self.b

  def predict(self, X): 
    #   -1,+1  0,1;     sklearn
    return (np.sign(self.decision_function(X)) + 1) // 2

      
      









Al crear un objeto de la clase SVM, puede especificar hiperparámetros. El entrenamiento se realiza llamando a la función de ajuste, las clases deben especificarse como 0 y 1 (convertido internamente a 1 y +1 , hecho para una mayor compatibilidad con sklearn), la dimensión del vector de características se permite arbitrariamente. La función de predicción se utiliza para la clasificación.





Comparación con sklearn.svm.SVC



No es que esta comparación tenga mucho sentido, porque estamos hablando de un algoritmo extremadamente simplificado, desarrollado únicamente con el propósito de enseñar a los estudiantes, pero aún así. Para probar (y ver cómo usarlo todo), puede hacer lo siguiente (este código también está disponible en github [14] ).



Comparación con sklearn.svm.SVC en un conjunto de datos 2D simple
from sklearn.svm import SVC
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
from sklearn.datasets import make_blobs, make_circles
from matplotlib.colors import ListedColormap

def test_plot(X, y, svm_model, axes, title):
  plt.axes(axes)
  xlim = [np.min(X[:, 0]), np.max(X[:, 0])]
  ylim = [np.min(X[:, 1]), np.max(X[:, 1])]
  xx, yy = np.meshgrid(np.linspace(*xlim, num=700), np.linspace(*ylim, num=700))
  rgb=np.array([[210, 0, 0], [0, 0, 150]])/255.0
  
  svm_model.fit(X, y)
  z_model = svm_model.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
  
  plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
  plt.contour(xx, yy, z_model, colors='k', levels=[-1, 0, 1], alpha=0.5, linestyles=['--', '-', '--'])
  plt.contourf(xx, yy, np.sign(z_model.reshape(xx.shape)), alpha=0.3, levels=2, cmap=ListedColormap(rgb), zorder=1)
  plt.title(title)

X, y = make_circles(100, factor=.1, noise=.1)
fig, axs = plt.subplots(nrows=1,ncols=2,figsize=(12,4))
test_plot(X, y, SVM(kernel='rbf', C=10, max_iter=60, gamma=1), axs[0], 'OUR ALGORITHM')
test_plot(X, y, SVC(kernel='rbf', C=10, gamma=1), axs[1], 'sklearn.svm.SVC')

X, y = make_blobs(n_samples=50, centers=2, random_state=0, cluster_std=1.4)
fig, axs = plt.subplots(nrows=1,ncols=2,figsize=(12,4))
test_plot(X, y, SVM(kernel='linear', C=10, max_iter=60), axs[0], 'OUR ALGORITHM')
test_plot(X, y, SVC(kernel='linear', C=10), axs[1], 'sklearn.svm.SVC')

fig, axs = plt.subplots(nrows=1,ncols=2,figsize=(12,4))
test_plot(X, y, SVM(kernel='poly', C=5, max_iter=60, degree=3), axs[0], 'OUR ALGORITHM')
test_plot(X, y, SVC(kernel='poly', C=5, degree=3), axs[1], 'sklearn.svm.SVC')

      
      







Después del lanzamiento, se generarán imágenes, pero como el algoritmo es aleatorio, serán ligeramente diferentes para cada lanzamiento. Aquí hay un ejemplo de cómo funciona el algoritmo simplificado (de izquierda a derecha: núcleos lineales, polinomiales y rbf)



Y este es el resultado de la versión industrial de la máquina de vectores de soporte.



Si la dimensión 2 parece demasiado pequeño, aún puede probar en MNIST



Comparación con sklearn.svm.SVC en 2 clases de MNIST
from sklearn import datasets, svm
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns

class_A = 3
class_B = 8

digits = datasets.load_digits()
mask = (digits.target == class_A) | (digits.target == class_B)
data = digits.images.reshape((len(digits.images), -1))[mask]
target = digits.target[mask] // max([class_A, class_B]) # rescale to 0,1
X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.5, shuffle=True)

def plot_confusion(clf):
  clf.fit(X_train, y_train)
  y_fit = clf.predict(X_test)

  mat = confusion_matrix(y_test, y_fit)
  sns.heatmap(mat.T, square=True, annot=True, fmt='d', cbar=False, xticklabels=[class_A,class_B], yticklabels=[class_A,class_B])
  plt.xlabel('true label')
  plt.ylabel('predicted label');
  plt.show()

print('sklearn:')
plot_confusion(svm.SVC(C=1.0, kernel='rbf', gamma=0.001))
print('custom svm:')
plot_confusion(SVM(kernel='rbf', C=1.0, max_iter=60, gamma=0.001))

      
      







Para MNIST, probé varios pares aleatorios de clases (el algoritmo simplificado solo admite clasificación binaria), pero no encontré ninguna diferencia en el trabajo del algoritmo simplificado y sklearn. La siguiente matriz de confusión da una idea de la calidad.







Conclusión



Gracias a todos los que leyeron hasta el final. En este artículo, analizamos una implementación tutorial simplificada de una máquina de vectores de soporte. Por supuesto, no puede competir con un diseño industrial, pero debido a la extrema simplicidad y el código compacto de Python, así como al hecho de que se han conservado todas las ideas básicas de SMO, esta versión de SVM bien puede ocupar su lugar en el salón de clases. Cabe señalar que el algoritmo es más simple no solo que un algoritmo muy complicado [9] , sino incluso su versión simplificada de la Universidad de Stanford [11] . Después de todo, una máquina de vectores de soporte en 30 líneas es hermosa.



Lista de referencias



  1. https://fbeilstein.github.io/simplest_smo_ever/
  2. página en http://www.machinelearning.ru
  3. "Inicios del aprendizaje automático", KAU
  4. https://en.wikipedia.org/wiki/Kernel_method
  5. https://en.wikipedia.org/wiki/Duality_(optimization)
  6. http://www.machinelearning.ru
  7. https://www.youtube.com/watch?v=MTY1Kje0yLg
  8. https://en.wikipedia.org/wiki/Sequential_minimal_optimization
  9. Platt, John. Fast Training of Support Vector Machines using Sequential Minimal Optimization, in Advances in Kernel Methods – Support Vector Learning, B. Scholkopf, C. Burges, A. Smola, eds., MIT Press (1998).
  10. https://en.wikipedia.org/wiki/Karush%E2%80%93Kuhn%E2%80%93Tucker_conditions
  11. http://cs229.stanford.edu/materials/smo.pdf
  12. https://www.researchgate.net/publication/344460740_Yet_more_simple_SMO_algorithm
  13. http://fourier.eng.hmc.edu/e176/lectures/ch9/node9.html
  14. https://github.com/fbeilstein/simplest_smo_ever



All Articles