|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Tensorflow ops to calibrate class predictions and background class.""" |
|
|
|
import tensorflow.compat.v1 as tf |
|
from object_detection.utils import shape_utils |
|
|
|
|
|
def _find_interval_containing_new_value(x, new_value): |
|
"""Find the index of x (ascending-ordered) after which new_value occurs.""" |
|
new_value_shape = shape_utils.combined_static_and_dynamic_shape(new_value)[0] |
|
x_shape = shape_utils.combined_static_and_dynamic_shape(x)[0] |
|
compare = tf.cast(tf.reshape(new_value, shape=(new_value_shape, 1)) >= |
|
tf.reshape(x, shape=(1, x_shape)), |
|
dtype=tf.int32) |
|
diff = compare[:, 1:] - compare[:, :-1] |
|
interval_idx = tf.argmin(diff, axis=1) |
|
return interval_idx |
|
|
|
|
|
def _tf_linear_interp1d(x_to_interpolate, fn_x, fn_y): |
|
"""Tensorflow implementation of 1d linear interpolation. |
|
|
|
Args: |
|
x_to_interpolate: tf.float32 Tensor of shape (num_examples,) over which 1d |
|
linear interpolation is performed. |
|
fn_x: Monotonically-increasing, non-repeating tf.float32 Tensor of shape |
|
(length,) used as the domain to approximate a function. |
|
fn_y: tf.float32 Tensor of shape (length,) used as the range to approximate |
|
a function. |
|
|
|
Returns: |
|
tf.float32 Tensor of shape (num_examples,) |
|
""" |
|
x_pad = tf.concat([fn_x[:1] - 1, fn_x, fn_x[-1:] + 1], axis=0) |
|
y_pad = tf.concat([fn_y[:1], fn_y, fn_y[-1:]], axis=0) |
|
interval_idx = _find_interval_containing_new_value(x_pad, x_to_interpolate) |
|
|
|
|
|
alpha = ( |
|
(x_to_interpolate - tf.gather(x_pad, interval_idx)) / |
|
(tf.gather(x_pad, interval_idx + 1) - tf.gather(x_pad, interval_idx))) |
|
interpolation = ((1 - alpha) * tf.gather(y_pad, interval_idx) + |
|
alpha * tf.gather(y_pad, interval_idx + 1)) |
|
|
|
return interpolation |
|
|
|
|
|
def _function_approximation_proto_to_tf_tensors(x_y_pairs_message): |
|
"""Extracts (x,y) pairs from a XYPairs message. |
|
|
|
Args: |
|
x_y_pairs_message: calibration_pb2..XYPairs proto |
|
Returns: |
|
tf_x: tf.float32 tensor of shape (number_xy_pairs,) for function domain. |
|
tf_y: tf.float32 tensor of shape (number_xy_pairs,) for function range. |
|
""" |
|
tf_x = tf.convert_to_tensor([x_y_pair.x |
|
for x_y_pair |
|
in x_y_pairs_message.x_y_pair], |
|
dtype=tf.float32) |
|
tf_y = tf.convert_to_tensor([x_y_pair.y |
|
for x_y_pair |
|
in x_y_pairs_message.x_y_pair], |
|
dtype=tf.float32) |
|
return tf_x, tf_y |
|
|
|
|
|
def _get_class_id_function_dict(calibration_config): |
|
"""Create a dictionary mapping class id to function approximations. |
|
|
|
Args: |
|
calibration_config: calibration_pb2 proto containing |
|
id_function_approximations. |
|
Returns: |
|
Dictionary mapping a class id to a tuple of TF tensors to be used for |
|
function approximation. |
|
""" |
|
class_id_function_dict = {} |
|
class_id_xy_pairs_map = ( |
|
calibration_config.class_id_function_approximations.class_id_xy_pairs_map) |
|
for class_id in class_id_xy_pairs_map: |
|
class_id_function_dict[class_id] = ( |
|
_function_approximation_proto_to_tf_tensors( |
|
class_id_xy_pairs_map[class_id])) |
|
|
|
return class_id_function_dict |
|
|
|
|
|
def build(calibration_config): |
|
"""Returns a function that calibrates Tensorflow model scores. |
|
|
|
All returned functions are expected to apply positive monotonic |
|
transformations to inputs (i.e. score ordering is strictly preserved or |
|
adjacent scores are mapped to the same score, but an input of lower value |
|
should never be exceed an input of higher value after transformation). For |
|
class-agnostic calibration, positive monotonicity should hold across all |
|
scores. In class-specific cases, positive monotonicity should hold within each |
|
class. |
|
|
|
Args: |
|
calibration_config: calibration_pb2.CalibrationConfig proto. |
|
Returns: |
|
Function that that accepts class_predictions_with_background and calibrates |
|
the output based on calibration_config's parameters. |
|
Raises: |
|
ValueError: No calibration builder defined for "Oneof" in |
|
calibration_config. |
|
""" |
|
|
|
|
|
|
|
if calibration_config.WhichOneof('calibrator') == 'function_approximation': |
|
|
|
def calibration_fn(class_predictions_with_background): |
|
"""Calibrate predictions via 1-d linear interpolation. |
|
|
|
Predictions scores are linearly interpolated based on a class-agnostic |
|
function approximation. Note that the 0-indexed background class is also |
|
transformed. |
|
|
|
Args: |
|
class_predictions_with_background: tf.float32 tensor of shape |
|
[batch_size, num_anchors, num_classes + 1] containing scores on the |
|
interval [0,1]. This is usually produced by a sigmoid or softmax layer |
|
and the result of calling the `predict` method of a detection model. |
|
|
|
Returns: |
|
tf.float32 tensor of the same shape as the input with values on the |
|
interval [0, 1]. |
|
""" |
|
|
|
flat_class_predictions_with_background = tf.reshape( |
|
class_predictions_with_background, shape=[-1]) |
|
fn_x, fn_y = _function_approximation_proto_to_tf_tensors( |
|
calibration_config.function_approximation.x_y_pairs) |
|
updated_scores = _tf_linear_interp1d( |
|
flat_class_predictions_with_background, fn_x, fn_y) |
|
|
|
|
|
original_detections_shape = shape_utils.combined_static_and_dynamic_shape( |
|
class_predictions_with_background) |
|
calibrated_class_predictions_with_background = tf.reshape( |
|
updated_scores, |
|
shape=original_detections_shape, |
|
name='calibrate_scores') |
|
return calibrated_class_predictions_with_background |
|
|
|
elif (calibration_config.WhichOneof('calibrator') == |
|
'class_id_function_approximations'): |
|
|
|
def calibration_fn(class_predictions_with_background): |
|
"""Calibrate predictions per class via 1-d linear interpolation. |
|
|
|
Prediction scores are linearly interpolated with class-specific function |
|
approximations. Note that after calibration, an anchor's class scores will |
|
not necessarily sum to 1, and score ordering may change, depending on each |
|
class' calibration parameters. |
|
|
|
Args: |
|
class_predictions_with_background: tf.float32 tensor of shape |
|
[batch_size, num_anchors, num_classes + 1] containing scores on the |
|
interval [0,1]. This is usually produced by a sigmoid or softmax layer |
|
and the result of calling the `predict` method of a detection model. |
|
|
|
Returns: |
|
tf.float32 tensor of the same shape as the input with values on the |
|
interval [0, 1]. |
|
|
|
Raises: |
|
KeyError: Calibration parameters are not present for a class. |
|
""" |
|
class_id_function_dict = _get_class_id_function_dict(calibration_config) |
|
|
|
|
|
|
|
|
|
class_tensors = tf.unstack(class_predictions_with_background, axis=-1) |
|
calibrated_class_tensors = [] |
|
for class_id, class_tensor in enumerate(class_tensors): |
|
flat_class_tensor = tf.reshape(class_tensor, shape=[-1]) |
|
if class_id in class_id_function_dict: |
|
output_tensor = _tf_linear_interp1d( |
|
x_to_interpolate=flat_class_tensor, |
|
fn_x=class_id_function_dict[class_id][0], |
|
fn_y=class_id_function_dict[class_id][1]) |
|
else: |
|
tf.logging.info( |
|
'Calibration parameters for class id `%d` not not found', |
|
class_id) |
|
output_tensor = flat_class_tensor |
|
calibrated_class_tensors.append(output_tensor) |
|
|
|
combined_calibrated_tensor = tf.stack(calibrated_class_tensors, axis=1) |
|
input_shape = shape_utils.combined_static_and_dynamic_shape( |
|
class_predictions_with_background) |
|
calibrated_class_predictions_with_background = tf.reshape( |
|
combined_calibrated_tensor, |
|
shape=input_shape, |
|
name='calibrate_scores') |
|
return calibrated_class_predictions_with_background |
|
|
|
elif (calibration_config.WhichOneof('calibrator') == |
|
'temperature_scaling_calibration'): |
|
|
|
def calibration_fn(class_predictions_with_background): |
|
"""Calibrate predictions via temperature scaling. |
|
|
|
Predictions logits scores are scaled by the temperature scaler. Note that |
|
the 0-indexed background class is also transformed. |
|
|
|
Args: |
|
class_predictions_with_background: tf.float32 tensor of shape |
|
[batch_size, num_anchors, num_classes + 1] containing logits scores. |
|
This is usually produced before a sigmoid or softmax layer. |
|
|
|
Returns: |
|
tf.float32 tensor of the same shape as the input. |
|
|
|
Raises: |
|
ValueError: If temperature scaler is of incorrect value. |
|
""" |
|
scaler = calibration_config.temperature_scaling_calibration.scaler |
|
if scaler <= 0: |
|
raise ValueError('The scaler in temperature scaling must be positive.') |
|
calibrated_class_predictions_with_background = tf.math.divide( |
|
class_predictions_with_background, |
|
scaler, |
|
name='calibrate_score') |
|
return calibrated_class_predictions_with_background |
|
|
|
|
|
else: |
|
raise ValueError('No calibration builder defined for "Oneof" in ' |
|
'calibration_config.') |
|
|
|
return calibration_fn |
|
|