Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
| | ├── disaggregator.py //Base Class
| | ├── energan_pytorch.py //EnerGAN
| | ├── seq2point_pytorch.py //Seq2Point
| | ├── Differential_Pytorch //Differential input
| | ├── attention_cnn_pytorch.py //CNN_Attention
| | ├── seq2seqcnn_pytorch.py //CNN_Seq2Seq
| | ├── bilstm_pytorch_multidim.py //Multiple input features BiLSTM
Expand Down Expand Up @@ -104,6 +105,9 @@ And several NILM algorithms with '_**multidim**' suffix, such as bilstm_pytorch_
[8] XU Xiaohui, ZHAO Shutao, CUI Kebin. Non-intrusive Load Disaggregate Algorithm Based on Convolutional Block Attention Module[J/0L]. Power System Technology, 2021: 1-8.

[9] ISOLA P, ZHU JY, ZHOU T, et al. Image-to-Image Translation with Conditional Adversarial Networks[C/OL]//2017 IEEE Conference on Computer Vision and Pattern Recognition (CVPR). Honolulu, HI: IEEE, 2017: 5967–5976.

[10] Zhang, Y., Yang, G., & Ma, S. (2019). Non-intrusive load monitoring based on convolutional neural network with differential input. Procedia CIRP, 83, 670-674.


------

Expand Down
323 changes: 323 additions & 0 deletions nilmtk/disaggregate/Differential_Pytorch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
# Package import
from __future__ import print_function, division
from warnings import warn
from nilmtk.disaggregate import Disaggregator
import os
import pickle
import pandas as pd
import numpy as np
from collections import OrderedDict
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import random
import sys
import torch
from torchsummary import summary
import torch.nn as nn
import torch.utils.data as tud
from torch.utils.data.dataset import TensorDataset
from torch.utils.tensorboard import SummaryWriter
import time

# Fix the random seed to ensure the reproducibility of the experiment
random_seed = 10
random.seed(random_seed)
np.random.seed(random_seed)
torch.manual_seed(random_seed)
torch.cuda.manual_seed_all(random_seed)

torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

# Use cuda or not
USE_CUDA = torch.cuda.is_available

class differential_Pytorch(nn.Module):
def __init__(self, sequence_length):
# Refer to "Zhang, Y., Yang, G., & Ma, S. (2019). Non-intrusive load monitoring based on convolutional neural network with differential input. Procedia CIRP, 83, 670-674."
super(differential_Pytorch, self).__init__()
self.seq_length = sequence_length

self.conv = nn.Sequential(
nn.ConstantPad1d((4, 5), 0),
nn.Conv1d(1, 30, 10, stride=1),
nn.ReLU(True),
nn.ConstantPad1d((3, 4), 0),
nn.Conv1d(30, 30, 8, stride=1),
nn.ReLU(True),
nn.ConstantPad1d((2, 3), 0),
nn.Conv1d(30, 40, 6, stride=1),
nn.ReLU(True),
nn.ConstantPad1d((2, 2), 0),
nn.Conv1d(40, 50, 5, stride=1),
nn.ReLU(True),
nn.ConstantPad1d((2, 2), 0),
nn.Conv1d(50, 50, 5, stride=1),
nn.ReLU(True)
)

self.dense = nn.Sequential(
nn.Linear((50 * sequence_length) + 4, 1024),
nn.ReLU(),
nn.Linear(1024, 1)
)

def forward(self, x):
# shape of x is [batch_size, 1, seq_length]
x_t = x[:,:,1:]
x_t_minus_1 = x[:,:,:-1]
input = torch.add(torch.mul((1 + self.n2 ), (x_t - x_t_minus_1)), torch.mul((self.n1 + self.n2), x_t_minus_1))

output = self.conv(input)
output = output.reshape(output.size(0), -1)
mean = torch.mean(x, dim = 2)
std = torch.std(x, dim = 2)
maxvalue = torch.max(x, dim = 2)
minvalue = torch.min(x, dim = 2)
output = torch.cat((output, mean, std, maxvalue, minvalue), dim = 1)
output = self.dense(output)
return output #[batch_size , 1]


def initialize(layer):
# Xavier_uniform will be applied to conv1d and dense layer, to be sonsistent with Keras and Tensorflow
if isinstance(layer,nn.Conv1d) or isinstance(layer, nn.Linear):
torch.nn.init.xavier_uniform_(layer.weight.data)
if layer.bias is not None:
torch.nn.init.constant_(layer.bias.data, val = 0.0)

