{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.11"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"cae75c22","cell_type":"markdown","source":"# Monet CycleGAN — Kaggle Notebook\n\nClean split-cell version with the corrected Kaggle dataset path, TFRecord/JPEG loading, CycleGAN-style models, training loop, and final `images.zip` export.","metadata":{}},{"id":"9dce4da8","cell_type":"code","source":"# imports & environment setup\nimport os, sys\n\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"]       = \"3\"   # suppress C++ INFO/WARN/ERROR\nos.environ[\"TF_ENABLE_ONEDNN_OPTS\"]     = \"0\"   # avoid oneDNN warnings\nos.environ[\"TF_FORCE_GPU_ALLOW_GROWTH\"] = \"true\"\n\nimport gc, io, zipfile, warnings\nfrom pathlib import Path\n\nimport numpy as np\n\ntry:                                              # silence absl/proto noise\n    import absl.logging\n    absl.logging.set_verbosity(absl.logging.ERROR)\nexcept Exception:\n    pass\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, initializers\n\nimport matplotlib\nmatplotlib.rcParams.update({\n    \"figure.facecolor\": \"#0d0d0d\",\n    \"axes.facecolor\":   \"#1a1a1a\",\n    \"text.color\":       \"white\",\n})\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nwarnings.filterwarnings(\"ignore\")\ntf.get_logger().setLevel(\"ERROR\")\n\ntry:\n    from kaggle_datasets import KaggleDatasets\n    HAS_KAGGLE_DATASETS = True\nexcept Exception:\n    KaggleDatasets = None\n    HAS_KAGGLE_DATASETS = False\n\ntry:\n    import tensorflow_addons as tfa\n    HAS_TFA = True\nexcept Exception:\n    tfa = None\n    HAS_TFA = False\n\ngpus = tf.config.list_physical_devices(\"GPU\")\nif gpus:\n    for gpu in gpus:\n        tf.config.experimental.set_memory_growth(gpu, True)\n\nprint(\"TensorFlow :\", tf.__version__)\nprint(\"TF-Addons  :\", \"available\" if HAS_TFA else \"not available (using tf.keras fallback)\")\nprint(\"GPUs       :\", gpus or \"none — using CPU\")\nprint(\"TPUs       :\", tf.config.list_physical_devices(\"TPU\") or \"none\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:05.036357Z","iopub.execute_input":"2026-06-06T04:15:05.036642Z","iopub.status.idle":"2026-06-06T04:15:05.066695Z","shell.execute_reply.started":"2026-06-06T04:15:05.036621Z","shell.execute_reply":"2026-06-06T04:15:05.065852Z"}},"outputs":[],"execution_count":null},{"id":"ea583fa0","cell_type":"code","source":"SEED = 42\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nIMAGE_SIZE = 256\nCROP_SIZE = 286\nBATCH_SIZE = 1\nAUTOTUNE = tf.data.AUTOTUNE\n\nLR_INIT = 2e-4\nBETA_1 = 0.5\nBETA_2 = 0.999\nEPSILON = 1e-8\n\nLAMBDA_CYCLE = 10.0\nLAMBDA_IDENTITY = 5.0\nN_RESIDUAL_BLOCKS = 9\nSE_REDUCTION = 16\n\nREPLAY_BUFFER_SIZE = 50\nEMA_DECAY = 0.999\nEPOCHS_CONSTANT = 100\nEPOCHS_DECAY = 100\nTOTAL_EPOCHS = EPOCHS_CONSTANT + EPOCHS_DECAY\n\nWORK_ROOT = \"/kaggle/working\"\nCKPT_DIR = os.path.join(WORK_ROOT, \"checkpoints\")\nSAMPLE_DIR = os.path.join(WORK_ROOT, \"samples\")\nGEN_DIR = os.path.join(WORK_ROOT, \"generated\")\nTMP_DIR = os.path.join(WORK_ROOT, \"tmp_export\")\n\nfor d in [CKPT_DIR, SAMPLE_DIR, GEN_DIR, TMP_DIR]:\n    os.makedirs(d, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:05.068171Z","iopub.execute_input":"2026-06-06T04:15:05.068435Z","iopub.status.idle":"2026-06-06T04:15:05.082804Z","shell.execute_reply.started":"2026-06-06T04:15:05.068415Z","shell.execute_reply":"2026-06-06T04:15:05.082138Z"}},"outputs":[],"execution_count":null},{"id":"08bdd656","cell_type":"code","source":"def resolve_data_root():\n    exact = \"/kaggle/input/competitions/gan-getting-started\"\n    if tf.io.gfile.exists(exact):\n        return exact\n\n    fallbacks = [\n        \"/kaggle/input/gan-getting-started\",\n        \"/kaggle/input/gan-getting-started-v2\",\n        \"/kaggle/input/gan-getting-started-2020\",\n    ]\n    for p in fallbacks:\n        if tf.io.gfile.exists(p):\n            return p\n\n    if HAS_KAGGLE_DATASETS:\n        try:\n            gcs = KaggleDatasets().get_gcs_path()\n            if tf.io.gfile.exists(gcs):\n                return gcs\n        except Exception:\n            pass\n\n    if tf.io.gfile.exists(\"/kaggle/input\"):\n        for sub in sorted(tf.io.gfile.listdir(\"/kaggle/input\")):\n            candidate = os.path.join(\"/kaggle/input\", sub)\n            if tf.io.gfile.glob(f\"{candidate}/monet_tfrec/*.tfrec\") or tf.io.gfile.glob(f\"{candidate}/photo_tfrec/*.tfrec\"):\n                return candidate\n            if tf.io.gfile.glob(f\"{candidate}/monet_jpg/*.jpg\") or tf.io.gfile.glob(f\"{candidate}/photo_jpg/*.jpg\"):\n                return candidate\n\n    return exact\n\nDATA_ROOT = resolve_data_root()\nprint(\"Dataset root:\", DATA_ROOT)\n\nMONET_TFREC = sorted(tf.io.gfile.glob(f\"{DATA_ROOT}/monet_tfrec/*.tfrec\"))\nPHOTO_TFREC = sorted(tf.io.gfile.glob(f\"{DATA_ROOT}/photo_tfrec/*.tfrec\"))\nMONET_JPEGS = sorted(tf.io.gfile.glob(f\"{DATA_ROOT}/monet_jpg/*.jpg\"))\nPHOTO_JPEGS = sorted(tf.io.gfile.glob(f\"{DATA_ROOT}/photo_jpg/*.jpg\"))\n\nprint(\"Monet TFRec shards:\", len(MONET_TFREC))\nprint(\"Photo TFRec shards:\", len(PHOTO_TFREC))\nprint(\"Monet JPEGs:\", len(MONET_JPEGS))\nprint(\"Photo JPEGs:\", len(PHOTO_JPEGS))\n\nif not (MONET_TFREC or PHOTO_TFREC or MONET_JPEGS or PHOTO_JPEGS):\n    raise FileNotFoundError(\"No Monet/Photo files found. Attach the competition dataset in Kaggle and rerun.\")\n\nUSE_JPEGS = bool(MONET_JPEGS and PHOTO_JPEGS)\nprint(\"Using JPEG pipeline:\", USE_JPEGS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:05.083690Z","iopub.execute_input":"2026-06-06T04:15:05.083985Z","iopub.status.idle":"2026-06-06T04:15:08.217131Z","shell.execute_reply.started":"2026-06-06T04:15:05.083959Z","shell.execute_reply":"2026-06-06T04:15:08.216225Z"}},"outputs":[],"execution_count":null},{"id":"eb196399","cell_type":"code","source":"def decode_image_bytes(image_bytes):\n    image = tf.image.decode_jpeg(image_bytes, channels=3)\n    image = tf.image.convert_image_dtype(image, tf.float32)\n    image = image * 2.0 - 1.0\n    image = tf.reshape(image, [IMAGE_SIZE, IMAGE_SIZE, 3])\n    return image\n\ndef parse_tfrec(example):\n    feature_description = {\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, feature_description)\n    return decode_image_bytes(example[\"image\"])\n\ndef decode_jpeg_file(filename):\n    image = tf.io.read_file(filename)\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = tf.image.resize(image, [IMAGE_SIZE, IMAGE_SIZE], method=\"bilinear\", antialias=True)\n    image = tf.cast(image, tf.float32) / 127.5 - 1.0\n    return image\n\ndef augment_train(image):\n    image = tf.image.resize(image, [CROP_SIZE, CROP_SIZE], method=\"bilinear\", antialias=True)\n    image = tf.image.random_crop(image, [IMAGE_SIZE, IMAGE_SIZE, 3])\n    image = tf.image.random_flip_left_right(image)\n    image = tf.cond(tf.random.uniform(()) > 0.85, lambda: tf.image.random_flip_up_down(image), lambda: image)\n    image = tf.clip_by_value(image + tf.random.truncated_normal(tf.shape(image), stddev=0.015), -1.0, 1.0)\n    return image\n\ndef augment_eval(image):\n    image = tf.image.resize(image, [IMAGE_SIZE, IMAGE_SIZE], method=\"bilinear\", antialias=True)\n    image = tf.clip_by_value(image, -1.0, 1.0)\n    return image\n\ndef build_dataset_from_tfrecords(files, training=True):\n    ds = tf.data.TFRecordDataset(files, num_parallel_reads=AUTOTUNE)\n    ds = ds.map(parse_tfrec, num_parallel_calls=AUTOTUNE)\n    if training:\n        ds = ds.shuffle(512, seed=SEED, reshuffle_each_iteration=True)\n        ds = ds.map(augment_train, num_parallel_calls=AUTOTUNE)\n        ds = ds.repeat()\n    else:\n        ds = ds.map(augment_eval, num_parallel_calls=AUTOTUNE)\n    ds = ds.batch(BATCH_SIZE, drop_remainder=training)\n    ds = ds.prefetch(AUTOTUNE)\n    return ds\n\ndef build_dataset_from_jpegs(files, training=True):\n    ds = tf.data.Dataset.from_tensor_slices(files)\n    if training:\n        ds = ds.shuffle(len(files), seed=SEED, reshuffle_each_iteration=True)\n    ds = ds.map(decode_jpeg_file, num_parallel_calls=AUTOTUNE)\n    if training:\n        ds = ds.map(augment_train, num_parallel_calls=AUTOTUNE)\n        ds = ds.repeat()\n    else:\n        ds = ds.map(augment_eval, num_parallel_calls=AUTOTUNE)\n    ds = ds.batch(BATCH_SIZE, drop_remainder=training)\n    ds = ds.prefetch(AUTOTUNE)\n    return ds\n\nif USE_JPEGS:\n    monet_train_ds = build_dataset_from_jpegs(MONET_JPEGS, training=True)\n    photo_train_ds = build_dataset_from_jpegs(PHOTO_JPEGS, training=True)\n    monet_eval_ds = build_dataset_from_jpegs(MONET_JPEGS, training=False)\n    photo_eval_ds = build_dataset_from_jpegs(PHOTO_JPEGS, training=False)\nelse:\n    monet_train_ds = build_dataset_from_tfrecords(MONET_TFREC, training=True)\n    photo_train_ds = build_dataset_from_tfrecords(PHOTO_TFREC, training=True)\n    monet_eval_ds = build_dataset_from_tfrecords(MONET_TFREC, training=False)\n    photo_eval_ds = build_dataset_from_tfrecords(PHOTO_TFREC, training=False)\n\ndef show_samples(photo_ds, monet_ds, n=4, save_path=None):\n    photo_batch = next(iter(photo_ds))\n    monet_batch = next(iter(monet_ds))\n    n = min(n, photo_batch.shape[0], monet_batch.shape[0])\n    fig, axes = plt.subplots(2, n, figsize=(4 * n, 8))\n    if n == 1:\n        axes = np.array([[axes[0]], [axes[1]]])\n    for i in range(n):\n        axes[0, i].imshow(((photo_batch[i] * 0.5 + 0.5).numpy()).clip(0, 1))\n        axes[0, i].axis(\"off\")\n        axes[1, i].imshow(((monet_batch[i] * 0.5 + 0.5).numpy()).clip(0, 1))\n        axes[1, i].axis(\"off\")\n    axes[0, 0].set_ylabel(\"Photo\")\n    axes[1, 0].set_ylabel(\"Monet\")\n    plt.tight_layout()\n    if save_path:\n        plt.savefig(save_path, bbox_inches=\"tight\", dpi=150)\n    plt.show()\n    plt.close(fig)\n\nshow_samples(photo_eval_ds, monet_eval_ds, n=4, save_path=os.path.join(SAMPLE_DIR, \"dataset_preview.png\"))\nprint(\"Dataset preview saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:08.218177Z","iopub.execute_input":"2026-06-06T04:15:08.218435Z","iopub.status.idle":"2026-06-06T04:15:08.942415Z","shell.execute_reply.started":"2026-06-06T04:15:08.218416Z","shell.execute_reply":"2026-06-06T04:15:08.941198Z"}},"outputs":[],"execution_count":null},{"id":"5c7f9cc6-ef54-43aa-a933-efdbeb5aaefc","cell_type":"code","source":"# dataset rich preview\ndef visualize_dataset_preview(photo_ds, monet_ds, n=4):\n    from matplotlib.patches import FancyBboxPatch\n    photo_batch = next(iter(photo_ds))\n    monet_batch = next(iter(monet_ds))\n    n = min(n, photo_batch.shape[0], monet_batch.shape[0])\n\n    fig = plt.figure(figsize=(n * 3.8, 9.5), facecolor='#0d0d0d')\n    fig.suptitle(\"CycleGAN — Domain Preview  |  Photo vs. Monet\",\n                 fontsize=15, fontweight='bold', color='white', y=1.01)\n    gs = fig.add_gridspec(3, n, hspace=0.07, wspace=0.04)\n\n    domains = [\n        (\"📷  Photo Domain\",  photo_batch, '#3a86ff'),\n        (\"🎨  Monet Domain\",  monet_batch, '#ff6b6b'),\n    ]\n    for row, (label, batch, color) in enumerate(domains):\n        for col in range(n):\n            ax = fig.add_subplot(gs[row, col])\n            img = (batch[col] * 0.5 + 0.5).numpy().clip(0, 1)\n            ax.imshow(img)\n            ax.axis('off')\n            for sp in ax.spines.values():\n                sp.set_edgecolor(color); sp.set_linewidth(2.0); sp.set_visible(True)\n            if col == 0:\n                ax.text(-0.06, 0.5, label, transform=ax.transAxes,\n                        color=color, fontsize=9, fontweight='bold',\n                        rotation=90, va='center', ha='right')\n            mean_r, mean_g, mean_b = img[:,:,0].mean(), img[:,:,1].mean(), img[:,:,2].mean()\n            ax.set_title(f\"R:{mean_r:.2f} G:{mean_g:.2f} B:{mean_b:.2f}\",\n                         color='#aaaaaa', fontsize=6.5, pad=3)\n\n    ch_colors = ['#ff4444', '#44cc44', '#4488ff']\n    ch_labels = ['R', 'G', 'B']\n    for col in range(n):\n        ax = fig.add_subplot(gs[2, col])\n        ax.set_facecolor('#1a1a1a')\n        p_img = (photo_batch[col] * 0.5 + 0.5).numpy().clip(0, 1)\n        m_img = (monet_batch[col] * 0.5 + 0.5).numpy().clip(0, 1)\n        for ch, (c, nm) in enumerate(zip(ch_colors, ch_labels)):\n            ax.hist(p_img[:,:,ch].flatten(), bins=48, alpha=0.40, color=c,\n                    density=True, histtype='stepfilled')\n            ax.hist(m_img[:,:,ch].flatten(), bins=48, alpha=0.90, color=c,\n                    density=True, histtype='step', linewidth=1.6, linestyle='--')\n        ax.set_xlim(0, 1); ax.tick_params(colors='#777777', labelsize=5.5)\n        ax.set_xlabel(\"Intensity\", color='#777777', fontsize=6.5)\n        if col == 0:\n            ax.set_ylabel(\"Density\", color='#777777', fontsize=6.5)\n            from matplotlib.lines import Line2D\n            handles = [\n                Line2D([0],[0], color='#aaaaaa', lw=2,   label='Photo (solid)'),\n                Line2D([0],[0], color='#aaaaaa', lw=2, linestyle='--', label='Monet (dashed)'),\n            ]\n            ax.legend(handles=handles, fontsize=5.5, facecolor='#2a2a2a',\n                      labelcolor='white', loc='upper right')\n        ax.set_title(\"RGB Distributions\", color='#888888', fontsize=6.5, pad=2)\n        for sp in ax.spines.values():\n            sp.set_edgecolor('#333333')\n\n    preview_path = os.path.join(SAMPLE_DIR, \"rich_domain_preview.png\")\n    plt.savefig(preview_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n    plt.show()\n    print(f\"Saved → {preview_path}\")\n\nvisualize_dataset_preview(photo_eval_ds, monet_eval_ds, n=4)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:08.944142Z","iopub.execute_input":"2026-06-06T04:15:08.944452Z","iopub.status.idle":"2026-06-06T04:15:09.418438Z","shell.execute_reply.started":"2026-06-06T04:15:08.944431Z","shell.execute_reply":"2026-06-06T04:15:09.417712Z"}},"outputs":[],"execution_count":null},{"id":"095a9e9e","cell_type":"code","source":"class 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        channels = input_shape[-1]\n        self.gamma = self.add_weight(name=\"gamma\", shape=(channels,), initializer=\"ones\", trainable=True)\n        self.beta = self.add_weight(name=\"beta\", shape=(channels,), initializer=\"zeros\", trainable=True)\n        super().build(input_shape)\n\n    def call(self, inputs):\n        mean, variance = tf.nn.moments(inputs, axes=[1, 2], keepdims=True)\n        x = (inputs - mean) * tf.math.rsqrt(variance + self.epsilon)\n        return x * self.gamma + self.beta\n\nclass SEBlock(layers.Layer):\n    def __init__(self, reduction=SE_REDUCTION, **kwargs):\n        super().__init__(**kwargs)\n        self.reduction = reduction\n\n    def build(self, input_shape):\n        channels = input_shape[-1]\n        hidden = max(channels // self.reduction, 4)\n        self.pool = layers.GlobalAveragePooling2D(keepdims=True)\n        self.fc1 = layers.Dense(hidden, activation=\"relu\")\n        self.fc2 = layers.Dense(channels, activation=\"sigmoid\")\n        super().build(input_shape)\n\n    def call(self, inputs):\n        x = self.pool(inputs)\n        x = self.fc1(x)\n        x = self.fc2(x)\n        return inputs * x\n\ndef conv_norm_relu(x, filters, kernel_size=3, strides=1, name=None):\n    init = initializers.TruncatedNormal(stddev=0.02)\n    x = layers.Conv2D(filters, kernel_size, strides=strides, padding=\"same\", use_bias=False, kernel_initializer=init, name=None if name is None else f\"{name}_conv\")(x)\n    x = InstanceNormalization(name=None if name is None else f\"{name}_in\")(x)\n    x = layers.ReLU(name=None if name is None else f\"{name}_relu\")(x)\n    return x\n\ndef residual_block(x, filters, idx):\n    init = initializers.TruncatedNormal(stddev=0.02)\n    shortcut = x\n    y = layers.Conv2D(filters, 3, padding=\"same\", use_bias=False, kernel_initializer=init, name=f\"res_{idx}_conv1\")(x)\n    y = InstanceNormalization(name=f\"res_{idx}_in1\")(y)\n    y = layers.ReLU(name=f\"res_{idx}_relu1\")(y)\n    y = layers.Conv2D(filters, 3, padding=\"same\", use_bias=False, kernel_initializer=init, name=f\"res_{idx}_conv2\")(y)\n    y = InstanceNormalization(name=f\"res_{idx}_in2\")(y)\n    y = SEBlock(name=f\"res_{idx}_se\")(y)\n    return layers.Add(name=f\"res_{idx}_add\")([shortcut, y])\n\ndef upsample_block(x, filters, name):\n    init = initializers.TruncatedNormal(stddev=0.02)\n    x = layers.UpSampling2D(interpolation=\"bilinear\", name=f\"{name}_up\")(x)\n    x = layers.Conv2D(filters, 3, padding=\"same\", use_bias=False, kernel_initializer=init, name=f\"{name}_conv\")(x)\n    x = InstanceNormalization(name=f\"{name}_in\")(x)\n    x = layers.ReLU(name=f\"{name}_relu\")(x)\n    return x\n\ndef build_generator(name=\"gen_G\", n_res=N_RESIDUAL_BLOCKS):\n    init = initializers.TruncatedNormal(stddev=0.02)\n    inputs = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3), name=f\"{name}_input\")\n    x = layers.ZeroPadding2D(3, name=f\"{name}_pad0\")(inputs)\n    x = layers.Conv2D(64, 7, padding=\"valid\", use_bias=False, kernel_initializer=init, name=f\"{name}_stem_conv\")(x)\n    x = InstanceNormalization(name=f\"{name}_stem_in\")(x)\n    x = layers.ReLU(name=f\"{name}_stem_relu\")(x)\n    x = conv_norm_relu(x, 128, 3, 2, name=f\"{name}_down1\")\n    x = conv_norm_relu(x, 256, 3, 2, name=f\"{name}_down2\")\n    for i in range(n_res):\n        x = residual_block(x, 256, i)\n    x = upsample_block(x, 128, f\"{name}_up1\")\n    x = upsample_block(x, 64, f\"{name}_up2\")\n    x = layers.ZeroPadding2D(3, name=f\"{name}_pad1\")(x)\n    x = layers.Conv2D(3, 7, padding=\"valid\", kernel_initializer=init, name=f\"{name}_out_conv\")(x)\n    outputs = layers.Activation(\"tanh\", name=f\"{name}_tanh\")(x)\n    return keras.Model(inputs, outputs, name=name)\n\ndef build_discriminator(name=\"disc_X\"):\n    init = initializers.TruncatedNormal(stddev=0.02)\n    inputs = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3), name=f\"{name}_input\")\n\n    def sn_conv(filters, stride=1, layer_name=None):\n        conv = layers.Conv2D(filters, 4, strides=stride, padding=\"same\", use_bias=True, kernel_initializer=init, name=layer_name)\n        if HAS_TFA:\n            return tfa.layers.SpectralNormalization(conv)\n        if hasattr(tf.keras.layers, \"SpectralNormalization\"):   # TF 2.8+\n            return tf.keras.layers.SpectralNormalization(conv)\n        return conv\n\n    x = sn_conv(64, 2, f\"{name}_conv1\")(inputs)\n    x = layers.LeakyReLU(0.2, name=f\"{name}_lrelu1\")(x)\n    x = sn_conv(128, 2, f\"{name}_conv2\")(x)\n    x = InstanceNormalization(name=f\"{name}_in2\")(x)\n    x = layers.LeakyReLU(0.2, name=f\"{name}_lrelu2\")(x)\n    x = sn_conv(256, 2, f\"{name}_conv3\")(x)\n    x = InstanceNormalization(name=f\"{name}_in3\")(x)\n    x = layers.LeakyReLU(0.2, name=f\"{name}_lrelu3\")(x)\n    x = sn_conv(512, 1, f\"{name}_conv4\")(x)\n    x = InstanceNormalization(name=f\"{name}_in4\")(x)\n    x = layers.LeakyReLU(0.2, name=f\"{name}_lrelu4\")(x)\n    x = layers.ZeroPadding2D(name=f\"{name}_pad\")(x)\n    outputs = layers.Conv2D(1, 4, padding=\"valid\", kernel_initializer=init, name=f\"{name}_out_conv\")(x)\n    return keras.Model(inputs, outputs, name=name)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:09.419350Z","iopub.execute_input":"2026-06-06T04:15:09.419589Z","iopub.status.idle":"2026-06-06T04:15:09.442214Z","shell.execute_reply.started":"2026-06-06T04:15:09.419560Z","shell.execute_reply":"2026-06-06T04:15:09.441123Z"}},"outputs":[],"execution_count":null},{"id":"dcaa2093","cell_type":"code","source":"strategy = tf.distribute.get_strategy()\nwith strategy.scope():\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\ngen_G.summary(line_length=110)\nprint()\ndisc_X.summary(line_length=110)\nprint()\nprint(\"Generator params:\", f\"{gen_G.count_params():,}\")\nprint(\"Discriminator params:\", f\"{disc_X.count_params():,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:09.443471Z","iopub.execute_input":"2026-06-06T04:15:09.443703Z","iopub.status.idle":"2026-06-06T04:15:11.152141Z","shell.execute_reply.started":"2026-06-06T04:15:09.443686Z","shell.execute_reply":"2026-06-06T04:15:11.151319Z"}},"outputs":[],"execution_count":null},{"id":"0f11f45c-ee59-4ab0-9867-7b7e988b43c3","cell_type":"code","source":"# architecture diagram\ndef draw_cyclegan_architecture():\n    from matplotlib.patches import FancyBboxPatch, FancyArrowPatch\n\n    fig, ax = plt.subplots(figsize=(20, 9), facecolor='#0d0d0d')\n    ax.set_facecolor('#0d0d0d')\n    ax.set_xlim(0, 20); ax.set_ylim(0, 9)\n    ax.axis('off')\n    ax.set_title(\"CycleGAN Architecture — Photo ↔ Monet Style Transfer\",\n                 fontsize=16, color='white', fontweight='bold', pad=14)\n\n    def blk(x, y, w, h, title, sub='', fc='#1e3a5f', ec='#3a86ff', tc='white', fs=9):\n        rect = FancyBboxPatch((x, y), w, h, boxstyle=\"round,pad=0.08\",\n                              facecolor=fc, edgecolor=ec, linewidth=1.8, zorder=3)\n        ax.add_patch(rect)\n        dy = 0.14 if sub else 0\n        ax.text(x+w/2, y+h/2+dy, title, ha='center', va='center',\n                fontsize=fs, fontweight='bold', color=tc, zorder=4)\n        if sub:\n            ax.text(x+w/2, y+h/2-0.24, sub, ha='center', va='center',\n                    fontsize=6.2, color='#cccccc', zorder=4)\n\n    def arr(x1, y1, x2, y2, color='#aaaaaa', lbl='', lpos='mid'):\n        ax.annotate('', xy=(x2,y2), xytext=(x1,y1),\n                    arrowprops=dict(arrowstyle='->', color=color, lw=1.8), zorder=5)\n        if lbl:\n            mx,my = (x1+x2)/2, (y1+y2)/2\n            ax.text(mx, my+0.18, lbl, ha='center', fontsize=7, color='#cccccc', zorder=5)\n\n    # ── domains\n    blk(0.2, 3.8, 1.7, 1.8, \"Photo\\nDomain\", \"Real  X\",  '#1b4332','#3a86ff','#90e0ef', 8)\n    blk(18.1,3.8, 1.7, 1.8, \"Monet\\nDomain\", \"Real  Y\",  '#7b2d00','#ff6b6b','#ffb4a2', 8)\n\n    # ── generators\n    blk(3.4, 6.1, 2.8, 1.4, \"Generator G\", \"Photo→Monet\\n9 ResBlocks+SE+IN\", '#003566','#3a86ff')\n    blk(3.4, 1.4, 2.8, 1.4, \"Generator F\", \"Monet→Photo\\n9 ResBlocks+SE+IN\", '#003566','#3a86ff')\n\n    # ── fake images\n    blk(7.0, 6.1, 2.0, 1.4, \"Fake Monet\",  \"G(x)\",  '#4a0072','#c77dff','#e0aaff', 8)\n    blk(7.0, 1.4, 2.0, 1.4, \"Fake Photo\",  \"F(y)\",  '#4a0072','#c77dff','#e0aaff', 8)\n\n    # ── discriminators\n    blk(9.5, 6.1, 2.8, 1.4, \"Disc Y\",  \"PatchGAN 70×70\\nSpectralNorm\", '#5c1a00','#e76f51')\n    blk(9.5, 1.4, 2.8, 1.4, \"Disc X\",  \"PatchGAN 70×70\\nSpectralNorm\", '#5c1a00','#e76f51')\n\n    # ── cycled images\n    blk(13.0,6.1, 2.2, 1.4, \"Cycled\\nPhoto\", \"F(G(x))\", '#003d33','#52b788','#95d5b2', 8)\n    blk(13.0,1.4, 2.2, 1.4, \"Cycled\\nMonet\", \"G(F(y))\", '#003d33','#52b788','#95d5b2', 8)\n\n    # ── loss boxes\n    blk(15.6,6.1, 2.1, 1.4, \"Cycle Loss\", \"‖F(G(x))−x‖₁  λ=10\", '#5c4400','#ffd166','#111', 7.5)\n    blk(15.6,1.4, 2.1, 1.4, \"Cycle Loss\", \"‖G(F(y))−y‖₁  λ=10\", '#5c4400','#ffd166','#111', 7.5)\n    blk(13.2,3.8, 1.9, 1.6, \"Identity\\nLoss\", \"‖G(y)−y‖₁  λ=5\", '#4a3000','#f4a261','#111', 7.5)\n\n    # ── arrows top cycle (photo → monet → cycled)\n    arr(1.9,  4.7,  3.35, 6.8,  '#3a86ff', \"Real X\")\n    arr(6.2,  6.8,  6.95, 6.8)\n    arr(9.0,  6.8,  9.45, 6.8,  '#aaaaaa', \"Adv Loss Y\")\n    arr(12.3, 6.8,  12.95,6.8)\n    arr(15.2, 6.8,  15.55,6.8)\n\n    # ── arrows bottom cycle (monet → photo → cycled)\n    arr(18.1, 4.7,  6.25, 2.1,  '#ff6b6b', \"Real Y\")\n    arr(9.0,  2.1,  9.45, 2.1,  '#aaaaaa', \"Adv Loss X\")\n    arr(12.3, 2.1,  12.95,2.1)\n    arr(15.2, 2.1,  15.55,2.1)\n\n    # ── identity path (optional dashed arrows)\n    ax.annotate('', xy=(13.2,4.7), xytext=(18.1,4.7),\n                arrowprops=dict(arrowstyle='->', color='#f4a261',\n                                lw=1.2, linestyle='dashed'), zorder=5)\n\n    # ── EMA + info footer\n    ax.text(10.0, 0.55,\n            \"EMA Weight Smoothing  •  Linear LR Decay (100 constant + 100 decay epochs)  \"\n            \"•  Replay Buffer (size 50)  •  LSGAN Objectives\",\n            ha='center', fontsize=8.5, color='#888888', style='italic', zorder=5)\n\n    arch_path = os.path.join(SAMPLE_DIR, \"cyclegan_architecture.png\")\n    plt.savefig(arch_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n    plt.show()\n    print(f\"Saved → {arch_path}\")\n\ndraw_cyclegan_architecture()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:11.153063Z","iopub.execute_input":"2026-06-06T04:15:11.153295Z","iopub.status.idle":"2026-06-06T04:15:11.559319Z","shell.execute_reply.started":"2026-06-06T04:15:11.153278Z","shell.execute_reply":"2026-06-06T04:15:11.558566Z"}},"outputs":[],"execution_count":null},{"id":"49fd265a","cell_type":"code","source":"def adv_loss_G(fake_logits):\n    return tf.reduce_mean(tf.math.squared_difference(fake_logits, 1.0))\n\ndef adv_loss_D(real_logits, fake_logits):\n    real_loss = tf.reduce_mean(tf.math.squared_difference(real_logits, 0.9))\n    fake_loss = tf.reduce_mean(tf.math.squared_difference(fake_logits, 0.0))\n    return 0.5 * (real_loss + fake_loss)\n\ndef cycle_consistency_loss(real, cycled):\n    return LAMBDA_CYCLE * tf.reduce_mean(tf.abs(real - cycled))\n\ndef identity_mapping_loss(real, same):\n    return LAMBDA_IDENTITY * tf.reduce_mean(tf.abs(real - same))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:11.560195Z","iopub.execute_input":"2026-06-06T04:15:11.560412Z","iopub.status.idle":"2026-06-06T04:15:11.566606Z","shell.execute_reply.started":"2026-06-06T04:15:11.560390Z","shell.execute_reply":"2026-06-06T04:15:11.565835Z"}},"outputs":[],"execution_count":null},{"id":"3c49bb53","cell_type":"code","source":"class LinearDecaySchedule(tf.keras.optimizers.schedules.LearningRateSchedule):\n    def __init__(self, initial_lr, steps_constant, steps_decay, **kwargs):\n        super().__init__(**kwargs)\n        self.initial_lr = tf.constant(float(initial_lr), dtype=tf.float32)\n        self.steps_constant = tf.constant(float(steps_constant), dtype=tf.float32)\n        self.steps_decay = tf.constant(float(steps_decay), dtype=tf.float32)\n\n    def __call__(self, step):\n        step = tf.cast(step, tf.float32)\n        decayed = self.initial_lr * tf.maximum(0.0, 1.0 - (step - self.steps_constant) / tf.maximum(self.steps_decay, 1.0))\n        return tf.where(step < self.steps_constant, self.initial_lr, decayed)\n\nsteps_per_epoch = max(1, min(len(MONET_JPEGS) or len(MONET_TFREC), len(PHOTO_JPEGS) or len(PHOTO_TFREC)) // BATCH_SIZE)\nlr_sched = LinearDecaySchedule(LR_INIT, EPOCHS_CONSTANT * steps_per_epoch, EPOCHS_DECAY * steps_per_epoch)\n\nwith strategy.scope():\n    opt_gen_G = keras.optimizers.Adam(lr_sched, beta_1=BETA_1, beta_2=BETA_2, epsilon=EPSILON)\n    opt_gen_F = keras.optimizers.Adam(lr_sched, beta_1=BETA_1, beta_2=BETA_2, epsilon=EPSILON)\n    opt_disc_X = keras.optimizers.Adam(lr_sched, beta_1=BETA_1, beta_2=BETA_2, epsilon=EPSILON)\n    opt_disc_Y = keras.optimizers.Adam(lr_sched, beta_1=BETA_1, beta_2=BETA_2, epsilon=EPSILON)\n\nclass EMATracker:\n    def __init__(self, model, decay=EMA_DECAY):\n        self.model = model\n        self.decay = float(decay)\n        self.shadows = [tf.Variable(tf.identity(w), trainable=False, dtype=tf.float32) for w in model.trainable_weights]\n        self.backup = None\n\n    def update(self):\n        d = tf.constant(self.decay, dtype=tf.float32)\n        for shadow, weight in zip(self.shadows, self.model.trainable_weights):\n            shadow.assign(d * shadow + (1.0 - d) * tf.cast(weight, tf.float32))\n\n    def apply(self):\n        self.backup = [tf.identity(w) for w in self.model.trainable_weights]\n        for weight, shadow in zip(self.model.trainable_weights, self.shadows):\n            weight.assign(tf.cast(shadow, weight.dtype))\n\n    def restore(self):\n        if self.backup is None:\n            return\n        for weight, backup in zip(self.model.trainable_weights, self.backup):\n            weight.assign(backup)\n        self.backup = None\n\nema_gen_G = EMATracker(gen_G)\nema_gen_F = EMATracker(gen_F)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:11.567501Z","iopub.execute_input":"2026-06-06T04:15:11.567822Z","iopub.status.idle":"2026-06-06T04:15:11.696231Z","shell.execute_reply.started":"2026-06-06T04:15:11.567798Z","shell.execute_reply":"2026-06-06T04:15:11.695048Z"}},"outputs":[],"execution_count":null},{"id":"995cd68d-ca5c-4584-a183-d7bb0a91d262","cell_type":"code","source":"# LR schedule plot\ndef plot_lr_schedule(lr_schedule, total_steps, steps_per_epoch_):\n    sample_points = max(500, total_steps)\n    step_arr = np.linspace(0, total_steps, sample_points, dtype=np.int32)\n    lr_arr   = np.array([float(lr_schedule(int(s))) for s in step_arr])\n    ep_arr   = step_arr / max(steps_per_epoch_, 1)\n    split_ep = EPOCHS_CONSTANT\n\n    fig, axes = plt.subplots(1, 2, figsize=(15, 5), facecolor='#0d0d0d')\n    fig.suptitle(\"Learning Rate Schedule — Constant + Linear Decay\",\n                 fontsize=14, color='white', fontweight='bold')\n\n    for ax, x_vals, xlabel in zip(axes,\n                                   [step_arr, ep_arr],\n                                   [\"Training Step\", \"Epoch\"]):\n        ax.set_facecolor('#1a1a1a')\n        split_x = split_ep * steps_per_epoch_ if xlabel == \"Training Step\" else split_ep\n\n        ax.plot(x_vals, lr_arr, color='#3a86ff', lw=2.2, zorder=3)\n        ax.fill_between(x_vals, lr_arr, alpha=0.15, color='#3a86ff', zorder=2)\n\n        _split = EPOCHS_CONSTANT if xlabel == \"Epoch\" else EPOCHS_CONSTANT * steps_per_epoch_\n        ax.axvline(x=_split, color='#ff6b6b', lw=1.4, ls='--', alpha=0.8, zorder=4)\n        ax.axhline(y=0,      color='#444444', lw=0.8, zorder=1)\n\n        mid_c = (x_vals[0] + _split) / 2\n        mid_d = (_split + x_vals[-1]) / 2\n        ax.text(mid_c, LR_INIT * 1.07, f\"Constant ({EPOCHS_CONSTANT} ep)\",\n                ha='center', fontsize=8.5, color='#44ff99')\n        ax.text(mid_d, LR_INIT * 0.55, f\"Decay ({EPOCHS_DECAY} ep)\",\n                ha='center', fontsize=8.5, color='#ff6b6b')\n\n        ax.set_xlabel(xlabel, color='#888888', fontsize=10)\n        ax.set_ylabel(\"Learning Rate\", color='#888888', fontsize=10)\n        ax.tick_params(colors='#888888')\n        ax.grid(True, alpha=0.1, color='#ffffff')\n        for sp in ax.spines.values():\n            sp.set_edgecolor('#333333')\n\n        ax.set_ylim(-LR_INIT * 0.05, LR_INIT * 1.18)\n        ax.annotate(f\"LR₀ = {LR_INIT}\", xy=(x_vals[0], LR_INIT),\n                    xytext=(x_vals[int(len(x_vals)*0.08)], LR_INIT * 1.1),\n                    fontsize=7.5, color='#3a86ff',\n                    arrowprops=dict(arrowstyle='->', color='#3a86ff', lw=1.0))\n\n    plt.tight_layout()\n    lr_path = os.path.join(SAMPLE_DIR, \"lr_schedule.png\")\n    plt.savefig(lr_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n    plt.show()\n    print(f\"Saved → {lr_path}\")\n\nplot_lr_schedule(lr_sched, TOTAL_EPOCHS * steps_per_epoch, steps_per_epoch)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:11.697431Z","iopub.execute_input":"2026-06-06T04:15:11.697700Z","iopub.status.idle":"2026-06-06T04:15:38.607894Z","shell.execute_reply.started":"2026-06-06T04:15:11.697671Z","shell.execute_reply":"2026-06-06T04:15:38.606552Z"}},"outputs":[],"execution_count":null},{"id":"9eca8a78","cell_type":"code","source":"@tf.function\ndef train_step(photo_batch, monet_batch):\n    with tf.GradientTape(persistent=True) as tape:\n        fake_monet = gen_G(photo_batch, training=True)\n        cycled_photo = gen_F(fake_monet, training=True)\n\n        fake_photo = gen_F(monet_batch, training=True)\n        cycled_monet = gen_G(fake_photo, training=True)\n\n        same_monet = gen_G(monet_batch, training=True)\n        same_photo = gen_F(photo_batch, training=True)\n\n        disc_fake_monet = disc_Y(fake_monet, training=True)\n        disc_fake_photo = disc_X(fake_photo, training=True)\n        disc_real_monet = disc_Y(monet_batch, training=True)\n        disc_real_photo = disc_X(photo_batch, training=True)\n\n        gen_G_loss = adv_loss_G(disc_fake_monet) + cycle_consistency_loss(photo_batch, cycled_photo) + identity_mapping_loss(monet_batch, same_monet)\n        gen_F_loss = adv_loss_G(disc_fake_photo) + cycle_consistency_loss(monet_batch, cycled_monet) + identity_mapping_loss(photo_batch, same_photo)\n        disc_X_loss = adv_loss_D(disc_real_photo, disc_fake_photo)\n        disc_Y_loss = adv_loss_D(disc_real_monet, disc_fake_monet)\n\n    gen_G_grads = tape.gradient(gen_G_loss, gen_G.trainable_variables)\n    gen_F_grads = tape.gradient(gen_F_loss, gen_F.trainable_variables)\n    disc_X_grads = tape.gradient(disc_X_loss, disc_X.trainable_variables)\n    disc_Y_grads = tape.gradient(disc_Y_loss, disc_Y.trainable_variables)\n    del tape\n\n    opt_gen_G.apply_gradients(zip(gen_G_grads, gen_G.trainable_variables))\n    opt_gen_F.apply_gradients(zip(gen_F_grads, gen_F.trainable_variables))\n    opt_disc_X.apply_gradients(zip(disc_X_grads, disc_X.trainable_variables))\n    opt_disc_Y.apply_gradients(zip(disc_Y_grads, disc_Y.trainable_variables))\n\n    ema_gen_G.update()\n    ema_gen_F.update()\n\n    return {\n        \"gen_G_loss\": gen_G_loss,\n        \"gen_F_loss\": gen_F_loss,\n        \"disc_X_loss\": disc_X_loss,\n        \"disc_Y_loss\": disc_Y_loss,\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.610787Z","iopub.execute_input":"2026-06-06T04:15:38.611054Z","iopub.status.idle":"2026-06-06T04:15:38.619367Z","shell.execute_reply.started":"2026-06-06T04:15:38.611034Z","shell.execute_reply":"2026-06-06T04:15:38.618361Z"}},"outputs":[],"execution_count":null},{"id":"d1a3d37a-c5fa-4230-843a-f38ea4c828a5","cell_type":"code","source":"# cycle visualization\ndef visualize_cycle(gen_G_model, gen_F_model, photo_batch, monet_batch,\n                    epoch=0, n=2, save=True):\n    n = min(n, photo_batch.shape[0], monet_batch.shape[0])\n\n    fake_monet  = gen_G_model(photo_batch[:n],  training=False)\n    cycled_photo = gen_F_model(fake_monet,       training=False)\n    fake_photo  = gen_F_model(monet_batch[:n],  training=False)\n    cycled_monet = gen_G_model(fake_photo,       training=False)\n\n    def to_np(t, idx=None):\n        t2 = t[idx] if idx is not None else t\n        return (t2 * 0.5 + 0.5).numpy().clip(0, 1)\n\n    rows_cfg = [\n        (\"Photo → Fake Monet → Cycled Photo\",\n         [photo_batch, fake_monet, cycled_photo],\n         [\"Real Photo  X\", \"Fake Monet  G(X)\", \"Cycled Photo  F(G(X))\"],\n         ['#3a86ff', '#c77dff', '#52b788']),\n        (\"Monet → Fake Photo → Cycled Monet\",\n         [monet_batch, fake_photo, cycled_monet],\n         [\"Real Monet  Y\", \"Fake Photo  F(Y)\", \"Cycled Monet  G(F(Y))\"],\n         ['#ff6b6b', '#c77dff', '#ffd166']),\n    ]\n\n    total_rows = len(rows_cfg) * n\n    fig = plt.figure(figsize=(15, total_rows * 3.6 + 1), facecolor='#0d0d0d')\n    fig.suptitle(f\"Cycle Consistency Check — Epoch {epoch:03d}\",\n                 fontsize=14, color='white', fontweight='bold', y=1.01)\n    gs = fig.add_gridspec(total_rows, 3, hspace=0.08, wspace=0.04)\n\n    for r_idx, (title, tensors, col_labels, colors) in enumerate(rows_cfg):\n        for img_i in range(n):\n            grid_r = r_idx * n + img_i\n            for col_i, (tensor, col_lbl, color) in enumerate(\n                    zip(tensors, col_labels, colors)):\n                ax = fig.add_subplot(gs[grid_r, col_i])\n                ax.imshow(to_np(tensor, img_i))\n                ax.axis('off')\n                for sp in ax.spines.values():\n                    sp.set_edgecolor(color); sp.set_linewidth(2.0); sp.set_visible(True)\n                if img_i == 0:\n                    ax.set_title(col_lbl, color=color, fontsize=9,\n                                 fontweight='bold', pad=4)\n            if img_i == 0:\n                # Row group label on the left\n                ax0 = fig.add_subplot(gs[grid_r, 0])\n                ax0.text(-0.08, 0.5, title, transform=ax0.transAxes,\n                         fontsize=7.5, color='#aaaaaa', rotation=90,\n                         va='center', ha='right')\n\n    if save:\n        cyc_path = os.path.join(SAMPLE_DIR, f\"cycle_viz_epoch_{epoch:03d}.png\")\n        plt.savefig(cyc_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n        plt.show()\n        print(f\"Saved → {cyc_path}\")\n    else:\n        plt.show()\n\nprint(\"Cycle visualization ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.620366Z","iopub.execute_input":"2026-06-06T04:15:38.620852Z","iopub.status.idle":"2026-06-06T04:15:38.649621Z","shell.execute_reply.started":"2026-06-06T04:15:38.620830Z","shell.execute_reply":"2026-06-06T04:15:38.648662Z"}},"outputs":[],"execution_count":null},{"id":"3904aaef","cell_type":"code","source":"# image generation & styled training loop\ndef generate_and_save_images(model, epoch, test_input, folder, prefix, show=True):\n    preds = model(test_input, training=False)\n    n = preds.shape[0]\n    fig, axes = plt.subplots(1, n, figsize=(5 * n, 5.4), facecolor=\"#0d0d0d\")\n    axes = [axes] if n == 1 else list(axes)\n    for i, ax in enumerate(axes):\n        ax.imshow((preds[i] * 0.5 + 0.5).numpy().clip(0, 1))\n        ax.axis(\"off\")\n        ax.set_facecolor(\"#0d0d0d\")\n        for sp in ax.spines.values():\n            sp.set_edgecolor(\"#3a86ff\" if \"photo_to_monet\" in prefix else \"#ff6b6b\")\n            sp.set_linewidth(1.8); sp.set_visible(True)\n    domain = \"Photo → Monet\" if \"photo_to_monet\" in prefix else \"Monet → Photo\"\n    fig.suptitle(f\"{domain}  |  Epoch {epoch:03d}\", color=\"white\",\n                 fontsize=12, fontweight=\"bold\")\n    out = os.path.join(folder, f\"{prefix}_epoch_{epoch:03d}.png\")\n    plt.savefig(out, bbox_inches=\"tight\", dpi=150, facecolor=\"#0d0d0d\")\n    if show:\n        plt.show()\n    plt.close(fig)\n    return out\n\n\ndef train(epochs=TOTAL_EPOCHS, steps_per_epoch=steps_per_epoch, sample_every=10):\n    photo_iter = iter(photo_train_ds)\n    monet_iter = iter(monet_train_ds)\n\n    for epoch in range(1, epochs + 1):\n        epoch_losses = []\n        for _ in range(steps_per_epoch):\n            photo_batch = next(photo_iter)\n            monet_batch = next(monet_iter)\n            losses = train_step(photo_batch, monet_batch)\n            epoch_losses.append({k: float(v.numpy()) for k, v in losses.items()})\n\n        mean_losses = {k: float(np.mean([x[k] for x in epoch_losses]))\n                       for k in epoch_losses[0]}\n        print(f\"Epoch {epoch:03d}/{epochs:03d} | \" +\n              \" | \".join(f\"{k}: {v:.4f}\" for k, v in mean_losses.items()))\n\n        if epoch % sample_every == 0 or epoch == 1:\n            photo_preview = next(iter(photo_eval_ds.take(1)))\n            monet_preview = next(iter(monet_eval_ds.take(1)))\n\n            ema_gen_G.apply(); ema_gen_F.apply()\n\n            generate_and_save_images(ema_gen_G.model, epoch, photo_preview,\n                                     GEN_DIR, \"photo_to_monet\")\n            generate_and_save_images(ema_gen_F.model, epoch, monet_preview,\n                                     GEN_DIR, \"monet_to_photo\")\n\n            ema_gen_G.restore(); ema_gen_F.restore()\n\n            ckpt = os.path.join(CKPT_DIR, f\"epoch_{epoch:03d}\")\n            gen_G.save_weights(ckpt + \"_gen_G.weights.h5\")\n            gen_F.save_weights(ckpt + \"_gen_F.weights.h5\")\n            disc_X.save_weights(ckpt + \"_disc_X.weights.h5\")\n            disc_Y.save_weights(ckpt + \"_disc_Y.weights.h5\")\n\n        gc.collect()\n\nprint(\"Training utilities ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.650580Z","iopub.execute_input":"2026-06-06T04:15:38.651032Z","iopub.status.idle":"2026-06-06T04:15:38.680682Z","shell.execute_reply.started":"2026-06-06T04:15:38.650988Z","shell.execute_reply":"2026-06-06T04:15:38.679632Z"}},"outputs":[],"execution_count":null},{"id":"608e7839-0b10-495e-a80d-811092805a11","cell_type":"code","source":"# loss tracker & visualization-enhanced trainer\nhistory = {\"gen_G_loss\": [], \"gen_F_loss\": [], \"disc_X_loss\": [], \"disc_Y_loss\": []}\n\ndef plot_training_history(hist, save=True):\n    if not any(hist.values()):\n        print(\"No history yet — run train_with_viz() first.\"); return\n\n    eps = list(range(1, len(hist[\"gen_G_loss\"]) + 1))\n    fig, axes = plt.subplots(2, 2, figsize=(15, 10), facecolor='#0d0d0d')\n    fig.suptitle(\"CycleGAN — Training Loss Curves\", fontsize=16,\n                 color='white', fontweight='bold')\n\n    cfgs = [\n        (\"gen_G_loss\",  \"Generator G Loss  (Photo→Monet)\", '#3a86ff', axes[0,0]),\n        (\"gen_F_loss\",  \"Generator F Loss  (Monet→Photo)\", '#ff6b6b', axes[0,1]),\n        (\"disc_X_loss\", \"Discriminator X Loss\",            '#44ff99', axes[1,0]),\n        (\"disc_Y_loss\", \"Discriminator Y Loss\",            '#ffd166', axes[1,1]),\n    ]\n    for key, title, color, ax in cfgs:\n        vals = hist[key]\n        ax.set_facecolor('#1a1a1a')\n        ax.plot(eps, vals, color=color, lw=1.8, alpha=0.9, label=\"Epoch loss\", zorder=3)\n        ax.fill_between(eps, vals, alpha=0.12, color=color, zorder=2)\n        if len(vals) >= 7:\n            w = 7\n            roll = np.convolve(vals, np.ones(w)/w, mode='valid')\n            ax.plot(range(w//2+1, w//2+1+len(roll)), roll, color='white',\n                    lw=1.8, ls='--', alpha=0.65, label=f\"{w}-ep avg\", zorder=4)\n        ax.set_title(title, color=color, fontsize=10.5, fontweight='bold')\n        ax.set_xlabel(\"Epoch\", color='#888888', fontsize=9)\n        ax.set_ylabel(\"Loss\",  color='#888888', fontsize=9)\n        ax.tick_params(colors='#888888')\n        ax.grid(True, alpha=0.1, color='#ffffff')\n        ax.legend(fontsize=8, facecolor='#2a2a2a', labelcolor='white')\n        for sp in ax.spines.values():\n            sp.set_edgecolor('#333333')\n\n    plt.tight_layout()\n    if save:\n        hist_path = os.path.join(SAMPLE_DIR, \"training_history.png\")\n        plt.savefig(hist_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n        print(f\"Saved → {hist_path}\")\n    plt.show()\n\n\ndef train_with_viz(epochs=TOTAL_EPOCHS, steps_per_epoch=steps_per_epoch, sample_every=10):\n    global history\n    photo_iter = iter(photo_train_ds)\n    monet_iter = iter(monet_train_ds)\n\n    for epoch in range(1, epochs + 1):\n        ep_losses = []\n        for _ in range(steps_per_epoch):\n            photo_batch = next(photo_iter)\n            monet_batch = next(monet_iter)\n            losses = train_step(photo_batch, monet_batch)\n            ep_losses.append({k: float(v.numpy()) for k, v in losses.items()})\n\n        mean_losses = {k: float(np.mean([x[k] for x in ep_losses]))\n                       for k in ep_losses[0]}\n        for k, v in mean_losses.items():\n            history[k].append(v)\n\n        print(f\"Epoch {epoch:03d}/{epochs:03d} | \" +\n              \" | \".join(f\"{k}: {v:.4f}\" for k, v in mean_losses.items()))\n\n        if epoch % sample_every == 0 or epoch == 1:\n            photo_prev = next(iter(photo_eval_ds.take(1)))\n            monet_prev = next(iter(monet_eval_ds.take(1)))\n\n            ema_gen_G.apply(); ema_gen_F.apply()\n\n            generate_and_save_images(ema_gen_G.model, epoch, photo_prev, GEN_DIR, \"photo_to_monet\")\n            generate_and_save_images(ema_gen_F.model, epoch, monet_prev, GEN_DIR, \"monet_to_photo\")\n            visualize_cycle(ema_gen_G.model, ema_gen_F.model, photo_prev, monet_prev, epoch)\n\n            ema_gen_G.restore(); ema_gen_F.restore()\n\n            ckpt = os.path.join(CKPT_DIR, f\"epoch_{epoch:03d}\")\n            gen_G.save_weights(ckpt + \"_gen_G.weights.h5\")\n            gen_F.save_weights(ckpt + \"_gen_F.weights.h5\")\n            disc_X.save_weights(ckpt + \"_disc_X.weights.h5\")\n            disc_Y.save_weights(ckpt + \"_disc_Y.weights.h5\")\n\n        gc.collect()\n\n    plot_training_history(history)\n    print(\"Training complete — loss curves saved.\")\n\nprint(\"Loss tracker + train_with_viz ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.681718Z","iopub.execute_input":"2026-06-06T04:15:38.681992Z","iopub.status.idle":"2026-06-06T04:15:38.710362Z","shell.execute_reply.started":"2026-06-06T04:15:38.681968Z","shell.execute_reply":"2026-06-06T04:15:38.709536Z"}},"outputs":[],"execution_count":null},{"id":"fae763fd","cell_type":"code","source":"def make_submission_zip_from_dataset(model, dataset, zip_path=\"/kaggle/working/images.zip\", limit=None):\n    with zipfile.ZipFile(zip_path, \"w\", compression=zipfile.ZIP_DEFLATED) as zf:\n        count = 0\n        for batch in dataset:\n            for img in batch:\n                pred = model(tf.expand_dims(img, 0), training=False)[0]\n                out = tf.clip_by_value((pred + 1.0) * 127.5, 0.0, 255.0)\n                out = tf.cast(out, tf.uint8).numpy()\n\n                buffer = io.BytesIO()\n                Image.fromarray(out).save(buffer, format=\"JPEG\", quality=95)\n                zf.writestr(f\"{count:05d}.jpg\", buffer.getvalue())\n                count += 1\n                if limit is not None and count >= limit:\n                    return zip_path\n    return zip_path\n\ndef export_images_zip():\n    if MONET_JPEGS:\n        source_ds = photo_eval_ds\n    else:\n        source_ds = photo_eval_ds\n\n    ema_gen_G.apply()\n    zip_path = make_submission_zip_from_dataset(ema_gen_G.model, source_ds, \"/kaggle/working/images.zip\")\n    ema_gen_G.restore()\n\n    print(\"Saved:\", zip_path)\n    return zip_path\n\nprint(\"Export helper ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.711291Z","iopub.execute_input":"2026-06-06T04:15:38.711517Z","iopub.status.idle":"2026-06-06T04:15:38.735462Z","shell.execute_reply.started":"2026-06-06T04:15:38.711496Z","shell.execute_reply":"2026-06-06T04:15:38.734728Z"}},"outputs":[],"execution_count":null},{"id":"5b6cf2b9-3272-445f-b2fb-bf1d6fd19864","cell_type":"code","source":"# color distribution analysis\ndef analyze_color_distribution(gen_G_model, photo_ds, monet_ds, n_batches=4):\n    photos, monets, fakes = [], [], []\n    for i, (pb, mb) in enumerate(zip(photo_ds, monet_ds)):\n        if i >= n_batches: break\n        fb = gen_G_model(pb, training=False)\n        photos.append((pb * 0.5 + 0.5).numpy().reshape(-1, 3))\n        monets.append((mb * 0.5 + 0.5).numpy().reshape(-1, 3))\n        fakes.append( (fb * 0.5 + 0.5).numpy().reshape(-1, 3))\n    photos = np.clip(np.concatenate(photos), 0, 1)\n    monets = np.clip(np.concatenate(monets), 0, 1)\n    fakes  = np.clip(np.concatenate(fakes),  0, 1)\n\n    fig, axes = plt.subplots(1, 3, figsize=(17, 5.5), facecolor='#0d0d0d')\n    fig.suptitle(\"Pixel Color Distribution — Real Photo vs. Real Monet vs. Generated Monet\",\n                 fontsize=13, color='white', fontweight='bold')\n\n    ch_names  = ['Red Channel', 'Green Channel', 'Blue Channel']\n    ch_colors = ['#ff4444', '#44cc44', '#4488ff']\n    datasets  = [\n        (\"Real Photo\",      photos, '#3a86ff'),\n        (\"Real Monet\",      monets, '#ff6b6b'),\n        (\"Fake Monet G(x)\", fakes,  '#ffd166'),\n    ]\n    for ax, ch_idx, ch_name, ch_c in zip(axes, range(3), ch_names, ch_colors):\n        ax.set_facecolor('#1a1a1a')\n        for lbl, data, lc in datasets:\n            ax.hist(data[:, ch_idx], bins=64, density=True,\n                    alpha=0.35, color=lc, histtype='stepfilled')\n            ax.hist(data[:, ch_idx], bins=64, density=True,\n                    alpha=0.90, color=lc, histtype='step', lw=1.8, label=lbl)\n        ax.set_title(ch_name, color=ch_c, fontsize=11, fontweight='bold')\n        ax.set_xlabel(\"Pixel Intensity\", color='#888888', fontsize=9)\n        ax.set_ylabel(\"Density\",         color='#888888', fontsize=9)\n        ax.tick_params(colors='#888888')\n        ax.legend(fontsize=8, facecolor='#2a2a2a', labelcolor='white')\n        ax.grid(True, alpha=0.10)\n        ax.set_xlim(0, 1)\n        for sp in ax.spines.values():\n            sp.set_edgecolor('#333333')\n\n    plt.tight_layout()\n    dist_path = os.path.join(SAMPLE_DIR, \"color_distribution.png\")\n    plt.savefig(dist_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n    plt.show()\n    print(f\"Saved → {dist_path}\")\n\nprint(\"Color distribution analysis ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.736513Z","iopub.execute_input":"2026-06-06T04:15:38.736806Z","iopub.status.idle":"2026-06-06T04:15:38.759552Z","shell.execute_reply.started":"2026-06-06T04:15:38.736787Z","shell.execute_reply":"2026-06-06T04:15:38.758160Z"}},"outputs":[],"execution_count":null},{"id":"6640999d-63f6-4153-a6c6-8773d783a763","cell_type":"code","source":"# generated gallery viewer\ndef display_generated_gallery(gen_G_model, photo_ds, n=16, cols=4):\n    rows = (n + cols - 1) // cols\n    fig, axes = plt.subplots(rows * 2, cols, figsize=(cols * 4, rows * 4.6),\n                             facecolor='#0d0d0d')\n    fig.suptitle(f\"Generated Monet Gallery  —  Top: Real Photo  |  Bottom: Fake Monet\",\n                 fontsize=14, color='white', fontweight='bold', y=1.01)\n\n    count = 0\n    for batch in photo_ds:\n        fakes = gen_G_model(batch, training=False)\n        for i in range(batch.shape[0]):\n            if count >= n: break\n            r = (count // cols) * 2\n            c = count  % cols\n            photo_img = (batch[i] * 0.5 + 0.5).numpy().clip(0, 1)\n            fake_img  = (fakes[i] * 0.5 + 0.5).numpy().clip(0, 1)\n\n            axes[r,   c].imshow(photo_img); axes[r,   c].axis('off')\n            axes[r+1, c].imshow(fake_img);  axes[r+1, c].axis('off')\n\n            axes[r,   c].set_title(f\"Photo #{count+1}\", color='#3a86ff', fontsize=8, pad=3)\n            axes[r+1, c].set_title(f\"Monet #{count+1}\", color='#ff6b6b', fontsize=8, pad=3)\n\n            for sp in axes[r,   c].spines.values():\n                sp.set_edgecolor('#3a86ff'); sp.set_linewidth(1.5); sp.set_visible(True)\n            for sp in axes[r+1, c].spines.values():\n                sp.set_edgecolor('#ff6b6b'); sp.set_linewidth(1.5); sp.set_visible(True)\n            count += 1\n        if count >= n: break\n\n    for ax in axes.flat:\n        if not ax.has_data(): ax.axis('off')\n\n    plt.tight_layout()\n    gal_path = os.path.join(SAMPLE_DIR, \"generated_gallery.png\")\n    plt.savefig(gal_path, bbox_inches='tight', dpi=150, facecolor='#0d0d0d')\n    plt.show()\n    print(f\"Saved → {gal_path}\")\n\nprint(\"Gallery viewer ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.760845Z","iopub.execute_input":"2026-06-06T04:15:38.761123Z","iopub.status.idle":"2026-06-06T04:15:38.790473Z","shell.execute_reply.started":"2026-06-06T04:15:38.761100Z","shell.execute_reply":"2026-06-06T04:15:38.789218Z"}},"outputs":[],"execution_count":null},{"id":"8047ae2c","cell_type":"code","source":"print(\"Preview batch shapes:\")\nprint(\"Photo:\", next(iter(photo_train_ds)).shape)\nprint(\"Monet:\", next(iter(monet_train_ds)).shape)\n\n# Train with your chosen schedule\n# train(epochs=10, sample_every=5)\n\n# After training, create the submission zip\n# export_images_zip()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:38.791708Z","iopub.execute_input":"2026-06-06T04:15:38.792080Z","iopub.status.idle":"2026-06-06T04:15:39.087783Z","shell.execute_reply.started":"2026-06-06T04:15:38.792053Z","shell.execute_reply":"2026-06-06T04:15:39.087099Z"}},"outputs":[],"execution_count":null},{"id":"61fbe68e","cell_type":"code","source":"print(\"Notebook is ready.\")\nprint(\"Dataset root used:\", DATA_ROOT)\nprint(\"Output zip should be /kaggle/working/images.zip\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T04:15:39.090236Z","iopub.execute_input":"2026-06-06T04:15:39.090450Z","iopub.status.idle":"2026-06-06T04:15:39.094846Z","shell.execute_reply.started":"2026-06-06T04:15:39.090432Z","shell.execute_reply":"2026-06-06T04:15:39.093875Z"}},"outputs":[],"execution_count":null}]}