Skip to content

Keras Models

physXAI.models.ann.keras_models.keras_models

Classes

NonNegPartial

Bases: Constraint

A Keras constraint that enforces non-negativity or non-positivity on specific parts of a weight tensor. This is useful for imposing monotonicity constraints on a neural network layer. For example, if a feature should have a non-decreasing relationship with the output, the corresponding weight can be constrained to be non-negative.

Source code in physXAI/models/ann/keras_models/keras_models.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@keras.saving.register_keras_serializable(package='custom_constraint', name='NonNegPartial')
class NonNegPartial(keras.constraints.Constraint):
    """
    A Keras constraint that enforces non-negativity or non-positivity on specific parts of a weight tensor.
    This is useful for imposing monotonicity constraints on a neural network layer.
    For example, if a feature should have a non-decreasing relationship with the output,
    the corresponding weight can be constrained to be non-negative.
    """

    def __init__(self, monotonicities: list[int]):
        """
        Initializes the NonNegPartial constraint.

        Args:
            monotonicities (list[int]): A list of integers specifying the monotonicity for each
                                        corresponding weight (or part of the weight tensor).
                                        -  1: Enforces non-negativity (weight >= 0).
                                        - -1: Enforces non-positivity (weight <= 0).
                                        -  0: No constraint is applied.
        Raises:
            ValueError: If any item in `monotonicities` is not -1, 0, or 1.
        """

        allowed_items = [-1, 0, 1]
        if not all(item in allowed_items for item in monotonicities):
            raise ValueError('Monotonicities must be in [-1, 0, 1]')
        self.monotonicities: list[int] = monotonicities

    def __call__(self, w):
        """
         Applies the constraint to the weight tensor.
         This method is called by Keras during the training process after each weight update.

         Args:
             w: The weight tensor to be constrained.

         Returns:
             The constrained weight tensor.

         Raises:
             ValueError: If the length of `monotonicities` does not match the first dimension
                         of the weight tensor `w`.
         """

        w = keras.ops.convert_to_tensor(w)

        if len(self.monotonicities) != w.shape[0]:
            raise ValueError('Length of monotonicities list must be equal'
                             ' to the first element of the weight tensor´s shape.')

        w_split = keras.ops.split(w, w.shape[0])
        for i in range(0, w.shape[0]):
            # non-negativity
            if self.monotonicities[i] == 1:
                w_split[i] = w_split[i] * keras.ops.cast(keras.ops.greater_equal(w_split[i], 0.),
                                                         dtype=w_split[i].dtype)
            # non - positivity
            elif self.monotonicities[i] == -1:
                w_split[i] = w_split[i] * keras.ops.cast(keras.ops.greater_equal(-w_split[i], 0.),
                                                         dtype=w_split[i].dtype)
            else:
                continue

        return keras.ops.concatenate(w_split)

    def get_config(self):
        return {'monotonicities': self.monotonicities}
Attributes
monotonicities: list[int] = monotonicities instance-attribute
Functions
__init__(monotonicities: list[int])

Initializes the NonNegPartial constraint.

Parameters:

Name Type Description Default
monotonicities list[int]

A list of integers specifying the monotonicity for each corresponding weight (or part of the weight tensor). - 1: Enforces non-negativity (weight >= 0). - -1: Enforces non-positivity (weight <= 0). - 0: No constraint is applied.

required

Raises: ValueError: If any item in monotonicities is not -1, 0, or 1.

Source code in physXAI/models/ann/keras_models/keras_models.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def __init__(self, monotonicities: list[int]):
    """
    Initializes the NonNegPartial constraint.

    Args:
        monotonicities (list[int]): A list of integers specifying the monotonicity for each
                                    corresponding weight (or part of the weight tensor).
                                    -  1: Enforces non-negativity (weight >= 0).
                                    - -1: Enforces non-positivity (weight <= 0).
                                    -  0: No constraint is applied.
    Raises:
        ValueError: If any item in `monotonicities` is not -1, 0, or 1.
    """

    allowed_items = [-1, 0, 1]
    if not all(item in allowed_items for item in monotonicities):
        raise ValueError('Monotonicities must be in [-1, 0, 1]')
    self.monotonicities: list[int] = monotonicities
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
74
75
def get_config(self):
    return {'monotonicities': self.monotonicities}

ConcaveActivation

A Keras activation function wrapper that transforms a given activation function into its concave counterpart. If f(x) is the original activation, the concave version is -f(-x).

