scikit Grid Search 和 Python 通常不会释放内存

Posted

技术标签:

【中文标题】scikit Grid Search 和 Python 通常不会释放内存【英文标题】:scikit's GridSearch and Python in general are not freeing memory 【发布时间】:2015-02-15 00:18:32 【问题描述】:

我做了一些奇怪的观察,我的 GridSearch 在几个小时后一直失败,我一开始不知道为什么。随着时间的推移,我监控了内存使用情况,发现它从几 GB(~6 Gb)开始并不断增加,直到达到最大值时节点崩溃。硬件可以占用 128 Gb。 我正在尝试使用随机森林对大量文本文档进行分类。为简单起见——为了弄清楚发生了什么——我回到了朴素贝叶斯。

我使用的版本是

Python 3.4.2 scikit-learn 0.15.2

我在 GitHub 上的 scikit-issue 列表上找到了一些关于这个主题的相关讨论:https://github.com/scikit-learn/scikit-learn/issues/565 和 https://github.com/scikit-learn/scikit-learn/pull/770

听起来它已经成功解决了!

所以,我使用的相关代码是

grid_search = GridSearchCV(pipeline, 
                           parameters, 
                           n_jobs=1, # 
                           cv=5, 
                           scoring='roc_auc',
                           verbose=2,
                           pre_dispatch='2*n_jobs',
                           refit=False)  # tried both True and False

grid_search.fit(X_train, y_train)  
print('Best score: 0'.format(grid_search.best_score_))  
print('Best parameters set:') 

出于好奇,我后来决定通过嵌套的 for 循环以快速而肮脏的方式进行网格搜索

for p1 in parameterset1:
    for p2 in parameterset2:
        ...
            pipeline = Pipeline([
                        ('vec', CountVectorizer(
                                   binary=True,
                                   tokenizer=params_dict[i][0][0],
                                   max_df=params_dict[i][0][1],
                                   max_features=params_dict[i][0][2],
                                   stop_words=params_dict[i][0][3],
                                   ngram_range=params_dict[i][0][4],)),
                         ('tfidf', TfidfTransformer(
                                      norm=params_dict[i][0][5],
                                      use_idf=params_dict[i][0][6],
                                      sublinear_tf=params_dict[i][0][7],)),
                         ('clf', MultinomialNB())])

            scores = cross_validation.cross_val_score(
                                        estimator=pipeline,
                                        X=X_train, 
                                        y=y_train, 
                                        cv=5, 
                                        scoring='roc_auc',
                                        n_jobs=1)

           params_dict[i][1] = '%s,%0.4f,%0.4f' % (params_dict[i][1], scores.mean(), scores.std())
           sys.stdout.write(params_dict[i][1] + '\n')

到目前为止一切顺利。网格搜索运行并将结果写入标准输出。但是,一段时间后,它再次超过了 128 Gb 的内存上限。与 scikit 中的 GridSearch 相同的问题。经过一番实验,我终于发现

gc.collect()
len(gc.get_objects()) # particularly this part!

在 for 循环中解决了问题,内存使用量在约 10 小时的运行时间内始终保持在 6.5 Gb。

最终,我得到了解决上述问题的方法,但是,我很想听听您对可能导致此问题的原因的想法以及您的提示和建议!

【问题讨论】:

这太奇怪了。您能否在 github 上提交一个新问题,包括使用随机生成的数据(甚至是恒定数据,例如 np.ones(shape=(n_samples, n_features), dtype=np.float))重现问题的脚本? 当然,没问题。我将一些导致此问题的代码上传到 github.com/rasbt/bugreport/tree/master/scikit-learn/… 并在此处打开了一个问题:github.com/scikit-learn/scikit-learn/issues/3973。谢谢! 过去我也发现sklearn中的一些东西(通常是随机森林)消耗太多内存。根据问题,我不得不解决它。一种评论是,对于 tfidf/文档问题,GradientBoostingClassifier 可能比 RandomForest 提供更好的结果。另外,我很确定 tfidf 转换器将返回一个稀疏矩阵(待办事项:确保为您的版本确定这一点)......所以您需要更新您的 sklearn,因为 0.15.2 中的 RandomForest 不支持稀疏输入。跨度> 您是如何在GridSearchCV() 方法中使用gc.collect()len(gc.get_objects()) 来解决它的?这种方法不会有正确的循环,因此没有地方放置您提到的 2 行? 【参考方案1】:

0.15.2 中的 RandomForest 不支持稀疏输入。

升级 sklearn 并重试...希望这将允许最终制作的多个副本消耗更少的内存。 (并加快速度)

【讨论】:

感谢您的提示。是的,我使用 toarray() 调用将稀疏数组转换为密集数组,这肯定非常昂贵。然而,这个问题也发生在“便宜”的朴素贝叶斯或逻辑回归分类器 (Sgd) 上。不知何故,垃圾收集出现了问题。我应该用 Python 3.4.3 和 scikit-learn 版本 0.16.1 再试一次 我相信 NB 和 LR 也支持稀疏输入。您可以尝试将 pre_dispatch='2*n_jobs' 更改为 1【参考方案2】:

我看不到您的确切代码,但我现在遇到了类似的问题。 值得一试。 当我们将值从可变数组或类似对象的列表复制到另一个变量以创建原始变量的副本,然后我们使用 append 或类似的方式修改新数组或列表以增加大小时,类似的内存爆炸很容易发生它同时也在背景中增加了原始对象。

