added code
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
|
||||
import lmdb
|
||||
import numpy as np
|
||||
from bson import ObjectId
|
||||
from torch.utils.data import IterableDataset
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils.dataset_creation import get_features, load_scalers, scale_item, combine_features
|
||||
from utils.dataset_utils import load_key_stats
|
||||
from utils.lmdb_utils import load_from_lmdb
|
||||
from vsm_datascience_common.cycle_database_connection.cycle_data import get_cycle_by_id
|
||||
from vsm_datascience_common.cycle_database_connection.db_utils import get_cycles_collection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_prepared_sequence_length(input_sequence: np.ndarray, model_configuration: dict) -> int:
|
||||
"""
|
||||
Returns the length of the input sequence after padding and resampling
|
||||
"""
|
||||
|
||||
initial_length = len(input_sequence)
|
||||
take_every_nth = model_configuration["preprocessing"]["take_every_nth"]
|
||||
padding_length = get_padding_length(model_configuration, resampled=False)
|
||||
raw_length = initial_length + padding_length
|
||||
adjusted = int(raw_length // take_every_nth)
|
||||
return adjusted
|
||||
|
||||
|
||||
def get_padding_length(model_configuration: dict, resampled: bool = True) -> int:
|
||||
"""
|
||||
Returns the length of the input sequence after padding and resampling
|
||||
"""
|
||||
take_every_nth = model_configuration["preprocessing"]["take_every_nth"]
|
||||
padding_length = int(model_configuration["input_window_length"] * (
|
||||
1 - model_configuration["preprocessing"]["min_input_length_fraction_for_padding"])) * (
|
||||
take_every_nth if not resampled else 1)
|
||||
return padding_length
|
||||
|
||||
|
||||
def get_number_of_windows(base_length: int, model_configuration: dict) -> int:
|
||||
"""
|
||||
Returns the number of windows for a given sequence length
|
||||
"""
|
||||
window_shift = model_configuration["preprocessing"]["window_shift"]
|
||||
input_window_length = model_configuration["input_window_length"]
|
||||
output_window_length = model_configuration["output_window_length"]
|
||||
output_window_offset = model_configuration["output_window_offset"]
|
||||
|
||||
if input_window_length > output_window_length + output_window_offset:
|
||||
return (base_length - input_window_length) // window_shift + 1
|
||||
else:
|
||||
return (base_length - output_window_length - output_window_offset) // window_shift + 1
|
||||
|
||||
|
||||
def get_collated_batch_for_key(sample_key: ObjectId | str,
|
||||
model_configuration: dict,
|
||||
start_cutoff: int = None,
|
||||
end_cutoff: int = None,
|
||||
lmdb_env=None) -> dict:
|
||||
sample_batch_for_key = get_batch_for_key(sample_key, model_configuration,
|
||||
start_cutoff=start_cutoff,
|
||||
end_cutoff=end_cutoff,
|
||||
lmdb_env=lmdb_env)
|
||||
collated = model_configuration["collate_fn"](sample_batch_for_key)
|
||||
return collated
|
||||
|
||||
|
||||
def get_batch_for_key(key,
|
||||
model_configuration: dict,
|
||||
start_cutoff: int = None,
|
||||
end_cutoff: int = None,
|
||||
lmdb_env=None) -> np.ndarray:
|
||||
"""
|
||||
Get the batch for a given key from the lmdb database or compute it directly.
|
||||
|
||||
The return has the same format as the batch produced by the batch_fn in the model configuration.
|
||||
Args:
|
||||
key: key to get the batch for, can also be object id from database
|
||||
model_configuration: model configuration to use
|
||||
start_cutoff: start cutoff for the batch, if None, the whole batch is used, CAUTION: cutoff should not be normalized -> in raw data points
|
||||
end_cutoff: end cutoff for the batch, if None, the whole batch is used, CAUTION: cutoff should not be normalized -> in raw data points
|
||||
lmdb_env: the lmdb environment to use, if None, the features are computed directly from the database
|
||||
|
||||
Returns:
|
||||
batch: batch for the given key, as returned by the batch_fn in the model configuration
|
||||
|
||||
"""
|
||||
if lmdb_env is None:
|
||||
# compute and scale features, ignored features are handled internally
|
||||
features = get_scaled_feature_for_key(key, model_configuration)
|
||||
else:
|
||||
# load features from lmdb
|
||||
features = load_from_lmdb(lmdb_env, str(key))
|
||||
# ignored features are handled internally, thus we need to remove them here
|
||||
ignored_features = model_configuration["feature_config"]["ignored_features"] if "ignored_features" in \
|
||||
model_configuration[
|
||||
"feature_config"] else []
|
||||
# add scaled versions of features
|
||||
ignored_features = ignored_features + [feat + "_scaled" for feat in ignored_features]
|
||||
for feature_set in features:
|
||||
for feature_name in list(features[feature_set].keys()):
|
||||
if feature_name in ignored_features:
|
||||
del features[feature_set][feature_name]
|
||||
|
||||
processed_chunk = process_chunk([key], [features], model_configuration)
|
||||
|
||||
# use cutoff, if provided
|
||||
if start_cutoff is not None or end_cutoff is not None:
|
||||
|
||||
item_length = len(processed_chunk[0]["target_features"])
|
||||
|
||||
if start_cutoff is None:
|
||||
start_cutoff = 0
|
||||
if end_cutoff is None:
|
||||
end_cutoff = item_length
|
||||
|
||||
# "normalize" cutoff to adjust for added padding and resampling
|
||||
padding_length = get_padding_length(model_configuration, resampled=False)
|
||||
take_every_nth = model_configuration["preprocessing"]["take_every_nth"]
|
||||
# make sure that there is no padding added to the start cutoff, so that it includes the padding added to the sequence
|
||||
start_cutoff_normalized = max(int(start_cutoff // take_every_nth), 0)
|
||||
# end cutoff must be adjusted to include the padding added to the sequence
|
||||
end_cutoff_normalized = min(int((end_cutoff + padding_length) // take_every_nth), item_length)
|
||||
for feature_set in processed_chunk[0]:
|
||||
if feature_set in model_configuration["feature_config"]["feature_sets"]:
|
||||
processed_chunk[0][feature_set] = processed_chunk[0][feature_set][
|
||||
start_cutoff_normalized:end_cutoff_normalized + 1]
|
||||
|
||||
if "batch_fn" in model_configuration and model_configuration["batch_fn"] is not None:
|
||||
batch = model_configuration["batch_fn"](processed_chunk, model_configuration)
|
||||
else:
|
||||
batch = processed_chunk
|
||||
if batch is None or len(batch) == 0:
|
||||
raise ValueError(f"Batch is empty for key {key}")
|
||||
return batch
|
||||
|
||||
|
||||
def get_scaled_feature_for_key(key: str,
|
||||
model_configuration: dict) -> tuple:
|
||||
feature_config = model_configuration["feature_config"]
|
||||
|
||||
user_cycles = list(
|
||||
get_cycles_collection().find({"user_id": ObjectId(key)} | feature_config["filter_criteria"]).sort("starts_at",
|
||||
1))
|
||||
cycle_features = list()
|
||||
for cycle in user_cycles:
|
||||
features = get_features(cycle, feature_config)
|
||||
cycle_features.append(features)
|
||||
|
||||
# combine all cycles for the user
|
||||
if len(cycle_features) == 0:
|
||||
raise ValueError(f"No features found for key {key}")
|
||||
features = combine_features(cycle_features, feature_config)
|
||||
scaler_dir = os.path.join(feature_config["dataset_dir"], "scalers")
|
||||
scalers = load_scalers(scaler_dir)
|
||||
# scale features
|
||||
scaled_features = scale_item(features, scalers)
|
||||
return scaled_features
|
||||
|
||||
|
||||
def augment_items(identifiers: list,
|
||||
items: list[dict],
|
||||
feature_config: dict,
|
||||
lmdb_env: lmdb.Environment = None) -> list:
|
||||
"""
|
||||
Augments the given sequences by attaching previous sequences
|
||||
Args:
|
||||
identifiers: identifiers of items for finding previous sequences
|
||||
items: actual items
|
||||
feature_config: feature config of the dataset
|
||||
lmdb_env: lmdb environment for loading previous sequences, can be None, in this case the items are computed directly
|
||||
|
||||
Returns:
|
||||
list of augmented items
|
||||
"""
|
||||
|
||||
if "augmentation" not in feature_config or \
|
||||
feature_config["augmentation"]["use_augmentation"] is False:
|
||||
return items
|
||||
|
||||
augmented_items = list()
|
||||
for i, item in enumerate(items):
|
||||
item_id = identifiers[i]
|
||||
try:
|
||||
item_data = get_cycles_collection().find_one({"_id": ObjectId(item_id)}, {"user_id": 1, "starts_at": 1})
|
||||
item_user_id = item_data["user_id"]
|
||||
item_starts_at = item_data["starts_at"]
|
||||
except KeyError:
|
||||
print(f"User ID not found for item {item_id}")
|
||||
continue
|
||||
previous_item_identifiers = list(get_cycles_collection().aggregate(
|
||||
# make sure to filter by user first to significantly reduce the number of items
|
||||
[
|
||||
{
|
||||
"$match": {
|
||||
"user_id": item_user_id,
|
||||
"starts_at": {"$lt": item_starts_at},
|
||||
}
|
||||
}
|
||||
] + feature_config["filter_criteria_pipeline"] + [
|
||||
{
|
||||
"$sort": {
|
||||
"starts_at": -1
|
||||
}
|
||||
},
|
||||
{
|
||||
"$project": {
|
||||
"_id": 1,
|
||||
"starts_at": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
))
|
||||
max_lookback = feature_config["augmentation"]["max_lookback"]
|
||||
previous_items = list()
|
||||
for previous_item in previous_item_identifiers:
|
||||
if len(previous_items) >= max_lookback:
|
||||
break
|
||||
previous_item_id = previous_item["_id"]
|
||||
if lmdb_env is None:
|
||||
# compute item
|
||||
cycle = get_cycle_by_id(previous_item_id)
|
||||
features = get_features(cycle, feature_config)
|
||||
scaler_dir = os.path.join(feature_config["dataset_dir"], "scalers")
|
||||
scalers = load_scalers(scaler_dir)
|
||||
previous_item = scale_item(features, scalers)
|
||||
previous_items.append(previous_item)
|
||||
else:
|
||||
# take item from lmdb
|
||||
try:
|
||||
# make sure to parse object id
|
||||
previous_item = load_from_lmdb(lmdb_env, str(previous_item_id))
|
||||
except KeyError:
|
||||
continue
|
||||
previous_items.append(previous_item)
|
||||
|
||||
# merge items into one
|
||||
feature_sets = feature_config["feature_sets"]
|
||||
augmented_item = copy.deepcopy(item)
|
||||
for feature_set in feature_sets:
|
||||
if feature_set not in augmented_item:
|
||||
continue
|
||||
if "static" in feature_set:
|
||||
continue
|
||||
for feature_name in augmented_item[feature_set]:
|
||||
if feature_name not in augmented_item[feature_set]:
|
||||
continue
|
||||
augmented_item[feature_set][feature_name] = np.concatenate(
|
||||
[previous_item[feature_set][feature_name] for previous_item in previous_items]
|
||||
+ [augmented_item[feature_set][feature_name]])
|
||||
|
||||
augmented_items.append(augmented_item)
|
||||
|
||||
return augmented_items
|
||||
|
||||
|
||||
def process_chunk(ids: list,
|
||||
chunk: list,
|
||||
model_configuration: dict,
|
||||
ignored_features: list = None,
|
||||
pad_sequences: bool = True,
|
||||
statics_as_list: bool = True):
|
||||
"""
|
||||
Creates a chunk of data as dataframe for training and inference of a tft model.
|
||||
:param ids: list of ids of the data points in the chunk
|
||||
:param chunk: list of data points
|
||||
|
||||
:return: dataframe with the data points in the chunk
|
||||
"""
|
||||
|
||||
if ignored_features is None:
|
||||
ignored_features = list()
|
||||
|
||||
input_features = list(chunk[0]["target_features"].keys())
|
||||
feature_config = model_configuration["feature_config"]
|
||||
|
||||
# variables for padding
|
||||
padding_value = 0
|
||||
|
||||
# create dataframe from items in chunk
|
||||
data = []
|
||||
for i, item in enumerate(chunk):
|
||||
item_length = item["target_features"][input_features[0]].shape[0]
|
||||
take_every_nth = model_configuration["preprocessing"]["take_every_nth"]
|
||||
|
||||
data_item = dict()
|
||||
for feature_set in feature_config["feature_sets"]:
|
||||
if feature_set in item and len(item[feature_set]) > 0:
|
||||
# fill in the data
|
||||
source = item[feature_set]
|
||||
features_to_use = [feature for feature in source.keys() if "_scaled" in feature]
|
||||
feature_set_data = [source[feature] for feature in features_to_use]
|
||||
|
||||
padding_length = get_padding_length(model_configuration, resampled=False)
|
||||
|
||||
if len(feature_set_data) == 0:
|
||||
raise ValueError(f"No features found for key {ids[i]} in feature set {feature_set}")
|
||||
|
||||
if len(feature_set_data[0]) == 1:
|
||||
# if the feature is constant, we need to repeat it for all time points
|
||||
if statics_as_list:
|
||||
if pad_sequences:
|
||||
desired_length = item_length + padding_length
|
||||
feature_set_data = np.array(
|
||||
[np.full((desired_length,), val) for val in feature_set_data]).T[::take_every_nth]
|
||||
else:
|
||||
feature_set_data = np.array(
|
||||
[np.full((item_length,), val) for val in feature_set_data]).T[::take_every_nth]
|
||||
else:
|
||||
feature_set_data = np.array(feature_set_data).flatten()
|
||||
else:
|
||||
if pad_sequences:
|
||||
padding_values = np.full((padding_length, len(feature_set_data)),
|
||||
[padding_value for vals in feature_set_data]).T
|
||||
feature_set_data = np.concatenate([padding_values, np.array(feature_set_data)], axis=1)
|
||||
# create bins of n values and apply mean
|
||||
|
||||
# check, if special accumulation function has been specified
|
||||
individual_feature_data = list()
|
||||
for i, feature_to_use in enumerate(features_to_use):
|
||||
try:
|
||||
# get config for feature, make sure to replace scaled to get actual config
|
||||
current_feature_config = \
|
||||
[x for x in feature_config[feature_set] if
|
||||
x["name"] == feature_to_use.replace("_scaled", "")][0]
|
||||
current_accumulation_fn = current_feature_config["accumulation_fn"]
|
||||
except (KeyError, IndexError):
|
||||
# default to mean
|
||||
current_accumulation_fn = np.mean
|
||||
|
||||
current_feature_data = apply_fn_to_bins(feature_set_data[i], take_every_nth,
|
||||
current_accumulation_fn)
|
||||
individual_feature_data.append(current_feature_data)
|
||||
|
||||
# stack individual features
|
||||
feature_set_data = np.stack(individual_feature_data).T
|
||||
|
||||
if feature_set not in data_item:
|
||||
data_item[feature_set] = feature_set_data
|
||||
else:
|
||||
data_item[feature_set] = np.concatenate([data_item[feature_set], feature_set_data], axis=1)
|
||||
|
||||
data.append(data_item)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def apply_fn_to_bins(input_sequence: np.ndarray,
|
||||
bin_size: int,
|
||||
fn: callable) -> np.ndarray:
|
||||
"""
|
||||
Applies a function to the bins of the input sequence along the last dimension and returns the results.
|
||||
Args:
|
||||
input_sequence: sequence to apply the function to (1D or 2D)
|
||||
bin_size: size of the bins
|
||||
fn: function to apply to the bins
|
||||
|
||||
Returns:
|
||||
np.ndarray: result of the function applied to the bins
|
||||
|
||||
"""
|
||||
if input_sequence.ndim == 1:
|
||||
input_sequence = input_sequence.reshape(1, -1)
|
||||
dims = 1
|
||||
else:
|
||||
dims = input_sequence.shape[-1]
|
||||
|
||||
result = []
|
||||
for row in input_sequence:
|
||||
row_result = []
|
||||
for start in range(0, len(row), bin_size):
|
||||
end = min(start + bin_size, len(row))
|
||||
bin_slice = row[start:end]
|
||||
row_result.append(fn(bin_slice))
|
||||
result.append(row_result)
|
||||
|
||||
if dims > 1:
|
||||
return np.array(result)
|
||||
else:
|
||||
return np.array(result[0])
|
||||
|
||||
|
||||
def produce_window_batches(data_chunk: list,
|
||||
model_configuration: dict,
|
||||
offsets: list | np.ndarray = None) -> list:
|
||||
"""
|
||||
Produces batches of windows from the data chunk.
|
||||
Args:
|
||||
data_chunk: data chunk
|
||||
model_configuration: configuration to use
|
||||
offsets: offsets used to determine the length of data to use, uses item[-offset:] of data
|
||||
|
||||
Returns:
|
||||
data: list of windows
|
||||
"""
|
||||
input_window_length = model_configuration["input_window_length"]
|
||||
output_window_length = model_configuration["output_window_length"]
|
||||
output_window_offset = model_configuration["output_window_offset"]
|
||||
window_shift = model_configuration["preprocessing"]["window_shift"]
|
||||
|
||||
if offsets is None:
|
||||
offsets = np.zeros(len(data_chunk), dtype=int)
|
||||
|
||||
data = list()
|
||||
for i, item in enumerate(data_chunk):
|
||||
item_length = item["target_features"][-offsets[i]:].shape[0]
|
||||
total_window_length = input_window_length if input_window_length > output_window_length + output_window_offset \
|
||||
else output_window_length + output_window_offset
|
||||
|
||||
if item_length < total_window_length:
|
||||
continue
|
||||
|
||||
# number of windows is defined by the input window length and output window length with offset
|
||||
num_windows = get_number_of_windows(item_length, model_configuration)
|
||||
for j in range(num_windows):
|
||||
window = dict()
|
||||
for key, value in item.items():
|
||||
if value is None:
|
||||
window[key] = None
|
||||
else:
|
||||
if key == "target_features":
|
||||
window[key] = value[-offsets[i]:][j * window_shift + output_window_offset:
|
||||
j * window_shift + output_window_length + output_window_offset]
|
||||
else:
|
||||
window[key] = value[-offsets[i]:][j * window_shift:j * window_shift + input_window_length]
|
||||
|
||||
data.append(window)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def produce_simple_batches(data_chunk: list,
|
||||
model_configuration: dict,
|
||||
offsets: list | np.ndarray = None) -> list:
|
||||
"""
|
||||
Produces batches without windowing.
|
||||
Args:
|
||||
data_chunk: data chunk
|
||||
model_configuration: configuration to use
|
||||
offsets: offsets used to determine the length of data to use, uses item[-offset:] of data
|
||||
|
||||
Returns:
|
||||
data: list of items
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class LMDBIterableDataset(IterableDataset):
|
||||
def __init__(self,
|
||||
lmdb_env_path: str,
|
||||
lmdb_keys: list[str],
|
||||
model_configuration: dict,
|
||||
batch_size: int = 32):
|
||||
self.lmdb_path = lmdb_env_path
|
||||
self.lmdb_env = None
|
||||
self.lmdb_keys = lmdb_keys
|
||||
self.key_subset = None
|
||||
self.model_configuration = model_configuration
|
||||
self.batch_size = batch_size
|
||||
self.keys_stats = load_key_stats(model_configuration["feature_config"]["dataset_dir"])
|
||||
self.len = None
|
||||
|
||||
try:
|
||||
self.model_configuration["batch_fn"]
|
||||
except KeyError:
|
||||
raise KeyError("Batch function not found in model configuration")
|
||||
|
||||
def set_key_subset(self, key_subset: list[str]):
|
||||
"""
|
||||
Set the key subset to use for the dataset.
|
||||
Args:
|
||||
key_subset: list of keys to use
|
||||
"""
|
||||
self.key_subset = key_subset
|
||||
# reset length
|
||||
self.len = None
|
||||
|
||||
def get_length_of_data_subset(self, key_set: list[str]):
|
||||
"""
|
||||
Get the length of the data subset.
|
||||
Args:
|
||||
key_set: list of keys to use
|
||||
"""
|
||||
# run simplified version of __iter__ to get the length
|
||||
|
||||
num_steps = 0
|
||||
i = 0
|
||||
num_batch_items = 0
|
||||
while True:
|
||||
if num_batch_items >= self.batch_size:
|
||||
num_steps += 1
|
||||
num_batch_items -= self.batch_size
|
||||
else:
|
||||
if i >= len(key_set):
|
||||
if num_batch_items > 0:
|
||||
num_steps += 1
|
||||
break
|
||||
key = key_set[i]
|
||||
i += 1
|
||||
|
||||
# try to fetch stats from key_stats
|
||||
try:
|
||||
current_key_stats = self.keys_stats["by_key"][key]
|
||||
base_length = current_key_stats["item_length"]
|
||||
take_every_nth = self.model_configuration["preprocessing"]["take_every_nth"]
|
||||
padding_length = get_padding_length(self.model_configuration, resampled=False)
|
||||
raw_length = base_length + padding_length
|
||||
item_length = int(raw_length // take_every_nth)
|
||||
except:
|
||||
item = load_from_lmdb(self.lmdb_env, key)
|
||||
item_length = get_prepared_sequence_length(
|
||||
item["target_features"][list(item["target_features"])[0]],
|
||||
self.model_configuration)
|
||||
num_windows = get_number_of_windows(item_length, self.model_configuration)
|
||||
num_batch_items += num_windows
|
||||
|
||||
return num_steps
|
||||
|
||||
def __iter__(self):
|
||||
self.init_lmdb_env()
|
||||
|
||||
# if no key subset is set, use all keys
|
||||
if self.key_subset is None:
|
||||
self.key_subset = self.lmdb_keys
|
||||
|
||||
random.shuffle(self.key_subset)
|
||||
|
||||
batch = list()
|
||||
counter = 0
|
||||
collate_fn = self.model_configuration["collate_fn"]
|
||||
while True:
|
||||
if len(batch) >= self.batch_size:
|
||||
if collate_fn is None:
|
||||
yield batch[:self.batch_size]
|
||||
else:
|
||||
yield collate_fn(batch[:self.batch_size])
|
||||
batch = batch[self.batch_size:]
|
||||
else:
|
||||
if counter >= len(self.key_subset):
|
||||
if len(batch) > 0:
|
||||
yield collate_fn(batch)
|
||||
break
|
||||
key = self.lmdb_keys[counter]
|
||||
counter += 1
|
||||
try:
|
||||
current_batch = get_batch_for_key(key, self.model_configuration, lmdb_env=self.lmdb_env)
|
||||
except ValueError:
|
||||
# if betch is empty, try next
|
||||
continue
|
||||
batch.extend(current_batch)
|
||||
|
||||
def init_lmdb_env(self):
|
||||
if self.lmdb_env is None:
|
||||
self.lmdb_env = lmdb.open(self.lmdb_path,
|
||||
readonly=True,
|
||||
lock=False,
|
||||
readahead=False,
|
||||
meminit=False)
|
||||
|
||||
def __len__(self):
|
||||
self.init_lmdb_env()
|
||||
|
||||
# if no key subset is set, use all keys
|
||||
if self.key_subset is None:
|
||||
self.key_subset = self.lmdb_keys
|
||||
|
||||
if self.len is None:
|
||||
num_steps = self.get_length_of_data_subset(self.key_subset)
|
||||
self.len = num_steps
|
||||
|
||||
return self.len
|
||||
Reference in New Issue
Block a user