一、连续傅里叶变换 和 离散傅里叶变换

假设$f$是复平面上的勒贝格可积的函数,则定义$f$的连续傅里叶变换$F$是
对于任意实数$w$,

$$
F(\omega) =
\int_{-\infty} ^{\infty} f(t) e^ { -i \omega t }dt
\tag{1}

$$

在这里,我们解释$\omega$为角频率,$F(\omega)$是复数,并且是信号在该频率成分处的幅度和相位。

傅里叶逆变换是

$$
f(t) = \frac {1} {2 \pi}
\int_{-\infty} ^{\infty} F(\omega)e^{i \omega t}dw
\tag{2}

$$

怎么理解傅里叶变换这2个公式呢。
傅里叶提出,任何连续周期函数,都可以由不同频率不同相位不同幅度的正弦波组合而成。
上述的傅里叶变换公式表达的是,如何求给定的频率,它在信号$f$中的幅度和相位。

在计算机中,我们无法处理连续的积分行为,因此只能进行离散化的采样了。
对于原始信号$f(t)$的离散采样$f(n)$, 我们定义,它的离散傅里叶变换(DFT)为

$$
F[k] = \sum_{n=0} ^{N-1} {f[n]e^{-\frac {2 \pi } {N} i k n}}
\tag{3}

$$

离散傅里叶逆变换(IDFT)为

$$
f[n] = \frac {1}{N} \sum_{k=0} ^{N-1} F[k] e^{\frac{2 \pi} {N} i k n}
\tag{4}

$$

这里k表达的是离散的频率,n是离散的信号采样点,$0 \le k \lt N$, $0 \le n \lt N$。

二、离散傅里叶变换的快速算法

快速傅里叶变换利用的是分治法的思想。

2.1 奇偶分割

假设$N=2^L$, 我们将$f[n]$按n的奇偶分为2组:
令 $r \in [0, 1... N/2-1]$,

$$
\begin{aligned}
F[k] &= \sum_{n=0} ^{N-1} {f[n]e^{-\frac {2 \pi } {N} i k n}}
\ &= \sum_{r=0} ^{\frac{N}{2}-1} {f[2r]e^{-\frac {2 \pi } {N} i k {(2r)}}} +
\sum_{r=0} ^{\frac{N}{2}-1} {f[2r+1]e^{-\frac {2 \pi } {N} i k {(2r+1)}}}
\end{aligned}

$$

我们可以进行一些函数代换,假设$f_1[r]=f[2r], f_2[r]=f[2r+1]$, $F_1[k]$ 和 $F_2[k]$分别是$f_1[n], f_2[n]$的傅里叶变换, 其中$r, k \in [0,1,2... \frac N 2-1]$。
则上式等于

$$
\begin{aligned}
F[k] &= \sum_{r=0} ^{\frac{N}{2}-1} {f[2r]e^{-\frac {2 \pi } {N/2} i k {r}}} +
\sum_{r=0} ^{\frac{N}{2}-1} {f[2r+1]e^{-\frac {2 \pi } {N/2} i k {r}}} * e^{-\frac {2 \pi } {N} i k}
\ &= \sum_{r=0} ^{\frac{N}{2}-1} {{f_1}[r]e^{-\frac {2 \pi } {N/2} i k {r}}} +
e^{-\frac {2 \pi } {N} i k} \sum_{r=0} ^{\frac{N}{2}-1} {f_2[r]e^{-\frac {2 \pi } {N/2} i k {r}}}
\ &= F_1[k] + e^{-\frac {2 \pi } {N} i k} F_2[k]
\end{aligned}

$$

上述式子中,我们成功的将$F[k]$转换成了2个子傅里叶变换之和,我们可以了解到上述式中k的范围在$[0,1,2... \frac N 2-1]$中,因为两个子傅里叶变换的k的区间只有这么大。

我们求出了离散傅里叶变换分解式

$$
F[k] = F_1[k] + e^{-\frac {2 \pi } {N} i k} F_2[k], k \in [0,1,2...\frac N 2 - 1] \tag{5}

$$

,但是k的区间还不够,还差一半。

2.2 求下一半的公式

下面将描述如何求取$F[k], k > N/2 -1$。

$$
e^{-\frac {2 \pi } {N/2} i {(k + N/2)} {r}} = e^{-\frac {2 \pi} {N/2} ikr} * e^{-2 \pi ir}

$$

由欧拉方程$e^{i\theta} = cos\theta + i sin\theta$,另外$r$属于正整数,可知

$$
e^{-2 \pi ir} = cos(-2\pi r) + i sin(-2 \pi r) = 1

$$

因此

