To extend the LightOnML API, follow the guide to extend scikit-learn.
LightOnML uses their same API for all objects, with methods
Writing custom Encoders and Decoders for sklearn¶
THe OPU accepts data in binary format, i.e. as arrays of zeros and ones, therefore we need to convert the data we want to treat
in a format compatible with the OPU. This operation is called encoding.
A selection of encoders is provided in
lightonml.encoding.base, but it’s possible to write and use new ones.
Following the guide to extend
sklearn, an encoder has the methods
transform in order to follow the scikit-learn Transformer interface.
It should accept an
np.ndarray of any shape and any type and return an 2D
np.ndarray of zeros and ones and
For example we can write an encoder that separates the bitplans of
uint8 elements and passes each bitplan to the OPU.
Remark: the following implementation shouldn’t be used in your code, because error handling has been removed for clarity.
class SeparatedBitPlanEncoder: def __init__(self, n_bits=8, starting_bit=0): self.n_bits = n_bits self.starting_bit = starting_bit def fit(self, X, y=None): # no-op: we don't need to fit anything for this encoder return self def transform(self, X): bitwidth = X.dtype.itemsize*8 n_samples, n_features = X.shape # add a dimension [n_samples, n_features, 1] and returns a view of the data as uint8 X_uint8 = np.expand_dims(X, axis=2).view(np.uint8) # Unpacks the bits along the auxiliary axis X_uint8_unpacked = np.unpackbits(X_uint8, axis=2) # Reverse the order of bits: from LSB to MSB X_uint8_reversed = np.flip(X_uint8_unpacked, axis=2) # Transpose and reshape to 2D X_enc = np.transpose(X_uint8_reversed, [0, 2, 1]) X_enc = X_enc[:, self.starting_bit:self.n_bits + self.starting_bit, :] X_enc = X_enc.reshape((n_samples * self.n_bits, n_features)) return X_enc
The class attributes are assigned in the
__init__ method and
transform performs a series of transformation on the input array
until it returns a 2D
uint8 containing only zeros and ones.
When designing encoders one should keep in mind that there is a trade-off between fine-grained resolution and performance. Models generally don’t need high resolution, a coarse representation can be sufficient and even act as a regularizer. For example, the last bitplan of RGB images is often just noise.
Some encoders just transform the input data to a binary format (e.g.
BinaryThresholdEncoder), some others, like
SeparatedBitPlanEncoder, need a decoding step after the data have been transformed by the OPU.
- Custom decoders can be created following the same steps: implementation of
As an example, we write the code for the
class MixingBitPlanDecoder: def __init__(self, n_bits=8, decoding_decay=0.5): self.n_bits = n_bits self.decoding_decay = decoding_decay def fit(self, X, y=None): # no-op: we don't need to fit anything for this decoder return self def transform(self, X, y=None): n_out, n_features = X.shape n_dim_0 = int(n_out / self.n_bits) X = np.reshape(X, (n_dim_0, self.n_bits, n_features)) # compute factors for each bit to weight their significance decay_factors = np.reshape(self.decoding_decay ** np.arange(self.n_bits), self.n_bits) X_dec = np.einsum('ijk,j->ik', X, decay_factors).astype('float32') return X_dec
Again, the class attributes are defined in the
__init__ call and
transform performs a series of operation in the input
vector until it returns an