|
1 | | -# Encoding of a Neural Network in Z3. |
| 1 | +# Encoding of a Neural Network in Z3 |
| 2 | +# A custom loss function. |
| 3 | +# A Forward pass over one hiddebn layer. |
2 | 4 | from z3 import * |
3 | 5 | import numpy as np |
4 | 6 |
|
5 | 7 | if __name__ == "__main__": |
6 | 8 |
|
| 9 | + MIDDLE_LAYER_OUT = 6 |
| 10 | + # Z3 Sovler instance. |
7 | 11 | S = z3.Solver() |
8 | 12 |
|
9 | | - inputs = [z3.Real(f"{i}_l1") for i in ["x_input","y_input","n_input","a_input"]] |
10 | | - outputs1 = [z3.Real(f"activation_{i}") for i in range(10)] |
11 | | - |
12 | | - outputs = [z3.Real(f"{i}_l1") for i in ["C_0","C_1","C_2","C_3"]] |
| 13 | + # Inputs to the neural network, loss and intermediate sumVal. |
| 14 | + inputs = [z3.Real(f"{i}_l1") for i in ["x_input","y_input","n_input","a_input"]] |
13 | 15 | sumVal = z3.Real("sumVal") |
14 | 16 | loss = z3.Real("loss") |
15 | 17 |
|
| 18 | + # First Fully Connected Layer. weigths, bias and outputs |
16 | 19 | layer_1_weights = [[z3.Real(f"weight_l1_{i}_{j}") |
17 | | - for j in range(10)] |
| 20 | + for j in range(MIDDLE_LAYER_OUT)] |
18 | 21 | for i in range(4)] |
19 | 22 |
|
20 | | - layer_1_bias = [z3.Real(f"bias_l1_{i}") for i in range(10)] |
| 23 | + layer_1_bias = [z3.Real(f"bias_l1_{i}") for i in range(MIDDLE_LAYER_OUT)] |
| 24 | + outputs1 = [z3.Real(f"activation_{i}") for i in range(MIDDLE_LAYER_OUT)] |
21 | 25 |
|
| 26 | + # Second Fully Connected Layer. weigths, bias and outputs |
22 | 27 | layer_2_weights = [[z3.Real(f"weight_l2_{i}_{j}") |
23 | 28 | for j in range(4)] |
24 | | - for i in range(10)] |
| 29 | + for i in range(MIDDLE_LAYER_OUT)] |
25 | 30 |
|
26 | 31 | layer_2_bias = [z3.Real(f"bias_l2_{i}") for i in range(4)] |
| 32 | + outputs = [z3.Real(f"{i}_l1") for i in ["C_0","C_1","C_2","C_3"]] |
27 | 33 |
|
| 34 | + # Convert to np.matrix, np.array |
| 35 | + # Just to make doing the math easier. |
28 | 36 | weights1 = np.matrix(layer_1_weights) |
29 | 37 | bias1 = np.matrix(layer_1_bias) |
30 | | - activation1 = np.matrix(outputs1) |
31 | 38 | weights2 = np.matrix(layer_2_weights) |
32 | 39 | bias2 = np.matrix(layer_2_bias) |
33 | 40 | inp = np.matrix(inputs) |
34 | 41 |
|
| 42 | + # Single Forward pass on the first layer. |
35 | 43 | layer_1_out = weights1.T * inp.T + bias1.T |
36 | 44 |
|
| 45 | + # Apply ReLU() to output of layer-1. |
37 | 46 | for index, elems in enumerate(layer_1_out): |
38 | 47 | rhs = elems.tolist()[0][0] |
39 | 48 | lhs = outputs1[index] |
40 | 49 | # ReLU Activation |
41 | 50 | S.add(z3.If(rhs >= 0, lhs == rhs, lhs == 0)) |
42 | 51 | if index == 0: |
43 | 52 | print(z3.If(rhs >= 0, lhs == rhs, lhs == 0)) |
44 | | - # print(f"{lhs} == {rhs}", end="\n\n") |
45 | 53 |
|
| 54 | + # Single Forward pass on the second layer. |
| 55 | + # Use the activations from the first layer. |
| 56 | + activation1 = np.matrix(outputs1) |
46 | 57 | layer_2_out = activation1 * weights2 + bias2 |
47 | 58 |
|
| 59 | + # Apply ReLU() to output to layer-2. |
48 | 60 | for index, elems in enumerate(layer_2_out): |
49 | 61 | rhs = elems.tolist()[0][0] |
50 | 62 | lhs = outputs[index] |
51 | 63 | # ReLU Activation |
52 | 64 | S.add(z3.If(rhs >= 0, lhs == rhs, lhs == 0)) |
53 | 65 | if index == 0: |
54 | 66 | print(z3.If(rhs >= 0, lhs == rhs, lhs == 0)) |
55 | | - # print(f"{lhs} == {rhs}", end="\n\n") |
56 | 67 |
|
| 68 | + # dot product of coefficients |
| 69 | + # This is the case for the specific loss function |
| 70 | + # we are trying to use here. |
57 | 71 | sumVal = z3.Sum([(inputs[index] * outputs[index]) for index in range(4)]) |
58 | 72 | print(sumVal) |
59 | 73 |
|
| 74 | + # Put some restrictions on what the values for |
| 75 | + # the weights and the biases can be for both the layers. |
60 | 76 | weights_constraint = [] |
61 | 77 | [[weights_constraint.append(j > 2) for j in i] for i in layer_1_weights] |
62 | 78 | S.add(z3.And(weights_constraint)) |
| 79 | + |
63 | 80 | weights_constraint = [] |
64 | 81 | [[weights_constraint.append(j > 3) for j in i] for i in layer_2_weights] |
65 | 82 | S.add(z3.And(weights_constraint)) |
66 | 83 |
|
| 84 | + # Put some restrictions on what the values for |
| 85 | + # the weights and the biases can be for both the layers. |
67 | 86 | [S.add(elem > 1) for elem in layer_1_bias] |
68 | 87 | [S.add(elem > 7) for elem in layer_2_bias] |
69 | 88 |
|
70 | | - [S.add(j > 14) for j in inputs] |
| 89 | + # Specifying a range of values that the |
| 90 | + # can be passed as input to the neural network. |
| 91 | + [S.add(j > 0) for j in inputs] |
| 92 | + [S.add(j <= 1024) for j in inputs] |
| 93 | + |
| 94 | + # We want to enforce that none of the |
| 95 | + # last layer neurons give zero output. |
| 96 | + # this is a requirement for this |
| 97 | + # specific problem. |
71 | 98 | [S.add(j != 0) for j in outputs] |
72 | 99 |
|
73 | | - # Loss |
| 100 | + # Final Loss function that uses inputs and the sumVal computed above. |
74 | 101 | S.add(loss == sumVal ** 2 + 10000 * z3.If(inputs[1] - inputs[-1] >= 0, inputs[1] - inputs[-1], 0)) |
75 | | - S.add(loss == 0) |
| 102 | + S.add(z3.ForAll([x for x in inputs], (sumVal ** 2 + 10000 * z3.If(inputs[1] - inputs[-1] >= 0, inputs[1] - inputs[-1], 0)) == 0)) |
76 | 103 |
|
77 | | - # print(S.assertions()) |
78 | | - print(S.check()) |
79 | | - print(S.model()) |
| 104 | + # Write query to the file and check |
| 105 | + # with z3. |
| 106 | + # z3 -v:5 -st -smt2 ... |
| 107 | + with open("query.smt2", mode="w") as smt_writer: |
| 108 | + smt_writer.write(S.to_smt2()) |
| 109 | + |
| 110 | + # Uncomment if needed. |
| 111 | + # # print(S.assertions()) |
| 112 | + # print(S.check()) |
| 113 | + # print(S.model()) |
80 | 114 |
|
| 115 | +# A possible answer after 210 hours. |
81 | 116 | # [weight_l1_3_3 = 131073/65536, |
82 | 117 | # weight_l2_2_2 = 4, |
83 | 118 | # n_input_l1 = 15, |
|
0 commit comments