#
È arrivato il momento di tagliuzzare

La maggior parte delle volte, i dati su cui lavorerai non saranno perfettamente pronti a essere usati per l’addestramento. In questa sezione esploreremo alcune funzionalità di 🤗 Datasets per pulire i tuoi dataset.

##
Tagliuzzare i tuoi dati

Proprio come Pandas, 🤗 Datasets offre diverse funzionalità per manipolare il contenuto degli oggetti `Dataset` e `DatasetDict`. Abbiamo già visto il metodo `Dataset.map()` nel Capitolo 3, e in questa sezione esploreremo altre funzioni a nostra disposizione.

Ai fini di quest’esempio useremo il Drug Review Dataset, che raccoglie le recensioni di pazienti su vari medicinali, assieme alla condizione curata e a una valutazione da 0 a 10 del grado di soddisfazione del paziente. Prima di tutto scarichiamo ed estraiamo i dati, utilizzando i comandi `wget` e `unzip`:

    !wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip"
    !unzip drugsCom_raw.zip

Poiché TSV non è altro che una variante di CSV che usa come separatore tabulatori al posto delle virgole, caricheremo questi file utilizzando lo script `csv` e specificando l’argomento `delimiter` nella funzione `load_dataset()`, come segue:

    from datasets import load_dataset

    data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
    # \t rappresenta il tabulatore in Python
    drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")

È buona prassi nell’analisi dati recuperare un piccolo campione casuale per farsi un’idea del tipo di dati con cui si sta lavorando. Utilizzando 🤗 Datasets, possiamo crare un campione casuale concatenando le funzioni `Dataset.shuffle()` e `Dataset.select()`:

    drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
    # Diamo un'occhiata ai primi esempi
    drug_sample[:3]

    {'Unnamed: 0': [87571, 178045, 80482],
     'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
     'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'],
     'review': ['"like the previous person mention, I&#039;m a strong believer of aleve, it works faster for my gout than the prescription meds I take. No more going to the doctor for refills.....Aleve works!"',
      '"I have taken Cymbalta for about a year and a half for fibromyalgia pain. It is great\r\nas a pain reducer and an anti-depressant, however, the side effects outweighed \r\nany benefit I got from it. I had trouble with restlessness, being tired constantly,\r\ndizziness, dry mouth, numbness and tingling in my feet, and horrible sweating. I am\r\nbeing weaned off of it now. Went from 60 mg to 30mg and now to 15 mg. I will be\r\noff completely in about a week. The fibro pain is coming back, but I would rather deal with it than the side effects."',
      '"I have been taking Mobic for over a year with no side effects other than an elevated blood pressure.  I had severe knee and ankle pain which completely went away after taking Mobic.  I attempted to stop the medication however pain returned after a few days."'],
     'rating': [9.0, 3.0, 10.0],
     'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'],
     'usefulCount': [36, 13, 128]}

Da notare che abbiamo impostato il seed in `Dataset.shuffle()` per motivi di riproducibilità. `Dataset.select()` ha bisogno di un iterabile di indici, per cui abbiamo utilizzato `range(1000)` per recuperare i primi 1.000 esempi dal dataset mescolato. Da questo campione possiamo già vedere alcune particolarità del nostor dataset:

- La colonna `Unnamed: 0` assomiglia molto a un ID anonimizzato per ognuno dei pazienti.
- La colonna `condizione` include un mix di etichette maiuscole e minuscole.
- Le recensioni sono di diversa lunghezza e contengono un mix di separatori di riga Python (`\r\n`) e di codici di caratteri HTML come `&\#039`.

Ora vediamo come utilizzare 🤗 Datasets per risolvere alcuni di questi problemi. Per confermare l’ipotesi che la colonna `Unnamed: 0` rappresenti gli ID dei pazienti, possiamo usare la funzione `Dataset.unique()` per verificare che il numero di ID corrisponda al numero delle righe in ognuna delle sezioni:

    for split in drug_dataset.keys():
        assert len(drug_dataset[split]) == len(drug_dataset[split].unique("Unnamed: 0"))