Source code in physXAI/models/ann/keras_models/keras_models.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@keras.saving.register_keras_serializable(package='custom_activation', name='ConcaveActivation')
class ConcaveActivation:
    """
    A Keras activation function wrapper that transforms a given activation function
    into its concave counterpart.
    If f(x) is the original activation, the concave version is -f(-x).
    """

    def __init__(self, activation: str):
        """
        Initializes the ConcaveActivation.

        Args:
            activation (str): The name of the Keras activation function to be made concave
                              (e.g., 'relu', 'sigmoid').
        """

        self.activation = activation
        self.activation_fcn = keras.activations.get(activation)

    def __call__(self, x):
        """
        Applies the concave transformation to the input tensor.

        Args:
            x: The input tensor.

        Returns:
            The tensor after applying the concave activation: -activation_fcn(-x).
        """

        return -self.activation_fcn(-x)

    def get_config(self):
        return {'activation': self.activation}

    @classmethod
    def from_config(cls, config):
        return cls(**config)
Attributes
activation = activation instance-attribute
activation_fcn = keras.activations.get(activation) instance-attribute
Functions
__init__(activation: str)

Initializes the ConcaveActivation.

Parameters:

Name Type Description Default
activation str

The name of the Keras activation function to be made concave (e.g., 'relu', 'sigmoid').

required
Source code in physXAI/models/ann/keras_models/keras_models.py
86
87
88
89
90
91
92
93
94
95
96
def __init__(self, activation: str):
    """
    Initializes the ConcaveActivation.

    Args:
        activation (str): The name of the Keras activation function to be made concave
                          (e.g., 'relu', 'sigmoid').
    """

    self.activation = activation
    self.activation_fcn = keras.activations.get(activation)
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
111
112
def get_config(self):
    return {'activation': self.activation}
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
114
115
116
@classmethod
def from_config(cls, config):
    return cls(**config)

SaturatedActivation

A Keras activation function that creates a saturated version of a given base activation. The saturation behavior is different for x <= 0 and x > 0. - For x <= 0: f(x + 1) - f(1) (where f is the base activation) - For x > 0: g(x - 1) + f(1) (where g is the concave version of f, and f(1) is a constant) This can be used to create activation functions that plateau or saturate at certain input ranges.

Source code in physXAI/models/ann/keras_models/keras_models.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
@keras.saving.register_keras_serializable(package='custom_activation', name='SaturatedActivation')
class SaturatedActivation:
    """
    A Keras activation function that creates a saturated version of a given base activation.
    The saturation behavior is different for x <= 0 and x > 0.
    - For x <= 0:  f(x + 1) - f(1)  (where f is the base activation)
    - For x > 0:  g(x - 1) + f(1)  (where g is the concave version of f, and f(1) is a constant)
    This can be used to create activation functions that plateau or saturate at certain input ranges.
    """

    def __init__(self, activation: str):
        """
        Initializes the SaturatedActivation.

        Args:
            activation (str): The name of the Keras activation function to be used as the base.
        """

        self.activation = activation
        self.activation_fcn = keras.activations.get(activation)
        self.activation_fcn_concave = ConcaveActivation(activation)

    def __call__(self, x):
        """
        Applies the saturated activation to the input tensor.

        Args:
            x: The input tensor.

        Returns:
            The tensor after applying the saturated activation.
        """

        cc = self.activation_fcn(keras.ops.ones_like(x))
        return keras.ops.where(
            x <= 0,
            self.activation_fcn(x + 1) - cc,
            self.activation_fcn_concave(x - 1) + cc,
        )

    def get_config(self):
        return {'activation': self.activation}

    @classmethod
    def from_config(cls, config):
        return cls(**config)
Attributes
activation = activation instance-attribute
activation_fcn = keras.activations.get(activation) instance-attribute
activation_fcn_concave = ConcaveActivation(activation) instance-attribute
Functions
__init__(activation: str)

Initializes the SaturatedActivation.

Parameters:

Name Type Description Default
activation str

The name of the Keras activation function to be used as the base.

required
Source code in physXAI/models/ann/keras_models/keras_models.py
129
130
131
132
133
134
135
136
137
138
139
def __init__(self, activation: str):
    """
    Initializes the SaturatedActivation.

    Args:
        activation (str): The name of the Keras activation function to be used as the base.
    """

    self.activation = activation
    self.activation_fcn = keras.activations.get(activation)
    self.activation_fcn_concave = ConcaveActivation(activation)
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
159
160
def get_config(self):
    return {'activation': self.activation}
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
162
163
164
@classmethod
def from_config(cls, config):
    return cls(**config)

LimitedActivation

A Keras activation function that clips the input tensor to a specified minimum and/or maximum value.

