Недавно я наткнулся на API для NASA Exoplanet Archive, общедоступного онлайн-хранилища данных с Космического телескопа Кеплера. Эта сокровищница высококачественных размеченных данных созрела для проекта классификации машинного обучения. Хотя у меня не было знаний в области астрофизики, я задавался вопросом, насколько легко (или сложно) для сидящего в кресле астронавта найти планеты в десятках световых лет от нас.

Набор данных KOI (Kepler Objects of Interest) тщательно контролируется и требует относительно небольшой очистки перед исследованием данных и моделированием. Я просто исключил наблюдения, содержащие нулевые значения, и любые столбцы, содержащие меры ошибок, информацию, не относящуюся к наблюдениям (например, идентификационные номера или информацию, полученную в результате анализа после наблюдения), и несколько почти избыточных столбцов, чтобы уменьшить мультиколлинеарность (например, равновесная температура и поток инсоляции — это два значения). различные меры температуры поверхности планеты).

С несколькими сильно мультиколлинеарными переменными есть по существу два пути вперед: (1) удалить несколько коллинеарных функций и/или спроектировать составные функции или (2) выполнить анализ основных компонентов (PCA), чтобы полностью избежать проблемы, но за счет интерпретируемости функций. …Я решил исследовать оба пути.

Подход (1) требовал тщательного изучения определений переменных, чтобы определить, какую переменную из каждой коллинеарной пары следует отбросить, повторной проверки глобальных корреляций, а затем перехода к следующей паре. После того, как самые крупные нарушители были устранены, я разработал взвешенные составные признаки из этих пар со средними показателями коллинеарности, используя приведенную ниже функцию:

import numpy as np
import pandas as pd
def feature_builder(var_1, var_2, target, dataset):
    """
    Parameters :
    ------------
    var_1 : column name of first correlated variable
    var_2 : column name of second correlated variable
    target : column name of target variable
    dataset : DataFrame that holds the data and will receive updates
    """
    weights = np.linspace(0, 1, 10000)
    max_corr = -1
    best_weights = None
    corrs = []
for index, weight in enumerate(weights):
        w1 = weight       # get the first weight value
        w2 = 1 - weight   # get the second weight value
        vals = w1*dataset[var_1] + w2*dataset[var_2] # create a linear combination of the columns
        corr_coeff = np.abs(np.corrcoef(vals, dataset[target]))[0][1] # get the corrcoeff with the target
# if the corr_coeff is larger than the max, store the weights and change the max
        if corr_coeff > max_corr:
            best_weights = [w1, w2]
            max_corr = corr_coeff
# store the correlation coefficients to a list
        corrs.append(corr_coeff)  
    
    # output the desired weights
    print('weight for [',var_1,'] : weight for [',var_2,']\n  ', best_weights)
    feat_label = str(var_1+'_'+var_2+'_feat')
    print('feature name:', feat_label)
    
    # add feature to dataset & remove input columns
    dataset[feat_label] = w1*dataset[var_1] + w2*dataset[var_2]
    dataset.drop([var_1, var_2], axis=1, inplace=True)
    pass

На этом этапе я применил метод рекурсивного исключения признаков (RFECV) sklearn, чтобы определить оптимальное количество признаков (оказывается, 7), которые нужно вернуть для дальнейшего моделирования.

Я протестировал несколько алгоритмов классификации в поисках лучшего (XGBClassifier, SVC, RandomForestClassifier и т. д.). Чтобы ускорить этот процесс, я создал несколько функций для автоматизации поиска в сетке оптимальных параметров, обновления этих параметров, повторного поиска в сетке, а также вывода матриц путаницы вместе с показателями точности для наборов данных для обучения и тестирования:

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import GridSearchCV, train_test_split
from xgboost.sklearn import XGBClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.decomposition import PCA
def convert_params(best_params):
    params = {}
    for key, val in best_params.items():
        params[key] = [val]
    return params
def get_best_params(cv_results):
    """
    input:     model.cv_results_
    returns:   dictionary of parameters with the highest harmonic 
    mean balancing mean_test_score and (1 - test_train_diff)
    This reduces overfitting while maximizing test score.
    """
    dfp = pd.DataFrame(cv_results)
    dfp['test_train_diff'] = np.abs(dfp['mean_train_score'] - dfp['mean_test_score'])
    dfp['harmonic'] = 2 / ((1 / dfp['mean_test_score']) + (1 / (1-dfp['test_train_diff'])))
    dfp.sort_values(by='harmonic', ascending=False, inplace=True)
    dfp.reset_index(drop=True, inplace=True)
    return convert_params(dfp.iloc[0].params)
# define a function to generate a confusion matrix
def confu_matrix(y_pred, x_tst, y_tst):
    import warnings
    warnings.filterwarnings('ignore')
    y_pred = np.array(y_pred).flatten()
    y_tst = np.array(y_tst).flatten()
    cm = confusion_matrix(y_tst.flatten(), y_pred.flatten())
    sns.heatmap(cm, annot=True, fmt='0g', 
                annot_kws={'size':14, 'ha':'center', 'va':'top'})
    sns.heatmap(cm/np.sum(cm), annot=True, fmt='0.01%', 
                annot_kws={'size':14, 'ha':'center', 'va':'bottom'})
    plt.title('Confusion Matrix', fontsize=14)
    plt.show();
