{"id":18024545,"url":"https://github.com/zinoex/bound_propagation","last_synced_at":"2025-06-15T23:05:51.662Z","repository":{"id":57092617,"uuid":"450864229","full_name":"Zinoex/bound_propagation","owner":"Zinoex","description":"Linear and interval bound propagation in Pytorch with easy-to-use API and GPU support.","archived":false,"fork":false,"pushed_at":"2024-02-26T12:10:44.000Z","size":175,"stargazers_count":9,"open_issues_count":2,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-22T18:11:35.230Z","etag":null,"topics":["adversarial-learning","bound-propagation","pytorch","robustness","verification"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Zinoex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-22T15:58:36.000Z","updated_at":"2025-03-06T13:24:14.000Z","dependencies_parsed_at":"2024-10-30T07:48:05.755Z","dependency_job_id":null,"html_url":"https://github.com/Zinoex/bound_propagation","commit_stats":{"total_commits":156,"total_committers":2,"mean_commits":78.0,"dds":0.4871794871794872,"last_synced_commit":"a6d99e886b31c2cda9b2494982069fd5174e579f"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zinoex%2Fbound_propagation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zinoex%2Fbound_propagation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zinoex%2Fbound_propagation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zinoex%2Fbound_propagation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Zinoex","download_url":"https://codeload.github.com/Zinoex/bound_propagation/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245760655,"owners_count":20667886,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adversarial-learning","bound-propagation","pytorch","robustness","verification"],"created_at":"2024-10-30T07:13:13.145Z","updated_at":"2025-03-27T00:31:06.164Z","avatar_url":"https://github.com/Zinoex.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bound propagation\nLinear and interval bound propagation in Pytorch with easy-to-use API and GPU support.\nInitially made as an alternative to [the original CROWN implementation](https://github.com/IBM/CROWN-Robustness-Certification) which featured only Numpy, lots of for-loops, and a cumbersome API.\nThis library is comparable to [auto_LiRPA](https://github.com/KaidiXu/auto_LiRPA) but differs in design philosophy - auto_LiRPA parses the ONNX computation graph and operates on generic computation nodes.\nThe downside of this approach is a giant violation of the Single Resposibility Principle and the Open/Closed Principle of the [SOLID](https://en.wikipedia.org/wiki/SOLID) design principles. \nFor this library, we instead assume that the network is structured in a tree of nn.Modules, which we map to BoundModules using a visitor and abstract factory pattern exploiting Python's type system.\nThis allows better separation of concerns and extensibility (see [New Modules](#new-modules) below), and improved mental map of the library.\n\nTo install:\n```\npip install bound-propagation\n```\n\nSupported bound propagation methods:\n- Interval Bound Propagation (IBP)\n- [CROWN](https://arxiv.org/abs/1811.00866)\n- [CROWN-IBP](https://arxiv.org/abs/1906.06316)\n\nFor the examples below assume the following network definition:\n```python\nfrom torch import nn\nfrom bound_propagation import BoundModelFactory, HyperRectangle\n\nclass Network(nn.Sequential):\n    def __init__(self):\n        in_size = 30\n        classes = 10\n\n        super().__init__(\n            nn.Linear(in_size, 16),\n            nn.Tanh(),\n            nn.Linear(16, 16),\n            nn.Tanh(),\n            nn.Linear(16, classes)\n        )\n\nnet = Network()\n\nfactory = BoundModelFactory()\nnet = factory.build(net)\n```\n\nThe method also works with ```nn.Sigmoid```, ```nn.ReLU```, ```nn.Identity```, and custom non-linear functions: ```Exp```, ```Log```, ```Reciprocal```, ```Sin```, ```Cos```, ```Pow```, ```UnivariateMonomial```, ```MultivariateMonomial```, ```Clamp```.\nThe following bivariate functions are supported: ```Add```, ```Sub```, ```Div```, and ```Mul```, and their vectorized equivalents.\nIn addition, we have the following convenience functions: ```Residual```, ```Cat```, ```Parallel```, ```Select```, ```Flip```, ```FixedLinear```, and ```ElementwiseLinear```.\nInitial efforts wrt. probability functions such as normal distribution PDF and CDF, and the error function has begun (consult [probability.py](https://github.com/Zinoex/bound_propagation/blob/main/src/bound_propagation/probability.py) for more info).\n\n\n## Interval bounds\nTo get interval bounds for either IBP, CROWN, or CROWN-IBP:\n\n```python\nx = torch.rand(100, 30)\nepsilon = 0.1\ninput_bounds = HyperRectangle.from_eps(x, epsilon)\n\nibp_bounds = net.ibp(input_bounds)\n\ncrown_bounds = net.crown(input_bounds).concretize()\ncrown_ibp_bounds = net.crown_ibp(input_bounds).concretize()\n\nalpha_crown_bounds = net.crown(input_bounds, alpha=True).concretize()\nalpha_crown_ibp_bounds = net.crown_ibp(input_bounds, alpha=True).concretize()\n```\n\nThe parameter `alpha=True` enables alpha-CROWN, which means bounds are optimized using projected gradient descent at the cost of more computation.\n\n## Linear bounds\nTo get linear bounds for either CROWN or CROWN-IBP:\n\n```python\nx = torch.rand(100, 30)\nepsilon = 0.1\ninput_bounds = HyperRectangle.from_eps(x, epsilon)\n\ncrown_bounds = net.crown(input_bounds)\ncrown_ibp_bounds = net.crown_ibp(input_bounds)\n\nalpha_crown_bounds = net.crown(input_bounds, alpha=True)\nalpha_crown_ibp_bounds = net.crown_ibp(input_bounds, alpha=True)\n```\nIf lower or upper bounds are not needed, then you can add `bound_lower=False` or `bound_lower=True` to avoid the unnecessary computation.\nThis also works for interval bounds with CROWN and CROWN-IBP.\nIBP accepts the two parameters for compatibility but ignores them since both bounds are necessary for the forward propagation. \nParameter `alpha=True` is explained under [Interval bounds](#interval-bounds).\n\n## New Modules\nTo show how to design a new module, we use a residual module as a running example (see [residual.py](https://github.com/Zinoex/bound_propagation/blob/main/examples/residual.py) for the full code).\nAdding a new module revolves around the assumption that each module as one input and one output, but a module can have submodules such as the case for residual.\n```python\nclass Residual(nn.Module):\n    def __init__(self, subnetwork):\n        super().__init__()\n        self.subnetwork = subnetwork\n\n    def forward(self, x):\n        return self.subnetwork(x) + x\n```\n\nNext, we build the corresponding BoundModule. The first parameter of the BoundModule is the nn.Module, in this case Residual, and the second is a BoundModelFactory.\nWhy factory? Because we do not know the structure of the subnetwork, and this could contain new network types defined by users of this library.\nSo the factory allows us to defer defining how to construct the BoundModule for the subnetwork, and the construction happens by calling `factory.build(subnetwork)` (possibly multiple times if there are multiple subnetworks).\n```python\nclass BoundResidual(BoundModule):\n    def __init__(self, model, factory, **kwargs):\n        super().__init__(model, factory, **kwargs)\n        self.subnetwork = factory.build(model.subnetwork)\n\n    def propagate_size(self, in_size):\n        out_size = self.subnetwork.propagate_size(in_size)\n        assert in_size == out_size\n\n        return out_size\n```\nA small note here is the need to implement `propagate_size` - some modules may need to know the input and output size without this being available on the nn.Module.\nHence we propagate the size, and even though we know the output size from the input size for `BoundResidual`, we still propagate through the subnetwork to store this info throughout the bound graph.\n\nNext, we consider linear relaxation for linear bounds - if you only use IBP, you can skip these. BoundModules must implement `need_relaxation` as a property and `clear_relaxation` as a method, and if the module requires relaxation (i.e. not linear and no submodules) it must implement `backward_relaxation` too.\nNote here that we let the subnetwork handle the `need_relaxation` since `Residual` itself does not need relaxation but the subnetwork might contain some activation/non-linear function. Similarly, we refer to subnetwork for `clear_relaxation`.\nThe `backward_relaxation` method is for adding relaxations to the bound graph using CROWN where the top-level function continually calls `backward_relaxation` until no more relaxation is needed as determined by `need_relaxation`.\nIn this case the method may be called more than once since the subnetwork may have multiple layers needing relaxations, e.g. nn.Sequential with multiple layers of activation functions.\nIf a module needs relaxation, it starts the backward relaxation chain with identity linear bounds (and zero bias accumulation) and self as the return tuple.\nThe return signature of `backward_relaxation` is `linear_bounds, relaxation_module` since the top-level method needs a way of referring to the module being relaxed.\n```python\n    @property\n    def need_relaxation(self):\n        return self.subnetwork.need_relaxation\n\n    def clear_relaxation(self):\n        self.subnetwork.clear_relaxation()\n\n    def backward_relaxation(self, region):\n        assert self.subnetwork.need_relaxation\n        return self.subnetwork.backward_relaxation(region)\n```\n\nFinally, we get to the IBP forward and CROWN backward propagations themselves.\nIBP forward is straightforward using interval arithmetic with the exception that you \u003cspan style=\"color:red\"\u003emust call `bounds.region`\u003c/span\u003e as the first parameter on `IntervalBounds` (and `LinearBounds` too).\nIf not the memory usage will be excessive. \nIBP forward must also propagate `save_relaxation` and if a relaxation is needed for a module the relaxation must be constructed when IBP forward is called - this is to support CROWN-IBP.\nSimilarly, IBP forward must also propagate `save_input_bounds` and save input if ```True```, which is to improve.\n`crown_backward` has two components: updating linear bounds backwards and potentially adding to the bias accumulator.\nThe structure depends largely on how the nn.Module is, but addition can be done as shown below for `Residual`.\nI recommend that you look at [activation.py](https://github.com/Zinoex/bound_propagation/blob/main/src/bound_propagation/activation.py), [linear.py](https://github.com/Zinoex/bound_propagation/blob/main/src/bound_propagation/linear.py), and [cat.py](https://github.com/Zinoex/bound_propagation/blob/main/src/bound_propagation/cat.py) to see how design `crown_backward`.\nNote that lower and upper will never interact, and that both can be `None` because we can avoid unnecessary computations if either is not needed.\n```python\n    def ibp_forward(self, bounds, save_relaxation=False, save_input_bounds=False):\n        residual_bounds = self.subnetwork.ibp_forward(bounds, save_relaxation=save_relaxation, save_input_bounds=save_input_bounds)\n\n        return IntervalBounds(bounds.region, bounds.lower + residual_bounds.lower, bounds.upper + residual_bounds.upper)\n    \n    def crown_backward(self, linear_bounds, optimize):\n        residual_linear_bounds = self.subnetwork.crown_backward(linear_bounds, optimize)\n\n        if linear_bounds.lower is None:\n            lower = None\n        else:\n            lower = (linear_bounds.lower[0] + residual_linear_bounds.lower[0], residual_linear_bounds.lower[1])\n\n        if linear_bounds.upper is None:\n            upper = None\n        else:\n            upper = (linear_bounds.upper[0] + residual_linear_bounds.upper[0], residual_linear_bounds.upper[1])\n\n        return LinearBounds(linear_bounds.region, lower, upper)\n```\n\nIf you use alpha-CROWN and the module contains submodules (or optimizable parameters itself), then the bound module must implement method for collecting bound parameters, projecting gradients, and clipping bound parameters, to support PGD.\n```python\n    def bound_parameters(self):\n        for module in self.bound_sequential:\n            yield from module.bound_parameters()\n\n    def clip_params(self):\n        for module in self.bound_sequential:\n            module.clip_params()\n\n    def project_grads(self):\n        for module in self.bound_sequential:\n            module.project_grads()\n```\n\nThe last task is to make a factory and register your new module (alternatively subclass the factory and add to the dictionary of nn.Modules and BoundModules).\n```python\nfactory = BoundModelFactory()\nfactory.register(Residual, BoundResidual)\n```\n\n## Authors\n- [Frederik Baymler Mathiesen](https://www.baymler.com) - PhD student @ TU Delft\n\n## Citing\n```\n@misc{Mathiesen2022,\n  author = {Frederik Baymler Mathiesen},\n  title = {Bound Propagation},\n  year = {2022},\n  publisher = {GitHub},\n  journal = {GitHub repository},\n  howpublished = {\\url{https://github.com/Zinoex/bound_propagation}}\n}\n```\n\n## Funding and support\n- TU Delft\n\n## Copyright notice:\nTechnische Universiteit Delft hereby disclaims all copyright\ninterest in the program “bound_propagation” \n(bound propagation methods for Pytorch)\nwritten by the Frederik Baymler Mathiesen. Theun Baller, Dean of Mechanical, Maritime and Materials Engineering\n\n© 2022, Frederik Baymler Mathiesen, HERALD Lab, TU Delft\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzinoex%2Fbound_propagation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzinoex%2Fbound_propagation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzinoex%2Fbound_propagation/lists"}