Coverage for mlair/model_modules/advanced_paddings.py: 71%

129 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2023-06-30 10:40 +0000

1"""Collection of customised padding layers.""" 

2 

3__author__ = 'Felix Kleinert' 

4__date__ = '2020-03-02' 

5 

6 

7from typing import Union, Tuple 

8 

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 

21 

22 

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) 

56 

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)) 

66 

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 

93 

94 

95class PadUtils: 

96 """Helper class for advanced padding.""" 

97 

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. 

102 

103 .. hint:: custom paddings are currently only implemented for strides = 1 

104 

105 :param kernel_size: size of padding kernel size 

106 :param strides: number of strides (default 1, currently only strides=1 supported) 

107 

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]}") 

115 

116 ks = np.array(kernel_size, dtype=np.int64) 

117 

118 if any(k <= 0 for k in ks): 

119 raise ValueError(f"All values of kernel_size must be > 0. Got: {kernel_size} ") 

120 

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}") 

128 

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. 

133 

134 # Arguments 

135 x: Tensor or variable. 

136 padding: Tuple of 2 tuples, padding pattern. 

137 data_format: string, `"channels_last"` or `"channels_first"`. 

138 

139 # Returns 

140 A padded 4D tensor. 

141 

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) 

149 

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 

156 

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 

195 

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 """ 

213 

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 {} 

229 

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) 

238 

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) 

262 

263 def call(self, inputs): 

264 raise NotImplementedError 

265 

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) 

277 

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())) 

283 

284 

285class ReflectionPadding2D(_ZeroPadding): 

286 """ 

287 Reflection padding layer for 2D input. 

288 

289 This custom padding layer is built on keras' zero padding layers. Doc is copy and pasted from the original 

290 functions/methods: 

291 

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. 

294 

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 

302 

303 

304 

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". 

326 

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)` 

333 

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 """ 

342 

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) 

353 

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') 

358 

359 

360class SymmetricPadding2D(_ZeroPadding): 

361 """ 

362 Symmetric padding layer for 2D input. 

363 

364 This custom padding layer is built on keras' zero padding layers. Doc is copy pasted from the original 

365 functions/methods: 

366 

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. 

369 

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 

377 

378 

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". 

400 

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)` 

407 

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 """ 

416 

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) 

427 

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') 

432 

433 

434class Padding2D: 

435 """ 

436 Combine all implemented padding methods. 

437 

438 You can call this method by defining a specific padding type. The __call__ method will return the corresponding 

439 Padding layer. 

440 

441 .. code-block:: python 

442 

443 input_x = ... # your input data 

444 kernel_size = (5, 1) 

445 padding_size = PadUtils.get_padding_for_same(kernel_size) 

446 

447 tower = layers.Conv2D(...)(input_x) 

448 tower = layers.Activation(...)(tower) 

449 tower = Padding2D('ZeroPad2D')(padding=padding_size, name=f'Custom_Pad')(tower) 

450 

451 Padding type can either be set by a string or directly by using an instance of a valid padding class. 

452 """ 

453 

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] 

460 

461 def __init__(self, padding_type: Union[str, padding_type]): 

462 """Set padding type.""" 

463 self.padding_type = padding_type 

464 

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 

481 

482 def __call__(self, *args, **kwargs): 

483 """Call padding.""" 

484 return self._check_and_get_padding()(*args, **kwargs) 

485 

486 

487if __name__ == '__main__': 

488 from tensorflow.keras.models import Model 

489 from tensorflow.keras.layers import Conv2D, Flatten, Dense, Input 

490 

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)) 

496 

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) 

501 

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) 

505 

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) 

511 

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)