Ide
Ide dasar penelitian di bidang speech emotion recognition (pengenalan emosi dari sinyal wicara) adalah bahwa sinyal wicara mengandung informasi emosi dari pembicara. Contoh sederhana, suara orang marah akan sangat berbeda dengan suara orang sedih dan senang. Dengan mengenali (pola) suara orang marah, senang, sedih, dll, kita akan bisa mengenali emosi seseorang dari suaranya. Berikut ilustrasi pengenalan emosi dari penelepon.Pengenalan emosi penelepon secara otomatis oleh Artificial Intelligence (AI) Sumber:https://medium.com/@alitech_2017/voice-based-emotion-recognition-framework-for-films-and-tv-programs-2a6abbb77242 |
Diagram Alir Sistem
Secara umum, sistem pengenalan emosi dari sinyal wicara terdiri dari dua blok utama (selain input dan output):- Ekstraksi Fitur
- Klasifikasi
Diagram blok pengenalan emosi dari sinyal wicara |
Dataset
Dataset yang kita gunakan adalah "The Ryerson Audio-Visual Database of Emotional Speech and Song (RAVDESS)". Dataset ini sebenarnya tidak khusus untuk pengenalan emosi dari sinyal wicara, namun juga berisi video dan juga musik dari 24 aktor berbeda, berimbang secara gender. Dataset ini tersedia secara gratis dan open source, bisa didownload disini: https://zenodo.org/re-CCcord/1188976#.XQdLn5_jpEQ. Emosi yang digunakan pada perekaman dataset tersebut meliputi 8 emosi yang telah disebutkan sebelumnya. Label emosi ini ada pada nama file yang terdiri dari 7 item sebagai beikut:AA-BB-CC-DD-EE-FF-GG
Dimana label emosi ada pada item ketiga, yakni CC. Jika CC bernilai 01 = neutral, 02 = calm, 03 = happy, 04 = sad, 05 = angry, 06 = fearful, 07 = disgust, 08 = surprised.
Pada komputer saya, direktori dataset yang nampak dengan perintah tree adalah sebagai berikut:
Audio_Speech_Actors_01-24 ├── Actor_01 │ ├── 03-01-01-01-01-01-01.wav │ ├── 03-01-01-01-01-02-01.wav │ ├── 03-01-01-01-02-01-01.wav │ ├── ... │ └── 03-01-08-02-02-02-01.wav ├── Actor_02 │ ├── 03-01-01-01-01-01-02.wav │ ├── 03-01-01-01-01-02-02.wav │ ├── ... │ └── 03-01-08-02-02-02-02.wav ├── Actor_03 │ ├── 03-01-01-01-01-01-03.wav │ ├── 03-01-01-01-01-02-03.wav │ ├── ... │ └── 03-01-08-02-02-02-03.wav ├── Actor_04 │ ├── 03-01-01-01-01-01-04.wav │ ├── 03-01-01-01-01-02-04.wav │ ├── ... │ └── 03-01-08-02-02-02-04.wav ├── ... └── Actor_24 ├── 03-01-01-01-01-01-24.wav ├── 03-01-01-01-01-02-24.wav ├── ... └── 03-01-08-02-02-02-24.wav 24 directories, 1440 files
Pada kode yang akan kita buat, lokasi dataset ini, lokasi direktori `Audio_Speech_Actors_01-24` di komputer saya akan berbeda dengan komputer anda. Saya biasa menyimpan dataset pada direktori `/media/bagustris/bagus/dataset/`. Anda perlu merubahnya pada skrip ekstraksi fitur di bawah ini.
Ekstraksi Fitur
File python pertama yang kita buat saya beri nama save_feature.py di bawah direktori ravdess_ser. Penamaan file ini sangat penting, begitu juga penamaan folder/direktori. Baca ini untuk lebih jelasnya. Oya, saya menggunakan jupyter-lab IPython untuk menulis dan mengeksekusi kode python karena kemampuannya bisa mengeksekusi per baris/blok baris. Saya sarankan anda untuk menggunakannya juga.
Pertama, seperti biasa, kita deklasarikan paket-paket yang kita gunakan.
import glob import os import librosa import numpy as np from keras.utils import to_categorical import ntpath
Kita akan scan baris per baris di atas. Paket glob digunakan untuk menemukan filepath pada system Unix. Bersama dengan paket os, dua paket tersebut biasa dikombinasikan untuk mencari file/direktory pada suatu path. Dari glob kita memakai glob (glob.glob) untuk mendaftar semua wav file pada direktori dataset, dengan os kita menggabungkan subdirektori (Actor_XX) dengan main direktori (Audio_Speech_Actors_01-24). Librosa adalah library utama untuk ekstraksi fitur akustik. Numpy merupakan library python yang pasti dan selalu kita pakai. Keras, pada save_feature.py ini, digunakan untuk konversi nilai kategori (01-08) ke nilai diskrit (misal 01 menjadi [1 0 0 0 0 0 0]), dan npath digunakan untuk mendapatkan nama file (basename) dari tiap file wav.
Kode-kode selanjutnya setelah import module diatas adalah dua fungsi sebagai berikut:
# function to extract feature def extract_feature(file_name): X, sample_rate = librosa.load(file_name) stft = np.abs(librosa.stft(X)) mfcc = np.mean(librosa.feature.mfcc(y=X, sr=sample_rate, n_mfcc=40).T,axis=0) #mfcc_delta = np.mean(librosa.feature.delta(librosa.feature.mfcc(X, sample_rate, 40).T, axis=0)) #mfcc_delta2 = np.mean(librosa.feature.delta(librosa.feature.mfcc(X, sample_rate, 40).T, axis=0), order=2) chroma = np.mean(librosa.feature.chroma_stft(S=stft, sr=sample_rate).T,axis=0) mel = np.mean(librosa.feature.melspectrogram(X, sr=sample_rate).T,axis=0) contrast = np.mean(librosa.feature.spectral_contrast(S=stft, sr=sample_rate).T,axis=0) tonnetz = np.mean(librosa.feature.tonnetz(y=librosa.effects.harmonic(X), sr=sample_rate).T,axis=0) return mfcc, chroma, mel, contrast, tonnetz # mfcc_delta, mfcc_delta2, # function to parse audio file given main dir and sub dir def parse_audio_files(parent_dir,sub_dirs,file_ext="*.wav"): features, labels = np.empty((0, 193)), np.empty(0) #273: number of features, ori:193 for label, sub_dir in enumerate(sub_dirs): for fn in glob.glob(os.path.join(parent_dir, sub_dir, file_ext)): try: print('process..', fn) mfcc, chroma, mel, contrast,tonnetz = extract_feature(fn) except Exception as e: print('cannot open', fn) continue ext_features = np.hstack([mfcc, chroma, mel, contrast, tonnetz]) #ext_features = np.hstack([mfcc, mfcc_delta, mfcc_delta2, chroma, mel, contrast,tonnetz ]) features = np.vstack([features, ext_features]) filename = ntpath.basename(fn) labels = np.append(labels, filename.split('-')[2]) # grab 3rd item #labels = np.append(labels, fn.split('/')[2].split('-')[1]) return np.array(features), np.array(labels, dtype = np.int)Pada beberapa baris dua fungsi, ada yang saya comment (#) karena tidak saya pakai. Namun, baris-baris tersebut sebelumnya saya pakai dan hasilnya saya sajikan/bahas di akhir tulisan ini. Baiklah, kita walk-in lebih detail denga kedua fungsi di atas.
1. Fungsi extract_feature
Ini adalah fungsi utama kita yang mengekstrak fitur akustik dari tiap file wav inputnya.
Input: wav file (baris 14)
Output: Fitur akustik dalam bentuk vektor dari satu file wav(baris 24)
Selanjutnya di antara baris 13 dan 25 tersebut dapat dijelaskan sebagai berikut ("dari" menunjukkan input dari baris/fungsi tersebut):
- Baris 15: membaca file wav (in/dari: file_name, out: X, sample_rate)
- Baris 16: Transformasi fourier dari X dengan fungsi dari librosa stft, disimpan sebagai STFT
- Baris 17: Ekstraksi fitur MFCC dari X
- Baris 18: Ekstraksi fitur delta MFCC (tidak digunakan)
- Baris 19: Ekstraksi fitur delta-delta MFCC (tidak digunakan)
- Baris 20: Ekstraksi fitur chromagram dari STFT
- Baris 21: Ekstrasi fitur mel-scaled spectrogram dari X
- Baris 22: Ekstraksi fitur contrast dari STFT [4]
- Baris 22: Ekstraksi fitur tonnetz dari X [5]
2. Fungsi parse_audio_files
Fungsi ini digunakan untuk mengektrak fitur (dengan fungsi extract_feature di atas) diberikan nama main directory, subdirectory dan ekstensi (*.wav) sebagai inputnya (baris 27).
Output: Tumpukan array fitur akustik dari semua file dan labelnya (baris 43).
- Baris 28: Membuat array kosong untuk menampung fitur dan label
- Baris 29: Berjalan/scan pada jumlah file pada subdirektori. Untuk lebih jelasnya, perhatikan potingan kode berikut:
main_dir = '/media/bagustris/bagus/dataset/Audio_Speech_Actors_01-24/' sub_dir = os.listdir(main_dir) for label, sub in enumerate(sub_dir): print(label, sub)Output
0 Actor_01 1 Actor_02 2 Actor_03 3 Actor_04 ...
main_dir = '/media/bagustris/bagus/dataset/Audio_Speech_Actors_01-24/' sub_dir = os.listdir(main_dir) print ("\ncollecting features and labels...") print("\nthis will take some time...") features, labels = parse_audio_files(main_dir,sub_dir) labels_oh = to_categorical(labels) # one hot conversion from integer to binary print("done")
Penjelasan kode-kode di atas adalah sebagai berikut:
- Baris 46: Definisi lokasi direktori utama dataset.
- Baris 47: List semua subdirektori di bawah direktori utama dengan modul os.
- Baris 50: Ekstraksi fitur dan label dari semua file di subdirektori dengan fungsi parse_audio_file (yang memanggil fungsi extract_feature di dalamnya).
- Baris 51: Konversi label dari digit ke binary dengan Keras to_categorical.
Untuk mengecek hasil ekstraksi fitur (input) dan label (output) saya biasanya mencetak ukurannya. Ini penting karena input dan output harus sama ukurannya (barisnya).
# make sure dimension is OK print(features.shape) print(labels.shape)
Jika ukurannya sudah sesuai (hanya baris/elemen pertama saja) kita lanjut ke kode selanjutnya. Ada sedikit modifikasi pada label. Yakni, karena keras to_categorical mengkonversi dari 0, sedangkan label kita mulai dari 1 maka ukuran output label menjadi kelebihan satu kategori, dalam hal ini yang seharisnya 8 dimensi menjadi 9 dimensi. Kita hapus dimensi (kolom) pertama karena hanya berisi 0 (lagi, label kita mulai dari 1, sehingga hanya kolom k2 dua yg terisi).
Terakhir, kita simpan fitur dan label yang sudah terekstrak dalam python .npy file. Masing-masing kita simpan sebagai X dan y (Biasanya, input disimpan dalam variabel X (capital) dan output target dalam y (kecil).
# remove first column because label start from 1 (not from 0) labels_oh = labels_oh[:,1:]
Terakhir, kita simpan fitur dan label yang sudah terekstrak dalam python .npy file. Masing-masing kita simpan sebagai X dan y (Biasanya, input disimpan dalam variabel X (capital) dan output target dalam y (kecil).
# If all is OK, let save it np.save('X', features) np.save('y', labels_oh)Selesai untuk ekstraksi fitur. Jika kita jalankan file save_feature.py, maka hasilnya seperti berikut, tidak ada error.
Klasifikasi dengan Deep Learning
File python kedua adalah ser_ravdess.py yang mengolah input X.npy dan y.npy hasil ekstraksi fitur dari file extract_feature.py. Berikut detailnya.Modul/paket yang kita gunakan/import adalah sebagari berikut,
import numpy as np from keras.models import Sequential from keras.layers import Dense, Activation from keras.layers import Dropout from sklearn.model_selection import train_test_split from sklearn.metrics import confusion_matrix import pandas as pd import seaborn as sns import matplotlib.pyplot as plt
Paket seaborn kita guanakan untuk membuat plot confusion matrix, scikit-learn kita gunakan untuk membagi dataset, selebihnya paket yang umum dan wajib kita gunakan (numpy, keras, matplotlib).
Selanjutnya, kita panggil data yang telah kita simpan dalam .npy:
# load feature data X=np.load('X.npy') y=np.load('y.npy') train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.33, random_state=42)
Arsitektur Deep neural network (DNN) atau deep learning yang akan kita buat adalah seperti gambar di bawah ini, terdiri dari 4 hidden layer, masing-masing berisi:193, 400, 200, dan 100 units/nodes. Kita simpan unit tersebut ke dalam beberapa variabel.
Arsitektur DNN yang akan dibuat. Layer pertama merupakan layer input. Total ada empat layer, layer terakhir terhubung ke layer output (tidak nampak pada gambar di atas). |
# DNN layer units n_dim = train_x.shape[1] n_classes = train_y.shape[1] n_hidden_units_1 = n_dim n_hidden_units_2 = 400 # approx n_dim * 2 n_hidden_units_3 = 200 # half of layer 2 n_hidden_units_4 = 100
Sedangkan deep learning-nya sendiri kita simpan dalam sebuah fungsi "create_model".
def create_model(activation_function='relu', init_type='normal', optimiser='adam', dropout_rate=0.2): model = Sequential() # layer 1 model.add(Dense(n_hidden_units_1, input_dim=n_dim, kernel_initializer=init_type, activation=activation_function)) # layer 2 model.add(Dense(n_hidden_units_2, kernel_initializer=init_type, activation=activation_function)) model.add(Dropout(dropout_rate)) # layer 3 model.add(Dense(n_hidden_units_3, kernel_initializer=init_type, activation=activation_function)) model.add(Dropout(dropout_rate)) #layer4 model.add(Dense(n_hidden_units_4, kernel_initializer=init_type, activation=activation_function)) model.add(Dropout(dropout_rate)) # output layer model.add(Dense(n_classes, kernel_initializer=init_type, activation='softmax')) # model compilation model.compile(loss='categorical_crossentropy', optimizer=optimiser, metrics=['accuracy']) return modelSemua baris dalam fungsi tersebut sangat mudah difahami, silahkan merefer ke sini untuk lebih detailnya.
Selanjutnya, kita panggil dan lihat arsitektur DNN yang telah dibuat.
# create the model model = create_model() print(model.summary())
Hasil (arsitekturnya) adalah sebagai berikut,
Model: "sequential_3" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_11 (Dense) (None, 193) 37442 _________________________________________________________________ dense_12 (Dense) (None, 400) 77600 _________________________________________________________________ dropout_7 (Dropout) (None, 400) 0 _________________________________________________________________ dense_13 (Dense) (None, 200) 80200 _________________________________________________________________ dropout_8 (Dropout) (None, 200) 0 _________________________________________________________________ dense_14 (Dense) (None, 100) 20100 _________________________________________________________________ dropout_9 (Dropout) (None, 100) 0 _________________________________________________________________ dense_15 (Dense) (None, 8) 808 ================================================================= Total params: 216,150 Trainable params: 216,150 Non-trainable params: 0 _________________________________________________________________ None
Terlihat 4 dense layer dengan total trainable parameter sebanyak 216150. Layer dropout merupakan pengurang unit untuk menghindari overfitting.
Selanjutnya kita latih model dengan model.fit()
# train the model hist = model.fit(train_x, train_y, epochs=200, validation_data=[test_x, test_y], batch_size=4)Untuk melihat akurasi maksimum yang dicapai ketika validasi/development,
print(max(hist.history['val_accuracy']))
Dalam hal ini saya memperoleh nilai seperti ini,
0.6449579831932774
Untuk melihat hasil akurasi ketika training, ganti 'val_acc' dengan 'acc'. Untuk menilai apakah arsitektur deeplearning yang kita buat sudah berjalan dengan baik sebenarnya bukan dengan melihat nilai akurasi/akurasi validasi akurasi, namun membandingkan nilai loss dengan val_loss. Jika kedua nilainya sama, berarti hasilnya sudah maksimal (sulit untuk ditingkatkan lagi).
Langkah terakhir adalah dengan melihat akurasi pada data test.
# evaluate model, test data may differ from validation data evaluate = model.evaluate(test_x, test_y, batch_size=4) print(evaluate)Variabel 'evaluate' menunjukkan nilai loss dan akurasi sebagai berikut.
[2.087342866086108, 0.6092436974789915]Semakin kecil nilai loss semakin baik, berbanding terbalik dengan akurasi. Pada data di atas, nilai loss yang dicapai pada data test adalah 2.08 dengan akurasi sebesar 60.92%.
Hasil
Untuk mendapatkan interpretasi dari hasil training yang telah dilakukan oleh deep learning, kita bisa memplot loss dan akurasi yang diperoleh ketika proses tersebut (sebagai fungsi epoch). Berikut kode sederhananya untuk membuat dua plot untuk loss dan akurasi.
fig, axs = plt.subplots(nrows=1, ncols=2, constrained_layout=True) ax = axs[0] ax.plot(hist.history['loss']) ax.set_ylabel('loss') ax.set_xlabel('epochs') ax = axs[1] ax.plot(hist.history['accuracy']) ax.set_ylabel('accuracy') ax.set_xlabel('epochs') plt.show()Hasilsnya adalah gambar berikut.
Untuk memonitor overfitting, langkah awal paling baik adalah dengan membandingkan loss dengan validation loss (begitu juga akurasi vs akurasi validasi). Kita modifikasi script untuk plotting di atas menjadi sebagai berikut,
fig, axs = plt.subplots(nrows=1, ncols=2, constrained_layout=True) ax = axs[0] ax.plot(hist.history['loss'], label='train') ax.plot(hist.history['val_loss'], label='val') ax.legend() ax.set_ylabel('loss') ax.set_xlabel('epochs') ax = axs[1] ax.plot(hist.history['accuracy'], label='train') ax.plot(hist.history['val_accuracy'], label='val') ax.legend() ax.set_ylabel('accuracy') ax.set_xlabel('epochs') plt.show()
Hasilnya adalah sebagai berikut.
Dari gambar di atas sebenarnya kita sudah mencapai loss validasi minimum pada epoch sekitar 75 kali. Namun, kalau kita perhatikan, akurasi tetap sedikit naik. Ini yang saya agak masih kurang faham. Dari teori deep learning, sistem akan mencari titik (loss) minimum. Dan pada saat itulah akurasinya maksimum. Mungkin karena sistem masih belajar walaupun sedikit, sehingga hasil (akurasinya) tetap dalam trend naik, walaupun sedikit.
Terakhir, untuk mengetahui hasil pengenalan per kategori (pada kategori emosi apa sistem kita bisa mengenali dengan baik, dan pada kategori apa yang kurang) kita bisa memplot confusion matrix (matrik kebingungan?). Untuk mendapatkan confusion matrix, kita harus membuat prediksi hasil pengenalan kategori emosi oleh sistem dan membandingkan hasilnya dengan nilai sebenarnya (true value). Kode berikut membuat prediksi kategori emosi dari data test (text_x), menyimpannya sebagai 'predict' dan membandingkan dengan nilai sebenarnya 'test_y'. Plot confusion matrix kita buat dengan bantuan scikit-learn dan pandas.
# predicting emotion of audio test data from the model predict = model.predict(test_x,batch_size=4) emotions=['neutral', 'calm', 'happy', 'sad', 'angry', 'fearful', 'disgust', 'surprised'] # predicted emotions from the test set y_pred = np.argmax(predict, 1) predicted_emo = [] for i in range(0,test_y.shape[0]): emo = emotions[y_pred[i]] predicted_emo.append(emo) actual_emo = [] y_true = np.argmax(test_y, 1) for i in range(0,test_y.shape[0]): emo = emotions[y_true[i]] actual_emo.append(emo) # generate the confusion matrix cm = confusion_matrix(actual_emo, predicted_emo) index = ['angry', 'calm', 'disgust', 'fearful', 'happy', 'neutral', 'sad', 'surprised'] columns = ['angry', 'calm', 'disgust', 'fearful', 'happy', 'neutral', 'sad', 'surprised'] cm_df = pd.DataFrame(cm, index, columns) plt.figure(figsize=(10,6)) sns.heatmap(cm_df, annot=True)
Hasilnya adalah sebagai berikut,
Angka pada tiap sel kolom di atas menunjukkan berapa data yang ditebak benar. Misal, data angry yang ditebak benar angry ada 46, sedangkan data calm yang ditebak angry tidak ada (alias 0). Dari plot confusion_matrix di atas kita tahu bahwa sistem kita handal pada kategori emosi surprise, calm dan angry, namun kurang akurat pada kategori neutral. Ini bisa menjadi input untuk perbaikan sistem kedepannya.
Plot model
Untuk memplot model dari Keras, gunakan kode berikut.
from keras.utils import plot_model plot_model(model, to_file='model_lstm_ravdess.pdf', show_shapes=True)
Pengaruh Jumlah Fitur pada Akurasi
Pada eksperimen kali ini, saya mencoba mengubah jumlah fitur yang dipakai sebagai input. Berikut perbandingan fitur dengan akurasinya (jumlah dalam kurung menunjukkan jumlah fitur):- mfcc, mfcc_delta, mfcc_delta2 (120) = 0.5
- mfcc, mfcc_delta, mfcc_delta2, chroma, mel, contrast, tonnetz (273) = 0.56
- mfcc, chroma, mel, contrast, tonnetz (193) = 0.6
Keseluruhan kode di atas saya host di repository github berikut,
https://github.com/bagustris/ravdess_ser
Halaman ini akan diupdate secara berkala dan direvisi, jika ada.
Referensi:
1. https://medium.com/@raihanh93/speech-emotion-recognition-using-deep-neural-network-part-i-68edb5921229
2. https://medium.com/@raihanh93/speech-emotion-recognition-using-deep-neural-network-part-ii-4189273f3e2a
3. Livingstone, Steven R., & Russo, Frank A. (2018). The Ryerson Audio-Visual Database of Emotional Speech and Song (RAVDESS) (Version 1.0.0) [Data set]. PLoS ONE. Zenodo. http://doi.org/10.5281/zenodo.1188976
4.https://ieeexplore.ieee.org/document/1035731?arnumber=1035731
5.https://dl.acm.org/citation.cfm?id=1178727