痛苦系列 | DSP-01 从连续到离散:DSP基础与采样

文中代码及相关仿真脚本均可在 BlogCode 中获取,本篇代码集中于 DSP/dsp01 目录下。

痛苦系列 DSP 专栏:抛弃死记硬背与纯符号堆砌,从工程中的真实痛点切入,剖析直觉误区,由第一性原理推导数学工具与物理边界,并结合 Python 仿真验证。

前言

关于痛苦系列

从神经科学的角度看,我们大脑中神经元突触建立连接,并不是凭空产生的。它需要经过长时间的电化学反应,利用生物电刺激,将原本微弱的临时连接,固化成稳定的物理连接。再经过一段时间的刻意训练,使其成为思考过程中的短距通道。这是让大脑和思维保持锐化的过程,往往伴随着极度的不适与"痛苦"。

作者按:如果我们不主动去经历这种认知的"痛苦",那么大脑的突触就会遵循能量最低原则,逐渐萎缩。也希望能给读者诸君,提供一点微小的电化学辅助。

关于DSP

笔者这几年从事 DBS(脑深部刺激)/经皮电刺激神经疗法/BCI(脑机接口),离不开信号处理,而这一部分,从书本理论指导到工程实践,依然有巨大鸿沟。

论读书,笔者已比不上正值青年的学生(长期脱离痛苦导致的突触萎缩、技能退化),但求温故而知新。不同于以往的温故系列以求新为目的,本系列在刻意追求痛苦,毕竟在AI时代,已经对亲手写下诸多文字感到疲倦。

正文

本篇博客将聚焦于以下几个方面:

  • 连续与离散的割裂:为什么我们要将世界切片?
  • 奈奎斯特与混叠:如何保证切片后不丢失灵魂?
  • 信号世界的"原子" :从 δ[n] 开始理解系统的本质响应。
  • LTI系统与卷积:过去与现在的叠加效应。

一、 连续与离散的割裂:为什么我们要"切片"?

我们的现实世界是极其丝滑的,是一条在时间轴上无限连续的曲线。你说话时的声压、窗外的温度变化,在任何一个无限短的瞬间,都有一个确切的物理值。这就是连续时间信号。

但计算机是冷酷且容量有限的。它理解不了"无限",只能处理一串串离散的0和1。这就引出了数字信号处理的第一步:采样。

所谓采样,就是以固定的时间间隔(采样周期 T,或者说采样频率 Fs = 1/T),去连续的河流里舀一勺水。原本无限光滑的曲线,被强行降维成了一串离散的数字序列 [x1, x2, x3, ...]。

作者按:有时候妥协不仅是工程的必需,更是认知的必然。无限意味着不可控,离散化就是将不可控的连续域,切割成可计算的数字切片。

二、 奈奎斯特与混叠:防止高频的"伪装"

既然是"舀水",问题就来了:多久舀一次才不会漏掉重要的信息? 这就用到奈奎斯特-香农采样定理了。

为了能从采样后的离散信号中无失真地恢复出原始连续信号,我们需要满足一个极其重要的条件: 即采样频率必须至少是原始信号最高频率分量的两倍。
Fs>2⋅ Fmax F_s > 2 \cdot F_{max} Fs>2⋅Fmax

其中, Fs F_s Fs 代表采样频率, Fmax F_{max} Fmax 则是信号中包含的最高频率成分。 简单来说,你切片的刀必须挥得足够快,至少比信号变化最快的地方还要快一倍。

如果没有满足这个条件会发生什么?会发生混叠。

仿真对比:充足采样 vs 欠采样混叠 上图为充足采样(Fs = 200Hz),完整保留原始波形形态;下图为欠采样(Fs = 50Hz),高频 40Hz 分量被伪装成 10Hz 低频成分。

大家肯定都在电影里见过一种诡异的画面:一辆马车正在飞驰,但马车的车轮看起来却像是缓慢倒转或者干脆静止的。这是因为摄像机的帧率(也就是视觉的采样频率)低于车轮条纹旋转变化的频率。那些高频的真实运动,因为采样不足,被"伪装"成了低频的错觉。

作者按:混叠一旦发生,在数字世界里是无法挽回的。这就像做架构设计,如果最底层的抽象粒度太粗,上层哪怕用尽各种奇技淫巧,也补不回丢失的核心业务逻辑。

