Ga naar hoofdinhoud

Support Vector Machines in R

In deze tutorial krijg je op hoofdlijnen inzicht in hoe SVM’s werken en implementeer je ze vervolgens met R.
· 12 min lezen

Verken met AI

In machine learning zijn support vector machines supervisede leermodellen met bijbehorende algoritmes die data analyseren voor classificatie en regressie. Meestal worden ze echter gebruikt voor classificatieproblemen. In deze tutorial proberen we op hoofdlijnen te begrijpen hoe SVM’s werken en implementeren we ze daarna in R. Ik richt me op intuïtie in plaats van strikt formalisme. Dat betekent in feite dat we zoveel mogelijk wiskunde overslaan en een sterke intuïtie opbouwen voor het werkingsprincipe.

Support Vector Machines-algoritme

Lineaire data

De basis van Support Vector Machines en hoe ze werken, begrijp je het best met een eenvoudig voorbeeld. Stel dat we twee labels hebben: rood en blauw, en onze data heeft twee features: x en y. We willen een classifier die, gegeven een paar (x,y)-coördinaten, aangeeft of het rood of blauw is. We plotten onze al gelabelde trainingsdata in een vlak:

grafiek

Een support vector machine neemt deze punten en geeft de hypervlakte (in twee dimensies is dat gewoon een lijn) die de labels het best scheidt. Deze lijn is de beslissingsgrens: alles aan de ene kant ervan classificeren we als blauw, en alles aan de andere kant als rood.

hypervlak

Maar wat is nu precies het beste hypervlak? Voor SVM is dat degene die de marges ten opzichte van beide labels maximaliseert. Met andere woorden: het hypervlak (in dit geval dus een lijn) waarvan de afstand tot het dichtstbijzijnde element van elk label het grootst is.

hypervlak

Niet-lineaire data

Het bovenstaande voorbeeld was eenvoudig omdat de data duidelijk lineair te scheiden was — we konden een rechte lijn trekken om rood en blauw te scheiden. Helaas is het meestal niet zo simpel. Kijk maar eens naar dit geval:

Niet-lineaire data

Het is vrij duidelijk dat er geen lineaire beslissingsgrens is (één enkele rechte lijn die beide labels scheidt). Toch zijn de vectoren heel duidelijk gescheiden, en lijkt het alsof je ze makkelijk zou moeten kunnen scheiden.

Dus dit is wat we doen: we voegen een derde dimensie toe. Tot nu toe hadden we twee dimensies: $x$ en $y$. We creëren een nieuwe z-dimensie, en we bepalen dat die op een voor ons handige manier wordt berekend: $z = x² + y²$ (je ziet dat dit de vergelijking van een cirkel is).

Dit levert een driedimensionale ruimte op. Neem je daar een doorsnede van, dan ziet het er zo uit:

Niet-lineaire data

Wat kan SVM hiermee? Laten we kijken:

Niet-lineaire data

Mooi! Let op: omdat we nu in drie dimensies zitten, is het hypervlak een vlak dat evenwijdig loopt aan de $x$-as bij een bepaalde $z$ (zeg $z = 1$).

Wat rest is dit terug te projecteren naar twee dimensies:

Niet-lineaire data

En daar is het! Onze beslissingsgrens is een omtrek met straal 1, die beide labels scheidt met SVM.

Kerneltruc

In het bovenstaande voorbeeld vonden we een manier om niet-lineaire data te classificeren door onze ruimte slim naar een hogere dimensie te mappen. Het berekenen van die transformatie kan echter behoorlijk rekenintensief worden: er kunnen veel nieuwe dimensies zijn, elk met mogelijk complexe berekeningen. Dit voor elke vector in de dataset doen, kost veel werk, dus een goedkopere oplossing zou fijn zijn.

Hier is een truc: SVM heeft de daadwerkelijke vectoren niet nodig om zijn werk te doen; het kan volstaan met de inproducten tussen die vectoren. Dit betekent dat we de dure berekeningen van de nieuwe dimensies kunnen omzeilen! Dit is wat we in plaats daarvan doen:

  • Stel je de nieuwe ruimte voor die we willen:

