L'une des tâches principales des systèmes de dialogue est non seulement de fournir les informations dont l'utilisateur a besoin, mais aussi de générer autant de réponses humaines que possible. Et la reconnaissance des émotions de l’interlocuteur n’est plus seulement une caractéristique intéressante, c’est une nécessité vitale. Dans cet article, nous nous pencherons sur l'
architecture d'un réseau neuronal récurrent pour déterminer les émotions dans les conversations textuelles , qui a participé au
SemEval-2019 Tâche 3 «EmoContext» , le concours annuel en linguistique informatique. La tâche consistait à classer les émotions («heureux», «triste», «en colère» et «autres») dans une conversation de trois remarques, à laquelle un robot de discussion et une personne ont participé.
Dans la première partie de l'article, nous examinerons l'ensemble des tâches dans EmoContext et les données fournies par les organisateurs. Dans les deuxième et troisième parties, nous analysons le traitement préliminaire du texte et les modes de représentation vectorielle des mots. Dans la quatrième partie, nous décrivons l'architecture LSTM que nous avons utilisée dans la compétition. Le code est écrit en Python à l'aide de la bibliothèque Keras.
1. Données d'entraînement
Le titre «EmoContext» du SemEval-2019 était dédié à la définition des émotions dans les conversations textuelles, en tenant compte du contexte de la correspondance. Le contexte dans ce cas est plusieurs remarques consécutives des participants au dialogue. Il y a deux participants à la conversation: un utilisateur anonyme (il possède la première et la troisième réplique) et un robot de discussion
Ruuh (il possède la deuxième réplique). Sur la base de trois répliques, il est nécessaire de déterminer l'émotion ressentie par l'utilisateur lors de la rédaction d'une réponse au chatbot (tableau 1). Au total, le balisage de l'ensemble de données contenait quatre émotions: «heureux», «triste», «en colère» ou «autres» (tableau 1). Une description détaillée est présentée ici: (
Chatterjee et al., 2019 ).
Tableau 1. Exemples de l'ensemble de données EmoContext ( Chatterjee et al., 2019 )Pendant le concours, les organisateurs ont fourni plusieurs ensembles de données. Le jeu de données de formation (Train) comprenait 30 160 textes marqués manuellement. Dans ces textes, il y avait environ 5 000 objets appartenant aux classes «heureux», «triste» et «en colère», ainsi que 15 000 textes de la classe «autres» (tableau 2).
Les organisateurs ont également fourni des ensembles de données pour le développement (Dev) et les tests (Test), dans lesquels, contrairement au jeu de données de formation, la distribution par classe d'émotions correspondait à la vie réelle: environ 4% pour chacune des classes «heureux», «triste» et « en colère ", et le reste est la classe" autres ". Données fournies par Microsoft, vous pouvez les télécharger dans le
groupe officiel sur LinkedIn .
Tableau 2. Distribution des étiquettes de classe d'émotion dans l'ensemble de données ( Chatterjee et al., 2019 ).En plus de ces données, nous avons collecté 900 000 messages en anglais sur Twitter pour créer un ensemble de données Distant (300 000 tweets pour chaque émotion). Lors de sa création, nous avons suivi la stratégie de Go et al. (2009), dans le cadre desquels les messages étaient simplement associés à la présence de mots liés aux émotions, tels que #angry, #annoyed, #happy, #sad, #surprised, etc. La liste des termes est basée sur les termes de SemEval-2018 AIT DISC (
Duppada et al., 2018 ).
La principale mesure de qualité dans le concours EmoContext est la mesure F1 moyenne pour les trois classes d'émotions, c'est-à-dire pour les classes «heureux», «triste» 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 du texte
Avant la formation, nous avons prétraité les textes à l'aide de l'outil Ekphrasis (Baziotis et al., 2017). Il permet de corriger l'orthographe, de normaliser les mots, de segmenter et également de déterminer quels jetons doivent être supprimés, normalisés ou annotés à l'aide de balises spéciales. Au stade du prétraitement, nous avons fait ce qui suit:
- Les URL et le courrier, la date et l'heure, les surnoms, les pourcentages, les devises et les nombres ont été remplacés par des balises correspondantes.
- Des termes majuscules répétés, censurés et allongés accompagnés d'étiquettes appropriées.
- Les mots allongés ont été automatiquement corrigés.
De plus, Emphasis contient un tokenizer qui peut identifier la plupart des emojis, émoticônes et expressions complexes, ainsi que les dates, heures, devises et acronymes.
Tableau 3. Exemples de prétraitement de texte. 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(
3. Représentation vectorielle des mots
La représentation vectorielle est devenue une partie intégrante de la plupart des approches de la création de systèmes PNL utilisant l'apprentissage en profondeur. Pour déterminer les modèles de cartographie vectorielle les plus appropriés, nous avons essayé Word2Vec (
Mikolov et al., 2013 ), GloVe (
Pennington et al., 2014 ) et FastText (
Joulin et al., 2017 ), ainsi que des vecteurs DataStories pré-formés (
Baziotis et al. ., 2017 ). Word2Vec trouve des relations entre les mots en supposant que les mots sémantiquement liés se trouvent dans des contextes similaires. Word2Vec essaie de prédire le mot cible (architecture CBOW) ou le contexte (architecture Skip-Gram), c'est-à-dire minimiser la fonction de perte, et GloVe calcule des vecteurs de mots, réduisant la dimension de la matrice d'adjacence. La logique de FastText est similaire à la logique de Word2Vec, sauf qu'elle utilise des n-grammes symboliques pour créer des vecteurs de mots et, par conséquent, elle peut résoudre le problème des mots inconnus.
Pour tous les modèles mentionnés, nous utilisons les paramètres de formation par défaut fournis par les auteurs. Nous avons formé un modèle LSTM simple (dim = 64) basé sur chacune de ces représentations vectorielles et comparé l'efficacité de la classification en utilisant la validation croisée. Le meilleur résultat dans les mesures F1 a été montré par des vecteurs DataStories pré-formés.
Pour enrichir l'affichage vectoriel sélectionné avec la coloration émotionnelle des mots, nous avons décidé d'affiner les vecteurs à l'aide du
jeu de données Distant automatiquement étiqueté (
Deriu et al., 2017 ). Nous avons utilisé l'ensemble de données Distant pour former un réseau LSTM simple afin de classer les messages «diaboliques», «tristes» et «heureux». La couche d'intégration a été gelée lors de la première itération de l'entraînement afin d'éviter de forts changements dans les poids des vecteurs, et pour les cinq prochaines itérations, la couche a été décongelée. Après l'entraînement, les vecteurs «retardés» ont été enregistrés pour une utilisation ultérieure dans le réseau neuronal, ainsi
que partagés .
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
Les réseaux de neurones récurrents (RNN) sont une famille de réseaux de neurones qui se spécialisent dans le traitement d'une série d'événements. Contrairement aux réseaux de neurones traditionnels, les RNN sont conçus pour fonctionner avec des séquences utilisant des équilibres internes. Pour cela, le graphe de calcul RNN contient des cycles qui reflètent l'influence des informations précédentes de la séquence d'événements sur l'actuel. Les réseaux de neurones LSTM (Long Short-Term Memory) ont été introduits comme une extension de RNN en 1997 (
Hochreiter et Schmidhuber, 1997 ). Les cellules de récurrence LSTM sont connectées pour éviter les problèmes d'éclatement et de fondu. Les LSTM traditionnels ne conservent que les informations passées car ils traitent la séquence dans une seule direction. Les LSTM bidirectionnels fonctionnant dans les deux directions combinent la sortie de deux couches LSTM cachées qui transmettent des informations dans des directions opposées - l'une au cours du temps et l'autre contre - recevant ainsi simultanément des données des états passés et futurs (
Schuster et Paliwal, 1997 ).
Figure 1: Version réduite de l'architecture. Le module LSTM utilise les mêmes poids pour les première et troisième étapes.Une représentation simplifiée de l'approche décrite est illustrée à la figure 1. L'architecture du réseau neuronal se compose d'une couche d'intégration et de deux modules LTSM bidirectionnels (dim = 64). Le premier module LTSM analyse les mots du premier utilisateur (c'est-à-dire les première et troisième répliques de la conversation), et le deuxième module analyse les mots du deuxième utilisateur (deuxième réplique). À la première étape, les mots de chaque utilisateur utilisant des représentations vectorielles pré-entraînées sont introduits dans le module LTSM bidirectionnel correspondant. Ensuite, les trois cartes d'entités résultantes sont combinées en un vecteur d'entités plat, puis transférées vers une couche cachée entièrement connectée (dim = 30), qui analyse les interactions entre les entités extraites. Enfin, ces caractéristiques sont traitées dans la couche de sortie à l'aide de la fonction d'activation softmax pour déterminer l'étiquette de classe finale. Pour réduire le surajustement, après les couches de la représentation vectorielle, des couches de régularisation avec bruit gaussien ont été ajoutées et des couches de décrochage ont été ajoutées à chaque module LTSM (p = 0,2) et à une couche entièrement connectée cachée (p = 0,1) (
Srivastava et al., 2014 )
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 la recherche de l'architecture optimale, nous avons expérimenté non seulement le nombre de neurones dans les couches, les fonctions d'activation et les paramètres de régularisation, mais aussi l'architecture du réseau neuronal lui-même. Ceci est décrit plus en détail dans l'
œuvre originale .
L'architecture décrite dans la section précédente a montré les meilleurs résultats lors de la formation sur le jeu de données Train et la validation sur le jeu de données Dev, elle a donc été utilisée dans la phase finale de la compétition. Lors du dernier ensemble de données de test, le modèle a montré une mesure F1 micro-moyenne de 72,59%, et le résultat maximum atteint parmi tous les participants était de 79,59%. Néanmoins, notre résultat était bien supérieur à la valeur de référence de 58,68% fixée par les organisateurs.
Le code source du modèle et de la représentation vectorielle des mots est disponible sur GitHub.
La version complète de l'article et le
travail avec la description de la tâche se trouvent sur le site Web d'ACL Anthology.
L'ensemble de données de formation peut être téléchargé à partir du groupe officiel LinkedIn.
Citant:
@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", }