算法的故事 / 第一季第二季目录0 / 30 章已读

第二季 · 第 32 章

把时间切成频率

《算法的故事》第二季 · 第一篇:数学家的秘密武器——傅里叶变换的速度革命1965年,哥伦比亚大学计算中心的 IBM 7094 大型计算机。照片展示的是当时真实的计算设备,而非本章所述研究现场。图片资料:哥伦比亚大学计算机历史档案。1963年,美国。一场关于核试验监测的科学讨论中,一位数学家在笔记纸上写下了一组计算公式。那时候,想分析一段复杂的地震波形,计算机可能需要执行数以百万计甚至更多的运算。问题并不是人类不知道怎样计算。恰恰相反,人们早就知道答案应该怎样计算,只是运算代价太高。数学家在纸上写下的那个想法,却可能让同样的问题只需要原来很小一部分的运算量。这个故事的主人公,是约翰·图基(John Tukey)。不过,这场计算革命的真正历史,比1963年还要久远。它要从法国的一位热传导研究者讲起,经过德国的一位天文学家,再穿过两次世界大战,最终抵达电子计算机的时代。而这场漫长故事的中心,是一个看似奇怪的问题:一条不断变化的曲线,究竟隐藏着多少种不同的周期?

第一幕:一个研究热的数学家,发现了波的秘密

约瑟夫·傅里叶(Joseph Fourier,1768—1830),法国数学家与物理学家。他对热传导方程和三角级数的研究,为后来的信号分析建立了重要数学基础。十九世纪初,法国。工业革命正改变欧洲的生产方式。蒸汽机、金属加工、机械制造不断发展,人们对热的认识也越来越重要。一块烧红的铁,放在寒冷的环境里会慢慢冷却。如果只加热金属棒的一端,热量会逐渐向另一端传递。对于日常生活,这些现象并不神秘。可对于数学家来说,它们提出了一组极其复杂的问题。如果我们知道一根金属棒最初每个位置的温度,能否算出十秒之后各处的温度?一分钟之后呢?如果这根金属棒的两端保持在固定温度,又该怎样描述它内部的变化?这些问题吸引了 约瑟夫·傅里叶。傅里叶的经历远不只是研究数学。他出生于1768年,经历过法国大革命时期,也曾随拿破仑的埃及远征活动前往埃及。后来,他担任过行政职务,同时继续从事数学研究。他的工作充满了一个时代的典型特征:人们希望使用数学,不只是解释抽象图形,还要理解真实的自然现象。

一块金属板上的难题

想象一根细长的金属棒。我们将它左边的一部分加热,右边保持相对低温。在初始时刻,温度分布可能是一条非常不规则的曲线。
图32-1:一根金属棒的温度分布示意
较热较冷初始分布稍后金属棒上的位置
查看 SVG 配图原件
示意图原件 32-003-f3c2615ee807.svg
32-003-f3c2615ee807.svg · 点击查看大图
教学示意:热量逐渐传递,使温度分布趋向平滑。图形不对应某次具体实验测量。
怎样描述这种变化?傅里叶提出了一种非常有力量的思路。某些复杂的温度分布,可以分解成一系列简单的正弦或余弦函数。而这些简单的函数在热传导方程中,有着相对容易求解的变化规律。如果把每一部分的变化计算出来,再将它们组合起来,就能重建整个温度分布随时间的演化。这一思路涉及我们今天熟悉的傅里叶级数。它并不是说现实世界里的所有曲线,无论条件如何,都能被随意分解。严格地说,函数展开需要满足相应的数学条件;在间断点处,级数收敛到什么值也必须仔细讨论。这些细节,在十九世纪引发了长期的数学研究。但它提供了一个极其重要的思想:复杂的变化,可以通过一组简单的周期成分来描述。

听见一个看不见的和弦

假设现在有两种声音。第一种,每秒振动三次。第二种,每秒振动十一​​次。单独观察,它们都是规律清晰的波。当它们同时出现时,空气中的振动会相互叠加。于是,我们听到的声音更加复杂。假设它们的数学表达式分别为:f1(t)=sin⁡(2π⋅3t)f_1(t)=\sin(2\pi\cdot3t)f2(t)=0.6sin⁡(2π⋅11t)f_2(t)=0.6\sin(2\pi\cdot11t)那么叠加后的信号就是:f(t)=f1(t)+f2(t)f(t)=f_1(t)+f_2(t)观察这条新曲线,我们可能很难一眼看出它由哪些波构成。但如果用合适的数学方法分析,我们就可以重新发现其中的3和11。这就像观察一杯混合了不同颜色颜料的水。肉眼看到的是混合后的结果。而傅里叶分析试图回答:构成这个结果的不同成分,各自有多少?声音只是一个特别直观的例子。同样的数学思想还能用于分析潮汐、机械振动、电信号,以及温度的空间分布。

