【问题标题】:Efficiently count word frequencies in python有效计算python中的词频
【发布时间】:2016-06-21 19:35:55
【问题描述】:

我想计算文本文件中所有单词的频率。

>>> countInFile('test.txt')

如果目标文本文件是这样的,应该返回{'aaa':1, 'bbb': 2, 'ccc':1}

# test.txt
aaa bbb ccc
bbb

我在some posts 之后用纯python 实现了它。但是,我发现纯 python 方式由于文件大小(> 1GB)而不足。

我认为借用sklearn的力量是一个候选。

如果你让 CountVectorizer 计算每一行的频率,我猜你会通过对每一列求和来获得词频。但是,这听起来有点间接。

用python计算文件中单词的最有效和最直接的方法是什么?

更新

我的(非常慢的)代码在这里:

from collections import Counter

def get_term_frequency_in_file(source_file_path):
    wordcount = {}
    with open(source_file_path) as f:
        for line in f:
            line = line.lower().translate(None, string.punctuation)
            this_wordcount = Counter(line.split())
            wordcount = add_merge_two_dict(wordcount, this_wordcount)
    return wordcount

def add_merge_two_dict(x, y):
    return { k: x.get(k, 0) + y.get(k, 0) for k in set(x) | set(y) }

【问题讨论】:

  • 在python中拆分单词将不得不为list分配内存并创建很多str对象,还要创建字典,python哈希不是很快。为了获得最佳性能,您可以编写 C 扩展,在不复制内存的情况下查找单词边界,然后使用最快的哈希对其进行计数,完成后,创建 python dict。
  • 您是在匹配某些单词,还是在尝试计算每个唯一的“单词”。您希望在 1 GB 大小的文件中找到多少个唯一词?另外,这些线路平均有多长?
  • 您可能无法通过切换到 C 或某些模块来改善 太多的执行时间(对 950M 数据集的基本 Python 测试需要我 25 秒,这不是这么慢)。问题在于它将所有单词都存储在内存中(因此您至少需要 1G 的空闲内存)。如果您的数据限制为 1G,那可能没问题。使用 SQLite/MySQL 之类的东西可以解决内存问题,但需要磁盘访问速度要慢得多;那么你在寻找什么“效率”?内存效率? CPU 高效?磁盘效率?省时吗?

标签: python nlp scikit-learn word-count frequency-distribution


【解决方案1】:

跳过 CountVectorizer 和 scikit-learn。

文件可能太大而无法加载到内存中,但我怀疑 python 字典太大了。对您来说最简单的选择可能是将大文件拆分为 10-20 个较小的文件,然后扩展您的代码以循环遍历较小的文件。

