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)
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)
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]} 个样本")
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模型来识别动作。到时候你会发现,前面预处理做得越扎实,模型训练就越省心。
嗯,今天就到这儿。有问题随时找我。