Ejemplo n.º 1
0
    def __init__(self,
                 neighborhood_constant=NEIGHBORHOOD_CONST, n_neighbors=None,
                 metric='euclidean', metric_kwargs=None,
                 n_cv_folds=CROSS_VAL_SIZE,
                 c_search_values=None,
                 approx_nearest_neighbors=True,
                 skip_dim_reduction=True,
                 model_dim_reduction=None,
                 n_jobs=1,
                 max_iter=200,
                 balanced_classification=True,
                 low_memory=False,
                 save_knn_indices_to_file=True,
                 seed_rng=SEED_DEFAULT):
        """

        :param neighborhood_constant: float value in (0, 1), that specifies the number of nearest neighbors as a
                                      function of the number of samples (data size). If `N` is the number of samples,
                                      then the number of neighbors is set to `N^neighborhood_constant`. It is
                                      recommended to set this value in the range 0.4 to 0.5.
        :param n_neighbors: None or int value specifying the number of nearest neighbors. If this value is specified,
                            the `neighborhood_constant` is ignored. It is sufficient to specify either
                            `neighborhood_constant` or `n_neighbors`.
        :param metric: string or a callable that specifies the distance metric to use.
        :param metric_kwargs: optional keyword arguments required by the distance metric specified in the form of a
                              dictionary.
        :param n_cv_folds: number of cross-validation folds.
        :param c_search_values: list or array of search values for the logistic regression hyper-parameter `C`. The
                                default value is `None`.
        :param approx_nearest_neighbors: Set to True in order to use an approximate nearest neighbor algorithm to
                                         find the nearest neighbors. The NN-descent method is used for approximate
                                         nearest neighbor searches.
        :param skip_dim_reduction: Set to True in order to skip dimension reduction of the layer embeddings.
        :param model_dim_reduction: 1. None if dimension reduction is not required; (OR)
                                    2. Path to a file containing the saved dimension reduction model. This will be
                                       a pickle file that loads into a list of model dictionaries; (OR)
                                    3. The dimension reduction model loaded into memory from the pickle file.
        :param n_jobs: Number of parallel jobs or processes. Set to -1 to use all the available cpu cores.
        :param max_iter: Maximum number of iterations for the optimization of the logistic classifier. The default
                         value set by the scikit-learn library is 100, but sometimes this does not allow for
                         convergence. Hence, increasing it to 200 here.
        :param balanced_classification: Set to True to assign sample weights to balance the binary classification
                                        problem separating adversarial from non-adversarial samples.
        :param low_memory: Set to True to enable the low memory option of the `NN-descent` method. Note that this
                           is likely to increase the running time.
        :param save_knn_indices_to_file: Set to True in order to save the KNN indices from each layer to a pickle
                                         file to reduce memory usage. This may not be needed when the data size
                                         and/or the number of layers is small. It avoids potential out-of-memory
                                         errors at the expense of time taken to write and read the files.
        :param seed_rng: int value specifying the seed for the random number generator. This is passed around to
                         all the classes/functions that require random number generation. Set this to a fixed value
                         for reproducible results.
        """
        self.neighborhood_constant = neighborhood_constant
        self.n_neighbors = n_neighbors
        self.metric = metric
        self.metric_kwargs = metric_kwargs
        self.n_cv_folds = n_cv_folds
        self.c_search_values = c_search_values
        self.approx_nearest_neighbors = approx_nearest_neighbors
        self.skip_dim_reduction = skip_dim_reduction
        self.n_jobs = get_num_jobs(n_jobs)
        self.max_iter = max_iter
        self.balanced_classification = balanced_classification
        self.low_memory = low_memory
        self.save_knn_indices_to_file = save_knn_indices_to_file
        self.seed_rng = seed_rng

        np.random.seed(self.seed_rng)
        # Load the dimension reduction models per-layer if required
        self.transform_models = None
        if not self.skip_dim_reduction:
            if model_dim_reduction is None:
                raise ValueError("Model file for dimension reduction is required but not specified as input.")
            elif isinstance(model_dim_reduction, str):
                # Pickle file is specified
                self.transform_models = load_dimension_reduction_models(model_dim_reduction)
            elif isinstance(model_dim_reduction, list):
                # Model already loaded from pickle file
                self.transform_models = model_dim_reduction
            else:
                raise ValueError("Invalid format for the dimension reduction model input.")

        if self.c_search_values is None:
            # Default search values for the `C` parameter of logistic regression
            self.c_search_values = np.logspace(-4, 4, num=10)

        self.n_layers = None
        self.n_samples = []
        self.index_knn = None
        self.model_logistic = None
        self.scaler = None
        # Temporary directory to save the KNN index files
        self.temp_direc = None
        self.temp_knn_files = None
Ejemplo n.º 2
0
    def __init__(self,
                 layer_statistic='multinomial',
                 score_type='pvalue',
                 ood_detection=False,
                 pvalue_fusion='fisher',
                 use_top_ranked=False,
                 num_top_ranked=NUM_TOP_RANKED,
                 skip_dim_reduction=False,
                 model_dim_reduction=None,
                 neighborhood_constant=NEIGHBORHOOD_CONST, n_neighbors=None,
                 metric=METRIC_DEF, metric_kwargs=None,
                 approx_nearest_neighbors=True,
                 n_jobs=1,
                 low_memory=False,
                 seed_rng=SEED_DEFAULT):
        """

        :param layer_statistic: Type of test statistic to calculate at the layers. Valid values are 'multinomial',
                                'binomial', 'lid', and 'lle'.
        :param score_type: Name of the scoring method to use. Valid options are: 'density' and 'pvalue'.
        :param ood_detection: Set to True to perform out-of-distribution detection instead of adversarial detection.
        :param pvalue_fusion: Method for combining the p-values across the layers. Options are 'harmonic_mean'
                              and 'fisher'. The former corresponds to the weighted harmonic mean of the p-values
                              and the latter corresponds to Fisher's method of combining p-values. This input is
                              used only when `score_type = 'pvalue'`.
        :param use_top_ranked: Set to True in order to use only a few top ranked test statistics for detection.
        :param num_top_ranked: If `use_top_ranked` is set to True, this specifies the number of top-ranked test
                               statistics to use for detection. This number should be smaller than the number of
                               layers considered for detection.
        :param skip_dim_reduction: Set to True in order to skip dimension reduction of the layer embeddings.
        :param model_dim_reduction: 1. None if dimension reduction is not required; (OR)
                                    2. Path to a file containing the saved dimension reduction model. This will be
                                       a pickle file that loads into a list of model dictionaries; (OR)
                                    3. The dimension reduction model loaded into memory from the pickle file.
        :param neighborhood_constant: float value in (0, 1), that specifies the number of nearest neighbors as a
                                      function of the number of samples (data size). If `N` is the number of samples,
                                      then the number of neighbors is set to `N^neighborhood_constant`. It is
                                      recommended to set this value in the range 0.4 to 0.5.
        :param n_neighbors: None or int value specifying the number of nearest neighbors. If this value is specified,
                            the `neighborhood_constant` is ignored. It is sufficient to specify either
                            `neighborhood_constant` or `n_neighbors`.
        :param metric: string or a callable that specifies the distance metric to use.
        :param metric_kwargs: optional keyword arguments required by the distance metric specified in the form of a
                              dictionary.
        :param approx_nearest_neighbors: Set to True in order to use an approximate nearest neighbor algorithm to
                                         find the nearest neighbors. The NN-descent method is used for approximate
                                         nearest neighbor searches.
        :param n_jobs: Number of parallel jobs or processes. Set to -1 to use all the available cpu cores.
        :param low_memory: Set to True to enable the low memory option of the `NN-descent` method. Note that this
                           is likely to increase the running time.
        :param seed_rng: int value specifying the seed for the random number generator. This is passed around to
                         all the classes/functions that require random number generation. Set this to a fixed value
                         for reproducible results.
        """
        self.layer_statistic = layer_statistic.lower()
        self.score_type = score_type.lower()
        self.ood_detection = ood_detection
        self.pvalue_fusion = pvalue_fusion
        self.use_top_ranked = use_top_ranked
        self.num_top_ranked = num_top_ranked
        self.skip_dim_reduction = skip_dim_reduction
        self.neighborhood_constant = neighborhood_constant
        self.n_neighbors = n_neighbors
        self.metric = metric
        self.metric_kwargs = metric_kwargs
        self.approx_nearest_neighbors = approx_nearest_neighbors
        self.n_jobs = n_jobs
        self.low_memory = low_memory
        self.seed_rng = seed_rng

        np.random.seed(self.seed_rng)
        if self.layer_statistic not in TEST_STATS_SUPPORTED:
            raise ValueError("Invalid value '{}' for the input argument 'layer_statistic'.".
                             format(self.layer_statistic))

        if self.score_type not in SCORE_TYPES:
            raise ValueError("Invalid value '{}' for the input argument 'score_type'.".format(self.score_type))

        if self.pvalue_fusion not in ['harmonic_mean', 'fisher']:
            raise ValueError("Invalid value '{}' for the input argument 'pvalue_fusion'.".format(self.pvalue_fusion))

        if self.layer_statistic in {'lid', 'lle'}:
            if not self.skip_dim_reduction:
                logger.warning("Option 'skip_dim_reduction' is set to False for the test statistic '{}'. Making sure "
                               "that this is the intended setting.".format(self.layer_statistic))

        # Load the dimension reduction models per-layer if required
        self.transform_models = None
        if not self.skip_dim_reduction:
            if model_dim_reduction is None:
                raise ValueError("Model file for dimension reduction is required but not specified as input.")
            elif isinstance(model_dim_reduction, str):
                # Pickle file is specified
                self.transform_models = load_dimension_reduction_models(model_dim_reduction)
            elif isinstance(model_dim_reduction, list):
                # Model already loaded from pickle file
                self.transform_models = model_dim_reduction
            else:
                raise ValueError("Invalid format for the dimension reduction model input.")

        self.n_layers = None
        self.labels_unique = None
        self.n_classes = None
        self.n_samples = None
        # List of test statistic model instances for each layer
        self.test_stats_models = []
        # dict mapping each class `c` to the joint density model of the test statistics conditioned on the predicted
        # or true class being `c`
        self.density_models_pred = dict()
        self.density_models_true = dict()
        # Negative log density values of data randomly sampled from the joint density models of the test statistics
        self.samples_neg_log_dens_pred = dict()
        self.samples_neg_log_dens_true = dict()
        # Log of the class prior probabilities estimated from the training data labels
        self.log_class_priors = None
        # Localized p-value estimation models for the data conditioned on each predicted and true class
        self.klpe_models_pred = dict()
        self.klpe_models_true = dict()
        # Test statistics calculated on the training data passed to the `fit` method. These test statistics follow
        # the distribution under the null hypothesis of no adversarial or OOD data
        self.test_stats_pred_null = None
        self.test_stats_true_null = None
