【问题标题】:Example python nfft fourier transform - Issues with signal reconstruction normalization示例 python nfft 傅立叶变换 - 信号重建归一化问题
【发布时间】:2021-07-24 18:46:47
【问题描述】:

我为nfftscipy.fft 编写了一个完整的工作示例。 在这两种情况下,我都从一个带有少量噪声的简单一维正弦信号开始,进行傅立叶变换,然后逆向重建原始信号。

这是我能做到的尽可能简洁易读的代码:

import numpy
import nfft
import scipy
import scipy.fft
import matplotlib.pyplot as plt

if True: #<--- Ensure non-global namespace
    #Define signal:
    x = -0.5 + numpy.random.rand(1000)
    #x = numpy.linspace(-.5, .5, 1000) #--> in case we want to run uniform time domain
    f = numpy.sin(10 * 2 * numpy.pi * x) + .1*numpy.random.randn( 1000 ) #Add some  'y' randomness to the sample

    #prepare wavenumbers for transform:
    N = len(x)
    k = - N // 2 + numpy.arange(N) 
    #print ('k', k) #---> Uniform Steps [-500, -499, ...0..., 499,500]

    f_k = nfft.nfft_adjoint(x, f, len(k), truncated=False )

    #plot transform
    plt.figure()
    plt.plot(k, f_k.real, label='real')
    plt.plot(k, f_k.imag, label='imag')
    plt.legend()

    #Reconstruct the original signal with nfft
    f_recon = nfft.nfft( x, f_k ) / 2000

    #Plot original vs reconstructed
    plt.figure()
    plt.title('nfft')
    plt.scatter(x, f, label='f(x)')
    plt.scatter(x, f_recon, label='f_recon(x)', marker='+')
    plt.legend()

if True: #<--- Ensure non-global namespace
    #Define signal:
    x = numpy.linspace(-.5, .5, 1000)
    f = numpy.sin(10 * 2 * numpy.pi * x) + .1*numpy.random.randn( 1000 ) #Add some 'y' randomness to the sample

    #prepare wavenumbers for transform:
    N = len(x)
    TimeSpacing = x[1] - x[0]
    k = scipy.fft.fftfreq(N, TimeSpacing)
    #print ('k', k) #---> Confusing steps: [0,1,...500,-500,-499,...-1]
    
    f_k = scipy.fft.fft(f)

    #plot transform
    plt.figure()
    plt.plot(k, f_k.real, label='real')
    plt.plot(k, f_k.imag, label='imag')
    plt.legend()

    #Reconstruct the original signal with scipy.fft
    f_recon = scipy.fft.ifft(f_k)

    #Plot original vs reconstructed
    plt.figure()
    plt.title('scipy.fft')
    plt.scatter(x, f, label='f(x)')
    plt.scatter(x, f_recon, label='f_recon(x)', marker='+')
    plt.legend()

plt.show()

以下是相关的生成图:

nfft 重建似乎无法正常化。 我任意将震级除以 2000 只是为了让它们能够很好地绘制。 什么是正确的归一化常数?

nfft 似乎也没有重现原始点。 即使我得到了正确的归一化常数,我也无法将原始点放回这里。

我做错了什么,我该如何解决?

【问题讨论】:

  • 从这个不相关的github issue 我推断归一化常数应该是'N = len(x)',在我的例子中是1000。当时域一致时,具有归一化 N 的 Nfft 似乎可以工作。当时域不均匀时,它似乎不能很好地工作,并且幅度至少增长了 2 倍。

标签: python scipy fft normalization nfft


【解决方案1】:

Bob 已经发了an excellent answer,这只是补充一些细节,希望能有所启发。

首先,比较计算出的频率分量的两个图。请注意,用于 NFFT 的噪声比用于常规 FFT 的噪声大得多。您正在为来自噪声样本的采样信号估计这些频率分量,在一种情况下,样本是规则间隔的,在另一种情况下,它们是随机间隔的。众所周知,常规采样比随机采样更有效(高效意味着您需要更少的样本来获得相同数量的信息)。因此,预计随机采样会产生更多噪声的结果。

