Datasets documentation

Processus

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Processus

🤗 Datasets fournit de nombreux outils pour modifier la structure et le contenu d'un jeu de données. Ces outils sont essentiels pour assainir un jeu de données, créer des colonnes supplémentaires, convertir entre les fonctionnalités et les formats, et bien plus encore.

Ce guide vous montre comment :

  • Réordonner les lignes et diviser le jeu de données.
  • Renommer et supprimer des colonnes, ainsi que d'autres opérations courantes sur les colonnes.
  • Appliquer des fonctions de traitement à chaque exemple d'un jeu de données.
  • Concaténer les jeux de données.
  • Appliquer une transformation de formatage personnalisée.
  • Enregistrer et exporter les jeux de données traités.

Pour plus de détails spécifiques au traitement d'autres modalités de jeu de données, consultez le guide de traitement des jeux de données audio, le guide de traitement des jeux de données image ou le guide de traitement des jeux de données texte.

Les exemples de ce guide utilisent le jeu de données MRPC, mais n’hésitez pas à charger le jeu de données de votre choix et à suivre le guide !

>>> from datasets import load_dataset
>>> dataset = load_dataset("nyu-mll/glue", "mrpc", split="train")

Toutes les méthodes de traitement de ce guide renvoient un nouvel objet Dataset. La modification n’est pas effectuée en place. Faites attention à ne pas écraser votre jeu de données précédent !

Trier, mélanger, sélectionner, diviser et fractionner

Plusieurs fonctions permettent de réorganiser la structure d'un jeu de données. Ces fonctions sont utiles pour sélectionner uniquement les lignes souhaitées, créer des ensembles d'entraînement et de test, et découper les très grands jeux de données en morceaux plus petits.

Trier

Utilisez sort() pour trier les valeurs d'une colonne selon leurs valeurs numériques. La colonne fournie doit être compatible avec NumPy.

>>> dataset["label"][:10]
[1, 0, 1, 0, 1, 1, 0, 1, 0, 0]
>>> sorted_dataset = dataset.sort("label")
>>> sorted_dataset["label"][:10]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> sorted_dataset["label"][-10:]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

En interne, cela crée une liste d’indices triée selon les valeurs de la colonne. Cette correspondance d’indices est ensuite utilisée pour accéder aux bonnes lignes de la table Arrow sous-jacente.

Mélanger

La fonction shuffle() réorganise aléatoirement les valeurs des colonnes. Vous pouvez spécifier le paramètre generator dans cette fonction pour utiliser un autre numpy.random.Generator si vous souhaitez un meilleur contrôle sur l'algorithme utilisé pour mélanger le jeu de données.

>>> shuffled_dataset = sorted_dataset.shuffle(seed=42)
>>> shuffled_dataset["label"][:10]
[1, 1, 1, 0, 1, 1, 1, 1, 1, 0]

Shuffling prend la liste d'indices [0:len(my_dataset)] et la mélange pour créer un mappage d'indices. Cependant, dès que votre Dataset possède un mappage d'indices, la vitesse peut devenir 10 fois plus lente. Cela est dû à une étape supplémentaire pour obtenir l'index de la ligne à lire à l'aide du mappage d'indices, et surtout, vous ne lisez plus des blocs de données contigus. Pour restaurer la vitesse, vous devez réécrire l'ensemble du jeu de données sur votre disque à l'aide de Dataset.flatten_indices(), ce qui supprime le mappage d'indices. Alternativement, vous pouvez passer à un IterableDataset et exploiter son mélange approximatif rapide IterableDataset.shuffle() :

>>> iterable_dataset = dataset.to_iterable_dataset(num_shards=128)
>>> shuffled_iterable_dataset = iterable_dataset.shuffle(seed=42, buffer_size=1000)

Sélectionner et filtrer

Il existe deux options pour filtrer les lignes d'un jeu de données : select() et filter().

  • select() renvoie des lignes selon une liste d’indices :
>>> small_dataset = dataset.select([0, 10, 20, 30, 40, 50])
>>> len(small_dataset)
6
  • filter() renvoie les lignes qui correspondent à une condition spécifiée :
>>> start_with_ar = dataset.filter(lambda example: example["sentence1"].startswith("Ar"))
>>> len(start_with_ar)
6
>>> start_with_ar["sentence1"]
['Around 0335 GMT , Tab shares were up 19 cents , or 4.4 % , at A {@html "4.56,havingearliersetarecordhighofA 4.56 , having earlier set a record high of A "} 4.57 .',
'Arison said Mann may have been one of the pioneers of the world music movement and he had a deep love of Brazilian music .',
'Arts helped coach the youth on an eighth-grade football team at Lombardi Middle School in Green Bay .',
'Around 9 : 00 a.m. EDT ( 1300 GMT ) , the euro was at $ 1.1566 against the dollar , up 0.07 percent on the day .',
"Arguing that the case was an isolated example , Canada has threatened a trade backlash if Tokyo 's ban is not justified on scientific grounds .",
'Artists are worried the plan would harm those who need help most - performers who have a difficult time lining up shows .'
]

