Как ускорить вложенную перекрестную проверку в python? - PullRequest
6 голосов
/ 23 апреля 2019

Из того, что я обнаружил, есть еще 1 такой вопрос ( Ускоренная вложенная перекрестная проверка ), однако установка MPI не работает для меня после того, как я попробовал несколько исправлений, также предложенных на этом сайте и в Microsoft Я надеюсь, что есть другой пакет или ответ на этот вопрос.

Я хочу сравнить несколько алгоритмов и поиск по сетке с широким диапазоном параметров (может быть, слишком много параметров?). Какие существуют способы, кроме mpi4py, которые могут ускорить выполнение моего кода? Насколько я понимаю, я не могу использовать n_jobs = -1, поскольку тогда он не является вложенным?

Также отметим, что я не смог запустить это по многим параметрам, которые я пытаюсь рассмотреть ниже (работает дольше, чем у меня есть время). Получите результаты только через 2 часа, если я дам каждой модели только 2 параметра для сравнения. Также я запускаю этот код в наборе данных из 252 строк и 25 столбцов функций с 4 категориальными переменными, чтобы предсказать («определенные», «вероятные», «возможные» или «неизвестные»), влияет ли ген (с 252 генами) на заболевание , Использование SMOTE увеличивает размер выборки до 420, что затем используется.

dataset= pd.read_csv('data.csv')
data = dataset.drop(["gene"],1)
df = data.iloc[:,0:24]
df = df.fillna(0)
X = MinMaxScaler().fit_transform(df)

le = preprocessing.LabelEncoder()
encoded_value = le.fit_transform(["certain", "likely", "possible", "unlikely"])
Y = le.fit_transform(data["category"])

sm = SMOTE(random_state=100)
X_res, y_res = sm.fit_resample(X, Y)

seed = 7
logreg = LogisticRegression(penalty='l1', solver='liblinear',multi_class='auto')
LR_par= {'penalty':['l1'], 'C': [0.5, 1, 5, 10], 'max_iter':[500, 1000, 5000]}

rfc =RandomForestClassifier()
param_grid = {'bootstrap': [True, False],
              'max_depth': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, None],
              'max_features': ['auto', 'sqrt'],
              'min_samples_leaf': [1, 2, 4,25],
              'min_samples_split': [2, 5, 10, 25],
              'n_estimators': [200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]}

mlp = MLPClassifier(random_state=seed)
parameter_space = {'hidden_layer_sizes': [(10,20), (10,20,10), (50,)],
     'activation': ['tanh', 'relu'],
     'solver': ['adam', 'sgd'],
     'max_iter': [10000],
     'alpha': [0.1, 0.01, 0.001],
     'learning_rate': ['constant','adaptive']}

gbm = GradientBoostingClassifier(min_samples_split=25, min_samples_leaf=25)
param = {"loss":["deviance"],
    "learning_rate": [0.15,0.1,0.05,0.01,0.005,0.001],
    "min_samples_split": [2, 5, 10, 25],
    "min_samples_leaf": [1, 2, 4,25],
    "max_depth":[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, None],
    "max_features":['auto', 'sqrt'],
    "criterion": ["friedman_mse"],
    "n_estimators":[200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]
    }

svm = SVC(gamma="scale", probability=True)
tuned_parameters = {'kernel':('linear', 'rbf'), 'C':(1,0.25,0.5,0.75)}

def baseline_model(optimizer='adam', learn_rate=0.01):
    model = Sequential()
    model.add(Dense(100, input_dim=X_res.shape[1], activation='relu')) 
    model.add(Dropout(0.5))
    model.add(Dense(50, activation='relu')) #8 is the dim/ the number of hidden units (units are the kernel)
    model.add(Dense(4, activation='softmax'))
    model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
    return model

keras = KerasClassifier(build_fn=baseline_model, batch_size=32, epochs=100, verbose=0)
learn_rate = [0.001, 0.01, 0.1, 0.2, 0.3]
optimizer = ['SGD', 'RMSprop', 'Adagrad', 'Adadelta', 'Adam', 'Adamax', 'Nadam']
kerasparams = dict(optimizer=optimizer, learn_rate=learn_rate)

