# Učitaj osnovne biblioteke...
import numpy as np
import sklearn
import mlutils
import matplotlib.pyplot as plt
%pylab inline

"""
Explanation: Sveučilište u Zagrebu
Fakultet elektrotehnike i računarstva  
Strojno učenje 2019/2020
http://www.fer.unizg.hr/predmet/su

Laboratorijska vježba 2: Linearni diskriminativni modeli
Verzija: 1.3
Zadnji put ažurirano: 27. rujna 2019.
(c) 2015-2019 Jan Šnajder, Domagoj Alagić  
Objavljeno: 30. rujna 2019.
Rok za predaju: 4. studenog 2019. u 07:00h

Upute
Prva laboratorijska vježba sastoji se od šest 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.
End of explanation
"""


from sklearn.linear_model import LinearRegression, RidgeClassifier
from sklearn.metrics import accuracy_score

"""
Explanation: Zadatci
1. Linearna regresija kao klasifikator
U prvoj laboratorijskoj vježbi koristili smo model linearne regresije za, naravno, regresiju. Međutim, model linearne regresije može se koristiti i za klasifikaciju. Iako zvuči pomalo kontraintuitivno, zapravo je dosta jednostavno. Naime, cilj je naučiti funkciju $f(\mathbf{x})$ koja za negativne primjere predviđa vrijednost $1$, dok za pozitivne primjere predviđa vrijednost $0$. U tom slučaju, funkcija $f(\mathbf{x})=0.5$ predstavlja granicu između klasa, tj. primjeri za koje vrijedi $h(\mathbf{x})\geq 0.5$ klasificiraju se kao pozitivni, dok se ostali klasificiraju kao negativni.
Klasifikacija pomoću linearne regresije implementirana je u razredu RidgeClassifier. U sljedećim podzadatcima istrenirajte taj model na danim podatcima i prikažite dobivenu granicu između klasa. Pritom isključite regularizaciju ($\alpha = 0$, odnosno alpha=0). Također i ispišite točnost vašeg klasifikacijskog modela (smijete koristiti funkciju metrics.accuracy_score). Skupove podataka vizualizirajte korištenjem pomoćne funkcije plot_clf_problem(X, y, h=None) koja je dostupna u pomoćnom paketu mlutils (datoteku mlutils.py možete preuzeti sa stranice kolegija). X i y predstavljaju ulazne primjere i oznake, dok h predstavlja funkciju predikcije modela (npr. model.predict). 
U ovom zadatku cilj je razmotriti kako se klasifikacijski model linearne regresije ponaša na linearno odvojim i neodvojivim podatcima.
End of explanation
"""


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, 0, 0, 0])

# Vaš kôd ovdje

"""
Explanation: (a)
Prvo, isprobajte ugrađeni model na linearno odvojivom skupu podataka seven ($N=7$).
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: Kako bi se uvjerili da se u isprobanoj implementaciji ne radi o ničemu doli o običnoj linearnoj regresiji, napišite kôd koji dolazi do jednakog rješenja korištenjem isključivo razreda LinearRegression. Funkciju za predikciju, koju predajete kao treći argument h funkciji plot_2d_clf_problem, možete definirati lambda-izrazom: lambda x : model.predict(x) &gt;= 0.5.
End of explanation
"""


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

# Vaš kôd ovdje

"""
Explanation: Q: Kako bi bila definirana granica između klasa ako bismo koristili oznake klasa $-1$ i $1$ umjesto $0$ i $1$?
(b)
Probajte isto na linearno odvojivom skupu podataka outlier ($N=8$):
End of explanation
"""


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

# Vaš kôd ovdje

"""
Explanation: Q: Zašto model ne ostvaruje potpunu točnost iako su podatci linearno odvojivi?
(c)
Završno, probajte isto na linearno neodvojivom skupu podataka unsep ($N=8$):
End of explanation
"""


from sklearn.datasets import make_classification

# Vaš kôd ovdje

