3. 可穿戴设备睡眠阶段自动识别

可穿戴设备睡眠阶段自动识别 学习卡片

睡眠分期的可穿戴信号基础

睡眠阶段自动识别不能只靠“睡了多久”,而要把两类信号放在同一时间轴上联合判断:

  • 心率变异性(HRV):由 PPG 逐拍间期(IBI)序列计算。深睡时副交感占优,RMSSD 升高、心率下降;REM 时交感活动增强,心率与 HRV 波动加剧。
  • 体动(Actigraphy):三轴加速度的合矢量变化。清醒时体动频繁,深睡时体动极少,REM 时出现零星小体动。

本地识别模型的目标是:以 30 秒为一个 epoch,输出 {清醒, 浅睡, 深睡, REM} 四类标签。下面这张图给出从原始信号到分期输出的完整信号链与特征映射关系。

PPG 原始波形 采样 25 Hz · 绿光 三轴加速度 采样 50 Hz · ±4 g 带通 + 峰值检测 输出 IBI 逐拍间期 合矢量 + 带通 0.25–3 Hz 体动强度 HRV 特征(30 s epoch) RMSSD · SDNN · 平均心率 LF/HF 比值 体动特征(30 s epoch) 活动计数 · 零交叉率 体动占比 特征向量 7 维 · 归一化 滑动窗口平滑 轻量级分类器(决策树 / 随机森林) 模型体积 < 50 KB · 单次推理 < 5 ms 清醒 体动高 · HRV 低 浅睡 体动低 · HRV 中 深睡 体动极低 · HRV 高 REM 零星体动 · HRV 波动

特征工程与模型选择

每个 30 秒 epoch 提取 7 维特征,送入轻量级分类器。特征定义如下:

特征来源计算方式典型区分作用
RMSSDIBI 序列相邻 IBI 差值的均方根深睡高、清醒低
SDNNIBI 序列IBI 标准差REM 波动大
meanHRIBI 序列60 / 平均 IBI深睡最低
LF/HFIBI 频域0.04–0.15 Hz 与 0.15–0.4 Hz 功率比REM 升高
activityCount加速度合矢量带通后绝对值积分清醒最高
zeroCrossRate加速度合矢量过零次数 / 窗口长度区分体动类型
motionRatio加速度超过阈值的采样点占比深睡接近 0

模型选用随机森林(n_estimators=30, max_depth=6),在 MCU 上以 C 数组形式固化。训练在 PC 端完成,部署时只保留推理逻辑。

本地推理实现

以下代码展示从特征向量到四类标签的完整推理路径,可直接嵌入可穿戴固件:

#include <stdint.h>
#include <math.h>

#define N_FEATURES 7
#define N_TREES    30
#define N_CLASSES  4

/* 类别索引:0=清醒 1=浅睡 2=深睡 3=REM */
static const char* LABEL_NAMES[N_CLASSES] = {"Wake", "Light", "Deep", "REM"};

/* 单棵决策树节点:feature=-1 表示叶节点 */
typedef struct {
    int8_t  feature;   /* 特征索引,-1 为叶 */
    float   threshold; /* 分裂阈值 */
    int16_t left;      /* 左子节点索引 */
    int16_t right;     /* 右子节点索引 */
    uint8_t votes[N_CLASSES]; /* 叶节点类别投票 */
} TreeNode;

/* 示例:一棵深度 3 的树(实际部署时由训练脚本导出全部 30 棵) */
static const TreeNode tree0[] = {
    /* 0 */ {4, 0.35f, 1, 2, {0,0,0,0}},   /* activityCount < 0.35 ? */
    /* 1 */ {0, 0.55f, 3, 4, {0,0,0,0}},   /* RMSSD < 0.55 ? */
    /* 2 */ {0, 0.0f, -1, -1, {1,0,0,0}},  /* 叶:清醒 */
    /* 3 */ {2, 0.62f, -1, -1, {0,1,0,0}}, /* 叶:浅睡 */
    /* 4 */ {3, 0.48f, -1, -1, {0,0,1,0}}, /* 叶:深睡 */
};

/* 特征归一化到 [0,1] */
static void normalize_features(const float raw[N_FEATURES], float out[N_FEATURES]) {
    /* 各特征 min/max 由训练集统计得到 */
    static const float fmin[N_FEATURES] = {0.0f, 0.0f, 45.0f, 0.2f, 0.0f, 0.0f, 0.0f};
    static const float fmax[N_FEATURES] = {120.0f, 150.0f, 110.0f, 8.0f, 1.0f, 1.0f, 1.0f};
    for (int i = 0; i < N_FEATURES; i++) {
        float v = (raw[i] - fmin[i]) / (fmax[i] - fmin[i]);
        out[i] = v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v);
    }
}

/* 单棵树推理,返回投票类别 */
static int tree_predict(const TreeNode* tree, const float feat[N_FEATURES]) {
    int idx = 0;
    while (tree[idx].feature >= 0) {
        idx = (feat[tree[idx].feature] < tree[idx].threshold)
              ? tree[idx].left : tree[idx].right;
    }
    /* 取票数最高的类别 */
    int best = 0;
    for (int c = 1; c < N_CLASSES; c++)
        if (tree[idx].votes[c] > tree[idx].votes[best]) best = c;
    return best;
}

/* 随机森林投票 */
int sleep_stage_predict(const float raw_feat[N_FEATURES]) {
    float feat[N_FEATURES];
    normalize_features(raw_feat, feat);

    int votes[N_CLASSES] = {0};
    /* 实际部署时遍历 30 棵树,此处以 tree0 示意 */
    votes[tree_predict(tree0, feat)]++;

    int best = 0;
    for (int c = 1; c < N_CLASSES; c++)
        if (votes[c] > votes[best]) best = c;
    return best;
}
部署要点:训练脚本导出每棵树的节点数组为 C 头文件,推理时只做浮点比较与计数,无需动态内存分配。30 棵树、深度 6 的模型在 Cortex-M4F 上单次推理约 2–4 ms,满足 30 秒 epoch 的实时性要求。

验证与判读

用一段已知参考标签的睡眠记录做离线验证,计算混淆矩阵与 Cohen's Kappa:

真实 \ 预测清醒浅睡深睡REM
清醒92%6%0%2%
浅睡5%78%12%5%
深睡0%14%83%3%
REM3%18%4%75%

判读标准:Kappa > 0.6 视为可用;浅睡与 REM 的混淆是主要误差来源,可通过增加 LF/HF 特征权重或引入心率波动熵来改善。若深睡召回率低于 70%,优先检查体动阈值是否过松导致深睡被误判为浅睡。

常见问题:PPG 信号在体动时易丢失,导致 IBI 序列出现缺口。应在特征提取前对 IBI 做异常值剔除(相邻 IBI 变化超过 30% 视为伪迹),否则 RMSSD 会被显著高估,深睡误判率上升。