This directory contains a thin Python front-end to the gRPC server in [../syrenn_server].
network.py
defines theNetwork
class, which represents an entire network (effectively a list of sequentially-applied layers). It has helper methods for importing from standard file types as well as calling the server.layer.py
and*_layer.py
define layers that can be added to a network and serialized for sending to the SyReNN server. Forward-passes can be computed using PyTorch to allow gradient computation.transformer_client.py
handles communication with the gRPC server; it is not part of the public API (instead use the corresponding methods on theNetwork
class).tests/{file}.py
contains PyTest code for the methods in{file}.py
. They can be run withmake pytest_coverage
in the parent directory (see ../README.md).
Before attempting to use the Python front-end, please ensure that there is a server running on the machine (see ../README.md).
The primary interface is the Network
class. You can import a Network from
the .eran
file format used by ERAN (note
--- not all models currently supported), the .onnx
format (again, not all
models are compatible), or by manually constructing one using the layers
described in *_layer.py
.
The ERAN format is the preferred way to import models that use only convolutional, fully-connected, and PWL-nonlinearities.
from pysyrenn import Network
# Importing from ERAN format.
network = Network.from_file("model.eran", file_type="eran")
# "eran" file type is implied by the suffix ".eran":
network = Network.from_file("model.eran")
Models can also be imported from ONNX format:
# Importing from ONNX format.
network = Network.from_file("model.onnx", file_type="onnx")
# "onnx" file type is implied by the suffix ".onnx":
network = Network.from_file("model.onnx")
However, please note that the ONNX importer is currently poorly-tested and may
not support all layer types/features from ONNX. Please double-check that your
ONNX model was load correctly by investigating network.layers
and using
network.compute
to compare the loaded model against your expectations. If
you find any issues with ONNX imports, please contact us.
Finally, you can directly construct a Network
using the Python API. This is
the best option if your program has its own model representation. Please refer
to the *_layer.py
files in this directory for more information about
creating Layer
s, particularly as to the data format used.
from pysyrenn import FullyConnectedLayer, ReluLayer
weights = ... # Numpy array, shape: (in_dims, out_dims)
biases = ... # Numpy array, shape: (out_dims,)
fullyconnected = FullyConnectedLayer(weights, biases)
relu = ReluLayer()
network = Network([fullyconnected, relu])
Once you have a Network
instance, the following functions can be used to
interface with the SyReNN server:
network.exactlines
network.exactline
network.transform_planes
network.transform_plane
Note that the singular functions are simply wrappers around their plural counterparts (see below).
Please refer to ../../experiments for usage of PySyReNN, along with tests for the front-end test cases.
First, we create a network with just a ReLU layer, along with three lines in 2-dimensional space described by their starting & ending coordinates:
network = Network([ReluLayer()]) # See above.
input_lines = [
# Line passing through three quadrants.
([-0.5, 1], [1, -0.5]),
# Line passing through two quadrants.
([-1, 3], [2, 0]),
# Line only in the first quadrant.
([0.5, 0.5], [0.25, 0.75]),
]
When compute_preimages=False
and include_post=False
, ExactLines will
return the ratios of the partition endpoints for each line:
exactlines = network.exactlines(input_lines,
compute_preimages=False,
include_post=False)
# Each line in input_lines is transformed and returned.
assert len(exactlines) == len(input_lines)
# The first line passes through 3 quadrants, so has 4 endpoints.
assert len(exactlines[0]) == 4
# They happen to be equally distributed for this line.
assert np.allclose(exactlines[0], [0.0, 1.0/3.0, 2.0/3.0, 1.0])
# The next passes through 2 quadrants for 3 endpoints.
assert len(exactlines[1]) == 3
assert np.allclose(exactlines[1], [0.0, 1.0/3.0, 1.0])
# The last is only in the first quadrant, so we have just the original two
# endpoints.
assert len(exactlines[2]) == 2
assert np.allclose(exactlines[2], [0.0, 1.0])
compute_preimages=True
returns the actual (preimage) points along each
line, instead of their ratios:
exactlines = network.exactlines(input_lines,
compute_preimages=True,
include_post=False)
assert len(exactlines) == 3
assert np.allclose(exactlines[0], [exactlines[0][0],
[0, 0.5],
[0.5, 0],
exactlines[0][1]])
... # Similarly for exactlines[1] and exactlines[2]
include_post=True
transforms each line into a tuple (pre, post) where pre
is equivalent to what you would get from calling exactlines
with
include_post=False
(i.e. the endpoints) and post is the output of the
network on each endpoint.
exactlines = network.exactlines(input_lines,
compute_preimages=False,
include_post=True)
assert len(exactlines) == 3
assert isinstance(exactlines[0], tuple) and len(exactlines[0]) == 2
pre, post = exactlines[0]
assert np.allclose(pre, [0.0, 1.0/3.0, 2.0/3.0, 1.0])
assert np.allclose(post, [[0.0, 1.0], [0.0, 0.5], [0.5, 0.0], [1.0, 0.0]])
... # Similarly for exactlines[1] and exactlines[2]
In all cases, the singular exactline
method does what you would expect,
except it takes start
and end
parameters instead of a tuple:
exactlines = network.exactlines(input_lines, ...)
start, end = input_lines[0]
exactline = network.exactline(start, end, ...)
assert np.allclose(exactline, exactlines[0])
Transforming planes is similar to ExactLine except planes are represented as Numpy arrays of vertices, not tuples of (start, end)-points.