lundi 18 septembre 2023

Notes sur l'extraction des PDF

Ce billet présente une module d’extraction d’information pour les règlements municipaux en format PDF, qui sert à alimenter un moteur de récherche. Il est la troisième partie d’une série de billets qui détaillent la conception et implémentation de celle-là.

Oui, ça fait longtemps depuis le dernier billet! Il y en aura un autre plus étoffé mais pour le moment je voulais juste ajouter quelques notes.

Au sujet du format PDF et de la structure logique

Bien que le format PDF soit utilisé très largement pour stocker, archiver, et distribuer des documents textuels, il est très important de comprendre qu’il est d’abord un format de présentation. Il n’y a aucune garantie que les objets textuels trouvées dans un PDF correspondent à des mots, phrases, ou alinéas d’une langue naturelle - par exemple, chaque caractère peut bien être représenté individuellement, ou des mots peuvent être scindés entre deux lignes, et ainsi de suite.

Par contre, il existe des fonctions dans le standard PDF qui permettent de superposer une structure logique par-dessus la présentation. C’est d’ailleurs ce qui permet un lecteur PDF de présenter une table des matières dans la barre latérale. Dans le cas où cette structure existe, on n’a qu’à l’utiliser… euh, non.

Le problème est, très évidemment, que rien n’oblige l’auteur d’un PDF ni son outil de réaction d’inclure cette structure logique, ni de la spécifier de façon prévisible et consistante. Par exemple, si on compare la structure (avec pdfinfo -struct-text) des règlements 1314-2021-DM et 1328, on voit que les énumérations sont tantôt exprimées (correctement) avec des éléments LI, tantôt avec des éléments H5:

H5 (block)
  "5. Lorsque deux dispositions ou plus du présent règlement s’appliquent...
L (block):
   /ListNumbering /LowerAlpha
  LI (block)
    LBody (block)
      "a. La disposition particulière prévaut sur la disposition générale; "

ou avec des LI seulement:

P (block)
  "Les fins municipales pour lesquelles un immeuble peut être acquis par...
P (block)
  " "
L (block):
   /ListNumbering /Decimal
  LI (block)
    LBody (block)
      "1. Habitation; "

Si on regarde le règlement de zonage, c’est dix fois pire, tout se trouve dans des L imbriqués à perte de vue!

Aussi, une fois qu’on a compris et correctement interpreté le très compliqué standard pour indiquer la structure logique d’un PDF (ce que ne font que partiellement les logiciels libres de PDF, à moins qu’on ait le goût de s’inféoder à Adobe), il reste qu’on doit encore extraire et traiter le contenu.

Finalement, rien n’assure que la structure sera présent à travers multiples versions d’un document, puisque le fonctionnaire qui génère le PDF doit se rappeler de cocher la case “Tagged PDF” ou “PDF/UA” en le faisant et ne pas simplement “imprimer en format PDF”, et parce que des outils de manipulation de fichiers PDF ont tendance à omettre la structure lors des transformations.

Pour cette raison non seulement est-il impossible de se fier complètement à la structure logique d’un PDF, mais il est aussi dangereux même de l’utiliser pour alimenter un modèle probabilistique. La démarche plus robuste est d’entraîner deux modèles, l’un avec les traits structurels et l’autre sans, et choisir le plus approprié selon la présence de structure ou pas.

Annotation des données (la suite)

À date, je n’ai jamais réussi à trouver un logiciel libre potable pour faire l’annotation de séquences ou étendues de texte, peut-être parce que l’annotation est un marché très lucratif - sans annotation, il n’y a aucune « intelligence » artificielle après tout!

Le plus promettant qui existe est Doccano mais j’avais jugé qu’il prendrait trop de travail pour l’adapter au cas d’usage particulier pour quelques raisons:

  • Il est conçu pour annoter du texte brut ou des images individuelles. Soit on perdrait la mise en page, soit le travail deviendrait ardu à force d’annoter des centaines de pages individuellement.
  • Il faudrait convertir les données dans son format préféré puis reconvertir les annotations à la sortie pour les aligner sur les données extraites du PDF.
  • Son architecture est quand même assez complexe alors qu’on a vraiment juste besoin de mettre des catégories sur des lignes dans un fichier CSV…

