Sveučilište u Zagrebu
Fakultet elektrotehnike i računarstva

Strojno učenje 2018/2019

http://www.fer.unizg.hr/predmet/su


Laboratorijska vježba 3: Stroj potpornih vektora i algoritam k-najbližih susjeda

Verzija: 0.3
Zadnji put ažurirano: 9. studenog 2018.

(c) 2015-2017 Jan Šnajder, Domagoj Alagić

Objavljeno: 9. studenog 2018.
Rok za predaju: 3. prosinca 2018. u 07:00h


Upute

Treća laboratorijska vježba sastoji se od sedam zadataka. U nastavku slijedite upute navedene u ćelijama s tekstom. Rješavanje vježbe svodi se na dopunjavanje ove bilježnice: umetanja ćelije ili više njih ispod teksta zadatka, pisanja odgovarajućeg kôda te evaluiranja ćelija.

Osigurajte da u potpunosti razumijete kôd koji ste napisali. Kod predaje vježbe, morate biti u stanju na zahtjev asistenta (ili demonstratora) preinačiti i ponovno evaluirati Vaš kôd. Nadalje, morate razumjeti teorijske osnove onoga što radite, u okvirima onoga što smo obradili na predavanju. Ispod nekih zadataka možete naći i pitanja koja služe kao smjernice za bolje razumijevanje gradiva (nemojte pisati odgovore na pitanja u bilježnicu). Stoga se nemojte ograničiti samo na to da riješite zadatak, nego slobodno eksperimentirajte. To upravo i jest svrha ovih vježbi.

Vježbe trebate raditi samostalno. Možete se konzultirati s drugima o načelnom načinu rješavanja, ali u konačnici morate sami odraditi vježbu. U protivnome vježba nema smisla.


In [1]:
import numpy as np
import scipy as sp
import pandas as pd
import mlutils
import matplotlib.pyplot as plt
%pylab inline


Populating the interactive namespace from numpy and matplotlib

1. Klasifikator stroja potpornih vektora (SVM)

(a)

Upoznajte se s razredom svm.SVC, koja ustvari implementira sučelje prema implementaciji libsvm. Primijenite model SVC s linearnom jezgrenom funkcijom (tj. bez preslikavanja primjera u prostor značajki) na skup podataka seven (dan niže) s $N=7$ primjera. Ispišite koeficijente $w_0$ i $\mathbf{w}$. Ispišite dualne koeficijente i potporne vektore. Završno, koristeći funkciju mlutils.plot_2d_svc_problem iscrtajte podatke, decizijsku granicu i marginu. Funkcija prima podatke, oznake i klasifikator (objekt klase SVC).

Izračunajte širinu dobivene margine (prisjetite se geometrije linearnih modela).


In [2]:
from sklearn.svm import SVC

seven_X = np.array([[2,1], [2,3], [1,2], [3,2], [5,2], [5,4], [6,3]])
seven_y = np.array([1, 1, 1, 1, -1, -1, -1])

fit = SVC(gamma = 'scale', kernel = 'linear').fit(seven_X, seven_y)
mlutils.plot_2d_svc_problem(seven_X, seven_y, fit)
mlutils.plot_2d_clf_problem(seven_X, seven_y, fit.predict)


Q: Koliko iznosi širina margine i zašto?
Q: Koji primjeri su potporni vektori i zašto?

(b)

Definirajte funkciju hinge(model, x, y) koja izračunava gubitak zglobnice modela SVM na primjeru x. Izračunajte gubitke modela naučenog na skupu seven za primjere $\mathbf{x}^{(2)}=(3,2)$ i $\mathbf{x}^{(1)}=(3.5,2)$ koji su označeni pozitivno ($y=1$) te za $\mathbf{x}^{(3)}=(4,2)$ koji je označen negativno ($y=-1$). Također, izračunajte prosječni gubitak SVM-a na skupu seven. Uvjerite se da je rezultat identičan onome koji biste dobili primjenom ugrađene funkcije metrics.hinge_loss.


In [3]:
from sklearn.metrics import hinge_loss

def hinge(model, x, y): 
    return max(0, 1-y*model.decision_function(x))

x1b = np.array([[3,2], [3.5, 2], [4,2]])
y1b = np.array([1, 1, -1])


suma = 0
for i in range(0, len(seven_X)):
    suma += hinge(fit, seven_X[i].reshape(1,-1), seven_y[i])
