Máquina de vetores de suporte, 30 linhas

Neste artigo, mostrarei como escrever sua máquina de vetores de suporte muito simples sem scikit-learn ou outras bibliotecas com uma implementação pronta em apenas 30 linhas em Python. Se você queria entender o algoritmo SMO, mas parecia muito complicado, este artigo pode ser útil para você.



É impossível explicar o que é a máquina de vetores de suporte Matrix ... Você tem que ver você mesmo.





Tendo aprendido que Habré tem a capacidade de inserir elementos de mídia, criei uma pequena demonstração (se de repente não funcionar, você ainda pode tentar a sorte com a versão no github [1] ). Coloque em um plano (espaço de dois recursos X e Y ) vários pontos vermelhos e azuis (este é o nosso conjunto de dados) e a máquina fará a classificação (cada ponto do fundo é pintado dependendo de onde a solicitação correspondente seria classificada). Mova os pontos, mude o kernel (eu aconselho você a tentar Funções de Base Radial) e a dureza da borda (constante C ). Minhas desculpas pelo terrível código em JS - escrevi nele apenas algumas vezes na minha vida, para entender o algoritmo, use o código Python posteriormente neste artigo.



Contente









Support Vector Machine é um método de aprendizado de máquina (aprendizado supervisionado) para resolver problemas de classificação, regressão, detecção de anomalias, etc. Vamos considerá-lo usando o exemplo de um problema de classificação binária. Nosso exemplo de treinamento é um conjunto de vetores de recursos xi classificado em uma das duas classes yi=±1 ... Solicitação de classificação - vetor x para o qual devemos atribuir a classe +1 ou 1 ...



No caso mais simples, as classes da amostra de treinamento podem ser divididas desenhando apenas uma linha reta como na figura (para um número maior de recursos, seria um hiperplano). Agora, quando um pedido chega para classificar algum ponto x , é razoável colocá-lo na classe de que lado ficará.



Como escolher a melhor linha reta? Intuitivamente, quero que a linha reta passe no meio entre as classes. Para fazer isso, escreva a equação da linha reta como xw+b=0 e dimensionar os parâmetros para que os pontos do conjunto de dados mais próximos da linha reta satisfaçam xw+b=±1 (mais ou menos, dependendo da classe) - esses pontos são chamados de vetores de suporte .



Neste caso, a distância entre os pontos de fronteira das classes é 2/|w| ... Obviamente, queremos maximizar esse valor para separar as classes da melhor forma possível. Este último é equivalente a minimizar 12|w|2 , todo o problema de otimização está escrito





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





Se você resolver, então a classificação a pedido x produzido assim





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





Esta é a máquina de vetores de suporte mais simples.



E o que fazer no caso em que pontos de classes diferentes se penetram mutuamente como na figura?



Não podemos mais resolver o problema de otimização anterior - não há parâmetros que satisfaçam essas condições. Então, é possível permitir que os pontos violem o limite pelo valor ξi0 , mas também é desejável que tais violadores sejam o mínimo possível. Isso pode ser alcançado modificando a função objetivo com um termo adicional (regularização L1 ):





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





e o procedimento de classificação continuará como antes. Aqui o hiperparâmetro C é responsável pela força de regularização, ou seja, determina o quão estritamente exigimos pontos para respeitar a fronteira: quanto mais C - o mais ξi irá desaparecer e menos pontos irão violar o limite. Os vetores de suporte, neste caso, são os pontos para os quais ξi>0 ...



Mas e se o conjunto de treinamento se parecer com o logotipo do The Who e os pontos nunca puderem ser separados por uma linha reta?



Aqui, seremos ajudados por uma técnica engenhosa - o truque do kernel [4] . Porém, para aplicá-lo, deve-se passar ao chamado problema de Lagrange dual (ou dual ). Uma descrição detalhada pode ser encontrada na Wikipedia [5] ou na sexta aula do curso [3] . As variáveis ​​em que o novo problema é resolvido são chamadas de multiplicadores duais ou de Lagrange... O problema duplo é freqüentemente mais simples do que o original e tem boas propriedades adicionais, por exemplo, é côncavo mesmo se o problema original não for convexo. Embora sua solução nem sempre coincida com a solução do problema original (quebra de dualidade), há uma série de teoremas que, sob certas condições, garantem tal coincidência (dualidade forte). E este é apenas o nosso caso, então você pode ir com segurança ao problema duplo





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





