Coverage for mlair/model_modules/advanced_paddings.py: 71%
129 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-12-02 15:24 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-12-02 15:24 +0000
1"""Collection of customised padding layers."""
3__author__ = 'Felix Kleinert'
4__date__ = '2020-03-02'
7from typing import Union, Tuple
9import numpy as np
10import tensorflow as tf
11# from tensorflow.keras.backend.common import normalize_data_format
12from tensorflow.keras.layers import ZeroPadding2D
13# from tensorflow.keras.layers.convolutional import _ZeroPadding
14from tensorflow.keras.layers import Layer
15# from tensorflow.keras.legacy import interfaces
16from mlair.keras_legacy import interfaces
17# from tensorflow.keras.utils import conv_utils
18from mlair.keras_legacy import conv_utils
19# from tensorflow.keras.utils.generic_utils import transpose_shape
20# from mlair.keras_legacy.generic_utils import transpose_shape
23""" TAKEN FROM KERAS 2.2.0 """
24def transpose_shape(shape, target_format, spatial_axes):
25 """Converts a tuple or a list to the correct `data_format`.
26 It does so by switching the positions of its elements.
27 # Arguments
28 shape: Tuple or list, often representing shape,
29 corresponding to `'channels_last'`.
30 target_format: A string, either `'channels_first'` or `'channels_last'`.
31 spatial_axes: A tuple of integers.
32 Correspond to the indexes of the spatial axes.
33 For example, if you pass a shape
34 representing (batch_size, timesteps, rows, cols, channels),
35 then `spatial_axes=(2, 3)`.
36 # Returns
37 A tuple or list, with the elements permuted according
38 to `target_format`.
39 # Example
40 ```python
41 >>> # from keras.utils.generic_utils import transpose_shape
42 >>> transpose_shape((16, 128, 128, 32),'channels_first', spatial_axes=(1, 2))
43 (16, 32, 128, 128)
44 >>> transpose_shape((16, 128, 128, 32), 'channels_last', spatial_axes=(1, 2))
45 (16, 128, 128, 32)
46 >>> transpose_shape((128, 128, 32), 'channels_first', spatial_axes=(0, 1))
47 (32, 128, 128)
48 ```
49 # Raises
50 ValueError: if `value` or the global `data_format` invalid.
51 """
52 if target_format == 'channels_first':
53 new_values = shape[:spatial_axes[0]]
54 new_values += (shape[-1],)
55 new_values += tuple(shape[x] for x in spatial_axes)
57 if isinstance(shape, list):
58 return list(new_values)
59 return new_values
60 elif target_format == 'channels_last':
61 return shape
62 else:
63 raise ValueError('The `data_format` argument must be one of '
64 '"channels_first", "channels_last". Received: ' +
65 str(target_format))
67""" TAKEN FROM KERAS 2.2.0 """
68def normalize_data_format(value):
69 """Checks that the value correspond to a valid data format.
70 # Arguments
71 value: String or None. `'channels_first'` or `'channels_last'`.
72 # Returns
73 A string, either `'channels_first'` or `'channels_last'`
74 # Example
75 ```python
76 >>> from tensorflow.keras import backend as K
77 >>> K.normalize_data_format(None)
78 'channels_first'
79 >>> K.normalize_data_format('channels_last')
80 'channels_last'
81 ```
82 # Raises
83 ValueError: if `value` or the global `data_format` invalid.
84 """
85 if value is None: 85 ↛ 87line 85 didn't jump to line 87, because the condition on line 85 was never false
86 value = 'channels_last'
87 data_format = value.lower()
88 if data_format not in {'channels_first', 'channels_last'}: 88 ↛ 89line 88 didn't jump to line 89, because the condition on line 88 was never true
89 raise ValueError('The `data_format` argument must be one of '
90 '"channels_first", "channels_last". Received: ' +
91 str(value))
92 return data_format
95class PadUtils:
96 """Helper class for advanced padding."""
98 @staticmethod
99 def get_padding_for_same(kernel_size: Tuple[int], strides: int = 1) -> Tuple[int]:
100 """
101 Calculate padding size to keep input and output dimensions equal for a given kernel size.
103 .. hint:: custom paddings are currently only implemented for strides = 1
105 :param kernel_size: size of padding kernel size
106 :param strides: number of strides (default 1, currently only strides=1 supported)
108 :return: padding size
109 """
110 if strides != 1:
111 raise NotImplementedError("Strides other than 1 not implemented!")
112 if not all(isinstance(k, int) for k in kernel_size):
113 raise ValueError(f"The `kernel_size` argument must have a tuple of integers. Got: {kernel_size} "
114 f"of type {[type(k) for k in kernel_size]}")
116 ks = np.array(kernel_size, dtype=np.int64)
118 if any(k <= 0 for k in ks):
119 raise ValueError(f"All values of kernel_size must be > 0. Got: {kernel_size} ")
121 if all(k % 2 == 1 for k in ks): # (d & 0x1 for d in ks):
122 pad = ((ks - 1) / 2).astype(np.int64)
123 # convert numpy int to base int
124 pad = [int(v.item()) for v in pad]
125 return tuple(pad)
126 else:
127 raise NotImplementedError(f"even kernel size not implemented. Got {kernel_size}")
129 @staticmethod
130 def spatial_2d_padding(padding=((1, 1), (1, 1)), data_format=None):
131 """
132 Pad the 2nd and 3rd dimensions of a 4D tensor.
134 # Arguments
135 x: Tensor or variable.
136 padding: Tuple of 2 tuples, padding pattern.
137 data_format: string, `"channels_last"` or `"channels_first"`.
139 # Returns
140 A padded 4D tensor.
142 # Raises
143 ValueError: if `data_format` is neither `"channels_last"` or `"channels_first"`.
144 """
145 assert len(padding) == 2
146 assert len(padding[0]) == 2
147 assert len(padding[1]) == 2
148 data_format = normalize_data_format(data_format)
150 pattern = [[0, 0],
151 list(padding[0]),
152 list(padding[1]),
153 [0, 0]]
154 pattern = transpose_shape(pattern, data_format, spatial_axes=(1, 2))
155 return pattern
157 @staticmethod
158 def check_padding_format(padding):
159 """Check padding format (int, 1D or 2D, >0)."""
160 if isinstance(padding, int):
161 normalized_padding = ((padding, padding), (padding, padding))
162 elif hasattr(padding, '__len__'):
163 if len(padding) != 2:
164 raise ValueError('`padding` should have two elements. '
165 'Found: ' + str(padding))
166 for idx_pad, sub_pad in enumerate(padding):
167 if isinstance(sub_pad, str):
168 raise ValueError(f'`padding[{idx_pad}]` is str but must be int')
169 if hasattr(sub_pad, '__len__'):
170 if len(sub_pad) != 2:
171 raise ValueError(f'`padding[{idx_pad}]` should have one or two elements. '
172 f'Found: {padding[idx_pad]}')
173 if not all(isinstance(sub_k, int) for sub_k in padding[idx_pad]):
174 raise ValueError(f'`padding[{idx_pad}]` should have one or two elements of type int. '
175 f"Found:{padding[idx_pad]} of type {[type(sub_k) for sub_k in padding[idx_pad]]}")
176 height_padding = conv_utils.normalize_tuple(padding[0], 2,
177 '1st entry of padding')
178 if not all(k >= 0 for k in height_padding):
179 raise ValueError(
180 f"The `1st entry of padding` argument must be >= 0. Received: {padding[0]} of type {type(padding[0])}")
181 width_padding = conv_utils.normalize_tuple(padding[1], 2,
182 '2nd entry of padding')
183 if not all(k >= 0 for k in width_padding):
184 raise ValueError(
185 f"The `2nd entry of padding` argument must be >= 0. Received: {padding[1]} of type {type(padding[1])}")
186 normalized_padding = (height_padding, width_padding)
187 else:
188 raise ValueError('`padding` should be either an int, '
189 'a tuple of 2 ints '
190 '(symmetric_height_pad, symmetric_width_pad), '
191 'or a tuple of 2 tuples of 2 ints '
192 '((top_pad, bottom_pad), (left_pad, right_pad)). '
193 f'Found: {padding} of type {type(padding)}')
194 return normalized_padding
196""" TAKEN FROM KERAS 2.2.0 """
197class InputSpec(object):
198 """Specifies the ndim, dtype and shape of every input to a layer.
199 Every layer should expose (if appropriate) an `input_spec` attribute:
200 a list of instances of InputSpec (one per input tensor).
201 A None entry in a shape is compatible with any dimension,
202 a None shape is compatible with any shape.
203 # Arguments
204 dtype: Expected datatype of the input.
205 shape: Shape tuple, expected shape of the input
206 (may include None for unchecked axes).
207 ndim: Integer, expected rank of the input.
208 max_ndim: Integer, maximum rank of the input.
209 min_ndim: Integer, minimum rank of the input.
210 axes: Dictionary mapping integer axes to
211 a specific dimension value.
212 """
214 def __init__(self, dtype=None,
215 shape=None,
216 ndim=None,
217 max_ndim=None,
218 min_ndim=None,
219 axes=None):
220 self.dtype = dtype
221 self.shape = shape
222 if shape is not None:
223 self.ndim = len(shape)
224 else:
225 self.ndim = ndim
226 self.max_ndim = max_ndim
227 self.min_ndim = min_ndim
228 self.axes = axes or {}
230 def __repr__(self):
231 spec = [('dtype=' + str(self.dtype)) if self.dtype else '',
232 ('shape=' + str(self.shape)) if self.shape else '',
233 ('ndim=' + str(self.ndim)) if self.ndim else '',
234 ('max_ndim=' + str(self.max_ndim)) if self.max_ndim else '',
235 ('min_ndim=' + str(self.min_ndim)) if self.min_ndim else '',
236 ('axes=' + str(self.axes)) if self.axes else '']
237 return 'InputSpec(%s)' % ', '.join(x for x in spec if x)
239""" TAKEN FROM KERAS 2.2.0 """
240class _ZeroPadding(Layer):
241 """Abstract nD ZeroPadding layer (private, used as implementation base).
242 # Arguments
243 padding: Tuple of tuples of two ints. Can be a tuple of ints when
244 rank is 1.
245 data_format: A string,
246 one of `"channels_last"` or `"channels_first"`.
247 The ordering of the dimensions in the inputs.
248 `"channels_last"` corresponds to inputs with shape
249 `(batch, ..., channels)` while `"channels_first"` corresponds to
250 inputs with shape `(batch, channels, ...)`.
251 It defaults to the `image_data_format` value found in your
252 Keras config file at `~/.keras/keras.json`.
253 If you never set it, then it will be "channels_last".
254 """
255 def __init__(self, padding, data_format=None, **kwargs):
256 # self.rank is 1 for ZeroPadding1D, 2 for ZeroPadding2D.
257 self.rank = len(padding)
258 self.padding = padding
259 self.data_format = normalize_data_format(data_format)
260 self.input_spec = tf.keras.layers.InputSpec(ndim=self.rank + 2)
261 super(_ZeroPadding, self).__init__(**kwargs)
263 def call(self, inputs):
264 raise NotImplementedError
266 def compute_output_shape(self, input_shape):
267 padding_all_dims = ((0, 0),) + self.padding + ((0, 0),)
268 spatial_axes = list(range(1, 1 + self.rank))
269 padding_all_dims = transpose_shape(padding_all_dims,
270 self.data_format,
271 spatial_axes)
272 output_shape = list(input_shape)
273 for dim in range(len(output_shape)):
274 if output_shape[dim] is not None:
275 output_shape[dim] += sum(padding_all_dims[dim])
276 return tuple(output_shape)
278 def get_config(self):
279 config = {'padding': self.padding,
280 'data_format': self.data_format}
281 base_config = super(_ZeroPadding, self).get_config()
282 return dict(list(base_config.items()) + list(config.items()))
285class ReflectionPadding2D(_ZeroPadding):
286 """
287 Reflection padding layer for 2D input.
289 This custom padding layer is built on keras' zero padding layers. Doc is copy and pasted from the original
290 functions/methods:
292 This layer can add rows and columns of reflected values
293 at the top, bottom, left and right side of an image like tensor.
295 Example:
296 6, 5, 4, 5, 6, 5, 4
297 _________
298 1, 2, 3 RefPad(padding=[[1, 1,], [2, 2]]) 3, 2,| 1, 2, 3,| 2, 1
299 4, 5, 6 =============================>>>> 6, 5,| 4, 5, 6,| 5, 4
300 _________
301 3, 2, 1, 2, 3, 2, 1
305 # Arguments
306 padding: int, or tuple of 2 ints, or tuple of 2 tuples of 2 ints.
307 - If int: the same symmetric padding
308 is applied to height and width.
309 - If tuple of 2 ints:
310 interpreted as two different
311 symmetric padding values for height and width:
312 `(symmetric_height_pad, symmetric_width_pad)`.
313 - If tuple of 2 tuples of 2 ints:
314 interpreted as
315 `((top_pad, bottom_pad), (left_pad, right_pad))`
316 data_format: A string,
317 one of `"channels_last"` or `"channels_first"`.
318 The ordering of the dimensions in the inputs.
319 `"channels_last"` corresponds to inputs with shape
320 `(batch, height, width, channels)` while `"channels_first"`
321 corresponds to inputs with shape
322 `(batch, channels, height, width)`.
323 It defaults to the `image_data_format` value found in your
324 Keras config file at `~/.keras/keras.json`.
325 If you never set it, then it will be "channels_last".
327 # Input shape
328 4D tensor with shape:
329 - If `data_format` is `"channels_last"`:
330 `(batch, rows, cols, channels)`
331 - If `data_format` is `"channels_first"`:
332 `(batch, channels, rows, cols)`
334 # Output shape
335 4D tensor with shape:
336 - If `data_format` is `"channels_last"`:
337 `(batch, padded_rows, padded_cols, channels)`
338 - If `data_format` is `"channels_first"`:
339 `(batch, channels, padded_rows, padded_cols)`
340 '
341 """
343 @interfaces.legacy_zeropadding2d_support
344 def __init__(self,
345 padding=(1, 1),
346 data_format=None,
347 **kwargs):
348 """Initialise ReflectionPadding2D."""
349 normalized_padding = PadUtils.check_padding_format(padding=padding)
350 super(ReflectionPadding2D, self).__init__(normalized_padding,
351 data_format,
352 **kwargs)
354 def call(self, inputs, mask=None):
355 """Call ReflectionPadding2D."""
356 pattern = PadUtils.spatial_2d_padding(padding=self.padding, data_format=self.data_format)
357 return tf.pad(tensor=inputs, paddings=pattern, mode='REFLECT')
360class SymmetricPadding2D(_ZeroPadding):
361 """
362 Symmetric padding layer for 2D input.
364 This custom padding layer is built on keras' zero padding layers. Doc is copy pasted from the original
365 functions/methods:
367 This layer can add rows and columns of symmetric values
368 at the top, bottom, left and right side of an image like tensor.
370 Example:
371 2, 1, 1, 2, 3, 3, 2
372 _________
373 1, 2, 3 SymPad(padding=[[1, 1,], [2, 2]]) 2, 1,| 1, 2, 3,| 3, 2
374 4, 5, 6 =============================>>>> 5, 4,| 4, 5, 6,| 6, 5
375 _________
376 5, 4, 4, 5, 6, 6, 5
379 '# Arguments
380 padding: int, or tuple of 2 ints, or tuple of 2 tuples of 2 ints.
381 - If int: the same symmetric padding
382 is applied to height and width.
383 - If tuple of 2 ints:
384 interpreted as two different
385 symmetric padding values for height and width:
386 `(symmetric_height_pad, symmetric_width_pad)`.
387 - If tuple of 2 tuples of 2 ints:
388 interpreted as
389 `((top_pad, bottom_pad), (left_pad, right_pad))`
390 data_format: A string,
391 one of `"channels_last"` or `"channels_first"`.
392 The ordering of the dimensions in the inputs.
393 `"channels_last"` corresponds to inputs with shape
394 `(batch, height, width, channels)` while `"channels_first"`
395 corresponds to inputs with shape
396 `(batch, channels, height, width)`.
397 It defaults to the `image_data_format` value found in your
398 Keras config file at `~/.keras/keras.json`.
399 If you never set it, then it will be "channels_last".
401 # Input shape
402 4D tensor with shape:
403 - If `data_format` is `"channels_last"`:
404 `(batch, rows, cols, channels)`
405 - If `data_format` is `"channels_first"`:
406 `(batch, channels, rows, cols)`
408 # Output shape
409 4D tensor with shape:
410 - If `data_format` is `"channels_last"`:
411 `(batch, padded_rows, padded_cols, channels)`
412 - If `data_format` is `"channels_first"`:
413 `(batch, channels, padded_rows, padded_cols)`
414 '
415 """
417 @interfaces.legacy_zeropadding2d_support
418 def __init__(self,
419 padding=(1, 1),
420 data_format=None,
421 **kwargs):
422 """Initialise SymmetricPadding2D."""
423 normalized_padding = PadUtils.check_padding_format(padding=padding)
424 super(SymmetricPadding2D, self).__init__(normalized_padding,
425 data_format,
426 **kwargs)
428 def call(self, inputs, mask=None):
429 """Call SymmetricPadding2D."""
430 pattern = PadUtils.spatial_2d_padding(padding=self.padding, data_format=self.data_format)
431 return tf.pad(tensor=inputs, paddings=pattern, mode='SYMMETRIC')
434class Padding2D:
435 """
436 Combine all implemented padding methods.
438 You can call this method by defining a specific padding type. The __call__ method will return the corresponding
439 Padding layer.
441 .. code-block:: python
443 input_x = ... # your input data
444 kernel_size = (5, 1)
445 padding_size = PadUtils.get_padding_for_same(kernel_size)
447 tower = layers.Conv2D(...)(input_x)
448 tower = layers.Activation(...)(tower)
449 tower = Padding2D('ZeroPad2D')(padding=padding_size, name=f'Custom_Pad')(tower)
451 Padding type can either be set by a string or directly by using an instance of a valid padding class.
452 """
454 allowed_paddings = {
455 **dict.fromkeys(("RefPad2D", "ReflectionPadding2D"), ReflectionPadding2D),
456 **dict.fromkeys(("SymPad2D", "SymmetricPadding2D"), SymmetricPadding2D),
457 **dict.fromkeys(("ZeroPad2D", "ZeroPadding2D"), ZeroPadding2D)
458 }
459 padding_type = Union[ReflectionPadding2D, SymmetricPadding2D, ZeroPadding2D]
461 def __init__(self, padding_type: Union[str, padding_type]):
462 """Set padding type."""
463 self.padding_type = padding_type
465 def _check_and_get_padding(self):
466 if isinstance(self.padding_type, str):
467 try:
468 pad2d = self.allowed_paddings[self.padding_type]
469 except KeyError as e:
470 raise NotImplementedError(
471 f"`{e}' is not implemented as padding. Use one of those: i) `RefPad2D', ii) `SymPad2D', "
472 f"iii) `ZeroPad2D'")
473 else:
474 if self.padding_type in self.allowed_paddings.values():
475 pad2d = self.padding_type
476 else:
477 raise TypeError(f"`{self.padding_type.__name__}' is not a valid padding layer type. "
478 "Use one of those: "
479 "i) ReflectionPadding2D, ii) SymmetricPadding2D, iii) ZeroPadding2D")
480 return pad2d
482 def __call__(self, *args, **kwargs):
483 """Call padding."""
484 return self._check_and_get_padding()(*args, **kwargs)
487if __name__ == '__main__':
488 from tensorflow.keras.models import Model
489 from tensorflow.keras.layers import Conv2D, Flatten, Dense, Input
491 kernel_1 = (3, 3)
492 kernel_2 = (5, 5)
493 kernel_3 = (3, 3)
494 x = np.array(range(2000)).reshape((-1, 10, 10, 1))
495 y = x.mean(axis=(1, 2))
497 x_input = Input(shape=x.shape[1:])
498 pad1 = PadUtils.get_padding_for_same(kernel_size=kernel_1)
499 x_out = ReflectionPadding2D(padding=pad1, name="RefPAD")(x_input)
500 x_out = Conv2D(5, kernel_size=kernel_1, activation='relu')(x_out)
502 pad2 = PadUtils.get_padding_for_same(kernel_size=kernel_2)
503 x_out = SymmetricPadding2D(padding=pad2, name="SymPAD")(x_out)
504 x_out = Conv2D(2, kernel_size=kernel_2, activation='relu')(x_out)
506 pad3 = PadUtils.get_padding_for_same(kernel_size=kernel_3)
507 x_out = Padding2D('RefPad2D')(padding=pad3, name="Padding2D_RefPad")(x_out)
508 x_out = Conv2D(2, kernel_size=kernel_3, activation='relu')(x_out)
509 x_out = Flatten()(x_out)
510 x_out = Dense(1, activation='linear')(x_out)
512 model = Model(inputs=x_input, outputs=x_out)
513 model.compile('adam', loss='mse')
514 model.summary()
515 model.fit(x, y, epochs=10)