my_hinge_loss = suma / len(seven_X)
    
print('my hinge loss: ' + str(my_hinge_loss[0]))
print('inbuild hinge loss: ' + str(hinge_loss(seven_y, fit.decision_function(seven_X))))


my hinge loss: 8.37053571429079e-05
inbuild hinge loss: 8.37053571429079e-05

(c)

Vratit ćemo se na skupove podataka outlier ($N=8$) i unsep ($N=8$) iz prošle laboratorijske vježbe (dani niže) i pogledati kako se model SVM-a nosi s njima. Naučite ugrađeni model SVM-a (s linearnom jezgrom) na ovim podatcima i iscrtajte decizijsku granicu (skupa s marginom). Također ispišite točnost modela korištenjem funkcije metrics.accuracy_score.


In [4]:
from sklearn.metrics import accuracy_score

outlier_X = np.append(seven_X, [[12,8]], axis=0)
outlier_y = np.append(seven_y, -1)

unsep_X = np.append(seven_X, [[2,2]], axis=0)
unsep_y = np.append(seven_y, -1)

In [5]:
outlier = SVC(kernel = 'linear').fit(outlier_X, outlier_y)
outlier_accuracy = accuracy_score(outlier_y, outlier.predict(outlier_X))

print('outlier acc:')
print(outlier_accuracy)

unsep = SVC(kernel = 'linear').fit(unsep_X, unsep_y)
unsep_accuracy = accuracy_score(unsep_y, unsep.predict(unsep_X))

print('unsep acc:')
print(unsep_accuracy)

figure(figsize(12,4))
subplot(1,2,1)
mlutils.plot_2d_svc_problem(outlier_X, outlier_y, outlier)
mlutils.plot_2d_clf_problem(outlier_X, outlier_y, outlier.predict)

subplot(1,2,2)
mlutils.plot_2d_svc_problem(unsep_X, unsep_y, unsep)
mlutils.plot_2d_clf_problem(unsep_X, unsep_y, unsep.predict)


outlier acc:
1.0
unsep acc:
0.875

Q: Kako stršeća vrijednost utječe na SVM?
Q: Kako se linearan SVM nosi s linearno neodvojivim skupom podataka?

2. Nelinearan SVM

Ovaj zadatak pokazat će kako odabir jezgre utječe na kapacitet SVM-a. Na skupu unsep iz prošlog zadatka trenirajte tri modela SVM-a s različitim jezgrenim funkcijama: linearnom, polinomijalnom i radijalnom baznom (RBF) funkcijom. Varirajte hiperparametar $C$ po vrijednostima $C\in\{10^{-2},1,10^2\}$, dok za ostale hiperparametre (stupanj polinoma za polinomijalnu jezgru odnosno hiperparametar $\gamma$ za jezgru RBF) koristite podrazumijevane vrijednosti. Prikažite granice između klasa (i margine) na grafikonu organiziranome u polje $3x3$, gdje su stupci različite jezgre, a retci različite vrijednosti parametra $C$.


In [6]:
C = [10**(-2), 1, 10**2]
jezgra = ['linear', 'poly', 'rbf']
k = 1

figure(figsize(12, 10))
subplots_adjust(wspace=0.1, hspace = 0.2) 

for i in C:
    for j in jezgra:
        
        uns = SVC(C = i, kernel = j, gamma='scale').fit(unsep_X, unsep_y)
        h = uns.predict

        subplot(3,3,k)
        mlutils.plot_2d_svc_problem(unsep_X, unsep_y, uns)
        mlutils.plot_2d_clf_problem(unsep_X, unsep_y, uns.predict)
        
        title(str(i) + ', ' + j);
    
        k+=1


3. Optimizacija hiperparametara SVM-a

Pored hiperparametra $C$, model SVM s jezgrenom funkcijom RBF ima i dodatni hiperparametar $\gamma=\frac{1}{2\sigma^2}$ (preciznost). Taj parametar također određuje složenost modela: velika vrijednost za $\gamma$ znači da će RBF biti uska, primjeri će biti preslikani u prostor u kojem su (prema skalarnome produktu) međusobno vrlo različiti, što će rezultirati složenijim modelima. Obrnuto, mala vrijednost za $\gamma$ znači da će RBF biti široka, primjeri će biti međusobno sličniji, što će rezultirati jednostavnijim modelima. To ujedno znači da, ako odabremo veći $\gamma$, trebamo jače regularizirati model, tj. trebamo odabrati manji $C$, kako bismo spriječili prenaučenost. Zbog toga je potrebno zajednički optimirati hiperparametre $C$ i $\gamma$, što se tipično radi iscrpnim pretraživanjem po rešetci (engl. grid search). Ovakav pristup primjenjuje se kod svih modela koji sadrže više od jednog hiperparametra.