Heureusement il existe un outil bien adapter pour l’annotation de CSV, qui s’appelle tantôt LibreOffice Calc, tantôt Microsoft Excel, blanc bonnet, bonnet blanc… ça marche vraiment très bien puisqu’il se rappelle des valeurs qu’on rentre dans une colonne, alors une fois les tags (étiquettes) rentrés on n’a que taper quelques lettres, puis on peut étendre une tag pour couvrir un segment entier de texte en glissant la souris, par exemple:

Annotation de CSV avec LibreOffice

Ce qu’il est important de comprendre, par contre, c’est que en tant qu’application de fiche de calcul, Calc (et Excel aussi) ont tendance a “interpreter” les données dans un CSV de façon inattendue. Alors, il faut absolument, lors de l’ouverture d’un fichier CSV, s’assurer dans le dialogue de conversion, que:

  • Le jeu de caractères est réglé sur “UTF-8”
  • Le type de colonne pour la colonne “text” est réglé à “Texte”

Pour ce faire, il faut dans le dialogue de conversion sélectionner la colonne en cliquant sur son titre:

Sélectionner une colonne lors de l’ouverture d’un CSV

Puis sélectionner “Texte” dans le menu d’options:

Sélectionner le type de colonne Texte

mercredi 6 septembre 2023

Extraire la structure et le contenu d'un PDF

Ce billet présente une module d’extraction d’information pour les règlements municipaux en format PDF, qui sert à alimenter un moteur de récherche. Il est la deuxième partie d’une série de billets qui détailleront la conception et implémentation de celle-là.

Analyse d’un règlement

Comme déjà mentionné, on se concentrera d’abord sur l’analyse d’une sorte précise de document, les règlements d’urbanisme de la ville de Sainte-Adèle. Ces documents ont en théorie une structure bien définie et énoncé explicitement dans la première chapitre:

Structure d’un règlement!

Dans l’abstrait on peut alors considérer cela comme une instance d’analyse syntaxique. En partant d’un grammaire (essentiellement le rubrique ci-haut) il faut simplement trouver l’arborescence correspondant aux titres et textes dans un document spécifique. Comme on verra, il y a plusieurs détails qui rendent cela un peu plus compliqué mais c’est grosso modo la démarche à suivre.

On n’utilisera pas pour autant une logithèque existant d’analyse (parsing) puisque celles-ci sont habituellement conçues pour fonctionner au niveau des mots et sont limitées à des textes assez courts. À sa place, on passera par une phase d’analyse textuelle pour identifier les étendues de textes (titres, alinéas, etc) pertinentes, puis on construira par la suite l’arborescence avec quelque chose qui ressemble à l’analyse ascendante.

Au merveilleux pays des PDF

Comment alors identifier ces fameuses étendues de textes? C’est ici que ça se complique! Comme vous savez peut-être, le PDF est un format de présentation, c’est à dire que c’est la forme visuelle d’un document qui y est représentée et pas la forme textuelle. Un PDF (numérisé par exemple) peut même ne contenir aucun texte. Souvent, un PDF contient à la fois l’image numérisé du document et un texte “invisible” provenant d’une reconnaissance optique des caractères - c’est ce texte qu’on voit lorsqu’on fait un copier-coller d’un paragraphe. On peut le voir dans ce règlement par exemple.

On ne fera pas de OCR ici, on présume à la base qu’il existe une forme textuelle accessible dans les fichiers PDF. Ce n’est pourtant pas tout à fait simple d’extraire ce texte. Il en existe plusieurs logiciels libres plus ou moins performants, mais mon choix s’est arrêté sur pdfplumber, ce qui peut sembler illogique puisqu’il n’est pas le plus performant. Si vous voulez tout simplement extraire le texte en soi avec la plus de fidelité il est conseillé d’utiliser plutôt pypdf ou pypdfium2 qui fournissent des fonctions rapides et simples à utiliser. L’avantage de pdfplumber est que, en plus d’extraire le texte, celui-ci est relié à sa mise en page, qu’on utilisera pour mieux identifier les titres et faire le lien entre le texte et les images.

L’autre avantage est que le code source de pdfplumber est facilement compréhensible, ce qui nous permet d’y ajouter de la fonctionalité. Par exemple, ce n’est pas tout à fait vrai qu’il n’y a pas de structure logique dans un PDF - en fait, il peut y en avoir, mais encore, les autres logithèques ne permettent pas de facilement le mettre en relation avec le texte. J’ai fini par contribuer des fonctionalités qui le permettent, et qui se trouvent pour l’instant dans une version modifiée.

