RuView ADR-076 实战:用 CNN 频谱图嵌入与图 Transformer 把 CSI 子载波-时间矩阵变成 128 维空间指纹
本篇基于 RuView 仓库中的 ADR-076(CSI Spectrogram Embeddings via CNN + Graph Transformer),讲解如何把 ESP32 采集的 64 子载波 CSI 原始数据组织成 64×20 的灰度“频谱图”图像,经 WASM CNN 提取 128 维嵌入,再用图注意力融合多节点观测,最终入库做环境指纹与 kNN 检索。读完后你将掌握从 iq_hex 解析、归一化、最近邻上采样到图融合、Cognitum Seed 入库的完整调用链,并能在 scripts/csi-spectrogram.js 与 scripts/mesh-graph-transformer.js 上直接复现实操。
一、问题背景:8 维手工特征丢掉了什么
RuView 现有的 CSI 处理链路对每帧提取一个 8 维手工特征向量:平均幅度、幅度方差、最大幅度、平均相位、相位方差、带宽、频谱质心与 RSSI。这套特征对基础存在检测和房间指纹足够用,但它把所有子载波信息坍缩成标量统计量,丢弃了原始数据中的二维空间-频率结构。
单个 ESP32-S3 的 CSI 帧包含 64 个子载波(HT40 模式下为 128 个),每个子载波带 I/Q 分量。把 20 个连续帧堆叠起来,就形成一个 64×20 的子载波-时间矩阵——本质上就是一张灰度频谱图。这张矩阵编码了四类 8 维特征“看不见”的信息:
- 频率选择性衰落——金属物体在特定子载波索引处形成持久空区(表现为竖直暗条纹);
- 多普勒特征——人体运动在子载波上产生时变幅度模式(表现为水平波纹);
- 多径结构——房间几何形状产生每个环境独有的干涉图案;
- 活动指纹——行走、坐姿、呼吸、跌倒在子载波-时间矩阵上呈现截然不同的二维纹理。
ADR 中给出的局限性对照表可以完整概括为什么需要换表征:
| 局限 | 影响 |
|---|---|
| 无子载波级信息 | 无法区分频率选择性衰落与宽带衰落 |
| 无时序模式编码 | 步态(周期性)与随机运动(非周期)看起来一样 |
| 无二维结构 | 房间指纹被压成 8 个数;统计量相近的两个房间无法区分 |
| 无跨子载波相关性 | 无法检测驻波、节点图案或多径簇 |
| kNN 区分度差 | 8 维超球面面积有限,难以分离不同环境 |
仓库中这套 8 维特征的实际来源可以在两处印证:固件侧 edge_processing.h 定义了 float features[8](8 维归一化特征向量),而 seed_csi_bridge.py 负责把这 8 维向量送入 Cognitum Seed(对应 ADR-069),--file 模式下 parse_raw_csi_packet 甚至给出了一组 8 元默认值。ADR-076 的动机正是:在这套快速特征之外,补一条能保留二维结构的 128 维表征通道。
二、总体架构与依赖
ADR-076 的决策一句话概括:把 CSI 子载波-时间矩阵当作灰度频谱图,用 CNN 生成保留二维空频结构的 128 维表示,并用图 Transformer 融合多个 ESP32 节点的嵌入。ADR 元信息为:状态 Proposed,日期 2026-04-02,依赖 ADR-018(二进制帧格式)、ADR-024(AETHER 对比嵌入)、ADR-029(RuvSense 多静态感知)、ADR-069(Cognitum Seed 桥)、ADR-073(多频网格扫描)。
ADR 原文给出的架构图如下(每个 ESP32 节点一路,两路汇合进图注意力):
ESP32 Node 1 ESP32 Node 2
| |
v v
UDP 5006 UDP 5006
| |
v v
[64 subcarriers] [64 subcarriers]
[20-frame window] [20-frame window]
| |
v v
64x20 amplitude 64x20 amplitude
matrix (grayscale) matrix (grayscale)
| |
v v
@ruvector/cnn @ruvector/cnn
CnnEmbedder CnnEmbedder
| |
v v
128-dim vector 128-dim vector
| |
+-------+ +----------+
| |
v v
Graph Transformer (2-node graph)
Edge weight = cross-node correlation
|
v
Fused 128-dim vector
|
+-------+-------+
| |
v v
Cognitum Seed kNN Search
(128-dim store) (similar rooms)
实现依赖三个 ruvector 侧的库(ADR 中声明的版本):
- @ruvector/cnn(v0.1.0):WASM CNN 特征提取(约 5ms/224×224 图像,约 900KB 模型),可配置嵌入维度(默认 512,本方案用 128 做紧凑存储)、L2 归一化嵌入与余弦检索、InfoNCE/三元组对比训练、SIMD 优化的层运算,Node.js 与浏览器双环境可用;
- ruvector-graph-transformer:基于 LSH 分桶与 PPR 采样的 O(n log n) 子线性图注意力、证据门控的变异底座、带 Granger 因果的时序因果注意力(与 CSI 时间序列直接相关)、Sⁿ×Hᵐ×ℝᵏ 乘积空间上的流形注意力;
- @ruvector/graph-wasm(v2.0.2):WASM 内的 Neo4j 兼容属性图数据库,支持任意属性与嵌入的节点/边、超边以及 Cypher 查询。
ADR 声明的依赖安装路径为 vendor/ruvector/npm/packages/ruvector-cnn/ 与 vendor/ruvector/npm/packages/graph-wasm/。需要注意:当前仓库快照中 vendor/ruvector 目录存在,但 npm 包子模块未检出(目录为空),本地运行前先完成对应子模块检出,否则 csi-spectrogram.js 的 initCnn() 会因读不到 WASM 文件而失败。
三、Step 1:CSI 帧到灰度频谱图
数据格式
CSI 帧以 ADR-018 二进制格式经 UDP 传输(JSONL 录制的 JSON 表示同样可用)。一帧的 JSON 示例来自 ADR:
{
"timestamp": 1775182186.123,
"node_id": 1,
"magic": 3289481217,
"size": 148,
"rssi": -45,
"type": "CSI",
"iq_hex": "00000f030d030e040d030d030d030c020d020d01...",
"subcarriers": 64
}
iq_hex 的编码规则:每字节 2 个十六进制字符,每个子载波 4 个字符(I 字节 + Q 字节),总长度 = subcarriers * 4。二进制 UDP 帧的 magic 为 0xC5110001,这在 ADR-018 与固件侧 csi_collector.h(#define CSI_MAGIC 0xC5110001)中交叉印证,csi_collector.c 注释也标明了 [0..3] Magic: 0xC5110001 (LE)。
幅度计算与归一化
每个子载波的幅度为:
Amplitude[sc] = sqrt(I[sc]^2 + Q[sc]^2)
取 20 帧滑动窗口得到 64×20 矩阵,再做 0–255 灰度归一化:
pixel[sc][t] = clamp(255 * (amplitude[sc][t] - min) / (max - min), 0, 255)
min/max 在整个 64×20 窗口上计算,即每窗口对比度归一化——这保证 CNN 看到的是相对结构,而与绝对信号强度(随距离、发射功率、环境吸收变化)无关。
源码实现位于 csi-spectrogram.js:parseIqHex() 按 4 字符步长逐子载波解析 I/Q 并开方;parseBinaryFrame()(L99-L121)则直接解析 ADR-018 二进制包:HEADER_SIZE = 20,从偏移 4/5/6/8 读取 node_id(u8)、rssi(i8)、n_subcarriers(u16 LE)、payload_size(u16 LE),并做 magic 校验与长度越界保护。
窗口缓存类 SpectrogramWindow(L127-L217)负责三件事:
push():环形缓冲,长度不匹配时补零/截断到nSubcarriers;toGrayscale()(L166-L192):先在frames[t][sc]双循环上找整窗 min/max(range = max - min || 1防除零),再按行主序(行=子载波、列=时间帧)输出Uint8Array像素;toCnnInput():执行 Step 2 的上采样(见下节)。
--ascii 模式还会把当前窗口打印成终端频谱图:行下采样到最多 32 行,每个像素映射到 BARS = [' ', '▁', '▂', ..., '█'] 八级强度字符,并带 sc### 行号与 t=0 ... t=19 时间轴标注(L227-L253),便于肉眼核对暗条纹与波纹是否如 ADR 描述。
四、Step 2:CNN 嵌入
64×20 灰度矩阵需要先适配 @ruvector/cnn 的输入约定:
- 上采样:最近邻插值到 224×224。ADR 明确选择最近邻而非双线性,是因为要保留离散的子载波结构,避免插值抹平;
- 通道复制:灰度复制成 3 通道 RGB,因为 CNN 期望 RGB 输入;
- 嵌入维度:128(从默认 512 降下来,换取紧凑存储与更快 kNN);
- 归一化:L2 启用——单位球面上余弦相似度退化为点积;
- 延迟:现代硬件上约 5ms/窗口。
源码中 toCnnInput()(L199-L216)的最近邻映射为 srcY = floor(y * height / 224)、srcX = floor(x * width / 224) 并做边界钳制,三个通道写同一灰度值。
一个值得注意的实现细节在 initCnn()(L265-L305):脚本绕开了 CnnEmbedder 封装类,直接加载 WASM 绑定。原因写在注释里——封装类构造函数调用 new wasm.WasmCnnEmbedder(wasmConfig) 时会消费(销毁)EmbedderConfig 指针,随后又尝试从已成 null 的指针读 wasmConfig.embedding_dim。因此脚本先 require('vendor/.../ruvector-cnn/ruvector_cnn_wasm.js')、读取 ruvector_cnn_wasm_bg.wasm 字节,再手工构造 EmbedderConfig(input_size = 224、embedding_dim = 128、normalize = true),并在构造前把维度保存下来自己跟踪。这段“踩坑记录”对复用该 WASM 包的读者有直接参考价值。
运行方式与参数
脚本以 parseArgs 严格模式解析参数,主要选项(默认值取自源码):
| 参数 | 短名 | 默认 | 作用 |
|---|---|---|---|
--file <path> |
-f |
- | 读取 .csi.jsonl 录制文件 |
--live |
- | false | 监听 UDP 实时 CSI |
--port <p> |
-p |
5006 | UDP 端口 |
--ascii |
- | false | 打印 ASCII 频谱图可视化 |
--ingest |
- | false | 把 128 维嵌入送入 Cognitum Seed |
--knn <k> |
-k |
0 | 检索 K 个最相似历史频谱图 |
--seed-url |
- | https://169.254.42.1:8443 |
Seed 地址 |
--seed-token |
- | 空(或 $SEED_TOKEN) |
鉴权 token,--ingest 必需 |
--window <n> |
-w |
20 | 每频谱图的帧数 |
--stride <n> |
-s |
10 | 窗口间滑动步长 |
--dim <d> |
-d |
128 | CNN 输出维度 |
--json |
- | false | JSON 输出(便于管道消费) |
--limit <n> |
-l |
∞ | 限制处理帧数 |
典型命令(引自 csi-spectrogram.js 文件头注释;其中 data/recordings/*.csi.jsonl 为 ADR 示例录制文件,需由采集流程自行产出,当前仓库快照未包含):
node scripts/csi-spectrogram.js --file data/recordings/pretrain-1775182186.csi.jsonl --ascii
node scripts/csi-spectrogram.js --live --port 5006 --ingest --seed-url https://169.254.42.1:8443
node scripts/csi-spectrogram.js --file data/recordings/pretrain-1775182186.csi.jsonl --knn 5
文件模式的核心循环在 processFile()(L427-L537):逐行 JSON.parse,按 node_id 维护各自的 SpectrogramWindow,窗口填满后以 (totalPushed - WINDOW_SIZE) % STRIDE === 0 为触发条件提取嵌入(L472),随后依次做 ASCII 打印、--json 输出(含 embedMs 计时)、kNN 检索与 Seed 入库。Live 模式(processLive())先尝试 ADR-018 二进制解析,失败再退化为 JSON 解析,格式与文件模式完全一致。
五、Step 3:图 Transformer 多节点融合
ADR 的融合图定义为:
Nodes: {Node_1, Node_2}
Edges: {(Node_1, Node_2, weight=cross_correlation)}
Node features: 128-dim CNN embedding per node
注意力机制按 GATv2 思路分四步:
- 每个节点 128 维嵌入投影出 Query/Key/Value;
- 边权 = 两节点原始幅度向量的 Pearson 互相关(刻画两个节点 CSI 观测的一致程度);
- 注意力得分 =
softmax(Q_i * K_j / sqrt(d) + edge_weight_bias); - 输出 = 值向量的加权和。
结果是一个融合 128 维向量,自动给信号更干净(SNR 更高、衰落更少)的节点更大权重。推广到 3 个及以上节点时,加一个节点只多两条边,注意力机制无需改架构即可处理变尺寸图。
mesh-graph-transformer.js 给出了一份纯 JS 实现(不依赖 WASM 即可运行融合本体):
GraphAttentionLayer(L148-L286):GATv2 风格多头注意力,headDim = floor(inputDim / numHeads);Wq/Wk/Wv/Wo采用 Xavier 均匀初始化;边权偏置项按scores[j] = dot * (1/sqrt(d)) + edgeWeight * 0.5(edgeBiasScale = 0.5)叠加后做数值稳定的 softmax(先减最大值);各头拼接后由Wo投回输入维度,最后对 N 个节点输出做平均池化得到融合向量;单节点时直接透传(attentionWeights = [[1.0]]);pearsonCorrelation()(L296-L312):标准 Pearson 公式,分母非正时返回 0;MeshGraph(L322-L395):以Map<nodeId, {features, amplitudes, rssi, timestamp}>维护节点,computeEdgeWeights()对所有节点两两计算相关系数(键为"i-j"且i<j排序),fuse()在节点数 ≥2 时产出{fused, attentionWeights, nodeIds, edgeWeights};- 8 维手工特征的 JS 版
extract8DimFeatures()(L91-L133):mean/variance/maxAmp、相位两项占位 0(注释说明需要原始 I/Q 才能得到真相位)、归一化带宽(幅度高于mean*0.1噪声底的子载波占比)、归一化频谱质心、|rssi|/100。--dim 8时用它,--dim 128的纯文件模式则用 8 维补零占位(真实 128 维嵌入需经由csi-spectrogram.js管线产生); - 可选持久化:
initGraphDb()(L407-L421)尝试加载@ruvector/graph-wasm并创建GraphDB('cosine');persistToGraphDb()把每个节点写成带ESP32/SensingNode标签的节点(属性含node_id、rssi、timestamp、feature_dim),节点对写成CSI_CORRELATION边(属性含相关系数与融合次数)。加载失败时优雅降级为纯内存图——这也是 ADR 风险表中“图 Transformer 开销”缓解措施在代码里的落点。
运行方式:
node scripts/mesh-graph-transformer.js --file data/recordings/pretrain-1775182186.csi.jsonl [--dim 8|128] [--heads 4]
node scripts/mesh-graph-transformer.js --live --port 5006 --dim 128
--heads 默认 4,融合触发频率为每 --window(默认 20)帧且在线节点 ≥2 时执行一次。
六、Step 4:存储与检索
融合后的 128 维嵌入与既有 8 维特征并存于 Cognitum Seed(ADR-069):
| 存储 | 维度 | 内容 | 用途 |
|---|---|---|---|
csi-features |
8 维 | 手工统计特征 | 快速存在检测 |
csi-spectrograms |
128 维 | CNN 频谱图嵌入 | 环境指纹、异常检测 |
csi-spectrograms-fused |
128 维 | 图融合多节点嵌入 | 跨视角房间签名 |
128 维库上的 kNN 检索找出“长得像”的历史频谱图,ADR 列出四类用法:环境指纹(这个射频图案匹配哪个房间)、跨房间迁移(哪个训练房间与部署房间最相似)、异常检测(与所有已知模式相似度都低 = 未知环境或新型活动)、时间分段(相似度骤降 = 活动转换边界)。
代码侧对应两个通道:
- 进程内 kNN:
EmbeddingStore(csi-spectrogram.js L321-L350)线性扫描 + 余弦相似度排序,查询时先取k+1再过滤自身; - Seed 入库:
ingestToSeed()(L372-L421)以 Bearer token POST/v1/vectors/upsert,payload 为{ store: 'csi-spectrograms', vectors: [{ id: 'spectrogram-<nodeId>-<windowIdx>', values: [...128 floats], metadata: { node_id, timestamp, window_idx, rssi, subcarriers } }]},token 取自--seed-token或环境变量SEED_TOKEN,缺失时明确报错。
ADR 中三套表征的对比表完整保留如下:
| 属性 | 8 维手工 | 128 维 CNN | 组合 |
|---|---|---|---|
| 子载波结构 | 丢失 | 保留 | 两者都有 |
| 时序模式 | 丢失 | 保留(20 帧窗口) | 两者都有 |
| 计算 | ~0.1ms | ~5ms | ~5ms |
| 单向量存储 | 32 字节 | 512 字节 | 544 字节 |
| kNN 区分度 | 低(8 维灾难) | 高(128 维超球面) | 最高 |
| 可解释性 | 高(具名特征) | 低(学习得到) | 混合 |
| 是否需要训练 | 否 | 可选(预训练可直接用) | 可选 |
| 多节点融合 | 平均/取最大 | 图注意力 | 图注意力 |
七、可选增强:对比训练
预训练权重开箱即用;若要在 CSI 域上进一步调优,ADR 给出对比训练配方:
- 正样本对:同一房间、不同时间窗(嵌入应相近);
- 负样本对:不同房间或不同活动(嵌入应相异);
- 损失:InfoNCE,温度 0.07(SimCLR 标准值);
- 数据增强:时间平移(窗口滑 1–5 帧)、子载波 dropout(置零 10% 行)、幅度抖动(乘以 uniform[0.8, 1.2])。
目标是让 CNN 学到“同一房间的不同时刻应产生相似嵌入,不同房间应产生不同嵌入”。参考文献即 SimCLR(Chen et al., 2020)与 GATv2(Brody et al., 2021),见 ADR 原文 References。
八、后果、风险与缓解
正面影响:128 维承载 8 维装不下的二维结构;能区分 8 维特征空间中“长得一样”的房间;步态周期、呼吸频率等时序模式被编码进频谱图纹理;图注意力自动加权最 informative 的节点,提升单节点遮挡/干扰下的鲁棒性;128 维库与 8 维库并行运行、零迁移成本;WASM CNN 可跑在 sensing-server UI 里做实时可视化。
负面影响:每窗口 5ms 延迟(对 ADR-073 的 750ms 轮转、约 1.3Hz 更新率可接受,但限制更实时场景);约 900KB 模型一次性下载、首次加载后缓存;每向量存储是 8 维的 16 倍(以“每个 20 帧窗口存一条而非每帧一条”缓解);嵌入不可人读;64×20 上采样到 224×224 存在填充区的算力浪费。
风险与缓解(ADR 原文表格):
| 风险 | 缓解 |
|---|---|
| CNN 嵌入对 CSI 区分度不足 | 在 CSI 频谱图上对比微调;若 128 维 kNN 召回更差则回退 8 维 |
| 2 节点图上图 Transformer 开销 | 轻量注意力(单头、无 MLP);2 节点时 O(1) |
| 64×20→224×224 上采样伪影 | 最近邻保留离散结构;可考虑在原生 64×20 输入上训练更小 CNN |
| WASM 初始化延迟 | 在服务器启动时调用 init(),而非每请求一次(initCnn() 用 cnnInitialized 标志保证只初始化一次,见 L259-L266) |
九、小结
ADR-076 的价值在于给 RuView 的 CSI 感知链路增加了一条“图像化”表征通道:以 csi-spectrogram.js 实现帧解析→频谱图归一化→WASM CNN 128 维嵌入→kNN/Seed 入库的完整管线,以 mesh-graph-transformer.js 实现 Pearson 边权 + GATv2 多头注意力的多节点融合,并与 ADR-069 的 8 维 csi-features 库并行共存、增量落地。相关决策脉络可继续追溯 ADR-018(帧格式)、ADR-069(Seed 桥)、ADR-029(多静态感知)与 ADR-073(多频网格扫描)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00