Stacking ile Kelimelerden Dil Tahmini

17 minute read

Published:

alt text

English Version

Devamı: Streamlit ve Heroku ile Canlıya Çıkmak

Problem

  • Türkçe ve İngilizce kelimelerden oluşan bir veri setimiz var ve kelimelere göre dili algılamak istiyoruz. Tahmin etmek istediğimiz hedef değişken ise ìs_turkish adında ikili (binary) bir değişken.
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.metrics import f1_score, accuracy_score, confusion_matrix, precision_score, recall_score
from sklearn.linear_model import RidgeClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import KFold, StratifiedKFold, train_test_split
from lightgbm import LGBMClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
import lightgbm as lgb
from sklearn.svm import LinearSVC
import gc

Veri Önişleme

  • Kelimelere ait meta değişkenler olan harf sayısı, sesli harf sayısı ve sessiz harf sayısı hesaplanmıştır.
  • TF-IDF ve Count Vectorizer kullanılarak kelimeler karakter bazında vektör haline getirilmiştir.
  • Vektörler oluşturulduktan sonra train ve test setleri 0.8 ve 0.2 oranlarında ayrılmıştır (Pareto prensibi).
  • Train setinde negatif olan gözlemler için downsampling yapılmıştır.
seed = 1001
df = pd.read_csv("language_detection.csv")
df.head()
Wordsis_turkish
0ihtiva1
1ajenjo0
2lamby0
3epee0
4ilmi1
df["is_turkish"].value_counts()
0    17332
1     2123
Name: is_turkish, dtype: int64
df["is_turkish"].value_counts(normalize = True)
0    0.890876
1    0.109124
Name: is_turkish, dtype: float64

Toplamda 19455 kelime var ve bunların %10 Türkçe. Hedef değişkenin dağılımı itibariyle de problemin dengesiz (imbalanced/unbalanced) olduğu söylenebilir!

df["Words"].apply(lambda x: len(x)).value_counts()
6    10550
5     6108
4     2797
Name: Words, dtype: int64

Kelimeler ise en az 4 en fazla 6 harfe sahip. Bu bilgiyi kullanacağız ileride.

Metin verilerinden çıkarılabilecek en basit düzeydeki değişkenler meta-features olarak isimlendirilmektedir. Buna göre aşağıdaki değişkenkler hesaplanmıştır;

  • Sesli harf sayısı
  • Sessiz harf sayısı
  • Toplam harf sayısı

Bunların yanı sıra tekil (unique) sesli, tekil sessiz harf sayısı hesaplanabilir ya da elimizdekinden farklı olarak cümleler içeren bir veri seti için noktalama işaretlerinin sayısı, boşluk karakterlerinin sayısı, tekil kelime sayısı gibi daha pek çok farklı değişken çıkarılabilir.

df['Vowels'] = df.Words.str.lower().str.count(r'[aeiou]')
df['Consonant'] = df.Words.str.lower().str.count(r'[a-z]') - df['Vowels']
df["Len"] = df.Words.apply(lambda x: len(x))
df.head()
Wordsis_turkishVowelsConsonantLen
0ihtiva1336
1ajenjo0336
2lamby0145
3epee0314
4ilmi1224

Metinlerin işleyebilmek için öncelikle sayısal değerlere çevirmeliyiz. Bunun için ise Count Vectorizer ve TF-IDF yöntemlerinden yararlanacağız. Yöntemlerin detayları için;

  • Count Vectorizer
  • TF-IDF

    n-gram oluştururken bunu karakter özelinde mi yoksa kelime özelinde mi yapacağımızı analyze argümanı ile belirtiyoruz. Burada kelime ve karakter olarak denedim fakat en iyi sonucu karakter ile aldım. Bunun nedeninin de elimizde tek kelime olması (muhtemelen). Zira elimizde cümleler olsaydı kelimeler üzerinden ilerleyebilecektik.
    N-Gram’ları oluştururken de 1,6’lık olacak şekilde oluşturdum. 6 sayısını ise elimizdeki kelimelerin en fazla 6 harfli olması nedeniyle seçtim. Farklı kombinasyonlar ile (n,6 gibi) farklı skorlar elde etmek mümkün.