Onde λi - variáveis ​​duais. Depois de resolver o problema de maximização, também é necessário calcular o parâmetro b , que não está incluído no problema duplo, mas é necessário para o classificador





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





O classificador pode (e deve) ser reescrito em termos de variáveis ​​duais





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





Qual é a vantagem dessa gravação? Observe que todos os vetores do conjunto de treinamento estão incluídos aqui exclusivamente na forma de produtos escalares (xixj) ... Você pode primeiro mapear pontos para uma superfície em um espaço de dimensão superior e só então calcular o produto escalar das imagens no novo espaço. Por que fazer isso pode ser visto na figura.



Se o mapeamento for bem-sucedido, as imagens dos pontos são separadas por um hiperplano! Na verdade, tudo é ainda melhor: não há necessidade de exibir, porque estamos apenas interessados ​​no produto escalar, e não nas coordenadas específicas dos pontos. Portanto, todo o procedimento pode ser emulado substituindo o produto escalar pela função K(xi;xj) , que é chamado de núcleo . Claro, nem toda função pode ser um kernel - pelo menos hipoteticamente, deve haver um mapeamento φ de tal modo que K(xi;xj)=(φ(xi)φ(xj)) ... As condições necessárias são determinadas pelo teorema de Mercer [6] . A implementação Python representará linear ( K(xi;xj)=xTixj ), polinomial ( K(xi;xj)=(xTixj)d ) o kernel e o kernel das funções de base radial ( K(xi;xj)=eγ|xixj|2 ) Como você pode ver nos exemplos, os kernels podem introduzir seus hiperparâmetros específicos no algoritmo, o que também afetará sua operação.



Você pode ter visto um vídeo onde a ação da gravidade é explicada usando o exemplo de um filme de borracha esticado em forma de funil [7] . Isso funciona, uma vez que o movimento de um ponto em uma superfície curva em um espaço de alta dimensão é equivalente ao movimento de sua imagem em um espaço de dimensão inferior, se você fornecer a ele uma métrica não trivial. Na verdade, o núcleo dobra o espaço.





Algoritmo SMO



Então, estamos no objetivo, resta resolver o problema duplo colocado na seção anterior





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





então encontre o parâmetro





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





e o classificador terá a seguinte forma





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





O algoritmo SMO (Sequential minimal optimization, [8] ) para resolver o problema dual é o seguinte. No loop, usando uma heurística complexa ( [9] ), um par de variáveis ​​duais é selecionado λM e λL , e então a função objetivo é minimizada sobre eles, com a condição da constância da soma yMλM+yLλL e restrições 0λMC , 0λLC (definindo a dureza da borda). A condição de soma armazena a soma de todos yiλi inalterado (afinal, não tocamos no resto dos lambdas). O algoritmo pára quando detecta uma observância suficientemente boa das chamadas condições KKT (Karush-Kuna-Tucker [10] ).



Vou fazer algumas simplificações.



  • Vou descartar a heurística complexa de seleção de índice (isso é feito no curso da Universidade de Stanford [11] ) e iterarei sobre um índice e selecionarei o outro aleatoriamente.
  • Recusarei verificar o CCP e executarei um número predeterminado de iterações com antecedência.
  • No próprio procedimento de otimização, em contraste com o trabalho clássico [9] ou a abordagem de Stanford [11] , usarei a equação vetorial de uma linha reta. Isso simplificará muito os cálculos (compare o volume de [12] e [13] ).


Agora, para os detalhes. As informações da amostra de treinamento podem ser escritas na forma de uma 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)).





A seguir, usarei a notação com dois índices ( Ki,j ) para se referir a um elemento da matriz e com um índice ( Kk ) para denotar o vetor coluna da matriz. Colete as variáveis ​​duais em um vetor de coluna λ ... Nós estamos interessados ​​em





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





