É impossível explicar o que é a máquina de vetores de suporte
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
- , , , . , — . , — [2] [3], , , .
- « SMO» SMO , , . , , SMO, .
- , «» Python .
- « sklearn.svc.svm» — 2D confusion matrix MNIST.
- «» - .
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 x⋅w+b=0 e dimensionar os parâmetros para que os pontos do conjunto de dados mais próximos da linha reta satisfaçam x⋅w+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(xi⋅w+b)−1≥0.
Se você resolver, então a classificação a pedido x produzido assim
class(x)=sign(x⋅w+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
ξi≥0
, 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+C∑iξi)subject to: ξi+yi(xi⋅w+b)−1≥0,ξi≥0,
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λ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,
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,ξk≠0[yk−∑iλiyi(xi⋅xk)].
O classificador pode (e deve) ser reescrito em termos de variáveis duais
class(x)=sign(f(x)),f(x)=∑iλiyi(xi⋅x)+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 (xi⋅xj) ... 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−γ|xi−xj|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λ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,
então encontre o parâmetro
b=Ek,ξk≠0[yk−∑iλ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 e restrições 0≤λM≤C , 0≤λL≤C (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λn∑i=1λi−12λ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.
Onde const denota termos independentes de λL ou λM ... Na última linha, usei a notação
Observe que k0+Qv0 não depende de λL nem de λM
O kernel é simétrico, portanto QT=Q e você pode escrever
L=(k0+Qv0−Qv0)Tv0+12vT0Qv0+const=(k0+Qv0)Tv0−12vT0Qv0+const
Queremos realizar a maximização para que permaneceu constante. Para isso, os novos valores devem estar em linha reta
É fácil ter certeza de que para qualquer t
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)Tdvdt−12(d(vTQv)dv)Tdvdt==kT0u+vT0QTu−vTQTu⏟(vT0−vT)Qu=kT0u−tuTQu.
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
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] .
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] ).
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
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
- https://fbeilstein.github.io/simplest_smo_ever/
- página em http://www.machinelearning.ru
- "Beginnings of Machine Learning", 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