Chaîne de traitement

Comme est l’habitude des projets en traitement automatique de texte, le processus est décomposé dans une chaîne d’étapes successives (ou pipeline) qui rajoutent de l’analyse au texte brut. Ceci nous permet de tester et optimiser chaque sous-analyse séparément. Cette chaîne comprend, notamment:

  1. La conversion de PDF en séquence de mots, figures, et tableaux avec leur mise en page.
  2. La segmentation du flux de mots en blocs de texte (des titres, des alinéas, des listes et des tableaux).
  3. L’identification automatique des types de blocs.
  4. L’extraction de faits saillants du texte de chaque bloc (les numéros d’articles, sections et chapitres et les dates d’adoption de règlements)
  5. L’analyse syntaxique qui donne une structure textuelle.

On va se concentrer sur les trois premières fonctions pour le moment.

Annotation des données

Parce que les documents sont d’une taille assez importante et l’analyse en est de nature inexacte, il nous prend d’abord un sous-ensemble sur lequel on pourra en vérifier la précision en développant l’algorithme, puis un échantillon indépendant pour tester celui-ci afin de vérifier sa capacité de traiter des nouveaux documents. On est bien sûr dans une méthodologie typique de l’apprentissage automatique, mais même si on en utilise pas, on aura besoin d’un ensemble de données annotées.

Ceci nécessite qu’on définisse notre schéma d’annotation et les traits (features) d’entrée pour la chaîne de traitement. Ce qui nous fournisse pdfplumber est une liste de tous les caractères pour chaque page d’un document, avec leur positions, tailles, noms de police, et couleurs. Il est également possible d’extraire les mots d’un page (ou ce qui pourrait ressembler à des mots) avec la fonction extract_words. Pour le moment, on va utiliser extract_words au lieu de faire une tokenisation nous-même en partant des caractères, mais celle-ci est aussi une option à regarder afin de profiter de modèles du type Transformer tel que CamemBERT, qui agissent sur des sous-séquences de caractères plutôt que des mots complets.

Les traits tels que fournis par pdfplumber sont trop détaillés dans certains aspects, alors on en fera un post-traitement avant de les écrire dans des fichiers CSV. Ceci nous permettra d’en faire l’annotation tout simplement avec un logiciel de feuille de calcul, puisqu’il ne semble pas exister une meilleur solution à la fois libre d’accès et gratuit pour cette tâche. Puisque Excel et LibreOffice ont une fonction saisie automatique basée sur les autres valeurs d’une colonne, il est assez facile de rentre les annotations, puis de les « étirer » pour couvrir les mots adjacents.

Pour le schéma d’annotation, comme mentionné ci-haut, on se limite à des blocs de texte. Par contre, puisque l’identification sera faite sur ces mêmes blocs de texte, on peut aussi marquer le type de chaque bloc. Nous allons faire l’annotation dans le format IOB, c’est-à-dire, le premier mot de chaque bloc est marqué avec B- suivi par le type de bloc, les mots subséquents avec I- suivi par le type de bloc, et les mots à exclure complètement (il n’y en a pas beaucoup) avec O. Voir par exemple, un exemple de fichier CSV complété ici.

Les types de blocs annotés sont:

  • TOC: Les tableaux de matières sont marqués au complet avec cette annotation. Pour le moment, on va les ignorer et simplement extraire la structure du texte lui-même.
  • Titre: Les titres qui ne correspondent pas à un élément structurel spécifique sont marqués ainsi.
  • Chapitre, Section, SousSection, Article: pour les titres d’éléments spécifiques. Si nécessaire il est possible de les transformer en Titre pour évaluer la segmentation toute seule.
  • Alinea: Les alinéas sans mise en page particulière.
  • Liste: Les éléments de listes (soit des énumérations ou des définitions). Chaque élément est marqué avec son propre bloc (c’est à dire avec B-Liste au premier mot et I-Liste pour les subséquents)
  • Tete, Pied: Les en-têtes et pieds de pages. On ne les marque pas avec O tout simplement puisque, même s’ils ne font pas partie du texte, ils peuvent contenir des informations utiles et faciles d’identifier, comme le titre du document ou le chapitre.

Dès qu’un document (ou même une partie d’un document) est annoté, il est possible d’entraîner un modèle là-dessus et l’utiliser pour accélérer l’annotation des documents suivants. Dans le prochain billet on parlera de l’entraînement des modèles CRF pour la ségmentation et de ce processus plus spécifiquement.

