Skip to content

Commit fb9f957

Browse files
add model code and its data
1 parent 8710dae commit fb9f957

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"""
2+
Adversarial attack optimization in GAMSPy (MNIST).
3+
4+
Builds and solves a bounded-noise attack that minimizes the logit margin
5+
between the predicted class and runner-up to induce misclassification.
6+
Users control the NN shape (hidden_layers, hidden_layer_neurons) and the
7+
modeling approach: MIP with CPLEX, NLP with CONOPT or MPEC with NLPEC.
8+
Weights are loaded from a pretrained NN (You can either train your own NN to
9+
check for yourself, or use the example file we provide in this repository).
10+
11+
Inputs: a correctly classified MNIST test image; noise ∈ [-MNIST_NOISE_BOUND, +MNIST_NOISE_BOUND].
12+
Outputs: a JSON performance report (objective, time, status, size).
13+
14+
Multi-start: pass `noise_init` to `main(...)`. See the Sobol example at the end
15+
of the file for running many instances with diverse initial points.
16+
"""
17+
18+
import os
19+
import math
20+
import json
21+
import numpy as np
22+
23+
import gamspy as gp
24+
import torch
25+
import torch.nn as nn
26+
from gamspy.math.matrix import dim
27+
from torchvision import datasets, transforms
28+
29+
30+
def build_network(hidden_layers, hidden_layer_neurons):
31+
layers = []
32+
layers.append(nn.Linear(784, hidden_layer_neurons))
33+
layers.append(nn.ReLU())
34+
for _ in range(hidden_layers - 1):
35+
layers.append(nn.Linear(hidden_layer_neurons, hidden_layer_neurons))
36+
layers.append(nn.ReLU())
37+
layers.append(nn.Linear(hidden_layer_neurons, 10))
38+
39+
network = nn.Sequential(*layers)
40+
network.load_state_dict(
41+
torch.load(
42+
f"ffn_data_{hidden_layers}_{hidden_layer_neurons}.pth", weights_only=True
43+
)
44+
)
45+
return network
46+
47+
48+
def get_image(network):
49+
transform = transforms.Compose([transforms.ToTensor()])
50+
dataset = datasets.MNIST("data", train=False, download=True, transform=transform)
51+
test_loader = torch.utils.data.DataLoader(dataset)
52+
53+
for data, target in test_loader:
54+
data, target = data, target
55+
single_image = data[0]
56+
single_target = target[0]
57+
single_image = single_image.reshape(single_image.size(0), -1)
58+
59+
if torch.argmax(network(single_image)) == single_target:
60+
return single_image
61+
62+
63+
def convert_nlp(m: gp.Container, layer: torch.nn.ReLU):
64+
return gp.math.relu_with_complementarity_var
65+
66+
67+
def relu_with_mpec(x: gp.Variable):
68+
assert isinstance(x.container, gp.Container)
69+
domain = x.domain
70+
71+
y = x.container.addVariable(
72+
type="positive",
73+
domain=domain,
74+
)
75+
76+
eq = x.container.addEquation(
77+
domain=domain,
78+
)
79+
80+
eq[...] = y - x >= gp.Number(0)
81+
return y, [], {eq: y}
82+
83+
84+
def mpec_wrapper(x: gp.Variable):
85+
output, equations, local_matches = relu_with_mpec(x)
86+
matches.update(local_matches)
87+
return output, equations
88+
89+
90+
def convert_mpec(m: gp.Container, layer):
91+
return mpec_wrapper
92+
93+
94+
def save_results(report, results_file: str = "performance_report.json") -> None:
95+
results = []
96+
97+
if os.path.exists(results_file):
98+
try:
99+
with open(results_file, "r") as f:
100+
data = json.load(f)
101+
results = data if isinstance(data, list) else [data]
102+
except (json.JSONDecodeError, IOError) as e:
103+
print(f"Warning: Could not read existing results file: {e}")
104+
105+
results.append(report)
106+
107+
try:
108+
with open(results_file, "w") as f:
109+
json.dump(results, f, indent=2)
110+
except IOError as e:
111+
print(f"Error saving results: {e}")
112+
113+
114+
global matches
115+
116+
MEAN = (0.1307,)
117+
STD = (0.3081,)
118+
MNIST_NOISE_BOUND = 0.1
119+
120+
problem_type_map = {
121+
"MIP": [None, "CPLEX"],
122+
"NLP": [{"ReLU": convert_nlp}, "CONOPT"],
123+
"MPEC": [{"ReLU": convert_mpec}, "NLPEC"],
124+
}
125+
126+
127+
def main(hidden_layers, hidden_layer_neurons, prob_type: str, noise_init=None):
128+
m = gp.Container()
129+
network = build_network(hidden_layers, hidden_layer_neurons)
130+
single_image = get_image(network)
131+
relu_converter, solver = problem_type_map[prob_type]
132+
133+
image_data = single_image.numpy().reshape(784)
134+
135+
image = gp.Parameter(
136+
m, name="image", domain=dim(image_data.shape), records=image_data
137+
)
138+
139+
noise = gp.Variable(m, name="noise", domain=dim([784]))
140+
a1 = gp.Variable(m, name="a1", domain=dim([784]))
141+
142+
# Set noise bounds
143+
noise.lo[...] = -MNIST_NOISE_BOUND
144+
noise.up[...] = MNIST_NOISE_BOUND
145+
146+
if noise_init is not None:
147+
noise_vals = gp.Parameter(m, name="noise_vals", domain=noise.domain)
148+
noise_vals.setRecords(noise_init)
149+
noise.l[...] = noise_vals[...]
150+
151+
# set input's lower and upper bounds
152+
a1.lo[...] = -MEAN[0] / STD[0]
153+
a1.up[...] = (1 - MEAN[0]) / STD[0]
154+
155+
set_a1 = gp.Equation(m, "set_a1", domain=dim(a1.shape))
156+
set_a1[...] = a1 == (image + noise - MEAN[0]) / STD[0]
157+
158+
seq_formulation = gp.formulations.TorchSequential(m, network, relu_converter)
159+
y, _ = seq_formulation(a1)
160+
161+
output_np = network(single_image.unsqueeze(0)).detach().numpy()[0][0]
162+
right_label = np.argsort(output_np)[-1]
163+
wrong_label = np.argsort(output_np)[-2]
164+
165+
obj = gp.Variable(m, name="z")
166+
167+
margin = gp.Equation(m, "margin")
168+
margin[...] = obj[...] == y[f"{right_label}"] - y[f"{wrong_label}"]
169+
170+
model = gp.Model(
171+
m,
172+
"min_noise",
173+
equations=m.getEquations(),
174+
objective=obj,
175+
sense="min",
176+
problem=prob_type,
177+
matches=matches,
178+
)
179+
180+
model.solve(solver=solver, options=gp.Options.fromGams({"reslim": 4000}))
181+
182+
report = {
183+
"hidden_layers": hidden_layers,
184+
"hidden_layer_neurons": hidden_layer_neurons,
185+
"problem_type": prob_type,
186+
"objective_value": round(model.objective_value, 5),
187+
"solve_time": round(model.total_solve_time, 5),
188+
"status": str(model.status).split(".")[-1],
189+
"variable_count": model.num_variables,
190+
}
191+
192+
save_results(report)
193+
194+
return model.objective_value
195+
196+
197+
if __name__ == "__main__":
198+
matches = {}
199+
obj_value = main(4, 40, prob_type="MPEC")
200+
assert math.isclose(obj_value, -1.96277, rel_tol=0.001)
201+
202+
# The script below is an example of how to run multiple instances of the model
203+
# with different hidden layers, hidden layer neurons, and problem types. This
204+
# can be used as a comprehensive performance evaluation across various configurations.
205+
206+
# hidden_layers = [1, 2, 3, 4, 5]
207+
# hidden_layer_neurons = [10, 20, 30, 40, 50, 60]
208+
# problem_types = ["MIP", "NLP", "MPEC"]
209+
# for hl in hidden_layers:
210+
# for hn in hidden_layer_neurons:
211+
# for prob_type in problem_types:
212+
# print(f"Running for HL: {hl}, HN: {hn}, Type: {prob_type}")
213+
# matches = {}
214+
# main(hl, hn, prob_type)
215+
# sys.stdout.flush()
216+
217+
# The script below is an example of how to run multiple instances of the model
218+
# with different noise initializations sampled via a Sobol sequence.
219+
220+
# sampler = qmc.Sobol(d=784) # dimension is the shape of the noise (28x28)
221+
# samples = sampler.random_base2(m=10) # Generates 2^10 = 1024 samples
222+
# scaled_samples = qmc.scale(samples, l_bounds=[-MNIST_NOISE_BOUND]*784, u_bounds=[MNIST_NOISE_BOUND]*784)
223+
# for sample in scaled_samples:
224+
# matches = {}
225+
# main(1, 10, prob_type="MPEC", noise_init=sample)
226+
# sys.stdout.flush()
147 KB
Binary file not shown.

0 commit comments

Comments
 (0)