def testReturnsCorrectAnchorWiseLossWithHighLogitScaleSetting(self): """At very high logit_scale, all predictions will be ~0.33.""" # TODO(yonib): Also test logit_scale with anchorwise=False. logit_scale = 10e16 prediction_tensor = tf.constant([[[-100, 100, -100], [100, -100, -100], [0, 0, -100], [-100, -100, 100]], [[-100, 0, 0], [-100, 100, -100], [-100, 100, -100], [100, -100, -100]]], tf.float32) target_tensor = tf.constant([[[0, 1, 0], [1, 0, 0], [1, 0, 0], [0, 0, 1]], [[0, 0, 1], [0, 1, 0], [0, 1, 0], [1, 0, 0]]], tf.float32) weights = tf.constant([[1, 1, 1, 1], [1, 1, 1, 1]], tf.float32) loss_op = losses.WeightedSoftmaxClassificationLoss(logit_scale=logit_scale) loss = loss_op(prediction_tensor, target_tensor, weights=weights) uniform_distribution_loss = - math.log(.33333333333) exp_loss = np.matrix([[uniform_distribution_loss] * 4, [uniform_distribution_loss] * 4]) with self.test_session() as sess: loss_output = sess.run(loss) self.assertAllClose(loss_output, exp_loss)
def testReturnsCorrectLoss(self): prediction_tensor = tf.constant([[[-100, 100, -100], [100, -100, -100], [0, 0, -100], [-100, -100, 100]], [[-100, 0, 0], [-100, 100, -100], [-100, 100, -100], [100, -100, -100]]], tf.float32) target_tensor = tf.constant([[[0, 1, 0], [1, 0, 0], [1, 0, 0], [0, 0, 1]], [[0, 0, 1], [0, 1, 0], [0, 1, 0], [1, 0, 0]]], tf.float32) weights = tf.constant([[1, 1, .5, 1], [1, 1, 1, 0]], tf.float32) loss_op = losses.WeightedSoftmaxClassificationLoss() loss = loss_op(prediction_tensor, target_tensor, weights=weights) exp_loss = - 1.5 * math.log(.5) with self.test_session() as sess: loss_output = sess.run(loss) self.assertAllClose(loss_output, exp_loss)
def _build_classification_loss(loss_config): """Builds a classification loss based on the loss config. Args: loss_config: A losses_pb2.ClassificationLoss object. Returns: Loss based on the config. Raises: ValueError: On invalid loss_config. """ if not isinstance(loss_config, losses_pb2.ClassificationLoss): raise ValueError( 'loss_config not of type losses_pb2.ClassificationLoss.') loss_type = loss_config.WhichOneof('classification_loss') print('hello', loss_type) if loss_type == 'weighted_sigmoid': return losses.WeightedSigmoidClassificationLoss() if loss_type == 'weighted_sigmoid_focal': config = loss_config.weighted_sigmoid_focal alpha = None if config.HasField('alpha'): alpha = config.alpha return losses.SigmoidFocalClassificationLoss(gamma=config.gamma, alpha=alpha) if loss_type == 'weighted_softmax': config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( logit_scale=config.logit_scale) if loss_type == 'weighted_logits_softmax': config = loss_config.weighted_logits_softmax return losses.WeightedSoftmaxClassificationAgainstLogitsLoss( logit_scale=config.logit_scale) if loss_type == 'bootstrapped_sigmoid': config = loss_config.bootstrapped_sigmoid return losses.BootstrappedSigmoidClassificationLoss( alpha=config.alpha, bootstrap_type=('hard' if config.hard_bootstrap else 'soft')) if loss_type == 'bounded_sigmoid': config = loss_config.bounded_sigmoid return losses.BoundedSigmoidCrossEntropyLoss() if loss_type == 'quadratic_bounded_sigmoid': config = loss_config.quadratic_bounded_sigmoid return losses.QuadraticBoundedSigmoidCrossEntropyLoss() if loss_type == 'soft_target_sigmoid': config = loss_config.soft_target_sigmoid return losses.SoftTargetSigmoidCrossEntropyLoss() raise ValueError('Empty loss config.')
def _build_classification_loss(loss_config): """Builds a classification loss based on the loss config. Args: loss_config: A losses_pb2.ClassificationLoss object. Returns: Loss based on the config. Raises: ValueError: On invalid loss_config. """ if not isinstance(loss_config, losses_pb2.ClassificationLoss): raise ValueError( 'loss_config not of type losses_pb2.ClassificationLoss.') loss_type = loss_config.WhichOneof('classification_loss') if loss_type == 'weighted_sigmoid': return losses.WeightedSigmoidClassificationLoss() elif loss_type == 'weighted_sigmoid_focal': config = loss_config.weighted_sigmoid_focal alpha = None if config.HasField('alpha'): alpha = config.alpha return losses.SigmoidFocalClassificationLoss(gamma=config.gamma, alpha=alpha) elif loss_type == 'weighted_softmax': config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( logit_scale=config.logit_scale) elif loss_type == 'weighted_logits_softmax': config = loss_config.weighted_logits_softmax return losses.WeightedSoftmaxClassificationAgainstLogitsLoss( logit_scale=config.logit_scale) elif loss_type == 'bootstrapped_sigmoid': config = loss_config.bootstrapped_sigmoid return losses.BootstrappedSigmoidClassificationLoss( alpha=config.alpha, bootstrap_type=('hard' if config.hard_bootstrap else 'soft')) elif loss_type == 'penalty_reduced_logistic_focal_loss': config = loss_config.penalty_reduced_logistic_focal_loss return losses.PenaltyReducedLogisticFocalLoss(alpha=config.alpha, beta=config.beta) elif loss_type == 'weighted_dice_classification_loss': config = loss_config.weighted_dice_classification_loss return losses.WeightedDiceClassificationLoss( squared_normalization=config.squared_normalization, is_prediction_probability=config.is_prediction_probability) else: raise ValueError('Empty loss config.')
def build(self): super(FasterRCNNFirstStageLoss, self).build() self._proposal_target_assigner = ( target_assigner.create_target_assigner('FasterRCNN', 'proposal')) self._sampler = ( balanced_positive_negative_sampler.BalancedPositiveNegativeSampler( positive_fraction=self.positive_balance_fraction, is_static=False)) self._localization_loss = od_losses.WeightedSmoothL1LocalizationLoss() self._objectness_loss = od_losses.WeightedSoftmaxClassificationLoss() return self
def build_faster_rcnn_classification_loss(loss_config): """Builds a classification loss for Faster RCNN based on the loss config. Args: loss_config: A losses_pb2.ClassificationLoss object. Returns: Loss based on the config. Raises: ValueError: On invalid loss_config. """ if not isinstance(loss_config, losses_pb2.ClassificationLoss): raise ValueError( 'loss_config not of type losses_pb2.ClassificationLoss.') loss_type = loss_config.WhichOneof('classification_loss') if loss_type == 'weighted_sigmoid': return losses.WeightedSigmoidClassificationLoss() if loss_type == 'weighted_softmax': config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( logit_scale=config.logit_scale) if loss_type == 'weighted_logits_softmax': config = loss_config.weighted_logits_softmax return losses.WeightedSoftmaxClassificationAgainstLogitsLoss( logit_scale=config.logit_scale) if loss_type == 'weighted_sigmoid_focal': config = loss_config.weighted_sigmoid_focal alpha = None if config.HasField('alpha'): alpha = config.alpha return losses.SigmoidFocalClassificationLoss(gamma=config.gamma, alpha=alpha) # By default, Faster RCNN second stage classifier uses Softmax loss # with anchor-wise outputs. config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( logit_scale=config.logit_scale)
def _build_classification_loss(loss_config): """Builds a classification loss based on the loss config. Args: loss_config: A losses_pb2.ClassificationLoss object. Returns: Loss based on the config. Raises: ValueError: On invalid loss_config. """ if not isinstance(loss_config, losses_pb2.ClassificationLoss): raise ValueError( 'loss_config not of type losses_pb2.ClassificationLoss.') loss_type = loss_config.WhichOneof('classification_loss') if loss_type == 'weighted_sigmoid': config = loss_config.weighted_sigmoid return losses.WeightedSigmoidClassificationLoss( anchorwise_output=config.anchorwise_output) if loss_type == 'confidence_weighted_sigmoid': config = loss_config.confidence_weighted_sigmoid return losses.ConfidenceWeightedSigmoidClassificationLoss( anchorwise_output=config.anchorwise_output) if loss_type == 'weighted_sigmoid_focal': config = loss_config.weighted_sigmoid_focal alpha = None if config.HasField('alpha'): alpha = config.alpha return losses.SigmoidFocalClassificationLoss( anchorwise_output=config.anchorwise_output, gamma=config.gamma, alpha=alpha) if loss_type == 'weighted_softmax': config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( anchorwise_output=config.anchorwise_output, logit_scale=config.logit_scale) if loss_type == 'bootstrapped_sigmoid': config = loss_config.bootstrapped_sigmoid return losses.BootstrappedSigmoidClassificationLoss( alpha=config.alpha, bootstrap_type=('hard' if config.hard_bootstrap else 'soft'), anchorwise_output=config.anchorwise_output) raise ValueError('Empty loss config.')
def build_loss(loss_type): """Builds the desired type of loss Args: loss_type: loss type (e.g. 'berHu', 'smooth_l1') Returns: Class of the specified loss_type """ if loss_type == 'berHu': return losses_custom.WeightedBerHu() elif loss_type == 'chamfer_dist': return losses_custom.ChamferDistance() elif loss_type == 'emd': return losses_custom.EarthMoversDistance() elif loss_type == 'smooth_l1': return losses.WeightedSmoothL1LocalizationLoss() elif loss_type == 'smooth_l1_nonzero': return losses_custom.WeightedNonZeroSmoothL1LocalizationLoss() elif loss_type == 'softmax': return losses.WeightedSoftmaxClassificationLoss() elif loss_type == 'focal': return losses.SigmoidFocalClassificationLoss() elif loss_type == 'softmax_temp': return losses.WeightedSoftmaxClassificationLoss(0.5) elif loss_type == 'sigmoid_ce': return losses_custom.SigmoidClassificationLoss() else: raise ValueError('Invalid loss type', loss_type)
def _build_classification_loss(loss_config): """Builds a classification loss based on the loss config. Args: loss_config: A losses_pb2.ClassificationLoss object. Returns: Loss based on the config. Raises: ValueError: On invalid loss_config. """ if not isinstance(loss_config, losses_pb2.ClassificationLoss): raise ValueError( 'loss_config not of type losses_pb2.ClassificationLoss.') loss_type = loss_config.WhichOneof('classification_loss') if loss_type == 'weighted_sigmoid': config = loss_config.weighted_sigmoid return losses.WeightedSigmoidClassificationLoss( anchorwise_output=config.anchorwise_output) if loss_type == 'weighted_softmax': config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( anchorwise_output=config.anchorwise_output) if loss_type == 'bootstrapped_sigmoid': config = loss_config.bootstrapped_sigmoid return losses.BootstrappedSigmoidClassificationLoss( alpha=config.alpha, bootstrap_type=('hard' if config.hard_bootstrap else 'soft'), anchorwise_output=config.anchorwise_output) if loss_type == 'weighted_l2': config = loss_config.weighted_l2 return losses.WeightedL2LocalizationLoss( anchorwise_output=config.anchorwise_output) if loss_type == 'weighted_smooth_l1': config = loss_config.weighted_smooth_l1 return losses.WeightedSmoothL1LocalizationLoss( anchorwise_output=config.anchorwise_output) raise ValueError('Empty loss config.')
def _build_classification_loss(loss_config): if not isinstance(loss_config, losses_pb2.ClassificationLoss): raise ValueError('loss_config not of type losses_pb2.ClassificationLoss.') loss_type = loss_config.WhichOneof('classification_loss') if loss_type == 'weighted_sigmoid': return losses.WeightedSigmoidClassificationLoss() if loss_type == 'weighted_sigmoid_focal': config = loss_config.weighted_sigmoid_focal alpha = None if config.HasField('alpha'): alpha = config.alpha return losses.SigmoidFocalClassificationLoss( gamma=config.gamma, alpha=alpha) if loss_type == 'weighted_softmax': config = loss_config.weighted_softmax return losses.WeightedSoftmaxClassificationLoss( logit_scale=config.logit_scale) if loss_type == 'weighted_logits_softmax': config = loss_config.weighted_logits_softmax return losses.WeightedSoftmaxClassificationAgainstLogitsLoss( logit_scale=config.logit_scale) if loss_type == 'bootstrapped_sigmoid': config = loss_config.bootstrapped_sigmoid return losses.BootstrappedSigmoidClassificationLoss( alpha=config.alpha, bootstrap_type=('hard' if config.hard_bootstrap else 'soft')) raise ValueError('Empty loss config.')
def _build_model(self, is_training, number_of_stages, second_stage_batch_size, first_stage_max_proposals=8, num_classes=2, hard_mining=False, softmax_second_stage_classification_loss=True, predict_masks=False, pad_to_max_dimension=None, masks_are_class_agnostic=False, use_matmul_crop_and_resize=False, clip_anchors_to_image=False, use_matmul_gather_in_matcher=False, use_static_shapes=False, calibration_mapping_value=None, share_box_across_classes=False, return_raw_detections_during_predict=False): use_keras = tf_version.is_tf2() def image_resizer_fn(image, masks=None): """Fake image resizer function.""" resized_inputs = [] resized_image = tf.identity(image) if pad_to_max_dimension is not None: resized_image = tf.image.pad_to_bounding_box( image, 0, 0, pad_to_max_dimension, pad_to_max_dimension) resized_inputs.append(resized_image) if masks is not None: resized_masks = tf.identity(masks) if pad_to_max_dimension is not None: resized_masks = tf.image.pad_to_bounding_box( tf.transpose(masks, [1, 2, 0]), 0, 0, pad_to_max_dimension, pad_to_max_dimension) resized_masks = tf.transpose(resized_masks, [2, 0, 1]) resized_inputs.append(resized_masks) resized_inputs.append(tf.shape(image)) return resized_inputs # anchors in this test are designed so that a subset of anchors are inside # the image and a subset of anchors are outside. first_stage_anchor_scales = (0.001, 0.005, 0.1) first_stage_anchor_aspect_ratios = (0.5, 1.0, 2.0) first_stage_anchor_strides = (1, 1) first_stage_anchor_generator = grid_anchor_generator.GridAnchorGenerator( first_stage_anchor_scales, first_stage_anchor_aspect_ratios, anchor_stride=first_stage_anchor_strides) first_stage_target_assigner = target_assigner.create_target_assigner( 'FasterRCNN', 'proposal', use_matmul_gather=use_matmul_gather_in_matcher) if use_keras: fake_feature_extractor = FakeFasterRCNNKerasFeatureExtractor() else: fake_feature_extractor = FakeFasterRCNNFeatureExtractor() first_stage_box_predictor_hyperparams_text_proto = """ op: CONV activation: RELU regularizer { l2_regularizer { weight: 0.00004 } } initializer { truncated_normal_initializer { stddev: 0.03 } } """ if use_keras: first_stage_box_predictor_arg_scope_fn = ( self._build_keras_layer_hyperparams( first_stage_box_predictor_hyperparams_text_proto)) else: first_stage_box_predictor_arg_scope_fn = ( self._build_arg_scope_with_hyperparams( first_stage_box_predictor_hyperparams_text_proto, is_training)) first_stage_box_predictor_kernel_size = 3 first_stage_atrous_rate = 1 first_stage_box_predictor_depth = 512 first_stage_minibatch_size = 3 first_stage_sampler = sampler.BalancedPositiveNegativeSampler( positive_fraction=0.5, is_static=use_static_shapes) first_stage_nms_score_threshold = -1.0 first_stage_nms_iou_threshold = 1.0 first_stage_max_proposals = first_stage_max_proposals first_stage_non_max_suppression_fn = functools.partial( post_processing.batch_multiclass_non_max_suppression, score_thresh=first_stage_nms_score_threshold, iou_thresh=first_stage_nms_iou_threshold, max_size_per_class=first_stage_max_proposals, max_total_size=first_stage_max_proposals, use_static_shapes=use_static_shapes) first_stage_localization_loss_weight = 1.0 first_stage_objectness_loss_weight = 1.0 post_processing_config = post_processing_pb2.PostProcessing() post_processing_text_proto = """ score_converter: IDENTITY batch_non_max_suppression { score_threshold: -20.0 iou_threshold: 1.0 max_detections_per_class: 5 max_total_detections: 5 use_static_shapes: """ + '{}'.format(use_static_shapes) + """ } """ if calibration_mapping_value: calibration_text_proto = """ calibration_config { function_approximation { x_y_pairs { x_y_pair { x: 0.0 y: %f } x_y_pair { x: 1.0 y: %f }}}}""" % (calibration_mapping_value, calibration_mapping_value) post_processing_text_proto = (post_processing_text_proto + ' ' + calibration_text_proto) text_format.Merge(post_processing_text_proto, post_processing_config) second_stage_non_max_suppression_fn, second_stage_score_conversion_fn = ( post_processing_builder.build(post_processing_config)) second_stage_target_assigner = target_assigner.create_target_assigner( 'FasterRCNN', 'detection', use_matmul_gather=use_matmul_gather_in_matcher) second_stage_sampler = sampler.BalancedPositiveNegativeSampler( positive_fraction=1.0, is_static=use_static_shapes) second_stage_localization_loss_weight = 1.0 second_stage_classification_loss_weight = 1.0 if softmax_second_stage_classification_loss: second_stage_classification_loss = ( losses.WeightedSoftmaxClassificationLoss()) else: second_stage_classification_loss = ( losses.WeightedSigmoidClassificationLoss()) hard_example_miner = None if hard_mining: hard_example_miner = losses.HardExampleMiner( num_hard_examples=1, iou_threshold=0.99, loss_type='both', cls_loss_weight=second_stage_classification_loss_weight, loc_loss_weight=second_stage_localization_loss_weight, max_negatives_per_positive=None) crop_and_resize_fn = (ops.matmul_crop_and_resize if use_matmul_crop_and_resize else ops.native_crop_and_resize) common_kwargs = { 'is_training': is_training, 'num_classes': num_classes, 'image_resizer_fn': image_resizer_fn, 'feature_extractor': fake_feature_extractor, 'number_of_stages': number_of_stages, 'first_stage_anchor_generator': first_stage_anchor_generator, 'first_stage_target_assigner': first_stage_target_assigner, 'first_stage_atrous_rate': first_stage_atrous_rate, 'first_stage_box_predictor_arg_scope_fn': first_stage_box_predictor_arg_scope_fn, 'first_stage_box_predictor_kernel_size': first_stage_box_predictor_kernel_size, 'first_stage_box_predictor_depth': first_stage_box_predictor_depth, 'first_stage_minibatch_size': first_stage_minibatch_size, 'first_stage_sampler': first_stage_sampler, 'first_stage_non_max_suppression_fn': first_stage_non_max_suppression_fn, 'first_stage_max_proposals': first_stage_max_proposals, 'first_stage_localization_loss_weight': first_stage_localization_loss_weight, 'first_stage_objectness_loss_weight': first_stage_objectness_loss_weight, 'second_stage_target_assigner': second_stage_target_assigner, 'second_stage_batch_size': second_stage_batch_size, 'second_stage_sampler': second_stage_sampler, 'second_stage_non_max_suppression_fn': second_stage_non_max_suppression_fn, 'second_stage_score_conversion_fn': second_stage_score_conversion_fn, 'second_stage_localization_loss_weight': second_stage_localization_loss_weight, 'second_stage_classification_loss_weight': second_stage_classification_loss_weight, 'second_stage_classification_loss': second_stage_classification_loss, 'hard_example_miner': hard_example_miner, 'crop_and_resize_fn': crop_and_resize_fn, 'clip_anchors_to_image': clip_anchors_to_image, 'use_static_shapes': use_static_shapes, 'resize_masks': True, 'return_raw_detections_during_predict': return_raw_detections_during_predict } return self._get_model( self._get_second_stage_box_predictor( num_classes=num_classes, is_training=is_training, use_keras=use_keras, predict_masks=predict_masks, masks_are_class_agnostic=masks_are_class_agnostic, share_box_across_classes=share_box_across_classes), **common_kwargs)
def __init__(self, desc): """Init faster rcnn. :param desc: config dict """ super(FasterRCNN, self).__init__() self.num_classes = int(desc.num_classes) self.number_of_stages = int(desc.number_of_stages) # Backbone for feature extractor self.feature_extractor = NetworkDesc(desc.backbone).to_model() # First stage anchor generator self.first_stage_anchor_generator = NetworkDesc( desc["first_stage_anchor_generator"]).to_model() # First stage target assigner self.use_matmul_gather_in_matcher = False # Default self.first_stage_target_assigner = target_assigner.create_target_assigner( 'FasterRCNN', 'proposal', use_matmul_gather=self.use_matmul_gather_in_matcher) # First stage box predictor self.first_stage_box_predictor_arg_scope_fn = scope_generator.get_hyper_params_scope( desc.first_stage_box_predictor_conv_hyperparams) self.first_stage_atrous_rate = 1 # Default: 1 self.first_stage_box_predictor_kernel_size = 3 # Default self.first_stage_box_predictor_depth = 512 # Default self.first_stage_minibatch_size = 256 # Default # First stage sampler self.first_stage_positive_balance_fraction = 0.5 # Default self.use_static_balanced_label_sampler = False # Default self.use_static_shapes = False # Default self.first_stage_sampler = sampler.BalancedPositiveNegativeSampler( positive_fraction=self.first_stage_positive_balance_fraction, is_static=(self.use_static_balanced_label_sampler and self.use_static_shapes)) # First stage NMS self.first_stage_nms_score_threshold = 0.0 self.first_stage_nms_iou_threshold = 0.7 self.first_stage_max_proposals = 300 self.use_partitioned_nms_in_first_stage = True # Default self.use_combined_nms_in_first_stage = False # Default self.first_stage_non_max_suppression_fn = functools.partial( post_processing.batch_multiclass_non_max_suppression, score_thresh=self.first_stage_nms_score_threshold, iou_thresh=self.first_stage_nms_iou_threshold, max_size_per_class=self.first_stage_max_proposals, max_total_size=self.first_stage_max_proposals, use_static_shapes=self.use_static_shapes, use_partitioned_nms=self.use_partitioned_nms_in_first_stage, use_combined_nms=self.use_combined_nms_in_first_stage) # First stage localization loss weight self.first_stage_localization_loss_weight = 2.0 # First stage objectness loss weight self.first_stage_objectness_loss_weight = 1.0 # Second stage target assigner self.second_stage_target_assigner = target_assigner.create_target_assigner( 'FasterRCNN', 'detection', use_matmul_gather=self.use_matmul_gather_in_matcher) # Second stage sampler self.second_stage_batch_size = 64 # Default self.second_stage_balance_fraction = 0.25 # Default self.second_stage_sampler = sampler.BalancedPositiveNegativeSampler( positive_fraction=self.second_stage_balance_fraction, is_static=(self.use_static_balanced_label_sampler and self.use_static_shapes)) # Second stage box predictor self.second_stage_box_predictor = NetworkDesc( desc.mask_rcnn_box).to_model() # Second stage NMS function self.second_stage_non_max_suppression_fn, self.second_stage_score_conversion_fn = \ post_processing_util.get_post_processing_fn(desc.second_stage_post_processing) # Second stage mask prediction loss weight self.second_stage_mask_prediction_loss_weight = 1.0 # default # Second stage localization loss weight self.second_stage_localization_loss_weight = 2.0 # Second stage classification loss weight self.second_stage_classification_loss_weight = 1.0 # Second stage classification loss self.logit_scale = 1.0 # Default self.second_stage_classification_loss = losses.WeightedSoftmaxClassificationLoss( logit_scale=self.logit_scale) self.hard_example_miner = None self.add_summaries = True # Crop and resize function self.use_matmul_crop_and_resize = False # Default self.crop_and_resize_fn = ( spatial_ops.multilevel_matmul_crop_and_resize if self.use_matmul_crop_and_resize else spatial_ops.native_crop_and_resize) self.clip_anchors_to_image = False # Default self.resize_masks = True # Default self.return_raw_detections_during_predict = False # Default self.output_final_box_features = False # Default # Image resizer function self.image_resizer_fn = image_resizer_util.get_image_resizer( desc.image_resizer) self.initial_crop_size = 14 self.maxpool_kernel_size = 2 self.maxpool_stride = 2 # Real model to be called self.model = None