(a)

Definirajte funkciju

grid_search(X_train, X_validate, y_train, y_validate, c_range=(c1,c2), g_range=(g1,g2), error_surface=False)

koja optimizira parametre $C$ i $\gamma$ pretraživanjem po rešetci. Funkcija treba pretražiti hiperparametre $C\in\{2^{c_1},2^{c_1+1},\dots,2^{c_2}\}$ i $\gamma\in\{2^{g_1},2^{g_1+1},\dots,2^{g_2}\}$. Funkcija treba vratiti optimalne hiperparametre $(C^*,\gamma^*)$, tj. one za koje na skupu za provjeru model ostvaruju najmanju pogrešku. Dodatno, ako je surface=True, funkcija treba vratiti matrice (tipa ndarray) pogreške modela (očekivanje gubitka 0-1) na skupu za učenje i skupu za provjeru. Svaka je matrica dimenzija $(c_2-c_1+1)\times(g_2-g_1+1)$ (retci odgovaraju različitim vrijednostima za $C$, a stupci različitim vrijednostima za $\gamma$).


In [7]:
from sklearn.metrics import accuracy_score, zero_one_loss

def grid_search(X_train, X_validate, y_train, y_validate, c_range=(0,5), g_range=(0,5), error_surface=False):
    
    # Vaš kôd ovdje
    C = []
    G = []

    for c in range(c_range[0], c_range[1] + 1):
        C.append(2**c)
    for g in range(g_range[0], g_range[1] + 1):
        G.append(2**g)
        
    err_train = 0
    err_validate = 0
    err_minimum = inf
    C_optimal = 0
    G_optimal = 0
    
    error_train_all = np.zeros((len(C), len(G)));
    error_validate_all = np.zeros((len(C), len(G)));
    
    for c in C:
        for g in G:
            svm = SVC(C=c, gamma=g).fit(X_train, y_train)
            
            h_train = svm.predict(X_train)
            err_train = zero_one_loss(y_train, h_train)
            
            h_validate = svm.predict(X_validate)
            err_validate = zero_one_loss(y_validate, h_validate)
                
            error_train_all[C.index(c)][G.index(g)] = err_train
            error_validate_all[C.index(c)][G.index(g)] = err_validate
            
            if err_validate < err_minimum:
                err_minimum = err_validate
                C_optimal = c
                G_optimal = g
                
    
    if error_surface:
        return C_optimal, G_optimal, error_train_all, error_validate_all
    else:
        return C_optimal, G_optimal

(b)

Pomoću funkcije datasets.make_classification generirajte dva skupa podataka od $N=200$ primjera: jedan s $n=2$ dimenzije i drugi s $n=1000$ dimenzija. Primjeri neka dolaze iz dviju klasa, s time da svakoj klasi odgovaraju dvije grupe (n_clusters_per_class=2), kako bi problem bio nešto složeniji, tj. nelinearniji. Neka sve značajke budu informativne. Podijelite skup primjera na skup za učenje i skup za ispitivanje u omjeru 1:1.

Na oba skupa optimirajte SVM s jezgrenom funkcijom RBF, u rešetci $C\in\{2^{-5},2^{-4},\dots,2^{15}\}$ i $\gamma\in\{2^{-15},2^{-14},\dots,2^{3}\}$. Prikažite površinu pogreške modela na skupu za učenje i skupu za provjeru, i to na oba skupa podataka (ukupno četiri grafikona) te ispišite optimalne kombinacije hiperparametara. Za prikaz površine pogreške modela možete koristiti funkciju mlutils.plot_error_surface.


In [8]:
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

In [9]:
# Vaš kôd ovdje
X1, y1 = make_classification(n_samples=200, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=2)
X1_train, X1_validate, y1_train, y1_validate = train_test_split(X1, y1, test_size = 0.5)

