Extraction de données structurées avec des LLMs
Le terme même d’ “extraction de données structurées” est en réalité assez générique et recouvre des dispositifs très diversifiés selon le contexte dans lesquels ils sont mis en oeuvre : cela peut aller de la “simple” Regex utilisée dans un script, à des techniques de NLP pour l’extraction d’entités nommées appliquée aux humanités numériques par exemple, ou encore des processus de récupération de données sur le web par scraping, sans parler de procédés automatisés d’extraction de sources diverses avec des outils de type ETL. D’une certaine manière, en élargissant le concept, la définition même de l’extraction de données structurées comme processus de récupération manuelle ou automatisée de données depuis différentes sources matérielles ou numériques et leur conversion en format utilisable (tabulaire à minima) voire standardisé (Marc, Dublin Core) dans des formats de sérialisation ouverts (xml, json) pourrait même être une des définitions du métier de bibliothécaire ;). Les LLMs, avec leurs capacités de compréhension du langage naturel via les représentations vectorielles sémantisées de données textuelles non structurées sur lesquelles ils s’appuient pour la génération de contenus, sont donc “nativement” dotés de capacités d’extraction “intelligente” et entrent dorénavant dans le champ des outils mobilisables pour ce type d’opération.
Ceci étant posé, comment concrètement structurer de l’information à partir de données non structurées avec un LLM ?
Préparation
L'exemple textuel qui servira de fil rouge dans ce billet est l'abstract en anglais et français de cette thèse de doctorat.
user_prompt_en = """
Text: Official painter of the Navy and founder of the French artists colonial society, Louis Jules Dumoulin (1860-1924) has travelled the world.
Between 1888, year of his first long travel and 1897, he carried out two official missions in Asia leading him to stay three times in Japan,
but also in others countries like China and French Indochina, a territory that will turn this patriot into a convinced and militant colonialist.
Prolific artist with a certain influence during his lifetime, Dumoulin has delivered many paintings inspired by his travels and executed
from in situ drawings or photographic models selected from his rich personal collection.
Despite the fact that he was one of the first French painters to have made the trip to Japan and despite the large amount of works inspired by this country in his production,
he remains in the little notoriety that the history of art gives him exclusively a colonial painter.Result of fundamental research on a figure not much studied,
this thesis is based on historical documents, but also on original primary sources including the photographic collection of more than a thousand photographs
acquired or taken by Dumoulin and annotated by him. One of its goal is to question the role played by the Far East and especially Japan
in the life and works of this artist and his place in late 19th-century Japonisme.
It also aims, according to an imagological and comparative approach with Dumoulin’s contemporary artists, intellectuals and writers textual and pictorial testimonies,
to analyze in his works and writings representations of Japan, of its people and its political and social evolution as well as the reception of his production in France and Japan.
"""
user_prompt_fr = """
Peintre officiel de la marine et fondateur de la société coloniale des artistes français, Louis Jules Dumoulin (1860-1924) a parcouru le monde.
Entre 1888, date de son premier voyage au long cours et 1897, il effectue deux missions officielles en Asie qui le conduiront à séjourner à trois reprises au Japon
mais aussi dans plusieurs autres pays dont la Chine et surtout l’Indochine française, territoire qui fera de ce patriote un colonialiste convaincu et militant.
Artiste prolifique et jouissant d’une influence certaine de son vivant, Dumoulin a livré de nombreux tableaux inspirés de ces voyages et exécutés à partir d’études in situ ou de modèles photographiques
puisées dans sa riche collection personnelle. Malgré le fait qu’il soit l’un des premiers peintres français à avoir effectué le voyage jusqu’au Japon et en dépit de la grande quantité d’œuvres inspirées
par ce pays dans sa production, il reste dans le peu de notoriété que lui accorde l’histoire de l’art exclusivement un peintre colonial. Fruit de recherches fondamentales au sujet d’un personnage très peu étudié,
le présent mémoire s’appuie sur des documents d’époque mais aussi sur des sources primaires inédites dont la collection photographique de plus d’un millier de clichés acquis ou pris par Dumoulin et annotés de sa main,
afin d’interroger le rôle qu'a joué l’Extrême-Orient et plus particulièrement le Japon dans la vie et l'œuvre de cet artiste et sa place dans le japonisme de la fin du XIXe siècle. Elle vise également,
selon une approche imagologique et comparatiste avec des témoignages écrits et picturaux d’artistes, d’intellectuels et d’écrivains contemporains de Dumoulin, à analyser dans l’œuvre et les écrits de ce dernier
les représentations du Japon, de son peuple et de son évolution politique et sociale ainsi que la réception de sa production en France et au Japon.
"""
Les librairies nécessaires sont :
!pip install openai transformers
Et pour réaliser les opérations d'inférence avec le client OpenAI, on utilise les LLMs hébergés par Groq via son serveur d'API, qui nécessite une clé d'API (gratuite).
Le plus simple (mais le moins fiable) : avec un prompt
A partir de ces résumés, demandons à un LLM (ici llama3-8b-8192) d’en extraire les principales entités et leurs attributs et de délivrer l’output en format Json. Nous utilisons ici la technique de prompt la plus simple dite zero-shot learning, c’est-à dire avec une instruction sans contexte ou exemples supplémentaires.
import json
from openai import OpenAI
system_prompt = """
Your task is to take the unstructured text provided and convert it into a well-organized table format using JSON. Identify the main entities, attributes, or categories mentioned in the text and use them as keys in the JSON object. Then, extract the relevant information from the text and populate the corresponding values in the JSON object. Ensure that the data is accurately represented and properly formatted within the JSON structure. The resulting JSON table should provide a clear, structured overview of the information presented in the original text.
"""
client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=<groq_api_key>)
response = client.chat.completions.create(
model = 'llama3-8b-8192',
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt_en},
],
response_format={ "type": "json_object" },
temperature=0.7,
top_p=0.7,
max_tokens=1024,
stream= False
)
print(response.choices[0].message.content)
Réponse
{
"Artist": {
"Name": "Louis Jules Dumoulin",
"Birth": 1860,
"Death": 1924,
"Occupation": "Official painter of the Navy and founder of the French artists colonial society"
},
"Travel": {
"Years": ["1888", "1897"],
"Countries": ["Japan", "China", "French Indochina"],
"Number of visits to Japan": 3
},
"Artwork": {
"Style": "Prolific artist with a certain influence during his lifetime",
"Inspiration": "Travels and in situ drawings or photographic models",
"Collection": "Rich personal collection"
},
"Legacy": {
"Notoriety": "Little notoriety in the history of art",
"Label": "Colonial painter"
},
"Research": {
"Methodology": "Fundamental research on a figure not much studied",
"Sources": ["Historical documents", "Original primary sources", "Photographic collection of over a thousand photographs"],
"Goals": ["Question the role of the Far East and Japan in the life and works of Dumoulin", "Analyze representations of Japan, its people, and its evolution in Dumoulin's works and writings"]
},
"Comparative approach": {
"Methodology": "Imagological and comparative approach with Dumoulin's contemporary artists, intellectuals, and writers",
"Goals": ["Analyze representations of Japan, its people, and its evolution in Dumoulin's works and writings", "Compare with the reception of his production in France and Japan"]
}
}
Même si la réponse est plutôt complète et bien structurée, avec ce type de prompt l’utilisateur ne contrôle pas finement le type d’informations extraites. Pour y remédier on peut passer un prompt un peu plus élaboré afin de mieux guider le modèle dans la génération de l’output attendu, par exemple avec du one-shot learning pour obtenir des entités nommées
system_prompt = """
You are a named entities extractor and your task is to take the unstructured text provided and convert it into a well-organized array format using JSON. Identify each named person and place entity mentioned in the text and use it as key-value in the JSON object. The returned Json array must then look like : [ {'person': 'the person full name'}, {'place': 'the place full name'} ]
"""
Réponse
[
{"person": "Louis Jules Dumoulin"},
{"place": "Japan"},
{"place": "China"},
{"place": "French Indochina"}
]
Une autre façon d'ajuster encore mieux le processus d’extraction souhaité, sans par ailleurs alourdir le prompt avec du few-shot learning, consiste à associer des appels de fonctions à l’API de chat completion. L’introduction des fonctions (dépréciées) et maintenant des “tools” (sets de fonctions requêtables en une seule fois) dans les requêtes aux LLMs OpenAI-like permet d’augmenter les capacités génératives des modèles en permettant de décrire des consignes de manière structurée ou de se connecter à des systèmes externes.
Attention cependant, cette fonctionnalité n’est utilisable qu’avec des LLM préalablement finetunés pour la détection de fonctions (les modèles GPT d’OpenAI, et les modèles dont le nom est en général suffixés “tool”, comme par exemple llama3-groq-8b-8192-tool-use-preview).
En ajoutant une fonction d'instruction avec le schéma de données suivant :
{
"person": {
"type": "string",
"description": "Name of the person",
"birthdate": {
"type": "string",
"description": "Date of birth"
},
"deathdate": {
"type": "string",
"description": "Date of death"
}
},
"place": {
"type": "string",
"description": "Name of the place"
},
"date": {
"type": "string",
"description": "Year of the event"
},
"event": {
"type": "string",
"description": "Name of the event"
}
}
Code
import json
from openai import OpenAI
tools=[
{
"type": "function",
"function": {
"name": "extract_entities_info",
"description": "Get named entities with some attributes from the body of the input text",
'parameters': {
'type': 'object',
'properties': {
'person': {
'type': 'string',
'description': 'Name of the person',
'birthdate': {
'type': 'string',
'description': 'Date of birth'
},
'deathdate': {
'type': 'string',
'description': 'Date of death'
}
},
'place': {
'type': 'string',
'description': 'Name of the place'
},
'date': {
'type': 'string',
'description': 'Year of the event'
},
'event': {
'type': 'string',
'description': 'Name of the event'
},
}
}
}
},
]
system_prompt = """
Your task is to take the unstructured text provided and convert it into a well-organized table format using JSON.
Use the supplied tools to assist the user.
"""
client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=<groq_api_key>)
response = client.chat.completions.create(
model = 'llama3-groq-8b-8192-tool-use-preview',
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt_en},
],
tools = tools,
tool_choice = 'auto'
)
result = []
for f in response.choices[0].message.tool_calls:
result.append(json.loads(f.function.arguments))
print(json.dumps(result, indent=2))
le LLM génère ce résultat (extrait) :
[
{
"date": "1860",
"event": "birth",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"date": "1924",
"event": "death",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"date": "1888",
"event": "first long travel",
"person": {
"name": "Louis Jules Dumoulin"
}
},
...
]
Résultat complet
[
{
"date": "1860",
"event": "birth",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"date": "1924",
"event": "death",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"date": "1888",
"event": "first long travel",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"date": "1897",
"event": "second long travel",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"place": "Japan",
"event": "visit",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"place": "China",
"event": "visit",
"person": {
"name": "Louis Jules Dumoulin"
}
},
{
"place": "French Indochina",
"event": "visit",
"person": {
"name": "Louis Jules Dumoulin"
}
}
]
Que conclure des résultats des exemples précédents, notamment si l'on s'amuse à rejouer ces tests plusieurs fois ? Qu’ils sont de nature diverse et présentent une qualité et un respect des consignes plus ou moins satisfaisants selon la technique employée : en effet, non seulement la capacité de détection d'entités et de concepts sémantiquement plus ou moins complexes dépend étroitement du LLM employé et de son paramétrage (température, tok-k, top-p...), mais on se rend compte également qu'ils illustrent bien le caractère intrinsèquement non déterminisme de la génération de texte par les LLMs, qui par construction n'opèrent pas forcément les mêmes calculs vectoriels pour un prompt identique, ce qui pose problème si l'objectif est d' automatiser la génération de données structurées sur des séries d’inputs. Autre remarque : mobiliser un LLM de plusieurs billions de paramètres pour ce genre de tâches peut se révéler peu judicieux car : 1. coûteux financièrement en cas d’utilisation de LLM payants à l’usage ; 2. quoi qu’il en soit, coûteux en terme de ressources et d’infrastructures mobilisées.
Précision importante : dans le cadre d’un objectif de mise en production, ces quelques tests ne représenteraient que la première étape de sélection rapide d’une stratégie, qui devrait être suivie de la mise en place d’un pipeline de validation manuelle afin d’évaluer de manière plus robuste la précision du modèle dans le cadre de la tâche à réaliser, puis potentiellement, si le LLM zero-shot n’est toujours pas suffisamment précis, de réutiliser le pipeline de validation existant pour annoter des données supplémentaires afin de peaufiner un modèle plus petit.
Le plus efficace : utiliser un Small Language Model (SLM) déjà pré-entraîné ou finetuné pour l’extraction de données : exemple du Named Entity Recognition (NER)
On trouve sur HuggingFace un certain nombre de petits modèles produits par finetuning du modèle Bert de Google pour les tâches de reconnaissance d’entités nommées et donc prêts à l’emploi pour leur récupération depuis des données textuelles (pour rappel Bert est un modèle de langage encoder-only, c’est-à dire n’utilisant que la partie encodeur du réseau Transformer, et bidirectionnel c’est-à dire appliquant le concept de l'attention sur les contextes précédents et suivants d'un token). En plus de leur légèreté et leur simplicité d’utilisation avec la librairie transformers d’HuggingFace, l’avantage des familles de modèles dérivés de Bert provient aussi de la création au fil du temps de modèles entraînés et déclinés en fonction de la langue, ainsi peut-on recourir pour la langue française aux modèles des familles FlauBERT ou CamemBERT par exemple.
Voici un exemple de code simplifiée pour récupérer des entités nommées :
from transformers import AutoTokenizer, AutoModelForTokenClassification
from transformers import pipeline
from huggingface_hub import login
login(token = <huggingface_token>)
tokenizer = AutoTokenizer.from_pretrained("Jean-Baptiste/camembert-ner")
model = AutoModelForTokenClassification.from_pretrained("Jean-Baptiste/camembert-ner")
nlp = pipeline('ner', model=model, tokenizer=tokenizer, aggregation_strategy="simple")
entities_fr = nlp(user_prompt_fr)
Code complet
from transformers import AutoTokenizer, AutoModelForTokenClassification
from transformers import pipeline
from huggingface_hub import login
login(token = <huggingface_token>)
tokenizer = AutoTokenizer.from_pretrained("Jean-Baptiste/camembert-ner")
model = AutoModelForTokenClassification.from_pretrained("Jean-Baptiste/camembert-ner")
nlp = pipeline('ner', model=model, tokenizer=tokenizer, aggregation_strategy="simple")
entities_fr = nlp(user_prompt_fr)
entity_groups_fr = defaultdict(list)
for entity in entities_fr:
entity_groups_fr[entity['entity_group']].append(entity)
def convert_to_serializable(obj):
if isinstance(obj, (dict, list)):
return [convert_to_serializable(v) for v in obj] if isinstance(obj, list) else {k: convert_to_serializable(v) for k, v in obj.items()}
if isinstance(obj, (np.float32, float, int)): # Explicitly handle np.float32
return float(obj)
return obj
serializable_entity_groups_fr = convert_to_serializable({k: [dict(e) for e in v] for k, v in dict(entity_groups_fr).items()})
print(json.dumps(serializable_entity_groups_fr, indent=2))
Résultat (extrait) :
{
"PER": [
{
"entity_group": "PER",
"score": 0.9979883432388306,
"word": "Louis Jules Dumoulin",
"start": 90.0,
"end": 111.0
},
{
"entity_group": "PER",
"score": 0.9989939332008362,
"word": "Dumoulin",
"start": 543.0,
"end": 552.0
},
{
"entity_group": "PER",
"score": 0.9989197850227356,
"word": "Dumoulin",
"start": 1278.0,
"end": 1287.0
},
...
]
}
Résultat complet
{
"PER": [
{
"entity_group": "PER",
"score": 0.9979883432388306,
"word": "Louis Jules Dumoulin",
"start": 90.0,
"end": 111.0
},
{
"entity_group": "PER",
"score": 0.9989939332008362,
"word": "Dumoulin",
"start": 543.0,
"end": 552.0
},
{
"entity_group": "PER",
"score": 0.9989197850227356,
"word": "Dumoulin",
"start": 1278.0,
"end": 1287.0
},
{
"entity_group": "PER",
"score": 0.9989628791809082,
"word": "Dumoulin",
"start": 1663.0,
"end": 1672.0
}
],
"LOC": [
{
"entity_group": "LOC",
"score": 0.9815301299095154,
"word": "Asie",
"start": 248.0,
"end": 253.0
},
{
"entity_group": "LOC",
"score": 0.9907000660896301,
"word": "Japon",
"start": 303.0,
"end": 309.0
},
{
"entity_group": "LOC",
"score": 0.9924607872962952,
"word": "Chine",
"start": 356.0,
"end": 362.0
},
{
"entity_group": "LOC",
"score": 0.8565614223480225,
"word": "Indochine fran\u00e7aise",
"start": 376.0,
"end": 395.0
},
{
"entity_group": "LOC",
"score": 0.9951086640357971,
"word": "Japon",
"start": 818.0,
"end": 824.0
},
{
"entity_group": "LOC",
"score": 0.9949280619621277,
"word": "Extr\u00eame-Orient",
"start": 1350.0,
"end": 1364.0
},
{
"entity_group": "LOC",
"score": 0.7683209180831909,
"word": "le Japon",
"start": 1389.0,
"end": 1398.0
},
{
"entity_group": "LOC",
"score": 0.9920973777770996,
"word": "Japon",
"start": 1749.0,
"end": 1755.0
},
{
"entity_group": "LOC",
"score": 0.9906196594238281,
"word": "France",
"start": 1854.0,
"end": 1861.0
},
{
"entity_group": "LOC",
"score": 0.993614673614502,
"word": "Japon",
"start": 1867.0,
"end": 1873.0
}
]
}
Autre possibilité intéressante à mentionner, Gliner est aussi un LLM inspiré des modèles Bert dédié à l’extraction d’entités nommées mais qui se distingue des autres modèles de NER en laissant la possibilité à l’utilisateur de déterminer les types d’entités à extraire.
!pip install gliner
from gliner import GLiNER
model = GLiNER.from_pretrained("urchade/gliner_mediumv2.1")
labels = ["person", "place", "date", "event"]
entities = model.predict_entities(user_prompt_en, labels, threshold=0.5)
result = []
for entity in entities:
result.append({"entity_type": entity["label"], "value": entity["text"]})
print(json.dumps(result, indent=2))
Résultat (extrait) :
[
{
"entity_type": "person",
"value": "Louis Jules Dumoulin"
},
{
"entity_type": "date",
"value": "1860-1924"
},
{
"entity_type": "date",
"value": "1888"
},
...
]
Résultat complet
[
{
"entity_type": "person",
"value": "Louis Jules Dumoulin"
},
{
"entity_type": "date",
"value": "1860-1924"
},
{
"entity_type": "date",
"value": "1888"
},
{
"entity_type": "date",
"value": "1897"
},
{
"entity_type": "place",
"value": "Asia"
},
{
"entity_type": "place",
"value": "Japan"
},
{
"entity_type": "place",
"value": "China"
},
{
"entity_type": "place",
"value": "French Indochina"
},
{
"entity_type": "place",
"value": "Japan"
},
{
"entity_type": "place",
"value": "Far East"
},
{
"entity_type": "place",
"value": "Japan"
},
{
"entity_type": "place",
"value": "Japan"
},
{
"entity_type": "place",
"value": "Japan"
}
]
Bien... mais si l’on souhaite extraire d’autres types de métadonnées que des entités nommées ? Si l’on souhaite construire son propre extracteur avec un SLM en paramétrant à la fois le type d’informations à extraire et le schéma de structuration de l’output ?
Le plus efficace et le plus flexible (au sens de généralisable) : avec un SLM déjà pré-entraîné ou finetuné pour l’extraction de données, mais sans schéma de données pré-déterminé.
Le modèle de fondation text-to-json NuExtract créé par NuMind permet précisément de réaliser ce type de tâches en transformant des textes en données structurées au format JSON selon un template json personnalisé passé en input en même temps que le texte.
NuExtract existe en 3 variantes selon le nombre de paramètres, chacune d'entre elles est le résultat du finetuning d’un SLM avec un dataset synthétique issu de l’annotation de séquences de texte par un LLM tierce (GPT-4 et Llama3 70b). - NuExtract-tiny-v1.5 à 500M de paramètres : finetuning de Qwen/Qwen2.5-0.5B - NuExtract-v1.5 à 3,8B de paramètres : finetuning de Phi-3.5-mini-instruc - NuExtract-large à 7B de paramètres : finetuning de phi-3-small
Tous les détails techniques sont explicités dans ces séries de billets : https://numind.ai/blog
Au final, Nuextract est donc un modèle de fondation multilingue, polyvalent et généralisable (agnostique des spécificités d’un domaine métier) et qui permet d’envisager le processus d’extraction comme la brique d’un pipeline automatisé dans la mesure où la possibilité de passer en input le template json souhaité garantit l’homogénéité et la normalisation de l’output. Pour tester NuExtract, on le trouve bien évidemment sur HuggingFace, accompagné d’un playground.
Pour l’utiliser en local, voici un exemple avec la librairie transformers et ce template de sortie souhaité :
schema = """{
"Persons": [{
"Name": "",
"birthdate": "",
"deathdate": "",
"occupation": "",
"nationality": ""
}],
"Places": [],
"Sources":[],
"Events": []
}
"""
Code complet
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("numind/NuExtract-tiny", trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained("numind/NuExtract-tiny", trust_remote_code=True)
schema = """{
"Persons": [{
"Name": "",
"birthdate": "",
"deathdate": "",
"occupation": "",
"nationality": ""
}],
"Places": [],
"Sources":[],
"Events": []
}
"""
schema = json.dumps(json.loads(schema), indent=4)
input_llm = "<|input|>\n### Template:\n" + schema + "\n" + "### Text:\n"+user_prompt_en +"\n<|output|>\n"
input_ids = tokenizer(input_llm, return_tensors="pt", truncation=True, max_length=4000)
output = tokenizer.decode(model.generate(**input_ids)[0], skip_special_tokens=True)
output.split("<|output|>")[1].split("<|end-output|>")[0]
print(output)
Résultat
{
"Persons": [
{
"Name": "Louis Jules Dumoulin",
"birthdate": "1860",
"deathdate": "1924",
"occupation": "official painter of the Navy and founder of the French artists colonial society",
"nationality": "French"
}
],
"Places": [
"Japan",
"China",
"French Indochina"
],
"Sources": [
"historical documents",
"original primary sources including the photographic collection of more than a thousand photographs"
],
"Events": [
"two official missions in Asia"
]
}
Ou plus simplement en le téléchargeant dans une instance Ollama afin de bénéficier d’un backend d’API
Code complet
!ollama pull nuextract:3.8b
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="whatever")
schema = """{
"Persons": [{
"Name": "",
"birthdate": "",
"deathdate": "",
"occupation": "",
"nationality": ""
}],
"Places": [],
"Sources":[],
"Events": []
}
"""
prompt = f"<|input|>### Template:\n{schema}\n### Text:\n{user_prompt_en}<|output|>"
response = client.completions.create(
model = 'nuextract:3.8b',
prompt = prompt,
response_format={ "type": "json_object" },
stream= False
)
print(response.choices[0].message.content)
En plus du contrôle sur la structuration du contenu généré même en mode zero-shot comme illustré ici, un autre avantage non négligable de NuExtract est de pouvoir aussi être finetuné avec une application de bureau no-code dédiée qui permet par un processus d’annotation manuelle itératif de lancer en temps réel de l'apprentissage machine afin d'ajuster les prédictions du modèle et au final de finetuner un nouveau modèle, déployable via un serveur d’API intégré à l’application. Avec ce procédé, on peut donc créer son propre dataset d'entraînement et ajuster NuExtract sur ses données métiers afin de créer son propre LLM prêt à être déployé et requêtable dans son workflow de données.
Application Numind
A noter que outre la détection d'entités, l'application permet de réaliser des tâches de classification mono ou multilabels et produit des statistiques d'évaluation de la performance des modèles finetunés.
Pistes d'exploitation
Dans un contexte documentaire, on pourrait envisager l’usage (et/ou le finetuning) de tels modèles de langue à la fois légers, généralistes et pré-entraînés pour la production automatisée de métadonnées structurées à plusieurs titres :
- pour l’extraction de métadonnées bibliographiques “classiques”, à partir de fulltext de publications lors du dépôt dans une archive ouverte par exemple. Un peu en avance sur le contenu de la suite du billet concernant l'extraction de données avec des VLM, voici par exemple ce que donne une requête d'inférence avec le modèle de vision OpenGVLab/InternVL2-40B sur un screenshot de preprint sur Arxiv et le simple prompt suivant : "Extracts the text from this image and convert it into a well-organized table format using JSON"
| Image | Data |
|---|---|
![]() |
![]() |
- mais aussi, pourquoi pas, pour l’extraction de nouvelles métadonnées qui viendraient enrichir les descriptions bibliographiques standards par de nouvelles métadonnées (donc potentiellement de nouveaux index de recherche), sur des corpus spécifiques tels que les corpus de thèses (qui d’ailleurs, et à de multiples points de vue, constituent des corpus tout à fait pertinents pour l’expérimentation de processus d’IA). Depuis des abstracts de thèses, on pourrait ainsi envisager d’extraire des métadonnées liées aux protocoles de recherche, résultats attendus, résultats obtenus… , et ce d’autant que les usages scientifiques communément respectés de rédaction de ces contenus leur confèrent une sorte de structuration sémantique implicite qui facilite le travail du LLM

