【问题标题】:How to speed up nested cross validation in python?如何加快python中的嵌套交叉验证?
【发布时间】:2019-04-30 23:07:58
【问题描述】:

根据我的发现,还有 1 个类似的问题 (Speed-up nested cross-validation) 但是在尝试了本网站和微软也建议的几个修复后,安装 MPI 对我不起作用,所以我希望有另一个包或回答这个问题。

我希望比较多种算法和网格搜索范围广泛的参数(可能参数太多?),除了 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 个参数,但是这仍然需要几个小时才能完成,所以虽然减少数字会有所帮助,如果有任何关于效率的建议我将不胜感激。

【问题讨论】:

  • 有几种方法可以让您的代码运行得更快:1) 在所有GridSearch 函数中设置n_jobs=2n_jobs=3 甚至n_jobs=-1(取决于可用的CPU 内核数量) ; 2)改用RandomizedSearchCV(它可以帮助您更快地获得最佳解决方案); 3)减少传递给GridSearch的网格点数; 4) 结合所有这些方法。
  • 感谢您的回复,不幸的是,对于嵌套 CV,除了 n_jobs=1(根据 scikit-learn 的网站:scikit-learn.org/stable/tutorial/statistical_inference/…)之外,我没有其他任何东西,但是 RandomizedSearchCV 听起来可能更理想对我来说,所以我会调查一下,谢谢!

标签: python parallel-processing scikit-learn dask cross-validation


【解决方案1】:

Dask-ML 具有可扩展的实现 GridSearchCVRandomSearchCV,我相信它们可以替代 Scikit-Learn。它们是与 Scikit-Learn 开发人员一起开发的。

它们可以更快有两个原因:

【讨论】:

    【解决方案2】:

    两件事:

    1. 尝试使用 HyperOpt 代替 GridSearch - 这是一个用于串行和并行优化的 Python 库。

    2. 我会使用UMAPPCA 来降低维度。可能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)。这应该会对性能产生重大影响。

    【讨论】:

    • 您好,谢谢您,我正在尝试您的建议,到目前为止似乎进展顺利(尽管卡在 hyperopt 上无法识别 n_jobs=1 但我会继续尝试),因为降维(我不太了解)我需要担心信息丢失吗?当我查看我的特征如何相关以及我的模型如何在没有网格搜索的情况下使用这些特征时,只有 3-4 个没有按重要性或相关性加权的特征,所以我担心 UMAP 或 PCA 会比这 3-4 个减少更多。跨度>
    • "arguably preserves more of the global structure with superior run time performance" 取自 arxiv.org/abs/1802.03426。如果它减少了超过这 3-4 个特征,这不一定是坏事,这意味着你的数据中有很多嘈杂的特征。如果它们很重要/不是噪音,UMAP 肯定会选择它们。也检查此评论github.com/lmcinnes/umap/issues/122#issuecomment-412530409
    【解决方案3】:

    在您的情况下很容易获胜,那就是....开始使用并行处理:)。 dask 如果你有一个集群会帮助你(它可以在单台机器上工作,但是与sklearn 中的默认调度相比改进并不显着),但是如果你打算在单台机器上运行它(但是有几个核心/线程和“足够”的内存),那么你可以并行运行嵌套的 CV。唯一的窍门是sklearn 不允许您在多个进程中运行outer CV 循环。但是,它将允许您在多个线程中运行内部循环

    目前,您在外部 CV 循环中有 n_jobs=None(这是 cross_val_score 中的默认值),这意味着 n_jobs=1 并且这是您可以在嵌套 CV 中与 sklearn 一起使用的唯一选项。

    但是,您可以通过在您使用的所有GridSearchCV 中设置n_jobs=some_reasonable_number 来轻松获得收益。 some_reasonable_number 不一定是-1(但这是一个很好的起点)。一些算法要么在n_jobs=n_cores 而不是n_threads(例如xgboost)上停滞不前,要么已经有内置的多处理(例如RandomForestClassifier),如果你产生太多进程可能会发生冲突.

    【讨论】:

    • 为什么 n_jobs=1 是嵌套简历中的唯一选项?
    • 不幸的是,我不知道“为什么”这个问题的答案,因为我没有编写代码。但我想逻辑可能是用户仍然在内部层具有并行化能力,并且在一台机器上(可以运行 sklearn)通常没有足够的资源来产生超过 4- 8 个线程高效地并行。
    【解决方案4】:

    IIUC,您正在尝试从 sklearn 文档中并行化 this example。如果是这种情况,那么这是一种可能的解决方法

    为什么 dask 不工作

    关于这个问题的任何建设性指导或进一步的知识

    一般进口

    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 question 中样本数据的稍长版本
    #### 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)
    

    使用dask_ml 实现的GridSearchCV(最初由@MRocklin here 建议)- 请参阅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)))
    

    初始化一个额外的空白列表以保存非嵌套的 CV 结果

    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 docs example 执行嵌套简历

    • 先执行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 的输出(带有脚本执行时间)

    输出+时序不带dask1

    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=1threads_per_worker=42

    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=4threads_per_worker=42

    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=1threads_per_worker=82

    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使用的一般cmets(据我了解)

    • Case 1:如果训练数据小到可以放入单机内存,但是测试数据集放不下内存,可以使用包装器ParallelPostFit
      • 将测试数据并行读取到集群中
      • 使用集群上的所有工作线程并行预测测试数据
      • IIUC,此案例与您的问题无关
    • Case 2:如果您想使用 joblib 在集群上训练大型 scikit-learn 模型(但训练/测试数据适合内存) - 又名分布式 scikit-learn - 那么您可以使用集群进行训练和骨架代码(根据dask_ml docs)如下所示
      • 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
    

    【讨论】:

    • 非常感谢您的详细回复。不幸的是,像您一样尝试复制 joblib 代码似乎让我的模型永远运行而没有输出。但是,单独使用 dask gridsearch 确实有效,虽然对于除 keras 之外的所有模型,但这对我来说仍然是一个很大的改进。
    • @DN1,很高兴它有点帮助。奇怪的是您在使用 joblib 时遇到了问题 - 我展示的实现直接针对您在 dask_ml docs 中的问题的适当案例(我的案例 2)。我唯一能想到的就是在另一台机器上或使用较小的数据集/最简单的模型上尝试一下,看看它是否完成了,或者它是否提供了有关执行缓慢的其他线索。
    猜你喜欢
    • 2020-03-03
    • 1970-01-01
    • 2021-02-10
    • 2022-12-04
    • 1970-01-01
    • 2020-12-31
    • 2017-06-12
    • 2020-08-12
    • 1970-01-01
    相关资源
    最近更新 更多