TIGER / GRID /src /data /loading /components /pre_processing.py
SYHDSG's picture
Upload TIGER project for AutoDL
d9115cb verified
Raw History Blame Contribute Delete
13.2 kB
from typing import Any, Dict, List, Optional, Union
import numpy as np
import tensorflow as tf
import torch
from src.utils.file_utils import load_json
from src.data.loading.components.interfaces import (
BaseDatasetConfig,
SemanticIDDatasetConfig,
)
from src.data.loading.components.interfaces import TokenizerConfig
from src.utils.utils import load_tokenize
# support functions
def convert_bytes_to_string(
batch_or_row: Dict[str, np.ndarray],
dataset_config: BaseDatasetConfig,
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, np.ndarray]:
# For each feature to apply, cast its np.ndarray of bytes to string.
for k in batch_or_row:
if is_feature_in_features_to_apply(features_to_apply, k):
batch_or_row[k] = batch_or_row[k].astype(str)
return batch_or_row
def is_feature_in_features_to_apply(features_to_apply: List[str], k: str) -> bool:
if len(features_to_apply) > 0 and k not in features_to_apply:
return False
return True
def filter_features_to_consider(
batch_or_row: Dict[str, tf.Tensor],
dataset_config: BaseDatasetConfig,
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, tf.Tensor]:
batch_or_row = map_feature_names(batch_or_row, dataset_config)
features_to_consider = set(dataset_config.features_to_consider)
if hasattr(dataset_config, "keep_user_id") and dataset_config.keep_user_id:
if dataset_config.user_id_field not in features_to_consider:
features_to_consider.add(dataset_config.user_id_field)
if hasattr(dataset_config, "keep_item_id") and dataset_config.keep_item_id:
if dataset_config.item_id_field not in features_to_consider:
features_to_consider.add(dataset_config.item_id_field)
if len(dataset_config.features_to_consider):
# Given a batch or row, filter the features to consider.
return {k: v for k, v in batch_or_row.items() if k in features_to_consider}
# if not specified, we consider all features
return batch_or_row
def convert_to_dense_numpy_array(
batch_or_row: Dict[str, tf.Tensor],
dataset_config: BaseDatasetConfig,
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, np.ndarray]:
# Transform a tfrecord example to a dictionary of numpy arrays, converting sparse tensors to dense numpy arrays.
for k in batch_or_row:
if is_feature_in_features_to_apply(features_to_apply, k):
batch_or_row[k] = tf.sparse.to_dense(batch_or_row[k]).numpy()
return batch_or_row
def map_feature_names(
batch_or_row: Dict[str, np.ndarray],
dataset_config: BaseDatasetConfig,
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, np.ndarray]:
# Given a batch or row, map the feature names to the desired feature names.
if dataset_config.feature_map:
batch_or_row = {
v: batch_or_row[k]
for k, v in dataset_config.feature_map.items()
if k in batch_or_row
}
return batch_or_row
def convert_fields_to_tensors(
batch_or_row: Dict[str, np.ndarray],
dataset_config: BaseDatasetConfig,
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, torch.Tensor]:
# Given a batch or row, convert all fields to torch tensors. Uses the field type map to determine the dtype, defaulting to torch.long
# if no dtype is specified.
for k, v in batch_or_row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
if isinstance(v, int) or isinstance(v, float):
v = [int(v)]
batch_or_row[k] = torch.tensor(v, dtype=dataset_config.field_type_map.get(k, torch.long)) # type: ignore
return batch_or_row
def filter_sequence_length_row(row: Dict[str, torch.Tensor], dataset_config: BaseDatasetConfig, features_to_apply: Optional[List[str]] = [], **kwargs) -> Dict[str, np.ndarray]: # type: ignore
# Only works for a row right now. This filters out rows that have fields with sequence length smaller than the min threshold.
# TODO(lneves): Make this work for a batch as well without creating batches of different sizes.
for _, tensor in row.items():
if len(tensor) < dataset_config.min_sequence_length:
return None
return row
def filter_empty_feature(row: Dict[str, torch.Tensor], dataset_config: BaseDatasetConfig, features_to_apply: Optional[List[str]] = [], **kwargs) -> Dict[str, np.ndarray]: # type: ignore
# Only works for a row right now. This filters out rows that have fields with empty tensors.
for k, v in row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
if len(v) == 0:
return None
return row
def map_sparse_id_to_semantic_id(
row: Dict[str, torch.Tensor],
dataset_config: SemanticIDDatasetConfig,
features_to_apply: Optional[List[str]] = [],
num_hierarchies: Optional[int] = None,
**kwargs,
) -> Dict[str, torch.Tensor]:
"""
Given a row of data, maps the sparse ids to semantic ids
based on the id_map in the dataset config.
"""
for k, v in row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
id_map: torch.Tensor = dataset_config.semantic_id_map.get(k, None)
# id_map is a D x N tensor
# where N is the number of unique items in the dataset
# and D is the number of hierarchies (semantic id digits)
if id_map is not None:
# flatten the semantic id sequence
if num_hierarchies is None:
row[k] = id_map.t()[v].view(-1)
else:
assert num_hierarchies <= id_map.size(
0
), "num_hierarchies must be less than or equal to the number of hierarchies in the semantic id map."
row[k] = id_map[:num_hierarchies].t()[v].view(-1)
else:
raise ValueError(f"Semantic id map not found for feature {k}")
return row
def trim_sequence_row(
row: Dict[str, Any],
dataset_config: BaseDatasetConfig,
sequence_length: int,
should_trim_left: bool,
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, Any]:
"""
Trim the sequences in the row to the sequence_length.
This function handles only rows (not batches) and assumes that the sequences are not
padded in the first dimension (the dimension to truncate).
Args:
row (Dict[str, Any]): A dictionary representing a row of data where each key
is a feature name and, if the feature is being trimmed, the corresponding
value is a sequential object to be truncated. The value will be trimmed on
the side determined by should_trim_left to the specified sequence_length in
the first dimension.
dataset_config (BaseDatasetConfig): The dataset configuration object.
sequence_length (int): The desired length to trim the sequences to.
should_trim_left (bool): If True, trim the left side of the sequence.
If False, trim the right side of the sequence.
features_to_apply (Optional[List[str]]): A list of feature names to apply the
trimming to. If empty, all features in the row will be trimmed.
Returns:
Dict[str, Any]:
A dictionary identical to the input row, but with the sequences trimmed to
the specified sequence_length on the specified side. Sequences that are
shorter than sequence_length will remain unchanged.
"""
if should_trim_left:
for k, v in row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
v = v[-sequence_length:]
row[k] = v
else:
for k, v in row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
v = v[:sequence_length]
row[k] = v
return row
def tokenize_text_features(
batch_or_row: Dict[str, Any],
features_to_apply: Optional[List[str]] = [],
tokenizer_config: Optional[TokenizerConfig] = None,
**kwargs,
) -> Dict[str, Any]:
# Tokenize text features. features_to_apply must contain only text features.
# This works for both rows and batches.
tokenize = load_tokenize(config=tokenizer_config)
batch_or_row_masks = {}
for k, v in batch_or_row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
k_mask = "_".join([k, "mask"])
if isinstance(v, np.ndarray) or isinstance(v, list):
tokenized_seq_list = [tokenize(s) for s in v]
batch_or_row[k] = torch.stack(
[seq["input_ids"].flatten() for seq in tokenized_seq_list]
) # seq_length x token_seq_length x 1
batch_or_row_masks[k_mask] = torch.stack(
[seq["attention_mask"].flatten() for seq in tokenized_seq_list]
) # seq_length x token_seq_length x 1
else:
tokenized_seq = tokenize(v)
batch_or_row[k] = tokenized_seq[
"input_ids"
].flatten() # token_seq_length
batch_or_row_masks[k_mask] = tokenized_seq[
"attention_mask"
].flatten() # token_seq_length
batch_or_row.update(batch_or_row_masks)
return batch_or_row
def preprocess_categorical_feature_to_idx(
batch_or_row: Dict[str, Any],
features_to_apply: Optional[List[str]] = [],
mapping_file: Optional[str] = "",
**kwargs,
) -> Dict[str, Any]:
# Translate categorical features to indices by looking at the mapping provided.
# features_to_apply must contain name of the categorical features whose mapping is available in the mapping_file.
# This works for both rows and batches.
# Load the mapping if a mapping file is provided
if mapping_file:
category_to_idx = load_json(mapping_file)
else:
raise ValueError("A valid path to the mapping file must be provided.")
# Helper function to translate feature values to index
def translate_to_index(value: Union[str, List[str]]) -> Union[int, List[int]]:
if isinstance(value, list):
return [
category_to_idx.get(v, 0) for v in value
] # Translate each element in the list
else:
return category_to_idx.get(
value, 0
) # Default to 0 (e.g., '<OOV>') if not found
# Determine if we are handling a single row or a batch of rows
is_batch = isinstance(batch_or_row, list)
# Apply the mapping to the appropriate features
if is_batch:
for row in batch_or_row:
for feature in features_to_apply:
if feature in row:
row[feature] = translate_to_index(row[feature])
else:
for feature in features_to_apply:
if feature in batch_or_row:
# if it's a sequence feature then process the entire sequence
batch_or_row[feature] = translate_to_index(batch_or_row[feature])
return batch_or_row
def map_sparse_id_to_embedding(
row: Dict[str, Any],
dataset_config = None,
features_to_apply: Optional[List[str]] = [],
sparse_id_field: str = "id",
embedding_field_to_add: str = "embedding",
**kwargs,
) -> Dict[str, Any]:
# Map sparse id to pre-computed embedding
embedding_map: torch.Tensor = dataset_config.embedding_map.get(
sparse_id_field, None
)
# embedding_map is an N x d tensor
# where N is the number of unique items in the dataset
# and d is the dimension of the embedding
if embedding_map is not None:
row[embedding_field_to_add] = embedding_map[row[sparse_id_field]].squeeze()
else:
raise ValueError(f"Embedding map not found")
return row
def squeeze_tensor_in_place(
batch_or_row: Dict[str, Any],
features_to_apply: Optional[List[str]] = [],
**kwargs,
) -> Dict[str, Any]:
# Squeeze the dimensions of the features to apply
# This squeeze is done in place, it does not create a new tensor
for k, v in batch_or_row.items():
if is_feature_in_features_to_apply(features_to_apply, k):
if isinstance(v, torch.Tensor):
batch_or_row[k] = v.squeeze_()
elif isinstance(v, list):
batch_or_row[k] = [
item.squeeze_() if isinstance(item, torch.Tensor) else item
for item in v
]
else:
raise ValueError(
f"Unsupported type for feature {k}: {type(v)}. Expected torch.Tensor or list."
)
return batch_or_row