转自AI Studio,原文链接:​​​​​​香港置地2022年置慧杯:商业综合体能耗预测基线 - 飞桨AI Studio

1. 大赛介绍

1.1 大赛背景

  • 香港置地积极响应国家“碳达峰碳中和”双碳战略,致力于提高建筑的能耗效率,以降低成本并减少排放。降本减排需要一条最优的能耗估值,问题在于如何获得最优的能耗估值?

  • 常规做法是由能耗顾问基于专业经验与历史数据进行估计,但这种做法强依赖于顾问的专业能力与公司的历史数据,无法实现标准化。

  • 本次比赛诚邀社会各界开发者、商业体能耗专家从业者、企业研发人员和教育科研机构人员等以个人或团队形式报名参赛,希望各位参赛选手基于主办方提供的真实业务场景数据,完成模型的开发与优化,支持标准化能耗数据预测工作的开展,助力香港置地优化商业运营能耗。

1.2 大赛信息

  • 比赛页面:香港置地2022年置慧杯:商业综合体能耗预测

  • 比赛赛程:

    时间赛程
    2022/4/1 00:00飞桨平台开放报名通道,开放答疑通道
    2022/4/18 10:00开放数据下载通道
    2022/4/25 10:00开放A榜测评通道,评测4.1日-4.7日的预测数据
    2022/5/7 18:00关闭A榜测评通道,开启入围赛评测通道
    2022/5/11 18:00关闭入围赛评测通道
    2022/5/16 18:00基于入围赛榜单与代码审查结果,决出TOP10队伍
    2022/5/20 18:00线上答辩,决出TOP5队伍
    2022/5/25 09:30线下答辩,决出最终排名
  • 奖金设置:

    名称数量奖金
    一等奖15万元人民币/队+荣誉证书与奖杯
    二等奖12万元人民币/队+荣誉证书与奖杯
    三等奖11万元人民币/队+荣誉证书与奖杯
    鼓励奖25千元人民币/队+荣誉证书与奖杯
  • 参赛要求:

    • 本次竞赛面向全社会开放,不限年龄、身份、国籍,相关领域的个人、高等院校、科研机构、企业单位、初创团队等均可报名参赛;

    • 大赛主办单位中有机会提前接触赛题和数据的人员不得参加比赛,其他员工可以参与比赛排名,但不可领取任何奖项。

    • 支持以个人或团队形式参赛,允许跨单位自由组队,但每人只能参加一支队伍;

    • 参赛选手报名须保证所提供的个人信息真实、准确、有效。

2. 数据内容

2.1 数据介绍

  • 2021 年 8 月 1 日 - 2022 年 3 月 31 日的商场能耗数据

  • 商业体基础信息基础资料;

  • 时间数据:

    • 大面积改造时间
    • 特殊时间段(疫情等)
    • 法定节假日(包括不限于商场运营调整)
  • 客流量;

  • 室外气象站数据以及室内环境监测系统数据;

  • 其他源头数据的使用:在比赛过程中,选手也可以使用一些免费公开的数据,比如天气信息等,如对公开数据有疑问可咨询主办方。

2.2 数据集文件目录

  • Datasets.xlsx:原始的完整数据文件

  • 能耗汇总结构.xlsx:Datasets.xlsx 中能耗的结构关系

  • 数据上传格式20220423.csv:提交文件示例

