#
E timpul să tăiem și să analizăm datele

În cea mai mare parte, datele cu care lucrezi nu vor fi perfect pregătite pentru antrenarea modelelor. În această secțiune vom explora features variate pe care 🤗 Datasets le oferă pentru curățirea dataseturilor.

##
Slicing și dicing asupra datelor

Asemenea Pandas, 🤗 Datasets oferă mai multe funcții pentru a manipula conținutul obiectelor `Dataset` și `DatasetDict`. Am întâlnit deja metoda `Dataset.map()` în Capitolul 3, iar în această secțiune vom explora alte funcții de care dispunem.

În acest exemplu, vom folosi Drug Review Dataset găzduit pe UC Irvine Machine Learning Repository, care conține reviewurile pacienților privind diverse medicamente, alături de bolile care sunt tratate și o evaluare de 10 stele a satisfacției pacientului.

În primul rând trebuie să descărcăm și să extragem datele, ceea ce se poate de făcut cu comenzile `wget` și `unzip`:

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

Deoarece TSV este o variantă a CSV care folosește taburi în loc de virgulă ca separator, putem încărca aceste fișiere prin folosirea scriptului de încărcare `csv` și specificarea argumentului `delimiter` în funcția `load_dataset()` astfel:

    from datasets import load_dataset

    data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
    # \t este caracterul tab de Python
    drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")

O practică bună atunci când faceți orice fel de analiză a datelor este să vă luați un mic random sample pentru a înțelege cu ce tip de date lucrați. În 🤗 Datasets, putem crea o colecție aleatorie prin legarea funcțiilor `Dataset.shuffle()` și `Dataset.select()`:

    drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
    # Vizualizați primele câteva exemple
    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]}

Atrageți atenția că am fixat seedul în `Dataset.shuffle()` pentru posibilitatea de reproducere. `Dataset.select()` se așteaptă la un iterabil cu indices, deci noi am scris `range(1000)` pentru a primi primele 1000 de exemple din datasetul amestecat. Din acest sample putem vedea câteva ciudățenii în datasetul nostru:

- Coloana `Unnamed: 0` are un aspect neobișnuit, care sugerează că este un anonymized ID a fiecărui pacient.
- Coloana `condition` conține labeluri majuscule și minuscule.
- Recenziile au lungimi variate și conțin caractere Python precum `\r\n` şi caractere HTML ca `&\#039;`.

Hai să vedem cum putem folosi 🤗 Datasets pentru a face față fiecărei dintre aceste probleme. Pentru a testa ipoteza identificării pacientului pentru coloana `Unnamed: 0`, putem folosi funcția `Dataset.unique()` pentru a verifica dacă numărul de ID-uri corespunde cu numărul de rânduri în fiecare split:

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

Aceasta pare să confirme ipoteza, deci putem curăța datasetul puțin, redenumind coloana `Unnamed: 0` pentru a-i da un nume mai interpretabil. Putem folosi funcția `DatasetDict.rename_column()` pentru a renumea coloana în același timp în ambele splituri:

    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
        })
    })

✏️ Încearcă! Folosiți funcția `Dataset.unique()` pentru a găsi numărul de medicamente și condiții unice în seturile de antrenare și testare.

