diff --git a/.flake8 b/.flake8 index 2abb4db..4f02f5c 100644 --- a/.flake8 +++ b/.flake8 @@ -5,7 +5,10 @@ extend-ignore = C101, # Coding magic comment D100, # Missing docstring in public module D104, # Missing docstring in public package + D202, # No blank lines allowed after function docstring D401, # First line should be in imperative mood + R504, # unnecessary variable assignment before return statement + R505, # unnecessary else after return statement per-file-ignores = sample_code.py: D100, D101, D102, D103, D104 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..e3252aa --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @vivswan \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f513a67..395798e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -19,8 +19,8 @@ sphinx: formats: all # Optionally declare the Python requirements required to build your docs python: - install: - - method: pip - path: . - extra_requirements: - - doc + install: + - method: pip + path: . + extra_requirements: + - doc diff --git a/analogvnn/__init__.py b/analogvnn/__init__.py index 73955d4..fb54294 100644 --- a/analogvnn/__init__.py +++ b/analogvnn/__init__.py @@ -1,11 +1,19 @@ import sys -import importlib_metadata +if sys.version_info[:2] >= (3, 8): + from importlib import metadata +else: + import importlib_metadata as metadata # pragma: no cover + __package__ = 'analogvnn' -__version__ = importlib_metadata.version(__package__) __author__ = 'Vivswan Shah (vivswanshah@pitt.edu)' +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + __version__ = '0.0.0' + if sys.version_info < (3, 7, 0): import warnings diff --git a/analogvnn/backward/BackwardFunction.py b/analogvnn/backward/BackwardFunction.py index 0760003..73fc7b5 100644 --- a/analogvnn/backward/BackwardFunction.py +++ b/analogvnn/backward/BackwardFunction.py @@ -27,6 +27,7 @@ def __init__(self, backward_function: Callable, layer: nn.Module = None): backward_function (Callable): The function used to compute the backward gradient. layer (nn.Module): The layer that this backward module is associated with. """ + super(BackwardFunction, self).__init__(layer) self._backward_function = backward_function @@ -37,6 +38,7 @@ def backward_function(self) -> Callable: Returns: Callable: The function used to compute the backward gradient. """ + return self._backward_function @backward_function.setter @@ -46,6 +48,7 @@ def backward_function(self, backward_function: Callable): Args: backward_function (Callable): The function used to compute the backward gradient with. """ + self.set_backward_function(backward_function) def set_backward_function(self, backward_function: Callable) -> BackwardFunction: @@ -57,6 +60,7 @@ def set_backward_function(self, backward_function: Callable) -> BackwardFunction Returns: BackwardFunction: self. """ + self._backward_function = backward_function return self @@ -73,6 +77,7 @@ def backward(self, *grad_output: Tensor, **grad_output_kwarg: Tensor) -> TENSORS Raises: NotImplementedError: If the backward function is not set. """ + if self._backward_function is None: raise ValueError('set backward_function first before invoking backward') diff --git a/analogvnn/backward/BackwardIdentity.py b/analogvnn/backward/BackwardIdentity.py index f8c9e61..96006bc 100644 --- a/analogvnn/backward/BackwardIdentity.py +++ b/analogvnn/backward/BackwardIdentity.py @@ -21,6 +21,7 @@ def backward(self, *grad_output: Tensor, **grad_output_kwarg: Tensor) -> TENSORS Returns: TENSORS: The gradients of the input of the layer. """ + if len(grad_output) == 0: return None diff --git a/analogvnn/backward/BackwardModule.py b/analogvnn/backward/BackwardModule.py index ed1fccf..6193a12 100644 --- a/analogvnn/backward/BackwardModule.py +++ b/analogvnn/backward/BackwardModule.py @@ -50,6 +50,7 @@ def forward(ctx: Any, backward_module: BackwardModule, _: Tensor, *args: Tensor, Returns: TENSORS: The output of the function. """ + ctx.backward_module = backward_module return ctx.backward_module._call_impl_forward(*args, **kwargs) @@ -64,6 +65,7 @@ def backward(ctx: Any, *grad_outputs: Tensor) -> Tuple[None, None, TENSORS]: Returns: TENSORS: The gradients of the input of the function. """ + backward_module: BackwardModule = ctx.backward_module results = backward_module._call_impl_backward(*grad_outputs) @@ -78,6 +80,7 @@ def __init__(self, layer: nn.Module = None): Args: layer (nn.Module): The layer for which the backward gradient is computed. """ + super(BackwardModule, self).__init__() self._layer = None self._set_autograd_backward() @@ -97,6 +100,7 @@ def forward(self, *inputs: Tensor, **inputs_kwarg: Tensor) -> TENSORS: Raises: NotImplementedError: If the forward pass is not implemented. """ + raise NotImplementedError(f'Module [{type(self).__name__}] is missing the required "forward" function') forward._implemented = False @@ -115,6 +119,7 @@ def backward(self, *grad_outputs: Tensor, **grad_output_kwarg: Tensor) -> TENSOR Raises: NotImplementedError: If the backward pass is not implemented. """ + raise NotImplementedError(f'Module [{type(self).__name__}] is missing the required "backward" function') def _call_impl_forward(self, *args: Tensor, **kwarg: Tensor) -> TENSORS: @@ -127,6 +132,7 @@ def _call_impl_forward(self, *args: Tensor, **kwarg: Tensor) -> TENSORS: Returns: TENSORS: The output of the layer. """ + return self.forward(*args, **kwarg) def _call_impl_backward(self, *grad_output: Tensor, **grad_output_kwarg: Tensor) -> TENSORS: @@ -139,6 +145,7 @@ def _call_impl_backward(self, *grad_output: Tensor, **grad_output_kwarg: Tensor) Returns: TENSORS: The gradients of the input of the layer. """ + return self.backward(*grad_output, **grad_output_kwarg) __call__: Callable[..., Any] = _call_impl_backward @@ -155,6 +162,7 @@ def auto_apply(self, *args: Tensor, to_apply=True, **kwargs: Tensor) -> TENSORS: Returns: TENSORS: The output of the layer. """ + if to_apply and not self._disable_autograd_backward: return self._autograd_backward.apply(self, self._empty_holder_tensor, *args, **kwargs) else: @@ -166,6 +174,7 @@ def has_forward(self) -> bool: Returns: bool: True if the forward pass is implemented, False otherwise. """ + return not hasattr(self.forward, '_implemented') @property @@ -175,6 +184,7 @@ def layer(self) -> Optional[nn.Module]: Returns: Optional[nn.Module]: layer """ + return self.get_layer() def get_layer(self) -> Optional[nn.Module]: @@ -183,6 +193,7 @@ def get_layer(self) -> Optional[nn.Module]: Returns: Optional[nn.Module]: layer """ + if isinstance(self, nn.Module): return self else: @@ -202,6 +213,7 @@ def set_layer(self, layer: Optional[nn.Module]) -> BackwardModule: ValueError: If the layer is already set. ValueError: If the layer is not an instance of nn.Module. """ + if isinstance(self, nn.Module): raise ValueError('layer of Backward Module is set to itself') if self._layer is not None: @@ -237,6 +249,7 @@ def set_grad_of(tensor: Tensor, grad: Tensor) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the tensor. """ + if tensor is None or tensor.requires_grad is False: return None @@ -267,6 +280,7 @@ def __getattr__(self, name: str) -> Any: Raises: AttributeError: If the attribute is not found. """ + if isinstance(self, nn.Module) or self == self._layer: return super(BackwardModule, self).__getattr__(name) if not str(name).startswith('__') and self._layer is not None and hasattr(self._layer, name): diff --git a/analogvnn/backward/BackwardUsingForward.py b/analogvnn/backward/BackwardUsingForward.py index 819eff7..95338d0 100644 --- a/analogvnn/backward/BackwardUsingForward.py +++ b/analogvnn/backward/BackwardUsingForward.py @@ -21,4 +21,5 @@ def backward(self, *grad_output: Tensor, **grad_output_kwarg: Tensor) -> TENSORS Returns: TENSORS: The gradients of the input of the layer. """ + return self._layer.forward(*grad_output, **grad_output_kwarg) diff --git a/analogvnn/fn/dirac_delta.py b/analogvnn/fn/dirac_delta.py index ca16db2..8e9f021 100644 --- a/analogvnn/fn/dirac_delta.py +++ b/analogvnn/fn/dirac_delta.py @@ -16,4 +16,5 @@ def dirac_delta(x: TENSOR_OPERABLE, a: TENSOR_OPERABLE = 0.001) -> TENSOR_OPERAB TENSOR_OPERABLE: TENSOR_OPERABLE with the same shape as x, but with values equal to the Dirac delta function of x. """ + return 1 / (np.abs(a) * np.sqrt(np.pi)) * np.exp(-((x / a) ** 2)) diff --git a/analogvnn/fn/reduce_precision.py b/analogvnn/fn/reduce_precision.py index 2d7ccc3..1c8f394 100644 --- a/analogvnn/fn/reduce_precision.py +++ b/analogvnn/fn/reduce_precision.py @@ -18,6 +18,7 @@ def reduce_precision(x: TENSOR_OPERABLE, precision: TENSOR_OPERABLE, divide: TEN TENSOR_OPERABLE: TENSOR_OPERABLE with the same shape as x, but with values rounded to the nearest multiple of precision. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) g: Tensor = x * precision f = torch.sign(g) * torch.maximum( @@ -38,6 +39,7 @@ def stochastic_reduce_precision(x: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) TENSOR_OPERABLE: TENSOR_OPERABLE with the same shape as x, but with values rounded to the nearest multiple of precision. """ + g: Tensor = x * precision rand_x = torch.rand_like(g, requires_grad=False) diff --git a/analogvnn/fn/test.py b/analogvnn/fn/test.py index c4496cf..a2087a3 100644 --- a/analogvnn/fn/test.py +++ b/analogvnn/fn/test.py @@ -18,6 +18,7 @@ def test(model: nn.Module, test_loader: DataLoader, test_run: bool = False) -> T Returns: tuple: the loss and accuracy of the model on the test set. """ + model.eval() total_loss = 0 total_accuracy = 0 diff --git a/analogvnn/fn/to_matrix.py b/analogvnn/fn/to_matrix.py index facd900..392b0ff 100644 --- a/analogvnn/fn/to_matrix.py +++ b/analogvnn/fn/to_matrix.py @@ -12,6 +12,7 @@ def to_matrix(tensor: Tensor) -> Tensor: Returns: Tensor: Tensor with the same values as the tensor, but with shape (1, -1). """ + if len(tensor.size()) == 1: return tensor.view(1, -1) return tensor diff --git a/analogvnn/fn/train.py b/analogvnn/fn/train.py index 2b32b7f..5de064c 100644 --- a/analogvnn/fn/train.py +++ b/analogvnn/fn/train.py @@ -23,6 +23,7 @@ def train( Returns: tuple: the loss and accuracy of the model on the train set. """ + model.train() total_loss = 0.0 total_accuracy = 0 diff --git a/analogvnn/graph/AccumulateGrad.py b/analogvnn/graph/AccumulateGrad.py index d46b9c9..4451250 100644 --- a/analogvnn/graph/AccumulateGrad.py +++ b/analogvnn/graph/AccumulateGrad.py @@ -29,6 +29,7 @@ def __init__(self, module: Union[nn.Module, Callable]): Args: module (Union[nn.Module, Callable]): Module from which to accumulate gradients. """ + super(AccumulateGrad, self).__init__() self.input_output_connections = {} self.module = module @@ -39,6 +40,7 @@ def __repr__(self): Returns: str: String representation of the module. """ + return f'AccumulateGrad({self.module})' def __call__( # noqa: C901 @@ -55,6 +57,7 @@ def __call__( # noqa: C901 Returns: ArgsKwargs: The output gradients. """ + grad_inputs_args = {} grad_inputs_kwargs = {} for key, grad_output in grad_outputs_args_kwargs.kwargs.items(): diff --git a/analogvnn/graph/AcyclicDirectedGraph.py b/analogvnn/graph/AcyclicDirectedGraph.py index 3477a8d..f333918 100644 --- a/analogvnn/graph/AcyclicDirectedGraph.py +++ b/analogvnn/graph/AcyclicDirectedGraph.py @@ -49,6 +49,7 @@ def __init__(self, graph_state: ModelGraphState = None): Raises: NotImplementedError: If allow_loops is True, since this is not implemented yet. """ + super(AcyclicDirectedGraph, self).__init__() self.graph = nx.MultiDiGraph() self.graph_state = graph_state @@ -69,6 +70,7 @@ def __call__(self, *args, **kwargs): Raises: NotImplementedError: since method is abstract """ + raise NotImplementedError def add_connection(self, *args: GRAPH_NODE_TYPE): @@ -80,6 +82,7 @@ def add_connection(self, *args: GRAPH_NODE_TYPE): Returns: AcyclicDirectedGraph: self. """ + for i in range(1, len(args)): self.add_edge(args[i - 1], args[i]) return self @@ -106,6 +109,7 @@ def add_edge( Returns: AcyclicDirectedGraph: self. """ + attr = self.check_edge_parameters(in_arg, in_kwarg, out_arg, out_kwarg) existing_edges = self.graph.get_edge_data(u_of_edge, v_of_edge) @@ -148,6 +152,7 @@ def check_edge_parameters( Raises: ValueError: If in and out parameters are invalid. """ + # @@@ in_arg: None in_kwarg: None out_arg: None out_kwarg: None 0 # @@ in_arg: True in_kwarg: None out_arg: True out_kwarg: None 1 # in_arg: None in_kwarg: True out_arg: True out_kwarg: None 2 @@ -237,6 +242,7 @@ def _create_edge_label( Returns: str: The edge's label. """ + label = '' if in_arg == in_kwarg == out_arg == out_kwarg is True: return '* -> *' @@ -274,6 +280,7 @@ def compile(self, is_static: bool = True): Raises: ValueError: If the graph is not acyclic. """ + for i in nx.simple_cycles(self.graph): raise ValueError(f'Cycle detected: {i}') @@ -292,6 +299,7 @@ def _reindex_out_args(graph: nx.MultiDiGraph) -> nx.MultiDiGraph: Returns: nx.MultiDiGraph: The graph with re-indexed output arguments. """ + # noinspection PyTypeChecker graph: nx.MultiDiGraph = graph.copy() @@ -324,6 +332,7 @@ def _create_static_sub_graph( Returns: List[Tuple[GRAPH_NODE_TYPE, List[GRAPH_NODE_TYPE]]]: The static sub graph. """ + if self._is_static and from_node in self._static_graphs: return self._static_graphs[from_node] @@ -358,6 +367,7 @@ def parse_args_kwargs( # noqa: C901 Returns: ArgsKwargs: The arguments and keyword arguments. """ + args = {} extra_args = [] kwargs = {} @@ -436,4 +446,5 @@ def render(self, *args, real_label: bool = False, **kwargs) -> str: Returns: str: The (possibly relative) path of the rendered file. """ + return to_graphviz_digraph(self.graph, real_label=real_label).render(*args, **kwargs) diff --git a/analogvnn/graph/ArgsKwargs.py b/analogvnn/graph/ArgsKwargs.py index 61c1116..0899ea4 100644 --- a/analogvnn/graph/ArgsKwargs.py +++ b/analogvnn/graph/ArgsKwargs.py @@ -38,6 +38,7 @@ def __init__(self, args=None, kwargs=None): args: The arguments. kwargs: The keyword arguments. """ + super(ArgsKwargs, self).__init__() if args is None: args = [] @@ -55,10 +56,12 @@ def __init__(self, args=None, kwargs=None): def is_empty(self): """Returns whether the ArgsKwargs object is empty.""" + return len(self.args) == 0 and not bool(self.kwargs) def __repr__(self): """Returns a string representation of the parameter.""" + return f'ArgsKwargs(args={self.args}, kwargs={self.kwargs})' @classmethod @@ -71,6 +74,7 @@ def to_args_kwargs_object(cls, outputs: ArgsKwargsInput) -> ArgsKwargs: Returns: ArgsKwargs: The ArgsKwargs object """ + if isinstance(outputs, cls): pass elif isinstance(outputs, dict): @@ -91,6 +95,7 @@ def from_args_kwargs_object(outputs: ArgsKwargs) -> ArgsKwargsOutput: Returns: ArgsKwargsOutput: object or tuple or dict """ + if len(outputs.kwargs.keys()) > 0: return outputs elif len(outputs.args) > 1: diff --git a/analogvnn/graph/BackwardGraph.py b/analogvnn/graph/BackwardGraph.py index 7aff94c..fe0813b 100644 --- a/analogvnn/graph/BackwardGraph.py +++ b/analogvnn/graph/BackwardGraph.py @@ -30,6 +30,7 @@ def __call__(self, gradient: TENSORS = None) -> ArgsKwargsOutput: Returns: ArgsKwargsOutput: gradient of the inputs function w.r.t. loss """ + self.graph_state.ready_for_backward(exception=True) if len(gradient) == 0: @@ -64,6 +65,7 @@ def compile(self, is_static=True): Raises: ValueError: If no forward pass has been performed yet. """ + if not self.graph.has_node(self.OUTPUT): raise ValueError("OUTPUT doesn't exist in the forward graph. Please preform a forward pass first.") @@ -78,6 +80,7 @@ def from_forward(self, forward_graph: Union[AcyclicDirectedGraph, nx.DiGraph]) - Returns: BackwardGraph: self. """ + if isinstance(forward_graph, AcyclicDirectedGraph): forward_graph = forward_graph.graph @@ -175,6 +178,7 @@ def calculate(self, *args, **kwargs) -> ArgsKwargsOutput: Raises: ValueError: If no forward pass has been performed yet. """ + if self.graph_state.forward_input_output_graph is None: raise ValueError('No forward pass has been performed yet. Please preform a forward pass first.') @@ -197,6 +201,7 @@ def _pass(self, from_node: GRAPH_NODE_TYPE, *args, **kwargs) -> Dict[GRAPH_NODE_ Returns: Dict[GRAPH_NODE_TYPE, InputOutput]: The input and output gradients of each node. """ + static_graph: List[Tuple[GRAPH_NODE_TYPE, List[GRAPH_NODE_TYPE]]] = self._create_static_sub_graph(from_node) input_output_graph: Dict[GRAPH_NODE_TYPE, InputOutput] = { from_node: InputOutput(inputs=ArgsKwargs( @@ -235,6 +240,7 @@ def _calculate_gradients( # noqa: C901 Returns: ArgsKwargs: The input gradients of the module. """ + if module in self.graph_state.forward_input_output_graph: module_inputs_outputs = self.graph_state.forward_input_output_graph[module] else: diff --git a/analogvnn/graph/ForwardGraph.py b/analogvnn/graph/ForwardGraph.py index 9dea69a..f66882c 100644 --- a/analogvnn/graph/ForwardGraph.py +++ b/analogvnn/graph/ForwardGraph.py @@ -26,6 +26,7 @@ def __call__(self, inputs: TENSORS, is_training: bool) -> ArgsKwargsOutput: Returns: ArgsKwargsOutput: Output of the graph """ + self.graph_state.ready_for_forward(exception=True) outputs = self.calculate(inputs, is_training) return outputs @@ -42,6 +43,7 @@ def compile(self, is_static: bool = True): Raises: ValueError: If no forward pass has been performed yet. """ + if not self.graph.has_node(self.INPUT): raise ValueError("INPUT doesn't exist in the forward graph. Please preform a forward pass first.") @@ -66,6 +68,7 @@ def calculate( Returns: ArgsKwargsOutput: Output of the graph """ + if not isinstance(inputs, Sequence): inputs = (inputs,) @@ -90,6 +93,7 @@ def _pass(self, from_node: GraphEnum, *inputs: Tensor) -> Dict[GraphEnum, InputO Returns: Dict[GraphEnum, InputOutput]: The input and output of each node """ + static_graph = self._create_static_sub_graph(from_node) input_output_graph = { from_node: InputOutput(inputs=ArgsKwargs(args=[*inputs])) @@ -124,6 +128,7 @@ def _detach_tensor(tensor: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: Detached tensor """ + tensor: torch.Tensor = tensor.detach() tensor.requires_grad = True return tensor diff --git a/analogvnn/graph/ModelGraph.py b/analogvnn/graph/ModelGraph.py index 57d7874..a14b5fd 100644 --- a/analogvnn/graph/ModelGraph.py +++ b/analogvnn/graph/ModelGraph.py @@ -25,6 +25,7 @@ def __init__(self, use_autograd_graph: bool = False, allow_loops: bool = False): use_autograd_graph: If True, the autograd graph is used to calculate the gradients. allow_loops: If True, the graph is allowed to contain loops. """ + super(ModelGraph, self).__init__(use_autograd_graph, allow_loops) self.forward_graph = ForwardGraph(self) self.backward_graph = BackwardGraph(self) @@ -39,6 +40,7 @@ def compile(self, is_static: bool = True, auto_backward_graph: bool = False) -> Returns: ModelGraph: self. """ + self.forward_graph.compile(is_static=is_static) if auto_backward_graph: diff --git a/analogvnn/graph/ModelGraphState.py b/analogvnn/graph/ModelGraphState.py index e7a3790..9021cab 100644 --- a/analogvnn/graph/ModelGraphState.py +++ b/analogvnn/graph/ModelGraphState.py @@ -45,6 +45,7 @@ def __init__(self, use_autograd_graph: bool = False, allow_loops=False): use_autograd_graph: If True, the autograd graph is used to calculate the gradients. allow_loops: If True, the graph is allowed to contain loops. """ + super(ModelGraphState, self).__init__() self.allow_loops = allow_loops self.use_autograd_graph = use_autograd_graph @@ -52,17 +53,20 @@ def __init__(self, use_autograd_graph: bool = False, allow_loops=False): self.forward_input_output_graph = None self._loss = None - # noinspection PyUnusedLocal - @staticmethod - def ready_for_forward(exception: bool = False) -> bool: + # noinspection PyUnusedLocal,PyMethodMayBeStatic + def ready_for_forward(self, exception: bool = False) -> bool: """Check if the state is ready for forward pass. Args: - exception (bool): if True, raise an exception if the state is not ready for forward pass. + exception (bool): If True, an exception is raised if the state is not ready for forward pass. Returns: bool: True if the state is ready for forward pass. + + Raises: + RuntimeError: If the state is not ready for forward pass and exception is True. """ + return True def ready_for_backward(self, exception: bool = False) -> bool: @@ -77,14 +81,15 @@ def ready_for_backward(self, exception: bool = False) -> bool: Raises: RuntimeError: if the state is not ready for backward pass and exception is True. """ + if exception: if self.outputs is None: raise RuntimeError('output is not set.') if self._loss is None: raise RuntimeError('loss is not set.') - else: - return not (self.outputs is None or self._loss is None) + + return not (self.outputs is None or self._loss is None) @property def inputs(self) -> Optional[ArgsKwargs]: @@ -93,6 +98,7 @@ def inputs(self) -> Optional[ArgsKwargs]: Returns: ArgsKwargs: the inputs. """ + if self.INPUT not in self.forward_input_output_graph: return None return self.forward_input_output_graph[self.INPUT].inputs @@ -104,6 +110,7 @@ def outputs(self) -> Optional[ArgsKwargs]: Returns: ArgsKwargs: the output. """ + if self.OUTPUT not in self.forward_input_output_graph: return None return self.forward_input_output_graph[self.OUTPUT].outputs @@ -115,6 +122,7 @@ def loss(self): Returns: Tensor: the loss. """ + return self._loss def set_loss(self, loss: Union[Tensor, None]) -> ModelGraphState: @@ -126,5 +134,6 @@ def set_loss(self, loss: Union[Tensor, None]) -> ModelGraphState: Returns: ModelGraphState: self. """ + self._loss = loss return self diff --git a/analogvnn/graph/to_graph_viz_digraph.py b/analogvnn/graph/to_graph_viz_digraph.py index 96fdda7..9f9ffd3 100644 --- a/analogvnn/graph/to_graph_viz_digraph.py +++ b/analogvnn/graph/to_graph_viz_digraph.py @@ -26,6 +26,7 @@ def to_graphviz_digraph(from_graph: networkx.DiGraph, real_label: bool = False) ImportError: if pygraphviz (https://pygraphviz.github.io/) is not available. ImportError: if graphviz (https://pygraphviz.github.io/) is not available. """ + try: # noinspection PyPackageRequirements import pygraphviz # noqa: F401 diff --git a/analogvnn/nn/Linear.py b/analogvnn/nn/Linear.py index eceb7d3..445493e 100644 --- a/analogvnn/nn/Linear.py +++ b/analogvnn/nn/Linear.py @@ -23,6 +23,7 @@ def forward(self, x: Tensor): Returns: Tensor: The output of the linear layer. """ + if self.bias is not None: y = torch.addmm(self.bias, x, self.weight.t()) else: @@ -40,6 +41,7 @@ def backward(self, grad_output: Optional[Tensor], weight: Optional[Tensor] = Non Returns: Optional[Tensor]: The gradient of the input. """ + grad_output = to_matrix(grad_output) weight = to_matrix(self.weight if weight is None else weight) @@ -74,6 +76,7 @@ def __init__(self, in_features: int, out_features: int, bias: bool = True): out_features (int): The number of output features. bias (bool): True if the layer has a bias. """ + super(Linear, self).__init__() self.in_features = in_features self.out_features = out_features @@ -89,6 +92,7 @@ def __init__(self, in_features: int, out_features: int, bias: bool = True): def reset_parameters(self): """Reset the parameters of the layer.""" + nn.init.xavier_uniform_(self.weight) if self.bias is not None: fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight) @@ -101,4 +105,5 @@ def extra_repr(self) -> str: Returns: str: The extra representation of the linear layer. """ + return f'in_features={self.in_features}, out_features={self.out_features}, bias={self.bias is not None}' diff --git a/analogvnn/nn/activation/Activation.py b/analogvnn/nn/activation/Activation.py index 902194b..e779823 100644 --- a/analogvnn/nn/activation/Activation.py +++ b/analogvnn/nn/activation/Activation.py @@ -21,6 +21,7 @@ def initialise(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform(tensor) @staticmethod @@ -33,6 +34,7 @@ def initialise_(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform_(tensor) diff --git a/analogvnn/nn/activation/BinaryStep.py b/analogvnn/nn/activation/BinaryStep.py index b8684b2..9213b53 100644 --- a/analogvnn/nn/activation/BinaryStep.py +++ b/analogvnn/nn/activation/BinaryStep.py @@ -21,6 +21,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return (x >= 0).type(torch.float) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -32,4 +33,5 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + return torch.zeros_like(grad_output) diff --git a/analogvnn/nn/activation/ELU.py b/analogvnn/nn/activation/ELU.py index e736a2c..03cabce 100644 --- a/analogvnn/nn/activation/ELU.py +++ b/analogvnn/nn/activation/ELU.py @@ -27,6 +27,7 @@ def __init__(self, alpha: float = 1.0507, scale_factor: float = 1.): alpha (float): the alpha parameter. scale_factor (float): the scale factor parameter. """ + super(SELU, self).__init__() self.alpha = nn.Parameter(torch.tensor(alpha), requires_grad=False) self.scale_factor = nn.Parameter(torch.tensor(scale_factor), requires_grad=False) @@ -40,6 +41,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return self.scale_factor * ( (x <= 0).type(torch.float) * self.alpha * (torch.exp(x) - 1) + (x > 0).type(torch.float) * x @@ -54,6 +56,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = self.scale_factor * ((x < 0).type(torch.float) * self.alpha * torch.exp(x) + (x >= 0).type(torch.float)) return grad_output * grad @@ -68,6 +71,7 @@ def initialise(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform(tensor, gain=nn.init.calculate_gain('selu')) @staticmethod @@ -80,6 +84,7 @@ def initialise_(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform_(tensor, gain=nn.init.calculate_gain('selu')) @@ -97,4 +102,5 @@ def __init__(self, alpha: float = 1.0507): Args: alpha (float): the alpha parameter. """ + super(ELU, self).__init__(alpha=alpha, scale_factor=1.) diff --git a/analogvnn/nn/activation/Gaussian.py b/analogvnn/nn/activation/Gaussian.py index 8e82c6a..2d0661e 100644 --- a/analogvnn/nn/activation/Gaussian.py +++ b/analogvnn/nn/activation/Gaussian.py @@ -22,6 +22,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return torch.exp(-torch.pow(x, 2)) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -33,6 +34,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = -2 * x * torch.exp(-torch.pow(x, 2)) return grad_output * grad @@ -51,6 +53,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return (1 / 2) * x * (1 + torch.erf(x / math.sqrt(2))) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -62,6 +65,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = (1 / 2) * ( (1 + torch.erf(x / math.sqrt(2))) + x * ((2 / math.sqrt(math.pi)) * torch.exp(-torch.pow(x, 2))) diff --git a/analogvnn/nn/activation/Identity.py b/analogvnn/nn/activation/Identity.py index d687226..136741d 100644 --- a/analogvnn/nn/activation/Identity.py +++ b/analogvnn/nn/activation/Identity.py @@ -22,6 +22,7 @@ def __init__(self, name=None): Args: name (str): the name of the activation function. """ + super(Identity, self).__init__() self.name = name @@ -31,6 +32,7 @@ def extra_repr(self) -> str: Returns: str: the extra representation of the identity activation function. """ + if self.name is not None: return f'name={self.name}' else: @@ -46,6 +48,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor same as the input tensor. """ + return x def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -57,4 +60,5 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor same as the gradient of the output tensor. """ + return grad_output diff --git a/analogvnn/nn/activation/ReLU.py b/analogvnn/nn/activation/ReLU.py index f00d576..c7358f9 100644 --- a/analogvnn/nn/activation/ReLU.py +++ b/analogvnn/nn/activation/ReLU.py @@ -27,6 +27,7 @@ def __init__(self, alpha: float): Args: alpha (float): the slope of the negative part of the activation function. """ + super(PReLU, self).__init__() self.alpha = nn.Parameter(torch.tensor(alpha), requires_grad=False) self._zero = nn.Parameter(torch.tensor(0), requires_grad=False) @@ -40,6 +41,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return torch.minimum(self._zero, x) * self.alpha + torch.maximum(self._zero, x) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -51,6 +53,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = (x < 0).type(torch.float) * self.alpha + (x >= 0).type(torch.float) return grad_output * grad @@ -65,6 +68,7 @@ def initialise(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.kaiming_uniform(tensor, a=math.sqrt(5), nonlinearity='leaky_relu') @staticmethod @@ -77,6 +81,7 @@ def initialise_(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.kaiming_uniform_(tensor, a=math.sqrt(5), nonlinearity='leaky_relu') @@ -89,6 +94,7 @@ class ReLU(PReLU): def __init__(self): """Initialize the rectified linear unit (ReLU) activation function.""" + super(ReLU, self).__init__(alpha=0) @staticmethod @@ -101,6 +107,7 @@ def initialise(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.kaiming_uniform(tensor, a=math.sqrt(5), nonlinearity='relu') @staticmethod @@ -113,6 +120,7 @@ def initialise_(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.kaiming_uniform_(tensor, a=math.sqrt(5), nonlinearity='relu') @@ -125,4 +133,5 @@ class LeakyReLU(PReLU): def __init__(self): """Initialize the leaky rectified linear unit (LeakyReLU) activation function.""" + super(LeakyReLU, self).__init__(alpha=0.01) diff --git a/analogvnn/nn/activation/SiLU.py b/analogvnn/nn/activation/SiLU.py index afb0265..974f8e1 100644 --- a/analogvnn/nn/activation/SiLU.py +++ b/analogvnn/nn/activation/SiLU.py @@ -21,6 +21,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return x / (1 + torch.exp(-x)) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -32,6 +33,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs neg_e = torch.exp(-x) grad = (1 + neg_e + x * neg_e) / torch.pow(1 + neg_e, 2) diff --git a/analogvnn/nn/activation/Sigmoid.py b/analogvnn/nn/activation/Sigmoid.py index a14e289..2a204ca 100644 --- a/analogvnn/nn/activation/Sigmoid.py +++ b/analogvnn/nn/activation/Sigmoid.py @@ -21,6 +21,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return 1 / (1 + torch.exp(-x)) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -32,6 +33,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = self.forward(x) * (1 - self.forward(x)) return grad_output * grad @@ -46,6 +48,7 @@ def initialise(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform(tensor, gain=nn.init.calculate_gain('sigmoid')) @staticmethod @@ -58,6 +61,7 @@ def initialise_(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform_(tensor, gain=nn.init.calculate_gain('sigmoid')) diff --git a/analogvnn/nn/activation/Tanh.py b/analogvnn/nn/activation/Tanh.py index 4d7d61e..4f435a7 100644 --- a/analogvnn/nn/activation/Tanh.py +++ b/analogvnn/nn/activation/Tanh.py @@ -21,6 +21,7 @@ def forward(x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return torch.tanh(x) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -32,6 +33,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = 1 - torch.pow(torch.tanh(x), 2) return grad_output * grad @@ -46,6 +48,7 @@ def initialise(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform(tensor, gain=nn.init.calculate_gain('tanh')) @staticmethod @@ -58,4 +61,5 @@ def initialise_(tensor: Tensor) -> Tensor: Returns: Tensor: the initialized tensor. """ + return nn.init.xavier_uniform_(tensor, gain=nn.init.calculate_gain('tanh')) diff --git a/analogvnn/nn/module/FullSequential.py b/analogvnn/nn/module/FullSequential.py index 1ab5a21..9411383 100644 --- a/analogvnn/nn/module/FullSequential.py +++ b/analogvnn/nn/module/FullSequential.py @@ -22,7 +22,6 @@ def compile(self, device: Optional[torch.device] = None, layer_data: bool = True Returns: FullSequential: self """ - arr = [self.graphs.INPUT, *list(self._runtime_module_list.values()), self.graphs.OUTPUT] - self.graphs.forward_graph.add_connection(*arr) + arr = [self.graphs.INPUT, *list(self.registered_children()), self.graphs.OUTPUT] self.graphs.backward_graph.add_connection(*reversed(arr)) return super().compile(device, layer_data) diff --git a/analogvnn/nn/module/Layer.py b/analogvnn/nn/module/Layer.py index e0a07c2..f73630e 100644 --- a/analogvnn/nn/module/Layer.py +++ b/analogvnn/nn/module/Layer.py @@ -1,7 +1,7 @@ from __future__ import annotations import functools -from typing import Union, Type, Callable, Sequence, Optional, TYPE_CHECKING +from typing import Union, Type, Callable, Sequence, Optional, TYPE_CHECKING, Set, Iterator, Tuple from torch import nn, Tensor @@ -26,6 +26,7 @@ def __nn_Module_init_updated__(function: Callable) -> Callable: Returns: Callable: Wrapped function """ + # noinspection PyUnusedLocal def _temp(*args, **kwargs) -> ...: pass @@ -92,6 +93,7 @@ def __call__(self, *inputs, **kwargs): *inputs: Inputs of the forward pass. **kwargs: Keyword arguments of the forward pass. """ + self._forward_wrapper(self.forward) outputs = super(Layer, self).__call__(*inputs, **kwargs) if self.training: @@ -107,6 +109,7 @@ def use_autograd_graph(self) -> bool: Returns: bool: use_autograd_graph. """ + if self.graphs is not None: return self.graphs.use_autograd_graph return self._use_autograd_graph @@ -118,6 +121,7 @@ def use_autograd_graph(self, use_autograd_graph: bool): Args: use_autograd_graph (bool): use_autograd_graph. """ + self._use_autograd_graph = use_autograd_graph if self.graphs is not None: self.graphs.use_autograd_graph = use_autograd_graph @@ -129,6 +133,7 @@ def inputs(self) -> ArgsKwargsOutput: Returns: ArgsKwargsOutput: inputs. """ + return ArgsKwargs.from_args_kwargs_object(self._inputs) @property @@ -138,6 +143,7 @@ def outputs(self) -> Union[None, Tensor, Sequence[Tensor]]: Returns: Union[None, Tensor, Sequence[Tensor]]: outputs. """ + return self._outputs @property @@ -147,6 +153,7 @@ def backward_function(self) -> Union[None, Callable, BackwardModule]: Returns: Union[None, Callable, BackwardModule]: backward_function. """ + if self._backward_module is not None: return self._backward_module @@ -162,6 +169,7 @@ def backward_function(self, function: Union[BackwardModule, Type[BackwardModule] Args: function (Union[BackwardModule, Type[BackwardModule], Callable]): backward_function. """ + self.set_backward_function(function) def set_backward_function(self, backward_class: Union[Callable, BackwardModule, Type[BackwardModule]]) -> Layer: @@ -176,6 +184,7 @@ def set_backward_function(self, backward_class: Union[Callable, BackwardModule, Raises: TypeError: If backward_class is not a callable or BackwardModule. """ + if backward_class == self: return self @@ -191,6 +200,50 @@ def set_backward_function(self, backward_class: Union[Callable, BackwardModule, return self + def named_registered_children( + self, + memo: Optional[Set[nn.Module]] = None + ) -> Iterator[Tuple[str, nn.Module]]: + """Returns an iterator over immediate registered children modules. + + Args: + memo: a memo to store the set of modules already added to the result + + + Yields: + (str, Module): Tuple containing a name and child module + + Note: + Duplicate modules are returned only once. In the following + example, ``l`` will be returned only once. + """ + + if memo is None: + memo = set() + + memo.add(self) + memo.add(self.backward_function) + + for name, module in self.named_children(): + if module in memo: + continue + + yield name, module + + def registered_children(self) -> Iterator[nn.Module]: + r"""Returns an iterator over immediate registered children modules. + + Yields: + nn.Module: a module in the network + + Note: + Duplicate modules are returned only once. In the following + example, ``l`` will be returned only once. + """ + + for _, module in self.named_registered_children(): + yield module + def _forward_wrapper(self, function: Callable) -> Callable: """Wrapper for the forward function. @@ -200,6 +253,7 @@ def _forward_wrapper(self, function: Callable) -> Callable: Returns: Callable: Wrapped function. """ + # noinspection PyUnresolvedReferences if hasattr(function, '__wrapper__') and function.__wrapper__ == Layer._forward_wrapper: return function @@ -233,6 +287,7 @@ def _call_impl_forward(self, *args: Tensor, **kwargs: Tensor) -> TENSORS: Returns: TENSORS: Outputs of the forward pass. """ + if isinstance(self.backward_function, BackwardModule) and self.backward_function.has_forward(): forward_functions = self.backward_function.forward else: diff --git a/analogvnn/nn/module/Model.py b/analogvnn/nn/module/Model.py index bb8ba0d..ca93f1b 100644 --- a/analogvnn/nn/module/Model.py +++ b/analogvnn/nn/module/Model.py @@ -1,10 +1,10 @@ from __future__ import annotations import typing -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Set, Iterator import torch -from torch import optim, Tensor +from torch import optim, Tensor, nn from torch.utils.data import DataLoader from analogvnn.fn.test import test @@ -59,6 +59,7 @@ def __init__(self, tensorboard_log_dir=None, device=is_cpu_cuda.device): tensorboard_log_dir (str): The log directory of the tensorboard logger. device (torch.device): The device to run the model on. """ + super(Model, self).__init__() self._compiled = False @@ -76,6 +77,25 @@ def __init__(self, tensorboard_log_dir=None, device=is_cpu_cuda.device): self.accuracy_function = None self.device = device + def __call__(self, *args, **kwargs): + """Call the model. + + Args: + *args: The arguments of the model. + **kwargs: The keyword arguments of the model. + + Returns: + TENSORS: The output of the model. + + Raises: + RuntimeError: if the model is not compiled. + """ + + if not self._compiled: + raise RuntimeError('Model is not compiled yet.') + + return super(Model, self).__call__(*args, **kwargs) + @property def use_autograd_graph(self): """Is the autograd graph used for the model. @@ -83,6 +103,7 @@ def use_autograd_graph(self): Returns: bool: If True, the autograd graph is used to calculate the gradients. """ + return self.graphs.use_autograd_graph @use_autograd_graph.setter @@ -92,8 +113,34 @@ def use_autograd_graph(self, use_autograd_graph: bool): Args: use_autograd_graph (bool): If True, the autograd graph is used to calculate the gradients. """ + self.graphs.use_autograd_graph = use_autograd_graph + def named_registered_children( + self, + memo: Optional[Set[nn.Module]] = None, + ) -> Iterator[Tuple[str, nn.Module]]: + """Returns an iterator over registered modules under self. + + Args: + memo: a memo to store the set of modules already added to the result + + Yields: + (str, nn.Module): Tuple of name and module + + Note: + Duplicate modules are returned only once. In the following + example, ``l`` will be returned only once. + """ + + if memo is None: + memo = set() + + memo.add(self.optimizer) + memo.add(self.loss_function) + memo.add(self.accuracy_function) + return super(Model, self).named_registered_children(memo=memo) + def compile(self, device: Optional[torch.device] = None, layer_data: bool = True): """Compile the model. @@ -104,6 +151,7 @@ def compile(self, device: Optional[torch.device] = None, layer_data: bool = True Returns: Model: The compiled model. """ + if device is not None: self.device = device @@ -128,6 +176,7 @@ def forward(self, *inputs: Tensor) -> TENSORS: Returns: TENSORS: The output of the model. """ + return self.graphs.forward_graph(inputs, self.training) @torch.no_grad() @@ -140,6 +189,7 @@ def backward(self, *inputs: Tensor) -> TENSORS: Returns: TENSORS: The output of the model. """ + return self.graphs.backward_graph(inputs) def loss(self, output: Tensor, target: Tensor) -> Tuple[Tensor, Tensor]: @@ -155,6 +205,7 @@ def loss(self, output: Tensor, target: Tensor) -> Tuple[Tensor, Tensor]: Raises: ValueError: if loss_function is None. """ + if self.loss_function is None: raise ValueError('loss_function is None') @@ -183,6 +234,7 @@ def train_on(self, train_loader: DataLoader, epoch: int = None, *args, **kwargs) Raises: RuntimeError: if model is not compiled. """ + if self._compiled is False: raise RuntimeError('Model is not compiled') @@ -209,6 +261,7 @@ def test_on(self, test_loader: DataLoader, epoch: int = None, *args, **kwargs) - Raises: RuntimeError: if model is not compiled. """ + if self._compiled is False: raise RuntimeError('Model is not compiled') @@ -237,6 +290,7 @@ def fit( Tuple[float, float, float, float]: The train loss, the train accuracy, the test loss and the test accuracy of the model. """ + train_loss, train_accuracy = self.train_on(train_loader=train_loader, epoch=epoch) test_loss, test_accuracy = self.test_on(test_loader=test_loader, epoch=epoch) return train_loss, train_accuracy, test_loss, test_accuracy @@ -250,6 +304,7 @@ def create_tensorboard(self, log_dir: str) -> TensorboardModelLog: Raises: ImportError: if tensorboard (https://www.tensorflow.org/) is not installed. """ + try: from analogvnn.utils.TensorboardModelLog import TensorboardModelLog except ImportError as e: @@ -268,6 +323,7 @@ def subscribe_tensorboard(self, tensorboard: TensorboardModelLog): Returns: Model: self. """ + self.tensorboard = tensorboard if self._compiled is True: self.tensorboard.on_compile() diff --git a/analogvnn/nn/module/Sequential.py b/analogvnn/nn/module/Sequential.py index 1ec0e67..e06afb5 100644 --- a/analogvnn/nn/module/Sequential.py +++ b/analogvnn/nn/module/Sequential.py @@ -1,13 +1,9 @@ from __future__ import annotations -import operator -from collections import OrderedDict -from itertools import islice -from typing import Iterator, TypeVar, Union, Dict, Optional +from typing import TypeVar, Optional import torch from torch import nn -from torch.nn import Module from analogvnn.nn.module.Model import Model @@ -16,22 +12,24 @@ __all__ = ['Sequential'] -class Sequential(Model): - """A sequential model. +class Sequential(Model, nn.Sequential): + """Base class for all sequential models.""" - Attributes: - _runtime_module_list (OrderedDict[str, nn.Module]): The ordered dictionary of the modules. - """ - - def __init__(self, *args): - """Initialize the model. + def __call__(self, *args, **kwargs): + """Call the model. Args: - *args: The modules to add. + *args: The input. + **kwargs: The input. + + Returns: + torch.Tensor: The output of the model. """ - super(Sequential, self).__init__() - self._runtime_module_list: Dict[str, Optional[Module]] = OrderedDict() - self.add_sequence(*args) + + if not self._compiled: + self.compile() + + return super().__call__(*args, **kwargs) def compile(self, device: Optional[torch.device] = None, layer_data: bool = True): """Compile the model and add forward graph. @@ -43,7 +41,7 @@ def compile(self, device: Optional[torch.device] = None, layer_data: bool = True Returns: Sequential: self """ - arr = [self.graphs.INPUT, *list(self._runtime_module_list.values()), self.graphs.OUTPUT] + arr = [self.graphs.INPUT, *list(self.registered_children()), self.graphs.OUTPUT] self.graphs.forward_graph.add_connection(*arr) return super().compile(device, layer_data) @@ -53,100 +51,5 @@ def add_sequence(self, *args): Args: *args (nn.Module): The modules to add. """ - if len(args) == 1 and isinstance(args[0], OrderedDict): - for key, module in args[0].items(): - self._add_run_module(key, module) - else: - for idx, module in enumerate(args): - self._add_run_module(str(idx), module) - - def _add_run_module(self, name: str, module: Optional[Module]): - """Add a module to the forward graph of model. - - Args: - name (str): The name of the module. - module (nn.Module): The module to add. - """ - self.add_module(name, module) - self._runtime_module_list[name] = module - return self - - def _get_item_by_idx(self, iterator, idx) -> T: - """Get the idx-th item of the iterator. - - Args: - iterator (Iterator): The iterator. - idx (int): The index of the item. - - Returns: - T: The idx-th item of the iterator. - """ - size = len(self) - idx = operator.index(idx) - if not -size <= idx < size: - raise IndexError('index {} is out of range'.format(idx)) - idx %= size - return next(islice(iterator, idx, None)) - - def __getitem__(self, idx) -> Union[Sequential, T]: - """Get the idx-th module of the model. - - Args: - idx (int): The index of the module. - - Returns: - Union[Sequential, T]: The idx-th module of the model. - """ - if isinstance(idx, slice): - return self.__class__(OrderedDict(list(self._runtime_module_list.items())[idx])) - else: - return self._get_item_by_idx(self._runtime_module_list.values(), idx) - - def __setitem__(self, idx: int, module: nn.Module) -> None: - """Set the idx-th module of the model. - - Args: - idx (int): The index of the module. - module (nn.Module): The module to set. - """ - key: str = self._get_item_by_idx(self._runtime_module_list.keys(), idx) - return setattr(self, key, module) - - def __delitem__(self, idx: Union[slice, int]) -> None: - """Remove the idx-th module from the model. - - Args: - idx (Union[slice, int]): The index of the module. - """ - if isinstance(idx, slice): - for key in list(self._runtime_module_list.keys())[idx]: - delattr(self, key) - else: - key = self._get_item_by_idx(self._runtime_module_list.keys(), idx) - delattr(self, key) - - def __len__(self) -> int: - """Return the number of modules in the model. - - Returns: - int: The number of modules in the model. - """ - return len(self._runtime_module_list) - - def __dir__(self): - """Return the list of attributes of the module. - - Returns: - list: The list of attributes of the module. - """ - keys = super(Sequential, self).__dir__() - keys = [key for key in keys if not key.isdigit()] - return keys - - def __iter__(self) -> Iterator[nn.Module]: - """Return an iterator over the modules of the model. - Returns: - Iterator[nn.Module]: An iterator over the modules of the model. - """ - return iter(self._runtime_module_list.values()) + return self.extend(args) diff --git a/analogvnn/nn/noise/GaussianNoise.py b/analogvnn/nn/noise/GaussianNoise.py index 4333332..4fe31bd 100644 --- a/analogvnn/nn/noise/GaussianNoise.py +++ b/analogvnn/nn/noise/GaussianNoise.py @@ -41,6 +41,7 @@ def __init__( leakage (float): the leakage of the Gaussian noise. precision (int): the precision of the Gaussian noise. """ + super(GaussianNoise, self).__init__() if (std is None) + (leakage is None) + (precision is None) != 1: @@ -70,6 +71,7 @@ def calc_std(leakage: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> TENSOR_OPE Returns: float: the standard deviation of the Gaussian noise. """ + return 1 / (2 * precision * scipy.special.erfinv(1 - leakage) * math.sqrt(2)) @staticmethod @@ -83,6 +85,7 @@ def calc_precision(std: TENSOR_OPERABLE, leakage: TENSOR_OPERABLE) -> TENSOR_OPE Returns: int: the precision of the Gaussian noise. """ + return 1 / (2 * std * scipy.special.erfinv(1 - leakage) * math.sqrt(2)) @staticmethod @@ -96,6 +99,7 @@ def calc_leakage(std: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> TENSOR_OPE Returns: float: the leakage of the Gaussian noise. """ + return 2 * GaussianNoise.static_cdf(x=-1 / (2 * precision), std=std) @property @@ -105,6 +109,7 @@ def stddev(self) -> Tensor: Returns: Tensor: the standard deviation of the Gaussian noise. """ + return self.std @property @@ -114,6 +119,7 @@ def variance(self) -> Tensor: Returns: Tensor: the variance of the Gaussian noise. """ + return self.stddev.pow(2) def pdf(self, x: Tensor, mean: Tensor = 0) -> Tensor: @@ -126,6 +132,7 @@ def pdf(self, x: Tensor, mean: Tensor = 0) -> Tensor: Returns: Tensor: the probability density function of the Gaussian noise. """ + return torch.exp(self.log_prob(x=x, mean=mean)) def log_prob(self, x: Tensor, mean: Tensor = 0) -> Tensor: @@ -138,6 +145,7 @@ def log_prob(self, x: Tensor, mean: Tensor = 0) -> Tensor: Returns: Tensor: the log probability density function of the Gaussian noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) mean = mean if isinstance(mean, Tensor) else torch.tensor(mean, requires_grad=False) @@ -157,6 +165,7 @@ def static_cdf(x: TENSOR_OPERABLE, std: TENSOR_OPERABLE, mean: TENSOR_OPERABLE = Returns: TENSOR_OPERABLE: the cumulative distribution function of the Gaussian noise. """ + return 1 / 2 * (1 + math.erf((x - mean) / (std * math.sqrt(2)))) def cdf(self, x: Tensor, mean: Tensor = 0) -> Tensor: @@ -169,6 +178,7 @@ def cdf(self, x: Tensor, mean: Tensor = 0) -> Tensor: Returns: Tensor: the cumulative distribution function of the Gaussian noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) mean = mean if isinstance(mean, Tensor) else torch.tensor(mean, requires_grad=False) return self.static_cdf(x=x, std=self.std, mean=mean) @@ -182,6 +192,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return torch.normal(mean=x, std=self.std) def extra_repr(self) -> str: @@ -190,4 +201,5 @@ def extra_repr(self) -> str: Returns: str: the extra representation of the Gaussian noise. """ + return f'std={float(self.std):.4f}, leakage={float(self.leakage):.4f}, precision={int(self.precision)}' diff --git a/analogvnn/nn/noise/LaplacianNoise.py b/analogvnn/nn/noise/LaplacianNoise.py index b88bc2f..ab2b00e 100644 --- a/analogvnn/nn/noise/LaplacianNoise.py +++ b/analogvnn/nn/noise/LaplacianNoise.py @@ -40,6 +40,7 @@ def __init__( leakage (float): the leakage of the Laplacian noise. precision (int): the precision of the Laplacian noise. """ + super(LaplacianNoise, self).__init__() if (scale is None) + (leakage is None) + (precision is None) != 1: @@ -69,6 +70,7 @@ def calc_scale(leakage: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> TENSOR_O Returns: float: the scale of the Laplacian noise. """ + return - 1 / (2 * math.log(leakage) * precision) @staticmethod @@ -82,6 +84,7 @@ def calc_precision(scale: TENSOR_OPERABLE, leakage: TENSOR_OPERABLE) -> TENSOR_O Returns: int: the precision of the Laplacian noise. """ + return - 1 / (2 * math.log(leakage) * scale) @staticmethod @@ -95,6 +98,7 @@ def calc_leakage(scale: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> Tensor: Returns: float: the leakage of the Laplacian noise. """ + return 2 * LaplacianNoise.static_cdf(x=-1 / (2 * precision), scale=scale) @property @@ -104,6 +108,7 @@ def stddev(self) -> Tensor: Returns: Tensor: the standard deviation of the Laplacian noise. """ + return (2 ** 0.5) * self.scale @property @@ -113,6 +118,7 @@ def variance(self) -> Tensor: Returns: Tensor: the variance of the Laplacian noise. """ + return 2 * self.scale.pow(2) def pdf(self, x: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0) -> Tensor: @@ -125,6 +131,7 @@ def pdf(self, x: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0) -> Tensor: Returns: Tensor: the probability density function of the Laplacian noise. """ + return torch.exp(self.log_prob(x=x, loc=loc)) def log_prob(self, x: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0) -> Tensor: @@ -137,6 +144,7 @@ def log_prob(self, x: TENSOR_OPERABLE, loc: TENSOR_OPERABLE = 0) -> Tensor: Returns: Tensor: the log probability density function of the Laplacian noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) loc = loc if isinstance(loc, Tensor) else torch.tensor(loc, requires_grad=False) return -torch.log(2 * self.scale) - torch.abs(x - loc) / self.scale @@ -153,6 +161,7 @@ def static_cdf(x: TENSOR_OPERABLE, scale: TENSOR_OPERABLE, loc: TENSOR_OPERABLE Returns: TENSOR_OPERABLE: the cumulative distribution function of the Laplacian noise. """ + return 0.5 - 0.5 * np.sign(x - loc) * np.expm1(-abs(x - loc) / scale) def cdf(self, x: Tensor, loc: Tensor = 0) -> Tensor: @@ -165,6 +174,7 @@ def cdf(self, x: Tensor, loc: Tensor = 0) -> Tensor: Returns: Tensor: the cumulative distribution function of the Laplacian noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) loc = loc if isinstance(loc, Tensor) else torch.tensor(loc, requires_grad=False) return self.static_cdf(x=x, scale=self.scale, loc=loc) @@ -178,6 +188,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor with Laplacian noise. """ + return torch.distributions.Laplace(loc=x, scale=self.scale).sample() def extra_repr(self) -> str: @@ -186,4 +197,5 @@ def extra_repr(self) -> str: Returns: str: the extra representation of the Laplacian noise. """ + return f'std={float(self.std):.4f}, leakage={float(self.leakage):.4f}, precision={int(self.precision)}' diff --git a/analogvnn/nn/noise/PoissonNoise.py b/analogvnn/nn/noise/PoissonNoise.py index df05371..39a14ad 100644 --- a/analogvnn/nn/noise/PoissonNoise.py +++ b/analogvnn/nn/noise/PoissonNoise.py @@ -42,6 +42,7 @@ def __init__( max_leakage (Optional[float]): the maximum leakage of the Poisson noise. precision (Optional[int]): the precision of the Poisson noise. """ + super(PoissonNoise, self).__init__() if (scale is None) + (max_leakage is None) + (precision is None) != 1: @@ -75,6 +76,7 @@ def calc_scale(max_leakage: TENSOR_OPERABLE, precision: TENSOR_OPERABLE, max_che Returns: TENSOR_OPERABLE: the scale of the Poisson noise function. """ + max_leakage = float(max_leakage) precision = float(precision) r, _ = toms748( @@ -97,6 +99,7 @@ def calc_precision(scale: TENSOR_OPERABLE, max_leakage: TENSOR_OPERABLE, max_che Returns: TENSOR_OPERABLE: the precision of the Poisson noise. """ + max_leakage = float(max_leakage) scale = float(scale) r, _ = toms748( @@ -118,6 +121,7 @@ def calc_max_leakage(scale: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> TENS Returns: TENSOR_OPERABLE: the maximum leakage of the Poisson noise. """ + return 1 - ( PoissonNoise.static_cdf(x=1. + 1 / (2 * precision), rate=1., scale_factor=scale * precision) - PoissonNoise.static_cdf(x=1. - 1 / (2 * precision), rate=1., scale_factor=scale * precision) @@ -135,6 +139,7 @@ def static_cdf(x: TENSOR_OPERABLE, rate: TENSOR_OPERABLE, scale_factor: TENSOR_O Returns: TENSOR_OPERABLE: the cumulative distribution function of the Poisson noise. """ + if np.isclose(rate, np.zeros_like(rate)): return np.ones_like(x) @@ -153,6 +158,7 @@ def staticmethod_leakage(scale: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> Returns: TENSOR_OPERABLE: the leakage of the Poisson noise. """ + cdf_domain = np.linspace(-1, 1, int(2 * precision + 1), dtype=float) correctness = 0 @@ -185,6 +191,7 @@ def leakage(self) -> float: Returns: float: the leakage of the Poisson noise. """ + return self.staticmethod_leakage(scale=float(self.scale), precision=int(self.precision)) @property @@ -194,6 +201,7 @@ def rate_factor(self) -> Tensor: Returns: Tensor: the rate factor of the Poisson noise. """ + return self.precision * self.scale def pdf(self, x: Tensor, rate: Tensor) -> Tensor: @@ -206,6 +214,7 @@ def pdf(self, x: Tensor, rate: Tensor) -> Tensor: Returns: Tensor: the probability density function of the Poisson noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) rate = rate if isinstance(rate, Tensor) else torch.tensor(rate, requires_grad=False) @@ -224,6 +233,7 @@ def log_prob(self, x: Tensor, rate: Tensor) -> Tensor: Returns: Tensor: the log probability of the Poisson noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) rate = rate if isinstance(rate, Tensor) else torch.tensor(rate, requires_grad=False) @@ -241,6 +251,7 @@ def cdf(self, x: Tensor, rate: Tensor) -> Tensor: Returns: Tensor: the cumulative distribution function of the Poisson noise. """ + x = x if isinstance(x, Tensor) else torch.tensor(x, requires_grad=False) rate = rate if isinstance(rate, Tensor) else torch.tensor(rate, requires_grad=False) return self.static_cdf(x, rate, self.rate_factor) @@ -254,6 +265,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output of the Poisson noise. """ + return torch.sign(x) * torch.poisson(torch.abs(x * self.rate_factor)) / self.rate_factor def extra_repr(self) -> str: @@ -262,6 +274,7 @@ def extra_repr(self) -> str: Returns: str: the extra representation of the Poisson noise. """ + return f'scale={float(self.scale):.4f}' \ f', max_leakage={float(self.max_leakage):.4f}' \ f', leakage={float(self.leakage):.4f}' \ diff --git a/analogvnn/nn/noise/UniformNoise.py b/analogvnn/nn/noise/UniformNoise.py index ac3a49d..7e73652 100644 --- a/analogvnn/nn/noise/UniformNoise.py +++ b/analogvnn/nn/noise/UniformNoise.py @@ -42,6 +42,7 @@ def __init__( leakage (float): the leakage of the uniform noise. precision (int): the precision of the uniform noise. """ + super(UniformNoise, self).__init__() if (low is None or high is None) + (leakage is None) + (precision is None) != 1: @@ -71,6 +72,7 @@ def calc_high_low(leakage: TENSOR_OPERABLE, precision: TENSOR_OPERABLE) -> Tuple Returns: Tuple[TENSOR_OPERABLE, TENSOR_OPERABLE]: the high and low of the uniform noise. """ + v = 1 / (1 - leakage) * (1 / precision) return -v / 2, v / 2 @@ -86,6 +88,7 @@ def calc_leakage(low: TENSOR_OPERABLE, high: TENSOR_OPERABLE, precision: TENSOR_ Returns: TENSOR_OPERABLE: the leakage of the uniform noise. """ + return 1 - min(1, (1 / precision) * (1 / (high - low))) @staticmethod @@ -100,6 +103,7 @@ def calc_precision(low: TENSOR_OPERABLE, high: TENSOR_OPERABLE, leakage: TENSOR_ Returns: TENSOR_OPERABLE: the precision of the uniform noise. """ + return 1 / (1 - leakage) * (1 / (high - low)) @property @@ -109,6 +113,7 @@ def mean(self) -> Tensor: Returns: Tensor: the mean of the uniform noise. """ + return (self.high + self.low) / 2 @property @@ -118,6 +123,7 @@ def stddev(self) -> Tensor: Returns: Tensor: the standard deviation of the uniform noise. """ + return (self.high - self.low) / 12 ** 0.5 @property @@ -127,6 +133,7 @@ def variance(self) -> Tensor: Returns: Tensor: the variance of the uniform noise. """ + return (self.high - self.low).pow(2) / 12 def pdf(self, x: Tensor) -> Tensor: @@ -138,6 +145,7 @@ def pdf(self, x: Tensor) -> Tensor: Returns: Tensor: the probability density function of the uniform noise. """ + return torch.exp(self.log_prob(x=x)) def log_prob(self, x: Tensor) -> Tensor: @@ -149,6 +157,7 @@ def log_prob(self, x: Tensor) -> Tensor: Returns: Tensor: the log probability density function of the uniform noise. """ + lb = self.low.le(x).type_as(self.low) ub = self.high.gt(x).type_as(self.low) return torch.log(lb.mul(ub)) - torch.log(self.high - self.low) @@ -162,6 +171,7 @@ def cdf(self, x: TENSOR_OPERABLE) -> TENSOR_OPERABLE: Returns: TENSOR_OPERABLE: the cumulative distribution function of the uniform noise. """ + result = (x - self.low) / (self.high - self.low) return result.clamp(min=0, max=1) @@ -174,6 +184,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return torch.distributions.Uniform(low=x + self.low, high=x + self.high).sample() def extra_repr(self) -> str: @@ -182,6 +193,7 @@ def extra_repr(self) -> str: Returns: str: the extra representation of the uniform noise. """ + return f'high={float(self.high):.4f}' \ f', low={float(self.low):.4f}' \ f', leakage={float(self.leakage):.4f}' \ diff --git a/analogvnn/nn/normalize/Clamp.py b/analogvnn/nn/normalize/Clamp.py index 7d96e56..7db26fc 100644 --- a/analogvnn/nn/normalize/Clamp.py +++ b/analogvnn/nn/normalize/Clamp.py @@ -21,6 +21,7 @@ def forward(x: Tensor): Returns: Tensor: the output tensor. """ + return torch.clamp(x, min=-1, max=1) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -32,6 +33,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = ((-1 <= x) * (x <= 1.)).type(torch.float) return grad_output * grad @@ -50,6 +52,7 @@ def forward(x: Tensor): Returns: Tensor: the output tensor. """ + return torch.clamp(x, min=0, max=1) def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: @@ -61,6 +64,7 @@ def backward(self, grad_output: Optional[Tensor]) -> Optional[Tensor]: Returns: Optional[Tensor]: the gradient of the input tensor. """ + x = self.inputs grad = ((0 <= x) * (x <= 1.)).type(torch.float) return grad_output * grad diff --git a/analogvnn/nn/normalize/LPNorm.py b/analogvnn/nn/normalize/LPNorm.py index 27bcce0..68cc2d5 100644 --- a/analogvnn/nn/normalize/LPNorm.py +++ b/analogvnn/nn/normalize/LPNorm.py @@ -25,6 +25,7 @@ def __init__(self, p: int, make_max_1=False): p (int): the pth power of the Lp norm. make_max_1 (bool): if True, the maximum absolute value of the output tensor will be 1. """ + super(LPNorm, self).__init__() self.p = nn.Parameter(torch.tensor(p), requires_grad=False) self.make_max_1 = nn.Parameter(torch.tensor(make_max_1), requires_grad=False) @@ -38,6 +39,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + norm = x if len(x.shape) > 1: norm = torch.flatten(norm, start_dim=1) @@ -64,6 +66,7 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + norm = torch.norm(x, self.p) norm = torch.clamp(norm, min=1e-4) x = torch.div(x, norm) @@ -79,6 +82,7 @@ class L1Norm(LPNorm): def __init__(self): """Initializes the row-wise L1 normalization function.""" + super(L1Norm, self).__init__(p=1, make_max_1=False) @@ -87,6 +91,7 @@ class L2Norm(LPNorm): def __init__(self): """Initializes the row-wise L2 normalization function.""" + super(L2Norm, self).__init__(p=2, make_max_1=False) @@ -95,6 +100,7 @@ class L1NormW(LPNormW): def __init__(self): """Initializes the whole matrix L1 normalization function.""" + super(L1NormW, self).__init__(p=1, make_max_1=False) @@ -103,6 +109,7 @@ class L2NormW(LPNormW): def __init__(self): """Initializes the whole matrix L2 normalization function.""" + super(L2NormW, self).__init__(p=2, make_max_1=False) @@ -111,6 +118,7 @@ class L1NormM(LPNorm): def __init__(self): """Initializes the row-wise L1 normalization function with maximum absolute value of 1.""" + super(L1NormM, self).__init__(p=1, make_max_1=True) @@ -119,6 +127,7 @@ class L2NormM(LPNorm): def __init__(self): """Initializes the row-wise L2 normalization function with maximum absolute value of 1.""" + super(L2NormM, self).__init__(p=2, make_max_1=True) @@ -127,6 +136,7 @@ class L1NormWM(LPNormW): def __init__(self): """Initializes the whole matrix L1 normalization function with maximum absolute value of 1.""" + super(L1NormWM, self).__init__(p=1, make_max_1=True) @@ -135,4 +145,5 @@ class L2NormWM(LPNormW): def __init__(self): """Initializes the whole matrix L2 normalization function with maximum absolute value of 1.""" + super(L2NormWM, self).__init__(p=2, make_max_1=True) diff --git a/analogvnn/nn/precision/ReducePrecision.py b/analogvnn/nn/precision/ReducePrecision.py index 3b5270e..207889a 100644 --- a/analogvnn/nn/precision/ReducePrecision.py +++ b/analogvnn/nn/precision/ReducePrecision.py @@ -30,6 +30,7 @@ def __init__(self, precision: int = None, divide: float = 0.5): divide (float): the rounding value that is if divide is 0.5, then 0.6 will be rounded to 1.0 and 0.4 will be rounded to 0.0. """ + super(ReducePrecision, self).__init__() if precision < 1: raise ValueError(f'precision has to be more than 0, but got {precision}') @@ -50,6 +51,7 @@ def precision_width(self) -> Tensor: Returns: Tensor: the precision width """ + return 1 / self.precision @property @@ -59,6 +61,7 @@ def bit_precision(self) -> Tensor: Returns: Tensor: the bit precision of the ReducePrecision module. """ + return torch.log2(self.precision + 1) @staticmethod @@ -71,6 +74,7 @@ def convert_to_precision(bit_precision: TENSOR_OPERABLE) -> TENSOR_OPERABLE: Returns: TENSOR_OPERABLE: the precision. """ + return 2 ** bit_precision - 1 def extra_repr(self) -> str: @@ -79,6 +83,7 @@ def extra_repr(self) -> str: Returns: str: string """ + return f'precision={int(self.precision)}, divide={float(self.divide):0.2f}' def forward(self, x: Tensor) -> Tensor: @@ -90,4 +95,5 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: the output tensor. """ + return reduce_precision(x, self.precision, self.divide) diff --git a/analogvnn/nn/precision/StochasticReducePrecision.py b/analogvnn/nn/precision/StochasticReducePrecision.py index 7e36ae9..dad532a 100644 --- a/analogvnn/nn/precision/StochasticReducePrecision.py +++ b/analogvnn/nn/precision/StochasticReducePrecision.py @@ -25,6 +25,7 @@ def __init__(self, precision: int = 8): Args: precision (int): the precision of the output tensor. """ + super(StochasticReducePrecision, self).__init__() if precision < 1: raise ValueError('precision has to be more than 0, but got {}'.format(precision)) @@ -41,6 +42,7 @@ def precision_width(self) -> Tensor: Returns: Tensor: the precision width """ + return 1 / self.precision @property @@ -50,6 +52,7 @@ def bit_precision(self) -> Tensor: Returns: Tensor: the bit precision of the ReducePrecision module. """ + return torch.log2(self.precision + 1) @staticmethod @@ -62,6 +65,7 @@ def convert_to_precision(bit_precision: TENSOR_OPERABLE) -> TENSOR_OPERABLE: Returns: TENSOR_OPERABLE: the precision. """ + return 2 ** bit_precision - 1 def extra_repr(self) -> str: @@ -70,6 +74,7 @@ def extra_repr(self) -> str: Returns: str: string """ + return f'precision={self.precision}' def forward(self, x: Tensor) -> Tensor: @@ -81,4 +86,5 @@ def forward(self, x: Tensor) -> Tensor: Returns: Tensor: output tensor. """ + return stochastic_reduce_precision(x, self.precision) diff --git a/analogvnn/parameter/Parameter.py b/analogvnn/parameter/Parameter.py index 54bdb2e..2999efe 100644 --- a/analogvnn/parameter/Parameter.py +++ b/analogvnn/parameter/Parameter.py @@ -21,6 +21,7 @@ def __new__(cls, data=None, requires_grad=True, *args, **kwargs): Returns: Parameter: the created parameter. """ + return super(Parameter, cls).__new__(cls, data, requires_grad) # noinspection PyUnusedLocal @@ -33,6 +34,7 @@ def __init__(self, data=None, requires_grad=True, *args, **kwargs): *args: additional arguments. **kwargs: additional keyword arguments. """ + super(Parameter, self).__init__() def __repr__(self, *args, **kwargs): @@ -45,4 +47,5 @@ def __repr__(self, *args, **kwargs): Returns: str: the string representation. """ + return super(Parameter, self).__repr__(*args, **kwargs) diff --git a/analogvnn/parameter/PseudoParameter.py b/analogvnn/parameter/PseudoParameter.py index 0d7b7bf..5b14945 100644 --- a/analogvnn/parameter/PseudoParameter.py +++ b/analogvnn/parameter/PseudoParameter.py @@ -31,6 +31,7 @@ def __init__(self, original, transformed): original (PseudoParameter): the original parameters. transformed (nn.Parameter): the transformed parameters. """ + super(PseudoParameterModule, self).__init__() self.original = original self._transformed = transformed @@ -45,6 +46,7 @@ def __call__(self, *args, **kwargs) -> nn.Parameter: Returns: nn.Parameter: The transformed parameter. """ + return self.original() forward = __call__ @@ -62,6 +64,7 @@ def set_original_data(self, data: Tensor) -> PseudoParameterModule: Returns: PseudoParameterModule: self. """ + self.original.data = data return self @@ -108,6 +111,7 @@ def identity(x: Any) -> Any: Returns: Any: the input tensor. """ + return x def __init__(self, data=None, requires_grad=True, transformation=None, *args, **kwargs): @@ -120,6 +124,7 @@ def __init__(self, data=None, requires_grad=True, transformation=None, *args, ** *args: additional arguments. **kwargs: additional keyword arguments. """ + super(PseudoParameter, self).__init__(data, requires_grad, *args, **kwargs) self._transformed = nn.Parameter(data=data, requires_grad=requires_grad) self._transformed.original = self @@ -144,6 +149,7 @@ def __call__(self, *args, **kwargs): Raises: RuntimeError: if the transformation callable fails. """ + try: self._transformed.data = self._transformation(self) except Exception as e: @@ -156,6 +162,7 @@ def __repr__(self): Returns: str: the string representation. """ + return f'{PseudoParameter.__name__}(' \ f'transform={self.transformation}' \ f', data={self.data}' \ @@ -168,6 +175,7 @@ def grad(self): Returns: Tensor: the gradient. """ + return self._transformed.grad @property @@ -177,6 +185,7 @@ def module(self): Returns: PseudoParameterModule: the module. """ + return self._module @property @@ -186,6 +195,7 @@ def transformation(self): Returns: Callable: the transformation. """ + return self._transformation @transformation.setter @@ -195,6 +205,7 @@ def transformation(self, transformation: Callable): Args: transformation (Callable): the transformation. """ + self.set_transformation(transformation) def set_transformation(self, transformation) -> PseudoParameter: @@ -206,6 +217,7 @@ def set_transformation(self, transformation) -> PseudoParameter: Returns: PseudoParameter: self. """ + self._transformation = transformation if isinstance(self._transformation, nn.Module): self._transformation.eval() @@ -223,6 +235,7 @@ def parameterize(cls, module: nn.Module, param_name: str, transformation: Callab Returns: PseudoParameter: the parameterized parameter. """ + assert hasattr(module, param_name) param = getattr(module, param_name) @@ -249,6 +262,7 @@ def parametrize_module(cls, module: nn.Module, transformation: Callable, require transformation (Callable): the transformation. requires_grad (bool): if True, only parametrized parameters that require gradients. """ + with torch.no_grad(): for name, parameter in list(module.named_parameters(recurse=False)): if isinstance(parameter, cls): diff --git a/analogvnn/utils/TensorboardModelLog.py b/analogvnn/utils/TensorboardModelLog.py index 13f08e1..5ee65aa 100644 --- a/analogvnn/utils/TensorboardModelLog.py +++ b/analogvnn/utils/TensorboardModelLog.py @@ -38,6 +38,7 @@ def __init__(self, model: Model, log_dir: str): model (nn.Module): the model to log. log_dir (str): the directory to log to. """ + super(TensorboardModelLog, self).__init__() self.model = model self.tensorboard = None @@ -63,6 +64,7 @@ def set_log_dir(self, log_dir: str) -> TensorboardModelLog: Raises: ValueError: if the log directory is invalid. """ + # https://github.com/tensorflow/tensorboard/pull/6135 from tensorboard.compat import tf if getattr(tf, 'io', None) is None: @@ -81,6 +83,7 @@ def _add_layer_data(self, epoch: int = None): Args: epoch (int): the epoch to add the data for. """ + for name, parameter in self.model.named_parameters(): if not parameter.requires_grad: continue @@ -93,6 +96,7 @@ def on_compile(self, layer_data: bool = True): Args: layer_data (bool): whether to log the layer data. """ + if self.layer_data: self.layer_data = layer_data @@ -116,6 +120,7 @@ def add_graph( Returns: TensorboardModelLog: self. """ + if model is None: model = self.model @@ -161,6 +166,7 @@ def add_summary( Raises: ImportError: if torchinfo (https://github.com/tyleryep/torchinfo) is not installed. """ + try: import torchinfo except ImportError as e: @@ -220,6 +226,7 @@ def register_training(self, epoch: int, train_loss: float, train_accuracy: float Returns: TensorboardModelLog: self. """ + self.tensorboard.add_scalar('Loss/train', train_loss, epoch) self.tensorboard.add_scalar('Accuracy/train', train_accuracy, epoch) if self.layer_data: @@ -237,6 +244,7 @@ def register_testing(self, epoch: int, test_loss: float, test_accuracy: float) - Returns: TensorboardModelLog: self. """ + self.tensorboard.add_scalar('Loss/test', test_loss, epoch) self.tensorboard.add_scalar('Accuracy/test', test_accuracy, epoch) return self @@ -249,6 +257,7 @@ def close(self, *args, **kwargs): *args: ignored. **kwargs: ignored. """ + if self.tensorboard is not None: self.tensorboard.close() self.tensorboard = None @@ -259,6 +268,7 @@ def __enter__(self): Returns: TensorboardModelLog: self. """ + return self __exit__ = close diff --git a/analogvnn/utils/is_cpu_cuda.py b/analogvnn/utils/is_cpu_cuda.py index 9c204fc..8d7d7c6 100644 --- a/analogvnn/utils/is_cpu_cuda.py +++ b/analogvnn/utils/is_cpu_cuda.py @@ -20,18 +20,31 @@ class CPUCuda: def __init__(self): """Initialize the CPUCuda class.""" + super(CPUCuda, self).__init__() self._device = None self.device_name = None - self.reset_device() + self.use_cpu() - def reset_device(self): - """Reset the device to the default device. + def use_cpu(self) -> CPUCuda: + """Use cpu. Returns: CPUCuda: self """ - self.set_device(f'cuda:{torch.cuda.current_device()}' if torch.cuda.is_available() else 'cpu') + + self.set_device('cpu') + return self + + def use_cuda_if_available(self) -> CPUCuda: + """Use cuda if available. + + Returns: + CPUCuda: self + """ + + if torch.cuda.is_available(): + self.set_device(f'cuda:{torch.cuda.current_device()}') return self def set_device(self, device_name: str) -> CPUCuda: @@ -43,27 +56,40 @@ def set_device(self, device_name: str) -> CPUCuda: Returns: CPUCuda: self """ + self._device = torch.device(device_name) self.device_name = self._device.type return self @property - def is_cuda(self) -> bool: - """Check if the device is cuda. + def device(self) -> torch.device: + """Get the device. Returns: - bool: True if the device is cuda, False otherwise. + torch.device: the device. + """ + + return self._device + + @property + def is_cpu(self) -> bool: + """Check if the device is cpu. + + Returns: + bool: True if the device is cpu, False otherwise. """ - return 'cuda' in self.device_name + + return self.device_name.startswith('cpu') @property - def device(self) -> torch.device: - """Get the device. + def is_cuda(self) -> bool: + """Check if the device is cuda. Returns: - torch.device: the device. + bool: True if the device is cuda, False otherwise. """ - return self._device + + return self.device_name.startswith('cuda') @property def is_using_cuda(self) -> Tuple[torch.device, bool]: @@ -72,6 +98,7 @@ def is_using_cuda(self) -> Tuple[torch.device, bool]: Returns: tuple: the device and True if the device is cuda, False otherwise. """ + return self.device, self.is_cuda def get_module_device(self, module) -> torch.device: @@ -83,6 +110,7 @@ def get_module_device(self, module) -> torch.device: Returns: torch.device: the device of the module. """ + # noinspection PyBroadException try: device: torch.device = getattr(module, 'device', None) diff --git a/analogvnn/utils/render_autograd_graph.py b/analogvnn/utils/render_autograd_graph.py index e6f9f65..d2edf16 100644 --- a/analogvnn/utils/render_autograd_graph.py +++ b/analogvnn/utils/render_autograd_graph.py @@ -49,6 +49,7 @@ def size_to_str(size): Returns: str: the string representation of the size. """ + return '(' + ', '.join(['%d' % s for s in size]) + ')' @@ -67,6 +68,7 @@ def resize_graph(dot: Digraph, size_per_element: float = 0.15, min_size: float = size_per_element**2 pixels. min_size (float): Minimum size of graph. """ + # Get the approximate number of nodes and edges num_rows = len(dot.body) content_size = num_rows * size_per_element @@ -86,6 +88,7 @@ def get_fn_name(fn: Callable, show_attrs: bool, max_attr_chars: int) -> str: Returns: str: the name of the function. """ + name = str(type(fn).__name__) if name.endswith('Backward'): name = name[:-8] @@ -159,6 +162,7 @@ def __post_init__(self): Raises: ImportError: if graphviz (https://pygraphviz.github.io/) is not available. """ + try: from graphviz import Digraph except ImportError as e: @@ -184,6 +188,7 @@ def inputs(self) -> Sequence[Tensor]: Returns: Sequence[Tensor]: the arg inputs to the module. """ + return self._inputs @inputs.setter @@ -193,6 +198,7 @@ def inputs(self, inputs: Union[Tensor, Sequence[Tensor]]): Args: inputs (Union[Tensor, Sequence[Tensor]]): the inputs to the module. """ + self._inputs = inputs self._called = False @@ -213,6 +219,7 @@ def inputs_kwargs(self) -> Dict[str, Tensor]: Args: Dict[str, Tensor]: the keyword inputs to the module. """ + return self._inputs_kwargs @inputs_kwargs.setter @@ -222,6 +229,7 @@ def inputs_kwargs(self, inputs_kwargs: Dict[str, Tensor]): Args: inputs_kwargs (Dict[str, Tensor]): the keyword inputs to the module. """ + self._inputs_kwargs = inputs_kwargs self._called = False @@ -239,6 +247,7 @@ def outputs(self) -> Optional[Sequence[Tensor]]: Returns: Optional[Sequence[Tensor]]: the outputs of the module. """ + return self._outputs @outputs.setter @@ -259,6 +268,7 @@ def module(self) -> nn.Module: Returns: nn.Module: the module to be traced. """ + return self._module @module.setter @@ -268,6 +278,7 @@ def module(self, module: nn.Module): Args: module (nn.Module): the module. """ + self._module = module self._called = False @@ -285,6 +296,7 @@ def reset_params(self): Returns: AutoGradDot: self. """ + self.param_map = {} self._seen = set() self.inputs = self.inputs @@ -299,6 +311,7 @@ def ignore_tensor(self) -> Dict[int, bool]: Returns: Dict[int, bool]: the ignore tensor dict. """ + return self._ignore_tensor def add_ignore_tensor(self, tensor: Tensor): @@ -310,6 +323,7 @@ def add_ignore_tensor(self, tensor: Tensor): Returns: AutoGradDot: self. """ + self._ignore_tensor[id(tensor)] = True return self @@ -322,6 +336,7 @@ def del_ignore_tensor(self, tensor: Tensor): Returns: AutoGradDot: self. """ + self._ignore_tensor.pop(id(tensor), None) return self @@ -335,6 +350,7 @@ def get_tensor_name(self, tensor: Tensor, name: Optional[str] = None) -> Tuple[s Returns: Tuple[str, str]: the name and size of the tensor. """ + if not name: if id(tensor) in self.param_map: name = self.param_map[id(tensor)] @@ -362,6 +378,7 @@ def add_tensor(self, tensor: Tensor, name: Optional[str] = None, _attributes=Non Returns: AutoGradDot: self. """ + self._seen.add(tensor) self.dot.node( name=str(id(tensor)), @@ -382,6 +399,7 @@ def add_fn(self, fn: Any, _attributes=None, **kwargs): Returns: AutoGradDot: self. """ + self._seen.add(fn) self.dot.node( name=str(id(fn)), @@ -404,6 +422,7 @@ def add_edge(self, u: Any, v: Any, label: Optional[str] = None, _attributes=None Returns: AutoGradDot: self. """ + self.dot.edge( tail_name=str(id(u)), head_name=str(id(v)), @@ -422,6 +441,7 @@ def add_seen(self, item: Any): Returns: AutoGradDot: self. """ + self._seen.add(item) return self @@ -434,6 +454,7 @@ def is_seen(self, item: Any) -> bool: Returns: bool: True if the item is in the seen set. """ + return item in self._seen @@ -444,6 +465,7 @@ def _add_grad_fn(link: Union[Tensor, Callable], autograd_dot: AutoGradDot) -> Op link (Union[Tensor, Callable]): the Tensor or Tensor.grad_fn. autograd_dot (AutoGradDot): the AutoGradDot object. """ + if autograd_dot.is_seen(link): return None @@ -558,6 +580,7 @@ def _compile_autograd_obj( Returns: AutoGradDot: graphviz representation of autograd graph """ + if LooseVersion(torch.__version__) < LooseVersion('1.9') and (show_attrs or show_saved): warnings.warn( 'make_dot: showing grad_fn attributes and saved variables' @@ -625,6 +648,7 @@ def make_autograd_obj_from_outputs( Returns: AutoGradDot: graphviz representation of autograd graph """ + autograd_dot = AutoGradDot() autograd_dot.outputs = outputs named_params = dict(named_params) @@ -665,6 +689,7 @@ def make_autograd_obj_from_module( Returns: AutoGradDot: graphviz representation of autograd graph """ + assert isinstance(module, nn.Module) new_args = [] new_kwargs = {} @@ -773,6 +798,7 @@ def get_autograd_dot_from_trace(trace) -> Digraph: Returns: graphviz.Digraph: the resulting graph. """ + try: from graphviz import Digraph except ImportError as e: @@ -835,6 +861,7 @@ def get_autograd_dot_from_outputs( Returns: Digraph: graphviz representation of autograd graph """ + return make_autograd_obj_from_outputs( outputs=outputs, named_params=named_params, @@ -874,6 +901,7 @@ def get_autograd_dot_from_module( Returns: Digraph: graphviz representation of autograd graph """ + return make_autograd_obj_from_module( module, *args, @@ -913,6 +941,7 @@ def save_autograd_graph_from_outputs( Returns: str: The (possibly relative) path of the rendered file. """ + return get_autograd_dot_from_outputs( outputs=outputs, named_params=named_params, @@ -954,6 +983,7 @@ def save_autograd_graph_from_module( Returns: str: The (possibly relative) path of the rendered file. """ + return get_autograd_dot_from_module( module, *args, @@ -976,4 +1006,5 @@ def save_autograd_graph_from_trace(filename: Union[str, Path], trace) -> str: Returns: str: The (possibly relative) path of the rendered file. """ + return get_autograd_dot_from_trace(trace).render(filename, format='svg', cleanup=True) diff --git a/analogvnn/utils/to_tensor_parameter.py b/analogvnn/utils/to_tensor_parameter.py index 3650688..7956a22 100644 --- a/analogvnn/utils/to_tensor_parameter.py +++ b/analogvnn/utils/to_tensor_parameter.py @@ -17,6 +17,7 @@ def to_float_tensor(*args) -> Tuple[Union[torch.Tensor, None], ...]: Returns: tuple: the converted arguments. """ + return tuple((None if i is None else torch.tensor(i, requires_grad=False, dtype=torch.float)) for i in args) @@ -31,4 +32,5 @@ def to_nongrad_parameter(*args) -> Tuple[Union[nn.Parameter, None], ...]: Returns: tuple: the converted arguments. """ + return tuple((None if i is None else nn.Parameter(i, requires_grad=False)) for i in to_float_tensor(*args)) diff --git a/docs/_static/AnalogVNN_Demo.ipynb b/docs/_static/AnalogVNN_Demo.ipynb index ee559f3..09adbe0 100644 --- a/docs/_static/AnalogVNN_Demo.ipynb +++ b/docs/_static/AnalogVNN_Demo.ipynb @@ -118,6 +118,7 @@ "print(f\"AnalogVNN version: {analogvnn.__version__}\")\n", "print(f\"PyTorch version: {torch.__version__}\")\n", "\n", + "is_cpu_cuda.use_cuda_if_available()\n", "torch.manual_seed(0)\n", "device, is_cuda = is_cpu_cuda.is_using_cuda\n", "print(f\"Device: {device}\")" diff --git a/docs/_static/analogvnn-logo-square-black.svg b/docs/_static/analogvnn-logo-square-black.svg index 6001b17..e71e98d 100644 --- a/docs/_static/analogvnn-logo-square-black.svg +++ b/docs/_static/analogvnn-logo-square-black.svg @@ -1,35 +1,39 @@ - - + + + + + + + + + + - \ No newline at end of file diff --git a/docs/_static/analogvnn-logo-square-white.svg b/docs/_static/analogvnn-logo-square-white.svg index 994efac..6621c76 100644 --- a/docs/_static/analogvnn-logo-square-white.svg +++ b/docs/_static/analogvnn-logo-square-white.svg @@ -1,35 +1,39 @@ - - + + + + + + + + + + - \ No newline at end of file diff --git a/docs/_static/analogvnn-logo-wide-black.svg b/docs/_static/analogvnn-logo-wide-black.svg index 8accb63..28187ea 100644 --- a/docs/_static/analogvnn-logo-wide-black.svg +++ b/docs/_static/analogvnn-logo-wide-black.svg @@ -1,48 +1,61 @@ - - + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/docs/_static/analogvnn-logo-wide-white.svg b/docs/_static/analogvnn-logo-wide-white.svg index 7c265ce..6f63f77 100644 --- a/docs/_static/analogvnn-logo-wide-white.svg +++ b/docs/_static/analogvnn-logo-wide-white.svg @@ -1,48 +1,61 @@ - - + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 70fc2c5..75425b7 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,3 @@ -.sidebar-brand-text{ +.sidebar-brand-text { display: none; } \ No newline at end of file diff --git a/docs/extra_classes.md b/docs/extra_classes.md index 4b89921..66300fa 100644 --- a/docs/extra_classes.md +++ b/docs/extra_classes.md @@ -93,6 +93,7 @@ p & : \ p \gt x ``` where: + - p, q ∈ ℜ (p ≤ q, Default value for photonics p = −1 and q = 1) ## Noise diff --git a/docs/inner_workings.md b/docs/inner_workings.md index ca2e1c9..0656556 100644 --- a/docs/inner_workings.md +++ b/docs/inner_workings.md @@ -15,13 +15,14 @@ the parameter of layer of `Parameter` class to `PseudoParameters`. layer to get parameterized data PyTorch's ParameterizedParameters vs AnalogVNN's PseudoParameters: + - Similarity (Forward or Parameterizing the data): > Data → ParameterizingModel → Parameterized Data - Difference (Backward or Gradient Calculations): - - ParameterizedParameters - > Parameterized Data → ParameterizingModel → Data - - PseudoParameters - > Parameterized Data → Data + - ParameterizedParameters + > Parameterized Data → ParameterizingModel → Data + - PseudoParameters + > Parameterized Data → Data So, by using `PseudoParameters` class the gradients of the parameter are calculated in such a way that the ParameterizingModel was never present. diff --git a/docs/install.md b/docs/install.md index 90d4f59..3d8e707 100644 --- a/docs/install.md +++ b/docs/install.md @@ -34,23 +34,23 @@ OR Install the required dependencies: - PyTorch - - Manual installation required: [https://pytorch.org/](https://pytorch.org/) + - Manual installation required: [https://pytorch.org/](https://pytorch.org/) - dataclasses - scipy - numpy - networkx - (optional) tensorboard - - For using tensorboard to visualize the network, with class - {py:class}`analogvnn.utils.TensorboardModelLog.TensorboardModelLog` + - For using tensorboard to visualize the network, with class + {py:class}`analogvnn.utils.TensorboardModelLog.TensorboardModelLog` - (optional) torchinfo - - For adding summary to tensorboard by using - {py:func}`analogvnn.utils.TensorboardModelLog.TensorboardModelLog.add_summary` + - For adding summary to tensorboard by using + {py:func}`analogvnn.utils.TensorboardModelLog.TensorboardModelLog.add_summary` - (optional) graphviz - - For saving and rendering forward and backward graphs using - {py:func}`analogvnn.graph.AcyclicDirectedGraph.AcyclicDirectedGraph.render` + - For saving and rendering forward and backward graphs using + {py:func}`analogvnn.graph.AcyclicDirectedGraph.AcyclicDirectedGraph.render` - (optional) python-graphviz - - For saving and rendering forward and backward graphs using - {py:func}`analogvnn.graph.AcyclicDirectedGraph.AcyclicDirectedGraph.render` + - For saving and rendering forward and backward graphs using + {py:func}`analogvnn.graph.AcyclicDirectedGraph.AcyclicDirectedGraph.render`

