在多核机器上的sklearn.naive_bayes.MultinomialNB上执行网格搜索不会使用所有可用的CPU资源

时间:2014-10-26 03:20:41

标签: python linux scikit-learn

我目前正在尝试使用Python和Scikit-learn构建一些文本分类工具。

我的文字不是英文的,因此,不受通常的干分解或其他基于英语的维度减少的处理。

因此,TfIdf矩阵变得非常大(150,000x150,000)它可以使用常规PC进行处理,但对它们运行网格搜索会太多,所以我获得了Amazon Web Service的帮助来运行网格搜索。 (我的参数设置也很大)

这是我的代码:

 # coding: utf-8  
    import os, json, codecs, nltk  
    import numpy as np  
    from sklearn.feature_extraction.text import TfidfVectorizer,  CountVectorizer,TfidfTransformer  
    from sklearn.grid_search import GridSearchCV  
    from time import time  
    from sklearn.pipeline import Pipeline  
    from sklearn.naive_bayes import MultinomialNB  
    print("Importing dataset...")  
    with open('y_data.json','r') as fp:  
        y = json.load(fp)  
    with open('dataset.json','r') as fp:  
        dataset = json.load(fp)  
    print("Importing stop words...")  
    with codecs.open('stopword.txt','r','utf-8') as fp:  
    stopword = []  
    for w in fp:  
        stopword.append(w.strip())  
    light_st = set(stopword)  
    with codecs.open('st_data.txt','r','cp874') as fp:  
    for w in fp:  
        stopword.append(w.strip())  
    heavy_st = set(stopword)  
    def pre_process_1(text):  
        return text.replace("|"," ")  
    def tokenize_1(text):  
        return text.split()  
    pipeline = Pipeline([('vec', CountVectorizer(encoding='cp874', preprocessor=pre_process_1, tokenizer=tokenize_1, stop_words=heavy_st, token_pattern=None)),('tfidf', TfidfTransformer()), ('clf',       MultinomialNB())])
    parameters = {  
    'vec__max_df': (0.5, 0.625, 0.75, 0.875, 1.0),  
    'vec__max_features': (None, 5000, 10000, 20000),  
    'vec__min_df': (1, 5, 10, 20, 50),  
    'tfidf__use_idf': (True, False),  
    'tfidf__sublinear_tf': (True, False),  
    'vec__binary': (True, False),  
    'tfidf__norm': ('l1', 'l2'),  
    'clf__alpha': (1, 0.1, 0.01, 0.001, 0.0001, 0.00001)  
    }  
    if __name__ == "__main__":  
        grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=2)  
        t0 = time()  
        grid_search.fit(dataset, y)  
        print("done in {0}s".format(time() - t0))  
        print("Best score: {0}".format(grid_search.best_score_))  
        print("Best parameters set:")  
        best_parameters = grid_search.best_estimator_.get_params()  
        for param_name in sorted(list(parameters.keys())):  
            print("\t{0}: {1}".format(param_name, best_parameters[param_name]))

以下是我的软件环境的详细信息:

  • Python3.4.2

  • scikit-learn 0.15.2(随Pip一起安装)

  • Ubuntu Server14.04 LTS,64位(使用HVM)

  • 尝试使用ec2 r3.8xlarge实例

首先,我使用一个更小的实例(r3.2xlarge; 8个核心)运行我的模型,但从计算中发现它需要相当长的时间(2天)。所以,我决定扩大我的机器并使用最大的实例(我使用r3,因为我的脚本非常耗费内存);然而,它并没有像我想象的那样快速地处理。

当我试图监控CPU负载时(观察-n 5正常运行时间)......我发现即使我让它运行一段时间,平均CPU负载也不会超过9。 (据我所知,32个核心机器在完全利用其所有核心时,应该大约为32个。)

我尝试过更改

  

n_job

具有相同结果的各种数字(8,32,128)的参数。 (但是我认为脚本会尝试按照指示运行多个作业,因为当我终止进程时,我会看到类似......" Process ForkPoolWorker-30:"并且他们的回溯超过了屏幕)

使用ps x -C python3.4命令进一步检查产生的只有8个python进程正在运行。我推断这可能是python或操作系统的一些限制(我使用t2.micro实例构建我的AMI,它没有很多内核)所以,我决定重做我从头开始重建环境的工作,包括使用c3.4xlarge编译Python,并将操作系统更改为Amazon Linux(我认为是Fedora的一个分支),以便更好地兼容硬件。

但是,我的脚本仍然没有超过8个核心。 最后,使用Scikit-learn网站上的演示文本分类代码:http://scikit-learn.org/stable/auto_examples/grid_search_text_feature_extraction.html (它使用SGDClassifier而不是MultinomialNB)它可以与所有32个核心完美运行!

