Avant-propos

Cet article est une traduction en français de celui que j’ai écris en anglais sur Hugging Face.



Introduction

Ces dernières semaines, je me suis intéressé à la tâche Reconnaissance d’Entités Nommées (NER abrégée en NER d’après l’anglais Named Entity Recognition) appliquée au français. Et vous savez quoi ? J’ai rencontré un gros problème : des fuites, des fuites partout.
Me demandant s’il s’agissait d’un phénomène propre au français ou à la tâche de la NER, j’ai décidé de télécharger plusieurs centaines de jeux de données du Hub d’Hugging Face afin d’évaluer leur qualité.
Et vous n’allez pas aimer ce que vous allez lire ci-dessous.

image/png



Méthodologie

En janvier 2024, il y a plus de 100 000 jeux de données sur le Hub d’Hugging Face, ce qui rend difficile de tous les évaluer. Ainsi, dans ma petite expérience, pour obtenir des résultats rapidement (je n’y ai consacré que 20 heures), j’ai défini un cadre limité. A savoir, je me suis concentré sur les jeux de données « canoniques », c’est-à-dire les jeux de données n’appartenant pas à une organisation sur le Hub (c’est en train de changer !), parmi eux, ceux ayant moins de 1M de lignes afin de les télécharger rapidement et de les analyser, et enfin je me suis concentré sur le texte (donc pas d’images ou d’audios).

Parmi ces jeux de données, j’ai ensuite exclu ceux qui nécessitaient un téléchargement manuel des données à partir d’une source externe autre que le Hub, ceux qui se trouvaient derrière un portail, et enfin les jeux de données sans échantillon de test (cela n’aurait pas de sens de calculer les fuites dans ce cas).

Au final, un total de 596 jeux de données a été analysé.


Ci-dessous, j’utilise le jeu de données conll2003 pour illustrer la façon dont je détermine si une fuite est présente ou non dans un jeu de données.

dataset = load_dataset("conll2003")

def concatenate_columns(example):
    return {'concatenated_column': str(example['tokens'])}
dataset = dataset.map(concatenate_columns)

train_inputs = dataset["train"]["concatenated_column"]
val_inputs = dataset["validation"]["concatenated_column"]
test_inputs = dataset["test"]["concatenated_column"]

leakage_train = set(train_inputs).intersection(set(test_inputs))
leakage_validation = set(val_inputs).intersection(set(test_inputs))

print("Fuites entre l'échantillon d'entraînement et l'échantillon de test :",len(leakage_train))
print("Fuites entre l'échantillon de validation et l'échantillon de test :",len(leakage_validation))
print("Lignes dupliquéés dans l'échantillon d'entraînement :", len(train_inputs) - len(set(train_inputs)))
print("Lignes dupliquéés dans l'échantillon de validation :", len(val_inputs) - len(set(val_inputs)))
print("Lignes dupliquéés dans l'échantillon de test :", len(test_inputs) - len(set(test_inputs)))

Code qui renvoie :

Fuites entre l'échantillon d'entraînement et l'échantillon de test : 78
Fuites entre l'échantillon de validation et l'échantillon de test : 25
Lignes dupliquéés dans l'échantillon d'entraînement : 1350
Lignes dupliquéés dans l'échantillon de validation : 180
Lignes dupliquéés dans l'échantillon de test : 269


Ici, nous pouvons voir qu’en plus de calculer les fuites entre l’échantillon d’entraînement et l’échantillon de test, ainsi que l’échantillon de validation et l’échantillon de test, je calcule également le nombre de lignes dupliquées dans chacun des échantillons.
Je n’ai pas calculé les fuites entre l’échantillon d’entraînement et celui de validation, il pourrait s’agir d’un manque.

En pratique, je n’affice pas le résultat mais le stocke directement dans un dataframe, que vous pouvez consulter plus bas dans l’article. Je calcule et stocke également le pourcentage de données de l’échantillon de test qui sont biaisées, c’est-à-dire (fuites entre l’échantillon d’entraînement et celui de test + fuites entre l’échantillon d’entraînement et celui de test + lignes dupliquées dans l’échantillon de test) / la taille de l’échantillon de test.
Dans le contexte de Conll2003, cela donne (78+25+269)/3453×100 = 10,77 %.
Il est clair qu’il s’agit d’un jeu de données qui ne devrait pas être utilisé comme benchmark à moins d’être corrigé.

Notons que je reproduis ensuite exactement la même chose, mais au lieu d’analyser uniquement l’entrée textuelle qui sera fournie au modèle, je regarde également les étiquettes :

dataset = load_dataset("conll2003")

def concatenate_columns(example):
    return {'concatenated_column': str(example['tokens'])+" "+str(example['ner_tags'])}
dataset = dataset.map(concatenate_columns)

train_inputs = dataset["train"]["concatenated_column"]
val_inputs = dataset["validation"]["concatenated_column"]
test_inputs = dataset["test"]["concatenated_column"]

