Qwen3-ASR 课程 · 第 4 课 · 补充课(第 3 课答疑)

傅里叶变换可视化 —— 从波形到频谱的每一步

⏱ 约 20 分钟(全程动手)· 前置:第 3 课(语音前端) · 本课目标:亲眼看着"波形 → 频谱"发生,不留任何一步魔法

你说得对——第 3 课把傅里叶变换一句"用 FFT 问每帧有哪些频率"带过去了,等于让你直接信了一个魔法。今天把它拆到不能再拆:零公式,两台可以动手操作的机器,每一步都看得见。

第 0 步:先认识主角——正弦波,以及"2Hz"是什么意思

正弦波是自然界最"默认"的波动形状:秋千的摆动、音叉的颤动、绳子轻甩出的涟漪,都是它。它长得像平滑的波浪,一遍遍重复同一个形状。

看懂它只需要两件事:

0s 0.5s 1s ↑ 一个完整周期:出发→峰→谷→回到起点 1Hz 每秒 1 次 ① ② ③ ④ 2Hz 每秒 2 次 5Hz 每秒 5 次
同样长的 1 秒窗口里:1Hz 的波完成 1 个完整来回,2Hz 完成 2 个,5Hz 完成 5 个。频率越高,波越"密"——Hz 决定密疏,不看高度。
所以"成分① 2Hz ×0.80"的意思是: 一条每秒振动 2 次的正弦波(2Hz,决定形状密疏),强度是 0.80(振幅,决定波峰多高)。频率 + 振幅,两个数就完全锁定一条正弦波——这也是为什么频谱只需要"哪些频率 + 各多少含量"就能描述整段信号。
两个容易误会的点 ① 演示用 2Hz/5Hz/9Hz 是"慢动作",为了让你肉眼数得清;真实声音是几百到几千 Hz(第 3 课声音实验室里的 440Hz 纯音,1 秒里振动 440 次)。低于约 20Hz 的振动人耳根本听不见。 ② 频率对应的是听感上的音调(高 Hz = 尖锐高音,低 Hz = 低沉),不是音量;音量对应振幅。

认识主角之后,马上让三条不同 Hz 的正弦波合体——拖下面机器里的滑块,注意 9Hz 那条最密、2Hz 那条最疏:

第一步:复杂波 = 简单正弦波的叠加

傅里叶的全部出发点只有一句话:任何复杂的波,都可以由若干个简单的正弦波相加得到。就像一杯混合果汁:喝到的是混合味道,但配方是"苹果 3 勺 + 橙子 2 勺 + 芹菜 1 勺"。

下面这台机器里,三种"果汁"的量给你拖。拖动滑块,看上面三个简单波怎么叠成下面的复杂波:

玩一下你会发现:把某个滑块拖到 0,复杂波立刻"变简单"一点。所以反过来问:给定一团复杂的波,能不能查出它是哪几个简单波、各多少成分配成的?——查配方的过程,就是傅里叶变换;"配方表"就是频谱。

关键观念 波形是"混好的果汁",频谱是"配方表"。傅里叶变换 = 从果汁反查配方。

第二步:怎么测某个频率"藏没藏"?拿尺子对拍

思路简单到好笑:拿一个已知频率的标准正弦波当尺子,去和信号逐点相乘,再取平均:

像推秋千:顺着秋千的节奏推,越推越高(共振);胡乱推,推了也白推。

别信我,自己算一遍:8 个数字看清测量全过程

规则只有一条:信号值 × 参考波值,逐点相乘,再把所有乘积加起来除以点数(取平均)。我们只取 8 个采样点(每 0.125 秒一个,铺满 1 秒),尺子固定用 1Hz 参考波。(表里的 0.71 是 cos45° ≈ 0.707 四舍五入。)

情形 A:信号本身就是 1Hz 正弦波(和尺子对上了)

先看图:紫色的信号和灰色的尺子(参考波)上下对齐画好,8 个采样时刻各画一个点、连一条竖虚线。竖线颜色就是那一列乘积的符号——绿 = 正,红 = 负,灰 = 零:

