Ajouter des Entités Nommées (NER) à votre modèle SpaCy

A

La reconnaissance des Entités Nommées (NER – Named Entity Recognition)

La reconnaissance des entités nommées (NER, pour Named Entity Recognition) est une technique de traitement du langage naturel (NLP) qui vise à identifier et classifier les entités présentes dans un texte en catégories prédéfinies telles que les noms de personnes, les organisations, les lieux, les dates,… Exemples de NER :

  • « Barack Obama » comme PERSON
  • « Google » comme ORG (Organisation)
  • « Paris » comme LOC (Lieu)
  • « 12 janvier 2024 » comme DATE

Dans le cadre de l’analyse de discours politique par exemple, la NER est particulièrement précieuse. Elle permet d’extraire automatiquement des informations clés, facilitant ainsi l’analyse et la compréhension des discours, des débats. Cependant, les modèles NER standard ne reconnaissent pas toujours précisément les entités spécifiques à certains contextes.
Par exemple, un modèle standard peut ne pas reconnaître l’expression « Nouveau Front Populaire » (Parti politique récemment crée) comme une entité unique mais pourrait le traiter comme trois mots indépendants.

L’objectif de ce script est de permettre l’analyse, la création et la suppression d’entités nommées (NER) à partir de segments de texte. Les entités ainsi créées ou modifiées sont sauvegardées dans un modèle qui peut ensuite être chargé pour entraîner la librairie SpaCy avant de procéder à l’analyse de grands corpus.

 

L’utilisateur a la possibilité de créer plusieurs modèles (par exemple : politique, économique, santé…) en fonction des spécificités des discours analysés.
Ces modèles permettent « d’affiner » la reconnaissance des entités (NER) en ajoutant celles qui ne sont pas initialement reconnues par SpaCy.
Les fichiers d’annotation contenant les entités créées ou supprimées pourront être réimportés pour entraîner le modèle et réaliser des analyses sur des corpus plus vastes.

Utilisation de SpaCy avec Streamlit

Streamlit est un framework open-source en Python conçu pour faciliter la création d’applications web interactives destinées aux data scientiste et développeurs. Grâce à sa simplicité et à sa flexibilité, il permet de transformer rapidement des scripts Python en applications web ergonomiques.

Pour faire fonctionner le script, vous devrez installer SpaCy en privilégiant le modèle « Large » (python -m spacy download fr_core_news_lg),  ainsi que les bibliothèques de Streamlit.
Je ne vais pas revenir dans cet article sur la bibliothèque NLP SpaCy et son fonctionnement.

pip install spacy
python -m spacy download fr_core_news_lg -> large modèle
pip install streamlit spacy
pip install streamlit spacy spacy-streamlit
pip install watchdog

L’importance du « fine-tuning » des NER

Affiner un modèle NER, comme celui de SpaCy, est crucial pour l’adapter à des contextes spécifiques et améliorer sa précision.
Voici le texte fictif (produit avec GPT) avec lequel j’ai développé le script visant à entraîner SpaCy en créant ou supprimant des entités (NER).

Dans le terminal Python, pour lancer le script, vous devez taper la commande suivante : streamlit run main.py

Le script exécute le fichier main.py depuis l’éditeur de code.
Streamlit lancera une interface graphique qui s’exécutera dans votre navigateur.

L’enrichissement du fichier annotations.spacy (que vous pouvez renommer) consiste à ajouter, modifier ou supprimer des entités nommées (NER) à partir de segments de texte.

Voici les étapes détaillées de ce processus :

  • Collecte de segments de texte

L’utilisateur commence par collecter des segments de texte pertinents pour le domaine d’application (ex. : textes politiques, descriptions de produits,). Ces segments sont ensuite insérés dans le champ de texte de l’interface.

  • Analyse initiale des Entités Nommées (NER)

En utilisant le modèle SpaCy de base (fr_core_news_lg), vous pouvez  analyser le texte pour identifier les entités nommées initialement reconnues. Vous pouvez visualiser les entités détectées et les tokens du texte pour mieux comprendre la structure linguistique (les texte de ces onglets sont en Anglais).

Dans notre cas, l’expression « Nouveau Front Populaire » est traité comme trois mots distincts, mais grâce au fine-tuning, le modèle reconnaîtra cette expression comme une seule entité nommée

  • Création d’Entités Nommées (NER)

Si certaines entités spécifiques ne sont pas reconnues par le modèle de base, vous pouvez les ajouter manuellement en sélectionnant les positions de début et de fin des mots formant l’entité, puis en attribuant un label pertinent (ex. : PERSON, LOC, ORG,… voir la liste plus bas). Les nouvelles entités sont ajoutées et sauvegardées dans le fichier annotations.spacy.

