4. 离散傅里叶变换(DFT):DFT的数学原理,DFT与相位的关系

离散傅里叶变换,简称 DFT,是咱们数字信号处理里最核心的工具之一。说白了,它就是把一段有限长的离散信号,从时域搬到频域。你想想看,我们平时采集到的信号,比如麦克风录下的声音、传感器读到的振动,都是一串串离散的数值。DFT 能告诉你,这串数值里都藏着哪些频率成分,每个成分的幅度有多大,相位又是多少。

我个人习惯把 DFT 看作是「连续傅里叶变换的数字化版本」。因为计算机只能处理离散的、有限长的数据,所以我们必须把傅里叶变换改造成适合计算机运算的形式。嗯,这里要注意,DFT 并不是近似,它本身就是一种精确的数学变换,只是定义域和值域都是离散的。

4.1 DFT 的数学定义

先看公式,别怕,我会拆开讲。

对于一个长度为 N 的离散序列 x[n](n = 0, 1, ..., N-1),它的 DFT 定义为:

X[k] = Σ_{n=0}^{N-1} x[n] · e^{-j(2π/N)kn} , k = 0, 1, ..., N-1

这个公式里,X[k] 就是频域的第 k 个点。k 对应的是数字频率,它和实际频率的关系是 f_k = k · fs / N,其中 fs 是采样率。

我刚开始学的时候,总觉得这个公式很抽象。后来我把它拆成三步理解:

  1. 旋转因子:e^{-j(2π/N)kn} 是一个在复平面上旋转的向量,旋转速度由 k 决定。
  2. 加权求和:把原始信号 x[n] 和这个旋转向量相乘,再对 n 求和。
  3. 相关性检测:如果 x[n] 里包含频率为 k 的成分,求和结果就会很大;否则接近零。

说白了,DFT 就是在做「猜你喜欢」——用不同频率的旋转向量去试探信号,看哪个频率和信号最「合拍」。

4.2 DFT 与相位的关系

DFT 的结果 X[k] 是一个复数。这个复数包含了两个信息:幅度和相位。

我们通常这样提取:

  • 幅度谱:|X[k]| = sqrt(Re(X[k])² + Im(X[k])²)
  • 相位谱:∠X[k] = atan2(Im(X[k]), Re(X[k]))

相位谱告诉我们什么?它告诉我们每个频率分量在时间轴上的起始位置。举个例子,两个频率相同的正弦波,一个从 0 度开始,一个从 90 度开始,它们的 DFT 相位谱就会差 90 度。

核心要点:DFT 的相位信息是绝对相位,它依赖于我们选取的时域窗口起始点。如果你把信号平移几个采样点,相位谱会发生变化,但幅度谱不变。这一点在做相位计算时特别重要。

我在项目中遇到过这样一个坑:用 DFT 分析一个振动信号,发现相位谱乱糟糟的,完全看不出规律。后来才意识到,是因为信号没有做整周期截断,产生了频谱泄漏,相位信息被污染了。嗯,这个后面会详细讲。

4.3 DFT 的矩阵形式

DFT 还可以写成矩阵形式,这样更直观,也方便用 Python 实现。

定义 DFT 矩阵 W,其元素为:

W_{k,n} = e^{-j(2π/N)kn}

那么 DFT 可以写成:

X = W · x

其中 x 是 N×1 的时域向量,X 是 N×1 的频域向量,W 是 N×N 的矩阵。

这个矩阵 W 有一个非常重要的性质:它是正交矩阵(差一个缩放因子)。这意味着 DFT 是可逆的,逆变换就是:

x[n] = (1/N) · Σ_{k=0}^{N-1} X[k] · e^{j(2π/N)kn}

你看,逆变换和正变换只差一个符号和缩放因子。这种对称性让 DFT 在工程实现上非常优雅。

4.4 用 Python 实现 DFT

理论讲完了,咱们直接上代码。我习惯先写一个最朴素的 DFT,不调任何库,让你看清楚每一步在干什么。

import numpy as np