时间点信号值参考波(1Hz)乘积
0s1.001.001.00
0.125s0.710.710.50
0.25s0.000.000.00
0.375s−0.71−0.710.50
0.5s−1.00−1.001.00
0.625s−0.71−0.710.50
0.75s0.000.000.00
0.875s0.710.710.50

关键观察:信号和尺子是同一形状,同起同落——两个负数相乘也得正。于是8 个乘积全部 ≥ 0,一个负的都没有。合计 = 1.00+0.50+0+0.50+1.00+0.50+0+0.50 = 4.00,平均 = 4.00 ÷ 8 = 0.50。判定:平均远大于 0 → 信号里藏着 1Hz!

情形 B:信号换成 2Hz 正弦波(尺子还是那把 1Hz,没对上)

再看图:信号振得比尺子快一倍,节奏错开——盯住 t=0 和 t=0.5 两列,点的位置一上一下互换:

时间点信号值(2Hz)参考波(1Hz)乘积
0s1.001.00+1.00
0.125s0.000.710.00
0.25s−1.000.000.00
0.375s0.00−0.710.00
0.5s1.00−1.00−1.00
0.625s0.00−0.710.00
0.75s−1.000.000.00
0.875s0.000.710.00

关键观察:信号振得比尺子快一倍,节奏错开——0 秒时同号(乘积 +1),0.5 秒时信号在峰、尺子在谷,符号相反(乘积 −1)。合计 = +1.00 + (−1.00) = 0,平均 = 0 ÷ 8 = 0.00。判定:正负精确抵消 → 信号里没有 1Hz。

测量的全部秘密,就是这两张表 对上 → 乘积全同号 → 平均大;没对上 → 乘积有正有负 → 抵消成 0。"藏没藏这个频率"就变成了一个可以算出来的数。补一句:对上时平均是 0.50 而不是 1.00,是因为同值相乘(平方)的平均恰好是一半——频谱柱高按 ×2 放大后,正好还原信号强度。

下面这台探测器,做的就是你刚才手算的事——只是把"8 个点"换成"800 个点"(2 秒 × 每秒 400 点),尺子(滑块)可以随意换。信号里藏着两个频率(保密!):先手动拖滑块,看乘积曲线什么时候整条趴在正半轴、什么时候上下乱穿;再点 自动扫描,看扫过每个频率、柱子一根根长出来——那就是频谱诞生的瞬间:

找到那两个藏起来的频率了吗?(提示:信号是归一化的,柱高和成分强度成正比——哪根柱子粗壮,哪个成分就多。)

两句诚实的简化 ① 这里信号成分相位都从零开始,所以一把"余弦尺"就够;真实信号相位各异,傅里叶会用 cos 和 sin 两把尺子一起量,再合并结果——思想完全一样。② FFT 不是别的算法,只是把"逐个频率都量一遍"算得飞快的巧妙排列,结论和这里一根根柱子完全一致。

第三步:把每次测量的结果,画进频谱

单次测量你已经会算了。频谱就是把这件事对每个频率各做一遍,再把结果排成一张图,规则只有三条:

拿你问的例子直接实验:信号 = 1Hz(强度 0.8)+ 2.5Hz(强度 0.5)。先在心里猜频谱长什么样,再点自动扫描:

结果:只在 1.00Hz 和 2.50Hz 两处立起柱子(约 0.62 和 0.38),其余全部贴地。读图就是读配方——

频谱特征告诉你的事
柱子立在哪个频率信号里藏着什么频率(1Hz、2.5Hz)
柱子多高该成分有多强(探测器把信号整体缩放到最大 1,所以柱高同比缩成 0.62/0.38,但相对比例仍是 0.8 : 0.5)
没有柱子的地方这些频率不存在(测量精确抵消为 0)

还有一个终极验证:你一根根柱子扫出来的东西,数学家早就造好了一步到位的机器——FFT。下面把"你的自动扫描"和 np.fft.rfft 放在同一张图上对拍(信号就是上面那个 1Hz + 2.5Hz):

