{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "name": "06. Text Mining 1.1 Intro.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "cells": [ { "cell_type": "code", "metadata": { "id": "QJNKuWwUYzsJ" }, "source": [ "!pip install --upgrade scikit-learn\n", "!pip install --upgrade numpy\n", "!pip install --upgrade scipy\n", "!pip install --upgrade nltk\n", "!pip install --upgrade matplotlib\n", "%matplotlib inline" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "j3AJwwzFsa6f" }, "source": [ "\n", "# Εξόρυξη κειμένου (Text Mining)\n", "\n", "To Text Mining είναι ένα σύνολο αυτόματων (μηχανικών) τεχνικών που στοχεύουν στην εξαγωγή υψηλής ποιότητας πληροφορίας από κειμενική πληροφορία. Ο ασυμπτωτικός ορίζοντας του text mining είναι η συνολική σημασιολογική κατανόηση του ανθρώπινου λόγου, κάτι όμως που ανήκει στους δύσκολους στόχους της λεγόμενης **Ισχυρής Τεχνητής Νοημοσύνης** (*Strong AI*). Στο δρόμο για την επίτευξη αυτού του στόχου η έρευνα στο text mining επικεντρώνεται σε μια σειρά πιο συγκεκριμένων και άρα περισσότερο προσιτών στόχων - tasks (*Weak AI*) όπως (μεταξύ άλλων):\n", "- **Κατηγοριοποίηση κειμένων** (*text categorization*) - Ταξινόμηση με βάση το περιεχόμενο σε συγκεκριμένες θεματικές κατηγορίες\n", "- **Συσταδοποίηση** (*text clustering*) - Συσταδοποίηση \"κοντινών\" σημασιολογικά κειμένων\n", "- **Εξαγωγή θεμάτων** (*topic extraction*) - Ανακάλυψη των θεμάτων που περιέχει ένα κείμενο\n", "- **Εξαγωγή εννοιών και οντοτήτων** (*concept/entity extraction*) - Σε ποιες έννοιες και οντότητες του φυσικού κόσμου αναφέρεται το κείμενο.\n", "- **Ανάλυση συναισθήματος** (*sentiment analysis*) - Χαρακτηρισμός του συναισθήματος\n", "- **Αυτόματη περίληψη** (*document summarization*) - Δημιουργία αυτόματης περίληψης\n", "- **Μοντελοποιηση σχέσεων μεταξύ οντοτήτων** (*entity relation modeling*) - Ποιες σχέσεις διέπουν τις οντότητες που εντοπίζονται εντός του κειμένου.\n", "- **Απάντηση ερωτήσεων** *(question answering*) - απάντηση ερώτησης και τα δύο σε φυσική γλώσσα\n", "\n", "\n", "Στην εξόρυξη κειμένου συνδυάζονται τεχνικές και προσεγγίσεις που προέρχονται από τη θεωρία της πληροφορίας και τη στατιστική, την αναγνώριση προτύπων, την εξόρυξη δεδομένων, τη μηχανική μάθηση, την ανάκτηση πληροφορίας, την επεξεργασία φυσικής γλώσσας (Natural Language Processing - NLP), τη γλωσσολογία, την αναπαράσταση γνώσεων, τις οντολογίες κ.α." ] }, { "cell_type": "markdown", "metadata": { "id": "fmKJbF4UVJOL" }, "source": [ "Για την εξόρυξη κειμένου και την επεξεργασία φυσικής γλώσσας θα βασιστούμε στο [Natural Language Toolkit](http://www.nltk.org/) της Python\n", "\n", "Περισσότερα θα δείτε στο [nltk book](http://www.nltk.org/book/)" ] }, { "cell_type": "code", "metadata": { "id": "64d0RcciVJOM" }, "source": [ "import numpy as np\n", "import nltk" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "a_RhBsaIVJOR" }, "source": [ "## Εισαγωγή κειμένων στο notebook\n", "\n", "\n", "Το NLTK από μόνο του έχει μόνο τις πολύ βασικές λειτουργίες. Για πιο σύνθετα πράγματα (τα οποία θα χρειαστούμε) χρειάζεται να κατεβάσουμε επιπλέον δυνατότητες της βιβλιοθήκης. Όταν τρέχουμε τοπικά την Python, αυτό μπορούμε να το κάνουμε μέσω της εντολής `nltk.download()`, η οποία ανοίγει ένα παράθυρο όπου επιλέγουμε ποιες λειτουργίες μας ενδιαφέρει να κατεβάσουμε. Σε κάποιες cloud πλατφόρμες αυτό δεν είναι δυνατό, γι' αυτό πρέπει να τα κατεβάζουμε ένα ένα τα επιπλέον πακέτα, όπως θα δούμε παρακάτω." ] }, { "cell_type": "markdown", "metadata": { "id": "yoaSYPnWoL5C" }, "source": [ "\n", "\n", "### Από βιβλιοθήκες της Python\n", "\n", "Στα πλαίσια της άσκησης θα χρησιμοποιήσουμε το [reuters dataset](https://archive.ics.uci.edu/ml/datasets/reuters-21578+text+categorization+collection). \n", "\n", "Το σώμα (corpus) κειμένων Reuters περιέχει 10788 κείμενα ειδήσεων. Κάθε κείμενο (document) ανήκει σε μία ή περισσότερες από 90 θεματικές κατηγορίες ειδήσεων που έχουν να κάνουν κυρίως με εμπορικά και χρηματιστηριακά αγαθά και υπηρεσίες (πχ \"fuel\", \"cotton\", \"ship\" κλπ). To Reuters είναι ήδη χωρισμένο σε train και test set.\n", "Μπορούμε να εισάγουμε το Reuters μέσω του NLTK:\n" ] }, { "cell_type": "code", "metadata": { "id": "RWZXceaPVJOR" }, "source": [ "nltk.download('reuters') # κατεβάζουμε το dataset\n", "\n", "from nltk.corpus import reuters # το κάνουμε import\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "MK0sHMNDc9Br" }, "source": [ "\n", "και τυπώνουμε κάποια βασικά χαρακτηριστικά:\n", "\n" ] }, { "cell_type": "code", "metadata": { "id": "Yp4tsAJLclo6" }, "source": [ "def collection_stats():\n", " # List of documents\n", " documents = reuters.fileids()\n", " print(str(len(documents)) + \" documents\");\n", " \n", " train_docs = list(filter(lambda doc: doc.startswith(\"train\"),\n", " documents));\n", " print(str(len(train_docs)) + \" total train documents\");\n", " \n", " test_docs = list(filter(lambda doc: doc.startswith(\"test\"),\n", " documents));\n", " print(str(len(test_docs)) + \" total test documents\");\n", " \n", " # List of categories\n", " categories = reuters.categories();\n", " print(str(len(categories)) + \" categories\");\n", "\n", "collection_stats()\n", "print(reuters.categories()[:20], '...')" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "T_qroRjPbhcS" }, "source": [ "Για ένα τυχαίο document μπορούμε να δούμε τις κατηγορίες που ανήκει και το ίδιο το κείμενο χρησιμοποιώντας το id του:" ] }, { "cell_type": "code", "metadata": { "id": "TtI_Oqxsb8uJ" }, "source": [ "def describe_doc(document_id):\n", " # Raw categories\n", " print(\"Categories\")\n", " doc_categories = reuters.categories(document_id) \n", " print(doc_categories)\n", " # Raw document\n", " print(\"Document\")\n", " print(reuters.raw(document_id));\n", "\n", "doc_id = 'training/9880'\n", "describe_doc(doc_id)\n", "doc_id = 'training/9865'\n", "describe_doc(doc_id)\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "yqg9kYW7moPW" }, "source": [ "Το Reuters χρησιμοποιείται συχνά για την μελέτη της απόδοσης αλγορίθμων Μηχανικής Μάθησης στην κατηγοριοποίηση ή ομάδοποιήση κειμένων." ] }, { "cell_type": "markdown", "metadata": { "id": "kcNsEV60VJOd" }, "source": [ "### Από το internet" ] }, { "cell_type": "code", "metadata": { "id": "cMLHmozBVJOe" }, "source": [ "import urllib\n", "\n", "# ορίζουμε το url που περιέχει το κείμενο (εδώ το Moby Dick)\n", "url = 'https://www.mirrorservice.org/sites/ftp.ibiblio.org/pub/docs/books/gutenberg/2/7/0/2701/2701-0.txt'\n", "\n", "with urllib.request.urlopen(url) as response:\n", " raw = response.read()\n", "\n", "#τυπώνουμε ένα κομμάτι του κειμένου\n", "text_chunk=raw[10000:11000]\n", "print(text_chunk)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "sOIlekSrk7W4" }, "source": [ "Το κείμενο είναι σε κωδικοποίηση Unicode UTF-8. Θα το μετατρέψουμε σε εκτυπώσιμους χαρακτήρες." ] }, { "cell_type": "code", "metadata": { "id": "lKb1lugrj9qU" }, "source": [ "s = text_chunk.decode('utf-8')\n", "print(s)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "EKDKy_NMVJOh" }, "source": [ "### Από τοπικό αρχείο \n", "\n", "Έστω ότι έχω ένα αρχείο στον υπολογιστή μου με όνομα `mydoc.txt` (κατεβάστε ένα από [εδώ](https://drive.google.com/uc?export=download&id=1WF31UdA9kmM5vmqgtvjTxoW0hHi0CmAV)). Αυτό πρέπει πρώτα να το ανεβάσουμε στο περιβάλλον του notebook. Γενικά αυτή η διαδικασία διαφέρει από cloud σε cloud, οπότε ο παρακάτω κώδικας θα τρέξει μόνο σε περιβάλλον Google Colaboratory. Αντίστοιχες διαδικασίες ανεβάσματος αρχείου από τον τοπικό υπολογιστή υπάρχουν και για τα υπόλοιπα cloud που έχουμε δείξει." ] }, { "cell_type": "code", "metadata": { "id": "drJ5uCxVnZDp" }, "source": [ "from google.colab import files\n", "\n", "uploaded = files.upload()\n", "\n", "for fn in uploaded.keys():\n", " print('User uploaded file \"{name}\" with length {length} bytes'.format(\n", " name=fn, length=len(uploaded[fn])))" ], "execution_count": null, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "NzYTrrzBndhX" }, "source": [ "!ls" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "Oo4gforhnyHw" }, "source": [ "Διαβάζω το περιεχόμενο του αρχείο μέσα στο string \"document\"" ] }, { "cell_type": "code", "metadata": { "id": "uJQ97LXDVJOp" }, "source": [ "with open('mydoc.txt', 'r') as f:\n", " document = ''\n", " for line in f:\n", " document += line\n", "\n", "print(document)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "XnlcPw_ivn_p" }, "source": [ "## Μοντέλο διανυσματικού χώρου (Vector Space Model)\n", "\n", "Ως σημείο εκκίνησης λαμβάνουμε ότι διαθέτουμε μια συλλογή από κείμενα (αρχεία text) και ότι οι αλγόριθμοι μηχανικής μάθησης που χρησιμοποιούμε λαμβάνουν στην είσοδο αριθμητικές τιμές (διανύσματα). Ένα πρώτο και πολύ βασικό ερώτημα λοιπόν είναι πως μπορούμε να μετατρέψουμε τα κείμενα σε κατάλληλη διανυσματική μορφή. Τί θα αποτελούσε όμως \"κατάλληλη διανυσματική μορφή\";\n", "\n", "Μια απάντηση που μπορούμε να δώσουμε από την υπολογιστική σκοπιά είναι ότι αν κάθε κείμενο της συλλογής μετατραπεί σε ένα διάνυσμα, θα θέλαμε η μετατροπή αυτή να κρατήσει τη σημασιολογική πληροφορία των κειμένων έτσι ώστε κείμενα που το κειμενικό τους περιεχόμενο είναι σημασιολογικά \"κοντινό\" (μιλάνε για κοντινά θέματα) να αντιστοιχούν σε σημεία του διανυσματικού χώρου αναπαράστασης που είναι κοντά μεταξύ τους και το αντίστροφο για κείμενα με ανόμοιο περιεχόμενο.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "vOY46Yu_B0Cu" }, "source": [ "\n", "### Σάκος λέξεων (bag of words)\n", "\n", "Ας θεωρήσουμε χωρίς βλάβη της γενικότητας την ακόλουθη μικρή συλλογή κειμένων (documents): \n", "\n", "d1 = \"a big black cat\"\n", "\n", "d2 = \"a cat and a dog\"\n", "\n", "d3 = \"a lovely town\"\n", "\n", "Τα d1 και d2 έχουν μεταξύ τους κοινό σημασιολογικό περιεχόμενο και δεν έχουν με το d3. Κατασκευάζουμε ένα διάνυσμα του οποίου κάθε χαρακτηριστικό είναι κάθε μοναδική λέξη της συλλογής μας σε αλφαβητική σειρά δηλαδή:\n", "\n", "\\[ a and big black cat dog lovely town \\]\n", "\n", "Με βάση αυτό τα 8 χαρακτηριστικά τώρα, αναπαριστούμε κάθε document με ένα διάνυσμα όπου τα χαρακτηριστικά λαμβάνουν τιμές ίσες με τη συχνότητα εμφάνισης της κάθε λέξης (term frequency) στο συγκεκριμένο document: \n", "\n", "d1 = \\[ 1 0 1 1 1 0 0 0 \\]\n", "\n", "d2 = \\[ 2 1 0 0 1 1 0 0 \\]\n", "\n", "d3 = \\[ 1 0 0 0 0 0 1 1 \\]\n", "\n", "Αυτή είναι το βασικό μοντέλο (αναπαράστασης) διανυσματικού χώρου που χρησιμοποιεί τις συχνότητες εμφάνισης κάθε λέξης. Εξαιτίας του γεγονότος ότι αγνοούμε τη σειρά των λέξεων (το \"a big black cat\" έχει την ίδια διανυσματική αναπαράσταση με το \"cat big a black\") το ονομάζουμε σάκο λέξεων - bag of words (BOW). \n", "\n", "Ας τα περάσουμε στο numpy και να δοκιμάσουμε να υπολογίσουμε αποστάσεις μεταξύ διανυσμάτων:" ] }, { "cell_type": "code", "metadata": { "id": "9CIQMyt4Gv6q" }, "source": [ "d1 = np.array([1,0,1,1,1,0,0,0])\n", "d2 = np.array([2,1,0,0,1,1,0,0])\n", "d3 = np.array([1,0,0,0,0,0,1,1])\n", "\n", "#Ευκλείδεια απόσταση\n", "\n", "d1d2 = print(\"d1 με d2\", np.linalg.norm(d1-d2))\n", "d1d3 = print(\"d1 με d3\", np.linalg.norm(d1-d3))\n", "d2d3 = print(\"d2 με d3\", np.linalg.norm(d2-d3))" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "7pScjhAZJMTM" }, "source": [ "Συνεπώς βλέπουμε ότι ακόμα δεν έχουμε πετύχει αυτό που θέσαμε ως αρχικό στόχο, τα d1 και d2 να είναι πιο κοντά μεταξύ τους απ' ότι είνα με το d3. Στην πράξη δεν χρησιμοποιούμε την ευκλείδεια απόσταση γιατί για παράδειγμα αν πάρουμε το\n", "\n", "d4 = \"a big black cat a big black cat\"\n", "\n", "που δεν έχει καμία σημασιολογική μονάδα πληροφορίας (λέξη) διαφορετική από το d1 (και άρα θα έπρεπε να έχει απόσταση 0) το οποίο έχει διανυσματική αναπαράσταση το διπλάσιο του d1\n", "\n", "d4 = \\[ 2 0 2 2 2 0 0 0 \\]\n", "\n", "Με ευκλείδεια απόσταση θα λάβουμε:" ] }, { "cell_type": "code", "metadata": { "id": "HlsolnvpLSBs" }, "source": [ "d4 = np.array([2,0,2,2,2,0,0,0])\n", "d1d4 = print(\"d1 με d4\", np.linalg.norm(d1-d4))" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "Z6Zh4ryILpPP" }, "source": [ "### Ομοιότητα συνημιτόνόυ (cosine similarity)\n", "\n", "Για το λόγο αυτό, στο Vector Space Model χρησιμοποιούμε την απόσταση (ή ομοιότητα) συνημιτόνου (cosine similarity): \n", "\n", "$ {\\text{similarity}}=\\cos(\\theta )={\\mathbf {A} \\cdot \\mathbf {B} \\over \\|\\mathbf {A} \\|\\|\\mathbf {B} \\|}={\\frac {\\sum \\limits _{i=1}^{n}{A_{i}B_{i}}}{{\\sqrt {\\sum \\limits _{i=1}^{n}{A_{i}^{2}}}}{\\sqrt {\\sum \\limits _{i=1}^{n}{B_{i}^{2}}}}}}$\n" ] }, { "cell_type": "code", "metadata": { "id": "FcoNf4CeL9Wy" }, "source": [ "import scipy as sp\n", "\n", "cosd1d4 = sp.spatial.distance.cosine(d1, d4)\n", "print(cosd1d4)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "ap8y7fDKMW3a" }, "source": [ "και αντίστοιχα:" ] }, { "cell_type": "code", "metadata": { "id": "Jn5n6KSsMav0" }, "source": [ "cosd1d2 = sp.spatial.distance.cosine(d1, d2)\n", "cosd1d3 = sp.spatial.distance.cosine(d1, d3)\n", "cosd2d3 = sp.spatial.distance.cosine(d2, d3)\n", "\n", "print(\"d1 με d2\", cosd1d2)\n", "print(\"d1 με d3\", cosd1d3)\n", "print(\"d2 με d3\", cosd2d3)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "IYdHyGzqN1ak" }, "source": [ "Η ομοιότητα (απόσταση) συνημιτόνου κανονικοποιεί με βάση τις νόρμες (μήκος) των κειμένων (άρα δεν έχουμε διανύσματα διαφορετικών μηκών) και είναι 1 (0) για διανύσματα που έχουν ακριβώς την ίδια \"γωνία\" στον υπερχώρο διαστάσεων Ν (o αριθμός των μοναδικών λέξεων) του vector space model.\n", "\n", "\n", "![Imgur](https://i.imgur.com/Kl6MFc8.png)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "frHOFj8VoYui" }, "source": [ "## Μετατροπή κειμένων σε διανύσματα\n" ] }, { "cell_type": "markdown", "metadata": { "id": "HSn9R6mUPWKT" }, "source": [ "Στη συνέχεια παρουσιάζουμε μερικά κλασικά βήματα που χρησιμοποιούμε για τη μετατροπή των κειμένων σε διανύσματα στο VSM. Τα βήματα αυτά περιλαμβάνουν κάποιες βελτιώσεις σε σχέση με το βασικό μοντέλο.\n", "\n", "Επειδή αν έχουμε μια μεγάλη συλλογή κειμένων το να λάβουμε όλες τις μοναδικές λέξεις ως χαρακτηριστικά του VSM μπορεί να οδηγήσει σε πάρα πολύ μεγάλες διαστάσεις, είναι βασικό μας μέλημα να χρησιμοποιούμε διάφορες τεχνικές ώστε να περιορίζουμε όσο είναι δυνατό αυτή τη διαστατικότητα χωρίς να χάνουμε περιεχόμενο (σημασία). " ] }, { "cell_type": "markdown", "metadata": { "id": "xrbaAeBTVJOu" }, "source": [ "### Προεπεξεργασία κειμένου\n", "\n", "Τώρα που φορτώσαμε το κείμενο στην python, πρέπει να το επεξεργαστούμε. Επειδή ο υπολογιστής θεωρεί τα κεφαλαία και τα μικρά ως διαφορετικούς χαρακτήρες, το πρώτο πράγμα που πρέπει να κάνουμε είναι να τα κάνουμε **όλα πεζά**. Έπειτα θέλουμε να **χωρίσουμε τις λέξεις μια προς μια**, ώστε να φτιάξουμε μια λίστα τα στοιχεία της οποίας θα είναι οι λέξεις." ] }, { "cell_type": "code", "metadata": { "id": "EQV7lIyYVJOu" }, "source": [ "documents = [\"Lionel Messi is the best football player in the world! Messi plays for Barcelona Football Club. Barcelona Football Club plays in the Spanish Primera Division.\",\n", " \"Lionel Messi a football player, playing for Barcelona Football Club, a Spanish football team.\", \n", " \"Barcelona is a city in a northern spanish province called Catalonia. It is the largest city in Catalonia and the second most populated spanish city.\", \n", " \"Python is a programming language. Python is an object-oriented programming language. Unlike COBOL, Python is a interpreted programming language.\", \n", " \"COBOL is a compiled computer programming language designed for business use. This programming language is imperative, procedural and, since 2002, object-oriented. But Python is better.\"]\n", "\n", "document = documents[1]\n", "\n", "nltk.download('punkt') # χρειάζεται για το tokenizer\n", "words = nltk.word_tokenize(document)\n", "\n", "print(words)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "6LhkTcBnVJOy" }, "source": [ "Το tokenizer ουσιαστικά κάνει ό,τι και η built-in μέθοδος `.split()` των string, αλλά λίγο πιο έξυπνα. Για αρχή χωρίζει με βάση τόσο τα κενά (`' '`), όσο και τα tabs (`'\\t'`) και τα new lines (`'\\n'`). Επίσης όπως μπορούμε να δούμε και παραπάνω χωρίζει και τις παρενθέσεις από το περιεχόμενό τους.\n", "\n", "Το επόμενο βήμα είναι να διαγράψουμε από τη λίστα μας τα **σημεία στίξης**. Μόλις το κάνουμε αυτό, θέλουμε να διαγράψουμε και μερικές συχνά χρησιμοποιούμενες λέξεις που δεν προσφέρουν σημασιολογική αξία στο κείμενο (**stopwords**). Τυπικά stopwords στα αγγλικά είναι λέξεις όπως \"the\", \"a\", \"to\", \"and\", \"he\", \"she\" κοκ." ] }, { "cell_type": "code", "metadata": { "id": "p6pypWyaVJOz" }, "source": [ "nltk.download('stopwords') # κατεβάζουμε ένα αρχείο που έχει stopwords στα αγγλικά\n", "from nltk.corpus import stopwords\n", "import string\n", "\n", "'''\n", "filtered_words = [word for word in words if word not in list(string.punctuation)] # το string.punctuation είναι απλά ένα\n", " # string που περιέχει όλα τα σημεία στίξης\n", "\n", "filtered_words = [word for word in filtered_words if word not in stopwords.words('english')] # το stopwords.words('english')\n", " # είναι μια λίστα που περιέχει\n", " # stopwords στα αγγλικά\n", "'''\n", "filtered_words = [word for word in words if word not in stopwords.words('english') + list(string.punctuation)]\n", "\n", "print(filtered_words)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "BlSuSAUzVJO3" }, "source": [ "Πρέπει να κάνουμε καλύτερη δουλειά στην αφαίρεση των σημείων στίξης γιατί δεν αφαιρούνται οι λέξεις που περιέχουν περισσότερα από ένα τέτοια σημεία." ] }, { "cell_type": "code", "metadata": { "id": "VcLJukuIVJO4" }, "source": [ "def thorough_filter(words):\n", " filtered_words = []\n", " for word in words:\n", " pun = []\n", " for letter in word:\n", " pun.append(letter in string.punctuation)\n", " if not all(pun):\n", " filtered_words.append(word)\n", " return filtered_words\n", " \n", "filtered_words = thorough_filter(filtered_words)\n", "print(filtered_words)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "WCySdKVxVJO9" }, "source": [ "### Stemming & Lemmatization\n", "\n", "Για γραμματικούς λόγους, τα κείμενα χρησιμοποιούν διαφορετικές μορφές μιας λέξης, όπως π.χ. *play*, *plays*, *playing*, *played*. Αυτό έχει σαν αποτέλεσμα πως, ενώ αναφερόμαστε σε κάποιο παρόμοιο σημασιολογικό περιεχόμενο, ο υπολογιστής τις καταλαβαίνει ως διαφορετικές και προσθέτει διαστάσεις στην αναπαράσταση. Για να λύσουμε αυτό το πρόβλημα, μπορούμε να χρησιμοποιήσουμε δύο γλωσσολογικούς μετασχηματισμούς, είτε την αφαίρεση της κατάληξης (stemming), είτε τη λημματοποίηση (lemmatization). Ο στόχος, τόσο της αφαίρεσης κατάληξης όσο και της λημματοποίησης, είναι να φέρουν τις διάφορες μορφές της λέξης σε μια κοινή μορφή βάσης. Πιο συγκεκριμένα:\n", "\n", "Η **αφαίρεση της κατάληξης** αναφέρεται σε μια ακατέργαστη ευριστική διαδικασία που απομακρύνει τα άκρα των λέξεων με την ελπίδα να επιτύχει αυτό το στόχο σωστά τις περισσότερες φορές.\n", "\n", "Η **λημματοποίηση** αναφέρεται στην απομάκρυνση της κλίσης των λέξεων και στην επιστροφή της μορφής της λέξης όπως θα τη βρίσκαμε στο λεξικό, με τη χρήση λεξιλογίου και μορφολογικής ανάλυσης των λέξεων. Η μορφή αυτή είναι γνωστή ως λήμμα (*lemma*)." ] }, { "cell_type": "code", "metadata": { "id": "bTe8-vDgVJO-" }, "source": [ "nltk.download('wordnet') # απαραίτητα download για τους stemmer/lemmatizer\n", "nltk.download('rslp')\n", "\n", "from nltk.stem import WordNetLemmatizer\n", "wordnet_lemmatizer = WordNetLemmatizer()\n", "\n", "from nltk.stem.porter import PorterStemmer\n", "porter_stemmer = PorterStemmer()\n", "\n", "lem_words = [wordnet_lemmatizer.lemmatize(word) for word in filtered_words]\n", "stem_words = [porter_stemmer.stem(word) for word in filtered_words]\n", "\n", "print('\\n{:<20} {:<20} {:<20}'.format('Original', 'Stemmed', 'Lemmatized'))\n", "print('-'*60)\n", "for i in range(len(filtered_words)):\n", " print('{:<20} {:<20} {:<20}'.format(filtered_words[i], stem_words[i], lem_words[i]))" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "3828s8RpVJPB" }, "source": [ "**Προσοχή:** χρησιμοποιούμε είτε stemming (πιο συχνά), είτε lemmatization, αλλά όχι και τα δύο μαζί. Το πρώτο βελτιώνει την ανάκληση, το δεύτερο την ακρίβεια.\n", "\n", "Αφότου έχουμε ολοκληρώσει τις γλωσσολογικές προεπεξεργασίες, θα ορίσουμε μια μικρή συλλογή κειμένων ώστε να προχωρήσουμε ένα παράδειγμα ομαδοποίησης κειμένων. \n", "\n", "Όπως βλέπουμε παρακάτω τα πρώτα δύο και τα τελευταία δύο κείμενα βρίσκονται σημασιολογικά κοντά μεταξύ τους." ] }, { "cell_type": "code", "metadata": { "id": "hjhcVYkhVJPI" }, "source": [ "# Το νέο σύνολο κειμένων μας\n", "documents = [\"Lionel Messi is the best football player in the world! Messi plays for Barcelona Football Club. Barcelona Football Club plays in the Spanish Primera Division.\",\n", " \"Lionel Messi a football player, playing for Barcelona Football Club, a Spanish football team.\", \n", " \"Barcelona is a city in a northern spanish province called Catalonia. It is the largest city in Catalonia and the second most populated spanish city.\", \n", " \"Python is a programming language. Python is an object-oriented programming language. Unlike COBOL, Python is a interpreted programming language.\", \n", " \"COBOL is a compiled computer programming language designed for business use. This programming language is imperative, procedural and, since 2002, object-oriented. But Python is better.\"]\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "BhosxSZKUZaI" }, "source": [ "Κάνουμε τη γνωστή μας προεπεξεργασία και τυπώνουμε τη συχνότητα κάθε token σε κάθε document." ] }, { "cell_type": "code", "metadata": { "id": "j1_d_UMzTYrk" }, "source": [ "import collections\n", "\n", "def preprocess_document(document):\n", " # όλα τα προηγούμενα βήματα που κάναμε μέχρι στιγμής\n", " words = nltk.word_tokenize(document.lower())\n", " filtered_words = [word for word in words if word not in stopwords.words('english') + list(string.punctuation)]\n", " filtered_words = thorough_filter(filtered_words)\n", " stemmed_words = [porter_stemmer.stem(wordnet_lemmatizer.lemmatize(word)) for word in filtered_words]\n", " cnt = collections.Counter(stemmed_words)\n", " return cnt\n", "\n", "preprocessed_documents = [preprocess_document(doc) for doc in documents]\n", "\n", "for doc in preprocessed_documents:\n", " print(doc)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "X-JniL0wVJPN" }, "source": [ "Μια τυπική μέθοδος μείωσης της διασταστικότητας είναι να πετάμε τους πάρα πολύ συχνούς όρους, τους πάρα πολύ σπάνιους ή τους όρους που εμφανίζονται σε πολύ λίγα documents (μιλάμε πάντα για το σύνολο της συλλογής, όχι τα μεμονωμένα documents).\n", "\n", "Στο μικρό αυτό dataset αποφασίζουμε να πετάξουμε τους όρους που εμφανίζονται μόνο μία φορά στο σύνολο της συλλογής" ] }, { "cell_type": "code", "metadata": { "id": "bU2vMn9DDVeu" }, "source": [ "threshold = 1\n", "\n", "total_counter = preprocessed_documents[0]\n", "\n", "for i in range(1, len(preprocessed_documents)):\n", " total_counter += preprocessed_documents[i] # counter που περιέχει τα συνολικά αθροίσματα σε όλα τα κείμενα\n", "\n", "print(total_counter, '\\n')\n", "\n", "vocabulary = [word for word in total_counter if total_counter[word] > threshold] # κρατάμε μόνο τους όρους με συχνότητα εμφάνισης πάνω από το κατώφλι\n", "preprocessed_documents = [preprocess_document(doc) for doc in documents]\n", "\n", "\n", "print(vocabulary)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "yFY2a06iVJPR" }, "source": [ "Για την ευκολία μας θα δημιουργήσουμε έναν πίνακα που στις γραμμές του θα έχει τα documents και στις στήλες του τις λέξεις και θα αποθηκεύσουμε μέσα σε αυτόν τον αριθμό εμφάνισης των όρων. " ] }, { "cell_type": "code", "metadata": { "id": "pUIfU63sVJPS" }, "source": [ "freq_array = np.zeros((len(preprocessed_documents), len(vocabulary)))\n", "\n", "for i in range(len(preprocessed_documents)):\n", " for j in range(len(vocabulary)):\n", " freq_array[i,j] = preprocessed_documents[i][vocabulary[j]] \n", "\n", "print(vocabulary, '\\n')\n", "print(freq_array)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "vmawczqZVJPW" }, "source": [ "### TF-IDF\n", "\n", "Η αναπαράσταση στο VSM μόνο με τις συχνότητες εμφάνισης κάθε όρου δεν είναι βέλτιστη. Στην πράξη χρησιμοποιούμε το **TF-IDF** (Term Frequency - Inverse Document Frequency).\n", "\n", "Όπως προσδίδει και το όνομά του, το tf-idf αποτελείται από 2 όρους. Ο πρώτος είναι το **Term Frequency (TF)**:\n", "\n", "$$ tf(i,d) = \\frac{f(i,d)}{\\sum_{i} f(i,d)}$$\n", "\n", "Όπου *i* ο όρος στο κείμενο *d*. Το tf είναι στην ουσία η συχνότητα με την οποία εμφανίζεται ο κάθε όρος στο κείμενο. Λέξεις με μεγάλη συχνότητα είναι σημαντικότερες για το κείμενο από ό,τι λέξεις με μικρή." ] }, { "cell_type": "code", "metadata": { "id": "YXLO-r_RVJPX" }, "source": [ "print(freq_array.sum(axis=1)), '\\n' # ο αριθμός των όρων ανά κείμενο\n", "\n", "\n", "for i in range(len(freq_array)):\n", " freq_array[i, :] = freq_array[i, :] / freq_array.sum(axis=1)[i] # Η συχνότητα του όρου (αριθμός εμφάνισης όρου / συνολικοί όροι στο κείμενο)\n", "\n", "print(freq_array)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "n-ft9F1eVJPa" }, "source": [ "Ο δεύτερος όρος στο tf-idf είναι το **Inverse Document Frequency**:\n", "\n", "$$ idf(i) = log \\frac{N}{df(i)}$$\n", "\n", "Όπου *Ν* ο αριθμός των κειμένων και *df(i)* ο αριθμός των κειμένων στους οποίους εμφανίζεται ο όρος *i*. Το idf είναι ένας δείκτης της πληροφορίας που δίνει η κάθε λέξη. Αν η λέξη εμφανίζεται σε όλα τα κείμενα τότε αυτή δε δίνει καθόλου πληροφορία και το κλάσμα θα γίνει 1, άρα ο λογάριθμος θα μας δώσει την τιμή 0. Αντίθετα σε όσο πιο λίγα κείμενα εμφανίζεται η λέξη, τόσο πιο μεγάλη τιμή θα έχει το κλάσμα. " ] }, { "cell_type": "code", "metadata": { "id": "YmsGVtFDVJPb" }, "source": [ "non_zero_elements_per_row = np.zeros((len(freq_array[0])))\n", "\n", "for i in range(len(freq_array)):\n", " for j in range(len(freq_array[0])):\n", " if freq_array[i,j]>0.0:\n", " non_zero_elements_per_row[j] += 1\n", "\n", "#non_zero_elements_per_row = np.count_nonzero(freq_array, axis=0)\n", "\n", "idf = np.log10(float(len(freq_array))/non_zero_elements_per_row) # ο αριθμητής του κλάσματος είναι ο αριθμός των κειμένων μας \n", " # (ή ο αριθμός των γραμμών στον πίνακα freq_array)\n", " # η np.count_zero μετράει πόσα μη μηδενικά στοιχεία έχει ο πίνακας \n", " # (στην περίπτωσή μας ο παρονομαστής του κλάσματος του idf)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "BhOpEdJ6VJPd" }, "source": [ "Το tf-idf τελικά υπολογίζεται ως το γινόμενο των δύο όρων:\n", "\n", "$$ tfidf(i) = tf(i,d) \\cdot idf(i)$$" ] }, { "cell_type": "code", "metadata": { "id": "oakbvK76VJPe", "scrolled": true }, "source": [ "tf_idf = freq_array * idf # το tf-idf είναι απλά το γινόμενο του tf με το idf\n", "\n", "print(tf_idf)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "eKCWHEfuVJPi" }, "source": [ "Για να δούμε ποιο κείμενο βρίσκεται πιο κοντά στο άλλο, υπολογίζουμε απλά τις αποστάσεις του ενός διανύσματος απ' το άλλο." ] }, { "cell_type": "code", "metadata": { "id": "QyW1IV6zVJPi", "scrolled": true }, "source": [ "distances = np.zeros((len(tf_idf), len(tf_idf)))\n", "\n", "import scipy as sp\n", "for i in range(len(tf_idf)):\n", " for j in range(len(tf_idf)):\n", " distances[i,j]= sp.spatial.distance.cosine(tf_idf[i],tf_idf[j])\n", " #distances[i,j] = sum(np.abs(tf_idf[i] - tf_idf[j])) # το ίδιο με ευκλείδια, εδώ είναι λιγότερο κακή γιατί έχουμε κανονικοποιησει ως προς το μήκος\n", "print(distances)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "N65CJy19VJPm" }, "source": [ "Όπως παρατηρούμε τα πρώτα 2 και τα τελευταία 2 διανύσματα του πίνακα έχουν πολύ μικρή απόσταση (της τάξης του 0.1). Αντίθετα όλα τα υπόλοιπα έχουν απόσταση μεγαλύτερη από 0.5.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "w0T-s4Zx19cL" }, "source": [ "## Εφαρμογή: Ομαδοποίηση κειμένων" ] }, { "cell_type": "markdown", "metadata": { "id": "oWmjWb8H159y" }, "source": [ "\n", "### Ιεραρχικό Clustering\n", "\n", "Στην εφαρμογή αυτή θα χρησιμοποιήσουμε ιεραρχικό clustering με την [μέθοδο ελαχιστοποίησης της διασποράς του Ward](https://en.wikipedia.org/wiki/Ward%27s_method). Ο αλγόριθμος αυτός ψάχνει αναδρομικά να βρει το ζεύγος των cluster που αν τα ενώσουμε θα δώσει την ελάχιστη αύξηση στη συνολική εσωτερική διασπορά των cluster. (Σημ. εσωτερική διασπορά θεωρούμε τη διασπορά όλων των σημείων από το κέντρο του cluster και ορίζεται για κάθε cluster ξεχωριστά. Συνολική εσωτερική διασπορά θεωρούμε το άθροισμα όλων των εσωτερικών διασπορών για κάθε cluster).\n", "\n", "Αρχικά θεωρεί ότι το κάθε σημείο είναι κι από ένα cluster. Έπειτα ψάχνει να βρει ποιο ζεύγος σημείων, αν ενωθούν σε ένα cluster, θα οδηγήσουν στην ελάχιστη αύξηση της συνολικής εσωτερικής διασποράς. Προφανώς, στην περίπτωση αυτή θα είναι τα δύο πιο κοντινά σημεία. Η διαδικασία αυτή επαναλαμβάνεται μέχρις ότου να καταλήξουμε σε 2 ομάδες. \n", "\n", "Αυτό μπορεί να υλοποιηθεί πολύ απλά στο [scikit-learn](https://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html) στο προηγούμενο παράδειγμα με τις προτάσεις:" ] }, { "cell_type": "code", "metadata": { "id": "0gzxgxaXVJPn" }, "source": [ "import matplotlib.pyplot as plt\n", "\n", "from scipy.cluster import hierarchy\n", "Z = hierarchy.linkage(tf_idf, 'ward') # εκπαιδεύει τον αλγόριθμο\n", "plt.figure()\n", "dn = hierarchy.dendrogram(Z) # σχεδιάζει ένα δενδρόγραμμα με το αποτέλεσμα του ιεραρχικού αλγορίθμου" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "uGx-U97PVJPq" }, "source": [ "Παρατηρούμε ότι οι προτάσεις που βρίσκονται κοντά μεταξύ τους καταλήγουν και σε κοινά cluster. Θα προσπαθήσουμε να εφαρμόσουμε την τεχνική αυτή και σε ένα πραγματικό πρόβλημα με αληθινά κείμενα.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "uHZHhc7iaZUi" }, "source": [ "\n", "### 20 Newsgroups dataset\n", "\n", "Ως πραγματικό παράδειγμα θα χρησιμοποιήσουμε το [20 Newsgroups](http://qwone.com/~jason/20Newsgroups/) dataset, το οποίο υπάρχει και μέσα στο [sklearn](http://scikit-learn.org/stable/datasets/twenty_newsgroups.html). Για το clustering θα χρησιμοποιήσουμε τον ιεραρχικό αλγόριθμο που μελετήσαμε προηγουμένως." ] }, { "cell_type": "code", "metadata": { "id": "01oYTGVYVJPr" }, "source": [ "from sklearn.datasets import fetch_20newsgroups\n", "newsgroups = fetch_20newsgroups(subset='all')" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "G_PUNTJkcU90" }, "source": [ "To 20 newsgroups είναι και αυτό ένα dataset για κατηγοριοποίηση ή ομαδοποίηση κειμένων. Τυπώνουμε τις κατηγορίες των κειμένων:" ] }, { "cell_type": "code", "metadata": { "id": "hGjtK1xabZ1d" }, "source": [ "print(newsgroups.target_names)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "pquHlH5tVJPw" }, "source": [ "Θα πάρουμε 3 κατηγορίες από το dataset αυτό και θα δημιουργήσουμε ένα corpus με τα πρώτα 5 κείμενα από κάθε κατηγορία. Για ευκολία θα επιλέξουμε 3 αρκετά ξεκάθαρες μεταξύ τους κατηγορίες." ] }, { "cell_type": "code", "metadata": { "id": "Tq6P3Xd3VJPy" }, "source": [ "import functools\n", "\n", "categ = ['alt.atheism', 'comp.graphics', 'rec.sport.baseball']\n", "data = functools.reduce(lambda x,y: x+y, [fetch_20newsgroups(categories=[x], remove=('headers', 'footers'))['data'][:5] for x in categ])\n", "print('Input shape:', len(data), '\\n')\n", "print(data[0][:1000])\n", "print(\"-----\")\n", "print(data[13][:1000])" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "UhkPMD3lVJP2" }, "source": [ "Σημειώστε εδώ ότι με το να πετάμε τους πολύ σπάνιους όρους ξεφορτωνόμαστε διάφορα σπάνια strings όπως emails, τυπογραφικά λάθη, \"παράξενα\" σύμβολα κλπ\n", "\n", "\n", "### TfidfVectorizer και μείωση της διαστατικότητας του VSM\n", "\n", "Για την προεπεξεργασία των αρχείων θα χρησιμοποιήσουμε τη συνάρτηση [TfidfVectorizer](http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html) του sklearn. Η συγκεκριμένη συνάρτηση έχει τη δυνατότητα να υποστηρίξει και όλη την [προεπεξεργασία](http://scikit-learn.org/stable/modules/feature_extraction.html#customizing-the-vectorizer-classes) που κάναμε προηγουμένως (stopwords, stemming, lematizing, κτλ). Επίσης δέχεται και πολλές επιπλέον παραμέτρους όπως την `max_df=x` η οποία αγνοεί τους όρους που εμφανίζονται σε ποσοστό `x` των κειμένων και πάνω (δηλ. λέξεις πολύ συχνές στο συγκεκριμένο σύνολο κειμένων), και την `min_df=y` η οποία αγνοεί τους όρους οι οποίοι εμφανίζονται σε ποσοστό μικρότερο από `y` του συνόλου των κειμένων (δηλ. πολύ σπάνιοι όροι). Για το παρακάτω παράδειγμα. Το " ] }, { "cell_type": "code", "metadata": { "id": "eGUujy-TVJP3" }, "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "\n", "print(\"Dimensions before optimizing TfidfVectorizer parameters\")\n", "vectorizer = TfidfVectorizer()\n", "tf_idf_array = vectorizer.fit_transform(data).toarray() # επιστρέφει sparse matrix, γι'αυτό το κάνουμε .toarray()\n", "print('TF-IDF array shape:', tf_idf_array.shape)\n", "\n", "print(\"Dimensions after optimizing TfidfVectorizer parameters\")\n", "vectorizer = TfidfVectorizer(max_df=0.5, min_df=2, stop_words='english')\n", "tf_idf_array = vectorizer.fit_transform(data).toarray() # επιστρέφει sparse matrix, γι'αυτό το κάνουμε .toarray()\n", "print('TF-IDF array shape:', tf_idf_array.shape)\n", "Z = hierarchy.linkage(tf_idf_array, 'ward')\n", "\n", "labels = ['a'] * 5 + ['g'] * 5 + ['b'] * 5 # 'a' = atheism, 'g' = graphics, 'b' = baseball \n", "plt.figure()\n", "dn = hierarchy.dendrogram(Z, labels=labels, color_threshold=0) # σχεδιάζει ένα δενδρόγραμμα με το αποτέλεσμα του ιεραρχικού αλγορίθμου\n", "\n", "colors = {'a':'r', 'g':'g', 'b':'b'}\n", "for l in plt.gca().get_xticklabels():\n", " l.set_color(colors[l.get_text()])\n", "print" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "hbstBcfgVJP6" }, "source": [ "Παρατηρούμε ότι όντως τοποθετεί με αρκετά καλή ακρίβεια τα κλαδιά που περιέχουν κείμενα από την ίδια κατηγορία. Επίσης ο αλγόριθμος αυτός μπορεί να εντοπίσει και ιεραρχίες εντός της κάθε ομάδας. (Σημ. σε ένα πραγματικό unsupervised πρόβλημα τα χρώματα και τα label στον άξονα x **δεν** είναι διαθέσιμα).\n" ] }, { "cell_type": "markdown", "metadata": { "id": "19cZxmnnhtu3" }, "source": [ "\n", "Σημειώστε επίσης την τεράστια επίδραση στις διαστάσεις των διανυσμάτων (το 1/10) που έχουν οι παράμετροι του TfidfVectorizer. Στην πράξη προσπαθούμε να μικρύνουμε όσο γίνεται τις διαστάσεις μέχρι το σημείο που αρχίζει να πέφτει η ποιότητα (της κατηγοριοποίησης, του clustering κοκ).\n" ] }, { "cell_type": "markdown", "metadata": { "id": "axdk56-nhvyn" }, "source": [ "\n", "### k-Means και πλήθος clusters\n", "\n", "Θα δοκιμάσουμε επίσης για το ίδιο πρόβλημα και τον **k-means**, σε περισσότερα κείμενα. Πρώτα φορτώνουμε τα κείμενα..." ] }, { "cell_type": "code", "metadata": { "id": "yiUI7OoLVJP6" }, "source": [ "categ = ['alt.atheism', 'comp.graphics', 'rec.sport.baseball']\n", "data = functools.reduce(lambda x,y: x+y, [fetch_20newsgroups(categories=[x], remove=('headers', 'footers'))['data'][:100] for x in categ])\n", "print('Σύνολο κειμένων:', len(data))" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "PVXBumV-VJP9" }, "source": [ "Στη συνέχεια εφαρμόζουμε την προεπεξεργασία και τρέχουμε τον k-means για διάφορα k για να βρούμε το βέλτιστο." ] }, { "cell_type": "code", "metadata": { "id": "fgJO85EcVJP-" }, "source": [ "from sklearn.cluster import KMeans\n", "from sklearn.metrics import silhouette_score\n", "tf_idf_array = vectorizer.fit_transform(data)\n", "\n", "silhouette_scores = []\n", "for k in range(2, 10):\n", " km = KMeans(k)\n", " preds = km.fit_predict(tf_idf_array)\n", " silhouette_scores.append(silhouette_score(tf_idf_array, preds))" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "b4FUiTrfVJQA" }, "source": [ "Σχεδιάζουμε τη γραφική του silhouette και βρίσκουμε το βέλτιστο k. Αυτό αντιπροσωπεύει τον αριθμό των κατηγοριών στις οποίες ανήκουν τα κείμενά μας." ] }, { "cell_type": "code", "metadata": { "id": "vrnA-CPuVJQB" }, "source": [ "plt.plot(range(2, 10), silhouette_scores)\n", "best_k = np.argmax(silhouette_scores) + 2 # +2 γιατί ξεκινάμε το range() από k=2 και όχι από 0 που ξεκινάει η αρίθμηση της λίστας\n", "plt.scatter(best_k, silhouette_scores[best_k-2], color='r') # για τον ίδιο λόγο το καλύτερο k είναι αυτό 2 θέσεις παρακάτω από το index της λίστας\n", "plt.xlim([2,9])\n", "plt.annotate(\"best k\", xy=(best_k, silhouette_scores[best_k-2]), xytext=(5, silhouette_scores[best_k-2]),arrowprops=dict(arrowstyle=\"->\")) # annotation\n", "print('Maximum average silhouette score for k =', best_k)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "5PmC1zVwVJQG" }, "source": [ "Με το κριτήριο silhouette βρήκαμε 3 cluster στα κείμενά μας, όσες κατηγορίες είχαμε και αρχικά.\n", "Ας εκτυπώσουμε τις ετικέτες που μας δίνει ο k-means:" ] }, { "cell_type": "code", "metadata": { "id": "00e7BytwVJQG" }, "source": [ "km = KMeans(best_k)\n", "km.fit(tf_idf_array)\n", "print(km.labels_)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "qlwgP3UfVJQN" }, "source": [ "Ξέρουμε ότι στο σύνολό μας, τα πρώτα 100 κείμενα ανήκουν στην 1η κατηγορία, τα επόμενα 100 στη δεύτερη, κτλ. Από τις παραπάνω προβλέψεις βλέπουμε ότι τα έχει πάει αρκετά καλά ο k-means. Σημειώστε ότι το label δεν έχει σημασία. \n", "Για να δούμε για ποιο πράγμα μιλάει η κάθε κατηγορία, μπορούμε να βρούμε τους top όρους για κάθε ομάδα." ] }, { "cell_type": "code", "metadata": { "id": "oDPxSPMmVJQO" }, "source": [ "terms = vectorizer.get_feature_names_out()\n", "order_centroids = km.cluster_centers_.argsort()[:, ::-1]\n", "for i in range(best_k):\n", " out = \"Cluster %d:\" % i\n", " for ind in order_centroids[i, :20]:\n", " out += ' %s' % terms[ind]\n", " print(out)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "Z-C8XiNYVJQR" }, "source": [ "Οι όροι βλέπουμε ότι έχουν όντως σχέση με το περιεχόμενο των κειμένων. Μπορούμε να τυπώσουμε και περισσότερα clusters και να διαπιστώσουμε ότι και αυτά έχουν σημασιολογική συνοχή." ] }, { "cell_type": "code", "metadata": { "id": "Wu2sqzOdVJQS" }, "source": [ "km = KMeans(10)\n", "km.fit(tf_idf_array)\n", "order_centroids = km.cluster_centers_.argsort()[:, ::-1]\n", "for i in range(10):\n", " out = \"Cluster %d:\" % i\n", " for ind in order_centroids[i, :20]:\n", " out += ' %s' % terms[ind]\n", " print(out)" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "fIMe-MMjjBXn" }, "source": [ "Σημειώστε ότι η παρουσία διάφορων όρων όπως edu, use, thanks που είτε υπάρχουν παντού είτε έχουν προφανώς μικρή σημασιολογική αξία, δείχνει ότι θα μπορούσαμε να κάνουμε ακόμα καλύτερη προεπεξεργασία και διανυσματική αναπαράσταση. " ] } ] }