{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"name":"Classification 2.2 data preprocessing - MLP.ipynb","provenance":[],"collapsed_sections":["OMR7d5XLlbWR","s8T3pG4bJKzw"]},"kernelspec":{"name":"python3","display_name":"Python 3"}},"cells":[{"cell_type":"code","metadata":{"scrolled":true,"id":"gd8SH8deJKzA"},"source":["!pip install --upgrade pip \n","!pip install --upgrade scikit-learn \n","!pip install --upgrade numpy \n","!pip install --upgrade scipy \n","!pip install --upgrade pandas "],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"_oZdbODzJKzF"},"source":["# Εισαγωγή και επισκόπηση των δεδομένων.\n","\n","## Ένα dataset αναγνώρισης αντικειμένων σε εικόνα\n","\n","Σε αυτή την άσκηση θα προεπεξεργαστούμε μια σειρά χαρακτηριστικών χαμηλού επιπέδου που έχουν εξαχθεί από τμήματα εικόνων, και στη συνέχεια θα εκπαιδεύσουμε έναν νευρωνικό δίκτυο που θα τις κατατάσσει σε μία από τις 5 διαφορετικές κατηγορίες στις οποίες μπορεί να ανήκουν.\n","\n","![alt text](https://cdn-images-1.medium.com/max/1600/1*r9ELExnk1B1zHnRReDW9Ow.png)\n","\n","Ένα από τα πιο δημοφιλή προβλήματα στο χώρο της σημασιολογικής ανάλυσης πολυμεσικού υλικού αποτελεί η ταξινόμηση τμημάτων εικόνων με βάση το περιεχόμενο. Μια συνηθισμένη διαδικασία για την επίτευξη αυτού του σκοπού έχει ως εξής. Αρχικά ένας αλγόριθμος τμηματοποίησης επεξεργάζεται την εικόνα και τη χωρίζει σε περιοχές με παρόμοια χαρακτηριστικά χαμηλού επιπέδου (για παράδειγμα ιστογράμματα χρώματος, αναγνώριση ακμών κλπ). Τα τμήματα αυτά ταξινομούνται χειροκίνητα σύμφωνα με το περιεχόμενο τους σε κάποιες κατηγορίες. Με αυτό το τρόπο δημιουργείται ένα σύνολο εκπαίδευσης (χαρακτηριστικά και επιθυμητές αποκρίσεις) το οποίο χρησιμοποιείται από ένα νευρωνικό δίκτυο για την ταξινόμηση των τμημάτων της εικόνας. \n","\n","Σύμφωνα με την περιγραφή του dataset, οι πίνακες που μας δίνονται για την εκπαίδευση του δικτύου έχουν τα εξής περιεχόμενα:\n","\n","1. Κάθε στήλη του πίνακα TrainData αντιστοιχεί στο τμήμα μίας εικόνας ενώ κάθε γραμμή περιέχει χαρακτηριστικά χαμηλού επιπέδου (που αντιστοιχούν σε Descriptors) του τμήματος αυτού. Σύμφωνα με την περιγραφή του dataset το σύνολο τμημάτων εικόνας (δειγμάτων) στο train set είναι 645 και το σύνολο των χαρακτηριστικών είναι 586 \n","2. Κάθε στήλη του πίνακα TrainDataTargets αντιστοιχεί στο τμήμα μίας εικόνας ενώ κάθε γραμμή αντιστοιχεί σε μία από τις κατηγορίες «Άλογο», «Σκύλος», «Πρόσωπο», «Αυτοκίνητο», «Ποδήλατο», «Λουλούδι» κ.ά. Γνωρίζουμε ότι το σύνολο των στηλών - δειγμάτων στο test set είναι 159 και ότι το σύνολο των κατηγοριών - γραμμών είναι 5 (Για την υλοποίηση της άσκησης έχουν δηλαδή επιλεξεί 5 μόνο κατηγορίες αντικειμένων). Στην περίπτωση που το τμήμα μιας εικόνας ανήκει σε μία από τις κατηγορίες τότε η αντίστοιχη γραμμή θα έχει την τιμή 1 ενώ οι υπόλοιπες γραμμές θα πάρουν την τιμή 0.\n","\n","Τα δεδομένα των πινάκων TrainData, TrainDataTargets θα χρησιμοποιηθούν για την εκπαίδευση του νευρωνικού δικτύου ενώ τα δεδομένα των πινάκων TestData, TestDataTargets θα χρησιμοποιηθούν ως μέτρο των επιδόσεων του νευρωνικού δικτύου. Προφανώς τα δεδομένα των πινάκων TestData και TestDataTargets έχουν αντίστοιχη δομή με τα δεδομένα των πινάκων TrainData και TrainDataTargets. \n","\n","Οι πίνακες των χαρακτηριστικών και ετικετών του train και test set στην περίπτωση αυτή μας δίνονται έτοιμοι και δεν κάνουμε εμείς split του dataset.\n","\n","Το πρώτο βήμα όταν διαχειριζόμαστε datasets είναι να τα εισάγουμε σωστά στο περιβάλλον μας και να βεβαιωθούμε ότι οι πίνακες έχουν τη σωστή μορφή.\n","\n","## Εισαγωγή dataset\n","\n","Θα εισάγουμε τα δεδομένα σε dataframes απευθείας από τα URL όπου είναι αποθηκευμένα."]},{"cell_type":"code","metadata":{"id":"4W8Eo5R-iIrv"},"source":["import pandas as pd\n","trainData = pd.read_csv(\"http://mycourses.ntua.gr/document/goto/?url=%2F%C5%F1%E3%E1%F3%F4%DE%F1%E9%EF_2018_-_2019%2FLab_4%2Fdata_files%2FtrainData.csv&cidReq=ECE1078\").values\n","trainDataTargets = pd.read_csv(\"http://mycourses.ntua.gr/document/goto/?url=%2F%C5%F1%E3%E1%F3%F4%DE%F1%E9%EF_2018_-_2019%2FLab_4%2Fdata_files%2FtrainDataTargets.csv&cidReq=ECE1078\").values\n","testData = pd.read_csv(\"http://mycourses.ntua.gr/document/goto/?url=%2F%C5%F1%E3%E1%F3%F4%DE%F1%E9%EF_2018_-_2019%2FLab_4%2Fdata_files%2FtestData.csv&cidReq=ECE1078\").values\n","testDataTargets = pd.read_csv(\"http://mycourses.ntua.gr/document/goto/?url=%2F%C5%F1%E3%E1%F3%F4%DE%F1%E9%EF_2018_-_2019%2FLab_4%2Fdata_files%2FtestDataTargets.csv&cidReq=ECE1078\").values\n","print(trainData)\n","print(trainDataTargets)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"s9fiXqGYJKzL"},"source":["Γνωρίζουμε ότι το csv μας περιέχουν μόνο αριθμητικά δεδομένα οπότε καλώντας τη μέθοδο .values τα δεδομένα μετατρέπονται αυτόματα σε πίνακα numpy αντί για data_frame. Προσοχή: σε csv που περιέχουν συνδυασμό strings και αριθμών ακολουθούμε τη μεθοδολογία με διαφορετικά dataframes ανάλογα τον τύπο δεδομένων που είδαμε στο τελευταίο section του notebook \"Classification 1.1.\" \n","\n","## Επισκόπηση του Dataset\n","\n","Αφού εισάγουμε τα δεδομένα και έχουμε τις κλάσεις με τις οποίες επιθυμούμε να δουλέψουμε, πρέπει να κάνουμε έναν αρχικό έλεγχο και να δούμε αν είναι σε κατάλληλη μορφή. Πολλές φορές τα δεδομένα έχουν αντίστροφα από το επιθυμητό τις σειρές και τις στήλες ή περιέχουν κατηγορίες (indexes) που κατά λάθος μπορεί να περάσουμε σαν έξτρα σειρές στους πίνακές μας. \n","\n","Σημείωση: ένας από τους λόγους που πολλοί επιστήμονες δεδομένων επιλέγουν να δουλεύουν με panda data_frames αντί απλά arrays είναι το ότι διαχειρίζονται αυτόματα τα indexes των δεδομένων. \n","\n","Πρέπει πάντοτε να κάνουμε μια επισκόπηση των πινάκων να δούμε αν είναι στη σωστή μορφή, ειδικά όταν δεν έχουμε dataset description (δηλαδή τι σημαίνουν οι κολώνες και οι γραμμές), όπως στην περίπτωση αυτή."]},{"cell_type":"code","metadata":{"id":"aWcZ1BLcJKzN"},"source":["# παίρνουμε πρώτα τις διαστάσεις όλων των πινάκων\n","print(trainData.shape)\n","print(trainDataTargets.shape)\n","print(testData.shape)\n","print(testDataTargets.shape)\n","print(\"\\n\")\n","\n","# κάνουμε μια πρώτη εκτύπωση των χαρακτηριστικών\n","print( trainData)\n","print(trainDataTargets)\n","print(testData)\n","print(testDataTargets)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"ip4laBb1HH_6"},"source":["### Multi-class dataset format\n","\n","Όταν έχουμε c διαφορετικές κλάσεις το scikit περιμένει το dataset να έχει την ακόλουθη μορφή:\n","\n","- ο πίνακας των δεδομένων να έχει σχήμα (n samples, m features)\n","- ο πίνακας των ετικετών να έχει σχήμα(n samples, c κλάσεις)\n","\n","Εάν έχουμε 3 κλάσεις και ένα δείγμα ανήκει στη δεύτερη (index 1) τότε η ετικέτα του θα είναι [0 1 0]. Αυτή είναι η πυκνή αναπαράσταση (dense). Μπορούμε επίσης να έχουμε την αραιή (sparse) αναπαράσταση:\n","\n","- ο πίνακας των δεδομένων να έχει σχήμα (n samples, m features)\n","- ο πίνακας των ετικετών να έχει σχήμα (n samples, ετικέτα κλάσης)\n","\n","ένα δείγμα της κατηγορίας 3 θα έχει ετικέτα '2' (δηλαδή 3 ξεκινώνατας το index από το 0). \n","\n","Το scikit καταλαβαίνει και τις δύο συμβάσεις για τις ετικέτες πολλών κατηγοριών. Τα ονόματα δεν χρειάζεται καν να είναι 0, 1, 2 κλπ, μπορούν να είναι και string αρκεί να είναι μοναδικά. Γενικά προτιμάται η αναπαράσταση sparse. H dense μετατρέπεται εσωτερικά σε sparse.\n"]},{"cell_type":"markdown","metadata":{"id":"gSc_xcxnJKzR"},"source":["## Συμβατότητα με scikit-learn\n","Οι παρατηρήσεις που κάνουμε για τα δείγματα και τα χαρακτηριστικά είναι δύο:\n","* Πρώτον, τα χαρακτηριστικά είναι σε γραμμές και τα δείγματα σε κολόνες. Αυτό είναι ασύμβατο με το scikit-learn \n","* Δεύτερον, οι κολόνες - δείγματα αντί να είναι 645 όπως λέει η περιγραφή είναι 646. Από το print λοιπόν βλέπουμε ότι η διάσταση αυτή δεν συμπίπτει με την περιγραφή γιατί η πρώτη κολώνα είναι ένα index από 1 μέχρι το 586 των χαρακτηριστικών (και όχι δείγμα). \n","\n","Συνεπώς θα πρέπει να αφαιρέσουμε αυτό το index (δεν αποτελεί δείγμα) και εν συνεχεία να κάνουμε αναστροφή στους πίνακες (που γινεται με τη μέθοδο \".Τ\") ώστε να έχουμε δείγματα στις γραμμές και χαρακτηριστικά στις κολώνες (standard στο scikit-kearn). \n","\n","Παρόμοια για τις ετικέτες των κατηγοριών, αφενός η πρώτη κολόνα είναι πάλι ένα index από 1 έως 5 των κατηγοριών (που δεν το θέλουμε) και αφετέρου θέλουμε να έχουμε τα δείγματα σε γραμμές και τα 0 και 1 των κατηγοριών σε 5 κολόνες, οπότε θα χρειαστούμε και εκεί αναστροφή των πινάκων."]},{"cell_type":"code","metadata":{"id":"f6QGego7JKzS"},"source":["# C for Corrected\n","C_trainData = trainData.T[1:,]\n","C_testData = testData.T[1:,]\n","C_trainDataTargets = trainDataTargets.T[1:,]\n","C_testDataTargets = testDataTargets.T[1:,]\n","\n","print(C_trainData.shape)\n","print(C_trainDataTargets.shape)\n","print(C_testData.shape)\n","print(C_testDataTargets.shape)\n","print(\"\\n\")\n","\n","#τυπώνουμε τις 5 πρώτες γραμμές και κολόνες\n","print(C_trainData[:5,:5])\n","print(C_trainDataTargets[:5,:5])\n","print(C_testData[:5,:5])\n","print(C_testDataTargets[:5,:5])"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"u-Odl2r4JKzX"},"source":["Συμπερασματικά: είναι πάντα καλή πρακτική να κάνουμε μια ποιοτική επισκόπηση του dataset και να εξετάζουμε αν οι πίνακες των δεδομένων είναι στη σωστή μορφή και να τους διορθώνουμε αν χρειαστεί. Μπορούμε να έχουμε ή να μην έχουμε ονόματα χαρακτηριστικών στην πρώτη γραμμή η κολώνα, αρίθμηση σειρών, να λείπουν τιμές χαρακτηριστικών, να έχουμε μαζί αριθμητικές και κατηγορικές μεταβλητές (strings) \n","\n","## Ένα απλό Multi Layer Perceptron (MLP)\n","Στη συνέχεια θα κάνουμε κάποιες διαδικασίες προεπεξεργασίας στα δεδομένα για να βελτιώσουμε την απόδοση των ταξινομητών. Για να δούμε τη διαφορά πριν και μετά την προεπεξεργασία, κάνουμε εκπαίδευση σε ένα Multi Layer Perceptron (MLP) με παραμέτρους (σταθερές, που δεν τις βελτιστοποιούμε) και μη επεξεργασμένα δεδομένα και υπολογίζουμε precision, recall, f1. Θα δούμε ότι το MLP αδυνατεί να μάθει να κάνει προβλέψεις. Σημειώστε επίσης ότι καλούμε τη μέθοδο fit στα δεδομένα train, και τη μέθοδο predict στα δεδομένα test."]},{"cell_type":"code","metadata":{"id":"0loPbjyDJKzZ"},"source":["from sklearn.neural_network import MLPClassifier\n","from sklearn.metrics import classification_report\n","\n","clf = MLPClassifier(solver='lbfgs', alpha=1e-5,\n"," hidden_layer_sizes=(5,), random_state=1)\n","clf.fit(C_trainData, C_trainDataTargets)\n","preds = clf.predict(C_testData)\n","print(classification_report(C_testDataTargets, preds))"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"hlSlLOCTJKze"},"source":["Το μοντέλο **δεν λειτουργεί** για δύο βασικούς λόγους:\n","1. δεν έχει γίνει επαρκής προεπεξεργασία στα πρότυπα εισόδου\n","2. οι υπερπαράμετροι του μοντέλου, όπως ο αριθμός των νευρώνων του κρυμμένου επιπέδου, δεν είναι βελτιστοποιημένοι. Σημειώστε ότι το MLP έχει ένα [πάρα πολύ μεγάλο πλήθος υπερπαραμέτρων](http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html)"]},{"cell_type":"markdown","metadata":{"id":"_rNmB92OJKzf"},"source":["# Προεπεξεργασία δεδομένων\n","\n","Αφού εισάγαμε τα δεδομένα και κάναμε μια πρώτη επισκόπηση και τα φέραμε στη σωστή μορφοποίηση, περνάμε στο επόμενο στάδιο, αυτό της προεπεξεργασίας (data preprocessing). Σε πολλά προβλήματα ταξινόμησης (και machine learning γενικότερα) η προεπεξεργασία είναι ένα πολύ σημαντικό κομμάτι (και συχνά πολύ χρονοβόρο) ολόκληρης της διαδικασίας το οποίο όμως συχνά έχει πολύ μεγάλο αντίκτυπο στην επίδοση του συστήματος. \n","\n","Τα βήματα προεπεξεργασίας στοχεύουν:\n","* Στην αφαίρεση η αντικατάσταση απουσιάζουσων τιμών από το dataset\n","* Στη μετατροπή των κατηγορικών μεταβλητών κατάλληλα ώστε να μπορούν να τους διαχειριστούν αλγόριθμοι μηχανικής μάθησης\n","* Στην επιλογή ή εξαγωγή των κατάλληλων χαρακτηριστικών για το μοντέλο μας"]},{"cell_type":"markdown","metadata":{"id":"BQOQYnhOotJ9"},"source":["## Απουσιάζουσες τιμές χαρακτηριστικών\n","\n","Επειδή τα datasets δημιουργούνται από μετρήσεις ή αντικείμενα του πραγματικού κόσμου, δεν είναι σπάνιο να υπάρχουν απουσιάζουσες τιμές κάποιων χαρακτηριστικών σε έναν αριθμό δειγμάτων. Ωστόσο, η είσοδος στους αλγόριθμους ΜΜ πρέπει να είναι πληρης. \n","\n"]},{"cell_type":"code","metadata":{"id":"uaa8-2ciNv9c"},"source":["from io import StringIO\n","\n","csv_data = '''A,B,C,D \\n 1.0,2.0,3.0,4.0 \\n 5.0,6.0,NaN,8.0 \\n 10.0,11.0,12.0,NaN'''\n","print(csv_data)\n","\n","df = pd.read_csv(StringIO(csv_data))\n","print(df)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"4CNKdT9HNu5r"},"source":["Με την StringIO διαβάζουμε σε ένα dataframe από ένα string ως να ήταν CSV αρχείο στο δίσκο. Παρατηρούμε ότι στο CSV λείπουν δύο τιμές τις οποίες το dataframe αντιλαμβάνεται και αντικαθιστά με \"NaN\" (\"Not A Number\").\n","\n","Μια στρατηγική θα ήταν να αφαιρέσουμε τα δείγματα (γραμμές) που έχουν χαρακτηριστικά με απουσιάζουσες τιμές ή παρόμοια τα χαρακτηριστικά αν σε κάποια δείγματα απουσιάζουν\n","\n"]},{"cell_type":"code","metadata":{"id":"Sjb8ROJ0QMRw"},"source":["df.dropna(axis=0) # κρατάμε μόνο τα δείγματα με αριθμητικές τιμές"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"Cft8yPHzRFNT"},"source":["df.dropna(axis=1) # κρατάμε μόνο τα χαρακτηριστικά με αριθμητικές τιμές"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"_UuI2_OFQK73"},"source":["(η μέθοδος dropna έχει πολλά ακόμη [options](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.dropna.html))\n","\n","Σημειώστε ότι αυτή μεθοδολογία είναι η μόνη μορφή προεπεξεργασίας που καλύτερο είναι να γίνει πριν διαχωρίσουμε σε train και test. Aν έχουμε κάνει το διαχωρισμό θα πρέπει ότι χαρακτηριστικό (στήλη) αφαιρέσουμε στο ένα, να το αφαιρέσουμε και στο άλλο.\n","\n","Μπορεί αυτή η προσέγγιση να είναι μοιάζει απλή, ωστόσο ειδικά αν απουσιάζουν πολλές τιμές, συνήθως δεν θέλουμε να θυσιάσουμε δεδομένα (δείγματα ) ούτε να αφαιρέσουμε χαρακτηριστικά που μπορεί να περιλαμβάνουν σημαντική πληροφορία για το διαχωρισμό των κλάσσεων.\n","\n","Πρακτικά συνήθως χρησιμοποιούμε το μετασχηματιστή “[Imputer](http://scikit-learn.org/stable/modules/impute.html)” του scikit learn που αντικαθιστά κάθε απουσιάζουσα τιμή χαρακτηριστικού με τη μέση τιμή (συνεχείς μεταβλητές) ή την πιο συχνή τιμή (κατηγορικές μεταβλητές) του χαρακτηριστικού στο train set. \n","\n","\n","Αν θεωρήσουμε ότι το df είναι το training set τότε:\n"]},{"cell_type":"code","metadata":{"id":"TsO-2v-Tif-v"},"source":["from sklearn.impute import SimpleImputer\n","import numpy as np\n","\n","df.replace('nan',np.NaN,inplace=True)\n","imp=SimpleImputer(missing_values=np.NaN,strategy='mean')\n","\n","idf=pd.DataFrame(imp.fit_transform(df))\n","idf.columns=df.columns\n","idf.index=df.index\n","\n","print(idf)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"svtRRwr7jt_N"},"source":["Αν τώρα έχουμε ένα test set με missing values οι τιμές θα αντικατασταθούν με τις μέσες / πιο συχνές τιμές που έχουν υπολογιστεί στο **train set**."]},{"cell_type":"code","metadata":{"id":"QhYZwc11kK2e"},"source":["csv_data = '''A,B,C,D \\n 3.0,4.0,,1.0 \\n 5.0,2.0,10.0,6.0'''\n","\n","test_df = pd.read_csv(StringIO(csv_data))\n","print(test_df)\n","imputed_test_data = imp.transform(test_df.values)\n","imputed_test_data"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"eI4ctBONifGA"},"source":["Ο μετασχηματισμός με Imputer γίνεται στην απόλυτη αρχή της προεπεξεργασίας."]},{"cell_type":"markdown","metadata":{"id":"OMR7d5XLlbWR"},"source":["## Μετασχηματικές και Εκτιμητές στο scikit-learn\n","\n","Η κλάση Imputer που είδαμε προηγουμένως ανήκει στο scikit στη κατηγορία των λεγόμενων μετασχηματιστών (**transformers**). Οι μετασχηματιστές έχουν δύο βασικές μεθόδους, την fit και την transform. Με την fit μαθαίνουν κάποιες παραμέτρους (πχ προηγουμένως τη μέση τιμή) με βάση τα δεδομένα train και με την transform μπορούν να μετασχηματίσουν τα δεδομένα (train ή test) βάσει των παραμέτρων που έχουν μάθει.\n","![alt text](https://i.imgur.com/uqtJyI8.jpg)\n","\n","Η δεύτερη μεγάλη κλάση του scikit learn είναι οι εκτιμητές (**estimators**). Αυτό που τους διαφοροποιεί είναι ότι ενώ έχουν τις δικές τους μεθόδους fit και σε κάποιες περιπτώσεις και transform, έχουν επιπλέον τη μέθοδο predict, που κάνει τις προβλέψεις πάνω στα δεδομένα του test set. Οι ταξινομητές στην επιβλεπόμενη μάθηση είναι λοιπόν όλοι τους εκτιμητές, αφόσον κάνουν fit πάνω στα δεδομένα train και predict στα δεδομένα test.\n","\n","![alt text](https://i.imgur.com/jhExL9i.jpg)\n","\n","Θα δούμε στη συνέχεια ότι μπορούμε να σχηματίζουμε αλυσίδες πολλών διαδοχικών μετασχηματιστών που καταλήγουν σε έναν εκτιμητή, τα λεγόμενα pipelines."]},{"cell_type":"markdown","metadata":{"id":"2UwCejs5JKzg"},"source":["## Αριθμητικά και κατηγορικά χαρακτηριστικά\n","Σε κάποιες περιπτώσεις τα datasets περιέχουν εκτός από αριθμητικά και κατηγορικά χαρακτηριστικά στη μορφή string. Για παράδειγμα:"]},{"cell_type":"code","metadata":{"id":"My-3PGZqJKzi"},"source":["# Όνομα Ηλικία Βάρος Κάτοικος\n","MixedType = np.array([\n"," ['John', 25, 63.2, 'UK'],\n"," ['Maria', 62, 54.3, 'US'],\n"," ['Nick', 18, 70.3, 'US'],\n","])\n","print(MixedType)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"c8qZnadWJKzp"},"source":["Για να μπορέσει ο αλγόριθμος να λειτουργήσει, θα μπορούσε κανείς να σκεφτεί να \n","αντικαταστήσει κάθε διαφορετική τιμή κάθε κατηγορικού χαρακτηριστικού με μια αριθμητική τιμή. Στο προηγούμενο παράδειγμα να θέσει John=0, Maria=1, Nick=2 και Europe=0, US=1. Αυτό όμως είναι λάθος γιατί θεωρεί ότι το σύνολο τιμών των δύο κατηγορικών μεταβλητών είναι διατεταγμένο (ordered) κάτι που προφανώς δεν ισχύει και θα οδηγήσει τον ταξινομητή σε λάθη.\n","\n","Μια λύση είναι να μετατρέψουμε κάθε κατηγορικό χαρακτηριστικό με m τιμές σε m binary χαρακτηριστικά από τα οποία μόνο ένα είναι ενεργό κάθε φορά:"]},{"cell_type":"code","metadata":{"id":"JC_DDUZ9JKzr"},"source":["# μετατρέπουμε σε dataframe και τυπώνουμε\n","mtdf = pd.DataFrame(MixedType)\n","print(mtdf)\n","# οι κολόνες 1 και 4 έχουν κατηγορικές μεταβλητές. \n","# Με την \"get_dummies\" κάνουμε τη μετατροπή σε binary χαρακτηριστικά που περιγράψαμε\n","dummies = pd.get_dummies(mtdf, columns=[0,3])\n","print(dummies)\n","# Μετατρέπουμε σε αριθμητικές τιμές (pd.to_numeric) και σε numpy array (.values)\n","np_dummies = dummies.apply(pd.to_numeric).values\n","print(np_dummies)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"93R8pvUiJKzv"},"source":["Θα πρέπει εδώ να σημειώσουμε την ειδική περίπτωση όπου ένα αριθμητικό χαρακτηριστικό είναι και αυτό κατηγορικό. Αν κάποια χαρακτηριστικά είναι αριθμητικά και αναφέρονται ως κατηγορικά, σημαίνει ότι έχουν ένα ορισμένο και πεπερασμένο σύνολο τιμών. Διακρίνουμε δύο περιπτώσεις: αν το σύνολο τιμών είναι διατεταγμένο (πχ size: \"10\" \"20\" \"30\") δεν χρειάζεται μετατροπή σε δυαδικά χαρακτηριστικά. Αν όμως το σύνολο τιμών είναι αριθμητικό αλλά μη διατεταγμένο, τότε χρειάζεται. Παράδειγμα person: \"1\" \"2\" \"3\", όπου 1 σημαίνει \"γυναίκα\", 2 \"άνδρας\" και 3 \"παιδί\".\n","\n","Τέλος, τα ονομαστικά χαρακτηριστικά μπορούν να είναι και αυτά διατεταγμένα πχ οι τιμές \"(M)edium\", \"(L)arge\", \"E(X)tra Large\": \n"]},{"cell_type":"code","metadata":{"id":"3-isPnb2RJT4"},"source":["cdf = pd.DataFrame([\n","['green', 'M', 10.1, 'class1'],\n","['red', 'L', 13.5, 'class2'],\n","['blue', 'XL', 15.3, 'class1']])\n","cdf.columns = ['color', 'size', 'price', 'classlabel']\n","cdf"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"yA7omPbzRoze"},"source":["Σε αυτήν την περίπτωση μπορούμε να αποφύγουμε τη μετατροπή σε δυαδικά χαρακτηριστικά, αλλά θα πρέπει να ορίσουμε ρητά μια αντιστοίχιση που να σέβεται την διάταξη των κατηγορικών τιμών:"]},{"cell_type":"code","metadata":{"id":"_-GtXjEPR9N-"},"source":["size_mapping = {'XL': 3, 'L': 2, 'M': 1}\n","cdf['size'] = cdf['size'].map(size_mapping)\n","cdf"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"mT9vWqt-iKiG"},"source":["Οι μετατροπές χαρακτηριστικών πρέπει να είναι οι ίδιες στο train και test set και γίνονται μετά τη διαχείριση τιμών που απουσιάζουν."]},{"cell_type":"markdown","metadata":{"id":"469akvZYS-1v"},"source":["## Κωδικοποίηση των ετικετών των κατηγοριών\n","\n","Πολλές βιβλιοθήκες μηχανικής μάθησης απαιτούν οι ετικέτες κλάσης να κωδικοποιούνται ως ακέραιοι αριθμοι. Αν και οι περισσότεροι εκτιμητές για ταξινόμηση στο scikit-learn μετατρέπουν εσωτερικά τις \n","ετικέτες σε ακέραιους αριθμούς, θεωρείται καλή πρακτική η παροχή ετικετών κλάσης ως ακέραιους\n","για να αποφευχθούν τυχόν δυσλειτουργίες. Για την κωδικοποίηση των ετικετών κλάσης, μπορούμε να χρησιμοποιήσουμε μια προσέγγιση παρόμοια με την αντιστοίχιση των διατεταγμένων χαρακτηριστικών που κάναμε προηγουμένως. Οι ετικέτες κλάσης δεν είναι διατεταγμένες και άρα δεν έχει σημασία ο ακέραιος αριθμός που εκχωρούμε σε μια συγκεκριμένη ετικέτα, αρκεί να είναι μοναδικός. Έτσι, μπορούμε απλά να απαριθμήσουμε το ετικέτες κλάσης, ξεκινώντας από το 0:"]},{"cell_type":"code","metadata":{"id":"k1TQyzAHUk4b"},"source":["# βρίσκουμε τις μοναδικές ετικέτες\n","class_mapping = {label:idx for idx,label in enumerate(np.unique(cdf['classlabel']))}\n","class_mapping\n","# και κάνουμε την μετατροπή\n","cdf['classlabel'] = cdf['classlabel'].map(class_mapping) \n","cdf"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"s8T3pG4bJKzw"},"source":["## Η κατάρα της διαστατικότητας (Curse of dimensionality)\n","\n","Μια πολύ σημαντική παράμετρος για την απόδοση των ταξινομητών είναι η διασταστικότητα των δεδομένων, ιδιαίτερα σε σχέση με τον διαθέσιμο αριθμό δειγμάτων. Γενικά και ανεξάρτητα από το μοντέλο του ταξινομητή, η απόδοση αυξάνεται όσο αυξάνεται το πλήθος και η ποιότητα των δεδομένων και όσο μειώνεται η διαστατικότητα. Αντίστροφα, τα προβλήματα δυσκολεύουν όσο η διαστατικότητα αυξάνεται και τα δείγματα δεν επαρκούν για να καλύψουν όλες τις κατηγορίες του προβλήματος. Αναφερόμαστε στο πρόβλημα αυτό ως την κατάρα της διαστατικότητας (the curse of dimensionality): όσο αυξάνει η διαστατικότητα, τόσο τα διαθέσιμα δεδομένα γίνονται αραιά (sparse). \n","\n","Ας πούμε ότι έχουμε n-διαστάσεων κατηγορικές μεταβλητές, με δυαδικές τιμές $x_n = 0$ ή $x_n = 1$, που περιγράφουν πχ. εικόνες. Θέλουμε να μετρήσουμε πόσα αντιπροσωπευτικά παραδείγματα πρέπει να έχουμε για να καλύψουμε όλες τις κατηγορίες. Αν n=1, θέλουμε 2 παραδείγματα, αν n=2 θέλουμε 4 κ.ο.κ. δηλαδή θέλουμε $n^2$ παραδείγματα ανάλογα την διάσταση. Εάν όμως έχουμε πάνω από 2 κατηγορίες σε κάθε μεταβλητή, δηλαδή $x_n = r, r >2$, τότε αντίστοιχα έχουμε $n^r$ δυνατές *περιοχές* που χωρίζουμε τον υπερχώρο.\n","\n","Στο παράδειγμα μας οι μεταβλητές έχουν διάσταση n = 586 με πραγματικές τιμές και μόνο 645 δείγματα, επομένως καταλαβαίνει κανείς ότι χωρίς μείωση της διάστασης των μεταβλητών εισόδου με κάποιον τρόπο, η απόδοση όποιου ταξινομητή και να χρησιμοποιήσουμε θα υπονομεύεται από τον μικρό αριθμό παραδειγμάτων, δεδομένης της διαστατικότητας.\n","\n","\n","![alt text](https://www.kdnuggets.com/wp-content/uploads/curse-dimensionality-2.png)\n","\n","\n","Σε γενικές γραμμές λοιπόν οι πολύ μεγάλες διαστάσεις του χώρου εισόδου (χαρακτηριστικών) κάνουν δυσκολότερο για τον ταξινομητή να υπολογίσει το σύνορο απόφασης μεταξύ των κλάσεων και αυξάνουν τις απαιτήσεις χώρου και χρόνου εκπαίδευσης ή/και ταξινόμησης. \n","\n","Για να μειωθεί η διαστατικότητα των δεδομένων χρησιμοποιούμε τεχνικές **μείωσης διαστατικότητας (dimensionality reduction)**. To dimensionality reduction γίνεται με τεχνικές επιλογής χαρακτηριστικών (feature selection) όπου ουσιαστικά αφαιρούμε κάποια χαρακτηριστικά με βάση ένα κριτήριο χωρίς μετασχηματισμό των τιμών τους και τεχνικές εξαγωγής χαρακτηριστικών (feature extraction), όπου μετασχηματίζουμε τις τιμές των χαρακτηριστικών σε νέες (εξάγουμε δηλαδή νέα χαρακτηριστικά) αλλά σε ένα χώρο μικρότερων διαστάσεων."]},{"cell_type":"markdown","metadata":{"id":"ALlm5XPDJKzx"},"source":["## Μείωση διαστάσεων εισόδου με επιλογή χαρακτηριστικών\n","\n","Για την επιλογή χαρακτηριστικών υπάρχουν πολλές διαφορετικές τεχνικές (πχ [εδώ](http://scikit-learn.org/stable/modules/classes.html#module-sklearn.feature_selection) οι επιλογές του scikit-learn). Μια απλή τεχνική επιλογής χαρακτηριστικών είναι το ελάχιστο κατώφλι της διακύμανσης (Variance threshold). Σε γενικές γραμμές αν η διακύμανση ενός χαρακτηριστικού εισόδου είναι πολύ χαμηλή, δεν μπορεί να προσφέρει σημαντικά στη διαχωριστική ικανότητα του ταξινομητή. Ειδικά στην περίπτωση που η διακύμανση είναι 0, δηλαδή το χαρακτηριστικό έχει σταθερή τιμή για όλα τα δείγματα εκπαίδευσης, δεν χρησιμεύει καθόλου στον ταξινομητή για να αποφασίσει αν ένα δείγμα ανήκει σε μία κλάση ή σε μια άλλη και επιπλέον μπορεί να δυσκολέψει άλλες διαδικασίες της προεπεξεργασίας όπως η κανονικοποίηση των χαρακτηριστικών. \n","\n","Μπορούμε να χρησιμοποιήσουμε τη συνάρτηση VarianceThreshold για να αφαιρέσουμε τα χαρακτηριστικά στο training set που έχουν μηδενική διακύμανση (default) ή πολύ χαμηλή. Πρέπει επίσης να πάρουμε μια μάσκα (index) των χαρακτηριστικών που επιλέγουμε, ώστε να την εφαρμόσουμε και στα δεδομένα train ώστε να έχουν τις ίδιες διαστάσεις. Αυτό δεν σπάει τον κανόνα ότι δεν χρησιμοποιούμε τα δεδομένα test γιατί μπορούμε να θεωρήσουμε ότι ο ταξινομητής απλώς αγνοεί τις εισόδους που δεν περιλαμβάνονται στη μάσκα. Ας δούμε τη VarianceThreshold σε ένα toy dataset:"]},{"cell_type":"code","metadata":{"scrolled":true,"id":"nV_smzezJKzz"},"source":["from sklearn.feature_selection import VarianceThreshold\n","\n","train = np.array([[2, 2, 0, 3], [3, 1, 4, 3], [0, 1, 1, 3]])\n","print(train)\n","# αρχικοποιούμε έναν selector\n","selector = VarianceThreshold()\n","# όπως κάναμε και με τους ταξινομητές τον κάνουμε fit στα δεδομένα εκπαίδευσης\n","train_reduced = selector.fit_transform(train)\n","print(train_reduced)\n","# φτιάχνουμε μια μάσκα που μας λέει αν ο selector κρατάει ένα χαρακτηριστικό ή όχι\n","mask = selector.get_support()\n","print(mask)\n","\n","test = np.array([[1, 2, 5, 3], [3, 4, 0, 3], [0, 3, 2, 0]])\n","print(test)\n","# καθώς δεν ξέρουμε τι θα συμβεί στο test set μπορεί η υπόθεση της μηδενικής διακύμανσης του 4ου χαρακτηριστικού να μην είναι απόλυτα σωστή. Ωστόσο θα είναι πρακτικά αμελητέα.\n","\n","#test_reduced = test[:,mask]\n","\n","test_reduced = selector.transform(test)\n","print(test_reduced)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"Xn_80U2-JKz4"},"source":["Ενώ οι σταθερές τιμές πρέπει να αφαιρούνται κατά κανόνα πάντα, μπορεί κανείς να δοκιμάσει με μεγαλύτερα κατώφλια από μηδέν, αφαιρώντας και χαρακτηριστικά που έχουν κάποια διακύμανση και να αξιολογεί την επίδραση που έχει η περαιτέρω μείωση των διαστάσεων στην επίδοση του ταξινομητή. Για να καταλάβουμε τι τιμές μπορούμε να δοκιμάζουμε στο κατώφλι πρέπει να δούμε τη διακύμανση όλων των μεταβλητών: \n"]},{"cell_type":"code","metadata":{"id":"YvvcPPhy6ueR"},"source":["X = [[2, 2, 0, 3], [3, 1, 4, 3], [0, 1, 1, 3]]\n","Xvar = np.var(X, axis=0)\n","print(Xvar)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"Oyb5eNNq6tuj"},"source":["\n","Με αυτό τον τρόπο αφαιρούμε τις κολόνες με 0 variance (σταθερές) από το αρχικό dataset αναγνώρισης αντικειμένων μας:"]},{"cell_type":"code","metadata":{"id":"2MfXXfEHJKz_"},"source":["selector = VarianceThreshold(threshold=0.5)\n","train_reduced = selector.fit_transform(C_trainData)\n","mask = selector.get_support()\n","test_reduced = C_testData[:,mask]\n","\n","print(train_reduced.shape)\n","print(test_reduced.shape)\n","# διαπιστώνουμε σημαντική μείωση του αριθμού χαρακτηριστικών από 586 που ήταν αρχικά"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"9UcyrQ6NJK0C"},"source":["## Κανονικοποίηση χαρακτηριστικών\n","\n","Μετά την επιλογή χαρακτηριστικών η επόμενη τεχνική προεπεξεργασίας που μπορούμε να κάνουμε είναι η κανονικοποίηση των χαρακτηριστικών. Χαρακτηριστικά με πολύ μεγάλες διαφορές στις απόλυτες τιμές τους μπορούν να προκαλέσουν προβλήματα στην εκπαίδευση και να δώσουν ταξινομητές με μη βέλτιστη απόδοση. Για παράδειγμα, ένα χαρακτηριστικό με πολύ μεγάλες τιμές θα έχει μεγαλύτερη επίδραση στον υπολογισμό της απόστασης στον kNN από τι ένα με μικρές τιμές, χωρίς αυτό να σημαίνει απαραίτητα ότι είναι περισσότερο καθοριστικό για το διαχωρισμό των κλάσεων. Η κανονικοποίηση μετασχηματίζει τις τιμές των χαρακτηριστικών ώστε να αμβλυνθούν αυτές οι διαφορές.\n","\n","Η κανονικοποίηση των χαρακτηριστικών μπορεί να γίνει με 2 βασικούς τρόπους, γνωστούς και από τη στατιστική. Με την διαίρεση με τη διαφορά μεγίστου-ελαχίστου (feature scaling) οπότε οι τιμές όλων των χαρακτηριστικών κλιμακώνονται γραμμικά στο διάστημα [0,1] ή με το z-score (ή standard score) του κάθε χαρακτηριστικού (standardization), που κάνει το χαρακτηριστικό να έχει μέση τιμή μηδέν και διακύμανση μονάδα, σαν την κανονική κατανομή. \n","\n","Η μετατροπή μεγίστου ελαχίστου γίνεται με τον τύπο: $$X' = {X - X_{min} \\over X_{max} - X_{min}}$$Η μετατροπή σε standard score γίνεται με τον τύπο: $$z = {X- \\mu \\over \\sigma}$$ όπου: $μ$ είναι η μέση τιμή του χαρακτηριστικού και $σ$ η απόκλιση. \n","\n","Στην πράξη, δεν μας ενδιαφέρει αν η πραγματική κατανομή των χαρακτηριστικών είναι κανονική, απλά αφαιρούμε τη μέση τιμή και διαιρούμε με την απόκλιση για να έχουν τα χαρακτηριστικά της.\n","\n","H μετατροπή σε standard score είναι απαραίτητη σε πολλούς ταξινομητές για να συμπεριφερθούν σωστά. Επίσης είναι πιο ανθεκτική από την min-max σε τιμές outliers δηλαδή σποραδικές τιμές που είναι πολύ μακριά απο τη μέση τιμή και τις υπόλοιπες τιμές του χαρακτηριστικού (η min-max θα συμπιέσει τις περισσότερες τιμές σε ένα μικρό διάστημα)\n","\n","Από την άλλη, η κλιμάκωση σε [0,1] είναι λιγότερο ευαίσθητη σε πολύ μικρές αποκλίσεις και επίσης σε αραιά (sparse) διανύσματα χαρακτηριστικών (δηλαδή με πολλές μηδενικές τιμές) η εφαρμογή της διατηρεί τα μηδέν, κάτι που μπορεί να είναι καθοριστικό για την ταχύτητα εκπαίδευσης."]},{"cell_type":"code","metadata":{"id":"MQy9EHw8JK0D"},"source":["import matplotlib.pyplot as plt\n","from scipy import stats as st\n","\n","x= np.random.randint(-30,30, size=10)\n","y = [0]*len(x)\n","plt.scatter(x,y)\n","print(\"10 τυχαίες τιμές από -30 ως 30\")\n","plt.show()\n","\n","# προσθέτουμε τον outlier 100\n","x = np.append(x,100)\n","y = [0]*len(x)\n","plt.scatter(x,y)\n","print(\"οι προηγούμενες 10 τιμές από -30 ως 30 και το οutlier 100\")\n","plt.show()\n","\n","# κανονικοποίηση στο [0,1] με min max\n","min_max_x = (x - np.min(x) )/ (np.max(x) - np.min(x))\n","plt.scatter(min_max_x,y)\n","print(\"οι προηγούμενες 11 τιμές κανονικοποιημένες στο [0,1]\")\n","plt.show()\n","\n","# standardization\n","std_x = st.zscore(x)\n","plt.scatter(std_x,y)\n","print(\"οι προηγούμενες 11 standarized\")\n","plt.show()"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"KmDQPEq6JK0L"},"source":["Παρατηρούμε ότι το min-max scaling μας στέλνει τις τιμές εκτός του outlier πολύ κοντά στο μηδέν και τις πιέζει εντός του \\[0, 0.4\\] περίπου , ενώ το z-score διατηρεί μεν το μακρινό outlier, αλλά αφήνει και τις υπόλοιπες τιμές να έχουν ένα εύρος περίπου στο \\[-1, 0.5\\] και να διαφοροποιούνται. Αλγόριθμοι όπως ο backpropagation για τα νευρωνικά δίκτυα συγκλίνουν γενικά καλύτερα με την κανονικοποίηση z-score.\n","\n","Ας εφαρμόσουμε τις δύο κανονικοποιήσεις σε ένα toy dataset:"]},{"cell_type":"code","metadata":{"id":"B8irr8xnJK0N"},"source":["from sklearn import preprocessing\n","X_train = np.array([[ 1., -1., 2.],\n"," [ 2., 0., 0.],\n"," [ 0., 1., -1.]])\n","\n","X_test = np.array([[ -3., -1., 4.],\n"," [ 3., 4., 2.]])\n","\n","# standardization των features του training set\n","X_train_scaled = preprocessing.scale(X_train)\n","print(X_train_scaled)\n","# μέση τιμη και απόκλιση των scaled χαρακτηριστικών\n","print(X_train_scaled.mean(axis=0))\n","print(X_train_scaled.std(axis=0))"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"hdp9Ff8gJK0W"},"source":["**Προσοχή:** ό,τι μετασχηματισμό κανονικοποίησης κάνουμε στο train set, θα τον κάνουμε και στο test set, χρησιμοποιώντας όμως απαραίτητα το max ή τα $μ$ και $σ$ που έχουμε βρει στο train set, πράγμα που σημαίνει ότι στο test set μετά την κανονικοποίηση δεν θα έχουμε απαραίτητα παντού τιμές μεταξύ 0 και 1 και αντίστοιχα ούτε μέση τιμή 0 και διακύμανση 1."]},{"cell_type":"code","metadata":{"id":"-9sXZ4HLJK0X"},"source":["# όριζουμε ένα αντικείμενο scaler και το κάνουμε fit στο train set\n","scaler = preprocessing.StandardScaler().fit(X_train)\n","# εφαρμόζουμε τον scaler στα δεδομένα test. ΠΡΟΣΟΧΗ μέθοδος transform, όχι fit!\n","X_test_scaled = scaler.transform(X_test)\n","print(X_test_scaled)\n","\n","# και τυπώνουμε τη μέση τιμ;h και απόκλιση του test set \n","print(X_test_scaled.mean(axis=0))\n","print(X_test_scaled.std(axis=0))\n"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"A2-AHrXGJK0d"},"source":["# το ίδιο με min max scaling\n","min_max_scaler = preprocessing.MinMaxScaler()\n","X_train_minmax = min_max_scaler.fit_transform(X_train)\n","print(X_train_minmax, \"\\n\")\n","X_test_minmax = min_max_scaler.transform(X_test)\n","print(X_test_minmax)\n","# παρατηρούμε ότι στο test set έχουμε τιμές εκτός [0,1] γιατί στο train set το min ήταν -1 και το max 2"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"sRbTEYvMJK0j"},"source":["Θα εφαρμόσουμε κανονικοποίηση με standardization στο (διορθωμένο και με επιλογή χαρακτηριστικών) dataset μας. Παρατηρήστε ότι μετασχηματίζουμε και τα δεδομένα του test, ΟΜΩΣ με το μετασχηματιστή που έχουμε μάθει στο training set. Έτσι δεν κάνουμε το λάθος να χρησιμοποιήσουμε δεδομένα test για εκπαίδευση."]},{"cell_type":"code","metadata":{"id":"gYE575p9JK0l"},"source":["scaler = preprocessing.StandardScaler().fit(train_reduced)\n","train_scaled = scaler.transform(train_reduced)\n","test_scaled = scaler.transform(test_reduced)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"Na9oAdUhBWhx"},"source":["**ΠΡΟΣΟΧΗ**\n","* Aν θέλουμε να δοκιμάσουμε κατώφλια μεγαλύτερα του 0 στο VarianceThreshold, πρέπει πρώτα να εφαρμόσουμε τον minmaxscaler (γιατί οι μεταβλητές με μεγάλες μέσες τιμές θα δώσουν γενικά μεγαλύτερη τιμή variance)\n","* H εφαρμογή του VarianceThreshold δεν έχει προφανώς νόημα μετά το StandarScaler (γιατί όλες οι μεταβλητές έχουν variance 1 μετά το transform)"]},{"cell_type":"markdown","metadata":{"id":"rZtgsSrpJK0r"},"source":["## Εξισορρόπηση μη ισορροπημένων datasets\n","\n","Με τον όρο μη ισορροπημένο dataset εννούμε ένα dataset στο οποίο τα πλήθη των δειγμάτων της κάθε κλάσης διαφέρουν σημαντικά μεταξύ τους. Χωρίς να υπάρχει κάποια συνολική απάντηση, όταν ο λόγος μεταξύ του αριθμού των δειγμάτων δύο κλάσεων αρχίζει να είναι μεγαλύτερος από 2:3, μπορούμε να αρχίζουμε να θεωρούμε το dataset μη ισορροπημένο (imbalanced). Στα πραγματικά datasets αυτό είναι κάτι πολύ κοινό. Οι περισσότεροι ταξινομητές ωστόσο εκπαιδεύονται καλύτερα όταν τα δείγματα όλων των κλάσεων είναι σχετικά ισάριθμα. \n","\n","Για να δούμε αν το dataset μας είναι ισορροπημένο ή μη, θα δούμε πόσα δείγματα έχουμε ανά κατηγορία στο training set. Εφόσον κάθε δείγμα ανήκει σε μια μόνο από τις 5 κατηγορίες κάνουμε απλά: "]},{"cell_type":"code","metadata":{"id":"aM6IKPrMJK0s"},"source":["summ = C_trainDataTargets.T.sum(axis=1)\n","print(summ)"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"Vf-Rtz4hJK0x"},"source":["Τα δεδομένα εκπαίδευσης δεν έχουν ισορροπημένα πλήθη ανά κατηγορία με την πιο συχνή κατηγορία να διαφέρει σημαντικά από τη λιγότερο συχνή.\n","\n","Έχουμε δύο βασικούς τρόπους για να εξισσοροπούμε ένα dataset, την υποδειγματοληψία (undersampling) και την υπερδειγματοληψία (oversampling). Εν ολίγοις, στο undersampling απλά αφαιρούμε τυχαία δείγματα από όλες τις κατηγορίες που έχουν μεγαλύτερο πλήθος από τη μικρότερη, ενώ στο oversampling επιλέγουμε τυχαία ορισμένα παραδείγματα από τις λιγότερο συχνές κατηγορίες και τα επαναλαμβάνουμε. Στην πρώτη δηλαδή αφαιρούμε δεδομένα ενώ στην άλλη προσθέτουμε. \n","\n","Γενικά το oversampling ενδείκνυται περισσότερο, αφού δεν χάνουμε δεδομένα εκπαίδευσης. Επίσης, σε κάποιους αλγορίθμους όπως πχ. random forests, έχει παρατηρηθεί ότι τα αποτελέσματα βελτιώνονται ακόμα και με oversampling με παράγοντες άνω του 2, δηλαδή αντιγράφοντας τα ίδια δεδομένα μπορεί να βοηθάμε τη σύγκλιση. Τα προηγούμενα βέβαια δεν παρατηρούνται σε όλες τις περιπτώσεις.\n","\n"," Η βιβλιοθήκη [imbalanced-learn (ή imblearn)](https://github.com/scikit-learn-contrib/imbalanced-learn) προσφέρει πολλές μεθόδους εξισορρόπησης datasets. Αφού εγκαταστήσουμε την imblearn θα κάνουμε ένα απλό random oversampling στο training set μας."]},{"cell_type":"code","metadata":{"id":"C4i0qcdqJK0z"},"source":["!pip install -U imbalanced-learn"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"vPEU3yPXJK03"},"source":["from imblearn.over_sampling import RandomOverSampler\n","from sklearn.preprocessing import MultiLabelBinarizer\n","# αρχικοποίηση του RandomOverSampler\n","ros = RandomOverSampler()\n","# o RandomOverSampler θέλει τις ετικέτες όχι ως δυαδικό διάνυσμα αλλά ως αριθμό κλάσης. \n","# το [0 0 1 0 0] πρέπει να γίνει 2 κοκ\n","\n","mlb = MultiLabelBinarizer().fit(['0', '1', '2', '3', '4'])\n","tmplabels = np.asarray(mlb.inverse_transform(C_trainDataTargets)).flatten()\n","train_resampled, trainTargets_resampled = ros.fit_resample(train_scaled,tmplabels)\n","# ξαναφέρνουμε τα labels σε binary μορφή\n","trainTargets_resampled = mlb.transform(trainTargets_resampled)\n","\n","# εκτυπώνουμε τις νέες διαστάσεις του train set\n","print(train_resampled.shape)\n","print(trainTargets_resampled.shape)\n","# επιβεβαιώνουμε ότι τα labels είναι στη binary μορφή\n","print(trainTargets_resampled)\n","# επιβεβαιώνουμε ότι το training set έχει εξισορροπηθεί με oversampling\n","summ = trainTargets_resampled.T.sum(axis=1)\n","print(summ) #πλήθος ανα κλάση \n","print(summ.sum()) #σύνολο δειγμάτων"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"WDODKrZdJK09"},"source":["Σημειώστε ότι δεν κάνουμε καμία επέμβαση sampling στο test set.\n","\n","Ας ξαναδοκιμάσουμε το αρχικό MLP με ακριβώς ίδες υπερπαραμέτρους, απλώς αυτή τη φορά στα προεπεξεργασμένα δεδομένα (variance threshold, standard scaler, oversampling) και ας τυπώσουμε τις μετρικές απόδοσης:"]},{"cell_type":"code","metadata":{"id":"r7T4CZmTJK0-"},"source":["clf = MLPClassifier(solver='lbfgs', alpha=1e-5,\n"," hidden_layer_sizes=(5,), random_state=1)\n","clf.fit(train_resampled, trainTargets_resampled)\n","preds = clf.predict(test_scaled)\n","print(classification_report(C_testDataTargets, preds))"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"6HzuJTw9Pk1o"},"source":["Συμπέρασμα: ακόμα και χωρίς καμία βελτιστοποίηση των υπερπαραμέτρων του ταξινομητή, έχουμε πολύ μεγάλη βελτίωση στην απόδοση, απλά ακολουθώντας τις βέλτιστες πρακτικές στην προεπεξεργασία των δεδομένων."]},{"cell_type":"markdown","metadata":{"id":"hPyBFR8XJK1U"},"source":["## Μείωση της διαστατικότητας με εξαγωγή χαρακτηριστικών\n","\n","Όπως είπαμε και προηγουμένως για να μειώσουμε τις διαστάσεις των μεταβλητών μας μπορούμε να κάνουμε δύο πράγματα: vα αφαιρέσουμε κατηγορίες που δεν προσφέρουν *σημαντική πληροφορία*, δηλαδή να κάνουμε **επιλογή μεταβλητών (feature selection)**. Εναλλακτικά, μπορούμε να κάνουμε εξαγωγή νέων χαρακτηριστικών σε ένα χώρο μικρότερων διαστάσεων (**feature extraction**). Η βασικότερη τεχνική feature extraction είναι η **ανάλυση σε κύριες συνιστώσες (principal components analysis - PCA)** όπου αναλύουμε τα δεδομένα σε κύριες συνιστώσες και δουλέυουμε με τελείως νέες, γραμμικά ασυσχέτιστες μεταβλητές μικρότερης διαστατικότητας."]},{"cell_type":"markdown","metadata":{"id":"UNgljy-eJK1X"},"source":["## Ανάλυση σε κύριες συνιστώσες\n","\n","Η ανάλυση σε κύριες συνιστώσες (PCA) είναι η ευρέως διαδεδομένη μέθοδος μείωσης της διαστατικότητας. Ούτε εδώ θα εμβαθύνουμε, αλλά θα εξηγήσουμε περιγραφικά τις αρχές της μεθόδου. Αρχικά υπολογίζουμε τον πίνακα συσχέτισης (covariance matrix) των μεταβλητών που έχουμε στα δεδομένα. Από αυτόν τον πίνακα βρίσκουμε τις γραμμικώς συσχετισμένες μεταβλητές και βρίσκοντας τα ιδιοδιανύσματα του πίνακα μπορούμε να μετατρέψουμε τον πίνακα με έναν ορθογώνιο μετασχηματισμό και να βρούμε την βάση του νέου πίνακα. Αυτή η βάση του χώρου αποτελεί ένα νέο σύνολο μεταβλητών που είναι *γραμμικά ασυσχέτιστες* και ονομάζονται κύριες συνιστώσες.\n","\n","![alt text](https://i.imgur.com/s5CfXoy.png)\n","\n","\n","[Ένα ωραίο online visual PCA demo](http://setosa.io/ev/principal-component-analysis/)\n","\n","Εκτός από τη μείωση της διαστατικότητας για καλύτερη ταξινόμηση, η PCA με 1 έως 3 συνιστώσες μπορεί να χρησιμοποιηθεί και για την οπτικοποίηση δεδομένων υψηλής διαστατικότητας. Περισσότερα για την PCA από το [indepth tutorial](https://jakevdp.github.io/PythonDataScienceHandbook/05.09-principal-component-analysis.html) του Python Data Science Handbook και το αντίστοιχο [jupyter notebook](https://github.com/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/05.09-Principal-Component-Analysis.ipynb).\n"]},{"cell_type":"code","metadata":{"id":"v25p32l7JK1X"},"source":["from sklearn.decomposition import PCA\n","\n","# Ορίζουμε την PCA και τον τελικό αριθμό features - αριθμό κύριων συνιστωσών\n","# είναι ακόμα μια υπερπαράμετρος με την οποία μπορούμε να πειραματιστούμε\n","n = 25\n","pca = PCA(n_components=n)\n","\n","# Εφαρμόζουμε στα δεδομένα εκπαίδευσης και ελέγχου τον *ΙΔΙΟ* μετασχηματισμό\n","# Οι κύριες συνιστώσες υπολογίζονται στο train set\n","# Στα train κάνουμε fit_transform στο test μόνο transform:\n","trainPCA = pca.fit_transform(train_resampled)\n","testPCA = pca.transform(test_scaled)\n","\n","print(train_resampled.shape)\n","print(trainPCA.shape)\n","print(\"\")\n","print(test_scaled.shape)\n","print(testPCA.shape)\n","\n","# πλεόν οι διαστάση των χαρακτηριστικών είναι 25"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"JMXmSWd1X_fx"},"source":["\n","Για να δούμε αν χάνεται πληροφοριά θα δούμε τι ποσοστό διακύμανσης εξηγείται σε συνάρτηση του αριθμού κύριων συνιστωσών"]},{"cell_type":"code","metadata":{"id":"6X5ncGgRJK1f"},"source":["# Θα τυπωσουμε το συσσωρευτικό ποσοστό διασποράς που εξηγείται από τις κύριες συνιστώσες\n","evar = pca.explained_variance_ratio_\n","cum_evar = np.cumsum(evar)\n","print(cum_evar)\n","plt.figure(1, figsize=(5, 5))\n","plt.xlabel(\"Principal Component number\")\n","plt.ylabel('Cumulative Variance')\n","plt.plot(cum_evar, linewidth=2)\n","plt.show()"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"9nldk_DtJK1j"},"source":["Με μόλις 15 κύριες συνιστώσες εξηγούμε 55% της διακύμανσης (πληροφορίας) του dataset, με 25 κοντά στο 70%.\n","\n","Τέλος, ας εφαρμόσουμε την PCA στα δεδομένα και ας ξανα\n","πάρουμε τα metrics του MLP:"]},{"cell_type":"code","metadata":{"id":"H09_-588JK1b"},"source":["clf = MLPClassifier(solver='lbfgs', alpha=1e-5,\n"," hidden_layer_sizes=(5,), random_state=1)\n","clf.fit(trainPCA, trainTargets_resampled)\n","preds = clf.predict(testPCA)\n","print(classification_report(C_testDataTargets, preds))"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"8fXVEy-xL5l0"},"source":["**ΠΑΡΑΤΗΡΗΣΕΙΣ**\n","* Στο συγκεκριμένο παράδειγμα, το MLP δουλεύει καλύτερα γιατί εφαρμόσαμε 4 μετασχηματιστές επεξεργασίας (variance threshold, standard scaler, oversampling, pca). Πιθανότατα το καθοριστικότερο ήταν η μείωση της διαστατικότητας γιατί ο λόγος samples / number of features είναι αρκετά μικρός (τυπικά έχουμε πολλαπλάσια ή τάξη μεγέθους περισσότερα samples από features.\n","* Το ποιους μετασχηματιστές προεπεξεργασίας θα εφαρμόσουμε και ποιες τιμές των παραμέτρων τους (variance threshold, αριθμός κυρίων συνιστωσών) θα αποδώσουν καλύτερα δεν το ξέρουμε από την αρχή, και μπορεί να είναι διαφορετικό ανάλογα τον ταξινομητή, ακόμα και στο ίδιο dataset. Έχουμε μόνο κάποιες εμπειρικές γνώσεις όπως ότι η κανονικοποίηση γενικά βοηθάει, ότι τα samples πρέπει να είναι αρκετά περισσότερα από τα features κλπ\n","\n","\n"]}]}