X2, y2 = make_classification(n_samples=200, n_features=1000, n_informative=1000, n_redundant=0, n_clusters_per_class=2)
X2_train, X2_validate, y2_train, y2_validate = train_test_split(X2, y2, test_size = 0.5)

c_range=(-5, 15)
g_range=(-15, 3)
    
C1_opt, G1_opt, err_train1, err_validate1 = grid_search(X1_train, X1_validate, y1_train, y1_validate, c_range, g_range, True)
C2_opt, G2_opt, err_train2, err_validate2 = grid_search(X2_train, X2_validate, y2_train, y2_validate, c_range, g_range, True)

print("C1 optimal:", C1_opt, "C2 optimal:", C2_opt)
print("gamma1 optimal:", G1_opt, "G2 optimal:", G2_opt)

fig = plt.figure(figsize=(12, 8))

ax = fig.add_subplot(2, 2, 1)
ax.grid()
mlutils.plot_error_surface(err_train1, c_range, g_range)
title("TRAIN1 error")

ax = fig.add_subplot(2, 2, 2)
ax.grid()
mlutils.plot_error_surface(err_train2, c_range, g_range)
title("TRAIN2 error")

ax = fig.add_subplot(2, 2, 3)
ax.grid()
mlutils.plot_error_surface(err_validate1, c_range, g_range)
title("VALIDATION1 errors")

ax = fig.add_subplot(2, 2, 4)
ax.grid()
mlutils.plot_error_surface(err_validate2, c_range, g_range)
title("VALIDATION2 errors")


C1 optimal: 512 C2 optimal: 0.03125
gamma1 optimal: 0.25 G2 optimal: 3.0517578125e-05
c:\users\ditoslav\appdata\local\programs\python\python37\lib\site-packages\matplotlib\contour.py:1243: UserWarning: No contour levels were found within the data range.
  warnings.warn("No contour levels were found"
Out[9]:
Text(0.5, 1.0, 'VALIDATION2 errors')

Q: Razlikuje li se površina pogreške na skupu za učenje i skupu za ispitivanje? Zašto?
Q: U prikazu površine pogreške, koji dio površine odgovara prenaučenosti, a koji podnaučenosti? Zašto?
Q: Kako broj dimenzija $n$ utječe na površinu pogreške, odnosno na optimalne hiperparametre $(C^*, \gamma^*)$?
Q: Preporuka je da povećanje vrijednosti za $\gamma$ treba biti popraćeno smanjenjem vrijednosti za $C$. Govore li vaši rezultati u prilog toj preporuci? Obrazložite.

4. Utjecaj standardizacije značajki kod SVM-a

U prvoj laboratorijskoj vježbi smo pokazali kako značajke različitih skala mogu onemogućiti interpretaciju naučenog modela linearne regresije. Međutim, ovaj problem javlja se kod mnogih modela pa je tako skoro uvijek bitno prije treniranja skalirati značajke, kako bi se spriječilo da značajke s većim numeričkim rasponima dominiraju nad onima s manjim numeričkim rasponima. To vrijedi i za SVM, kod kojega skaliranje nerijetko može znatno poboljšati rezultate. Svrha ovog zadataka jest eksperimentalno utvrditi utjecaj skaliranja značajki na točnost SVM-a.

Generirat ćemo dvoklasni skup od $N=500$ primjera s $n=2$ značajke, tako da je dimenzija $x_1$ većeg iznosa i većeg raspona od dimenzije $x_0$, te ćemo dodati jedan primjer koji vrijednošću značajke $x_1$ odskače od ostalih primjera:


In [10]:
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=500,n_features=2,n_classes=2,n_redundant=0,n_clusters_per_class=1, random_state=69)
X[:,1] = X[:,1]*100+1000
X[0,1] = 3000

mlutils.plot_2d_svc_problem(X, y)


(a)

Proučite funkciju za iscrtavanje histograma hist. Prikažite histograme vrijednosti značajki $x_0$ i $x_1$ (ovdje i u sljedećim zadatcima koristite bins=50).


In [11]:
figure(figsize(10, 5))
subplot(1,2,1)
hist(X[:,0], bins = 50);
subplot(1,2,2)
hist(X[:,1], bins = 50);


(b)

Proučite razred preprocessing.MinMaxScaler. Prikažite histograme vrijednosti značajki $x_0$ i $x_1$ ako su iste skalirane min-max skaliranjem (ukupno dva histograma).


