TL;DR: Vous pouvez utiliser
docling-parse car ses
modèles sont les plus fiables et efficaces de ceux que j’ai testés.
Mais pour comprendre comment ça fonctionne, continuez à lire!
Nous allons voir ici comment utiliser des modèles de vision
computationelle pour faire de l’analyse de mise en page d’un PDF et en
extraire les titres de sections, alinéas, listes, et tableaux. Pour
ce faire, on fera appel à la logithèque libre
Transformers qui facilite
beaucoup le téléchargement et utilisation de ces modèles en Python.
On prendra d’abord un document très simple d’une seule
page,
fait sur mesure pour ce genre d’analyse. Ceci nous permettra plus
tard (dans un autre texte) de mettre en lumière des défaillances de
certains modèles.
Dépendances
D’abord installons des dépendances nécessaires:
pip install pillow playa-pdf timm torch torchvision transformers \
--extra-index-url https://download.pytorch.org/whl/cpu
On utilisera pillow
pour manipuler et annoter des images des pages,
playa-pdf
pour extraire des metadonnées des PDFs, et le reste sont
des nécessaires pour rouler les modèles de vision. On les installe
ici en mode CPU pour éviter de télécharger des gigaoctets de n’importe
quoi que nous impose NVidia, mais si vous avez un GPU pris en charge
(même un petit GT1030 suffit) vous pouvez enlever le --extra-index-url
.
Il faut dire tout de suite que les programmeurs de NVidia non
seulement génèrent des logiciels obèses mais aussi peu fiables, alors,
sous Ubuntu, il faut aussi gosser quelques affaires pour éviter d’être
pris avec un fâchant problème de
C++ (en réalité, c’est
C++ le vrai problème, comme d’habitude):
sudo apt install gcc-10 g++-10
export CC=gcc-10
export CXX=g++-10
Traîtement des PDF en image
Si on veut faire de la vision, bon, ça prend des images matricielles.
Pour le moment, on utilisera le bon vieux outil
Poppler qui est installé partout
sous GNU/Linux (et beaucoup moins ailleurs, désolé) pour convertir des
PDF en images de pages.
Ce qu’il faut comprendre avec des modèles d’apprentissage automatique
(et que les auteurs de certains logithèques populaires semblent
ignorer) c’est que, lorsque possible, ces modèles sont plus
performants lorsque les données qu’on leur demande de traiter
ressemblent à celles sur lesquelles ils sont entraînés. Puisque tous
les modèles d’analyse de mise en page sont entraînés sur
DocLayNet, qui est
composé d’images de page en format carré avec
anticrénelage,
et puisque PDF est un format vectoriel qui nous permet de générer
des images de n’importe quelle taille et format, il est préférable de
créer d’abord des images dans le format attendu.
D’ailleurs il ne sert à rien de créer des images de plus haute
résolution que celle prise en charge par le modèle! C’est tout
simplement du gaspillage d’énergie, de temps et de stockage (ou,
autrement dit, un crime contre le climat et l’économie) puisqu’il
faudra rééchantilloner par la suite ces images. Heureusement les bons
modèles comme ceux de Docling nous diront leur format préféré.
Alors, on va construire une simple fonction pour nous donner des images
du format souhaité, en utilisant les arguments -scale-to-x
et
-scale-to-y
de Poppler:
import subprocess
import tempfile
from pathlib import Path
from typing import Iterator
from PIL import Image
def popple(path: Path, width: int, height: int) -> Iterator[Image.Image]:
with tempfile.TemporaryDirectory() as tempdir:
temppath = Path(tempdir)
subprocess.run(
[
"pdftoppm",
"-scale-to-x",
str(width),
"-scale-to-y",
str(height),
str(path),
temppath / "ppm",
],
check=True,
)
for ppm in sorted(temppath.iterdir()):
yield Image.open(ppm)
Reconnaissance des éléments de mise en page
On utilisera un modèle
RT-DETR
pour identifier les éléments dans les images. Pour des raisons
inconnues, le groupe Docling n’a pas mis son modèle dans un endroit
standard, alors on ne peut malheureusement pas utiliser
AutoModel.from_pretrained
. Ce n’est pas grave, on va tout
simplement télécharger les fichiers manuellement:
from huggingface_hub import hf_hub_download
processor_config_path = hf_hub_download(
"ds4sd/docling-models",
"model_artifacts/layout/preprocessor_config.json"
)
config_path = hf_hub_download("ds4sd/docling-models",
"model_artifacts/layout/config.json")
weights_path = hf_hub_download("ds4sd/docling-models",
"model_artifacts/layout/model.safetensors")
Pour la suite on va créer deux objets, un RTDetrImageProcessorFast
et un RTDetrForObjectDetection
:
import os
processor = RTDetrImageProcessorFast.from_json_file(processor_config_path)
model = RTDetrForObjectDetection.from_pretrained(os.path.dirname(config_path))
Maintenant on peut savoir le format attendu par le modèle:
width = processor.size["width"]
height = processor.size["height"]
Et les noms des éléments qu’il peut extraire (mais lisez plus loin…):
id2label = model.config.id2label
On va télécharger le document:
import requests
r = requests.get("https://ecolingui.ca/pdf_structure.pdf")
r.raise_for_status()
with open("pdf_structure.pdf", "wb") as fh:
fh.write(r.data)
Et hop, utiliser le modèle est très simple:
import torch
with torch.inference_mode():
for image in popple("pdf_structure.pdf"):
inputs = processor(images=image, return_tensors="pt")
outputs = model(**inputs)
Ces outputs
ne sont pas dans un format super intéressant, alors on
va utiliser le RTDetrImageProcessorFast
pour avoir des coordonnées
qui correspondent à l’image:
img_results = processor.post_process_object_detection(
outputs,
target_sizes=[(image.height, image.width)],
)[0]
print(img_results)
Ceci nous donne quelque chose plus intéressant, qu’on peut interpréter
en utilisant le id2label
mentionné ci-haut (notez qu’il faut
extraire les valeurs des tensor
qui nous retourne le modèle):
Mais oups! Les classifications sont un peu suspects, car il n’existe
aucune image (Picture) dans notre document! Bon, il semble que les
développeurs de DocLing se sont trompés un peu et il faut ajouter 1
aux indices des classes:
for score, label, box in zip(
img_results["scores"], img_results["labels"], img_results["boxes"]
):
label = id2label[label.item() + 1]
box = [round(x) for x in box.tolist()]
score = score.item()
print(f"Élément: {label} à {box} avec confiance {score}")
Ce qui nous donne quelque chose d’intéressant:
Élément: Text à [59, 138, 575, 197] avec confiance 0.9866001009941101
Élément: List-item à [78, 309, 571, 369] avec confiance 0.9816325902938843
Élément: List-item à [97, 290, 234, 300] avec confiance 0.9407719969749451
Élément: List-item à [78, 271, 171, 279] avec confiance 0.9286511540412903
Élément: Table à [58, 400, 581, 438] avec confiance 0.9260659217834473
Élément: List-item à [78, 252, 170, 261] avec confiance 0.925058901309967
Élément: Section-header à [59, 216, 109, 226] avec confiance 0.9144930243492126
Élément: Section-header à [59, 384, 113, 393] avec confiance 0.9069509506225586
Élément: Section-header à [59, 98, 115, 109] avec confiance 0.8970564603805542
Élément: Section-header à [192, 59, 447, 76] avec confiance 0.8865164518356323
Élément: Text à [59, 119, 224, 128] avec confiance 0.8446756601333618
Élément: Text à [59, 235, 157, 242] avec confiance 0.8046810626983643
Élément: Section-header à [59, 235, 157, 242] avec confiance 0.5821080803871155
Élément: Section-header à [59, 119, 224, 128] avec confiance 0.5068144202232361
On peut vérifier que tout a été bien identifier en utilisant ImageDraw
:
from PIL import ImageDraw
draw = ImageDraw.Draw(image)
for label, box in zip(img_results["labels"], img_results["boxes"]):
label = id2label[label.item() + 1]
box = [round(x) for x in box.tolist()]
draw.rectangle(box, outline="red")
draw.text((box[0], max(0, box[1] - 12)), label, fill="red")
image.save("pdf_structure.png")
Et voilà:
Mais attend, cette image a un aspect un peu bizarre! Si on veut
vraiment utiliser les coordonnées, il faut plutôt les transformer pour
correspondre à la page originelle. On peut le faire facilement en
cherchant les informations avec PLAYA-PDF:
import playa
with playa.open("pdf_structure.pdf") as pdf:
page = pdf.pages[0]
page_results = processor.post_process_object_detection(
outputs,
target_sizes=[(page.height, page.width)],
)[0]
print(page_results)
Conclusion
Nous avons vu comment utiliser un modèle de vision pour identifier les
éléments de mise en page dans un PDF. Pour la suite des choses, on
regardera comment trouver les textes qui correspondent à ces éléments.
Comme mentionné dans le dernier billet de ce blogue, il existe
d’autres modèles de vision qui fonctionnent essentiellement de la même
manière, mais qui sont en général beaucoup moins rapides et fiables.
Un prochain texte va en faire la comparaison.
Comme mentionné dorénavant, le format PDF est un format
de présentation, à la différence du HTML par exemple, qui sépare dans
la mésure du possible la structure sémantique du texte et sa mise en
page. Concrètement cela veut dire que, en théorie (on aimerait tous y
vivre!), l’extraction du texte même d’une page HTML très « visuelle »,
avec une mise en page comprenant des multiples colonnes, des images,
des figures, etc, se résume à simplement enlever les tags.
Comme aussi mentionné dorénavant, le standard PDF dans sa
déclinaison « universellement
accessible »
admet une extraction du texte et même de la structure sémantique
légèrement plus compliquée mais néanmoins faisable. Malheureusement,
il suffit qu’on oublie de cocher la case PDF/UA en sauvegardant un
fichier, ou qu’on sélectionne « imprimer en format PDF » au lieu de
« exporter », ou qu’on passe le PDF par un logiciel douteux, pour que
toute cette belle structure tombe à l’eau. On se retrouve avec des
fragments de texte positionnés absolument, souvent sans séparation
entre les mots et parfois même dans une ordre arbitraire.
Dont la nécessité d’une analyse de la mise en page, pour identifier et
séparer le texte, les figures, et les tableaux, mais aussi pour
identifier les éléments textuelles, dont les titres et listes, ainsi
que les artéfacts textuelles qui ne font pas partie du contenu, dont
les en-têtes, les pieds de page et les captions de figures. Étant
donné le fait que les éléments d’un PDF sont positionnés absolument
sur une page (sans référence à une grille ou autre structure
visuelle), la diversité de formats de papier, de polices de
caractères, de marges et entrelignes, entre autres, il est presque
impossible de concevoir des règles pour prendre en charge tout cela à
moins de le refaire pour chaque nouveau document ou presque.
Dont aussi la nécessité (il me semble que je me répète souvent!)
d‘utiliser… l’apprentissage machine (toé IA chose machin). On en a
déjà parlé un peu par rapport aux éléments du texte. On
peut catégoriser les types d’analyses propices à l’apprentissage.
Analyse textuelle
Ceci est le type d’analyse le plus simpliste et probablement le plus
répandu. En présumant une extraction préalable du texte,
correspondant le plus possible à la forme perçue par un lecteur, on
peut faire une analyse textuelle (parsing) ou une classification
de séquence pour répérer des éléments structurels.
Évidemment, puisque toute l’information provenant de la mise en page a
été évacué par le processus d’extraction de texte, on n’a peu de
chances de reconstruire la structure du document de façon robuste ou
fiable.
Analyse espacielle
Par défaut, ALEXI fait une
analyse espacielle, c’est à dire qu’il prend compte de l’emplacement
des éléments de texte ainsi que des attributs typographiques (taille
de police, caractères gras ou italiques, etc.) pour identifier des
éléments tels que titres de sections, éléments de listes, etc.
Bien qu’il utilise l’apprentissage machine, d’autres logiciels
utilisent aussi des méthodes algorithmiques ou heuristiques, par
exemple pdfminer.six ou
camelot.
Analyse visuelle
Dernièrement, il existe une tendance à faire une analyse purement
visuelle de la mise en page pour identifier les éléments
structurelles. Alors que l’analyse textuelle évacuait toute la mise
en page et les attributs visuels du document, une analyse visuelle
fait exactement le contraire - chaque page d’un PDF est transformé en
image matricielle, qui est par la suite analyser par un modèle de
vision tel que
DeformableDETR,
YOLOX ou
YOLO, qui a été
préalablement entraîner sur un corpus d’images semblables.
Quoiqu’il soit possible d’utiliser ces modèles par le biais de des
logithèques peu fiables provenant de compagnies qui préfèrent prendre
votre argent et/ou vos données personnelles, ceci n’est aucunement
nécessaire. En plus, il semble que ces logithèques libres
fonctionnent essentiellement comme appât pour les services payants /
espions. Nous allons donc, dans le prochain billet, regarder comment
utiliser les modèles directement pour éviter divers problèmes reliés à
ces logithèques.
(La seule exception dans cette bande là est probablement
DocLing qui provient d’un groupe
de récherche réputé.)
À quoi ça sert, au juste, tout cet effort
d’analyse des fichiers PDF? D’abord, bien
sûr, ça facilite la recherche, telle qu’implémentée dans
SÈRAFIM (un SystÈme de
Recherche Ad-hoc pour Fouiller dans les Informations
Municipales) puisqu’on est capable d’indexer chaque article et
chapitre individuellement - on peut ainsi comparer les dispositions
par rapport aux piétons dans l’aménagement des
stationnements
à travers quelques villes des Laurentides, par exemple.
Mais pas juste ça! Le fait d’avoir extrait les unités semantiques des
règlements nous permet d’ajouter plusieurs fonctionnalités pour en
faciliter la lecture et la compréhension, dont:
- L’ajout d’hyperliens. Par exemple, dans l’article
264
du règlement de zonage de Sainte-Adèle, on peut maintenant naviguer
vers le règlement de
construction
cité là-dedans ainsi que l’article
251
sur les aménagements piétonniers.
- La navigation structurée en-ligne. Au lieu d’avoir besoin de
télécharger le PDF pour trouver un chapitre ou section spécifique,
on peut voir tous les règlements dans une
arborescence et
expansionner pour obtenir rapidement le contenu recherché. Des
hyperliens vers la page spécifique du PDF sont aussi fournis.
- Mais surtout, ça nous permet d’utiliser… 🎉des grands modèles de
langage🎆
(oui, ces célèbres patentes qu’on appelle à tort de l’intelligence
artificielle) pour en faire des analyses automatiques, des résumés,
et d’autres manipulations.
L’analyse spécifique qu’on en fera ici est une analyse du plus proche
voisin
qui nous permettra de faire de façon plus efficace une analyse du type
mentionné ci-haut, c’est à dire, répondre à des questions du genre
«comment se comparent les dispositions par rapport à l’aménagement des
-stationnements entre Sainte-Adèle, Saint-Sauveur et Prévost». (si cela
vous semble une question hautement inutile, vous n’êtes sûrement pas
le public cible de ce blogue)
Pour ce faire, nous utiliserons la célèbre (ou pas) logithèque
SentenceTransformers pour calculer des
réprésentations vectorielles (embeddings) correspondant à chaque
unité sémantique dans l’ensemble des règlements. Par la suite, on
peut utiliser une multiplication matricielle très rapide pour obtenir
les proches
voisins
de chaque élément (c’est à dire les articles semblables dans d’autres
règlements ou d’autres villes). C’est vraiment très simple et
efficace! Par contre, pour des très grands corpus de documents, il
sera nécessaire d’utiliser un outil de référencement optimisé tel que
FAISS ou
RAGatouille.
Préalablement on aura téléchargé un ensemble de règlements avec
ALEXI. Pour faciliter la chose
on se limitera à trois règlements de zonage:
Les ayant téléchargés, on va les analyser avec ALEXI:
alexi extract *.pdf
Cela va prendre quelques minutes pour créer le répertoire export
avec plein de fichiers HTML et JSON (si vous voulez avoir plus
d’information sur ce qui se passe vous pouvez utiliser alexi -v extract *.pdf
). Par exemple, on voit que tous les articles du
règlement de zonage de Sainte-Adèle se trouvent maintenant sous
export/Rgl-1314-2021-Z-en-vigueur-20240823/Article
, chacun dans un
répertoire avec un seul fichier index.html
.
Le texte des unités sémantiques est maintenant converti d’une forme
graphique (des PDF) vers une forme moyennement sémantique (du HTML).
Ce qui nous intéresse, par contre, c’est le texte brut. On peut alors
utiliser BeautifulSoup ou
lxml pour l’extraire de nouveau et le
passer dans le modèle SentenceTransformers:
from bs4 import BeautifulSoup
def get_text(path) -> str:
with open(path) as infh:
soup = BeautifulSoup(infh)
return soup.article.text
articles = {path : get_text(path) for path
in Path("export").glob("**/Article/*/*.html")}
Réprésentation vectorielle et proches voisins
Pour générer les réprésentations vectorielles (embeddings) il nous
faut un modèle pré-entraîné. Il en existe plusieurs sur le site
HuggingFace qui sont spécialisés pour le français, qu’on retrouve
(avec plusieurs informations utiles sur leur performance) dans
l’espace
DécouvrIR.
Pour notre démonstration on utilisera la variété le plus simple et
rapide, un «Single-vector dense bi-encoder» de moins de 100M
paramètres. En cochant les cases on trouve que le meilleur à date est
biencoder-distilcamembert-mmarcoFR… on
y va!
import sentence_transformers as st
model = st.SentenceTransformer("antoinelouis/biencoder-distilcamembert-mmarcoFR")
Il nous reste qu’à transformer (lolle) les textes extraits en vecteurs
ou plus précisément en une matrice. Pour ce faire on va les énumérer
et retenir les indices pour faire la correspondance entre documents et
rangées de cette matrice:
artidx = {path: idx for idx, path in enumerate(articles)}
idxart = list(articles)
Par la suite on calcule les vecteurs:
embeddings = model.encode(list(articles.values()), convert_to_tensor=True,
show_progress_bar=True)
Sur une carte graphique GT1030, qu’on peut acheter usagée autour de 75
$, cela prend environ 30 secondes. Enfin, on utilise la fonction
semantic_search
pour trouver les plus proches voisins de chaque
rangée de la matrice (c’est instantané):
neighbours = st.util.semantic_search(embeddings, embeddings, top_k=20)
Pour voir ce que cela donne pour l’article
264
mentionné ci-haut, on va trouver le vecteur correspondant à cet
article, puis ensuite afficher les plus proches articles qui ne
viennent pas du même règlement:
import textwrap
# la rangée qui correspond à l'article 264 du règlement 1314-2021-Z
srcpath = Path("export/Rgl-1314-2021-Z-en-vigueur-20240823/Article/264/index.html")
idx = artidx[srcpath]
for n in neighbours[idx][1:]:
neighbour_idx = n["corpus_id"]
dstpath = idxart[neighbour_idx]
if dstpath.parts[1] == srcpath.parts[1]:
continue
print(idxart[neighbour_idx])
print(textwrap.fill(articles[idxart[neighbour_idx]].strip()[0:250]), "...")
print()
On voit que le modèle a bel et bien trouvé des articles pertinents:
export/RUD_T6_VR/Article/6.4.6.3/index.html
Article 6.4.6.3 Aménagement d’une aire de stationnement extérieure de
15 cases ou plus En plus des dispositions de l’article précédent, les
dispositions suivantes s’appliquent à toute aire de stationnement de
15 cases ou plus. L’aménagement d’une ai ...
export/RUD_T6_VR/Article/6.4.6.4/index.html
Article 6.4.6.4 Aménagement d’une aire de stationnement extérieure de
100 cases ou plus En plus des dispositions des articles précédents,
une aire de stationnement pour véhicule de 100 cases et plus doit
respecter les dispositions suivantes : 1° une ...
export/RUD_T6_VR/Article/6.4.9.1/index.html
Article 6.4.9.1 Nombre de cases de stationnement pour véhicule
automobile Tous les usages principaux doivent disposer d’un
stationnement hors rue d’une capacité minimale et maximale conforme
aux dispositions du présent article. Cette exigence est co ...
export/RUD_T6_VR/Article/6.4.5.1/index.html
Article 6.4.5.1 Aménagement d’une aire de stationnement extérieure de
moins de 3 cases ou allée de stationnement Une aire de stationnement
extérieure de moins de 3 cases ou une allée de stationnement doit
respecter les dispositions suivantes : 1° el ...
export/Reglement-2009-U53-fevrier-2024/Article/12.1.7/index.html
Article 12.1.7 Accès aux aires de stationnement (modifié, règlement
numéro 2011-U53-18, entré en vigueur le 2011-07-21) (modifié,
règlement numéro 2011-U53-21, entré en vigueur le 2011-12-15) Toute
case de stationnement doit être implantée de telle ...
Ce qu’on voit aussi est que la plupart des articles les plus
similaires viennent du règlement de Prévost. Ceci nous indique que le
règlement de zonage de Prévost (2024) serait plus similaire à
celui de Sainte-Adèle que celui de Sainte-Agathe (2009).
Recherche semantique
Bien sûr, rien nous empêche de comparer autre chose que des articles
des règlements. On peut également convertir des questions ou d’autres
documents en vecteurs. Si par exemple on voulait savoir quelles
articles ressemblent aux normes proposées par le CRE Montréal pour le
stationnement écoresponsable en matière de
verdissement… on
peut le faire! Il faut simplement télécharger et extraire le texte de
cette page:
import requests
r = requests.get("https://reglementaction.com/verdissement-du-stationnement/")
soup = BeautifulSoup(r.content)
Puis le transformer avec le modèle et faire semantic_search
sur les
règlements:
tvec = model.encode(soup.text)
for n in st.util.semantic_search(tvec, embeddings)[0][0:4]:
neighbour_idx = n["corpus_id"]
score = n["score"]
print(score, idxart[neighbour_idx])
print(textwrap.fill(articles[idxart[neighbour_idx]].strip()[0:500]), "...")
print()
Et on trouve des dispositions potentiellement intéressantes dans les règlements:
0.413027286529541 export/Rgl-1314-2021-Z-en-vigueur-20240823/Article/188/index.html
Article 188 Compensation de la surface végétalisée minimale par des
espaces de stationnements perméables La surface d’une portion d’un
espace de stationnement situé à l’intérieur du périmètre urbain conçu
à l’aide de pavés alvéolés ou de gazon renforcé avec dalle alvéolée
peut être comptabilisée dans le calcul de la surface végétale minimale
par un ratio de 50% (exemple : 100 m2 de stationnement en dalle
alvéolé peut donc représenter un crédit de 50 m2 de surface
végétalisée). La surface ainsi ...
0.38985273241996765 export/Rgl-1314-2021-Z-en-vigueur-20240823/Article/267/index.html
Article 267 Ilot de verdure Un espace de stationnement hors rue
extérieur comportant 20 cases ou plus doit être aménagé de façon à ce
que toute série de 20 cases de stationnement adjacentes soit isolée
par un îlot de verdure conforme aux dispositions suivantes : Un îlot
de verdure doit respecter les dimensions suivantes : une largeur
minimale de 2 mètres; une superficie minimale de 25 mètres carrés pour
les cases aménagées en rang double, soit dos-à-dos; une superficie
minimale de 13 mètres car ...
0.3851878046989441 export/RUD_T6_VR/Article/6.1.2.6/index.html
Article 6.1.2.6 Agrandissement majeur d’un bâtiment Un agrandissement
majeur représentant 25 % ou plus de la superficie de plancher du
bâtiment principal, mais moins de 100 % de cette superficie peut être
réalisé malgré l’existence d’un aménagement d’une aire de
stationnement dérogatoire pourvu que les conditions suivantes soient
respectées : 1° dans le cas d’une aire de stationnement extérieure de
15 cases et plus, l’aire de stationnement doit être réaménagée de
manière à respecter les exigenc ...
0.3690324127674103 export/RUD_T6_VR/Article/6.4.4.1/index.html
Article 6.4.4.1 Revêtement d’une aire de stationnement Une aire de
stationnement extérieure doit être recouverte par l’un des matériaux
de revêtement suivants qui peuvent être perméables ou non, le cas
échéant : 1° l’asphalte; 2° le béton; 3° le pavé d e béton; 4° le pavé
de béton avec alvéoles végétalisées ou remplies de poussières de roche
ou de pierres concassées. Une aire de stationnement située dans une
zone de type T1, T2 ou ZP.1 et ZP.3 peut être recouverte de gravier. ...
On peut également extraire les règlements modèles de la page et
chercher les articles similaires (ce qui marche mieux lorsqu’on a une
plus grande collection de règlements).
Conclusion
J’ai fait un survol très rapide de ce qu’on est capable de faire avec
une extraction sémantique et structurée du texte d’un PDF de
règlement, et comment c’est facile de faire des analyses en utilisant
SentenceTransformers.
Dans des futurs billets on regardera l’agglomération des articles, la
possibilité d’entraîner un modèle spécifique pour ce domaine, ainsi
que la possibilité de faire une comparaison quantitative entre
règlements pour des critères spécifiques par rapport à
l’environnement.