Skip to content

schéma de base de données postgresql EAV hybride pour l'analyse de textes en français

License

Notifications You must be signed in to change notification settings

thjbdvlt/litteralement

Repository files navigation

littéralement

littéralement est un schéma de base de données postgresql conçu pour l'analyse de texte en français et construit selon un modèle générique (EAV) hybride.

le modèle générique que j'utilise comme base, librement emprunté à Francesco Beretta1 (et dont je ne reprends qu'une minuscule partie) est plus complet que ce que désigne le terme EAV (Entity-Attribute-Value), puisqu'il n'implémente pas seulement une manière de décrire les propriétés des entités, mais aussi, par exemple, leurs relations.

au schéma de la base de données s'ajoutent des modules Python qui permettent, entre autres choses, de facilement importer des données, de les annoter à l'aide de la librairie spacy et d'insérer les annotations dans la base de données.

modèle EAV hybride

  • une première partie implémente un modèle générique minimal et est destinée à décrire le monde duquel on extrait des textes, qu'il s'agisse des supports matériels qui les contiennent, des acteurices sociaux qui les font circuler ou en relisent le contenu, ou encore des événements qui en motivent la rédaction. la flexibilité de ce modèle permet de décrire un grand nombre de chose très diverses à l'aide d'un nombre restreint et fixe de tables et de colonnes, et d'ajouter de nouveaux types d'objets (ou de relations) sans avoir à modifier le schéma2.
  • la seconde partie de la base de données est, à l'inverse, conçue pour accueillir des données dont la structure est à la fois prévisible et invariable, car l'analyse automatique des textes se fait souvent à l'aide d'outils et de concepts non seulement relativement standardisés, mais aussi uniformément appliqués: qu'on utilise la librairie spacy, stanza ou nltk, on manipulera toujours des sentences et des tokens, lesquels tokens se verront quasi-systématiquement attribués, entre autres choses, un lemma (lemme), un part-of-speech tag (nature), un dependency label (fonction), des caractéristiques morphologiques représentées selon le format FEATS, un id numérique indiquant leur position dans le texte, etc. et s'il y a évidemment différents stocks de propriétés, le choix d'une méthode d'annotation est généralement adoptée pour l'ensemble du corpus (c'est l'élément invariable, lequel permet la comparaison et l'analyse). la flexibilité du modèle EAV est donc inutile pour stocker ces données. comme le modèle EAV engendre par ailleurs des baisses importantes de performances, qu'il requiert un espace de stockage plus grand et qu'en plus il complexifie les requêtes (les rendant moins lisibles), il est préférable de construire cette seconde partie selon un modèle relationnel plus standard3.

le diagramme ci-dessous représente la structure de la base de données. chaque rectangle représente une table. les flèches traitillées représentent les héritages entre tables: la table mot hérite par exemple de la table token qui elle-même hérite de la table segment, les colonnes texte, debut et fin4. d'un point de vue conceptuelle, la relation d'héritage correspond à la relation sous-classe de5. les autres flèches (pleines) représentent des foreign keys. les lignes commençant par _ indiquent, elles-aussi des foreign keys: la valeur des colonnes en question est toujours integer ou, pour des raisons d'optimisation, smallint, car il est très improbable pour certaines tables de dépasser le millier de lignes (typiquement: les part-of-speech tags et dependency labels, respectivement stockés dans les tables nature et fonction). les colonnes qui commencent par le signe + représente des valeurs littérales. si le nom d'une colonne est souligné, cette colonne est utilisée comme primary key (il s'agit toujours de la colonne id).

nlp

  • la structure de la partie nlp6 de la base de données n'est pas spécifique à une librairie, quoi que des modules spécifiques permettent l'analyse avec spacy. la délimitation des différents objets, en revanche, est peut-être relativement spécifique à la langue française. en particulier, la table lexème (le mot hors contexte, comme élément du lexique) définit un objet qui regroupe des caractéristiques attribué par spacy aux tokens, mais qui en français ne varient pas d'un contexte à l'autre. en français, peu importe dans quel contexte on rencontrera le mot "magiques", il n'agira toujours de l'adjectif (part-of-speech) "magique" (lemma) au pluriel (morphology), et sa forme graphique canonique (norm) sera toujours "magique". il est donc inutile d'attribuer ces quatre propriétés à chaque occurrence du mot "magique": les propriétés lemme, nature, norme et morphologie sont donc, dans une base de données littéralement, des propriétés des lexèmes, tandis que les mots ont des propriétés contextuelles: fonctions (dep: par exemple "obj"), noyau (head), ainsi que les propriétés héritées des tokens (debut, fin, num), à quoi s'ajoute la référence au lexème dont ils sont une instance. l'ensemble des ligne de la table mot constitue donc le discours (les mots réelles) tandis que l'ensemble des lignes de la table lexème constitue le lexique7 (les mots possibles).
  • les mots eux-mêmes, par ailleurs, sont également un ajout par rapport aux objets utilisés par spacy qui ne différencie pas les différents types de tokens. or, il n'y a pas de sens à attribuer des lemmes à des signes de ponctuation, à des urls, à des emoticons ou des chiffres, ni à leur associer une analyse morphologique car les chiffres ne sont pas au pluriel ni les urls fléchies. ces objets textuels sont donc, dans une base de données littéralement, des tokens mais pas des mots, ils n'ont pas de fonction grammaticale ni de noyau (quoi que cela puisse être discutable), ni non plus de lexème (ce qui est en revanche plus légitime à mon avis). de cette façon, le lexique n'est pas pollué par des nombres ou des dates (en nombre virtuellement infini).
  • les tables token, mot, phrase ou span héritent toutes de la table segment qui a trois colonnes: texte (l'identifiant du texte dans lequel le segment se trouve), debut (la position du premier caractère) et fin (la position du dernier caractère).

eav

  • la table entité regroupe les choses du monde: personnes, lieux, objets matériels, idées, n'importe quoi que l'on veut pouvoir désigner et mettre en relation avec d'autres choses. ses colonnes sont réduites au minimum: un id qui permet d'y faire référence et une classe qui en définit la nature. la valeur dans la colonne classe est l'identifiant (id) d'une ligne de la table classe qui contient aussi les colonnes nom (unique et nécessaire) et definition (optionnelle, sans contrainte). la table classe est identique aux tables type_relation et type_propriete (qui ont une position et une fonction identique pour les tables relation et propriete), c'est pourquoi elles sont définies dans le diagramme comme étant toutes dérivées d'une table concept (en fait un type et non une table).
  • la table relation met en lien deux entités (sujet et objet).
  • la table propriete permet d'assigner des propriétés aux entités. une propriété peut optionnellement avoir une valeur et cette valeur peut avoir différents datatype: le type de propriété "age" requiert une valeur numérique entière (integer), tandis que la propriété "existe" ne nécessite aucune valeur. la propriété "existe" sera donc placée dans la table propriété, qui n'a pas de colonne val tandis que la propriété "age" sera placée dans la table prop_int, laquelle table hérite de la table propriete et possède en plus une colonne val dont la valeur est un entier (integer). naturellement, il est aussi possible d'insérer manuellement des données "age" comme texte dans la table destinée aux valeurs textuelles, ou dans celle qui est dédiée au format jsonb. le plus facile, néanmoins, est d'utiliser les modules proposés pour l'importation qui insère automatiquement dans la table appropriée (voir plus bas). (il y a en réalité davantage de table propriétés que dans le diagramme ci-dessous.)
  • c'est par la table texte que sont mises en lien les deux parties de la base de donneés. elle hérite de la table propriete, tout comme les tables prop_int ou prop_float mais elle a également une colonne id qui est référencée par la table segment (et toutes les tables qui héritent de segment).

importation

si l'insertion d'entités, de propriétés ou de relations peut évidemment se faire manuellement, il est aussi possible d'importer des données structurées au format JSON comme suit, chaque objet JSON décrivant une entité, ses propriétés et les relations dont elle est le sujet.

{"id": 1, "classe": "bibliothèque"}
{"id": 2, "classe": "lieu", "est_magique": null, "magicité": 1.2}
{
    "classe": "personne", "nom": "becky", "relations": [
        {"type": "fréquente", "objet": 1}
    ]
}
{"classe": "livre", "relations": [{"type": "dans", "objet": 1}]},

le seul champ requis est, pour chaque entité, le champ classe. le champ id permet de définir les relations entre les entités (il ne correspond pas à l'id de l'entité dans la base de données). dans les entités, tous les champs qui ne sont pas classe, id ou relations sont interprétés comme des propriétés et sont insérées dans les tables qui correspondent au datatype:

{"est_magique": None}  # ira dans la table propriete, sans valeur (`null` en JSON!)
{"nom": "becky"}       # ira dans la table texte
{"nom": 1231}          # ira dans la table prop_int
{"nom": 1.2}          # ira dans la table prop_float
{"noms": {"prénom": "!", "nom": "?"}}  # ira dans la table prop_json

l'importation se fait en ajoutant dans la table import._entite des données au format décrit ci-dessus et en utilisant la procedure import.importer():

call procedure import.importer();

annoter

import psycopg
import litteralement.nlp.text_annotation
import spacy

dbname = "litteralement"
conn = psycopg.connect(dbname=dbname)
nlp = spacy.load("fr_core_news_lg")

litteralement.nlp.text_annotation.annoter(
    # connection à la database
    conn,
    # la requête qui doit retourner les ID des textes, et les textes.
    # elle peut en revanche avoir n'importe quelle condition.
    # pour annoter tous les textes pas encore annotés, passer 'all'.
    query="select id, val from texte",
    # le modèle de langue chargé par spacy.
    nlp=nlp,
)

la fonction d'annotation, par défaut, insère automatiquement les données dans les textes, mais c'est un comportement qui peut être modifié pour que les annotations soient simplement placées dans la table import._document en passant la valeur True dans le paramètre noinsert. le paramètre isword, quand à lui, permet de passer une fonction qui distinguera les mots des simples tokens. pour que tous les tokens soient sans distinctions placés dans la table mot, on pourra donc simplement passer une fonction qui retourne toujours la valeur True:

litteralement.nlp.text_annotation.annoter(
    conn, query="select id, val from texte", nlp=nlp,
    isword=lambda token: True,  # tous les tokens dans la table 'mot'
    noinsert=True  # pas d'insertion
)

# on peut ensuite insérer les annotations à l'aide de la fonction suivante:
litteralement.nlp.row_insertion.inserer(conn)

des propriétés supplémentaires peuvent être ajoutées dans des colonnes supplémentaires (pour l'instant seulement pour les lexèmes). pour chaque propriété additionnelle, un dict définit un nom de propriété (le nom de la colonne, et éventuellement de la table associée), un datatype, une function permettant d'obtenir la valeur à partir d'un Token, définit aussi si la valeur est une valeur littérale ou s'il faut récupérer l'id dans une table de référence (comme pour les part-of-speech tags, par exemple). la colonne et la tables sont créées automatiquement si elles n'existent pas. si la valeur est stockée dans une table de référence, il faut spécifier le nom de la colonne qui contient cette valeur dans le paramètre value_column.

import viceverser

nlp.add_pipe('viceverser_lemmatizer')
lex_user_attrs = [
    {
        "name": "vv_pos",
        "function": lambda token: token._.vv_pos,
        "is_literal": False,
        "datatype": "jsonb",
        "value_column": "val"
    },
    {
        "name": "vv_morph",
        "function": lambda token: token._.vv_morph,
        "is_literal": False,
        "datatype": "jsonb",
        "value_column": "val",
    },
]

# ces annotations supplémentaires seront automatiquement importées dans la table "lexeme", dans une colonne "random_morph".
litteralement.nlp.text_annotation.annoter(
    conn,
    lex_user_attrs=lex_user_attrs
)

Footnotes

  1. Francesco Beretta, "Des sources aux données structurées", 14 octobre 2022, En ligne, license CC BY-SA 4.0.

  2. il est particulièrement utile si l'on ne sait pas, au départ d'une recherche, ce qu'on va exactement collecter et la manière dont on va organiser le résultat de notre collecte (ou de nos analyses), ou la manière dont les objets de notre recherche peuvent se connecter par l'analyse.

  3. on évite par exemple de surcharger la table entité avec des millions de mots.

  4. les foreign keys (comme, pour le cas de segment, texte.id) ne se transmettent par par héritage; elles sont systématiquement ajoutées dans la définition du schéma, ainsi que toutes les autres contraintes.

  5. exemple typique, extrait de la documentation de postgresql: villes et capitales; la table capitale hérite de la table ville (les capitales sont un type spécifique de ville), auquel est ajoutée des propriétés ou contraintes (ex. "état").

  6. Natural Language Processing (analyse automatique de textes en langage naturel).

  7. de la même manière que dans n'importe quel lexique ou dictionnaire, un même forme graphique peut être utilisée dans différentes entrées lexicale: être-verbe, être-nom, etc.

About

schéma de base de données postgresql EAV hybride pour l'analyse de textes en français

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published