Comment créer un ChatGPT privé à l'aide d'une technologie open source ? Téléchargez notre livre blanc gratuit.

Bist-Parser : une implémentation de bout en bout d'un analyseur de dépendances

Cet article est le deuxième et dernier article sur l'analyse des dépendances. Nous vous donnerons quelques directives simples pour la mise en œuvre et les outils pour vous aider à l'améliorer.

Cet article est le deuxième et dernier article sur l'analyse des dépendances. Nous vous donnerons quelques directives simples pour la mise en œuvre et les outils pour vous aider à l'améliorer.

vocabulaire

  • Une banque d'arbres est un corpus de texte analysé qui annote la structure syntaxique ou sémantique des phrases. Les TreeBanks de dépendances sont créés selon différentes approches : soit directement grâce à des annotateurs humains, soit en utilisant des analyseurs automatiques pour fournir une première analyse, puis vérifiés par des annotateurs. Une approche courante consiste à utiliser un processus déterministe pour traduire les TreeBanks existants dans une nouvelle langue au moyen de règles de base. La production d'une TreeBank de haute qualité est à la fois longue et coûteuse.
  • avec LL-U - Computational Natural Language Learning-Universal est une version révisée du format ConLL-X. Les phrases de TreeBanks sont séparées et chaque mot ou signe de ponctuation est placé sur une ligne distincte. Chacun des éléments suivants suit le mot, séparé par des tabulations :
  • ID : index des mots dans la phrase, commençant à 1
  • FORME : forme d'un mot ou ponctuation
  • LEMME : Lemma ou racine de la forme d'un mot
  • UPOS : partie universelle de la balise vocale
  • XPOS : partie de la balise vocale spécifique à la langue ; ne sera pas utilisée dans notre modèle
  • PROUESSES : liste non ordonnée de caractéristiques morphologiques, définies par les dépendances universelles ; indique le genre et le numéro d'un nom, le temps d'un verbe, etc.
  • TÊTE : Tête du mot, indique l'index du mot auquel le mot actuel est lié
  • DEPREL : relation de dépendances universelles ; indique la relation entre deux mots (sujet ou objet d'un verbe, déterminant d'un nom, etc.)
  • DEPS : partie spécifique à la langue des balises vocales ; ne sera pas utilisée dans notre modèle
  • MISC : commentaire ou autre annotation
An example of CoNLL-U format

Exemple de format ConLL-U

  • Une entrée est un mot ou un signe de ponctuation dans une phrase. Il possède de multiples attributs, définis ci-dessus. Une phrase est généralement une concaténation d'entrées (un mot lui-même est un attribut d'une entrée : sa forme), séparées par un espace.
Want to learn how to build a private ChatGPT using open-source technology?

Mise en œuvre

L'implémentation du Bist-Parser provient de les auteurs de son article. Une mise à jour a été publiée sur GitHub par Xiezhq Hermann. Vous pouvez le trouver ici. Il fonctionne sur Python 3.x, avec torch 0.3.1 (avec ou sans Cuda). Il est très complet et peut être utilisé tel quel. Cependant, pour adapter le code à vos données ou les mettre à niveau, vous devez passer par tous les modules, ce qui peut être une tâche difficile. Cette partie de l'article vous guidera à travers tous les fichiers et processus.

Universal Dependencies (UD) est un framework communautaire ouvert pour l'annotation grammaticale. Il fournit des corpus et des outils qui aident grandement à développer un analyseur de dépendances.