一项曾经引起争议的研究

1807年,傅里叶向法国科学院提交关于热传播的研究。他的理论引起了学界关注,也面临严格性的质疑。当时的数学家并不完全接受他对一般函数使用三角级数展开的论证。这里有一个值得尊重的历史细节。傅里叶提出的数学思想极其深刻,但他最初的论证并不是今天意义上所有条件都已完善的严格理论。后来,狄利克雷、黎曼等数学家继续推动相关问题的发展。1822年,傅里叶出版了《热的解析理论》(Théorie analytique de la chaleur)。1822年《热的解析理论》初版书名页。这部著作从热传导问题出发,对数学物理产生了深远影响。[math.jhu.edu][staff.bath.ac.uk]傅里叶最初研究的,并不是数字音乐,也不是计算机频谱。他的中心问题是热量如何流动。然而,他所发展的数学方法,后来成为数字信号处理的重要理论基础。有时科学发现的真正力量,恰恰不在于研究者最初想解决的那个问题。而在于后人发现,这套数学思想能够解释远远超出原始领域的现象。不过,傅里叶并不是这个故事里唯一的早期探索者。在他发表热传导研究之前,一位德国数学家已经研究过如何更高效地计算周期数据。这个人的名字,是高斯。

第二幕:高斯的手稿,一项被历史暂时隐藏的计算技巧

卡尔·弗里德里希·高斯(Carl Friedrich Gauss,1777—1855)。图为1840年前后由 Christian Albrecht Jensen 绘制的著名肖像。十八世纪末和十九世纪初,欧洲的天文学研究正处于一个关键时期。望远镜提供了越来越多的观测数据。数学家则需要从这些数据中推测天体运行的轨道。然而,真实的天体运动并不总能用一个最简单的圆周运动来描述。行星之间存在引力扰动。观测也会存在误差。有时,一颗天体在天空中的位置,需要由多个不同周期的数学成分共同描述。这类问题非常适合进行三角分析。但问题再次出现了。如果观测数据很多,计算工作就会变得极其繁重。

在计算机出现之前,计算本身就是一项工程

今天,我们可以把几百个数字输入计算机。几毫秒之后,答案就可能出现在屏幕上。可是,在高斯生活的时代,这些运算通常需要手工计算,或依赖数值表与辅助工具。一次复杂的三角函数计算,可能涉及大量查表、乘法和加法。如果计算出现错误,还必须花费时间检查。对于天文学家而言,减少运算次数绝不是形式上的数学游戏。它能够直接改变研究效率。高斯曾对小行星的运动进行深入研究。尤其是智神星(Pallas)的轨道问题,成为他相关三角插值计算的一个应用背景。他在一份名为 Theoria Interpolationis Methodo Nova Tractata 的手稿中,描述了如何通过拆分较大的周期数据集合,减少计算三角级数系数所需的工作。现代数学史研究认为,这份手稿很可能写于1805年前后。但它并没有在当时正式发表,而是在高斯去世之后,于1866年收入他的文集。这里有一个重要的时间差:思想可能出现在1805年前后,文献直到1866年才公开出版。而我们今天广泛使用的 FFT,则是在1965年前后形成了极为成功的计算机实现和传播浪潮。[ResearchGate][Studylib][Studylib]

把十二个问题拆开

考虑一个简化的例子。假设我们在一个周期内均匀取得12个观测值:x0,x1,…,x11x_0,x_1,\ldots,x_{11}直接计算全部频率成分,需要进行许多乘法与加法。但12可以分解成:12=3×412=3\times4于是,我们可以将观测值分组,对较小的子序列先进行计算,然后使用数学关系,把中间结果组合起来。高斯手稿中对智神星问题的一个实例,正涉及12个观测位置,并讨论将其按3与4的因子进行分解。这里的思想与现代 FFT 的复合长度分解具有明确的数学联系。不过,我们不能说高斯已经拥有了后来计算机里的 FFT 软件。他没有电子计算机,也没有现代复数数组、内存布局和机器指令这些概念。他所使用的是三角插值和手工数值计算的语言。是后来的数学史研究,证明了其中一些步骤与现代 Cooley–Tukey 算法在数学结构上的等价性。

