| import tensorflow as tf |
| from tensorflow import keras |
| import numpy as np |
| from habit.model.layers import ( |
| MonotonicFunction, |
| ResidualBlock, |
| EnhancedPropertyEncoder, |
| SwishActivation, |
| CrossAttentionLayer, |
| GatingMechanism |
| ) |
|
|
|
|
| |
| def swish(x): |
| return x * tf.keras.backend.sigmoid(x) |
|
|
| |
| tf.keras.utils.get_custom_objects().update({'swish': tf.keras.layers.Activation(swish)}) |
|
|
| class HABIT(keras.Model): |
| def __init__(self, embedding_dim=128, num_heads=4, num_monotonic_basis=16, dropout_rate=0.1): |
| super(HABIT, self).__init__() |
| |
| |
| self.embedding_dim = embedding_dim |
| self.num_heads = num_heads |
| self.num_monotonic_basis = num_monotonic_basis |
| self.dropout_rate = dropout_rate |
| |
| |
| self.texture_encoder = self._build_property_encoder(3) |
| self.bd_encoder = self._build_property_encoder(1) |
| self.oc_encoder = self._build_property_encoder(1) |
| self.ksat_encoder = self._build_property_encoder(1) |
| |
| |
| self.texture_bd_cross_attention = CrossAttentionLayer( |
| embedding_dim=embedding_dim, |
| num_heads=self.num_heads, |
| dropout_rate=dropout_rate |
| ) |
| |
| self.texture_oc_cross_attention = CrossAttentionLayer( |
| embedding_dim=embedding_dim, |
| num_heads=max(2, self.num_heads // 2), |
| dropout_rate=dropout_rate |
| ) |
| |
| |
| self.texture_bd_gate = GatingMechanism() |
| self.texture_oc_gate = GatingMechanism() |
| |
| |
| self.texture_bd_fusion = keras.layers.Dense(embedding_dim, activation='swish') |
| self.texture_oc_fusion = keras.layers.Dense(embedding_dim, activation='swish') |
| |
| |
| self.property_attention = keras.layers.MultiHeadAttention( |
| num_heads=self.num_heads, |
| key_dim=self.embedding_dim // self.num_heads, |
| value_dim=self.embedding_dim // self.num_heads, |
| dropout=dropout_rate, |
| use_bias=True |
| ) |
| |
| |
| self.wp_attention = keras.layers.MultiHeadAttention( |
| num_heads=self.num_heads, |
| key_dim=self.embedding_dim // self.num_heads, |
| value_dim=self.embedding_dim // self.num_heads, |
| dropout=dropout_rate, |
| use_bias=True |
| ) |
| |
| |
| self.property_layer_norm = keras.layers.LayerNormalization(epsilon=1e-6) |
| self.wp_layer_norm = keras.layers.LayerNormalization(epsilon=1e-6) |
| self.fusion_layer_norm = keras.layers.LayerNormalization(epsilon=1e-6) |
| |
| |
| self.wp_embedding_layer = keras.layers.Dense(self.embedding_dim, activation='swish') |
| |
| |
| self.soil_dense = keras.layers.Dense(self.embedding_dim, activation='swish') |
| self.flatten = keras.layers.Flatten() |
| |
| |
| self.property_fusion = keras.Sequential([ |
| keras.layers.Dense(self.embedding_dim * 2, activation='swish'), |
| keras.layers.BatchNormalization(epsilon=1e-5), |
| keras.layers.Dropout(self.dropout_rate), |
| keras.layers.Dense(self.embedding_dim, activation='swish') |
| ]) |
| |
| |
| self.parameter_network = keras.Sequential([ |
| keras.layers.Dense(self.embedding_dim, activation='swish'), |
| keras.layers.BatchNormalization(epsilon=1e-5), |
| keras.layers.Dense(self.embedding_dim, activation='swish'), |
| keras.layers.BatchNormalization(epsilon=1e-5), |
| keras.layers.Dense(self.num_monotonic_basis * 2) |
| ]) |
| |
| self.monotonic_function = MonotonicFunction(self.num_monotonic_basis) |
| |
| |
| self._property_attention_weights = None |
| self._wp_attention_weights = None |
| self._cross_attention_weights = None |
|
|
| def build(self, input_shape): |
| |
| if isinstance(input_shape, list): |
| texture_shape, bd_shape, oc_shape, ksat_shape, prop_mask_shape, wp_shape = input_shape |
| else: |
| |
| texture_shape = (None, 3) |
| bd_shape = (None, 1) |
| oc_shape = (None, 1) |
| ksat_shape = (None, 1) |
| prop_mask_shape = (None, 4) |
| wp_shape = (None, None) |
| |
| |
| self.texture_encoder.build(texture_shape) |
| self.bd_encoder.build(bd_shape) |
| self.oc_encoder.build(oc_shape) |
| self.ksat_encoder.build(ksat_shape) |
| |
| |
| self.texture_bd_cross_attention.build([texture_shape, bd_shape]) |
| self.texture_oc_cross_attention.build([texture_shape, oc_shape]) |
| |
| |
| if not hasattr(self, 'get_attention_weights'): |
| def get_attention_weights(self): |
| """Return attention weights for interpretation""" |
| return { |
| 'property_attention': getattr(self, '_property_attention_weights', None), |
| 'wp_attention': getattr(self, '_wp_attention_weights', None), |
| 'cross_attention': getattr(self, '_cross_attention_weights', None) |
| } |
| |
| |
| import types |
| self.get_attention_weights = types.MethodType(get_attention_weights, self) |
| |
| super().build(input_shape) |
| |
| def _build_property_encoder(self, input_dim): |
| """ |
| Build an enhanced property encoder with residual connections and batch normalization |
| """ |
| return EnhancedPropertyEncoder( |
| embedding_dim=self.embedding_dim, |
| input_dim=input_dim, |
| dropout_rate=self.dropout_rate |
| ) |
| |
| def call(self, inputs, training=False): |
| |
| texture, bd, oc, ksat, properties_mask, water_potential = inputs |
| |
| self.texture_input = texture |
| self.bd_input = bd |
| self.oc_input = oc |
| self.ksat_input = ksat |
| self.properties_mask_input = properties_mask |
| self.wp_input = water_potential |
| |
| |
| |
| |
| wp_mask = tf.cast(tf.greater(water_potential, -99), tf.float32) |
|
|
| |
| texture_features = self.texture_encoder(texture, training=training) |
| bd_features = self.bd_encoder(bd, training=training) |
| |
| |
| texture_features = tf.ensure_shape(texture_features, [None, self.embedding_dim]) |
| bd_features = tf.ensure_shape(bd_features, [None, self.embedding_dim]) |
| |
| |
| oc_features = self.oc_encoder(oc, training=training) * tf.expand_dims(properties_mask[:, 2], -1) |
| ksat_features = self.ksat_encoder(ksat, training=training) * tf.expand_dims(properties_mask[:, 3], -1) |
| |
| |
| oc_features_safe = oc_features if oc_features.shape[-1] == self.embedding_dim else \ |
| tf.zeros_like(texture_features, dtype=texture_features.dtype) |
|
|
| |
| |
| |
| |
| |
| cross_attention_weights = {} |
| |
| |
| bd_availability = tf.expand_dims(properties_mask[:, 1], -1) |
| |
| |
| texture_bd_combined, texture_bd_weights = self.texture_bd_cross_attention( |
| inputs=[texture_features, bd_features], |
| training=training, |
| return_attention_weights=True |
| ) |
|
|
| |
| texture_enhanced_raw = texture_bd_combined[:, 0, :] |
| bd_enhanced_raw = texture_bd_combined[:, 1, :] |
|
|
| |
| texture_bd_gated = self.texture_bd_gate([texture_features, texture_enhanced_raw]) * bd_availability |
| bd_texture_gated = self.texture_bd_gate([bd_features, bd_enhanced_raw]) * bd_availability |
|
|
| |
| texture_enhanced_by_bd_raw = self.texture_bd_fusion(texture_bd_gated) |
| bd_enhanced_by_texture_raw = self.texture_bd_fusion(bd_texture_gated) |
| |
| |
| texture_enhanced_by_bd = texture_features + (texture_enhanced_by_bd_raw - texture_features) * bd_availability |
| bd_enhanced_by_texture = bd_features + (bd_enhanced_by_texture_raw - bd_features) * bd_availability |
| |
| |
| cross_attention_weights['texture_bd'] = texture_bd_weights |
| |
| |
| oc_availability = tf.expand_dims(properties_mask[:, 2], -1) |
|
|
| |
| texture_oc_combined, texture_oc_weights = self.texture_oc_cross_attention( |
| inputs=[texture_features, oc_features_safe], |
| training=training, |
| return_attention_weights=True |
| ) |
|
|
| |
| texture_enhanced_raw_oc = texture_oc_combined[:, 0, :] |
| oc_enhanced_raw = texture_oc_combined[:, 1, :] |
| |
| |
| texture_oc_gated = self.texture_oc_gate([texture_features, texture_enhanced_raw_oc]) * oc_availability |
| oc_texture_gated = self.texture_oc_gate([oc_features_safe, oc_enhanced_raw]) * oc_availability |
|
|
| |
| texture_enhanced_by_oc_raw = self.texture_oc_fusion(texture_oc_gated) |
| oc_enhanced_by_texture_raw = self.texture_oc_fusion(oc_texture_gated) |
| |
| |
| texture_enhanced_by_oc = texture_features + (texture_enhanced_by_oc_raw - texture_features) * oc_availability |
| oc_enhanced_by_texture = oc_features_safe + (oc_enhanced_by_texture_raw - oc_features_safe) * oc_availability |
| |
| |
| cross_attention_weights['texture_oc'] = texture_oc_weights |
|
|
| |
| self._cross_attention_weights = cross_attention_weights |
|
|
| |
| |
| |
| |
| |
| combined_texture = texture_enhanced_by_bd + texture_enhanced_by_oc |
| combined_texture = self.fusion_layer_norm(combined_texture) |
| |
| |
| enhanced_bd = bd_enhanced_by_texture |
| enhanced_oc = oc_enhanced_by_texture |
| |
| |
| batch_size = tf.shape(texture)[0] |
| |
| |
| |
| property_features = tf.stack([ |
| combined_texture, |
| enhanced_bd, |
| enhanced_oc, |
| ksat_features |
| ], axis=1) |
| |
| |
| |
| property_attention_mask = tf.expand_dims( |
| tf.concat([ |
| tf.ones_like(properties_mask[:, 0:2]), |
| tf.reshape(properties_mask[:, 2:4], [batch_size, 2]) |
| ], axis=1), |
| axis=1 |
| ) |
| |
| |
| property_attention_output, property_weights = self.property_attention( |
| query=property_features, |
| key=property_features, |
| value=property_features, |
| attention_mask=property_attention_mask, |
| return_attention_scores=True, |
| training=training |
| ) |
| |
| |
| self._property_attention_weights = property_weights |
| |
| |
| property_features = self.property_layer_norm(property_features + property_attention_output) |
| |
| |
| flattened_features = self.flatten(property_features) |
| soil_embedding = self.property_fusion(flattened_features, training=training) |
| |
| |
| batch_size = tf.shape(water_potential)[0] |
| num_points = tf.shape(water_potential)[1] |
| |
| |
| soil_embedding_expanded = tf.expand_dims(soil_embedding, axis=1) |
| |
| |
| wp_expanded = tf.expand_dims(water_potential, axis=-1) |
| |
| |
| wp_embedding = self.wp_embedding_layer(wp_expanded) |
| |
| |
| wp_attention_mask = tf.expand_dims(wp_mask, axis=1) |
| |
| |
| wp_attention_output, wp_weights = self.wp_attention( |
| query=soil_embedding_expanded, |
| key=wp_embedding, |
| value=wp_embedding, |
| attention_mask=wp_attention_mask, |
| return_attention_scores=True, |
| training=training |
| ) |
| |
| |
| self._wp_attention_weights = wp_weights |
| |
| |
| soil_wp_embedding = self.wp_layer_norm(soil_embedding_expanded + wp_attention_output) |
| soil_wp_embedding = tf.squeeze(soil_wp_embedding, axis=1) |
| |
| |
| soil_expanded = tf.repeat(soil_wp_embedding, num_points, axis=0) |
| |
| |
| wp_flat = tf.reshape(water_potential, [batch_size * num_points, 1]) |
| |
| |
| combined = tf.concat([soil_expanded, wp_flat], axis=1) |
| |
| |
| parameters = self.parameter_network(combined, training=training) |
| |
| |
| water_content_flat = self.monotonic_function([wp_flat, parameters]) |
| |
| |
| water_content = tf.reshape(water_content_flat, [batch_size, num_points]) |
| |
| |
| return water_content * wp_mask |
| |
| def get_attention_weights(self): |
| """Return attention weights for interpretation""" |
| return { |
| 'property_attention': self._property_attention_weights, |
| 'wp_attention': self._wp_attention_weights, |
| 'cross_attention': self._cross_attention_weights |
| } |