n-gram nedir?

Bu aşamada göz önünde bulundurmanız gerken şeylerden biri de sahip olduğunuz RAM. Muhtemelen vektör boyutlarını (max_features) arttırdıkça çok daha iyi sonuçlar alacaksınız fakat hem vektörlerin dönüşümü sırasında hem de modelleme aşamasında OOM (out of memory) hatasıyla karşılaşabilirsiniz. Bu nedenle biraz deneme yanılma ile maksimum RAM limitini bilerek ve bunun altında kalmaya çalışmak çok daha sağlıklı.

word = False
word_tfidf = False
count_para_word = {
    "analyzer": "word",
    "dtype": np.float32,
}

count_para_char = {
    "analyzer": "char_wb",
    "dtype": np.float32, # RAM için bir diğer optimize edilebilecek argüman.
    "ngram_range": (1, 6) # maksimum harf sayımız 6 olduğu için üst sınır 6
}
if word:
    vectorizer = CountVectorizer(
        max_features=50000, 
        **count_para_word)
else:
    vectorizer = CountVectorizer(
        max_features=50000, 
        **count_para_char)
tfidf_para_word = {
    "analyzer": "word",
    "sublinear_tf": True,
    "dtype": np.float32,
    "norm": 'l2',
    "min_df": 5,
    "max_df": 0.9,
    "smooth_idf": False
}

tfidf_para_char = {
    "analyzer": "char_wb",
    "dtype": np.float32,
    "ngram_range": (1, 6) # maksimum harf sayımız 6 olduğu için üst sınır 6
}

if word_tfidf:
    vectorizer_tfidf = TfidfVectorizer(
                max_features=5000,
                **tfidf_para_word)
else:
    vectorizer_tfidf = TfidfVectorizer(
                max_features=50000,
                **tfidf_para_char)
vectorizer.fit(df["Words"])
vectorizer_tfidf.fit(df["Words"])

Metni sayıya dönüştürmeden önce veriyi train ve test olacak şekilde bölüyoruz. Burada problemimiz dengesiz olduğu için bu dağılımı korumak adına stratify argümanı ile veriyi y değişkeni üzerinden tabakalandırarak bölüyoruz ve Pareto prensibi ile %80’e %20 olacak şekilde ayırıyoruz.

train_vector, test_vector = train_test_split(df, test_size = 0.2, stratify = df["is_turkish"], random_state = seed)

Ayırdığımız train setinde ise downsampling yaparak örneklemi küçültüyoruz. Pozitif olan yani Türkçe olan kelimelere karşılık İngilizce kelimelerin %40’ını (rastgele) alarak yeni bir alt veri seti oluşturuyoruz. (random_state kullanarak tekrar edilebilir bir işlem olmasını sağlıyoruz).

train_vector = train_vector[(train_vector.is_turkish == 1) | (
    train_vector.is_turkish == 0).sample(frac=0.4, random_state=seed)].sample(
        frac=1, random_state=seed).reset_index(drop=True)
y_train = train_vector["is_turkish"].values
y_test = test_vector["is_turkish"].values

TF-IDF ve Count Vec. dönüşümlerini train ve test verisi için yapıyoruz.

train_vector_ = vectorizer.transform(train_vector["Words"])
test_vector_ = vectorizer.transform(test_vector["Words"])
train_vector_tfidf = vectorizer_tfidf.transform(train_vector["Words"])
test_vector_tfidf = vectorizer_tfidf.transform(test_vector["Words"])
from scipy.sparse import hstack, csr_matrix

Count Vec. ve TF-IDF’i kelimeler üzerinde uyguladık ve artık elimizde seyrek (sparse) matrisler var. Aynı zamanda daha önce oluşturduğumuz meta değişkenler vardı. Şimdi onları birleştirerek modele girecek olan veri setinin nihai halini oluşturuyoruz. Bunun için de CSR (compressed sparse row matrix) formatını kullanıyoruz.

