{"id":19883404,"url":"https://github.com/steffo99/unimore-bda-6-steffo","last_synced_at":"2026-05-13T15:35:28.647Z","repository":{"id":68998775,"uuid":"595874456","full_name":"Steffo99/unimore-bda-6-steffo","owner":"Steffo99","description":"Sesta attività di Big Data Analytics","archived":false,"fork":false,"pushed_at":"2024-05-15T02:07:48.000Z","size":340,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-25T12:53:17.489Z","etag":null,"topics":["huggingface-tokenizers","nltk","python","sentiment-analysis","tensorflow","unimore-informatica"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc-by-sa-4.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Steffo99.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"ko_fi":"steffo"}},"created_at":"2023-02-01T01:27:05.000Z","updated_at":"2024-05-16T22:56:49.000Z","dependencies_parsed_at":"2024-05-15T18:02:00.939Z","dependency_job_id":null,"html_url":"https://github.com/Steffo99/unimore-bda-6-steffo","commit_stats":null,"previous_names":["steffo99/unimore-bda-6-steffo"],"tags_count":1,"template":false,"template_full_name":"Steffo99/template-poetry","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Steffo99%2Funimore-bda-6-steffo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Steffo99%2Funimore-bda-6-steffo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Steffo99%2Funimore-bda-6-steffo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Steffo99%2Funimore-bda-6-steffo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Steffo99","download_url":"https://codeload.github.com/Steffo99/unimore-bda-6-steffo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241311976,"owners_count":19942265,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["huggingface-tokenizers","nltk","python","sentiment-analysis","tensorflow","unimore-informatica"],"created_at":"2024-11-12T17:20:07.859Z","updated_at":"2026-05-13T15:35:28.618Z","avatar_url":"https://github.com/Steffo99.png","language":"Python","funding_links":["https://ko-fi.com/steffo"],"categories":[],"sub_categories":[],"readme":"[ Stefano Pigozzi | Traccia #3 | Tema Text Analytics | Big Data Analytics | A.A. 2022/2023 | Unimore ]\n\n# Confronto tra modelli di sentiment analysis per recensioni Amazon\n\n\u003e ### Sentiment analysis su recensioni Amazon\n\u003e\n\u003e Modificare l’esercizio di sentiment analysis sulle review Amazon svolto in classe e verificare l’efficacia del metodo effettuando queste varianti:\n\u003e\n\u003e 1. Utilizzare come tokenizer il “sentiment tokenizer” di Christopher Potts (link disponibile nelle slide del corso);\n\u003e 2. Modificare il dataset recuperando anche recensioni a 2, 3 e 4 stelle ed effettuare una classificazione a più classi (es. 5 classi di sentiment corrispondenti al numero di stelle delle recensioni).\n\u003e\n\u003e Effettuare quindi un confronto di efficacia tra queste varianti e la versione originale vista in classe.\n\u003e\n\u003e Valutare anche l’inclusione di altre feature estratte dai dati, con l’obiettivo di aumentare l’efficacia.\n\u003e\n\u003e * E’ necessario effettuare tutti i test su un numero significativo di run (es., almeno 50), scegliendo ogni volta in maniera casuale la composizione di test-set e training-set a partire dall’insieme di post estratti (è possibile utilizzare le feature automatiche di cross validation viste per scikit-learn)\n\u003e * E’ possibile (e gradito) estendere in ampiezza la propria analisi:\n\u003e    * utilizzare e confrontare una o più delle librerie di ML viste a lezione (NLTK/scikitlearn/XGBoost/Tensorflow) (NOTA: per le tracce 2 e 3 è necessario sperimentare anche almeno una libreria diversa da NLTK)\n\u003e    * utilizzare e confrontare diversi classificatori tra quelli offerti (es. quelli citati a lezione in scikit-learn) e una o più delle tecniche citate/viste a lezione (es. codifica del testo tramite TF-IDF, word embeddings per tensorflow, hyper-parameter tuning per scikit-learn, tecniche specifiche per sent. analysis, …)\n\u003e    * utilizzare librerie per l’elaborazione del testo alternative (es. SpaCy https://spacy.io/ ) per estrarre feature aggiuntive, valutandone l’effetto sul modello\n\u003e    * in generale: studiare, riassumere brevemente e applicare eventuali altre tecniche o strumenti ritenuti utili all’obiettivo (cioè, migliorare l’efficacia del modello proposto).\n\u003e\n\u003e Consegna: PDF commentato con discussione e codice Python (includere dati e codice anche in un file .txt per facilitarne il testing)\n\u003e\n\u003e Per quanto riguarda il codice Python, è possibile (e gradito) produrre e consegnare un notebook jupyter .ipynb\n\u003e (https://jupyter.org/) invece di codice .py e relativi commenti separati su PDF (per comodità di consultazione,\n\u003e consegnare comunque anche una stampa PDF del notebook oltre al notebook stesso).\n\n## Sinossi\n\nIn questo progetto si è realizzata una struttura che permettesse di mettere a confronto diversi modi per effettuare sentiment analysis, e poi si sono realizzati su di essa alcuni modelli di sentiment analysis con caratteristiche diverse, al fine di confrontarli.\n\n## Premessa\n\n### Codice e packaging\n\nIl codice dell'attività è incluso come package Python 3.10 compatibile con PEP518.\n\n\u003e **Note**\n\u003e\n\u003e In questo documento sono riportate parti del codice: in esse, è stato rimosso il codice superfluo come comandi di logging, docstring e commenti, in modo da accorciare la relazione e per mantenere l'attenzione sull'argomento della rispettiva sezione.\n\u003e\n\u003e Nel titolo di ciascuna sezione è evidenziato il file da cui gli spezzoni di codice provengono: se si necessitano sapere più dettagli sul funzionamento di esso, si consiglia di andare a vedere i file sorgente allegati, che contengono la documentazione necessaria.\n\n\u003e **Warning**\n\u003e\n\u003e Il progetto non supporta Python 3.11 per via del mancato supporto di Tensorflow a quest'ultimo.\n\n#### Installazione del package\n\nPer installare il package, è necessario eseguire i seguenti comandi dall'interno della directory del progetto:\n\n```console\n$ python3.10 -m venv .venv\n$ source venv/bin/activate\n$ pip install .\n```\n\n##### NLTK\n\nNLTK ha dipendenze aggiuntive che non possono essere scaricate tramite `pip`.\n\nEsse possono essere scaricate eseguendo su un terminale lo script fornito assieme al progetto:\n\n```console\n$ ./scripts/download-nltk.sh\n```\n\n#### Esecuzione del programma\n\nPer eseguire il programma principale, è possibile eseguire i seguenti comandi dall'interno della directory del progetto:\n\n```console\n$ source venv/bin/activate\n$ python3.10 -m unimore_bda_6\n```\n\n### Dati\n\nIl codice dell'attività richiede la connessione a un server MongoDB 6 contenente la collezione di recensioni Amazon fornita a lezione.\n\n\u003e **Warning**\n\u003e\n\u003e La collezione non è inclusa con il repository, in quanto occupa 21 GB!\n\nSi forniscono alcuni script nella cartella `./data/scripts` per facilitare la configurazione e l'esecuzione di quest'ultima.\n\n#### Esecuzione del database\n\nPer eseguire il database MongoDB come processo utente, salvando i dati nella cartella `./data/db`, è possibile eseguire il seguente comando:\n\n```console\n$ ./data/scripts/run-db.sh\n```\n\nSe [`jq`] è installato sul sistema, è possibile sfruttarlo per ottenere logs più human-friendly con il seguente comando:\n\n```console\n$ ./data/scripts/run-db.sh | jq '.msg'\n```\n\n#### Importazione dei dati da JSON\n\nPer importare il dataset `./data/raw/reviewsexport.json` fornito a lezione nel database MongoDB è disponibile il seguente script:\n\n```console\n$ ./data/scripts/import-db.sh\n```\n\n#### Creazione indici\n\nPer creare indici MongoDB potenzialmente utili al funzionamento efficiente del database è possibile eseguire il seguente comando:\n\n```console\n$ mongosh \u003c ./data/scripts/index-db.js\n```\n\n## Costruzione di una struttura per il confronto\n\nAl fine di effettuare i confronti richiesti dalla consegna dell'attività, si è deciso di realizzare un package Python che permettesse di confrontare vari modelli di Sentiment Analysis tra loro, con tokenizer, training set e evaluation set (spesso detto *test set*) diversi tra loro.\n\nIl package, chiamato `unimore_bda_6`, è composto da vari moduli, ciascuno descritto nelle seguenti sezioni.\n\n### Configurazione ambiente e iperparametri - `.config`\n\nIl primo modulo, `unimore_bda_6.config`, definisce le variabili configurabili del package usando la libreria [`cfig`], e, se eseguito, mostra all'utente un'interfaccia command-line che le descrive e ne mostra i valori attuali.\n\nViene prima creato un oggetto [`cfig.Configuration`], che opera come contenitore per le variabili configurabili:\n\n```python\nimport cfig\nconfig = cfig.Configuration()\n```\n\nIn seguito, viene definita una funzione per ogni variabile configurabile, che elabora il valore ottenuto dalle variabili di ambiente del contesto in cui il programma è eseguito, convertendolo in un formato più facilmente utilizzabile dal programma.\n\nSi fornisce un esempio di una di queste funzioni, che definisce la variabile per configurare la dimensione del training set:\n\n```python\n@config.optional()\ndef TRAINING_SET_SIZE(val: str | None) -\u003e int:\n    \"\"\"\n    The number of reviews from each category to fetch for the training dataset.\n    Defaults to `4000`.\n    \"\"\"\n    if val is None:\n        return 4000\n    try:\n        return int(val)\n    except ValueError:\n        raise cfig.InvalidValueError(\"Not an int.\")\n```\n\n(Nel gergo del machine learning / deep learning, queste variabili sono dette iperparametri, perchè configurano il modello, e non vengono alterate nell'addestramento del modello stesso.)\n\nInfine, si aggiunge una chiamata al metodo `cli()` della configurazione, eseguita solo se il modulo viene eseguito direttamente, che mostra all'utente l'interfaccia precedentemente menzionata:\n\n```python\nif __name__ == \"__main__\":\n    config.cli()\n```\n\nL'esecuzione del modulo `unimore_bda_6.config`, senza variabili d'ambiente definite, darà quindi il seguente output:\n\n```console\n$ python -m unimore_bda_6.config\n===== Configuration =====\n...\nTRAINING_SET_SIZE         = 4000\nThe number of reviews from each category to fetch for the training dataset.\nDefaults to `4000`.\n...\n===== End =====\n```\n\n### Recupero dati dal database - `.database`\n\nIl modulo `unimore_bda_6.database` si occupa della connessione al database [MongoDB], del recupero della collezione contenente il dataset di partenza, del recupero dei documenti nella corretta distribuzione, della conversione di essi in un formato più facilmente leggibile da Python, e della creazione di cache su disco per permettere alle librerie che lo supportano di non caricare l'intero dataset in memoria durante l'addestramento di un modello.\n\n#### Connessione al database - `.database.connection`\n\nIl modulo `unimore_bda_6.database.connection` si occupa della conessione (e disconnessione) al database utilizzando il package [`pymongo`].\n\nDefinisce un context manager che effettua automaticamente la disconnessione dal database una volta usciti dal suo scope:\n\n```python\n@contextlib.contextmanager\ndef mongo_client_from_config() -\u003e t.ContextManager[pymongo.MongoClient]:\n    client: pymongo.MongoClient = pymongo.MongoClient(\n        host=MONGO_HOST.__wrapped__,\n        port=MONGO_PORT.__wrapped__,\n    )\n    yield client\n    client.close()\n```\n\nEsso è utilizzabile nel seguente modo:\n\n```python\nwith mongo_client_from_config() as client:\n    ...\n```\n\n#### Recupero della collezione - `.database.collections`\n\nIl modulo `unimore_bda_6.database.collection` si occupa di recuperare la collezione `reviews` dal database MongoDB:\n\n```python\ndef reviews_collection(db: pymongo.MongoClient) -\u003e pymongo.collection.Collection[MongoReview]:\n    collection: pymongo.collection.Collection[MongoReview] = db.reviews.reviews\n    return collection\n```\n\n#### Contenitori di dati - `.database.datatypes`\n\nIl modulo `unimore_bda_6.database.datatypes` contiene contenitori ottimizzati (attraverso l'attributo magico [`__slots__`]) per i dati recuperati dal database, che possono essere riassunti con le seguenti classi circa equivalenti:\n\n```python\n@dataclasses.dataclass\nclass TextReview:\n    text: str\n    rating: float\n\n@dataclasses.dataclass\nclass TokenizedReview:\n    tokens: list[str]\n    rating: float\n```\n\n#### Query su MongoDB - `.database.queries`\n\nIl modulo `unimore_bda_6.database.queries` contiene alcune query pre-costruite utili per operare sulla collezione `reviews`.\n\n##### Working set\n\nEssendo il dataset completo composto da 23 milioni, 831 mila e 908 documenti (`23_831_908`), effettuare campionamenti su di esso in fase di sviluppo risulta eccessivamente lento e dispendioso, pertanto ad ogni query il dataset viene rimpicciolito ad un *working set* attraverso l'uso del seguente aggregation pipeline stage, dove `WORKING_SET_SIZE` è sostituito dal suo corrispondente valore nella configurazione (di default `1_000_000`):\n\n```javascript\n{\"$limit\": WORKING_SET_SIZE},\n```\n\n##### Dataset con solo recensioni 1* e 5* - `sample_reviews_polar`\n\nPer recuperare un dataset bilanciato di recensioni 1* e 5*, viene utilizzata la seguente funzione con relativa query MongoDB:\n\n```python\ndef sample_reviews_polar(collection: pymongo.collection.Collection, amount: int) -\u003e t.Iterator[TextReview]:\n    category_amount = amount // 2\n\n    cursor = collection.aggregate([\n        {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n        {\"$match\": {\"overall\": 1.0}},\n        {\"$sample\": {\"size\": category_amount}},\n        {\"$unionWith\": {\n            \"coll\": collection.name,\n            \"pipeline\": [\n                {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n                {\"$match\": {\"overall\": 5.0}},\n                {\"$sample\": {\"size\": category_amount}},\n            ],\n        }},\n        {\"$addFields\": {\n            \"sortKey\": {\"$rand\": {}},\n        }},\n        {\"$sort\": {\n            \"sortKey\": 1,\n        }}\n    ])\n\n    cursor = map(TextReview.from_mongoreview, cursor)\n\n    return cursor\n```\n\nL'aggregazione eseguita non è altro che l'unione dei risultati delle seguenti due aggregazioni, i cui risultati vengono poi mescolati attraverso l'ordinamento su un campo contenente il risultato dell'operatore [`$rand`]:\n\n```javascript\ndb.reviews.aggregate([\n    {\"$limit\": WORKING_SET_SIZE},\n    {\"$match\": {\"overall\": 1.0}},\n    {\"$sample\": {\"size\": amount / 2}},\n])\n// unita a\ndb.reviews.aggregate([\n    {\"$limit\": WORKING_SET_SIZE},\n    {\"$match\": {\"overall\": 5.0}},\n    {\"$sample\": {\"size\": amount / 2}},\n])\n// e poi mescolate\n```\n\n##### Dataset bilanciato con recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`\n\nLo stesso procedimento viene usato per ottenere un dataset bilanciato di recensioni con ogni numero possibile di stelle:\n\n```python\ndef sample_reviews_varied(collection: pymongo.collection.Collection, amount: int) -\u003e t.Iterator[TextReview]:\n    category_amount = amount // 5\n\n    cursor = collection.aggregate([\n        {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n        {\"$match\": {\"overall\": 1.0}},\n        {\"$sample\": {\"size\": category_amount}},\n        {\"$unionWith\": {\n            \"coll\": collection.name,\n            \"pipeline\": [\n                {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n                {\"$match\": {\"overall\": 2.0}},\n                {\"$sample\": {\"size\": category_amount}},\n                {\"$unionWith\": {\n                    \"coll\": collection.name,\n                    \"pipeline\": [\n                        {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n                        {\"$match\": {\"overall\": 3.0}},\n                        {\"$sample\": {\"size\": category_amount}},\n                        {\"$unionWith\": {\n                            \"coll\": collection.name,\n                            \"pipeline\": [\n                                {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n                                {\"$match\": {\"overall\": 4.0}},\n                                {\"$sample\": {\"size\": category_amount}},\n                                {\"$unionWith\": {\n                                    \"coll\": collection.name,\n                                    \"pipeline\": [\n                                        {\"$limit\": WORKING_SET_SIZE.__wrapped__},\n                                        {\"$match\": {\"overall\": 5.0}},\n                                        {\"$sample\": {\"size\": category_amount}},\n                                    ],\n                                }}\n                            ],\n                        }}\n                    ],\n                }}\n            ],\n        }},\n        {\"$addFields\": {\n            \"sortKey\": {\"$rand\": {}},\n        }},\n        {\"$sort\": {\n            \"sortKey\": 1,\n        }}\n    ])\n\n    cursor = map(TextReview.from_mongoreview, cursor)\n\n    return cursor\n```\n\n### Tokenizzatore astratto - `.tokenizer.base` e `.tokenizer.plain`\n\nSi è realizzata una classe astratta che rappresentasse un tokenizer qualunque, in modo da avere la stessa interfaccia a livello di codice indipendentemente dal package di tokenizzazione utilizzato:\n\n```python\nclass BaseTokenizer(metaclass=abc.ABCMeta):\n\n    @abc.abstractmethod\n    def tokenize(self, text: str) -\u003e t.Iterator[str]:\n        \"Convert a text `str` into another `str` containing a series of whitespace-separated tokens.\"\n        raise NotImplementedError()\n\n    def tokenize_review(self, review: TextReview) -\u003e TokenizedReview:\n        \"Apply `.tokenize` to the text of a `TextReview`, converting it in a `TokenizedReview`.\"\n        tokens = self.tokenize(review.text)\n        return TokenizedReview(rating=review.rating, tokens=tokens)\n```\n\nSi sono poi realizzate due classi triviali che ne implementano i metodi astratti, `PlainTokenizer` e `LowercaseTokenizer`, che separano il testo in tokens attraverso la funzione builtin [`str.split`] di Python, rispettivamente mantenendo e rimuovendo la capitalizzazione del testo.\n\n```python\nclass PlainTokenizer(BaseTokenizer):\n    def tokenize(self, text: str) -\u003e t.Iterator[str]:\n        tokens = text.split()\n        return tokens\n\nclass LowercaseTokenizer(BaseTokenizer):\n    def tokenize(self, text: str) -\u003e t.Iterator[str]:\n        text = text.lower()\n        tokens = text.split()\n        return tokens\n```\n\n### Analizzatore astratto - `.analysis.base`\n\nAllo stesso modo, si è realizzato una classe astratta per tutti i modelli di Sentiment Analysis:\n\n```python\nclass BaseSentimentAnalyzer(metaclass=abc.ABCMeta):\n    @abc.abstractmethod\n    def train(self, training_dataset_func: CachedDatasetFunc) -\u003e None:\n        \"Train the analyzer with the given training dataset.\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def use(self, text: str) -\u003e float:\n        \"Run the model on the given input, and return the predicted rating.\"\n        raise NotImplementedError()\n\n    def evaluate(self, evaluation_dataset_func: CachedDatasetFunc) -\u003e EvaluationResults:\n        \"\"\"\n        Perform a model evaluation by calling repeatedly `.use` on every text of the test dataset and by comparing its resulting category with the expected category.\n        \"\"\"\n        er = EvaluationResults()\n        for review in evaluation_dataset_func():\n            er.add(expected=review.rating, predicted=self.use(review.text))\n        return er\n```\n\nIl metodo `evaluate` inserisce i risultati di ciascuna predizione effettuata in un oggetto di tipo `EvaluationResults`.\n\nEsso tiene traccia della matrice di confusione per un'iterazione di valutazione, e da essa è in grado di ricavare i valori di richiamo e precisione per ciascuna categoria supportata dal modello; inoltre, calcola l'errore medio assoluto e quadrato tra previsioni e valori effettivi:\n\n```python\nclass EvaluationResults:\n    def __init__(self):\n        self.confusion_matrix: dict[float, dict[float, int]] = collections.defaultdict(lambda: collections.defaultdict(lambda: 0))\n        \"Confusion matrix of the evaluation. First key is the expected rating, second key is the output label.\"\n\n        self.absolute_error_total: float = 0.0\n        \"Sum of the absolute errors committed in the evaluation.\"\n\n        self.squared_error_total: float = 0.0\n        \"Sum of the squared errors committed in the evaluation.\"\n\n    def keys(self) -\u003e set[float]:\n        \"Return all processed categories.\"\n        keys: set[float] = set()\n        for expected, value in self.confusion_matrix.items():\n            keys.add(expected)\n            for predicted, _ in value.items():\n                keys.add(predicted)\n        return keys\n\n    def evaluated_count(self) -\u003e int:\n        \"Return the total number of evaluated reviews.\"\n        total: int = 0\n        for row in self.confusion_matrix.values():\n            for el in row.values():\n                total += el\n        return total\n\n    def perfect_count(self) -\u003e int:\n        \"\"\"\n        Return the total number of perfect reviews.\n        \"\"\"\n        total: int = 0\n        for key in self.keys():\n            total += self.confusion_matrix[key][key]\n        return total\n\n    def recall_count(self, rating: float) -\u003e int:\n        \"Return the number of reviews processed with the given rating.\"\n        total: int = 0\n        for el in self.confusion_matrix[rating].values():\n            total += el\n        return total\n\n    def precision_count(self, rating: float) -\u003e int:\n        \"Return the number of reviews for which the model returned the given rating.\"\n        total: int = 0\n        for col in self.confusion_matrix.values():\n            total += col[rating]\n        return total\n\n    def recall(self, rating: float) -\u003e float:\n        \"Return the recall for a given rating.\"\n        try:\n            return self.confusion_matrix[rating][rating] / self.recall_count(rating)\n        except ZeroDivisionError:\n            return float(\"inf\")\n\n    def precision(self, rating: float) -\u003e float:\n        \"Return the precision for a given rating.\"\n        try:\n            return self.confusion_matrix[rating][rating] / self.precision_count(rating)\n        except ZeroDivisionError:\n            return float(\"inf\")\n\n    def add(self, expected: float, predicted: float) -\u003e None:\n        \"Count a new prediction.\"\n        self.confusion_matrix[expected][predicted] += 1\n```\n\nSi è poi realizzata un'implementazione triviale della classe astratta, `ThreeCheat`, che identifica tutte le recensioni come aventi una valutazione di di 3.0*, in modo da verificare facilmente la correttezza della precedente classe:\n\n```python\nclass ThreeCheat(BaseSentimentAnalyzer):\n    def train(self, training_dataset_func: CachedDatasetFunc) -\u003e None:\n        pass\n\n    def use(self, text: str) -\u003e float:\n        return 3.0\n```\n\n### Logging - `.log`\n\nSi è configurato il modulo [`logging`] di Python affinchè esso scrivesse report sull'esecuzione:\n\n- nello stream stderr della console, in formato colorato e user-friendly\n- sul file `./data/logs/last_run.tsv`, in formato machine-readable\n\nIl livello di logging viene regolato attraverso la costante magica [`__debug__`] di Python, il cui valore cambia in base alla presenza dell'opzione di ottimizzazione [`-O`] dell'interprete Python; senza quest'ultima, i log stampati su console saranno molto più dettagliati.\n\n### Tester - `.__main__`\n\nInfine, si è preparato un tester che effettuasse ripetute valutazioni di efficacia per ogni combinazione di funzione di campionamento, tokenizzatore, e modello di Sentiment Analysis, con una struttura simile alla seguente:\n\n```python\n# Pseudo-codice non corrispondente al main finale\nif __name__ == \"__main__\":\n    for sample_func in [sample_reviews_polar, sample_reviews_varied]:\n        for SentimentAnalyzer in [ThreeCheat, ...]:\n            for Tokenizer in [PlainTokenizer, LowercaseTokenizer, ...]:\n                for run in range(TARGET_RUNS):\n                    model = SentimentAnalyzer(tokenizer=Tokenizer())\n                    model.train(training_set=sample_func(amount=TRAINING_SET_SIZE))\n                    model.evaluate(evaluation_set=sample_func(amount=EVALUATION_SET_SIZE))\n```\n\nLe valutazioni di efficacia vengono effettuate fino al raggiungimento di `TARGET_RUNS` addestramenti e valutazioni riuscite, o fino al raggiungimento di `MAXIMUM_RUNS` valutazioni totali (come descritto più avanti, l'addestramento di alcuni modelli potrebbe fallire e dover essere ripetuto).\n\nIl tester inoltre genera il file `./data/logs/results.tsv`, a cui viene aggiunta una riga per ciascuna valutazione effettuata che ne contiene un riepilogo dei risultati:\n\n- la funzione di campionamento e costruzione dataset utilizzata\n- il sentiment analyzer utilizzato\n- il tokenizer utilizzato\n- il numero di run richieste per raggiungere quei risultati\n- lo scarto assoluto medio\n- lo scarto quadratico medio\n- il numero di valutazioni corrette effettuate\n- i valori di recall per le recensioni di 1*, 2*, 3*, 4*, e 5*\n- i valori di precision per le recensioni di 1*, 2*, 3*, 4*, e 5*\n\n\n## Ri-implementazione dell'esercizio con NLTK\n\nCome prima cosa, si è ricreato l'esempio di sentiment analysis realizzato a lezione all'interno del package `unimore_bda_6`.\n\n### Wrapping del tokenizzatore di NLTK - `.tokenizer.nltk_word_tokenize`\n\nSi è creata una nuova sottoclasse di `BaseTokenizer`, `NLTKWordTokenizer`, che usa la tokenizzazione inclusa con NLTK.\n\nPer separare le parole in token, essa chiama [`nltk.word_tokenize`], funzione built-in che sfrutta i tokenizer [Punkt] e [Treebank] per dividere rispettivamente in frasi e parole la stringa passata come input.\n\nLa lista di tokens viene poi passata a [`nltk.sentiment.util.mark_negation`], che aggiunge il suffisso `_NEG` a tutti i token che si trovano tra una negazione e un segno di punteggiatura, in modo che la loro semantica venga preservata anche all'interno di un contesto *bag of words*, in cui le posizioni dei token vengono ignorate.\n\n(È considerato negazione qualsiasi token che finisce con `n't`, oppure uno dei seguenti token: `never`, `no`, `nothing`, `nowhere`, `noone`, `none`, `not`, `havent`, `hasnt`, `hadnt`, `cant`, `couldnt`, `shouldnt`, `wont`, `wouldnt`, `dont`, `doesnt`, `didnt`, `isnt`, `arent`, `aint`.)\n\n```python\nclass NLTKWordTokenizer(BaseTokenizer):\n    def tokenize(self, text: str) -\u003e t.Iterator[str]:\n        tokens = nltk.word_tokenize(text)\n        nltk.sentiment.util.mark_negation(tokens, shallow=True)\n        return tokens\n```\n\n### Costruzione del modello - `.analysis.nltk_sentiment`\n\nSi è creata anche una sottoclasse di `BaseSentimentAnalyzer`, `NLTKSentimentAnalyzer`, che utilizza per la classificazione un modello di tipo [`nltk.sentiment.SentimentAnalyzer`].\n\n```python\nclass NLTKSentimentAnalyzer(BaseSentimentAnalyzer):\n    def __init__(self, *, tokenizer: BaseTokenizer) -\u003e None:\n        super().__init__(tokenizer=tokenizer)\n        self.model: nltk.sentiment.SentimentAnalyzer = nltk.sentiment.SentimentAnalyzer()\n        self.trained: bool = False\n\n    ...\n```\n\nEsattamente come il modello realizzato a lezione, in fase di addestramento esso:\n\n1. Prende il testo di ogni recensione del training set\n2. Lo converte in una lista di token\n3. Conta le occorrenze totali di ogni token della precedente lista per selezionare quelli che compaiono in almeno 4 recensioni diverse\n4. Utilizza i token selezionati nel passo precedente per identificare le caratteristiche (\"features\") da usare per effettuare la classificazione\n\nSuccessivamente:\n\n5. Identifica la presenza delle caratteristiche in ciascun elemento del training set\n6. Addestra un classificatore Bayesiano semplice (\"naive Bayes\") perchè determini la probabilità che data una certa feature, una recensione abbia un certo numero di stelle\n\n```python\n    ...\n\n    def _add_feature_unigrams(self, dataset: t.Iterator[TokenizedReview]) -\u003e None:\n        \"Register the `nltk.sentiment.util.extract_unigram_feats` feature extrator on the model.\"\n        tokenbags = map(lambda r: r.tokens, dataset)\n        all_words = self.model.all_words(tokenbags, labeled=False)\n        unigrams = self.model.unigram_word_feats(words=all_words, min_freq=4)\n        self.model.add_feat_extractor(nltk.sentiment.util.extract_unigram_feats, unigrams=unigrams)\n\n    def _add_feature_extractors(self, dataset: t.Iterator[TextReview]):\n        \"Register new feature extractors on the `.model`.\"\n        dataset: t.Iterator[TokenizedReview] = map(self.tokenizer.tokenize_review, dataset)\n        self._add_feature_unigrams(dataset)\n\n    def __extract_features(self, review: TextReview) -\u003e tuple[Features, str]:\n        \"Convert a TextReview to a (Features, str) tuple. Does not use `SentimentAnalyzer.apply_features` due to unexpected behaviour when using iterators.\"\n        review: TokenizedReview = self.tokenizer.tokenize_review(review)\n        return self.model.extract_features(review.tokens), str(review.rating)\n\n    def train(self, training_dataset_func: CachedDatasetFunc) -\u003e None:\n        if self.trained:\n            raise AlreadyTrainedError()\n        self._add_feature_extractors(training_dataset_func())\n        featureset: t.Iterator[tuple[Features, str]] = map(self.__extract_features, training_dataset_func())\n        self.model.classifier = nltk.classify.NaiveBayesClassifier.train(featureset)\n        self.trained = True\n\n    ...\n```\n\nInfine, implementa la funzione `use`, che:\n\n1. tokenizza la stringa ottenuta in input\n2. utilizza il modello precedentemente addestrato per determinare la categoria (\"rating\") di appartenenza\n\n```python\n    ...\n\n    def use(self, text: str) -\u003e float:\n        if not self.trained:\n            raise NotTrainedError()\n        tokens = self.tokenizer.tokenize(text)\n        rating = self.model.classify(instance=tokens)\n        rating = float(rating)\n        return rating\n```\n\n### Ri-creazione del tokenizer di Christopher Potts - `.tokenizer.potts`\n\nPer realizzare il punto 1 della consegna, si sono creati due nuovi tokenizer, `PottsTokenizer` e `PottsTokenizerWithNegation`, che implementano il [tokenizer di Christopher Potts] rispettivamente senza marcare e marcando le negazioni sui token attraverso [`ntlk.sentiment.util.mark_negation`].\n\nEssendo il tokenizer originale scritto per Python 2, e non immediatamente compatibile con `BaseTokenizer`, si è scelto di studiare il codice originale e ricrearlo in un formato più adatto a questo progetto.\n\nPrima di effettuare la tokenizzazione, il tokenizer normalizza l'input:\n\n1. convertendo tutte le entità HTML come `\u0026lt;` nel loro corrispondente unicode `\u003c`\n2. convertendo il carattere `\u0026` nel token `and`\n\nIl tokenizer effettua poi la tokenizzazione usando espressioni regolari definite in `words_re_string` per catturare token di diversi tipi, in ordine:\n\n* emoticon testuali `:)`\n* numeri di telefono statunitensi `+1 123 456 7890`\n* tag HTML `\u003cb\u003e`\n* username stile Twitter `@steffo`\n* hashtag stile Twitter `#Big_Data_Analytics`\n* parole con apostrofi `i'm`\n* numeri `-9000`\n* parole senza apostrofi `data`\n* ellissi `. . .`\n* gruppi di caratteri non-whitespace `🇮🇹`\n\nDopo aver tokenizzato, il tokenizer processa il risultato convertendo il testo a lowercase, facendo attenzione però a non cambiare la capitalizzazione delle emoticon per non cambiare il loro significato (`:D` è diverso da `:d`).\n\nIl codice riassunto del tokenizer è dunque:\n\n```python\nclass PottsTokenizer(BaseTokenizer):\n    emoticon_re_string = r\"\"\"[\u003c\u003e]?[:;=8][\\-o*']?[)\\](\\[dDpP/:}{@|\\\\]\"\"\"\n    emoticon_re = re.compile(emoticon_re_string)\n\n    words_re_string = \"(\" + \"|\".join([\n        emoticon_re_string,\n        r\"\"\"(?:[+]?[01][\\s.-]*)?(?:[(]?\\d{3}[\\s.)-]*)?\\d{3}[\\-\\s.]*\\d{4}\"\"\",\n        r\"\"\"\u003c[^\u003e]+\u003e\"\"\",\n        r\"\"\"@[\\w_]+\"\"\",\n        r\"\"\"#+[\\w_]+[\\w'_-]*[\\w_]+\"\"\",\n        r\"\"\"[a-z][a-z'_-]+[a-z]\"\"\",\n        r\"\"\"[+-]?\\d+(?:[,/.:-]\\d+)?\"\"\",\n        r\"\"\"[\\w_]+\"\"\",\n        r\"\"\"[.](?:\\s*[.])+\"\"\",\n        r\"\"\"\\S+\"\"\",\n    ]) + \")\"\n\n    words_re = re.compile(words_re_string, re.I)\n\n    digit_re_string = r\"\u0026#\\d+;\"\n    digit_re = re.compile(digit_re_string)\n\n    alpha_re_string = r\"\u0026\\w+;\"\n    alpha_re = re.compile(alpha_re_string)\n\n    amp = \"\u0026amp;\"\n\n    @classmethod\n    def html_entities_to_chr(cls, s: str) -\u003e str:\n        \"Internal metod that seeks to replace all the HTML entities in s with their corresponding characters.\"\n        # First the digits:\n        ents = set(cls.digit_re.findall(s))\n        if len(ents) \u003e 0:\n            for ent in ents:\n                entnum = ent[2:-1]\n                try:\n                    entnum = int(entnum)\n                    s = s.replace(ent, chr(entnum))\n                except (ValueError, KeyError):\n                    pass\n        # Now the alpha versions:\n        ents = set(cls.alpha_re.findall(s))\n        ents = filter((lambda x: x != cls.amp), ents)\n        for ent in ents:\n            entname = ent[1:-1]\n            try:\n                s = s.replace(ent, chr(html.entities.name2codepoint[entname]))\n            except (ValueError, KeyError):\n                pass\n            s = s.replace(cls.amp, \" and \")\n        return s\n\n    @classmethod\n    def lower_but_preserve_emoticons(cls, word):\n        \"Internal method which lowercases the word if it does not match `.emoticon_re`.\"\n        if cls.emoticon_re.search(word):\n            return word\n        else:\n            return word.lower()\n\n    def tokenize(self, text: str) -\u003e t.Iterator[str]:\n        text = self.html_entities_to_chr(text)\n        tokens = self.words_re.findall(text)\n        tokens = map(self.lower_but_preserve_emoticons, tokens)\n        tokens = list(tokens)\n        return tokens\n```\n\n## Implementazione di modelli con Tensorflow+Keras - `.analysis.tf_text`\n\nVisti i problemi riscontrati con NLTK, si è deciso di realizzare nuovi modelli utilizzando stavolta [Tensorflow], il package per il deep learning sviluppato da Google, unito a [Keras], API di Tensorflow che permette la definizione di modelli di deep learning attraverso un linguaggio ad alto livello.\n\nTensorflow prende il nome dai *tensori*, le strutture matematiche su cui si basa, che consistono in una maggiore astrazione delle matrici o degli array, e che vengono implementate dalla libreria stessa nella classe [`tensorflow.Tensor`].\n\n### Aggiunta di un validation set\n\nLa documentazione di Tensorflow suggerisce, in fase di addestramento di modello, di includere un *validation set*, un piccolo dataset su cui misurare le metriche del modello ad ogni epoca di addestramento, in modo da poter verificare in tempo reale che non si stia verificando underfitting o overfitting.\n\nSi è quindi deciso di includerlo come parametro di `BaseSentimentAnalyzer.train`:\n\n```python\n    ...\n\n    @abc.abstractmethod\n    def train(self, training_dataset_func: CachedDatasetFunc, validation_dataset_func: CachedDatasetFunc) -\u003e None:\n        \"\"\"\n        Train the analyzer with the given training and validation datasets.\n        \"\"\"\n        raise NotImplementedError()\n\n    ...\n```\n\nSi è anche aggiornato il `.__main__` e la `.config` per supportare questa nuova funzionalità:\n\n```python\n# Pseudo-codice non corrispondente al main finale\nif __name__ == \"__main__\":\n    for sample_func in [sample_reviews_polar, sample_reviews_varied]:\n        for SentimentAnalyzer in [ThreeCheat, NLTKSentimentAnalyzer, ...]:\n            for Tokenizer in [PlainTokenizer, LowercaseTokenizer, PottsTokenizer, PottsTokenizerWithNegation, ...]:\n                for run in range(TARGET_RUNS):\n                    model = SentimentAnalyzer(tokenizer=Tokenizer())\n                    model.train(training_set=sample_func(amount=TRAINING_SET_SIZE), validation_set=sample_func(amount=VALIDATION_SET_SIZE))\n                    model.evaluate(evaluation_set=sample_func(amount=EVALUATION_SET_SIZE))\n```\n\n### Caching - `.database.cache` e `.gathering`\n\nPer essere efficienti, i modelli di Tensorflow richiedono che i dati vengano inseriti in un formato molto specifico: un'istanza della classe [`tensorflow.data.Dataset`].\n\nI dataset, per essere creati, richiedono però che gli venga dato in input un *generatore* (funzione che crea un iteratore quando chiamata), e non un *iteratore* (oggetto con un puntatore al successivo) come quello restituito dalle query di MongoDB, in quanto Tensorflow necessita di ricominciare l'iterazione da capo dopo ogni epoca di addestramento.\n\nUn modo semplice per ovviare al problema sarebbe stato raccogliere in una [`list`] l'iteratore creato da MongoDB, ma ciò caricherebbe l'intero dataset contemporaneamente in memoria, ricreando il problema riscontrato con NLTK.\n\nSi è allora adottata una soluzione alternativa: creare una cache su disco composta un file per ciascun documento recuperato da MongoDB, in modo che quando Tensorflow necessita di ritornare al primo documento, possa farlo ritornando semplicemente al primo file.\n\n```python\ndef store_cache(reviews: t.Iterator[TextReview], path: str | pathlib.Path) -\u003e None:\n    \"Store the contents of the given `Review` iterator to different files in a directory at the given path.\"\n    path = pathlib.Path(path)\n    path.mkdir(parents=True)\n    for index, document in enumerate(reviews):\n        document_path = path.joinpath(f\"{index}.pickle\")\n        with open(document_path, \"wb\") as file:\n            pickle.dump(document, file)\n\ndef load_cache(path: str | pathlib.Path) -\u003e CachedDatasetFunc:\n    \"Load the contents of a directory into a `Review` generator.\"\n    path = pathlib.Path(path)\n\n    def data_cache_loader():\n        document_paths = path.iterdir()\n        for document_path in document_paths:\n            document_path = pathlib.Path(document_path)\n            with open(document_path, \"rb\") as file:\n                result: TextReview = pickle.load(file)\n                yield result\n\n    return data_cache_loader\n\ndef delete_cache(path: str | pathlib.Path) -\u003e None:\n    \"Delete the given cache directory.\"\n    path = pathlib.Path(path)\n    shutil.rmtree(path)\n```\n\nSi è poi creata una classe `Caches` che si occupa di creare, gestire, ed eliminare le cache dei tre dataset nelle cartelle `./data/training`, `./data/validation` e `./data/evaluation`:\n\n```python\n@dataclasses.dataclass\nclass Caches:\n    \"\"\"\n    Container for the three generators that can create datasets.\n    \"\"\"\n\n    training: CachedDatasetFunc\n    validation: CachedDatasetFunc\n    evaluation: CachedDatasetFunc\n\n    @classmethod\n    @contextlib.contextmanager\n    def from_database_samples(cls, collection: pymongo.collection.Collection, sample_func: SampleFunc) -\u003e t.ContextManager[\"Caches\"]:\n        \"Create a new caches object from a database collection and a sampling function.\"\n\n        reviews_training = sample_func(collection, TRAINING_SET_SIZE.__wrapped__)\n        reviews_validation = sample_func(collection, VALIDATION_SET_SIZE.__wrapped__)\n        reviews_evaluation = sample_func(collection, EVALUATION_SET_SIZE.__wrapped__)\n\n        store_cache(reviews_training, \"./data/training\")\n        store_cache(reviews_validation, \"./data/validation\")\n        store_cache(reviews_evaluation, \"./data/evaluation\")\n\n        training_cache = load_cache(\"./data/training\")\n        validation_cache = load_cache(\"./data/validation\")\n        evaluation_cache = load_cache(\"./data/evaluation\")\n\n        yield Caches(training=training_cache, validation=validation_cache, evaluation=evaluation_cache)\n\n        delete_cache(\"./data/training\")\n        delete_cache(\"./data/validation\")\n        delete_cache(\"./data/evaluation\")\n\n    ...\n```\n\n### Creazione del modello base - `.analysis.tf_text.Tensorflow\n\nSi è determinata una struttura comune che potesse essere usata per tutti i tipi di Sentiment Analyzer realizzati con Tensorflow:\n\n```python\nclass TensorflowSentimentAnalyzer(BaseSentimentAnalyzer, metaclass=abc.ABCMeta):\n    ...\n```\n\n#### Formato del modello\n\nEssa richiede che le sottoclassi usino un modello `tensorflow.keras.Sequential`, ovvero con un solo layer di neuroni in input e un solo layer di neuroni in output:\n\n```python\n    ...\n\n    @abc.abstractmethod\n    def _build_model(self) -\u003e tensorflow.keras.Sequential:\n        \"Create the `tensorflow.keras.Sequential` model that should be executed by this sentiment analyzer.\"\n        raise NotImplementedError()\n\n    ...\n```\n\n#### Conversione da-a tensori\n\nDato che i modelli di Tensorflow richiedono che ciascun dato fornito in input o emesso in output sia un'istanza di `tensorflow.Tensor`, le sottoclassi devono anche definire metodi per convertire le stelle delle recensioni in un equivalente `tensorflow.Tensor` e viceversa:\n\n```python\n    ...\n\n    @abc.abstractmethod\n    def _rating_to_input(self, rating: float) -\u003e tensorflow.Tensor:\n        \"Convert a review rating to a `tensorflow.Tensor`.\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def _prediction_to_rating(self, prediction: tensorflow.Tensor) -\u003e float:\n        \"Convert the results of `tensorflow.keras.Sequential.predict` into a review rating.\"\n        raise NotImplementedError()\n\n    ...\n```\n\nAttraverso di essi, la classe è in grado di costruire il [`tensorflow.data.Dataset`] necessario al modello:\n\n```python\n    ...\n\n    @staticmethod\n    def _tokens_to_tensor(tokens: t.Iterator[str]) -\u003e tensorflow.Tensor:\n        \"Convert an iterator of tokens to a `tensorflow.Tensor`.\"\n        tensor = tensorflow.convert_to_tensor(\n            [list(tokens)],\n            dtype=tensorflow.string,\n            name=\"tokens\"\n        )\n        return tensor\n\n    def _build_dataset(self, dataset_func: CachedDatasetFunc) -\u003e tensorflow.data.Dataset:\n        \"Create a `tensorflow.data.Dataset` from the given `CachedDatasetFunc`.\"\n        def dataset_generator():\n            for review in dataset_func():\n                review: TextReview\n                review: TokenizedReview = self.tokenizer.tokenize_review(review)\n                tokens: tensorflow.Tensor = self._tokens_to_tensor(review.tokens)\n                rating: tensorflow.Tensor = self._rating_to_input(review.rating)\n                yield tokens, rating\n\n        dataset = tensorflow.data.Dataset.from_generator(\n            dataset_generator,\n            output_signature=(\n                tensorflow.TensorSpec(shape=(1, None,), dtype=tensorflow.string, name=\"tokens\"),\n                self._ratingtensor_shape(),\n            ),\n        )\n        dataset = dataset.cache()\n        dataset = dataset.prefetch(buffer_size=tensorflow.data.AUTOTUNE)\n        return dataset\n\n    ...\n```\n\n#### Lookup delle stringhe\n\nI modelli di deep learning di Tensorflow non sono in grado di processare stringhe direttamente; esse devono essere prima convertite in formato numerico.\n\nAll'inizializzazione, la struttura base crea un layer di tipo [`tensorflow.keras.layers.StringLookup`], che prende in input una lista di token e la converte in una lista di numeri interi, assegnando a ciascun token un numero diverso:\n\n```python\n    ...\n\n    def __init__(self, *, tokenizer: BaseTokenizer):\n        ...\n        self.string_lookup_layer = tensorflow.keras.layers.StringLookup(max_tokens=TENSORFLOW_MAX_FEATURES)\n        ...\n\n    ...\n```\n\nCiò comporta che, prima dell'addestramento del modello, il layer deve essere adattato, ovvero deve essere costruito un vocabolario che associa ogni possibile termine ad un numero; qualsiasi token al di fuori da questo vocabolario verrà convertito in `0`.\n\nPer esempio, `[\"ciao\", \"come\", \"stai\", \"?\"]` potrebbe essere convertito in `[1, 2, 0, 3]` se il modello non è stato adattato con il token `\"stai\"`.\n\n#### Addestramento\n\nLa struttura base `TensorflowSentimentAnalyzer` uniforma la fase di addestramento per tutti i modelli realizzandola attraverso le seguenti fasi:\n\n1. Creazione di `tensorflow.data.Dataset` dalla cache del training set e del validation set\n2. Adattamento del layer di string lookup\n3. Fitting del modello per `TENSORFLOW_EPOCHS` epoche\n\n```python\n    ...\n\n    def train(self, training_dataset_func: CachedDatasetFunc, validation_dataset_func: CachedDatasetFunc) -\u003e None:\n        training_set = self._build_dataset(training_dataset_func)\n        validation_set = self._build_dataset(validation_dataset_func)\n\n        vocabulary = training_set.map(lambda tokens, rating: tokens)\n        self.string_lookup_layer.adapt(vocabulary)\n\n        self.history: tensorflow.keras.callbacks.History | None  = self.model.fit(\n            training_set,\n            validation_data=validation_set,\n            epochs=TENSORFLOW_EPOCHS.__wrapped__,\n            callbacks=[\n                tensorflow.keras.callbacks.TerminateOnNaN()\n            ],\n        )\n\n        if len(self.history.epoch) \u003c TENSORFLOW_EPOCHS.__wrapped__:\n            self.failed = True\n            raise TrainingFailedError()\n        else:\n            self.trained = True\n\n    ...\n```\n\n##### Esplosione del gradiente\n\nIl metodo `train` si occupa anche di gestire una situazione particolare: quella in cui l'errore del modello sul training set diventi `NaN` per via del fenomeno di [esplosione del gradiente].\n\nGrazie al callback `tensorflow.keras.callbacks.TerminateOnNaN`, nel momento in cui viene riconosciuto che l'errore è diventato `NaN`, l'addestramento viene interrotto, e viene sollevato un `TrainingFailedError`.\n\nSi è quindi aggiornato il main per gestire l'eccezione e ricominciare l'addestramento da capo qualora essa si verificasse:\n\n```python\n# Pseudo-codice non corrispondente al main finale\nif __name__ == \"__main__\":\n    for sample_func in [sample_reviews_polar, sample_reviews_varied]:\n        for SentimentAnalyzer in [ThreeCheat, NLTKSentimentAnalyzer, ...]:\n            for Tokenizer in [PlainTokenizer, LowercaseTokenizer, PottsTokenizer, PottsTokenizerWithNegation, ...]:\n                runs = 0\n                successful_runs = 0\n                while True:\n                    if runs \u003e= MAXIMUM_RUNS or successful_runs \u003e= TARGET_RUNS:\n                        break\n                    runs += 1\n                    model = SentimentAnalyzer(tokenizer=Tokenizer())\n                    try:\n                        model.train(training_set=sample_func(amount=TRAINING_SET_SIZE), validation_set=sample_func(amount=VALIDATION_SET_SIZE))\n                    except TrainingFailedError:\n                        continue\n                    model.evaluate(evaluation_set=sample_func(amount=EVALUATION_SET_SIZE))\n                    successful_runs += 1\n```\n\n#### Utilizzo\n\nAnche l'utilizzo del modello è uniformato da `TensorflowSentimentAnalyzer`:\n\n```python\n    ...\n\n    def use(self, text: str) -\u003e float:\n        tokens = self.tokenizer.tokenize(text)\n        tokens = self._tokens_to_tensor(tokens)\n        prediction = self.model.predict(tokens, verbose=False)\n        prediction = self._prediction_to_rating(prediction)\n        return prediction\n\n    ...\n```\n\n1. Il testo passato in input viene tokenizzato dal tokenizzatore selezionato;\n2. i token vengono trasformati in un tensore di stringhe;\n3. il tensore di stringhe viene passato al modello, il primo layer del quale effettua string lookup;\n4. il modello emette un output in forma di tensore;\n5. il tensore viene convertito nel numero di stelle predetto.\n\n### Creazione di un modello di regressione - `.analysis.tf_text.TensorflowPolarSentimentAnalyzer`\n\nUno dei due tipi di modello di deep learning realizzati è un modello di regressione, ovvero un modello che dà in output un singolo valore a virgola mobile `0 \u003c y \u003c 1` rappresentante la confidenza che la recensione data sia positiva, il cui complementare `z = 1 - y` rappresenta la confidenza che la recensione data sia negativa:\n\n```python\nclass TensorflowPolarSentimentAnalyzer(TensorflowSentimentAnalyzer):\n    def _ratingtensor_shape(self) -\u003e tensorflow.TensorSpec:\n        spec = tensorflow.TensorSpec(shape=(1,), dtype=tensorflow.float32, name=\"rating_value\")\n        return spec\n\n    ...\n```\n\nSi considera valida la predizione in cui il modello ha più confidenza: positiva, o 5.0*, se `y \u003e= 0.5`, oppure negativa, o 1.0*, se `y \u003c 0.5`:\n\n```python\n    ...\n\n    def _prediction_to_rating(self, prediction: numpy.array) -\u003e float:\n        rating: float = prediction[0, 0]\n        rating = 1.0 if rating \u003c 0.5 else 5.0\n        return rating\n\n    ...\n```\n\nSeguendo le best practices per i modelli di questo tipo, si normalizza il valore in input a un numero `0.0 \u003c x \u003c 1.0`:\n\n\n```python\n    ...\n\n    def _rating_to_input(self, rating: float) -\u003e tensorflow.Tensor:\n        normalized_rating = (rating - 1) / 4\n        tensor = tensorflow.convert_to_tensor(\n            [normalized_rating],\n            dtype=tensorflow.float32,\n            name=\"rating_value\"\n        )\n        return tensor\n\n    ...\n```\n\nInfine, si costruiscono i layer del modello di deep learning:\n\n1. il primo layer, [`tensorflow.keras.layers.Embedding`], impara a convertire i tensori di interi di dimensione variabile che riceve in input in tensori di numeri a virgola mobile di dimensione fissa in cui ciascun valore rappresenta un significato delle parole;\n\n2. il secondo (e quarto e sesto) layer, [`tensorflow.keras.layers.Dropout`], imposta casualmente a `0.0` il 25% dei valori contenuti nei tensori che riceve in input, rendendo \"più indipendenti\" le correlazioni apprese dallo strato precedente di neuroni e così evitando l'overfitting;\n\n3. il terzo layer, [`tensorflow.keras.layers.GlobalAveragePooling1D`], calcola l'influenza media di ciascun significato sulla confidenza del modello relativamente a una determinata recensione\n\n4. il quinto (e sesto) layer, [`tensorflow.keras.layers.Dense`], sono strati di neuroni interconnessi in grado di apprendere semplici collegamenti tra significati e sentimenti\n\n```python\n    ...\n\n    def _build_model(self) -\u003e tensorflow.keras.Sequential:\n        model = tensorflow.keras.Sequential([\n            self.string_lookup_layer,\n            tensorflow.keras.layers.Embedding(\n                input_dim=TENSORFLOW_MAX_FEATURES.__wrapped__ + 1,\n                output_dim=TENSORFLOW_EMBEDDING_SIZE.__wrapped__,\n            ),\n            tensorflow.keras.layers.Dropout(0.25),\n            tensorflow.keras.layers.GlobalAveragePooling1D(),\n            tensorflow.keras.layers.Dropout(0.25),\n            tensorflow.keras.layers.Dense(8),\n            tensorflow.keras.layers.Dropout(0.25),\n            tensorflow.keras.layers.Dense(1, activation=tensorflow.keras.activations.sigmoid),\n        ])\n\n        model.compile(\n            optimizer=tensorflow.keras.optimizers.Adam(clipnorm=1.0),\n            loss=tensorflow.keras.losses.MeanAbsoluteError(),\n        )\n\n        log.debug(\"Compiled model: %s\", model)\n        return model\n```\n\n### Creazione di un modello di categorizzazione - `.analysis.tf_text.TensorflowCategorySentimentAnalyzer`\n\nL'altro tipo di modello realizzato è invece un modello di categorizzazione one-of, ovvero un modello che dà in output cinque diversi valori a virgola mobile, ciascuno rappresentante la confidenza che la data recensione appartenga a ciascuna delle date categorie:\n\n```python\nclass TensorflowCategorySentimentAnalyzer(TensorflowSentimentAnalyzer):\n    def _ratingtensor_shape(self) -\u003e tensorflow.TensorSpec:\n        spec = tensorflow.TensorSpec(shape=(1, 5), dtype=tensorflow.float32, name=\"rating_one_hot\")\n        return spec\n\n    ...\n```\n\nSi considera valida la predizione nella quale il modello ha confidenza più alta:\n\n```python\n    ...\n\n    def _prediction_to_rating(self, prediction: tensorflow.Tensor) -\u003e float:\n        best_prediction = None\n        best_prediction_index = None\n\n        for index, prediction in enumerate(iter(prediction[0])):\n            if best_prediction is None or prediction \u003e best_prediction:\n                best_prediction = prediction\n                best_prediction_index = index\n\n        result = float(best_prediction_index) + 1.0\n        return result\n\n    ...\n```\n\nQuesta volta, si utilizza l'encoding *one-hot* per gli input del modello in modo da creare una separazione netta tra le cinque possibili categorie in cui una recensione potrebbe cadere (1*, 2*, 3*, 4*, 5*).\n\nEsso consiste nel creare un tensore di cinque elementi, ciascuno rappresentante una categoria, e di impostarlo a 1.0 se la recensione appartiene a una categoria o a 0.0 se essa non vi appartiene.\n\n```python\n    ...\n\n    def _rating_to_input(self, rating: float) -\u003e tensorflow.Tensor:\n        tensor = tensorflow.convert_to_tensor(\n            [[\n                1.0 if rating == 1.0 else 0.0,\n                1.0 if rating == 2.0 else 0.0,\n                1.0 if rating == 3.0 else 0.0,\n                1.0 if rating == 4.0 else 0.0,\n                1.0 if rating == 5.0 else 0.0,\n            ]],\n            dtype=tensorflow.float32,\n            name=\"rating_one_hot\"\n        )\n        return tensor\n\n    ...\n```\n\nInfine, si costruisce un modello molto simile al precedente, ma con 5 neuroni in output, il cui valore viene normalizzato attraverso la funzione *softmax*:\n\n```python\n    ...\n\n    def _build_model(self) -\u003e tensorflow.keras.Sequential:\n        model = tensorflow.keras.Sequential([\n            self.string_lookup_layer,\n            tensorflow.keras.layers.Embedding(\n                input_dim=TENSORFLOW_MAX_FEATURES.__wrapped__ + 1,\n                output_dim=TENSORFLOW_EMBEDDING_SIZE.__wrapped__,\n            ),\n            tensorflow.keras.layers.Dropout(0.25),\n            tensorflow.keras.layers.GlobalAveragePooling1D(),\n            tensorflow.keras.layers.Dropout(0.25),\n            tensorflow.keras.layers.Dense(8),\n            tensorflow.keras.layers.Dropout(0.25),\n            tensorflow.keras.layers.Dense(5, activation=\"softmax\"),\n        ])\n\n        model.compile(\n            optimizer=tensorflow.keras.optimizers.Adam(clipnorm=1.0),\n            loss=tensorflow.keras.losses.CategoricalCrossentropy(),\n            metrics=[\n                tensorflow.keras.metrics.CategoricalAccuracy(),\n            ]\n        )\n\n        return model\n\n```\n\n## Implementazione di tokenizzatori di HuggingFace - `.tokenizer.hugging`\n\nCome ultima funzionalità, si implementa la possibilità di importare tokenizzatori presenti su [HuggingFace] con la classe astratta `HuggingTokenizer`:\n\n```python\nclass HuggingTokenizer(BaseTokenizer, metaclass=abc.ABCMeta):\n    def __init__(self):\n        super().__init__()\n        self.hug: tokenizers.Tokenizer = self._build_hugging_tokenizer()\n\n    @abc.abstractmethod\n    def _build_hugging_tokenizer(self) -\u003e tokenizers.Tokenizer:\n        raise NotImplementedError()\n\n    def tokenize(self, text: str) -\u003e t.Iterator[str]:\n        return self.hug.encode(text).tokens\n```\n\nUtilizzandola, si implementa il tokenizzatore [`bert-base-cased`] per testarne l'efficacia:\n\n```python\nclass HuggingBertTokenizer(HuggingTokenizer):\n    def _build_hugging_tokenizer(self):\n        return tokenizers.Tokenizer.from_pretrained(\"bert-base-cased\")\n```\n\n## Regolazione degli iperparametri\n\nIl tester è stato eseguito alcune volte con diverse configurazioni di parametri per verificarne il corretto funzionamento e determinare empiricamente gli iperparametri migliori da utilizzare durante la run vera e propria.\n\nSi riportano i parametri regolati assieme ai valori a cui essi sono stati impostati.\n\n### `TRAINING_SET_SIZE`\n\n\u003e Il numero di recensioni di ogni categoria da recuperare per formare il training set.\n\nL'approccio all'addestramento utilizzato da [`nltk.sentiment.SentimentAnalyzer`] si è rivelato problematico, in quanto non in grado di scalare per dimensioni molto grandi di training set: i suoi metodi non sembrano gestire correttamente gli iteratori, meccanismo attraverso il quale Python può realizzare lazy-loading di dati.\n\nInoltre, si è notato che il problema di [esplosione del gradiente](#esplosione-del-gradiente) si verifica tanto più di frequente quanto è grande il training set.\n\nPer questi due motivi si è deciso di limitare la dimensione del training set a `4_000` documenti per categoria.\n\n### `VALIDATION_SET_SIZE`\n\n\u003e Il numero di recensioni di ogni categoria da recuperare per formare il validation set.\n\nSi è scelto di creare un validation set della dimensione di un decimo del training set, ovvero di `400` documenti per categoria.\n\n### `EVALUATION_SET_SIZE`\n\n\u003e Il numero di recensioni di ogni categoria da recuperare per formare il test set.\n\nDurante la sperimentazione manuale, si è notato che i risultati della valutazione del test set giungevano a convergenza dopo l'elaborazione di circa `1_000` documenti, pertanto si è impostato l'iperparametro a quel numero.\n\n### `WORKING_SET_SIZE`\n\n\u003e Il numero di recensioni del database da considerare.\n\u003e \n\u003e Si suggerisce di impostarlo a un numero basso per evitare rallentamenti nell'esecuzione delle query.\n\nSi è determinato che `5_000_000` fosse un buon numero che permettesse di avere ottima casualità nel dataset senza comportare tempi di campionamento troppo lunghi.\n\n### `TENSORFLOW_EMBEDDING_SIZE`\n\n\u003e La dimensione del tensore degli embeddings da usare nei modelli Tensorflow.\n\nSi sono testati vari valori per questo iperparametro, e non sono state notate differenze significative nei risultati ottenuti; perciò, l'iperparametro è stato impostato a un valore di `12`, leggermente superiore a quello minimo di `8` suggerito dalla documentazione di Tensorflow.\n\n### `TENSORFLOW_MAX_FEATURES`\n\n\u003e Il numero massimo di features da usare nei modelli Tensorflow.\n\nCome per il parametro precedente, non si sono notate particolari differenze, quindi si è scelto di rimanere sul sicuro permettendo fino a `300_000` token diversi di essere appresi.\n\n### `TENSORFLOW_EPOCHS`\n\n\u003e Il numero di epoche per cui addestrare i modelli Tensorflow.\n\nSi è notato che qualsiasi addestramento successivo alla terza epoca risultava in un aumento nella loss dei modelli, probabilmente dovuta all'occorrenza di overfitting in essi.\n\nPer prevenire il fenomeno si è allora deciso di impostare il numero massimo di epoche a `3`.\n\n## Confronto dei modelli\n\nSi sono effettuate 5 esecuzioni del tester, totalizzando 245 run dei modelli.\n\nI risultati grezzi ottenuti sono disponibili all'interno dei file `./data/logs/results-success2.tsv`,  `./data/logs/results-success3.tsv`,  `./data/logs/results-success4.tsv`,  `./data/logs/results-success5.tsv`, e `./data/logs/results-success6.tsv`. \n\nSi riportano invece direttamente all'interno di questa relazione i risultati cumulativi ottenuti, ottenuti effettuando la media tra i risultati ottenuti nelle cinque esecuzioni.\n\n### Scarto\n\n#### Recensioni 1* e 5* - `sample_reviews_polar`\n\n| Analyzer | Tokenizer | Scarto assoluto medio | Scarto quadratico medio |\n|---|---|--:|--:|\n| `ThreeCheat` | `PlainTokenizer` | 2.000 | 4.000 |\n| `ThreeCheat` | `LowercaseTokenizer` | 2.000 | 4.000 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 2.000 | 4.000 |\n| `ThreeCheat` | `PottsTokenizer` | 2.000 | 4.000 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 2.000 | 4.000 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 2.000 | 4.000 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 1.015 | 4.061 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.981 | 3.923 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 1.086 | 4.346 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.985 | 3.939 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 1.107 | 4.429 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 1.118 | 4.474 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.573 | 2.291 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.522 | 2.086 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.534 | 2.134 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.502 | 2.010 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.465 | 1.859 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.526 | 2.106 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.503 | 2.013 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.514 | 2.058 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.506 | 2.026 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.490 | 1.958 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.513 | 2.051 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.530 | 2.118 |\n\n#### Recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`\n\n| Analyzer | Tokenizer | Scarto assoluto medio | Scarto quadratico medio |\n|---|---|--:|--:|\n| `ThreeCheat` | `PlainTokenizer` | 1.200 | 2.000 |\n| `ThreeCheat` | `LowercaseTokenizer` | 1.200 | 2.000 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 1.200 | 2.000 |\n| `ThreeCheat` | `PottsTokenizer` | 1.200 | 2.000 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 1.200 | 2.000 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 1.200 | 2.000 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 1.254 | 3.096 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 1.274 | 3.188 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 1.267 | 3.116 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 1.263 | 3.128 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 1.280 | 3.164 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 1.340 | 3.441 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 1.287 | 3.149 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 1.261 | 3.045 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 1.252 | 3.006 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 1.218 | 2.870 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 1.229 | 2.915 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 1.208 | 2.834 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.866 | 1.583 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.821 | 1.523 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.813 | 1.446 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.792 | 1.441 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.839 | 1.516 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.821 | 1.513 |\n\n### Accuracy\n\n#### Recensioni 1* e 5* - `sample_reviews_polar`\n\n| Analyzer | Tokenizer | Accuracy |\n|---|---|---:|\n| `ThreeCheat` | `PlainTokenizer` | 0.000 |\n| `ThreeCheat` | `LowercaseTokenizer` | 0.000 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 0.000 |\n| `ThreeCheat` | `PottsTokenizer` | 0.000 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 0.000 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 0.000 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.746 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.755 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.728 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.754 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.723 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.720 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.857 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.870 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.867 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.874 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.884 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.868 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.874 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.871 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.873 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.878 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.872 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.868 |\n\n#### Recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`\n\n| Analyzer | Tokenizer | Accuracy |\n|---|---|---:|\n| `ThreeCheat` | `PlainTokenizer` | 0.200 |\n| `ThreeCheat` | `LowercaseTokenizer` | 0.200 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 0.200 |\n| `ThreeCheat` | `PottsTokenizer` | 0.200 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 0.200 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 0.200 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.340 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.335 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.329 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.339 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.326 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.321 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.332 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.337 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.339 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.343 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.339 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.346 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.398 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.428 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.427 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.444 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.414 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.427 |\n\n### Recall\n\n#### Recensioni 1* e 5* - `sample_reviews_polar`\n\n| Analyzer | Tokenizer | Recall 1* | Recall 5* | Recall Avg |\n|---|---|---:|---:|---|\n| `ThreeCheat` | `PlainTokenizer` | 0.000 | 0.000 | 0.000 |\n| `ThreeCheat` | `LowercaseTokenizer` | 0.000 | 0.000 | 0.000 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 0.000 | 0.000 | 0.000 |\n| `ThreeCheat` | `PottsTokenizer` | 0.000 | 0.000 | 0.000 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 0.000 | 0.000 | 0.000 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 0.000 | 0.000 | 0.000 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.646 | 0.847 | 0.746 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.660 | 0.849 | 0.755 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.615 | 0.842 | 0.728 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.648 | 0.860 | 0.754 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.600 | 0.846 | 0.723 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.601 | 0.840 | 0.720 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.874 | 0.840 | 0.857 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.879 | 0.860 | 0.870 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.871 | 0.862 | 0.867 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.884 | 0.865 | 0.874 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.898 | 0.870 | 0.884 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.882 | 0.854 | 0.868 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.891 | 0.857 | 0.874 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.879 | 0.864 | 0.871 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.880 | 0.867 | 0.873 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.890 | 0.865 | 0.878 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.858 | 0.886 | 0.872 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.872 | 0.863 | 0.868 |\n\n#### Recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`\n\n| Analyzer | Tokenizer | Recall 1* | Recall 2* | Recall 3* | Recall 4* | Recall 5* | Recall Avg |\n|---|---|---:|---:|---:|---:|---:|--:|\n| `ThreeCheat` | `PlainTokenizer` | 0.000 | 0.000 | 1.000 | 0.000 | 0.000 | 0.200 |\n| `ThreeCheat` | `LowercaseTokenizer` | 0.000 | 0.000 | 1.000 | 0.000 | 0.000 | 0.200 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 0.000 | 0.000 | 1.000 | 0.000 | 0.000 | 0.200 |\n| `ThreeCheat` | `PottsTokenizer` | 0.000 | 0.000 | 1.000 | 0.000 | 0.000 | 0.200 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 0.000 | 0.000 | 1.000 | 0.000 | 0.000 | 0.200 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 0.000 | 0.000 | 1.000 | 0.000 | 0.000 | 0.200 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.384 | 0.302 | 0.182 | 0.121 | 0.710 | 0.340 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.376 | 0.239 | 0.200 | 0.146 | 0.713 | 0.335 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.386 | 0.245 | 0.170 | 0.131 | 0.713 | 0.329 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.406 | 0.251 | 0.196 | 0.127 | 0.714 | 0.339 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.361 | 0.203 | 0.207 | 0.111 | 0.749 | 0.326 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.304 | 0.271 | 0.176 | 0.131 | 0.725 | 0.321 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.840 | 0.000 | 0.000 | 0.000 | 0.820 | 0.332 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.858 | 0.000 | 0.000 | 0.000 | 0.828 | 0.337 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.816 | 0.000 | 0.000 | 0.000 | 0.881 | 0.339 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.870 | 0.000 | 0.000 | 0.000 | 0.845 | 0.343 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.839 | 0.000 | 0.000 | 0.000 | 0.858 | 0.339 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.858 | 0.000 | 0.000 | 0.000 | 0.870 | 0.346 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.431 | 0.366 | 0.402 | 0.347 | 0.443 | 0.398 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.522 | 0.332 | 0.385 | 0.447 | 0.456 | 0.428 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.531 | 0.307 | 0.403 | 0.400 | 0.493 | 0.427 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.514 | 0.405 | 0.408 | 0.384 | 0.511 | 0.444 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.520 | 0.326 | 0.381 | 0.344 | 0.497 | 0.414 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.488 | 0.311 | 0.379 | 0.414 | 0.543 | 0.427 |\n\n### Precision\n\n#### Recensioni 1* e 5* - `sample_reviews_polar`\n\n| Analyzer | Tokenizer | Precision 1* | Precision 5* | Precision Avg |\n|---|---|---:|---:|---|\n| `ThreeCheat` | `PlainTokenizer` | - | - | - |\n| `ThreeCheat` | `LowercaseTokenizer` | - | - | - |\n| `ThreeCheat` | `NLTKWordTokenizer` | - | - | - |\n| `ThreeCheat` | `PottsTokenizer` | - | - | - |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | - | - | - |\n| `ThreeCheat` | `HuggingBertTokenizer` | - | - | - |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.808 | 0.705 | 0.757 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.814 | 0.715 | 0.765 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.795 | 0.686 | 0.741 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.822 | 0.710 | 0.766 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.796 | 0.679 | 0.738 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.789 | 0.678 | 0.734 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.845 | 0.870 | 0.858 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.864 | 0.877 | 0.871 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.866 | 0.872 | 0.869 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.869 | 0.883 | 0.876 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.874 | 0.896 | 0.885 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.860 | 0.879 | 0.870 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.862 | 0.888 | 0.875 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.866 | 0.878 | 0.872 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.870 | 0.879 | 0.874 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.868 | 0.888 | 0.878 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.883 | 0.863 | 0.873 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.864 | 0.872 | 0.868 |\n\n#### Recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`\n\n| Analyzer | Tokenizer | Precision 1* | Precision 2* | Precision 3* | Precision 4* | Precision 5* | Precision Avg |\n|---|---|---:|---:|---:|---:|---:|---|\n| `ThreeCheat` | `PlainTokenizer` | - | - | 0.200 | - | - | 0.200 |\n| `ThreeCheat` | `LowercaseTokenizer` | - | - | 0.200 | - | - | 0.200 |\n| `ThreeCheat` | `NLTKWordTokenizer` | - | - | 0.200 | - | - | 0.200 |\n| `ThreeCheat` | `PottsTokenizer` | - | - | 0.200 | - | - | 0.200 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | - | - | 0.200 | - | - | 0.200 |\n| `ThreeCheat` | `HuggingBertTokenizer` | - | - | 0.200 | - | - | 0.200 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.487 | 0.341 | 0.294 | 0.352 | 0.305 | 0.356 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.465 | 0.306 | 0.325 | 0.393 | 0.298 | 0.357 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.430 | 0.305 | 0.306 | 0.347 | 0.305 | 0.338 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.507 | 0.313 | 0.302 | 0.369 | 0.298 | 0.358 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.463 | 0.294 | 0.293 | 0.333 | 0.305 | 0.338 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.490 | 0.318 | 0.328 | 0.357 | 0.279 | 0.354 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.335 | - | - | - | 0.335 | 0.335 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.344 | - | - | - | 0.341 | 0.343 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.359 | - | - | - | 0.323 | 0.341 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.338 | - | - | - | 0.352 | 0.345 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.345 | - | - | - | 0.342 | 0.344 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.334 | - | - | - | 0.360 | 0.347 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.549 | 0.326 | 0.301 | 0.360 | 0.574 | 0.422 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.555 | 0.395 | 0.348 | 0.347 | 0.569 | 0.443 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.566 | 0.367 | 0.341 | 0.358 | 0.568 | 0.440 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.567 | 0.393 | 0.350 | 0.383 | 0.583 | 0.455 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.537 | 0.356 | 0.362 | 0.323 | 0.539 | 0.424 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.549 | 0.350 | 0.331 | 0.374 | 0.580 | 0.437 |\n\n### F1-measure\n\nSi calcola la F1-measure sui valori medi di recall e precision ottenuti da ciascun modello.\n\n#### Recensioni 1* e 5* - `sample_reviews_polar`\n\n| Analyzer | Tokenizer | F1-measure |\n|---|---|---:|\n| `ThreeCheat` | `PlainTokenizer` | - |\n| `ThreeCheat` | `LowercaseTokenizer` | - |\n| `ThreeCheat` | `NLTKWordTokenizer` | - |\n| `ThreeCheat` | `PottsTokenizer` | - |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | - |\n| `ThreeCheat` | `HuggingBertTokenizer` | - |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.751 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.760 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.735 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.760 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.730 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.727 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.857 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.870 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.868 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.875 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.884 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.869 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.875 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.872 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.874 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.878 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.872 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.868 |\n\n#### Recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`\n\n| Analyzer | Tokenizer | F1-measure |\n|---|---|---:|\n| `ThreeCheat` | `PlainTokenizer` | 0.200 |\n| `ThreeCheat` | `LowercaseTokenizer` | 0.200 |\n| `ThreeCheat` | `NLTKWordTokenizer` | 0.200 |\n| `ThreeCheat` | `PottsTokenizer` | 0.200 |\n| `ThreeCheat` | `PottsTokenizerWithNegation` | 0.200 |\n| `ThreeCheat` | `HuggingBertTokenizer` | 0.200 |\n| `NLTKSentimentAnalyzer` | `PlainTokenizer` | 0.348 |\n| `NLTKSentimentAnalyzer` | `LowercaseTokenizer` | 0.346 |\n| `NLTKSentimentAnalyzer` | `NLTKWordTokenizer` | 0.334 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizer` | 0.348 |\n| `NLTKSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.332 |\n| `NLTKSentimentAnalyzer` | `HuggingBertTokenizer` | 0.337 |\n| `TensorflowPolarSentimentAnalyzer` | `PlainTokenizer` | 0.334 |\n| `TensorflowPolarSentimentAnalyzer` | `LowercaseTokenizer` | 0.340 |\n| `TensorflowPolarSentimentAnalyzer` | `NLTKWordTokenizer` | 0.340 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizer` | 0.344 |\n| `TensorflowPolarSentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.342 |\n| `TensorflowPolarSentimentAnalyzer` | `HuggingBertTokenizer` | 0.346 |\n| `TensorflowCategorySentimentAnalyzer` | `PlainTokenizer` | 0.410 |\n| `TensorflowCategorySentimentAnalyzer` | `LowercaseTokenizer` | 0.435 |\n| `TensorflowCategorySentimentAnalyzer` | `NLTKWordTokenizer` | 0.433 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizer` | 0.450 |\n| `TensorflowCategorySentimentAnalyzer` | `PottsTokenizerWithNegation` | 0.419 |\n| `TensorflowCategorySentimentAnalyzer` | `HuggingBertTokenizer` | 0.432 |\n\n### Commento\n\nTutte le misure effettuate, dallo scarto, all'accuracy, alla F1-measure, indicano come più efficaci i modelli Tensorflow, favorendo leggermente `TensorflowPolarSentimentAnalyzer` quando il dataset è `sample_reviews_polar`, e invece favorendo fortemente `TensorflowCategorySentimentAnalyzer` quando il dataset è `sample_reviews_varied`.\n\nMantenendo fisso invece l'analyzer, e confrontando tra loro i tokenizer, si nota che non c'è chiarezza su quali siano meglio degli altri; l'unico pattern individuabile è che il `PottsTokenizer` tende ad avere metriche leggermente migliori rispetto agli altri, ottenendo spesso poco più di un punto percentuale di differenza rispetto agli altri tokenizer.\n\nLa combinazione migliore pertanto si direbbe quella del `TensorflowCategorySentimentAnalyzer` con il `PottsTokenizer`.\n\n\n\u003c!--Collegamenti--\u003e\n\n[`jq`]: https://jqlang.github.io/jq/\n[`cfig`]: https://cfig.readthedocs.io\n[`cfig.Configuration`]: https://cfig.readthedocs.io/en/latest/reference.html#cfig.config.Configuration\n[MongoDB]: https://www.mongodb.com\n[`$sample`]: https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/\n[`$rand`]: https://www.mongodb.com/docs/v6.0/reference/operator/aggregation/rand/\n[`__debug__`]: https://docs.python.org/3/library/constants.html#debug__\n[`-O`]: https://docs.python.org/3/using/cmdline.html#cmdoption-O\n[`str.split`]: https://docs.python.org/3/library/stdtypes.html?highlight=str%20split#str.split\n[`nltk.tokenize.word_tokenize`]: https://www.nltk.org/api/nltk.tokenize.word_tokenize.html?highlight=word_tokenize#nltk.tokenize.word_tokenize\n[Punkt]: https://www.nltk.org/api/nltk.tokenize.PunktSentenceTokenizer.html#nltk.tokenize.PunktSentenceTokenizer\n[Treebank]: https://www.nltk.org/api/nltk.tokenize.TreebankWordTokenizer.html#nltk.tokenize.TreebankWordTokenizer\n[`nltk.sentiment.util.mark_negation`]: https://www.nltk.org/api/nltk.sentiment.util.html?highlight=nltk+sentiment+util+mark_negation#nltk.sentiment.util.mark_negation\n[`nltk.sentiment.SentimentAnalyzer`]: https://www.nltk.org/api/nltk.sentiment.sentiment_analyzer.html?highlight=nltk+sentiment+sentimentanalyzer#nltk.sentiment.sentiment_analyzer.SentimentAnalyzer\n[`list`]: https://docs.python.org/3/library/stdtypes.html?highlight=list#list\n[tokenizer di Christopher Potts]: http://sentiment.christopherpotts.net/tokenizing.html\n[Tensorflow]: https://www.tensorflow.org\n[`tensorflow.data.Dataset`]: https://www.tensorflow.org/api_docs/python/tf/data/Dataset\n[Keras]: https://www.tensorflow.org/api_docs/python/tf/keras\n[`tensorflow.Tensor`]: https://www.tensorflow.org/api_docs/python/tf/Tensor\n[`tensorflow.keras.layers.StringLookup`]: https://www.tensorflow.org/api_docs/python/tf/keras/layers/StringLookup\n[esplosione del gradiente]: https://towardsdatascience.com/the-vanishing-exploding-gradient-problem-in-deep-neural-networks-191358470c11\n[`tensorflow.keras.layers.Embedding`]: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding\n[`tensorflow.keras.layers.Dropout`]: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dropout\n[`tensorflow.keras.layers.Dense`]: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense\n[HuggingFace]: https://huggingface.co\n[`bert-base-cased`]: https://huggingface.co/bert-base-cased\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteffo99%2Funimore-bda-6-steffo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsteffo99%2Funimore-bda-6-steffo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteffo99%2Funimore-bda-6-steffo/lists"}