Soluções para erros de arredondamento de ponto flutuante

18

Ao criar um aplicativo que lida com muitos cálculos matemáticos, encontrei o problema de que certos números causam erros de arredondamento.

Embora eu entenda que o ponto flutuante não é exato , o problema é como lido com os números exatos para garantir que, quando os cálculos sejam feitos neles, o arredondamento do ponto flutuante não cause problemas?

JNL
fonte
2
Existe um problema específico que você está enfrentando? Existem muitas maneiras de fazer o teste, tudo certo para algum problema. Perguntas que podem ter várias respostas não são adequadas para o formato de perguntas e respostas. Seria melhor se você pudesse definir o problema que está tendo de uma maneira que pudesse ter uma resposta certa, em vez de lançar uma rede de idéias e recomendações.
Estou construindo um aplicativo de software com muitas cálculos matemáticos. Entendo que o teste NUNIT ou JUNIT seria bom, mas gostaria de ter uma idéia de como abordar os problemas com calulações matemáticas.
JNL
1
Você pode dar um exemplo de cálculo que você testaria? Normalmente, não seria um teste de unidade de matemática bruta (a menos que você esteja testando seus próprios tipos numéricos), mas testar algo como distanceTraveled(startVel, duration, acceleration)seria testado.
Um exemplo será lidar com pontos decimais. Por exemplo, digamos que estamos construindo um muro com configurações especiais para dist x-0 ex = 14,589 e, em seguida, alguns arranjos de x = 14,589 ex = final da parede. A distância 0,59 quando convertida em binária não é a mesma .... Especialmente se adicionarmos algumas distâncias ... como 14,589 + 0,25 não será igual a 14,84 em binário .... Espero que não seja confuso?
JNL
1
@ MichaelT obrigado por editar a pergunta. Ajudou muito. Como sou novo nisso, não sou muito bom em como enquadrar as perguntas. :) ... Mas será bom em breve.
JNL

Respostas:

22

Existem três abordagens fundamentais para criar tipos numéricos alternativos sem arredondamento de ponto flutuante. O tema comum é que eles usam matemática inteira de várias maneiras.

Racionais

Represente o número como uma parte inteira e o número racional com um numerador e um denominador. O número 15.589seria representado como w: 15; n: 589; d:1000.

Quando adicionado a 0,25 (que é w: 0; n: 1; d: 4), isso envolve o cálculo do LCM e a adição dos dois números. Isso funciona bem para muitas situações, embora possa resultar em números muito grandes quando você está trabalhando com muitos números racionais que são relativamente primos entre si.

Ponto fixo

Você tem a parte inteira e a parte decimal. Todos os números são arredondados (existe essa palavra - mas você sabe onde está) com essa precisão. Por exemplo, você pode ter um ponto fixo com três casas decimais. 15.589+ 0.250se torna adicionando 589 + 250 % 1000para a parte decimal (e depois qualquer carrega para a parte inteira). Isso funciona muito bem com bancos de dados existentes. Como mencionado, existe um arredondamento, mas você sabe onde ele está e pode especificá-lo de forma que seja mais preciso do que o necessário (você está medindo apenas 3 casas decimais, portanto, fixe-o 4).

Ponto fixo flutuante

Armazene um valor e a precisão. 15.589é armazenado como 15589para o valor e 3para a precisão, enquanto 0.25é armazenado como 25e 2. Isso pode lidar com precisão arbitrária. Eu acredito que isso é o que os internos de usos BigDecimal do Java (não olhei recentemente) usos. Em algum momento, convém recuperá-lo deste formato e exibi-lo - e isso pode envolver arredondamentos (novamente, você controla onde está).


Depois de determinar a escolha da representação, você pode encontrar bibliotecas de terceiros existentes que usam isso ou criar suas próprias. Ao escrever o seu, certifique-se de fazer o teste unitário e verifique se está fazendo as contas corretamente.


fonte
2
É um bom começo, mas é claro que não resolve completamente o problema de arredondamento. Números irracionais como π, e e √2 não têm uma representação estritamente numérica; você precisa representá-los simbolicamente se desejar uma representação exata ou avaliá-los o mais tarde possível, se quiser minimizar o erro de arredondamento.
Caleb
O @Caleb for irrationals precisaria avaliá-los para além de onde qualquer arredondamento possa causar problemas. Por exemplo, 22/7 é preciso para 0,1% de pi, 355/113 é preciso para 10 ^ -8. Se você estiver trabalhando apenas com números com 3 casas decimais, ter 3,141592653 deve evitar erros de arredondamento com 3 casas decimais.
@ MichaelT: Para adicionar números racionais, você não precisa encontrar o LCM e é mais rápido não (e mais rápido cancelar "zeros LSB" depois e apenas simplificar totalmente quando for absolutamente necessário). Para números racionais em geral, normalmente é apenas "numerador / denominador" sozinho ou "numerador / denominador << expoente" (e não "parte inteira + numerador / denominador"). Além disso, seu "ponto fixo flutuante" é uma representação de ponto flutuante e seria melhor descrito como "ponto flutuante de tamanho arbitrário" (para diferenciá-lo de "ponto flutuante de tamanho fixo").
Brendan
algumas de suas terminologias são um pouco duvidosas - ponto fixo flutuante não faz sentido - acho que você está tentando dizer decimal flutuante.
jk.
10

