├── Deformable_Conv ├── __init__.py ├── dataset.py ├── layers.py ├── network.py ├── callbacks.py └── deform_conv.py ├── BlurPooling ├── __init__.py ├── BlurPooling.py ├── MaxBlurPooling.py └── AverageBlurPooling.py ├── BlurPooling_test.py ├── SE_module.py ├── deform_conv_test.py ├── .gitignore ├── CBAM_module.py ├── sacled_mnist.py ├── README.md ├── non_local.py ├── stn_module.py ├── non_local_test.py └── STN_test.ipynb /Deformable_Conv/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BlurPooling/__init__.py: -------------------------------------------------------------------------------- 1 | from .AverageBlurPooling import * 2 | from .BlurPooling import * 3 | from .MaxBlurPooling import * 4 | -------------------------------------------------------------------------------- /Deformable_Conv/dataset.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import, division 3 | import tensorflow as tf 4 | from tensorflow.keras.datasets import mnist 5 | from tensorflow.keras.preprocessing.image import ImageDataGenerator 6 | 7 | 8 | def get_mnist_dataset(): 9 | (X_train, y_train), (X_test, y_test) = mnist.load_data() 10 | X_train = X_train.astype('float32') / 255 11 | X_test = X_test.astype('float32') / 255 12 | X_train = X_train[..., None] 13 | X_test = X_test[..., None] 14 | Y_train = tf.keras.utils.to_categorical(y_train, 10) 15 | Y_test = tf.keras.utils.to_categorical(y_test, 10) 16 | 17 | return (X_train, Y_train), (X_test, Y_test) 18 | 19 | 20 | def get_gen(set_name, batch_size, translate, scale, 21 | shuffle=True): 22 | if set_name == 'train': 23 | (X, Y), _ = get_mnist_dataset() 24 | elif set_name == 'test': 25 | _, (X, Y) = get_mnist_dataset() 26 | 27 | image_gen = ImageDataGenerator( 28 | zoom_range=scale, 29 | width_shift_range=translate, 30 | height_shift_range=translate 31 | ) 32 | gen = image_gen.flow(X, Y, batch_size=batch_size, shuffle=shuffle) 33 | return gen -------------------------------------------------------------------------------- /BlurPooling_test.py: -------------------------------------------------------------------------------- 1 | import BlurPooling as pooling 2 | import tensorflow as tf 3 | from tensorflow.keras.layers import * 4 | from tensorflow.keras.models import Model 5 | import numpy as np 6 | 7 | def test_pooling_model(input_shape, pooling_type): 8 | input = Input(input_shape) 9 | layer_pool = eval('pooling.'+pooling_type)()(input) 10 | layer_flattern = Flatten()(layer_pool) 11 | output = Dense(1)(layer_flattern) 12 | model = Model(input, output) 13 | model.summary() 14 | return model 15 | 16 | if __name__=='__main__': 17 | poolingtype = [ 18 | 'MaxBlurPooling1D', 19 | 'MaxBlurPooling2D', 20 | 'AverageBlurPooling1D', 21 | 'AverageBlurPooling2D', 22 | 'BlurPool2D', 23 | 'BlurPool1D' 24 | ] 25 | for item in poolingtype: 26 | if '2D' in item: 27 | input_shape = (224, 224, 3) 28 | model = test_pooling_model(input_shape, item) 29 | model.predict([np.random.random((1, 224, 224, 3))]) 30 | else: 31 | input_shape = (224, 3) 32 | model = test_pooling_model(input_shape, item) 33 | model.predict([np.random.random((1, 224, 3))]) 34 | 35 | -------------------------------------------------------------------------------- /SE_module.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Activation, Reshape, Conv2D, BatchNormalization 3 | import numpy as np 4 | 5 | class Squeeze_excitation_layer(tf.keras.Model): 6 | def __init__(self, filter_sq): 7 | super().__init__() 8 | self.filter_sq = filter_sq 9 | 10 | def call(self, inputs): 11 | channel = inputs.shape[-1] 12 | squeeze = GlobalAveragePooling2D()(inputs) 13 | excitation = Dense(channel//self.filter_sq)(squeeze) 14 | excitation = Activation('relu')(excitation) 15 | excitation = Dense(channel)(excitation) 16 | excitation = Activation('sigmoid')(excitation) 17 | # reshape excitation: 1*1*input.shape[-1] 18 | excitation = Reshape((1, 1, channel))(excitation) 19 | # 获得通道权重 20 | outputs = inputs*excitation 21 | return outputs 22 | 23 | def SEBottleneck(input, filter_sq=16, stride=1): 24 | residual = inputs 25 | se_module = Squeeze_excitation_layer(16) 26 | 27 | x = Conv2D(16, kernel_size=1)(input) 28 | x = BatchNormalization()(x) 29 | x = Activation('relu')(x) 30 | 31 | x = Conv2D(16, kernel_size=3, strides=stride, padding='same')(input) 32 | x = BatchNormalization()(x) 33 | x = Activation('relu')(x) 34 | 35 | x = Conv2D(32, kernel_size=1)(input) 36 | x = BatchNormalization()(x) 37 | x = se_module(x) 38 | 39 | output = x+residual 40 | output = Activation('relu')(output) 41 | 42 | return output 43 | 44 | 45 | 46 | 47 | SE_module = Squeeze_excitation_layer(16) 48 | inputs = np.zeros((1, 32, 32, 32), dtype=np.float32) 49 | out_shape = SE_module(inputs).shape 50 | print(out_shape) 51 | 52 | 53 | inputs = np.zeros((1, 32, 32, 32), dtype=np.float32) 54 | SEB = SEBottleneck(inputs) 55 | print(SEB.shape) 56 | 57 | -------------------------------------------------------------------------------- /Deformable_Conv/layers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | import tensorflow as tf 3 | from tensorflow.keras.layers import Conv2D 4 | from .deform_conv import tf_batch_map_offsets 5 | 6 | 7 | class ConvOffset2D(Conv2D): 8 | """ConvOffset2D""" 9 | 10 | def __init__(self, filters, init_normal_stddev=0.01, **kwargs): 11 | """Init""" 12 | 13 | self.filters = filters 14 | super(ConvOffset2D, self).__init__( 15 | self.filters * 2, (3, 3), padding='same', use_bias=False, 16 | # TODO gradients are near zero if init is zeros 17 | kernel_initializer='zeros', 18 | # kernel_initializer=RandomNormal(0, init_normal_stddev), 19 | **kwargs 20 | ) 21 | 22 | def call(self, x): 23 | # TODO offsets probably have no nonlinearity? 24 | x_shape = x.get_shape() 25 | offsets = super(ConvOffset2D, self).call(x) 26 | 27 | offsets = self._to_bc_h_w_2(offsets, x_shape) 28 | x = self._to_bc_h_w(x, x_shape) 29 | x_offset = tf_batch_map_offsets(x, offsets) 30 | x_offset = self._to_b_h_w_c(x_offset, x_shape) 31 | return x_offset 32 | 33 | def compute_output_shape(self, input_shape): 34 | return input_shape 35 | 36 | @staticmethod 37 | def _to_bc_h_w_2(x, x_shape): 38 | """(b, h, w, 2c) -> (b*c, h, w, 2)""" 39 | x = tf.transpose(x, [0, 3, 1, 2]) 40 | x = tf.reshape(x, (-1, int(x_shape[1]), int(x_shape[2]), 2)) 41 | return x 42 | 43 | @staticmethod 44 | def _to_bc_h_w(x, x_shape): 45 | """(b, h, w, c) -> (b*c, h, w)""" 46 | x = tf.transpose(x, [0, 3, 1, 2]) 47 | x = tf.reshape(x, (-1, int(x_shape[1]), int(x_shape[2]))) 48 | return x 49 | 50 | @staticmethod 51 | def _to_b_h_w_c(x, x_shape): 52 | """(b*c, h, w) -> (b, h, w, c)""" 53 | x = tf.reshape( 54 | x, (-1, int(x_shape[3]), int(x_shape[1]), int(x_shape[2])) 55 | ) 56 | x = tf.transpose(x, [0, 2, 3, 1]) 57 | return x -------------------------------------------------------------------------------- /deform_conv_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import tensorflow.keras.backend as K 3 | from scipy.ndimage.interpolation import map_coordinates 4 | from Deformable_Conv.deform_conv import ( 5 | tf_map_coordinates, 6 | sp_batch_map_coordinates, tf_batch_map_coordinates, 7 | sp_batch_map_offsets, tf_batch_map_offsets 8 | ) 9 | import tensorflow as tf 10 | tf.compat.v1.disable_eager_execution() 11 | 12 | def test_tf_map_coordinates(): 13 | np.random.seed(42) 14 | input = np.random.random((100, 100)) 15 | coords = np.random.random((200, 2)) * 99 16 | 17 | sp_mapped_vals = map_coordinates(input, coords.T, order=1) 18 | tf_mapped_vals = tf_map_coordinates( 19 | K.variable(input), K.variable(coords) 20 | ) 21 | assert np.allclose(sp_mapped_vals, K.eval(tf_mapped_vals), atol=1e-5) 22 | 23 | 24 | def test_tf_batch_map_coordinates(): 25 | np.random.seed(42) 26 | input = np.random.random((4, 100, 100)) 27 | coords = np.random.random((4, 200, 2)) * 99 28 | 29 | sp_mapped_vals = sp_batch_map_coordinates(input, coords) 30 | tf_mapped_vals = tf_batch_map_coordinates( 31 | K.variable(input), K.variable(coords) 32 | ) 33 | assert np.allclose(sp_mapped_vals, K.eval(tf_mapped_vals), atol=1e-5) 34 | 35 | 36 | def test_tf_batch_map_offsets(): 37 | np.random.seed(42) 38 | input = np.random.random((4, 100, 100)) 39 | offsets = np.random.random((4, 100, 100, 2)) * 2 40 | 41 | sp_mapped_vals = sp_batch_map_offsets(input, offsets) 42 | tf_mapped_vals = tf_batch_map_offsets( 43 | K.variable(input), K.variable(offsets) 44 | ) 45 | assert np.allclose(sp_mapped_vals, K.eval(tf_mapped_vals), atol=1e-5) 46 | 47 | 48 | def test_tf_batch_map_offsets_grad(): 49 | np.random.seed(42) 50 | input = np.random.random((4, 100, 100)) 51 | offsets = np.random.random((4, 100, 100, 2)) * 2 52 | 53 | input = K.variable(input) 54 | offsets = K.variable(offsets) 55 | 56 | tf_mapped_vals = tf_batch_map_offsets(input, offsets) 57 | grad = K.gradients(tf_mapped_vals, input)[0] 58 | grad = K.eval(grad) 59 | assert not np.allclose(grad, 0) 60 | 61 | if __name__ == '__main__': 62 | test_tf_map_coordinates() 63 | test_tf_batch_map_coordinates() 64 | test_tf_batch_map_offsets() 65 | test_tf_batch_map_offsets_grad() 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .history -------------------------------------------------------------------------------- /CBAM_module.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.keras import backend as K 3 | import tensorflow.keras.layers as layer 4 | import numpy as np 5 | 6 | class CBAM_module(tf.keras.Model): 7 | def __init__(self, ratio=16, name=''): 8 | super().__init__() 9 | self._ratio = ratio 10 | self._name = name 11 | 12 | def channel_attention(self, input): 13 | channel = input.shape[-1] 14 | # 同时进行avg pooling和 max pooling 15 | avg_pool = layer.GlobalAveragePooling2D()(input) 16 | max_pool = layer.GlobalAveragePooling2D()(input) 17 | avg_pool = layer.Reshape((1, 1, channel))(avg_pool) 18 | max_pool = layer.Reshape((1, 1, channel))(max_pool) 19 | 20 | # 对pooling结果经过两层全连接层,第一层核数量为input的通道数//ratio,第二层则恢复到原通道数 21 | avg_pool = layer.Dense(channel//self._ratio, activation='relu', kernel_initializer='he_normal', name=self._name)(avg_pool) 22 | max_pool = layer.Dense(channel//self._ratio, activation='relu',kernel_initializer='he_normal', name=self._name)(max_pool) 23 | 24 | avg_pool = layer.Dense(channel, activation='relu', kernel_initializer='he_normal', name=self._name)(avg_pool) 25 | max_pool = layer.Dense(channel, activation='relu', kernel_initializer='he_normal', name=self._name)(max_pool) 26 | 27 | # 对avg_pool与max_pool相加做激活,得到(batchsize, 1,1 channel)的tensor,作为权重与input相乘 28 | output = layer.Add()([avg_pool, max_pool]) 29 | output = layer.Activation('sigmoid')(output) 30 | output = layer.multiply([input, output]) 31 | return output 32 | 33 | def spatial_attention(self, input, kernel_size=7): 34 | avg_pool = layer.Lambda(lambda x:K.mean(x,axis=3, keepdims=True))(input) 35 | max_pool = layer.Lambda(lambda x:K.max(x,axis=3, keepdims=True))(input) 36 | 37 | concat_feature = layer.Concatenate(axis=3)([avg_pool, max_pool]) 38 | output =layer.Conv2D(filters = 1, 39 | kernel_size=kernel_size, 40 | strides=1, 41 | padding='same', 42 | kernel_initializer='he_normal')(concat_feature) 43 | output = layer.Activation('sigmoid')(output) 44 | output = layer.multiply([input, output]) 45 | return output 46 | 47 | def call(self, input): 48 | cbam_feature = self.channel_attention(input) 49 | cbam_feature = self.spatial_attention(cbam_feature) 50 | return cbam_feature 51 | 52 | CBAM_module = CBAM_module() 53 | inputs = np.zeros((1, 32, 32, 32), dtype=np.float32) 54 | out_shape = CBAM_module(inputs).shape 55 | print(out_shape) -------------------------------------------------------------------------------- /Deformable_Conv/network.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | from tensorflow.keras.layers import Input, Conv2D, Activation, GlobalAvgPool2D, Dense, BatchNormalization 3 | from .layers import ConvOffset2D 4 | 5 | 6 | def build_cnn(): 7 | inputs = l = Input((28, 28, 1), name='input') 8 | 9 | # conv11 10 | l = Conv2D(32, (3, 3), padding='same', name='conv11')(l) 11 | l = Activation('relu', name='conv11_relu')(l) 12 | l = BatchNormalization(name='conv11_bn')(l) 13 | 14 | # conv12 15 | l = Conv2D(64, (3, 3), padding='same', strides=(2, 2), name='conv12')(l) 16 | l = Activation('relu', name='conv12_relu')(l) 17 | l = BatchNormalization(name='conv12_bn')(l) 18 | 19 | # conv21 20 | l = Conv2D(128, (3, 3), padding='same', name='conv21')(l) 21 | l = Activation('relu', name='conv21_relu')(l) 22 | l = BatchNormalization(name='conv21_bn')(l) 23 | 24 | # conv22 25 | l = Conv2D(128, (3, 3), padding='same', strides=(2, 2), name='conv22')(l) 26 | l = Activation('relu', name='conv22_relu')(l) 27 | l = BatchNormalization(name='conv22_bn')(l) 28 | 29 | # out 30 | l = GlobalAvgPool2D(name='avg_pool')(l) 31 | l = Dense(10, name='fc1')(l) 32 | outputs = l = Activation('softmax', name='out')(l) 33 | 34 | return inputs, outputs 35 | 36 | 37 | def build_deform_cnn(trainable): 38 | inputs = l = Input((28, 28, 1), name='input') 39 | 40 | # conv11 41 | l = Conv2D(32, (3, 3), padding='same', name='conv11', trainable=trainable)(l) 42 | l = Activation('relu', name='conv11_relu')(l) 43 | l = BatchNormalization(name='conv11_bn')(l) 44 | 45 | # conv12 46 | l_offset = ConvOffset2D(32, name='conv12_offset')(l) 47 | l = Conv2D(64, (3, 3), padding='same', strides=(2, 2), name='conv12', trainable=trainable)(l_offset) 48 | l = Activation('relu', name='conv12_relu')(l) 49 | l = BatchNormalization(name='conv12_bn')(l) 50 | 51 | # conv21 52 | l_offset = ConvOffset2D(64, name='conv21_offset')(l) 53 | l = Conv2D(128, (3, 3), padding='same', name='conv21', trainable=trainable)(l_offset) 54 | l = Activation('relu', name='conv21_relu')(l) 55 | l = BatchNormalization(name='conv21_bn')(l) 56 | 57 | # conv22 58 | l_offset = ConvOffset2D(128, name='conv22_offset')(l) 59 | l = Conv2D(128, (3, 3), padding='same', strides=(2, 2), name='conv22', trainable=trainable)(l_offset) 60 | l = Activation('relu', name='conv22_relu')(l) 61 | l = BatchNormalization(name='conv22_bn')(l) 62 | 63 | # out 64 | l = GlobalAvgPool2D(name='avg_pool')(l) 65 | l = Dense(10, name='fc1', trainable=trainable)(l) 66 | outputs = l = Activation('softmax', name='out')(l) 67 | 68 | return inputs, outputs -------------------------------------------------------------------------------- /BlurPooling/BlurPooling.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.keras.layers import Layer 3 | from tensorflow.keras import backend as K 4 | import numpy as np 5 | 6 | class BlurPool2D(Layer): 7 | def __init__(self, pool_size: int = 2, kernel_size: int = 3, **kwargs): 8 | self.pool_size = pool_size 9 | self.blur_kernel = None 10 | self.kernel_size = kernel_size 11 | 12 | super(BlurPool2D, self).__init__(**kwargs) 13 | 14 | def build(self, input_shape): 15 | 16 | if self.kernel_size == 3: 17 | blur_kernel = np.array([[1, 2, 1], 18 | [2, 4, 2], 19 | [1, 2, 1]]) 20 | blur_kernel = blur_kernel / np.sum(blur_kernel) 21 | elif self.kernel_size == 5: 22 | blur_kernel = np.array([[1, 4, 6, 4, 1], 23 | [4, 16, 24, 16, 4], 24 | [6, 24, 36, 24, 6], 25 | [4, 16, 24, 16, 4], 26 | [1, 4, 6, 4, 1]]) 27 | blur_kernel = blur_kernel / np.sum(blur_kernel) 28 | else: 29 | raise ValueError 30 | 31 | blur_kernel = np.repeat(blur_kernel, input_shape[3]) 32 | 33 | blur_kernel = np.reshape(blur_kernel, (self.kernel_size, self.kernel_size, input_shape[3], 1)) 34 | blur_init = tf.keras.initializers.constant(blur_kernel) 35 | 36 | self.blur_kernel = self.add_weight(name='blur_kernel', 37 | shape=(self.kernel_size, self.kernel_size, input_shape[3], 1), 38 | initializer=blur_init, 39 | trainable=False) 40 | 41 | super(BlurPool2D, self).build(input_shape) # Be sure to call this at the end 42 | 43 | def call(self, x): 44 | x = K.depthwise_conv2d(x, self.blur_kernel, padding='same', strides=(self.pool_size, self.pool_size)) 45 | 46 | return x 47 | 48 | def compute_output_shape(self, input_shape): 49 | return input_shape[0], int(np.ceil(input_shape[1] / 2)), int(np.ceil(input_shape[2] / 2)), input_shape[3] 50 | 51 | 52 | class BlurPool1D(Layer): 53 | 54 | def __init__(self, pool_size: int = 2, kernel_size: int = 3, **kwargs): 55 | self.pool_size = pool_size 56 | self.blur_kernel = None 57 | self.kernel_size = kernel_size 58 | 59 | super(BlurPool1D, self).__init__(**kwargs) 60 | 61 | def build(self, input_shape): 62 | 63 | if self.kernel_size == 3: 64 | blur_kernel = np.array([2, 4, 2]) 65 | elif self.kernel_size == 5: 66 | blur_kernel = np.array([6, 24, 36, 24, 6]) 67 | else: 68 | raise ValueError 69 | 70 | blur_kernel = blur_kernel / np.sum(blur_kernel) 71 | blur_kernel = np.repeat(blur_kernel, input_shape[2]) 72 | blur_kernel = np.reshape(blur_kernel, (self.kernel_size, 1, input_shape[2], 1)) 73 | blur_init = tf.keras.initializers.constant(blur_kernel) 74 | 75 | self.blur_kernel = self.add_weight(name='blur_kernel', 76 | shape=(self.kernel_size, 1, input_shape[2], 1), 77 | initializer=blur_init, 78 | trainable=False) 79 | 80 | super(BlurPool1D, self).build(input_shape) # Be sure to call this at the end 81 | 82 | def call(self, x): 83 | 84 | x = K.expand_dims(x, axis=-2) 85 | x = K.depthwise_conv2d(x, self.blur_kernel, padding='same', strides=(self.pool_size, self.pool_size)) 86 | x = K.squeeze(x, axis=-2) 87 | 88 | return x 89 | 90 | def compute_output_shape(self, input_shape): 91 | return input_shape[0], int(np.ceil(input_shape[1] / 2)), input_shape[2] -------------------------------------------------------------------------------- /BlurPooling/MaxBlurPooling.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.keras.layers import Layer 3 | from tensorflow.keras import backend as K 4 | import numpy as np 5 | 6 | class MaxBlurPooling1D(Layer): 7 | def __init__(self, pool_size=2, kernel_size=3, **kwargs): 8 | super().__init__(**kwargs) 9 | self._pool_size = pool_size 10 | self._kernel_size = kernel_size 11 | self._avg_kernel = None 12 | self._blur_kernel = None 13 | 14 | def build(self, input_shape): 15 | if self._kernel_size == 3: 16 | blur_kernel = np.array([2, 4, 2]) 17 | elif self._kernel_size == 5: 18 | blur_kernel = np.array([6, 24, 36, 24, 6]) 19 | else: 20 | raise ValueError 21 | blur_kernel = blur_kernel/np.sum(blur_kernel) 22 | blur_kernel = np.repeat(blur_kernel, input_shape[2]) 23 | blur_kernel = np.reshape(blur_kernel, (self._kernel_size, 1, input_shape[2], 1)) 24 | blur_init = tf.keras.initializers.constant(blur_kernel) 25 | 26 | self._blur_kernel = self.add_weight(name='blur_kernel', shape=(self._kernel_size, 1, input_shape[2], 1), initializer=blur_init, trainable=False) 27 | super(MaxBlurPooling1D,self).build(input_shape) 28 | 29 | def call(self, x): 30 | x = tf.nn.pool(x, (self._pool_size, ), strides=(1, ), padding='SAME', pooling_type='MAX',data_format='NWC') 31 | x = K.expand_dims(x, axis=-2) 32 | x = K.depthwise_conv2d(x, self._blur_kernel, padding='same', strides=(self._pool_size, self._pool_size)) 33 | x = K.squeeze(x, axis=-2) 34 | return x 35 | def compute_output_shape(self, input_shape): 36 | return input_shape[0], int(np.ceil(input_shape[1]/2)), input_shape[2] 37 | 38 | class MaxBlurPooling2D(Layer): 39 | 40 | def __init__(self, pool_size: int = 2, kernel_size: int = 3, **kwargs): 41 | self.pool_size = pool_size 42 | self.blur_kernel = None 43 | self.kernel_size = kernel_size 44 | 45 | super(MaxBlurPooling2D, self).__init__(**kwargs) 46 | 47 | def build(self, input_shape): 48 | 49 | if self.kernel_size == 3: 50 | blur_kernel = np.array([[1, 2, 1], 51 | [2, 4, 2], 52 | [1, 2, 1]]) 53 | blur_kernel = blur_kernel / np.sum(blur_kernel) 54 | elif self.kernel_size == 5: 55 | blur_kernel = np.array([[1, 4, 6, 4, 1], 56 | [4, 16, 24, 16, 4], 57 | [6, 24, 36, 24, 6], 58 | [4, 16, 24, 16, 4], 59 | [1, 4, 6, 4, 1]]) 60 | blur_kernel = blur_kernel / np.sum(blur_kernel) 61 | else: 62 | raise ValueError 63 | 64 | blur_kernel = np.repeat(blur_kernel, input_shape[3]) 65 | 66 | blur_kernel = np.reshape(blur_kernel, (self.kernel_size, self.kernel_size, input_shape[3], 1)) 67 | blur_init =tf.keras.initializers.constant(blur_kernel) 68 | 69 | self.blur_kernel = self.add_weight(name='blur_kernel', 70 | shape=(self.kernel_size, self.kernel_size, input_shape[3], 1), 71 | initializer=blur_init, 72 | trainable=False) 73 | 74 | super(MaxBlurPooling2D, self).build(input_shape) # Be sure to call this at the end 75 | 76 | def call(self, x): 77 | 78 | x = tf.nn.pool(x, (self.pool_size, self.pool_size), 79 | strides=(1, 1), padding='SAME', pooling_type='MAX', data_format='NHWC') 80 | x = K.depthwise_conv2d(x, self.blur_kernel, padding='same', strides=(self.pool_size, self.pool_size)) 81 | 82 | return x 83 | 84 | def compute_output_shape(self, input_shape): 85 | return input_shape[0], int(np.ceil(input_shape[1] / 2)), int(np.ceil(input_shape[2] / 2)), input_shape[3] -------------------------------------------------------------------------------- /BlurPooling/AverageBlurPooling.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.keras.layers import Layer 3 | from tensorflow.keras import backend as K 4 | import numpy as np 5 | 6 | class AverageBlurPooling1D(Layer): 7 | 8 | def __init__(self, pool_size: int = 2, kernel_size: int = 3, **kwargs): 9 | self.pool_size = pool_size 10 | self.blur_kernel = None 11 | self.kernel_size = kernel_size 12 | 13 | super(AverageBlurPooling1D, self).__init__(**kwargs) 14 | 15 | def build(self, input_shape): 16 | 17 | if self.kernel_size == 3: 18 | blur_kernel = np.array([2, 4, 2]) 19 | elif self.kernel_size == 5: 20 | blur_kernel = np.array([6, 24, 36, 24, 6]) 21 | else: 22 | raise ValueError 23 | 24 | blur_kernel = blur_kernel / np.sum(blur_kernel) 25 | blur_kernel = np.repeat(blur_kernel, input_shape[2]) 26 | blur_kernel = np.reshape(blur_kernel, (self.kernel_size, 1, input_shape[2], 1)) 27 | blur_init =tf.keras.initializers.constant(blur_kernel) 28 | 29 | self.blur_kernel = self.add_weight(name='blur_kernel', 30 | shape=(self.kernel_size, 1, input_shape[2], 1), 31 | initializer=blur_init, 32 | trainable=False) 33 | 34 | super(AverageBlurPooling1D, self).build(input_shape) # Be sure to call this at the end 35 | 36 | def call(self, x): 37 | 38 | x = tf.nn.pool(x, (self.pool_size,), strides=(1,), padding='SAME', pooling_type='AVG', 39 | data_format='NWC') 40 | x = K.expand_dims(x, axis=-2) 41 | x = K.depthwise_conv2d(x, self.blur_kernel, padding='same', strides=(self.pool_size, self.pool_size)) 42 | x = K.squeeze(x, axis=-2) 43 | 44 | return x 45 | 46 | def compute_output_shape(self, input_shape): 47 | return input_shape[0], int(np.ceil(input_shape[1] / 2)), input_shape[2] 48 | 49 | class AverageBlurPooling2D(Layer): 50 | 51 | def __init__(self, pool_size: int = 2, kernel_size: int = 3, **kwargs): 52 | self.pool_size = pool_size 53 | self.blur_kernel = None 54 | self.kernel_size = kernel_size 55 | 56 | super(AverageBlurPooling2D, self).__init__(**kwargs) 57 | 58 | def build(self, input_shape): 59 | 60 | if self.kernel_size == 3: 61 | blur_kernel = np.array([[1, 2, 1], 62 | [2, 4, 2], 63 | [1, 2, 1]]) 64 | blur_kernel = blur_kernel / np.sum(blur_kernel) 65 | elif self.kernel_size == 5: 66 | blur_kernel = np.array([[1, 4, 6, 4, 1], 67 | [4, 16, 24, 16, 4], 68 | [6, 24, 36, 24, 6], 69 | [4, 16, 24, 16, 4], 70 | [1, 4, 6, 4, 1]]) 71 | blur_kernel = blur_kernel / np.sum(blur_kernel) 72 | else: 73 | raise ValueError 74 | 75 | blur_kernel = np.repeat(blur_kernel, input_shape[3]) 76 | 77 | blur_kernel = np.reshape(blur_kernel, (self.kernel_size, self.kernel_size, input_shape[3], 1)) 78 | blur_init = tf.keras.initializers.constant(blur_kernel) 79 | 80 | self.blur_kernel = self.add_weight(name='blur_kernel', 81 | shape=(self.kernel_size, self.kernel_size, input_shape[3], 1), 82 | initializer=blur_init, 83 | trainable=False) 84 | 85 | super(AverageBlurPooling2D, self).build(input_shape) # Be sure to call this at the end 86 | 87 | def call(self, x): 88 | 89 | x = tf.nn.pool(x, (self.pool_size, self.pool_size), strides=(1, 1), padding='SAME', pooling_type='AVG', 90 | data_format='NHWC') 91 | x = K.depthwise_conv2d(x, self.blur_kernel, padding='same', strides=(self.pool_size, self.pool_size)) 92 | 93 | return x 94 | 95 | def compute_output_shape(self, input_shape): 96 | return input_shape[0], int(np.ceil(input_shape[1] / 2)), int(np.ceil(input_shape[2] / 2)), input_shape[3] -------------------------------------------------------------------------------- /sacled_mnist.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | import tensorflow as tf 4 | import tensorflow.keras.backend as K 5 | from tensorflow.keras.models import Model 6 | from tensorflow.keras.losses import categorical_crossentropy 7 | from tensorflow.keras.optimizers import Adam, SGD 8 | from Deformable_Conv.layers import ConvOffset2D 9 | from Deformable_Conv.callbacks import TensorBoard 10 | from Deformable_Conv.network import build_cnn, build_deform_cnn 11 | from Deformable_Conv.dataset import get_gen 12 | config = tf.ConfigProto() 13 | config.gpu_options.allow_growth = True 14 | sess = tf.Session(config=config) 15 | K.set_session(sess) 16 | 17 | # --- 18 | # Config 19 | 20 | batch_size = 32 21 | n_train = 60000 22 | n_test = 10000 23 | steps_per_epoch = int(np.ceil(n_train / batch_size)) 24 | validation_steps = int(np.ceil(n_test / batch_size)) 25 | 26 | train_gen = get_gen( 27 | 'train', batch_size=batch_size, 28 | scale=(1.0, 1.0), translate=0.0, 29 | shuffle=True 30 | ) 31 | test_gen = get_gen( 32 | 'test', batch_size=batch_size, 33 | scale=(1.0, 1.0), translate=0.0, 34 | shuffle=False 35 | ) 36 | train_scaled_gen = get_gen( 37 | 'train', batch_size=batch_size, 38 | scale=(1.0, 2.5), translate=0.2, 39 | shuffle=True 40 | ) 41 | test_scaled_gen = get_gen( 42 | 'test', batch_size=batch_size, 43 | scale=(1.0, 2.5), translate=0.2, 44 | shuffle=False 45 | ) 46 | 47 | 48 | # --- 49 | # Normal CNN 50 | 51 | inputs, outputs = build_cnn() 52 | model = Model(inputs=inputs, outputs=outputs) 53 | model.summary() 54 | optim = Adam(1e-3) 55 | # optim = SGD(1e-3, momentum=0.99, nesterov=True) 56 | loss = categorical_crossentropy 57 | model.compile(optim, loss, metrics=['accuracy']) 58 | 59 | model.fit_generator( 60 | train_gen, steps_per_epoch=steps_per_epoch, 61 | epochs=10, verbose=1, 62 | validation_data=test_gen, validation_steps=validation_steps 63 | ) 64 | # model.save_weights('models/cnn.h5') 65 | # # 1875/1875 [==============================] - 24s - loss: 0.0090 - acc: 0.9969 - val_loss: 0.0528 - val_acc: 0.9858 66 | 67 | # # --- 68 | # # Evaluate normal CNN 69 | 70 | # model.load_weights('models/cnn.h5', by_name=True) 71 | 72 | val_loss, val_acc = model.evaluate_generator( 73 | test_gen, steps=validation_steps 74 | ) 75 | print('Test accuracy', val_acc) 76 | # 0.9874 77 | 78 | val_loss, val_acc = model.evaluate_generator( 79 | test_scaled_gen, steps=validation_steps 80 | ) 81 | print('Test accuracy with scaled images', val_acc) 82 | # 0.5701 83 | 84 | # --- 85 | # Deformable CNN 86 | 87 | inputs, outputs = build_deform_cnn(trainable=False) 88 | model = Model(inputs=inputs, outputs=outputs) 89 | # model.load_weights('models/cnn.h5', by_name=True) 90 | model.summary() 91 | optim = Adam(5e-4) 92 | # optim = SGD(1e-4, momentum=0.99, nesterov=True) 93 | loss = categorical_crossentropy 94 | model.compile(optim, loss, metrics=['accuracy']) 95 | 96 | model.fit_generator( 97 | train_scaled_gen, steps_per_epoch=steps_per_epoch, 98 | epochs=20, verbose=1, 99 | validation_data=test_scaled_gen, validation_steps=validation_steps 100 | ) 101 | # Epoch 20/20 102 | # 1875/1875 [==============================] - 504s - loss: 0.2838 - acc: 0.9122 - val_loss: 0.2359 - val_acc: 0.9231 103 | # model.save_weights('models/deform_cnn.h5') 104 | 105 | # # -- 106 | # # Evaluate deformable CNN 107 | 108 | # model.load_weights('models/deform_cnn.h5') 109 | 110 | val_loss, val_acc = model.evaluate_generator( 111 | test_scaled_gen, steps=validation_steps 112 | ) 113 | print('Test accuracy of deformable convolution with scaled images', val_acc) 114 | # 0.9255 115 | 116 | val_loss, val_acc = model.evaluate_generator( 117 | test_gen, steps=validation_steps 118 | ) 119 | print('Test accuracy of deformable convolution with regular images', val_acc) 120 | # 0.9727 121 | 122 | deform_conv_layers = [l for l in model.layers if isinstance(l, ConvOffset2D)] 123 | 124 | Xb, Yb = next(test_gen) 125 | for l in deform_conv_layers: 126 | print(l) 127 | _model = Model(inputs=inputs, outputs=l.output) 128 | offsets = _model.predict(Xb) 129 | offsets = offsets.reshape(offsets.shape[0], offsets.shape[1], offsets.shape[2], -1, 2) 130 | print(offsets.min()) 131 | print(offsets.mean()) 132 | print(offsets.max()) -------------------------------------------------------------------------------- /Deformable_Conv/callbacks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | import numpy as np 3 | import tensorflow as tf 4 | from tensorflow.keras.callbacks import Callback 5 | import tensorflow.keras.backend as K 6 | 7 | 8 | class TensorBoard(Callback): 9 | """Tensorboard basic visualizations""" 10 | 11 | def __init__(self, log_dir='./logs', 12 | histogram_freq=0, 13 | write_graph=True, 14 | write_images=False): 15 | super(TensorBoard, self).__init__() 16 | if K.backend() != 'tensorflow': 17 | raise RuntimeError('TensorBoard callback only works ' 18 | 'with the TensorFlow backend.') 19 | self.log_dir = log_dir 20 | self.histogram_freq = histogram_freq 21 | self.merged = None 22 | self.write_graph = write_graph 23 | self.write_images = write_images 24 | 25 | def set_model(self, model): 26 | self.model = model 27 | self.sess = K.get_session() 28 | total_loss = self.model.total_loss 29 | if self.histogram_freq and self.merged is None: 30 | for layer in self.model.layers: 31 | for weight in layer.weights: 32 | # dense_1/bias:0 > dense_1/bias_0 33 | name = weight.name.replace(':', '_') 34 | tf.summary.histogram(name, weight) 35 | tf.summary.histogram( 36 | '{}_gradients'.format(name), 37 | K.gradients(total_loss, [weight])[0] 38 | ) 39 | if self.write_images: 40 | w_img = tf.squeeze(weight) 41 | shape = w_img.get_shape() 42 | if len(shape) > 1 and shape[0] > shape[1]: 43 | w_img = tf.transpose(w_img) 44 | if len(shape) == 1: 45 | w_img = tf.expand_dims(w_img, 0) 46 | w_img = tf.expand_dims(tf.expand_dims(w_img, 0), -1) 47 | tf.summary.image(name, w_img) 48 | 49 | if hasattr(layer, 'output'): 50 | tf.summary.histogram('{}_out'.format(layer.name), 51 | layer.output) 52 | self.merged = tf.summary.merge_all() 53 | 54 | if self.write_graph: 55 | self.writer = tf.summary.FileWriter(self.log_dir, 56 | self.sess.graph) 57 | else: 58 | self.writer = tf.summary.FileWriter(self.log_dir) 59 | 60 | def on_epoch_end(self, epoch, logs=None): 61 | logs = logs or {} 62 | 63 | if self.validation_data and self.histogram_freq: 64 | if epoch % self.histogram_freq == 0: 65 | # TODO: implement batched calls to sess.run 66 | # (current call will likely go OOM on GPU) 67 | if self.model.uses_learning_phase: 68 | cut_v_data = len(self.model.inputs) 69 | val_data = self.validation_data[:cut_v_data][:32] + [0] 70 | tensors = self.model.inputs + self.model.targets + [K.learning_phase()] 71 | else: 72 | val_data = self.validation_data 73 | tensors = self.model.inputs + self.model.targets 74 | 75 | feed_dict = dict(zip(tensors, val_data)) 76 | sample_weights = self.model.sample_weights 77 | for w in sample_weights: 78 | w_val = np.ones(len(val_data[0]), dtype=np.float32) 79 | feed_dict.update({w.name: w_val}) 80 | result = self.sess.run([self.merged], feed_dict=feed_dict) 81 | summary_str = result[0] 82 | self.writer.add_summary(summary_str, epoch) 83 | 84 | for name, value in logs.items(): 85 | if name in ['batch', 'size']: 86 | continue 87 | 88 | if name[:3] != 'val': 89 | name = 'train_' + name 90 | 91 | summary = tf.Summary() 92 | summary_value = summary.value.add() 93 | summary_value.simple_value = value.item() 94 | summary_value.tag = name 95 | self.writer.add_summary(summary, epoch) 96 | self.writer.flush() 97 | 98 | def on_train_end(self, _): 99 | self.writer.close() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CNNComponent 2 | 3 | 基于`Tensorflow2.X`实现卷积神经网络即插即用模块。 4 | 5 | ## 已实现 6 | - [x] STN 7 | - [x] SE 8 | - [x] CBAM 9 | - [x] non_local 10 | - [x] blur pooling 11 | - [x] Deformable Conv 12 | 13 | ## STN 14 | 主要参考:[Hands-on: implement a spatial transformer network by yourself](https://xeonqq.github.io/machine%20learning/spatial-transformer-networks/)。简单的理论部分可以参考我的博客:[深度学习 卷积神经网络即插即用的小插件](https://blog.csdn.net/u012655441/article/details/121919291)。STN结构如下图所示: 15 | ![image](https://user-images.githubusercontent.com/27406337/145952361-5d738cbc-ca73-40ce-bd89-4244b81358d6.png) 16 | 里面包括三个组件: 17 | - **Localization net**:该网络可以是卷积神经网络或者是全连接神经网络,它们有个特点是最后一层是一个回归层,主要生成6个值表示仿射变换的参数θ。 18 | - **Grid Generator**:它首先在目标图像V上生成一个网格,网格的每个点刚好对应目标图像中每个像素的像素坐标。然后它使用Localization net生成的θ来变换网格。 19 | - **Sampler**:变换后的网格就像源图像U上的遮罩,它检索遮罩下的像素。然而,变换的网格不再包含整数值,因此对源图像U执行双线性插值,以获得变换网格下的估计像素值。 20 | 21 | ### Localization Net 22 | 23 | Localization Net输入为\[批量大小、高度、宽度、通道]的输入图像,并为每个维度的输入图像生成转换参数。转换的维度为\[batch_size,6]。 24 | ```python 25 | def create_localization_head(inputs): 26 | x = Conv2D(14, (5,5),padding='valid',activation="relu")(inputs) 27 | x = MaxPooling2D((2, 2), strides=2)(x) 28 | x = Conv2D(32, (5,5), padding='valid',activation="relu")(x) 29 | x = MaxPooling2D((2, 2),strides=2)(x) 30 | x = Flatten()(x) 31 | x = Dense(120, activation='relu')(x) 32 | x = Dropout(0.2)(x) 33 | x = Dense(84, activation='relu')(x) 34 | x = Dense(6, activation="linear", kernel_initializer="zeros", 35 | bias_initializer=lambda shape, dtype: tf.constant([1,0,0,0,1,0], dtype=dtype))(x) # 6 elements to describe the transformation 36 | return tf.keras.Model(inputs, x) 37 | ``` 38 | 39 | ### Grid Generator 40 | 41 | 在网格生成器中,必须注意,变换θ应用于从目标图像V而不是源图像U生成的网格,在图像处理领域称为逆映射。另一方面,如果我们将源图像U转换为目标图像V,这个过程称为前向映射。 42 | 43 | **正向映射**迭代输入图像的每个像素,为其计算新坐标,并将其值复制到新位置。但新坐标可能不在输出图像的边界内,也可能不是整数。通过在复制像素值之前检查计算的坐标,前一个问题很容易解决。第二个问题通过将最近的整数指定给x′和y′并将其用作变换像素的输出坐标来解决。问题在于,每个输出像素可能会被寻址多次或根本不寻址(后一种情况会导致“孔”,其中输出图像中的像素没有赋值)。**逆映射**迭代输出图像的每个像素,并使用逆变换确定输入图像中必须从中采样值的位置。在这种情况下,确定的位置也可能不在输入图像的边界内,也可能不是整数。但是输出图像没有孔。 44 | 45 | ```python 46 | def generate_normalized_homo_meshgrids(inputs): 47 | # for x, y in grid, -1 <=x,y<=1 48 | batch_size = tf.shape(inputs)[0] 49 | _, H, W,_ = inputs.shape 50 | x_range = tf.range(W) 51 | y_range = tf.range(H) 52 | x_mesh, y_mesh = tf.meshgrid(x_range, y_range) 53 | x_mesh = (x_mesh/W-0.5)*2 54 | y_mesh = (y_mesh/H-0.5)*2 55 | y_mesh = tf.reshape(y_mesh, (*y_mesh.shape,1)) 56 | x_mesh = tf.reshape(x_mesh, (*x_mesh.shape,1)) 57 | ones_mesh = tf.ones_like(x_mesh) 58 | homogeneous_grid = tf.concat([x_mesh, y_mesh, ones_mesh],-1) 59 | homogeneous_grid = tf.reshape(homogeneous_grid, (-1, 3,1)) 60 | homogeneous_grid = tf.dtypes.cast(homogeneous_grid, tf.float32) 61 | homogeneous_grid = tf.expand_dims(homogeneous_grid, 0) 62 | return tf.tile(homogeneous_grid, [batch_size, 1,1,1]) 63 | ``` 64 | 65 | 在```generate_normalized_homo_meshgrid```s函数中,给定输入维数,我们可以生成一个```meshgrid```。然后在[-1,1]之间对网格网格进行规格化,以便相对于图像中心执行旋转或平移。每个网格还扩展了第三维,并填充了第三维,因此被称为均质网格,在以下变换网格中更方便地执行变换。 66 | 67 | 在变换网格中,我们将从本地化网络生成的变换应用到从generate_normalized_homo_meshgrids生成的网格上,以获得重新```reprojected_grids```。变换后,```reprojected_grids```将重新缩放回输入图像的宽度和高度范围内。 68 | 69 | ### Sampler 70 | ```python 71 | def generate_four_neighbors_from_reprojection(inputs, reprojected_grids): 72 | _, H, W, _ = inputs.shape 73 | x, y = tf.split(reprojected_grids, 2, axis=-1) 74 | x1 = tf.floor(x) 75 | x1 = tf.dtypes.cast(x1, tf.int32) 76 | x2 = x1 + tf.constant(1) 77 | y1 = tf.floor(y) 78 | y1 = tf.dtypes.cast(y1, tf.int32) 79 | y2 = y1 + tf.constant(1) 80 | y_max = tf.constant(H - 1, dtype=tf.int32) 81 | x_max = tf.constant(W - 1, dtype=tf.int32) 82 | zero = tf.zeros([1], dtype=tf.int32) 83 | x1_safe = tf.clip_by_value(x1, zero, x_max) 84 | y1_safe = tf.clip_by_value(y1, zero, y_max) 85 | x2_safe = tf.clip_by_value(x2, zero, x_max) 86 | y2_safe = tf.clip_by_value(y2, zero, y_max) 87 | return x1_safe, y1_safe, x2_safe, y2_safe 88 | 89 | def bilinear_sample(inputs, reprojected_grids): 90 | x1, y1, x2, y2 = generate_four_neighbors_from_reprojection(inputs, reprojected_grids) 91 | x1y1 = tf.concat([y1,x1],-1) 92 | x1y2 = tf.concat([y2,x1],-1) 93 | x2y1 = tf.concat([y1,x2],-1) 94 | x2y2 = tf.concat([y2,x2],-1) 95 | pixel_x1y1 = tf.gather_nd(inputs, x1y1, batch_dims=1) 96 | pixel_x1y2 = tf.gather_nd(inputs, x1y2, batch_dims=1) 97 | pixel_x2y1 = tf.gather_nd(inputs, x2y1, batch_dims=1) 98 | pixel_x2y2 = tf.gather_nd(inputs, x2y2, batch_dims=1) 99 | x, y = tf.split(reprojected_grids, 2, axis=-1) 100 | wx = tf.concat([tf.dtypes.cast(x2, tf.float32) - x, x -tf.dtypes.cast(x1, tf.float32)],-1) 101 | wx = tf.expand_dims(wx, -2) 102 | wy = tf.concat([tf.dtypes.cast(y2, tf.float32) - y, y - tf.dtypes.cast(y1, tf.float32)],-1) 103 | wy = tf.expand_dims(wy, -1) 104 | Q = tf.concat([pixel_x1y1, pixel_x1y2, pixel_x2y1, pixel_x2y2], -1) 105 | Q_shape = tf.shape(Q) 106 | Q = tf.reshape(Q, (Q_shape[0], Q_shape[1],2,2)) 107 | Q = tf.cast(Q, tf.float32) 108 | 109 | r = wx@Q@wy 110 | _, H, W, channels = inputs.shape 111 | r = tf.reshape(r, (-1,H,W,1)) 112 | return r 113 | ``` 114 | 115 | ## Non-Local 116 | ![image](https://user-images.githubusercontent.com/27406337/146329854-5e1f5d7c-b69d-493e-8f88-60019b0eaae8.png) 117 | 118 | 119 | ## Deformable Convolution 120 | 121 | 参考:https://github.com/kastnerkyle/deform-conv 122 | -------------------------------------------------------------------------------- /non_local.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.keras.layers import Activation,Reshape, Lambda 3 | from tensorflow.keras.layers import Conv1D, Conv2D, Conv3D 4 | from tensorflow.keras.layers import MaxPool1D 5 | from tensorflow.keras import backend as K 6 | from tensorflow.python.keras.layers.merge import add, dot 7 | 8 | 9 | def _convND(input, rank, channels): 10 | assert rank in [3, 4, 5], "Rank of input must be 3, 4 or 5" 11 | if rank == 3: 12 | x = Conv1D(channels, 1, padding='same', kernel_initializer = 'he_normal')(input) 13 | elif rank == 4: 14 | x = Conv2D(channels, (1, 1), padding='same', use_bias=False, kernel_initializer='he_normal')(input) 15 | else: 16 | x = Conv3D(channels, (1, 1, 1), padding='same', use_bias=False, kernel_initializer='he_normal')(input) 17 | return x 18 | 19 | 20 | def non_local_block(input, intermediate_dim=None, compression=2, mode='embedded', add_residual=True): 21 | ''' 22 | Adds a Non-Local block for self attention to the input tensor. 23 | Input tensor can be or rank 3(temporal), 4(spatial) or 5(spatio-temporal) 24 | 25 | Arguments: 26 | input:input tensor 27 | intermediate_dim: The dimension of the intermediate representation 28 | compression: None or positive integer. 29 | mode: Mode of operation 30 | add_residual: Boolean value to decide if the residual connection should be added or not. 31 | 32 | Returns: 33 | a tensor of same shape of input 34 | ''' 35 | # 获取通道数所在的维度 36 | channel_dim =1 if K.image_data_format() == 'channel_first' else -1 37 | input_shape = K.int_shape(input) 38 | if mode not in ['gaussian', 'embedded', 'dot', 'concatenate']: 39 | raise ValueError('`mode` must be one of `gaussian`, `embedded`, `dot` or `concatenate`') 40 | 41 | if compression is None: 42 | compression = 1 43 | 44 | # check rank and calculate the input shape 45 | if len(input_shape) == 3: # temporal / time series data 46 | rank = 3 47 | batchsize, dim1, channels = input_shape 48 | 49 | elif len(input_shape) == 4: # spatial / image data 50 | rank = 4 51 | 52 | if channel_dim == 1: 53 | batchsize, channels, dim1, dim2 = input_shape 54 | else: 55 | batchsize, dim1, dim2, channels = input_shape 56 | 57 | elif len(input_shape) == 5: # spatio-temporal / Video or Voxel data 58 | rank = 5 59 | 60 | if channel_dim == 1: 61 | batchsize, channels, dim1, dim2, dim3 = input_shape 62 | else: 63 | batchsize, dim1, dim2, dim3, channels = input_shape 64 | 65 | else: 66 | raise ValueError('Input dimension has to be either 3 (temporal), 4 (spatial) or 5 (spatio-temporal)') 67 | 68 | if intermediate_dim is None: 69 | intermediate_dim=channels//2 70 | 71 | if intermediate_dim<1: 72 | intermediate_dim=1 73 | else: 74 | intermediate_dim = int(intermediate_dim) 75 | if intermediate_dim<1: 76 | raise ValueError('`intermediate_dim` must be either `None` or positive integer greater than 1.') 77 | 78 | # instantiation 79 | if mode == 'gaussian': 80 | x1 = Reshape((-1, channels))(input) 81 | x2 = Reshape((-1, channels))(input) 82 | f = dot([x1, x2], axes=2) 83 | f = Activation('softmax')(f) 84 | elif mode == 'dot': 85 | # theta path 86 | theta = _convND(input, rank, intermediate_dim) 87 | theta = Reshape((-1, intermediate_dim))(theta) 88 | 89 | # phi path 90 | phi = _convND(input, rank, intermediate_dim) 91 | phi = Reshape((-1, intermediate_dim))(phi) 92 | 93 | f = dot([theta, phi], axes=2) 94 | 95 | size = K.int_shape(f) 96 | 97 | # scale the values to make it size invariant 98 | f = Lambda(lambda z: (1. / float(size[-1])) * z)(f) 99 | else: 100 | # theta path 101 | theta = _convND(input, rank, intermediate_dim) 102 | theta = Reshape((-1, intermediate_dim))(theta) 103 | 104 | # phi path 105 | phi = _convND(input, rank, intermediate_dim) 106 | phi = Reshape((-1, intermediate_dim))(phi) 107 | 108 | if compression > 1: 109 | # shielded computation 110 | phi = MaxPool1D(compression)(phi) 111 | 112 | f = dot([theta, phi], axes=2) 113 | f = Activation('softmax')(f) 114 | 115 | # g path 116 | g = _convND(input, rank, intermediate_dim) 117 | g = Reshape((-1, intermediate_dim))(g) 118 | 119 | if compression > 1 and mode == 'embedded': 120 | # shielded computation 121 | g = MaxPool1D(compression)(g) 122 | 123 | # compute output path 124 | y = dot([f, g], axes=[2, 1]) 125 | 126 | # reshape to input tensor format 127 | if rank == 3: 128 | y = Reshape((dim1, intermediate_dim))(y) 129 | elif rank == 4: 130 | if channel_dim == -1: 131 | y = Reshape((dim1, dim2, intermediate_dim))(y) 132 | else: 133 | y = Reshape((intermediate_dim, dim1, dim2))(y) 134 | else: 135 | if channel_dim == -1: 136 | y = Reshape((dim1, dim2, dim3, intermediate_dim))(y) 137 | else: 138 | y = Reshape((intermediate_dim, dim1, dim2, dim3))(y) 139 | 140 | # project filters 141 | y = _convND(y, rank, channels) 142 | 143 | # residual connection 144 | if add_residual: 145 | y = add([input, y]) 146 | 147 | return y 148 | -------------------------------------------------------------------------------- /stn_module.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import numpy as np 3 | from tensorflow.keras.layers import Conv2D, MaxPooling2D,Flatten,Dense,Input,Dropout 4 | ''' 5 | 以DenseNet为例,添加stn模块 6 | https://github.com/xeonqq/spatial_transformer_network 7 | ''' 8 | 9 | 10 | def create_localizaton_head(inputs): 11 | x = Conv2D(14, (5, 5), padding='valid', activation='relu')(inputs) 12 | x = MaxPooling2D((2, 2), strides=2)(x) 13 | x = Conv2D(32, (5, 5), padding='valid', activation='relu')(x) 14 | x = MaxPooling2D((2,2), strides=2)(x) 15 | x = Flatten()(x) 16 | x = Dense(120,activation='relu')(x) 17 | x = Dropout(0.2)(x) 18 | x = Dense(84, activation='relu')(x) 19 | # 6 elements to describe the transformation 20 | x = Dense(6, activation='linear', kernel_initializer='zeros', 21 | bias_initializer=lambda shape, dtype:tf.constant([1,0,0,0,1,0], dtype=dtype))(x) 22 | return tf.keras.Model(inputs, x) 23 | 24 | def generate_normalized_homo_meshgrids(inputs): 25 | # for x, y in grid, -1 <=x,y<=1 26 | batch_size = tf.shape(inputs)[0] 27 | _, H, W,_ = inputs.shape 28 | x_range = tf.range(W) 29 | y_range = tf.range(H) 30 | x_mesh, y_mesh = tf.meshgrid(x_range, y_range) 31 | x_mesh = (x_mesh/W-0.5)*2 32 | y_mesh = (y_mesh/H-0.5)*2 33 | y_mesh = tf.reshape(y_mesh, (*y_mesh.shape,1)) 34 | x_mesh = tf.reshape(x_mesh, (*x_mesh.shape,1)) 35 | ones_mesh = tf.ones_like(x_mesh) 36 | homogeneous_grid = tf.concat([x_mesh, y_mesh, ones_mesh],-1) 37 | homogeneous_grid = tf.reshape(homogeneous_grid, (-1, 3,1)) 38 | homogeneous_grid = tf.dtypes.cast(homogeneous_grid, tf.float32) 39 | homogeneous_grid = tf.expand_dims(homogeneous_grid, 0) 40 | return tf.tile(homogeneous_grid, [batch_size, 1,1,1]) 41 | 42 | def transform_grids(transformations, grids, inputs): 43 | with tf.name_scope("transform_grids"): 44 | trans_matrices=tf.reshape(transformations, (-1, 2,3)) 45 | batch_size = tf.shape(trans_matrices)[0] 46 | gs = tf.squeeze(grids, -1) 47 | 48 | reprojected_grids = tf.matmul(trans_matrices, gs, transpose_b=True) 49 | # transform grid range from [-1,1) to the range of [0,1) 50 | reprojected_grids = (tf.linalg.matrix_transpose(reprojected_grids) + 1)*0.5 51 | _, H, W, _ = inputs.shape 52 | reprojected_grids = tf.math.multiply(reprojected_grids, [W, H]) 53 | 54 | return reprojected_grids 55 | 56 | def generate_four_neighbors_from_reprojection(inputs, reprojected_grids): 57 | _, H, W, _ = inputs.shape 58 | 59 | x, y = tf.split(reprojected_grids, 2, axis=-1) 60 | 61 | x1 = tf.floor(x) 62 | x1 = tf.dtypes.cast(x1, tf.int32) 63 | 64 | x2 = x1 + tf.constant(1) 65 | 66 | y1 = tf.floor(y) 67 | y1 = tf.dtypes.cast(y1, tf.int32) 68 | y2 = y1 + tf.constant(1) 69 | 70 | y_max = tf.constant(H - 1, dtype=tf.int32) 71 | x_max = tf.constant(W - 1, dtype=tf.int32) 72 | zero = tf.zeros([1], dtype=tf.int32) 73 | 74 | x1_safe = tf.clip_by_value(x1, zero, x_max) 75 | y1_safe = tf.clip_by_value(y1, zero, y_max) 76 | x2_safe = tf.clip_by_value(x2, zero, x_max) 77 | y2_safe = tf.clip_by_value(y2, zero, y_max) 78 | return x1_safe, y1_safe, x2_safe, y2_safe 79 | 80 | def bilinear_sample(inputs, reprojected_grids): 81 | x1, y1, x2, y2 = generate_four_neighbors_from_reprojection(inputs, reprojected_grids) 82 | x1y1 = tf.concat([y1,x1],-1) 83 | x1y2 = tf.concat([y2,x1],-1) 84 | x2y1 = tf.concat([y1,x2],-1) 85 | x2y2 = tf.concat([y2,x2],-1) 86 | 87 | pixel_x1y1 = tf.gather_nd(inputs, x1y1, batch_dims=1) 88 | pixel_x1y2 = tf.gather_nd(inputs, x1y2, batch_dims=1) 89 | pixel_x2y1 = tf.gather_nd(inputs, x2y1, batch_dims=1) 90 | pixel_x2y2 = tf.gather_nd(inputs, x2y2, batch_dims=1) 91 | x, y = tf.split(reprojected_grids, 2, axis=-1) 92 | wx = tf.concat([tf.dtypes.cast(x2, tf.float32) - x, x -tf.dtypes.cast(x1, tf.float32)],-1) 93 | wx = tf.expand_dims(wx, -2) 94 | wy = tf.concat([tf.dtypes.cast(y2, tf.float32) - y, y - tf.dtypes.cast(y1, tf.float32)],-1) 95 | wy = tf.expand_dims(wy, -1) 96 | Q = tf.concat([pixel_x1y1, pixel_x1y2, pixel_x2y1, pixel_x2y2], -1) 97 | Q_shape = tf.shape(Q) 98 | Q = tf.reshape(Q, (Q_shape[0], Q_shape[1],2,2)) 99 | Q = tf.cast(Q, tf.float32) 100 | 101 | r = wx@Q@wy 102 | _, H, W, channels = inputs.shape 103 | 104 | r = tf.reshape(r, (-1,H,W,1)) 105 | return r 106 | 107 | def spatial_transform_input(inputs, transormations): 108 | grids = generate_normalized_homo_meshgrids(inputs) 109 | reprojected_grids = transform_grids(transormations, grids,inputs) 110 | result = bilinear_sample(inputs, reprojected_grids) 111 | return result 112 | 113 | def stn_module(inputs): 114 | localication_head = create_localizaton_head(inputs) 115 | x = spatial_transform_input(inputs, localication_head.output) 116 | return x 117 | 118 | def model(input_shape): 119 | inputs = Input(input_shape) 120 | inputs_stn = stn_module(inputs) 121 | x = Conv2D(6, (3,3),padding='valid',activation="relu")(inputs_stn) 122 | x = MaxPooling2D((2, 2))(x) 123 | x = Conv2D(16, (3,3),padding='valid',activation="relu")(x) 124 | x = MaxPooling2D((2, 2))(x) 125 | x = Flatten()(x) 126 | x = Dense(120, activation='relu')(x) 127 | x = Dense(84, activation='relu')(x) 128 | x = Dense(10)(x) 129 | return tf.keras.Model(inputs, x) 130 | 131 | input_shape = (28, 28, 1) 132 | st_model = model(input_shape) 133 | st_model.summary() 134 | 135 | 136 | -------------------------------------------------------------------------------- /Deformable_Conv/deform_conv.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | import numpy as np 3 | from scipy.ndimage.interpolation import map_coordinates as sp_map_coordinates 4 | import tensorflow as tf 5 | 6 | 7 | def tf_flatten(a): 8 | """Flatten tensor""" 9 | return tf.reshape(a, [-1]) 10 | 11 | 12 | def tf_repeat(a, repeats, axis=0): 13 | """TensorFlow version of np.repeat for 1D""" 14 | # https://github.com/tensorflow/tensorflow/issues/8521 15 | assert len(a.get_shape()) == 1 16 | 17 | a = tf.expand_dims(a, -1) 18 | a = tf.tile(a, [1, repeats]) 19 | a = tf_flatten(a) 20 | return a 21 | 22 | 23 | def tf_repeat_2d(a, repeats): 24 | """Tensorflow version of np.repeat for 2D""" 25 | 26 | assert len(a.get_shape()) == 2 27 | a = tf.expand_dims(a, 0) 28 | a = tf.tile(a, [repeats, 1, 1]) 29 | return a 30 | 31 | 32 | def tf_map_coordinates(input, coords, order=1): 33 | """Tensorflow verion of scipy.ndimage.map_coordinates 34 | Note that coords is transposed and only 2D is supported 35 | Parameters 36 | ---------- 37 | input : tf.Tensor. shape = (s, s) 38 | coords : tf.Tensor. shape = (n_points, 2) 39 | """ 40 | 41 | assert order == 1 42 | 43 | coords_lt = tf.cast(tf.floor(coords), 'int32') 44 | coords_rb = tf.cast(tf.math.ceil(coords), 'int32') 45 | coords_lb = tf.stack([coords_lt[:, 0], coords_rb[:, 1]], axis=1) 46 | coords_rt = tf.stack([coords_rb[:, 0], coords_lt[:, 1]], axis=1) 47 | 48 | vals_lt = tf.gather_nd(input, coords_lt) 49 | vals_rb = tf.gather_nd(input, coords_rb) 50 | vals_lb = tf.gather_nd(input, coords_lb) 51 | vals_rt = tf.gather_nd(input, coords_rt) 52 | 53 | coords_offset_lt = coords - tf.cast(coords_lt, 'float32') 54 | vals_t = vals_lt + (vals_rt - vals_lt) * coords_offset_lt[:, 0] 55 | vals_b = vals_lb + (vals_rb - vals_lb) * coords_offset_lt[:, 0] 56 | mapped_vals = vals_t + (vals_b - vals_t) * coords_offset_lt[:, 1] 57 | 58 | return mapped_vals 59 | 60 | 61 | def sp_batch_map_coordinates(inputs, coords): 62 | """Reference implementation for batch_map_coordinates""" 63 | coords = coords.clip(0, inputs.shape[1] - 1) 64 | mapped_vals = np.array([ 65 | sp_map_coordinates(input, coord.T, mode='nearest', order=1) 66 | for input, coord in zip(inputs, coords) 67 | ]) 68 | return mapped_vals 69 | 70 | 71 | def tf_batch_map_coordinates(input, coords, order=1): 72 | """Batch version of tf_map_coordinates 73 | Only supports 2D feature maps 74 | Parameters 75 | ---------- 76 | input : tf.Tensor. shape = (b, s, s) 77 | coords : tf.Tensor. shape = (b, n_points, 2) 78 | """ 79 | 80 | input_shape = tf.shape(input) 81 | batch_size = input_shape[0] 82 | input_size = input_shape[1] 83 | n_coords = tf.shape(coords)[1] 84 | 85 | coords = tf.clip_by_value(coords, 0, tf.cast(input_size, 'float32') - 1) 86 | coords_lt = tf.cast(tf.floor(coords), 'int32') 87 | coords_rb = tf.cast(tf.math.ceil(coords), 'int32') 88 | coords_lb = tf.stack([coords_lt[..., 0], coords_rb[..., 1]], axis=-1) 89 | coords_rt = tf.stack([coords_rb[..., 0], coords_lt[..., 1]], axis=-1) 90 | 91 | idx = tf_repeat(tf.range(batch_size), n_coords) 92 | 93 | def _get_vals_by_coords(input, coords): 94 | indices = tf.stack([ 95 | idx, tf_flatten(coords[..., 0]), tf_flatten(coords[..., 1]) 96 | ], axis=-1) 97 | vals = tf.gather_nd(input, indices) 98 | vals = tf.reshape(vals, (batch_size, n_coords)) 99 | return vals 100 | 101 | vals_lt = _get_vals_by_coords(input, coords_lt) 102 | vals_rb = _get_vals_by_coords(input, coords_rb) 103 | vals_lb = _get_vals_by_coords(input, coords_lb) 104 | vals_rt = _get_vals_by_coords(input, coords_rt) 105 | 106 | coords_offset_lt = coords - tf.cast(coords_lt, 'float32') 107 | vals_t = vals_lt + (vals_rt - vals_lt) * coords_offset_lt[..., 0] 108 | vals_b = vals_lb + (vals_rb - vals_lb) * coords_offset_lt[..., 0] 109 | mapped_vals = vals_t + (vals_b - vals_t) * coords_offset_lt[..., 1] 110 | 111 | return mapped_vals 112 | 113 | 114 | def sp_batch_map_offsets(input, offsets): 115 | """Reference implementation for tf_batch_map_offsets""" 116 | 117 | batch_size = input.shape[0] 118 | input_size = input.shape[1] 119 | 120 | offsets = offsets.reshape(batch_size, -1, 2) 121 | grid = np.stack(np.mgrid[:input_size, :input_size], -1).reshape(-1, 2) 122 | grid = np.repeat([grid], batch_size, axis=0) 123 | coords = offsets + grid 124 | coords = coords.clip(0, input_size - 1) 125 | 126 | mapped_vals = sp_batch_map_coordinates(input, coords) 127 | return mapped_vals 128 | 129 | 130 | def tf_batch_map_offsets(input, offsets, order=1): 131 | """Batch map offsets into input 132 | Parameters 133 | --------- 134 | input : tf.Tensor. shape = (b, s, s) 135 | offsets: tf.Tensor. shape = (b, s, s, 2) 136 | """ 137 | 138 | input_shape = tf.shape(input) 139 | batch_size = input_shape[0] 140 | input_size = input_shape[1] 141 | 142 | offsets = tf.reshape(offsets, (batch_size, -1, 2)) 143 | grid = tf.meshgrid( 144 | tf.range(input_size), tf.range(input_size), indexing='ij' 145 | ) 146 | grid = tf.stack(grid, axis=-1) 147 | grid = tf.cast(grid, 'float32') 148 | grid = tf.reshape(grid, (-1, 2)) 149 | grid = tf_repeat_2d(grid, batch_size) 150 | coords = offsets + grid 151 | 152 | mapped_vals = tf_batch_map_coordinates(input, coords) 153 | return mapped_vals -------------------------------------------------------------------------------- /non_local_test.py: -------------------------------------------------------------------------------- 1 | import six 2 | from tensorflow.keras.models import Model 3 | from tensorflow.keras.layers import Input, Activation, Reshape, Dense, Conv2D, MaxPooling2D, GlobalMaxPooling2D, GlobalAveragePooling2D, Dropout, BatchNormalization 4 | from tensorflow.python.keras.layers.merge import add 5 | from tensorflow.keras.regularizers import l2 6 | from tensorflow.keras import backend as K 7 | from keras_applications.imagenet_utils import _obtain_input_shape 8 | 9 | from non_local import non_local_block 10 | 11 | 12 | def _bn_relu(x, bn_name=None, relu_name=None): 13 | """Helper to build a BN -> relu block 14 | """ 15 | norm = BatchNormalization(axis=CHANNEL_AXIS, name=bn_name)(x) 16 | return Activation("relu", name=relu_name)(norm) 17 | 18 | 19 | def _conv_bn_relu(**conv_params): 20 | """Helper to build a conv -> BN -> relu residual unit activation function. 21 | This is the original ResNet v1 scheme in https://arxiv.org/abs/1512.03385 22 | """ 23 | filters = conv_params["filters"] 24 | kernel_size = conv_params["kernel_size"] 25 | strides = conv_params.setdefault("strides", (1, 1)) 26 | dilation_rate = conv_params.setdefault("dilation_rate", (1, 1)) 27 | conv_name = conv_params.setdefault("conv_name", None) 28 | bn_name = conv_params.setdefault("bn_name", None) 29 | relu_name = conv_params.setdefault("relu_name", None) 30 | kernel_initializer = conv_params.setdefault("kernel_initializer", "he_normal") 31 | padding = conv_params.setdefault("padding", "same") 32 | kernel_regularizer = conv_params.setdefault("kernel_regularizer", l2(1.e-4)) 33 | 34 | def f(x): 35 | x = Conv2D(filters=filters, kernel_size=kernel_size, 36 | strides=strides, padding=padding, 37 | dilation_rate=dilation_rate, 38 | kernel_initializer=kernel_initializer, 39 | kernel_regularizer=kernel_regularizer, 40 | name=conv_name)(x) 41 | return _bn_relu(x, bn_name=bn_name, relu_name=relu_name) 42 | 43 | return f 44 | 45 | 46 | def _bn_relu_conv(**conv_params): 47 | """Helper to build a BN -> relu -> conv residual unit with full pre-activation function. 48 | This is the ResNet v2 scheme proposed in http://arxiv.org/pdf/1603.05027v2.pdf 49 | """ 50 | filters = conv_params["filters"] 51 | kernel_size = conv_params["kernel_size"] 52 | strides = conv_params.setdefault("strides", (1, 1)) 53 | dilation_rate = conv_params.setdefault("dilation_rate", (1, 1)) 54 | conv_name = conv_params.setdefault("conv_name", None) 55 | bn_name = conv_params.setdefault("bn_name", None) 56 | relu_name = conv_params.setdefault("relu_name", None) 57 | kernel_initializer = conv_params.setdefault("kernel_initializer", "he_normal") 58 | padding = conv_params.setdefault("padding", "same") 59 | kernel_regularizer = conv_params.setdefault("kernel_regularizer", l2(1.e-4)) 60 | 61 | def f(x): 62 | activation = _bn_relu(x, bn_name=bn_name, relu_name=relu_name) 63 | return Conv2D(filters=filters, kernel_size=kernel_size, 64 | strides=strides, padding=padding, 65 | dilation_rate=dilation_rate, 66 | kernel_initializer=kernel_initializer, 67 | kernel_regularizer=kernel_regularizer, 68 | name=conv_name)(activation) 69 | 70 | return f 71 | 72 | 73 | def _shortcut(input_feature, residual, conv_name_base=None, bn_name_base=None): 74 | """Adds a shortcut between input and residual block and merges them with "sum" 75 | """ 76 | # Expand channels of shortcut to match residual. 77 | # Stride appropriately to match residual (width, height) 78 | # Should be int if network architecture is correctly configured. 79 | input_shape = K.int_shape(input_feature) 80 | residual_shape = K.int_shape(residual) 81 | stride_width = int(round(input_shape[ROW_AXIS] / residual_shape[ROW_AXIS])) 82 | stride_height = int(round(input_shape[COL_AXIS] / residual_shape[COL_AXIS])) 83 | equal_channels = input_shape[CHANNEL_AXIS] == residual_shape[CHANNEL_AXIS] 84 | 85 | shortcut = input_feature 86 | # 1 X 1 conv if shape is different. Else identity. 87 | if stride_width > 1 or stride_height > 1 or not equal_channels: 88 | print('reshaping via a convolution...') 89 | if conv_name_base is not None: 90 | conv_name_base = conv_name_base + '1' 91 | shortcut = Conv2D(filters=residual_shape[CHANNEL_AXIS], 92 | kernel_size=(1, 1), 93 | strides=(stride_width, stride_height), 94 | padding="valid", 95 | kernel_initializer="he_normal", 96 | kernel_regularizer=l2(0.0001), 97 | name=conv_name_base)(input_feature) 98 | if bn_name_base is not None: 99 | bn_name_base = bn_name_base + '1' 100 | shortcut = BatchNormalization(axis=CHANNEL_AXIS, name=bn_name_base)(shortcut) 101 | 102 | return add([shortcut, residual]) 103 | 104 | 105 | def _residual_block(block_function, filters, blocks, stage, 106 | transition_strides=None, transition_dilation_rates=None, 107 | dilation_rates=(1, 1), is_first_layer=False, dropout=None, 108 | residual_unit=_bn_relu_conv): 109 | """Builds a residual block with repeating bottleneck blocks. 110 | stage: integer, current stage label, used for generating layer names 111 | blocks: number of blocks 'a','b'..., current block label, used for generating layer names 112 | transition_strides: a list of tuples for the strides of each transition 113 | transition_dilation_rates: a list of tuples for the dilation rate of each transition 114 | """ 115 | if transition_dilation_rates is None: 116 | transition_dilation_rates = [(1, 1)] * blocks 117 | if transition_strides is None: 118 | transition_strides = [(1, 1)] * blocks 119 | 120 | def f(x): 121 | for i in range(blocks): 122 | x = block_function(filters=filters, stage=stage, block=i, 123 | transition_strides=transition_strides[i], 124 | dilation_rate=transition_dilation_rates[i], 125 | is_first_block_of_first_layer=(is_first_layer and i == 0), 126 | dropout=dropout, 127 | residual_unit=residual_unit)(x) 128 | 129 | # Non Local Blook 130 | if filters >= 256: 131 | print("Filters : ", filters, "Adding Non Local Blocks") 132 | x = non_local_block(x, mode='embedded', compression=2) 133 | 134 | return x 135 | 136 | return f 137 | 138 | 139 | def _block_name_base(stage, block): 140 | """Get the convolution name base and batch normalization name base defined by stage and block. 141 | If there are less than 26 blocks they will be labeled 'a', 'b', 'c' to match the paper and keras 142 | and beyond 26 blocks they will simply be numbered. 143 | """ 144 | if block < 27: 145 | block = '%c' % (block + 97) # 97 is the ascii number for lowercase 'a' 146 | conv_name_base = 'res' + str(stage) + block + '_branch' 147 | bn_name_base = 'bn' + str(stage) + block + '_branch' 148 | return conv_name_base, bn_name_base 149 | 150 | 151 | def basic_block(filters, stage, block, transition_strides=(1, 1), 152 | dilation_rate=(1, 1), is_first_block_of_first_layer=False, dropout=None, 153 | residual_unit=_bn_relu_conv): 154 | """Basic 3 X 3 convolution blocks for use on resnets with layers <= 34. 155 | Follows improved proposed scheme in http://arxiv.org/pdf/1603.05027v2.pdf 156 | """ 157 | def f(input_features): 158 | conv_name_base, bn_name_base = _block_name_base(stage, block) 159 | if is_first_block_of_first_layer: 160 | # don't repeat bn->relu since we just did bn->relu->maxpool 161 | x = Conv2D(filters=filters, kernel_size=(3, 3), 162 | strides=transition_strides, 163 | dilation_rate=dilation_rate, 164 | padding="same", 165 | kernel_initializer="he_normal", 166 | kernel_regularizer=l2(1e-4), 167 | name=conv_name_base + '2a')(input_features) 168 | else: 169 | x = residual_unit(filters=filters, kernel_size=(3, 3), 170 | strides=transition_strides, 171 | dilation_rate=dilation_rate, 172 | conv_name_base=conv_name_base + '2a', 173 | bn_name_base=bn_name_base + '2a')(input_features) 174 | 175 | if dropout is not None: 176 | x = Dropout(dropout)(x) 177 | 178 | x = residual_unit(filters=filters, kernel_size=(3, 3), 179 | conv_name_base=conv_name_base + '2b', 180 | bn_name_base=bn_name_base + '2b')(x) 181 | 182 | return _shortcut(input_features, x) 183 | 184 | return f 185 | 186 | 187 | def bottleneck(filters, stage, block, transition_strides=(1, 1), 188 | dilation_rate=(1, 1), is_first_block_of_first_layer=False, dropout=None, 189 | residual_unit=_bn_relu_conv): 190 | """Bottleneck architecture for > 34 layer resnet. 191 | Follows improved proposed scheme in http://arxiv.org/pdf/1603.05027v2.pdf 192 | Returns: 193 | A final conv layer of filters * 4 194 | """ 195 | def f(input_feature): 196 | conv_name_base, bn_name_base = _block_name_base(stage, block) 197 | if is_first_block_of_first_layer: 198 | # don't repeat bn->relu since we just did bn->relu->maxpool 199 | x = Conv2D(filters=filters, kernel_size=(1, 1), 200 | strides=transition_strides, 201 | dilation_rate=dilation_rate, 202 | padding="same", 203 | kernel_initializer="he_normal", 204 | kernel_regularizer=l2(1e-4), 205 | name=conv_name_base + '2a')(input_feature) 206 | else: 207 | x = residual_unit(filters=filters, kernel_size=(1, 1), 208 | strides=transition_strides, 209 | dilation_rate=dilation_rate, 210 | conv_name_base=conv_name_base + '2a', 211 | bn_name_base=bn_name_base + '2a')(input_feature) 212 | 213 | if dropout is not None: 214 | x = Dropout(dropout)(x) 215 | 216 | x = residual_unit(filters=filters, kernel_size=(3, 3), 217 | conv_name_base=conv_name_base + '2b', 218 | bn_name_base=bn_name_base + '2b')(x) 219 | 220 | if dropout is not None: 221 | x = Dropout(dropout)(x) 222 | 223 | x = residual_unit(filters=filters * 4, kernel_size=(1, 1), 224 | conv_name_base=conv_name_base + '2c', 225 | bn_name_base=bn_name_base + '2c')(x) 226 | 227 | return _shortcut(input_feature, x) 228 | 229 | return f 230 | 231 | 232 | def _handle_dim_ordering(): 233 | global ROW_AXIS 234 | global COL_AXIS 235 | global CHANNEL_AXIS 236 | if K.image_data_format() == 'channels_last': 237 | ROW_AXIS = 1 238 | COL_AXIS = 2 239 | CHANNEL_AXIS = 3 240 | else: 241 | CHANNEL_AXIS = 1 242 | ROW_AXIS = 2 243 | COL_AXIS = 3 244 | 245 | 246 | def _string_to_function(identifier): 247 | if isinstance(identifier, six.string_types): 248 | res = globals().get(identifier) 249 | if not res: 250 | raise ValueError('Invalid {}'.format(identifier)) 251 | return res 252 | return identifier 253 | 254 | 255 | def NonLocalResNet(input_shape=None, classes=10, block='bottleneck', residual_unit='v2', repetitions=None, 256 | initial_filters=64, activation='softmax', include_top=True, input_tensor=None, dropout=None, 257 | transition_dilation_rate=(1, 1), initial_strides=(2, 2), initial_kernel_size=(7, 7), 258 | initial_pooling='max', final_pooling=None, top='classification'): 259 | """Builds a custom ResNet like architecture. Defaults to ResNet50 v2. 260 | Args: 261 | input_shape: optional shape tuple, only to be specified 262 | if `include_top` is False (otherwise the input shape 263 | has to be `(224, 224, 3)` (with `channels_last` dim ordering) 264 | or `(3, 224, 224)` (with `channels_first` dim ordering). 265 | It should have exactly 3 inputs channels, 266 | and width and height should be no smaller than 8. 267 | E.g. `(224, 224, 3)` would be one valid value. 268 | classes: The number of outputs at final softmax layer 269 | block: The block function to use. This is either `'basic'` or `'bottleneck'`. 270 | The original paper used `basic` for layers < 50. 271 | repetitions: Number of repetitions of various block units. 272 | At each block unit, the number of filters are doubled and the input size is halved. 273 | Default of None implies the ResNet50v2 values of [3, 4, 6, 3]. 274 | transition_dilation_rate: Used for pixel-wise prediction tasks such as image segmentation. 275 | residual_unit: the basic residual unit, 'v1' for conv bn relu, 'v2' for bn relu conv. 276 | See [Identity Mappings in Deep Residual Networks](https://arxiv.org/abs/1603.05027) 277 | for details. 278 | dropout: None for no dropout, otherwise rate of dropout from 0 to 1. 279 | Based on [Wide Residual Networks.(https://arxiv.org/pdf/1605.07146) paper. 280 | transition_dilation_rate: Dilation rate for transition layers. For semantic 281 | segmentation of images use a dilation rate of (2, 2). 282 | initial_strides: Stride of the very first residual unit and MaxPooling2D call, 283 | with default (2, 2), set to (1, 1) for small images like cifar. 284 | initial_kernel_size: kernel size of the very first convolution, (7, 7) for imagenet 285 | and (3, 3) for small image datasets like tiny imagenet and cifar. 286 | See [ResNeXt](https://arxiv.org/abs/1611.05431) paper for details. 287 | initial_pooling: Determine if there will be an initial pooling layer, 288 | 'max' for imagenet and None for small image datasets. 289 | See [ResNeXt](https://arxiv.org/abs/1611.05431) paper for details. 290 | final_pooling: Optional pooling mode for feature extraction at the final model layer 291 | when `include_top` is `False`. 292 | - `None` means that the output of the model 293 | will be the 4D tensor output of the 294 | last convolutional layer. 295 | - `avg` means that global average pooling 296 | will be applied to the output of the 297 | last convolutional layer, and thus 298 | the output of the model will be a 299 | 2D tensor. 300 | - `max` means that global max pooling will 301 | be applied. 302 | top: Defines final layers to evaluate based on a specific problem type. Options are 303 | 'classification' for ImageNet style problems, 'segmentation' for problems like 304 | the Pascal VOC dataset, and None to exclude these layers entirely. 305 | Returns: 306 | The keras `Model`. 307 | """ 308 | if activation not in ['softmax', 'sigmoid', None]: 309 | raise ValueError('activation must be one of "softmax", "sigmoid", or None') 310 | if activation == 'sigmoid' and classes != 1: 311 | raise ValueError('sigmoid activation can only be used when classes = 1') 312 | if repetitions is None: 313 | repetitions = [3, 4, 6, 3] 314 | # Determine proper input shape 315 | input_shape = _obtain_input_shape (input_shape, 316 | default_size=32, 317 | min_size=8, 318 | data_format=K.image_data_format(), 319 | require_flatten=include_top) 320 | _handle_dim_ordering() 321 | if len(input_shape) != 3: 322 | raise Exception("Input shape should be a tuple (nb_channels, nb_rows, nb_cols)") 323 | 324 | if block == 'basic': 325 | block_fn = basic_block 326 | elif block == 'bottleneck': 327 | block_fn = bottleneck 328 | elif isinstance(block, six.string_types): 329 | block_fn = _string_to_function(block) 330 | else: 331 | block_fn = block 332 | 333 | if residual_unit == 'v2': 334 | residual_unit = _bn_relu_conv 335 | elif residual_unit == 'v1': 336 | residual_unit = _conv_bn_relu 337 | elif isinstance(residual_unit, six.string_types): 338 | residual_unit = _string_to_function(residual_unit) 339 | else: 340 | residual_unit = residual_unit 341 | 342 | # Permute dimension order if necessary 343 | if K.image_data_format() == 'channels_first': 344 | input_shape = (input_shape[1], input_shape[2], input_shape[0]) 345 | # Determine proper input shape 346 | input_shape = _obtain_input_shape(input_shape, 347 | default_size=32, 348 | min_size=8, 349 | data_format=K.image_data_format(), 350 | require_flatten=include_top) 351 | 352 | img_input = Input(shape=input_shape, tensor=input_tensor) 353 | x = _conv_bn_relu(filters=initial_filters, kernel_size=initial_kernel_size, strides=initial_strides)(img_input) 354 | if initial_pooling == 'max': 355 | x = MaxPooling2D(pool_size=(3, 3), strides=initial_strides, padding="same")(x) 356 | 357 | 358 | block = x 359 | filters = initial_filters 360 | for i, r in enumerate(repetitions): 361 | transition_dilation_rates = [transition_dilation_rate] * r 362 | transition_strides = [(1, 1)] * r 363 | if transition_dilation_rate == (1, 1): 364 | transition_strides[0] = (2, 2) 365 | block = _residual_block(block_fn, filters=filters, 366 | stage=i, blocks=r, 367 | is_first_layer=(i == 0), 368 | dropout=dropout, 369 | transition_dilation_rates=transition_dilation_rates, 370 | transition_strides=transition_strides, 371 | residual_unit=residual_unit)(block) 372 | filters *= 2 373 | 374 | # Last activation 375 | x = _bn_relu(block) 376 | 377 | # Classifier block 378 | if include_top and top is 'classification': 379 | x = GlobalAveragePooling2D()(x) 380 | x = Dense(units=classes, activation=activation, kernel_initializer="he_normal")(x) 381 | elif include_top and top is 'segmentation': 382 | x = Conv2D(classes, (1, 1), activation='linear', padding='same')(x) 383 | 384 | if K.image_data_format() == 'channels_first': 385 | channel, row, col = input_shape 386 | else: 387 | row, col, channel = input_shape 388 | 389 | x = Reshape((row * col, classes))(x) 390 | x = Activation(activation)(x) 391 | x = Reshape((row, col, classes))(x) 392 | elif final_pooling == 'avg': 393 | x = GlobalAveragePooling2D()(x) 394 | elif final_pooling == 'max': 395 | x = GlobalMaxPooling2D()(x) 396 | 397 | model = Model(inputs=img_input, outputs=x) 398 | return model 399 | 400 | 401 | def NonLocalResNet18(input_shape, classes): 402 | """ResNet with 18 layers and v2 residual units 403 | """ 404 | return NonLocalResNet(input_shape, classes, basic_block, repetitions=[2, 2, 2, 2]) 405 | 406 | 407 | def NonLocalResNet34(input_shape, classes): 408 | """ResNet with 34 layers and v2 residual units 409 | """ 410 | return NonLocalResNet(input_shape, classes, basic_block, repetitions=[3, 4, 6, 3]) 411 | 412 | 413 | def NonLocalResNet50(input_shape, classes): 414 | """ResNet with 50 layers and v2 residual units 415 | """ 416 | return NonLocalResNet(input_shape, classes, bottleneck, repetitions=[3, 4, 6, 3]) 417 | 418 | 419 | def NonLocalResNet101(input_shape, classes): 420 | """ResNet with 101 layers and v2 residual units 421 | """ 422 | return NonLocalResNet(input_shape, classes, bottleneck, repetitions=[3, 4, 23, 3]) 423 | 424 | 425 | def NonLocalResNet152(input_shape, classes): 426 | """ResNet with 152 layers and v2 residual units 427 | """ 428 | return NonLocalResNet(input_shape, classes, bottleneck, repetitions=[3, 8, 36, 3]) 429 | 430 | 431 | if __name__ == '__main__': 432 | model = NonLocalResNet18((128, 160, 3), classes=10) 433 | model.summary() -------------------------------------------------------------------------------- /STN_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## STN测试报告\n", 8 | "以公开mnist数据集为例做训练,比较有stn模块和没有stn模块的网络两者性能的差异" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "import tensorflow as tf\n", 18 | "import matplotlib\n", 19 | "import numpy as np\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import imgaug.augmenters as iaa\n", 22 | "import imgaug as ia\n", 23 | "from datetime import datetime\n", 24 | "from math import cos, sin, pi" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "# 加载数据集\n", 34 | "mnist = tf.keras.datasets.mnist\n", 35 | "(x_train, y_train), (x_test, y_test) = mnist.load_data()\n", 36 | "# 归一化\n", 37 | "x_train, x_test = x_train / 255.0, x_test / 255.0\n", 38 | "# reshape\n", 39 | "x_train = x_train.reshape(-1, 28, 28, 1)\n", 40 | "x_test = x_test.reshape(-1, 28, 28, 1)\n", 41 | "# 获取图像的长宽\n", 42 | "H, W,_ = x_train[0].shape" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "### 数据增强\n", 50 | "生成一些数据增强的数据,数据增强的方法是仿射变换与原数据构成新的训练集" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# Apply affine transformations to some of the images\n", 60 | "# - scale to 80-120% of image height/width (each axis independently)\n", 61 | "# - translate by -20 to +20 relative to height/width (per axis)\n", 62 | "# - rotate by -45 to +45 degrees\n", 63 | "# - shear by -16 to +16 degrees\n", 64 | "# - order: use nearest neighbour or bilinear interpolation (fast)\n", 65 | "# - mode: use any available mode to fill newly created pixels\n", 66 | "# see API or scikit-image for which modes are available\n", 67 | "# - cval: if the mode is constant, then use a random brightness\n", 68 | "# for the newly created pixels (e.g. sometimes black,\n", 69 | "# sometimes white)\n", 70 | "seq = iaa.Sequential([\n", 71 | " iaa.OneOf([\n", 72 | " iaa.Affine(\n", 73 | " scale={\"x\": (0.6, 1.1), \"y\": (0.5, 1.1)},\n", 74 | " translate_percent={\"x\": (-0.2, 0.2), \"y\": (-0.2, 0.2)},\n", 75 | " rotate=(-30, 30),\n", 76 | " shear=(-15, 15),\n", 77 | " order=[0, 1],\n", 78 | " cval=(0),\n", 79 | " ),\n", 80 | " iaa.Affine(\n", 81 | " scale={\"x\": (0.6, 1.1), \"y\": (0.6, 1.1)},\n", 82 | " order=[0, 1],\n", 83 | " cval=(0),\n", 84 | " ),\n", 85 | " iaa.Affine(\n", 86 | " scale={\"x\": (0.6, 0.8), \"y\": (0.6, 0.8)},\n", 87 | " translate_percent={\"x\": (-0.2, 0.2), \"y\": (-0.2, 0.2)},\n", 88 | " order=[0, 1],\n", 89 | " cval=(0),\n", 90 | " ),\n", 91 | " iaa.Affine(\n", 92 | " rotate=(-60, 60),\n", 93 | " #shear=(-30, 30),\n", 94 | " order=[0, 1],\n", 95 | " cval=(0),\n", 96 | " ),\n", 97 | " iaa.Affine(\n", 98 | " shear=(-40, 40),\n", 99 | " order=[0, 1],\n", 100 | " cval=(0),\n", 101 | " ),\n", 102 | " ]\n", 103 | " )\n", 104 | "])\n", 105 | "x_train_aug = seq(images=x_train) \n", 106 | "x_test_aug = seq(images=x_test) " 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "### 可视化\n", 114 | "可视化训练集的函数" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 4, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "from math import ceil\n", 124 | "def draw_samples(images, images_per_row=5):\n", 125 | " num = len(images)\n", 126 | " per_row = min(images_per_row, num)\n", 127 | " rows = ceil(num /per_row)\n", 128 | " fig, axs = plt.subplots(rows, per_row)\n", 129 | " count = 0 \n", 130 | " for i in range(rows):\n", 131 | " \n", 132 | " for j in range(images_per_row):\n", 133 | " count+=1\n", 134 | " if (count > num):\n", 135 | " break\n", 136 | " if rows == 1:\n", 137 | " axs[j+i*per_row].imshow(images[j+i*per_row], cmap='gray')\n", 138 | " else:\n", 139 | " axs[i,j].imshow(images[j+i*per_row], cmap='gray')\n", 140 | " \n", 141 | " plt.show()" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 5, 147 | "metadata": {}, 148 | "outputs": [ 149 | { 150 | "data": { 151 | "image/png": "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", 152 | "text/plain": [ 153 | "
" 154 | ] 155 | }, 156 | "metadata": { 157 | "needs_background": "light" 158 | }, 159 | "output_type": "display_data" 160 | } 161 | ], 162 | "source": [ 163 | "# 原数据集数据\n", 164 | "draw_samples(x_train[:30])" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 6, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "data": { 174 | "image/png": "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", 175 | "text/plain": [ 176 | "
" 177 | ] 178 | }, 179 | "metadata": { 180 | "needs_background": "light" 181 | }, 182 | "output_type": "display_data" 183 | } 184 | ], 185 | "source": [ 186 | "# 仿射变换的数据\n", 187 | "draw_samples(x_train_aug[:30])" 188 | ] 189 | }, 190 | { 191 | "cell_type": "markdown", 192 | "metadata": {}, 193 | "source": [ 194 | "### 神经网络\n", 195 | "定义简单的密集神经网络进行训练" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 7, 201 | "metadata": {}, 202 | "outputs": [ 203 | { 204 | "name": "stdout", 205 | "output_type": "stream", 206 | "text": [ 207 | "Model: \"sequential\"\n", 208 | "_________________________________________________________________\n", 209 | "Layer (type) Output Shape Param # \n", 210 | "=================================================================\n", 211 | "flatten (Flatten) (None, 784) 0 \n", 212 | "_________________________________________________________________\n", 213 | "dense (Dense) (None, 128) 100480 \n", 214 | "_________________________________________________________________\n", 215 | "dropout (Dropout) (None, 128) 0 \n", 216 | "_________________________________________________________________\n", 217 | "dense_1 (Dense) (None, 10) 1290 \n", 218 | "=================================================================\n", 219 | "Total params: 101,770\n", 220 | "Trainable params: 101,770\n", 221 | "Non-trainable params: 0\n", 222 | "_________________________________________________________________\n" 223 | ] 224 | } 225 | ], 226 | "source": [ 227 | "# baseline\n", 228 | "model = tf.keras.models.Sequential([\n", 229 | " tf.keras.layers.Flatten(input_shape=(H, W)),\n", 230 | " tf.keras.layers.Dense(128, activation='relu'),\n", 231 | " tf.keras.layers.Dropout(0.2),\n", 232 | " tf.keras.layers.Dense(10)\n", 233 | "])\n", 234 | "model.summary()" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": 8, 240 | "metadata": {}, 241 | "outputs": [ 242 | { 243 | "name": "stdout", 244 | "output_type": "stream", 245 | "text": [ 246 | "Epoch 1/5\n", 247 | "3750/3750 [==============================] - 5s 1ms/step - loss: 0.9359 - accuracy: 0.7041\n", 248 | "Epoch 2/5\n", 249 | "3750/3750 [==============================] - 4s 987us/step - loss: 0.4982 - accuracy: 0.8430\n", 250 | "Epoch 3/5\n", 251 | "3750/3750 [==============================] - 4s 1ms/step - loss: 0.4187 - accuracy: 0.8673\n", 252 | "Epoch 4/5\n", 253 | "3750/3750 [==============================] - 4s 1ms/step - loss: 0.3788 - accuracy: 0.8810\n", 254 | "Epoch 5/5\n", 255 | "3750/3750 [==============================] - 4s 1ms/step - loss: 0.3533 - accuracy: 0.8883\n" 256 | ] 257 | }, 258 | { 259 | "data": { 260 | "text/plain": [ 261 | "" 262 | ] 263 | }, 264 | "execution_count": 8, 265 | "metadata": {}, 266 | "output_type": "execute_result" 267 | } 268 | ], 269 | "source": [ 270 | "# 定义损失函数\n", 271 | "loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)\n", 272 | "model.compile(optimizer='adam',\n", 273 | " loss=loss_fn,\n", 274 | " metrics=['accuracy'])\n", 275 | "# completely retrain the original model with mnist dataset + distorted mnist dataset\n", 276 | "model.fit(tf.concat([x_train_aug, x_train],0), tf.concat([y_train, y_train],0), epochs=5)" 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": 11, 282 | "metadata": {}, 283 | "outputs": [ 284 | { 285 | "name": "stdout", 286 | "output_type": "stream", 287 | "text": [ 288 | "313/313 [==============================] - 0s 929us/step - loss: 0.5379 - accuracy: 0.8329\n", 289 | "distorted data: [0.5378811955451965, 0.8328999876976013]\n", 290 | "313/313 [==============================] - 0s 663us/step - loss: 0.0755 - accuracy: 0.9776\n", 291 | "undistorted original data: [0.07546955347061157, 0.9775999784469604]\n" 292 | ] 293 | } 294 | ], 295 | "source": [ 296 | "# not good for distorted data\n", 297 | "print('distorted data:',model.evaluate(x_test_aug, y_test))\n", 298 | "# still good for undistorted original data\n", 299 | "print('undistorted original data:',model.evaluate(x_test, y_test))" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "metadata": {}, 305 | "source": [ 306 | "定义STN模块的网络进行训练" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": 13, 312 | "metadata": {}, 313 | "outputs": [ 314 | { 315 | "name": "stdout", 316 | "output_type": "stream", 317 | "text": [ 318 | "Model: \"model_3\"\n", 319 | "__________________________________________________________________________________________________\n", 320 | "Layer (type) Output Shape Param # Connected to \n", 321 | "==================================================================================================\n", 322 | "input_3 (InputLayer) [(None, 28, 28, 1)] 0 \n", 323 | "__________________________________________________________________________________________________\n", 324 | "conv2d_5 (Conv2D) (None, 24, 24, 14) 364 input_3[0][0] \n", 325 | "__________________________________________________________________________________________________\n", 326 | "max_pooling2d_4 (MaxPooling2D) (None, 12, 12, 14) 0 conv2d_5[0][0] \n", 327 | "__________________________________________________________________________________________________\n", 328 | "conv2d_6 (Conv2D) (None, 8, 8, 32) 11232 max_pooling2d_4[0][0] \n", 329 | "__________________________________________________________________________________________________\n", 330 | "max_pooling2d_5 (MaxPooling2D) (None, 4, 4, 32) 0 conv2d_6[0][0] \n", 331 | "__________________________________________________________________________________________________\n", 332 | "flatten_3 (Flatten) (None, 512) 0 max_pooling2d_5[0][0] \n", 333 | "__________________________________________________________________________________________________\n", 334 | "dense_8 (Dense) (None, 120) 61560 flatten_3[0][0] \n", 335 | "__________________________________________________________________________________________________\n", 336 | "dropout_2 (Dropout) (None, 120) 0 dense_8[0][0] \n", 337 | "__________________________________________________________________________________________________\n", 338 | "tf.compat.v1.shape_3 (TFOpLambd (4,) 0 input_3[0][0] \n", 339 | "__________________________________________________________________________________________________\n", 340 | "dense_9 (Dense) (None, 84) 10164 dropout_2[0][0] \n", 341 | "__________________________________________________________________________________________________\n", 342 | "tf.__operators__.getitem_4 (Sli () 0 tf.compat.v1.shape_3[0][0] \n", 343 | "__________________________________________________________________________________________________\n", 344 | "dense_10 (Dense) (None, 6) 510 dense_9[0][0] \n", 345 | "__________________________________________________________________________________________________\n", 346 | "tf.tile_1 (TFOpLambda) (None, 784, 3, 1) 0 tf.__operators__.getitem_4[0][0] \n", 347 | "__________________________________________________________________________________________________\n", 348 | "tf.reshape_3 (TFOpLambda) (None, 2, 3) 0 dense_10[0][0] \n", 349 | "__________________________________________________________________________________________________\n", 350 | "tf.compat.v1.squeeze_1 (TFOpLam (None, 784, 3) 0 tf.tile_1[0][0] \n", 351 | "__________________________________________________________________________________________________\n", 352 | "tf.linalg.matmul_3 (TFOpLambda) (None, 2, 784) 0 tf.reshape_3[0][0] \n", 353 | " tf.compat.v1.squeeze_1[0][0] \n", 354 | "__________________________________________________________________________________________________\n", 355 | "tf.linalg.matrix_transpose_1 (T (None, 784, 2) 0 tf.linalg.matmul_3[0][0] \n", 356 | "__________________________________________________________________________________________________\n", 357 | "tf.__operators__.add_3 (TFOpLam (None, 784, 2) 0 tf.linalg.matrix_transpose_1[0][0\n", 358 | "__________________________________________________________________________________________________\n", 359 | "tf.math.multiply_2 (TFOpLambda) (None, 784, 2) 0 tf.__operators__.add_3[0][0] \n", 360 | "__________________________________________________________________________________________________\n", 361 | "tf.math.multiply_3 (TFOpLambda) (None, 784, 2) 0 tf.math.multiply_2[0][0] \n", 362 | "__________________________________________________________________________________________________\n", 363 | "tf.split_2 (TFOpLambda) [(None, 784, 1), (No 0 tf.math.multiply_3[0][0] \n", 364 | "__________________________________________________________________________________________________\n", 365 | "tf.math.floor_2 (TFOpLambda) (None, 784, 1) 0 tf.split_2[0][0] \n", 366 | "__________________________________________________________________________________________________\n", 367 | "tf.math.floor_3 (TFOpLambda) (None, 784, 1) 0 tf.split_2[0][1] \n", 368 | "__________________________________________________________________________________________________\n", 369 | "tf.cast_7 (TFOpLambda) (None, 784, 1) 0 tf.math.floor_2[0][0] \n", 370 | "__________________________________________________________________________________________________\n", 371 | "tf.cast_8 (TFOpLambda) (None, 784, 1) 0 tf.math.floor_3[0][0] \n", 372 | "__________________________________________________________________________________________________\n", 373 | "tf.__operators__.add_4 (TFOpLam (None, 784, 1) 0 tf.cast_7[0][0] \n", 374 | "__________________________________________________________________________________________________\n", 375 | "tf.__operators__.add_5 (TFOpLam (None, 784, 1) 0 tf.cast_8[0][0] \n", 376 | "__________________________________________________________________________________________________\n", 377 | "tf.clip_by_value_6 (TFOpLambda) (None, 784, 1) 0 tf.__operators__.add_4[0][0] \n", 378 | "__________________________________________________________________________________________________\n", 379 | "tf.clip_by_value_4 (TFOpLambda) (None, 784, 1) 0 tf.cast_7[0][0] \n", 380 | "__________________________________________________________________________________________________\n", 381 | "tf.clip_by_value_5 (TFOpLambda) (None, 784, 1) 0 tf.cast_8[0][0] \n", 382 | "__________________________________________________________________________________________________\n", 383 | "tf.clip_by_value_7 (TFOpLambda) (None, 784, 1) 0 tf.__operators__.add_5[0][0] \n", 384 | "__________________________________________________________________________________________________\n", 385 | "tf.concat_7 (TFOpLambda) (None, 784, 2) 0 tf.clip_by_value_5[0][0] \n", 386 | " tf.clip_by_value_4[0][0] \n", 387 | "__________________________________________________________________________________________________\n", 388 | "tf.concat_8 (TFOpLambda) (None, 784, 2) 0 tf.clip_by_value_7[0][0] \n", 389 | " tf.clip_by_value_4[0][0] \n", 390 | "__________________________________________________________________________________________________\n", 391 | "tf.concat_9 (TFOpLambda) (None, 784, 2) 0 tf.clip_by_value_5[0][0] \n", 392 | " tf.clip_by_value_6[0][0] \n", 393 | "__________________________________________________________________________________________________\n", 394 | "tf.concat_10 (TFOpLambda) (None, 784, 2) 0 tf.clip_by_value_7[0][0] \n", 395 | " tf.clip_by_value_6[0][0] \n", 396 | "__________________________________________________________________________________________________\n", 397 | "tf.compat.v1.gather_nd_4 (TFOpL (None, 784, 1) 0 input_3[0][0] \n", 398 | " tf.concat_7[0][0] \n", 399 | "__________________________________________________________________________________________________\n", 400 | "tf.compat.v1.gather_nd_5 (TFOpL (None, 784, 1) 0 input_3[0][0] \n", 401 | " tf.concat_8[0][0] \n", 402 | "__________________________________________________________________________________________________\n", 403 | "tf.compat.v1.gather_nd_6 (TFOpL (None, 784, 1) 0 input_3[0][0] \n", 404 | " tf.concat_9[0][0] \n", 405 | "__________________________________________________________________________________________________\n", 406 | "tf.compat.v1.gather_nd_7 (TFOpL (None, 784, 1) 0 input_3[0][0] \n", 407 | " tf.concat_10[0][0] \n", 408 | "__________________________________________________________________________________________________\n", 409 | "tf.concat_13 (TFOpLambda) (None, 784, 4) 0 tf.compat.v1.gather_nd_4[0][0] \n", 410 | " tf.compat.v1.gather_nd_5[0][0] \n", 411 | " tf.compat.v1.gather_nd_6[0][0] \n", 412 | " tf.compat.v1.gather_nd_7[0][0] \n", 413 | "__________________________________________________________________________________________________\n", 414 | "tf.cast_9 (TFOpLambda) (None, 784, 1) 0 tf.clip_by_value_6[0][0] \n", 415 | "__________________________________________________________________________________________________\n", 416 | "tf.split_3 (TFOpLambda) [(None, 784, 1), (No 0 tf.math.multiply_3[0][0] \n", 417 | "__________________________________________________________________________________________________\n", 418 | "tf.cast_10 (TFOpLambda) (None, 784, 1) 0 tf.clip_by_value_4[0][0] \n", 419 | "__________________________________________________________________________________________________\n", 420 | "tf.compat.v1.shape_5 (TFOpLambd (3,) 0 tf.concat_13[0][0] \n", 421 | "__________________________________________________________________________________________________\n", 422 | "tf.math.subtract_4 (TFOpLambda) (None, 784, 1) 0 tf.cast_9[0][0] \n", 423 | " tf.split_3[0][0] \n", 424 | "__________________________________________________________________________________________________\n", 425 | "tf.math.subtract_5 (TFOpLambda) (None, 784, 1) 0 tf.split_3[0][0] \n", 426 | " tf.cast_10[0][0] \n", 427 | "__________________________________________________________________________________________________\n", 428 | "tf.__operators__.getitem_6 (Sli () 0 tf.compat.v1.shape_5[0][0] \n", 429 | "__________________________________________________________________________________________________\n", 430 | "tf.__operators__.getitem_7 (Sli () 0 tf.compat.v1.shape_5[0][0] \n", 431 | "__________________________________________________________________________________________________\n", 432 | "tf.cast_11 (TFOpLambda) (None, 784, 1) 0 tf.clip_by_value_7[0][0] \n", 433 | "__________________________________________________________________________________________________\n", 434 | "tf.cast_12 (TFOpLambda) (None, 784, 1) 0 tf.clip_by_value_5[0][0] \n", 435 | "__________________________________________________________________________________________________\n", 436 | "tf.concat_11 (TFOpLambda) (None, 784, 2) 0 tf.math.subtract_4[0][0] \n", 437 | " tf.math.subtract_5[0][0] \n", 438 | "__________________________________________________________________________________________________\n", 439 | "tf.reshape_4 (TFOpLambda) (None, None, 2, 2) 0 tf.concat_13[0][0] \n", 440 | " tf.__operators__.getitem_6[0][0] \n", 441 | " tf.__operators__.getitem_7[0][0] \n", 442 | "__________________________________________________________________________________________________\n", 443 | "tf.math.subtract_6 (TFOpLambda) (None, 784, 1) 0 tf.cast_11[0][0] \n", 444 | " tf.split_3[0][1] \n", 445 | "__________________________________________________________________________________________________\n", 446 | "tf.math.subtract_7 (TFOpLambda) (None, 784, 1) 0 tf.split_3[0][1] \n", 447 | " tf.cast_12[0][0] \n", 448 | "__________________________________________________________________________________________________\n", 449 | "tf.expand_dims_2 (TFOpLambda) (None, 784, 1, 2) 0 tf.concat_11[0][0] \n", 450 | "__________________________________________________________________________________________________\n", 451 | "tf.cast_13 (TFOpLambda) (None, None, 2, 2) 0 tf.reshape_4[0][0] \n", 452 | "__________________________________________________________________________________________________\n", 453 | "tf.concat_12 (TFOpLambda) (None, 784, 2) 0 tf.math.subtract_6[0][0] \n", 454 | " tf.math.subtract_7[0][0] \n", 455 | "__________________________________________________________________________________________________\n", 456 | "tf.linalg.matmul_4 (TFOpLambda) (None, 784, 1, 2) 0 tf.expand_dims_2[0][0] \n", 457 | " tf.cast_13[0][0] \n", 458 | "__________________________________________________________________________________________________\n", 459 | "tf.expand_dims_3 (TFOpLambda) (None, 784, 2, 1) 0 tf.concat_12[0][0] \n", 460 | "__________________________________________________________________________________________________\n", 461 | "tf.linalg.matmul_5 (TFOpLambda) (None, 784, 1, 1) 0 tf.linalg.matmul_4[0][0] \n", 462 | " tf.expand_dims_3[0][0] \n", 463 | "__________________________________________________________________________________________________\n", 464 | "tf.reshape_5 (TFOpLambda) (None, 28, 28, 1) 0 tf.linalg.matmul_5[0][0] \n", 465 | "__________________________________________________________________________________________________\n", 466 | "conv2d_7 (Conv2D) (None, 26, 26, 6) 60 tf.reshape_5[0][0] \n", 467 | "__________________________________________________________________________________________________\n", 468 | "max_pooling2d_6 (MaxPooling2D) (None, 13, 13, 6) 0 conv2d_7[0][0] \n", 469 | "__________________________________________________________________________________________________\n", 470 | "conv2d_8 (Conv2D) (None, 11, 11, 16) 880 max_pooling2d_6[0][0] \n", 471 | "__________________________________________________________________________________________________\n", 472 | "max_pooling2d_7 (MaxPooling2D) (None, 5, 5, 16) 0 conv2d_8[0][0] \n", 473 | "__________________________________________________________________________________________________\n", 474 | "flatten_4 (Flatten) (None, 400) 0 max_pooling2d_7[0][0] \n", 475 | "__________________________________________________________________________________________________\n", 476 | "dense_11 (Dense) (None, 120) 48120 flatten_4[0][0] \n", 477 | "__________________________________________________________________________________________________\n", 478 | "dense_12 (Dense) (None, 84) 10164 dense_11[0][0] \n", 479 | "__________________________________________________________________________________________________\n", 480 | "dense_13 (Dense) (None, 10) 850 dense_12[0][0] \n", 481 | "==================================================================================================\n", 482 | "Total params: 143,904\n", 483 | "Trainable params: 143,904\n", 484 | "Non-trainable params: 0\n", 485 | "__________________________________________________________________________________________________\n" 486 | ] 487 | } 488 | ], 489 | "source": [ 490 | "from stn_module import stn_module\n", 491 | "from tensorflow.keras.layers import Conv2D, MaxPooling2D,Flatten,Dense,Input,Dropout\n", 492 | "def model(input_shape):\n", 493 | " inputs = Input(input_shape)\n", 494 | " inputs_stn = stn_module(inputs)\n", 495 | " x = Conv2D(6, (3,3),padding='valid',activation=\"relu\")(inputs_stn)\n", 496 | " x = MaxPooling2D((2, 2))(x)\n", 497 | " x = Conv2D(16, (3,3),padding='valid',activation=\"relu\")(x)\n", 498 | " x = MaxPooling2D((2, 2))(x)\n", 499 | " x = Flatten()(x)\n", 500 | " x = Dense(120, activation='relu')(x)\n", 501 | " x = Dense(84, activation='relu')(x)\n", 502 | " x = Dense(10)(x)\n", 503 | " return tf.keras.Model(inputs, x)\n", 504 | "st= model(input_shape=(H, W, 1))\n", 505 | "st.summary()" 506 | ] 507 | }, 508 | { 509 | "cell_type": "code", 510 | "execution_count": 15, 511 | "metadata": {}, 512 | "outputs": [ 513 | { 514 | "name": "stdout", 515 | "output_type": "stream", 516 | "text": [ 517 | "Epoch 1/5\n", 518 | "3750/3750 [==============================] - 76s 20ms/step - loss: 0.4957 - accuracy: 0.8421\n", 519 | "Epoch 2/5\n", 520 | "3750/3750 [==============================] - 76s 20ms/step - loss: 0.1169 - accuracy: 0.9625\n", 521 | "Epoch 3/5\n", 522 | "3750/3750 [==============================] - 74s 20ms/step - loss: 0.0902 - accuracy: 0.9719\n", 523 | "Epoch 4/5\n", 524 | "3750/3750 [==============================] - 72s 19ms/step - loss: 0.0817 - accuracy: 0.9746\n", 525 | "Epoch 5/5\n", 526 | "3750/3750 [==============================] - 73s 19ms/step - loss: 0.0713 - accuracy: 0.9778\n" 527 | ] 528 | }, 529 | { 530 | "data": { 531 | "text/plain": [ 532 | "" 533 | ] 534 | }, 535 | "execution_count": 15, 536 | "metadata": {}, 537 | "output_type": "execute_result" 538 | } 539 | ], 540 | "source": [ 541 | "# 训练\n", 542 | "loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)\n", 543 | "st.compile(optimizer=\"adam\",\n", 544 | " loss=loss_fn,\n", 545 | " metrics=['accuracy'])\n", 546 | "st.fit(tf.concat([x_train_aug, x_train],0), tf.concat([y_train, y_train],0), epochs=5)" 547 | ] 548 | }, 549 | { 550 | "cell_type": "code", 551 | "execution_count": 16, 552 | "metadata": {}, 553 | "outputs": [ 554 | { 555 | "name": "stdout", 556 | "output_type": "stream", 557 | "text": [ 558 | "313/313 [==============================] - 3s 9ms/step - loss: 0.0346 - accuracy: 0.9900\n", 559 | "undistorted origin data: [0.034552980214357376, 0.9900000095367432]\n", 560 | "313/313 [==============================] - 3s 9ms/step - loss: 0.1000 - accuracy: 0.9684\n", 561 | "distorted data: [0.1000135987997055, 0.9684000015258789]\n" 562 | ] 563 | } 564 | ], 565 | "source": [ 566 | "# 评估模型\n", 567 | "print('undistorted origin data:', st.evaluate(x_test, y_test))\n", 568 | "print('distorted data:', st.evaluate(x_test_aug, y_test))" 569 | ] 570 | }, 571 | { 572 | "cell_type": "code", 573 | "execution_count": null, 574 | "metadata": {}, 575 | "outputs": [], 576 | "source": [] 577 | } 578 | ], 579 | "metadata": { 580 | "kernelspec": { 581 | "display_name": "Python 3.7.6 ('tf2')", 582 | "language": "python", 583 | "name": "python3" 584 | }, 585 | "language_info": { 586 | "codemirror_mode": { 587 | "name": "ipython", 588 | "version": 3 589 | }, 590 | "file_extension": ".py", 591 | "mimetype": "text/x-python", 592 | "name": "python", 593 | "nbconvert_exporter": "python", 594 | "pygments_lexer": "ipython3", 595 | "version": "3.7.6" 596 | }, 597 | "orig_nbformat": 4, 598 | "vscode": { 599 | "interpreter": { 600 | "hash": "c6c6e9ad919e43ea991096268ac22857d89ff5f05140928bb8d03f6bb8d6e7c0" 601 | } 602 | } 603 | }, 604 | "nbformat": 4, 605 | "nbformat_minor": 2 606 | } 607 | --------------------------------------------------------------------------------