În continuare, vom normaliza toate `condition` labels folosind `Dataset.map()`. La fel cum am făcut cu tokenizarea în Capitolul 3, putem defini o funcție simplă care poate fi aplicată pe toate rândurile fiecărui split din `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, am întâmpinat o problemă cu funcția map! Din eroarea noastră se poate deduce că unele intrări din coloana `condition` sunt `None`, care nu pot fi convertite la caracterul mic pentru că nu sunt string-uri. Vom elimina aceste rânduri folosind `Dataset.filter()`, care funcționează în mod similar cu `Dataset.map()` și se așteaptă o funcție care primește un exemplu al datasetului.

În loc de a scrie o funcție explicită ca:

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

și apoi să rulăm `drug_dataset.filter(filter_nones)`, putem face acest lucru într-o linie folosind o funcție lambda. În Python, funcțiile lambda sunt funcții mici care pot fi definite fără a le numi. Ele au forma generală:

    lambda <argumente> : <expresie>

unde `lambda` este unul dintre cuvintele cheie Python, `<argumente>` reprezintă o listă/set de valori separate prin virgulă care definesc inputurile funcției și `<expresie>` reprezintă operațiile pe care dorim să le executăm. De exemplu, putem defini o funcție lambda care ridică un număr la pătrat:

    lambda x: x * x

Pentru a aplica această funcție la un input, trebuie să îi facem wrap și pe să punem inputul în paranteze:

    (lambda x: x * x)(3)

    9

La fel, putem defini funcții lambda cu mai multe argumente prin separarea acestora prin virgulă. De exemplu, putem calcula suprafața unui triunghi ca:

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

    16.0

Funcțiile lambda sunt utile atunci când dorim să definim funcții mici, pentru o singură folosire (pentru mai multe informații despre ele, recomandăm citirea excelentului Real Python tutorial scris de Andre Burgaud). În contextul 🤗 Datasets, putem utiliza funcțiile lambda pentru a defini operații simple de map și filter, astfel încât să eliminăm intrările `None` din datasetul nostru:

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

Cu intrările `None` eliminate, putem normaliza coloana `condition`:

    drug_dataset = drug_dataset.map(lowercase_condition)
    # Verificăm dacă lowercasing a funcționat
    drug_dataset["train"]["condition"][:3]

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

Funcționează! Acum că am curățat labelurile, să vedem cum putem curăți și recenziile.

##
Crearea de noi coloane

Atunci când lucrați cu recenziile clienților, o practică bună este să verificați numărul de cuvinte în fiecare recenzie. O recenzie poate fi doar un singur cuvânt, cum ar fi “Excelent!” sau un eseu complet care are sute de cuvinte și depinde de cazul pe care îl aveți la vedere, aici trebuie să vă asigurați că faceți față acestor extreme diferit. Pentru a calcula numărul de cuvinte în fiecare recenzie, vom folosi un heuristic aproximativ bazat pe splittingul textului prin spații.

Vom defini o funcție simplă care numără numărul de cuvinte din fiecare recenzie:

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

Spre deosebire de funcția `lowercase_condition()`, `compute_review_length()` returnează un dicționar ale cărui key nu corespund uneia dintre numele coloanelor din dataset. În acest caz, atunci când `compute_review_length()` este transmis în `Dataset.map()`, el va fi aplicat pe toate rândurile din dataset pentru a crea o nouă coloană `review_length`:

    drug_dataset = drug_dataset.map(compute_review_length)
    # Inspectăm primul exemplu de training
    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}

Așa cum era de așteptat, putem vedea o nouă coloană `review_length` adăugată la setul de antrenare. Putem sorta această nouă coloană cu `Dataset.sort()` pentru a vedea cum valorile extreme arată:

    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]}

Precum am presupus, unele recenii conțin doar un singur cuvânt, ceea ce, deși ar putea fi OK pentru analiza sentimentului, nu ar fi informativ dacă vrem să prezicem condiției.

🙋 O alternativă la adăugarea unei noi coloane într-un dataset este funcția `Dataset.add_column()`. Aceasta permite să oferiți coloana ca o listă Python sau array NumPy și poate fi utilă în situații în care `Dataset.map()` nu este bine adaptat pentru analiza dumneavoastră.

Hai să folosim funcția `Dataset.filter()` pentru a elimina recenziile care conțin mai puțin de 30 de cuvinte. Similar cum am făcut în cazul coloanei `condition`, putem elimina recenziile foarte scurte cerând ca recenziile să aibă o lungime mai mare decât acest prag:

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

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

După cum vedeți, aceasta a eliminat aproximativ 15% din recenziile noastrem, din seturile originale de antrenare și testare.

✏️ Încercați! Folosiți funcția `Dataset.sort()` pentru a inspecta recenziile cu cele mai mari numere de cuvinte. Vezi documentația pentru a vedea ce argument trebuie să folosești pentru a sorta recenziile în ordine descrescătoare.

Ultima chestie de care trebuie să ne ocupăm este prezența caracterelor HTML în recenziile noastre. Putem folosi modulul `html` din Python pentru a face unescape acestor caractere:

    import html

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

    "I'm a transformer called BERT"

Vom folosi `Dataset.map()` pentru a face unescape toate caracterele HTML din corpus:

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

În mod evident, metoda `Dataset.map()` este foarte utilă pentru procesarea datelor – și nu am abordat decât o mică parte din ceea ce poate face!

##
Superputerile metodei map()

Metoda `Dataset.map()` acceptă un argument `batched` care, dacă este setat pe `True`, cauzează ca ea să trimită un batch de exemple la funcția map în același timp (dimensiunea batchului poate fi configurată dar defaultul este 1.000). De exemplu, anterior am folosit o funcție map care a făcut unescaped toate caracterele HTML din recenziile noastre și i-a luat câteva secunde să execute (puteți citi timpul pe progress bars). Putem accelera acest lucru prin procesarea mai multor elemente în același timp folosind list comprehension.

Când specificați `batched=True` funcția primește un dicționar cu câmpurile datasetului, dar fiecare valoare este acum list of values și nu doar o singură valoare. Valoarea de return a `Dataset.map()` ar trebui să fie la fel: un dicționar cu câmpurile pe care dorim să le actualizăm sau adăugăm în datasetul nostru, și o listă de valori. De exemplu, mai jos este alt mod de a face unescape tuturor caracterelor HTML din recenziile noastre, folosind `batched=True`:

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

Dacă executați acest cod într-un notebook, veți vedea că această comandă se execută mult mai rapid decât cea anterioră. Și nu pentru că recenziile noastre au fost deja HTML-unescaped – dacă reexecutați instrucția precedentă (fără `batched=True`), ea va lua același timp ca înainte. Acest lucru se datorează faptului că list comprehension sunt mai rapide decât executarea aceluiași cod într-un `for` loop, și am câștigat, de asemenea, puțină performanțp accesând multe elemente în același timp, în loc unul câte unul.

Folosirea `Dataset.map()` cu `batched=True` este esențială pentru a obține viteza “rapidă” a tokenizerilor pe care îi vom întâlni în Capitolul 6, care pot repede să tokenizeze listelor mari de texte. De exemplu, pentru tokenizarea tuturor recenziilor medicamentelor cu un tokenizer rapid, putem folosi o funcție ca aceasta:

    from transformers import AutoTokenizer

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

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

Așa cum am văzut în Capitolul 3, putem transmite un singur sau câteva exemple către tokenizer, așadar putem folosi această funcție cu sau fără `batched=True`. Hai să ne folosim de această oportunitate și să comparăm performanța diferitelor opțiuni. Într-un notebook puteți măsura timpul unei instrucțiie de o line prin adăugarea `%time` înaintea acelei linii de cod pe care doriți să o măsurați:

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

Puteți și să măsurați un întreg cell prin scrierea `%%time` la începutul celulei. În hardware-ul pe care l-am executat, acest lucru a arătat 10.8s pentru această instrucție (este numărul scris după “Wall time”).

✏️ Încercați! Executați aceeași instrucție cu și fără `batched=True`, apoi încercați-o cu un tokenizer lent (adaugați `use_fast=False` în metoda `AutoTokenizer.from_pretrained()`), astfel să puteți vedea ce numere obțineți pe hardwareul vostru.

Aici sunt rezultatele pe care le-am obținut cu și fără batching, folosind un tokenizer rapid și lent:

Options | Fast tokenizer | Slow tokenizer
--- | --- | ---
`batched=True` | 10.8s | 4min41s
`batched=False` | 59.2s | 5min3s

Aceasta înseamnă că utilizarea unui tokenizer rapid cu opțiunea `batched=True` este de 30 de ori mai rapidă decât varianta lentă fără batching - acest lucru este pur și simplu uimitor! Acesta este motivul principal pentru care tokenizerii rapizi sunt setați implicit când se utilizează `AutoTokenizer` (și de ce sunt numiți “rapizi”). Ei pot atinge o asemenea accelerație datorită faptului că codul de tokenizare este executat în Rust, care este un limbaj care facilitează paralelizarea execuției.

Parallelization este și motivul pentru care tokenizerul rapid realizează o accelerare de aproape 6 ori cu batching: nu puteți paraleliza o singură operație de tokenizare, dar atunci când doriți să tokenizați multe texte în același timp, puteți să faceți split execuției pe mai multe procese, fiecare răspunzând pentru propriile texte.

`Dataset.map()` are și o capacitate de parallelization proprie. Deoarece nu sunt susținute de Rust, nu pot să le ofere aceeași accelerație tokenizerilori înceți ca tokenizerilor rapizi, dar pot încă fi utili (în special dacă utilizați un tokenizer care nu are o variantă rapidă). Pentru a activa multiprocessingul, folosiți argumentul `num_proc` și specificați numărul de procese să fie utilizate în apelul `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)

Puteți experimenta puțin cu timpii pentru a determina numărul de procese optime; în cazul nostru 8 s-a dovedit a produce cea mai mare accelerație. Aici sunt rezultatele pe care le-am obținut cu și fără multiprocessing:

Options | Fast tokenizer | Slow tokenizer
--- | --- | ---
`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

Aceste rezultate sunt mult mai bune pentru tokenizerul lent, dar și performanța tokenizerului rapid a fost semnificativ îmbunătățită. Cu toate acestea, trebuie să reamintim că aceasta nu va fi întotdeauna cazul - testele noastre au arătat că este mai rapid să utilizați `batched=True` fără acest argument în cazurile în care valoarea lui `num_proc` diferă de 8. În general, nu vă recomandăm utilizarea multiplicării proceselor pentru tokenizorii rapizi cu `batched=True`.

<tip>

Utilizarea `num_proc` pentru a accelera procesarea este de obicei o idee excelentă, atâta timp cât funcția pe care o utilizați nu utilizează deja multiprocessing.

</tip>

Toate aceste funcționalități condensate într-o singură metodă este foarte impresionant, dar asta nu e totul! Cu `Dataset.map()` și `batched=True` puteți modifica numărul de elemente din datasetul dumneavoastră. Acesta este extrem de util în numeroase situații în care doriți să creați mai multe caracteristici de antrenare dintr-un singur exemplu, și vom avea nevoie de acest lucru ca parte a preprocesării pentru câteva dintre sarcinile NLP pe care le vom discuta în Capitolul 7.

<tip>

💡 În machine learning, un exemplu este de obicei definit ca fiind un set de features care se oferă modelului. În unele contexte, acestea vor fi seturile de coloane dintr-un `Dataset`, dar în altele (ca și aici și pentru răspunderea la întrebări) mai multe caracteristici pot fi extrase dintr-un singur exemplu și să aparțină unei singure coloane.

</tip>

Hai să vedem cum funcționează! Aici vom tokeniza exemplele și le vom face truncatela lungimea maximă de 128, dar vom cere tokenizerului să returneze toate chunkurile de text în loc de prima. Acest lucru poate fi făcut cu `return_overflowing_tokens=True`:

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

Hai să testăm acest lucru pe un exemplu înainte de a folosi `Dataset.map()` pentru întreg datasetul:

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

    [128, 49]

Așadar, primul nostru exemplu din setul de antrenare a devenit două features pentru că a fost tokenizat mai mult decât lungimea maximă de tokenuri pe care am specificat-o: prima cu lungimea 128 și a doua cu lungimea 49. Acum trebuie să facem acest lucru pentru toate elementele din 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! Acesta nu a funcționat! De ce? Citind eroarea vom afla motivul: existența unei incompatibilități în lungimea uneia dintre coloane - una cu o lungime de 1,463 și alta de 1,000. Dacă ați privit documentația Dataset.map(), puteți să vă reamintiți că este numărul de sampleuri care sunt oferite funcției pe care noi le facem mapping; aici aceste 1,000 exemplare creează 1,463 features noi, ceea ce duce la un shape error.

Problema este că încercăm să amestecăm două dataseturi cu mărimi diferite: coloanele `drug_dataset` vor avea un număr determinat de exemple (cele 1,000 din eroare), dar `tokenized_dataset` pe care îl construim va fi mai mare (cel cu 1,463 din eroare; el este mai mare decât 1,000 pentru că tokenizăm reviewrile lungi în mai multe exemple folosind `return_overflowing_tokens=True`). Acest lucru nu funcționează pentru un `Dataset`, așadar trebuie să eliminăm sau să modificămm coloanele din datasetul vechi pentru a se potrivi dimensiunea cu cea din noul dataset. Putem face ultima din cele două opțiuni folosind argumentul `remove_columns`:

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

Acum acest lucru funcționează fără erori. Putem verifica că noul dataset are mai multe elemente decât datasetul original prin compararea lungimilor:

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

    (206772, 138514)

Am menționat că putem rezolva problema lungimilor diferite are coloanelor prin schimbarea vechilor coloane la aceeași dimensiune cu cele noi. Pentru aceasta, vom avea nevoie de câmpul `overflow_to_sample_mapping` returnat de tokenizer atunci când setăm `return_overflowing_tokens=True`. El ne oferă un mapping de la un nou feature index la indicele sampleului din care a provenit. Prin intermediul acesta, putem asocia fiecărei key prezentă în datasetul original cu o listă de valori de dimensiune corectă prin repetarea valorilor fiecărui exemplu atâta timp cât produce caracteristici noi:

    def tokenize_and_split(examples):
        result = tokenizer(
            examples["review"],
            truncation=True,
            max_length=128,
            return_overflowing_tokens=True,
        )
        # Extragem maparea între noul și vechiul indice
        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

Putem observa că funcționează cu `Dataset.map()` fără ca noi să avem nevoie să eliminăm coloanele vechi:

    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
        })
    })

Obținem același număr de features training ca și înainte, dar acum am păstrat toate câmpurile vechi. Dacă ai nevoie de ele pentru post-procesare după aplicarea modelului, ar fi util să folosiți această abordare.

Acum ați învățat cum pot fi utilizate 🤗 Datasets pentru preprocesarea datelor prin metode diferite. Deși funcțiile de preprocesare ale 🤗 Datasets vor acoperi majoritatea nevoilor de antrenare a modelului, există momente în care veți avea nevoie să treceți la Pandas pentru accesul la caracteristici mai puternice, cum ar fi `DataFrame.groupby()` sau high-level APIs pentru vizualizare. Din fericire, 🤗 Dataset a fost proiectat astfel încât să fie interoperabil cu biblioteci precum Pandas, NumPy, PyTorch, TensorFlow și JAX. Hai să vedem cum se face acest lucru.

##
De la Dataset s la DataFrame s și înapoi

Pentru a permite conversia între diferite biblioteci, 🤗 Datasets oferă o funcția `Dataset.set_format()`. Această funcție schimbă doar output format al datasetului, deci puteți ușor să treceți la un alt format fără a afecta data format de bază, care este Apache Arrow. Formatarea se face direct. Pentru a demonstra acest lucru, hai să convertim datasetul nostru în Pandas:

    drug_dataset.set_format("pandas")

Acum, atunci când accesăm elementele din dataset, obținem `pandas.DataFrame` în loc de un dicționar:

    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

În continuare, vom crea un `pandas.DataFrame` pentru întregul set de antrenare prin selectarea tuturor elementelor din `drug_dataset["train"]`:

    train_df = drug_dataset["train"][:]

🚨 În spatele scenei, `Dataset.set_format()` schimbă formatul returnat pentru `__getitem__()` dunder method a datasetului. Asta înseamnă că atunci când dorim să creăm un nou obiect ca `train_df` dintr-un `Dataset` în formatul `"pandas"`, trebuie să tăiem întreg datasetul pentru a obține un `pandas.DataFrame`. Puteți verifica voi înşivă că tipul lui `drug_dataset["train"]` este `Dataset`, indiferent de output format.

Acum putem utiliza toate funcționalitățile Pandas pe care le dorim. De exemplu, putem face fancy chaining pentru a calcula distribuția clasei printre intrările `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

Și odată ce suntem gata cu analiza noastră Pandas, putem crea întotdeauna un nou obiect `Dataset` prin utilizarea funcției `Dataset.from_pandas()`:

    from datasets import Dataset

    freq_dataset = Dataset.from_pandas(frequencies)
    freq_dataset

✏️ Încercați! Calculați media ratingului per medicament și salvați rezultatul într-un nou `Dataset`.

Acest lucru completează turul nostru de tehnici de preprocesare disponibile în 🤗 Datasets. Pentru a finisa secțiunea, vom crea un set de validare pentru a pregăti datasetul pentru antrenarea unui clasificator. Înainte de a face asta, noi vom reseta output formatul `drug_dataset` de la `"pandas"` la `“arrow” :

    drug_dataset.reset_format()

##
Crearea unui set de validare

Deși avem deja un set de testare pe care îl putem folosi pentru evaluare, este o bună practică să lăsăm setul de test neschimbat și să creăm un set de validare în timpul developmentului. Odată ce sunteți fericiți cu performanța modelului pe setul de validare, puteți face o verificare finală a setului de test. Acest proces ajută la reducerea riscului ca să vă adaptați prea mult setul de test și să depuneți un model care poate eșua analizând date reale.

🤗 Datasets oferă o funcție `Dataset.train_test_split()` bazată pe funcționalitatea celebră din `scikit-learn`. O vom folosi pentru a împărți setul nostru de antrenare în split-uri de `train` și `validation` (setăm argumentul `seed` pentru reproductabilitate):

    drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
    # Renumește implicit "test" split-ul la "valide"
    drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
    # Adaugă setul de test în `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
        })
    })

Foarte bine, am pregătit acum un dataset care este gata pentru antrenarea unor modele! În Secțiunea 5 vom arăta cum puteți încărca seturile de date în Hugging Face Hub, dar în acest moment hai să punem capăt analizei noastre prin examinarea a câteva modalități de salvare a seturilor de date pe dispozitivele locale.

##
Salvarea unui dataset

Deși 🤗 Datasets va face cache fiecărui dataset și a operațiilor efectuate asupra acestuia, există momente în care veți dori să salvați un dataset pe disc (de exemplu, în cazul în care cache-ul se șterge). După cum vedeți în tabelul de mai jos, 🤗 Datasets oferă trei funcții principale pentru salvarea datelor în formate diferite:

Format de date | Funcție
--- | ---
Arrow | `Dataset.save_to_disk()`
CSV | `Dataset.to_csv()`
JSON | `Dataset.to_json()`

Spre exemplu, hai să salvăm datasetul nostru curățat în formatul Arrow:

    drug_dataset_clean.save_to_disk("drug-reviews")

Acest lucru va crea un folder cu următoarea structură:

    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

unde se poate vedea că fiecare split este asociat cu propriul său tabel `dataset.arrow`, iar unele metadate în `dataset_info.json` și `state.json`. Poți să te gândești la formatul Arrow ca fiind un tabel fancy de coloane și rânduri, optimizat pentru construirea aplicațiilor high-performance care procesează și transportă dataseturi mari.