leakage_train = set(train_inputs).intersection(set(test_inputs))
leakage_validation = set(val_inputs).intersection(set(test_inputs))

print("Fuites entre l'échantillon d'entraînement et l'échantillon de test :",len(leakage_train))
print("Fuites entre l'échantillon de validation et l'échantillon de test :",len(leakage_validation))
print("Lignes dupliquéés dans l'échantillon d'entraînement :", len(train_inputs) - len(set(train_inputs)))
print("Lignes dupliquéés dans l'échantillon de validation :", len(val_inputs) - len(set(val_inputs)))
print("Lignes dupliquéés dans l'échantillon de test :", len(test_inputs) - len(set(test_inputs)))

Code qui renvoie :

Fuites entre l'échantillon d'entraînement et l'échantillon de test : 73
Fuites entre l'échantillon de validation et l'échantillon de test : 23
Lignes dupliquéés dans l'échantillon d'entraînement : 1348
Lignes dupliquéés dans l'échantillon de validation : 179
Lignes dupliquéés dans l'échantillon de test : 266

Vous pouvez constater que les chiffres varient légèrement par rapport au cas précédent. Je sauvegarde également ces chiffres dans notre dataframe final. Ce qui m’intéresse le plus ici, c’est de calculer la différence entre les deux cas, car cela permettra de déterminer s’il y a des fuites ou des duplications, mais aussi des problèmes d’annotation dans les données.
Dans le contexte de Conll2003, cela donne abs(78-73+25-23) = 8.

Le processus décrit ci-dessus est ensuite appliqué à tous les jeux de données considérés.


Résultats

Présentation des données

Tous les résultats ont été centralisés dans un jeu de données que vous pouvez trouver sur le mon profil Hugging Face.


Les colonnes doivent être interprétées comme suit :

  • dataset_name : le nom du jeu de données analysé. Si une virgule est présente dans le nom, cela signifie qu’il contient plusieurs sous-ensembles. Par exemple, glue,sst2 signifie que c’est le sous-ensemble sst2 du jeu de données glue qui est analysé. Si des parenthèses sont présentes dans le nom, cela signifie que nous n’analysons que l’étiquette qui se trouve entre les parenthèses. En effet, un jeu de données peut contenir plusieurs colonnes qui peuvent être utilisées comme étiquettes (potentiellement pour plusieurs tâches différentes). Par exemple, si nous prenons l’exemple de conll2003, il contient les colonnes ner_tags et pos_tags, qui sont toutes deux des tâches de classification de tokens, mais l’une pour la NER et l’autre du POS. Ainsi, les résultats du code ci-dessus seront trouvés dans la ligne conll2003(ner_tags) pour la différencier de conll2003(pos_tags).
  • text_leaks_train_wrt_test : le nombre de fuites entre l’échantillon d’entraînement et l’échantillon de test, en ne considérant que l’entrée textuelle qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 78.
  • text_leaks_valididation_wrt_test : le nombre de fuites entre l’échantillon de validation et l’échantillon de test, en ne considérant que l’entrée textuelle qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 25.
  • text_duplication_train : le nombre de données dupliquées dans l’échantillon d’entraînement en ne considérant que l’entrée textuelle qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 1350.
  • text_duplication_valididation : le nombre de données dupliquées dans l’échantillon de validation en ne considérant que l’entrée textuelle qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 180.
  • text_duplication_test : le nombre de données dupliquées dans l’échantillon de test en ne considérant que l’entrée textuelle qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 269.
  • text_test_biased : le pourcentage de l’échantillon de test qui est biaisé, c’est-à-dire la somme des valeurs de text_leaks_train_wrt_test, text_leaks_valid_wrt_test et text_duplication_test, divisée par la longueur de l’échantillon de test. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 10,049%.
  • text_and_label_leaks_train_wrt_test : le nombre de fuites entre l’échantillon d’entraînement et l’échantillon de test, en considérant la concaténation de l’entrée textuelle et de l’étiquette qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 73.
  • text_and_label_leaks_valididation_wrt_test : le nombre de fuites entre l’échantillon de validation et l’échantillon de test, en considérant la concaténation de l’entrée textuelle et de l’étiquette qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 23.
  • text_and_label_duplication_train : le nombre de données dupliquées dans l’échantillon d’entraînement en considérant la concaténation de l’entrée textuelle et de l’étiquette qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 1348.
  • text_and_label_duplication_valididation : le nombre de données dupliquées dans l’échantillon de validation en considérant la concaténation de l’entrée textuelle et de l’étiquette qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 179.
  • text_and_label_duplication_test : le nombre de données dupliquées dans l’échantillon de test en considérant la concaténation de l’entrée textuelle et de l’étiquette qui sera donnée au modèle. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 179.
  • text_and_label_test_biased : le pourcentage de l’échantillon de test qui est biaisé, c’est-à-dire la somme des valeurs de text_and_label_leaks_train_wrt_test, text_and_label_leaks_valid_wrt_test et text_and_label_duplication_test, divisée par la longueur de l’échantillon de test. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 9,818%.
  • difference_annotation_in_train_and_valididation_splits : la différence d’annotation entre les échantillons d’entraînement text_leaks_train_wrt_test et text_and_label_leaks_train_wrt_test, de même pour l’échantillon de la valdiation. Dans le cas de conll2003(ner_tags), la valeur de la colonne est 8.

