onestop utility for preprocessing and encoding structured data arguments featurenames dict mapping the names of your features to their type specification e g myfeature integercategorical or myfeature featurespace integercategorical for a complete list of all supported types see available feature types paragraph below outputmode one of concat or dict in concat mode all features get concatenated together into a single vector in dict mode the featurespace returns a dict of individually encoded features with the same keys as the input dict keys crosses list of features to be crossed together e g crossesfeature1 feature2 the features will be crossed by hashing their combined value into a fixedlength vector crossingdim default vector size for hashing crossed features defaults to 32 hashingdim default vector size for hashing features of type integerhashed and stringhashed defaults to 32 numdiscretizationbins default number of bins to be used for discretizing features of type floatdiscretized defaults to 32 available feature types note that all features can be referred to by their string name e g integercategorical when using the string name the default argument values are used python plain float values featurespace floatnamenone float values to be preprocessed via featurewise standardization i e via a keras layers normalization layer featurespace floatnormalizednamenone float values to be preprocessed via linear rescaling i e via a keras layers rescaling layer featurespace floatrescaledscale1 offset0 namenone float values to be discretized by default the discrete representation will then be onehot encoded featurespace floatdiscretized numbins binboundariesnone outputmodeonehot namenone integer values to be indexed by default the discrete representation will then be onehot encoded featurespace integercategorical maxtokensnone numoovindices1 outputmodeonehot namenone string values to be indexed by default the discrete representation will then be onehot encoded featurespace stringcategorical maxtokensnone numoovindices1 outputmodeonehot namenone integer values to be hashed into a fixed number of bins by default the discrete representation will then be onehot encoded featurespace integerhashednumbins outputmodeonehot namenone string values to be hashed into a fixed number of bins by default the discrete representation will then be onehot encoded featurespace stringhashednumbins outputmodeonehot namenone examples basic usage with a dict of input data python rawdata floatvalues 0 0 0 1 0 2 0 3 stringvalues zero one two three intvalues 0 1 2 3 dataset tf data dataset fromtensorslicesrawdata featurespace featurespace features floatvalues floatnormalized stringvalues stringcategorical intvalues integercategorical crossesstringvalues intvalues outputmodeconcat before you start using the featurespace you must adapt it on some data featurespace adaptdataset you can call the featurespace on a dict of data batched or unbatched outputvector featurespacerawdata basic usage with tf data python unlabeled data preprocessedds unlabeleddataset mapfeaturespace labeled data preprocessedds labeleddataset maplambda x y featurespacex y basic usage with the keras functional api python retrieve a dict keras input objects inputs featurespace getinputs retrieve the corresponding encoded keras tensors encodedfeatures featurespace getencodedfeatures build a functional model outputs keras layers dense1 activationsigmoidencodedfeatures model keras modelinputs outputs customizing each feature or feature cross python featurespace featurespace features floatvalues featurespace floatnormalized stringvalues featurespace stringcategoricalmaxtokens10 intvalues featurespace integercategoricalmaxtokens10 crosses featurespace crossstringvalues intvalues crossingdim32 outputmodeconcat returning a dict of integerencoded features python featurespace featurespace features stringvalues featurespace stringcategoricaloutputmodeint intvalues featurespace integercategoricaloutputmodeint crosses featurespace cross featurenamesstringvalues intvalues crossingdim32 outputmodeint outputmodedict specifying your own keras preprocessing layer python let s say that one of the features is a short text paragraph that we want to encode as a vector one vector per paragraph via tfidf data text 1st string 2nd string 3rd string there s a keras layer for this textvectorization customlayer layers textvectorizationoutputmodetfidf we can use featurespace feature to create a custom feature that will use our preprocessing layer featurespace featurespace features text featurespace feature preprocessorcustomlayer dtypestring outputmodefloat outputmodeconcat featurespace adapttf data dataset fromtensorslicesdata outputvector featurespacedata retrieving the underlying keras preprocessing layers python the preprocessing layer of each feature is available in preprocessors preprocessinglayer featurespace preprocessorsfeature1 the crossing layer of each feature cross is available in crossers it s an instance of keras layers hashedcrossing crossinglayer featurespace crossersfeature1xfeature2 saving and reloading a featurespace python featurespace savefeaturespace keras reloadedfeaturespace keras models loadmodelfeaturespace keras special case a normalization layer with preset meanvariance not adaptable call adapt on each individual adaptable layer todo consider rewriting this to instead iterate on the dataset once split each batch into individual features and call the layer s adaptfunction on each batch to simulate the behavior of adapt in a more performant fashion todo consider adding an adapt progress bar sample 1 element to check the rank the dataset yields unbatched scalars batch it if the rank is 1 add a dimension so we can reduce on axis1 note if rank was previously 0 it is now 1 fast mode if the object isn t built we create the encoder and concat layers below finishes building many preprocessing layers support all backends but many do not switch to tf to make featurespace work universally this scope is to make sure that inner tfdatalayers will not convert outputs back to backendnative they should be tf tensors throughout save the featurespace instance to a keras file you can reload it via keras models loadmodel python featurespace savefeaturespace keras reloadedfs keras models loadmodelfeaturespace keras one stop utility for preprocessing and encoding structured data arguments feature_names dict mapping the names of your features to their type specification e g my_feature integer_categorical or my_feature featurespace integer_categorical for a complete list of all supported types see available feature types paragraph below output_mode one of concat or dict in concat mode all features get concatenated together into a single vector in dict mode the featurespace returns a dict of individually encoded features with the same keys as the input dict keys crosses list of features to be crossed together e g crosses feature_1 feature_2 the features will be crossed by hashing their combined value into a fixed length vector crossing_dim default vector size for hashing crossed features defaults to 32 hashing_dim default vector size for hashing features of type integer_hashed and string_hashed defaults to 32 num_discretization_bins default number of bins to be used for discretizing features of type float_discretized defaults to 32 available feature types note that all features can be referred to by their string name e g integer_categorical when using the string name the default argument values are used python plain float values featurespace float name none float values to be preprocessed via featurewise standardization i e via a keras layers normalization layer featurespace float_normalized name none float values to be preprocessed via linear rescaling i e via a keras layers rescaling layer featurespace float_rescaled scale 1 offset 0 name none float values to be discretized by default the discrete representation will then be one hot encoded featurespace float_discretized num_bins bin_boundaries none output_mode one_hot name none integer values to be indexed by default the discrete representation will then be one hot encoded featurespace integer_categorical max_tokens none num_oov_indices 1 output_mode one_hot name none string values to be indexed by default the discrete representation will then be one hot encoded featurespace string_categorical max_tokens none num_oov_indices 1 output_mode one_hot name none integer values to be hashed into a fixed number of bins by default the discrete representation will then be one hot encoded featurespace integer_hashed num_bins output_mode one_hot name none string values to be hashed into a fixed number of bins by default the discrete representation will then be one hot encoded featurespace string_hashed num_bins output_mode one_hot name none examples basic usage with a dict of input data python raw_data float_values 0 0 0 1 0 2 0 3 string_values zero one two three int_values 0 1 2 3 dataset tf data dataset from_tensor_slices raw_data feature_space featurespace features float_values float_normalized string_values string_categorical int_values integer_categorical crosses string_values int_values output_mode concat before you start using the featurespace you must adapt it on some data feature_space adapt dataset you can call the featurespace on a dict of data batched or unbatched output_vector feature_space raw_data basic usage with tf data python unlabeled data preprocessed_ds unlabeled_dataset map feature_space labeled data preprocessed_ds labeled_dataset map lambda x y feature_space x y basic usage with the keras functional api python retrieve a dict keras input objects inputs feature_space get_inputs retrieve the corresponding encoded keras tensors encoded_features feature_space get_encoded_features build a functional model outputs keras layers dense 1 activation sigmoid encoded_features model keras model inputs outputs customizing each feature or feature cross python feature_space featurespace features float_values featurespace float_normalized string_values featurespace string_categorical max_tokens 10 int_values featurespace integer_categorical max_tokens 10 crosses featurespace cross string_values int_values crossing_dim 32 output_mode concat returning a dict of integer encoded features python feature_space featurespace features string_values featurespace string_categorical output_mode int int_values featurespace integer_categorical output_mode int crosses featurespace cross feature_names string_values int_values crossing_dim 32 output_mode int output_mode dict specifying your own keras preprocessing layer python let s say that one of the features is a short text paragraph that we want to encode as a vector one vector per paragraph via tf idf data text 1st string 2nd string 3rd string there s a keras layer for this textvectorization custom_layer layers textvectorization output_mode tf_idf we can use featurespace feature to create a custom feature that will use our preprocessing layer feature_space featurespace features text featurespace feature preprocessor custom_layer dtype string output_mode float output_mode concat feature_space adapt tf data dataset from_tensor_slices data output_vector feature_space data retrieving the underlying keras preprocessing layers python the preprocessing layer of each feature is available in preprocessors preprocessing_layer feature_space preprocessors feature1 the crossing layer of each feature cross is available in crossers it s an instance of keras layers hashedcrossing crossing_layer feature_space crossers feature1_x_feature2 saving and reloading a featurespace python feature_space save featurespace keras reloaded_feature_space keras models load_model featurespace keras special case a normalization layer with preset mean variance not adaptable call adapt on each individual adaptable layer todo consider rewriting this to instead iterate on the dataset once split each batch into individual features and call the layer s _adapt_function on each batch to simulate the behavior of adapt in a more performant fashion todo consider adding an adapt progress bar sample 1 element to check the rank the dataset yields unbatched scalars batch it if the rank is 1 add a dimension so we can reduce on axis 1 note if rank was previously 0 it is now 1 finish building the layer fast mode if the object isn t built we create the encoder and concat layers below finishes building many preprocessing layers support all backends but many do not switch to tf to make featurespace work universally this scope is to make sure that inner tfdatalayers will not convert outputs back to backend native they should be tf tensors throughout save the featurespace instance to a keras file you can reload it via keras models load_model python feature_space save featurespace keras reloaded_fs keras models load_model featurespace keras
import tree

