Newer
Older
import numpy as np
from .layer import Layer
from .weights import Weights
def checkDims(input: np.ndarray) -> None:
assert input.ndim == 2, f"Input input should have 2 dimensions, got {input.ndim}"
batchsize, numFeatures = input.shape
assert batchsize > 0 and numFeatures > 0, "All dimensions should be greater than 0"
from abc import ABC, abstractmethod
from .weights import Weights
import numpy as np
from numpy.typing import ArrayLike
class Layer(ABC):
this is an abstract class and can only be used indirectly through inherited classes
__slots__ = ['name', 'mode', 'layerID']
id = 0
def __init__(self) -> None:
self.name = self.__class__.__name__
self.mode = ''
self.layerID = Layer.id
Layer.id += 1
@property
def qualifiedName(self) -> tuple:
return self.__class__.__module__, self.__class__.__name__
@abstractmethod
def forward(self, input: ArrayLike) -> np.ndarray:
"""
it's an abstract method, thus forcing the coder to implement it in daughter classes
"""
pass
def __call__(self, *args: ArrayLike) -> np.ndarray:
"""
this is used to make layers behave more like functions
"""
return self.forward(*args)
@abstractmethod
def backward(self, gradient: ArrayLike) -> np.ndarray:
it's an abstract method, thus forcing the coder to implement it in daughter classes
used to put layer in to training mode
meaning unfreezes parameters
self.mode = 'train'
def eval(self) -> None:
used to put layer in to evaluation mode
meaning freezes parameters
def __str__(self) -> str:
"""
used for print the layer in a human readable manner
"""
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class Flatten(Layer):
"""
This layer flattens any given input, the purpose is to use it after a
convolution block, in order squeeze all channels into one and prepare
the input for use in a linear layer
"""
__slots__ = ['inputShape', 'flatShape']
def __init__(self) -> None:
super().__init__()
self.inputShape = None
self.flatShape = None
def forward(self, input: np.ndarray) -> np.ndarray:
"""
flattens input into a 1D array, according to batchsize
"""
if self.inputShape is None:
self.inputShape = input.shape[1:]
self.flatShape = np.prod(self.inputShape)
return input.reshape(-1, self.flatShape)
def backward(self, gradient: np.ndarray) -> np.ndarray:
"""
unflattens upstream gradient into original input
"""
return gradient.reshape(-1, *self.inputShape)
class Dropout(Layer):
"""
dropout layer randomly zeros neurons during forward pass
and masks the gradient accordingly on the backward pass
this is used to prevent overfitting
"""
__slots__ = ['size', 'probability', 'mask']
def __init__(self, size: int, probability: float) -> None:
super().__init__()
self.size = size
if probability < 0 or probability > 1:
raise ValueError('probability has to be between 0 and 1')
self.probability = probability
def forward(self, input: np.ndarray) -> np.ndarray:
"""
masking input from a linear layer
"""
checkDims(input)
if self.mode == 'train':
self.mask = np.random.random(input.shape) < (1 - self.probability)
return np.multiply(input, self.mask) / (1 - self.probability)
else:
return input
def backward(self, gradient: np.ndarray) -> np.ndarray:
"""
# masking gradient from a linear layer
"""
return np.multiply(gradient, self.mask) / (1 - self.probability)
def __str__(self) -> str:
"""
used for print the layer in a human readable manner
"""
printString = self.name
printString += ' size: ' + str(self.size)
printString += ' probability: ' + str(self.probability)
return printString