π AI/ML Security Lab β β οΈ Adversarial Examples π
Posted on: October 9, 2026
Description:
Machine learning models can achieve high accuracy on normal test data, but that does not necessarily mean they are robust against manipulated inputs.
Adversarial examples are inputs that have been deliberately modified to influence a model's prediction. Even relatively small changes to input features can sometimes push an input across a model's learned decision boundary and result in a different classification.
In this first AI/ML Security Lab, we'll use Python and Scikit-learn to build a simple classifier, establish its normal behaviour, simulate controlled input manipulation, observe the impact, and introduce a basic defensive validation layer.
π― Understanding Adversarial Examples
A machine learning model learns patterns and decision boundaries from its training data.
During normal inference, new input is evaluated against those learned patterns to produce a prediction. But an attacker may deliberately manipulate input values in an attempt to make the model behave differently.
The basic idea is:
Original Input β Correct Prediction
Manipulated Input β Different Prediction
The model itself hasn't changed. Only the input presented to it has.
This makes adversarial examples particularly interesting from a security perspective because a model that performs well under normal conditions may behave very differently when its inputs are intentionally manipulated.
π’ Building the Baseline Model
We'll use the Iris dataset, which contains measurements for three species of Iris flowers.
After loading the dataset, we split it into training and testing sets and standardise the feature values.
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = pd.Series(iris.target)
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.25,
random_state=42,
stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
We then train a Logistic Regression classifier and calculate its baseline accuracy.
model = LogisticRegression(max_iter=1000)
model.fit(X_train_scaled, y_train)
baseline_predictions = model.predict(X_test_scaled)
baseline_accuracy = accuracy_score(y_test, baseline_predictions)
print(f"Baseline Accuracy: {baseline_accuracy:.2%}")
This gives us a reference point for how the model behaves before any adversarial manipulation is introduced.
π Selecting a Target Input
Instead of modifying the entire test dataset, we'll select a sample that the model originally classified correctly.
correct_indices = np.where(
baseline_predictions == y_test.to_numpy()
)[0]
target_index = correct_indices[0]
original_sample = X_test.iloc[target_index].copy()
We can then record the original prediction and prediction confidence.
original_scaled = scaler.transform(
original_sample.to_frame().T
)
original_prediction = model.predict(original_scaled)[0]
original_confidence = model.predict_proba(original_scaled).max()
At this point, we have our baseline input and know exactly how the model normally classifies it.
π΄ Simulating an Adversarial Input
Now we deliberately modify selected features of the sample.
We'll gradually increase the petal length and petal width values and check whether the classifier's prediction changes.
feature_ranges = X.max() - X.min()
perturbation_levels = np.linspace(0.01, 0.50, 50)
for level in perturbation_levels:
candidate = original_sample.copy()
candidate["petal length (cm)"] += (
feature_ranges["petal length (cm)"] * level
)
candidate["petal width (cm)"] += (
feature_ranges["petal width (cm)"] * level
)
candidate_scaled = scaler.transform(
candidate.to_frame().T
)
candidate_prediction = model.predict(candidate_scaled)[0]
if candidate_prediction != original_prediction:
adversarial_sample = candidate
adversarial_prediction = candidate_prediction
used_perturbation = level
break
This is a controlled educational demonstration rather than a sophisticated adversarial attack algorithm.
The important idea is that we're deliberately searching for an input modification capable of changing the model's decision.
π Observing the Impact
Once a prediction change is found, we can compare the original and manipulated inputs.
comparison = pd.DataFrame({
"Original": original_sample,
"Adversarial": adversarial_sample,
"Difference": adversarial_sample - original_sample
})
print(comparison)
We can also compare the predictions:
print(f"Original Prediction: {iris.target_names[original_prediction]}")
print(f"Adversarial Prediction: {iris.target_names[adversarial_prediction]}")
This illustrates an important ML security principle:
Correct behaviour on normal test data doesn't guarantee robust behaviour under intentionally manipulated inputs.
Traditional model evaluation primarily measures predictive performance. Security testing asks an additional question: How does the model behave when someone deliberately tries to make it fail?
π‘οΈ Adding a Defensive Layer
One basic defence is to validate incoming feature values before allowing them to reach the model.
We can determine the minimum and maximum values observed in the training data:
training_min = X_train.min()
training_max = X_train.max()
Then create a validation function:
def validate_input(sample):
violations = []
for feature in X.columns:
if sample[feature] < training_min[feature] or sample[feature] > training_max[feature]:
violations.append(feature)
return violations
An input containing unexpected values can then be flagged for additional handling rather than automatically being trusted.
π§ͺ Is Input Validation Enough?
No.
Range validation is intentionally a simple defensive example for this lab.
An adversarial input may remain completely within expected feature ranges while still causing an incorrect prediction. Real-world adversarial ML defenses can involve techniques such as adversarial training, robustness testing, anomaly detection, preprocessing, monitoring, and model-specific defensive strategies.
This distinction is important: adding one validation rule does not make an ML model "adversarially secure."
Instead, the validation layer demonstrates a broader security principle:
Inputs reaching an ML model should be treated as untrusted data.
π Security Takeaways
- Adversarial examples deliberately manipulate model inputs to influence predictions.
- A model can perform well on ordinary test data while still being sensitive to manipulated inputs.
- Comparing baseline and adversarial behaviour helps reveal model weaknesses.
- Input validation can detect certain suspicious inputs but cannot prevent every adversarial example.
- ML security requires evaluating robustness and security, not only model accuracy.
π Final Note
Machine learning security begins by recognising that models operate in environments where inputs may not always be trustworthy.
By testing a model under controlled adversarial conditions, we can better understand its weaknesses and design defences around those risks.
Code Snippet:
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def load_dataset():
"""Load the Iris dataset."""
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = pd.Series(iris.target)
print(f"β
Dataset loaded | samples={len(X)} | features={X.shape[1]}")
return iris, X, y
def prepare_data(X, y):
"""Split and standardize the dataset."""
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"β
Data prepared | training={len(X_train)} | testing={len(X_test)}")
return X_train, X_test, y_train, y_test, X_train_scaled, X_test_scaled, scaler
def train_baseline_model(X_train_scaled, y_train):
"""Train the baseline Logistic Regression model."""
model = LogisticRegression(max_iter=1000)
model.fit(X_train_scaled, y_train)
print("β
Baseline model trained")
return model
def evaluate_baseline(model, X_test_scaled, y_test):
"""Evaluate normal model performance."""
predictions = model.predict(X_test_scaled)
accuracy = accuracy_score(y_test, predictions)
print(f"π Baseline Accuracy: {accuracy:.2%}")
return predictions
def select_target_sample(X_test, y_test, predictions, scaler, model, iris):
"""Select a correctly classified sample for the adversarial test."""
correct_indices = np.where(predictions == y_test.to_numpy())[0]
if len(correct_indices) == 0:
raise ValueError("No correctly classified samples are available.")
target_index = correct_indices[0]
original_sample = X_test.iloc[target_index].copy()
original_scaled = scaler.transform(original_sample.to_frame().T)
original_prediction = model.predict(original_scaled)[0]
original_confidence = model.predict_proba(original_scaled).max()
actual_class = y_test.iloc[target_index]
print("\nπ― Target Sample")
print(original_sample)
print(f"\nActual Class: {iris.target_names[actual_class]}")
print(f"Predicted Class: {iris.target_names[original_prediction]}")
print(f"Confidence: {original_confidence:.2%}")
return original_sample, original_prediction
def create_adversarial_sample(original_sample, original_prediction, X, scaler, model):
"""Create controlled input variations until the prediction changes."""
feature_ranges = X.max() - X.min()
perturbation_levels = np.linspace(0.01, 0.50, 50)
for level in perturbation_levels:
candidate = original_sample.copy()
candidate["petal length (cm)"] += feature_ranges["petal length (cm)"] * level
candidate["petal width (cm)"] += feature_ranges["petal width (cm)"] * level
candidate_scaled = scaler.transform(candidate.to_frame().T)
candidate_prediction = model.predict(candidate_scaled)[0]
if candidate_prediction != original_prediction:
print(f"\nπ΄ Adversarial input found | perturbation={level:.2%}")
return candidate, candidate_prediction, level
print("\nβ οΈ No prediction change was found within the tested perturbation range.")
return None, original_prediction, None
def observe_impact(original_sample, adversarial_sample, original_prediction, adversarial_prediction, used_perturbation, iris):
"""Compare the original and adversarial inputs."""
if adversarial_sample is None:
print("No adversarial sample available for comparison.")
return
comparison = pd.DataFrame({
"Original": original_sample,
"Adversarial": adversarial_sample,
"Difference": adversarial_sample - original_sample,
})
print("\nπ Adversarial Impact")
print(comparison)
print(f"\nPerturbation Level: {used_perturbation:.2%}")
print(f"Original Prediction: {iris.target_names[original_prediction]}")
print(f"Adversarial Prediction: {iris.target_names[adversarial_prediction]}")
def validate_input(sample, training_min, training_max, features):
"""Check whether input values fall outside the training range."""
violations = []
for feature in features:
if sample[feature] < training_min[feature] or sample[feature] > training_max[feature]:
violations.append(feature)
return violations
def verify_defense(adversarial_sample, X_train, features):
"""Validate the adversarial sample using expected feature ranges."""
if adversarial_sample is None:
print("\nβ οΈ No adversarial sample available for validation.")
return
training_min = X_train.min()
training_max = X_train.max()
violations = validate_input(adversarial_sample, training_min, training_max, features)
print("\nπ‘οΈ Defense Verification")
if violations:
print("β οΈ Suspicious input detected.")
print(f"Features outside expected range: {violations}")
else:
print("β
Input passed range validation.")
print("Additional adversarial defenses may still be required.")
def run_lab():
"""Run the complete adversarial examples security lab."""
print("π Starting AI/ML Security Lab β Adversarial Examples\n")
iris, X, y = load_dataset()
X_train, X_test, y_train, y_test, X_train_scaled, X_test_scaled, scaler = prepare_data(X, y)
model = train_baseline_model(X_train_scaled, y_train)
predictions = evaluate_baseline(model, X_test_scaled, y_test)
original_sample, original_prediction = select_target_sample(
X_test, y_test, predictions, scaler, model, iris
)
adversarial_sample, adversarial_prediction, used_perturbation = create_adversarial_sample(
original_sample, original_prediction, X, scaler, model
)
observe_impact(
original_sample,
adversarial_sample,
original_prediction,
adversarial_prediction,
used_perturbation,
iris,
)
verify_defense(adversarial_sample, X_train, X.columns)
print("\nβ
AI/ML Security Lab completed successfully")
if __name__ == "__main__":
run_lab()
No comments yet. Be the first to comment!