Penser avant de cataloguer : entraîner un LLM de raisonnement sur des métadonnées bibliographiques

Ce billet présente un exemple de mobilisation de modèles d'IA générative pour automatiser la production de notices bibliographiques en Unimarc/XML dans un contexte de génération assistée par IA de métadonnées documentaires.

Dans l'esprit, cette expérimentation se rapproche du AI Metadata Assistant introduit dans Alma par Ex Libris dans le but affiché de simplifier et réduire le temps dédié au catalogage manuel des documents par le recours à l'IA pour la création et l'enrichissement des métadonnées structurées.

... Dans l'esprit seulement, car outre le problème de gouvernance (cet assistant s'appuie en fait sur GPT 4o) et le manque à ce jour de transparence du dispositif (aucun prompt ni code publié, aucune documentation technique sur "l'entraînement" du modèle...), le problème induit par ce type d'outillage intégré dans les logiciels de gestion des bibliothèques est d'embarquer de fait les bibliothécaires dans un type d'usage de l'IA sans contrôle à priori ni sur le modèle utilisé, ni sur les modalités ou le périmètre applicatifs.

Or, par exemple, si elle peut paraître triviale dans le cas du catalogage de ressources courantes, l'assistance d'un modèle d'IA spécialisé et spécifiquement entraîné pourrait prendre tout son sens dans un contexte local de catalogage de ressources spécifiques (thèses, livres anciens...) ou d'opérations massives de type rétroconversion, auquel cas les bibliothèques auraient tout intérêt à maîtriser de manière autonome l'écosystème technique du post-training et de l'inférence afin de développer des modèles au plus près de leurs besoins locaux, et accessoirement de se conformer à un usage raisonné de modèles de langage ouverts.

A titre d'illustration, la suite du billet décrit donc un exemple de fine-tuning d'un modèle de raisonnement léger et ouvert afin d'en faire un modèle expert en Unimarc déployable dans un pipeline de données ou une application exécutable sans GPU.

Les modèles de raisonnement