def train(appliance_name, model, mains, appliance, epochs, batch_size, pretrain = False,checkpoint_interval = None, train_patience = 3):
# Model configuration
if USE_CUDA:
model = model.cuda()
if not pretrain:
model.apply(initialize)
summary(model, (1, mains.shape[1]))
# Split the train and validation set
train_mains,valid_mains,train_appliance,valid_appliance = train_test_split(mains, appliance, test_size=.2, random_state = random_seed)

# Create optimizer, loss function, and dataloader
optimizer = torch.optim.Adam(model.parameters(), lr = 1e-3)
loss_fn = torch.nn.MSELoss(reduction = 'mean')

train_dataset = TensorDataset(torch.from_numpy(train_mains).float().permute(0,2,1), torch.from_numpy(train_appliance).float())
train_loader = tud.DataLoader(train_dataset, batch_size = batch_size, shuffle = True, num_workers = 0, drop_last = True)

valid_dataset = TensorDataset(torch.from_numpy(valid_mains).float().permute(0,2,1), torch.from_numpy(valid_appliance).float())
valid_loader = tud.DataLoader(valid_dataset, batch_size = batch_size, shuffle = True, num_workers = 0, drop_last = True)

writer = SummaryWriter(comment='train_visual')
patience, best_loss = 0, None

for epoch in range(epochs):
# Earlystopping
if(patience == train_patience):
print("val_loss did not improve after {} Epochs, thus Earlystopping is calling".format(train_patience))
break
# train the model
model.train()
st = time.time()
for i, (batch_mains, batch_appliance) in enumerate(train_loader):
if USE_CUDA:
batch_mains = batch_mains.cuda()
batch_appliance = batch_appliance.cuda()

batch_pred = model(batch_mains)
loss = loss_fn(batch_appliance, batch_pred)

model.zero_grad()
loss.backward()
optimizer.step()
ed = time.time()

# Evaluate the model
model.eval()
with torch.no_grad():
cnt, loss_sum = 0, 0
for i, (batch_mains, batch_appliance) in enumerate(valid_loader):
if USE_CUDA:
batch_mains = batch_mains.cuda()
batch_appliance = batch_appliance.cuda()

batch_pred = model(batch_mains)
loss = loss_fn(batch_appliance, batch_pred)
loss_sum += loss
cnt += 1

final_loss = loss_sum / cnt
final_loss = loss_sum / cnt
# Save best only
if best_loss is None or final_loss < best_loss:
best_loss = final_loss
patience = 0
net_state_dict = model.state_dict()
path_state_dict = "./"+appliance_name+"_differential_best_state_dict.pt"
torch.save(net_state_dict, path_state_dict)
else:
patience = patience + 1

print("Epoch: {}, Valid_Loss: {}, Time consumption: {}s.".format(epoch, final_loss, ed - st))

# For the visualization of training process
for name,param in model.named_parameters():
writer.add_histogram(name + '_grad', param.grad, epoch)
writer.add_histogram(name + '_data', param, epoch)
writer.add_scalars("MSELoss", {"Valid":final_loss}, epoch)