为什么如此好的方法没有立即改变世界?

这是数学史中一个很值得思考的问题。如果高斯已经提出了类似的高效分解方法,为什么人类又等了一百多年,才广泛使用它?首先,他的相关手稿没有在研究形成时及时传播。其次,十九世纪多数实际计算任务的数据规模,与今天的数字信号处理相比要小得多。某些针对少数几个频率的专门计算方法,已经可以满足当时的需求。再者,只有当观测数据和计算任务变得足够庞大时,通用高效算法的优势才会变得特别突出。这并不意味着在高斯之后,相关研究完全中断。十九世纪,Archibald Smith 等人发展过高效的调和分析计算技巧。二十世纪初,Carl Runge 研究了利用倍增方式计算傅里叶系数的方法。1942年,Gordon Danielson 与 Cornelius Lanczos 也描述了相关分解思想。1958年,I. J. Good 提出了重要的因子分解算法。这些工作并不全部等价,也不是一条简单的师徒传承链。但它们共同说明:计算傅里叶系数的高效方法,曾经在不同时代、不同数学问题中反复出现。1984年,Michael Heideman、Don Johnson 与 C. Sidney Burrus 发表了关于高斯与 FFT 历史的系统研究,重新梳理了这段容易被忽略的科学史。[ResearchGate][Studylib]它告诉我们:一个数学思想被提出的时间,和它真正改变世界的时间,可能相隔一百多年。思想需要问题。问题需要技术条件。有时候,还需要一群恰好能够把彼此连接起来的人。到1963年,这些条件终于开始汇聚。

第三幕:冷战年代,一张写着公式的笔记纸

传统地震仪以连续曲线记录地面振动。识别信号中的频率成分,是地震数据分析的基础技术之一。配图为历史设备示意参考,并非1963年核试验监测项目的原始记录。1963年。世界已经进入核武器时代。美国与苏联之间的紧张局势,让核试验监测成为重要的科学与政治问题。当一枚核装置在地下爆炸时,会产生向周围传播的地震波。这些振动能够被地震仪记录下来。但地下核爆炸并不是唯一能够引起地震波的事件。天然地震也会产生复杂的振动。因此,研究者需要从大量测量数据中寻找有用的特征,并结合其他证据判断波形来源。傅里叶分析能够帮助研究者分析不同频率成分。可是,如果要监测大量地区、处理许多传感器的长时间数据,计算成本就会成为严重障碍。

一个真实发生过的科学交流

1963年前后,在美国总统科学顾问委员会的一次会议上,数学家 约翰·图基 正在笔记纸上记录关于傅里叶计算的公式。坐在他附近的,是物理学家 理查德·加温(Richard Garwin)。加温注意到了图基正在研究的内容。根据后来 Cooley 和 Tukey 自己撰写的回顾文章,加温询问之后,得知图基正在探索一种更高效计算离散傅里叶变换的方法。如果观测点数量 NN 能够分解为两个因子的乘积,那么原来庞大的求和计算,就可能拆分为一系列规模较小的计算。继续分解,还能进一步减少运算次数。对于当时正在思考地震波谱分析和其他物理问题的加温来说,这显然是一项具有现实意义的发现。这里不需要虚构一句激动人心的对话。史料所记录的事实本身,就已经足够精彩。一次科学委员会会议上偶然发生的数学交流,最终推动了一项重要算法的工程实现。 [加菲尔德图书馆][IEEE Spectrum]理查德·加温(Richard Garwin)。他在 FFT 的现代传播史中发挥了关键的沟通、组织和推动作用。[加菲尔德图书馆][IEEE Spectrum]

从笔记纸走向真正的计算机