Source code in physXAI/models/ann/keras_models/keras_models.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
@keras.saving.register_keras_serializable(package='custom_activation', name='LimitedActivation')
class LimitedActivation:
    """
    A Keras activation function that clips the input tensor to a specified minimum and/or maximum value.
    """

    def __init__(self, max_value: float = None, min_value: float = None):
        """
        Initializes the LimitedActivation.

        Args:
            max_value (float, optional): The maximum value to clip to. If None, no upper limit is applied.
                                         Defaults to None.
            min_value (float, optional): The minimum value to clip to. If None, no lower limit is applied.
                                         Defaults to None.
        """

        self.max_value = max_value
        self.min_value = min_value

    def __call__(self, x):
        """
        Applies the clipping to the input tensor.

        Args:
            x: The input tensor.

        Returns:
            The clipped tensor.
        """

        if self.min_value is not None:
            x = keras.ops.maximum(x, self.min_value)
        if self.max_value is not None:
            x = keras.ops.minimum(x, self.max_value)
        return x

    def get_config(self):
        return {'max_value': self.max_value, 'min_value': self.min_value}

    @classmethod
    def from_config(cls, config):
        return cls(**config)
Attributes
max_value = max_value instance-attribute
min_value = min_value instance-attribute
Functions
__init__(max_value: float = None, min_value: float = None)

Initializes the LimitedActivation.

Parameters:

Name Type Description Default
max_value float

The maximum value to clip to. If None, no upper limit is applied. Defaults to None.

None
min_value float

The minimum value to clip to. If None, no lower limit is applied. Defaults to None.

None
Source code in physXAI/models/ann/keras_models/keras_models.py
173
174
175
176
177
178
179
180
181
182
183
184
185
def __init__(self, max_value: float = None, min_value: float = None):
    """
    Initializes the LimitedActivation.

    Args:
        max_value (float, optional): The maximum value to clip to. If None, no upper limit is applied.
                                     Defaults to None.
        min_value (float, optional): The minimum value to clip to. If None, no lower limit is applied.
                                     Defaults to None.
    """

    self.max_value = max_value
    self.min_value = min_value
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
204
205
def get_config(self):
    return {'max_value': self.max_value, 'min_value': self.min_value}
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
207
208
209
@classmethod
def from_config(cls, config):
    return cls(**config)

RBFLayer

Bases: Layer

Custom Radial Basis Function (RBF) Layer.

This layer implements RBF neurons, where the activation is typically a Gaussian function of the Euclidean distance between the input and the neuron's center.

Parameters:

Name Type Description Default
units int

Positive integer, dimensionality of the output space (number of RBF neurons).

required
gamma float or list / array

The gamma parameter of the Gaussian function, controlling the width. Can be a scalar (same gamma for all neurons) or a tensor/array of length units (individual gamma per neuron).

1.0
initial_centers ndarray

A NumPy array of shape (units, input_dim) for the initial centers. If None, they are initialized using a default initializer (RandomUniform).

None
learnable_centers bool

Whether the centers should be trainable. Defaults to True.

True
learnable_gamma bool

Whether gamma should be trainable. Defaults to True.

True
Input shape

2D tensor with shape (batch_size, input_dim).

Output shape

2D tensor with shape (batch_size, units).