Questo sembra confermare la nostra ipotesi, quindi puliamo un po’ il nostro dataset cambiando il nome della colonna `Unnamed: 0` in qualcosa di un po’ più comprensibile. Possiamo usare la funzione `DatasetDict.rename_column()` per rinominare la colonna in entrambe le sezioni:

    drug_dataset = drug_dataset.rename_column(
        original_column_name="Unnamed: 0", new_column_name="patient_id"
    )
    drug_dataset

    DatasetDict({
        train: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
            num_rows: 161297
        })
        test: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
            num_rows: 53766
        })
    })

✏️ Prova tu! Usa la funzione `Dataset.unique()` per trovare il numero di medicine diverse e condizioni nelle sezioni di addestramento e di test.

Ora, normaliziamo le etichette in `condition` utilizzando `Dataset.map()`. Così come abbiamo fatto con la tokenizzazione nel Capitolo 3, possiamo definire una semplice funzione che può essere applicata a tutte le righe di ogni sezione nel `drug_dataset`:

    def lowercase_condition(example):
        return {"condition": example["condition"].lower()}

    drug_dataset.map(lowercase_condition)

    AttributeError: 'NoneType' object has no attribute 'lower'

Oh no, abbiamo incontrato un problema con la nostra funzione! Dall’errore possiamo dedurre che alcuni dei valori nella colonna `condition` sono `None`, che non essendo stringhe non possono essere convertiti in lettere minuscole. Eliminiamo queste righe utilizzando `Dataset.filter()`, che funziona come `Dataset.map()` e accetta una funziona che riceve un singolo esempio del dataset. Invece di scrivere una funzione esplicita come:

    def filter_nones(x):
        return x["condition"] is not None

e utilizzare `drug_dataset.filter(filter_nones)`, possiamo utilizzare una funzione lambda e completare tutto in un’unica riga. In Python, le funzioni lambda sono funzioni che possiamo definire senza nominarle esplicitamente. Hanno la forma generale:

    lambda <argomenti> : <espressione>