diff --git a/pyproject.toml b/pyproject.toml index 3cfcf69..51a3cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ name = "analogvnn" [project] # $ pip install analogvnn name = "analogvnn" -version = "1.0.0rc5" +version = "1.0.0rc6" description = "A fully modular framework for modeling and optimizing analog/photonic neural networks" # Optional readme = "README.md" requires-python = ">=3.7" @@ -60,7 +60,7 @@ dependencies = [ "scipy", "numpy>=1.16.5", "networkx", - "importlib_metadata", + "importlib-metadata<5.0.0,>=2.0.0; python_version < '3.8'", ] # List additional groups of dependencies here (e.g. development @@ -101,6 +101,9 @@ flake8 = [ "flake8-comprehensions", "flake8-executable", "flake8-coding", + "flake8-return", +# "flake8-noreturn; python_version >= '3.8'", + "flake8-deprecated", ] dev = [ "setuptools>=61.0.0", diff --git a/requirements-all.txt b/requirements-all.txt new file mode 100644 index 0000000..617af85 --- /dev/null +++ b/requirements-all.txt @@ -0,0 +1,4 @@ +-r requirements.txt +-r requirements/requirements-docs.txt +-r requirements/requirements-test.txt +-r requirements/requirements-dev.txt diff --git a/requirements.txt b/requirements.txt index 99fb2b1..464c65c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,12 +5,12 @@ torchaudio numpy scipy networkx -importlib_metadata +importlib-metadata<5.0.0,>=2.0.0; python_version < '3.8' # Full -tensorflow -tensorboard +tensorflow>=2.0.0 +tensorboard>=2.0.0 torchinfo pillow -# conda install python-graphviz graphviz +# conda install graphviz graphviz diff --git a/requirements-dev.txt b/requirements/requirements-dev.txt similarity index 100% rename from requirements-dev.txt rename to requirements/requirements-dev.txt diff --git a/requirements-docs.txt b/requirements/requirements-docs.txt similarity index 100% rename from requirements-docs.txt rename to requirements/requirements-docs.txt diff --git a/requirements-test.txt b/requirements/requirements-test.txt similarity index 59% rename from requirements-test.txt rename to requirements/requirements-test.txt index bba026e..54c409a 100644 --- a/requirements-test.txt +++ b/requirements/requirements-test.txt @@ -6,3 +6,6 @@ flake8-bugbear flake8-comprehensions flake8-executable flake8-coding +flake8-return +# flake8-noreturn>=1.0.1; python_version >= '3.8' +flake8-deprecated diff --git a/sample_code.py b/sample_code.py index 020437d..b4bc9c1 100644 --- a/sample_code.py +++ b/sample_code.py @@ -16,6 +16,7 @@ def load_vision_dataset(dataset, path, batch_size, is_cuda=False, grayscale=True): """ + Loads a vision dataset with optional grayscale conversion and CUDA support. Args: @@ -28,6 +29,7 @@ def load_vision_dataset(dataset, path, batch_size, is_cuda=False, grayscale=True Returns: A tuple containing the train and test data loaders, the input shape, and a tuple of class labels. """ + dataset_kwargs = { 'batch_size': batch_size, 'shuffle': True @@ -69,6 +71,7 @@ def cross_entropy_accuracy(output, target) -> float: Returns: float: accuracy from 0 to 1 """ + _, preds = torch.max(output.data, 1) correct = (preds == target).sum().item() return correct / len(output) @@ -89,6 +92,7 @@ def __init__(self, activation_class, norm_class, precision_class, precision, noi (i.e., the probability that the digital values transmitted and detected are different after passing through the analog channel). """ + super(LinearModel, self).__init__() self.activation_class = activation_class @@ -112,6 +116,7 @@ def add_layer(self, layer): Args: layer (BaseLayer): digital layer module """ + self.all_layers.append(self.norm_class()) self.all_layers.append(self.precision_class(precision=self.precision)) self.all_layers.append(self.noise_class(leakage=self.leakage, precision=self.precision)) @@ -137,6 +142,7 @@ def __init__(self, norm_class, precision_class, precision, noise_class, leakage) (i.e., the probability that the digital values transmitted and detected are different after passing through the analog channel). """ + super(WeightModel, self).__init__() self.all_layers = [] @@ -150,6 +156,8 @@ def __init__(self, norm_class, precision_class, precision, noise_class, leakage) def run_linear3_model(): """The main function to train photonics image classifier with 3 linear/dense nn for MNIST dataset.""" + + is_cpu_cuda.use_cuda_if_available() torch.backends.cudnn.benchmark = True torch.manual_seed(0) device, is_cuda = is_cpu_cuda.is_using_cuda @@ -183,15 +191,18 @@ def run_linear3_model(): leakage=0.5 ) + # Parametrizing Parameters of the Models + PseudoParameter.parametrize_module(nn_model, transformation=weight_model) + # Setting Model Parameters nn_model.loss_function = nn.CrossEntropyLoss() nn_model.accuracy_function = cross_entropy_accuracy + nn_model.optimizer = optim.Adam(params=nn_model.parameters()) + + # Compile Model nn_model.compile(device=device) weight_model.compile(device=device) - PseudoParameter.parametrize_module(nn_model, transformation=weight_model) - nn_model.optimizer = optim.Adam(params=nn_model.parameters()) - # Training print('Starting Training...') for epoch in range(10): diff --git a/sample_code_with_logs.py b/sample_code_with_logs.py index ea64f2b..acafa9c 100644 --- a/sample_code_with_logs.py +++ b/sample_code_with_logs.py @@ -32,6 +32,7 @@ def load_vision_dataset(dataset, path, batch_size, is_cuda=False, grayscale=True Returns: A tuple containing the train and test data loaders, the input shape, and a tuple of class labels. """ + dataset_kwargs = { 'batch_size': batch_size, 'shuffle': True @@ -73,6 +74,7 @@ def cross_entropy_accuracy(output, target) -> float: Returns: float: accuracy from 0 to 1 """ + _, preds = torch.max(output.data, 1) correct = (preds == target).sum().item() return correct / len(output) @@ -93,6 +95,7 @@ def __init__(self, activation_class, norm_class, precision_class, precision, noi (i.e., the probability that the digital values transmitted and detected are different after passing through the analog channel). """ + super(LinearModel, self).__init__() self.activation_class = activation_class @@ -116,6 +119,7 @@ def add_layer(self, layer): Args: layer (BaseLayer): digital layer module """ + self.all_layers.append(self.norm_class()) self.all_layers.append(self.precision_class(precision=self.precision)) self.all_layers.append(self.noise_class(leakage=self.leakage, precision=self.precision)) @@ -141,6 +145,7 @@ def __init__(self, norm_class, precision_class, precision, noise_class, leakage) (i.e., the probability that the digital values transmitted and detected are different after passing through the analog channel). """ + super(WeightModel, self).__init__() self.all_layers = [] @@ -154,6 +159,8 @@ def __init__(self, norm_class, precision_class, precision, noise_class, leakage) def run_linear3_model(): """The main function to train photonics image classifier with 3 linear/dense nn for MNIST dataset.""" + + is_cpu_cuda.use_cuda_if_available() torch.backends.cudnn.benchmark = True torch.manual_seed(0) data_path = Path('_data') @@ -191,15 +198,19 @@ def run_linear3_model(): leakage=0.5 ) + # Parametrizing Parameters of the Models + PseudoParameter.parametrize_module(nn_model, transformation=weight_model) + # Setting Model Parameters nn_model.loss_function = nn.CrossEntropyLoss() nn_model.accuracy_function = cross_entropy_accuracy + nn_model.optimizer = optim.Adam(params=nn_model.parameters()) + + # Compile Model nn_model.compile(device=device) weight_model.compile(device=device) - PseudoParameter.parametrize_module(nn_model, transformation=weight_model) - nn_model.optimizer = optim.Adam(params=nn_model.parameters()) - + # Creating Tensorboard for Visualization and Saving the Model nn_model.create_tensorboard(str(data_path.joinpath('tensorboard'))) print('Saving Summary and Graphs...') diff --git a/unit_tests/test_model_graphs.py b/unit_tests/test_model_graphs.py index 1d0d115..f695523 100644 --- a/unit_tests/test_model_graphs.py +++ b/unit_tests/test_model_graphs.py @@ -12,15 +12,19 @@ l1 = nn.Linear(1, 1, bias=False) l1.weight.data = torch.ones_like(l1.weight.data) * 2 + def l2(*x): return torch.add(*x), torch.sub(*x) + def l3(x, y): return {"a": torch.sub(x, y), "b": torch.add(x, y)} + def l4(x, y, z, a, b): return ((x + y) + (a + b)) + z + def l5(x): return {"c": x * 0.5} diff --git a/unit_tests/test_pseudo_parameter.py b/unit_tests/test_pseudo_parameter.py index 6291243..726d5ef 100644 --- a/unit_tests/test_pseudo_parameter.py +++ b/unit_tests/test_pseudo_parameter.py @@ -43,13 +43,16 @@ def __init__(self): def forward(self, x): return x + (torch.ones_like(x) * self.weight) + class Symmetric(BackwardIdentity, Model): def forward(self, x): return torch.rand((1, x.size()[0])) @ x @ torch.rand((x.size()[1], 1)) + def pstr(s): return str(s).replace(" ", "").replace("\n", "") + model = Layer() parametrization = Symmetric() # parametrization.eval()