In [12]:
from sklearn.preprocessing import MinMaxScaler

x0b = MinMaxScaler().fit_transform(X[:,0].reshape(-1,1), y)
x1b = MinMaxScaler().fit_transform(X[:,1].reshape(-1,1), y)

figure(figsize(10, 5))
subplot(1,2,1)
hist(x0b, bins = 50)
subplot(1,2,2)
hist(x1b, bins = 50)


Out[12]:
(array([ 10.,  89., 109.,  42.,  13.,  47.,  87.,  73.,  27.,   2.,   0.,
          0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          0.,   0.,   0.,   0.,   0.,   1.]),
 array([0.  , 0.02, 0.04, 0.06, 0.08, 0.1 , 0.12, 0.14, 0.16, 0.18, 0.2 ,
        0.22, 0.24, 0.26, 0.28, 0.3 , 0.32, 0.34, 0.36, 0.38, 0.4 , 0.42,
        0.44, 0.46, 0.48, 0.5 , 0.52, 0.54, 0.56, 0.58, 0.6 , 0.62, 0.64,
        0.66, 0.68, 0.7 , 0.72, 0.74, 0.76, 0.78, 0.8 , 0.82, 0.84, 0.86,
        0.88, 0.9 , 0.92, 0.94, 0.96, 0.98, 1.  ]),
 <a list of 50 Patch objects>)

Q: Kako radi ovo skaliranje?
Q: Dobiveni histogrami su vrlo slični. U čemu je razlika?

(c)

Proučite razred preprocessing.StandardScaler. Prikažite histograme vrijednosti značajki $x_0$ i $x_1$ ako su iste skalirane standardnim skaliranjem (ukupno dva histograma).


In [13]:
from sklearn.preprocessing import StandardScaler

x0b = StandardScaler().fit_transform(X[:,0].reshape(-1,1), y)
x1b = StandardScaler().fit_transform(X[:,1].reshape(-1,1), y)

figure(figsize(10, 5))
subplot(1,2,1)
hist(x0b, bins = 50)
subplot(1,2,2)
hist(x1b, bins = 50)


Out[13]:
(array([ 10.,  89., 109.,  42.,  13.,  47.,  87.,  73.,  27.,   2.,   0.,
          0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
          0.,   0.,   0.,   0.,   0.,   1.]),
 array([-1.43844406, -1.12241838, -0.8063927 , -0.49036702, -0.17434135,
         0.14168433,  0.45771001,  0.77373568,  1.08976136,  1.40578704,
         1.72181271,  2.03783839,  2.35386407,  2.66988975,  2.98591542,
         3.3019411 ,  3.61796678,  3.93399245,  4.25001813,  4.56604381,
         4.88206948,  5.19809516,  5.51412084,  5.83014652,  6.14617219,
         6.46219787,  6.77822355,  7.09424922,  7.4102749 ,  7.72630058,
         8.04232625,  8.35835193,  8.67437761,  8.99040329,  9.30642896,
         9.62245464,  9.93848032, 10.25450599, 10.57053167, 10.88655735,
        11.20258302, 11.5186087 , 11.83463438, 12.15066006, 12.46668573,
        12.78271141, 13.09873709, 13.41476276, 13.73078844, 14.04681412,
        14.36283979]),
 <a list of 50 Patch objects>)

Q: Kako radi ovo skaliranje?
Q: Dobiveni histogrami su vrlo slični. U čemu je razlika?

(d)

Podijelite skup primjera na skup za učenje i skup za ispitivanje u omjeru 1:1. Trenirajte SVM s jezgrenom funkcijom RBF na skupu za učenje i ispitajte točnost modela na skupu za ispitivanje, koristeći tri varijante gornjeg skupa: neskalirane značajke, standardizirane značajke i min-max skaliranje. Koristite podrazumijevane vrijednosti za $C$ i $\gamma$. Izmjerite točnost svakog od triju modela na skupu za učenje i skupu za ispitivanje. Ponovite postupak više puta (npr. 30) te uprosječite rezultate (u svakom ponavljanju generirajte podatke kao što je dano na početku ovog zadatka).

NB: Na skupu za učenje treba najprije izračunati parametre skaliranja te zatim primijeniti skaliranje (funkcija fit_transform), dok na skupu za ispitivanje treba samo primijeniti skaliranje s parametrima koji su dobiveni na skupu za učenje (funkcija transform).