def dft_naive(x):
    """
    最朴素的 DFT 实现
    参数 x: 输入信号,长度为 N 的一维数组
    返回 X: 频域复数数组
    """
    N = len(x)
    X = np.zeros(N, dtype=complex)
    
    for k in range(N):
        for n in range(N):
            angle = -2 * np.pi * k * n / N
            X[k] += x[n] * np.exp(1j * angle)
    
    return X

# 测试:生成一个 10Hz 的正弦波,采样率 100Hz,时长 1 秒
fs = 100
t = np.arange(0, 1, 1/fs)
f0 = 10
x = np.sin(2 * np.pi * f0 * t)

# 计算 DFT
X = dft_naive(x)

# 提取幅度和相位
magnitude = np.abs(X)
phase = np.angle(X)

print(f"信号长度 N = {len(x)}")
print(f"幅度谱最大值索引: {np.argmax(magnitude)}")
print(f"对应频率: {np.argmax(magnitude) * fs / len(x):.1f} Hz")
print(f"该频率的相位: {phase[np.argmax(magnitude)]:.3f} 弧度")

运行这段代码,你会看到幅度谱最大值在索引 10 的位置,对应频率 10Hz,相位接近 -π/2 弧度。为什么是 -π/2?因为 sin 函数在 DFT 的定义下,初始相位就是 -π/2。这个细节我当年也困惑了很久。

小技巧:实际工程中千万别用这种双重循环的 DFT,复杂度是 O(N²)。N=1024 时还能忍,N=10000 时你会等到怀疑人生。FFT(快速傅里叶变换)把复杂度降到了 O(N log N),这才是工程实践的标准选择。但我们先用朴素 DFT 理解原理,后面再讲 FFT。

4.5 DFT 的性质一览

DFT 有很多漂亮的性质,我整理了一个表格,方便你查阅:

性质 时域 频域
线性 a·x[n] + b·y[n] a·X[k] + b·Y[k]
时移 x[n - n₀] X[k] · e^{-j(2π/N)kn₀}
频移 x[n] · e^{j(2π/N)k₀n} X[k - k₀]
共轭对称 x[n] 为实信号 X[k] = X*[-k](共轭对称)
帕塞瓦尔定理 Σ|x[n]|² (1/N) · Σ|X[k]|²
卷积定理 x[n] * y[n] X[k] · Y[k]

这里面,时移性质和我们做相位计算关系最密切。你看,时域平移 n₀ 个点,频域只是乘了一个相位因子 e^{-j(2π/N)kn₀},幅度谱完全不变。这就是为什么我说「相位反映的是时间偏移」。

避坑指南:我曾经在分析雷达回波信号时,直接用 DFT 的相位值去做距离解算,结果误差很大。后来发现是因为信号起始点没有对齐,导致相位里混入了固定的偏移量。正确的做法是先做互相关,找到时延,再补偿这个相位偏移。记住:DFT 的相位是相对于你选取的时域窗口起点的,不是绝对的。

4.6 知识结构图

下面这张图总结了 DFT 的核心知识体系,从时域到频域的映射关系一目了然:

DFT 知识体系结构图 时域离散信号 x[n], n=0..N-1 DFT 变换 X[k] = Σ x[n]·e^{-j2πkn/N} 频域复数序列 X[k], k=0..N-1 幅度谱 |X[k]| 相位谱 ∠X[k] DFT 重要性质 线性 · 时移 · 频移 共轭对称 · 帕塞瓦尔 卷积定理 工程实现方式 朴素 DFT(O(N²)) FFT 快速算法(O(N log N)) NumPy / SciPy 库函数 DFT 将时域离散信号映射到频域,提取幅度和相位信息 工程中通常使用 FFT 加速计算

4.7 小结

这一章我们讲了 DFT 的数学原理,以及它和相位的关系。核心就三点:

  • DFT 把时域离散信号变换到频域,结果 X[k] 是复数
  • 相位谱 ∠X[k] 反映每个频率分量的时间偏移
  • DFT 的相位是相对于时域窗口起点的,不是绝对相位