Самый быстрый способ вычисления энтропии в Python

В моем проекте мне нужно много раз вычислять энтропию векторов 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)

Есть ли более быстрый способ?


person blueSurfer    schedule 16.03.2013    source источник
comment
Какая типичная длина labels?   -  person unutbu    schedule 16.03.2013
comment
Длина не фиксированная ..   -  person blueSurfer    schedule 16.03.2013
comment
Для сравнительного анализа было бы полезно знать типичные значения labels. Если labels слишком короткое, чистая реализация python может быть быстрее, чем использование NumPy.   -  person unutbu    schedule 16.03.2013
comment
просто чтобы подтвердить, этот вопрос касается энтропии дискретной (двоичной) случайной величины? а не дифференциальная энтропия непрерывной с.в.?   -  person develarist    schedule 04.08.2020


Ответы (12)


@Sanjeet Gupta ответ хорош, но его можно было бы сократить. В этом вопросе конкретно задается вопрос о «самом быстром» способе, но я вижу только время для одного ответа, поэтому я опубликую сравнение использования scipy и numpy с ответом entropy2 исходного плаката с небольшими изменениями.

Четыре разных подхода: scipy / numpy, numpy / math, pandas / numpy, numpy.

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 (entropy2).

Также стоит отметить, что приведенная выше функция entropy2 может обрабатывать числовые И текстовые данные. пример: entropy2(list('abcdefabacdebcab')). Исходный ответ постера относится к 2013 году и имеет конкретный вариант использования для объединения целых чисел, но он не работает для текста.

person Jarad    schedule 13.07.2017
comment
Вы используете такой маленький массив, что ваши тесты в основном бесполезны. На самом деле вы просто измеряете накладные расходы на вызовы для различных интерфейсов. - person Fake Name; 08.07.2018
comment
На этой странице есть кнопка Добавить еще один ответ. Не стесняйтесь предложить свой лучший ответ. - person Jarad; 08.07.2018
comment
Используя этот код, я только что получил время для своего ответа (ответа, который тоже не полагается на numpy ...) - и это Method: eta, Avg.: 10.461799. Как кто-то предположил, мне интересно, действительно ли вы здесь тестируете накладные расходы на вызовы. - person blamblambunny; 17.09.2018
comment
Лучше брать минимум результатов timeit вместо среднего. См. Примечание под функцией повтора модуля timeit. - person Mike R; 19.12.2019

С данными в виде pd.Series и scipy.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 нормализует предоставленные данные, поэтому это не нужно делать явно, т.е. передача массива счетчиков работает нормально.

person Sanjeet Gupta    schedule 29.08.2016

Ответ, который также не зависит от 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: (я считаю, что это примерно в 4 раза быстрее, чем лучший подход numpy)

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

Method: eta, Avg.: 10.461799
person blamblambunny    schedule 17.06.2016
comment
зачем тебе probs = [p вместо p в probs, если p ›0]? - person Vlad; 20.08.2018
comment
Поскольку я делаю этот тест пятью строками позже, подозреваю, что он мне вообще не нужен :) Отредактировано. - person blamblambunny; 17.09.2018
comment
плюс один без новых зависимостей - person curob; 09.04.2020
comment
Можете ли вы сделать counts = Counter (data) вместо того, чтобы перебирать символы данных? - person Alan Hamlett; 23.10.2020

Следуя предложению unutbu, я создаю реализацию на чистом питоне.

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 элементов. Используя чистый питон, мое приложение теперь работает вдвое быстрее.

person blueSurfer    schedule 18.03.2013
comment
Следует ли установить «base» на количество классов? Я думал, что естественный журнал был стандартом (и тем, что вы использовали в своем исходном вопросе). - person blamblambunny; 18.06.2016

Моя любимая функция энтропии следующая:

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. Прокомментирую еще раз, если найду.

person Ottotos    schedule 17.02.2017
comment
хорошо, используйте коллекции. Счетчик лучше бы. - person NullPointer; 20.05.2017
comment
В python2 labels.count(x)/len(labels) должно быть labels.count(x)/float(len(labels)) - person user553965; 10.02.2019

Вот мой подход:

labels = [0, 0, 1, 1]

from collections import Counter
from scipy import stats

stats.entropy(list(Counter(labels).values()), base=2)
person Tan Duong    schedule 10.06.2018
comment
Кажется, это работает для моих фрагментов изображения, но мне действительно нужна вероятность значений пикселей в фрагменте от 0 до 255. - person mLstudent33; 12.11.2019

Равномерно распределенные данные (высокая энтропия):

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 энтропия оценщик:

>>> 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
person kravietz    schedule 17.11.2017
comment
Это очень ясно дает возможность вычислять энтропию в указанном диапазоне значений. Мне нужно применить этот метод к 8-соединенной области вокруг пикселя и их значений в градациях серого. Интересно, могу ли я обойтись и встроенным методом. - person mLstudent33; 12.11.2019

Этот метод расширяет другие решения, позволяя группировать. Например, 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 
person D.Deriso    schedule 08.01.2020

Биэнтропия не будет самым быстрым способом вычисления энтропии, но она строгая и четко определенным образом основывается на энтропии Шеннона. Он был протестирован в различных областях, включая приложения, связанные с изображениями. Он реализован на Python на Github.

person Grenville Croll    schedule 17.01.2020

Приведенный выше ответ хорош, но если вам нужна версия, которая может работать по разным осям, вот рабочая реализация.

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))
person d.b    schedule 27.08.2015

person    schedule
comment
Хотя это может дать ответ на вопрос, ответы, содержащие только код, обычно считаются некачественными. Предоставление более подробного описания и контекста того, почему улучшит качество этого ответа. Спасибо. - person Dutts; 29.06.2019
comment
@Dutts Чего бы ты ожидал? Да, есть более быстрый способ: .... Опубликованный код, на мой взгляд, не требует пояснений, и вопрос не требует подробного ответа. - person user32434999; 10.12.2019
comment
Был отмечен как низкокачественный ответ по причинам, которые я написал. - person Dutts; 11.12.2019

person    schedule
comment
Когда вы отвечаете кодом, вы должны написать какое-то объяснение. - person Damian Kozlak; 29.12.2019
comment
В вопросе аргументом является список ярлыков - person Orkun Berk Yuzbasioglu; 06.04.2021