Автоматическое определение эмоций в текстовых беседах с использованием нейронных сетей

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Одна из основных задач диалоговых систем состоит не только в предоставлении нужной пользователю информации, но и в генерации как можно более человеческих ответов. А распознание эмоций собеседника — уже не просто крутая фича, это жизненная необходимость. В этой статье мы рассмотрим архитектуру рекуррентной нейросети для определения эмоций в текстовых беседах, которая принимала участие в SemEval-2019 Task 3 “EmoContext”, ежегодном соревновании по компьютерной лингвистике. Задача состояла в классификации эмоций (“happy”, “sad”, “angry” и “others”) в беседе из трех реплик, в которой участвовали чат-бот и человек. В первой части статьи мы рассмотрим поставленную в EmoContext задачу и предоставленные организаторами данные. Во второй и третьей частях разберём предварительную обработку текста и способы векторного представления слов. В четвёртой части мы опишем архитектуру LSTM, которую мы использовали в соревновании. Код написан на языке Python с использованием библиотеки Keras.

1. Обучающие данные

Трек “EmoContext” на SemEval-2019 был посвящен определению эмоций в текстовых беседах с учетом контекста переписки. Контекст в данном случае — это несколько последовательных реплик участников диалога. В беседе два участника: анонимный пользователь (ему принадлежит первая и третья реплика) и чат-бот Ruuh (ему принадлежит вторая реплика). На основе трех реплик необходимо определить, какую эмоцию испытывал пользователь при написании ответа чат-боту (Таблица 1). Всего разметка датасета содержала четыре эмоции: «happy», «sad», «angry» или «others» (Таблица 1). Подробное описание представлено здесь: (Chatterjee et al., 2019). Таблица 1. Примеры из датасета EmoContext (Chatterjee et al., 2019)

Пользователь (Этап-1) Диалоговый робот (Этап-1) Пользователь (Этап-2) True Class
I just qualified for the Nabard internship WOOT! That’s great news. Congratulations! I started crying Счастье
How dare you to slap my child If you spoil my car, I will do that to you too Just try to do that once Злость
I was hurt by u more You didn’t mean it. say u love me Грусть
I will do night. Alright. Keep me in loop. Not giving WhatsApp no. Другое
В ходе состязания организаторы предоставили несколько наборов данных. Обучающий датасет (Train) состоял из 30 160 размеченных вручную текстов. В этих текстах было примерно по 5000 объектов, относящихся к классам «happy», «sad» и «angry», а также 15000 текстов из класса «others» (Таблица 2). Также организаторы предоставили наборы данных для разработки (Dev) и тестирования (Test), в которых, в отличие от обучающего датасета, распределение по классам эмоций соответствовало реальной жизни: примерно по 4 % для каждого из классов «happy», «sad» и «angry», а остальное — класс «others». Данные предоставлены Microsoft, скачать их можно в официальной группе в LinkedIn. Таблица 2. Распределение меток классов эмоций в датасете (Chatterjee et al., 2019).
Датасет Счастье Грусть Злость Другое Итого
Учебный
14,07 %
18,11 %
18,26 %
49,56 %
30 160
Для разработки
5,15 %
4,54 %
5,45 %
84,86 %
2755
Тестовый
5,16 %
4,54 %
5,41 %
84,90 %
5509
Дистанцированный
33,33 %
33,33 %
33,33 %
0 %
900 тыс.
В дополнение к этим данным мы собрали 900 тыс. англоязычных сообщений из Twitter, чтобы создать Distant-датасет (300 тыс. твитов на каждую эмоцию). При его создании мы придерживались стратегии Go et al. (2009), в рамках которой просто ассоциировали сообщения с наличием относящихся к эмоциям слов, таких как #angry, #annoyed, #happy, #sad, #surprised и так далее. Список терминов основан на терминах из SemEval-2018 AIT DISC (Duppada et al., 2018). Главной метрикой качества в соревновании EmoContext является усредненная F1-мера для трёх классов эмоций, то есть для классов «happy», «sad» и «angry».
def preprocessData(dataFilePath, mode): 	conversations = [] 	labels = [] 	with io.open(dataFilePath, encoding="utf8") as finput:     	finput.readline()     	for line in finput:         	line = line.strip().split('	')         	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. Предварительная обработка текста

Перед обучением мы предварительно обработали тексты с помощью инструмента Ekphrasis (Baziotis et al., 2017). Он помогает исправить орфографию, нормализовать слова, сегментировать, а также определить, какие токены следует отбросить, нормализовать или аннотировать с помощью специальных тегов. На этапе предварительной обработки мы сделали следующее:

  • Адреса URL и почту, дату и время, ники, проценты, валюты и числа заменили соответствующими тегами.
  • Повторяющиеся, цензурированные, удлинённые написанные прописными буквами термины мы сопроводили соответствующими метками.
  • Удлинённые слова были автоматически скорректированы.

Кроме того, Emphasis содержит токенизатор, который может идентифицировать большинство эмодзи, эмотиконов и сложных выражений, а также даты, время, валюты и акронимы.