Source code in physXAI/models/ann/keras_models/keras_models.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
@keras.saving.register_keras_serializable(package='custom_layer', name='RBFLayer')
class RBFLayer(keras.Layer):
    """
        Custom Radial Basis Function (RBF) Layer.

        This layer implements RBF neurons, where the activation is typically a Gaussian function
        of the Euclidean distance between the input and the neuron's center.

        Arguments:
            units (int): Positive integer, dimensionality of the output space (number of RBF neurons).
            gamma (float or list/array): The gamma parameter of the Gaussian function, controlling the width.
                                         Can be a scalar (same gamma for all neurons) or a tensor/array
                                         of length `units` (individual gamma per neuron).
            initial_centers (np.ndarray, optional): A NumPy array of shape (units, input_dim)
                                                     for the initial centers. If None, they are
                                                     initialized using a default initializer (RandomUniform).
            learnable_centers (bool): Whether the centers should be trainable. Defaults to True.
            learnable_gamma (bool): Whether gamma should be trainable. Defaults to True.

        Input shape:
            2D tensor with shape `(batch_size, input_dim)`.

        Output shape:
            2D tensor with shape `(batch_size, units)`.
    """

    def __init__(self, units, gamma=1.0, initial_centers=None,
                 learnable_centers=True, learnable_gamma=True, **kwargs):
        """
        Initializes the RBFLayer.

        Args:
            units (int): Number of RBF neurons.
            gamma (float or list/np.ndarray): Initial value(s) for the gamma parameter.
            initial_centers (np.ndarray, optional): Initial positions for the RBF centers.
            learnable_centers (bool): If True, centers will be updated during training.
            learnable_gamma (bool): If True, gamma values will be updated during training.
        """

        super().__init__(**kwargs)
        self.units = units
        self.gamma_init_value = gamma
        self.initial_centers = initial_centers
        self.learnable_centers = learnable_centers
        self.learnable_gamma = learnable_gamma

        self.input_dim = None
        self.centers = None
        self.log_gamma = None

    def build(self, input_shape):
        """
        Creates the layer's weights (centers and gamma).
        This method is called the first time the layer is used, with the shape of the input.

        Args:
            input_shape (tuple): Shape of the input tensor.
        """

        # Extract the input feature dimension
        self.input_dim = input_shape[-1]

        # Initialize RBF centers
        if self.initial_centers is not None:
            # Validate the shape of provided initial centers
            if self.initial_centers.shape != (self.units, self.input_dim):
                raise ValueError(
                    f"Shape of initial_centers {self.initial_centers.shape} "
                    f"does not match expected shape ({self.units}, {self.input_dim})"
                )
            centers_initializer = keras.initializers.Constant(self.initial_centers)
        else:
            # Default initializer for centers if none are provided (RandomUniform)
            centers_initializer = keras.initializers.RandomUniform(minval=0., maxval=1.)

        # Add centers as a trainable weight to the layer
        self.centers = self.add_weight(
            name='centers',
            shape=(self.units, self.input_dim),
            initializer=centers_initializer,
            trainable=self.learnable_centers
        )

        # Initialize gamma parameters (width of the Gaussian function)
        # We store and train log_gamma to ensure gamma = exp(log_gamma) remains positive.
        if isinstance(self.gamma_init_value, (list, np.ndarray)):
            # If gamma is provided as a list or array, it's for individual neurons
            if len(self.gamma_init_value) != self.units:
                raise ValueError("If gamma is a list/array, its length must be equal to units.")
            # Convert initial gamma values to log_gamma
            initial_log_gamma = np.log(self.gamma_init_value).astype(np.float32)
        else:
            # If gamma is a scalar, use the same value for all neurons
            # Convert initial gamma values to log_gamma
            initial_log_gamma = np.full(self.units, np.log(self.gamma_init_value), dtype=np.float32)

        # Add log_gamma as a trainable weight
        self.log_gamma = self.add_weight(
            name='log_gamma',
            shape=(self.units,),
            initializer=keras.initializers.Constant(initial_log_gamma),
            trainable=self.learnable_gamma
        )
        super().build(input_shape)

    def call(self, inputs):
        """
        Defines the forward pass of the RBF layer.

        Args:
            inputs (tf.Tensor): Input tensor of shape (batch_size, input_dim).

        Returns:
            tf.Tensor: Output tensor of shape (batch_size, units), representing
                       the activation of each RBF neuron for each input sample.
        """
        # inputs shape: (batch_size, input_dim)
        # centers shape: (units, input_dim)
        # Goal: Calculate ||inputs_batch_item - center_unit||^2 for all combinations

        # Expand dimensions of inputs and centers to enable broadcasting for distance calculation
        # inputs_expanded shape: (batch_size, 1, input_dim)
        inputs_expanded = keras.ops.expand_dims(inputs, axis=1)
        # centers_expanded shape: (1, units, input_dim)
        centers_expanded = keras.ops.expand_dims(self.centers, axis=0)

        # Calculate squared Euclidean distances between each input sample and each RBF center
        # (inputs_expanded - centers_expanded) results in shape (batch_size, units, input_dim)
        # Then, sum the squares along the input_dim axis (axis=2)
        distances_sq = keras.ops.sum(
            keras.ops.square(inputs_expanded - centers_expanded), axis=2
        )   # Resulting shape: (batch_size, units)

        # Apply the Gaussian RBF activation function: exp(-gamma * ||x - c||^2)
        # Retrieve gamma from log_gamma (shape: (units,))
        gamma = keras.ops.exp(self.log_gamma)
        # Broadcasting will apply each gamma to its respective column in distances_sq
        # Output shape: (batch_size, units)
        phi = keras.ops.exp(-gamma * distances_sq)
        return phi

    def compute_output_shape(self, input_shape):
        """
        Computes the output shape of the layer.

        Args:
            input_shape (tuple): Shape of the input tensor.

        Returns:
            tuple: Shape of the output tensor (batch_size, units).
        """

        return input_shape[0], self.units

    def get_config(self):
        config = super().get_config()
        config.update({
            "units": self.units,
            # Store the original gamma value(s), not log_gamma, for easier interpretatio
            "gamma": np.exp(self.log_gamma.numpy()).tolist() if self.log_gamma is not None else self.gamma_init_value,
            # Convert initial centers to list
            "initial_centers": self.initial_centers.tolist() if isinstance(self.initial_centers, np.ndarray) else None,
            "learnable_centers": self.learnable_centers,
            "learnable_gamma": self.learnable_gamma
        })
        return config

    @classmethod
    def from_config(cls, config):
        # Retrieve 'initial_centers' from config and convert back to NumPy array if it was stored as a list
        initial_centers_list = config.pop("initial_centers", None)
        if initial_centers_list is not None:
            config["initial_centers"] = np.array(initial_centers_list)
        return cls(**config)