filter() peut également filtrer par indices si vous définissez with_indices=True :

>>> even_dataset = dataset.filter(lambda example, idx: idx % 2 == 0, with_indices=True)
>>> len(even_dataset)
1834
>>> len(dataset) / 2
1834.0

À moins que la liste des indices à conserver ne soit contiguë, ces méthodes créent également une correspondance d'indices en interne.

Séparer

La fonction train_test_split() crée des ensembles d’entraînement et de test si votre jeu de données n’en contient pas déjà. Cela vous permet d’ajuster les proportions relatives ou le nombre absolu d’échantillons dans chaque ensemble. Dans l’exemple ci-dessous, utilisez le paramètre test_size pour créer un ensemble de test représentant 10 % du jeu de données d’origine :

>>> dataset.train_test_split(test_size=0.1)
{'train': Dataset(schema: {'sentence1': 'string', 'sentence2': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 3301),
'test': Dataset(schema: {'sentence1': 'string', 'sentence2': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 367)}
>>> 0.1 * len(dataset)
366.8

Les splits sont mélangés par défaut, mais vous pouvez définir shuffle=False pour empêcher le mélange.

Shard

🤗 Datasets prend en charge le sharding pour diviser un très grand jeu de données en un nombre prédéfini de morceaux. Spécifiez le paramètre num_shards dans shard() pour déterminer le nombre de shards en lesquels diviser le jeu de données. Vous devez également fournir le shard que vous souhaitez renvoyer avec le paramètre index.

Par exemple, le jeu de données stanfordnlp/imdb contient 25000 exemples :

>>> from datasets import load_dataset
>>> dataset = load_dataset("stanfordnlp/imdb", split="train")
>>> print(dataset)
Dataset({
    features: ['text', 'label'],
    num_rows: 25000
})

Après avoir découpé le jeu de données en quatre morceaux, la première tranche ne contiendra que 6250 exemples :

>>> dataset.shard(num_shards=4, index=0)
Dataset({
    features: ['text', 'label'],
    num_rows: 6250
})
>>> print(25000/4)
6250.0

Renommer, supprimer, convertir et aplatir

Les fonctions suivantes vous permettent de modifier les colonnes d'un jeu de données. Ces fonctions sont utiles pour renommer ou supprimer des colonnes, changer les colonnes pour un nouvel ensemble de caractéristiques, et aplatir les structures de colonnes imbriquées.

Renommer

Utilisez rename_column() lorsque vous devez renommer une colonne de votre jeu de données. Les fonctionnalités associées à la colonne d’origine sont en réalité déplacées sous le nouveau nom de colonne, plutôt que de simplement remplacer la colonne d’origine sur place.

Fournissez à rename_column() le nom de la colonne d’origine et le nouveau nom de la colonne :

>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'label', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.rename_column("sentence1", "sentenceA")
>>> dataset = dataset.rename_column("sentence2", "sentenceB")
>>> dataset
Dataset({
    features: ['sentenceA', 'sentenceB', 'label', 'idx'],
    num_rows: 3668
})

Supprimer

Lorsque vous devez supprimer une ou plusieurs colonnes, fournissez le nom de la colonne à supprimer à la fonction remove_columns(). Pour supprimer plusieurs colonnes, fournissez une liste de noms de colonnes :

>>> dataset = dataset.remove_columns("label")
>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.remove_columns(["sentence1", "sentence2"])
>>> dataset
Dataset({
    features: ['idx'],
    num_rows: 3668
})

À l’inverse, select_columns() sélectionne une ou plusieurs colonnes à conserver et supprime les autres. Cette fonction accepte soit un nom de colonne, soit une liste de noms de colonnes :

>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'label', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.select_columns(['sentence1', 'sentence2', 'idx'])
>>> dataset
Dataset({
    features: ['sentence1', 'sentence2', 'idx'],
    num_rows: 3668
})
>>> dataset = dataset.select_columns('idx')
>>> dataset
Dataset({
    features: ['idx'],
    num_rows: 3668
})

Cast

La fonction cast() transforme le type de fonctionnalité d'une ou de plusieurs colonnes. Cette fonction accepte vos nouvelles Features comme argument. L'exemple ci-dessous montre comment modifier les fonctionnalités ClassLabel et Value :

>>> dataset.features
{'sentence1': Value('string'),
'sentence2': Value('string'),
'label': ClassLabel(names=['not_equivalent', 'equivalent']),
'idx': Value('int32')}

>>> from datasets import ClassLabel, Value
>>> new_features = dataset.features.copy()
>>> new_features["label"] = ClassLabel(names=["negative", "positive"])
>>> new_features["idx"] = Value("int64")
>>> dataset = dataset.cast(new_features)
>>> dataset.features
{'sentence1': Value('string'),
'sentence2': Value('string'),
'label': ClassLabel(names=['negative', 'positive']),
'idx': Value('int64')}

Le casting ne fonctionne que si le type de fonctionnalité d’origine et le nouveau type de fonctionnalité sont compatibles. Par exemple, vous pouvez caster une colonne avec le type de fonctionnalité Value("int32") en Value("bool") si la colonne d’origine ne contient que des uns et des zéros.

Utilisez la fonction cast_column() pour changer le type de fonctionnalité d’une seule colonne. Passez le nom de la colonne et son nouveau type de fonctionnalité en arguments :

>>> dataset.features
{'audio': Audio(sampling_rate=44100, mono=True)}

>>> dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))
>>> dataset.features
{'audio': Audio(sampling_rate=16000, mono=True)}