import warnings warnings.filterwarnings("ignore") import numpy as np import matplotlib.pyplot as plt fs, dur = 400, 2.0 t = np.arange(int(fs*dur)) / fs x = 0.8*np.cos(2*np.pi*1*t) + 0.5*np.cos(2*np.pi*2.5*t) x = x / np.abs(x).max() # 探测器同款归一化 # 你的"自动扫描":逐频对拍,柱高 = 2×|平均| freqs = np.arange(0.5, 6.01, 0.25) scan = np.array([2*abs(np.mean(x * np.cos(2*np.pi*f*t))) for f in freqs]) # FFT:现成的"对所有频率一次算完"的机器 spec = np.fft.rfft(x) fft_f = np.fft.rfftfreq(len(x), 1/fs) amp = 2*np.abs(spec) / len(x) # 换算成振幅,与柱高同尺度 plt.figure(figsize=(8, 3.2)) plt.step(freqs, scan, where="mid", lw=2, label="你的自动扫描(逐频对拍)") plt.plot(fft_f, amp, ".", ms=6, alpha=0.65, label="np.fft.rfft(一步算完)") plt.xlabel("frequency (Hz)"); plt.ylabel("content (bar height)") plt.legend(); plt.tight_layout(); plt.show() print("扫描 1.00Hz:", round(scan[2],3), " 2.50Hz:", round(scan[8],3)) print("FFT @1.0Hz:", round(amp[int(1.0/0.5)],3), " @2.5Hz:", round(amp[int(2.5/0.5)],3)) print("练习:给 x 加一项 0.3*np.cos(2*np.pi*4.5*t),两条谱会同时长出新柱")
这张图的意义 两组点完全重合——你手算的"对拍取平均"和 FFT 是同一件事,FFT 只是把它算得飞快。你在探测器里拖滑块时发生的事,与生产级代码里一行 np.fft 发生的事,没有本质区别。加 4.5Hz 的练习跑完,两条谱会一起长出第三根柱——现在信号配方表归你管了。
重要:步长(网格)必须够细,否则成分会被漏掉 同一个信号,把扫描步长从 0.25Hz 改成 1Hz(从 1Hz 起步,只测 1、2、3、4…整数频率)再看一次:

看到了吗?2.5Hz 凭空消失了——它落在 2Hz 和 3Hz 两根柱子的缝里,哪次测量都没对上它,两次都精确抵消为 0。频谱只剩 1Hz 孤柱一根。所以"网格多细"决定了你能看清多细的频率差别(真实 STFT 里由帧长决定:帧越长,频率网格越细——第 5 课看 Qwen3-ASR 前端参数时会再遇到这件事)。补一句真话:真实世界的信号往往不恰好整周期,漏网的能量会摊到邻近柱上(叫"频谱泄漏"),不会像这里一样消失得这么干净,但结论不变——网格不够细,就看不清。

第四步:从频谱到语谱图,只差一个"逐帧"

现在你已经会读频谱这张"配方表"了。但语音是变化的——1 秒内音节都换了好几个。所以:

  1. 把波形切成小片(帧,约 25 毫秒一片,每 10 毫秒前移);
  2. 对每一帧单独做一遍刚才的"扫描 → 柱状图";
  3. 把每帧的柱状图竖起来、按时间横向排开 → 就是第 3 课看到的语谱图。
回看第 3 课 现在再打开第 3 课的声音实验室,选"元音「啊」":那一排水平条纹 = 每一帧的频谱里,谐波位置上都有柱子;条纹随时间不动 → 每帧配方相同。"扫频"的斜线 = 每帧配方的主峰逐帧升高。没有魔法了,全是测量。

检索练习

本课主看材料

去看(约 20 分钟,强烈推荐)

3Blue1Brown:But what is a Fourier transform? A visual introduction

它用"把波形缠绕到圆上求质心"的视角讲同一个道理,和今天"对拍取平均"互为印证(链接打不开就在 B 站/YouTube 搜这个标题,有官方中文版)。看完你会拥有两个互补的直觉,傅里叶从此不再是黑盒。

我是你的老师,别客气 哪一步"看着还是魔法",告诉我具体是拖到哪个滑块时糊涂的。想加料也行:比如藏三个频率的信号、带噪声的信号,一句话我就给你改出来。