Aller au contenu

Annotation sémantique CSV vers RDF

mars 2025 → mai 20257 min de lecture

Code sourceSemTab 2024, Accuracy Track

J’ai fait ce projet entre mars et mai 2025, pour le cours de web sémantique de mon M2 ICE-LD à l’université Toulouse Jean Jaurès. Le sujet imposait le SemTab, un challenge international qui revient chaque année, et plus précisément son Accuracy Track. L’édition 2024 était déjà finie, alors j’ai repris ses tables et j’ai tout évalué en local.

Le problème

Un fichier CSV, ce n’est que du texte. Rien n’y dit que la deuxième colonne contient des villes, ni que « Paris » désigne la capitale de la France plutôt que Paris au Texas. Tant que ce sens reste implicite, tout ce qu’on fait ensuite avec les données se fait à l’aveugle.

Sur les tables Wikidata, le challenge pose trois tâches. La CTA (Column Type Annotation) donne le type d’une colonne, par exemple « ville ». La CEA (Cell Entity Annotation) relie chaque cellule à une entité précise de Wikidata, et ce « Paris »-là devient Q90. La CPA cherche les relations entre colonnes. J’ai traité les deux premières.

Prenons une table avec des villes et leurs pays : Paris et France, Berlin et Allemagne, Rome et Italie. En CEA, il faut relier chaque ville à la bonne entité. En CTA, il faut dire si la première colonne contient des villes ou des capitales. Les deux se défendent, et une valeur seule ne suffit pas pour trancher. Il faut voter sur toute la colonne.

Mon outil traite les deux tâches. Il se lance en ligne de commande, avec un petit menu qui demande ce qu’on veut annoter. On lui donne un CSV, ou tout un lot de tables au format du challenge, et il remplit le fichier de réponses. Le code est sur GitHub.

Sans modèle de langage

Le sujet excluait de faire prédire la réponse par un modèle de langage. Je suis donc parti sur la voie classique. On cherche chaque valeur dans les bases de connaissances, on récupère les types des entités trouvées, puis on vote. Pour compenser la simplicité de la méthode, j’ai voulu croiser deux sources, Wikidata et DBpedia.

Pourquoi TypeScript

Dans ce domaine, presque tout se fait en Python. J’ai quand même pris TypeScript avec Bun, parce que je le maîtrise beaucoup mieux, et parce que je voulais voir si on pouvait faire de l’analyse de données sérieuse avec.

Le typage strict m’a vraiment aidé. L’annotation d’une colonne passe par huit étapes, et chacune reprend ce que lui donne la précédente. Une erreur de forme se voit donc à la compilation, pas au milieu d’un traitement qui tourne depuis une heure. Côté outillage par contre, il n’y a presque rien. Il a fallu écrire les clients Wikidata et DBpedia et la lecture des CSV, là où un projet Python aurait pris des bibliothèques toutes faites. J’ai appris pas mal de choses, mais c’était du temps en moins pour l’algorithme.

Cinq versions

L’algorithme, lui, est passé par cinq versions, chacune partant d’une limite de la précédente. C’est le projet où j’ai le plus avancé comme ça.

La première n’interrogeait que Wikidata. Pour chaque colonne, elle prenait un échantillon de valeurs, cherchait les entités correspondantes, récupérait leurs types et les types au-dessus, puis votait. Ça marchait, mais ça cassait sur à peu près tout : les mots ambigus, les fautes d’orthographe, un type tantôt trop général, tantôt trop précis, et un résultat qui changeait selon l’échantillon.

La deuxième a ajouté DBpedia, pour croiser les réponses plutôt que remplacer Wikidata. Quand une entité est trouvée dans les deux bases et que leurs types correspondent, ce type gagne plus de points qu’un type trouvé d’un seul côté.

La troisième s’est attaquée à la qualité des données, avec la casse, les formats de date, les séparateurs dans les nombres et les espaces en trop. Sur des données réelles, ce nettoyage change pas mal les entités trouvées ensuite.

La quatrième a apporté le contexte. Jusque-là, chaque colonne était traitée seule. Sauf que si une colonne contient « France » et une autre « Paris », la première aide à comprendre la seconde. J’ai donc ajouté la détection des relations entre colonnes, comme pays et capitale. J’ai aussi regardé les URI des candidats : quand l’URI d’une entité contient une valeur présente ailleurs dans la ligne, sa note monte.

