4、数据增强技巧:时间扭曲、噪声注入、通道置换、滑动窗口切割
小样本学习,说白了就是数据不够用。你辛辛苦苦采集了几十段神经信号,模型一训练就过拟合,验证集上准确率惨不忍睹。这时候,数据增强就是你的救命稻草。
我个人习惯把数据增强叫做「无中生有」的艺术。不是凭空捏造,而是基于先验知识,对现有样本做合理变换。神经信号和图像不一样,你不能随便旋转裁剪,得尊重它的物理意义。
4.1 时间扭曲:让信号「伸缩」一下
时间扭曲,就是沿着时间轴对信号做非线性拉伸或压缩。为什么有效?因为同一个动作,不同人做出来的速度不一样。有人快,有人慢,但波形形态是相似的。
具体做法很简单:
def time_warp(signal, sigma=0.2):
"""
signal: (T, C) 时间×通道
sigma: 扭曲强度
"""
T = signal.shape[0]
# 生成随机扭曲路径
warp = np.cumsum(
np.random.normal(0, sigma, T)
)
warp = warp - warp[0] # 保证起点对齐
# 插值重采样
new_indices = np.linspace(0, T-1, T) + warp
new_indices = np.clip(new_indices, 0, T-1)
warped = np.zeros_like(signal)
for c in range(signal.shape[1]):
warped[:, c] = np.interp(
np.arange(T), new_indices, signal[:, c]
)
return warped
4.2 噪声注入:给信号加点「干扰」
真实世界的神经信号,从来都不是干净的。工频干扰、肌电噪声、电极漂移……你想想看,如果模型只在纯净数据上训练,到了实际场景肯定崩。
噪声注入有两种常见方式:
- 高斯噪声:最简单,但要注意信噪比。我一般控制在 SNR=10~20dB。
- 真实噪声模板:从空白段截取噪声,叠加到信号上。效果比高斯噪声好很多。
def add_real_noise(signal, noise_template, snr_db=15):
"""
noise_template: 从空白段截取的噪声
"""
signal_power = np.mean(signal**2)
noise_power = np.mean(noise_template**2)
scale = np.sqrt(
signal_power / (noise_power * 10**(snr_db/10))
)
return signal + scale * noise_template
4.3 通道置换:打乱空间布局
通道置换,就是随机交换不同电极通道的数据。这招对多通道脑电、肌电信号特别有效。为什么?因为电极位置本身就有冗余性,相邻通道的信息高度相关。
但要注意:不能完全随机置换,否则会破坏空间拓扑结构。我建议用「块置换」——把相邻的 2~4 个通道作为一个块,在块内随机交换。
| 置换方式 | 适用场景 | 风险 |
|---|---|---|
| 完全随机置换 | 通道数少(<8) | 破坏空间结构 |
| 块内置换 | 高密度阵列 | 块大小需调参 |
| 镜像置换 | 左右对称布局 | 仅适用于对称电极 |
def channel_swap(signal, block_size=4):
"""
signal: (T, C)
"""
C = signal.shape[1]
n_blocks = C // block_size
blocks = np.array_split(
np.arange(C), n_blocks
)
swapped = signal.copy()
for block in blocks:
perm = np.random.permutation(block)
swapped[:, block] = signal[:, perm]
return swapped
4.4 滑动窗口切割:把长信号「切碎」
滑动窗口切割,是数据增强里最朴实但最有效的一招。一段 10 秒的信号,用 2 秒窗口、50% 重叠去切,能切出 9 段样本。数据量直接翻了 9 倍。
但这里有个坑:窗口长度怎么选?
- 太短:丢失了低频成分,比如运动相关电位。
- 太长:样本数量上不去,而且容易包含多个动作。
我个人的经验是:窗口长度取目标事件持续时间的 1.5~2 倍。比如运动想象任务,事件持续 3 秒,窗口就设 4.5~6 秒。
def sliding_window(signal, window_len, step_len):
"""
signal: (T, C)
"""
T = signal.shape[0]
windows = []
start = 0
while start + window_len <= T:
windows.append(
signal[start:start+window_len]
)
start += step_len
return np.array(windows)
核心要点:滑动窗口切割时,一定要保证每个窗口的标签正确。如果窗口跨越了两个不同的事件,要么丢弃,要么用多数投票决定标签。
4.5 四种方法的组合策略
单独用某一种方法,效果有限。我建议按以下顺序组合:
- 先用滑动窗口切割,把长信号切成短样本
- 对每个短样本,随机选择是否做时间扭曲(概率 30%)
- 然后随机选择是否加噪声(概率 50%)
- 最后随机选择是否做通道置换(概率 20%)
这个组合策略,是我在多个项目里试出来的。你想想看,如果四种方法全用上,每个原始样本能生成 10~20 个增强样本,小样本问题基本就解决了。
好了,这四种方法够你用了。记住:数据增强不是万能药,但它能帮你把有限的数据发挥出最大价值。下次遇到小样本问题,先别急着换模型,试试这些技巧再说。