Aplatir

Parfois, une colonne peut être une structure imbriquée de plusieurs types. Jetez un œil à la structure imbriquée ci-dessous issue du jeu de données SQuAD :

>>> from datasets import load_dataset
>>> dataset = load_dataset("rajpurkar/squad", split="train")
>>> dataset.features
{'id': Value('string'),
 'title': Value('string'),
 'context': Value('string'),
 'question': Value('string'),
 'answers': {'text': List(Value('string')),
  'answer_start': List(Value('int32'))}}

Le champ answers contient deux sous-champs : text et answer_start. Utilisez la fonction flatten() pour extraire les sous-champs dans des colonnes distinctes :

>>> flat_dataset = dataset.flatten()
>>> flat_dataset
Dataset({
    features: ['id', 'title', 'context', 'question', 'answers.text', 'answers.answer_start'],
 num_rows: 87599
})

Remarquez que les sous-champs sont désormais des colonnes indépendantes : answers.text et answers.answer_start.

Carte

Certaines des applications les plus puissantes de 🤗 Datasets proviennent de l'utilisation de la fonction map(). L'objectif principal de map() est d'accélérer les fonctions de traitement. Elle permet d'appliquer une fonction de traitement à chaque exemple d'un jeu de données, de manière indépendante ou par lots. Cette fonction peut même créer de nouvelles lignes et colonnes.

Dans l'exemple suivant, préfixez chaque valeur sentence1 du jeu de données par 'My sentence: '.

Commencez par créer une fonction qui ajoute 'My sentence: ' au début de chaque phrase. La fonction doit accepter et renvoyer un dict :

>>> def add_prefix(example):
...     example["sentence1"] = 'My sentence: ' + example["sentence1"]
...     return example

Utilisez maintenant map() pour appliquer la fonction add_prefix à l'ensemble du jeu de données :

>>> updated_dataset = small_dataset.map(add_prefix)
>>> updated_dataset["sentence1"][:5]
['My sentence: Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
"My sentence: Yucaipa owned Dominick 's before selling the chain to Safeway in 1998 for $ 2.5 billion .",
'My sentence: They had published an advertisement on the Internet on June 10 , offering the cargo for sale , he added .',
'My sentence: Around 0335 GMT , Tab shares were up 19 cents , or 4.4 % , at A {@html "4.56,havingearliersetarecordhighofA 4.56 , having earlier set a record high of A "} 4.57 .',
]

Prenons un autre exemple, mais cette fois-ci, vous supprimerez une colonne avec map(). Lorsque vous supprimez une colonne, elle n’est retirée qu’après que l’exemple a été transmis à la fonction mappée. Cela permet à la fonction mappée d’utiliser le contenu des colonnes avant leur suppression.

Spécifiez la colonne à supprimer avec le paramètre remove_columns dans map() :

>>> updated_dataset = dataset.map(lambda example: {"new_sentence": example["sentence1"]}, remove_columns=["sentence1"])
>>> updated_dataset.column_names
['sentence2', 'label', 'idx', 'new_sentence']

🤗 Datasets dispose également d’une fonction remove_columns() plus rapide, car elle ne copie pas les données des colonnes restantes.

Vous pouvez également utiliser map() avec des indices si vous définissez with_indices=True. L’exemple ci-dessous ajoute l’indice au début de chaque phrase :

>>> updated_dataset = dataset.map(lambda example, idx: {"sentence2": f"{idx}: " + example["sentence2"]}, with_indices=True)
>>> updated_dataset["sentence2"][:5]
['0: Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .',
 "1: Yucaipa bought Dominick 's in 1995 for {@html "693millionandsoldittoSafewayfor 693 million and sold it to Safeway for "} 1.8 billion in 1998 .",
 "2: On June 10 , the ship 's owners had published an advertisement on the Internet , offering the explosives for sale .",
 '3: Tab shares jumped 20 cents , or 4.6 % , to set a record closing high at A $ 4.57 .',
 '4: PG & E Corp. shares jumped {@html "1.63or8percentto 1.63 or 8 percent to "} 21.03 on the New York Stock Exchange on Friday .'
]

Traitement parallèle

Le multiprocessing accélère considérablement le traitement en parallélisant les processus sur le CPU. Définissez le paramètre num_proc dans map() pour fixer le nombre de processus à utiliser :

>>> updated_dataset = dataset.map(lambda example, idx: {"sentence2": f"{idx}: " + example["sentence2"]}, with_indices=True, num_proc=4)

