Prepare Image Data for a CNN in Python: Beginner Guide
Your CNN can have the right layers and still receive the wrong data.
An image might be missing its channel dimension. Labels might become separated from their images. Pixel values might be scaled twice.
Some mistakes produce an immediate error. Others let training continue while quietly making the results less useful.
Before building a more complicated model, it helps to understand exactly what you are feeding it.
In this tutorial, we will prepare the Fashion-MNIST dataset for a convolutional neural network. By the end, you will understand image dimensions, integer labels, pixel scaling, and a few checks that catch common problems.
You can run the code blocks in order in a fresh Python notebook. Basic knowledge of Python variables, arrays, and indexing is enough to follow along.
How do you prepare images for a CNN?
For a CNN using the channels-last format, image inputs usually have four dimensions: number of images, height, width, and channels.
Preparing the data means checking those dimensions, choosing the expected pixel range, preserving the image-label pairs, and keeping training, validation, and test examples separate.
For our grayscale images, the final training shape will be:
(54000, 28, 28, 1)
Let’s work through where each number comes from.
1. Load the images and labels
We will use Fashion-MNIST, the same dataset introduced in the first lesson. It contains small grayscale images of clothing across ten categories.
Start with these imports:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plttf.keras.utils.set_random_seed(42)(x_train_raw, y_train_raw), (x_test_raw, y_test) = (
tf.keras.datasets.fashion_mnist.load_data()
)class_names = [
"T-shirt/top", "Trouser", "Pullover", "Dress", "Coat",
"Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"
]
The first run may download the dataset, so you need internet access.
Notice the names ending in _raw. We will preserve these original arrays and create prepared versions separately. This makes it easier to inspect the original pixels and avoid accidentally processing an already processed array.
Now check what loaded:
print("Training images:", x_train_raw.shape)
print("Training labels:", y_train_raw.shape)
print("Test images:", x_test_raw.shape)
print("Pixel data type:", x_train_raw.dtype)Expected output:
Training images: (60000, 28, 28)
Training labels: (60000,)
Test images: (10000, 28, 28)
Pixel data type: uint8There are 60,000 training images. Each image has 28 rows and 28 columns. Each corresponding label is a single integer identifying its category.
2. Understand the image dimensions
The shape (60000, 28, 28) describes a collection of grayscale images, but it does not explicitly include a channel dimension.
A channel stores one component of the pixel information.
A grayscale image typically has one channel. An RGB image has three: red, green, and blue.
With the channels-last format, a batch of RGB images might have the shape:
(32, 224, 224, 3)
That means 32 images, each 224 pixels high and 224 pixels wide, with three color channels.
For our dataset, we need to turn:
(60000, 28, 28)
into:
(60000, 28, 28, 1)
We are adding information about the array’s structure. We are not adding new visual detail to the images.
3. Inspect image-label pairs
Before changing anything, look at several examples.
fig, axes = plt.subplots(2, 4, figsize=(10, 5))for i, ax in enumerate(axes.flat):
ax.imshow(
x_train_raw[i],
cmap="gray",
vmin=0,
vmax=255
)
ax.set_title(class_names[int(y_train_raw[i])])
ax.axis("off")plt.tight_layout()
plt.show()
Each displayed title comes from the label at the same array position as its image.
This relationship matters throughout the workflow. If you shuffle images without applying the same ordering to labels, you can train a model against incorrect answers.
The code may still run. The learning task, however, has changed into something you never intended.
Checking a few examples is a simple way to catch obvious mismatches early.
4. Create a validation set before training
The original training portion contains 60,000 examples. We will reserve 6,000 for validation and use the remaining 54,000 for fitting.
Use one shared index ordering for images and labels:
rng = np.random.default_rng(42)
indices = rng.permutation(len(x_train_raw))val_indices = indices[:6000]
train_indices = indices[6000:]x_train = x_train_raw[train_indices]
y_train = y_train_raw[train_indices]x_val = x_train_raw[val_indices]
y_val = y_train_raw[val_indices]
This is a reproducible random split. It is not explicitly stratified, so it does not guarantee identical class proportions in both portions.
The test data remains untouched.
Get Iqra Mahmood’s stories in your inbox
Join Medium for free to get updates from this writer.
Each portion has a different purpose:
- Training: Used to update the model’s parameters.
- Validation: Used to monitor training and compare development choices.
- Test: Used for the final assessment after those choices are made.
For your own datasets, check whether related examples need to stay together. Random splitting can be inappropriate when several images come from the same patient, person, object, or recording session.
5. Scale pixel values to the range 0–1
Our original pixels are unsigned integers ranging from 0 to 255.
For this tutorial, we will convert them to floating-point values and divide by 255:
def prepare_images(images):
images = images.astype("float32") / 255.0
return np.expand_dims(images, axis=-1)x_train = prepare_images(x_train)
x_val = prepare_images(x_val)
x_test = prepare_images(x_test_raw)The function performs two operations:
- It scales the pixel values.
- It adds the final channel dimension.
For example, an original pixel value of 255 becomes 1.0, while 0 remains 0.0.
The division changes the numerical representation, not the image’s underlying content. It also does not guarantee that every training problem will improve by a specific amount.
This is often called image normalization, although more precisely we are applying fixed rescaling. Standardizing pixels using a learned mean and standard deviation is a different operation.
If preprocessing requires learning statistics from data, estimate them from the training portion and reuse them for validation and test data.
6. Verify the prepared arrays
Do not rely on memory when checking shapes. Print them.
print("Training:", x_train.shape)
print("Validation:", x_val.shape)
print("Test:", x_test.shape)
print("Data type:", x_train.dtype)
print("Pixel range:", x_train.min(), x_train.max())Expected output:
Training: (54000, 28, 28, 1)
Validation: (6000, 28, 28, 1)
Test: (10000, 28, 28, 1)
Data type: float32
Pixel range: 0.0 1.0Now add checks that stop execution if important assumptions fail:
assert x_train.shape[1:] == (28, 28, 1)
assert x_val.shape[1:] == (28, 28, 1)
assert x_test.shape[1:] == (28, 28, 1)assert len(x_train) == len(y_train)
assert len(x_val) == len(y_val)
assert len(x_test) == len(y_test)for images in (x_train, x_val, x_test):
assert images.dtype == np.float32
assert np.isfinite(images).all()
assert images.min() >= 0.0
assert images.max() <= 1.0for labels in (y_train, y_val, y_test):
assert np.issubdtype(labels.dtype, np.integer)
assert labels.min() >= 0
assert labels.max() < len(class_names)
These checks cannot prove your dataset is correct. For example, shuffled labels would still satisfy the shape checks. They do catch several avoidable structural and numerical mistakes.
7. Keep labels compatible with the loss function
Our labels remain integers such as 0, 3, or 7.
That representation works with sparse categorical cross-entropy:
loss_function = (
tf.keras.losses.SparseCategoricalCrossentropy()
)You do not need to convert these labels into one-hot vectors for this loss.
A one-hot representation would use a vector with one position set to 1 and the others set to 0. That representation generally pairs with categorical cross-entropy instead.
The practical rule is to check three things together: label format, model output, and loss configuration.
8. Confirm that the data can enter a CNN
We can perform a forward pass without training a model:
model = tf.keras.Sequential([
tf.keras.Input(shape=(28, 28, 1)),
tf.keras.layers.Conv2D(
filters=16,
kernel_size=3,
activation="relu",
data_format="channels_last"
),
tf.keras.layers.GlobalAveragePooling2D(
data_format="channels_last"
),
tf.keras.layers.Dense(10, activation="softmax")
])outputs = model(x_train[:8], training=False)print("Output shape:", tuple(outputs.shape))
Expected shape:
Output shape: (8, 10)Eight images entered the model, and each received ten output scores.
This verifies a basic input-output connection. It does not establish prediction quality: the weights have not been trained.
We will compare trained models in the next lesson.
Three preprocessing mistakes to avoid
Scaling twice
If you divide the arrays by 255 here, do not also add Rescaling(1./255) inside the model. That would shrink the values again.
Choose one location for this transformation and apply it consistently during training and prediction.
Treating every image format the same
This function is designed for grayscale arrays shaped like Fashion-MNIST. An RGB array already containing a channel dimension should not receive another one.
Using the wrong preprocessing for a pretrained model
Some pretrained networks expect different input ranges or model-specific preparation. Check the documentation instead of automatically dividing every image by 255.
Your next experiment
Display the first prepared training image using:
plt.imshow(
x_train[0, :, :, 0],
cmap="gray",
vmin=0,
vmax=1
)
plt.title(class_names[int(y_train[0])])
plt.axis("off")
plt.show()The final 0 selects the single grayscale channel.
Explain the shape (54000, 28, 28, 1) aloud. Then explain why the labels still have only one dimension.
If you can answer both, you understand the structure your CNN will receive — and you are ready to compare a dense network with a convolutional one.
Further reading
TensorFlow: Fashion-MNIST dataset








