Skip to content

Commit 5f077b0

Browse files
committed
relu_to_optimization replaces some linear constraints on variable with variable bounds.
1 parent 0757db2 commit 5f077b0

File tree

3 files changed

+45
-21
lines changed

3 files changed

+45
-21
lines changed

neural_network_lyapunov/relu_to_optimization.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def _compute_linear_output_bound_by_lp(
298298
) if linear_layer.bias is None else linear_layer.bias[j]
299299
Ain_linear_input, Ain_neuron_output, Ain_neuron_binary,\
300300
rhs_in, Aeq_linear_input, Aeq_neuron_output,\
301-
Aeq_neuron_binary, rhs_eq, _, _ =\
301+
Aeq_neuron_binary, rhs_eq, _, _, _, _ =\
302302
_add_constraint_by_neuron(
303303
linear_layer.weight[j], bij, relu_layer,
304304
torch.tensor(previous_neuron_input_lo[
@@ -588,7 +588,14 @@ def output_constraint(self, x_lo, x_up,
588588
(self.model[-1].out_features, ), dtype=self.dtype)
589589
else:
590590
mip_constr_return.Cout = self.model[-1].bias.clone()
591-
591+
binary_lo = torch.zeros((self.num_relu_units,), dtype=self.dtype)
592+
binary_up = torch.ones((self.num_relu_units,), dtype=self.dtype)
593+
# If the input to the relu is always >= 0, then the relu will always
594+
# be active.
595+
binary_lo[z_pre_relu_lo >= 0] = 1.
596+
# If the input to the relu is always <= 0, then the relu will always
597+
# be inactive.
598+
binary_up[z_pre_relu_up <= 0] = 0.
592599
mip_constr_return.Ain_input = Ain_input[:ineq_constr_count]
593600
mip_constr_return.Ain_slack = Ain_slack[:ineq_constr_count]
594601
mip_constr_return.Ain_binary = Ain_binary[:ineq_constr_count]
@@ -597,6 +604,10 @@ def output_constraint(self, x_lo, x_up,
597604
mip_constr_return.Aeq_slack = Aeq_slack[:eq_constr_count]
598605
mip_constr_return.Aeq_binary = Aeq_binary[:eq_constr_count]
599606
mip_constr_return.rhs_eq = rhs_eq[:eq_constr_count]
607+
mip_constr_return.input_lo = x_lo
608+
mip_constr_return.input_up = x_up
609+
mip_constr_return.binary_lo = binary_lo
610+
mip_constr_return.binary_up = binary_up
600611
return (mip_constr_return, z_pre_relu_lo, z_pre_relu_up,
601612
z_post_relu_lo, z_post_relu_up, output_lo, output_up)
602613

@@ -994,6 +1005,8 @@ def _add_constraint_by_neuron(
9941005
Aeq_neuron_output = torch.empty((0, 1), dtype=dtype)
9951006
Aeq_binary = torch.empty((0, 1), dtype=dtype)
9961007
rhs_eq = torch.empty((0, ), dtype=dtype)
1008+
binary_lo = 0
1009+
binary_up = 1
9971010
else:
9981011
# The (leaky) ReLU is always active, or always inactive. If
9991012
# the lower bound output_lo[j] >= 0, then it is always active,
@@ -1004,18 +1017,17 @@ def _add_constraint_by_neuron(
10041017
# zᵢ₊₁(j) = c*((Wᵢzᵢ)(j) + bᵢ(j)) and βᵢ(j) = 0
10051018
if neuron_input_lo >= 0:
10061019
slope = 1.
1007-
binary_value = 1
1020+
binary_lo = 1
1021+
binary_up = 1
10081022
elif neuron_input_up <= 0:
10091023
slope = relu_layer.negative_slope if isinstance(
10101024
relu_layer, nn.LeakyReLU) else 0.
1011-
binary_value = 0.
1012-
Aeq_linear_input = torch.cat((-slope * Wij.reshape(
1013-
(1, -1)), torch.zeros((1, Wij.numel()), dtype=dtype)),
1014-
dim=0)
1015-
Aeq_neuron_output = torch.tensor([[1.], [0]], dtype=dtype)
1016-
Aeq_binary = torch.tensor([[0.], [1.]], dtype=dtype)
1017-
rhs_eq = torch.stack(
1018-
(slope * bij, torch.tensor(binary_value, dtype=dtype)))
1025+
binary_lo = 0
1026+
binary_up = 1
1027+
Aeq_linear_input = -slope * Wij.reshape((1, -1))
1028+
Aeq_neuron_output = torch.tensor([[1.]], dtype=dtype)
1029+
Aeq_binary = torch.tensor([[0.]], dtype=dtype)
1030+
rhs_eq = slope * bij.reshape((1,))
10191031
Ain_linear_input = torch.empty((0, Wij.numel()), dtype=dtype)
10201032
Ain_neuron_output = torch.empty((0, 1), dtype=dtype)
10211033
Ain_binary = torch.empty((0, 1), dtype=dtype)
@@ -1024,7 +1036,7 @@ def _add_constraint_by_neuron(
10241036
relu_layer, neuron_input_lo, neuron_input_up)
10251037
return Ain_linear_input, Ain_neuron_output, Ain_binary, rhs_in,\
10261038
Aeq_linear_input, Aeq_neuron_output, Aeq_binary, rhs_eq,\
1027-
neuron_output_lo, neuron_output_up
1039+
neuron_output_lo, neuron_output_up, binary_lo, binary_up
10281040

10291041

10301042
def _add_constraint_by_layer(linear_layer, relu_layer,
@@ -1057,10 +1069,13 @@ def _add_constraint_by_layer(linear_layer, relu_layer,
10571069
z_next_up = []
10581070
bias = linear_layer.bias if linear_layer.bias is not None else \
10591071
torch.zeros((linear_layer.out_features,), dtype=dtype)
1072+
binary_lo = torch.zeros((linear_layer.out_features,), dtype=dtype)
1073+
binary_up = torch.ones((linear_layer.out_features,), dtype=dtype)
10601074
for j in range(linear_layer.out_features):
10611075
Ain_linear_input, Ain_neuron_output, Ain_binary_j, rhs_in_j,\
10621076
Aeq_linear_input, Aeq_neuron_output, Aeq_binary_j, rhs_eq_j,\
1063-
neuron_output_lo, neuron_output_up = _add_constraint_by_neuron(
1077+
neuron_output_lo, neuron_output_up, binary_lo[j], binary_up[j] =\
1078+
_add_constraint_by_neuron(
10641079
linear_layer.weight[j], bias[j], relu_layer,
10651080
linear_output_lo[j], linear_output_up[j])
10661081
Ain_z_curr.append(Ain_linear_input)
@@ -1089,4 +1104,4 @@ def _add_constraint_by_layer(linear_layer, relu_layer,
10891104
torch.cat(Ain_binary, dim=0), torch.cat(rhs_in, dim=0),\
10901105
torch.cat(Aeq_z_curr, dim=0), torch.cat(Aeq_z_next, dim=0),\
10911106
torch.cat(Aeq_binary, dim=0), torch.cat(rhs_eq, dim=0),\
1092-
torch.stack(z_next_lo), torch.stack(z_next_up)
1107+
torch.stack(z_next_lo), torch.stack(z_next_up), binary_lo, binary_up

neural_network_lyapunov/test/test_relu_to_optimization.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,7 @@ def test_model(model, method):
297297
num_ineq = (relu_free_pattern.num_relu_units -
298298
num_z_pre_relu_lo_positive -
299299
num_z_pre_relu_up_negative) * 4 + 4
300-
num_eq = (num_z_pre_relu_lo_positive + num_z_pre_relu_up_negative)\
301-
* 2
300+
num_eq = (num_z_pre_relu_lo_positive + num_z_pre_relu_up_negative)
302301
self.assertEqual(mip_constr_return.Ain_input.shape, (num_ineq, 2))
303302
self.assertEqual(mip_constr_return.Ain_slack.shape,
304303
(num_ineq, relu_free_pattern.num_relu_units))
@@ -345,6 +344,10 @@ def test_input_output(x):
345344
mip_constr_return.Aeq_binary.detach().numpy() @
346345
beta_var
347346
== mip_constr_return.rhs_eq.squeeze().detach().numpy())
347+
if mip_constr_return.binary_lo is not None:
348+
con.append(beta_var <= mip_constr_return.binary_up)
349+
if mip_constr_return.binary_up is not None:
350+
con.append(beta_var >= mip_constr_return.binary_lo)
348351
objective = cp.Minimize(0.)
349352
prob = cp.Problem(objective, con)
350353
prob.solve(solver=cp.GUROBI)

neural_network_lyapunov/test/test_relu_to_optimization_utils.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def constraint_test(self, Wij, bij, relu_layer, neuron_input_lo,
1515
neuron_input_up):
1616
Ain_linear_input, Ain_neuron_output, Ain_binary, rhs_in,\
1717
Aeq_linear_input, Aeq_neuron_output, Aeq_binary, rhs_eq,\
18-
neuron_output_lo, neuron_output_up = \
18+
neuron_output_lo, neuron_output_up, binary_lo, binary_up = \
1919
relu_to_optimization._add_constraint_by_neuron(
2020
Wij, bij, relu_layer, neuron_input_lo, neuron_input_up)
2121

@@ -35,7 +35,10 @@ def constraint_test(self, Wij, bij, relu_layer, neuron_input_lo,
3535
linear_input = model.addVars(Wij.numel(),
3636
lb=-gurobipy.GRB.INFINITY)
3737
neuron_output = model.addVars(1, lb=-gurobipy.GRB.INFINITY)
38-
binary = model.addVars(1, vtype=gurobipy.GRB.BINARY)
38+
binary = model.addVars(1,
39+
lb=binary_lo,
40+
ub=binary_up,
41+
vtype=gurobipy.GRB.BINARY)
3942
model.addMConstrs(
4043
[Ain_linear_input, Ain_neuron_output, Ain_binary],
4144
[linear_input, neuron_output, binary],
@@ -107,7 +110,7 @@ def constraint_test(self, linear_layer, relu_layer, z_curr_lo, z_curr_up):
107110
linear_output_lo, linear_output_up = mip_utils.propagate_bounds(
108111
linear_layer, z_curr_lo, z_curr_up)
109112
Ain_z_curr, Ain_z_next, Ain_binary, rhs_in, Aeq_z_curr, Aeq_z_next,\
110-
Aeq_binary, rhs_eq, z_next_lo, z_next_up = \
113+
Aeq_binary, rhs_eq, z_next_lo, z_next_up, binary_lo, binary_up = \
111114
relu_to_optimization._add_constraint_by_layer(
112115
linear_layer, relu_layer, linear_output_lo, linear_output_up)
113116
z_next_lo_expected, z_next_up_expected = mip_utils.propagate_bounds(
@@ -130,6 +133,8 @@ def constraint_test(self, linear_layer, relu_layer, z_curr_lo, z_curr_up):
130133
z_next = model.addVars(linear_layer.out_features,
131134
lb=-gurobipy.GRB.INFINITY)
132135
beta = model.addVars(linear_layer.out_features,
136+
lb=binary_lo,
137+
ub=binary_up,
133138
vtype=gurobipy.GRB.BINARY)
134139
model.addMConstrs(
135140
[torch.eye(linear_layer.in_features, dtype=self.dtype)],
@@ -185,7 +190,7 @@ def constraint_gradient_test(self, linear_layer, relu_layer, z_curr_lo,
185190
linear_output_lo, linear_output_up = mip_utils.propagate_bounds(
186191
linear_layer, z_curr_lo, z_curr_up)
187192
Ain_z_curr, Ain_z_next, Ain_binary, rhs_in, Aeq_z_curr, Aeq_z_next,\
188-
Aeq_binary, rhs_eq, z_next_lo, z_next_up =\
193+
Aeq_binary, rhs_eq, z_next_lo, z_next_up, binary_lo, binary_up =\
189194
relu_to_optimization._add_constraint_by_layer(
190195
linear_layer, relu_layer, linear_output_lo, linear_output_up)
191196

@@ -213,7 +218,8 @@ def eval_fun(linear_layer_weight, linear_layer_bias, input_lo_np,
213218
linear_layer, torch.from_numpy(input_lo_np),
214219
torch.from_numpy(input_up_np))
215220
Ain_z_curr, Ain_z_next, Ain_binary, rhs_in, Aeq_z_curr,\
216-
Aeq_z_next, Aeq_binary, rhs_eq, z_next_lo, z_next_up =\
221+
Aeq_z_next, Aeq_binary, rhs_eq, z_next_lo, z_next_up,\
222+
binary_lo, binary_up =\
217223
relu_to_optimization._add_constraint_by_layer(
218224
linear_layer, relu_layer, linear_output_lo,
219225
linear_output_up)

0 commit comments

Comments
 (0)