La méthode map() fonctionne également avec le rang du processus si vous définissez with_rank=True. Cela est analogue au paramètre with_indices. Le paramètre with_rank de la fonction mappée est placé après le paramètre index si celui-ci est déjà présent.

>>> import torch
>>> from multiprocess import set_start_method
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
>>> from datasets import load_dataset
>>>
>>> # Get an example dataset
>>> dataset = load_dataset("fka/awesome-chatgpt-prompts", split="train")
>>>
>>> # Get an example model and its tokenizer
>>> model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen1.5-0.5B-Chat").eval()
>>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen1.5-0.5B-Chat")
>>>
>>> def gpu_computation(batch, rank):
...     # Move the model on the right GPU if it's not there already
...     device = f"cuda:{(rank or 0) % torch.cuda.device_count()}"
...     model.to(device)
...
...     # Your big GPU call goes here, for example:
...     chats = [[
...         {"role": "system", "content": "You are a helpful assistant."},
...         {"role": "user", "content": prompt}
...     ] for prompt in batch["prompt"]]
...     texts = [tokenizer.apply_chat_template(
...         chat,
...         tokenize=False,
...         add_generation_prompt=True
...     ) for chat in chats]
...     model_inputs = tokenizer(texts, padding=True, return_tensors="pt").to(device)
...     with torch.no_grad():
...         outputs = model.generate(**model_inputs, max_new_tokens=512)
...     batch["output"] = tokenizer.batch_decode(outputs, skip_special_tokens=True)
...     return batch
>>>
>>> if __name__ == "__main__":
...     set_start_method("spawn")
...     updated_dataset = dataset.map(
...         gpu_computation,
...         batched=True,
...         batch_size=16,
...         with_rank=True,
...         num_proc=torch.cuda.device_count(),  # one process per GPU
...     )

Le cas d’usage principal de rank est de paralléliser le calcul sur plusieurs GPU. Cela nécessite de définir multiprocess.set_start_method("spawn"). À défaut, vous recevrez l’erreur CUDA suivante :

RuntimeError: Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, you must use the 'spawn' start method.

Traitement par lots

La fonction map() prend en charge le traitement par lots d'exemples. Pour opérer sur des lots, définissez batched=True. La taille de lot par défaut est de 1000, mais vous pouvez l'ajuster avec le paramètre batch_size. Le traitement par lots permet des applications intéressantes, telles que la division de longues phrases en segments plus courts et l'augmentation de données.

Découper les exemples longs

Lorsque les exemples sont trop longs, vous pouvez vouloir les diviser en plusieurs morceaux plus petits. Commencez par créer une fonction qui :

  1. Divise le champ sentence1 en segments de 50 caractères.

  2. Empile tous les morceaux ensemble pour créer le nouveau jeu de données.

>>> def chunk_examples(examples):
...     chunks = []
...     for sentence in examples["sentence1"]:
...         chunks += [sentence[i:i + 50] for i in range(0, len(sentence), 50)]
...     return {"chunks": chunks}

Appliquez la fonction avec map() :

>>> chunked_dataset = dataset.map(chunk_examples, batched=True, remove_columns=dataset.column_names)
>>> chunked_dataset[:10]
{'chunks': ['Amrozi accused his brother , whom he called " the ',
            'witness " , of deliberately distorting his evidenc',
            'e .',
            "Yucaipa owned Dominick 's before selling the chain",
            ' to Safeway in 1998 for $ 2.5 billion .',
            'They had published an advertisement on the Interne',
            't on June 10 , offering the cargo for sale , he ad',
            'ded .',
            'Around 0335 GMT , Tab shares were up 19 cents , or',
            ' 4.4 % , at A $ 4.56 , having earlier set a record']}

Remarquez que les phrases sont désormais découpées en segments plus courts et que le jeu de données contient davantage de lignes.

>>> dataset
Dataset({
 features: ['sentence1', 'sentence2', 'label', 'idx'],
 num_rows: 3668
})
>>> chunked_dataset
Dataset({
    features: ['chunks'],
    num_rows: 10470
})

Augmentation des données

La fonction map() peut également être utilisée pour l’augmentation de données. L’exemple suivant génère des mots supplémentaires pour un jeton masqué dans une phrase.

Charger et utiliser le modèle RoBERTa dans le FillMaskPipeline de 🤗 Transformers :

>>> from random import randint
>>> from transformers import pipeline

>>> fillmask = pipeline("fill-mask", model="roberta-base")
>>> mask_token = fillmask.tokenizer.mask_token
>>> smaller_dataset = dataset.filter(lambda e, i: i<100, with_indices=True)

Créez une fonction pour sélectionner aléatoirement un mot à masquer dans la phrase. La fonction doit également renvoyer la phrase d’origine et les deux meilleurs remplacements générés par RoBERTA.

>>> def augment_data(examples):
...     outputs = []
...     for sentence in examples["sentence1"]:
...         words = sentence.split(' ')
...         K = randint(1, len(words)-1)
...         masked_sentence = " ".join(words[:K]  + [mask_token] + words[K+1:])
...         predictions = fillmask(masked_sentence)
...         augmented_sequences = [predictions[i]["sequence"] for i in range(3)]
...         outputs += [sentence] + augmented_sequences
...
...     return {"data": outputs}

