"""Neural network layer classes."""
import itertools
import numpy as np
[docs]class Layer:
"""
Base layer class.
Parameters
----------
input_size : tuple
size of the layer input
output_size : tuple
size of the layer output
activation : str or None
activation function name
input_index_mapper : IndexMapper or None
map indexes from this layer index to the input layer index size
"""
def __init__(self, input_size, output_size, *, activation=None, input_index_mapper=None):
assert isinstance(input_size, list)
assert isinstance(output_size, list)
if activation is None:
activation = "linear"
self.__input_size = input_size
self.__output_size = output_size
self.__activation = activation
self.__input_index_mapper = input_index_mapper
@property
def input_size(self):
"""Return the size of the input tensor"""
return self.__input_size
@property
def output_size(self):
"""Return the size of the output tensor"""
return self.__output_size
@property
def activation(self):
"""Return the activation function"""
return self.__activation
@property
def input_index_mapper(self):
"""Return the index mapper"""
return self.__input_index_mapper
@property
def input_indexes_with_input_layer_indexes(self):
"""
Return an iterator generating a tuple of local and input indexes.
Local indexes are indexes over the elements of the current layer.
Input indexes are indexes over the elements of the previous layer.
"""
if self.__input_index_mapper is None:
for index in self.input_indexes:
yield index, index
else:
mapper = self.__input_index_mapper
for index in self.input_indexes:
yield index, mapper(index)
@property
def input_indexes(self):
"""Return a list of the input indexes"""
return list(itertools.product(*[range(v) for v in self.__input_size]))
@property
def output_indexes(self):
"""Return a list of the output indexes"""
return list(itertools.product(*[range(v) for v in self.__output_size]))
[docs] def eval(self, x):
"""
Evaluate the layer at x.
Parameters
----------
x : array-like
the input tensor. Must have size `self.input_size`.
"""
if self.__input_index_mapper is not None:
x = np.reshape(x, self.__input_index_mapper.output_size)
assert x.shape == tuple(self.input_size)
y = self._eval(x)
return self._apply_activation(y)
def __repr__(self):
return f"<{str(self)} at {hex(id(self))}>"
def _eval(self, x):
raise NotImplementedError()
def _apply_activation(self, x):
if self.__activation == "linear" or self.__activation is None:
return x
elif self.__activation == "relu":
return np.maximum(x, 0)
elif self.__activation == "sigmoid":
return 1.0 / (1.0 + np.exp(-x))
elif self.__activation == 'tanh':
return np.tanh(x)
else:
raise ValueError(f"Unknown activation function {self.__activation}")
[docs]class DenseLayer(Layer):
"""
Dense layer implementing `output = activation(dot(input, weights) + biases)`.
Parameters
----------
input_size : tuple
the size of the input.
output_size : tuple
the size of the output.
weight : matrix-like
the weight matrix.
biases : array-like
the biases.
activation : str or None
activation function name
input_index_mapper : IndexMapper or None
map indexes from this layer index to the input layer index size
"""
def __init__(self, input_size, output_size, weights, biases, *, activation=None, input_index_mapper=None):
super().__init__(input_size, output_size, activation=activation, input_index_mapper=input_index_mapper)
self.__weights = weights
self.__biases = biases
@property
def weights(self):
return self.__weights
@property
def biases(self):
return self.__biases
def __str__(self):
return f"DenseLayer(input_size={self.input_size}, output_size={self.output_size})"
def _eval(self, x):
y = np.dot(x, self.__weights) + self.__biases
y = np.reshape(y, tuple(self.output_size))
assert y.shape == tuple(self.output_size)
return y
[docs]class ConvLayer(Layer):
"""
Two-dimensional convolutional layer.
Parameters
----------
input_size : tuple
the size of the input.
output_size : tuple
the size of the output.
strides : matrix-like
stride of the cross-correlation kernel.
kernel : matrix-like
the cross-correlation kernel.
activation : str or None
activation function name
input_index_mapper : IndexMapper or None
map indexes from this layer index to the input layer index size
"""
def __init__(self, input_size, output_size, strides, kernel, *, activation=None, input_index_mapper=None):
super().__init__(input_size, output_size, activation=activation, input_index_mapper=input_index_mapper)
self.__strides = strides
self.__kernel = kernel
@property
def strides(self):
return self.__strides
@property
def kernel_shape(self):
return self.__kernel.shape[2:]
@property
def kernel(self):
return self.__kernel
def __str__(self):
return f"ConvLayer(input_size={self.input_size}, output_size={self.output_size}, strides={self.strides}, kernel_shape={self.kernel_shape})"
def _eval(self, x):
y = np.empty(shape=self.output_size)
assert len(self.output_size) == 3
[depth, rows, cols] = self.output_size
for out_d in range(depth):
for out_r in range(rows):
for out_c in range(cols):
acc = 0.0
for (k, index) in self.kernel_with_input_indexes(out_d, out_r, out_c):
acc += k * x[index]
y[out_d, out_r, out_c] = acc
return y
[docs]class IndexMapper:
"""
Map indexes from one layer to the other.
Parameters
----------
input_size : tuple
the input size
output_size : tuple
the mapped input layer's output size
"""
def __init__(self, input_size, output_size):
self.__input_size = input_size
self.__output_size = output_size
@property
def input_size(self):
return self.__input_size
@property
def output_size(self):
return self.__output_size
def __call__(self, index):
flat_index = np.ravel_multi_index(index, self.__output_size)
return np.unravel_index(flat_index, self.__input_size)
def __str__(self):
return f"IndexMapper(input_size={self.input_size}, output_size={self.output_size})"