> 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).
> * 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)
> * E’ possibile (e gradito) estendere in ampiezza la propria analisi:
> * 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)
> * 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, …)
> * utilizzare librerie per l’elaborazione del testo alternative (es. SpaCy https://spacy.io/ ) per estrarre feature aggiuntive, valutandone l’effetto sul modello
> * in generale: studiare, riassumere brevemente e applicare eventuali altre tecniche o strumenti ritenuti utili all’obiettivo (cioè, migliorare l’efficacia del modello proposto).
>
> Consegna: PDF commentato con discussione e codice Python (includere dati e codice anche in un file .txt per facilitarne il testing)
In questo progetto si è realizzato 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 per confrontarli.
> 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 mantenere l'attenzione sull'argomento della rispettiva sezione.
L'accelerazione hardware di Tensorflow richiede che una scheda grafica NVIDIA con supporto a CUDA sia disponibile sul dispositivo, e che gli strumenti di sviluppo di CUDA siano installati sul sistema operativo.
Per indicare a Tensorflow il percorso degli strumenti di sviluppo di CUDA, è necessario impostare la seguente variabile d'ambiente, sostituendo a `/opt/cuda` il percorso in cui gli strumenti sono installati sul dispositivo:
Per più informazioni, si suggerisce di consultare la pagina [Install Tensorflow 2](https://www.tensorflow.org/install) della documentazione di Tensorflow.
Al 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 test set diversi tra loro.
Il primo modulo, `unimore_bda_6.config`, definisce le variabili configurabili del package usando [`cfig`], e, se eseguito, mostra all'utente un'interfaccia command-line che le descrive e ne mostra i valori attuali.
Viene prima creato un oggetto [`cfig.Configuration`], che opera come contenitore per le variabili configurabili:
```python
import cfig
config = cfig.Configuration()
```
In seguito, per ogni variabile configurabile viene definita una funzione, 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.
Si fornisce un esempio di una di queste funzioni, che definisce la variabile per configurare la dimensione del training set:
```python
@config.optional()
def TRAINING_SET_SIZE(val: str | None) -> int:
"""
The number of reviews from each category to fetch for the training dataset.
Defaults to `4000`.
"""
if val is None:
return 4000
try:
return int(val)
except ValueError:
raise cfig.InvalidValueError("Not an int.")
```
> In gergo del machine learning / deep learning, queste variabili sono dette iperparametri, perchè configurano la creazione del modello, e non vengono configurati dall'addestramento del modello stesso!
Infine, si aggiunge una chiamata al metodo `cli()` della configurazione, eseguita solo se il modulo viene eseguito come main, che mostra all'utente l'interfaccia precedentemente menzionata:
```python
if __name__ == "__main__":
config.cli()
```
L'esecuzione del modulo `unimore_bda_6.config`, senza variabili d'ambiente definite, dà quindi il seguente output:
```console
$ python -m unimore_bda_6.config
===== Configuration =====
MONGO_HOST = '127.0.0.1'
The hostname of the MongoDB database to connect to.
Defaults to `"127.0.0.1"`.
MONGO_PORT = 27017
The port of the MongoDB database to connect to.
Defaults to `27017`.
WORKING_SET_SIZE = 1000000
The number of reviews to consider from the database.
Set this to a low number to prevent slowness due to the dataset's huge size.
TRAINING_SET_SIZE = 4000
The number of reviews from each category to fetch for the training dataset.
Defaults to `4000`.
VALIDATION_SET_SIZE = 400
The number of reviews from each category to fetch for the training dataset.
Defaults to `400`.
EVALUATION_SET_SIZE = 1000
The number of reviews from each category to fetch for the evaluation dataset.
Defaults to `1000`.
TENSORFLOW_MAX_FEATURES = 300000
The maximum number of features to use in Tensorflow models.
Defaults to `300000`.
TENSORFLOW_EMBEDDING_SIZE = 12
The size of the embeddings tensor to use in Tensorflow models.
Defaults to `12`.
TENSORFLOW_EPOCHS = 3
The number of epochs to train Tensorflow models for.
Il modulo `unimore_bda_6.database` si occupa della connessione al database [MongoDB] e la collezione contenente il dataset di partenza, del recupero dei documenti in modo bilanciato, 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.
#### Connessione al database - `.database.connection`
Il modulo `unimore_bda_6.database.connection` si occupa della conessione (e disconnessione) al database utilizzando il package [`pymongo`].
Definisce un context manager che effettua automaticamente la disconnessione dal database una volta usciti dal suo scope:
Il 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:
```python
@dataclasses.dataclass
class TextReview:
text: str
rating: float
@dataclasses.dataclass
class TokenizedReview:
tokens: list[str]
rating: float
```
#### Query su MongoDB - `.database.queries`
Il modulo `unimore_bda_6.database.queries` contiene alcune query pre-costruite utili per operare sulla collezione `reviews`.
##### Working set
Essendo il dataset completo composto da 23 milioni, 831 mila e 908 documenti (23_831_908), effettuare campionamenti su di esso in fase di sviluppo risulterebbe eccessivamente lento e dispendioso, pertanto in ogni query il dataset viene rimpicciolito a 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):
```javascript
{"$limit": WORKING_SET_SIZE},
```
##### Dataset con solo recensioni 1* e 5* - `sample_reviews_polar`
Per recuperare un dataset bilanciato di recensioni 1* e 5*, viene utilizzata la seguente funzione:
L'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`]:
```javascript
db.reviews.aggregate([
{"$limit": WORKING_SET_SIZE},
{"$match": {"overall": 1.0}},
{"$sample": {"size": amount / 2}},
])
// unita a
db.reviews.aggregate([
{"$limit": WORKING_SET_SIZE},
{"$match": {"overall": 5.0}},
{"$sample": {"size": amount / 2}},
])
// e poi mescolate
```
##### Dataset bilanciato con recensioni 1*, 2*, 3*, 4* e 5*
Lo stesso procedimento viene usato per ottenere un dataset bilanciato di recensioni con ogni numero possibile di stelle:
Si è realizzata una classe astratta che rappresentasse un tokenizer qualcunque, in modo da avere la stessa interfaccia a livello di codice indipendentemente dal package di tokenizzazione utilizzato:
```python
class BaseTokenizer(metaclass=abc.ABCMeta):
@abc.abstractmethod
def tokenize(self, text: str) -> t.Iterator[str]:
"Convert a text `str` into another `str` containing a series of whitespace-separated tokens."
"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."
Si è configurato il modulo [`logging`] di Python affinchè esso scrivesse sia su console sia sul file `./data/logs/last_run.tsv` le operazioni eseguite dal programma.
Il 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.
Infine, si è preparato un tester che effettuasse una valutazione di efficacia per ogni combinazione di funzione di campionamento, tokenizzatore, e modello di Sentiment Analysis: