NLP Course documentation

Il est temps de trancher et de découper

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Il est temps de trancher et de découper

Ask a Question

La plupart du temps, les données avec lesquelles vous travaillez ne sont pas parfaitement préparées pour l’entraînements de modèles. Dans cette section, nous allons explorer les différentes fonctionnalités fournies par 🤗 Datasets pour nettoyer vos jeux de données.

Trancher et découper nos données

Semblable à Pandas, 🤗 Datasets fournit plusieurs fonctions pour manipuler le contenu des objets Dataset et DatasetDict. Nous avons déjà rencontré la méthode Dataset.map() dans le chapitre 3 et dans cette section nous allons explorer certaines des autres fonctions à notre disposition.

Pour cet exemple, nous utiliserons le Drug Review Dataset qui est hébergé sur UC Irvine Machine Learning Repository et contenant des avis de patients sur divers médicaments ainsi que la condition traitée et une note de 10 étoiles sur la satisfaction du patient.

Nous devons d’abord télécharger et extraire les données, ce qui peut être fait avec les commandes wget et unzip :

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

Étant donné que TSV n’est qu’une variante de CSV qui utilise des tabulations au lieu de virgules comme séparateurs, nous pouvons charger ces fichiers en utilisant le script de chargement csv et en spécifiant l’argument delimiter dans la fonction load_dataset() comme suit :

from datasets import load_dataset

data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
# \t est le caractère de tabulation en Python
drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")

Une bonne pratique lors de toute sorte d’analyse de données consiste à prélever un petit échantillon aléatoire pour avoir une idée rapide du type de données avec lesquelles vous travaillez. Dans 🤗 Datasets, nous pouvons créer un échantillon aléatoire en enchaînant les fonctions Dataset.shuffle() et Dataset.select() :

drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
# Un coup d'œil sur les premiers exemples
drug_sample[:3]
{'Unnamed: 0': [87571, 178045, 80482],
 'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
 'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'], 
 #['Goutte aiguë', 'ibromyalgie', 'Affections inflammatoires']
 '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!"', 
 # comme la personne précédente l'a mentionné, je suis un fervent partisan de l'aleve, il fonctionne plus rapidement pour ma goutte que les médicaments sur ordonnance que je prends. Je n'ai plus besoin d'aller chez le médecin pour des renouvellements.....Aleve fonctionne !"
  '"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."', 
  # J'ai pris du Cymbalta pendant environ un an et demi pour des douleurs de la fibromyalgie. C'est un excellent analgésique et un antidépresseur, mais les effets secondaires l'ont emporté sur tous les avantages que j'en ai tirés. J'ai eu des problèmes d'agitation, de fatigue constante, de vertiges, de bouche sèche, d'engourdissement, de picotements dans les pieds, et de transpiration horrible. Je suis en train de m'en sevrer maintenant. Je suis passée de 60 mg à 30 mg et maintenant à 15 mg. Je l'arrêterai complètement dans environ une semaine. La douleur de la fibrose revient, mais je préfère la supporter plutôt que les effets secondaires.
  '"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."'], 
  # J'ai pris Mobic pendant plus d'un an sans effets secondaires autres qu'une pression sanguine élevée.  J'avais de fortes douleurs au genou et à la cheville qui ont complètement disparu après avoir pris Mobic. J'ai essayé d'arrêter le médicament mais la douleur est revenue après quelques jours."
 'rating': [9.0, 3.0, 10.0],
 'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'], 
        #['2 septembre 2015', '7 novembre 2011', '5 juin 2013']
 'usefulCount': [36, 13, 128]}

Notez que nous avons corrigé la graine dans Dataset.shuffle() à des fins de reproductibilité. Dataset.select() attend un itérable d’indices, nous avons donc passé range(1000) pour récupérer les 1 000 premiers exemples du jeu de données mélangé. À partir de cet échantillon, nous pouvons déjà voir quelques bizarreries dans notre jeu de données :

  • la colonne Unnamed: 0 ressemble étrangement à un identifiant anonyme pour chaque patient,
  • la colonne condition comprend un mélange d’étiquettes en majuscules et en minuscules,
  • les avis sont de longueur variable et contiennent un mélange de séparateurs de lignes Python (\r\n) ainsi que des codes de caractères HTML comme &\#039;.

Voyons comment nous pouvons utiliser 🤗 Datasets pour traiter chacun de ces problèmes. Pour tester l’hypothèse de l’ID patient pour la colonne Unnamed : 0, nous pouvons utiliser la fonction Dataset.unique() pour vérifier que le nombre d’ID correspond au nombre de lignes dans chaque division :

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

Cela semble confirmer notre hypothèse, alors nettoyons un peu en renommant la colonne Unnamed: 0 en quelque chose d’un peu plus interprétable. Nous pouvons utiliser la fonction DatasetDict.rename_column() pour renommer la colonne sur les deux divisions en une seule fois :

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

✏️ Essayez ! Utilisez la fonction Dataset.unique() pour trouver le nombre de médicaments et de conditions uniques dans les échantillons d’entraînement et de test.

Ensuite, normalisons toutes les étiquettes condition en utilisant Dataset.map(). Comme nous l’avons fait avec la tokenisation dans le chapitre 3, nous pouvons définir une fonction simple qui peut être appliquée sur toutes les lignes de chaque division dans drug_dataset :

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


drug_dataset.map(lowercase_condition)
AttributeError: 'NoneType' object has no attribute 'lower'

Oh non, nous rencontrons un problème avec notre fonction ! À partir de l’erreur, nous pouvons déduire que certaines des entrées de la colonne condition sont None ne pouvant donc pas être mises en minuscules car ce ne sont pas des chaînes. Supprimons ces lignes en utilisant Dataset.filter(), qui fonctionne de manière similaire à Dataset.map() et attend une fonction qui reçoit un seul exemple issu du jeu de données. Au lieu d’écrire une fonction explicite comme :

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

puis exécuter drug_dataset.filter(filter_nones), nous pouvons le faire en une seule ligne en utilisant une fonction lambda. En Python, les fonctions lambda sont de petites fonctions que vous pouvez définir sans les nommer explicitement. Ils prennent la forme générale :

lambda <arguments> : <expression>

lambda est l’un des mots clés spéciaux de Python, <arguments> est une liste/ensemble de valeurs séparées par des virgules qui définissent les entrées de la fonction et <expression> représente les opérations que vous souhaitez exécuter. Par exemple, nous pouvons définir une simple fonction lambda qui met au carré un nombre comme suit :

lambda x : x * x

Pour appliquer cette fonction à une entrée, nous devons l’envelopper ainsi que l’entrée entre parenthèses :

(lambda x: x * x)(3)
9

De même, nous pouvons définir des fonctions lambda avec plusieurs arguments en les séparant par des virgules. Par exemple, nous pouvons calculer l’aire d’un triangle comme suit :

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

Les fonctions lambda sont pratiques lorsque vous souhaitez définir de petites fonctions à usage unique (pour plus d’informations à leur sujet, nous vous recommandons de lire l’excellent tutoriel Real Python d’André Burgaud) . Dans le contexte de la bibliothèque 🤗 Datasets, nous pouvons utiliser des fonctions lambda pour définir des opérations simples de « mappage » et de filtrage. Utilisons cette astuce pour éliminer les entrées None dans notre jeu de données :

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

Avec les entrées None supprimées, nous pouvons normaliser notre colonne condition :

drug_dataset = drug_dataset.map(lowercase_condition)
# Vérification que la mise en minuscule a fonctionné
drug_dataset["train"]["condition"][:3]
['left ventricular dysfunction', 'adhd', 'birth control']

Ça marche ! Maintenant que nous avons nettoyé les étiquettes, examinons le nettoyage des avis eux-mêmes.

Création de nouvelles colonnes

Chaque fois que vous avez affaire à des avis de clients, une bonne pratique consiste à vérifier le nombre de mots dans chaque avis. Une critique peut être un simple mot comme « Génial ! » ou un essai complet avec des milliers de mots. Selon le cas d’usage, vous devrez gérer ces extrêmes différemment. Pour calculer le nombre de mots dans chaque révision, nous utiliserons une heuristique approximative basée sur la division de chaque texte par des espaces.