inner_cv = KFold(n_splits=10, shuffle=True, random_state=seed)
outer_cv = KFold(n_splits=10, shuffle=True, random_state=seed)

models = []
models.append(('GBM', GridSearchCV(gbm, param, cv=inner_cv,iid=False, n_jobs=1)))
models.append(('RFC', GridSearchCV(rfc, param_grid, cv=inner_cv,iid=False, n_jobs=1)))
models.append(('LR', GridSearchCV(logreg, LR_par, cv=inner_cv, iid=False, n_jobs=1)))
models.append(('SVM', GridSearchCV(svm, tuned_parameters, cv=inner_cv, iid=False, n_jobs=1)))
models.append(('MLP', GridSearchCV(mlp, parameter_space, cv=inner_cv,iid=False, n_jobs=1)))
models.append(('Keras', GridSearchCV(estimator=keras, param_grid=kerasparams, cv=inner_cv,iid=False, n_jobs=1)))


results = []
names = []
scoring = 'accuracy'
X_train, X_test, Y_train, Y_test = train_test_split(X_res, y_res, test_size=0.2, random_state=0)


for name, model in models:
    nested_cv_results = model_selection.cross_val_score(model, X_res, y_res, cv=outer_cv, scoring=scoring)
    results.append(nested_cv_results)
    names.append(name)
    msg = "Nested CV Accuracy %s: %f (+/- %f )" % (name, nested_cv_results.mean()*100, nested_cv_results.std()*100)
    print(msg)
    model.fit(X_train, Y_train)
    print('Test set accuracy: {:.2f}'.format(model.score(X_test, Y_test)*100),  '%')
    print("Best Parameters: \n{}\n".format(model.best_params_))
    print("Best CV Score: \n{}\n".format(model.best_score_))

Например, большая часть набора данных является двоичной и выглядит следующим образом:

gene   Tissue    Druggable Eigenvalue CADDvalue Catalogpresence   Category
ACE      1           1         1          0           1            Certain
ABO      1           0         0          0           0            Likely
TP53     1           1         0          0           0            Possible

Буду признателен за любые указания о том, как я могу ускорить это.

Редактировать: Я также пытался использовать параллельную обработку с dask, но я не уверен, что я делаю это правильно, и кажется, что он не работает быстрее:

for name, model in models:
    with joblib.parallel_backend('dask'):
        nested_cv_results = model_selection.cross_val_score(model, X_res, y_res, cv=outer_cv, scoring=scoring)
        results.append(nested_cv_results)
        names.append(name)
        msg = "Nested CV Accuracy %s: %f (+/- %f )" % (name, nested_cv_results.mean()*100, nested_cv_results.std()*100)
        print(msg)
        model.fit(X_train, Y_train)
        print('Test set accuracy: {:.2f}'.format(model.score(X_test, Y_test)*100),  '%')
    #print("Best Estimator: \n{}\n".format(model.best_estimator_))
        print("Best Parameters: \n{}\n".format(model.best_params_))
        print("Best CV Score: \n{}\n".format(model.best_score_)) #average of all cv folds for a single combination of the parameters you specify 

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

Ответы [ 4 ]

3 голосов
/ 27 апреля 2019

Dask-ML имеет масштабируемые реализации GridSearchCV и RandomSearchCV, которые, как мне кажется, заменяют Scikit-Learn.Они были разработаны вместе с разработчиками Scikit-Learn.

Они могут быть быстрее по двум причинам:

2 голосов
/ 25 апреля 2019

Две вещи:

  1. Вместо GridSearch попробуйте использовать HyperOpt - это библиотека Python для последовательной и параллельной оптимизации.

  2. Я бы уменьшил размерность, используя UMAP или PCA . Вероятно, UMAP - лучший выбор.

После подачи заявления SMOTE:

import umap

dim_reduced = umap.UMAP(
        min_dist=min_dist,
        n_neighbors=neighbours,
        random_state=1234,
    ).fit_transform(smote_output)

И затем вы можете использовать dim_reduced для разделения теста на поезд.

Уменьшение размерности поможет убрать шум из данных, и вместо 25 функций вы уменьшите их до 2 (с помощью UMAP) или числа компонентов, которые вы выберете (с помощью PCA). Что должно иметь существенное влияние на производительность.

1 голос
/ 30 апреля 2019

