relucent 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relucent-0.1.0/PKG-INFO +48 -0
- relucent-0.1.0/README.md +25 -0
- relucent-0.1.0/pyproject.toml +30 -0
- relucent-0.1.0/src/relucent/__init__.py +7 -0
- relucent-0.1.0/src/relucent/bvs.py +221 -0
- relucent-0.1.0/src/relucent/complex.py +777 -0
- relucent-0.1.0/src/relucent/convert_model.py +167 -0
- relucent-0.1.0/src/relucent/model.py +97 -0
- relucent-0.1.0/src/relucent/poly.py +684 -0
- relucent-0.1.0/src/relucent/utils.py +231 -0
relucent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: relucent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Author: Blake B. Gaines
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
|
+
Requires-Dist: torch
|
|
7
|
+
Requires-Dist: torchvision
|
|
8
|
+
Requires-Dist: pandas
|
|
9
|
+
Requires-Dist: gurobipy
|
|
10
|
+
Requires-Dist: networkx
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: scipy
|
|
13
|
+
Requires-Dist: tqdm
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Requires-Dist: plotly
|
|
16
|
+
Requires-Dist: pillow
|
|
17
|
+
Requires-Dist: pyvis ; extra == 'cli'
|
|
18
|
+
Requires-Dist: kaleido ; extra == 'cli'
|
|
19
|
+
Requires-Python: >=3.13, <3.14
|
|
20
|
+
Project-URL: Homepage, https://github.com/bl-ake/relucent
|
|
21
|
+
Provides-Extra: cli
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
[](https://github.com/bl-ake/relucent/actions/workflows/python-package.yml)
|
|
25
|
+
|
|
26
|
+
# Relucent
|
|
27
|
+
Explore polyhedral complexes associated with ReLU networks
|
|
28
|
+
|
|
29
|
+
## Environment Setup
|
|
30
|
+
1. Install Python 3.13
|
|
31
|
+
2. Install [PyTorch 2.3.0](https://pytorch.org/get-started/previous-versions/#:~:text=org/whl/cpu-,v2.3.0)
|
|
32
|
+
3. Install the remaining dependencies with `pip install -r requirements.txt`
|
|
33
|
+
|
|
34
|
+
## Code Structure
|
|
35
|
+
* [model.py](src/relucent/model.py): PyTorch Module that acts as an interface between the model and the rest of the code
|
|
36
|
+
* [poly.py](src/relucent/poly.py): Class for calculations involving individual polyhedrons (e.g. computing boundaries, neighbors, volume)
|
|
37
|
+
* [complex.py](src/relucent/complex.py): Class for calculations involving the polyhedral cplx (e.g. polyhedron search, connectivity graph calculation)
|
|
38
|
+
* [convert_model.py](src/relucent/convert_model.py): Utilities for converting various PyTorch.nn layers to Linear layers
|
|
39
|
+
* [bvs.py](src/relucent/bvs.py): Data structures for storing large numbers of sign vectors
|
|
40
|
+
|
|
41
|
+
## Obtaining a Gurobi License
|
|
42
|
+
|
|
43
|
+
**The following steps are not necessary when replicating the experiments from the paper.**
|
|
44
|
+
|
|
45
|
+
Without a [license](https://support.gurobi.com/hc/en-us/articles/12872879801105-How-do-I-retrieve-and-set-up-a-Gurobi-license), Gurobi will only work with a limited feature set. This includes a limit on the number of decision variables in the models it can solve, which limits the size of the networks this code is able to analyze. There are multiple ways to install the software, but we recommend the following steps to those eligible for an academic license:
|
|
46
|
+
1. Install the [Gurobi Python library](https://pypi.org/project/gurobipy/), for example using `pip install gurobipy`
|
|
47
|
+
2. [Obtain a Gurobi license](https://support.gurobi.com/hc/en-us/articles/360040541251-How-do-I-obtain-a-free-academic-license) (Note: a WLS license will limit the number of concurrent sessions across multiple devices, which can result in slowdowns when using this library on different machines simultaneously.)
|
|
48
|
+
3. In your Conda environment, run `grbgetkey` followed by your license key
|
relucent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[](https://github.com/bl-ake/relucent/actions/workflows/python-package.yml)
|
|
2
|
+
|
|
3
|
+
# Relucent
|
|
4
|
+
Explore polyhedral complexes associated with ReLU networks
|
|
5
|
+
|
|
6
|
+
## Environment Setup
|
|
7
|
+
1. Install Python 3.13
|
|
8
|
+
2. Install [PyTorch 2.3.0](https://pytorch.org/get-started/previous-versions/#:~:text=org/whl/cpu-,v2.3.0)
|
|
9
|
+
3. Install the remaining dependencies with `pip install -r requirements.txt`
|
|
10
|
+
|
|
11
|
+
## Code Structure
|
|
12
|
+
* [model.py](src/relucent/model.py): PyTorch Module that acts as an interface between the model and the rest of the code
|
|
13
|
+
* [poly.py](src/relucent/poly.py): Class for calculations involving individual polyhedrons (e.g. computing boundaries, neighbors, volume)
|
|
14
|
+
* [complex.py](src/relucent/complex.py): Class for calculations involving the polyhedral cplx (e.g. polyhedron search, connectivity graph calculation)
|
|
15
|
+
* [convert_model.py](src/relucent/convert_model.py): Utilities for converting various PyTorch.nn layers to Linear layers
|
|
16
|
+
* [bvs.py](src/relucent/bvs.py): Data structures for storing large numbers of sign vectors
|
|
17
|
+
|
|
18
|
+
## Obtaining a Gurobi License
|
|
19
|
+
|
|
20
|
+
**The following steps are not necessary when replicating the experiments from the paper.**
|
|
21
|
+
|
|
22
|
+
Without a [license](https://support.gurobi.com/hc/en-us/articles/12872879801105-How-do-I-retrieve-and-set-up-a-Gurobi-license), Gurobi will only work with a limited feature set. This includes a limit on the number of decision variables in the models it can solve, which limits the size of the networks this code is able to analyze. There are multiple ways to install the software, but we recommend the following steps to those eligible for an academic license:
|
|
23
|
+
1. Install the [Gurobi Python library](https://pypi.org/project/gurobipy/), for example using `pip install gurobipy`
|
|
24
|
+
2. [Obtain a Gurobi license](https://support.gurobi.com/hc/en-us/articles/360040541251-How-do-I-obtain-a-free-academic-license) (Note: a WLS license will limit the number of concurrent sessions across multiple devices, which can result in slowdowns when using this library on different machines simultaneously.)
|
|
25
|
+
3. In your Conda environment, run `grbgetkey` followed by your license key
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build >= 0.9.5, <0.10.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "relucent"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">= 3.13, <3.14"
|
|
9
|
+
dependencies = [ ## TODO: Constrain Versions
|
|
10
|
+
"torch",
|
|
11
|
+
"torchvision",
|
|
12
|
+
"pandas",
|
|
13
|
+
"gurobipy",
|
|
14
|
+
"networkx",
|
|
15
|
+
"numpy",
|
|
16
|
+
"scipy",
|
|
17
|
+
"tqdm",
|
|
18
|
+
"matplotlib",
|
|
19
|
+
"plotly",
|
|
20
|
+
"Pillow",
|
|
21
|
+
]
|
|
22
|
+
authors = [{ name = "Blake B. Gaines" }]
|
|
23
|
+
license = "AGPL-3.0-or-later"
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
cli = ["pyvis", "kaleido"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/bl-ake/relucent"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .bvs import BVManager
|
|
2
|
+
from .complex import Complex
|
|
3
|
+
from .model import NN, get_mlp_model
|
|
4
|
+
from .convert_model import convert
|
|
5
|
+
from .utils import get_env, split_sequential, data_graph, set_seeds
|
|
6
|
+
|
|
7
|
+
__all__ = [Complex, NN, get_mlp_model, BVManager, convert, get_env, split_sequential, data_graph, set_seeds]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from heapq import heappop, heappush
|
|
2
|
+
|
|
3
|
+
from torch import Tensor
|
|
4
|
+
|
|
5
|
+
from .poly import encode_bv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BVManager:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.index2bv = list()
|
|
11
|
+
self.tag2index = dict() ## Tags are just hashable versions of bvs, should be unique
|
|
12
|
+
self._len = 0
|
|
13
|
+
|
|
14
|
+
def _get_tag(self, bv):
|
|
15
|
+
if isinstance(bv, Tensor):
|
|
16
|
+
bv = bv.detach().cpu().numpy()
|
|
17
|
+
return encode_bv(bv)
|
|
18
|
+
|
|
19
|
+
def add(self, bv):
|
|
20
|
+
tag = self._get_tag(bv)
|
|
21
|
+
if tag not in self.tag2index:
|
|
22
|
+
self.tag2index[tag] = len(self.index2bv)
|
|
23
|
+
self.index2bv.append(bv)
|
|
24
|
+
self._len += 1
|
|
25
|
+
|
|
26
|
+
def __getitem__(self, bv):
|
|
27
|
+
tag = self._get_tag(bv)
|
|
28
|
+
index = self.tag2index[tag]
|
|
29
|
+
if self.index2bv[index] is None:
|
|
30
|
+
raise KeyError
|
|
31
|
+
return index
|
|
32
|
+
|
|
33
|
+
def __contains__(self, bv):
|
|
34
|
+
tag = self._get_tag(bv)
|
|
35
|
+
if tag not in self.tag2index:
|
|
36
|
+
return False
|
|
37
|
+
return self.index2bv[self.tag2index[tag]] is not None
|
|
38
|
+
|
|
39
|
+
def __delitem__(self, bv):
|
|
40
|
+
tag = self._get_tag(bv)
|
|
41
|
+
index = self.tag2index[tag]
|
|
42
|
+
self.index2bv[index] = None
|
|
43
|
+
self._len -= 1
|
|
44
|
+
|
|
45
|
+
def __iter__(self):
|
|
46
|
+
return iter((bv for bv in self.index2bv if bv is not None))
|
|
47
|
+
|
|
48
|
+
def __len__(self):
|
|
49
|
+
return self._len
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# TODO: Move to utils as general priority queue
|
|
53
|
+
class BVPriorityQueue:
|
|
54
|
+
## From https://docs.python.org/3/library/heapq.html
|
|
55
|
+
REMOVED = "<removed-task>" # placeholder for a removed task
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.pq = [] # list of entries arranged in a heap
|
|
59
|
+
self.entry_finder = {} # mapping of tasks to entries
|
|
60
|
+
self.counter = 0 # unique sequence count
|
|
61
|
+
|
|
62
|
+
def push(self, task, priority=0):
|
|
63
|
+
"Add a new task or update the priority of an existing task"
|
|
64
|
+
bv, *task = task
|
|
65
|
+
task = tuple(task)
|
|
66
|
+
if task in self.entry_finder:
|
|
67
|
+
self.remove_task(task)
|
|
68
|
+
entry = [priority, self.counter, bv, task]
|
|
69
|
+
self.entry_finder[task] = entry
|
|
70
|
+
heappush(self.pq, entry)
|
|
71
|
+
self.counter += 1
|
|
72
|
+
|
|
73
|
+
def remove_task(self, task):
|
|
74
|
+
"Mark an existing task as REMOVED. Raise KeyError if not found."
|
|
75
|
+
entry = self.entry_finder.pop(task)
|
|
76
|
+
entry[-1] = self.REMOVED
|
|
77
|
+
|
|
78
|
+
def pop(self):
|
|
79
|
+
"Remove and return the lowest priority task. Raise KeyError if empty."
|
|
80
|
+
while self.pq:
|
|
81
|
+
_, _, bv, task = heappop(self.pq)
|
|
82
|
+
if task is not self.REMOVED:
|
|
83
|
+
del self.entry_finder[task]
|
|
84
|
+
return bv, *task
|
|
85
|
+
raise KeyError("pop from an empty priority queue")
|
|
86
|
+
|
|
87
|
+
def __len__(self):
|
|
88
|
+
return len(self.entry_finder)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# class BVPriorityQueue:
|
|
92
|
+
# def __init__(self):
|
|
93
|
+
# self.pq = [] # list of entries arranged in a heap
|
|
94
|
+
|
|
95
|
+
# def push(self, task, priority=0):
|
|
96
|
+
# "Add a new task or update the priority of an existing task"
|
|
97
|
+
# # bv, *task = task
|
|
98
|
+
# self.pq.append((priority, task))
|
|
99
|
+
# self.pq.sort(reverse=True, key=lambda x: x[0])
|
|
100
|
+
|
|
101
|
+
# def remove_task(self, task):
|
|
102
|
+
# "Mark an existing task as REMOVED. Raise KeyError if not found."
|
|
103
|
+
# self.pq = [(p, t) for p, t in self.pq if t != task]
|
|
104
|
+
|
|
105
|
+
# def pop(self):
|
|
106
|
+
# "Remove and return the lowest priority task. Raise KeyError if empty."
|
|
107
|
+
# return self.pq.pop(-1)[1]
|
|
108
|
+
|
|
109
|
+
# def __len__(self):
|
|
110
|
+
# return len(self.pq)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# class BVNode:
|
|
114
|
+
# def __init__(self, key):
|
|
115
|
+
# self.key = key ## Key of bv being set
|
|
116
|
+
# self.left = None ## for all nodes in subtree, bv[0, key] = -1
|
|
117
|
+
# self.middle = None ## ...bv[0, key] = 0
|
|
118
|
+
# self.right = None ## ...bv[0, key] = 1
|
|
119
|
+
|
|
120
|
+
# def get_child(self, bv):
|
|
121
|
+
# # return either BVNode or int
|
|
122
|
+
# if bv[0, self.key] == -1:
|
|
123
|
+
# next_node = self.left
|
|
124
|
+
# elif bv[0, self.key] == 0:
|
|
125
|
+
# next_node = self.middle
|
|
126
|
+
# elif bv[0, self.key] == 1:
|
|
127
|
+
# next_node = self.right
|
|
128
|
+
# return next_node
|
|
129
|
+
|
|
130
|
+
# def set_child(self, bv, node):
|
|
131
|
+
# if bv[0, self.key] == -1:
|
|
132
|
+
# self.left = node
|
|
133
|
+
# elif bv[0, self.key] == 0:
|
|
134
|
+
# self.middle = node
|
|
135
|
+
# elif bv[0, self.key] == 1:
|
|
136
|
+
# self.right = node
|
|
137
|
+
|
|
138
|
+
# def print(self, level=0):
|
|
139
|
+
# print(" " * level + str(self.key) + ":")
|
|
140
|
+
# for name, k in zip(("L", "M", "R"), (self.left, self.middle, self.right)):
|
|
141
|
+
# if isinstance(k, BVNode):
|
|
142
|
+
# print(" " * (level + 2) + name)
|
|
143
|
+
# k.print(level=level + 4)
|
|
144
|
+
# elif isinstance(k, int):
|
|
145
|
+
# print(" " * (level + 2) + name + ": leaf " + str(k))
|
|
146
|
+
|
|
147
|
+
# # Trie
|
|
148
|
+
# # Each edge in the tree sets a dimension to a value
|
|
149
|
+
# # Leaf nodes are just indices of bvs in index2bv
|
|
150
|
+
# # TODO: Replace hashmaps in Complex class with this
|
|
151
|
+
# class BVManager:
|
|
152
|
+
# def __init__(self):
|
|
153
|
+
# self.root = BVNode(0)
|
|
154
|
+
# self.index2bv = list()
|
|
155
|
+
|
|
156
|
+
# def add(self, bv):
|
|
157
|
+
# assert bv.ndim == 2
|
|
158
|
+
# node = self.root
|
|
159
|
+
# child = self.root.get_child(bv)
|
|
160
|
+
# while isinstance(child, BVNode):
|
|
161
|
+
# node = child
|
|
162
|
+
# child = node.get_child(bv)
|
|
163
|
+
# if child is None:
|
|
164
|
+
# node.set_child(bv, len(self.index2bv))
|
|
165
|
+
# self.index2bv.append(bv)
|
|
166
|
+
# elif isinstance(child, int): ## TODO: This check should be redundant
|
|
167
|
+
# child_bv = self.index2bv[child]
|
|
168
|
+
# if not isinstance(child_bv, type(bv)):
|
|
169
|
+
# if isinstance(bv, Tensor):
|
|
170
|
+
# bv = bv.detach().cpu().numpy()
|
|
171
|
+
# else:
|
|
172
|
+
# child_bv = child_bv.detach().cpu().numpy()
|
|
173
|
+
# if not (child_bv == bv).all():
|
|
174
|
+
# if child_bv[0, node.key] == bv[0, node.key]: ## TODO: This check should be redundant
|
|
175
|
+
# for i in range(bv.shape[1]):
|
|
176
|
+
# if child_bv[0, i] != bv[0, i]:
|
|
177
|
+
# break
|
|
178
|
+
|
|
179
|
+
# ## Replace the existing child of the node with a new node
|
|
180
|
+
# new_bvnode = BVNode(i)
|
|
181
|
+
# node.set_child(bv, new_bvnode)
|
|
182
|
+
|
|
183
|
+
# ## Set the new node's children
|
|
184
|
+
# new_bvnode.set_child(child_bv, child)
|
|
185
|
+
# new_bvnode.set_child(bv, len(self.index2bv))
|
|
186
|
+
# self.index2bv.append(bv)
|
|
187
|
+
# else:
|
|
188
|
+
# raise ValueError("Something went wrong")
|
|
189
|
+
|
|
190
|
+
# def __getitem__(self, bv):
|
|
191
|
+
# assert bv.ndim == 2
|
|
192
|
+
# node = self.root
|
|
193
|
+
# while isinstance(node, BVNode):
|
|
194
|
+
# node = node.get_child(bv)
|
|
195
|
+
# if isinstance(node, int):
|
|
196
|
+
# found_bv = self.index2bv[node]
|
|
197
|
+
# if not isinstance(bv, type(found_bv)):
|
|
198
|
+
# if isinstance(bv, Tensor):
|
|
199
|
+
# bv = bv.detach().cpu().numpy()
|
|
200
|
+
# else:
|
|
201
|
+
# found_bv = found_bv.detach().cpu().numpy()
|
|
202
|
+
# if (found_bv == bv).all():
|
|
203
|
+
# return node
|
|
204
|
+
# raise KeyError
|
|
205
|
+
|
|
206
|
+
# def __contains__(self, bv):
|
|
207
|
+
# try:
|
|
208
|
+
# self[bv]
|
|
209
|
+
# return True
|
|
210
|
+
# except KeyError:
|
|
211
|
+
# return False
|
|
212
|
+
|
|
213
|
+
# def __iter__(self):
|
|
214
|
+
# return iter(self.index2bv)
|
|
215
|
+
|
|
216
|
+
# def __len__(self):
|
|
217
|
+
# return len(self.index2bv)
|
|
218
|
+
|
|
219
|
+
# def print(self):
|
|
220
|
+
# print("Number of BVs:", len(self))
|
|
221
|
+
# self.root.print()
|