Définissons une fonction simple qui compte le nombre de mots dans chaque avis :

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

Contrairement à notre fonction lowercase_condition(), compute_review_length() renvoie un dictionnaire dont la clé ne correspond pas à l’un des noms de colonne du jeu de données. Dans ce cas, lorsque compute_review_length() est passé à Dataset.map(), il est appliqué à toutes les lignes du jeu de données pour créer une nouvelle colonne review_length :

drug_dataset = drug_dataset.map(compute_review_length)
# Inspecter le premier exemple d'entraînement
drug_dataset["train"][0]
{'patient_id': 206461,
 'drugName': 'Valsartan',
 'condition': 'left ventricular dysfunction', # dysfonctionnement du ventricule gauche
 'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"', 
           # Il n'a aucun effet secondaire, je le prends en combinaison avec Bystolic 5 mg et de l'huile de poisson.
 'rating': 9.0,
 'date': 'May 20, 2012', # 20 mai 2012
 'usefulCount': 27,
 'review_length': 17}

Comme prévu, nous pouvons voir qu’une colonne review_length a été ajoutée à notre jeu d’entraînement. Nous pouvons trier cette nouvelle colonne avec Dataset.sort() pour voir à quoi ressemblent les valeurs extrêmes :

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'], 
              # contraception, spasme musculaire, douleur.
 'review': ['"Excellent."', '"useless"', '"ok"'], # Excellent, inutile, ok 
 'rating': [10.0, 1.0, 6.0],
 'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'], 
         # 4 novembre 2008, 24 mars 2017, 20 août 2016
 'usefulCount': [5, 2, 10],
 'review_length': [1, 1, 1]}

Comme nous le soupçonnions, certaines critiques ne contiennent qu’un seul mot, ce qui, bien que cela puisse convenir à l’analyse des sentiments, n’est pas informatif si nous voulons prédire la condition.

🙋 Une autre façon d’ajouter de nouvelles colonnes à un jeu de données consiste à utiliser la fonction Dataset.add_column(). Cela vous permet de donner la colonne sous forme de liste Python ou de tableau NumPy et peut être utile dans les situations où Dataset.map() n’est pas bien adapté à votre analyse.

Utilisons la fonction Dataset.filter() pour supprimer les avis contenant moins de 30 mots. De la même manière que nous l’avons fait avec la colonne condition, nous pouvons filtrer les avis très courts en exigeant que les avis aient une longueur supérieure à ce seuil :

drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
print(drug_dataset.num_rows)
{'train': 138514, 'test': 46108}

Comme vous pouvez le constater, cela a supprimé environ 15 % des avis de nos jeux d’entraînement et de test d’origine.

✏️ Essayez ! Utilisez la fonction Dataset.sort() pour inspecter les avis avec le plus grand nombre de mots. Consultez la documentation pour voir quel argument vous devez utiliser pour trier les avis par longueur dans l’ordre décroissant.

La dernière chose à laquelle nous devons faire face est la présence de caractères HTML dans nos avis. Nous pouvons utiliser le module html de Python pour supprimer ces caractères, comme ceci :

import html

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

Nous utilisons Dataset.map() pour démasquer tous les caractères HTML de notre corpus :

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

Comme vous pouvez le voir, la méthode Dataset.map() est très utile pour le traitement des données. Et nous n’avons même pas effleuré la surface de tout ce qu’elle peut faire !

Les superpouvoirs de la méthode map()

La méthode Dataset.map() prend un argument batched qui, s’il est défini sur True, l’amène à envoyer un batch d’exemples à la fonction map en une seule fois (la taille du batch est configurable mais est fixé par défaut à 1 000). Par exemple, la fonction map() précédente qui supprime tout le code HTML prend un peu de temps à s’exécuter (vous pouvez lire le temps pris dans les barres de progression). On peut accélérer cela en traitant plusieurs éléments en même temps à l’aide d’une compréhension de liste.

