1
Fork 0
mirror of https://github.com/Steffo99/unimore-bda-6.git synced 2024-11-21 15:34:18 +00:00

Proofreading

This commit is contained in:
Steffo 2023-06-02 02:27:17 +02:00
parent 33f332ce09
commit 36deada894
Signed by: steffo
GPG key ID: 2A24051445686895

View file

@ -58,7 +58,9 @@ $ pip install .
##### NLTK
NLTK richiede dipendenze aggiuntive per funzionare, che possono essere scaricate eseguendo il seguente comando su console:
NLTK ha dipendenze aggiuntive che non possono essere scaricate tramite `pip`.
Esse possono essere scaricate eseguendo su un terminale lo script fornito assieme al progetto:
```console
$ ./scripts/download-nltk.sh
@ -85,15 +87,21 @@ Si forniscono alcuni script nella cartella `./data/scripts` per facilitare la co
#### Esecuzione del database
Per eseguire il database MongoDB come processo utente, salvando i dati nella cartella `./data/db`:
Per eseguire il database MongoDB come processo utente, salvando i dati nella cartella `./data/db`, è possibile eseguire il seguente comando:
```console
$ ./data/scripts/run-db.sh
```
Se [`jq`] è installato sul sistema, è possibile sfruttarlo per ottenere logs più human-friendly con il seguente comando:
```console
$ ./data/scripts/run-db.sh | jq '.msg'
```
#### Importazione dei dati da JSON
Per importare il dataset `./data/raw/reviewsexport.json` fornito a lezione nel database MongoDB:
Per importare il dataset `./data/raw/reviewsexport.json` fornito a lezione nel database MongoDB è disponibile il seguente script:
```console
$ ./data/scripts/import-db.sh
@ -101,7 +109,7 @@ $ ./data/scripts/import-db.sh
#### Creazione indici
Per creare indici MongoDB potenzialmente utili al funzionamento efficiente del codice:
Per creare indici MongoDB potenzialmente utili al funzionamento efficiente del database è possibile eseguire il seguente comando:
```console
$ mongosh < ./data/scripts/index-db.js
@ -115,7 +123,7 @@ Il package, chiamato `unimore_bda_6`, è composto da vari moduli, ciascuno descr
### Configurazione ambiente e iperparametri - `.config`
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.
Il 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.
Viene prima creato un oggetto [`cfig.Configuration`], che opera come contenitore per le variabili configurabili:
@ -124,7 +132,7 @@ 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.
In 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.
Si fornisce un esempio di una di queste funzioni, che definisce la variabile per configurare la dimensione del training set:
@ -143,16 +151,16 @@ def TRAINING_SET_SIZE(val: str | None) -> int:
raise cfig.InvalidValueError("Not an int.")
```
(Nel gergo del machine learning / deep learning, queste variabili sono dette iperparametri, perchè configurano la creazione del modello, e non vengono configurate dall'addestramento del modello stesso.)
(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.)
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:
Infine, 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:
```python
if __name__ == "__main__":
config.cli()
```
L'esecuzione del modulo `unimore_bda_6.config`, senza variabili d'ambiente definite, dà quindi il seguente output:
L'esecuzione del modulo `unimore_bda_6.config`, senza variabili d'ambiente definite, darà quindi il seguente output:
```console
$ python -m unimore_bda_6.config
@ -167,7 +175,7 @@ Defaults to `4000`.
### Recupero dati dal database - `.database`
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.
Il 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.
#### Connessione al database - `.database.connection`
@ -186,10 +194,10 @@ def mongo_client_from_config() -> t.ContextManager[pymongo.MongoClient]:
client.close()
```
Esso sarà poi utilizzato in questo modo:
Esso è utilizzabile nel seguente modo:
```python
with mongo_client_from_config as client:
with mongo_client_from_config() as client:
...
```
@ -205,7 +213,7 @@ def reviews_collection(db: pymongo.MongoClient) -> pymongo.collection.Collection
#### Contenitori di dati - `.database.datatypes`
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:
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 le seguenti classi circa equivalenti:
```python
@dataclasses.dataclass
@ -225,7 +233,7 @@ Il modulo `unimore_bda_6.database.queries` contiene alcune query pre-costruite u
##### 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):
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 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`):
```javascript
{"$limit": WORKING_SET_SIZE},
@ -233,7 +241,7 @@ Essendo il dataset completo composto da 23 milioni, 831 mila e 908 documenti (23
##### 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:
Per recuperare un dataset bilanciato di recensioni 1* e 5*, viene utilizzata la seguente funzione con relativa query MongoDB:
```python
def sample_reviews_polar(collection: pymongo.collection.Collection, amount: int) -> t.Iterator[TextReview]:
@ -281,7 +289,7 @@ db.reviews.aggregate([
// e poi mescolate
```
##### Dataset bilanciato con recensioni 1*, 2*, 3*, 4* e 5*
##### Dataset bilanciato con recensioni 1*, 2*, 3*, 4* e 5* - `sample_reviews_varied`
Lo stesso procedimento viene usato per ottenere un dataset bilanciato di recensioni con ogni numero possibile di stelle:
@ -340,7 +348,7 @@ def sample_reviews_varied(collection: pymongo.collection.Collection, amount: int
### Tokenizzatore astratto - `.tokenizer.base` e `.tokenizer.plain`
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:
Si è 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:
```python
class BaseTokenizer(metaclass=abc.ABCMeta):
@ -356,7 +364,7 @@ class BaseTokenizer(metaclass=abc.ABCMeta):
return TokenizedReview(rating=review.rating, tokens=tokens)
```
Si sono poi realizzate due classi di esempio che implementassero i metodi astratti della precedente: `PlainTokenizer` e `LowerTokenizer`, che semplicemente separano il testo in tokens attraverso la funzione builtin [`str.split`] di Python, rispettivamente mantenendo e rimuovendo la capitalizzazione del testo.
Si 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.
```python
class PlainTokenizer(BaseTokenizer):
@ -397,9 +405,9 @@ class BaseSentimentAnalyzer(metaclass=abc.ABCMeta):
return er
```
Si può notare che il metodo `evaluate` inserisce i risultati di ciascuna predizione effettuata in un oggetto di tipo `EvaluationResults`.
Il metodo `evaluate` inserisce i risultati di ciascuna predizione effettuata in un oggetto di tipo `EvaluationResults`.
Esso tiene traccia della matrice di confusione per la valutazione, e da essa è in grado di ricavarne i valori di richiamo e precisione per ciascuna categoria implementata dal modello; inoltre, calcola l'errore medio assoluto e quadrato tra previsioni e valori effettivi:
Esso 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:
```python
class EvaluationResults:
@ -472,7 +480,7 @@ class EvaluationResults:
self.confusion_matrix[expected][predicted] += 1
```
Si è inoltre realizzata un'implementazione di esempio della classe astratta, `ThreeCheat`, che "prevede" che tutte le recensioni siano di 3.0*, in modo da verificare facilmente la correttezza della precedente classe:
Si è 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:
```python
class ThreeCheat(BaseSentimentAnalyzer):
@ -510,6 +518,19 @@ if __name__ == "__main__":
Le 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).
Il 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:
- la funzione di campionamento e costruzione dataset utilizzata
- il sentiment analyzer utilizzato
- il tokenizer utilizzato
- il numero di run richieste per raggiungere quei risultati
- lo scarto assoluto medio
- lo scarto quadratico medio
- il numero di valutazioni corrette effettuate
- i valori di recall per le recensioni di 1*, 2*, 3*, 4*, e 5*
- i valori di precision per le recensioni di 1*, 2*, 3*, 4*, e 5*
## Ri-implementazione dell'esercizio con NLTK
Come prima cosa, si è ricreato l'esempio di sentiment analysis realizzato a lezione all'interno del package `unimore_bda_6`.
@ -550,8 +571,8 @@ Esattamente come il modello realizzato a lezione, in fase di addestramento esso:
1. Prende il testo di ogni recensione del training set
2. Lo converte in una lista di token
3. Conta le occorrenze totali di ogni token della precedente lista per determinare quelli che compaiono in almeno 4 recensioni diverse
4. Utilizza questi token frequenti per identificare le caratteristiche ("features") da usare per effettuare la classificazione
3. Conta le occorrenze totali di ogni token della precedente lista per selezionare quelli che compaiono in almeno 4 recensioni diverse
4. Utilizza i token selezionati nel passo precedente per identificare le caratteristiche ("features") da usare per effettuare la classificazione
Successivamente:
@ -608,7 +629,7 @@ Infine, implementa la funzione `use`, che:
#### Problemi di RAM
L'approccio utilizzato da [`nltk.sentiment.SentimentAnalyzer`] si è rivelato problematico, in quanto non in grado di scalare a dimensioni molto grandi di training set: i suoi metodi non gestiscono correttamente gli iteratori, meccanismo attraverso il quale Python può realizzare lazy-loading di dati, e richiedono invece che l'intero training set sia caricato contemporaneamente in memoria in una [`list`].
L'approccio 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 gestiscono correttamente gli iteratori, meccanismo attraverso il quale Python può realizzare lazy-loading di dati, e richiedono invece che l'intero training set sia caricato contemporaneamente in memoria in una [`list`].
Per permetterne l'esecuzione su computer con 16 GB di RAM, si è deciso di impostare la dimensione predefinita del training set a `4000` documenti; dimensioni maggiori richiederebbero una riscrittura completa dei metodi di NLTK, e ciò andrebbe fuori dallo scopo di questa attività.
@ -616,7 +637,7 @@ Per permetterne l'esecuzione su computer con 16 GB di RAM, si è deciso di impos
Per 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`].
Essendo il tokenizer originale scritto per Python 2, e non direttamente immediatamente compatibile con `BaseTokenizer`, si è scelto di studiare il codice originale e ricrearlo in un formato più adatto a questo progetto.
Essendo 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.
Prima di effettuare la tokenizzazione, il tokenizer normalizza l'input:
@ -912,7 +933,7 @@ Attraverso di essi, la classe è in grado di costruire il [`tensorflow.data.Data
#### Lookup delle stringhe
I modelli di deep learning di Tensorflow non sono in grado di processare direttamente stringhe; esse devono essere prima convertite in formato numerico.
I modelli di deep learning di Tensorflow non sono in grado di processare stringhe direttamente; esse devono essere prima convertite in formato numerico.
All'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:
@ -927,7 +948,7 @@ All'inizializzazione, la struttura base crea un layer di tipo [`tensorflow.keras
...
```
Prima dell'addestramento del modello, il layer deve essere adattato, ovvero deve costruire un vocabolario che associa ogni possibile termine ad un numero; qualsiasi token al di fuori da questo vocabolario verrà convertito in `0`.
Ciò 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`.
Per esempio, `["ciao", "come", "stai", "?"]` potrebbe essere convertito in `[1, 2, 0, 3]` se il modello non è stato adattato con il token `"stai"`.
@ -1133,7 +1154,7 @@ Si considera valida la predizione nella quale il modello ha confidenza più alta
...
```
Questa 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.0*, 2.0*, 3.0*, 4.0*, 5.0*).
Questa 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*).
Esso 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.
@ -1223,6 +1244,7 @@ class HuggingBertTokenizer(HuggingTokenizer):
[`jq`]: https://jqlang.github.io/jq/
[`cfig`]: https://cfig.readthedocs.io
[`cfig.Configuration`]: https://cfig.readthedocs.io/en/latest/reference.html#cfig.config.Configuration
[MongoDB]: https://www.mongodb.com