3、数据预处理与标注:让原始数据“开口说话”

好,咱们接着往下走。上一章我们把加速度数据从STM32里读出来了,但说实话,那会儿的数据还是“毛坯房”——噪声大、格式乱、没标签。这一章,我们就来给它“精装修”。

我个人习惯把数据预处理和标注放在一起讲,因为这两步是连着的。你想想看,数据不干净,模型学到的就是噪声;数据没标签,模型连学什么都不知道。所以,这一步做扎实了,后面训练模型才能事半功倍。

3.1 数据滤波:把“抖”掉的部分找回来

加速度传感器有个毛病——特别敏感。你明明站着不动,它输出的数据却在上下跳。这不是传感器坏了,是高频噪声在捣乱。

我在项目中遇到过这种情况:采集静止数据时,原始数据的标准差居然有0.15g。这要是直接拿去训练,模型肯定以为你在抖腿。所以,滤波是第一步。

常用的滤波方法有两种:

  • 移动平均滤波:简单粗暴,适合实时处理。取前后N个点的平均值,噪声就平滑了。
  • 低通滤波:更专业一些。加速度信号的变化频率其实不高(走路、跑步也就几赫兹),而噪声往往是高频的。用低通滤波把高频部分切掉,保留有用的低频成分。

我个人更推荐低通滤波,尤其是用Python的scipy.signal库实现。下面是我常用的代码:

import numpy as np
from scipy.signal import butter, filtfilt

def lowpass_filter(data, cutoff=5, fs=50, order=4):
    """
    低通滤波器
    :param data: 原始加速度数据 (N, 3)
    :param cutoff: 截止频率 (Hz),走路跑步一般设5Hz
    :param fs: 采样频率 (Hz),我们之前设的是50Hz
    :param order: 滤波器阶数
    """
    nyquist = 0.5 * fs
    normal_cutoff = cutoff / nyquist
    b, a = butter(order, normal_cutoff, btype='low', analog=False)
    y = filtfilt(b, a, data, axis=0)
    return y

# 使用示例
filtered_data = lowpass_filter(raw_data, cutoff=5, fs=50)
我的小技巧:截止频率设多少合适?我一般先看一眼原始数据的频谱。如果走路时主要能量在1-3Hz,那就设5Hz。别设太低,否则连走路信号都被削没了。

3.2 归一化:把数据“拉”到同一个尺度

滤波之后,数据还是“各说各话”。X轴的范围可能是[-2, 2],Y轴是[-1.5, 1.8],Z轴是[-0.5, 3.5]。模型一看,哎?Z轴数值这么大,是不是更重要?其实不是,只是重力分量在Z轴上而已。

归一化就是为了解决这个问题。把所有轴的数据都映射到[0, 1]或[-1, 1]之间,让模型公平对待每个维度。

常用的归一化方法:

  • Min-Max归一化:把数据缩放到[0, 1]。公式是 (x - min) / (max - min)。简单,但容易受异常值影响。
  • Z-score标准化:把数据变成均值为0、标准差为1的分布。公式是 (x - mean) / std。抗异常值能力强一些。

我个人习惯用Z-score。为什么?因为加速度数据偶尔会有毛刺(比如你突然拍了一下传感器),Min-Max会被这个毛刺带偏,而Z-score影响小得多。

def zscore_normalize(data):
    """
    Z-score标准化
    """
    mean = np.mean(data, axis=0)
    std = np.std(data, axis=0)
    # 防止除零
    std[std == 0] = 1e-6
    normalized = (data - mean) / std
    return normalized, mean, std

# 使用示例
normalized_data, mean_val, std_val = zscore_normalize(filtered_data)
注意:归一化的参数(mean, std)一定要用训练集的,然后应用到验证集和测试集上。千万别把全部数据一起归一化再切分,那叫“数据泄露”,模型会作弊的。我曾经吃过这个亏,模型在测试集上准确率99%,一上真机就拉胯。