Attributes
units = units instance-attribute
gamma_init_value = gamma instance-attribute
initial_centers = initial_centers instance-attribute
learnable_centers = learnable_centers instance-attribute
learnable_gamma = learnable_gamma instance-attribute
input_dim = None instance-attribute
centers = None instance-attribute
log_gamma = None instance-attribute
Functions
__init__(units, gamma=1.0, initial_centers=None, learnable_centers=True, learnable_gamma=True, **kwargs)

Initializes the RBFLayer.

Parameters:

Name Type Description Default
units int

Number of RBF neurons.

required
gamma float or list / ndarray

Initial value(s) for the gamma parameter.

1.0
initial_centers ndarray

Initial positions for the RBF centers.

None
learnable_centers bool

If True, centers will be updated during training.

True
learnable_gamma bool

If True, gamma values will be updated during training.

True
Source code in physXAI/models/ann/keras_models/keras_models.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def __init__(self, units, gamma=1.0, initial_centers=None,
             learnable_centers=True, learnable_gamma=True, **kwargs):
    """
    Initializes the RBFLayer.

    Args:
        units (int): Number of RBF neurons.
        gamma (float or list/np.ndarray): Initial value(s) for the gamma parameter.
        initial_centers (np.ndarray, optional): Initial positions for the RBF centers.
        learnable_centers (bool): If True, centers will be updated during training.
        learnable_gamma (bool): If True, gamma values will be updated during training.
    """

    super().__init__(**kwargs)
    self.units = units
    self.gamma_init_value = gamma
    self.initial_centers = initial_centers
    self.learnable_centers = learnable_centers
    self.learnable_gamma = learnable_gamma

    self.input_dim = None
    self.centers = None
    self.log_gamma = None
build(input_shape)

Creates the layer's weights (centers and gamma). This method is called the first time the layer is used, with the shape of the input.

Parameters:

Name Type Description Default
input_shape tuple

Shape of the input tensor.

required
Source code in physXAI/models/ann/keras_models/keras_models.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def build(self, input_shape):
    """
    Creates the layer's weights (centers and gamma).
    This method is called the first time the layer is used, with the shape of the input.

    Args:
        input_shape (tuple): Shape of the input tensor.
    """

    # Extract the input feature dimension
    self.input_dim = input_shape[-1]

    # Initialize RBF centers
    if self.initial_centers is not None:
        # Validate the shape of provided initial centers
        if self.initial_centers.shape != (self.units, self.input_dim):
            raise ValueError(
                f"Shape of initial_centers {self.initial_centers.shape} "
                f"does not match expected shape ({self.units}, {self.input_dim})"
            )
        centers_initializer = keras.initializers.Constant(self.initial_centers)
    else:
        # Default initializer for centers if none are provided (RandomUniform)
        centers_initializer = keras.initializers.RandomUniform(minval=0., maxval=1.)

    # Add centers as a trainable weight to the layer
    self.centers = self.add_weight(
        name='centers',
        shape=(self.units, self.input_dim),
        initializer=centers_initializer,
        trainable=self.learnable_centers
    )

    # Initialize gamma parameters (width of the Gaussian function)
    # We store and train log_gamma to ensure gamma = exp(log_gamma) remains positive.
    if isinstance(self.gamma_init_value, (list, np.ndarray)):
        # If gamma is provided as a list or array, it's for individual neurons
        if len(self.gamma_init_value) != self.units:
            raise ValueError("If gamma is a list/array, its length must be equal to units.")
        # Convert initial gamma values to log_gamma
        initial_log_gamma = np.log(self.gamma_init_value).astype(np.float32)
    else:
        # If gamma is a scalar, use the same value for all neurons
        # Convert initial gamma values to log_gamma
        initial_log_gamma = np.full(self.units, np.log(self.gamma_init_value), dtype=np.float32)

    # Add log_gamma as a trainable weight
    self.log_gamma = self.add_weight(
        name='log_gamma',
        shape=(self.units,),
        initializer=keras.initializers.Constant(initial_log_gamma),
        trainable=self.learnable_gamma
    )
    super().build(input_shape)
call(inputs)

Defines the forward pass of the RBF layer.

Parameters:

Name Type Description Default
inputs Tensor

Input tensor of shape (batch_size, input_dim).

required