Les modèles dits de raisonnement sont des LLMs dotés de capacités de "réflexion" qui génèrent à la fois du texte et les chaînes de pensée ayant conduit à la production de ce texte. Cette capacité pour un modèle de langage à résoudre une tâche ou répondre à une question en la décomposant en étapes intermédiaires et en explicitant son cheminement de pensée jusqu'au résultat final constitue l'une des dernières avancées majeures dans le champ de la recherche menée par les laboratoires de pointe, Deepseek en tête (puisque c'est ce lab qui a rendu public et en open source les premiers modèles de raisonnement DeepSeek-R1-Zero et DeepSeek-R1).

L'augmentation du processus de génération par un processus de réflexion s'impose ainsi peu à peu comme un nouveau standard (en tant que niveau minimal requis) pour les grands LLM du marché qui, pour la plupart d'entre eux, proposent dorénavant des variantes dotées de capacités de raisonnement, et ceci selon des modalités diverses : que ce raisonnement fasse partie intégrante du texte généré, qu'il soit présenté dans une discussion séparée, qu'il reste caché pour l'utilisateur (l'API des modèles de la série o de GPT), voire même que le mode raisonnement puisse être activé ou désactivé à l'aide d'un simple paramètre dans le prompt (les modèles de la famille Qwen3).

Le moteur de cette approche tient aux recherches menées sur les LLMs qui démontrent que, pour la résolution de problèmes logiques ou complexes, l'activation du raisonnement pendant l'inférence (en gros, le modèle réfléchit avant de répondre, et fait état de ses réflexions) agit comme un levier de création d'une sorte d'espace cognitif à l'intérieur duquel le LLM peut appliquer ses connaissances préformées. Cette étape durant laquelle le modèle explore en autonomie son espace vectoriel augmente ainsi significativement la qualité des réponses, tout en rendant le processus génératif plus long certes, mais aussi plus explicite, donc évaluable et monitorable. L'augmentation du temps et des ressources dédiés à l'inférence apparaît donc non seulement comme une stratégie concurrente au pre-training des modèles pour l'amélioration de leur performance (https://arxiv.org/abs/2408.03314), mais aussi comme un moyen à faible coût d'améliorer les performances des Small Language Models (SLM) exécutés en local puisque par définition la structure du modèle n'est pas modifiée. Il est d'ailleurs très facile de tester soi-même ce procédé d'inference-time compute (ou test-time scaling ou encore Scaling LLM Test-Time Compute), c'est-à dire d'introduction de raisonnement au moment de l'inférence, par un simple prompt incitant le modèle à structurer sa "pensée" avant de formuler sa réponse.

Exemple de Chain of Thoughts (CoT) avec SmolLM2-1.7B-Instruct

system_prompt = """
Please answer the question using the following format by Chain-of-Thoughts, with each step clearly marked:

Let's solve this step by step:
<step number="1">
[First step of reasoning]
</step>
... (add more steps as needed)
<answer>
[Final answer]
</answer>
"""
user_prompt = """
The book's title is "El amor en los tiempos del cólera", and it lists "Gabriel García Márquez" as the author. 
The metadata indicates it's published in Paris, 2020, by Gallimard. 
What is the original language of this work?
"""

Réponse :

<step number="1">
The metadata indicates that the book is published in Paris, 2020, by Gallimard.
</step>

<step number="2">
The book's title, "El amor en los tiempos del cólera", is in Spanish.
</step>

<step number="3">
The author listed is Gabriel García Márquez, who is a Colombian writer.
</step>

<step number="4">
Since the book's title and author are in Spanish, and the metadata indicates it was published in Paris, the original language of this work is Spanish.
</step>

<answer>
The original language of this work is Spanish.
</answer>

On peut évidemment guider le modèle vers des modalités de réflexion plus ou moins complexes, incluant par exemple du backtracking (retour en arrière pour explorer d'autres pistes de solution), ou de la self-validation (vérification de ses propres résultats).

Exemple de Beam Search avec SmolLM2-1.7B-Instruct

system_prompt = """
"""
Please solve this question using Beam Search reasoning. For each step:
1. Explore multiple paths fully regardless of intermediate scores
2. Assign a score between 0 and 1 to each node based on how promising that step is
3. Calculate path_score for each result by summing scores along the path from root to result
4. The final choice will be based on the highest cumulative path score

<node id="root" score="[score]">
[Initial analysis - Break down the key aspects of the problem]
</node>

# First approach branch
<node id="approach1" parent="root" score="[score]">
[First approach - Outline the general strategy]
</node>

<node id="impl1.1" parent="approach1" score="[score]">
[Implementation 1.1 - Detail the specific steps and methods]
</node>

<node id="result1.1" parent="impl1.1" score="[score]" path_score="[sum of scores from root to here]">
[Result 1.1 - Describe concrete outcome and effectiveness]
</node>

<node id="impl1.2" parent="approach1" score="[score]">
[Implementation 1.2 - Detail alternative steps and methods]
</node>

<node id="result1.2" parent="impl1.2" score="[score]" path_score="[sum of scores from root to here]">
[Result 1.2 - Describe concrete outcome and effectiveness]
</node>

# Second approach branch
<node id="approach2" parent="root" score="[score]">
[Second approach - Outline an alternative general strategy]
</node>

<node id="impl2.1" parent="approach2" score="[score]">
[Implementation 2.1 - Detail the specific steps and methods]
</node>

<node id="result2.1" parent="impl2.1" score="[score]" path_score="[sum of scores from root to here]">
[Result 2.1 - Describe concrete outcome and effectiveness]
</node>

<node id="impl2.2" parent="approach2" score="[score]">
[Implementation 2.2 - Detail alternative steps and methods]
</node>

<node id="result2.2" parent="impl2.2" score="[score]" path_score="[sum of scores from root to here]">
[Result 2.2 - Describe concrete outcome and effectiveness]
</node>

<answer>
Best path (path_score: [highest_path_score]):
[Identify the path with the highest cumulative score]
[Explain why this path is most effective]
[Provide the final synthesized solution]
</answer>
"""
user_prompt = """
A book titled "Introduction to Quantum Mechanics" by David J. Griffiths covers topics such as wave functions, operators, and Schrödinger’s equation.
What Dewey Decimal number should it be assigned?
"""

Réponse

To determine the Dewey Decimal number for "Introduction to Quantum Mechanics" by David J. Griffiths, we need to follow these steps:

1. Identify the main subject of the book: Quantum Mechanics.
2. Identify the subtopics within Quantum Mechanics: Wave functions, operators, and Schrödinger's equation.
3. Assign Dewey Decimal numbers to each subtopic.
4. Calculate the Dewey Decimal number for the book by adding the Dewey Decimal numbers of the subtopics.

Step 1: Identify the main subject of the book: Quantum Mechanics.
Step 2: Identify the subtopics within Quantum Mechanics: Wave functions, operators, and Schrödinger's equation.
Step 3: Assign Dewey Decimal numbers to each subtopic.

Wave functions: 530
Operators: 530
Schrödinger's equation: 530

Step 4: Calculate the Dewey Decimal number for the book by adding the Dewey Decimal numbers of the subtopics.

Dewey Decimal number for the book = 530 + 530 + 530 = 1600

So, the Dewey Decimal number for "Introduction to Quantum Mechanics" by David J. Griffiths is 1600.

Cette réponse est très intéressante car elle illustre parfaitement le fait que malgré les apparences et le suivi des instructions, le modèle peut manipuler des concepts qu'il ne maîtrise pas, ou plutôt qu' en réalité il ne raisonne pas sur des concepts mais que sa réflexion s'inscrit dans le processus génératif token après token sans garantie de cohérence entre ses pensées et le resultat produit.

Ceci dit, et même si l'on peut suspecter que certains chatbots même parmi les plus utilisés fonctionnent ainsi avec cette forme d'inférence augmentée à la volée, la spécialisation de modèle sur des tâches assez complexes dont on peut estimer qu'elles seront mieux réalisées avec l'activation de chaînes de pensées, pose la question du passage de l'inference-time scaling par prompting au training-time scaling : autrement dit, comment passer de la mise à l'échelle explicite du temps d'inférence implémentée au niveau de l'application qui expose le LLM (API, chatbot...) au raisonnement intégré au sein du modèle lui-même ? Comment entraîner un modèle afin qu'il acquière "en dur" des capacités de raisonnement dans son processus génératif ?

Les techniques communément mises en œuvre à cette fin sont principalement :

  • Le fine-tuning supervisé par distillation, qui consiste à transférer les capacités de raisonnement d'un modèle expert vers un modèle "élève" plus petit à partir de traces de raisonnement générées par le modèle expert.
  • Des dispositifs de Reinforcement Learning (RL) dérivés du Reinforcement Learning from Human Feedback (RLFH) consistant à aligner le modèle sur des préférences humaines (qui répliquent l'apprentissage humain par essais-erreurs) grâce à des datasets manuellement annotés en amont. A noter que si l'on de dispose pas de tels jeux de données ou si le coût d'obtention de ces préférences est trop élevé, les diverses variantes de RL ci-dessous peuvent s'appuyer sur des données synthétiques :
    • Direct Preference Optimization (DPO) : le modèle est entraîné à partir de paires de Q/A comportant une réponse de bonne qualité et une autre de qualité inférieure afin qu’il apprenne à préférer systématiquement la meilleure des deux
    • Proximal Policy Optimization (PPO) : le modèle est entraîné à partir de paires de Q/A mais sans voir les réponses, il ajuste sa politique de génération en maximisant une fonction de récompense prédéfinie qui évalue la qualité des réponses en leur assignant un score, tout en limitant les changements brusques entre deux itérations grâce à une contrainte de proximité par rapport à la réponse "native" du modèle.
    • Group Relative Policy Optimization (GRPO) : le modèle est également entraîné à partir de paires de Q/A sans voir les réponses, il explore son espace vectoriel sans instructions spécifiques pour générer plusieurs réponses différentes par question, et est incité à préférer pour chacune d'entre elles la réponse obtenant le meilleur score calculé par des fonctions de récompenses vérifiables.

Jusqu'à DeepSeek-R1-Zero, l'apprentissage de type RL était généralement considéré comme une étape supplémentaire d'alignement intervenant après un fine-tuning supervisé (SFT), et c'est d'ailleurs une partie de l'innovation apportée par ce modèle d'avoir été exclusivement entraîné avec l'apprentissage par GRPO sans étape initiale de SFT. Dans ce type de training, le modèle a auto-appris à résoudre des problèmes sans voir d'exemples à partir desquels se généraliser au préalable, avec même un moment "Aha!" durant lequel le modèle a commencé spontanément à générer des traces de raisonnement dans le cadre de ses réponses, bien qu'il n'ait pas été explicitement formé pour le faire.

À partir de là, le RL a émergé comme une alternative efficace et évolutive au SFT pour le développement de LLM très performants dans des domaines ou tâches spécifiques. Cette solution est d'autant plus intéressante qu'elle nécessite beaucoup moins d'exemples par rapport à l'apprentissage par SFT (http://arxiv.org/abs/2504.20571), et qu'elle élimine le recours à des annotations humaines coûteuses à obtenir dans la mesure où le RL s'appuie sur des fonctions de récompense spécifiques prédéfinies. Les recherches sont très actives dans ce domaine afin d'optimiser les chaînes de raisonnement des modèles, avec par exemple des pistes consistant à :

  • Ajouter des contraintes de minimisation du nombre de tokens générés (budget reward, length-based reward), d'autant qu'il semblerait qu'il y ait une corrélation positive entre concision et précision de la réponse (http://arxiv.org/abs/2501.19393)
  • Supprimer les chaînes de pensée (CoT) de la sortie finale du modèle (http://arxiv.org/abs/2504.09858)
  • Injecter du RAG (Retrieval-Augmented Generation) durant l'apprentissage pour alimenter la réflexion (https://arxiv.org/abs/2503.09516)
  • Encourager l'exploration durant le processus de thinking en minimisant la supervision, par exemple en se passant de fonctions de récompenses (http://arxiv.org/abs/2505.04588)
  • Déporter tous les raisonnements dans l'espace latent du modèle afin d'exploiter au maximum les capacités intrinsèques des modèles (même des plus légers) tout en réduisant la place prise par les tokens relatifs aux chaînes de pensée intermédiaires dans la fenêtre de contexte du modèle, dans la mesure où ces tokens font aussi partie de la génération auto-régressive au même titre que l'output à proprement parler (https://arxiv.org/abs/2502.05171).

A noter : le mode raisonnement n'est pas forcément approprié pour tous les contextes d'usage d'un LLM, et peut même parfois être contre-productif car susceptible de conduire à de l'"overthinking" (suranalyse) et très gourmand en tokens pour des tâches "simples" comme du question-réponse en RAG par exemple.


Apprendre à penser l'Unimarc

Après avoir déjà abordé le transfert par distillation de capacités de raisonnement d'un modèle vers un autre pour apprendre à un SLM comment générer des métadonnées structurées en EAD/XML, on se propose ici de tester l'apprentissage par GRPO afin de créer un modèle expert en Unimarc capable de produire des notices conformes en Unimarc/XML à partir d'informations bibliographiques non structurées. Un cas d'usage possible pourrait ainsi être le développement d'une application qui associe un modèle de vision pour extraire les informations depuis des images de couverture, page de titre et 4ème de couverture, et le modèle spécifiquement fine-tuné pour produire la notice.


Ce billet ne traite que de la partie création du SLM expert, la première partie du cas d'usage étant assurée par un modèle de vision open source.


A l'instar des LLMs de raisonnement, abordons par étapes cette problématique de post-entraînement d'un modèle ouvert afin qu'il apprenne l'Unimarc et réfléchisse avant de générer des notices :

1. Quel modèle choisir ?

On opte pour un petit modèle à moins de 1 milliard de paramètres pouvant être exécuté pour de l'inférence sans GPU, ce qui aura aussi l'avantage, puisqu'a priori le modèle ne connaîtra pas l'Unimarc, de vraiment spécialiser le modèle sur ce format précis avec moins de risque d'hallucinations (le modèle qui "dériverait" dans son espace en produisant des connaissances fausses ou inadéquates, issues du Marc21 par exemple). On choisit également un modèle déjà doté de capacités de raisonnement, ce qui facilitera son adaptation à notre tâche spécifique. Notre choix s'est donc porté sur Qwen3-0.6B, qui offre un bon équilibre entre taille réduite et capacités de raisonnement.

2. Quel dispositif d'apprentissage mettre en place ?

Pour l'instant et en règle générale, les modèles de raisonnement sont plutôt centrés sur la résolution de problèmes logiques et/ou mathématiques (et d'ailleurs la plupart des tutoriels existants sur le RL sont illustrés avec des jeux de données relatifs à des résolutions de problèmes mathématiques). Cette orientation est facilitée par le fait que ce type de données fait en général déjà partie des données de pré-entraînement et que le modèle possède déjà des connaissances mathématiques.

Par contre, dans notre cas, Qwen3 n'ayant aucune connaissance préalable des formats Marc (ce que l'on peut vérifier en testant l'inférence avec une requête portant sur ce sujet), on peut intuitivement anticiper que partir sur du RL pur ne serait pas optimal, car les chances que le modèle devine la bonne structuration XML avec seulement des fonctions de récompense seraient trop faibles. Une solution consisterait à revenir aux cycles de post-entraînement classiques avec du SFT sur la base de jeux de données input-output pour introduire des connaissances en Unimarc, puis du RL pour inciter le modèle à raisonner sur la base de ces connaissances préalables, ou encore à du fine-tuning par distillation. Néanmoins on remarque que recourir au prompt formatting ("mise en forme de requête") sur Qwen3, en lui fournissant dans le prompt un template de la structure en xml attendue ainsi que des consignes strictes de mapping, suffit à guider le modèle pour qu'il produise de l'Unimarc tout à fait satisfaisant (de l'inference-time scaling guidé par du prompt templating en quelque sorte). On peut donc réutiliser cette faculté de généralisation à partir de consignes précises en structurant ainsi le prompt système dans l'auto-apprentissage par RL, ce qui augmentera le niveau du point de départ du raisonnement pour mieux converger vers une solution satisfaisante.

Voir le prompt système

# UNIMARC XML Record Generation Prompt

## Task Instructions

You are a bibliographic cataloging expert. Your task is to convert raw bibliographic metadata into a properly structured UNIMARC XML record. Follow the template and field mappings provided below to create a complete, valid UNIMARC record.

## Input Format
The user will provide bibliographic metadata in various formats (text, key-value pairs, or structured data). Extract and map each element to the appropriate UNIMARC field according to the mapping guide.

## Output Requirements
Generate a complete UNIMARC XML record using the template structure below, populating all available fields with the provided metadata.

---

## UNIMARC XML Template

<record>
    <leader> cam0 22 450 </leader>
    <controlfield tag="001">#{RECORD_ID}#</controlfield>
    <controlfield tag="003">#{RECORD_SOURCE_URL}#</controlfield>
    <controlfield tag="005">#{TIMESTAMP}#</controlfield>

    <!-- ISBN and Pricing Information -->
    <datafield tag="010" ind1=" " ind2=" ">
        <subfield code="a">#{ISBN}#</subfield>
        <subfield code="b">#{BINDING_TYPE}#</subfield>
        <subfield code="d">#{PRICE}#</subfield>
    </datafield>

    <!-- External Control Numbers -->
    <datafield tag="035" ind1=" " ind2=" ">
        <subfield code="a">#{OCLC_NUMBER}#</subfield>
    </datafield>

    <!-- Barcode/EAN -->
    <datafield tag="073" ind1=" " ind2="1">
        <subfield code="a">#{BARCODE}#</subfield>
    </datafield>

    <!-- General Processing Data -->
    <datafield tag="100" ind1=" " ind2=" ">
        <subfield code="a">#{PROCESSING_DATA}#</subfield>
    </datafield>

    <!-- Language Information -->
    <datafield tag="101" ind1="#{TRANSLATION_INDICATOR}#" ind2=" ">
        <subfield code="a">#{PRIMARY_LANGUAGE}#</subfield>
        <subfield code="c">#{ORIGINAL_LANGUAGE}#</subfield>
        <subfield code="2">#{LANGUAGE_SCHEME}#</subfield>
    </datafield>

    <!-- Country of Publication -->
    <datafield tag="102" ind1=" " ind2=" ">
        <subfield code="a">#{COUNTRY_CODE}#</subfield>
    </datafield>

    <!-- Content Type Information (RDA) -->
    <datafield tag="105" ind1=" " ind2=" ">
        <subfield code="a">a a 000yy</subfield>
    </datafield>

    <datafield tag="106" ind1=" " ind2=" ">
        <subfield code="a">r</subfield>
    </datafield>

    <!-- RDA Content/Media/Carrier Types -->
    <datafield tag="181" ind1=" " ind2=" ">
        <subfield code="6">z01</subfield>
        <subfield code="c">txt</subfield>
        <subfield code="2">rdacontent</subfield>
    </datafield>

    <datafield tag="181" ind1=" " ind2="1">
        <subfield code="6">z01</subfield>
        <subfield code="a">i#</subfield>
        <subfield code="b">xxxe##</subfield>
    </datafield>

    <datafield tag="182" ind1=" " ind2=" ">
        <subfield code="6">z01</subfield>
        <subfield code="c">n</subfield>
        <subfield code="2">rdamedia</subfield>
    </datafield>

    <datafield tag="182" ind1=" " ind2="1">
        <subfield code="6">z01</subfield>
        <subfield code="a">n</subfield>
    </datafield>

    <datafield tag="183" ind1=" " ind2="1">
        <subfield code="6">z01</subfield>
        <subfield code="a">nga</subfield>
        <subfield code="2">RDAfrCarrier</subfield>
    </datafield>

    <!-- Title and Statement of Responsibility -->
    <datafield tag="200" ind1="1" ind2=" ">
        <subfield code="a">#{MAIN_TITLE}#</subfield>
        <subfield code="e">#{SUBTITLE}#</subfield>
        <subfield code="f">#{AUTHORS_COLLECTIVE_STATEMENT}#</subfield>
        <subfield code="g">#{TRANSLATOR_STATEMENT}#</subfield>
    </datafield>

    <!-- Publication Information -->
    <datafield tag="214" ind1=" " ind2="0">
        <subfield code="a">#{PLACE_OF_PUBLICATION}#</subfield>
        <subfield code="c">#{PUBLISHER}#</subfield>
        <subfield code="d">#{PUBLICATION_DATE}#</subfield>
    </datafield>

    <!-- Physical Description -->
    <datafield tag="215" ind1=" " ind2=" ">
        <subfield code="a">#{EXTENT}#</subfield>
        <subfield code="c">#{ILLUSTRATIONS_DETAILS}#</subfield>
        <subfield code="d">#{DIMENSIONS}#</subfield>
    </datafield>

    <!-- Collection or series Description -->
    <datafield tag="225" ind1="0" ind2=" ">
        <subfield code="a">{COLLECTION_NAME}</subfield>
        <subfield code="v">{ISSUE_NUMBER}</subfield>
    </datafield>

    <!-- Collection or series Linking Information -->
    <datafield tag="410" ind1=" " ind2="|">
        <subfield code="0">{COLLECTION_AUTHORITY_ID}</subfield>
        <subfield code="t">{COLLECTION_NAME}</subfield>
        <subfield code="x">{COLLECTION_ISSN}</subfield>
        <subfield code="v">{ISSUE_NUMBER}</subfield>
    </datafield>

    <!-- Bibliography Note -->
    <datafield tag="320" ind1=" " ind2=" ">
        <subfield code="a">#{BIBLIOGRAPHY_NOTE}#</subfield>
    </datafield>

    <!-- Summary/Abstract -->
    <datafield tag="330" ind1=" " ind2=" ">
        <subfield code="a">#{ABSTRACT_SUMMARY}#</subfield>
        <subfield code="2">#{SUMMARY_SOURCE}#</subfield>
    </datafield>

    <!-- Variant Title -->
    <datafield tag="516" ind1="|" ind2=" ">
        <subfield code="a">#{SPINE_TITLE}#</subfield>
    </datafield>

    <!-- Subject Headings -->
    <datafield tag="606" ind1=" " ind2=" ">
        <subfield code="3">#{SUBJECT_AUTHORITY_ID}#</subfield>
        <subfield code="a">#{MAIN_SUBJECT}#</subfield>
        <subfield code="3">#{SUBDIVISION_AUTHORITY_ID}#</subfield>
        <subfield code="x">#{SUBJECT_SUBDIVISION}#</subfield>
        <subfield code="2">#{SUBJECT_SCHEME}#</subfield>
    </datafield>

    <!-- Dewey Classification -->
    <datafield tag="676" ind1=" " ind2=" ">
        <subfield code="a">#{DEWEY_NUMBER}#</subfield>
    </datafield>

    <!-- Main Author Entry -->
    <datafield tag="700" ind1=" " ind2="1">
        <subfield code="3">#{AUTHOR_AUTHORITY_ID}#</subfield>
        <subfield code="a">#{AUTHOR_SURNAME}#</subfield>
        <subfield code="b">#{AUTHOR_FORENAME}#</subfield>
        <subfield code="4">#{AUTHOR_ROLE_CODE}#</subfield>
    </datafield>

    <!-- Additional Author Entries (repeat as needed) -->
    <datafield tag="701" ind1=" " ind2="1">
        <subfield code="3">#{ADDITIONAL_AUTHOR_AUTHORITY_ID}#</subfield>
        <subfield code="a">#{ADDITIONAL_AUTHOR_SURNAME}#</subfield>
        <subfield code="b">#{ADDITIONAL_AUTHOR_FORENAME}#</subfield>
        <subfield code="4">#{ADDITIONAL_AUTHOR_ROLE_CODE}#</subfield>
    </datafield>

    <!-- Cataloging Source -->
    <datafield tag="801" ind1=" " ind2="3">
        <subfield code="a">#{CATALOGING_COUNTRY}#</subfield>
        <subfield code="b">#{CATALOGING_AGENCY}#</subfield>
        <subfield code="c">#{CATALOGING_DATE}#</subfield>
        <subfield code="g">#{CATALOGING_RULES}#</subfield>
    </datafield>
</record>

---

## Field Mapping Guide

### Essential Metadata Elements

| **Metadata Element**                | **UNIMARC/XML Tag** | **Subfield(s)**              | **Notes / Instructions**                                           |
|------------------------------------|----------------------|------------------------------|--------------------------------------------------------------------|
| **Title**                          | 200                  | $a                           | Main title of the work                                             |
| **Subtitle**                       | 200                  | $e                           | Subtitle or explanatory title                                      |
| **Statement of responsibility**    | 200                  | $f                           | All authors or contributors                                        |
| **Translator statement**           | 200                  | $g                           | Statement about translator(s)                                      |
| **Individual Authors**             | 700 / 701            | $a $b $3 $4 / $f $c          | Surname, forename, authority ID, role, full name and profession    |
| **Place of publication**           | 214                  | $a                           | City (use brackets if inferred)                                    |
| **Publisher**                      | 214                  | $c                           | Publisher name                                                     |
| **Publication date**               | 214                  | $d                           | DL date (format: DL YYYY)                                          |
| **Copyright date**                 | 214                  | $d                           | Same field as publication date                                     |
| **Imprint (printer info)**         | 214                  | $a $c                        | Place and name of printer                                          |
| **Edition**                        | 205                  | $a                           | Edition info in brackets                                           |
| **Physical description**           | 215                  | $a $c $d                     | Extent, illustrations, dimensions                                  |
| **ISBN (original)**                | 010                  | $a                           | ISBN 13 with hyphens                                               |
| **Binding**                        | 010                  | $b                           | Binding format (e.g., "br." for paperback)                         |
| **Price**                          | 010                  | $d                           | Price information                                                  |
| **Other identifier (ISBN no hyphens)** | 073              | $a                           | ISBN/Barcode without hyphens                                       |
| **OCLC number**                    | 035                  | $a                           | OCLC control number, e.g., (OCoLC)number                           |
| **Language**                       | 101                  | $a $2                        | ISO 639-2 language code and source                                 |
| **Original language**              | 101                  | $c                           | Original language if translated                                    |
| **Language scheme**                | 101                  | $2                           | Language code scheme                                               |
| **Country of publication**         | 102                  | $a                           | ISO country code (e.g., "FR")                                      |
| **Series title**                   | 225                  | $a                           | Series name                                                        |
| **Series number/volume**           | 225                  | $v                           | Number in series                                                   |
| **Series added entry**             | 410                  | $0 $t $x $v                  | Control number, full title, ISSN, volume                           |
| **Subject headings**               | 606, 608             | $a $x $3 $y $2               | Subjects, subdivisions, authority ID, geographic, source (RAMEAU) |
| **Classification (Dewey)**         | 676                  | $a $v                        | Dewey Decimal Classification number and edition                    |
| **Bibliography / Index note**      | 320                  | $a                           | Bibliography info or "Index"                                       |
| **Notes**                          | 303, 312             | $a                           | General notes from metadata                                        |
| **Summary / Abstract**             | 330                  | $a $2                        | Abstract and source                                                |
| **Intended audience**              | 333                  | $a                           | Audience description                                               |
| **Material type (content)**        | 181                  | $a $b $c $2                  | Content type, form codes, and code source                          |
| **Carrier type / details**         | 182, 183             | $a $c $2                     | Carrier type codes and standards                                   |
| **Cataloging agency info**         | 801                  | $a $b $c $g                  | Country, cataloging agency, date, standard used                    |


### Default Values and Standards

- **Leader**: Use ` cam0 22 450 ` for monographic text resources
- **Translation indicator (101)**: Use "1" if translated, " " if original
- **Author role codes (4)**: Use "070" for authors, "730" for translators
- **Subject scheme (606)**: Use "rameau" for French subject headings
- **Cataloging rules (801)**: Use "AFNOR" for French cataloging standards

### Processing Instructions

1. **Extract** all available metadata from the user's input
2. **Map** each element to the appropriate UNIMARC field using the guide above
3. **Generate** control numbers and timestamps if not provided:
   - Record ID (001): Create unique identifier
   - Timestamp (005): Use format YYYYMMDDHHMMSS.000
4. **Handle multiple authors**: Use tag 700 for the first/main author, 701 for additional authors
5. **Format indicators**: Pay attention to ind1 and ind2 values as specified in template
6. **Include only populated fields**: Omit template sections where no data is available

### Example Usage

**Input**: "Title: Digital Libraries, Author: John Smith, Publisher: Academic Press, Year: 2023, ISBN: 978-0123456789"

**Expected Output**: Complete UNIMARC XML record with all provided elements properly mapped to their corresponding fields and subfields.

---

**Generate the UNIMARC XML record now using the metadata provided by the user.**

3. Avec quelles données d’entraînement ?

Nous avons donc besoin d’un dataset qui contiendrait en input une simple liste de métadonnées du type Titre:... Auteur(s) : …. etc, en output la notice Unimarc/XML correspondante (ou Unimarc ISO, ou Marc21 etc… le processus est généralisable à n’importe quel format de données). Constituer un set de notices Unimarc est une opération triviale grâce au SRU de l’Abes qui expose les données du Sudoc, par contre les raw data en entrée ne sont à priori pas exportables d’une application existante, il faut donc créer soi-même ces nouvelles données synthétiques par une opération de “backtranslation”, c’est-à dire en s’appuyant sur un LLM pour inférer les inputs à partir des outputs.

Techniquement ici dans le cadre d’un apprentissage par GRPO, nous n’avons pas besoin de produire des données synthétiques additionnelles contenant des étapes de raisonnement permettant au modèle de générer l’output à partir de l’input, néanmoins pour tout de même enrichir le dataset et le rendre actionnable pour d’autres types d’apprentissage, on réalise également cette opération dite de “backreasoning” au cours de laquelle on prompte un LLM tiers pour qu’il explicite les étapes nécessaires permettant de passer des données textuelles aux notices Unimarc.

Le dataset est accessible sur HuggingFace, et le notebook avec le code ayant permis de le produire disponible sur Kaggle.

enter image description here

4. Sur quelles fonctions de récompenses s'appuyer ?

Les “rewards functions” sont le point central du dispositif puisque ce sont les niveaux de récompenses attribués à chaque notice générée qui guident l’apprentissage du modèle. Cette notation est traditionnellement orientée par deux types de critères (la structure et la précision) mais il est bien sûr possible de personnaliser et d’ajouter les fonctions selon son domaine métier et les types d’outputs attendus. Les trois types de récompenses mises en place ici sont respectivement fondées sur

  • la qualité de la structure du xml (vérification de la présence des principaux tags leader, datafield et subfield)
  • la comparaison entre les données fournies et les valeurs contenues dans les balises xml (est-ce que la chaîne de caractère fournie en titre est bien présente dans le xml généré ?)
  • une évaluation plus qualitative de la conversion des types d’informations dans les bonnes étiquettes (est-ce que la chaîne de caractère fournie en titre se retrouve bien dans un datafied[@tag=’200’] ?)

Voir les rewards functions

def extract_xml(text: str) -> str | None:
    """
    Extracts the XML content from a model-generated response.

    Tries to match either a markdown-formatted XML block (```xml ... ```)
    or a raw <record>...</record> XML structure.

    Args:
        text (str): The full text output from the model.

    Returns:
        str | None: The extracted XML string if found, else None.
    """
    match = re.search(r'```xml(.*?)```', text, re.DOTALL)
    if match:
        return match.group(1).strip()
    # Fallback: try looser pattern if no markdown
    match = re.search(r'<record.*?</record>', text, re.DOTALL)
    if match:
        return match.group(0).strip()
    return None

def extract_field_values(xml_str):
    """
    Extracts all <datafield> tag contents from an XML string and maps them by tag.

    Args:
        xml_str (str): A string containing a UNIMARC/XML record.

    Returns:
        dict: A mapping from datafield tag to concatenated subfield text content.
    """
    parser = etree.XMLParser(recover=True)
    root = etree.fromstring(xml_str, parser=parser)
    fields = {}
    for df in root.findall(".//datafield"):
        tag = df.get("tag")
        if tag is None:
            continue
        # Only include subfields with non-None text
        subfields = [sf.text for sf in df.findall("subfield") if sf.text is not None]
        fields[tag] = " ".join(subfields)
    return fields

def parse_metadata(user_input: str) -> dict:
    """
    Parses simple key-value bibliographic metadata text into a dictionary.

    Args:
        user_input (str): Metadata string, e.g., "Title: Example\nAuthor: Jane Doe"

    Returns:
        dict: Parsed metadata fields.
    """
    metadata = {}
    lines = user_input.strip().split('\n')
    for line in lines:
        if ":" in line:
            key, value = line.split(":", 1)
            metadata[key.strip()] = value.strip()
    return metadata

def format_reward(xml_output: str) -> float:
    """
    Evaluates whether the generated XML follows basic UNIMARC/XML structure.

    The reward is:
        - 1.0 if all required elements (leader, datafield, subfield) exist
        - 0.6 if only datafield and subfield exist
        - 0.0 otherwise

    Args:
        xml_output (str): The XML string extracted from model output.

    Returns:
        float: Structural conformity reward.
    """
    if xml_output is None:
        return 0.0
    try:
        parser = etree.XMLParser(recover=True)
        root = etree.fromstring(xml_output, parser=parser)
    except etree.XMLSyntaxError as e:
        return 0.0

    # Only give non-zero reward if ALL key elements exist
    has_leader = root.find(".//leader") is not None
    has_datafield = root.find(".//datafield") is not None
    has_subfield = root.find(".//subfield") is not None

    if has_leader and has_datafield and has_subfield:
        return 1.0
    elif has_datafield and has_subfield:
        return 0.6
    else:
        return 0.0


def accuracy_reward(generated_xml: str, target_xml: str) -> float:
    """
    Computes a field-level similarity score between generated and target XML records.

    Uses difflib string similarity over each shared field tag.

    Args:
        generated_xml (str): Model-generated XML.
        target_xml (str): Ground-truth UNIMARC XML.

    Returns:
        float: Averaged field-level similarity score in [0, 1].
    """
    if generated_xml is None or target_xml is None:
        return 0.0
    try:
        gen_fields = extract_field_values(generated_xml)
        tgt_fields = extract_field_values(target_xml)
    except ET.ParseError:
        return 0.0

    shared_keys = set(gen_fields) & set(tgt_fields)
    if not shared_keys:
        return 0.0

    total_sim = 0
    for key in shared_keys:
        sim = difflib.SequenceMatcher(None, gen_fields[key], tgt_fields[key]).ratio()
        total_sim += sim

    return total_sim / len(shared_keys)

def semantic_field_reward(user_prompt: str, generated_xml: str) -> float:
    """
    Scores whether semantic elements from user input are correctly reflected in XML fields.

    Matches common bibliographic fields (e.g., Title, Author, Publisher) to expected
    UNIMARC tags using fuzzy field name matching, then compares the content values.

    Args:
        user_prompt (str): Original input metadata text.
        generated_xml (str): Generated UNIMARC XML.

    Returns:
        float: Proportion of semantic fields correctly reflected in output, in [0, 1].
    """
    if generated_xml is None or user_prompt is None:
        return 0.0
    FIELD_MAPPINGS = {
        "Title": ("200", "a"),              # Title → 200$a
        "Subtitle": ("200", "e"),           # Subtitle → 200$e
        "Year": ("214", "d"),               # Year → 214$d (Publication date)
        "Authors": ("700", "a"),            # Authors → 700$a (Main Author)
        "Publisher": ("214", "c"),          # Publisher → 214$c
        "ISBN": ("010", "a"),               # ISBN → 010$a
        "Language": ("101", "a"),           # Language → 101$a
        "Collection/Series": ("225", "a"),         # Collection/Series → 225$a
        "Material description": ("215", "a"), # Material description → 215$a
        "Price": ("010", "d"),              # Price → 010$d
        "Abstract/Notes Source": ("330", "a"),           # Abstract → 330$a
        "Country of publication": ("102", "a"),            # Country of publication → 102$a
        "Edition": ("205", "a"),            # Edition → 205$a
        "Notes": ("300", "a"),              # Notes → 300$a
        "Keywords / Subject Headings": ("600", "a")            # Keywords → 600$a (Subject Headings)
    }
    try:
        parser = etree.XMLParser(recover=True)
        root = etree.fromstring(generated_xml, parser=parser)
    except etree.XMLSyntaxError as e:
        return 0.0
    metadata = parse_metadata(user_prompt)
    total_possible_matches = len(FIELD_MAPPINGS)
    successful_matches = 0

    for input_field, input_value in metadata.items():
        best_match_score = 0
        best_matched_unimarc = None

        for unimarc_field_label, (tag, subfield) in FIELD_MAPPINGS.items():
            similarity_score = fuzz.partial_ratio(input_field.lower(), unimarc_field_label.lower())
            if similarity_score > best_match_score and similarity_score > 80: # Adjust threshold
                best_match_score = similarity_score
                best_matched_unimarc = (tag, subfield)

        if best_matched_unimarc:
            tag, subfield = best_matched_unimarc
            datafield = root.find(f".//datafield[@tag='{tag}']/subfield[@code='{subfield}']")
            if datafield is not None and datafield.text:
                generated_value = datafield.text.strip().lower()
                metadata_value = input_value.strip().lower()
                if metadata_value in generated_value or generated_value in metadata_value:
                    successful_matches += 1

    reward = successful_matches / total_possible_matches if total_possible_matches > 0 else 0.0
    return round(reward, 2)

def format_reward_func(completions, **kwargs) -> list[float]:
     """
    Wrapper for batch evaluation using `format_reward`.

    Args:
        completions (list of str): List of generated outputs.

    Returns:
        list of float: Structural reward scores for each output.
    """
    for c in completions:
      print("==== format Extracted xml ====")
      if extract_xml(c) is not None:
          print(extract_xml(c)[:100])
      print("==== format Reward ====")
      print(format_reward(extract_xml(c)))
      print("====================")

    return [format_reward(extract_xml(c)) for c in completions]

def accuracy_reward_func(completions, answer, **kwargs) -> list[float]:
    """
    Wrapper for batch evaluation using `accuracy_reward`.

    Args:
        completions (list of str): Model completions (generated XMLs).
        answer (list of str): Ground-truth XML records.

    Returns:
        list of float: Accuracy rewards per pair.
    """
    rewards = []
    for completion, target in zip(completions, answer):
        extracted_xml = extract_xml(completion)
        reward = accuracy_reward(extracted_xml, target)
        print("==== accuracy Reward ====")
        print(reward)
        print("====================")
        rewards.append(reward)

    return rewards

def semantic_field_reward_func(prompts, completions, **kwargs) -> list[float]:
    """
    Wrapper for batch evaluation using `semantic_field_reward`.

    Args:
        prompts (list of str): Original user prompts.
        completions (list of str): Model completions.

    Returns:
        list of float: Semantic matching rewards.
    """
    rewards = []
    for prompt_text, completion in zip(prompts, completions):
        extracted_xml = extract_xml(completion)
        # Still valid: prompt_group[-1]['content'] = user's metadata -> user_prompt = prompt_group[-1]['content']
        user_prompt = prompt_text
        if extracted_xml:
            reward = semantic_field_reward(user_prompt, extracted_xml)
            print("==== semantic Reward ====")
            print(reward)
            print("====================")
            rewards.append(reward)
        else:
            rewards.append(0.0)
    return rewards

5. L'apprentissage

L’ensemble du code se trouve dans ce notebook, avec des métriques d’apprentissage monitorées par wandb.

Le modèle fine-tuné est disponible sur Huggingface, avec comme d’habitude des versions quantifiées compatibles pour llama.cpp et tous les outils d’inférence et autres chatbots s’appuyant dessus (Ollama, llamafiles, LMStudio etc…).

6. L'application

L'application de démonstration déployée sur HuggingFace permet donc d'uploader un fichier image de couverture de monographie, puis combine un modèle de vision Image-to-text qui a pour instruction d'extraire les éléments textuels avec notre modèle de raisonnement spécialisé en Unimarc pour la génération de la notice correspondante.

enter image description here

7. Limites

Les résultats obtenus en l'état avec ce modèle fine-tuné sont plus ou moins satisfaisants (disons encourageants), les limites de l'exercice touchant autant :

  • à l'entraînement perfectible du modèle de base : possibilités d'optimisation des hyper-paramèters d'entraînement, amélioration des fonctions de récompenses, meilleure gestion des données système, choix d'un modèle de base avec plus de paramètres...
  • qu'aux limites théoriques même des modèles de raisonnement : inflation du nombre de tokens générés augmentant la probabilité de phénomènes de "context overflow" que l'on peut rapprocher de ceux de "Lost in the middle" (https://arxiv.org/abs/2307.03172) appliqués aux outputs lorsque les CoT sont trop longues , absence de compréhension "réelle" par le modèle des concepts et de leur signification en dépit des apparences de raisonnement articulé (https://arxiv.org/abs/2505.17117), difficulté à gérer des contextes complexes....

Néanmoins, sans être encore utilisables en production, ils démontrent tout de même, sous réserve d'un training optimisé, une capacité théorique du modèle à structurer des données bibliographiques brutes en notices Unimarc/XML valides, avec un certain niveau de raisonnement intermédiaire.

Il suffit de suivre les derniers développements des recherches autour de l’apprentissage par renforcement (et notamment par GRPO) pour projeter un contexte personnalisé d'usage de l'IA générative capable de prendre en compte des critères métier complexes et qui s’avère très prometteur.

De plus, un tel modèle combiné à des serveurs MCP pour l’alignement automatique avec des autorités d'Idref ou encore avec le service d’indexation automatique des sujets RAMEAU developpé par l'Abes pourrait constituer un outil d’aide au catalogage fiable pouvant s’inscrire dans une chaîne de traitement documentaire réellement pilotée par l'IA.

"> ');