Source code for cnn2snn.converter

#!/usr/bin/env python
# ******************************************************************************
# Copyright 2019 Brainchip Holdings Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ******************************************************************************
"""Conversion of a Keras/CNN2SNN model into an Akida model"""

import os
import tensorflow as tf
from keras import Sequential
from .model_generator import generate_model as cnn2snn_generate_model
from .quantizeml import generate_model as qml_generate_model

from .transforms import sequentialize, syncretize
from .compatibility_checks import check_sequential_compatibility


def _sync_and_check_model(model, input_is_image):
    # Make sure the model is sequential
    seq_model = sequentialize(model)

    # For now, we support only models with a single branch
    if not isinstance(seq_model, Sequential):
        raise RuntimeError(
            "The model contains more than one sequential branch.")

    # Transform model to prepare conversion: change the order of layers,
    # fold BN, freeze quantizers, remove useless layers.
    sync_model = syncretize(seq_model)

    # Check model compatibility
    check_sequential_compatibility(sync_model, input_is_image)

    return sync_model


[docs]def convert(model, file_path=None, input_scaling=None, input_is_image=True): """Converts a Keras quantized model to an Akida one. This method is compatible with model quantized with :func:`cnn2snn.quantize` and :func:`quantizeml.quantize`. To check the difference between the two conversion processes check the methods _convert_cnn2snn and _convert_quantizeml below. Args: model (:obj:`tf.keras.Model`): a tf.keras model file_path (str, optional): destination for the akida model. (Default value = None) input_scaling (2 elements tuple, optional): value of the input scaling. (Default value = None) input_is_image (bool, optional): True if input is an image (3-D 8-bit input with 1 or 3 channels) followed by QuantizedConv2D. Akida model input will be InputConvolutional. If False, Akida model input will be InputData. (Default value = True) Returns: :obj:`akida.Model`: an Akida model. """ if not tf.executing_eagerly(): raise SystemError("Tensorflow eager execution is disabled. " "It is required to convert Keras weights to Akida.") # Check if the model has been quantized with quantizeml by checking quantized layers type cnn2snn_model = not any("quantizeml" in str(type(layer)) for layer in model.layers) # Convert the model if cnn2snn_model: ak_model = _convert_cnn2snn(model, input_scaling, input_is_image) else: ak_model = _convert_quantizeml(model, input_is_image) # Save model if file_path is given if file_path: # Create directories dir_name, base_name = os.path.split(file_path) if base_name: file_root, file_ext = os.path.splitext(base_name) if not file_ext: file_ext = '.fbz' else: file_root = model.name file_ext = '.fbz' if dir_name and not os.path.exists(dir_name): os.makedirs(dir_name) save_path = os.path.join(dir_name, file_root + file_ext) ak_model.save(save_path) return ak_model
def _convert_quantizeml(model, input_is_image): """Converts a Keras quantized model with quantizeml to an Akida one. After quantizing a Keras model with :func:`quantizeml.quantize`, it can be converted to an Akida model. Args: model (:obj:`tf.keras.Model`): a tf.keras model input_is_image (bool): True if input is an 8-bit unsigned tensors (like images). Returns: :obj:`akida.Model`: an Akida model. """ # Generate Akida model with empty weights/thresholds for now ak_model = qml_generate_model(model, input_is_image) return ak_model def _convert_cnn2snn(model, input_scaling=None, input_is_image=True): """Converts a Keras quantized model to an Akida one. After quantizing a Keras model with :func:`cnn2snn.quantize`, it can be converted to an Akida model. By default, the conversion expects that the Akida model takes 8-bit images as inputs. ``input_scaling`` defines how the images have been rescaled to be fed into the Keras model (see note below). If inputs are spikes, you can set ``input_is_image=False``. In this case, Akida inputs are then expected to be integers between 0 and 15. Note: The relationship between Keras and Akida inputs is defined as:: input_akida = input_scaling[0] * input_keras + input_scaling[1]. If a :class:`tf.keras.layers.Rescaling` layer is present as first layer of the model, ``input_scaling`` must be None: the :class:`Rescaling` parameters will be used to compute the input scaling. Examples: >>> # Convert a quantized Keras model with Keras inputs as images >>> # rescaled between -1 and 1 >>> inputs_akida = images.astype('uint8') >>> inputs_keras = (images.astype('float32') - 128) / 128 >>> model_akida = cnn2snn.convert(model_keras, input_scaling=(128, 128)) >>> model_akida.predict(inputs_akida) >>> # Convert a quantized Keras model with Keras inputs as spikes and >>> # input scaling of (2.5, 0). Akida spikes must be integers between >>> # 0 and 15 >>> inputs_akida = spikes.astype('uint8') >>> inputs_keras = spikes.astype('float32') / 2.5 >>> model_akida = cnn2snn.convert(model_keras, input_scaling=(2.5, 0)) >>> model_akida.predict(inputs_akida) >>> # Convert and directly save the Akida model to fbz file. >>> cnn2snn.convert(model_keras, 'model_akida.fbz') Args: model (:obj:`tf.keras.Model`): a tf.keras model input_scaling (2 elements tuple, optional): value of the input scaling. (Default value = None) input_is_image (bool, optional): True if input is an image (3-D 8-bit input with 1 or 3 channels) followed by QuantizedConv2D. Akida model input will be InputConvolutional. If False, Akida model input will be InputData. (Default value = True) Returns: :obj:`akida.Model`: an Akida model. Raises: ValueError: If ``input_scaling[0]`` is null or negative. ValueError: If a :class:`Rescaling` layer is present and ``input_scaling`` is not None. SystemError: If Tensorflow is not run in eager mode. """ # Check Keras Rescaling layer to replace the input_scaling rescaling_input_scaling = _get_rescaling_layer_params(model) if rescaling_input_scaling is not None and input_scaling is not None: raise ValueError("If a Rescaling layer is present in the model, " "'input_scaling' argument must be None. Receives " f"{input_scaling}.") input_scaling = rescaling_input_scaling or input_scaling or (1, 0) if input_scaling[0] <= 0: raise ValueError("The scale factor 'input_scaling[0]' must be strictly" f" positive. Receives: input_scaling={input_scaling}") # Prepare model for conversion and check its compatibility sync_model = _sync_and_check_model(model, input_is_image) # Generate Akida model with converted weights/thresholds ak_model = cnn2snn_generate_model(sync_model, input_scaling, input_is_image) return ak_model def _get_rescaling_layer_params(model): """Computes the new input scaling retrieved from the Keras `Rescaling` layer. Keras Rescaling layer works as: input_k = scale * input_ak + offset CNN2SNN input scaling works as: input_ak = input_scaling[0] * input_k + input_scaling[1] Equivalence leads to: input_scaling[0] = 1 / scale input_scaling[1] = -offset / scale Args: model (:obj:`tf.keras.Model`): a tf.keras model. Returns: tuple: the new input scaling from the Rescaling layer or None if no Rescaling layer is at the beginning of the model. """ Rescaling = tf.keras.layers.Rescaling for layer in model.layers[:2]: if isinstance(layer, Rescaling): return (1 / layer.scale, -layer.offset / layer.scale) return None
[docs]def check_model_compatibility(model, input_is_image=True): r"""Checks if a Keras model is compatible for cnn2snn conversion. This function doesn't convert the Keras model to an Akida model but only checks if the model design is compatible. Note that this function doesn't check if the model is compatible with Akida hardware. To check compatibility with a specific hardware device, convert the model and call `model.map` with this device as argument. **1. How to build a compatible Keras quantized model?** The following lines give details and constraints on how to build a Keras model compatible for the conversion to an Akida model. **2. General information about layers** An Akida layer must be seen as a block of Keras layers starting with a processing layer (Conv2D, SeparableConv2D, Dense). All blocks of Keras layers except the last block must have exactly one activation layer (ReLU or ActivationDiscreteRelu). Other optional layers can be present in a block such as a pooling layer or a batch normalization layer. Here are all the supported Keras layers for an Akida-compatible model: - Processing layers: - tf.keras Conv2D/SeparableConv2D/Dense - cnn2snn QuantizedConv2D/QuantizedSeparableConv2D/QuantizedDense - Activation layers: - tf.keras ReLU - cnn2snn ActivationDiscreteRelu - any increasing activation function (only for the last block of layers) such as softmax, sigmoid set as last layer. This layer must derive from tf.keras.layers.Activation, and it will be removed during conversion to an Akida model. - Pooling layers: - MaxPool2D - GlobalAvgPool2D - BatchNormalization - Dropout - Flatten - Input - Reshape Example of a block of Keras layers:: ---------- | Conv2D | ---------- || \/ ---------------------- | BatchNormalization | ---------------------- || \/ ------------- | MaxPool2D | ------------- || \/ -------------------------- | ActivationDiscreteRelu | -------------------------- **3. Constraints about inputs** An Akida model can accept two types of inputs: sparse events or 8-bit images. Whatever the input type, the Keras inputs must respect the following relation: input_akida = scale * input_keras + shift where the Akida inputs must be positive integers, the input scale must be a float value and the input shift must be an integer. In other words, scale * input_keras must be integers. Depending on the input type: - if the inputs are events (sparse), the first layer of the Keras model can be any processing layer. The input shift must be zero. - if the inputs are images, the first layer must be a Conv2D layer. **4. Constraints about layers' parameters** To be Akida-compatible, the Keras layers must observe the following rules: - all layers with the 'data_format' parameter must be 'channels_last' - all processing quantized layers and ActivationDiscreteRelu must have a valid quantization bitwidth - a Dense layer must have an input shape of (N,) or (1, 1, N) - a BatchNormalization layer must have 'axis' set to -1 (default) - a BatchNormalization layer cannot have negative gammas - Reshape layers can only be used to transform a tensor of shape (N,) to a tensor of shape (1, 1, N), and vice-versa - only one pooling layer can be used in each block - a MaxPool2D layer must have the same 'padding' as the corresponding processing quantized layer **5. Constraints about the order of layers** To be Akida-compatible, the order of Keras layers must observe the following rules: - a block of Keras layers must start with a processing quantized layer - where present, a BatchNormalization/GlobalAvgPool2D layer must be placed before the activation - a Flatten layer can only be used before a Dense layer - an Activation layer other than ReLU can only be used in the last layer Args: model (:obj:`tf.keras.Model`): the model to check. input_is_image (bool, optional): True if input is an image (8-bit input with 1 or 3 channels) followed by QuantizedConv2D. Akida model input will be InputConvolutional. If False, Akida model input will be InputData. (Default value = True) """ try: _sync_and_check_model(model, input_is_image) return True except RuntimeError as e: print( "The Keras quantized model is not compatible for a conversion " "to an Akida model:\n", str(e)) return False