{"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":"gpu","dataSources":[{"sourceType":"competition","sourceId":21755,"databundleVersionId":1475600,"isSourceIdPinned":false}],"dockerImageVersionId":31401,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"cell_0","cell_type":"code","source":"import numpy as np\nimport os, io, time, zipfile\nfrom pathlib import Path\nfrom glob import glob\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\nimport tensorflow as tf\n\nprint('TF version:', tf.__version__)\nprint('GPU available:', tf.config.list_physical_devices('GPU'))\n\n# List input files to confirm mount path\nfor dirname, _, fnames in os.walk('/kaggle/input'):\n    if fnames:\n        print(f'{dirname}  ({len(fnames)} files, e.g. {fnames[0]})')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:43:46.026823Z","iopub.execute_input":"2026-05-29T10:43:46.027407Z","iopub.status.idle":"2026-05-29T10:44:19.950232Z","shell.execute_reply.started":"2026-05-29T10:43:46.027378Z","shell.execute_reply":"2026-05-29T10:44:19.949469Z"}},"outputs":[],"execution_count":null},{"id":"cell_1","cell_type":"code","source":"# ── FIX 1: Auto-detect mount path (handles both Kaggle mount styles) ──────────\n_PATH_A = '/kaggle/input/gan-getting-started'\n_PATH_B = '/kaggle/input/competitions/gan-getting-started'\nBASE = _PATH_A if Path(_PATH_A).exists() else _PATH_B\nprint(f'Data root: {BASE}')\n\nmonet_paths = sorted(glob(f'{BASE}/monet_jpg/*.jpg'))\nphoto_paths  = sorted(glob(f'{BASE}/photo_jpg/*.jpg'))\n\nprint(f'Monet images : {len(monet_paths)}')\nprint(f'Photo images : {len(photo_paths)}')\n\n# Guard: crash here with clear message rather than IndexError later\nassert len(monet_paths) > 0, 'No Monet images found — verify data source is attached'\nassert len(photo_paths)  > 0, 'No Photo images found — verify data source is attached'\n\n# ── Hyperparameters ───────────────────────────────────────────────────────────\nIMG_SIZE     = 256\nBATCH_SIZE   = 1      # CycleGAN paper uses batch_size=1 with InstanceNorm\nEPOCHS       = 15\nLR           = 2e-4\nLAMBDA_CYC   = 10     # cycle-consistency weight\nLAMBDA_ID    = 5      # identity loss weight (= 0.5 * LAMBDA_CYC)\nN_RES_BLOCKS = 6      # residual blocks in generator\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:19.954319Z","iopub.execute_input":"2026-05-29T10:44:19.954535Z","iopub.status.idle":"2026-05-29T10:44:19.977834Z","shell.execute_reply.started":"2026-05-29T10:44:19.954513Z","shell.execute_reply":"2026-05-29T10:44:19.977199Z"}},"outputs":[],"execution_count":null},{"id":"cell_2","cell_type":"code","source":"img1 = mpimg.imread(monet_paths[0])\nimg2 = mpimg.imread(photo_paths[0])\n\nplt.figure(figsize=(10, 4))\nplt.subplot(1, 2, 1); plt.imshow(img1); plt.title('Monet'); plt.axis('off')\nplt.subplot(1, 2, 2); plt.imshow(img2); plt.title('Photo');  plt.axis('off')\nplt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:19.979207Z","iopub.execute_input":"2026-05-29T10:44:19.979695Z","iopub.status.idle":"2026-05-29T10:44:20.366816Z","shell.execute_reply.started":"2026-05-29T10:44:19.979669Z","shell.execute_reply":"2026-05-29T10:44:20.365912Z"}},"outputs":[],"execution_count":null},{"id":"cell_3","cell_type":"code","source":"def load_image_train(path):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE])\n    img = tf.image.random_flip_left_right(img)  # augmentation\n    img = (tf.cast(img, tf.float32) / 127.5) - 1.0  # normalise to [-1, 1]\n    return img\n\ndef load_image_infer(path):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE])\n    img = (tf.cast(img, tf.float32) / 127.5) - 1.0\n    return img\n\n# ── FIX 2a: monet_ds — cache THEN repeat so 300 images loop to match photo count\n#    Original had no .repeat() → zip stopped at 300 steps/epoch (96% of photos skipped)\nmonet_ds = (\n    tf.data.Dataset.from_tensor_slices(monet_paths)\n    .shuffle(len(monet_paths))\n    .map(load_image_train, num_parallel_calls=tf.data.AUTOTUNE)\n    .cache()           # cache 300 decoded tensors (~230 MB) — well within RAM\n    .repeat()          # cycle through cache; zip() now runs for all photo steps\n    .batch(BATCH_SIZE)\n    .prefetch(tf.data.AUTOTUNE)\n)\n\nphoto_ds = (\n    tf.data.Dataset.from_tensor_slices(photo_paths)\n    .shuffle(len(photo_paths))\n    .map(load_image_train, num_parallel_calls=tf.data.AUTOTUNE)\n    # NOTE: do NOT cache photo_ds — 7038 × 768 KB ≈ 5.4 GB would OOM\n    .batch(BATCH_SIZE)\n    .prefetch(tf.data.AUTOTUNE)\n)\n\n# Separate clean inference dataset — no flip, no cache needed (used once)\nphoto_infer_ds = (\n    tf.data.Dataset.from_tensor_slices(photo_paths)\n    .map(load_image_infer, num_parallel_calls=tf.data.AUTOTUNE)\n    .batch(1)\n    .prefetch(tf.data.AUTOTUNE)\n)\n\nsteps_per_epoch = len(photo_paths) // BATCH_SIZE\nprint(f'Steps per epoch: {steps_per_epoch}  (full photo set used each epoch now)')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:24.802623Z","iopub.execute_input":"2026-05-29T10:44:24.803694Z","iopub.status.idle":"2026-05-29T10:44:25.140465Z","shell.execute_reply.started":"2026-05-29T10:44:24.803661Z","shell.execute_reply":"2026-05-29T10:44:25.139544Z"}},"outputs":[],"execution_count":null},{"id":"cell_4","cell_type":"code","source":"# ── FIX 3: InstanceNormalization — mandatory for style-transfer GANs\n#    Original had NO normalisation at all; BatchNorm in bronze notebook is wrong for this task.\n#    InstanceNorm operates per-sample, per-channel, so style information does not\n#    leak across the batch — critical when batch_size=1.\nclass InstanceNormalization(tf.keras.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        ch = input_shape[-1]\n        # Explicitly use name='gamma' and name='beta'\n        self.gamma = self.add_weight(name='gamma', shape=[ch], initializer='ones')\n        self.beta = self.add_weight(name='beta', shape=[ch], initializer='zeros')\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\ndef residual_block(x):\n    \"\"\"Residual block with InstanceNorm — core of the style transformation.\n    FIX 3a: added InstanceNorm after both Conv2D layers (was missing entirely).\"\"\"\n    ch = x.shape[-1]\n    y = tf.keras.layers.Conv2D(ch, 3, padding='same')(x)\n    y = InstanceNormalization()(y)   # FIX: was missing\n    y = tf.keras.layers.ReLU()(y)\n    y = tf.keras.layers.Conv2D(ch, 3, padding='same')(y)\n    y = InstanceNormalization()(y)   # FIX: was missing\n    return tf.keras.layers.Add()([x, y])\n\n\ndef build_generator(n_res=N_RES_BLOCKS):\n    \"\"\"ResNet generator with InstanceNorm throughout.\n    FIX 3b: added InstanceNorm after every Conv in encoder and decoder.\"\"\"\n    inputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n\n    # Encoder (downsample)\n    x = tf.keras.layers.Conv2D(64, 7, padding='same')(inputs)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.ReLU()(x)\n\n    x = tf.keras.layers.Conv2D(128, 3, strides=2, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.ReLU()(x)\n\n    x = tf.keras.layers.Conv2D(256, 3, strides=2, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.ReLU()(x)\n\n    # Residual blocks (style transformation core)\n    for _ in range(n_res):\n        x = residual_block(x)\n\n    # Decoder (upsample)\n    x = tf.keras.layers.Conv2DTranspose(128, 3, strides=2, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.ReLU()(x)\n\n    x = tf.keras.layers.Conv2DTranspose(64, 3, strides=2, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.ReLU()(x)\n\n    outputs = tf.keras.layers.Conv2D(3, 7, padding='same', activation='tanh')(x)\n    return tf.keras.Model(inputs, outputs)\n\nprint('Generator + InstanceNorm defined.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:25.141931Z","iopub.execute_input":"2026-05-29T10:44:25.142316Z","iopub.status.idle":"2026-05-29T10:44:25.155006Z","shell.execute_reply.started":"2026-05-29T10:44:25.142289Z","shell.execute_reply":"2026-05-29T10:44:25.154235Z"}},"outputs":[],"execution_count":null},{"id":"cell_5","cell_type":"code","source":"def build_discriminator():\n    \"\"\"70x70 PatchGAN discriminator with InstanceNorm.\n    FIX 3c: added InstanceNorm after Conv layers 2-4 (was missing entirely).\n    No norm on the first layer — standard practice per the original paper.\"\"\"\n    inputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n\n    # Layer 1 — no norm (standard)\n    x = tf.keras.layers.Conv2D(64, 4, strides=2, padding='same')(inputs)\n    x = tf.keras.layers.LeakyReLU(0.2)(x)\n\n    x = tf.keras.layers.Conv2D(128, 4, strides=2, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.LeakyReLU(0.2)(x)\n\n    x = tf.keras.layers.Conv2D(256, 4, strides=2, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.LeakyReLU(0.2)(x)\n\n    x = tf.keras.layers.Conv2D(512, 4, strides=1, padding='same')(x)\n    x = InstanceNormalization()(x)           # FIX\n    x = tf.keras.layers.LeakyReLU(0.2)(x)\n\n    # Patch output — each value = real/fake prediction for one 70x70 patch\n    outputs = tf.keras.layers.Conv2D(1, 4, padding='same')(x)\n    return tf.keras.Model(inputs, outputs)\n\nprint('PatchGAN Discriminator defined.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:25.402331Z","iopub.execute_input":"2026-05-29T10:44:25.402616Z","iopub.status.idle":"2026-05-29T10:44:25.410375Z","shell.execute_reply.started":"2026-05-29T10:44:25.402593Z","shell.execute_reply":"2026-05-29T10:44:25.409619Z"}},"outputs":[],"execution_count":null},{"id":"cell_6","cell_type":"code","source":"# ── FIX 4: LSGAN (MSE) losses instead of BinaryCrossentropy ──────────────────\n#    BCE with logits saturates when D is confident → vanishing gradients for G.\n#    LSGAN uses MSE: gradients stay non-zero even for well-classified samples.\n#    This is the standard loss for CycleGAN (used in the original paper).\n\ndef discriminator_loss(real, fake):\n    \"\"\"LSGAN: real patches -> 1, fake patches -> 0.\"\"\"\n    real_loss = tf.reduce_mean(tf.square(real - 1.0))\n    fake_loss = tf.reduce_mean(tf.square(fake))\n    return (real_loss + fake_loss) * 0.5\n\ndef generator_loss(fake):\n    \"\"\"LSGAN: generator wants disc to output 1 for its fakes.\"\"\"\n    return tf.reduce_mean(tf.square(fake - 1.0))\n\ndef cycle_loss(real, cycled):\n    \"\"\"L1 reconstruction loss weighted by LAMBDA_CYC.\"\"\"\n    return LAMBDA_CYC * tf.reduce_mean(tf.abs(real - cycled))\n\ndef identity_loss(real, same):\n    \"\"\"Keeps colours stable when generator receives its own domain image.\"\"\"\n    return LAMBDA_ID * tf.reduce_mean(tf.abs(real - same))\n\nprint('Losses defined (LSGAN).')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:35.064585Z","iopub.execute_input":"2026-05-29T10:44:35.065276Z","iopub.status.idle":"2026-05-29T10:44:35.071906Z","shell.execute_reply.started":"2026-05-29T10:44:35.065247Z","shell.execute_reply":"2026-05-29T10:44:35.071017Z"}},"outputs":[],"execution_count":null},{"id":"cell_7","cell_type":"code","source":"# generator_g : Photo -> Monet  (this is what we submit)\n# generator_f : Monet -> Photo  (needed for cycle-consistency)\n# discriminator_x : Photo critic\n# discriminator_y : Monet critic\ngenerator_g      = build_generator()\ngenerator_f      = build_generator()\ndiscriminator_x  = build_discriminator()\ndiscriminator_y  = build_discriminator()\n\ngenerator_g_optimizer      = tf.keras.optimizers.Adam(LR, beta_1=0.5)\ngenerator_f_optimizer      = tf.keras.optimizers.Adam(LR, beta_1=0.5)\ndiscriminator_x_optimizer  = tf.keras.optimizers.Adam(LR, beta_1=0.5)\ndiscriminator_y_optimizer  = tf.keras.optimizers.Adam(LR, beta_1=0.5)\n\nprint(f'Generator params      : {generator_g.count_params():,}')\nprint(f'Discriminator params  : {discriminator_x.count_params():,}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:35.503746Z","iopub.execute_input":"2026-05-29T10:44:35.504908Z","iopub.status.idle":"2026-05-29T10:44:37.790843Z","shell.execute_reply.started":"2026-05-29T10:44:35.504869Z","shell.execute_reply":"2026-05-29T10:44:37.789876Z"}},"outputs":[],"execution_count":null},{"id":"cell_8","cell_type":"code","source":"@tf.function\ndef train_step(real_x, real_y):\n    \"\"\"\n    real_x = photo batch   real_y = monet batch\n    generator_g(photo) -> fake monet   [direction is correct; unchanged from original]\n    generator_f(monet) -> fake photo   [for cycle consistency only]\n    \"\"\"\n    with tf.GradientTape(persistent=True) as tape:\n\n        # ── Forward passes ───────────────────────────────────────────────────\n        fake_y    = generator_g(real_x, training=True)   # photo -> fake monet\n        fake_x    = generator_f(real_y, training=True)   # monet -> fake photo\n        cycled_x  = generator_f(fake_y, training=True)   # photo -> monet -> photo\n        cycled_y  = generator_g(fake_x, training=True)   # monet -> photo -> monet\n        same_x    = generator_f(real_x, training=True)   # identity: photo -> photo\n        same_y    = generator_g(real_y, training=True)   # identity: monet -> monet\n\n        disc_real_x = discriminator_x(real_x, training=True)\n        disc_real_y = discriminator_y(real_y, training=True)\n        disc_fake_x = discriminator_x(fake_x, training=True)\n        disc_fake_y = discriminator_y(fake_y, training=True)\n\n        # ── Generator losses ─────────────────────────────────────────────────\n        gen_g_adv       = generator_loss(disc_fake_y)\n        gen_f_adv       = generator_loss(disc_fake_x)\n        total_cycle     = cycle_loss(real_x, cycled_x) + cycle_loss(real_y, cycled_y)\n        total_gen_g     = gen_g_adv + total_cycle + identity_loss(real_y, same_y)\n        total_gen_f     = gen_f_adv + total_cycle + identity_loss(real_x, same_x)\n\n        # ── Discriminator losses ─────────────────────────────────────────────\n        disc_x_loss = discriminator_loss(disc_real_x, disc_fake_x)\n        disc_y_loss = discriminator_loss(disc_real_y, disc_fake_y)\n\n    # ── Apply gradients ──────────────────────────────────────────────────────\n    generator_g_optimizer.apply_gradients(zip(\n        tape.gradient(total_gen_g, generator_g.trainable_variables),\n        generator_g.trainable_variables))\n\n    generator_f_optimizer.apply_gradients(zip(\n        tape.gradient(total_gen_f, generator_f.trainable_variables),\n        generator_f.trainable_variables))\n\n    discriminator_x_optimizer.apply_gradients(zip(\n        tape.gradient(disc_x_loss, discriminator_x.trainable_variables),\n        discriminator_x.trainable_variables))\n\n    discriminator_y_optimizer.apply_gradients(zip(\n        tape.gradient(disc_y_loss, discriminator_y.trainable_variables),\n        discriminator_y.trainable_variables))\n\n    del tape\n    return total_gen_g, total_gen_f, disc_x_loss, disc_y_loss\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:38.144456Z","iopub.execute_input":"2026-05-29T10:44:38.145213Z","iopub.status.idle":"2026-05-29T10:44:38.154871Z","shell.execute_reply.started":"2026-05-29T10:44:38.145180Z","shell.execute_reply":"2026-05-29T10:44:38.153931Z"}},"outputs":[],"execution_count":null},{"id":"cell_9","cell_type":"code","source":"TIME_BUDGET = 4.3 * 3600  # stop well before Kaggle's 5-hour limit\nstart = time.time()\n\nprint(f'Training {EPOCHS} epochs x {steps_per_epoch} steps/epoch')\nprint(f'(Original stopped at 300 steps/epoch; now uses all {len(photo_paths)} photos)\\n')\n\nfor epoch in range(EPOCHS):\n    ep_start = time.time()\n    gg_sum = gf_sum = dx_sum = dy_sum = 0.0\n\n    # zip stops when photo_ds is exhausted; monet_ds loops via .repeat()\n    paired = tf.data.Dataset.zip((photo_ds, monet_ds)).take(steps_per_epoch)\n    for step, (image_x, image_y) in enumerate(paired):\n        gg, gf, dx, dy = train_step(image_x, image_y)\n        gg_sum += gg.numpy()\n        gf_sum += gf.numpy()\n        dx_sum += dx.numpy()\n        dy_sum += dy.numpy()\n\n    ep_t = time.time() - ep_start\n    n    = steps_per_epoch\n    print(f'Epoch {epoch+1:2d}/{EPOCHS} | '\n          f'G_g={gg_sum/n:.3f}  G_f={gf_sum/n:.3f}  '\n          f'D_x={dx_sum/n:.3f}  D_y={dy_sum/n:.3f} | {ep_t:.0f}s')\n\n    if time.time() - start > TIME_BUDGET:\n        print(f'Time budget reached at epoch {epoch+1} — stopping early.')\n        break\n\nprint(f'\\nTraining done in {(time.time()-start)/60:.1f} min')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T10:44:41.438917Z","iopub.execute_input":"2026-05-29T10:44:41.439644Z"}},"outputs":[],"execution_count":null},{"id":"cell_10","cell_type":"code","source":"for img_batch in photo_ds.take(3):\n    pred = generator_g(img_batch, training=False)\n    orig = np.clip((img_batch[0].numpy() + 1) / 2, 0, 1)\n    gen  = np.clip((pred[0].numpy()       + 1) / 2, 0, 1)\n\n    plt.figure(figsize=(10, 4))\n    plt.subplot(1, 2, 1); plt.imshow(orig); plt.title('Original Photo'); plt.axis('off')\n    plt.subplot(1, 2, 2); plt.imshow(gen);  plt.title('Generated Monet'); plt.axis('off')\n    plt.tight_layout(); plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"cell_11","cell_type":"code","source":"OUTPUT_ZIP = '/kaggle/working/images.zip'  # explicit absolute path\ntotal      = len(photo_paths)\nprint(f'Generating {total} Monet-style images...')\n\nwith zipfile.ZipFile(OUTPUT_ZIP, 'w', zipfile.ZIP_DEFLATED) as img_zip:\n    for i, img_batch in enumerate(photo_infer_ds):\n\n        pred = generator_g(img_batch, training=False)\n        img  = pred[0].numpy()\n\n        # ── FIX 5: clip before uint8 cast ──────────────────────────────────\n        #    tanh output can be slightly outside [-1, 1] due to float precision.\n        #    Without clip, e.g. 255.0001 wraps to 0 as uint8 — pixel corruption.\n        img = np.clip((img + 1) * 127.5, 0, 255).astype(np.uint8)\n\n        buf = io.BytesIO()\n        Image.fromarray(img).save(buf, format='JPEG', quality=95)\n        img_zip.writestr(f'{i:05d}.jpg', buf.getvalue())\n\n        if i % 1000 == 0:\n            print(f'  {i}/{total}')\n\nsize_mb = os.path.getsize(OUTPUT_ZIP) / 1024 / 1024\nprint(f'\\nDone! {OUTPUT_ZIP}  ({size_mb:.1f} MB)')\n\n# Verify submission file\nwith zipfile.ZipFile(OUTPUT_ZIP, 'r') as z:\n    n = len(z.namelist())\n    ok = 7000 <= n <= 10000\n    print(f'Images in zip: {n}  {\"OK\" if ok else \"ERROR: outside 7000-10000 range\"}')\n    print(f'First 3: {z.namelist()[:3]}')\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"2579d826-d601-45aa-9a11-195b85769420","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}