| import tensorflow as tf |
| from tensorflow import keras |
|
|
| class MonotonicFunction(tf.keras.layers.Layer): |
| """ |
| Pure neural network approach for monotonic water retention curve prediction |
| """ |
| def __init__(self, num_basis=16): |
| super(MonotonicFunction, self).__init__() |
| self.num_basis = num_basis |
|
|
| def build(self, input_shape): |
| |
| self.basis_weights = self.add_weight( |
| shape=(self.num_basis,), |
| initializer=tf.keras.initializers.GlorotNormal(), |
| trainable=True, |
| name='basis_weights' |
| ) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| self.output_scale = self.add_weight( |
| shape=(1,), |
| initializer=tf.keras.initializers.Constant(0.90), |
| trainable=True, |
| constraint=None, |
| name='output_scale' |
| ) |
| |
| self.output_shift = self.add_weight( |
| shape=(1,), |
| initializer=tf.keras.initializers.Constant(0.01), |
| trainable=True, |
| constraint=None, |
| name='output_shift' |
| ) |
| |
| super(MonotonicFunction, self).build(input_shape) |
| |
| def call(self, inputs): |
| water_potential, parameters = inputs |
| epsilon = tf.keras.backend.epsilon() |
| |
| |
| water_potential = tf.where(tf.math.is_finite(water_potential), water_potential, tf.zeros_like(water_potential)) |
| parameters = tf.where(tf.math.is_finite(parameters), parameters, tf.zeros_like(parameters)) |
| |
| |
| a_params = parameters[:, :self.num_basis] |
| b_params = parameters[:, self.num_basis:] |
| |
| |
| a_params = tf.math.softplus(a_params) + epsilon |
| |
| |
| basis_values = [] |
| for i in range(self.num_basis): |
| |
| scaled_wp = water_potential |
| |
| |
| logit = -a_params[:, i:i+1] * scaled_wp + b_params[:, i:i+1] |
| g_i = tf.sigmoid(logit) |
| |
| basis_values.append(g_i) |
| |
| basis_values = tf.concat(basis_values, axis=-1) |
| |
| |
| weighted_sum = tf.reduce_sum(basis_values * self.basis_weights, axis=-1, keepdims=True) |
| |
| |
| normalized_output = tf.sigmoid(weighted_sum) |
| |
| |
| water_content = normalized_output * self.output_scale + self.output_shift |
| |
| return water_content |
|
|
| class ResidualBlock(tf.keras.layers.Layer): |
| """ |
| Residual block with batch normalization and Swish activation |
| |
| This block implements a residual connection with the following architecture: |
| Input -> Dense -> BatchNorm -> Swish -> Dense -> BatchNorm -> Add -> Swish -> Output |
| | |
| Input --------> (Optional projection) -----------------------> |
| |
| Parameters: |
| ----------- |
| units : int |
| Number of output units |
| projection : bool |
| Whether to use projection for input if dimensions don't match |
| dropout_rate : float |
| Dropout rate applied after the residual connection |
| """ |
| def __init__(self, units, projection=True, dropout_rate=0.1): |
| super(ResidualBlock, self).__init__() |
| self.units = units |
| self.projection = projection |
| self.dropout_rate = dropout_rate |
| |
| |
| self.dense1 = tf.keras.layers.Dense( |
| units, |
| kernel_initializer='he_normal' |
| ) |
| self.bn1 = tf.keras.layers.BatchNormalization(epsilon=1e-5) |
| self.activation1 = tf.keras.layers.Activation('swish') |
| |
| self.dense2 = tf.keras.layers.Dense( |
| units, |
| kernel_initializer='he_normal' |
| ) |
| self.bn2 = tf.keras.layers.BatchNormalization(epsilon=1e-5) |
| |
| |
| self.use_projection = False |
| self.projection_layer = None |
| self.projection_bn = None |
| |
| |
| self.activation2 = tf.keras.layers.Activation('swish') |
| self.dropout = tf.keras.layers.Dropout(dropout_rate) |
| |
| def build(self, input_shape): |
| input_dim = input_shape[-1] |
| |
| |
| if input_dim != self.units and self.projection: |
| self.use_projection = True |
| self.projection_layer = tf.keras.layers.Dense( |
| self.units, |
| kernel_initializer='he_normal' |
| ) |
| self.projection_bn = tf.keras.layers.BatchNormalization(epsilon=1e-5) |
| |
| super(ResidualBlock, self).build(input_shape) |
| |
| def call(self, inputs, training=False): |
| |
| x = self.dense1(inputs) |
| x = self.bn1(x, training=training) |
| x = self.activation1(x) |
| |
| x = self.dense2(x) |
| x = self.bn2(x, training=training) |
| |
| |
| if self.use_projection: |
| shortcut = self.projection_layer(inputs) |
| shortcut = self.projection_bn(shortcut, training=training) |
| else: |
| shortcut = inputs |
| |
| |
| x = x + shortcut |
| |
| |
| x = self.activation2(x) |
| x = self.dropout(x, training=training) |
| |
| return x |
|
|
| class SwishActivation(tf.keras.layers.Layer): |
| """ |
| Swish activation function layer: x * sigmoid(x) |
| Can be used as a drop-in replacement for other activation layers |
| """ |
| def __init__(self, **kwargs): |
| super(SwishActivation, self).__init__(**kwargs) |
| |
| def call(self, inputs): |
| return inputs * tf.sigmoid(inputs) |
| |
| def compute_output_shape(self, input_shape): |
| return input_shape |
|
|
| class EnhancedPropertyEncoder(tf.keras.layers.Layer): |
| """ |
| Enhanced property encoder with residual connections and batch normalization |
| |
| Architecture: |
| Input -> Dense -> BN -> Swish -> ResidualBlock -> ResidualBlock -> Output |
| |
| Parameters: |
| ----------- |
| embedding_dim : int |
| Final embedding dimension |
| input_dim : int |
| Input dimension (number of features for the property) |
| dropout_rate : float |
| Dropout rate to apply in residual blocks |
| """ |
| def __init__(self, embedding_dim, input_dim, dropout_rate=0.1, **kwargs): |
| super(EnhancedPropertyEncoder, self).__init__(**kwargs) |
| self.embedding_dim = embedding_dim |
| self.input_dim = input_dim |
| self.dropout_rate = dropout_rate |
| |
| |
| self.initial_dense = tf.keras.layers.Dense( |
| embedding_dim // 2, |
| kernel_initializer='he_normal' |
| ) |
| self.initial_bn = tf.keras.layers.BatchNormalization(epsilon=1e-5) |
| self.initial_activation = tf.keras.layers.Activation('swish') |
| |
| |
| self.residual1 = ResidualBlock( |
| units=embedding_dim // 2, |
| projection=False, |
| dropout_rate=dropout_rate |
| ) |
| |
| |
| self.residual2 = ResidualBlock( |
| units=embedding_dim, |
| projection=True, |
| dropout_rate=dropout_rate |
| ) |
| |
| def build(self, input_shape): |
| |
| self.initial_dense.build(input_shape) |
| |
| |
| intermediate_shape = (input_shape[0], self.embedding_dim // 2) |
| |
| |
| self.initial_bn.build(intermediate_shape) |
| self.initial_activation.build(intermediate_shape) |
| |
| |
| self.residual1.build(intermediate_shape) |
| |
| |
| |
| res1_output_shape = intermediate_shape |
| |
| |
| self.residual2.build(res1_output_shape) |
| |
| |
| self.built = True |
| |
| super(EnhancedPropertyEncoder, self).build(input_shape) |
|
|
| def call(self, inputs, training=False): |
| |
| x = self.initial_dense(inputs) |
| x = self.initial_bn(x, training=training) |
| x = self.initial_activation(x) |
| |
| |
| x = self.residual1(x, training=training) |
| x = self.residual2(x, training=training) |
| |
| return x |
| |
| def compute_output_shape(self, input_shape): |
| return (input_shape[0], self.embedding_dim) |
|
|
| class CrossAttentionLayer(tf.keras.layers.Layer): |
| """ |
| Specialized cross-attention between soil properties with attention weight return capability |
| """ |
| def __init__(self, embedding_dim, num_heads=4, dropout_rate=0.1): |
| super(CrossAttentionLayer, self).__init__() |
| self.embedding_dim = embedding_dim |
| self.num_heads = num_heads |
| self.dropout_rate = dropout_rate |
| |
| |
| self._attention = None |
| |
| def build(self, input_shape): |
| |
| if not isinstance(input_shape, list) or len(input_shape) != 2: |
| raise ValueError("Inputs must be a list of two tensors") |
| |
| |
| self._attention = tf.keras.layers.MultiHeadAttention( |
| num_heads=self.num_heads, |
| key_dim=self.embedding_dim // self.num_heads, |
| dropout=self.dropout_rate, |
| kernel_initializer='glorot_uniform' |
| ) |
| |
| |
| self.layer_norm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6) |
| self.layer_norm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6) |
| |
| |
| self.ffn = tf.keras.Sequential([ |
| tf.keras.layers.Dense(self.embedding_dim * 2, activation='swish'), |
| tf.keras.layers.Dropout(self.dropout_rate), |
| tf.keras.layers.Dense(self.embedding_dim) |
| ]) |
| |
| super(CrossAttentionLayer, self).build(input_shape) |
| |
| def call(self, inputs, training=False, return_attention_weights=False): |
| |
| if isinstance(inputs, list) and len(inputs) == 2: |
| |
| prop1, prop2 = inputs |
| prop1 = tf.ensure_shape(prop1, [None, self.embedding_dim]) |
| prop2 = tf.ensure_shape(prop2, [None, self.embedding_dim]) |
| stacked_features = tf.stack([prop1, prop2], axis=1) |
| else: |
| |
| stacked_features = inputs |
| |
| |
| if return_attention_weights: |
| attention_output, attention_weights = self._attention( |
| query=stacked_features, |
| key=stacked_features, |
| value=stacked_features, |
| training=training, |
| return_attention_scores=True |
| ) |
| else: |
| attention_output = self._attention( |
| query=stacked_features, |
| key=stacked_features, |
| value=stacked_features, |
| training=training |
| ) |
| attention_weights = None |
| |
| |
| x = self.layer_norm1(stacked_features + attention_output) |
| ffn_output = self.ffn(x, training=training) |
| output = self.layer_norm2(x + ffn_output) |
| |
| if return_attention_weights: |
| return output, attention_weights |
| else: |
| return output |
|
|
| class GatingMechanism(tf.keras.layers.Layer): |
| """ |
| Gating mechanism to control information flow between properties |
| """ |
| def __init__(self): |
| super(GatingMechanism, self).__init__() |
| |
| def build(self, input_shape): |
| |
| if not isinstance(input_shape, list) or len(input_shape) != 2: |
| raise ValueError("Inputs must be a list of two tensors") |
| |
| |
| self.input_dim = input_shape[0][-1] |
| |
| |
| self.gate_weights = self.add_weight( |
| shape=(self.input_dim,), |
| initializer=tf.keras.initializers.GlorotNormal(), |
| trainable=True, |
| name='gate_weights' |
| ) |
| |
| super(GatingMechanism, self).build(input_shape) |
| |
| def call(self, inputs): |
| |
| if not isinstance(inputs, list) or len(inputs) != 2: |
| raise ValueError("Inputs must be a list of two tensors") |
| |
| primary_input, secondary_input = inputs |
| |
| |
| primary_input = tf.ensure_shape(primary_input, secondary_input.shape) |
| |
| |
| gate = tf.sigmoid(tf.reduce_sum(primary_input * self.gate_weights, axis=-1, keepdims=True)) |
| |
| |
| gated_output = primary_input + gate * secondary_input |
| |
| return gated_output |
|
|
| |
| tf.keras.utils.get_custom_objects().update({ |
| 'swish': tf.keras.layers.Activation(lambda x: x * tf.sigmoid(x)) |
| }) |
|
|