Skip to content

Commit 1a1d70b

Browse files
committed
Merge branch 'dev' of github.ibm.com:nemesis/nemesis into dev
2 parents e769459 + 8e26dfa commit 1a1d70b

File tree

15 files changed

+546
-194
lines changed

15 files changed

+546
-194
lines changed

art/classifiers/classifier.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ def class_gradient(self, x, label=None, logits=False):
119119
120120
:param x: Sample input with shape as expected by the model.
121121
:type x: `np.ndarray`
122-
:param label: Index of a specific per-class derivative. If `None`, then gradients for all
123-
classes will be computed.
124-
:type label: `int`
122+
:param label: Index of a specific per-class derivative. If an integer is provided, the gradient of that class
123+
output is computed for all samples. If multiple values as provided, the first dimension should
124+
match the batch size of `x`, and each value will be used as target for its corresponding sample in
125+
`x`. If `None`, then gradients for all classes will be computed for each sample.
126+
:type label: `int` or `list`
125127
:param logits: `True` if the prediction should be done at the logits layer.
126128
:type logits: `bool`
127129
:return: Array of gradients of input features w.r.t. each class in the form

art/classifiers/keras.py

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -126,39 +126,59 @@ def class_gradient(self, x, label=None, logits=False):
126126
127127
:param x: Sample input with shape as expected by the model.
128128
:type x: `np.ndarray`
129-
:param label: Index of a specific per-class derivative. If `None`, then gradients for all
130-
classes will be computed.
131-
:type label: `int`
129+
:param label: Index of a specific per-class derivative. If an integer is provided, the gradient of that class
130+
output is computed for all samples. If multiple values as provided, the first dimension should
131+
match the batch size of `x`, and each value will be used as target for its corresponding sample in
132+
`x`. If `None`, then gradients for all classes will be computed for each sample.
133+
:type label: `int` or `list`
132134
:param logits: `True` if the prediction should be done at the logits layer.
133135
:type logits: `bool`
134136
:return: Array of gradients of input features w.r.t. each class in the form
135137
`(batch_size, nb_classes, input_shape)` when computing for all classes, otherwise shape becomes
136138
`(batch_size, 1, input_shape)` when `label` parameter is specified.
137139
:rtype: `np.ndarray`
138140
"""
139-
if label is not None and label not in range(self._nb_classes):
140-
raise ValueError('Label %s is out of range.' % label)
141+
# Check value of label for computing gradients
142+
if not (label is None or (isinstance(label, (int, np.integer)) and label in range(self.nb_classes))
143+
or (type(label) is np.ndarray and len(label.shape) == 1 and (label < self.nb_classes).all()
144+
and label.shape[0] == x.shape[0])):
145+
raise ValueError('Label %s is out of range.' % str(label))
141146

142147
self._init_class_grads(label=label, logits=logits)
143148

144149
x_ = self._apply_processing(x)
145150

146-
if label is not None:
151+
if label is None:
152+
# Compute the gradients w.r.t. all classes
153+
if logits:
154+
grads = np.swapaxes(np.array(self._class_grads_logits([x_])), 0, 1)
155+
else:
156+
grads = np.swapaxes(np.array(self._class_grads([x_])), 0, 1)
157+
158+
grads = self._apply_processing_gradient(grads)
159+
160+
elif isinstance(label, (int, np.integer)):
161+
# Compute the gradients only w.r.t. the provided label
147162
if logits:
148163
grads = np.swapaxes(np.array(self._class_grads_logits_idx[label]([x_])), 0, 1)
149164
else:
150165
grads = np.swapaxes(np.array(self._class_grads_idx[label]([x_])), 0, 1)
151166

152167
grads = self._apply_processing_gradient(grads)
153168
assert grads.shape == (x_.shape[0], 1) + self.input_shape
169+
154170
else:
171+
# For each sample, compute the gradients w.r.t. the indicated target class (possibly distinct)
172+
unique_label = list(np.unique(label))
155173
if logits:
156-
grads = np.swapaxes(np.array(self._class_grads_logits([x_])), 0, 1)
174+
grads = np.array([self._class_grads_logits_idx[l]([x_]) for l in unique_label])
157175
else:
158-
grads = np.swapaxes(np.array(self._class_grads([x_])), 0, 1)
176+
grads = np.array([self._class_grads_idx[l]([x_]) for l in unique_label])
177+
grads = np.swapaxes(np.squeeze(grads, axis=1), 0, 1)
178+
lst = [unique_label.index(i) for i in label]
179+
grads = np.expand_dims(grads[np.arange(len(grads)), lst], axis=1)
159180

160181
grads = self._apply_processing_gradient(grads)
161-
assert grads.shape == (x_.shape[0], self.nb_classes) + self.input_shape
162182

163183
return grads
164184

@@ -278,35 +298,49 @@ def _init_class_grads(self, label=None, logits=False):
278298
import keras.backend as k
279299
k.set_learning_phase(0)
280300

281-
if label is not None:
282-
logger.debug('Computing class gradients for class %i.', label)
283-
if logits:
284-
if not hasattr(self, '_class_grads_logits_idx'):
285-
self._class_grads_logits_idx = [None for _ in range(self.nb_classes)]
286-
287-
if self._class_grads_logits_idx[label] is None:
288-
class_grads_logits = [k.gradients(self._preds_op[:, label], self._input)[0]]
289-
self._class_grads_logits_idx[label] = k.function([self._input], class_grads_logits)
290-
else:
291-
if not hasattr(self, '_class_grads_idx'):
292-
self._class_grads_idx = [None for _ in range(self.nb_classes)]
293-
294-
if self._class_grads_idx[label] is None:
295-
class_grads = [k.gradients(k.softmax(self._preds_op)[:, label], self._input)[0]]
296-
self._class_grads_idx[label] = k.function([self._input], class_grads)
301+
if len(self._output.shape) == 2:
302+
nb_outputs = self._output.shape[1]
297303
else:
304+
raise ValueError('Unexpected output shape for classification in Keras model.')
305+
306+
if label is None:
298307
logger.debug('Computing class gradients for all %i classes.', self.nb_classes)
299308
if logits:
300309
if not hasattr(self, '_class_grads_logits'):
301310
class_grads_logits = [k.gradients(self._preds_op[:, i], self._input)[0]
302-
for i in range(self.nb_classes)]
311+
for i in range(nb_outputs)]
303312
self._class_grads_logits = k.function([self._input], class_grads_logits)
304313
else:
305314
if not hasattr(self, '_class_grads'):
306315
class_grads = [k.gradients(k.softmax(self._preds_op)[:, i], self._input)[0]
307-
for i in range(self.nb_classes)]
316+
for i in range(nb_outputs)]
308317
self._class_grads = k.function([self._input], class_grads)
309318

319+
else:
320+
if type(label) is int:
321+
unique_labels = [label]
322+
logger.debug('Computing class gradients for class %i.', label)
323+
else:
324+
unique_labels = np.unique(label)
325+
logger.debug('Computing class gradients for classes %s.', str(unique_labels))
326+
327+
if logits:
328+
if not hasattr(self, '_class_grads_logits_idx'):
329+
self._class_grads_logits_idx = [None for _ in range(nb_outputs)]
330+
331+
for l in unique_labels:
332+
if self._class_grads_logits_idx[l] is None:
333+
class_grads_logits = [k.gradients(self._preds_op[:, l], self._input)[0]]
334+
self._class_grads_logits_idx[l] = k.function([self._input], class_grads_logits)
335+
else:
336+
if not hasattr(self, '_class_grads_idx'):
337+
self._class_grads_idx = [None for _ in range(nb_outputs)]
338+
339+
for l in unique_labels:
340+
if self._class_grads_idx[l] is None:
341+
class_grads = [k.gradients(k.softmax(self._preds_op)[:, l], self._input)[0]]
342+
self._class_grads_idx[l] = k.function([self._input], class_grads)
343+
310344
def _get_layers(self):
311345
"""
312346
Return the hidden layers in the model, if applicable.

