特征提取基础:时域与频域特征
好,咱们今天聊聊特征提取。说实话,这是整个深度学习分类流程里最「接地气」的一步。你想想看,原始神经信号就是一堆电压值,几万甚至几十万个采样点堆在一起,模型根本看不懂。我们得帮它提炼出关键信息——这就是特征提取的意义。
我个人习惯把特征分成两大类:时域特征和频域特征。时域特征直接对时间序列下手,简单粗暴但有效;频域特征则把信号拆解成不同频率成分,能挖出时域里看不到的规律。两者结合,往往效果最好。
核心观点:特征提取不是越复杂越好。我见过太多人一上来就上小波包、Hilbert-Huang变换,结果过拟合得一塌糊涂。先把手头的时域和频域基础特征用明白,往往能解决80%的问题。
时域特征:最直观的信号描述
时域特征说白了就是直接对信号波形做统计。你拿到一段脑电或肌电信号,第一个念头肯定是:这个信号大概在什么水平?波动大不大?过零频繁吗?这三个问题正好对应三个经典特征。
1. 均值(Mean)
均值反映信号的直流分量。对于神经信号来说,均值通常被当作基线漂移处理——我们一般会先做去均值。但别急着把它扔掉,在某些场景下,比如不同肌肉收缩程度的肌电信号,均值差异其实很有区分度。
import numpy as np
def compute_mean(signal):
"""计算信号均值"""
return np.mean(signal)
# 示例:一段10秒的EEG信号,采样率250Hz
eeg_segment = np.random.randn(2500) # 模拟数据
mean_val = compute_mean(eeg_segment)
print(f"信号均值: {mean_val:.4f}")
我的经验:做运动想象分类时,我习惯把每个通道的均值单独作为一个特征。虽然简单,但C3和C4通道的均值差异往往能直接反映左右手想象。别小看它。
2. 方差(Variance)
方差衡量信号的离散程度。说白了就是信号抖得有多厉害。在神经信号里,方差大通常意味着有事件发生——比如肌肉收缩、癫痫发作、或者P300电位出现。
def compute_variance(signal):
"""计算信号方差"""
return np.var(signal)
var_val = compute_variance(eeg_segment)
print(f"信号方差: {var_val:.4f}")
注意:方差对异常值非常敏感。我曾经在处理一份肌电数据时,有个电极接触不良,方差直接飙到正常值的100倍。如果你发现某个通道的方差异常大,先检查数据质量,别急着当特征用。
3. 过零率(Zero Crossing Rate, ZCR)
过零率统计信号穿越零点的次数。高频信号过零快,低频信号过零慢。这个特征在语音信号处理里用得特别多,但在神经信号里同样有效——比如区分alpha波(8-12Hz)和beta波(13-30Hz)时,过零率能直接反映频率高低。
def compute_zcr(signal):
"""计算过零率"""
zero_crossings = np.where(np.diff(np.sign(signal)))[0]
return len(zero_crossings) / len(signal)
zcr_val = compute_zcr(eeg_segment)
print(f"过零率: {zcr_val:.4f}")
避坑指南:计算过零率前一定要先去除直流分量。我曾经忘了去均值,结果所有信号都在零线以上,过零率全是0——折腾了半天才发现问题。
频域特征:从频率角度看信号
时域特征虽然直观,但有个硬伤:它没法告诉你信号里有哪些频率成分。比如一段10Hz的alpha波和一段10Hz叠加了50Hz工频干扰的信号,时域波形可能看起来差不多,但频域里一目了然。
1. 功率谱密度(Power Spectral Density, PSD)
PSD描述信号功率在频率上的分布。常用的估计方法有Welch法——说白了就是把信号分段,每段做FFT,然后平均。这样做的好处是能降低噪声影响。
from scipy import signal
def compute_psd(signal, fs=250):
"""使用Welch法计算功率谱密度"""
freqs, psd = signal.welch(signal, fs, nperseg=fs*2)
return freqs, psd
fs = 250 # 采样率
freqs, psd = compute_psd(eeg_segment, fs)
# 打印前5个频率点的功率
for i in range(5):
print(f"频率 {freqs[i]:.2f} Hz: 功率 {psd[i]:.4f}")
关键参数:Welch法里的nperseg(每段长度)很关键。设得太短,频率分辨率低;设得太长,时间分辨率低。我一般取2秒的窗口长度,也就是nperseg=2*fs。对于250Hz采样率,就是500个点。
2. 带功率(Band Power)
带功率就是把PSD在特定频段上积分。神经信号有经典的频段划分:delta(0.5-4Hz)、theta(4-8Hz)、alpha(8-13Hz)、beta(13-30Hz)、gamma(30-50Hz)。每个频段的功率变化对应不同的脑状态。
def compute_band_power(freqs, psd, band):
"""计算指定频段的带功率"""
idx = np.where((freqs >= band[0]) & (freqs <= band[1]))[0]
return np.trapz(psd[idx], freqs[idx])
# 定义频段
bands = {
'delta': (0.5, 4),
'theta': (4, 8),
'alpha': (8, 13),
'beta': (13, 30),
'gamma': (30, 50)
}
# 计算各频段功率
for band_name, band_range in bands.items():
power = compute_band_power(freqs, psd, band_range)
print(f"{band_name} 带功率: {power:.4f}")
| 频段 | 频率范围 | 典型脑状态 | 我的使用建议 |
|---|---|---|---|
| Delta | 0.5-4 Hz | 深度睡眠 | 睡眠分期必用 |
| Theta | 4-8 Hz | 困倦、冥想 | 注意疲劳检测 |
| Alpha | 8-13 Hz | 放松、闭眼 | 我最常用的特征 |
| Beta | 13-30 Hz | 专注、活跃 | 运动想象分类 |
| Gamma | 30-50 Hz | 高级认知 | 信号质量好时再用 |
注意:带功率对工频干扰非常敏感。50Hz(或60Hz)的噪声会直接污染beta和gamma频段。我建议在计算带功率之前,先用陷波滤波器把工频干掉。不然你算出来的gamma带功率,可能有一半是电网贡献的。
特征组合策略
讲到这里,你可能会问:这些特征怎么组合?我个人的经验是:
- 基础配置:每个通道提取均值+方差+5个频段带功率,一共7个特征。对于32通道就是224维特征向量。这个配置能覆盖大部分分类任务。
- 进阶配置:加上过零率和PSD的峰值频率。当基础配置分类效果不理想时,这两个特征往往能提供额外信息。
- 避坑:别把所有特征一股脑全塞进去。特征维度太高,模型容易过拟合。我一般控制在每个通道10个特征以内。
一个小技巧:特征提取后,记得做标准化。我习惯用Z-score标准化——每个特征减去均值除以标准差。这样不同量纲的特征才能公平地参与后续的分类。
好了,时域和频域的基础特征就这些。说白了,均值、方差、过零率是「快准狠」的时域三件套,PSD和带功率则是频域里的「显微镜」。两者结合,你就能从神经信号里挖出足够多的信息来训练分类模型了。