def opu_tutorial(opu: OPU): # Rule is that OPU input is binary, so input vector elements should be 0 or 1 # To get your input as binary, see the encoders from lightonml package. # The tutorial makes use of NumPy arrays, but you can use PyTorch tensors as input. # The output then is a tensor as well. # The dimension of output is governed by the n_components attribute: print("Number of components:", opu.n_components) # Simplest input is a batch of N 1d vectors, of arbitrary size inp_1d = random_bin((2000, 1000)) out1 = opu.fit_transform1d(inp_1d) print("1D transform out shape", out1.shape) # (2000, opu.n_components) # In this example we fit the OPU with the input and then transform or linear_transform, # but if we have several batches to be transformed, we must call fit first # on one the batches, and then transform: inp_1d_b = random_bin((2000, 1000)) opu.fit1d(inp_1d) out1 = opu.transform(inp_1d) out1_b = opu.transform(inp_1d_b) print("transform of two separate batches", out1.shape, out1_b.shape) # For a linear transform, use OPU.linear_transform with the same parameters: out1_l = opu.linear_transform(inp_1d) print("linear_transform of a batch", out1.shape, out1_l.shape) # You can also run fit with the number of features, instead: opu.fit1d(n_features=inp_1d.shape[1]) out1_alt = opu.transform(inp_1d) print("transform 1d", out1_alt.shape) # But you can transform a single vector if you wish single = opu.fit_transform1d(inp_1d[0]) print("single transform out shape", single.shape) # n_components # However if you have many single vectors, you will need either to use the # online mode (see below), or have them transformed in a single batch, # you will gain a lot of performance # if you have 2d vectors, you'll benefit from the fact that OPU input is # physically in 2d, so don't reshape them! Instead, use fit_transform2d: inp_2d = random_bin((2000, 13, 10)) # 200 vectors of shape 13x10 out2 = opu.fit_transform2d(inp_2d) print("2D transform out shape", out2.shape) # (2000, opu.n_components) # If you're batch is 2D-shaped, transform will recognize it and return # an output of the same shape: inp_3d = random_bin((300, 100, 3000)) # call 1D transform on a 3D vector out_3d = opu.fit_transform1d(inp_3d) # batch of 300x100 1D vectors print("3D transform out shape", out_3d.shape) # (300, 100, opu.n_components) # You can also have bit-packed input, which will optimize memory-space, # as well as being a bit more efficient inp_1d_p = np.packbits(inp_1d, axis=1) print("1D packed transform in shape", inp_1d_p.shape) # (2000, 1000/8) out1_p = opu.fit_transform1d(inp_1d_p, packed=True) print("1D packed transform out shape", out1_p.shape) # (2000, opu.n_components) # If your input is 2d AND bit-packed, you must tell what shape it is inp_2d_p = np.packbits(inp_2d.reshape(2000, -1), axis=1) print("1D packed transform in shape", inp_2d_p.shape) # (2000, 1300/8) out2_p = opu.fit_transform2d(inp_2d_p, packed=True, n_2d_features=(13, 10)) print("2D packed transform out shape", out2_p.shape) # (2000, opu.n_components) # Input is formatted to match OPU's input device, but you can pass directly # formatted input of the correct input size (should it be 1d or 2d) input_shape = tuple(opu.device.input_shape) single_raw_2d = random_bin(input_shape) single_raw_1d = np.reshape(single_raw_2d, -1) out_raw_1d = opu.fit_transform1d(single_raw_1d) print("1D raw out shape", out_raw_1d.shape) # (2000, opu.n_components) out_raw_2d = opu.fit_transform2d(single_raw_2d) print("1D raw out shape", out_raw_2d.shape) # (2000, opu.n_components) # Of course, the equivalent for packed input of input device's size # will work the same way # It can also be a batch of them many_raw_2d = random_bin((100, ) + input_shape) out_many_raw_2d = opu.fit_transform2d(many_raw_2d) print("Many raw out shape", out_many_raw_2d.shape) # (100, opu.n_components) # The online mode allows you to run accelerate the run of single vectors: n_features1d = 1200 opu.fit1d(n_features=n_features1d, online=True) for _ in range(10): online_out = opu.transform(random_bin(n_features1d)) print("Online out shape", online_out.shape) n_features2d = (50, 50) opu.fit2d(n_features=n_features2d, online=True) for _ in range(10): online_out = opu.transform(random_bin(n_features2d)) print("Online out shape", online_out.shape)
class OPUMap(BaseEstimator, TransformerMixin): """Adapter of the OPU to scikit-learn. Transform method is mapped to `transform <lightonml.opu.OPU.transform>` of the `OPU <lightonml.opu.OPU>` class. .. seealso:: `lightonml.opu.OPU` Parameters ---------- n_components: int, dimensionality of the target projection space. opu : lightonml.opu.OPU, optical processing unit instance (created at init if not provided) ndims : int, number of dimensions of an input. Can be 1 or 2. if ndims is 1, transform accepts 1d vector or batch of 1d vectors. if ndims is 2, transform accepts 2d vector or batch of 2d vectors. packed: bool, optional whether the input data is in bit-packed representation if packed is True and ndims is 2, each input vector is assumed to be a 1d array, and the "real" number of features must be provided using n_2d_features parameter defaults to False n_2d_features: list(int) or tuple(int) or np.ndarray (optional) number of 2d features if the input is packed simulated: bool, default False, use real or simulated OPU linear: bool, default False, use the linear version of the OPU transform (lightonml.opu.OPU.linear_transform) max_n_features: int, optional maximum number of binary features that the OPU will transform used only if simulated=True, in order to initiate the random matrix verbose_level: int, optional Levels are 0: nothing, 1: print info, 2: debug info, 3: trace info deprecated, use lightonml.set_verbose_level instead Attributes ---------- opu : lightonml.opu.OPU, optical processing unit instance n_components : int, dimensionality of the target projection space. ndims : int, number of dimensions of an input. Can be 1 or 2. if ndims is 1, transform accepts 1d vector or batch of 1d vectors. if ndims is 2, transform accepts 2d vector or batch of 2d vectors. packed: bool, optional whether the input data is in bit-packed representation if packed is True and ndims is 2, each input vector is assumed to be a 1d array, and the "real" number of features must be provided using n_2d_features parameter defaults to False n_2d_features: list(int) or tuple(int) or np.ndarray (optional) number of 2d features if the input is packed simulated: bool, default False, use real or simulated OPU linear: bool, default False, use the linear version of the OPU transform (lightonml.opu.OPU.linear_transform) max_n_features: int, optional maximum number of binary features that the OPU will transform used only if simulated=True, in order to initiate the random matrix """ def __init__(self, n_components, opu=None, ndims=1, n_2d_features=None, packed=False, simulated=False, max_n_features=None, verbose_level=-1, linear=False): # verbose_level shouldn't be used anymore, but put it as attributes # in order to comply with sklearn estimator if verbose_level >= 0: lightonml.set_verbose_level(verbose_level) self.verbose_level = lightonml.get_verbose_level() if opu is None: if simulated: simulated_opu_device = SimulatedOpuDevice() if max_n_features is None: raise ValueError( "When using simulated=True, you need to provide max_n_features." ) self.opu = OPU(opu_device=simulated_opu_device, max_n_features=max_n_features, n_components=n_components) else: self.opu = OPU(n_components=n_components) else: self.opu = opu self.opu.n_components = n_components if simulated and not isinstance(opu.device, SimulatedOpuDevice): warnings.warn( "You provided a real OPU object but set simulated=True." " Will use the real OPU.") if isinstance(opu.device, SimulatedOpuDevice) and not simulated: warnings.warn( "You provided a simulated OPU object but set simulated=False." " Will use simulated OPU.") if ndims not in [1, 2]: raise ValueError("Number of input dimensions must be 1 or 2") self.ndims = ndims self.n_2d_features = n_2d_features self.packed = packed self.simulated = simulated self.linear = linear self.max_n_features = max_n_features self.fitted = False @property def n_components(self): return self.opu.n_components @n_components.setter def n_components(self, value): self.opu.n_components = value def fit(self, X=None, y=None, n_features=None, packed=False, online=False): """ Configure OPU transform for 1d or 2d vectors The function can be either called with input vector, for fitting OPU parameters to it, or just vector dimensions, with `n_features`. When input is bit-packed the packed flag must be set to True. When input vectors must be transformed one by one, performance will be improved with the online flag set to True. Parameters ---------- X: np.ndarray, Fit will be made on this vector to optimize transform parameters y: np.ndarray, For sklearn interface compatibility n_features: int or tuple(int), Number of features for the input, necessary if X parameter isn't provided packed: bool, optional Set to true if the input vectors will be already bit-packed defaults to False online: bool, optional Set to true if the transforms will be made one vector after the other defaults to False Returns ------- self """ if self.ndims == 1: self.opu.fit1d(X, n_features=n_features, packed=packed, online=online) elif self.ndims == 2: self.opu.fit2d(X, n_features=n_features, packed=packed, online=online) else: assert False, "SklearnOPU.ndims={}; expected 1 or 2.".format( self.ndims) self.fitted = True return self def transform(self, X, y=None): """Performs the nonlinear random projections. .. seealso:: `lightonml.opu.OPU.transform` """ if self.opu.n_components != self.n_components: self.opu.n_components = self.n_components if not self.fitted: print( "OPUMap was not fit to data. Performing fit on the input with default parameters..." ) self.fit(X) transform = self.opu.linear_transform if self.linear else self.opu.transform return np.array(transform(X)) def open(self): self.opu.open() def close(self): self.opu.close() def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_value, exc_traceback): self.close()
class OPUMap(nn.Module): """Adapter of the OPU to the Pytorch interface. Forward method is mapped to `transform <lightonml.opu.OPU.transform>` of the `OPU <lightonml.opu.OPU>` class, depending on `ndims` parameter at the construction. .. seealso:: `lightonml.opu.OPU` Parameters ---------- n_components: int, dimensionality of the target projection space. opu : lightonml.opu.OPU, optical processing unit instance ndims : int, number of dimensions of an input. Can be 1, 2 or 3. if ndims is 1, transform accepts 1d vector or batch of 1d vectors. if ndims is 2, transform accepts 2d vector or batch of 2d vectors. packed: bool, optional whether the input data is in bit-packed representation if packed is True and ndims is 2, each input vector is assumed to be a 1d array, and the "real" number of features must be provided using n_2d_features parameter defaults to False n_2d_features: list(int) or tuple(int) or np.ndarray (optional) number of 2d features if the input is packed simulated: bool, default False, use real or simulated OPU max_n_features: int, optional maximum number of binary features that the OPU will transform used only if simulated=True, in order to initiate the random matrix verbose_level: int, optional Levels are 0: nothing, 1: print info, 2: debug info, 3: trace info deprecated, use lightonml.set_verbose_level instead Attributes ---------- opu : lightonml.opu.OPU, optical processing unit instance n_components : int, dimensionality of the target projection space. ndims : int, number of dimensions of an input. Can be 1, 2 or 3. if ndims is 1, transform accepts 1d vector or batch of 1d vectors. if ndims is 2, transform accepts 2d vector or batch of 2d vectors. packed: bool, optional whether the input data is in bit-packed representation if packed is True and ndims is 2, each input vector is assumed to be a 1d array, and the "real" number of features must be provided using n_2d_features parameter defaults to False n_2d_features: list(int) or tuple(int) or np.ndarray (optional) number of 2d features if the input is packed simulated: bool, default False, use real or simulated OPU max_n_features: int, optional maximum number of binary features that the OPU will transform used only if simulated=True, in order to initiate the random matrix fitted: bool if the OPU parameters have already been chosen. """ def __init__(self, n_components, opu=None, ndims=1, n_2d_features=None, packed=False, simulated=False, max_n_features=None, verbose_level=-1): if verbose_level >= 0: lightonml.set_verbose_level(verbose_level) self.verbose_level = lightonml.get_verbose_level() super(OPUMap, self).__init__() if opu is None: if simulated: simulated_opu = SimulatedOpuDevice() if max_n_features is None: raise ValueError("When using simulated=True, you need to provide max_n_features.") self.opu = OPU(opu_device=simulated_opu, max_n_features=max_n_features, n_components=n_components) else: self.opu = OPU(n_components=n_components) else: self.opu = opu self.opu.n_components = n_components if simulated and not isinstance(opu.device, SimulatedOpuDevice): warnings.warn("You provided a real OPU object but set simulated=True." " Will use the real OPU.") if isinstance(opu.device, SimulatedOpuDevice) and not simulated: warnings.warn("You provided a simulated OPU object but set simulated=False. " "Will use simulated OPU.") self.n_components = self.opu.n_components if ndims not in [1, 2]: raise ValueError("Number of input dimensions must be 1 or 2") self.ndims = ndims self.n_2d_features = n_2d_features self.packed = packed self.simulated = simulated self.max_n_features = max_n_features self.fitted = False self.online = False if lightonml.get_verbose_level() >= 1: print("OPU output is detached from the computational graph.") @property def n_components(self): return self.opu.n_components @n_components.setter def n_components(self, value): self.opu.n_components = value def forward(self, input): """Performs the nonlinear random projections. .. seealso:: `lightonml.opu.OPU.transform` """ if not self.fitted: print("OPUMap was not fit to data. Performing fit on the first batch with default parameters...") self.fit(input) if self.online: output = torch.empty((len(input), self.n_components), dtype=torch.uint8) for i in range(len(input)): output[i] = self.opu.transform(input[i]) return output.detach() else: output = self.opu.transform(input) return output.detach() def reset_parameters(self, input, y, n_features, packed, online): if online: self.online = True if self.ndims == 1: self.opu.fit1d(input, n_features=n_features, packed=packed, online=self.online) elif self.ndims == 2: self.opu.fit2d(input, n_features=n_features, packed=packed, online=self.online) else: assert False, "OPUMap.ndims={}; expected 1 or 2.".format(self.ndims) self.fitted = True return def fit(self, X=None, y=None, n_features=None, packed=False, online=False): """Configure OPU transform for 1d or 2d vectors The function can be either called with input vector, for fitting OPU parameters to it, or just vector dimensions, with `n_features`. When input is bit-packed the packed flag must be set to True. When input vectors must be transformed one by one, performance will be improved with the online flag set to True. Parameters ---------- X: np.ndarray or torch.Tensor, optional, Fit will be made on this vector to optimize transform parameters y: np.ndarray or torch.Tensor, optional, For consistence with Sklearn API. n_features: int or tuple(int) Number of features for the input, necessary if X parameter isn't provided packed: bool Set to true if the input vectors will be already bit-packed online: bool, optional Set to true if the transforms will be made one vector after the other defaults to False .. seealso:: `lightonml.opu.OPU.fit1d` .. seealso:: `lightonml.opu.OPU.fit2d` """ return self.reset_parameters(X, y, n_features, packed, online) def extra_repr(self): return 'out_features={}, n_dims={}, packed={} simulated={}'.format( self.n_components, self.n_dims, self.packed, self.simulated ) def open(self): self.opu.open() def close(self): self.opu.close() def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_value, exc_traceback): self.close()