art/classifiers/keras_unittest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,39 @@ def _test_shapes(self, custom_activation=False):
139139
loss_grads = classifier.loss_gradient(x_test[:11], y_test[:11])
140140
self.assertTrue(loss_grads.shape == x_test[:11].shape)
141141

142+
def test_class_gradient(self):
143+
(_, _), (x_test, y_test) = self.mnist
144+
classifier = KerasClassifier((0, 1), self.model_mnist)
145+
146+
# Test all gradients label
147+
grads = classifier.class_gradient(x_test)
148+
149+
self.assertTrue(np.array(grads.shape == (NB_TEST, 10, 28, 28, 1)).all())
150+
self.assertTrue(np.sum(grads) != 0)
151+
152+
# Test 1 gradient label = 5
153+
grads = classifier.class_gradient(x_test, label=5)
154+
155+
self.assertTrue(np.array(grads.shape == (NB_TEST, 1, 28, 28, 1)).all())
156+
self.assertTrue(np.sum(grads) != 0)
157+
158+
# Test a set of gradients label = array
159+
label = np.random.randint(5, size=NB_TEST)
160+
grads = classifier.class_gradient(x_test, label=label)
161+
162+
self.assertTrue(np.array(grads.shape == (NB_TEST, 1, 28, 28, 1)).all())
163+
self.assertTrue(np.sum(grads) != 0)
164+
165+
def test_loss_gradient(self):
166+
(_, _), (x_test, y_test) = self.mnist
167+
classifier = KerasClassifier((0, 1), self.model_mnist)
168+
169+
# Test gradient
170+
grads = classifier.loss_gradient(x_test, y_test)
171+
172+
self.assertTrue(np.array(grads.shape == (NB_TEST, 28, 28, 1)).all())
173+
self.assertTrue(np.sum(grads) != 0)
174+
142175
def test_functional_model(self):
143176
self._test_functional_model(custom_activation=True)
144177
self._test_functional_model(custom_activation=False)

