Предположим, у меня есть большой массив с кучей поплавков в нем, и мне нужно найти продукт, теряя как можно меньше точности ошибок с плавающей запятой:
import numpy as np
randoms = np.random.uniform(0.5, 1.61, 10000)
print(randoms[0:10])
array([ 1.01422339, 0.65581167, 0.8154046 , 1.49519379, 0.96114304,
1.20167417, 0.93667198, 0.66899907, 1.26731008, 1.59689486])
Предположительно плохой подход состоит в том, чтобы перебирать массив и итеративно умножать. Очевидно, что это будет ошибка, связанная с каждым умножением, поэтому следует избегать, если это возможно:
product_1 = 1
for i in randoms:
product_1 = product_1 * i
print(product_1)
64355009.758539267
Следующий метод заключается в использовании numpy
встроенного prod
функции, однако это возвращает ту же самую величину, как указано выше, что свидетельствует о том, что это, как prod
на самом деле это вычисление:
product_2 = np.prod(randoms)
print(product_2)
64355009.758539267
print(product_1 == product_2)
True
Третий способ - вычислить логарифм каждого терма, суммировать их и экспоненциально в конце. Каждый логарифм вычисляется отдельно, так что не существует такой же комбинации ошибки, но процесс логарифма и процесс возведения в степень сами вводят некоторую ошибку. В любом случае он дает другой ответ:
product_3 = np.exp(np.sum(np.log(randoms)))
print(product_3)
64355009.758538999
print(product_3 == product_1)
False
Я знаю, что в этом примере я не теряю такой высокой точности, но для того, что мне действительно нужно делать, ошибки компаундирования в конечном итоге вызывают проблемы, достаточно того, что я рассматриваю возможность использования пакета, который может выполнять вычисление символьной/произвольной точности, Итак, какой метод лучше всего здесь? Есть ли другие способы, которые я не рассматривал?
Я попробовал несколько экспериментов. Код ниже, но сначала некоторые комментарии.
Можно точно вычислить результат, переведя значения в точные рациональные числа, точно вычислив продукт, а затем выполнив окончательное преобразование в поплавок. Это можно сделать с помощью модуля fractions
входящего в состав Python, но в конечном итоге он будет очень медленным. Я использовал модуль gmpy2
для более быстрой рациональной арифметики.
Существуют некоторые тонкости с форматированием двоичных значений с плавающей запятой для отображения. Последние версии Python возвращают кратчайшую десятичную строку, которая приведет к исходному значению. numpy
floats имеют другое форматирование. А также тип gmpy2.mpfr
. И Decimal
явно использовал другое правило форматирования. Поэтому я всегда конвертирую результат, вычисляемый в плавающий Python.
В дополнение к определяемой пользователем десятичной точности Decimal
типа, я также использовал gmpy2.mpfr
поскольку он поддерживает определяемую пользователем двоичную точность.
Программа выводит несколько значений:
Вот код. Вы можете изменить точность Decimal
и mpfr
и проверить точность.
import numpy as np
from gmpy2 import mpq, mpfr, get_context, round2
from decimal import Decimal, getcontext
randoms = np.random.uniform(0.5, 1.61, 10000)
# Sequential multiplication using 53-bit binary precision.
product_1 = 1
for i in randoms:
product_1 = product_1 * i
print("53-bit binary: ", float(product_1))
# Exact value by converting all floats to fractions and then a final
# conversion to float. Uses gmpy2 for speed.
product_2 = 1
for i in randoms:
product_2 = product_2 * mpq(i)
print("exact using mpq: ", float(mpfr(product_2, precision=53)))
# Decimal math with 28 decimal digits (~93 bits of precision.)
product_3 = 1
for i in randoms:
product_3 = product_3 * Decimal(i)
print("Decimal(prec=28): ", float(product_3))
# Choose your own decimal precision.
getcontext().prec=18
product_4 = 1
for i in randoms:
product_4 = product_4 * Decimal(i)
print("Decimal(prec=%s): %s" % (getcontext().prec, float(product_4)))
# Choose your own binary precision.
get_context().precision = 60
product_5 = 1
for i in randoms:
product_5 = product_5 * mpfr(i)
print("mpfr(precision=%s): %s" % (get_context().precision, float(product_5)))
# Recursively multiply pairs of numbers together.
def rmult(d):
if len(d) == 1:
return d[0]
# If the length is odd, extend with 1.
if len(d) & 1:
d.append(1)
temp = []
for i in range(len(d)//2):
temp.append(d[2*i] * d[2*i+1])
return rmult(temp)
print("recursive 53-bit: ", float(rmult(list(randoms))))
В качестве приблизительного ориентира, по мере увеличения количества умножений, промежуточная точность должна увеличиться. Рациональная арифметика эффективно даст вам бесконечную промежуточную точность.
Насколько важно, чтобы результат был на 100% точным?
NumPy
«Scumprod
который выступает за совокупный продукт. Это то, что вы делаете в основном. Просто возьмите последний элемент[-1]
какproduct_1 = np.cumprod(randoms)[-1]
. Вы можете сравнить свой ответ