所以这是一个指数过程,所以一段时间后我们内存不足。我能够并且也许您可以通过 deepcopy() 传递值的原始对象来避免这种现象。

我遇到了类似的问题,我用类似的过程炸毁了内存,然后我设法保持在 10% 的内存负载。

更新: 现在我看到了带有 pandas DataFrame 的代码的 sn-p。很容易出现这样的 valuecopy 问题。

【讨论】:

我必须再次查看 scikit-learn 代码库,但出于安全原因,我很确定内部(在估算器中)有很多副本。然而,在某些时候它们应该是“垃圾收集和处理”,当我使用gc.collect() len(gc.get_objects()) 时,它神奇地起作用了。我必须在未来一段时间再试一次,看看它是特定于架构还是 Python 版本 我至少会尝试传递 pandas df 值,但无论如何祝你好运。【参考方案3】:

先生,我不熟悉 GridSearch,但我建议当内存和大列表成为问题时编写一个小型自定义生成器。它可以重复用于您的所有项目,只需使用一个包含任何列表的项目。如果在此处的较低解决方案之外实施,请首先阅读本文,这是我找到的最佳生成器文章。我都打了一遍又一遍,看完有什么问题我也可以试试

https://www.jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/

不需要: for p1 in parameterset1:

试试

 def listerator(this_list):
    i = 0
    while True:
       yield this_list[i]
       i += 1

'yield' 词(在声明中的任何地方)使它成为生成器,而不是常规函数。这贯穿并说 i 等于 0,而 True 我必须做一些事情,他们希望我产生 this_list[0],如果你再次需要我,我会在 i += 1 等你。下一次调用它时,它拿起并执行i += 1,并注意到它仍在一个while循环中并给出this_list [1],并记录它的位置(再次i += 1......它将在那里等待直到再次调用)。请注意,当我将列表输入一次并制作一个生成器(此处为 x)时,它将耗尽您的列表。

In [141]: x = listerator([1,2,3])

In [142]: next(x)
Out[142]: 1

In [143]: next(x)
Out[143]: 2

In [144]: next(x)
Out[144]: 3

In [148]: next(x)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-148-5e4e57af3a97> in <module>()
----> 1 next(x)

<ipython-input-139-ed3d6d61a17c> in listerator(this_list)
      2     i = 0
      3     while True:
----> 4             yield this_list[i]
      5             i += 1
      6 

IndexError: list index out of range

让我们看看我们是否可以在for中使用它:

In [221]: for val in listerator([1,2,3,4]):
   .....:     print val
   .....:     
1
2
3
4
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-221-fa4f59138165> in <module>()
----> 1 for val in listerator([1,2,3,4]):
      2     print val
      3 

<ipython-input-220-263fba1d810b> in listerator(this_list, seed)
      2         i = seed or 0
      3         while True:
----> 4             yield this_list[i]
      5             i += 1

IndexError: list index out of range

不。让我们尝试处理:

def listerator(this_list):
   i = 0
   while True:
       try:
           yield this_list[i]
       except IndexError:
           break
       i += 1

In [223]: for val in listerator([1,2,3,4]):
    print val
   .....:     
1
2
3
4

这行得通。现在它不会盲目地尝试返回一个列表元素,即使它不存在。从你所说的,我几乎可以保证你需要能够播种(从某个地方捡起,或者从某个地方重新开始):

def listerator(this_list, seed=None):
   i = seed or 0
   while True:
       try:
           yield this_list[i]
       except IndexError:
           break
       i += 1

In [150]: l = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

In [151]: x = listerator(l, 8)

In [152]: next(x)
Out[152]: 9

In [153]: next(x)
Out[153]: 10

In [154]: next(x)
Out[154]: 11

i = seed or 0 是一个寻找种子的东西,但是种子默认为 None 所以通常只会从逻辑位置开始,0,列表的开头

你如何在不使用(几乎)任何内存的情况下使用这个野兽?

parameterset1 = [1,2,3,4]
parameterset2 = ['a','b','c','d']

In [224]: for p1 in listerator(parameterset1):
    for p2 in listerator(parameterset2):
        print p1, p2
   .....:         
1 a
1 b
1 c
1 d
2 a
2 b
2 c
2 d
3 a
3 b
3 c
3 d
4 a
4 b
4 c
4 d

看起来很眼熟吧?现在您可以一个一个地处理一万亿个值,挑选重要的值写入磁盘,并且永远不会破坏您的系统。享受吧!

【讨论】:

您好,感谢您的评论,我知道如何使用生成器,但这更多是关于我想讨论的潜在平台特定错误。当然,您可以使用 HashingVectorizer 和 SGD 分类器来处理流数据;但是要正确计算 Tf-idfs,您需要访问整个词汇表 是的,我发现在我完成后可能没有帮助,我得慢慢读这个问题,但很高兴你解决了它

以上是关于scikit Grid Search 和 Python 通常不会释放内存的主要内容,如果未能解决你的问题,请参考以下文章

Scikit-learn 的 GridSearchCV 中的 Grid_scores_ 是啥意思

grid-search建模过程中自动调优

在 scikit learn 中结合网格搜索和交叉验证

什么取代了 scikit 中的 GridSearchCV._grid_scores_?

Scikit-learn GridSearchCV - 为啥我在执行 grid.fit() 时会收到数据类型错误?

Sklearn超参调优手段:网格搜索(Grid Search)和随机搜索(Randomized Search)