{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.16","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":21154,"databundleVersionId":1243559,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# TPU-Powered Flower Image Classifier\n\nThis notebook demonstrates building and training a machine learning model to classify flower images using the speed of TPUs. We cover data preparation, model definition, and training acceleration on Google's Tensor Processing Units, showcasing how to efficiently tackle image classification tasks.","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nimport math, re, os\nimport matplotlib.pyplot as plt\nfrom kaggle_datasets import KaggleDatasets\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau\n\nimport numpy as np\n\nprint(\"Tensorflow version \" + tf.__version__)","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:44:24.133829Z","iopub.execute_input":"2025-05-06T09:44:24.134396Z","iopub.status.idle":"2025-05-06T09:44:45.708320Z","shell.execute_reply.started":"2025-05-06T09:44:24.134366Z","shell.execute_reply":"2025-05-06T09:44:45.702111Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Setup TPU\n\nThis code snippet automatically detects if a TPU (Tensor Processing Unit) is available and configures the appropriate TensorFlow distribution strategy for model training. \n\nIf a TPU is found, it sets up a TPUStrategy for accelerated training; otherwise, it uses the default strategy for CPU/GPU. Finally, it reports the number of processing replicas available.","metadata":{}},{"cell_type":"code","source":"# Detect hardware, return appropriate distribution strategy\ntry:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect(tpu='local') # set tpu is local as it should be available in the VM\n    print('✅ Running on TPU ', tpu.master())\nexcept ValueError:\n    print('❌ Using CPU/GPU')\n    tpu = None\n\nif tpu:\n    strategy = tf.distribute.TPUStrategy(tpu)\nelse:\n    strategy = tf.distribute.get_strategy() # default distribution strategy in Tensorflow. Works on CPU and single GPU.\n\nprint(\"REPLICAS: \", strategy.num_replicas_in_sync)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:44:45.710562Z","iopub.execute_input":"2025-05-06T09:44:45.711000Z","iopub.status.idle":"2025-05-06T09:44:53.938261Z","shell.execute_reply.started":"2025-05-06T09:44:45.710973Z","shell.execute_reply":"2025-05-06T09:44:53.932738Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Setup CONSTANTS","metadata":{}},{"cell_type":"code","source":"GCS_DS_PATH = KaggleDatasets().get_gcs_path()\n\nIMAGE_SIZE = [512, 512] # at this size, a GPU will run out of memory. Use the TPU\nEPOCHS = 5\nNUM_CLASSES = 104\nBATCH_SIZE = 16 * strategy.num_replicas_in_sync\n\nNUM_TRAINING_IMAGES = 12753\nNUM_TEST_IMAGES = 7382\nTRAIN_SAMPLES = 12753\nVAL_SAMPLES = 3712\nSTEPS_PER_EPOCH = NUM_TRAINING_IMAGES // BATCH_SIZE\n\nCLASSES = ['pink primrose',    'hard-leaved pocket orchid', 'canterbury bells', 'sweet pea',     'wild geranium',     'tiger lily',           'moon orchid',              'bird of paradise', 'monkshood',        'globe thistle',         # 00 - 09\n           'snapdragon',       \"colt's foot\",               'king protea',      'spear thistle', 'yellow iris',       'globe-flower',         'purple coneflower',        'peruvian lily',    'balloon flower',   'giant white arum lily', # 10 - 19\n           'fire lily',        'pincushion flower',         'fritillary',       'red ginger',    'grape hyacinth',    'corn poppy',           'prince of wales feathers', 'stemless gentian', 'artichoke',        'sweet william',         # 20 - 29\n           'carnation',        'garden phlox',              'love in the mist', 'cosmos',        'alpine sea holly',  'ruby-lipped cattleya', 'cape flower',              'great masterwort', 'siam tulip',       'lenten rose',           # 30 - 39\n           'barberton daisy',  'daffodil',                  'sword lily',       'poinsettia',    'bolero deep blue',  'wallflower',           'marigold',                 'buttercup',        'daisy',            'common dandelion',      # 40 - 49\n           'petunia',          'wild pansy',                'primula',          'sunflower',     'lilac hibiscus',    'bishop of llandaff',   'gaura',                    'geranium',         'orange dahlia',    'pink-yellow dahlia',    # 50 - 59\n           'cautleya spicata', 'japanese anemone',          'black-eyed susan', 'silverbush',    'californian poppy', 'osteospermum',         'spring crocus',            'iris',             'windflower',       'tree poppy',            # 60 - 69\n           'gazania',          'azalea',                    'water lily',       'rose',          'thorn apple',       'morning glory',        'passion flower',           'lotus',            'toad lily',        'anthurium',             # 70 - 79\n           'frangipani',       'clematis',                  'hibiscus',         'columbine',     'desert-rose',       'tree mallow',          'magnolia',                 'cyclamen ',        'watercress',       'canna lily',            # 80 - 89\n           'hippeastrum ',     'bee balm',                  'pink quill',       'foxglove',      'bougainvillea',     'camellia',             'mallow',                   'mexican petunia',  'bromelia',         'blanket flower',        # 90 - 99\n           'trumpet creeper',  'blackberry lily',           'common tulip',     'wild rose']  ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:44:53.940394Z","iopub.execute_input":"2025-05-06T09:44:53.940635Z","iopub.status.idle":"2025-05-06T09:44:53.953852Z","shell.execute_reply.started":"2025-05-06T09:44:53.940609Z","shell.execute_reply":"2025-05-06T09:44:53.949145Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load data functions\n\n\ndecode_image(image_data)\n  - Decodes a JPEG image from raw data.\n  - Scales pixel values to the [0, 1] range and reshapes the image tensor.\n    \nread_labeled_tfrecord(example)\n  - Parses a single TFRecord example containing an image and its label.\n  - Calls decode_image to process the image data.\n  - Returns the image tensor and its corresponding integer label.\n\nread_unlabeled_tfrecord(example)\n  - Parses a single TFRecord example containing an image and its ID (for test data).\n  - Calls decode_image to process the image data.\n  - Returns the image tensor and its string ID.\n\n- load_dataset(filenames, labeled, ordered)\n  - Creates a TensorFlow TFRecordDataset from a list of filenames.\n  - Maps the appropriate parsing function (read_labeled_tfrecord or read_unlabeled_tfrecord) to each record.\n  - Optionally disables data order (ordered=False) for faster reading.\n  - Returns the configured dataset.\n\nrotation_layer_single\n  - (This is a Keras layer, not a function) Defines a layer for applying random image rotation during data augmentation.\n\ndata_augment(image, label)\n  - Applies various random transformations to an image (flipping, saturation, brightness) for data augmentation.\n  - Returns the augmented image and the original label.\n\nget_training_dataset()\n  - Loads the training data using load_dataset.\n  - Applies the data_augment function.\n  - Repeats, shuffles, and batches the dataset.\n  - Prefetches data for optimal training speed.\n\nget_validation_dataset()\n  - Loads the validation data using load_dataset.\n  - Batches the dataset.\n  - Caches the dataset in memory for faster access during evaluation.\n\nget_test_dataset(ordered)\n  - Loads the test data using load_dataset.\n  - Batches the dataset.\n  - Optionally keeps the data in the original order (ordered=True), which is often necessary for generating submission files.\n\ncheck_for_nan_and_inf(images, labels)\n  - Checks if any NaN (Not a Number) or Inf (Infinity) values are present in the image or label tensors.\n  - Prints a warning message if such values are found.\n  - Returns the tensors unchanged.","metadata":{}},{"cell_type":"code","source":"def decode_image(image_data):\n    image = tf.image.decode_jpeg(image_data, channels=3)\n    image = tf.cast(image, tf.float32) / 255.0  # convert image to floats in [0, 1] range\n    image = tf.reshape(image, [*IMAGE_SIZE, 3]) # explicit size needed for TPU\n    return image\n\ndef read_labeled_tfrecord(example):\n    LABELED_TFREC_FORMAT = {\n        \"image\": tf.io.FixedLenFeature([], tf.string), # tf.string means bytestring\n        \"class\": tf.io.FixedLenFeature([], tf.int64),  # shape [] means single element\n    }\n    example = tf.io.parse_single_example(example, LABELED_TFREC_FORMAT)\n    image = decode_image(example['image'])\n    label = tf.cast(example['class'], tf.int32)\n    return image, label \n\ndef read_unlabeled_tfrecord(example):\n    UNLABELED_TFREC_FORMAT = {\n        \"image\": tf.io.FixedLenFeature([], tf.string), # tf.string means bytestring\n        \"id\": tf.io.FixedLenFeature([], tf.string),  # shape [] means single element\n    }\n    example = tf.io.parse_single_example(example, UNLABELED_TFREC_FORMAT)\n    image = decode_image(example['image'])\n    idnum = example['id']\n    return image, idnum # returns a dataset of image(s)\n\ndef load_dataset(filenames, labeled=True, ordered=False):\n    ignore_order = tf.data.Options()\n    if not ordered:\n        ignore_order.experimental_deterministic = False # disable order, increase speed\n\n    dataset = tf.data.TFRecordDataset(filenames) # automatically interleaves reads from multiple files\n    dataset = dataset.with_options(ignore_order) # uses data as soon as it streams in, rather than in its original order\n    dataset = dataset.map(read_labeled_tfrecord if labeled else read_unlabeled_tfrecord)\n    return dataset\n\nrotation_layer_single = tf.keras.layers.RandomRotation(\n    factor=0.05,\n    fill_mode='nearest' # Or 'nearest', 'constant', etc.\n)\n\ndef data_augment(image, label):\n    image = tf.image.random_flip_left_right(image)\n    image = tf.image.random_saturation(image, 0, 2)\n    image = tf.image.random_brightness(image, 0.2)\n    # image = rotation_layer_single(tf.expand_dims(image, 0), training=True)\n    # image = tf.squeeze(image, 0)\n    return image, label \n\ndef get_training_dataset():\n    dataset = load_dataset(tf.io.gfile.glob(GCS_DS_PATH + '/tfrecords-jpeg-512x512/train/*.tfrec'), labeled=True)\n    dataset = dataset.map(data_augment, num_parallel_calls=tf.data.AUTOTUNE)\n    dataset = dataset.repeat() # Repeat the augmented dataset\n    dataset = dataset.shuffle(2048) # Shuffle the combined dataset (original + augmented)\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.prefetch(tf.data.AUTOTUNE)\n\n    return dataset\n\ndef get_validation_dataset():\n    dataset = load_dataset(tf.io.gfile.glob(GCS_DS_PATH + '/tfrecords-jpeg-512x512/val/*.tfrec'), labeled=True, ordered=False)\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.cache()\n    return dataset\n\ndef get_test_dataset(ordered=False):\n    dataset = load_dataset(tf.io.gfile.glob(GCS_DS_PATH + '/tfrecords-jpeg-512x512/test/*.tfrec'), labeled=False, ordered=ordered)\n    dataset = dataset.batch(BATCH_SIZE)\n    return dataset\n\ndef check_for_nan_and_inf(images, labels):\n    if tf.reduce_any(tf.math.is_nan(images)):\n        tf.print(\"NaN values found in images!\")\n    if tf.reduce_any(tf.math.is_inf(images)):\n        tf.print(\"Infinite values found in images!\")\n    if labels is not None and tf.reduce_any(tf.math.is_nan(labels)):\n         tf.print(\"NaN values found in labels!\")\n    if labels is not None and tf.reduce_any(tf.math.is_inf(labels)):\n         tf.print(\"Infinite values found in labels!\")\n    return images, labels # Return the data unchanged\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:44:53.955247Z","iopub.execute_input":"2025-05-06T09:44:53.955458Z","iopub.status.idle":"2025-05-06T09:44:53.998986Z","shell.execute_reply.started":"2025-05-06T09:44:53.955438Z","shell.execute_reply":"2025-05-06T09:44:53.992770Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Learning Rate Scheduler:\n\nThis defines a learning rate schedule that gradually changes the learning rate over epochs.\nA Keras callback is set up to apply this schedule automatically during training.","metadata":{}},{"cell_type":"code","source":"ds_train = get_training_dataset()\nds_valid = get_validation_dataset()\nds_test = get_test_dataset()\n\ndef exponential_lr(epoch,\n                   start_lr = 0.00001, min_lr = 0.00001 , max_lr = 0.00005 * strategy.num_replicas_in_sync,\n                   rampup_epochs = 5, sustain_epochs = 0,\n                   exp_decay = 0.8):\n\n    def lr(epoch, start_lr, min_lr, max_lr, rampup_epochs, sustain_epochs, exp_decay):\n        # linear increase from start to rampup_epochs\n        if epoch < rampup_epochs:\n            lr = ((max_lr - start_lr) /\n                  rampup_epochs * epoch + start_lr)\n        # constant max_lr during sustain_epochs\n        elif epoch < rampup_epochs + sustain_epochs:\n            lr = max_lr\n        # exponential decay towards min_lr\n        else:\n            lr = ((max_lr - min_lr) *\n                  exp_decay**(epoch - rampup_epochs - sustain_epochs) +\n                  min_lr)\n        return lr\n    return lr(epoch,\n              start_lr,\n              min_lr,\n              max_lr,\n              rampup_epochs,\n              sustain_epochs,\n              exp_decay)\n\nlr_callback = tf.keras.callbacks.LearningRateScheduler(exponential_lr, verbose=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:44:54.000664Z","iopub.execute_input":"2025-05-06T09:44:54.000938Z","iopub.status.idle":"2025-05-06T09:44:54.428254Z","shell.execute_reply.started":"2025-05-06T09:44:54.000913Z","shell.execute_reply":"2025-05-06T09:44:54.423672Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Build the model!\n\nThis code section sets up training parameters and defines a deep learning model using transfer learning. It loads a pre-trained DenseNet201 model (excluding its top layer) and adds a new classification head, freezing the pre-trained weights initially. Configured within the distribution strategy scope for efficient parallel processing, the model is compiled with an Adam optimizer and sparse categorical crossentropy loss, and then trained on the prepared datasets for a fixed number of epochs using the defined learning rate schedule.","metadata":{}},{"cell_type":"code","source":"EPOCHS = 20\nSTEPS_PER_EPOCH = NUM_TRAINING_IMAGES // BATCH_SIZE\n\nwith strategy.scope():\n    #pretrained_model = tf.keras.applications.VGG16(weights='imagenet',include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    pretrained_model = tf.keras.applications.DenseNet201(weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    #pretrained_model = efficientnet.EfficientNetB7(weights = 'noisy-student', include_top = False, input_shape = [*IMAGE_SIZE, 3])\n    #pretrained_model = tf.keras.applications.EfficientNetB0(include_top=False, weights='imagenet',input_shape=(*IMAGE_SIZE, 3))\n    #pretrained_model = tf.keras.applications.Xception(weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    #pretrained_model = tf.keras.applications.inception_v3.InceptionV3(weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    #pretrained_model = tf.keras.applications.VGG16(weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    #pretrained_model = tf.keras.applications.ResNet50(weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    #pretrained_model = tf.keras.applications.mobilenet.MobileNet(weights='imagenet', include_top=False ,input_shape=[*IMAGE_SIZE, 3])\n    pretrained_model.trainable = False\n    \n    model = tf.keras.Sequential([\n        pretrained_model,\n        tf.keras.layers.GlobalAveragePooling2D(),\n        tf.keras.layers.Dense(104, activation = 'softmax')\n    ])\n\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n        loss = 'sparse_categorical_crossentropy',\n        metrics=['sparse_categorical_accuracy'],\n    )\n\nmodel.summary()\n\n\nhistory = model.fit(\n    ds_train,\n    validation_data=ds_valid,\n    epochs=EPOCHS,\n    steps_per_epoch=STEPS_PER_EPOCH,\n    callbacks=[lr_callback],\n    #verbose=0 \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:44:54.430656Z","iopub.execute_input":"2025-05-06T09:44:54.430928Z","iopub.status.idle":"2025-05-06T10:09:42.227896Z","shell.execute_reply.started":"2025-05-06T09:44:54.430903Z","shell.execute_reply":"2025-05-06T10:09:42.220904Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Test predictions","metadata":{}},{"cell_type":"code","source":"test_ds = get_test_dataset(ordered=True)\ntest_images_ds = test_ds.map(lambda image, idnum: image)\nprobabilities = model.predict(test_images_ds)\npredictions = np.argmax(probabilities, axis=-1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T10:09:42.230188Z","iopub.execute_input":"2025-05-06T10:09:42.230493Z","iopub.status.idle":"2025-05-06T10:12:09.068439Z","shell.execute_reply.started":"2025-05-06T10:09:42.230464Z","shell.execute_reply":"2025-05-06T10:12:09.062682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from matplotlib import pyplot as plt\n\ndef batch_to_numpy_images_and_labels(data):\n    images, labels = data\n    numpy_images = images.numpy()\n    numpy_labels = labels.numpy()\n    if numpy_labels.dtype == object: # binary string in this case,\n                                     # these are image ID strings\n        numpy_labels = [None for _ in enumerate(numpy_images)]\n    # If no labels, only image IDs, return None for labels (this is\n    # the case for test data)\n    return numpy_images, numpy_labels\n\ndef title_from_label_and_target(label, correct_label):\n    if correct_label is None:\n        return CLASSES[label], True\n    correct = (label == correct_label)\n    return \"{} [{}{}{}]\".format(CLASSES[label], 'OK' if correct else 'NO', u\"\\u2192\" if not correct else '',\n                                CLASSES[correct_label] if not correct else ''), correct\n\ndef display_one_flower(image, title, subplot, red=False, titlesize=16):\n    plt.subplot(*subplot)\n    plt.axis('off')\n    plt.imshow(image)\n    if len(title) > 0:\n        plt.title(title, fontsize=int(titlesize) if not red else int(titlesize/1.2), color='red' if red else 'black', fontdict={'verticalalignment':'center'}, pad=int(titlesize/1.5))\n    return (subplot[0], subplot[1], subplot[2]+1)\n    \ndef display_batch_of_images(databatch, predictions=None):\n    images, labels = batch_to_numpy_images_and_labels(databatch)\n    if labels is None:\n        labels = [None for _ in enumerate(images)]\n        \n    rows = int(math.sqrt(len(images)))\n    cols = len(images)//rows\n        \n    # size and spacing\n    FIGSIZE = 13.0\n    SPACING = 0.1\n    subplot=(rows,cols,1)\n    if rows < cols:\n        plt.figure(figsize=(FIGSIZE,FIGSIZE/cols*rows))\n    else:\n        plt.figure(figsize=(FIGSIZE/rows*cols,FIGSIZE))\n    \n    # display\n    for i, (image, label) in enumerate(zip(images[:rows*cols], labels[:rows*cols])):\n        title = '' if label is None else CLASSES[label]\n        correct = True\n        if predictions is not None:\n            title, correct = title_from_label_and_target(predictions[i], label)\n        dynamic_titlesize = FIGSIZE*SPACING/max(rows,cols)*40+3 # magic formula tested to work from 1x1 to 10x10 images\n        subplot = display_one_flower(image, title, subplot, not correct, titlesize=dynamic_titlesize)\n    \n    #layout\n    plt.tight_layout()\n    if label is None and predictions is None:\n        plt.subplots_adjust(wspace=0, hspace=0)\n    else:\n        plt.subplots_adjust(wspace=SPACING, hspace=SPACING)\n    plt.show()\n\n\ndef display_training_curves(training, validation, title, subplot):\n    if subplot%10==1: # set up the subplots on the first call\n        plt.subplots(figsize=(10,10), facecolor='#F0F0F0')\n        plt.tight_layout()\n    ax = plt.subplot(subplot)\n    ax.set_facecolor('#F8F8F8')\n    ax.plot(training)\n    ax.plot(validation)\n    ax.set_title('model '+ title)\n    ax.set_ylabel(title)\n    #ax.set_ylim(0.28,1.05)\n    ax.set_xlabel('epoch')\n    ax.legend(['train', 'valid.'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T10:12:09.070058Z","iopub.execute_input":"2025-05-06T10:12:09.070312Z","iopub.status.idle":"2025-05-06T10:12:09.092614Z","shell.execute_reply.started":"2025-05-06T10:12:09.070287Z","shell.execute_reply":"2025-05-06T10:12:09.086018Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = get_validation_dataset()\ndataset = dataset.unbatch().batch(20)\nbatch = iter(dataset)\nimages, labels = next(batch)\nprobabilities = model.predict(images)\npredictions = np.argmax(probabilities, axis=-1)\ndisplay_batch_of_images((images, labels), predictions)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T10:12:09.094235Z","iopub.execute_input":"2025-05-06T10:12:09.094488Z","iopub.status.idle":"2025-05-06T10:12:34.053216Z","shell.execute_reply.started":"2025-05-06T10:12:09.094466Z","shell.execute_reply":"2025-05-06T10:12:34.046663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Create submition","metadata":{}},{"cell_type":"code","source":"print('Creating submission.csv...')\ntest_images_ds = test_ds.map(lambda image, idnum: image)\nprobabilities = model.predict(test_images_ds)\npredictions = np.argmax(probabilities, axis=-1)\n\nprint('Generando submission.csv..')\ntest_ids_ds = test_ds.map(lambda image, idnum: idnum).unbatch()\nprint(f\"NUM_TEST_IMAGES: {NUM_TEST_IMAGES}\")\ntest_ids = next(iter(test_ids_ds.batch(NUM_TEST_IMAGES))).numpy().astype('U')\n\nnp.savetxt('submission.csv', np.rec.fromarrays([test_ids, predictions]), fmt=['%s', '%d'], delimiter=',', header='id,label', comments='')\n!head submission.csv\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T10:12:34.055508Z","iopub.execute_input":"2025-05-06T10:12:34.055799Z","iopub.status.idle":"2025-05-06T10:14:10.630743Z","shell.execute_reply.started":"2025-05-06T10:12:34.055772Z","shell.execute_reply":"2025-05-06T10:14:10.625140Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null}]}