Odată ce setul de date este salvat, putem încărca-o folosind funcția `load_from_disk()` următoarea:

    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
        })
    })

Pentru formatele CSV și JSON, trebuie să stocați fiecare split într-un fișier separat. Un mod de a face acest lucru estw iterarea asupra cheilor și valorilor obiectului `DatasetDict`:

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

Acesta salvează fiecare split în JSON Lines format, unde fiecare rând din setul de date este stocat ca o singură linie JSON. Aici puteți vedea cum arată primul exemplu:

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

Putem apoi folosi tehnicile de la Secțiunea 2 pentru încărcarea fișierelor JSON:

    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)

Și ăsta este finalul excursiei noastre în lumea manipulării datelor cu 🤗 Datasets! Acum că avem un dataset curat, pregătit pentru antrenarea unui model, aici sunt câteva idei pe care le puteți încerca:

1. Folosiți tehnicile din Capitolul 3 pentru a antrena un classifier care poate prezice starea pacientului pe baza recenziei medicamentului.
2. Folosiți pipelineul `summarization` din Capitolul 1 pentru a genera rezumate ale recenziilor.

În următoarea secțiune, vom vedea cum 🤗 Datasets vă permite să lucrați cu dataseturi mari fără ca laptopul tău să explodeze :)!

< > Update on GitHub