FFT反复计算函数导数时出现的数值误差

Numerical error building up when computing derivative of function repeatedly with FFT

我编写了一个 C 程序,它使用 FFTW 计算(重复)函数的导数。我正在测试简单的 sin(x) 函数。每一步计算前一步答案的导数。我观察到该错误已建立,并且在 20 步之后该数字是纯粹的垃圾。附件是示例输出。答案(在特定点)应该是 0、+1 或 -1,但事实并非如此。

---- out ----  data '(0) = 1.000000 -0.000000 
---- out ----  data '(1) = 0.000000 -0.000000 
---- out ----  data '(2) = -1.000000 0.000000 
---- out ----  data '(3) = -0.000000 0.000000 
---- out ----  data '(4) = 1.000000 -0.000000 
---- out ----  data '(5) = 0.000000 -0.000000 
---- out ----  data '(6) = -1.000000 0.000000 
---- out ----  data '(7) = -0.000000 0.000000 
---- out ----  data '(8) = 1.000000 -0.000000 
---- out ----  data '(9) = 0.000000 -0.000000 
---- out ----  data '(10) = -1.000000 0.000000 
---- out ----  data '(11) = -0.000000 0.000000 
---- out ----  data '(12) = 1.000000 -0.000002 
---- out ----  data '(13) = 0.000007 -0.000000 
---- out ----  data '(14) = -1.000000 0.000028 
---- out ----  data '(15) = -0.000113 0.000000 
---- out ----  data '(16) = 0.999997 -0.000444 
---- out ----  data '(17) = 0.001798 -0.000000 
---- out ----  data '(18) = -0.999969 0.007110 
---- out ----  data '(19) = -0.028621 0.000004  

我不明白为什么错误不断增加。任何建议深表感谢。我将实函数包装成复数据类型并将虚部设置为零。 这是代码:

# include <stdlib.h>
# include <stdio.h>
# include <time.h>
# include <math.h>
# include <complex.h>
# include <fftw3.h>

int main ( int argc, char *argv[] ){
  int i;
  fftw_complex *in;
  fftw_complex *in2;
  fftw_complex *out;

  double pi = 3.14159265359;
  int nx = 8, k, t, tf = 20;
  double xi = 0, xf = 2*pi;
  double dx = (xf - xi)/((double)nx); // Step size

  complex double  *kx;

  fftw_plan plan_backward;
  fftw_plan plan_forward;

  in = fftw_malloc ( sizeof ( fftw_complex ) * nx );
  out = fftw_malloc ( sizeof ( fftw_complex ) * nx );
  in2 = fftw_malloc ( sizeof ( fftw_complex ) * nx );


  kx = malloc ( sizeof ( complex ) * nx );

  // only need it once, hence outside the loop
  for (k = 0; k < nx; k++){
     if (k < nx/2){
            kx[k] = I*2*pi*k/xf;
     } else if (k > nx/2){
           kx[k] = I*2*pi*(k-nx)/xf;
     } else if (k == nx/2){
        kx[k] = 0.0;
     }
  }

  // create plan outside the loop
  plan_forward = fftw_plan_dft_1d ( nx, in, out, FFTW_FORWARD, FFTW_ESTIMATE );
  plan_backward = fftw_plan_dft_1d ( nx, out, in2, FFTW_BACKWARD, FFTW_ESTIMATE );


  // initialize data
  for ( i = 0; i < nx; i++ )
  {
    in[i] = sin(i*dx) + I*0.0;  // note the complex notation.
  }
//-------------------- start time loop ---------------------------------------//

for (t = 0; t < tf; t++){
  // print input data 
  //for ( i = 0; i < nx; i++ ) { printf("initial data '(%f) = %f %f \n", i*dx, creal(in[i]), cimag(in[i]) ); }

  fftw_execute ( plan_forward );

  for ( i = 0; i < nx; i++ )
  {
    out[i] = out[i]*kx[i]; // for first order derivative
  }

  fftw_execute ( plan_backward );
  // normalize
  for ( i = 0; i < nx; i++ )
  {
    in2[i] = in2[i]/nx;
  }
    printf("---- out ----  data '(%d) = %f %f \n", t, creal(in2[0]), cimag(in2[0]) );
  // overwrite input array with this new output and loop over
  for ( i = 0; i < nx; i++ )
  {
    in[i] = in2[i];
  }
  // done with the curent loop.
}
//--------------------- end of loop ----------------------------------------//

  fftw_destroy_plan ( plan_forward );
  fftw_destroy_plan ( plan_backward );

  fftw_free ( in );
  fftw_free ( in2 );
  fftw_free ( out );

  return 0;
}