def gridsearch_params(estimator, params_test, old_params=None, 
                      update_params=True, scoring='accuracy'):
    """
    Inputs an instantiated estimator and a dictionary of parameters
    for tuning (optionally an old dictionary of established parameters)
    Returns a dictionary of the new best parameters.
    Requires X_train, X_test, y_train, y_test to exist as global variables.
    """
    import warnings
    warnings.filterwarnings('ignore')
    if update_params:
        old_params.update(params_test)
        params_test = old_params
    gsearch1 = GridSearchCV(estimator=estimator, refit=True,
                            param_grid=params_test, scoring=scoring,
                            n_jobs=4, iid=False, cv=5)
    gsearch1.fit(X_train, y_train.values.flatten())
    best_params = get_best_params(gsearch1.cv_results_)
    gsearch1a = GridSearchCV(estimator=estimator, refit=True,
                             param_grid=best_params,
                             scoring=scoring,n_jobs=4,
                             iid=False, cv=5)
    gsearch1a.fit(X_train, y_train.values.flatten())
    confu_matrix(gsearch1a.predict(X_test), X_test, y_test)
    tr_acc = round(metrics.accuracy_score(y_train.values.flatten(), gsearch1a.predict(X_train)), 4)*100
    tst_acc = round(metrics.accuracy_score(y_test.values.flatten(), gsearch1a.predict(X_test)), 4)*100
    print(f"Train accuracy: {tr_acc}%\nTest accuracy: {tst_acc}%\n{best_params}")
    return best_params, gsearch1a

Мой лучший результат (максимизация точности теста при минимизации переобучения обучающим данным) был получен с моделью SVC, которая использовала gridsearchCV для настройки гиперпараметров. Эта модель показала точность тестирования 82,34%.

Как уже упоминалось, я также снова попробовал все эти алгоритмы моделирования на наборе данных, прошедшем PCA. Теоретически при этом сохраняется больше информации, поскольку ни один из признаков не удаляется, при этом полностью избегая ловушек мультиколлинеарности. Версия SVC для PCA дала идентичные результаты версии без PCA! Другие попытки не были столь успешными. Тем не менее, я попытался объединить модели SVC и XGBClassifier, используя LogisticRegression в качестве объединяющей модели.

from sklearn.model_selection import StratifiedKFold
def Stacking(model, train, test, y, n_fold):
    train = pd.DataFrame(train)
    test = pd.DataFrame(test)
    folds = StratifiedKFold(n_splits=n_fold, random_state=42)
    test_pred = np.empty((0,1), float)
    train_pred = np.empty((0,1), float)
    for train_indices, val_indices in folds.split(train, y.values):
        x_tr, x_val = train.iloc[train_indices], train.iloc[val_indices]
        y_tr, y_val = y.iloc[train_indices], y.iloc[val_indices]
            model.fit(x_tr, y_tr)
        train_pred = np.append(train_pred, model.predict(x_val))
    test_pred = np.append(test_pred, model.predict(test))
    return test_pred, train_pred
#base model 1 (XGBoostClassifier)
model1 = clf_xgb
test_pred1, train_pred1 = Stacking(model=model1, n_fold=10, train=X_train, test=X_test, y=y_train)
train_pred1 = pd.DataFrame(train_pred1)
test_pred1 = pd.DataFrame(test_pred1)
#base model 2 (SVC)
model2 = clf_svc
test_pred2, train_pred2 = Stacking(model=model2, n_fold=10, train=X_train, test=X_test, y=y_train)
train_pred2 = pd.DataFrame(train_pred2)
test_pred2 = pd.DataFrame(test_pred2)
# stacking a new model (LogisticRegression) on top of the two bases
df_stack_tr = pd.concat([train_pred1, train_pred2], axis=1)
df_stack_tr.columns = ['xgb','svc']
df_stack_tst = pd.concat([test_pred1, test_pred2], axis=1)
df_stack_tst.columns = ['xgb','svc']
model_stack = LogisticRegression(solver='lbfgs', random_state=42)
model_stack.fit(df_stack_tr, y_train)
print('Train Score:', model_stack.score(df_stack_tr, y_train))
print('Test Score:', model_stack.score(df_stack_tst, y_test))
y_pred = model_stack.predict(df_stack_tst)
confu_matrix(y_pred, df_stack_tst, y_test)
train_pred1 = pd.DataFrame(train_pred1)
test_pred1 = pd.DataFrame(test_pred1)

Этот стек моделей действительно дал несколько более высокий показатель точности теста 82,86 %, но показатель обучения 79,9 % был дальше, чем для простого SVC. Хотя это говорит о недостаточномсоответствии данных, после дальнейшего изучения кажется, что это обычный артефакт алгоритма SVC. Таким образом, невозможно определить, насколько модель недостаточно/избыточно/хорошо соответствует обучающим данным.