IIUC, вы пытаетесь распараллелить этот пример из sklearn документов. Если это так, то вот один из возможных подходов к адресу

почему даска не работает

и

Любое конструктивное руководство или дополнительные знания по этой проблеме

Общий импорт

import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn import preprocessing
from imblearn.over_sampling import SMOTE
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score, GridSearchCV, KFold, train_test_split
from sklearn.neural_network import MLPClassifier
import dask_ml.model_selection as dcv


import time

Данные

  • Я определил 3 набора данных, чтобы опробовать реализацию dask_ml
    • размер, # строк, третьего (набор данных 3), который можно регулировать, и может быть произвольно увеличен в зависимости от вашей вычислительной мощности
      • Я рассчитал выполнение dask_ml, используя только этот набор данных
    • приведенный ниже код работает для всех 3 наборов данных
    • Набор данных 1 - это немного более длинная версия образца данных в вопросе SO
#### Dataset 1 - longer version of data in the question
d = """gene Tissue Druggable Eigenvalue CADDvalue Catalogpresence Category
ACE 1 1 1 0 1 Certain
ABO 1 0 0 0 0 Likely
TP53 1 1 0 0 0 Possible"""
data = pd.DataFrame([x.split(' ') for x in d.split('\n')])
data.columns = data.loc[0,:]
data.drop(0, axis=0, inplace=True)
data = pd.concat([data]*15)

data = data.drop(["gene"],1)
df = data.iloc[:,0:5]

X = MinMaxScaler().fit_transform(df)
le = preprocessing.LabelEncoder()
encoded_value = le.fit_transform(["Certain", "Likely", "Possible"])
Y = le.fit_transform(data["Category"])

sm = SMOTE(random_state=100)
X_res, y_res = sm.fit_resample(X, Y)
#### Dataset 2 - iris dataset from example in sklearn nested cross validation docs
# Load the dataset
from sklearn.datasets import load_iris
iris = load_iris()
X_res = iris.data
y_res = iris.target
#### Dataset 3 - size (#rows, #columns) is adjustable (I used this to time code execution)
X_res = pd.DataFrame(np.random.rand(300,50), columns=['col_'+str(c+1) for c in list(range(50))])
from random import shuffle
cats = ["paris", "barcelona", "kolkata", "new york", 'sydney']
y_values = cats*int(len(X_res)/len(cats))
shuffle(y_values)
y_res = pd.Series(y_values)

Создание классификаторов - без изменений по коду в вопросе

seed = 7
logreg = LogisticRegression(penalty='l1', solver='liblinear',multi_class='auto')
LR_par= {'penalty':['l1'], 'C': [0.5, 1, 5, 10], 'max_iter':[500, 1000, 5000]}

mlp = MLPClassifier(random_state=seed)
parameter_space = {'hidden_layer_sizes': [(10,20), (10,20,10), (50,)],
     'activation': ['tanh', 'relu'],
     'solver': ['adam', 'sgd'],
     'max_iter': [10000],
     'alpha': [0.1, 0.01, 0.001],
     'learning_rate': ['constant','adaptive']}

rfc =RandomForestClassifier()
param_grid = {'bootstrap': [True, False],
              'max_depth': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, None],
              'max_features': ['auto', 'sqrt'],
              'min_samples_leaf': [1, 2, 4,25],
              'min_samples_split': [2, 5, 10, 25],
              'n_estimators': [200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]}

gbm = GradientBoostingClassifier(min_samples_split=25, min_samples_leaf=25)
param = {"loss":["deviance"],
    "learning_rate": [0.15,0.1,0.05,0.01,0.005,0.001],
    "min_samples_split": [2, 5, 10, 25],
    "min_samples_leaf": [1, 2, 4,25],
    "max_depth":[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, None],
    "max_features":['auto', 'sqrt'],
    "criterion": ["friedman_mse"],
    "n_estimators":[200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]
    }

svm = SVC(gamma="scale", probability=True)
tuned_parameters = {'kernel':('linear', 'rbf'), 'C':(1,0.25,0.5,0.75)}
inner_cv = KFold(n_splits=10, shuffle=True, random_state=seed)
outer_cv = KFold(n_splits=10, shuffle=True, random_state=seed)