Lorsque vous spécifiez batched=True, la fonction reçoit un dictionnaire avec les champs du jeu de données mais chaque valeur est maintenant une liste de valeurs et non plus une seule valeur. La valeur retournée par Dataset.map() devrait être la même : un dictionnaire avec les champs que nous voulons mettre à jour ou ajouter à notre jeu de données, et une liste de valeurs. Par exemple, voici une autre façon de supprimer tous les caractères HTML, mais en utilisant batched=True :

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

Si vous exécutez ce code dans un notebook, vous verrez que cette commande s’exécute beaucoup plus rapidement que la précédente. Et ce n’est pas parce que nos critiques ont déjà été scannées au format HTML. Si vous ré-exécutez l’instruction de la section précédente (sans batched=True), cela prendra le même temps qu’avant. En effet, les compréhensions de liste sont généralement plus rapides que l’exécution du même code dans une boucle for et nous gagnons également en performances en accédant à de nombreux éléments en même temps au lieu d’un par un.

L’utilisation de Dataset.map() avec batched=True est essentielle pour les tokenizers rapides que nous rencontrerons dans le chapitre 6 et qui peuvent rapidement tokeniser de grandes listes de textes. Par exemple, pour tokeniser toutes les critiques de médicaments avec un tokenizer rapide nous pouvons utiliser une fonction comme celle-ci :

from transformers import AutoTokenizer

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


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

Comme vous l’avez vu dans le chapitre 3, nous pouvons passer un ou plusieurs exemples au tokenizer. Nous pouvons donc utiliser cette fonction avec ou sans batched=True. Profitons-en pour comparer les performances des différentes options. Dans un notebook, vous pouvez chronométrer une instruction d’une ligne en ajoutant %time avant la ligne de code que vous souhaitez mesurer :

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

Vous pouvez également chronométrer une cellule entière en mettant %%time au début de la cellule. Sur le matériel sur lequel nous avons exécuté cela, cela affichait 10,8 s pour cette instruction (c’est le nombre écrit après “Wall time”).

✏️ Essayez ! Exécutez la même instruction avec et sans batched=True, puis essayez-le avec un tokenizer lent (ajoutez use_fast=False dans la méthode AutoTokenizer.from_pretrained()) afin que vous puissiez voir quels temps vous obtenez sur votre matériel.

Voici les résultats que nous avons obtenus avec et sans batching, avec un tokenizer rapide et un lent :

Options Tokenizer rapide Tokenizer lent
batched=True 10.8s 4min41s
batched=False 59.2s 5min3s

Cela signifie que l’utilisation d’un tokenizer rapide avec l’option batched=True est 30 fois plus rapide que son homologue lent sans batch. C’est vraiment incroyable ! C’est la raison principale pour laquelle les tokenizers rapides sont la valeur par défaut lors de l’utilisation de AutoTokenizer (et pourquoi ils sont appelés « rapides »). Ils sont capables d’atteindre une telle vitesse car en coulisses le code de tokenisation est exécuté en Rust qui est un langage facilitant la parallélisation de l’exécution du code.

La parallélisation est également la raison du gain de vitesse de près de 6 fois obtenue par le tokenizer rapide avec batch. Vous ne pouvez pas paralléliser une seule opération de tokenisation, mais lorsque vous souhaitez tokeniser de nombreux textes en même temps, vous pouvez simplement répartir l’exécution sur plusieurs processus. Chacun responsable de ses propres textes.

Dataset.map() possède aussi ses propres capacités de parallélisation. Comme elles ne sont pas soutenus par Rust, un tokenizer lent ne peut pas rattraper un rapide mais cela peut toujours être utile (surtout si vous utilisez un tokenizer qui n’a pas de version rapide). Pour activer le multitraitement, utilisez l’argument num_proc et spécifiez le nombre de processus à utiliser dans votre appel à 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)

Vous pouvez faire des tests pour déterminer le nombre optimal de processus à utiliser. Dans notre cas 8 semble produire le meilleur gain de vitesse. Voici les chiffres que nous avons obtenus avec et sans multitraitement :