vergelijking

  • Bepaal hoe het inproduct er in die ruimte uitziet:

vergelijking

  • Laat SVM zijn werk doen, maar dan met het nieuwe inproduct — we noemen dit een kernelfunctie.

Dit staat bekend als de kerneltruc, die de feature space vergroot om een niet-lineaire grens tussen klassen mogelijk te maken. Veelgebruikte typen kernels om niet-lineaire data te scheiden zijn polynomiale kernels, radial basis-kernels en lineaire kernels (die overeenkomen met support vector-classifiers). Simpel gezegd transformeren deze kernels onze data zodat er een lineair hypervlak doorheen past en zo classificeren we de data.

Voordelen en nadelen

Laten we nu kijken naar enkele voordelen en nadelen van SVM:

Voordelen

  • Hoge dimensionaliteit: SVM is effectief in hoog-dimensionale ruimtes, wat met name van toepassing is op documentclassificatie en sentimentanalyse, waar de dimensionaliteit extreem groot kan zijn.

  • Geheugenefficiëntie: Omdat alleen een subset van de trainingspunten wordt gebruikt in het daadwerkelijke beslissingsproces voor nieuwe leden, hoeven bij het nemen van beslissingen alleen deze punten in het geheugen te worden opgeslagen (en op te worden gerekend).

  • Veelzijdigheid: Klassenscheiding is vaak sterk niet-lineair. De mogelijkheid om nieuwe kernels toe te passen biedt veel flexibiliteit voor de beslissingsgrenzen, wat leidt tot betere classificatieprestaties.

Nadelen

  • Selectie van kernelparameters: SVM’s zijn erg gevoelig voor de keuze van de kernelparameters. In situaties waarin het aantal features per object groter is dan het aantal trainingsvoorbeelden, kunnen SVM’s slecht presteren. Intuïtief kun je dit zo zien: als de hoog-dimensionale feature space veel groter is dan het aantal voorbeelden, zijn er minder effectieve support vectors om de optimale lineaire hypervlakken op te steunen, wat tot slechtere classificatieprestaties leidt wanneer nieuwe, ongeziene voorbeelden worden toegevoegd.

  • Niet-probabilistisch: Omdat de classifier werkt door objecten boven en onder een classificerend hypervlak te plaatsen, is er geen directe probabilistische interpretatie voor groepslidmaatschap. Een mogelijke maatstaf voor de "effectiviteit" van de classificatie is echter hoe ver het nieuwe punt van de beslissingsgrens af ligt.

Support Vector Machines in R

Lineaire SVM-classifier

Laten we eerst wat data in 2 dimensies genereren en ze een beetje scheiden. Na het zetten van de random seed maak je een matrix x, normaal verdeeld met 20 observaties in 2 klassen op 2 variabelen. Vervolgens maak je een y-variabele, die -1 of 1 wordt, met 10 in elke klasse. Voor y = 1 verschuif je de gemiddelden van 0 naar 1 in elk van de coördinaten. Tot slot kun je de data plotten en de punten kleuren op basis van hun respons. Het plotkarakter 19 geeft je mooie, goed zichtbare punten in blauw of rood, afhankelijk van of de respons 1 of -1 is.

set.seed(10111)
x = matrix(rnorm(40), 20, 2)
y = rep(c(-1, 1), c(10, 10))
x[y == 1,] = x[y == 1,] + 1
plot(x, col = y + 3, pch = 19)

Nu laad je het pakket e1071, dat de functie svm bevat (denk eraan het pakket te installeren als je dat nog niet hebt gedaan).

library(e1071)

