Skip to content

Commit

Permalink
Better support for converted Keras models (#12)
Browse files Browse the repository at this point in the history
* support activation layers (with dense only) and ignore inputlayer

* clean up

* test new support

* bump version
  • Loading branch information
sshane authored Jul 14, 2021
1 parent dd0cd3f commit 76995e1
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 45 deletions.
21 changes: 13 additions & 8 deletions konverter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from konverter.utils.model_attributes import Activations, Layers, watermark
from konverter.utils.model_attributes import Activations, Layers, LAYERS_IGNORED, watermark
from konverter.utils.konverter_support import KonverterSupport
from konverter.utils.general import success, error, info, warning, COLORS
import numpy as np
Expand Down Expand Up @@ -45,7 +45,7 @@ def start(self):
self.get_layers()
if self.verbose:
self.print_model_architecture()
self.remove_unused_layers()
self.remove_ignored_layers()
self.parse_output_file()
self.build_konverted_model()

Expand All @@ -72,7 +72,7 @@ def build_konverted_model(self):
if layer.name == Layers.Dense.name:
model_line = f'l{idx} = {layer.string.format(prev_output, idx, idx)}'
model_builder['model'].append(model_line)
if layer.info.has_activation:
if layer.info.has_activation and layer.info.activation.name != Activations.Linear.name:
if layer.info.activation.needs_function:
lyr_w_act = f'l{idx} = {layer.info.activation.alias.lower()}(l{idx})'
else: # eg. tanh or relu
Expand Down Expand Up @@ -165,8 +165,8 @@ def save_model(self, model_builder):
with open(f'{self.output_file}.py', 'w') as f:
f.write(output.replace('\t', self.indent))

def remove_unused_layers(self):
self.layers = [layer for layer in self.layers if layer.name not in support.unused_layers]
def remove_ignored_layers(self):
self.layers = [layer for layer in self.layers if layer.name not in LAYERS_IGNORED]

def parse_output_file(self):
if self.output_file is None: # user hasn't supplied output file path, use input file name in same dir
Expand All @@ -186,7 +186,8 @@ def parse_output_file(self):
def print_model_architecture(self):
success('\nSuccessfully got model architecture! 😄\n')
info('Layers:')
to_print = [[COLORS.BASE.format(74) + f'name: {layer.alias}' + COLORS.ENDC] for layer in self.layers]
ignored_txt = {True: ' (ignored)', False: ''}
to_print = [[COLORS.BASE.format(74) + f'name: {layer.alias}{ignored_txt[layer.info.is_ignored]}' + COLORS.ENDC] for layer in self.layers]
max_len = 0
indentation = ' '
for idx, layer in enumerate(self.layers):
Expand All @@ -205,8 +206,12 @@ def print_model_architecture(self):
print(COLORS.ENDC, end='')

def get_layers(self):
for layer in self.model.layers:
layer = support.get_layer_info(layer)
for idx, layer in enumerate(self.model.layers):
next_layer = None
if idx < len(self.model.layers) - 1:
next_layer = self.model.layers[idx + 1]

layer = support.get_layer_info(layer, next_layer)
if layer.info.supported:
self.layers.append(layer)
else:
Expand Down
2 changes: 1 addition & 1 deletion konverter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import konverter
from konverter.utils.general import success, info, warning, error, COLORS, color_logo, blue_grad

KONVERTER_VERSION = "v0.2.4.1" # fixme: unify this
KONVERTER_VERSION = "v0.2.5" # fixme: unify this
KONVERTER_LOGO_COLORED = color_logo(KONVERTER_VERSION)


Expand Down
75 changes: 43 additions & 32 deletions konverter/utils/konverter_support.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from konverter.utils.model_attributes import BaseLayerInfo, BaseModelInfo, Models, Activations, Layers
from konverter.utils.model_attributes import BaseLayerInfo, BaseModelInfo, Models, Activations, Layers, \
LAYERS_NO_ACTIVATION, LAYERS_IGNORED, LAYERS_RECURRENT
import numpy as np


Expand All @@ -8,11 +9,6 @@ def __init__(self):
self.layers = [getattr(Layers, i) for i in dir(Layers) if '_' not in i]
self.activations = [getattr(Activations, i) for i in dir(Activations) if '_' not in i]

self.attrs_without_activations = [Layers.Dropout.name, Activations.Linear.name, Layers.BatchNormalization.name]
self.unused_layers = [Layers.Dropout.name]
self.recurrent_layers = [Layers.SimpleRNN.name, Layers.GRU.name]
self.ignored_layers = [Layers.Dropout.name]

def get_class_from_name(self, name, search_in):
"""
:param name: A name of an attribute, ex. keras.layers.Dense, keras.activations.relu
Expand Down Expand Up @@ -73,63 +69,78 @@ def get_model_info(self, model):

return model_class

def get_layer_info(self, layer):
@staticmethod
def _get_layer_name(layer):
name = getattr(layer, '_keras_api_names_v1')
if not len(name):
name = getattr(layer, '_keras_api_names')
return name

def _get_layer_activation(self, layer):
if hasattr(layer.activation, '_keras_api_names'):
activation = getattr(layer.activation, '_keras_api_names')
else: # fixme: TF 2.3 is missing _keras_api_names
activation = 'keras.activations.' + getattr(layer.activation, '__name__')
activation = (activation,)

if len(activation) == 1:
return self.get_class_from_name(activation[0], 'activations')
else:
raise Exception('None or multiple activations?')

def get_layer_info(self, layer, next_layer):
# Identify layer
name = self._get_layer_name(layer)
layer_class = self.get_class_from_name(name[0], 'layers') # assume only one name
layer_class.info = BaseLayerInfo()
if not layer_class:
layer_class = Layers.Unsupported() # add activation below to raise exception with
layer_class.name = name

layer_class.info.is_ignored = layer_class.name in self.ignored_layers

is_linear = False
if layer_class.name not in self.attrs_without_activations:
if hasattr(layer.activation, '_keras_api_names'):
activation = getattr(layer.activation, '_keras_api_names')
else: # fixme: TF 2.3 is missing _keras_api_names
activation = 'keras.activations.' + getattr(layer.activation, '__name__')
activation = (activation,) # fixme: expects this as a tuple

if len(activation) == 1:
layer_class.info.activation = self.get_class_from_name(activation[0], 'activations')
if layer_class.info.activation.name not in self.attrs_without_activations:
layer_class.info.has_activation = True
else:
is_linear = True
else:
raise Exception('None or multiple activations?')

if layer_class.info.has_activation:
if layer_class.info.activation.name == 'keras.layers.LeakyReLU': # set alpha
layer_class.info.is_ignored = layer_class.name in LAYERS_IGNORED

# Handle layer activation
if layer_class.name not in LAYERS_NO_ACTIVATION:
layer_class.info.activation = self._get_layer_activation(layer)
layer_class.info.has_activation = True

# Note: special case for when activation is a separate layer after dense
if layer_class.name == Layers.Dense.name and layer_class.info.activation.name == Activations.Linear.name:
if next_layer is not None and self._get_layer_name(next_layer)[0] == Layers.Activation.name:
layer_class.info.activation = self._get_layer_activation(next_layer)

# Check if layer is supported given ignored status and activation
if layer_class.info.has_activation and not layer_class.info.is_ignored:
if layer_class.info.activation.name == Activations.LeakyReLU.name: # set alpha
layer_class.info.activation.alpha = round(float(layer.activation.alpha), 5)

# check layer activation against this layer's supported activations
if layer_class.info.activation.name in self.attr_map(layer_class.supported_activations, 'name'):
layer_class.info.supported = True
elif layer_class.info.is_ignored or is_linear: # skip activation check if layer has no activation (eg. dropout or linear)

elif layer_class.info.is_ignored:
layer_class.info.supported = True
elif layer_class.name in self.attrs_without_activations:

elif layer_class.name in LAYERS_NO_ACTIVATION: # skip activation check if layer has no activation (eg. dropout)
layer_class.info.supported = True

# if not layer_class.info.supported or (not is_linear and not layer_class.info.has_activation):
# return layer_class
if not layer_class.info.supported:
return layer_class

# Parse weights and biases from layer if available
try:
wb = layer.get_weights()
if len(wb) == 0:
return layer_class
except:
return layer_class

if len(wb) == 2:
if len(wb) == 2: # Dense
layer_class.info.weights = np.array(wb[0])
layer_class.info.biases = np.array(wb[1])
elif len(wb) == 3 and layer_class.name in self.recurrent_layers:
elif len(wb) == 3 and layer_class.name in LAYERS_RECURRENT:
layer_class.info.weights = np.array(wb[:2]) # input and recurrent weights
layer_class.info.biases = np.array(wb[-1])
layer_class.info.returns_sequences = layer.return_sequences
Expand Down
14 changes: 14 additions & 0 deletions konverter/utils/model_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ class Dropout(_BaseLayer):
name = 'keras.layers.Dropout'
alias = 'dropout'

class InputLayer(_BaseLayer):
name = 'keras.layers.InputLayer'
alias = 'InputLayer'

class Activation(_BaseLayer):
name = 'keras.layers.Activation'
alias = 'Activation'

class BatchNormalization(_BaseLayer):
name = 'keras.layers.BatchNormalization'
alias = 'batch_norm'
Expand Down Expand Up @@ -125,6 +133,12 @@ class Unsupported(_BaseLayer): # propogated with layer info and returned to Kon
pass


# LAYERS_NO_ACTIVATION = [Layers.Dropout.name, Layers.InputLayer.name, Activations.Linear.name, Layers.BatchNormalization.name]
LAYERS_NO_ACTIVATION = [Layers.Dropout.name, Layers.InputLayer.name, Layers.BatchNormalization.name]
LAYERS_IGNORED = [Layers.Dropout.name, Layers.InputLayer.name, Layers.Activation.name]
LAYERS_RECURRENT = [Layers.SimpleRNN.name, Layers.GRU.name]


class BaseModelInfo:
supported = False
input_shape = None # this will need to be moved if we support functional models
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "keras-konverter"
version = "0.2.4.1"
version = "0.2.5"
description = "A tool to convert simple Keras models to pure Python + NumPy"
readme = "README.md"
repository = "https://github.com/ShaneSmiskol/Konverter"
Expand Down
9 changes: 6 additions & 3 deletions tests/build_test_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
from tensorflow.keras.layers import Dense, SimpleRNN, GRU, BatchNormalization
from tensorflow.keras.layers import Dense, SimpleRNN, GRU, BatchNormalization, InputLayer, Activation
from tensorflow.keras.models import Sequential
from tensorflow.keras.backend import clear_session

Expand All @@ -14,9 +14,12 @@ def create_model(model_type):
y_train = (np.mean(x_train, axis=1) ** 2) / 2 # half of squared mean of sample

model = Sequential()
model.add(Dense(128, activation='relu', input_shape=x_train.shape[1:]))
model.add(InputLayer(input_shape=x_train.shape[1:]))
model.add(Dense(128))
model.add(Activation(activation='relu'))
model.add(BatchNormalization())
model.add(Dense(64, activation='tanh'))
model.add(Dense(64))
model.add(Activation(activation='tanh'))
model.add(BatchNormalization())
model.add(Dense(32, activation='relu'))
model.add(BatchNormalization())
Expand Down

0 comments on commit 76995e1

Please sign in to comment.