This contains C++ code that computes SyReNN for 1- and 2-dimensional input restriction domains of interest. At a high-level, we have a few main types of files:
server.cc
implements the GRPC server that handles receiving queries from the clients and dispatching calls to the rest of the C++ methods. Methods of particular interest include:main
, which registers all of the layers supported by the server and sets the address to use.transform_polytope
, which computes the symbolic representation for a particular network (series of layers) and input UPolytope (see below).transform_lines
does the same for a set ofSegmentedLine
instances. It usessplit_line
to dynamically sub-divide the line and avoid running out of memory.
{layer}_transformer.{h, cc}
contain algorithms to compute SyReNN for certain layer types. Currently, only 1- and 2-dimensional input regions are supported for the non-linear transformers.pwl_transformer.{h, cc}
contains algorithms for computing SyReNN for any piecewise-linear function with convex-polytope partitions. It is sub-classed by, eg.,relu_transformer.{h, cc}
andargmax_transformer.{h, cc}
.affine_transformer.{h, cc}
contains algorithms for computing SyReNN for any affine function. It is sub-classed by, eg.,conv2d_transformer.{h, cc}
andfullyconnected_transformer.{h, cc}
.
strided_window_data.{h, cc}
contains a helper class for all layers that work with striding windows (eg. 2D Convolution and MaxPool).segmented_line.{h, cc}
andupolytope.{h, cc}
are the underlying datatypes used to represent SyReNN's input and output for 1- and arbitrarily-many dimensions (respectively). See below for more details.shared.h
contains a few helpful typedefs; in particular, it helps to know thatRMMatrixXf
stands for "Row-Major Eigen Matrix in arbitrary (X) dimensions using single-precision floats."
We use two core data structures, described below.
The SegmentedLine
class represents, unsurprisingly, a segmented line ---
i.e., a union of connected line segments in potentially-high-dimensional space.
We represent the line as a vector of endpoints.
Each endpoint has two main representations, a preimage ratio and a post-value (usually referred to just as "endpoint" in the code). Using the terminology of [1], the preimage ratios correspond to the actual ExactLine partitioning, while the post-values are the result of applying the network to the point described by the corresponding preimage ratio. Note that each preimage ratio is between 0.0 and 1.0, with 0.0 being the start point of the line before applying any layers and 1.0 being the endpoint before applying any layers.
Furthermore, there is an associated class SegmentedLineStub
which is like a
SegmentedLine
except it only has preimage ratios. This is used by the
server to dynamically sub-divide lines, and is very effective in controlling
the memory usage on large networks. The basic idea is that a very large
SegmentedLine
can be broken up into a smaller SegmentedLine
(say, maybe
only the first 10 endpoints) with the rest of the endpoints being stored in a
memory-friendly SegmentedLineStub
until the first 10 endpoints have been
fully transformed.
To actually perform transformations (compute ExactLine), however, the
SegmentedLineStub
needs to be turned back into a SegmentedLine
, which
necessitates re-computing the post-values. To help control this computational
cost, we keep track of the layer at which each endpoint was introduced,
allowing us to wait until that layer is encountered in the re-computing process
before adding the endpoint back. This is kept track of primarily by the member
interpolate_before_layer_
.
Finally, we note that transforming a line with a layer is actually a two step process: first, endpoints are added to the line (just their preimage ratios, not their post-values); then, the corresponding postimage-values are computed. We separate these two steps, as the postimage-values are often extremely memory-intensive, so we need to decide how to sub-divide the line (to save memory, see above) before they are actually computed (but after we know how many there are). This also has the slight benefit that we do not have to actually compute the very last layer's post-values if the client only wants the partitioning in the input space.
The UPolytope
class represents a union of convex polytopes. Note that
this is a generalization of the SegmentedLine
concept to higher-dimensional
regions (the existence of many important line-specific optimizations motivates
our keeping two separate classes).
Each convex polytope is represented by its vertices, i.e. in a V-representation
(which is efficient and precise for low-dimensional regions). Notably, when the
layers used are continuous, many of the polytopes tend to share vertices.
UPolytope
handles this by keeping a "master-list" of all vertices used by
polytopes in the union, then each polytope keeps a vector of indices into that
master list to specify what its own vertices are.
Each endpoint again has two representations, the preimage combination and
post-value. These have the same interpretation as in a SegmentedLine
, with
the slight difference that the preimage combination has one entry for each
vertex in the pre-transformation UPolytope
instead of just one ratio (this
is a natural extension of a ratio-along-a-line for higher-dimensional regions).
Notably, we have not made as many optimizations for memory usage with
UPolytope
as we did with SegmentedLine
. This is due primarily to the
use cases we have explored for each; image networks with extremely large
activation vectors for SegmentedLine
vs. relatively smaller controller
networks for UPolytope
.
Instead, we have optimized for parallelization: we use TBB concurrency-safe
containers, and have a two-phased vertex-insertion process that minimizes the
number of expensive Eigen resizes we need to do. This is primarily documented
in upolytope.h
and example uses are in pwl_transformer.cc
.