├── .gitmodules ├── README.md ├── dataset_convert.sh ├── utils.py ├── sync_disaggregate.py ├── parallelsource.py ├── disaggregator.py ├── LICENSE ├── metrics.py ├── dae_kelly_URED_.py ├── s2p_zhang_URED_.py ├── dfcn_brewitt_URED_.py ├── s2s_zhang_URED_.py ├── glu_chen_URED_.py └── sgnsp_shin_URED_.py /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "neuralnilm"] 2 | path = neuralnilm 3 | url = git@github.com:KaibinBao/neuralnilm.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neuralnilm-pytorch 2 | Non-Intrusive Appliance Load Monitoring (NILM) based on Convolutional Neural Networks for PyTorch. 3 | 4 | This project contains the final code I used for the NILM experiments in my PhD (in progress). 5 | I will upload more model architectures, trained model parameters and a notebook demonstrating disaggregation in the future. 6 | Stay tuned :) 7 | -------------------------------------------------------------------------------- /dataset_convert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /srv/envs/kai/miniconda3/bin/activate 4 | cd /srv/experiments/neuralnilm-pytorch 5 | 6 | EXP=dataset_final_URED_ 7 | #EXP=dataset_final__R___ 8 | 9 | IFS=";" 10 | 11 | APPLIANCES=(\ 12 | "dish washer" \ 13 | "washing machine"\ 14 | "tumble dryer"\ 15 | "kettle" \ 16 | "microwave" \ 17 | "television" \ 18 | "fridge" \ 19 | ) 20 | 21 | for APPLIANCE in ${APPLIANCES[@]}; do 22 | python ${EXP}.py dataset.convert with dataset.TARGET_APPLIANCE="${APPLIANCE}" $@ 23 | done 24 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | 3 | from copy import copy 4 | import numpy as np 5 | import torch 6 | import time 7 | import tensorboardX 8 | 9 | def SW_add_scalars2(self, main_tag, tag_scalar_dict, global_step=None): 10 | """Adds many scalar data to summary. 11 | Args: 12 | tag (string): Data identifier 13 | main_tag (string): The parent name for the tags 14 | tag_scalar_dict (dict): Key-value pair storing the tag and corresponding values 15 | global_step (int): Global step value to record 16 | Examples:: 17 | writer.add_scalars('run_14h',{'xsinx':i*np.sin(i/r), 18 | 'xcosx':i*np.cos(i/r), 19 | 'arctanx': numsteps*np.arctan(i/r)}, i) 20 | # This function adds three values to the same scalar plot with the tag 21 | # 'run_14h' in TensorBoard's scalar section. 22 | """ 23 | timestamp = time.time() 24 | fw_logdir = self.file_writer.get_logdir() 25 | for tag, scalar_value in tag_scalar_dict.items(): 26 | fw_tag = fw_logdir + "/" + tag 27 | #fw_tag_full = fw_logdir + "/" + main_tag + "/" + tag 28 | if fw_tag in self.all_writers.keys(): 29 | fw = self.all_writers[fw_tag] 30 | else: 31 | fw = tensorboardX.FileWriter(logdir=fw_tag) 32 | self.all_writers[fw_tag] = fw 33 | fw.add_summary(tensorboardX.summary.scalar(main_tag, scalar_value), global_step) 34 | #self.__append_to_scalar_dict(fw_tag_full, scalar_value, global_step, timestamp) 35 | -------------------------------------------------------------------------------- /sync_disaggregate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import re 5 | import sys 6 | import jsonpickle 7 | from os.path import join 8 | 9 | BASE_PATH = "trained_models" 10 | RESULT_BASE_PATH = "results" 11 | TEST_DATA_PATH = "input/evaluation_data_UR__K" 12 | GAN_EXP_PATTERN = re.compile("([a-zA-Z])+_gan.*") 13 | 14 | if len(sys.argv) > 1: 15 | EXP_PATTERN = re.compile(sys.argv[1]) 16 | else: 17 | EXP_PATTERN = None 18 | 19 | def check_disaggregate(experiment_name, resolution, appliance, experiment_id, EXPERIMENT_PATH): 20 | try: 21 | with open(join(EXPERIMENT_PATH, 'run.json'), 'r') as file: 22 | run = jsonpickle.decode(file.read()) 23 | except: 24 | return 25 | 26 | if run['status'] != 'COMPLETED': 27 | return 28 | 29 | RESULT_FILE = join(RESULT_BASE_PATH, experiment_name, resolution, experiment_id, "{}.json".format(appliance)) 30 | if os.path.exists(RESULT_FILE): 31 | return 32 | else: 33 | if EXP_PATTERN is not None: 34 | if EXP_PATTERN.match(experiment_name) is None: 35 | return 36 | if GAN_EXP_PATTERN.match(experiment_name) is not None: 37 | disagg_command = "python3 gan_g_verifier.py {exp} {appl} --id {id} --modeldir '{exp_base}/{exp}/{res}' --data '{testdir}' --resultdir '{res_base}/{exp}/{res}' --nosequences".format( 38 | exp_base=BASE_PATH, exp=experiment_name, appl=appliance, res=resolution, testdir=TEST_DATA_PATH, res_base=RESULT_BASE_PATH, id=experiment_id) 39 | print("would run '{}'...".format(disagg_command)) 40 | return 41 | os.makedirs(join(RESULT_BASE_PATH, experiment_name, resolution, experiment_id), exist_ok=True) 42 | disagg_command = "python3 disaggregator.py {exp} {appl} --id {id} --modeldir '{exp_base}/{exp}/{res}' --data '{testdir}' --resultdir '{res_base}/{exp}/{res}' --nosequences".format( 43 | exp_base=BASE_PATH, exp=experiment_name, appl=appliance, res=resolution, testdir=TEST_DATA_PATH, res_base=RESULT_BASE_PATH, id=experiment_id) 44 | print("running '{}'...".format(disagg_command)) 45 | #print("would run '{}'...".format(disagg_command)) 46 | #return 47 | if os.system(disagg_command) != 0: 48 | sys.exit(1) 49 | 50 | 51 | # traverse trained models directory 52 | for experiment_name in sorted(os.listdir(BASE_PATH)): 53 | EXPERIMENT_BASE_PATH = join(BASE_PATH, experiment_name) 54 | if not os.path.isdir(EXPERIMENT_BASE_PATH): 55 | continue 56 | for resolution in sorted(os.listdir(EXPERIMENT_BASE_PATH)): 57 | RESOLUTION_BASE_PATH = join(EXPERIMENT_BASE_PATH, resolution) 58 | if not os.path.isdir(RESOLUTION_BASE_PATH): 59 | continue 60 | for appliance in sorted(os.listdir(RESOLUTION_BASE_PATH)): 61 | APPLIANCE_BASE_PATH = join(RESOLUTION_BASE_PATH, appliance) 62 | if not os.path.isdir(APPLIANCE_BASE_PATH): 63 | continue 64 | for experiment_id in sorted(os.listdir(APPLIANCE_BASE_PATH)): 65 | EXPERIMENT_PATH = join(APPLIANCE_BASE_PATH, experiment_id) 66 | if not os.path.isdir(EXPERIMENT_PATH): 67 | continue 68 | check_disaggregate(experiment_name, resolution, appliance, experiment_id, EXPERIMENT_PATH) 69 | -------------------------------------------------------------------------------- /parallelsource.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import numpy as np 4 | #import torch 5 | #from torch.multiprocessing import Process, Event, Queue 6 | from multiprocessing import Process, Event, Queue 7 | 8 | from neuralnilm.data.source import Source, Sequence 9 | 10 | import logging 11 | logger = logging.getLogger(__name__) 12 | 13 | #torch.multiprocessing.set_sharing_strategy('file_system') 14 | 15 | #class TorchSequence(): 16 | # def __init__(self, seq): 17 | # self.__converted = False 18 | # self.input = torch.from_numpy(seq.input).share_memory_() 19 | # self.target = torch.from_numpy(seq.target).share_memory_() 20 | # self.all_appliances = None if seq.all_appliances.empty else seq.all_appliances 21 | # self.metadata = seq.metadata 22 | # self.weights = seq.weights 23 | # 24 | # def to_sequence(self): 25 | # if self.__converted is False: 26 | # self.input = self.input.numpy() 27 | # self.target = self.target.numpy() 28 | # if self.all_appliances is None: 29 | # self.all_appliances = Sequence.empty_df 30 | # self.__converted = True 31 | # return self 32 | 33 | class TrainingSequence(): 34 | def __init__(self, seq): 35 | self.input = seq.input 36 | self.target = seq.target 37 | self.weights = seq.weights 38 | 39 | def to_sequence(self): 40 | self.all_appliances = Sequence.empty_df 41 | self.metadata = {} 42 | return self 43 | 44 | class MultiprocessActivationsSource(): 45 | def __init__(self, source, num_processes=4, num_seq_per_batch=64, master_seed=42, **get_sequence_kwargs): 46 | self._stop = Event() 47 | self._is_started = False 48 | self._queue = Queue(maxsize=32) 49 | self._source = source 50 | self._get_sequence_kwargs = get_sequence_kwargs 51 | self._master_seed = master_seed 52 | self._num_processes = num_processes 53 | self._num_seq_per_batch = num_seq_per_batch 54 | self._processes = [] 55 | 56 | @property 57 | def num_batches_for_validation(self): 58 | return self._source.num_batches_for_validation 59 | 60 | def run(self, rng_seed): 61 | self._source.rng_seed = rng_seed 62 | self._source.rng = np.random.RandomState(rng_seed) 63 | 64 | def compile_batch(): 65 | batch = [] 66 | for i in range(self._num_seq_per_batch): 67 | seq = self._source._get_sequence(fold='train', **self._get_sequence_kwargs) 68 | batch.append(TrainingSequence(seq)) 69 | return batch 70 | 71 | batch = compile_batch() 72 | while not self._stop.is_set(): 73 | try: 74 | self._queue.put(batch) 75 | except AssertionError: 76 | # queue is closed 77 | break 78 | batch = compile_batch() 79 | 80 | def get_batch(self, *args, **kwargs): 81 | return Source.get_batch(self, *args, **kwargs) 82 | 83 | def get_sequence(self, fold='train', timeout=30, **get_sequence_kwargs): 84 | if fold == 'train': 85 | if self._is_started: 86 | while True: 87 | batch = self._queue.get(timeout=timeout) 88 | for seq in batch: 89 | yield seq.to_sequence() 90 | else: 91 | raise RuntimeError("Process is not running!") 92 | else: 93 | return self._source.get_sequence(fold=fold, **get_sequence_kwargs) 94 | 95 | def start(self): 96 | if self._is_started == False: 97 | rng = np.random.RandomState(self._master_seed) 98 | MAX_SEED = 2**32-1 99 | self._is_started = True 100 | for i in range(self._num_processes): 101 | seed = rng.randint(MAX_SEED) 102 | p = Process(target=self.run, args=(seed,), name='neuralnilm-source-process-{}'.format(i)) 103 | self._processes.append(p) 104 | p.start() 105 | 106 | def stop(self): 107 | self._stop.set() 108 | self._queue.close() 109 | for p in self._processes: 110 | p.terminate() 111 | for p in self._processes: 112 | p.join() 113 | 114 | def report(self): 115 | return self._source.report() -------------------------------------------------------------------------------- /disaggregator.py: -------------------------------------------------------------------------------- 1 | import os, re 2 | import numpy as np 3 | import pandas as pd 4 | 5 | from metrics import MetricsAccumulator 6 | 7 | class Disaggregator(): 8 | def __init__(self, 9 | EVALUATION_DATA_PATH, 10 | TARGET_APPLIANCE, 11 | ON_POWER_THRESHOLD, 12 | MAX_TARGET_POWER, 13 | disagg_func, 14 | disagg_kwargs, 15 | remove_vampire_power=False, 16 | pad_mains=True, 17 | pad_appliance=False, 18 | downsample_factor=1): 19 | self.EVALUATION_DATA_PATH = EVALUATION_DATA_PATH 20 | self.BUILDINGS = [] 21 | self.TARGET_APPLIANCE = TARGET_APPLIANCE 22 | if TARGET_APPLIANCE == "dish washer": 23 | self.ON_POWER_THRESHOLD = 15 24 | else: 25 | self.ON_POWER_THRESHOLD = ON_POWER_THRESHOLD 26 | self.MAX_TARGET_POWER = MAX_TARGET_POWER 27 | self.PAD_WIDTH = 1536 28 | self.pad_mains = pad_mains 29 | self.pad_appliance = pad_appliance 30 | self.disagg_func = disagg_func 31 | self.disagg_kwargs = disagg_kwargs 32 | self.downsample_factor = downsample_factor 33 | self.metrics = MetricsAccumulator(self.ON_POWER_THRESHOLD, 4200) 34 | self._init_data(remove_vampire_power) 35 | 36 | def _init_data(self, remove_vampire_power): 37 | re_building_filename = re.compile("^((.*)_(.*))\\.pkl$") 38 | self.mains = {} 39 | self.appliance_y_true = {} 40 | for filename in os.listdir(self.EVALUATION_DATA_PATH): 41 | re_match = re_building_filename.match(filename) 42 | if re_match: 43 | building_i = re_match.group(1) 44 | mains, y_true = ( 45 | self._load_data(filename, 46 | remove_vampire_power, 47 | pad_mains=self.pad_mains, 48 | pad_appliance=self.pad_appliance)) 49 | if mains is None: 50 | continue 51 | self.BUILDINGS.append(building_i) 52 | self.mains[building_i] = mains 53 | self.appliance_y_true[building_i] = y_true 54 | 55 | 56 | def _load_data(self, filename, remove_vampire_power, pad_mains=True, pad_appliance=False): 57 | # Load mains 58 | filename = os.path.join(self.EVALUATION_DATA_PATH, filename) 59 | df = pd.read_pickle(filename) 60 | 61 | if not self.TARGET_APPLIANCE in df: 62 | return None, None 63 | 64 | mains = df['mains'].values 65 | if remove_vampire_power: 66 | vampire_power = df['mains'].quantile(0.0002) 67 | mains = np.clip(mains-vampire_power, 0, None) 68 | if self.downsample_factor > 1: 69 | mains = self._resample_mains(mains) 70 | 71 | # Pad 72 | if pad_mains: 73 | mains = np.pad( 74 | mains, pad_width=(self.PAD_WIDTH, self.PAD_WIDTH), mode='constant') 75 | 76 | y_true = df[self.TARGET_APPLIANCE].values 77 | 78 | if pad_appliance: 79 | y_true = np.pad( 80 | y_true, pad_width=(self.PAD_WIDTH, self.PAD_WIDTH), mode='constant') 81 | 82 | return mains, y_true 83 | 84 | 85 | def _resample_mains(self, mains): 86 | mains_length_odd = len(mains) 87 | downsample_factor = self.downsample_factor 88 | n_samples_new = int(np.ceil(mains_length_odd/downsample_factor)) 89 | mains_length_even = n_samples_new*downsample_factor 90 | mains_resampled = np.pad(mains, pad_width=(0, mains_length_even-mains_length_odd), mode='constant') 91 | mains_resampled = mains_resampled.reshape((n_samples_new, downsample_factor)) 92 | mains_resampled[:, :] = mains_resampled.mean(axis=1)[:, np.newaxis] 93 | return mains_resampled.reshape((-1))[:mains_length_odd] 94 | 95 | def get_mains(self, building_i): 96 | if pad_mains: 97 | return self.mains[building_i][self.PAD_WIDTH:-self.PAD_WIDTH] 98 | else: 99 | return self.mains[building_i] 100 | 101 | 102 | def disaggregate(self, building_i, return_sequences=False): 103 | kwargs = dict( 104 | mains=self.mains[building_i], 105 | target=self.appliance_y_true[building_i], 106 | max_target_power=self.MAX_TARGET_POWER, 107 | building_i=building_i, 108 | return_sequences=return_sequences 109 | ) 110 | kwargs.update(self.disagg_kwargs) 111 | 112 | if return_sequences: 113 | estimates, sequences = self.disagg_func(**kwargs) 114 | else: 115 | estimates = self.disagg_func(**kwargs) 116 | 117 | if self.pad_mains and not self.pad_appliance: 118 | estimates = estimates[self.PAD_WIDTH:-self.PAD_WIDTH] # remove padding 119 | estimates = np.round(estimates).astype(int) 120 | 121 | if return_sequences: 122 | return estimates, sequences 123 | else: 124 | return estimates 125 | 126 | 127 | def calculate_metrics(self, return_sequences=False): 128 | scores = {} 129 | estimates = {} 130 | sequences = {} 131 | for building_i in self.BUILDINGS: 132 | mains = self.mains[building_i] 133 | y_true = self.appliance_y_true[building_i] 134 | if return_sequences: 135 | y_pred, y_sequences = self.disaggregate(building_i, return_sequences=True) 136 | else: 137 | y_pred = self.disaggregate(building_i) 138 | 139 | if self.pad_appliance: 140 | y_true = y_true[self.PAD_WIDTH:-self.PAD_WIDTH] # remove padding 141 | y_pred = y_pred[self.PAD_WIDTH:-self.PAD_WIDTH] 142 | 143 | # Truncate 144 | n = min(len(y_true), len(y_pred)) 145 | y_true = y_true[:n] 146 | y_pred = y_pred[:n] 147 | 148 | #np.savez("building_{}".format(building_i), y_true=y_true, y_pred=y_pred, mains=mains) 149 | scores[building_i] = self.metrics.run_metrics(y_true, y_pred, mains) 150 | 151 | if return_sequences: 152 | sequences[building_i] = y_sequences 153 | estimates[building_i] = y_pred 154 | 155 | if return_sequences: 156 | return scores, estimates, sequences 157 | else: 158 | return scores 159 | 160 | 161 | def save_disaggregation_data(self, 162 | model_name, 163 | estimates, 164 | sequences, 165 | SAVETO_DIR='./disaggregation_data/'): 166 | 167 | model_name = model_name.replace(" ", "_") 168 | 169 | SAVETO_DIR = os.path.join(SAVETO_DIR, model_name) 170 | 171 | for building_i in sequences: 172 | SAVETO_PartialPATH = os.path.join(SAVETO_DIR, str(building_i)) 173 | os.makedirs(SAVETO_PartialPATH, exist_ok=True) 174 | 175 | SAVETO_PATHs = [os.path.join(SAVETO_PartialPATH, "estimate_windows.npz"), 176 | os.path.join(SAVETO_PartialPATH, "estimate_average.npy"), 177 | os.path.join(SAVETO_PartialPATH, "mains.npy"), 178 | os.path.join(SAVETO_PartialPATH, "appliance.npy")] 179 | 180 | for SAVETO_PATH in SAVETO_PATHs: 181 | assert not os.path.exists(SAVETO_PATH), "ERROR: File {} already exists !!!!!!".format(SAVETO_PATH) 182 | 183 | np.savez(SAVETO_PATHs[0], **sequences[building_i]) 184 | np.save(SAVETO_PATHs[1], estimates[building_i]) 185 | np.save(SAVETO_PATHs[2], self.mains[building_i]) 186 | np.save(SAVETO_PATHs[3], self.appliance_y_true[building_i]) 187 | 188 | print("INFO: saved estimates windows to {}".format(SAVETO_PATHs[0])) 189 | print("INFO: saved estimates averages to {}".format(SAVETO_PATHs[1])) 190 | print("INFO: saved mains to {}".format(SAVETO_PATHs[2])) 191 | print("INFO: saved appliance_y_true to {}".format(SAVETO_PATHs[3])) 192 | 193 | 194 | def load_disaggregation_data(PATH): 195 | return np.load(PATH) 196 | 197 | 198 | 199 | if __name__ == "__main__": 200 | import os, sys 201 | import jsonpickle 202 | import importlib 203 | import argparse 204 | 205 | DEFAULT_DATA_DIR = './input/evaluation_data' 206 | 207 | parser = argparse.ArgumentParser(description='Disaggregate experiment') 208 | parser.add_argument('experiment_name', help='experiment script name') 209 | parser.add_argument('appliance', metavar='appliance', help='target appliance') 210 | parser.add_argument('--nosequences', help='do not save all disaggregation windows', action='store_true') 211 | parser.add_argument('--id', metavar='id', type=int, help='experiment id', nargs='?') 212 | parser.add_argument('--modeldir', metavar='modeldir', help='model base folder', nargs='?') 213 | parser.add_argument('--modelfn', metavar='modelfn', help='model file name', nargs='?') 214 | parser.add_argument('--resultdir', metavar='resultdir', help='result folder', nargs='?') 215 | parser.add_argument('--data', metavar='data', help='data folder', nargs='?') 216 | #parser.add_argument('--downsample', metavar='factor', type=int, help='downsample mains by factor', default=1) 217 | 218 | args = parser.parse_args() 219 | 220 | experiment_name = args.experiment_name 221 | target_device = args.appliance 222 | experiment_id = args.id 223 | model_basedir = args.modeldir 224 | if model_basedir is None: 225 | model_basedir = experiment_name 226 | result_dir = args.resultdir 227 | if result_dir is None: 228 | result_dir = os.path.join("results", experiment_name) 229 | data_dir = args.data 230 | if data_dir is None: 231 | data_dir = DEFAULT_DATA_DIR 232 | model_name = args.modelfn 233 | if model_name is None: 234 | model_name = "" 235 | #downsample_factor = args.downsample 236 | 237 | def find_newest_id(path): 238 | def cast_to_int(s): 239 | try: 240 | return int(s) 241 | except ValueError: 242 | return 0 243 | ids = list(map(cast_to_int, os.listdir(path))) 244 | ids.sort() 245 | return ids[-1] 246 | 247 | if experiment_id is None: 248 | experiment_id = find_newest_id(os.path.join(model_basedir,target_device)) 249 | 250 | experiment = importlib.import_module(experiment_name) 251 | 252 | disagg, _ = experiment.load_disaggregator(data_dir, '{}/{}/{}/{}'.format(model_basedir,target_device,experiment_id,model_name)) 253 | 254 | result_dir = os.path.join(result_dir, str(experiment_id)) 255 | 256 | os.makedirs(result_dir, exist_ok=True) 257 | 258 | if args.nosequences: 259 | results = disagg.calculate_metrics(return_sequences=False) 260 | else: 261 | results, estimates, sequences = disagg.calculate_metrics(return_sequences=True) 262 | disagg.save_disaggregation_data(target_device, estimates, sequences, SAVETO_DIR=result_dir) 263 | 264 | with open(os.path.join(result_dir, "{}.json".format(target_device)), "w") as f: 265 | f.write(jsonpickle.dumps(results)) 266 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /metrics.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | from copy import copy 3 | import numpy as np 4 | import torch 5 | from sklearn import metrics as skmetrics 6 | 7 | class MetricsAccumulator(): 8 | def __init__(self, on_power_threshold, max_power): 9 | self.__mean_metric_names = [ 10 | "average_error", 11 | "mean_absolute_error", 12 | "mean_squared_error", 13 | "relative_error_in_total_target_energy", 14 | "error_in_total_energy_assigned" 15 | ] 16 | self.__sum_metric_names = [ 17 | "sum_abs_diff", 18 | "energy_error", 19 | "estimate", 20 | "target", 21 | "tp", "tn", "fp", "fn", "union", 22 | "tp_thres", "tn_thres", "fp_thres", "fn_thres", 23 | "mae_on", "mae_off", 24 | "mse_on", "mse_off", 25 | ] 26 | self.__sum_metric_results = [ 27 | "sum_abs_diff", 28 | "energy_error" 29 | ] 30 | self.__counter_names = [ 31 | "tp", "tn", "fp", "fn", 32 | "tp_thres", "tn_thres", "fp_thres", "fn_thres", 33 | "on", "off", "count", "calls" 34 | ] 35 | self.on_power_threshold = on_power_threshold 36 | self.max_power = max_power 37 | self.reset_accumulator() 38 | 39 | 40 | def reset_accumulator(self): 41 | self.accumulated = {k: 0.0 for k in self.__mean_metric_names} 42 | self.summed = {k: 0.0 for k in self.__sum_metric_names} 43 | self.counter = {k: 0 for k in self.__counter_names} 44 | 45 | 46 | def accumulate_metrics(self, true_y, pred_y, **kwargs): 47 | assert(true_y.shape == pred_y.shape) 48 | true_y = true_y.flatten() 49 | pred_y = pred_y.flatten() 50 | 51 | clipped_pred_y = np.clip(pred_y, 0, None) 52 | clipped_true_y = np.clip(true_y, 0, None) 53 | 54 | for k, v in kwargs.items(): 55 | v_old = self.accumulated.setdefault(k, 0.0) 56 | self.accumulated[k] = v_old + v 57 | 58 | count = len(true_y) 59 | 60 | abs_diff = np.fabs(clipped_pred_y - clipped_true_y) 61 | sum_abs_diff = np.sum(abs_diff) 62 | square_diff = np.square(clipped_pred_y - clipped_true_y) 63 | sum_square_diff = np.sum(square_diff) 64 | signed_error = clipped_pred_y - clipped_true_y 65 | sum_signed_error = np.sum(signed_error) 66 | 67 | self.summed["r2_score"] = skmetrics.r2_score(clipped_true_y, clipped_pred_y) 68 | self.summed["standard_deviation_of_error"] = np.sqrt(np.sum(np.square( \ 69 | sum_signed_error - (sum_signed_error/count) )) / count) 70 | 71 | self.counter["calls"] += 1 72 | 73 | self.summed["sum_abs_diff"] += sum_abs_diff 74 | self.accumulated["mean_absolute_error"] += sum_abs_diff 75 | self.accumulated["mean_squared_error"] += sum_square_diff 76 | self.accumulated["average_error"] += sum_signed_error 77 | 78 | self.summed['estimate'] += np.sum(pred_y) 79 | self.summed['target'] += np.sum(true_y) 80 | 81 | self.summed["union"] += np.sum(np.maximum(clipped_pred_y, clipped_true_y)) 82 | self.summed["tp"] += np.sum(np.minimum(clipped_pred_y, clipped_true_y)) 83 | self.summed["fp"] += np.sum(np.clip(clipped_pred_y - clipped_true_y, 0, None)) 84 | self.summed["fn"] += np.sum(np.clip(clipped_true_y - clipped_pred_y, 0, None)) 85 | self.summed["tn"] += np.sum(np.minimum(self.max_power - clipped_pred_y, self.max_power - clipped_true_y)) 86 | 87 | above_threshold_pred = np.greater_equal(clipped_pred_y, self.on_power_threshold) 88 | above_threshold_true = np.greater_equal(clipped_true_y, self.on_power_threshold) 89 | below_threshold_pred = np.less(clipped_pred_y, self.on_power_threshold) 90 | below_threshold_true = np.less(clipped_true_y, self.on_power_threshold) 91 | tp = np.sum(above_threshold_pred & above_threshold_true) 92 | tn = np.sum(below_threshold_pred & below_threshold_true) 93 | true_above_threshold_count = np.sum(above_threshold_true) 94 | true_below_threshold_count = (len(above_threshold_true)-true_above_threshold_count) 95 | self.counter["tp"] += tp 96 | self.counter["tn"] += tn 97 | self.counter["fn"] += true_above_threshold_count - tp 98 | self.counter["fp"] += true_below_threshold_count - tn 99 | 100 | self.summed["mae_on"] += np.sum(np.where(above_threshold_true, abs_diff, 0)) 101 | self.summed["mae_off"] += np.sum(np.where(below_threshold_true, abs_diff, 0)) 102 | self.summed["mse_on"] += np.sum(np.where(above_threshold_true, square_diff, 0)) 103 | self.summed["mse_off"] += np.sum(np.where(below_threshold_true, square_diff, 0)) 104 | self.counter["on"] += true_above_threshold_count 105 | self.counter["off"] += true_below_threshold_count 106 | 107 | above_threshold_pred_y = np.where(above_threshold_pred, clipped_pred_y, 0) 108 | above_threshold_true_y = np.where(above_threshold_true, clipped_true_y, 0) 109 | self.summed["tp_thres"] += np.sum(np.minimum(above_threshold_pred_y, above_threshold_true_y)) 110 | self.summed["fp_thres"] += np.sum(np.clip(above_threshold_pred_y - above_threshold_true_y, 0, None)) 111 | self.summed["fn_thres"] += np.sum(np.clip(above_threshold_true_y - above_threshold_pred_y, 0, None)) 112 | self.summed["tn_thres"] += np.sum(np.minimum(self.max_power - above_threshold_pred_y, self.max_power - above_threshold_true_y)) 113 | 114 | self.counter["count"] += count 115 | 116 | 117 | def finalize_metrics(self): 118 | result = {} 119 | 120 | count = self.counter["count"] # would be very strange if this is 0 121 | 122 | for k, v in self.accumulated.items(): 123 | result[k] = float(v / count) 124 | 125 | for k in self.__sum_metric_results: 126 | result[k] = float(self.summed[k]) 127 | 128 | tp = float(self.counter["tp"]) 129 | fp = float(self.counter["fp"]) 130 | tn = float(self.counter["tn"]) 131 | fn = float(self.counter["fn"]) 132 | 133 | predicted_positives = tp + fp 134 | precision = float(tp / predicted_positives) if predicted_positives != 0.0 else np.nan 135 | result["precision_score"] = precision 136 | 137 | condition_positives = tp + fn 138 | recall = float(tp / condition_positives) if condition_positives != 0.0 else np.nan 139 | result["recall_score"] = recall 140 | 141 | divisor = precision + recall 142 | f1 = float( (2.0 * precision * recall) / divisor ) if divisor != 0.0 else np.nan 143 | result["f1_score"] = f1 144 | 145 | divisor = tp + fp + tn + fn 146 | accuracy = float((tp + tn) / divisor) if divisor != 0.0 else np.nan 147 | result["accuracy_score"] = accuracy 148 | 149 | condition_negatives = tn + fp 150 | specificity = float(tn / condition_negatives) if condition_negatives != 0.0 else np.nan 151 | result["specificity_score"] = specificity 152 | 153 | predicted_negatives = tn + fn 154 | npv = float(tn / predicted_negatives) if predicted_negatives != 0.0 else np.nan 155 | result["npv_score"] = npv 156 | 157 | condition_positives = tp + fn 158 | condition_negatives = tn + fp 159 | balanced_accuracy = float((tp) / condition_positives) if condition_positives != 0.0 else np.nan 160 | balanced_accuracy += float((tn) / condition_negatives) if condition_negatives != 0.0 else np.nan 161 | balanced_accuracy /= 2 162 | result["balanced_accuracy_score"] = balanced_accuracy 163 | 164 | divisor = (tp+fp)*(tp+fn)*(tn+fp)*(tn+fn) 165 | divisor = np.sqrt(divisor) if divisor >= 0.0 else np.nan 166 | mcc = float( ((tp*tn)-(fp*fn))/divisor ) if divisor != 0.0 else np.nan 167 | result["mcc_score"] = mcc 168 | 169 | tp = self.summed["tp"] 170 | fp = self.summed["fp"] 171 | tn = self.summed["tn"] 172 | fn = self.summed["fn"] 173 | 174 | intersection = tp 175 | union = self.summed["union"] 176 | result["match_rate"] = float(intersection / union) if union != 0.0 else np.nan 177 | 178 | predicted_positives = tp + fp 179 | precision = float(tp / predicted_positives) if predicted_positives != 0.0 else np.nan 180 | result["precision_energy"] = precision 181 | 182 | divisor = tp + fn 183 | recall = float(tp / divisor) if divisor != 0.0 else np.nan 184 | result["recall_energy"] = recall 185 | 186 | divisor = result["precision_energy"] + result["recall_energy"] 187 | f1 = float( (2.0 * precision * recall) / divisor ) if divisor != 0.0 else np.nan 188 | result["f1_energy"] = f1 189 | 190 | divisor = tp + fp + tn + fn 191 | accuracy = float((tp + tn) / divisor) if divisor != 0.0 else np.nan 192 | result["accuracy_energy"] = accuracy 193 | 194 | condition_negatives = tn + fp 195 | specificity = float(tn / condition_negatives) if condition_negatives != 0.0 else np.nan 196 | result["specificity_energy"] = specificity 197 | 198 | predicted_negatives = tn + fn 199 | npv = float(tn / predicted_negatives) if predicted_negatives != 0.0 else np.nan 200 | result["npv_energy"] = npv 201 | 202 | condition_positives = tp + fn 203 | condition_negatives = tn + fp 204 | balanced_accuracy = float((tp) / condition_positives) if condition_positives != 0.0 else np.nan 205 | balanced_accuracy += float((tn) / condition_negatives) if condition_negatives != 0.0 else np.nan 206 | balanced_accuracy /= 2 207 | result["balanced_accuracy_energy"] = balanced_accuracy 208 | 209 | divisor = (tp+fp)*(tp+fn)*(tn+fp)*(tn+fn) 210 | divisor = np.sqrt(divisor) if divisor >= 0.0 else np.nan 211 | mcc = float( ((tp*tn)-(fp*fn))/divisor ) if divisor != 0.0 else np.nan 212 | result["mcc_energy"] = mcc 213 | 214 | num_samples = tp + fp + tn + fn 215 | factor = np.log(num_samples) if num_samples > 0.0 else np.nan 216 | l = num_samples * factor 217 | divisor = (tp+fp)*(tp+fn) 218 | factor = np.log(tp/divisor) if ((divisor != 0.0) and (tp > 0.0)) else np.nan 219 | ltp = tp * factor 220 | divisor = (fp+tp)*(fp+tn) 221 | factor = np.log(fp/divisor) if ((divisor != 0.0) and (fp > 0.0)) else np.nan 222 | lfp = fp * factor 223 | divisor = (fn+tp)*(fn+tn) 224 | factor = np.log(fn/divisor) if ((divisor != 0.0) and (fn > 0.0)) else np.nan 225 | lfn = fn * factor 226 | divisor = (tn+fp)*(tn+fn) 227 | factor = np.log(tn/divisor) if ((divisor != 0.0) and (tn > 0.0)) else np.nan 228 | ltn = tn * factor 229 | condition_positives = tp + fn 230 | condition_negatives = tn + fp 231 | factor = np.log(condition_positives/num_samples) if ((num_samples != 0.0) and (condition_positives > 0.0)) else np.nan 232 | lp = condition_positives * factor 233 | factor = np.log(condition_negatives/num_samples) if ((num_samples != 0.0) and (condition_negatives > 0.0)) else np.nan 234 | ln = condition_negatives * factor 235 | divisor = l + lp + ln 236 | proficiency = ((l+ltp+lfp+lfn+ltn)/divisor) if divisor != 0.0 else np.nan 237 | result["proficiency_energy"] = proficiency 238 | 239 | tp = self.summed["tp_thres"] 240 | fp = self.summed["fp_thres"] 241 | tn = self.summed["tn_thres"] 242 | fn = self.summed["fn_thres"] 243 | 244 | predicted_positives = tp + fp 245 | precision = float(tp / predicted_positives) if predicted_positives != 0.0 else np.nan 246 | result["precision_energy_on"] = precision 247 | 248 | divisor = tp + fn 249 | recall = float(tp / divisor) if divisor != 0.0 else np.nan 250 | result["recall_energy_on"] = recall 251 | 252 | divisor = result["precision_energy_on"] + result["recall_energy_on"] 253 | f1 = float( (2.0 * precision * recall) / divisor ) if divisor != 0.0 else np.nan 254 | result["f1_energy_on"] = f1 255 | 256 | divisor = tp + fp + tn + fn 257 | accuracy = float((tp + tn) / divisor) if divisor != 0.0 else np.nan 258 | result["accuracy_energy_on"] = accuracy 259 | 260 | condition_negatives = tn + fp 261 | specificity = float(tn / condition_negatives) if condition_negatives != 0.0 else np.nan 262 | result["specificity_energy_on"] = specificity 263 | 264 | predicted_negatives = tn + fn 265 | npv = float(tn / predicted_negatives) if predicted_negatives != 0.0 else np.nan 266 | result["npv_energy_on"] = npv 267 | 268 | condition_positives = tp + fn 269 | condition_negatives = tn + fp 270 | balanced_accuracy = float((tp) / condition_positives) if condition_positives != 0.0 else np.nan 271 | balanced_accuracy += float((tn) / condition_negatives) if condition_negatives != 0.0 else np.nan 272 | balanced_accuracy /= 2 273 | result["balanced_accuracy_energy_on"] = balanced_accuracy 274 | 275 | divisor = (tp+fp)*(tp+fn)*(tn+fp)*(tn+fn) 276 | divisor = np.sqrt(divisor) if divisor >= 0.0 else np.nan 277 | mcc = float( ((tp*tn)-(fp*fn))/divisor ) if divisor != 0.0 else np.nan 278 | result["mcc_energy_on"] = mcc 279 | 280 | num_samples = tp + fp + tn + fn 281 | factor = np.log(num_samples) if num_samples > 0.0 else np.nan 282 | l = num_samples * factor 283 | divisor = (tp+fp)*(tp+fn) 284 | factor = np.log(tp/divisor) if ((divisor != 0.0) and (tp > 0.0)) else np.nan 285 | ltp = tp * factor 286 | divisor = (fp+tp)*(fp+tn) 287 | factor = np.log(fp/divisor) if ((divisor != 0.0) and (fp > 0.0)) else np.nan 288 | lfp = fp * factor 289 | divisor = (fn+tp)*(fn+tn) 290 | factor = np.log(fn/divisor) if ((divisor != 0.0) and (fn > 0.0)) else np.nan 291 | lfn = fn * factor 292 | divisor = (tn+fp)*(tn+fn) 293 | factor = np.log(tn/divisor) if ((divisor != 0.0) and (tn > 0.0)) else np.nan 294 | ltn = tn * factor 295 | condition_positives = tp + fn 296 | condition_negatives = tn + fp 297 | factor = np.log(condition_positives/num_samples) if ((num_samples != 0.0) and (condition_positives > 0.0)) else np.nan 298 | lp = condition_positives * factor 299 | factor = np.log(condition_negatives/num_samples) if ((num_samples != 0.0) and (condition_negatives > 0.0)) else np.nan 300 | ln = condition_negatives * factor 301 | divisor = l + lp + ln 302 | proficiency = ((l+ltp+lfp+lfn+ltn)/divisor) if divisor != 0.0 else np.nan 303 | result["proficiency_energy_on"] = proficiency 304 | 305 | sum_estimate = self.summed['estimate'] 306 | sum_target = self.summed['target'] 307 | 308 | error_in_total_energy_assigned = np.fabs(float(sum_estimate - sum_target)) 309 | result["error_in_total_energy_assigned"] = error_in_total_energy_assigned 310 | 311 | result["deviation"] = float(error_in_total_energy_assigned / sum_target) 312 | 313 | relative_error_in_total_target_energy = float( \ 314 | (sum_estimate - sum_target) / sum_target ) 315 | result["relative_error_in_total_target_energy"] = relative_error_in_total_target_energy 316 | 317 | result["energy_error"] = float(self.summed["sum_abs_diff"] / sum_target) 318 | 319 | if self.counter["calls"] == 1: 320 | result["r2_score"] = self.summed["r2_score"] 321 | result["standard_deviation_of_error"] = self.summed["standard_deviation_of_error"] 322 | else: 323 | result["r2_score"] = np.nan 324 | result["standard_deviation_of_error"] = np.nan 325 | 326 | result["fraction_of_energy_explained"] = float(sum_estimate / sum_target) 327 | 328 | result["normalized_mean_absolute_error"] = float(result["mean_absolute_error"] / sum_target) 329 | result["normalized_mean_squared_error"] = float(result["mean_squared_error"] / sum_target) 330 | 331 | result["mean_absolute_error_on"] = float(self.summed["mae_on"] / self.counter["on"]) if self.counter["on"] != 0.0 else np.nan 332 | result["mean_absolute_error_off"] = float(self.summed["mae_off"] / self.counter["off"]) if self.counter["off"] != 0.0 else np.nan 333 | result["mean_squared_error_on"] = float(self.summed["mse_on"] / self.counter["on"]) if self.counter["on"] != 0.0 else np.nan 334 | result["mean_squared_error_off"] = float(self.summed["mse_off"] / self.counter["off"]) if self.counter["off"] != 0.0 else np.nan 335 | 336 | return result 337 | 338 | 339 | def run_metrics(self, y_true, y_pred, mains): 340 | # Truncate 341 | n = min(len(y_true), len(y_pred)) 342 | y_true = y_true[:n] 343 | y_pred = y_pred[:n] 344 | 345 | self.reset_accumulator() 346 | self.accumulate_metrics(y_true, y_pred) 347 | result = self.finalize_metrics() 348 | 349 | # For total energy correctly assigned 350 | denominator = 2 * np.sum(mains) 351 | sum_abs_diff = result['sum_abs_diff'] 352 | total_energy_correctly_assigned = 1 - (sum_abs_diff / denominator) 353 | total_energy_correctly_assigned = float(total_energy_correctly_assigned) 354 | result['total_energy_correctly_assigned'] = total_energy_correctly_assigned 355 | 356 | return result 357 | 358 | 359 | def calculate_pr_curve(accumulated_pr, max_target_power, true_y, pred_y, num_thresholds): 360 | assert(true_y.shape == pred_y.shape) 361 | true_y = np.clip(true_y, 0, None).flatten() 362 | pred_y = np.clip(pred_y, 0, None).flatten() 363 | num_y = true_y.shape[0] 364 | 365 | tp = accumulated_pr["tp"] 366 | fp = accumulated_pr["fp"] 367 | tn = accumulated_pr["tn"] 368 | fn = accumulated_pr["fn"] 369 | 370 | for i, thres in enumerate(np.linspace(0, max_target_power, num=num_thresholds), 0): 371 | threshold_pred_y = (pred_y >= thres) 372 | threshold_true_y = (true_y >= thres) 373 | threshold_true_y_neg = np.logical_not(threshold_true_y) 374 | tp_i = np.sum(threshold_pred_y & threshold_true_y) 375 | tp[i] += int(tp_i) 376 | fp_i = np.sum(threshold_pred_y & threshold_true_y_neg) 377 | fp[i] += int(fp_i) 378 | fn[i] += int(np.sum(threshold_true_y) - tp_i) 379 | tn[i] += int(np.sum(threshold_true_y_neg) - fp_i) 380 | 381 | return accumulated_pr 382 | 383 | 384 | def calculate_pr_curve_torch(accumulated_pr, max_target_power, true_y, pred_y, num_thresholds): 385 | assert(true_y.shape == pred_y.shape) 386 | true_y = torch.from_numpy(true_y).clamp_(min=0).view(-1) 387 | pred_y = torch.from_numpy(pred_y).clamp_(min=0).view(-1) 388 | num_y = true_y.shape[0] 389 | 390 | tp = accumulated_pr["tp"] 391 | fp = accumulated_pr["fp"] 392 | tn = accumulated_pr["tn"] 393 | fn = accumulated_pr["fn"] 394 | 395 | for i, thres in enumerate(np.linspace(0, max_target_power, num=num_thresholds), 0): 396 | threshold_pred_y = torch.ge(pred_y, thres) 397 | threshold_true_y = torch.ge(true_y, thres) 398 | threshold_true_y_neg = 1 - threshold_true_y 399 | tp_i = torch.sum(threshold_pred_y * threshold_true_y) 400 | tp[i] += tp_i 401 | fp_i = torch.sum(threshold_pred_y * threshold_true_y_neg) 402 | fp[i] += fp_i 403 | fn[i] += torch.sum(threshold_true_y) - tp_i 404 | tn[i] += torch.sum(threshold_true_y_neg) - fp_i 405 | 406 | return accumulated_pr 407 | 408 | -------------------------------------------------------------------------------- /dae_kelly_URED_.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import pandas as pd 3 | import numpy as np 4 | 5 | import os, pickle 6 | 7 | from tqdm import tqdm 8 | from collections import OrderedDict 9 | 10 | ## PYTORCH 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.parallel 14 | import torch.backends.cudnn as cudnn 15 | import torch.optim as optim 16 | import torch.optim.lr_scheduler as lr_scheduler 17 | import torch.utils.data 18 | import torchvision.datasets as dset 19 | import torchvision.transforms as transforms 20 | import torchvision.utils as vutils 21 | from torch.autograd import Variable 22 | 23 | from tensorboardX import SummaryWriter 24 | 25 | import matplotlib 26 | if not matplotlib.is_interactive(): 27 | matplotlib.use("AGG") 28 | #import matplotlib.pyplot as plt 29 | #import matplotlib.gridspec as gridspec 30 | 31 | from neuralnilm.data.datapipeline import DataPipeline 32 | from neuralnilm.data.datathread import DataThread 33 | from neuralnilm.data.dataprocess import DataProcess 34 | from neuralnilm.data.processing import DivideBy, IndependentlyCenter, Transpose, DownSample 35 | from neuralnilm.consts import DATA_FOLD_NAMES 36 | 37 | from disaggregator import Disaggregator 38 | from metrics import MetricsAccumulator 39 | from parallelsource import MultiprocessActivationsSource 40 | from utils import SW_add_scalars2 41 | 42 | from sacred import Experiment 43 | from sacred.observers import FileStorageObserver 44 | 45 | from dataset_final_URED_ import dataset, load_activity_data, get_sources 46 | 47 | ex = Experiment(ingredients=[dataset]) 48 | 49 | #from sacred.utils import apply_backspaces_and_linefeeds 50 | #ex.captured_out_filter = apply_backspaces_and_linefeeds 51 | 52 | @ex.config 53 | def base_experiment_config(dataset): 54 | INPUT_MEAN = 500.0 55 | INPUT_STD = 700.0 56 | 57 | TARGET_APPLIANCE = dataset["TARGET_APPLIANCE"] 58 | SOURCE_TYPES = ["BalancedActivityRealAggregateSource", "BalancedActivityAugmentedAggregateSource"] 59 | VALIDATION_SOURCE_TYPES = ["RandomizedSequentialSource"] 60 | 61 | if TARGET_APPLIANCE == 'television': 62 | MAX_TARGET_POWER = 1200 63 | SEQ_LENGTH = 1024 + 512 64 | elif TARGET_APPLIANCE == 'microwave': 65 | MAX_TARGET_POWER = 3000 66 | SEQ_LENGTH = 288 67 | elif TARGET_APPLIANCE == 'tumble dryer': 68 | MAX_TARGET_POWER = 2500 69 | SEQ_LENGTH = 1024 70 | elif TARGET_APPLIANCE == 'fridge': 71 | SOURCE_TYPES = ["BalancedBuildingRealAggregateSource"] 72 | MAX_TARGET_POWER = 300 73 | SEQ_LENGTH = 512 74 | elif TARGET_APPLIANCE == 'kettle': 75 | MAX_TARGET_POWER = 3100 76 | SEQ_LENGTH = 128 77 | elif TARGET_APPLIANCE == 'washing machine': 78 | MAX_TARGET_POWER = 2500 79 | SEQ_LENGTH = 1024 80 | elif TARGET_APPLIANCE == 'dish washer': 81 | MAX_TARGET_POWER = 2500 82 | SEQ_LENGTH = 1024 + 512 83 | 84 | TRAINING_SEED = 42 85 | VERBOSE_TRAINING = True 86 | DOWNSAMPLE_FACTOR = 0 87 | 88 | DESCRIPTION = """ Re-Impl. of JK's dAE in PyTorch 89 | """ 90 | 91 | NUM_SEQ_PER_BATCH = 64 92 | EPOCHS = 100 93 | STEPS_PER_EPOCH = 1000 94 | 95 | LEARNING_RATE = 1e-1 96 | NUM_BATCHES_FOR_VALIDATION = 64 97 | USE_CUDA = True 98 | CHECKPOINT_BEST_MSE = False 99 | CHECKPOINTING_EVERY_N_EPOCHS = None 100 | TEST_DISAGGREGATE_EVERY_N_EPOCHS = 1 101 | 102 | 103 | @ex.capture 104 | def get_validation_batches(data_pipeline, VALIDATION_SOURCE_TYPES, NUM_BATCHES_FOR_VALIDATION): 105 | shortname_sources = { 106 | "BalancedActivityRealAggregateSource": "bas", 107 | "BalancedActivityAugmentedAggregateSource": "aas", 108 | "RandomizedSequentialSource": "rss", 109 | "BalancedBuildingRealAggregateSource": "bbs" 110 | } 111 | shortname_folds = { # listed are only validation folds 112 | "unseen_activations_of_seen_appliances": "unseen_activations" 113 | } 114 | 115 | validation_batches = {} 116 | 117 | for source in VALIDATION_SOURCE_TYPES: 118 | for fold in shortname_folds: 119 | fold_batches = [] 120 | try: 121 | batch = data_pipeline.get_batch(fold=fold, source_id=source, reset_iterator=True, validation=False) 122 | fold_batches.append(batch) 123 | except KeyError: 124 | print("For fold `{}` no validation data available".format(fold)) 125 | continue 126 | 127 | for i in range(1,NUM_BATCHES_FOR_VALIDATION): 128 | batch = data_pipeline.get_batch(fold=fold, source_id=source, validation=False) 129 | if batch is None: # Here we reach the end of the validation data for the current fold. 130 | break 131 | else: 132 | fold_batches.append(batch) 133 | 134 | if i == NUM_BATCHES_FOR_VALIDATION-1: 135 | print("WARNING: Validation data may not be fully covered") 136 | 137 | validation_batches[(shortname_folds[fold],shortname_sources[source])] = fold_batches 138 | return validation_batches 139 | 140 | 141 | def disag_seq2seq(model, mains, target, input_processing, target_processing, max_target_power, n_seq_per_batch, seq_length, target_seq_length, building_i, stride, USE_CUDA, return_sequences): 142 | def apply_inverse_processing(batch, processing_steps): 143 | reversed_processing_steps = processing_steps[::-1] 144 | for step in reversed_processing_steps: 145 | batch = step.inverse(batch) 146 | 147 | return batch 148 | 149 | def apply_processing(batch, processing_steps): 150 | for step in processing_steps: 151 | batch = step(batch) 152 | 153 | return batch 154 | 155 | def mains_to_batches(mains, n_seq_per_batch, seq_length, processing_steps, stride=1): 156 | assert mains.ndim == 1 157 | n_mains_samples = len(mains) 158 | input_shape = (n_seq_per_batch, seq_length, 1) 159 | 160 | # Divide mains data into batches 161 | n_batches = (n_mains_samples / stride) / n_seq_per_batch 162 | n_batches = np.ceil(n_batches).astype(int) 163 | batches = [] 164 | for batch_i in range(n_batches): 165 | batch = np.zeros(input_shape, dtype=np.float32) 166 | batch_start = batch_i * n_seq_per_batch * stride 167 | for seq_i in range(n_seq_per_batch): 168 | mains_start_i = batch_start + (seq_i * stride) 169 | mains_end_i = mains_start_i + seq_length 170 | seq = mains[mains_start_i:mains_end_i] 171 | batch[seq_i, :len(seq), 0] = seq 172 | processed_batch = apply_processing(batch, processing_steps) 173 | batches.append(processed_batch) 174 | 175 | return batches 176 | 177 | 178 | if stride is None: 179 | stride = seq_length 180 | 181 | batches = mains_to_batches(mains, n_seq_per_batch, seq_length, input_processing, stride) 182 | estimates = np.zeros(len(mains), dtype=np.float32) 183 | offset = (seq_length - target_seq_length) // 2 184 | 185 | if return_sequences: 186 | # `estimate_windows` is array with shape [#sliding_windows_in_mains x lenth_of_window] 187 | #. it stores disag results for all sliding windows seperately 188 | # note: beware of padding. also last batch may be not be filled entirely 189 | #. therefore have some extra margin 190 | estimate_windows = np.zeros((len(batches)*n_seq_per_batch, target_seq_length), dtype=np.float32) 191 | 192 | # Iterate over each batch 193 | window_i = 0 194 | for batch_i, net_input in enumerate(batches): 195 | input = torch.from_numpy(net_input.astype(np.float32)) 196 | if USE_CUDA: 197 | input = input.cuda() 198 | with torch.no_grad(): 199 | inputv = Variable(input) 200 | output = model(inputv) 201 | net_output = output.cpu().data.numpy() 202 | net_output = apply_inverse_processing(net_output, target_processing) 203 | batch_start = (batch_i * n_seq_per_batch * stride) + offset 204 | for seq_i in range(n_seq_per_batch): 205 | start_i = batch_start + (seq_i * stride) 206 | end_i = start_i + target_seq_length 207 | n = len(estimates[start_i:end_i]) 208 | # The net output is not necessarily the same length 209 | # as the mains (because mains might not fit exactly into 210 | # the number of batches required) 211 | estimates[start_i:end_i] += net_output[seq_i, :n, 0] 212 | if return_sequences: 213 | estimate_windows[window_i, :] = net_output[seq_i, :, 0] 214 | 215 | window_i += 1 216 | 217 | n_overlaps = target_seq_length / stride 218 | estimates /= n_overlaps 219 | estimates[estimates < 0] = 0 220 | 221 | if return_sequences: 222 | return estimates, dict(sequences=estimate_windows) 223 | else: 224 | return estimates 225 | 226 | 227 | class _JKsDenoisingAutoEncoderOriginal(nn.Module): 228 | def __init__(self, SEQ_LENGTH): 229 | super(_JKsDenoisingAutoEncoderOriginal, self).__init__() 230 | 231 | self.seq_length = SEQ_LENGTH 232 | self.nc = 1 # number of channels 233 | self.nef = 8 # number of filters 234 | nrepr = 128 # dimension of internal representation 235 | self.fs = 4 # filter size 236 | 237 | self.pad1 = nn.ConstantPad1d((1, 2), 0) 238 | self.conv1 = nn.Conv1d(self.nc, self.nef, self.fs, stride=1, padding=0) 239 | 240 | self.n_dense_units = self.nef*self.seq_length 241 | self.dense1 = nn.Sequential( 242 | nn.Linear(self.n_dense_units, self.n_dense_units), 243 | nn.ReLU(True), 244 | 245 | nn.Linear(self.n_dense_units, nrepr), 246 | nn.ReLU(True), 247 | 248 | nn.Linear(nrepr, self.n_dense_units), 249 | nn.ReLU(True) 250 | ) 251 | 252 | self.pad2 = nn.ConstantPad1d((1, 2), 0) 253 | self.conv2 = nn.Conv1d(self.nef, self.nc, self.fs, stride=1, padding=0) 254 | 255 | for m in self.modules(): 256 | if isinstance(m, nn.Conv1d): 257 | nn.init.xavier_uniform_(m.weight) 258 | m.bias.data.zero_() 259 | if isinstance(m, nn.Linear): 260 | nn.init.xavier_uniform_(m.weight) 261 | m.bias.data.zero_() 262 | 263 | def forward(self, input): 264 | #TODO: Die Anzahl der veruegbaren GPUs beruecksichtigen. 265 | x = self.pad1(input) 266 | x = self.conv1(x) 267 | x = self.dense1(x.view(-1, self.n_dense_units)) 268 | x = self.pad2(x.view(-1, self.nef, self.seq_length)) 269 | x = self.conv2(x) 270 | return x 271 | 272 | _Net = _JKsDenoisingAutoEncoderOriginal 273 | 274 | 275 | def load_disaggregator(EVALUATION_DATA_PATH, MODEL_PATH, config=None, USE_CUDA=True): 276 | """ 277 | Helper function for the disaggregator script 278 | """ 279 | 280 | if config is None: 281 | config = os.path.dirname(MODEL_PATH) 282 | 283 | if type(config) == str: 284 | try: 285 | import jsonpickle 286 | with open(os.path.join(config, 'config.json'), 'r') as configfile: 287 | config = jsonpickle.decode(configfile.read()) 288 | except: 289 | return None 290 | 291 | assert(type(config) == dict) 292 | 293 | dataset = config['dataset'] 294 | SEQ_LENGTH = config['SEQ_LENGTH'] 295 | TARGET_APPLIANCE = dataset['TARGET_APPLIANCE'] 296 | ON_POWER_THRESHOLD = dataset['ON_POWER_THRESHOLD'] 297 | MAX_TARGET_POWER = config['MAX_TARGET_POWER'] 298 | NUM_SEQ_PER_BATCH = config['NUM_SEQ_PER_BATCH'] 299 | INPUT_STD = config['INPUT_STD'] 300 | INPUT_MEAN = config['INPUT_MEAN'] 301 | DOWNSAMPLE_FACTOR = config['DOWNSAMPLE_FACTOR'] 302 | 303 | net = _Net(SEQ_LENGTH) 304 | 305 | input_processing_steps = [DivideBy(INPUT_STD), IndependentlyCenter(), Transpose((0, 2, 1))] 306 | target_processing_steps = [DivideBy(MAX_TARGET_POWER), Transpose((0, 2, 1))] 307 | 308 | if MODEL_PATH.endswith("/"): 309 | MODEL_PATH = MODEL_PATH + 'net_step_{:06d}.pth.tar'.format(config['EPOCHS']*config['STEPS_PER_EPOCH']) 310 | 311 | if USE_CUDA: 312 | training_state = torch.load(MODEL_PATH) 313 | else: 314 | training_state = torch.load(MODEL_PATH, map_location='cpu') 315 | 316 | if MODEL_PATH.endswith("tar"): 317 | model = training_state['model'] 318 | else: 319 | model = training_state 320 | 321 | net.load_state_dict(model) 322 | if USE_CUDA: 323 | net.cuda() 324 | 325 | return Disaggregator( 326 | EVALUATION_DATA_PATH=EVALUATION_DATA_PATH, 327 | TARGET_APPLIANCE = TARGET_APPLIANCE, 328 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 329 | MAX_TARGET_POWER = MAX_TARGET_POWER, 330 | pad_mains = True, 331 | pad_appliance = False, 332 | disagg_func = disag_seq2seq, 333 | downsample_factor = DOWNSAMPLE_FACTOR, 334 | disagg_kwargs = dict( 335 | USE_CUDA=USE_CUDA, 336 | model = net, 337 | input_processing=input_processing_steps, 338 | target_processing=target_processing_steps, 339 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 340 | seq_length = SEQ_LENGTH, 341 | target_seq_length = SEQ_LENGTH, 342 | stride = 16 343 | ) 344 | ), training_state 345 | 346 | 347 | @ex.automain 348 | def run_experiment(dataset, 349 | INPUT_STD, 350 | SOURCE_TYPES, 351 | VALIDATION_SOURCE_TYPES, 352 | DOWNSAMPLE_FACTOR, 353 | SEQ_LENGTH, 354 | MAX_TARGET_POWER, 355 | TARGET_APPLIANCE, 356 | TRAINING_SEED, 357 | VERBOSE_TRAINING, 358 | LEARNING_RATE, 359 | NUM_SEQ_PER_BATCH, 360 | EPOCHS, 361 | STEPS_PER_EPOCH, 362 | USE_CUDA, 363 | CHECKPOINT_BEST_MSE, 364 | CHECKPOINTING_EVERY_N_EPOCHS, 365 | TEST_DISAGGREGATE_EVERY_N_EPOCHS, 366 | _run): 367 | 368 | torch.manual_seed(TRAINING_SEED) 369 | 370 | OUTPUT_FOLDER = os.path.join(ex.get_experiment_info()['name'],"output") 371 | for observer in _run.observers: 372 | if type(observer) is FileStorageObserver: 373 | OUTPUT_FOLDER = os.path.join(observer.basedir, str(_run._id)) 374 | VERBOSE_TRAINING = 0 375 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 376 | 377 | writer = SummaryWriter(log_dir=OUTPUT_FOLDER) 378 | 379 | # From dataset Ingredient 380 | TRAIN_BUILDINGS = dataset["TRAIN_BUILDINGS"] 381 | ON_POWER_THRESHOLD = dataset["ON_POWER_THRESHOLD"] 382 | 383 | ############################################################################################## 384 | #PREPARE DATASET (DATALOADERs) 385 | ############################################################################################## 386 | running_data_processes = [] # stop these at the end 387 | sources, validation_sources = get_sources( 388 | training_source_names=SOURCE_TYPES, 389 | validation_source_names=VALIDATION_SOURCE_TYPES, 390 | seq_length=SEQ_LENGTH, 391 | sources_seed=TRAINING_SEED, 392 | validation_stride=128 ) 393 | 394 | if DOWNSAMPLE_FACTOR > 1: 395 | downsample_rng = np.random.RandomState(TRAINING_SEED) 396 | input_processing_steps = [DownSample(DOWNSAMPLE_FACTOR, downsample_rng)] 397 | else: 398 | input_processing_steps = [] 399 | input_processing_steps += [DivideBy(INPUT_STD), IndependentlyCenter(), Transpose((0, 2, 1))] 400 | target_processing_steps = [DivideBy(MAX_TARGET_POWER), Transpose((0, 2, 1))] 401 | 402 | validation_pipeline = DataPipeline( 403 | sources=validation_sources, 404 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 405 | input_processing=input_processing_steps, 406 | target_processing=target_processing_steps 407 | ) 408 | validation_batches = get_validation_batches(validation_pipeline) 409 | print("appliance {} has {} validation batches".format( 410 | TARGET_APPLIANCE, 411 | sum([len(v) for k, v in validation_batches.items()]) )) 412 | 413 | data_pipeline = DataPipeline( 414 | sources=sources, 415 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 416 | input_processing=input_processing_steps, 417 | target_processing=target_processing_steps 418 | ) 419 | data_thread = DataProcess(data_pipeline) 420 | data_thread.start() 421 | running_data_processes.append(data_thread) 422 | 423 | net = _Net(SEQ_LENGTH) 424 | print(net) 425 | 426 | metrics_accu = MetricsAccumulator( 427 | on_power_threshold=ON_POWER_THRESHOLD, 428 | max_power=MAX_TARGET_POWER) 429 | 430 | # note: MSE - Mean Squared Error 431 | criterion = torch.nn.MSELoss() 432 | 433 | stop_training = False 434 | best_mse = None 435 | 436 | # PREPARE DISAGGREGATOR 437 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 438 | test_disaggregator = Disaggregator( 439 | EVALUATION_DATA_PATH = dataset['EVALUATION_DATA_PATH'], 440 | TARGET_APPLIANCE = TARGET_APPLIANCE, 441 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 442 | MAX_TARGET_POWER = MAX_TARGET_POWER, 443 | pad_mains = True, 444 | pad_appliance = False, 445 | disagg_func = disag_seq2seq, 446 | downsample_factor = DOWNSAMPLE_FACTOR, 447 | disagg_kwargs = dict( 448 | model = net, 449 | input_processing=input_processing_steps, 450 | target_processing=target_processing_steps, 451 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 452 | seq_length = SEQ_LENGTH, 453 | target_seq_length = SEQ_LENGTH, 454 | USE_CUDA=USE_CUDA, 455 | stride = 16 456 | ) 457 | ) 458 | 459 | # PREPARE TENSORS, WHICH WILL BE FED USED DURING TRAINING AND VALIDATION 460 | input = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 461 | target = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 462 | 463 | if USE_CUDA: 464 | # note: push to GPU 465 | net.cuda() 466 | criterion.cuda() 467 | input, target = input.cuda(), target.cuda() 468 | 469 | # setup optimizer. TODO: Should we use 'Adam' for disaggregator? 470 | #optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE) 471 | optimizer = optim.SGD(net.parameters(), momentum=0.9, nesterov=True, lr=LEARNING_RATE) 472 | scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[50,75], gamma=0.1) 473 | #scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10) 474 | 475 | history = {} 476 | csvpath = os.path.join(OUTPUT_FOLDER, "history.csv") 477 | if os.path.exists(csvpath): 478 | print("Already exists: {}".format(csvpath)) 479 | return -1 480 | 481 | progbar_epoch = tqdm(desc="Epoch", total=EPOCHS, unit="epoch", disable=(not VERBOSE_TRAINING)) 482 | for epoch in range(EPOCHS): 483 | # TRAINING 484 | metrics_log = {'training':{}} 485 | training_loss = 0.0 486 | progbar = tqdm(desc="Train", total=STEPS_PER_EPOCH, leave=False, disable=(not VERBOSE_TRAINING)) 487 | for i in range(STEPS_PER_EPOCH): 488 | net.zero_grad() 489 | batch = data_thread.get_batch() 490 | while batch is None: 491 | batch = data_thread.get_batch() 492 | qsize = data_thread._queue.qsize() 493 | 494 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 495 | target_signal = torch.from_numpy(batch.after_processing.target) 496 | if USE_CUDA: 497 | aggregated_signal = aggregated_signal.cuda() 498 | target_signal = target_signal.cuda() 499 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 500 | target.resize_as_(target_signal).copy_(target_signal) 501 | inputv = Variable(input, requires_grad=False) 502 | targetv = Variable(target, requires_grad=False) 503 | output = net(inputv) 504 | loss = criterion(output, targetv) 505 | loss.backward() 506 | optimizer.step() 507 | training_loss += loss.item() 508 | 509 | progbar.set_postfix(dict( 510 | loss = "{:.4f}".format(loss.item()), 511 | qsize = qsize 512 | ), refresh=False) 513 | progbar.update() 514 | 515 | metrics_log['training']['loss'] = float(training_loss/STEPS_PER_EPOCH) 516 | metrics_log['training']['lr'] = optimizer.param_groups[0]['lr'] 517 | 518 | # VALIDATION 519 | #pr_num_thresholds = 127 520 | for fold in validation_batches: 521 | metrics_accu.reset_accumulator() 522 | #accumulated_pr = {} 523 | #for cl in ["tp", "tn", "fp", "fn"]: 524 | # accumulated_pr[cl] = torch.LongTensor(pr_num_thresholds).zero_() 525 | for batch in validation_batches[fold]: 526 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 527 | target_signal = torch.from_numpy(batch.after_processing.target) 528 | if USE_CUDA: 529 | aggregated_signal = aggregated_signal.cuda() 530 | target_signal = target_signal.cuda() 531 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 532 | target.resize_as_(target_signal).copy_(target_signal) 533 | with torch.no_grad(): 534 | inputv = Variable(input) 535 | targetv = Variable(target) 536 | output = net(inputv) 537 | val_loss = criterion(output, targetv) 538 | loss_value = val_loss.item() 539 | # other metrics 540 | pred_y = data_pipeline.apply_inverse_processing(output.cpu().data.numpy(), 'target') 541 | true_y = batch.before_processing.target 542 | metrics_accu.accumulate_metrics(true_y, pred_y, val_loss=loss_value) 543 | #calculate_pr_curve_torch(accumulated_pr, MAX_TARGET_POWER, true_y, pred_y, num_thresholds=pr_num_thresholds) 544 | 545 | for key, value in metrics_accu.finalize_metrics().items(): 546 | metrics_log.setdefault(fold[0], {}).setdefault(key, {})[fold[1]] = value 547 | 548 | #precision = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fp"]) 549 | #recall = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fn"]) 550 | #writer.add_pr_curve_raw("pr_{}/{}".format(fold[0], fold[1]), 551 | # true_positive_counts=accumulated_pr["tp"], 552 | # false_positive_counts=accumulated_pr["fp"], 553 | # true_negative_counts=accumulated_pr["tn"], 554 | # false_negative_counts=accumulated_pr["fn"], 555 | # precision=precision, recall=recall, 556 | # global_step=(epoch+1)*STEPS_PER_EPOCH, num_thresholds=pr_num_thresholds) 557 | 558 | # LR Scheduler 559 | val_loss = metrics_log['unseen_activations']['val_loss']['rss'] 560 | #val_loss = metrics_log['mean_squared_error']['unseen_activations']['rss'] 561 | #scheduler.step(val_loss) 562 | scheduler.step() 563 | 564 | # PRINT STATS 565 | if not VERBOSE_TRAINING: 566 | print('[{:d}/{:d}] {}'.format(epoch+1, EPOCHS, metrics_log['training'])) 567 | else: 568 | progbar_epoch.set_postfix(dict(loss=metrics_log['training']['loss']), refresh=False) 569 | 570 | progbar_epoch.update() 571 | progbar.close() 572 | 573 | # store in history / tensorboard 574 | for fold, metrics_for_fold in metrics_log.items(): 575 | for metric_name, value in metrics_for_fold.items(): 576 | if type(value) == dict: 577 | SW_add_scalars2(writer, "{}/{}".format(fold, metric_name), value, (epoch+1)*STEPS_PER_EPOCH) 578 | for k, v in value.items(): 579 | name = "{}/{}/{}".format(fold, metric_name, k) 580 | history.setdefault(name, []).append(v) 581 | else: 582 | name = "{}/{}".format(fold, metric_name) 583 | writer.add_scalar(name, value, (epoch+1)*STEPS_PER_EPOCH) 584 | history.setdefault(name, []).append(value) 585 | 586 | # CHECKPOINTING 587 | if CHECKPOINT_BEST_MSE: 588 | mse = val_loss 589 | if best_mse is None: 590 | best_mse = mse 591 | if best_mse > mse: 592 | msg = "[{:d}/{:d}] MSE improved from {:.4f} to {:.4f} (d={:f}), saving model...".format(epoch+1, EPOCHS, best_mse, mse, best_mse-mse) 593 | if not VERBOSE_TRAINING: 594 | print(msg) 595 | else: 596 | progbar_epoch.write(msg) 597 | torch.save({ 598 | 'epoch': epoch + 1, 599 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 600 | 'mse' : mse, 601 | 'model': net.state_dict()}, '{}/net_best_mse.pth.tar'.format(OUTPUT_FOLDER)) 602 | best_mse = mse 603 | 604 | if CHECKPOINTING_EVERY_N_EPOCHS is not None: 605 | if (epoch+1) % CHECKPOINTING_EVERY_N_EPOCHS == 0: 606 | torch.save(net.state_dict(), '{}/net_step_{:06d}.pth'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 607 | 608 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 609 | if (epoch+1) % TEST_DISAGGREGATE_EVERY_N_EPOCHS == 0: 610 | scores = test_disaggregator.calculate_metrics() 611 | scores_by_metric = {} 612 | for building_i, building in scores.items(): 613 | for metric, value in building.items(): 614 | scores_by_metric.setdefault(metric, {})[building_i] = value 615 | for metric, building_d in scores_by_metric.items(): 616 | SW_add_scalars2(writer, "test_score/{}".format(metric), building_d, (epoch+1)*STEPS_PER_EPOCH) 617 | 618 | if stop_training: 619 | break 620 | 621 | # CHECKPOINTING at end 622 | torch.save({ 623 | 'epoch': epoch + 1, 624 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 625 | 'model': net.state_dict(), 626 | 'optimizer': optimizer.state_dict(), 627 | #'scheduler': scheduler.state_dict() 628 | # TODO: scheduler is not saved this way, scheduler.state_dict() does not exist 629 | }, '{}/net_step_{:06d}.pth.tar'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 630 | 631 | df = pd.DataFrame(history) 632 | df.to_csv(csvpath) 633 | 634 | for p in running_data_processes: 635 | p.stop() 636 | writer.close() 637 | 638 | #return 42 639 | return metrics_log['training']['loss'] 640 | -------------------------------------------------------------------------------- /s2p_zhang_URED_.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import pandas as pd 3 | import numpy as np 4 | 5 | import os, pickle 6 | 7 | from tqdm import tqdm 8 | from collections import OrderedDict 9 | 10 | ## PYTORCH 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.parallel 14 | import torch.backends.cudnn as cudnn 15 | import torch.optim as optim 16 | import torch.optim.lr_scheduler as lr_scheduler 17 | import torch.utils.data 18 | import torchvision.datasets as dset 19 | import torchvision.transforms as transforms 20 | import torchvision.utils as vutils 21 | from torch.autograd import Variable 22 | 23 | from tensorboardX import SummaryWriter 24 | 25 | import matplotlib 26 | if not matplotlib.is_interactive(): 27 | matplotlib.use("AGG") 28 | #import matplotlib.pyplot as plt 29 | #import matplotlib.gridspec as gridspec 30 | 31 | from neuralnilm.data.datapipeline import DataPipeline 32 | from neuralnilm.data.datathread import DataThread 33 | from neuralnilm.data.dataprocess import DataProcess 34 | from neuralnilm.data.processing import DivideBy, IndependentlyCenter, Transpose, Add, SubSequence, DownSample 35 | from neuralnilm.consts import DATA_FOLD_NAMES 36 | 37 | from disaggregator import Disaggregator 38 | from metrics import MetricsAccumulator 39 | from parallelsource import MultiprocessActivationsSource 40 | from utils import SW_add_scalars2 41 | 42 | from sacred import Experiment 43 | from sacred.observers import FileStorageObserver 44 | 45 | from dataset_final_URED_ import dataset, load_activity_data, get_sources 46 | 47 | ex = Experiment(ingredients=[dataset]) 48 | 49 | #from sacred.utils import apply_backspaces_and_linefeeds 50 | #ex.captured_out_filter = apply_backspaces_and_linefeeds 51 | 52 | @ex.config 53 | def base_experiment_config(dataset): 54 | 55 | TARGET_APPLIANCE = dataset["TARGET_APPLIANCE"] 56 | SOURCE_TYPES = ["BalancedActivityRealAggregateSource", "BalancedActivityAugmentedAggregateSource"] 57 | VALIDATION_SOURCE_TYPES = ["RandomizedSequentialSource"] 58 | 59 | SEQ_LENGTH = 599 60 | TARGET_SEQ_LENGTH = 1 61 | 62 | if TARGET_APPLIANCE == 'television': 63 | INPUT_MEAN = 200.0 64 | INPUT_STD = 400.0 65 | elif TARGET_APPLIANCE == 'microwave': 66 | INPUT_MEAN = 500.0 67 | INPUT_STD = 800.0 68 | elif TARGET_APPLIANCE == 'tumble dryer': 69 | INPUT_MEAN = 700.0 70 | INPUT_STD = 1000.0 71 | elif TARGET_APPLIANCE == 'fridge': 72 | SOURCE_TYPES = ["BalancedBuildingRealAggregateSource"] 73 | INPUT_MEAN = 200.0 74 | INPUT_STD = 400.0 75 | elif TARGET_APPLIANCE == 'kettle': 76 | INPUT_MEAN = 700.0 77 | INPUT_STD = 1000.0 78 | elif TARGET_APPLIANCE == 'washing machine': 79 | INPUT_MEAN = 400.0 80 | INPUT_STD = 700.0 81 | elif TARGET_APPLIANCE == 'dish washer': 82 | INPUT_MEAN = 700.0 83 | INPUT_STD = 1000.0 84 | 85 | MAX_TARGET_POWER = 4200 86 | 87 | TRAINING_SEED = 42 88 | VERBOSE_TRAINING = True 89 | DOWNSAMPLE_FACTOR = 0 90 | 91 | DESCRIPTION = """ Re-Impl. of Zhangs's Seq2Point 92 | """ 93 | 94 | NUM_SEQ_PER_BATCH = 64 95 | EPOCHS = 100 96 | STEPS_PER_EPOCH = 1000 97 | 98 | LEARNING_RATE = 1e-3 99 | NUM_BATCHES_FOR_VALIDATION = 64 100 | USE_CUDA = True 101 | CHECKPOINT_BEST_MSE = False 102 | CHECKPOINTING_EVERY_N_EPOCHS = None 103 | TEST_DISAGGREGATE_EVERY_N_EPOCHS = None 104 | 105 | 106 | @ex.capture 107 | def get_validation_batches(data_pipeline, VALIDATION_SOURCE_TYPES, NUM_BATCHES_FOR_VALIDATION): 108 | shortname_sources = { 109 | "BalancedActivityRealAggregateSource": "bas", 110 | "BalancedActivityAugmentedAggregateSource": "aas", 111 | "RandomizedSequentialSource": "rss", 112 | "BalancedBuildingRealAggregateSource": "bbs" 113 | } 114 | shortname_folds = { 115 | #"train": "training", 116 | #"unseen_appliances": "unseen_appliances", 117 | "unseen_activations_of_seen_appliances": "unseen_activations" 118 | } 119 | 120 | validation_batches = {} 121 | 122 | for source in VALIDATION_SOURCE_TYPES: 123 | for fold in shortname_folds: 124 | fold_batches = [] 125 | try: 126 | batch = data_pipeline.get_batch(fold=fold, source_id=source, reset_iterator=True, validation=False) 127 | fold_batches.append(batch) 128 | except KeyError: 129 | print("For fold `{}` no validation data available".format(fold)) 130 | continue 131 | 132 | for i in range(1,NUM_BATCHES_FOR_VALIDATION): 133 | batch = data_pipeline.get_batch(fold=fold, source_id=source, validation=False) 134 | if batch is None: # Here we reach the end of the validation data for the current fold. 135 | break 136 | else: 137 | fold_batches.append(batch) 138 | 139 | if i == NUM_BATCHES_FOR_VALIDATION-1: 140 | print("WARNING: Validation data may not be fully covered") 141 | 142 | validation_batches[(shortname_folds[fold],shortname_sources[source])] = fold_batches 143 | return validation_batches 144 | 145 | 146 | def disag_seq2seq(model, mains, target, input_processing, target_processing, max_target_power, n_seq_per_batch, seq_length, target_seq_length, building_i, stride, USE_CUDA, return_sequences): 147 | def apply_inverse_processing(batch, processing_steps): 148 | reversed_processing_steps = processing_steps[::-1] 149 | for step in reversed_processing_steps: 150 | batch = step.inverse(batch) 151 | 152 | return batch 153 | 154 | def apply_processing(batch, processing_steps): 155 | for step in processing_steps: 156 | batch = step(batch) 157 | 158 | return batch 159 | 160 | def mains_to_batches(mains, n_seq_per_batch, seq_length, processing_steps, stride=1): 161 | assert mains.ndim == 1 162 | n_mains_samples = len(mains) 163 | input_shape = (n_seq_per_batch, seq_length, 1) 164 | 165 | # Divide mains data into batches 166 | n_batches = (n_mains_samples / stride) / n_seq_per_batch 167 | n_batches = np.ceil(n_batches).astype(int) 168 | batches = [] 169 | for batch_i in range(n_batches): 170 | batch = np.zeros(input_shape, dtype=np.float32) 171 | batch_start = batch_i * n_seq_per_batch * stride 172 | for seq_i in range(n_seq_per_batch): 173 | mains_start_i = batch_start + (seq_i * stride) 174 | mains_end_i = mains_start_i + seq_length 175 | seq = mains[mains_start_i:mains_end_i] 176 | batch[seq_i, :len(seq), 0] = seq 177 | processed_batch = apply_processing(batch, processing_steps) 178 | batches.append(processed_batch) 179 | 180 | return batches 181 | 182 | 183 | if stride is None: 184 | stride = seq_length 185 | 186 | batches = mains_to_batches(mains, n_seq_per_batch, seq_length, input_processing, stride) 187 | estimates = np.zeros(len(mains), dtype=np.float32) 188 | offset = (seq_length - target_seq_length) // 2 189 | 190 | if return_sequences: 191 | # `estimate_windows` is array with shape [#sliding_windows_in_mains x lenth_of_window] 192 | #. it stores disag results for all sliding windows seperately 193 | # note: beware of padding. also last batch may be not be filled entirely 194 | #. therefore have some extra margin 195 | estimate_windows = np.zeros((len(batches)*n_seq_per_batch, target_seq_length), dtype=np.float32) 196 | 197 | # Iterate over each batch 198 | window_i = 0 199 | for batch_i, net_input in enumerate(batches): 200 | input = torch.from_numpy(net_input.astype(np.float32)) 201 | if USE_CUDA: 202 | input = input.cuda() 203 | with torch.no_grad(): 204 | inputv = Variable(input) 205 | output = model(inputv) 206 | net_output = output.cpu().data.numpy() 207 | net_output = apply_inverse_processing(net_output, target_processing) 208 | batch_start = (batch_i * n_seq_per_batch * stride) + offset 209 | for seq_i in range(n_seq_per_batch): 210 | start_i = batch_start + (seq_i * stride) 211 | end_i = start_i + target_seq_length 212 | n = len(estimates[start_i:end_i]) 213 | # The net output is not necessarily the same length 214 | # as the mains (because mains might not fit exactly into 215 | # the number of batches required) 216 | estimates[start_i:end_i] += net_output[seq_i, :n, 0] 217 | if return_sequences: 218 | estimate_windows[window_i, :] = net_output[seq_i, :, 0] 219 | 220 | window_i += 1 221 | 222 | n_overlaps = target_seq_length / stride 223 | estimates /= n_overlaps 224 | estimates[estimates < 0] = 0 225 | 226 | if return_sequences: 227 | return estimates, dict(sequences=estimate_windows) 228 | else: 229 | return estimates 230 | 231 | 232 | class _Seq2PointNetPaddingSame(nn.Module): 233 | def __init__(self, SEQ_LENGTH): 234 | super(_Seq2PointNetPaddingSame, self).__init__() 235 | 236 | self.seq_length = SEQ_LENGTH 237 | self.nc = 1 # number of channels 238 | 239 | self.conv1 = nn.Sequential( 240 | nn.ConstantPad1d((4, 5), 0), 241 | nn.Conv1d(self.nc, 30, 10, stride=1), 242 | nn.ReLU(True), 243 | nn.ConstantPad1d((3, 4), 0), 244 | nn.Conv1d(30, 30, 8, stride=1), 245 | nn.ReLU(True), 246 | nn.ConstantPad1d((2, 3), 0), 247 | nn.Conv1d(30, 40, 6, stride=1), 248 | nn.ReLU(True), 249 | nn.ConstantPad1d((2, 2), 0), 250 | nn.Conv1d(40, 50, 5, stride=1), 251 | nn.ReLU(True), 252 | nn.ConstantPad1d((2, 2), 0), 253 | nn.Conv1d(50, 50, 5, stride=1), 254 | nn.ReLU(True), 255 | ) 256 | 257 | self.n_dense_units = self.seq_length * 50 258 | self.dense1 = nn.Linear(self.n_dense_units, 1024) 259 | self.act1 = nn.ReLU(True) 260 | 261 | self.dense2 = nn.Linear(1024, 1) 262 | 263 | for m in self.modules(): 264 | if isinstance(m, nn.Conv1d): 265 | nn.init.xavier_uniform_(m.weight) 266 | m.bias.data.zero_() 267 | if isinstance(m, nn.Linear): 268 | nn.init.xavier_uniform_(m.weight) 269 | m.bias.data.zero_() 270 | 271 | def forward(self, input): 272 | x = self.conv1(input) 273 | x = self.dense1(x.view(-1, self.n_dense_units)) 274 | x = self.act1(x) 275 | x = self.dense2(x) 276 | x = x.view(-1, 1, 1) 277 | return x 278 | 279 | 280 | _Net = _Seq2PointNetPaddingSame 281 | 282 | 283 | def load_disaggregator(EVALUATION_DATA_PATH, MODEL_PATH, config=None, USE_CUDA=True): 284 | """ 285 | Helper function for the disaggregator script 286 | """ 287 | 288 | if config is None: 289 | config = os.path.dirname(MODEL_PATH) 290 | 291 | if type(config) == str: 292 | try: 293 | import jsonpickle 294 | with open(os.path.join(config, 'config.json'), 'r') as configfile: 295 | config = jsonpickle.decode(configfile.read()) 296 | except: 297 | return None 298 | 299 | assert(type(config) == dict) 300 | 301 | dataset = config['dataset'] 302 | SEQ_LENGTH = config['SEQ_LENGTH'] 303 | TARGET_SEQ_LENGTH = config['TARGET_SEQ_LENGTH'] 304 | TARGET_APPLIANCE = dataset['TARGET_APPLIANCE'] 305 | ON_POWER_THRESHOLD = dataset['ON_POWER_THRESHOLD'] 306 | MAX_TARGET_POWER = config['MAX_TARGET_POWER'] 307 | NUM_SEQ_PER_BATCH = config['NUM_SEQ_PER_BATCH'] 308 | INPUT_STD = config['INPUT_STD'] 309 | INPUT_MEAN = config['INPUT_MEAN'] 310 | DOWNSAMPLE_FACTOR = config['DOWNSAMPLE_FACTOR'] 311 | NUM_SEQ_PER_BATCH = 1024 # override 312 | 313 | net = _Net(SEQ_LENGTH) 314 | 315 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 316 | input_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 317 | target_processing_steps = [SubSequence(offset,-offset), Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 318 | 319 | if MODEL_PATH.endswith("/"): 320 | MODEL_PATH = MODEL_PATH + 'net_step_{:06d}.pth.tar'.format(config['EPOCHS']*config['STEPS_PER_EPOCH']) 321 | 322 | if USE_CUDA: 323 | training_state = torch.load(MODEL_PATH) 324 | else: 325 | training_state = torch.load(MODEL_PATH, map_location='cpu') 326 | 327 | if MODEL_PATH.endswith("tar"): 328 | model = training_state['model'] 329 | else: 330 | model = training_state 331 | 332 | net.load_state_dict(model) 333 | if USE_CUDA: 334 | net.cuda() 335 | 336 | return Disaggregator( 337 | EVALUATION_DATA_PATH=EVALUATION_DATA_PATH, 338 | TARGET_APPLIANCE = TARGET_APPLIANCE, 339 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 340 | MAX_TARGET_POWER = MAX_TARGET_POWER, 341 | pad_mains = True, 342 | pad_appliance = False, 343 | disagg_func = disag_seq2seq, 344 | downsample_factor = DOWNSAMPLE_FACTOR, 345 | disagg_kwargs = dict( 346 | USE_CUDA=USE_CUDA, 347 | model = net, 348 | input_processing=input_processing_steps, 349 | target_processing=target_processing_steps, 350 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 351 | seq_length = SEQ_LENGTH, 352 | target_seq_length = TARGET_SEQ_LENGTH, 353 | stride = 1 354 | ) 355 | ), training_state 356 | 357 | 358 | @ex.automain 359 | def run_experiment(dataset, 360 | INPUT_MEAN, 361 | INPUT_STD, 362 | SOURCE_TYPES, 363 | VALIDATION_SOURCE_TYPES, 364 | DOWNSAMPLE_FACTOR, 365 | SEQ_LENGTH, 366 | TARGET_SEQ_LENGTH, 367 | MAX_TARGET_POWER, 368 | TARGET_APPLIANCE, 369 | TRAINING_SEED, 370 | VERBOSE_TRAINING, 371 | LEARNING_RATE, 372 | NUM_SEQ_PER_BATCH, 373 | EPOCHS, 374 | STEPS_PER_EPOCH, 375 | USE_CUDA, 376 | CHECKPOINT_BEST_MSE, 377 | CHECKPOINTING_EVERY_N_EPOCHS, 378 | TEST_DISAGGREGATE_EVERY_N_EPOCHS, 379 | _run): 380 | 381 | torch.manual_seed(TRAINING_SEED) 382 | 383 | OUTPUT_FOLDER = os.path.join(ex.get_experiment_info()['name'],"output") 384 | for observer in _run.observers: 385 | if type(observer) is FileStorageObserver: 386 | OUTPUT_FOLDER = os.path.join(observer.basedir, str(_run._id)) 387 | VERBOSE_TRAINING = 0 388 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 389 | 390 | writer = SummaryWriter(log_dir=OUTPUT_FOLDER) 391 | 392 | # From dataset Ingredient 393 | TRAIN_BUILDINGS = dataset["TRAIN_BUILDINGS"] 394 | ON_POWER_THRESHOLD = dataset["ON_POWER_THRESHOLD"] 395 | 396 | ############################################################################################## 397 | #PREPARE DATASET (DATALOADERs) 398 | ############################################################################################## 399 | running_data_processes = [] # stop these at the end 400 | sources, validation_sources = get_sources( 401 | training_source_names=SOURCE_TYPES, 402 | validation_source_names=VALIDATION_SOURCE_TYPES, 403 | seq_length=SEQ_LENGTH, 404 | sources_seed=TRAINING_SEED, 405 | validation_stride=128 ) 406 | 407 | if DOWNSAMPLE_FACTOR > 1: 408 | downsample_rng = np.random.RandomState(TRAINING_SEED) 409 | input_processing_steps = [DownSample(DOWNSAMPLE_FACTOR, downsample_rng)] 410 | else: 411 | input_processing_steps = [] 412 | 413 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 414 | groundtruth_processing = SubSequence(offset,-offset) 415 | input_processing_steps += [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 416 | target_processing_steps = [groundtruth_processing, Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 417 | 418 | validation_pipeline = DataPipeline( 419 | sources=validation_sources, 420 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 421 | input_processing=input_processing_steps, 422 | target_processing=target_processing_steps 423 | ) 424 | validation_batches = get_validation_batches(validation_pipeline) 425 | print("appliance {} has {} validation batches".format( 426 | TARGET_APPLIANCE, 427 | sum([len(v) for k, v in validation_batches.items()]) )) 428 | 429 | data_pipeline = DataPipeline( 430 | sources=sources, 431 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 432 | input_processing=input_processing_steps, 433 | target_processing=target_processing_steps 434 | ) 435 | data_thread = DataProcess(data_pipeline) 436 | data_thread.start() 437 | running_data_processes.append(data_thread) 438 | 439 | net = _Net(SEQ_LENGTH) 440 | print(net) 441 | 442 | metrics_accu = MetricsAccumulator( 443 | on_power_threshold=ON_POWER_THRESHOLD, 444 | max_power=MAX_TARGET_POWER) 445 | 446 | # note: MSE - Mean Squared Error 447 | criterion = torch.nn.MSELoss() 448 | 449 | stop_training = False 450 | best_mse = None 451 | 452 | # PREPARE DISAGGREGATOR 453 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 454 | test_disaggregator = Disaggregator( 455 | EVALUATION_DATA_PATH = dataset['EVALUATION_DATA_PATH'], 456 | TARGET_APPLIANCE = TARGET_APPLIANCE, 457 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 458 | MAX_TARGET_POWER = MAX_TARGET_POWER, 459 | pad_mains = True, 460 | pad_appliance = False, 461 | disagg_func = disag_seq2seq, 462 | downsample_factor = DOWNSAMPLE_FACTOR, 463 | disagg_kwargs = dict( 464 | model = net, 465 | input_processing=input_processing_steps, 466 | target_processing=target_processing_steps, 467 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 468 | seq_length = SEQ_LENGTH, 469 | target_seq_length = TARGET_SEQ_LENGTH, 470 | USE_CUDA=USE_CUDA, 471 | stride = 1 472 | ) 473 | ) 474 | 475 | # PREPARE TENSORS, WHICH WILL BE FED USED DURING TRAINING AND VALIDATION 476 | input = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 477 | target = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, 1) 478 | 479 | if USE_CUDA: 480 | # note: push to GPU 481 | net.cuda() 482 | criterion.cuda() 483 | input, target = input.cuda(), target.cuda() 484 | 485 | # setup optimizer. TODO: Should we use 'Adam' for disaggregator? 486 | optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, betas=(0.9, 0.999)) 487 | #optimizer = optim.SGD(net.parameters(), momentum=0.9, nesterov=True, lr=LEARNING_RATE) 488 | scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[50,75], gamma=0.1) 489 | 490 | history = {} 491 | csvpath = os.path.join(OUTPUT_FOLDER, "history.csv") 492 | if os.path.exists(csvpath): 493 | print("Already exists: {}".format(csvpath)) 494 | return -1 495 | 496 | progbar_epoch = tqdm(desc="Epoch", total=EPOCHS, unit="epoch", disable=(not VERBOSE_TRAINING)) 497 | for epoch in range(EPOCHS): 498 | # TRAINING 499 | metrics_log = {'training':{}} 500 | training_loss = 0.0 501 | progbar = tqdm(desc="Train", total=STEPS_PER_EPOCH, leave=False, disable=(not VERBOSE_TRAINING)) 502 | for i in range(STEPS_PER_EPOCH): 503 | net.zero_grad() 504 | batch = data_thread.get_batch() 505 | while batch is None: 506 | batch = data_thread.get_batch() 507 | qsize = data_thread._queue.qsize() 508 | 509 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 510 | target_signal = torch.from_numpy(batch.after_processing.target) 511 | if USE_CUDA: 512 | aggregated_signal = aggregated_signal.cuda() 513 | target_signal = target_signal.cuda() 514 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 515 | target.resize_as_(target_signal).copy_(target_signal) 516 | inputv = Variable(input, requires_grad=False) 517 | targetv = Variable(target, requires_grad=False) 518 | output = net(inputv) 519 | loss = criterion(output, targetv) 520 | loss.backward() 521 | optimizer.step() 522 | training_loss += loss.item() 523 | 524 | progbar.set_postfix(dict( 525 | loss = "{:.4f}".format(loss.item()), 526 | qsize = qsize 527 | ), refresh=False) 528 | progbar.update() 529 | 530 | metrics_log['training']['loss'] = float(training_loss/STEPS_PER_EPOCH) 531 | metrics_log['training']['lr'] = optimizer.param_groups[0]['lr'] 532 | 533 | # VALIDATION 534 | #pr_num_thresholds = 127 535 | for fold in validation_batches: 536 | metrics_accu.reset_accumulator() 537 | #accumulated_pr = {} 538 | #for cl in ["tp", "tn", "fp", "fn"]: 539 | # accumulated_pr[cl] = torch.LongTensor(pr_num_thresholds).zero_() 540 | for batch in validation_batches[fold]: 541 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 542 | target_signal = torch.from_numpy(batch.after_processing.target) 543 | if USE_CUDA: 544 | aggregated_signal = aggregated_signal.cuda() 545 | target_signal = target_signal.cuda() 546 | with torch.no_grad(): 547 | output = net(aggregated_signal) 548 | val_loss = criterion(output, target_signal) 549 | loss_value = val_loss.item() 550 | # other metrics 551 | pred_y = data_pipeline.apply_inverse_processing(output.cpu().data.numpy(), 'target') 552 | true_y = groundtruth_processing(batch.before_processing.target) 553 | metrics_accu.accumulate_metrics(true_y, pred_y, val_loss=loss_value) 554 | #calculate_pr_curve_torch(accumulated_pr, MAX_TARGET_POWER, true_y, pred_y, num_thresholds=pr_num_thresholds) 555 | 556 | for key, value in metrics_accu.finalize_metrics().items(): 557 | metrics_log.setdefault(fold[0], {}).setdefault(key, {})[fold[1]] = value 558 | 559 | #precision = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fp"]) 560 | #recall = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fn"]) 561 | #writer.add_pr_curve_raw("pr_{}/{}".format(fold[0], fold[1]), 562 | # true_positive_counts=accumulated_pr["tp"], 563 | # false_positive_counts=accumulated_pr["fp"], 564 | # true_negative_counts=accumulated_pr["tn"], 565 | # false_negative_counts=accumulated_pr["fn"], 566 | # precision=precision, recall=recall, 567 | # global_step=(epoch+1)*STEPS_PER_EPOCH, num_thresholds=pr_num_thresholds) 568 | 569 | # LR Scheduler 570 | val_loss = metrics_log['unseen_activations']['val_loss']['rss'] 571 | #val_loss = metrics_log['mean_squared_error']['unseen_activations']['rss'] 572 | #scheduler.step(val_loss) 573 | scheduler.step() 574 | 575 | # PRINT STATS 576 | if not VERBOSE_TRAINING: 577 | print('[{:d}/{:d}] {}'.format(epoch+1, EPOCHS, metrics_log['training'])) 578 | else: 579 | progbar_epoch.set_postfix(dict(loss=metrics_log['training']['loss']), refresh=False) 580 | 581 | progbar_epoch.update() 582 | progbar.close() 583 | 584 | # store in history / tensorboard 585 | for fold, metrics_for_fold in metrics_log.items(): 586 | for metric_name, value in metrics_for_fold.items(): 587 | if type(value) == dict: 588 | SW_add_scalars2(writer, "{}/{}".format(fold, metric_name), value, (epoch+1)*STEPS_PER_EPOCH) 589 | for k, v in value.items(): 590 | name = "{}/{}/{}".format(fold, metric_name, k) 591 | history.setdefault(name, []).append(v) 592 | else: 593 | name = "{}/{}".format(fold, metric_name) 594 | writer.add_scalar(name, value, (epoch+1)*STEPS_PER_EPOCH) 595 | history.setdefault(name, []).append(value) 596 | 597 | # CHECKPOINTING 598 | if CHECKPOINT_BEST_MSE: 599 | mse = val_loss 600 | if best_mse is None: 601 | best_mse = mse 602 | if best_mse > mse: 603 | msg = "[{:d}/{:d}] MSE improved from {:.4f} to {:.4f} (d={:f}), saving model...".format(epoch+1, EPOCHS, best_mse, mse, best_mse-mse) 604 | if not VERBOSE_TRAINING: 605 | print(msg) 606 | else: 607 | progbar_epoch.write(msg) 608 | torch.save({ 609 | 'epoch': epoch + 1, 610 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 611 | 'mse' : mse, 612 | 'model': net.state_dict()}, '{}/net_best_mse.pth.tar'.format(OUTPUT_FOLDER)) 613 | best_mse = mse 614 | 615 | if CHECKPOINTING_EVERY_N_EPOCHS is not None: 616 | if (epoch+1) % CHECKPOINTING_EVERY_N_EPOCHS == 0: 617 | torch.save(net.state_dict(), '{}/net_step_{:06d}.pth'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 618 | 619 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 620 | if (epoch+1) % TEST_DISAGGREGATE_EVERY_N_EPOCHS == 0: 621 | scores = test_disaggregator.calculate_metrics() 622 | scores_by_metric = {} 623 | for building_i, building in scores.items(): 624 | for metric, value in building.items(): 625 | scores_by_metric.setdefault(metric, {})[building_i] = value 626 | for metric, building_d in scores_by_metric.items(): 627 | SW_add_scalars2(writer, "test_score/{}".format(metric), building_d, (epoch+1)*STEPS_PER_EPOCH) 628 | 629 | if stop_training: 630 | break 631 | 632 | # CHECKPOINTING at end 633 | torch.save({ 634 | 'epoch': epoch + 1, 635 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 636 | 'model': net.state_dict(), 637 | 'optimizer': optimizer.state_dict(), 638 | #'scheduler': scheduler.state_dict() 639 | # TODO: scheduler is not saved this way, scheduler.state_dict() does not exist 640 | }, '{}/net_step_{:06d}.pth.tar'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 641 | 642 | df = pd.DataFrame(history) 643 | df.to_csv(csvpath) 644 | 645 | for p in running_data_processes: 646 | p.stop() 647 | writer.close() 648 | 649 | #return 42 650 | return metrics_log['training']['loss'] 651 | -------------------------------------------------------------------------------- /dfcn_brewitt_URED_.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import pandas as pd 3 | import numpy as np 4 | 5 | import os, pickle 6 | 7 | from tqdm import tqdm 8 | from collections import OrderedDict 9 | 10 | ## PYTORCH 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.parallel 14 | import torch.backends.cudnn as cudnn 15 | import torch.optim as optim 16 | import torch.optim.lr_scheduler as lr_scheduler 17 | import torch.utils.data 18 | import torchvision.datasets as dset 19 | import torchvision.transforms as transforms 20 | import torchvision.utils as vutils 21 | from torch.autograd import Variable 22 | 23 | from tensorboardX import SummaryWriter 24 | 25 | import matplotlib 26 | if not matplotlib.is_interactive(): 27 | matplotlib.use("AGG") 28 | #import matplotlib.pyplot as plt 29 | #import matplotlib.gridspec as gridspec 30 | 31 | from neuralnilm.data.datapipeline import DataPipeline 32 | from neuralnilm.data.datathread import DataThread 33 | from neuralnilm.data.dataprocess import DataProcess 34 | from neuralnilm.data.processing import DivideBy, IndependentlyCenter, Transpose, Add, SubSequence, DownSample 35 | from neuralnilm.consts import DATA_FOLD_NAMES 36 | 37 | from disaggregator import Disaggregator 38 | from metrics import MetricsAccumulator 39 | from parallelsource import MultiprocessActivationsSource 40 | from utils import SW_add_scalars2 41 | 42 | from sacred import Experiment 43 | from sacred.observers import FileStorageObserver 44 | 45 | from dataset_final_URED_ import dataset, load_activity_data, get_sources 46 | 47 | ex = Experiment(ingredients=[dataset]) 48 | 49 | #from sacred.utils import apply_backspaces_and_linefeeds 50 | #ex.captured_out_filter = apply_backspaces_and_linefeeds 51 | 52 | @ex.config 53 | def base_experiment_config(dataset): 54 | 55 | TARGET_APPLIANCE = dataset["TARGET_APPLIANCE"] 56 | SOURCE_TYPES = ["BalancedActivityRealAggregateSource", "BalancedActivityAugmentedAggregateSource"] 57 | VALIDATION_SOURCE_TYPES = ["RandomizedSequentialSource"] 58 | 59 | TARGET_SEQ_LENGTH = 2053 60 | SEQ_LENGTH = TARGET_SEQ_LENGTH + 2052 61 | 62 | if TARGET_APPLIANCE == 'dish washer': 63 | TARGET_ON_MEAN = 1059.8963789506392 64 | elif TARGET_APPLIANCE == 'fridge': 65 | SOURCE_TYPES = ["BalancedBuildingRealAggregateSource"] 66 | TARGET_ON_MEAN = 109.11351108551025 67 | elif TARGET_APPLIANCE == 'kettle': 68 | TARGET_ON_MEAN = 2127.9581037248886 69 | elif TARGET_APPLIANCE == 'microwave': 70 | TARGET_ON_MEAN = 738.0303453717913 71 | elif TARGET_APPLIANCE == 'television': 72 | TARGET_ON_MEAN = 85.93876961299351 73 | elif TARGET_APPLIANCE == 'tumble dryer': 74 | TARGET_ON_MEAN = 1211.016840616862 75 | elif TARGET_APPLIANCE == 'washing machine': 76 | TARGET_ON_MEAN = 576.8361129760742 77 | 78 | INPUT_MEAN = 500.0 79 | INPUT_STD = 700.0 80 | 81 | MAX_TARGET_POWER = 4200 82 | 83 | TRAINING_SEED = 42 84 | VERBOSE_TRAINING = True 85 | DOWNSAMPLE_FACTOR = 0 86 | 87 | DESCRIPTION = """ Re-Impl. of Brewitts's Fully Convolutional Network 88 | """ 89 | 90 | NUM_SEQ_PER_BATCH = 64 91 | EPOCHS = 100 92 | STEPS_PER_EPOCH = 1000 93 | 94 | LEARNING_RATE = 1e-4 95 | NUM_BATCHES_FOR_VALIDATION = 64 96 | USE_CUDA = True 97 | CHECKPOINT_BEST_MSE = False 98 | CHECKPOINTING_EVERY_N_EPOCHS = None 99 | TEST_DISAGGREGATE_EVERY_N_EPOCHS = None 100 | 101 | 102 | @ex.capture 103 | def get_validation_batches(data_pipeline, VALIDATION_SOURCE_TYPES, NUM_BATCHES_FOR_VALIDATION): 104 | shortname_sources = { 105 | "BalancedActivityRealAggregateSource": "bas", 106 | "BalancedActivityAugmentedAggregateSource": "aas", 107 | "RandomizedSequentialSource": "rss", 108 | "BalancedBuildingRealAggregateSource": "bbs" 109 | } 110 | shortname_folds = { 111 | #"train": "training", 112 | #"unseen_appliances": "unseen_appliances", 113 | "unseen_activations_of_seen_appliances": "unseen_activations" 114 | } 115 | 116 | validation_batches = {} 117 | 118 | for source in VALIDATION_SOURCE_TYPES: 119 | for fold in shortname_folds: 120 | fold_batches = [] 121 | try: 122 | batch = data_pipeline.get_batch(fold=fold, source_id=source, reset_iterator=True, validation=False) 123 | fold_batches.append(batch) 124 | except KeyError: 125 | print("For fold `{}` no validation data available".format(fold)) 126 | continue 127 | 128 | for i in range(1,NUM_BATCHES_FOR_VALIDATION): 129 | batch = data_pipeline.get_batch(fold=fold, source_id=source, validation=False) 130 | if batch is None: # Here we reach the end of the validation data for the current fold. 131 | break 132 | else: 133 | fold_batches.append(batch) 134 | 135 | if i == NUM_BATCHES_FOR_VALIDATION-1: 136 | print("WARNING: Validation data may not be fully covered") 137 | 138 | validation_batches[(shortname_folds[fold],shortname_sources[source])] = fold_batches 139 | return validation_batches 140 | 141 | 142 | def disag_seq2seq(model, mains, target, input_processing, target_processing, max_target_power, n_seq_per_batch, seq_length, target_seq_length, building_i, stride, USE_CUDA, return_sequences): 143 | def apply_inverse_processing(batch, processing_steps): 144 | reversed_processing_steps = processing_steps[::-1] 145 | for step in reversed_processing_steps: 146 | batch = step.inverse(batch) 147 | 148 | return batch 149 | 150 | def apply_processing(batch, processing_steps): 151 | for step in processing_steps: 152 | batch = step(batch) 153 | 154 | return batch 155 | 156 | def mains_to_batches(mains, n_seq_per_batch, seq_length, processing_steps, stride=1): 157 | assert mains.ndim == 1 158 | n_mains_samples = len(mains) 159 | input_shape = (n_seq_per_batch, seq_length, 1) 160 | 161 | # Divide mains data into batches 162 | n_batches = (n_mains_samples / stride) / n_seq_per_batch 163 | n_batches = np.ceil(n_batches).astype(int) 164 | batches = [] 165 | for batch_i in range(n_batches): 166 | batch = np.zeros(input_shape, dtype=np.float32) 167 | batch_start = batch_i * n_seq_per_batch * stride 168 | for seq_i in range(n_seq_per_batch): 169 | mains_start_i = batch_start + (seq_i * stride) 170 | mains_end_i = mains_start_i + seq_length 171 | seq = mains[mains_start_i:mains_end_i] 172 | batch[seq_i, :len(seq), 0] = seq 173 | processed_batch = apply_processing(batch, processing_steps) 174 | batches.append(processed_batch) 175 | 176 | return batches 177 | 178 | 179 | if stride is None: 180 | stride = seq_length 181 | 182 | batches = mains_to_batches(mains, n_seq_per_batch, seq_length, input_processing, stride) 183 | estimates = np.zeros(len(mains), dtype=np.float32) 184 | offset = (seq_length - target_seq_length) // 2 185 | 186 | if return_sequences: 187 | # `estimate_windows` is array with shape [#sliding_windows_in_mains x lenth_of_window] 188 | #. it stores disag results for all sliding windows seperately 189 | # note: beware of padding. also last batch may be not be filled entirely 190 | #. therefore have some extra margin 191 | estimate_windows = np.zeros((len(batches)*n_seq_per_batch, target_seq_length), dtype=np.float32) 192 | 193 | # Iterate over each batch 194 | window_i = 0 195 | for batch_i, net_input in enumerate(batches): 196 | input = torch.from_numpy(net_input.astype(np.float32)) 197 | if USE_CUDA: 198 | input = input.cuda() 199 | with torch.no_grad(): 200 | inputv = Variable(input) 201 | output = model(inputv) 202 | net_output = output.cpu().data.numpy() 203 | net_output = apply_inverse_processing(net_output, target_processing) 204 | batch_start = (batch_i * n_seq_per_batch * stride) + offset 205 | for seq_i in range(n_seq_per_batch): 206 | start_i = batch_start + (seq_i * stride) 207 | end_i = start_i + target_seq_length 208 | n = len(estimates[start_i:end_i]) 209 | # The net output is not necessarily the same length 210 | # as the mains (because mains might not fit exactly into 211 | # the number of batches required) 212 | estimates[start_i:end_i] += net_output[seq_i, :n, 0] 213 | if return_sequences: 214 | estimate_windows[window_i, :] = net_output[seq_i, :, 0] 215 | 216 | window_i += 1 217 | 218 | n_overlaps = target_seq_length / stride 219 | estimates /= n_overlaps 220 | estimates[estimates < 0] = 0 221 | 222 | if return_sequences: 223 | return estimates, dict(sequences=estimate_windows) 224 | else: 225 | return estimates 226 | 227 | 228 | class _FCNBrewitt(nn.Module): 229 | def __init__(self): 230 | super(_FCNBrewitt, self).__init__() 231 | 232 | self.nc = 1 # number of channels 233 | self.nf = 128 234 | self.n_stacked_convs = 9 235 | 236 | self.stem = nn.Conv1d(self.nc, self.nf, 9, stride=1) 237 | 238 | convs = [nn.ReLU(True)] 239 | for i in range(self.n_stacked_convs): 240 | d = 2**(i+1) 241 | c = nn.Conv1d(self.nf, self.nf, 3, dilation=d) 242 | convs.append(c) 243 | convs.append(nn.ReLU(True)) 244 | 245 | self.stacked_convs = nn.Sequential(*convs) 246 | 247 | self.head = nn.Sequential( 248 | nn.Conv1d(self.nf, self.nf, 1), 249 | nn.ReLU(True), 250 | nn.Conv1d(self.nf, 1, 1) 251 | ) 252 | 253 | for m in self.modules(): 254 | if isinstance(m, nn.Conv1d): 255 | nn.init.xavier_uniform_(m.weight) 256 | m.bias.data.zero_() 257 | if isinstance(m, nn.Linear): 258 | nn.init.xavier_uniform_(m.weight) 259 | m.bias.data.zero_() 260 | 261 | def forward(self, input): 262 | x = self.stem(input) 263 | x = self.stacked_convs(x) 264 | x = self.head(x) 265 | return x 266 | 267 | 268 | _Net = _FCNBrewitt 269 | 270 | 271 | def load_disaggregator(EVALUATION_DATA_PATH, MODEL_PATH, config=None, USE_CUDA=True): 272 | """ 273 | Helper function for the disaggregator script 274 | """ 275 | 276 | if config is None: 277 | config = os.path.dirname(MODEL_PATH) 278 | 279 | if type(config) == str: 280 | try: 281 | import jsonpickle 282 | with open(os.path.join(config, 'config.json'), 'r') as configfile: 283 | config = jsonpickle.decode(configfile.read()) 284 | except: 285 | return None 286 | 287 | assert(type(config) == dict) 288 | 289 | dataset = config['dataset'] 290 | SEQ_LENGTH = config['SEQ_LENGTH'] 291 | TARGET_SEQ_LENGTH = config['TARGET_SEQ_LENGTH'] 292 | TARGET_APPLIANCE = dataset['TARGET_APPLIANCE'] 293 | ON_POWER_THRESHOLD = dataset['ON_POWER_THRESHOLD'] 294 | MAX_TARGET_POWER = config['MAX_TARGET_POWER'] 295 | NUM_SEQ_PER_BATCH = config['NUM_SEQ_PER_BATCH'] 296 | INPUT_STD = config['INPUT_STD'] 297 | TARGET_ON_MEAN = config['TARGET_ON_MEAN'] 298 | DOWNSAMPLE_FACTOR = config['DOWNSAMPLE_FACTOR'] 299 | #NUM_SEQ_PER_BATCH = 1024 # override 300 | 301 | net = _Net() 302 | 303 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 304 | input_processing_steps = [DivideBy(INPUT_STD), IndependentlyCenter(), Transpose((0, 2, 1))] 305 | target_processing_steps = [SubSequence(offset,offset+TARGET_SEQ_LENGTH), DivideBy(TARGET_ON_MEAN), Transpose((0, 2, 1))] 306 | 307 | if MODEL_PATH.endswith("/"): 308 | MODEL_PATH = MODEL_PATH + 'net_step_{:06d}.pth.tar'.format(config['EPOCHS']*config['STEPS_PER_EPOCH']) 309 | 310 | if USE_CUDA: 311 | training_state = torch.load(MODEL_PATH) 312 | else: 313 | training_state = torch.load(MODEL_PATH, map_location='cpu') 314 | 315 | if MODEL_PATH.endswith("tar"): 316 | model = training_state['model'] 317 | else: 318 | model = training_state 319 | 320 | net.load_state_dict(model) 321 | if USE_CUDA: 322 | net.cuda() 323 | 324 | return Disaggregator( 325 | EVALUATION_DATA_PATH=EVALUATION_DATA_PATH, 326 | TARGET_APPLIANCE = TARGET_APPLIANCE, 327 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 328 | MAX_TARGET_POWER = MAX_TARGET_POWER, 329 | pad_mains = True, 330 | pad_appliance = False, 331 | disagg_func = disag_seq2seq, 332 | downsample_factor = DOWNSAMPLE_FACTOR, 333 | disagg_kwargs = dict( 334 | USE_CUDA=USE_CUDA, 335 | model = net, 336 | input_processing=input_processing_steps, 337 | target_processing=target_processing_steps, 338 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 339 | seq_length = SEQ_LENGTH, 340 | target_seq_length = TARGET_SEQ_LENGTH, 341 | stride = TARGET_SEQ_LENGTH 342 | ) 343 | ), training_state 344 | 345 | 346 | @ex.automain 347 | def run_experiment(dataset, 348 | INPUT_MEAN, 349 | INPUT_STD, 350 | TARGET_ON_MEAN, 351 | SOURCE_TYPES, 352 | VALIDATION_SOURCE_TYPES, 353 | DOWNSAMPLE_FACTOR, 354 | SEQ_LENGTH, 355 | TARGET_SEQ_LENGTH, 356 | MAX_TARGET_POWER, 357 | TARGET_APPLIANCE, 358 | TRAINING_SEED, 359 | VERBOSE_TRAINING, 360 | LEARNING_RATE, 361 | NUM_SEQ_PER_BATCH, 362 | EPOCHS, 363 | STEPS_PER_EPOCH, 364 | USE_CUDA, 365 | CHECKPOINT_BEST_MSE, 366 | CHECKPOINTING_EVERY_N_EPOCHS, 367 | TEST_DISAGGREGATE_EVERY_N_EPOCHS, 368 | _run): 369 | 370 | torch.manual_seed(TRAINING_SEED) 371 | 372 | OUTPUT_FOLDER = os.path.join(ex.get_experiment_info()['name'],"output") 373 | for observer in _run.observers: 374 | if type(observer) is FileStorageObserver: 375 | OUTPUT_FOLDER = os.path.join(observer.basedir, str(_run._id)) 376 | VERBOSE_TRAINING = 0 377 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 378 | 379 | writer = SummaryWriter(log_dir=OUTPUT_FOLDER) 380 | 381 | # From dataset Ingredient 382 | TRAIN_BUILDINGS = dataset["TRAIN_BUILDINGS"] 383 | ON_POWER_THRESHOLD = dataset["ON_POWER_THRESHOLD"] 384 | 385 | ############################################################################################## 386 | #PREPARE DATASET (DATALOADERs) 387 | ############################################################################################## 388 | running_data_processes = [] # stop these at the end 389 | sources, validation_sources = get_sources( 390 | training_source_names=SOURCE_TYPES, 391 | validation_source_names=VALIDATION_SOURCE_TYPES, 392 | seq_length=SEQ_LENGTH, 393 | sources_seed=TRAINING_SEED, 394 | validation_stride=TARGET_SEQ_LENGTH ) 395 | 396 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 397 | input_processing_steps = [DivideBy(INPUT_STD), IndependentlyCenter(), Transpose((0, 2, 1))] 398 | groundtruth_processing = SubSequence(offset,offset+TARGET_SEQ_LENGTH) 399 | target_processing_steps = [groundtruth_processing, DivideBy(TARGET_ON_MEAN), Transpose((0, 2, 1))] 400 | 401 | if DOWNSAMPLE_FACTOR > 1: 402 | downsample_rng = np.random.RandomState(TRAINING_SEED) 403 | input_processing_steps_training = [DownSample(DOWNSAMPLE_FACTOR, downsample_rng)] + input_processing_steps 404 | else: 405 | input_processing_steps_training = input_processing_steps 406 | 407 | validation_pipeline = DataPipeline( 408 | sources=validation_sources, 409 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 410 | input_processing=input_processing_steps_training, 411 | target_processing=target_processing_steps 412 | ) 413 | validation_batches = get_validation_batches(validation_pipeline) 414 | print("appliance {} has {} validation batches".format( 415 | TARGET_APPLIANCE, 416 | sum([len(v) for k, v in validation_batches.items()]) )) 417 | 418 | data_pipeline = DataPipeline( 419 | sources=sources, 420 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 421 | input_processing=input_processing_steps_training, 422 | target_processing=target_processing_steps 423 | ) 424 | data_thread = DataProcess(data_pipeline) 425 | data_thread.start() 426 | running_data_processes.append(data_thread) 427 | 428 | net = _Net() 429 | print(net) 430 | 431 | metrics_accu = MetricsAccumulator( 432 | on_power_threshold=ON_POWER_THRESHOLD, 433 | max_power=MAX_TARGET_POWER) 434 | 435 | # note: MSE - Mean Squared Error 436 | criterion = torch.nn.MSELoss() 437 | 438 | stop_training = False 439 | best_mse = None 440 | 441 | # PREPARE DISAGGREGATOR 442 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 443 | test_disaggregator = Disaggregator( 444 | EVALUATION_DATA_PATH = dataset['EVALUATION_DATA_PATH'], 445 | TARGET_APPLIANCE = TARGET_APPLIANCE, 446 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 447 | MAX_TARGET_POWER = MAX_TARGET_POWER, 448 | pad_mains = True, 449 | pad_appliance = False, 450 | disagg_func = disag_seq2seq, 451 | downsample_factor = DOWNSAMPLE_FACTOR, 452 | disagg_kwargs = dict( 453 | model = net, 454 | input_processing=input_processing_steps, 455 | target_processing=target_processing_steps, 456 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 457 | seq_length = SEQ_LENGTH, 458 | target_seq_length = TARGET_SEQ_LENGTH, 459 | USE_CUDA=USE_CUDA, 460 | stride = TARGET_SEQ_LENGTH 461 | ) 462 | ) 463 | 464 | # PREPARE TENSORS, WHICH WILL BE FED USED DURING TRAINING AND VALIDATION 465 | input = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 466 | target = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, TARGET_SEQ_LENGTH) 467 | 468 | if USE_CUDA: 469 | # note: push to GPU 470 | net.cuda() 471 | criterion.cuda() 472 | input, target = input.cuda(), target.cuda() 473 | 474 | # Trace the Network 475 | #traced_net = torch.jit.trace(net, input) 476 | #net = traced_net 477 | 478 | # setup optimizer. TODO: Should we use 'Adam' for disaggregator? 479 | optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, betas=(0.9, 0.999)) 480 | #optimizer = optim.SGD(net.parameters(), momentum=0.9, nesterov=True, lr=LEARNING_RATE) 481 | scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[50,75], gamma=0.1) 482 | 483 | history = {} 484 | csvpath = os.path.join(OUTPUT_FOLDER, "history.csv") 485 | if os.path.exists(csvpath): 486 | print("Already exists: {}".format(csvpath)) 487 | return -1 488 | 489 | progbar_epoch = tqdm(desc="Epoch", total=EPOCHS, unit="epoch", disable=(not VERBOSE_TRAINING)) 490 | for epoch in range(EPOCHS): 491 | # TRAINING 492 | metrics_log = {'training':{}} 493 | training_loss = 0.0 494 | progbar = tqdm(desc="Train", total=STEPS_PER_EPOCH, leave=False, disable=(not VERBOSE_TRAINING)) 495 | for i in range(STEPS_PER_EPOCH): 496 | net.zero_grad() 497 | batch = data_thread.get_batch() 498 | while batch is None: 499 | batch = data_thread.get_batch() 500 | qsize = data_thread._queue.qsize() 501 | 502 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 503 | target_signal = torch.from_numpy(batch.after_processing.target) 504 | if USE_CUDA: 505 | aggregated_signal = aggregated_signal.cuda() 506 | target_signal = target_signal.cuda() 507 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 508 | target.resize_as_(target_signal).copy_(target_signal) 509 | inputv = Variable(input, requires_grad=False) 510 | targetv = Variable(target, requires_grad=False) 511 | output = net(inputv) 512 | loss = criterion(output, targetv) 513 | loss.backward() 514 | optimizer.step() 515 | training_loss += loss.item() 516 | 517 | progbar.set_postfix(dict( 518 | loss = "{:.4f}".format(loss.item()), 519 | qsize = qsize 520 | ), refresh=False) 521 | progbar.update() 522 | 523 | metrics_log['training']['loss'] = float(training_loss/STEPS_PER_EPOCH) 524 | metrics_log['training']['lr'] = optimizer.param_groups[0]['lr'] 525 | 526 | # VALIDATION 527 | #pr_num_thresholds = 127 528 | for fold in validation_batches: 529 | metrics_accu.reset_accumulator() 530 | #accumulated_pr = {} 531 | #for cl in ["tp", "tn", "fp", "fn"]: 532 | # accumulated_pr[cl] = torch.LongTensor(pr_num_thresholds).zero_() 533 | for batch in validation_batches[fold]: 534 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 535 | target_signal = torch.from_numpy(batch.after_processing.target) 536 | if USE_CUDA: 537 | aggregated_signal = aggregated_signal.cuda() 538 | target_signal = target_signal.cuda() 539 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 540 | target.resize_as_(target_signal).copy_(target_signal) 541 | with torch.no_grad(): 542 | inputv = Variable(input) 543 | targetv = Variable(target) 544 | output = net(inputv) 545 | val_loss = criterion(output, targetv) 546 | loss_value = val_loss.item() 547 | # other metrics 548 | pred_y = data_pipeline.apply_inverse_processing(output.cpu().data.numpy(), 'target') 549 | true_y = groundtruth_processing(batch.before_processing.target) 550 | metrics_accu.accumulate_metrics(true_y, pred_y, val_loss=loss_value) 551 | #calculate_pr_curve_torch(accumulated_pr, MAX_TARGET_POWER, true_y, pred_y, num_thresholds=pr_num_thresholds) 552 | 553 | for key, value in metrics_accu.finalize_metrics().items(): 554 | metrics_log.setdefault(fold[0], {}).setdefault(key, {})[fold[1]] = value 555 | 556 | #precision = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fp"]) 557 | #recall = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fn"]) 558 | #writer.add_pr_curve_raw("pr_{}/{}".format(fold[0], fold[1]), 559 | # true_positive_counts=accumulated_pr["tp"], 560 | # false_positive_counts=accumulated_pr["fp"], 561 | # true_negative_counts=accumulated_pr["tn"], 562 | # false_negative_counts=accumulated_pr["fn"], 563 | # precision=precision, recall=recall, 564 | # global_step=(epoch+1)*STEPS_PER_EPOCH, num_thresholds=pr_num_thresholds) 565 | 566 | # LR Scheduler 567 | val_loss = metrics_log['unseen_activations']['val_loss']['rss'] 568 | #val_loss = metrics_log['mean_squared_error']['unseen_activations']['rss'] 569 | #scheduler.step(val_loss) 570 | scheduler.step() 571 | 572 | # PRINT STATS 573 | if not VERBOSE_TRAINING: 574 | print('[{:d}/{:d}] {}'.format(epoch+1, EPOCHS, metrics_log['training'])) 575 | else: 576 | progbar_epoch.set_postfix(dict(loss=metrics_log['training']['loss']), refresh=False) 577 | 578 | progbar_epoch.update() 579 | progbar.close() 580 | 581 | # store in history / tensorboard 582 | for fold, metrics_for_fold in metrics_log.items(): 583 | for metric_name, value in metrics_for_fold.items(): 584 | if type(value) == dict: 585 | SW_add_scalars2(writer, "{}/{}".format(fold, metric_name), value, (epoch+1)*STEPS_PER_EPOCH) 586 | for k, v in value.items(): 587 | name = "{}/{}/{}".format(fold, metric_name, k) 588 | history.setdefault(name, []).append(v) 589 | else: 590 | name = "{}/{}".format(fold, metric_name) 591 | writer.add_scalar(name, value, (epoch+1)*STEPS_PER_EPOCH) 592 | history.setdefault(name, []).append(value) 593 | 594 | # CHECKPOINTING 595 | if CHECKPOINT_BEST_MSE: 596 | mse = val_loss 597 | if best_mse is None: 598 | best_mse = mse 599 | if best_mse > mse: 600 | msg = "[{:d}/{:d}] MSE improved from {:.4f} to {:.4f} (d={:f}), saving model...".format(epoch+1, EPOCHS, best_mse, mse, best_mse-mse) 601 | if not VERBOSE_TRAINING: 602 | print(msg) 603 | else: 604 | progbar_epoch.write(msg) 605 | torch.save({ 606 | 'epoch': epoch + 1, 607 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 608 | 'mse' : mse, 609 | 'model': net.state_dict()}, '{}/net_best_mse.pth.tar'.format(OUTPUT_FOLDER)) 610 | best_mse = mse 611 | 612 | if CHECKPOINTING_EVERY_N_EPOCHS is not None: 613 | if (epoch+1) % CHECKPOINTING_EVERY_N_EPOCHS == 0: 614 | torch.save(net.state_dict(), '{}/net_step_{:06d}.pth'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 615 | 616 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 617 | if (epoch+1) % TEST_DISAGGREGATE_EVERY_N_EPOCHS == 0: 618 | scores = test_disaggregator.calculate_metrics() 619 | scores_by_metric = {} 620 | for building_i, building in scores.items(): 621 | for metric, value in building.items(): 622 | scores_by_metric.setdefault(metric, {})[building_i] = value 623 | for metric, building_d in scores_by_metric.items(): 624 | SW_add_scalars2(writer, "test_score/{}".format(metric), building_d, (epoch+1)*STEPS_PER_EPOCH) 625 | 626 | if stop_training: 627 | break 628 | 629 | # CHECKPOINTING at end 630 | torch.save({ 631 | 'epoch': epoch + 1, 632 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 633 | 'model': net.state_dict(), 634 | 'optimizer': optimizer.state_dict(), 635 | #'scheduler': scheduler.state_dict() 636 | # TODO: scheduler is not saved this way, scheduler.state_dict() does not exist 637 | }, '{}/net_step_{:06d}.pth.tar'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 638 | 639 | df = pd.DataFrame(history) 640 | df.to_csv(csvpath) 641 | 642 | for p in running_data_processes: 643 | p.stop() 644 | writer.close() 645 | 646 | #return 42 647 | return metrics_log['training']['loss'] 648 | -------------------------------------------------------------------------------- /s2s_zhang_URED_.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import pandas as pd 3 | import numpy as np 4 | 5 | import os, pickle 6 | 7 | from tqdm import tqdm 8 | from collections import OrderedDict 9 | 10 | ## PYTORCH 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.parallel 14 | import torch.backends.cudnn as cudnn 15 | import torch.optim as optim 16 | import torch.optim.lr_scheduler as lr_scheduler 17 | import torch.utils.data 18 | import torchvision.datasets as dset 19 | import torchvision.transforms as transforms 20 | import torchvision.utils as vutils 21 | from torch.autograd import Variable 22 | 23 | from tensorboardX import SummaryWriter 24 | 25 | import matplotlib 26 | if not matplotlib.is_interactive(): 27 | matplotlib.use("AGG") 28 | #import matplotlib.pyplot as plt 29 | #import matplotlib.gridspec as gridspec 30 | 31 | from neuralnilm.data.datapipeline import DataPipeline 32 | from neuralnilm.data.datathread import DataThread 33 | from neuralnilm.data.dataprocess import DataProcess 34 | from neuralnilm.data.processing import DivideBy, IndependentlyCenter, Transpose, Add, SubSequence, DownSample 35 | from neuralnilm.consts import DATA_FOLD_NAMES 36 | 37 | from disaggregator import Disaggregator 38 | from metrics import MetricsAccumulator 39 | from parallelsource import MultiprocessActivationsSource 40 | from utils import SW_add_scalars2 41 | 42 | from sacred import Experiment 43 | from sacred.observers import FileStorageObserver 44 | 45 | from dataset_final_URED_ import dataset, load_activity_data, get_sources 46 | 47 | ex = Experiment(ingredients=[dataset]) 48 | 49 | #from sacred.utils import apply_backspaces_and_linefeeds 50 | #ex.captured_out_filter = apply_backspaces_and_linefeeds 51 | 52 | @ex.config 53 | def base_experiment_config(dataset): 54 | 55 | TARGET_APPLIANCE = dataset["TARGET_APPLIANCE"] 56 | SOURCE_TYPES = ["BalancedActivityRealAggregateSource", "BalancedActivityAugmentedAggregateSource"] 57 | VALIDATION_SOURCE_TYPES = ["RandomizedSequentialSource"] 58 | 59 | SEQ_LENGTH = 599 60 | TARGET_SEQ_LENGTH = 599 61 | 62 | if TARGET_APPLIANCE == 'television': 63 | INPUT_MEAN = 200.0 64 | INPUT_STD = 400.0 65 | elif TARGET_APPLIANCE == 'microwave': 66 | INPUT_MEAN = 500.0 67 | INPUT_STD = 800.0 68 | elif TARGET_APPLIANCE == 'tumble dryer': 69 | INPUT_MEAN = 700.0 70 | INPUT_STD = 1000.0 71 | elif TARGET_APPLIANCE == 'fridge': 72 | SOURCE_TYPES = ["BalancedBuildingRealAggregateSource"] 73 | INPUT_MEAN = 200.0 74 | INPUT_STD = 400.0 75 | elif TARGET_APPLIANCE == 'kettle': 76 | INPUT_MEAN = 700.0 77 | INPUT_STD = 1000.0 78 | elif TARGET_APPLIANCE == 'washing machine': 79 | INPUT_MEAN = 400.0 80 | INPUT_STD = 700.0 81 | elif TARGET_APPLIANCE == 'dish washer': 82 | INPUT_MEAN = 700.0 83 | INPUT_STD = 1000.0 84 | 85 | MAX_TARGET_POWER = 4200 86 | 87 | TRAINING_SEED = 42 88 | VERBOSE_TRAINING = True 89 | DOWNSAMPLE_FACTOR = 0 90 | 91 | DESCRIPTION = """ Re-Impl. of Zhangs's Seq2Seq 92 | """ 93 | 94 | NUM_SEQ_PER_BATCH = 64 95 | EPOCHS = 100 96 | STEPS_PER_EPOCH = 1000 97 | 98 | LEARNING_RATE = 1e-3 99 | NUM_BATCHES_FOR_VALIDATION = 64 100 | USE_CUDA = True 101 | CHECKPOINT_BEST_MSE = False 102 | CHECKPOINTING_EVERY_N_EPOCHS = None 103 | TEST_DISAGGREGATE_EVERY_N_EPOCHS = None 104 | 105 | 106 | @ex.capture 107 | def get_validation_batches(data_pipeline, VALIDATION_SOURCE_TYPES, NUM_BATCHES_FOR_VALIDATION): 108 | shortname_sources = { 109 | "BalancedActivityRealAggregateSource": "bas", 110 | "BalancedActivityAugmentedAggregateSource": "aas", 111 | "RandomizedSequentialSource": "rss", 112 | "BalancedBuildingRealAggregateSource": "bbs" 113 | } 114 | shortname_folds = { 115 | #"train": "training", 116 | #"unseen_appliances": "unseen_appliances", 117 | "unseen_activations_of_seen_appliances": "unseen_activations" 118 | } 119 | 120 | validation_batches = {} 121 | 122 | for source in VALIDATION_SOURCE_TYPES: 123 | for fold in shortname_folds: 124 | fold_batches = [] 125 | try: 126 | batch = data_pipeline.get_batch(fold=fold, source_id=source, reset_iterator=True, validation=False) 127 | fold_batches.append(batch) 128 | except KeyError: 129 | print("For fold `{}` no validation data available".format(fold)) 130 | continue 131 | 132 | for i in range(1,NUM_BATCHES_FOR_VALIDATION): 133 | batch = data_pipeline.get_batch(fold=fold, source_id=source, validation=False) 134 | if batch is None: # Here we reach the end of the validation data for the current fold. 135 | break 136 | else: 137 | fold_batches.append(batch) 138 | 139 | if i == NUM_BATCHES_FOR_VALIDATION-1: 140 | print("WARNING: Validation data may not be fully covered") 141 | 142 | validation_batches[(shortname_folds[fold],shortname_sources[source])] = fold_batches 143 | return validation_batches 144 | 145 | 146 | def disag_seq2seq(model, mains, target, input_processing, target_processing, max_target_power, n_seq_per_batch, seq_length, target_seq_length, building_i, stride, USE_CUDA, return_sequences): 147 | def apply_inverse_processing(batch, processing_steps): 148 | reversed_processing_steps = processing_steps[::-1] 149 | for step in reversed_processing_steps: 150 | batch = step.inverse(batch) 151 | 152 | return batch 153 | 154 | def apply_processing(batch, processing_steps): 155 | for step in processing_steps: 156 | batch = step(batch) 157 | 158 | return batch 159 | 160 | def mains_to_batches(mains, n_seq_per_batch, seq_length, processing_steps, stride=1): 161 | assert mains.ndim == 1 162 | n_mains_samples = len(mains) 163 | input_shape = (n_seq_per_batch, seq_length, 1) 164 | 165 | # Divide mains data into batches 166 | n_batches = (n_mains_samples / stride) / n_seq_per_batch 167 | n_batches = np.ceil(n_batches).astype(int) 168 | batches = [] 169 | for batch_i in range(n_batches): 170 | batch = np.zeros(input_shape, dtype=np.float32) 171 | batch_start = batch_i * n_seq_per_batch * stride 172 | for seq_i in range(n_seq_per_batch): 173 | mains_start_i = batch_start + (seq_i * stride) 174 | mains_end_i = mains_start_i + seq_length 175 | seq = mains[mains_start_i:mains_end_i] 176 | batch[seq_i, :len(seq), 0] = seq 177 | processed_batch = apply_processing(batch, processing_steps) 178 | batches.append(processed_batch) 179 | 180 | return batches 181 | 182 | 183 | if stride is None: 184 | stride = seq_length 185 | 186 | batches = mains_to_batches(mains, n_seq_per_batch, seq_length, input_processing, stride) 187 | estimates = np.zeros(len(mains), dtype=np.float32) 188 | offset = (seq_length - target_seq_length) // 2 189 | 190 | if return_sequences: 191 | # `estimate_windows` is array with shape [#sliding_windows_in_mains x lenth_of_window] 192 | #. it stores disag results for all sliding windows seperately 193 | # note: beware of padding. also last batch may be not be filled entirely 194 | #. therefore have some extra margin 195 | estimate_windows = np.zeros((len(batches)*n_seq_per_batch, target_seq_length), dtype=np.float32) 196 | 197 | # Iterate over each batch 198 | window_i = 0 199 | for batch_i, net_input in enumerate(batches): 200 | input = torch.from_numpy(net_input.astype(np.float32)) 201 | if USE_CUDA: 202 | input = input.cuda() 203 | with torch.no_grad(): 204 | inputv = Variable(input) 205 | output = model(inputv) 206 | net_output = output.cpu().data.numpy() 207 | net_output = apply_inverse_processing(net_output, target_processing) 208 | batch_start = (batch_i * n_seq_per_batch * stride) + offset 209 | for seq_i in range(n_seq_per_batch): 210 | start_i = batch_start + (seq_i * stride) 211 | end_i = start_i + target_seq_length 212 | n = len(estimates[start_i:end_i]) 213 | # The net output is not necessarily the same length 214 | # as the mains (because mains might not fit exactly into 215 | # the number of batches required) 216 | estimates[start_i:end_i] += net_output[seq_i, :n, 0] 217 | if return_sequences: 218 | estimate_windows[window_i, :] = net_output[seq_i, :, 0] 219 | 220 | window_i += 1 221 | 222 | n_overlaps = target_seq_length / stride 223 | estimates /= n_overlaps 224 | estimates[estimates < 0] = 0 225 | 226 | if return_sequences: 227 | return estimates, dict(sequences=estimate_windows) 228 | else: 229 | return estimates 230 | 231 | 232 | class _Seq2SeqNetPaddingSame(nn.Module): 233 | def __init__(self, SEQ_LENGTH): 234 | super(_Seq2SeqNetPaddingSame, self).__init__() 235 | 236 | self.seq_length = SEQ_LENGTH 237 | self.nc = 1 # number of channels 238 | 239 | self.conv1 = nn.Sequential( 240 | nn.ConstantPad1d((4, 5), 0), 241 | nn.Conv1d(self.nc, 30, 10, stride=1, bias=True), 242 | nn.ReLU(True), 243 | nn.ConstantPad1d((3, 4), 0), 244 | nn.Conv1d(30, 30, 8, stride=1, bias=True), 245 | nn.ReLU(True), 246 | nn.ConstantPad1d((2, 3), 0), 247 | nn.Conv1d(30, 40, 6, stride=1, bias=True), 248 | nn.ReLU(True), 249 | nn.ConstantPad1d((2, 2), 0), 250 | nn.Conv1d(40, 50, 5, stride=1, bias=True), 251 | nn.ReLU(True), 252 | nn.ConstantPad1d((2, 2), 0), 253 | nn.Conv1d(50, 50, 5, stride=1, bias=True), 254 | nn.ReLU(True), 255 | ) 256 | 257 | self.n_dense_units = self.seq_length * 50 258 | self.dense1 = nn.Linear(self.n_dense_units, 1024) 259 | self.act1 = nn.ReLU(True) 260 | 261 | self.dense2 = nn.Linear(1024, 599) 262 | 263 | for m in self.modules(): 264 | if isinstance(m, nn.Conv1d): 265 | nn.init.xavier_uniform_(m.weight) 266 | m.bias.data.zero_() 267 | if isinstance(m, nn.Linear): 268 | nn.init.xavier_uniform_(m.weight) 269 | m.bias.data.zero_() 270 | 271 | def forward(self, input): 272 | x = self.conv1(input) 273 | x = self.dense1(x.view(-1, self.n_dense_units)) 274 | x = self.act1(x) 275 | x = self.dense2(x) 276 | x = x.view(-1, 1, self.seq_length) 277 | return x 278 | 279 | 280 | _Net = _Seq2SeqNetPaddingSame 281 | 282 | 283 | def load_disaggregator(EVALUATION_DATA_PATH, MODEL_PATH, config=None, USE_CUDA=True): 284 | """ 285 | Helper function for the disaggregator script 286 | """ 287 | 288 | if config is None: 289 | config = os.path.dirname(MODEL_PATH) 290 | 291 | if type(config) == str: 292 | try: 293 | import jsonpickle 294 | with open(os.path.join(config, 'config.json'), 'r') as configfile: 295 | config = jsonpickle.decode(configfile.read()) 296 | except: 297 | return None 298 | 299 | assert(type(config) == dict) 300 | 301 | dataset = config['dataset'] 302 | SEQ_LENGTH = config['SEQ_LENGTH'] 303 | TARGET_SEQ_LENGTH = config['TARGET_SEQ_LENGTH'] 304 | TARGET_APPLIANCE = dataset['TARGET_APPLIANCE'] 305 | ON_POWER_THRESHOLD = dataset['ON_POWER_THRESHOLD'] 306 | MAX_TARGET_POWER = config['MAX_TARGET_POWER'] 307 | NUM_SEQ_PER_BATCH = config['NUM_SEQ_PER_BATCH'] 308 | INPUT_STD = config['INPUT_STD'] 309 | INPUT_MEAN = config['INPUT_MEAN'] 310 | DOWNSAMPLE_FACTOR = config['DOWNSAMPLE_FACTOR'] 311 | #NUM_SEQ_PER_BATCH = 1024 # override 312 | 313 | net = _Net(SEQ_LENGTH) 314 | 315 | input_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 316 | target_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 317 | 318 | if MODEL_PATH.endswith("/"): 319 | MODEL_PATH = MODEL_PATH + 'net_step_{:06d}.pth.tar'.format(config['EPOCHS']*config['STEPS_PER_EPOCH']) 320 | 321 | if USE_CUDA: 322 | training_state = torch.load(MODEL_PATH) 323 | else: 324 | training_state = torch.load(MODEL_PATH, map_location='cpu') 325 | 326 | if MODEL_PATH.endswith("tar"): 327 | model = training_state['model'] 328 | else: 329 | model = training_state 330 | 331 | net.load_state_dict(model) 332 | if USE_CUDA: 333 | net.cuda() 334 | 335 | return Disaggregator( 336 | EVALUATION_DATA_PATH=EVALUATION_DATA_PATH, 337 | TARGET_APPLIANCE = TARGET_APPLIANCE, 338 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 339 | MAX_TARGET_POWER = MAX_TARGET_POWER, 340 | pad_mains = True, 341 | pad_appliance = False, 342 | disagg_func = disag_seq2seq, 343 | downsample_factor = DOWNSAMPLE_FACTOR, 344 | disagg_kwargs = dict( 345 | USE_CUDA=USE_CUDA, 346 | model = net, 347 | input_processing=input_processing_steps, 348 | target_processing=target_processing_steps, 349 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 350 | seq_length = SEQ_LENGTH, 351 | target_seq_length = TARGET_SEQ_LENGTH, 352 | stride = 1 353 | ) 354 | ), training_state 355 | 356 | 357 | @ex.automain 358 | def run_experiment(dataset, 359 | INPUT_MEAN, 360 | INPUT_STD, 361 | SOURCE_TYPES, 362 | VALIDATION_SOURCE_TYPES, 363 | DOWNSAMPLE_FACTOR, 364 | SEQ_LENGTH, 365 | TARGET_SEQ_LENGTH, 366 | MAX_TARGET_POWER, 367 | TARGET_APPLIANCE, 368 | TRAINING_SEED, 369 | VERBOSE_TRAINING, 370 | LEARNING_RATE, 371 | NUM_SEQ_PER_BATCH, 372 | EPOCHS, 373 | STEPS_PER_EPOCH, 374 | USE_CUDA, 375 | CHECKPOINT_BEST_MSE, 376 | CHECKPOINTING_EVERY_N_EPOCHS, 377 | TEST_DISAGGREGATE_EVERY_N_EPOCHS, 378 | _run): 379 | 380 | torch.manual_seed(TRAINING_SEED) 381 | 382 | OUTPUT_FOLDER = os.path.join(ex.get_experiment_info()['name'],"output") 383 | for observer in _run.observers: 384 | if type(observer) is FileStorageObserver: 385 | OUTPUT_FOLDER = os.path.join(observer.basedir, str(_run._id)) 386 | VERBOSE_TRAINING = 0 387 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 388 | 389 | writer = SummaryWriter(log_dir=OUTPUT_FOLDER) 390 | 391 | # From dataset Ingredient 392 | TRAIN_BUILDINGS = dataset["TRAIN_BUILDINGS"] 393 | ON_POWER_THRESHOLD = dataset["ON_POWER_THRESHOLD"] 394 | 395 | ############################################################################################## 396 | #PREPARE DATASET (DATALOADERs) 397 | ############################################################################################## 398 | running_data_processes = [] # stop these at the end 399 | sources, validation_sources = get_sources( 400 | training_source_names=SOURCE_TYPES, 401 | validation_source_names=VALIDATION_SOURCE_TYPES, 402 | seq_length=SEQ_LENGTH, 403 | sources_seed=TRAINING_SEED, 404 | validation_stride=128 ) 405 | 406 | input_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 407 | target_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 408 | 409 | if DOWNSAMPLE_FACTOR > 1: 410 | downsample_rng = np.random.RandomState(TRAINING_SEED) 411 | input_processing_steps_training = [DownSample(DOWNSAMPLE_FACTOR, downsample_rng)] + input_processing_steps 412 | else: 413 | input_processing_steps_training = input_processing_steps 414 | 415 | validation_pipeline = DataPipeline( 416 | sources=validation_sources, 417 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 418 | input_processing=input_processing_steps_training, 419 | target_processing=target_processing_steps 420 | ) 421 | validation_batches = get_validation_batches(validation_pipeline) 422 | print("appliance {} has {} validation batches".format( 423 | TARGET_APPLIANCE, 424 | sum([len(v) for k, v in validation_batches.items()]) )) 425 | 426 | data_pipeline = DataPipeline( 427 | sources=sources, 428 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 429 | input_processing=input_processing_steps_training, 430 | target_processing=target_processing_steps 431 | ) 432 | data_thread = DataProcess(data_pipeline) 433 | data_thread.start() 434 | running_data_processes.append(data_thread) 435 | 436 | net = _Net(SEQ_LENGTH) 437 | print(net) 438 | 439 | metrics_accu = MetricsAccumulator( 440 | on_power_threshold=ON_POWER_THRESHOLD, 441 | max_power=MAX_TARGET_POWER) 442 | 443 | # note: MSE - Mean Squared Error 444 | criterion = torch.nn.MSELoss() 445 | 446 | stop_training = False 447 | best_mse = None 448 | 449 | # PREPARE TEST DISAGGREGATOR 450 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 451 | test_disaggregator = Disaggregator( 452 | EVALUATION_DATA_PATH = dataset['EVALUATION_DATA_PATH'], 453 | TARGET_APPLIANCE = TARGET_APPLIANCE, 454 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 455 | MAX_TARGET_POWER = MAX_TARGET_POWER, 456 | pad_mains = True, 457 | pad_appliance = False, 458 | disagg_func = disag_seq2seq, 459 | downsample_factor = DOWNSAMPLE_FACTOR, 460 | disagg_kwargs = dict( 461 | model = net, 462 | input_processing=input_processing_steps, 463 | target_processing=target_processing_steps, 464 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 465 | seq_length = SEQ_LENGTH, 466 | target_seq_length = TARGET_SEQ_LENGTH, 467 | USE_CUDA=USE_CUDA, 468 | stride = 1 469 | ) 470 | ) 471 | 472 | # PREPARE TENSORS, WHICH WILL BE FED USED DURING TRAINING AND VALIDATION 473 | input = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 474 | target = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, TARGET_SEQ_LENGTH) 475 | 476 | if USE_CUDA: 477 | # note: push to GPU 478 | net.cuda() 479 | criterion.cuda() 480 | input, target = input.cuda(), target.cuda() 481 | 482 | # setup optimizer. TODO: Should we use 'Adam' for disaggregator? 483 | optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, betas=(0.9, 0.999)) 484 | #optimizer = optim.SGD(net.parameters(), momentum=0.9, nesterov=True, lr=LEARNING_RATE) 485 | scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[50,75], gamma=0.1) 486 | 487 | history = {} 488 | csvpath = os.path.join(OUTPUT_FOLDER, "history.csv") 489 | if os.path.exists(csvpath): 490 | print("Already exists: {}".format(csvpath)) 491 | return -1 492 | 493 | progbar_epoch = tqdm(desc="Epoch", total=EPOCHS, unit="epoch", disable=(not VERBOSE_TRAINING)) 494 | for epoch in range(EPOCHS): 495 | # TRAINING 496 | metrics_log = {'training':{}} 497 | training_loss = 0.0 498 | progbar = tqdm(desc="Train", total=STEPS_PER_EPOCH, leave=False, disable=(not VERBOSE_TRAINING)) 499 | for i in range(STEPS_PER_EPOCH): 500 | net.zero_grad() 501 | batch = data_thread.get_batch() 502 | while batch is None: 503 | batch = data_thread.get_batch() 504 | qsize = data_thread._queue.qsize() 505 | 506 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 507 | target_signal = torch.from_numpy(batch.after_processing.target) 508 | if USE_CUDA: 509 | aggregated_signal = aggregated_signal.cuda() 510 | target_signal = target_signal.cuda() 511 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 512 | target.resize_as_(target_signal).copy_(target_signal) 513 | inputv = Variable(input, requires_grad=False) 514 | targetv = Variable(target, requires_grad=False) 515 | output = net(inputv) 516 | loss = criterion(output, targetv) 517 | loss.backward() 518 | optimizer.step() 519 | training_loss += loss.item() 520 | 521 | progbar.set_postfix(dict( 522 | loss = "{:.4f}".format(loss.item()), 523 | qsize = qsize 524 | ), refresh=False) 525 | progbar.update() 526 | 527 | metrics_log['training']['loss'] = float(training_loss/STEPS_PER_EPOCH) 528 | metrics_log['training']['lr'] = optimizer.param_groups[0]['lr'] 529 | 530 | # VALIDATION 531 | #pr_num_thresholds = 127 532 | for fold in validation_batches: 533 | metrics_accu.reset_accumulator() 534 | #accumulated_pr = {} 535 | #for cl in ["tp", "tn", "fp", "fn"]: 536 | # accumulated_pr[cl] = torch.LongTensor(pr_num_thresholds).zero_() 537 | for batch in validation_batches[fold]: 538 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 539 | target_signal = torch.from_numpy(batch.after_processing.target) 540 | if USE_CUDA: 541 | aggregated_signal = aggregated_signal.cuda() 542 | target_signal = target_signal.cuda() 543 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 544 | target.resize_as_(target_signal).copy_(target_signal) 545 | with torch.no_grad(): 546 | inputv = Variable(input) 547 | targetv = Variable(target) 548 | output = net(inputv) 549 | val_loss = criterion(output, targetv) 550 | loss_value = val_loss.item() 551 | # other metrics 552 | pred_y = data_pipeline.apply_inverse_processing(output.cpu().data.numpy(), 'target') 553 | true_y = batch.before_processing.target 554 | metrics_accu.accumulate_metrics(true_y, pred_y, val_loss=loss_value) 555 | #calculate_pr_curve_torch(accumulated_pr, MAX_TARGET_POWER, true_y, pred_y, num_thresholds=pr_num_thresholds) 556 | 557 | for key, value in metrics_accu.finalize_metrics().items(): 558 | metrics_log.setdefault(fold[0], {}).setdefault(key, {})[fold[1]] = value 559 | 560 | #precision = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fp"]) 561 | #recall = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fn"]) 562 | #writer.add_pr_curve_raw("pr_{}/{}".format(fold[0], fold[1]), 563 | # true_positive_counts=accumulated_pr["tp"], 564 | # false_positive_counts=accumulated_pr["fp"], 565 | # true_negative_counts=accumulated_pr["tn"], 566 | # false_negative_counts=accumulated_pr["fn"], 567 | # precision=precision, recall=recall, 568 | # global_step=(epoch+1)*STEPS_PER_EPOCH, num_thresholds=pr_num_thresholds) 569 | 570 | # LR Scheduler 571 | val_loss = metrics_log['unseen_activations']['val_loss']['rss'] 572 | #val_loss = metrics_log['mean_squared_error']['unseen_activations']['rss'] 573 | #scheduler.step(val_loss) 574 | scheduler.step() 575 | 576 | # PRINT STATS 577 | if not VERBOSE_TRAINING: 578 | print('[{:d}/{:d}] {}'.format(epoch+1, EPOCHS, metrics_log['training'])) 579 | else: 580 | progbar_epoch.set_postfix(dict(loss=metrics_log['training']['loss']), refresh=False) 581 | 582 | progbar_epoch.update() 583 | progbar.close() 584 | 585 | # store in history / tensorboard 586 | for fold, metrics_for_fold in metrics_log.items(): 587 | for metric_name, value in metrics_for_fold.items(): 588 | if type(value) == dict: 589 | SW_add_scalars2(writer, "{}/{}".format(fold, metric_name), value, (epoch+1)*STEPS_PER_EPOCH) 590 | for k, v in value.items(): 591 | name = "{}/{}/{}".format(fold, metric_name, k) 592 | history.setdefault(name, []).append(v) 593 | else: 594 | name = "{}/{}".format(fold, metric_name) 595 | writer.add_scalar(name, value, (epoch+1)*STEPS_PER_EPOCH) 596 | history.setdefault(name, []).append(value) 597 | 598 | # CHECKPOINTING 599 | if CHECKPOINT_BEST_MSE: 600 | mse = val_loss 601 | if best_mse is None: 602 | best_mse = mse 603 | if best_mse > mse: 604 | msg = "[{:d}/{:d}] MSE improved from {:.4f} to {:.4f} (d={:f}), saving model...".format(epoch+1, EPOCHS, best_mse, mse, best_mse-mse) 605 | if not VERBOSE_TRAINING: 606 | print(msg) 607 | else: 608 | progbar_epoch.write(msg) 609 | torch.save({ 610 | 'epoch': epoch + 1, 611 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 612 | 'mse' : mse, 613 | 'model': net.state_dict()}, '{}/net_best_mse.pth.tar'.format(OUTPUT_FOLDER)) 614 | best_mse = mse 615 | 616 | if CHECKPOINTING_EVERY_N_EPOCHS is not None: 617 | if (epoch+1) % CHECKPOINTING_EVERY_N_EPOCHS == 0: 618 | torch.save(net.state_dict(), '{}/net_step_{:06d}.pth'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 619 | 620 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 621 | if (epoch+1) % TEST_DISAGGREGATE_EVERY_N_EPOCHS == 0: 622 | scores = test_disaggregator.calculate_metrics() 623 | scores_by_metric = {} 624 | for building_i, building in scores.items(): 625 | for metric, value in building.items(): 626 | scores_by_metric.setdefault(metric, {})[building_i] = value 627 | for metric, building_d in scores_by_metric.items(): 628 | SW_add_scalars2(writer, "test_score/{}".format(metric), building_d, (epoch+1)*STEPS_PER_EPOCH) 629 | 630 | if stop_training: 631 | break 632 | 633 | # CHECKPOINTING at end 634 | torch.save({ 635 | 'epoch': epoch + 1, 636 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 637 | 'model': net.state_dict(), 638 | 'optimizer': optimizer.state_dict(), 639 | #'scheduler': scheduler.state_dict() 640 | # TODO: scheduler is not saved this way, scheduler.state_dict() does not exist 641 | }, '{}/net_step_{:06d}.pth.tar'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 642 | 643 | df = pd.DataFrame(history) 644 | df.to_csv(csvpath) 645 | 646 | for p in running_data_processes: 647 | p.stop() 648 | writer.close() 649 | 650 | #return 42 651 | return metrics_log['training']['loss'] 652 | -------------------------------------------------------------------------------- /glu_chen_URED_.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import pandas as pd 3 | import numpy as np 4 | 5 | import os, pickle 6 | 7 | from tqdm import tqdm 8 | from collections import OrderedDict 9 | 10 | ## PYTORCH 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.parallel 14 | import torch.backends.cudnn as cudnn 15 | import torch.optim as optim 16 | import torch.optim.lr_scheduler as lr_scheduler 17 | import torch.utils.data 18 | import torchvision.datasets as dset 19 | import torchvision.transforms as transforms 20 | import torchvision.utils as vutils 21 | from torch.autograd import Variable 22 | 23 | from tensorboardX import SummaryWriter 24 | 25 | import matplotlib 26 | if not matplotlib.is_interactive(): 27 | matplotlib.use("AGG") 28 | #import matplotlib.pyplot as plt 29 | #import matplotlib.gridspec as gridspec 30 | 31 | from neuralnilm.data.datapipeline import DataPipeline 32 | from neuralnilm.data.datathread import DataThread 33 | from neuralnilm.data.dataprocess import DataProcess 34 | from neuralnilm.data.processing import DivideBy, IndependentlyCenter, Transpose, Add, SubSequence, DownSample 35 | from neuralnilm.consts import DATA_FOLD_NAMES 36 | 37 | from disaggregator import Disaggregator 38 | from metrics import MetricsAccumulator 39 | from parallelsource import MultiprocessActivationsSource 40 | from utils import SW_add_scalars2 41 | 42 | from sacred import Experiment 43 | from sacred.observers import FileStorageObserver 44 | 45 | from dataset_final_URED_ import dataset, load_activity_data, get_sources 46 | 47 | ex = Experiment(ingredients=[dataset]) 48 | 49 | #from sacred.utils import apply_backspaces_and_linefeeds 50 | #ex.captured_out_filter = apply_backspaces_and_linefeeds 51 | 52 | @ex.config 53 | def base_experiment_config(dataset): 54 | 55 | TARGET_APPLIANCE = dataset["TARGET_APPLIANCE"] 56 | SOURCE_TYPES = ["BalancedActivityRealAggregateSource", "BalancedActivityAugmentedAggregateSource"] 57 | VALIDATION_SOURCE_TYPES = ["RandomizedSequentialSource"] 58 | 59 | SEQ_LENGTH = 800 60 | TARGET_SEQ_LENGTH = 100 61 | 62 | INPUT_STD = 1000.0 63 | if TARGET_APPLIANCE == 'television': 64 | TARGET_STD = 400.0 65 | elif TARGET_APPLIANCE == 'microwave': 66 | TARGET_STD = 800.0 67 | elif TARGET_APPLIANCE == 'tumble dryer': 68 | TARGET_STD = 1500.0 69 | elif TARGET_APPLIANCE == 'fridge': 70 | SOURCE_TYPES = ["BalancedBuildingRealAggregateSource"] 71 | TARGET_STD = 500.0 72 | elif TARGET_APPLIANCE == 'kettle': 73 | TARGET_STD = 2000.0 74 | elif TARGET_APPLIANCE == 'washing machine': 75 | TARGET_STD = 700.0 76 | elif TARGET_APPLIANCE == 'dish washer': 77 | TARGET_STD = 1400.0 78 | 79 | MAX_TARGET_POWER = 4200 80 | 81 | TRAINING_SEED = 42 82 | VERBOSE_TRAINING = True 83 | DOWNSAMPLE_FACTOR = 0 84 | 85 | DESCRIPTION = """ Re-Impl. of Chen's Seq2Seq with GLUs 86 | """ 87 | 88 | NUM_SEQ_PER_BATCH = 64 89 | EPOCHS = 100 90 | STEPS_PER_EPOCH = 1000 91 | 92 | LEARNING_RATE = 1e-3 93 | NUM_BATCHES_FOR_VALIDATION = 64 94 | USE_CUDA = True 95 | CHECKPOINT_BEST_MSE = False 96 | CHECKPOINTING_EVERY_N_EPOCHS = None 97 | TEST_DISAGGREGATE_EVERY_N_EPOCHS = None 98 | 99 | 100 | @ex.capture 101 | def get_validation_batches(data_pipeline, VALIDATION_SOURCE_TYPES, NUM_BATCHES_FOR_VALIDATION): 102 | shortname_sources = { 103 | "BalancedActivityRealAggregateSource": "bas", 104 | "BalancedActivityAugmentedAggregateSource": "aas", 105 | "RandomizedSequentialSource": "rss", 106 | "BalancedBuildingRealAggregateSource": "bbs" 107 | } 108 | shortname_folds = { 109 | #"train": "training", 110 | #"unseen_appliances": "unseen_appliances", 111 | "unseen_activations_of_seen_appliances": "unseen_activations" 112 | } 113 | 114 | validation_batches = {} 115 | 116 | for source in VALIDATION_SOURCE_TYPES: 117 | for fold in shortname_folds: 118 | fold_batches = [] 119 | try: 120 | batch = data_pipeline.get_batch(fold=fold, source_id=source, reset_iterator=True, validation=False) 121 | fold_batches.append(batch) 122 | except KeyError: 123 | print("For fold `{}` no validation data available".format(fold)) 124 | continue 125 | 126 | for i in range(1,NUM_BATCHES_FOR_VALIDATION): 127 | batch = data_pipeline.get_batch(fold=fold, source_id=source, validation=False) 128 | if batch is None: # Here we reach the end of the validation data for the current fold. 129 | break 130 | else: 131 | fold_batches.append(batch) 132 | 133 | if i == NUM_BATCHES_FOR_VALIDATION-1: 134 | print("WARNING: Validation data may not be fully covered") 135 | 136 | validation_batches[(shortname_folds[fold],shortname_sources[source])] = fold_batches 137 | return validation_batches 138 | 139 | 140 | def disag_seq2seq(model, mains, target, input_processing, target_processing, max_target_power, n_seq_per_batch, seq_length, target_seq_length, building_i, stride, USE_CUDA, return_sequences): 141 | def apply_inverse_processing(batch, processing_steps): 142 | reversed_processing_steps = processing_steps[::-1] 143 | for step in reversed_processing_steps: 144 | batch = step.inverse(batch) 145 | 146 | return batch 147 | 148 | def apply_processing(batch, processing_steps): 149 | for step in processing_steps: 150 | batch = step(batch) 151 | 152 | return batch 153 | 154 | def mains_to_batches(mains, n_seq_per_batch, seq_length, processing_steps, stride=1): 155 | assert mains.ndim == 1 156 | n_mains_samples = len(mains) 157 | input_shape = (n_seq_per_batch, seq_length, 1) 158 | 159 | # Divide mains data into batches 160 | n_batches = (n_mains_samples / stride) / n_seq_per_batch 161 | n_batches = np.ceil(n_batches).astype(int) 162 | batches = [] 163 | for batch_i in range(n_batches): 164 | batch = np.zeros(input_shape, dtype=np.float32) 165 | batch_start = batch_i * n_seq_per_batch * stride 166 | for seq_i in range(n_seq_per_batch): 167 | mains_start_i = batch_start + (seq_i * stride) 168 | mains_end_i = mains_start_i + seq_length 169 | seq = mains[mains_start_i:mains_end_i] 170 | batch[seq_i, :len(seq), 0] = seq 171 | processed_batch = apply_processing(batch, processing_steps) 172 | batches.append(processed_batch) 173 | 174 | return batches 175 | 176 | 177 | if stride is None: 178 | stride = seq_length 179 | 180 | batches = mains_to_batches(mains, n_seq_per_batch, seq_length, input_processing, stride) 181 | estimates = np.zeros(len(mains), dtype=np.float32) 182 | offset = (seq_length - target_seq_length) // 2 183 | 184 | if return_sequences: 185 | # `estimate_windows` is array with shape [#sliding_windows_in_mains x lenth_of_window] 186 | #. it stores disag results for all sliding windows seperately 187 | # note: beware of padding. also last batch may be not be filled entirely 188 | #. therefore have some extra margin 189 | estimate_windows = np.zeros((len(batches)*n_seq_per_batch, target_seq_length), dtype=np.float32) 190 | 191 | # Iterate over each batch 192 | window_i = 0 193 | for batch_i, net_input in enumerate(batches): 194 | input = torch.from_numpy(net_input.astype(np.float32)) 195 | if USE_CUDA: 196 | input = input.cuda() 197 | with torch.no_grad(): 198 | inputv = Variable(input) 199 | output = model(inputv) 200 | net_output = output.cpu().data.numpy() 201 | net_output = apply_inverse_processing(net_output, target_processing) 202 | batch_start = (batch_i * n_seq_per_batch * stride) + offset 203 | for seq_i in range(n_seq_per_batch): 204 | start_i = batch_start + (seq_i * stride) 205 | end_i = start_i + target_seq_length 206 | n = len(estimates[start_i:end_i]) 207 | # The net output is not necessarily the same length 208 | # as the mains (because mains might not fit exactly into 209 | # the number of batches required) 210 | estimates[start_i:end_i] += net_output[seq_i, :n, 0] 211 | if return_sequences: 212 | estimate_windows[window_i, :] = net_output[seq_i, :, 0] 213 | 214 | window_i += 1 215 | 216 | n_overlaps = target_seq_length / stride 217 | estimates /= n_overlaps 218 | estimates[estimates < 0] = 0 219 | 220 | if return_sequences: 221 | return estimates, dict(sequences=estimate_windows) 222 | else: 223 | return estimates 224 | 225 | 226 | class _GLUBlock(nn.Module): 227 | def __init__(self, n_c_in, n_c_out): 228 | super(_GLUBlock, self).__init__() 229 | self.pad = nn.ConstantPad1d((1, 2), 0) 230 | self.conv_data = nn.Conv1d(n_c_in, n_c_out, 4, stride=1, bias=True) 231 | self.conv_gate = nn.Conv1d(n_c_in, n_c_out, 4, stride=1, bias=True) 232 | self.sigmoid = nn.Sigmoid() 233 | self.pool = nn.MaxPool1d(2) 234 | 235 | def forward(self, input): 236 | padded_input = self.pad(input) 237 | x = self.conv_data(padded_input) 238 | g = self.conv_gate(padded_input) 239 | x = self.sigmoid(g)*x 240 | x = self.pool(x) 241 | return x 242 | 243 | class _ResBlock(nn.Module): 244 | def __init__(self, n_inoutput, n_hidden): 245 | super(_ResBlock, self).__init__() 246 | self.residual = nn.Sequential( 247 | nn.Linear(n_inoutput, n_hidden), 248 | nn.ReLU(True), 249 | nn.Linear(n_hidden, n_inoutput) 250 | ) 251 | 252 | def forward(self, input): 253 | x = self.residual(input) 254 | x = x + input 255 | return x 256 | 257 | class _Seq2SeqChen(nn.Module): 258 | def __init__(self, SEQ_LENGTH, TARGET_SEQ_LENGTH): 259 | super(_Seq2SeqChen, self).__init__() 260 | 261 | self.seq_length = SEQ_LENGTH 262 | self.target_seq_length = TARGET_SEQ_LENGTH 263 | self.nc = 1 # number of channels 264 | self.n_dense_input = (SEQ_LENGTH//8)*100 265 | 266 | self.glunet = nn.Sequential( 267 | _GLUBlock(1, 100), 268 | _GLUBlock(100, 100), 269 | _GLUBlock(100, 100), 270 | ) 271 | 272 | self.fc1 = nn.Linear(self.n_dense_input,100) 273 | self.act1 = nn.ReLU(True) 274 | 275 | self.resnet = nn.Sequential( 276 | _ResBlock(100, 50), 277 | _ResBlock(100, 50), 278 | _ResBlock(100, 50), 279 | ) 280 | 281 | self.fc2 = nn.Linear(100, 100) 282 | 283 | for m in self.modules(): 284 | if isinstance(m, nn.Conv1d): 285 | nn.init.xavier_uniform_(m.weight) 286 | m.bias.data.zero_() 287 | if isinstance(m, nn.Linear): 288 | nn.init.xavier_uniform_(m.weight) 289 | m.bias.data.zero_() 290 | 291 | def forward(self, input): 292 | x = self.glunet(input) 293 | x = self.fc1(x.view(-1, self.n_dense_input)) 294 | x = self.act1(x) 295 | x = self.resnet(x) 296 | x = self.fc2(x) 297 | x = x.view(-1, 1, self.target_seq_length) 298 | return x 299 | 300 | 301 | _Net = _Seq2SeqChen 302 | 303 | 304 | def load_disaggregator(EVALUATION_DATA_PATH, MODEL_PATH, config=None, USE_CUDA=True): 305 | """ 306 | Helper function for the disaggregator script 307 | """ 308 | 309 | if config is None: 310 | config = os.path.dirname(MODEL_PATH) 311 | 312 | if type(config) == str: 313 | try: 314 | import jsonpickle 315 | with open(os.path.join(config, 'config.json'), 'r') as configfile: 316 | config = jsonpickle.decode(configfile.read()) 317 | except: 318 | return None 319 | 320 | assert(type(config) == dict) 321 | 322 | dataset = config['dataset'] 323 | SEQ_LENGTH = config['SEQ_LENGTH'] 324 | TARGET_SEQ_LENGTH = config['TARGET_SEQ_LENGTH'] 325 | TARGET_APPLIANCE = dataset['TARGET_APPLIANCE'] 326 | ON_POWER_THRESHOLD = dataset['ON_POWER_THRESHOLD'] 327 | MAX_TARGET_POWER = config['MAX_TARGET_POWER'] 328 | NUM_SEQ_PER_BATCH = config['NUM_SEQ_PER_BATCH'] 329 | INPUT_STD = config['INPUT_STD'] 330 | TARGET_STD = config['TARGET_STD'] 331 | DOWNSAMPLE_FACTOR = config['DOWNSAMPLE_FACTOR'] 332 | #NUM_SEQ_PER_BATCH = 1024 # override 333 | 334 | net = _Net(SEQ_LENGTH, TARGET_SEQ_LENGTH) 335 | 336 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 337 | input_processing_steps = [DivideBy(INPUT_STD), Transpose((0, 2, 1))] 338 | target_processing_steps = [SubSequence(offset,-offset), DivideBy(TARGET_STD), Transpose((0, 2, 1))] 339 | 340 | if MODEL_PATH.endswith("/"): 341 | MODEL_PATH = MODEL_PATH + 'net_step_{:06d}.pth.tar'.format(config['EPOCHS']*config['STEPS_PER_EPOCH']) 342 | 343 | if USE_CUDA: 344 | training_state = torch.load(MODEL_PATH) 345 | else: 346 | training_state = torch.load(MODEL_PATH, map_location='cpu') 347 | 348 | if MODEL_PATH.endswith("tar"): 349 | model = training_state['model'] 350 | else: 351 | model = training_state 352 | 353 | net.load_state_dict(model) 354 | if USE_CUDA: 355 | net.cuda() 356 | 357 | return Disaggregator( 358 | EVALUATION_DATA_PATH=EVALUATION_DATA_PATH, 359 | TARGET_APPLIANCE = TARGET_APPLIANCE, 360 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 361 | MAX_TARGET_POWER = MAX_TARGET_POWER, 362 | pad_mains = True, 363 | pad_appliance = False, 364 | disagg_func = disag_seq2seq, 365 | disagg_kwargs = dict( 366 | USE_CUDA=USE_CUDA, 367 | model = net, 368 | input_processing=input_processing_steps, 369 | target_processing=target_processing_steps, 370 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 371 | seq_length = SEQ_LENGTH, 372 | target_seq_length = TARGET_SEQ_LENGTH, 373 | stride = 5 374 | ) 375 | ), training_state 376 | 377 | 378 | @ex.automain 379 | def run_experiment(dataset, 380 | INPUT_STD, 381 | TARGET_STD, 382 | SOURCE_TYPES, 383 | VALIDATION_SOURCE_TYPES, 384 | DOWNSAMPLE_FACTOR, 385 | SEQ_LENGTH, 386 | TARGET_SEQ_LENGTH, 387 | MAX_TARGET_POWER, 388 | TARGET_APPLIANCE, 389 | TRAINING_SEED, 390 | VERBOSE_TRAINING, 391 | LEARNING_RATE, 392 | NUM_SEQ_PER_BATCH, 393 | EPOCHS, 394 | STEPS_PER_EPOCH, 395 | USE_CUDA, 396 | CHECKPOINT_BEST_MSE, 397 | CHECKPOINTING_EVERY_N_EPOCHS, 398 | TEST_DISAGGREGATE_EVERY_N_EPOCHS, 399 | _run): 400 | 401 | torch.manual_seed(TRAINING_SEED) 402 | 403 | OUTPUT_FOLDER = os.path.join(ex.get_experiment_info()['name'],"output") 404 | for observer in _run.observers: 405 | if type(observer) is FileStorageObserver: 406 | OUTPUT_FOLDER = os.path.join(observer.basedir, str(_run._id)) 407 | VERBOSE_TRAINING = 0 408 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 409 | 410 | writer = SummaryWriter(log_dir=OUTPUT_FOLDER) 411 | 412 | # From dataset Ingredient 413 | TRAIN_BUILDINGS = dataset["TRAIN_BUILDINGS"] 414 | ON_POWER_THRESHOLD = dataset["ON_POWER_THRESHOLD"] 415 | 416 | ############################################################################################## 417 | #PREPARE DATASET (DATALOADERs) 418 | ############################################################################################## 419 | running_data_processes = [] # stop these at the end 420 | sources, validation_sources = get_sources( 421 | training_source_names=SOURCE_TYPES, 422 | validation_source_names=VALIDATION_SOURCE_TYPES, 423 | seq_length=SEQ_LENGTH, 424 | sources_seed=TRAINING_SEED, 425 | validation_stride=128 ) 426 | 427 | if DOWNSAMPLE_FACTOR > 1: 428 | downsample_rng = np.random.RandomState(TRAINING_SEED) 429 | input_processing_steps = [DownSample(DOWNSAMPLE_FACTOR, downsample_rng)] 430 | else: 431 | input_processing_steps = [] 432 | 433 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 434 | groundtruth_processing = SubSequence(offset,-offset) 435 | input_processing_steps += [DivideBy(INPUT_STD), Transpose((0, 2, 1))] 436 | target_processing_steps = [groundtruth_processing, DivideBy(TARGET_STD), Transpose((0, 2, 1))] 437 | 438 | validation_pipeline = DataPipeline( 439 | sources=validation_sources, 440 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 441 | input_processing=input_processing_steps, 442 | target_processing=target_processing_steps 443 | ) 444 | validation_batches = get_validation_batches(validation_pipeline) 445 | print("appliance {} has {} validation batches".format( 446 | TARGET_APPLIANCE, 447 | sum([len(v) for k, v in validation_batches.items()]) )) 448 | 449 | data_pipeline = DataPipeline( 450 | sources=sources, 451 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 452 | input_processing=input_processing_steps, 453 | target_processing=target_processing_steps 454 | ) 455 | data_thread = DataProcess(data_pipeline) 456 | data_thread.start() 457 | running_data_processes.append(data_thread) 458 | 459 | net = _Net(SEQ_LENGTH, TARGET_SEQ_LENGTH) 460 | print(net) 461 | 462 | metrics_accu = MetricsAccumulator( 463 | on_power_threshold=ON_POWER_THRESHOLD, 464 | max_power=MAX_TARGET_POWER) 465 | 466 | # note: MSE - Mean Squared Error 467 | criterion = torch.nn.MSELoss() 468 | 469 | stop_training = False 470 | best_mse = None 471 | 472 | # PREPARE DISAGGREGATOR 473 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 474 | test_disaggregator = Disaggregator( 475 | EVALUATION_DATA_PATH='input/evaluation_data_48h', 476 | TARGET_APPLIANCE = TARGET_APPLIANCE, 477 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 478 | MAX_TARGET_POWER = MAX_TARGET_POWER, 479 | pad_mains = True, 480 | pad_appliance = False, 481 | disagg_func = disag_seq2seq, 482 | disagg_kwargs = dict( 483 | model = net, 484 | input_processing=input_processing_steps, 485 | target_processing=target_processing_steps, 486 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 487 | seq_length = SEQ_LENGTH, 488 | target_seq_length = TARGET_SEQ_LENGTH, 489 | USE_CUDA=USE_CUDA, 490 | stride = 5 491 | ) 492 | ) 493 | 494 | # PREPARE TENSORS, WHICH WILL BE FED USED DURING TRAINING AND VALIDATION 495 | input = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 496 | target = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, TARGET_SEQ_LENGTH) 497 | 498 | if USE_CUDA: 499 | # note: push to GPU 500 | net.cuda() 501 | criterion.cuda() 502 | input, target = input.cuda(), target.cuda() 503 | 504 | # setup optimizer. TODO: Should we use 'Adam' for disaggregator? 505 | optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, betas=(0.9, 0.999)) 506 | #optimizer = optim.SGD(net.parameters(), momentum=0.9, nesterov=True, lr=LEARNING_RATE) 507 | scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[50,75], gamma=0.1) 508 | 509 | history = {} 510 | csvpath = os.path.join(OUTPUT_FOLDER, "history.csv") 511 | if os.path.exists(csvpath): 512 | print("Already exists: {}".format(csvpath)) 513 | return -1 514 | 515 | progbar_epoch = tqdm(desc="Epoch", total=EPOCHS, unit="epoch", disable=(not VERBOSE_TRAINING)) 516 | for epoch in range(EPOCHS): 517 | # TRAINING 518 | metrics_log = {'training':{}} 519 | training_loss = 0.0 520 | progbar = tqdm(desc="Train", total=STEPS_PER_EPOCH, leave=False, disable=(not VERBOSE_TRAINING)) 521 | for i in range(STEPS_PER_EPOCH): 522 | net.zero_grad() 523 | batch = data_thread.get_batch() 524 | qsize = data_thread._queue.qsize() 525 | 526 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 527 | target_signal = torch.from_numpy(batch.after_processing.target) 528 | if USE_CUDA: 529 | aggregated_signal = aggregated_signal.cuda() 530 | target_signal = target_signal.cuda() 531 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 532 | target.resize_as_(target_signal).copy_(target_signal) 533 | inputv = Variable(input, requires_grad=False) 534 | targetv = Variable(target, requires_grad=False) 535 | output = net(inputv) 536 | loss = criterion(output, targetv) 537 | loss.backward() 538 | optimizer.step() 539 | training_loss += loss.item() 540 | 541 | progbar.set_postfix(dict( 542 | loss = "{:.4f}".format(loss.item()), 543 | qsize = qsize 544 | ), refresh=False) 545 | progbar.update() 546 | 547 | metrics_log['training']['loss'] = float(training_loss/STEPS_PER_EPOCH) 548 | metrics_log['training']['lr'] = optimizer.param_groups[0]['lr'] 549 | 550 | # VALIDATION 551 | #pr_num_thresholds = 127 552 | for fold in validation_batches: 553 | metrics_accu.reset_accumulator() 554 | #accumulated_pr = {} 555 | #for cl in ["tp", "tn", "fp", "fn"]: 556 | # accumulated_pr[cl] = torch.LongTensor(pr_num_thresholds).zero_() 557 | for batch in validation_batches[fold]: 558 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 559 | target_signal = torch.from_numpy(batch.after_processing.target) 560 | if USE_CUDA: 561 | aggregated_signal = aggregated_signal.cuda() 562 | target_signal = target_signal.cuda() 563 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 564 | target.resize_as_(target_signal).copy_(target_signal) 565 | with torch.no_grad(): 566 | inputv = Variable(input) 567 | targetv = Variable(target) 568 | output = net(inputv) 569 | val_loss = criterion(output, targetv) 570 | loss_value = val_loss.item() 571 | # other metrics 572 | pred_y = data_pipeline.apply_inverse_processing(output.cpu().data.numpy(), 'target') 573 | true_y = groundtruth_processing(batch.before_processing.target) 574 | metrics_accu.accumulate_metrics(true_y, pred_y, val_loss=loss_value) 575 | #calculate_pr_curve_torch(accumulated_pr, MAX_TARGET_POWER, true_y, pred_y, num_thresholds=pr_num_thresholds) 576 | 577 | for key, value in metrics_accu.finalize_metrics().items(): 578 | metrics_log.setdefault(fold[0], {}).setdefault(key, {})[fold[1]] = value 579 | 580 | #precision = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fp"]) 581 | #recall = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fn"]) 582 | #writer.add_pr_curve_raw("pr_{}/{}".format(fold[0], fold[1]), 583 | # true_positive_counts=accumulated_pr["tp"], 584 | # false_positive_counts=accumulated_pr["fp"], 585 | # true_negative_counts=accumulated_pr["tn"], 586 | # false_negative_counts=accumulated_pr["fn"], 587 | # precision=precision, recall=recall, 588 | # global_step=(epoch+1)*STEPS_PER_EPOCH, num_thresholds=pr_num_thresholds) 589 | 590 | # LR Scheduler 591 | val_loss = metrics_log['unseen_activations']['val_loss']['rss'] 592 | #val_loss = metrics_log['mean_squared_error']['unseen_activations']['rss'] 593 | #scheduler.step(val_loss) 594 | scheduler.step() 595 | 596 | # PRINT STATS 597 | if not VERBOSE_TRAINING: 598 | print('[{:d}/{:d}] {}'.format(epoch+1, EPOCHS, metrics_log['training'])) 599 | else: 600 | progbar_epoch.set_postfix(dict(loss=metrics_log['training']['loss']), refresh=False) 601 | 602 | progbar_epoch.update() 603 | progbar.close() 604 | 605 | # store in history / tensorboard 606 | for fold, metrics_for_fold in metrics_log.items(): 607 | for metric_name, value in metrics_for_fold.items(): 608 | if type(value) == dict: 609 | SW_add_scalars2(writer, "{}/{}".format(fold, metric_name), value, (epoch+1)*STEPS_PER_EPOCH) 610 | for k, v in value.items(): 611 | name = "{}/{}/{}".format(fold, metric_name, k) 612 | history.setdefault(name, []).append(v) 613 | else: 614 | name = "{}/{}".format(fold, metric_name) 615 | writer.add_scalar(name, value, (epoch+1)*STEPS_PER_EPOCH) 616 | history.setdefault(name, []).append(value) 617 | 618 | # CHECKPOINTING 619 | if CHECKPOINT_BEST_MSE: 620 | mse = val_loss 621 | if best_mse is None: 622 | best_mse = mse 623 | if best_mse > mse: 624 | msg = "[{:d}/{:d}] MSE improved from {:.4f} to {:.4f} (d={:f}), saving model...".format(epoch+1, EPOCHS, best_mse, mse, best_mse-mse) 625 | if not VERBOSE_TRAINING: 626 | print(msg) 627 | else: 628 | progbar_epoch.write(msg) 629 | torch.save({ 630 | 'epoch': epoch + 1, 631 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 632 | 'mse' : mse, 633 | 'model': net.state_dict()}, '{}/net_best_mse.pth.tar'.format(OUTPUT_FOLDER)) 634 | best_mse = mse 635 | 636 | if CHECKPOINTING_EVERY_N_EPOCHS is not None: 637 | if (epoch+1) % CHECKPOINTING_EVERY_N_EPOCHS == 0: 638 | torch.save(net.state_dict(), '{}/net_step_{:06d}.pth'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 639 | 640 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 641 | if (epoch+1) % TEST_DISAGGREGATE_EVERY_N_EPOCHS == 0: 642 | scores = test_disaggregator.calculate_metrics() 643 | scores_by_metric = {} 644 | for building_i, building in scores.items(): 645 | for metric, value in building.items(): 646 | scores_by_metric.setdefault(metric, {})[building_i] = value 647 | for metric, building_d in scores_by_metric.items(): 648 | SW_add_scalars2(writer, "test_score/{}".format(metric), building_d, (epoch+1)*STEPS_PER_EPOCH) 649 | 650 | if stop_training: 651 | break 652 | 653 | # CHECKPOINTING at end 654 | torch.save({ 655 | 'epoch': epoch + 1, 656 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 657 | 'model': net.state_dict(), 658 | 'optimizer': optimizer.state_dict(), 659 | #'scheduler': scheduler.state_dict() 660 | # TODO: scheduler is not saved this way, scheduler.state_dict() does not exist 661 | }, '{}/net_step_{:06d}.pth.tar'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 662 | 663 | df = pd.DataFrame(history) 664 | df.to_csv(csvpath) 665 | 666 | for p in running_data_processes: 667 | p.stop() 668 | writer.close() 669 | 670 | #return 42 671 | return metrics_log['training']['loss'] 672 | -------------------------------------------------------------------------------- /sgnsp_shin_URED_.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division, absolute_import 2 | import pandas as pd 3 | import numpy as np 4 | 5 | import os, pickle 6 | 7 | from tqdm import tqdm 8 | from collections import OrderedDict 9 | 10 | ## PYTORCH 11 | import torch 12 | import torch.nn as nn 13 | import torch.nn.parallel 14 | import torch.backends.cudnn as cudnn 15 | import torch.optim as optim 16 | import torch.optim.lr_scheduler as lr_scheduler 17 | import torch.utils.data 18 | import torchvision.datasets as dset 19 | import torchvision.transforms as transforms 20 | import torchvision.utils as vutils 21 | from torch.autograd import Variable 22 | 23 | from tensorboardX import SummaryWriter 24 | 25 | import matplotlib 26 | if not matplotlib.is_interactive(): 27 | matplotlib.use("AGG") 28 | #import matplotlib.pyplot as plt 29 | #import matplotlib.gridspec as gridspec 30 | 31 | from neuralnilm.data.datapipeline import DataPipeline 32 | from neuralnilm.data.datathread import DataThread 33 | from neuralnilm.data.dataprocess import DataProcess 34 | from neuralnilm.data.processing import DivideBy, IndependentlyCenter, Transpose, Add, SubSequence, DownSample 35 | from neuralnilm.consts import DATA_FOLD_NAMES 36 | 37 | from disaggregator import Disaggregator 38 | from metrics import MetricsAccumulator 39 | from parallelsource import MultiprocessActivationsSource 40 | from utils import SW_add_scalars2 41 | 42 | from sacred import Experiment 43 | from sacred.observers import FileStorageObserver 44 | 45 | from dataset_final_URED_ import dataset, load_activity_data, get_sources 46 | 47 | ex = Experiment(ingredients=[dataset]) 48 | 49 | #from sacred.utils import apply_backspaces_and_linefeeds 50 | #ex.captured_out_filter = apply_backspaces_and_linefeeds 51 | 52 | @ex.config 53 | def base_experiment_config(dataset): 54 | 55 | TARGET_APPLIANCE = dataset["TARGET_APPLIANCE"] 56 | SOURCE_TYPES = ["BalancedActivityRealAggregateSource", "BalancedActivityAugmentedAggregateSource"] 57 | VALIDATION_SOURCE_TYPES = ["RandomizedSequentialSource"] 58 | 59 | SEQ_LENGTH = 432 60 | TARGET_SEQ_LENGTH = 32 61 | 62 | if TARGET_APPLIANCE == 'television': 63 | INPUT_MEAN = 200.0 64 | INPUT_STD = 400.0 65 | elif TARGET_APPLIANCE == 'microwave': 66 | INPUT_MEAN = 500.0 67 | INPUT_STD = 800.0 68 | elif TARGET_APPLIANCE == 'tumble dryer': 69 | INPUT_MEAN = 700.0 70 | INPUT_STD = 1000.0 71 | elif TARGET_APPLIANCE == 'fridge': 72 | SOURCE_TYPES = ["BalancedBuildingRealAggregateSource"] 73 | INPUT_MEAN = 200.0 74 | INPUT_STD = 400.0 75 | elif TARGET_APPLIANCE == 'kettle': 76 | INPUT_MEAN = 700.0 77 | INPUT_STD = 1000.0 78 | elif TARGET_APPLIANCE == 'washing machine': 79 | INPUT_MEAN = 400.0 80 | INPUT_STD = 700.0 81 | elif TARGET_APPLIANCE == 'dish washer': 82 | INPUT_MEAN = 700.0 83 | INPUT_STD = 1000.0 84 | 85 | MAX_TARGET_POWER = 4200 86 | 87 | TRAINING_SEED = 42 88 | VERBOSE_TRAINING = True 89 | DOWNSAMPLE_FACTOR = 0 90 | 91 | DESCRIPTION = """ Re-Impl. of Shin's Subtask Gated Network with standby power (SGN-SP) 92 | """ 93 | 94 | NUM_SEQ_PER_BATCH = 64 95 | EPOCHS = 100 96 | STEPS_PER_EPOCH = 1000 97 | 98 | LEARNING_RATE = 1e-4 99 | NUM_BATCHES_FOR_VALIDATION = 64 100 | USE_CUDA = True 101 | CHECKPOINT_BEST_MSE = False 102 | CHECKPOINTING_EVERY_N_EPOCHS = None 103 | TEST_DISAGGREGATE_EVERY_N_EPOCHS = None 104 | 105 | 106 | @ex.capture 107 | def get_validation_batches(data_pipeline, VALIDATION_SOURCE_TYPES, NUM_BATCHES_FOR_VALIDATION): 108 | shortname_sources = { 109 | "BalancedActivityRealAggregateSource": "bas", 110 | "BalancedActivityAugmentedAggregateSource": "aas", 111 | "RandomizedSequentialSource": "rss", 112 | "BalancedBuildingRealAggregateSource": "bbs" 113 | } 114 | shortname_folds = { 115 | #"train": "training", 116 | #"unseen_appliances": "unseen_appliances", 117 | "unseen_activations_of_seen_appliances": "unseen_activations" 118 | } 119 | 120 | validation_batches = {} 121 | 122 | for source in VALIDATION_SOURCE_TYPES: 123 | for fold in shortname_folds: 124 | fold_batches = [] 125 | try: 126 | batch = data_pipeline.get_batch(fold=fold, source_id=source, reset_iterator=True, validation=False) 127 | fold_batches.append(batch) 128 | except KeyError: 129 | print("For fold `{}` no validation data available".format(fold)) 130 | continue 131 | 132 | for i in range(1,NUM_BATCHES_FOR_VALIDATION): 133 | batch = data_pipeline.get_batch(fold=fold, source_id=source, validation=False) 134 | if batch is None: # Here we reach the end of the validation data for the current fold. 135 | break 136 | else: 137 | fold_batches.append(batch) 138 | 139 | if i == NUM_BATCHES_FOR_VALIDATION-1: 140 | print("WARNING: Validation data may not be fully covered") 141 | 142 | validation_batches[(shortname_folds[fold],shortname_sources[source])] = fold_batches 143 | return validation_batches 144 | 145 | 146 | def disag_seq2seq_sgn(model, mains, target, input_processing, target_processing, max_target_power, n_seq_per_batch, seq_length, target_seq_length, building_i, stride, USE_CUDA, return_sequences): 147 | def apply_inverse_processing(batch, processing_steps): 148 | reversed_processing_steps = processing_steps[::-1] 149 | for step in reversed_processing_steps: 150 | batch = step.inverse(batch) 151 | 152 | return batch 153 | 154 | def apply_processing(batch, processing_steps): 155 | for step in processing_steps: 156 | batch = step(batch) 157 | 158 | return batch 159 | 160 | def mains_to_batches(mains, n_seq_per_batch, seq_length, processing_steps, stride=1): 161 | assert mains.ndim == 1 162 | n_mains_samples = len(mains) 163 | input_shape = (n_seq_per_batch, seq_length, 1) 164 | 165 | # Divide mains data into batches 166 | n_batches = (n_mains_samples / stride) / n_seq_per_batch 167 | n_batches = np.ceil(n_batches).astype(int) 168 | batches = [] 169 | for batch_i in range(n_batches): 170 | batch = np.zeros(input_shape, dtype=np.float32) 171 | batch_start = batch_i * n_seq_per_batch * stride 172 | for seq_i in range(n_seq_per_batch): 173 | mains_start_i = batch_start + (seq_i * stride) 174 | mains_end_i = mains_start_i + seq_length 175 | seq = mains[mains_start_i:mains_end_i] 176 | batch[seq_i, :len(seq), 0] = seq 177 | processed_batch = apply_processing(batch, processing_steps) 178 | batches.append(processed_batch) 179 | 180 | return batches 181 | 182 | 183 | if stride is None: 184 | stride = seq_length 185 | 186 | batches = mains_to_batches(mains, n_seq_per_batch, seq_length, input_processing, stride) 187 | estimates = np.zeros(len(mains), dtype=np.float32) 188 | offset = (seq_length - target_seq_length) // 2 189 | 190 | if return_sequences: 191 | # `estimate_windows` is array with shape [#sliding_windows_in_mains x lenth_of_window] 192 | #. it stores disag results for all sliding windows seperately 193 | # note: beware of padding. also last batch may be not be filled entirely 194 | #. therefore have some extra margin 195 | estimate_windows = np.zeros((len(batches)*n_seq_per_batch, target_seq_length), dtype=np.float32) 196 | 197 | # Iterate over each batch 198 | window_i = 0 199 | for batch_i, net_input in enumerate(batches): 200 | input = torch.from_numpy(net_input.astype(np.float32)) 201 | if USE_CUDA: 202 | input = input.cuda() 203 | with torch.no_grad(): 204 | inputv = Variable(input) 205 | output, _ = model(inputv) 206 | net_output = output.cpu().data.numpy() 207 | net_output = apply_inverse_processing(net_output, target_processing) 208 | batch_start = (batch_i * n_seq_per_batch * stride) + offset 209 | for seq_i in range(n_seq_per_batch): 210 | start_i = batch_start + (seq_i * stride) 211 | end_i = start_i + target_seq_length 212 | n = len(estimates[start_i:end_i]) 213 | # The net output is not necessarily the same length 214 | # as the mains (because mains might not fit exactly into 215 | # the number of batches required) 216 | estimates[start_i:end_i] += net_output[seq_i, :n, 0] 217 | if return_sequences: 218 | estimate_windows[window_i, :] = net_output[seq_i, :, 0] 219 | 220 | window_i += 1 221 | 222 | n_overlaps = target_seq_length / stride 223 | estimates /= n_overlaps 224 | estimates[estimates < 0] = 0 225 | 226 | if return_sequences: 227 | return estimates, dict(sequences=estimate_windows) 228 | else: 229 | return estimates 230 | 231 | 232 | class _SGNShinSubNetPaddingSame(nn.Module): 233 | def __init__(self, SEQ_LENGTH, TARGET_SEQ_LENGTH): 234 | super(_SGNShinSubNetPaddingSame, self).__init__() 235 | 236 | self.seq_length = SEQ_LENGTH 237 | self.target_seq_length = TARGET_SEQ_LENGTH 238 | self.nc = 1 # number of channels 239 | 240 | self.conv1 = nn.Sequential( 241 | nn.ConstantPad1d((4, 5), 0), 242 | nn.Conv1d(self.nc, 30, 10, stride=1, bias=True), 243 | nn.ReLU(True), 244 | nn.ConstantPad1d((3, 4), 0), 245 | nn.Conv1d(30, 30, 8, stride=1, bias=True), 246 | nn.ReLU(True), 247 | nn.ConstantPad1d((2, 3), 0), 248 | nn.Conv1d(30, 40, 6, stride=1, bias=True), 249 | nn.ReLU(True), 250 | nn.Conv1d(40, 50, 5, stride=1, padding=2, bias=True), 251 | nn.ReLU(True), 252 | nn.Conv1d(50, 50, 5, stride=1, padding=2, bias=True), 253 | nn.ReLU(True), 254 | nn.Conv1d(50, 50, 5, stride=1, padding=2, bias=True), 255 | nn.ReLU(True), 256 | ) 257 | 258 | self.n_dense_units = self.seq_length * 50 259 | self.dense1 = nn.Linear(self.n_dense_units, 1024) 260 | self.act1 = nn.ReLU(True) 261 | 262 | self.dense2 = nn.Linear(1024, self.target_seq_length) 263 | 264 | def forward(self, input): 265 | x = self.conv1(input) 266 | x = self.dense1(x.view(-1, self.n_dense_units)) 267 | x = self.act1(x) 268 | x = self.dense2(x) 269 | x = x.view(-1, 1, self.target_seq_length) 270 | return x 271 | 272 | 273 | class _SGNSPShinNet(nn.Module): 274 | def __init__(self, SEQ_LENGTH, TARGET_SEQ_LENGTH): 275 | super(_SGNSPShinNet, self).__init__() 276 | 277 | self.seq_length = SEQ_LENGTH 278 | self.regression = _SGNShinSubNetPaddingSame(SEQ_LENGTH, TARGET_SEQ_LENGTH) 279 | self.gate = _SGNShinSubNetPaddingSame(SEQ_LENGTH, TARGET_SEQ_LENGTH) 280 | self.sigmoid = nn.Sigmoid() 281 | self.standby_power = nn.parameter.Parameter(torch.Tensor(1)) 282 | self.standby_power.data.zero_() 283 | 284 | for m in self.modules(): 285 | if isinstance(m, nn.Conv1d): 286 | nn.init.kaiming_uniform_(m.weight) 287 | m.bias.data.zero_() 288 | if isinstance(m, nn.Linear): 289 | nn.init.kaiming_uniform_(m.weight) 290 | m.bias.data.zero_() 291 | 292 | def forward(self, input): 293 | x = self.regression(input) 294 | g = self.gate(input) 295 | sg = self.sigmoid(g) 296 | x = (sg*x)+((1-sg)*self.standby_power) 297 | return x, g 298 | 299 | _Net = _SGNSPShinNet 300 | 301 | 302 | def load_disaggregator(EVALUATION_DATA_PATH, MODEL_PATH, config=None, USE_CUDA=True): 303 | """ 304 | Helper function for the disaggregator script 305 | """ 306 | 307 | if config is None: 308 | config = os.path.dirname(MODEL_PATH) 309 | 310 | if type(config) == str: 311 | try: 312 | import jsonpickle 313 | with open(os.path.join(config, 'config.json'), 'r') as configfile: 314 | config = jsonpickle.decode(configfile.read()) 315 | except: 316 | return None 317 | 318 | assert(type(config) == dict) 319 | 320 | dataset = config['dataset'] 321 | SEQ_LENGTH = config['SEQ_LENGTH'] 322 | TARGET_SEQ_LENGTH = config['TARGET_SEQ_LENGTH'] 323 | TARGET_APPLIANCE = dataset['TARGET_APPLIANCE'] 324 | ON_POWER_THRESHOLD = dataset['ON_POWER_THRESHOLD'] 325 | MAX_TARGET_POWER = config['MAX_TARGET_POWER'] 326 | NUM_SEQ_PER_BATCH = config['NUM_SEQ_PER_BATCH'] 327 | INPUT_STD = config['INPUT_STD'] 328 | INPUT_MEAN = config['INPUT_MEAN'] 329 | DOWNSAMPLE_FACTOR = config['DOWNSAMPLE_FACTOR'] 330 | #NUM_SEQ_PER_BATCH = 1024 # override 331 | 332 | net = _Net(SEQ_LENGTH, TARGET_SEQ_LENGTH) 333 | 334 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 335 | input_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 336 | target_processing_steps = [SubSequence(offset,-offset), Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 337 | 338 | if MODEL_PATH.endswith("/"): 339 | MODEL_PATH = MODEL_PATH + 'net_step_{:06d}.pth.tar'.format(config['EPOCHS']*config['STEPS_PER_EPOCH']) 340 | 341 | if USE_CUDA: 342 | training_state = torch.load(MODEL_PATH) 343 | else: 344 | training_state = torch.load(MODEL_PATH, map_location='cpu') 345 | 346 | if MODEL_PATH.endswith("tar"): 347 | model = training_state['model'] 348 | else: 349 | model = training_state 350 | 351 | net.load_state_dict(model) 352 | if USE_CUDA: 353 | net.cuda() 354 | 355 | return Disaggregator( 356 | EVALUATION_DATA_PATH=EVALUATION_DATA_PATH, 357 | TARGET_APPLIANCE = TARGET_APPLIANCE, 358 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 359 | MAX_TARGET_POWER = MAX_TARGET_POWER, 360 | pad_mains = True, 361 | pad_appliance = False, 362 | disagg_func = disag_seq2seq_sgn, 363 | downsample_factor = DOWNSAMPLE_FACTOR, 364 | disagg_kwargs = dict( 365 | USE_CUDA=USE_CUDA, 366 | model = net, 367 | input_processing=input_processing_steps, 368 | target_processing=target_processing_steps, 369 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 370 | seq_length = SEQ_LENGTH, 371 | target_seq_length = TARGET_SEQ_LENGTH, 372 | stride = 1 373 | ) 374 | ), training_state 375 | 376 | 377 | @ex.automain 378 | def run_experiment(dataset, 379 | INPUT_MEAN, 380 | INPUT_STD, 381 | SOURCE_TYPES, 382 | VALIDATION_SOURCE_TYPES, 383 | DOWNSAMPLE_FACTOR, 384 | SEQ_LENGTH, 385 | TARGET_SEQ_LENGTH, 386 | MAX_TARGET_POWER, 387 | TARGET_APPLIANCE, 388 | TRAINING_SEED, 389 | VERBOSE_TRAINING, 390 | LEARNING_RATE, 391 | NUM_SEQ_PER_BATCH, 392 | EPOCHS, 393 | STEPS_PER_EPOCH, 394 | USE_CUDA, 395 | CHECKPOINT_BEST_MSE, 396 | CHECKPOINTING_EVERY_N_EPOCHS, 397 | TEST_DISAGGREGATE_EVERY_N_EPOCHS, 398 | _run): 399 | 400 | torch.manual_seed(TRAINING_SEED) 401 | 402 | OUTPUT_FOLDER = os.path.join(ex.get_experiment_info()['name'],"output") 403 | for observer in _run.observers: 404 | if type(observer) is FileStorageObserver: 405 | OUTPUT_FOLDER = os.path.join(observer.basedir, str(_run._id)) 406 | VERBOSE_TRAINING = 0 407 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 408 | 409 | writer = SummaryWriter(log_dir=OUTPUT_FOLDER) 410 | 411 | # From dataset Ingredient 412 | TRAIN_BUILDINGS = dataset["TRAIN_BUILDINGS"] 413 | ON_POWER_THRESHOLD = dataset["ON_POWER_THRESHOLD"] 414 | 415 | ############################################################################################## 416 | #PREPARE DATASET (DATALOADERs) 417 | ############################################################################################## 418 | running_data_processes = [] # stop these at the end 419 | sources, validation_sources = get_sources( 420 | training_source_names=SOURCE_TYPES, 421 | validation_source_names=VALIDATION_SOURCE_TYPES, 422 | seq_length=SEQ_LENGTH, 423 | sources_seed=TRAINING_SEED, 424 | validation_stride=128 ) 425 | 426 | offset = (SEQ_LENGTH - TARGET_SEQ_LENGTH) // 2 427 | groundtruth_processing = SubSequence(offset,-offset) 428 | input_processing_steps = [Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 429 | target_processing_steps = [groundtruth_processing, Add(-INPUT_MEAN), DivideBy(INPUT_STD), Transpose((0, 2, 1))] 430 | 431 | if DOWNSAMPLE_FACTOR > 1: 432 | downsample_rng = np.random.RandomState(TRAINING_SEED) 433 | input_processing_steps_training = [DownSample(DOWNSAMPLE_FACTOR, downsample_rng)] + input_processing_steps 434 | else: 435 | input_processing_steps_training = input_processing_steps 436 | 437 | validation_pipeline = DataPipeline( 438 | sources=validation_sources, 439 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 440 | input_processing=input_processing_steps_training, 441 | target_processing=target_processing_steps 442 | ) 443 | validation_batches = get_validation_batches(validation_pipeline) 444 | print("appliance {} has {} validation batches".format( 445 | TARGET_APPLIANCE, 446 | sum([len(v) for k, v in validation_batches.items()]) )) 447 | 448 | data_pipeline = DataPipeline( 449 | sources=sources, 450 | num_seq_per_batch=NUM_SEQ_PER_BATCH, 451 | input_processing=input_processing_steps_training, 452 | target_processing=target_processing_steps 453 | ) 454 | data_thread = DataProcess(data_pipeline) 455 | data_thread.start() 456 | running_data_processes.append(data_thread) 457 | 458 | net = _Net(SEQ_LENGTH, TARGET_SEQ_LENGTH) 459 | print(net) 460 | 461 | metrics_accu = MetricsAccumulator( 462 | on_power_threshold=ON_POWER_THRESHOLD, 463 | max_power=MAX_TARGET_POWER) 464 | 465 | # note: MSE - Mean Squared Error 466 | criterion = torch.nn.MSELoss() 467 | state_criterion = torch.nn.BCEWithLogitsLoss() 468 | 469 | stop_training = False 470 | best_mse = None 471 | 472 | # PREPARE DISAGGREGATOR 473 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 474 | test_disaggregator = Disaggregator( 475 | EVALUATION_DATA_PATH='input/evaluation_data_48h', 476 | TARGET_APPLIANCE = TARGET_APPLIANCE, 477 | ON_POWER_THRESHOLD = ON_POWER_THRESHOLD, 478 | MAX_TARGET_POWER = MAX_TARGET_POWER, 479 | pad_mains = True, 480 | pad_appliance = False, 481 | disagg_func = disag_seq2seq_sgn, 482 | downsample_factor = DOWNSAMPLE_FACTOR, 483 | disagg_kwargs = dict( 484 | model = net, 485 | input_processing=input_processing_steps, 486 | target_processing=target_processing_steps, 487 | n_seq_per_batch = NUM_SEQ_PER_BATCH, 488 | seq_length = SEQ_LENGTH, 489 | target_seq_length = TARGET_SEQ_LENGTH, 490 | USE_CUDA=USE_CUDA, 491 | stride = 1 492 | ) 493 | ) 494 | 495 | # PREPARE TENSORS, WHICH WILL BE FED USED DURING TRAINING AND VALIDATION 496 | input = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, SEQ_LENGTH) 497 | target = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, TARGET_SEQ_LENGTH) 498 | target_class = torch.FloatTensor(NUM_SEQ_PER_BATCH, 1, TARGET_SEQ_LENGTH) 499 | 500 | if USE_CUDA: 501 | # note: push to GPU 502 | net.cuda() 503 | criterion.cuda() 504 | input, target = input.cuda(), target.cuda() 505 | target_class = target_class.cuda() 506 | 507 | # setup optimizer. TODO: Should we use 'Adam' for disaggregator? 508 | optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, betas=(0.9, 0.999)) 509 | #optimizer = optim.SGD(net.parameters(), momentum=0.9, nesterov=True, lr=LEARNING_RATE) 510 | scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[50,75], gamma=0.1) 511 | 512 | history = {} 513 | csvpath = os.path.join(OUTPUT_FOLDER, "history.csv") 514 | if os.path.exists(csvpath): 515 | print("Already exists: {}".format(csvpath)) 516 | return -1 517 | 518 | progbar_epoch = tqdm(desc="Epoch", total=EPOCHS, unit="epoch", disable=(not VERBOSE_TRAINING)) 519 | for epoch in range(EPOCHS): 520 | # TRAINING 521 | metrics_log = {'training':{}} 522 | training_loss = 0.0 523 | progbar = tqdm(desc="Train", total=STEPS_PER_EPOCH, leave=False, disable=(not VERBOSE_TRAINING)) 524 | for i in range(STEPS_PER_EPOCH): 525 | net.zero_grad() 526 | batch = data_thread.get_batch() 527 | while batch is None: 528 | batch = data_thread.get_batch() 529 | qsize = data_thread._queue.qsize() 530 | 531 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 532 | target_signal = torch.from_numpy(batch.after_processing.target) 533 | target_class_np = np.float32(groundtruth_processing(batch.before_processing.target) > 15.0) 534 | target_class_t = torch.from_numpy(target_class_np.transpose((0, 2, 1))) 535 | if USE_CUDA: 536 | aggregated_signal = aggregated_signal.cuda() 537 | target_signal = target_signal.cuda() 538 | target_class_t = target_class_t.cuda() 539 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 540 | target.resize_as_(target_signal).copy_(target_signal) 541 | target_class.resize_as_(target_class_t).copy_(target_class_t) 542 | output, on_logit = net(input) 543 | loss = criterion(output, target) + state_criterion(on_logit, target_class) 544 | loss.backward() 545 | optimizer.step() 546 | training_loss += loss.item() 547 | 548 | progbar.set_postfix(dict( 549 | loss = "{:.4f}".format(loss.item()), 550 | qsize = qsize 551 | ), refresh=False) 552 | progbar.update() 553 | 554 | metrics_log['training']['loss'] = float(training_loss/STEPS_PER_EPOCH) 555 | metrics_log['training']['lr'] = optimizer.param_groups[0]['lr'] 556 | 557 | # VALIDATION 558 | #pr_num_thresholds = 127 559 | for fold in validation_batches: 560 | metrics_accu.reset_accumulator() 561 | #accumulated_pr = {} 562 | #for cl in ["tp", "tn", "fp", "fn"]: 563 | # accumulated_pr[cl] = torch.LongTensor(pr_num_thresholds).zero_() 564 | for batch in validation_batches[fold]: 565 | aggregated_signal = torch.from_numpy(batch.after_processing.input) 566 | target_signal = torch.from_numpy(batch.after_processing.target) 567 | target_class_np = np.float32(groundtruth_processing(batch.before_processing.target) > 15.0) 568 | target_class_t = torch.from_numpy(target_class_np.transpose((0, 2, 1))) 569 | if USE_CUDA: 570 | aggregated_signal = aggregated_signal.cuda() 571 | target_signal = target_signal.cuda() 572 | target_class_t = target_class_t.cuda() 573 | input.resize_as_(aggregated_signal).copy_(aggregated_signal) 574 | target.resize_as_(target_signal).copy_(target_signal) 575 | target_class.resize_as_(target_class_t).copy_(target_class_t) 576 | with torch.no_grad(): 577 | output, on_logit = net(input) 578 | val_loss = criterion(output, target) + state_criterion(on_logit, target_class) 579 | loss_value = val_loss.item() 580 | # other metrics 581 | pred_y = data_pipeline.apply_inverse_processing(output.cpu().data.numpy(), 'target') 582 | true_y = groundtruth_processing(batch.before_processing.target) 583 | metrics_accu.accumulate_metrics(true_y, pred_y, val_loss=loss_value) 584 | #calculate_pr_curve_torch(accumulated_pr, MAX_TARGET_POWER, true_y, pred_y, num_thresholds=pr_num_thresholds) 585 | 586 | for key, value in metrics_accu.finalize_metrics().items(): 587 | metrics_log.setdefault(fold[0], {}).setdefault(key, {})[fold[1]] = value 588 | 589 | #precision = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fp"]) 590 | #recall = accumulated_pr["tp"] / (accumulated_pr["tp"] + accumulated_pr["fn"]) 591 | #writer.add_pr_curve_raw("pr_{}/{}".format(fold[0], fold[1]), 592 | # true_positive_counts=accumulated_pr["tp"], 593 | # false_positive_counts=accumulated_pr["fp"], 594 | # true_negative_counts=accumulated_pr["tn"], 595 | # false_negative_counts=accumulated_pr["fn"], 596 | # precision=precision, recall=recall, 597 | # global_step=(epoch+1)*STEPS_PER_EPOCH, num_thresholds=pr_num_thresholds) 598 | 599 | # LR Scheduler 600 | val_loss = metrics_log['unseen_activations']['val_loss']['rss'] 601 | #val_loss = metrics_log['mean_squared_error']['unseen_activations']['rss'] 602 | #scheduler.step(val_loss) 603 | scheduler.step() 604 | 605 | # PRINT STATS 606 | if not VERBOSE_TRAINING: 607 | print('[{:d}/{:d}] {}'.format(epoch+1, EPOCHS, metrics_log['training'])) 608 | else: 609 | progbar_epoch.set_postfix(dict(loss=metrics_log['training']['loss']), refresh=False) 610 | 611 | progbar_epoch.update() 612 | progbar.close() 613 | 614 | # store in history / tensorboard 615 | for fold, metrics_for_fold in metrics_log.items(): 616 | for metric_name, value in metrics_for_fold.items(): 617 | if type(value) == dict: 618 | SW_add_scalars2(writer, "{}/{}".format(fold, metric_name), value, (epoch+1)*STEPS_PER_EPOCH) 619 | for k, v in value.items(): 620 | name = "{}/{}/{}".format(fold, metric_name, k) 621 | history.setdefault(name, []).append(v) 622 | else: 623 | name = "{}/{}".format(fold, metric_name) 624 | writer.add_scalar(name, value, (epoch+1)*STEPS_PER_EPOCH) 625 | history.setdefault(name, []).append(value) 626 | 627 | # CHECKPOINTING 628 | if CHECKPOINT_BEST_MSE: 629 | mse = val_loss 630 | if best_mse is None: 631 | best_mse = mse 632 | if best_mse > mse: 633 | msg = "[{:d}/{:d}] MSE improved from {:.4f} to {:.4f} (d={:f}), saving model...".format(epoch+1, EPOCHS, best_mse, mse, best_mse-mse) 634 | if not VERBOSE_TRAINING: 635 | print(msg) 636 | else: 637 | progbar_epoch.write(msg) 638 | torch.save({ 639 | 'epoch': epoch + 1, 640 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 641 | 'mse' : mse, 642 | 'model': net.state_dict()}, '{}/net_best_mse.pth.tar'.format(OUTPUT_FOLDER)) 643 | best_mse = mse 644 | 645 | if CHECKPOINTING_EVERY_N_EPOCHS is not None: 646 | if (epoch+1) % CHECKPOINTING_EVERY_N_EPOCHS == 0: 647 | torch.save(net.state_dict(), '{}/net_step_{:06d}.pth'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 648 | 649 | if TEST_DISAGGREGATE_EVERY_N_EPOCHS is not None: 650 | if (epoch+1) % TEST_DISAGGREGATE_EVERY_N_EPOCHS == 0: 651 | scores = test_disaggregator.calculate_metrics() 652 | scores_by_metric = {} 653 | for building_i, building in scores.items(): 654 | for metric, value in building.items(): 655 | scores_by_metric.setdefault(metric, {})[building_i] = value 656 | for metric, building_d in scores_by_metric.items(): 657 | SW_add_scalars2(writer, "test_score/{}".format(metric), building_d, (epoch+1)*STEPS_PER_EPOCH) 658 | 659 | if stop_training: 660 | break 661 | 662 | # CHECKPOINTING at end 663 | torch.save({ 664 | 'epoch': epoch + 1, 665 | 'step' : (epoch+1)*STEPS_PER_EPOCH, 666 | 'model': net.state_dict(), 667 | 'optimizer': optimizer.state_dict(), 668 | #'scheduler': scheduler.state_dict() 669 | # TODO: scheduler is not saved this way, scheduler.state_dict() does not exist 670 | }, '{}/net_step_{:06d}.pth.tar'.format(OUTPUT_FOLDER, (epoch+1)*STEPS_PER_EPOCH)) 671 | 672 | df = pd.DataFrame(history) 673 | df.to_csv(csvpath) 674 | 675 | for p in running_data_processes: 676 | p.stop() 677 | writer.close() 678 | 679 | #return 42 680 | return metrics_log['training']['loss'] 681 | --------------------------------------------------------------------------------