Таблица 3. Примеры предварительной обработки текста.
Исходный текст Предварительно обработанный текст
I FEEL YOU… I'm breaking into million pieces <allcaps> i feel you </allcaps>. <repeated> i am breaking into million pieces
tired and I missed you too :?( tired and i missed you too <sad>
you should liiiiiiisten to this: www.youtube.com/watch?v=99myH1orbs4 you should listen <elongated> to this: <url>
My apartment takes care of it. My rent is around $650. my apartment takes care of it. my rent is around <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. Векторное представление слов

Векторное представление стало неотъемлемой частью большинства подходов к созданию NLP-систем с применением глубокого обучения. Чтобы определить наиболее подходящие модели векторного отображения, мы попробовали Word2Vec (Mikolov et al., 2013), GloVe (Pennington et al., 2014) и FastText (Joulin et al., 2017), а также предварительно обученные векторы DataStories (Baziotis et al., 2017). Word2Vec находит взаимосвязи между словами согласно предположению, что в похожих контекстах встречаются семантически близкие слова. Word2Vec пытается прогнозировать целевое слово (архитектура CBOW) или контекст (архитектура Skip-Gram), то есть минимизировать функцию потерь, а GloVe рассчитывает вектора слов, уменьшая размерность матрицы смежности. Логика работы FastText похожа на логику Word2Vec, за исключением того, что для построения векторов слов она использует символьные n-граммы, и как следствие, может решать проблему неизвестных слов. Для всех упомянутых моделей мы используем параметры обучения по умолчанию, предоставленные авторами. Мы обучили простую LSTM-модель (dim=64) на основе каждого из этих векторных представлений и сравнили эффективность классификации с помощью кросс-валидации. Наилучший результат в F1-меры показали предварительно обученные вектора DataStories. Для обогащения выбранного векторного отображения эмоциональной окраской слов мы решили произвести тонкую настройку векторов с помощью автоматически размеченного Distant-датасета (Deriu et al., 2017). Мы использовали Distant-датасет для обучения простой LSTM-сети, чтобы классифицировать «злые», «грустные» и «счастливые» сообщения. Эмбеддинг слой был заморожен в течение первой итерации обучения, чтобы избежать сильных изменений у весов векторов, а для последующих пяти итераций слой был разморожен. После обучения «оттюненные» векторы были сохранены для последующего использования в нейронной сети, а также выложены в общий доступ.

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. Архитектура нейросети

Рекуррентные нейросети (RNN) — это семейство нейросетей, специализирующихся на обработке серии событий. В отличие от традиционных нейросетей, RRN предназначены для работы с последовательностями путем использования внутренних весов. Для этого вычислительный граф RNN содержит циклы, отражающие влияние предыдущей информации из последовательности событий на текущую. LSTM-нейросети (Long Short-Term Memory) были представлены в качестве расширения RNN в 1997-м (Hochreiter and Schmidhuber, 1997). Рекуррентные ячейки LSTM соединены так, чтобы избегать проблем с взрывом и затуханием градиентов. Традиционные LSTM лишь сохраняют прошлую информацию, поскольку обрабатывают последовательность в одном направлении. Двунаправленные LSTM, работающие в обоих направлениях, комбинируют выходные данные двух скрытых LSTM-слоёв, передающих информацию в противоположных направлениях — один по ходу времени, другой против, — тем самым одновременно получая данные из прошлого и будущего состояний (Schuster and Paliwal, 1997).

Рисунок 1: Уменьшенная версия архитектуры. LSTM-модуль использует одни и те же веса для первого и третьего этапов.

Упрощённое представление описанного подхода представлено на рисунке 1. Архитектура нейросети состоит из эмбеддинг-слоя и двух двунаправленных LTSM-модулей (dim = 64). Первый LTSM-модуль анализирует слова первого пользователя (то есть первую и третью реплику беседы), а второй модуль анализирует слова второго пользователя (вторую реплику). На первом этапе слова каждого пользователя с помощью заранее обученных векторных представлений подаются в соответствующий двунаправленный LTSM-модуль. Затем получившиеся три карты признаков объединяются в плоский вектор признаков, а затем передаются в полносвязный скрытый слой (dim=30), который анализирует взаимодействия между извлечёнными признаками. Наконец, эти признаки обрабатываются в выходном слое с помощью функции softmax-активации, чтобы определить финальную метку класса. Для уменьшения переобучения после слоёв векторного представления были добавлены слои регуляризации с гауссовским шумом, а также в каждый LTSM-модуль (p = 0.2) и перед скрытым полностью связным слоем (p = 0.1) были добавлены dropout-слои (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. Результаты

В ходе поиска оптимальной архитектуры мы экспериментировали не только с количеством нейронов в слоях, функциями активации и параметрами регуляризации, но и с самой архитектурой нейросети. Подробнее об этом говорится в исходной работе. Описанная в предыдущем разделе архитектура продемонстрировала наилучшие результаты при обучении на датасете Train и валидации на датасете Dev, поэтому она использовалась на финальной стадии состязания. На последнем тестовом датасете модель показала микро-усреджненную F1-меру 72,59 %, а максимально достигнутый результат среди всех участников составил 79,59 %. Тем не менее, наш результат оказался гораздо выше базового значения в 58,68 %, заданного организаторами. Исходный код модели и векторного представления слов доступен на GitHub. Полная версия статьи и работа с описанием задачи лежат на сайте ACL Anthology. Учебный датасет можно скачать в официальной группе на LinkedIn. Цитирование:

@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", }

Источник: habr.com

Комментарии: