Небольшая разница в функции Fortran Sine с использованием расширения Маклаурина - PullRequest
0 голосов
/ 17 мая 2018

Я создаю программу на Фортране, которая принимает x для sin (x) в радианах, а затем количество термов для вычисления.

Это моя программа:

! Sine value using MacLaurin series 

program SineApprox
    implicit none
    integer :: numTerms, denom, i
    double precision :: x, temp, summ

    ! Read Angle in Radians, and numTerms
    read(*,*) x, numTerms

    ! Computing MacLaurin series
    denom = (2*numTerms)-1  !Gives denominator
    temp = x                !Temp calculates each term
    summ = x

    do i = 3, denom, 2
        temp = (((-temp)*(x**2))/(i*(i-1)))
        summ = summ + temp 
    end do
    print *, summ

end program SineApprox

Однако я не получаю того же значения, которое требует мой проф для ввода: 5 30

Вывод моего кода:

-0.95892427466314001  

Но требуемый вывод:

-0.95892427466313568
                ^^^^

Я не могу понять, где ошибка.

Ответы [ 4 ]

0 голосов
/ 22 мая 2018

В отличие от того, что было продемонстрировано на Chux's и aka.nice свои ответы и упоминал в разных комментариях я полагаю, что мы сделали поспешный вывод, сосредоточившись только на одном вычислении, то есть sin(5). Даже если ответ ОП ближе к истинному значению, Надо сказать, что как алгоритм ОП и факториал алгоритм столь же плох для вычисления sin(5), но в конце концов, Факториал алгоритм лучше.

Этот ответ не будет вдаваться в подробности о Floating-Point Арифметика. Превосходную монографию можно найти по этой теме в Что Каждый ученый должен знать о с плавающей точкой Арифметика . Также я не открою банку с червями Средний Точность с плавающей точкой .

отказ от ответственности: Я не хочу утверждать, что то, что я здесь пишу, является на 100% правильным, и я точно не авторитет в этой области. Я просто нашел вопрос чрезвычайно интересным и попытался извлечь из него как можно больше. Я, конечно, приветствую любой комментарий!

некоторые интересные чтения, которые приводят к этому:

Некоторые исходные понятия

  • представление с плавающей точкой (FPR): в конечной плавающей точке представление, число записывается как ± 0.d 1 d 2 ... d p x b e . Он имеет точность p и четность base b где все цифры d i являются целыми числами с 0 & le; d i . Это просто, что такое представление не может представлять все действительные числа. Например, если b = 10 и p = 3 , число 1/3 приблизительно равно 0,333 x 10 0 или если b = 2 и p = 24 нужно приблизительно 0,1 как 0,110011001100110011001101 x 2 5 .

  • абсолютная ошибка (AE): , если действительное число x имеет AE, равное & Delta; x , тогда мы знаем, что ожидаемое истинное значение из x лежит между x - & Delta; x & le; x̃ & le; х + & Delta; х . Если x ближайший FPR к истинному значению , то мы знаем, что его AE is ((b / 2) b -p - 1 ) x b e . Например, если b = 10 и p = 3 , число 1/3 приблизительно равно 0,333 x 10 0 и имеет абсолютную ошибку 0,0005 x 10 0 указывая, что 0,3325 x 10 0 & le; 1/3 & le; 0,3335 х 10 0

Что все это значит для серии Тейлор-Маклаурин?

Для стандартного IEEE binary64 (двойная точность) вычисления ( b = 2 и p = 53 ) из серии Тейлора-Маклаурина sin(5) один быстро видит, что лошадь уже сбежала на 2-й и 3-й срок. Здесь FPR является точным только до 2^-49, как можно видеть в таблица ниже (представляющая ближайшее двоичное представление истинные дроби):

j term        FPR                     e AE
              0.12345678901234567    
1  5^1/1!     5.00000000000000000E+00 3 2^-51 4.4E-16
3 -5^3/3!    -2.08333333333333321E+01 5 2^-49 1.8E-15
5  5^5/5!     2.60416666666666679E+01 5 2^-49 1.8E-15
7 -5^7/7!    -1.55009920634920633E+01 4 2^-50 8.9E-16

j  sum        FPR                      e AE
              0.12345678901234567    
1     5       5.00000000000000000E+00  3 2^-51 4.4E-16
3   -95/6    -1.58333333333333339E+01  4 2^-50 8.9E-16
5   245/24    1.02083333333333339E+01  4 2^-50 8.9E-16
7 -5335/1008 -5.29265873015873023E+00  3 2^-51 4.4E-16

