APP_Framework/Framework/:add NNoM(v0.4.3) source code

This commit is contained in:
WentaoWong
2022-02-18 15:51:18 +08:00
parent e4393985f8
commit cde42331ee
69 changed files with 15430 additions and 0 deletions
@@ -0,0 +1,4 @@
fully_connected_opt_weight_generation.py - is from https://github.com/ARM-software/CMSIS_5/tree/develop/CMSIS/NN/Scripts/NNFunctions witch is not a part of NNoM
Please refer to NNoM documents for its usages.
@@ -0,0 +1 @@
# package
@@ -0,0 +1,153 @@
#!/usr/bin/env python
'''
This file is apart of CMSIS-NN release
https://github.com/ARM-software/CMSIS_5/tree/develop/CMSIS/NN/Scripts/NNFunctions
'''
import numpy as np
def convert_to_x4_q7_weights(weights):
[r, h, w, c] = weights.shape
weights = np.reshape(weights, (r, h*w*c))
num_of_rows = r
num_of_cols = h*w*c
new_weights = np.copy(weights)
new_weights = np.reshape(new_weights, (r*h*w*c))
counter = 0
for i in range(int(num_of_rows/4)):
# we only need to do the re-ordering for every 4 rows
row_base = 4*i
for j in range(int(num_of_cols/4)):
# for each 4 entries
column_base = 4*j
new_weights[counter] = weights[row_base ][column_base ]
new_weights[counter+1] = weights[row_base+1][column_base ]
new_weights[counter+2] = weights[row_base ][column_base+2]
new_weights[counter+3] = weights[row_base+1][column_base+2]
new_weights[counter+4] = weights[row_base+2][column_base ]
new_weights[counter+5] = weights[row_base+3][column_base ]
new_weights[counter+6] = weights[row_base+2][column_base+2]
new_weights[counter+7] = weights[row_base+3][column_base+2]
new_weights[counter+8] = weights[row_base ][column_base+1]
new_weights[counter+9] = weights[row_base+1][column_base+1]
new_weights[counter+10] = weights[row_base ][column_base+3]
new_weights[counter+11] = weights[row_base+1][column_base+3]
new_weights[counter+12] = weights[row_base+2][column_base+1]
new_weights[counter+13] = weights[row_base+3][column_base+1]
new_weights[counter+14] = weights[row_base+2][column_base+3]
new_weights[counter+15] = weights[row_base+3][column_base+3]
counter = counter + 16
# the remaining ones are in order
for j in range((int)(num_of_cols-num_of_cols%4), int(num_of_cols)):
new_weights[counter] = weights[row_base][j]
new_weights[counter+1] = weights[row_base+1][j]
new_weights[counter+2] = weights[row_base+2][j]
new_weights[counter+3] = weights[row_base+3][j]
counter = counter + 4
return new_weights
def convert_to_x4_q15_weights(weights):
[r, h, w, c] = weights.shape
weights = np.reshape(weights, (r, h*w*c))
num_of_rows = r
num_of_cols = h*w*c
new_weights = np.copy(weights)
new_weights = np.reshape(new_weights, (r*h*w*c))
counter = 0
for i in range(int(num_of_rows/4)):
# we only need to do the re-ordering for every 4 rows
row_base = 4*i
for j in range(int(num_of_cols/2)):
# for each 2 entries
column_base = 2*j
new_weights[counter] = weights[row_base ][column_base ]
new_weights[counter+1] = weights[row_base ][column_base+1]
new_weights[counter+2] = weights[row_base+1][column_base ]
new_weights[counter+3] = weights[row_base+1][column_base+1]
new_weights[counter+4] = weights[row_base+2][column_base ]
new_weights[counter+5] = weights[row_base+2][column_base+1]
new_weights[counter+6] = weights[row_base+3][column_base ]
new_weights[counter+7] = weights[row_base+3][column_base+1]
counter = counter + 8
# the remaining ones are in order
for j in range((int)(num_of_cols-num_of_cols%2), int(num_of_cols)):
new_weights[counter] = weights[row_base][j]
new_weights[counter+1] = weights[row_base+1][j]
new_weights[counter+2] = weights[row_base+2][j]
new_weights[counter+3] = weights[row_base+3][j]
counter = counter + 4
return new_weights
def convert_q7_q15_weights(weights):
[r, h, w, c] = weights.shape
weights = np.reshape(weights, (r, h*w*c))
num_of_rows = r
num_of_cols = h*w*c
new_weights = np.copy(weights)
new_weights = np.reshape(new_weights, (r*h*w*c))
counter = 0
for i in range(int(num_of_rows/4)):
# we only need to do the re-ordering for every 4 rows
row_base = 4*i
for j in range(int(num_of_cols/2)):
# for each 2 entries
column_base = 2*j
new_weights[counter] = weights[row_base ][column_base ]
new_weights[counter+1] = weights[row_base+1][column_base ]
new_weights[counter+2] = weights[row_base ][column_base+1]
new_weights[counter+3] = weights[row_base+1][column_base+1]
new_weights[counter+4] = weights[row_base+2][column_base ]
new_weights[counter+5] = weights[row_base+3][column_base ]
new_weights[counter+6] = weights[row_base+2][column_base+1]
new_weights[counter+7] = weights[row_base+3][column_base+1]
counter = counter + 8
# the remaining ones are in order
for j in range((int)(num_of_cols-num_of_cols%2), int(num_of_cols)):
new_weights[counter] = weights[row_base][j]
new_weights[counter+1] = weights[row_base+1][j]
new_weights[counter+2] = weights[row_base+2][j]
new_weights[counter+3] = weights[row_base+3][j]
counter = counter + 4
return new_weights
if __name__ == "__main__":
# input dimensions
vec_dim = 127
row_dim = 127
weight = np.zeros((row_dim,vec_dim), dtype=int)
# generate random inputs
for i in range(row_dim):
for j in range(vec_dim):
weight[i][j] = np.random.randint(256)-128
weight = np.reshape(weight, (row_dim, vec_dim, 1, 1))
outfile = open("../Ref_Implementations/fully_connected_testing_weights.h", "w")
outfile.write("#define IP2_WEIGHT {")
weight.tofile(outfile,sep=",",format="%d")
outfile.write("}\n\n")
new_weight = convert_to_x4_q7_weights(weight)
outfile.write("#define IP4_WEIGHT {")
new_weight.tofile(outfile,sep=",",format="%d")
outfile.write("}\n\n")
new_weight = convert_q7_q15_weights(weight)
outfile.write("#define IP4_q7_q15_WEIGHT {")
new_weight.tofile(outfile,sep=",",format="%d")
outfile.write("}\n\n")
new_weight = convert_to_x4_q15_weights(weight)
outfile.write("#define IP4_WEIGHT_Q15 {")
new_weight.tofile(outfile,sep=",",format="%d")
outfile.write("}\n\n")
outfile.close()
@@ -0,0 +1,561 @@
'''
Copyright (c) 2018-2020
Jianjia Ma
majianjia@live.com
SPDX-License-Identifier: Apache-2.0
Change Logs:
Date Author Notes
2020-05-22 Jianjia Ma The first version
'''
from tensorflow.keras.layers import *
import numpy as np
def convert_tensor_name(t):
return 'tensor_'+t.name.replace('/', '_').replace(':', '_')
def to_cstyle(data, integer=True):
#Convert an array to C style basket, not to be used for very large array. size > options['threshold'] will lead to ...
if(integer):
data = np.array(data, dtype=np.int).flatten()
else:
data = np.array(data).flatten()
s = np.array2string(data, separator=',')
s = s.replace("\n","").replace("\r","").replace(' ','')
s = s.replace(',', ', ')
s = s.replace('(', '[').replace(')', ']')
return s.replace('[', '{').replace(']', '}')
def tensor_shape(tensor, is_io_tensor=False):
# inconsistance of TF1 and TF2
# get tensor shape without None or ?
try:
shape = tensor.shape.as_list() # tf1
except:
shape = tensor.get_shape().as_list() # tf2
if(shape[0] == None or is_io_tensor):
shape = shape[1:]
else:
shape = shape
# for rnn input with timestamp = None, need a better implementation
for i in range(len(shape)):
shape[i] = shape[i] if shape[i] is not None else 1
return shape
def gen_base_config(layer):
config = '{.name = "%s"}' % (layer.name)
return config
def gen_values(var_name, var, size='', dtype='const int8_t'):
s = '<dtype> <var_name>[<size>] = <var>;\n'
s = s.replace('<var_name>', var_name).replace('<var>', var).replace('<size>', size).replace('<dtype>', dtype)
return s
# generate tensor by the tensor config
def gen_tensor(tensor, dec_bits, tensor_value='NULL', per_axis=False, is_io_tensor=False):
config = '''
const nnom_shape_data_t <tensor_name>_dim[] = <dim>;
const nnom_qformat_param_t <tensor_name>_dec[] = <q_dec>;
const nnom_qformat_param_t <tensor_name>_offset[] = <q_offset>;
const nnom_tensor_t <tensor_name> = {
.p_data = (void*)<value>,
.dim = (nnom_shape_data_t*)<tensor_name>_dim,
.q_dec = (nnom_qformat_param_t*)<tensor_name>_dec,
.q_offset = (nnom_qformat_param_t*)<tensor_name>_offset,
.qtype = <qtype>,
.num_dim = <num_dim>,
.bitwidth = <bitwidth>
};
'''
# inconsistance of TF1 and TF2
shape = tensor_shape(tensor, is_io_tensor)
config = config.replace('<tensor_name>', convert_tensor_name(tensor))#.name.replace('/','_').split(':')[0]) #conv2d/kernel:0
config = config.replace('<bitwidth>', '8')
config = config.replace('<value>', tensor_value)
config = config.replace('<dim>', to_cstyle(shape))
config = config.replace('<num_dim>', str(len(shape)))
if(type(dec_bits) == str):
config = config.replace('<q_dec>', dec_bits)
config = config.replace('<q_offset>', to_cstyle([0]))
else:
config = config.replace('<q_dec>', to_cstyle(dec_bits))
config = config.replace('<q_offset>', to_cstyle([0]))
if(per_axis):
config = config.replace('<qtype>', 'NNOM_QTYPE_PER_AXIS')
else:
config = config.replace('<qtype>', 'NNOM_QTYPE_PER_TENSOR')
return config
# create tensor by directly setting up the value
def gen_create_tensor(tensor_name, shape, dec_bits, tensor_value='NULL', per_axis=False):
config = '''
const nnom_shape_data_t <tensor_name>_dim[] = <dim>;
const nnom_qformat_param_t <tensor_name>_dec[] = <q_dec>;
const nnom_qformat_param_t <tensor_name>_offset[] = <q_offset>;
const nnom_tensor_t <tensor_name> = {
.p_data = (void*)<value>,
.dim = (nnom_shape_data_t*)<tensor_name>_dim,
.q_dec = (nnom_qformat_param_t*)<tensor_name>_dec,
.q_offset = (nnom_qformat_param_t*)<tensor_name>_offset,
.qtype = <qtype>,
.num_dim = <num_dim>,
.bitwidth = <bitwidth>
};
'''
config = config.replace('<tensor_name>', tensor_name)
config = config.replace('<bitwidth>', '8')
config = config.replace('<value>', tensor_value)
config = config.replace('<dim>', to_cstyle(shape))
config = config.replace('<num_dim>', str(len(shape)))
if(type(dec_bits) == str):
config = config.replace('<q_dec>', dec_bits)
config = config.replace('<q_offset>', to_cstyle([0]))
else:
config = config.replace('<q_dec>', to_cstyle(dec_bits))
config = config.replace('<q_offset>', to_cstyle([0]))
if(per_axis):
config = config.replace('<qtype>', 'NNOM_QTYPE_PER_AXIS')
else:
config = config.replace('<qtype>', 'NNOM_QTYPE_PER_TENSOR')
return config
def gen_conv2d_config(layer, output_shifts, bias_shifts):
c = '''
const nnom_qformat_param_t <layer_name>_output_shift[] = <output_shift_values>;
const nnom_qformat_param_t <layer_name>_bias_shift[] = <bias_shift_values>;
const nnom_conv2d_config_t <layer_name>_config = {
.super = <base_config>,
.qtype = <qtype>,
.weight = (nnom_tensor_t*)&<weight>,
.bias = (nnom_tensor_t*)&<bias>,
.output_shift = (nnom_qformat_param_t *)&<layer_name>_output_shift,
.bias_shift = (nnom_qformat_param_t *)&<layer_name>_bias_shift,
.filter_size = <filter_size>,
.kernel_size = <kernel_size>,
.stride_size = <stride_size>,
.padding_size = <padding_size>,
.dilation_size = <dilation_size>,
.padding_type = <padding_type>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<qtype>', "NNOM_QTYPE_PER_TENSOR")
c = c.replace('<weight>',convert_tensor_name(layer.weights[0]))
c = c.replace('<bias>',convert_tensor_name(layer.weights[1]))
c = c.replace('<output_shift_values>', output_shifts)
c = c.replace('<bias_shift_values>', bias_shifts)
c = c.replace('<filter_size>', str(layer.filters) if layer.filters is not None else str(layer.depth_multiplier)) # output channel
c = c.replace('<kernel_size>', to_cstyle(layer.kernel_size))
c = c.replace('<stride_size>', to_cstyle(layer.strides))
c = c.replace('<padding_size>', '{0, 0}') # not using it with keras, defined by padding type instead
c = c.replace('<dilation_size>', to_cstyle(layer.dilation_rate))
c = c.replace('<padding_type>', 'PADDING_'+layer.padding.upper())
return c
def gen_conv2d_trans_config(layer, output_shifts, bias_shifts):
c = '''
const nnom_qformat_param_t <layer_name>_output_shift[] = <output_shift_values>;
const nnom_qformat_param_t <layer_name>_bias_shift[] = <bias_shift_values>;
const nnom_conv2d_trans_config_t <layer_name>_config = {
.super = <base_config>,
.qtype = <qtype>,
.weight = (nnom_tensor_t*)&<weight>,
.bias = (nnom_tensor_t*)&<bias>,
.output_shift = (nnom_qformat_param_t *)&<layer_name>_output_shift,
.bias_shift = (nnom_qformat_param_t *)&<layer_name>_bias_shift,
.filter_size = <filter_size>,
.kernel_size = <kernel_size>,
.stride_size = <stride_size>,
.padding_size = <padding_size>,
.dilation_size = <dilation_size>,
.padding_type = <padding_type>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<qtype>', "NNOM_QTYPE_PER_TENSOR")
c = c.replace('<weight>',convert_tensor_name(layer.weights[0]))
c = c.replace('<bias>',convert_tensor_name(layer.weights[1]))
c = c.replace('<output_shift_values>', output_shifts)
c = c.replace('<bias_shift_values>', bias_shifts)
c = c.replace('<filter_size>', str(layer.filters)) # output channel
c = c.replace('<kernel_size>', to_cstyle(layer.kernel_size))
c = c.replace('<stride_size>', to_cstyle(layer.strides))
c = c.replace('<padding_size>', '{0, 0}') # not using it with keras, defined by padding type instead
c = c.replace('<dilation_size>', to_cstyle(layer.dilation_rate))
c = c.replace('<padding_type>', 'PADDING_'+layer.padding.upper())
return c
def gen_dense_config(layer, output_shifts, bias_shift):
c = '''
const nnom_qformat_param_t <layer_name>_output_shift[] = <output_shift_values>;
const nnom_qformat_param_t <layer_name>_bias_shift[] = <bias_shift_values>;
const nnom_dense_config_t <layer_name>_config = {
.super = <base_config>,
.qtype = <qtype>,
.weight = (nnom_tensor_t*)&<weight>,
.bias = (nnom_tensor_t*)&<bias>,
.output_shift = (nnom_qformat_param_t *)&<layer_name>_output_shift,
.bias_shift = (nnom_qformat_param_t *)&<layer_name>_bias_shift
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<qtype>', "NNOM_QTYPE_PER_TENSOR")
c = c.replace('<weight>', convert_tensor_name(layer.weights[0]))
c = c.replace('<bias>', convert_tensor_name(layer.weights[1]))
c = c.replace('<output_shift_values>', output_shifts)
c = c.replace('<bias_shift_values>', bias_shift)
return c
def gen_io_config(layer, tensor_name):
c = '''
const nnom_io_config_t <layer_name>_config = {
.super = <base_config>,
.tensor = (nnom_tensor_t*)&<tensor>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<tensor>', tensor_name)
return c
def gen_output_config(previous_layer, dec_bits, output_num, value_name='nnom_output_data'): #cheat at the moments
c = '''
const nnom_shape_data_t <tensor_name>_dim[] = <dim>;
const nnom_qformat_param_t <tensor_name>_dec[] = <q_dec>;
const nnom_qformat_param_t <tensor_name>_offset[] = <q_offset>;
const nnom_tensor_t <tensor_name> = {
.p_data = (void*)<value>,
.dim = (nnom_shape_data_t*)<tensor_name>_dim,
.q_dec = (nnom_qformat_param_t*)<tensor_name>_dec,
.q_offset = (nnom_qformat_param_t*)<tensor_name>_offset,
.qtype = <qtype>,
.num_dim = <num_dim>,
.bitwidth = 8
};
const nnom_io_config_t <layer_name>_config = {
.super = <base_config>,
.tensor = (nnom_tensor_t*)&<tensor_name>
};
'''
shape = tensor_shape(previous_layer.output, is_io_tensor=True)
c = c.replace('<tensor_name>', 'tensor_output'+str(output_num))
c = c.replace('<layer_name>', 'output'+str(output_num))
c = c.replace('<base_config>', '{.name = "output'+str(output_num)+'"}') # cheating at the moment.
c = c.replace('<value>', value_name)
c = c.replace('<qtype>', 'NNOM_QTYPE_PER_TENSOR')
c = c.replace('<num_dim>', str(len(shape)))
c = c.replace('<dim>', to_cstyle(shape))
c = c.replace('<q_dec>', '{'+dec_bits+'}')
c = c.replace('<q_offset>', to_cstyle([0]))
return c
def gen_pooling_config(layer, output_shifts='0'):
c = '''
const nnom_pool_config_t <layer_name>_config = {
.super = <base_config>,
.padding_type = <padding_type>,
.output_shift = <output_shift>,
.kernel_size = <kernel_size>,
.stride_size = <stride_size>,
.num_dim = <num_dim>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<padding_type>', 'PADDING_'+layer.padding.upper())
c = c.replace('<kernel_size>', to_cstyle(layer.pool_size))
c = c.replace('<stride_size>', to_cstyle(layer.strides))
c = c.replace('<num_dim>', str(len(layer.pool_size)))
c = c.replace('<output_shift>', output_shifts) # not used at the moment
return c
def gen_gl_pooling_config(layer, output_shifts='0'):
c = '''
const nnom_global_pool_config_t <layer_name>_config = {
.super = <base_config>,
.output_shift = <output_shift>,
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<output_shift>', output_shifts)
return c
def gen_matrix_config(layer, output_shift_name='0'):
c = '''
const nnom_matrix_config_t <layer_name>_config = {
.super = <base_config>,
.output_shift = <output_shift>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<output_shift>', output_shift_name) # not used at the moment
return c
def gen_zero_padding_config(layer):
c = '''
const nnom_zero_padding_config_t <layer_name>_config = {
.super = <base_config>,
.pad = <padding>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
try:
c = c.replace('<padding>', to_cstyle(sum(layer.padding, ())))
except:
pad = ((0, 0), layer.padding)
c = c.replace('<padding>', to_cstyle(sum(pad, ())))
return c
def gen_cropping_config(layer):
c = '''
const nnom_cropping_config_t <layer_name>_config = {
.super = <base_config>,
.pad = <padding>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
try:
c = c.replace('<padding>', to_cstyle(sum(layer.cropping, ()))) #((top_crop, bottom_crop), (left_crop, right_crop))
except:
pad = ((0, 0), layer.cropping)
c = c.replace('<padding>', to_cstyle(sum(pad, ())))
return c
def gen_upsampling_config(layer):
c = '''
const nnom_upsample_config_t <layer_name>_config = {
.super = <base_config>,
.kernel = <kernel>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<kernel>', to_cstyle(layer.size))
return c
def gen_softmax_config(layer):
c = '''
const nnom_softmax_config_t <layer_name>_config = {
.super = <base_config>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
return c
def gen_flatten_config(layer):
c = '''
const nnom_flatten_config_t <layer_name>_config = {
.super = <base_config>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
return c
def gen_reshape_config(layer):
c = '''
const nnom_shape_data_t <layer_name>_targeted_shape[] = <shape>;
const nnom_reshape_config_t <layer_name>_config = {
.super = <base_config>,
.dim = (nnom_shape_data_t*)<layer_name>_targeted_shape,
.num_dim = <num_dim>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<shape>', to_cstyle(layer.output_shape[1:]))
c = c.replace('<num_dim>', str(len(layer.output_shape[1:])))
return c
def gen_concat_config(layer):
c = '''
const nnom_concat_config_t <layer_name>_config = {
.super = <base_config>,
.axis = <axis>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<axis>', str(layer.axis))
return c
def gen_lambda_config(layer, run_func_name='NULL', build_func_name='NULL', free_func_name='NULL', parameters_name='NULL'):
c = '''
const nnom_lambda_config_t <layer_name>_config = {
.super = <base_config>,
.run_func_name = <run_func_name>,
.build_func_name = <build_func_name>,
.free_func_name = <free_func_name>,
.parameters = <parameters_name>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<run_func_name>', run_func_name)
c = c.replace('<build_func_name>', build_func_name)
c = c.replace('<free_func_name>', free_func_name)
c = c.replace('<parameters_name>', parameters_name)
return c
def gen_rnn_config(layer):
c = '''
const nnom_rnn_config_t <layer_name>_config = {
.super = <base_config>,
.return_sequence = <return_sequence>,
.stateful = <stateful>,
.go_backwards = <go_backwards>
};
'''
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<stateful>', 'true' if layer.stateful else 'false')
c = c.replace('<go_backwards>', 'true' if layer.go_backwards else 'false')
c = c.replace('<return_sequence>', 'true' if layer.return_sequences else 'false')
return c
def gen_simple_cell_config(layer, q_list):
c = '''
const nnom_simple_cell_config_t <layer_name>_simple_cell_config = {
.super = <base_config>,
.weights = (nnom_tensor_t*)&<weights>,
.recurrent_weights = (nnom_tensor_t*)&<recurrent_weights>,
.bias = (nnom_tensor_t*)&<bias>,
.q_dec_iw = <q_dec_iw>,
.q_dec_hw = <q_dec_hw>,
.q_dec_h = <q_dec_h>,
.act_type = <act_type>,
.units = <units>
};
'''
try:
cell_cfg = layer.get_config()['cell']['config']
except:
cell_cfg = layer.get_config()
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<weights>', convert_tensor_name(layer.weights[0]))
c = c.replace('<recurrent_weights>', convert_tensor_name(layer.weights[1]))
c = c.replace('<bias>', convert_tensor_name(layer.weights[2]))
c = c.replace('<q_dec_iw>', str(q_list[1])) # the qfmt of input x weight
c = c.replace('<q_dec_hw>', str(q_list[2])) # q of hidden x recurrent weight
c = c.replace('<q_dec_h>', str(q_list[0])) # output, if act != relu, should be 7 (consider delete it.)
c = c.replace('<act_type>', 'ACT_' + cell_cfg['activation'].upper())
c = c.replace('<units>', str(cell_cfg['units']))
return c
def gen_lstm_cell_config(layer, q_list):
c = '''
const nnom_lstm_cell_config_t <layer_name>_lstm_cell_config = {
.super = <base_config>,
.weights = (nnom_tensor_t*)&<weights>,
.recurrent_weights = (nnom_tensor_t*)&<recurrent_weights>,
.bias = (nnom_tensor_t*)&<bias>,
.q_dec_z = <q_dec_z>,
.q_dec_h = <q_dec_h>,
.q_dec_c = <q_dec_c>,
.units = <units>
};
'''
try:
cell_cfg = layer.get_config()['cell']['config']
except:
cell_cfg = layer.get_config()
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<weights>', convert_tensor_name(layer.weights[0]))
c = c.replace('<recurrent_weights>', convert_tensor_name(layer.weights[1]))
c = c.replace('<bias>', convert_tensor_name(layer.weights[2]))
c = c.replace('<q_dec_h>', str(q_list[0])) # output and memory state, (should be q0.7. consider delete it)
c = c.replace('<q_dec_c>', str(q_list[1])) # cell state
c = c.replace('<q_dec_z>', str(q_list[2])) # input*weight + hidden*weight + bias
c = c.replace('<units>', str(cell_cfg['units']))
return c
def gen_gru_cell_config(layer, q_list):
c = '''
const nnom_gru_cell_config_t <layer_name>_gru_cell_config = {
.super = <base_config>,
.weights = (nnom_tensor_t*)&<weights>,
.recurrent_weights = (nnom_tensor_t*)&<recurrent_weights>,
.bias = (nnom_tensor_t*)&<bias>,
.q_dec_z = <q_dec_z>,
.q_dec_h = <q_dec_h>,
.units = <units>
};
'''
try:
cell_cfg = layer.get_config()['cell']['config']
except:
cell_cfg = layer.get_config()
c = c.replace('<layer_name>', layer.name)
c = c.replace('<base_config>', gen_base_config(layer))
c = c.replace('<weights>', convert_tensor_name(layer.weights[0]))
c = c.replace('<recurrent_weights>', convert_tensor_name(layer.weights[1]))
c = c.replace('<bias>', convert_tensor_name(layer.weights[2]))
c = c.replace('<q_dec_h>', str(q_list[0])) #
c = c.replace('<q_dec_z>', str(q_list[1])) #
c = c.replace('<units>', str(cell_cfg['units']))
return c
if __name__ == "__main__":
# test only
from tensorflow.keras.models import load_model
model = load_model("../model.h5")
print(gen_tensor(model.layers[1].weights[0], dec_bits=(1, 2, 3, 4, 5)))
print(gen_tensor(model.layers[1].weights[1], dec_bits=(1, 2, 3, 4, 5)))
print(gen_conv2d_config(model.layers[1], (1,2,3), 3))
with open("test.h", 'w') as fp:
# fp.write(gen_tensor(model.layers[1].weights[0], dec_bits=(1, 2, 3, 4, 5)))
# fp.write(gen_tensor(model.layers[1].weights[1], dec_bits=(1, 2, 3, 4, 5)))
# fp.write(gen_conv2d_config(model.layers[1], (1,2,3,)))
fp.write('#include "nnom.h"\n')
# test all
for layer in model.layers:
if(type(layer) in [Conv2D, Conv1D]):
for w in layer.weights:
fp.write(gen_tensor(w, [3]))
fp.write(gen_conv2d_config(layer, {0}, 2))
elif(type(layer) in [Dense]):
for w in layer.weights:
fp.write(gen_tensor(w, [3]))
fp.write(gen_dense_config(layer, 2, 2))
elif(type(layer) in [Input]):
fp.write(gen_io_config(layer, [9,1,1]))
elif(type(layer) in [MaxPooling2D, GlobalMaxPooling2D, AveragePooling2D, GlobalAveragePooling2D]):
fp.write(gen_pooling_config(layer))
elif(type(layer) in [Multiply, Add, Subtract]):
fp.write(gen_matrix_config(layer))
elif(type(layer) in [ZeroPadding2D, ZeroPadding1D]):
fp.write(gen_zero_padding_config(layer))
elif(type(layer) in [Cropping2D, Cropping1D]):
fp.write(gen_cropping_config(layer))
elif(type(layer) in [Softmax]):
fp.write(gen_softmax_config(layer))
elif(type(layer) in [Flatten]):
fp.write(gen_flatten_config(layer))
elif(type(layer) in [Concatenate]):
fp.write(gen_concat_config(layer))
elif(type(layer) in [Lambda]):
fp.write(gen_lambda_config(layer))
elif(type(layer) in [UpSampling2D, UpSampling1D]):
fp.write(gen_upsampling_config(layer))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,845 @@
'''
Copyright (c) 2018-2020
Jianjia Ma
majianjia@live.com
SPDX-License-Identifier: Apache-2.0
Change Logs:
Date Author Notes
2019-02-05 Jianjia Ma The first version
This file provides:
-> fake_quantisation layers which simulate the output quantisation on fixed-point NN models.
-> weights/bias quantisation of Convolution and Dense Layer. "weight.h" file generations
-> export "testing set" binary data file.
-> print output ranges of each layers.
Currently, this script does not support RNN (type) layers.
'''
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.layers import InputLayer
from tensorflow.keras.models import Model
from sklearn import metrics
from .fully_connected_opt_weight_generation import *
import time
import warnings
"""
this is the generate the test set data to a bin file
bin file can be used to validate the implementation in MCU
"""
def generate_test_bin(x, y, name='test_data_with_label.bin'):
'''
this method generate the
:param x: input x data size
:param y: input label (one hot label)
:return:
'''
# quantize input x
min_value = np.min(x)
max_value = np.max(x)
int_bits = int(np.ceil(np.log2(max(abs(min_value), abs(max_value)))))
dec_bits = 7 - int_bits
x = np.round(x*2**dec_bits).astype(np.int8)
# get label
if(len(y.shape) >1):
test_label = np.argwhere(y == 1).astype(np.int8) # test data
test_label = test_label[:, 1]
else:
test_label = y
# get data
dat = x.astype(dtype="byte") # test data
batch_size = dat.shape[0] # total pices of data
dat = dat.flatten() # flatten to get the total size.
block_size = int(dat.size / batch_size) # this must be integer but... just to confirm
# write (label x 128) (data_block x 128)
label_batch = 128 # the Y-modem example uses 128 batch
with open(name, 'wb') as f:
start = 0
while start <= (test_label.size - label_batch):
test_label[start: start + label_batch].tofile(f)
dat[block_size * start: block_size * (start + label_batch)].tofile(f)
start += label_batch
# the rest data
if (start < test_label.size):
rest_len = test_label.size - start
new_labls = test_label[start:]
new_labls = np.pad(new_labls, (0, label_batch - rest_len), mode='constant')
new_labls.tofile(f)
dat[block_size * start:].tofile(f)
print("binary test file generated:", name)
print("test data length:", test_label.size)
return
def is_shift_layer(layer):
''' layer which can change the output encoding'''
#FIXME: add more which will change the output shift
if('input' in layer.name or
'conv2d' in layer.name or
'conv1d' in layer.name or
'dense' in layer.name or
'softmax' in layer.name or
'sigmoid' in layer.name or
'tanh' in layer.name or
('add' in layer.name and 'zero' not in layer.name) or # the name, zero_padding contains 'add'
'subtract' in layer.name or
'multiply' in layer.name or
('activation' in layer.name and layer.get_config()['activation'] == 'softmax')or
('activation' in layer.name and layer.get_config()['activation'] == 'sigmoid') or
('activation' in layer.name and layer.get_config()['activation'] == 'tanh')
):
return True
return False
def is_shift_fixed(layer):
''' layer which shift to a fixed value'''
#FIXME: add more which will change the output shift
if('softmax' in layer.name or
'sigmoid' in layer.name or
'tanh' in layer.name or
('activation' in layer.name and layer.get_config()['activation'] == 'softmax') or
('activation' in layer.name and layer.get_config()['activation'] == 'sigmoid') or
('activation' in layer.name and layer.get_config()['activation'] == 'tanh')
):
return True
return False
def fuse_bn_to_conv(layer):
# try to fuse BN layer to convolutional
if ('conv' in layer.name) and \
('batch_normalization' in layer._outbound_nodes[0].outbound_layer.name):
print("fusing batch normalization to", layer.name)
bn_layer = layer._outbound_nodes[0].outbound_layer
c_w = layer.get_weights()[0]
c_b = layer.get_weights()[1]
print('original weight max', c_w.max(), 'min', c_w.min())
print('original bias max', c_b.max(), 'min', c_b.min())
bn_gamma = bn_layer.get_weights()[0]
bn_beta = bn_layer.get_weights()[1]
bn_mean = bn_layer.get_weights()[2]
bn_variance = bn_layer.get_weights()[3]
if ('conv2d' in layer.name):
epsilon = 1e-3 # default epsilon for tf.slim.batch_norm
for l in range(c_w.shape[3]):
for k in range(c_w.shape[2]):
for j in range(c_w.shape[1]):
for i in range(c_w.shape[0]):
if "depthwise" in layer.name: # depthwise batchnorm params are ordered differently
c_w[i][j][k][l] *= bn_gamma[k] / np.sqrt(bn_variance[k] + epsilon)
else:
c_w[i][j][k][l] *= bn_gamma[l] / np.sqrt(bn_variance[l] + epsilon)
if "depthwise" in layer.name:
depth_dim = c_w.shape[2]
else:
depth_dim = c_w.shape[3]
for l in range(depth_dim):
c_b[l] = (bn_gamma[l] * (c_b[l] - bn_mean[l]) / np.sqrt(bn_variance[l] + epsilon)) + bn_beta[l]
# conv1d
else:
epsilon = 1e-3 # default epsilon for tf.slim.batch_norm
for k in range(c_w.shape[2]):
for j in range(c_w.shape[1]):
for i in range(c_w.shape[0]):
if "depthwise" in layer.name: # depthwise batchnorm params are ordered differently
c_w[i][j][k] *= bn_gamma[j] / np.sqrt(bn_variance[j] + epsilon)
else:
c_w[i][j][k] *= bn_gamma[k] / np.sqrt(bn_variance[k] + epsilon)
if "depthwise" in layer.name:
depth_dim = c_w.shape[1]
else:
depth_dim = c_w.shape[2]
for l in range(depth_dim):
c_b[l] = (bn_gamma[l] * (c_b[l] - bn_mean[l]) / np.sqrt(bn_variance[l] + epsilon)) + bn_beta[l]
print('fused weight max', c_w.max(), 'min', c_w.min())
print('fused bias max', c_b.max(), 'min', c_b.min())
# write the weights back to the layer
# after that, the model will be destroyed.. need a better way to pass the new weight
layer.set_weights([c_w, c_b])
def generate_weights(model, name='weights.h', format='hwc', shift_list=None):
# Quantize weights to 8-bits using (min,max) and write to file
f = open(name, 'w')
f.write('#include "nnom.h"\n\n')
f.close()
for curr_idx, layer in enumerate(model.layers):
if (not layer.weights):
continue
# before merging bn layer, check if the bn is "legally" after Conv
if('batch_normalization' in layer.name) and \
('conv' not in layer.inbound_nodes[0].inbound_layers.name):
raise Exception('Currently only support batch_normalization after conv', layer.name,
layer._inbound_nodes[0].inbound_layers[0].name)
# try to fuse BN layer to convolutional
if ('conv' in layer.name) and \
('batch_normalization' in layer.outbound_nodes[0].outbound_layer.name):
fuse_bn_to_conv(layer)
# generate weights and bias now
weight_dec_shift = 0
print('weights for layer', layer.name)
for var in layer.weights:
var_name = str(var.name)
if("kernel" in var_name ):
var_values = layer.get_weights()[0] # weight
print(" weight:", var_name)
elif("bias" in var_name):
var_values = layer.get_weights()[1] # bias
print(" bias: ",var_name)
else:
continue
print(" original shape: ", var_values.shape)
min_value = np.min(var_values)
max_value = np.max(var_values)
int_bits = int(np.ceil(np.log2(max(abs(min_value), abs(max_value)))))
dec_bits = 7 - int_bits
print(" dec bit", dec_bits)
bSameAsKernel = False
if(is_shift_layer(layer)):
bSameAsKernel = False
inp = layer.input.name.replace(':','/').split('/')[0]
input_encoding = shift_list[inp]
if ("kernel" in var_name):
weight_dec_shift = dec_bits
else:
shift = input_encoding+weight_dec_shift-dec_bits
if(shift < 0):
bSameAsKernel = True
if(shift_list is None or bSameAsKernel):
# check if bias shift > weight shift, then reduce bias shift to weight shift
if ("kernel" in var_name):
weight_dec_shift = dec_bits
else:
if(dec_bits > weight_dec_shift):
dec_bits = weight_dec_shift
print(" new dec bit", dec_bits)
# convert to [-128,128) or int8
var_values = np.round(var_values * 2 ** dec_bits)
var_name = var_name.replace('/', '_')
var_name = var_name.replace(':', '_')
with open(name, 'a') as f:
f.write('#define ' + var_name.upper() + ' {')
# CHW format
if ('chw' in format):
if "dense" in var_name and "kernel" in var_name:
transposed_wts = np.transpose(var_values)
transposed_wts = convert_to_x4_q7_weights(
np.reshape(transposed_wts, (transposed_wts.shape[0], transposed_wts.shape[1], 1, 1)))
# all other kernels, bias stay the same
else:
transposed_wts = var_values
# HWC format
else:
if (len(var_values.shape) == 3): # 1D convolution layer weights
transposed_wts = np.transpose(var_values, (2, 0, 1))
elif (len(var_values.shape) == 4): # 2D convolution layer weights
transposed_wts = np.transpose(var_values, (3, 0, 1, 2))
else: # fully connected layer weights or biases of any layer
# test, use opt weight reorder
if "dense" in var_name and "kernel" in var_name:
transposed_wts = np.transpose(var_values)
transposed_wts = convert_to_x4_q7_weights(np.reshape(transposed_wts ,(transposed_wts.shape[0], transposed_wts.shape[1], 1, 1)))
else:
transposed_wts = np.transpose(var_values)
print(" reshape to:",transposed_wts.shape)
with open(name, 'a') as f:
transposed_wts.tofile(f, sep=", ", format="%d")
f.write('}\n\n')
if ("bias" in var_name):
f.write('#define ' + var_name.upper() + '_SHIFT ' + '(' + str(dec_bits) + ')\n\n\n')
if ("kernel" in var_name ):
f.write('#define ' + var_name.upper() + '_SHIFT ' + '(' + str(dec_bits) + ')\n\n')
"""
# for checking the quantised and dequantised range.
with K.tf.Session() as session:
# convert back original range but quantized to 8-bits or 256 levels
var_values = var_values / (2 ** dec_bits)
var_values = session.run(K.tf.assign(var, var_values))
print(' '+var_name + ' number of wts/bias: ' + str(var_values.shape) + \
' dec bits: ' + str(dec_bits) + \
' max: (' + str(np.max(var_values)) + ',' + str(max_value) + ')' + \
' min: (' + str(np.min(var_values)) + ',' + str(min_value) + ')')
"""
def layers_output_ranges(model, x_test, quantize_method='max_min', calibrate_size=1000):
# limit the test data size
np.random.shuffle(x_test)
if(x_test.shape[0] > calibrate_size):
x_test = x_test[:1000]
# test, show the output ranges
shift_list = {}
# FIXME: only support one input
if(type(model.layers[0]) != InputLayer):
L = [model.input] + model.layers
else:
L = model.layers
last_layer = None
for layer in L: # layer loop
if("input" in layer.name):
features = x_test
else:
# batch_normalization will need to be handled differently, since we are fusing the weight to its predecessor.
# sigmoid and tanh are different, their shift is fixed to 7
if(is_shift_layer(layer) or
('batch_normalization' in layer.name)):
layer_model = Model(inputs=model.input, outputs=layer.output)
features = layer_model.predict(x_test)
else:
# leave the features not changed, so this layer shift will be the same
# as its inputs
pass
# calculate no saturation shift
max_val = features.max()
min_val = features.min()
int_bits = int(np.ceil(np.log2(max(abs(max_val), abs(min_val)))))
dec_bits = 7 - int_bits
# saturation shift, using KLD method
# Ref: http://on-demand.gputechconf.com/gtc/2017/presentation/s7310-8-bit-inference-with-tensorrt.pdf
if('kld' in quantize_method and not is_shift_fixed(layer) and "input" not in layer.name and "dense" not in layer.name): # test, also do not use kld in input layer
import scipy.stats
abs_max = max(abs(max_val), abs(min_val))
small_var = 1e-5
bins = np.arange(-abs_max, abs_max, abs_max/2048*2)
q_bins = np.arange(-abs_max, abs_max, abs_max/256*2)
flat_hist = np.histogram(features.flatten(), bins=bins)[0]
kl_loss = []
kl_shifts = []
for shift in range(4):
t = 2 ** (dec_bits + shift) # 2-based threshold
act = np.round(features.flatten() * t)
act = act / t
act = np.clip(act, -128/t, 127/t)
act = np.histogram(act, bins=q_bins)[0]
act_hist = np.zeros(2047)
chunk = int(2048/256)
for i in range(int(255)):
none_zero = np.count_nonzero(flat_hist[i*chunk:(i+1)*chunk])
if none_zero == 0:
continue
for j in range(chunk):
act_hist[i*chunk+j] = act[i]/none_zero if flat_hist[i*chunk+j] != 0 else 0
flat_hist[flat_hist==0] = small_var
act_hist[act_hist==0] = small_var
kl = scipy.stats.entropy(flat_hist, act_hist)
kl_loss.append(kl)
kl_shifts.append(dec_bits + shift)
"""
ax = plt.subplot(8, 1, shift+1)
ax.plot(flat_hist)
ax.plot(act_hist)
"""
new_dec = kl_shifts[np.argmin(kl_loss)] # set the dec_bit to the KLD results
#plt.show()
print("KLD loss", kl_loss)
print("KLD shift", kl_shifts)
if(new_dec != dec_bits):
print(layer.name,"is using KLD method, original shift",dec_bits, "KLD results", new_dec)
dec_bits = new_dec
print( layer.name, "max value:", max_val, "min value:", min_val,"dec bit", dec_bits)
# record the shift
if(type(model.input) == tf.Tensor and type(model.layers[0]) != InputLayer):
shift_list[layer.name.split(':')[0]] = dec_bits
else:
shift_list[layer.name] = dec_bits
if ('batch_normalization' in layer.name):
shift_list[last_layer.name] = dec_bits # use the bn layer shift to update the last layer.
last_layer = layer
LM = {}
for layer in model.layers:
LM[layer.name] = layer
L = [l for l in model.layers[1:]]
L.reverse()
def update_previous_layer_shift(layer, Q):
if(type(layer.input) == list):
for inp in layer.input:
iname = inp.name.split('/')[0]
if('input' in iname):
continue
shift_list[iname] = Qmin
if(not is_shift_layer(LM[iname])):
update_previous_layer_shift(LM[iname], Q)
else:
iname = layer.input.name.split('/')[0]
if('input' in iname):
return
shift_list[iname] = Qmin
if(not is_shift_layer(LM[iname])):
update_previous_layer_shift(LM[iname], Q)
for layer in L:
if(type(layer.input) == list):
iname = layer.input[0].name.split('/')[0]
Qmin = shift_list[iname]
for inp in layer.input:
iname = inp.name.split('/')[0]
if(shift_list[iname] < Qmin):
Qmin = shift_list[iname]
if(shift_list[iname] != Qmin):
bFlag = True
for inp in layer.input:
iname = inp.name.split('/')[0]
shift_list[iname] = Qmin
if(not is_shift_layer(LM[iname])):
update_previous_layer_shift(LM[iname], Qmin)
print('set shift', Qmin, 'for the input of', layer.name, ':', [inp.name.split('/')[0] for inp in layer.input])
if(not is_shift_layer(layer) or Qmin < shift_list[layer.name]): # update current layer's shift only when we cannot change the shift
shift_list[layer.name] = Qmin
print("shift list", shift_list)
return shift_list
def generate_model(model, x_test, name='weights.h', format='hwc', quantize_method='max_min'):
shift_list = layers_output_ranges(model, x_test, quantize_method=quantize_method)
generate_weights(model, name=name, format=format, shift_list=shift_list)
if(type(model.layers[0]) != InputLayer):
L = [model.input] + model.layers
else:
L = model.layers
with open(name,'a') as fp:
fp.write('\n/* output enconding for each layer */\n')
for layer in L:
if(type(model.input) == tf.Tensor and type(model.layers[0]) != InputLayer):
iname = layer.name.split(':')[0]
else:
iname = layer.name
fp.write('#define %s_OUTPUT_SHIFT %s\n'%(iname.upper(), shift_list[iname]))
fp.write('\n/* bias shift and output shift for each layer */\n')
for layer in model.layers:
if(is_shift_layer(layer)):
iname = layer.name.upper()
if(len(layer.weights) == 2 and
'kernel' in layer.weights[0].name and
'bias' in layer.weights[1].name):
kname = layer.weights[0].name.upper().replace('/', '_').replace(':', '_')
bname = layer.weights[1].name.upper().replace('/', '_').replace(':', '_')
inp = layer.input.name.replace(':','/').split('/')[0].upper()
fp.write('#define {0}_OUTPUT_RSHIFT ({1}_OUTPUT_SHIFT+{2}_SHIFT-{0}_OUTPUT_SHIFT)\n'.format(
iname, inp, kname))
fp.write('#define {0}_BIAS_LSHIFT ({1}_OUTPUT_SHIFT+{2}_SHIFT-{3}_SHIFT)\n'.format(
iname, inp, kname, bname))
fp.write('#if {0}_OUTPUT_RSHIFT < 0\n#error {0}_OUTPUT_RSHIFT must be bigger than 0\n#endif\n'.format(iname))
fp.write('#if {0}_BIAS_LSHIFT < 0\n#error {0}_BIAS_RSHIFT must be bigger than 0\n#endif\n'.format(iname))
# add, sub
elif ('add' in layer.name or
'subtract' in layer.name):
# only consider the first, they have been set to same in out_put_range()
inp = layer.input[0].name.replace(':','/').split('/')[0].upper()
fp.write('#define {0}_OUTPUT_RSHIFT ({1}_OUTPUT_SHIFT-{0}_OUTPUT_SHIFT)\n'.format(
iname, inp))
fp.write('#if {0}_OUTPUT_RSHIFT < 0\n#error {0}_OUTPUT_RSHIFT must be bigger than 0\n#endif\n'.format(iname))
# mult is different, Q3.4 * Q3.4 = Q6.8. if mult out is Q4.3, then shift (Q.4+q.4)-Q.3=5. Am I right?
elif ('multiply' in layer.name ):
inp = layer.input[0].name.replace(':','/').split('/')[0].upper()
fp.write('#define {0}_OUTPUT_RSHIFT ({1}_OUTPUT_SHIFT*2-{0}_OUTPUT_SHIFT)\n'.format(
iname, inp))
fp.write('#if {0}_OUTPUT_RSHIFT < 0\n#error {0}_OUTPUT_RSHIFT must be bigger than 0\n#endif\n'.format(iname))
fp.write('\n/* weights for each layer */\n')
LI = {}
ID = 0
def is_skipable_layer(layer):
# FIXME: add more that could be skiped
if('lambda' in layer.name or
'dropout' in layer.name or
'batch_normalization' in layer.name or
('flatten' in layer.name and 'chw' not in format)): # flatten layer can be skipped in HWC but have to present in CHW
return True
return False
for id,layer in enumerate(L):
if(is_skipable_layer(layer)):
inp = layer.input.name.replace(':','/').split('/')[0]
LI[layer.name] = (LI[inp][0], layer)
else:
if(type(model.input) == tf.Tensor and type(model.layers[0]) != InputLayer):
LI[layer.name.split(':')[0]] = (ID, layer)
else:
LI[layer.name] = (ID, layer)
ID += 1
if ('input' in layer.name or not layer.weights):
continue
for var in layer.weights:
var_name = str(var.name).replace('/', '_').replace(':', '_')
if("kernel" in var_name):
fp.write('static const int8_t %s_weights[] = %s;\n'%(layer.name, var_name.upper()))
fp.write('static const nnom_weight_t %s_w = { (const void*)%s_weights, %s_OUTPUT_RSHIFT};\n'%(layer.name,layer.name, layer.name.upper()))
elif("bias" in var_name):
fp.write('static const int8_t %s_bias[] = %s;\n'%(layer.name, var_name.upper()))
fp.write('static const nnom_bias_t %s_b = { (const void*)%s_bias, %s_BIAS_LSHIFT};\n'%(layer.name,layer.name, layer.name.upper()))
fp.write('\n/* nnom model */\n')
# FIXME: now only support one input and one output
sz = 1
for d in model.input.shape[1:]:
sz = sz*d
fp.write('static int8_t nnom_input_data[%d];\n'%(sz))
sz = 1
for d in model.output.shape[1:]:
sz = sz*d
fp.write('static int8_t nnom_output_data[%d];\n'%(sz))
fp.write('static nnom_model_t* nnom_model_create(void)\n{\n')
fp.write('\tstatic nnom_model_t model;\n')
if(ID>32):
fp.write('\tnnom_layer_t ** layer = malloc(sizeof(nnom_layer_t *)*%d);\n'%(ID+1))
fp.write('\tif(NULL == layer) return NULL;\n')
else:
fp.write('\tnnom_layer_t* layer[%d];\n'%(ID+1))
fp.write('\n\tnew_model(&model);\n\n')
for layer in L:
if(is_skipable_layer(layer)):
continue
#FIXME: need a better solution to seperate the input 'tensor' from other layers
if (type(model.input) == tf.Tensor and type(model.layers[0]) != InputLayer):
id,_ = LI[layer.name.split(':')[0]]
else:
id,_ = LI[layer.name]
if('input' in layer.name):
try:
inshape = layer.input_shape[0][1:] # new changes in tf2?
except:
inshape = layer.shape[1:]
if (len(inshape) == 1): # 1-D input
fp.write('\tlayer[%d] = Input(shape(%d,1,1), nnom_input_data);\n' % (id, inshape[0]))
elif (len(inshape) == 2): # 1-D input
fp.write('\tlayer[%d] = Input(shape(1,%d,%d), nnom_input_data);\n' % (id, inshape[0], inshape[1]))
else:
fp.write('\tlayer[%d] = Input(shape%s, nnom_input_data);\n' % (id, inshape))
# convlutional
elif('conv1d' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if('depthwise' in layer.name):
fp.write('\tlayer[{0}] = model.hook(DW_Conv2D({1}, kernel(1,{2}), stride(1,{3}), dilation(1,{4}), PADDING_{5}, &{6}_w, &{6}_b), layer[{7}]);\n'.format(
id, 1, cfg['kernel_size'][0], cfg['strides'][0], cfg['dilation_rate'][0], cfg['padding'].upper(),
layer.name, LI[inp][0]))
else:
fp.write('\tlayer[{0}] = model.hook(Conv2D({1}, kernel(1,{2}), stride(1,{3}), dilation(1,{4}), PADDING_{5}, &{6}_w, &{6}_b), layer[{7}]);\n'.format(
id, cfg['filters'], cfg['kernel_size'][0], cfg['strides'][0], cfg['dilation_rate'][0], cfg['padding'].upper(),
layer.name, LI[inp][0]))
elif('conv2d' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if ('depthwise' in layer.name):
fp.write('\tlayer[{0}] = model.hook(DW_Conv2D({1}, kernel{2}, stride{3}, dilation{4}, PADDING_{5}, &{6}_w, &{6}_b), layer[{7}]);\n'.format(
id, 1, cfg['kernel_size'], cfg['strides'], cfg['dilation_rate'], cfg['padding'].upper(),
layer.name, LI[inp][0]))
else:
fp.write('\tlayer[{0}] = model.hook(Conv2D({1}, kernel{2}, stride{3}, dilation{4}, PADDING_{5}, &{6}_w, &{6}_b), layer[{7}]);\n'.format(
id, cfg['filters'], cfg['kernel_size'], cfg['strides'], cfg['dilation_rate'], cfg['padding'].upper(),
layer.name, LI[inp][0]))
# activations
elif('activation' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if(cfg['activation'] == 'relu'):
fp.write('\tlayer[%s] = model.active(act_relu(), layer[%s]);\n'%(id, LI[inp][0]))
if(cfg['activation'] == 'tanh'):
fp.write('\tlayer[%s] = model.active(act_tanh(%s_OUTPUT_SHIFT), layer[%s]);\n'%(id, inp.upper(), LI[inp][0]))
if(cfg['activation'] == 'sigmoid'):
fp.write('\tlayer[%s] = model.active(act_sigmoid(%s_OUTPUT_SHIFT), layer[%s]);\n'%(id, inp.upper(), LI[inp][0]))
elif(cfg['activation'] == 'softmax'):
fp.write('\tlayer[%s] = model.hook(Softmax(), layer[%s]);\n'%(id, LI[inp][0]))
elif('re_lu' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
fp.write('\tlayer[%s] = model.active(act_relu(), layer[%s]);\n'%(id, LI[inp][0]))
# pooling
elif('max_pooling' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if ('global' in layer.name):
fp.write('\tlayer[%s] = model.hook(GlobalMaxPool(), layer[%s]);\n' % (id, LI[inp][0]))
elif('2d' in layer.name):
fp.write('\tlayer[%s] = model.hook(MaxPool(kernel%s, stride%s, PADDING_%s), layer[%d]);\n'%(
id, cfg['pool_size'], cfg['strides'], cfg['padding'].upper(), LI[inp][0]))
elif('1d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(MaxPool(kernel(1,{1}), stride(1,{2}), PADDING_{3}), layer[{4}]);\n'.format(
id, cfg['pool_size'][0], cfg['strides'][0], cfg['padding'].upper(), LI[inp][0]))
elif('average_pooling' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if ('global' in layer.name):
# a global avg pool before softmax can be replace by sumpool in MCU (recommend)
if(layer == model.layers[-2] and 'Softmax' in model.layers[-1].output.name):
print(layer.name, 'has been replaced by GlobalSumPool()')
fp.write('\tlayer[%s] = model.hook(GlobalSumPool(), layer[%s]);\n' % (id, LI[inp][0]))
else:
fp.write('\tlayer[%s] = model.hook(GlobalAvgPool(), layer[%s]);\n' % (id, LI[inp][0]))
elif('2d' in layer.name):
fp.write('\tlayer[%s] = model.hook(AvgPool(kernel%s, stride%s, PADDING_%s), layer[%d]);\n'%(
id, cfg['pool_size'], cfg['strides'], cfg['padding'].upper(), LI[inp][0]))
elif('1d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(AvgPool(kernel(1,{1}), stride(1,{2}), PADDING_{3}), layer[{4}]);\n'.format(
id, cfg['pool_size'][0], cfg['strides'][0], cfg['padding'].upper(), LI[inp][0]))
elif ('up_sampling' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if('2d' in layer.name):
fp.write('\tlayer[%s] = model.hook(UpSample(kernel%s), layer[%d]);\n'%(id, cfg['size'], LI[inp][0]))
elif('1d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(UpSample(kernel(1,{1})), layer[{2}]);\n'.format(
id, cfg['size'][0], LI[inp][0]))
# zero padding
elif ('zero_padding' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if('2d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(ZeroPadding(border({1},{2},{3},{4})), layer[{5}]);\n'.format(
id, cfg['padding'][0][0], cfg['padding'][0][1], cfg['padding'][1][0],cfg['padding'][1][1], LI[inp][0]))
elif('1d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(ZeroPadding(border(0,0,{1},{2})), layer[{3}]);\n'.format(
id, cfg['padding'][0], cfg['padding'][1], LI[inp][0]))
# Cropping
elif ('cropping' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
if('2d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(Cropping(border({1},{2},{3},{4})), layer[{5}]);\n'.format(
id, cfg['cropping'][0][0], cfg['cropping'][0][1], cfg['cropping'][1][0],cfg['cropping'][1][1], LI[inp][0]))
elif('1d' in layer.name):
fp.write('\tlayer[{0}] = model.hook(Cropping(border(0,0,{1},{2})), layer[{3}]);\n'.format(
id, cfg['cropping'][0], cfg['cropping'][1], LI[inp][0]))
# others
elif('flatten' in layer.name): # flatten is needed in CHW backend but not needed in HWC
inp = layer.input.name.replace(':', '/').split('/')[0]
fp.write('\tlayer[%s] = model.hook(Flatten(), layer[%s]);\n'%(id, LI[inp][0]))
elif('concatenate' in layer.name):
inps = [input.name.replace(':','/').split('/')[0] for input in layer.input]
inX = ''
for inp in inps:
inX += ' ,layer[%d]'%(LI[inp][0])
cfg = layer.get_config()
fp.write('\tlayer[%s] = model.mergex(Concat(%s), %s%s);\n'%(
id, cfg['axis'], len(inps), inX))
elif('add' in layer.name):
inps = [input.name.replace(':','/').split('/')[0] for input in layer.input]
inX = ''
for inp in inps:
inX += ' ,layer[%d]'%(LI[inp][0])
fp.write('\tlayer[%s] = model.mergex(Add(%s_OUTPUT_RSHIFT), %s%s);\n'%(
id, layer.name.upper(), len(inps), inX))
elif('subtract' in layer.name):
inps = [input.name.replace(':','/').split('/')[0] for input in layer.input]
inX = ''
for inp in inps:
inX += ' ,layer[%d]'%(LI[inp][0])
fp.write('\tlayer[%s] = model.mergex(Sub(%s_OUTPUT_RSHIFT), %s%s);\n'%(
id, layer.name.upper(), len(inps), inX))
elif('multiply' in layer.name):
warnings.warn("Warning mutiply is under testing")
inps = [input.name.replace(':','/').split('/')[0] for input in layer.input]
inX = ''
for inp in inps:
inX += ' ,layer[%d]'%(LI[inp][0])
fp.write('\tlayer[%s] = model.mergex(Mult(%s_OUTPUT_RSHIFT), %s%s);\n'%(
id, layer.name.upper(), len(inps), inX))
elif('dense' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
cfg = layer.get_config()
fp.write('\tlayer[{0}] = model.hook(Dense({1}, &{2}_w, &{2}_b), layer[{3}]);\n'.format(
id, cfg['units'], layer.name, LI[inp][0]))
elif('softmax' in layer.name):
inp = layer.input.name.replace(':','/').split('/')[0]
fp.write('\tlayer[%s] = model.hook(Softmax(), layer[%s]);\n'%(id, LI[inp][0]))
else:
raise Exception('unsupported layer', layer.name, layer)
"""
# temporary fixed for activations attached into layers in construction
def is_activation_attached(layer):
if(("Softmax" in layer.output.name and "softmax" not in layer.name)or
("Relu" in layer.output.name and "re_lu" not in layer.name) or
("Sigmoid" in layer.output.name and "sigmoid" not in layer.name) or
("Tanh" in layer.output.name and "tanh" not in layer.name)):
return True
return False
if "input" not in layer.name and is_activation_attached(layer):
inp = layer.output.name.replace(':', '/').split('/')[0]
cfg = layer.get_config()
if(cfg['activation'] == 'relu'):
fp.write('\tlayer[%s] = model.active(act_relu(), layer[%s]);\n'%(id, LI[inp][0]))
if(cfg['activation'] == 'tanh'):
fp.write('\tlayer[%s] = model.active(act_tanh(%s_OUTPUT_SHIFT), layer[%s]);\n'%(id, inp.upper(), LI[inp][0]))
if(cfg['activation'] == 'sigmoid'):
fp.write('\tlayer[%s] = model.active(act_sigmoid(%s_OUTPUT_SHIFT), layer[%s]);\n'%(id, inp.upper(), LI[inp][0]))
elif(cfg['activation'] == 'softmax'):
fp.write('\tlayer[%s] = model.hook(Softmax(), layer[%s]);\n'%(id, LI[inp][0]))
"""
# FIXME, test later.
if('softmax' in layer.name
or ('activation' in layer.name and layer.get_config()['activation'] == 'softmax')):
fp.write('\tlayer[%s] = model.hook(Output(shape(%s,1,1), nnom_output_data), layer[%s]);\n'%(id+1, layer.output.shape[1], id))
elif len(layer.output.shape) == 4:
fp.write('\tlayer[%s] = model.hook(Output(shape%s, nnom_output_data), layer[%s]);\n'%(id+1, layer.output.shape[1:], id))
elif len(layer.output.shape) == 3:
fp.write('\tlayer[%s] = model.hook(Output(shape(1,%s,%s), nnom_output_data), layer[%s]);\n'%(id+1, layer.output.shape[1], layer.output.shape[2], id))
elif len(layer.output.shape) == 2:
fp.write('\tlayer[%s] = model.hook(Output(shape(%s,1,1), nnom_output_data), layer[%s]);\n'%(id+1, layer.output.shape[1], id))
else:
raise Exception('unsupported output shape of the last layer', layer.name, layer)
fp.write('\tmodel_compile(&model, layer[0], layer[%s]);\n'%(id+1))
if(ID>32):
fp.write('\tfree(layer);\n')
fp.write('\treturn &model;\n}\n')
with open('.shift_list','w') as fp:
fp.write(str(shift_list))
def evaluate_model(model, x_test, y_test, running_time=False, to_file='evaluation.txt'):
# Score trained model.
scores = model.evaluate(x_test, y_test, verbose=2)
print('Test loss:', scores[0])
print('Top 1:', scores[1])
if(len(y_test.shape)>1):
# predictions = model.predict(x_test)
# output = tf.keras.metrics.top_k_categorical_accuracy(y_test, predictions, k=2)
# # with tf.Session() as sess:
# # result = sess.run(output)
# result =
# print("Top 2:",result)
predictions = model.predict(x_test)
matrix = metrics.confusion_matrix(y_test.argmax(axis=1), predictions.argmax(axis=1))
print(matrix)
run_time = 0
if running_time:
# try to calculate the time
T = time.time()
for i in range(10):
model.predict(x_test)
T = time.time() - T
run_time = round((T / 10 / x_test.shape[0] * 1000 * 1000), 2)
print("Runing time:",run_time , "us" )
#
with open(to_file, 'w') as f:
f.write("Runing time: "+ str(run_time) + "us" + "\n")
f.write('Test loss:'+ str(scores[0]) + "\n")
f.write('Top 1:'+ str(scores[1])+ "\n")
if (len(y_test.shape) > 1):
#f.write("Top 2:"+ str(result)+ "\n")
#f.write(str(matrix))
for row in matrix:
row.tofile(f, sep=',')
f.write("\n")
# try to check the weight and bias dec ranges
for layer in model.layers:
if (not layer.weights):
continue
for var in layer.weights:
var_name = str(var.name)
if ("kernel" in var_name):
var_values = layer.get_weights()[0] # weight
else:
var_values = layer.get_weights()[1] # bias
min_value = np.min(var_values)
max_value = np.max(var_values)
intt = int(np.ceil(np.log2(max(abs(min_value), abs(max_value)))))
dec = 7 - intt
print(var_name, "Dec num:", dec)
return scores
def f2q(d, Q):
'''To convert a number from floating point to Qm.n format:
1. Multiply the floating point number by 2n
2. Round to the nearest integer
'''
return np.round(d*2**Q)
def q2f(d, Q):
'''To convert a number from Qm.n format to floating point:
1. Convert the number to floating point as if it were an integer, in other words remove the binary point
2. Multiply by 2-n
'''
return d*2**-Q
def show_weights(w, name):
sz = 1
for s in w.shape:
sz = sz*s
aL = w.reshape(sz,)
MIN,MAX=min(aL),max(aL)
Q = int(np.ceil(np.log2(max(abs(MIN),abs(MAX)))))
Q = 7-Q
qL = f2q(aL,Q)
qL = q2f(qL,Q)
plt.figure(figsize=(18, 3))
plt.subplot(131)
plt.title(name)
plt.plot(aL)
plt.grid()
aL.sort()
plt.plot(aL,'r')
plt.grid()
plt.subplot(132)
plt.title('Q%s'%(Q))
qL.sort()
plt.plot(aL,'r')
plt.plot(qL,'g')
plt.grid()
plt.subplot(133)
plt.hist(aL,100)
plt.title('hist')
plt.grid()
plt.show()
def compare(a,b,name):
sz = 1
for s in a.shape:
sz = sz*s
aL = a.reshape(sz,)
bL = b.reshape(sz,)
assert(len(aL) == len(bL))
Z = list(zip(aL,bL))
Z.sort(key=lambda x: x[0])
aL1,bL1=zip(*Z)
plt.figure(figsize=(18, 3))
plt.subplot(131)
plt.plot(aL)
plt.plot(aL1,'r')
plt.grid()
plt.title('tf-%s'%(name))
plt.subplot(133)
plt.plot(bL1,'g')
plt.plot(aL1,'r')
plt.grid()
plt.title('compare')
plt.subplot(132)
bL1=list(bL1)
bL1.sort()
plt.plot(bL)
plt.plot(bL1,'g')
plt.grid()
plt.title('nn-%s'%(name))
plt.show()