In [1]:
from __future__ import print_function, division
from functools import partial
from StringIO import StringIO

import numpy as np
import pandas as pd

import theano
import theano.tensor as T


Couldn't import dot_parser, loading of dot files will not be possible.

In [2]:
### utils
def sfloatX(data):
    """Convert scalar to floatX"""
    return getattr(np, theano.config.floatX)(data)


def none_to_dict(data):
    return {} if data is None else data


def ndim_tensor(name, ndim, dtype=theano.config.floatX):
    tensor_type = T.TensorType(dtype=dtype, broadcastable=((False,) * ndim))
    return tensor_type(name=name)

In [3]:
from lasagne.layers.helper import get_all_layers, get_all_params, get_output

class Net(object):
    def __init__(self, output_layer):
        self.layers = get_all_layers(output_layer)
        self._deterministic_output_func = None
        
    @property
    def deterministic_output_func(self):
        if self._deterministic_output_func is None:
            self._deterministic_output_func = self._compile_deterministic_output_func()
        return self._deterministic_output_func
        
    def _compile_deterministic_output_func(self):
        network_input = self.get_symbolic_input()
        deterministic_output = self.get_symbolic_output(deterministic=True)
        net_output_func = theano.function(
            inputs=[network_input],
            outputs=deterministic_output,
            on_unused_input='warn',
            allow_input_downcast=True
        )
        return net_output_func
   
    def get_symbolic_input(self):
        network_input = self.layers[0].input_var
        return network_input
    
    def get_symbolic_output(self, deterministic=False):
        network_input = self.get_symbolic_input()
        network_output = get_output(
            self.layers[-1], network_input, deterministic=deterministic)
        return network_output

In [4]:
from lasagne.updates import nesterov_momentum
from lasagne.objectives import aggregate, squared_error


class Trainer(object):
    def __init__(self, net, data_pipeline,
                 loss_func=squared_error, 
                 loss_aggregation_mode='mean',
                 updates_func=partial(nesterov_momentum, momentum=0.9),
                 learning_rate=1e-2,
                 callbacks=None,
                 repeat_callbacks=None):
        self.net = net
        self.data_pipeline = data_pipeline
        self.validation_batch = self.data_pipeline.get_batch(validation=True)
        def _loss_func(prediction, target):
            loss = loss_func(prediction, target)
            return aggregate(loss, mode=loss_aggregation_mode)
        self.loss_func = _loss_func
        self.updates_func = updates_func
        self._learning_rate = theano.shared(sfloatX(learning_rate), name='learning_rate')
        
        # Callbacks
        def callbacks_dataframe(lst):
            return pd.DataFrame(lst, columns=['iteration', 'function'])
        self.callbacks = callbacks_dataframe(callbacks)
        self.repeat_callbacks = callbacks_dataframe(repeat_callbacks)
        
        # Training and validation state
        self._train_func = None
        self._validation_cost_func = None
        self.training_costs = []
        self.validation_costs = []
        self.iteration = 0
        
    @property
    def train_func(self):
        if self._train_func is None:
            self._trian_func = self._compile_train_func()
        return self._train_func
    
    @property
    def validation_cost_func(self):
        if self._validation_cost_func is None:
            self._validation_cost_func = self._compile_validation_cost_func()
        return self._validation_cost_func
        
    def _compile_train_func(self):
        network_input = self.net.get_symbolic_input()
        train_output = self.net.get_symbolic_output(deterministic=False)
        target_var = ndim_tensor(name='target', ndim=train_output.ndim)
        train_loss = self.loss_func(train_output, target_var)
        all_params = get_all_params(self.layers[-1], trainable=True)
        updates = self.updates_func(train_loss, all_params, learning_rate=self._learning_rate)
        train_func = theano.function(
            inputs=[network_input, target_var],
            outputs=train_loss,
            updates=updates,
            on_unused_input='warn',
            allow_input_downcast=True)
        return train_func
    
    def _compile_validation_cost_func(self):
        network_input = self.net.get_symbolic_input()
        deterministic_output = self.net.get_symbolic_output(deterministic=True)
        validation_loss = self.loss_func(deterministic_output, target_var)
        validation_cost_func = theano.function(
            inputs=[network_input, target_var],
            outputs=[validation_loss, deterministic_output],
            on_unused_input='warn',
            allow_input_downcast=True)
        return validation_cost_func

    @property
    def learning_rate(self):
        return self._learning_rate.get_value()

    @learning_rate.setter
    def learning_rate(self, rate):
        rate = sfloatX(rate)
        self.logger.info(
            "Iteration {:d}: Change learning rate to {:.1E}"
            .format(self.n_iterations(), rate))
        self._learning_rate.set_value(rate)
        
    def fit(self, num_iterations=None):
        while self.iteration != num_iterations:
            self.iteration = len(training_costs)
                    
            # Training
            self.train_batch = self.data_pipeline.get_batch()
            train_cost = self.train_func(self.train_batch.input, self.train_batch.target)
            self.training_costs.append(train_cost.flatten()[0])
            
            # Callbacks
            def run_callbacks(df):
                for callback in df['function']:
                    callback(self)
            repeat_callbacks = self.repeat_callbacks[(iteration % self.repeat_callbacks['iteration']) == 0]
            run_callbacks(repeat_callbacks)
            callbacks = self.callbacks[self.callbacks['iteration'] == iteration]
            run_callbacks(callbacks)
            
    def validate(self):
        pass

In [5]:
APPLIANCE_PARAMS_CSV = StringIO("""
       on_power_threshold, max_power, min_on_duration, min_off_duration
kettle,                10,      2500,              20,               30
toaster,              100,      2000,              20,               30
""")

