{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Week 11: Interactive CNN Demonstrations\n",
    "## MPS311/439 Machine Learning - Dr. Wei Xing\n",
    "\n",
    "This notebook contains interactive visualizations to help understand Convolutional Neural Networks.\n",
    "\n",
    "**Instructions:**\n",
    "1. Run all cells in order\n",
    "2. Play with the sliders and buttons\n",
    "3. Observe how outputs change\n",
    "4. Try to understand the relationship between parameters and results\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ All libraries imported successfully!\n",
      "Ready to explore CNNs!\n"
     ]
    }
   ],
   "source": [
    "# Install required packages (if needed in Colab)\n",
    "try:\n",
    "    import google.colab\n",
    "    IN_COLAB = True\n",
    "except:\n",
    "    IN_COLAB = False\n",
    "\n",
    "# Import libraries\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib import patches\n",
    "from ipywidgets import interact, interactive, fixed, interact_manual\n",
    "import ipywidgets as widgets\n",
    "from IPython.display import display, clear_output\n",
    "from scipy.ndimage import convolve\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "print(\"✓ All libraries imported successfully!\")\n",
    "print(\"Ready to explore CNNs!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Demo 1: Real-Time Convolution Visualization\n",
    "\n",
    "**Goal:** Understand how a filter slides across an image\n",
    "\n",
    "**What to explore:**\n",
    "- Move the position slider to see the filter slide\n",
    "- Adjust filter weights to see different patterns\n",
    "- Try the preset filters (vertical edge, horizontal edge, blur)\n",
    "- Change stride to see how it affects output size"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🎮 Interactive Convolution Visualizer\n",
      "==================================================\n",
      "1. Choose a preset filter OR adjust custom weights\n",
      "2. Move position slider to see filter slide across image\n",
      "3. Try different strides (1 or 2)\n",
      "4. Watch how the output feature map is built!\n",
      "\n",
      "\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "3bc1a8f7b196452197e05a48e2d3af17",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=0, description='Position:', max=48), IntSlider(value=1, description='Str…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Create a simple test image\n",
    "def create_test_image():\n",
    "    img = np.zeros((9, 9))\n",
    "    # Create a pattern with vertical and horizontal edges\n",
    "    img[2:7, 3:4] = 1  # Vertical bar\n",
    "    img[3:4, 2:7] = 1  # Horizontal bar\n",
    "    img[5:7, 5:7] = 0.5  # Square\n",
    "    return img\n",
    "\n",
    "test_image = create_test_image()\n",
    "\n",
    "# Preset filters\n",
    "preset_filters = {\n",
    "    'Custom': np.zeros((3, 3)),\n",
    "    'Vertical Edge': np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]),\n",
    "    'Horizontal Edge': np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]]),\n",
    "    'Blur': np.ones((3, 3)) / 9,\n",
    "    'Sharpen': np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]),\n",
    "    'Identity': np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]])\n",
    "}\n",
    "\n",
    "def visualize_convolution(position, stride, filter_preset, \n",
    "                         w00, w01, w02, w10, w11, w12, w20, w21, w22):\n",
    "    \n",
    "    # Get filter\n",
    "    if filter_preset != 'Custom':\n",
    "        kernel = preset_filters[filter_preset]\n",
    "    else:\n",
    "        kernel = np.array([[w00, w01, w02], [w10, w11, w12], [w20, w21, w22]])\n",
    "    \n",
    "    # Calculate output size\n",
    "    output_size = (test_image.shape[0] - 3) // stride + 1\n",
    "    \n",
    "    # Calculate all positions\n",
    "    positions = []\n",
    "    for i in range(0, test_image.shape[0] - 2, stride):\n",
    "        for j in range(0, test_image.shape[1] - 2, stride):\n",
    "            positions.append((i, j))\n",
    "    \n",
    "    if position >= len(positions):\n",
    "        position = len(positions) - 1\n",
    "    \n",
    "    i, j = positions[position]\n",
    "    \n",
    "    # Compute full output\n",
    "    output_full = convolve(test_image, kernel, mode='constant')[::stride, ::stride]\n",
    "    \n",
    "    # Extract current region\n",
    "    region = test_image[i:i+3, j:j+3]\n",
    "    output_val = np.sum(region * kernel)\n",
    "    \n",
    "    # Create visualization\n",
    "    fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n",
    "    \n",
    "    # 1. Input image with highlighted region\n",
    "    axes[0].imshow(test_image, cmap='gray', vmin=0, vmax=1)\n",
    "    rect = patches.Rectangle((j-0.5, i-0.5), 3, 3, linewidth=3, \n",
    "                            edgecolor='red', facecolor='none')\n",
    "    axes[0].add_patch(rect)\n",
    "    axes[0].set_title(f'Input Image\\nPosition: ({i}, {j})', fontsize=12, fontweight='bold')\n",
    "    axes[0].axis('off')\n",
    "    \n",
    "    # Add grid\n",
    "    for x in range(10):\n",
    "        axes[0].axhline(x-0.5, color='gray', linewidth=0.5, alpha=0.3)\n",
    "        axes[0].axvline(x-0.5, color='gray', linewidth=0.5, alpha=0.3)\n",
    "    \n",
    "    # 2. Filter\n",
    "    im = axes[1].imshow(kernel, cmap='RdBu', vmin=-2, vmax=2)\n",
    "    for ii in range(3):\n",
    "        for jj in range(3):\n",
    "            axes[1].text(jj, ii, f'{kernel[ii, jj]:.2f}', \n",
    "                        ha='center', va='center', fontsize=10, fontweight='bold')\n",
    "    axes[1].set_title('Filter (Kernel)', fontsize=12, fontweight='bold')\n",
    "    axes[1].axis('off')\n",
    "    plt.colorbar(im, ax=axes[1], fraction=0.046)\n",
    "    \n",
    "    # 3. Element-wise multiplication\n",
    "    product = region * kernel\n",
    "    im = axes[2].imshow(product, cmap='RdBu', vmin=-1, vmax=1)\n",
    "    for ii in range(3):\n",
    "        for jj in range(3):\n",
    "            axes[2].text(jj, ii, f'{product[ii, jj]:.2f}', \n",
    "                        ha='center', va='center', fontsize=9)\n",
    "    axes[2].set_title(f'Element-wise Product\\nSum = {output_val:.2f}', \n",
    "                     fontsize=12, fontweight='bold')\n",
    "    axes[2].axis('off')\n",
    "    plt.colorbar(im, ax=axes[2], fraction=0.046)\n",
    "    \n",
    "    # 4. Output feature map\n",
    "    im = axes[3].imshow(output_full, cmap='viridis')\n",
    "    # Highlight current output position\n",
    "    out_i, out_j = position // output_size, position % output_size\n",
    "    rect = patches.Rectangle((out_j-0.5, out_i-0.5), 1, 1, linewidth=3,\n",
    "                            edgecolor='red', facecolor='none')\n",
    "    axes[3].add_patch(rect)\n",
    "    axes[3].set_title(f'Output Feature Map\\n{output_full.shape[0]}×{output_full.shape[1]}',\n",
    "                     fontsize=12, fontweight='bold')\n",
    "    axes[3].axis('off')\n",
    "    plt.colorbar(im, ax=axes[3], fraction=0.046)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "# Create interactive widget\n",
    "stride_widget = widgets.IntSlider(value=1, min=1, max=2, description='Stride:')\n",
    "position_widget = widgets.IntSlider(value=0, min=0, max=48, description='Position:')\n",
    "filter_preset_widget = widgets.Dropdown(\n",
    "    options=list(preset_filters.keys()),\n",
    "    value='Vertical Edge',\n",
    "    description='Preset:'\n",
    ")\n",
    "\n",
    "# Filter weight sliders (for custom filter)\n",
    "weight_sliders = []\n",
    "for i in range(3):\n",
    "    for j in range(3):\n",
    "        slider = widgets.FloatSlider(\n",
    "            value=0, min=-2, max=2, step=0.1,\n",
    "            description=f'w[{i}][{j}]:',\n",
    "            style={'description_width': '60px'},\n",
    "            layout=widgets.Layout(width='250px')\n",
    "        )\n",
    "        weight_sliders.append(slider)\n",
    "\n",
    "print(\"🎮 Interactive Convolution Visualizer\")\n",
    "print(\"=\"*50)\n",
    "print(\"1. Choose a preset filter OR adjust custom weights\")\n",
    "print(\"2. Move position slider to see filter slide across image\")\n",
    "print(\"3. Try different strides (1 or 2)\")\n",
    "print(\"4. Watch how the output feature map is built!\")\n",
    "print(\"\\n\")\n",
    "\n",
    "interact(visualize_convolution,\n",
    "         position=position_widget,\n",
    "         stride=stride_widget,\n",
    "         filter_preset=filter_preset_widget,\n",
    "         w00=weight_sliders[0], w01=weight_sliders[1], w02=weight_sliders[2],\n",
    "         w10=weight_sliders[3], w11=weight_sliders[4], w12=weight_sliders[5],\n",
    "         w20=weight_sliders[6], w21=weight_sliders[7], w22=weight_sliders[8]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Demo 2: Parameter Counting - FC vs CNN\n",
    "\n",
    "**Goal:** See the dramatic parameter reduction of CNNs\n",
    "\n",
    "**What to explore:**\n",
    "- Increase image size and watch FC parameters explode\n",
    "- Add more filters to CNN and see the modest increase\n",
    "- Compare the parameter counts\n",
    "- Understand why CNNs are more efficient"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "📊 Parameter Efficiency Explorer\n",
      "==================================================\n",
      "Adjust the parameters to see how CNNs reduce parameter count!\n",
      "\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "bc0ffaad8eb04a9f8992916e671ba478",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=28, description='Image Size:', max=256, min=16, step=8, style=SliderStyl…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def parameter_comparison(image_size, channels, fc_neurons, num_filters, filter_size):\n",
    "    # Calculate parameters\n",
    "    input_size = image_size * image_size * channels\n",
    "    \n",
    "    # Fully-connected\n",
    "    fc_params = input_size * fc_neurons\n",
    "    \n",
    "    # Convolutional\n",
    "    conv_params = (filter_size * filter_size * channels + 1) * num_filters\n",
    "    \n",
    "    # Reduction\n",
    "    reduction = (1 - conv_params / fc_params) * 100\n",
    "    \n",
    "    # Visualization\n",
    "    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))\n",
    "    \n",
    "    # Bar chart\n",
    "    methods = ['Fully-Connected', 'CNN']\n",
    "    params = [fc_params, conv_params]\n",
    "    colors = ['#e74c3c', '#2ecc71']\n",
    "    \n",
    "    bars = ax1.bar(methods, params, color=colors, alpha=0.7, edgecolor='black', linewidth=2)\n",
    "    ax1.set_ylabel('Number of Parameters', fontsize=12, fontweight='bold')\n",
    "    ax1.set_title('Parameter Count Comparison', fontsize=14, fontweight='bold')\n",
    "    ax1.set_yscale('log')\n",
    "    ax1.grid(axis='y', alpha=0.3)\n",
    "    \n",
    "    # Add value labels\n",
    "    for bar, param in zip(bars, params):\n",
    "        height = bar.get_height()\n",
    "        if param >= 1e6:\n",
    "            label = f'{param/1e6:.2f}M'\n",
    "        elif param >= 1e3:\n",
    "            label = f'{param/1e3:.1f}K'\n",
    "        else:\n",
    "            label = f'{int(param)}'\n",
    "        ax1.text(bar.get_x() + bar.get_width()/2., height * 1.1,\n",
    "                label, ha='center', va='bottom', fontsize=12, fontweight='bold')\n",
    "    \n",
    "    # Calculation breakdown\n",
    "    ax2.axis('off')\n",
    "    \n",
    "    breakdown_text = f\"\"\"\n",
    "    📊 CALCULATION BREAKDOWN\n",
    "    \n",
    "    Input Specifications:\n",
    "    • Image size: {image_size}×{image_size}\n",
    "    • Channels: {channels}\n",
    "    • Total input features: {input_size:,}\n",
    "    \n",
    "    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
    "    \n",
    "    Fully-Connected Network:\n",
    "    • Hidden neurons: {fc_neurons}\n",
    "    • Parameters = {input_size} × {fc_neurons}\n",
    "    • Total: {fc_params:,} parameters\n",
    "    \n",
    "    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
    "    \n",
    "    Convolutional Network:\n",
    "    • Filters: {num_filters}\n",
    "    • Filter size: {filter_size}×{filter_size}\n",
    "    • Params per filter: ({filter_size}×{filter_size}×{channels}+1)\n",
    "    • Total: {conv_params:,} parameters\n",
    "    \n",
    "    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
    "    \n",
    "    🎯 PARAMETER REDUCTION: {reduction:.1f}%\n",
    "    \n",
    "    CNN uses {fc_params/conv_params:.1f}× FEWER parameters!\n",
    "    \"\"\"\n",
    "    \n",
    "    ax2.text(0.1, 0.95, breakdown_text, transform=ax2.transAxes,\n",
    "            fontsize=11, verticalalignment='top', family='monospace',\n",
    "            bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.3))\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "print(\"📊 Parameter Efficiency Explorer\")\n",
    "print(\"=\"*50)\n",
    "print(\"Adjust the parameters to see how CNNs reduce parameter count!\\n\")\n",
    "\n",
    "interact(parameter_comparison,\n",
    "         image_size=widgets.IntSlider(value=28, min=16, max=256, step=8, \n",
    "                                     description='Image Size:', \n",
    "                                     style={'description_width': '120px'}),\n",
    "         channels=widgets.Dropdown(options=[1, 3], value=1, \n",
    "                                  description='Channels:',\n",
    "                                  style={'description_width': '120px'}),\n",
    "         fc_neurons=widgets.IntSlider(value=100, min=50, max=512, step=50,\n",
    "                                     description='FC Neurons:',\n",
    "                                     style={'description_width': '120px'}),\n",
    "         num_filters=widgets.IntSlider(value=32, min=8, max=128, step=8,\n",
    "                                      description='CNN Filters:',\n",
    "                                      style={'description_width': '120px'}),\n",
    "         filter_size=widgets.Dropdown(options=[3, 5, 7], value=3,\n",
    "                                     description='Filter Size:',\n",
    "                                     style={'description_width': '120px'}));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Demo 3: Pooling Effect Visualizer\n",
    "\n",
    "**Goal:** Understand what pooling does and why it's useful\n",
    "\n",
    "**What to explore:**\n",
    "- Compare Max Pooling vs Average Pooling\n",
    "- Try different pool sizes (2×2, 3×3, 4×4)\n",
    "- Shift the input pattern to see translation invariance\n",
    "- Observe dimension reduction"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🏊 Pooling Effect Visualizer\n",
      "==================================================\n",
      "See how pooling reduces dimensions and provides translation invariance!\n",
      "\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "8d7bdf9b24de406b8e3f64a9c56c49ce",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(Dropdown(description='Pool Type:', options=('Max', 'Average'), style=DescriptionStyle(de…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def create_feature_map(shift_x=0, shift_y=0):\n",
    "    \"\"\"Create a sample feature map with a pattern\"\"\"\n",
    "    feature_map = np.random.rand(16, 16) * 0.2  # Background noise\n",
    "    # Add a strong pattern\n",
    "    x, y = 6 + shift_x, 6 + shift_y\n",
    "    feature_map[x:x+4, y:y+4] = np.array([\n",
    "        [0.3, 0.8, 0.9, 0.4],\n",
    "        [0.9, 1.0, 0.9, 0.8],\n",
    "        [0.8, 0.9, 1.0, 0.7],\n",
    "        [0.3, 0.6, 0.7, 0.3]\n",
    "    ])\n",
    "    return feature_map\n",
    "\n",
    "def apply_pooling(feature_map, pool_size, pool_type):\n",
    "    \"\"\"Apply pooling operation\"\"\"\n",
    "    h, w = feature_map.shape\n",
    "    out_h = h // pool_size\n",
    "    out_w = w // pool_size\n",
    "    \n",
    "    output = np.zeros((out_h, out_w))\n",
    "    \n",
    "    for i in range(out_h):\n",
    "        for j in range(out_w):\n",
    "            region = feature_map[i*pool_size:(i+1)*pool_size, \n",
    "                                j*pool_size:(j+1)*pool_size]\n",
    "            if pool_type == 'Max':\n",
    "                output[i, j] = np.max(region)\n",
    "            else:  # Average\n",
    "                output[i, j] = np.mean(region)\n",
    "    \n",
    "    return output\n",
    "\n",
    "def visualize_pooling(pool_type, pool_size, shift_x, shift_y, show_original):\n",
    "    # Create feature map\n",
    "    original_map = create_feature_map(0, 0)\n",
    "    shifted_map = create_feature_map(shift_x, shift_y)\n",
    "    \n",
    "    # Apply pooling\n",
    "    original_pooled = apply_pooling(original_map, pool_size, pool_type)\n",
    "    shifted_pooled = apply_pooling(shifted_map, pool_size, pool_type)\n",
    "    \n",
    "    # Calculate difference\n",
    "    difference = np.abs(original_pooled - shifted_pooled)\n",
    "    avg_diff = np.mean(difference)\n",
    "    \n",
    "    # Visualization\n",
    "    if show_original:\n",
    "        fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n",
    "        \n",
    "        # Original - before pooling\n",
    "        im1 = axes[0, 0].imshow(original_map, cmap='viridis', vmin=0, vmax=1)\n",
    "        axes[0, 0].set_title('Original Feature Map\\n(No Shift)', fontsize=12, fontweight='bold')\n",
    "        axes[0, 0].axis('off')\n",
    "        plt.colorbar(im1, ax=axes[0, 0], fraction=0.046)\n",
    "        \n",
    "        # Original - after pooling\n",
    "        im2 = axes[1, 0].imshow(original_pooled, cmap='viridis', vmin=0, vmax=1)\n",
    "        axes[1, 0].set_title(f'After {pool_type} Pooling\\n{original_pooled.shape[0]}×{original_pooled.shape[1]}',\n",
    "                            fontsize=12, fontweight='bold')\n",
    "        axes[1, 0].axis('off')\n",
    "        plt.colorbar(im2, ax=axes[1, 0], fraction=0.046)\n",
    "        \n",
    "        # Shifted - before pooling\n",
    "        im3 = axes[0, 1].imshow(shifted_map, cmap='viridis', vmin=0, vmax=1)\n",
    "        axes[0, 1].set_title(f'Shifted Feature Map\\nShift: ({shift_x}, {shift_y})',\n",
    "                            fontsize=12, fontweight='bold')\n",
    "        axes[0, 1].axis('off')\n",
    "        plt.colorbar(im3, ax=axes[0, 1], fraction=0.046)\n",
    "        \n",
    "        # Shifted - after pooling\n",
    "        im4 = axes[1, 1].imshow(shifted_pooled, cmap='viridis', vmin=0, vmax=1)\n",
    "        axes[1, 1].set_title(f'After {pool_type} Pooling\\n{shifted_pooled.shape[0]}×{shifted_pooled.shape[1]}',\n",
    "                            fontsize=12, fontweight='bold')\n",
    "        axes[1, 1].axis('off')\n",
    "        plt.colorbar(im4, ax=axes[1, 1], fraction=0.046)\n",
    "        \n",
    "        # Difference map\n",
    "        im5 = axes[0, 2].imshow(difference, cmap='hot', vmin=0, vmax=0.5)\n",
    "        axes[0, 2].set_title(f'Absolute Difference\\nAvg: {avg_diff:.4f}',\n",
    "                            fontsize=12, fontweight='bold')\n",
    "        axes[0, 2].axis('off')\n",
    "        plt.colorbar(im5, ax=axes[0, 2], fraction=0.046)\n",
    "        \n",
    "        # Statistics\n",
    "        axes[1, 2].axis('off')\n",
    "        stats_text = f\"\"\"\n",
    "        🎯 Translation Invariance Test\n",
    "        \n",
    "        Input size: 16×16\n",
    "        Pool size: {pool_size}×{pool_size}\n",
    "        Output size: {original_pooled.shape[0]}×{original_pooled.shape[1]}\n",
    "        \n",
    "        Dimension reduction:\n",
    "        {100*(1 - original_pooled.size/original_map.size):.1f}%\n",
    "        \n",
    "        ━━━━━━━━━━━━━━━━━━━━━\n",
    "        \n",
    "        Pattern shift: ({shift_x}, {shift_y})\n",
    "        \n",
    "        Average difference after pooling:\n",
    "        {avg_diff:.4f}\n",
    "        \n",
    "        \"\"\"\n",
    "        \n",
    "        if avg_diff < 0.05:\n",
    "            stats_text += \"\\n✅ Very similar!\\nPooling provides\\ntranslation invariance!\"\n",
    "            color = 'lightgreen'\n",
    "        elif avg_diff < 0.15:\n",
    "            stats_text += \"\\n⚠️ Some difference\\nbut mostly preserved\"\n",
    "            color = 'lightyellow'\n",
    "        else:\n",
    "            stats_text += \"\\n❌ Significant difference\\nLarge shift affects output\"\n",
    "            color = 'lightcoral'\n",
    "        \n",
    "        axes[1, 2].text(0.1, 0.9, stats_text, transform=axes[1, 2].transAxes,\n",
    "                       fontsize=10, verticalalignment='top', family='monospace',\n",
    "                       bbox=dict(boxstyle='round', facecolor=color, alpha=0.5))\n",
    "        \n",
    "    else:\n",
    "        # Simplified view - just show pooling effect\n",
    "        fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n",
    "        \n",
    "        # Input\n",
    "        im1 = axes[0].imshow(shifted_map, cmap='viridis', vmin=0, vmax=1)\n",
    "        axes[0].set_title(f'Input Feature Map\\n16×16', fontsize=12, fontweight='bold')\n",
    "        axes[0].axis('off')\n",
    "        # Draw pooling windows\n",
    "        for i in range(0, 16, pool_size):\n",
    "            for j in range(0, 16, pool_size):\n",
    "                rect = patches.Rectangle((j-0.5, i-0.5), pool_size, pool_size,\n",
    "                                        linewidth=1.5, edgecolor='red', facecolor='none')\n",
    "                axes[0].add_patch(rect)\n",
    "        plt.colorbar(im1, ax=axes[0], fraction=0.046)\n",
    "        \n",
    "        # Arrow\n",
    "        axes[1].text(0.5, 0.5, f'{pool_type}\\nPooling\\n{pool_size}×{pool_size}',\n",
    "                    transform=axes[1].transAxes, fontsize=16, fontweight='bold',\n",
    "                    ha='center', va='center',\n",
    "                    bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))\n",
    "        axes[1].axis('off')\n",
    "        \n",
    "        # Output\n",
    "        im2 = axes[2].imshow(shifted_pooled, cmap='viridis', vmin=0, vmax=1)\n",
    "        axes[2].set_title(f'Output Feature Map\\n{shifted_pooled.shape[0]}×{shifted_pooled.shape[1]}',\n",
    "                         fontsize=12, fontweight='bold')\n",
    "        axes[2].axis('off')\n",
    "        plt.colorbar(im2, ax=axes[2], fraction=0.046)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "print(\"🏊 Pooling Effect Visualizer\")\n",
    "print(\"=\"*50)\n",
    "print(\"See how pooling reduces dimensions and provides translation invariance!\\n\")\n",
    "\n",
    "interact(visualize_pooling,\n",
    "         pool_type=widgets.Dropdown(options=['Max', 'Average'], value='Max',\n",
    "                                   description='Pool Type:',\n",
    "                                   style={'description_width': '120px'}),\n",
    "         pool_size=widgets.Dropdown(options=[2, 4], value=2,\n",
    "                                   description='Pool Size:',\n",
    "                                   style={'description_width': '120px'}),\n",
    "         shift_x=widgets.IntSlider(value=0, min=-2, max=2, description='Shift X:',\n",
    "                                  style={'description_width': '120px'}),\n",
    "         shift_y=widgets.IntSlider(value=0, min=-2, max=2, description='Shift Y:',\n",
    "                                  style={'description_width': '120px'}),\n",
    "         show_original=widgets.Checkbox(value=True, description='Show comparison',\n",
    "                                       style={'description_width': '120px'}));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Demo 4: CNN Architecture Builder\n",
    "\n",
    "**Goal:** Design CNN architectures and understand dimension flow\n",
    "\n",
    "**What to explore:**\n",
    "- Add convolutional layers and watch dimensions change\n",
    "- Add pooling layers to reduce size\n",
    "- See how parameters accumulate\n",
    "- Compare to fully-connected equivalent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🏗️ CNN Architecture Builder\n",
      "==================================================\n",
      "Design your own CNN and see how dimensions flow!\n",
      "\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "937f071ea2dc49b782c8608b9744dc38",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=28, description='Input Size:', max=64, min=16, step=4, style=SliderStyle…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def build_cnn_architecture(input_size, input_channels,\n",
    "                          conv1_filters, conv1_size, pool1_size,\n",
    "                          conv2_filters, conv2_size, pool2_size,\n",
    "                          dense_units, num_classes):\n",
    "    \n",
    "    # Track dimensions and parameters\n",
    "    layers = []\n",
    "    total_params = 0\n",
    "    \n",
    "    # Input\n",
    "    h, w, c = input_size, input_size, input_channels\n",
    "    layers.append({\n",
    "        'name': 'Input',\n",
    "        'output': f'{h}×{w}×{c}',\n",
    "        'params': 0,\n",
    "        'type': 'input'\n",
    "    })\n",
    "    \n",
    "    # Conv1\n",
    "    params_conv1 = (conv1_size * conv1_size * c + 1) * conv1_filters\n",
    "    total_params += params_conv1\n",
    "    # Assume 'same' padding\n",
    "    layers.append({\n",
    "        'name': f'Conv2D({conv1_filters})',\n",
    "        'output': f'{h}×{w}×{conv1_filters}',\n",
    "        'params': params_conv1,\n",
    "        'type': 'conv'\n",
    "    })\n",
    "    \n",
    "    # Pool1\n",
    "    h, w = h // pool1_size, w // pool1_size\n",
    "    layers.append({\n",
    "        'name': f'MaxPool({pool1_size}×{pool1_size})',\n",
    "        'output': f'{h}×{w}×{conv1_filters}',\n",
    "        'params': 0,\n",
    "        'type': 'pool'\n",
    "    })\n",
    "    c = conv1_filters\n",
    "    \n",
    "    # Conv2\n",
    "    params_conv2 = (conv2_size * conv2_size * c + 1) * conv2_filters\n",
    "    total_params += params_conv2\n",
    "    layers.append({\n",
    "        'name': f'Conv2D({conv2_filters})',\n",
    "        'output': f'{h}×{w}×{conv2_filters}',\n",
    "        'params': params_conv2,\n",
    "        'type': 'conv'\n",
    "    })\n",
    "    \n",
    "    # Pool2\n",
    "    h, w = h // pool2_size, w // pool2_size\n",
    "    layers.append({\n",
    "        'name': f'MaxPool({pool2_size}×{pool2_size})',\n",
    "        'output': f'{h}×{w}×{conv2_filters}',\n",
    "        'params': 0,\n",
    "        'type': 'pool'\n",
    "    })\n",
    "    c = conv2_filters\n",
    "    \n",
    "    # Flatten\n",
    "    flatten_size = h * w * c\n",
    "    layers.append({\n",
    "        'name': 'Flatten',\n",
    "        'output': f'{flatten_size}',\n",
    "        'params': 0,\n",
    "        'type': 'flatten'\n",
    "    })\n",
    "    \n",
    "    # Dense\n",
    "    params_dense1 = (flatten_size + 1) * dense_units\n",
    "    total_params += params_dense1\n",
    "    layers.append({\n",
    "        'name': f'Dense({dense_units})',\n",
    "        'output': f'{dense_units}',\n",
    "        'params': params_dense1,\n",
    "        'type': 'dense'\n",
    "    })\n",
    "    \n",
    "    # Output\n",
    "    params_output = (dense_units + 1) * num_classes\n",
    "    total_params += params_output\n",
    "    layers.append({\n",
    "        'name': f'Dense({num_classes})',\n",
    "        'output': f'{num_classes}',\n",
    "        'params': params_output,\n",
    "        'type': 'output'\n",
    "    })\n",
    "    \n",
    "    # Calculate equivalent FC network\n",
    "    fc_total = (input_size * input_size * input_channels) * dense_units + \\\n",
    "               dense_units * num_classes\n",
    "    \n",
    "    # Visualization\n",
    "    fig = plt.figure(figsize=(16, 8))\n",
    "    gs = fig.add_gridspec(2, 1, height_ratios=[3, 1])\n",
    "    \n",
    "    # Architecture diagram\n",
    "    ax1 = fig.add_subplot(gs[0])\n",
    "    ax1.set_xlim(0, len(layers) + 1)\n",
    "    ax1.set_ylim(0, 10)\n",
    "    ax1.axis('off')\n",
    "    \n",
    "    colors = {\n",
    "        'input': '#95a5a6',\n",
    "        'conv': '#3498db',\n",
    "        'pool': '#2ecc71',\n",
    "        'flatten': '#9b59b6',\n",
    "        'dense': '#e67e22',\n",
    "        'output': '#e74c3c'\n",
    "    }\n",
    "    \n",
    "    for i, layer in enumerate(layers):\n",
    "        x = i + 1\n",
    "        \n",
    "        # Draw box\n",
    "        if layer['type'] in ['conv', 'pool']:\n",
    "            # 3D-ish box\n",
    "            rect = patches.FancyBboxPatch((x - 0.3, 4), 0.6, 3,\n",
    "                                         boxstyle=\"round,pad=0.05\",\n",
    "                                         linewidth=2, edgecolor='black',\n",
    "                                         facecolor=colors[layer['type']], alpha=0.7)\n",
    "        else:\n",
    "            # 1D bar\n",
    "            rect = patches.FancyBboxPatch((x - 0.15, 4), 0.3, 3,\n",
    "                                         boxstyle=\"round,pad=0.05\",\n",
    "                                         linewidth=2, edgecolor='black',\n",
    "                                         facecolor=colors[layer['type']], alpha=0.7)\n",
    "        ax1.add_patch(rect)\n",
    "        \n",
    "        # Labels\n",
    "        ax1.text(x, 7.5, layer['name'], ha='center', va='center',\n",
    "                fontsize=9, fontweight='bold', wrap=True)\n",
    "        ax1.text(x, 5.5, layer['output'], ha='center', va='center',\n",
    "                fontsize=8)\n",
    "        if layer['params'] > 0:\n",
    "            param_str = f\"{layer['params']:,}\" if layer['params'] < 1000 else f\"{layer['params']/1000:.1f}K\"\n",
    "            ax1.text(x, 3.5, param_str, ha='center', va='center',\n",
    "                    fontsize=7, style='italic', color='darkred')\n",
    "        \n",
    "        # Arrow\n",
    "        if i < len(layers) - 1:\n",
    "            ax1.annotate('', xy=(x + 0.8, 5.5), xytext=(x + 0.3, 5.5),\n",
    "                        arrowprops=dict(arrowstyle='->', lw=2, color='black'))\n",
    "    \n",
    "    ax1.set_title('CNN Architecture', fontsize=14, fontweight='bold', pad=20)\n",
    "    \n",
    "    # Statistics panel\n",
    "    ax2 = fig.add_subplot(gs[1])\n",
    "    ax2.axis('off')\n",
    "    \n",
    "    stats_text = f\"\"\"\n",
    "    📊 ARCHITECTURE SUMMARY\n",
    "    \n",
    "    Total Layers: {len(layers)}\n",
    "    Total Parameters: {total_params:,}\n",
    "    Memory (float32): ~{total_params * 4 / 1024 / 1024:.2f} MB\n",
    "    \n",
    "    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
    "    \n",
    "    COMPARISON TO FULLY-CONNECTED:\n",
    "    \n",
    "    CNN Parameters:         {total_params:,}\n",
    "    FC Equivalent:          {fc_total:,}\n",
    "    Parameter Reduction:    {100*(1 - total_params/fc_total):.1f}%\n",
    "    \n",
    "    CNN is {fc_total/total_params:.1f}× more efficient!\n",
    "    \"\"\"\n",
    "    \n",
    "    ax2.text(0.5, 0.5, stats_text, transform=ax2.transAxes,\n",
    "            fontsize=11, verticalalignment='center', horizontalalignment='center',\n",
    "            family='monospace',\n",
    "            bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.7))\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "print(\"🏗️ CNN Architecture Builder\")\n",
    "print(\"=\"*50)\n",
    "print(\"Design your own CNN and see how dimensions flow!\\n\")\n",
    "\n",
    "interact(build_cnn_architecture,\n",
    "         input_size=widgets.IntSlider(value=28, min=16, max=64, step=4,\n",
    "                                     description='Input Size:',\n",
    "                                     style={'description_width': '130px'}),\n",
    "         input_channels=widgets.Dropdown(options=[1, 3], value=1,\n",
    "                                        description='Input Channels:',\n",
    "                                        style={'description_width': '130px'}),\n",
    "         conv1_filters=widgets.IntSlider(value=32, min=8, max=128, step=8,\n",
    "                                        description='Conv1 Filters:',\n",
    "                                        style={'description_width': '130px'}),\n",
    "         conv1_size=widgets.Dropdown(options=[3, 5], value=3,\n",
    "                                    description='Conv1 Size:',\n",
    "                                    style={'description_width': '130px'}),\n",
    "         pool1_size=widgets.Dropdown(options=[2, 3], value=2,\n",
    "                                    description='Pool1 Size:',\n",
    "                                    style={'description_width': '130px'}),\n",
    "         conv2_filters=widgets.IntSlider(value=64, min=16, max=256, step=16,\n",
    "                                        description='Conv2 Filters:',\n",
    "                                        style={'description_width': '130px'}),\n",
    "         conv2_size=widgets.Dropdown(options=[3, 5], value=3,\n",
    "                                    description='Conv2 Size:',\n",
    "                                    style={'description_width': '130px'}),\n",
    "         pool2_size=widgets.Dropdown(options=[2, 3], value=2,\n",
    "                                    description='Pool2 Size:',\n",
    "                                    style={'description_width': '130px'}),\n",
    "         dense_units=widgets.IntSlider(value=128, min=32, max=512, step=32,\n",
    "                                      description='Dense Units:',\n",
    "                                      style={'description_width': '130px'}),\n",
    "         num_classes=widgets.IntSlider(value=10, min=2, max=100, step=1,\n",
    "                                      description='Output Classes:',\n",
    "                                      style={'description_width': '130px'}));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Demo 5: Training Dynamics - Overfitting Monitor\n",
    "\n",
    "**Goal:** See how different regularization techniques affect training\n",
    "\n",
    "**What to explore:**\n",
    "- Watch training curves evolve\n",
    "- Increase model capacity and see overfitting\n",
    "- Add dropout to reduce overfitting\n",
    "- Reduce training data and see the effect\n",
    "\n",
    "**Note:** This demo simulates training curves for speed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "📈 Training Dynamics - Overfitting Monitor\n",
      "==================================================\n",
      "Explore how hyperparameters affect overfitting!\n",
      "\n",
      "Note: This demo simulates training for demonstration purposes.\n",
      "\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "01a5faefc681425d9170cac3dcaa39a3",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=32, description='Num Filters:', max=128, min=8, step=8, style=SliderStyl…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def simulate_training(num_filters, dropout_rate, training_samples, epochs):\n",
    "    \"\"\"\n",
    "    Simulate training curves based on hyperparameters\n",
    "    In practice, these would come from actual training\n",
    "    \"\"\"\n",
    "    np.random.seed(42)\n",
    "    \n",
    "    # Complexity factor (higher = more capacity = more overfitting potential)\n",
    "    complexity = num_filters / 32.0\n",
    "    \n",
    "    # Data sufficiency (more data = less overfitting)\n",
    "    data_factor = training_samples / 10000.0\n",
    "    \n",
    "    # Dropout effect\n",
    "    regularization = 1.0 - dropout_rate * 0.7\n",
    "    \n",
    "    # Generate curves\n",
    "    epoch_range = np.arange(1, epochs + 1)\n",
    "    \n",
    "    # Training accuracy (always improves)\n",
    "    train_acc = 0.6 + 0.35 * (1 - np.exp(-epoch_range / 3)) + \\\n",
    "                0.05 * complexity * (1 - np.exp(-epoch_range / 2))\n",
    "    train_acc = np.minimum(train_acc, 0.995)\n",
    "    \n",
    "    # Validation accuracy (may plateau or decrease if overfitting)\n",
    "    val_acc_base = 0.6 + 0.30 * (1 - np.exp(-epoch_range / 4))\n",
    "    \n",
    "    # Overfitting effect\n",
    "    overfit_factor = complexity / data_factor / regularization\n",
    "    if overfit_factor > 1.5:\n",
    "        # Starts overfitting\n",
    "        peak_epoch = max(3, int(8 / overfit_factor))\n",
    "        overfit_penalty = np.where(epoch_range > peak_epoch,\n",
    "                                  0.03 * (epoch_range - peak_epoch) * (overfit_factor - 1),\n",
    "                                  0)\n",
    "        val_acc = val_acc_base - overfit_penalty\n",
    "    else:\n",
    "        val_acc = val_acc_base * regularization + 0.02\n",
    "    \n",
    "    val_acc = np.minimum(val_acc, 0.98)\n",
    "    \n",
    "    # Add some noise\n",
    "    train_acc += np.random.randn(epochs) * 0.005\n",
    "    val_acc += np.random.randn(epochs) * 0.01\n",
    "    \n",
    "    # Loss curves (inverse of accuracy, roughly)\n",
    "    train_loss = 1.5 * np.exp(-epoch_range / 3) + 0.05 + np.random.randn(epochs) * 0.01\n",
    "    val_loss_base = 1.2 * np.exp(-epoch_range / 4) + 0.1\n",
    "    \n",
    "    if overfit_factor > 1.5:\n",
    "        peak_epoch = max(3, int(8 / overfit_factor))\n",
    "        overfit_penalty = np.where(epoch_range > peak_epoch,\n",
    "                                  0.05 * (epoch_range - peak_epoch) * (overfit_factor - 1),\n",
    "                                  0)\n",
    "        val_loss = val_loss_base + overfit_penalty\n",
    "    else:\n",
    "        val_loss = val_loss_base / regularization\n",
    "    \n",
    "    val_loss += np.random.randn(epochs) * 0.02\n",
    "    \n",
    "    # Clip values\n",
    "    train_acc = np.clip(train_acc, 0.5, 1.0)\n",
    "    val_acc = np.clip(val_acc, 0.5, 1.0)\n",
    "    train_loss = np.clip(train_loss, 0.01, 2.0)\n",
    "    val_loss = np.clip(val_loss, 0.01, 2.0)\n",
    "    \n",
    "    return epoch_range, train_acc, val_acc, train_loss, val_loss, overfit_factor\n",
    "\n",
    "def visualize_training(num_filters, dropout_rate, training_samples, epochs):\n",
    "    # Simulate training\n",
    "    epoch_range, train_acc, val_acc, train_loss, val_loss, overfit_factor = \\\n",
    "        simulate_training(num_filters, dropout_rate, training_samples, epochs)\n",
    "    \n",
    "    # Detect overfitting\n",
    "    if len(epoch_range) > 5:\n",
    "        recent_train = train_acc[-3:]\n",
    "        recent_val = val_acc[-3:]\n",
    "        gap = np.mean(recent_train) - np.mean(recent_val)\n",
    "        is_overfitting = gap > 0.05\n",
    "    else:\n",
    "        is_overfitting = False\n",
    "    \n",
    "    # Visualization\n",
    "    fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
    "    \n",
    "    # Accuracy plot\n",
    "    axes[0].plot(epoch_range, train_acc, 'o-', linewidth=2, markersize=6,\n",
    "                label='Training', color='#3498db')\n",
    "    axes[0].plot(epoch_range, val_acc, 's--', linewidth=2, markersize=6,\n",
    "                label='Validation', color='#e67e22')\n",
    "    axes[0].set_xlabel('Epoch', fontsize=12, fontweight='bold')\n",
    "    axes[0].set_ylabel('Accuracy', fontsize=12, fontweight='bold')\n",
    "    axes[0].set_title('Model Accuracy', fontsize=13, fontweight='bold')\n",
    "    axes[0].legend(loc='lower right', fontsize=10)\n",
    "    axes[0].grid(True, alpha=0.3)\n",
    "    axes[0].set_ylim(0.5, 1.0)\n",
    "    \n",
    "    # Loss plot\n",
    "    axes[1].plot(epoch_range, train_loss, 'o-', linewidth=2, markersize=6,\n",
    "                label='Training', color='#3498db')\n",
    "    axes[1].plot(epoch_range, val_loss, 's--', linewidth=2, markersize=6,\n",
    "                label='Validation', color='#e67e22')\n",
    "    axes[1].set_xlabel('Epoch', fontsize=12, fontweight='bold')\n",
    "    axes[1].set_ylabel('Loss', fontsize=12, fontweight='bold')\n",
    "    axes[1].set_title('Model Loss', fontsize=13, fontweight='bold')\n",
    "    axes[1].legend(loc='upper right', fontsize=10)\n",
    "    axes[1].grid(True, alpha=0.3)\n",
    "    \n",
    "    # Analysis panel\n",
    "    axes[2].axis('off')\n",
    "    \n",
    "    final_train_acc = train_acc[-1]\n",
    "    final_val_acc = val_acc[-1]\n",
    "    gap = final_train_acc - final_val_acc\n",
    "    \n",
    "    analysis_text = f\"\"\"\n",
    "    📊 TRAINING ANALYSIS\n",
    "    \n",
    "    Model Configuration:\n",
    "    • Filters: {num_filters}\n",
    "    • Dropout: {dropout_rate:.2f}\n",
    "    • Training samples: {training_samples:,}\n",
    "    • Epochs: {epochs}\n",
    "    \n",
    "    ━━━━━━━━━━━━━━━━━━━━━━━━\n",
    "    \n",
    "    Final Results:\n",
    "    • Train Acc: {final_train_acc:.3f}\n",
    "    • Val Acc: {final_val_acc:.3f}\n",
    "    • Gap: {gap:.3f}\n",
    "    \n",
    "    ━━━━━━━━━━━━━━━━━━━━━━━━\n",
    "    \n",
    "    \"\"\"\n",
    "    \n",
    "    if is_overfitting:\n",
    "        analysis_text += \"\"\"\n",
    "    ⚠️ OVERFITTING DETECTED!\n",
    "    \n",
    "    Suggestions:\n",
    "    • Increase dropout rate\n",
    "    • Add more training data\n",
    "    • Reduce model capacity\n",
    "    • Use data augmentation\n",
    "    \"\"\"\n",
    "        bgcolor = '#ffcccc'\n",
    "    elif gap < 0.02:\n",
    "        analysis_text += \"\"\"\n",
    "    ✅ GOOD GENERALIZATION!\n",
    "    \n",
    "    The model is learning well\n",
    "    without overfitting.\n",
    "    \n",
    "    You could try:\n",
    "    • Increase model capacity\n",
    "    • Train for more epochs\n",
    "    \"\"\"\n",
    "        bgcolor = '#ccffcc'\n",
    "    else:\n",
    "        analysis_text += \"\"\"\n",
    "    ⚡ REASONABLE PERFORMANCE\n",
    "    \n",
    "    Some gap between train/val\n",
    "    but not severe overfitting.\n",
    "    \n",
    "    Consider:\n",
    "    • Slight dropout increase\n",
    "    • More training data\n",
    "    \"\"\"\n",
    "        bgcolor = '#ffffcc'\n",
    "    \n",
    "    axes[2].text(0.1, 0.9, analysis_text, transform=axes[2].transAxes,\n",
    "                fontsize=10, verticalalignment='top', family='monospace',\n",
    "                bbox=dict(boxstyle='round', facecolor=bgcolor, alpha=0.7))\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "print(\"📈 Training Dynamics - Overfitting Monitor\")\n",
    "print(\"=\"*50)\n",
    "print(\"Explore how hyperparameters affect overfitting!\\n\")\n",
    "print(\"Note: This demo simulates training for demonstration purposes.\\n\")\n",
    "\n",
    "interact(visualize_training,\n",
    "         num_filters=widgets.IntSlider(value=32, min=8, max=128, step=8,\n",
    "                                      description='Num Filters:',\n",
    "                                      style={'description_width': '130px'}),\n",
    "         dropout_rate=widgets.FloatSlider(value=0.0, min=0.0, max=0.7, step=0.1,\n",
    "                                         description='Dropout Rate:',\n",
    "                                         style={'description_width': '130px'}),\n",
    "         training_samples=widgets.IntSlider(value=10000, min=1000, max=50000, step=1000,\n",
    "                                           description='Train Samples:',\n",
    "                                           style={'description_width': '130px'}),\n",
    "         epochs=widgets.IntSlider(value=15, min=5, max=30, step=1,\n",
    "                                 description='Epochs:',\n",
    "                                 style={'description_width': '130px'}));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Summary\n",
    "\n",
    "🎉 **Congratulations!** You've explored the key concepts of CNNs through interactive visualizations.\n",
    "\n",
    "### Key Takeaways:\n",
    "\n",
    "1. **Convolutions** are just local weighted sums that slide across images\n",
    "2. **Parameter sharing** makes CNNs dramatically more efficient than fully-connected networks\n",
    "3. **Pooling** reduces dimensions and provides translation invariance\n",
    "4. **Architecture design** involves balancing model capacity with overfitting\n",
    "5. **Regularization** (dropout, more data) helps prevent overfitting\n",
    "\n",
    "### What to do next:\n",
    "\n",
    "- Review the lecture notes for mathematical details\n",
    "- Try the actual CNN implementation on MNIST\n",
    "- Experiment with different architectures\n",
    "- Apply CNNs to your own image datasets\n",
    "- Explore transfer learning with pre-trained models\n",
    "\n",
    "---\n",
    "\n",
    "**Questions?** Ask in class or on the discussion forum!\n",
    "\n",
    "**Want to explore more?** Check out:\n",
    "- CNN Explainer: https://poloclub.github.io/cnn-explainer/\n",
    "- TensorFlow Playground: https://playground.tensorflow.org\n",
    "- Distill.pub for beautiful ML explanations"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "expTorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