Pour aller plus loin : les Vision Language Models (VLM) dans la boucle
De manière générale, les VLMs ajoutent une couche de sophistication supplémentaire en combinant simultanément le traitement visuel des données avec la compréhension du langage, rendant cette approche et cette architecture multimodale particulièrement avantageuse pour apprendre à associer des informations issues des modalités image et texte et pour extraire des données à partir d’images ou de sources comprenant à la fois du textes et des images. Techniquement les VLM associent plusieurs types de réseaux de neurones afin de visuellement tokeniser et encoder les images en embeddings et dans le même temps de générer des tokens textuels correspondant au contenu de l’image, reliant ainsi les 2 types de représentation sémantique dans le même espace. Un VLM est donc calibré pour intégrer des inputs visuels puis pour générer des descriptions textuelles, répondre à des questions, extraire des données ou créer des images basées sur des descriptions textuelles.
Les VLM connaissent un rythme d’innovation extrêmement rapide, et sont probablement en train de renouveler complètement les techniques d’OCR classiques, voire de HTR. Voir par exemple https://github.com/Ucas-HaoranWei/GOT-OCR2.0 ou https://huggingface.co/blog/manu/colpali.
D’ailleurs les applications de RAG intégrant des fichiers pdf utilisent de plus en plus des stratégies de conversion des pdf au format image puis d’extraction du texte depuis les images pour générer les embeddings plutôt que d’obtenir les contenus textuels en parsant directement les pdf, la précision des résultats étant bien meilleure dans le 1er cas.
Les modèles de vision sont évidemment pléthore sur HuggingFace, et concernant les statégies d’extraction de données structurées, on retrouve bien sûr avec cette typologie de modèle le même paradigme qu’avec les LLM, la différence étant que le spectre des usages possibles pour la production de métadonnées classiques (type indexation) ou enrichies s'étend de fait à des contenus visuels (de bibliothèque numérique par exemple).
En plus de l’exemple mentionné ci-dessus à propos de l’extraction de métadonnées bibliographiques à partir d’une publication, les VLM peuvent donc être utilisés pour la description textuelles d’images, puis pourquoi pas couplé à un LLM pour la récupération de données structurées.
A partir de la description d'une image
Source image https://humazur.univ-cotedazur.fr/iiif-img/22952/full/1000,644/0/default.jpg et VLM Ovis1.6-Gemma2-9b
On obtient ainsi facilement de nouvelles métadonnées descriptives

Pour ceux que cela intéresse, ce procédé de production de contenus sur des collections de photographies à des fins d'exploration et de curation est étudié et documenté de manière assez extensive dans le cadre des travaux du Distant Viewing Lab.
Et pour conclure, comment passer à côté de la génération automatisée de notices Marc à partir d'une photo de page de titre voire même de screenshot depuis Amazon (sans recourir aux API payantes sur des LLMs multimodaux type GPT-4o ou Gemini 1.5 Flash) ?
Le code de l'application est accessible ici




