Datasets documentation
Utiliser Datasets avec TensorFlow
Utiliser Datasets avec TensorFlow
Ce document est une introduction rapide à l'utilisation de datasets avec TensorFlow, avec un accent particulier sur la manière d'obtenir des objets tf.Tensor à partir de nos datasets, et sur la façon de diffuser les données des objets Dataset de Hugging Face vers les méthodes Keras
comme model.fit().
Format du jeu de données
Par défaut, les jeux de données renvoient des objets Python classiques : des entiers, des flottants, des chaînes de caractères, des listes, etc.
Pour obtenir des tenseurs TensorFlow à la place, vous pouvez définir le format du jeu de données sur tf :
>>> from datasets import Dataset
>>> data = [[1, 2],[3, 4]]
>>> ds = Dataset.from_dict({"data": data})
>>> ds = ds.with_format("tf")
>>> ds[0]
{'data': <tf.Tensor: shape=(2,), dtype=int64, numpy=array([1, 2])>}
>>> ds[:2]
{'data': <tf.Tensor: shape=(2, 2), dtype=int64, numpy=
array([[1, 2],
[3, 4]])>}Un objet Dataset est un conteneur d'un tableau Arrow, qui permet des lectures rapides depuis les tableaux du jeu de données vers des tenseurs TensorFlow.
Cela peut être utile pour convertir votre jeu de données en dictionnaire d'objets Tensor, ou pour écrire un générateur afin de charger des échantillons TF à partir de celui-ci. Si vous souhaitez convertir l'ensemble du jeu de données en Tensor, il suffit d'interroger le jeu de données complet :
>>> ds[:]
{'data': <tf.Tensor: shape=(2, 2), dtype=int64, numpy=
array([[1, 2],
[3, 4]])>}Tableaux N-dimensionnels
Si votre jeu de données est constitué de tableaux N-dimensionnels, vous constaterez qu’ils sont considérés par défaut comme le même tenseur si la forme est fixe :
>>> from datasets import Dataset
>>> data = [[[1, 2],[3, 4]],[[5, 6],[7, 8]]] # fixed shape
>>> ds = Dataset.from_dict({"data": data})
>>> ds = ds.with_format("tf")
>>> ds[0]
{'data': <tf.Tensor: shape=(2, 2), dtype=int64, numpy=
array([[1, 2],
[3, 4]])>}Sinon, un jeu de données au format TensorFlow renvoie un RaggedTensor au lieu d’un tenseur unique :
>>> from datasets import Dataset
>>> data = [[[1, 2],[3]],[[4, 5, 6],[7, 8]]] # varying shape
>>> ds = Dataset.from_dict({"data": data})
>>> ds = ds.with_format("torch")
>>> ds[0]
{'data': <tf.RaggedTensor [[1, 2], [3]]>}Cependant, cette logique nécessite souvent des comparaisons de formes et des copies de données lentes.
Pour l’éviter, vous devez utiliser explicitement le type de fonctionnalité Array et spécifier la forme de vos tenseurs :
>>> from datasets import Dataset, Features, Array2D
>>> data = [[[1, 2],[3, 4]],[[5, 6],[7, 8]]]
>>> features = Features({"data": Array2D(shape=(2, 2), dtype='int32')})
>>> ds = Dataset.from_dict({"data": data}, features=features)
>>> ds = ds.with_format("tf")
>>> ds[0]
{'data': <tf.Tensor: shape=(2, 2), dtype=int64, numpy=
array([[1, 2],
[3, 4]])>}
>>> ds[:2]
{'data': <tf.Tensor: shape=(2, 2, 2), dtype=int64, numpy=
array([[[1, 2],
[3, 4]],
[[5, 6],
[7, 8]]])>}Autres types de fonctionnalités
Les données ClassLabel sont correctement converties en tenseurs :
>>> from datasets import Dataset, Features, ClassLabel
>>> labels = [0, 0, 1]
>>> features = Features({"label": ClassLabel(names=["negative", "positive"])})
>>> ds = Dataset.from_dict({"label": labels}, features=features)
>>> ds = ds.with_format("tf")
>>> ds[:3]
{'label': <tf.Tensor: shape=(3,), dtype=int64, numpy=array([0, 0, 1])>}Les chaînes de caractères et les objets binaires sont également pris en charge :
>>> from datasets import Dataset, Features
>>> text = ["foo", "bar"]
>>> data = [0, 1]
>>> ds = Dataset.from_dict({"text": text, "data": data})
>>> ds = ds.with_format("tf")
>>> ds[:2]
{'text': <tf.Tensor: shape=(2,), dtype=string, numpy=array([b'foo', b'bar'], dtype=object)>,
'data': <tf.Tensor: shape=(2,), dtype=int64, numpy=array([0, 1])>}Vous pouvez également formater explicitement certaines colonnes et laisser les autres colonnes non formatées :
>>> ds = ds.with_format("tf", columns=["data"], output_all_columns=True)
>>> ds[:2]
{'data': <tf.Tensor: shape=(2,), dtype=int64, numpy=array([0, 1])>,
'text': ['foo', 'bar']}Les objets de type chaîne et binaire ne sont pas modifiés, car PyTorch ne prend en charge que les nombres.
Les types de fonctionnalités Image et Audio sont également pris en charge.
Pour utiliser le type de fonctionnalité Image, vous devez installer l'extra
visionviapip install datasets[vision].
>>> from datasets import Dataset, Features, Audio, Image
>>> images = ["path/to/image.png"] * 10
>>> features = Features({"image": Image()})
>>> ds = Dataset.from_dict({"image": images}, features=features)
>>> ds = ds.with_format("tf")
>>> ds[0]
{'image': <tf.Tensor: shape=(512, 512, 4), dtype=uint8, numpy=
array([[[255, 215, 106, 255],
[255, 215, 106, 255],
...,
[255, 255, 255, 255],
[255, 255, 255, 255]]], dtype=uint8)>}
>>> ds[:2]
{'image': <tf.Tensor: shape=(2, 512, 512, 4), dtype=uint8, numpy=
array([[[[255, 215, 106, 255],
[255, 215, 106, 255],
...,
[255, 255, 255, 255],
[255, 255, 255, 255]]]], dtype=uint8)>}Pour utiliser le type de fonctionnalité Audio, vous devez installer l'extra
audioviapip install datasets[audio].
>>> from datasets import Dataset, Features, Audio, Image
>>> audio = ["path/to/audio.wav"] * 10
>>> features = Features({"audio": Audio()})
>>> ds = Dataset.from_dict({"audio": audio}, features=features)
>>> ds = ds.with_format("tf")
>>> ds[0]["audio"]["array"]
<tf.Tensor: shape=(202311,), dtype=float32, numpy=
array([ 6.1035156e-05, 1.5258789e-05, 1.6784668e-04, ...,
-1.5258789e-05, -1.5258789e-05, 1.5258789e-05], dtype=float32)>
>>> ds[0]["audio"]["sampling_rate"]
<tf.Tensor: shape=(), dtype=int32, numpy=44100>Chargement des données
Bien que vous puissiez charger des échantillons et des lots individuels en indexant simplement votre jeu de données, cela ne fonctionnera pas si vous souhaitez utiliser des méthodes Keras comme fit() et predict(). Vous pourriez écrire une fonction générateur qui mélange et charge des lots depuis votre jeu de données et fit() dessus, mais cela semble être beaucoup de travail inutile. À la place, si vous souhaitez diffuser des données depuis votre jeu de données à la volée, nous recommandons de convertir votre jeu de données en tf.data.Dataset à l’aide de la méthode to_tf_dataset().
La classe tf.data.Dataset couvre un large éventail de cas d'utilisation - elle est souvent créée à partir de tenseurs en mémoire, ou en utilisant une fonction de chargement pour lire des fichiers sur disque
ou sur un stockage externe. Le jeu de données peut être transformé arbitrairement avec la méthode map(), ou des méthodes comme batch() et shuffle() peuvent être utilisées pour créer un jeu de données prêt pour l'entraînement. Ces méthodes ne modifient pas les données
stockées de quelque manière que ce soit - à la place, elles construisent un graphe de pipeline de données qui sera exécuté lorsque le jeu de données sera parcouru,
généralement pendant l'entraînement ou l'inférence du modèle. Cela diffère de la méthode map() des objets Dataset de Hugging Face,
qui exécute la fonction map immédiatement et enregistre les nouvelles colonnes ou les colonnes modifiées.
Étant donné que l’ensemble du pipeline de prétraitement des données peut être compilé dans un tf.data.Dataset, cette approche permet un chargement et un entraînement des données massivement parallèles et asynchrones. Toutefois, l’exigence de compilation en graphe peut constituer une limitation, en particulier pour les tokenizers Hugging Face, qui ne sont généralement pas (encore !) compilables dans le cadre d’un graphe TF. Par conséquent, nous recommandons habituellement de prétraiter le jeu de données en tant que jeu de données Hugging Face, où des fonctions Python arbitraires peuvent être utilisées, puis de le convertir en tf.data.Dataset par la suite à l’aide de to_tf_dataset() pour obtenir un jeu de données par lots prêt pour l’entraînement. Pour voir des exemples de cette approche, veuillez consulter les exemples ou les carnets pour transformers.
Utilisation de to_tf_dataset()
L’utilisation de to_tf_dataset() est simple. Une fois votre jeu de données prétraité et prêt, il suffit de l’appeler ainsi :
>>> from datasets import Dataset
>>> data = {"inputs": [[1, 2],[3, 4]], "labels": [0, 1]}
>>> ds = Dataset.from_dict(data)
>>> tf_ds = ds.to_tf_dataset(
columns=["inputs"],
label_cols=["labels"],
batch_size=2,
shuffle=True
)L'objet tf_ds retourné ici est désormais entièrement prêt pour l'entraînement et peut être transmis directement à model.fit(). Notez que vous avez défini la taille du lot lors de la création du jeu de données, il n'est donc pas nécessaire de la spécifier lors de l'appel de fit() :
>>> model.fit(tf_ds, epochs=2)Pour une description complète des arguments, veuillez consulter la documentation de to_tf_dataset(). Dans de nombreux cas,
vous devrez également ajouter un collate_fn à votre appel. Il s’agit d’une fonction qui prend plusieurs éléments du jeu de données
et les combine en un seul lot. Lorsque tous les éléments ont la même longueur, le collateur par défaut intégré
suffira, mais pour des tâches plus complexes, un collateur personnalisé peut s’avérer nécessaire. En particulier, de nombreuses tâches comportent des échantillons
de longueurs de séquence variables, ce qui nécessitera un data collator capable de rembourrer correctement les lots. Vous pouvez en trouver des exemples
dans les examples et les notebooks NLP de transformers, où les longueurs de séquence variables sont très courantes.
Si vous constatez que le chargement avec to_tf_dataset est lent, vous pouvez également utiliser l'argument num_workers. Cela lance plusieurs sous-processus pour charger les données en parallèle. Cette fonctionnalité est récente et encore quelque peu expérimentale - veuillez ouvrir un ticket si vous rencontrez des bugs lors de son utilisation !
Quand utiliser to_tf_dataset
Le lecteur attentif aura peut-être remarqué à ce stade que nous avons proposé deux approches pour atteindre le même objectif - si vous souhaitez passer votre jeu de données à un modèle TensorFlow, vous pouvez soit convertir le jeu de données en Tensor ou dict de Tensors à l'aide de .with_format('tf'), soit convertir le jeu de données en tf.data.Dataset avec to_tf_dataset(). L'une ou l'autre de ces options peut être transmise à model.fit(), alors laquelle devriez-vous choisir ?
L’élément clé à retenir est que lorsque vous convertissez l’ensemble du jeu de données en Tensors, celui-ci devient statique et entièrement chargé en RAM. C’est simple et pratique, mais si l’une des conditions suivantes s’applique, vous devriez probablement utiliser to_tf_dataset() à la place :
- Votre jeu de données est trop volumineux pour tenir en RAM.
to_tf_dataset()ne transmet qu'un seul lot à la fois, ce qui permet de traiter même des jeux de données très volumineux avec cette méthode. - Vous souhaitez appliquer des transformations aléatoires à l’aide de
dataset.with_transform()ou decollate_fn. C’est courant dans plusieurs modalités, comme les augmentations d’images lors de l’entraînement de modèles de vision, ou le masquage aléatoire lors de l’entraînement de modèles de langage masqués. L’utilisation deto_tf_dataset()appliquera ces transformations au moment où un lot est chargé, ce qui signifie que les mêmes échantillons recevront des augmentations différentes à chaque fois qu’ils sont chargés. C’est généralement ce que vous souhaitez. - Vos données ont une dimension variable, comme les textes d’entrée en NLP qui sont composés d’un nombre variable de jetons. Lorsque vous créez un lot avec des échantillons de dimension variable, la solution standard consiste à compléter les échantillons les plus courts jusqu’à la longueur du plus long. Lorsque vous transmettez des échantillons d’un jeu de données avec
to_tf_dataset, vous pouvez appliquer ce remplissage à chaque lot via votrecollate_fn. Cependant, si vous souhaitez convertir un tel jeu de données enTensordenses, vous devrez compléter les échantillons jusqu’à la longueur de l’échantillon le plus long de l’ensemble du jeu de données ! Cela peut entraîner une quantité énorme de remplissage, ce qui gaspille de la mémoire et réduit la vitesse de votre modèle.
Limites et restrictions
Actuellement, to_tf_dataset() renvoie toujours un jeu de données par lots - nous ajouterons bientôt la prise en charge des jeux de données non lotis !