from keras import backend
from keras import layers
from keras.api_export import keras_export
from keras.layers.layer import Layer
from keras.saving import saving_lib
from keras.saving import serialization_lib
from keras.utils import backend_utils
from keras.utils.module_utils import tensorflow as tf
from keras.utils.naming import auto_name


class Cross:
    def __init__(self, feature_names, crossing_dim, output_mode="one_hot"):
        if output_mode not in {"int", "one_hot"}:
            raise ValueError(
                "Invalid value for argument `output_mode`. "
                "Expected one of {'int', 'one_hot'}. "
                f"Received: output_mode={output_mode}"
            )
        self.feature_names = tuple(feature_names)
        self.crossing_dim = crossing_dim
        self.output_mode = output_mode

    @property
    def name(self):
        return "_X_".join(self.feature_names)

    def get_config(self):
        return {
            "feature_names": self.feature_names,
            "crossing_dim": self.crossing_dim,
            "output_mode": self.output_mode,
        }

    @classmethod
    def from_config(cls, config):
        return cls(**config)


class Feature:
    def __init__(self, dtype, preprocessor, output_mode):
        if output_mode not in {"int", "one_hot", "float"}:
            raise ValueError(
                "Invalid value for argument `output_mode`. "
                "Expected one of {'int', 'one_hot', 'float'}. "
                f"Received: output_mode={output_mode}"
            )
        self.dtype = dtype
        if isinstance(preprocessor, dict):
            preprocessor = serialization_lib.deserialize_keras_object(
                preprocessor
            )
        self.preprocessor = preprocessor
        self.output_mode = output_mode

    def get_config(self):
        return {
            "dtype": self.dtype,
            "preprocessor": serialization_lib.serialize_keras_object(
                self.preprocessor
            ),
            "output_mode": self.output_mode,
        }

    @classmethod
    def from_config(cls, config):
        return cls(**config)


