mirror of
https://github.com/Steffo99/unimore-bda-6.git
synced 2024-11-21 23:44:19 +00:00
Write chapter 2
This commit is contained in:
parent
ff4fff3052
commit
f0561cf078
1 changed files with 214 additions and 11 deletions
225
README.md
225
README.md
|
@ -36,14 +36,16 @@ In questo progetto si è realizzato una struttura che permettesse di mettere a c
|
|||
|
||||
Il codice dell'attività è incluso come package Python 3.10 compatibile con PEP518.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> 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.
|
||||
>
|
||||
> 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.
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Il progetto non supporta Python 3.11 per via del mancato supporto di Tensorflow a quest'ultimo.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> 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.
|
||||
|
||||
#### Installazione del package
|
||||
|
||||
Per installare il package, è necessario eseguire i seguenti comandi dall'interno della directory del progetto:
|
||||
|
@ -520,20 +522,214 @@ 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).
|
||||
|
||||
## Ri-implementazione dell'esercizio con NLTK - `.analysis.nltk_sentiment`
|
||||
## 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`.
|
||||
|
||||
### Wrapping del tokenizzatore di NLTK - `.tokenizer.nltk_word_tokenize`
|
||||
### Ri-creazione del tokenizer di Christopher Potts - `.tokenizer.potts`
|
||||
### Problemi di memoria
|
||||
|
||||
## Ottimizzazione di memoria
|
||||
### Caching - `.database.cache` e `.gathering`
|
||||
Si è creata una nuova sottoclasse di `BaseTokenizer`, `NLTKWordTokenizer`, che usa la tokenizzazione inclusa con NLTK.
|
||||
|
||||
Per 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.
|
||||
|
||||
La 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.
|
||||
|
||||
(È 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`.)
|
||||
|
||||
```python
|
||||
class NLTKWordTokenizer(BaseTokenizer):
|
||||
def tokenize(self, text: str) -> t.Iterator[str]:
|
||||
tokens = nltk.word_tokenize(text)
|
||||
nltk.sentiment.util.mark_negation(tokens, shallow=True)
|
||||
return tokens
|
||||
```
|
||||
|
||||
### Costruzione del modello - `.analysis.nltk_sentiment`
|
||||
|
||||
Si è creata anche una sottoclasse di `BaseSentimentAnalyzer`, `NLTKSentimentAnalyzer`, che utilizza per la classificazione un modello di tipo [`nltk.sentiment.SentimentAnalyzer`].
|
||||
|
||||
```python
|
||||
class NLTKSentimentAnalyzer(BaseSentimentAnalyzer):
|
||||
def __init__(self, *, tokenizer: BaseTokenizer) -> None:
|
||||
super().__init__(tokenizer=tokenizer)
|
||||
self.model: nltk.sentiment.SentimentAnalyzer = nltk.sentiment.SentimentAnalyzer()
|
||||
self.trained: bool = False
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Successivamente:
|
||||
|
||||
5. Identifica la presenza delle caratteristiche in ciascun elemento del training set
|
||||
6. Addestra un classificatore Bayesiano semplice ("naive Bayes") perchè determini la probabilità che data una certa feature, una recensione abbia un certo numero di stelle
|
||||
|
||||
```python
|
||||
...
|
||||
|
||||
def _add_feature_unigrams(self, dataset: t.Iterator[TokenizedReview]) -> None:
|
||||
"Register the `nltk.sentiment.util.extract_unigram_feats` feature extrator on the model."
|
||||
tokenbags = map(lambda r: r.tokens, dataset)
|
||||
all_words = self.model.all_words(tokenbags, labeled=False)
|
||||
unigrams = self.model.unigram_word_feats(words=all_words, min_freq=4)
|
||||
self.model.add_feat_extractor(nltk.sentiment.util.extract_unigram_feats, unigrams=unigrams)
|
||||
|
||||
def _add_feature_extractors(self, dataset: t.Iterator[TextReview]):
|
||||
"Register new feature extractors on the `.model`."
|
||||
dataset: t.Iterator[TokenizedReview] = map(self.tokenizer.tokenize_review, dataset)
|
||||
self._add_feature_unigrams(dataset)
|
||||
|
||||
def __extract_features(self, review: TextReview) -> tuple[Features, str]:
|
||||
"Convert a TextReview to a (Features, str) tuple. Does not use `SentimentAnalyzer.apply_features` due to unexpected behaviour when using iterators."
|
||||
review: TokenizedReview = self.tokenizer.tokenize_review(review)
|
||||
return self.model.extract_features(review.tokens), str(review.rating)
|
||||
|
||||
def train(self, training_dataset_func: CachedDatasetFunc, validation_dataset_func: CachedDatasetFunc) -> None:
|
||||
if self.trained:
|
||||
raise AlreadyTrainedError()
|
||||
self._add_feature_extractors(training_dataset_func())
|
||||
featureset: t.Iterator[tuple[Features, str]] = map(self.__extract_features, training_dataset_func())
|
||||
self.model.classifier = nltk.classify.NaiveBayesClassifier.train(featureset)
|
||||
self.trained = True
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Infine, implementa la funzione `use`, che:
|
||||
|
||||
1. tokenizza la stringa ottenuta in input
|
||||
2. utilizza il modello precedentemente addestrato per determinare la categoria ("rating") di appartenenza
|
||||
|
||||
```python
|
||||
...
|
||||
|
||||
def use(self, text: str) -> float:
|
||||
if not self.trained:
|
||||
raise NotTrainedError()
|
||||
tokens = self.tokenizer.tokenize(text)
|
||||
rating = self.model.classify(instance=tokens)
|
||||
rating = float(rating)
|
||||
return rating
|
||||
```
|
||||
|
||||
#### 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`].
|
||||
|
||||
Per permetterne l'esecuzione su computer con 16 GB di RAM, si è deciso di impostare la dimensione predefinita del training set a `4000` documenti.
|
||||
|
||||
### Ri-creazione del tokenizer di Christopher Potts - `.tokenizer.potts`
|
||||
|
||||
> 1. Utilizzare come tokenizer il “sentiment tokenizer” di Christopher Potts (link disponibile nelle slide del corso);
|
||||
|
||||
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.
|
||||
|
||||
Prima di effettuare la tokenizzazione, il tokenizer normalizza l'input:
|
||||
|
||||
1. convertendo tutte le entità HTML come `<` nel loro corrispondente unicode `<`
|
||||
2. convertendo il carattere `&` nel token `and`
|
||||
|
||||
Il tokenizer effettua poi la tokenizzazione usando espressioni regolari definite in `words_re_string` per catturare token di diversi tipi, in ordine:
|
||||
|
||||
* emoticon testuali `:)`
|
||||
* numeri di telefono statunitensi `+1 123 456 7890`
|
||||
* tag HTML `<b>`
|
||||
* username stile Twitter `@steffo`
|
||||
* hashtag stile Twitter `#Big_Data_Analytics`
|
||||
* parole con apostrofi `i'm`
|
||||
* numeri `-9000`
|
||||
* parole senza apostrofi `data`
|
||||
* ellissi `. . .`
|
||||
* gruppi di caratteri non-whitespace `🇮🇹`
|
||||
|
||||
Dopo aver tokenizzato, il tokenizer processa il risultato:
|
||||
|
||||
1. 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`)
|
||||
|
||||
Il codice riassunto del tokenizer è dunque:
|
||||
|
||||
```python
|
||||
class PottsTokenizer(BaseTokenizer):
|
||||
emoticon_re_string = r"""[<>]?[:;=8][\-o*']?[)\](\[dDpP/:}{@|\\]"""
|
||||
emoticon_re = re.compile(emoticon_re_string)
|
||||
|
||||
words_re_string = "(" + "|".join([
|
||||
emoticon_re_string,
|
||||
r"""(?:[+]?[01][\s.-]*)?(?:[(]?\d{3}[\s.)-]*)?\d{3}[\-\s.]*\d{4}""",
|
||||
r"""<[^>]+>""",
|
||||
r"""@[\w_]+""",
|
||||
r"""#+[\w_]+[\w'_-]*[\w_]+""",
|
||||
r"""[a-z][a-z'_-]+[a-z]""",
|
||||
r"""[+-]?\d+(?:[,/.:-]\d+)?""",
|
||||
r"""[\w_]+""",
|
||||
r"""[.](?:\s*[.])+""",
|
||||
r"""\S+""",
|
||||
]) + ")"
|
||||
|
||||
words_re = re.compile(words_re_string, re.I)
|
||||
|
||||
digit_re_string = r"&#\d+;"
|
||||
digit_re = re.compile(digit_re_string)
|
||||
|
||||
alpha_re_string = r"&\w+;"
|
||||
alpha_re = re.compile(alpha_re_string)
|
||||
|
||||
amp = "&"
|
||||
|
||||
@classmethod
|
||||
def html_entities_to_chr(cls, s: str) -> str:
|
||||
"Internal metod that seeks to replace all the HTML entities in s with their corresponding characters."
|
||||
# First the digits:
|
||||
ents = set(cls.digit_re.findall(s))
|
||||
if len(ents) > 0:
|
||||
for ent in ents:
|
||||
entnum = ent[2:-1]
|
||||
try:
|
||||
entnum = int(entnum)
|
||||
s = s.replace(ent, chr(entnum))
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
# Now the alpha versions:
|
||||
ents = set(cls.alpha_re.findall(s))
|
||||
ents = filter((lambda x: x != cls.amp), ents)
|
||||
for ent in ents:
|
||||
entname = ent[1:-1]
|
||||
try:
|
||||
s = s.replace(ent, chr(html.entities.name2codepoint[entname]))
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
s = s.replace(cls.amp, " and ")
|
||||
return s
|
||||
|
||||
@classmethod
|
||||
def lower_but_preserve_emoticons(cls, word):
|
||||
"Internal method which lowercases the word if it does not match `.emoticon_re`."
|
||||
if cls.emoticon_re.search(word):
|
||||
return word
|
||||
else:
|
||||
return word.lower()
|
||||
|
||||
def tokenize(self, text: str) -> t.Iterator[str]:
|
||||
text = self.html_entities_to_chr(text)
|
||||
tokens = self.words_re.findall(text)
|
||||
tokens = map(self.lower_but_preserve_emoticons, tokens)
|
||||
tokens = list(tokens)
|
||||
return tokens
|
||||
```
|
||||
|
||||
## Implementazione di modelli con Tensorflow - `.analysis.tf_text`
|
||||
### Creazione di tokenizzatori compatibili con Tensorflow - `.tokenizer.plain` e `.tokenizer.lower`
|
||||
### Caching - `.database.cache` e `.gathering`
|
||||
### Creazione di un modello di regressione - `.analysis.tf_text.TensorflowPolarSentimentAnalyzer`
|
||||
### Creazione di un modello di categorizzazione - `.analysis.tf_text.TensorflowCategorySentimentAnalyzer`
|
||||
#### Esplosione del gradiente
|
||||
### Esplosione del gradiente
|
||||
|
||||
## Implementazione di tokenizzatori di HuggingFace - `.tokenizer.hugging`
|
||||
|
||||
|
@ -550,3 +746,10 @@ Le valutazioni di efficacia vengono effettuate fino al raggiungimento di `TARGET
|
|||
[`__debug__`]: https://docs.python.org/3/library/constants.html#debug__
|
||||
[`-O`]: https://docs.python.org/3/using/cmdline.html#cmdoption-O
|
||||
[`str.split`]: https://docs.python.org/3/library/stdtypes.html?highlight=str%20split#str.split
|
||||
[`nltk.tokenize.word_tokenize`]: https://www.nltk.org/api/nltk.tokenize.word_tokenize.html?highlight=word_tokenize#nltk.tokenize.word_tokenize
|
||||
[Punkt]: https://www.nltk.org/api/nltk.tokenize.PunktSentenceTokenizer.html#nltk.tokenize.PunktSentenceTokenizer
|
||||
[Treebank]: https://www.nltk.org/api/nltk.tokenize.TreebankWordTokenizer.html#nltk.tokenize.TreebankWordTokenizer
|
||||
[`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
|
||||
[`nltk.sentiment.SentimentAnalyzer`]: https://www.nltk.org/api/nltk.sentiment.sentiment_analyzer.html?highlight=nltk+sentiment+sentimentanalyzer#nltk.sentiment.sentiment_analyzer.SentimentAnalyzer
|
||||
[`list`]: https://docs.python.org/3/library/stdtypes.html?highlight=list#list
|
||||
[tokenizer di Christopher Potts]: http://sentiment.christopherpotts.net/tokenizing.html
|
||||
|
|
Loading…
Reference in a new issue