def postprocess_xlnet_answer(
    results,
    examples_file,
    features_file,
    tokenizer,
    n_best_size=20,
    n_top_start=5,
    n_top_end=5,
    max_answer_length=30,
    unanswerable_exists=False,
    output_prediction_file="./qa_predictions.json",
    output_nbest_file="./nbest_predictions.json",
    output_null_log_odds_file="./null_odds.json",
    verbose_logging=False,
):
    """
    Postprocesses start and end logits generated by :meth:`AnswerExtractor.fit`
        for XLNet.

    Args:
        results (list): List of :class:`QAResultExtended`.
        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.
        tokenizer (XLNetTokenizer): Tokenizer used during data preprocessing.
        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.
        n_top_start (int, optional): Beam size for span start. Note that this needs to
            be consistent with the XLNet model configuration. Defaults to 5.
        n_top_end (int, optional): Beam size for span end. Note that this needs to be
            consistent with the XLNet model configuration. Defaults to 5.
        max_answer_length (int, optional): Maximum length of the answer. Defaults to 30.
        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.
        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".
        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:
        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

        for (feature_index, feature) in enumerate(features):
            result = unique_id_to_result[feature["unique_id"]]

            cur_null_score = result.cls_logits

            # if we could have irrelevant answers, get the min score of irrelevant
            score_null = min(score_null, cur_null_score)

            for i in range(n_top_start):
                for j in range(n_top_end):
                    start_log_prob = result.start_top_log_probs[i]
                    start_index = result.start_top_index[i]

                    j_index = i * n_top_end + j

                    end_log_prob = result.end_top_log_probs[j_index]
                    end_index = result.end_top_index[j_index]

                    # 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 >= feature["paragraph_len"] - 1:
                        continue
                    if end_index >= feature["paragraph_len"] - 1:
                        continue

                    if not feature["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=start_log_prob,
                            end_logit=end_log_prob,
                        )
                    )

        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
            feature = features[pred.feature_index]

            # XLNet un-tokenizer
            # Let's keep it simple for now and see if we need all this later.
            #
            # tok_start_to_orig_index = feature.tok_start_to_orig_index
            # tok_end_to_orig_index = feature.tok_end_to_orig_index
            # start_orig_pos = tok_start_to_orig_index[pred.start_index]
            # end_orig_pos = tok_end_to_orig_index[pred.end_index]
            # paragraph_text = example.paragraph_text
            # final_text = paragraph_text[start_orig_pos: end_orig_pos + 1].strip()

            # Previously used Bert untokenizer
            tok_tokens = feature["tokens"][pred.start_index : (pred.end_index + 1)]
            orig_doc_start = feature["token_to_orig_map"][str(pred.start_index)]
            orig_doc_end = feature["token_to_orig_map"][str(pred.end_index)]
            orig_tokens = example["doc_tokens"][orig_doc_start : (orig_doc_end + 1)]
            tok_text = tokenizer.convert_tokens_to_string(tok_tokens)

            # 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, tokenizer.do_lower_case, verbose_logging
            )

            if final_text in seen_predictions:
                continue

            seen_predictions[final_text] = True

            nbest.append(
                _NbestPrediction(
                    text=final_text,
                    start_logit=pred.start_logit,
                    end_logit=pred.end_logit,
                )
            )

        # 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="", start_logit=-1e6, end_logit=-1e6))

        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:
                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)

        assert len(nbest_json) >= 1
        assert best_non_null_entry is not None

        score_diff = score_null
        scores_diff_json[example["qa_id"]] = score_diff
        # note(zhiliny): always predict best_non_null_entry
        # and the evaluation script will search for the best threshold
        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 XLNet.

    Args:
        results (list): List of :class:`QAResultExtended`.
        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.
        tokenizer (XLNetTokenizer): Tokenizer used during data preprocessing.
        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.
        n_top_start (int, optional): Beam size for span start. Note that this needs to
            be consistent with the XLNet model configuration. Defaults to 5.
        n_top_end (int, optional): Beam size for span end. Note that this needs to be
            consistent with the XLNet model configuration. Defaults to 5.
        max_answer_length (int, optional): Maximum length of the answer. Defaults to 30.
        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.
        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".
        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