Atualmente, conversar com agentes de conversação está se tornando uma rotina diária, e é crucial que os sistemas de diálogo gerem respostas o mais humanas possível. Como um dos aspectos principais, atenção primária deve ser dada ao fornecimento de respostas emocionalmente conscientes aos usuários. Neste artigo, descreveremos
a arquitetura de rede neural recorrente para detecção de emoções em conversas textuais , que participaram da
tarefa 3 SemEval-2019 "EmoContext" , ou seja, um workshop anual sobre avaliação semântica. O objetivo da tarefa é classificar a emoção (ou seja, feliz, triste, zangada e outras) em um conjunto de dados de conversação em três turnos.
O restante do artigo está organizado da seguinte maneira. A Seção 1 fornece uma breve visão geral da tarefa do EmoContext e dos dados fornecidos. As seções 2 e 3 enfocam o pré-processamento de textos e a incorporação de palavras, consequentemente. Na seção 4, descrevemos a arquitetura do modelo LSTM usado em nossa submissão. Em conclusão, são apresentados o desempenho final do nosso sistema e o código fonte. O modelo é implementado em Python usando a biblioteca Keras.
1. Dados de treinamento
A tarefa 3 do SemEval-2019 “EmoContext” está focada na detecção de emoções contextuais na conversa em texto. No EmoContext, dada uma declaração textual do usuário juntamente com 2 turnos de contexto em uma conversa, devemos classificar se a emoção da próxima frase do usuário é "feliz", "triste", "zangada" ou "outras" (Tabela 1). Existem apenas dois participantes da conversa: uma pessoa anônima (Tuen-1 e Turn-3) e o chatbot baseado em IA
Ruuh (Turn-2). Para uma descrição detalhada, consulte (
Chatterjee et al., 2019 ).
Tabela 1. Exemplos mostrando o conjunto de dados EmoContext ( Chatterjee et al., 2019 )Durante a competição, tivemos acesso a 30160 textos rotulados como humanos, fornecidos pelos organizadores das tarefas, onde cerca de 5000 amostras cada uma das classes “zangado”, “triste”, “feliz” e 15000 para a classe “outros” (Tabela 2). Os conjuntos de desenvolvimento e teste, que também foram fornecidos pelos organizadores, em contraste com o conjunto de trens, têm uma distribuição na vida real, que é de cerca de 4% para cada classe emocional e o restante para a classe "outros". Os dados são da Microsoft e podem ser encontrados no
grupo oficial do LinkedIn .
Tabela 2. Distribuição do rótulo da classe Emotion em conjuntos de dados ( Chatterjee et al., 2019 ).Além desses dados, coletamos 900k tweets em inglês para criar um conjunto de dados distante de 300k tweets para cada emoção. Para formar o conjunto de dados distante, baseamos-nos na estratégia de Go et al. (2009), sob o qual simplesmente associamos tweets à presença de palavras relacionadas à emoção, como '#angry', '#annoyed', '#happy', '#sad,' #surprised 'etc. A lista de termos de consulta foi baseada nos termos de consulta do SemEval-2018 AIT DISC (
Duppada et al., 2018 ).
A principal métrica de desempenho do EmoContext é uma pontuação micro-média da F1 para três classes de emoções, ou seja, 'triste', 'feliz' e 'zangada'.
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é-processamento de textos
Antes de qualquer etapa do treinamento, os textos eram pré-processados pela ferramenta Ekphrasis (Baziotis et al., 2017). Essa ferramenta ajuda a executar correção ortográfica, normalização de palavras, segmentação e permite especificar quais tokens devem ser omitidos, normalizados ou anotados com tags especiais. Usamos as seguintes técnicas para o estágio de pré-processamento.
- URLs, e-mails, data e hora, nomes de usuários, porcentagem, moedas e números foram substituídos pelas tags correspondentes.
- Termos repetidos, censurados, alongados e em maiúsculas foram anotados com as tags correspondentes.
- Palavras alongadas foram corrigidas automaticamente com base no corpus estatístico de palavras incorporado.
- A descompactação de hashtags e contrações (ou seja, segmentação de palavras) foi realizada com base no corpus estatístico de palavras incorporado.
- Um dicionário criado manualmente para substituir termos extraídos do texto foi usado para reduzir uma variedade de emoções.
Além disso, o Emphasis fornece o tokenizador capaz de identificar a maioria dos emojis, emoticons e expressões complicadas, como palavras censuradas, enfatizadas e alongadas, além de datas, horas, moedas e acrônimos.
Tabela 3. Exemplos de pré-processamento de texto. 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. Incorporação de palavras
A incorporação de palavras se tornou uma parte essencial de qualquer abordagem de aprendizado profundo para sistemas de PNL. Para determinar os vetores mais adequados para a tarefa de detecção de emoções, experimentamos os modelos Word2Vec (
Mikolov et al., 2013 ), GloVe (
Pennington et al., 2014 ) e FastText (
Joulin et al., 2017 ), bem como os DataStories pré-treinados vetores de palavras (
Baziotis et al., 2017 ). O conceito principal do Word2Vec é localizar palavras, que compartilham contextos comuns no corpus de treinamento, nas proximidades do espaço vetorial. Os modelos Word2Vec e Glove aprendem codificações geométricas de palavras a partir de suas informações de co-ocorrência, mas essencialmente o primeiro é um modelo preditivo e o último é um modelo baseado em contagem. Em outras palavras, enquanto o Word2Vec tenta prever uma palavra de destino (arquitetura CBOW) ou um contexto (arquitetura de Skip-gram), ou seja, para minimizar a função de perda, o GloVe calcula vetores de palavras fazendo redução de dimensionalidade na matriz de contagens de co-ocorrência. O FastText é muito semelhante ao Word2Vec, exceto pelo fato de que ele usa caracteres n-gramas para aprender vetores de palavras; portanto, ele é capaz de resolver o problema de falta de vocabulário.
Para todas as técnicas mencionadas acima, usamos os carrinhos de treinamento padrão fornecidos pelos autores. Nós treinamos um modelo LSTM simples (dim = 64) com base em cada um desses embeddings e comparamos a eficácia usando a validação cruzada. De acordo com o resultado, os casamentos pré-treinados do DataStories demonstraram a melhor pontuação média na F1.
Para enriquecer as combinações de palavras selecionadas com a polaridade emocional das palavras, consideramos a execução de uma frase de pré-treinamento distante, ajustando as combinações no conjunto de dados distantes rotulado automaticamente. A importância do uso do pré-treinamento foi demonstrada em (
Deriu et al., 201 7). Usamos o conjunto de dados distante para treinar a rede LSTM simples para classificar tweets irritados, tristes e felizes. A camada de encaixes foi congelada durante a primeira época de treinamento, a fim de evitar alterações significativas nos pesos dos encaixes e, em seguida, foi descongelada nas próximas cinco épocas. Após a fase de treinamento, os ajustes foram salvos para as fases de treinamento adicionais e
disponibilizados ao 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. Arquitetura de Rede Neural
Uma rede neural recorrente (RNN) é uma família de redes neurais artificiais especializada no processamento de dados seqüenciais. Ao contrário das redes neurais tradicionais, os RRNs são projetados para lidar com dados seqüenciais compartilhando seus pesos internos ao processar a sequência. Para esse fim, o gráfico computacional de RRNs inclui ciclos, representando a influência das informações anteriores sobre a atual. Como uma extensão das RNNs, redes de memória de curto prazo (LSTMs) foram introduzidas em 1997 (
Hochreiter e Schmidhuber, 1997 ). Nos LSTMs, as células recorrentes são conectadas de uma maneira específica para evitar problemas de gradiente que desaparecem e explodem. Os LSTMs tradicionais preservam apenas as informações do passado, pois processam a sequência apenas em uma direção. Os LSTMs bidirecionais combinam a saída de duas camadas ocultas de LSTM, movendo-se em direções opostas, onde uma avança no tempo e outra retrocede no tempo, permitindo capturar informações de estados passados e futuros simultaneamente (
Schuster e Paliwal, 1997 ).
Figura 1: A arquitetura de uma versão menor da arquitetura proposta. A unidade LSTM no primeiro e no terceiro turno têm pesos compartilhados.Uma visão geral de alto nível de nossa abordagem é fornecida na Figura 1. A arquitetura proposta da rede neural consiste na unidade de incorporação e duas unidades LSTM bidirecionais (dim = 64). A primeira unidade LSTM destina-se a analisar o enunciado do primeiro usuário (ou seja, o primeiro turno e o terceiro turno da conversa), e o último se destina a analisar o enunciado do segundo usuário (ou seja, o segundo turno). Essas duas unidades aprendem não apenas a representação de recursos semânticos e de sentimentos, mas também como capturar recursos de conversação específicos do usuário, o que permite classificar emoções com mais precisão. Na primeira etapa, cada enunciado do usuário é alimentado em uma unidade LSTM bidirecional correspondente usando incorporação de palavras pré-treinadas. Em seguida, esses três mapas de características são concatenados em um vetor de característica achatada e depois passados para uma camada oculta totalmente conectada (dim = 30), que analisa as interações entre os vetores obtidos. Finalmente, esses recursos prosseguem na camada de saída com a função de ativação softmax para prever um rótulo de classe final. Para reduzir o sobreajuste, camadas de regularização com ruído gaussiano foram adicionadas após a camada de incorporação, camadas de abandono (
Srivastava et al., 2014 ) foram adicionadas em cada unidade LSTM (p = 0,2) e antes da camada totalmente oculta e 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
No processo de busca da arquitetura ideal, experimentamos não apenas o número de células em camadas, funções de ativação e parâmetros de regularização, mas também a arquitetura da rede neural. As informações detalhadas sobre esta frase podem ser encontradas no
artigo original .
O modelo descrito na seção anterior demonstrou as melhores pontuações no conjunto de dados do desenvolvedor, por isso foi usado na etapa de avaliação final da competição. No conjunto de dados de teste final, obteve 72,59% da pontuação micro-média da F1 para as classes emocionais, enquanto a pontuação máxima entre todos os participantes foi de 79,59%. No entanto, isso está bem acima da linha de base oficial divulgada pelos organizadores de tarefas, que foi de 58,68%.
O código fonte do modelo e a incorporação de palavras estão disponíveis no GitHub.
A versão completa do artigo e
o documento de descrição da tarefa podem ser encontrados em ACL Anthology.
O conjunto de dados de treinamento está localizado no grupo oficial da competição no LinkedIn.
Citação:
@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", }