Ejemplo n.º 3
0
def main():
    # Training settings
    parser = argparse.ArgumentParser()
    parser.add_argument('--model-type',
                        '-m',
                        choices=['mnist', 'cifar10', 'svhn'],
                        default='mnist',
                        help='model type or name of the dataset')
    parser.add_argument('--detection-method',
                        '--dm',
                        choices=DETECTION_METHODS,
                        default='proposed',
                        help="Detection method to run. Choices are: {}".format(
                            ', '.join(DETECTION_METHODS)))
    parser.add_argument(
        '--index-adv',
        type=int,
        default=0,
        help=
        'Index of the adversarial attack parameter to use. This indexes the sorted directories '
        'containing the adversarial data files from different attack parameters.'
    )
    parser.add_argument('--batch-size',
                        type=int,
                        default=256,
                        help='batch size of evaluation')
    ################ Optional arguments for the proposed method
    parser.add_argument(
        '--test-statistic',
        '--ts',
        choices=TEST_STATS_SUPPORTED,
        default='multinomial',
        help=
        "Test statistic to calculate at the layers for the proposed method. Choices are: {}"
        .format(', '.join(TEST_STATS_SUPPORTED)))
    parser.add_argument(
        '--score-type',
        '--st',
        choices=SCORE_TYPES,
        default='pvalue',
        help="Score type to use for the proposed method. Choices are: {}".
        format(', '.join(SCORE_TYPES)))
    parser.add_argument(
        '--pvalue-fusion',
        '--pf',
        choices=['harmonic_mean', 'fisher'],
        default='harmonic_mean',
        help=
        "Name of the method to use for combining p-values from multiple layers for the "
        "proposed method. Choices are: 'harmonic_mean' and 'fisher'")
    parser.add_argument(
        '--ood-detection',
        '--ood',
        action='store_true',
        default=False,
        help=
        "Option that enables out-of-distribution detection instead of adversarial detection "
        "for the proposed method")
    parser.add_argument(
        '--use-top-ranked',
        '--utr',
        action='store_true',
        default=False,
        help=
        "Option that enables the proposed method to use only the top-ranked (by p-values) test statistics for "
        "detection. The number of test statistics is specified through the option '--num-layers'"
    )
    parser.add_argument(
        '--use-deep-layers',
        '--udl',
        action='store_true',
        default=False,
        help=
        "Option that enables the proposed method to use only a given number of last few layers of the DNN. "
        "The number of layers is specified through the option '--num-layers'")
    parser.add_argument(
        '--num-layers',
        '--nl',
        type=int,
        default=NUM_TOP_RANKED,
        help=
        "If the option '--use-top-ranked' or '--use-deep-layers' is provided, this option specifies the number "
        "of layers or test statistics to be used by the proposed method")
    parser.add_argument(
        '--combine-classes',
        '--cc',
        action='store_true',
        default=False,
        help=
        "Option that allows low probability classes to be automatically combined into one group for the "
        "multinomial test statistic used with the proposed method")
    ################ Optional arguments for the proposed method
    parser.add_argument(
        '--num-neighbors',
        '--nn',
        type=int,
        default=-1,
        help=
        'Number of nearest neighbors (if applicable to the method). By default, this is set '
        'to be a power of the number of samples (n): n^{:.1f}'.format(
            NEIGHBORHOOD_CONST))
    parser.add_argument(
        '--modelfile-dim-reduc',
        '--mdr',
        default='',
        help=
        'Path to the saved dimension reduction model file. Specify only if the default path '
        'needs to be changed.')
    parser.add_argument(
        '--output-dir',
        '-o',
        default='',
        help='directory path for saving the results of detection')
    parser.add_argument(
        '--adv-attack',
        '--aa',
        choices=['FGSM', 'PGD', 'CW', CUSTOM_ATTACK, 'none'],
        default='PGD',
        help=
        "Type of adversarial attack. Use 'none' to evaluate on clean samples.")
    parser.add_argument(
        '--max-attack-prop',
        '--map',
        type=float,
        default=0.5,
        help=
        "Maximum proportion of attack samples in the test fold. Should be a value in (0, 1]"
    )
    parser.add_argument('--num-folds',
                        '--nf',
                        type=int,
                        default=CROSS_VAL_SIZE,
                        help='number of cross-validation folds')
    parser.add_argument('--no-cuda',
                        action='store_true',
                        default=False,
                        help='disables CUDA training')
    parser.add_argument('--gpu',
                        type=str,
                        default='2',
                        help='which gpus to execute code on')
    parser.add_argument(
        '--n-jobs',
        type=int,
        default=8,
        help='number of parallel jobs to use for multiprocessing')
    parser.add_argument('--seed',
                        '-s',
                        type=int,
                        default=SEED_DEFAULT,
                        help='seed for random number generation')
    args = parser.parse_args()

    if args.use_top_ranked and args.use_deep_layers:
        raise ValueError(
            "Cannot provide both command line options '--use-top-ranked' and '--use-deep-layers'. "
            "Specify only one of them.")

    os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
    use_cuda = not args.no_cuda and torch.cuda.is_available()
    device = torch.device("cuda" if use_cuda else "cpu")
    kwargs_loader = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}

    # Number of neighbors
    n_neighbors = args.num_neighbors
    if n_neighbors <= 0:
        n_neighbors = None

    # Output directory
    if not args.output_dir:
        base_dir = get_output_path(args.model_type)
        output_dir = os.path.join(base_dir, 'prediction')
    else:
        output_dir = args.output_dir

    if not os.path.isdir(output_dir):
        os.makedirs(output_dir)

    # Method name for results and plots
    method_name = METHOD_NAME_MAP[args.detection_method]

    # Dimensionality reduction to the layer embeddings is applied only for methods in certain configurations
    apply_dim_reduc = False
    if args.detection_method == 'proposed':
        # Name string for the proposed method based on the input configuration
        # Score type suffix in the method name
        st = '{:.4s}'.format(args.score_type)
        if args.score_type == 'pvalue':
            if args.pvalue_fusion == 'harmonic_mean':
                st += '_hmp'
            if args.pvalue_fusion == 'fisher':
                st += '_fis'

        if not args.ood_detection:
            method_name = '{:.5s}_{:.5s}_{}_adv'.format(
                method_name, args.test_statistic, st)
        else:
            method_name = '{:.5s}_{:.5s}_{}_ood'.format(
                method_name, args.test_statistic, st)

        if args.use_top_ranked:
            method_name = '{}_top{:d}'.format(method_name, args.num_layers)
        elif args.use_deep_layers:
            method_name = '{}_last{:d}'.format(method_name, args.num_layers)

        # If `n_neighbors` is specified, append that value to the name string
        if n_neighbors is not None:
            method_name = '{}_k{:d}'.format(method_name, n_neighbors)

        apply_dim_reduc = True

    elif args.detection_method == 'dknn':
        apply_dim_reduc = False
        # If `n_neighbors` is specified, append that value to the name string
        if n_neighbors is not None:
            method_name = '{}_k{:d}'.format(method_name, n_neighbors)

    # Model file for dimension reduction, if required
    model_dim_reduc = None
    if apply_dim_reduc:
        if args.modelfile_dim_reduc:
            fname = args.modelfile_dim_reduc
        else:
            # Path to the dimension reduction model file
            fname = get_path_dr_models(args.model_type,
                                       args.detection_method,
                                       test_statistic=args.test_statistic)

        if not os.path.isfile(fname):
            raise ValueError(
                "Model file for dimension reduction is required, but does not exist: {}"
                .format(fname))
        else:
            # Load the dimension reduction models for each layer from the pickle file
            model_dim_reduc = load_dimension_reduction_models(fname)

    # Data loader and pre-trained DNN model corresponding to the dataset
    if args.model_type == 'mnist':
        num_classes = 10
        model = MNIST().to(device)
        model = load_model_checkpoint(model, args.model_type)

    elif args.model_type == 'cifar10':
        num_classes = 10
        model = ResNet34().to(device)
        model = load_model_checkpoint(model, args.model_type)

    elif args.model_type == 'svhn':
        num_classes = 10
        model = SVHN().to(device)
        model = load_model_checkpoint(model, args.model_type)

    else:
        raise ValueError("'{}' is not a valid model type".format(
            args.model_type))

    # Set model in evaluation mode
    model.eval()

    # Check if the numpy data directory exists
    d = os.path.join(NUMPY_DATA_PATH, args.model_type)
    if not os.path.isdir(d):
        raise ValueError(
            "Directory for the numpy data files not found: {}".format(d))

    if args.adv_attack.lower() == 'none':
        evaluate_on_clean = True
    else:
        evaluate_on_clean = False

    # Initialization
    labels_true_folds = []
    labels_pred_dnn_folds = []
    scores_detec_folds = []
    labels_pred_detec_folds = []
    thresholds_folds = []
    ti = time.time()
    # Cross-validation
    for i in range(args.num_folds):
        print("\nProcessing cross-validation fold {:d}:".format(i + 1))
        # Load the saved clean numpy data from this fold
        numpy_save_path = get_clean_data_path(args.model_type, i + 1)
        # Temporary hack to use backup data directory
        # numpy_save_path = numpy_save_path.replace('varun', 'jayaram', 1)

        data_tr, labels_tr, data_te, labels_te = load_numpy_data(
            numpy_save_path)
        num_clean_tr = labels_tr.shape[0]
        num_clean_te = labels_te.shape[0]
        # Data loader for the train and test fold
        train_fold_loader = convert_to_loader(data_tr,
                                              labels_tr,
                                              dtype_x=torch.float,
                                              batch_size=args.batch_size,
                                              device=device)
        test_fold_loader = convert_to_loader(data_te,
                                             labels_te,
                                             dtype_x=torch.float,
                                             batch_size=args.batch_size,
                                             device=device)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the clean train data split:"
        )
        layer_embeddings_tr, labels_pred_tr = helper_layer_embeddings(
            model, device, train_fold_loader, args.detection_method, labels_tr)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the clean test data split:"
        )
        layer_embeddings_te, labels_pred_te = helper_layer_embeddings(
            model, device, test_fold_loader, args.detection_method, labels_te)
        del train_fold_loader
        del test_fold_loader

        if not evaluate_on_clean:
            # Load the saved adversarial numpy data generated from this training and test fold
            _, _, data_tr_adv, labels_tr_adv, data_te_adv, labels_te_adv = load_adversarial_wrapper(
                i,
                args.model_type,
                args.adv_attack,
                args.max_attack_prop,
                num_clean_te,
                index_adv=args.index_adv)
            num_adv_tr = labels_tr_adv.shape[0]
            num_adv_te = labels_te_adv.shape[0]
            print(
                "\nTrain fold: number of clean samples = {:d}, number of adversarial samples = {:d}, % of "
                "adversarial samples = {:.4f}".format(
                    num_clean_tr, num_adv_tr,
                    (100. * num_adv_tr) / (num_clean_tr + num_adv_tr)))
            print(
                "Test fold: number of clean samples = {:d}, number of adversarial samples = {:d}, % of adversarial "
                "samples = {:.4f}".format(num_clean_te, num_adv_te,
                                          (100. * num_adv_te) /
                                          (num_clean_te + num_adv_te)))
            # Adversarial data loader for the test fold
            adv_test_fold_loader = convert_to_loader(
                data_te_adv,
                labels_te_adv,
                dtype_x=torch.float,
                batch_size=args.batch_size,
                device=device)
            print(
                "\nCalculating the layer embeddings and DNN predictions for the adversarial test data split:"
            )
            layer_embeddings_te_adv, labels_pred_te_adv = helper_layer_embeddings(
                model, device, adv_test_fold_loader, args.detection_method,
                labels_te_adv)
            check_label_mismatch(labels_te_adv, labels_pred_te_adv)
            del adv_test_fold_loader

            # True class labels of adversarial samples from this test fold
            labels_true_folds.append(labels_te_adv)
            # Class predictions of the DNN on adversarial samples from this test fold
            labels_pred_dnn_folds.append(labels_pred_te_adv)
            num_expec = num_adv_te
        else:
            print("\nTrain fold: number of clean samples = {:d}".format(
                num_clean_tr))
            print("Test fold: number of clean samples = {:d}".format(
                num_clean_te))
            # True class labels of clean samples from this test fold
            labels_true_folds.append(labels_te)
            # Class predictions of the DNN on clean samples from this test fold
            labels_pred_dnn_folds.append(labels_pred_te)
            num_expec = num_clean_te

        # Detection methods
        if args.detection_method == 'proposed':
            nl = len(layer_embeddings_tr)
            st_ind = 0
            if args.use_deep_layers:
                if args.num_layers > nl:
                    print(
                        "WARNING: number of layers specified using the option '--num-layers' exceeds the number "
                        "of layers in the model. Using all the layers.")
                    st_ind = 0
                else:
                    st_ind = nl - args.num_layers
                    print(
                        "Using only the last {:d} layer embeddings from the {:d} layers for the proposed method."
                        .format(args.num_layers, nl))

            mod_dr = None if (
                model_dim_reduc is None) else model_dim_reduc[st_ind:]
            det_model = DetectorLayerStatistics(
                layer_statistic=args.test_statistic,
                score_type=args.score_type,
                ood_detection=args.ood_detection,
                pvalue_fusion=args.pvalue_fusion,
                use_top_ranked=args.use_top_ranked,
                num_top_ranked=args.num_layers,
                skip_dim_reduction=(not apply_dim_reduc),
                model_dim_reduction=mod_dr,
                n_neighbors=n_neighbors,
                n_jobs=args.n_jobs,
                seed_rng=args.seed)
            # Fit the detector on clean data from the training fold
            if args.combine_classes and (args.test_statistic == 'multinomial'):
                _ = det_model.fit(layer_embeddings_tr[st_ind:],
                                  labels_tr,
                                  labels_pred_tr,
                                  combine_low_proba_classes=True)
            else:
                _ = det_model.fit(layer_embeddings_tr[st_ind:], labels_tr,
                                  labels_pred_tr)

            # Find the score thresholds corresponding to the target FPRs using the scores from the clean train
            # fold data
            scores_detec_train = det_model.score(layer_embeddings_tr[st_ind:],
                                                 labels_pred_tr,
                                                 test_layer_pairs=True,
                                                 is_train=True)
            thresholds = find_score_thresholds(scores_detec_train, FPRS_TARGET)
            if evaluate_on_clean:
                # Scores and class predictions on clean data from the test fold
                scores_detec, labels_pred_detec = det_model.score(
                    layer_embeddings_te[st_ind:],
                    labels_pred_te,
                    return_corrected_predictions=True,
                    test_layer_pairs=True)
            else:
                # Scores and class predictions on adversarial data from the test fold
                scores_detec, labels_pred_detec = det_model.score(
                    layer_embeddings_te_adv[st_ind:],
                    labels_pred_te_adv,
                    return_corrected_predictions=True,
                    test_layer_pairs=True)

        elif args.detection_method == 'dknn':
            det_model = DeepKNN(n_neighbors=n_neighbors,
                                skip_dim_reduction=(not apply_dim_reduc),
                                model_dim_reduction=model_dim_reduc,
                                n_jobs=args.n_jobs,
                                seed_rng=args.seed)
            # Fit the detector on clean data from the training fold
            _ = det_model.fit(layer_embeddings_tr, labels_tr)
            # Find the score thresholds corresponding to the target FPRs using the scores from the clean train
            # fold data
            scores_detec_train, _ = det_model.score(layer_embeddings_tr,
                                                    is_train=True)
            thresholds = find_score_thresholds(scores_detec_train, FPRS_TARGET)
            if evaluate_on_clean:
                # Scores and class predictions on clean data from the test fold
                scores_detec, labels_pred_detec = det_model.score(
                    layer_embeddings_te)
            else:
                # Scores and class predictions on adversarial data from the test fold
                scores_detec, labels_pred_detec = det_model.score(
                    layer_embeddings_te_adv)

        else:
            raise ValueError("Unknown detection method name '{}'".format(
                args.detection_method))

        # Sanity check
        if (scores_detec.shape[0] != num_expec) or (labels_pred_detec.shape[0]
                                                    != num_expec):
            raise ValueError(
                "Detection scores and/or predicted labels do not have the expected length of {:d}; method = {}, "
                "fold = {:d}".format(num_expec, args.detection_method, i + 1))

        scores_detec_folds.append(scores_detec)
        labels_pred_detec_folds.append(labels_pred_detec)
        thresholds_folds.append(thresholds)

    print(
        "\nCalculating the combined classification accuracy of the DNN and detector system:"
    )
    fname = os.path.join(output_dir,
                         'corrected_accuracies_{}.pkl'.format(method_name))
    results = combined_classification_performance(scores_detec_folds,
                                                  thresholds_folds,
                                                  labels_pred_detec_folds,
                                                  labels_pred_dnn_folds,
                                                  labels_true_folds,
                                                  FPRS_TARGET,
                                                  output_file=fname)
    print("Performance metrics saved to the file: {}".format(fname))
    tf = time.time()
    print("Total time taken: {:.4f} minutes".format((tf - ti) / 60.))