Depuis UD, vous pouvez télécharger un corpus de phrases de votre choix (dans toutes les langues disponibles, même l'ancien français !) , utilisez-les tels quels et commencez à entraîner votre Bist-Parser avec ce type de commande :

python src/parser.py --outdir [répertoire de résultats] --train training.conll --dev development.conll --epochs 30 --lstmdims 125 --lstmlayers 2 [--extrn extrn.vector]

Vous pouvez détailler ici les hyperparamètres, capturés par le modèle grâce au fichier parser.py

Comme vous le savez peut-être, lorsque vous entraînez un modèle sur un corpus, le modèle est biaisé en faveur de ce corpus. Vous pourriez entraîner votre modèle sur plusieurs corpus afin de le généraliser davantage. Plusieurs techniques vous permettent d'augmenter vos scores, avec Intégration à TreeBank à titre d'exemple. Ici, nous venons de concaténer quelques TreeBanks, sans aucun autre traitement.

utils

  • Créez un Entrée Conll classe : chaque entrée possède des attributs bien connus : identifiant, forme, lemme, balise PoS universelle, balise PoS spécifique à la langue, caractéristiques morphologiques, tête du mot actuel, relation de dépendance, relation de dépendance améliorée et commentaire. Ces attributs sont définis à partir du format Universal Dependencies ConLL-U. Ce format est utile pour que le modèle comprenne quelles sont ses entrées et ce qu'il doit prévoir.
  • Lisez un fichier ConLL-U et transformez chaque phrase en ConllEntry.
  • Compter le vocabulaire : cette fonction crée un Compteur des attributs ConllEntry et vous permet de savoir comment ces attributs sont distribués dans votre ensemble de données. Si vous souhaitez déterminer les mots ou les relations les plus fréquents dans votre jeu de données, cette fonction peut s'avérer utile.

mstlstm

Ce fichier contient votre modèle. Tous vos hyperparamètres et la majeure partie de votre travail de surveillance sont enregistrés dans ce fichier.

La méthode vers l'avant répète chaque entrée de la phrase. Il calcule d'abord les vecteurs pour chaque attribut d'entrée. Avec notre modèle, nous obtenons plusieurs vecteurs qui décrivent le mot, la balise PoS et les exploits. Ces vecteurs sont ensuite concaténés pour former un vecteur avec une dimension plus grande pour chaque entrée. Ces entrées sont ensuite concaténées pour former le vecteur de phrase.


def forward(self, sentence, errs, lerrs):
        for entry in sentence:
            c = float(self.wordsCount.get(entry.norm, 0))
            # dropFlag = (random.random() < (c / (0.33 + c)))
            dropFlag = (random.random() < (c / (0.25 + c)))
            wordvec = self.wlookup(scalar(
                int(self.vocab.get(entry.norm, 0)) if dropFlag else 0)) if self.wdims > 0 else None
            ontovec = self.olookup(scalar(int(self.onto[entry.onto]) if random.random(
            ) < 0.9 else 0)) if self.odims > 0 else None
            cposvec = self.clookup(scalar(int(self.cpos[entry.cpos]) if random.random(
            ) < 0.9 else 0)) if self.cdims > 0 else None
            posvec = self.plookup(
                scalar(int(self.pos[entry.pos]))) if self.pdims > 0 else None
            # posvec = self.plookup(
            #     scalar(0 if dropFlag and random.random() < 0.1 else int(self.pos[entry.pos]))) if self.pdims > 0 else None
            evec = None
            if self.external_embedding is not None:
                evec = self.elookup(scalar(self.extrnd.get(entry.form, self.extrnd.get(entry.norm, 0)) if (
                    dropFlag or (random.random() < 0.5)) else 0))
            entry.vec = cat([wordvec, posvec, ontovec, cposvec, evec])
            entry.lstms = [entry.vec, entry.vec]
            entry.headfov = None
            entry.modfov = None
            entry.rheadfov = None
            entry.rmodfov = None
        num_vec = len(sentence)
        vec_for = torch.cat(
            [entry.vec for entry in sentence]).view(num_vec, 1, -1)
        vec_back = torch.cat(
            [entry.vec for entry in reversed(sentence)]).view(num_vec, 1, -1)
        res_for_1, self.hid_for_1 = self.lstm_for_1(vec_for, self.hid_for_1)
        res_back_1, self.hid_back_1 = self.lstm_back_1(
            vec_back, self.hid_back_1)
        vec_cat = [cat([res_for_1[i], res_back_1[num_vec - i - 1]])
                   for i in range(num_vec)]
        vec_for_2 = torch.cat(vec_cat).view(num_vec, 1, -1)
        vec_back_2 = torch.cat(list(reversed(vec_cat))).view(num_vec, 1, -1)
        res_for_2, self.hid_for_2 = self.lstm_for_2(vec_for_2, self.hid_for_2)
        res_back_2, self.hid_back_2 = self.lstm_back_2(
            vec_back_2, self.hid_back_2)
        for i in range(num_vec):
            sentence[i].lstms[0] = res_for_2[i]
            sentence[i].lstms[1] = res_back_2[num_vec - i - 1]
        scores, exprs = self.__evaluate(sentence, True)
        gold = [entry.parent_id for entry in sentence]
        heads = decoder.parse_proj(scores, gold)
        for modifier, head in enumerate(gold[1:]):
            rscores, rexprs = self.__evaluateLabel(
                sentence, head, modifier + 1)
            goldLabelInd = self.rels[sentence[modifier + 1].relation]
            wrongLabelInd = \
                max(((l, scr) for l, scr in enumerate(rscores)
                     if l != goldLabelInd), key=itemgetter(1))[0]
            if rscores[goldLabelInd] < rscores[wrongLabelInd] + 1:
                lerrs += [rexprs[wrongLabelInd] - rexprs[goldLabelInd]]
        e = sum([1 for h, g in zip(heads[1:], gold[1:]) if h != g])
        if e > 0:
            errs += [(exprs[h][i] - exprs[g][i])[0]
                     for i, (h, g) in enumerate(zip(heads, gold)) if h != g]
        return e

Tout d'abord, il convertit les entrées en vecteurs. Ici, les principaux attributs sont l'intégration de mots, de lemmes (onto) et de balises PoS (pos). Cependant, nous vous conseillons d'ajouter autant de fonctionnalités que possible. Par exemple, vous pouvez avoir accès à des caractéristiques de mots qui indiquent si le nom est singulier ou pluriel, son genre ou son temps... L'intégration de ces fonctionnalités permet à votre BilsTM de trouver de nombreux autres modèles.

Évolution de l'intégration des PoS sur deux dimensions

Ensuite, il alimente le BilsTM avec ces vecteurs (for = forward, back = back). La ligne 52 évalue les notes de la phrase. Il s'agit de la partie où le digraphe pondéré complet est créé. À la ligne 57, il évalue le score de la relation. Ce modèle présente une astuce intéressante : au lieu d'évaluer toutes les possibilités en même temps (|possibilities|=|arcs|.|labels|, ce qui est bien trop élevé), il prédit d'abord les dépendances, puis les relations.

Nous verrons à peu près erreurs, * erreurs *et e plus tard.

Dans les illustrations ci-dessous, vous pouvez voir l'évolution de l'évaluation des dépendances par lots. Une case bleu foncé correspond à un arc pondéré. L'exemple provient d'une phrase typiquement française, »Les commotions cérébrales sont devenues si courantes dans ce sport qu'on les considère presque comme de la routine.« Vous pouvez repérer des fautes d'orthographe dans la phrase ; ce n'est pas rare dans TreeBanks.

Les commotions cérébrales sont devenu si courantes dans ce sport qu'on les considére presque comme la routine.

Les commotions cérébrales sont devenues si courantes dans ce sport qu'on les considère presque comme de la routine.

Les points remarquables de cette phrase sont le mot « »devenu», qui est la racine (c'est-à-dire le mot principal), et les trois propositions clairement définies séparées par les mots, »que« et »comme.«

Évolution des scores de dépendance pour la phrase « root Les commotions cérébrales sont devenues si courantes dans ce sport que l'on les considère presque comme la routine ».

Scores figure (1) - Initialization

Figure des scores (1) - Initialisation

Scores figure (2) - 200 sentences

Chiffre des scores (2) - 200 phrases

Scores figure (3) - 1000 sentences

Chiffre des scores (3) - 1000 phrases

Score figure (4) - 6000 sentences

Chiffre de score (4) - 6000 phrases

Dans les illustrations ci-dessus, nous pouvons mieux comprendre l'évolution de notre réseau neuronal. Chaque colonne correspond à un jeton en tant que tête, chaque ligne correspond à un jeton en tant que dépendant et chaque cellule correspond au score (ou poids) de l'arc allant de la tête à la personne dépendante.

Nous reconnaissons l'initialisation aléatoire sur la figure 1, où tous les scores sont autour de zéro, et nous ne pouvons voir aucune forme dans la matrice.

Sur la deuxième image, nous pouvons voir que POS=vecteur déterminant a été pris en compte, et nous pouvons distinguer une forme dans leurs colonnes. Les modifications le long des lignes sont moins visibles pour le moment.

Dans la troisième illustration, on distingue clairement les deux propositions, séparées par »que. » Les arcs sont bien définis avant et un peu moins après. Le signe de ponctuation est bien lié à la racine »devenu.«

Dans le dernier cas, nous avons une idée claire des arcs principaux, le modèle gagne en confiance et les valeurs des scores sont plus étalées.

Une fois les dépendances prévues, le modèle prédit le type de relation. Voici les graphiques des scores donnés pour chaque relation, en ce qui concerne la relation prédite, en fonction de la dépendance correcte.

Relation score figure (1) - Initialization

Figure du score de relation (1) - Initialisation

Relation score figure (2) - 200 sentences

Figure du score de relation (2) - 200 phrases

Relation score figure (3) - 1000 sentences

Figure du score de relation (3) - 1000 phrases

Relation score figure (4) - 6000 sentences

Figure du score de relation (4) - 6000 phrases

Le type de relation prédit est en jaune et le type de relation incorrect le plus élevé est en rouge.

Après un peu d'entraînement, la prédiction devient de plus en plus sûre.


def train(self, conll_path):
        print('pytorch version:', torch.__version__)
        batch = 1
        eloss = 0.0
        mloss = 0.0
        eerrors = 0
        etotal = 0
        iSentence = 0
        start = time.time()
        with open(conll_path, 'r') as conllFP:
            shuffledData = list(read_conll(conllFP))
            random.shuffle(shuffledData)
            errs = []
            lerrs = []
            for iSentence, sentence in enumerate(shuffledData):
                self.model.hid_for_1, self.model.hid_back_1, self.model.hid_for_2, self.model.hid_back_2 = [
                    self.model.init_hidden(self.model.ldims) for _ in range(4)]
                if iSentence % 100 == 0 and iSentence != 0:
                    print('Processing sentence number:', iSentence,
                          'Loss:', eloss / etotal,
                          'Errors:', (float(eerrors)) / etotal,
                          'Time', time.time() - start)
                    start = time.time()
                    eerrors = 0
                    eloss = 0.0
                    etotal = 0

                conll_sentence = [entry for entry in sentence if isinstance(
                    entry, utils.ConllEntry)]
                e = self.model.forward(conll_sentence, errs, lerrs)
                eerrors += e
                eloss += e
                mloss += e
                etotal += len(sentence)
                if iSentence % batch == 0 or len(errs) > 0 or len(lerrs) > 0:
                    if len(errs) > 0 or len(lerrs) > 0:
                        eerrs = torch.sum(cat(errs + lerrs))
                        eerrs.backward()
                        self.trainer.step()
                        errs = []
                        lerrs = []
                self.trainer.zero_grad()
        if len(errs) > 0:
            eerrs = (torch.sum(errs + lerrs))
            eerrs.backward()
            self.trainer.step()
        self.trainer.zero_grad()
        print("Loss: ", mloss / iSentence)

La méthode principale du modèle est entraîner. Il mélange d'abord les phrases pour entraîner le modèle dans un ordre différent à chaque fois. Ensuite, la méthode vers l'avant est appelé pour chaque phrase. Cela mettra à jour les poids et renverra un score e et mettez à jour deux listes erreurs et erres. Souvenez-vous que e compte chaque jeton pour lequel la tête prédite est différente de la tête en or (la référence, tirée du corpus). *errs *calcule la perte lors de la prédiction des arcs, et erres calcule la prévision de perte sur les étiquettes. erreurs et erres sont ensuite additionnés pour produire la perte par rétropropagation : des erreurs.

Améliorations et quelques résultats

Afin de créer un modèle encore plus efficace, vous devez essayer d'ajouter la suppression de mots en tant qu'hyperparamètre et essayer différentes valeurs. Nous avons essayé 0, 0,25, 0,33, 0,5, 0,9 et 1 comme valeurs, et la meilleure était 0,9. Cela permet au modèle d'en apprendre beaucoup plus à partir d'autres vecteurs que les mots.

Vous pouvez également séparer chaque fonctionnalité et utiliser l'intégration sur chacune d'elles. Cela permettrait une plus grande flexibilité et une meilleure compréhension du modèle.

Avec une telle implémentation, nous avons obtenu un **score LAS de 87,70** sur French Sequoia (Universal Dependencies) TreeBank, sans formation spécifique sur ce corpus (après traduction des balises, car nos balises sont différentes de celles des UD).

Une fois que votre analyseur de dépendances est entraîné, vous pouvez utiliser les dépendances et les relations pour mieux comprendre vos données. Même si un modèle NLU est capable de trouver de nombreux modèles, l'ajout de ces entrées peut constituer un réel avantage à prendre en compte.

Les sources de cet article sont les suivantes :

Callout

Créez votre pipeline NLP gratuitement
Commencez ->