In [14]:
from sklearn.model_selection import train_test_split

err_unscaled = []
err_std = []
err_minmax = []

for i in range(0, 30):
    
    X_train, X_validate, y_train, y_validate = train_test_split(X, y, test_size = 0.5)
    
    model_unscaled = SVC(gamma = 'scale').fit(X_train, y_train)
    prediction_unscaled = model_unscaled.predict(X_validate)
    err_unscaled.append(accuracy_score(y_validate, prediction_unscaled))
    
    ss = StandardScaler()
    X_std_train = ss.fit_transform(X_train)
    X_std_valid = ss.transform(X_validate)
    model_standard = SVC(gamma = 'scale').fit(X_std_train, y_train)
    prediction_standard = model_standard.predict(X_std_valid)
    err_std.append(accuracy_score(y_validate, prediction_standard))
    
    mm = MinMaxScaler()
    X_minmax_train = mm.fit_transform(X_train) 
    X_minmax_valid = mm.transform(X_validate) 
    model_minmax = SVC(gamma = 'scale').fit(X_minmax_train, y_train) 
    prediction_minmax = model_minmax.predict(X_minmax_valid)
    err_minmax.append(accuracy_score(y_validate, prediction_minmax))

print('Unscaled')
print(mean(err_unscaled))
print('Std')
print(mean(err_std))
print('Min max')
print(mean(err_minmax))


Unscaled
0.9932
Std
0.99
Min max
0.9729333333333333

Q: Jesu li rezultati očekivani? Obrazložite.
Q: Bi li bilo dobro kada bismo funkciju fit_transform primijenili na cijelom skupu podataka? Zašto? Bi li bilo dobro kada bismo tu funkciju primijenili zasebno na skupu za učenje i zasebno na skupu za ispitivanje? Zašto?

5. Algoritam k-najbližih susjeda

U ovom zadatku promatrat ćemo jednostavan klasifikacijski model imena algoritam k-najbližih susjeda. Najprije ćete ga samostalno isprogramirati kako biste se detaljno upoznali s radom ovog modela, a zatim ćete prijeći na analizu njegovih hiperparametara (koristeći ugrađeni razred, radi efikasnosti).

(a)

Implementirajte klasu KNN, koja implementira algoritam $k$ najbližih susjeda. Neobavezan parametar konstruktora jest broj susjeda n_neighbours ($k$), čija je podrazumijevana vrijednost 3. Definirajte metode fit(X, y) i predict(X), koje služe za učenje modela odnosno predikciju. Kao mjeru udaljenosti koristite euklidsku udaljenost (numpy.linalg.norm; pripazite na parametar axis). Nije potrebno implementirati nikakvu težinsku funkciju.


In [15]:
from numpy.linalg import norm
from collections import Counter

class KNN(object):
    def __init__(self, n_neighbors=3):
        self.n_neighbors = n_neighbors
        self.domain = []
        
    def fit(self, X_train, y_train):
        for x,y in zip(X_train, y_train):
            self.domain.append([x, y])
        return self.domain
        
    def predict(self, X_test):
        pred = []
        for x in X_test:
            dist = []
            y = []
            counter = []
            for xd, yd in self.domain:
                dist.append(norm(x-xd))
                y.append(yd)
            
            dat = sorted(zip(dist, y))[0: self.n_neighbors]
            
            for i in range(0, self.n_neighbors):
                counter.append(dat[i][1])
                
            pred.append((Counter(counter).most_common()[0])[0])

        return pred

(b)

Kako biste se uvjerili da je Vaša implementacija ispravna, usporedite ju s onom u razredu neighbors.KNeighborsClassifier. Budući da spomenuti razred koristi razne optimizacijske trikove pri pronalasku najboljih susjeda, obavezno postavite parametar algorithm=brute, jer bi se u protivnom moglo dogoditi da Vam se predikcije razlikuju. Usporedite modele na danom (umjetnom) skupu podataka (prisjetite se kako se uspoređuju polja; numpy.all).


In [16]:
from sklearn.datasets import make_classification
X_art, y_art = make_classification(n_samples=100, n_features=2, n_classes=2, 
                                   n_redundant=0, n_clusters_per_class=2,
                                   random_state=69)
mlutils.plot_2d_clf_problem(X_art, y_art)



In [17]:
from sklearn.neighbors import KNeighborsClassifier

