【问题标题】:Fastest way to compute entropy in Python在 Python 中计算熵的最快方法
【发布时间】:2013-03-05 05:13:07
【问题描述】:

在我的项目中,我需要多次计算 0-1 向量的熵。这是我的代码:

def entropy(labels):
    """ Computes entropy of 0-1 vector. """
    n_labels = len(labels)

    if n_labels <= 1:
        return 0

    counts = np.bincount(labels)
    probs = counts[np.nonzero(counts)] / n_labels
    n_classes = len(probs)

    if n_classes <= 1:
        return 0
    return - np.sum(probs * np.log(probs)) / np.log(n_classes)

有没有更快的方法?

【问题讨论】:

  • labels 的典型长度是多少?
  • 长度不固定..
  • 了解labels 的典型值有助于进行基准测试。如果labels 太短,纯python 实现实际上可能比使用NumPy 更快。
  • 只是为了确认,这个问题是针对离散(二进制)随机变量的熵?而不是连续 r.v. 的微分熵?
  • 我不知道,你的代码运行速度有多快?这是基于当前格式的意见。

标签: python numpy entropy


【解决方案1】:

聚会迟到了,但我偶然发现了这一点,所有答案似乎都依赖于 Kullback–Leibler 散度,它没有上限,因此不符合我的需求。

这里我有一个从 [0,1] 出发的熵函数的近似值(TODO! 可以改进)。

它计算单个列的偏差。

class Pandas_Dataframe_helper:
    #some other methods here...
    @staticmethod
    def column_biass(df_column):
        df_column_as_list           =   list(df_column)
        N                           =   len(df_column_as_list)
        values,counts               =   np.unique(df_column_as_list, return_counts=True)
        #generate synth list (TODO! what if not even number? Minimum Comun Multiple of(num_different_labels,[x for x in counts]))
        num_different_labels        =   len(values)
        num_items_per_label         =   N // num_different_labels
        synthetic_list              =   []
        for current_value in values:
            synthetic_list.extend([current_value] * num_items_per_label)
        #TODO! aproximacion
        if(len(synthetic_list) != len(df_column_as_list)):
            synthetic_list.extend([current_value] * (len(df_column_as_list) - len(synthetic_list)))
        #now, extrapolate differences between sorted-input-list and synsthetic_list
        df_column_as_list_sorted    =   sorted(df_column_as_list)
        counter_unmatches           =   0
        for i in range(0,N):
            if(df_column_as_list_sorted[i] != synthetic_list[i]):
                counter_unmatches   +=  1
        #upper_bound = g(N,num_different_labels)
        #((K-1)M)-1 K==num_different_labels , M==num theorically perfect distribution's items per label 
        upper_bound                 =   ((num_different_labels-1)*num_items_per_label)-1
        return counter_unmatches/upper_bound
    #---------------------------------------------------------------------------------------------------------------------

完整代码https://github.com/glezo1/pcommonlibs/blob/master/com/glezo/pandas_dataframe_helper/Pandas_Dataframe_Helper.py