def main():
    # Training settings
    parser = argparse.ArgumentParser()
    parser.add_argument('--batch-size',
                        type=int,
                        default=256,
                        help='batch size of evaluation')
    parser.add_argument('--model-type',
                        '-m',
                        choices=['mnist', 'cifar10', 'cifar10aug', 'svhn'],
                        default='mnist',
                        help='model type or name of the dataset')
    parser.add_argument('--detection-method',
                        '--dm',
                        choices=DETECTION_METHODS,
                        default='proposed',
                        help="Detection method to run. Choices are: {}".format(
                            ', '.join(DETECTION_METHODS)))
    parser.add_argument(
        '--resume-from-ckpt',
        action='store_true',
        default=False,
        help=
        'Use this option to load results and resume from a previous partially completed run. '
        'Cross-validation folds that were completed earlier will be skipped in the current run.'
    )
    parser.add_argument(
        '--save-detec-model',
        action='store_true',
        default=False,
        help=
        'Use this option to save the list of detection models from the CV folds to a pickle '
        'file. Note that the files tend to large in size.')
    parser.add_argument(
        '--censor-classes',
        action='store_true',
        default=False,
        help=
        'Use this option to censor data from a random subset of classes in the training fold.'
    )
    ################ Optional arguments for the proposed method
    parser.add_argument(
        '--test-statistic',
        '--ts',
        choices=TEST_STATS_SUPPORTED,
        default='multinomial',
        help=
        "Test statistic to calculate at the layers for the proposed method. Choices are: {}"
        .format(', '.join(TEST_STATS_SUPPORTED)))
    parser.add_argument(
        '--score-type',
        '--st',
        choices=SCORE_TYPES,
        default='pvalue',
        help="Score type to use for the proposed method. Choices are: {}".
        format(', '.join(SCORE_TYPES)))
    parser.add_argument(
        '--pvalue-fusion',
        '--pf',
        choices=['harmonic_mean', 'fisher'],
        default='harmonic_mean',
        help=
        "Name of the method to use for combining p-values from multiple layers for the "
        "proposed method. Choices are: 'harmonic_mean' and 'fisher'")
    parser.add_argument(
        '--use-top-ranked',
        '--utr',
        action='store_true',
        default=False,
        help=
        "Option that enables the proposed method to use only the top-ranked (by p-values) test statistics for "
        "detection. The number of test statistics is specified through the option '--num-layers'"
    )
    parser.add_argument(
        '--use-deep-layers',
        '--udl',
        action='store_true',
        default=False,
        help=
        "Option that enables the proposed method to use only a given number of last few layers of the DNN. "
        "The number of layers is specified through the option '--num-layers'")
    parser.add_argument(
        '--num-layers',
        '--nl',
        type=int,
        default=NUM_TOP_RANKED,
        help=
        "If the option '--use-top-ranked' or '--use-deep-layers' is provided, this option specifies the number "
        "of layers or test statistics to be used by the proposed method")
    parser.add_argument(
        '--combine-classes',
        '--cc',
        action='store_true',
        default=False,
        help=
        "Option that allows low probability classes to be automatically combined into one group for the "
        "multinomial test statistic used with the proposed method")
    ################ Optional arguments for the proposed method
    parser.add_argument(
        '--layer-trust-score',
        '--lts',
        choices=LAYERS_TRUST_SCORE,
        default='input',
        help=
        "Which layer to use for the trust score calculation. Choices are: {}".
        format(', '.join(LAYERS_TRUST_SCORE)))
    parser.add_argument(
        '--batch-lid',
        action='store_true',
        default=False,
        help=
        'Use this option to enable batched, faster version of the LID detector'
    )
    parser.add_argument(
        '--num-neighbors',
        '--nn',
        type=int,
        default=-1,
        help=
        'Number of nearest neighbors (if applicable to the method). By default, this is set '
        'to be a power of the number of samples (n): n^{:.1f}'.format(
            NEIGHBORHOOD_CONST))
    parser.add_argument(
        '--modelfile-dim-reduc',
        '--mdr',
        default='',
        help=
        'Path to the saved dimension reduction model file. Specify only if the default path '
        'needs to be changed.')
    parser.add_argument(
        '--output-dir',
        '-o',
        default='',
        help='directory path for saving the results of detection')
    parser.add_argument(
        '--max-outlier-prop',
        '--mop',
        type=float,
        default=0.25,
        help=
        "Maximum proportion of outlier samples in the test fold. Should be a value in (0, 1]"
    )
    parser.add_argument('--num-folds',
                        '--nf',
                        type=int,
                        default=CROSS_VAL_SIZE,
                        help='number of cross-validation folds')
    parser.add_argument('--no-cuda',
                        action='store_true',
                        default=False,
                        help='disables CUDA training')
    parser.add_argument('--gpu',
                        type=str,
                        default='2',
                        help='which gpus to execute code on')
    parser.add_argument(
        '--n-jobs',
        type=int,
        default=8,
        help='number of parallel jobs to use for multiprocessing')
    parser.add_argument('--seed',
                        '-s',
                        type=int,
                        default=SEED_DEFAULT,
                        help='seed for random number generation')
    args = parser.parse_args()

    if args.use_top_ranked and args.use_deep_layers:
        raise ValueError(
            "Cannot provide both command line options '--use-top-ranked' and '--use-deep-layers'. "
            "Specify only one of them.")

    os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
    use_cuda = not args.no_cuda and torch.cuda.is_available()
    device = torch.device("cuda" if use_cuda else "cpu")
    kwargs_loader = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
    random.seed(args.seed)

    # Number of neighbors
    n_neighbors = args.num_neighbors
    if n_neighbors <= 0:
        n_neighbors = None

    # Output directory
    if not args.output_dir:
        base_dir = get_output_path(args.model_type)
        output_dir = os.path.join(base_dir, 'detection_ood')
    else:
        output_dir = args.output_dir

    if not os.path.isdir(output_dir):
        os.makedirs(output_dir)

    # Method name for results and plots
    method_name = METHOD_NAME_MAP[args.detection_method]

    # Dimensionality reduction to the layer embeddings is applied only for methods in certain configurations
    apply_dim_reduc = False
    if args.detection_method == 'proposed':
        # Name string for the proposed method based on the input configuration
        # Score type suffix in the method name
        st = '{:.4s}'.format(args.score_type)
        if args.score_type == 'pvalue':
            if args.pvalue_fusion == 'harmonic_mean':
                st += '_hmp'
            if args.pvalue_fusion == 'fisher':
                st += '_fis'

        method_name = '{:.5s}_{:.5s}_{}_ood'.format(method_name,
                                                    args.test_statistic, st)
        if args.use_top_ranked:
            method_name = '{}_top{:d}'.format(method_name, args.num_layers)
        elif args.use_deep_layers:
            method_name = '{}_last{:d}'.format(method_name, args.num_layers)

        # If `n_neighbors` is specified, append that value to the name string
        if n_neighbors is not None:
            method_name = '{}_k{:d}'.format(method_name, n_neighbors)

        apply_dim_reduc = True

    elif args.detection_method == 'trust':
        # Append the layer name to the method name
        method_name = '{:.5s}_{}'.format(method_name, args.layer_trust_score)
        # If `n_neighbors` is specified, append that value to the name string
        if n_neighbors is not None:
            method_name = '{}_k{:d}'.format(method_name, n_neighbors)

        # Dimension reduction is not applied to the logit layer
        if args.layer_trust_score != 'logit':
            apply_dim_reduc = True

    elif args.detection_method == 'dknn':
        apply_dim_reduc = False
        # If `n_neighbors` is specified, append that value to the name string
        if n_neighbors is not None:
            method_name = '{}_k{:d}'.format(method_name, n_neighbors)

    elif args.detection_method == 'mahalanobis':
        # No dimensionality reduction needed here
        # According to the paper, they internally transform a `C x H x W` layer embedding to a `C x 1` vector
        # through global average pooling
        apply_dim_reduc = False

    # Model file for dimension reduction, if required
    model_dim_reduc = None
    if apply_dim_reduc:
        if args.modelfile_dim_reduc:
            fname = args.modelfile_dim_reduc
        else:
            # Path to the dimension reduction model file
            fname = get_path_dr_models(args.model_type,
                                       args.detection_method,
                                       test_statistic=args.test_statistic)

        if not os.path.isfile(fname):
            raise ValueError(
                "Model file for dimension reduction is required, but does not exist: {}"
                .format(fname))
        else:
            # Load the dimension reduction models for each layer from the pickle file
            model_dim_reduc = load_dimension_reduction_models(fname)

    config_trust_score = dict()
    if args.detection_method == 'trust':
        # Get the layer index and the layer-specific dimensionality reduction model for the trust score
        config_trust_score = get_config_trust_score(model_dim_reduc,
                                                    args.layer_trust_score,
                                                    n_neighbors)

    # Data loader and pre-trained DNN model corresponding to the dataset
    data_path = DATA_PATH
    if args.model_type == 'mnist':
        '''
        transform = transforms.Compose(
            [transforms.ToTensor(),
             transforms.Normalize(*NORMALIZE_IMAGES['mnist'])]
        )
        test_loader = torch.utils.data.DataLoader(
            datasets.MNIST(data_path, train=False, download=True, transform=transform),
            batch_size=args.batch_size, shuffle=True, **kwargs_loader
        )
        '''
        num_classes = 10
        model = MNIST().to(device)
        model = load_model_checkpoint(model, args.model_type)

    elif args.model_type in ('cifar10', 'cifar10aug'):
        '''
        transform_test = transforms.Compose(
            [transforms.ToTensor(),
             transforms.Normalize(*NORMALIZE_IMAGES['cifar10'])]
        )
        testset = datasets.CIFAR10(root=data_path, train=False, download=True, transform=transform_test)
        test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=True, **kwargs_loader)
        '''
        num_classes = 10
        model = ResNet34().to(device)
        model = load_model_checkpoint(model, args.model_type)

    elif args.model_type == 'svhn':
        '''
        transform = transforms.Compose(
            [transforms.ToTensor(),
             transforms.Normalize(*NORMALIZE_IMAGES['svhn'])]
        )
        testset = datasets.SVHN(root=data_path, split='test', download=True, transform=transform)
        test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=True, **kwargs_loader)
        '''
        num_classes = 10
        model = SVHN().to(device)
        model = load_model_checkpoint(model, args.model_type)

    else:
        raise ValueError("'{}' is not a valid model type".format(
            args.model_type))

    # Set model in evaluation mode
    model.eval()

    # Check if the numpy data directory exists
    d = os.path.join(NUMPY_DATA_PATH, args.model_type)
    if not os.path.isdir(d):
        raise ValueError(
            "Directory for the numpy data files not found: {}".format(d))

    # Initialization
    if args.resume_from_ckpt:
        scores_folds, labels_folds, models_folds, init_fold = load_detector_checkpoint(
            output_dir, method_name, args.save_detec_model)
        print(
            "Loading saved results from a previous run. Completed {:d} fold(s). Resuming from fold {:d}."
            .format(init_fold, init_fold + 1))
    else:
        scores_folds = []
        labels_folds = []
        models_folds = []
        init_fold = 0

    ti = time.time()
    # Cross-validation
    for i in range(init_fold, args.num_folds):
        print("\nProcessing cross-validation fold {:d}:".format(i + 1))
        # Load the saved clean numpy data from this fold
        numpy_save_path = get_clean_data_path(args.model_type, i + 1)
        # Temporary hack to use backup data directory
        # numpy_save_path = numpy_save_path.replace('varun', 'jayaram', 1)

        data_tr, labels_tr, data_te, labels_te = load_numpy_data(
            numpy_save_path)
        # Data loader for the train fold
        train_fold_loader = convert_to_loader(data_tr,
                                              labels_tr,
                                              batch_size=args.batch_size,
                                              device=device,
                                              dtype_x=torch.float)
        # Data loader for the test fold
        test_fold_loader = convert_to_loader(data_te,
                                             labels_te,
                                             batch_size=args.batch_size,
                                             device=device,
                                             dtype_x=torch.float)

        # Get the range of values in the data array
        # bounds = get_data_bounds(np.concatenate([data_tr, data_te], axis=0))
        print(
            "\nCalculating the layer embeddings and DNN predictions for the clean train data split:"
        )
        layer_embeddings_tr, labels_pred_tr = helper_layer_embeddings(
            model, device, train_fold_loader, args.detection_method, labels_tr)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the clean test data split:"
        )
        layer_embeddings_te, labels_pred_te = helper_layer_embeddings(
            model, device, test_fold_loader, args.detection_method, labels_te)
        # Delete the data loaders in case they are not used further
        del test_fold_loader
        if args.detection_method != 'mahalanobis':
            del train_fold_loader

        ############################ OUTLIERS ########################################################
        # path to the OOD dataset
        numpy_save_path_ood = get_clean_data_path(
            inlier_outlier_map[args.model_type], i + 1)
        # Temporary hack to use backup data directory
        # numpy_save_path_ood = numpy_save_path_ood.replace('varun', 'jayaram', 1)

        data_tr_ood, labels_tr_ood, data_te_ood, labels_te_ood = load_numpy_data(
            numpy_save_path_ood)
        if args.censor_classes:
            # Exclude data from a random subset of classes for the training fold
            data_tr_ood, labels_tr_ood, data_te_ood, labels_te_ood = filter_data_classes(
                data_tr_ood,
                labels_tr_ood,
                data_te_ood,
                labels_te_ood,
                i,
                include_noise_samples=True)
        '''
        # Data loader for the outlier data from the train fold
        train_fold_loader_ood = convert_to_loader(data_tr_ood, labels_tr_ood, batch_size=args.batch_size, 
                                                  device=device, dtype_x=torch.float)
        print("\nCalculating the layer embeddings and DNN predictions for the ood train data split:")
        layer_embeddings_tr_ood, labels_pred_tr_ood = helper_layer_embeddings(
            model, device, train_fold_loader_ood, args.detection_method, labels_tr_ood
        )
        '''
        # Data loader for the outlier data from the test fold
        test_fold_loader_ood = convert_to_loader(data_te_ood,
                                                 labels_te_ood,
                                                 batch_size=args.batch_size,
                                                 device=device,
                                                 dtype_x=torch.float)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the ood test data split:"
        )
        layer_embeddings_te_ood, labels_pred_te_ood = helper_layer_embeddings(
            model, device, test_fold_loader_ood, args.detection_method,
            labels_te_ood)
        # Delete the data loaders in case they are not used further
        del test_fold_loader_ood

        ############################# NOISY #########################################################
        # Load the saved noisy (Gaussian noise) numpy data generated from this training and test fold
        numpy_save_path = get_noisy_data_path(args.model_type, i + 1)
        # Temporary hack to use backup data directory
        # numpy_save_path = numpy_save_path.replace('varun', 'jayaram', 1)

        data_tr_noisy, data_te_noisy = load_noisy_data(numpy_save_path)
        # Noisy data have the same labels as the clean data
        # labels_tr_noisy = labels_tr
        # labels_te_noisy = labels_te

        # Run the detection method
        # Detection labels (0 denoting clean and 1 outlier)
        labels_detec = np.concatenate([
            np.zeros(labels_pred_te.shape[0], dtype=np.int),
            np.ones(labels_pred_te_ood.shape[0], dtype=np.int)
        ])
        if args.detection_method == 'proposed':
            nl = len(layer_embeddings_tr)
            st_ind = 0
            if args.use_deep_layers:
                if args.num_layers > nl:
                    print(
                        "WARNING: number of layers specified using the option '--num-layers' exceeds the number "
                        "of layers in the model. Using all the layers.")
                    st_ind = 0
                else:
                    st_ind = nl - args.num_layers
                    print(
                        "Using only the last {:d} layer embeddings from the {:d} layers for the proposed method."
                        .format(args.num_layers, nl))

            mod_dr = None if (
                model_dim_reduc is None) else model_dim_reduc[st_ind:]
            det_model = DetectorLayerStatistics(
                layer_statistic=args.test_statistic,
                score_type=args.score_type,
                ood_detection=True,
                pvalue_fusion=args.pvalue_fusion,
                use_top_ranked=args.use_top_ranked,
                num_top_ranked=args.num_layers,
                skip_dim_reduction=(not apply_dim_reduc),
                model_dim_reduction=mod_dr,
                n_neighbors=n_neighbors,
                n_jobs=args.n_jobs,
                seed_rng=args.seed)
            # Fit the detector on clean data from the training fold
            if args.combine_classes and (args.test_statistic == 'multinomial'):
                _ = det_model.fit(layer_embeddings_tr[st_ind:],
                                  labels_tr,
                                  labels_pred_tr,
                                  combine_low_proba_classes=True)
            else:
                _ = det_model.fit(layer_embeddings_tr[st_ind:], labels_tr,
                                  labels_pred_tr)

            # Scores on clean data from the test fold
            scores_adv1 = det_model.score(layer_embeddings_te[st_ind:],
                                          labels_pred_te,
                                          test_layer_pairs=True)

            # Scores on ood data from the test fold
            scores_adv2 = det_model.score(layer_embeddings_te_ood[st_ind:],
                                          labels_pred_te_ood,
                                          test_layer_pairs=True)

            scores_adv = np.concatenate([scores_adv1, scores_adv2])
            if args.save_detec_model:
                models_folds.append(det_model)

        elif args.detection_method == 'dknn':
            det_model = DeepKNN(n_neighbors=n_neighbors,
                                skip_dim_reduction=(not apply_dim_reduc),
                                model_dim_reduction=model_dim_reduc,
                                n_jobs=args.n_jobs,
                                seed_rng=args.seed)
            # Fit the detector on clean data from the training fold
            _ = det_model.fit(layer_embeddings_tr, labels_tr)

            # Scores on clean data from the test fold
            scores_adv1, labels_pred_dknn1 = det_model.score(
                layer_embeddings_te)

            # Scores on ood data from the test fold
            scores_adv2, labels_pred_dknn2 = det_model.score(
                layer_embeddings_te_ood)

            scores_adv = np.concatenate([scores_adv1, scores_adv2])
            # labels_pred_dknn = np.concatenate([labels_pred_dknn1, labels_pred_dknn2])
            if args.save_detec_model:
                models_folds.append(det_model)

        elif args.detection_method == 'trust':
            ind_layer = config_trust_score['layer']
            det_model = TrustScore(
                alpha=config_trust_score['alpha'],
                n_neighbors=config_trust_score['n_neighbors'],
                skip_dim_reduction=(not apply_dim_reduc),
                model_dim_reduction=config_trust_score['model_dr'],
                n_jobs=args.n_jobs,
                seed_rng=args.seed)
            # Fit the detector on clean data from the training fold
            _ = det_model.fit(layer_embeddings_tr[ind_layer], labels_tr,
                              labels_pred_tr)

            # Scores on clean data from the test fold
            scores_adv1 = det_model.score(layer_embeddings_te[ind_layer],
                                          labels_pred_te)

            # Scores on adversarial data from the test fold
            #line below needs to be changed
            scores_adv2 = det_model.score(layer_embeddings_te_ood[ind_layer],
                                          labels_pred_te_ood)

            scores_adv = np.concatenate([scores_adv1, scores_adv2])
            if args.save_detec_model:
                models_folds.append(det_model)

        elif args.detection_method == 'mahalanobis':
            # Sub-directory for this fold so that the output files are not overwritten
            temp_direc = os.path.join(output_dir, 'fold_{}'.format(i + 1))
            if not os.path.isdir(temp_direc):
                os.makedirs(temp_direc)

            # Calculate the mahalanobis distance features per layer and fit a logistic classifier on the extracted
            # features using data from the training fold
            model_detector = fit_mahalanobis_scores(model,
                                                    device,
                                                    'ood',
                                                    args.model_type,
                                                    num_classes,
                                                    temp_direc,
                                                    train_fold_loader,
                                                    data_tr,
                                                    data_tr_ood,
                                                    data_tr_noisy,
                                                    n_jobs=args.n_jobs)
            # Calculate the mahalanobis distance features per layer for the best noise magnitude and predict the
            # logistic classifer to score the samples.
            # Scores on clean data from the test fold
            scores_adv1 = get_mahalanobis_scores(model_detector, data_te,
                                                 model, device,
                                                 args.model_type)

            # Scores on adversarial data from the test fold
            scores_adv2 = get_mahalanobis_scores(model_detector, data_te_ood,
                                                 model, device,
                                                 args.model_type)

            scores_adv = np.concatenate([scores_adv1, scores_adv2])
        else:
            raise ValueError("Unknown detection method name '{}'".format(
                args.detection_method))

        # Sanity check
        if scores_adv.shape[0] != labels_detec.shape[0]:
            raise ValueError(
                "Detection scores and labels do not have the same length ({:d} != {:d}); method = {}, fold = {:d}"
                .format(scores_adv.shape[0], labels_detec.shape[0],
                        args.detection_method, i + 1))

        scores_folds.append(scores_adv)
        labels_folds.append(labels_detec)
        save_detector_checkpoint(scores_folds, labels_folds, models_folds,
                                 output_dir, method_name,
                                 args.save_detec_model)

    print(
        "\nCalculating performance metrics for different proportion of outlier samples:"
    )
    fname = os.path.join(output_dir,
                         'detection_metrics_{}.pkl'.format(method_name))
    results_dict = metrics_varying_positive_class_proportion(
        scores_folds,
        labels_folds,
        output_file=fname,
        max_pos_proportion=args.max_outlier_prop,
        log_scale=False)
    print("Performance metrics saved to the file: {}".format(fname))
    tf = time.time()
    print("Total time taken: {:.4f} minutes".format((tf - ti) / 60.))
    def __init__(self,
                 neighborhood_constant=NEIGHBORHOOD_CONST,
                 n_neighbors=None,
                 metric=METRIC_DEF,
                 metric_kwargs=None,
                 approx_nearest_neighbors=True,
                 skip_dim_reduction=True,
                 model_dim_reduction=None,
                 n_jobs=1,
                 low_memory=False,
                 seed_rng=SEED_DEFAULT):
        """
        :param neighborhood_constant: float value in (0, 1), that specifies the number of nearest neighbors as a
                                      function of the number of samples (data size). If `N` is the number of samples,
                                      then the number of neighbors is set to `N^neighborhood_constant`. It is
                                      recommended to set this value in the range 0.4 to 0.5.
        :param n_neighbors: None or int value specifying the number of nearest neighbors. If this value is specified,
                            the `neighborhood_constant` is ignored. It is sufficient to specify either
                            `neighborhood_constant` or `n_neighbors`.
        :param metric: string or a callable that specifies the distance metric to use.
        :param metric_kwargs: optional keyword arguments required by the distance metric specified in the form of a
                              dictionary.
        :param approx_nearest_neighbors: Set to True in order to use an approximate nearest neighbor algorithm to
                                         find the nearest neighbors. The NN-descent method is used for approximate
                                         nearest neighbor searches.
        :param skip_dim_reduction: Set to True in order to skip dimension reduction of the layer embeddings.
        :param model_dim_reduction: 1. None if dimension reduction is not required; (OR)
                                    2. Path to a file containing the saved dimension reduction model. This will be
                                       a pickle file that loads into a list of model dictionaries; (OR)
                                    3. The dimension reduction model loaded into memory from the pickle file.
        :param n_jobs: Number of parallel jobs or processes. Set to -1 to use all the available cpu cores.
        :param low_memory: Set to True to enable the low memory option of the `NN-descent` method. Note that this
                           is likely to increase the running time.
        :param seed_rng: int value specifying the seed for the random number generator. This is passed around to
                         all the classes/functions that require random number generation. Set this to a fixed value
                         for reproducible results.
        """
        self.neighborhood_constant = neighborhood_constant
        self.n_neighbors = n_neighbors
        self.metric = metric
        self.metric_kwargs = metric_kwargs
        self.approx_nearest_neighbors = approx_nearest_neighbors
        self.skip_dim_reduction = skip_dim_reduction
        self.n_jobs = get_num_jobs(n_jobs)
        self.low_memory = low_memory
        self.seed_rng = seed_rng

        np.random.seed(self.seed_rng)
        # Load the dimension reduction models per-layer if required
        self.transform_models = None
        if not self.skip_dim_reduction:
            if model_dim_reduction is None:
                raise ValueError(
                    "Model file for dimension reduction is required but not specified as input."
                )
            elif isinstance(model_dim_reduction, str):
                # Pickle file is specified
                self.transform_models = load_dimension_reduction_models(
                    model_dim_reduction)
            elif isinstance(model_dim_reduction, list):
                # Model already loaded from pickle file
                self.transform_models = model_dim_reduction
            else:
                raise ValueError(
                    "Invalid format for the dimension reduction model input.")

        self.n_layers = None
        self.labels_unique = None
        self.n_classes = None
        self.n_samples = None
        self.label_encoder = None
        # Encoded labels of train data
        self.labels_train_enc = None
        # KNN index for data from each layer
        self.index_knn = None
        self.mask_exclude = None
        # Non-conformity values on the calibration data
        self.nonconformity_calib = None