knn = KNN()
knn.fit(X_art, y_art)
predict_knn = knn.predict(X_art)

builtin = KNeighborsClassifier(algorithm = 'brute', n_neighbors = 3).fit(X_art, y_art)
predict_knc = builtin.predict(X_art)

print('diff')
print(norm(predict_knn - predict_knc))


diff
0.0

6. Analiza algoritma k-najbližih susjeda

Algoritam k-nn ima hiperparametar $k$ (broj susjeda). Taj hiperparametar izravno utječe na složenost algoritma, pa je stoga izrazito važno dobro odabrati njegovu vrijednost. Kao i kod mnogih drugih algoritama, tako i kod algoritma k-nn optimalna vrijednost hiperametra $k$ ovisi o konkretnom problemu, uključivo broju primjera $N$, broju značajki (dimenzija) $n$ te broju klasa $K$.

Kako bismo dobili pouzdanije rezultate, potrebno je neke od eksperimenata ponoviti na različitim skupovima podataka i zatim uprosječiti dobivene vrijednosti pogrešaka. Koristite funkciju: mlutils.knn_eval koja trenira i ispituje model k-najbližih susjeda na ukupno n_instances primjera, i to tako da za svaku vrijednost hiperparametra iz zadanog intervala k_range ponovi n_trials mjerenja, generirajući za svako od njih nov skup podataka i dijeleći ga na skup za učenje i skup za ispitivanje. Udio skupa za ispitivanje definiran je parametrom test_size. Povratna vrijednost funkcije jest četvorka (ks, best_k, train_errors, test_errors). Vrijednost best_k je optimalna vrijednost hiperparametra $k$ (vrijednost za koju je pogreška na skupu za ispitivanje najmanja). Vrijednosti train_errors i test_errors liste su pogrešaka na skupu za učenja odnosno skupu za testiranje za sve razmatrane vrijednosti hiperparametra $k$, dok ks upravo pohranjuje sve razmatrane vrijednosti hiperparametra $k$.

(a)

Na podatcima iz zadatka 5, pomoću funkcije mlutils.plot_2d_clf_problem iscrtajte prostor primjera i područja koja odgovaraju prvoj odnosno drugoj klasi. Ponovite ovo za $k\in[1, 5, 20, 100]$.

NB: Implementacija algoritma KNeighborsClassifier iz paketa scikit-learn vjerojatno će raditi brže od Vaše implementacije, pa u preostalim eksperimentima koristite nju.


In [18]:
def knn_eval(n_instances=100, n_features=2, n_classes=2, n_informative=2, test_size=0.3, k_range=(1, 20), n_trials=100):
    
    best_k = 0; train_errors = []; test_errors = [];
    
    for k in range(k_range[0],k_range[1]+1):
        
        train_error = []
        test_error = []
        
        for j in range(0, n_trials):
            
            X, y = make_classification(n_samples=n_instances, n_features=n_features, n_classes=n_classes, n_informative=n_informative, n_redundant=0, n_clusters_per_class=1)
            X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = test_size)
            
            mod = KNeighborsClassifier(algorithm = 'brute', n_neighbors = k).fit(X_train, y_train)
            prediction_train = mod.predict(X_train)
            prediction_test = mod.predict(X_test)
            
            train_error.append(zero_one_loss(y_train, prediction_train))
            test_error.append(zero_one_loss(y_test, prediction_test))
            
        train_errors.append(mean(train_error))
        test_errors.append(mean(test_error))
        
    best_k = k_range[0] + test_errors.index(min(test_errors))
    
    return (best_k, train_errors, test_errors)

Q: Kako $k$ utječe na izgled granice između klasa?
Q: Kako se algoritam ponaša u ekstremnim situacijama: $k=1$ i $k=100$?

(b)

Pomoću funkcije mlutils.knn_eval, iscrtajte pogreške učenja i ispitivanja kao funkcije hiperparametra $k\in\{1,\dots,20\}$, za $N=\{100, 500, 1000, 3000\}$ primjera. Načinite 4 zasebna grafikona (generirajte ih u 2x2 polju). U svakoj iteraciji ispišite optimalnu vrijednost hiperparametra $k$ (najlakše kao naslov grafikona; vidi plt.title).


In [21]:
hiperparams = range(1, 21)
N = [100, 500, 1000, 3000]

figure(figsize(11, 8))