Options Tokenizer rapide Tokenizer lent
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

Ce sont des résultats beaucoup plus raisonnables pour le tokenizer lent mais les performances du tokenizer rapide ont également été considérablement améliorées. Notez, cependant, que ce ne sera pas toujours le cas : pour des valeurs de num_proc autres que 8, nos tests ont montré qu’il était plus rapide d’utiliser batched=True sans cette option. En général, nous ne recommandons pas d’utiliser le multitraitement pour les tokenizers rapides avec batched=True.

Utiliser num_proc pour accélérer votre traitement est généralement une bonne idée tant que la fonction que vous utilisez n’effectue pas déjà une sorte de multitraitement.

Toutes ces fonctionnalités condensées en une seule méthode sont déjà assez étonnantes, mais il y a plus ! Avec Dataset.map() et batched=True vous pouvez modifier le nombre d’éléments dans votre jeu de données. Ceci est très utile dans de nombreuses situations où vous souhaitez créer plusieurs fonctionnalités d’entraînement à partir d’un exemple. Nous devrons le faire dans le cadre du prétraitement de plusieurs des tâches de traitement du langage naturel que nous entreprendrons dans le chapitre 7.

💡 En apprentissage automatique, un exemple est généralement défini comme l’ensemble de features que nous donnons au modèle. Dans certains contextes, ces caractéristiques seront l’ensemble des colonnes d’un Dataset, mais dans d’autres (comme ici et pour la réponse aux questions), plusieurs caractéristiques peuvent être extraites d’un seul exemple et appartenir à une seule colonne.

Voyons comment cela fonctionne ! Ici, nous allons tokeniser nos exemples et les tronquer à une longueur maximale de 128 mais nous demanderons au tokenizer de renvoyer tous les morceaux des textes au lieu du premier. Cela peut être fait avec return_overflowing_tokens=True :

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

Testons cela sur un exemple avant d’utiliser Dataset.map() sur le jeu de données :

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

Notre premier exemple du jeu d’entraînement est devenu deux caractéristiques car il a été segmenté à plus que le nombre maximum de tokens que nous avons spécifié : le premier de longueur 128 et le second de longueur 49. Faisons maintenant cela pour tous les éléments du jeu de données !

tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000

Oh non ! Cela n’a pas fonctionné ! Pourquoi ? L’examen du message d’erreur nous donne un indice : il y a une incompatibilité dans les longueurs de l’une des colonnes. L’une étant de longueur 1 463 et l’autre de longueur 1 000. Si vous avez consulté la documentation de Dataset.map(), vous vous souvenez peut-être qu’il s’agit du nombre d’échantillons passés à la fonction que nous mappons. Ici, ces 1 000 exemples ont donné 1 463 nouvelles caractéristiques, entraînant une erreur de forme.

Le problème est que nous essayons de mélanger deux jeux de données différents de tailles différentes : les colonnes drug_dataset auront un certain nombre d’exemples (les 1 000 dans notre erreur), mais le tokenized_dataset que nous construisons en aura plus (le 1 463 dans le message d’erreur). Cela ne fonctionne pas pour un Dataset, nous devons donc soit supprimer les colonnes de l’ancien jeu de données, soit leur donner la même taille que dans le nouveau jeu de données. Nous pouvons faire la première option avec l’argument remove_columns :

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

Maintenant, cela fonctionne sans erreur. Nous pouvons vérifier que notre nouveau jeu de données contient beaucoup plus d’éléments que le jeu de données d’origine en comparant les longueurs :

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

Nous avons mentionné que nous pouvions également résoudre le problème de longueur non concordante en donnant aux anciennes colonnes la même taille que les nouvelles. Pour ce faire, nous avons besoin du champ overflow_to_sample_mapping que le tokenizer renvoie lorsque nous définissons return_overflowing_tokens=True. Il nous donne une correspondance entre un nouvel index de caractéristique et l’index de l’échantillon dont il est issu. Grâce à cela, nous pouvons associer chaque clé présente dans notre jeu de données d’origine à une liste de valeurs de la bonne taille en répétant les valeurs de chaque exemple autant de fois qu’il génère de nouvelles caractéristiques :