Utilisez map() pour appliquer la fonction à l'ensemble du jeu de données :

>>> augmented_dataset = smaller_dataset.map(augment_data, batched=True, remove_columns=dataset.column_names, batch_size=8)
>>> augmented_dataset[:9]["data"]
['Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
 'Amrozi accused his brother, whom he called " the witness ", of deliberately withholding his evidence.',
 'Amrozi accused his brother, whom he called " the witness ", of deliberately suppressing his evidence.',
 'Amrozi accused his brother, whom he called " the witness ", of deliberately destroying his evidence.',
 "Yucaipa owned Dominick 's before selling the chain to Safeway in 1998 for $ 2.5 billion .",
 'Yucaipa owned Dominick Stores before selling the chain to Safeway in 1998 for $ 2.5 billion.',
 "Yucaipa owned Dominick's before selling the chain to Safeway in 1998 for $ 2.5 billion.",
 'Yucaipa owned Dominick Pizza before selling the chain to Safeway in 1998 for $ 2.5 billion.'
]

Pour chaque phrase originale, RoBERTa a augmenté un mot aléatoire avec trois alternatives. Le mot original distorting est complété par withholding, suppressing et destroying.

Traitement asynchrone

Les fonctions asynchrones sont utiles pour appeler des points de terminaison d’API en parallèle, par exemple pour télécharger du contenu comme des images ou appeler un point de terminaison de modèle.

Vous pouvez définir une fonction asynchrone à l'aide des mots-clés async et await. Voici un exemple de fonction pour appeler un modèle de chat de Hugging Face :

>>> import aiohttp
>>> import asyncio
>>> from huggingface_hub import get_token
>>> sem = asyncio.Semaphore(20)  # max number of simultaneous queries
>>> async def query_model(model, prompt):
...     api_url = f"https://huggingface.co/static-proxy/api-inference.huggingface.co/models/{model}/v1/chat/completions"
...     headers = {"Authorization": f"Bearer {get_token()}", "Content-Type": "application/json"}
...     json = {"messages": [{"role": "user", "content": prompt}], "max_tokens": 20, "seed": 42}
...     async with sem, aiohttp.ClientSession() as session, session.post(api_url, headers=headers, json=json) as response:
...         output = await response.json()
...         return {"Output": output["choices"][0]["message"]["content"]}

Asynchronous functions run in parallel, which accelerates the process a lot. The same code takes a lot more time if it’s run sequentially, because it does nothing while waiting for the model response. It is generally recommended to use async / await when you function has to wait for a response from an API for example, or if it downloads data and it can take some time.

Notez la présence d'un Semaphore : il définit le nombre maximal de requêtes pouvant s'exécuter en parallèle. Il est recommandé d'utiliser un Semaphore lors de l'appel d'API afin d'éviter les erreurs de limite de débit.

Utilisons-le pour appeler le modèle microsoft/Phi-3-mini-4k-instruct et lui demander de renvoyer le sujet principal de chaque problème de mathématiques du jeu de données Maxwell-Jia/AIME_2024 :

>>> from datasets import load_dataset
>>> ds = load_dataset("Maxwell-Jia/AIME_2024", split="train")
>>> model = "microsoft/Phi-3-mini-4k-instruct"
>>> prompt = 'What is this text mainly about ? Here is the text:\n\n```\n{Problem}\n```\n\nReply using one or two words max, e.g. "The main topic is Linear Algebra".'
>>> async def get_topic(example):
...     return await query_model(model, prompt.format(Problem=example['Problem']))
>>> ds = ds.map(get_topic)
>>> ds[0]
{'ID': '2024-II-4',
 'Problem': 'Let {@html "x,yx,y"} and {@html "zz"} be positive real numbers that...',
 'Solution': 'Denote $\\log_2(x) = a$, $\\log_2(y) = b$, and...,
 'Answer': 33,
 'Output': 'The main topic is Logarithms.'}

Ici, Dataset.map() exécute de nombreuses fonctions get_topic de manière asynchrone afin de ne pas avoir à attendre chaque réponse du modèle, ce qui prendrait beaucoup de temps si l’opération était effectuée séquentiellement.

Par défaut, Dataset.map() exécute jusqu’à mille fonctions map en parallèle. N’oubliez donc pas de définir le nombre maximal d’appels API pouvant s’exécuter en parallèle à l’aide d’un Semaphore, sinon le modèle pourrait renvoyer des erreurs de limitation de débit ou être surchargé. Pour les cas d’utilisation avancés, vous pouvez modifier le nombre maximal de requêtes en parallèle dans datasets.config.

Traiter plusieurs splits

De nombreux jeux de données possèdent des sous-ensembles qui peuvent être traités simultanément avec DatasetDict.map(). Par exemple, tokenisez le champ sentence1 des sous-ensembles train et test en :

>>> from datasets import load_dataset