dove `lambda' è una delle [keyword](https://docs.python.org/3/reference/lexical_analysis.html#keywords) speciali di Python,` <argomenti>`è una lista/set di valori separati da virgole che definisce l'input della funzione, e`<espressione>` rappresenta le operazioni che vogliamo eseguire. Ad esempio, posiamo definire una semplice funzione lamda che calcola il quadrato di un numero:

    lambda x : x * x

Per applicare questa funzione a un input, dobbiamo includere sia la funzione che l’input in parentesi:

    (lambda x: x * x)(3)

    9

Allo stesso modo, possiamo definire funzioni lmabda con argomenti multipli separandoli con virgoli. Ad esempio, possiamo calcolare l’area di un triangolo come segue:

    (lambda base, altezza: 0.5 * base * altezza)(4, 8)

    16.0

Le funzioni lambda sono utili quando vogliamo definire piccole funzioni monouso (per maggiori informazioni, invitiamo alla lettura dell’ottimo tutorial di Real Python di Andre Burgaud). In 🤗 Datasets, possiamo usare le funzioni lambda per definire semplici operazioni di mappatura e filtraggio. Utilizziamo questo trucchetto per eliminare i valori `None` nel nostro dataset:

    drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None)

Una volta rimosse le voci `None`, possiamo normalizzare la colonna `condition`:

    drug_dataset = drug_dataset.map(lowercase_condition)
    # Check that lowercasing worked
    drug_dataset["train"]["condition"][:3]

    ['left ventricular dysfunction', 'adhd', 'birth control']

Funziona! Or ache abbiamo pulito le nostre etichette, diamo un’occhiata a come pulire le recensioni.

##
Creare nuove colonne

Quando abbiamo a che fare con le recensioni di clienti, è buona pratica controllare il numero di parole in ogni recensione. Una recensione potrebbe contenere solo una parola com “Ottimo!” o un vero e proprio saggio di migliaia di parole, e a seconda dell’uso che ne farai dovrai affrontare queste situazioni in maniera diversa. Per calculare il numero di parole in ogni recensione, useremo un’euristica grezza basata sulla divisione dei testi sugli spazi.

Definiamo una semplice funzione che conta il numero di parole in ogni recensione:

    def compute_review_length(example):
        return {"review_length": len(example["review"].split())}

A differenza della nostra funzione `lowercase_condition()`, `compute_review_length()` ritorna un dizionario le cui chiavi non corrispondono a nessuna delle colonne nel dataset. In questo caso, quando `compute_review_length()` è passata a `Dataset.map()`, si applicherà a tutte le righe nel dataset per creare una nuova colonna `review_lenght`;

    drug_dataset = drug_dataset.map(compute_review_length)
    # Inspect the first training example
    drug_dataset["train"][0]

    {'patient_id': 206461,
     'drugName': 'Valsartan',
     'condition': 'left ventricular dysfunction',
     'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"',
     'rating': 9.0,
     'date': 'May 20, 2012',
     'usefulCount': 27,
     'review_length': 17}

Come previsto, una colonna `review_length` è stata aggiunta al nostro set di addestramento. Possiamo ordinare questa nuova colonna utilizzando `Dataset.sort()` per dare un’occhiata ai valori estremi:

    drug_dataset["train"].sort("review_length")[:3]

    {'patient_id': [103488, 23627, 20558],
     'drugName': ['Loestrin 21 1 / 20', 'Chlorzoxazone', 'Nucynta'],
     'condition': ['birth control', 'muscle spasm', 'pain'],
     'review': ['"Excellent."', '"useless"', '"ok"'],
     'rating': [10.0, 1.0, 6.0],
     'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'],
     'usefulCount': [5, 2, 10],
     'review_length': [1, 1, 1]}

Come sospettato, alcune revisioni contengono una sola parola che, benché potrebbe essere utile per la sentiment analysis, non dà informazioni utili per predirre la condizione.

🙋Un altro modo per aggiungere nuove colonne a un dataset è attraverso la funzione `Dataset.add_column()`. Questo ti permette di inserire le colonne come una lista Python o unarray NumPy, e può tornare utile in situazioni in cui `Dataset.map()` non è indicata per le tue analisi.

Usiamo la funzione `Dataset.filter()` per rimuovere le recensioni che contengono meno di 30 parole. Proprio come abbiamo fatto per la colonna `condizione`, possiamo eliminare le recensioni più brevi aggiungendo un filtro che lascia passare solo le recensioni più lunghe di una certa soglia:

    drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
    print(drug_dataset.num_rows)

    {'train': 138514, 'test': 46108}

Come puoi vedere, questo ha rimosso circa il 15% delle recensioni nelle sezioni di training e di test.

✏️ Prova tu! Usa la funzione `Dataset.sort()` per analizzare le revisioni con il maggior numero di parole. Controlla la documentazione per vedere quali argomenti bisogna usare per ordinare le recensioni in ordine decrescente di lunghezza.

L’ultima cosa che ci resta da risolvere è la presenza di codici HTML di caratteri nelle nostre recensioni. Possiamo usare il modulo Python `html` per sostituirli, così:

    import html

    text = "I&#039;m a transformer called BERT"
    html.unescape(text)

    "I'm a transformer called BERT"

We’ll use `Dataset.map()` to unescape all the HTML characters in our corpus:

    drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])})

Come puoi vedere, il metodo `Dataset.map()` è molto utile per processare i dati — e questo non è che la punta dell’iceberg di ciò che è in grado di fare!

##
I superpoteri del metodo `map()`

Il metodo `Dataset.map()` accetta un argomento `batched` che, se impostato su `True`, gli fa inviare un batch di esempi alla funzione map in una sola volta (la grandezza del batch è configurabile, ma di default è impostta a 1.000). Ad esempio, l’esecuzione delle funzione map precedente che ha sostituito tutti i caratteri HTML è stata un po’ lenta (puoi leggere il tempo impiegato dalle barre di progresso). Possiamo accelerare questo processo processando diversi elementi in contemporanea usando una comprensione di lista.

Quando si specifica `batched=True` la funzione riceva un dizionario con i campi del dataset, ma ogni valore è ora una lista di valori, e non un valore singolo. Il valore ritornato da `Dataset.map()` dovrebbe essere lo stesso: un dizionario con i campi che vogliano aggiornare o aggiungere al nostro dataset, e una lista di valori. Ad esempio, ecco un altro modo per sostituire tutti i carattere HTML, ma utilizzando `batched=True`:

    new_drug_dataset = drug_dataset.map(
        lambda x: {"review": [html.unescape(o) for o in x["review"]]}, batched=True
    )

Se utilizzi questo codice in un notebook, noterai che questo comando è molto più veloce del precedente. E non perché le nostre recensioni già state preprocessate, se esegui nuovamente le istruzioni della sezione precedente (senza `batched=True'), ci metterà lo stesso tempo di prima. Questo è perchè le comprensioni di lista sono solitamente più veloci delle loro controparti con ciclo` for`, e inoltre abbiamo guadagnato performance permettendo l’accesso a molti elementi in contemporanea invece di uno per volta.