加温不仅理解算法的数学意义。他还知道,想让这套方法改变研究实践,必须有人把它变成能够在计算机上运行的程序。他找到 IBM 研究机构的计算部门,希望有人实现这种方法。在 IBM 负责相关工作的人员协调下,这项任务来到了数学家 詹姆斯·库利(James W. Cooley)手中。库利从事数值分析与计算研究。在他的后来回忆中,最初他并没有立刻把这项工作放在最高优先级。加温不断推动研究进展,库利才逐渐投入这项工作。这段经历很有启发性。重大技术突破,并不总是某个天才独自在实验室里突然完成的。它也可能来自一条真实的合作链:一个人发现数学机会。另一个人看见应用价值。第三个人负责让它在实际机器上运行。在这一过程中,缺少任何一个环节,结果都可能不同。而最令人意想不到的是,最初推动算法的现实需求之一,竟然与核试验监测和国际安全有关。但几十年之后,普通人使用这项算法最多的场合之一,已经变成音乐、图像、通信和日常电子设备。历史有时会用非常曲折的方式,连接起完全不同的领域。

第四幕:1964年,一台大型计算机开始重新理解速度

IBM 7094 大型计算机及其所在时代的机房设备。1964年,Cooley–Tukey FFT 的重要计算机实现就在 IBM 7094 上得到演示。二十世纪六十年代,电子计算机已经不再是完全陌生的技术。但是它们与今天的个人计算机相差极大。一台大型计算机需要专门的机房、供电和操作人员。数据可能通过穿孔卡片、磁带等方式输入。程序员也不可能像今天这样,随时打开笔记本电脑,运行数百万次浮点运算。对于数值计算研究者来说,计算时间是一种需要认真管理的资源。如果某个实验需要处理大量数据,那么运算效率可能直接决定这个实验是否具备可行性。

库利面对的任务

图基的想法已经表明,可以利用离散傅里叶变换内部的数学结构减少计算。但要在计算机上真正实现,还会遇到其他问题。数据应该如何安排?中间结果存放在哪里?怎样控制访问内存的次数?怎样组织计算,让机器不必反复计算相同的内容?库利特别研究了适合二进制计算机的基数为2的分解算法,也就是后来常说的 Radix-2 FFT。通过恰当安排数据,算法可以在原数组中不断更新中间结果。这种原地计算方式,对于当时内存资源有限的计算机尤为有价值。1964年,IBM 的研究人员成功进行了相关算法的计算机演示。IEEE 的技术史里程碑资料将这一工作明确记为1964年在 IBM Research 的重要演示。[IEEE Milestones Wiki][IEEE Milestones Wiki]

从研究程序到公开论文

1964年8月17日,库利和图基的论文收到投稿登记。1965年4月,论文发表于《Mathematics of Computation》,题目是:An Algorithm for the Machine Calculation of Complex Fourier Series中文可以译为《一种用于复数傅里叶级数机器计算的算法》。1965年 Cooley 与 Tukey 论文首页。论文第297—301页系统描述了如何通过分解计算,将直接方法的二次增长量级降低到 Nlog⁡NN\log N 量级。[IBM Research][Department of Computer Science]这篇论文只有五页。但它对数字信号处理的影响非常深远。论文开头就提到了 Yates 在实验设计中的计算方法,以及 I. J. Good 的研究成果。作者并没有把所有数学思想都归功于自己,而是明确指出与前人的关系。论文重点讨论如何把一个大规模的变换拆成较小的计算,并展示这种算法如何在机器上高效执行。

一个容易忽略的细节:这项技术差点进入专利讨论

根据库利与图基后来共同撰写的历史回顾,相关研究在 IBM 内部传播后,曾有人提出是否存在申请专利的可能性。研究人员和法律人员对此进行了讨论。最后,相关人员选择通过公开发表等方式,让算法进入公共知识领域,避免其被单独控制。随后论文正式发表。这项算法能够迅速传播,除了数学性能出色,也与其在科学界获得了广泛交流和实现有关。[加菲尔德图书馆][加菲尔德图书馆]这是一个值得思考的技术史问题。一种算法即使具有巨大的价值,如果无法被广泛学习、实现、比较和改进,其社会影响也可能受到限制。而 FFT 的历史恰好展现了另一种发展道路。从一篇短论文开始,越来越多的研究人员对它进行实现、优化和推广。数学上的分解技巧,最终成为电子工程师和程序员都能使用的基础工具。但是,究竟是什么数学结构,使它拥有这样的速度?要回答这个问题,我们必须先看看计算机原本在做什么。

第五幕:一场关于重复计算的革命

图32-2:FFT 如何分解一个8点序列
8个采样点偶数位置0, 2, 4, 6奇数位置1, 3, 5, 72点2点2点2点1111逐层计算,再组合中间结果
查看 SVG 配图原件
示意图原件 32-010-9602fcdc0772.svg
32-010-9602fcdc0772.svg · 点击查看大图
示意图展示了基数为2的 FFT 递归分解:8点变4点,4点变2点,最后到单点。每层合并时进行蝶形运算。图中位置编号从0开始。

