Détection d'émotions contextuelles dans les conversations textuelles à l'aide de réseaux de neurones


De nos jours, parler aux agents conversationnels devient une routine quotidienne, et il est crucial pour les systèmes de dialogue de générer des réponses aussi humaines que possible. Comme l'un des principaux aspects, une attention particulière devrait être accordée à la fourniture de réponses sensibles aux émotions aux utilisateurs. Dans cet article, nous allons décrire l'architecture de réseau de neurones récurrente pour la détection des émotions dans les conversations textuelles , qui a participé à la tâche 3 d'EmoContext SemEval-2019 , c'est-à-dire un atelier annuel sur l'évaluation sémantique. L'objectif de la tâche est de classer l'émotion (c'est-à-dire heureuse, triste, en colère et autres) dans un ensemble de données conversationnelles à 3 tours.

Le reste de l'article est organisé comme suit. La section 1 donne un bref aperçu de la tâche EmoContext et des données fournies. Les sections 2 et 3 se concentrent par conséquent sur le prétraitement des textes et l'intégration des mots. Dans la section 4, nous avons décrit l'architecture du modèle LSTM utilisé dans notre mémoire. En conclusion, les performances finales de notre système et le code source sont présentés. Le modèle est implémenté en Python à l'aide de la bibliothèque Keras.

1. Données de formation


La Tâche 3 «EmoContext» du SemEval-2019 se concentre sur la détection contextuelle des émotions dans les conversations textuelles. Dans EmoContext, étant donné un énoncé utilisateur textuel avec 2 tours de contexte dans une conversation, nous devons classer si l'émotion du prochain énoncé utilisateur est «heureuse», «triste», «en colère» ou «autres» (tableau 1). Il n'y a que deux participants à la conversation: une personne anonyme (Tuen-1 et Turn-3) et le chatbot basé sur l'IA Ruuh (Turn-2). Pour une description détaillée, voir ( Chatterjee et al., 2019 ).

Tableau 1. Exemples montrant l'ensemble de données EmoContext ( Chatterjee et al., 2019 )
Utilisateur (Turn-1)Agent conversationnel (Turn-1)Utilisateur (Turn-2)Véritable classe
Je viens de me qualifier pour le stage NabardWOOT! Voilà d'excellentes nouvelles. Félicitations!J'ai commencé à pleurerHeureux
Comment osez-vous gifler mon enfantSi vous gâtez ma voiture, je vous le ferai aussiEssayez juste de le faire une foisFâché
J'ai été blessé par toi plusTu ne le pensais pas.dis que tu m'aimesTriste
Je ferai la nuit.D'accord. Tenez-moi au courant.Ne pas donner WhatsApp non.Autres

Pendant le concours, nous avons eu accès à 30160 textes étiquetés par des humains fournis par des organisateurs de tâches, où environ 5000 échantillons chacun de la classe «en colère», «triste», «heureux» et 15000 pour la classe «autres» (tableau 2). Les kits de développement et de test, qui étaient également fournis par les organisateurs, contrairement à un ensemble de trains, ont une distribution réelle, qui est d'environ 4% pour chaque classe émotionnelle et le reste pour la classe «autres». Les données fournies par Microsoft et peuvent être trouvées dans le groupe officiel LinkedIn .

Tableau 2. Distribution des étiquettes de classe d'émotion dans les jeux de données ( Chatterjee et al., 2019 ).
Jeu de donnéesHeureuxTristeFâchéAutresTotal
Train
14,07%
18,11%
18,26%
49,56%
30160
Dev
5,15%
4,54%
5,45%
84,86%
2755
Test
5,16%
4,54%
5,41%
84,90%
5509
Éloigné
33,33%
33,33%
33,33%
0%
900k