Utilizzare `Dataset.map()` con `batched=True` sarà essenziale per sbloccare la velocità dei tokenizzatori “fast” che incontreremo nel Capitolo 6, che permettono di tokenizzare velocemente grandi liste di testi. Ad esempio, per tokenizzare tutte le recensioni di medicinali con un tokenizzatore veloce, potremmo usare una funzione come questa:

    from transformers import AutoTokenizer

    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

    def tokenize_function(examples):
        return tokenizer(examples["review"], truncation=True)

Come visto nel Capitolo 3, possiamo passare uno o più esempi al tokenizzatore. Le funzione può essere usata con o senza `batched=True`. Approfittiamo di quest’occasione per paragonare la performance delle diverse opzioni. In un notebook, possiamo cronomotrare un’istruzione su una singola riga aggiungendo `%time` prima della riga di codice che desideri cronometrare:

    %time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True)

Possiamo cronometrare anche un’intera cella inserento `%%time` all’inizio della cella. Sull’hardware che stiamo utilizzando, mostrava 10.8s pe rquest’istruzione (è il numero scritto dopo “Wall time”).

✏️ Prova tu! Esegui la stessa istruzione con e senza `batched=True`, poi prova con un tokenizzatore lento (aggiungi `add_fast=False` al metodo `AutoTokenizer.from_pretrained()`) così che puoi controllare i tempi sul tuo hardware.

Ecco i risultati che otteniamo con e senza utilizzare batch, con un tokenizzatore lento e uno veloce:

Opzioni | Tokenizzatore veloce | Tokenizzatore lento
--- | --- | ---
`batched=True` | 10.8s | 4min41s
`batched=False` | 59.2s | 5min3s

Questo significa che utilizzare un tokenizzatore veloce con l’opzione `batched=True` è 30 volte più veloce della sua controparte lenta con `batched=False` — ottimo! Questa è la ragione principale per cui i tokenizzatori veloci sono di default utilizzando `AutoTokenizer` (e il motivo per cui vengono chiamati “fast”). Sono in grado di raggiungere certe velocità perché dietro le quinte il codice di tokenizzazione è eseguito in Rust, un linguaggio che rende semplice l’esecuzione di codici in parallelo.

L’esecuzione in parallelo è anche il motivo per l’aumento di velocità x6 che il tokenizzatore veloce ottiene con `batched=True`: non è possibile eseguire in parallelo una sola operazione di tokenizzazione, ma quando vuoi tokenizzare molti testi contemporaneamente puoi dividere l’esecuzione su vari processi, ognuno responsabile dei propri testi.

