Files
openvino/model-optimizer/extensions/middle/ConvToBinaryConv.py
T
Alexey Suhov 6478f1742a Align copyright notice in python scripts (CVS-51320) (#4974)
* Align copyright notice in python scripts (CVS-51320)
2021-03-26 17:54:28 +03:00

113 lines
4.7 KiB
Python

# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import logging as log
import numpy as np
from extensions.ops.elementwise import Mul, Add
from mo.front.tf.graph_utils import create_op_node_with_second_input
from mo.graph.graph import Graph
from mo.middle.replacement import MiddleReplacementPattern
class ConvToBinaryConv(MiddleReplacementPattern):
""" Transform usual convolution with [0,+1] input and [-1,+1] to BinaryConvolution
Modifies output terms after the Convolution to be able to apply BinaryConvolution
operation instead that accepts [-1,1] input and [-1,1] weights. It requires modification
channel-wise addition with weights reduced along all axis except output channel dimension.
"""
enabled = True
force_clean_up = True
def pattern(self):
return dict(
nodes=[
# This pass is applicable for binarization only. Other intX variants are not relevant.
('quantize', dict(kind='op', op='FakeQuantize', levels=2)),
('quantized', dict()), # input tensor, not weights
('operator', dict(kind='op', type='Convolution')),
],
edges=[
('quantize', 'quantized'),
('quantized', 'operator', {'in':0}),
]
)
def replace_pattern(self, graph: Graph, match: dict):
assert match['operator'].has('multiplication_transparent_ports')
quantize = match['quantize']
port = match['operator'].input_ports_with(match['quantized'])
assert len(port) >= 1
if len(port) > 1:
log.debug('BinarizeWeightsM1P1 cannot apply transformation for data {} because it consumed more'
' than once'.format(match['quantized'].name))
return
assert len(port) == 1
port = port[0]
applicable = [pair for pair in match['operator'].multiplication_transparent_ports if pair[0] == port]
if len(applicable) == 0:
return
# Look at 3-rd and 4-th inputs of FakeQuantize -- they have constants that should be passed through.
# Assume that the constant that should be passed through is a scalar.
output_low = quantize.in_node(3)
output_high = quantize.in_node(4)
assert len(output_low.out_nodes()) == 1
assert len(output_high.out_nodes()) == 1
if not output_low.has_valid('value') and not output_high.has_valid('value'):
return
output_low = output_low.value
output_high = output_high.value
operator = match['operator']
weights = operator.in_node(1).value
weights_rounded = np.round(weights)
weights_consistent = np.all(np.isclose(weights, weights_rounded)) and \
set(np.unique(weights_rounded)).issubset({-1, 1})
if weights_consistent and np.all(np.isclose(output_low, 0)) and np.all(np.isclose(output_high, 1)):
reduction_indices = set(range(len(weights.shape))) - set([operator.output_feature_channel])
weights_reduced = np.add.reduce(weights, axis=tuple(reduction_indices))
weights_reduced = weights_reduced.reshape([len(weights_reduced), 1, 1]) # FIXME: works for NCHW only
operator_name = operator.soft_get('name', operator.id)
add = create_op_node_with_second_input(graph, Add, weights_reduced, {'name': operator_name + '/Add_'})
mul = create_op_node_with_second_input(graph, Mul, np.array(0.5), {'name': operator_name + '/Mul_'})
add.out_port(0).connect(mul.in_port(0))
operator.out_port(0).get_connection().set_source(mul.out_port(0))
add.in_port(0).connect(operator.out_port(0))
operator['pad_value'] = float(-1.0)
elif weights_consistent and np.all(np.isclose(output_low, -1)) and np.all(np.isclose(output_high, +1)):
pass
else:
log.debug('ConvToBinaryConv: cannot apply transformation because input range is neither in [0, +1] nor '
'in [-1, +1].')
return
operator['type'] = 'BinaryConvolution'
operator['mode'] = 'xnor-popcount'
operator['pad_value'] = operator.soft_get('pad_value', float(0))
operator['input'] = operator.in_node(0).shape[1]
# Weights are not bit-packed yet; there should be a separate transformation to do that
assert output_low.size == 1
assert output_high.size == 1
output_low = quantize.in_node(3)
output_high = quantize.in_node(4)
# Make sure that low/high values are exactly 0/1
output_low.value = np.zeros(output_low.shape)
output_high.value = np.ones(output_high.shape)