En plus de ces données, nous avons collecté 900k tweets anglais afin de créer un ensemble de données éloigné de 300k tweets pour chaque émotion. Pour former l'ensemble de données distant, nous nous sommes basés sur la stratégie de Go et al. (2009), dans lesquels nous associons simplement les tweets à la présence de mots liés aux émotions tels que `` #angry '', `` nannoyed '', '#happy', '#sad,' #surprised ', etc. La liste des termes de requête était basée sur les termes de requête de SemEval-2018 AIT DISC ( Duppada et al., 2018 ).

La mesure de performance clé d'EmoContext est un score F1 micro-moyen pour trois classes d'émotions, à savoir «triste», «heureux» et «en colère».

def preprocessData(dataFilePath, mode): conversations = [] labels = [] with io.open(dataFilePath, encoding="utf8") as finput: finput.readline() for line in finput: line = line.strip().split('\t') for i in range(1, 4): line[i] = tokenize(line[i]) if mode == "train": labels.append(emotion2label[line[4]]) conv = line[1:4] conversations.append(conv) if mode == "train": return np.array(conversations), np.array(labels) else: return np.array(conversations) texts_train, labels_train = preprocessData('./starterkitdata/train.txt', mode="train") texts_dev, labels_dev = preprocessData('./starterkitdata/dev.txt', mode="train") texts_test, labels_test = preprocessData('./starterkitdata/test.txt', mode="train") 

2. Prétraitement des textes


