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
我的经验:sigma 值别设太大,0.1~0.3 就够了。我曾经试过 0.5,结果波形扭曲得面目全非,模型直接学废了。

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 四种方法的组合策略

单独用某一种方法,效果有限。我建议按以下顺序组合:

  1. 先用滑动窗口切割,把长信号切成短样本
  2. 对每个短样本,随机选择是否做时间扭曲(概率 30%)
  3. 然后随机选择是否加噪声(概率 50%)
  4. 最后随机选择是否做通道置换(概率 20%)

这个组合策略,是我在多个项目里试出来的。你想想看,如果四种方法全用上,每个原始样本能生成 10~20 个增强样本,小样本问题基本就解决了。

避坑指南:我曾经在训练时同时用了所有增强方法,结果模型训练特别慢,而且效果反而下降了。后来发现是增强强度太大,破坏了信号的可识别特征。记住:数据增强不是越多越好,适度才是关键。
数据增强技巧体系 原始神经信号 时间扭曲 非线性拉伸/压缩 噪声注入 高斯/真实噪声 通道置换 块内随机交换 滑动窗口切割 重叠切片 组合策略:切割 → 扭曲(30%) → 噪声(50%) → 置换(20%) 数据量可提升 10~20 倍,有效缓解过拟合

好了,这四种方法够你用了。记住:数据增强不是万能药,但它能帮你把有限的数据发挥出最大价值。下次遇到小样本问题,先别急着换模型,试试这些技巧再说。

专注资料整理