|
|
import pickle |
|
|
import torch |
|
|
import numpy as np |
|
|
import pandas as pd |
|
|
import scipy.sparse as sp |
|
|
from scipy.sparse import linalg |
|
|
|
|
|
class DataLoader(object): |
|
|
def __init__(self, xs, ys, batch_size, pad_with_last_sample=True, shuffle=False): |
|
|
""" |
|
|
|
|
|
:param xs: |
|
|
:param ys: |
|
|
:param batch_size: |
|
|
:param pad_with_last_sample: pad with the last sample to make number of samples divisible to batch_size. |
|
|
""" |
|
|
self.batch_size = batch_size |
|
|
self.current_ind = 0 |
|
|
if pad_with_last_sample: |
|
|
num_padding = (batch_size - (len(xs) % batch_size)) % batch_size |
|
|
x_padding = np.repeat(xs[-1:], num_padding, axis=0) |
|
|
y_padding = np.repeat(ys[-1:], num_padding, axis=0) |
|
|
xs = np.concatenate([xs, x_padding], axis=0) |
|
|
ys = np.concatenate([ys, y_padding], axis=0) |
|
|
self.size = len(xs) |
|
|
self.num_batch = int(self.size // self.batch_size) |
|
|
if shuffle: |
|
|
permutation = np.random.permutation(self.size) |
|
|
xs, ys = xs[permutation], ys[permutation] |
|
|
self.xs = xs |
|
|
self.ys = ys |
|
|
|
|
|
def get_iterator(self): |
|
|
self.current_ind = 0 |
|
|
|
|
|
def _wrapper(): |
|
|
while self.current_ind < self.num_batch: |
|
|
start_ind = self.batch_size * self.current_ind |
|
|
end_ind = min(self.size, self.batch_size * (self.current_ind + 1)) |
|
|
x_i = self.xs[start_ind: end_ind, ...] |
|
|
y_i = self.ys[start_ind: end_ind, ...] |
|
|
yield (x_i, y_i) |
|
|
self.current_ind += 1 |
|
|
|
|
|
return _wrapper() |
|
|
|
|
|
class StandardScaler(): |
|
|
def __init__(self, mean, std): |
|
|
self.mean = mean |
|
|
self.std = std |
|
|
|
|
|
def transform(self, data): |
|
|
return (data - self.mean) / self.std |
|
|
|
|
|
def inverse_transform(self, data): |
|
|
return (data * self.std) + self.mean |
|
|
|
|
|
def getTimestamp(data): |
|
|
num_samples, num_nodes = data.shape |
|
|
time_ind = (data.index.values - data.index.values.astype("datetime64[D]")) / np.timedelta64(1, "D") |
|
|
time_in_day = np.tile(time_ind, [num_nodes,1]).transpose((1, 0)) |
|
|
return time_in_day |
|
|
|
|
|
def getDayTimestamp(data): |
|
|
|
|
|
df = pd.DataFrame({'timestamp':data.index.values}) |
|
|
df['weekdaytime'] = df['timestamp'].dt.weekday * 288 + (df['timestamp'].dt.hour * 60 + df['timestamp'].dt.minute)//5 |
|
|
df['weekdaytime'] = df['weekdaytime'] / df['weekdaytime'].max() |
|
|
num_samples, num_nodes = data.shape |
|
|
time_ind = df['weekdaytime'].values |
|
|
time_ind_node = np.tile(time_ind, [num_nodes,1]).transpose((1, 0)) |
|
|
return time_ind_node |
|
|
|
|
|
def getDayTimestamp_(start, end, freq, num_nodes): |
|
|
|
|
|
df = pd.DataFrame({'timestamp':pd.date_range(start=start, end=end, freq=freq)}) |
|
|
df['weekdaytime'] = df['timestamp'].dt.weekday * 288 + (df['timestamp'].dt.hour * 60 + df['timestamp'].dt.minute)//5 |
|
|
df['weekdaytime'] = df['weekdaytime'] / df['weekdaytime'].max() |
|
|
time_ind = df['weekdaytime'].values |
|
|
time_ind_node = np.tile(time_ind, [num_nodes, 1]).transpose((1, 0)) |
|
|
return time_ind_node |
|
|
|
|
|
def masked_mse(preds, labels, null_val=1e-3): |
|
|
if np.isnan(null_val): |
|
|
mask = ~torch.isnan(labels) |
|
|
else: |
|
|
mask = (labels > null_val) |
|
|
mask = mask.float() |
|
|
mask /= torch.mean((mask)) |
|
|
mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) |
|
|
loss = (preds-labels)**2 |
|
|
loss = loss * mask |
|
|
loss = torch.where(torch.isnan(loss), torch.zeros_like(loss), loss) |
|
|
return torch.mean(loss) |
|
|
|
|
|
def masked_rmse(preds, labels, null_val=1e-3): |
|
|
return torch.sqrt(masked_mse(preds=preds, labels=labels, null_val=null_val)) |
|
|
|
|
|
|
|
|
def masked_mae(preds, labels, null_val=1e-3): |
|
|
if np.isnan(null_val): |
|
|
mask = ~torch.isnan(labels) |
|
|
else: |
|
|
mask = (labels > null_val) |
|
|
mask = mask.float() |
|
|
mask /= torch.mean((mask)) |
|
|
mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) |
|
|
loss = torch.abs(preds-labels) |
|
|
loss = loss * mask |
|
|
loss = torch.where(torch.isnan(loss), torch.zeros_like(loss), loss) |
|
|
return torch.mean(loss) |
|
|
|
|
|
|
|
|
def masked_mape(preds, labels, null_val=1e-3): |
|
|
if np.isnan(null_val): |
|
|
mask = ~torch.isnan(labels) |
|
|
else: |
|
|
mask = (labels > null_val) |
|
|
mask = mask.float() |
|
|
mask /= torch.mean((mask)) |
|
|
mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) |
|
|
loss = torch.abs(preds-labels)/labels |
|
|
loss = loss * mask |
|
|
loss = torch.where(torch.isnan(loss), torch.zeros_like(loss), loss) |
|
|
return torch.mean(loss) |
|
|
|
|
|
|
|
|
def masked_mae_loss(y_pred, y_true): |
|
|
mask = (y_true != 0).float() |
|
|
mask /= mask.mean() |
|
|
loss = torch.abs(y_pred - y_true) |
|
|
loss = loss * mask |
|
|
|
|
|
loss[loss != loss] = 0 |
|
|
return loss.mean() |
|
|
|
|
|
def masked_mape_loss(y_pred, y_true): |
|
|
mask = (y_true != 0).float() |
|
|
mask /= mask.mean() |
|
|
loss = torch.abs(torch.div(y_true - y_pred, y_true)) |
|
|
loss = loss * mask |
|
|
|
|
|
loss[loss != loss] = 0 |
|
|
return loss.mean() |
|
|
|
|
|
def masked_rmse_loss(y_pred, y_true): |
|
|
mask = (y_true != 0).float() |
|
|
mask /= mask.mean() |
|
|
loss = torch.pow(y_true - y_pred, 2) |
|
|
loss = loss * mask |
|
|
|
|
|
loss[loss != loss] = 0 |
|
|
return torch.sqrt(loss.mean()) |
|
|
|
|
|
def masked_mse_loss(y_pred, y_true): |
|
|
mask = (y_true != 0).float() |
|
|
mask /= mask.mean() |
|
|
loss = torch.pow(y_true - y_pred, 2) |
|
|
loss = loss * mask |
|
|
|
|
|
loss[loss != loss] = 0 |
|
|
return loss.mean() |
|
|
|
|
|
def load_pickle(pickle_file): |
|
|
try: |
|
|
with open(pickle_file, 'rb') as f: |
|
|
pickle_data = pickle.load(f) |
|
|
except UnicodeDecodeError as e: |
|
|
with open(pickle_file, 'rb') as f: |
|
|
pickle_data = pickle.load(f, encoding='latin1') |
|
|
except Exception as e: |
|
|
print('Unable to load data ', pickle_file, ':', e) |
|
|
raise |
|
|
return pickle_data |
|
|
|
|
|
def sym_adj(adj): |
|
|
"""Symmetrically normalize adjacency matrix.""" |
|
|
adj = sp.coo_matrix(adj) |
|
|
rowsum = np.array(adj.sum(1)) |
|
|
d_inv_sqrt = np.power(rowsum, -0.5).flatten() |
|
|
d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0. |
|
|
d_mat_inv_sqrt = sp.diags(d_inv_sqrt) |
|
|
return adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).astype(np.float32).todense() |
|
|
|
|
|
def asym_adj(adj): |
|
|
adj = sp.coo_matrix(adj) |
|
|
rowsum = np.array(adj.sum(1)).flatten() |
|
|
d_inv = np.power(rowsum, -1).flatten() |
|
|
d_inv[np.isinf(d_inv)] = 0. |
|
|
d_mat = sp.diags(d_inv) |
|
|
return d_mat.dot(adj).astype(np.float32).todense() |
|
|
|
|
|
def calculate_normalized_laplacian(adj): |
|
|
""" |
|
|
# L = D^-1/2 (D-A) D^-1/2 = I - D^-1/2 A D^-1/2 |
|
|
# D = diag(A 1) |
|
|
:param adj: |
|
|
:return: |
|
|
""" |
|
|
adj = sp.coo_matrix(adj) |
|
|
d = np.array(adj.sum(1)) |
|
|
d_inv_sqrt = np.power(d, -0.5).flatten() |
|
|
d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0. |
|
|
d_mat_inv_sqrt = sp.diags(d_inv_sqrt) |
|
|
normalized_laplacian = sp.eye(adj.shape[0]) - adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).tocoo() |
|
|
return normalized_laplacian |
|
|
|
|
|
def calculate_random_walk_matrix(adj_mx): |
|
|
adj_mx = sp.coo_matrix(adj_mx) |
|
|
d = np.array(adj_mx.sum(1)) |
|
|
d_inv = np.power(d, -1).flatten() |
|
|
d_inv[np.isinf(d_inv)] = 0. |
|
|
d_mat_inv = sp.diags(d_inv) |
|
|
random_walk_mx = d_mat_inv.dot(adj_mx).tocoo() |
|
|
return random_walk_mx |
|
|
|
|
|
def calculate_reverse_random_walk_matrix(adj_mx): |
|
|
return calculate_random_walk_matrix(np.transpose(adj_mx)) |
|
|
|
|
|
def calculate_scaled_laplacian(adj_mx, lambda_max=2, undirected=True): |
|
|
if undirected: |
|
|
adj_mx = np.maximum.reduce([adj_mx, adj_mx.T]) |
|
|
L = calculate_normalized_laplacian(adj_mx) |
|
|
if lambda_max is None: |
|
|
lambda_max, _ = linalg.eigsh(L, 1, which='LM') |
|
|
lambda_max = lambda_max[0] |
|
|
L = sp.csr_matrix(L) |
|
|
M, _ = L.shape |
|
|
I = sp.identity(M, format='csr', dtype=L.dtype) |
|
|
L = (2 / lambda_max * L) - I |
|
|
return L.astype(np.float32) |
|
|
|
|
|
def load_adj(pkl_filename, adjtype): |
|
|
if "PEMS0" in pkl_filename or "D7" in pkl_filename: |
|
|
adj_mx = load_pickle(pkl_filename) |
|
|
else: |
|
|
sensor_ids, sensor_id_to_ind, adj_mx = load_pickle(pkl_filename) |
|
|
if adjtype == "scalap": |
|
|
adj = [calculate_scaled_laplacian(adj_mx)] |
|
|
elif adjtype == "normlap": |
|
|
adj = [calculate_normalized_laplacian(adj_mx).astype(np.float32).todense()] |
|
|
elif adjtype == "symadj": |
|
|
adj = [sym_adj(adj_mx)] |
|
|
elif adjtype == "transition": |
|
|
adj = [asym_adj(adj_mx)] |
|
|
elif adjtype == "doubletransition": |
|
|
adj = [asym_adj(adj_mx), asym_adj(np.transpose(adj_mx))] |
|
|
elif adjtype == "identity": |
|
|
adj = [np.diag(np.ones(adj_mx.shape[0])).astype(np.float32)] |
|
|
else: |
|
|
error = 0 |
|
|
assert error, "adj type not defined" |
|
|
return adj |
|
|
|
|
|
def print_params(model): |
|
|
|
|
|
param_count = 0 |
|
|
print('Trainable parameter list:') |
|
|
for name, param in model.named_parameters(): |
|
|
if param.requires_grad: |
|
|
print(name, param.shape, param.numel()) |
|
|
param_count += param.numel() |
|
|
print(f'\n In total: {param_count} trainable parameters. \n') |
|
|
return |