def tokenize_and_split(examples):
    result = tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )
    # Extraire la correspondance entre les nouveaux et les anciens indices
    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

Nous pouvons voir que cela fonctionne avec Dataset.map() sans que nous ayons besoin de supprimer les anciennes colonnes :

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

Nous obtenons le même nombre de caractéristiques d’entraînement qu’auparavant, mais ici nous avons conservé tous les anciens champs. Si vous en avez besoin pour un post-traitement après l’application de votre modèle, vous pouvez utiliser cette approche.

Vous avez maintenant vu comment 🤗 Datasets peut être utilisé pour prétraiter un jeu de données de différentes manières. Bien que les fonctions de traitement de 🤗 Datasets couvrent la plupart de vos besoins, il peut arriver que vous deviez passer à Pandas pour accéder à des fonctionnalités plus puissantes, telles que DataFrame.groupby() ou des API de haut niveau pour la visualisation. Heureusement, 🤗 Datasets est conçu pour être interopérable avec des bibliothèques telles que Pandas, NumPy, PyTorch, TensorFlow et JAX. Voyons comment cela fonctionne.

De Dataset à DataFrame et vice versa

Pour permettre la conversion entre diverses bibliothèques tierces, 🤗 Datasets fournit une fonction Dataset.set_format(). Cette fonction ne modifie que le format de sortie du jeu de données. Vous pouvez donc facilement passer à un autre format sans affecter le format de données sous-jacent, qui est Apache Arrow. Le formatage se fait sur place. Pour démontrer, convertissons notre jeu de données vers Pandas :

drug_dataset.set_format("pandas")

Maintenant, lorsque nous accédons aux éléments du jeu de données, nous obtenons un pandas.DataFrame au lieu d’un dictionnaire :

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

Créons un pandas.DataFrame pour l’ensemble d’entraînement en sélectionnant tous les éléments de drug_dataset["train"] :

train_df = drug_dataset["train"][:]

🚨 Sous le capot, Dataset.set_format() change le format de retour pour la méthode __getitem__(). Cela signifie que lorsque nous voulons créer un nouvel objet comme train_df à partir d’un Dataset au format "pandas", nous devons découper tout le jeu de données pour obtenir un pandas.DataFrame. Vous pouvez vérifier par vous-même que le type de drug_dataset["train"] est Dataset, quel que soit le format de sortie.

De là, nous pouvons utiliser toutes les fonctionnalités Pandas que nous voulons. Par exemple, nous pouvons faire un chaînage sophistiqué pour calculer la distribution de classe parmi les entrées 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

Et une fois que nous avons terminé notre analyse Pandas, nous pouvons toujours créer un nouvel objet Dataset en utilisant la fonction Dataset.from_pandas() comme suit :

from datasets import Dataset

freq_dataset = Dataset.from_pandas(frequencies)
freq_dataset
Dataset({
    features: ['condition', 'frequency'],
    num_rows: 819
})

✏️ Essayez ! Calculez la note moyenne par médicament et stockez le résultat dans un nouveau jeu de données.

Ceci conclut notre visite des différentes techniques de prétraitement disponibles dans 🤗 Datasets. Pour compléter la section, créons un ensemble de validation pour préparer le jeu de données à l’entraînement d’un classifieur. Avant cela, nous allons réinitialiser le format de sortie de drug_dataset de "pandas" à "arrow" :

drug_dataset.reset_format()

Création d’un ensemble de validation

Bien que nous ayons un jeu de test que nous pourrions utiliser pour l’évaluation, il est recommandé de ne pas toucher au jeu de test et de créer un jeu de validation séparé pendant le développement. Une fois que vous êtes satisfait des performances de vos modèles sur l’ensemble de validation, vous pouvez effectuer une dernière vérification d’intégrité sur l’ensemble test. Ce processus permet d’atténuer le risque de surentraînement sur le jeu de test et de déployer un modèle qui échoue sur des données du monde réel.

