Derin Öğrenme ile Duygu Analizi
Merhaba VBO okuyucuları, bir önceki yazımda (buradan erişebilirsiniz) sizlere konu tahminlemesi ile ilgili uygulama yaparak bilgi vermeye çalıştım. Bu yazımda size derin öğrenme ile duygu analizi hakkında bildiklerimi aktarmaya çalışacağım.
Sosyal medyada ya da herhangi bir forumda herhangi bir konu hakkında yorum yazarken o konuya olan yaklaşımımıza duygularımızı da katarız. Konu eğer sevdiğimiz bir konuysa o konuya yorum yazarken mutlu ve sevgi dolu cümleler, değilse o konuyla ilgili daha olumsuz veya hiçbir duygu yükü olmayan cümleler kurarız. Aldığımız ya da ilgilendiğimiz bir ürün hakkında da iyi kötü yorum yaparız ya da filmler hakkında. Peki bilgisayarlar bu yorumların iyi ya da kötü bir yorum olduğunu nasıl anlar?
Duygu analiziyle ilgili ilk başta araştırmalar yapmaya başladığımda kendi kendime hep neden makine öğrenmesi algoritmaları değilde derin öğrenme diye sorardım. Daha sonra makine öğrenmesinin metin sınıflamada kısa süreli bir hafızaya sahip olduğunu bu nedenle ilgili kelimelerin kısa süreli olarak hafızada tutulduğunu keşfettim. Doğal dil işlemede kullanılan özyinelemeli sinir ağları(RNN) algoritmaları ise cümlede yer alan kelimelerin kendisinden bir önceki ve bir sonraki kelimelerle birbirine bağlı olduğunu varsayarak onları birbiri ile ilişkili olduğunu hafızasında tutar. Bu sayede metin sınıflamasında çok daha iyi sonuçlar elde eder.
Özyinelemeli sinir ağları(RNN) uzun cümlelerde ve ifadelerde çok başarılı değildir bu nedenle çoğunlukla LSTM(Long Short Term Memory) algoritması daha çok kullanılmaktadır. Ben de bu çalışmamda Uzun-Kısa Vadeli Bellek(LSTM) algoritmasını kullandım.
Bu çalışma için veri setini kendim hazırladım. Veri setini iki sınıftan oluşturdum, olumlu ve olumsuz olarak. Bunlar veri seti içerisinde 0 ve 1 olarak etiket ismiyle yer alıyor. Veriyi aşağıdaki bağlantıdan indirebilirsiniz.
Uygulamayı Ubuntu 18.04 işletim sistemi üzerinde, Jupyter Notebook geliştirme ortamı ile python 3.6.9 ve tensorflow 1.14.0 versiyonlarını kullanarak geliştirdim.
Veri Ön İşleme
import numpy as np import pandas as pd from tensorflow.python.keras.models import Sequential from tensorflow.python.keras.layers import Dense, Embedding,LSTM from tensorflow.python.keras.optimizers import Adam from tensorflow.python.keras.preprocessing.text import Tokenizer from tensorflow.python.keras.preprocessing.sequence import pad_sequences from tensorflow.python.keras.models import load_model from sklearn.model_selection import train_test_split import re import nltk nltk.download('punkt')
WPT = nltk.WordPunctTokenizer() stop_word_list = nltk.corpus.stopwords.words('turkish') stop_word_list
['acaba', 'ama', 'aslında', 'az', 'bazı', 'belki', 'biri', 'birkaç', 'birşey', 'biz', 'bu', 'çok', 'çünkü', 'da', 'daha', 'de', 'defa', 'diye', 'eğer', 'en', 'gibi', 'hem', 'hep', 'hepsi', 'her', 'hiç', 'için', 'ile', 'ise', 'kez', 'ki', 'kim', 'mı', 'mu', 'mü', 'nasıl', 'ne', 'neden', 'nerde', 'nerede', 'nereye', 'niçin', 'niye', 'o', 'sanki', 'şey', 'siz', 'şu', 'tüm', 've', 'veya', 'ya', 'yani']
dataset = pd.read_excel('sentiment_analysis.xlsx' , sheet_name = 'Sheet1') dataset.head()
Text Sentiment
0 bana beklediğim cevapları vermiyorsun 0
1 senden istediğim cevaplar bunlar değil 0
2 verdiğin yanıtlar doğru değil 0
3 duymak istediğim cevaplar bunlar değil 0
4 seni seviyorum bro 1
Öncelikle nltk kütüphanesi içerisinde bulunan Türkçe stopword’leri yükleyip verinin ilk halini inceliyoruz. Daha sonra veriyi temizleyerek hem varsa noktalama işaretlerinden arındırmak hem de stopword’lerden arındırma işlemlerini gerçekleştiriyoruz.
#verimizde bulunan noktalama işaretlerinin temizlenme işlemi dataset['Text'] = dataset['Text'].apply(lambda x: re.sub('[,\.!?:()"]', '', x)) #büyük harflerin küçük harfe çevrilmesi dataset['Text'] = dataset['Text'].apply(lambda x: x.lower()) #fazladan boşlukların temizlenmesi dataset['Text'] = dataset['Text'].apply(lambda x: x.strip()) #cümleler içerisinde bulunan stopword'lerin kaldırılması def token(values): words = nltk.tokenize.word_tokenize(values) filtered_words = [word for word in words if word not in stopwordlist] not_stopword_doc = " ".join(filtered_words) return not_stopword_doc dataset['Text'] = dataset['Text'].apply(lambda x: token(x))
Gelin verimizin son haline bir bakalım.
dataset['Text']
0 bana beklediğim cevapları vermiyorsun 1 senden istediğim cevaplar bunlar değil 2 verdiğin yanıtlar doğru değil 3 duymak istediğim cevaplar bunlar değil 4 seni seviyorum bro … 19018 j7 pro cihazı geldi fakat faturası gelmedi 19019 müşteri hizmetlerine ulaşamama sorunu 19020 para i̇adesi sorunu 19021 mağdur ediyor 19022 ürünü aldığı halde parayı i̇ade etmedi Name: Text, Length: 19023, dtype: object
Artık verimizi temizlediğimize göre üzerinde işlem yapmaya başlayabiliriz. Verideki etiketlerle yorumlarımı birer değişkene atayarak eğitim ve test için veriyi ayırıyoruz.
data = dataset['Text'].values.tolist() sentiment = dataset['Sentiment'].values.tolist() x_train, x_test, y_train, y_test = train_test_split(data,sentiment,test_size = 0.2, random_state = 42)
Daha sonra verinin içerisinden en çok kullanılan 10000 kelimeye göre bir sözlük oluşturuyoruz. Tabi bu sayıyı belirlemek size kalmış veriye göre değişkenlik gösterebilir.
tokenizer = Tokenizer(num_words = 10000) tokenizer.fit_on_texts(data) tokenizer.word_index
{'bir': 1, 'ürün': 2, 'iyi': 3, 'güzel': 4, 'tavsiye': 5, 'gayet': 6, 'ederim': 7, 'hızlı': 8, 'aldım': 9, 'yok': 10, 'telefon': 11, 'olarak': 12, 'ürünü': 13, 'göre': 14, 'kadar': 15, ...
Burada her bir kelime kullanım sırasına göre numaralandırılıyor. Biliyoruzki cümlelerimizin uzunlukları birbirinden farklı fakat RNN algoritmaları için farklı uzunluklarda olan cümlelerle eğitemiyoruz ki farklı uzunluklarda cümleleri tahmin etmesi olanaksızlaşıyor. Bu nedenle veri setimizde bulunan bütün cümleleri aynı boyuta getirmemiz gerekiyor.
#her bir yorumu aynı boyuta getirmek gerekiyor RNN böyle çalışıyor. x_train_tokens = tokenizer.texts_to_sequences(x_train) x_test_tokens = tokenizer.texts_to_sequences(x_test)
Önce cümlelerimizdeki kelimeleri, yukarıda oluşturmuş olduğumuz sözlükte karşılığında hangi index’te yer alıyorsa onunla değiştiriyoruz.
num_tokens = [len(tokens) for tokens in x_train_tokens + x_test_tokens] num_tokens = np.array(num_tokens)
Daha sonra verimizdeki cümlelerimizin her birinin kelime sayısını alıp birer liste oluşturuyoruz.
#burada token sayısı ayarlanırken ortalama etrafındaki değişkenlik dikkate alınarak bir sayı belirlenir max_tokens = np.mean(num_tokens) + 2 * np.std(num_tokens) max_tokens = int(max_tokens)
Sonra oluşturmuş olduğumuz listenin ortalamasını alıp daha sonra standart sapmasını 2 ile çarpıp bir değer elde ediyoruz. Bu değer bize verimizdeki cümlelerimizin dağılımı ile varsa aykırı uzunluğa sahip cümleleri ortalamaya indirgememize sağlayacak.
#belirlenen bu sayı verinin yüzde kaçını kapsadığına bakılır. np.sum(num_tokens < max_tokens) / len(num_tokens)
Buradaki hesapla elde ettiğimiz değerin verimizdeki cümlelerin yüzde kaçını kapsadığını görebiliriz.
#veriler belirlenen token sayısına göre ayarlanır x_train_pad = pad_sequences(x_train_tokens, maxlen=max_tokens) x_test_pad = pad_sequences(x_test_tokens, maxlen=max_tokens)
Daha sonra cümlelerimizi yukarıda elde ettiğimiz değer kadar optimize ediyoruz ve eksik kalan kelime sayısı kadar 0 ekliyoruz. Örnek olarak aşağıdaki çıktıyı inceleyebilirsiniz.
x_train_pad[3027]
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 545, 10, 1834, 15, 9899, 3943, 228, 1817, 195, 112, 6740, 179, 690, 644, 55, 10, 2689], dtype=int32)
Daha sonra token haline getirdiğimiz cümleleri tekrar cümle haline getirmek için bir fonksiyon yazmamız gerekiyor.
#tokenlaştırılan kelimeler tekrar string hale geitirilmek için bir fonksiyon yazılması gerekiyor. idx = tokenizer.word_index inverse_map = dict(zip(idx.values(), idx.keys())) #tokenlaştırılan cümleyi tekrar string hale getirmek def tokens_to_string(tokens): words = [inverse_map[token] for token in tokens if token!=0] text = ' '.join(words) return text
Burada daha önce verimizdeki kelimelerden oluşturmuş olduğumuz sözlüğümüzü tekrar kelime ve index’ini birleştirerek bir python sözlüğü haline getiriyoruz. Daha sonra oluşturmuş olduğumuz fonksiyonda sayısal olarak gelen cümlenin içerisindeki 0 değerleri atıp kelimeleri index’leriyle yer değiştiriyoruz. Sonrada boşlukla birleştirip tekrar cümle olarak geri döndürüyoruz.
Model Oluşturma
Artık verimizi modelimizde kullanmak için hazır. Sinir ağımızı oluşturmaya geçebiliriz.
#ardışık bir model model = Sequential() #her kelimeye karşılık gelen 50 uzunluğunda bir vektör oluşturulur. (Embedding matrisi) embedding_size = 50 #matris kelime sayısı ve embedding büyüklüğünde olacak, yani 10bine 50 uzunluğunda. Buna da bir isim veriliyor name değişkeniyle. model.add(Embedding(input_dim=10000, output_dim=embedding_size, input_length=max_tokens, name='embedding_layer')) #LSTM layerlerinin eklenmesi ## 16 nöronlu LSTM (16 outputlu , return_sequences=True demek output'un tamamını ver demek) model.add(LSTM(units=16, return_sequences=True)) ## 8 nöronlu LSTM (8 outputlu , return_sequences=True demek output'un tamamını ver demek) model.add(LSTM(units=8, return_sequences=True)) ## 4 nöronlu LSTM (4 outputlu , return_sequences=False yani default değer, tek bir output verecek) model.add(LSTM(units=4)) ## output layer'ı , görsel olarak gösterilirken dense layer kullanılır. Tek bir nörondan oluştuğu için 1 yazılır. model.add(Dense(1, activation='sigmoid')) #optimizasyon algoritması, 1e-3 = 0.001 demek. optimizer = Adam(lr=1e-3) #modeli derlemek, loss fonksiyonu binary_crossentropy -> sadece 2 sınıf ama daha fazla sınıflar için categorical_crossentropy kullanılır. #metrics -> modelin başarısını görmek için. model.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
Burada ardışık bir model oluşturarak içerisine bir embedding katmanı, 3 adet LSTM katmanı ve bir çıkış katmanı ekledim. Çıkış katmanımda aktivasyon fonksiyonu olarak sigmoid fonksiyonunu kullandım çünkü iki sınıflı bir veri setim var. Modelin özetine bakarsak;
model.summary()
Model: “sequential” _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_layer (Embedding) (None, 61, 50) 500000 _________________________________________________________________ lstm (LSTM) (None, 61, 16) 4288 _________________________________________________________________ lstm_1 (LSTM) (None, 61, 8) 800 _________________________________________________________________ lstm_2 (LSTM) (None, 4) 208 _________________________________________________________________ dense (Dense) (None, 1) 5 ================================================================= Total params: 505,301 Trainable params: 505,301 Non-trainable params: 0
Sinir ağımızı oluşturduğumuza göre artık eğitme işlemine geçebiliriz. Ben 5 kez eğitilmesini ve modelimin 256’şar şekilde beslenmesini istedim. Eğitim verimi tekrar 0.25 oranında bölerek test etmesini istedim.
#model eğitimi, bir defa eğitimden geçmesi -> epoch , batch_size -> 256'şar 256'şar beslenecek. history = model.fit(x_train_pad, y_train,validation_split=0.25, epochs=5, batch_size=256)
Train on 11413 samples, validate on 3805 samples Epoch 1/5 11413/11413 [==============================] – 7s 581us/sample – loss: 0.6109 – acc: 0.7108 – val_loss: 0.5099 – val_acc: 0.7556 Epoch 2/5 11413/11413 [==============================] – 5s 425us/sample – loss: 0.4399 – acc: 0.8333 – val_loss: 0.3742 – val_acc: 0.8649 Epoch 3/5 11413/11413 [==============================] – 6s 503us/sample – loss: 0.2953 – acc: 0.9148 – val_loss: 0.2884 – val_acc: 0.9038 Epoch 4/5 11413/11413 [==============================] – 5s 433us/sample – loss: 0.2119 – acc: 0.9452 – val_loss: 0.2581 – val_acc: 0.9180 Epoch 5/5 11413/11413 [==============================] – 5s 413us/sample – loss: 0.1597 – acc: 0.9633 – val_loss: 0.2442 – val_acc: 0.9275
Gördüğünüz gibi oluşturmuş olduğumuz modelimizin train datasında başarısı bir hayli iyi. Bir de test datasına bakalım.
result = model.evaluate(x_test_pad, y_test)
3805/3805 [==============================] - 4s 927us/sample - loss: 0.2488 - acc: 0.9235
Modelimizi oluşturduk ve iki sınıflı duygu analizimizde test datamızda da güzel bir sonuç aldık. Gelin bir de sonuçlarımızı görselleştirerek bakalım.
Görsele gördüğümüz gibi epoch sayımız 3 e geldiğinde modelimiz en iyi sonuçları vermektedir. Epoch sayısı arttıkça modelimizin eğitim verisinde başarısı artsada test verisinde artış giderek azalmakta.
Buradaki görselde de modelimizin epoch sayısı 3’e geldiğinde test verisinin artık loss değerinin azalmadığını görüyoruz. Bu nedenle bu modelimiz için en iyi eğitim süresi 3 diyebiliriz.
Github hesabımdan kodların tamamına erişebilirsiniz.
https://github.com/serkanars/turkishsentimentanalysis
Bir yazının daha sonuna gelmiş bulunuyoruz, okuduğunuz için teşekkür ederim. Bir sonraki yazıda görüşmek üzere, veriyle kalın.
Hocam Merhaba,
Öncelikle emeğine sağlık.Ben sizin yukarıdaki datasetinizle çalışıp aynı adımları yaptım ancak aşağıdaki aşamaya geldiğimde hata alıyorum.
#model eğitimi, bir defa eğitimden geçmesi -> epoch , batch_size -> 256’şar 256’şar beslenecek.
history = model.fit(x_train_pad, y_train,validation_split=0.25, epochs=5, batch_size=256)
Konu hakkında yardımcı olabilir misiniz ?
İyi Çalışmalar.
history model fitte hata alanlar bir üst satırda
x_train_pad = np.array(x_train_pad)
y_train = np.array(y_train)
x_test_pad = np.array(x_test_pad)
y_test = np.array(y_test)
kodlarını çalıştırsın
Merhabalar çalışma için çok teşekkür ediyorum benim için çok faydalı oldu.
Ancak şöyle bir durum var verileri çoğalttım ve toplam 28000 verim var. Bunların 21000 eğitim ve 7000 test verisi olarak kullandım.
Epoch 5/5
64/64 [==============================] – 8s 127ms/step – loss: 0.1414 – accuracy: 0.9689 – val_loss: 0.3127 – val_accuracy: 0.8894
170/170 [==============================] – 2s 14ms/step – loss: 0.3138 – accuracy: 0.8892
eğitim yukarıdaki şekilde oluyor. Neden 64 görünüyor 21000 olması gerekmiyor mu veya 170 yerine 7000 olması gerekmiyor mu?
Yardımcı olursanız çok mutlu olurum.