{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# Monet CycleGAN V3: Optimized for MiFID Score [TPU]\n\n---\n\n> **Credits & Acknowledgments**\n> \n> This notebook builds upon the excellent work of:\n> - **Amy Jang** - [Monet CycleGAN Tutorial](https://www.kaggle.com/amyjang/monet-cyclegan-tutorial)\n> - **DimitreOliveira** - [Improving CycleGAN](https://www.kaggle.com/dimitreoliveira/improving-cyclegan-monet-paintings)\n\n---\n\n## V3 Improvements for Better MiFID Score\n\n| Technique | Impact | Risk Level |\n|-----------|--------|------------|\n| **Random Horizontal Flip** | +Diversity, -Overfitting | Very Low |\n| **One-sided Label Smoothing** | +Training Stability | Very Low |\n| **Discriminator Noise** | +Robustness | Low |\n| **More Epochs (30)** | +Quality | None |\n| **More Images (10,000)** | +MiFID Score | None |\n\n---\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## 1. Configuration & Score Optimization Parameters\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# IMPORTS & CONFIGURATION\n# =============================================================================\nimport os\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport matplotlib.pyplot as plt\nfrom matplotlib.patches import FancyBboxPatch\nimport matplotlib.patches as mpatches\nfrom PIL import Image\nimport shutil\n\nprint(f\"TensorFlow version: {tf.__version__}\")\n\n# -----------------------------------------------------------------------------\n# HYPERPARAMETERS - V3 OPTIMIZED\n# -----------------------------------------------------------------------------\nDRY_RUN = False  # False = Full submission mode\n\nif DRY_RUN:\n    EPOCHS = 1\n    BATCH_SIZE = 1\n    MAX_IMAGES = 100\nelse:\n    EPOCHS = 30              # V3: Increased from 25 for better convergence\n    BATCH_SIZE = 1           # CycleGAN standard\n    MAX_IMAGES = 10000       # V3: Generate MORE images for better MiFID\n\n# Model parameters\nIMG_HEIGHT = 256\nIMG_WIDTH = 256\nOUTPUT_CHANNELS = 3\n\n# Loss weights\nLAMBDA_CYCLE = 10\nLAMBDA_IDENTITY = 5\n\n# V3 NEW: Score optimization parameters\nLABEL_SMOOTHING = 0.1        # One-sided label smoothing (0.9 instead of 1.0)\nDISC_NOISE_STD = 0.05        # Small noise to discriminator inputs\nUSE_DATA_AUGMENTATION = True # Random horizontal flip\n\n# Optimizer\nLEARNING_RATE = 2e-4\nBETA_1 = 0.5\n\n# Paths\nif os.path.exists('/kaggle/input/gan-getting-started'):\n    DATA_DIR = '/kaggle/input/gan-getting-started'\n    OUTPUT_DIR = '/kaggle/working'\nelse:\n    DATA_DIR = '.'\n    OUTPUT_DIR = '.'\n\nprint(f\"[CONFIG] Mode: {'DRY RUN' if DRY_RUN else 'FULL SUBMISSION'}\")\nprint(f\"[CONFIG] Epochs: {EPOCHS}\")\nprint(f\"[CONFIG] Max Images: {MAX_IMAGES}\")\nprint(f\"[V3] Label Smoothing: {LABEL_SMOOTHING}\")\nprint(f\"[V3] Augmentation: {USE_DATA_AUGMENTATION}\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "### V3 Score Optimization Techniques Explained\n\nBelow we visualize why each technique improves the MiFID score:\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# V3 IMPROVEMENTS VISUALIZATION\n# =============================================================================\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\nfig.suptitle('V3 Score Optimization Techniques', fontsize=16, fontweight='bold')\n\n# 1. Data Augmentation Effect\nax1 = axes[0, 0]\nax1.set_title('1. Random Horizontal Flip', fontsize=12, fontweight='bold')\ncategories = ['Without Flip', 'With Flip']\neffective_samples = [300, 600]  # Doubles effective training data\nbars = ax1.bar(categories, effective_samples, color=['#e74c3c', '#27ae60'])\nax1.set_ylabel('Effective Training Samples')\nax1.set_ylim(0, 700)\nfor bar, val in zip(bars, effective_samples):\n    ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 20, \n             f'{val}', ha='center', fontweight='bold', fontsize=12)\nax1.annotate('2x Data!', xy=(1, 600), fontsize=14, color='green',\n             fontweight='bold', ha='center')\n\n# 2. Label Smoothing Effect\nax2 = axes[0, 1]\nax2.set_title('2. One-sided Label Smoothing', fontsize=12, fontweight='bold')\nx = np.linspace(0, 1, 100)\n# Hard labels vs soft labels\nax2.axvline(x=1.0, color='red', linestyle='--', linewidth=2, label='Hard Label (1.0)')\nax2.axvline(x=0.9, color='green', linestyle='-', linewidth=3, label='Soft Label (0.9)')\nax2.fill_betweenx([0, 1], 0.85, 0.95, alpha=0.3, color='green', label='Target Range')\nax2.set_xlim(0.7, 1.1)\nax2.set_ylim(0, 1)\nax2.set_xlabel('Discriminator Target for Real Images')\nax2.legend(loc='upper left')\nax2.text(0.9, 0.5, 'Prevents\\nOverconfidence', fontsize=10, ha='center',\n         bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))\n\n# 3. Discriminator Noise Effect\nax3 = axes[1, 0]\nax3.set_title('3. Discriminator Input Noise', fontsize=12, fontweight='bold')\n# Show noise distribution\nnoise = np.random.normal(0, DISC_NOISE_STD, 1000)\nax3.hist(noise, bins=50, color='#3498db', alpha=0.7, edgecolor='black')\nax3.axvline(x=0, color='red', linestyle='--', linewidth=2)\nax3.set_xlabel('Noise Value Added to Pixel')\nax3.set_ylabel('Frequency')\nax3.annotate(f'std={DISC_NOISE_STD}\\n(Very small!)', xy=(0.05, 100), fontsize=10,\n             bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))\nax3.set_xlim(-0.2, 0.2)\n\n# 4. Training & Generation Increase\nax4 = axes[1, 1]\nax4.set_title('4. Training & Generation Volume', fontsize=12, fontweight='bold')\nversions = ['V2', 'V3']\nepochs_vals = [25, 30]\nimages_vals = [7000, 10000]\n\nx_pos = np.arange(len(versions))\nwidth = 0.35\n\nbars1 = ax4.bar(x_pos - width/2, epochs_vals, width, label='Epochs', color='#9b59b6')\nax4_twin = ax4.twinx()\nbars2 = ax4_twin.bar(x_pos + width/2, images_vals, width, label='Images', color='#f39c12')\n\nax4.set_ylabel('Epochs', color='#9b59b6')\nax4_twin.set_ylabel('Generated Images', color='#f39c12')\nax4.set_xticks(x_pos)\nax4.set_xticklabels(versions)\nax4.set_ylim(0, 40)\nax4_twin.set_ylim(0, 12000)\n\n# Add value labels\nfor bar, val in zip(bars1, epochs_vals):\n    ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, \n             str(val), ha='center', fontweight='bold')\nfor bar, val in zip(bars2, images_vals):\n    ax4_twin.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 300, \n                  f'{val:,}', ha='center', fontweight='bold')\n\nax4.annotate('+20% Epochs\\n+43% Images', xy=(1, 35), fontsize=10, color='green',\n             fontweight='bold', ha='center',\n             bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/v3_improvements.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] v3_improvements.png\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "### Why These Techniques Improve MiFID Score\n\n> **[Deep Learning Engineer's Analysis]**\n\n| Technique | Problem Solved | How It Helps MiFID |\n|-----------|----------------|-------------------|\n| **Random Flip** | Limited Monet data (~300) | Doubles effective training samples, reducing overfitting |\n| **Label Smoothing** | Discriminator becomes too confident | Prevents discriminator from giving extreme predictions, better gradients for generator |\n| **Disc. Noise** | Generator mode collapse | Forces discriminator to learn robust features, not memorize |\n| **More Epochs** | Underfitting | Better convergence of style transfer |\n| **More Images** | MiFID variance | Larger sample = more stable FID estimation |\n\n#### Key Insight: The MiFID Metric\n\nMiFID (Memorization-informed FID) penalizes:\n1. **Low quality images** - Poor style transfer\n2. **Memorized images** - Copying training data too closely\n3. **Low diversity** - Generating similar images\n\nOur V3 improvements address all three:\n- Augmentation -> More diversity\n- Label smoothing -> Better quality\n- More images -> Stable MiFID estimation\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# TPU SETUP\n# =============================================================================\ntry:\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    DEVICE_TYPE = \"TPU\"\n    print(f\"[OK] TPU connected with {strategy.num_replicas_in_sync} replicas\")\nexcept Exception as e:\n    strategy = tf.distribute.get_strategy()\n    DEVICE_TYPE = \"GPU\" if tf.config.list_physical_devices('GPU') else \"CPU\"\n    print(f\"[INFO] Using {DEVICE_TYPE}\")\n\nGLOBAL_BATCH_SIZE = BATCH_SIZE * strategy.num_replicas_in_sync\nprint(f\"[CONFIG] Global batch size: {GLOBAL_BATCH_SIZE}\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "### Hyperparameter Impact Visualization\n\nUnderstanding each parameter is crucial for GAN performance:\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# VISUALIZE HYPERPARAMETERS (V2 + V3)\n# =============================================================================\nfig, axes = plt.subplots(1, 3, figsize=(15, 4))\nfig.suptitle('CycleGAN Hyperparameter Guide', fontsize=14, fontweight='bold')\n\n# Chart 1: Loss Weights Balance\nax1 = axes[0]\nweights = ['Adversarial', 'Cycle (x10)', 'Identity (x5)']\nvalues = [1, LAMBDA_CYCLE, LAMBDA_IDENTITY]\ncolors = ['#e74c3c', '#3498db', '#2ecc71']\nbars = ax1.bar(weights, values, color=colors, edgecolor='black', linewidth=1.5)\nax1.set_ylabel('Weight Multiplier')\nax1.set_title('Loss Function Weights')\nax1.set_ylim(0, 12)\nfor bar, val in zip(bars, values):\n    ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, \n             f'{val}x', ha='center', fontweight='bold')\nax1.annotate('Preserves\\nContent', xy=(1, 10), fontsize=9, ha='center',\n             bbox=dict(boxstyle='round', facecolor='#3498db', alpha=0.3))\n\n# Chart 2: Training Configuration\nax2 = axes[1]\nparams = ['Epochs', 'Batch\\nSize', 'Image\\nSize']\nparam_values = [EPOCHS, BATCH_SIZE, IMG_HEIGHT]\nax2.barh(params, param_values, color=['#9b59b6', '#f39c12', '#1abc9c'])\nax2.set_xlabel('Value')\nax2.set_title('Training Configuration')\nfor i, v in enumerate(param_values):\n    ax2.text(v + 1, i, str(v), va='center', fontweight='bold')\n\n# Chart 3: V3 Optimizer Settings\nax3 = axes[2]\nopt_info = f'''\nOptimizer: Adam\n\nLearning Rate: {LEARNING_RATE}\nBeta_1: {BETA_1}\n\nV3 Enhancements:\n- Label Smoothing: {LABEL_SMOOTHING}\n- Disc Noise: {DISC_NOISE_STD}\n- Augmentation: ON\n'''\nax3.text(0.1, 0.5, opt_info, transform=ax3.transAxes, fontsize=10,\n         verticalalignment='center', fontfamily='monospace',\n         bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))\nax3.axis('off')\nax3.set_title('Optimizer & V3 Settings')\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/hyperparameters.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] hyperparameters.png\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## Art History: Monet's Impressionist Style\n\n> **[Art Historian's Analysis]**\n\nClaude Monet (1840-1926) pioneered Impressionism with distinctive techniques:\n\n| Technique | Description | CycleGAN Learning |\n|-----------|-------------|-------------------|\n| **Broken Color** | Small, distinct brushstrokes | Texture patterns |\n| **Light Effects** | Emphasis on natural light | Color distribution |\n| **Soft Edges** | Blurred boundaries | Low-frequency features |\n| **Color Harmony** | Blues, greens, soft pinks | Style consistency |\n\nThe generator must learn to:\n1. **Add brushstroke texture** - Transform smooth pixels to painterly strokes\n2. **Shift color palette** - Move toward Monet's characteristic hues\n3. **Soften edges** - Create atmospheric effects\n4. **Preserve composition** - Keep the \"what\" while changing the \"how\"\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## CycleGAN Architecture Visualization\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# ARCHITECTURE DIAGRAM\n# =============================================================================\nfrom matplotlib.patches import FancyArrowPatch\n\nfig, ax = plt.subplots(figsize=(16, 10))\nax.set_xlim(0, 16)\nax.set_ylim(0, 10)\nax.axis('off')\nax.set_title('CycleGAN Architecture: Photo <-> Monet', fontsize=16, fontweight='bold', pad=20)\n\ndef draw_box(ax, x, y, w, h, text, color, fontsize=9):\n    box = FancyBboxPatch((x, y), w, h, boxstyle=\"round,pad=0.05\",\n                          facecolor=color, edgecolor='black', linewidth=2)\n    ax.add_patch(box)\n    ax.text(x + w/2, y + h/2, text, ha='center', va='center', \n            fontsize=fontsize, fontweight='bold', wrap=True)\n\n# Draw domains\ndraw_box(ax, 0.5, 7, 2.5, 2, 'Photo\\nDomain\\n(7000+ images)', '#AED6F1', 10)\ndraw_box(ax, 13, 7, 2.5, 2, 'Monet\\nDomain\\n(~300 paintings)', '#F9E79F', 10)\n\n# Generators and Discriminators\ndraw_box(ax, 5, 7.5, 2.5, 1.5, 'Generator G\\nPhoto->Monet', '#82E0AA', 9)\ndraw_box(ax, 8.5, 7.5, 2.5, 1.5, 'Generator F\\nMonet->Photo', '#F5B7B1', 9)\ndraw_box(ax, 5, 4, 2.5, 1.5, 'Discriminator Y\\nReal Monet?', '#D7BDE2', 9)\ndraw_box(ax, 8.5, 4, 2.5, 1.5, 'Discriminator X\\nReal Photo?', '#D5F5E3', 9)\ndraw_box(ax, 6.5, 1, 3, 1.5, 'Cycle Consistency Loss\\n|F(G(x)) - x| + |G(F(y)) - y|', '#FADBD8', 9)\n\n# Arrows\nax.annotate('', xy=(5, 8.25), xytext=(3, 8),\n            arrowprops=dict(arrowstyle='->', color='#2980B9', lw=2))\nax.annotate('', xy=(13, 8.25), xytext=(7.5, 8.25),\n            arrowprops=dict(arrowstyle='->', color='#27AE60', lw=2))\nax.annotate('', xy=(6.25, 5.5), xytext=(6.25, 7.5),\n            arrowprops=dict(arrowstyle='->', color='#27AE60', lw=2))\n\n# Legend\nlegend_elements = [\n    mpatches.Patch(facecolor='#82E0AA', edgecolor='black', label='Generator (U-Net)'),\n    mpatches.Patch(facecolor='#D7BDE2', edgecolor='black', label='Discriminator (PatchGAN)'),\n    mpatches.Patch(facecolor='#FADBD8', edgecolor='black', label='Loss Functions'),\n]\nax.legend(handles=legend_elements, loc='upper left', fontsize=10)\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/architecture.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] architecture.png\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## 2. Data Loading with Augmentation\n\n> **V3 Enhancement**: Random horizontal flip doubles effective training data!\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# DATA LOADING WITH V3 AUGMENTATION\n# =============================================================================\ndef decode_image(image):\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = tf.cast(image, tf.float32) / 127.5 - 1\n    image = tf.image.resize(image, [IMG_HEIGHT, IMG_WIDTH])\n    return image\n\ndef augment_image(image):\n    '''\n    V3: Random horizontal flip augmentation.\n\n    Why this works:\n    - Monet paintings have no inherent left/right bias\n    - Flipping creates valid new training samples\n    - Effectively doubles our limited Monet dataset\n    '''\n    if USE_DATA_AUGMENTATION:\n        image = tf.image.random_flip_left_right(image)\n    return image\n\ndef read_tfrecord(example):\n    features = tf.io.parse_single_example(\n        example, features={'image': tf.io.FixedLenFeature([], tf.string)})\n    return decode_image(features['image'])\n\ndef load_dataset(filenames, augment=False):\n    dataset = tf.data.TFRecordDataset(filenames)\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=tf.data.AUTOTUNE)\n    if augment:\n        dataset = dataset.map(augment_image, num_parallel_calls=tf.data.AUTOTUNE)\n    return dataset\n\n# Load file paths\nMONET_FILES = tf.io.gfile.glob(f'{DATA_DIR}/monet_tfrec/*.tfrec')\nPHOTO_FILES = tf.io.gfile.glob(f'{DATA_DIR}/photo_tfrec/*.tfrec')\n\nprint(f\"[DATA] Monet: {len(MONET_FILES)} TFRecords\")\nprint(f\"[DATA] Photo: {len(PHOTO_FILES)} TFRecords\")\n\n# Count images\nmonet_count = sum(1 for _ in load_dataset(MONET_FILES))\nphoto_count = sum(1 for _ in load_dataset(PHOTO_FILES))\nprint(f\"[DATA] Monet images: {monet_count}\")\nprint(f\"[DATA] Photo images: {photo_count}\")\nprint(f\"[V3] With augmentation: ~{monet_count * 2} effective Monet samples\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# VISUALIZE AUGMENTATION EFFECT\n# =============================================================================\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\nfig.suptitle('V3: Data Augmentation (Random Horizontal Flip)', fontsize=14, fontweight='bold')\n\n# Get sample Monet images\nsample_monets = list(load_dataset(MONET_FILES).take(4))\n\nfor i in range(4):\n    original = (sample_monets[i].numpy() + 1) / 2\n    flipped = np.fliplr(original)\n\n    axes[0, i].imshow(original)\n    axes[0, i].set_title('Original' if i == 0 else '')\n    axes[0, i].axis('off')\n\n    axes[1, i].imshow(flipped)\n    axes[1, i].set_title('Flipped (New Sample!)' if i == 0 else '')\n    axes[1, i].axis('off')\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/augmentation_demo.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] augmentation_demo.png\")\nprint(\"[INSIGHT] Each Monet painting becomes TWO training samples!\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# VISUAL EDA: Side-by-Side Comparison (from V2)\n# =============================================================================\nfig, axes = plt.subplots(2, 6, figsize=(18, 6))\nfig.suptitle('Dataset Comparison: Photos vs Monet Paintings', fontsize=14, fontweight='bold')\n\nmonet_samples = list(load_dataset(MONET_FILES).take(6))\nphoto_samples = list(load_dataset(PHOTO_FILES).take(6))\n\nfor i in range(6):\n    axes[0, i].imshow((photo_samples[i].numpy() + 1) / 2)\n    axes[0, i].axis('off')\n    if i == 0:\n        axes[0, i].set_title('Photos (Input)', fontsize=12, fontweight='bold')\n\n    axes[1, i].imshow((monet_samples[i].numpy() + 1) / 2)\n    axes[1, i].axis('off')\n    if i == 0:\n        axes[1, i].set_title('Monet (Target Style)', fontsize=12, fontweight='bold')\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/data_comparison.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] data_comparison.png\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# COLOR DISTRIBUTION ANALYSIS (from V2)\n# =============================================================================\nfig, axes = plt.subplots(1, 2, figsize=(14, 4))\nfig.suptitle('Color Distribution: Photos vs Monet', fontsize=14, fontweight='bold')\n\nphoto_colors = {'R': [], 'G': [], 'B': []}\nmonet_colors = {'R': [], 'G': [], 'B': []}\n\nfor img in list(load_dataset(PHOTO_FILES).take(50)):\n    img_np = (img.numpy() + 1) / 2 * 255\n    photo_colors['R'].extend(img_np[:,:,0].flatten())\n    photo_colors['G'].extend(img_np[:,:,1].flatten())\n    photo_colors['B'].extend(img_np[:,:,2].flatten())\n\nfor img in list(load_dataset(MONET_FILES).take(50)):\n    img_np = (img.numpy() + 1) / 2 * 255\n    monet_colors['R'].extend(img_np[:,:,0].flatten())\n    monet_colors['G'].extend(img_np[:,:,1].flatten())\n    monet_colors['B'].extend(img_np[:,:,2].flatten())\n\naxes[0].hist(photo_colors['R'], bins=50, alpha=0.5, color='red', label='Red')\naxes[0].hist(photo_colors['G'], bins=50, alpha=0.5, color='green', label='Green')\naxes[0].hist(photo_colors['B'], bins=50, alpha=0.5, color='blue', label='Blue')\naxes[0].set_title('Photos: Color Distribution')\naxes[0].set_xlabel('Pixel Value')\naxes[0].set_ylabel('Frequency')\naxes[0].legend()\n\naxes[1].hist(monet_colors['R'], bins=50, alpha=0.5, color='red', label='Red')\naxes[1].hist(monet_colors['G'], bins=50, alpha=0.5, color='green', label='Green')\naxes[1].hist(monet_colors['B'], bins=50, alpha=0.5, color='blue', label='Blue')\naxes[1].set_title('Monet: Color Distribution')\naxes[1].set_xlabel('Pixel Value')\naxes[1].set_ylabel('Frequency')\naxes[1].legend()\naxes[1].annotate('Monet shows\\nmore Blue/Green', xy=(150, 2000000), fontsize=10,\n                 bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5))\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/color_distribution.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] color_distribution.png\")\nprint(\"[INSIGHT] The model needs to shift the color distribution toward Monet's palette!\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# PREPARE TRAINING DATASETS (WITH AUGMENTATION)\n# =============================================================================\n# V3: Apply augmentation to both datasets\nmonet_ds = load_dataset(MONET_FILES, augment=True).cache().shuffle(1000).batch(GLOBAL_BATCH_SIZE, drop_remainder=True).prefetch(tf.data.AUTOTUNE)\nphoto_ds = load_dataset(PHOTO_FILES, augment=True).cache().shuffle(1000).batch(GLOBAL_BATCH_SIZE, drop_remainder=True).prefetch(tf.data.AUTOTUNE)\n\nif DRY_RUN:\n    monet_ds = monet_ds.take(5)\n    photo_ds = photo_ds.take(5)\n    print(\"[DRY RUN] Limited to 5 batches\")\nelse:\n    print(\"[FULL] Using all data with augmentation\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## 3. Model Definition\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# BUILDING BLOCKS\n# =============================================================================\ndef downsample(filters, size, apply_norm=True):\n    init = tf.random_normal_initializer(0., 0.02)\n    block = keras.Sequential()\n    block.add(layers.Conv2D(filters, size, strides=2, padding='same',\n                            kernel_initializer=init, use_bias=False))\n    if apply_norm:\n        block.add(layers.GroupNormalization(groups=-1))\n    block.add(layers.LeakyReLU())\n    return block\n\ndef upsample(filters, size, apply_dropout=False):\n    init = tf.random_normal_initializer(0., 0.02)\n    block = keras.Sequential()\n    block.add(layers.Conv2DTranspose(filters, size, strides=2, padding='same',\n                                      kernel_initializer=init, use_bias=False))\n    block.add(layers.GroupNormalization(groups=-1))\n    if apply_dropout:\n        block.add(layers.Dropout(0.5))\n    block.add(layers.ReLU())\n    return block\n\ndef Generator():\n    inputs = layers.Input(shape=[IMG_HEIGHT, IMG_WIDTH, 3])\n\n    down_stack = [\n        downsample(64, 4, apply_norm=False),\n        downsample(128, 4),\n        downsample(256, 4),\n        downsample(512, 4),\n        downsample(512, 4),\n        downsample(512, 4),\n        downsample(512, 4),\n        downsample(512, 4),\n    ]\n\n    up_stack = [\n        upsample(512, 4, apply_dropout=True),\n        upsample(512, 4, apply_dropout=True),\n        upsample(512, 4, apply_dropout=True),\n        upsample(512, 4),\n        upsample(256, 4),\n        upsample(128, 4),\n        upsample(64, 4),\n    ]\n\n    init = tf.random_normal_initializer(0., 0.02)\n    last = layers.Conv2DTranspose(OUTPUT_CHANNELS, 4, strides=2, padding='same',\n                                   kernel_initializer=init, activation='tanh')\n\n    x = inputs\n    skips = []\n    for down in down_stack:\n        x = down(x)\n        skips.append(x)\n    skips = reversed(skips[:-1])\n    for up, skip in zip(up_stack, skips):\n        x = up(x)\n        x = layers.Concatenate()([x, skip])\n    x = last(x)\n    return keras.Model(inputs=inputs, outputs=x)\n\ndef Discriminator():\n    init = tf.random_normal_initializer(0., 0.02)\n    inp = layers.Input(shape=[IMG_HEIGHT, IMG_WIDTH, 3])\n\n    x = downsample(64, 4, False)(inp)\n    x = downsample(128, 4)(x)\n    x = downsample(256, 4)(x)\n    x = layers.ZeroPadding2D()(x)\n    x = layers.Conv2D(512, 4, strides=1, kernel_initializer=init, use_bias=False)(x)\n    x = layers.GroupNormalization(groups=-1)(x)\n    x = layers.LeakyReLU()(x)\n    x = layers.ZeroPadding2D()(x)\n    x = layers.Conv2D(1, 4, strides=1, kernel_initializer=init)(x)\n\n    return keras.Model(inputs=inp, outputs=x)\n\nprint(\"[MODEL] Building blocks defined\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# INSTANTIATE MODELS\n# =============================================================================\nwith strategy.scope():\n    gen_G = Generator()\n    gen_F = Generator()\n    disc_X = Discriminator()\n    disc_Y = Discriminator()\n\n    gen_G_opt = keras.optimizers.Adam(LEARNING_RATE, beta_1=BETA_1)\n    gen_F_opt = keras.optimizers.Adam(LEARNING_RATE, beta_1=BETA_1)\n    disc_X_opt = keras.optimizers.Adam(LEARNING_RATE, beta_1=BETA_1)\n    disc_Y_opt = keras.optimizers.Adam(LEARNING_RATE, beta_1=BETA_1)\n\nprint(f\"[MODEL] Generator: {gen_G.count_params():,} parameters\")\nprint(f\"[MODEL] Discriminator: {disc_X.count_params():,} parameters\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## 4. V3 Enhanced Loss Functions\n\n> **Key V3 Changes**:\n> - One-sided label smoothing: Real labels = 0.9 instead of 1.0\n> - Discriminator input noise: Small Gaussian noise for robustness\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# V3 ENHANCED LOSS FUNCTIONS\n# =============================================================================\nloss_obj = keras.losses.BinaryCrossentropy(from_logits=True, \n                                            reduction=keras.losses.Reduction.NONE)\n\ndef discriminator_loss(real, generated):\n    '''\n    V3: One-sided label smoothing.\n\n    Instead of: real_label = 1.0\n    We use:     real_label = 1.0 - LABEL_SMOOTHING = 0.9\n\n    Why this helps:\n    - Prevents discriminator from becoming too confident\n    - Provides better gradients to the generator\n    - Reduces mode collapse risk\n    '''\n    # V3: Smoothed labels for real images (0.9 instead of 1.0)\n    smoothed_real_labels = tf.ones_like(real) * (1.0 - LABEL_SMOOTHING)\n    real_loss = loss_obj(smoothed_real_labels, real)\n    fake_loss = loss_obj(tf.zeros_like(generated), generated)\n    return tf.reduce_mean(real_loss + fake_loss) * 0.5\n\ndef generator_loss(generated):\n    return tf.reduce_mean(loss_obj(tf.ones_like(generated), generated))\n\ndef cycle_loss(real, cycled):\n    return LAMBDA_CYCLE * tf.reduce_mean(tf.abs(real - cycled))\n\ndef identity_loss(real, same):\n    return LAMBDA_IDENTITY * tf.reduce_mean(tf.abs(real - same))\n\ndef add_discriminator_noise(image):\n    '''\n    V3: Add small Gaussian noise to discriminator inputs.\n\n    Why this helps:\n    - Prevents discriminator from learning trivial patterns\n    - Forces focus on semantic features, not pixel-level noise\n    - Improves generator diversity\n    '''\n    noise = tf.random.normal(shape=tf.shape(image), mean=0.0, stddev=DISC_NOISE_STD)\n    return image + noise\n\nprint(\"[LOSS] V3 Enhanced loss functions defined\")\nprint(f\"  - Label Smoothing: {LABEL_SMOOTHING} (real labels = {1.0 - LABEL_SMOOTHING})\")\nprint(f\"  - Disc Noise Std: {DISC_NOISE_STD}\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# VISUALIZE LABEL SMOOTHING EFFECT\n# =============================================================================\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\nfig.suptitle('V3: Label Smoothing Comparison', fontsize=14, fontweight='bold')\n\n# Without smoothing\nax1 = axes[0]\nax1.set_title('Without Smoothing (V2)')\nax1.bar(['Fake', 'Real'], [0.0, 1.0], color=['#e74c3c', '#27ae60'], edgecolor='black')\nax1.set_ylim(0, 1.2)\nax1.set_ylabel('Target Label')\nax1.axhline(y=1.0, color='gray', linestyle='--', alpha=0.5)\nax1.annotate('Discriminator\\nbecomes\\noverconfident!', xy=(1, 1.05), fontsize=10,\n             ha='center', color='red')\n\n# With smoothing\nax2 = axes[1]\nax2.set_title('With Smoothing (V3)')\nax2.bar(['Fake', 'Real'], [0.0, 0.9], color=['#e74c3c', '#27ae60'], edgecolor='black')\nax2.set_ylim(0, 1.2)\nax2.set_ylabel('Target Label')\nax2.axhline(y=0.9, color='green', linestyle='--', alpha=0.5)\nax2.annotate('\"Almost real\\nis good enough\"\\n-> Better gradients!', xy=(1, 0.95), fontsize=10,\n             ha='center', color='green')\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/label_smoothing.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] label_smoothing.png\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## 5. Training with V3 Enhancements\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# V3 TRAINING STEP WITH NOISE INJECTION\n# =============================================================================\n@tf.function\ndef train_step(real_x, real_y):\n    '''\n    V3 Training step with discriminator noise injection.\n    '''\n    with tf.GradientTape(persistent=True) as tape:\n        # Generator forward passes\n        fake_y = gen_G(real_x, training=True)\n        fake_x = gen_F(real_y, training=True)\n        cycled_x = gen_F(fake_y, training=True)\n        cycled_y = gen_G(fake_x, training=True)\n        same_x = gen_F(real_x, training=True)\n        same_y = gen_G(real_y, training=True)\n\n        # V3: Add noise to discriminator inputs for robustness\n        noisy_real_x = add_discriminator_noise(real_x)\n        noisy_real_y = add_discriminator_noise(real_y)\n        noisy_fake_x = add_discriminator_noise(fake_x)\n        noisy_fake_y = add_discriminator_noise(fake_y)\n\n        # Discriminator forward passes (with noisy inputs)\n        disc_real_x = disc_X(noisy_real_x, training=True)\n        disc_real_y = disc_Y(noisy_real_y, training=True)\n        disc_fake_x = disc_X(noisy_fake_x, training=True)\n        disc_fake_y = disc_Y(noisy_fake_y, training=True)\n\n        # Calculate losses\n        gen_G_loss = generator_loss(disc_fake_y)\n        gen_F_loss = 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_loss + total_cycle + identity_loss(real_y, same_y)\n        total_gen_F = gen_F_loss + total_cycle + identity_loss(real_x, same_x)\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    gen_G_grad = tape.gradient(total_gen_G, gen_G.trainable_variables)\n    gen_F_grad = tape.gradient(total_gen_F, gen_F.trainable_variables)\n    disc_X_grad = tape.gradient(disc_X_loss, disc_X.trainable_variables)\n    disc_Y_grad = tape.gradient(disc_Y_loss, disc_Y.trainable_variables)\n\n    gen_G_opt.apply_gradients(zip(gen_G_grad, gen_G.trainable_variables))\n    gen_F_opt.apply_gradients(zip(gen_F_grad, gen_F.trainable_variables))\n    disc_X_opt.apply_gradients(zip(disc_X_grad, disc_X.trainable_variables))\n    disc_Y_opt.apply_gradients(zip(disc_Y_grad, disc_Y.trainable_variables))\n\n    return {\n        'gen_G': total_gen_G, 'gen_F': total_gen_F,\n        'disc_X': disc_X_loss, 'disc_Y': disc_Y_loss,\n        'cycle': total_cycle\n    }\n\nprint(\"[TRAIN] V3 training function ready with noise injection\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# TRAINING LOOP\n# =============================================================================\nhistory = {'gen_G': [], 'gen_F': [], 'disc_X': [], 'disc_Y': [], 'cycle': []}\n\nprint(\"=\"*60)\nprint(f\"[V3 TRAINING] Starting CycleGAN with optimizations\")\nprint(f\"  Epochs: {EPOCHS}\")\nprint(f\"  Device: {DEVICE_TYPE}\")\nprint(f\"  Augmentation: {USE_DATA_AUGMENTATION}\")\nprint(f\"  Label Smoothing: {LABEL_SMOOTHING}\")\nprint(f\"  Disc Noise: {DISC_NOISE_STD}\")\nprint(\"=\"*60)\n\nfor epoch in range(EPOCHS):\n    print(f\"\\nEpoch {epoch + 1}/{EPOCHS}\")\n    epoch_losses = {k: [] for k in history.keys()}\n\n    n = 0\n    for real_x, real_y in tf.data.Dataset.zip((photo_ds, monet_ds)):\n        losses = train_step(real_x, real_y)\n        for k, v in losses.items():\n            epoch_losses[k].append(float(v))\n        n += 1\n        if n % 50 == 0:\n            print(f\"  Batch {n}: G={losses['gen_G']:.3f}, D={losses['disc_Y']:.3f}\")\n\n    for k in history.keys():\n        history[k].append(np.mean(epoch_losses[k]))\n\n    print(f\"  Complete: {n} batches, Avg G={history['gen_G'][-1]:.3f}\")\n\nprint(\"\\n[V3 TRAINING] Complete!\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# VISUALIZE TRAINING PROGRESS\n# =============================================================================\nif len(history['gen_G']) > 0:\n    fig, axes = plt.subplots(1, 3, figsize=(15, 4))\n    fig.suptitle('V3 Training Progress', fontsize=14, fontweight='bold')\n\n    epochs_range = range(1, len(history['gen_G']) + 1)\n\n    axes[0].plot(epochs_range, history['gen_G'], 'g-', label='Gen G', linewidth=2)\n    axes[0].plot(epochs_range, history['gen_F'], 'r-', label='Gen F', linewidth=2)\n    axes[0].set_xlabel('Epoch')\n    axes[0].set_ylabel('Loss')\n    axes[0].set_title('Generator Losses')\n    axes[0].legend()\n    axes[0].grid(True, alpha=0.3)\n\n    axes[1].plot(epochs_range, history['disc_X'], 'b-', label='Disc X', linewidth=2)\n    axes[1].plot(epochs_range, history['disc_Y'], 'm-', label='Disc Y', linewidth=2)\n    axes[1].axhline(y=0.5, color='gray', linestyle='--', alpha=0.5)\n    axes[1].set_xlabel('Epoch')\n    axes[1].set_ylabel('Loss')\n    axes[1].set_title('Discriminator Losses')\n    axes[1].legend()\n    axes[1].grid(True, alpha=0.3)\n\n    axes[2].plot(epochs_range, history['cycle'], 'orange', linewidth=2)\n    axes[2].set_xlabel('Epoch')\n    axes[2].set_ylabel('Loss')\n    axes[2].set_title('Cycle Consistency Loss')\n    axes[2].grid(True, alpha=0.3)\n\n    plt.tight_layout()\n    plt.savefig(f'{OUTPUT_DIR}/training_progress.png', dpi=150, bbox_inches='tight')\n    plt.show()\n    print(\"[SAVED] training_progress.png\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## 6. Image Generation (10,000 Images for Better MiFID)\n\n> **V3 Enhancement**: Generating 10,000 images instead of ~7,000 for more stable MiFID estimation\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# V3: GENERATE MORE IMAGES FOR BETTER MIFID\n# =============================================================================\nprint(\"=\"*60)\nprint(f\"[V3 GENERATION] Creating {MAX_IMAGES if MAX_IMAGES else 'ALL'} Monet-style images...\")\nprint(\"=\"*60)\n\ntest_ds = load_dataset(PHOTO_FILES)\ngenerated_images = []\nphoto_list = list(test_ds)  # Get all photos\n\n# V3: Generate up to MAX_IMAGES, cycling through photos if needed\ntarget_count = MAX_IMAGES if MAX_IMAGES else len(photo_list)\n\nfor i in range(target_count):\n    # Cycle through available photos\n    photo_idx = i % len(photo_list)\n    photo = tf.expand_dims(photo_list[photo_idx], 0)\n\n    fake_monet = gen_G(photo, training=False)\n    fake_monet = (fake_monet[0].numpy() + 1) * 127.5\n    fake_monet = fake_monet.astype(np.uint8)\n    generated_images.append(fake_monet)\n\n    if (i + 1) % 1000 == 0:\n        print(f\"  Generated {i + 1}/{target_count} images...\")\n\nprint(f\"[V3 GENERATION] Total: {len(generated_images)} images\")\nprint(f\"[V3] This is {len(generated_images)/7000*100:.1f}% more than V2's ~7000 images!\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# VISUALIZE RESULTS\n# =============================================================================\nfig, axes = plt.subplots(3, 6, figsize=(18, 9))\nfig.suptitle('V3 Photo to Monet Transformation Results', fontsize=16, fontweight='bold')\n\nsample_photos = photo_list[:6]\n\nfor i in range(6):\n    axes[0, i].imshow((sample_photos[i].numpy() + 1) / 2)\n    axes[0, i].set_title('Original' if i == 0 else '')\n    axes[0, i].axis('off')\n\n    axes[1, i].imshow(generated_images[i] / 255.0)\n    axes[1, i].set_title('Monet Style' if i == 0 else '')\n    axes[1, i].axis('off')\n\n    diff = np.abs((sample_photos[i].numpy() + 1) / 2 - generated_images[i] / 255.0)\n    axes[2, i].imshow(np.mean(diff, axis=-1), cmap='hot')\n    axes[2, i].set_title('Change Map' if i == 0 else '')\n    axes[2, i].axis('off')\n\nplt.tight_layout()\nplt.savefig(f'{OUTPUT_DIR}/transformations.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"[SAVED] transformations.png\")\n"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# =============================================================================\n# CREATE SUBMISSION\n# =============================================================================\nprint(\"\\n[SUBMISSION] Creating images.zip...\")\n\nos.makedirs(f'{OUTPUT_DIR}/images', exist_ok=True)\n\nfor i, img in enumerate(generated_images):\n    img_pil = Image.fromarray(img)\n    img_pil.save(f'{OUTPUT_DIR}/images/{i+1}.jpg', 'JPEG', quality=95)\n\nshutil.make_archive(f'{OUTPUT_DIR}/images', 'zip', f'{OUTPUT_DIR}/images')\n\nprint(f\"[V3 SUCCESS] Created: {OUTPUT_DIR}/images.zip\")\nprint(f\"  Images: {len(generated_images)}\")\nprint(f\"  Format: 256x256 RGB JPEG\")\n"}, {"cell_type": "markdown", "metadata": {}, "source": "---\n\n## V3 Summary: Score Optimization Techniques\n\n| Technique | V2 | V3 | Expected Impact |\n|-----------|----|----|-----------------|\n| **Data Augmentation** | None | Random Flip | +Diversity |\n| **Label Smoothing** | 1.0 | 0.9 | +Stability |\n| **Disc. Noise** | None | 0.05 std | +Robustness |\n| **Epochs** | 25 | 30 | +Quality |\n| **Generated Images** | ~7,000 | 10,000 | +MiFID Stability |\n\n### Why These Low-Risk Techniques Work\n\n1. **No Model Architecture Change** - Same proven U-Net + PatchGAN\n2. **No Major Hyperparameter Change** - Same learning rate, loss weights\n3. **Research-Backed** - All techniques from GAN best practices literature\n4. **Additive, Not Replacing** - Each technique adds value without breaking existing code\n\n---\n\n**Thank you for reading! Good luck with your Monet paintings!**\n"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.8.5"}}, "nbformat": 4, "nbformat_minor": 4}