【讨论】:

    【解决方案2】:

    这就够了。

    def countinfile(filename):
        d = {}
        with open(filename, "r") as fin:
            for line in fin:
                words = line.strip().split()
                for word in words:
                    try:
                        d[word] += 1
                    except KeyError:
                        d[word] = 1
        return d
    

    【讨论】:

    • 仅供参考,当split 没有给出参数时,不需要在split 之前strip(); no arg split 已经忽略了前导和尾随空格。
    【解决方案3】:

    最简洁的方法是使用 Python 提供的工具。

    from future_builtins import map  # Only on Python 2
    
    from collections import Counter
    from itertools import chain
    
    def countInFile(filename):
        with open(filename) as f:
            return Counter(chain.from_iterable(map(str.split, f)))
    

    就是这样。 map(str.split, f) 正在制作一个生成器,它从每行返回 lists 的单词。包装 chain.from_iterable 将其转换为一次生成一个单词的单个生成器。 Counter 接受一个可迭代的输入并计算其中的所有唯一值。最后,你return 一个类似dict 的对象(一个Counter),它存储所有唯一单词及其计数,并且在创建期间,您一次只存储一行数据和总计数,而不是一次完整的文件。

    理论上,在 Python 2.7 和 3.1 上,您自己循环链接结果并使用 dictcollections.defaultdict(int) 进行计数可能会稍微好一些(因为 Counter 是在 Python 中实现的,这可能会使其变慢在某些情况下),但让Counter 完成这项工作更简单,更自我记录(我的意思是,整个目标是计数,所以使用Counter)。除此之外,在 CPython(参考解释器)3.2 及更高版本上,Counter 有一个 C 级加速器,用于计算可迭代输入,其运行速度比您用纯 Python 编写的任何东西都要快。

    更新:您似乎希望去除标点符号和不区分大小写,所以这是我早期代码的变体:

    from string import punctuation
    
    def countInFile(filename):
        with open(filename) as f:
            linewords = (line.translate(None, punctuation).lower().split() for line in f)
            return Counter(chain.from_iterable(linewords))
    

    您的代码运行速度要慢得多,因为它正在创建和销毁许多小的 Counterset 对象,而不是 .update-每行一次单个 Counter(虽然比我给出的稍慢在更新的代码块中,在缩放因子上至少在算法上相似)。

    【讨论】:

    • 我发现(在 C-Python 中)defaultdict(int) 在 Python 2 中比 Counter 快,但在 Python 3 中则相反。顺便说一句,这是一个很好的答案。在这个网站上投票发生了什么?
    • 谢谢@ShadowRanger。您的代码完美运行!但是,请在更新的问题中查看我之前的代码。我也用过Counter。我的代码有什么问题?
    • @rkjt50r983:嗯,除此之外,创建许多Counters 并将它们组合起来比创建一个成本要高得多;如果您不喜欢我提供的过于简洁的代码,我仍然建议创建一个 Counter 并在其上调用 .update 并使用每行中的单词,这会增加单个 Counter 的数量,而不是在每一步创建全新的Counters 并合并dicts。
    • @mattsap: str.split 没有参数会在空格运行时拆分,并且在字符串以空格开头或结尾时不会在末尾返回空组,使其有效地成为 strip 后跟split 在空白处运行。另外,旁注,Windows 上的排序是\r\n,而不是\n\r,尽管str.rstrip 对顺序不敏感,因此当您尝试从输入行中删除换行符(但没有其他空格)时,任一顺序都有效.
    • @mhawke:晚更新:我去检查了;从 Python 3.2 开始,Counter 有一个C-accelerated helper function for updating itself by counting an input iterable,它几乎肯定是相对于defaultdict(int) 的加速的原因。我没有注意到它,因为在我写这篇文章时,我正在查看 2.7 代码(OP 的代码使用 str.translate 的 2.x 版本)。很高兴知道您不再为了Counter 的方便而牺牲任何速度。
    【解决方案4】:

    一种高效且准确的记忆方式是利用

    • scikit 中的 CountVectorizer(用于 ngram 提取)
    • word_tokenize 的 NLTK
    • numpy矩阵求和收集计数
    • collections.Counter 用于收集计数和词汇

    一个例子:

    import urllib.request
    from collections import Counter
    
    import numpy as np 
    
    from nltk import word_tokenize
    from sklearn.feature_extraction.text import CountVectorizer
    
    # Our sample textfile.
    url = 'https://raw.githubusercontent.com/Simdiva/DSL-Task/master/data/DSLCC-v2.0/test/test.txt'
    response = urllib.request.urlopen(url)
    data = response.read().decode('utf8')
    
    
    # Note that `ngram_range=(1, 1)` means we want to extract Unigrams, i.e. tokens.
    ngram_vectorizer = CountVectorizer(analyzer='word', tokenizer=word_tokenize, ngram_range=(1, 1), min_df=1)
    # X matrix where the row represents sentences and column is our one-hot vector for each token in our vocabulary
    X = ngram_vectorizer.fit_transform(data.split('\n'))
    
    # Vocabulary
    vocab = list(ngram_vectorizer.get_feature_names())
    
    # Column-wise sum of the X matrix.
    # It's some crazy numpy syntax that looks horribly unpythonic
    # For details, see http://stackoverflow.com/questions/3337301/numpy-matrix-to-array
    # and http://stackoverflow.com/questions/13567345/how-to-calculate-the-sum-of-all-columns-of-a-2d-numpy-array-efficiently
    counts = X.sum(axis=0).A1
    
    freq_distribution = Counter(dict(zip(vocab, counts)))
    print (freq_distribution.most_common(10))
    

    [出]:

    [(',', 32000),
     ('.', 17783),
     ('de', 11225),
     ('a', 7197),
     ('que', 5710),
     ('la', 4732),
     ('je', 4304),
     ('se', 4013),
     ('на', 3978),
     ('na', 3834)]
    

    基本上,您也可以这样做:

    from collections import Counter
    import numpy as np 
    from nltk import word_tokenize
    from sklearn.feature_extraction.text import CountVectorizer
    
    def freq_dist(data):
        """
        :param data: A string with sentences separated by '\n'
        :type data: str
        """
        ngram_vectorizer = CountVectorizer(analyzer='word', tokenizer=word_tokenize, ngram_range=(1, 1), min_df=1)
        X = ngram_vectorizer.fit_transform(data.split('\n'))
        vocab = list(ngram_vectorizer.get_feature_names())
        counts = X.sum(axis=0).A1
        return Counter(dict(zip(vocab, counts)))
    

    让我们timeit:

    import time
    
    start = time.time()
    word_distribution = freq_dist(data)
    print (time.time() - start)
    

    [出]:

    5.257147789001465
    

    请注意,CountVectorizer 也可以使用文件而不是字符串,并且这里不需要将整个文件读入内存。在代码中:

    import io
    from collections import Counter
    
    import numpy as np
    from sklearn.feature_extraction.text import CountVectorizer
    
    infile = '/path/to/input.txt'
    
    ngram_vectorizer = CountVectorizer(analyzer='word', ngram_range=(1, 1), min_df=1)
    
    with io.open(infile, 'r', encoding='utf8') as fin:
        X = ngram_vectorizer.fit_transform(fin)
        vocab = ngram_vectorizer.get_feature_names()
        counts = X.sum(axis=0).A1
        freq_distribution = Counter(dict(zip(vocab, counts)))
        print (freq_distribution.most_common(10))
    

    【讨论】:

      【解决方案5】:

      我没有解码从 url 读取的整个字节,而是处理二进制数据。因为bytes.translate 期望它的第二个参数是一个字节串,所以我对punctuation 进行了utf-8 编码。删除标点符号后,我对字节字符串进行 utf-8 解码。

      函数freq_dist 需要一个可迭代的。这就是我通过data.splitlines() 的原因。

      from urllib2 import urlopen
      from collections import Counter
      from string import punctuation
      from time import time
      import sys
      from pprint import pprint
      
      url = 'https://raw.githubusercontent.com/Simdiva/DSL-Task/master/data/DSLCC-v2.0/test/test.txt'
      
      data = urlopen(url).read()
      
      def freq_dist(data):
          """
          :param data: file-like object opened in binary mode or
                       sequence of byte strings separated by '\n'
          :type data: an iterable sequence
          """
          #For readability   
          #return Counter(word for line in data
          #    for word in line.translate(
          #    None,bytes(punctuation.encode('utf-8'))).decode('utf-8').split())
      
          punc = punctuation.encode('utf-8')
          words = (word for line in data for word in line.translate(None, punc).decode('utf-8').split())
          return Counter(words)
      
      
      start = time()
      word_dist = freq_dist(data.splitlines())
      print('elapsed: {}'.format(time() - start))
      pprint(word_dist.most_common(10))
      

      输出;

      elapsed: 0.806480884552
      
      [(u'de', 11106),
       (u'a', 6742),
       (u'que', 5701),
       (u'la', 4319),
       (u'je', 4260),
       (u'se', 3938),
       (u'\u043d\u0430', 3929),
       (u'na', 3623),
       (u'da', 3534),
       (u'i', 3487)]
      

      似乎dictCounter 对象更有效。

      def freq_dist(data):
          """
          :param data: A string with sentences separated by '\n'
          :type data: str
          """
          d = {}
          punc = punctuation.encode('utf-8')
          words = (word for line in data for word in line.translate(None, punc).decode('utf-8').split())
          for word in words:
              d[word] = d.get(word, 0) + 1
          return d
      
      start = time()
      word_dist = freq_dist(data.splitlines())
      print('elapsed: {}'.format(time() - start))
      pprint(sorted(word_dist.items(), key=lambda x: (x[1], x[0]), reverse=True)[:10])
      

      输出;

      elapsed: 0.642680168152
      
      [(u'de', 11106),
       (u'a', 6742),
       (u'que', 5701),
       (u'la', 4319),
       (u'je', 4260),
       (u'se', 3938),
       (u'\u043d\u0430', 3929),
       (u'na', 3623),
       (u'da', 3534),
       (u'i', 3487)]
      

      为了在打开大文件时提高内存效率,您必须只传递打开的 url。但时间也会包括文件下载时间。

      data = urlopen(url)
      word_dist = freq_dist(data)
      

      【讨论】:

        【解决方案6】:

        这里有一些基准。它看起来很奇怪,但最粗略的代码会胜出。

        [代码]:

        from collections import Counter, defaultdict
        import io, time
        
        import numpy as np
        from sklearn.feature_extraction.text import CountVectorizer
        
        infile = '/path/to/file'
        
        def extract_dictionary_sklearn(file_path):
            with io.open(file_path, 'r', encoding='utf8') as fin:
                ngram_vectorizer = CountVectorizer(analyzer='word')
                X = ngram_vectorizer.fit_transform(fin)
                vocab = ngram_vectorizer.get_feature_names()
                counts = X.sum(axis=0).A1
            return Counter(dict(zip(vocab, counts)))
        
        def extract_dictionary_native(file_path):
            dictionary = Counter()
            with io.open(file_path, 'r', encoding='utf8') as fin:
                for line in fin:
                    dictionary.update(line.split())
            return dictionary
        
        def extract_dictionary_paddle(file_path):
            dictionary = defaultdict(int)
            with io.open(file_path, 'r', encoding='utf8') as fin:
                for line in fin:
                    for words in line.split():
                        dictionary[word] +=1
            return dictionary
        
        start = time.time()
        extract_dictionary_sklearn(infile)
        print time.time() - start
        
        start = time.time()
        extract_dictionary_native(infile)
        print time.time() - start
        
        start = time.time()
        extract_dictionary_paddle(infile)
        print time.time() - start
        

        [出]:

        38.306814909
        24.8241138458
        12.1182529926
        

        上述基准测试中使用的数据大小 (154MB):

        $ wc -c /path/to/file
        161680851
        
        $ wc -l /path/to/file
        2176141
        

        注意事项:

        • sklearn 版本中,创建矢量化器 + numpy 操作和转换为 Counter 对象会产生开销
        • 然后原生Counter更新版本,看来Counter.update()是一个昂贵的操作

        【讨论】:

          【解决方案7】:

          你可以试试 sklearn

          from sklearn.feature_extraction.text import CountVectorizer
              vectorizer = CountVectorizer()
          
              data=['i am student','the student suffers a lot']
              transformed_data =vectorizer.fit_transform(data)
              vocab= {a: b for a, b in zip(vectorizer.get_feature_names(), np.ravel(transformed_data.sum(axis=0)))}
              print (vocab)
          

          【讨论】:

            【解决方案8】:

            结合其他人的观点和我自己的一些观点:) 这是我为你准备的东西

            from collections import Counter
            from nltk.tokenize import RegexpTokenizer
            from nltk.corpus import stopwords
            
            text='''Note that if you use RegexpTokenizer option, you lose 
            natural language features special to word_tokenize 
            like splitting apart contractions. You can naively 
            split on the regex \w+ without any need for the NLTK.
            '''
            
            # tokenize
            raw = ' '.join(word_tokenize(text.lower()))
            
            tokenizer = RegexpTokenizer(r'[A-Za-z]{2,}')
            words = tokenizer.tokenize(raw)
            
            # remove stopwords
            stop_words = set(stopwords.words('english'))
            words = [word for word in words if word not in stop_words]
            
            # count word frequency, sort and return just 20
            counter = Counter()
            counter.update(words)
            most_common = counter.most_common(20)
            most_common
            

            输出

            (全部)

            [('注', 1), ('使用', 1), ('regexptokenizer', 1), ('选项1), ('输', 1), ('自然', 1), ('语言', 1), ('特征', 1), ('特殊', 1), ('字', 1), ('tokenize', 1), ('喜欢', 1), ('分裂', 1), ('分开', 1), (“宫缩”,1), ('天真', 1), ('分裂', 1), ('正则表达式', 1), ('没有', 1), ('需要', 1)]

            在效率方面可以做得比这更好,但如果你不是太担心的话,这个代码是最好的。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-01-07
              • 2019-11-22
              • 1970-01-01
              • 2021-02-28
              • 2011-02-22
              相关资源
              最近更新 更多