def gather_test_stats(args):
    detection_method = 'proposed'
    os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
    use_cuda = not args.no_cuda and torch.cuda.is_available()
    device = torch.device("cuda" if use_cuda else "cpu")
    kwargs_loader = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}

    # Number of neighbors
    n_neighbors = args.num_neighbors
    if n_neighbors <= 0:
        n_neighbors = None

    # Model file for dimension reduction
    apply_dim_reduc = True
    model_dim_reduc = None
    if apply_dim_reduc:
        if args.modelfile_dim_reduc:
            fname = args.modelfile_dim_reduc
        else:
            # Path to the dimension reduction model file
            fname = get_path_dr_models(args.model_type,
                                       detection_method,
                                       test_statistic=args.test_statistic)

        if not os.path.isfile(fname):
            raise ValueError(
                "Model file for dimension reduction is required, but does not exist: {}"
                .format(fname))
        else:
            # Load the dimension reduction models for each layer from the pickle file
            model_dim_reduc = load_dimension_reduction_models(fname)

    # Pre-trained DNN model corresponding to the dataset
    if args.model_type == 'mnist':
        num_classes = 10
        model = MNIST().to(device)
        model = load_model_checkpoint(model, args.model_type)
    elif args.model_type == 'cifar10':
        num_classes = 10
        model = ResNet34().to(device)
        model = load_model_checkpoint(model, args.model_type)
    elif args.model_type == 'svhn':
        num_classes = 10
        model = SVHN().to(device)
        model = load_model_checkpoint(model, args.model_type)
    else:
        raise ValueError("'{}' is not a valid model type".format(
            args.model_type))

    # Set model in evaluation mode
    model.eval()
    # Check if the numpy data directory exists
    d = os.path.join(NUMPY_DATA_PATH, args.model_type)
    if not os.path.isdir(d):
        raise ValueError(
            "Directory for the numpy data files not found: {}".format(d))

    n_samples_per_class = 5000
    test_stats_pred = {'clean': [], 'adversarial': []}
    test_stats_true = {'clean': [], 'adversarial': []}
    # Select a particular data fold
    ind_fold = 0
    for i in range(ind_fold, ind_fold + 1):
        print("\nProcessing cross-validation fold {:d}:".format(i + 1))
        # Load the saved clean numpy data from this fold
        numpy_save_path = get_clean_data_path(args.model_type, i + 1)
        # Temporary hack to use backup data directory
        # numpy_save_path = numpy_save_path.replace('varun', 'jayaram', 1)
        data_tr, labels_tr, data_te, labels_te = load_numpy_data(
            numpy_save_path)
        num_clean_tr = labels_tr.shape[0]
        num_clean_te = labels_te.shape[0]
        # Data loader for the train fold
        train_fold_loader = convert_to_loader(data_tr,
                                              labels_tr,
                                              dtype_x=torch.float,
                                              batch_size=args.batch_size,
                                              device=device)
        # Data loader for the test fold
        test_fold_loader = convert_to_loader(data_te,
                                             labels_te,
                                             dtype_x=torch.float,
                                             batch_size=args.batch_size,
                                             device=device)
        # Get the range of values in the data array
        # bounds = get_data_bounds(np.concatenate([data_tr, data_te], axis=0))
        print(
            "\nCalculating the layer embeddings and DNN predictions for the clean train data split:"
        )
        layer_embeddings_tr, labels_pred_tr = helper_layer_embeddings(
            model, device, train_fold_loader, detection_method, labels_tr)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the clean test data split:"
        )
        layer_embeddings_te, labels_pred_te = helper_layer_embeddings(
            model, device, test_fold_loader, detection_method, labels_te)
        del train_fold_loader, test_fold_loader

        # Load the saved noisy (Gaussian noise) numpy data generated from this training and test fold
        numpy_save_path = get_noisy_data_path(args.model_type, i + 1)
        # Temporary hack to use backup data directory
        # numpy_save_path = numpy_save_path.replace('varun', 'jayaram', 1)
        data_tr_noisy, data_te_noisy = load_noisy_data(numpy_save_path)
        # Noisy data have the same labels as the clean data
        labels_tr_noisy = labels_tr
        labels_te_noisy = labels_te
        # Check the number of noisy samples
        assert data_tr_noisy.shape[0] == num_clean_tr, (
            "Number of noisy samples from the train fold is different "
            "from expected")
        assert data_te_noisy.shape[0] == num_clean_te, (
            "Number of noisy samples from the test fold is different "
            "from expected")
        # Data loader for the noisy train and test fold data
        noisy_train_fold_loader = convert_to_loader(data_tr_noisy,
                                                    labels_tr_noisy,
                                                    dtype_x=torch.float,
                                                    batch_size=args.batch_size,
                                                    device=device)
        noisy_test_fold_loader = convert_to_loader(data_te_noisy,
                                                   labels_te_noisy,
                                                   dtype_x=torch.float,
                                                   batch_size=args.batch_size,
                                                   device=device)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the noisy train data split:"
        )
        layer_embeddings_tr_noisy, labels_pred_tr_noisy = helper_layer_embeddings(
            model, device, noisy_train_fold_loader, detection_method,
            labels_tr_noisy)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the noisy test data split:"
        )
        layer_embeddings_te_noisy, labels_pred_te_noisy = helper_layer_embeddings(
            model, device, noisy_test_fold_loader, detection_method,
            labels_te_noisy)
        del noisy_train_fold_loader, noisy_test_fold_loader

        # Load the saved adversarial numpy data generated from this training and test fold
        _, data_te_clean, data_tr_adv, labels_tr_adv, data_te_adv, labels_te_adv = load_adversarial_wrapper(
            i,
            args.model_type,
            args.adv_attack,
            args.max_attack_prop,
            num_clean_te,
            index_adv=args.index_adv)
        # `labels_te_adv` corresponds to the class labels of the clean samples, not that predicted by the DNN
        labels_te_clean = labels_te_adv
        num_adv_tr = labels_tr_adv.shape[0]
        num_adv_te = labels_te_adv.shape[0]
        print(
            "\nTrain fold: number of clean samples = {:d}, number of adversarial samples = {:d}, % of adversarial "
            "samples = {:.4f}".format(num_clean_tr, num_adv_tr,
                                      (100. * num_adv_tr) /
                                      (num_clean_tr + num_adv_tr)))
        print(
            "Test fold: number of clean samples = {:d}, number of adversarial samples = {:d}, % of adversarial "
            "samples = {:.4f}".format(num_clean_te, num_adv_te,
                                      (100. * num_adv_te) /
                                      (num_clean_te + num_adv_te)))

        # Adversarial data loader for the train fold
        adv_train_fold_loader = convert_to_loader(data_tr_adv,
                                                  labels_tr_adv,
                                                  dtype_x=torch.float,
                                                  batch_size=args.batch_size,
                                                  device=device)
        # Adversarial data loader for the test fold
        adv_test_fold_loader = convert_to_loader(data_te_adv,
                                                 labels_te_adv,
                                                 dtype_x=torch.float,
                                                 batch_size=args.batch_size,
                                                 device=device)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the adversarial train data split:"
        )
        layer_embeddings_tr_adv, labels_pred_tr_adv = helper_layer_embeddings(
            model, device, adv_train_fold_loader, detection_method,
            labels_tr_adv)
        check_label_mismatch(labels_tr_adv, labels_pred_tr_adv)
        print(
            "\nCalculating the layer embeddings and DNN predictions for the adversarial test data split:"
        )
        layer_embeddings_te_adv, labels_pred_te_adv = helper_layer_embeddings(
            model, device, adv_test_fold_loader, detection_method,
            labels_te_adv)
        check_label_mismatch(labels_te_adv, labels_pred_te_adv)
        del adv_train_fold_loader, adv_test_fold_loader

        # Detection labels (0 denoting clean and 1 adversarial)
        labels_detec = np.concatenate([
            np.zeros(labels_pred_te.shape[0], dtype=np.int),
            np.ones(labels_pred_te_adv.shape[0], dtype=np.int)
        ])
        # Proposed method
        nl = len(layer_embeddings_tr)
        st_ind = 0
        if args.use_deep_layers:
            if args.num_layers > nl:
                print(
                    "WARNING: number of layers specified using the option '--num-layers' exceeds the number "
                    "of layers in the model. Using all the layers.")
                st_ind = 0
            else:
                st_ind = nl - args.num_layers
                print(
                    "Using only the last {:d} layer embeddings from the {:d} layers for the proposed method."
                    .format(args.num_layers, nl))

        mod_dr = None if (
            model_dim_reduc is None) else model_dim_reduc[st_ind:]
        for cat in ('clean', 'adversarial'):
            det_model = DetectorLayerStatistics(
                layer_statistic=args.test_statistic,
                score_type=args.score_type,
                ood_detection=args.ood_detection,
                pvalue_fusion=args.pvalue_fusion,
                use_top_ranked=args.use_top_ranked,
                num_top_ranked=args.num_layers,
                skip_dim_reduction=(not apply_dim_reduc),
                model_dim_reduction=mod_dr,
                n_neighbors=n_neighbors,
                n_jobs=args.n_jobs,
                seed_rng=args.seed)
            # Fit the detector on clean or adversarial data from the training fold
            if cat == 'clean':
                _ = det_model.fit(layer_embeddings_tr[st_ind:], labels_tr,
                                  labels_pred_tr)
            else:
                _ = det_model.fit(layer_embeddings_tr_adv[st_ind:],
                                  labels_tr_adv, labels_pred_tr_adv)

            # Test statistics from each layer conditioned on the predicted class
            for c, arr in det_model.test_stats_pred_null.items():
                if n_samples_per_class < arr.shape[0]:
                    ind_samp = np.random.permutation(
                        arr.shape[0])[:n_samples_per_class]
                    test_stats_pred[cat].append(arr[ind_samp, :])
                else:
                    test_stats_pred[cat].append(arr)

            # Test statistics from each layer conditioned on the true class
            for c, arr in det_model.test_stats_true_null.items():
                if n_samples_per_class < arr.shape[0]:
                    ind_samp = np.random.permutation(
                        arr.shape[0])[:n_samples_per_class]
                    test_stats_true[cat].append(arr[ind_samp, :])
                else:
                    test_stats_true[cat].append(arr)

    test_stats_pred['clean'] = np.concatenate(test_stats_pred['clean'], axis=0)
    test_stats_pred['adversarial'] = np.concatenate(
        test_stats_pred['adversarial'], axis=0)
    test_stats_true['clean'] = np.concatenate(test_stats_true['clean'], axis=0)
    test_stats_true['adversarial'] = np.concatenate(
        test_stats_true['adversarial'], axis=0)
    return test_stats_pred, test_stats_true