Returns:

Type Description

tf.Tensor: Output tensor of shape (batch_size, units), representing the activation of each RBF neuron for each input sample.

Source code in physXAI/models/ann/keras_models/keras_models.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
def call(self, inputs):
    """
    Defines the forward pass of the RBF layer.

    Args:
        inputs (tf.Tensor): Input tensor of shape (batch_size, input_dim).

    Returns:
        tf.Tensor: Output tensor of shape (batch_size, units), representing
                   the activation of each RBF neuron for each input sample.
    """
    # inputs shape: (batch_size, input_dim)
    # centers shape: (units, input_dim)
    # Goal: Calculate ||inputs_batch_item - center_unit||^2 for all combinations

    # Expand dimensions of inputs and centers to enable broadcasting for distance calculation
    # inputs_expanded shape: (batch_size, 1, input_dim)
    inputs_expanded = keras.ops.expand_dims(inputs, axis=1)
    # centers_expanded shape: (1, units, input_dim)
    centers_expanded = keras.ops.expand_dims(self.centers, axis=0)

    # Calculate squared Euclidean distances between each input sample and each RBF center
    # (inputs_expanded - centers_expanded) results in shape (batch_size, units, input_dim)
    # Then, sum the squares along the input_dim axis (axis=2)
    distances_sq = keras.ops.sum(
        keras.ops.square(inputs_expanded - centers_expanded), axis=2
    )   # Resulting shape: (batch_size, units)

    # Apply the Gaussian RBF activation function: exp(-gamma * ||x - c||^2)
    # Retrieve gamma from log_gamma (shape: (units,))
    gamma = keras.ops.exp(self.log_gamma)
    # Broadcasting will apply each gamma to its respective column in distances_sq
    # Output shape: (batch_size, units)
    phi = keras.ops.exp(-gamma * distances_sq)
    return phi
compute_output_shape(input_shape)

Computes the output shape of the layer.

Parameters:

Name Type Description Default
input_shape tuple

Shape of the input tensor.

required

Returns:

Name Type Description
tuple

Shape of the output tensor (batch_size, units).

Source code in physXAI/models/ann/keras_models/keras_models.py
353
354
355
356
357
358
359
360
361
362
363
364
def compute_output_shape(self, input_shape):
    """
    Computes the output shape of the layer.

    Args:
        input_shape (tuple): Shape of the input tensor.

    Returns:
        tuple: Shape of the output tensor (batch_size, units).
    """

    return input_shape[0], self.units
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
366
367
368
369
370
371
372
373
374
375
376
377
def get_config(self):
    config = super().get_config()
    config.update({
        "units": self.units,
        # Store the original gamma value(s), not log_gamma, for easier interpretatio
        "gamma": np.exp(self.log_gamma.numpy()).tolist() if self.log_gamma is not None else self.gamma_init_value,
        # Convert initial centers to list
        "initial_centers": self.initial_centers.tolist() if isinstance(self.initial_centers, np.ndarray) else None,
        "learnable_centers": self.learnable_centers,
        "learnable_gamma": self.learnable_gamma
    })
    return config
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
379
380
381
382
383
384
385
@classmethod
def from_config(cls, config):
    # Retrieve 'initial_centers' from config and convert back to NumPy array if it was stored as a list
    initial_centers_list = config.pop("initial_centers", None)
    if initial_centers_list is not None:
        config["initial_centers"] = np.array(initial_centers_list)
    return cls(**config)

InputSliceLayer

Bases: Layer

A simple layer to select specific features from the last axis.

Source code in physXAI/models/ann/keras_models/keras_models.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
@keras.saving.register_keras_serializable(package='custom_layer', name='InputSliceLayer')
class InputSliceLayer(keras.Layer):
    """
    A simple layer to select specific features from the last axis.
    """

    def __init__(self, feature_indices: Union[int, list[int]], **kwargs):
        """
        Initializes the layer.

        Args:
            feature_indices (int or list): The index or indices to select.
                - If int (e.g., 1), selects the feature at that index and
                  reduces the rank.
                - If list (e.g., [1]), selects the feature(s) and
                  keeps the rank.
        """
        super().__init__(**kwargs)
        self.feature_indices = feature_indices

    def call(self, inputs):
        return keras.ops.take(inputs, self.feature_indices, axis=-1)

    def get_config(self):
        config = super().get_config()
        config.update({
            "feature_indices": self.feature_indices
        })
        return config

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

    def compute_output_shape(self, input_shape):
        output_shape = list(input_shape)

        if isinstance(self.feature_indices, int):
            output_shape.pop(-1)

        elif isinstance(self.feature_indices, (list, tuple)):
            output_shape[-1] = len(self.feature_indices)

        return tuple(output_shape)