i = 1
for n in N:
    [k, err_tr, err_tst] = knn_eval(n_instances=n, k_range=(1, 20))
    
    subplot(2,2,i)
    plot(np.array(range(1,21)), err_tr)
    plot(np.array(range(1,21)), err_tst)
    scatter(k,err_tst[hiperparams.index(k)])
    legend(['train error', 'test error', 'min test err'], loc = 'best', prop={'size':10})
    title('\nN = ' + str(n) + '\nk = ' + str(k))
    xticks(rng)
    grid()
    
    i+=1


Q: Kako se mijenja optimalna vrijednost hiperparametra $k$ s obzirom na broj primjera $N$? Zašto?
Q: Kojem području odgovara prenaučenost, a kojem podnaučenost modela? Zašto?
Q: Je li uvijek moguće doseći pogrešku od 0 na skupu za učenje?

(c)

Kako bismo provjerili u kojoj je mjeri algoritam k-najbližih susjeda osjetljiv na prisustvo nebitnih značajki, možemo iskoristiti funkciju datasets.make_classification kako bismo generirali skup primjera kojemu su neke od značajki nebitne. Naime, parametar n_informative određuje broj bitnih značajki, dok parametar n_features određuje ukupan broj značajki. Ako je n_features > n_informative, onda će neke od značajki biti nebitne. Umjesto da izravno upotrijebimo funkciju make_classification, upotrijebit ćemo funkciju mlutils.knn_eval, koja samo preuzime ove parametre, ali nam omogućuje pouzdanije procjene.

Koristite funkciju mlutils.knn_eval na dva načina. U oba koristite $N=1000$ primjera, $n=10$ značajki i $K=5$ klasa, ali za prvi neka su svih 10 značajki bitne, a za drugi neka je bitno samo 5 od 10 značajki. Ispišite pogreške učenja i ispitivanja za oba modela za optimalnu vrijednost $k$ (vrijednost za koju je ispitna pogreška najmanja).


In [ ]:

Q: Je li algoritam k-najbližih susjeda osjetljiv na nebitne značajke? Zašto?
Q: Je li ovaj problem izražen i kod ostalih modela koje smo dosad radili (npr. logistička regresija)?
Q: Kako bi se model k-najbližih susjeda ponašao na skupu podataka sa značajkama različitih skala? Detaljno pojasnite.

7. "Prokletstvo dimenzionalnosti"

"Prokletstvo dimenzionalnosti" zbirni je naziv za niz fenomena povezanih s visokodimenzijskim prostorima. Ti fenomeni, koji se uglavnom protive našoj intuiciji, u većini slučajeva dovode do toga da se s porastom broja dimenzija (značajki) smanjenje točnost modela.

Općenito, povećanje dimenzija dovodi do toga da sve točke u ulaznome prostoru postaju (u smislu euklidske udaljenosti) sve udaljenije jedne od drugih te se, posljedično, gube razlike u udaljenostima između točaka. Eksperimentalno ćemo provjeriti da je to doista slučaj. Proučite funkciju metrics.pairwise.pairwise_distances. Generirajte 100 slučajnih vektora u različitim dimenzijama $n\in[1,2,\ldots,50]$ dimenzija te izračunajte prosječnu euklidsku udaljenost između svih parova tih vektora. Za generiranje slučajnih vektora koristite funkciju numpy.random.random. Na istom grafu skicirajte krivulje prosječnih udaljenosti za euklidsku i kosinusnu udaljenost (parametar metric).


In [27]:
from sklearn.metrics.pairwise import pairwise_distances
from numpy.random import random, random_sample

dims = []
vals = []
cosine = []

for dim in range(1, 50):
    arrs = random_sample((50, dim))
    arrs2 = random_sample((50, dim))
    # print(arrs)
    dists = norm(pairwise_distances(X=arrs, Y=arrs2))
    #print(dists)
    dims.append(dim)
    vals.append(dists)
    dists = norm(pairwise_distances(X=arrs, Y=arrs2, metric='cosine'))
    cosine.append(dists)

#print(dims)
#print(vals)
    
plot(dims, vals)
plot(dims, cosine)
grid()


Q: Pokušajte objasniti razlike u rezultatima. Koju biste od ovih dviju mjera koristili za klasifikaciju visokodimenzijskih podataka?
Q: Zašto je ovaj problem osobito izražen kod algoritma k-najbližih susjeda?