# Save checkpoint
if (checkpoint_interval != None) and ((epoch + 1) % checkpoint_interval == 0):
checkpoint = {"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"epoch": epoch}
path_checkpoint = "./"+appliance_name+"_differential_{}_epoch.pkl".format(epoch)
torch.save(checkpoint, path_checkpoint)

def test(model, test_mains, batch_size = 512):
# Model test
st = time.time()
model.eval()
# Create test dataset and dataloader
batch_size = test_mains.shape[0] if batch_size > test_mains.shape[0] else batch_size
test_dataset = TensorDataset(torch.from_numpy(test_mains).float().permute(0,2,1))
test_loader = tud.DataLoader(test_dataset, batch_size = batch_size, shuffle = False, num_workers = 0)
with torch.no_grad():
for i, batch_mains in enumerate(test_loader):
batch_pred = model(batch_mains[0])
if i == 0:
res = batch_pred
else:
res = torch.cat((res, batch_pred), dim = 0)
ed = time.time()
print("Inference Time consumption: {}s.".format(ed - st))
return res.numpy()

class Differential(Disaggregator):

def __init__(self, params):
self.MODEL_NAME = "Differential"
self.models = OrderedDict()
self.chunk_wise_training = params.get('chunk_wise_training',False)
self.sequence_length = params.get('sequence_length',129)
self.n_epochs = params.get('n_epochs', 10 )
self.batch_size = params.get('batch_size',512)
self.appliance_params = params.get('appliance_params',{})
self.mains_mean = params.get('mains_mean',None)
self.mains_std = params.get('mains_std',None)
if self.sequence_length % 2 == 0:
print ("Sequence length should be odd!")
raise (SequenceLengthError)

def partial_fit(self,train_main,train_appliances,pretrain = False, do_preprocessing=True, **load_kwargs):
# Seq2Point version
# If no appliance wise parameters are provided, then copmute them using the first chunk
if len(self.appliance_params) == 0:
self.set_appliance_params(train_appliances)

print("...............Differential partial_fit running...............")
# Preprocess the data and bring it to a valid shape

if do_preprocessing:
train_main, train_appliances = self.call_preprocessing(
train_main, train_appliances, 'train')

train_main = pd.concat(train_main,axis=0)
train_main = train_main.values.reshape((-1,self.sequence_length,1))

new_train_appliances = []
for app_name, app_df in train_appliances:
app_df = pd.concat(app_df,axis=0)
app_df_values = app_df.values.reshape((-1,1))
new_train_appliances.append((app_name, app_df_values))
train_appliances = new_train_appliances

for appliance_name, power in train_appliances:
if appliance_name not in self.models:
print("First model training for ", appliance_name)
self.models[appliance_name] = differential_Pytorch(self.sequence_length)
# Load pretrain dict or not
if pretrain is True:
self.models[appliance_name].load_state_dict(torch.load("./"+appliance_name+"_differential_pre_state_dict.pt"))

model = self.models[appliance_name]
train(appliance_name, model, train_main, power, self.n_epochs, self.batch_size,pretrain,checkpoint_interval = 3)
# Model test will be based on the best model
self.models[appliance_name].load_state_dict(torch.load("./"+appliance_name+"_differential_best_state_dict.pt"))


def disaggregate_chunk(self,test_main_list,model=None,do_preprocessing=True):
# Disaggregate (test process)
if do_preprocessing:
test_main_list = self.call_preprocessing(test_main_list, submeters_lst=None, method='test')

test_predictions = []
for test_main in test_main_list:
test_main = test_main.values
test_main = test_main.reshape((-1, self.sequence_length, 1))
disggregation_dict = {}
for appliance in self.models:
# Move the model to cpu, and then test it
model = self.models[appliance].to('cpu')
prediction = test(model, test_main)
prediction = self.appliance_params[appliance]['mean'] + prediction * self.appliance_params[appliance]['std']
valid_predictions = prediction.flatten()
valid_predictions = np.where(valid_predictions > 0, valid_predictions, 0)
df = pd.Series(valid_predictions)
disggregation_dict[appliance] = df
results = pd.DataFrame(disggregation_dict, dtype='float32')
test_predictions.append(results)
return test_predictions

def call_preprocessing(self, mains_lst, submeters_lst, method):
# Seq2Point Version
if method == 'train':
# Preprocess the main and appliance data, the parameter 'overlapping' will be set 'True'
mains_df_list = []
for mains in mains_lst:
new_mains = mains.values.flatten()
self.mains_mean, self.mains_std = new_mains.mean(), new_mains.std()
n = self.sequence_length
units_to_pad = n // 2
new_mains = np.pad(new_mains,(units_to_pad,units_to_pad),'constant',constant_values=(0,0))
new_mains = np.array([new_mains[i:i + n] for i in range(len(new_mains) - n + 1)])
new_mains = (new_mains - self.mains_mean) / self.mains_std
mains_df_list.append(pd.DataFrame(new_mains))

appliance_list = []
for app_index, (app_name, app_df_list) in enumerate(submeters_lst):
if app_name in self.appliance_params:
app_mean = self.appliance_params[app_name]['mean']
app_std = self.appliance_params[app_name]['std']
else:
print ("Parameters for ", app_name ," were not found!")
raise ApplianceNotFoundError()

processed_appliance_dfs = []

for app_df in app_df_list:
new_app_readings = app_df.values.reshape((-1, 1))
new_app_readings = (new_app_readings - app_mean) / app_std
processed_appliance_dfs.append(pd.DataFrame(new_app_readings))
appliance_list.append((app_name, processed_appliance_dfs))
return mains_df_list, appliance_list

else:
# Preprocess the main data only, the parameter 'overlapping' will be set 'False'
mains_df_list = []

for mains in mains_lst:
new_mains = mains.values.flatten()
n = self.sequence_length
units_to_pad = n // 2
new_mains = np.pad(new_mains,(units_to_pad,units_to_pad),'constant',constant_values=(0,0))
new_mains = np.array([new_mains[i:i + n] for i in range(len(new_mains) - n + 1)])
new_mains = (new_mains - new_mains.mean()) / new_mains.std()
mains_df_list.append(pd.DataFrame(new_mains))
return mains_df_list

def set_appliance_params(self, train_appliances):
# Set appliance mean and std to normalize the label(appliance data)
for (app_name, df_list) in train_appliances:
l = np.array(pd.concat(df_list, axis = 0))
app_mean = np.mean(l)
app_std = np.std(l)
self.appliance_params.update({app_name:{'mean':app_mean,'std':app_std}})