Detección de emociones contextuales en conversaciones textuales usando redes neuronales


Hoy en día, hablar con agentes de conversación se está convirtiendo en una rutina diaria, y es crucial que los sistemas de diálogo generen respuestas lo más parecidas a los humanos posible. Como uno de los aspectos principales, se debe prestar atención primaria a proporcionar respuestas emocionalmente conscientes a los usuarios. En este artículo, vamos a describir la arquitectura de red neuronal recurrente para la detección de emociones en conversaciones textuales , que participó en SemEval-2019 Tarea 3 "EmoContext" , es decir, un taller anual sobre evaluación semántica. El objetivo de la tarea es clasificar la emoción (es decir, feliz, triste, enojado y otros) en un conjunto de datos conversacionales de 3 turnos.

El resto del artículo está organizado de la siguiente manera. La Sección 1 ofrece una breve descripción de la tarea EmoContext y los datos proporcionados. Las secciones 2 y 3 se centran en el procesamiento previo de textos y la inserción de palabras, en consecuencia. En la sección 4, describimos la arquitectura del modelo LSTM utilizado en nuestra presentación. En conclusión, se presentan el rendimiento final de nuestro sistema y el código fuente. El modelo se implementa en Python usando la biblioteca Keras.

1. Datos de entrenamiento


La SemEval-2019 Tarea 3 "EmoContext" se centra en la detección de emociones contextuales en una conversación textual. En EmoContext, dado un enunciado textual del usuario junto con 2 turnos de contexto en una conversación, debemos clasificar si la emoción del próximo enunciado del usuario es "feliz", "triste", "enojado" u "otros" (Tabla 1). Solo hay dos participantes en la conversación: una persona anónima (Tuen-1 y Turn-3) y el chatbot basado en IA Ruuh (Turn-2). Para una descripción detallada, ver ( Chatterjee et al., 2019 ).

Tabla 1. Ejemplos que muestran el conjunto de datos EmoContext ( Chatterjee et al., 2019 )
Usuario (Turn-1)Agente conversacional (Turn-1)Usuario (Turno-2)Clase verdadera
Acabo de calificar para la pasantía NabardWOOT! Esa es una gran noticia. FelicidadesMe puse a llorarFeliz
¿Cómo te atreves a abofetear a mi hijo?Si arruinas mi auto, yo también te haré esoSolo trata de hacer eso una vezEnojado
Fui lastimado por ti másNo lo dijiste en serio.di que me amasTriste
Lo haré de noche.De acuerdo Mantenme al tanto.No dar WhatsApp no.Otros

Durante la competencia, tuvimos acceso a 30160 textos con etiqueta humana proporcionados por los organizadores de la tarea, donde alrededor de 5000 muestras de cada clase de "enojado", "triste", "feliz" y 15000 para la clase "otros" (Tabla 2). Los conjuntos de desarrollo y prueba, que también fueron proporcionados por los organizadores, en contraste con un conjunto de trenes, tienen una distribución de la vida real, que es aproximadamente del 4% para cada clase emocional y el resto para la clase de "otros". Datos proporcionados por Microsoft y se pueden encontrar en el grupo oficial de LinkedIn .

Tabla 2. Distribución de etiquetas de clase de emoción en conjuntos de datos ( Chatterjee et al., 2019 ).
Conjunto de datosFelizTristeEnojadoOtrosTotal
Tren
14,07%
18,11%
18,26%
49,56%
30160
Dev
5,15%
4,54%
5,45%
84,86%
2755
Prueba
5,16%
4,54%
5,41%
84,90%
5509
Distante
33,33%
33,33%
33,33%
0%
900k

Además de estos datos, recolectamos 900k tweets en inglés para crear un conjunto de datos distante de 300k tweets para cada emoción. Para formar el conjunto de datos distante, nos basamos en la estrategia de Go et al. (2009), en virtud del cual simplemente asociamos tweets con la presencia de palabras relacionadas con la emoción, como '#angry', '#annoyed', '#happy', '#sad,' #surprised ', etc. La lista de términos de consulta se basó en los términos de consulta de SemEval-2018 AIT DISC ( Duppada et al., 2018 ).