2.3 数据详解

  • Datasets.xlsx 表格中包含非常多种的数据,其中最重要的是如下三张表格,它们收录了具体的能耗数据,而其他表格则是一些其他来源的数据(比如:天气 / 人流量等等),可供辅助使用:

    • 1.1 t_meter_info:传感器相关信息

    • 1.3 t_research_energyitem:区域相关信息

    • 1.4 data_servicedata_1d:能耗相关信息

  • 这三张表格的逻辑关系如下:

  • 换用人话来讲:

    • 首先 1.4 data_servicedata_1d 这个表格的每一行记录了每个传感器每天的能耗值

    • 接下来通过任意一行能耗值中对应的传感器 id(即 meter)可以在 1.1 t_meter_info 这个表格中找到这个传感器安装的具体位置区域 id(即 energy_item_id)

    • 然后可以通过 1.3 t_research_energyitem 这个表格查询具体位置区域 id (对应其中的 c_logic_id)的父节点 id(即 c_parent)

    • 当然父节点也有自己的父节点,不断嵌套,类似下面这样:

      c_logic_idc_namec_parent
      EI1001总能耗-1
      EI101001光环中心A座EI1001
      EI101030102001办公公区(办公)EI101001
      EI101030103001暖通空调(办公)EI101030102001
      EI101030303001动力设备(办公)EI101030102001

    • 通过“能耗 -> 传感器 id -> 区域 id -> 父节点区域 id -> ... -> 根节点 id(-1)”这样的逻辑链条,就可以把每个传感器每天的能耗值汇总到每个具体的区域中

    • 各个区域的能耗计算逻辑如下:

      • 总能耗 = 光环A座 + 光环B座 + 光环购物广场 + 超市
      • 光环A座 = 光环A座(公区各分项+租区分项)
      • 光环B座 = 光环B座(公区各分项+租区分项)
      • 光环购物广场 = 光环A座(公区各分项+租区分项)
    • 公共用电和商户用电计算逻辑如下(根据 1.3 t_research_energyitem 这个表格中的 c_order 值对应的区域):

      • 公共用电 = (c_order=2) + (c_order=31) + (c_order=72)
      • 商户用电 = (c_order=29) + (c_order=58) + (c_order=60) + (c_order=124)

3. 目标任务

3.1 任务说明

  • 给定(2021.8.1-2022.3.31)的商场能耗数据,建立精确的能耗预测模型

  • 预测未来一段时间(2022.4.1-2022.4.7 / 2022.4.18-2022.4.24)的商场能耗数据

  • 具体的预测内容分两项:公共用电和商户用电,具体的计算逻辑参考上一节的内容

3.2 提交内容

  • A 榜的提交文件格式为 csv,具体内容如下:

    c_order,c_logic_id,c_name,c_parent,2022/4/1,2022/4/2,2022/4/3,2022/4/4,2022/4/5,2022/4/6,2022/4/7
    998,998,公共用电,998,0,0,0,0,0,0,0
    999,999,商户用电,999,0,0,0,0,0,0,0
    

3.3 评审规则

  • 评估指标:

    • 本次比赛的能耗预测结果的评估指标采用平均百分比误差(MAPE):

  • 评分标准:

    • ECS = (公共用电 MAPE + 商户用电 MAPE) / 2

    • ECS 能耗模型对应得分MS(百分制):

4. 基线算法

  • 本基线项目中使用到的模型和数据较为简单

  • 能耗预测本质上属于时间序列预测问题

  • 在模型层面上,使用了一个非常经典的循环神经网络模型 LSTM(Long Short-Term Memory),这个模型可以很好的处理时间序列问题

  • 在数据层面上,只使用了最基础的能耗数据,即使用前几天各个传感器的历史能耗数据来预测未来一天各个传感器的能耗数据

5. 代码实现

5.1 构建数据结构

  • 为了方便的读取和计算能耗数据,构建一个简单的节点数据结构来储存每个区域的数据

In [1]

import json
import numpy as np
from datetime import date