我们可以根据 NFFT 估计的频率分量计算“正常”逆 FFT:

f_recon = numpy.fft.fftshift(numpy.fft.ifft(numpy.fft.ifftshift(f_k)))
x_recon = numpy.linspace(-.5, .5, N)

我使用了ifftshift,因为 NFFT 将 k 定义为从 -N/2N/2-1,而 FFT 将它定义为从 0N-1ifftshift 交换信号的两半以将第一半变成第二半(kN/2N-1 等于 -N/2-1)。我还在 IFFT 的结果上使用了fftshift,因为同样的事情适用于时间轴,它将原点从第一个样本移动到序列的中间。

注意f_recon 的嘈杂程度。这是因为我们可以对非均匀采样信号做出的f_k 估计不佳。还有一个符号错误,当我们比较f_k 的两个估计值时,我们已经可以观察到这个错误。这来自伴随的 NFFT,在指数中与逆 DFT 具有相同的符号,这实际上意味着 f_recon 被翻转 w.r.t。 x.

如果我们增加随机样本的数量,我们可以获得更好的估计:

import numpy
import nfft
import matplotlib.pyplot as plt

#Define signal:
N = 1024 * 16  # power of two for speed
x = -0.5 + numpy.random.rand(N)
f = numpy.sin(10 * 2 * numpy.pi * x) + .1 * numpy.random.randn(N) # Add some  'y' randomness to the sample

#prepare wavenumbers for transform:
k = - N // 2 + numpy.arange(N) 

N2 = 1024
f_k = nfft.nfft_adjoint(x, f, N2, truncated=False)

#Reconstruct the original signal with nfft
#   (note the minus sign to flip the signal, in reality we should flip x)
f_recon = - numpy.fft.fftshift(numpy.fft.ifft(numpy.fft.ifftshift(f_k))) / (N / N2)
x_recon = numpy.linspace(-.5, .5, N2, endpoint=False)

#Plot original vs reconstructed
plt.figure()
plt.title('nfft')
plt.scatter(x[:N2], f[:N2], label='f(x)') # don't plot all samples, there's too many
plt.scatter(x_recon, f_recon, label='f_recon(x)')
plt.legend()
plt.show()