Nu maak je een dataframe van de data en zet je y om naar een factorvariabele. Daarna roep je svm aan op dit dataframe, met y als responsvariabele en de andere variabelen als voorspellers. Het dataframe pakt de matrix x uit in 2 kolommen met de namen x1 en x2. Je vertelt SVM dat de kernel lineair is, de afstemmingsparameter cost 10 is, en scale false is. In dit voorbeeld vraag je dus om de variabelen niet te standaardiseren.

dat = data.frame(x, y = as.factor(y))
svmfit = svm(y ~ ., data = dat, kernel = "linear", cost = 10, scale = FALSE)
print(svmfit)

Het printen van svmfit geeft de samenvatting. Je ziet dat het aantal support vectors 6 is — dat zijn de punten die dicht bij de grens liggen of aan de verkeerde kant ervan.

Er is een plotfunctie voor SVM die de beslissingsgrens laat zien, zoals je hieronder ziet. Het lijkt erop dat je weinig controle hebt over de kleuren. Het wijkt af van de conventie, omdat het x2 op de horizontale as zet en x1 op de verticale as.

plot(svmfit, dat)
SVM

Laten we proberen zelf een plot te maken. Het eerste wat je doet, is een rooster van waarden maken voor x1 en x2 dat het hele domein dekt op een vrij fijn raster. Daarvoor maak je een functie make.grid. Die neemt je datamatrix x, plus een argument n dat het aantal punten per richting aangeeft. Hier vraag je om een rooster van 75 x 75.

In deze functie gebruik je de apply-functie om het bereik van elk van de variabelen in x te krijgen. Vervolgens gebruik je voor zowel x1 als x2 de functie seq om van de laagste naar de hoogste waarde te gaan en zo een rooster van lengte n te maken. Nu heb je x1 en x2, elk met 75 gelijkmatig verdeelde waarden op elk van de coördinaten. Tot slot gebruik je de functie expand.grid, die x1 en x2 pakt en het raster maakt.

make.grid = function(x, n = 75) {
  grange = apply(x, 2, range)
  x1 = seq(from = grange[1,1], to = grange[2,1], length = n)
  x2 = seq(from = grange[1,2], to = grange[2,2], length = n)
  expand.grid(X1 = x1, X2 = x2)
}

Nu kun je de functie make.grid toepassen op x. Laten we naar de eerste paar waarden van het raster kijken, van 1 tot en met 10.

xgrid = make.grid(x)
xgrid[1:10,]

Zoals je ziet, loopt het raster eerst door de 1e coördinaat, terwijl de 2e coördinaat vastgehouden wordt.

Met het raster gemaakt, ga je nu op elk punt in het raster een voorspelling doen. Met de nieuwe data xgrid gebruik je predict en noem je de respons ygrid. Daarna plot en kleur je de punten volgens de classificatie, zodat de beslissingsgrens duidelijk is. Laten we de originele punten ook in deze plot zetten met de functie points.

svmfit heeft een component index die aangeeft welke punten support vectors zijn. Je voegt ze toe aan de plot door nogmaals de functie points te gebruiken.

ygrid = predict(svmfit, xgrid)
plot(xgrid, col = c("red","blue")[as.numeric(ygrid)], pch = 20, cex = .2)
points(x, col = y + 3, pch = 19)
points(x[svmfit$index,], pch = 5, cex = 2)
plot

Zoals je in de plot ziet, liggen de punten in de vakjes dicht bij de beslissingsgrens en zijn ze bepalend voor die grens.

Helaas is de functie svm niet heel vriendelijk, want je moet wat werk doen om de lineaire coëfficiënten terug te krijgen. De reden is waarschijnlijk dat dit alleen zinvol is voor lineaire kernels, en de functie algemener is. Laten we daarom een formule gebruiken om de coëfficiënten efficiënter te extraheren. Je extraheert beta en beta0, de lineaire coëfficiënten.

beta = drop(t(svmfit$coefs)%*%x[svmfit$index,])
beta0 = svmfit$rho

Nu kun je de punten op het raster opnieuw plotten, vervolgens de punten terugzetten (inclusief de support vector-punten). Daarna kun je de coëfficiënten gebruiken om de beslissingsgrens te tekenen met een eenvoudige vergelijking van de vorm:

Op basis van die vergelijking moet je een helling en een intercept voor de beslissingsgrens bepalen. Vervolgens kun je de functie abline gebruiken met die twee argumenten. De daaropvolgende twee abline-aanroepen stellen respectievelijk de bovenste en onderste marge van de beslissingsgrens voor.

plot(xgrid, col = c("red", "blue")[as.numeric(ygrid)], pch = 20, cex = .2)
points(x, col = y + 3, pch = 19)
points(x[svmfit$index,], pch = 5, cex = 2)
abline(beta0 / beta[2], -beta[1] / beta[2])
abline((beta0 - 1) / beta[2], -beta[1] / beta[2], lty = 2)
abline((beta0 + 1) / beta[2], -beta[1] / beta[2], lty = 2)
plot

Je ziet duidelijk dat sommige support vectors exact op de marge liggen, terwijl andere binnen de marge liggen.

Niet-lineaire SVM-classifier

Dat was de lineaire SVM in de vorige sectie. Laten we nu doorgaan naar de niet-lineaire versie van SVM. Je bekijkt een voorbeeld uit het boek Elements of Statistical Learning, met een canoniek voorbeeld in 2 dimensies waar de beslissingsgrens niet-lineair is. Je gaat de kernel support vector machine gebruiken om die grens te leren.

Eerst haal je de data voor dat voorbeeld uit het boek door ze direct te downloaden van deze URL, de webpagina waar de data staan. De data zijn gemengd en gesimuleerd. Daarna kun je de kolomnamen inspecteren.

load(file = "ESL.mixture.rda")
names(ESL.mixture)

Voor nu zijn de trainingsdata x en y. Je had al een x en y aangemaakt voor het vorige voorbeeld. Laten we die dus verwijderen, zodat je deze nieuwe data kunt attachen.

rm(x, y)
attach(ESL.mixture)

De data zijn ook 2-dimensionaal. Laten we ze plotten om een goed beeld te krijgen.

plot(x, col = y + 1)
Niet-lineaire SVM-classifier

De data overlappen behoorlijk, maar je ziet dat er iets bijzonders in de structuur zit. Laten we nu een dataframe maken met de respons y, en die omzetten naar een factor. Daarna kun je een SVM met radial kernel fitten en cost op 5 zetten.

dat = data.frame(y = factor(y), x)
fit = svm(factor(y) ~ ., data = dat, scale = FALSE, kernel = "radial", cost = 5)

Tijd om een raster te maken en je voorspellingen te doen. Deze data werden eigenlijk meegeleverd met rasterpunten. Als je in de samenvatting naar de namen in de lijst kijkt, zijn er 2 variabelen px1 en px2, het raster van waarden voor elk van die variabelen. Je kunt expand.grid gebruiken om het raster van waarden te maken. Daarna voorspel je de classificatie op elk van de waarden in het raster.

xgrid = expand.grid(X1 = px1, X2 = px2)
ygrid = predict(fit, xgrid)

Tot slot plot je de punten en kleur je ze volgens de beslissingsgrens. Je ziet dat de beslissingsgrens niet-lineair is. Je kunt de datapunten ook in de plot zetten om te zien waar ze liggen.

plot(xgrid, col = as.numeric(ygrid), pch = 20, cex = .2)
points(x, col = y + 1, pch = 19)
Niet-lineaire SVM-classifier

De beslissingsgrens volgt in grote lijnen waar de data liggen, maar op een sterk niet-lineaire manier.

Laten we kijken of je deze plot nog iets kunt verbeteren en de predict-functie de daadwerkelijke functieschatting op elk rasterpunt kunt laten produceren. Je wilt in het bijzonder een curve toevoegen die de beslissingsgrens geeft met behulp van de functie contour. In het dataframe staat ook een variabele prob, de ware kans op klasse 1 voor deze data, op de rasterpunten. Als je de 0,5-contour daarvan plot, krijg je de Bayes-beslissingsgrens, de best mogelijke grens.

