def test_one_point(): """Point-wise test case with only one constraint. This leads to weight bounds that are unbounded-on-one-side. """ # Where the weight is too big. network = Network([ FullyConnectedLayer(np.eye(2), np.zeros(shape=(2,))), ReluLayer(), FullyConnectedLayer(np.array([[1.0, 0.0], [1.0, 0.0]]), np.array([0.0, 0.0])), ]) layer_index = 0 points = [[1.0, 0.5]] labels = [1] patcher = NetPatcher(network, layer_index, points, labels) patched = patcher.compute(steps=1) assert np.argmax(patched.compute(points)) == 1 # Where the weight is too small. network = Network([ FullyConnectedLayer(np.eye(2), np.array([0.0, 0.0])), ReluLayer(), FullyConnectedLayer(np.array([[1.0, 0.0], [1.0, 0.0]]), np.array([0.0, 2.0])), ]) layer_index = 0 points = [[1.0, 0.0]] labels = [0] patcher = NetPatcher(network, layer_index, points, labels) patched = patcher.compute(steps=1) assert np.argmax(patched.compute(points)) == 0
def test_from_planes(): """Test case to load key points from a set of labeled 2D polytopes. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ ReluLayer(), FullyConnectedLayer(np.eye(2), np.zeros(shape=(2,))) ]) layer_index = 1 planes = [ np.array([[-1.0, -3.0], [-0.5, -3.0], [-0.5, 9.0], [-1.0, 9.0]]), np.array([[8.0, -2.0], [16.0, -2.0], [16.0, 6.0], [8.0, 6.0]]), ] labels = [1, 0] patcher = NetPatcher.from_planes(network, layer_index, planes, labels) assert patcher.network is network assert patcher.layer_index is layer_index assert len(patcher.inputs) == (4 + 2) + (4 + 2) true_key_points = list(planes[0]) true_key_points += [np.array([-1.0, 0.0]), np.array([-0.5, 0.0])] true_key_points += list(planes[1]) true_key_points += [np.array([8.0, 0.0]), np.array([16.0, 0.0])] true_labels = ([1] * 6) + ([0] * 6) for true_point, true_label in zip(true_key_points, true_labels): try: i = next(i for i, point in enumerate(patcher.inputs) if np.allclose(point, true_point)) except StopIteration: assert False assert true_label == patcher.labels[i]
def test_compute_from_syrenn(): """Tests the it works given an arbitrary network and planes. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ReluLayer()]) planes = [np.array([[1.0, 2.0], [3.0, 4.0], [1.0, 2.0]]), np.array([[3.0, 2.0], [7.0, 4.0], [5.0, 2.0]])] transformed = network.transform_planes(planes, True, True) classifier = PlanesClassifier.from_syrenn(transformed) assert classifier.partially_computed classified = classifier.compute() assert len(classified) == len(planes) regions, labels = classified[0] assert np.allclose(regions, planes[0]) assert np.allclose(labels, [1]) regions, labels = classified[1] assert np.allclose(regions, planes[1]) assert np.allclose(labels, [0]) # Ensure it doesn't re-compute things it already knows. assert classifier.compute() is classified
def test_from_spec(): """Test case to load key points from a spec function. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ ReluLayer(), FullyConnectedLayer(np.eye(2), np.zeros(shape=(2,))) ]) layer_index = 1 region_of_interest = np.array([ [0.5, -3.0], [1.0, -3.0], [1.0, 9.0], [0.5, 9.0], ]) spec_fn = lambda i: np.isclose(i[:, 0], 1.0).astype(np.float32) patcher = NetPatcher.from_spec_function(network, layer_index, region_of_interest, spec_fn) assert patcher.network is network assert patcher.layer_index is layer_index assert len(patcher.inputs) == (4 + 2) true_key_points = list(region_of_interest) true_key_points += [np.array([0.5, 0.0]), np.array([1.0, 0.0])] true_labels = [0, 1, 1, 0, 0, 1] for true_point, true_label in zip(true_key_points, true_labels): try: i = next(i for i, point in enumerate(patcher.inputs) if np.allclose(point, true_point)) except StopIteration: assert False assert true_label == patcher.labels[i]
def test_compute_from_network(): """Tests the it works given an arbitrary network and planes. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ReluLayer()]) planes = [np.array([[1.0, 2.0], [3.0, 4.0], [1.0, 2.0]]), np.array([[3.0, 2.0], [7.0, 4.0], [5.0, 2.0]])] classifier = PlanesClassifier(network, planes, preimages=True) classifier.partial_compute() assert classifier.partially_computed transformed = network.transform_planes(planes, True, True) assert all(all(np.allclose(actual_polytope[0], truth_polytope[0]) and np.allclose(actual_polytope[1], truth_polytope[1]) for actual_polytope, truth_polytope in zip(actual, truth)) for actual, truth in zip(classifier.transformed_planes, transformed)) classified = classifier.compute() assert len(classified) == len(planes) regions, labels = classified[0] assert np.allclose(regions, planes[0]) assert np.allclose(labels, [1]) regions, labels = classified[1] assert np.allclose(regions, planes[1]) assert np.allclose(labels, [0]) # Ensure it doesn't re-compute things it already knows. assert classifier.compute() is classified
def test_compute_from_network(): """Tests the it works given an arbitrary network and lines. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ReluLayer()]) lines = [(np.array([0.0, 1.0]), np.array([0.0, -1.0])), (np.array([2.0, 3.0]), np.array([4.0, 3.0]))] classifier = LinesClassifier(network, lines, preimages=True) classifier.partial_compute() exactlines = network.exactlines(lines, True, True) assert all( np.allclose(actual[0], truth[0]) and np.allclose(actual[1], truth[1]) for actual, truth in zip(classifier.transformed_lines, exactlines)) classified = classifier.compute() assert len(classified) == len(lines) regions, labels = classified[0] assert np.allclose(regions, [[[0.0, 1.0], [0.0, 0.0]], [[0.0, 0.0], [0.0, -1.0]]]) assert np.allclose(labels, [1, 0]) regions, labels = classified[1] assert np.allclose(regions, [[[2.0, 3.0], [3.0, 3.0]], [[3.0, 3.0], [4.0, 3.0]]]) assert np.allclose(labels, [1, 0]) # Ensure it doesn't re-compute things it already knows. assert classifier.compute() is classified
def layer_jacobian(self, points, representatives): """Computes the Jacobian of the FULLY CONNECTED layer parameters. Returns (n_points, n_outputs, [weight_shape]) Basically, we assume WLOG that the layer is the first layer then we get something like for each point: B_nB_{n-1}...B_1Ax For each point we can collapse B_n...B_1 into a matrix B of shape (n_outputs, n_A_outputs) then we compute the einsum with x to get (n_outputs, n_A_outputs, n_A_inputs) which is what we want. """ pre_network = Network(self.network.layers[:self.layer_index]) points = pre_network.compute(points) n_points, A_in_dims = points.shape representatives = pre_network.compute(representatives) representatives = self.network.layers[self.layer_index].compute(representatives) n_points, A_out_dims = representatives.shape post_network = Network(self.network.layers[(self.layer_index+1):]) # (n_points, A_out, A_out) jacobian = np.repeat([np.eye(A_out_dims)], n_points, axis=0) # (A_out, n_points, A_out) jacobian = jacobian.transpose((1, 0, 2)) for layer in post_network.layers: if isinstance(layer, LINEAR_LAYERS): if isinstance(layer, ConcatLayer): assert not any(isinstance(input_layer, ConcatLayer) for input_layer in layer.input_layers) assert all(isinstance(input_layer, LINEAR_LAYERS) for input_layer in layer.input_layers) representatives = layer.compute(representatives) jacobian = jacobian.reshape((A_out_dims * n_points, -1)) jacobian = layer.compute(jacobian, jacobian=True) jacobian = jacobian.reshape((A_out_dims, n_points, -1)) elif isinstance(layer, ReluLayer): # (n_points, running_dims) zero_indices = (representatives <= 0) representatives[zero_indices] = 0. # (A_out, n_points, running_dims) jacobian[:, zero_indices] = 0. elif isinstance(layer, HardTanhLayer): big_indices = (representatives >= 1.) small_indices = (representatives <= -1.) np.clip(representatives, -1.0, 1.0, out=representatives) jacobian[:, big_indices] = 0. jacobian[:, small_indices] = 0. else: raise NotImplementedError # (A_out, n_points, n_classes) -> (n_points, n_classes, A_out) B = jacobian.transpose((1, 2, 0)) # (n_points, n_classes, n_A_in, n_A_out) C = np.einsum("nco,ni->ncio", B, points) return C, B
def test_compute_from_exactline_error(): """Tests that it requires the plural exactline*s*(), not the singular. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ReluLayer()]) exactline = network.exactline([-1.0, 1.0], [1.0, 0.0], True, True) try: classifier = LinesClassifier.from_exactlines(exactline) assert False except TypeError as e: pass
def linearize_around(self, inputs, network, layer_index): """Linearizes a network before/after a certain layer. We return a 3-tuple, (pre_lins, mid_inputs, post_lins) satisfying: 1) For all x in the same linear region as x_i: Network(x) = PostLin_i(Layer_l(PreLin_i(x))) 2) For all x_i: mid_inputs_i = PreLin_i(x_i) pre_lins and post_lins are lists of FullyConnectedLayers, one per input, and mid_inputs is a Numpy array. """ pre_network = Network(network.layers[:layer_index]) pre_linearized = self.linearize_network(inputs, pre_network) mid_inputs = pre_network.compute(inputs) pre_post_inputs = network.layers[layer_index].compute(mid_inputs) post_network = Network(network.layers[(layer_index + 1):]) post_linearized = self.linearize_network(pre_post_inputs, post_network) return pre_linearized, mid_inputs, post_linearized
def test_compute_from_network(): """Tests the it works given an arbitrary network and lines. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ReluLayer()]) lines = [(np.array([0.0, 1.0]), np.array([0.0, -1.0])), (np.array([2.0, 3.0]), np.array([4.0, 3.0]))] helper = IntegratedGradients(network, lines) helper.partial_compute() assert len(helper.exactlines) == len(lines) assert np.allclose(helper.exactlines[0], [0.0, 0.5, 1.0]) assert np.allclose(helper.exactlines[1], [0.0, 1.0]) assert helper.n_samples == [2, 1] attributions_0 = helper.compute_attributions(0) assert len(attributions_0) == len(lines) # The second component doesn't affect the 0-label at all, and the first # component is 0 everywhere, so we have int_0^0 0.0dx = 0.0 assert np.allclose(attributions_0[0], [0.0, 0.0]) # Gradient of the 0-label is (1.0, 0.0) everywhere since its in the first # orthant, and the partition has a size of (2.0, 0.0), so the IG is (2.0, # 0.0). assert np.allclose(attributions_0[1], [2.0, 0.0]) attributions_1 = helper.compute_attributions(1) assert len(attributions_1) == len(lines) # The gradient in the first partition is (0.0, 1.0) with a size of (0.0, # -1.0) -> contribution of (0.0, -1.0). In the second partition, (0.0, # 0.0)*(0.0, -1.0) = (0.0, 0.0). assert np.allclose(attributions_1[0], [0.0, -1.0]) # Gradient is (0, 1) and the size is (2, 0) so IG is (0, 0). assert np.allclose(attributions_1[1], [0.0, 0.0]) attributions_1_re = helper.compute_attributions(1) # Ensure it doesn't re-compute the attributions. assert attributions_1 is attributions_1_re
def test_compute_from_exactlines(): """Tests the it works given pre-transformed lines. """ if not Network.has_connection(): pytest.skip("No server connected.") network = Network([ReluLayer()]) lines = [(np.array([0.0, 1.0]), np.array([0.0, -1.0])), (np.array([2.0, 3.0]), np.array([4.0, 3.0]))] exactlines = network.exactlines(lines, True, True) classifier = LinesClassifier.from_exactlines(exactlines) classified = classifier.compute() assert len(classified) == len(lines) regions, labels = classified[0] assert np.allclose(regions, [[[0.0, 1.0], [0.0, 0.0]], [[0.0, 0.0], [0.0, -1.0]]]) assert np.allclose(labels, [1, 0]) regions, labels = classified[1] assert np.allclose(regions, [[[2.0, 3.0], [3.0, 3.0]], [[3.0, 3.0], [4.0, 3.0]]]) assert np.allclose(labels, [1, 0])
def test_already_good(): """Point-wise test case where all constraints are already met. We want to make sure that it doesn't change any weights unnecessarily. """ network = Network([ FullyConnectedLayer(np.eye(2), np.zeros(shape=(2,))), ReluLayer(), ]) layer_index = 0 points = [[1.0, 0.5], [2.0, -0.5], [4.0, 5.0]] labels = [0, 0, 1] patcher = NetPatcher(network, layer_index, points, labels) patched = patcher.compute(steps=1) patched_layer = patched.value_layers[0] assert np.allclose(patched_layer.weights.numpy(), np.eye(2)) assert np.allclose(patched_layer.biases.numpy(), np.zeros(shape=(2,)))
def test_optimal(): """Point-wise test case that the greedy algorithm can solve in 1 step. All it needs to do is triple the second component. """ network = Network([ FullyConnectedLayer(np.eye(2), np.zeros(shape=(2, ))), ReluLayer(), ]) layer_index = 0 points = [[1.0, 0.5], [2.0, -0.5], [5.0, 4.0]] labels = [1, 0, 1] patcher = ProvableRepair(network, layer_index, points, labels) patched = patcher.compute() assert patched.differ_index == 0 assert np.count_nonzero( np.argmax(patched.compute(points), axis=1) == labels) == 3
def compute(self): network = Network.deserialize(self.network.serialize()) if self.layer is not None: for param in self.get_parameters(network): param.requires_grad = False parameters = self.get_parameters(network, self.layer) for param in parameters: param.requires_grad = True if self.norm_objective: original_parameters = [param.detach().clone() for param in parameters] for param in original_parameters: # Do not train these, they're just for reference. param.requires_grad = False start = timer() optimizer = torch.optim.SGD(parameters, lr=self.lr, momentum=self.momentum) indices = list(range(len(self.inputs))) random.seed(24) self.epoched_out = None holdout_n_correct = self.holdout_n_correct(network) for epoch in range(self.epochs): # NOTE: In the paper, we checked this _after_ the inner loop. It # should only make a difference in the case where the network # already met the specification, so should make no difference to # the results. if self.auto_stop and self.is_done(network): self.maybe_print("100% training accuracy!") self.epoched_out = False break random.shuffle(indices) losses = [] for batch_start in range(0, len(self.inputs), self.batch_size): batch = slice(batch_start, batch_start + self.batch_size) inputs = torch.tensor([self.inputs[i] for i in indices[batch]]) labels = torch.tensor([self.labels[i] for i in indices[batch]]) # representatives = [self.representatives[i] for i in indices[batch]] optimizer.zero_grad() output = network.compute(inputs) loss = torch.nn.functional.cross_entropy(output, labels) if self.norm_objective: for curr_param, og_param in zip(parameters, original_parameters): delta = (curr_param - og_param).flatten() loss += torch.linalg.norm(delta, ord=2) loss += torch.linalg.norm(delta, ord=float("inf")) loss.backward() losses.append(loss) optimizer.step() self.maybe_print("Average Loss:", torch.mean(torch.tensor(losses))) if self.holdout_set is not None: new_holdout_n_correct = self.holdout_n_correct(network) self.maybe_print("New holdout n correct:", new_holdout_n_correct, "/", len(self.holdout_set)) if new_holdout_n_correct < holdout_n_correct: self.maybe_print("Holdout accuracy dropped, ending!") break holdout_n_correct = new_holdout_n_correct else: self.epoched_out = True for param in parameters: param.requires_grad = False self.timing = dict({ "total": timer() - start, }) return network