Используйте GridSearchCV в соответствии с dask_ml (как первоначально предложено @MRocklin здесь ) - см. dask_ml документы для dask_ml.model_selection.GridSearchCV

  • для краткости я исключаю KerasClassifier и вспомогательную функцию baseline_model(), но мой подход к обработке первого будет таким же, как и для других
models = []
models.append(('MLP', dcv.GridSearchCV(mlp, parameter_space, cv=inner_cv,iid=False, n_jobs=1)))
models.append(('GBM', dcv.GridSearchCV(gbm, param, cv=inner_cv,iid=False, n_jobs=1)))
models.append(('RFC', dcv.GridSearchCV(rfc, param_grid, cv=inner_cv,iid=False, n_jobs=1)))
models.append(('LR', dcv.GridSearchCV(logreg, LR_par, cv=inner_cv, iid=False, n_jobs=1)))
models.append(('SVM', dcv.GridSearchCV(svm, tuned_parameters, cv=inner_cv, iid=False, n_jobs=1)))

Инициализировать дополнительный пустой список для хранения не вложенных результатов резюме

non_nested_results = []
nested_results = []
names = []
scoring = 'accuracy'
X_train, X_test, Y_train, Y_test = train_test_split(X_res, y_res, test_size=0.2, random_state=0)

Joblib и dask настройка клиента

# Create a local cluster
from dask.distributed import Client
client = Client(processes=False, threads_per_worker=4,
        n_workers=1, memory_limit='6GB')
from sklearn.externals import joblib

Выполнение вложенного резюме в соответствии с примером sklearn документов

  • первое исполнение GridSearchCV
  • второе использование cross_val_score
  • обратите внимание, что в демонстрационных целях я использовал только модель 1 sklearn (SVC) из списка моделей в примере кода в вопросе
start = time.time()
for name, model in [models[-1]]:
  # Non_nested parameter search and scoring
  with joblib.parallel_backend('dask'):
    model.fit(X_train, Y_train)
  non_nested_results.append(model.best_score_)

  # Nested CV with parameter optimization
  nested_score = cross_val_score(model, X=X_train, y=Y_train, cv=outer_cv)
  nested_results.append(nested_score.mean())

  names.append(name)
  msg = "Nested CV Accuracy %s: %f (+/- %f )" %\
        (name, np.mean(nested_results)*100, np.std(nested_results)*100)
  print(msg)
  print('Test set accuracy: {:.2f}'.format(model.score(X_test, Y_test)*100),  '%')
  print("Best Estimator: \n{}\n".format(model.best_estimator_))
  print("Best Parameters: \n{}\n".format(model.best_params_))
  print("Best CV Score: \n{}\n".format(model.best_score_))

score_difference = [a_i - b_i for a_i, b_i in zip(non_nested_results, nested_results)]
print("Average difference of {0:6f} with std. dev. of {1:6f}."
      .format(np.mean(score_difference), np.std(score_difference)))

print('Total running time of the script: {:.2f} seconds' .format(time.time()-start))

client.close()

Ниже приведены выходные данные (с указанием времени выполнения скрипта) с использованием набора данных 3

Выход + синхронизация без dask 1

