236 lines
9.2 KiB
Python
236 lines
9.2 KiB
Python
import os
|
|
|
|
import lmdb
|
|
import numpy as np
|
|
import pandas as pd
|
|
import torch
|
|
from sklearn.preprocessing import StandardScaler, MinMaxScaler
|
|
import joblib
|
|
|
|
from utils.lmdb_utils import load_from_lmdb, get_lmdb_keys
|
|
|
|
|
|
def get_features(cycle: dict,
|
|
feature_config: dict) -> dict:
|
|
"""
|
|
Computes the feature for a given cycle defined by the feature_config
|
|
:param cycle: cycle data as dictionary
|
|
:param feature_config: feature config as dictionary
|
|
:return: dict with feature according feature config
|
|
"""
|
|
features = {}
|
|
feature_sets = feature_config["feature_sets"]
|
|
for feature_set in feature_sets:
|
|
for feature_def in feature_config[feature_set]:
|
|
if feature_set not in features:
|
|
features[feature_set] = {}
|
|
|
|
# skip features that are marked as ignored
|
|
if "ignored_features" in feature_config and feature_def["name"] in feature_config["ignored_features"]:
|
|
continue
|
|
|
|
feature_return = feature_def["fn"](cycle=cycle)
|
|
if isinstance(feature_return, dict):
|
|
if len(feature_return) > 1:
|
|
for feature_name, feature in feature_return.items():
|
|
features[feature_set][f"{feature_def['name']}_{feature_name}"] = feature
|
|
else:
|
|
features[feature_set][feature_def["name"]] = list(feature_return.values())[0]
|
|
else:
|
|
features[feature_set][feature_def["name"]] = feature_return
|
|
|
|
# check, if all features have same length
|
|
if feature_set in features and len(features[feature_set]) > 0 and isinstance(
|
|
list(features[feature_set].values())[0], np.ndarray):
|
|
feature_lengths = [len(x) for x in features[feature_set].values()]
|
|
if len(set(feature_lengths)) > 1:
|
|
raise ValueError(f"Feature set {feature_set} has features of different lengths: {feature_lengths}")
|
|
|
|
return features
|
|
|
|
|
|
def save_scalers(scalers: dict,
|
|
scaler_dir: str):
|
|
if not os.path.exists(scaler_dir):
|
|
os.makedirs(scaler_dir)
|
|
for feature_type in scalers:
|
|
for feature_name in scalers[feature_type]:
|
|
joblib.dump(scalers[feature_type][feature_name],
|
|
f"{scaler_dir}/{feature_name}.pkl")
|
|
|
|
|
|
def load_scalers(scaler_dir: str) -> dict:
|
|
scalers = dict()
|
|
for scaler_file in os.listdir(scaler_dir):
|
|
if scaler_file.endswith(".pkl"):
|
|
feature_name = scaler_file.replace(".pkl", "")
|
|
scalers[feature_name] = joblib.load(f"{scaler_dir}/{scaler_file}")
|
|
return scalers
|
|
|
|
|
|
def get_scalers_for_model(model_configuration: dict):
|
|
"""
|
|
Get the scalers for the features of a model configuration
|
|
Args:
|
|
model_configuration: configuration of the model
|
|
|
|
Returns:
|
|
scalers: scalers for the model
|
|
|
|
"""
|
|
|
|
scaler_dir = os.path.join(model_configuration["feature_config"]["dataset_dir"], "scalers")
|
|
scalers = load_scalers(scaler_dir)
|
|
return scalers
|
|
|
|
|
|
def get_feature_values(feature_type: str | None,
|
|
feature_name: str,
|
|
env: lmdb.Environment,
|
|
keys: list = None) -> list:
|
|
"""
|
|
Get the values of a feature from the lmdb dataset for all keys
|
|
:param feature_type: type of feature, can be None, then the first feature with the given name will be used
|
|
:param feature_name: feature name to extract
|
|
:param env: lmdb environment
|
|
:param keys: keys to extract the feature from, if None, all keys will be used / fetched from the database
|
|
:return: list with feature values
|
|
"""
|
|
if keys is None:
|
|
keys = get_lmdb_keys(env)
|
|
feature_values = []
|
|
for key in keys:
|
|
data = load_from_lmdb(env, key)
|
|
if feature_type is None:
|
|
for feature_type, feature_data in data.items():
|
|
if feature_name in feature_data:
|
|
feature_values.append(feature_data[feature_name])
|
|
break
|
|
else:
|
|
for current_feature_name, feature_data in data[feature_type].items():
|
|
# catch sub features that have been prefixed with the feature name
|
|
if current_feature_name.startswith(feature_name):
|
|
feature_values.append(feature_data)
|
|
break
|
|
return feature_values
|
|
|
|
|
|
def train_scalers(feature_type: str,
|
|
feature_name: str,
|
|
scaler_type,
|
|
sample,
|
|
env) -> dict:
|
|
scalers = dict()
|
|
all_features = sample[feature_type]
|
|
individual_feature_names = list()
|
|
for individual_feature_name in all_features:
|
|
if individual_feature_name.startswith(feature_name) and not individual_feature_name.endswith("_scaled"):
|
|
individual_feature_names.append(individual_feature_name)
|
|
|
|
for individual_feature_name in individual_feature_names:
|
|
feature_values = get_feature_values(feature_type, individual_feature_name, env)
|
|
if len(feature_values) == 0:
|
|
raise ValueError(f"No feature values found for feature {individual_feature_name}")
|
|
if isinstance(feature_values[0], list) or isinstance(feature_values[0], np.ndarray):
|
|
features_reshaped = np.concatenate(feature_values).reshape(-1, 1)
|
|
else:
|
|
features_reshaped = np.array(feature_values).reshape(-1, 1)
|
|
del feature_values
|
|
|
|
if scaler_type is not None:
|
|
scaler = scaler_type()
|
|
scaler.fit(features_reshaped)
|
|
else:
|
|
scaler = None
|
|
|
|
scalers[individual_feature_name] = scaler
|
|
return scalers
|
|
|
|
|
|
def scale_item(data: dict,
|
|
scalers: dict):
|
|
data_format = dict()
|
|
for feature_set in data:
|
|
if feature_set not in data_format:
|
|
data_format[feature_set] = dict()
|
|
for feature_name in data[feature_set]:
|
|
if feature_name.endswith("_scaled"):
|
|
continue
|
|
data_format[feature_set][feature_name] = data[feature_set][feature_name]
|
|
|
|
for feature_set in data_format:
|
|
for feature_name in data_format[feature_set]:
|
|
feature_values = data[feature_set][feature_name]
|
|
if scalers[feature_name] is not None:
|
|
if isinstance(feature_values, list) or isinstance(feature_values, np.ndarray):
|
|
scaled_feature_values = scalers[feature_name].transform(
|
|
feature_values.reshape(-1, 1)).flatten()
|
|
else:
|
|
scaled_feature_values = scalers[feature_name].transform(
|
|
np.array(feature_values).reshape(-1, 1)).flatten()
|
|
else:
|
|
scaled_feature_values = feature_values
|
|
data[feature_set][f"{feature_name}_scaled"] = scaled_feature_values
|
|
|
|
return data
|
|
|
|
|
|
def inverse_scale_feature(input_feature: np.ndarray | int | float | torch.Tensor,
|
|
feature_names: np.ndarray | list | str,
|
|
scalers: dict) -> np.ndarray:
|
|
"""
|
|
Inverse scales the input features, can handle single and multi feature input
|
|
Args:
|
|
input_feature: input feature as a numpy array
|
|
feature_names: names of input features as reference for scalers
|
|
scalers: dict of feature scalers
|
|
|
|
Returns:
|
|
scaled input features as numpy array
|
|
"""
|
|
|
|
if isinstance(feature_names, str):
|
|
feature_names = [feature_names]
|
|
|
|
if isinstance(input_feature, np.ndarray) or isinstance(input_feature, torch.Tensor):
|
|
input_dim = input_feature.shape[1] if len(input_feature.shape) > 1 else 1
|
|
elif isinstance(input_feature, float) or isinstance(input_feature, int):
|
|
# handle case, where input is scalar
|
|
input_scaled = scalers[feature_names[0]].inverse_transform([[input_feature]])[0][0]
|
|
return input_scaled
|
|
elif isinstance(input_feature, list):
|
|
input_feature = np.array(input_feature)
|
|
input_dim = input_feature.shape[1] if len(input_feature.shape) > 1 else 1
|
|
else:
|
|
raise ValueError(f"Unsupported input type: {type(input_feature)}")
|
|
|
|
# reshape, if input is one dimensional
|
|
if input_dim == 1:
|
|
input_feature = input_feature.reshape(-1, 1)
|
|
|
|
input_scaled = input_feature.copy()
|
|
for i in range(input_dim):
|
|
input_scaled[:, i] = scalers[feature_names[i]].inverse_transform(np.array([input_scaled[:, i]]))
|
|
|
|
return input_scaled
|
|
|
|
|
|
def combine_features(cycles: list[dict], feature_config: dict) -> dict:
|
|
combined_features = dict()
|
|
for feature_set in feature_config["feature_sets"]:
|
|
combined_features[feature_set] = dict()
|
|
if feature_set not in cycles[0]:
|
|
continue
|
|
features_in_set = cycles[0][feature_set].keys()
|
|
for feature in features_in_set:
|
|
feature_values = [cycles[i][feature_set][feature] for i in range(len(cycles))]
|
|
if isinstance(feature_values[0], dict):
|
|
for key in feature_values[0].keys():
|
|
feature_array = np.concatenate([feature_values[i][key] for i in range(len(feature_values))])
|
|
combined_features[feature_set][key] = feature_array
|
|
else:
|
|
feature_array = np.concatenate(feature_values)
|
|
combined_features[feature_set][feature] = feature_array
|
|
|
|
return combined_features
|