Secara otomatis mendeteksi emosi dalam percakapan teks menggunakan jaringan saraf


Salah satu tugas utama sistem dialog adalah tidak hanya menyediakan informasi yang dibutuhkan pengguna, tetapi juga menghasilkan sebanyak mungkin jawaban manusia. Dan pengenalan emosi lawan bicaranya bukan lagi fitur keren, itu adalah kebutuhan vital. Dalam artikel ini, kita akan melihat arsitektur jaringan saraf berulang untuk menentukan emosi dalam percakapan teks , yang mengambil bagian dalam SemEval-2019 Tugas 3 "EmoContext" , kompetisi tahunan dalam linguistik komputer. Tugasnya adalah untuk mengklasifikasikan emosi ("bahagia", "sedih", "marah" dan "lain-lain") dalam percakapan tiga komentar, di mana sebuah obrolan bot dan seseorang berpartisipasi.

Pada bagian pertama artikel kami akan mempertimbangkan tugas yang diatur dalam EmoContext dan data yang disediakan oleh penyelenggara. Di bagian kedua dan ketiga, kami menganalisis pemrosesan awal teks dan cara-cara representasi vektor kata-kata. Pada bagian keempat, kami menggambarkan arsitektur LSTM yang kami gunakan dalam kompetisi. Kode ditulis dalam Python menggunakan perpustakaan Keras.

1. Data pelatihan


Lagu "EmoContext" di SemEval-2019 didedikasikan untuk definisi emosi dalam percakapan teks, dengan mempertimbangkan konteks korespondensi. Konteks dalam kasus ini adalah beberapa komentar berturut-turut dari peserta dialog. Ada dua peserta dalam percakapan: pengguna anonim (ia memiliki replika pertama dan ketiga) dan bot obrolan Ruuh (ia memiliki replika kedua). Berdasarkan tiga replika, perlu untuk menentukan emosi apa yang dialami pengguna saat menulis jawaban ke chatbot (Tabel 1). Secara total, markup dataset berisi empat emosi: "bahagia", "sedih", "marah" atau "lain-lain" (Tabel 1). Penjelasan terperinci disajikan di sini: ( Chatterjee et al., 2019 ).

Tabel 1. Contoh dari dataset EmoContext ( Chatterjee et al., 2019 )
Pengguna (Tahap-1)Robot Interaktif (Tahap-1)Pengguna (Tahap 2)Kelas yang benar
Saya hanya memenuhi syarat untuk magang NabardWOOT! Itu berita bagus. Selamat!Saya mulai menangisKebahagiaan
Beraninya kau menampar anakkuJika Anda merusak mobil saya, saya akan melakukannya untuk Anda jugaCoba lakukan itu sekali sajaAmarah
Aku lebih disakiti olehmuAnda tidak bersungguh-sungguh.katakan aku cinta akuKesedihan
Saya akan melakukan malam.Baiklah Buat saya tetap di dalam lingkaran.Tidak memberi WhatsApp no.Lainnya

Selama kompetisi, panitia menyediakan beberapa set data. Dataset pelatihan (Kereta) terdiri dari 30.160 teks yang ditandai secara manual. Dalam teks-teks ini ada sekitar 5000 objek milik kelas "bahagia", "sedih" dan "marah", serta 15000 teks dari kelas "lain-lain" (Tabel 2).

Panitia juga menyediakan set data untuk pengembangan (Dev) dan pengujian (Tes), di mana, tidak seperti dataset pelatihan, distribusi berdasarkan kelas emosi sesuai dengan kehidupan nyata: sekitar 4% untuk masing-masing kelas "bahagia", "sedih" dan " marah ", dan sisanya adalah kelas" orang lain ". Data yang disediakan oleh Microsoft, Anda dapat mengunduhnya di grup resmi di LinkedIn .

Tabel 2. Distribusi label kelas emosi dalam dataset ( Chatterjee et al., 2019 ).
DatacetKebahagiaanKesedihanAmarahLainnyaTotal
Pelatihan
14,07%
18.11%
18,26%
49,56%
30 160
Untuk berkembang
5,15%
4,54%
5,45%
84,86%
2755
Tes
5,16%
4,54%
5,41%
84,90%
5509
Jauh
33,33%
33,33%
33,33%
0%
900 ribu

