Вопрос

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

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

0.0000000000000000000000000000000001 (1.0E-34)

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

1.0000000000000000000000000000000001

В результате я быстро теряю точность (предел, по-видимому, составляет около 1,000000000000001 для двойных значений Java).

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

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

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

Другие советы

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

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

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

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

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

Отредактировано, чтобы добавить:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(1+х)(1+у)(1+z)*(1+w)

ответ =

      1.00004231009302

Однако посмотрите, что произойдет, если мы просто выполним простое аддитивное приближение.

1 + (х+у+z+w)

ответ =

            1.00004231

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

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

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

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

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

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

1+результат

ответ =

      1.00004231009302

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top