train_vector = hstack([
    train_vector_,
    train_vector_tfidf,
    csr_matrix(train_vector.Vowels.values.reshape(-1, 1)),
    csr_matrix(train_vector.Consonant.values.reshape(-1, 1)),
    csr_matrix(train_vector.Len.values.reshape(-1, 1))
])
test_vector = hstack([
    test_vector_,
    test_vector_tfidf,
    csr_matrix(test_vector.Vowels.values.reshape(-1, 1)),
    csr_matrix(test_vector.Consonant.values.reshape(-1, 1)),
    csr_matrix(test_vector.Len.values.reshape(-1, 1))
])
train_vector = train_vector.tocsr()
test_vector = test_vector.tocsr()
train_vector.shape, y_train.shape
((7254, 100003), (7254,))
 test_vector.shape, y_test.shape
((3891, 100003), (3891,))
del train_vector_, test_vector_
del train_vector_tfidf, test_vector_tfidf

(RAM’den tasarruf!)

Modelleme

Kaggle’da katıldığım ilk yarışma olan Allstate Claims Severity yarışmasında Stacking Starter olarak Faron tarafından paylaşılan Stacking kodundan yararlanacağız.

Buradaki asıl soru ise şu; Stacking nedir? Basitçe anlatmak gerekirse normal şartlarda modellerimizi bağımsız değişkenleri (independent variables a.k.a x’ler) kullanarak bağımlı değişkeni (ya da değişkenleri - dependent variable) tahmin edecek şekilde kuruyoruz. Fakat Stacking ile bu işi bir adım daha ileriye götürerek öğrendiklerimizden öğrenmeye çalışıyoruz. Artık bir modelin çıktısı (tahminleri) başka bir modelde girdi halini alıyor.

Peki nelere dikkat etmek gerekiyor? Öncelikle single model dediğimiz yani çıktılarını kullanacağımız tekli modellerde uyguladığımız CV şemasını uygulamamız gerekiyor. Herhangi bir leakage olmasını istemeyoruz. Bir de iyi sonuç alabilmek için model çeşitliliğini (diversity) arttırmamız gerekiyor. Bu nedenle bu notebook’ta Random Forest ve Lojistik Regresyon için iki farklı parametre seti ile modeller kurdum. Genel olarak Stacking aşamasında skorunuzu arttırmak istiyorsanız çare bol bol farklı model ve farklı subset’ler (farklı değişkenlere sahip alt veri setleri) ile eğtilimiş modeller. Bu noktada modellere ait tahminler arasındaki korelasyona bakmakta fayda var. Ayrıca mümkün olduğunca yüksek skora sahip düşük korelasyonlu modelleri kullanmak gerekir. Yoksa benzer skora sahip, aralarında 0.99998 korelasyon olan modellerin bir getirisi olmayacaktır!


Stacking’i özetlemek gerekirse de aşağıdaki görsel bize bir hayli yardımcı olacaktır.

Kaynak

Stacking için kaynaklar

Aşağıdaki kod bloğu bildiğim kadarıyla Faron’a ait bu nedenle referans olarak linkimizi ekleyelim.
And the oscar goes to Faron: https://www.kaggle.com/mmueller/stacking-starter
Buna ek olarak ben predict_proba metodunu ve fold skorlarını görmek adına ufak print kısımlarını ekledim.

class SklearnWrapper(object):
    def __init__(self, clf, seed=0, params=None, seed_bool = True):
        if(seed_bool == True):
            params['random_state'] = seed
        self.clf = clf(**params)

    def train(self, x_train, y_train):
        self.clf.fit(x_train, y_train)

    def predict(self, x):
        return self.clf.predict(x)
        
    def predict_proba(self, x):
        return self.clf.predict_proba(x)
    
    
