ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

NILM入门实战:用nilmtk和UKDALE跑通非侵入式负荷监测全流程

NILM入门实战:用nilmtk和UKDALE跑通非侵入式负荷监测全流程 我第一次接触NILM非侵入式负荷监测的时候最痛苦的不是算法看不懂而是连数据集怎么下载、工具包怎么安装都折腾了整整一个周末。论文里清一色地写着“我们在UKDALE上做了验证”可nilmtk的文档又老又散UKDALE的官网还要填申请表格下载回来的HDF5文件加载出来是一堆报错。这篇文章就是为了把这套流程彻底跑通从NILM是什么、为什么用UKDALE和nilmtk到环境配置、数据下载、电器数据提取再到跑第一个分解算法全部串起来。只要你有一台电脑、装过Python哪怕完全没有NILM基础也能一步步复现出来。1. NILM到底是干什么的为什么都用UKDALE和nilmtk1.1 核心思路从一块总电表里拆出每一个电器NILM的全称是Non-Intrusive Load Monitoring非侵入式负荷监测。所谓“非侵入”就是不去每个电器插座上装传感器只在家庭总电表处装一个采样设备通过分析总功率、总电流的变化反过来推断家里有哪些电器在工作、每个电器消耗了多少电。你可以把它理解成从一张全家福照片里用图像分析技术把每个人的脸单独识别出来。你不用把每个人叫过来单独拍照只需要一张合影剩下的交给算法去拆。这件事在工程上其实是个盲源分离问题。不同电器工作时会在总功率曲线上留下独特的“签名”冰箱是周期性启停、电热水壶是功率瞬间拉高几百瓦然后几秒钟结束、微波炉的功率则带有明显的波动。NILM算法要做的就是识别这些签名并估计每个电器的运行状态。听起来不复杂但真正落地时会遇到采样率不齐、多电器同时开启、同类型电器特征重叠等问题这也是为什么需要一套标准工具来处理数据而不是自己从零写文件解析逻辑。1.2 UKDALE为什么入门首选这个数据集UKDALE全称是UK Domestic Appliance-Level Electricity dataset是英国多个家庭住宅的电器级电耗公开数据集。它最大的优势可以用三个词概括时间跨度长、电器类型全、采样周期短。UKDALE记录了多栋房子的整屋用电和各个电器独立回路的功率数据其中House 1的数据从2012年持续到2017年跨度达到几年整屋和电器通道的采样周期最短到1秒到6秒足够捕捉绝大多数电器的启停瞬间。对比其他常见数据集UKDALE的特点更加直观数据集住宅数量典型采样周期时间跨度主要场景UKDALE5栋1-6秒数月到数年长期负荷监测、多电器分解REDD6栋1-3秒数周到数月NILM经典基准AMPds21栋1秒-1分钟2年加拿大住宅含水电燃气ECO6栋1秒8个月德国住宅细粒度插座级我个人的体会是UKDALE之所以适合零基础入门一是House 1的电器通道包含冰箱、洗衣机、洗碗机、热水壶、微波炉、插座组等常见类别足够覆盖大多数实验场景二是官方提供了转换好的HDF5格式能直接被nilmtk的DataStore机制读取省去了自己写CSV解析器的时间三是数据量适中如果只截取几天做实验普通笔记本电脑完全跑得动。1.3 nilmtk帮我们省掉了哪些脏活累活nilmtkNILM Toolkit是学术界开源的一套NILM数据处理与分析工具包。它主要解决三件事数据格式统一、数据预处理、算法快速验证。数据格式统一这点特别重要。NILM领域有十多个公开数据集各自存储格式完全不同有CSV、有HDF5、有的还带单独的元数据文件直接混用会疯掉。nilmtk定义了标准的DataStore结构把不同数据集转换成统一的HDF5格式后上层代码的读写接口完全一致换数据集只需要换路径。除了格式统一nilmtk还封装了采样率重采样、时间窗截取、电器元数据管理、负荷分解算法如CO、FHMM、Hart85以及评估指标MAE、RMSE等常用组件。如果你是从零开始光是把UKDALE的原始数据读出来再把整屋和电器的采样率对齐至少得写上百行代码而nilmtk可以把这些操作压缩到几行调用。正因如此学术界发表NILM论文时很多对比实验都运行在nilmtk之上用它复现别人论文的结果也比较省力。2. 环境准备把nilmtk跑起来的正确姿势2.1 先认清版本兼容这个大坑nilmtk的问题在于它不是一个非常活跃更新的项目早期版本对Python和依赖库的版本比较挑剔。我自己实测下来Python 3.7搭配pandas 1.0.x、numpy 1.19.x是稳定性较好的组合而如果你用最新的Python 3.11很可能在安装依赖阶段就遇到wheel不匹配的问题。所以第一步强烈建议用conda创建独立的Python 3.7环境而不是直接装到系统Python里。conda create -n nilm python3.7 conda activate nilm pip install -U pip pip install numpy1.19.5 pandas1.0.1 scikit-learn0.24.1 pip install nilmtk装完之后可以验证一下是否正常python -c import nilmtk; print(nilmtk.__version__)如果能够输出版本号说明依赖基本就位。这里有一个容易踩的坑如果直接pip install nilmtk它可能会自动升级或降级你环境里的pandas进而引发不兼容。所以先固定numpy和pandas版本再装nilmtk能省去很多麻烦。2.2 不想折腾环境直接用Docker方案如果你对conda不太熟悉或者嫌版本问题太烦nilmtk官方提供了Docker镜像拉下来之后环境就是现成的。我个人在给朋友推荐入门方案时反而更推荐Docker因为它真正做到了开箱即用数据目录通过挂载映射进容器容器内的环境烂了随时可以重新拉一个不污染宿主机。docker pull nilmtk/nilmtk docker run -it -v ~/nilm_data:/data nilmtk/nilmtk /bin/bash启动后你会进入容器内的bash/data目录对应本机的~/nilm_data。把UKDALE的h5文件放到本机这个目录下在容器里直接用/data/ukdale.h5作为路径加载即可。需要注意的是容器的Python版本和依赖是官方打包好的原则上不会出现依赖冲突但如果你的宿主机器比较老Docker本身跑不起来那就还是走conda路线比较稳。2.3 安装验证的小技巧环境装好之后不要急着下载大数据集先用一个小数据集跑通流程是更靠谱的做法。nilmtk仓库里自带了iAWE和REDD的小样本转换脚本但最简单的方式是构造一个只有几百行记录的临时CSV通过nilmtk.dataset_converters转换成HDF5然后加载看看能否正常读取。这相当于对整个工具链做了个“冒烟测试”能快速暴露pandas版本、HDF5库、时间索引解析等方面的问题。做完验证再进入UKDALE的正式数据流程我心里就踏实很多。毕竟UKDALE的HDF5文件几个GB起步如果加载到一半才发现环境有问题来回排查的成本就太高了。3. 拉取并准备UKDALE数据保姆级实操3.1 获取UKDALE数据的完整路径UKDALE数据的官方获取渠道是填写申请表因为数据集用于学术研究作者要求登记使用意图填完之后会收到下载链接。下载页面会提供几种格式最重要的是HDF5格式文件名一般情况下是ukdale.h5。这个文件已经经过官方预处理包含了所有建筑的电表元数据和功率数据符合nilmtk的DataStore规范是零基础最推荐的起点。下载时需要留意的点是文件体积。ukdale.h5完整版有好几个GB如果网络状况一般下载可能要等一段时间。我个人的做法是先确认够用的范围再选文件。如果你只做House 1的实验可以先只下载H1对应的部分数据有些版本允许按建筑拆分的文件千万不要一上来就全量下载否则光是等下载就消耗掉你一半的耐心。下载完成后把文件放到一个清晰的目录下比如~/nilm_data/ukdale.h5后续所有代码都基于这个路径操作。3.2 加载UKDALE并查看建筑与电表信息用nilmtk加载HDF5数据集核心类是DataSet。下面的代码演示了怎么打开UKDALE并列出第一个建筑的基本电表信息from nilmtk import DataSet data DataSet(/Users/yourname/nilm_data/ukdale.h5) # 查看有哪些建筑 print(data.buildings) # 查看建筑1的电表列表 elec data.buildings[1].elec print(elec.metadata())elec对象是整屋所有电表的集合调用metadata()可以看到每个电表通道的设备名称、所在房间、采样周期等信息。对于UKDALE的House 1你会看到类似fridge、washer dryer、dish washer、kettle、microwave、sockets这样的标签。这里的关键点是elec对象同时包含总表mains和分项电表submeters后续提取数据时一定要清楚自己在操作哪一部分。3.3 设置时间窗不要让内存爆炸UKDALE跨度长达数年如果你直接把整年的数据加载进内存8GB内存的电脑基本会卡死。所以加载数据的第一个习惯动作就是设置时间窗set_window。比如我们只想看2013年的某一天data.set_window(start2013-05-01, end2013-05-02) elec data.buildings[1].elec设置时间窗之后所有后续的load()操作都只读取这个时间范围内的数据内存压力会小很多。这也是处理长期序列数据的一个通用技巧先缩小问题规模验证流程跑通再逐步扩大时间范围。你在看别人论文里的实验时往往只看到“用某几个月做训练、某几天做测试”实际上跑起来时也是按时间窗切块处理的而不是一次性把几年的数据塞进内存。4. nilmtk核心API实操提取电器数据与基础分析4.1 用ElecMeter对象读取单个电器的功率序列拿到elec对象后提取单个电器的数据是最高频的操作。nilmtk把每个电表封装成ElecMeter对象你可以通过设备名称直接索引fridge elec[fridge] df next(fridge.load()) print(df.head())这里的load()返回的是一个生成器里面每个元素是一个时间段内的DataFrame索引是时间戳列一般是功率数值单位通常是瓦特。用next()取第一个片段就能看到类似这样的结构power 2013-05-01 00:00:0100:00 62.33 2013-05-01 00:00:0700:00 61.01 2013-05-01 00:00:1300:00 62.80拿到的DataFrame可以直接用pandas做各种操作比如计算日均功耗、绘制曲线、统计启停次数。这一步其实就是NILM数据分析的起点先理解单个电器的行为模式再谈算法分解。4.2 整屋总功率与单个电器数据对齐NILM实验里你需要同时拿到总功率和单个电器功率并且要求两者的时间索引对齐。这通常涉及采样率重采样。UKDALE的总表和电器表采样周期略有差异直接用原始时间戳做计算会造成索引对齐错误所以要先把数据重采样到固定周期mains_df next(elec.mains().load()) fridge_df next(elec[fridge].load()) # 重采样到6秒间隔取平均值 mains_6s mains_df.resample(6s).mean() fridge_6s fridge_df.resample(6s).mean() # 合并到一个DataFrame里 aligned pd.concat([mains_6s, fridge_6s], axis1) aligned.columns [mains, fridge]这段代码里面有个细节值得展开为什么重采样用mean()而不是sum()或ffill()因为功率是瞬时量在6秒窗口内可能出现波动取平均能反映这个时段的平均功耗如果换成能量数据如Wh则应该用sum()累加。理解这一点可以避免后续在计算能耗时出现数量级错误。4.3 用select_top_k快速筛选高功耗电器实际住宅中电表通道很多不是每个通道都对实验有价值。nilmtk提供了一个非常方便的筛选方法select_top_k(k)按平均功耗从高到低取出前k个电器。在UKDALE的House 1里通常冰箱、洗衣机、热水壶会排在前几位top_elec elec.select_top_k(k5) for meter in top_elec.meters: print(meter.metadata())这个方法在数据分析阶段特别有用你不需要把几十个通道全部处理一遍先聚焦功耗高、行为模式明显的电器既省内存又容易上手。等模型跑通了再逐步增加电器种类看看算法在多电器场景下表现如何。4.4 画一条功率曲线看清电器行为对初学者来说“看图说话”是理解NILM问题的绝佳方式。直接用matplotlib画出一两天的总功率和冰箱功率曲线你会直观地看到冰箱的周期性启停特性也会看到总功率曲线上冰箱启停造成的台阶状跳变。import matplotlib.pyplot as plt aligned[2013-05-01].plot(figsize(12, 4)) plt.ylabel(Power (W)) plt.xlabel(Time) plt.show()画完图之后你可以尝试回答几个问题冰箱每次运行持续多久运行周期是多久它的平均运行功率是多少瓦这些问题看起来简单但正是NILM算法要做的事情从总功率信号中识别出这样的周期性模式并归属到具体电器上去。5. 跑通第一个算法用CO做负荷分解5.1 拆分训练集和测试集掌握数据提取之后就可以进入算法环节了。nilmtk内置了多种行李分解算法最简单的当属COCombinatorial Optimisation组合优化算法。CO的思路很朴素把每个电器的典型功率状态建模成离散状态比如冰箱有“待机、运行”两态然后穷举各电器状态的组合找一种组合使得状态总功率之和最接近观测到的总功率。实验开始前先用时间窗划分训练集和测试集。比如用2013年上半年某几天的数据训练模型再用2013年上半年另一天的数据做测试。需要注意NILM领域的训练集和测试集一般要求在时间上分离不建议交叉混用否则模型会“偷看”到测试时段的信息。5.2 CO算法的训练与分解实战下面是基于nilmtk跑CO算法的核心代码from nilmtk import DataSet from nilmtk.disaggregate import CO from nilmtk.utils import save_results # 加载训练数据 train DataSet(/path/to/ukdale.h5) train.set_window(start2013-05-01, end2013-05-07) train_elec train.buildings[1].elec # 加载测试数据 test DataSet(/path/to/ukdale.h5) test.set_window(start2013-05-08, end2013-05-09) test_elec test.buildings[1].elec # 训练模型对目标电器fridge进行建模 co CO() co.train(train_elec, fridge) # 对测试集做分解结果写入输出文件 output DataSet(/path/to/output.h5) prediction co.disaggregate(test_elec, output)disaggregate执行完之后预测结果被写入output.h5里面是模型估计的冰箱功率序列。你可以按前面的方式重新加载这个输出文件取出预测值与真实冰箱功率做对比。这一步跑通的意义很大。你亲手完成了一次“从总功率中分解出单一电器”的全流程数据加载、时间窗切分、训练、分解、结果落盘。后面换FHMM、换深度学习模型时很多流程都是类似的。5.3 评估MAE和RMSE怎么计算有了预测值和真实值量化算法性能就很重要。nilmtk提供了通用的评估指标函数但需要注意的是计算前必须保证预测结果和真实数据的时间索引完全对齐这意味着它们要经过相同的重采样处理。from nilmtk.metrics import mean_absolute_error, root_mean_squared_error # ground_truth是测试集里的真实冰箱功率prediction是分解预测值 mae mean_absolute_error(ground_truth, prediction) rmse root_mean_squared_error(ground_truth, prediction) print(MAE:, mae) print(RMSE:, rmse)MAE平均绝对误差直观反映预测偏差的平均水平单位是瓦特RMSE均方根误差对大偏差更敏感如果算法偶尔出现很大的估计偏差RMSE会明显变大。通常刚上手跑CO时冰箱这类强周期性电器的RMSE会在几十瓦到一百多瓦之间这个基线数值可以作为后续调参的参照。6. 常见问题与排查技巧实录6.1 安装与依赖问题速查表在帮助不少朋友跑通这套流程后我把最常见的问题和解决方法整理成了表格遇到报错时先对照这里排查问题现象常见原因解决方法pip安装nilmtk时卡在编译Python版本过高部分C扩展无预编译包换Python 3.7环境或使用Docker镜像import nilmtk时报错pandas或numpy版本不兼容固定pandas 1.0.1和numpy 1.19.5后重装加载HDF5文件失败文件路径不对或数据文件损坏校验文件md5确认路径中无中文特殊字符内存不足加载数据直接卡死没有设置set_window加载了过长时间范围的数据先缩小时间窗确认流程后再扩大范围某个电器名称索引不到用电表元数据里的实际名称不是中文或自己想当然的缩写先调用elec.metadata()查看准确的电器标签6.2 实操中的几个重要心得第一个心得跑算法前一定要先做数据可视化。很多人直接跳到算法步骤结果分解结果很差也不知道问题出在数据还是模型。先画出总功率和各个电器的曲线你能够提前判断哪些电器的特征比较明显、哪些电器可能很难分解比如和别的电器同时开启频率很高这样对后面算法的表现就会有一个合理预期。第二个心得重采样周期不是越小越好。虽然UKDALE有6秒精度的数据但训练CO时我建议可以尝试30秒或1分钟的重采样。原因很简单CO是一种基于稳态状态的算法它不太需要在秒级尺度上捕捉暂态特征反而细粒度数据会增加训练和搜索的耗时。先用低频粗粒度跑通整个流程再用高频细粒度做精度调优这是更高效的实验节奏。第三个心得如果是做深度学习方法比如后续尝试LSTM、Transformer类模型UKDALE的长时间跨度是很大的优势但同样面临数据切分的问题。切割数据集时千万不要让同一天的数据既出现在训练集又出现在测试集里按照不重叠时间窗去切分是基本原则。另一方面对于冰箱这类连续运行电器模型容易“学”到昼夜周期规律评估时最好覆盖完整的一天以上避免只测了几个小时的片段。6.3 如何把流程扩展到更多电器和更大时间范围当你成功跑通单个电器比如冰箱的分解流程后自然想扩展到洗衣机、洗碗机、热水壶等更多电器。做法很简单把co.train(train_elec, fridge)中的目标电器换成其他名称即可。但这里有一个务实的提醒不是所有电器都适合用CO分解。CO的前提是电器能建模成有限个离散功率状态冰箱、热水壶这类状态少的电器效果尚可而洗衣机内部有加热、洗涤、脱水等多个阶段状态数会变多CO的搜索空间和误差也会同步增长。遇到这种情况可以换用FHMM因子隐马尔可夫模型它同样在nilmtk中内置API和CO非常接近只是把“状态组合最优”换成了“状态序列概率最大”的思路。时间范围扩展也一样只需要调整set_window的起止时间注意控制单次加载的数据量必要时分批处理再汇总结果。写在最后的一些经验和建议这套流程我前前后后帮好几拨人跑通过最深的感触是NILM入门最大的门槛不在算法而在数据工程。UKDALE的下载、nilmtk环境的搭建、HDF5文件的读取、时间对齐这些环节每一项都卡过一批人。所以如果你也在折腾遇到装不上的包、读不出来的数据先别急着怀疑自己笨大概率只是版本或路径的问题对照上面的排查表多试几次就能过去。另外一个小技巧是刚开始做实验时不要追求复现论文里的精确数值先以“跑通流程”为目标。哪怕MAE高一点、曲线拟合得不够好只要整个训练-分解-评估链路能走通你就已经掌握了NILM的基本工作框架。之后再慢慢调参数、换模型、扩数据量都会有明确的方向。如果你跑通了这套流程可以再回头看看UKDALE官方文档里关于各个电器标记的说明也可以尝试用同样的代码加载其他NIH数据集比如REDD或AMPds2感受一下nilmtk数据集格式统一带来的便利。这一步跨过去NILM的大门就算真正打开了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进