Pour ajouter des entités, vous devrez vous référer aux indices des mots dans le texte.

À noter que le script comporte un double affichage. Le résultat est visible en bleu à travers l’affichage « simple » du corpus de texte, mais également avec la fonction « embellie » de l’affichage du texte de Streamlit utilisant la fonction spacy_streamlit.visualize_ner (bien plus sexy ! mais en anglais).

  • Suppression des Entités Nommées :

Les entités incorrectes ou non pertinentes peuvent être supprimées. L’utilisateur sélectionne les entités à supprimer et confirme l’opération, ce qui met à jour le fichier annotations.spacy.

  • Sauvegarde et chargement des modèles

Après chaque opération (ajout, modification, suppression), les annotations sont sauvegardées dans le fichier annotations.spacy.

  • Analyse avec le modèle fine-tuné

Pour vérifier l’efficacité des modifications apportées, l’utilisateur peut analyser de nouveaux textes avec le modèle enrichi. Cette étape permet de valider les entités ajoutées et de s’assurer que le modèle répond correctement à vos exigences.
Pour confirmer cette étape je vais prendre un nouveau texte (auparavant j’ai crée (avec une petite erreur de label pour la date) les NER suivant :

  • 2024 => Label : « PERSON » (Personne)
  • Nouveau Front Populaire => Label : « ORG » (Organisation)

En suivant ce processus, l’utilisateur peut continuellement enrichir le fichier annotations.spacy, améliorant ainsi la précision et la pertinence du modèle NLP pour des applications spécifiques, telles que des chatbots ou des systèmes de recommandation.

Les labels utilisés par SpaCy pour décrire les NER

SpaCy utilise plusieurs labels permettant de décrire les NER, chacun représentant une catégorie d’entités spécifiques :

  • PER : Personnes (ex : Emmanuel Macron)
  • ORG : Organisations (ex : Nouveau Front Populaire)
  • GPE :  Lieux géographiques (ex : Paris)
  • DATE : Dates et périodes (ex : 2024)
  • TIME : Heures (ex :, 14:00)
  • MONEY : Montants financiers (ex : 1 million d’euros)
  • PERCENT : Pourcentages (ex : 50%)
  • FAC : Bâtiments, aéroports, etc. (ex : Tour Eiffel)
  • PRODUCT : Produits (ex : iPhone)
  • EVENT : Événements (ex : Coupe du Monde)
  • WORK_OF_ART : Oeuvres d’art (ex : Mona Lisa)
  • LAW : Documents législatifs (ex : Déclaration des droits de l’homme)
  • LANGUAGE : Langues (ex : Français)
  • MISC : Divers, pour les entités qui ne rentrent pas dans les autres catégories spécifiques.

Ces symboles (Traduction de la doc SpaCy) permettent de structurer et de catégoriser les NER, facilitant ainsi l’analyse et l’interprétation des textes.

Le script

La bibliothèque from spacy.training import Example est utilisée pour créer des exemples d’entraînement pour spaCy. Dans le contexte de l’enrichissement d’un modèle avec de nouvelles annotations, elle permet de créer des exemples à partir des documents annotés pour l’entraînement du modèle.

# pip install spacy
# python -m spacy download fr_core_news_lg -> large modèle
# pip install streamlit spacy
# pip install streamlit spacy spacy-streamlit
# pip install watchdog

import streamlit as st
import spacy
import spacy_streamlit
from spacy.tokens import DocBin
from spacy.training import Example
import os


def log(message):
    st.write(f"LOG: {message}")


@st.cache_resource
def load_model():
    log("Chargement du modèle SpaCy large")
    return spacy.load('fr_core_news_lg')


def save_annotations(doc, path):
    log(f"Sauvegarde des annotations dans le fichier : {path}")
    doc_bin = DocBin()
    doc_bin.add(doc)
    if not os.path.exists(os.path.dirname(path)):
        os.makedirs(os.path.dirname(path))
    doc_bin.to_disk(path)


def load_annotations(path, nlp):
    if os.path.exists(path):
        log(f"Chargement des annotations depuis le fichier : {path}")
        doc_bin = DocBin().from_disk(path)
        return list(doc_bin.get_docs(nlp.vocab))[0]
    return None


def display_entities(doc):
    entity_text = ""
    last_end = 0
    for ent in doc.ents:
        entity_text += doc.text[last_end:ent.start_char]
        entity_text += f"**[{ent.text}]({ent.label_})**"
        last_end = ent.end_char
    entity_text += doc.text[last_end:]
    st.markdown(entity_text)


def load_enriched_model(nlp, annotations_path):
    if os.path.exists(annotations_path):
        doc_bin = DocBin().from_disk(annotations_path)
        docs = list(doc_bin.get_docs(nlp.vocab))
        for doc in docs:
            for ent in doc.ents:
                nlp.get_pipe('ner').add_label(ent.label_)
        optimizer = nlp.resume_training()
        for _ in range(10):  # Vous pouvez ajuster le nombre d'itérations
            losses = {}
            for doc in docs:
                example = Example.from_dict(doc, {
                    "entities": [(ent.start_char, ent.end_char, ent.label_) for ent in doc.ents]})
                nlp.update([example], sgd=optimizer, losses=losses)
    return nlp


LABELS = {
    "PERSON": "Personne",
    "NORP": "Groupes nationaux, politiques, religieux, etc.",
    "FAC": "Bâtiments, aéroports, routes, ponts, etc.",
    "ORG": "Organisations (entreprises, agences, institutions, etc.)",
    "GPE": "Entités géopolitiques (pays, villes, états)",
    "LOC": "Emplacements autres que les GPE (montagnes, planètes, etc.)",
    "PRODUCT": "Objets, véhicules, aliments, etc. (pas de services)",
    "EVENT": "Événements (guerres, compétitions sportives, etc.)",
    "WORK_OF_ART": "Titres d'oeuvres d'art, livres, etc.",
    "LAW": "Documents nommés comme lois, traités, etc.",
    "LANGUAGE": "Langues",
    "DATE": "Dates ou périodes",
    "TIME": "Heures spécifiques dans la journée",
    "PERCENT": "Pourcentages (incluant les signes %)",
    "MONEY": "Montants monétaires",
    "QUANTITY": "Mesures (poids, distance, etc.)",
    "ORDINAL": "Adjoints ordinaux (premier, deuxième, etc.)",
    "CARDINAL": "Numéros de base (mais pas des nombres comme 'deuxième')",
    "MISC": "Divers"
}


def main():
    """Application NLP avec Spacy-Streamlit"""
    col1, col2 = st.columns([3, 1])
    with col1:
        st.title("Application NLP Recherche / Création / Suppression de NER avec Spacy")
    with col2:
        st.write("Version 1.0")
        st.write("Date: 2023-07-23")
        st.markdown("[www.codeandcortex.fr](https://www.codeandcortex.fr)")
    st.markdown("---")

    menu = ["Analyse des Entités", "Création d'Entité", "Suppression des Entités", "Test du Modèle enrichi"]
    choice = st.sidebar.selectbox("Menu", menu)
    nlp = load_model()
    annotations_path = st.text_input("Nom du fichier d'annotations des NER", "./annotations.spacy")

    if choice == "Analyse des Entités":
        st.subheader("Analyse des Entités de votre texte")
        raw_text = st.text_area("Entrez votre texte ici", "Entrez le texte ici")
        if st.button("Analyser", key="analyze_text"):
            if raw_text:
                doc = nlp(raw_text)
                saved_doc = load_annotations(annotations_path, nlp)
                if saved_doc:
                    doc.ents = saved_doc.ents
                st.subheader("Entités détectées:")
                display_entities(doc)
                st.subheader("Visualisation des entités:")
                spacy_streamlit.visualize_ner(doc, labels=nlp.get_pipe('ner').labels, key="ner_analysis")

    elif choice == "Création d'Entité (NER)":
        st.subheader("Création d'Entité")
        raw_text = st.text_area("Entrez votre texte ici", "Entrez le texte ici...")
        if st.button("Analyser le texte", key="analyze_creation"):
            doc = nlp(raw_text)
            saved_doc = load_annotations(annotations_path, nlp)
            if saved_doc:
                doc.ents = saved_doc.ents
            st.session_state.doc = doc
            st.subheader("Entités détectées:")
            display_entities(doc)
            st.subheader("Visualisation des entités:")
            spacy_streamlit.visualize_ner(doc, labels=nlp.get_pipe('ner').labels, key="ner_analysis_creation")
            st.session_state.word_positions = [(token.text, i) for i, token in enumerate(doc)]

        if 'doc' in st.session_state:
            doc = st.session_state.doc
            st.subheader("Ajouter des Entités Nommées")
            if 'word_positions' in st.session_state:
                st.subheader("Texte avec indices")
                st.write(", ".join([f"{text} ({i})" for text, i in st.session_state.word_positions]))

            word_start = st.number_input("Position de début (mot)", min_value=0, max_value=len(doc) - 1, step=1,
                                         key="start_word_creation")
            word_end = st.number_input("Position de fin (mot)", min_value=0, max_value=len(doc) - 1, step=1,
                                       key="end_word_creation")
            label = st.selectbox("Label de l'entité", list(LABELS.keys()), format_func=lambda x: f"{x} : {LABELS[x]}",
                                 key="entity_label_creation")

            if st.button("Ajouter Entité", key="add_entity_creation"):
                if word_start <= word_end < len(doc):
                    char_start = doc[word_start].idx
                    char_end = doc[word_end].idx + len(doc[word_end].text)
                    span = doc.char_span(char_start, char_end, label=label)
                    if span:
                        conflicts = [ent for ent in doc.ents if not (span.end <= ent.start or span.start >= ent.end)]
                        if not conflicts:
                            new_ents = list(doc.ents) + [span]
                            doc.ents = new_ents
                            st.session_state.doc = doc
                            save_annotations(doc, annotations_path)
                            st.success("Entité ajoutée et annotations sauvegardées avec succès.")
                            st.subheader("Entités mises à jour:")
                            display_entities(doc)
                            st.subheader("Visualisation des entités mises à jour:")
                            spacy_streamlit.visualize_ner(doc, labels=nlp.get_pipe('ner').labels,
                                                          key="ner_after_add_creation")
                        else:
                            st.error("Les nouvelles entités ne doivent pas chevaucher les entités existantes.")
                    else:
                        st.error("Impossible de créer une entité avec ces indices.")
                else:
                    st.error("Les positions de début et de fin sont invalides.")

    elif choice == "Suppression des Entités":
        st.subheader("Suppression des Entités")
        if st.button("Charger les annotations", key="load_model_suppression"):
            doc = load_annotations(annotations_path, nlp)
            if doc:
                st.session_state.doc = doc
                st.subheader("Entités détectées:")
                display_entities(doc)
                st.subheader("Visualisation des entités:")
                spacy_streamlit.visualize_ner(doc, labels=nlp.get_pipe('ner').labels, key="ner_analysis_suppression")
            else:
                st.error("Aucune annotation trouvée.")

        if 'doc' in st.session_state:
            doc = st.session_state.doc
            st.subheader("Supprimer des Entités Nommées")
            ents = [(ent.text, ent.start, ent.end, ent.label_) for ent in doc.ents]
            ents_to_remove = st.multiselect("Choisissez des entités à supprimer", ents,
                                            key="remove_entities_suppression")
            if st.button("Supprimer les Entités Sélectionnées", key="delete_entities_suppression"):
                if ents_to_remove:
                    new_ents = [ent for ent in doc.ents if
                                (ent.text, ent.start, ent.end, ent.label_) not in ents_to_remove]
                    doc.ents = new_ents
                    st.session_state.doc = doc
                    save_annotations(doc, annotations_path)
                    st.success("Entités supprimées et annotations sauvegardées avec succès.")
                    st.subheader("Entités mises à jour:")
                    display_entities(doc)
                    st.subheader("Visualisation des entités mises à jour:")
                    spacy_streamlit.visualize_ner(doc, labels=nlp.get_pipe('ner').labels,
                                                  key="ner_after_delete_suppression")

    elif choice == "Test du Modèle Enrichi":
        st.subheader("Test du Modèle Enrichi avec les Annotations")

        # Charger le modèle enrichi
        enriched_nlp = load_enriched_model(nlp, annotations_path)

        # Zone de texte pour entrer un nouveau texte à tester
        test_text = st.text_area("Entrez un nouveau texte à tester", "Entrez le texte ici")

        if st.button("Tester le Modèle Enrichi"):
            if test_text:
                doc = enriched_nlp(test_text)

                st.subheader("Entités détectées par le modèle enrichi:")
                display_entities(doc)

                st.subheader("Visualisation des entités:")
                spacy_streamlit.visualize_ner(doc, labels=enriched_nlp.get_pipe('ner').labels, key="ner_enriched_model")
            else:
                st.error("Veuillez entrer un texte à tester.")


if __name__ == '__main__':
    main()

 

Conclusion

L’approche décrite dans ce script est particulièrement utile pour des applications telles que les chatbots, où une compréhension précise du langage de l’utilisateur est cruciale.
Par exemple, dans le domaine du tourisme, un utilisateur demandant des informations sur le « lac de Bethmale » nécessite que le mot « Bethmale » soit reconnu comme une entité nommée (lieu). Cela permet au processus de traitement du langage de reconnaître cette entité spécifique et d’optimiser les réponses fournies à l’utilisateur.
De même, dans le secteur marchand, si un utilisateur pose une question sur un modèle de chaussure de trail de la marque Salomon, il est essentiel que « Salomon » soit reconnu comme une entité nommée (marque).
En affinant le modèle NLP avec des entités spécifiques, on améliore la précision et la pertinence des réponses du chatbot. Cette approche montre donc l’intérêt et l’importance de créer et de modifier des entités nommées pour différents secteurs, tels que le tourisme et le commerce, afin de mieux répondre aux besoins des utilisateurs.

 

A propos de l'auteur

Stéphane Meurisse

Ajouter un commentaire

Stéphane Meurisse