def get_oof(clf, x_train, y, x_test, prob = False):
    oof_train = np.zeros((ntrain,))
    oof_test = np.zeros((ntest,))
    oof_test_skf = np.empty((NFOLDS, ntest))

    for i, (train_index, test_index) in enumerate(kf.split(x_train, y)):
        x_tr = x_train[train_index]
        y_tr = y[train_index]
        x_te = x_train[test_index]
        y_te = y[test_index]

        clf.train(x_tr, y_tr)
        if prob:
            oof_train[test_index] = clf.predict_proba(x_te)[:,1]
            oof_test_skf[i, :] = clf.predict_proba(x_test)[:,1]
        else:
            oof_train[test_index] = clf.predict(x_te)
            oof_test_skf[i, :] = clf.predict(x_test)
        print("Fold:", i+1)
        print("F1", np.round(f1_score(y_te, np.round(oof_train[test_index])),6))
        print("\n")
        
        
    oof_test[:] = oof_test_skf.mean(axis=0)
    return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)
def scorer(y_true, y_pred, is_return = False):
    if is_return:
        return [f1_score(y_true, np.round(y_pred)), accuracy_score(y_true, np.round(y_pred)), recall_score(y_true, np.round(y_pred)), precision_score(y_true, np.round(y_pred))]
    else:
        print("F1: {:.4f}".format(f1_score(y_true, np.round(y_pred))))
        print("Accuracy: {:.4f}".format(accuracy_score(y_true, np.round(y_pred))))
        print("Recall: {:.4f}".format(recall_score(y_true, np.round(y_pred))))
        print("Precision: {:.4f}".format(precision_score(y_true, np.round(y_pred))))
        print("AUC: {:.4f}".format(roc_auc_score(y_true, y_pred)))
        print((confusion_matrix(y_true, np.round(y_pred))))
NFOLDS = 5
ntrain = train_vector.shape[0]
ntest = test_vector.shape[0]
kf = StratifiedKFold(n_splits=NFOLDS, shuffle=True, random_state=seed)

Kullanılan Algoritmalar

  • Ridge Regresyon
  • Naive Bayes
  • SVM (Lineer Kernel ile)
  • Lojistik Regresyon
  • Random Forest
  • GBM (LGB)

Modellere ait parametreler ise elle (deneme yanılma) ayarlandı. Pek tabii farklı yöntemler (grid & bayesian) ile daha iyi sonuçlar elde etmek mümkün.

Ridge

ridge_params = {'alpha':250.0, 'fit_intercept':True, 'normalize':True, 'copy_X':True,
                'max_iter':None, 'tol':0.001, 'solver':'auto', 'random_state':seed,
               "class_weight": "balanced"
               }
ridge = SklearnWrapper(clf=RidgeClassifier, seed = seed, params = ridge_params)
%%time
ridge_oof_train, ridge_oof_test = get_oof(ridge, train_vector, y_train, test_vector, prob=False)
Fold: 1
F1 0.642005


Fold: 2
F1 0.596871


Fold: 3
F1 0.633735


Fold: 4
F1 0.608696


Fold: 5
F1 0.612717


Wall time: 190 ms
scorer(ridge_oof_train, y_train)
F1: 0.6186
Accuracy: 0.7760
Recall: 0.5142
Precision: 0.7762
[[4311  380]
 [1245 1318]]
scorer(ridge_oof_test, y_test)
F1: 0.4139
Accuracy: 0.7708
Recall: 0.2871
Precision: 0.7412
[[2684  110]
 [ 782  315]]

SVM

svm_params = {"C": 3, "max_iter": 10000, "class_weight": "balanced"}
svm = SklearnWrapper(clf=LinearSVC, seed = seed, params = svm_params)
%%time
svm_oof_train, svm_oof_test = get_oof(svm, train_vector.toarray(), y_train, test_vector.toarray(), prob=False)
Fold: 1
F1 0.654321


Fold: 2
F1 0.630265


Fold: 3
F1 0.622291


Fold: 4
F1 0.636364


Fold: 5
F1 0.638427


Wall time: 44 s
scorer(svm_oof_train, y_train)
F1: 0.6364
Accuracy: 0.8357
Recall: 0.6601
Precision: 0.6143
[[5019  655]
 [ 537 1043]]
