文中代码及相关仿真脚本均可在 BlogCode 中获取,本篇代码集中于
DSP/dsp01目录下。痛苦系列 DSP 专栏:抛弃死记硬背与纯符号堆砌,从工程中的真实痛点切入,剖析直觉误区,由第一性原理推导数学工具与物理边界,并结合 Python 仿真验证。
前言
关于痛苦系列
从神经科学的角度看,我们大脑中神经元突触建立连接,并不是凭空产生的。它需要经过长时间的电化学反应,利用生物电刺激,将原本微弱的临时连接,固化成稳定的物理连接。再经过一段时间的刻意训练,使其成为思考过程中的短距通道。这是让大脑和思维保持锐化的过程,往往伴随着极度的不适与"痛苦"。
作者按:如果我们不主动去经历这种认知的"痛苦",那么大脑的突触就会遵循能量最低原则,逐渐萎缩。也希望能给读者诸君,提供一点微小的电化学辅助。
关于DSP
笔者这几年从事 DBS(脑深部刺激)/经皮电刺激神经疗法/BCI(脑机接口),离不开信号处理,而这一部分,从书本理论指导到工程实践,依然有巨大鸿沟。
论读书,笔者已比不上正值青年的学生(长期脱离痛苦导致的突触萎缩、技能退化),但求温故而知新。不同于以往的温故系列以求新为目的,本系列在刻意追求痛苦,毕竟在AI时代,已经对亲手写下诸多文字感到疲倦。
正文
本篇博客将聚焦于以下几个方面:
- 连续与离散的割裂:为什么我们要将世界切片?
- 奈奎斯特与混叠:如何保证切片后不丢失灵魂?
- 信号世界的"原子" :从
δ[n]开始理解系统的本质响应。- LTI系统与卷积:过去与现在的叠加效应。
一、 连续与离散的割裂:为什么我们要"切片"?
我们的现实世界是极其丝滑的,是一条在时间轴上无限连续的曲线。你说话时的声压、窗外的温度变化,在任何一个无限短的瞬间,都有一个确切的物理值。这就是连续时间信号。
但计算机是冷酷且容量有限的。它理解不了"无限",只能处理一串串离散的0和1。这就引出了数字信号处理的第一步:采样。
所谓采样,就是以固定的时间间隔(采样周期 T,或者说采样频率 Fs = 1/T),去连续的河流里舀一勺水。原本无限光滑的曲线,被强行降维成了一串离散的数字序列 [x1, x2, x3, ...]。
作者按:有时候妥协不仅是工程的必需,更是认知的必然。无限意味着不可控,离散化就是将不可控的连续域,切割成可计算的数字切片。
二、 奈奎斯特与混叠:防止高频的"伪装"
既然是"舀水",问题就来了:多久舀一次才不会漏掉重要的信息? 这就用到奈奎斯特-香农采样定理了。
为了能从采样后的离散信号中无失真地恢复出原始连续信号,我们需要满足一个极其重要的条件: 即采样频率必须至少是原始信号最高频率分量的两倍。
Fs>2⋅Fmax
其中, Fs 代表采样频率, Fmax 则是信号中包含的最高频率成分。 简单来说,你切片的刀必须挥得足够快,至少比信号变化最快的地方还要快一倍。
如果没有满足这个条件会发生什么?会发生混叠。
仿真对比:充足采样 vs 欠采样混叠
上图为充足采样(Fs = 200Hz),完整保留原始波形形态;下图为欠采样(Fs = 50Hz),高频 40Hz 分量被伪装成 10Hz 低频成分。
大家肯定都在电影里见过一种诡异的画面:一辆马车正在飞驰,但马车的车轮看起来却像是缓慢倒转或者干脆静止的。这是因为摄像机的帧率(也就是视觉的采样频率)低于车轮条纹旋转变化的频率。那些高频的真实运动,因为采样不足,被"伪装"成了低频的错觉。
作者按:混叠一旦发生,在数字世界里是无法挽回的。这就像做架构设计,如果最底层的抽象粒度太粗,上层哪怕用尽各种奇技淫巧,也补不回丢失的核心业务逻辑。
三、 信号世界的"原子":δ[n] 与 u[n]
有了数字序列,我们该如何用数学去描述和分析它?这时候我们需要找到信号世界的"原子"。
第一个原子是单位脉冲序列 δ[n] : 它只在 n = 0 时等于 1,其他时刻全为 0。这就像一根极其纯粹的探针。
为什么它如此重要? 因为任何极其复杂的离散序列,都可以被硬核地拆解。 它们都能表示为单位脉冲序列在不同时间点上的平移与加权组合。
xn=k=−∞∑∞xkδn−k
在物理意义上,这意味着任何波形都能看作是无数根不同高度的脉冲探针依次排列。 例如,有一个序列的值依次为 2, 4, 2。 我们可以将其拆解表述为:在第 0 秒高度为 2,在第 1 秒高度为 4,在第 2 秒高度为 2。
xn=2δn+4δn−1+2δn−2
第二个原子是单位阶跃序列 u[n] :在 n >= 0 时全为 1,之前全为 0。 它其实就是 δ[n] 的累加结果。
作者按:当我们将复杂的庞然大物,拆解为最基础的标准单元后,接下来的系统处理就可以"力大砖飞"。
四、 系统的过滤与卷积和:过去与现在的纠缠
有了信号原子,接下来就是处理信号的"容器"------系统 。 在DSP的世界里,我们极度偏爱一种特定类型的系统:线性时不变系统(LTI,Linear Time-Invariant)。
- 线性 :系统满足叠加性与齐次性。 你输入两份努力,就得到两份收获。 不同输入信号混合后的输出,等于它们单独输出的混合。 Ta⋅x1n+b⋅x2n=a⋅Tx1n+b⋅Tx2n
- 时不变:系统的特性不随时间改变。
- 因果性:输出只取决于当前和过去的输入,无法预知未来。
- 稳定性:有界的输入必产生有界的输出。
一旦确定一个系统是 LTI 系统,我们就迎来了 DSP 领域最伟大的核心公式:卷积和 (Convolution)。
假设系统的单位脉冲响应 是 hn。
也就是说,如果你往系统里扔一个最单纯的单位脉冲 δn,它吐出来的反馈波形就是 hn。
那么,当你输入任意复杂的信号 xn 时。 系统的输出 yn 将是输入信号与系统脉冲响应的卷积。
yn=xn∗hn=k=−∞∑∞xkhn−k
作者按:初学者接触卷积很头疼,越往后越头疼。我在图书馆借阅了几本相关书籍,作者会专注于给出公式、公式证明、计算过程,而工程应用,更注重是什么、为什么用、怎么用。我们应当理解,一个系统,不仅仅只有当前的输入,还有过去所有历史输入所产生的"余音"在这个时刻的干涉与叠加
让我们暂时忘却四步法 :翻转 -> 平移 -> 逐点相乘 -> 求和。这个方法太机械化了,我喜欢用空谷回声来具象化这个过程:
想象你站在一个宽度大约340米的山谷中(声音在空气中往返一次恰好约1秒)。你作为信号源,每秒钟大喊一声"号子"(输入序列
x[n])。而你的耳朵,就是接收系统。如果你只在第0秒喊了一声(也就是输入了一个单位脉冲
δ[n],假设响度为1),你会听到什么?
- 第0秒 :你听到自己最原始的喊声,响度为
1(即h[0] = 1)。- 第1秒 :声音从山壁反弹回来,经过衰减,你听到第一声回声,响度变成了
0.5(即h[1] = 0.5)。- 第2秒 :回声在两侧山壁再次反弹,你听到第二声回声,响度衰减为
0.25(即h[2] = 0.25)。- 第3秒 :回声消散,响度为
0(即h[3] = 0)。
这串回声的衰减规律 h[n] = {1, 0.5, 0.25, 0},完美表征了这个山谷的声学特性,这就是系统的单位脉冲响应。
现在,为了给自己打气,你开始每秒连续喊号子 。那么在第2秒时,你的耳朵(系统输出
y[2])究竟听到了什么混响?
- 你正在喊第3个号子(当前输入
x[2]的原声,对应x[2] * h[0])- 1秒前喊的那个号子,刚好形成第1次反弹回声传到耳朵(过去输入
x[1]的一次回声,对应x[1] * h[1])- 2秒前喊的第1个号子,刚好形成第2次反弹回声传到耳朵(更早输入
x[0]的二次回声,对应x[0] * h[2])
所以,你在第 2 秒听到的所有声音的叠加结果如下:
y2=x2⋅h0+x1⋅h1+x0⋅h2
这就是将当前原声与所有历史回声进行加和。
把这个推导泛化到任意时刻 n,这正是极其优美的卷积公式。
仿真对比:空谷回声与脉冲卷积合成
图 1 为输入序列(喊号子脉冲);图 2 为山谷单位脉冲响应(衰减特性);图 3 为卷积合成输出(耳朵听到的原声与历史回声叠加)。
当我们理解之后,在工程上都是运用函数库来完成卷积工作。
python
import numpy as np
def demo_convolution():
# 输入信号
x = np.array([2, 4, 2])
# 系统的单位脉冲响应
h = np.array([1, 0.5])
# 调库,计算卷积
y = np.convolve(x, h)
print(f"输入 x: {x}")
print(f"系统 h: {h}")
print(f"输出 y: {y}")
if __name__ == '__main__':
demo_convolution()
注:上述Demo仅做演示使用。在实际商用的实时DSP系统中,如果数据流无限长,我们不能直接使用全量的 np.convolve,而是要考虑 Overlap-Add 或 Overlap-Save 算法,否则会导致内存爆炸。
五、 尾声:在痛苦中进化
这一篇是开胃小菜,让我们适应写作和阅读的痛苦。今天 Gimini 告诉我一段很有趣的话,分享给读者诸君:
我们常常会在宏大的架构设计中迷失,却忘了最底层的数据流究竟是如何被切割、被离散、被卷积的。保持这种对底层的敬畏与痛苦的思考,也许就是我们作为工程师对抗内卷与时间流逝的唯一方式。
附录:本篇可运行验证代码
本篇涉及的全部仿真代码均已开源在代码仓库中:DSP/dsp01/pain_series_01_demo.py。
bash
# 依赖安装
pip install numpy matplotlib scipy
# 运行本篇仿真实验
python DSP/dsp01/pain_series_01_demo.py
上图为充足采样(Fs = 200Hz),完整保留原始波形形态;下图为欠采样(Fs = 50Hz),高频 40Hz 分量被伪装成 10Hz 低频成分。
图 1 为输入序列(喊号子脉冲);图 2 为山谷单位脉冲响应(衰减特性);图 3 为卷积合成输出(耳朵听到的原声与历史回声叠加)。