先理解离散傅里叶变换

想象我们拿到一段数字信号。计算机并不直接看到声音、温度或地震。它接收到的是一系列数字:x[0],x[1],x[2],…,x[N−1]x[0],x[1],x[2],\ldots,x[N-1]这里,NN 是采样点的数量。如果在一秒内均匀取得64个样本,就得到64个数据点。我们希望知道,这段数据中包含哪些周期成分。离散傅里叶变换,简称 DFT,使用下面的公式:X[k]=∑n=0N−1x[n]e−i2πkn/NX[k]=\sum_{n=0}^{N-1}x[n]e^{-i2\pi kn/N}先不用被公式吓到。它的基本思想是:对某个候选频率,构造一个对应的复指数波。把原始信号与这个波进行计算,观察它们能够形成多大程度的匹配。当某种频率在信号中占有显著成分时,对应的频率系数通常具有较大的幅值。这里使用复数,能够同时表示正弦、余弦以及相位关系。对于初学者,可以暂时把它理解成:给每一个候选频率安排一套数学检测器,再依次计算它与原始信号的关系。当然,DFT 的输出本质上是复数系数,准确的幅度还涉及归一化,不能直接把所有输出绝对值都当作真实物理振幅。

为什么直接计算这么慢?

假设有 NN 个输入数据。我们想求出全部 NN 个频率系数。根据上面的公式,计算每一个 X[k]X[k],都需要对 NN 个数据进行求和。需要计算的频率数量同样是 NN。因此,直接按照定义计算,需要的主要运算次数随以下数量增长:N×N=N2N\times N=N^2这就是:O(N2)\boxed{O(N^2)}如果:N=1024N=1024那么仅从乘加项数量的量级看,就需要处理:10242=1 048 5761024^2=1\,048\,576个计算项。如果数据增加到原来的十倍,直接算法的计算规模就会增加到约一百倍。这种增长方式,正是大型信号处理中的困难来源。

FFT 的第一个秘密:奇偶拆分

现在假设 NN 是偶数。我们把序列分成两部分。偶数编号的数据:x[0],x[2],x[4],…x[0],x[2],x[4],\ldots奇数编号的数据:x[1],x[3],x[5],…x[1],x[3],x[5],\ldots原本需要对 NN 个样本进行的计算,现在可以分别对两组 N/2N/2 个样本进行处理。这件事听起来很普通。许多算法都采用过类似的分治思路。但 FFT 的巧妙之处是:这两个较小问题的计算结果,可以通过非常简单的关系组合成原始问题的答案。定义:WN=e−i2π/NW_N=e^{-i2\pi/N}令 E[k]E[k] 表示偶数编号子序列的 N/2N/2 点 DFT,O[k]O[k] 表示奇数编号子序列的 N/2N/2 点 DFT。那么对于 0≤k<N/20\le k<N/2,我们得到:X[k]=E[k]+WNkO[k]\boxed{X[k]=E[k]+W_N^kO[k]}另一个频率位置则是:X[k+N/2]=E[k]−WNkO[k]\boxed{X[k+N/2]=E[k]-W_N^kO[k]}这两条公式是基数为2的时间抽取 FFT 的核心。它们意味着:完成两次较小规模的傅里叶变换之后,只需要进行一些额外的乘法、加法和减法,就能恢复原来的全部结果。其中的 WNkW_N^k,通常被称为旋转因子(Twiddle Factor)。它负责在组合时引入正确的相位关系。

FFT 的第二个秘密:蝶形运算

