非侵入式负荷分解(NILM)这几年被聊得很多,但真正把一个公开数据集完整跑通的人其实不多。问题通常出在两个地方:一是工具链太旧,二是很多人一上来就想上深度学习。我去年做能耗分析项目时,被NILMTK和REDD数据集折腾了差不多一周,后来把环境固定下来,才发现从拿到REDD到跑出第一个分解结果,前后就是一杯咖啡的时间。这篇文章就把整个流程完整摸一遍:NILMTK怎么装、REDD数据集怎么下怎么转、FHMM模型怎么训练怎么评估,还有哪些坑我提前帮你踩一遍。适合刚接触负荷分解的学生、做智能家居产品的工程师,以及任何想理解“只靠总电表能不能拆出单个电器功耗”的人。
先说清楚标题里的“5分钟”不是玄学。如果你已经装好环境、转好h5文件,后面从切窗口到出图确实能控制在5分钟左右。一次性准备环境的时间不算,否则光解决pandas版本冲突就可能耗掉一下午。下面我按自己实际操作过的顺序来讲,尽量给出可以直接抄的步骤。
1. 做第一个负荷分解模型前,先搞懂这些事
1.1 负荷分解到底在做什么
负荷分解的学名是Non-intrusive Load Monitoring,简称NILM。它解决的问题非常生活化:你家门口只有一个总电表,你只看到这个月总用电量,但你想知道冰箱、空调、微波炉、热水器各自用了多少电,又不想在每个插座上装独立监控硬件。办法就是在总表处采集整户的功率序列,再通过算法把总功率拆成不同设备各自的功率序列。
这个过程可以类比成:你只在公司门口放了一台摄像头,却想统计每个员工几点下班。要做到这一点,你必须事先知道每个人的出行习惯。在NILM里,那些“习惯”就是每种电器的功率指纹。冰箱是周期性的小波动,微波炉是短暂的大功率脉冲,烤箱是长时间连续的高功率平稳段,电热壶则是干脆利落的一锤子买卖。模型做的就是把混杂在一起的总功率曲线,按这些指纹重新分装回各台设备。
1.2 为什么用NILMTK而不是自己造轮子
NILMTK全称Non-intrusive Load Monitoring Toolkit,是NILM领域使用面最广的开源工具包之一。它最主要的价值是提供了三块积木:统一的数据集读取接口、多个经典分解算法、标准化的评估指标。比如你从REDD、UK-DALE、iAWE这些数据集拿到的原始格式完全不同,但都转换进NILMTK的HDF5结构后,后续算法和评估就能共用一套代码。这个抽象设计在最开始看可能不明显,等你换第二个数据集时就会感谢它。
NILMTK内置了组合优化CO、因子隐马尔可夫模型FHMM、独立HMM等一堆baseline算法,也提供了MAE、能耗误差等评估方法。用这些现成工具跑出来的结果,可以直接和论文里的baseline对齐。当然它也有明显的槽点:维护频率低,依赖的pandas版本偏老,很多新接口都没跟。但这不代表它没用。至少在做对比实验时,学术界就是认这一套。先把NILMTK跑通,再谈自己写深度学习模型,对我来说是效率最高的路线。
1.3 为什么选REDD数据集
REDD全称Reference Energy Disaggregation Dataset,是NILM领域最经典的公开数据集,源自麻省理工的实验项目。它采集了6套家庭的总表和多个单回路功率数据,同时提供低频和高频两种版本。很多NILM论文都在REDD上做对比,相当于这个方向默认的练功房。用REDD做实验,别人不用问你的数据从哪来,复现门槛也比较低。
我做第一个demo时直接选用REDD的低频部分。低频采样大概在1Hz左右,文件比高频小一个数量级,但对学习算法已经完全够用。如果一开始就选十几个GB的高频电流电压数据,下载慢、解析慢、训练更慢,很容易劝退。先低频把全流程跑通,再考虑高频数据里的电流电压特征,是更合理的安排。
1.4 “5分钟跑通”的真实含义
这里必须说清楚:5分钟不是从零开始到模型收敛。我的实际操作中,从已经装好NILMTK、已经有redd.h5文件的状态开始,选房子、切时间窗、训练FHMM、输出分解结果、画图,这个流程5分钟确实够用。原因在于我们只截取了一小段时间的数据,FHMM模型本身参数也不算多,跑起来非常快。
如果你连环境都没装,那请预留一两个小时。特别是conda环境创建和依赖安装,第一次容易在pandas版本上翻车。下面的章节我会把环境组合直接给出来,你照抄就能减少很多折腾。本文的目标是让你先把完整链路走通,先有结果再谈调参。
2. 环境搭建:和NILMTK的“旧版依赖”和解
2.1 版本冲突是最大的坑
NILMTK最后一次活跃维护大概在2017年前后,那时主流Python还是3.6、3.7,底层的pandas接口也和现在很不一样。在pandas 1.0之后,很多旧API被重写或移除,到了pandas 2.x基本是全线崩溃。所以安装NILMTK的第一原则就是:不要用最新的Python和最新的pandas,老老实实建一个旧版本环境。
我这里给出一组我实际测试过比较稳的组合:Python 3.7、pandas 0.25.3、numpy 1.21.6、scipy 1.7.3、scikit-learn 0.24.2、hmmlearn 0.2.7、tables 3.7.0。不敢说在所有机器上100%丝滑,但至少能把我下面要说的流程完整跑下来。如果你机器上已经存在其他项目,尤其是有深度学习环境,强烈建议不要用同一个环境装NILMTK,否则后续你的深度学习环境可能被pandas降级搞坏。
2.2 用Anaconda或Miniconda创建独立环境
为了避免把系统Python弄乱,建议用Anaconda或者Miniconda新建一个环境。命令非常简单:
conda create -n nilm python=3.7 -y conda activate nilm python --version看到输出Python 3.7就说明环境建好了。为什么强调独立环境?因为NILMTK依赖的旧pandas很可能和你日常数据分析、深度学习环境冲突。一旦装了,你用Jupyter跑其他项目时可能莫名其妙遇到DataFrame接口变化,排查起来很浪费时间。独立环境在这个场景里就是保命符,以后不想要了直接conda env remove -n nilm删掉,不会污染主环境。
2.3 安装NILMTK本体和依赖
安装顺序建议先装依赖,再装NILMTK本体。官方GitHub仓库是nilmtk/nilmtk,安装命令如下:
pip install --upgrade pip pip install numpy==1.21.6 pandas==0.25.3 scipy==1.7.3 scikit-learn==0.24.2 hmmlearn==0.2.7 tables==3.7.0 matplotlib joblib git clone https://github.com/nilmtk/nilmtk.git cd nilmtk python setup.py install如果GitHub速度慢,也可以直接用pip安装:
pip install nilmtk但我更推荐克隆源码后安装,因为NILMTK在实际使用中多多少少会遇到版本兼容问题,手上有源码可以直接grep,看某个函数到底在哪个模块里,比瞎猜快得多。安装完成后别急着开始,先进Python试一下导入:
from nilmtk import DataSet from nilmtk.disaggregate import FHMM如果导入不报错,说明核心包正常。有些教程里写的是from nilmtk.legacy.disaggregate import FHMM,这是因为NILMTK有过新老API分支,老API被挪到了legacy目录。你安装的版本到底用哪种,直接试一下就知道。
2.4 安装后先做一个最小验证
导入成功只能说包在,不一定代表数据I/O能正常工作。我建议再执行一段最简单的DataSet空指针测试:
from nilmtk import DataSet ds = DataSet("不存在的文件.h5")这一步预期会报文件不存在,而不是报其他奇怪的属性错误。如果你看到的是pandas的AttributeError或者MissingOptionalDependency之类,说明某些依赖的版本没对齐,建议按上面的版本组合重新装一遍。这种提前验证能帮你把环境问题隔离在正式实验之前,省下的时间远比你想象的要多。
3. 拿到REDD数据集后,第一步永远是转成h5
3.1 下载和解压REDD低频数据
REDD官网是redd.csail.mit.edu,通常需要填写一份下载申请表,然后你会收到下载链接或者FTP信息。建议直接下载redd_low_freq.tar.bz2,低频部分对入门足够。下载后解压:
tar xjf redd_low_freq.tar.bz2解压后你会看到redd_low_freq/目录,里面是house_1到house_6六个子目录。每个子目录里有很多channel_x.data文件,x是通道号。在REDD的定义里,通道1和通道2通常是两路总电表,其他通道分别对应不同单独回路,比如插座、冰箱、微波炉等。
这里有一个比较实用的建议:下载完先不要直接转整个目录,先只把house_1单独复制出来练习。等整个流程能跑通,再回头转换house_2到house_6。REDD低频文件虽然单个不大,但6个房子一起转也是要等一会儿的,先小步快跑会舒服很多。
3.2 原始文件格式长什么样
转换前先自己读一读原始数据,能帮你理解后续h5里到底是什么东西。用pandas读第一个通道:
import pandas as pd path = "redd_low_freq/house_1/channel_1.data" df = pd.read_csv(path, header=None, delimiter=' ', usecols=[0, 1], names=["unix_ts", "watts"]) print(df.head())REDD低频数据每行是“时间戳 功率 状态”三列,状态列在负荷分解里基本用不上,所以读取时只取前两列。时间戳是Unix时间戳,单位秒,时区默认UTC。NILMTK转换器会把这些时间戳转成带时区的Timestamp,然后用MultiIndex把不同房子的不同通道组织起来。了解这个格式后,你在排查异常数据时就不再是黑盒操作。
3.3 用NILMTK转换器生成redd.h5
核心命令就一行:
from nilmtk.legacy.dataset_converters import convert_redd convert_redd("redd_low_freq", "redd.h5")不同版本的NILMTK导入路径不一样。有的版本要写成:
from nilmtk.dataset_converters.redd import convert_redd如果导入报错,直接去你本地克隆的NILMTK源码目录里找dataset_converters文件夹,看到redd相关脚本,把导入路径改成实际路径即可。这不是你的问题,是工具包版本差异太乱。
转换过程会遍历所有房子和通道,打印每个通道的起始时间、结束时间和样本数。运行时间取决于机器,一般几分钟到十几分钟。转换完成后,redd.h5文件通常比原始数据小一些,读起来却快得多。这一步是整个流程里的关键基础设施,h5文件相当于被NILMTK整理过索引的“数据库”,之后所有set_window、build_meter_group操作都依赖它。
3.4 为什么非要转成h5
很多人一开始不理解为什么要多这一步,直接读csv不就行了?NILMTK之所以要求h5,是因为它需要在读取时快速做时间窗口切片、跨房子对齐、设备重采样,这些操作如果每次直接扫csv会非常慢,而且内存占用大。HDF5格式天生适合这种按区间随机访问的场景,NILMTK内部的数据结构也围绕它设计。你可以把h5理解成一本带目录的字典,而csv是一堆散装卡片。以后跑不同实验,只需要在h5上切不同的时间窗就行,原始csv完全不用再碰。
3.5 验证转换结果
转换成功不代表万事大吉,先打开看看:
from nilmtk import DataSet redd = DataSet("redd.h5") print(redd.store.window) redd.close()如果能看到所有房子的时间范围,说明h5文件基本正常。也可以进一步尝试redd.buildings之类的属性,但我建议第一次只要确认能打开、能看到时间范围就够。不要急着构建MeterGroup,那一步会在训练时做。先保持轻量验证,避免内存被提前占满。
4. 第一个FHMM模型:从训练到分解的完整过程
4.1 切分训练集和测试集
NILM模型评估的核心原则是测试数据不能和训练数据重叠。所以我要把同一栋房子的不同时间段切出来,前面训练,后面测试。这里用房子1做示例,取2011年4月中旬到下旬,前12天训练,后6天测试。
from nilmtk import DataSet from nilmtk.disaggregate import FHMM train_ds = DataSet("redd.h5") train_ds.set_window(start="2011-04-12", end="2011-04-24") train_elec = train_ds.build_meter_group() train_elec.consume_data()这里有个很关键的经验:不要贪心,不要一下子把整栋房子三个月的数据全部读进内存。REDD低频数据单通道不大,但6个房子的几十个通道叠加在一起,内存照样容易爆。短时间窗口能跑通,后面再逐步扩大,千万不要第一步就把自己卡死。
4.2 确认总表和待分解设备
MeterGroup是NILMTK的核心抽象,它会把一个房子的总表和所有子电表组织起来。训练之前,先打印一下结构:
print(train_elec) print("mains:", train_elec.mains()) print("submeters:", train_elec.submeters())NILMTK把总电表叫mains,把单独回路叫submeter。如果你发现submeters是空的,或者mains不存在,说明REDD转换不完整,或者选择的房子本身没有记录完整回路。实际操作中,house_1的数据比较规整,适合第一次跑通。不同房子的通道对应用途如下表,不过具体还是要以metadata为准。
| 通道号范围 | 常见对应设备 | 功率特点 |
|---|---|---|
| 1、2 | 总电表(两相) | 整户总功率 |
| 3 | 冰箱 | 周期波动,峰值约200W |
| 4 | 洗碗机 | 长时间多状态运行 |
| 5、6 | 厨房插座/烤箱 | 功率较高,运行频繁 |
| 7+ | 洗衣机等 | 阶段变化明显 |
训练和测试都要保证目标设备在对应时间段内有数据。如果你发现某台设备一整天功率都是0,说明它根本没被录到,或者设备本身没有运行。
4.3 实例化并训练FHMM模型
FHMM全称是Factorial Hidden Markov Model,因子隐马尔可夫模型。你可以把它理解成多个隐马尔可夫模型(每个设备一个)的叠加:每台设备的开关状态服从一个马尔可夫链,每个状态对应一个功率发射分布,而总表功率观测是所有设备功率的叠加。NILMTK的FHMM实现基于hmmlearn,训练时传入MeterGroup,代码非常简单:
model = FHMM() model.train(train_elec)训练过程会在控制台打印正在处理的设备,以及拟合出的状态数。如果训练没有报错,说明模型已经把各个设备的功率分布参数学出来了。第一次跑的时候你可能觉得“这就完了?”但确实就是这么简单。这个阶段真正花时间的不是训练,而是之前的环境配置和数据转换。
4.4 对测试集做分解
测试集要用训练之外的时间段。注意要新建一个DataSet对象,避免和训练用的train_elec混在一起:
test_ds = DataSet("redd.h5") test_ds.set_window(start="2011-04-24", end="2011-04-30") test_elec = test_ds.build_meter_group() test_elec.consume_data() from nilmtk import HDFDataStore output = HDFDataStore("disagg_out.h5", mode="w") model.disaggregate(test_elec, output, sample_period=60) output.close()这里面的sample_period=60是我强烈建议加上的参数,意思就是让输出按60秒重采样,每分钟输出一个预测点。如果省略,NILMTK会按原始采样间隔输出,数据量瞬间膨胀,画图的时候曲线又多又乱,内存压力也会变大。加上这个参数后,输出文件小,曲线干净,调试体验好很多。
4.5 评估指标:MAE和能耗误差怎么看
分解结果不能光用眼睛看,还得有些量化指标。NILMTK的评估函数在nilmtk.metrics模块,常用的有两个:平均绝对误差MAE和总能耗误差。
from nilmtk.metrics import mean_absolute_error, relative_error_in_total_energy pred_ds = DataSet("disagg_out.h5") pred_elec = pred_ds.build_meter_group() mae = mean_absolute_error(pred_elec, test_elec) print("MAE:", mae)如果版本不同导致函数签名对不上,那就自己算:把预测功率序列和真实功率序列按时间对齐,求绝对差平均。MAE的单位是瓦特,但它不能脱离设备类型看。同样50W的MAE,对微波炉来说是很好的结果,对冰箱来说就已经很糟糕了。所以评估时最好挑1到2个设备单独算,不要只看整体平均。
4.6 画一条预测对比曲线
直观对比预测值和真实值能帮助快速定位问题。画图代码大致如下:
import matplotlib.pyplot as plt # 这里假设你从pred_elec和test_elec中提取到了单设备的Series pred_series = pred_elec.data.resample("1min").mean() gt_series = test_elec.data.resample("1min").mean() plt.figure(figsize=(12, 4)) pred_series.plot(label="pred") gt_series.plot(label="ground truth") plt.legend() plt.ylabel("power (W)") plt.title("FHMM disaggregation on REDD house_1") plt.show()这里有个实操经验:绘图前一定要先统一频率。NILMTK内部不同数据源可能采样间隔不同,直接用原始DataFrame画图会出现时间索引对不齐的锯齿。统一resample到1分钟之后,图形干净很多。先用眼睛看一遍曲线,再去调参数,比纯看指标靠谱得多。
5. 常见报错与排查技巧实录
5.1 import时报No module named nilmtk
这个问题十有八九是NILMTK没装进当前环境,或者装到了别的Python环境。先检查当前环境路径:
which python python -c "import sys; print(sys.prefix)"如果路径不是你创建的nilm环境,说明没激活,重新conda activate nilm。激活后再pip list | grep nilmtk确认包存在。如果存在但还是导入失败,尝试源码安装,并且直接把NILMTK源码目录放在当前工作目录下,因为有些老版本存在相对导入问题,当前目录能看到就能救回来。
5.2 转换REDD时提示找不到convert_redd
这是版本差异的经典问题。常见可用导入路径:
from nilmtk.legacy.dataset_converters import convert_redd from nilmtk.dataset_converters.redd import convert_redd如果都不行,就直接在源码目录里搜索def convert_redd,看它在哪个模块下,改一下from导入就行。这个办法看起来很土,但确实能解决绝大多数老项目安装问题。
5.3 数据集转换时某个channel读取失败
先不要整体重跑,找到报错对应的房子和通道,用pandas单独读一下原始文件。很多时候是因为数据文件本身不完整,或者某个通道在采集时间段内没数据。最直接的方案是跳过出问题的房子,先跑通其他房子。我实操时house_2有过类似问题,直接跳过并不影响学习流程,等到后面做大型对比实验再回来补数据。
5.4 训练过程内存暴涨
这种情况八成是数据全部进了内存。解决办法有三个:第一,缩短时间窗口,比如只取3天;第二,在读取时降采样,MeterGroup的consume_data方法可以传入sample_period,或者在disaggregate时指定sample_period=60;第三,只选择一栋房子,不要全量6套房子一起处理。尤其注意要在set_window之后再build_meter_group,顺序不能反。先切窗再读数据,内存差异非常大。
5.5 分解结果全为0
预测结果全是0时,最常见原因是测试数据的mains没读进去,或者模型训练时根本没看到目标设备。可以先单独打印test_elec的mains和submeters,确认不是空对象。另一个常见原因是设备运行频率太低,模型没有学到“开”状态。这种情况换一个运行次数较多的设备,比如微波炉或电热水壶,会比冰箱这种低功率周期设备更容易出效果。
5.6 set_window之后数据为空,但不报错
这个问题最阴险,因为程序不报错,只是结果全空。REDD原始Unix时间戳是UTC,NILMTK转换时可能根据metadata转成了某个本地时区。set_window里如果不带时区,可能匹配不到数据。解决办法是显式加上时区偏移,比如:
train_ds.set_window(start="2011-04-12 00:00:00+00:00", end="2011-04-24 00:00:00+00:00")这种问题在排查时很容易被忽略,但你只要打印一下DataSet的时间范围,对比一下你要切的时间窗,就能发现端倪。
5.7 汇总速查表
| 现象 | 最可能原因 | 快速处理 |
|---|---|---|
| 导入nilmtk失败 | 环境不对或依赖版本冲突 | 检查python路径,按固定版本重装 |
| convert_redd找不到 | 版本路径差异 | 源码里搜函数实际位置 |
| h5读取不到指定时间窗 | 时区或窗口格式问题 | 加+00:00时区后缀 |
| 训练时内存爆 | 数据窗口太大 | 缩短窗口,先切窗再读取 |
| 预测结果全0 | mains缺失或设备运行太少 | 检查MeterGroup,换高频运行设备 |
6. 后续怎么继续往下走
跑通这个流程之后,建议你再做三件事:第一,把house_2到house_6也转成h5,在不同房屋上验证同一套模型,看模型泛化能力如何;第二,换不同长度的时间窗,观察训练数据量对分解精度的影响;第三,尝试NILMTK里的CO模型,和FHMM做一个对比。这些动作能帮你在走向深度学习之前,先建立起对NILM问题的直觉。
就我自己的经历来说,NILMTK的价值不在于算法多新,而在于它把数据集、电表、分解算法、评估指标这些抽象揉成了一个框架。后来我转向seq2point这类深度NILM模型时,很多数据预处理和坏数据识别的方法,都是从折腾REDD和NILMTK的过程中积累下来的。如果你打算把NILM当长期方向,这个入门流程绝对不能跳过。
最后再分享一个小技巧:redd.h5转换成功后一定要留一份备份。之后跑实验只需要在h5文件上反复set_window,根本不需要重新解压原始数据。我见过不少人每次换实验都删了h5重新转换,纯属浪费时间。把环境版本记好,把h5文件保管好,你的第一个负荷分解模型5分钟跑通是完全可以实现的。