Selain data ini, kami mengumpulkan 900 ribu pesan berbahasa Inggris dari Twitter untuk membuat dataset Jauh (300 ribu tweet untuk setiap emosi). Saat membuatnya, kami mengikuti strategi Go et al. (2009), dalam kerangka yang pesannya hanya dikaitkan dengan kehadiran kata-kata yang berhubungan dengan emosi, seperti #angry, #annoyed, #happy, #sad, #surprised, dan sebagainya. Daftar istilah didasarkan pada ketentuan dari SemEval-2018 AIT DISC ( Duppada et al., 2018 ).

Metrik kualitas utama dalam kompetisi EmoContext adalah ukuran rata-rata F1 untuk tiga kelas emosi, yaitu, untuk kelas "senang", "sedih" dan "marah".

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. Pra-pemrosesan teks


Sebelum pelatihan, kami memproses teks menggunakan alat Ekphrasis (Baziotis et al., 2017). Ini membantu untuk memperbaiki ejaan, menormalkan kata-kata, segmen, dan juga menentukan token mana yang harus dibuang, dinormalisasi atau dijelaskan menggunakan tag khusus. Pada tahap pra-pemrosesan, kami melakukan hal berikut:

  • URL dan surat, tanggal dan waktu, nama panggilan, persentase, mata uang dan angka diganti dengan tag yang sesuai.
  • Istilah huruf besar berulang, disensor, memanjang kami disertai dengan label yang sesuai.
  • Kata-kata memanjang telah diperbaiki secara otomatis.

Selain itu, Penekanan mengandung tokenizer yang dapat mengidentifikasi sebagian besar emoji, emotikon dan ekspresi kompleks, serta tanggal, waktu, mata uang, dan akronim.