观察上面两条式子。它们都使用相同的中间结果:E[k],O[k]E[k],\quad O[k]只不过一个相加,一个相减。因此,程序可以先计算:t=WNkO[k]t=W_N^kO[k]然后:X[k]=E[k]+tX[k]=E[k]+tX[k+N/2]=E[k]−tX[k+N/2]=E[k]-t由于这种计算的连线形状常常像一只展开翅膀的蝴蝶,研究者把它称为蝶形运算。
图32-3:蝶形运算的结构
E[k]O[k]+−× WX[k]X[k+N/2]W = e^(-i2πk/N)
查看 SVG 配图原件
示意图原件 32-011-f55ade2c03b8.svg
32-011-f55ade2c03b8.svg · 点击查看大图
一个蝶形单元同时生成两个频率输出。示意图使用交叉连线强调数据复用,旋转因子只乘在奇数分支上。
这件事情为什么重要?因为我们不必分别对两个输出重复进行完整的求和计算。一个中间结果,可以被多个输出共同使用。当整个序列继续被分成4组、8组、16组时,这种重复利用会不断发生。对于 N=2mN=2^m 的基数为2 FFT,分解层数只有:m=log⁡2Nm=\log_2N每一层需要处理的蝶形单元数量与 NN 成正比。因此,总计算量便变成:O(Nlog⁡N)\boxed{O(N\log N)}这就是速度革命的真正来源。不是计算机突然变快了。不是硬件增加了更多晶体管。而是数学家发现,原本大量重复的工作根本不需要再做。技术说明:这里推导的是输入长度为2的幂时的典型 Radix-2 FFT。Cooley–Tukey 方法本身也能处理其他可分解的复合长度;对于任意长度还有其他 FFT 设计。FFT 计算的是 DFT,并不是与 DFT 不同的频谱定义。

第六幕:当一百万次计算变成几千次

从数学推导走向可以亲手验证的计算实验现在,我们终于可以准确理解 FFT 的速度优势。下面这张表采用 N2N^2 与 Nlog⁡2NN\log_2N 两个数学增长量作为比较指标。
采样点数 NN直接 DFT 的 N2N^2FFT 的 Nlog⁡2NN\log_2N
644,096384
1,0241,048,57610,240
4,09616,777,21649,152
65,5364,294,967,2961,048,576
这张表比较的是复杂度增长指标,不是实际耗时或严格一致的运算指令数。具体速度还受实现方式、内存访问、硬件、数值精度与算法常数影响。
2684354560642561024409616384
查看图表数据
n直接DFT:N²FFT:N log₂N
644096384
256655362048
1024104857610240
40961677721649152
16384268435456229376
在1024个采样点时:N2Nlog⁡2N=102410=102.4\frac{N^2}{N\log_2N} =\frac{1024}{10}=102.4这说明两个增长量之间已经出现百倍量级的差异。数据越多,优势通常越明显。这还不是一项具体程序的实际加速倍数。我们只能说,FFT 在理论计算复杂度上实现了根本改进。1964年的研究人员面对的就是这种数量级上的变化。对于一台性能有限的大型计算机而言,这可能意味着原本难以承担的信号分析任务,开始进入可以实际计算的范围。

互动实验:把混合信号重新分离

现在,让我们亲手完成傅里叶当年无法使用电子计算机完成的实验。我们构造两组正弦波,把它们相加,然后通过离散傅里叶变换重新找出隐藏的频率。为了方便观察,设采样率为64次/秒,采样时长为1秒。第一组波的频率固定为3 Hz。第二组由你自己调整。需要说明:3 Hz和11 Hz属于非常低的频率,并不是人耳通常能够听见的音调;这里使用它们,是为了清晰显示数学上的频率分解。如果要研究可听声音,可以将频率改为300 Hz和1100 Hz,同时相应提高采样率。
图32-4:交互式频谱实验室调整第二种波的频率与振幅,观察混合波形和频谱如何变化。
11 Hz
0.6
时域:混合后的波形64个采样点
横轴是采样位置,纵轴是信号振幅。两种波叠加后,曲线比单独的正弦波复杂。

频域:分解后的频率0—31 Hz
横轴为频率(Hz)。两个显著峰值分别对应3 Hz和你选择的第二种频率。这里采用单边振幅归一化,且信号频率恰好对齐DFT频率格点。
实验分析
第一种成分

3 Hz

振幅 1.0
第二种成分

11 Hz

振幅 0.6
当前实验的输入信号是:x(t)=sin⁡(6πt)+0.6sin⁡(22πt)x(t)=\sin(6\pi t)+0.6\sin(22\pi t)
这个实验有一个非常重要的细节。上面为了演示频谱,使用了直接 DFT 的数学计算方式。如果将其替换为正确实现的 FFT,理论上应该得到相同的变换结果,只会存在正常的浮点数舍入差异。FFT 与 DFT 并不是两套分别产生不同答案的频谱分析理论。FFT 是更高效计算 DFT 的算法。这正是本章的核心。

动手编程:不调用 FFT 库,自己实现