Se os valores de ponto flutuante tiverem problemas de arredondamento e você não precisar enfrentar problemas de arredondamento, logicamente segue-se que o único curso de ação é não usar valores de ponto flutuante.

Agora a pergunta passa a ser: "como faço matemática envolvendo valores não inteiros sem variáveis ​​de ponto flutuante?" A resposta é com tipos de dados de precisão arbitrária . Os cálculos são mais lentos porque precisam ser implementados no software e não no hardware, mas são precisos. Você não disse qual idioma está usando, por isso não posso recomendar um pacote, mas existem bibliotecas de precisão arbitrárias disponíveis para as linguagens de programação mais populares.

Mason Wheeler
fonte
Estou usando o VC ++ agora ... Mas também gostaria de receber mais informações sobre outras linguagens de programação.
JNL
Mesmo sem valores de ponto flutuante, você ainda terá problemas redondos.
Chad
2
@Chad True, mas o objetivo não é eliminar os problemas de arredondamento (que sempre existirão, porque em qualquer base que você use existem alguns números que não têm representação exata, e você não tem memória infinita e poder de processamento), é reduza-o a ponto de não ter efeito no cálculo que você está tentando fazer.
Iker
@ Iker Você está certo. Embora você, nem a pessoa que fez a pergunta tenha especificado quais cálculos exatamente estão tentando obter e a precisão que deseja. Ele precisa responder a essa pergunta antes de pular a arma na teoria dos números. Apenas dizer lot of mathematical calculationsnão é útil nem as respostas dadas. Na grande maioria dos casos (se você não está lidando com moeda), o float deve ser suficiente.
Chad
@Chad esse é um ponto justo, certamente não há dados suficientes do OP para dizer qual é exatamente o nível de precisão de que eles precisam.
Iker
7