【讨论】:

  • 谢谢@Cris Luengo,如果我们可以使用在不同于 x 的位置重建的函数,这是一个选项。只有一个观察,你的x_recon = linspace(-.5, .5, len(f_recon)),应该是linspace(-.5, .5, len(f_recon), endpoint=True)
  • @Bob:我不确定这是不是真的。 IFFT 返回样本 n=0..N-1,其中 n=0 和 n=N 将相等(即它是周期性的)。如果您将 n=0 转换为 x=-0.5 并将 n=N 转换为 x=0.5,那么您不会输入终点。但是您也可以为需要包含的要点提出理由。我必须仔细阅读伴随 NFFT 的数学定义,以确定哪个是正确的。无论如何,它不会对这张图表产生显着影响,因为有很多点。
  • 好吧,我可以问你一些问题,(1)linspace(-.5, .5, N)中连续元素之间的间隔是多少,(2)exp(-.5*2j*pi)e(.5*2*pi)不一样?包括两者不会使转换矩阵奇异? linspace(-.5, .5, N) 是否包含0(DC 级别),例如(N=2)?
  • @Bob:我刚刚了解到 NumPy linspace 默认包含端点,我不知何故假设它会像 arange 一样而不包含它。对于偶数点,如果要对原点进行采样,则不应包括终点,对于奇数,应包括终点。设置x_recon = ( numpy.arange(N) - N//2 ) / N 可能会更好。 arange(N)-N//2 部分对应于 fftshift(ifft(...)) 的假设。
【解决方案2】:

上面提到的包没有实现逆nfft

ndftf_hat @ np.exp(-2j * np.pi * x * k[:, None]) ndft_adjointf @ np.exp(2j * np.pi * k * x[:, None])

k = -N//2 + np.arange(N)A = np.exp(-2j * np.pi * k * k[:, None])

A @ np.conj(A) = N * np.eye(N)(数字检查)

因此,对于随机x,伴随变换等于逆变换。给定的参考论文提供了一些选项,我实现了Algorithm 1 CGNE, from page 9

import numpy as np # I have the habit to use np
def nfft_inverse(x, y, N, w = 1, L=100):
    f = np.zeros(N, dtype=np.complex128);
    r = y - nfft.nfft(x, f);
    p = nfft.nfft_adjoint(x, r, N);
    r_norm = np.sum(abs(r)**2 * w)
    for l in range(L):
        p_norm = np.sum(abs(p)**2 * w);
        alpha = r_norm / p_norm
        f += alpha * w * p;
        r = y - nfft.nfft(x, f)
        r_norm_2 = np.sum(abs(r)**2 * w)
        beta = r_norm_2 / r_norm
        p = beta * p + nfft.nfft_adjoint(x, w * r, N)
        r_norm = r_norm_2;
        #print(l, r_norm)
    return f;

算法收敛缓慢且效果不佳

    plt.figure(figsize=(14, 7))
    plt.title('inverse nfft error histogram')
    #plt.scatter(x, f_hat, label='f(x)')
    h_hat = nfft_inverse(x, f, N, L = 1)
    plt.hist(f_hat - numpy.real(h_hat), bins=30, label='1 iteration')
    h_hat = nfft_inverse(x, f, N, L = 10)
    plt.hist(f_hat - numpy.real(h_hat), bins=30, label='10 iterations')
    h_hat = nfft_inverse(x, f, N, L = 1000)
    plt.hist(f_hat - numpy.real(h_hat), bins=30, label='1000 iterations')
    plt.xlabel('error')
    plt.ylabel('occurrencies')
    plt.legend()

我也尝试使用scipy minimization,以显式最小化剩余||nfft(x, f) - y||**2

import numpy as np # the habit
import scipy.optimize
def nfft_gradient_descent(x, y, N, L=10, tol=1e-8, method='CG'):
    '''
    compute $min || A @ f - y ||**2 via gradient descent
    the gradient is
    
    `A^H @ (A @ f - y)`
    
    Multiply by A using nfft.nfft
    
    '''
    def cost(fpack):
        f = fpack[0::2] + 1j * fpack[1::2]
        u = np.sum(np.abs(nfft.nfft(x, f) - y)**2)
        return u
    def grad(fpack):
        f = fpack[0::2] + 1j * fpack[1::2]
        r = nfft.nfft(x, f) - y
        u = nfft.nfft_adjoint(x, r, N)
        return np.stack([np.real(u), np.imag(u)], axis=1).reshape(-1)
    
    x0 = np.zeros([N, 2])
    result = scipy.optimize.minimize(cost, x0=x0, jac=grad, tol=tol, method=method, options={'maxiter': L, 'disp': True})
    return result.x[0::2] + 1j * result.x[1::2];

结果看起来差不多,如果你愿意,你可以自己尝试不同的方法或参数。但我认为转换是病态的,因为转换后的残差大大减少,但重建值的残差很大。

编辑 1

你发现算法没有真正的逆,这基本上是真的吗?我无法获得我的原始积分? x != nfft(nfft_adjoint(x))

请查看参考paper的第2.3节

数值比较

Cris Luengo answer 提到了另一种可能性,也就是说,您可以使用 ifft 在等距点处重建重采样版本,而不是在点 f 处重建。所以你已经有了三个选择,我会做一个快速的比较。请记住,此处显示的图基于在 16k 样本中计算的 NFFT,而这里我使用的是 1k 样本。

由于 FFT 方法使用不同的点,我们无法与原始信号进行比较,我要做的是与没有噪声的调和函数进行比较。噪声的方差为0.01,因此精确重构会导致该均方误差。


N = 1024
x = -0.5 + numpy.random.rand(N)
f_hat = numpy.sin(10 * 2 * numpy.pi * x) + .1*numpy.random.randn( N ) #Add some  'y' randomness to the sample

k = - N // 2 + numpy.arange(N)
f = nfft.nfft(x, f_hat)

print('nfft_inverse')
h_hat = nfft_inverse(x, f, len(x), L = 10)
print('10 iterations: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x) - numpy.real(h_hat))**2))
h_hat = nfft_inverse(x, f, len(x), L = 100)
print('100 iterations: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x) - numpy.real(h_hat))**2))
h_hat = nfft_inverse(x, f, len(x), L = 1000)
print('1000 iterations: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x) - numpy.real(h_hat))**2))
print('nfft_gradient_descent')
h_hat = nfft_gradient_descent(x, f, len(x), L = 10)
print('10 iterations: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x) - numpy.real(h_hat))**2))
h_hat = nfft_gradient_descent(x, f, len(x), L = 100)
print('100 iterations: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x) - numpy.real(h_hat))**2))
h_hat = nfft_gradient_descent(x, f, len(x), L = 1000)
print('1000 iterations: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x) - numpy.real(h_hat))**2))


#Reconstruct the original at a spaced grid based on nfft result using ifft
f_recon = - numpy.fft.fftshift(numpy.fft.ifft(numpy.fft.ifftshift(f_k))) / (N / N2)
x_recon = k / N; 
print('using IFFT: ', np.mean((numpy.sin(10 * 2 * numpy.pi * x_recon) - numpy.real(f_recon))**2))

结果:

nfft_inverse
10 iterations:  0.0798988590351581
100 iterations:  0.05136853850272318
1000 iterations:  0.037316315280700896
nfft_gradient_descent
10 iterations:  0.08832834348902704
100 iterations:  0.05901599049633016
1000 iterations:  0.043921864589484
using IFFT:  0.49044932854606377

另一种查看方式是

plt.plot(numpy.sin(10 * 2 * numpy.pi * x_recon), numpy.real(f_recon), '.', label='ifft')
plt.plot(numpy.sin(10 * 2 * numpy.pi * x), numpy.real(nfft_gradient_descent(x, f, len(x), L = 5)), '.', label='gradient descent L=5')
plt.plot(numpy.sin(10 * 2 * numpy.pi * x), numpy.real(nfft_inverse(x, f, len(x), L = 5)), '.', label='nfft_inverse L=5')
plt.plot(numpy.sin(10 * 2 * numpy.pi * x), np.real(f_hat), '.', label='original')
plt.legend()

尽管 IFFT 矩阵的条件更好,但它会导致信号重建效果更差。同样从最后一张图中可以看出,有轻微的衰减。可能是由于系统的能量泄漏到虚部(我的代码中有错误??)。只是一个快速测试,将其乘以 1.3 会得到更好的结果

【讨论】:

  • 这个答案很棒。只是一个关于令人困惑的语言的快速注释,而不用担心实际的数学:nfft.nfft_adjoint scipy.fft.fft 和 nfft.nfft scipy.fft.ifft。 “正向”和“反向”这两个词是模棱两可的,并且是基于惯例的。 scipy 和 nfft 都实现了两个方向(如我在原始问题中的代码所示)。
  • 那么您发现算法不存在真正的逆,这基本上是真的吗?我无法获得我的原始积分? x != nfft(nfft_adjoint(x))
  • 很好的比较,谢谢!迭代逆方法试图找到生成给定f_k 的样本集,因此将趋向于 0.01 方差(输入样本为零误差),而 IFFT 仅显示f_k 估计中的噪声量原始正弦曲线的傅里叶变换。我当然不建议将这条路线用于任何数值分析。
  • @DAdams:您说“nfft.nfft_adjoint scipy.fft.fft”,但请注意两者的指数符号不同! nfft.nfft 与 DFT 具有相同的方程,只是交换 k 和 x。我不知道为什么NFFT是这样定义的,但我觉得很混乱!
  • 谢谢,这是一些努力,但我很满意它值得:)
猜你喜欢
  • 1970-01-01
  • 2019-07-02
  • 2016-04-29
  • 1970-01-01
  • 2015-04-04
  • 2022-01-02
  • 1970-01-01
  • 2014-08-06
  • 1970-01-01
相关资源
最近更新 更多