Master the architecture that revolutionized computer vision. Learn convolution operations, pooling layers, feature maps, and how CNNs automatically learn visual hierarchies from raw pixels.
Convolutional Neural Networks (CNNs) revolutionized artificial intelligence in 2012 when AlexNet achieved breakthrough performance on ImageNet. Today, CNNs power:
But what makes CNNs so special? Why can't we just use regular neural networks for images?
The Problem with Fully-Connected Networks:
A 224Γ224 RGB image has 150,528 pixels. A single hidden layer with 1,000 neurons would need 150 million parametersβimpossible to train, prone to overfitting, and computationally impractical.
The CNN Solution:
CNNs exploit the spatial structure of images through:
In this lesson, you'll build CNNs from first principles, understand convolution mathematics, and train networks that see and understand images like humans do.
Ready to unlock the secrets of computer vision? Let's dive in! ποΈ
Before CNNs, applying neural networks to images faced fundamental challenges. Understanding these problems reveals why CNNs are revolutionary.
Fully-Connected Network Problem:
1import numpy as np
2
3# Small 28Γ28 grayscale image (MNIST digit)
4image_shape = (28, 28, 1)
5pixels = 28 * 28 * 1 # 784 pixels
6
7# Fully-connected network
8hidden_units = 128
9
10# Parameters needed:
11fc_parameters = pixels * hidden_units + hidden_units
12print(f"Fully-Connected Parameters: {fc_parameters:,}")
13print(f" (784 Γ 128 + 128 bias = 100,480)")
14
15# Now try ImageNet-sized image: 224Γ224Γ3 RGB
16imagenet_pixels = 224 * 224 * 3 # 150,528 pixels
17fc_parameters_large = imagenet_pixels * hidden_units + hidden_units
18
19print(f"\nImageNet-sized FC Parameters: {fc_parameters_large:,}")
20print(f" (150,528 Γ 128 = 19+ million parameters!)")
21print("\nβ Problem: Too many parameters β overfitting, slow training")CNN Solution: Weight Sharing
1# Convolutional layer
2kernel_size = 3
3num_filters = 32
4
5# Parameters per filter
6params_per_filter = kernel_size * kernel_size * 1 + 1 # +1 for bias
7total_params = params_per_filter * num_filters
8
9print(f"\nCNN Parameters: {total_params}")
10print(f" (3Γ3 kernel Γ 32 filters = 320 parameters)")
11print("\nβ
Solution: 99.7% fewer parameters!")Images of the same object in different positions should be recognized identically.
1import matplotlib.pyplot as plt
2
3# Create simple image with a vertical edge
4image = np.zeros((10, 10))
5image[:, 4:6] = 1 # Vertical bar in center
6
7# Shift the bar to the right
8image_shifted = np.zeros((10, 10))
9image_shifted[:, 6:8] = 1 # Vertical bar shifted
10
11fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
12
13ax1.imshow(image, cmap='gray')
14ax1.set_title('Original Position', fontweight='bold')
15ax1.axis('off')
16
17ax2.imshow(image_shifted, cmap='gray')
18ax2.set_title('Shifted Position', fontweight='bold')
19ax2.axis('off')
20
21plt.tight_layout()
22plt.show()
23
24print("β Fully-connected network: Treats these as completely different inputs")
25print("β
CNN: Recognizes both as 'vertical edge' regardless of position")Why FC Networks Fail:
Why CNNs Succeed:
Visual understanding is hierarchical: pixels β edges β textures β parts β objects
1Image Recognition Hierarchy:
2
3Layer 1 (Low-level):
4ββ Horizontal edges
5ββ Vertical edges
6ββ Diagonal edges
7ββ Colors, gradients
8
9Layer 2 (Mid-level):
10ββ Corners
11ββ Circles
12ββ Simple textures
13ββ Basic shapes
14
15Layer 3 (High-level):
16ββ Eyes, nose, mouth (faces)
17ββ Wheels, windows (cars)
18ββ Fur, ears (animals)
19ββ Object parts
20
21Layer 4 (Object-level):
22ββ Full faces
23ββ Complete cars
24ββ Specific animals
25ββ Scene understandingCNN Architecture Matches This Hierarchy:
1# Simple CNN architecture mimics visual hierarchy
2from tensorflow.keras import Sequential
3from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense
4
5model = Sequential([
6 # Layer 1: Detect edges (low-level features)
7 Conv2D(32, (3,3), activation='relu', input_shape=(224, 224, 3)),
8 MaxPooling2D((2,2)),
9
10 # Layer 2: Combine edges into textures (mid-level)
11 Conv2D(64, (3,3), activation='relu'),
12 MaxPooling2D((2,2)),
13
14 # Layer 3: Combine textures into parts (high-level)
15 Conv2D(128, (3,3), activation='relu'),
16 MaxPooling2D((2,2)),
17
18 # Layer 4: Combine parts into objects
19 Flatten(),
20 Dense(512, activation='relu'),
21 Dense(10, activation='softmax') # 10 classes
22])
23
24print("\nβ
Each CNN layer learns progressively complex features")
25print(" This matches how human visual cortex processes images!")1# Compare parameter counts for ImageNet (224Γ224Γ3)
2
3architectures = {
4 'Fully-Connected (1 hidden layer)': 150528 * 1000,
5 'Fully-Connected (2 hidden layers)': 150528 * 512 + 512 * 256,
6 'Simple CNN (AlexNet-style)': 35000, # Approximate
7 'Modern CNN (ResNet-50)': 25000000, # Still less than FC!
8}
9
10print("\n" + "="*60)
11print("PARAMETER COMPARISON (ImageNet-sized images)")
12print("="*60)
13
14for name, params in architectures.items():
15 print(f"{name:40} {params:>15,}")
16
17print("\n" + "="*60)
18print("Key Insight: CNNs achieve better results with fewer parameters")
19print("by exploiting spatial structure and local patterns!")
20print("="*60)Why CNNs Dominate Computer Vision:
Real-World Impact:
CNNs aren't just neural networks applied to imagesβthey're a fundamental rethinking of how to process spatial data!
Convolution is the fundamental operation in CNNs. Understanding it deeply is essential for mastering computer vision.
Mathematical Definition:
Convolution slides a filter (kernel) over an image, computing element-wise products and summing the results.
1(f β
g)(x, y) = Ξ£ Ξ£ f(i, j) Γ g(x - i, y - j)
2 i jDon't worry about the mathβlet's see it in action!
1import numpy as np
2
3# Step 1: Define a simple 5Γ5 image
4image = np.array([
5 [0, 0, 0, 0, 0],
6 [0, 1, 1, 1, 0],
7 [0, 1, 1, 1, 0],
8 [0, 1, 1, 1, 0],
9 [0, 0, 0, 0, 0]
10])
11
12print("Original Image (5Γ5):")
13print(image)
14print()
15
16# Step 2: Define a 3Γ3 edge detection filter
17# This filter detects vertical edges
18kernel = np.array([
19 [-1, 0, 1],
20 [-1, 0, 1],
21 [-1, 0, 1]
22])
23
24print("Vertical Edge Detection Kernel (3Γ3):")
25print(kernel)
26print()
27
28# Step 3: Manual convolution at position (1, 1)
29def convolve_position(image, kernel, row, col):
30 """Apply convolution at specific position"""
31 region = image[row:row+3, col:col+3]
32 result = np.sum(region * kernel)
33 return result
34
35print("Convolution at position (1, 1):")
36print("Image region:")
37print(image[1:4, 1:4])
38print("\nElement-wise multiply with kernel:")
39print(image[1:4, 1:4] * kernel)
40print(f"\nSum of products: {convolve_position(image, kernel, 1, 1)}")
41print()
42
43# Step 4: Apply convolution across entire image
44def convolve2d(image, kernel):
45 """Full 2D convolution"""
46 i_height, i_width = image.shape
47 k_height, k_width = kernel.shape
48
49 # Output dimensions (no padding)
50 o_height = i_height - k_height + 1
51 o_width = i_width - k_width + 1
52
53 output = np.zeros((o_height, o_width))
54
55 # Slide kernel across image
56 for i in range(o_height):
57 for j in range(o_width):
58 # Extract region
59 region = image[i:i+k_height, j:j+k_width]
60 # Convolve: element-wise multiply and sum
61 output[i, j] = np.sum(region * kernel)
62
63 return output
64
65# Apply full convolution
66feature_map = convolve2d(image, kernel)
67
68print("Feature Map (after convolution):")
69print(feature_map)
70print()
71
72print("Interpretation:")
73print(" Large positive values β Strong vertical edge (dark β light)")
74print(" Large negative values β Strong vertical edge (light β dark)")
75print(" Values near 0 β No vertical edge")Output:
1Feature Map (after convolution):
2[[-3. 0. 3.]
3 [-3. 0. 3.]
4 [-3. 0. 3.]]
5
6β
The filter detected vertical edges on left and right!1import matplotlib.pyplot as plt
2
3# Create test image with different edges
4test_image = np.array([
5 [0, 0, 0, 1, 1, 1],
6 [0, 0, 0, 1, 1, 1],
7 [0, 0, 0, 1, 1, 1],
8 [1, 1, 1, 0, 0, 0],
9 [1, 1, 1, 0, 0, 0],
10 [1, 1, 1, 0, 0, 0]
11]) * 255 # Scale to 0-255
12
13# Define various edge detection kernels
14kernels = {
15 'Vertical Edge': np.array([
16 [-1, 0, 1],
17 [-1, 0, 1],
18 [-1, 0, 1]
19 ]),
20 'Horizontal Edge': np.array([
21 [-1, -1, -1],
22 [ 0, 0, 0],
23 [ 1, 1, 1]
24 ]),
25 'Diagonal Edge': np.array([
26 [-2, -1, 0],
27 [-1, 0, 1],
28 [ 0, 1, 2]
29 ]),
30 'Sharpen': np.array([
31 [ 0, -1, 0],
32 [-1, 5, -1],
33 [ 0, -1, 0]
34 ])
35}
36
37fig, axes = plt.subplots(2, 5, figsize=(18, 8))
38
39# Show original
40axes[0, 0].imshow(test_image, cmap='gray')
41axes[0, 0].set_title('Original Image', fontweight='bold')
42axes[0, 0].axis('off')
43axes[1, 0].axis('off')
44
45# Apply each kernel
46for idx, (name, kernel) in enumerate(kernels.items(), 1):
47 # Convolve
48 feature_map = convolve2d(test_image, kernel)
49
50 # Show kernel
51 axes[0, idx].imshow(kernel, cmap='RdBu', vmin=-2, vmax=2)
52 axes[0, idx].set_title(f'{name} Kernel', fontweight='bold', fontsize=10)
53 axes[0, idx].axis('off')
54
55 # Show result
56 axes[1, idx].imshow(feature_map, cmap='gray')
57 axes[1, idx].set_title(f'{name} Output', fontweight='bold', fontsize=10)
58 axes[1, idx].axis('off')
59
60plt.tight_layout()
61plt.show()
62
63print("\nβ
Different kernels detect different features!")1. Stride
How many pixels the filter moves each step.
1def convolve2d_stride(image, kernel, stride=1):
2 """Convolution with stride parameter"""
3 i_height, i_width = image.shape
4 k_height, k_width = kernel.shape
5
6 # Output dimensions with stride
7 o_height = (i_height - k_height) // stride + 1
8 o_width = (i_width - k_width) // stride + 1
9
10 output = np.zeros((o_height, o_width))
11
12 for i in range(o_height):
13 for j in range(o_width):
14 region = image[i*stride:i*stride+k_height,
15 j*stride:j*stride+k_width]
16 output[i, j] = np.sum(region * kernel)
17
18 return output
19
20# Compare strides
21image_8x8 = np.random.rand(8, 8)
22kernel_3x3 = np.random.rand(3, 3)
23
24stride_1 = convolve2d_stride(image_8x8, kernel_3x3, stride=1)
25stride_2 = convolve2d_stride(image_8x8, kernel_3x3, stride=2)
26
27print(f"Input: 8Γ8 image, 3Γ3 kernel")
28print(f" Stride=1 β Output: {stride_1.shape}")
29print(f" Stride=2 β Output: {stride_2.shape}")
30print("\nLarger stride β Smaller output, faster computation")2. Padding
Adding borders to control output size.
1def add_padding(image, pad_size):
2 """Add zero padding around image"""
3 return np.pad(image, pad_size, mode='constant', constant_values=0)
4
5# Original 5Γ5 image
6image_5x5 = np.ones((5, 5))
7
8# Add padding
9padded = add_padding(image_5x5, pad_size=1)
10
11print(f"Original: {image_5x5.shape}")
12print(f"With padding=1: {padded.shape}")
13print("\nPadding types:")
14print(" β’ VALID: No padding (output shrinks)")
15print(" β’ SAME: Padding to keep output size = input size")1Output Height = β(H - K + 2P) / Sβ + 1
2Output Width = β(W - K + 2P) / Sβ + 1
3
4Where:
5 H, W = Input height, width
6 K = Kernel size
7 P = Padding
8 S = Stride
9 ββ = Floor divisionExamples:
1def calculate_output_size(input_size, kernel_size, padding, stride):
2 """Calculate output dimensions"""
3 return ((input_size - kernel_size + 2 * padding) // stride) + 1
4
5# Common scenarios
6scenarios = [
7 ("VALID (no padding)", 28, 3, 0, 1),
8 ("SAME (padding=1)", 28, 3, 1, 1),
9 ("Stride=2", 28, 3, 0, 2),
10 ("Large kernel", 28, 5, 0, 1),
11]
12
13print("\n" + "="*70)
14print("OUTPUT SIZE CALCULATIONS (Input: 28Γ28)")
15print("="*70)
16print(f"{'Scenario':<20} {'Kernel':>6} {'Pad':>4} {'Stride':>6} {'Output':>10}")
17print("-"*70)
18
19for name, input_s, kernel, pad, stride in scenarios:
20 output = calculate_output_size(input_s, kernel, pad, stride)
21 print(f"{name:<20} {kernel:>6} {pad:>4} {stride:>6} {output:>9}Γ{output}")
22
23print("="*70)What Convolution Accomplishes:
Learned vs Hand-Crafted Filters:
1# Traditional computer vision: Hand-design filters
2hand_crafted_filters = [
3 "Sobel edge detector",
4 "Gaussian blur",
5 "Laplacian sharpening"
6]
7
8# Modern deep learning: CNN learns optimal filters!
9print("β Traditional CV: Manually design filters")
10print("β
CNN: Automatically learn optimal filters from data")
11print("\nCNNs discover filters humans never thought of!")Why This Matters:
Pooling layers reduce spatial dimensions while retaining important features. They provide translation invariance and computational efficiency.
Problem: Feature maps can be large and computationally expensive.
Solution: Downsample spatial dimensions while keeping important information.
Max pooling takes the maximum value in each region.
1import numpy as np
2
3def max_pool2d(image, pool_size=2, stride=2):
4 """
5 Max pooling: Take maximum value in each pool region
6
7 Args:
8 image: Input feature map
9 pool_size: Size of pooling window
10 stride: Step size
11 """
12 i_height, i_width = image.shape
13
14 o_height = (i_height - pool_size) // stride + 1
15 o_width = (i_width - pool_size) // stride + 1
16
17 output = np.zeros((o_height, o_width))
18
19 for i in range(o_height):
20 for j in range(o_width):
21 region = image[i*stride:i*stride+pool_size,
22 j*stride:j*stride+pool_size]
23 output[i, j] = np.max(region)
24
25 return output
26
27# Example feature map
28feature_map = np.array([
29 [1, 3, 2, 4],
30 [5, 6, 1, 3],
31 [2, 1, 4, 2],
32 [3, 2, 5, 1]
33])
34
35print("Original Feature Map (4Γ4):")
36print(feature_map)
37print()
38
39# Apply max pooling
40pooled = max_pool2d(feature_map, pool_size=2, stride=2)
41
42print("After Max Pooling (2Γ2, stride=2):")
43print(pooled)
44print()
45
46print("How it works:")
47print("βββββββ¬ββββββ")
48print("β 1 3 β 2 4 β β ββββββ¬βββββ")
49print("β 5 6 β 1 3 β β β 6 β 4 β")
50print("βββββββΌββββββ€ ββββββΌβββββ€")
51print("β 2 1 β 4 2 β β β 3 β 5 β")
52print("β 3 2 β 5 1 β β ββββββ΄βββββ")
53print("βββββββ΄ββββββ")
54print()
55print("Takes max from each 2Γ2 region β Output is 2Γ2")Average pooling takes the mean value in each region.
1def average_pool2d(image, pool_size=2, stride=2):
2 """Average pooling: Take mean value in each region"""
3 i_height, i_width = image.shape
4
5 o_height = (i_height - pool_size) // stride + 1
6 o_width = (i_width - pool_size) // stride + 1
7
8 output = np.zeros((o_height, o_width))
9
10 for i in range(o_height):
11 for j in range(o_width):
12 region = image[i*stride:i*stride+pool_size,
13 j*stride:j*stride+pool_size]
14 output[i, j] = np.mean(region)
15
16 return output
17
18# Same feature map
19avg_pooled = average_pool2d(feature_map, pool_size=2, stride=2)
20
21print("\nAverage Pooling (2Γ2, stride=2):")
22print(avg_pooled)
23print()
24
25# Compare
26print("Comparison:")
27print(f" Max Pooling: {pooled.flatten()}")
28print(f" Average Pooling: {avg_pooled.flatten()}")
29print("\nMax pooling: Preserves strongest activations")
30print("Average pooling: Smooths the feature map")1import matplotlib.pyplot as plt
2
3# Create feature map with distinct patterns
4test_map = np.array([
5 [10, 50, 20, 80, 15, 25, 90, 30],
6 [40, 70, 60, 100, 35, 55, 75, 45],
7 [20, 30, 25, 35, 50, 60, 40, 50],
8 [55, 65, 75, 85, 70, 80, 60, 70],
9 [15, 25, 35, 45, 20, 30, 40, 50],
10 [50, 60, 70, 80, 55, 65, 75, 85],
11 [30, 40, 50, 60, 35, 45, 55, 65],
12 [70, 80, 90, 95, 75, 85, 90, 100]
13])
14
15# Apply pooling
16max_pooled = max_pool2d(test_map, pool_size=2, stride=2)
17avg_pooled = average_pool2d(test_map, pool_size=2, stride=2)
18
19fig, axes = plt.subplots(1, 3, figsize=(15, 5))
20
21# Original
22im1 = axes[0].imshow(test_map, cmap='viridis', interpolation='nearest')
23axes[0].set_title('Original Feature Map (8Γ8)', fontweight='bold', fontsize=13)
24axes[0].axis('off')
25plt.colorbar(im1, ax=axes[0], fraction=0.046)
26
27# Max pooled
28im2 = axes[1].imshow(max_pooled, cmap='viridis', interpolation='nearest')
29axes[1].set_title('Max Pooling (4Γ4)', fontweight='bold', fontsize=13)
30axes[1].axis('off')
31plt.colorbar(im2, ax=axes[1], fraction=0.046)
32
33# Average pooled
34im3 = axes[2].imshow(avg_pooled, cmap='viridis', interpolation='nearest')
35axes[2].set_title('Average Pooling (4Γ4)', fontweight='bold', fontsize=13)
36axes[2].axis('off')
37plt.colorbar(im3, ax=axes[2], fraction=0.046)
38
39plt.tight_layout()
40plt.show()
41
42print("\nβ
Spatial dimensions reduced by 50% (8Γ8 β 4Γ4)")
43print("β
Computational cost reduced by 75%!")1. Translation Invariance
Small shifts in input don't change pooled output.
1# Original image
2original = np.array([
3 [0, 0, 0, 0],
4 [0, 5, 5, 0],
5 [0, 5, 5, 0],
6 [0, 0, 0, 0]
7])
8
9# Slightly shifted
10shifted = np.array([
11 [0, 0, 0, 0],
12 [0, 0, 5, 5],
13 [0, 0, 5, 5],
14 [0, 0, 0, 0]
15])
16
17# Apply max pooling
18pool_original = max_pool2d(original, 2, 2)
19pool_shifted = max_pool2d(shifted, 2, 2)
20
21print("Original pooled:")
22print(pool_original)
23print("\nShifted pooled:")
24print(pool_shifted)
25print("\nβ
Both produce similar outputs β Translation invariance!")2. Computational Efficiency
1# Compare computational costs
2def compute_operations(layer_type, input_size, params):
3 """Estimate computational operations"""
4 if layer_type == "conv":
5 h, w, in_c = input_size
6 k_size, out_c = params
7 ops = h * w * k_size * k_size * in_c * out_c
8 elif layer_type == "pool":
9 h, w, c = input_size
10 pool_size = params
11 ops = h * w * c * pool_size * pool_size
12 return ops
13
14# Example: 224Γ224Γ64 feature map
15conv_ops = compute_operations("conv", (224, 224, 64), (3, 128))
16pool_ops = compute_operations("pool", (224, 224, 64), 2)
17
18print(f"Convolution operations: {conv_ops:,}")
19print(f"Pooling operations: {pool_ops:,}")
20print(f"\nPooling is {conv_ops // pool_ops}Γ faster than convolution!")3. Larger Receptive Fields
After pooling, each neuron "sees" a larger region of the original image.
1Without Pooling:
2Layer 1: 3Γ3 receptive field
3Layer 2: 5Γ5 receptive field
4Layer 3: 7Γ7 receptive field
5
6With Pooling (2Γ2 after each layer):
7Layer 1: 3Γ3 receptive field
8β Pool
9Layer 2: 10Γ10 receptive field
10β Pool
11Layer 3: 40Γ40 receptive field
12
13β
Pooling dramatically increases receptive field!1# Comparison table
2comparison = """
3ββββββββββββββββββββ¦ββββββββββββββββββββββββ¦ββββββββββββββββββββββββ
4β β MAX POOLING β AVERAGE POOLING β
5β βββββββββββββββββββ¬ββββββββββββββββββββββββ¬ββββββββββββββββββββββββ£
6β What it keeps β Strongest activation β Overall activation β
7β Best for β Sharp features β Smooth features β
8β Translation β Highly invariant β Moderately invariant β
9β Use case β Classification β Segmentation β
10β Popularity β βββββ (Default) β βββ (Special cases) β
11ββββββββββββββββββββ©ββββββββββββββββββββββββ©ββββββββββββββββββββββββ
12"""
13print(comparison)Global Average Pooling
Average entire feature map into single value per channel.
1def global_average_pool2d(feature_map):
2 """
3 Global Average Pooling: Reduce each channel to single value
4 Popular in modern architectures (ResNet, Inception)
5 """
6 # feature_map shape: (height, width, channels)
7 return np.mean(feature_map, axis=(0, 1))
8
9# Example: 7Γ7Γ512 feature map
10feature_map_3d = np.random.rand(7, 7, 512)
11global_pooled = global_average_pool2d(feature_map_3d)
12
13print(f"Input shape: {feature_map_3d.shape}")
14print(f"Output shape: {global_pooled.shape}")
15print("\nβ
Replaces fully-connected layers, reduces parameters!")Strided Convolution
Modern CNNs sometimes replace pooling with convolutions having stride > 1.
1print("\nModern Trend:")
2print(" Traditional: Conv (stride=1) β Max Pool (stride=2)")
3print(" Modern: Conv (stride=2) β [no pooling]")
4print("\nAdvantage: Learnable downsampling instead of fixed pooling")Why Pooling Matters:
Best Practices:
Pooling is essential for building deep, efficient CNN architectures!
Now let's combine convolution, pooling, and fully-connected layers into complete CNN architectures using TensorFlow/Keras.
1INPUT β [CONV β RELU β POOL] Γ N β FLATTEN β [FC β RELU] Γ M β SOFTMAXKey principles:
1import tensorflow as tf
2from tensorflow.keras import Sequential
3from tensorflow.keras.layers import (
4 Conv2D, MaxPooling2D, Flatten, Dense, Dropout
5)
6
7# Simple CNN for MNIST digit classification
8model = Sequential([
9 # Input: 28Γ28Γ1 (grayscale images)
10
11 # Block 1: Detect edges and simple patterns
12 Conv2D(32, kernel_size=(3, 3), activation='relu',
13 input_shape=(28, 28, 1), padding='same', name='conv1'),
14 MaxPooling2D(pool_size=(2, 2), name='pool1'),
15 # Output: 14Γ14Γ32
16
17 # Block 2: Combine edges into textures
18 Conv2D(64, kernel_size=(3, 3), activation='relu',
19 padding='same', name='conv2'),
20 MaxPooling2D(pool_size=(2, 2), name='pool2'),
21 # Output: 7Γ7Γ64
22
23 # Block 3: High-level features
24 Conv2D(64, kernel_size=(3, 3), activation='relu',
25 padding='same', name='conv3'),
26 # Output: 7Γ7Γ64
27
28 # Flatten to 1D for fully-connected layers
29 Flatten(name='flatten'),
30 # Output: 3136 (7Γ7Γ64)
31
32 # Fully-connected layers
33 Dense(64, activation='relu', name='fc1'),
34 Dropout(0.5, name='dropout'), # Prevent overfitting
35 Dense(10, activation='softmax', name='output')
36 # Output: 10 classes (digits 0-9)
37], name='SimpleCNN')
38
39# Display architecture
40model.summary()
41
42# Output shapes at each layer:
43# conv1: (None, 28, 28, 32)
44# pool1: (None, 14, 14, 32)
45# conv2: (None, 14, 14, 64)
46# pool2: (None, 7, 7, 64)
47# conv3: (None, 7, 7, 64)
48# flatten: (None, 3136)
49# fc1: (None, 64)
50# dropout: (None, 64)
51# output: (None, 10)1. Convolutional Layers
1# Conv2D parameters explained
2Conv2D(
3 filters=32, # Number of filters (output channels)
4 kernel_size=(3, 3), # Filter size (height, width)
5 strides=(1, 1), # Step size (default=1)
6 padding='same', # 'same' preserves size, 'valid' shrinks
7 activation='relu', # Activation function
8 use_bias=True, # Add bias term (default=True)
9 kernel_initializer='glorot_uniform' # Weight initialization
10)
11
12# Parameter count: (kernel_h Γ kernel_w Γ in_channels + 1) Γ out_filters
13# Example: (3 Γ 3 Γ 1 + 1) Γ 32 = 320 parameters2. Pooling Layers
1# MaxPooling2D parameters
2MaxPooling2D(
3 pool_size=(2, 2), # Pooling window size
4 strides=None, # Default: same as pool_size
5 padding='valid' # 'valid' = no padding
6)
7
8# Effect: Reduces spatial dimensions by factor of pool_size
9# 28Γ28 β 14Γ14 (with pool_size=2)
10# No trainable parameters!3. Dropout for Regularization
1# Dropout randomly deactivates neurons during training
2Dropout(
3 rate=0.5, # Probability of dropping a neuron (0.5 = 50%)
4)
5
6# Benefits:
7# - Prevents co-adaptation of neurons
8# - Reduces overfitting
9# - Creates ensemble effect1def calculate_conv_output(input_size, kernel_size, stride, padding):
2 """Calculate output size after convolution"""
3 if padding == 'same':
4 return (input_size + stride - 1) // stride
5 else: # padding == 'valid'
6 return (input_size - kernel_size + stride) // stride
7
8def calculate_cnn_dims(input_shape):
9 """
10 Track dimensions through CNN layers
11 Input: (height, width, channels)
12 """
13 h, w, c = input_shape
14
15 print(f"Input: {h}Γ{w}Γ{c}")
16 print()
17
18 # Conv1: 3Γ3, 32 filters, padding='same'
19 h1, w1, c1 = h, w, 32
20 print(f"After Conv1 (3Γ3, 32 filters, same): {h1}Γ{w1}Γ{c1}")
21
22 # Pool1: 2Γ2
23 h1, w1 = h1 // 2, w1 // 2
24 print(f"After Pool1 (2Γ2): {h1}Γ{w1}Γ{c1}")
25 print()
26
27 # Conv2: 3Γ3, 64 filters, padding='same'
28 c2 = 64
29 print(f"After Conv2 (3Γ3, 64 filters, same): {h1}Γ{w1}Γ{c2}")
30
31 # Pool2: 2Γ2
32 h2, w2 = h1 // 2, w1 // 2
33 print(f"After Pool2 (2Γ2): {h2}Γ{w2}Γ{c2}")
34 print()
35
36 # Conv3: 3Γ3, 64 filters, padding='same'
37 print(f"After Conv3 (3Γ3, 64 filters, same): {h2}Γ{w2}Γ{c2}")
38 print()
39
40 # Flatten
41 flattened = h2 * w2 * c2
42 print(f"After Flatten: {flattened}")
43 print()
44
45 return flattened
46
47# Example: MNIST (28Γ28Γ1)
48calculate_cnn_dims((28, 28, 1))Pattern 1: VGG-style (Repetitive blocks)
1# Inspired by VGGNet
2model_vgg_style = Sequential([
3 # Block 1: 2 conv layers
4 Conv2D(64, (3,3), activation='relu', padding='same', input_shape=(224, 224, 3)),
5 Conv2D(64, (3,3), activation='relu', padding='same'),
6 MaxPooling2D((2,2)),
7
8 # Block 2: 2 conv layers, double filters
9 Conv2D(128, (3,3), activation='relu', padding='same'),
10 Conv2D(128, (3,3), activation='relu', padding='same'),
11 MaxPooling2D((2,2)),
12
13 # Block 3: 3 conv layers, double filters again
14 Conv2D(256, (3,3), activation='relu', padding='same'),
15 Conv2D(256, (3,3), activation='relu', padding='same'),
16 Conv2D(256, (3,3), activation='relu', padding='same'),
17 MaxPooling2D((2,2)),
18
19 # Classifier
20 Flatten(),
21 Dense(4096, activation='relu'),
22 Dropout(0.5),
23 Dense(4096, activation='relu'),
24 Dropout(0.5),
25 Dense(1000, activation='softmax')
26], name='VGG_Style')
27
28print("VGG Pattern: Depth doubles, spatial dimensions halve at each block")Pattern 2: Modern Efficient (Smaller FCs)
1# Modern efficient design
2model_modern = Sequential([
3 # Feature extraction
4 Conv2D(32, (3,3), activation='relu', padding='same', input_shape=(32, 32, 3)),
5 Conv2D(32, (3,3), activation='relu', padding='same'),
6 MaxPooling2D((2,2)),
7 Dropout(0.25),
8
9 Conv2D(64, (3,3), activation='relu', padding='same'),
10 Conv2D(64, (3,3), activation='relu', padding='same'),
11 MaxPooling2D((2,2)),
12 Dropout(0.25),
13
14 Conv2D(128, (3,3), activation='relu', padding='same'),
15 Conv2D(128, (3,3), activation='relu', padding='same'),
16 MaxPooling2D((2,2)),
17 Dropout(0.25),
18
19 # Global pooling instead of Flatten + large FC
20 tf.keras.layers.GlobalAveragePooling2D(),
21
22 # Small classifier
23 Dense(128, activation='relu'),
24 Dropout(0.5),
25 Dense(10, activation='softmax')
26], name='Modern_Efficient')
27
28print("Modern Pattern: Global pooling reduces parameters dramatically")1. Filter Progression: Double at Each Stage
1Input β 32 β 64 β 128 β 256 β 512Intuition: As spatial dimensions shrink, increase depth to maintain capacity.
2. Receptive Field Growth
1def calculate_receptive_field(layers):
2 """
3 Calculate receptive field of CNN
4 layers: list of (kernel_size, stride) tuples
5 """
6 rf = 1 # Start with 1Γ1
7 stride_product = 1
8
9 for k, s in layers:
10 rf += (k - 1) * stride_product
11 stride_product *= s
12
13 return rf
14
15# Example CNN
16layers = [
17 (3, 1), # Conv1
18 (2, 2), # Pool1
19 (3, 1), # Conv2
20 (2, 2), # Pool2
21 (3, 1), # Conv3
22]
23
24rf = calculate_receptive_field(layers)
25print(f"Receptive field: {rf}Γ{rf}")
26print("\nEach output neuron 'sees' this much of the input image")3. Parameter Budget
1def count_parameters(model):
2 """Count trainable parameters in each layer"""
3 print("\n" + "="*70)
4 print("PARAMETER COUNT BY LAYER")
5 print("="*70)
6 print(f"{'Layer':<20} {'Type':<15} {'Parameters':>15} {'Output Shape':<20}")
7 print("-"*70)
8
9 total = 0
10 for layer in model.layers:
11 params = layer.count_params()
12 total += params
13 output_shape = str(layer.output_shape)
14 print(f"{layer.name:<20} {layer.__class__.__name__:<15} {params:>15,} {output_shape:<20}")
15
16 print("-"*70)
17 print(f"{'TOTAL':<20} {'':<15} {total:>15,}")
18 print("="*70)
19 return total
20
21# Analyze model
22total_params = count_parameters(model)1# Compile model
2model.compile(
3 optimizer='adam',
4 loss='sparse_categorical_crossentropy',
5 metrics=['accuracy']
6)
7
8# Load MNIST dataset
9from tensorflow.keras.datasets import mnist
10
11(X_train, y_train), (X_test, y_test) = mnist.load_data()
12
13# Preprocess
14X_train = X_train.reshape(-1, 28, 28, 1).astype('float32') / 255
15X_test = X_test.reshape(-1, 28, 28, 1).astype('float32') / 255
16
17print(f"Training data: {X_train.shape}")
18print(f"Testing data: {X_test.shape}")
19
20# Train model
21history = model.fit(
22 X_train, y_train,
23 batch_size=128,
24 epochs=10,
25 validation_split=0.2,
26 verbose=1
27)
28
29# Evaluate
30test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
31print(f"\nTest Accuracy: {test_acc * 100:.2f}%")Architecture Design Guidelines:
Common Mistakes to Avoid:
1# β Too many parameters in FC layers
2Dense(4096) # Huge! 7Γ7Γ512 β 4096 = 102M parameters
3
4# β
Use global pooling instead
5GlobalAveragePooling2D() # 0 parameters!
6
7# β Not enough depth in conv layers
8Conv2D(16, (3,3)) # Too shallow for complex patterns
9
10# β
Increase filters progressively
11Conv2D(32, (3,3)) β Conv2D(64, (3,3)) β Conv2D(128, (3,3))You now understand how to build production-ready CNN architectures!
CNNs learn hierarchical features automatically. Visualizing these learned representations provides deep insights into how the network "sees" images.
1Layer 1 (Early): Layer 2 (Middle): Layer 3 (Deep):
2ββ Edges ββ Textures ββ Object parts
3β β’ Horizontal β β’ Wood grain β β’ Eyes
4β β’ Vertical β β’ Fabric β β’ Wheels
5β β’ Diagonal β β’ Fur β β’ Windows
6β β’ Curves β β’ Metal β β’ Doors
7β β β
8ββ Colors ββ Patterns ββ Complex shapes
9 β’ Red/green β’ Stripes β’ Faces
10 β’ Blue/yellow β’ Checkers β’ Cars
11 β’ Gradients β’ Polka dots β’ Animals1import matplotlib.pyplot as plt
2import numpy as np
3
4def visualize_conv_filters(model, layer_name, n_filters=8):
5 """
6 Visualize learned convolutional filters
7
8 Args:
9 model: Trained CNN
10 layer_name: Name of conv layer to visualize
11 n_filters: Number of filters to show
12 """
13 # Get layer
14 layer = model.get_layer(layer_name)
15 filters, biases = layer.get_weights()
16
17 print(f"Filter shape: {filters.shape}")
18 print(f" (height, width, in_channels, out_filters)")
19 print()
20
21 # Normalize filters to [0, 1] for visualization
22 f_min, f_max = filters.min(), filters.max()
23 filters = (filters - f_min) / (f_max - f_min)
24
25 # Plot first n_filters
26 n_cols = 8
27 n_rows = (n_filters + n_cols - 1) // n_cols
28
29 fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 2*n_rows))
30 axes = axes.flatten() if n_rows > 1 else [axes]
31
32 for i in range(min(n_filters, filters.shape[3])):
33 # Get filter for first input channel
34 f = filters[:, :, 0, i]
35
36 axes[i].imshow(f, cmap='viridis')
37 axes[i].set_title(f'Filter {i+1}', fontsize=9)
38 axes[i].axis('off')
39
40 # Hide extra subplots
41 for i in range(n_filters, len(axes)):
42 axes[i].axis('off')
43
44 plt.suptitle(f'Learned Filters: {layer_name}',
45 fontweight='bold', fontsize=14)
46 plt.tight_layout()
47 plt.show()
48
49# Visualize first layer filters
50visualize_conv_filters(model, 'conv1', n_filters=32)
51
52print("Interpretation:")
53print(" β’ Bright regions: Positive weights (detect bright pixels)")
54print(" β’ Dark regions: Negative weights (detect dark pixels)")
55print(" β’ First layer learns edge detectors automatically!")1def visualize_feature_maps(model, layer_names, input_image):
2 """
3 Visualize intermediate feature maps for given input
4
5 Args:
6 model: Trained CNN
7 layer_names: List of layer names to visualize
8 input_image: Input image (preprocessed)
9 """
10 # Create model that outputs intermediate layers
11 outputs = [model.get_layer(name).output for name in layer_names]
12 visualization_model = tf.keras.Model(inputs=model.input, outputs=outputs)
13
14 # Get activations
15 feature_maps = visualization_model.predict(input_image[np.newaxis, ...])
16
17 # Plot feature maps for each layer
18 for layer_name, fmap in zip(layer_names, feature_maps):
19 # fmap shape: (1, height, width, n_filters)
20 fmap = fmap[0] # Remove batch dimension
21 n_filters = fmap.shape[-1]
22
23 # Show first 16 feature maps
24 n_show = min(16, n_filters)
25 n_cols = 8
26 n_rows = 2
27
28 fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4))
29
30 for i, ax in enumerate(axes.flat):
31 if i < n_show:
32 # Display feature map
33 ax.imshow(fmap[:, :, i], cmap='viridis')
34 ax.set_title(f'Filter {i+1}', fontsize=9)
35 ax.axis('off')
36
37 plt.suptitle(f'Feature Maps: {layer_name} ({fmap.shape})',
38 fontweight='bold', fontsize=14)
39 plt.tight_layout()
40 plt.show()
41
42 print(f"{layer_name}:")
43 print(f" Shape: {fmap.shape}")
44 print(f" Active filters: {np.sum(fmap.max(axis=(0,1)) > 0)} / {n_filters}")
45 print(f" Mean activation: {fmap.mean():.4f}")
46 print()
47
48# Example: Visualize activations for a test image
49test_image = X_test[0] # Single MNIST digit
50
51# Visualize progression through network
52visualize_feature_maps(
53 model,
54 layer_names=['conv1', 'conv2', 'conv3'],
55 input_image=test_image
56)1def visualize_filter_activation(model, layer_name, filter_idx, size=(128, 128), steps=100):
2 """
3 Generate image that maximally activates a specific filter
4 Uses gradient ascent to optimize input image
5
6 This shows what pattern each filter is looking for!
7 """
8 # Create model that outputs target layer
9 layer_output = model.get_layer(layer_name).output
10 activation_model = tf.keras.Model(inputs=model.input, outputs=layer_output)
11
12 # Random starting image
13 input_image = tf.Variable(tf.random.normal((1, size[0], size[1], 1)))
14
15 # Optimization loop
16 for step in range(steps):
17 with tf.GradientTape() as tape:
18 # Forward pass
19 activation = activation_model(input_image)
20
21 # Loss: maximize activation of target filter
22 loss = -tf.reduce_mean(activation[:, :, :, filter_idx])
23
24 # Compute gradient
25 gradients = tape.gradient(loss, input_image)
26
27 # Normalize gradients
28 gradients = tf.math.l2_normalize(gradients)
29
30 # Update image
31 input_image.assign_add(gradients * 0.1)
32
33 # Normalize for visualization
34 img = input_image.numpy()[0, :, :, 0]
35 img = (img - img.min()) / (img.max() - img.min())
36
37 return img
38
39# Generate patterns that activate different filters
40fig, axes = plt.subplots(2, 4, figsize=(12, 6))
41
42for idx, ax in enumerate(axes.flat):
43 pattern = visualize_filter_activation(model, 'conv1', filter_idx=idx)
44 ax.imshow(pattern, cmap='gray')
45 ax.set_title(f'Filter {idx+1} Pattern', fontsize=10)
46 ax.axis('off')
47
48plt.suptitle('Patterns That Maximally Activate Each Filter',
49 fontweight='bold', fontsize=14)
50plt.tight_layout()
51plt.show()
52
53print("\nInterpretation:")
54print(" These are the 'ideal' patterns each filter is looking for!")
55print(" Notice edge detectors, texture patterns, etc.")Visualize which image regions contribute to classification decision.
1def generate_class_activation_map(model, image, class_idx, last_conv_layer_name):
2 """
3 Generate Class Activation Map (CAM)
4 Shows which image regions were important for classification
5
6 Args:
7 model: Trained CNN
8 image: Input image
9 class_idx: Target class index
10 last_conv_layer_name: Name of last convolutional layer
11 """
12 # Create model that outputs both predictions and last conv layer
13 grad_model = tf.keras.Model(
14 inputs=model.input,
15 outputs=[model.get_layer(last_conv_layer_name).output, model.output]
16 )
17
18 with tf.GradientTape() as tape:
19 conv_outputs, predictions = grad_model(image[np.newaxis, ...])
20 loss = predictions[:, class_idx]
21
22 # Gradient of class score w.r.t. last conv layer
23 grads = tape.gradient(loss, conv_outputs)
24
25 # Global average pooling of gradients
26 pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
27
28 # Multiply each channel by gradient importance
29 conv_outputs = conv_outputs.numpy()[0]
30 pooled_grads = pooled_grads.numpy()
31
32 for i in range(pooled_grads.shape[0]):
33 conv_outputs[:, :, i] *= pooled_grads[i]
34
35 # Channel-wise sum
36 heatmap = np.sum(conv_outputs, axis=-1)
37
38 # Normalize
39 heatmap = np.maximum(heatmap, 0) # ReLU
40 heatmap /= np.max(heatmap) if np.max(heatmap) != 0 else 1
41
42 return heatmap
43
44# Generate and visualize CAM
45test_image = X_test[0]
46predicted_class = np.argmax(model.predict(test_image[np.newaxis, ...]))
47
48heatmap = generate_class_activation_map(
49 model, test_image, predicted_class, 'conv3'
50)
51
52# Visualize
53fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))
54
55# Original image
56ax1.imshow(test_image.squeeze(), cmap='gray')
57ax1.set_title('Original Image', fontweight='bold')
58ax1.axis('off')
59
60# Heatmap
61ax2.imshow(heatmap, cmap='jet')
62ax2.set_title('Class Activation Map', fontweight='bold')
63ax2.axis('off')
64
65# Overlay
66import cv2
67heatmap_resized = cv2.resize(heatmap, (28, 28))
68ax3.imshow(test_image.squeeze(), cmap='gray')
69ax3.imshow(heatmap_resized, cmap='jet', alpha=0.5)
70ax3.set_title(f'Overlay (Class: {predicted_class})', fontweight='bold')
71ax3.axis('off')
72
73plt.tight_layout()
74plt.show()
75
76print("\nβ
Heatmap shows which regions influenced the prediction!")
77print(" Bright areas = important for classification")1def deep_dream(model, image, layer_name, steps=50, step_size=0.01):
2 """
3 DeepDream: Amplify patterns the network sees
4 Makes network hallucinate features in the image
5 """
6 # Create model that outputs target layer
7 layer_output = model.get_layer(layer_name).output
8 dream_model = tf.keras.Model(inputs=model.input, outputs=layer_output)
9
10 img = tf.Variable(image)
11
12 for step in range(steps):
13 with tf.GradientTape() as tape:
14 activation = dream_model(img)
15 loss = tf.reduce_mean(activation)
16
17 gradients = tape.gradient(loss, img)
18 gradients = tf.math.l2_normalize(gradients)
19 img.assign_add(gradients * step_size)
20
21 return img.numpy()
22
23# Apply deep dream
24dreamed = deep_dream(model, test_image[np.newaxis, ...], 'conv2', steps=100)
25
26fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
27
28ax1.imshow(test_image.squeeze(), cmap='gray')
29ax1.set_title('Original', fontweight='bold')
30ax1.axis('off')
31
32ax2.imshow(dreamed[0].squeeze(), cmap='gray')
33ax2.set_title('After Deep Dream', fontweight='bold')
34ax2.axis('off')
35
36plt.tight_layout()
37plt.show()
38
39print("\nDeep Dream amplifies patterns the network has learned!")What We Learn:
Why Visualization Matters:
1visualization_benefits = """
2β
Debugging: See if network learns meaningful features
3β
Interpretability: Understand model decisions
4β
Trust: Verify network focuses on relevant regions
5β
Bias Detection: Identify spurious correlations
6β
Research: Discover how deep learning actually works
7"""
8print(visualization_benefits)Surprising Discoveries:
Visualization bridges the "black box" gap and reveals the inner workings of CNNs!
Implement a full CNN pipeline: data loading, preprocessing, architecture design, training, evaluation, and visualization. Train on CIFAR-10 dataset (60,000 color images across 10 classes) and achieve production-ready performance.
1# Complete CNN Pipeline: CIFAR-10 Image Classification
2# 60,000 32Γ32 color images across 10 classes
3
4import numpy as np
5import matplotlib.pyplot as plt
6import tensorflow as tf
7from tensorflow.keras import Sequential
8from tensorflow.keras.layers import (
9 Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization
10)
11from tensorflow.keras.datasets import cifar10
12from tensorflow.keras.utils import to_categorical
13
14print("=" * 80)
15print("CNN PIPELINE: CIFAR-10 IMAGE CLASSIFICATION")
16print("=" * 80)
17print()
18
19# ============================================================================
20# PART 1: Load and Explore Data
21# ============================================================================
22
23print("PART 1: Loading CIFAR-10 Dataset")
24print("-" * 80)
25
26# Load CIFAR-10
27(X_train, y_train), (X_test, y_test) = cifar10.load_data()
28
29# Class names
30class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',
31 'dog', 'frog', 'horse', 'ship', 'truck']
32
33print(f"Training data: {X_train.shape}")
34print(f"Training labels: {y_train.shape}")
35print(f"Test data: {X_test.shape}")
36print(f"Test labels: {y_test.shape}")
37print(f"\nClasses: {class_names}")
38print()
39
40# Visualize samples
41fig, axes = plt.subplots(4, 10, figsize=(20, 8))
42fig.suptitle('CIFAR-10 Sample Images (10 Classes)', fontweight='bold', fontsize=16)
43
44for class_idx in range(10):
45 # Find 4 examples of each class
46 indices = np.where(y_train.flatten() == class_idx)[0][:4]
47
48 for i, idx in enumerate(indices):
49 axes[i, class_idx].imshow(X_train[idx])
50 if i == 0:
51 axes[i, class_idx].set_title(class_names[class_idx],
52 fontweight='bold', fontsize=11)
53 axes[i, class_idx].axis('off')
54
55plt.tight_layout()
56plt.show()
57
58# ============================================================================
59# PART 2: Data Preprocessing
60# ============================================================================
61
62print("\nPART 2: Data Preprocessing")
63print("-" * 80)
64
65# Normalize pixel values to [0, 1]
66X_train_normalized = X_train.astype('float32') / 255.0
67X_test_normalized = X_test.astype('float32') / 255.0
68
69print(f"Original range: [{X_train.min()}, {X_train.max()}]")
70print(f"Normalized range: [{X_train_normalized.min():.2f}, {X_train_normalized.max():.2f}]")
71print()
72
73# Convert labels to categorical (one-hot encoding)
74y_train_categorical = to_categorical(y_train, 10)
75y_test_categorical = to_categorical(y_test, 10)
76
77print(f"Label before: {y_train[0]} β '{class_names[y_train[0][0]]}'")
78print(f"Label after: {y_train_categorical[0]}")
79print(" [airplane, auto, bird, cat, deer, dog, frog, horse, ship, truck]")
80print()
81
82# Data augmentation (optional - improves performance)
83from tensorflow.keras.preprocessing.image import ImageDataGenerator
84
85datagen = ImageDataGenerator(
86 rotation_range=15, # Rotate Β±15 degrees
87 width_shift_range=0.1, # Shift horizontally 10%
88 height_shift_range=0.1, # Shift vertically 10%
89 horizontal_flip=True, # Random horizontal flip
90 fill_mode='nearest'
91)
92
93print("Data Augmentation Enabled:")
94print(" β’ Random rotations (Β±15Β°)")
95print(" β’ Random shifts (10%)")
96print(" β’ Random horizontal flips")
97print()
98
99# Visualize augmentation
100sample_image = X_train_normalized[0:1]
101augmented_images = [datagen.flow(sample_image, batch_size=1)[0][0] for _ in range(9)]
102
103fig, axes = plt.subplots(3, 3, figsize=(10, 10))
104fig.suptitle('Data Augmentation Examples', fontweight='bold', fontsize=14)
105
106for ax, aug_img in zip(axes.flat, augmented_images):
107 ax.imshow(aug_img)
108 ax.axis('off')
109
110plt.tight_layout()
111plt.show()
112
113# ============================================================================
114# PART 3: Build CNN Architecture
115# ============================================================================
116
117print("\nPART 3: Building CNN Architecture")
118print("-" * 80)
119
120model = Sequential([
121 # Input: 32Γ32Γ3 (RGB images)
122
123 # Block 1: Learn basic features (edges, colors)
124 Conv2D(32, (3, 3), activation='relu', padding='same',
125 input_shape=(32, 32, 3), name='conv1_1'),
126 BatchNormalization(name='bn1_1'),
127 Conv2D(32, (3, 3), activation='relu', padding='same', name='conv1_2'),
128 BatchNormalization(name='bn1_2'),
129 MaxPooling2D((2, 2), name='pool1'),
130 Dropout(0.25, name='dropout1'),
131 # Output: 16Γ16Γ32
132
133 # Block 2: Learn mid-level features (textures, patterns)
134 Conv2D(64, (3, 3), activation='relu', padding='same', name='conv2_1'),
135 BatchNormalization(name='bn2_1'),
136 Conv2D(64, (3, 3), activation='relu', padding='same', name='conv2_2'),
137 BatchNormalization(name='bn2_2'),
138 MaxPooling2D((2, 2), name='pool2'),
139 Dropout(0.25, name='dropout2'),
140 # Output: 8Γ8Γ64
141
142 # Block 3: Learn high-level features (object parts)
143 Conv2D(128, (3, 3), activation='relu', padding='same', name='conv3_1'),
144 BatchNormalization(name='bn3_1'),
145 Conv2D(128, (3, 3), activation='relu', padding='same', name='conv3_2'),
146 BatchNormalization(name='bn3_2'),
147 MaxPooling2D((2, 2), name='pool3'),
148 Dropout(0.25, name='dropout3'),
149 # Output: 4Γ4Γ128
150
151 # Classifier
152 Flatten(name='flatten'),
153 Dense(256, activation='relu', name='fc1'),
154 BatchNormalization(name='bn_fc'),
155 Dropout(0.5, name='dropout_fc'),
156 Dense(10, activation='softmax', name='output')
157], name='CIFAR10_CNN')
158
159# Display architecture
160print("\nModel Architecture:")
161model.summary()
162print()
163
164# Count parameters per layer type
165conv_params = sum([layer.count_params() for layer in model.layers
166 if isinstance(layer, Conv2D)])
167bn_params = sum([layer.count_params() for layer in model.layers
168 if isinstance(layer, BatchNormalization)])
169dense_params = sum([layer.count_params() for layer in model.layers
170 if isinstance(layer, Dense)])
171
172print("Parameter Breakdown:")
173print(f" Convolutional layers: {conv_params:,}")
174print(f" Batch normalization: {bn_params:,}")
175print(f" Dense layers: {dense_params:,}")
176print(f" Total: {model.count_params():,}")
177print()
178
179# ============================================================================
180# PART 4: Compile and Train Model
181# ============================================================================
182
183print("PART 4: Training CNN")
184print("-" * 80)
185
186# Compile model
187model.compile(
188 optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
189 loss='categorical_crossentropy',
190 metrics=['accuracy']
191)
192
193# Callbacks
194from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
195
196early_stop = EarlyStopping(
197 monitor='val_loss',
198 patience=10,
199 restore_best_weights=True,
200 verbose=1
201)
202
203reduce_lr = ReduceLROnPlateau(
204 monitor='val_loss',
205 factor=0.5,
206 patience=5,
207 min_lr=1e-7,
208 verbose=1
209)
210
211print("Training Configuration:")
212print(" Optimizer: Adam (lr=0.001)")
213print(" Loss: Categorical Crossentropy")
214print(" Batch size: 64")
215print(" Epochs: 50 (with early stopping)")
216print(" Callbacks: Early stopping, learning rate reduction")
217print()
218
219# Train model
220print("Starting training...")
221print("-" * 80)
222
223history = model.fit(
224 datagen.flow(X_train_normalized, y_train_categorical, batch_size=64),
225 epochs=50,
226 validation_data=(X_test_normalized, y_test_categorical),
227 callbacks=[early_stop, reduce_lr],
228 verbose=1
229)
230
231print("\nβ
Training complete!")
232print()
233
234# ============================================================================
235# PART 5: Evaluate Performance
236# ============================================================================
237
238print("PART 5: Model Evaluation")
239print("-" * 80)
240
241# Evaluate on test set
242test_loss, test_accuracy = model.evaluate(X_test_normalized, y_test_categorical,
243 verbose=0)
244
245print(f"Test Loss: {test_loss:.4f}")
246print(f"Test Accuracy: {test_accuracy * 100:.2f}%")
247print()
248
249# Make predictions
250predictions = model.predict(X_test_normalized, verbose=0)
251predicted_classes = np.argmax(predictions, axis=1)
252true_classes = y_test.flatten()
253
254# Confusion matrix
255from sklearn.metrics import confusion_matrix, classification_report
256import seaborn as sns
257
258cm = confusion_matrix(true_classes, predicted_classes)
259
260plt.figure(figsize=(12, 10))
261sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
262 xticklabels=class_names, yticklabels=class_names,
263 cbar_kws={'label': 'Count'})
264plt.title('Confusion Matrix: CIFAR-10 CNN', fontweight='bold', fontsize=14)
265plt.xlabel('Predicted Class', fontweight='bold')
266plt.ylabel('True Class', fontweight='bold')
267plt.xticks(rotation=45, ha='right')
268plt.yticks(rotation=0)
269plt.tight_layout()
270plt.show()
271
272# Per-class accuracy
273print("\nPer-Class Accuracy:")
274print("-" * 50)
275for i, class_name in enumerate(class_names):
276 class_correct = cm[i, i]
277 class_total = cm[i, :].sum()
278 class_acc = 100 * class_correct / class_total if class_total > 0 else 0
279 print(f"{class_name:12} {class_acc:6.2f}% ({class_correct}/{class_total})")
280print()
281
282# Classification report
283print("Detailed Classification Report:")
284print("-" * 80)
285print(classification_report(true_classes, predicted_classes,
286 target_names=class_names, digits=4))
287
288# ============================================================================
289# PART 6: Visualize Training History
290# ============================================================================
291
292print("PART 6: Training Visualization")
293print("-" * 80)
294
295# Plot training curves
296fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
297
298# Loss curves
299epochs = range(1, len(history.history['loss']) + 1)
300
301ax1.plot(epochs, history.history['loss'], 'b-', linewidth=2, label='Training Loss')
302ax1.plot(epochs, history.history['val_loss'], 'r-', linewidth=2, label='Validation Loss')
303ax1.set_xlabel('Epoch', fontweight='bold', fontsize=12)
304ax1.set_ylabel('Loss', fontweight='bold', fontsize=12)
305ax1.set_title('Training and Validation Loss', fontweight='bold', fontsize=14)
306ax1.legend(fontsize=11)
307ax1.grid(True, alpha=0.3)
308
309# Accuracy curves
310ax2.plot(epochs, [acc * 100 for acc in history.history['accuracy']],
311 'b-', linewidth=2, label='Training Accuracy')
312ax2.plot(epochs, [acc * 100 for acc in history.history['val_accuracy']],
313 'r-', linewidth=2, label='Validation Accuracy')
314ax2.set_xlabel('Epoch', fontweight='bold', fontsize=12)
315ax2.set_ylabel('Accuracy (%)', fontweight='bold', fontsize=12)
316ax2.set_title('Training and Validation Accuracy', fontweight='bold', fontsize=14)
317ax2.legend(fontsize=11)
318ax2.grid(True, alpha=0.3)
319
320plt.tight_layout()
321plt.show()
322
323# ============================================================================
324# PART 7: Visualize Predictions
325# ============================================================================
326
327print("\nPART 7: Sample Predictions")
328print("-" * 80)
329
330# Show correct and incorrect predictions
331fig, axes = plt.subplots(4, 8, figsize=(20, 10))
332
333correct_indices = np.where(predicted_classes == true_classes)[0]
334incorrect_indices = np.where(predicted_classes != true_classes)[0]
335
336# 16 correct, 16 incorrect
337sample_correct = np.random.choice(correct_indices, 16, replace=False)
338sample_incorrect = np.random.choice(incorrect_indices, 16, replace=False)
339
340for idx, ax_idx in enumerate(range(16)):
341 row = ax_idx // 8
342 col = ax_idx % 8
343
344 # Correct prediction
345 img_idx = sample_correct[idx]
346 axes[row, col].imshow(X_test[img_idx])
347 true_label = class_names[true_classes[img_idx]]
348 pred_label = class_names[predicted_classes[img_idx]]
349 confidence = predictions[img_idx][predicted_classes[img_idx]] * 100
350
351 axes[row, col].set_title(f'β {pred_label}\n{confidence:.1f}%',
352 color='green', fontweight='bold', fontsize=9)
353 axes[row, col].axis('off')
354
355for idx, ax_idx in enumerate(range(16, 32)):
356 row = ax_idx // 8
357 col = ax_idx % 8
358
359 # Incorrect prediction
360 img_idx = sample_incorrect[idx]
361 axes[row, col].imshow(X_test[img_idx])
362 true_label = class_names[true_classes[img_idx]]
363 pred_label = class_names[predicted_classes[img_idx]]
364 confidence = predictions[img_idx][predicted_classes[img_idx]] * 100
365
366 axes[row, col].set_title(f'β {pred_label} ({true_label})\n{confidence:.1f}%',
367 color='red', fontweight='bold', fontsize=9)
368 axes[row, col].axis('off')
369
370plt.suptitle('CNN Predictions: β Correct (top 2 rows) | β Incorrect (bottom 2 rows)',
371 fontweight='bold', fontsize=16)
372plt.tight_layout()
373plt.show()
374
375# ============================================================================
376# SUMMARY
377# ============================================================================
378
379print("\n" + "=" * 80)
380print("CNN TRAINING SUMMARY")
381print("=" * 80)
382print()
383
384print("π DATASET")
385print(f" β’ Training samples: {len(X_train):,}")
386print(f" β’ Test samples: {len(X_test):,}")
387print(f" β’ Classes: {len(class_names)}")
388print(f" β’ Image size: 32Γ32Γ3 (RGB)")
389print()
390
391print("ποΈ ARCHITECTURE")
392print(f" β’ Convolutional blocks: 3")
393print(f" β’ Total layers: {len(model.layers)}")
394print(f" β’ Parameters: {model.count_params():,}")
395print(f" β’ Techniques: Batch normalization, dropout, data augmentation")
396print()
397
398print("π― PERFORMANCE")
399print(f" β’ Test Accuracy: {test_accuracy * 100:.2f}%")
400print(f" β’ Test Loss: {test_loss:.4f}")
401print(f" β’ Training epochs: {len(history.history['loss'])}")
402print()
403
404print("π KEY TECHNIQUES DEMONSTRATED")
405print(" 1οΈβ£ Convolutional layers: Automatic feature learning")
406print(" 2οΈβ£ Pooling: Spatial downsampling, translation invariance")
407print(" 3οΈβ£ Batch normalization: Stabilizes training, faster convergence")
408print(" 4οΈβ£ Dropout: Prevents overfitting")
409print(" 5οΈβ£ Data augmentation: Improves generalization")
410print(" 6οΈβ£ Learning rate scheduling: Adaptive optimization")
411print(" 7οΈβ£ Early stopping: Prevents overtraining")
412print()
413
414print("=" * 80)
415print("π You've built a production-ready CNN!")
416print(" Next: Learn advanced architectures (ResNet, Inception, EfficientNet)")
417print("=" * 80)This comprehensive example demonstrates a complete CNN pipeline from data loading to evaluation on CIFAR-10, a challenging real-world dataset.
1. Real-World Dataset
CIFAR-10 is significantly harder than MNIST:
2. Modern CNN Architecture
Our architecture incorporates best practices:
1Input (32Γ32Γ3)
2 β
3[Conv 32 β BN β Conv 32 β BN β Pool β Dropout] Block 1
4 β (16Γ16Γ32)
5[Conv 64 β BN β Conv 64 β BN β Pool β Dropout] Block 2
6 β (8Γ8Γ64)
7[Conv 128 β BN β Conv 128 β BN β Pool β Dropout] Block 3
8 β (4Γ4Γ128)
9[Flatten β Dense 256 β BN β Dropout β Dense 10] Classifier
10 β
11Output (10 classes)Key Design Choices:
3. Advanced Training Techniques
Data Augmentation:
1ImageDataGenerator(
2 rotation_range=15, # Handles rotated objects
3 width_shift_range=0.1, # Handles position variation
4 height_shift_range=0.1,
5 horizontal_flip=True # Doubles effective dataset size
6)Callbacks:
4. Comprehensive Evaluation
Performance Benchmarks:
1Without augmentation: ~75-80% accuracy
2With augmentation: ~85-88% accuracy
3State-of-the-art CNN: ~95-97% accuracy (ResNet, EfficientNet)
4Human performance: ~94% accuracyCommon Observations:
Batch Normalization:
Dropout:
Data Augmentation:
Progressive Depth:
This architecture can be adapted for various tasks:
1# Medical imaging (X-rays)
2input_shape = (224, 224, 1) # Grayscale, higher resolution
3filters = [64, 128, 256, 512] # More capacity
4
5# Satellite imagery
6input_shape = (512, 512, 3) # Very high resolution
7filters = [32, 64, 128, 256, 512] # Deeper network
8
9# Mobile deployment
10input_shape = (96, 96, 3) # Lower resolution
11filters = [16, 32, 64] # Fewer filters (faster inference)Why This CNN Works:
Training Best Practices:
This example is production-ready code you can adapt for any image classification task!
Outstanding work! You've mastered the fundamentals of Convolutional Neural Networksβfrom understanding why CNNs work to building and training production-ready architectures. You've learned convolution, pooling, and visualization techniques that reveal how CNNs "see" images.
Next: In Lesson 8: Advanced CNN Architectures, we'll study the breakthrough networks that revolutionized computer vision:
You'll learn architectural innovations like skip connections, bottleneck layers, and compound scaling that power modern computer vision systems.
Your Computer Vision Journey Continues β Ready to master state-of-the-art architectures? Let's go! π
Your progress is saved in this browser, on this device only.