`Dataset.map()` possiede inoltre alcune capacità di parallelizzazione per conto proprio. Non avendo però Rust alle proprie spalle, non può permettere a un tokenizzatore lento di raggiungere uno veloce, ma possono comunque tornare utili (soprattutto se stai utilizzando un tokenizatore che non possiede una versione veloce). Per abilitare il multiprocessing, usa l’argomenti `num_proc` e specifica il numero di processi da utilizzare quando evoci `Dataset.map()`:

    slow_tokenizer = AutoTokenizer.from_pretrained("bert-base-cased", use_fast=False)

    def slow_tokenize_function(examples):
        return slow_tokenizer(examples["review"], truncation=True)

    tokenized_dataset = drug_dataset.map(slow_tokenize_function, batched=True, num_proc=8)

Puoi sperimentare con le tempistiche per determinare il numero ottimale di processi da utilizzare; nel nostro caso 8 sembra produrre i risultati migliori. Ecco i numeri che abbiamo ottenuto con e senza multiprocessing:

Opzioni | Tokenizzatore veloce | Tokenizzatore lento
--- | --- | ---
`batched=True` | 10.8s | 4min41s
`batched=False` | 59.2s | 5min3s
`batched=True`, `num_proc=8` | 6.52s | 41.3s
`batched=False`, `num_proc=8` | 9.49s | 45.2s

Questi sono dei risultati molto più accettabili per il tokenizzatore lento, ma anche la performance dei tokenizzatori veloci è notevolmente migliorata. Notare, comunque, che non è sempre questo il caso: per valori di `num_proc` diversi da 8, i nostri test hanno mostrato che è più veloce utilizzare `batched=True` senza l’opzione `num_proc`. In generale, non raccomandiamo l’utilizzo di multiprocessing Python per tokenizzatori veloci con `batched=True`.

Utilizzare `num_proc` per accelerare i processi è generalmente una buona idea, a patto che la funzione che stai utilizzando non stia già usando un qualche tipo di multiprocessing per conto proprio.

Tutte queste funzionalità condensate in un unico metodo sono già molto utili, ma c’è altro! Con `Dataset.map()` e `batched=True`, è possibile modificare il numero di elementi nel tuo dataset. È particolarmente utile quando vuoi creare diverse feature di addestramento da un unico esempio, e ne avremo bisogno come parte di preprocessing per molti dei task NLP che affronteremo nel Capitolo 7.

💡 Nel machine learning, un esempio è solitamente definito come un insieme di feature che diamo in pasto al modello. In alcuni contesti, queste feature saranno l’insieme delle colonne in un `Dataset`, ma in altri casi (come ad esempio questo, o per il question answering), molte feature possono essere estratte da un singolo esempio, e appartenere a una sola colonna.

Diamo un’occhiata a come funziona! Tokenizziamo i nostri esempi e tronchiamoli a una lunghezza massima di 128, ma chiediamo al tokenizzatore di restituire tutti i pezzi di testo e non solo il primo. Questo può essere fatto con `return_overflowing_tokens=True`:

    def tokenize_and_split(examples):
        return tokenizer(
            examples["review"],
            truncation=True,
            max_length=128,
            return_overflowing_tokens=True,
        )

Testiamo questa funzione su un esempio prima di utilizzare `Dataset.map()` sull’intero dataset:

    result = tokenize_and_split(drug_dataset["train"][0])
    [len(inp) for inp in result["input_ids"]]

    [128, 49]

Quindi, il nostro primo esempio nel set di train è stato trasformaro in due feature perché tokenizzato in un numero maggiore di token di quelli specificati: il primo gruppo di lunghezza 128 token e il secondo di lunghezza 49. Facciamo la stessa cosa per tutti gli elementi del dataset!

    tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)

    ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000

Oh no! Non ha funzionato! Perché? Il messaggio di errore ci dà un indizio: c’è una discordanza tra la lungheza di una delle colonne (una è lunga 1.463 e l’altra 1.000). Se hai guardato la documentazione di `Dataset.map()`, ricorderai che quello è il numero di campioni passati alla funzione map; qui quei 1.000 esempi danno 1.463 nuove feature, che risulta in un errore di shape.

Il problema è che stiamo cercando di mescolare due dataset diversi di grandezze diverse: le colonne del `drug_dataset` avranno un certo numero di esempi (il 1.000 del nostro errore), ma il `tokenized_dataset` che stiamo costruendo ne avrà di più (il 1.463 nel nostro messaggio di errore). Non va bene per un `Dataset`, per cui abbiamo bisogno o di rimuovere le colonne dal dataset vecchio, o renderle della stessa dimensione del nuovo dataset. La prima opzione può essere effettuata utilizzando l’argomento `remove_columns`:

    tokenized_dataset = drug_dataset.map(
        tokenize_and_split, batched=True, remove_columns=drug_dataset["train"].column_names
    )

Ora funziona senza errori. Possiamo controllare che il nostro nuovo dataset contiene più elementi del dataset originale paragonando le lunghezze:

    len(tokenized_dataset["train"]), len(drug_dataset["train"])

    (206772, 138514)

Abbiamo già menzionato che possiamo risolvere il problema delle lunghezze discordanti cambiando la dimenzione delle vecchie colonne. Per far ciò, abbiamo bisogno del campo `overflow_to_sample_mapping` restituito dal tokenizzatore quando impostiamo `return_overflowing_tokens=True`. Così facendo avremo una mappatura degli indici delle nuove feature all’indice di campioni da cui sono state generate. Usando questa mappatura, possiamo associare a ogni chiava presente nel nostro dataset originale una lista di valori delle dimensioni giuste, ripetendo il valore di ogni esempio finché genera nuove feature:

    def tokenize_and_split(examples):
        result = tokenizer(
            examples["review"],
            truncation=True,
            max_length=128,
            return_overflowing_tokens=True,
        )
        # Estraiamo la mappatura tra gli indici vecchi e quelli nuovi
        sample_map = result.pop("overflow_to_sample_mapping")
        for key, values in examples.items():
            result[key] = [values[i] for i in sample_map]
        return result

Possiamo vedere come funziona con `Dataset.map()` senza aver bisogno di rimuovere le colonne vecchie:

    tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
    tokenized_dataset

    DatasetDict({
        train: Dataset({
            features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
            num_rows: 206772
        })
        test: Dataset({
            features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
            num_rows: 68876
        })
    })

Otteniamo lo stesso numero di feature di addestramento di prima, ma qui abbiamo conservato i campi originali. Se ti servono per un post-processing dopo aver applicato il tuo modello, potresti usare quest’approccio.

Ora abbiamo visto come usare 🤗 Datasets per preprocessare un dataset in diversi modi. Benché le funzioni di processamento di 🤗 Datasets soddisferà la maggior parte delle esigenze del modello che vuoi addestrare, ci saranno momenti in cui avrai bisogno di utilizzare Pandas per avere funzionalità ancora più potenti, come `DataFrame.groupby()` o API di alto livello per visualizzazione. Per fortuna, 🤗 Datasets è progettato per essere utilizzato con librerie come Pandas, NumPy, PyTorch, TensorFlow e JAX. Diamo un’occhiata a come funziona.

##
Da `Dataset` a `DataFrame` e ritorno

Per permettere la conversione tra librerie terze, 🤗 Datasets fornisce una funzione `Dataset.set_format()`. Questa funzione cambia il formato di output del dataset, così che puoi passare a un altro formato senza modificare il formato di dati soggiacente, che è Apache Arrow. La formattazione avviene direttamente in place. Per provare, convertiamo il nostro dataset per Pandas:

    drug_dataset.set_format("pandas")

Ora quando accediamo agli elementi del dataset otteniamo un `pandas.DataFrame` e non un dizionario:

    drug_dataset["train"][:3]

| patient_id | drugName | condition | review | rating | date | usefulCount | review_length
--- | --- | --- | --- | --- | --- | --- | --- | ---
0 | 95260 | Guanfacine | adhd | "My son is halfway through his fourth week of Intuniv..." | 8.0 | April 27, 2010 | 192 | 141
1 | 92703 | Lybrel | birth control | "I used to take another oral contraceptive, which had 21 pill cycle, and was very happy- very light periods, max 5 days, no other side effects..." | 5.0 | December 14, 2009 | 17 | 134
2 | 138000 | Ortho Evra | birth control | "This is my first time using any form of birth control..." | 8.0 | November 3, 2015 | 10 | 89

