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

Имам куп числа с плаваща запетая (удвоявания на Java), повечето от които са много близки до 1, и трябва да ги умножа заедно като част от по-голямо изчисление. Трябва да правя това много.

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

0.0000000000000000000000000000000001 (1.0E-34)

те не могат да представляват нещо като:

1.0000000000000000000000000000000001

Вследствие на това бързо губя прецизност (лимитът изглежда е около 1.000000000000001 за двойките на Java).

Обмислях просто да съхраня числата с извадено 1, така че например 1,0001 ще бъде съхранено като 0,0001 - но проблемът е, че за да ги умножа отново заедно, трябва да добавя 1 и в този момент губя точност.

За да се справя с това, бих могъл да използвам BigDecimal, за да извърша изчислението (преобразуване в BigDecimal, добавяне на 1.0, след това умножение) и след това обратно преобразуване в двойни след това, но имам сериозни притеснения относно последиците от това за производителността.

Може ли някой да види начин за това, който избягва използването на BigDecimal?

Редактиране за яснота: Това е за широкомащабен филтър за сътрудничество, който използва алгоритъм за оптимизиране на градиентно спускане. Точността е проблем, тъй като често филтърът за сътрудничество работи с много малки числа (като вероятността човек да кликне върху реклама за продукт, която може да бъде 1 на 1000 или 1 на 10 000).

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


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, те ще бъдат достатъчно близки до математически прецизните резултати, че няма да ви интересува разликата.

РЕДАКТИРАНЕ: Току-що забелязано: казвате, че повечето от тях са много близки до 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?

Редактирано за добавяне:

„Съгласно предпоследния параграф бих предпочел да избегна BigDecimal, ако е възможно от съображения за производителност.“ – здравия разум

„Преждевременната оптимизация е коренът на всяко зло“ – Кнут

Има просто решение, направено практически по поръчка за вашия проблем. Притеснявате се, че може да не е достатъчно бързо, така че искате да направите нещо сложно, което мислите, че ще бъде по-бързо. Цитатът на Кнут понякога се използва прекалено много, но това е точно ситуацията, срещу която той предупреждаваше. Напишете го по простия начин. Тествайте го. Профилирайте го. Вижте дали е твърде бавно. Ако е тогава започнете да мислите за начини да го направите по-бързо. Не добавяйте целия този допълнителен сложен, податлив на грешки код, докато не разберете, че е необходим.

person Chris Upchurch    schedule 04.04.2009
comment
Съгласно предпоследния параграф бих предпочел да избягвам BigDecimal, ако е възможно от съображения за ефективност. - 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)

ans =

      1.00004231009302

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

1 + (x+y+z+w)

ans =

            1.00004231

Загубихме битовете от нисък ред, които може да са били важни. Това е проблем само ако някои от разликите от 1 в продукта са поне sqrt(eps), където eps е точността, с която работите.

Опитайте това вместо това:

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

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

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

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

1+резултат

ans =

      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