$$
e^{-\frac {2 \pi } {N/2} i {(k + N/2)} {r}} = e^{-\frac {2 \pi} {N/2} ikr}

$$

因此

$$
\begin{aligned}
F_1[\frac N 2 + k]
&= \sum_{r=0} ^{\frac{N}{2}-1} {{f_1}[r]e^{-\frac {2 \pi } {N/2} i (k + N/2) {r}}}
\ &= \sum_{r=0} ^{\frac{N}{2}-1} {{f_1}[r]e^{-\frac {2 \pi } {N/2} i k {r}}} \ &= F_1[k]
\end{aligned}

$$

同理,$F_2[\frac N 2 + k] = F_2[k]$,
因此

$$
\begin{aligned}
F[\frac N 2 + k]
&= F_1[\frac N 2 + k] + e^{-\frac {2 \pi } {N} i ({\frac N 2 + k})} F_2[\frac N 2 + k]
\ &= F_1[\frac N 2 + k] + e^{-\pi i } * e^{-\frac {2 \pi } {N} i k} F_2[\frac N 2 + k]
\ &= F_1[\frac N 2 + k] - e^{-\frac {2 \pi } {N} i k} F_2[\frac N 2 + k]
\ &= F_1[k] - e^{-\frac {2 \pi } {N} i k} F_2[k]
\ &= F_1[k] + e^{-\frac {2 \pi } {N} i {(k + \frac N 2)}} F_2[k]
\end{aligned}

$$

离散傅里叶变换的另一半的分解式

$$
\begin{aligned}
F[\frac N 2 + k] &= F_1[k] - e^{-\frac {2 \pi } {N} i k} F_2[k]
\ &= F_1[k] + e^{-\frac {2 \pi } {N} i {(k + \frac N 2)}} F_2[k]
\ &k \in [0,1,2...\frac N 2 - 1] \tag{6}
\end{aligned}

$$

注意到,这里公式的2种方式都是可以滴,有的代码实现就是用第二个子式实现的。

2.3 蝶形运算

为了简化下面的一些符号的表示,我们令

$$
W^k_N = e^{-\frac {2 \pi } {N} i k}

$$

我们定义,蝶形运算公式

$$
\begin{aligned}
F[k] &= F_1[k] + W^k_N F_2[k]
\
F[k + \frac N 2] &= F_1[k] - W^k_N F_2[k] = F_1[k] + W^{k+\frac N 2}_N F_2[k]
\end{aligned}
\tag{7}

$$

它的图示为
butterfly.svg
上图中,$F_1$和$F_2$分别是$F$的奇数部分和偶数部分的子傅里叶变换的结果,他们的大小为$\frac N 2$。

2.4 两个DFT4利用蝶形变换生成DFT8

如下图所示,我们利用2个黑盒生成的DFT4生成DFT8.
首先,我们先取DFT8的原数据的奇数和偶数的信号,然后用某种黑盒过程生成DFT4,然后再利用蝶形变换生成DFT8.
fft4-8.svg

2.5 利用4个DFT2生成2个DFT4,再生成DFT8

上述说的黑盒过程,可以进一步利用蝶形变换分解。下图的绿色部分标记出了新增的蝶形变换部分。 注意到,上面的序号发生了变化,每个4个DFT的奇数取出来。
dft2-8.svg

2.6 将DFT2直接转换为蝶形变换

通过上述三个递归步骤,可以了解到FFT的详细做法了。
利用分治思想,一直2分,直到只有N=2时,即可直接变为蝶形变换。
dft8.svg

2.7 序列输入顺序

我们可以注意到,最原始的DFT8在完全分解成蝶形变换时,$f[t],\space \space t \in [0, 8)$,序列发生了变化,因为除了N=2之外,每一次分解为蝶形变换时,因为取奇偶导致顺序发生了错位。
这种看起来没啥规律,但是很明显,这种规律交换的蝶形运算,最后这个序号肯定有规律的。规律刚好是二进制的位反 (bitreverse)。
如下图所示
afc5d934cf6e506102fa764e3074c0db.png

三、FFT具体实现

我们注意到,上述过程
FFT的具体蝶形如何做,有很多种方案,下面这个链接的PDF里总结了好多种。

Fast Fourier Transform

fft-enc11.pdf

下面我着重讲述2个最常见的类型,第一个是最原版的先换顺序然后进行蝶形变换。第二个是GPU中最常见的FFT实现。
首先,为了简化细节,我直接将第二章描述的蝶形变换内部细节标记为一个黑盒,如下图。
butterfly-simple.svg

3.1 经典快速傅里叶变换算法(Cooley-Tukey FFT)

经典FFT算法,就是先进行一次序号的位反计算出新的序号,然后按照鲽形图进行递归或者非递归计算。
下图是Cooley-Tukey FFT的DFT8的详细流程图
Cooley-TukeyFFT.svg