@keras_export("keras.utils.FeatureSpace")
class FeatureSpace(Layer):
    

    @classmethod
    def cross(cls, feature_names, crossing_dim, output_mode="one_hot"):
        return Cross(feature_names, crossing_dim, output_mode=output_mode)

    @classmethod
    def feature(cls, dtype, preprocessor, output_mode):
        return Feature(dtype, preprocessor, output_mode)

    @classmethod
    def float(cls, name=None):
        from keras.layers.core import identity

        name = name or auto_name("float")
        preprocessor = identity.Identity(
            dtype="float32", name=f"{name}_preprocessor"
        )
        return Feature(
            dtype="float32", preprocessor=preprocessor, output_mode="float"
        )

    @classmethod
    def float_rescaled(cls, scale=1.0, offset=0.0, name=None):
        name = name or auto_name("float_rescaled")
        preprocessor = layers.Rescaling(
            scale=scale, offset=offset, name=f"{name}_preprocessor"
        )
        return Feature(
            dtype="float32", preprocessor=preprocessor, output_mode="float"
        )

    @classmethod
    def float_normalized(cls, name=None):
        name = name or auto_name("float_normalized")
        preprocessor = layers.Normalization(
            axis=-1, name=f"{name}_preprocessor"
        )
        return Feature(
            dtype="float32", preprocessor=preprocessor, output_mode="float"
        )

    @classmethod
    def float_discretized(
        cls, num_bins, bin_boundaries=None, output_mode="one_hot", name=None
    ):
        name = name or auto_name("float_discretized")
        preprocessor = layers.Discretization(
            num_bins=num_bins,
            bin_boundaries=bin_boundaries,
            name=f"{name}_preprocessor",
        )
        return Feature(
            dtype="float32", preprocessor=preprocessor, output_mode=output_mode
        )

    @classmethod
    def integer_categorical(
        cls,
        max_tokens=None,
        num_oov_indices=1,
        output_mode="one_hot",
        name=None,
    ):
        name = name or auto_name("integer_categorical")
        preprocessor = layers.IntegerLookup(
            name=f"{name}_preprocessor",
            max_tokens=max_tokens,
            num_oov_indices=num_oov_indices,
        )
        return Feature(
            dtype="int32", preprocessor=preprocessor, output_mode=output_mode
        )

    @classmethod
    def string_categorical(
        cls,
        max_tokens=None,
        num_oov_indices=1,
        output_mode="one_hot",
        name=None,
    ):
        name = name or auto_name("string_categorical")
        preprocessor = layers.StringLookup(
            name=f"{name}_preprocessor",
            max_tokens=max_tokens,
            num_oov_indices=num_oov_indices,
        )
        return Feature(
            dtype="string", preprocessor=preprocessor, output_mode=output_mode
        )

    @classmethod
    def string_hashed(cls, num_bins, output_mode="one_hot", name=None):
        name = name or auto_name("string_hashed")
        preprocessor = layers.Hashing(
            name=f"{name}_preprocessor", num_bins=num_bins
        )
        return Feature(
            dtype="string", preprocessor=preprocessor, output_mode=output_mode
        )

    @classmethod
    def integer_hashed(cls, num_bins, output_mode="one_hot", name=None):
        name = name or auto_name("integer_hashed")
        preprocessor = layers.Hashing(
            name=f"{name}_preprocessor", num_bins=num_bins
        )
        return Feature(
            dtype="int32", preprocessor=preprocessor, output_mode=output_mode
        )

    def __init__(
        self,
        features,
        output_mode="concat",
        crosses=None,
        crossing_dim=32,
        hashing_dim=32,
        num_discretization_bins=32,
        name=None,
    ):
        super().__init__(name=name)
        if not features:
            raise ValueError("The `features` argument cannot be None or empty.")
        self.crossing_dim = crossing_dim
        self.hashing_dim = hashing_dim
        self.num_discretization_bins = num_discretization_bins
        self.features = {
            name: self._standardize_feature(name, value)
            for name, value in features.items()
        }
        self.crosses = []
        if crosses:
            feature_set = set(features.keys())
            for cross in crosses:
                if isinstance(cross, dict):
                    cross = serialization_lib.deserialize_keras_object(cross)
                if isinstance(cross, Cross):
                    self.crosses.append(cross)
                else:
                    if not crossing_dim:
                        raise ValueError(
                            "When specifying `crosses`, the argument "
                            "`crossing_dim` "
                            "(dimensionality of the crossing space) "
                            "should be specified as well."
                        )
                    for key in cross:
                        if key not in feature_set:
                            raise ValueError(
                                "All features referenced "
                                "in the `crosses` argument "
                                "should be present in the `features` dict. "
                                f"Received unknown features: {cross}"
                            )
                    self.crosses.append(Cross(cross, crossing_dim=crossing_dim))
        self.crosses_by_name = {cross.name: cross for cross in self.crosses}

        if output_mode not in {"dict", "concat"}:
            raise ValueError(
                "Invalid value for argument `output_mode`. "
                "Expected one of {'dict', 'concat'}. "
                f"Received: output_mode={output_mode}"
            )
        self.output_mode = output_mode

        self.inputs = {
            name: self._feature_to_input(name, value)
            for name, value in self.features.items()
        }
        self.preprocessors = {
            name: value.preprocessor for name, value in self.features.items()
        }
        self.encoded_features = None
        self.crossers = {
            cross.name: self._cross_to_crosser(cross) for cross in self.crosses
        }
        self.one_hot_encoders = {}
        self._is_adapted = False
        self.concat = None
        self._preprocessed_features_names = None
        self._crossed_features_names = None
        self._sublayers_built = False

    def _feature_to_input(self, name, feature):
        return layers.Input(shape=(1,), dtype=feature.dtype, name=name)

    def _standardize_feature(self, name, feature):
        if isinstance(feature, Feature):
            return feature

        if isinstance(feature, dict):
            return serialization_lib.deserialize_keras_object(feature)

        if feature == "float":
            return self.float(name=name)
        elif feature == "float_normalized":
            return self.float_normalized(name=name)
        elif feature == "float_rescaled":
            return self.float_rescaled(name=name)
        elif feature == "float_discretized":
            return self.float_discretized(
                name=name, num_bins=self.num_discretization_bins
            )
        elif feature == "integer_categorical":
            return self.integer_categorical(name=name)
        elif feature == "string_categorical":
            return self.string_categorical(name=name)
        elif feature == "integer_hashed":
            return self.integer_hashed(self.hashing_dim, name=name)
        elif feature == "string_hashed":
            return self.string_hashed(self.hashing_dim, name=name)
        else:
            raise ValueError(f"Invalid feature type: {feature}")

    def _cross_to_crosser(self, cross):
        return layers.HashedCrossing(cross.crossing_dim, name=cross.name)

    def _list_adaptable_preprocessors(self):
        adaptable_preprocessors = []
        for name in self.features.keys():
            preprocessor = self.preprocessors[name]
            
            
            if isinstance(preprocessor, layers.Normalization):
                if preprocessor.input_mean is not None:
                    continue
            if hasattr(preprocessor, "adapt"):
                adaptable_preprocessors.append(name)
        return adaptable_preprocessors

    def adapt(self, dataset):
        if not isinstance(dataset, tf.data.Dataset):
            raise ValueError(
                "`adapt()` can only be called on a tf.data.Dataset. "
                f"Received instead: {dataset} (of type {type(dataset)})"
            )

        for name in self._list_adaptable_preprocessors():
            

            
            
            
            

            feature_dataset = dataset.map(lambda x: x[name])
            preprocessor = self.preprocessors[name]
            
            
            for x in feature_dataset.take(1):
                pass
            if len(x.shape) == 0:
                
                feature_dataset = feature_dataset.batch(32)
            if len(x.shape) in {0, 1}:
                
                
                
                feature_dataset = feature_dataset.map(
                    lambda x: tf.expand_dims(x, -1)
                )
            preprocessor.adapt(feature_dataset)
        self._is_adapted = True
        self.get_encoded_features()  
        self.built = True
        self._sublayers_built = True

    def get_inputs(self):
        self._check_if_built()
        return self.inputs

    def get_encoded_features(self):
        self._check_if_adapted()

        if self.encoded_features is None:
            preprocessed_features = self._preprocess_features(self.inputs)
            crossed_features = self._cross_features(preprocessed_features)
            merged_features = self._merge_features(
                preprocessed_features, crossed_features
            )
            self.encoded_features = merged_features
        return self.encoded_features

    def _preprocess_features(self, features):
        return {
            name: self.preprocessors[name](features[name])
            for name in features.keys()
        }

    def _cross_features(self, features):
        all_outputs = {}
        for cross in self.crosses:
            inputs = [features[name] for name in cross.feature_names]
            outputs = self.crossers[cross.name](inputs)
            all_outputs[cross.name] = outputs
        return all_outputs

    def _merge_features(self, preprocessed_features, crossed_features):
        if not self._preprocessed_features_names:
            self._preprocessed_features_names = sorted(
                preprocessed_features.keys()
            )
            self._crossed_features_names = sorted(crossed_features.keys())

        all_names = (
            self._preprocessed_features_names + self._crossed_features_names
        )
        all_features = [
            preprocessed_features[name]
            for name in self._preprocessed_features_names
        ] + [crossed_features[name] for name in self._crossed_features_names]

        if self.output_mode == "dict":
            output_dict = {}
        else:
            features_to_concat = []

        if self._sublayers_built:
            
            for name, feature in zip(all_names, all_features):
                encoder = self.one_hot_encoders.get(name, None)
                if encoder:
                    feature = encoder(feature)
                if self.output_mode == "dict":
                    output_dict[name] = feature
                else:
                    features_to_concat.append(feature)
            if self.output_mode == "dict":
                return output_dict
            else:
                return self.concat(features_to_concat)

        
        
        all_specs = [
            self.features[name] for name in self._preprocessed_features_names
        ] + [
            self.crosses_by_name[name] for name in self._crossed_features_names
        ]

        for name, feature, spec in zip(all_names, all_features, all_specs):
            if tree.is_nested(feature):
                dtype = tree.flatten(feature)[0].dtype
            else:
                dtype = feature.dtype
            dtype = backend.standardize_dtype(dtype)

            if spec.output_mode == "one_hot":
                preprocessor = self.preprocessors.get(
                    name
                ) or self.crossers.get(name)

                cardinality = None
                if not dtype.startswith("int"):
                    raise ValueError(
                        f"Feature '{name}' has `output_mode='one_hot'`. "
                        "Thus its preprocessor should return an integer dtype. "
                        f"Instead it returns a {dtype} dtype."
                    )

                if isinstance(
                    preprocessor, (layers.IntegerLookup, layers.StringLookup)
                ):
                    cardinality = preprocessor.vocabulary_size()
                elif isinstance(preprocessor, layers.CategoryEncoding):
                    cardinality = preprocessor.num_tokens
                elif isinstance(preprocessor, layers.Discretization):
                    cardinality = preprocessor.num_bins
                elif isinstance(
                    preprocessor, (layers.HashedCrossing, layers.Hashing)
                ):
                    cardinality = preprocessor.num_bins
                else:
                    raise ValueError(
                        f"Feature '{name}' has `output_mode='one_hot'`. "
                        "However it isn't a standard feature and the "
                        "dimensionality of its output space is not known, "
                        "thus it cannot be one-hot encoded. "
                        "Try using `output_mode='int'`."
                    )
                if cardinality is not None:
                    encoder = layers.CategoryEncoding(
                        num_tokens=cardinality, output_mode="multi_hot"
                    )
                    self.one_hot_encoders[name] = encoder
                    feature = encoder(feature)

            if self.output_mode == "concat":
                dtype = feature.dtype
                if dtype.startswith("int") or dtype == "string":
                    raise ValueError(
                        f"Cannot concatenate features because feature '{name}' "
                        f"has not been encoded (it has dtype {dtype}). "
                        "Consider using `output_mode='dict'`."
                    )
                features_to_concat.append(feature)
            else:
                output_dict[name] = feature

        if self.output_mode == "concat":
            self.concat = layers.Concatenate(axis=-1)
            return self.concat(features_to_concat)
        else:
            return output_dict

    def _check_if_adapted(self):
        if not self._is_adapted:
            if not self._list_adaptable_preprocessors():
                self._is_adapted = True
            else:
                raise ValueError(
                    "You need to call `.adapt(dataset)` on the FeatureSpace "
                    "before you can start using it."
                )

    def _check_if_built(self):
        if not self._sublayers_built:
            self._check_if_adapted()
            
            self.get_encoded_features()
            self._sublayers_built = True

    def _convert_input(self, x):
        if not isinstance(x, (tf.Tensor, tf.SparseTensor, tf.RaggedTensor)):
            if not isinstance(x, (list, tuple, int, float)):
                x = backend.convert_to_numpy(x)
            x = tf.convert_to_tensor(x)
        return x

    def __call__(self, data):
        self._check_if_built()
        if not isinstance(data, dict):
            raise ValueError(
                "A FeatureSpace can only be called with a dict. "
                f"Received: data={data} (of type {type(data)}"
            )

        
        
        data = {key: self._convert_input(value) for key, value in data.items()}
        rebatched = False
        for name, x in data.items():
            if len(x.shape) == 0:
                data[name] = tf.reshape(x, (1, 1))
                rebatched = True
            elif len(x.shape) == 1:
                data[name] = tf.expand_dims(x, -1)

        with backend_utils.TFGraphScope():
            
            
            
            preprocessed_data = self._preprocess_features(data)
            preprocessed_data = tree.map_structure(
                lambda x: self._convert_input(x), preprocessed_data
            )

            crossed_data = self._cross_features(preprocessed_data)
            crossed_data = tree.map_structure(
                lambda x: self._convert_input(x), crossed_data
            )

            merged_data = self._merge_features(preprocessed_data, crossed_data)

        if rebatched:
            if self.output_mode == "concat":
                assert merged_data.shape[0] == 1
                if backend.backend() != "tensorflow":
                    merged_data = backend.convert_to_numpy(merged_data)
                merged_data = tf.squeeze(merged_data, axis=0)
            else:
                for name, x in merged_data.items():
                    if len(x.shape) == 2 and x.shape[0] == 1:
                        merged_data[name] = tf.squeeze(x, axis=0)

        if (
            backend.backend() != "tensorflow"
            and not backend_utils.in_tf_graph()
        ):
            merged_data = tree.map_structure(
                lambda x: backend.convert_to_tensor(x, dtype=x.dtype),
                merged_data,
            )
        return merged_data

    def get_config(self):
        return {
            "features": serialization_lib.serialize_keras_object(self.features),
            "output_mode": self.output_mode,
            "crosses": serialization_lib.serialize_keras_object(self.crosses),
            "crossing_dim": self.crossing_dim,
            "hashing_dim": self.hashing_dim,
            "num_discretization_bins": self.num_discretization_bins,
        }

    @classmethod
    def from_config(cls, config):
        return cls(**config)

    def get_build_config(self):
        return {
            name: feature.preprocessor.get_build_config()
            for name, feature in self.features.items()
        }

    def build_from_config(self, config):
        for name in config.keys():
            preprocessor = self.features[name].preprocessor
            if not preprocessor.built:
                preprocessor.build_from_config(config[name])
        self._is_adapted = True

    def save(self, filepath):
        
        saving_lib.save_model(self, filepath)

    def save_own_variables(self, store):
        return

    def load_own_variables(self, store):
        return