3.3 滑动窗口切片:把连续数据变成“样本”

滤波和归一化之后,数据是一条长长的连续曲线。但深度学习模型不能吃整条曲线,它需要一个个固定长度的“样本”。

滑动窗口就是干这个的。想象一下,用一个固定大小的“框”在数据上滑动,每次框住一段数据,这就是一个样本。

窗口大小怎么定?这得看你的动作周期。走路一步大概0.5秒,跑步一步大概0.3秒。我一般设2秒的窗口(采样率50Hz,就是100个点),这样能保证每个窗口至少包含一个完整的动作周期。

窗口重叠也很重要。如果不重叠,两个窗口之间会丢失信息。我习惯设50%的重叠,这样数据量翻倍,模型学得更充分。

def sliding_window(data, window_size=100, step_size=50):
    """
    滑动窗口切片
    :param data: 归一化后的数据 (N, 3)
    :param window_size: 窗口大小(点数)
    :param step_size: 步长(点数)
    :return: 切片后的数据 (num_windows, window_size, 3)
    """
    windows = []
    for i in range(0, len(data) - window_size + 1, step_size):
        window = data[i:i+window_size]
        windows.append(window)
    return np.array(windows)

# 使用示例
windows_data = sliding_window(normalized_data, window_size=100, step_size=50)
print(f"切片后得到 {windows_data.shape[0]} 个样本")
关键点:切片后的数据形状是 (样本数, 窗口长度, 特征维度)。对于加速度数据,就是 (N, 100, 3)。这个形状可以直接喂给1D-CNN或LSTM模型。

3.4 数据标注:给每个样本贴上“标签”

好了,数据已经切成一块一块的了。但每一块代表什么动作?没人告诉模型。所以我们需要标注——给每个样本打上标签,比如“静止”、“走路”、“跑步”。

标注有两种方式:

  • 手动标注:用LabelImg之类的工具,在数据曲线上框出动作区间,然后打标签。适合小数据集,但费眼睛。
  • 自动标注:写个脚本,根据规则自动打标签。比如,加速度方差小于某个阈值就标“静止”,大于某个阈值就标“跑步”。

我建议先用自动标注打个底,再手动微调。下面是我常用的自动标注脚本思路:

def auto_label(windows_data, threshold_still=0.1, threshold_run=0.5):
    """
    自动标注:根据加速度方差判断动作
    :param windows_data: 切片后的数据 (N, 100, 3)
    :param threshold_still: 静止阈值(方差小于此值)
    :param threshold_run: 跑步阈值(方差大于此值)
    :return: 标签列表 (0:静止, 1:走路, 2:跑步)
    """
    labels = []
    for window in windows_data:
        variance = np.var(window)  # 计算整个窗口的方差
        if variance < threshold_still:
            labels.append(0)  # 静止
        elif variance > threshold_run:
            labels.append(2)  # 跑步
        else:
            labels.append(1)  # 走路
    return np.array(labels)

# 使用示例
labels = auto_label(windows_data, threshold_still=0.1, threshold_run=0.5)
避坑指南:自动标注的阈值需要根据你的数据调整。我曾经用同一套阈值处理不同人的数据,结果一个人走路被标成了跑步,另一个人跑步被标成了走路。后来我改成先看数据分布,再定阈值,就好多了。

标注完成后,记得把数据和标签保存起来。我一般存成NumPy的.npz格式,方便后续加载:

np.savez('dataset.npz', X=windows_data, y=labels)

3.5 本章小结

这一章我们干了三件事:滤波去噪、归一化统一尺度、滑动窗口切片。最后用自动标注给数据贴上了标签。说白了,就是把原始数据从“毛坯房”变成了“精装房”,模型可以直接拎包入住了。

下一章,我们会用这些处理好的数据,搭建一个1D-CNN模型来识别动作。到时候你会发现,前面预处理做得越扎实,模型训练就越省心。

嗯,今天就到这儿。有问题随时找我。