7
7
from util_model import SimpleNeuralNet , MNISTClassifier
8
8
from adversarial_training import AdversarialTraining , ProjectedGradientTraining
9
9
10
+
10
11
class ProjetcedDRO (AdversarialTraining ):
11
12
"""
12
13
Execute distributionally robust optimization (DRO) using the Euclidean
@@ -25,13 +26,13 @@ def attack(self, budget, data, steps=15):
25
26
images_adv = images .clone ().detach ().to (self .device )
26
27
images_adv .requires_grad_ (True )
27
28
28
- # images.size()[0] corresponds to the batch size.
29
+ # images.size()[0] corresponds to the batch size.
29
30
desirable_distance = budget * math .sqrt (images .size ()[0 ])
30
31
31
32
# Choose a random strating point where the constraint for perturbations
32
33
# is tight. Without randomly choosing a starting point, the adversarial
33
34
# attack fails most of the time because the loss function is flat near
34
- # the training input, which was used in training the neural network.
35
+ # the training input, which was used in training the neural network.
35
36
randomStart (images_adv , budget )
36
37
for i in range (steps ):
37
38
if images_adv .grad is not None :
@@ -45,13 +46,15 @@ def attack(self, budget, data, steps=15):
45
46
distance = torch .norm (diff_tensor , p = 2 ).item ()
46
47
47
48
# Inside this conditional statement, we can be certain that
48
- # distance > 0, provided that budget > 0.
49
- # Hence, there is no risk of division by 0.
49
+ # distance > 0, provided that budget > 0.
50
+ # Hence, there is no risk of division by 0.
50
51
if distance > desirable_distance :
51
- images_adv .data .add_ ((1 - (desirable_distance / distance )) * diff_tensor )
52
- images_adv .data .clamp_ (0 , 1 )
52
+ images_adv .data .add_ (
53
+ (1 - (desirable_distance / distance )) * diff_tensor )
54
+ images_adv .data .clamp_ (0 , 1 )
53
55
return images_adv , labels
54
56
57
+
55
58
class LagrangianDRO (AdversarialTraining ):
56
59
"""
57
60
Execute DRO using the Lagrangian relaxation of the original theoretical
@@ -78,21 +81,23 @@ def attack(self, budget, data, steps=15):
78
81
budget: gamma in the original paper. Note that this parameter is
79
82
different from the budget parameter in other DRO classes.
80
83
"""
81
-
84
+
82
85
images , labels = data
83
86
images_adv = images .clone ().detach ().to (self .device )
84
87
images_adv .requires_grad_ (True )
85
-
88
+
86
89
for i in range (steps ):
87
90
if images_adv .grad is not None :
88
91
images_adv .grad .data .zero_ ()
89
92
outputs = self .model (images_adv )
90
- loss = self .loss_criterion (outputs , labels ) - budget * self .cost_function (images , images_adv )
93
+ loss = self .loss_criterion (
94
+ outputs , labels ) - budget * self .cost_function (images , images_adv )
91
95
loss .backward ()
92
96
images_adv .data .add_ (1 / math .sqrt (i + 1 ) * images_adv .grad )
93
97
images_adv .data .clamp_ (0 , 1 )
94
98
return images_adv , labels
95
99
100
+
96
101
class FrankWolfeDRO (AdversarialTraining ):
97
102
"""
98
103
Execute DRO using the Frank-Wolfe method together with the stochastic
@@ -124,19 +129,21 @@ def attack(self, budget, data, steps=15):
124
129
images , labels = data
125
130
images_adv = images .clone ().detach ().to (self .device )
126
131
images_adv .requires_grad_ (True )
127
-
132
+
128
133
for i in range (steps ):
129
134
if images_adv .grad is not None :
130
135
images_adv .grad .zero_ ()
131
136
outputs = self .model (images_adv )
132
137
loss = self .loss_criterion (outputs , labels )
133
138
loss .backward ()
134
139
135
- # desitnation corresponds to y_t in the paper by Bubeck.
136
- destination = images_adv .data + self .getOptimalDirection (budget = budget , data = images_adv .grad )
140
+ # desitnation corresponds to y_t in the paper by Bubeck.
141
+ destination = images_adv .data + \
142
+ self .getOptimalDirection (budget = budget , data = images_adv .grad )
137
143
destination = destination .to (self .device )
138
144
gamma = 2 / (i + 2 )
139
- images_adv .data = (1 - gamma ) * images_adv .data + gamma * destination
145
+ images_adv .data = (1 - gamma ) * \
146
+ images_adv .data + gamma * destination
140
147
images_adv .data .clamp_ (0 , 1 )
141
148
return images_adv , labels
142
149
@@ -154,15 +161,15 @@ def getOptimalDirection(self, budget, data):
154
161
data: gradient of the total loss with respect to the current
155
162
batch of adversarial examples. This corresponds to C in
156
163
Appendix B of the paper by Staib et al.
157
-
164
+
158
165
Returns:
159
166
X in Appendix B of Staib et al.'s paper
160
167
"""
161
168
162
169
# The number of samples
163
170
batch_size = data .size ()[0 ]
164
171
165
- # 'directions' corresponds to v's in Staib et al.'s paper.
172
+ # 'directions' corresponds to v's in Staib et al.'s paper.
166
173
directions = data .clone ().detach ().view ((batch_size , - 1 ))
167
174
directions = directions .to (self .device )
168
175
@@ -173,17 +180,17 @@ def getOptimalDirection(self, budget, data):
173
180
directions .pow_ (normalize_dim )
174
181
directions = F .normalize (directions , p = self .q , dim = 1 )
175
182
else :
176
- raise ValueError ("The value of q must be larger than 1." )
177
-
178
- # This corresponds to a's in the original paper.
183
+ raise ValueError ("The value of q must be larger than 1." )
184
+
185
+ # This corresponds to a's in the original paper.
179
186
products = []
180
187
for i , direction in enumerate (directions ):
181
188
sample = data [i ].view (- 1 )
182
189
products .append (torch .dot (direction , sample ))
183
190
products = torch .stack (products )
184
191
products = products .to (self .device )
185
192
186
- # This corresponds to epsilons in the original paper.
193
+ # This corresponds to epsilons in the original paper.
187
194
size_factors = products .clone ().detach ()
188
195
size_factors = size_factors .to (self .device )
189
196
if self .p == np .inf :
@@ -192,16 +199,17 @@ def getOptimalDirection(self, budget, data):
192
199
normalize_dim = 1 / (self .p - 1 )
193
200
size_factors .pow_ (normalize_dim )
194
201
distance = torch .norm (size_factors , p = self .p ).item ()
195
- size_factors = size_factors / distance # This is now normalized.
202
+ size_factors = size_factors / distance # This is now normalized.
196
203
else :
197
- raise ValueError ("The value of p must be larger than 1." )
198
-
204
+ raise ValueError ("The value of p must be larger than 1." )
205
+
199
206
outputs = []
200
207
for i , size_factor in enumerate (size_factors ):
201
208
outputs .append (directions [i ] * size_factor * budget )
202
209
result = torch .stack (outputs ).view (data .size ())
203
210
return result .to (self .device )
204
211
212
+
205
213
def trainDROModel (dro_type , epochs , steps_adv , budget , activation , batch_size , loss_criterion , cost_function = None ):
206
214
"""
207
215
Train a neural network using one of the following DRO methods:
@@ -210,7 +218,7 @@ def trainDROModel(dro_type, epochs, steps_adv, budget, activation, batch_size, l
210
218
This is also called WRM.
211
219
- the Frank-Wolfe method based approach developed by Staib et al.
212
220
"""
213
-
221
+
214
222
model = MNISTClassifier (activation = activation )
215
223
if dro_type == 'PGD' :
216
224
train_module = ProjetcedDRO (model , loss_criterion )
@@ -222,11 +230,16 @@ def trainDROModel(dro_type, epochs, steps_adv, budget, activation, batch_size, l
222
230
else :
223
231
raise ValueError ("The type of DRO is not valid." )
224
232
225
- train_module .train (budget = budget , batch_size = batch_size , epochs = epochs , steps_adv = steps_adv )
233
+ train_module .train (budget = budget , batch_size = batch_size ,
234
+ epochs = epochs , steps_adv = steps_adv )
226
235
folderpath = "./DRO_models/"
227
- filepath = folderpath + "{}_DRO_activation={}_epsilon={}.pt" .format (dro_type , activation , budget )
236
+ filepath = folderpath + \
237
+ "{}_DRO_activation={}_epsilon={}.pt" .format (
238
+ dro_type , activation , budget )
228
239
torch .save (model .state_dict (), filepath )
229
- print ("A neural network adversarially trained using {} is now saved at {}." .format (dro_type , filepath ))
240
+ print ("A neural network adversarially trained using {} is now saved at {}." .format (
241
+ dro_type , filepath ))
242
+
230
243
231
244
if __name__ == "__main__" :
232
245
epochs = 25
@@ -235,14 +248,21 @@ def trainDROModel(dro_type, epochs, steps_adv, budget, activation, batch_size, l
235
248
gammas = [0.0001 , 0.0003 , 0.001 , 0.003 , 0.01 , 0.03 , 0.1 , 0.3 , 1.0 , 3.0 ]
236
249
batch_size = 128
237
250
loss_criterion = nn .CrossEntropyLoss ()
238
- cost_function = lambda x , y : torch .dist (x , y , p = 2 ) ** 2
239
251
240
- trainDROModel ('PGD' , epochs , steps_adv , epsilon , 'relu' , batch_size , loss_criterion )
241
- trainDROModel ('FW' , epochs , steps_adv , epsilon , 'relu' , batch_size , loss_criterion )
252
+ def cost_function (x , y ): return torch .dist (x , y , p = 2 ) ** 2
253
+
254
+ trainDROModel ('PGD' , epochs , steps_adv , epsilon ,
255
+ 'relu' , batch_size , loss_criterion )
256
+ trainDROModel ('FW' , epochs , steps_adv , epsilon ,
257
+ 'relu' , batch_size , loss_criterion )
242
258
243
- trainDROModel ('PGD' , epochs , steps_adv , epsilon , 'elu' , batch_size , loss_criterion )
244
- trainDROModel ('FW' , epochs , steps_adv , epsilon , 'elu' , batch_size , loss_criterion )
259
+ trainDROModel ('PGD' , epochs , steps_adv , epsilon ,
260
+ 'elu' , batch_size , loss_criterion )
261
+ trainDROModel ('FW' , epochs , steps_adv , epsilon ,
262
+ 'elu' , batch_size , loss_criterion )
245
263
246
264
for gamma in gammas :
247
- trainDROModel ('Lag' , epochs , steps_adv , gamma , 'relu' , batch_size , loss_criterion , cost_function = cost_function )
248
- trainDROModel ('Lag' , epochs , steps_adv , gamma , 'elu' , batch_size , loss_criterion , cost_function = cost_function )
265
+ trainDROModel ('Lag' , epochs , steps_adv , gamma , 'relu' ,
266
+ batch_size , loss_criterion , cost_function = cost_function )
267
+ trainDROModel ('Lag' , epochs , steps_adv , gamma , 'elu' ,
268
+ batch_size , loss_criterion , cost_function = cost_function )
0 commit comments