Как справиться с умножением чисел, близких к 1

У меня есть куча чисел с плавающей запятой (двойники Java), большинство из которых очень близки к 1, и мне нужно перемножить их вместе как часть более крупного вычисления. Мне нужно сделать это часто.

Проблема в том, что у двойников Java нет проблем с такими числами, как:

0.0000000000000000000000000000000001 (1.0E-34)

они не могут представлять что-то вроде:

1.0000000000000000000000000000000001

Следовательно, я быстро теряю точность (для двойников Java предел составляет около 1.0000000000000001).

Я думал просто хранить числа с вычтенной 1, поэтому, например, 1,0001 будет храниться как 0,0001, но проблема в том, что для их повторного умножения мне нужно добавить 1, и в этот момент я теряю точность.

Чтобы решить эту проблему, я мог бы использовать BigDecimals для выполнения вычислений (преобразовать в BigDecimal, добавить 1,0, затем умножить), а затем преобразовать обратно в двойные числа, но у меня есть серьезные опасения по поводу последствий этого для производительности.

Может ли кто-нибудь увидеть способ сделать это, избегая использования BigDecimal?

Изменить для ясности: это крупномасштабный совместный фильтр, в котором используется алгоритм оптимизации градиентного спуска. Точность является проблемой, потому что часто совместный фильтр имеет дело с очень небольшими числами (например, вероятность того, что человек нажмет на рекламу продукта, которая может составлять 1 из 1000 или 1 из 10000).

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


person sanity    schedule 04.04.2009    source источник
comment
Производительность не будет проблемой с тем, что вы предложили.   -  person Kevin Crowell    schedule 05.04.2009
comment
Зачем нужна такая точность и производительность? Возможно, в лучшем контексте проблемы мы могли бы предложить более подходящее решение?   -  person Alex Spurling    schedule 05.04.2009
comment
Кевин, можно поподробнее? Алекс, я попытался объяснить больше о контексте.   -  person sanity    schedule 05.04.2009


Ответы (8)


Ага: потому что

(1 + x) * (1 + y) = 1 + x + y + x*y

В вашем случае x и y очень малы, поэтому x*y будет намного меньше — слишком мало, чтобы повлиять на результаты ваших вычислений. Итак, что касается вас,

(1 + x) * (1 + y) = 1 + x + y

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

EDIT: только что заметил: вы говорите, что большинство из них очень близки к 1. Очевидно, что этот метод не будет работать для чисел, которые не близки к 1, то есть, если x и y большие. Но если один большой, а другой маленький, это все равно может работать; вас интересует только величина продукта x*y. (И если оба числа не близки к 1, вы можете просто использовать обычное умножение Java double...)

person David Z    schedule 04.04.2009
comment
Спасибо, Дэвид, это, безусловно, дало пищу для размышлений - это может быть ответом, но я оставлю его немного дольше, чтобы посмотреть, что предлагают другие. - person sanity; 05.04.2009
comment
В любом случае лучше просто использовать первое уравнение. Если x * y близко к 0 или нет, он все еще работает... - person Pool; 05.04.2009
comment
Отбрасывая xy, вы избавляетесь от умножения, что значительно ускоряет все вычисления. А поскольку двойники хранят только около 15 цифр точности (IIRC), если (xy)/(x + y) меньше 10 ^ -15, оно все равно будет усечено. - person David Z; 06.04.2009

Может быть, вы могли бы использовать логарифмы?

Логарифмы удобно сводят умножение к сложению.

Кроме того, чтобы позаботиться о начальной потере точности, существует функция log1p (по крайней мере, она существует в C/C++), которая возвращает log(1+x) без потери точности. (например, log1p(1e-30) возвращает мне 1e-30)

Затем вы можете использовать expm1, чтобы получить десятичную часть фактического результата.

person v3.    schedule 04.04.2009
comment
Примерно та же идея, что и в моем ответе, поскольку log (1 + x) = x для очень маленького x ... в любом случае +1 за использование математики для оптимизации ;-) - person David Z; 05.04.2009

Разве не для таких ситуаций предназначен BigDecimal?

Отредактировано для добавления:

«В предпоследнем абзаце я бы предпочел избегать BigDecimals, если это возможно, по соображениям производительности». - здравомыслие

«Преждевременная оптимизация — корень всех зол» — Кнут

Существует простое решение практически на заказ для вашей проблемы. Вы обеспокоены тем, что это может быть недостаточно быстро, поэтому хотите сделать что-то сложное, что, по вашему думанию, будет быстрее. Цитатой Кнута иногда злоупотребляют, но это именно та ситуация, от которой он предостерегал. Пишите по простому. Попробуй это. Профилируйте это. Посмотрите, не слишком ли это медленно. Если это так, то начните думать о том, как сделать это быстрее. Не добавляйте весь этот дополнительный сложный, подверженный ошибкам код, пока не поймете, что это необходимо.

person Chris Upchurch    schedule 04.04.2009
comment
В предпоследнем абзаце я бы предпочел избегать BigDecimals, если это возможно, по соображениям производительности. - person sanity; 05.04.2009
comment
Это не преждевременная оптимизация. double уже очень медленный, я провел сравнительный анализ, и BigDecimal кажется на несколько порядков медленнее. Это может быть решение, которое я выбираю, но я хочу рассмотреть альтернативы. - person sanity; 05.04.2009
comment
Хм :/ вы не привели полную цитату Кнута: Мы должны забыть о малой эффективности, скажем, примерно в 97% случаев: преждевременная оптимизация - корень всех зол en.wikipedia.org/wiki/Optimization_%28computer_science%29 - person Jason S; 06.04.2009

В зависимости от того, откуда берутся числа и как вы их используете, вы можете использовать рациональные числа вместо чисел с плавающей запятой. Не во всех случаях это правильный ответ, но если это правильный ответ, другого действительно нет.

Если рациональные числа не подходят, я бы поддержал ответ логарифмов.

Изменить в ответ на ваше редактирование:

Если вы имеете дело с числами, представляющими низкий уровень ответов, сделайте то, что делают ученые:

  • Представьте их как избыток/дефицит (нормализуйте часть 1.0)
  • Масштабируйте их. Подумайте о «частях на миллион» или о чем угодно.

Это позволит вам иметь дело с разумными числами для расчетов.

person MarkusQ    schedule 04.04.2009

Стоит отметить, что вы тестируете пределы своего оборудования, а не Java. Java использует 64-битную плавающую точку в вашем процессоре.

Я предлагаю вам проверить производительность BigDecimal, прежде чем вы решите, что она будет недостаточно быстрой для вас. Вы по-прежнему можете выполнять десятки тысяч вычислений в секунду с помощью BigDecimal.

person Peter Lawrey    schedule 05.04.2009

Как указывает Дэвид, вы можете просто сложить смещения.

(1+x) * (1+y) = 1 + x + y + x*y

Однако кажется рискованным отказаться от последнего срока. Не надо. Например, попробуйте следующее:

x = 1e-8 y = 2e-6 z = 3e-7 w = 4e-5

Что такое (1+x)(1+y)(1+z)*(1+w)? В двойной точности я получаю:

(1+x)(1+y)(1+z)*(1+w)

ответ =

      1.00004231009302

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

1 + (x+y+z+w)

ответ =

            1.00004231

Мы потеряли младшие биты, которые могли быть важны. Это проблема, только если некоторые из отличий от 1 в продукте составляют по крайней мере sqrt (eps), где eps — это точность, с которой вы работаете.

Попробуйте это вместо этого:

f = @(u,v) u + v + u*v;

результат = f(x,y);

результат = f(результат,z);

результат = f(результат,w);

1+результат

ответ =

      1.00004231009302

Как видите, это возвращает нас к результату двойной точности. На самом деле это немного точнее, так как внутреннее значение результата равно 4,23100930230249e-05.

person Community    schedule 07.04.2009

Если вам действительно нужна точность, вам придется использовать что-то вроде BigDecimal, даже если оно медленнее, чем Double.

Если вам действительно не нужна точность, возможно, вы могли бы пойти с ответом Дэвида. Но даже если вы часто используете умножения, это может быть некоторая преждевременная оптимизация, поэтому BIgDecimal в любом случае может быть подходящим способом.

person Caotic    schedule 04.04.2009

Когда вы говорите «большинство из которых очень близки к 1», сколько именно?

Возможно, вы могли бы иметь неявное смещение 1 во всех ваших числах и просто работать с дробями.

person Jimmy J    schedule 05.04.2009