Enfin, veuillez noter que si une cellule contient le terme « NR », cela signifie que les calculs ne sont pas pertinents (en fait, simplement impossibles). Le cas le plus courant est celui d’un jeu de données ne comportant qu’un échantillon « train » et un de « test ». La colonne text_duplication_val, par exemple, ne peut pas être calculée, c’est pourquoi « NR » est indiqué.



Observations

Maintenant que nous avons présenté les données, voyons ce que nous obtenons.

Commençons par les échantillons d’entraînement et de validation :

Echantillon Fuites (texte) Fuites (texte et label) Duplications (texte) Duplications (texte et label)
Entraînement 54,530% 46,477% 73,322% 69,295%
Validation* 48,544% 39,806% 55,922% 55,922%

*Calculé sur les cellules non « NR ».


Nous pouvons ainsi constater que plus de 46,5 % des jeux de données analysés avec un échantillon d’entraînement présentent des fuites, et 39,8 % pour les jeux de données avec un échantillon de validation.
Dans le même temps, 69,3 % des jeux de données analysés avec un échantillon d’entraînement ont des données dupliquées, et 55,9 % pour les jeux de données avec un échantillon de validation.

Poursuivons avec l’échantillon de test :

Echantillon Fuites (texte) Fuites (texte et label) Duplications (texte) Duplications (texte et label)
Test 56,544% 50,503% 66,611% 60,570%


Plus de la moitié des échantillons de test contiennent des données dupliquées et 60 % contiennent un biais (duplication ou fuites ou les deux).

Nous avons donc une majorité de jeux de données contenant des biais dans leur échantillon de test. Mais ces jeux de données sont-ils seulement légèrement biaisés (par exemple moins de 0,1%, ce qui aurait peu d’impact sur les benchmarks) ou, au contraire, massivement biaisés (par exemple 10%, ce qui rendrait les benchmarks non significatifs) ? Le tableau ci-dessous fournit un peu plus de granularité :

% de biais autorisé dans l’échantillon de test % de jeux de données avec au moins le % de biais indiqué dans l’échantillon de test
0,1 61,242
0,2 57,215
0,3 52,852
0,4 49,664
0,5 45,638
0,6 42,953
0,7 41,443
0,8 39,597
0,9 35,906
1 34,564
1,5 27,517
2 24,161
2,5 22,819
3 21,141
4 19,631
5 18,456
10 13,926
20 9,228
30 7,718
40 7,215
50 6,040
60 5,201
70 4,362
80 4,195
90 3,188


Enfin, nous pensons que 33,054 % des jeux de données peuvent contenir des problèmes d’annotation.



Conclusion et perspectives

Le but de cette expérience est d’établir rapidement la qualité de certains jeux de données disponibles sur le Hub d’Hugging Face.
Il apparaît que les jeux de données sans duplications ou fuites ne constituent pas la majorité des cas traités.

Un travail plus important et plus long est nécessaire.
Premièrement, davantage de jeux de données doivent être analysés afin d’affiner cette première image.
Deuxièmement, les jeux de données problématiques devraient être corrigés en supprimant les données dupliquées ainsi que les fuites.
Troisièmement, il serait probablement nécessaire de réexécuter les modèles entraînés sur des jeux de données biaisés sur les jeux de données corrigés du point 2. L’objectif est de pouvoir estimer leur niveau de performance réel.


Enfin, concluons par deux points.
Le premier est une invitation à la vigilance. Si vous concaténez des jeux de données, une donnée dans l’échantillon d’entraînement du jeu de données A peut ne pas se trouver dans l’échantillon de test de A, mais peut être présente dans l’échantillon de test du jeu de données B, ce qui crée une fuite lorsque nous créons le jeu de données A+B. La même logique s’applique aux données dupliquées.
N’oubliez pas de nettoyer vos jeux de données lorsque vous procédez de la sorte.

Le deuxième point est un appel à l’action.
N’est-il pas possible d’avoir un bot qui scanne les jeux de données téléchargés sur le Hub ? Il indiquerait si un jeu de données est fiable et, si ce n’est pas le cas, le nombre de fuites et de données dupliquées.
Comment se fait-il qu’en 2024, nous entraînions encore des modèles sur des jeux de données dont la qualité n’a pas été vérifiée ?