scorer(svm_oof_test, y_test)
F1: 0.5319
Accuracy: 0.8810
Recall: 0.4663
Precision: 0.6188
[[3165  162]
 [ 301  263]]

LightGBM

lgbm_params =  {
    'task': 'train',
    'boosting_type': 'gbdt',
    'objective': 'binary',
    'metric': 'binary_error',
    'num_leaves': 17,
    "max_depth": -1,
    'feature_fraction': 0.95,
    'bagging_fraction': 0.9,
    'bagging_freq': 8,
    'learning_rate': 0.075,
    'verbose': 0,
    "n_estimator": 50,
    "pos_bagging_fraction": 0.8,
    "neg_bagging_fraction": 0.6,
}
%%time
lgbc = SklearnWrapper(clf=LGBMClassifier, seed = seed, params = lgbm_params)
lgbc_oof_train, lgbc_oof_test = get_oof(lgbc, train_vector, y_train, test_vector, prob=True)
Fold: 1
F1 0.680062


Fold: 2
F1 0.594324


Fold: 3
F1 0.664516


Fold: 4
F1 0.65


Fold: 5
F1 0.664596


Wall time: 3.81 s
scorer(lgbc_oof_train, y_train)
F1: 0.6514
Accuracy: 0.8467
Recall: 0.6964
Precision: 0.6119
[[5103  659]
 [ 453 1039]]
scorer(lgbc_oof_test, y_test)
F1: 0.5293
Accuracy: 0.8885
Recall: 0.4909
Precision: 0.5741
[[3213  181]
 [ 253  244]]

Random Forest

rf_params = {"n_jobs":12,
            "n_estimators": 380,
            "class_weight": "balanced",
            "min_samples_split": 5}
%%time
rf = SklearnWrapper(clf=RandomForestClassifier, seed = seed, params = rf_params)
rf_oof_train, rf_oof_test = get_oof(rf, train_vector, y_train, test_vector, prob=True)
Fold: 1
F1 0.635417


Fold: 2
F1 0.561798


Fold: 3
F1 0.620939


Fold: 4
F1 0.616667


Fold: 5
F1 0.623917


Wall time: 32.2 s
scorer(rf_oof_train, y_train)
F1: 0.6125
Accuracy: 0.8482
Recall: 0.7612
Precision: 0.5124
[[5283  828]
 [ 273  870]]
scorer(rf_oof_test, y_test)
F1: 0.5308
Accuracy: 0.9041
Recall: 0.5703
Precision: 0.4965
[[3307  214]
 [ 159  211]]
rf_2_params = {"n_jobs":12,
               "max_depth": 15,
               "n_estimators": 500,
               "criterion": "gini",
               "min_samples_split": 5,
               "min_samples_leaf": 3,
               "class_weight": "balanced"}
%%time
rf_2 = SklearnWrapper(clf=RandomForestClassifier, seed = seed, params = rf_2_params)
rf_2_oof_train, rf_2_oof_test = get_oof(rf_2, train_vector, y_train, test_vector, prob=True)
Fold: 1
F1 0.675393


Fold: 2
F1 0.625337


Fold: 3
F1 0.655827


Fold: 4
F1 0.65404


Fold: 5
F1 0.657068


Wall time: 10.2 s
scorer(rf_2_oof_train, y_train)
F1: 0.6537
Accuracy: 0.8186
Recall: 0.5909
Precision: 0.7314
[[4696  456]
 [ 860 1242]]
scorer(rf_2_oof_test, y_test)
F1: 0.4760
Accuracy: 0.8314
Recall: 0.3603
Precision: 0.7012
[[2937  127]
 [ 529  298]]

Lojistik Regresyon

lr_params = {"n_jobs":12,
            "solver": "saga",
             "C": 10,
            "max_iter": 1000}
%%time
lr = SklearnWrapper(clf=LogisticRegression, seed = seed, params = lr_params)
lr_oof_train, lr_oof_test = get_oof(lr, train_vector, y_train, test_vector, prob = True)
Fold: 1
F1 0.6752