art/classifiers/mxnet.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,11 @@ def class_gradient(self, x, label=None, logits=False):
151151
152152
:param x: Sample input with shape as expected by the model.
153153
:type x: `np.ndarray`
154-
:param label: Index of a specific per-class derivative. If `None`, then gradients for all
155-
classes will be computed.
156-
:type label: `int`
154+
:param label: Index of a specific per-class derivative. If an integer is provided, the gradient of that class
155+
output is computed for all samples. If multiple values as provided, the first dimension should
156+
match the batch size of `x`, and each value will be used as target for its corresponding sample in
157+
`x`. If `None`, then gradients for all classes will be computed for each sample.
158+
:type label: `int` or `list`
157159
:param logits: `True` if the prediction should be done at the logits layer.
158160
:type logits: `bool`
159161
:return: Array of gradients of input features w.r.t. each class in the form
@@ -163,14 +165,31 @@ def class_gradient(self, x, label=None, logits=False):
163165
"""
164166
from mxnet import autograd, nd
165167

166-
if label is not None and label not in range(self._nb_classes):
167-
raise ValueError('Label %s is out of range.' % label)
168+
# Check value of label for computing gradients
169+
if not (label is None or (isinstance(label, (int, np.integer)) and label in range(self.nb_classes))
170+
or (type(label) is np.ndarray and len(label.shape) == 1 and (label < self.nb_classes).all()
171+
and label.shape[0] == x.shape[0])):
172+
raise ValueError('Label %s is out of range.' % str(label))
168173

169174
x_ = self._apply_processing(x)
170175
x_ = nd.array(x_, ctx=self._ctx)
171176
x_.attach_grad()
172177

173-
if label is not None:
178+
if label is None:
179+
with autograd.record(train_mode=False):
180+
if logits is True:
181+
preds = self._model(x_)
182+
else:
183+
preds = self._model(x_).softmax()
184+
class_slices = [preds[:, i] for i in range(self.nb_classes)]
185+
186+
grads = []
187+
for slice_ in class_slices:
188+
slice_.backward(retain_graph=True)
189+
grad = x_.grad.asnumpy()
190+
grads.append(grad)
191+
grads = np.swapaxes(np.array(grads), 0, 1)
192+
elif isinstance(label, (int, np.integer)):
174193
with autograd.record(train_mode=False):
175194
if logits is True:
176195
preds = self._model(x_)
@@ -181,19 +200,26 @@ def class_gradient(self, x, label=None, logits=False):
181200
class_slice.backward()
182201
grads = np.expand_dims(x_.grad.asnumpy(), axis=1)
183202
else:
203+
unique_labels = list(np.unique(label))
204+
184205
with autograd.record(train_mode=False):
185206
if logits is True:
186207
preds = self._model(x_)
187208
else:
188209
preds = self._model(x_).softmax()
189-
class_slices = [preds[:, i] for i in range(self.nb_classes)]
210+
class_slices = [preds[:, i] for i in unique_labels]
190211

191212
grads = []
192213
for slice_ in class_slices:
193214
slice_.backward(retain_graph=True)
194215
grad = x_.grad.asnumpy()
195216
grads.append(grad)
217+
196218
grads = np.swapaxes(np.array(grads), 0, 1)
219+
lst = [unique_labels.index(i) for i in label]
220+
grads = grads[np.arange(len(grads)), lst]
221+
grads = np.expand_dims(grads, axis=1)
222+
grads = self._apply_processing_gradient(grads)
197223

198224
grads = self._apply_processing_gradient(grads)
199225

@@ -220,7 +246,8 @@ def loss_gradient(self, x, y):
220246
with autograd.record(train_mode=False):
221247
preds = self._model(x_)
222248
loss = loss(preds, y_)
223-
loss.backward()
249+
250+
loss.backward()
224251
grads = x_.grad.asnumpy()
225252
grads = self._apply_processing_gradient(grads)
226253
assert grads.shape == x.shape

art/classifiers/mxnet_unittest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ def test_class_gradient(self):
8181
# Assert gradient computed for the same class on same input are equal
8282
self.assertAlmostEqual(np.sum(grads_all[:, 3] - grads), 0, places=6)
8383

84+
# Test a set of gradients label = array
85+
labels = np.random.randint(5, size=NB_TEST)
86+
grads = self.classifier.class_gradient(x_test, label=labels)
87+
88+
self.assertTrue(np.array(grads.shape == (NB_TEST, 1, 1, 28, 28)).all())
89+
self.assertTrue(np.sum(grads) != 0)
90+
8491
def test_loss_gradient(self):
8592
# Get MNIST
8693
(_, _), (x_test, y_test) = self.mnist

0 commit comments

Comments
 (0)