第三章 传统去噪方法回顾:滤波、小波与ICA
说实话,每次带新人做神经信号处理,我都会先问一个问题:「你知道深度学习之前,大家是怎么去噪的吗?」
这个问题很关键。你想想看,如果连传统方法都搞不清楚,你怎么知道深度学习到底牛在哪?我在项目里见过太多人,一上来就扔给神经网络,结果模型学了一堆噪声特征,反而把信号搞坏了。
今天咱们就把传统去噪的几把刷子捋一遍。嗯,都是经典,但各有各的脾气。
3.1 滤波:最朴素的去噪手段
滤波这玩意儿,说白了就是「让想要的频率通过,挡住不想要的」。神经信号里,不同成分的频率范围大致是这样的:
| 信号成分 | 频率范围 | 典型噪声 |
|---|---|---|
| 局部场电位(LFP) | 0.5–100 Hz | 工频干扰(50/60 Hz) |
| 动作电位(Spike) | 300–5000 Hz | 肌电噪声、电极漂移 |
| EEG | 0.5–40 Hz | 眼动伪迹、呼吸干扰 |
3.1.1 低通滤波
低通滤波就是「让低频通过,高频滚蛋」。我习惯用它来去除高频噪声,比如肌电干扰。举个例子,EEG信号里我们通常只关心40 Hz以下,那直接设个截止频率40 Hz的低通滤波器就行。
from scipy import signal
import numpy as np
# 设计一个4阶巴特沃斯低通滤波器,截止频率40 Hz
fs = 1000 # 采样率
cutoff = 40
b, a = signal.butter(4, cutoff / (fs/2), btype='low')
filtered_signal = signal.filtfilt(b, a, raw_signal)
filtfilt 而不是 lfilter,因为前者是零相位滤波,不会造成信号偏移。我在处理Spike数据时吃过这个亏,后来再也不敢用 lfilter 了。
3.1.2 高通滤波
高通滤波正好相反——「让高频通过,低频滚蛋」。它主要用来去除基线漂移。你想想看,电极在脑子里待久了,信号基线会慢慢飘,这时候高通滤波就派上用场了。
# 高通滤波,截止频率0.5 Hz,去除基线漂移
b, a = signal.butter(4, 0.5 / (fs/2), btype='high')
filtered_signal = signal.filtfilt(b, a, raw_signal)
3.1.3 带通与带阻滤波
带通滤波就是「只让某个频段通过」,比如提取Spike信号时,我们通常用300–3000 Hz的带通。带阻滤波则是「挡住某个频段」,最经典的就是50 Hz工频陷波。
# 带通滤波:300–3000 Hz
b, a = signal.butter(4, [300, 3000] / (fs/2), btype='band')
spike_band = signal.filtfilt(b, a, raw_signal)
# 带阻滤波:陷波50 Hz
b, a = signal.iirnotch(50, 30, fs)
clean_signal = signal.filtfilt(b, a, raw_signal)
嗯,这里要注意:陷波滤波器的Q值(品质因数)很关键。Q值太高会引入振铃效应,Q值太低又去不干净。我个人习惯用30,效果比较均衡。
3.2 小波变换去噪
滤波的问题在哪?它假设信号和噪声的频带是分开的。但现实哪有这么理想?神经信号和噪声经常「你中有我,我中有你」。这时候小波变换就登场了。
小波变换的核心思想是:把信号分解到不同尺度和时间位置上。噪声通常集中在精细尺度(高频细节),而信号则分布在较粗尺度。
- 分解:选择小波基(比如db4、sym8),对信号做多层小波分解
- 阈值处理:对细节系数做软阈值或硬阈值处理
- 重构:用处理后的系数重构信号
import pywt
def wavelet_denoise(signal, wavelet='db4', level=5, method='soft'):
# 1. 小波分解
coeffs = pywt.wavedec(signal, wavelet, level=level)
# 2. 计算阈值(通用阈值公式)
sigma = np.median(np.abs(coeffs[-1])) / 0.6745
threshold = sigma * np.sqrt(2 * np.log(len(signal)))
# 3. 对细节系数做阈值处理
coeffs_thresh = [coeffs[0]] # 近似系数不动
for i in range(1, len(coeffs)):
coeffs_thresh.append(pywt.threshold(coeffs[i], threshold, mode=method))
# 4. 重构信号
return pywt.waverec(coeffs_thresh, wavelet)
小波变换的优势在于「时频局部化」——它既能抓住信号的突变(比如Spike),又能保留低频趋势。但缺点也很明显:阈值怎么选?小波基怎么定?这些都需要经验。
3.3 独立成分分析(ICA)
ICA这玩意儿,说白了就是「盲源分离」。它假设你观测到的信号是多个独立源信号的线性混合。比如EEG里,大脑信号、眼电、肌电、心电都是独立的源,ICA能把它们拆开。
我习惯用ICA来处理眼动伪迹。你想想看,被试眨一下眼,EEG信号里就出现一个巨大的尖峰,这玩意儿用滤波根本去不掉,因为它和脑电信号频带重叠。但ICA可以。
from sklearn.decomposition import FastICA
# 假设 data 是 [通道数, 时间点] 的矩阵
ica = FastICA(n_components=32, random_state=42)
sources = ica.fit_transform(data.T) # 得到源信号
# 识别并去除眼动成分(通常看成分的时域波形和地形图)
# 假设第3个成分是眼动伪迹
sources[:, 2] = 0
# 重构信号
clean_data = ica.inverse_transform(sources).T
3.4 传统方法的局限性
好了,咱们把传统方法都过了一遍。你可能会问:「既然有这么多方法,为什么还要用深度学习?」
问得好。我总结一下传统方法的几个硬伤:
| 方法 | 局限性 | 典型场景 |
|---|---|---|
| 滤波 | 频带重叠时无效;会损失信号成分 | 工频干扰、基线漂移 |
| 小波变换 | 阈值选择依赖经验;小波基不通用 | Spike检测、EEG去噪 |
| ICA | 需要多通道;假设独立性;不稳定 | 眼动伪迹、肌电去除 |
说白了,传统方法都是「手工特征+固定规则」。你告诉它「噪声是高频的」,它就滤掉高频;你告诉它「噪声是独立的」,它就做ICA。但现实中的噪声哪有这么听话?
举个例子,我在做自由行为小鼠的神经信号记录时,小鼠跑动产生的运动伪迹,既有低频漂移,又有高频抖动,还和神经信号高度相关。滤波?滤不干净。小波?阈值不好定。ICA?单通道数据根本没法用。
这就是为什么我们需要深度学习——它能自动学习噪声的模式,不需要你手工设计规则。但话说回来,传统方法也不是一无是处。我现在的项目里,经常先用滤波做预处理,再用深度学习做精细去噪。两者结合,效果最好。
嗯,这一章就到这。传统方法就像老黄牛,虽然慢,但踏实。下一章咱们就要进入深度学习的世界了,准备好迎接新玩具吧。