La cinquième a rendu le tout utilisable, avec un cache devant les requêtes, parce que les mêmes valeurs reviennent sans arrêt, et un menu avec trois réglages tout prêts. J’ai aussi changé le choix final, qui privilégie maintenant les types Wikidata. Quand seul DBpedia a répondu, une table de correspondance traduit ses types. Le challenge attend des identifiants Wikidata, et il compte tout le reste comme faux, même quand c’est juste.

La CEA est arrivée à ce moment-là, et elle n’a presque rien demandé. Le nettoyage et la recherche d’entités étaient déjà là, il restait juste à changer ce qu’on fait des candidats à la fin.

Les résultats

Pour noter un outil, le challenge compare ses réponses à celles attendues, et en tire une précision, un rappel et un F1. Évalué en local sur les tables Wikidata de 2024, mon outil arrive à un F1 de 0,55 en CTA et de 0,65 en CEA. La précision et le rappel sont identiques dans les deux cas, parce que l’algorithme répond toujours quelque chose. Il se trompe à peu près une fois sur deux en CTA, une fois sur trois en CEA. S’il savait dire « je ne sais pas », il gagnerait en précision, au prix du rappel.

Pour situer, seules deux équipes ont publié des résultats sur ces tables lors du challenge 2024. En CTA, ISI-KG dépassait 0,9. En CEA, seule TSOTSA a rendu un score, à 0,07. Ce n’est pas un classement, puisque je n’ai rien soumis et que je n’étais pas dans les mêmes conditions. En typage de colonnes, l’écart avec les meilleurs est net. En annotation de cellules, 0,65 avec un simple vote sur deux bases, je trouve ça correct.

Ce qui n’a pas marché

Les nombres, d’abord. Une cellule qui contient 1789 ou 42 ne se cherche pas comme un nom propre. Il n’y a pas d’entité à trouver, juste une valeur dont le sens dépend du reste de la table. Je n’ai jamais rien sorti de convaincant là-dessus, dans aucune version.

La désambiguïsation reste fragile aussi. « Paris » ou « Mercury » sont toujours des pièges quand toute la table est ambiguë, et les relations entre colonnes n’y changent plus rien. Quand j’ai simplifié le vote final, elles sont sorties du calcul. Elles sont toujours détectées, mais elles ne pèsent plus sur le type choisi.

Pour les fautes de frappe, je n’ai pas trouvé comment les corriger sans passer par un modèle, ce que le sujet excluait. Il n’y a qu’une petite liste de fautes courantes de l’anglais, qui ne sert pas à grand-chose sur des noms propres. La recherche se fait d’ailleurs sur les libellés anglais des bases, l’outil est pensé pour des tables en anglais.

Et puis il y a eu les serveurs publics. Wikidata et DBpedia m’ont bloqué plusieurs fois, et j’ai fini par trouver au tâtonnement des réglages qui passaient : cinq requêtes à la fois, une demi-seconde entre deux lots, une seconde entre deux colonnes. Rien d’élégant, mais avec ça, des traitements d’une heure et demie allaient au bout sans que je me fasse couper. Le challenge indiquait pourtant quel dump de Wikidata prendre pour travailler hors ligne. Je n’y ai pas pensé à l’époque, et ça aurait sûrement aidé.

Faute de temps, la CPA n’a jamais été rendue, alors que l’outil détecte déjà les relations entre colonnes.

Avec l’IA

Pour le code, je me suis aidé de Junie, l’agent IA de JetBrains. Un fichier de consignes dans le dépôt lui demandait de lancer les tests, le lint et le build avant de me rendre la main, et de tenir le README à jour.

Les deux tables au cœur du croisement des sources, je les ai fait générer par IA. La première fait correspondre les types DBpedia et Wikidata, la seconde liste les relations entre types. Plus gênant, je les ai intégrées telles quelles, sans jamais les relire ligne par ligne.

Où en est le projet

Le dépôt est archivé depuis juin 2025. C’était un projet de cours, il s’arrête là. On était deux dessus, et la dernière partie, un site pour visualiser les données, a été faite par quelqu’un d’autre. Je n’en parle donc pas ici.

Ce que je retiens surtout, c’est que deux bases de connaissances ne décrivent pas le monde de la même façon. Les types ne sont pas rangés pareil, et une entité présente dans l’une peut manquer dans l’autre. Les faire parler ensemble a pris une bonne partie du travail. Avec le recul, je regarderais du côté des embeddings, pour rapprocher des valeurs proches sans être identiques. Ça réglerait sûrement une partie des fautes et des ambiguïtés, sans faire prédire la réponse par un modèle de langage.

Technologies utilisées