# load all the splits
>>> dataset = load_dataset('nyu-mll/glue', 'mrpc')
>>> encoded_dataset = dataset.map(lambda examples: tokenizer(examples["sentence1"]), batched=True)
>>> encoded_dataset["train"][0]
{'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .',
'label': 1,
'idx': 0,
'input_ids': [  101,  7277,  2180,  5303,  4806,  1117,  1711,   117,  2292, 1119,  1270,   107,  1103,  7737,   107,   117,  1104,  9938, 4267, 12223, 21811,  1117,  2554,   119,   102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}

Utilisation distribuée

Lorsque vous utilisez map() dans un contexte distribué, vous devez également utiliser torch.distributed.barrier. Cela garantit que le processus principal effectue le mappage, tandis que les autres processus chargent les résultats, évitant ainsi un travail en double.

L'exemple suivant montre comment utiliser torch.distributed.barrier pour synchroniser les processus :

>>> from datasets import Dataset
>>> import torch.distributed

>>> dataset1 = Dataset.from_dict({"a": [0, 1, 2]})

>>> if training_args.local_rank > 0:
...     print("Waiting for main process to perform the mapping")
...     torch.distributed.barrier()

>>> dataset2 = dataset1.map(lambda x: {"a": x["a"] + 1})

>>> if training_args.local_rank == 0:
...     print("Loading results from main process")
...     torch.distributed.barrier()

Lot

La méthode batch() permet de regrouper les échantillons du jeu de données en lots. Cela est particulièrement utile lorsque vous souhaitez créer des lots de données pour l'entraînement ou l'évaluation, en particulier lors du travail avec des modèles d'apprentissage profond.

Voici un exemple d’utilisation de la méthode batch() :

>>> from datasets import load_dataset
>>> dataset = load_dataset("cornell-movie-review-data/rotten_tomatoes", split="train")
>>> batched_dataset = dataset.batch(batch_size=4)
>>> batched_dataset[0]
{'text': ['the rock is destined to be the 21st century\'s new " conan " and that he\'s going to make a splash even greater than arnold schwarzenegger , jean-claud van damme or steven segal .',
        'the gorgeously elaborate continuation of " the lord of the rings " trilogy is so huge that a column of words cannot adequately describe co-writer/director peter jackson\'s expanded vision of j . r . r . tolkien\'s middle-earth .',
        'effective but too-tepid biopic',
        'if you sometimes like to go to the movies to have fun , wasabi is a good place to start .'],
'label': [1, 1, 1, 1]}

La méthode batch() accepte les paramètres suivants :

  • batch_size (int) : Le nombre d'échantillons dans chaque lot.
  • drop_last_batch (bool, par défaut False) : Indique s'il faut supprimer le dernier lot incomplet si la taille du jeu de données n'est pas divisible par la taille du lot.
  • num_proc (int, facultatif, par défaut None) : Le nombre de processus à utiliser pour le multiprocessing. Si None, aucun multiprocessing n'est utilisé. Cela peut accélérer considérablement le batch processing pour les grands jeux de données.

Notez que Dataset.batch() renvoie un nouveau Dataset où chaque élément est un lot de plusieurs échantillons issus du jeu de données d’origine. Si vous souhaitez traiter les données par lots, vous devriez utiliser directement un map() par lots, qui applique une fonction aux lots mais dont le jeu de données de sortie n’est pas groupé en lots.

Concaténer

Des jeux de données distincts peuvent être concaténés s'ils partagent les mêmes types de colonnes. Concaténez les jeux de données avec concatenate_datasets() :

>>> from datasets import concatenate_datasets, load_dataset

>>> stories = load_dataset("ajibawa-2023/General-Stories-Collection", split="train")
>>> stories = stories.select_columns(["text"])  # only keep the 'text' column
>>> wiki = load_dataset("wikimedia/wikipedia", "20231101.en", split="train")
>>> wiki = wiki.select_columns(["text"])  # only keep the 'text' column

>>> assert stories.features.type == wiki.features.type
>>> bert_dataset = concatenate_datasets([stories, wiki])

Vous pouvez également concaténer deux jeux de données horizontalement en définissant axis=1, à condition que les jeux de données aient le même nombre de lignes :

>>> from datasets import Dataset
>>> stories_ids = Dataset.from_dict({"ids": list(range(len(stories)))})
>>> stories_with_ids = concatenate_datasets([stories, stories_ids], axis=1)

Interleaver

Vous pouvez également mélanger plusieurs ensembles de données en prenant des exemples alternés de chacun pour créer un nouvel ensemble de données. Cette opération est connue sous le nom de interleaving et est activée par la fonction interleave_datasets(). Les fonctions interleave_datasets() et concatenate_datasets() fonctionnent avec les objets Dataset et IterableDataset classiques. Reportez-vous au guide Stream pour un exemple de la façon d'intercaler des objets IterableDataset.

Vous pouvez définir des probabilités d’échantillonnage pour chacun des jeux de données d’origine afin de spécifier comment intercaler les jeux de données. Dans ce cas, le nouveau jeu de données est construit en récupérant des exemples un par un à partir d’un jeu de données aléatoire jusqu’à ce que l’un des jeux de données épuise ses échantillons.

>>> from datasets import Dataset, interleave_datasets
>>> seed = 42
>>> probabilities = [0.3, 0.5, 0.2]
>>> d1 = Dataset.from_dict({"a": [0, 1, 2]})
>>> d2 = Dataset.from_dict({"a": [10, 11, 12, 13]})
>>> d3 = Dataset.from_dict({"a": [20, 21, 22]})
>>> dataset = interleave_datasets([d1, d2, d3], probabilities=probabilities, seed=seed)
>>> dataset["a"]
[10, 11, 20, 12, 0, 21, 13]

Vous pouvez également spécifier stopping_strategy. La stratégie par défaut, first_exhausted, est une stratégie de sous-échantillonnage, c'est-à-dire que la construction du jeu de données est arrêtée dès qu'un des jeux de données épuise ses échantillons. Vous pouvez spécifier stopping_strategy=all_exhausted pour exécuter une stratégie de sur-échantillonnage. Dans ce cas, la construction du jeu de données est arrêtée dès que chaque échantillon de chaque jeu de données a été ajouté au moins une fois. En pratique, cela signifie qu'un jeu de données épuisé reviendra au début de ce jeu de données jusqu'à ce que le critère d'arrêt soit atteint. Notez que si aucune probabilité d'échantillonnage n'est spécifiée, le nouveau jeu de données aura max_length_datasets*nb_dataset samples. Il existe également stopping_strategy=all_exhausted_without_replacement pour s'assurer que chaque échantillon est vu exactement une fois.

>>> d1 = Dataset.from_dict({"a": [0, 1, 2]})
>>> d2 = Dataset.from_dict({"a": [10, 11, 12, 13]})
>>> d3 = Dataset.from_dict({"a": [20, 21, 22]})
>>> dataset = interleave_datasets([d1, d2, d3], stopping_strategy="all_exhausted")
>>> dataset["a"]
[0, 10, 20, 1, 11, 21, 2, 12, 22, 0, 13, 20]

Format

La fonction with_format() modifie le format d’une colonne pour la rendre compatible avec certains formats de données courants. Spécifiez la sortie souhaitée dans le paramètre type. Vous pouvez également choisir les colonnes à formater à l’aide de columns=. Le formatage est appliqué à la volée.

Par exemple, créez des tenseurs PyTorch en définissant type="torch" :

>>> dataset = dataset.with_format(type="torch")

La fonction set_format() modifie également le format d’une colonne, mais elle s’exécute en place :

>>> dataset.set_format(type="torch")

Si vous devez réinitialiser le jeu de données à son format d'origine, définissez le format sur None (ou utilisez reset_format()) :

>>> dataset.format
{'type': 'torch', 'format_kwargs': {}, 'columns': [...], 'output_all_columns': False}
>>> dataset = dataset.with_format(None)
>>> dataset.format
{'type': None, 'format_kwargs': {}, 'columns': [...], 'output_all_columns': False}

Formats de tenseurs

Plusieurs formats de tenseurs ou de tableaux sont pris en charge. Il est généralement recommandé d’utiliser ces formats plutôt que de convertir manuellement les sorties d’un jeu de données en tenseurs ou en tableaux, afin d’éviter des copies de données inutiles et d’accélérer le chargement des données.

Voici la liste des formats de tenseurs ou de tableaux pris en charge :

Consultez le guide Using Datasets with TensorFlow pour plus de détails sur la création efficace d'un jeu de données TensorFlow.

Lorsqu’un jeu de données est formaté au format tenseur ou tableau, toutes les données sont formatées sous forme de tenseurs ou de tableaux (à l’exception des types non pris en charge, comme les chaînes de caractères, par exemple pour PyTorch) :

>>> ds = Dataset.from_dict({"text": ["foo", "bar"], "tokens": [[0, 1, 2], [3, 4, 5]]})
>>> ds = ds.with_format("torch")
>>> ds[0]
{'text': 'foo', 'tokens': tensor([0, 1, 2])}
>>> ds[:2]
{'text': ['foo', 'bar'],
 'tokens': tensor([[0, 1, 2],
         [3, 4, 5]])}

Formats tabulaires

Vous pouvez utiliser un format de type dataframes ou tables pour optimiser le chargement et le traitement des données, car ils offrent généralement des opérations sans copie et des transformations écrites dans des langages de bas niveau.

Voici la liste des formats de dataframes ou de tables pris en charge :

Lorsqu’un jeu de données est formaté sous forme de dataframe ou de tableau, chaque ligne du jeu de données ou chaque lot de lignes est formaté sous forme de dataframe ou de tableau, et les colonnes du jeu de données sont formatées sous forme de série ou de tableau :

>>> ds = Dataset.from_dict({"text": ["foo", "bar"], "label": [0, 1]})
>>> ds = ds.with_format("pandas")
>>> ds[:2]
  text  label
0  foo      0
1  bar      1

Ces formats permettent d'itérer plus rapidement sur les données en évitant les copies, et facilitent également un traitement plus rapide des données dans map() ou filter() :

>>> ds = ds.map(lambda df: df.assign(upper_text=df.text.str.upper()), batched=True)
>>> ds[:2]
  text  label upper_text
0  foo      0        FOO
1  bar      1        BAR

Transformation de format personnalisé

La fonction with_transform() applique une transformation de formatage personnalisée à la volée. Cette fonction remplace tout format précédemment spécifié. Par exemple, vous pouvez utiliser cette fonction pour tokeniser et mettre en place des jetons à la volée. La tokenisation n'est appliquée que lorsque les exemples sont accédés :

>>> from transformers import AutoTokenizer

>>> tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
>>> def encode(batch):
...     return tokenizer(batch["sentence1"], batch["sentence2"], padding="longest", truncation=True, max_length=512, return_tensors="pt")
>>> dataset = dataset.with_transform(encode)
>>> dataset.format
{'type': 'custom', 'format_kwargs': {'transform': <function __main__.encode(batch)>}, 'columns': ['idx', 'label', 'sentence1', 'sentence2'], 'output_all_columns': False}

Il existe également set_transform() qui fait la même chose mais s'exécute en place.

Vous pouvez également utiliser la fonction with_transform() pour un décodage personnalisé sur Features.

L'exemple ci-dessous utilise le package pydub comme alternative au décodage par torchcodec :

>>> import numpy as np
>>> from pydub import AudioSegment

>>> audio_dataset_amr = Dataset.from_dict({"audio": ["audio_samples/audio.amr"]})

>>> def decode_audio_with_pydub(batch, sampling_rate=16_000):
...     def pydub_decode_file(audio_path):
...         sound = AudioSegment.from_file(audio_path)
...         if sound.frame_rate != sampling_rate:
...             sound = sound.set_frame_rate(sampling_rate)
...         channel_sounds = sound.split_to_mono()
...         samples = [s.get_array_of_samples() for s in channel_sounds]
...         fp_arr = np.array(samples).T.astype(np.float32)
...         fp_arr /= np.iinfo(samples[0].typecode).max
...         return fp_arr
...
...     batch["audio"] = [pydub_decode_file(audio_path) for audio_path in batch["audio"]]
...     return batch

>>> audio_dataset_amr.set_transform(decode_audio_with_pydub)

Enregistrer

Une fois votre jeu de données prêt, vous pouvez l'enregistrer au format Parquet en tant que Hugging Face Dataset et le réutiliser plus tard avec load_dataset().

Enregistrez votre jeu de données en fournissant le nom du dépôt de jeu de données sur Hugging Face auquel vous souhaitez l’enregistrer à push_to_hub() :

encoded_dataset.push_to_hub("username/my_dataset")

Vous pouvez utiliser plusieurs processus pour le téléverser en parallèle. C'est particulièrement utile si vous souhaitez accélérer le processus :

dataset.push_to_hub("username/my_dataset", num_proc=8)

Utilisez la fonction load_dataset() pour recharger le jeu de données (en mode streaming ou non) :

from datasets import load_dataset
reloaded_dataset = load_dataset("username/my_dataset", streaming=True)

Alternativement, vous pouvez l’enregistrer localement au format Arrow sur le disque. Par rapport à Parquet, Arrow est non compressé, ce qui le rend beaucoup plus rapide à recharger, ce qui est idéal pour un usage local sur disque et un cache éphémère. Mais comme il est plus volumineux et contient moins de métadonnées, il est plus lent à téléverser/télécharger/interroger que Parquet et moins adapté au stockage à long terme.

Utilisez les fonctions save_to_disk() et load_from_disk() pour recharger le jeu de données depuis votre disque :

>>> encoded_dataset.save_to_disk("path/of/my/dataset/directory")
>>> # later
>>> from datasets import load_from_disk
>>> reloaded_dataset = load_from_disk("path/of/my/dataset/directory")

Export

🤗 Datasets prend en charge l'exportation, vous permettant ainsi de travailler avec votre jeu de données dans d'autres applications. Le tableau suivant présente les formats de fichiers actuellement pris en charge pour l'exportation :

Type de fichierMéthode d'export
CSVDataset.to_csv()
JSONDataset.to_json()
ParquetDataset.to_parquet()
SQLDataset.to_sql()
Objet Python en mémoireDataset.to_pandas(), Dataset.to_polars() or Dataset.to_dict()

Par exemple, exportez votre jeu de données dans un fichier CSV comme suit :

>>> encoded_dataset.to_csv("path/of/my/dataset.csv")

Utilisez un chemin hf:// pour exporter vers un dépôt de Dataset ou un Storage Bucket sur Hugging Face :

>>> encoded_dataset.to_csv("hf://datasets/username/dataset_name/path/of/my/dataset.csv")
>>> encoded_dataset.to_csv("hf://buckets/username/raw_data_bucket/path/of/my/dataset.csv")
Update on GitHub