Недавно я наткнулся на 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. Таким образом, невозможно определить, насколько модель недостаточно/избыточно/хорошо соответствует обучающим данным.