APPLIANCE_PARAMS = pd.read_csv(APPLIANCE_PARAMS_CSV, index_col=0)
APPLIANCE_PARAMS


Out[5]:
on_power_threshold max_power min_on_duration min_off_duration
kettle 10 2500 20 30
toaster 100 2000 20 30

In [6]:
from lasagne.layers import InputLayer, RecurrentLayer, DenseLayer, ReshapeLayer

def get_net_0(input_shape, target_shape=None):
    NUM_UNITS = {'dense_layer_0': 100}
    if target_shape is None:
        target_shape = input_shape
    
    # Define layers
    input_layer = InputLayer(
        shape=input_shape
    )
    dense_layer_0 = DenseLayer(
        input_layer, 
        num_units=NUM_UNITS['dense_layer_0']
    )
    final_dense_layer = DenseLayer(
        dense_layer_0,
        num_units=target_shape[1] * target_shape[2]
    )
    output_layer = ReshapeLayer(
        final_dense_layer,
        shape=target_shape
    )
    return output_layer

In [7]:
output_layer = get_net_0((4, 16, 1))
net = Net(output_layer)

In [8]:
class Disaggregator(object):
    def __init__(self, mains, output_path, stride=1):
        self.mains = mains
        self.stride = stride
        self.output_path = output_path
        seq_length = data_pipeline.seq_length
        num_seq_per_batch = data_pipeline.num_seq_per_batch
    
    def _get_mains_data_pipeline(self, trainer):
        data_pipeline = trainer.data_pipeline
        mains_source = MainsSource(
            self.mains,
            seq_length=data_pipeline.source.seq_length,
            stride=self.stride
        )
        mains_data_pipeline = DataPipeline(
            mains_source,
            num_seq_per_batch=data_pipeline.num_seq_per_batch,
            input_processsing=data_pipeline.input_processing
        )
        return mains_data_pipeline
    
    def disaggregate(self, trainer):
        net = trainer.net
        mains_data_pipeline = self._get_mains_data_pipeline(trainer)
        while True:
            try:
                batch = mains_data_pipeline.get_batch()
            except:
                break
            output_for_batch = net.deterministic_output_func(batch.input)
            output_for_batch = mains_data_pipeline.convert_to_watts(output_for_batch)
            # TODO:
            # merge overlapping segments
            # save disag output to self.output_path

In [ ]:
plotter = Plotter()
disaggregator = Disaggregator()


trainer = Trainer(
    net,
    data_pipeline=None,
    repeat_callbacks=[
        (  10, Trainer.validate), 
        (1000, plotter.plot),
        (1000, disaggregator.disaggregate),
        (1000, disag_metrics.run_metrics)
    ]
)

In [ ]:
def get_data_pipeline_0():
    APPLIANCES = ['fridge', 'kettle', 'toaster']
    TARGET_APPLIANCE = 'kettle'
    NILMTK_FILENAME = '/data/mine/vadeec/merged/ukdale.h5'
    SEQ_LENGTH = 1024
    CONFIG = {
        'nilmtk_activations': {
            'appliances': APPLIANCES,
            'filename': NILMTK_FILENAME,
            'window_per_building': {},
            'buildings': [1]
        },
        'tracebase_activations': {
            'appliances': APPLIANCES,
            'path': '/data/tracebase'
        }
    }

    nilmtk_activations = load_nilmtk_activations(**CONFIG['nilmtk_activations'])    
    tracebase_activations = load_tracebase_activations(**CONFIG['tracebase_activations'])
    
    # These activations are dicts of the form:
    # {'kettle': {'UK-DALE_house_1': [], 'TraceBase_house_1': []}}
    
    all_activations = merge_activations(nilmtk_activations, tracebase_activations)
    
    synthetic_source = SyntheticAggregateSource(
        all_activations, 
        target_appliance=TARGET_APPLIANCE,
        uniform_prob_of_selecting_each_building=True,
        seq_length=SEQ_LENGTH
    )
    
    # Real data
    real_source = NILMTKSource(
        filename=NILMTK_FILENAME,
        target_appliance=TARGET_APPLIANCE,
        train_window_per_building={},
        train_building=[1, 2, 3],
        validation_buildings=[5],
        seq_length=SEQ_LENGTH
    )
    
    # Get standard deviation of input
    sample_sequences = []
    NUM_SEQ_TO_SAMPLE = 512
    for i in range(NUM_SEQ_TO_SAMPLE):
        sample_sequences.append(real_source.get_data().flatten())
    sample = np.concatenate(sample_sequences)
    del sample_sequences
    input_std = sample.std()
    del sample
    
    data_pipeline = DataPipeline(
        sources=[synthetic_source, real_source],
        train_probs=[0.5, 0.5],
        validation_probs=[0.0, 1.0],
        num_seq_per_batch=64,
        input_processing=[
            DivideBy(input_std), 
            Downsample(3),
            IndepdendentlyCentre()
        ],
        target_processing=[
            DivideBy(APPLIANCE_PARAMS['max_power'][TARGET_APPLIANCE])
        ]
    )
    
    data_pipeline_thread = DataPipeLineThread(data_pipeline)
    data_pipeline_thread.start()
    return data_pipeline_thread


experiments = [
        {
            'data_pipeline': get_data_pipeline_0,
            'net': get_net_0
        }
    ]

def run_experiments():
    for experiment in experiments:
        data_pipeline = experiment['data_pipeline']()
        validation_data = pipeline.get_data(validation=True)
        input_shape = validation_data.shape
        
        output_layer = experiment['net'](input_shape)
        net = Net(output_layer)
        
        trainer = Trainer(
            net=net,
            data_pipeline=data_pipeline,
            loss_function=squared_error,
            repeat_callbacks={100: [plotter.plot_estimates, save_parameters, PlotTSNE(layers=[2])]}
        )