我们可以注意到,在Cooley-Tukey FFT的实现中,除了一开始需要映射表的序号之外,所有的蝶形变换中,输入的两个元素的序号和输出的2个元素的序号没有发生变化。
例如DFT2->DFT4中,4和6元素的下标分别是1, 3, 他们进行蝶形变换后输出的下标也是1, 3

3.2 适合GPU的FFT(Stockham FFT)

上述的FFT,输入数据要进行一次序号重映射行为,在GPU端,这种映射可通过查表实现。但是,有一种Stockham FFT,它提出了一种巧妙的映射方式,使得id能直接计算映射,这在GPU中相比查表会更加快捷,对于大型FFT来说,构建表传输表都比较慢。
下图来自其他地方,这两个图简单明了的描述了区别。
11869ab0191c205973171e514efef6d4.png

下图是Stockham FFT的详细流程图。
StockhamFFT.svg

下面,我们解析一下Stockham FFT的流程图
我们从第0轮开始分析,
首先,数据被分成8组,每个数据单独成组。然后,(0,4)(1,5)(2,6)(3,7)分别是DFT2的奇偶子项,他们分别做蝶形变换,完成了第0轮。
第一轮中,数据被分成了4组,$(0,4)(1,5)(2,6)(3,7)$, 其中$[(0,4)(2,6)]$与$[(1,5)(3,7)]$分别是DFT4的奇偶子项,然后他们分别做蝶形变换,完成第一轮。
以此类推。。。

下面,我们解析以下如何计算Stockham FFT的映射。

3.2.1 Stockham蝶形变换的输入下标的求取

我们观察可以发现以下特征:

  • 所有蝶形变换的2个输入对的下标都符合$x, x + \frac N 2$的形式,$N$是数据的长度。例如上面的图里N=8.
  • 所有蝶形变换的第一个输入的下标组合起来刚好是$[0, 1, 2, ... \frac N 2)$的形式。

根据这2点,我们可以在代码里确定蝶形变换的输入的下标了。

假设某个蝶形变换左边输入的下标为$x$,则第二个输入的下标是$x + \frac N 2$, 其中$x \in [0,1...\frac N 2]$。

注意到每一轮FFT合并过程中,都执行了$N/2$次蝶形变换,因此我们可以定义:

假设某个蝶形变换左边输入的下标为$x$,则称这个蝶形变换为第$x$个蝶形变换。

然后我们来求,蝶形变换的输出的下标。

3.2.2 Stockham蝶形变换的输出下标的求取

可以观察到:

  1. 每个蝶形变换的2个输出的下标间隔会随着每一轮蝶形变换而呈指数扩张。例如 DFT1 -> DFT2中,2个输出的下标间隔是1. DFT2->DFT4中,2个输出下标的间隔是2。 DFT4->DFT8中,2个输出下标间隔是4.
  2. 每一轮内,每组DFT内的蝶形变换,都是左边输出排在右边输出前面。然后,每一轮内不同组之间延后摆放。

因此,我们可以求出以下结论:

第m轮(m从0开始),该轮内的子DFT的大小是$p = 2^m$, 输出的DFT的大小是$2^{m+1}$。
第m轮(m从0开始),第$x$个蝶形变换,它在某个子DFT内的下标是$k = x mod p$。
第m轮(m从0开始),第$x$个蝶形变换的左边输出的下标为

$$
floor(\frac x {2^m}) * 2^{m+1} + x mod(2^m) =(x-k)*2 + k

$$

右边输出的下标为

$$
floor(\frac x {2^m}) * 2^{m+1} + x mod(2^m) + 2^m =(x-k) * 2 + k + p

$$

3.2.3 Stockham根据输出下标求取输入的下标

在我浏览的一些实现中,GPU是通过输出的下标进行分发任务的,也就是说,每个GPU函数中输出的下标确定了,这时候我们需要反向求出蝶形变换的2个输入下标,以及该输出在蝶形变换中的正负。
我们通过计算和分析可得出

假设第m轮(m从0开始),第x个输出下标所属的

  • 蝶形变换的序号是 $floor(\frac x {2^{m+1}})*2^m + x mod(2^m)$
  • 根据蝶形变换下标的定义可知 左边的输入的下标就是蝶形变换的序号
  • 右边的输入的下标是 $floor(\frac x {2^{m+1}})*2^m + x mod(2^m) + \frac N 2$
  • 输出的正负性是 $x mod 2^{m+1}<2^m$ 为正,否则为负。(一般这种实现,不需要利用正负性,直接利用公式$(7)$的第二种形式即可)

Unity中的FFT实现