Tabel 3. Contoh preprocessing teks.
Sumber teksTeks pra-diproses
AKU MERASA ANDA ... Saya membobol jutaan keping <allcaps> saya merasa Anda </allcaps>. [Diulang] saya membobol jutaan keping
lelah dan aku juga merindukanmu :โ€“(lelah dan aku juga merindukanmu <sad>
Anda harus liiiiii mendengarkan ini: www.youtube.com/watch?v=99myH1orbs4Anda harus mendengarkan <elongated> untuk ini: <url>
Apartemen saya yang mengurusnya. Sewa saya sekitar $ 650.apartemen saya mengurusnya. sewa saya sekitar <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. Representasi vektor kata-kata


Representasi vektor telah menjadi bagian integral dari sebagian besar pendekatan untuk penciptaan sistem NLP menggunakan pembelajaran yang mendalam. Untuk menentukan model pemetaan vektor yang paling cocok, kami mencoba Word2Vec ( Mikolov et al., 2013 ), GloVe ( Pennington et al., 2014 ) dan FastText ( Joulin et al., 2017 ), serta vektor DataStories yang telah dilatih sebelumnya ( Baziotis et al. ., 2017 ). Word2Vec menemukan hubungan antara kata-kata dengan mengasumsikan bahwa kata-kata yang berhubungan secara semantik ditemukan dalam konteks yang sama. Word2Vec mencoba untuk memprediksi kata target (arsitektur CBOW) atau konteks (arsitektur Skip-Gram), yaitu, meminimalkan fungsi kerugian, dan GloVe menghitung vektor kata, mengurangi dimensi dari matriks adjacency. Logika FastText mirip dengan logika Word2Vec, kecuali bahwa ia menggunakan n-gram simbolis untuk membangun vektor kata, dan sebagai hasilnya, ia dapat memecahkan masalah kata-kata yang tidak dikenal.

Untuk semua model yang disebutkan, kami menggunakan parameter pelatihan default yang disediakan oleh penulis. Kami melatih model LSTM sederhana (dim = 64) berdasarkan masing-masing representasi vektor ini dan membandingkan efisiensi klasifikasi menggunakan cross-validation. Hasil terbaik dalam tindakan F1 ditunjukkan oleh vektor DataStories yang telah dilatih sebelumnya.

Untuk memperkaya pemetaan vektor yang dipilih dengan pewarnaan emosional kata-kata, kami memutuskan untuk menyempurnakan vektor menggunakan dataset Jauh yang berlabel secara otomatis ( Deriu et al., 2017 ). Kami menggunakan dataset Jauh untuk melatih jaringan LSTM sederhana untuk mengklasifikasikan pesan "jahat", "sedih" dan "bahagia". Lapisan embedding dibekukan selama iterasi pertama pelatihan untuk menghindari perubahan kuat dalam bobot vektor, dan untuk lima iterasi berikutnya lapisan dicairkan. Setelah pelatihan, vektor "tertunda" disimpan untuk digunakan nanti dalam jaringan saraf, serta dibagikan .

 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. Arsitektur jaringan saraf


Recurrent Neural Networks (RNNs) adalah keluarga jaringan saraf yang berspesialisasi dalam memproses serangkaian acara. Tidak seperti jaringan saraf tradisional, RNN dirancang untuk bekerja dengan urutan menggunakan keseimbangan internal. Untuk ini, grafik komputasi RNN berisi siklus yang mencerminkan pengaruh informasi sebelumnya dari urutan peristiwa pada saat ini. Jaringan saraf LSTM (Memori Jangka Pendek Panjang) diperkenalkan sebagai perpanjangan RNN pada tahun 1997 ( Hochreiter dan Schmidhuber, 1997 ). Sel rekurensi LSTM terhubung untuk menghindari masalah meledak dan pudar. LSTM tradisional hanya menyimpan informasi masa lalu ketika memproses urutan dalam satu arah. LSTM dua arah yang beroperasi di kedua arah menggabungkan output dari dua lapisan LSTM tersembunyi yang mentransmisikan informasi dalam arah yang berlawanan - satu dalam perjalanan waktu dan yang lainnya terhadap - dengan demikian secara bersamaan menerima data dari kondisi masa lalu dan masa depan ( Schuster dan Paliwal, 1997 ).


Gambar 1: Versi arsitektur yang dikurangi. Modul LSTM menggunakan bobot yang sama untuk tahap pertama dan ketiga.

Representasi yang disederhanakan dari pendekatan yang dijelaskan disajikan pada Gambar 1. Arsitektur jaringan saraf terdiri dari lapisan embedding dan dua modul LTSM dua arah (redup = 64). Modul LTSM pertama menganalisis kata-kata pengguna pertama (mis., Replika pertama dan ketiga dari percakapan), dan modul kedua menganalisis kata-kata pengguna kedua (replika kedua). Pada tahap pertama, kata-kata setiap pengguna yang menggunakan representasi vektor pra-dilatih dimasukkan ke dalam modul LTSM dua arah yang sesuai. Kemudian tiga peta fitur yang dihasilkan digabungkan menjadi vektor fitur datar, dan kemudian ditransfer ke lapisan tersembunyi yang sepenuhnya terhubung (dim = 30), yang menganalisis interaksi antara fitur yang diekstraksi. Akhirnya, karakteristik ini diproses dalam lapisan keluaran menggunakan fungsi aktivasi softmax untuk menentukan label kelas akhir. Untuk mengurangi overfitting, setelah lapisan representasi vektor, lapisan regularisasi dengan noise Gaussian ditambahkan, dan lapisan dropout ditambahkan ke setiap modul LTSM (p = 0,2) dan lapisan yang terhubung sepenuhnya tersembunyi (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. Hasil


Dalam mencari arsitektur optimal, kami bereksperimen tidak hanya dengan jumlah neuron di lapisan, fungsi aktivasi dan parameter regularisasi, tetapi juga dengan arsitektur jaringan saraf itu sendiri. Ini dijelaskan lebih detail dalam karya aslinya .

Arsitektur yang dijelaskan pada bagian sebelumnya menunjukkan hasil terbaik saat pelatihan tentang dataset Kereta Api dan validasi pada dataset Dev, sehingga digunakan pada tahap akhir kompetisi. Pada dataset tes terakhir, model menunjukkan ukuran F1 mikro rata-rata 72,59%, dan hasil maksimum yang dicapai di antara semua peserta adalah 79,59%. Namun demikian, hasil kami jauh lebih tinggi dari nilai dasar 58,68% yang ditetapkan oleh penyelenggara.

Kode sumber untuk model dan representasi kata-kata vektor tersedia di GitHub.
Versi lengkap artikel dan bekerja dengan deskripsi tugas ada di situs web ACL Anthology.
Dataset pelatihan dapat diunduh dari grup LinkedIn resmi.

Mengutip:

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


All Articles