NLP Course documentation

È arrivato il momento di tagliuzzare

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

È arrivato il momento di tagliuzzare

Ask a Question Open In Colab Open In Studio Lab

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'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!