jeudi 29 juin 2023

ALEXI, EXtracteur d'Information

Ce billet présente une module d’extraction d’information pour les règlements municipaux en format PDF, qui sert à alimenter un moteur de récherche. Il est la première partie d’une série de billets qui détailleront la conception et implémentation de celle-là.

Justification

Bien qu’il existe des solutions commerciales et en logiciel libre qui semblerait à première vue posséder cette fonctionnalité, j’ai choisi de l’implémenter au complet, pour quelques raisons à la fois pédagogiques et pratiques:

  1. Il est d’abord hors de question pour une petite municipalité de payer un GAFAM pour ce service, qui implique aussi une configuration infonuagique parfois très lourde et alambiquée, hors de la portée d’un organisme sans personnel dédié en TI.
  2. Les logiciels libres sont performants, et servent bien à certains cas d’usage communs sans trop de configuration. Par contre, lorsqu’il sera question de prendre en charge la structure des documents, cela nécessitera quand même un effort de programmation identique à ce qui sera détaillé ci-dessous.
  3. Les SoLR, ElasticSearch, etc, de ce monde sont aussi assez lourds par rapport à l’installation et la configuration, et fonctionnent en mode client-serveur, alors que mon objectif est de bâtir une fonctionalité très légère qui peut rouler hors connexion et être téléchargée sur un téléphone.
  4. Tout ce travail d’extraction d’information sera encore utile au moment où il sera question de passer à un système plus robuste.

Ici il sera question de parler uniquement de cette phase d’extraction, prise en charge par ALEXI, alors que l’application de recherche (qui s’appelle, bien sûr, SÈRAFIM) sera détaillée plus tard.

Pour le moment, nous allons utiliser Whoosh comme moteur de recherche étant donné qu’il est bien documenté et performe bien avec un minimum de codage superflu. L’aboutissement de ce document sera un petit API REST qui répondra à des requêtes au sujet des règlements d’urbanisme de la ville de Sainte-Adèle.

Problématique

Pour faciliter l’accès du citoyen aux informations contenues dans les règlements municipaux, il est important de pouvoir le diriger non seulement vers le document en soi mais vers la section ou article spécifique qui répond à sa question. Dans le cas d’exemple des règlements d’urbanisme, un seul document peut contenir des centaines de pages et d’articles. Même si on permettait une recherche sur les alinéas individuels, ceux-ci ont besoin d’être présentés avec leur contexte de chapitre, section, etc, pour être compris. Il est alors obligatoire de prendre en charge non seulement le texte du règlement mais également sa structure.

Cette structure est représentée dans le schéma d’un moteur de recherche, qui est généralement composé de champs qui correspondent aux composants et informations auxiliaires d’un document.

Dans le cas des règlements d’urbanisme, il existe heureusement une structure bien définie, plutôt respectée dans la rédaction desdits règlements. Pour faciliter la génération d’un API REST avec FastAPI, nous allons définir cette structure avec des types Pydantic, comme vous pouvez voir dans le module alexi.types.

La tâche alors se résume à:

  1. Extraire le texte des documents de la ville. Idéalement ceux-là viendrait dans un format, comme HTML ou même DOCX, qui conserve des aspects structurels, mais la plupart du temps ces versions sources ne sont accessibles que par les employés de la ville, ou résident même sur des ordinateurs individuels. Alors, il est nécessaire de prendre en charge les documents en format PDF.
  2. Extraire aussi des aspects pertinents de la mise en page des documents. En absence de structure explicite, des éléments comme la taille de police, l’interligne, et la couleur du texte peuvent être très utile pour reconstruire cette structure.
  3. Extraire et traiter les tableaux séparément du texte. Ceux-ci ont une linéarisation qui les rend au mieux incompréhensible, au pire incorrects, lorsqu’ils sont traité comme des lignes de texte.
  4. Induire la structure du texte (alinéas, énumérations, définitions, articles, sections, chapitres).
  5. Conserver les positions des éléments pertinents dans le PDF pour que l’utilisateur puisse les retrouver et consulter dans leur forme originale.

Étapes

Dans les prochains textes de cette série nous allons examiner de plus près les étapes pour faire cette extraction d’information, avec quelques approches qui pourraient être adaptées à d’autres villes ou domaines.