使用 gcc 编译 source.c -lfftw3 -lm

更新:这是 M_PI 循环 25 次的输出。相同的错误累积。

---- out ----  data '(0) = 1.000000 0.000000 
---- out ----  data '(1) = -0.000000 -0.000000 
---- out ----  data '(2) = -1.000000 -0.000000 
---- out ----  data '(3) = 0.000000 0.000000 
---- out ----  data '(4) = 1.000000 0.000000 
---- out ----  data '(5) = -0.000000 -0.000000 
---- out ----  data '(6) = -1.000000 -0.000000 
---- out ----  data '(7) = 0.000000 0.000000 
---- out ----  data '(8) = 1.000000 0.000000 
---- out ----  data '(9) = -0.000000 -0.000000 
---- out ----  data '(10) = -1.000000 -0.000000 
---- out ----  data '(11) = 0.000000 0.000000 
---- out ----  data '(12) = 1.000000 0.000000 
---- out ----  data '(13) = -0.000000 -0.000000 
---- out ----  data '(14) = -1.000000 -0.000000 
---- out ----  data '(15) = 0.000000 0.000000 
---- out ----  data '(16) = 1.000000 0.000001 
---- out ----  data '(17) = -0.000002 -0.000000 
---- out ----  data '(18) = -0.999999 -0.000008 
---- out ----  data '(19) = 0.000033 0.000004 
---- out ----  data '(20) = 0.999984 0.000132 
---- out ----  data '(21) = -0.000527 -0.000069 
---- out ----  data '(22) = -0.999735 -0.002104 
---- out ----  data '(23) = 0.008427 0.001099 
---- out ----  data '(24) = 0.995697 0.033667 

确实,细化 pi 的值并不能解决您的问题,即使它提高了 20 次导数的准确性。 问题是重复微分滤波器会放大小误差。要限制此问题,建议引入低通滤波器并使用四精度。引入 条件数 ​​ 的概念有助于理解错误扩大的方式并相应地设置过滤器。 然而,计算 20 阶导数仍将是一场噩梦,因为 computing the 20th derivative is ill-conditionned:这根本不可能,即使对于最干净的实验输入...

1.小错误总是存在的。

导数滤波器,也称为斜坡滤波器,使高频比小频率膨胀更多。通过重复使用斜坡滤波器,高频上最轻微的误差会被显着放大。

让我们通过

打印频率来查看小的初始误差
printf("---- out ----  data '(%d) %d = %20g %20g \n", t,i, creal(out[i]), cimag(out[i]) );

由于使用了pi=3.14159265359,你得到:

---- out ----  data '(0) 0 =         -2.06712e-13                    0 
---- out ----  data '(0) 1 =          6.20699e-13                   -4 
---- out ----  data '(0) 2 =         -2.06823e-13          2.92322e-13 
---- out ----  data '(0) 3 =         -2.07053e-13          1.03695e-13 
---- out ----  data '(0) 4 =         -2.06934e-13                    0 
---- out ----  data '(0) 5 =         -2.07053e-13         -1.03695e-13 
---- out ----  data '(0) 6 =         -2.06823e-13         -2.92322e-13 
---- out ----  data '(0) 7 =          6.20699e-13                    4 

由于 pi 的缺失数字引起的不连续性,所有频率都有小的非空值,这些值通过取导数膨胀。

由于使用了pi=M_PI,这些初始误差较小,但仍然是非空的:

---- out ----  data '(0) 0 =          1.14424e-17                    0 
---- out ----  data '(0) 1 =         -4.36483e-16                   -4 
---- out ----  data '(0) 2 =          1.22465e-16         -1.11022e-16 
---- out ----  data '(0) 3 =          1.91554e-16         -4.44089e-16 
---- out ----  data '(0) 4 =          2.33487e-16                    0 
---- out ----  data '(0) 5 =          1.91554e-16          4.44089e-16 
---- out ----  data '(0) 6 =          1.22465e-16          1.11022e-16 
---- out ----  data '(0) 7 =         -4.36483e-16                    4 

这些小错误和前面的一样被夸大了,问题并没有完全解决。让我们尝试在第一个循环中将这些频率归零:

if(t==0){
     for (k = 0; k < nx; k++){
         if (k==1 || nx-k==1){
            out[k] = I*4.0;
         }else{
            out[k] =0.0;
         }
     }
}

这次第一个循环 t=0 中唯一的非空频率是正确的。再来看第二个循环:

---- out ----  data '(1) 0 =                    0                    0 
---- out ----  data '(1) 1 =                   -4                    0 
---- out ----  data '(1) 2 =                    0                    0 
---- out ----  data '(1) 3 =         -4.44089e-16                    0 
---- out ----  data '(1) 4 =                    0                    0 
---- out ----  data '(1) 5 =          4.44089e-16                    0 
---- out ----  data '(1) 6 =                    0                    0 
---- out ----  data '(1) 7 =                    4                    0 

由于 DFT backward/forward 变换和缩放期间的有限精度计算,会出现小错误并被放大。再次。

2。为了限制错误的增长,可以引入过滤。

大多数实验输入都受到较大的高频噪声的干扰,可以通过应用低通滤波器(例如巴特沃斯滤波器)来降低这种噪声。有关详细信息和替代方案,请参阅 https://www.hindawi.com/journals/ijbi/2011/693795/。该滤波器的特征在于截止频率 kc 和指数,斜坡滤波器的频率响应修改如下:

    //parameters of Butterworth Filter:
    double kc=3;
    double n=16;
    // only need it once, hence outside the loop
    for (k = 0; k < nx; k++){
      if (k < nx/2){
        // add low pass filter:
        kx[k] = I*2*pi*k/xf;
        kx[k]*=1./(1.+pow(k/kc,n));
      } else if (k > nx/2){
        kx[k] = I*2*pi*(k-nx)/xf;
        kx[k]*=1./(1.+pow((nx-k)/kc,n));
      } else if (k == nx/2){
        kx[k] = 0.0;
      }
    }

使用这些参数,20 阶导数的误差从 5.27e-7 到 1.22e-12。

另一个改进是通过不返回导数之间的真实 space 来实现的。这样,就避免了浮点计算过程中的大量舍入错误。在这种特殊情况下,将输入频率归零可确保误差保持为零,但它有点人为......从实际角度来看,如果输入信号以真实 space 形式提供,则使用滤波器计算几乎需要衍生品。

3。由于导数滤波器的条件数

,误差越来越大

导数是一个线性应用,它的特点是condition number。假设输入在所有频率上都受到错误 eps 的困扰。如果第一个频率被放大 alpha 倍,则频率 k 被放大 k*alpha 倍。因此,每次应用导数时,信噪比除以比率 kc(最大频率),称为条件数。如果滤波器重复20次,则信噪比除以kc^20.

双精度数大约 eps=10e-14 精确:这是您可以获得的最佳信噪比!大多数实验输入会比这差得多。例如,灰度图像通常使用 16bit=65536 灰度级进行采样。因此,灰度图像最多 eps=1/65536 精确。同样,典型的音频位深为24,对应eps=6e-8。对于接近分析的输入,可以建议使用四精度,它的精度约为 esp=1e-34...让我们找到 kc^20*eps<1:

的频率
eps=10e-14    kc=5
eps=1/65536   kc=1
eps=1/2^24    kc=2
esp=1e-34     kc=44

因此,如果输入是双精度的,最多只有 4 个频率的 20 阶导数是重要的... 所有高于 4 的频率都必须被过滤掉通过强大的低通滤波器。因此,可以建议使用四精度:参见 fftw's documentation to compile fftw for gcc's quad precision type __float128 linking against quadmath如果输入是图像,计算 20 阶导数就超出了范围:none 的频率将变得非常重要!