BERT modelleri son yıllarda doğal dil işleme alanında pek çok alanda diğer modellere üstünlük sağlamayı başardı. Bu modellerin iç işleyişi oldukça karmaşık ve eğitim için de ciddi miktarda veri ve işlemci gücü gerekiyor. Öte yandan hazır eğitilmiş pek çok BERT modeli var. Bunları kullanarak ve hatta yapmak istediğiniz işe bağlı olarak ek eğitim kümeleriyle besleyerek güzel sonuçlar elde etmek mümkün. İşin güzeli yakın zaman önce Türkçe bir BERT modeli de yayınlandı (https://github.com/stefan-it/turkish-bert) ve bu modele Python'da transformers
paketiyle erişilebiliyor.
Biz bu modeli Türkçe bir film yorumları verisi üzerinde kullanacağız. Veri setine https://www.kaggle.com/mustfkeskin/turkish-movie-sentiment-analysis-dataset adresinden ulaşabilirsiniz, sadece kaggle'a üye olmanız yeterli. Bu veri setinden yorumları okuyup, yoruma karşılık gelen puana bakarak kullanıcı filmi beğenmiş mi beğenmemiş mi anlamaya çalışacağız. Bir çeşit sentiment (duygu) analizi.
Öncelikle kullanacağımız kütüphaneler.
import numpy as np
import pandas as pd
import torch
import transformers as ppb
from torch import nn
import torch.nn.functional as F
from torch import optim
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix
from sklearn.metrics import roc_auc_score
import os
Veri setini yükleyip nasıl göründüğüne bakalım.
dirpath = os.getcwd()
basedir = os.path.split(dirpath)[0]
datafolder = os.path.join(basedir, 'Data/')
df = pd.read_csv(datafolder + 'turkish_movie_sentiment_dataset.csv')
df.head()
comment film_name point
0 \n Jean Reno denince zate... Sevginin Gücü 5,0
1 \n Ekşın falan izlemek is... Sevginin Gücü 5,0
2 \n Bu yapım hakkında öyle... Sevginin Gücü 5,0
3 \n finali yeter... (sting... Sevginin Gücü 5,0
4 \n Jean Reno..\r\nbu adam... Sevginin Gücü 5,0
BERT modellerinin kendilerine ait kelimelere ayırma yöntemleri olduğu için metni temizleme (\n'leri silme vs gibi) işlerini atlayabiliriz. Fakat point sütununda düzeltmemiz gereken bir sorun var, puanlar virgülle ayrılmış bunların nokta olması gerekir. Bunu yapalım ve bu örnek için üzerinde çalışmak için veriden küçük bir kısım seçelim.
batch1 = df.sample(n=1000)
mask = batch1['comment'].str.len() < 300
batch2 = batch1.loc[mask]
batch2 = batch2.assign(pointc = [x.replace(',', '.') for x in batch2['point']])
batch2.loc[:,'pointc'] = batch2['pointc'].astype(float).copy()
batch2 = batch2.assign(lab = pd.cut(batch2['pointc'],bins=[0,3,5],labels=[0,1]).values)
batch2["lab"] = pd.to_numeric(batch2["lab"], downcast="float")
batch2.head()
comment film_name point pointc lab
33401 \n Filmi çok beğendiğimi ... Domino 2,5 2.5 0.0
25270 \n film çok eğlenceliydi.... Sevgi Herşeydir 2,5 2.5 0.0
72177 \n ilk piranha filmi çok ... Pirana 3D 3,0 3.0 0.0
28791 \n yaklaşık 2 ay önce izl... Arthur ile Minimoylar 2,5 2.5 0.0
65170 \n film güzeldide seks sa... Watchmen 0,5 0.5 0.0
lab
sütununa 3'e kadar (3 dahil) puanlara karşılık 0, daha fazlasına 1 yazdık. Yani 3'ten fazla puan vermişse filmi beğenmiştir diye bir yorumladık puanları. Şimdi transformers paketinden hazır modeli ve bu modelin kelime ayırıcısını (tokenizer) yükleyelim.
from transformers import AutoModel, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("dbmdz/distilbert-base-turkish-cased")
model = AutoModel.from_pretrained("dbmdz/distilbert-base-turkish-cased")
Şimdi bu tokenizerı kullanarak metinleri kelimelerine ayrıştıralım. Bunu yaparken (şu anda aslında ihtiyacımız olmasa da) her metne dolgu (padding) uygulayalım ki bütün yorumlar eşit uzunlukta olsun. Bu şekilde her bir yorumu tek seferde modele verebiliriz ve bu büyük veri setlerinde ciddi performans kazandırır. Tabii dolgulu metinlerde nereye bakması gerektiğini de modele söylemek lazım, bu da aşağıda attention_mask
değişkeninde.
tokenized = batch2['comment'].apply((lambda x: tokenizer.encode(x, add_special_tokens=True)))
max_len = 0
for i in tokenized.values:
if (len(i) > max_len):
max_len = len(i)
padded = np.array([i + [0]*(max_len-len(i)) for i in tokenized.values])
attention_mask = np.where(padded != 0, 1, 0)
Şimdi metinleri BERT modeline verip çıktıları alabiliriz.
input_ids = torch.tensor(padded).to(torch.int64)
attention_mask = torch.tensor(attention_mask).to(torch.int64)
with torch.no_grad():
last_hidden_states = model(input_ids, attention_mask=attention_mask)
last_hidden_states
değişkeni bir liste ve bu listenin ilk elemanında BERT modelin çıktıları var. Buna yakından bakarsanız 3 boyutlu bir torch tensor nesnesi olduğunu göreceksiniz:
last_hidden_states[0].size()
torch.Size([668, 75, 768])
Bu boyutlardan ilki 668, elimizdeki yorum sayısı, ikincisi bu yorumların maksimum uzunluğu (çünkü bütün yorumları bu uzunluğa gelecek şekilde dolgu ile modele verdik), en sonuncusuda model çıktıları. Yani model her bir metindeki her bir token (kelime) için 768 boyutlu bir vektör üretiyor.
BERT modelinin bir alameti farikası her metnin başına ve sonuna özel başlangıç ve bitiş işaretleri koyması. Başlangıç işaretine denk gelen model çıktıları ise, aslında modelin metnin tümüne verdiği özellikler olarak değerlendirilebilir. Modelin metinlere verdiği çıktılar olarak bunları kullanacağız.
features = last_hidden_states[0][:,0,:].numpy()
labels = batch2['lab']
x_train, x_test, y_train, y_test = train_test_split(features, labels, test_size=0.2)
x_train, x_test = map(
torch.tensor, (x_train, x_test)
)
y_train = torch.tensor(y_train.values, dtype=torch.long)
y_test = torch.tensor(y_test.values, dtype=torch.long)
Şimdi basit bir model tanımlayıp eğitelim. Bu tek gizli katmanı olan bir neural network, aslında lojistik regresyona denk gelir.
input_size = len(features[0])
num_classes = len(labels.unique())
class M_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.lin = nn.Linear(input_size, num_classes)
def forward(self, xb):
return self.lin(xb)
loss_func = F.cross_entropy
def get_model(lr):
model = M_Logistic()
return model, optim.SGD(model.parameters(), lr=lr)
m1, opt = get_model(3e-2)
def fit(model, epochs, batch_size):
for epoch in range(epochs):
n = len(x_train)
for i in range((n - 1) // batch_size + 1):
start_i = i * batch_size
end_i = start_i + batch_size
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
if epoch % 500 == 499:
print("Epoch: %d, loss: %1.5f" % (epoch, loss.item()))
fit(m1,3000,100)
Epoch: 499, loss: 0.95722
Epoch: 999, loss: 0.66753
Epoch: 1499, loss: 0.51791
Epoch: 1999, loss: 0.42640
Epoch: 2499, loss: 0.40514
Epoch: 2999, loss: 0.36442
Bu kadar gelmişken sonuçlara da bakalım.
preds1 = torch.argmax(m1(x_test), dim=1)
print(roc_auc_score(y_test, preds1))
confusion_matrix(y_test, preds1)
0.5868178596739899
[[15, 36],
[10, 73]]
0.58 AUC muhteşem değil. Fakat neredeyse hiç bir şey yapmadan, ve çok çok küçük bir veri setiyle bu sonuca ulaşmış olmak oldukça iyi.