Eine der Hauptaufgaben von Dialogsystemen besteht nicht nur darin, die Informationen bereitzustellen, die der Benutzer benötigt, sondern auch so viele menschliche Antworten wie möglich zu generieren. Und das Erkennen der Emotionen des Gesprächspartners ist nicht mehr nur ein cooles Feature, sondern eine wichtige Notwendigkeit. In diesem Artikel werden wir uns mit der
Architektur eines wiederkehrenden neuronalen Netzwerks zur Bestimmung von Emotionen in Textkonversationen befassen , das am
SemEval-2019 Task 3 „EmoContext“ , dem jährlichen Wettbewerb für Computerlinguistik, teilgenommen hat. Die Aufgabe bestand darin, Emotionen („glücklich“, „traurig“, „wütend“ und „andere“) in einem Gespräch mit drei Bemerkungen zu klassifizieren, an dem ein Chat-Bot und eine Person teilnahmen.
Im ersten Teil des Artikels werden wir die in EmoContext festgelegte Aufgabe und die von den Organisatoren bereitgestellten Daten betrachten. Im zweiten und dritten Teil analysieren wir die vorläufige Verarbeitung des Textes und die Art und Weise der Vektordarstellung von Wörtern. Im vierten Teil beschreiben wir die LSTM-Architektur, die wir im Wettbewerb verwendet haben. Der Code wird in Python mithilfe der Keras-Bibliothek geschrieben.
1. Trainingsdaten
Der Titel „EmoContext“ auf der SemEval-2019 widmete sich der Definition von Emotionen in Textkonversationen unter Berücksichtigung des Kontextes der Korrespondenz. Der Kontext in diesem Fall sind mehrere aufeinanderfolgende Bemerkungen von Dialogteilnehmern. Es gibt zwei Teilnehmer an der Konversation: einen anonymen Benutzer (er besitzt die erste und dritte Replik) und einen Chat-Bot
Ruuh (er besitzt die zweite Replik). Anhand von drei Replikaten muss ermittelt werden, welche Emotionen der Benutzer beim Schreiben einer Antwort auf den Chatbot verspürt hat (Tabelle 1). Insgesamt enthielt das Markup des Datensatzes vier Emotionen: „glücklich“, „traurig“, „wütend“ oder „andere“ (Tabelle 1). Eine detaillierte Beschreibung wird hier vorgestellt: (
Chatterjee et al., 2019 ).
Tabelle 1. Beispiele aus dem EmoContext-Datensatz ( Chatterjee et al., 2019 )Während des Wettbewerbs stellten die Organisatoren mehrere Datensätze zur Verfügung. Der Trainingsdatensatz (Train) bestand aus 30.160 manuell markierten Texten. In diesen Texten befanden sich ungefähr 5000 Objekte der Klassen „glücklich“, „traurig“ und „wütend“ sowie 15000 Texte der Klasse „andere“ (Tabelle 2).
Die Organisatoren stellten auch Datensätze für Entwicklung (Dev) und Test (Test) zur Verfügung, in denen im Gegensatz zum Trainingsdatensatz die Verteilung nach Emotionsklassen dem realen Leben entsprach: etwa 4% für jede der Klassen „glücklich“, „traurig“ und „ wütend ", und der Rest ist die Klasse" andere ". Von Microsoft bereitgestellte Daten können Sie in der
offiziellen Gruppe auf LinkedIn herunterladen.
Tabelle 2. Verteilung der Emotionsklassenbezeichnungen im Datensatz ( Chatterjee et al., 2019 ).Zusätzlich zu diesen Daten haben wir 900.000 englischsprachige Nachrichten von Twitter gesammelt, um einen entfernten Datensatz zu erstellen (300.000 Tweets für jede Emotion). Bei der Erstellung folgten wir der Strategie von Go et al. (2009), in dessen Rahmen Botschaften einfach mit dem Vorhandensein von Wörtern in Verbindung gebracht wurden, die sich auf Emotionen beziehen, wie z. B. #angry, #annoyed, #happy, #sad, #surprised und so weiter. Die Liste der Begriffe basiert auf den Begriffen von SemEval-2018 AIT DISC (
Duppada et al., 2018 ).
Die Hauptqualitätsmetrik im EmoContext-Wettbewerb ist das durchschnittliche F1-Maß für die drei Klassen von Emotionen, dh für die Klassen „glücklich“, „traurig“ und „wütend“.
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. Textvorverarbeitung
Vor dem Training haben wir die Texte mit dem Ekphrasis-Tool vorverarbeitet (Baziotis et al., 2017). Es hilft, Rechtschreibung zu korrigieren, Wörter und Segmente zu normalisieren und mithilfe spezieller Tags zu bestimmen, welche Token gelöscht, normalisiert oder mit Anmerkungen versehen werden sollen. In der Vorverarbeitungsphase haben wir Folgendes durchgeführt:
- URLs und E-Mails, Datum und Uhrzeit, Spitznamen, Prozentsätze, Währungen und Zahlen wurden durch entsprechende Tags ersetzt.
- Wiederholte, zensierte, verlängerte Großbuchstaben werden von entsprechenden Etiketten begleitet.
- Längliche Wörter wurden automatisch korrigiert.
Darüber hinaus enthält Emphasis einen Tokenizer, mit dem die meisten Emojis, Emoticons und komplexen Ausdrücke sowie Datums-, Uhrzeit-, Währungs- und Akronyme identifiziert werden können.
Tabelle 3. Beispiele für die Textvorverarbeitung. 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. Vektordarstellung von Wörtern
Die Vektordarstellung ist ein wesentlicher Bestandteil der meisten Ansätze zur Erstellung von NLP-Systemen mithilfe von Deep Learning geworden. Um die am besten geeigneten Vektorkartierungsmodelle zu bestimmen, haben wir Word2Vec (
Mikolov et al., 2013 ), GloVe (
Pennington et al., 2014 ) und FastText (
Joulin et al., 2017 ) sowie vorab trainierte DataStories-Vektoren (
Baziotis et al.) Versucht. ., 2017 ). Word2Vec findet Beziehungen zwischen Wörtern, indem angenommen wird, dass semantisch verwandte Wörter in ähnlichen Kontexten gefunden werden. Word2Vec versucht, das Zielwort (CBOW-Architektur) oder den Kontext (Skip-Gram-Architektur) vorherzusagen, dh die Verlustfunktion zu minimieren, und GloVe berechnet Wortvektoren, wodurch die Dimension der Adjazenzmatrix verringert wird. Die Logik von FastText ähnelt der Logik von Word2Vec, verwendet jedoch symbolische n-Gramm, um Wortvektoren zu erstellen, und kann daher das Problem unbekannter Wörter lösen.
Für alle genannten Modelle verwenden wir die von den Autoren angegebenen Standard-Trainingsparameter. Wir haben ein einfaches LSTM-Modell (dim = 64) basierend auf jeder dieser Vektordarstellungen trainiert und die Klassifizierungseffizienz mithilfe einer Kreuzvalidierung verglichen. Das beste Ergebnis bei F1-Messungen wurde durch vorab trainierte DataStories-Vektoren gezeigt.
Um die ausgewählte Vektorkartierung mit der emotionalen Färbung von Wörtern anzureichern, haben wir beschlossen, die Vektoren mithilfe des automatisch beschrifteten Distant-
Datensatzes zu optimieren (
Deriu et al., 2017 ). Wir haben den Distant-Datensatz verwendet, um ein einfaches LSTM-Netzwerk zu trainieren, um "böse", "traurige" und "glückliche" Nachrichten zu klassifizieren. Die Einbettungsschicht wurde während der ersten Iteration des Trainings eingefroren, um starke Änderungen in den Gewichten der Vektoren zu vermeiden, und für die nächsten fünf Iterationen wurde die Schicht aufgetaut. Nach dem Training wurden die "verzögerten" Vektoren für die spätere Verwendung im neuronalen Netzwerk gespeichert
und gemeinsam genutzt .
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. Neuronale Netzwerkarchitektur
Recurrent Neural Networks (RNNs) sind eine Familie neuronaler Netze, die sich auf die Verarbeitung einer Reihe von Ereignissen spezialisiert haben. Im Gegensatz zu herkömmlichen neuronalen Netzen sind RNNs so konzipiert, dass sie mit Sequenzen unter Verwendung interner Salden arbeiten. Zu diesem Zweck enthält der Berechnungsgraph RNN Zyklen, die den Einfluss vorheriger Informationen aus der Abfolge von Ereignissen auf die aktuelle widerspiegeln. LSTM-Neuronale Netze (Long Short-Term Memory) wurden 1997 als Erweiterung von RNN eingeführt (
Hochreiter und Schmidhuber, 1997 ). LSTM-Wiederholungszellen sind verbunden, um Burst- und Fade-Probleme zu vermeiden. Herkömmliche LSTMs behalten nur frühere Informationen bei, wenn sie die Sequenz in eine Richtung verarbeiten. Bidirektionale LSTMs, die in beide Richtungen arbeiten, kombinieren die Ausgabe von zwei verborgenen LSTM-Schichten, die Informationen in entgegengesetzte Richtungen übertragen - eine im Laufe der Zeit und die andere dagegen - und gleichzeitig Daten aus vergangenen und zukünftigen Zuständen empfangen (
Schuster und Paliwal, 1997 ).
Abbildung 1: Reduzierte Version der Architektur. Das LSTM-Modul verwendet für die erste und dritte Stufe die gleichen Gewichte.Eine vereinfachte Darstellung des beschriebenen Ansatzes ist in Abbildung 1 dargestellt. Die Architektur des neuronalen Netzwerks besteht aus einer Einbettungsschicht und zwei bidirektionalen LTSM-Modulen (dim = 64). Das erste LTSM-Modul analysiert die Wörter des ersten Benutzers (d. H. Die erste und dritte Replik der Konversation), und das zweite Modul analysiert die Wörter des zweiten Benutzers (zweite Replik). In der ersten Stufe werden die Wörter jedes Benutzers, die vorab trainierte Vektordarstellungen verwenden, in das entsprechende bidirektionale LTSM-Modul eingespeist. Dann werden die resultierenden drei Merkmalskarten zu einem flachen Merkmalsvektor kombiniert und dann auf eine vollständig verbundene verborgene Schicht (dim = 30) übertragen, die die Wechselwirkungen zwischen den extrahierten Merkmalen analysiert. Schließlich werden diese Eigenschaften in der Ausgabeschicht unter Verwendung der Softmax-Aktivierungsfunktion verarbeitet, um die endgültige Klassenbezeichnung zu bestimmen. Um die Überanpassung zu verringern, wurden nach Schichten der Vektordarstellung Regularisierungsschichten mit Gaußschem Rauschen hinzugefügt, und jedem LTSM-Modul (p = 0,2) und einer verborgenen vollständig verbundenen Schicht (p = 0,1) wurden Dropout-Schichten hinzugefügt (
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. Ergebnisse
Auf der Suche nach der optimalen Architektur haben wir nicht nur mit der Anzahl der Neuronen in den Schichten, Aktivierungsfunktionen und Regularisierungsparametern experimentiert, sondern auch mit der Architektur des neuronalen Netzwerks selbst. Dies wird in der
Originalarbeit ausführlicher beschrieben.
Die im vorherigen Abschnitt beschriebene Architektur zeigte die besten Ergebnisse beim Training des Train-Datensatzes und der Validierung des Dev-Datensatzes, sodass sie in der letzten Phase des Wettbewerbs verwendet wurde. Beim letzten Testdatensatz zeigte das Modell ein mikro-gemitteltes F1-Maß von 72,59%, und das maximal erzielte Ergebnis unter allen Teilnehmern betrug 79,59%. Trotzdem war unser Ergebnis viel höher als der von den Organisatoren festgelegte Basiswert von 58,68%.
Der Quellcode für die Modell- und Vektordarstellung von Wörtern ist auf GitHub verfügbar.
Die Vollversion des Artikels und die
Arbeit mit der Aufgabenbeschreibung finden Sie auf der ACL Anthology-Website.
Der Trainingsdatensatz kann von der offiziellen LinkedIn-Gruppe heruntergeladen werden.
Zitat:
@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", }