def postprocess_bert_answer(
    results,
    examples_file,
    features_file,
    do_lower_case,
    unanswerable_exists=False,
    n_best_size=20,
    max_answer_length=30,
    output_prediction_file="./qa_predictions.json",
    output_nbest_file="./nbest_predictions.json",
    output_null_log_odds_file="./null_odds.json",
    null_score_diff_threshold=0.0,
    verbose_logging=False,
):
    """
    Postprocesses start and end logits
    generated by :meth:`AnswerExtractor.fit` for BERT.

    Args:
        results (list): List of :class:`QAResult`.
        examples_file (str): One of the files cached by :meth:`QAProcessor.preprocess`.
            This file contains the original document tokens that are used to generate
            the final answers from the predicted start and end positions.
        features_file (str): One of the files cached by :meth:`QAProcessor.preprocess`.
            This file contains the mapping from indices in the processed token list
            to the original document tokens that are used to generate the final
            predicted answers.
        do_lower_case (bool): Whether an uncased tokenizer was used during
            data preprocessing. This is required during answer finalization
            by comparing the predicted answer text and the original
            text span in :func:`_get_final_text`.
        unanswerable_exists (bool, optional): Whether there are unanswerable
            questions in the data. If True, the start and end logits of the [CLS]
            token, which indicate the probability of the answer being empty,
            are included in the candidate answer list.
            Defaults to False.
        n_best_size (int, optional): The number of candidates to choose from each
            QAResult to generate the final prediction. It's also the maximum number
            of n-best answers to output for each question.
            Note that the number of n-best answers can be smaller than `n_best_size`
            because some unqualified answers,
            e.g. answer that are too long, are removed.
        max_answer_length (int, optional): Maximum length of the answer. Defaults to 30.
        output_prediction_file (str, optional): Path of the file to save the
            predicted answers. Defaults to "./qa_predictions.json".
        output_nbest_file (str, optional): Path of the file to save the n-best answers.
            Defaults to "./nbest_predictions.json".
        output_null_log_odds_file (str, optional): If unanswerable_exists is True,
            the score difference between empty prediction and best non-empty prediction
            are saved to this file. These scores can be used to find the best threshold
            for predicting an empty answer. Defaults to "./null_odds.json".
        null_score_diff_threshold (float, optional): If unanswerable_exists=True
            and the score difference between empty prediction and best non-empty
            prediction is higher than this threshold, the final predicted
            answer is empty.
            Defaults to 0.0.
        verbose_logging (bool, optional): Whether to log details of
            answer postprocessing. Defaults to False.

    Returns:
        tuple: (OrderedDict, OrderedDict, OrderedDict)
            The keys of the dictionaries are the `qa_id` in the original
            :class:`utils_nlp.dataset.pytorch.QADataset`
            The values of the first dictionary are the predicted answer texts
            in string type. The values of the second dictionary are the softmax
            probabilities of the predicted answers.
            The values of the third dictionary are the n-best answers for each qa_id.
            Note that the number of n-best answers can be smaller than `n_best_size`
            because some unqualified answers, e.g. answers that are too long,
            are removed.

    """
    with jsonlines.open(examples_file) as reader:
        examples_all = list(reader.iter())

    with jsonlines.open(features_file) as reader:
        features_all = list(reader.iter())

    qa_id_to_features = collections.defaultdict(list)
    # Map unique features to the original doc-question-answer triplet
    # Each doc-question-answer triplet can have multiple features because the doc
    # could be split into multiple spans
    for f in features_all:
        qa_id_to_features[f["qa_id"]].append(f)

    unique_id_to_result = {}
    for r in results:
        unique_id_to_result[r.unique_id] = r

    all_predictions = collections.OrderedDict()
    all_probs = collections.OrderedDict()
    all_nbest_json = collections.OrderedDict()
    scores_diff_json = collections.OrderedDict()

    for example in examples_all:
        # get all the features belonging to the same example,
        # i.e. paragaraph/question pair.
        features = qa_id_to_features[example["qa_id"]]

        prelim_predictions = []
        # keep track of the minimum score of null start+end of position 0
        score_null = 1000000  # large and positive

        min_null_feature_index = 0  # the paragraph slice with min null score
        null_start_logit = 0  # the start logit at the slice with min null score
        null_end_logit = 0  # the end logit at the slice with min null score
        for (feature_index, f) in enumerate(features):
            result = unique_id_to_result[f["unique_id"]]
            start_indexes = _get_best_indexes(result.start_logits, n_best_size)
            end_indexes = _get_best_indexes(result.end_logits, n_best_size)
            # if we could have irrelevant answers, get the min score of irrelevant
            if unanswerable_exists:
                # The first element of the start end end logits is the
                # probability of predicting the [CLS] token as the start and
                # end positions of the answer, which means the answer is
                # empty.
                feature_null_score = result.start_logits[0] + result.end_logits[0]
                if feature_null_score < score_null:
                    score_null = feature_null_score
                    min_null_feature_index = feature_index
                    null_start_logit = result.start_logits[0]
                    null_end_logit = result.end_logits[0]
            for start_index in start_indexes:
                for end_index in end_indexes:
                    # We could hypothetically create invalid predictions, e.g., predict
                    # that the start of the span is in the question. We throw out all
                    # invalid predictions.
                    if start_index >= len(f["tokens"]):
                        continue
                    if end_index >= len(f["tokens"]):
                        continue
                    if str(start_index) not in f["token_to_orig_map"]:
                        continue
                    if str(end_index) not in f["token_to_orig_map"]:
                        continue
                    if not f["token_is_max_context"].get(str(start_index), False):
                        continue
                    if end_index < start_index:
                        continue
                    length = end_index - start_index + 1
                    if length > max_answer_length:
                        continue
                    prelim_predictions.append(
                        _PrelimPrediction(
                            feature_index=feature_index,
                            start_index=start_index,
                            end_index=end_index,
                            start_logit=result.start_logits[start_index],
                            end_logit=result.end_logits[end_index],
                        )
                    )
        if unanswerable_exists:
            prelim_predictions.append(
                _PrelimPrediction(
                    feature_index=min_null_feature_index,
                    start_index=0,
                    end_index=0,
                    start_logit=null_start_logit,
                    end_logit=null_end_logit,
                )
            )

        # Sort by the sum of the start and end logits in ascending order,
        # so that the first element is the most probable answer
        prelim_predictions = sorted(
            prelim_predictions,
            key=lambda x: (x.start_logit + x.end_logit),
            reverse=True,
        )

        seen_predictions = {}
        nbest = []
        for pred in prelim_predictions:
            if len(nbest) >= n_best_size:
                break
            f = features[pred.feature_index]
            if pred.start_index > 0:  # this is a non-null prediction
                tok_tokens = f["tokens"][pred.start_index : (pred.end_index + 1)]
                orig_doc_start = f["token_to_orig_map"][str(pred.start_index)]
                orig_doc_end = f["token_to_orig_map"][str(pred.end_index)]
                orig_tokens = example["doc_tokens"][orig_doc_start : (orig_doc_end + 1)]
                tok_text = " ".join(tok_tokens)

                # De-tokenize WordPieces that have been split off.
                tok_text = tok_text.replace(" ##", "")
                tok_text = tok_text.replace("##", "")

                # Clean whitespace
                tok_text = tok_text.strip()
                tok_text = " ".join(tok_text.split())
                orig_text = " ".join(orig_tokens)

                final_text = _get_final_text(
                    tok_text, orig_text, do_lower_case, verbose_logging
                )
                if final_text in seen_predictions:
                    continue

                seen_predictions[final_text] = True
            else:
                final_text = ""
                seen_predictions[final_text] = True

            nbest.append(
                _NbestPrediction(
                    text=final_text,
                    start_logit=pred.start_logit,
                    end_logit=pred.end_logit,
                )
            )
        # if we didn't include the empty option in the n-best, include it
        if unanswerable_exists:
            if "" not in seen_predictions:
                nbest.append(
                    _NbestPrediction(
                        text="", start_logit=null_start_logit, end_logit=null_end_logit
                    )
                )

            # In very rare edge cases we could only have single null prediction.
            # So we just create a nonce prediction in this case to avoid failure.
            if len(nbest) == 1:
                nbest.insert(
                    0, _NbestPrediction(text="empty", start_logit=0.0, end_logit=0.0)
                )

        # In very rare edge cases we could have no valid predictions. So we
        # just create a nonce prediction in this case to avoid failure.
        if not nbest:
            nbest.append(_NbestPrediction(text="empty", start_logit=0.0, end_logit=0.0))

        assert len(nbest) >= 1

        total_scores = []
        best_non_null_entry = None
        for ie, entry in enumerate(nbest):
            total_scores.append(entry.start_logit + entry.end_logit)
            if not best_non_null_entry:
                if entry.text:
                    best_non_null_entry = entry
                    best_non_null_entry_index = ie

        probs = _compute_softmax(total_scores)

        nbest_json = []
        for (i, entry) in enumerate(nbest):
            output = collections.OrderedDict()
            output["text"] = entry.text
            output["probability"] = probs[i]
            output["start_logit"] = entry.start_logit
            output["end_logit"] = entry.end_logit
            nbest_json.append(output)

            if entry.text == "":
                null_prediction_index = i

        assert len(nbest_json) >= 1

        if not unanswerable_exists:
            all_predictions[example["qa_id"]] = nbest_json[0]["text"]
            all_probs[example["qa_id"]] = nbest_json[0]["probability"]
        else:
            # predict "" iff the null score - the score of best non-null > threshold
            score_diff = (
                score_null
                - best_non_null_entry.start_logit
                - (best_non_null_entry.end_logit)
            )
            scores_diff_json[example["qa_id"]] = score_diff
            if score_diff > null_score_diff_threshold:
                all_predictions[example["qa_id"]] = ""
                # TODO: double check this
                all_probs[example["qa_id"]] = probs[null_prediction_index]
            else:
                all_predictions[example["qa_id"]] = best_non_null_entry.text
                all_probs[example["qa_id"]] = probs[best_non_null_entry_index]
        all_nbest_json[example["qa_id"]] = nbest_json

    """Write final predictions to the json file and log-odds of null if needed."""
    logger.info("Writing predictions to: %s" % (output_prediction_file))
    logger.info("Writing nbest to: %s" % (output_nbest_file))

    with open(output_prediction_file, "w") as writer:
        writer.write(json.dumps(all_predictions, indent=4) + "\n")

    with open(output_nbest_file, "w") as writer:
        writer.write(json.dumps(all_nbest_json, indent=4) + "\n")

    if unanswerable_exists:
        logger.info("Writing null odds to: %s" % (output_null_log_odds_file))
        with open(output_null_log_odds_file, "w") as writer:
            writer.write(json.dumps(scores_diff_json, indent=4) + "\n")

    return all_predictions, all_probs, all_nbest_json

    Postprocesses start and end logits
    generated by :meth:`AnswerExtractor.fit` for BERT.

    Args:
        results (list): List of :class:`QAResult`.
        examples_file (str): One of the files cached by :meth:`QAProcessor.preprocess`.
            This file contains the original document tokens that are used to generate
            the final answers from the predicted start and end positions.
        features_file (str): One of the files cached by :meth:`QAProcessor.preprocess`.
            This file contains the mapping from indices in the processed token list
            to the original document tokens that are used to generate the final
            predicted answers.
        do_lower_case (bool): Whether an uncased tokenizer was used during
            data preprocessing. This is required during answer finalization
            by comparing the predicted answer text and the original
            text span in :func:`_get_final_text`.
        unanswerable_exists (bool, optional): Whether there are unanswerable
            questions in the data. If True, the start and end logits of the [CLS]
            token, which indicate the probability of the answer being empty,
            are included in the candidate answer list.
            Defaults to False.
        n_best_size (int, optional): The number of candidates to choose from each
            QAResult to generate the final prediction. It's also the maximum number
            of n-best answers to output for each question.
            Note that the number of n-best answers can be smaller than `n_best_size`
            because some unqualified answers,
            e.g. answer that are too long, are removed.
        max_answer_length (int, optional): Maximum length of the answer. Defaults to 30.
        output_prediction_file (str, optional): Path of the file to save the
            predicted answers. Defaults to "./qa_predictions.json".
        output_nbest_file (str, optional): Path of the file to save the n-best answers.
            Defaults to "./nbest_predictions.json".
        output_null_log_odds_file (str, optional): If unanswerable_exists is True,
            the score difference between empty prediction and best non-empty prediction
            are saved to this file. These scores can be used to find the best threshold
            for predicting an empty answer. Defaults to "./null_odds.json".
        null_score_diff_threshold (float, optional): If unanswerable_exists=True
            and the score difference between empty prediction and best non-empty
            prediction is higher than this threshold, the final predicted
            answer is empty.
            Defaults to 0.0.
        verbose_logging (bool, optional): Whether to log details of
            answer postprocessing. Defaults to False.

    Returns:
        tuple: (OrderedDict, OrderedDict, OrderedDict)
            The keys of the dictionaries are the `qa_id` in the original
            :class:`utils_nlp.dataset.pytorch.QADataset`
            The values of the first dictionary are the predicted answer texts
            in string type. The values of the second dictionary are the softmax
            probabilities of the predicted answers.
            The values of the third dictionary are the n-best answers for each qa_id.
            Note that the number of n-best answers can be smaller than `n_best_size`
            because some unqualified answers, e.g. answers that are too long,
            are removed.

    
utils_nlp/models/transformers/question_answering.py