三、 信号世界的"原子":δ[n] 与 u[n]

有了数字序列,我们该如何用数学去描述和分析它?这时候我们需要找到信号世界的"原子"。

第一个原子是单位脉冲序列 δ[n] : 它只在 n = 0 时等于 1,其他时刻全为 0。这就像一根极其纯粹的探针。

为什么它如此重要? 因为任何极其复杂的离散序列,都可以被硬核地拆解。 它们都能表示为单位脉冲序列在不同时间点上的平移与加权组合。
xn= ∑k=−∞∞ xkδn−kxn = \sum_{k=-\infty}^{\infty} xk \deltan-k xn=k=−∞∑∞xkδn−k

在物理意义上,这意味着任何波形都能看作是无数根不同高度的脉冲探针依次排列。 例如,有一个序列的值依次为 2, 4, 2。 我们可以将其拆解表述为:在第 0 秒高度为 2,在第 1 秒高度为 4,在第 2 秒高度为 2。
xn=2δn+4δn−1+2δn−2xn = 2\deltan + 4\deltan-1 + 2\deltan-2 xn=2δn+4δn−1+2δn−2

第二个原子是单位阶跃序列 u[n] :在 n >= 0 时全为 1,之前全为 0。 它其实就是 δ[n] 的累加结果。

作者按:当我们将复杂的庞然大物,拆解为最基础的标准单元后,接下来的系统处理就可以"力大砖飞"。

四、 系统的过滤与卷积和:过去与现在的纠缠

有了信号原子,接下来就是处理信号的"容器"------系统 。 在DSP的世界里,我们极度偏爱一种特定类型的系统:线性时不变系统(LTI,Linear Time-Invariant)。

  • 线性 :系统满足叠加性与齐次性。 你输入两份努力,就得到两份收获。 不同输入信号混合后的输出,等于它们单独输出的混合。 T a⋅x1n+b⋅x2n =a⋅T x1n +b⋅T x2n T{a \cdot x_1n + b \cdot x_2n} = a \cdot T{x_1n} + b \cdot T{x_2n} Ta⋅x1n+b⋅x2n=a⋅Tx1n+b⋅Tx2n
  • 时不变:系统的特性不随时间改变。
  • 因果性:输出只取决于当前和过去的输入,无法预知未来。
  • 稳定性:有界的输入必产生有界的输出。

一旦确定一个系统是 LTI 系统,我们就迎来了 DSP 领域最伟大的核心公式:卷积和 (Convolution)。

假设系统的单位脉冲响应 是 hnhn hn。

也就是说,如果你往系统里扔一个最单纯的单位脉冲 δn\deltan δn,它吐出来的反馈波形就是 hnhn hn。

那么,当你输入任意复杂的信号 xnxn xn 时。 系统的输出 ynyn yn 将是输入信号与系统脉冲响应的卷积。
yn=xn∗hn= ∑k=−∞∞ xkhn−kyn = xn * hn = \sum_{k=-\infty}^{\infty} xk hn-k 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⋅h2y2 = x2 \cdot h0 + x1 \cdot h1 + x0 \cdot h2 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
相关推荐
高频因子挖掘机2 小时前
同一只股票前复权和不复权价格对不上?先检查这几个口径
后端·github·api
Bazingga2 小时前
Harness学习笔记:从马具到工程外壳
后端
法欧特斯卡雷特2 小时前
Kotlin 新特性抢先看:伴生扩展与伴生块
后端·面试·开源
桃李醉春风3 小时前
被微信拒审那天,我才真正学会 Vibecoding:一个人 + AI,3 万行代码、1.8 万张素材的小程序全复盘
后端
她的男孩3 小时前
打印模板草稿能保存,一点发布就报主从关系:我们把校验拆成了两档
java·spring boot·后端
Bazingga3 小时前
Embabel学习笔记:把Agent的决策权从LLM手里拿回来
后端
用户8314550980313 小时前
基于 Firecracker 自建 Agent 沙箱集群:状态机 + 文件管理 + 进程编排,附 E2B 生产经验校准
后端
武子康4 小时前
LingBot-VA 2.0 深度解析:为什么要同时预测未来世界与机器人动作
人工智能·后端·agent
wno7044 小时前
Spring Boot整合Quartz
java·spring boot·后端