下面用 Python 编写一个递归版的 Radix-2 FFT。为了突出原理,我们暂时不使用 NumPy 等现成函数。
python

import cmath
import math


def fft(x):
    n = len(x)

    if n == 1:
        return [complex(x[0])]

    if n == 0 or n & (n - 1):
        raise ValueError("长度必须是2的正整数次幂")

    # 递归处理偶数与奇数编号的采样点
    even = fft(x[0::2])
    odd = fft(x[1::2])

    result = [0j] * n

    for k in range(n // 2):
        w = cmath.exp(-2j * math.pi * k / n)
        t = w * odd[k]

        # 蝶形运算
        result[k] = even[k] + t
        result[k + n // 2] = even[k] - t

    return result


def dft(x):
    n = len(x)

    return [
        sum(
            x[t] * cmath.exp(-2j * math.pi * k * t / n)
            for t in range(n)
        )
        for k in range(n)
    ]


N = 64
signal = [
    math.sin(2 * math.pi * 3 * n / N)
    + 0.6 * math.sin(2 * math.pi * 11 * n / N)
    for n in range(N)
]

fast_result = fft(signal)
slow_result = dft(signal)

error = max(
    abs(a - b)
    for a, b in zip(fast_result, slow_result)
)

print("FFT与DFT的最大差异:", error)

for k in range(1, N // 2):
    amplitude = 2 * abs(fast_result[k]) / N

    if amplitude > 0.1:
        print(f"频率 {k} Hz, 振幅 {amplitude:.3f}")
运行程序,应该可以观察到两个显著的频率成分:
text
频率 3 Hz, 振幅 1.000
频率 11 Hz, 振幅 0.600
两种算法的结果之间,只会出现很小的浮点数计算差异。这份代码与1964年的工业级实现当然还有很大距离。它使用递归,反复创建数组,也没有进行旋转因子的缓存或向量化优化。但它准确展示了 FFT 最重要的计算结构。对于相同输入,不再逐个频率进行完整求和,而是将大问题分解并复用中间结果。当你理解了这部分,就已经理解了 FFT 为什么能够改变计算速度。

第七幕:从地下核试验到每个人的数字生活

现代声音分析软件会同时展示时域波形与频域信息。快速傅里叶变换是许多数字频谱分析系统的基础计算工具。1965年的研究者不可能完全预见 FFT 后来的所有应用。当时,数字计算机仍然昂贵,数字音乐尚未形成现代产业,医学成像技术也处于完全不同的发展阶段。但接下来的几十年里,FFT 的影响逐渐扩散。

让声音可以被分析

一段录音,本质上可以表示为大量随时间变化的数字样本。音乐制作软件需要分析声音中有哪些频率。工程师需要判断电器或发动机产生的噪声集中在哪些频率范围。通信接收机需要识别和处理无线信号。FFT 为这些任务提供了高效的频谱计算能力。不过,频谱分析并不能自动理解声音的全部语义。例如,一段录音中同时存在两个人的说话声,FFT 可以展示频率组成,却不能单凭一次变换,就把两个人的声音完整分离。真正的语音分离通常还需要时频分析、统计模型或机器学习等其他方法。

让通信系统更高效

在现代数字通信中,特别是在 OFDM 等多载波通信技术里,FFT 和逆 FFT 是非常重要的计算组成部分。许多不同的子载波可以一起承载信息。发送端通过逆变换组合信号,接收端再利用变换恢复各个子载波上的信息。我们今天使用的许多高速无线通信系统,都包含类似的数学处理过程。这也是为什么一个起源于周期数据分析的算法,最终能够进入现代通信芯片。

让医学设备重建图像

磁共振成像(MRI)会采集与空间频率有关的数据。从这些数据重建图像,需要傅里叶变换及相关数值处理。FFT 因此成为许多 MRI 图像重建流程的重要组成部分。计算机断层扫描(CT)也与傅里叶分析密切相关,尤其涉及投影、频域关系与滤波技术,但具体实现并不能简单归结为“把图像执行一次 FFT”。这些技术的发展有着独立而复杂的科学史。FFT 所发挥的作用,是降低其中某些关键计算的代价。

让科学家能够分析更大规模的数据

地震波、天文信号、无线电观测、机械振动、光谱数据……这些领域存在不同的物理问题,却都可能包含类似的计算结构。当研究者需要从大量连续变化的数据中寻找周期性规律时,傅里叶方法就是一种重要的工具。这件事再次印证了算法的普遍性:不同学科不一定需要发明完全不同的计算方法。只要问题拥有相同的数学结构,它们就可能分享同一套算法。IEEE 将1964年的 FFT 计算机演示列为技术史里程碑,正是因为它的作用早已超越最初的研究任务。[IEEE Milestones Wiki][IEEE Spectrum]

但是,FFT 并不是一把万能钥匙

到这里,我们还需要讨论它的局限。假如一段信号只有很短的采样时间,能够区分的频率就受到观测窗口的限制。假如信号的频率没有恰好落在 DFT 对应的频率格点上,频谱能量就可能分散到多个相邻位置。这种现象称为频谱泄漏(Spectral Leakage)。另外,采样率也会决定能够无歧义地分析多高的频率。如果采样不足,高频信号可能被错误地表现为其他频率,产生混叠(Aliasing)。因此,使用 FFT 并不等于可以随意采样,然后自动得到准确结果。真正的数字信号处理系统,还需要考虑采样定理、滤波、窗函数、噪声和数值误差。这些限制,也让傅里叶分析成为一门持续发展的科学,而不只是一条已经解决的数学公式。

尾声:一条曲线,两百年的等待

让我们把时间重新拨回1807年。那时,傅里叶正在思考一个问题:为什么热量会按照某些数学规律扩散?他研究的是温度。他所发展的数学思想,却为后来分析各种周期变化奠定了基础。差不多同一时期,高斯从天体运动和三角插值中,探索如何减少繁重的计算。他的相关手稿没有在研究形成时及时公开。后来,其他数学家又通过不同道路,逐渐接近类似的高效计算结构。进入二十世纪,计算机终于出现。庞大的数据量,使原先并不总被重视的运算效率问题,变得迫切起来。1963年,图基的数学研究引起了加温的注意。1964年,库利与合作者在 IBM 的大型计算机上展示了高效变换的能力。1965年,一篇只有五页的论文把它带进更广阔的计算科学世界。至此,一项长期存在于数学研究中的思想,真正迎来了广泛应用的时代。这个故事之所以特别,并不是因为某位科学家突然创造了一套从未有人想过的全部知识。恰恰相反。它展示了科学进步更真实的样子。一个人发现了新的数学结构。另一个人对它进行推广。后来的人在不同问题中重新发现它。等到技术条件成熟,又有人把它变成高效而可靠的程序。最终,这些成果汇聚在一起,改变整个时代的计算能力。从傅里叶研究热量,到高斯计算轨道,再到库利和图基的 FFT,跨越了不同的问题、人物与时代。然而其中始终存在一个相同的问题:怎样用更少的计算,得到同样正确的结果?上一章,我们学习欧几里得算法。它告诉我们,可以通过保持答案不变的数学变换,把一个问题不断缩小。这一章,我们学习快速傅里叶变换。它告诉我们,某些看似需要大量独立计算的问题,可能隐藏着能够重复利用的中间结构。两种算法相隔两千多年。但它们共同体现了一项重要的计算原则:算法的进步,不一定来自计算更多,而往往来自发现哪些计算根本不必重复。当人类拥有越来越强大的硬件时,这个原则并没有过时。恰恰相反。因为数据增长得越快,避免不必要计算的重要性就越大。

历史档案:真实记录与创作边界

本章主要依据以下一手资料与数学史研究:本文中可以确认的史实包括傅里叶研究热传导、高斯的三角插值手稿、1963年前后的科学交流、IBM 的计算机演示及1965年的论文发表。对科学家工作时的心理状态、人物对话和具体实验室场景,本文没有作为确凿史实进行描述。文中的64点信号、3 Hz与11 Hz频率、算法图解和数学实验,均为现代教学设计,并非历史研究者当年使用的原始实验数据。
下一章预告 · 第33章

《计算机如何掷骰子:伪随机数的诞生》

一台计算机,严格按照指令执行每一步操作。如果给它相同的输入,它就应该产生相同的输出。那么问题来了:一台完全遵循确定性规则的机器,究竟怎样才能制造出看起来毫无规律的随机数字?下一章,我们将进入二十世纪四十年代的电子计算机实验室,从冯·诺依曼的中平方法讲起,探索伪随机数的诞生、令人意外的失败案例,以及为什么现代计算机几乎离不开随机数生成器。
——《算法的故事》第二季 · 第32章 完