class Node:
    def __init__(self, _id, parent_id, dates, mode, name=None, order=None):
        '''
            创建节点

            参数:
                _id: 节点 id
                parent_id:父节点id
                dates:日期列表
                mode:'node' / 'meter'
                name:节点名称
                order:节点序号
        '''
        self._id = _id
        self.parent_id = parent_id
        self.name = name
        self.order = order
        self.dates = dates
        self.mode = mode
        self.values = [0] * len(dates)
        self.parent = None
        self.children = []

    def add_child(self, node):
        '''
        添加子节点
        
        参数:
            node:子节点
        '''
        if node not in self.children:
            self.children.append(node)
            node.parent = self

    def get_values(self):
        '''
        获取节点能耗数据

        返回:
           节点能耗数据 
        '''
        sum_values = [self.values]
        for child in self.children:
            sum_values.append(child.get_values())
        return np.asarray(sum_values).sum(0).tolist()
    
    def set_value(self, date, value):
        '''
        设置指定日期的 meter 节点能耗数据

        参数:
            date:日期
            value:能耗数据
        '''
        assert self.mode=='meter'
        self.values[self.dates.index(date)] += value

    def set_values(self, values):
        '''
        设置 meter 节点能耗数据
        
        参数:
            values:能耗数据
        '''
        assert self.mode=='meter'
        assert len(values) == len(self.dates)
        self.values = values
    
    def reset_dates(self, dates):
        '''
        重设日期

        参数:
            dates:日期
        '''
        self.dates = dates
        self.values = [0] * len(dates)
        for child in self.children:
            child.reset_dates(dates)

    def meters(self):
        '''
        获取所有 meter

        返回:
            所有 meter
        '''
        if self.mode == 'meter':
            return [self]
        else: 
            meters = []
            for child in self.children:
                meters += child.meters()
            return meters

    def nodes(self):
        '''
        获取所有 node

        返回:
            所有 node
        '''
        if self.mode == 'meter':
            return []
        else: 
            nodes = [self]
            for child in self.children:
                nodes += child.nodes()
            return nodes
    
    def __repr__(self):
        '''
        获取节点信息

        返回:
            节点信息
        '''
        if self.mode == 'meter':
            return json.dumps({
                'mode': 'meter',
                'meter_id': self._id,
                'logic_id': self.parent_id,
                'values': self.values,
            }, indent=4, ensure_ascii=False)
        else:
            return json.dumps({
                'mode': 'node',
                'logic_id': self._id,
                'parent_id': self.parent_id,
                'name': self.name,
                'order': self.order,
                'values': self.get_values(),
                'children': {child._id: json.loads(child.__repr__()) for child in self.children}
            }, indent=4, ensure_ascii=False)

    def dump(self, json_file):
        '''
        保存节点

        参数:
            json_file:json 文件
        '''
        with open(json_file, 'w', encoding='UTF-8') as f:
            f.write(self.__repr__()) 

    @staticmethod
    def load(json_file, dates):
        '''
        加载节点

        参数:
            json_file:json 文件
            dates:日期

        返回:
            节点
        '''
        with open(json_file, 'r', encoding='UTF-8') as f:
            node_info = json.load(f)
            return Node.create_node(node_info, dates)

    @staticmethod
    def create_node(node_info, dates):
        '''
        创建节点

        参数:
            node_info:节点信息
            dates:日期

        返回:
            节点
        '''
        if node_info['mode'] == 'node':
            node = Node(node_info['logic_id'], node_info['parent_id'], dates, 'node', node_info['name'], node_info['order'])
            for child_info in node_info['children'].values():
                node.add_child(Node.create_node(child_info, dates))
        else:
            node = Node(node_info['meter_id'], node_info['logic_id'], dates, 'meter')
            node.values = node_info['values']
        return node

5.2 读取数据

  • 因为原始文件很大,处理起来比较耗时,所有这里直接加载预先处理完成的数据

  • 使用如下代码重新生成数据文件,详细的处理流程可以参考该脚本文件

    $ python gen_root.py
    

In [2]

# 根节点数据
data_json = 'root.json'

# 生成日期序列
dates = [
    str(date.fromordinal(i).strftime("%Y%m%d000000"))
    for i in range(date(2021, 8, 1).toordinal(), date(2022, 3, 31).toordinal() + 1)
]

# 加载根节点数据
root = Node.load(data_json, dates)

# 打印节点信息
print({
   'name': root.name, # 节点名称
   'order': root.order, # 节点序号
   'id': root._id, # 节点 id
   'parent_id': root.parent_id, # 父节点 id
   'children_ids': [child._id for child in root.children], # 子节点 id
})
{'name': '总能耗', 'order': 0, 'id': 'EI1001', 'parent_id': '-1', 'children_ids': ['EI5201314083', 'EI5201314084', 'EI5201314085', 'EI101001']}

5.3 构建数据集

In [3]

# 获取所有传感器
meters = root.meters()

# 获取所有传感器能耗数据
meters_values = []
for meter in meters:
    meters_values.append(meter.get_values())
meters_values = np.asarray(meters_values).transpose(1, 0)

# 切分数据集
split_num = 21 # 验证集大小
train_dataset = meters_values[0: -split_num, :]
val_dataset = meters_values[-split_num: , :]
train_num = train_dataset.shape[0]
val_num = val_dataset.shape[0]

