目录
一、傅里叶变换
二、为什么需要傅里叶变换
三、傅立叶变换分类
四、离散傅里叶变换(DFT)
五、快速傅里叶变换(FFT)
5.1 多项式的两种表示方法
5.2 多项式相乘
5.3 多项式的系数表示法转换成点值表示法(Evaluation)
5.4 多项式的点值表示法转换成系数表示法(Interpolation)
六、参考材料
在学习快速傅里叶变换之前,我们首先需要了解傅里叶变换。傅里叶变换,是将信号从时域的表现形式换成频域上的表现形式。如下面的正弦波:
上图为该正弦波在时域上的表现形式,而在频域上的表现形式如下:
可以看到该正弦波在频域上的表现形式就是一个竖直的直线,横轴为频率值,纵轴为幅度值。但是光是这样还不能理解频域的重要性,所以接下来引出一个重要的结论:任何连续周期信号可以由一组适当的正弦曲线组合而成。如下面的左上方的波形就是由其余三个三个波形叠加而成:
所以通过傅里叶变换,我们可以将下面的波形从时域变换到频域(下图右边即为频域的表现形式):
下面是直观一点的图示:
对于时域上不好处理的信号,可能变换到频域上就好处理了。比如我们需要去除某个信号中的无效信号(如特定频段的噪声),假设无效信号对应的频段为0.4~0.7hz,有效信号对应的频段为1.2~2.4hz,那么我们可以去除0.4~0.7频段的所有分量,剩下的就是有用信号,这个操作也被称为滤波。当然这只是傅里叶变换的其中一个作用,它在其他领域也有很多的用处,这里就不一一列举了。
根据原信号的不同类型,我们可以把傅立叶变换分为四种类别:
函数在时(频)域的离散对应于其像函数在频(时)域的周期性。反之连续则意味着在对应域的信号的非周期性。也就是说,时间上的离散性对应着频率上的周期性。同时,注意,离散时间傅里叶变换,时间离散,频率不离散,它在频域依然是连续的。
离散傅里叶变换(DFT),是连续傅里叶变换在时域和频域上都离散的形式,且时域和频域都是周期性的。在形式上,变换两端(时域和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主值序列。即使对有限长的离散信号作DFT,也应当将其看作经过周期延拓成为周期信号再作变换。在实际应用中通常采用快速傅里叶变换以高效计算DFT。
为了在科学计算和数字信号处理等领域使用计算机进行傅里叶变换,必须将函数xn定义在离散点而非连续域内,且须满足有限性或周期性条件。这种情况下,使用离散傅里叶变换(DFT),将函数xn表示为下面的求和形式:
其中Xk是傅里叶幅度。直接使用这个公式计算的计算复杂度为O(n*n),而快速傅里叶变换(FFT)可以将复杂度改进为O(n*lgn)。计算复杂度的降低以及数字电路计算能力的发展使得DFT成为在信号处理领域十分实用且重要的方法。
FFT(Fast Fourier Transformation),中文名快速傅里叶变换,是离散傅氏变换的快速算法,它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。我们以FFT加速多项式相乘来作为例子。
(1)系数表示法
(2)点值表示法
相信大家对两点确定一条直线这个结论非常熟悉,比如y=x+1可以由 (0, 1)、(1, 2)这两点确定,那么一阶多项式y=x+1的点值表示即为 {(0, 1), (1, 2)}。同样地,对于任何n阶多项式,都可以由至少n+1个点来唯一确定,这n+1(或大于n+1)个点即为该n阶多项式的点值表示。
设两个多项式分别为f(x) = x+1, g(x) = x+2,则两个多项式相乘的结果f(x)*g(x) = + 3x + 2
如果用系数表示法:我们要枚举 f 的每一位的系数与 g 的每一位的系数相乘,多项式乘法时间复杂度O (n^2) 。
如果用点值表示法,则有如下结论:
若取f(x)的点值表示为{(0, 1), (1, 2), (2, 3)},g(x)的点值表示为{(0, 2), (1, 3), (2, 4)},那么可以得到f(x)*g(x)的点值表示为{(0, 2), (1, 6), (2, 12)}(这里因为f(x)*g(x)为2阶多项式,所以要取三个点),带入f(x)*g(x)中验算可以看到没有问题。并且复杂度只有的O(n)!
所以,如果使用点值表示法来确定多项式,那么多项式相乘的步骤就可以分为:将多项式的系数表示法转换成点值表示法 --> 点值一一相乘得到多项式乘积的点值表示法 --> 将多项式乘积的点值表示法转换成系数表示法 --> 得到多项式乘积。
所以接下来我们需要去了解如何将多项式的系数表示法转换成点值表示法(FFT),以及如何将多项式的点值表示法转换成系数表示法(IFFT)(这两个步骤也是最核心、最精妙的部分)。
首先是如何利用FFT高效地把系数表示法转换成点值表示法。
对于任意 d 阶多项式P(x),如果要将多项式的系数表示法转换成点值表示法,最容易的方法就是随机取n个点 (x1, x2, .... , xn),然后计算相应的 (P(x1), P(x2), .... , P(xn)),n>=d+1。
我们可以直接写成矩阵乘法的形式来计算:
每个点计算的时间复杂度为O(d),n个点即为O(nd) >= O(d^2) ,这样计算时间复杂度还是很高,有没有更简单的方法呢?
当然是有的,下面就要介绍我们大名鼎鼎的FFT了。
为了引出FFT,我们先举个例子。假设多项式P(x)为一个偶函数,我们计算一组正半轴上的点(x1, x2, ...)对应的函数值(P(x1), P(x2), ...),那么我们可以立刻知道负半轴上的一组点(-x1, -x2, ....)对应的函数值(P(x1), P(x2), ....),若P(x)为奇函数也是一样的,所以在这个例子中我们只需要计算一半的函数值。
推广到更一般的情形,我们可以把d阶多项式P(x)的偶次幂和奇次幂分开,再从奇次幂中提出一个x,那么可以得到一个式子P(x) = P1() + x*P2(),其中P1()和P2()对于自变量x都为偶函数,那么由上面的结论可以得到,我们计算一组正半轴上的点(x1, x2, ....)对应的函数值(P1() + x1*P2(), P1() + x2*P2(), ....),那么我们可以立刻知道负半轴上的一组点(-x1, -x2, ....)对应的函数值(P1() - x1*P2(), P1() - x2*P2(), ....)。令t = ,那么P1(t)和P2(t)的阶数降为d/2(d为多项式P(x)的阶数)。
那么P1(t)和P2(t)的函数值又要怎么求呢,我们此时只需要继续递归地按照上面的方法分解P1(t)和P2(t),直到被分解后的多项式只剩下一个常数。
但是上面说的有一点问题,有人可能已经注意到了,就是在令t = 的时候,t就只能为正数了,不能采用上面的求一组相反数的方法,那么有什么方法可以解决这个问题呢?答案是——使用复数,这也是FFT最天才的想法!我们可以专门挑选一些复数,使得它们平方后,依旧是正负成对出现的。那么,我们要如何挑选这些复数呢?下面我们举一个栗子:
对于上述三阶多项式,我们需要四个点来确定,假设这四个点为{x1,-x1,x2,-x2}:
递归到下一层,我们只需要两个点来确定,即{,}:
为了让其成为相反数,我们令 = :
这样到了最后一层递归,我们只需要一个点来确定,即{}:
假设x1=1,则可以得到下面的内容:
因为之前我们令 = ,所以解出方程 = -1即可得到x2,答案显而易见,x2=i:
所以最终得到的四个点为{1,-1,i,-i},从另外一个视角来看{1,-1,i,-i}相当于方程x^4=1的解。
更进一步,对于一个五阶多项式,我们至少需要6个点来确定它,但由于我们每次递归点的数量都会除2,所以点的个数n选取2的幂次会更方便,这里我们选取8个点:
上图我们可以看到,这相当于在上面3阶多项式的例子的基础上又加了一层,相当于求=1的所有解。
如果推广到d阶多项式,那么就需要n个点来确定(n >= d+1,并且n为2的幂次),那么就相当于解出方程=1的所有解。
那么如何找到方程=1的所有解呢?1的n次方根(即=1的所有解)可以被解释为复平面上沿着单位圆等距排布的一系列点,每个点相隔的夹角,并且每个点在复平面上可以用欧拉公式写成复指数的形式 = (为,k为0, 1, 2, ... , n-1):
若令,则每个点在复平面上的位置可以用 {, , .... , } 来表示:
所以我们可以得到 =1在复平面上的一组解为 {, , .... , }。
那么为什么可以用该单位圆上的一组点来表示呢?首先,因为对于单位圆上的任意点,都有其对应的相反点:
其次,对初始的所有点平方后(按照性质 化简),生成的一组点 {, , .... , }(下面右图)正好满足我们上面的递归性质(点数量减少一半,每个点都有对应的相反点),对生成的点继续平方.... 直到最后只剩一个点{}:
是不是感觉很巧妙!
看懂了上面这些,我们就来介绍FFT算法了。下面是FFT的输入:其中P(x)为n-1阶多项式,最少由n个点值确定,其中n必须为2的幂次,如果不够可以补0;令,那么由上面的结论可知,P(x)的一组点值表示的横坐标为 {, , .... , }。
所以我们最终的目的就是求解横坐标 {, , .... , } 对应的函数值 {, , ... , },从而得到多项式P(x)的一组点值表示 {(, ), (, ), ... , (, )}。 然后我们要继续递归分解,将上面的问题分解成两个子问题和(注意这里它们的自变量变成了),两个子问题只需要求横坐标 {, , .... , }对应的函数值即可:
继续分解,直到只剩一个点 {},因为最后一层一定是一个一阶多项式,即常数,所以直接返回结果给上一层即可,上一层再用下一层的结果计算自己的函数值,直到计算出顶层的函数值。递归公式如下(上面我们介绍过和互为相反数):
为了方便写代码,我们可以将递归公式继续简化成如下形式:
递归公式得出来了,那么写出代码也很容易了,下图是FFT伪代码的实现,将上面讲的一大堆东西浓缩成了这11行代码,是不是非常神奇!
下面是我用C++实现的一个版本(代码写的有点烂,轻喷):
#include
#include
#include
#include
using namespace std;
//定义圆周率派
#define PI acos(-1)
//FFT,最终返回的结果为求得的纵坐标y
void FFT(vector> P, vector> &x, vector> &y, int n){
if(n == 1){
//递归末端,此时P只剩常数,直接返回
y[0] = P[0];
return;
}
//定义复数w,利用欧拉公式w = cos(2Π/n) + i*sin(2Π/n)
complex w(cos(2*PI/n), sin(2*PI/n));
//初始化横坐标
for(int i = 0;i < n;i++){
x[i] = pow(w, i);
}
//分解后的偶次幂和奇次幂
vector> Pe(n/2);
vector> Po(n/2);
//初始化偶次幂和奇次幂
for(int i = 0;i < n/2;i++){
Pe[i] = P[2*i];
Po[i] = P[2*i+1];
}
//子递归传上来的函数值
vector> ye(n/2);
vector> yo(n/2);
vector> x_next(n/2);
//递归
FFT(Pe, x_next, ye, n/2);
FFT(Po, x_next, yo, n/2);
//根据递归公式给y赋值
for(int i = 0;i < n/2;i++){
y[i] = ye[i] + x[i] * yo[i];
y[i+n/2] = ye[i] - x[i] * yo[i];
}
}
int main() {
//输入多项式P(x)=3*x^3 + 4*x^2 + 0*x^1 + 1,可以修改这个数组来计算自己的多项式
vector> P = {1, 0, 4, 3};
//n的长度必须为2的幂次
int n = P.size();
//横坐标
vector> x(n);
//纵坐标
vector> y(n);
FFT(P, x, y, n);
//输出点值坐标
for(int i = 0;i < n;i++){
cout<
对于多项式P(x) = ,其输出结果如下((1, 0)表示复数1+0*i,左边为横坐标,右边为纵坐标,结果有一点点不精确目测是圆周率的原因):
精确一点的结果应该为{(1, 8), (i, -3-3i), (-1, 2), (-i, -3+3i)},大家可以修改P数组来计算自己的多项式。
其次是如何把多项式的点值表示法转换成系数表示法,相当于FFT的逆过程,叫做IFFT(逆快速傅里叶变换)。
上面我们提到过FFT的计算结果和如下矩阵乘法计算的结果一样:
如果令横坐标{x0, x1, ... , xn-1}为 {, , .... , },那么这个矩阵可以改写成如下形式(这也是DFT的基本思想,只不过FFT加速了这一过程):
上图左边的矩阵对应函数值,中间的矩阵对应自变量,也叫DFT矩阵,右边的矩阵对应多项式的系数。在FFT中,我们通过多项式系数得到函数值,而如果想反过来从函数值得到多项式的系数,那么直接把中间的DFT矩阵求个逆不就行了嘛:
而DFT矩阵的逆变换几乎和原来的矩阵一样(这里具体逆变换我就不作证明了,感兴趣的话可以自己去了解一下证明过程),只不过变成了:
下面我们来对比一下FFT和IFFT:
可以看到,IFFT只是将FFT的输入输出对调,将从变成了。所以在伪代码中,我们只需要把输入输出对调,并且内部只需要修改一行,对,你没听错,只需要修改一行就能实现IFFT!(这里的图片中讲的其实有一点问题,是在矩阵外面的,根本不需要放入矩阵中,只需要将最终求得的系数矩阵整体乘以即可,所以下面图片的应该等于,最后将IFFT求得的结果乘以即可):
下面是我用C++实现的一个版本:
#include
#include
#include
#include
using namespace std;
//定义圆周率派
#define PI acos(-1)
//IFFT,最终返回的结果为系数矩阵P
void IFFT(vector> &P, vector> &x, vector> y, int n){
if(n == 1){
//递归末端,直接返回
P[0] = y[0];
return;
}
//定义复数w,这个地方要改一下
complex w(cos(-2*PI/n), sin(-2*PI/n));
//初始化横坐标
for(int i = 0;i < n;i++){
x[i] = pow(w, i);
}
//分解后的偶次幂和奇次幂
vector> ye(n/2);
vector> yo(n/2);
//初始化偶次幂和奇次幂
for(int i = 0;i < n/2;i++){
ye[i] = y[2*i];
yo[i] = y[2*i+1];
}
//子递归传上来的P
vector> Pe(n/2);
vector> Po(n/2);
vector> x_next(n/2);
IFFT(Pe, x_next, ye, n/2);
IFFT(Po, x_next, yo, n/2);
//利用递归公式给y赋值
for(int i = 0;i < n/2;i++){
P[i] = Pe[i] + x[i] * Po[i];
P[i+n/2] = Pe[i] - x[i] * Po[i];
}
}
int main() {
//输入函数值y = {8, -3-3i, 2, -3+3i}
vector> y(4);
y[0].real(8);y[0].imag(0);
y[1].real(-3);y[1].imag(-3);
y[2].real(2);y[2].imag(0);
y[3].real(-3);y[3].imag(3);
//n的长度必须为2的幂次
int n = y.size();
//横坐标
vector> x(n);
//系数矩阵P
vector> P(n);
IFFT(P, x, y, n);
//将最后IFFT求得的结果除以n
complex n_com(n,0);
for(int i = 0;i < n;i++){
P[i] = P[i] / n_com;
}
//输出系数矩阵
for(int i = 0;i < n;i++){
cout<
我们以上面FFT的运行结果作为输入,得到IFFT的结果如下,这和我们上面FFT中的系数矩阵是一致的:
至此,关于FFT和IFFT的内容已经讲解完毕,希望能对你有帮助,如果有问题也欢迎在评论区一起讨论,之后我会继续更新FFT在FPGA中的实现(导师的任务罢了)。
超详细易懂FFT(快速傅里叶变换)及代码实现_Trilarflagz的博客-CSDN博客
快速傅里叶变换(FFT)——有史以来最巧妙的算法?_哔哩哔哩_bilibili
傅里叶分析之掐死教程(完整版)更新于2014.06.06 - 知乎