【讨论】:

    【解决方案2】:

    这是迄今为止我发现的最快的 Python 实现:

    import numpy as np
    
    def entropy(labels):
        ps = np.bincount(labels) / len(labels)
        return -np.sum([p * np.log2(p) for p in ps if p > 0])
    

    【讨论】:

      【解决方案3】:

      均匀分布的数据(高熵):

      s=range(0,256)
      

      香农熵逐步计算:

      import collections
      import math
      
      # calculate probability for each byte as number of occurrences / array length
      probabilities = [n_x/len(s) for x,n_x in collections.Counter(s).items()]
      # [0.00390625, 0.00390625, 0.00390625, ...]
      
      # calculate per-character entropy fractions
      e_x = [-p_x*math.log(p_x,2) for p_x in probabilities]
      # [0.03125, 0.03125, 0.03125, ...]
      
      # sum fractions to obtain Shannon entropy
      entropy = sum(e_x)
      >>> entropy 
      8.0
      

      单线(假设import collections):

      def H(s): return sum([-p_x*math.log(p_x,2) for p_x in [n_x/len(s) for x,n_x in collections.Counter(s).items()]])
      

      适当的功能:

      import collections
      import math
      
      def H(s):
          probabilities = [n_x/len(s) for x,n_x in collections.Counter(s).items()]
          e_x = [-p_x*math.log(p_x,2) for p_x in probabilities]    
          return sum(e_x)
      

      测试用例 - 取自CyberChef entropy estimator的英文文本:

      >>> H(range(0,256))
      8.0
      >>> H(range(0,64))
      6.0
      >>> H(range(0,128))
      7.0
      >>> H([0,1])
      1.0
      >>> H('Standard English text usually falls somewhere between 3.5 and 5')
      4.228788210509104
      

      【讨论】:

      • 这使得在指定值范围内计算熵的能力非常清楚。我需要将此方法应用于像素周围的 8 连接区域及其灰度值。想知道我是否也可以使用内置方法。
      【解决方案4】:

      BiEntropy 不会是计算熵的最快方法,但它是严格的,并且以明确定义的方式建立在 Shannon Entropy 之上。它已经在包括图像相关应用在内的各个领域进行了测试。 它是在 Github 上用 Python 实现的。

      【讨论】:

        【解决方案5】:

        此方法通过允许分箱扩展了其他解决方案。例如,bin=None(默认)不会对 x 进行分箱,而是为x 的每个元素计算经验概率,而bin=256 在计算经验概率之前将x 分块到 256 个分箱中。

        import numpy as np
        
        def entropy(x, bins=None):
            N   = x.shape[0]
            if bins is None:
                counts = np.bincount(x)
            else:
                counts = np.histogram(x, bins=bins)[0] # 0th idx is counts
            p   = counts[np.nonzero(counts)]/N # avoids log(0)
            H   = -np.dot( p, np.log2(p) )
            return H 
        

        【讨论】:

          【解决方案6】:
          def entropy(base, prob_a, prob_b ):
            import math
            base=2
            x=prob_a
            y=prob_b
            expression =-((x*math.log(x,base)+(y*math.log(y,base))))    
            return [expression]
          

          【讨论】:

          • 用代码回答时,应该写一些解释。
          • 在问题中,参数是标签列表
          【解决方案7】:

          一个不依赖于 numpy 的答案:

          import math
          from collections import Counter
          
          def eta(data, unit='natural'):
              base = {
                  'shannon' : 2.,
                  'natural' : math.exp(1),
                  'hartley' : 10.
              }
          
              if len(data) <= 1:
                  return 0
          
              counts = Counter()
          
              for d in data:
                  counts[d] += 1
          
              ent = 0
          
              probs = [float(c) / len(data) for c in counts.values()]
              for p in probs:
                  if p > 0.:
                      ent -= p * math.log(p, base[unit])
          
              return ent
          

          这将接受您可以扔给它的任何数据类型:

          >>> eta(['mary', 'had', 'a', 'little', 'lamb'])
          1.6094379124341005
          
          >>> eta([c for c in "mary had a little lamb"])
          2.311097886212714
          

          @Jarad 提供的答案也建议了时间。为此:

          repeat_number = 1000000
          e = timeit.repeat(
              stmt='''eta(labels)''', 
              setup='''labels=[1,3,5,2,3,5,3,2,1,3,4,5];from __main__ import eta''', 
              repeat=3, 
              number=repeat_number)
          

          Timeit 结果:(我相信这比最好的 numpy 方法快约 4 倍)

          print('Method: {}, Avg.: {:.6f}'.format("eta", np.array(e).mean()))
          
          Method: eta, Avg.: 10.461799
          

          【讨论】:

          • 为什么你需要 probs = [p for p in probs if p > 0.]?
          • 因为我要在五行之后进行该测试,所以我怀疑我根本不需要它:) 已编辑。
          • 加一表示没有新的依赖项
          • 你能用 counts = Counter(data) 代替循环遍历 data 的字符吗?
          【解决方案8】:

          这是我的方法:

          labels = [0, 0, 1, 1]
          
          from collections import Counter
          from scipy import stats
          
          stats.entropy(list(Counter(labels).values()), base=2)
          

          【讨论】:

          • 这似乎适用于我的图像切片,但我实际上需要切片中像素值从 0 到 255 的概率。
          【解决方案9】:
          from collections import Counter
          from scipy import stats
          
          labels = [0.9, 0.09, 0.1]
          stats.entropy(list(Counter(labels).keys()), base=2)
          

          【讨论】:

          • 虽然这可以回答问题,但仅代码的答案通常被认为是低质量的。提供更多关于为什么会提高此答案质量的描述和上下文。谢谢。
          【解决方案10】:

          将数据作为pd.Seriesscipy.stats,计算给定数量的熵非常简单:

          import pandas as pd
          import scipy.stats
          
          def ent(data):
              """Calculates entropy of the passed `pd.Series`
              """
              p_data = data.value_counts()           # counts occurrence of each value
              entropy = scipy.stats.entropy(p_data)  # get entropy from counts
              return entropy
          

          注意:scipy.stats 将规范化提供的数据,所以这不需要明确地完成,即传递一个计数数组可以正常工作。

          【讨论】:

            【解决方案11】:

            @Sanjeet Gupta 的回答很好,但可以精简。这个问题专门询问“最快”的方式,但我只看到一个答案的时间,所以我将发布使用 scipy 和 numpy 与原始发帖人的 entropy2 答案的比较,稍作改动。

            四种不同的方法:scipy/numpynumpy/mathpandas/numpynumpy

            import numpy as np
            from scipy.stats import entropy
            from math import log, e
            import pandas as pd
            
            import timeit
            
            def entropy1(labels, base=None):
              value,counts = np.unique(labels, return_counts=True)
              return entropy(counts, base=base)
            
            def entropy2(labels, base=None):
              """ Computes entropy of label distribution. """
            
              n_labels = len(labels)
            
              if n_labels <= 1:
                return 0
            
              value,counts = np.unique(labels, return_counts=True)
              probs = counts / n_labels
              n_classes = np.count_nonzero(probs)
            
              if n_classes <= 1:
                return 0
            
              ent = 0.
            
              # Compute entropy
              base = e if base is None else base
              for i in probs:
                ent -= i * log(i, base)
            
              return ent
            
            def entropy3(labels, base=None):
              vc = pd.Series(labels).value_counts(normalize=True, sort=False)
              base = e if base is None else base
              return -(vc * np.log(vc)/np.log(base)).sum()
            
            def entropy4(labels, base=None):
              value,counts = np.unique(labels, return_counts=True)
              norm_counts = counts / counts.sum()
              base = e if base is None else base
              return -(norm_counts * np.log(norm_counts)/np.log(base)).sum()
            

            Timeit 操作:

            repeat_number = 1000000
            
            a = timeit.repeat(stmt='''entropy1(labels)''',
                              setup='''labels=[1,3,5,2,3,5,3,2,1,3,4,5];from __main__ import entropy1''',
                              repeat=3, number=repeat_number)
            
            b = timeit.repeat(stmt='''entropy2(labels)''',
                              setup='''labels=[1,3,5,2,3,5,3,2,1,3,4,5];from __main__ import entropy2''',
                              repeat=3, number=repeat_number)
            
            c = timeit.repeat(stmt='''entropy3(labels)''',
                              setup='''labels=[1,3,5,2,3,5,3,2,1,3,4,5];from __main__ import entropy3''',
                              repeat=3, number=repeat_number)
            
            d = timeit.repeat(stmt='''entropy4(labels)''',
                              setup='''labels=[1,3,5,2,3,5,3,2,1,3,4,5];from __main__ import entropy4''',
                              repeat=3, number=repeat_number)
            

            Timeit 结果:

            # for loop to print out results of timeit
            for approach,timeit_results in zip(['scipy/numpy', 'numpy/math', 'pandas/numpy', 'numpy'], [a,b,c,d]):
              print('Method: {}, Avg.: {:.6f}'.format(approach, np.array(timeit_results).mean()))
            
            Method: scipy/numpy, Avg.: 63.315312
            Method: numpy/math, Avg.: 49.256894
            Method: pandas/numpy, Avg.: 884.644023
            Method: numpy, Avg.: 60.026938
            

            获胜者:numpy/math(熵2)

            还值得注意的是,上面的entropy2 函数可以处理数字和文本数据。例如:entropy2(list('abcdefabacdebcab'))。原始发帖人的答案来自 2013 年,并且有一个特定的用例用于合并整数,但它不适用于文本。

            【讨论】:

            • 您使用的数组如此之小,以至于您的测试基本上没用。您实际上只是在测量各种接口的调用开销。
            • 使用这段代码,我刚刚得到了我的答案的时机(“一个不依赖于 numpy 的答案,或者......”) - 它是 Method: eta, Avg.: 10.461799。正如有人建议的那样,我想知道您是否真的在这里测试调用开销。
            • 最好取 timeit 结果的最小值,而不是平均值。请参阅"note" under the repeat function of the timeit module
            • 矢量化操作可能更适合大型输入。在这种情况下,选项 2 中的“for”循环可能不是一个好的选择。在我看来,选项 1 可能更可取。
            【解决方案12】:

            我最喜欢的熵函数如下:

            def entropy(labels):
                prob_dict = {x:labels.count(x)/len(labels) for x in labels}
                probs = np.array(list(prob_dict.values()))
            
                return - probs.dot(np.log2(probs))
            

            我仍在寻找一种更好的方法来避免 dict -> values -> list -> np.array 转换。如果我找到它会再次评论。

            【讨论】:

            • 不错,使用 collections.Counter 会更好。
            • 在python2中,labels.count(x)/len(labels)应该是labels.count(x)/float(len(labels))
            【解决方案13】:

            上面的答案很好,但是如果您需要一个可以沿不同轴操作的版本,这里有一个可行的实现。

            def entropy(A, axis=None):
                """Computes the Shannon entropy of the elements of A. Assumes A is 
                an array-like of nonnegative ints whose max value is approximately 
                the number of unique values present.
            
                >>> a = [0, 1]
                >>> entropy(a)
                1.0
                >>> A = np.c_[a, a]
                >>> entropy(A)
                1.0
                >>> A                   # doctest: +NORMALIZE_WHITESPACE
                array([[0, 0], [1, 1]])
                >>> entropy(A, axis=0)  # doctest: +NORMALIZE_WHITESPACE
                array([ 1., 1.])
                >>> entropy(A, axis=1)  # doctest: +NORMALIZE_WHITESPACE
                array([[ 0.], [ 0.]])
                >>> entropy([0, 0, 0])
                0.0
                >>> entropy([])
                0.0
                >>> entropy([5])
                0.0
                """
                if A is None or len(A) < 2:
                    return 0.
            
                A = np.asarray(A)
            
                if axis is None:
                    A = A.flatten()
                    counts = np.bincount(A) # needs small, non-negative ints
                    counts = counts[counts > 0]
                    if len(counts) == 1:
                        return 0. # avoid returning -0.0 to prevent weird doctests
                    probs = counts / float(A.size)
                    return -np.sum(probs * np.log2(probs))
                elif axis == 0:
                    entropies = map(lambda col: entropy(col), A.T)
                    return np.array(entropies)
                elif axis == 1:
                    entropies = map(lambda row: entropy(row), A)
                    return np.array(entropies).reshape((-1, 1))
                else:
                    raise ValueError("unsupported axis: {}".format(axis))
            

            【讨论】:

              【解决方案14】:

              根据 unutbu 的建议,我创建了一个纯 python 实现。

              def entropy2(labels):
               """ Computes entropy of label distribution. """
                  n_labels = len(labels)
              
                  if n_labels <= 1:
                      return 0
              
                  counts = np.bincount(labels)
                  probs = counts / n_labels
                  n_classes = np.count_nonzero(probs)
              
                  if n_classes <= 1:
                      return 0
              
                  ent = 0.
              
                  # Compute standard entropy.
                  for i in probs:
                      ent -= i * log(i, base=n_classes)
              
                  return ent
              

              我缺少的一点是标签是一个大数组,但概率是 3 或 4 个元素长。使用纯 python,我的应用程序现在速度是原来的两倍。

              【讨论】:

              • 应该将'base'设置为类数吗?我认为自然日志是标准(以及您在原始问题中使用的内容。)
              猜你喜欢
              • 2016-02-09
              • 1970-01-01
              • 2017-03-10
              • 1970-01-01
              • 2021-09-02
              • 1970-01-01
              • 1970-01-01
              • 2010-09-07
              • 1970-01-01
              相关资源
              最近更新 更多