天天看点

快速入门 FFT快速入门 FFT

快速入门 FFT

本文适合不想完全了解 FFT 原理不过想实现 FFT 算法的人

FFT 简介

FFT 也就是常说的快速傅里叶变换,其实本质上和傅里叶变换没有什么不同,也就是通过算法减少了一些乘法和加法的运算;DFT 的时间复杂度是 N² 级别的,而 FFT 的却是对数级别的复杂度,从而在 FFT 在数据量大的时候运算时间并不会高太多,这就使得 FFT 具有了在工程上的使用价值

FFT 算法简介

快速入门 FFT快速入门 FFT

上面这张图是来自维基的介绍,简单来说的话,FFT 将数据按照索引地址按奇数和偶数分为两半,这两半数据还可以继续抽取,直到最后分解为单独的数据;然后利用了单位根的一些特性,减少了一些乘加步骤

但是顺序输入的数据,最后得到的结果是乱序的;为了得到顺序的输出,一般我们会将输入数据提前进行整理;因为 FFT 的输出索引地址会位反转,可以利用这个特性提前整理好输入数据,例如:输入 [1 2 3 4 5 6 7 8] 就整理为 [1 5 3 7 2 6 4 8]

FFT 第一个版本实现

最开始我为了能弄懂 FFT ,我就通过手算来了解 FFT 的过程,也就是通过上面那张图中的公式,一级级计算 FFT 的结果

最后根据手算的步骤实现了下面这个版本,其中单位根的复指数计算就直接手动计算完存表表了,在实现后发现只能够适用 4 个数的输入

import cmath

table = [1+0j,2+0j,3+0j,4+0j]
rotate_table = [1+0j,0-1j,-1+0j,0+1j]
result = []

N = 4
k = 0
r = 0
tmp = 0
while k < len(table):
    while r < len(table)/2:
        tmp = tmp + table[2*r]*rotate_table[(2*r*k)%4] + table[2*r+1]*rotate_table[((2*r+1)*k)%4]
        r = r + 1
    result.append(tmp)
    tmp = 0
    k = k + 1
    r = 0

print(result)
           

运行结果如下

PS D:\Files\dsp\python> python.exe .\my_fft.py
[(10+0j), (-2+2j), (-2+0j), (-2-2j)]
           

matlab fft 的结果如下

>> x = [1 2 3 4];X = fft(x)
X =

   10 +  0i   -2 +  2i   -2 +  0i   -2 -  2i
           

FFT 第二个版本的实现

快速入门 FFT快速入门 FFT

最开始看到书上的 FFT 特点的时候,还不是太了解,所以上面的那个实现非常的简陋

原址运算的特点使得 FFT 占用空间为 N,每个蝶形运算结束后,输出节点的两个值直接放到原输入的两个节点中;这个特点给了我对算法的基本思路,算法是从整理输入数据顺序开始的

不满足 2 的 N 次方的情况直接不考虑了

通过观察,可以看出每层蝶形运算的规律:如果将每层的蝶形运算分组的话,每层的蝶形运算的组数和每组的蝶形运算次数的乘积是一定的,为 N/2。具体可以参考这篇文章

基于这些特点就有了下面这个第二个版本的 FFT

import cmath
import math

def reverseBits(n, bits): # 整理数据子函数
    return int(bin(n)[2:].rjust(int(bits), '0')[::-1], base=2)

table = [1,2,3,4,5,6,7,8] # 输入数据
result = [] # 存储结果
L = math.log(len(table), 2) # 蝶形运算的层数
N = len(table) # 输入数据的长度

tmp = 0
tmp_i = 0
while tmp_i < N/2: # 整理数据过程
    tmp = table[tmp_i]
    table[tmp_i] = table[reverseBits(int(tmp_i), int(L))]
    table[reverseBits(tmp_i, L)] = tmp
    tmp_i = tmp_i + 1

print(table)

k = 0 # 一组里的 k 次蝶形
m = 1
i = len(table)/2 # i 组蝶形
j = 0 # 第 j 组蝶形

while m <= L:
    while j < i: # i组蝶形 4/2=2
        while k < N/2/i: # k次蝶形运算
            r = (j*(2**m) + k)*(2**(L-m))
            tmp_1 = table[j*(2**m) + k]
            tmp_2 = table[j*(2**m) + k+2**(m-1)] * cmath.exp(-1j*2*cmath.pi*r/N)
            table[j*(2**m) + k] = tmp_1 + tmp_2
            table[j*(2**m) + k+2**(m-1)] = tmp_1 - tmp_2
            k = k + 1
        j = j + 1
        k = 0
    i = i/2
    j = 0
    m = m+1

print(table)
           

运行结果如下:

PS D:\Files\dsp\python> python.exe .\my_fft_2.py
[1, 5, 3, 7, 2, 6, 4, 8]
[(36+1.3471114790620885e-14j), (-4.000000000000009+9.656854249492383j), (-4.000000000000006+3.9999999999999982j), (-4.000000000000005+1.656854249492377j), (-4-1.0042103753008295e-14j), (-3.9999999999999942-1.6568542494923832j), (-3.999999999999994-4.000000000000002j), (-3.999999999999991-9.656854249492376j)]
           

matlab 中的结果如下:

>> x = [1 2 3 4 5 6 7 8];X = fft(x)
X =

 Columns 1 through 4:

   36.0000 +  0.0000i   -4.0000 +  9.6569i   -4.0000 +  4.0000i   -4.0000 +  1.6569i

 Columns 5 through 8:

   -4.0000 +  0.0000i   -4.0000 -  1.6569i   -4.0000 -  4.0000i   -4.0000 -  9.6569i
           

和 matlab 中的对比还存在精度的问题,这个与 python 的 cmath 库提供的指数运算有关,我觉得可以考虑像第一版的实现一样,将指数运算优化掉,不过暂时不打算不去考虑这个问题了

继续阅读