This notebook is an experiment with the NeuralVerification package (that we shorten to NV
here) and decomposition methods available in LazySets.
using Revise, NeuralVerification, LazySets # requires schillic/1176_supportfunction
using LazySets.Approximations
const NV = NeuralVerification
networks_folder = "/Users/forets/.julia/dev/NeuralVerification/examples/networks/"
In NV
, neural networks are represented as a vector of layers, where a Layer
consists of the weights matrix, the bias (an affine translation) and the activation function.
struct Layer{F<:ActivationFunction, N<:Number}
weights::Matrix{N}
bias::Vector{N}
activation::F
end
struct Network
layers::Vector{Layer} # layers includes output layer
end
Now we will work with one "small" examples in NeuralVerification/examples/networks/.
model = "cartpole_nnet.nnet" # 4 layers, first one 16x4 and the other ones 16 x 16
#model = "ACASXU_run2a_4_5_batch_2000.nnet" # 7 layers, 50x5
#model = "mnist1.nnet" # 25 x 784 and 10 x 25
#model = "mnist_large.nnet" # 25 x 784 and 10 x 25
#model = "mnist2.nnet" # 100 x 784 and 10 x 100
nnet = read_nnet(networks_folder * model);
typeof(nnet)
The number of layers in this neural network as well as the number of nodes in each layer can be obtained as follows.
L = nnet.layers
length(L)
The first two layers have two nodes each and the last layer (the output layer) has one node.
NV.n_nodes.(L)
dump(L[1])
[size(Li.weights) for Li in L]
We can directly see the weights matrix and the bias fields of the first layer:
L[1].weights
n = size(L[1].weights)[2]
L[1].bias
Random input set for cartpole
:
H0 = rand(Hyperrectangle, dim=n)
An input set for ACAS is defined in the test/runtime3.jl
file so we use it:
center = [0.40143256, 0.30570418, -0.49920412, 0.52838383, 0.4]
radius = [0.0015, 0.0015, 0.0015, 0.0015, 0.0015]
H0 = Hyperrectangle(center, radius)
dim(H)
Let $X_1$ be the set obtained after we apply the first layer, $X_1 = A_1 H_0 \oplus b_1$.
A1 = L[1].weights
b1 = L[1].bias
X1 = A1 * H0 ⊕ b1
X1_am = AffineMap(A1, H0, b1) # as an affine map
Dimension of $X_1$:
dim(X1)
Let's consider an element from $X_1$ and apply the rectification operation:
an_element(X1)[1:10]
v = LazySets.rectify(an_element(X1))
v[1:10]
count(!iszero, v) # number of elements which are not zero
We can apply a box approximation to the set and then apply the rectification, since it is easy to apply the rectification to a hyperrectangular set.
function rectify(H::AbstractHyperrectangle)
Hyperrectangle(low=LazySets.rectify(low(H)), high=LazySets.rectify(high(H)))
end
rectify_oa(X) = rectify(box_approximation(X))
LazySets.rectify(rand(2))
X1_r = rectify_oa(X1) # concrete set
dim(X1_r)
L[2].weights
# next layer
A2 = L[2].weights
b2 = L[2].bias
X2(Y) = A2 * (Y) ⊕ b2
using Plots, LazySets, LazySets.Approximations
using LazySets: translate
# generate some data
NUMLAYERS = 5
weight_matrices = [rand(2, 2) for i in 1:NUMLAYERS]
bias_vectors = [rand(2) for i in 1:NUMLAYERS];
# initial set
H0 = Hyperrectangle{Float64}([0.841145, -4.496269], [0.911519, 0.962476])
vertices_list(H0)
# showing the set after the first application of affine map
plot(H0, color=:blue)
W, b = weight_matrices[1], bias_vectors[1]
plot!(translate(linear_map(W, H0), b), color=:red)
function nnet_box(H0, weight_matrices, bias_vectors)
relued_subsets = Vector{Hyperrectangle{Float64}}()
result = H0
NUMLAYERS = length(bias_vectors)
@inbounds for i in 1:NUMLAYERS
W, b = weight_matrices[i], bias_vectors[i]
# lazy affine map
Z = AffineMap(W, result, b)
# overapproximate with a box and rectify
result = rectify_oa(Z)
push!(relued_subsets, result)
end
return relued_subsets
end
relued_subsets = nnet_box(H0, weight_matrices, bias_vectors)
plot(relued_subsets)
plot!(first(relued_subsets), color=:red)
plot!(last(relued_subsets), color=:grey)
plot!(translate(linear_map(first(weight_matrices), H0), first(bias_vectors)), color=:red)
plot!(H0)
u = translate(linear_map(first(weight_matrices), H0), first(bias_vectors))
ru = rectify_oa(u)
plot(u)
plot!(ru)
W, b = weight_matrices[1], bias_vectors[1]
result = H0
Z = AffineMap(W, result, b)
using LazySets: compute_union_of_projections!
R = Rectification(Z)
res = compute_union_of_projections!(R)
array(res)
using Optim
# res_ch = overapproximate(ConvexHullArray(array(res)), HPolygon, 1e-2)
# the exact support vector of an intersection is not implemented
# this doesn't work: need that iterative refinement works with support functions . . .
res_ch = overapproximate(ConvexHullArray(array(res)), PolarDirections(40))
plot(res_ch)
using LazySets: compute_union_of_projections!
function nnet_lazy(H0, weight_matrices, bias_vectors)
relued_subsets = Vector{ConvexHullArray}()
result = H0
NUMLAYERS = length(bias_vectors)
@inbounds for i in 1:NUMLAYERS
W, b = weight_matrices[i], bias_vectors[i]
# lazy affine map
Z = AffineMap(W, result, b)
# overapproximate with a box and rectify
R = Rectification(Z)
result = compute_union_of_projections!(R)
result = helper_convexify(result)
# Idea: make helper_convexify to return a concrete set, not a lazy set,
# by using template directions
push!(relued_subsets, result)
end
return relued_subsets
end
helper_convexify(res::UnionSetArray) = ConvexHullArray(array(res))
helper_convexify(res::LazySet) = ConvexHullArray([res]) # it is a single set => already convex
result = nnet_lazy(H0, weight_matrices, bias_vectors);
plot(overapproximate.(result, Ref(PolarDirections(40))))
plot!(nnet_box(H0, weight_matrices, bias_vectors))
@btime nnet_box($H0, $weight_matrices, $bias_vectors);
@btime nnet_lazy($H0, $weight_matrices, $bias_vectors);
using LazySets.Approximations: AbstractDirections
using LazySets: compute_union_of_projections!
using Optim
function nnet_lazy_template(H0, weight_matrices, bias_vectors, dirs::Type{AbstractDirections})
relued_subsets = Vector{HPolytope{Float64}}()
result = H0
NUMLAYERS = length(bias_vectors)
@inbounds for i in 1:NUMLAYERS
W, b = weight_matrices[i], bias_vectors[i]
# lazy affine map
Z = AffineMap(W, result, b)
# overapproximate with a box and rectify
R = Rectification(Z)
result = compute_union_of_projections!(R)
result = helper_convexify(result)
n = size(W, 2) # state-space dimension
result = overapproximate(result, dirs(n))
push!(relued_subsets, result)
end
return relued_subsets
end
result_lazy_template = nnet_lazy_template(H0, weight_matrices, bias_vectors, OctDirections(2));
result_lazy = nnet_lazy(H0, weight_matrices, bias_vectors);
plot(overapproximate.(result_lazy, Ref(PolarDirections(40))))
#plot!(nnet_box(H0, weight_matrices, bias_vectors))
plot!(nnet_lazy_template(H0, weight_matrices, bias_vectors, OctDirections(2)))
@btime nnet_lazy_template($H0, $weight_matrices, $bias_vectors, OctDirections(2));
@btime begin
result_lazy = nnet_lazy(H0, weight_matrices, bias_vectors);
overapproximate.(result_lazy, Ref(PolarDirections(40)))
end;
So overapproximating at each layer is actually faster than overapproximated the nested lazy set (as expected), but it is less precise as the plot above shows.
@btime ρ([1.0, 1.0], nnet_lazy_template($H0, $weight_matrices, $bias_vectors, OctDirections(2))[1])
@btime ρ([1.0, 1.0], nnet_lazy($H0, $weight_matrices, $bias_vectors)[1])
However, if one is interested in checking the support function it is faster to use the nested lazy solution.
# (NOT TESTED YET)
using LazySets.Approximations: AbstractDirections
using LazySets: compute_union_of_projections!
function nnet_zonotope(H0, weight_matrices, bias_vectors)
relued_subsets = [] #Vector{Zonotope{Float64}}()
result = convert(Zonotope, H0)
NUMLAYERS = length(bias_vectors)
@inbounds for i in 1:NUMLAYERS
W, b = weight_matrices[i], bias_vectors[i]
# lazy affine map
Z = translate(linear_map(W, result), b)
# overapproximate with a box and rectify
R = Rectification(Z)
result = compute_union_of_projections!(R)
result = helper_convexify(result)
n = size(W, 2) # state-space dimension
push!(relued_subsets, result)
end
return relued_subsets
end
model = "mnist_small.nnet"
nnet = read_nnet(networks_folder * model);
weight_matrices = [li.weights for li in nnet.layers]
bias_vectors = [li.bias for li in nnet.layers];
# entry 23 in MNIST datset
input_center = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,121,254,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,230,253,248,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,118,253,253,225,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,61,253,253,253,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,206,253,253,186,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,211,253,253,239,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,254,253,253,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,142,255,253,186,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,229,254,207,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,54,229,253,254,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,152,254,254,213,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,251,253,253,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29,212,253,250,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,214,253,253,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,253,253,253,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,253,253,189,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,253,253,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,235,253,126,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,99,248,253,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,225,235,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
output_center = [-1311.1257826380004,4633.767704436501,-654.0718535670002,-1325.349417307,1175.2361184373997,-1897.8607293569007,-470.3405972940001,830.8337987382,-377.7467076115001,572.3674015264198]
in_epsilon = 1 # 0-255
out_epsilon = 10 # logit domain
input_low = input_center .- in_epsilon
input_high = input_center .+ in_epsilon
output_low = output_center .- out_epsilon
output_high = output_center .+ out_epsilon
inputSet = Hyperrectangle(low=input_low, high=input_high)
outputSet = Hyperrectangle(low=output_low, high=output_high);
dim(inputSet)
[size(Wi) for Wi in weight_matrices]
sol = nnet_lazy(inputSet, weight_matrices, bias_vectors);
last(sol) ⊆ outputSet
@btime last($sol) ⊆ $outputSet
UnionSetArray(array(last(sol))) ⊆ outputSet
lazyOutput = AffineMap(weight_matrices[1], inputSet, bias_vectors[1])
lazyOutput ⊆ outputSet
clist_outputSet = constraints_list(outputSet)
for i in 1:20
println(ρ(clist_outputSet[i].a, lazyOutput) - clist_outputSet[i].b)
end
model = "mnist_large.nnet"
nnet = read_nnet(networks_folder * model);
length(nnet.layers)
weight_matrices = [li.weights for li in nnet.layers]
bias_vectors = [li.bias for li in nnet.layers];
# See https://github.com/sisl/NeuralVerification.jl/blob/master/test/runtests2.jl#L50
# entry 23 in MNIST datset
input_center = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,121,254,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,230,253,248,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,118,253,253,225,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,61,253,253,253,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,206,253,253,186,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,211,253,253,239,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,254,253,253,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,142,255,253,186,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,229,254,207,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,54,229,253,254,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,152,254,254,213,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,251,253,253,26,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,29,212,253,250,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,214,253,253,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,253,253,253,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,253,253,189,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,253,253,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,43,235,253,126,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,99,248,253,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,225,235,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
output_center = [131.8781755,134.8987015,141.6166255,158.34307,129.8803525,104.8286425,98.64196,133.6358395,131.1716215,105.10621]
in_epsilon = 1 # 0-255
out_epsilon = 10 #logit domain
input_low = input_center .- in_epsilon
input_high = input_center .+ in_epsilon
output_low = output_center .- out_epsilon
output_high = output_center .+ out_epsilon
inputSet = Hyperrectangle(low=input_low, high=input_high)
outputSet = Hyperrectangle(low=output_low, high=output_high);
[size(Wi) for Wi in weight_matrices]
dim(inputSet)
The idea is to check that the result of the network is included in the outputSet
.
sol = nnet_lazy(inputSet, weight_matrices, bias_vectors); # expensive
sol = nnet_lazy_template(inputSet, weight_matrices, bias_vectors, BoxDirections);