Attributes
feature_indices = feature_indices instance-attribute
Functions
__init__(feature_indices: Union[int, list[int]], **kwargs)

Initializes the layer.

Parameters:

Name Type Description Default
feature_indices int or list

The index or indices to select. - If int (e.g., 1), selects the feature at that index and reduces the rank. - If list (e.g., [1]), selects the feature(s) and keeps the rank.

required
Source code in physXAI/models/ann/keras_models/keras_models.py
394
395
396
397
398
399
400
401
402
403
404
405
406
def __init__(self, feature_indices: Union[int, list[int]], **kwargs):
    """
    Initializes the layer.

    Args:
        feature_indices (int or list): The index or indices to select.
            - If int (e.g., 1), selects the feature at that index and
              reduces the rank.
            - If list (e.g., [1]), selects the feature(s) and
              keeps the rank.
    """
    super().__init__(**kwargs)
    self.feature_indices = feature_indices
call(inputs)
Source code in physXAI/models/ann/keras_models/keras_models.py
408
409
def call(self, inputs):
    return keras.ops.take(inputs, self.feature_indices, axis=-1)
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
411
412
413
414
415
416
def get_config(self):
    config = super().get_config()
    config.update({
        "feature_indices": self.feature_indices
    })
    return config
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
418
419
420
@classmethod
def from_config(cls, config):
    return cls(**config)
compute_output_shape(input_shape)
Source code in physXAI/models/ann/keras_models/keras_models.py
422
423
424
425
426
427
428
429
430
431
def compute_output_shape(self, input_shape):
    output_shape = list(input_shape)

    if isinstance(self.feature_indices, int):
        output_shape.pop(-1)

    elif isinstance(self.feature_indices, (list, tuple)):
        output_shape[-1] = len(self.feature_indices)

    return tuple(output_shape)

ConstantLayer

Bases: Layer

A layer that returns a constant tensor, broadcasted to the batch size.

This layer ignores its input and simply returns a tensor of a pre-defined shape, initialized with a constant value.

The constant is created as a Keras weight, which can be trainable or non-trainable.

Source code in physXAI/models/ann/keras_models/keras_models.py
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
@keras.saving.register_keras_serializable(package='custom_layer', name='ConstantLayer')
class ConstantLayer(keras.Layer):
    """
    A layer that returns a constant tensor, broadcasted to the batch size.

    This layer ignores its input and simply returns a tensor of a
    pre-defined shape, initialized with a constant value.

    The constant is created as a Keras weight, which can be
    trainable or non-trainable.
    """

    def __init__(self, value=0.0, shape=(1,), trainable=False, weight_name: str = None, **kwargs):
        """
        Initializes the layer.

        Args:
            initial_value (float): The value to initialize the constant tensor with.
            shape (tuple): The shape of the constant, *excluding* the batch
                dimension. For a single number to be added, use (1,).
            trainable (bool): Whether this constant is a learnable parameter.
        """
        super().__init__(trainable=trainable, **kwargs)
        self.value = value
        self.target_shape = tuple(shape)
        self.weight_name = weight_name

    def build(self, input_shape):
        if self.value is not None:
            init = keras.initializers.Constant(self.value)
        else:
            init = keras.initializers.glorot_uniform()
        self.constant = self.add_weight(
            shape=self.target_shape,
            initializer=init,
            trainable=self.trainable,
            name=self.weight_name,
        )

    def call(self, inputs):
        batch_size = keras.ops.shape(inputs)[0]

        # Create the full target shape, including the batch dimension
        # e.g., (batch_size,) + (1,) -> (batch_size, 1)
        full_shape = (batch_size,) + self.target_shape

        return keras.ops.broadcast_to(self.constant, full_shape)

    def compute_output_shape(self, input_shape):
        # The output shape is (batch_size,) + our target_shape
        return (input_shape[0],) + self.target_shape

    def get_config(self):
        config = super().get_config()
        config.update({
            "value": self.value,
            "shape": self.target_shape,
        })
        return config

    @classmethod
    def from_config(cls, config):
        return cls(**config)
Attributes
value = value instance-attribute
target_shape = tuple(shape) instance-attribute
weight_name = weight_name instance-attribute
Functions
__init__(value=0.0, shape=(1,), trainable=False, weight_name: str = None, **kwargs)

Initializes the layer.

Parameters:

Name Type Description Default
initial_value float

The value to initialize the constant tensor with.

required
shape tuple

The shape of the constant, excluding the batch dimension. For a single number to be added, use (1,).

(1,)
trainable bool

Whether this constant is a learnable parameter.