Avant toute étape de formation, les textes ont été prétraités par l'outil de texte Ekphrasis (Baziotis et al., 2017). Cet outil permet d'effectuer la correction orthographique, la normalisation des mots, la segmentation et permet de spécifier quels jetons doivent être omis, normalisés ou annotés avec des balises spéciales. Nous avons utilisé les techniques suivantes pour l'étape de prétraitement.

  • Les URL, les e-mails, la date et l'heure, les noms d'utilisateur, le pourcentage, les devises et les nombres ont été remplacés par les balises correspondantes.
  • Les termes répétés, censurés, allongés et en majuscules ont été annotés avec les balises correspondantes.
  • Les mots allongés ont été automatiquement corrigés sur la base du corpus de statistiques de mots intégré.
  • Des hashtags et des décompactations (c'est-à-dire la segmentation des mots) ont été effectués sur la base d'un corpus de statistiques de mots intégré.
  • Un dictionnaire créé manuellement pour remplacer les termes extraits du texte a été utilisé afin de réduire une variété d'émotions.

De plus, Emphasis fournit le tokenizer qui est capable d'identifier la plupart des emojis, émoticônes et expressions compliquées telles que les mots censurés, accentués et allongés ainsi que les dates, les heures, les devises et les acronymes.

Tableau 3. Exemples de prétraitement de texte.
Texte originalTexte prétraité
JE ME SENS ... Je me brise en millions de morceaux <allcaps> je vous sens </allcaps>. <répété> je me brise en millions de morceaux
fatigué et tu m'as manqué aussi :‑(fatigué et tu m'as manqué aussi <sad>
vous devez vous y connecter: www.youtube.com/watch?v=99myH1orbs4vous devriez écouter <allongé> à ceci: <url>
Mon appartement s'en occupe. Mon loyer est d'environ 650 $.mon appartement s'en occupe. mon loyer est d'environ <argent>.

 from ekphrasis.classes.preprocessor import TextPreProcessor from ekphrasis.classes.tokenizer import SocialTokenizer from ekphrasis.dicts.emoticons import emoticons import numpy as np import re import io label2emotion = {0: "others", 1: "happy", 2: "sad", 3: "angry"} emotion2label = {"others": 0, "happy": 1, "sad": 2, "angry": 3} emoticons_additional = { '(^・^)': '<happy>', ':‑c': '<sad>', '=‑d': '<happy>', ":'‑)": '<happy>', ':‑d': '<laugh>', ':‑(': '<sad>', ';‑)': '<happy>', ':‑)': '<happy>', ':\\/': '<sad>', 'd=<': '<annoyed>', ':‑/': '<annoyed>', ';‑]': '<happy>', '(^ ^)': '<happy>', 'angru': 'angry', "d‑':": '<annoyed>', ":'‑(": '<sad>', ":‑[": '<annoyed>', '( ? )': '<happy>', 'x‑d': '<laugh>', } text_processor = TextPreProcessor( # terms that will be normalized normalize=['url', 'email', 'percent', 'money', 'phone', 'user', 'time', 'url', 'date', 'number'], # terms that will be annotated annotate={"hashtag", "allcaps", "elongated", "repeated", 'emphasis', 'censored'}, fix_html=True, # fix HTML tokens # corpus from which the word statistics are going to be used # for word segmentation segmenter="twitter", # corpus from which the word statistics are going to be used # for spell correction corrector="twitter", unpack_hashtags=True, # perform word segmentation on hashtags unpack_contractions=True, # Unpack contractions (can't -> can not) spell_correct_elong=True, # spell correction for elongated words # select a tokenizer. You can use SocialTokenizer, or pass your own # the tokenizer, should take as input a string and return a list of tokens tokenizer=SocialTokenizer(lowercase=True).tokenize, # list of dictionaries, for replacing tokens extracted from the text, # with other expressions. You can pass more than one dictionaries. dicts=[emoticons, emoticons_additional] ) def tokenize(text): text = " ".join(text_processor.pre_process_doc(text)) return text 

3. Intégrations de mots


L'incorporation de mots est devenue une partie essentielle de toute approche d'apprentissage en profondeur pour les systèmes de PNL. Pour déterminer les vecteurs les plus appropriés pour la tâche de détection des émotions, nous essayons les modèles Word2Vec ( Mikolov et al., 2013 ), GloVe ( Pennington et al., 2014 ) et FastText ( Joulin et al., 2017 ) ainsi que DataStories pré-formés vecteurs de mots ( Baziotis et al., 2017 ). Le concept clé de Word2Vec est de localiser des mots, qui partagent des contextes communs dans le corpus d'apprentissage, à proximité immédiate de l'espace vectoriel. Les modèles Word2Vec et Glove apprennent les encodages géométriques des mots à partir de leurs informations de co-occurrence, mais essentiellement le premier est un modèle prédictif et le dernier est un modèle basé sur le comptage. En d'autres termes, alors que Word2Vec essaie de prédire un mot cible (architecture CBOW) ou un contexte (architecture Skip-gram), c'est-à-dire pour minimiser la fonction de perte, GloVe calcule des vecteurs de mots en réduisant la dimensionnalité sur la matrice de comptage de cooccurrence. FastText est très similaire à Word2Vec à l'exception du fait qu'il utilise des n-grammes de caractères pour apprendre les vecteurs de mots, il est donc capable de résoudre le problème de vocabulaire.

Pour toutes les techniques mentionnées ci-dessus, nous avons utilisé les landaus de formation par défaut fournis par les auteurs. Nous formons un modèle LSTM simple (dim = 64) basé sur chacun de ces plongements et comparons l'efficacité en utilisant la validation croisée. Selon le résultat, les intégrations pré-entraînées de DataStories ont démontré le meilleur score F1 moyen.

Pour enrichir les incorporations de mots sélectionnés avec la polarité émotionnelle des mots, nous envisageons d'effectuer une phrase de pré-apprentissage à distance par un réglage fin des incorporations sur l'ensemble de données distant étiqueté automatiquement. L'importance de l'utilisation de la pré-formation a été démontrée dans ( Deriu et al., 201 7). Nous utilisons l'ensemble de données distant pour former le réseau LSTM simple à classer les tweets en colère, tristes et heureux. La couche des plongements a été gelée pour la première époque d'entraînement afin d'éviter des changements importants dans les poids des plongements, puis elle a été dégelée pour les 5 époques suivantes. Après la phase de formation, les intégrations affinées ont été enregistrées pour les phases de formation ultérieures et rendues publiques .

 def getEmbeddings(file): embeddingsIndex = {} dim = 0 with io.open(file, encoding="utf8") as f: for line in f: values = line.split() word = values[0] embeddingVector = np.asarray(values[1:], dtype='float32') embeddingsIndex[word] = embeddingVector dim = len(embeddingVector) return embeddingsIndex, dim def getEmbeddingMatrix(wordIndex, embeddings, dim): embeddingMatrix = np.zeros((len(wordIndex) + 1, dim)) for word, i in wordIndex.items(): embeddingMatrix[i] = embeddings.get(word) return embeddingMatrix from keras.preprocessing.text import Tokenizer embeddings, dim = getEmbeddings('emosense.300d.txt') tokenizer = Tokenizer(filters='') tokenizer.fit_on_texts([' '.join(list(embeddings.keys()))]) wordIndex = tokenizer.word_index print("Found %s unique tokens." % len(wordIndex)) embeddings_matrix = getEmbeddingMatrix(wordIndex, embeddings, dim) 

4. Architecture du réseau neuronal


Un réseau neuronal récurrent (RNN) est une famille de réseaux neuronaux artificiels spécialisés dans le traitement de données séquentielles. Contrairement aux réseaux de neurones traditionnels, les RRN sont conçus pour traiter des données séquentielles en partageant leurs poids internes traitant la séquence. À cet effet, le graphe de calcul des RRN comprend des cycles, représentant l'influence des informations précédentes sur la présente. Dans le prolongement des RNN, des réseaux de mémoire à long terme (LSTM) ont été introduits en 1997 ( Hochreiter et Schmidhuber, 1997 ). Dans les LSTM, les cellules récurrentes sont connectées d'une manière particulière pour éviter la disparition et l'explosion des problèmes de gradient. Les LSTM traditionnels ne conservent que les informations du passé car ils ne traitent la séquence que dans une seule direction. Les LSTM bidirectionnels combinent la sortie de deux couches LSTM cachées se déplaçant dans des directions opposées, où l'une avance dans le temps et l'autre recule dans le temps, permettant ainsi de capturer simultanément des informations sur les états passés et futurs ( Schuster et Paliwal, 1997 ).


Figure 1: L'architecture d'une version plus petite de l'architecture proposée. Les unités LSTM pour le premier et le troisième tour ont des poids communs.

Un aperçu de haut niveau de notre approche est fourni à la figure 1. L'architecture proposée du réseau de neurones se compose de l'unité d'intégration et de deux unités LSTM bidirectionnelles (dim = 64). L'ancienne unité LSTM est destinée à analyser la parole du premier utilisateur (c'est-à-dire le premier tour et le troisième tour de la conversation), et la dernière est destinée à analyser la parole du deuxième utilisateur (c'est-à-dire le deuxième tour). Ces deux unités apprennent non seulement la représentation des fonctionnalités sémantiques et des sentiments, mais également la façon de capturer les fonctionnalités de conversation spécifiques à l'utilisateur, ce qui permet de classer les émotions plus précisément. À la première étape, chaque énoncé utilisateur est introduit dans une unité LSTM bidirectionnelle correspondante en utilisant des incorporations de mots pré-entraînées. Ensuite, ces trois cartes d'entités sont concaténées dans un vecteur d'entités aplati puis transmises à une couche cachée entièrement connectée (dim = 30), qui analyse les interactions entre les vecteurs obtenus. Enfin, ces fonctionnalités traversent la couche de sortie avec la fonction d'activation softmax pour prédire une étiquette de classe finale. Pour réduire le sur-ajustement, des couches de régularisation avec du bruit gaussien ont été ajoutées après la couche d'intégration, des couches de décrochage ( Srivastava et al., 2014 ) ont été ajoutées à chaque unité LSTM (p = 0,2) et avant la couche cachée entièrement connectée (p = 0,1).

 from keras.layers import Input, Dense, Embedding, Concatenate, Activation, \ Dropout, LSTM, Bidirectional, GlobalMaxPooling1D, GaussianNoise from keras.models import Model def buildModel(embeddings_matrix, sequence_length, lstm_dim, hidden_layer_dim, num_classes, noise=0.1, dropout_lstm=0.2, dropout=0.2): turn1_input = Input(shape=(sequence_length,), dtype='int32') turn2_input = Input(shape=(sequence_length,), dtype='int32') turn3_input = Input(shape=(sequence_length,), dtype='int32') embedding_dim = embeddings_matrix.shape[1] embeddingLayer = Embedding(embeddings_matrix.shape[0], embedding_dim, weights=[embeddings_matrix], input_length=sequence_length, trainable=False) turn1_branch = embeddingLayer(turn1_input) turn2_branch = embeddingLayer(turn2_input) turn3_branch = embeddingLayer(turn3_input) turn1_branch = GaussianNoise(noise, input_shape=(None, sequence_length, embedding_dim))(turn1_branch) turn2_branch = GaussianNoise(noise, input_shape=(None, sequence_length, embedding_dim))(turn2_branch) turn3_branch = GaussianNoise(noise, input_shape=(None, sequence_length, embedding_dim))(turn3_branch) lstm1 = Bidirectional(LSTM(lstm_dim, dropout=dropout_lstm)) lstm2 = Bidirectional(LSTM(lstm_dim, dropout=dropout_lstm)) turn1_branch = lstm1(turn1_branch) turn2_branch = lstm2(turn2_branch) turn3_branch = lstm1(turn3_branch) x = Concatenate(axis=-1)([turn1_branch, turn2_branch, turn3_branch]) x = Dropout(dropout)(x) x = Dense(hidden_layer_dim, activation='relu')(x) output = Dense(num_classes, activation='softmax')(x) model = Model(inputs=[turn1_input, turn2_input, turn3_input], outputs=output) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc']) return model model = buildModel(embeddings_matrix, MAX_SEQUENCE_LENGTH, lstm_dim=64, hidden_layer_dim=30, num_classes=4) 

5. Résultats


Dans le processus de recherche d'une architecture optimale, nous avons expérimenté non seulement le nombre de cellules en couches, les fonctions d'activation et les paramètres de régularisation mais également l'architecture du réseau neuronal. Les informations détaillées sur cette phrase peuvent être trouvées dans l'article original .

Le modèle décrit dans la section précédente a démontré les meilleurs scores sur l'ensemble de données de développement, il a donc été utilisé dans la phase d'évaluation finale du concours. Sur l'ensemble de données de test final, il a obtenu un score F1 micro-moyen de 72,59% pour les classes émotionnelles, tandis que le score maximum parmi tous les participants était de 79,59%. Cependant, c'est bien au-dessus de la ligne de base officielle publiée par les organisateurs de tâches, qui était de 58,68%.

Le code source du modèle et les intégrations de mots sont disponibles sur GitHub.
La version complète de l'article et le document de description des tâches sont disponibles sur ACL Anthology.
L'ensemble de données d'entraînement est situé dans le groupe de compétition officiel sur LinkedIn.

Citation:

 @inproceedings{smetanin-2019-emosense, title = "{E}mo{S}ense at {S}em{E}val-2019 Task 3: Bidirectional {LSTM} Network for Contextual Emotion Detection in Textual Conversations", author = "Smetanin, Sergey", booktitle = "Proceedings of the 13th International Workshop on Semantic Evaluation", year = "2019", address = "Minneapolis, Minnesota, USA", publisher = "Association for Computational Linguistics", url = "https://www.aclweb.org/anthology/S19-2034", pages = "210--214", } 

Source: https://habr.com/ru/post/fr439850/


All Articles