Image Classification¶
Verify that adversarial perturbations cannot fool image classifiers. This tutorial walks through complete MNIST and GTSRB verification workflows from start to finish.
What You Will Learn¶
How to load a pre-trained image classifier and convert it to NNV format
How to define an adversarial perturbation region using ImageStar
How to run robustness verification with
verify_robustness()How to interpret verification results (robust / not robust / unknown)
How to visualize output ranges to understand classifier behavior
MNIST Digit Classification¶
This example verifies that small pixel perturbations to a handwritten digit image cannot cause the classifier to change its prediction.
Prerequisites¶
NNV installed and on the MATLAB path (
install.mhas been run)Pre-trained MNIST model:
mnist_model.mat(included in the Tutorial folder)MATLAB’s built-in DigitDataset (ships with Deep Learning Toolbox, no download needed)
Step 1: Load and Convert the Network¶
% Load the pre-trained MNIST network
mnist_model = load('mnist_model.mat');
% Convert from MATLAB deep learning format to NNV format
% matlab2nnv() handles SeriesNetwork, DAGNetwork, and dlnetwork
net = matlab2nnv(mnist_model.net);
The matlab2nnv() function inspects each layer of the MATLAB network and
creates the corresponding NNV layer objects (FullyConnectedLayer, ReluLayer,
Conv2DLayer, etc.). After conversion, net is an NNV NN object that
supports reachability analysis.
Step 2: Load a Test Image¶
% Load MATLAB's built-in digit dataset (no download needed)
digitDatasetPath = fullfile(toolboxdir('nnet'), ...
'nndemos', 'nndatasets', 'DigitDataset');
imds = imageDatastore(digitDatasetPath, ...
'IncludeSubfolders', true, 'LabelSource', 'foldernames');
% Read a specific test image
idx = 10; % Image index
[img, fileInfo] = readimage(imds, idx);
target = single(fileInfo.Label); % True class label
% img is a 28x28 grayscale image with pixel values in [0, 255]
Step 3: Create the Adversarial Input Set¶
An ImageStar represents the set of all images within a perturbation region. For L-infinity robustness, we define a box around the original image where each pixel can vary by a fixed amount:
% Define perturbation magnitude
epsilon = 1; % Each pixel can change by ±1 (out of [0, 255])
% Compute perturbed bounds, clipped to valid pixel range
lb = max(single(img) - epsilon, 0); % Lower bound (no negative pixels)
ub = min(single(img) + epsilon, 255); % Upper bound (no overflow)
% Create the ImageStar input set
% ImageStar(lb, ub) represents ALL images where each pixel is in [lb, ub]
IS = ImageStar(lb, ub);
What is an ImageStar?
An ImageStar is a set of images: every image where pixel (h,w,c) falls
between lb(h,w,c) and ub(h,w,c) is contained in the set.
NNV propagates this entire set through the network in one pass, computing
the set of all possible outputs. See Set Representations
for the mathematical definition.
Step 4: Quick Falsification Check¶
Before running the expensive reachability analysis, check whether the network gives the correct answer on the original image and the extreme corners of the perturbation region:
% Evaluate on the original image
y_orig = net.evaluate(single(img));
[~, pred_orig] = max(y_orig);
% Evaluate on the lower and upper bounds
y_lb = net.evaluate(lb);
[~, pred_lb] = max(y_lb);
y_ub = net.evaluate(ub);
[~, pred_ub] = max(y_ub);
% If any prediction differs from the target, the network is NOT robust
if pred_orig ~= target || pred_lb ~= target || pred_ub ~= target
fprintf('Network is NOT robust: classification changes at bounds.\n');
else
fprintf('Bounds check passed. Running full verification...\n');
end
This quick check catches many non-robust cases in milliseconds, saving the cost of full reachability analysis.
Step 5: Run Robustness Verification¶
% Configure reachability options
reachOptions = struct;
reachOptions.reachMethod = 'approx-star'; % Sound over-approximation
% Run verification
% Returns: 1 = robust, 0 = not robust, 2 = unknown
result = net.verify_robustness(IS, reachOptions, target);
% Interpret the result
switch result
case 1
fprintf('VERIFIED: Network is robust for ALL images in the input set.\n');
case 0
fprintf('NOT ROBUST: A counterexample exists within the perturbation.\n');
case 2
fprintf('UNKNOWN: Over-approximation is too coarse to determine.\n');
fprintf('Try: smaller epsilon, exact-star method, or relaxFactor=0.\n');
end
Understanding Verification Results
1 (Robust): The correct class has the highest score for every image in the perturbation set. This is a formal guarantee.
0 (Not Robust): There exists at least one image in the perturbation set that is classified differently. The network is vulnerable.
2 (Unknown): The approximate method cannot determine robustness. The over-approximation of the output set overlaps between classes. This does NOT mean the network is vulnerable – it means the analysis is inconclusive. Try a more precise method.
Step 6: Visualize Output Ranges¶
% Compute the reachable output set
output_sets = net.reach(IS, reachOptions);
% Extract output bounds for each class
[lb_out, ub_out] = output_sets.getRanges();
% Plot output ranges for all 10 digit classes
figure;
n_classes = length(lb_out);
for i = 1:n_classes
plot([i i], [lb_out(i) ub_out(i)], 'b-', 'LineWidth', 2); hold on;
plot(i, y_orig(i), 'rx', 'MarkerSize', 10, 'LineWidth', 2);
end
xlabel('Class (0-9)');
ylabel('Network output score');
title('Output ranges under adversarial perturbation');
legend('Reachable range', 'Original output');
The blue bars show the range of possible output scores for each class across all images in the perturbation set. If the bar for the correct class is entirely above all other bars, the network is verified robust.
GTSRB Traffic Sign Recognition¶
The same workflow applies to traffic sign classification, with minor differences for image dimensions and class count.
% Load pre-trained GTSRB model
gtsrb_model = load('gtsrb_model.mat');
net = matlab2nnv(gtsrb_model.net);
% Load dataset from NNV's data directory
datapath = fullfile(nnvroot(), 'data', 'GTSRB');
imds = imageDatastore(datapath, ...
'IncludeSubfolders', true, 'LabelSource', 'foldernames');
% Read and resize image (GTSRB model expects 30x29 pixels)
[img, fileInfo] = readimage(imds, 1);
img = imresize(img, [30 29]);
target = single(fileInfo.Label);
% Create input set and verify (same as MNIST)
epsilon = 1;
lb = max(single(img) - epsilon, 0);
ub = min(single(img) + epsilon, 255);
IS = ImageStar(lb, ub);
reachOptions.reachMethod = 'approx-star';
result = net.verify_robustness(IS, reachOptions, target);
Note
The key difference from MNIST is image resizing: imresize(img, [30 29])
matches the network’s expected input dimensions. The number of output classes
is determined by net.OutputSize (43 for GTSRB).
MedMNIST and CIFAR-10¶
For larger networks (MedMNIST organ classifiers, CIFAR-10 CNNs), the same workflow applies but may require:
Higher relaxFactor (e.g., 0.5) for faster but coarser analysis
Conformal prediction for networks where full reachability is intractable
See Medical Imaging for MedMNIST examples and Conformal Prediction Setup for the probabilistic verification setup.
Adapting to Your Own Classifier¶
To verify your own image classifier:
Train or load your network in MATLAB (or import from ONNX)
Convert to NNV:
net = matlab2nnv(your_network)Choose epsilon: Start small (e.g., 1/255) and increase
Create ImageStar:
IS = ImageStar(max(img-eps, 0), min(img+eps, 1))(normalize bounds to match your network’s expected input range)Verify:
result = net.verify_robustness(IS, reachOptions, target)
Tip
If verification returns UNKNOWN for most samples, try:
Reducing epsilon (smaller perturbation region)
Using
reachOptions.relaxFactor = 0(full LP optimization)Using
reachOptions.reachMethod = 'exact-star'(complete but slower)Using
reachOptions.numCores = 4(parallel computation)