first commit
This commit is contained in:
74
utils/ADFtest.py
Normal file
74
utils/ADFtest.py
Normal file
@ -0,0 +1,74 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import os
|
||||
from statsmodels.tsa.stattools import adfuller
|
||||
from arch.unitroot import ADF
|
||||
|
||||
def calculate_ADF(root_path,data_path):
|
||||
df_raw = pd.read_csv(os.path.join(root_path,data_path))
|
||||
cols = list(df_raw.columns)
|
||||
cols.remove('date')
|
||||
df_raw = df_raw[cols]
|
||||
adf_list = []
|
||||
for i in cols:
|
||||
df_data = df_raw[i]
|
||||
adf = adfuller(df_data, maxlag = 1)
|
||||
print(adf)
|
||||
adf_list.append(adf)
|
||||
return np.array(adf_list)
|
||||
|
||||
def calculate_target_ADF(root_path,data_path,target='OT'):
|
||||
df_raw = pd.read_csv(os.path.join(root_path,data_path))
|
||||
target_cols = target.split(',')
|
||||
# df_data = df_raw[target]
|
||||
df_raw = df_raw[target_cols]
|
||||
adf_list = []
|
||||
for i in target_cols:
|
||||
df_data = df_raw[i]
|
||||
adf = adfuller(df_data, maxlag = 1)
|
||||
# print(adf)
|
||||
adf_list.append(adf)
|
||||
return np.array(adf_list)
|
||||
|
||||
def archADF(root_path, data_path):
|
||||
df = pd.read_csv(os.path.join(root_path,data_path))
|
||||
cols = df.columns[1:]
|
||||
stats = 0
|
||||
for target_col in cols:
|
||||
series = df[target_col].values
|
||||
adf = ADF(series)
|
||||
stat = adf.stat
|
||||
stats += stat
|
||||
return stats/len(cols)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# * Exchange - result: -1.902402344564288 | report: -1.889
|
||||
ADFmetric = archADF(root_path="./dataset/exchange_rate/",data_path="exchange_rate.csv")
|
||||
print("Exchange ADF metric", ADFmetric)
|
||||
|
||||
# * Illness - result: -5.33416661870624 | report: -5.406
|
||||
ADFmetric = archADF(root_path="./dataset/illness/",data_path="national_illness.csv")
|
||||
print("Illness ADF metric", ADFmetric)
|
||||
|
||||
# * ETTm2 - result: -5.663628743471695 | report: -6.225
|
||||
ADFmetric = archADF(root_path="./dataset/ETT-small/",data_path="ETTm2.csv")
|
||||
print("ETTm2 ADF metric", ADFmetric)
|
||||
|
||||
# * Electricity - result: -8.44485821939281 | report: -8.483
|
||||
ADFmetric = archADF(root_path="./dataset/electricity/",data_path="electricity.csv")
|
||||
print("Electricity ADF metric", ADFmetric)
|
||||
|
||||
# * Traffic - result: -15.020978067839014 | report: -15.046
|
||||
ADFmetric = archADF(root_path="./dataset/traffic/",data_path="traffic.csv")
|
||||
print("Traffic ADF metric", ADFmetric)
|
||||
|
||||
# * Weather - result: -26.681433085204866 | report: -26.661
|
||||
ADFmetric = archADF(root_path="./dataset/weather/",data_path="weather.csv")
|
||||
print("Weather ADF metric", ADFmetric)
|
||||
|
||||
|
||||
# print(ADFmetric)
|
||||
|
||||
# mean_ADFmetric = ADFmetric[:,0].mean()
|
||||
# print(mean_ADFmetric)
|
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
434
utils/augmentation.py
Normal file
434
utils/augmentation.py
Normal file
@ -0,0 +1,434 @@
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
def jitter(x, sigma=0.03):
|
||||
# https://arxiv.org/pdf/1706.00527.pdf
|
||||
return x + np.random.normal(loc=0., scale=sigma, size=x.shape)
|
||||
|
||||
|
||||
def scaling(x, sigma=0.1):
|
||||
# https://arxiv.org/pdf/1706.00527.pdf
|
||||
factor = np.random.normal(loc=1., scale=sigma, size=(x.shape[0],x.shape[2]))
|
||||
return np.multiply(x, factor[:,np.newaxis,:])
|
||||
|
||||
def rotation(x):
|
||||
x = np.array(x)
|
||||
flip = np.random.choice([-1, 1], size=(x.shape[0],x.shape[2]))
|
||||
rotate_axis = np.arange(x.shape[2])
|
||||
np.random.shuffle(rotate_axis)
|
||||
return flip[:,np.newaxis,:] * x[:,:,rotate_axis]
|
||||
|
||||
def permutation(x, max_segments=5, seg_mode="equal"):
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
|
||||
num_segs = np.random.randint(1, max_segments, size=(x.shape[0]))
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
for i, pat in enumerate(x):
|
||||
if num_segs[i] > 1:
|
||||
if seg_mode == "random":
|
||||
split_points = np.random.choice(x.shape[1]-2, num_segs[i]-1, replace=False)
|
||||
split_points.sort()
|
||||
splits = np.split(orig_steps, split_points)
|
||||
else:
|
||||
splits = np.array_split(orig_steps, num_segs[i])
|
||||
warp = np.concatenate(np.random.permutation(splits)).ravel()
|
||||
# ? Question: What is the point of making segments?
|
||||
# for i in range(len(splits)):
|
||||
# permute = np.random.permutation(splits[i])
|
||||
|
||||
|
||||
ret[i] = pat[warp]
|
||||
else:
|
||||
ret[i] = pat
|
||||
return ret
|
||||
|
||||
def magnitude_warp(x, sigma=0.2, knot=4):
|
||||
from scipy.interpolate import CubicSpline
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
|
||||
random_warps = np.random.normal(loc=1.0, scale=sigma, size=(x.shape[0], knot+2, x.shape[2]))
|
||||
warp_steps = (np.ones((x.shape[2],1))*(np.linspace(0, x.shape[1]-1., num=knot+2))).T
|
||||
ret = np.zeros_like(x)
|
||||
for i, pat in enumerate(x):
|
||||
warper = np.array([CubicSpline(warp_steps[:,dim], random_warps[i,:,dim])(orig_steps) for dim in range(x.shape[2])]).T
|
||||
ret[i] = pat * warper
|
||||
|
||||
return ret
|
||||
|
||||
def time_warp(x, sigma=0.2, knot=4):
|
||||
from scipy.interpolate import CubicSpline
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
|
||||
random_warps = np.random.normal(loc=1.0, scale=sigma, size=(x.shape[0], knot+2, x.shape[2]))
|
||||
warp_steps = (np.ones((x.shape[2],1))*(np.linspace(0, x.shape[1]-1., num=knot+2))).T
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
for i, pat in enumerate(x):
|
||||
for dim in range(x.shape[2]):
|
||||
time_warp = CubicSpline(warp_steps[:,dim], warp_steps[:,dim] * random_warps[i,:,dim])(orig_steps)
|
||||
scale = (x.shape[1]-1)/time_warp[-1]
|
||||
ret[i,:,dim] = np.interp(orig_steps, np.clip(scale*time_warp, 0, x.shape[1]-1), pat[:,dim]).T
|
||||
return ret
|
||||
|
||||
def window_slice(x, reduce_ratio=0.9):
|
||||
# https://halshs.archives-ouvertes.fr/halshs-01357973/document
|
||||
target_len = np.ceil(reduce_ratio*x.shape[1]).astype(int)
|
||||
if target_len >= x.shape[1]:
|
||||
return x
|
||||
starts = np.random.randint(low=0, high=x.shape[1]-target_len, size=(x.shape[0])).astype(int)
|
||||
ends = (target_len + starts).astype(int)
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
for i, pat in enumerate(x):
|
||||
for dim in range(x.shape[2]):
|
||||
ret[i,:,dim] = np.interp(np.linspace(0, target_len, num=x.shape[1]), np.arange(target_len), pat[starts[i]:ends[i],dim]).T
|
||||
return ret
|
||||
|
||||
def window_warp(x, window_ratio=0.1, scales=[0.5, 2.]):
|
||||
# https://halshs.archives-ouvertes.fr/halshs-01357973/document
|
||||
warp_scales = np.random.choice(scales, x.shape[0])
|
||||
warp_size = np.ceil(window_ratio*x.shape[1]).astype(int)
|
||||
window_steps = np.arange(warp_size)
|
||||
|
||||
window_starts = np.random.randint(low=1, high=x.shape[1]-warp_size-1, size=(x.shape[0])).astype(int)
|
||||
window_ends = (window_starts + warp_size).astype(int)
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
for i, pat in enumerate(x):
|
||||
for dim in range(x.shape[2]):
|
||||
start_seg = pat[:window_starts[i],dim]
|
||||
window_seg = np.interp(np.linspace(0, warp_size-1, num=int(warp_size*warp_scales[i])), window_steps, pat[window_starts[i]:window_ends[i],dim])
|
||||
end_seg = pat[window_ends[i]:,dim]
|
||||
warped = np.concatenate((start_seg, window_seg, end_seg))
|
||||
ret[i,:,dim] = np.interp(np.arange(x.shape[1]), np.linspace(0, x.shape[1]-1., num=warped.size), warped).T
|
||||
return ret
|
||||
|
||||
def spawner(x, labels, sigma=0.05, verbose=0):
|
||||
# https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6983028/
|
||||
# use verbose=-1 to turn off warnings
|
||||
# use verbose=1 to print out figures
|
||||
|
||||
import utils.dtw as dtw
|
||||
random_points = np.random.randint(low=1, high=x.shape[1]-1, size=x.shape[0])
|
||||
window = np.ceil(x.shape[1] / 10.).astype(int)
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
l = np.argmax(labels, axis=1) if labels.ndim > 1 else labels
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
# for i, pat in enumerate(tqdm(x)):
|
||||
for i, pat in enumerate(x):
|
||||
# guarentees that same one isnt selected
|
||||
choices = np.delete(np.arange(x.shape[0]), i)
|
||||
# remove ones of different classes
|
||||
choices = np.where(l[choices] == l[i])[0]
|
||||
if choices.size > 0:
|
||||
random_sample = x[np.random.choice(choices)]
|
||||
# SPAWNER splits the path into two randomly
|
||||
path1 = dtw.dtw(pat[:random_points[i]], random_sample[:random_points[i]], dtw.RETURN_PATH, slope_constraint="symmetric", window=window)
|
||||
path2 = dtw.dtw(pat[random_points[i]:], random_sample[random_points[i]:], dtw.RETURN_PATH, slope_constraint="symmetric", window=window)
|
||||
combined = np.concatenate((np.vstack(path1), np.vstack(path2+random_points[i])), axis=1)
|
||||
if verbose:
|
||||
# print(random_points[i])
|
||||
dtw_value, cost, DTW_map, path = dtw.dtw(pat, random_sample, return_flag = dtw.RETURN_ALL, slope_constraint=slope_constraint, window=window)
|
||||
dtw.draw_graph1d(cost, DTW_map, path, pat, random_sample)
|
||||
dtw.draw_graph1d(cost, DTW_map, combined, pat, random_sample)
|
||||
mean = np.mean([pat[combined[0]], random_sample[combined[1]]], axis=0)
|
||||
for dim in range(x.shape[2]):
|
||||
ret[i,:,dim] = np.interp(orig_steps, np.linspace(0, x.shape[1]-1., num=mean.shape[0]), mean[:,dim]).T
|
||||
else:
|
||||
# if verbose > -1:
|
||||
# print("There is only one pattern of class {}, skipping pattern average".format(l[i]))
|
||||
ret[i,:] = pat
|
||||
return jitter(ret, sigma=sigma)
|
||||
|
||||
def wdba(x, labels, batch_size=6, slope_constraint="symmetric", use_window=True, verbose=0):
|
||||
# https://ieeexplore.ieee.org/document/8215569
|
||||
# use verbose = -1 to turn off warnings
|
||||
# slope_constraint is for DTW. "symmetric" or "asymmetric"
|
||||
x = np.array(x)
|
||||
import utils.dtw as dtw
|
||||
|
||||
if use_window:
|
||||
window = np.ceil(x.shape[1] / 10.).astype(int)
|
||||
else:
|
||||
window = None
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
l = np.argmax(labels, axis=1) if labels.ndim > 1 else labels
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
# for i in tqdm(range(ret.shape[0])):
|
||||
for i in range(ret.shape[0]):
|
||||
# get the same class as i
|
||||
choices = np.where(l == l[i])[0]
|
||||
if choices.size > 0:
|
||||
# pick random intra-class pattern
|
||||
k = min(choices.size, batch_size)
|
||||
random_prototypes = x[np.random.choice(choices, k, replace=False)]
|
||||
|
||||
# calculate dtw between all
|
||||
dtw_matrix = np.zeros((k, k))
|
||||
for p, prototype in enumerate(random_prototypes):
|
||||
for s, sample in enumerate(random_prototypes):
|
||||
if p == s:
|
||||
dtw_matrix[p, s] = 0.
|
||||
else:
|
||||
dtw_matrix[p, s] = dtw.dtw(prototype, sample, dtw.RETURN_VALUE, slope_constraint=slope_constraint, window=window)
|
||||
|
||||
# get medoid
|
||||
medoid_id = np.argsort(np.sum(dtw_matrix, axis=1))[0]
|
||||
nearest_order = np.argsort(dtw_matrix[medoid_id])
|
||||
medoid_pattern = random_prototypes[medoid_id]
|
||||
|
||||
# start weighted DBA
|
||||
average_pattern = np.zeros_like(medoid_pattern)
|
||||
weighted_sums = np.zeros((medoid_pattern.shape[0]))
|
||||
for nid in nearest_order:
|
||||
if nid == medoid_id or dtw_matrix[medoid_id, nearest_order[1]] == 0.:
|
||||
average_pattern += medoid_pattern
|
||||
weighted_sums += np.ones_like(weighted_sums)
|
||||
else:
|
||||
path = dtw.dtw(medoid_pattern, random_prototypes[nid], dtw.RETURN_PATH, slope_constraint=slope_constraint, window=window)
|
||||
dtw_value = dtw_matrix[medoid_id, nid]
|
||||
warped = random_prototypes[nid, path[1]]
|
||||
weight = np.exp(np.log(0.5)*dtw_value/dtw_matrix[medoid_id, nearest_order[1]])
|
||||
average_pattern[path[0]] += weight * warped
|
||||
weighted_sums[path[0]] += weight
|
||||
|
||||
ret[i,:] = average_pattern / weighted_sums[:,np.newaxis]
|
||||
else:
|
||||
# if verbose > -1:
|
||||
# print("There is only one pattern of class {}, skipping pattern average".format(l[i]))
|
||||
ret[i,:] = x[i]
|
||||
return ret
|
||||
|
||||
# Proposed
|
||||
|
||||
def random_guided_warp(x, labels, slope_constraint="symmetric", use_window=True, dtw_type="normal", verbose=0):
|
||||
# use verbose = -1 to turn off warnings
|
||||
# slope_constraint is for DTW. "symmetric" or "asymmetric"
|
||||
# dtw_type is for shapeDTW or DTW. "normal" or "shape"
|
||||
|
||||
import utils.dtw as dtw
|
||||
|
||||
if use_window:
|
||||
window = np.ceil(x.shape[1] / 10.).astype(int)
|
||||
else:
|
||||
window = None
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
l = np.argmax(labels, axis=1) if labels.ndim > 1 else labels
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
# for i, pat in enumerate(tqdm(x)):
|
||||
for i, pat in enumerate(x):
|
||||
# guarentees that same one isnt selected
|
||||
choices = np.delete(np.arange(x.shape[0]), i)
|
||||
# remove ones of different classes
|
||||
choices = np.where(l[choices] == l[i])[0]
|
||||
if choices.size > 0:
|
||||
# pick random intra-class pattern
|
||||
random_prototype = x[np.random.choice(choices)]
|
||||
|
||||
if dtw_type == "shape":
|
||||
path = dtw.shape_dtw(random_prototype, pat, dtw.RETURN_PATH, slope_constraint=slope_constraint, window=window)
|
||||
else:
|
||||
path = dtw.dtw(random_prototype, pat, dtw.RETURN_PATH, slope_constraint=slope_constraint, window=window)
|
||||
|
||||
# Time warp
|
||||
warped = pat[path[1]]
|
||||
for dim in range(x.shape[2]):
|
||||
ret[i,:,dim] = np.interp(orig_steps, np.linspace(0, x.shape[1]-1., num=warped.shape[0]), warped[:,dim]).T
|
||||
else:
|
||||
# if verbose > -1:
|
||||
# print("There is only one pattern of class {}, skipping timewarping".format(l[i]))
|
||||
ret[i,:] = pat
|
||||
return ret
|
||||
|
||||
def random_guided_warp_shape(x, labels, slope_constraint="symmetric", use_window=True):
|
||||
return random_guided_warp(x, labels, slope_constraint, use_window, dtw_type="shape")
|
||||
|
||||
def discriminative_guided_warp(x, labels, batch_size=6, slope_constraint="symmetric", use_window=True, dtw_type="normal", use_variable_slice=True, verbose=0):
|
||||
# use verbose = -1 to turn off warnings
|
||||
# slope_constraint is for DTW. "symmetric" or "asymmetric"
|
||||
# dtw_type is for shapeDTW or DTW. "normal" or "shape"
|
||||
|
||||
import utils.dtw as dtw
|
||||
|
||||
if use_window:
|
||||
window = np.ceil(x.shape[1] / 10.).astype(int)
|
||||
else:
|
||||
window = None
|
||||
orig_steps = np.arange(x.shape[1])
|
||||
l = np.argmax(labels, axis=1) if labels.ndim > 1 else labels
|
||||
|
||||
positive_batch = np.ceil(batch_size / 2).astype(int)
|
||||
negative_batch = np.floor(batch_size / 2).astype(int)
|
||||
|
||||
ret = np.zeros_like(x)
|
||||
warp_amount = np.zeros(x.shape[0])
|
||||
# for i, pat in enumerate(tqdm(x)):
|
||||
for i, pat in enumerate(x):
|
||||
# guarentees that same one isnt selected
|
||||
choices = np.delete(np.arange(x.shape[0]), i)
|
||||
|
||||
# remove ones of different classes
|
||||
positive = np.where(l[choices] == l[i])[0]
|
||||
negative = np.where(l[choices] != l[i])[0]
|
||||
|
||||
if positive.size > 0 and negative.size > 0:
|
||||
pos_k = min(positive.size, positive_batch)
|
||||
neg_k = min(negative.size, negative_batch)
|
||||
positive_prototypes = x[np.random.choice(positive, pos_k, replace=False)]
|
||||
negative_prototypes = x[np.random.choice(negative, neg_k, replace=False)]
|
||||
|
||||
# vector embedding and nearest prototype in one
|
||||
pos_aves = np.zeros((pos_k))
|
||||
neg_aves = np.zeros((pos_k))
|
||||
if dtw_type == "shape":
|
||||
for p, pos_prot in enumerate(positive_prototypes):
|
||||
for ps, pos_samp in enumerate(positive_prototypes):
|
||||
if p != ps:
|
||||
pos_aves[p] += (1./(pos_k-1.))*dtw.shape_dtw(pos_prot, pos_samp, dtw.RETURN_VALUE, slope_constraint=slope_constraint, window=window)
|
||||
for ns, neg_samp in enumerate(negative_prototypes):
|
||||
neg_aves[p] += (1./neg_k)*dtw.shape_dtw(pos_prot, neg_samp, dtw.RETURN_VALUE, slope_constraint=slope_constraint, window=window)
|
||||
selected_id = np.argmax(neg_aves - pos_aves)
|
||||
path = dtw.shape_dtw(positive_prototypes[selected_id], pat, dtw.RETURN_PATH, slope_constraint=slope_constraint, window=window)
|
||||
else:
|
||||
for p, pos_prot in enumerate(positive_prototypes):
|
||||
for ps, pos_samp in enumerate(positive_prototypes):
|
||||
if p != ps:
|
||||
pos_aves[p] += (1./(pos_k-1.))*dtw.dtw(pos_prot, pos_samp, dtw.RETURN_VALUE, slope_constraint=slope_constraint, window=window)
|
||||
for ns, neg_samp in enumerate(negative_prototypes):
|
||||
neg_aves[p] += (1./neg_k)*dtw.dtw(pos_prot, neg_samp, dtw.RETURN_VALUE, slope_constraint=slope_constraint, window=window)
|
||||
selected_id = np.argmax(neg_aves - pos_aves)
|
||||
path = dtw.dtw(positive_prototypes[selected_id], pat, dtw.RETURN_PATH, slope_constraint=slope_constraint, window=window)
|
||||
|
||||
# Time warp
|
||||
warped = pat[path[1]]
|
||||
warp_path_interp = np.interp(orig_steps, np.linspace(0, x.shape[1]-1., num=warped.shape[0]), path[1])
|
||||
warp_amount[i] = np.sum(np.abs(orig_steps-warp_path_interp))
|
||||
for dim in range(x.shape[2]):
|
||||
ret[i,:,dim] = np.interp(orig_steps, np.linspace(0, x.shape[1]-1., num=warped.shape[0]), warped[:,dim]).T
|
||||
else:
|
||||
# if verbose > -1:
|
||||
# print("There is only one pattern of class {}".format(l[i]))
|
||||
ret[i,:] = pat
|
||||
warp_amount[i] = 0.
|
||||
if use_variable_slice:
|
||||
max_warp = np.max(warp_amount)
|
||||
if max_warp == 0:
|
||||
# unchanged
|
||||
ret = window_slice(ret, reduce_ratio=0.9)
|
||||
else:
|
||||
for i, pat in enumerate(ret):
|
||||
# Variable Sllicing
|
||||
ret[i] = window_slice(pat[np.newaxis,:,:], reduce_ratio=0.9+0.1*warp_amount[i]/max_warp)[0]
|
||||
return ret
|
||||
|
||||
def discriminative_guided_warp_shape(x, labels, batch_size=6, slope_constraint="symmetric", use_window=True):
|
||||
return discriminative_guided_warp(x, labels, batch_size, slope_constraint, use_window, dtw_type="shape")
|
||||
|
||||
|
||||
def run_augmentation(x, y, args):
|
||||
print("Augmenting %s"%args.data)
|
||||
np.random.seed(args.seed)
|
||||
x_aug = x
|
||||
y_aug = y
|
||||
if args.augmentation_ratio > 0:
|
||||
augmentation_tags = "%d"%args.augmentation_ratio
|
||||
for n in range(args.augmentation_ratio):
|
||||
x_temp, augmentation_tags = augment(x, y, args)
|
||||
x_aug = np.append(x_aug, x_temp, axis=0)
|
||||
y_aug = np.append(y_aug, y, axis=0)
|
||||
print("Round %d: %s done"%(n, augmentation_tags))
|
||||
if args.extra_tag:
|
||||
augmentation_tags += "_"+args.extra_tag
|
||||
else:
|
||||
augmentation_tags = args.extra_tag
|
||||
return x_aug, y_aug, augmentation_tags
|
||||
|
||||
def run_augmentation_single(x, y, args):
|
||||
# print("Augmenting %s"%args.data)
|
||||
np.random.seed(args.seed)
|
||||
|
||||
x_aug = x
|
||||
y_aug = y
|
||||
|
||||
|
||||
if len(x.shape)<3:
|
||||
# Augmenting on the entire series: using the input data as "One Big Batch"
|
||||
# Before - (sequence_length, num_channels)
|
||||
# After - (1, sequence_length, num_channels)
|
||||
# Note: the 'sequence_length' here is actually the length of the entire series
|
||||
x_input = x[np.newaxis,:]
|
||||
elif len(x.shape)==3:
|
||||
# Augmenting on the batch series: keep current dimension (batch_size, sequence_length, num_channels)
|
||||
x_input = x
|
||||
else:
|
||||
raise ValueError("Input must be (batch_size, sequence_length, num_channels) dimensional")
|
||||
|
||||
if args.augmentation_ratio > 0:
|
||||
augmentation_tags = "%d"%args.augmentation_ratio
|
||||
for n in range(args.augmentation_ratio):
|
||||
x_aug, augmentation_tags = augment(x_input, y, args)
|
||||
# print("Round %d: %s done"%(n, augmentation_tags))
|
||||
if args.extra_tag:
|
||||
augmentation_tags += "_"+args.extra_tag
|
||||
else:
|
||||
augmentation_tags = args.extra_tag
|
||||
|
||||
if(len(x.shape)<3):
|
||||
# Reverse to two-dimensional in whole series augmentation scenario
|
||||
x_aug = x_aug.squeeze(0)
|
||||
return x_aug, y_aug, augmentation_tags
|
||||
|
||||
|
||||
def augment(x, y, args):
|
||||
import utils.augmentation as aug
|
||||
augmentation_tags = ""
|
||||
if args.jitter:
|
||||
x = aug.jitter(x)
|
||||
augmentation_tags += "_jitter"
|
||||
if args.scaling:
|
||||
x = aug.scaling(x)
|
||||
augmentation_tags += "_scaling"
|
||||
if args.rotation:
|
||||
x = aug.rotation(x)
|
||||
augmentation_tags += "_rotation"
|
||||
if args.permutation:
|
||||
x = aug.permutation(x)
|
||||
augmentation_tags += "_permutation"
|
||||
if args.randompermutation:
|
||||
x = aug.permutation(x, seg_mode="random")
|
||||
augmentation_tags += "_randomperm"
|
||||
if args.magwarp:
|
||||
x = aug.magnitude_warp(x)
|
||||
augmentation_tags += "_magwarp"
|
||||
if args.timewarp:
|
||||
x = aug.time_warp(x)
|
||||
augmentation_tags += "_timewarp"
|
||||
if args.windowslice:
|
||||
x = aug.window_slice(x)
|
||||
augmentation_tags += "_windowslice"
|
||||
if args.windowwarp:
|
||||
x = aug.window_warp(x)
|
||||
augmentation_tags += "_windowwarp"
|
||||
if args.spawner:
|
||||
x = aug.spawner(x, y)
|
||||
augmentation_tags += "_spawner"
|
||||
if args.dtwwarp:
|
||||
x = aug.random_guided_warp(x, y)
|
||||
augmentation_tags += "_rgw"
|
||||
if args.shapedtwwarp:
|
||||
x = aug.random_guided_warp_shape(x, y)
|
||||
augmentation_tags += "_rgws"
|
||||
if args.wdba:
|
||||
x = aug.wdba(x, y)
|
||||
augmentation_tags += "_wdba"
|
||||
if args.discdtw:
|
||||
x = aug.discriminative_guided_warp(x, y)
|
||||
augmentation_tags += "_dgw"
|
||||
if args.discsdtw:
|
||||
x = aug.discriminative_guided_warp_shape(x, y)
|
||||
augmentation_tags += "_dgws"
|
||||
return x, augmentation_tags
|
223
utils/dtw.py
Normal file
223
utils/dtw.py
Normal file
@ -0,0 +1,223 @@
|
||||
__author__ = 'Brian Iwana'
|
||||
|
||||
import numpy as np
|
||||
import math
|
||||
import sys
|
||||
|
||||
RETURN_VALUE = 0
|
||||
RETURN_PATH = 1
|
||||
RETURN_ALL = -1
|
||||
|
||||
# Core DTW
|
||||
def _traceback(DTW, slope_constraint):
|
||||
i, j = np.array(DTW.shape) - 1
|
||||
p, q = [i-1], [j-1]
|
||||
|
||||
if slope_constraint == "asymmetric":
|
||||
while (i > 1):
|
||||
tb = np.argmin((DTW[i-1, j], DTW[i-1, j-1], DTW[i-1, j-2]))
|
||||
|
||||
if (tb == 0):
|
||||
i = i - 1
|
||||
elif (tb == 1):
|
||||
i = i - 1
|
||||
j = j - 1
|
||||
elif (tb == 2):
|
||||
i = i - 1
|
||||
j = j - 2
|
||||
|
||||
p.insert(0, i-1)
|
||||
q.insert(0, j-1)
|
||||
elif slope_constraint == "symmetric":
|
||||
while (i > 1 or j > 1):
|
||||
tb = np.argmin((DTW[i-1, j-1], DTW[i-1, j], DTW[i, j-1]))
|
||||
|
||||
if (tb == 0):
|
||||
i = i - 1
|
||||
j = j - 1
|
||||
elif (tb == 1):
|
||||
i = i - 1
|
||||
elif (tb == 2):
|
||||
j = j - 1
|
||||
|
||||
p.insert(0, i-1)
|
||||
q.insert(0, j-1)
|
||||
else:
|
||||
sys.exit("Unknown slope constraint %s"%slope_constraint)
|
||||
|
||||
return (np.array(p), np.array(q))
|
||||
|
||||
def dtw(prototype, sample, return_flag = RETURN_VALUE, slope_constraint="asymmetric", window=None):
|
||||
""" Computes the DTW of two sequences.
|
||||
:param prototype: np array [0..b]
|
||||
:param sample: np array [0..t]
|
||||
:param extended: bool
|
||||
"""
|
||||
p = prototype.shape[0]
|
||||
assert p != 0, "Prototype empty!"
|
||||
s = sample.shape[0]
|
||||
assert s != 0, "Sample empty!"
|
||||
|
||||
if window is None:
|
||||
window = s
|
||||
|
||||
cost = np.full((p, s), np.inf)
|
||||
for i in range(p):
|
||||
start = max(0, i-window)
|
||||
end = min(s, i+window)+1
|
||||
cost[i,start:end]=np.linalg.norm(sample[start:end] - prototype[i], axis=1)
|
||||
|
||||
DTW = _cummulative_matrix(cost, slope_constraint, window)
|
||||
|
||||
if return_flag == RETURN_ALL:
|
||||
return DTW[-1,-1], cost, DTW[1:,1:], _traceback(DTW, slope_constraint)
|
||||
elif return_flag == RETURN_PATH:
|
||||
return _traceback(DTW, slope_constraint)
|
||||
else:
|
||||
return DTW[-1,-1]
|
||||
|
||||
def _cummulative_matrix(cost, slope_constraint, window):
|
||||
p = cost.shape[0]
|
||||
s = cost.shape[1]
|
||||
|
||||
# Note: DTW is one larger than cost and the original patterns
|
||||
DTW = np.full((p+1, s+1), np.inf)
|
||||
|
||||
DTW[0, 0] = 0.0
|
||||
|
||||
if slope_constraint == "asymmetric":
|
||||
for i in range(1, p+1):
|
||||
if i <= window+1:
|
||||
DTW[i,1] = cost[i-1,0] + min(DTW[i-1,0], DTW[i-1,1])
|
||||
for j in range(max(2, i-window), min(s, i+window)+1):
|
||||
DTW[i,j] = cost[i-1,j-1] + min(DTW[i-1,j-2], DTW[i-1,j-1], DTW[i-1,j])
|
||||
elif slope_constraint == "symmetric":
|
||||
for i in range(1, p+1):
|
||||
for j in range(max(1, i-window), min(s, i+window)+1):
|
||||
DTW[i,j] = cost[i-1,j-1] + min(DTW[i-1,j-1], DTW[i,j-1], DTW[i-1,j])
|
||||
else:
|
||||
sys.exit("Unknown slope constraint %s"%slope_constraint)
|
||||
|
||||
return DTW
|
||||
|
||||
def shape_dtw(prototype, sample, return_flag = RETURN_VALUE, slope_constraint="asymmetric", window=None, descr_ratio=0.05):
|
||||
""" Computes the shapeDTW of two sequences.
|
||||
:param prototype: np array [0..b]
|
||||
:param sample: np array [0..t]
|
||||
:param extended: bool
|
||||
"""
|
||||
# shapeDTW
|
||||
# https://www.sciencedirect.com/science/article/pii/S0031320317303710
|
||||
|
||||
p = prototype.shape[0]
|
||||
assert p != 0, "Prototype empty!"
|
||||
s = sample.shape[0]
|
||||
assert s != 0, "Sample empty!"
|
||||
|
||||
if window is None:
|
||||
window = s
|
||||
|
||||
p_feature_len = np.clip(np.round(p * descr_ratio), 5, 100).astype(int)
|
||||
s_feature_len = np.clip(np.round(s * descr_ratio), 5, 100).astype(int)
|
||||
|
||||
# padding
|
||||
p_pad_front = (np.ceil(p_feature_len / 2.)).astype(int)
|
||||
p_pad_back = (np.floor(p_feature_len / 2.)).astype(int)
|
||||
s_pad_front = (np.ceil(s_feature_len / 2.)).astype(int)
|
||||
s_pad_back = (np.floor(s_feature_len / 2.)).astype(int)
|
||||
|
||||
prototype_pad = np.pad(prototype, ((p_pad_front, p_pad_back), (0, 0)), mode="edge")
|
||||
sample_pad = np.pad(sample, ((s_pad_front, s_pad_back), (0, 0)), mode="edge")
|
||||
p_p = prototype_pad.shape[0]
|
||||
s_p = sample_pad.shape[0]
|
||||
|
||||
cost = np.full((p, s), np.inf)
|
||||
for i in range(p):
|
||||
for j in range(max(0, i-window), min(s, i+window)):
|
||||
cost[i, j] = np.linalg.norm(sample_pad[j:j+s_feature_len] - prototype_pad[i:i+p_feature_len])
|
||||
|
||||
DTW = _cummulative_matrix(cost, slope_constraint=slope_constraint, window=window)
|
||||
|
||||
if return_flag == RETURN_ALL:
|
||||
return DTW[-1,-1], cost, DTW[1:,1:], _traceback(DTW, slope_constraint)
|
||||
elif return_flag == RETURN_PATH:
|
||||
return _traceback(DTW, slope_constraint)
|
||||
else:
|
||||
return DTW[-1,-1]
|
||||
|
||||
# Draw helpers
|
||||
def draw_graph2d(cost, DTW, path, prototype, sample):
|
||||
import matplotlib.pyplot as plt
|
||||
plt.figure(figsize=(12, 8))
|
||||
# plt.subplots_adjust(left=.02, right=.98, bottom=.001, top=.96, wspace=.05, hspace=.01)
|
||||
|
||||
#cost
|
||||
plt.subplot(2, 3, 1)
|
||||
plt.imshow(cost.T, cmap=plt.cm.gray, interpolation='none', origin='lower')
|
||||
plt.plot(path[0], path[1], 'y')
|
||||
plt.xlim((-0.5, cost.shape[0]-0.5))
|
||||
plt.ylim((-0.5, cost.shape[0]-0.5))
|
||||
|
||||
#dtw
|
||||
plt.subplot(2, 3, 2)
|
||||
plt.imshow(DTW.T, cmap=plt.cm.gray, interpolation='none', origin='lower')
|
||||
plt.plot(path[0]+1, path[1]+1, 'y')
|
||||
plt.xlim((-0.5, DTW.shape[0]-0.5))
|
||||
plt.ylim((-0.5, DTW.shape[0]-0.5))
|
||||
|
||||
#prototype
|
||||
plt.subplot(2, 3, 4)
|
||||
plt.plot(prototype[:,0], prototype[:,1], 'b-o')
|
||||
|
||||
#connection
|
||||
plt.subplot(2, 3, 5)
|
||||
for i in range(0,path[0].shape[0]):
|
||||
plt.plot([prototype[path[0][i],0], sample[path[1][i],0]],[prototype[path[0][i],1], sample[path[1][i],1]], 'y-')
|
||||
plt.plot(sample[:,0], sample[:,1], 'g-o')
|
||||
plt.plot(prototype[:,0], prototype[:,1], 'b-o')
|
||||
|
||||
#sample
|
||||
plt.subplot(2, 3, 6)
|
||||
plt.plot(sample[:,0], sample[:,1], 'g-o')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
def draw_graph1d(cost, DTW, path, prototype, sample):
|
||||
import matplotlib.pyplot as plt
|
||||
plt.figure(figsize=(12, 8))
|
||||
# plt.subplots_adjust(left=.02, right=.98, bottom=.001, top=.96, wspace=.05, hspace=.01)
|
||||
p_steps = np.arange(prototype.shape[0])
|
||||
s_steps = np.arange(sample.shape[0])
|
||||
|
||||
#cost
|
||||
plt.subplot(2, 3, 1)
|
||||
plt.imshow(cost.T, cmap=plt.cm.gray, interpolation='none', origin='lower')
|
||||
plt.plot(path[0], path[1], 'y')
|
||||
plt.xlim((-0.5, cost.shape[0]-0.5))
|
||||
plt.ylim((-0.5, cost.shape[0]-0.5))
|
||||
|
||||
#dtw
|
||||
plt.subplot(2, 3, 2)
|
||||
plt.imshow(DTW.T, cmap=plt.cm.gray, interpolation='none', origin='lower')
|
||||
plt.plot(path[0]+1, path[1]+1, 'y')
|
||||
plt.xlim((-0.5, DTW.shape[0]-0.5))
|
||||
plt.ylim((-0.5, DTW.shape[0]-0.5))
|
||||
|
||||
#prototype
|
||||
plt.subplot(2, 3, 4)
|
||||
plt.plot(p_steps, prototype[:,0], 'b-o')
|
||||
|
||||
#connection
|
||||
plt.subplot(2, 3, 5)
|
||||
for i in range(0,path[0].shape[0]):
|
||||
plt.plot([path[0][i], path[1][i]],[prototype[path[0][i],0], sample[path[1][i],0]], 'y-')
|
||||
plt.plot(p_steps, sample[:,0], 'g-o')
|
||||
plt.plot(s_steps, prototype[:,0], 'b-o')
|
||||
|
||||
#sample
|
||||
plt.subplot(2, 3, 6)
|
||||
plt.plot(s_steps, sample[:,0], 'g-o')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
156
utils/dtw_metric.py
Normal file
156
utils/dtw_metric.py
Normal file
@ -0,0 +1,156 @@
|
||||
from numpy import array, zeros, full, argmin, inf, ndim
|
||||
from scipy.spatial.distance import cdist
|
||||
from math import isinf
|
||||
|
||||
|
||||
def dtw(x, y, dist, warp=1, w=inf, s=1.0):
|
||||
"""
|
||||
Computes Dynamic Time Warping (DTW) of two sequences.
|
||||
|
||||
:param array x: N1*M array
|
||||
:param array y: N2*M array
|
||||
:param func dist: distance used as cost measure
|
||||
:param int warp: how many shifts are computed.
|
||||
:param int w: window size limiting the maximal distance between indices of matched entries |i,j|.
|
||||
:param float s: weight applied on off-diagonal moves of the path. As s gets larger, the warping path is increasingly biased towards the diagonal
|
||||
Returns the minimum distance, the cost matrix, the accumulated cost matrix, and the wrap path.
|
||||
"""
|
||||
assert len(x)
|
||||
assert len(y)
|
||||
assert isinf(w) or (w >= abs(len(x) - len(y)))
|
||||
assert s > 0
|
||||
r, c = len(x), len(y)
|
||||
if not isinf(w):
|
||||
D0 = full((r + 1, c + 1), inf)
|
||||
for i in range(1, r + 1):
|
||||
D0[i, max(1, i - w):min(c + 1, i + w + 1)] = 0
|
||||
D0[0, 0] = 0
|
||||
else:
|
||||
D0 = zeros((r + 1, c + 1))
|
||||
D0[0, 1:] = inf
|
||||
D0[1:, 0] = inf
|
||||
D1 = D0[1:, 1:] # view
|
||||
for i in range(r):
|
||||
for j in range(c):
|
||||
if (isinf(w) or (max(0, i - w) <= j <= min(c, i + w))):
|
||||
D1[i, j] = dist(x[i], y[j])
|
||||
C = D1.copy()
|
||||
jrange = range(c)
|
||||
for i in range(r):
|
||||
if not isinf(w):
|
||||
jrange = range(max(0, i - w), min(c, i + w + 1))
|
||||
for j in jrange:
|
||||
min_list = [D0[i, j]]
|
||||
for k in range(1, warp + 1):
|
||||
i_k = min(i + k, r)
|
||||
j_k = min(j + k, c)
|
||||
min_list += [D0[i_k, j] * s, D0[i, j_k] * s]
|
||||
D1[i, j] += min(min_list)
|
||||
if len(x) == 1:
|
||||
path = zeros(len(y)), range(len(y))
|
||||
elif len(y) == 1:
|
||||
path = range(len(x)), zeros(len(x))
|
||||
else:
|
||||
path = _traceback(D0)
|
||||
return D1[-1, -1], C, D1, path
|
||||
|
||||
|
||||
def accelerated_dtw(x, y, dist, warp=1):
|
||||
"""
|
||||
Computes Dynamic Time Warping (DTW) of two sequences in a faster way.
|
||||
Instead of iterating through each element and calculating each distance,
|
||||
this uses the cdist function from scipy (https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html)
|
||||
|
||||
:param array x: N1*M array
|
||||
:param array y: N2*M array
|
||||
:param string or func dist: distance parameter for cdist. When string is given, cdist uses optimized functions for the distance metrics.
|
||||
If a string is passed, the distance function can be 'braycurtis', 'canberra', 'chebyshev', 'cityblock', 'correlation', 'cosine', 'dice', 'euclidean', 'hamming', 'jaccard', 'kulsinski', 'mahalanobis', 'matching', 'minkowski', 'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener', 'sokalsneath', 'sqeuclidean', 'wminkowski', 'yule'.
|
||||
:param int warp: how many shifts are computed.
|
||||
Returns the minimum distance, the cost matrix, the accumulated cost matrix, and the wrap path.
|
||||
"""
|
||||
assert len(x)
|
||||
assert len(y)
|
||||
if ndim(x) == 1:
|
||||
x = x.reshape(-1, 1)
|
||||
if ndim(y) == 1:
|
||||
y = y.reshape(-1, 1)
|
||||
r, c = len(x), len(y)
|
||||
D0 = zeros((r + 1, c + 1))
|
||||
D0[0, 1:] = inf
|
||||
D0[1:, 0] = inf
|
||||
D1 = D0[1:, 1:]
|
||||
D0[1:, 1:] = cdist(x, y, dist)
|
||||
C = D1.copy()
|
||||
for i in range(r):
|
||||
for j in range(c):
|
||||
min_list = [D0[i, j]]
|
||||
for k in range(1, warp + 1):
|
||||
min_list += [D0[min(i + k, r), j],
|
||||
D0[i, min(j + k, c)]]
|
||||
D1[i, j] += min(min_list)
|
||||
if len(x) == 1:
|
||||
path = zeros(len(y)), range(len(y))
|
||||
elif len(y) == 1:
|
||||
path = range(len(x)), zeros(len(x))
|
||||
else:
|
||||
path = _traceback(D0)
|
||||
return D1[-1, -1], C, D1, path
|
||||
|
||||
|
||||
def _traceback(D):
|
||||
i, j = array(D.shape) - 2
|
||||
p, q = [i], [j]
|
||||
while (i > 0) or (j > 0):
|
||||
tb = argmin((D[i, j], D[i, j + 1], D[i + 1, j]))
|
||||
if tb == 0:
|
||||
i -= 1
|
||||
j -= 1
|
||||
elif tb == 1:
|
||||
i -= 1
|
||||
else: # (tb == 2):
|
||||
j -= 1
|
||||
p.insert(0, i)
|
||||
q.insert(0, j)
|
||||
return array(p), array(q)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
w = inf
|
||||
s = 1.0
|
||||
if 1: # 1-D numeric
|
||||
from sklearn.metrics.pairwise import manhattan_distances
|
||||
x = [0, 0, 1, 1, 2, 4, 2, 1, 2, 0]
|
||||
y = [1, 1, 1, 2, 2, 2, 2, 3, 2, 0]
|
||||
dist_fun = manhattan_distances
|
||||
w = 1
|
||||
# s = 1.2
|
||||
elif 0: # 2-D numeric
|
||||
from sklearn.metrics.pairwise import euclidean_distances
|
||||
x = [[0, 0], [0, 1], [1, 1], [1, 2], [2, 2], [4, 3], [2, 3], [1, 1], [2, 2], [0, 1]]
|
||||
y = [[1, 0], [1, 1], [1, 1], [2, 1], [4, 3], [4, 3], [2, 3], [3, 1], [1, 2], [1, 0]]
|
||||
dist_fun = euclidean_distances
|
||||
else: # 1-D list of strings
|
||||
from nltk.metrics.distance import edit_distance
|
||||
# x = ['we', 'shelled', 'clams', 'for', 'the', 'chowder']
|
||||
# y = ['class', 'too']
|
||||
x = ['i', 'soon', 'found', 'myself', 'muttering', 'to', 'the', 'walls']
|
||||
y = ['see', 'drown', 'himself']
|
||||
# x = 'we talked about the situation'.split()
|
||||
# y = 'we talked about the situation'.split()
|
||||
dist_fun = edit_distance
|
||||
dist, cost, acc, path = dtw(x, y, dist_fun, w=w, s=s)
|
||||
|
||||
# Vizualize
|
||||
from matplotlib import pyplot as plt
|
||||
plt.imshow(cost.T, origin='lower', cmap=plt.cm.Reds, interpolation='nearest')
|
||||
plt.plot(path[0], path[1], '-o') # relation
|
||||
plt.xticks(range(len(x)), x)
|
||||
plt.yticks(range(len(y)), y)
|
||||
plt.xlabel('x')
|
||||
plt.ylabel('y')
|
||||
plt.axis('tight')
|
||||
if isinf(w):
|
||||
plt.title('Minimum distance: {}, slope weight: {}'.format(dist, s))
|
||||
else:
|
||||
plt.title('Minimum distance: {}, window widht: {}, slope weight: {}'.format(dist, w, s))
|
||||
plt.show()
|
89
utils/losses.py
Normal file
89
utils/losses.py
Normal file
@ -0,0 +1,89 @@
|
||||
# This source code is provided for the purposes of scientific reproducibility
|
||||
# under the following limited license from Element AI Inc. The code is an
|
||||
# implementation of the N-BEATS model (Oreshkin et al., N-BEATS: Neural basis
|
||||
# expansion analysis for interpretable time series forecasting,
|
||||
# https://arxiv.org/abs/1905.10437). The copyright to the source code is
|
||||
# licensed under the Creative Commons - Attribution-NonCommercial 4.0
|
||||
# International license (CC BY-NC 4.0):
|
||||
# https://creativecommons.org/licenses/by-nc/4.0/. Any commercial use (whether
|
||||
# for the benefit of third parties or internally in production) requires an
|
||||
# explicit license. The subject-matter of the N-BEATS model and associated
|
||||
# materials are the property of Element AI Inc. and may be subject to patent
|
||||
# protection. No license to patents is granted hereunder (whether express or
|
||||
# implied). Copyright © 2020 Element AI Inc. All rights reserved.
|
||||
|
||||
"""
|
||||
Loss functions for PyTorch.
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
import torch.nn as nn
|
||||
import numpy as np
|
||||
import pdb
|
||||
|
||||
|
||||
def divide_no_nan(a, b):
|
||||
"""
|
||||
a/b where the resulted NaN or Inf are replaced by 0.
|
||||
"""
|
||||
result = a / b
|
||||
result[result != result] = .0
|
||||
result[result == np.inf] = .0
|
||||
return result
|
||||
|
||||
|
||||
class mape_loss(nn.Module):
|
||||
def __init__(self):
|
||||
super(mape_loss, self).__init__()
|
||||
|
||||
def forward(self, insample: t.Tensor, freq: int,
|
||||
forecast: t.Tensor, target: t.Tensor, mask: t.Tensor) -> t.float:
|
||||
"""
|
||||
MAPE loss as defined in: https://en.wikipedia.org/wiki/Mean_absolute_percentage_error
|
||||
|
||||
:param forecast: Forecast values. Shape: batch, time
|
||||
:param target: Target values. Shape: batch, time
|
||||
:param mask: 0/1 mask. Shape: batch, time
|
||||
:return: Loss value
|
||||
"""
|
||||
weights = divide_no_nan(mask, target)
|
||||
return t.mean(t.abs((forecast - target) * weights))
|
||||
|
||||
|
||||
class smape_loss(nn.Module):
|
||||
def __init__(self):
|
||||
super(smape_loss, self).__init__()
|
||||
|
||||
def forward(self, insample: t.Tensor, freq: int,
|
||||
forecast: t.Tensor, target: t.Tensor, mask: t.Tensor) -> t.float:
|
||||
"""
|
||||
sMAPE loss as defined in https://robjhyndman.com/hyndsight/smape/ (Makridakis 1993)
|
||||
|
||||
:param forecast: Forecast values. Shape: batch, time
|
||||
:param target: Target values. Shape: batch, time
|
||||
:param mask: 0/1 mask. Shape: batch, time
|
||||
:return: Loss value
|
||||
"""
|
||||
return 200 * t.mean(divide_no_nan(t.abs(forecast - target),
|
||||
t.abs(forecast.data) + t.abs(target.data)) * mask)
|
||||
|
||||
|
||||
class mase_loss(nn.Module):
|
||||
def __init__(self):
|
||||
super(mase_loss, self).__init__()
|
||||
|
||||
def forward(self, insample: t.Tensor, freq: int,
|
||||
forecast: t.Tensor, target: t.Tensor, mask: t.Tensor) -> t.float:
|
||||
"""
|
||||
MASE loss as defined in "Scaled Errors" https://robjhyndman.com/papers/mase.pdf
|
||||
|
||||
:param insample: Insample values. Shape: batch, time_i
|
||||
:param freq: Frequency value
|
||||
:param forecast: Forecast values. Shape: batch, time_o
|
||||
:param target: Target values. Shape: batch, time_o
|
||||
:param mask: 0/1 mask. Shape: batch, time_o
|
||||
:return: Loss value
|
||||
"""
|
||||
masep = t.mean(t.abs(insample[:, freq:] - insample[:, :-freq]), dim=1)
|
||||
masked_masep_inv = divide_no_nan(mask, masep[:, None])
|
||||
return t.mean(t.abs(target - forecast) * masked_masep_inv)
|
140
utils/m4_summary.py
Normal file
140
utils/m4_summary.py
Normal file
@ -0,0 +1,140 @@
|
||||
# This source code is provided for the purposes of scientific reproducibility
|
||||
# under the following limited license from Element AI Inc. The code is an
|
||||
# implementation of the N-BEATS model (Oreshkin et al., N-BEATS: Neural basis
|
||||
# expansion analysis for interpretable time series forecasting,
|
||||
# https://arxiv.org/abs/1905.10437). The copyright to the source code is
|
||||
# licensed under the Creative Commons - Attribution-NonCommercial 4.0
|
||||
# International license (CC BY-NC 4.0):
|
||||
# https://creativecommons.org/licenses/by-nc/4.0/. Any commercial use (whether
|
||||
# for the benefit of third parties or internally in production) requires an
|
||||
# explicit license. The subject-matter of the N-BEATS model and associated
|
||||
# materials are the property of Element AI Inc. and may be subject to patent
|
||||
# protection. No license to patents is granted hereunder (whether express or
|
||||
# implied). Copyright 2020 Element AI Inc. All rights reserved.
|
||||
|
||||
"""
|
||||
M4 Summary
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from data_provider.m4 import M4Dataset
|
||||
from data_provider.m4 import M4Meta
|
||||
import os
|
||||
|
||||
|
||||
def group_values(values, groups, group_name):
|
||||
return np.array([v[~np.isnan(v)] for v in values[groups == group_name]])
|
||||
|
||||
|
||||
def mase(forecast, insample, outsample, frequency):
|
||||
return np.mean(np.abs(forecast - outsample)) / np.mean(np.abs(insample[:-frequency] - insample[frequency:]))
|
||||
|
||||
|
||||
def smape_2(forecast, target):
|
||||
denom = np.abs(target) + np.abs(forecast)
|
||||
# divide by 1.0 instead of 0.0, in case when denom is zero the enumerator will be 0.0 anyway.
|
||||
denom[denom == 0.0] = 1.0
|
||||
return 200 * np.abs(forecast - target) / denom
|
||||
|
||||
|
||||
def mape(forecast, target):
|
||||
denom = np.abs(target)
|
||||
# divide by 1.0 instead of 0.0, in case when denom is zero the enumerator will be 0.0 anyway.
|
||||
denom[denom == 0.0] = 1.0
|
||||
return 100 * np.abs(forecast - target) / denom
|
||||
|
||||
|
||||
class M4Summary:
|
||||
def __init__(self, file_path, root_path):
|
||||
self.file_path = file_path
|
||||
self.training_set = M4Dataset.load(training=True, dataset_file=root_path)
|
||||
self.test_set = M4Dataset.load(training=False, dataset_file=root_path)
|
||||
self.naive_path = os.path.join(root_path, 'submission-Naive2.csv')
|
||||
|
||||
def evaluate(self):
|
||||
"""
|
||||
Evaluate forecasts using M4 test dataset.
|
||||
|
||||
:param forecast: Forecasts. Shape: timeseries, time.
|
||||
:return: sMAPE and OWA grouped by seasonal patterns.
|
||||
"""
|
||||
grouped_owa = OrderedDict()
|
||||
|
||||
naive2_forecasts = pd.read_csv(self.naive_path).values[:, 1:].astype(np.float32)
|
||||
naive2_forecasts = np.array([v[~np.isnan(v)] for v in naive2_forecasts])
|
||||
|
||||
model_mases = {}
|
||||
naive2_smapes = {}
|
||||
naive2_mases = {}
|
||||
grouped_smapes = {}
|
||||
grouped_mapes = {}
|
||||
for group_name in M4Meta.seasonal_patterns:
|
||||
file_name = self.file_path + group_name + "_forecast.csv"
|
||||
if os.path.exists(file_name):
|
||||
model_forecast = pd.read_csv(file_name).values
|
||||
|
||||
naive2_forecast = group_values(naive2_forecasts, self.test_set.groups, group_name)
|
||||
target = group_values(self.test_set.values, self.test_set.groups, group_name)
|
||||
# all timeseries within group have same frequency
|
||||
frequency = self.training_set.frequencies[self.test_set.groups == group_name][0]
|
||||
insample = group_values(self.training_set.values, self.test_set.groups, group_name)
|
||||
|
||||
model_mases[group_name] = np.mean([mase(forecast=model_forecast[i],
|
||||
insample=insample[i],
|
||||
outsample=target[i],
|
||||
frequency=frequency) for i in range(len(model_forecast))])
|
||||
naive2_mases[group_name] = np.mean([mase(forecast=naive2_forecast[i],
|
||||
insample=insample[i],
|
||||
outsample=target[i],
|
||||
frequency=frequency) for i in range(len(model_forecast))])
|
||||
|
||||
naive2_smapes[group_name] = np.mean(smape_2(naive2_forecast, target))
|
||||
grouped_smapes[group_name] = np.mean(smape_2(forecast=model_forecast, target=target))
|
||||
grouped_mapes[group_name] = np.mean(mape(forecast=model_forecast, target=target))
|
||||
|
||||
grouped_smapes = self.summarize_groups(grouped_smapes)
|
||||
grouped_mapes = self.summarize_groups(grouped_mapes)
|
||||
grouped_model_mases = self.summarize_groups(model_mases)
|
||||
grouped_naive2_smapes = self.summarize_groups(naive2_smapes)
|
||||
grouped_naive2_mases = self.summarize_groups(naive2_mases)
|
||||
for k in grouped_model_mases.keys():
|
||||
grouped_owa[k] = (grouped_model_mases[k] / grouped_naive2_mases[k] +
|
||||
grouped_smapes[k] / grouped_naive2_smapes[k]) / 2
|
||||
|
||||
def round_all(d):
|
||||
return dict(map(lambda kv: (kv[0], np.round(kv[1], 3)), d.items()))
|
||||
|
||||
return round_all(grouped_smapes), round_all(grouped_owa), round_all(grouped_mapes), round_all(
|
||||
grouped_model_mases)
|
||||
|
||||
def summarize_groups(self, scores):
|
||||
"""
|
||||
Re-group scores respecting M4 rules.
|
||||
:param scores: Scores per group.
|
||||
:return: Grouped scores.
|
||||
"""
|
||||
scores_summary = OrderedDict()
|
||||
|
||||
def group_count(group_name):
|
||||
return len(np.where(self.test_set.groups == group_name)[0])
|
||||
|
||||
weighted_score = {}
|
||||
for g in ['Yearly', 'Quarterly', 'Monthly']:
|
||||
weighted_score[g] = scores[g] * group_count(g)
|
||||
scores_summary[g] = scores[g]
|
||||
|
||||
others_score = 0
|
||||
others_count = 0
|
||||
for g in ['Weekly', 'Daily', 'Hourly']:
|
||||
others_score += scores[g] * group_count(g)
|
||||
others_count += group_count(g)
|
||||
weighted_score['Others'] = others_score
|
||||
scores_summary['Others'] = others_score / others_count
|
||||
|
||||
average = np.sum(list(weighted_score.values())) / len(self.test_set.groups)
|
||||
scores_summary['Average'] = average
|
||||
|
||||
return scores_summary
|
26
utils/masking.py
Normal file
26
utils/masking.py
Normal file
@ -0,0 +1,26 @@
|
||||
import torch
|
||||
|
||||
|
||||
class TriangularCausalMask():
|
||||
def __init__(self, B, L, device="cpu"):
|
||||
mask_shape = [B, 1, L, L]
|
||||
with torch.no_grad():
|
||||
self._mask = torch.triu(torch.ones(mask_shape, dtype=torch.bool), diagonal=1).to(device)
|
||||
|
||||
@property
|
||||
def mask(self):
|
||||
return self._mask
|
||||
|
||||
|
||||
class ProbMask():
|
||||
def __init__(self, B, H, L, index, scores, device="cpu"):
|
||||
_mask = torch.ones(L, scores.shape[-1], dtype=torch.bool).to(device).triu(1)
|
||||
_mask_ex = _mask[None, None, :].expand(B, H, L, scores.shape[-1])
|
||||
indicator = _mask_ex[torch.arange(B)[:, None, None],
|
||||
torch.arange(H)[None, :, None],
|
||||
index, :].to(device)
|
||||
self._mask = indicator.view(scores.shape).to(device)
|
||||
|
||||
@property
|
||||
def mask(self):
|
||||
return self._mask
|
41
utils/metrics.py
Normal file
41
utils/metrics.py
Normal file
@ -0,0 +1,41 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def RSE(pred, true):
|
||||
return np.sqrt(np.sum((true - pred) ** 2)) / np.sqrt(np.sum((true - true.mean()) ** 2))
|
||||
|
||||
|
||||
def CORR(pred, true):
|
||||
u = ((true - true.mean(0)) * (pred - pred.mean(0))).sum(0)
|
||||
d = np.sqrt(((true - true.mean(0)) ** 2 * (pred - pred.mean(0)) ** 2).sum(0))
|
||||
return (u / d).mean(-1)
|
||||
|
||||
|
||||
def MAE(pred, true):
|
||||
return np.mean(np.abs(true - pred))
|
||||
|
||||
|
||||
def MSE(pred, true):
|
||||
return np.mean((true - pred) ** 2)
|
||||
|
||||
|
||||
def RMSE(pred, true):
|
||||
return np.sqrt(MSE(pred, true))
|
||||
|
||||
|
||||
def MAPE(pred, true):
|
||||
return np.mean(np.abs((true - pred) / true))
|
||||
|
||||
|
||||
def MSPE(pred, true):
|
||||
return np.mean(np.square((true - pred) / true))
|
||||
|
||||
|
||||
def metric(pred, true):
|
||||
mae = MAE(pred, true)
|
||||
mse = MSE(pred, true)
|
||||
rmse = RMSE(pred, true)
|
||||
mape = MAPE(pred, true)
|
||||
mspe = MSPE(pred, true)
|
||||
|
||||
return mae, mse, rmse, mape, mspe
|
58
utils/print_args.py
Normal file
58
utils/print_args.py
Normal file
@ -0,0 +1,58 @@
|
||||
def print_args(args):
|
||||
print("\033[1m" + "Basic Config" + "\033[0m")
|
||||
print(f' {"Task Name:":<20}{args.task_name:<20}{"Is Training:":<20}{args.is_training:<20}')
|
||||
print(f' {"Model ID:":<20}{args.model_id:<20}{"Model:":<20}{args.model:<20}')
|
||||
print()
|
||||
|
||||
print("\033[1m" + "Data Loader" + "\033[0m")
|
||||
print(f' {"Data:":<20}{args.data:<20}{"Root Path:":<20}{args.root_path:<20}')
|
||||
print(f' {"Data Path:":<20}{args.data_path:<20}{"Features:":<20}{args.features:<20}')
|
||||
print(f' {"Target:":<20}{args.target:<20}{"Freq:":<20}{args.freq:<20}')
|
||||
print(f' {"Checkpoints:":<20}{args.checkpoints:<20}')
|
||||
print()
|
||||
|
||||
if args.task_name in ['long_term_forecast', 'short_term_forecast']:
|
||||
print("\033[1m" + "Forecasting Task" + "\033[0m")
|
||||
print(f' {"Seq Len:":<20}{args.seq_len:<20}{"Label Len:":<20}{args.label_len:<20}')
|
||||
print(f' {"Pred Len:":<20}{args.pred_len:<20}{"Seasonal Patterns:":<20}{args.seasonal_patterns:<20}')
|
||||
print(f' {"Inverse:":<20}{args.inverse:<20}')
|
||||
print()
|
||||
|
||||
if args.task_name == 'imputation':
|
||||
print("\033[1m" + "Imputation Task" + "\033[0m")
|
||||
print(f' {"Mask Rate:":<20}{args.mask_rate:<20}')
|
||||
print()
|
||||
|
||||
if args.task_name == 'anomaly_detection':
|
||||
print("\033[1m" + "Anomaly Detection Task" + "\033[0m")
|
||||
print(f' {"Anomaly Ratio:":<20}{args.anomaly_ratio:<20}')
|
||||
print()
|
||||
|
||||
print("\033[1m" + "Model Parameters" + "\033[0m")
|
||||
print(f' {"Top k:":<20}{args.top_k:<20}{"Num Kernels:":<20}{args.num_kernels:<20}')
|
||||
print(f' {"Enc In:":<20}{args.enc_in:<20}{"Dec In:":<20}{args.dec_in:<20}')
|
||||
print(f' {"C Out:":<20}{args.c_out:<20}{"d model:":<20}{args.d_model:<20}')
|
||||
print(f' {"n heads:":<20}{args.n_heads:<20}{"e layers:":<20}{args.e_layers:<20}')
|
||||
print(f' {"d layers:":<20}{args.d_layers:<20}{"d FF:":<20}{args.d_ff:<20}')
|
||||
print(f' {"Moving Avg:":<20}{args.moving_avg:<20}{"Factor:":<20}{args.factor:<20}')
|
||||
print(f' {"Distil:":<20}{args.distil:<20}{"Dropout:":<20}{args.dropout:<20}')
|
||||
print(f' {"Embed:":<20}{args.embed:<20}{"Activation:":<20}{args.activation:<20}')
|
||||
print()
|
||||
|
||||
print("\033[1m" + "Run Parameters" + "\033[0m")
|
||||
print(f' {"Num Workers:":<20}{args.num_workers:<20}{"Itr:":<20}{args.itr:<20}')
|
||||
print(f' {"Train Epochs:":<20}{args.train_epochs:<20}{"Batch Size:":<20}{args.batch_size:<20}')
|
||||
print(f' {"Patience:":<20}{args.patience:<20}{"Learning Rate:":<20}{args.learning_rate:<20}')
|
||||
print(f' {"Des:":<20}{args.des:<20}{"Loss:":<20}{args.loss:<20}')
|
||||
print(f' {"Lradj:":<20}{args.lradj:<20}{"Use Amp:":<20}{args.use_amp:<20}')
|
||||
print()
|
||||
|
||||
print("\033[1m" + "GPU" + "\033[0m")
|
||||
print(f' {"Use GPU:":<20}{args.use_gpu:<20}{"GPU:":<20}{args.gpu:<20}')
|
||||
print(f' {"Use Multi GPU:":<20}{args.use_multi_gpu:<20}{"Devices:":<20}{args.devices:<20}')
|
||||
print()
|
||||
|
||||
print("\033[1m" + "De-stationary Projector Params" + "\033[0m")
|
||||
p_hidden_dims_str = ', '.join(map(str, args.p_hidden_dims))
|
||||
print(f' {"P Hidden Dims:":<20}{p_hidden_dims_str:<20}{"P Hidden Layers:":<20}{args.p_hidden_layers:<20}')
|
||||
print()
|
148
utils/timefeatures.py
Normal file
148
utils/timefeatures.py
Normal file
@ -0,0 +1,148 @@
|
||||
# From: gluonts/src/gluonts/time_feature/_base.py
|
||||
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
# You may not use this file except in compliance with the License.
|
||||
# A copy of the License is located at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# or in the "license" file accompanying this file. This file is distributed
|
||||
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied. See the License for the specific language governing
|
||||
# permissions and limitations under the License.
|
||||
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.tseries import offsets
|
||||
from pandas.tseries.frequencies import to_offset
|
||||
|
||||
|
||||
class TimeFeature:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + "()"
|
||||
|
||||
|
||||
class SecondOfMinute(TimeFeature):
|
||||
"""Minute of hour encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return index.second / 59.0 - 0.5
|
||||
|
||||
|
||||
class MinuteOfHour(TimeFeature):
|
||||
"""Minute of hour encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return index.minute / 59.0 - 0.5
|
||||
|
||||
|
||||
class HourOfDay(TimeFeature):
|
||||
"""Hour of day encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return index.hour / 23.0 - 0.5
|
||||
|
||||
|
||||
class DayOfWeek(TimeFeature):
|
||||
"""Hour of day encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return index.dayofweek / 6.0 - 0.5
|
||||
|
||||
|
||||
class DayOfMonth(TimeFeature):
|
||||
"""Day of month encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return (index.day - 1) / 30.0 - 0.5
|
||||
|
||||
|
||||
class DayOfYear(TimeFeature):
|
||||
"""Day of year encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return (index.dayofyear - 1) / 365.0 - 0.5
|
||||
|
||||
|
||||
class MonthOfYear(TimeFeature):
|
||||
"""Month of year encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return (index.month - 1) / 11.0 - 0.5
|
||||
|
||||
|
||||
class WeekOfYear(TimeFeature):
|
||||
"""Week of year encoded as value between [-0.5, 0.5]"""
|
||||
|
||||
def __call__(self, index: pd.DatetimeIndex) -> np.ndarray:
|
||||
return (index.isocalendar().week - 1) / 52.0 - 0.5
|
||||
|
||||
|
||||
def time_features_from_frequency_str(freq_str: str) -> List[TimeFeature]:
|
||||
"""
|
||||
Returns a list of time features that will be appropriate for the given frequency string.
|
||||
Parameters
|
||||
----------
|
||||
freq_str
|
||||
Frequency string of the form [multiple][granularity] such as "12H", "5min", "1D" etc.
|
||||
"""
|
||||
|
||||
features_by_offsets = {
|
||||
offsets.YearEnd: [],
|
||||
offsets.QuarterEnd: [MonthOfYear],
|
||||
offsets.MonthEnd: [MonthOfYear],
|
||||
offsets.Week: [DayOfMonth, WeekOfYear],
|
||||
offsets.Day: [DayOfWeek, DayOfMonth, DayOfYear],
|
||||
offsets.BusinessDay: [DayOfWeek, DayOfMonth, DayOfYear],
|
||||
offsets.Hour: [HourOfDay, DayOfWeek, DayOfMonth, DayOfYear],
|
||||
offsets.Minute: [
|
||||
MinuteOfHour,
|
||||
HourOfDay,
|
||||
DayOfWeek,
|
||||
DayOfMonth,
|
||||
DayOfYear,
|
||||
],
|
||||
offsets.Second: [
|
||||
SecondOfMinute,
|
||||
MinuteOfHour,
|
||||
HourOfDay,
|
||||
DayOfWeek,
|
||||
DayOfMonth,
|
||||
DayOfYear,
|
||||
],
|
||||
}
|
||||
|
||||
offset = to_offset(freq_str)
|
||||
|
||||
for offset_type, feature_classes in features_by_offsets.items():
|
||||
if isinstance(offset, offset_type):
|
||||
return [cls() for cls in feature_classes]
|
||||
|
||||
supported_freq_msg = f"""
|
||||
Unsupported frequency {freq_str}
|
||||
The following frequencies are supported:
|
||||
Y - yearly
|
||||
alias: A
|
||||
M - monthly
|
||||
W - weekly
|
||||
D - daily
|
||||
B - business days
|
||||
H - hourly
|
||||
T - minutely
|
||||
alias: min
|
||||
S - secondly
|
||||
"""
|
||||
raise RuntimeError(supported_freq_msg)
|
||||
|
||||
|
||||
def time_features(dates, freq='h'):
|
||||
return np.vstack([feat(dates) for feat in time_features_from_frequency_str(freq)])
|
120
utils/tools.py
Normal file
120
utils/tools.py
Normal file
@ -0,0 +1,120 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
import math
|
||||
|
||||
plt.switch_backend('agg')
|
||||
|
||||
|
||||
def adjust_learning_rate(optimizer, epoch, args):
|
||||
# lr = args.learning_rate * (0.2 ** (epoch // 2))
|
||||
if args.lradj == 'type1':
|
||||
lr_adjust = {epoch: args.learning_rate * (0.5 ** ((epoch - 1) // 1))}
|
||||
elif args.lradj == 'type2':
|
||||
lr_adjust = {
|
||||
2: 5e-5, 4: 1e-5, 6: 5e-6, 8: 1e-6,
|
||||
10: 5e-7, 15: 1e-7, 20: 5e-8
|
||||
}
|
||||
elif args.lradj == 'type3':
|
||||
lr_adjust = {epoch: args.learning_rate if epoch < 3 else args.learning_rate * (0.9 ** ((epoch - 3) // 1))}
|
||||
elif args.lradj == "cosine":
|
||||
lr_adjust = {epoch: args.learning_rate /2 * (1 + math.cos(epoch / args.train_epochs * math.pi))}
|
||||
if epoch in lr_adjust.keys():
|
||||
lr = lr_adjust[epoch]
|
||||
for param_group in optimizer.param_groups:
|
||||
param_group['lr'] = lr
|
||||
print('Updating learning rate to {}'.format(lr))
|
||||
|
||||
|
||||
class EarlyStopping:
|
||||
def __init__(self, patience=7, verbose=False, delta=0):
|
||||
self.patience = patience
|
||||
self.verbose = verbose
|
||||
self.counter = 0
|
||||
self.best_score = None
|
||||
self.early_stop = False
|
||||
self.val_loss_min = np.inf
|
||||
self.delta = delta
|
||||
|
||||
def __call__(self, val_loss, model, path):
|
||||
score = -val_loss
|
||||
if self.best_score is None:
|
||||
self.best_score = score
|
||||
self.save_checkpoint(val_loss, model, path)
|
||||
elif score < self.best_score + self.delta:
|
||||
self.counter += 1
|
||||
print(f'EarlyStopping counter: {self.counter} out of {self.patience}')
|
||||
if self.counter >= self.patience:
|
||||
self.early_stop = True
|
||||
else:
|
||||
self.best_score = score
|
||||
self.save_checkpoint(val_loss, model, path)
|
||||
self.counter = 0
|
||||
|
||||
def save_checkpoint(self, val_loss, model, path):
|
||||
if self.verbose:
|
||||
print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}). Saving model ...')
|
||||
torch.save(model.state_dict(), path + '/' + 'checkpoint.pth')
|
||||
self.val_loss_min = val_loss
|
||||
|
||||
|
||||
class dotdict(dict):
|
||||
"""dot.notation access to dictionary attributes"""
|
||||
__getattr__ = dict.get
|
||||
__setattr__ = dict.__setitem__
|
||||
__delattr__ = dict.__delitem__
|
||||
|
||||
|
||||
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 visual(true, preds=None, name='./pic/test.pdf'):
|
||||
"""
|
||||
Results visualization
|
||||
"""
|
||||
plt.figure()
|
||||
if preds is not None:
|
||||
plt.plot(preds, label='Prediction', linewidth=2)
|
||||
plt.plot(true, label='GroundTruth', linewidth=2)
|
||||
plt.legend()
|
||||
plt.savefig(name, bbox_inches='tight')
|
||||
|
||||
|
||||
def adjustment(gt, pred):
|
||||
anomaly_state = False
|
||||
for i in range(len(gt)):
|
||||
if gt[i] == 1 and pred[i] == 1 and not anomaly_state:
|
||||
anomaly_state = True
|
||||
for j in range(i, 0, -1):
|
||||
if gt[j] == 0:
|
||||
break
|
||||
else:
|
||||
if pred[j] == 0:
|
||||
pred[j] = 1
|
||||
for j in range(i, len(gt)):
|
||||
if gt[j] == 0:
|
||||
break
|
||||
else:
|
||||
if pred[j] == 0:
|
||||
pred[j] = 1
|
||||
elif gt[i] == 0:
|
||||
anomaly_state = False
|
||||
if anomaly_state:
|
||||
pred[i] = 1
|
||||
return gt, pred
|
||||
|
||||
|
||||
def cal_accuracy(y_pred, y_true):
|
||||
return np.mean(y_pred == y_true)
|
Reference in New Issue
Block a user