Лог-вычисления в Python

1

Я хочу вычислить что-то вроде:

Изображение 174551

Где f(i) - это функция, которая возвращает действительное число в [-1,1] для любого i в {1,2,...,5000}.

Очевидно, результат суммы находится где-то в [-1,1], но когда я не могу вычислить его в Python, используя прямое кодирование, поскольку 0.55000 становится 0 и comb(5000,2000) становится inf, в результате чего вычисленная сумма превращается в NaN.

Необходимым решением является использование журнала с обеих сторон.

То есть, используя идентификатор a × b = 2log(a) + log(b), если бы я мог вычислить log(a) и log(b), я мог бы вычислить сумму, даже если a большой и b почти 0.

Итак, я думаю, что я спрашиваю, есть ли простой способ вычислить

log2(scipy.misc.comb(5000,2000))

Итак, я могу вычислить свою сумму просто

sum([2**(log2comb(5000,i)-5000) * f(i) for i in range(1,5000) ])

@abarnert, в то время как работа на 5000 фигуре решает проблему, увеличивая точность вычисления гребня. Это работает для этого примера, но не масштабируется, поскольку требуемая память значительно увеличится, если вместо 5000 у нас было 1e7, например.

В настоящее время я использую обходное решение, которое является уродливым, но сохраняет низкое потребление памяти:

log2(comb(5000,2000)) = sum([log2 (x) for x in 1:5000])-sum([log2 (x) for x in 1:2000])-sum([log2 (x) for x in 1:3000])

Есть ли способ сделать это в читаемом выражении?

  • 0
    Как вы думаете, сколько памяти занимают целые числа? comb(1000000, 200000) составляет около 2**721918 , что означает, что для точного хранения требуется около 720К. И, так как каждый из них вам нужен только временно, кого это волнует?
Теги:
scipy
large-data

3 ответа

8
Лучший ответ

Сумма

Изображение 174551

- это ожидание f относительно биномиального распределения с n = 5000 и p = 0.5.

Вы можете вычислить это с помощью scipy.stats.binom.expect:

import scipy.stats as stats

def f(i):
    return i
n, p = 5000, 0.5
print(stats.binom.expect(f, (n, p), lb=0, ub=n))
# 2499.99999997

Также обратите внимание, что как n переходит в бесконечность, при фиксированном p биномиальное распределение приближается к нормальному распределению со средним значением np и дисперсии np*(1-p). Поэтому для больших n вы можете вычислить:

import math
print(stats.norm.expect(f, loc=n*p, scale=math.sqrt((n*p*(1-p))), lb=0, ub=n))
# 2500.0
3

EDIT: @unutbu ответил на реальный вопрос, но я оставлю это здесь, если log2comb(n, k) полезен для всех.


comb(n, k) является n!/((n-k)! k!) и n! может быть вычислен с помощью функции Gamma gamma(n+1). Scipy предоставляет функцию scipy.special.gamma. Scipy также предоставляет gammaln, который является журналом (естественным логом, то есть) функции Гамма.

Итак, log(comb(n, k)) можно вычислить как gammaln(n+1) - gammaln(n-k+1) - gammaln(k+1)

Например, log (гребень (100, 8)) (после выполнения from scipy.special import gammaln):

In [26]: log(comb(100, 8))
Out[26]: 25.949484949043022

In [27]: gammaln(101) - gammaln(93) - gammaln(9)
Out[27]: 25.949484949042962

и log (гребень (5000, 2000)):

In [28]: log(comb(5000, 2000))  # Overflow!
Out[28]: inf

In [29]: gammaln(5001) - gammaln(3001) - gammaln(2001)
Out[29]: 3360.5943053174142

(Конечно, чтобы получить логарифм базы-2, просто разделите на log(2).)

Для удобства вы можете определить:

from math import log
from scipy.special import gammaln

def log2comb(n, k):
    return (gammaln(n+1) - gammaln(n-k+1) - gammaln(k+1)) / log(2) 
3

По умолчанию comb дает float64, который переполняется и дает вам inf.

Но если вы пройдете exact=True, вместо этого вы получите Python с переменным размером int, который не может переполняться (если вы не получите настолько смехотворно огромным, что у вас не хватает памяти).

И, хотя вы не можете использовать np.log2 в int, вы можете использовать Python math.log2.

Итак:

math.log2(scipy.misc.comb(5000, 2000, exact=True))

В качестве альтернативы вы относитесь к тому, что n выбирает k, как n!k / k!, правильно? Вы можете уменьшить это до ∏(i=1...k)((n+1-i)/i), которое просто вычислить.

Или, если вы хотите избежать переполнения, вы можете сделать это, чередуя * (n-i) и / (k-i).

Что, конечно, вы также можете сократить до добавления и вычитания журналов. Я думаю, что цикл в Python и вычисление логарифмов 4000 будет медленнее, чем цикл в C и вычисление 4000 умножений, но мы всегда можем его векторизовать, а затем, возможно, быстрее. Пусть написано это и протестируйте:

In [1327]: n, k = 5000, 2000
In [1328]: %timeit math.log2(scipy.misc.comb(5000, 2000, exact=True))
100 loops, best of 3: 1.6 ms per loop
In [1329]: %timeit np.log2(np.arange(n-k+1, n+1)).sum() - np.log2(np.arange(1, k+1)).sum()
10000 loops, best of 3: 91.1 µs per loop

Конечно, если вы больше озабочены памятью, а не временем... ну, это, очевидно, делает ее еще хуже. У нас есть 2000 8-байтовых поплавков вместо одного 608-байтового целого числа за раз. И если вы дойдете до 100000, 20000, вы получите 20000 8-байтовых поплавков вместо одного 9K целого числа. И при 1000000, 200000, это 200000 8-байтных поплавков против одного целых 720K.

Я не уверен, почему в любом случае проблема для вас. Особенно учитывая, что вы используете listcomp вместо genxpr, и поэтому создание ненужного списка 5000, 100000 или 1000000 Python-float-24MB не проблема, но 720K есть? Но если это так, мы можем, очевидно, просто сделать то же самое итеративно, ценой некоторой скорости:

r = sum(math.log2(n-i) - math.log2(k-i) for i in range(n-k))

Это не слишком медленнее, чем решение scipy, и оно никогда не использует больше, чем небольшое постоянное количество байтов (небольшое количество плавающих Python). (Если вы не на Python 2, и в этом случае... просто используйте xrange вместо range и вернитесь к константе.)


В качестве побочного примечания, почему вы используете понимание списка вместо массива NumPy с векторизованными операциями (для скорости, а также бит компактности) или выражение генератора вместо понимания списка (без использования памяти вообще, бесплатно для скорости)?

  • 0
    Спасибо @abarnert, но это кажется неэффективным. Я не против потерять точность, но хочу сохранить низкое использование памяти. младшие значащие цифры журнала здесь бесполезны в любом случае, в результате после возведения в степень они будут храниться в float64, верно? Мое текущее решение позволяет избежать этого, вычисляя журнал самостоятельно (то есть log2 (comb (5000,2000)) = sum ([log2 (x) для x in 1: 5000]) - sum ([log2 (x) для x in 1 : 2000]) - сумма ([log2 (x) для x в 1: 3000]), но выглядит очень уродливо).
  • 0
    @RB: Это несколько неэффективно, но мы говорим о 1 мс на моем ноутбуке, вы делаете это 4999 раз, так что ... 5 секунд это проблема?
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню