Responsabili:
Data publicării: 7 mai
Deadline: 20 mai, ora 23:55
Actualizari si modificari:
compute_unique
În urma realizării acestei teme:
Optical Character Recognition este un proces prin care se transformă text scanat în text editabil. În cadrul acestei teme, veți dezvolta un program care va aplica un algoritm de Machine Learning în acest scop. Algoritmul are doua etape: Etapa de învățare, în care codul vostru va primi o serie de imagini și va trebui să învețe să le recunoască; Etapa de prezicere, unde codul vostru va primi o serie de imagini care nu au mai fost vazute și va trebui să decidă ce cifră este reprezentată de imagine.
Va trebui să implementați o versiune puțin simplificată a algoritmului Random Forest pentru clasificare.
Fiecare cifră este reprezentată ca o imagine de 28×28. Pentru simplitate, în loc să folosim o matrice pentru a reprezenta o imagine, vom folosi un vector de lungime 784 (28 * 28). Pentru etapa de învățare, fiecare vector va mai avea o dimensiune în plus, anume: v[0] va fi cifra reprezentată de imagine. Deci, în etapa de învățare, fiecare vector va avea 785 de intrări. Pentru predicția cifrei, vectorii nu vor avea răspunsul pe prima pozitie, deci vor avea 784 de intrări.
Fiecare pixel din imagine este de fapt un întreg de la 0 la 255, imaginea fiind practic grey-scale.
În cazul nostru, cifra care trebuie prezisă. Termenul de clasă vine din faptul că această problema este una de clasificare.
Un vector (ca mai sus) care, în cazul nostru, reprezintă o imagine a unei cifre.
Index al vectorului (denumirea vine de la vectorii din algebră, un vector cu 3 dimensiuni fiind un vector de lungime 3).
Un arbore de decizie este un arbore binar (în cazul nostru, pentru că parametrii sunt variabile continue).
Acesta este similar cu un arbore binar de căutare. Spre deosebire de BST, în nodurile care nu sunt frunze
avem două valori, un index de split (splitIdx
) și o valoare de split (splitValue
). Decizia de a merge în
stânga sau în dreapta este efectuată în felul următor: dacă input[splitIndex] <= splitValue
, mergem în stânga,
daca nu, în dreapta. Astfel, singura diferență la nivelul nodurilor care nu sunt frunze este faptul câ inputul
primit la search într-un arbore de decizie este un vector, iar comparațiile în fiecare nod se fac pe dimensiunea
specificată în nod.
Un nod frunză conține clasa prezisă pentru vectorul input.
Învățarea se face prin împărțirea setului de date după o anumită regulă, în mod recursiv. Când dintr-o împărțire rezultă un set de date cu o singură clasă, atunci se creează o frunză cu clasa respectivă. Split-urile se efectuează în felul următor: pentru fiecare dimensiune (aici apare o modificare la random forest, vezi mai jos, Random subspace projection) și pentru fiecare valoare de pe dimensiunea respectivă se face un split. Pe fiecare din aceste split-uri se aplică o metrică care trebuie minimizată/maximizată (în funcție de metrică). Noi vom folosi o metrică numită Information Gain.
Information Gain se bazează pe noțiunea de entropie din Teoria Informației. Entropia este definită astfel:
Entropia unui set de teste în cazul nostru se măsoară în felul următor: fie $pi$ ($i$ de la $0$ la $9$) $=$ numărul de teste din set care are ca rezultat numărul i împărțit la numărul de teste. $pi$ este practic probabilitatea ca un test să aibă rezultatul $i$.
În cazul în care $ pi = 0 $, nu se adaugă nimic la suma ($ \log_2 pi $ fiind nedefinit).
Information Gain este o metrică definită astfel:
Practic, pentru a calcula Information Gain pentru un anumit set de test samples și un index și o valoare de split, calculăm entropia părintelui, respectiv entropia copiiilor rezultați în urma acelui split și aplicăm formula din imagine.
În cazul nostru, suma ponderată a entropiilor copiilor este:
$$ \dfrac{n_{stanga} \cdot H(stanga) + n_{dreapta} \cdot H(dreapta)}{n} $$
Unde:
Se alege split-ul care maximizează Information Gain, se stochează indexul de split și valoarea de split în nodul respectiv, după care mergem recursiv pe copii. În cazul în care toate split-urile au ca rezultat un copil care nu are niciun element, atunci vom face un nod frunză care are ca valoare clasa majoritară din setul de date.
Dacă printre splituri există unele care au un copil fără elemente, atunci considerăm că split-ul este prost, și nu-l luăm în considerare. Un element devine frunză doar atunci când toate spliturile au un copil vid.
Random forest este un algoritm care combină mai mulți decision tree pentru a da o predicție mai bună. Primul pas pentru această combinare este antrenarea fiecărui din cei n decision trees cu seturi de date mai mici, alese în mod aleator ca subseturi din setul inițial de training. Aceste subseturi se pot intersecta între ele. Va trebui să implementați generarea acestor subseturi.
Random subspace projection este o modificare adusă de Random forest la Decision trees-ii pe care îi folosește. Această modificare constă în forțarea split-urilor doar pe anumite dimensiuni, selectate aleator. Astfel, la fiecare split, se aleg mai întâi $\sqrt{num\_dimensiuni}$ dimensiuni, dupa care se încearcă split-uri si se maximizează information gain doar pe acele dimensiuni. Dacă spre exemplu, un sample are $16$ dimensiuni, la fiecare split veți considera doar $4$ dintre ele.
compute_unique
pentru a găsi valorile unice de pe dimensiunile
selectate. Astfel, în loc să verificați toate numerele de la $0$ la $255$, le veți verifica doar pe cele care apar. Acest lucru
crește viteza algoritmului cu mult, având în vedere faptul că, în general, pixelii vor fi aproape albi sau aproape negri. Valorile
intermediare apărând în mai puține locuri.
Aveți de implementat funcțiile cu TODO din randomForest.cpp și decisionTree.cpp. Nu aveți voie să modificați altceva.
void make_leaf(const vector<vector<int>> &samples, const bool is_single_class)
bool same_class(const vector<vector<int>> &samples)
same_class(samples)
va întoarce true;same_class(samples)
va întoarce false;float get_entropy_by_indexes(const vector<vector<int>> &samples, const vector<int> &index)
vector<int> compute_unique(const vector<vector<int>> &samples, const int col)
compute_unique(samples, col)
va întoarce vectorulpair<vector<int>, vector<int>> get_split_as_indexes(const vector<vector<int>> &samples, const int split_index, const int split_value)
samples[i]
are samples[i][split_index] <= split_value
, i aparținând primului rezultat returnatsamples[j]
are samples[j][split_index > split_value
, j aparținând celui de-al doilea rezultat returnatget_split_as_indexes(samples, split_index, split_value)
va întoarce perechea formată din {0, 2} si {1}vector<int> random_dimensions(const int size)
int predict(const vector<int> &image) const
(1, 10) / \ (10) (5)
image[split_index]
$=$ 15, $15 \gt 10$, deci mergem în dreapta. Nodul din dreapta este nod frunză și are rezultatul 5.int predict(const vector<int> &image) (din RandomForest)
vector<vector<int>> get_random_samples(const vector<vector<int>>& samples, int num_to_return)
pair<int, int> find_best_split(const vector<vector<int>> &samples, const vector<int> &dimensions)
void train(const vector<vector<int>> &samples)
Folosiți-vă cât de mult puteți de funcții care operează cu indecși. Copierea matricelor este o operație scumpă.
Scheletul se ocupa de citirea datelor, deci voi nu trebuie sa implementati nimic.
Scheletul se ocupa de datele de iesire.
Scheletul si checkerul sunt disponibile aici.
randomForest.h
si decisionTree.h
.shared_ptr
). Acest tip de pointer se comporta la fel ca un pointer obisnuit, doar ca nu trebuie sa dati free
sau delete
pe el. Alocarea se face folosind functia make_shared
. Acesteia ii veti pasa aceeasi parametri pe care i-ati pasa constructorului: shared_ptr<Node> n = make_shared<Node>(...)
. Am ales sa folosim shared_ptr
pentru a va permite sa va concentrati mai mult pe implementare in loc sa va concentrati pe erori de memorie.predict
, marimea vectorului dat ca parametru este cu $1$ mai mica decat la celelalte functii. In cazul predict
-ului, pe prima pozitie din vector nu va mai fi cifra care trebuie prezisa. Aveti grija cum navigati arborii!Temele vor trebui trimise pe vmchecker. Atenție! Temele trebuie trimise în secțiunea Structuri de Date (CA).
Arhiva trebuie să conțină:
Toate soluțiile vor fi verificate folosind o unealtă de detectare a plagiatului. În cazul detectării unui astfel de caz, atât plagiatorul cât și autorul original (nu contează cine care este) vor primi punctaj 0 pe toate temele!
De aceea, vă sfătuim să nu lăsați rezolvări ale temelor pe calculatoare partajate (la laborator etc), pe mail/liste de discuții/grupuri etc.
Nu folositi comentarii NOLINT in cod (cu exceptia celor care sunt deja prezente in schelet). Daca aveti astfel de comentarii in plus, primiti 0p pe coding style.