Creiamo un `pandas.DataFrame` per l’intero set di addestramento selezionando tutti gli elementi di `drug_dataset["train"]`:

    train_df = drug_dataset["train"][:]

🚨 Dietro le quinte, `Dataset.set_format()` modifica il formato di restituzione del meteodo dunder `__getitem__()` del dataset. Questo significa che quando vogliamo creare un nuovo oggetto come ad esempio `train_df` da un `Dataset` in formato `"pandas"`, abbiamo bisogno di suddividere l’intero dataset per ottenere un `pandas.DataFrame`. Puoi verificare da te che `drug_dataset["train"]` ha come tipo `Dataset`, a prescindere dal formato di output.

Da qui possiamo usare tutte le funzionalità Pandas che vogliamo. Ad esempio, possiamo creare un concatenamento per calcolare la distribuzione delle classi nelle voci `condition`:

    frequencies = (
        train_df["condition"]
        .value_counts()
        .to_frame()
        .reset_index()
        .rename(columns={"index": "condition", "condition": "frequency"})
    )
    frequencies.head()

| condition | frequency
--- | --- | ---
0 | birth control | 27655
1 | depression | 8023
2 | acne | 5209
3 | anxiety | 4991
4 | pain | 4744

E una volta che abbiamo finito con le nostre analisi su Pandas, possiamo sempre creare un nuovo oggetto `Dataset` utilizzando la funzione `Dataset.from_pandas()`:

    from datasets import Dataset

    freq_dataset = Dataset.from_pandas(frequencies)
    freq_dataset

    Dataset({
        features: ['condition', 'frequency'],
        num_rows: 819
    })

✏️ Prova tu! Calcola la valutazione media per i medicinali, e salviamo i risultati in un nuovo `Dataset`.

Questo conclude il nostro tour delle diverse tecniche di prepocessamento disponibile in 🤗 Datasets. Per riepilogare la sezione, creiamo un set di validazione per preparare il dataset su cui addestreremo un classificatore. Prima di far ciò, resettiamo il formato di output di `drug_dataset` da `"pandas"` a `"arrow"`:

    drug_dataset.reset_format()

##
Creare un set di validazione

Pur avendo un set di test che potremmo usare per la valutazione, è buona prassi lasciare il set di test intatto e creare un set di validazione sepearato durante lo sviluppo de lmodello. Una volta che sei soddisfatto della performance del tuo modello sul set di validazione, puoi proseguire con un ultimo check sul set di test. Questo processo aiuta a ridurre i rischi di overfitting sul set di test e di creare un modello che fallisce sui dati del mondo reale.

🤗 Datasets possiede una funzione `Dataset.train_test_split()`, basata sulla famosa funzionalità da `scikit-learn`. Proviamo a utilizzarla per dividere il nostro set di addestramento in sezioni di `addestramento` e di `validazione` (impostiamo l’argomento `seed` per motivi di riproducibilità):

    drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
    # Rinominare la sezione di "test" in "validazione"
    drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
    # Aggiungere il set "test" al nostor `DatasetDict`
    drug_dataset_clean["test"] = drug_dataset["test"]
    drug_dataset_clean

    DatasetDict({
        train: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
            num_rows: 110811
        })
        validation: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
            num_rows: 27703
        })
        test: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
            num_rows: 46108
        })
    })

Bene! Abbiamo preparato un dataset che è pronto per l’addestramento di modelli! Nella sezione 5 ti mostreremo come caricare i dataset nell’Hub di Hugging Face, ma per ora concludiamo la nostra analisi esplorando alcuni modi per salvare i dataset sulla tua macchina locale.

##
Salvare un dataset