Fold: 2
F1 0.628205


Fold: 3
F1 0.644013


Fold: 4
F1 0.645161


Fold: 5
F1 0.662441


Wall time: 16.9 s
scorer(lr_oof_train, y_train)
F1: 0.6510
Accuracy: 0.8485
Recall: 0.7064
Precision: 0.6037
[[5130  673]
 [ 426 1025]]
scorer(lr_oof_test, y_test)
F1: 0.5527
Accuracy: 0.8964
Recall: 0.5231
Precision: 0.5859
[[3239  176]
 [ 227  249]]
lr_2_params = {
            "solver": "liblinear",
    "class_weight": "balanced",
            "max_iter": 750,
            "C":25,
              "penalty": "l1"}
%%time
lr_2 = SklearnWrapper(clf=LogisticRegression, seed = seed, params = lr_2_params)
lr_2_oof_train, lr_2_oof_test = get_oof(lr_2, train_vector, y_train, test_vector, prob = True)
Fold: 1
F1 0.666667


Fold: 2
F1 0.642314


Fold: 3
F1 0.660633


Fold: 4
F1 0.623748


Fold: 5
F1 0.670537


Wall time: 2.95 s
scorer(lr_2_oof_train, y_train)
F1: 0.6527
Accuracy: 0.8380
Recall: 0.6552
Precision: 0.6502
[[4975  594]
 [ 581 1104]]
scorer(lr_2_oof_test, y_test)
F1: 0.5309
Accuracy: 0.8792
Recall: 0.4610
Precision: 0.6259
[[3155  159]
 [ 311  266]]

Threshold Optimization (Eşik Optimizasyonu)

Bu noktada modellerden elde ettiğimiz olasılıkları kullandığımız metrik olan F1 için maksimize edeceğiz. Bu nedenle F1 skorunun en büyüklendiği kesim noktasını bulmamız gerekiyor. Benim bunu yapma sebebim ise Ridge ve SVM’in olasılık döndürmemesi. Tüm girdilerin aynı formatta olması adına böyle bir yuvarlama yapıyorum fakat olasılık olarak girdi vermeniz durumunda da model olasılık tahmin edeceği için zorunlu bir durum değil.
SVM ve Ridge’e ait eşiklerin 0 gelme nedeni ise bahsettiğim gibi olasılık yerine halihazırda yuvarlanmış değer dönüyor olması.

def threshold_search(y_true, y_proba):
    best_threshold = 0
    best_score = 0
    for threshold in [i * 0.01 for i in range(100)]:
        score = f1_score(y_true=y_true, y_pred=y_proba > threshold)
        if score > best_score:
            best_threshold = threshold
            best_score = score
    search_result = {'threshold': best_threshold, 'f1': best_score}
    return search_result
ridge_thr = threshold_search(y_train, ridge_oof_train)
ridge_thr
{'threshold': 0.0, 'f1': 0.6186341234452007}
lgb_thr = threshold_search(y_train, lgbc_oof_train)
lgb_thr
{'threshold': 0.39, 'f1': 0.6808743169398906}
rf_thr = threshold_search(y_train, rf_oof_train)
rf_thr
{'threshold': 0.36, 'f1': 0.6817420435510888}
rf2_thr = threshold_search(y_train, rf_2_oof_train)
rf2_thr
{'threshold': 0.5, 'f1': 0.6536842105263159}
lr_thr = threshold_search(y_train, lr_oof_train)
lr_thr
{'threshold': 0.19, 'f1': 0.6791243993593166}
lr2_thr = threshold_search(y_train, lr_2_oof_train)
lr2_thr
{'threshold': 0.37, 'f1': 0.6624857468643103}
svm_thr = threshold_search(y_train, svm_oof_train)
svm_thr
{'threshold': 0.0, 'f1': 0.6363636363636365}

Stacking

Stacking için yeni modellerden çıkan tahminlerimizi kullanarak girdi matrisini oluşturuyoruz.