🤗 Datasets fournit une fonction Dataset.train_test_split() basée sur la célèbre fonctionnalité de scikit-learn. Utilisons-la pour diviser notre ensemble d’entraînement train et validation (nous définissons l’argument seed pour la reproductibilité) :

drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
# Renommer la division par défaut "test" en "validation"
drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
# Ajoutez le jeu "test" à notre `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
    })
})

Génial, nous avons maintenant préparé un jeu de données prêt pour l’entraînement de certains modèles ! Dans la section 5, nous vous montrerons comment télécharger des jeux de données sur le Hub. Mais pour l’instant, terminons notre analyse en examinant quelques façons d’enregistrer des jeux de données sur votre ordinateur local.

Enregistrer un jeu de données

Bien que 🤗 Datasets mette en cache chaque jeu de données téléchargé et les opérations qui y sont effectuées, il y a des moments où vous voudrez enregistrer un jeu de données sur le disque (par exemple, au cas où le cache serait supprimé). Comme indiqué dans le tableau ci-dessous, 🤗 Datasets fournit trois fonctions principales pour enregistrer votre jeu de données dans différents formats :

Format de données Fonction
Arrow Dataset.save_to_disk()
CSV Dataset.to_csv()
JSON Dataset.to_json()

Par exemple, enregistrons notre jeu de données nettoyé au format Arrow :

drug_dataset_clean.save_to_disk("drug-reviews")

Cela créera un répertoire avec la structure suivante :

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

où nous pouvons voir que chaque division est associée à sa propre table dataset.arrow et à certaines métadonnées dans dataset_info.json et state.json. Vous pouvez considérer le format Arrow comme un tableau sophistiqué de colonnes et de lignes optimisé pour la création d’applications hautes performances qui traitent et transportent de grands ensembles de données.

Une fois le jeu de données enregistré, nous pouvons le charger en utilisant la fonction load_from_disk() comme suit :

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

Pour les formats CSV et JSON, nous devons stocker chaque fractionnement dans un fichier séparé. Pour ce faire, vous pouvez parcourir les clés et les valeurs de l’objet DatasetDict :

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

Cela enregistre chaque fractionnement au format JSON Lines, où chaque ligne du jeu de données est stockée sous la forme d’une seule ligne de JSON. Voici à quoi ressemble le premier exemple :

!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}
                                                                                  # Il semble que je ressente les effets secondaires habituels de LEXAPRO : insomnie, baisse de la libido, somnolence pendant la journée. Je le prends le soir parce que mon médecin m'a dit de le prendre le soir s'il me fatiguait. J'ai supposé que ce serait le cas et j'ai commencé à le prendre la nuit. Rêves étranges, certains agréables. On m'a diagnostiqué une fibromyalgie. Il semble que ce médicament aide à soulager la douleur. J'ai eu de l'anxiété et de la dépression dans ma famille, et j'ai essayé plusieurs autres médicaments qui n'ont pas fonctionné. Cela ne fait que deux semaines que je prends ce médicament, mais je me sens plus positif dans mon esprit et je veux accomplir davantage dans ma vie. J'espère que les effets secondaires vont s'estomper, cela vaut la peine de s'y tenir d'après les réponses des autres. C'est un excellent médicament.

Nous pouvons ensuite utiliser les techniques de section 2 pour charger les fichiers JSON comme suit :

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)

Et c’est tout pour notre excursion dans la manipulation des données avec 🤗 Datasets ! Maintenant que nous disposons d’un ensemble de données nettoyé pour entraîner un modèle, voici quelques idées que vous pouvez essayer :

  1. Utilisez les techniques du chapitre 3 pour entraîner un classifieur capable de prédire l’état du patient en fonction de l’examen du médicament.
  2. Utilisez le pipeline summarization du chapitre 1 pour générer des résumés des révisions.

Ensuite, nous verrons comment 🤗 Datasets peut vous permettre de travailler avec d’énormes jeux de données sans faire exploser votre ordinateur portable !