"""
Explanation: Q: Očito je zašto model nije u mogućnosti postići potpunu točnost na ovom skupu podataka. Međutim, smatrate li da je problem u modelu ili u podacima? Argumentirajte svoj stav.
2. Višeklasna klasifikacija
Postoji više načina kako se binarni klasifikatori mogu se upotrijebiti za višeklasnu klasifikaciju. Najčešće se koristi shema tzv. jedan-naspram-ostali (engl. one-vs-rest, OVR), u kojoj se trenira po jedan klasifikator $h_j$ za svaku od $K$ klasa. Svaki klasifikator $h_j$ trenira se da razdvaja primjere klase $j$ od primjera svih drugih klasa, a primjer se klasificira u klasu $j$ za koju je $h_j(\mathbf{x})$ maksimalan.
Pomoću funkcije datasets.make_classification generirajte slučajan dvodimenzijski skup podataka od tri klase i prikažite ga koristeći funkciju plot_2d_clf_problem. Radi jednostavnosti, pretpostavite da nema redundantnih značajki te da je svaka od klasa "zbijena" upravo u jednu grupu.
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: Trenirajte tri binarna klasifikatora, $h_1$, $h_2$ i $h_3$ te prikažite granice između klasa (tri grafikona). Zatim definirajte $h(\mathbf{x})=\mathrm{argmax}_j h_j(\mathbf{x})$  (napišite svoju funkciju predict koja to radi) i prikažite granice između klasa za taj model. Zatim se uvjerite da biste identičan rezultat dobili izravno primjenom modela RidgeClassifier, budući da taj model za višeklasan problem zapravo interno implementira shemu jedan-naspram-ostali.
Q: Alternativna shema jest ona zvana jedan-naspram-jedan (engl, one-vs-one, OVO). Koja je prednost sheme OVR nad shemom OVO? A obratno?
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: 3. Logistička regresija
Ovaj zadatak bavi se probabilističkim diskriminativnim modelom, logističkom regresijom, koja je, unatoč nazivu, klasifikacijski model.
Logistička regresija tipičan je predstavnik tzv. poopćenih linearnih modela koji su oblika: $h(\mathbf{x})=f(\mathbf{w}^\intercal\tilde{\mathbf{x}})$. Logistička funkcija za funkciju $f$ koristi tzv. logističku (sigmoidalnu) funkciju $\sigma (x) = \frac{1}{1 + \textit{exp}(-x)}$.
(a)
Definirajte logističku (sigmoidalnu) funkciju $\mathrm{sigm}(x)=\frac{1}{1+\exp(-\alpha x)}$ i prikažite je za $\alpha\in{1,2,4}$.
End of explanation
"""


from numpy import linalg

def lr_h(x, w): 
    # Vaš kôd ovdje
    pass

def cross_entropy_error(X, y, w): 
    # Vaš kôd ovdje
    pass

def lr_train(X, y, eta=0.01, max_iter=2000, trace=False, alpha=0, epsilon=0.000001):
    # Vaš kôd ovdje
    pass

"""
Explanation: Q: Zašto je sigmoidalna funkcija prikladan izbor za aktivacijsku funkciju poopćenoga linearnog modela? 
</br>
Q: Kakav utjecaj ima faktor $\alpha$ na oblik sigmoide? Što to znači za model logističke regresije (tj. kako izlaz modela ovisi o normi vektora težina $\mathbf{w}$)?
(b)
Implementirajte funkciju 

lr_train(X, y, eta=0.01, max_iter=2000, alpha=0, epsilon=0.0001, trace=False) 

za treniranje modela logističke regresije gradijentnim spustom (batch izvedba). Funkcija uzima označeni skup primjera za učenje (matrica primjera X i vektor oznaka y) te vraća $(n+1)$-dimenzijski vektor težina tipa ndarray. Ako je trace=True, funkcija dodatno vraća listu (ili matricu) vektora težina $\mathbf{w}^0,\mathbf{w}^1,\dots,\mathbf{w}^k$ generiranih kroz sve iteracije optimizacije, od 0 do $k$. Optimizaciju treba provoditi dok se ne dosegne max_iter iteracija, ili kada razlika u pogrešci unakrsne entropije između dviju iteracija padne ispod vrijednosti epsilon. Parametar alpha predstavlja faktor L2-regularizacije.
Preporučamo definiranje pomoćne funkcije lr_h(x,w) koja daje predikciju za primjer x uz zadane težine w. Također, preporučamo i funkciju cross_entropy_error(X,y,w) koja izračunava pogrešku unakrsne entropije modela na označenom skupu (X,y) uz te iste težine.
NB: Obratite pozornost na to da je način kako su definirane oznake (${+1,-1}$ ili ${1,0}$) kompatibilan s izračunom funkcije gubitka u optimizacijskome algoritmu.
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: (c)
Koristeći funkciju lr_train, trenirajte model logističke regresije na skupu seven, prikažite dobivenu granicu između klasa te  izračunajte pogrešku unakrsne entropije. 
NB: Pripazite da modelu date dovoljan broj iteracija.
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: Q: Koji kriterij zaustavljanja je aktiviran?
Q: Zašto dobivena pogreška unakrsne entropije nije jednaka nuli?
Q: Kako biste utvrdili da je optimizacijski postupak doista pronašao hipotezu koja minimizira pogrešku učenja? O čemu to ovisi?
Q: Na koji način biste preinačili kôd ako biste htjeli da se optimizacija izvodi stohastičkim gradijentnim spustom (online learning)?
(d)
Prikažite na jednom grafikonu pogrešku unakrsne entropije (očekivanje logističkog gubitka) i pogrešku klasifikacije (očekivanje gubitka 0-1) na skupu seven kroz iteracije optimizacijskog postupka. Koristite trag težina funkcije lr_train iz zadatka (b) (opcija trace=True). Na drugom grafikonu prikažite pogrešku unakrsne entropije kao funkciju broja iteracija za različite stope učenja, $\eta\in{0.005,0.01,0.05,0.1}$.
End of explanation
"""