# 数据预处理
max_value = train_dataset.max()
train_dataset /= max_value
val_dataset /= max_value

5.4 模型构建和配置

In [ ]

import paddle
import paddle.nn as nn
from paddle.optimizer import Adam

# 构建 LSTM 模型
net = nn.LSTM(626, 626, num_layers=1, direction='forward')

# 配置 Adam 优化器
opt = Adam(parameters=net.parameters(), learning_rate=0.0001)

# 配置训练参数
batch_size = 256 # 批处理大小
steps = 20000 # 迭代次数
days = 7 # 时间窗口
log_iter = 20 # log 步幅
keep_iter = 999999999 # 停止迭代步幅
min_loss = 9999999999 # 最小 loss
best_step = 0 # 最佳迭代次数
submit_csv = 'submit.csv' # 提交文件
best_model = 'best_model.pdparams' # 最佳模型文件

5.5 模型训练和评估

In [5]

for step in range(steps):
    # 数据采样
    input_datas = []
    label_datas = []
    for _ in range(batch_size):
        start = np.random.randint(0, train_num - days - 1)
        end = start + days
        input_datas.append(train_dataset[start: end, :])
        label_datas.append(train_dataset[end, :])
    input_datas = paddle.to_tensor(input_datas, dtype='float32')
    label_datas = paddle.to_tensor(label_datas, dtype='float32')

    # 前向计算
    h = net(input_datas)[1][0][-1]

    # 计算损失
    loss = nn.functional.mse_loss(h, label_datas)

    # 反向传播
    loss.backward()

    # 优化模型
    opt.step()

    # 清除梯度
    opt.clear_grad()

    # 模型评估
    if step % log_iter == 0:
        net.eval()
        with paddle.no_grad():
            input_datas = paddle.to_tensor([train_dataset[-days:,: ]], dtype='float32') 
            predicts = []
            for _ in range(val_num):
                h = net(input_datas)[1][0][-1].clip(0)
                predicts.append(h)
                input_datas = paddle.concat([input_datas[:, 1:, :], h[None,...]], 1)
            predicts = paddle.concat(predicts, 0)
            label_datas = paddle.to_tensor(val_dataset, dtype='float32')
            loss = nn.functional.mse_loss(predicts * max_value, label_datas * max_value).item()

            # 模型保存
            if loss < min_loss:
                min_loss = loss
                best_step = step
                paddle.save(net.state_dict(), best_model)
                print('saving best model, loss: %f step: %d' % (loss, step))
        net.train()
    
    if (step - best_step) > keep_iter:
        break
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/tensor/creation.py:130: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. 
Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
  if data.dtype == np.object:
saving best model, loss: 276994.156250 step: 0
saving best model, loss: 230093.750000 step: 20
saving best model, loss: 193405.031250 step: 40
saving best model, loss: 157615.359375 step: 60
saving best model, loss: 115932.000000 step: 80
saving best model, loss: 82303.750000 step: 100
saving best model, loss: 67326.789062 step: 120
saving best model, loss: 58744.937500 step: 140
saving best model, loss: 49890.906250 step: 160
saving best model, loss: 44489.367188 step: 180
saving best model, loss: 41577.460938 step: 200
saving best model, loss: 39550.421875 step: 220
saving best model, loss: 36614.867188 step: 240
saving best model, loss: 35523.441406 step: 260
saving best model, loss: 35439.527344 step: 280
saving best model, loss: 35354.671875 step: 300
saving best model, loss: 34999.957031 step: 320
saving best model, loss: 34962.304688 step: 1380
saving best model, loss: 34935.687500 step: 2380
saving best model, loss: 34919.925781 step: 2420
saving best model, loss: 34893.078125 step: 6960
saving best model, loss: 34886.433594 step: 11300
saving best model, loss: 34796.238281 step: 11460
saving best model, loss: 34639.136719 step: 12000
saving best model, loss: 34360.699219 step: 15960
saving best model, loss: 34348.332031 step: 16180
saving best model, loss: 34245.355469 step: 17780

5.6 模型加载和预测

In [6]

