{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import tensorflow as tf\n", "from tensorflow.keras.preprocessing.image import ImageDataGenerator\n", "import os\n", "from tensorflow.keras import layers, models\n", "from tensorflow.keras.optimizers import Adam\n", "import numpy as np\n", "from sklearn.metrics import f1_score, precision_score, recall_score, confusion_matrix" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Found 32 images belonging to 4 classes.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Found 8 images belonging to 4 classes.\n", "Class indices: {'Cyst': 0, 'Normal': 1, 'Stone': 2, 'Tumor': 3}\n", "done\n" ] } ], "source": [ "# Define paths for the dataset\n", "base_dir = '../images'\n", "\n", "# Create ImageDataGenerators for training, validation, and testing\n", "data_gen = ImageDataGenerator(\n", " rescale=1.0/255, # Normalize pixel values\n", " validation_split=0.2 # Split for validation\n", ")\n", "\n", "# Load training data\n", "train_data = data_gen.flow_from_directory(\n", " base_dir,\n", " target_size=(150, 150),\n", " batch_size=2,\n", " class_mode='categorical',\n", " subset='training'\n", ")\n", "\n", "# Load validation data\n", "val_data = data_gen.flow_from_directory(\n", " base_dir,\n", " target_size=(150, 150),\n", " batch_size=2,\n", " class_mode='categorical',\n", " subset='validation'\n", ")\n", "\n", "# Print class indices\n", "print(\"Class indices:\", train_data.class_indices)\n", "print('done')" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "input_shape = (150, 150, 3) # 750x750 RGB images\n", "num_classes = 4" ] }, { "cell_type": "code", "execution_count": 109, "metadata": {}, "outputs": [], "source": [ "# Create the CNN model\n", "model = models.Sequential([\n", " # Input layer\n", " layers.Input(shape=input_shape),\n", " \n", " # First Convolutional Block\n", " layers.Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same'),\n", " layers.BatchNormalization(),\n", " layers.MaxPooling2D(pool_size=(2, 2)),\n", " \n", " # Second Convolutional Block\n", " layers.Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'),\n", " layers.BatchNormalization(),\n", " layers.MaxPooling2D(pool_size=(2, 2)),\n", " \n", " # Third Convolutional Block\n", " layers.Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same'),\n", " layers.BatchNormalization(),\n", " layers.MaxPooling2D(pool_size=(2, 2)),\n", " \n", " # Fourth Convolutional Block\n", " layers.Conv2D(256, kernel_size=(3, 3), activation='relu', padding='same'),\n", " layers.BatchNormalization(),\n", " layers.MaxPooling2D(pool_size=(2, 2)),\n", " \n", " # Fifth Convolutional Block\n", " layers.Conv2D(512, kernel_size=(3, 3), activation='relu', padding='same'),\n", " layers.BatchNormalization(),\n", " layers.MaxPooling2D(pool_size=(2, 2)),\n", " \n", " # Fully Connected Layers\n", " layers.Flatten(),\n", " # layers.Dense(1024, activation='relu'), # Adjusted to match the input shape\n", " # layers.Dropout(0.5),\n", " # layers.Dense(128, activation='relu'),\n", " # layers.Dropout(0.5),\n", " # \n", " # Output Layer\n", " layers.Dense(num_classes, activation='softmax')\n", "])\n", "\n", "# Compile the model\n", "model.compile(optimizer=Adam(learning_rate=0.00001),\n", " loss='categorical_crossentropy', # Use 'categorical_crossentropy' for one-hot encoded labels\n", " metrics=['accuracy', 'f1_score'])\n" ] }, { "cell_type": "code", "execution_count": 110, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 1/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m6s\u001b[0m 158ms/step - accuracy: 0.5123 - f1_score: 0.4374 - loss: 1.7693 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.3858\n", "Epoch 2/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 127ms/step - accuracy: 0.9428 - f1_score: 0.8688 - loss: 0.0977 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.3971\n", "Epoch 3/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 144ms/step - accuracy: 1.0000 - f1_score: 0.9412 - loss: 0.0092 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.4072\n", "Epoch 4/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 141ms/step - accuracy: 1.0000 - f1_score: 0.9265 - loss: 0.0102 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.4199\n", "Epoch 5/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 140ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 0.0051 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.4359\n", "Epoch 6/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 140ms/step - accuracy: 1.0000 - f1_score: 0.9118 - loss: 0.0038 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.4536\n", "Epoch 7/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 130ms/step - accuracy: 1.0000 - f1_score: 0.9265 - loss: 0.0032 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.4734\n", "Epoch 8/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 136ms/step - accuracy: 1.0000 - f1_score: 0.8824 - loss: 0.0044 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.4929\n", "Epoch 9/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 138ms/step - accuracy: 1.0000 - f1_score: 0.9265 - loss: 0.0031 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.5133\n", "Epoch 10/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 129ms/step - accuracy: 1.0000 - f1_score: 0.9706 - loss: 0.0024 - val_accuracy: 0.2500 - val_f1_score: 0.1000 - val_loss: 1.5346\n", "Epoch 11/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 127ms/step - accuracy: 1.0000 - f1_score: 0.8971 - loss: 0.0029 - val_accuracy: 0.2500 - val_f1_score: 0.1111 - val_loss: 1.5535\n", "Epoch 12/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 135ms/step - accuracy: 1.0000 - f1_score: 0.9412 - loss: 0.0029 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.5660\n", "Epoch 13/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 141ms/step - accuracy: 1.0000 - f1_score: 0.9118 - loss: 0.0014 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.5759\n", "Epoch 14/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 129ms/step - accuracy: 1.0000 - f1_score: 0.8824 - loss: 0.0016 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.5809\n", "Epoch 15/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 140ms/step - accuracy: 1.0000 - f1_score: 0.9412 - loss: 0.0014 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.5782\n", "Epoch 16/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 132ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 0.0011 - val_accuracy: 0.2500 - val_f1_score: 0.1250 - val_loss: 1.5731\n", "Epoch 17/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 133ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 0.0015 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.5475\n", "Epoch 18/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 130ms/step - accuracy: 1.0000 - f1_score: 0.9412 - loss: 0.0010 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.5120\n", "Epoch 19/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 137ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 0.0010 - val_accuracy: 0.2500 - val_f1_score: 0.1250 - val_loss: 1.4640\n", "Epoch 20/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 142ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 0.0013 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.4229\n", "Epoch 21/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 135ms/step - accuracy: 1.0000 - f1_score: 0.9118 - loss: 9.3909e-04 - val_accuracy: 0.3750 - val_f1_score: 0.2429 - val_loss: 1.3761\n", "Epoch 22/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 133ms/step - accuracy: 1.0000 - f1_score: 0.9118 - loss: 8.4527e-04 - val_accuracy: 0.5000 - val_f1_score: 0.3333 - val_loss: 1.3416\n", "Epoch 23/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 136ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 8.5071e-04 - val_accuracy: 0.5000 - val_f1_score: 0.3333 - val_loss: 1.3063\n", "Epoch 24/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 134ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 7.2937e-04 - val_accuracy: 0.5000 - val_f1_score: 0.3429 - val_loss: 1.2619\n", "Epoch 25/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 135ms/step - accuracy: 1.0000 - f1_score: 0.9265 - loss: 6.2996e-04 - val_accuracy: 0.5000 - val_f1_score: 0.3429 - val_loss: 1.1914\n", "Epoch 26/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 135ms/step - accuracy: 1.0000 - f1_score: 0.9265 - loss: 7.7824e-04 - val_accuracy: 0.5000 - val_f1_score: 0.3429 - val_loss: 1.1314\n", "Epoch 27/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 135ms/step - accuracy: 1.0000 - f1_score: 0.8971 - loss: 6.3872e-04 - val_accuracy: 0.6250 - val_f1_score: 0.5333 - val_loss: 1.0617\n", "Epoch 28/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 134ms/step - accuracy: 1.0000 - f1_score: 0.9412 - loss: 6.7060e-04 - val_accuracy: 0.7500 - val_f1_score: 0.7333 - val_loss: 1.0080\n", "Epoch 29/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 140ms/step - accuracy: 1.0000 - f1_score: 0.9559 - loss: 6.5673e-04 - val_accuracy: 0.7500 - val_f1_score: 0.7333 - val_loss: 0.9536\n", "Epoch 30/30\n", "\u001b[1m16/16\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 135ms/step - accuracy: 1.0000 - f1_score: 0.9412 - loss: 7.8768e-04 - val_accuracy: 0.7500 - val_f1_score: 0.7333 - val_loss: 0.9146\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "WARNING:absl:You are saving your model as an HDF5 file via `model.save()` or `keras.saving.save_model(model)`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')` or `keras.saving.save_model(model, 'my_model.keras')`. \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model saved as medical_classifier.h5\n" ] } ], "source": [ "# Train the model\n", "history = model.fit(\n", " train_data,\n", " steps_per_epoch=len(train_data),\n", " epochs=30,\n", " validation_data=val_data,\n", " validation_steps=len(val_data)\n", ")\n", "\n", "# Save the model\n", "model.save('medical_classifier.h5')\n", "print(\"Model saved as medical_classifier.h5\")" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m4/4\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 32ms/step\n", "F1 Score on validation data: 0.35\n" ] } ], "source": [ "\n", "val_data.reset()\n", "predictions = model.predict(val_data, steps=len(val_data), verbose=1)\n", "y_pred = np.argmax(predictions, axis=1)\n", "y_true = val_data.classes\n", "f1 = f1_score(y_true, y_pred, average='weighted')\n", "print(\"F1 Score on validation data:\", f1)" ] }, { "cell_type": "code", "execution_count": 111, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m4/4\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 25ms/step\n", "F1 Score on validation data: 0.3666666666666667\n" ] } ], "source": [ "from sklearn.metrics import f1_score\n", "import numpy as np\n", "from tensorflow.keras.preprocessing import image\n", "\n", "# Calculate F1 score on validation data\n", "val_data.reset()\n", "predictions = model.predict(val_data, steps=len(val_data), verbose=1)\n", "y_pred = np.argmax(predictions, axis=1)\n", "y_true = val_data.classes\n", "f1 = f1_score(y_true, y_pred, average='weighted')\n", "print(\"F1 Score on validation data:\", f1)\n", "\n", "# Test the model on a random image\n", "def test_random_image(img_path):\n", " img = image.load_img(img_path, target_size=(150, 150))\n", " img_array = image.img_to_array(img)\n", " img_array = np.expand_dims(img_array, axis=0)\n", " img_array /= 255.0\n", "\n", " prediction = model.predict(img_array)\n", " predicted_class = np.argmax(prediction, axis=1)\n", " class_indices = {v: k for k, v in train_data.class_indices.items()}\n", " predicted_label = class_indices[predicted_class[0]]\n", "\n", " print(f\"Predicted class: {predicted_label}\")\n" ] }, { "cell_type": "code", "execution_count": 114, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m4/4\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 29ms/step\n", "F1 Score on validation data: 0.3666666666666667\n", "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 48ms/step\n", "Predicted class: Cyst\n" ] } ], "source": [ "val_data.reset()\n", "predictions = model.predict(val_data, steps=len(val_data), verbose=1)\n", "y_pred = np.argmax(predictions, axis=1)\n", "y_true = val_data.classes\n", "f1 = f1_score(y_true, y_pred, average='weighted')\n", "print(\"F1 Score on validation data:\", f1)\n", "random_image_path = os.path.join(base_dir, 'test', 'Cyst', 'Cyst- (18).jpg') # Replace 'class_name' and 'random_image.jpg' with actual values\n", "test_random_image(random_image_path)" ] }, { "cell_type": "code", "execution_count": 118, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 50ms/step\n", "Predicted class: Normal\n" ] } ], "source": [ "random_image_path = os.path.join(base_dir, 'test', 'Normal', 'Normal- (286).jpg') # Replace 'class_name' and 'random_image.jpg' with actual values\n", "test_random_image(random_image_path)" ] }, { "cell_type": "code", "execution_count": 120, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 51ms/step\n", "Predicted class: Stone\n" ] } ], "source": [ "random_image_path = os.path.join(base_dir, 'test', 'Stone', 'Stone- (62).jpg') # Replace 'class_name' and 'random_image.jpg' with actual values\n", "test_random_image(random_image_path)" ] }, { "cell_type": "code", "execution_count": 122, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 56ms/step\n", "Predicted class: Tumor\n" ] } ], "source": [ "random_image_path = os.path.join(base_dir, 'test', 'Tumor', 'Tumor- (54).jpg') # Replace 'class_name' and 'random_image.jpg' with actual values\n", "test_random_image(random_image_path)" ] }, { "cell_type": "code", "execution_count": 132, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "WARNING:absl:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 55ms/step\n", "Predicted class: Tumor\n" ] } ], "source": [ "r_img_path = os.path.join(base_dir, 'test', 'Tumor', 'Tumor- (44).jpg') # Replace 'class_name' and 'random_image.jpg' with actual values\n", "import_model = tf.keras.models.load_model('./medical_classifier.h5')\n", "img = image.load_img(r_img_path, target_size=(150, 150))\n", "img_array = image.img_to_array(img)\n", "img_array = np.expand_dims(img_array, axis=0)\n", "img_array /= 255.0\n", "\n", "prediction = model.predict(img_array)\n", "predicted_class = np.argmax(prediction, axis=1)\n", "class_indices = {v: k for k, v in train_data.class_indices.items()}\n", "predicted_label = class_indices[predicted_class[0]]\n", "\n", "print(f\"Predicted class: {predicted_label}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.10.11" } }, "nbformat": 4, "nbformat_minor": 2 }