那么......也许,与网格搜索算法和朴素贝叶斯分类器有关?

我正在考虑提交一个错误,但首先想知道这是Naive Bayes的预期行为,还是我的代码出错?

更新

我无法找到一种方法来测试内存带宽是否直接成为罪魁祸首。但我尝试以各种方式计算并行代码和CPU使用情况,以找出确实存在瓶颈的地方。

实验1:仅执行矢量化和转换。

使用我的真实数据作为输入(150,000个文本文档;每个包含大约130个单词)
参数空间约为400 多线程由Joblib(Scikit-learn使用的相同模块)完成。我得到了:
使用8个线程:在841.017783164978 s中完成并使用24.636999999999993%的CPU。
使用16个线程:在842.9525656700134 s中完成并使用24.700749999999985%的CPU。
使用全部32个线程:在857.024197101593 s中完成并使用24.242250000000013%的CPU。

结果清楚地表明,随着处理能力的增加,矢量化过程无法扩展。

实验2:这次我只在预矢量化数据上执行MultinomialNB。

像以前一样使用大约400的参数空间,我得到了:
使用8个线程:在2102.0565922260284 s中完成并使用25.486000000000054%的CPU。
使用16个线程:在1385.6887295246124 s中完成并使用49.83674999999993%的CPU。
使用全部32个线程:在1319.416403055191中完成并使用89.90074999999997%的CPU。

从8个线程到16个线程的转换显示出巨大的改进。但是,随着线程数增加到32,完成总时间只会略微缩短,而CPU使用率会大幅增加。这一点我不太明白。

实验3:我将两个过程结合在一起。

使用8个线程:在3385.3253166675568 s中完成并使用25.68999999999995%的CPU。
使用16个线程:在2066.499200105667 s中完成并使用49.359249999999996%的CPU。
使用全部32个线程:在2018.8800330162048 s中完成并使用54.55375000000004%的CPU。

我从我自己的并行代码和GridsearchCV的代码之间存在一些差异,但这可能是因为我在代码中完成的简化(我不是像在Gridsearch中那样进行交叉验证或完整参数迭代)

结论

从我的测试中,我得出结论。 (如果我错了,请纠正我)

  1. 矢量化阶段使用内存更加密集;因此,最有可能使带宽饱和。这可以从完成时间和CPU利用率来观察它遇到某种瓶颈并且没有扩展。但是,这是一个相对快速的过程。 (我消除了IO限制,因为所有数据都存储在RAM中,并且在此期间内存使用率约为30%)
  2. MultinomialNB使用的内存不如矢量化器强;大多数计算似乎是在核心内处理的。因此,它可以比矢量化器(8> 16)更好地扩展,但在此之后,它也会碰到某种瓶颈,并且MultinomialNB比矢量化器花费更多的时间。
  3. 当将两个进程组合​​在一起时,完成时间显示与MultinomialNB相同的趋势,因为在我看来,内存带宽可能是向量化阶段的瓶颈,但与MultinomialNB相比,该阶段相对较短。因此,如果并发任务的数量很少,则可以同时将这两个阶段一起进行并且不会使带宽饱和,但是当进程数足够高时,将有足够数量的并发进程执行向量化以使其饱和带宽;从而迫使操作系统减少运行过程。 (解释我之前发现的8-9运行python进程)
  4. 我不太确定,但我认为SGDClassifier可以使用100%CPU的原因是因为SGDClassifier的内核处理时间比MultinomialNB长得多。因此,在每次迭代中,大部分时间用于计算SGDClassifier in-core而不是进行矢量化,而SGDClassifier需要很长时间才能计算,这可以减少许多工作人员同时进入矢量化阶段的可能性(因为每个矢量化)任务相对较短但内存密集)
  5. 我认为我现在最好的选择是进行集群计算。 :)

1 个答案:

答案 0 :(得分:4)

看起来你的工作都受内存限制。

Naive Bayes是一个非常简单的模型,其训练算法由单个(稀疏)矩阵乘法和几个和组成。类似地,tf-idf计算起来非常简单:它对输入求和,计算几个日志,并存储结果。

事实上,NB是所以简单,这个程序的瓶颈几乎肯定在CountVectorizer,它会多次转换内存中的数据结构,直到它的所有术语计数都被填入进入正确的矩阵格式。如果你并行地做很多事情,你可能会遇到内存带宽瓶颈。

(这是所有受过教育的猜测,但它是基于我参与scikit-learn开发。我是MultinomialNB的作者之一,也是许多遭遇黑客攻击的人之一在CountVectorizer加快速度。)