net.set_state_dict(paddle.load(best_model))
net.eval()
with paddle.no_grad():
    input_datas = paddle.to_tensor([val_dataset[-days:,: ]], dtype='float32') 
    predicts = []
    for _ in range(7):
        h = net(input_datas)[1][0][-1].clip(0)
        predicts.append(h)
        input_datas = paddle.concat([input_datas[:, 1:, :], h[None,...]], 1)
    predicts = paddle.concat(predicts, 0)
    predicts = (predicts * max_value).numpy().transpose(1, 0)
net.train()

5.7 数据重构

  • 将根节点的日期重设并清除所有数据

  • 将传感器的预测能耗数据重新添加回各个传感器节点

  • 就能够通过根节点来计算所需要的各个区域的预测能耗数据

In [7]

dates = [
    str(date.fromordinal(i).strftime("%Y%m%d000000"))
    for i in range(date(2022, 4, 1).toordinal(), date(2022, 4, 7).toordinal() + 1)
]
root.reset_dates(dates)
meters = root.meters()
for (meter, predict) in zip(meters, predicts):
    meter.set_values(predict.tolist())

5.8 导出预测数据

In [8]

publics = []
business = []

for node in root.nodes():
    if node.order in [2, 31, 72]:
        publics.append(node.get_values())
    elif node.order in [29, 58, 60, 124]:
        business.append(node.get_values())

publics = np.asarray(publics).sum(0).tolist()
business = np.asarray(business).sum(0).tolist()

with open(submit_csv, 'w', encoding='UTF-8') as f:
    f.write('c_order,c_logic_id,c_name,c_parent,2022/4/1,2022/4/2,2022/4/3,2022/4/4,2022/4/5,2022/4/6,2022/4/7\n')
    f.write('998,998,公共用电,998,'+','.join([str(_) for _ in publics])+'\n')
    f.write('999,999,商户用电,999,'+','.join([str(_) for _ in business])+'\n')

6. 提交答案

  • 前往 比赛页面 的提交结果选项卡进行答案提交

  • 上传完结果文件即可在下方看到你的得分

  • 基线的得分为:0 / 10.84%(高情商:差一点就 75 分),由于训练的随机性,分数会有变动,优化空间极大

  • 基于此基线稍微优化一下即可达到:84 / 6.57%

  • 当然这里就只提供了初始版本的代码和模型参数文件

  • 使用如下代码复现上述结果(0 / 10.84%):

In [9]

net.set_state_dict(paddle.load('baseline.pdparams'))
net.eval()
with paddle.no_grad():
    input_datas = paddle.to_tensor([val_dataset[-days:,: ]], dtype='float32') 
    predicts = []
    for _ in range(7):
        h = net(input_datas)[1][0][-1].clip(0)
        predicts.append(h)
        input_datas = paddle.concat([input_datas[:, 1:, :], h[None,...]], 1)
    predicts = paddle.concat(predicts, 0)
    predicts = (predicts * max_value).numpy().transpose(1, 0)
net.train()
dates = [
    str(date.fromordinal(i).strftime("%Y%m%d000000"))
    for i in range(date(2022, 4, 1).toordinal(), date(2022, 4, 7).toordinal() + 1)
]
root.reset_dates(dates)
meters = root.meters()
for (meter, predict) in zip(meters, predicts):
    meter.set_values(predict.tolist())
publics = []
business = []

for node in root.nodes():
    if node.order in [2, 31, 72]:
        publics.append(node.get_values())
    elif node.order in [29, 58, 60, 124]:
        business.append(node.get_values())

publics = np.asarray(publics).sum(0).tolist()
business = np.asarray(business).sum(0).tolist()

with open('baseline.csv', 'w', encoding='UTF-8') as f:
    f.write('c_order,c_logic_id,c_name,c_parent,2022/4/1,2022/4/2,2022/4/3,2022/4/4,2022/4/5,2022/4/6,2022/4/7\n')
    f.write('998,998,公共用电,998,'+','.join([str(_) for _ in publics])+'\n')
    f.write('999,999,商户用电,999,'+','.join([str(_) for _ in business])+'\n')

7. 优化指南

  • 模型调参:更换时间窗口大小、调整学习率等
  • 更换模型:更换模型为 GRU、Transformer 等
  • 训练配置:更换损失函数、优化器等
  • 更多数据:加入天气、人流量信息等

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