Точность до 2^-49 можно понять следующим образом. Если вы смотрите на термин 5^3/3!, то ближайший FPR этого числа фракция (5864062014805333 / 9007199254740992) * 2^5. Как и ты видите, нам здесь не хватает части, а именно

5^3/3! - 5864062014805333 / 9007199254740992) * 2^5
    = 1/844424930131968
    ~ 1.1842378929335002E-15 < 2^-49

Так что независимо от того, что мы пытаемся сделать, мы всегда пропускаем эту часть. так что это не так можно вычислить sin(5) с точностью лучше 2^-49. Как это точность наибольшего термина (абсолютное значение) прибавил к сумме. Тем не менее, другие термины также вносят ошибки и все эти ошибки накапливаются. Итак, после первых 30 семестров, мы знаем, что AE для sin(5) накапливается так:

2^-51 + 2^-49 + 2^-49 + 2^50 + ... = 5.45594...E-15

Это число, однако, слишком идеалистично, поскольку больше потерь писправление из-за потери значимости .

Какой алгоритм лучше?

Два представленных алгоритма (при условии, что все переменные IEEE binary64 (двойная точность) переменные, за исключением i):

  • алгоритм 1: Это слегка принятая версия алгоритма, представленная здесь

    fact = 1.0_dp; pow  = x; sum = x; xx = x*x
    do i=2,30
       j=2*i-1
       fact = fact*j*(j-1)
       pow  = -pow * xx
       term = pow/fact
       sum = sum + term
    end do
    
  • алгоритм 2: Это слегка принятый вариант алгоритма, представленный здесь

    term = x; sum = x; xx = x*x
    do i=2,30
      j=2*i-1
       term = (-term*xx)/(j*(j-1))
       sum = sum + term
    end do
    

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

Именно поэтому алгоритм 2 немного хуже, чем алгоритм 1 .Вычисление term = (-term*xx)/(j*(j-1)) содержит несколько умножений и делений и уже использует приблизительную версию term.Это в отличие от алгоритма 1 , где term = pow/fact - это одиночная операция.

В таблице ниже показаны различия в term, начиная с j=5:

 j  term(alg-1)              term(alg-2)
    0.12345678901234567      0.12345678901234567
 1  5.00000000000000000E+00  5.00000000000000000E+00
 3 -2.08333333333333321E+01 -2.08333333333333321E+01
 5  2.60416666666666679E+01  2.60416666666666643E+01
 7 -1.55009920634920633E+01 -1.55009920634920633E+01
 9  5.38228891093474449E+00  5.38228891093474360E+00
11 -1.22324747975789649E+00 -1.22324747975789627E+00
13  1.96033249961201361E-01  1.96033249961201306E-01
15 -2.33372916620477808E-02 -2.33372916620477738E-02
17  2.14497166011468543E-03  2.14497166011468499E-03

Размер создаваемой ошибки имеет величину:

AE(term(j-2)) * xx / (j*(j-1))

Можем ли мы еще улучшить?

Ответ однозначно да, но нам нужноиспользовать некоторые «трюки».Самый простой способ - использовать более высокую точность, а затем преобразовать все в двойную точность, но это обман!

Ранее упоминалось, что потеря значимости приведет к большему количеству ошибок ив этом комментарии он упоминается начиная с 23! и далее, вы теряете точность в факториале, так как он больше не может быть корректно представлен в виде двоичного числа64.

Мы попробуемчтобы улучшить ответ, отслеживая термин ошибки и используя так называемое компенсированное суммирование или суммирование Кахана .Как указывалось в начале этого поста, обратный факториал по существу приводит к потере точности.

вычисляя обратный факториал: вместо вычисления факториала мы вычислимобратный факториал следующим образом.Представьте, что мы вычислили f ~ 1 / (n-1)! с соответствующим значением ошибки q таким, что f + q ~ 1 / (n-1)! Мы знаем, что «FPR» f можно записать следующим образом: f = a * b ep с a, b, e, p целые числа.Таким образом, можно использовать целочисленное деление для вычисления 1 / n! и соответствующего ему члена ошибки, используя следующий набор операций:

f = (a/n) * b^(e-p)
q = (q + mod(a,n) * b^(e-p))/n

в Фортране, что приводит к следующему коду:

f = 1.0_dp; q = 0.0_dp
do i = 2,10
   ! retrieving the integer from 1/(i-1)!
   a = int(scale(fraction(f),digits(f)),kind=INT64)
   ! computing the error on f while keeping track of the
   ! previous error
   q = (q + scale(real(mod(a,i),kind=dp),exponent(f)-digits(f)))/i
   ! computing f/i resembling 1/i!
   f =      scale(real(a/i     ,kind=dp),exponent(f)-digits(f))
   ! rebalancing the terms
   t = f; f = f + q; q = q - (f - t)