train_df = np.concatenate((
    ridge_oof_train,
    (lr_oof_train > lr_thr["threshold"]).astype(int),
    (lr_2_oof_train > lr2_thr["threshold"]).astype(int),
    (rf_oof_train > rf_thr["threshold"]).astype(int),
    (rf_2_oof_train > rf2_thr["threshold"]).astype(int),
    (lgbc_oof_train > lgb_thr["threshold"]).astype(int),
    svm_oof_train
),
                          axis=1)
test_df = np.concatenate((
    ridge_oof_test,
    (lr_oof_test > lr_thr["threshold"]).astype(int),
    (lr_2_oof_test > lr2_thr["threshold"]).astype(int),
    (rf_oof_test > rf_thr["threshold"]).astype(int),
    (rf_2_oof_test > rf2_thr["threshold"]).astype(int),
    (lgbc_oof_test > lgb_thr["threshold"]).astype(int),
    svm_oof_test
),
                         axis=1)
# Yeni veri setimiz
pd.DataFrame(train_df).head()
0123456
00.00.00.00.00.00.00.0
10.00.00.00.00.01.00.0
20.00.00.00.00.00.00.0
31.00.00.00.01.00.00.0
40.00.00.00.00.00.00.0
y_train.shape, train_df.shape
((7254,), (7254, 7))

Stacking modeli olarak LightGBM kullanıyoruz. Burada dikkat edilmesi gerekenlerden biri stacking modelinin bir önceki adımdaki modellere oranla daha basit bir model olarak seçilmesi. LightGBM için konuşacak olursak maksimum derinlik parametresi tekli modeller için örneğin 7 ise bu aşamada kuracağımız modelin derinliğinin 7’den küçük olması gerekiyor. Bu şekilde modelin karmaşıklığını (model complexity) azaltarak varyans - yanlılık (variance - bias trade-off) değiş tokuşunu kontrol altında tutup aşırı öğrenmenin (overfit) önüne geçebiliriz.

Bu aşamada dilersek yine bir önceki aşamadaki gibi aynı CV şemasını kullanarak bir çıktı elde edebiliriz. Fakat burada LightGBM’in built-in CV fonksiyonunu kullanarak ağaç sayısını belirliyoruz. Elde edilen ağaç sayısını da %10 arttırarak tekli modeldeki ağaç sayısı seçiyoruz.

dtrain = lgb.Dataset(train_df, label=y_train)
def lgb_f1_score(y_hat, data):
    y_true = data.get_label()
    y_hat = np.round(y_hat)
    return 'f1', f1_score(y_true, y_hat), True
lgbm_params =  {
    'task': 'train',
    'boosting_type': 'gbdt',
    'objective': 'binary',
    'metric': 'auc',
    'num_leaves': 11,
    "max_depth": 4,
    'feature_fraction': 0.9,
    'bagging_fraction': 0.95,
    'bagging_freq': 12,
    'learning_rate': 0.05,
    "scale_pos_weight":  0.9,
    'verbose': 0,
    "pos_bagging_fraction": 0.8,
    "neg_bagging_fraction": 0.6,
}
cv = lgb.cv(lgbm_params,
            dtrain,
            num_boost_round=1000,
            folds=kf,
            verbose_eval=5,
            early_stopping_rounds=20,
            feval=lgb_f1_score,
            eval_train_metric=False)
