Es ist unmöglich zu erklären, was die
Nachdem ich erfahren habe, dass Habré Medienelemente einfügen kann, habe ich eine kleine Demo erstellt (wenn es plötzlich nicht mehr funktioniert, können Sie Ihr Glück mit der Version auf Github [1] versuchen ). Platzieren Sie es in einer Ebene (Abstand von zwei Merkmalen X und Y ) mehrere rote und blaue Punkte (dies ist unser Datensatz) und die Maschine führt die Klassifizierung durch (jeder Punkt des Hintergrunds wird übermalt, je nachdem, wo die entsprechende Anforderung klassifiziert werden würde). Verschieben Sie die Punkte, ändern Sie den Kernel (ich empfehle Ihnen, Radial Basis Functions auszuprobieren) und die Härte des Randes (konstant) C ). Ich entschuldige mich für den schrecklichen Code in JS - ich habe ihn nur einige Male in meinem Leben geschrieben, um den Algorithmus zu verstehen, verwenden Sie den Python-Code später in diesem Artikel.
Inhalt
- , , , . , — . , — [2] [3], , , .
- « SMO» SMO , , . , , SMO, .
- , «» Python .
- « sklearn.svc.svm» — 2D confusion matrix MNIST.
- «» - .
Support Vector Machine ist eine maschinelle Lernmethode (überwachtes Lernen) zur Lösung von Klassifizierungs-, Regressions-, Anomalieerkennungsproblemen usw. Wir werden es am Beispiel eines binären Klassifizierungsproblems betrachten. Unser Trainingsbeispiel besteht aus einer Reihe von Merkmalsvektoren xich in eine von zwei Klassen eingeteilt yi=±1 ... Klassifizierungsanforderung - Vektor x dem wir die Klasse zuordnen müssen +1 oder −1 ...
Im einfachsten Fall können die Klassen des Trainingsmusters unterteilt werden, indem nur eine gerade Linie wie in der Abbildung gezeichnet wird (für eine größere Anzahl von Merkmalen wäre dies eine Hyperebene). Nun, wenn eine Anfrage kommt, um einen Punkt zu klassifizieren
x
Es ist vernünftig, es in die Klasse einzuteilen, auf welcher Seite es sein wird.
Wie wählt man die beste gerade Linie? Intuitiv möchte ich, dass die gerade Linie in der Mitte zwischen den Klassen verläuft. Schreiben Sie dazu die Gleichung der Geraden als x⋅w+b=0 und skalieren Sie die Parameter so, dass die Datensatzpunkte, die der geraden Linie am nächsten liegen, erfüllt werden x⋅w+b=±1 (Plus oder Minus, je nach Klasse) - Diese Punkte werden als Unterstützungsvektoren bezeichnet .
In diesem Fall beträgt der Abstand zwischen den Grenzpunkten der Klassen
2/|w|
... Natürlich möchten wir diesen Wert maximieren, um die Klassen so gut wie möglich zu trennen. Letzteres entspricht einer Minimierung
12|w|2
wird das gesamte Optimierungsproblem geschrieben
min12|w|2subject to: yi(xi⋅w+b)−1≥0.
Wenn Sie es lösen, dann die Klassifizierung auf Anfrage x so produziert
class(x)=sign(x⋅w+b).
Dies ist die einfachste Support-Vektor-Maschine.
Und was tun, wenn sich Punkte verschiedener Klassen wie in der Abbildung gegenseitig durchdringen?
Wir können das vorherige Optimierungsproblem nicht mehr lösen - es gibt keine Parameter, die diese Bedingungen erfüllen. Dann ist es möglich, dass die Punkte die Grenze um den Betrag verletzen
ξi≥0
Es ist jedoch auch wünschenswert, dass solche Verstöße so gering wie möglich sind. Dies kann erreicht werden, indem die Zielfunktion durch einen zusätzlichen Begriff geändert wird (Regularisierung)
L1
):
min(12|w|2+C∑iξi)subject to: ξi+yi(xi⋅w+b)−1≥0,ξi≥0,
und das Klassifizierungsverfahren wird wie zuvor fortgesetzt. Hier der Hyperparameter C ist verantwortlich für die Stärke der Regularisierung, das heißt, sie bestimmt, wie streng wir Punkte benötigen, um die Grenze zu respektieren: je mehr C - je mehr ξi verschwindet und je weniger Punkte die Grenze verletzen. Unterstützungsvektoren sind in diesem Fall die Punkte, für die ξi>0 ...
Aber was ist, wenn das Trainingsset dem Logo von The Who ähnelt und die Punkte niemals durch eine gerade Linie getrennt werden können?
Hier wird uns eine geniale Technik helfen - der
Kernel-Trick
[4] . Um es anzuwenden, muss man jedoch auf das sogenannte duale (oder
duale ) Lagrange-Problem übergehen
. Eine ausführliche Beschreibung finden Sie in Wikipedia
[5] oder in der sechsten Vorlesung des Kurses
[3] . Die Variablen, in denen das neue Problem gelöst wird, werden als
Dual- oder
Lagrange-Multiplikatoren bezeichnet... Das doppelte Problem ist oft einfacher als das ursprüngliche und hat gute zusätzliche Eigenschaften. Beispielsweise ist es konkav, selbst wenn das ursprüngliche Problem nicht konvex ist. Obwohl seine Lösung nicht immer mit der Lösung des ursprünglichen Problems übereinstimmt (Dualitätsbruch), gibt es eine Reihe von Theoremen, die unter bestimmten Bedingungen einen solchen Zufall garantieren (starke Dualität). Und dies ist nur unser Fall, damit Sie sicher zum doppelten Problem gelangen können
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,
Wo λi - doppelte Variablen. Nach dem Lösen des Maximierungsproblems muss auch der Parameter berechnet werden b , das nicht im dualen Problem enthalten ist, aber für den Klassifikator benötigt wird
b=Ek,ξk≠0[yk−∑iλiyi(xi⋅xk)].
Der Klassifikator kann (und sollte) in Form von dualen Variablen umgeschrieben werden
class(x)=sign(f(x)),f(x)=∑iλiyi(xi⋅x)+b.
Was ist der Vorteil dieser Aufnahme? Bitte beachten Sie, dass alle Vektoren aus dem Trainingssatz hier ausschließlich in Form von Punktprodukten enthalten sind (xi⋅xj) ... Sie können zuerst Punkte auf eine Oberfläche in einem höherdimensionalen Raum abbilden und erst dann das Punktprodukt der Bilder im neuen Raum berechnen. Warum dies so ist, ist aus der Abbildung ersichtlich.
Wenn die Zuordnung erfolgreich ist, werden die Bilder der Punkte durch eine Hyperebene getrennt! In der Tat ist alles noch besser: Es muss nicht angezeigt werden, da wir nur am Punktprodukt interessiert sind und nicht an den spezifischen Koordinaten der Punkte. So kann der gesamte Vorgang emuliert werden, indem das Punktprodukt durch die Funktion ersetzt wird
K(xi;xj)
, das heißt der
Kern . Natürlich kann nicht jede Funktion ein Kernel sein - zumindest hypothetisch muss es eine Zuordnung geben
φ
so dass
K(xi;xj)=(φ(xi)⋅φ(xj))
... Die notwendigen Bedingungen werden durch den Satz von Mercer
[6] bestimmt . Die Python-Implementierung wird linear darstellen (
K(xi;xj)=xTixj
), Polynom (
K(xi;xj)=(xTixj)d
) der Kernel und der Kernel der radialen Basisfunktionen (
K(xi;xj)=e−γ|xi−xj|2
). Wie Sie den Beispielen entnehmen können, können Kernel ihre spezifischen Hyperparameter in den Algorithmus einführen, was sich auch auf dessen Betrieb auswirkt.
Möglicherweise haben Sie ein Video gesehen, in dem die Wirkung der Schwerkraft am Beispiel eines gedehnten Gummifilms in Form eines Trichters erklärt wird [7] . Dies funktioniert, da die Bewegung eines Punkts auf einer gekrümmten Oberfläche in einem hochdimensionalen Raum der Bewegung seines Bildes in einem niedrigerdimensionalen Raum entspricht, wenn Sie ihm eine nichttriviale Metrik zur Verfügung stellen. Tatsächlich verbiegt der Kern den Raum.
SMO-Algorithmus
Wir sind also am Ziel, das im vorherigen Abschnitt aufgeworfene Doppelproblem zu lösen
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,
dann finden Sie den Parameter
b=Ek,ξk≠0[yk−∑iλiyiK(xi;xk)],
und der Klassifikator nimmt die folgende Form an
class(x)=sign(f(x)),f(x)=∑iλiyiK(xi;x)+b.
Der SMO-Algorithmus (Sequential Minimal Optimization, [8] ) zur Lösung des Doppelproblems lautet wie folgt. In der Schleife wird unter Verwendung einer komplexen Heuristik ( [9] ) ein Paar von Doppelvariablen ausgewählt λM und λL und dann wird die Zielfunktion über ihnen unter der Bedingung der Konstanz der Summe minimiert und Einschränkungen 0≤λM≤C , 0≤λL≤C (Einstellen der Härte der Grenze). Die Summenbedingung speichert die Summe aller yiλi unverändert (schließlich haben wir den Rest der Lambdas nicht berührt). Der Algorithmus stoppt, wenn er eine ausreichend gute Einhaltung der sogenannten KKT-Bedingungen feststellt (Karush-Kuna-Tucker [10] ).
Ich werde einige Vereinfachungen vornehmen.
- Ich werde die komplexen Heuristiken der Indexauswahl verwerfen (dies wird im Kurs der Stanford University [11] durchgeführt ) und über einen Index iterieren und den anderen zufällig auswählen.
- Ich werde mich weigern, die KPCh zu überprüfen, und im Voraus eine vorgegebene Anzahl von Iterationen durchführen.
- Im Optimierungsverfahren selbst werde ich im Gegensatz zur klassischen Arbeit [9] oder zum Stanford-Ansatz [11] die Vektorgleichung einer geraden Linie verwenden. Dies vereinfacht die Berechnungen erheblich (vergleiche das Volumen von [12] und [13] ).
Nun zu den Details. Informationen aus dem Trainingsmuster können in Form einer Matrix geschrieben werden
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)).
Im Folgenden werde ich die Notation mit zwei Indizes verwenden ( Ki,j ), um sich auf ein Element der Matrix und mit einem Index zu beziehen ( Kk ), um den Spaltenvektor der Matrix zu bezeichnen. Sammeln Sie die beiden Variablen in einem Spaltenvektor λ ... Wir sind interessiert an
maxλn∑i=1λi−12λTKλ⏟L.
Angenommen, wir möchten bei der aktuellen Iteration die Zielfunktion durch Indizes maximieren L und M ... Wir werden Derivate nehmen, daher ist es zweckmäßig, die Begriffe auszuwählen, die die Indizes enthalten L und M ... Es ist einfach, in dem Teil mit der Menge zu tun λi Die quadratische Form erfordert jedoch mehrere Transformationen.
Bei der Berechnung λTKλ Die Summierung erfolgt über zwei Indizes i und j ... Markieren Sie Indexpaare mit L oder M ...
Schreiben wir das Problem neu, indem wir alles kombinieren, was nicht enthalten ist λL oder λM ... Beachten Sie Folgendes, um die Übersicht über die Indizes zu erleichtern K im Bild.
Wo const bezeichnet Begriffe unabhängig von λL oder λM ... In der letzten Zeile habe ich die Notation verwendet
beachten Sie, dass k0+Qv0 hängt nicht ab von λL noch von λM
Der Kernel ist daher symmetrisch QT=Q und du kannst schreiben
L=(k0+Qv0−Qv0)Tv0+12vT0Qv0+const=(k0+Qv0)Tv0−12vT0Qv0+const
Wir wollen die Maximierung so durchführen konstant geblieben. Dazu müssen die neuen Werte auf der Geraden liegen
Es ist einfach, das für jeden sicherzustellen t
In diesem Fall müssen wir maximieren
L(t)=(k0+Qv0)Tv(t)−12vT(t)Qv(t)+const,
Das ist einfach, wenn man das Derivat nimmt
dL(t)dt=(k0+Qv0)Tdvdt−12(d(vTQv)dv)Tdvdt==kT0u+vT0QTu−vTQTu⏟(vT0−vT)Qu=kT0u−tuTQu.
Wenn wir die Ableitung mit Null gleichsetzen, erhalten wir
t∗=kT0uuTQu.
Und noch etwas: Vielleicht klettern wir weiter als nötig und befinden uns wie auf dem Bild außerhalb des Platzes. Dann müssen Sie einen Schritt zurücktreten und an die Grenze zurückkehren
Damit ist die Iteration abgeschlossen und es werden neue Indizes ausgewählt.
Implementierung
Das schematische Diagramm des Trainings einer vereinfachten Unterstützungsvektormaschine kann wie folgt geschrieben werden
Schauen wir uns den Code in einer echten Programmiersprache an. Wenn Ihnen der Code in den Artikeln nicht gefällt, können Sie ihn auf dem Github [14] studieren .
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
Beim Erstellen eines Objekts der SVM-Klasse können Sie Hyperparameter angeben. Das Training erfolgt durch Aufrufen der Fit-Funktion. Die Klassen müssen als angegeben werden 0 und 1 (intern konvertiert zu −1 und +1 Aus Gründen der besseren Kompatibilität mit sklearn) ist die Dimension des Merkmalsvektors beliebig zulässig. Die Vorhersagefunktion wird zur Klassifizierung verwendet.
Vergleich mit sklearn.svm.SVC
Nicht, dass dieser Vergleich viel Sinn macht, denn es handelt sich um einen extrem vereinfachten Algorithmus, der ausschließlich zum Unterrichten von Schülern entwickelt wurde, aber dennoch. Zum Testen (und um zu sehen, wie man alles benutzt) können Sie Folgendes tun (dieser Code ist auch auf dem Github verfügbar [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')
Nach dem Start werden Bilder generiert. Da der Algorithmus jedoch zufällig ausgewählt wird, unterscheiden sie sich bei jedem Start geringfügig. Hier ist ein Beispiel für die Funktionsweise des vereinfachten Algorithmus (von links nach rechts: lineare, polynomiale und rbf-Kernel)
|
|
|
Und dies ist das Ergebnis der industriellen Version der Support-Vektor-Maschine.
|
|
|
Wenn die Dimension 2 scheint zu klein, können Sie immer noch auf MNIST testen
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))
Für MNIST habe ich mehrere zufällige Klassenpaare ausprobiert (der vereinfachte Algorithmus unterstützt nur die binäre Klassifizierung), aber ich habe keinen Unterschied in der Arbeit des vereinfachten Algorithmus und von sklearn festgestellt. Die folgende Verwirrungsmatrix gibt eine Vorstellung von der Qualität.
Fazit
Vielen Dank an alle, die bis zum Ende gelesen haben. In diesem Artikel haben wir uns eine vereinfachte Tutorial-Implementierung einer Support-Vektor-Maschine angesehen. Natürlich kann es nicht mit einem Industriedesign mithalten, aber aufgrund der extremen Einfachheit und des kompakten Codes in Python sowie der Tatsache, dass alle Grundideen von SMO erhalten geblieben sind, kann diese Version von SVM durchaus ihren Platz in der Klassenzimmer. Es sollte beachtet werden, dass der Algorithmus nicht nur einfacher ist als ein sehr kniffliger Algorithmus [9] , sondern auch seine vereinfachte Version von der Stanford University [11] . Immerhin ist eine Support-Vektor-Maschine in 30 Zeilen wunderschön.
Referenzliste
- https://fbeilstein.github.io/simplest_smo_ever/
- Seite unter http://www.machinelearning.ru
- "Anfänge des maschinellen Lernens", 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