La forma más rápida de calcular la entropía en Python

8 minutos de lectura

Avatar de usuario de blueSurfer
surfista azul

En mi proyecto, necesito calcular la entropía de los vectores 0-1 muchas veces. Aquí está mi código:

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)

¿Hay una manera mas rápida?

  • ¿Cuál es una longitud típica de labels?

    – unutbu

    16 de marzo de 2013 a las 14:09

  • La longitud no es fija..

    – surfista azul

    16 de marzo de 2013 a las 14:12

  • Sería útil con la evaluación comparativa conocer los valores típicos de labels. Si labels es demasiado corto, una implementación de python pura en realidad podría ser más rápida que usar NumPy.

    – unutbu

    16 de marzo de 2013 a las 14:13

  • solo para confirmar, ¿esta pregunta es para la entropía de una variable aleatoria discreta (binaria)? y no la entropía diferencial de un rv continuo?

    – develarista

    4 de agosto de 2020 a las 8:37


  • No sé, ¿qué tan rápido se ejecuta tu código? Esto está basado en opiniones en su formato actual.

    – TylerH

    3 de noviembre de 2021 a las 19:26

Avatar de usuario de Jarad
Jarad

La respuesta de @Sanjeet Gupta es buena pero podría condensarse. Esta pregunta se refiere específicamente a la forma “más rápida”, pero solo veo tiempos en una respuesta, así que publicaré una comparación del uso de scipy y numpy con la respuesta entropy2 del póster original con ligeras modificaciones.

Cuatro enfoques diferentes: (1) scipy/numpy, (2) numpy/matemáticas, (3) pandas/numpy, (4) 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()
    

Operaciones de tiempo:

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)

Resultados de tiempo:

# 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

Ganador: numpy/matemáticas (entropy2)

También vale la pena señalar que el entropy2 La función anterior puede manejar datos numéricos Y de texto. ex: entropy2(list('abcdefabacdebcab')). La respuesta del cartel original es de 2013 y tenía un caso de uso específico para agrupar entradas, pero no funcionará para texto.

  • Está utilizando una matriz tan pequeña que sus pruebas son básicamente inútiles. Realmente solo está midiendo la sobrecarga de llamadas para las diversas interfaces.

    – Nombre falso

    8 de julio de 2018 a las 4:03

  • Usando este código, obtuve el tiempo para mi respuesta (“Una respuesta que no depende de numpy, tampoco…”) también, y es Method: eta, Avg.: 10.461799. Como alguien sugirió, me pregunto si realmente está probando la sobrecarga de llamadas aquí.

    – blamblambunny

    17/09/2018 a las 11:40

  • Es mejor tomar el mínimo de los resultados de tiempo, en lugar de la media. Ver el “nota” bajo la función de repetición del módulo timeit.

    – MikeR

    19 de diciembre de 2019 a las 0:06

  • las operaciones de vectorizador pueden ser preferibles para entradas grandes. El ciclo ‘for’ en la opción 2 puede no ser una buena opción en tales casos. En mi opinión, la opción 1 puede ser preferible.

    – Allohvk

    20 sep 2021 a las 14:15

Avatar de usuario de Sanjeet Gupta
Sanjeet Gupta

Con los datos como pd.Series y scipy.statscalcular la entropía de una cantidad dada es bastante sencillo:

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

Nota: scipy.stats normalizará los datos proporcionados, por lo que no es necesario hacerlo explícitamente, es decir, pasar una matriz de conteos funciona bien.

  • los value_counts() el método solo cuenta. probablemente p_data no sería p_data = data.value_counts()/sum(data.value_counts().values) Entonces tendrá el conteo de cada clase dividido por la cantidad total de datos, luego tendrá la probabilidad de cada clase.

    – mnsosa

    28 de marzo a las 14:03

avatar de usuario de blamblambunny
blablablabunny

Una respuesta que tampoco depende de 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

Esto aceptará cualquier tipo de datos que pueda arrojarle:

>>> eta(['mary', 'had', 'a', 'little', 'lamb'])
1.6094379124341005

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

La respuesta proporcionada por @Jarad también sugirió tiempos. Con ese fin:

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)

Resultados de Timeit: (Creo que esto es ~ 4 veces más rápido que el mejor enfoque numpy)

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

Method: eta, Avg.: 10.461799

  • por qué necesitas problemas = [p for p in probs if p > 0.]?

    – Vlad

    20 de agosto de 2018 a las 5:41

  • Como estoy haciendo esa prueba cinco líneas después, sospecho que no la necesito en absoluto 🙂 Editado.

    – blamblambunny

    17 de septiembre de 2018 a las 11:28


  • más uno para no tener nuevas dependencias

    – curob

    9 de abril de 2020 a las 15:36

  • ¿Puedes hacer recuentos = Contador (datos) en lugar de recorrer los caracteres de los datos?

    – Alan Hamlett

    23 de octubre de 2020 a las 4:22

Siguiendo la sugerencia de unutbu, creo una implementación pura de 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

El punto que me faltaba era que las etiquetas son una matriz grande, sin embargo, los problemas tienen 3 o 4 elementos de largo. Usando Python puro, mi aplicación ahora es el doble de rápida.

Avatar de usuario de Tan Duong
bronceado duong

Aquí está mi enfoque:

labels = [0, 0, 1, 1]

from collections import Counter
from scipy import stats

stats.entropy(list(Counter(labels).values()), base=2)

  • Esto parece funcionar para mis segmentos de imagen, pero en realidad necesito la probabilidad de valores de píxeles en el segmento de 0 a 255.

    – mlestudiante33

    12 de noviembre de 2019 a las 8:52

Avatar de usuario de Ottotos
Ottotos

Mi función favorita para la entropía es la siguiente:

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))

Todavía estoy buscando una mejor manera de evitar el dict -> valores -> lista -> conversión np.array. Volveré a comentar si lo encuentro.

  • Esto parece funcionar para mis segmentos de imagen, pero en realidad necesito la probabilidad de valores de píxeles en el segmento de 0 a 255.

    – mlestudiante33

    12 de noviembre de 2019 a las 8:52

Avatar de usuario de la comunidad
Comunidad

Datos distribuidos uniformemente (alta entropía):

s=range(0,256)

Cálculo de la entropía de Shannon paso a paso:

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

De una sola línea (asumiendo 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()]])

Una función adecuada:

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)

Casos de prueba – Texto en inglés tomado de Estimador de entropía 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

  • Esto deja muy claro la capacidad de calcular la entropía en un rango específico de valores. Necesito aplicar este método al área conectada en 8 alrededor de un píxel y sus valores de escala de grises. Preguntándome si también me vendría bien un método incorporado.

    – mlestudiante33

    12 de noviembre de 2019 a las 8:09


¿Ha sido útil esta solución?