False
Source code in physXAI/models/ann/keras_models/keras_models.py
446
447
448
449
450
451
452
453
454
455
456
457
458
459
def __init__(self, value=0.0, shape=(1,), trainable=False, weight_name: str = None, **kwargs):
    """
    Initializes the layer.

    Args:
        initial_value (float): The value to initialize the constant tensor with.
        shape (tuple): The shape of the constant, *excluding* the batch
            dimension. For a single number to be added, use (1,).
        trainable (bool): Whether this constant is a learnable parameter.
    """
    super().__init__(trainable=trainable, **kwargs)
    self.value = value
    self.target_shape = tuple(shape)
    self.weight_name = weight_name
build(input_shape)
Source code in physXAI/models/ann/keras_models/keras_models.py
461
462
463
464
465
466
467
468
469
470
471
def build(self, input_shape):
    if self.value is not None:
        init = keras.initializers.Constant(self.value)
    else:
        init = keras.initializers.glorot_uniform()
    self.constant = self.add_weight(
        shape=self.target_shape,
        initializer=init,
        trainable=self.trainable,
        name=self.weight_name,
    )
call(inputs)
Source code in physXAI/models/ann/keras_models/keras_models.py
473
474
475
476
477
478
479
480
def call(self, inputs):
    batch_size = keras.ops.shape(inputs)[0]

    # Create the full target shape, including the batch dimension
    # e.g., (batch_size,) + (1,) -> (batch_size, 1)
    full_shape = (batch_size,) + self.target_shape

    return keras.ops.broadcast_to(self.constant, full_shape)
compute_output_shape(input_shape)
Source code in physXAI/models/ann/keras_models/keras_models.py
482
483
484
def compute_output_shape(self, input_shape):
    # The output shape is (batch_size,) + our target_shape
    return (input_shape[0],) + self.target_shape
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
486
487
488
489
490
491
492
def get_config(self):
    config = super().get_config()
    config.update({
        "value": self.value,
        "shape": self.target_shape,
    })
    return config
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
494
495
496
@classmethod
def from_config(cls, config):
    return cls(**config)

DivideLayer

Bases: Layer

A layer that divides two layers.

Source code in physXAI/models/ann/keras_models/keras_models.py
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
@keras.saving.register_keras_serializable(package='custom_layer', name='DivideLayer')
class DivideLayer(keras.Layer):
    """
    A layer that divides two layers.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def call(self, inputs):
        return keras.ops.divide(inputs[0], inputs[1])

    def compute_output_shape(self, input_shape):
        return input_shape[0]

    def get_config(self):
        config = super().get_config()
        return config

    @classmethod
    def from_config(cls, config):
        return cls(**config)
Functions
__init__(**kwargs)
Source code in physXAI/models/ann/keras_models/keras_models.py
505
506
def __init__(self, **kwargs):
    super().__init__(**kwargs)
call(inputs)
Source code in physXAI/models/ann/keras_models/keras_models.py
508
509
def call(self, inputs):
    return keras.ops.divide(inputs[0], inputs[1])
compute_output_shape(input_shape)
Source code in physXAI/models/ann/keras_models/keras_models.py
511
512
def compute_output_shape(self, input_shape):
    return input_shape[0]
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
514
515
516
def get_config(self):
    config = super().get_config()
    return config
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
518
519
520
@classmethod
def from_config(cls, config):
    return cls(**config)

PowerLayer

Bases: Layer

A layer that computes the power of two layers.

Source code in physXAI/models/ann/keras_models/keras_models.py
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
@keras.saving.register_keras_serializable(package='custom_layer', name='PowerLayer')
class PowerLayer(keras.Layer):
    """
    A layer that computes the power of two layers.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def call(self, inputs):
        return keras.ops.power(inputs[0], inputs[1])

    def compute_output_shape(self, input_shape):
        return input_shape[0]

    def get_config(self):
        config = super().get_config()
        return config

    @classmethod
    def from_config(cls, config):
        return cls(**config)
Functions
__init__(**kwargs)
Source code in physXAI/models/ann/keras_models/keras_models.py
529
530
def __init__(self, **kwargs):
    super().__init__(**kwargs)
call(inputs)
Source code in physXAI/models/ann/keras_models/keras_models.py
532
533
def call(self, inputs):
    return keras.ops.power(inputs[0], inputs[1])
compute_output_shape(input_shape)
Source code in physXAI/models/ann/keras_models/keras_models.py
535
536
def compute_output_shape(self, input_shape):
    return input_shape[0]
get_config()
Source code in physXAI/models/ann/keras_models/keras_models.py
538
539
540
def get_config(self):
    config = super().get_config()
    return config
from_config(config) classmethod
Source code in physXAI/models/ann/keras_models/keras_models.py
542
543
544
@classmethod
def from_config(cls, config):
    return cls(**config)