La métrica clave de rendimiento de EmoContext es un puntaje F1 promedio de tres clases de emoción, es decir, "triste", "feliz" y "enojado".

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. Procesamiento previo de textos


Antes de cualquier etapa de entrenamiento, los textos fueron preprocesados ​​por la herramienta de texto Ekphrasis (Baziotis et al., 2017). Esta herramienta ayuda a realizar la corrección de ortografía, la normalización de palabras, la segmentación y permite especificar qué tokens se deben omitir, normalizar o anotar con etiquetas especiales. Utilizamos las siguientes técnicas para la etapa de preprocesamiento.

  • Las URL, los correos electrónicos, la fecha y la hora, los nombres de usuario, el porcentaje, las monedas y los números se reemplazaron con las etiquetas correspondientes.
  • Los términos repetidos, censurados, alargados y en mayúscula se anotaron con las etiquetas correspondientes.
  • Las palabras alargadas se corrigieron automáticamente según el corpus de estadísticas de palabras incorporado.
  • El desempaquetado de hashtags y contracciones (es decir, segmentación de palabras) se realizó en base al corpus de estadísticas de palabras incorporado.
  • Se utilizó un diccionario creado manualmente para reemplazar los términos extraídos del texto con el fin de reducir una variedad de emociones.

Además, Emphasis proporciona el tokenizador que es capaz de identificar la mayoría de los emojis, emoticones y expresiones complicadas como palabras censuradas, enfatizadas y alargadas, así como fechas, horas, monedas y acrónimos.

