Convolutional neural networks (CNNs) learn spatial filters that transform raw image data into higher‑level feature representations. Consider this: when you train a CNN with PyTorch, each convolutional layer stores a set of learned kernels (also called weights) that encode patterns such as edges, textures, or more abstract shapes. Visualizing these kernels and the resulting feature maps provides insight into what the network actually “sees” at different stages of processing. This article shows how to extract and visualize the filters from a convolutional layer using PyTorch, step by step, so you can inspect the learned representations and debug or improve your models.
Understanding Convolutional Layers
A convolutional layer operates by sliding a small receptive field—called a kernel or filter—across the input tensor. Now, for each position, the element‑wise product of the kernel and the corresponding input patch is summed, producing a single value in the output feature map. The layer typically applies a bias term and an activation function (e.g., ReLU).
- Number of filters: determines how many distinct patterns the layer can detect.
- Kernel size: the spatial dimensions of each filter (e.g., 3×3, 5×5).
- Stride: the step size the kernel moves across the input.
- Padding: optional zero‑padding to preserve spatial dimensions.
When you inspect the weight tensor of a layer, you will see its shape as (out_channels, in_channels, kernel_height, kernel_width). Each filter is a 3‑D matrix that multiplies the input channels independently and then sums across them.
Setting Up PyTorch for Visualization
Before visualizing, you need a trained or untrained model. The following code creates a minimal CNN and runs a forward pass to obtain the weight tensor:
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=8, kernel_size=3, stride=1, padding=1)
self.relu = nn.ReLU()
self.fc = nn.
def forward(self, x):
x = self.conv1(x)
x = self.Practically speaking, relu(x)
x = torch. flatten(x, 1)
x = self.
model = SimpleCNN()
# Dummy input to trigger weight initialization
dummy_input = torch.randn(1, 3, 32, 32)
_ = model(dummy_input)
The weight tensor for conv1 can be accessed via model.conv1.weight. It has shape (8, 3, 3, 3), meaning eight filters, each with three input channels and a 3×3 spatial size.
Extracting the Filters
To visualize a filter, you need to isolate it from the weight tensor. The following helper function extracts a single filter and converts it to a NumPy array for plotting:
import numpy as np
import matplotlib.pyplot as plt
def get_filter(tensor, out_idx, in_idx=None):
"""
Return a 2‑D array of a specific filter.
Here's the thing — if in_idx is None, the filter is summed over all input channels. That said, """
if in_idx is None:
# Sum over input channels to get a single‑channel filter
filter_tensor = tensor[out_idx]. sum(dim=0) # shape (kernel_h, kernel_w)
else:
# Select a specific input channel
filter_tensor = tensor[out_idx, in_idx] # shape (kernel_h, kernel_w)
return filter_tensor.cpu().
You can now retrieve any filter by its output channel index (`out_idx`) and optionally a specific input channel (`in_idx`). Summing over input channels is common when you want to see the overall pattern a filter learns across all channels.
## Visualizing Individual Filters
Below is a function that plots a grid of filters, optionally showing each input channel separately:
```python
def visualize_filters(weight_tensor, n_filters=8, cols=4):
"""
Plot a grid of filters from a convolutional weight tensor.
"""
plt.figure(figsize=(cols * 2, 2 * n_filters))
for i in range(n_filters):
plt.subplot(n_filters // cols + 1, cols, i + 1)
# Sum over input channels for a unified view
filt = get_filter(weight_tensor, out_idx=i)
plt.imshow(filt, cmap='viridis')
plt.axis('off')
plt.title(f'Filter {i+1}')
plt.tight_layout()
plt.show()
Calling visualize_filters(model.weight, n_filters=8) will display the eight learned kernels. conv1.Bold the phrase “learned kernels” to point out that these are the parameters the network optimized during training That's the part that actually makes a difference..
Visualizing Feature Maps
Filters alone are not enough; you also want to see how they respond to input images. The following code extracts feature maps after the ReLU activation:
def get_feature_maps(model, input_tensor):
"""
Return the feature maps after the first convolution and ReLU.
"""
model.eval()
x = input_tensor.clone()
x = model.conv1(x) # convolution
x = model.relu(x) # activation
return x
To visualize a particular feature map, you can pick a channel and plot it as a heatmap:
def visualize_feature_map(feature_tensor, channel=0):
plt.figure(figsize=(4, 4))
plt.imshow(feature_tensor[0, channel], cmap='viridis')
plt.title(f'Feature Map {channel+1}')
plt.axis('off')
plt.show()
You can loop over all channels to see how different filters activate on various parts of the input.
Putting It All Together
Here is a concise workflow that combines the previous steps:
- Load or define a model and run a forward pass with a real image batch.
- Extract the weight tensor from the desired convolutional layer.
- Visualize the filters using
visualize_filters. - Run the model on sample images to obtain feature maps.
- Select channels of interest and plot them with
visualize_feature_map.
Below is a complete example using a pretrained ResNet‑18 model (available via torchvision) and a single image from the validation set:
import torchvision.models as models
import torchvision.transforms as T
from PIL import Image
# 1. Load a pretrained model and set to evaluation mode
resnet = models.resnet18(pretrained=True)
resnet.eval()
# 2. Define preprocessing (same as training)
preprocess = T.Compose([
T.Resize(256),
T.CenterCrop(224),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406],
std =[0.229, 0.224, 0.225])
])
# 3. Load an image and apply transforms
img = Image.open('example.jpg').convert('RGB')
img_t = preprocess(img).unsqueeze(0) # add batch dimension
# 4. Forward pass to obtain intermediate activations
# Hook to capture the output of the first conv layer
activations = None
def hook_fn(module, input, output):
global activations
activations = output
handle = resnet.Practically speaking, conv1. register_forward_hook(hook_fn)
_ = resnet(img_t) # forward pass
handle.
# 5. Visualize the first‑layer filters
visualize_filters(resnet.conv1.weight, n_filters=12, cols=4)
# 6. Visualize a few feature maps
for i in range(3): # show first three channels
visualize_feature_map(activations, channel=i)
Key points to remember:
- Sum over input channels when you want a single visual representation of a filter; this reveals the combined effect across all color channels.
- Use a colormap like
viridisorplasmato highlight positive and negative values; many libraries default to gray‑scale, which can be misleading for signed weights. - Normalize the feature maps if they have extreme ranges; dividing by the max absolute value makes the visualization more comparable across channels.
Practical Tips and Common Issues
- Device placement: Ensure the weight tensor and input images are on the same device (CPU or GPU). Moving tensors between devices can cause shape mismatches.
- Batch dimension: When visualizing, remove the batch axis (
tensor.squeeze(0)) before converting to NumPy for plotting. - Activation scaling: Feature maps after ReLU are non‑negative, but raw convolution outputs can be negative. Visualizing raw outputs may require a symmetric colormap (e.g.,
seismic). - Layer selection: Deeper layers capture more abstract concepts (e.g., parts of objects). If you need low‑level details, focus on early conv layers.
- Memory constraints: Large models (e.g., Vision Transformers) may not fit the GPU memory when extracting many feature maps. Use
torch.no_grad()to disable gradient computation during inference.
Frequently Asked Questions (FAQ)
Q1: Can I visualize filters from a depthwise‑separable convolution?
Yes. Depthwise separable convolutions have a single input channel per filter, so you can directly plot weight[out_idx, 0, :, :]. For grouped convolutions, you may need to sum over the grouped input channels.
Q2: Why do some filters look noisy or almost zero?
During training, many filters may become redundant or receive very small gradients. Pruning or using regularization can reduce the number of “dead” filters, making the visualizations cleaner That's the whole idea..
Q3: How does padding affect the visual output?
Padding adds zeros around the input border, allowing the kernel to be applied to edge pixels. When visualizing filters, padding does not change the kernel shape, but the corresponding feature maps will have the same spatial size as the input Easy to understand, harder to ignore. Surprisingly effective..
Q4: Is it possible to animate the filter evolution during training?
Yes. Save the weight tensor at successive checkpoints and re‑run the visualization script. Tools like Matplotlib’s animation module can create a video showing how filters become sharper or more structured over time.
Q5: Do I need to normalize the images before visualization?
Normalization is required for the model’s expected input distribution, but for pure visual inspection you can denormalize the image back to the original pixel range (e.g., [0, 255]) using the inverse of the normalization constants Less friction, more output..
Conclusion
Visualizing the kernels and feature maps of a convolutional layer in PyTorch is a straightforward process that offers deep insight into what a network has learned. By extracting the weight tensor, optionally summing over input channels, and plotting the results with Matplotlib, you can inspect filter patterns and understand how different parts of an image activate specific channels. Practically speaking, this practice not only aids debugging but also enriches your intuition about how CNNs perceive visual data. Apply the steps outlined above to your own models, experiment with various layers, and let the visualizations guide you toward more effective and interpretable deep‑learning solutions Easy to understand, harder to ignore. That's the whole idea..