[5]	cv_agg's auc: 0.874307 + 0.011259	cv_agg's f1: 0 + 0
[10]	cv_agg's auc: 0.876046 + 0.0124176	cv_agg's f1: 0 + 0
[15]	cv_agg's auc: 0.876212 + 0.0122827	cv_agg's f1: 0.631688 + 0.0228328
[20]	cv_agg's auc: 0.87627 + 0.0122567	cv_agg's f1: 0.645804 + 0.0201663
[25]	cv_agg's auc: 0.876185 + 0.0122421	cv_agg's f1: 0.668783 + 0.0182651
[30]	cv_agg's auc: 0.876121 + 0.0123478	cv_agg's f1: 0.676299 + 0.0233192
[35]	cv_agg's auc: 0.876392 + 0.0123643	cv_agg's f1: 0.683745 + 0.0182382
[40]	cv_agg's auc: 0.876375 + 0.0123732	cv_agg's f1: 0.686564 + 0.0171678
[45]	cv_agg's auc: 0.876349 + 0.0124942	cv_agg's f1: 0.68731 + 0.0191348
[50]	cv_agg's auc: 0.876425 + 0.0125615	cv_agg's f1: 0.688089 + 0.0185613
[55]	cv_agg's auc: 0.876408 + 0.0123638	cv_agg's f1: 0.686265 + 0.0162085
[60]	cv_agg's auc: 0.876458 + 0.0123725	cv_agg's f1: 0.687302 + 0.0159655
[65]	cv_agg's auc: 0.876493 + 0.0123624	cv_agg's f1: 0.687565 + 0.0165241
[70]	cv_agg's auc: 0.876494 + 0.0122952	cv_agg's f1: 0.690089 + 0.0192894
[75]	cv_agg's auc: 0.876437 + 0.0123238	cv_agg's f1: 0.690022 + 0.0194542
[80]	cv_agg's auc: 0.876431 + 0.0123069	cv_agg's f1: 0.690445 + 0.0192664
[85]	cv_agg's auc: 0.876308 + 0.012435	cv_agg's f1: 0.690434 + 0.0194785
cv["f1-mean"][-1]
0.6900975355028047
best_n = len(cv["f1-mean"])
best_n
68
model = lgb.train(lgbm_params, dtrain, num_boost_round=int(best_n * 1.1))
test_preds = model.predict(test_df)
scorer(y_test, np.round(test_preds))
F1: 0.5632
Accuracy: 0.8836
Recall: 0.6871
Precision: 0.4771
[[3146  320]
 [ 133  292]]
results_df = pd.DataFrame([scorer(ridge_oof_test, y_test, is_return=True),
scorer(svm_oof_test, y_test, is_return=True),
scorer(lgbc_oof_test, y_test, is_return=True),
scorer(rf_oof_test, y_test, is_return=True),
scorer(rf_2_oof_test, y_test, is_return=True),
scorer(lr_oof_test, y_test, is_return=True),
scorer(lr_2_oof_test, y_test, is_return=True),
scorer(y_test, np.round(test_preds), is_return=True)], columns=["F1", "Accuracy", "Recall", "Precision"], index = ["Ridge", "SVM", "LGB", "RF", "RF2", "LR", "LR2", "Stacked Model"])
results_df
F1AccuracyRecallPrecision
Ridge0.4139290.7707530.2871470.741176
SVM0.5318500.8810070.4663120.618824
LGB0.5292840.8884610.4909460.574118
RF0.5308180.9041380.5702700.496471
RF20.4760380.8314060.3603390.701176
LR0.5527190.8964280.5231090.585882
LR20.5309380.8792080.4610050.625882
Stacked Model0.5631630.8835770.6870590.477124

Sonuç itibariyle en iyi F1 skorunu Stacking modeli ile elde ettik. Fakat burada Precision ve Recall arasında ufak bir değiş tokuş (trade-off) yapıyoruz. Bu nedenle problemin içeriği & kısıtları, dengesizlik oranı (imbalanced ratio) gibi durumlar göz önüne alınarak hem önişleme hem de modelleme aşamalarının tekrar gözden geçirilmesi gerekir. Örneğin downsampling oranının değiştirilmesi, parametre optimizasyonu ya da maliyet fonksiyonun (cost function) değiştirilmesi gibi değişiklikler yapılabilir.
Stacking yerine de seçilen modeller için lineer ağırlıklandırma kullanılarak blending denenebilir (weighted linear blending).

Son olarak bu notebook’ta Stacking yöntemlerine basit bir giriş yapmayı ve bunun yanı sıra metin işlemek için Deep Learning harici, back to basics diyebileceğimiz yöntemleri göstermeyi amaçladım.