from sklearn.linear_model import LogisticRegression

# Vaš kôd ovdje

"""
Explanation: Q: Zašto je pogreška unakrsne entropije veća od pogreške klasifikacije? Je li to uvijek slučaj kod logističke regresije i zašto?
Q: Koju stopu učenja $\eta$ biste odabrali i zašto?
(e)
Upoznajte se s klasom linear_model.LogisticRegression koja implementira logističku regresiju. Usporedite rezultat modela na skupu seven s rezultatom koji dobivate pomoću vlastite implementacije algoritma.
NB: Kako ugrađena implementacija koristi naprednije verzije optimizacije funkcije, vrlo je vjerojatno da Vam se rješenja neće poklapati, ali generalne performanse modela bi trebale. Ponovno, pripazite na broj iteracija i snagu regularizacije.
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: 4. Analiza logističke regresije
(a)
Koristeći ugrađenu implementaciju logističke regresije, provjerite kako se logistička regresija nosi s vrijednostima koje odskaču. Iskoristite skup outlier iz prvog zadatka. Prikažite granicu između klasa.
Q: Zašto se rezultat razlikuje od onog koji je dobio model klasifikacije linearnom regresijom iz prvog zadatka?
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: (b)
Trenirajte model logističke regresije na skupu seven te na dva odvojena grafikona prikažite, kroz iteracije optimizacijskoga algoritma, (1) izlaz modela $h(\mathbf{x})$ za svih sedam primjera te (2) vrijednosti težina $w_0$, $w_1$, $w_2$.
End of explanation
"""


# Vaš kôd ovdje

"""
Explanation: (c)
Ponovite eksperiment iz podzadatka (b) koristeći linearno neodvojiv skup podataka unsep iz prvog zadatka.
Q: Usporedite grafikone za slučaj linearno odvojivih i linearno neodvojivih primjera te komentirajte razliku.
End of explanation
"""


from numpy.linalg import norm

# Vaš kôd ovdje

"""
Explanation: 5. Regularizirana logistička regresija
Trenirajte model logističke regresije na skupu seven s različitim faktorima L2-regularizacije, $\alpha\in{0,1,10,100}$. Prikažite na dva odvojena grafikona (1) pogrešku unakrsne entropije te (2) L2-normu vektora $\mathbf{w}$ kroz iteracije optimizacijskog algoritma.
Q: Jesu li izgledi krivulja očekivani i zašto?
Q: Koju biste vrijednost za $\alpha$ odabrali i zašto?
End of explanation
"""


from sklearn.preprocessing import PolynomialFeatures

#poly = PolynomialFeatures(2)
#...
#mlutils.plot_2d_clf_problem(X, y, lambda x : model.predict(poly.transform(x))

# Vaš kôd ovdje

"""
Explanation: 6. Logistička regresija s funkcijom preslikavanja
Proučite funkciju datasets.make_classification. Generirajte i prikažite dvoklasan skup podataka s ukupno $N=100$ dvodimenzijskih ($n=2)$ primjera, i to sa dvije grupe po klasi (n_clusters_per_class=2). Malo je izgledno da će tako generiran skup biti linearno odvojiv, međutim to nije problem jer primjere možemo preslikati u višedimenzijski prostor značajki pomoću klase preprocessing.PolynomialFeatures, kao što smo to učinili kod linearne regresije u prvoj laboratorijskoj vježbi. Trenirajte model logističke regresije koristeći za preslikavanje u prostor značajki polinomijalnu funkciju stupnja $d=2$ i stupnja $d=3$. Prikažite dobivene granice između klasa. Možete koristiti svoju implementaciju, ali se radi brzine preporuča koristiti linear_model.LogisticRegression. Regularizacijski faktor odaberite po želji.
NB: Kao i ranije, za prikaz granice između klasa koristite funkciju plot_2d_clf_problem. Funkciji kao argumente predajte izvorni skup podataka, a preslikavanje u prostor značajki napravite unutar poziva funkcije h koja čini predikciju, na sljedeći način:
End of explanation
"""