Suponha que, na iteração atual, queiramos maximizar a função objetivo por índices L e M ... Tomaremos derivados, por isso é conveniente selecionar os termos que contêm os índices L e M ... É fácil fazer na parte com a quantidade λi , mas a forma quadrática exigirá várias transformações.



Ao calcular λTKλ a soma é realizada em dois índices, vamos i e j ... Realce pares de índices contendo L ou M ...





Vamos reescrever o problema combinando tudo o que não contém λL ou λM ... Para tornar mais fácil acompanhar os índices, preste atenção a K na imagem.

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,





Onde const denota termos independentes de λL ou λM ... Na última linha, usei a notação

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





Observe que k0+Qv0 não depende de λL nem 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.





O kernel é simétrico, portanto QT=Q e você pode escrever





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





Queremos realizar a maximização para que yLλL+yMλM permaneceu constante. Para isso, os novos valores devem estar em linha reta

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





É fácil ter certeza de que para qualquer t

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





Neste caso, devemos maximizar





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





o que é fácil de fazer tomando a derivada





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





Equacionando a derivada a zero, obtemos





t=kT0uuTQu.





E mais uma coisa: talvez subamos mais do que o necessário e nos encontremos fora da praça como na foto. Então você precisa dar um passo para trás e retornar à sua fronteira

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





Isso completa a iteração e seleciona novos índices.





Implementação





O diagrama esquemático de treinamento de uma máquina de vetores de suporte simplificado pode ser escrito como







Vamos dar uma olhada no código em uma linguagem de programação real. Se você não gosta do código dos artigos, pode estudá-lo no github [14] .



Código-fonte de máquina de vetor de suporte 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

      
      









Ao criar um objeto da classe SVM, você pode especificar hiperparâmetros. O treinamento é feito chamando a função de ajuste, as classes devem ser especificadas como 0 e 1 (convertido internamente para 1 e +1 , feito para maior compatibilidade com sklearn), a dimensão do vetor de recursos é permitida arbitrariamente. A função de previsão é usada para classificação.





Comparação com sklearn.svm.SVC



Não que essa comparação faça muito sentido, pois estamos falando de um algoritmo extremamente simplificado, desenvolvido exclusivamente para fins de ensino aos alunos, mas ainda assim. Para testar (e ver como usar tudo), você pode fazer o seguinte (este código também está disponível no github [14] ).



Comparação com sklearn.svm.SVC em um conjunto de dados 2D simples
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')

      
      







Após o lançamento, as imagens serão geradas, mas como o algoritmo é aleatório, elas serão ligeiramente diferentes a cada lançamento. Aqui está um exemplo de como o algoritmo simplificado funciona (da esquerda para a direita: kernels linear, polinomial e rbf)



E este é o resultado da versão industrial da máquina de vetores de suporte.



Se a dimensão 2 parece muito pequeno, você ainda pode testar no MNIST



Comparação com sklearn.svm.SVC em 2 classes 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, tentei vários pares aleatórios de classes (o algoritmo simplificado só suporta classificação binária), mas não encontrei nenhuma diferença no trabalho do algoritmo simplificado e do sklearn. A seguinte matriz de confusão dá uma ideia da qualidade.







Conclusão



Obrigado a todos que leram até o fim. Neste artigo, vimos uma implementação simplificada de tutorial de uma máquina de vetores de suporte. Claro, ele não pode competir com um design industrial, mas devido à extrema simplicidade e código compacto em Python, bem como o fato de que todas as idéias básicas de SMO foram preservadas, esta versão de SVM pode muito bem tomar seu lugar no Sala de aula. Deve-se notar que o algoritmo é mais simples não apenas do que um algoritmo muito complicado [9] , mas até mesmo sua versão simplificada da Universidade de Stanford [11] . Afinal, uma máquina de vetores de suporte em 30 linhas é linda.



Lista de referências



  1. https://fbeilstein.github.io/simplest_smo_ever/
  2. página em http://www.machinelearning.ru
  3. "Beginnings of Machine Learning", 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