Home FFT
Post
Cancel

FFT

最近了解了下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世纪十大算法

离散傅立叶变换
快速傅立叶变换
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
This post is licensed under CC BY 4.0 by the author.

关于WAVE那些事情

机器学习模型评估指标