diff --git a/docs/exploratory_analyses/normality_test.md b/docs/exploratory_analyses/normality_test.md new file mode 100644 index 00000000..d76bb137 --- /dev/null +++ b/docs/exploratory_analyses/normality_test.md @@ -0,0 +1,3 @@ +# Normality test + +::: eis_toolkit.exploratory_analyses.normality_test diff --git a/eis_toolkit/exploratory_analyses/normality_test.py b/eis_toolkit/exploratory_analyses/normality_test.py new file mode 100644 index 00000000..4034d2b1 --- /dev/null +++ b/eis_toolkit/exploratory_analyses/normality_test.py @@ -0,0 +1,126 @@ +from numbers import Number + +import numpy as np +import pandas as pd +from beartype import beartype +from beartype.typing import Dict, Optional, Sequence, Tuple +from scipy.stats import shapiro + +from eis_toolkit.exceptions import ( + EmptyDataException, + InvalidColumnException, + InvalidDataShapeException, + InvalidRasterBandException, + NonNumericDataException, + SampleSizeExceededException, +) +from eis_toolkit.utilities.checks.dataframe import check_columns_numeric, check_columns_valid, check_empty_dataframe + + +@beartype +def normality_test_dataframe( + data: pd.DataFrame, columns: Optional[Sequence[str]] = None +) -> Dict[str, Tuple[float, float]]: + """ + Compute Shapiro-Wilk test for normality on the input DataFrame. + + Nodata values are dropped automatically. + + Args: + data: Dataframe containing the input data. + columns: Column selection. If none, normality is tested for all columns. + + Returns: + Test statistic and p_value for each selected column in a dictionary. + + Raises: + EmptyDataException: The input data is empty. + InvalidColumnException: All selected columns were not found in the input data. + NonNumericDataException: Selected data or columns contains non-numeric data. + SampleSizeExceededException: Input data exceeds the maximum of 5000 samples. + """ + if check_empty_dataframe(data): + raise EmptyDataException("The input Dataframe is empty.") + + if columns is not None: + if not check_columns_valid(data, columns): + raise InvalidColumnException("All selected columns were not found in the input DataFrame.") + if not check_columns_numeric(data, columns): + raise NonNumericDataException("The selected columns contain non-numeric data.") + + data = data[columns].dropna() + + else: + if not check_columns_numeric(data, data.columns): + raise NonNumericDataException("The input data contain non-numeric data.") + columns = data.columns + + statistics = {} + for column in columns: + if len(data[column]) > 5000: + raise SampleSizeExceededException(f"Sample size for column '{column}' exceeds the limit of 5000 samples.") + statistics[column] = shapiro(data[column]) + + return statistics + + +@beartype +def normality_test_array( + data: np.ndarray, bands: Optional[Sequence[int]] = None, nodata_value: Optional[Number] = None +) -> Dict[int, Tuple[float, float]]: + """ + Compute Shapiro-Wilk test for normality on the input Numpy array. + + It is assumed that 3D input array represents multiband raster and the first dimension is the number of bands + (same shape as Rasterio reads a raster into an array). Normality is calculated for each band separately. + NaN values and optionally a specified nodata value are masked out before calculations. + + Args: + data: Numpy array containing the input data. Array should either be 1D, 2D or 3D. + bands: Band selection. Applies only if input array is 3D. If None, normality is tested for each band. + nodata_value: Nodata value to be masked out. Optional parameter. + + Returns: + Test statistic and p_value for each selected band in a dictionary. + + Raises: + EmptyDataException: The input data is empty. + InvalidRasterBandException: All selected bands were not found in the input data. + InvalidDataShapeException: Input data has incorrect number of dimensions (> 3). + SampleSizeExceededException: Input data exceeds the maximum of 5000 samples. + """ + if data.size == 0: + raise EmptyDataException("The input Numpy array is empty.") + + if data.ndim == 1 or data.ndim == 2: + prepared_data = np.expand_dims(data, axis=0) + bands = range(1) + + elif data.ndim == 3: + if bands is not None: + if not all(band < len(data) for band in bands): + raise InvalidRasterBandException("All selected bands were not found in the input array.") + else: + bands = range(len(data)) + prepared_data = data + + else: + raise InvalidDataShapeException(f"The input data has unexpected number of dimensions: {data.ndim}.") + + statistics = {} + + for band in bands: + flattened_data = prepared_data[band].ravel() + + nan_mask = flattened_data == np.nan + if nodata_value is not None: + nodata_mask = flattened_data == nodata_value + nan_mask = nan_mask & nodata_mask + masked_data = np.ma.masked_array(data=flattened_data, mask=nan_mask) + + if len(masked_data) > 5000: + raise SampleSizeExceededException(f"Sample size for band '{band}' exceeds the limit of 5000 samples.") + + statistics[band] = shapiro(masked_data) + + return statistics diff --git a/eis_toolkit/exploratory_analyses/statistical_tests.py b/eis_toolkit/exploratory_analyses/statistical_tests.py index 80d66d2f..c7f80117 100644 --- a/eis_toolkit/exploratory_analyses/statistical_tests.py +++ b/eis_toolkit/exploratory_analyses/statistical_tests.py @@ -1,17 +1,9 @@ -import numpy as np import pandas as pd from beartype import beartype -from beartype.typing import Dict, Literal, Optional, Sequence, Tuple, Union -from scipy.stats import chi2_contingency, shapiro - -from eis_toolkit.exceptions import ( - EmptyDataException, - EmptyDataFrameException, - InvalidColumnException, - InvalidParameterValueException, - NonNumericDataException, - SampleSizeExceededException, -) +from beartype.typing import Literal, Optional, Sequence +from scipy.stats import chi2_contingency + +from eis_toolkit.exceptions import EmptyDataFrameException, InvalidParameterValueException, NonNumericDataException from eis_toolkit.utilities.checks.dataframe import check_columns_numeric, check_columns_valid, check_empty_dataframe @@ -57,68 +49,6 @@ def chi_square_test(data: pd.DataFrame, target_column: str, columns: Optional[Se return statistics -@beartype -def normality_test( - data: Union[pd.DataFrame, np.ndarray], columns: Optional[Sequence[str]] = None -) -> Union[Dict[str, Tuple[float, float]], Tuple[float, float]]: - """Compute Shapiro-Wilk test for normality on the input data. - - Args: - data: Dataframe or Numpy array containing the input data. - columns: Optional columns to be used for testing. - - Returns: - Test statistics for each variable, output differs based on input data type. - Numpy array input returns a Tuple of statistic and p_value. - Dataframe input returns a dictionary where keys are column names - and values are tuples containing the statistic and p-value. - - Raises: - EmptyDataException: The input data is empty. - InvalidColumnException: All selected columns were not found in the input data. - NonNumericDataException: Selected data or columns contains non-numeric data. - SampleSizeExceededException: Input data exceeds the maximum of 5000 samples. - """ - statistics = {} - if isinstance(data, pd.DataFrame): - if check_empty_dataframe(data): - raise EmptyDataException("The input Dataframe is empty.") - - if columns is not None: - if not check_columns_valid(data, columns): - raise InvalidColumnException("All selected columns were not found in the input DataFrame.") - if not check_columns_numeric(data, columns): - raise NonNumericDataException("The selected columns contain non-numeric data.") - - data = data[columns].dropna() - - else: - if not check_columns_numeric(data, data.columns): - raise NonNumericDataException("The input data contain non-numeric data.") - columns = data.columns - - for column in columns: - if len(data[column]) > 5000: - raise SampleSizeExceededException(f"Sample size for '{column}' exceeds the limit of 5000 samples.") - statistic, p_value = shapiro(data[column]) - statistics[column] = (statistic, p_value) - - else: - if data.size == 0: - raise EmptyDataException("The input numpy array is empty.") - if len(data) > 5000: - raise SampleSizeExceededException("Sample size exceeds the limit of 5000 samples.") - - nan_mask = np.isnan(data) - data = data[~nan_mask] - - flattened_data = data.flatten() - statistic, p_value = shapiro(flattened_data) - statistics = (statistic, p_value) - - return statistics - - @beartype def correlation_matrix( data: pd.DataFrame, diff --git a/eis_toolkit/prediction/autoencoder.py b/eis_toolkit/prediction/autoencoder.py new file mode 100644 index 00000000..b0ecfa71 --- /dev/null +++ b/eis_toolkit/prediction/autoencoder.py @@ -0,0 +1,489 @@ +from typing import Any, List, Optional, Tuple + +# from sklearn.manifold import TSNE +import matplotlib.pyplot as plt +import numpy as np +import tensorflow as tf + +# from sklearn.model_selection import train_test_split +from beartype import beartype +from keras import backend as K +from keras.callbacks import EarlyStopping +from keras.models import Model +from keras.regularizers import l1 +from skimage.metrics import structural_similarity as ssim +from tensorflow.keras.layers import ( + Activation, + Add, + BatchNormalization, + Conv2D, + Dropout, + Input, + MaxPooling2D, + Multiply, + UpSampling2D, + concatenate, +) + +"""Model-inference and building functions""" + + +@beartype +def train( + model: Model, + x_train: np.ndarray | List[np.ndarray], + y_train: np.ndarray | List[np.ndarray] | None = None, + epochs: int = 50, + batch_size: int = 128, + validation_split: float = 0.1, + shuffle: bool = True, + use_early_stopping: bool = True, + validation_data: None | np.ndarray | Tuple[np.ndarray, np.ndarray] = None, +) -> Model: + """ + Train the provided model using the given training data. + + Parameters: + - model (tf.keras.Model): The model to train + - x_train (numpy.ndarray or list of numpy.ndarray): Training data + - y_train: target labels, but in case of autoencoders, it's optional as the targets are often the input + - epochs (int): Number of epochs for training + - batch_size (int): Batch size for training + - validation_split (float): Fraction of the training data to be used as validation data. Only used when no explicit + validation_data is provided + - shuffle (bool): Whether to shuffle the samples at each epoch + - use_early_stopping (bool): Whether to use early stopping + - validation_data: validation data, either as a tuple of (x_val, y_val) or x_val + Returns: + - Trained model + """ + + # If no y_train, targets == inputs + if y_train is None: + y_train = x_train + + callbacks_list = [] + + # EarlyStopping function + if use_early_stopping: + early_stopping = EarlyStopping( + monitor="val_loss", # Value to be monitored + patience=5, # Number of epochs with no improvement after which the training will be stopped + mode="auto", # 'auto', 'min' or 'max'. In 'auto', algorithm will detect the direction + restore_best_weights=True, # Whether to restore model weights from the epoch with the best value result + ) + callbacks_list.append(early_stopping) + + # Check if validation data is provided, and copy the targets to input like training data + # in case there are no targets + if validation_data is not None: + if isinstance(validation_data, tuple) and len(validation_data) == 2: + x_val, y_val = validation_data + else: + x_val = validation_data + y_val = validation_data + else: + x_val, y_val = None, None + + # Check if using U-Net (multi-modal) or regular autoencoder + if isinstance(x_train, list) and len(x_train) > 1: + # U-Net multi-modal training + model.fit( + x_train, + y_train[0], # U-Net takes multi-modal inputs but has a single output + epochs=epochs, + batch_size=batch_size, + shuffle=shuffle, + validation_data=(x_val, y_val) if x_val is not None else None, + validation_split=None if x_val is not None else validation_split, + callbacks=callbacks_list, + ) + else: + # Regular autoencoder training + model.fit( + x_train, + y_train, + epochs=epochs, + batch_size=batch_size, + shuffle=shuffle, + validation_data=(x_val, y_val) if x_val is not None else None, + validation_split=None if x_val is not None else validation_split, + callbacks=callbacks_list, + ) + + return model + + +@beartype +def build_autoencoder( + input_shape: tuple, + modality: int = 1, + dropout: float = 0.2, + regularization: float = 0, + number_of_layers: int = 2, + filter_size_start: int = 16, +) -> Model: + """ + Build an autoencoder model that can handle multiple modalities/bands. + + Parameters: + - input_shape (tuple): Shape of the input data (excluding batch dimension). + - number_of_layers (int): Number of layers in encoder and decoder. + - filter_size_start (int): Initial number of filters in the encoder. + - dropout (float): Dropout rate to apply between layers. + - regularization (float): Regularization strength for L1 regularization. + - modality (int): Number of modalities or bands. + Returns: + - model: Compiled autoencoder model. + """ + + # List to hold all input layers + input_imgs = [Input(shape=input_shape) for _ in range(modality)] + + encoded_layers = [] + for idx, input_img in enumerate(input_imgs): + x = input_img + current_filter_size = filter_size_start + for i in range(number_of_layers): + x = Conv2D( + current_filter_size, (3, 3), activation="relu", padding="same", kernel_regularizer=l1(regularization) + )(x) + x = Dropout(dropout)(x) + x = MaxPooling2D((2, 2), padding="same")(x) + current_filter_size *= 2 + encoded_layers.append(x) + + x = concatenate(encoded_layers, axis=-1) if modality > 1 else encoded_layers[0] + + # Decoding + current_filter_size //= 2 + for i in range(number_of_layers): + x = Conv2D( + current_filter_size, (3, 3), activation="relu", padding="same", kernel_regularizer=l1(regularization) + )(x) + x = Dropout(dropout)(x) + x = UpSampling2D((2, 2))(x) + current_filter_size //= 2 + + decoded = Conv2D( + input_shape[-1], (3, 3), activation="sigmoid", padding="same", kernel_regularizer=l1(regularization) + )(x) + + autoencoder = Model(input_imgs, decoded) + return autoencoder + + +@beartype +def build_autoencoder_u_net( + resolution: int, + modality: int, + dropout: float = 0.2, + regularization: float = 0, + modality_multipliers: Optional[List[int]] = None, + number_of_layers: int = 2, + filter_size_start: int = 8, +) -> Model: + """ + Build a U-Net architecture with attention blocks and support for multiple modalities/bands. + + Recommended when regular autoencoder cannot perform well with a task, or when having multiple modalities + + Parameters: + - resolution: Image resolution for each modality. + - modality: The number of modalities or bands. + - dropout: Dropout rate. + - regularization: Regularization rate for L1. + - modality_multipliers: Multipliers for each modality. + - number_of_layers: Number of layers in the encoder/decoder. + - filter_size_start: Initial filter size. + Returns: + - model: Compiled U-Net autoencoder model. + """ + + # List to hold all input layers + # input_imgs = [Input(shape=(resolution, resolution, 1)) for _ in range(modality)] + input_imgs: List[Any] = [Input(shape=(resolution, resolution, 1)) for _ in range(modality)] + skip_connections = [] + encoded_imgs = [] + + if modality_multipliers is None: + multipliers = [1 for _ in range(modality)] + else: + multipliers = modality_multipliers + + # Scaling + scaled_imgs = scale_tensors(input_imgs, multipliers) + + # Encoder + for idx, input_img in enumerate(scaled_imgs): + x = input_img + current_filter_size = filter_size_start + for i in range(number_of_layers): + x = Conv2D(current_filter_size, (3, 3), padding="same", kernel_regularizer=l1(regularization))(x) + x = Dropout(dropout)(x) + x = BatchNormalization()(x) + x = Activation("relu")(x) + + # Store the encoder output for skip connections + skip_connections.append(x) + + x = MaxPooling2D((2, 2), padding="same")(x) + current_filter_size *= 2 # Double the filter size for the next layer + encoded_imgs.append(x) + + x = concatenate(encoded_imgs, axis=-1) + + # Decoder + current_filter_size = filter_size_start * number_of_layers + for i in range(number_of_layers): + x = Conv2D(current_filter_size, (3, 3), padding="same", kernel_regularizer=l1(regularization))(x) + x = Dropout(dropout)(x) + x = BatchNormalization()(x) + x = Activation("relu")(x) + x = UpSampling2D((2, 2))(x) + + # Get the corresponding encoder output for skip connection + skip = skip_connections[-(i + 1)] + + # Apply the attention block to skip connection + x = attention_block_skip(x, skip, current_filter_size) + + current_filter_size //= 2 # Halve the filter size for the next layer + + # Output layer + decoded = Conv2D(1, (3, 3), activation="sigmoid", padding="same")(x) + + autoencoder_multi_channel = Model(input_imgs, decoded) + return autoencoder_multi_channel + + +@beartype +def reshape(data: np.ndarray, shape: tuple | int) -> np.ndarray | None: + """ + Reshapes the provided data to the specified shape. + + Parameters: + - data (numpy.ndarray): Input data to be reshaped + - shape: Desired shape + Returns: + - Reshaped data + """ + + try: + data = data.reshape(shape) + return data + except ValueError as e: + print(f"Error reshaping data: {e}") + return None + + +@beartype +def model_predict(model: Model, input: np.ndarray) -> np.ndarray: + """ + Predict using an autoencoder. + + Parameters: + - model: Trained autoencoder model + - input: List or numpy array of images to predict (take care to reshape) + Returns: + - List of numpy array of predictions + """ + + # Make a prediction + predicted_image = model.predict(input) + + return predicted_image + + +@beartype +def preview(model: Model, data: np.ndarray, max_display: int = 10) -> None: + """ + Reshapes the provided data to the specified shape. + + Parameters: + - model: model to predict and preview on + - data: Data to predict and preview + - max_display: the maximum number of samples to show + Returns: + - A matplotlib table showcasing input vs predictions + """ + + if len(data) > max_display: + data = data[:max_display] + + # Get predictions + predictions = model_predict(model, data) + + # Create a subplot of 2 rows (for original and reconstruction) and columns equal to number of samples + n_samples = len(data) + fig, axes = plt.subplots(2, n_samples, figsize=(20, 4)) + + for i in range(n_samples): + # Display original + ax = axes[0, i] + ax.imshow(data[i].squeeze(), cmap="gray" if data[i].shape[-1] == 1 else None) + ax.axis("off") + if i == 0: + ax.set_title("Original") + + # Display reconstruction + ax = axes[1, i] + ax.imshow(predictions[i].squeeze(), cmap="gray" if predictions[i].shape[-1] == 1 else None) + ax.axis("off") + if i == 0: + ax.set_title("Reconstructed") + + plt.show() + + +@beartype +def evaluate(model: Model, test_data: np.ndarray) -> Tuple[float, float]: + """ + Compute the MSE (Mean squared error) and SSIM (Structural similarity index) for the set of test images. + + Parameters: + - model: Trained autoencoder model + - test_data: List or numpy array of test images + Returns: + - Average MSE and average SSIM for the test set + """ + + # Get the autoencoder's predictions + reconstructed_images = model.predict(test_data) + + # Initialize accumulators for MSE and SSIM + mse_accumulator = 0.0 + ssim_accumulator = 0.0 + + # Compute MSE and SSIM for each image + for original, reconstructed in zip(test_data, reconstructed_images): + # MSE + mse_accumulator += K.mean(K.square(original - reconstructed)) + + # Scale the images to be in the range [0,255] for SSIM computation + original_for_ssim = (original * 255).astype(np.uint8) + reconstructed_for_ssim = (reconstructed * 255).astype(np.uint8) + + # SSIM (used on 2D grayscale images; adapt as necessary for color images) + ssim_value, _ = ssim(original_for_ssim.squeeze(), reconstructed_for_ssim.squeeze(), full=True) + ssim_accumulator += ssim_value + + # Calculate average MSE and SSIM + avg_mse = mse_accumulator / len(test_data) + avg_ssim = ssim_accumulator / len(test_data) + + print(f"Average MSE: {avg_mse}, Average SSIM: {avg_ssim}") + return avg_mse, avg_ssim + + +@beartype +def prepare_data_for_model( + dataset: (Tuple[np.ndarray, np.ndarray] | Tuple[np.ndarray]), input_shape: Tuple[int, ...], is_unet: bool = False +) -> Tuple[np.ndarray, np.ndarray] | Tuple[np.ndarray]: + """ + Prepare the dataset based on the model's expected input shape. + + Parameters: + - dataset (tuple): Input data in the format (x_train, x_test). + - input_shape (tuple): Expected input shape of the model excluding the batch size. + - is_unet (bool): Whether the target model is a U-Net style model or not. + Returns: + - tuple: Reshaped training and test data. + """ + if len(dataset) == 2: + x_train, x_test = dataset + x_test = x_test.astype("float32") / 255.0 + else: + x_train = dataset[0] + x_test = None + + x_train = x_train.astype("float32") / 255.0 + + # If it's a U-Net model or a multi-modal regular autoencoder, split the channels + if is_unet or (len(input_shape) == 3 and input_shape[2] > 1): + x_train = [x_train[..., i : i + 1] for i in range(input_shape[2])] # noqa: E203 + if x_test is not None: + x_test = [x_test[..., i : i + 1] for i in range(input_shape[2])] # noqa: E203 + + # Handle case with no test data + if x_test is None: + return (x_train,) + + return x_train, x_test + + +""" +_______________________________________ +Utility functions +_______________________________________ +""" + + +def scale_tensors(input_imgs, multipliers): + """ + Scales each tensor in the input based on the given multipliers. + + Parameters + - input_imgs (list of tf.Tensor): List of input tensors + - multipliers (list of float): List of scaling factors + Returns: + - list of tf.Tensor: Scaled tensors. + """ + multipliers_const = tf.constant(multipliers, dtype=tf.float32) + + return [input_imgs[i] * multipliers_const[i] for i in range(len(input_imgs))] + + +@beartype +def attention_block_skip(x, g, inter_channel): + """ + Implement an attention block with a skip connection. + + Parameters: + - x (tf.Tensor): The input feature map + - g (tf.Tensor): The gating signal + - inter_channel (int): Number of filters for the intermediate convolutional layers + Returns: + - tf.Tensor: Output feature map after the attention block + """ + + # Linear transformation of the input to create new feature map of the input with inter_channel filters + theta_x = Conv2D(inter_channel, [1, 1], strides=[1, 1])(x) + + # Linear transformation of the gating signal (phi operation) and creates a feature map with inter_channel filters + phi_g = Conv2D(inter_channel, [1, 1], strides=[1, 1])(g) + + # Add the transformed input feature map and the transformed gating signal + # Apply the ReLU activation function and then combine the input and the gating signal + f = Activation("relu")(Add()([theta_x, phi_g])) + + # Reduce the channel dimension of the fused feature map to 1 using a 1x1 convolution + # Generates the attention coefficients + psi_f = Conv2D(1, [1, 1], strides=[1, 1])(f) + + # Apply the sigmoid activation function to the attention coefficients + # Results in values between 0 and 1, showcasing the attention scores + rate = Activation("sigmoid")(psi_f) + + # Multiply the original input feature map by the attention scores + # Which amplifies the features in the input where the att* scores are high + att_x = Multiply()([x, rate]) + + # Return the modified feature map after applying attention + return att_x + + +"""Usage""" +# Regular autoencoder +# x_train_regular, x_test_regular = prepare_data_for_model((x_train_data, x_test_data), input_shape_regular) +# model = build_autoencoder(input_shape, modality) +# model.compile(optimizer='adam', loss='binary_crossentropy') # or loss='mse', if data range is not between 0 and 1 +# (in that case, consider also changing the autoencoder model's output activation of 'sigmoid' to 'tanh' or similar +# model = train(model, x_train_regular) + +# U-Net autoencoder +# x_train_unet, x_test_unet = prepare_data_for_model((x_train_data, x_test_data), input_shape_unet, is_unet=True) +# model = build_autoencoder_u_net(resolution, modality) +# model.compile(optimizer='adam', loss='binary_crossentropy') # or loss='mse', if data range is not between 0 and 1 +# (in that case, consider also changing the model's output activation of 'sigmoid' to 'tanh' or similar +# model = train(model, x_train_unet) diff --git a/eis_toolkit/prediction/autoencoder_new.py b/eis_toolkit/prediction/autoencoder_new.py new file mode 100644 index 00000000..cce0fd6e --- /dev/null +++ b/eis_toolkit/prediction/autoencoder_new.py @@ -0,0 +1,330 @@ +from typing import Any, List, Optional, Sequence, Tuple, Union + +import numpy as np +import tensorflow as tf +from beartype import beartype +from keras.callbacks import EarlyStopping +from keras.layers import ( + Activation, + Add, + BatchNormalization, + Conv2D, + Dropout, + Input, + MaxPooling2D, + Multiply, + UpSampling2D, + concatenate, +) +from keras.models import Model +from keras.regularizers import l1 + +from eis_toolkit.exceptions import NonMatchingParameterLengthsException + + +@beartype +def _scale_tensors(input_images: Sequence[tf.Tensor], multipliers: Sequence[float]) -> Sequence[tf.Tensor]: + """ + Scales each tensor in the input based on the given multipliers. + + Parameters + input_images: List of input tensors + multipliers: List of scaling factors + + Returns: + Scaled tensors. + + Raises: + NonMatchingParameterLengthsException: Length of input images does not match the length of multipliers. + """ + if len(input_images) != len(multipliers): + raise NonMatchingParameterLengthsException("The number of input images must match the number of multipliers.") + + multipliers_const = tf.constant(multipliers, dtype=tf.float32) + + return [input_images[i] * multipliers_const[i] for i in range(len(input_images))] + + +@beartype +def _attention_block_skip(x: tf.Tensor, g: tf.Tensor, inter_channel: int) -> tf.Tensor: + """ + Implement an attention block with a skip connection. + + Parameters: + x: The input feature map. + g: The gating signal. + inter_channel: Number of filters for the intermediate convolutional layers. + + Returns: + Output feature map after the attention block. + """ + # Linear transformation of the input to create new feature map of the input with inter_channel filters + theta_x = Conv2D(inter_channel, [1, 1], strides=[1, 1])(x) + + # Linear transformation of the gating signal (phi operation) and creates a feature map with inter_channel filters + phi_g = Conv2D(inter_channel, [1, 1], strides=[1, 1])(g) + + # Add the transformed input feature map and the transformed gating signal + # Apply the ReLU activation function and then combine the input and the gating signal + f = Activation("relu")(Add()([theta_x, phi_g])) + + # Reduce the channel dimension of the fused feature map to 1 using a 1x1 convolution + # Generates the attention coefficients + psi_f = Conv2D(1, [1, 1], strides=[1, 1])(f) + + # Apply the sigmoid activation function to the attention coefficients + # Results in values between 0 and 1, showcasing the attention scores + rate = Activation("sigmoid")(psi_f) + + # Multiply the original input feature map by the attention scores + # Which amplifies the features in the input where the att* scores are high + att_x = Multiply()([x, rate]) + + # Return the modified feature map after applying attention + return att_x + + +@beartype +def train_autoencoder_regular( + X: Union[np.ndarray, List[np.ndarray]], + y: Optional[Union[np.ndarray, List[np.ndarray]]], + input_shape: tuple, + modality: int = 1, + dropout: float = 0.2, + regularization: float = 0, + number_of_layers: int = 2, + filter_size_start: int = 16, + epochs: int = 50, + batch_size: int = 128, + validation_split: float = 0.1, + validation_data: Optional[Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]] = None, + shuffle: bool = True, + early_stopping: bool = True, +) -> Tuple[Model, dict]: + """ + Build an autoencoder model that can handle multiple modalities/bands. + + Each modality/band is processed in a common way. + + Parameters: + X: Feature/evidence data. + y: Target labels. In case of autoencoders, it's optional as the targets are often the input. + input_shape: Shape of the input data (excluding batch dimension). + number_of_layers: Number of layers in encoder and decoder. + filter_size_start: Initial number of filters in the encoder. + dropout: Dropout rate to apply between layers. + regularization: Regularization strength for L1 regularization. + modality: Number of modalities or bands. + epochs: Number of epochs for training. + batch_size: Batch size for training. + validation_split: Fraction of the training data to be used as validation data. Only used when no explicit + validation_data is provided. + validation_data: Validation data, either as a tuple of (x_val, y_val) or x_val. + shuffle: Whether to shuffle the samples at each epoch. + early_stopping: Whether to use early stopping. + + Returns: + Trained autoencoder model and training history. + """ + # 1. Check input data + # TODO + + # 2. Build and compile regular autoencoder model + input_images = [Input(shape=input_shape) for _ in range(modality)] + + # Encoding + encoded_layers = [] + for image in input_images: + x = image + current_filter_size = filter_size_start + for _ in range(number_of_layers): + x = Conv2D( + current_filter_size, (3, 3), activation="relu", padding="same", kernel_regularizer=l1(regularization) + )(x) + x = Dropout(dropout)(x) + x = MaxPooling2D((2, 2), padding="same")(x) + current_filter_size *= 2 + encoded_layers.append(x) + + x = concatenate(encoded_layers, axis=-1) if modality > 1 else encoded_layers[0] + + # Decoding + current_filter_size //= 2 + for _ in range(number_of_layers): + x = Conv2D( + current_filter_size, (3, 3), activation="relu", padding="same", kernel_regularizer=l1(regularization) + )(x) + x = Dropout(dropout)(x) + x = UpSampling2D((2, 2))(x) + current_filter_size //= 2 + + decoded = Conv2D( + input_shape[-1], (3, 3), activation="sigmoid", padding="same", kernel_regularizer=l1(regularization) + )(x) + + model = Model(input_images, decoded) + + model.compile(optimizer="adam", loss="binary_crossentropy") + + # 3. Train the model + # Autoencoding with targets = inputs + if y is None: + y = X + + # Check if using U-Net (multi-modal) or regular autoencoder + if isinstance(X, list) and len(y) > 1: + # U-Net multi-modal training + y = y[0] + + # Use input data as targets for validation if no separate validation targets are provided + if validation_data and len(validation_data) == 1: + validation_data = (validation_data, validation_data) + + callbacks = [EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True)] if early_stopping else [] + + history = model.fit( + X, + y, + epochs=epochs, + validation_data=validation_data if validation_data else None, + validation_split=validation_split if not validation_data else None, + batch_size=batch_size, + shuffle=shuffle, + callbacks=callbacks, + ) + + return model, history.history + + +@beartype +def train_autoencoder_unet( + X: Union[np.ndarray, List[np.ndarray]], + y: Optional[Union[np.ndarray, List[np.ndarray]]], + resolution: int, + modality: int, + dropout: float = 0.2, + regularization: float = 0, + modality_multipliers: Optional[List[int]] = None, + number_of_layers: int = 2, + filter_size_start: int = 8, + epochs: int = 50, + batch_size: int = 128, + validation_split: float = 0.1, + validation_data: Optional[Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]] = None, + shuffle: bool = True, + early_stopping: bool = True, +) -> Tuple[Model, dict]: + """ + Build and trains a U-Net architecture with attention blocks and support for multiple modalities/bands. + + Recommended when regular autoencoder cannot perform well with a task, or when having multiple modalities. + Each modality/band is processed in a common way. + + Parameters: + X: Feature/evidence data. + y: Target labels. In case of autoencoders, it's optional as the targets are often the input. + resolution: Image resolution for each modality. + modality: The number of modalities or bands. + dropout: Dropout rate. + regularization: Regularization rate for L1. + modality_multipliers: Multipliers for each modality. + number_of_layers: Number of layers in the encoder/decoder. + filter_size_start: Initial filter size. + epochs: Number of epochs for training. + batch_size: Batch size for training. + validation_split: Fraction of the training data to be used as validation data. Only used when no explicit + validation_data is provided. + validation_data: validation data, either as a tuple of (x_val, y_val) or x_val. + shuffle: Whether to shuffle the samples at each epoch. + early_stopping: Whether to use early stopping. + + Returns: + Trained autoencoder model and training history. + """ + # 1. Check input data + # TODO + + # 2. Build and compile unet autoencoder model + input_images: List[Any] = [Input(shape=(resolution, resolution, 1)) for _ in range(modality)] + + # Scaling + modality_multipliers = [1 for _ in range(modality)] if modality_multipliers is None else modality_multipliers + scaled_images = _scale_tensors(input_images, modality_multipliers) + + # Encoding + skip_connections = [] + encoded_images = [] + for input_img in scaled_images: + x = input_img + current_filter_size = filter_size_start + for i in range(number_of_layers): + x = Conv2D(current_filter_size, (3, 3), padding="same", kernel_regularizer=l1(regularization))(x) + x = Dropout(dropout)(x) + x = BatchNormalization()(x) + x = Activation("relu")(x) + + # Store the encoder output for skip connections + skip_connections.append(x) + + x = MaxPooling2D((2, 2), padding="same")(x) + + # Double the filter size for the next layer + current_filter_size *= 2 + + encoded_images.append(x) + + x = concatenate(encoded_images, axis=-1) + + # Decoding + current_filter_size = filter_size_start * number_of_layers + for i in range(number_of_layers): + x = Conv2D(current_filter_size, (3, 3), padding="same", kernel_regularizer=l1(regularization))(x) + x = Dropout(dropout)(x) + x = BatchNormalization()(x) + x = Activation("relu")(x) + x = UpSampling2D((2, 2))(x) + + # Get the corresponding encoder output for skip connection + skip = skip_connections[-(i + 1)] + + # Apply the attention block to skip connection + x = _attention_block_skip(x, skip, current_filter_size) + + # Halve the filter size for the next layer + current_filter_size //= 2 + + # Output layer + decoded = Conv2D(1, (3, 3), activation="sigmoid", padding="same")(x) + + model = Model(input_images, decoded) + + model.compile(optimizer="adam", loss="binary_crossentropy") + + # 3. Train the model + # Autoencoding with targets = inputs + if y is None: + y = X + + # Check if using U-Net (multi-modal) or regular autoencoder + if isinstance(X, list) and len(y) > 1: + # U-Net multi-modal training + y = y[0] + + # Use input data as targets for validation if no separate validation targets are provided + if validation_data and len(validation_data) == 1: + validation_data = (validation_data, validation_data) + + callbacks = [EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True)] if early_stopping else [] + + history = model.fit( + X, + y, + epochs=epochs, + validation_data=validation_data if validation_data else None, + validation_split=validation_split if not validation_data else None, + batch_size=batch_size, + shuffle=shuffle, + callbacks=callbacks, + ) + + return model, history.history diff --git a/eis_toolkit/prediction/autoencoder_utils.py b/eis_toolkit/prediction/autoencoder_utils.py new file mode 100644 index 00000000..039d6380 --- /dev/null +++ b/eis_toolkit/prediction/autoencoder_utils.py @@ -0,0 +1,171 @@ +from typing import Tuple + +import matplotlib.pyplot as plt +import numpy as np +from beartype import beartype +from keras import backend as K + +# from keras.losses import mean_squared_error +# from keras.metrics import binary_crossentropy +from keras.models import Model +from skimage.metrics import structural_similarity as ssim + +from eis_toolkit.prediction.machine_learning_general import predict + + +@beartype +def reshape(data: np.ndarray, shape: tuple | int) -> np.ndarray | None: + """ + Reshapes the provided data to the specified shape. + + Parameters: + data: Input data to be reshaped. + shape: Desired shape. + + Returns: + Reshaped data. + """ + try: + data = data.reshape(shape) + return data + except ValueError as e: + print(f"Error reshaping data: {e}") + return None + + +@beartype +def preview(model: Model, data: np.ndarray, max_display: int = 10) -> None: + """ + Reshapes the provided data to the specified shape. + + Parameters: + model: Model to predict and preview on. + data: Data to predict and preview. + max_display: The maximum number of samples to show. + + Returns: + A matplotlib table showcasing input vs predictions. + """ + if len(data) > max_display: + data = data[:max_display] + + # Get predictions + predictions = predict(data, model) + + # Create a subplot of 2 rows (for original and reconstruction) and columns equal to number of samples + n_samples = len(data) + fig, axes = plt.subplots(2, n_samples, figsize=(20, 4)) + + for i in range(n_samples): + # Display original + ax = axes[0, i] + ax.imshow(data[i].squeeze(), cmap="gray" if data[i].shape[-1] == 1 else None) + ax.axis("off") + if i == 0: + ax.set_title("Original") + + # Display reconstruction + ax = axes[1, i] + ax.imshow(predictions[i].squeeze(), cmap="gray" if predictions[i].shape[-1] == 1 else None) + ax.axis("off") + if i == 0: + ax.set_title("Reconstructed") + + plt.show() + + +@beartype +def evaluate(model: Model, test_data: np.ndarray) -> Tuple[float, float]: + """ + Compute the MSE (Mean squared error) and SSIM (Structural similarity index) for the set of test images. + + Parameters: + model: Trained autoencoder model. + test_data: List or numpy array of test images. + + Returns: + Average MSE and average SSIM for the test set. + """ + + # Get the autoencoder's predictions + reconstructed_images = model.predict(test_data) + + # Initialize accumulators for MSE and SSIM + mse_accumulator = 0.0 + ssim_accumulator = 0.0 + + # Compute MSE and SSIM for each image + for original, reconstructed in zip(test_data, reconstructed_images): + # MSE + mse_accumulator += K.mean(K.square(original - reconstructed)) + + # Scale the images to be in the range [0,255] for SSIM computation + original_for_ssim = (original * 255).astype(np.uint8) + reconstructed_for_ssim = (reconstructed * 255).astype(np.uint8) + + # SSIM (used on 2D grayscale images; adapt as necessary for color images) + ssim_value, _ = ssim(original_for_ssim.squeeze(), reconstructed_for_ssim.squeeze(), full=True) + ssim_accumulator += ssim_value + + # Calculate average MSE and SSIM + avg_mse = mse_accumulator / len(test_data) + avg_ssim = ssim_accumulator / len(test_data) + + print(f"Average MSE: {avg_mse}, Average SSIM: {avg_ssim}") + return avg_mse, avg_ssim + + +@beartype +def prepare_data_for_model( + dataset: (Tuple[np.ndarray, np.ndarray] | Tuple[np.ndarray]), input_shape: Tuple[int, ...], is_unet: bool = False +) -> Tuple[np.ndarray, np.ndarray] | Tuple[np.ndarray]: + """ + Prepare the dataset based on the model's expected input shape. + + Parameters: + dataset: Input data in the format (x_train, x_test). + input_shape: Expected input shape of the model excluding the batch size. + is_unet: Whether the target model is a U-Net style model or not. + + Returns: + Reshaped training and test data. + """ + if len(dataset) == 2: + x_train, x_test = dataset + x_test = x_test.astype("float32") / 255.0 + else: + x_train = dataset[0] + x_test = None + + x_train = x_train.astype("float32") / 255.0 + + # If it's a U-Net model or a multi-modal regular autoencoder, split the channels + if is_unet or (len(input_shape) == 3 and input_shape[2] > 1): + x_train = [x_train[..., i : i + 1] for i in range(input_shape[2])] # noqa: E203 + if x_test is not None: + x_test = [x_test[..., i : i + 1] for i in range(input_shape[2])] # noqa: E203 + + # Handle case with no test data + if x_test is None: + return (x_train,) + + return x_train, x_test + + +""" +Usage + +REGULAR AUTOENCODER +> x_train_regular, x_test_regular = prepare_data_for_model((x_train_data, x_test_data), input_shape_regular) +> model = build_autoencoder(input_shape, modality) +> model.compile(optimizer='adam', loss='binary_crossentropy') # or loss='mse', if data range is not between 0 and 1 + (in that case, consider also changing the autoencoder model's output activation of 'sigmoid' to 'tanh' or similar +> model = train(model, x_train_regular) + +U-NET AUTOECNDOER +> x_train_unet, x_test_unet = prepare_data_for_model((x_train_data, x_test_data), input_shape_unet, is_unet=True) +> model = build_autoencoder_u_net(resolution, modality) +> model.compile(optimizer='adam', loss='binary_crossentropy') # or loss='mse', if data range is not between 0 and 1 + (in that case, consider also changing the model's output activation of 'sigmoid' to 'tanh' or similar +> model = train(model, x_train_unet) +""" diff --git a/eis_toolkit/prediction/machine_learning_general.py b/eis_toolkit/prediction/machine_learning_general.py index 2880fb7a..455b7543 100644 --- a/eis_toolkit/prediction/machine_learning_general.py +++ b/eis_toolkit/prediction/machine_learning_general.py @@ -23,9 +23,12 @@ from sklearn.model_selection import KFold, LeaveOneOut, StratifiedKFold, train_test_split from tensorflow import keras -from eis_toolkit.exceptions import InvalidParameterValueException, NonMatchingParameterLengthsException - -# from eis_toolkit.utilities.checks.raster import check_raster_grids +from eis_toolkit.exceptions import ( + InvalidParameterValueException, + NonMatchingParameterLengthsException, + NonMatchingRasterMetadataException, +) +from eis_toolkit.utilities.checks.raster import check_raster_grids from eis_toolkit.vector_processing.rasterize_vector import rasterize_vector SPLIT = "split" @@ -182,50 +185,55 @@ def reshape_predictions( @beartype def prepare_data_for_ml( - training_raster_files: Sequence[Union[str, os.PathLike]], + feature_raster_files: Sequence[Union[str, os.PathLike]], label_file: Optional[Union[str, os.PathLike]] = None, ) -> Tuple[np.ndarray, Optional[np.ndarray], rasterio.profiles.Profile, Any]: """ Prepare data ready for machine learning model training. Performs the following steps: - - Read all bands of all training rasters into a stacked Numpy array + - Read all bands of all feature/evidence rasters into a stacked Numpy array - Read label data (and rasterize if a vector file is given) - - Create a nodata mask using all training rasters and labels, and mask nodata cells out + - Create a nodata mask using all feature rasters and labels, and mask nodata cells out Args: - training_raster_files: List of filepaths of training rasters. Files should only include + feature_raster_files: List of filepaths of feature/evidence rasters. Files should only include raster that have the same grid properties and extent. label_file: Filepath to label (deposits) data. File can be either a vector file or raster file. - If a vector file is provided, it will be rasterized into similar grid as training rasters. If - a raster file is provided, it needs to have same grid properties and extent as training rasters. + If a vector file is provided, it will be rasterized into similar grid as feature rasters. If + a raster file is provided, it needs to have same grid properties and extent as feature rasters. Optional parameter and can be omitted if preparing data for predicting. Defaults to None. Returns: - Training data (X) in prepared shape, target labels (y) in prepared shape (if `label_file` was given), - refrence raster metadata and nodata mask applied to X and y. + Feature data (X) in prepared shape. + Target labels (y) in prepared shape (if `label_file` was given). + Refrence raster metadata . + Nodata mask applied to X and y. + + Raises: + NonMatchingRasterMetadataException: Input feature rasters don't have same grid properties. """ - def _read_and_stack_training_raster(filepath: Union[str, os.PathLike]) -> Tuple[np.ndarray, dict]: - """Read all bands of raster file with training data in a stack.""" + def _read_and_stack_feature_raster(filepath: Union[str, os.PathLike]) -> Tuple[np.ndarray, dict]: + """Read all bands of raster file with feature/evidence data in a stack.""" with rasterio.open(filepath) as src: raster_data = np.stack([src.read(i) for i in range(1, src.count + 1)]) profile = src.profile return raster_data, profile - # Read and stack training rasters - training_data, profiles = zip(*[_read_and_stack_training_raster(file) for file in training_raster_files]) - # # TODO. Waiting for check_raster_grids input modification to profiles - # if not check_raster_grids(profiles, same_extent=True): - # raise NonMatchingRasterGridException + # Read and stack feature rasters + feature_data, profiles = zip(*[_read_and_stack_feature_raster(file) for file in feature_raster_files]) + if not check_raster_grids(profiles, same_extent=True): + raise NonMatchingRasterMetadataException("Input feature rasters should have same grid properties.") + reference_profile = profiles[0] nodata_values = [profile["nodata"] for profile in profiles] - # Reshape training data for ML and create mask + # Reshape feature rasters for ML and create mask reshaped_data = [] nodata_mask = None - for raster, nodata in zip(training_data, nodata_values): + for raster, nodata in zip(feature_data, nodata_values): raster_reshaped = raster.reshape(raster.shape[0], -1).T reshaped_data.append(raster_reshaped) diff --git a/eis_toolkit/raster_processing/reclassify.py b/eis_toolkit/raster_processing/reclassify.py index f79c2ea4..f56b79ee 100644 --- a/eis_toolkit/raster_processing/reclassify.py +++ b/eis_toolkit/raster_processing/reclassify.py @@ -6,6 +6,7 @@ from beartype import beartype from beartype.typing import Optional, Sequence, Tuple +from eis_toolkit.exceptions import InvalidParameterValueException, InvalidRasterBandException from eis_toolkit.utilities.checks.raster import check_raster_bands @@ -35,16 +36,19 @@ def reclassify_with_manual_breaks( # type: ignore[no-any-unimported] bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with manual breaks and metadata. + Raster data classified with manual breaks. + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. """ + # Add check for input breaks at some point? if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() @@ -84,16 +88,22 @@ def reclassify_with_defined_intervals( # type: ignore[no-any-unimported] bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with defined intervals and metadata. + Raster data classified with defined intervals. + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. + InvalidParameterValueException: Interval size is less than 1. """ if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") + + if interval_size < 1: + raise InvalidParameterValueException("Interval size must be 1 or more.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() @@ -135,16 +145,22 @@ def reclassify_with_equal_intervals( # type: ignore[no-any-unimported] bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with equal intervals. + Raster data classified with equal intervals. + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. + InvalidParameterValueException: Number of intervals is less than 2. """ if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") + + if number_of_intervals < 2: + raise InvalidParameterValueException("Number of intervals must be 2 or more.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() @@ -183,16 +199,22 @@ def reclassify_with_quantiles( # type: ignore[no-any-unimported] bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with quantiles and metadata. + Raster data classified with quantiles. + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. + InvalidParameterValueException: Number of quantiles is less than 2. """ if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") + + if number_of_quantiles < 2: + raise InvalidParameterValueException("Number of quantiles must be 2 or more.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() @@ -231,16 +253,22 @@ def reclassify_with_natural_breaks( # type: ignore[no-any-unimported] bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with natural breaks (Jenks Caspall) and metadata. + Raster data classified with natural breaks (Jenks Caspall). + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. + InvalidParameterValueException: Number of classes is less than 2. """ if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") + + if number_of_classes < 2: + raise InvalidParameterValueException("Number of classes must be 2 or more.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() @@ -323,20 +351,26 @@ def reclassify_with_geometrical_intervals( # type: ignore[no-any-unimported] Args: raster: Raster to be classified. number_of_classes: The number of classes. The true number of classes is at most double the amount, - depending how symmetrical the input data is. + depending how symmetrical the input data is. bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with geometrical intervals and metadata. + Raster data classified with geometrical intervals. + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. + InvalidParameterValueException: Number of classes is less than 2. """ if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") + + if number_of_classes < 2: + raise InvalidParameterValueException("Number of classes must be 2 or more.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() @@ -392,16 +426,22 @@ def reclassify_with_standard_deviation( # type: ignore[no-any-unimported] bands: Selected bands from multiband raster. Indexing begins from one. Defaults to None. Returns: - Raster classified with standard deviation and metadata. + Raster data classified with standard deviation. + Raster metadata. Raises: - InvalidParameterValueException: Bands contain negative values. + InvalidRasterBandException: All selected bands are not contained in the input raster. + InvalidParameterValueException: Number of intervals is less than 2. """ if bands is None or len(bands) == 0: bands = range(1, raster.count + 1) else: - check_raster_bands(raster, bands) + if not check_raster_bands(raster, bands): + raise InvalidRasterBandException(f"Input raster does not contain all selected bands: {bands}.") + + if number_of_intervals < 2: + raise InvalidParameterValueException("Number of intervals must be 2 or more.") out_image = np.empty((len(bands), raster.height, raster.width)) out_meta = raster.meta.copy() diff --git a/notebooks/autoencoder_convolutional_geophysical.ipynb b/notebooks/autoencoder_convolutional_geophysical.ipynb new file mode 100755 index 00000000..e6a49a6f --- /dev/null +++ b/notebooks/autoencoder_convolutional_geophysical.ipynb @@ -0,0 +1,1691 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0b81422b-ec75-48ad-a51e-22fd2ed4f67f", + "metadata": {}, + "source": [ + "## Data preprocessing" + ] + }, + { + "cell_type": "markdown", + "id": "6a637bb4-ed80-4be3-9323-3f7412a1af9d", + "metadata": {}, + "source": [ + "### Loading libraries and defining functions" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "e7853fb5-58bd-4f75-b639-6ed55a6cfe56", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import os\n", + "from skimage.io import imread, imsave\n", + "import matplotlib.pyplot as plt\n", + "import tensorflow as tf\n", + "from tensorflow.keras.layers import Input, Layer, Conv2D, MaxPooling2D, UpSampling2D, Dense, Dropout, Lambda, Flatten, Reshape, Conv2DTranspose, Activation, concatenate, Multiply, Add, BatchNormalization\n", + "from tensorflow.keras.optimizers import Adam\n", + "from keras.models import Model\n", + "from tensorflow.keras.callbacks import LearningRateScheduler\n", + "from tensorflow.keras.preprocessing.image import ImageDataGenerator\n", + "from keras.callbacks import EarlyStopping\n", + "from sklearn.manifold import TSNE\n", + "import keras\n", + "from keras.metrics import binary_crossentropy \n", + "from keras import backend as K\n", + "from skimage.metrics import structural_similarity as ssim\n", + "from skimage import exposure\n", + "from keras.regularizers import l1, l2\n", + "from keras.losses import mean_squared_error\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "\"\"\"Different types of normalizers for variable data shapes, including data with very small (-1e+30) NaN values\"\"\"\n", + "\n", + "def normalize_tile_z(tile):\n", + " \"\"\"Normalize the tile using its mean and standard deviation.\"\"\"\n", + " mean = np.nanmean(tile)\n", + " std = np.nanstd(tile)\n", + " \n", + " # Return the standardized tile\n", + " return (tile - mean) / (std + 1e-7)\n", + "\n", + "def normalize_tile_one(tile):\n", + " valid_mask = ~np.isnan(tile)\n", + " \n", + " # Check if there's any valid data in the tile\n", + " if np.any(valid_mask):\n", + " min_val = np.min(tile[valid_mask])\n", + " max_val = np.max(tile[valid_mask])\n", + " \n", + " if max_val != min_val:\n", + " tile[valid_mask] = (tile[valid_mask] - min_val) / (max_val - min_val)\n", + " else:\n", + " tile[valid_mask] = 0.0 # If max equals min, set to 0\n", + " return tile\n", + "\n", + "def normalize_tile_adaptive(tile, method=\"minmax\"):\n", + " if method == \"standard\":\n", + " mean = np.nanmean(tile)\n", + " std = np.nanstd(tile)\n", + " return (tile - mean) / (std + 1e-7)\n", + " elif method == \"minmax\":\n", + " min_val = np.nanmin(tile)\n", + " max_val = np.nanmax(tile)\n", + " return (tile - min_val) / (max_val - min_val)\n", + "\n", + "def normalize_tile_clipping(tile):\n", + " \"\"\"Normalize the tile using its mean and standard deviation.\"\"\"\n", + " # Clip the tile values\n", + " lower_threshold = np.nanpercentile(tile, 1)\n", + " upper_threshold = np.nanpercentile(tile, 99)\n", + " tile = np.clip(tile, lower_threshold, upper_threshold)\n", + " \n", + " # Normalize the tile\n", + " mean = np.nanmean(tile)\n", + " std = np.nanstd(tile)\n", + " \n", + " return (tile - mean) / (std + 1e-7)\n", + "\n", + "def normalize_tile_clipping_255(tile):\n", + " \"\"\"Normalize the tile using its mean and standard deviation and clip it based on the range [mean-2*std, mean+2*std].\"\"\"\n", + "\n", + " mean_tile = np.nanmean(tile)\n", + " std_tile = np.nanstd(tile)\n", + "\n", + " # Clipping the tile based on the standard deviation and mean\n", + " np.clip(tile, mean_tile - 2 * std_tile, mean_tile + 2 * std_tile, out=tile)\n", + "\n", + " # Normalize to 0-255\n", + " tile = ((tile - np.nanmin(tile)) / (np.nanmax(tile) - np.nanmin(tile))) * 255\n", + "\n", + " return tile.astype(np.uint8)\n", + "\n", + "def adaptive_clip_and_scale(tile, factor=2):\n", + " \"\"\"\n", + " Perform adaptive clipping and scaling of the tile.\n", + "\n", + " Parameters:\n", + " - tile: The image tile to be processed.\n", + " - factor: Multiplier for std deviation to compute clipping bounds.\n", + "\n", + " Returns:\n", + " - Processed tile with values between 0 and 255.\n", + " \"\"\"\n", + " \n", + " # If the tile consists only of NaNs, return an all-zero tile.\n", + " if np.all(np.isnan(tile)):\n", + " return np.zeros_like(tile, dtype=np.uint8)\n", + "\n", + " # Calculate local statistics\n", + " mean = np.nanmean(tile)\n", + " std = np.nanstd(tile)\n", + "\n", + " # Check if the standard deviation is 0, if so return a tile filled with mean value scaled to [0,255]\n", + " if std == 0:\n", + " scaled_mean = ((mean - np.nanmin(tile)) / (np.nanmax(tile) - np.nanmin(tile)) * 255).astype(np.uint8)\n", + " return np.full_like(tile, scaled_mean, dtype=np.uint8)\n", + "\n", + " # Define bounds for clipping\n", + " lower_bound = mean - factor * std\n", + " upper_bound = mean + factor * std\n", + "\n", + " # Clip values\n", + " tile_clipped = np.clip(tile, lower_bound, upper_bound)\n", + "\n", + " # Scale values to [0, 255]\n", + " min_val = np.min(tile_clipped)\n", + " max_val = np.max(tile_clipped)\n", + " \n", + " # Check if after clipping, max is less than or equal to min\n", + " if max_val <= min_val:\n", + " return np.full_like(tile, 127, dtype=np.uint8) # Return a gray tile\n", + "\n", + " tile_scaled_0_1 = (tile_clipped - min_val) / (max_val - min_val)\n", + " tile_scaled_0_255 = (tile_scaled_0_1 * 255).astype(np.uint8)\n", + "\n", + " return tile_scaled_0_255\n", + "\n", + "def clip_and_scale(tile, global_mean, global_std, factor=2):\n", + " \"\"\"\n", + " Perform clipping based on global statistics and scale the tile.\n", + "\n", + " Parameters:\n", + " - tile: The image tile to be processed.\n", + " - global_mean: Mean of the entire dataset.\n", + " - global_std: Standard deviation of the entire dataset.\n", + " - factor: Multiplier for std deviation to compute clipping bounds.\n", + "\n", + " Returns:\n", + " - Processed tile with values between 0 and 255.\n", + " \"\"\"\n", + " \n", + " # Define bounds for clipping based on global statistics\n", + " lower_bound = global_mean - factor * global_std\n", + " upper_bound = global_mean + factor * global_std\n", + "\n", + " # Clip values\n", + " tile_clipped = np.clip(tile, lower_bound, upper_bound)\n", + "\n", + " # Scale values to [0, 255]\n", + " min_val = np.min(tile_clipped)\n", + " max_val = np.max(tile_clipped)\n", + " \n", + " if max_val <= min_val:\n", + " return np.full_like(tile, 127, dtype=np.uint8) # Return a gray tile\n", + "\n", + " tile_scaled_0_1 = (tile_clipped - min_val) / (max_val - min_val)\n", + " tile_scaled_0_255 = (tile_scaled_0_1 * 255).astype(np.uint8)\n", + "\n", + " return tile_scaled_0_255\n", + "\n", + "def extract_tiles(img, tile_size):\n", + " images_list = []\n", + "\n", + " # Calculate the number of tiles needed in both dimensions\n", + " n_tiles_x = img.shape[0] // tile_size\n", + " n_tiles_y = img.shape[1] // tile_size\n", + "\n", + " # Pad the image if necessary to extract tiles\n", + " pad_width_x = tile_size - (img.shape[0] % tile_size) if img.shape[0] % tile_size != 0 else 0\n", + " pad_width_y = tile_size - (img.shape[1] % tile_size) if img.shape[1] % tile_size != 0 else 0\n", + "\n", + " img = np.pad(img, ((0, pad_width_x), (0, pad_width_y)), mode='constant')\n", + "\n", + " for i in range(0, img.shape[0], tile_size):\n", + " for j in range(0, img.shape[1], tile_size):\n", + " tile = img[i:i+tile_size, j:j+tile_size]\n", + " images_list.append(tile)\n", + "\n", + " return np.array(images_list)\n", + "\n", + "\n", + "def load_and_tile_images_from_folder(folder_path, tile_size=64):\n", + " images_list = []\n", + " for filename in sorted(os.listdir(folder_path)): # Sorting ensures images from different folders align properly\n", + " if filename.endswith('.tif'):\n", + " img = imread(os.path.join(folder_path, filename))\n", + " \n", + " images_list.extend(extract_tiles(img, tile_size))\n", + "\n", + " return np.stack(images_list, axis=0) #np.array(images_list)\n", + "\n", + "\n", + "datagen = ImageDataGenerator(\n", + " rotation_range=40,\n", + " width_shift_range=0.2,\n", + " height_shift_range=0.2,\n", + " zoom_range=0.2,\n", + " horizontal_flip=True,\n", + " fill_mode='constant',\n", + " cval=0\n", + ")\n", + "\n", + "datagen_adjuste = ImageDataGenerator(\n", + " rotation_range=40,\n", + " width_shift_range=0.2,\n", + " height_shift_range=0.2,\n", + " zoom_range=0.2,\n", + " horizontal_flip=True,\n", + " fill_mode='constant',\n", + " cval=0,\n", + " brightness_range=[0.5, 1.5], # adjust brightness\n", + " channel_shift_range=0.2 # shift channel values\n", + ")\n", + "\n", + "def augment_images_together(*image_batches):\n", + " # Assuming all image_batches are of the same length, \n", + " # and images in position i of each batch correspond to each other\n", + " augmented_batches = [[] for _ in range(len(image_batches))]\n", + " \n", + " for i in range(len(image_batches[0])):\n", + " combined_image = np.stack([image_batches[j][i] for j in range(len(image_batches))], axis=-1) # stack along the channel dimension\n", + " augmentation_iterator = datagen.flow(np.expand_dims(combined_image, axis=0), batch_size=1)\n", + " augmented_image = augmentation_iterator[0].squeeze(axis=0)\n", + " \n", + " for j in range(len(image_batches)):\n", + " augmented_batches[j].append(augmented_image[..., j]) # separate the channels\n", + " \n", + " return [np.array(batch) for batch in augmented_batches]\n", + "\n", + "def handle_missing_values(images):\n", + " \"\"\"Handle NaN and extremely small values in the dataset.\"\"\"\n", + " \n", + " # Check for NaN values\n", + " has_nan = np.isnan(images).any()\n", + " print(f\"Dataset contains NaN values: {has_nan}\")\n", + " images[np.isnan(images)] = 0.0 # replace NaN with 0\n", + " \n", + " # Handle 'no-data' or extremely small values\n", + " no_data_mask = images < -1e+29\n", + " print(f\"Number of 'no-data' values: {np.sum(no_data_mask)}\")\n", + " #images[no_data_mask] = 0.0\n", + " \n", + " mean_val = np.mean(images[~no_data_mask])\n", + " images[no_data_mask] = mean_val\n", + " \n", + " return images\n", + "\n", + "def replace_nans(image, value=0):\n", + " \"\"\"Replace NaN values in the image with the specified value.\"\"\"\n", + " nan_mask = np.isnan(image)\n", + " image[nan_mask] = value\n", + " return image\n", + "\n", + "def replace_nans_with_mean(image):\n", + " \"\"\"Replace NaN values in the image with the mean of the non-NaN values.\"\"\"\n", + " nan_mask = np.isnan(image)\n", + " image[nan_mask] = np.nanmean(image)\n", + " return image\n", + "\n", + "def handle_nans_and_infs(image):\n", + " \"\"\"Replace NaN and Inf values in an image.\"\"\"\n", + " non_finite_mask = ~np.isfinite(image) # True for NaN and Inf values\n", + " mean_val = np.mean(image[np.isfinite(image)]) # mean of finite values\n", + " image[non_finite_mask] = mean_val\n", + " return image\n", + "\n", + "def normalize_images_old(images):\n", + " return images.astype('float32') / 255.0\n", + "\n", + "def remove_nan(img_np):\n", + " # handle null values\n", + " img_np[img_np < -1e+30] = np.nan \n", + "\n", + " return img_np\n", + "\n", + "# To avoid division by zero error, when max_val == min_val\n", + "def normalize_tile(tile):\n", + " min_val = np.min(tile)\n", + " max_val = np.max(tile)\n", + " if max_val != min_val:\n", + " tile = ((tile - min_val) / (max_val - min_val)) * 255\n", + " else:\n", + " tile = np.zeros_like(tile)\n", + "\n", + " return tile\n", + "\n", + "def equalize_images(images):\n", + " return exposure.equalize_hist(images) * 255\n", + "\n", + "def contrast_stretching(image, new_min=0, new_max=1):\n", + " \"\"\"Perform contrast stretching on the given image.\"\"\"\n", + " return (image - np.min(image)) / (np.max(image) - np.min(image)) * (new_max - new_min) + new_min\n", + "\n", + "def contrast_stretching_consider_uniform_values(image, new_min=0, new_max=1, epsilon=1e-7):\n", + " \"\"\"Perform contrast stretching on the given image.\"\"\"\n", + " min_val = np.min(image)\n", + " max_val = np.max(image)\n", + "\n", + " # Check if the max and min values are very close or identical\n", + " if np.isclose(max_val, min_val, atol=epsilon):\n", + " return np.ones_like(image) * new_min # or any other default behavior\n", + "\n", + " return (image - min_val) / (max_val - min_val + epsilon) * (new_max - new_min) + new_min\n", + "\n", + "\n", + "def apply_preprocessing(images_list):\n", + " \"\"\"Apply preprocessing techniques to a list of images.\"\"\"\n", + " processed_images = []\n", + " \n", + " for image in images_list:\n", + " # Handle NaN values\n", + " image = handle_nans_and_infs(image)\n", + " \n", + " # Apply contrast stretching\n", + " image = contrast_stretching_consider_uniform_values(image)\n", + " \n", + " # Apply histogram equalization\n", + " equalized_image = exposure.equalize_hist(image) * 255\n", + " \n", + " processed_images.append(equalized_image)\n", + " \n", + " return processed_images\n", + "\n", + "def normalize_images(images):\n", + " min_val = np.min(images)\n", + " max_val = np.max(images)\n", + " return (images - min_val) / (max_val - min_val)\n", + "\n", + "# Create a datagenerator\n", + "def create_datagenerator(rotation_range=20, width_shift_range=0.1, eight_shift_range=0.1, zoom_range=0.1, horizontal_flip=True, fill_mode='constant', cval=0,brightness_range=[0.5, 1.5], channel_shift_range=0.2):\n", + " datagen = ImageDataGenerator(\n", + " rotation_range=rotation_range,\n", + " width_shift_range=width_shift_range,\n", + " height_shift_range=height_shift_range,\n", + " zoom_range=zoom_range,\n", + " horizontal_flip=horizontal_flip,\n", + " fill_mode=fill_mode,\n", + " cval=cval,\n", + " brightness_range=brightness_range, # adjust brightness\n", + " channel_shift_range=channel_shift_range # shift channel values\n", + " )\n", + " \n", + " return datagen\n", + "\n", + "def augment_image_dataset(image_list, augment_times=5):\n", + " # Initialize the ImageDataGenerator with the desired augmentations\n", + " datagen = ImageDataGenerator(\n", + " rotation_range=40,\n", + " width_shift_range=0.2,\n", + " height_shift_range=0.2,\n", + " zoom_range=0.2,\n", + " horizontal_flip=True,\n", + " fill_mode='constant',\n", + " cval=0\n", + " )\n", + "\n", + " datagen_adjusted = ImageDataGenerator(\n", + " rotation_range=20,\n", + " #width_shift_range=0.1,\n", + " #height_shift_range=0.1,\n", + " #zoom_range=0.1,\n", + " horizontal_flip=True,\n", + " fill_mode='constant',\n", + " cval=0,\n", + " brightness_range=[0.5, 1.5], # adjust brightness\n", + " channel_shift_range=0.2 # shift channel values\n", + " )\n", + " \n", + " augmented_images = []\n", + "\n", + " for image in image_list:\n", + " #augmentation_iterator = datagen.flow(np.expand_dims(image, axis=0), batch_size=1)\n", + " augmentation_iterator = datagen_adjusted.flow(np.expand_dims(image, axis=0), batch_size=1)\n", + " \n", + " for j in range(augment_times):\n", + " augmented_images.append(augmentation_iterator[0].squeeze(axis=0))\n", + "\n", + " return np.array(augmented_images)\n", + "\n", + "# Clip the dataset length to the shortest dataset (recommended to use data augmentation first, to not lose data)\n", + "def prepare_multimodal_dataset(augmented_AEM_images, augmented_Gravity_images, augmented_Radiometric_images, augmented_Magnetic_images):\n", + " # Find the smallest length\n", + " min_length = min([len(x) for x in [augmented_AEM_images, augmented_Gravity_images, augmented_Radiometric_images, augmented_Magnetic_images]])\n", + " \n", + " # Trim datasets to match the smallest length\n", + " x1_trimmed = augmented_AEM_images[:min_length]\n", + " x2_trimmed = augmented_Gravity_images[:min_length]\n", + " x3_trimmed = augmented_Radiometric_images[:min_length]\n", + " x4_trimmed = augmented_Magnetic_images[:min_length]\n", + " \n", + " combined_data = np.stack((x1_trimmed, x2_trimmed, x3_trimmed, x4_trimmed), axis=-1)\n", + "\n", + " return combined_data\n", + "\n", + "# Delete failed augmentations or null images, that are extracted from the border of the original image\n", + "def delete_null_or_1_value_images(data, min_value=1):\n", + " removed = 0\n", + " indices_to_remove = [i for i in range(len(data)) if len(np.unique(data[i])) <= min_value]\n", + " removed = len(indices_to_remove)\n", + " X = np.delete(data, indices_to_remove, axis=0)\n", + "\n", + " print(\"Removed \", removed) \n", + "\n", + " return X" + ] + }, + { + "cell_type": "markdown", + "id": "5b684f22-cfc9-436f-b511-7daed9cbb75f", + "metadata": {}, + "source": [ + "### Load your data" + ] + }, + { + "cell_type": "markdown", + "id": "76a3f8c5-bd7e-4ae6-9324-dba0c23e67b5", + "metadata": {}, + "source": [ + "Here, geophysical images were used (AEM, Gravity, Radiometric, Magnetic)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "c385f84d-b2fc-4eee-b9a5-64803d4df5e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(2288, 64, 64)\n", + "(9724, 64, 64)\n", + "(2288, 64, 64)\n", + "(6864, 64, 64)\n", + "Dataset contains NaN values: False\n", + "Number of 'no-data' values: 1171204\n", + "Dataset contains NaN values: False\n", + "Number of 'no-data' values: 6623948\n", + "Dataset contains NaN values: False\n", + "Number of 'no-data' values: 0\n", + "Dataset contains NaN values: False\n", + "Number of 'no-data' values: 391198\n" + ] + } + ], + "source": [ + "# Load all images from each category and stack them\n", + "\n", + "#AEM_folder = ...\n", + "#Gravity_folder = ...\n", + "#Radiometric_folder = ... \n", + "#Magnetic_folder = ...\n", + "\n", + "AEM_images = load_and_tile_images_from_folder(AEM_folder, 64)\n", + "Gravity_images = load_and_tile_images_from_folder(Gravity_folder, 64)\n", + "Radiometric_images = load_and_tile_images_from_folder(Radiometric_folder, 64)\n", + "Magnetic_images = load_and_tile_images_from_folder(Magnetic_folder, 64)\n", + "\n", + "augmented_AEM_images, augmented_Gravity_images, augmented_Radiometric_images, augmented_Magnetic_images = AEM_images, Gravity_images, Radiometric_images, Magnetic_images\n", + "\n", + "print(augmented_AEM_images.shape)\n", + "print(augmented_Gravity_images.shape)\n", + "print(augmented_Radiometric_images.shape)\n", + "print(augmented_Magnetic_images.shape)\n", + "\n", + "augmented_AEM_images = handle_missing_values(augmented_AEM_images)\n", + "augmented_Gravity_images = handle_missing_values(augmented_Gravity_images)\n", + "augmented_Radiometric_images = handle_missing_values(augmented_Radiometric_images)\n", + "augmented_Magnetic_images = handle_missing_values(augmented_Magnetic_images)\n", + "\n", + "augmented_AEM_images = apply_preprocessing(augmented_AEM_images)\n", + "augmented_Gravity_images = apply_preprocessing(augmented_Gravity_images)\n", + "augmented_Radiometric_images = apply_preprocessing(augmented_Radiometric_images)\n", + "augmented_Magnetic_images = apply_preprocessing(augmented_Magnetic_images)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9e3792ee-f2b0-4251-bdee-0d4d64c451e4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Removed 676\n", + "Removed 0\n", + "Range for x_train: 0.0, 1.0\n", + "Range for x_test: 0.0, 1.0\n" + ] + } + ], + "source": [ + "\"\"\"Check data range\"\"\"\n", + "\n", + "# Transform the data to numpy array (if not already, change type to float32, normalize to [0, 1], split the data to train and test sets)\n", + "def process_for_training(input, split_size=0.9):\n", + " images = np.array(input)\n", + " images = images.astype('float32') / 255.\n", + " x_train, x_test = train_test_split(images, test_size=1-split_size)\n", + "\n", + " return x_train, x_test\n", + "\n", + "data = np.array(augmented_Radiometric_images)\n", + "data = data.reshape(-1, 64, 64, 1)\n", + "\n", + "data = delete_null_or_1_value_images(data, 3)\n", + "\n", + "data = augment_image_dataset(data, 3)\n", + "\n", + "data = delete_null_or_1_value_images(data, 3)\n", + "\n", + "x_train, x_test = process_for_training(data, 0.95)\n", + "\n", + "datasets = [x_train, x_test]\n", + "dataset_names = [\"x_train\", \"x_test\"]\n", + "\n", + "for d, name in zip(datasets, dataset_names):\n", + " min_val = np.min(d)\n", + " max_val = np.max(d)\n", + " print(f\"Range for {name}: {min_val}, {max_val}\")" + ] + }, + { + "cell_type": "markdown", + "id": "048dde14-971a-4d5f-9361-7a0b94fa1b70", + "metadata": {}, + "source": [ + "### Visualize data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3945e6ef-9da7-4f0e-a578-f937c586ac11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(4594, 64, 64, 1) (242, 64, 64, 1)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAIGCAYAAAC7ycYTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAACs1ElEQVR4nOzdeZRcd33m/0/1vlfvi/bNkizvFtiIHceDMYSwOAlwYMCEkDADJMHMZHBCCCHMOAlzgCw2ZBLHZgKOZ+CwHCfsDjYBbDA2xqtk2ZIldbd637eq7qr6/ZGfNTbW85R01a3ult+vc/oc6Mf33u+93/V+u9SdKhQKhQAAAAAAAAAAAM9SstwFAAAAAAAAAABgpWITHQAAAAAAAAAAgU10AAAAAAAAAAAENtEBAAAAAAAAABDYRAcAAAAAAAAAQGATHQAAAAAAAAAAgU10AAAAAAAAAAAENtEBAAAAAAAAABDYRAcAAAAAAAAAQGATHcBpkUql4qMf/ehyFwMAAJwg5m4AAFauq6++OjZt2rTcxQCeM9hEB1axo0ePxoc+9KF4xSteEfX19ZFKpeKOO+5IdK6vf/3rvCgDALDEbr/99viN3/iN2L59e9TU1MSWLVviN3/zN+Po0aMnfS7mbgAAVq7e3t746Ec/Gvfff/9yFwXAIkgVCoXCchcCQDJ33HFHvOIVr4izzjorWltb46677orvfe978fKXv/ykz/W+970vrr/++liqIWFubi7KysqirKxsSc4PAMBq8LznPS9GRkbi137t1+Kss86KAwcOxN/8zd9ETU1N3H///dHZ2XnC52LuBgBg5frpT38az3/+8+Omm26Kq6++etHPPz8/H/l8PiorKxf93ACejRUxsIrt3r07hoeHo7m5Ob70pS/Fr/3ar52W6y4sLEQ+n4+KiooTPqaqqmoJSwQAwOrwyU9+Ml784hdHScn/+wehr3rVq+JlL3tZ/M3f/E18/OMfX5LrMncDALCyzczMRE1NzQn/9+Xl5UtYGgC/iF/nAqxAs7OzsXPnzti5c2fMzs4e+/7IyEh0dXXFC1/4wsjlclFfXx/Nzc2nfL2rr746rr/++oj4999/+tRXRMSTTz4ZqVQq/uf//J/x6U9/OrZu3RqVlZXxyCOPRDabjY985COxe/fuSKfTUVtbGy95yUvie9/73rOu8Yu/V/WjH/1opFKpePzxx+Pqq6+OxsbGSKfT8c53vjNmZmZO+Z4AADidTnTufulLX/qMDfSIiJe+9KXR3Nwcjz766Alfj7kbAICl8b3vfS9SqVR85StfeVZ2yy23RCqVirvuusue44477ojnP//5ERHxzne+89g8ffPNN0dExMtf/vI499xz4957742XvvSlUVNTE3/wB38QERFf+9rX4jWveU2sWbMmKisrY+vWrfGnf/qnkcvlnnGNX/yd6E+f///X//pfx+b/5z//+XHPPfecwhMBEMEn0YEVqbq6Oj73uc/Fi170ovjDP/zD+OQnPxkREe9973tjfHw8br755igtLV206/32b/929Pb2xne+8534x3/8x+P+NzfddFPMzc3Fb/3Wb0VlZWU0NzfHxMRE/P3f/3285S1viXe/+90xOTkZN954Y1xxxRXxk5/8JC688MKi1/71X//12Lx5c1x33XVx3333xd///d9He3t7/Pmf//mi3R8AAEvtVObuqampmJqaitbW1hO+HnM3AABL4+Uvf3msX78+vvCFL8Qb3vCGZ2Rf+MIXYuvWrbFnzx57jrPPPjs+9rGPxUc+8pH4rd/6rXjJS14SEREvfOELj/03w8PDceWVV8ab3/zmeNvb3hYdHR0REXHzzTdHXV1dXHPNNVFXVxf/+q//Gh/5yEdiYmIiPvGJTxQt/y233BKTk5Px27/925FKpeIv/uIv4o1vfGMcOHCAT68Dp4BNdGCFuvTSS+P3f//348///M/jDW94Q/T398ett94an/70p2P79u2Leq09e/bE9u3b4zvf+U687W1vO+5/093dHY8//ni0tbUd+14ul4snn3zyGf80/N3vfnfs3Lkz/vqv/zpuvPHGote+6KKLnvHfDQ8Px4033siLOABg1Uk6d3/605+ObDYbb3rTm074WszdAAAsjVQqFW9729vik5/8ZIyPj0c6nY6IiMHBwfj2t78df/iHf1j0HB0dHXHllVfGRz7ykdizZ89x5+q+vr747Gc/G7/927/9jO/fcsstUV1dfez/v+c974n3vOc9ccMNN8THP/7xor8D/fDhw7F///5oamqKiIgdO3bE6173uvjWt74Vv/zLv1y07ACOj1/nAqxgH/3oR+Occ86Jd7zjHfGf//N/jpe97GXxO7/zO8tSlquuuuoZL+EREaWlpcdewvP5fIyMjMTCwkI873nP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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(x_train.shape, x_test.shape)\n", + "\n", + "# Visualize the images side by side\n", + "\n", + "plt.figure(figsize=(15, 5))\n", + "\n", + "j = 66\n", + "\n", + "plt.subplot(1, 3, 1)\n", + "plt.imshow(x_train[j], cmap='gray')\n", + "plt.title('x1_train')\n", + "\n", + "plt.subplot(1, 3, 2)\n", + "plt.imshow(x_train[j+1, :, :], cmap='gray')\n", + "plt.title('x2_train')\n", + "\n", + "plt.subplot(1, 3, 3)\n", + "plt.imshow(x_train[j+2, :, :], cmap='gray')\n", + "plt.title('y_train')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5506bf54-3541-4159-9293-630429fa8540", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Min: 0.0\n", + "Max: 0.9882353\n", + "Mean: 0.44932503\n", + "Standard Deviation: 0.3045543\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAMWCAYAAAAgRDUeAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAACwQElEQVR4nOzdeXhU5fn/8U8SSQIJSQiQhCUsorILFQrGXRuNlqJ8pS0uX0DqXoJCrFUqiLiAK2JrhKoIXaRY/VZrBUGaitYaRKL8qiAoi4JAAohJIJAEkvP7wysjw7kHJiGZnGTer+uaS3PPc855zjLhzJ3n3E+E4ziOAAAAAAAAgBCKbOwOAAAAAAAAIPyQlAIAAAAAAEDIkZQCAAAAAABAyJGUAgAAAAAAQMiRlAIAAAAAAEDIkZQCAAAAAABAyJGUAgAAAAAAQMiRlAIAAAAAAEDIkZQCAAAAAABAyJGUAtBounXrpuuuu66xuwEAAAAAaAQkpQDUmwULFigiIkKrV68237/gggvUr1+/E9rGkiVLdN99953QOgAAAMLRpk2bdPPNN+vkk09WbGysEhISdPbZZ+upp57SwYMHJX33R8OIiAhFREQoMjJSSUlJ6t+/v2666SZ98MEH5npr2h/9SktLC+XuAWiCTmrsDgAIXxs2bFBkZO1y40uWLFFubi6JKQAAgFpYvHixfvaznykmJkZjxoxRv379VFlZqffee0933nmn1q5dq2effVaSNHDgQN1xxx2SpH379umzzz7Tyy+/rOeee06TJk3SrFmzXOu/+OKLNWbMGL9Yy5YtG37HADRpJKUANJqYmJjG7kKtlZWVKS4urrG7AQAAELQtW7boqquuUteuXfWvf/1LHTp08L03fvx4bdy4UYsXL/bFOnXqpP/93//1W8cjjzyia665Rk8++aROPfVU3XrrrX7vn3baaa5lAOB4eHwPQKM5uqbUoUOHNH36dJ166qmKjY1V27Ztdc4552j58uWSpOuuu065ubmS/IeJ1ygrK9Mdd9yh9PR0xcTEqGfPnnr88cflOI7fdg8ePKjbbrtN7dq1U+vWrXX55Zdr+/btioiI8BuBdd999ykiIkLr1q3TNddcozZt2uicc86RJP33v//Vdddd5xv+npaWpl/84hf65ptv/LZVs47PP/9c//u//6vExES1b99eU6dOleM42rZtm6644golJCQoLS1NTzzxRH0eYgAAAD366KPav3+/5s2b55eQqnHKKafo9ttvP+Y6WrZsqT/96U9KTk7WQw895Lq/AoC6YKQUgHpXUlKiPXv2uOKHDh065nL33XefZs6cqRtuuEFDhgxRaWmpVq9erY8++kgXX3yxbr75Zu3YsUPLly/Xn/70J79lHcfR5ZdfrrffflvXX3+9Bg4cqGXLlunOO+/U9u3b9eSTT/raXnfddfrrX/+q0aNH68wzz9Q777yjYcOGBezXz372M5166qmaMWOG7wZs+fLl2rx5s8aNG6e0tDTfkPe1a9dq5cqVfskySRo1apR69+6thx9+WIsXL9aDDz6o5ORk/f73v9dFF12kRx55RC+++KJ+9atf6Yc//KHOO++84x5nAACAYPzjH//QySefrLPOOuuE1hMfH6//+Z//0bx587Ru3Tr17dvX9155ebnr/q9169ZNcmQ8gBByAKCezJ8/35F0zFffvn197bt27eqMHTvW9/OAAQOcYcOGHXMb48ePd6xfXa+99pojyXnwwQf94j/96U+diIgIZ+PGjY7jOE5BQYEjyZk4caJfu+uuu86R5EybNs0XmzZtmiPJufrqq13bO3DggCv2l7/8xZHkvPvuu6513HTTTb7Y4cOHnc6dOzsRERHOww8/7It/++23TsuWLf2OCQAAwIkoKSlxJDlXXHFFUO27du16zPuxJ5980pHk/P3vf/fFAt33zZ8//wR7D6C5Y6QUgHqXm5ur0047zRW/4447VFVVFXC5pKQkrV27Vl988YVOPfXUWm1zyZIlioqK0m233eba5iuvvKI333xT2dnZWrp0qSTpl7/8pV+7CRMmaMGCBea6b7nlFlfsyMKd5eXl2r9/v84880xJ0kcffaRzzz3Xr/0NN9zg+/+oqCgNHjxYX3/9ta6//npfPCkpST179tTmzZuD2GMAAIDjKy0tlfTdqKX6EB8fL+m7AuhHuuKKK5Sdne0XO3IkFQBYSEoBqHdDhgzR4MGDXfE2bdqYj/XVuP/++3XFFVfotNNOU79+/XTppZdq9OjROv3004+7za+++kodO3Z03XD17t3b937NfyMjI9W9e3e/dqecckrAdR/dVpL27t2r6dOna9GiRdq1a5ffeyUlJa72Xbp08fs5MTFRsbGxateunSt+dF0qAACAukpISJDkTiLV1f79+yW5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Jm/hvslbuG/yFu6bvIX7pqYvJP+e11vJ9CZm0aJFTkxMjLNgwQJn3bp1zk033eQkJSU5hYWFjuM4zujRo527777b1/4///mPc9JJJzmPP/6489lnnznTpk1jauN6VNvz8fDDDzvR0dHOK6+84uzcudP32rdvX2PtQrNS2/NxNGaRqV+1PR9bt251Wrdu7WRnZzsbNmxw3njjDSclJcV58MEHG2sXmpXano9p06Y5rVu3dv7yl784mzdvdt566y2nR48ezs9//vPG2oVmY9++fc7HH3/sfPzxx44kZ9asWc7HH3/sfPXVV47jOM7dd9/tjB492te+ZlrjO++80/nss8+c3Nzcep/WuLnivslbuG/yFu6bvIX7Jm/hvslbvHjvFLZJKcdxnN/97ndOly5dnOjoaGfIkCHOypUrfe+df/75ztixY/3a//Wvf3VOO+00Jzo62unbt6+zePHiEPe4eavN+ejatasjyfWaNm1a6DveTNX283Ekbq7qX23Px/vvv+8MHTrUiYmJcU4++WTnoYcecg4fPhziXjdftTkfhw4dcu677z6nR48eTmxsrJOenu788pe/dL799tvQd7yZefvtt81/C2qO/9ixY53zzz/ftczAgQOd6Oho5+STT3bmz58f8n43Vdw3eQv3Td7CfZO3cN/kLdw3eYcX750iHIcxcAAAAAAAAAitsKwpBQAAAAAAgMZFUgoAAAAAAAAhR1IKAAAAAAAAIUdSCgAAAAAAACFHUgoAAAAAAAAhR1IKAAAAAAAAIUdSCgAAAAAAACFHUgoAAAAAAAAhR1IKAAAAAAAAIUdSCgAAAAAAACFHUgoAAAAAAAAhR1IKAAAAAAAAIUdSCgAAAAAAACFHUgoAAAAAAAAhR1IKAAAAAAAAIUdSCgAAAAAAACFHUgoAAAAAAAAhR1IKgM+qVasUHR2tr776qrG7Auibb75RXFyclixZ0thdAQAAANAASEoBx7FgwQJFRERo9erVjd2VBnfPPffo6quvVteuXRu7K42muLhYN910k9q3b6+4uDhdeOGF+uijj4Je/rPPPtOll16q+Ph4JScna/To0dq9e7erXXV1tR599FF1795dsbGxOv300/WXv/zlhNZp2b9/v6ZNm6ZLL71UycnJioiI0IIFC4LeH6l2x+T111/XGWecodjYWHXp0kXTpk3T4cOH67TOtm3b6oYbbtDUqVNr1V8AAAAATQNJKQCSpDVr1uif//ynbrnllsbuSqOprq7WsGHDtHDhQmVnZ+vRRx/Vrl27dMEFF+iLL7447vJff/21zjvvPG3cuFEzZszQr371Ky1evFgXX3yxKisr/drec889uuuuu3TxxRfrd7/7nbp06aJrrrlGixYtqvM6LXv27NH999+vzz77TAMGDKjdAVHtjsmbb76pESNGKCkpSb/73e80YsQIPfjgg5owYUKd13nLLbfoo48+0r/+9a9a9x0AAACAxzkAjmn+/PmOJOfDDz9s7K40qNtuu83p0qWLU11dfcx21dXVzoEDB0LUq9B66aWXHEnOyy+/7Ivt2rXLSUpKcq6++urjLn/rrbc6LVu2dL766itfbPny5Y4k5/e//70v9vXXXzstWrRwxo8f74tVV1c75557rtO5c2fn8OHDtV5nIOXl5c7OnTsdx3GcDz/80JHkzJ8//7jL1ajNMenTp48zYMAA59ChQ77YPffc40RERDifffZZndbpOI7Tr18/Z/To0UH3GQAAAEDTwEgpoA6uu+46xcfHa+vWrfrJT36i+Ph4derUSbm5uZKkTz75RBdddJHi4uLUtWtXLVy40G/5vXv36le/+pX69++v+Ph4JSQk6LLLLtP/+3//z7Wtr776Spdffrni4uKUkpKiSZMmadmyZYqIiNCKFSv82n7wwQe69NJLlZiYqFatWun888/Xf/7zn6D26bXXXtNFF12kiIgIv3i3bt30k5/8RMuWLdPgwYPVsmVL/f73v5ckzZ8/XxdddJFSUlIUExOjPn36aM6cOa5116zjvffe05AhQxQbG6uTTz5Zf/zjH11t//vf/+r8889Xy5Yt1blzZz344IOaP3++IiIi9OWXX/q1ffPNN3XuuecqLi5OrVu31rBhw7R27Vq/NocOHdL69eu1c+fO4x6DV155Rampqbryyit9sfbt2+vnP/+5/v73v6uiouKYy//f//2ffvKTn6hLly6+WGZmpk477TT99a9/9cX+/ve/69ChQ/rlL3/pi0VEROjWW2/V119/rfz8/FqvM5CYmBilpaUdt10gwR6TdevWad26dbrpppt00kkn+dr+8pe/lOM4euWVV2q9zhoXX3yx/vGPf8hxnDrvBwAAAADvISkF1FFVVZUuu+wypaen69FHH1W3bt2UnZ2tBQsW6NJLL9XgwYP1yCOPqHXr1hozZoy2bNniW3bz5s167bXX9JOf/ESzZs3SnXfeqU8++UTnn3++duzY4WtXVlamiy66SP/85z9122236Z577tH777+vu+66y9Wff/3rXzrvvPNUWlqqadOmacaMGSouLtZFF12kVatWHXNftm/frq1bt+qMM84w39+wYYOuvvpqXXzxxXrqqac0cOBASdKcOXPUtWtX/eY3v9ETTzyh9PR0/fKXv/Ql5460ceNG/fSnP9XFF1+sJ554Qm3atNF1113nl0Tavn27LrzwQq1du1aTJ0/WpEmT9OKLL+qpp55yre9Pf/qThg0bpvj4eD3yyCOaOnWq1q1bp3POOccvebV9+3b17t1bkydPPuYxkKSPP/5YZ5xxhiIj/X81DhkyRAcOHNDnn38ecNnt27dr165dGjx4sOu9IUOG6OOPP/bbTlxcnHr37u1qV/N+bdfZUII9JjV9ObqvHTt2VOfOnV37X5vjPGjQIBUXF7sSjgAAAACatpOO3wSApby8XP/7v//rS3Zcc8016tixo37xi1/oL3/5i0aNGiXpu1EevXr10h/+8Afdd999kqT+/fvr888/9/tSPnr0aPXq1Uvz5s3zFXb+/e9/70tgXXHFFZKkm2++WT/4wQ/8+uI4jm655RZdeOGFevPNN32jnW6++Wb17dtXU6ZM0VtvvRVwX9avXy9J6t69u/n+xo0btXTpUmVlZfnF33nnHbVs2dL3c3Z2ti699FLNmjVL48eP92u7YcMGvfvuuzr33HMlST//+c+Vnp6u+fPn6/HHH5ckPfLII/r222/10Ucf+RJf48aN06mnnuq3rv379+u2227TDTfcoGeffdYXHzt2rHr27KkZM2b4xYO1c+dOnXfeea54hw4dJEk7duxQ//79Ay57ZNujl9+7d68qKioUExOjnTt3KjU11TUq7cjt1HadDSXYY3K8vh6ZbK3tcT755JMlfTcaq1+/fiewNwAAAAC8hJFSwAm44YYbfP+flJSknj17Ki4uTj//+c998Z49eyopKUmbN2/2xWJiYnwJqaqqKn3zzTeKj49Xz549/WYgW7p0qTp16qTLL7/cF4uNjdWNN97o1481a9boiy++0DXXXKNvvvlGe/bs0Z49e1RWVqYf/ehHevfdd1VdXR1wP7755htJUps2bcz3u3fv7kpISfJLSJWUlGjPnj06//zztXnzZpWUlPi17dOnjy8hJX33uFbPnj39jsvSpUuVkZHhS0hJUnJysq699lq/dS1fvlzFxcW6+uqrffu6Z88eRUVFaejQoXr77bd9bbt16ybHcYKace7gwYNmgic2Ntb3/rGWlRTU8sFupzbrbCj11dcj+1nb41xzXe7Zs6cuuwAAAADAoxgpBdRRbGys2rdv7xdLTExU586dXSNgEhMT9e233/p+rq6u1lNPPaVnnnlGW7ZsUVVVle+9tm3b+v7/q6++Uo8ePVzrO+WUU/x+rpmxbOzYsQH7W1JSEjDpVCNQzZ5AI6j+85//aNq0acrPz9eBAwdc20tMTPT9fGRNpBpt2rTxOy5fffWVMjIyXO0C7e9FF11k9ishIcGMH0/Lli3NulHl5eW+94+1rKSglg92O8Gus6qqSrt37/Z7Pzk5WdHR0QH7G6z66uuRx662x7nmujz6cwAAAACgaSMpBdRRVFRUreJHJnxmzJihqVOn6he/+IUeeOABJScnKzIyUhMnTjzmiKZAapZ57LHH/EYZHSk+Pj7g8jWJsCMTREeykjGbNm3Sj370I/Xq1UuzZs1Senq6oqOjtWTJEj355JOu/QjmuASrZt1/+tOfzCLeRxbaro0OHTqYBdFrYh07djzmske2PXr55ORk3+igDh066O2335bjOH6JlqO3E+w6v/zyS1fi8O2339YFF1wQsL/BCvaYHNnX9PR0V9uaelm1WWeNmuuyXbt2dd0NAAAAAB5EUgpoBK+88oouvPBCzZs3zy9eXFzs98W7a9euWrdunSt5sXHjRr/levToIem7EUKZmZm17k+vXr0kya8Y+/H84x//UEVFhV5//XW/UVBHPjpXW127dnXtmxR4f1NSUuq0v4EMHDhQ//73v1VdXe1X7+uDDz5Qq1atdNpppwVctlOnTmrfvr1Wr17tem/VqlV+ycKBAwfq+eef12effaY+ffr4bafm/dqsMy0tTcuXL/d7f8CAAcfd32AEe0xq+rJ69Wq/BNSOHTv09ddf66abbqr1OmvUXJdHF4YHAAAA0LRRUwpoBFFRUa4RQi+//LK2b9/uF8vKytL27dv1+uuv+2Ll5eV67rnn/NoNGjRIPXr00OOPP679+/e7tnf0o11H69Spk9LT083kx7H2QfIf6VRSUqL58+cHvY6jZWVlKT8/X2vWrPHF9u7dqxdffNHVLiEhQTNmzNChQ4dc6zlyfw8dOqT169ebI3OO9tOf/lRFRUX629/+5ovt2bNHL7/8soYPH+5XB2nTpk3atGmT3/IjR47UG2+8oW3btvlieXl5+vzzz/Wzn/3MF7viiivUokULPfPMM76Y4ziaO3euOnXqpLPOOqtW64yNjVVmZqbf63iPalp27typ9evX+x3TYI9J37591atXLz377LN+j6POmTNHERER+ulPf1rrddYoKChQYmKi+vbtW+t9AgAAAOBdjJQCGsFPfvIT3X///Ro3bpzOOussffLJJ3rxxRd9s4zVuPnmm/X000/r6quv1u23364OHTroxRdf9BWErhk9FRkZqeeff16XXXaZ+vbtq3HjxqlTp07avn273n77bSUkJOgf//jHMft0xRVX6NVXX3WNygrkkksuUXR0tIYPH66bb75Z+/fv13PPPaeUlJSgEkCWX//61/rzn/+siy++WBMmTFBcXJyef/55denSRXv37vX1KyEhQXPmzNHo0aN1xhln6KqrrlL79u21detWLV68WGeffbaefvppSdL27dvVu3dvjR079rjFzn/605/qzDPP1Lhx47Ru3Tq1a9dOzzzzjKqqqjR9+nS/tj/60Y8kSV9++aUv9pvf/EYvv/yyLrzwQt1+++3av3+/HnvsMfXv31/jxo3ztevcubMmTpyoxx57TIcOHdIPf/hDvfbaa/r3v/+tF1980e9Rx2DXeSxPP/20iouLfTPg/eMf/9DXX38tSZowYYKv9tfkyZP1hz/8QVu2bFG3bt1qfUwee+wxXX755brkkkt01VVX6dNPP9XTTz+tG264wW+UU23WKX1X2H748OHUlAIAAACaGwfAMc2fP9+R5Hz44Ye+2NixY524uDhX2/PPP9/p27evK961a1dn2LBhvp/Ly8udO+64w+nQoYPTsmVL5+yzz3by8/Od888/3zn//PP9lt28ebMzbNgwp2XLlk779u2dO+64w/m///s/R5KzcuVKv7Yff/yxc+WVVzpt27Z1YmJinK5duzo///nPnby8vOPu50cffeRIcv79738fs+9Hev31153TTz/diY2Ndbp16+Y88sgjzgsvvOBIcrZs2XLcdVj7+/HHHzvnnnuuExMT43Tu3NmZOXOm89vf/taR5BQWFvq1ffvtt52srCwnMTHRiY2NdXr06OFcd911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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def visualize_image(img_data):\n", + " # Load the image using raster.io\n", + " \"\"\"with rasterio.open(image_path, 'r') as src:\n", + " img_data = src.read(1)\"\"\"\n", + " \n", + " # Print basic statistics\n", + " print(\"Min:\", np.nanmin(img_data))\n", + " print(\"Max:\", np.nanmax(img_data))\n", + " print(\"Mean:\", np.nanmean(img_data))\n", + " print(\"Standard Deviation:\", np.nanstd(img_data))\n", + " \n", + " valid_data = img_data[~np.isnan(img_data)]\n", + " \n", + " # Plot the histogram\n", + " plt.figure(figsize=(12, 8))\n", + " plt.subplot(2, 2, 1)\n", + " plt.hist(valid_data.ravel(), bins=100)\n", + " plt.title(\"Histogram\")\n", + " \n", + " # Plot the CDF\n", + " plt.subplot(2, 2, 2)\n", + " plt.hist(valid_data.ravel(), bins=100, cumulative=True, density=True, histtype='step', color='blue', alpha=0.7)\n", + " plt.title(\"CDF\")\n", + " \n", + " # Plot the image using mean±2 standard deviations as the display range\n", + " plt.subplot(2, 2, 3)\n", + " vmin = max(0, np.nanmean(img_data) - 2*np.nanstd(img_data))\n", + " vmax = min(1, np.nanmean(img_data) + 2*np.nanstd(img_data))\n", + " plt.imshow(img_data, cmap='gray', vmin=vmin, vmax=vmax)\n", + " plt.title(f\"Image (range: {vmin:.3f}-{vmax:.3f})\")\n", + " plt.colorbar()\n", + " \n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "visualize_image(x_train[j])" + ] + }, + { + "cell_type": "markdown", + "id": "014cf3d7-3bac-4a70-9d6c-ccb3c7a7ce26", + "metadata": {}, + "source": [ + "## Training an autoencoder" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "e6fc9858-2bdb-4f76-a823-802e272a8170", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model: \"model\"\n", + "_________________________________________________________________\n", + " Layer (type) Output Shape Param # \n", + "=================================================================\n", + " input_1 (InputLayer) [(None, 64, 64, 1)] 0 \n", + " \n", + " conv2d (Conv2D) (None, 64, 64, 32) 320 \n", + " \n", + " max_pooling2d (MaxPooling2D (None, 32, 32, 32) 0 \n", + " ) \n", + " \n", + " conv2d_1 (Conv2D) (None, 32, 32, 64) 18496 \n", + " \n", + " max_pooling2d_1 (MaxPooling (None, 16, 16, 64) 0 \n", + " 2D) \n", + " \n", + " conv2d_2 (Conv2D) (None, 16, 16, 64) 36928 \n", + " \n", + " up_sampling2d (UpSampling2D (None, 32, 32, 64) 0 \n", + " ) \n", + " \n", + " conv2d_3 (Conv2D) (None, 32, 32, 32) 18464 \n", + " \n", + " up_sampling2d_1 (UpSampling (None, 64, 64, 32) 0 \n", + " 2D) \n", + " \n", + " conv2d_4 (Conv2D) (None, 64, 64, 1) 289 \n", + " \n", + "=================================================================\n", + "Total params: 74,497\n", + "Trainable params: 74,497\n", + "Non-trainable params: 0\n", + "_________________________________________________________________\n" + ] + } + ], + "source": [ + "def scheduler(epoch, lr):\n", + " if epoch % 10 == 0 and epoch != 0:\n", + " return lr * 0.9\n", + " return lr\n", + "\n", + "def create_autoencoder(input_shape):\n", + " input_img = Input(shape=input_shape) # adapt this if using `channels_first` image data format\n", + "\n", + " # Encoder\n", + " x = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " x = Conv2D(64, (3, 3), activation='relu', padding='same')(x)\n", + " encoded = MaxPooling2D((2, 2), padding='same')(x)\n", + "\n", + " # Decoder\n", + " x = Conv2D(64, (3, 3), activation='relu', padding='same')(encoded)\n", + " x = UpSampling2D((2, 2))(x)\n", + " x = Conv2D(32, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)\n", + "\n", + "\n", + " # Define the optimizer with a custom learning rate\n", + " optimizer = Adam(learning_rate=0.0005)\n", + " \n", + " autoencoder = Model(input_img, decoded)\n", + " autoencoder.compile(optimizer=optimizer, loss='mean_squared_error')\n", + "\n", + " return autoencoder\n", + "\n", + "def create_adjusted_autoencoder(input_shape):\n", + " input_img = Input(shape=input_shape)\n", + "\n", + " # Encoder\n", + " x = Conv2D(64, (3, 3), activation='relu', padding='same')(input_img)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " x = Conv2D(128, (3, 3), activation='relu', padding='same')(x)\n", + " encoded = MaxPooling2D((2, 2), padding='same')(x)\n", + "\n", + " # Decoder\n", + " x = Conv2D(128, (3, 3), activation='relu', padding='same')(encoded)\n", + " x = UpSampling2D((2, 2))(x)\n", + " x = Conv2D(64, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)\n", + "\n", + " autoencoder = Model(input_img, decoded)\n", + " autoencoder.compile(optimizer=tf.keras.optimizers.Adam(lr=0.0005), loss='mean_squared_error')\n", + "\n", + " return autoencoder\n", + "\n", + "#autoencoder = create_adjusted_autoencoder((64, 64, 1))\n", + "\n", + "autoencoder = create_autoencoder((64, 64, 1))\n", + "autoencoder.summary()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "7ada6881-2e27-4c89-b377-d21c849582dc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/50\n", + "43/43 [==============================] - 7s 25ms/step - loss: 0.0528 - val_loss: 0.0202\n", + "Epoch 2/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0170 - val_loss: 0.0141\n", + "Epoch 3/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0126 - val_loss: 0.0110\n", + "Epoch 4/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0102 - val_loss: 0.0090\n", + "Epoch 5/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0086 - val_loss: 0.0078\n", + "Epoch 6/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0078 - val_loss: 0.0071\n", + "Epoch 7/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0072 - val_loss: 0.0066\n", + "Epoch 8/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0065 - val_loss: 0.0061\n", + "Epoch 9/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0065 - val_loss: 0.0058\n", + "Epoch 10/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0059 - val_loss: 0.0056\n", + "Epoch 11/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0056 - val_loss: 0.0054\n", + "Epoch 12/50\n", + "43/43 [==============================] - 1s 18ms/step - loss: 0.0053 - val_loss: 0.0049\n", + "Epoch 13/50\n", + "43/43 [==============================] - 1s 18ms/step - loss: 0.0051 - val_loss: 0.0048\n", + "Epoch 14/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0049 - val_loss: 0.0045\n", + "Epoch 15/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0046 - val_loss: 0.0044\n", + "Epoch 16/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0044 - val_loss: 0.0043\n", + "Epoch 17/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0043 - val_loss: 0.0040\n", + "Epoch 18/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0041 - val_loss: 0.0039\n", + "Epoch 19/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0041 - val_loss: 0.0039\n", + "Epoch 20/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0039 - val_loss: 0.0038\n", + "Epoch 21/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0038 - val_loss: 0.0036\n", + "Epoch 22/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0037 - val_loss: 0.0035\n", + "Epoch 23/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0036 - val_loss: 0.0034\n", + "Epoch 24/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0036 - val_loss: 0.0036\n", + "Epoch 25/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0034 - val_loss: 0.0034\n", + "Epoch 26/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0033 - val_loss: 0.0031\n", + "Epoch 27/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0032 - val_loss: 0.0036\n", + "Epoch 28/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0032 - val_loss: 0.0030\n", + "Epoch 29/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0030 - val_loss: 0.0029\n", + "Epoch 30/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0029 - val_loss: 0.0028\n", + "Epoch 31/50\n", + "43/43 [==============================] - 1s 19ms/step - loss: 0.0029 - val_loss: 0.0028\n", + "Epoch 32/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0028 - val_loss: 0.0028\n", + "Epoch 33/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0028 - val_loss: 0.0029\n", + "Epoch 34/50\n", + "43/43 [==============================] - 1s 18ms/step - loss: 0.0027 - val_loss: 0.0026\n", + "Epoch 35/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0027 - val_loss: 0.0025\n", + "Epoch 36/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0026 - val_loss: 0.0026\n", + "Epoch 37/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0026 - val_loss: 0.0025\n", + "Epoch 38/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0025 - val_loss: 0.0024\n", + "Epoch 39/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0024 - val_loss: 0.0024\n", + "Epoch 40/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0025 - val_loss: 0.0023\n", + "Epoch 41/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0024 - val_loss: 0.0023\n", + "Epoch 42/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0023 - val_loss: 0.0023\n", + "Epoch 43/50\n", + "43/43 [==============================] - 1s 19ms/step - loss: 0.0023 - val_loss: 0.0023\n", + "Epoch 44/50\n", + "43/43 [==============================] - 1s 17ms/step - loss: 0.0022 - val_loss: 0.0022\n", + "Epoch 45/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0022 - val_loss: 0.0022\n", + "Epoch 46/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0022 - val_loss: 0.0021\n", + "Epoch 47/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0022 - val_loss: 0.0021\n", + "Epoch 48/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0022 - val_loss: 0.0021\n", + "Epoch 49/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0021 - val_loss: 0.0020\n", + "Epoch 50/50\n", + "43/43 [==============================] - 1s 16ms/step - loss: 0.0021 - val_loss: 0.0020\n" + ] + } + ], + "source": [ + "# Reshaping the channel\n", + "x_train = x_train.reshape(-1, 64, 64, 1)\n", + "\n", + "# EarlyStopping function\n", + "early_stopping = EarlyStopping(\n", + " monitor='val_loss', # Value to be monitored\n", + " patience=5, # Number of epochs with no improvement after which the training will be stopped\n", + " mode='auto', # 'auto', 'min' or 'max'. In 'auto', algorithm will detect the direction\n", + " restore_best_weights=True # Whether to restore model weights from the epoch with the best value result\n", + ")\n", + "\n", + "# Train the autoencoder\n", + "history = autoencoder.fit(\n", + " x_train, x_train,\n", + " epochs=50,\n", + " batch_size=128,\n", + " shuffle=True,\n", + " validation_split=0.1,\n", + " callbacks=[early_stopping]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0da63585-614d-4d47-b50e-a845d5cd9430", + "metadata": {}, + "source": [ + "### Visualize results" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "c0d51bb7-0ce1-4f19-94ad-9c01c6430b1d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1/1 [==============================] - 0s 10ms/step\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9AAAAH6CAYAAADvBqSRAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABx3klEQVR4nO3debBfd33f/7dXSdZ6pXt1dbVeLZaNjQO2iRMWYyC0DMUEE5YEiLHBMUtY4o5xS4ayBDIQSJhxJmVLCoHWSVswhjjNSoBASZiEksZ2vGvfpbtot2Rb1rd/MFIR8uf5/vA5upJxno+Z38yvfvt8zzmf81nOxzec12m9Xq8XkiRJkiQJnX6qL0CSJEmSpJ8EbqAlSZIkSargBlqSJEmSpApuoCVJkiRJquAGWpIkSZKkCm6gJUmSJEmq4AZakiRJkqQKbqAlSZIkSargBlqSJEmSpApuoKUKH/jAB+K0005rOvbzn/98nHbaabFu3boTe1E/ZN26dXHaaafF5z//+Qk7hyRJ+sni+4F04rmB1pPa3XffHb/8y78cCxYsiEmTJsX8+fPjda97Xdx9992n+tJOib/927+N0047LW699dZTfSmSpCeoI//h98j/d+aZZ8aCBQvi2muvjc2bN5/qyzvhPvnJT57yDeapvgbfD6R6bqD1pHXbbbfFJZdcEl//+tfjDW94Q3zyk5+M6667Lr75zW/GJZdcEl/5yleqf+s//af/FAcOHGi6jquvvjoOHDgQS5YsaTpekqRT4YMf/GD8t//23+LTn/50vPjFL45bbrklrrjiijh48OCpvrQT6lRvXp8o1yCpzpmn+gKkibB69eq4+uqrY9myZfHtb387BgYGjtZ+7dd+LS6//PK4+uqr484774xly5YVf2f//v0xderUOPPMM+PMM9uGyxlnnBFnnHFG07GSJJ0qL37xi+MZz3hGRET8yq/8SvT398dHP/rRuP322+PVr371Kb66U+PIe4Gkf738C7SelH77t387Hnroofj93//9YzbPERH9/f3xmc98Jvbv3x8f+9jHjv7zI/8753vuuSde+9rXRl9fXzznOc85pvbDDhw4EO985zujv78/pk+fHj//8z8fmzdvjtNOOy0+8IEPHP33Hu9/Az08PBxXXnllfOc734nLLrssJk+eHMuWLYv/+l//6zHnGB8fj3e9611x0UUXxbRp02LGjBnx4he/OO64444T1FL//94eeOCB+OVf/uWYOXNmDAwMxHvf+97o9XqxcePGeNnLXhYzZsyIefPmxcc//vFjjn/kkUfife97X1x66aUxc+bMmDp1alx++eXxzW9+87hzjY2NxdVXXx0zZsyIWbNmxTXXXBN33HHH4/7vs+6777545StfGbNnz47JkyfHM57xjLj99ttP2H1Lkn48l19+eUT84D9S/7Da+XrXrl3x7//9v4/h4eGYNGlSLFy4MF7/+tfH6Ojo0X9nx44dcd1118Xg4GBMnjw5nva0p8UXvvCFY37nyP+u93d+53fi93//92P58uUxadKk+Omf/un43ve+d8y/u23btnjDG94QCxcujEmTJsXQ0FC87GUvO7omDw8Px9133x3f+ta3jv6frD/vec+LiP+/fn/rW9+KX/3VX425c+fGwoULIyLi2muvjeHh4ePusfTNlFtuuSUuu+yyOOecc6Kvry+e+9znxl//9V+n13Ck3W644YZYtGhRTJo0KVasWBEf/ehH4/Dhw8e177XXXhszZ848usbu2rXruGup5fuB9Pj8C7SelP70T/80hoeHjy72P+q5z31uDA8Px5/92Z8dV3vVq14V5557bnz4wx+OXq9XPMe1114bX/ziF+Pqq6+On/3Zn41vfetb8ZKXvKT6GletWhWvfOUr47rrrotrrrkmPve5z8W1114bl156aVx44YUREbFmzZr46le/Gq961ati6dKlsX379vjMZz4TV1xxRdxzzz0xf/786vNlfvEXfzGe8pSnxG/91m/Fn/3Zn8Vv/uZvxuzZs+Mzn/lMvOAFL4iPfvSj8Ud/9Efxrne9K376p386nvvc50ZExJ49e+K//Jf/Eq95zWvi+uuvj71798ZnP/vZeNGLXhT/+I//GE9/+tMjIuLw4cPx0pe+NP7xH/8x3vrWt8b5558ff/InfxLXXHPNcddy9913x7Of/exYsGBBvPvd746pU6fGF7/4xbjqqqviy1/+crz85S8/YfctSapzZNPZ19d39J/Vztf79u2Lyy+/PO6999544xvfGJdcckmMjo7G7bffHps2bYr+/v44cOBAPO95z4tVq1bF29/+9li6dGl86UtfimuvvTZ27doVv/Zrv3bM9fzxH/9x7N27N9785jfHaaedFh/72MfiF37hF2LNmjVx1llnRUTEK17xirj77rvjHe94RwwPD8eOHTvia1/7WmzYsCGGh4fj5ptvjne84x0xbdq0eM973hMREYODg8ec51d/9VdjYGAg3ve+98X+/ft/7Hb7jd/4jfjABz4Qz3rWs+KDH/xgnH322fEP//AP8Y1vfCP+7b/9t3gNDz30UFxxxRWxefPmePOb3xyLFy+Ov//7v49f//Vfj61bt8bNN98cERG9Xi9e9rKXxXe+8514y1veEk95ylPiK1/5yuOusT8u3w+kH9GTnmR27drVi4jey172Mvz3fv7nf74XEb09e/b0er1e7/3vf38vInqvec1rjvt3j9SO+P73v9+LiN4NN9xwzL937bXX9iKi9/73v//oP/vDP/zDXkT01q5de/SfLVmypBcRvW9/+9tH/9mOHTt6kyZN6t14441H/9nBgwd7jz322DHnWLt2bW/SpEm9D37wg8f8s4jo/eEf/iHe8ze/+c1eRPS+9KUvHXdvb3rTm47+s0OHDvUWLlzYO+2003q/9Vu/dfSf79y5szdlypTeNddcc8y/+/DDDx9znp07d/YGBwd7b3zjG4/+sy9/+cu9iOjdfPPNR//ZY4891nvBC15w3LX/3M/9XO+iiy7qHTx48Og/O3z4cO9Zz3pW79xzz8V7lCR1c2Td+pu/+ZveyMhIb+PGjb1bb721NzAw0Js0aVJv48aNR//d2vn6fe97Xy8ierfddttx5zt8+HCv1+v1br755l5E9G655ZajtUceeaT3zGc+szdt2rSj6/WRNW/OnDm98fHxo//un/zJn/Qiovenf/qnvV7vB2tRRPR++7d/G+/3wgsv7F1xxRXFdnjOc57TO3To0DG1a665prdkyZLjjvnR94UHH3ywd/rpp/de/vKXH7eeH7lvuoYPfehDvalTp/YeeOCBY/75u9/97t4ZZ5zR27BhQ6/X6/W++tWv9iKi97GPfezov3Po0KHe5Zdf7vuBdIL5f8KtJ529e/dGRMT06dPx3ztS37NnzzH//C1veUt6jr/8y7+MiB/8V+kf9o53vKP6Oi+44IJj/kI+MDAQ5513XqxZs+boP5s0aVKcfvoPhuljjz0WY2NjMW3atDjvvPPin/7pn6rPVeNXfuVXjv7/n3HGGfGMZzwjer1eXHfddUf/+axZs467xjPOOCPOPvvsiPjBf0UeHx+PQ4cOxTOe8YxjrvEv//Iv46yzzorrr7/+6D87/fTT421ve9sx1zE+Ph7f+MY34tWvfnXs3bs3RkdHY3R0NMbGxuJFL3pRPPjgg0/Kr8BK0hPNC1/4whgYGIhFixbFK1/5ypg6dWrcfvvtR//PmH+c+frLX/5yPO1pT3vcvxAe+T95/vM///OYN29evOY1rzlaO+uss+Kd73xn7Nu3L771rW8dc9wv/uIvHvPX8CNr6pE1asqUKXH22WfH3/7t38bOnTub2+H6669v/pbJV7/61Th8+HC8733vO7qeH1ETj/mlL30pLr/88ujr6zvavqOjo/HCF74wHnvssfj2t78dET9ouzPPPDPe+ta3Hj32jDPO+LHeS0p8P5CO5f8Jt550jmyMj2ykS0ob7aVLl6bnWL9+fZx++unH/bsrVqyovs7Fixcf98/6+vqOWeQPHz4cv/u7vxuf/OQnY+3atfHYY48drc2ZM6f6XC3XM3PmzJg8eXL09/cf98/HxsaO+Wdf+MIX4uMf/3jcd9998eijjx795z/cPuvXr4+hoaE455xzjjn2R9ts1apV0ev14r3vfW+8973vfdxr3bFjRyxYsKD+5iRJP7ZPfOITsXLlyti9e3d87nOfi29/+9sxadKko/UfZ75evXp1vOIVr8DzrV+/Ps4999zjNppPecpTjtZ/2I+uW0c200fW0UmTJsVHP/rRuPHGG2NwcDB+9md/Nq688sp4/etfH/PmzatogR+oeS8oWb16dZx++ulxwQUXNB3/4IMPxp133nnc91yO2LFjR0T8/zV22rRpx9TPO++8pvP+MN8PpGO5gdaTzsyZM2NoaCjuvPNO/PfuvPPOWLBgQcyYMeOYfz5lypSJvLyjSv81u/dD/7vrD3/4w/He97433vjGN8aHPvShmD17dpx++ulxww03HPfxkIm4npprvOWWW+Laa6+Nq666Km666aaYO3dunHHGGfGRj3zkuA/N1DhyX+9617viRS960eP+Oz/Of6iQJLW57LLLjn6F+6qrrornPOc58drXvjbuv//+mDZt2imfr2vWqBtuuCFe+tKXxle/+tX4q7/6q3jve98bH/nIR+Ib3/hGXHzxxVXnebz3gtJfj3/4P3SfCIcPH45/82/+TfyH//AfHre+cuXKE3q+x+P7gXQsN9B6UrryyivjD/7gD+I73/nO0S9p/7D//b//d6xbty7e/OY3N/3+kiVL4vDhw7F27do499xzj/7zVatWNV/z47n11lvj+c9/fnz2s5895p/v2rXruP/ye6rceuutsWzZsrjtttuOeaF4//vff8y/t2TJkvjmN78ZDz300DH/lflH2+xIrNhZZ50VL3zhCyfwyiVJtY5sfJ7//OfHf/7P/zne/e53/1jz9fLly+Nf/uVf8N9ZsmRJ3HnnnXH48OFj/gp93333Ha23WL58edx4441x4403xoMPPhhPf/rT4+Mf/3jccsstEVH3f0r9o/r6+h73C9c/+lfy5cuXx+HDh+Oee+45+tGsx1O6huXLl8e+ffvS9l2yZEl8/etfj3379h3zV+j7778fj5tIvh/oycr/DbSelG666aaYMmVKvPnNbz7u/5xofHw83vKWt8Q555wTN910U9PvH/kvn5/85CeP+ee/93u/13bBBWecccZxXwL/0pe+9IT63/gc+a/QP3yd//AP/xDf/e53j/n3XvSiF8Wjjz4af/AHf3D0nx0+fDg+8YlPHPPvzZ07N573vOfFZz7zmdi6detx5xsZGTmRly9JqvS85z0vLrvssrj55pvj4MGDP9Z8/YpXvCLuuOOO+MpXvnLcv3dk/fh3/+7fxbZt2+J//s//ebR26NCh+L3f+72YNm1aXHHFFT/W9T700ENx8ODBY/7Z8uXLY/r06fHwww8f/WdTp079seOeli9fHrt37z7m/9pt69atx93fVVddFaeffnp88IMfPO7/cuyH183SNbz61a+O7373u/FXf/VXx9V27doVhw4diogftN2hQ4fiU5/61NH6Y489dsLfS34cvh/oycq/QOtJ6dxzz40vfOEL8brXvS4uuuiiuO6662Lp0qWxbt26+OxnPxujo6Px3//7f4/ly5c3/f6ll14ar3jFK+Lmm2+OsbGxozFWDzzwQES0/dfsx3PllVfGBz/4wXjDG94Qz3rWs+Kuu+6KP/qjPzr6X2GfCK688sq47bbb4uUvf3m85CUvibVr18anP/3puOCCC2Lfvn1H/72rrroqLrvssrjxxhtj1apVcf7558ftt98e4+PjEXFsm33iE5+I5zznOXHRRRfF9ddfH8uWLYvt27fHd7/73di0adMJzcGWJNW76aab4lWvelV8/vOfj7e85S3V8/VNN90Ut956a7zqVa+KN77xjXHppZfG+Ph43H777fHpT386nva0p8Wb3vSm+MxnPhPXXnttfP/734/h4eG49dZb4+/+7u/i5ptvTj8O+qMeeOCB+Lmf+7l49atfHRdccEGceeaZ8ZWvfCW2b98ev/RLv3T037v00kvjU5/6VPzmb/5mrFixIubOnRsveMEL8Ld/6Zd+Kf7jf/yP8fKXvzze+c53xkMPPRSf+tSnYuXKlcd8IGvFihXxnve8Jz70oQ/F5ZdfHr/wC78QkyZNiu9973sxf/78+MhHPoLXcNNNN8Xtt98eV1555dGoy/3798ddd90Vt956a6xbty76+/vjpS99aTz72c+Od7/73bFu3bq44IIL4rbbbovdu3f/WG12Ivl+oCetU/Hpb+lkufPOO3uvec1rekNDQ72zzjqrN2/evN5rXvOa3l133XXcv3skrmFkZKRY+2H79+/vve1tb+vNnj27N23atN5VV13Vu//++3sRcUy0QynG6iUveclx57niiiuOibE4ePBg78Ybb+wNDQ31pkyZ0nv2s5/d++53v3vcv3ciYqx+9L6vueaa3tSpUx/3Gi+88MKj/+/Dhw/3PvzhD/eWLFnSmzRpUu/iiy/u/a//9b8eN+JjZGSk99rXvrY3ffr03syZM3vXXntt7+/+7u96EdH7H//jfxzz765evbr3+te/vjdv3rzeWWed1VuwYEHvyiuv7N166614j5Kkbo6sW9/73veOqz322GO95cuX95YvX3402ql2vh4bG+u9/e1v7y1YsKB39tln9xYuXNi75ppreqOjo0f/ne3bt/fe8IY39Pr7+3tnn31276KLLjpubTuy5j1ePFX8UJTk6Oho721ve1vv/PPP702dOrU3c+bM3s/8zM/0vvjFLx5zzLZt23oveclLetOnT+9FxNH1ldqh1+v1/vqv/7r31Kc+tXf22Wf3zjvvvN4tt9zyuO8LvV6v97nPfa538cUX9yZNmtTr6+vrXXHFFb2vfe1r6TX0er3e3r17e7/+67/eW7FiRe/ss8/u9ff39571rGf1fud3fqf3yCOPHNO+V199dW/GjBm9mTNn9q6++ure//2//9f3A+kEO63X+5H/+1BJzf75n/85Lr744rjlllvida973am+nJ8IX/3qV+PlL395fOc734lnP/vZp/pyJEnSE4DvB3qi8n8DLTU6cODAcf/s5ptvjtNPPz2e+9znnoIreuL70TY78r/PmjFjRlxyySWn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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Choose an image for visualization\n", + "index = 11\n", + "test_image = x_test[index]\n", + "test_image_reshaped = test_image.reshape(1, 64, 64, 1) # reshape for prediction\n", + "\n", + "# Make a prediction\n", + "predicted_image = autoencoder.predict(test_image_reshaped)\n", + "\n", + "# Reshape predicted image for visualization\n", + "predicted_image = predicted_image.reshape(64, 64)\n", + "\n", + "# Plot original vs reconstructed\n", + "plt.figure(figsize=(10, 5))\n", + "\n", + "# Original image\n", + "plt.subplot(1, 2, 1)\n", + "plt.imshow(test_image, cmap='gray')\n", + "plt.title(\"Original Image\")\n", + "plt.axis(\"off\")\n", + "\n", + "# Reconstructed image\n", + "plt.subplot(1, 2, 2)\n", + "plt.imshow(predicted_image, cmap='gray')\n", + "plt.title(\"Reconstructed Image\")\n", + "plt.axis(\"off\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "5835bb7f-fab1-47b2-93ed-6083bd476fd5", + "metadata": {}, + "source": [ + "## Evaluation" + ] + }, + { + "cell_type": "markdown", + "id": "5d231a12-6799-46ea-b7f8-c92c7c60e2bc", + "metadata": {}, + "source": [ + "Lets evaluate using Mean squared error (MSE, i.e. reconstruction loss) and Structural similarity index (SSIM)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "323a8981-3873-4872-954e-9106ab3a5efc", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10/10 [==============================] - 0s 2ms/step\n", + "Average MSE: 0.002289454685524106, Average SSIM: 0.9365273173131262\n" + ] + } + ], + "source": [ + "def compute_mse_ssim(autoencoder, test_images):\n", + " \"\"\"\n", + " Compute the MSE and SSIM for the set of test images using the provided autoencoder.\n", + "\n", + " :param autoencoder: Trained autoencoder model\n", + " :param test_images: List or numpy array of test images\n", + " :return: Average MSE and average SSIM for the test set\n", + " \"\"\"\n", + "\n", + " # Get the autoencoder's predictions\n", + " reconstructed_images = autoencoder.predict(test_images)\n", + "\n", + " # Initialize accumulators for MSE and SSIM\n", + " mse_accumulator = 0.0\n", + " ssim_accumulator = 0.0\n", + "\n", + " # Compute MSE and SSIM for each image\n", + " for original, reconstructed in zip(test_images, reconstructed_images):\n", + " # MSE\n", + " mse_accumulator += K.mean(K.square(original - reconstructed))\n", + "\n", + " # Scale the images to be in the range [0,255] for SSIM computation\n", + " original_for_ssim = (original * 255).astype(np.uint8)\n", + " reconstructed_for_ssim = (reconstructed * 255).astype(np.uint8)\n", + "\n", + " # SSIM (used on 2D grayscale images; adapt as necessary for color images)\n", + " ssim_value, _ = ssim(original_for_ssim.squeeze(), reconstructed_for_ssim.squeeze(), full=True)\n", + " ssim_accumulator += ssim_value\n", + "\n", + " # Calculate average MSE and SSIM\n", + " avg_mse = mse_accumulator / len(test_images)\n", + " avg_ssim = ssim_accumulator / len(test_images)\n", + "\n", + " return avg_mse, avg_ssim\n", + "\n", + "avg_mse, avg_ssim = compute_mse_ssim(autoencoder, x_test)\n", + "print(f\"Average MSE: {avg_mse}, Average SSIM: {avg_ssim}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0868fc9e-ad50-48b8-a94d-60eeddee9fad", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Applications" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9b00e3f-7ad9-408d-8791-5bfc6de79e3e", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Latent space visualization\"\"\"\n", + "\n", + "print(x_test.shape)\n", + "\n", + "for layer in autoencoder.layers:\n", + " print('Layer name:', layer.name)\n", + "\n", + "#encoder_model = Model(inputs=autoencoder.input, outputs=autoencoder.get_layer('encoder').output)\n", + "encoder_model = Model(inputs=autoencoder.input, outputs=autoencoder.get_layer('max_pooling2d_1').output)\n", + "\n", + "encoded_images = encoder_model.predict(x_test)\n", + "\n", + "print(encoded_images.shape)\n", + "\n", + "reshaped_data = encoded_images.reshape(encoded_images.shape[0], -1)\n", + "\n", + "# Reduce dimensionality to 2D for visualization\n", + "encoded_2D = TSNE(n_components=2, perplexity=20).fit_transform(reshaped_data)\n", + "encoded_3D = TSNE(n_components=3, perplexity=20).fit_transform(reshaped_data)\n", + "\n", + "plt.scatter(encoded_2D[:, 0], encoded_2D[:, 1])\n", + "plt.xlabel('Dimension 1')\n", + "plt.ylabel('Dimension 2')\n", + "plt.title('2D TSNE of Encoded Images')\n", + "plt.show()\n", + "\n", + "\n", + "\"\"\"Dimensionality reduction\"\"\"\n", + "\n", + "# Extract the encoder\n", + "#encoder_model = Model(inputs=autoencoder.input, outputs=autoencoder.get_layer('encoder').output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b88b6580-c77f-4fe6-a0a8-facd8029c5d8", + "metadata": {}, + "outputs": [], + "source": [ + "# 3D representation\n", + "\n", + "fig = plt.figure(figsize=(10, 8))\n", + "ax = fig.add_subplot(111, projection='3d')\n", + "\n", + "ax.scatter(encoded_3D[:, 0], encoded_3D[:, 1], encoded_3D[:, 2], marker='o')\n", + "\n", + "ax.set_xlabel('X Label')\n", + "ax.set_ylabel('Y Label')\n", + "ax.set_zlabel('Z Label')\n", + "plt.title(\"3D t-SNE Visualization\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "013cde28-57d1-4cd2-8f53-1f11148c5f4d", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Find an optimal number of computing units (Convolutional filters & layers)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c56a9c56-ddab-44b3-95bc-dc5265156c05", + "metadata": {}, + "outputs": [], + "source": [ + "def create_autoencoder_looping(input_shape, num_layers, num_filters):\n", + " input_img = Input(shape=input_shape)\n", + " x = input_img\n", + " \n", + " # Encoder\n", + " for _ in range(num_layers):\n", + " x = Conv2D(num_filters, (3, 3), activation='relu', padding='same')(x)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " \n", + " # Decoder\n", + " for _ in range(num_layers):\n", + " x = Conv2D(num_filters, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " decoded = Conv2D(input_shape[-1], (3, 3), activation='sigmoid', padding='same')(x)\n", + " \n", + " return Model(input_img, decoded)\n", + "\n", + "def create_adjusted_autoencoder_looping(input_shape, num_layers, num_filters):\n", + " input_img = Input(shape=input_shape)\n", + " x = input_img\n", + " num_filters = initial_num_filters\n", + " \n", + " # Encoder\n", + " for _ in range(num_layers):\n", + " x = Conv2D(num_filters, (3, 3), activation='relu', padding='same')(x)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " num_filters *= 2 # Double the number of filters for the next layer\n", + " \n", + " # Decoder\n", + " # Start with half of the final number of filters from the encoder\n", + " num_filters //= 2\n", + " for _ in range(num_layers):\n", + " x = Conv2D(num_filters, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " num_filters //= 2 # Halve the number of filters for the next layer\n", + "\n", + " decoded = Conv2D(input_shape[-1], (3, 3), activation='sigmoid', padding='same')(x)\n", + " \n", + " return Model(input_img, decoded)\n", + "\n", + "def create_autoencoder_symmetrical_looping(input_shape, num_layers, initial_num_filters):\n", + " input_img = Input(shape=input_shape)\n", + " x = input_img\n", + " filters = []\n", + " num_filters = initial_num_filters\n", + " \n", + " # Encoder\n", + " for _ in range(num_layers):\n", + " x = Conv2D(num_filters, (3, 3), activation='relu', padding='same')(x)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " filters.append(num_filters)\n", + " num_filters *= 2 # Double the number of filters for the next layer\n", + " \n", + " # Decoder\n", + " for f in reversed(filters): # take the stored list of filters and apply it reversely (from largest to smallest) to achieve symmetry\n", + " x = Conv2D(f, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + "\n", + " decoded = Conv2D(input_shape[-1], (3, 3), activation='sigmoid', padding='same')(x)\n", + " \n", + " return Model(input_img, decoded)\n", + "\n", + "results = []\n", + "\n", + "x_train = x_train.reshape(-1, 64, 64, 1)\n", + "\n", + "# Loop over different architectures to find the best one\n", + "for num_layers in [1, 2, 3, 4, 5]: # list of layer counts to test\n", + " for num_filters in [8, 16, 32, 64, 128]: # list of filter counts counts to test\n", + " model = create_autoencoder_symmetrical_looping(x_train.shape[1:], num_layers, num_filters)\n", + " model.compile(optimizer=tf.keras.optimizers.Adam(lr=0.0005), loss='mean_squared_error')\n", + " \n", + " model.fit(x_train, x_train, epochs=10, batch_size=128, validation_data=(x_test, x_test))\n", + " \n", + " loss = model.evaluate(x_test, x_test)\n", + " \n", + " results.append({\n", + " 'num_layers': num_layers,\n", + " 'num_filters': num_filters,\n", + " 'loss': loss\n", + " })\n", + "\n", + "# Sort results by loss to find the best configuration\n", + "sorted_results = sorted(results, key=lambda x: x['loss'])\n", + "print(sorted_results[0]) # print the architecture with the lowest loss" + ] + }, + { + "cell_type": "markdown", + "id": "b075679e-d8b9-4c57-924b-95d0214fabff", + "metadata": {}, + "source": [ + "Find the smallest model (as per layers, then convolutional filters) that exceeds some baseline, here MSI loss" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f0d1328-dea4-4a79-af6e-b816503246f2", + "metadata": {}, + "outputs": [], + "source": [ + "# Flatten the loops into a list of configurations\n", + "configs = [(layers, filters) for layers in [1, 2, 3, 4, 5] for filters in [8, 16, 32, 64, 128]]\n", + "\n", + "# Sort configurations by complexity\n", + "configs.sort(key=lambda x: (x[0], x[1]))\n", + "\n", + "min_acceptable_performance = 0.1 # 90% accuracy in terms of mean squared error\n", + "best_config = None\n", + "\n", + "for num_layers, initial_num_filters in configs:\n", + " model_minimum = create_autoencoder_symmetrical_looping(x_train.shape[1:], num_layers, initial_num_filters)\n", + " model_minimum.compile(optimizer='adam', loss='mean_squared_error')\n", + " model_minimum.fit(x_train, x_train, epochs=50, batch_size=256, shuffle=True, validation_data=(x_test, x_test), verbose=1)\n", + " \n", + " # Evaluate the model\n", + " predictions = model_minimum.predict(x_test)\n", + " mse = mean_squared_error(x_test.flatten(), predictions.flatten())\n", + " \n", + " if mse <= min_acceptable_performance:\n", + " best_config = (num_layers, initial_num_filters)\n", + " break\n", + "\n", + "print(f\"The smallest configuration that exceeds the performance criteria is {best_config}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fff2d16-d5ed-48da-97c2-6666ad00abd1", + "metadata": {}, + "outputs": [], + "source": [ + "# Choose an image for visualization\n", + "index = 1 \n", + "test_image = x_test[index]\n", + "test_image_reshaped = test_image.reshape(1, 64, 64, 1) # reshape for prediction\n", + "\n", + "# Make a prediction\n", + "predictions = autoencoder.predict(test_image_reshaped)\n", + "\n", + "# Reshape predicted image for visualization\n", + "predictions = predictions.reshape(64, 64)\n", + "\n", + "print(x_test[0].shape, predictions[0].shape)\n", + "\n", + "# Plot original vs reconstructed\n", + "plt.figure(figsize=(10, 5))\n", + "\n", + "# Original image\n", + "plt.subplot(1, 2, 1)\n", + "plt.imshow(x_test[0], cmap='gray')\n", + "plt.title(\"Original Image\")\n", + "plt.axis(\"off\")\n", + "\n", + "# Reconstructed image\n", + "plt.subplot(1, 2, 2)\n", + "plt.imshow(predictions[0], cmap='gray')\n", + "plt.title(\"Reconstructed Image\")\n", + "plt.axis(\"off\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4506a6b4-3de9-4c54-b5bf-9341f1192a5e", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Autoencoder with skip-connections and attention block" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "2156273e-837f-4a7c-818a-126a237eac8d", + "metadata": {}, + "outputs": [], + "source": [ + "# A layer to scale the multimodal inputs\n", + "class ScalingLayer(Layer):\n", + " def __init__(self, multipliers, **kwargs):\n", + " super(ScalingLayer, self).__init__(**kwargs)\n", + " self.multipliers = tf.constant(multipliers, dtype=tf.float32)\n", + "\n", + " def call(self, inputs):\n", + " # Rescale each tensor based on its corresponding multiplier\n", + " return [inputs[i] * self.multipliers[i] for i in range(len(inputs))]\n", + "\n", + " def get_config(self):\n", + " config = super(ScalingLayer, self).get_config()\n", + " config.update({'multipliers': self.multipliers.numpy().tolist()})\n", + " return config\n", + "\n", + "# Attention blocks\n", + "\n", + "def attention_block(x, shortcut):\n", + " g = Conv2D(filters=shortcut.shape[-1], kernel_size=1, strides=1, padding='same')(x)\n", + " x = Conv2D(filters=shortcut.shape[-1], kernel_size=1, strides=1, padding='same')(shortcut)\n", + " f = Activation('relu')(g + x)\n", + " psi = Conv2D(filters=1, kernel_size=1, strides=1, padding='same')(f)\n", + " rate = Activation('sigmoid')(psi)\n", + " att_x = Multiply()([shortcut, rate])\n", + " return att_x\n", + "\n", + "def attention_block_skip(x, g, inter_channel):\n", + " theta_x = Conv2D(inter_channel, [1, 1], strides=[1, 1])(x)\n", + " phi_g = Conv2D(inter_channel, [1, 1], strides=[1, 1])(g)\n", + " f = Activation('relu')(Add()([theta_x, phi_g]))\n", + " psi_f = Conv2D(1, [1, 1], strides=[1, 1])(f)\n", + " rate = Activation('sigmoid')(psi_f)\n", + " att_x = Multiply()([x, rate])\n", + " return att_x\n", + "\n", + "def build_autoencoder_multichannel_skip_attention_modular(resolution, modality, dropout=0.2, regularization=0, \n", + " modality_multipliers=None, number_of_layers=3, filter_size_start=8):\n", + " \n", + " # List to hold all input layers\n", + " input_imgs = [Input(shape=(resolution, resolution, 1)) for _ in range(modality)]\n", + " skip_connections = [] \n", + " encoded_imgs = []\n", + " \n", + " if modality_multipliers is None:\n", + " multipliers = [1 for _ in range(modality)]\n", + " else:\n", + " multipliers = modality_multipliers\n", + "\n", + " # Scaling\n", + " scaling_layer = ScalingLayer(multipliers=multipliers)\n", + " scaled_imgs = scaling_layer(input_imgs)\n", + "\n", + " # Encoder\n", + " for idx, input_img in enumerate(scaled_imgs):\n", + " x = input_img\n", + " current_filter_size = filter_size_start\n", + " for i in range(number_of_layers):\n", + " x = Conv2D(current_filter_size, (3, 3), padding='same', kernel_regularizer=l1(regularization))(x)\n", + " x = Dropout(dropout)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " \n", + " # Store the encoder output for skip connections\n", + " skip_connections.append(x)\n", + " \n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " current_filter_size *= 2 # Double the filter size for the next layer\n", + " encoded_imgs.append(x)\n", + "\n", + " x = concatenate(encoded_imgs, axis=-1)\n", + "\n", + " # Decoder\n", + " current_filter_size = filter_size_start * number_of_layers\n", + " for i in range(number_of_layers):\n", + " x = Conv2D(current_filter_size, (3, 3), padding='same', kernel_regularizer=l1(regularization))(x)\n", + " x = Dropout(dropout)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " \n", + " # Get the corresponding encoder output for skip connection\n", + " skip = skip_connections[-(i+1)]\n", + " \n", + " # Apply the attention block to skip connection\n", + " x = attention_block_skip(x, skip, current_filter_size)\n", + " \n", + " current_filter_size //= 2 # Halve the filter size for the next layer\n", + "\n", + " # Output layer\n", + " decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)\n", + "\n", + " autoencoder_multi_channel = Model(input_imgs, decoded)\n", + " return autoencoder_multi_channel" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "57d685d1-5dcd-4506-a9cb-a7bf3edbe0cd", + "metadata": {}, + "outputs": [], + "source": [ + "# Usage\n", + "autoencoder_unet = build_autoencoder_multichannel_skip_attention_modular(256, 3, 0.15, 0, [0.8, 1, 0.15], 3, 8)\n", + "autoencoder_unet.compile(optimizer=tf.keras.optimizers.Adam(lr=0.0005), loss='mean_squared_error')" + ] + }, + { + "cell_type": "markdown", + "id": "1e961a64-eda3-46d3-ba3c-3d0520de296d", + "metadata": {}, + "source": [ + "## Use pre-trained VGG models for the encoder" + ] + }, + { + "cell_type": "markdown", + "id": "7fd7472e-f789-4bf9-b77b-3e17416777d4", + "metadata": {}, + "source": [ + "Option to load a pre-trained VGG model and use it for the encoder, while freezing it's weights (i.e. using it as-is). Note that VGG and most other models as well are RGB, so we need to create a 3 -> 1 band layer (which is also done here)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "23d6ce3a-2522-4b80-8b3f-812d58bdca1a", + "metadata": {}, + "outputs": [], + "source": [ + "from tensorflow.keras.applications.vgg16 import preprocess_input\n", + "from tensorflow.keras.applications.vgg19 import preprocess_input\n", + "from tensorflow.keras.applications import VGG16, VGG19\n", + "\n", + "def build_vgg19_autoencoder(resolution):\n", + " \n", + " # Load VGG without its top layers, and with default input shape\n", + " vgg19 = keras.applications.VGG19(include_top=False, weights='imagenet')\n", + " \n", + " # Create a new input layer for single channel input\n", + " input_layer = keras.layers.Input(shape=(resolution, resolution, 1))\n", + " \n", + " # The new first layer won't have pretrained weights\n", + " x = keras.layers.Conv2D(64, (3, 3), padding='same', activation='relu')(input_layer)\n", + " \n", + " x = Conv2D(3, (1, 1), activation='relu')(x) # Convert 64 channels to 3 channels\n", + " \n", + " # Apply the VGG layers (skip the original input layer of VGG)\n", + " for layer in vgg19.layers[1:]:\n", + " x = layer(x)\n", + " \n", + " # New VGG model with modified input\n", + " modified_vgg19 = keras.models.Model(inputs=input_layer, outputs=x)\n", + " \n", + " #Freeze VGG19 layers\n", + " for layer in modified_vgg19.layers:\n", + " layer.trainable = False\n", + " \n", + " #Summary of the pre-trained encoder model\n", + " print(modified_vgg19.summary())\n", + " \n", + " input_img = Input(shape=(resolution, resolution, 1))\n", + " encoded = modified_vgg19(input_img)\n", + " \n", + " # decoder to match the pre-trained encoder and upscale\n", + " x = Conv2D(256, (3, 3), padding='same')(encoded)\n", + " x = Dropout(0.5)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " \n", + " x = Conv2D(128, (3, 3), activation='relu', padding='same')(x)\n", + " x = Dropout(0.5)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " \n", + " x = Conv2D(64, (3, 3), padding='same')(x)\n", + " x = Dropout(0.5)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " \n", + " x = Conv2D(32, (3, 3), padding='same')(x)\n", + " x = Dropout(0.5)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " \n", + " x = Conv2D(16, (3, 3), padding='same')(x)\n", + " x = Dropout(0.5)(x)\n", + " x = BatchNormalization()(x)\n", + " x = Activation('relu')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " \n", + " decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)\n", + " \n", + " # Create the Autoencoder model\n", + " autoencoder_vgg19 = Model(input_img, decoded)\n", + " \n", + " print(autoencoder_vgg19.summary())\n", + "\n", + " return autoencoder_vgg19" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be51147b-fdf6-4623-b48c-b45e007ce53d", + "metadata": {}, + "outputs": [], + "source": [ + "def preprocess_grayscale_for_vgg(image):\n", + " # Convert 1-channel grayscale to 3-channel grayscale\n", + " image_rgb = tf.concat([image, image, image], axis=-1)\n", + " return preprocess_input(image_rgb * 255.0) # VGG's 'preprocess_input' function expects pixel values in [0, 255]\n", + "\n", + "autoencoder_vgg19.compile(optimizer=tf.keras.optimizers.Adadelta(lr=0.003), loss='mean_squared_error')" + ] + }, + { + "cell_type": "markdown", + "id": "04275882-08e0-475d-98dd-412aee080790", + "metadata": {}, + "source": [ + "## Variational autoencoder (Not completed, experimental)" + ] + }, + { + "cell_type": "markdown", + "id": "46af1808-f401-411d-a96e-1c4df10d6982", + "metadata": {}, + "source": [ + "Variational autoencoder can be used to generate more data, augmentate it or find some patterns. Like autoencoder, can capture the strucurality of the data and compress it" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b3d76e28-e985-4226-9f1f-a12bf85a7c4d", + "metadata": {}, + "outputs": [], + "source": [ + "original_dim = 64 * 64\n", + "intermediate_dim = 256\n", + "latent_dim = 2\n", + "batch_size = 128\n", + "epsilon_std = 1.0\n", + "\n", + "def sampling(args):\n", + " z_mean, z_log_var = args\n", + " batch = K.shape(z_mean)[0]\n", + " dim = K.int_shape(z_mean)[1]\n", + " epsilon = K.random_normal(shape=(batch, dim))\n", + " return z_mean + K.exp(0.5 * z_log_var) * epsilon" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02970f19-2068-480a-a332-58494da00f51", + "metadata": {}, + "outputs": [], + "source": [ + "def create_vae(input_shape=(64, 64, 1)):\n", + " input_img = Input(shape=input_shape)\n", + " \n", + " # Encoder\n", + " x = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " x = Conv2D(64, (3, 3), activation='relu', padding='same')(x)\n", + " x = MaxPooling2D((2, 2), padding='same')(x)\n", + " shape_before_flattening = K.int_shape(x)\n", + " x = Flatten()(x)\n", + " x = Dense(32, activation='relu')(x)\n", + " \n", + " # outputs: latent mean, and log variance\n", + " z_mean = Dense(latent_dim)(x)\n", + " z_log_var = Dense(latent_dim)(x)\n", + " \n", + " # Use reparameterization to push the sampling as input\n", + " z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])\n", + " \n", + " # Initialize the encoder\n", + " encoder = Model(input_img, [z_mean, z_log_var, z])\n", + " \n", + " # Decoder\n", + " decoder_input = Input(K.int_shape(z)[1:])\n", + " x = Dense(np.prod(shape_before_flattening[1:]), activation='relu')(decoder_input)\n", + " x = Reshape(shape_before_flattening[1:])(x)\n", + " x = Conv2DTranspose(64, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " x = Conv2DTranspose(32, (3, 3), activation='relu', padding='same')(x)\n", + " x = UpSampling2D((2, 2))(x)\n", + " decoded = Conv2DTranspose(1, (3, 3), activation='sigmoid', padding='same')(x)\n", + " \n", + " # Initialize the decoder\n", + " decoder = Model(decoder_input, decoded)\n", + " \n", + " # Apply the decoder to the sample from the latent distribution\n", + " z_decoded = decoder(z)\n", + " \n", + " # Initialize the VAE\n", + " vae = Model(input_img, z_decoded)\n", + "\n", + " return vae\n", + "\n", + "vae = create_vae()\n", + "\n", + "# Compute VAE loss\n", + "xent_loss = 64 * 64 * binary_crossentropy(K.flatten(x_train[0]), K.flatten(z_decoded))\n", + "kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)\n", + "vae_loss = K.mean(xent_loss + kl_loss)\n", + "beta = 1\n", + "\n", + "reconstruction_loss = binary_crossentropy(K.flatten(input_img), K.flatten(z_decoded))\n", + "reconstruction_loss *= 64 * 64\n", + "\n", + "vae_loss_reconstruction = K.mean(reconstruction_loss + beta * kl_loss) # beta can be adjusted\n", + "\n", + "#vae.add_loss(vae_loss)\n", + "\n", + "vae.add_loss(vae_loss_reconstruction)\n", + "#vae.compile(optimizer='rmsprop')\n", + "vae.compile(optimizer=tf.keras.optimizers.Adam(lr=0.002))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "249957ff-62de-4bfa-aff0-4c085c78536c", + "metadata": {}, + "outputs": [], + "source": [ + "# Train the model\n", + "vae.fit(x_train, None, epochs=1000, batch_size=batch_size, validation_data=(x_test, None))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/exploratory_analyses/normality_test_test.py b/tests/exploratory_analyses/normality_test_test.py new file mode 100644 index 00000000..c2012578 --- /dev/null +++ b/tests/exploratory_analyses/normality_test_test.py @@ -0,0 +1,97 @@ +import numpy as np +import pandas as pd +import pytest + +from eis_toolkit.exceptions import ( + EmptyDataException, + InvalidColumnException, + InvalidDataShapeException, + InvalidRasterBandException, + NonNumericDataException, + SampleSizeExceededException, +) +from eis_toolkit.exploratory_analyses.normality_test import normality_test_array, normality_test_dataframe + +DATA_ARRAY = np.array( + [ + [[0, 1, 2, 1, 3, 3], [2, 0, 1, 2, 1, 2], [2, 1, 0, 2, 0, 3], [3, 1, 1, 1, 0, 5], [2, 2, 1, 0, 3, 4]], + [[0, 1, 2, 1, 3, 3], [2, 0, 1, 2, 1, 2], [2, 1, 0, 2, 0, 3], [3, 1, 1, 1, 0, 5], [2, 2, 1, 0, 3, 4]], + ] +) +DATA_DF = pd.DataFrame(DATA_ARRAY[0], columns=["a", "b", "c", "d", "e", "f"]) + + +def test_normality_test_dataframe(): + """Test that returned normality statistics for DataFrame data are correct.""" + output_statistics = normality_test_dataframe(data=DATA_DF, columns=["a"]) + np.testing.assert_array_almost_equal(output_statistics["a"], (0.82827, 0.13502), decimal=5) + + +def test_normality_test_array(): + """Test that returned normality statistics for Numpy array data are correct.""" + # 3D array + output_statistics = normality_test_array(data=DATA_ARRAY, bands=[0]) + np.testing.assert_array_almost_equal(output_statistics[0], (0.91021, 0.01506), decimal=5) + + # 2D array + output_statistics = normality_test_array(data=DATA_ARRAY[0]) + np.testing.assert_array_almost_equal(output_statistics[0], (0.91021, 0.01506), decimal=5) + + # 1D array + output_statistics = normality_test_array(data=DATA_ARRAY[0][0]) + np.testing.assert_array_almost_equal(output_statistics[0], (0.9067, 0.41504), decimal=5) + + +def test_normality_test_dataframe_missing_data(): + """Test that DataFrame input with missing data returns statistics correctly.""" + df_with_nan = DATA_DF.replace(3, np.nan) + output_statistics = normality_test_dataframe(data=df_with_nan, columns=["a"]) + np.testing.assert_array_almost_equal(output_statistics["a"], (0.62978, 0.00124), decimal=5) + + +def test_normality_test_array_nodata(): + """Test that Numpy array input with missing data returns statistics correctly.""" + output_statistics = normality_test_array(data=DATA_ARRAY, nodata_value=3) + np.testing.assert_array_almost_equal(output_statistics[0], (0.91021, 0.01506), decimal=5) + + +def test_invalid_selection(): + """Test that invalid column names and invalid bands raise the correct exception.""" + with pytest.raises(InvalidColumnException): + normality_test_dataframe(data=DATA_DF, columns=["g", "h"]) + + with pytest.raises(InvalidRasterBandException): + normality_test_array(data=DATA_ARRAY, bands=[2, 3]) + + +def test_empty_input(): + """Test that empty input raises the correct exception.""" + with pytest.raises(EmptyDataException): + normality_test_dataframe(data=pd.DataFrame()) + + with pytest.raises(EmptyDataException): + normality_test_array(data=np.array([])) + + +def test_max_samples(): + """Test that sample count > 5000 raises the correct exception.""" + large_data = np.random.normal(size=5001) + large_df = pd.DataFrame(large_data, columns=["a"]) + + with pytest.raises(SampleSizeExceededException): + normality_test_dataframe(data=large_df, columns=["a"]) + + with pytest.raises(SampleSizeExceededException): + normality_test_array(data=large_data) + + +def test_non_numeric_data(): + """Test that non-numeric data for input DataFrame raises the correct exception.""" + with pytest.raises(NonNumericDataException): + normality_test_dataframe(data=pd.DataFrame(["hey", "there"], columns=["a"]), columns=["a"]) + + +def test_invalid_input_data_shape(): + """Test that invalid shape for input Numpy array raises the correct exception.""" + with pytest.raises(InvalidDataShapeException): + normality_test_array(data=np.stack([DATA_ARRAY, DATA_ARRAY])) diff --git a/tests/exploratory_analyses/statistical_tests_test.py b/tests/exploratory_analyses/statistical_tests_test.py index 891905ac..71954c0f 100644 --- a/tests/exploratory_analyses/statistical_tests_test.py +++ b/tests/exploratory_analyses/statistical_tests_test.py @@ -3,18 +3,8 @@ import pytest from beartype.roar import BeartypeCallHintParamViolation -from eis_toolkit.exceptions import ( - EmptyDataException, - InvalidParameterValueException, - NonNumericDataException, - SampleSizeExceededException, -) -from eis_toolkit.exploratory_analyses.statistical_tests import ( - chi_square_test, - correlation_matrix, - covariance_matrix, - normality_test, -) +from eis_toolkit.exceptions import InvalidParameterValueException, NonNumericDataException +from eis_toolkit.exploratory_analyses.statistical_tests import chi_square_test, correlation_matrix, covariance_matrix data = np.array([[0, 1, 2, 1], [2, 0, 1, 2], [2, 1, 0, 2], [0, 1, 2, 1]]) missing_data = np.array([[0, 1, 2, 1], [2, 0, np.nan, 2], [2, 1, 0, 2], [0, 1, 2, 1]]) @@ -35,26 +25,6 @@ def test_chi_square_test(): np.testing.assert_array_equal((output_statistics["f"]), (0.0, 1.0, 1)) -def test_normality_test(): - """Test that returned statistics for normality are correct.""" - output_statistics = normality_test(data=numeric_data, columns=["a"]) - np.testing.assert_array_almost_equal(output_statistics["a"], (0.72863, 0.02386), decimal=5) - output_statistics = normality_test(data=data) - np.testing.assert_array_almost_equal(output_statistics, (0.8077, 0.00345), decimal=5) - output_statistics = normality_test(data=np.array([0, 2, 2, 0])) - np.testing.assert_array_almost_equal(output_statistics, (0.72863, 0.02386), decimal=5) - - -def test_normality_test_missing_data(): - """Test that input with missing data returns statistics correctly.""" - output_statistics = normality_test(data=missing_data) - np.testing.assert_array_almost_equal(output_statistics, (0.79921, 0.00359), decimal=5) - output_statistics = normality_test(data=np.array([0, 2, 2, 0, np.nan])) - np.testing.assert_array_almost_equal(output_statistics, (0.72863, 0.02386), decimal=5) - output_statistics = normality_test(data=missing_values_df, columns=["a", "b"]) - np.testing.assert_array_almost_equal(output_statistics["a"], (0.72863, 0.02386), decimal=5) - - def test_correlation_matrix_nan(): """Test that returned correlation matrix is correct, when NaN present in the dataframe.""" expected_correlation_matrix = np.array( @@ -123,33 +93,6 @@ def test_covariance_matrix_negative_min_periods(): covariance_matrix(data=numeric_data, min_periods=-1) -def test_empty_df(): - """Test that empty DataFrame raises the correct exception.""" - empty_df = pd.DataFrame() - with pytest.raises(EmptyDataException): - normality_test(data=empty_df) - - -def test_max_samples(): - """Test that sample count > 5000 raises the correct exception.""" - with pytest.raises(SampleSizeExceededException): - normality_test(data=large_data) - normality_test(data=large_df, columns=["a"]) - - -def test_invalid_columns(): - """Test that invalid column name in raises the correct exception.""" - with pytest.raises(InvalidParameterValueException): - chi_square_test(data=categorical_data, target_column=target_column, columns=["f", "x"]) - normality_test(data=numeric_data, columns=["e", "f"]) - - -def test_non_numeric_data(): - """Test that non-numeric data raises the correct exception.""" - with pytest.raises(NonNumericDataException): - normality_test(data=non_numeric_df, columns=["a"]) - - def test_invalid_target_column(): """Test that invalid target column raises the correct exception.""" with pytest.raises(InvalidParameterValueException): diff --git a/tests/raster_processing/reclassify_raster_test.py b/tests/raster_processing/reclassify_raster_test.py index 8b0ac6c5..e8a4d9ee 100644 --- a/tests/raster_processing/reclassify_raster_test.py +++ b/tests/raster_processing/reclassify_raster_test.py @@ -1,7 +1,9 @@ import numpy as np +import pytest import rasterio from beartype.typing import Tuple +from eis_toolkit.exceptions import InvalidParameterValueException, InvalidRasterBandException from eis_toolkit.raster_processing import reclassify from tests.raster_processing.clip_test import raster_path as SMALL_RASTER_PATH @@ -194,3 +196,52 @@ def test_reclassify_with_quantiles_main(): assert isinstance(result, Tuple) assert isinstance(result[0], np.ndarray) assert isinstance(result[1], dict) + + +def test_invalid_band_selection(): + """Test that an invalid band selection raises the correct exception.""" + with pytest.raises(InvalidRasterBandException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_defined_intervals(raster=raster, interval_size=2, bands=[3, 4]) + + +def test_reclassify_with_defined_intervals_invalid_interval_size(): + """Test that an invalid interval size raises the correct exception.""" + with pytest.raises(InvalidParameterValueException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_defined_intervals(raster=raster, interval_size=0) + + +def test_reclassify_with_equal_intervals_invalid_number_of_intervals(): + """Test that an invalid number of intervals raises the correct exception.""" + with pytest.raises(InvalidParameterValueException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_equal_intervals(raster=raster, number_of_intervals=1) + + +def test_reclassify_with_quantiles_invalid_number_of_quantiles(): + """Test that an invalid number of quantiles raises the correct exception.""" + with pytest.raises(InvalidParameterValueException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_quantiles(raster=raster, number_of_quantiles=1) + + +def test_reclassify_with_natural_breaks_invalid_number_of_classes(): + """Test that an invalid number of classes raises the correct exception.""" + with pytest.raises(InvalidParameterValueException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_natural_breaks(raster=raster, number_of_classes=1) + + +def test_reclassify_with_geometric_intervals_invalid_number_of_classes(): + """Test that an invalid number of classes raises the correct exception.""" + with pytest.raises(InvalidParameterValueException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_geometrical_intervals(raster=raster, number_of_classes=1) + + +def test_reclassify_with_standard_deviation_invalid_number_of_intervals(): + """Test that an invalid number of intervals raises the correct exception.""" + with pytest.raises(InvalidParameterValueException): + with rasterio.open(SMALL_RASTER_PATH) as raster: + reclassify.reclassify_with_standard_deviation(raster=raster, number_of_intervals=1)