Benché 🤗 Datasets memorizzi in cache tutti i dataset scaricati e le operazioni effettuate, ci sono momenti in cui vorrai salvare un dataset su disco (ad esempio, nel caso la cache venga eliminata). Come mostrato nella tabella successiva, 🤗 Datasets fornisce tre funzioni principali per salvare il tuo dataset in diversi formati:

Formato dati | Funzione
--- | ---
Arrow | `Dataset.save_to_disk()`
CSV | `Dataset.to_csv()`
JSON | `Dataset.to_json()`

Ad esempio, salviamo il nostro dataset pulito in formato Arrow:

    drug_dataset_clean.save_to_disk("drug-reviews")

Questo creerà un dizionario con la seguente struttura:

    drug-reviews/
    ├── dataset_dict.json
    ├── test
    │   ├── dataset.arrow
    │   ├── dataset_info.json
    │   └── state.json
    ├── train
    │   ├── dataset.arrow
    │   ├── dataset_info.json
    │   ├── indices.arrow
    │   └── state.json
    └── validation
        ├── dataset.arrow
        ├── dataset_info.json
        ├── indices.arrow
        └── state.json

dove possiamo vedere che ogni sezione è associata alla propria tavola dataset.arrow, e alcuni metadata sono salvati in dataset_info.json e state.json. Puoi pensare al formato Arrow come a una tavola sofisticata di colonne e righe, ottimizzata per costruire applicazioni ad alte prestazioni che processano e trasportanto grandi dataset.

Una volta che il dataset è stato salvato, possiamo caricarlo utilizzando la funzione `load_from_disk()`:

    from datasets import load_from_disk

    drug_dataset_reloaded = load_from_disk("drug-reviews")
    drug_dataset_reloaded

    DatasetDict({
        train: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
            num_rows: 110811
        })
        validation: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
            num_rows: 27703
        })
        test: Dataset({
            features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
            num_rows: 46108
        })
    })

Per i formati CSV e JSON, dobbiamo salvare ogni sezione come file separato. Un modo per farlo è iterando sulle chiavi e i valori dell’oggetti `DatasetDict`:

    for split, dataset in drug_dataset_clean.items():
        dataset.to_json(f"drug-reviews-{split}.jsonl")

Questo salva ogni sezione in formato JSON Lines, in cui ogni riga del dataset è salvata come una singola riga di JSON. Ecco come appare il primo esempio:

    !head -n 1 drug-reviews-train.jsonl

    {"patient_id":141780,"drugName":"Escitalopram","condition":"depression","review":"\"I seemed to experience the regular side effects of LEXAPRO, insomnia, low sex drive, sleepiness during the day. I am taking it at night because my doctor said if it made me tired to take it at night. I assumed it would and started out taking it at night. Strange dreams, some pleasant. I was diagnosed with fibromyalgia. Seems to be helping with the pain. Have had anxiety and depression in my family, and have tried quite a few other medications that haven't worked. Only have been on it for two weeks but feel more positive in my mind, want to accomplish more in my life. Hopefully the side effects will dwindle away, worth it to stick with it from hearing others responses. Great medication.\"","rating":9.0,"date":"May 29, 2011","usefulCount":10,"review_length":125}

Possiamo usare le tecniche studiate nella sezione 2 per caricare i file JSON come segue:

    data_files = {
        "train": "drug-reviews-train.jsonl",
        "validation": "drug-reviews-validation.jsonl",
        "test": "drug-reviews-test.jsonl",
    }
    drug_dataset_reloaded = load_dataset("json", data_files=data_files)

E questo è tutto per la nostra visita nel data wrangling con 🤗 Datasets! Ora che abbiamo un dataset pulito su cui addestrare un modello, ecco alcune idee che potresti testare:

1. Usa le tecniche studiate nel Capitolo 3 per addestrare un classificatore che può predirre la condizione del pazionte sulla base della recensione del medicinale.
2. Usa la pipeline `summarization` del Capitolo 1 per generare riassunti delle recensioni.

In seguito, daremo un’occhiata a come 🤗 Datasets ti permette di lavorare su enormi dataset senza far scoppiare il tuo portatile!