Nested CV Accuracy SVM: 20.416667 (+/- 0.000000 )
Test set accuracy: 16.67 %
Best Estimator: 
SVC(C=0.75, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='scale', kernel='linear',
  max_iter=-1, probability=True, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

Best Parameters: 
{'C': 0.75, 'kernel': 'linear'}

Best CV Score: 
0.2375

Average difference of 0.033333 with std. dev. of 0.000000.
Total running time of the script: 23.96 seconds

Выход + синхронизация с dask (с использованием n_workers=1 и threads_per_worker=4) 2

Nested CV Accuracy SVM: 18.750000 (+/- 0.000000 )
Test set accuracy: 13.33 %
Best Estimator: 
SVC(C=0.5, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf',
  max_iter=-1, probability=True, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

Best Parameters: 
{'C': 0.5, 'kernel': 'rbf'}

Best CV Score: 
0.1916666666666667

Average difference of 0.004167 with std. dev. of 0.000000.
Total running time of the script: 8.84 seconds

Выход + синхронизация с dask (с использованием n_workers=4 и threads_per_worker=4) 2

Nested CV Accuracy SVM: 23.333333 (+/- 0.000000 )
Test set accuracy: 21.67 %
Best Estimator: 
SVC(C=0.25, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='scale', kernel='linear',
  max_iter=-1, probability=True, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

Best Parameters: 
{'C': 0.25, 'kernel': 'linear'}

Best CV Score: 
0.25

Average difference of 0.016667 with std. dev. of 0.000000.
Total running time of the script: 7.52 seconds

Выход + синхронизация с dask (с использованием n_workers=1 и threads_per_worker=8) 2

Nested CV Accuracy SVM: 20.416667 (+/- 0.000000 )
Test set accuracy: 18.33 %
Best Estimator: 
SVC(C=1, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf',
  max_iter=-1, probability=True, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

Best Parameters: 
{'C': 1, 'kernel': 'rbf'}

Best CV Score: 
0.23333333333333334

Average difference of 0.029167 with std. dev. of 0.000000.
Total running time of the script: 7.06 seconds

1 использует sklearn.model_selection.GridSearchCV() и не использует joblib()

2 использует dask_ml.model_selection.GridSearchCV() для замены sklearn.model_selection.GridSearchCV() и использует joblib()

Примечания о коде и выводе в этом ответе

  • Я заметил в вашем вопросе, у вас был порядок sklearn.model_selection.GridSearchCV() и cross_val_score в обратном порядке, по сравнению с примером в документации
    • не уверен, что это повлияет на ваш вопрос слишком сильно, но подумал, что я упомяну это
  • У меня нет опыта с вложенной перекрестной проверкой, поэтому я не могу комментировать, является ли Client(..., n_workers=n, threads_per_worker=m) с n>1 и / или m=4 or m=8 приемлемым / неправильным

Общие комментарии по использованию dask_ml (насколько я понимаю)

  • Случай 1 : если данные обучения достаточно малы, чтобы поместиться в память на одной машине, , но набор данных тестирования не помещается в память, вы можете использовать оболочку ParallelPostFit
    • читать данные тестирования параллельно в кластер
    • делает прогнозы при параллельном тестировании данных, используя всех работников в кластере
    • IIUC, этот случай не относится к вашему вопросу
  • Случай 2 : если вы хотите использовать joblib для обучения большой модели scikit-learn в кластере (но данные обучения / тестирования помещаются в память) - то есть распределенный scikit-learn - тогда вы можно использовать кластер для обучения, а код скелета (согласно документам dask_ml) показан ниже
    • IIUC это дело
      • имеет отношение к вашему вопросу
      • подход, который я использовал в этом ответе

Сведения о системе (используется для выполнения кода)

dask==1.2.0
dask-ml==0.12.0
numpy==1.16.2+mkl
pandas==0.24.0
scikit-learn==0.20.3
sklearn==0.0
OS==Windows 8 (64-bit)
Python version (import platform; print(platform.python_version()))==3.7.2
1 голос
/ 27 апреля 2019

В вашей ситуации легко победить, и это .... начать использовать параллельную обработку :). dask поможет вам, если у вас есть кластер (он будет работать на одной машине, но улучшение по сравнению с планированием по умолчанию в sklearn незначительно), но если вы планируете запустить его на одной машине (но иметь несколько ядер / потоков и «достаточно» памяти), тогда вы можете запустить вложенное резюме параллельно. Единственная хитрость в том, что sklearn не позволит вам запустить внешний цикл CV в нескольких процессах. Однако позволит вам запустить внутренний цикл в несколько потоков .

На данный момент у вас есть n_jobs=None во внешнем цикле CV (это значение по умолчанию в cross_val_score), что означает n_jobs=1, и это единственная опция, которую вы можете использовать с sklearn во вложенном резюме.

Тем не менее, вы можете добиться легкого усиления, установив n_jobs=some_reasonable_number во всех GridSearchCV, которые вы используете. some_reasonable_number не обязательно должно быть -1 (но это хорошая отправная точка). Некоторые алгоритмы либо плато на n_jobs=n_cores вместо n_threads (например, xgboost), либо уже имеют встроенную многопроцессорность (например, RandomForestClassifier), и могут возникнуть конфликты, если вы создаете слишком много процессов .

...