{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpuV5e8","dataSources":[{"sourceType":"competition","sourceId":21755,"databundleVersionId":1475600,"isSourceIdPinned":false}],"dockerImageVersionId":31331,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🎨 I'm Something of a Painter Myself — Monet CycleGAN\nThis notebook trains a **CycleGAN** to transform photos into Monet-style paintings and generates 7,000+ images for submission.","metadata":{}},{"cell_type":"markdown","source":"## 1. Install & Import Dependencies","metadata":{}},{"cell_type":"code","source":"import os, re, zipfile, time\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\n\nprint('TF version:', tf.__version__)\nprint('GPUs:', tf.config.list_physical_devices('GPU'))\nprint('TPUs:', tf.config.list_physical_devices('TPU'))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. TPU / GPU Strategy Setup","metadata":{}},{"cell_type":"code","source":"try:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()\n    tf.config.experimental_connect_to_cluster(tpu)\n    tf.tpu.experimental.initialize_tpu_system(tpu)\n    strategy = tf.distribute.TPUStrategy(tpu)\n    print('Running on TPU:', tpu.master())\nexcept Exception:\n    strategy = tf.distribute.get_strategy()\n    print('Running on GPU/CPU — replicas:', strategy.num_replicas_in_sync)\n\nAUTOTUNE      = tf.data.AUTOTUNE\nIMAGE_SIZE    = [256, 256]\nBATCH_SIZE    = 1            # CycleGAN standard\nEPOCHS        = 25           # Increase for better quality (try 50–100)\nLAMBDA_CYCLE  = 10.0\nLAMBDA_ID     = 0.5 * LAMBDA_CYCLE\n\n# Paths\nMONET_TFREC_PATH = '/kaggle/input/competitions/gan-getting-started/monet_tfrec' \nPHOTO_TFREC_PATH = '/kaggle/input/competitions/gan-getting-started/photo_tfrec'\nMONET_JPG_PATH   = '/kaggle/input/competitions/gan-getting-started/monet_jpg'\nPHOTO_JPG_PATH   = '/kaggle/input/competitions/gan-getting-started/photo_jpg'\nOUTPUT_DIR       = '/kaggle/working/generated'\nZIP_PATH         = '/kaggle/working/images.zip'\n\nos.makedirs(OUTPUT_DIR, exist_ok=True)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Data Loading from TFRecords","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) / 127.5 - 1.0  # [-1, 1]\n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    return image\n\ndef read_tfrecord(example):\n    tfrecord_format = {\n        'image_name': tf.io.FixedLenFeature([], tf.string),\n        'image':      tf.io.FixedLenFeature([], tf.string),\n        'target':     tf.io.FixedLenFeature([], tf.string),\n    }\n    example = tf.io.parse_single_example(example, tfrecord_format)\n    image   = decode_image(example['image'])\n    return image\n\ndef load_dataset(filenames, augment=False):\n    dataset = tf.data.TFRecordDataset(filenames)\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=AUTOTUNE)\n    if augment:\n        dataset = dataset.map(augment_image, num_parallel_calls=AUTOTUNE)\n    dataset = dataset.shuffle(1000).repeat().batch(BATCH_SIZE)\n    dataset = dataset.prefetch(AUTOTUNE)\n    return dataset\n\ndef augment_image(image):\n    # Random flip\n    image = tf.image.random_flip_left_right(image)\n    # Random crop: resize to 286 then crop back to 256\n    image = tf.image.resize(image, [286, 286])\n    image = tf.image.random_crop(image, size=[*IMAGE_SIZE, 3])\n    return image\n\nmonet_files = tf.io.gfile.glob(MONET_TFREC_PATH + '/*.tfrec')\nphoto_files  = tf.io.gfile.glob(PHOTO_TFREC_PATH + '/*.tfrec')\n\nprint(f'Monet TFRec files: {len(monet_files)}')\nprint(f'Photo TFRec files: {len(photo_files)}')\n\nmonet_ds = load_dataset(monet_files, augment=True)\nphoto_ds  = load_dataset(photo_files, augment=True)\n\n# Count images\nn_monet_images = sum(1 for _ in tf.data.TFRecordDataset(monet_files))\nn_photo_images = sum(1 for _ in tf.data.TFRecordDataset(photo_files))\nprint(f'Monet images: {n_monet_images}, Photo images: {n_photo_images}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Preview Sample Images","metadata":{}},{"cell_type":"code","source":"def show_images(ds, title, n=4):\n    fig, axes = plt.subplots(1, n, figsize=(4*n, 4))\n    fig.suptitle(title, fontsize=14)\n    for ax, img in zip(axes, ds.unbatch().take(n)):\n        ax.imshow((img.numpy() + 1) / 2)  # back to [0,1]\n        ax.axis('off')\n    plt.tight_layout()\n    plt.show()\n\nshow_images(monet_ds, 'Monet Paintings')\nshow_images(photo_ds,  'Photos')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. CycleGAN Architecture\n\n### 5.1 Building Blocks","metadata":{}},{"cell_type":"code","source":"# ─── Instance Normalization ──────────────────────────────────────────────────\nclass InstanceNormalization(layers.Layer):\n    def __init__(self, epsilon=1e-5, **kwargs):\n        super().__init__(**kwargs)\n        self.epsilon = epsilon\n\n    def build(self, input_shape):\n        self.gamma = self.add_weight(shape=(input_shape[-1],), initializer='ones',  trainable=True)\n        self.beta  = self.add_weight(shape=(input_shape[-1],), initializer='zeros', trainable=True)\n\n    def call(self, x):\n        mean, var = tf.nn.moments(x, axes=[1, 2], keepdims=True)\n        return self.gamma * (x - mean) / tf.sqrt(var + self.epsilon) + self.beta\n\n\n# ─── Residual Block ───────────────────────────────────────────────────────────\ndef residual_block(x, filters=256):\n    init = tf.random_normal_initializer(0., 0.02)\n    res = x\n    x = layers.Conv2D(filters, 3, padding='same', kernel_initializer=init, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    x = layers.ReLU()(x)\n    x = layers.Conv2D(filters, 3, padding='same', kernel_initializer=init, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    return layers.Add()([res, x])\n\n\n# ─── Downsampling Block ───────────────────────────────────────────────────────\ndef downsample(x, filters, size=3, apply_norm=True):\n    init = tf.random_normal_initializer(0., 0.02)\n    x = layers.Conv2D(filters, size, strides=2, padding='same',\n                      kernel_initializer=init, use_bias=False)(x)\n    if apply_norm:\n        x = InstanceNormalization()(x)\n    x = layers.ReLU()(x)\n    return x\n\n\n# ─── Upsampling Block ─────────────────────────────────────────────────────────\ndef upsample(x, filters, size=3, apply_dropout=False):\n    init = tf.random_normal_initializer(0., 0.02)\n    x = layers.Conv2DTranspose(filters, size, strides=2, padding='same',\n                               kernel_initializer=init, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    if apply_dropout:\n        x = layers.Dropout(0.5)(x)\n    x = layers.ReLU()(x)\n    return x\n\n\nprint('Building blocks defined ✓')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.2 Generator (ResNet-based)","metadata":{}},{"cell_type":"code","source":"def build_generator(n_resblocks=9, name='generator'):\n    \"\"\"\n    ResNet-based generator:\n    c7s1-64 → d128 → d256 → R×9 → u128 → u64 → c7s1-3\n    \"\"\"\n    init = tf.random_normal_initializer(0., 0.02)\n    inp = layers.Input(shape=[*IMAGE_SIZE, 3], name='input_image')\n\n    # Initial conv\n    x = layers.Conv2D(64, 7, padding='same', kernel_initializer=init, use_bias=False)(inp)\n    x = InstanceNormalization()(x)\n    x = layers.ReLU()(x)\n\n    # Downsampling\n    x = downsample(x, 128, size=3)\n    x = downsample(x, 256, size=3)\n\n    # Residual blocks\n    for _ in range(n_resblocks):\n        x = residual_block(x, 256)\n\n    # Upsampling\n    x = upsample(x, 128, size=3)\n    x = upsample(x,  64, size=3)\n\n    # Output conv\n    x = layers.Conv2D(3, 7, padding='same', kernel_initializer=init,\n                      activation='tanh')(x)\n\n    return keras.Model(inp, x, name=name)\n\n\ngen_G = build_generator(name='generator_G')   # Photo → Monet\ngen_F = build_generator(name='generator_F')   # Monet → Photo\nprint(f'Generator G params: {gen_G.count_params():,}')\nprint(f'Generator F params: {gen_F.count_params():,}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.3 Discriminator (PatchGAN)","metadata":{}},{"cell_type":"code","source":"def build_discriminator(name='discriminator'):\n    \"\"\"\n    70×70 PatchGAN discriminator\n    \"\"\"\n    init = tf.random_normal_initializer(0., 0.02)\n    inp  = layers.Input(shape=[*IMAGE_SIZE, 3], name='input_image')\n\n    x = layers.Conv2D(64, 4, strides=2, padding='same',\n                      kernel_initializer=init)(inp)         # no norm on first\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(128, 4, strides=2, padding='same',\n                      kernel_initializer=init, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(256, 4, strides=2, padding='same',\n                      kernel_initializer=init, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.ZeroPadding2D()(x)\n    x = layers.Conv2D(512, 4, strides=1, padding='valid',\n                      kernel_initializer=init, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.ZeroPadding2D()(x)\n    x = layers.Conv2D(1, 4, strides=1, padding='valid',\n                      kernel_initializer=init)(x)           # patch output\n\n    return keras.Model(inp, x, name=name)\n\n\ndisc_X = build_discriminator(name='discriminator_X')   # Monet discriminator\ndisc_Y = build_discriminator(name='discriminator_Y')   # Photo discriminator\nprint(f'Discriminator X params: {disc_X.count_params():,}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. CycleGAN Model Class","metadata":{}},{"cell_type":"code","source":"class CycleGAN(keras.Model):\n    def __init__(self, gen_G, gen_F, disc_X, disc_Y,\n                 lambda_cycle=10.0, lambda_id=5.0):\n        super().__init__()\n        self.gen_G       = gen_G\n        self.gen_F       = gen_F\n        self.disc_X      = disc_X\n        self.disc_Y      = disc_Y\n        self.lambda_cycle = lambda_cycle\n        self.lambda_id    = lambda_id\n\n    def compile(self, gen_G_opt, gen_F_opt, disc_X_opt, disc_Y_opt):\n        super().compile()\n        self.gen_G_opt  = gen_G_opt\n        self.gen_F_opt  = gen_F_opt\n        self.disc_X_opt = disc_X_opt\n        self.disc_Y_opt = disc_Y_opt\n\n        self.gen_loss_fn   = self._adversarial_loss\n        self.disc_loss_fn  = self._discriminator_loss\n        self.cycle_loss_fn = keras.losses.MeanAbsoluteError()\n        self.id_loss_fn    = keras.losses.MeanAbsoluteError()\n\n    @staticmethod\n    def _adversarial_loss(disc_real, disc_fake):\n        \"\"\"LSGAN loss for generator.\"\"\"\n        return tf.reduce_mean(tf.square(disc_fake - 1.0))\n\n    @staticmethod\n    def _discriminator_loss(disc_real, disc_fake):\n        \"\"\"LSGAN loss for discriminator.\"\"\"\n        real_loss = tf.reduce_mean(tf.square(disc_real - 1.0))\n        fake_loss = tf.reduce_mean(tf.square(disc_fake))\n        return (real_loss + fake_loss) * 0.5\n\n    def train_step(self, batch_data):\n        real_x, real_y = batch_data   # real_x = photo, real_y = monet\n\n        with tf.GradientTape(persistent=True) as tape:\n            # Generators\n            fake_y  = self.gen_G(real_x, training=True)   # photo → monet\n            fake_x  = self.gen_F(real_y, training=True)   # monet → photo\n            cycle_x = self.gen_F(fake_y, training=True)   # monet → photo (cycle)\n            cycle_y = self.gen_G(fake_x, training=True)   # photo → monet (cycle)\n            same_x  = self.gen_F(real_x, training=True)   # identity\n            same_y  = self.gen_G(real_y, training=True)   # identity\n\n            # Discriminators\n            disc_real_x = self.disc_X(real_x, training=True)\n            disc_real_y = self.disc_Y(real_y, training=True)\n            disc_fake_x = self.disc_X(fake_x, training=True)\n            disc_fake_y = self.disc_Y(fake_y, training=True)\n\n            # Generator losses\n            gen_G_loss = self.gen_loss_fn(disc_real_y, disc_fake_y)\n            gen_F_loss = self.gen_loss_fn(disc_real_x, disc_fake_x)\n\n            # Cycle-consistency losses\n            cyc_x_loss = self.cycle_loss_fn(real_x, cycle_x) * self.lambda_cycle\n            cyc_y_loss = self.cycle_loss_fn(real_y, cycle_y) * self.lambda_cycle\n\n            # Identity losses\n            id_x_loss = self.id_loss_fn(real_x, same_x) * self.lambda_id\n            id_y_loss = self.id_loss_fn(real_y, same_y) * self.lambda_id\n\n            total_gen_G = gen_G_loss + cyc_x_loss + cyc_y_loss + id_y_loss\n            total_gen_F = gen_F_loss + cyc_x_loss + cyc_y_loss + id_x_loss\n\n            # Discriminator losses\n            disc_X_loss = self.disc_loss_fn(disc_real_x, disc_fake_x)\n            disc_Y_loss = self.disc_loss_fn(disc_real_y, disc_fake_y)\n\n        # Apply gradients\n        self.gen_G_opt.apply_gradients(\n            zip(tape.gradient(total_gen_G, self.gen_G.trainable_variables), self.gen_G.trainable_variables))\n        self.gen_F_opt.apply_gradients(\n            zip(tape.gradient(total_gen_F, self.gen_F.trainable_variables), self.gen_F.trainable_variables))\n        self.disc_X_opt.apply_gradients(\n            zip(tape.gradient(disc_X_loss, self.disc_X.trainable_variables), self.disc_X.trainable_variables))\n        self.disc_Y_opt.apply_gradients(\n            zip(tape.gradient(disc_Y_loss, self.disc_Y.trainable_variables), self.disc_Y.trainable_variables))\n\n        del tape\n        return {\n            'gen_G_loss':  total_gen_G,\n            'gen_F_loss':  total_gen_F,\n            'disc_X_loss': disc_X_loss,\n            'disc_Y_loss': disc_Y_loss,\n        }\n\nprint('CycleGAN class defined ✓')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Callbacks & Visualization","metadata":{}},{"cell_type":"code","source":"class GANMonitor(keras.callbacks.Callback):\n    \"\"\"Visualizes generated Monet-style images every N epochs.\"\"\"\n    def __init__(self, sample_photos, num_img=4, save_every=5):\n        self.sample_photos = sample_photos\n        self.num_img       = num_img\n        self.save_every    = save_every\n\n    def on_epoch_end(self, epoch, logs=None):\n        if (epoch + 1) % self.save_every != 0:\n            return\n        fig, axes = plt.subplots(2, self.num_img, figsize=(4*self.num_img, 8))\n        for i, img in enumerate(self.sample_photos.take(self.num_img)):\n            gen_img = self.model.gen_G(img, training=False)[0].numpy()\n            axes[0, i].imshow((img[0].numpy() + 1) / 2)\n            axes[0, i].set_title('Photo')\n            axes[0, i].axis('off')\n            axes[1, i].imshow((gen_img + 1) / 2)\n            axes[1, i].set_title(f'Monet (epoch {epoch+1})')\n            axes[1, i].axis('off')\n        plt.tight_layout()\n        plt.show()\n        print(f'Epoch {epoch+1} | gen_G={logs[\"gen_G_loss\"]:.4f} '\n              f'disc_Y={logs[\"disc_Y_loss\"]:.4f}')\n\n\nsample_photos = photo_ds.unbatch().batch(1)\nmonitor_cb    = GANMonitor(sample_photos, num_img=4, save_every=5)\nprint('Callbacks ready ✓')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Compile & Train","metadata":{}},{"cell_type":"code","source":"def lr_schedule(epoch):\n    \"\"\"Linear decay after epoch 10.\"\"\"\n    if epoch < 10:\n        return 2e-4\n    return 2e-4 * (EPOCHS - epoch) / (EPOCHS - 10 + 1e-8)\n\nwith strategy.scope():\n    cycle_gan = CycleGAN(\n        gen_G   = build_generator(name='gen_G'),\n        gen_F   = build_generator(name='gen_F'),\n        disc_X  = build_discriminator(name='disc_X'),\n        disc_Y  = build_discriminator(name='disc_Y'),\n        lambda_cycle = LAMBDA_CYCLE,\n        lambda_id    = LAMBDA_ID,\n    )\n    cycle_gan.compile(\n        gen_G_opt  = keras.optimizers.Adam(2e-4, beta_1=0.5),\n        gen_F_opt  = keras.optimizers.Adam(2e-4, beta_1=0.5),\n        disc_X_opt = keras.optimizers.Adam(2e-4, beta_1=0.5),\n        disc_Y_opt = keras.optimizers.Adam(2e-4, beta_1=0.5),\n    )\n\nlr_cb = keras.callbacks.LearningRateScheduler(lr_schedule, verbose=0)\n\nsteps_per_epoch = max(n_monet_images, n_photo_images) // BATCH_SIZE\nprint(f'Steps per epoch: {steps_per_epoch}')\n\nhistory = cycle_gan.fit(\n    tf.data.Dataset.zip((photo_ds, monet_ds)),\n    epochs           = EPOCHS,\n    steps_per_epoch  = steps_per_epoch,\n    callbacks        = [monitor_cb, lr_cb],\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Plot Training Curves","metadata":{}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\naxes[0].plot(history.history['gen_G_loss'], label='gen_G')\naxes[0].plot(history.history['gen_F_loss'], label='gen_F')\naxes[0].set_title('Generator Losses')\naxes[0].set_xlabel('Epoch')\naxes[0].legend()\n\naxes[1].plot(history.history['disc_X_loss'], label='disc_X')\naxes[1].plot(history.history['disc_Y_loss'], label='disc_Y')\naxes[1].set_title('Discriminator Losses')\naxes[1].set_xlabel('Epoch')\naxes[1].legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Generate 7,000+ Monet-Style Images\n\nWe'll generate images from all 7,028 photos, then repeat until we have ≥7,000 images.","metadata":{}},{"cell_type":"code","source":"TARGET_IMAGES = 7000\ngenerated     = 0\n\n# Load all photos (no shuffle, no repeat)\ndef load_photo_ds_for_inference():\n    ds = tf.data.TFRecordDataset(photo_files)\n    ds = ds.map(read_tfrecord, num_parallel_calls=AUTOTUNE)\n    ds = ds.batch(BATCH_SIZE)\n    ds = ds.prefetch(AUTOTUNE)\n    return ds\n\nphoto_ds_inf = load_photo_ds_for_inference()\n\nprint('Generating Monet-style images...')\nt0 = time.time()\n\nwhile generated < TARGET_IMAGES:\n    for batch in photo_ds_inf:\n        if generated >= TARGET_IMAGES:\n            break\n        fake_monet = cycle_gan.gen_G(batch, training=False).numpy()\n        for img in fake_monet:\n            if generated >= TARGET_IMAGES:\n                break\n            # Convert from [-1,1] to [0,255]\n            img_uint8 = ((img + 1) * 127.5).astype(np.uint8)\n            pil_img   = Image.fromarray(img_uint8)\n            pil_img.save(os.path.join(OUTPUT_DIR, f'monet_{generated:05d}.jpg'), quality=95)\n            generated += 1\n\nelapsed = time.time() - t0\nprint(f'✓ Generated {generated} images in {elapsed:.1f}s')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Preview Generated Images","metadata":{}},{"cell_type":"code","source":"gen_files = sorted(os.listdir(OUTPUT_DIR))[:8]\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\nfig.suptitle('Generated Monet-Style Images', fontsize=14)\nfor ax, fname in zip(axes.flat, gen_files):\n    img = Image.open(os.path.join(OUTPUT_DIR, fname))\n    ax.imshow(np.array(img))\n    ax.axis('off')\nplt.tight_layout()\nplt.show()\nprint(f'Total images in output dir: {len(os.listdir(OUTPUT_DIR))}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. Create Submission ZIP","metadata":{}},{"cell_type":"code","source":"print(f'Zipping {len(os.listdir(OUTPUT_DIR))} images → {ZIP_PATH}')\nt0 = time.time()\n\nwith zipfile.ZipFile(ZIP_PATH, 'w', zipfile.ZIP_DEFLATED) as zf:\n    for fname in sorted(os.listdir(OUTPUT_DIR)):\n        if fname.lower().endswith('.jpg'):\n            zf.write(os.path.join(OUTPUT_DIR, fname), arcname=fname)\n\nzip_size_mb = os.path.getsize(ZIP_PATH) / 1e6\nelapsed     = time.time() - t0\nprint(f'✓ images.zip created: {zip_size_mb:.1f} MB  ({elapsed:.1f}s)')\nprint(f'  File count inside zip: {len(zipfile.ZipFile(ZIP_PATH).namelist())}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. Validate Submission","metadata":{}},{"cell_type":"code","source":"with zipfile.ZipFile(ZIP_PATH, 'r') as zf:\n    names  = zf.namelist()\n    n_imgs = len(names)\n    print(f'Images in zip: {n_imgs}')\n    assert 7000 <= n_imgs <= 10000, f'Expected 7000–10000 images, got {n_imgs}'\n\n    # Check a sample of images\n    errors = []\n    for fname in names[:20]:\n        with zf.open(fname) as f:\n            img = Image.open(f)\n            img.load()\n        if img.size != (256, 256):\n            errors.append(f'{fname}: {img.size}')\n        if img.mode != 'RGB':\n            errors.append(f'{fname}: mode={img.mode}')\n\nif errors:\n    print('⚠ Validation errors:', errors)\nelse:\n    print('✅ Validation passed — 7,000–10,000 images, all 256×256 RGB')\n    print(f'\\nSubmission file: {ZIP_PATH}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ✅ Done!\n\nYour `images.zip` is ready for submission. It contains **7,000–10,000 Monet-style 256×256 JPEG images** generated by the trained CycleGAN.\n\n### Tips to improve MiFID score:\n| Technique | Effect |\n|-----------|--------|\n| More epochs (50–100) | Better style transfer |\n| Increase `n_resblocks` to 9 (already set) | Richer features |\n| Use TPU for faster training | More epochs in budget |\n| Tune `lambda_cycle` (10) & `lambda_id` (5) | Balance fidelity/style |\n| Replay buffer for discriminator | More stable training |","metadata":{}}]}