Tema 3 - Optical Character Recognition pe cifre scrise de mână

Responsabili:

Data publicării: 7 mai

Deadline: 20 mai, ora 23:55

Actualizari si modificari:

  • 11 mai : actualizari la checker si la exemplul de la compute_unique
  • 16 mai : actualizare checker (nu mai da 101.2 puncte) + specificatii trimitere tema.

Obiective

În urma realizării acestei teme:

  • veți învăța cum să adaptați structuri de date cunoscute la cerințe mai complicate
  • veți exersa lucrul colaborativ în echipe de câte doi
  • veți învăța ceva despre cum funcționează anumiți algoritmi de inteligență artificială

Intro

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.

Cerința

Va trebui să implementați o versiune puțin simplificată a algoritmului Random Forest pentru clasificare.

Reprezentarea Datelor

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.

Terminologie

Clasa

În cazul nostru, cifra care trebuie prezisă. Termenul de clasă vine din faptul că această problema este una de clasificare.

Sample

Un vector (ca mai sus) care, în cazul nostru, reprezintă o imagine a unei cifre.

Dimensiune

Index al vectorului (denumirea vine de la vectorii din algebră, un vector cu 3 dimensiuni fiind un vector de lungime 3).

Descrierea algoritmului

Decision Tree

Structura

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ățare

Î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.

Entropia

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$.

Atenție!

În cazul în care $ pi = 0 $, nu se adaugă nimic la suma ($ \log_2 pi $ fiind nedefinit).

Information Gain

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:

  • $n_{stanga}$ = numărul de samples din copilul din stânga
  • $n_{dreapta}$ = numărul de samples din copilul dreapta
  • $n$ este numărul total de samples. Astfel $n = n_{stânga} + n_{dreapta}$

Pentru mai multe detalii, vedeți aici.

Algoritm

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.

Atenție!

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.

Bootstrap aggregation (bagging)

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 (feature bagging)

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.

Ca optimizare la selectarea unui split, puteți folosi 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.

Schelet

Aveți de implementat funcțiile cu TODO din randomForest.cpp și decisionTree.cpp. Nu aveți voie să modificați altceva.

