Es imposible explicar qué es la máquina de vectores de soporte de
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
- , , , . , — . , — [2] [3], , , .
- « SMO» SMO , , . , , SMO, .
- , «» Python .
- « sklearn.svc.svm» — 2D confusion matrix MNIST.
- «» - .
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 x⋅w+b=0 y escalar los parámetros para que el punto del conjunto de datos más cercano a la línea recta satisfaga x⋅w+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(xi⋅w+b)−1≥0.
Si lo resuelve, entonces la clasificación a pedido. x producido así
class(x)=sign(x⋅w+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 ξi≥0 , 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+C∑iξi)subject to: ξi+yi(xi⋅w+b)−1≥0,ξi≥0,
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λn∑i=1λi−12n∑i=1n∑j=1yiyj(xi⋅xj)λiλj,subject to: 0≤λi≤C, for i=1,2,…,n,n∑i=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,ξk≠0[yk−∑iλiyi(xi⋅xk)].
El clasificador puede (y debe) reescribirse en términos de variables duales
class(x)=sign(f(x)),f(x)=∑iλiyi(xi⋅x)+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 (xi⋅xj) ... 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−γ|xi−xj|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λn∑i=1λi−12n∑i=1n∑j=1yiyjK(xi;xj)λiλj,subject to: 0≤λi≤C, for i=1,2,…,n,n∑i=1yiλi=0,
luego encuentra el parámetro
b=Ek,ξk≠0[yk−∑iλ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 y restricciones 0≤λM≤C , 0≤λL≤C (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λn∑i=1λi−12λ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.
Dónde const denota términos independientes de λL o λM ... En la última línea, usé la notación
tenga en cuenta que k0+Qv0 no depende de λL ni de λM
El kernel es simétrico, por lo tanto QT=Q y tu puedes escribir
L=(k0+Qv0−Qv0)Tv0+12vT0Qv0+const=(k0+Qv0)Tv0−12vT0Qv0+const
Queremos realizar la maximización para que permaneció constante. Para ello, los nuevos valores deben estar en línea recta.
Es fácil asegurarse de que para cualquier t
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)Tdvdt−12(d(vTQv)dv)Tdvdt==kT0u+vT0QTu−vTQTu⏟(vT0−vT)Qu=kT0u−tuTQu.
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.
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] .
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] ).
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
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
- https://fbeilstein.github.io/simplest_smo_ever/
- página en http://www.machinelearning.ru
- "Inicios del aprendizaje automático", KAU
- https://en.wikipedia.org/wiki/Kernel_method
- https://en.wikipedia.org/wiki/Duality_(optimization)
- http://www.machinelearning.ru
- https://www.youtube.com/watch?v=MTY1Kje0yLg
- https://en.wikipedia.org/wiki/Sequential_minimal_optimization
- 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).
- https://en.wikipedia.org/wiki/Karush%E2%80%93Kuhn%E2%80%93Tucker_conditions
- http://cs229.stanford.edu/materials/smo.pdf
- https://www.researchgate.net/publication/344460740_Yet_more_simple_SMO_algorithm
- http://fourier.eng.hmc.edu/e176/lectures/ch9/node9.html
- https://github.com/fbeilstein/simplest_smo_ever