{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"},{"sourceId":778,"sourceType":"modelInstanceVersion","modelInstanceId":645,"modelId":55}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# The code is used for training on kaggle the input is cassava disease classification\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        # Check if the file extension is not '.jpg'\n        if not filename.lower().endswith('.jpg'):\n            print(os.path.join(dirname, filename))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras import layers, Model, optimizers\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\n\n# Define paths\nPATHS = {\n    'TRAIN_CSV': '/kaggle/input/cassava-leaf-disease-classification/train.csv',\n    'TEST_CSV': '/kaggle/input/cassava-leaf-disease-classification/sample_submission.csv',\n    'DISEASE_MAP': '/kaggle/input/cassava-leaf-disease-classification/label_num_to_disease_map.json',\n    'TRAIN_IMAGES': '/kaggle/input/cassava-leaf-disease-classification/train_images',\n    'TEST_IMAGES': '/kaggle/input/cassava-leaf-disease-classification/test_images',\n    'OUTPUT': '/kaggle/working/submission.csv',\n    'MODEL_CACHE': '/kaggle/working/model_cache',\n    'WEIGHTS': '/kaggle/working/weights',\n    'PLOTS': '/kaggle/working/plots',\n    'SAVED_MODEL': '/kaggle/working/cassava_disease_model_tf'\n}\n\n# Create necessary directories\nfor directory in ['MODEL_CACHE', 'WEIGHTS', 'PLOTS', 'SAVED_MODEL']:\n    os.makedirs(PATHS[directory], exist_ok=True)\n\n# Load disease mapping\nwith open(PATHS['DISEASE_MAP'], 'r') as f:\n    disease_map = json.load(f)\n    \n# Convert from string keys to integer keys\ndisease_map = {int(k): v for k, v in disease_map.items()}\nnum_classes = len(disease_map)\nprint(f\"Number of classes: {num_classes}\")\nprint(\"Disease mapping:\", disease_map)\n\n# Load training data\ntrain_df = pd.read_csv(PATHS['TRAIN_CSV'])\nprint(f\"Training data shape: {train_df.shape}\")\nprint(train_df.head())\n\n# Check class distribution\nclass_distribution = train_df['label'].value_counts().sort_index()\nprint(\"Class distribution:\")\nfor class_id, count in class_distribution.items():\n    print(f\"Class {class_id} ({disease_map[class_id]}): {count} images\")\n\n# Split data into train and validation sets\ntrain_df, val_df = train_test_split(train_df, test_size=0.2, stratify=train_df['label'], random_state=42)\nprint(f\"Training set: {train_df.shape[0]} images\")\nprint(f\"Validation set: {val_df.shape[0]} images\")\n\n# Data augmentation for training - slightly reduced parameters for more stability\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255,\n    rotation_range=15,          # Reduced from 20\n    width_shift_range=0.15,     # Reduced from 0.2\n    height_shift_range=0.15,    # Reduced from 0.2\n    shear_range=0.15,           # Reduced from 0.2\n    zoom_range=0.15,            # Reduced from 0.2\n    horizontal_flip=True,\n    fill_mode='nearest'\n)\n\n# Only rescaling for validation\nval_datagen = ImageDataGenerator(rescale=1./255)\n\n# Image dimensions\nimg_height, img_width = 224, 224\nbatch_size = 32\n\n# Convert label integers to strings to work with categorical mode\ntrain_df['label_str'] = train_df['label'].astype(str)\nval_df['label_str'] = val_df['label'].astype(str)\n\n# Create data generators with explicit shuffle setting\ntrain_generator = train_datagen.flow_from_dataframe(\n    dataframe=train_df,\n    directory=PATHS['TRAIN_IMAGES'],\n    x_col='image_id',\n    y_col='label_str',\n    target_size=(img_height, img_width),\n    batch_size=batch_size,\n    class_mode='categorical',\n    shuffle=True  # Explicit setting\n)\n\nvalidation_generator = val_datagen.flow_from_dataframe(\n    dataframe=val_df,\n    directory=PATHS['TRAIN_IMAGES'],\n    x_col='image_id',\n    y_col='label_str',\n    target_size=(img_height, img_width),\n    batch_size=batch_size,\n    class_mode='categorical',\n    shuffle=False  # No shuffling for validation\n)\n\n# Verify shuffling is working\ndef verify_shuffling(generator, num_batches=2):\n    \"\"\"Check if a generator is actually shuffling data between epochs\"\"\"\n    # Get first batch indices from first epoch\n    batch_indices_epoch1 = []\n    for i in range(num_batches):\n        batch_x, _ = next(generator)\n        # Store some sample data from this batch to identify it\n        batch_indices_epoch1.append(batch_x[0, 0, 0, 0])  # Use first pixel value as identifier\n    \n    # Reset the generator to simulate a new epoch\n    generator.reset()\n    \n    # Get first batch indices from second \"epoch\"\n    batch_indices_epoch2 = []\n    for i in range(num_batches):\n        batch_x, _ = next(generator)\n        batch_indices_epoch2.append(batch_x[0, 0, 0, 0])\n    \n    # Compare the batches\n    different_batches = sum(abs(epoch1 - epoch2) > 1e-5 \n                           for epoch1, epoch2 in zip(batch_indices_epoch1, batch_indices_epoch2))\n    \n    print(f\"Shuffling verification: {different_batches}/{num_batches} batches were different between epochs\")\n    print(f\"First epoch batch identifiers: {batch_indices_epoch1}\")\n    print(f\"Second epoch batch identifiers: {batch_indices_epoch2}\")\n    \n    # Reset the generator again for actual training\n    generator.reset()\n    return different_batches > 0\n\n# Run the verification\nis_shuffling = verify_shuffling(train_generator)\nprint(f\"Training data is being shuffled: {is_shuffling}\")\n\n# Validate dataset splits\ndef validate_dataset_splits(train_gen, val_gen):\n    \"\"\"Validate that the dataset splits have reasonable sizes and class distributions\"\"\"\n    # Check total samples\n    print(\"\\n=== Dataset Split Validation ===\")\n    print(f\"Total training samples: {train_gen.n}\")\n    print(f\"Total validation samples: {val_gen.n}\")\n    \n    # Ensure reasonable split ratio\n    total_samples = train_gen.n + val_gen.n\n    train_ratio = train_gen.n / total_samples\n    val_ratio = val_gen.n / total_samples\n    \n    print(f\"Training split: {train_ratio:.2%}\")\n    print(f\"Validation split: {val_ratio:.2%}\")\n    \n    # Warn if validation set is too small or too large\n    if val_gen.n < 100:\n        print(\"WARNING: Validation set may be too small (<100 samples)\")\n    if val_ratio < 0.1:\n        print(\"WARNING: Validation set may be too small (<10% of data)\")\n    if val_ratio > 0.3:\n        print(\"WARNING: Validation set may be too large (>30% of data)\")\n    \n    # Check class distribution in both splits\n    print(\"\\n--- Class Distribution ---\")\n    class_counts_train = train_df['label'].value_counts().sort_index()\n    class_counts_val = val_df['label'].value_counts().sort_index()\n    \n    print(\"Class distribution in training set:\")\n    for class_id in sorted(class_counts_train.index):\n        count = class_counts_train.get(class_id, 0)\n        percentage = count / train_gen.n * 100\n        class_name = disease_map.get(class_id, f\"Class {class_id}\")\n        print(f\"  {class_name}: {count} samples ({percentage:.1f}%)\")\n    \n    print(\"\\nClass distribution in validation set:\")\n    for class_id in sorted(class_counts_val.index):\n        count = class_counts_val.get(class_id, 0)\n        percentage = count / val_gen.n * 100\n        class_name = disease_map.get(class_id, f\"Class {class_id}\")\n        print(f\"  {class_name}: {count} samples ({percentage:.1f}%)\")\n    \n    # Check if any class has very few samples\n    min_samples_warning = 50  # Arbitrary threshold\n    for class_id in sorted(class_counts_val.index):\n        if class_counts_val.get(class_id, 0) < min_samples_warning:\n            print(f\"WARNING: Class {class_id} has fewer than {min_samples_warning} samples in validation set\")\n    \n    print(\"=== End of Dataset Validation ===\\n\")\n\n# Run the validation\nvalidate_dataset_splits(train_generator, validation_generator)\n\n# Calculate steps properly - IMPORTANT FIX\nsteps_per_epoch = train_generator.n // train_generator.batch_size\nvalidation_steps = validation_generator.n // validation_generator.batch_size\n\n# Print steps information\nprint(f\"Training generator has {train_generator.n} samples with batch size {train_generator.batch_size}\")\nprint(f\"Using {steps_per_epoch} steps per epoch for training\")\nprint(f\"Validation generator has {validation_generator.n} samples with batch size {validation_generator.batch_size}\")\nprint(f\"Using {validation_steps} steps per epoch for validation\")\n\n# Load the pretrained model\ncropnet_path = \"/kaggle/input/cropnet/tensorflow1/classifier-cassava-disease-v1/1\"\n\n# Load the base model using TFSMLayer\nbase_model_layer = tf.keras.layers.TFSMLayer(\n    cropnet_path,\n    call_endpoint='default'\n)\n\n# Examine the model's input/output signature\nloaded = tf.saved_model.load(cropnet_path)\nprint(\"Model signature info:\", loaded.signatures['default'])\n\n# Create a new model with the pretrained base\ninputs = tf.keras.Input(shape=(img_height, img_width, 3))\nbase_outputs = base_model_layer(inputs)\n\n# Print the output type and content to understand its structure\nprint(f\"Base model output type: {type(base_outputs)}\")\nprint(f\"Base model output keys: {base_outputs.keys() if isinstance(base_outputs, dict) else 'Not a dictionary'}\")\n\n# Extract the appropriate tensor from the dictionary\nif isinstance(base_outputs, dict):\n    # Try to find the most likely output tensor from the dictionary\n    output_key = list(base_outputs.keys())[0]\n    print(f\"Using output key: {output_key}\")\n    x = base_outputs[output_key]\nelse:\n    x = base_outputs\n\nprint(f\"Selected output shape: {x.shape}\")\n\n# Add new classification head\nx = layers.Dense(256, activation='relu')(x)\nx = layers.Dropout(0.5)(x)\noutputs = layers.Dense(num_classes, activation='softmax')(x)\n\nmodel = Model(inputs=inputs, outputs=outputs)\n\n# Freeze the base model initially\nbase_model_layer.trainable = False\n\n# Compile the model\nmodel.compile(\n    optimizer=optimizers.Adam(learning_rate=1e-4),\n    loss='categorical_crossentropy',\n    metrics=['accuracy']\n)\n\n# Function to validate label configurations\ndef validate_label_configuration(train_gen, model):\n    \"\"\"Verify that the class_mode and model output layer are compatible\"\"\"\n    print(\"\\n=== Label Configuration Validation ===\")\n    \n    # Check class_mode setting\n    class_mode = train_gen.class_mode\n    print(f\"Generator class_mode: {class_mode}\")\n    \n    # Get the model's output layer\n    output_layer = model.layers[-1]\n    \n    # Get output shape correctly - this is the fix\n    output_shape = model.output_shape\n    \n    # Get activation function\n    output_activation = output_layer.activation.__name__ if hasattr(output_layer.activation, '__name__') else 'unknown'\n    \n    print(f\"Model output layer shape: {output_shape}\")\n    print(f\"Model output activation function: {output_activation}\")\n    \n    # Validate compatibility\n    is_compatible = True\n    error_message = None\n    \n    if class_mode == 'categorical':\n        if output_activation != 'softmax':\n            is_compatible = False\n            error_message = \"Using 'categorical' class_mode but output layer activation is not 'softmax'\"\n        if output_shape[-1] != num_classes:\n            is_compatible = False\n            error_message = f\"Output layer has {output_shape[-1]} units but there are {num_classes} classes\"\n    elif class_mode == 'binary':\n        if output_activation != 'sigmoid':\n            is_compatible = False\n            error_message = \"Using 'binary' class_mode but output layer activation is not 'sigmoid'\"\n        if output_shape[-1] != 1:\n            is_compatible = False\n            error_message = f\"Binary classification should have 1 output unit, but found {output_shape[-1]}\"\n    elif class_mode == 'sparse':\n        if output_activation != 'softmax':\n            is_compatible = False\n            error_message = \"Using 'sparse' class_mode but need 'softmax' activation for multi-class\"\n    \n    # Print validation results\n    if is_compatible:\n        print(\"✓ Class mode and model output layer are compatible\")\n    else:\n        print(f\"⚠ CONFIGURATION ERROR: {error_message}\")\n        print(\"This will likely cause training issues!\")\n    \n    # Verify label encoding by checking a sample batch\n    batch_x, batch_y = next(train_gen)\n    print(f\"\\nSample batch shape - X: {batch_x.shape}, Y: {batch_y.shape}\")\n    \n    # For categorical, we expect one-hot encoding (shape should be (batch_size, num_classes))\n    if class_mode == 'categorical':\n        if batch_y.shape[1] != num_classes:\n            print(f\"⚠ LABEL ERROR: Expected y shape to be (batch_size, {num_classes}) but got {batch_y.shape}\")\n        else:\n            print(f\"✓ Labels are correctly one-hot encoded with {num_classes} classes\")\n    \n    # Reset the generator\n    train_gen.reset()\n    print(\"=== End of Label Configuration Validation ===\\n\")\n\n# Run label validation\nvalidate_label_configuration(train_generator, model)\n\n# Define callbacks\ncheckpoint = ModelCheckpoint(\n    os.path.join(PATHS['WEIGHTS'], 'best_model.keras'),\n    monitor='val_accuracy',\n    save_best_only=True,\n    mode='max',\n    verbose=1\n)\n\nearly_stopping = EarlyStopping(\n    monitor='val_accuracy',\n    patience=10,\n    restore_best_weights=True,\n    verbose=1\n)\n\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_loss',\n    factor=0.2,\n    patience=5,\n    min_lr=1e-6,\n    verbose=1\n)\n\ncallbacks = [checkpoint, early_stopping, reduce_lr]\n\n# Train the model with frozen base - using corrected steps_per_epoch\nhistory_frozen = model.fit(\n    train_generator,\n    steps_per_epoch=steps_per_epoch,  # Use calculated value\n    epochs=10,\n    validation_data=validation_generator,\n    validation_steps=validation_steps,  # Use calculated value\n    callbacks=callbacks\n)\n\n# Unfreeze the base model for fine-tuning\nbase_model_layer.trainable = True\n\n# Recompile with a lower learning rate for fine-tuning\n# IMPROVEMENT: Reduced learning rate to prevent overfitting\nmodel.compile(\n    optimizer=optimizers.Adam(learning_rate=5e-6),  # Reduced from 1e-5 to 5e-6\n    loss='categorical_crossentropy',\n    metrics=['accuracy']\n)\n\n# Continue training with unfrozen base - using corrected steps_per_epoch\n# IMPROVEMENT: Reduced number of fine-tuning epochs\nhistory_unfrozen = model.fit(\n    train_generator,\n    steps_per_epoch=steps_per_epoch,  # Use calculated value\n    epochs=20,  # Reduced from 30 to 20\n    validation_data=validation_generator,\n    validation_steps=validation_steps,  # Use calculated value\n    callbacks=callbacks,\n    initial_epoch=history_frozen.epoch[-1] + 1  # Continue from where we left off\n)\n\n# Improved function to plot training history\ndef plot_improved_training_history(history_frozen, history_unfrozen=None):\n    \"\"\"Plot training history with properly combined epochs for frozen and unfrozen training\"\"\"\n    # Create figure\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))\n    \n    # Merge histories for continuous plotting\n    merged_history = {}\n    \n    # Start with frozen history metrics\n    for metric in history_frozen.history:\n        merged_history[metric] = list(history_frozen.history[metric])\n    \n    # Append unfrozen history if available\n    if history_unfrozen:\n        for metric in history_unfrozen.history:\n            if metric in merged_history:\n                merged_history[metric].extend(history_unfrozen.history[metric])\n            else:\n                # Handle case where a metric might only exist in one history\n                merged_history[metric] = list(history_unfrozen.history[metric])\n    \n    # Create a single x-axis for all epochs\n    epochs = range(1, len(merged_history.get('accuracy', [])) + 1)\n    \n    # Add a vertical line to mark the transition from frozen to unfrozen\n    frozen_epochs = len(history_frozen.history.get('accuracy', []))\n    \n    # Plot accuracy\n    ax1.plot(epochs, merged_history.get('accuracy', []), 'b-', label='Training Accuracy')\n    if 'val_accuracy' in merged_history:\n        ax1.plot(epochs, merged_history.get('val_accuracy', []), 'r-', label='Validation Accuracy')\n    \n    # Add transition line for accuracy plot\n    if history_unfrozen:\n        ax1.axvline(x=frozen_epochs, color='g', linestyle='--', \n                   label='Transition: Frozen → Unfrozen')\n    \n    ax1.set_title('Model Accuracy')\n    ax1.set_ylabel('Accuracy')\n    ax1.set_xlabel('Epoch')\n    ax1.legend()\n    ax1.grid(True, linestyle='--', alpha=0.7)\n    \n    # Plot loss\n    ax2.plot(epochs, merged_history.get('loss', []), 'b-', label='Training Loss')\n    if 'val_loss' in merged_history:\n        ax2.plot(epochs, merged_history.get('val_loss', []), 'r-', label='Validation Loss')\n    \n    # Add transition line for loss plot\n    if history_unfrozen:\n        ax2.axvline(x=frozen_epochs, color='g', linestyle='--',\n                   label='Transition: Frozen → Unfrozen')\n    \n    ax2.set_title('Model Loss')\n    ax2.set_ylabel('Loss')\n    ax2.set_xlabel('Epoch')\n    ax2.legend()\n    ax2.grid(True, linestyle='--', alpha=0.7)\n    \n    # Add a text annotation to explain the phases\n    if history_unfrozen:\n        plt.figtext(0.5, 0.01, \n                   f\"Phase 1 (Epochs 1-{frozen_epochs}): Base model frozen | \"\n                   f\"Phase 2 (Epochs {frozen_epochs+1}-{len(epochs)}): Full model fine-tuning\",\n                   ha=\"center\", fontsize=10, bbox={\"facecolor\":\"orange\", \"alpha\":0.2, \"pad\":5})\n    \n    plt.tight_layout()\n    plt.subplots_adjust(bottom=0.15)  # Make room for the text\n    \n    # Save figure\n    plt.savefig(os.path.join(PATHS['PLOTS'], 'improved_training_history.png'), dpi=300)\n    plt.show()\n    \n    # Print summary statistics\n    print(\"\\n=== Training History Summary ===\")\n    \n    # Initial phase stats\n    print(f\"Phase 1 (Frozen base model) - {frozen_epochs} epochs:\")\n    print(f\"  Starting train accuracy: {history_frozen.history['accuracy'][0]:.4f}\")\n    print(f\"  Final train accuracy: {history_frozen.history['accuracy'][-1]:.4f}\")\n    \n    if 'val_accuracy' in history_frozen.history:\n        print(f\"  Starting validation accuracy: {history_frozen.history['val_accuracy'][0]:.4f}\")\n        print(f\"  Final validation accuracy: {history_frozen.history['val_accuracy'][-1]:.4f}\")\n    \n    # Fine-tuning phase stats\n    if history_unfrozen:\n        unfrozen_epochs = len(history_unfrozen.history['accuracy'])\n        print(f\"\\nPhase 2 (Fine-tuning) - {unfrozen_epochs} epochs:\")\n        print(f\"  Starting train accuracy: {history_unfrozen.history['accuracy'][0]:.4f}\")\n        print(f\"  Final train accuracy: {history_unfrozen.history['accuracy'][-1]:.4f}\")\n        \n        if 'val_accuracy' in history_unfrozen.history:\n            print(f\"  Starting validation accuracy: {history_unfrozen.history['val_accuracy'][0]:.4f}\")\n            print(f\"  Final validation accuracy: {history_unfrozen.history['val_accuracy'][-1]:.4f}\")\n        \n        # Improvement calculation\n        acc_improvement = history_unfrozen.history['accuracy'][-1] - history_frozen.history['accuracy'][-1]\n        print(f\"\\nImprovement from fine-tuning: {acc_improvement:.4f} accuracy\")\n        \n        if 'val_accuracy' in history_unfrozen.history and 'val_accuracy' in history_frozen.history:\n            val_acc_improvement = history_unfrozen.history['val_accuracy'][-1] - history_frozen.history['val_accuracy'][-1]\n            print(f\"Validation accuracy improvement: {val_acc_improvement:.4f}\")\n    \n    print(\"=== End of Training History Summary ===\\n\")\n\n# Use the improved plotting function\nplot_improved_training_history(history_frozen, history_unfrozen)\n\n# Export model as SavedModel (TF2 format)\n@tf.function(input_signature=[tf.TensorSpec(shape=[None, img_height, img_width, 3], dtype=tf.float32, name='input_image')])\ndef serving_fn(input_image):\n    return {'predictions': model(input_image, training=False)}\n\n# Save the model in SavedModel format\ntf.saved_model.save(\n    model,\n    PATHS['SAVED_MODEL'],\n    signatures={'serving_default': serving_fn}\n)\n\nprint(f\"Model saved to {PATHS['SAVED_MODEL']} in SavedModel format\")\n\n# Create a zip file of the SavedModel directory for easy download\nimport shutil\nshutil.make_archive(\n    os.path.join('/kaggle/working', 'cassava_disease_model'),  # output name\n    'zip',                                                     # format\n    PATHS['SAVED_MODEL']                                      # source directory\n)\n\nprint(f\"SavedModel zipped to /kaggle/working/cassava_disease_model.zip\")\n\n# Load test data\ntest_df = pd.read_csv(PATHS['TEST_CSV'])\nprint(f\"Test data shape: {test_df.shape}\")\n\n# Create test generator (only rescaling, no augmentation)\ntest_datagen = ImageDataGenerator(rescale=1./255)\ntest_generator = test_datagen.flow_from_dataframe(\n    dataframe=test_df,\n    directory=PATHS['TEST_IMAGES'],\n    x_col='image_id',\n    y_col=None,  # No labels for test data\n    target_size=(img_height, img_width),\n    batch_size=batch_size,\n    class_mode=None,  # No labels\n    shuffle=False  # Keep the order for submission\n)\n\n# Calculate steps for test predictions - CRITICAL FIX: Convert to integer\ntest_steps = int(np.ceil(test_generator.n / test_generator.batch_size))\nprint(f\"Test generator has {test_generator.n} samples\")\nprint(f\"Using {test_steps} steps for prediction\")\n\n# Predict on test data\npredictions = model.predict(test_generator, steps=test_steps)\npredicted_classes = np.argmax(predictions, axis=1)\n\n# Create submission file\ntest_df['label'] = predicted_classes\ntest_df.to_csv(PATHS['OUTPUT'], index=False)\nprint(f\"Submission file saved to {PATHS['OUTPUT']}\")\n\n# Print final model summary\nmodel.summary()\n\n# Verify the saved model can be loaded\nprint(\"\\nVerifying SavedModel by loading it and making a test prediction...\")\ntry:\n    # Load the model\n    loaded_model = tf.saved_model.load(PATHS['SAVED_MODEL'])\n    \n    # Get the serving signature\n    serving_signature = loaded_model.signatures['serving_default']\n    print(f\"Model loaded successfully with signature: {serving_signature}\")\n    \n    # Create a sample input (random tensor with correct shape)\n    sample_input = np.random.random((1, img_height, img_width, 3)).astype(np.float32)\n    \n    # Make a prediction\n    test_prediction = serving_signature(tf.constant(sample_input))\n    print(f\"Test prediction shape: {list(test_prediction.values())[0].shape}\")\n    print(\"SavedModel verification successful!\")\nexcept Exception as e:\n    print(f\"Error verifying SavedModel: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-10T01:52:02.847132Z","iopub.execute_input":"2025-04-10T01:52:02.84759Z"}},"outputs":[],"execution_count":null}]}