Note pentru implementare

  void make_leaf(const vector<vector<int>> &samples, const bool is_single_class)
  • ar trebui să lucreze cu variabilele is_leaf și result din Node
  • dacă is_single_class este false, trebuie să găsiți clasa majoritară din setul de date primit. Dacă două clase apar la fel de des, o veți lua pe prima
  bool same_class(const vector<vector<int>> &samples) 
  • întoarce true dacă toate sample-urile din samples au aceeași clasă. Altfel, întoarceți false
  • Exemplu: samples = {{1, 0, 0, 10….}, {1, 2, 5, 9….}, {1, 4, 99, 7…}}; same_class(samples) va întoarce true;
  • samples = {{1, 0, 0, 10…}, {0, 9, 7, 111…}}; same_class(samples) va întoarce false;
  float get_entropy_by_indexes(const vector<vector<int>> &samples, const vector<int> &index)    
  • calculează entropia subsetului din samples format considerând doar sample-urile de forma samples[i], cu i element din index
  vector<int> compute_unique(const vector<vector<int>> &samples, const int col)
  • întoarce un vector care conține valorile unice care apar în matricea samples pe coloana col
  • Exemplu: samples = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 6, 14, 15}}, col = 1; compute_unique(samples, col) va întoarce vectorul
  • {2, 6, 10} (nu contează ordinea)
  pair<vector<int>, vector<int>> get_split_as_indexes(const vector<vector<int>> &samples, const int split_index, const int split_value)
  • în funcție de split_index și split_value, întoarce doi vectori care conțin indecși în vectorul samples, astfel încât:
    • oricare samples[i] are samples[i][split_index] <= split_value, i aparținând primului rezultat returnat
    • oricare samples[j] are samples[j][split_index > split_value, j aparținând celui de-al doilea rezultat returnat
    • exemplu: pentru samples = {{1, 2, 3}, {5, 6, 7}, {0, 9, 7}}, split_index = 0, split_value = 2
    • get_split_as_indexes(samples, split_index, split_value) va întoarce perechea formată din {0, 2} si {1}
  vector<int> random_dimensions(const int size) 
  • selectează în mod aleator sqrt(size), rotunjit la întreg prin lipsa) dimensiuni.
  • dimensiunile trebuie să fie unice și să nu existe dimensiunea 0 (pentru că ar reprezenta răspunsul)
  • exemplu: random_dimensions(5) → {1, 3}
  • explicație:
    • floor(sqrt(5)) = 2, selectăm în mod aleator două dimensiuni din mulțimea {1, 2, 3, 4}
  int predict(const vector<int> &image) const
  • navigați arborele de decizie până ajungeți la o frunză și returnați valoarea din frunză
  • Exemplu: Pentru arborele:
                 (1, 10)
                  /  \
               (10)  (5)
  • unde nodul(1, 10) este nod de decizie iar (10) și (5) noduri frunză. 1 reprezintă indexul de split, 10 reprezintă valoarea de split.
  • dacă avem image = {1, 15, 20, 7, 19} predict(image) va întoarce 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)
  • Pentru fiecare Decision Tree din Forest, află răspunsul prezis și în final, întoarce cel mai des întâlnit răspuns.
  vector<vector<int>> get_random_samples(const vector<vector<int>>& samples, int num_to_return)
  • pentru matricea samples, întoarce o submatrice cu num_to_return linii selectate random
  pair<int, int> find_best_split(const vector<vector<int>> &samples, const vector<int> &dimensions)
  • bazat pe dimensions, găsiți split-ul care maximizează information gain și întoarceți o pereche de forma (split_index, split_value)
  • dacă nu se găsește niciun split bun, atunci nodul trebuie să fie frunză. Recomandăm să întoarceți ceva care nu are sens, cum ar fi (-1, -1).
  void train(const vector<vector<int>> &samples)
  • folosindu-vă de funcțiile descrise mai sus, implementați algoritmul de învățare pentru decision tree
  • pseudocod:
    • verificați dacă ați ajuns la un nod care trebuie să fie frunză
    • căutați cea mai bună valoare de split (nu uitați să folosiți random_dimensions
    • dacă nu a fost găsit niciun split bun, faceți frunza
    • dacă ați găsit un split, salvați split_index și split_value în nodul curent, creați copii pe baza split-ului și mergeți recursiv

Hint!

Folosiți-vă cât de mult puteți de funcții care operează cu indecși. Copierea matricelor este o operație scumpă.

Date de intrare

Scheletul se ocupa de citirea datelor, deci voi nu trebuie sa implementati nimic.

Date de ieșire

Scheletul se ocupa de datele de iesire.

Schelet de cod si Checker

Resurse

Scheletul si checkerul sunt disponibile aici.

Detalii schelet

  • Puteti vedea definitiile structurilor si parametrii functiilor in fisierele randomForest.h si decisionTree.h.
  • In structura arborelui folosim un smart pointer (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.
  • In functia 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!

Reguli pentru trimitere

Temele vor trebui trimise pe vmchecker. Atenție! Temele trebuie trimise în secțiunea Structuri de Date (CA).

Arhiva trebuie să conțină:

  • sursele voastre (randomForest.cpp, decisionTree.cpp si alte surse, pe care le scrieti voi)
  • fisier README care să conțină detalii despre implementarea temei si despre modul in care ati impartit sarcinile. Va recomandam sa folositi un sistem de versionare al surselor (git). Atentie, nu folositi repository-uri publice de git. De asemenea, fisierul README trebuie sa contine numele si grupa celor 2 membri ai echipei.
  • încercați să evitați folosirea altor fișiere sursa. Nu aveți voie să modificați Makefile-ul. Dacă totuși simțiți nevoia să modularizați mai mult, puteți să folosiți fisiere .h în care să scrieți și implementările
  • trimiteti o singura submisie pe echipa.
  • din cauza timpului lung de testare al temei, va rugam sa nu trimiteti foarte multe arhive pe vmchecker. Testarea are loc cu acelasi checker, deci nu ar trebui sa aveti probleme prea mari.

Punctaj

  1. 50p get_split_as_indexes, same_class, random_dimensions, get_random_samples, compute_unique(cate 10p fiecare)
  2. 30p Acuratetea algoritmului: > 85% → 30p, > 55% → 20p, > 25% → 10p
  3. 10p README + comentarii/claritate cod (ATENȚIE! Fișierul README trebuie făcut explicit, cât să se înțeleagă ce ați făcut în sursă, dar fără comentarii inutile și detalii inutile)
  4. 10p pentru coding-style, proporțional cu punctajul obținut pe teste. De exemplu, pentru o temă care obține maxim pe teste, se pot obține 10p dacă nu aveți erori de coding style. Pentru o temă care obține 40p pe teste, se pot obține 5p dacă nu aveți erori de coding style.
  5. O temă care obține 0p pe vmchecker este punctată cu 0.
  6. O temă care nu compilează va fi punctată cu 0.

Nu copiați!

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.

Atentie!

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.

FAQ

  • Q: Putem folosi STL?
  • A: Da, puteți să folosiți STL.
sd-ca/2018/teme/tema3.txt · Last modified: 2019/02/01 13:36 by teodora.serbanescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0