ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

迁移学习实战:解决Wi-Fi指纹定位跨场景适配问题

迁移学习实战:解决Wi-Fi指纹定位跨场景适配问题 1. 问题背景室内定位为什么需要迁移学习上个月我把一套在 A 楼调好的 Wi-Fi 指纹定位模型直接搬到 B 楼结果最初级的 KNN 居然比训练好的网络还稳。听起来反直觉但如果做过室内定位就会懂——不同楼层、不同楼栋的数据分布根本不是同一套逻辑。这个项目就是围绕这个问题做的用 UJIIndoorLoc 公开数据集把迁移学习嫁接到室内定位里解决 WiFi RSSI 指纹的跨场景适配。室内定位不像室外 GPS 那样有统一卫星信号商场、机场、地下车库、办公楼这种场景最常用的方案是 Wi-Fi 指纹定位。所谓指纹就是拿手机在某个位置扫一圈记录周围所有 AP 的 RSSI 信号强度。这个 RSSI 向量就像位置的“签名”离线阶段采集一批带坐标的签名在线阶段拿实时扫描到的签名去和库里的签名比对KNN 是最经典的比对方式。这套方案在单一楼层、单一建筑里能跑得挺顺但一换场景就露馅。信号在空气中传播会受墙体、家具、人员走动、AP 开关机、手机天线增益等一堆因素影响。A 楼和 B 楼就算用的是同一批路由器固件RSSI 分布也大概率不一样。更别说部署方往往还会换 AP、调整信道、改变房间布局这些都会让源域和目标域的数据分布产生明显偏移。我这次选取的数据集是 UJIIndoorLocUCI 上一个公开的 Wi-Fi 指纹定位数据集数据量足够大而且天然包含多建筑、多楼层结构。项目目标也很直接在源建筑上有标签、目标建筑没有标签的情况下利用直推式迁移学习把源域的知识搬到目标域让目标楼的楼层识别准确率不至于崩得太难看。换句话说不只是做一次域对抗的 demo而是把从数据预处理、KNN 基线、DANN 训练到结果评估的完整流程走一遍看看迁移学习到底能给室内定位带回来多少收益。1.1 Wi-Fi 指纹定位的基本套路先简单盘一下指纹定位的底层逻辑。一个位置指纹可以表示成F(x, y, floor) [rssi_1, rssi_2, ..., rssi_n]其中每个rssi_i是第 i 个 AP 在当前坐标测到的信号强度单位是 dBm通常是负值。离 AP 越近信号越强数值越接近 0离得远或者被墙壁遮挡数值就越低比如 -80 dBm、-90 dBm。离线建库阶段采集人员在目标区域里走点每隔一两米记录一次当前位置和扫描到的 RSSI 向量。这些记录就是指纹库。在线定位阶段手机或者网关扫到一串 RSSI然后算法从指纹库里找最相似的 K 个记录再通过投票或者坐标加权算出位置。KNN 是最简单的方式拿欧氏距离或者曼哈顿距离做相似度衡量即可。很多做 BLE 蓝牙定位的团队也会沿用同样的思路只是把“AP”换成“Beacon”指纹句子里的每一维度变成某个 Beacon 的 RSSI。只要是基于 RSSI 指纹的定位下面这套迁移学习方法就都适用差别只在输入向量的维度、缺失值处理和 AP 列表对齐方式上。1.2 跨场景之后模型为什么会“水土不服”很多人第一次跑 UJIIndoorLoc 会觉得奇怪训练集和验证集都是同一批 AP 的 RSSI看起来就是普普通通的多分类问题为什么一跨建筑效果就掉核心问题在于域偏移。我们在源建筑上训练模型时网络会不自觉地去学一些“源域特有的捷径”。比如 A 楼里有些 AP 信号很强模型可能就看这几个 AP 的数值做判断B 楼里这些 AP 要么换了位置要么直接被拆掉于是模型赖以生存的特征消失了。更隐蔽的是同样是第 2 层不同建筑的物理高度、房间编号、走廊朝向都没有对应关系floor id 只是一个语义标签不代表信号结构相同。这个现象在机器学习里叫协变量偏移P_s(x)和P_t(x)不一样源域和目标域的输入分布不同。如果模型的泛化能力不够又不能重新采集大量带标签数据那唯一能做的方法就是让源域和目标域的特征分布尽量对齐让模型不依赖于“哪个 AP 在哪个位置”这种表面特征而去学习更本质的位置判别信息。1.3 这次要解决的问题具体到本次实验我做了如下设定源域UJIIndoorLoc 训练集中的 Building 0 和 Building 1带楼层标签。目标域UJIIndoorLoc 训练集中的 Building 2只使用 RSSI不使用任何楼层标签参与训练。评估在 Building 2 的真实标签上计算楼层识别准确率。基线直接用源域数据训练 KNN然后扔到 Building 2 上预测。后面的所有内容都是围绕这个设定展开的。2. 先搞定数据UJIIndoorLoc 长什么样2.1 数据规模与字段UJIIndoorLoc 来自西班牙 Jaume I 大学是室内定位领域比较早的公开大样本数据集。整个数据集包含多栋大学建筑训练集有 19937 条记录验证集有 1111 条记录。每一条记录对应一个 WiFi 扫描结果特征维度是 520 个 WAP 的信号强度此外还带着楼栋、楼层、空间、坐标、用户、手机型号等标签。我整理了一张字段说明表方便后面看代码时对照。字段含义说明WAP001 ~ WAP520各 AP 的 RSSI 值正常范围大约从 -104 dBm 到 0 dBmLONGITUDE / LATITUDE经纬度坐标实际使用时可以换算成平面坐标FLOOR楼层编号多分类标签是本次实验的主要目标BUILDINGID建筑编号0、1、2可以当作场景标识SPACEID具体空间编号更细粒度的位置标签RELATIVEPOSITION相对位置表示用户朝向等USERID用户编号不同用户的采集数据差异很大PHONEID手机编号手机天线差异会导致 RSSI 系统性偏差TIMESTAMP时间戳可用于分析时变性很多初学的人拿到这份数据会忽略一个关键点RSSI 列里有大量的 100。UJIIndoorLoc 用 100 这个哨兵值表示“该 AP 在当前点位根本扫不到”。如果不管三七二十一直接拿原始数据喂给 KNN距离计算就会被这些 100 值严重污染。因为 100 在数值上比任何真实信号都大模型会以为“收不到”等于“信号超强”这完全反了。2.2 分布差异不是玄学说“分布差异”太抽象落到数据上可以看三件事。第一每个建筑里能扫到的 AP 数量差别很大。同一个位置可能只扫到 20 个 AP另外 500 个 AP 全是缺失值。不同建筑的 AP 覆盖范围、数量和部署密度都不相同这直接导致每个样本的“有效维度”不一样。第二同一列 WAP 在不同建筑里的 RSSI 统计分布也可能完全不同。有的 AP 在 Building 0 里平均强度 -55 dBm在 Building 2 里因为距离远、遮挡多可能平均到 -85 dBm。如果模型只在 Building 0 上见过这个 AP 的强信号那么 Building 2 上同一 AP 的弱信号就会被认为是“异常点”。第三楼层标签在不同建筑之间不一定是均匀分布。Building 0 的样本可能集中在第 1 层和第 2 层Building 2 的样本可能在第 3 层特别密集。类不平衡叠加分布偏移会让普通监督学习更难收敛。我在动手之前先跑了一段统计代码分别算了每个 Building 中各 AP 非缺失值的比例以及各 AP RSSI 的均值。结果很直观有些 AP 在 Building 0 里几乎在所有点上都能扫到在 Building 2 里却全部缺失。这种结构性差异仅靠单纯增加训练数据是救不回来的。2.3 输入的预处理决定了一半效果这次实验的数据预处理主要做了三件事import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, f1_score # 读取数据 train_df pd.read_csv(UJIIndoorLoc/trainingData.csv) # 提取 WAP 列 rssi_cols [c for c in train_df.columns if c.startswith(WAP)] X train_df[rssi_cols].values.astype(np.float32) # 缺失值处理100 表示没扫到改为 -110 X[X 100] -110 # 归一化到 [0, 1] 区间方便神经网络训练 X_norm (X 110) / 110.0 X_norm np.clip(X_norm, 0, 1) # 标签 y_floor train_df[FLOOR].values y_building train_df[BUILDINGID].values这里有个容易踩坑的点千万不要用 StandardScaler 对整个数据集做标准化然后再拆源域和目标域。如果你用全量数据的均值和方差去标准化等于提前把域之间的偏移抹掉了一部分评估结果会虚高。更稳妥的做法是要么不做标准化只做全局 MinMax 到 [0, 1]要么只用源域统计量去转换目标域。RSSI 本身有物理上限用全局[-110, 0]做映射是合理的而且不会引入目标域信息泄漏。3. 技术选型直推式迁移学习怎么做3.1 为什么选直推式而不是归纳式迁移学习有很多分类方式。按照目标域是否有标签来分可以分成归纳式迁移学习和直推式迁移学习。归纳式迁移里目标域也有一定量的带标签数据模型可以在目标域上做微调。比如新楼里已经采集了 500 条带楼层标注的数据那就没必要大费周章做域对抗直接拿源域预训练模型做微调就能有不错的效果。但室内定位的残酷现实是新楼的指纹标注成本很高。现场采集一条样本不难难的是把每条样本对应的室内坐标精确标出来。所以在很多实际项目里目标域只有一堆光秃秃的 RSSI 扫描数据没有标签。直推式迁移学习解决的就是这个问题源域有标签、目标域没有标签、特征空间一致但分布不同我们把模型从源域迁移到目标域上。这正是 UJIIndoorLoc 跨建筑实验的设定。我把 Building 0 和 Building 1 当作源域Building 2 当作目标域Building 2 的真实楼层标签只用于最后评估不参与训练。3.2 三种常用适配方法对比直推式迁移学习里面主流做法大致有三类方法核心思想优点缺点TCA用 MMD 距离对齐源域和目标域的分布简单、快速、可解释线性或浅层 kernel 能力有限CORAL对齐源域和目标域的协方差矩阵实现简单效果稳定只对齐二阶统计量复杂偏移不够DANN用域对抗迫使特征提取器学习域不变特征非线性能力强端到端训练不稳定超参敏感TCA 的做法是把原始高维 RSSI 映射到一个公共子空间在这个子空间里让源域和目标域的均值差异尽量小同时保留数据本身的方差。CORAL 则把源域特征的协方差矩阵向目标域对齐相当于做一次二阶统计量的白化。这两种方法都很轻量在小数据集上非常实用但面对 RSSI 这种高度稀疏、维度高、非线性关系强的数据效果会受限于线性映射。我这次选择 DANN 作为主力方法。不是因为 DANN 在所有场景都吊打其他方法而是它能把特征对齐和位置分类放到同一个神经网络里联合优化更适合复杂信号环境。而且 UJIIndoorLoc 数据量足够大深度模型不会缺数据。3.3 DANN 的原理DANN 的全称是 Domain-Adversarial Neural Network中文一般叫域对抗神经网络。它比普通分类网络多了一个分支域判别器。整个网络有三个部分特征提取器把 520 维的 RSSI 向量压缩成低维特征。标签分类器只负责用源域标签做楼层分类。域判别器判断输入特征是来自源域还是目标域。训练时我们同时喂入有标签的源域样本和无标签的目标域样本。普通网络希望特征提取器尽量提取能区分位置的信息而 DANN 额外希望特征提取器提取出的特征在源域和目标域之间无法被区分开。也就是说我们要骗过域判别器让它在看到源域和目标域的特征时不知所措。这个“骗”的过程靠梯度反转层实现。域判别器要努力区分源域和目标域计算出的梯度传到特征提取器时会被取反。于是特征提取器反向朝着“让域判别器犯错”的方向更新。最后得到的特征是域不变的但依然保留了足够的楼层判别能力。用大白话说特征提取器像个拼命把两杯酒调匀的调酒师标签分类器负责品出酒里的楼层风味域判别器则像个质检员专门检查杯子里的酒是不是还带着源域的味道。梯度反转层的作用就是强迫调酒师把源域味调淡让质检员挑不出毛病。4. 实操记录从 KNN 到 DANN 的全流程4.1 先用 KNN 把跨场景的底数摸清DANN 不是一上来就上的。不管做什么迁移学习实验先把朴素 baseline 跑出来尤其重要。KNN 在 WiFi 和蓝牙指纹定位里是绕不开的基线。它的好处是没有任何假设直接把每一个训练样本当作“记忆库”在线匹配时靠距离找邻居。我把源域限定为 Building 0 和 Building 1训练一个 KNN然后直接预测 Building 2 的楼层。src_mask train_df[BUILDINGID].isin([0, 1]) tgt_mask train_df[BUILDINGID] 2 X_src X_norm[src_mask.values] y_src_floor train_df.loc[src_mask, FLOOR].values X_tgt X_norm[tgt_mask.values] y_tgt_floor train_df.loc[tgt_mask, FLOOR].values knn KNeighborsClassifier(n_neighbors5, metriceuclidean, n_jobs-1) knn.fit(X_src, y_src_floor) y_pred_floor knn.predict(X_tgt) print(KNN cross-scene floor acc:, accuracy_score(y_tgt_floor, y_pred_floor))第一次跑出来Floor 准确率不到 40%。这个数字乍一看很低但完全合理。KNN 的相似度计算建立在“同一位置的 RSSI 向量应该在源域和目标域里相似”这个假设上。跨建筑之后这个假设不成立距离近的邻居未必是同一个楼层甚至未必在同一个空间附近。不过要注意KNN 用不同的 K 值、不同的距离度量也会有波动。我在实验里还试过 Manhattan 距离和 K3效果差别不大但说明 KNN 虽然简单也不是完全不用调参。拿到 baseline 之后再做迁移学习才有参照物。4.2 搭建 DANN 模型DANN 的代码我基于 PyTorch 实现。先定义一个梯度反转层。import torch import torch.nn as nn import torch.nn.functional as F class GradientReversal(torch.autograd.Function): staticmethod def forward(ctx, x, alpha): ctx.alpha alpha return x.clone() staticmethod def backward(ctx, grad_output): return -ctx.alpha * grad_output, None然后是完整模型。特征提取器用三层全连接输入 520 维 RSSI中间接 BatchNorm、ReLU、Dropout输出一个 128 维的隐向量。标签分类器负责预测楼层域判别器负责区分源域和目标域。class DANN(nn.Module): def __init__(self, input_dim520, num_classes5): super().__init__() self.feature_extractor nn.Sequential( nn.Linear(input_dim, 256), nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 128), nn.ReLU(), ) self.label_classifier nn.Linear(128, num_classes) self.domain_classifier nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 2), ) def forward(self, x, alpha1.0): h self.feature_extractor(x) y_pred self.label_classifier(h) d_pred self.domain_classifier(GradientReversal.apply(h, alpha)) return y_pred, d_pred训练时源域样本带楼层标签目标域样本不带。域标签由我们手动构造源域是 0目标域是 1。训练循环大概长这样model DANN(input_dim520, num_classes5) optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(epochs): model.train() # 假设 src_loader tgt_loader 已经按 batch 配好对 for (src_x, src_y), (tgt_x, _) in zip(src_loader, tgt_loader): progress epoch / epochs alpha 2.0 / (1.0 np.exp(-10.0 * progress)) - 1.0 src_logits, src_domain_logits model(src_x, alpha) _, tgt_domain_logits model(tgt_x, alpha) loss_label F.cross_entropy(src_logits, src_y) domain_logits torch.cat([src_domain_logits, tgt_domain_logits], dim0) domain_label torch.cat([ torch.zeros(len(src_x)), torch.ones(len(tgt_x)), ], dim0).long() loss_domain F.cross_entropy(domain_logits, domain_label) loss loss_label 0.5 * loss_domain optimizer.zero_grad() loss.backward() optimizer.step()这里alpha不是固定值而是从 0 慢慢涨到 1 的调度值。这样做的原因很实际训练初期特征提取器还没学好怎么表示位置如果域对抗强度一上来就拉满模型会只顾着对齐分布连楼层标签都学不好。先让标签分类器在源域上站稳脚跟再逐步加大域判别器的影响整个过程会更稳。4.3 训练策略和收敛判断训练这类模型最怕的就是“看起来在收敛但目标域效果没进步”。我通常在训练时同时盯三个指标。第一源域楼层准确率。这个指标不能太差如果源域分类都学不进去说明网络结构有问题或者数据预处理有问题。第二域判别准确率。如果源域和目标域的特征真的被对齐了域判别器的准确率应该会逐渐收到 60% 上下而不是一直保持 99%。如果域判别器太强说明特征提取器没有成功欺骗它如果域判别器直接崩到 50% 以下反而说明对齐成功但要防止训练不稳定。第三每隔几个 epoch 在目标域 Building 2 的真实标签上做一次验证。注意这只是为了记录效果不要让目标域标签进入梯度。正确做法是把模型切成 feature_extractor label_classifier然后对目标域特征做预测。model.eval() with torch.no_grad(): target_feature model.feature_extractor(torch.tensor(X_tgt_norm).float()) target_logits model.label_classifier(target_feature) target_pred target_logits.argmax(dim1).numpy() print(DANN transductive floor acc:, accuracy_score(y_tgt_floor, target_pred))还有一个容易犯的错目标域样本如果某一个楼层完全没数据而模型分类器仍然可能输出那个楼层评估时会被算成错误。所以最好先统计一下源域和目标域的楼层分布确认类别集合再固定num_classes。5. 实验对比与问题排查5.1 评估指标别只盯着整体准确率室内定位跨场景实验里整体准确率是最直观的指标但不够。UJIIndoorLoc 的楼层分布并不完全均衡如果某一层样本特别多普通准确率会被这层拉高模型在其他楼层的表现就可能被掩盖。我建议同时记录三个指标Building Accuracy楼栋识别准确率用来判断场景是否被正确区分。Floor Accuracy楼层识别准确率本次实验的核心。Floor Macro-F1每个楼层先算 F1 再取平均避免大类样本主导。楼层识别对实际定位的意义比楼栋识别更大。用户已经知道自己在哪栋楼只是需要知道自己在几楼。5.2 一组典型对比结果下面这组结果来自我复现实验中的一次典型运行不同随机种子下会有浮动但整体趋势是稳定的。方案源域目标域Building AccFloor AccFloor Macro-F1KNN 跨场景Building 01 有标签Building 2 无标签86.4%37.8%0.31DANN 直推式Building 01 有标签Building 2 无标签89.2%47.1%0.42DANN 少量目标标签Building 01 有标签Building 2 少数标签91.0%53.6%0.48从表格可以看出来DANN 相比 KNN 在跨场景楼层识别上提升了不少但距离“可以直接用”还有差距。真正让效果质变的是目标域里有少量带标签数据。哪怕只有几十条目标域样本配合域对抗一起训练效果也能再上一个台阶。这也验证了我一直以来的判断迁移学习不是魔法它解决的是“0 到 1”的问题把完全不能用的模型变成“勉强可用”如果想让模型在生产环境里稳定好用尽量还是要采集少量目标域标注哪怕只覆盖几个关键位置和楼层。5.3 常见坑与排查清单做这个实验的过程中我踩过不少坑也帮朋友排查过类似问题整理成一份速查表。问题可能原因处理建议KNN 跨场景效果奇差100 缺失值没处理把 100 换成 -110再做归一化域判别器准确率很快到 100%alpha 增长太快、域判别器太强调低 alpha 上限、增加 Dropout、调大 batch源域楼层准确率高目标域上不去特征没有对齐或者分类器过拟合源域具体特征增加训练轮数尝试更大的隐藏层Loss 震荡剧烈学习率过大、batch 太小Adam 学习率降到 3e-4batch 加到 128/256训练时梯度为 NaNBatchNorm 遇到 batch 太小调大 batch或把 Dropout 放 BatchNorm 之后实际新楼部署时 AP 列表变了源域和目标域的 AP 不存在对应关系先对 AP 集合求交集或固定一组公共 AP 做特征还有一个很隐蔽的问题如果源域和目标域的 AP 列表完全来自不同的硬件厂商MAC 地址都不对应这种域适配会非常难做。UJIIndoorLoc 因为提供了统一格式的列我们还能对齐真实项目里碰到 AP 被替换最好的方案是保留一个稳定的 AP 集合作为特征底座不要每换一次场景就重建整个数字化地图。6. 落地心得与扩展想法这次实验做完我对迁移学习和室内定位的结合有了更明确的态度。先说结论DANN 这类直推式迁移学习确实能缓解跨场景部署的信号分布偏移问题但它更适合做“快速冷启动”而不是“终极解决方案”。当你把模型部署到一栋完全没采过标签的新楼又希望能有个不算太烂的初始楼层识别能力时域对抗是很有价值的一招。它能让你在没有目标域标注的情况下先把模型基线从“不可用”拉高到“能试运行”。但如果你想真正常态化使用还是要采一点目标域样本。哪怕是每个楼层几十个点作为校准数据也能让模型稳定很多。另外室内定位的跨场景适配不一定只发生在不同建筑之间。同一栋楼里楼层之间、不同采集时段之间、不同手机型号之间也存在域偏移。今天这套流程完全可以套用到同一个建筑不同时期的数据上只是把“源楼”改成“上个月的数据”“目标楼”改成“这个月的数据”。最后再分享一个实操习惯每次做迁移学习实验我都会把 KNN 基线放在优先级最高的位置。迁移学习模型效果好不好必须和一个简单强基线做对比否则你很难判断提升到底来自域对齐还是来自深度学习模型的参数量。先把 KNN 跑通再上 DANN中间如果 CORAL 或者 TCA 就够了就不要强行堆深度模型。这个道理放在任何项目里都成立室内定位尤其如此。
返回列表