Annotation sémantique CSV vers RDF
mars 2025 → mai 2025
Annoter automatiquement un CSV avec des entités Wikidata
Ce projet est celui de l'UE Ontologies et web sémantique de mon M2 ICE-LD, à l'université Toulouse Jean Jaurès, mené entre mars et mai 2025. Le sujet imposait le cadre : le SemTab, un challenge international annuel, et plus précisément son Accuracy Track.
Le problème
Un fichier CSV ne contient que du texte. Rien n'y indique que la colonne 2 contient des villes, ni que la cellule « Paris » désigne la capitale française plutôt que Paris au Texas ou le personnage mythologique. Tant que cette signification reste implicite, tout ce qui vient ensuite dans un pipeline de données travaille à l'aveugle : l'intégration, le nettoyage, l'apprentissage automatique.
Le challenge découpe le problème en trois tâches. CTA, l'annotation de type de colonne, consiste à dire que cette colonne contient des villes. CEA, l'annotation d'entité de cellule, consiste à dire que cette cellule-ci désigne précisément Q90, la ville de Paris en France. CPA identifie les relations entre colonnes. J'ai traité les deux premières.
Un exemple rend les deux tâches concrètes. Soit cette table :
| ville | pays |
|---|---|
| Paris | France |
| Berlin | Allemagne |
| Rome | Italie |
| Madrid | Espagne |
CEA travaille cellule par cellule et doit relier « Paris » à Q90, « Berlin » à Q64, « Rome » à Q220. CTA travaille sur la colonne entière : contient-elle des villes (Q515) ou des capitales (Q5119) ? Les deux types se défendent, et c'est le vote sur l'ensemble des cellules qui tranche. La difficulté centrale est déjà là : une valeur isolée est ambiguë, seule la colonne entière permet de décider.
Pour l'évaluation, les annotations produites sont comparées à un ensemble de référence, ce qui donne une précision, un rappel et un F1. Sur le jeu de données WikidataTables 2024, il y avait 623 colonnes et 4 246 cellules à annoter.
Ce qui existait déjà, et pourquoi je n'ai pas pris cette voie
Avant de coder, j'ai regardé ce que les équipes du challenge avaient produit. Quatre familles se dégagent. Les approches traditionnelles comme TSOTSA ou DREIFLUSS reposent sur SPARQL et des règles heuristiques : rapides, simples, mais avec des performances très variables selon les tâches. Les approches à base de modèles de langage exploitent la compréhension du contexte, au prix de ressources considérables. Les systèmes hybrides comme GRAMS+ ou ISI-KG combinent les deux et dominent le classement, mais leur complexité technique est un frein réel. Enfin certaines approches ciblent des cas particuliers, comme les tables sans données.
Le sujet excluait de prédire avec un modèle de langage. Restait donc la voie traditionnelle : interroger les bases de connaissances, extraire des types, voter. C'est celle que j'ai suivie, avec l'idée que le croisement de plusieurs sources pouvait compenser la simplicité de la méthode.
Pourquoi TypeScript et Bun
Le domaine est écrasé par Python. Tous les outils, tous les exemples, toute la documentation. J'ai choisi TypeScript avec Bun pour deux raisons : je maîtrise nettement mieux ce langage, et je voulais voir si on pouvait faire de l'analyse de données sérieuse avec cette pile, avec des performances correctes.
La réponse est oui, avec une nuance. Le typage strict a vraiment aidé sur un pipeline à huit étapes où chaque module transforme la structure précédente : une erreur de forme se voit à la compilation plutôt qu'après quarante minutes de requêtes réseau. En revanche, côté outillage, il n'y a presque rien. Là où un projet Python assemble trois bibliothèques éprouvées, j'ai écrit mes clients Wikidata et DBpedia, mon cache, mon parseur CSV. C'est instructif, mais c'est aussi du temps qui n'est pas passé sur l'algorithme.
Cinq versions, et pourquoi chacune a remplacé la précédente
Ce projet est celui où j'ai le plus travaillé par itérations successives, chaque version naissant d'une limite constatée sur la précédente.
La version 1 interrogeait Wikidata seul. Pour chaque colonne, un échantillon de valeurs, une recherche d'entités, la récupération des types via la propriété P31 et de leurs types parents, puis un vote sur les fréquences. Ça marchait, et ça se cassait sur à peu près tout : l'ambiguïté des termes, les valeurs mal orthographiées, un type retenu tantôt trop général, tantôt trop spécifique, et une sensibilité forte à l'échantillon choisi.
La version 2 a ajouté DBpedia. L'idée n'était pas de remplacer Wikidata mais de croiser : quand une entité est trouvée dans les deux bases et que leurs types se correspondent, le score de ce type monte davantage que celui d'un type isolé. Ça a aussi rendu l'outil plus généraliste, puisqu'il ne dépend plus d'une base unique.
La version 3 s'est attaquée à la qualité des données. Casse incohérente, formats de dates disparates, séparateurs numériques, espaces superflus. Sur les données réelles, ce nettoyage en amont change nettement les correspondances trouvées ensuite.
La version 4 a introduit le contexte. Jusque-là chaque colonne était traitée isolément. Or si une colonne contient « France » et une autre « Paris », la première désambiguïse la seconde. J'ai donc ajouté une détection de relations sémantiques entre colonnes, qui vient ajuster les scores, et une analyse des URI des entités candidates : quand l'URI d'un candidat contient une valeur présente ailleurs dans la ligne, sa confiance augmente.
La version 5 a rendu le tout utilisable. Un cache LRU sur les requêtes réseau, parce que les mêmes valeurs reviennent sans arrêt et que chaque appel coûte une seconde. Un CLI interactif avec des presets, pour ne plus éditer un fichier de configuration à chaque essai. Et surtout un changement dans la sélection finale : au lieu de prendre le type au meilleur score quelle que soit sa source, je ne retiens plus que des types Wikidata, en passant par la table de correspondance quand seul DBpedia a répondu. Le challenge attend des URI Wikidata ; produire autre chose, aussi juste soit-il, compte comme une erreur.
Cette architecture s'est révélée assez modulaire pour que l'ajout de CEA ne demande presque rien : les étapes de nettoyage et de recherche d'entités étaient déjà là, il ne restait qu'à changer ce qu'on fait des candidats à la fin.
Les résultats
| Tâche | F1 | Précision | Rappel | Temps | Cibles |
|---|---|---|---|---|---|
| CTA | 0,552 | 0,552 | 0,552 | 1 h 32 | 623 colonnes |
| CEA | 0,651 | 0,651 | 0,651 | 1 h 16 | 4 246 cellules |
Je n'ai rien soumis officiellement au challenge, l'évaluation s'est faite en local contre l'ensemble de référence. La comparaison qui suit est donc indicative, pas un classement.
| Approche | CTA | CEA |
|---|---|---|
| ISI-KG (hybride) | 0,93 à 0,96 | non soumis |
| TSOTSA (traditionnelle) | 0,19 à 0,72 | 0,069 |
| Cet outil (traditionnelle, local) | 0,552 | 0,651 |
En typage de colonnes, l'écart avec ISI-KG et ses 0,9 est net : une approche sans apprentissage ne rivalise pas avec un système hybride. Mes 0,552 se situent entre les deux passages de TSOTSA, dont le score passe de 0,72 à 0,19 d'un round à l'autre.
Pour l'annotation de cellules, c'est différent. Le seul score officiel publié sur ce benchmark est celui de TSOTSA, à 0,069 ; ni ISI-KG, ni les autres favoris n'ont soumis sur cette tâche. Soit CEA sur WikidataTables est plus difficile qu'il n'y paraît, soit les équipes ont concentré leurs efforts ailleurs. Dans les deux cas, obtenir 0,651 avec un vote sur deux bases de connaissances reste un résultat correct.
Un point ressort des scores : la précision et le rappel sont identiques dans les deux tâches. L'algorithme ne s'abstient jamais. Il répond toujours quelque chose, et se trompe une fois sur deux en CTA, une fois sur trois en CEA. Un système capable de dire « je ne sais pas » aurait eu une meilleure précision, au prix du rappel.
Ce qui n'a pas marché
Les nombres. C'est la limite qui a survécu à toutes les versions. Une cellule contenant 1789 ou 42 ne se cherche pas dans une base de connaissances comme un nom propre : il n'y a pas d'entité à trouver, seulement un littéral dont le type dépend entièrement du contexte. Je n'ai jamais rien produit de convaincant là-dessus.
La désambiguïsation. Malgré le contexte apporté par la version 4, « Paris » et « Mercury » restent des pièges : le signal des colonnes voisines est trop faible quand la table elle-même est ambiguë. Le tout ne fonctionne d'ailleurs qu'en anglais, puisque toute la correction de données est calibrée pour cette langue.
Des fondations que je n'ai pas vérifiées. La correction orthographique existe dans le code sous forme de fonction vide : je n'ai pas trouvé comment corriger des fautes de frappe sans passer par un modèle, ce que le sujet écartait, et j'ai préféré laisser le point d'entrée en place plutôt que de faire semblant. Plus gênant, la table de correspondance entre types DBpedia et Wikidata ainsi que le jeu de relations entre entités ont été générés par IA puis intégrés tels quels. Ces tables sont au cœur du croisement des sources, et je ne les ai pas relues entrée par entrée.
Les endpoints publics. Wikidata et DBpedia m'ont bloqué plusieurs fois. J'ai fini par poser des valeurs trouvées au tâtonnement jusqu'à ce que ça passe : cinq requêtes par lot, une demi-seconde entre deux lots, une seconde entre deux colonnes. Rien d'élégant, mais ça a rendu les traitements d'une heure et demie possibles sans me faire couper. Un dump local de Wikidata aurait sûrement aidé, je n'y ai pas pensé à l'époque. C'est aussi cette contrainte de temps qui explique que la troisième tâche du challenge, CPA, n'ait jamais été rendue : les relations entre colonnes sont pourtant détectées, la version 4 en dépend, mais elles ne sont pas produites au format attendu.
Ce que j'en retiens
Une base de connaissances n'est pas une vérité unique. Wikidata et DBpedia ne modélisent pas le monde de la même façon : les hiérarchies de types diffèrent, les identifiants ne correspondent pas, une entité présente dans l'une peut manquer dans l'autre. Interroger les deux et réconcilier leurs réponses a représenté une bonne partie du travail.
Sur la méthode, c'est le projet qui m'a le plus appris à avancer par versions. Cinq itérations, chacune partant d'une limite constatée sur la précédente plutôt que d'une intuition. À chaque fois, noter ce qui ne marchait pas dans la version courante m'a donné la suivante.
Le dépôt est archivé depuis juin 2025 : c'est un projet d'étude, il s'arrête là. Avec le recul, la piste que j'explorerais est celle des embeddings pour le rapprochement flou, qui aurait probablement réglé une partie du problème d'orthographe et de désambiguïsation sans tomber dans la prédiction par modèle de langage que le sujet écartait.