Eerst voorspel je je fit op het raster. Je geeft decision values gelijk aan TRUE door, omdat je de daadwerkelijke functie wilt, niet alleen de classificatie. Het retourneert een attribuut van de geclassificeerde waarden, dus je moet dat attribuut eruit halen. Daarna pak je degene die decision heet.

Vervolgens kun je dezelfde stappen als hierboven volgen om het raster te creëren, de voorspellingen te doen en de punten te plotten.

Dan is het tijd om de functie contour te gebruiken. Die vereist de 2 rasterreeksen, een functie en 2 argumenten level en add. Je wilt de functie in de vorm van een matrix, met de afmetingen van px1 en px2 (respectievelijk 69 en 99). Je zet level op 0 en voegt die toe aan de plot. Je ziet dat de contour de beslissingsgrens volgt — een handige manier om een niet-lineaire beslissingsgrens in 2 dimensies te plotten.

Tot slot voeg je de waarheid toe, namelijk de contour van de waarschijnlijkheden. Dat is de 0,5-contour, oftewel de beslissingsgrens in termen van waarschijnlijkheden (ook bekend als de Bayes-beslissingsgrens).

func = predict(fit, xgrid, decision.values = TRUE)
func = attributes(func)$decision

xgrid = expand.grid(X1 = px1, X2 = px2)
ygrid = predict(fit, xgrid)
plot(xgrid, col = as.numeric(ygrid), pch = 20, cex = .2)
points(x, col = y + 1, pch = 19)

contour(px1, px2, matrix(func, 69, 99), level = 0, add = TRUE)
contour(px1, px2, matrix(func, 69, 99), level = 0.5, add = TRUE, col = "blue", lwd = 2)
Niet-lineaire SVM-classifier

Je ziet dat je niet-lineaire SVM behoorlijk dicht in de buurt komt van de Bayes-beslissingsgrens.

Conclusie

Samengevat: Support Vector Machines zijn een subklasse van supervisede classifiers die proberen een feature space op te delen in twee of meer groepen. Ze doen dit door een optimale manier te vinden om zulke groepen te scheiden op basis van hun bekende klasselabels:

  • In eenvoudigere gevallen is de scheidings"grens" lineair, wat leidt tot groepen die worden gescheiden door lijnen (of vlakken) in hoog-dimensionale ruimtes.
  • In complexere gevallen (waar groepen niet netjes door lijnen of vlakken te scheiden zijn) kunnen SVM’s niet-lineaire opsplitsing uitvoeren. Dit gebeurt met behulp van een kernelfunctie.
  • Dit maakt het uiteindelijk zeer geavanceerde en capabele classifiers, maar met het gebruikelijke risico dat ze vatbaar kunnen zijn voor overfitting.

Persoonlijk vind ik SVM’s geweldige classifiers voor specifieke situaties waarin de groepen duidelijk gescheiden zijn. Ze doen het ook erg goed wanneer je data niet-lineair gescheiden is. Je kunt data transformeren om het lineair te scheiden, of je laat SVM’s de data transformeren en de twee klassen direct lineair scheiden. Dit is een van de belangrijkste redenen om SVM’s te gebruiken. Je hoeft niet zelf niet-lineaire data te transformeren. Een nadeel van SVM’s is het blackbox-karakter van deze functies. Het gebruik van kernels om niet-lineaire data te scheiden, maakt ze lastig (zo niet onmogelijk) te interpreteren. Als je ze begrijpt, heb je een alternatief voor GLM’s en beslisbomen voor classificatie. Ik hoop dat deze tutorial je een breder beeld geeft van het SVM-landschap en je helpt deze machines beter te begrijpen.

Wil je meer leren over R? Volg dan DataCamp’s cursus Machine Learning Toolbox.

Bekijk ook onze Support Vector Machines met Scikit-learn-tutorial.

Onderwerpen

Gerelateerd