之前在做行为识别相关的可穿戴数据项目时,反复遇到单模态信号精度不够、模型在个体差异下泛化差、以及标注稀疏导致训练困难等问题。尤其当目标从“运动识别”转向更细粒度的重复性行为检测后,单纯依赖加速度计几乎很难区分“摸头发”和“拔头发”这一类细微动作。本文围绕 Deep Multimodal Wearable Sensor Fusion 这一方向,完整拆解面向 Body-Focused Repetitive Behaviors(BFRBs,身体聚焦重复行为)检测的多模态融合建模流程,包括传感器选型、数据预处理、滑动窗口、融合网络设计、训练评估与工程落地建议。新手可以借此入门可穿戴行为检测,有经验的开发者也能直接复用代码与排查思路。
1. 背景与核心概念
1.1 什么是 BFRBs,为什么需要自动化检测
Body-Focused Repetitive Behaviors(BFRBs)是一类反复出现的、指向自身身体的行为,常见包括拔毛癖(trichotillomania)、咬指甲(nail biting)、抓皮肤(skin picking)、咬嘴唇等。这类行为通常不带有明确的自伤意图,但长期持续会导致皮肤损伤、毛囊破坏、社交困扰,甚至影响正常生活节奏。
在临床和健康管理场景中,BFRBs 的评估通常依赖患者自我报告、访谈量表和临床医生的观察。但这里有一个很现实的问题:很多 BFRBs 发生在无意识状态下,患者可能只有在事后发现“又拔掉了一撮头发”才意识到行为发生了。单纯依靠主观回忆很容易低估行为频率,也难以给出细粒度的发生时间点。
所以,研究者开始尝试用可穿戴传感器去捕捉行为发生时的手部运动、身体接触、肌肉活动甚至生理唤起变化。如果能做到自动、连续、客观地检测,就可以为行为治疗提供数据支持,比如帮助患者觉察行为发生的时间段、频率和触发场景,从而更好地配合认知行为治疗。
1.2 为什么单模态不够,多模态融合解决了什么
先说单模态的问题。加速度计是很多活动识别方案的首选,因为它低功耗、普及度高。但是 BFRBs 的动作幅度通常比较小,比如“手指在头皮上反复抓取”可能只有几十毫秒级别的细微运动,加速度变化在频域上和普通的触摸、抓挠、手势很难分开。
这时候引入更多模态就能提供互补信息:
- 加速度计:提供线性运动的强弱和方向信息,判断手部运动幅度。
- 陀螺仪:提供角速度信息,对手腕旋转、手指抓取等动作更敏感。
- 肌电信号(EMG):反映前臂肌肉的主动收缩,对“执行抓/拔/扯”这类动作有较强的特异性。
- 皮肤电导(EDA)/心率(PPG):可以反映行为前后的紧张、焦虑等生理唤起变化,辅助判断行为诱因。
所谓 Deep Multimodal Wearable Sensor Fusion,就是利用深度学习模型,把多个传感器的原始信号融合起来,让模型自动学习不同模态之间的时间依赖和互补关系,而不是人为手动拼接特征后交给传统分类器。
1.3 核心术语澄清
几个容易混淆的概念需要先说明:
- 数据级融合:把多个传感器的原始采样值在时间轴上拼接,形成多通道输入,直接送进模型。实现简单,但要求各传感器采样率一致,且对噪声敏感。
- 特征级融合:先从每个模态分别提取特征,再把特征拼接或加权组合。灵活性强,可以结合手工特征与深度特征。
- 决策级融合:每个模态先单独训练一个分类器,最后对分类结果投票或加权平均。容错性好,但模型复杂,训练周期长。
- 端到端融合:以深度学习网络为骨架,多个模态分别经过独立的特征提取分支,再在某一层进行融合。这也是当前“Deep Multimodal”背景下最常用的实现方式。
本文后面采用的实践方案,本质上是一个“多分支 CNN 特征级融合”的端到端设计,兼顾了可解释性和工程可落地性。
2. 技术架构与总体流程
2.1 总体检测流程
一个面向 BFRBs 的可穿戴检测方案,大致可以分为五个阶段:
数据采集 → 数据预处理 → 滑动窗口切分 → 多模态融合模型 → 后处理与事件判定数据采集阶段负责从可穿戴设备上获得加速度、角速度、肌电等原始信号;预处理阶段包括降噪、去基线、缺失值处理、归一化;滑动窗口切分把连续时间序列切成短片段,让模型可以逐段判断“该片段内是否发生了目标行为”;融合模型输出每个窗口属于某类行为的概率;后处理阶段通过阈值和事件合并规则,把连续的概率输出转换成“某时间段内发生了一次行为事件”。
2.2 传感器选型与佩戴位置
不是所有传感器都适合检测所有 BFRBs,先要明确自己的检测子类。以下是一个常见匹配建议:
| 目标行为 | 推荐传感器 | 佩戴位置 | 关键信号 |
|---|---|---|---|
| 拔头发 | 加速度计 + 陀螺仪 + EMG | 惯用手手腕 | 手部到头皮附近的快速位移、肌肉收缩 |
| 咬指甲 | 加速度计 + EMG | 惯用手手腕或颈部 | 手-口部的接近运动、前臂肌肉活动 |
| 抓皮肤 | 加速度计 + 压力/触觉传感器 | 惯用手手指或腕带内侧 | 指尖反复按压、抓取动作 |
| 伴随焦虑发作 | PPG + EDA | 手腕 | 心率和皮肤电导变化 |
实际项目中,很多消费级手环只有加速度计和 PPG,开发时可以先以“加速度计 + 陀螺仪”作为最小组合。如果你有研究级设备或自研硬件,再叠加 EMG 和 EDA。
2.3 为什么用深度学习而不是传统机器学习
在早期研究中,常见的做法是手动提取时域、频域、统计特征,然后交给 SVM、随机森林或梯度提升树。这种方案在特征设计得当的情况下也能达到不错的精度,但有两个痛点:第一,BFRBs 的动作多样,极难用手工特征覆盖全部模式;第二,不同个体动作差异大,特征迁移性差。
深度学习可以通过多个卷积层自动学习局部时间模式,再用循环网络或 Transformer 建模长期依赖。特别是多模态场景下,每个模态有独立的时间动态,用“多分支结构 + 融合层”可以让模型自己决定哪些特征有用、哪些特征需要抑制,减少人工调参负担。
3. 环境准备与版本说明
3.1 推荐运行环境
本文代码以 Python 为基础,建议使用 Python 3.8 及以上版本。深度学习框架以 PyTorch 为例,是因为它可以灵活搭建多分支网络,动态图特性也更方便调试。实际开发时,TensorFlow 或 Keras 也能实现同样的结构,只是语法不同。
依赖清单如下:
numpy>=1.21 pandas>=1.3 scikit-learn>=1.0 imbalanced-learn>=0.10 torch>=1.10 matplotlib>=3.5版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。如果你使用的是 PyTorch 2.x,代码基本兼容;如果使用 CPU 训练,也可以跑通,只是训练速度会慢一些。
3.2 数据格式约定
为方便演示,本文约定传感器数据保存在 CSV 文件中,每一行是一个采样点,字段如下:
timestamp, acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z, emg, label其中:
- timestamp:时间戳(秒)。
- acc_x / acc_y / acc_z:三轴加速度计读数,单位 g。
- gyro_x / gyro_y / gyro_z:三轴陀螺仪读数,单位 dps。
- emg:肌电信号幅度,单位 mV。
- label:标签,0 表示该采样点无目标行为,1 表示发生了目标行为。
这里为了演示代码,我们假设数据已经完成时间对齐,且所有传感器采样率一致。如果采样率不一致,需要先做重采样(比如把 EMG 降低到与加速度计一致),这一点在第 4 节会展开说明。
4. 数据预处理与时间序列切分
4.1 读取数据与缺失值处理
先读取原始数据,并做基础清洗。可穿戴设备在佩戴松动或信号传输异常时,常常会出现短时缺失值。
import pandas as pd import numpy as np def load_sensor_data(csv_path): df = pd.read_csv(csv_path) # 按时间戳排序,避免采集回传顺序错乱 df = df.sort_values("timestamp").reset_index(drop=True) return df def fill_missing(df, method="linear"): sensor_cols = ["acc_x", "acc_y", "acc_z", "gyro_x", "gyro_y", "gyro_z", "emg"] df[sensor_cols] = df[sensor_cols].interpolate(method=method, limit_direction="both") return df这里有几个细节值得说明:
interpolate默认是线性插值,适用于短时间缺失;如果缺失超过几百毫秒,建议直接丢弃对应片段,而不是插值填充,因为长时间插值会制造虚假信号。- 排序很重要。可穿戴设备通过蓝牙回传数据时,有时会出现延迟或乱序,直接按行序处理会导致时间轴错乱。
- 标签列一般是离散的 0/1,不需要插值,如果标签缺失,只能将该段标记为噪声或丢弃。
4.2 滤波与去基线
加速度计和陀螺仪信号在佩戴状态下通常包含大量低频漂移。常见做法是用高通滤波器去除重力分量和基线漂移,保留与动作相关的中高频成分。
这里用一个简单的一阶高通滤波器:
def highpass_filter(data, alpha=0.95): filtered = np.zeros_like(data, dtype=float) prev = data[0] for i in range(1, len(data)): prev = alpha * prev + alpha * (data[i] - data[i-1]) filtered[i] = data[i] - prev return filtered实际项目中更推荐 SciPy 的butter滤波器,这里用简易版本是为了展示原理。滤波的截止频率需要根据动作频段和经验调整:BFRBs 的手部动作通常是 0.5~8 Hz 的量级,过低会把呼吸、走路等大运动带进来,过高则可能把有效动作信息也滤掉。
4.3 滑动窗口切分
模型输入是固定长度的时间片段,不能直接把整段长序列扔进网络。我们需要用滑动窗口把连续数据切成样本。
下面是窗口切分函数,这一步是行为检测中最基础也最容易出错的环节:
def sliding_window(df, window_size=128, step_size=32): """ 将长序列切成窗口 window_size: 每个窗口内的采样点数 step_size: 窗口滑动步长 """ sensor_cols = ["acc_x", "acc_y", "acc_z", "gyro_x", "gyro_y", "gyro_z", "emg"] x_data = df[sensor_cols].values y_data = df["label"].values windows = [] labels = [] start = 0 total = len(df) while start + window_size <= total: x_window = x_data[start:start + window_size] y_window = y_data[start:start + window_size] windows.append(x_window) # 窗口标签策略:只要窗口内目标行为占比超过阈值,就标记为正样本 pos_ratio = np.mean(y_window) label = 1 if pos_ratio >= 0.3 else 0 labels.append(label) start += step_size return np.array(windows), np.array(labels)窗口长度和步长需要结合采样率来定。假设采样率是 50 Hz,128 个采样点对应约 2.56 秒,足以覆盖一次完整的拔发或抓取动作。步长选 32(即 0.64 秒),会让相邻窗口有重叠,提高模型对动作起始位置的鲁棒性,但也增加了计算量。
关于窗口标签,有三种常见策略:
- 只要窗口内存在行为,就标为正样本。
- 只有行为覆盖比例超过某个阈值(如 30%),才标为正样本。
- 把窗口标签细化到每个采样点,使用逐点预测,但训练难度更高。
本文采用第二种,因为 BFRBs 动作的时间边界本身很模糊,强行要求窗口内 100% 都是行为反而会让模型难以学习。
4.4 归一化与数据集划分
不同传感器量纲差异很大,加速度计通常以 g 为单位,陀螺仪以 dps 为单位,EMG 则可能是 mV。如果不做归一化,大数值模态会主导损失,模型很难学到各模态的独立模式。
处理方式:先按训练集计算均值和标准差,再用同一参数对验证集和测试集做标准化。注意不要用全量数据计算统计量,否则会造成数据泄漏。
from sklearn.preprocessing import StandardScaler def normalize_windows(X_train, X_val, X_test): """ X shape: (样本数, 窗口长度, 通道数) 按通道维度标准化 """ n_train = X_train.shape[0] n_val = X_val.shape[0] n_test = X_test.shape[0] n_channels = X_train.shape[2] X_all = np.concatenate([X_train, X_val, X_test], axis=0) # 只使用训练集的均值和标准差 scaler = StandardScaler() # 把 (N, T, C) 转为 (N*T, C) 计算每个通道的统计量 X_all = X_all.reshape(-1, n_channels) X_scaled = scaler.fit_transform(X_all) X_scaled = X_scaled.reshape(n_train + n_val + n_test, -1, n_channels) X_train = X_scaled[:n_train] X_val = X_scaled[n_train:n_train + n_val] X_test = X_scaled[n_train + n_val:] return X_train, X_val, X_test标准化之后,各通道都近似服从均值为 0、方差为 1 的分布,模型训练也会更稳定。
5. 多模态融合模型设计
5.1 多分支 CNN 结构
设计思路:不同传感器信号在时间维度上的特征不同,比如加速度计和陀螺仪都属于运动学信号,可以共享一个分支;EMG 是肌肉电信号,采样特性与运动信号差异大,应该单独走一个分支。每个分支先提取低层局部模式,再在某个高度拼接。
这里给出一个简洁但有效的多分支 CNN 模型:
import torch import torch.nn as nn class MultiModalCNN(nn.Module): def __init__(self, n_channels_imu=6, n_channels_emg=1, hidden_dim=64, n_classes=2): super().__init__() # IMU 分支(加速度计 + 陀螺仪共 6 个通道) self.imu_branch = nn.Sequential( nn.Conv1d(n_channels_imu, 32, kernel_size=7, padding=3), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=5, padding=2), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) # EMG 分支(单通道) self.emg_branch = nn.Sequential( nn.Conv1d(n_channels_emg, 16, kernel_size=5, padding=2), nn.BatchNorm1d(16), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(16, 32, kernel_size=3, padding=1), nn.BatchNorm1d(32), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) # 融合分类头 self.classifier = nn.Sequential( nn.Linear(64 + 32, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, n_classes) ) def forward(self, imu, emg): imu_feat = self.imu_branch(imu) # (B, 64, 1) emg_feat = self.emg_branch(emg) # (B, 32, 1) imu_feat = imu_feat.flatten(1) emg_feat = emg_feat.flatten(1) fused = torch.cat([imu_feat, emg_feat], dim=1) logits = self.classifier(fused) return logits需要说明的是,n_channels_imu=6表示三轴加速度和三轴陀螺仪,如果你还有更多运动传感器,可以把这个数字扩展。AdaptiveAvgPool1d(1)的作用是不管输入窗口长度是多少,都压缩成一个固定长度的特征向量,方便后续拼接。
5.2 为什么要用多个分支而不是直接拼接
直接把所有通道拼成一个多通道输入,然后送进单一 CNN,也可以工作,但研究表明,多分支结构有更好的可解释性和训练稳定性。原因也很直观:
- 每个分支可以先学习自己模态内的局部模式,再在高层做融合,模型不需要在一开始就强行对齐不同传感器的时间特征。
- IMU 和 EMG 的采样特性、噪声水平不一样。单独分支相当于给每个模态独立的特征提取阶段,抑制了强模态对弱模态的干扰。
- 在调试时,你可以单独查看某个分支的输出,定位是哪个传感器出了问题。
5.3 融合层级:从特征级到决策级
上面的代码属于特征级融合。如果你希望做决策级融合,改动也很简单:每个分支各自接一个分类头,输出各自的概率,最后用平均或加权投票得到最终结果。
class MultiModalDecisionFusion(nn.Module): def __init__(self, base_model): super().__init__() self.base_model = base_model self.imu_head = nn.Linear(64, 2) self.emg_head = nn.Linear(32, 2) def forward(self, imu, emg, alpha=0.6): with torch.no_grad(): imu_feat = self.base_model.imu_branch(imu).flatten(1) emg_feat = self.base_model.emg_branch(emg).flatten(1) logits_imu = self.imu_head(imu_feat) logits_emg = self.emg_head(emg_feat) prob_imu = torch.softmax(logits_imu, dim=1) prob_emg = torch.softmax(logits_emg, dim=1) fused_prob = alpha * prob_imu + (1 - alpha) * prob_emg return torch.log(fused_prob + 1e-8)决策级融合的好处是即使某个传感器信号被干扰,另一个分支仍然可以独立完成判断。缺点是整体模型更大,且没有充分挖掘模态之间的交互信息。实际项目中,可以先做特征级融合,再根据业务需求决定是否加决策级融合。
6. 训练、评估与类别不平衡处理
6.1 定义 Dataset 与 DataLoader
先把切分好的窗口包装成 PyTorch 的 Dataset。注意这里需要把 IMU 和 EMG 分开成两个输入。
from torch.utils.data import Dataset, DataLoader class SensorWindowDataset(Dataset): def __init__(self, X_imu, X_emg, y): self.X_imu = torch.FloatTensor(X_imu) # (N, T, 6) self.X_emg = torch.FloatTensor(X_emg) # (N, T, 1) self.y = torch.LongTensor(y) def __len__(self): return len(self.y) def __getitem__(self, idx): imu = self.X_imu[idx].permute(1, 0) # (C, T) emg = self.X_emg[idx].permute(1, 0) # (C, T) return imu, emg, self.y[idx]上面的permute很关键。因为 PyTorch 卷积层期望的输入形状是(B, C, T),而 CSV 切出来的窗口形状是(T, C),所以要先转置。
6.2 处理类别不平衡
BFRBs 的行为时间占一天的比例通常很低,正负样本比例可能达到 1:50 甚至更低。如果直接训练,模型会一直预测“无行为”也能得到很高的准确率,但这没有任何意义。
推荐两种处理手段:
第一,使用加权损失函数。对少数类样本给予更高的损失权重:
def compute_pos_weight(y_train): neg_count = (y_train == 0).sum() pos_count = (y_train == 1).sum() pos_weight = torch.tensor([neg_count / max(pos_count, 1)], dtype=torch.float32) return pos_weight在训练时,把pos_weight传给BCEWithLogitsLoss:
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)第二,使用重采样。用imbalanced-learn的RandomUnderSampler或SMOTE平衡训练集。但要注意,时间序列数据不能直接做随机过采样,因为相邻窗口之间有重叠,随机过采样可能造成训练集和验证集之间的重叠泄漏。
比较稳妥的做法是:只对训练集做少数类窗口的复制或简单数据增强,验证集和测试集保持原始分布。
6.3 训练循环
下面给出一个完整的训练循环示例,包含验证集评估与模型保存:
import torch.optim as optim def train_model(model, train_loader, val_loader, pos_weight, epochs=20, lr=1e-3): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight.to(device)) optimizer = optim.Adam(model.parameters(), lr=lr) # 处理二分类,输出单元数为 1 for epoch in range(epochs): model.train() total_loss = 0 for imu, emg, label in train_loader: imu, emg, label = imu.to(device), emg.to(device), label.to(device) optimizer.zero_grad() logits = model(imu, emg).squeeze(1) # (B,) loss = criterion(logits, label.float()) loss.backward() optimizer.step() total_loss += loss.item() # 验证 model.eval() correct = 0 total = 0 val_loss = 0 with torch.no_grad(): for imu, emg, label in val_loader: imu, emg, label = imu.to(device), emg.to(device), label.to(device) logits = model(imu, emg).squeeze(1) probs = torch.sigmoid(logits) preds = (probs >= 0.5).long() correct += (preds == label).sum().item() total += label.size(0) val_loss += criterion(logits, label.float()).item() print(f"Epoch {epoch+1}/{epochs}, Train Loss: {total_loss/len(train_loader):.4f}, " f"Val Loss: {val_loss/len(val_loader):.4f}, Val Acc: {correct/total:.4f}") return model需要注意的是,这里为了匹配BCEWithLogitsLoss,模型输出层只需要 1 个神经元,而不是n_classes=2。如果你习惯用CrossEntropyLoss,则输出层保持 2 个神经元,并把标签也换成对应类别索引。两种方式效果接近,关键是前后要保持一致。
6.4 评估指标选择:不要只看 Accuracy
正如前面所说,BFRBs 数据极不平衡,Accuracy 会严重失真。更推荐以下指标:
- Precision:预测为正样本的窗口中有多少是真的行为。
- Recall:真实行为窗口中有多少被正确检出。
- F1-score:Precision 和 Recall 的调和平均。
- AUC-PR:精确率-召回率曲线下的面积,适合不平衡场景。
- 误报率:在连续监测场景中,误报会导致佩戴者频繁收到无意义提醒,因此要单独统计。
示例评估代码:
from sklearn.metrics import classification_report, precision_recall_curve, auc def evaluate_model(model, test_loader, device="cpu"): model.eval() all_probs = [] all_labels = [] with torch.no_grad(): for imu, emg, label in test_loader: imu, emg = imu.to(device), emg.to(device) logits = model(imu, emg).squeeze(1) probs = torch.sigmoid(logits).cpu().numpy() all_probs.extend(probs) all_labels.extend(label.numpy()) all_probs = np.array(all_probs) all_labels = np.array(all_labels) preds = (all_probs >= 0.5).astype(int) print(classification_report(all_labels, preds, target_names=["non-BFRB", "BFRB"])) precision, recall, _ = precision_recall_curve(all_labels, all_probs) print(f"AUC-PR: {auc(recall, precision):.4f}")实际部署时,阈值 0.5 不一定是最优的。你可以根据业务对误报和漏报的容忍度,在验证集上搜索合适的阈值。比如希望尽可能少打扰患者,可以把阈值调高到 0.7;希望不漏检,则把阈值调低到 0.3。
7. 常见问题与排查思路
7.1 数据与预处理阶段
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练时 loss 不下降 | 输入数据未归一化,量纲差异大 | 按通道做标准化,并检查是否存在 NaN |
| 模型永远输出“无行为” | 正负样本极度不平衡 | 使用pos_weight或对训练集做重采样 |
| 验证集指标远高于测试集 | 窗口重叠导致数据泄漏 | 确保验证/测试集与训练集按时间切分,不要随机洗牌混入 |
| 预测结果频繁跳变 | 窗口步长太小,相邻窗口预测不稳定 | 后处理中做滑动平均或事件合并 |
| 某个通道全为 0 | 传感器未对齐或硬件故障 | 检查原始 CSV,确认无设备断连 |
7.2 模型训练阶段
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练集指标好但测试集很差 | 过拟合,或个体差异大 | 增大样本量,加入 Dropout,按个体划分数据集 |
| 每个 epoch 训练时间过长 | 窗口数量太多,数据冗余 | 增大步长,减少重叠;或先用 1D CNN 压缩输入 |
permute后维度不对 | 输入形状理解错误 | 打印imu.shape,确认是(B, C, T)而非(B, T, C) |
| 训练不稳定,loss 波动大 | 学习率过高 | 降低 lr 到 1e-4 或使用学习率调度器 |
| GPU 显存不够 | 窗口长度大、batch 太大 | 降低 batch size,或使用更小的卷积核 |
7.3 后处理与事件判定
模型输出的概率序列还不足以直接用于业务。一条原始概率曲线可能在一次拔发动作中连续出现多个峰值,如果每个峰值都触发一次提醒,用户会非常烦躁。
推荐用事件级后处理:
- 对概率序列做长度为 5~10 的滑动平均。
- 设置一个高于阈值的“触发阈值”,当概率超过触发阈值时认为事件开始。
- 设置一个低于阈值的“释放阈值”,当概率低于释放阈值持续一段时间后认为事件结束。
- 对持续时间过短的事件进行剔除,比如少于 1 秒的事件视为噪声。
这种“双阈值迟滞”机制非常实用,可以避免输出在边界处反复切换,类似于信号处理里的 detection switch 思路。
8. 工程化落地与伦理边界
8.1 从模型到可穿戴设备的部署思路
模型训练完成后,面对的第一个问题是:怎么部署到真实设备上?
- 如果设备算力足够(如手机、边缘网关),可以直接用 PyTorch 的 TorchScript 或 ONNX Runtime 导出模型。
- 如果是低功耗 MCU 设备(如 Cortex-M4 级别的腕带),需要考虑模型压缩。常见的做法是把 1D CNN 结构简化、量化到 int8,或者把模型蒸馏成一个轻量网络。
- 传感器原始数据回传云端再做检测,也是一种方案,但要考虑网络延迟、数据隐私和功耗成本。BFRBs 检测对实时性要求并不极端,允许几百毫秒延迟,所以边缘+云端的混合架构更常见。
8.2 隐私与数据安全
可穿戴设备采集的是连续的人体生理和行为数据,属于高度敏感的个人信息。做项目时需要注意:
- 数据采集前必须获得参与者知情同意,说明采集范围、用途和存储方式。
- 原始信号尽量在设备端完成脱敏处理,不上传原始 EMG、EDA 等敏感信息。
- 数据存储和传输要加密,训练数据不要随意对外共享。
- 涉及真实患者数据时,要遵守相关法律法规和伦理审查要求。
8.3 医学与健康边界
BFRBs 的检测系统本质上是一个辅助工具,不是临床诊断设备。即使检测准确率很高,也不能直接用于确诊疾病或替代医生评估。正确的定位是:
- 帮助患者提高行为觉察,记录行为发生频率与时间段。
- 为认知行为治疗提供客观数据支持。
- 提示用户“可能发生了重复行为”,而不是“你有病”。
在写论文或产品方案时,要把这一点表述清楚,避免夸大技术能力,也不要对治疗效果做任何绝对承诺。
8.4 个体差异与长期泛化
行为识别模型最大的敌人是个体差异。不同用户的动作幅度、频率、佩戴习惯都不一样,同一个模型在不同用户身上表现可能差异巨大。
解决思路包括:
- 数据采集时尽量覆盖更多年龄、性别、手型的人群。
- 使用“按用户分组”的交叉验证,而不是随机划分窗口。否则同一个人相邻窗口会同时出现在训练集和验证集中,指标虚高。
- 上线后收集少量用户数据做个性化微调,比如在云端用用户自己的无标签或弱标签数据做增量训练。
- 每隔一段时间重新评估模型,因为用户的动作习惯可能随着治疗进程发生变化。
9. 总结与下一步学习路线
从本文的完整流程可以看到,面向 BFRBs 的检测任务并不是简单套一个分类模型就能解决。它涉及传感器选型、时间序列预处理、多模态融合网络设计、类别不平衡处理、后处理事件判定,以及工程化落地中的隐私和伦理问题。掌握这些环节,你就具备了一个相对完整的行为识别系统设计能力。
下一步建议从以下几个方向继续深入:
- 学习更先进的时序模型:把 1D CNN 替换为 LSTM、GRU 或 Time-Series Transformer,处理更长距离的时间依赖。
- 研究跨模态注意力机制:使用 Cross-Attention 让模型动态关注当前最有判别力的模态,而不是固定权重融合。
- 尝试自监督预训练:用大量无标注的可穿戴数据先做自监督预训练,再用少量标注数据微调,缓解 BFRBs 标注成本高的问题。
- 做真实场景验证:把模型部署到开发板上,连续记录一周数据,统计误报次数、漏报次数和用户反馈,再回到数据侧优化。
如果你正准备开始自己的可穿戴行为检测项目,建议先跑通本文提供的最小闭环,把原始数据、窗口切分、模型训练、评估指标这条链路走通,再逐步替换更复杂的网络结构和部署方案。动手采集一份真实数据,往往比读十篇论文更有收获。