最近了解了下fft,在此记录下:
离散傅里叶变换(Discrete Fourier Transform): 缩写为DFT,是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其DTFT的频域采样。在形式上,变换两端(时域和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主值序列。即使对有限长的离散信号作DFT,也应当将其看作其周期延拓的变换,傅立叶变换的结果通常是对称的。在实际应用中通常采用快速傅里叶变换计算DFT。
快速傅里叶变换(Fast Fourier Transform, FFT): 是快速计算序列的离散傅里叶变换(DFT)或其逆变换的方法[1]。傅里叶分析将信号从原始域(通常是时间或空间)转换到频域的表示或者逆过来转换。FFT会通过把DFT矩阵分解为稀疏(大多为零)因子之积来快速计算此类变换。[2] 因此,它能够将计算DFT的复杂度从只用DFT定义计算需要的 $O(n^{2})$,降低到$O(n\log n)$,其中$n$为数据大小。快速傅里叶变换广泛的应用于工程、科学和数学领域。这里的基本思想在1965年才得到普及,但早在1805年就已推导出来。[3] 1994年美国数学家吉尔伯特·斯特朗把FFT描述为“我们一生中最重要的数值算法”[4],它还被IEEE科学与工程计算期刊列入20世纪十大算法
DFT
对于$N$点序列${x[n]}_{0{\leq}n<N}$,它的离散傅里叶变换(DFT)为
\(\begin{aligned}
\hat{x}[k] = \sum\limits_{n=0}^{N-1}e^{\dfrac{-i2{\pi}nk}{N}}x[n] \qquad k = 0,1,..,N-1.
\end{aligned}\)
其中$e$是自然对数的底数,$i$是虚数单位。通常以符号$\mathcal{F}$表示这一变换,即:
\(\begin{aligned}
\hat{x} = \mathcal{F}x
\end{aligned}\)
离散傅里叶变换的逆变换(IDFT)为:
\(\begin{aligned}
x[n] = \dfrac{1}{N}\sum\limits_{k=0}^{N-1}e^{\dfrac{i2{\pi}nk}{N}}\hat{x}[k] \qquad n = 0,1,..,N-1.
\end{aligned}\)
可以记为:
\(\begin{aligned}
x = \mathcal{F}^{-1}\hat{x}
\end{aligned}\)
实际上,DFT和IDFT变换式中和式前面的归一化系数并不重要。在上面的定义中,DFT和IDFT前的系数分别为$1$和$\dfrac{1}{N}$有时会将这两个系数都改成$\dfrac{1}{\sqrt{N}}$。
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def dft(x):
"""
离散傅立叶变换
https://zh.wikipedia.org/wiki/%E7%A6%BB%E6%95%A3%E5%82%85%E9%87%8C%E5%8F%B6%E5%8F%98%E6%8D%A2
https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E5%82%85%E9%87%8C%E5%8F%B6%E5%8F%98%E6%8D%A2
https://pythonnumericalmethods.berkeley.edu/notebooks/chapter24.02-Discrete-Fourier-Transform.html
"""
N = len(x)
# print("dft data size: ", N)
# 循环方式
y = np.zeros(N, dtype="complex_")
for k in range(N):
t0 = x * np.exp(-2j * np.pi * k * np.arange(N)/N)
# t1 = x * np.power(-1, -2 * k * np.arange(n)/n) e^(i * pi) = -1 的替换,没有虚数
y[k] = np.sum(t0)
# 矩阵方式 虽然优雅,内存消耗极大O(n*n)
# n = np.arange(N)
# k = n.reshape((N, 1))
# e = np.exp(-2j * np.pi * k * n / N)
# y = e @ x
return y
FFT
用FFT计算DFT会得到与直接用DFT定义计算相同的结果,最重要的区别是FFT更快,直接用DFT求解需要$O(N^2)$,而用FFT则是能够在$O(N\log{N})$内计算出来。其中库利-图基(Cooley and Tukey)算法是最常见的FFT算法.这一方法以分治法为策略递归地将长度为$N=N_{1}N_{2}$的离散傅里叶变换分解为长度为$N_{1}$的$N_{2}$个较短序列的离散傅里叶变换,因此:
\(\begin{aligned} \hat{x}[k] &= \sum\limits_{n=0}^{N-1}e^{\dfrac{-i2{\pi}nk}{N}}x[n] \\ &= \sum\limits_{m=0}^{N/2-1}e^{\dfrac{-i2{\pi}(2m)k}{N}}x[2m] + \sum\limits_{m=0}^{N/2-1}e^{\dfrac{-i2{\pi}(2m+1)k}{N}}x[2m+1] \\ &= \sum\limits_{m=0}^{N/2-1}e^{\dfrac{-i2{\pi}mk}{N/2}}x[2m] + e^{\dfrac{-i2{\pi}k}{N}}\sum\limits_{m=0}^{N/2-1}e^{\dfrac{-i2{\pi}mk}{N/2}}x[2m+1] \end{aligned}\) 因此将长度为$N$的DFT计算分成了两个相似的DFT,其中一个是奇数序列,一个是偶数序列,因此递归下去时间复杂度接近$O(N{\log}N)$, 由于需要不断的切分两个序列,即要求序列长度满足$N=2^j$,就就是要求长度是2的幂
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def fft(x):
"""
https://pythonnumericalmethods.berkeley.edu/notebooks/chapter24.03-Fast-Fourier-Transform.html
https://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/
A recursive implementation of
the 1D Cooley-Tukey FFT, the
input should have a length of
power of 2.
"""
N = len(x)
if N == 1:
return x
else:
X_even = fft(x[::2])
X_odd = fft(x[1::2])
factor = \
np.exp(-2j*np.pi*np.arange(N) / N)
X = np.concatenate(
[X_even+factor[:int(N/2)]*X_odd,
X_even+factor[int(N/2):]*X_odd])
return X
对于一般长度的序列通常采用布鲁斯坦(bluestein)算法,这个没有看懂。而且各大算法库(numpy,scipy)基本找不到实现,这里记录一个开源免费的实现https://www.nayuki.io/page/free-small-fft-in-multiple-languages。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#
# Computes the discrete Fourier transform (DFT) or inverse transform of the given complex vector, returning the result as a new vector.
# The vector can have any length. This is a wrapper function. The inverse transform does not perform scaling, so it is not a true inverse.
#
def transform(vec, inverse):
n = len(vec)
if n == 0:
return []
elif n & (n - 1) == 0: # Is power of 2
return transform_radix2(vec, inverse)
else: # More complicated algorithm for arbitrary sizes
return transform_bluestein(vec, inverse)
#
# Computes the discrete Fourier transform (DFT) of the given complex vector, returning the result as a new vector.
# The vector's length must be a power of 2. Uses the Cooley-Tukey decimation-in-time radix-2 algorithm.
#
def transform_radix2(vec, inverse):
# Returns the integer whose value is the reverse of the lowest 'width' bits of the integer 'val'.
def reverse_bits(val, width):
result = 0
for _ in range(width):
result = (result << 1) | (val & 1)
val >>= 1
return result
# Initialization
n = len(vec)
levels = n.bit_length() - 1
if 2**levels != n:
raise ValueError("Length is not a power of 2")
# Now, levels = log2(n)
coef = (2 if inverse else -2) * cmath.pi / n
exptable = [cmath.rect(1, i * coef) for i in range(n // 2)]
vec = [vec[reverse_bits(i, levels)] for i in range(n)] # Copy with bit-reversed permutation
# Radix-2 decimation-in-time FFT
size = 2
while size <= n:
halfsize = size // 2
tablestep = n // size
for i in range(0, n, size):
k = 0
for j in range(i, i + halfsize):
temp = vec[j + halfsize] * exptable[k]
vec[j + halfsize] = vec[j] - temp
vec[j] += temp
k += tablestep
size *= 2
return vec
#
# Computes the discrete Fourier transform (DFT) of the given complex vector, returning the result as a new vector.
# The vector can have any length. This requires the convolution function, which in turn requires the radix-2 FFT function.
# Uses Bluestein's chirp z-transform algorithm.
#
def transform_bluestein(vec, inverse):
# Find a power-of-2 convolution length m such that m >= n * 2 + 1
n = len(vec)
if n == 0:
return []
m = 2**((n * 2).bit_length())
coef = (1 if inverse else -1) * cmath.pi / n
exptable = [cmath.rect(1, (i * i % (n * 2)) * coef) for i in range(n)] # Trigonometric table
avec = [(x * y) for (x, y) in zip(vec, exptable)] + [0] * (m - n) # Temporary vectors and preprocessing
bvec = exptable[ : n] + [0] * (m - (n * 2 - 1)) + exptable[ : 0 : -1]
bvec = [x.conjugate() for x in bvec]
cvec = convolve(avec, bvec, False)[ : n] # Convolution
return [(x * y) for (x, y) in zip(cvec, exptable)] # Postprocessing
#
# Computes the circular convolution of the given real or complex vectors, returning the result as a new vector. Each vector's length must be the same.
# realoutput=True: Extract the real part of the convolution, so that the output is a list of floats. This is useful if both inputs are real.
# realoutput=False: The output is always a list of complex numbers (even if both inputs are real).
#
def convolve(xvec, yvec, realoutput=True):
assert len(xvec) == len(yvec)
n = len(xvec)
xvec = transform(xvec, False)
yvec = transform(yvec, False)
for i in range(n):
xvec[i] *= yvec[i]
xvec = transform(xvec, True)
# Scaling (because this FFT implementation omits it) and postprocessing
if realoutput:
return [(val.real / n) for val in xvec]
else:
return [(val / n) for val in xvec]
## 以上所有完整代码见fft.py