Tabla 3. Ejemplos de preprocesamiento de texto.
Texto originalTexto preprocesado
TE SIENTO ... Me estoy rompiendo en millones de piezas <allcaps> te siento </allcaps>. <repetido> me estoy rompiendo en millones de piezas
cansado y yo también te extrañé :‑(cansado y yo también te extrañé <sad>
debe escuchar esto: www.youtube.com/watch?v=99myH1orbs4deberías escuchar <alargado> a esto: <url>
Mi departamento se encarga de eso. Mi renta es de alrededor de $ 650.mi departamento se encarga de eso. mi renta es alrededor de <money>.

 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. Incrustaciones de palabras


Las incorporaciones de palabras se han convertido en una parte esencial de cualquier enfoque de aprendizaje profundo para los sistemas de PNL. Para determinar los vectores más adecuados para la tarea de detección de emociones, probamos los modelos Word2Vec ( Mikolov et al., 2013 ), GloVe ( Pennington et al., 2014 ) y FastText ( Joulin et al., 2017 ), así como modelos preformados de DataStories. vectores de palabras ( Baziotis et al., 2017 ). El concepto clave de Word2Vec es localizar palabras, que comparten contextos comunes en el corpus de entrenamiento, muy cerca en el espacio vectorial. Los modelos Word2Vec y Glove aprenden codificaciones geométricas de palabras a partir de su información de coincidencia, pero esencialmente el primero es un modelo predictivo, y el segundo es un modelo basado en el conteo. En otras palabras, mientras Word2Vec intenta predecir una palabra objetivo (arquitectura CBOW) o un contexto (arquitectura Skip-gram), es decir, para minimizar la función de pérdida, GloVe calcula los vectores de palabras haciendo una reducción de dimensionalidad en la matriz de recuento de coincidencias. FastText es muy similar a Word2Vec, excepto por el hecho de que usa n-gramas de caracteres para aprender vectores de palabras, por lo que puede resolver el problema de falta de vocabulario.

Para todas las técnicas mencionadas anteriormente, utilizamos los cochecitos de entrenamiento predeterminados proporcionados por los autores. Capacitamos un modelo LSTM simple (dim = 64) basado en cada una de estas incorporaciones y comparamos la efectividad mediante validación cruzada. Según el resultado, las incrustaciones preformadas de DataStories demostraron la mejor puntuación media de F1.

Para enriquecer las incrustaciones de palabras seleccionadas con la polaridad emocional de las palabras, consideramos realizar frases distantes previas al entrenamiento mediante un ajuste fino de las incrustaciones en el conjunto de datos distantes etiquetado automáticamente. La importancia de utilizar el pre-entrenamiento se demostró en ( Deriu et al., 201 7). Usamos el conjunto de datos distantes para entrenar a la simple red LSTM para clasificar tweets enojados, tristes y felices. La capa de incrustaciones se congeló durante la primera época de entrenamiento para evitar cambios significativos en los pesos de las incrustaciones, y luego se descongeló durante las siguientes 5 épocas. Después de la etapa de capacitación, las incrustaciones afinadas se guardaron para las fases de capacitación adicionales y se pusieron a disposición del público .

 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. Arquitectura de red neuronal


Una red neuronal recurrente (RNN) es una familia de redes neuronales artificiales que se especializa en el procesamiento de datos secuenciales. A diferencia de las redes neuronales tradicionales, los RRN están diseñados para manejar datos secuenciales compartiendo sus pesos internos procesando la secuencia. Para este propósito, el gráfico de cálculo de RRNs incluye ciclos, que representan la influencia de la información previa sobre la presente. Como una extensión de los RNN, las redes de memoria a corto plazo (LSTM) se han introducido en 1997 ( Hochreiter y Schmidhuber, 1997 ). En los LSTM, las celdas recurrentes están conectadas de una manera particular para evitar desaparecer y explotar los problemas de gradiente. Los LSTM tradicionales solo conservan información del pasado, ya que procesan la secuencia solo en una dirección. Los LSTM bidireccionales combinan la salida de dos capas LSTM ocultas que se mueven en direcciones opuestas, donde una avanza a través del tiempo y otra retrocede a través del tiempo, lo que permite capturar información de estados pasados ​​y futuros simultáneamente ( Schuster y Paliwal, 1997 ).


Figura 1: La arquitectura de una versión más pequeña de la arquitectura propuesta. La unidad LSTM para el primer turno y para el tercer turno tienen pesos compartidos.

En la Figura 1 se proporciona una visión general de alto nivel de nuestro enfoque. La arquitectura propuesta de la red neuronal consiste en la unidad de inclusión y dos unidades LSTM bidireccionales (dim = 64). La primera unidad LSTM está destinada a analizar el enunciado del primer usuario (es decir, el primer turno y el tercer turno de la conversación), y el segundo está destinado a analizar el enunciado del segundo usuario (es decir, el segundo turno). Estas dos unidades aprenden no solo la representación de características semánticas y de sentimiento, sino también cómo capturar características de conversación específicas del usuario, lo que permite clasificar las emociones con mayor precisión. En el primer paso, cada enunciado del usuario se introduce en una unidad LSTM bidireccional correspondiente utilizando incrustaciones de palabras previamente capacitadas. A continuación, estos tres mapas de características se concatenan en un vector de características de aplanamiento y luego se pasan a una capa oculta completamente conectada (dim = 30), que analiza las interacciones entre los vectores obtenidos. Finalmente, estas características avanzan a través de la capa de salida con la función de activación softmax para predecir una etiqueta de clase final. Para reducir el sobreajuste, se agregaron capas de regularización con ruido gaussiano después de la capa de inclusión, se agregaron capas de abandono ( Srivastava et al., 2014 ) en cada unidad LSTM (p = 0.2) y antes de la capa oculta completamente conectada (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. Resultados


En el proceso de búsqueda de una arquitectura óptima, experimentamos no solo con el número de células en capas, funciones de activación y parámetros de regularización, sino también con la arquitectura de la red neuronal. La información detallada sobre esta frase se puede encontrar en el documento original .

El modelo descrito en la sección anterior demostró las mejores puntuaciones en el conjunto de datos de desarrollo, por lo que se utilizó en la etapa de evaluación final de la competencia. En el conjunto de datos de la prueba final, logró un puntaje F1 promedio de 72.59% para las clases emocionales, mientras que el puntaje máximo entre todos los participantes fue del 79.59%. Sin embargo, esto está muy por encima de la línea de base oficial publicada por los organizadores de tareas, que fue del 58,68%.

El código fuente del modelo y las incorporaciones de palabras están disponibles en GitHub.
La versión completa del artículo y el documento de descripción de la tarea se pueden encontrar en ACL Anthology.
El conjunto de datos de entrenamiento se encuentra en el grupo oficial de competencia en LinkedIn.

Cita:

 @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/439850/


All Articles