A aritmética de ponto flutuante é geralmente bastante precisa (15 dígitos decimais para a double) e bastante flexível. Os problemas surgem quando você está fazendo contas que reduz significativamente a quantidade de dígitos de precisão. aqui estão alguns exemplos:

  • Cancelamento na subtração:, 1234567890.12345 - 1234567890.12300o resultado 0.0045possui apenas dois dígitos decimais de precisão. Isso ocorre sempre que você subtrai dois números de magnitude semelhante.

  • Ingestão de precisão: 1234567890.12345 + 0.123456789012345avalia como 1234567890.24691, os últimos dez dígitos do segundo operando são perdidos.

  • Multiplicações: se você multiplicar dois números de 15 dígitos, o resultado terá 30 dígitos que precisam ser armazenados. Como você não pode armazená-los, os últimos 15 bits são perdidos. Isso é especialmente irritante quando combinado com um sqrt()(como em sqrt(x*x + y*y): O resultado terá apenas 7,5 dígitos de precisão.

Estas são as principais armadilhas que você precisa estar ciente. E uma vez que você os conhece, pode tentar formular sua matemática de uma maneira que os evite. Por exemplo, se você precisar incrementar um valor repetidamente em um loop, evite fazer isso:

for(double f = f0; f < f1; f += df) {

Após algumas iterações, o maior fengolirá parte da precisão de df. Pior, os erros se acumularão, levando à situação contra-intuitiva de que um menor dfpode levar a piores resultados gerais. Melhor escrever isso:

for(int i = 0; i < (f1 - f0)/df; i++) {
    double f = f0 + i*df;

Como você está combinando os incrementos em uma única multiplicação, o resultado fserá preciso com 15 dígitos decimais.

Este é apenas um exemplo, existem outras maneiras de evitar a perda de precisão devido a outros motivos. Mas já ajuda bastante pensar na magnitude dos valores envolvidos e imaginar o que aconteceria se você fizesse suas contas com caneta e papel, arredondando para um número fixo de dígitos após cada etapa.

cmaster - restabelece monica
fonte
2

Como garantir que você não tenha problemas: aprenda sobre problemas aritméticos de ponto flutuante ou contrate alguém que o faça ou use algum bom senso.

O primeiro problema é a precisão. Em muitos idiomas, você tem "float" e "double" (posição dupla para "double precision") e, em muitos casos, "float" fornece cerca de 7 dígitos de precisão, enquanto o dobro fornece 15. O senso comum é que, se você tiver um Numa situação em que a precisão pode ser um problema, 15 dígitos são muito melhores que 7 dígitos. Em muitas situações levemente problemáticas, usar "duplo" significa que você se safa e "flutuar" significa que não. Digamos que o valor de mercado de uma empresa é de 700 bilhões de dólares. Represente isso no float e o bit mais baixo é $ 65536. Representá-lo usando o dobro, e o bit mais baixo é de cerca de 0,012 centavos. Portanto, a menos que você saiba realmente o que está fazendo, use o dobro, não o flutuador.

O segundo problema é mais uma questão de princípio. Se você fizer dois cálculos diferentes que devem dar o mesmo resultado, eles geralmente não o fazem devido a erros de arredondamento. Dois resultados que devem ser iguais serão "quase iguais". Se dois resultados estiverem próximos, os valores reais podem ser iguais. Ou eles podem não ser. Você deve ter isso em mente e deve escrever e usar funções que digam "x é definitivamente maior que y" ou "x é definitivamente menor que y" ou "x e y podem ser iguais".

Esse problema fica muito pior se você usar o arredondamento, por exemplo "arredondar x para baixo para o número inteiro mais próximo". Se você multiplicar 120 * 0,05, o resultado deve ser 6, mas o que você obtém é "algum número muito próximo de 6". Se você "arredondar para o número inteiro mais próximo", esse "número muito próximo de 6" poderá ser "um pouco menor que 6" e arredondado para 5. E observe que não importa quanta precisão você tenha. Não importa quão próximo de 6 seja o seu resultado, desde que seja menor que 6.

E terceiro, alguns problemas são difíceis . Isso significa que não há regra rápida e fácil. Se o seu compilador suportar "long double" com mais precisão, você poderá usar "long double" e verificar se isso faz diferença. Se isso não faz diferença, você está bem ou tem um problema realmente complicado. Se faz o tipo de diferença que você esperaria (como uma mudança no décimo nono decimal), provavelmente está bem. Se realmente alterar seus resultados, você terá um problema. Peça por ajuda.

gnasher729
fonte
1
Não há "senso comum" em matemática de ponto flutuante.
Whatsisname
Saiba mais sobre isso.
gnasher729
0

Muitas pessoas cometem o erro quando vêem o dobro e gritam BigDecimal, quando na verdade acabam de mudar o problema para outro lugar. O dobro fornece bit de sinal: 1 bit, largura do expoente: 11 bits. Precisão significativa: 53 bits (52 armazenados explicitamente). Devido à natureza do duplo, quanto maior o interger, você perde a precisão relativa. Para calcular a precisão relativa que usamos aqui, abaixo.

Precisão relativa do dobro no cálculo, usamos o seguinte foluma 2 ^ E <= abs (X) <2 ^ (E + 1)

epsilon = 2 ^ (E-10)% Para um flutuador de 16 bits (meia precisão)

 Accuracy Power | Accuracy -/+| Maximum Power | Max Interger Value
 2^-1           | 0.5         | 2^51          | 2.2518E+15
 2^-5           | 0.03125     | 2^47          | 1.40737E+14
 2^-10          | 0.000976563 | 2^42          | 4.39805E+12
 2^-15          | 3.05176E-05 | 2^37          | 1.37439E+11
 2^-20          | 9.53674E-07 | 2^32          | 4294967296
 2^-25          | 2.98023E-08 | 2^27          | 134217728
 2^-30          | 9.31323E-10 | 2^22          | 4194304
 2^-35          | 2.91038E-11 | 2^17          | 131072
 2^-40          | 9.09495E-13 | 2^12          | 4096
 2^-45          | 2.84217E-14 | 2^7           | 128
 2^-50          | 8.88178E-16 | 2^2           | 4

Em outras palavras, se você deseja uma precisão de +/- 0,5 (ou 2 ^ -1), o tamanho máximo que o número pode ter é 2 ^ 52. Qualquer que seja maior que isso e a distância entre números de ponto flutuante seja maior que 0,5.

Se você deseja uma precisão de +/- 0,0005 (cerca de 2 ^ -11), o tamanho máximo que o número pode ter é 2 ^ 42. Qualquer que seja maior que isso e a distância entre números de ponto flutuante seja maior que 0,0005.

Eu realmente não posso dar uma resposta melhor do que isso. O usuário precisará descobrir qual precisão deseja ao executar o cálculo necessário e seu valor unitário (metros, pés, polegadas, mm, cm). Para a grande maioria dos casos, o float é suficiente para simulações simples, dependendo da escala do mundo que você deseja simular.

Embora seja algo a ser dito, se você estiver apenas com o objetivo de simular um mundo de 100 metros por 100 metros, terá um lugar na ordem de precisão próximo de 2 ^ -45. Isso nem mesmo mostra como a FPU moderna dentro das CPUs fará cálculos fora do tamanho do tipo nativo e somente após a conclusão do cálculo eles serão arredondados (dependendo do modo de arredondamento da FPU) para o tamanho do tipo nativo.

Chade
fonte