end do

Здесь последняя строка перебалансирует f с ошибкой q.Трюк, вытекающий из суммирования Кахана .

Суммирование Кахана с обратным факториалом ... новый sin:

Теперь это возможнообъединить этот трюк с алгоритмом 2 , чтобы сделать его немного лучше:

pow = x; sum = x; xx = x*x; err = 0.0_dp;
f = 1.0_dp; q = 0.0_dp
do i=2,30
   j=2*i-1
   ! The factorial part
   a = int(scale(fraction(f),digits(f)),kind=INT64)  
   q = (q + scale(real(mod(a,j*(j-1)),kind=dp),exponent(f)-digits(f)))/(j*(j-1))
   f =      scale(real(a/(j*(j-1))   ,kind=dp),exponent(f)-digits(f))
   t = f; f = f + q; q = q - (f - t)

   pow = -pow*xx  ! computes x^j
   ! Kahan summation
   t = pow*f; err = err + (t - ((sum + t) - sum)); sum = sum + t
   t = pow*q; err = err + (t - ((sum + t) - sum)); sum = sum + t
   t = sum; sum = sum + err; err = err - (sum - t)
end do

Этот алгоритм приводит к удивительно хорошим результатам.Отличная точность в квадранте 1 и 2 и немного хуже в третьем квадранте.Четвертый квадрант все еще в порядке.Однако он имеет ужасную точность вблизи Pi и 2Pi.

0 голосов
/ 17 мая 2018

Я не могу понять, где ошибка.

высокая точность sine(5.0) составляет -0.95892427466313846889...

Результат ОП лучше, чем у Проф.

Результат OP находится в пределах 14 ULP от лучшего ответа, тогда как у Prof - 25 ULP.

Так что никаких проблем со стороны ОП. Чтобы получить точное совпадение с ответом профессора, вам придется кодировать неполноценный подход.

Простая возможная причина неполноценного ответа профессора заключается в том, что код Prof зацикливался только при i<=30 (15 терминов, а не 30) - что примерно объясняет разницу. Попробуйте использовать свой код с меньшим количеством итераций и посмотрите, какое количество итераций ближе всего соответствует ответу Prof.


//  sine(5.0)             Delta from correct  Source
//  
//-0.95892427466313846889...    0 000000      calc
//-0.95892427466313823        -23 889...      chux (via some C code)
//-0.95892427466314001       +154 111...      OP
//-0.95892427466313568       -278 889...      Prof
// 0.00000000000000089      +/-89 ...         ulp(5.0)
// 0.00000000000000011      +/-11 ...         ulp(-0.9589)
//   12345678901234567(digit count)

Примечания:
Около x == 5.0, примерно после 17-го семестра, термин temp настолько мал, что не оказывает существенного влияния на результат. Поэтому, безусловно, используются достаточно термины.

При обычном представлении FP единица в последнем месте 5 равна ~ 89e-17. ULP -0,9589, если ~ 11e-17.

0 голосов
/ 17 мая 2018

Я буду эмулировать два алгоритма с некоторым ArbitraryPrecisionFloat, чтобы проиллюстрировать, насколько хуже численное решение для факториала: я буду использовать Squeak Smalltalk здесь, но язык на самом деле не имеет значения, вы можете сделать это в Maple или Python, еслиу вас есть библиотека произвольной точности ...

Ближайшее двоичное число с плавающей запятой к точному результату sin(5) равно -0.9589242746631385.

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

p := 24 to: 64.
e1 := p collect: [:n |
    | x denom temp summ closest |
    closest := (5 asArbitraryPrecisionFloatNumBits: 100) sin asFloat.
    x := 5 asArbitraryPrecisionFloatNumBits: n.
    numTerms := 30.
    denom := (2*numTerms)-1.
    temp := x.
    summ := x.
    3 to: denom by: 2 do: [:i |
        temp := (((0-temp)*(x**2))/(i*(i-1))).
        summ := summ + temp ].
    (summ asFloat - closest) abs].

Затем факториальная перезапись:

e2 := p collect: [:n |
    | x denom temp summ closest fact |
    closest := (5 asArbitraryPrecisionFloatNumBits: 100) sin asFloat.
    x := 5 asArbitraryPrecisionFloatNumBits: n.
    numTerms := 30.
    denom := (2*numTerms)-1.
    temp := x.
    summ := x.
    3 to: denom by: 2 do: [:i |
        fact := ((1 to: i) collect: [:k | k asArbitraryPrecisionFloatNumBits: n]) product.
        temp := ((x ** i)*(-1 ** (i//2)))/fact.
        summ := summ + temp ].
    (summ asFloat - closest) abs].

Затем мы можем отобразить результат на любом языке (здесь Matlab)

p=24:64;
e1=[1.8854927952283163e-8 4.8657250339978475e-8 2.5848555629259806e-8 6.355841153382613e-8 3.953766758435506e-9 2.071757310151412e-8 2.0911216092045493e-9 6.941377472813315e-10 4.700154709880167e-10 9.269683909352011e-10 6.256184459374481e-11 3.1578795134379334e-10 2.4749646776456302e-11 3.202560439063973e-11 1.526812010155254e-11 8.378742144543594e-12 3.444688978504473e-12 6.105005390111273e-12 9.435785486289205e-13 7.617240171953199e-13 2.275957200481571e-14 1.6486811915683575e-13 2.275957200481571e-14 5.1181281435219717e-14 1.27675647831893e-14 1.2101430968414206e-14 1.2212453270876722e-15 2.7755575615628914e-15 5.551115123125783e-16 1.5543122344752192e-15 1.1102230246251565e-16 1.1102230246251565e-16 0.0 1.1102230246251565e-16 0.0 0.0 0.0 0.0 0.0 0.0 0.0];
e2=[9.725292443585332e-7 4.281799078631465e-7 2.721746682476933e-7 1.823107481646602e-7 9.336073392152144e-8 5.1925587718493205e-8 1.6992282803052206e-8 6.756442849642497e-9 5.1179199767048544e-9 3.0311525511805826e-9 1.2180066955025382e-9 6.155346232716852e-10 2.8668412088705963e-10 6.983780220792823e-11 6.476741365446514e-11 3.8914982347648674e-11 1.7473689162272876e-11 1.2084888645347291e-11 4.513389662008649e-12 1.7393864126802328e-12 1.273314786942592e-12 5.172529071728604e-13 2.5013324744804777e-13 1.6198153929281034e-13 6.894484982922222e-14 2.8754776337791554e-14 1.6542323066914832e-14 8.770761894538737e-15 4.773959005888173e-15 2.7755575615628914e-15 7.771561172376096e-16 3.3306690738754696e-16 3.3306690738754696e-16 1.1102230246251565e-16 1.1102230246251565e-16 0.0 0.0 0.0 0.0 0.0 0.0];
figure; semilogy(p,e1,p,e2,'-.'); legend('your algorithm','factorial algorithm'); xlabel('float precision'); ylabel('error')

Ваш алгоритм работает лучше: одна величина ошибки меньше, чем у факториального варианта:

enter image description here

Хуже всего в факториальном варианте является то, что оно зависит от внутренней степенной функции x**power.Эта функция не обязательно отвечает ближайшей с плавающей точкой на точный результат и может варьироваться в зависимости от базовой математической реализации библиотеки.Поэтому требовать немного идентичного результата, который зависит не только от строгого соответствия стандарту IEEE 754, но и от точности, определяемой реализацией, - это действительно глупая вещь - если только у всех студентов нет абсолютно одинакового аппаратного и программного обеспечения - но даже тогда, какой урок это извлекает.что каждый ученый должен знать о плавающей точке?

0 голосов
/ 17 мая 2018

Я закончил тем, что разговаривал с одноклассником, который получил точный ответ.Он сказал мне, что он построил факториальную функцию, а затем предложил мне найти термины, используя оператор if else: if (odd), затем add.иначе если (даже), то вычтите.Поэтому я последовал его предложению и в итоге получил правильный вывод.

Для справки, это тестовые примеры моего профессора:

5 30 -> -0.95892427466313568

4100 -> -0,75680249530792754

Это мой код:

! Sine value using MacLaurin series 

recursive function factorial(n) result (f)
    double precision :: f
    double precision, intent(in) :: n
    if (n <= 0) then
        f = 1
    else
        f = n * factorial(n-1)
    end if
end function factorial

program SineApprox
    implicit none
    integer :: numTerms, i, oddeven
    double precision :: x, summ, factorial, odd, even, power

    ! Read Angle in Radians, and numTerms
    read(*,*) x, numTerms

    ! Computing MacLaurin series
    summ = 0
    power = 1
    do i = 1, numTerms, 1
        oddeven = modulo(i,2)

        if(oddeven == 1) then
            odd = (x**power)/factorial(power)
            summ = summ + odd
        else if (oddeven == 0) then
            even = (x**(power))/factorial(power)
            summ = summ - even
        end if
        power = power + 2
    end do

    print *, summ

end program SineApprox
...