def evaluate_qa(
    actual_dataset, preds, na_probs=None, na_prob_thresh=0, unanswerable_exists=False, out_file=None
):
    """
    Evaluate question answering prediction results against ground truth answers.

    Args:
        Evaluates question answering model performance.

        Args:
            actual_dataset (:class:`utils_nlp.dataset.pytorch.QADataset`): Input question answering
                dataset with ground truth answers.
            preds (dict): The key of the dictionary is the qa_id in the original
                :class:`utils_nlp.dataset.pytorch.QADataset`. The values of the dictionary are
                the predicted answer texts in string type.
            na_probs (dict, optional): Dictionary of qa_id and unanswerable probability pairs.
                If None, unanswerable probabilities are all set to zero. Defaults to None.
            na_prob_thresh (float, optional): Probability threshold to predict a question to be
                unanswerable. For an unanswerable question, if `na_probs` > `na_prob_thresh`,
                the prediction is considered as correct. Otherwise, the prediction is considered as
                incorrect. Defaults to 0.
            out_file (str, optional): Path of the file to save the evaluation results to.
                Defaults to None.

        Returns:
            dict: A dictionary with exact_match and f1 values.
    """

    # Helper functions
    def _apply_no_ans_threshold(scores, na_probs, qid_to_has_ans, na_prob_thresh):
        """Update the input scores by applying unanswerable probability threshold."""

        new_scores = {}
        for qid, s in scores.items():
            pred_na = na_probs[qid] > na_prob_thresh
            if pred_na:
                new_scores[qid] = float(not qid_to_has_ans[qid])
            else:
                new_scores[qid] = s
        return new_scores

    def _make_eval_dict(exact_scores, f1_scores, qid_list=None):
        """Create a dictionary of evaluation results."""
        if not qid_list:
            total = len(exact_scores)
            return collections.OrderedDict(
                [
                    ("exact", 100.0 * sum(exact_scores.values()) / total),
                    ("f1", 100.0 * sum(f1_scores.values()) / total),
                    ("total", total),
                ]
            )
        else:
            total = len(qid_list)
            return collections.OrderedDict(
                [
                    ("exact", 100.0 * sum(exact_scores[k] for k in qid_list) / total),
                    ("f1", 100.0 * sum(f1_scores[k] for k in qid_list) / total),
                    ("total", total),
                ]
            )

    def _merge_eval(main_eval, new_eval, prefix):
        """Merge multiple evaluation result dictionaries."""
        for k in new_eval:
            main_eval["%s_%s" % (prefix, k)] = new_eval[k]

    # Helper functions end

    if na_probs is None:
        na_probs_available = False
        na_probs = {k: 0.0 for k in preds}
    else:
        na_probs_available = True

    qa_ids = [item.qa_id for item in actual_dataset]
    actuals = [item.answer_text for item in actual_dataset]

    qid_to_has_ans = {qa_id: bool(ans) for (qa_id, ans) in zip(qa_ids, actuals)}
    has_ans_qids = [k for k, v in qid_to_has_ans.items() if v]
    no_ans_qids = [k for k, v in qid_to_has_ans.items() if not v]
    exact_raw, f1_raw = get_raw_scores(qa_ids, actuals, preds)
    exact_thresh = _apply_no_ans_threshold(exact_raw, na_probs, qid_to_has_ans, na_prob_thresh)
    f1_thresh = _apply_no_ans_threshold(f1_raw, na_probs, qid_to_has_ans, na_prob_thresh)
    out_eval = _make_eval_dict(exact_thresh, f1_thresh)
    if has_ans_qids:
        has_ans_eval = _make_eval_dict(exact_thresh, f1_thresh, qid_list=has_ans_qids)
        _merge_eval(out_eval, has_ans_eval, "HasAns")
    if no_ans_qids:
        no_ans_eval = _make_eval_dict(exact_thresh, f1_thresh, qid_list=no_ans_qids)
        _merge_eval(out_eval, no_ans_eval, "NoAns")

    if na_probs_available:
        find_all_best_thresh(
            out_eval, preds, exact_raw, f1_raw, na_probs, qid_to_has_ans, unanswerable_exists
        )

    if out_file:
        with open(out_file, "w") as f:
            json.dump(out_eval, f)
    else:
        print(json.dumps(out_eval, indent=2))
    return out_eval

    Evaluate question answering prediction results against ground truth answers.

    Args:
        Evaluates question answering model performance.

        Args:
            actual_dataset (:class:`utils_nlp.dataset.pytorch.QADataset`): Input question answering
                dataset with ground truth answers.
            preds (dict): The key of the dictionary is the qa_id in the original
                :class:`utils_nlp.dataset.pytorch.QADataset`. The values of the dictionary are
                the predicted answer texts in string type.
            na_probs (dict, optional): Dictionary of qa_id and unanswerable probability pairs.
                If None, unanswerable probabilities are all set to zero. Defaults to None.
            na_prob_thresh (float, optional): Probability threshold to predict a question to be
                unanswerable. For an unanswerable question, if `na_probs` > `na_prob_thresh`,
                the prediction is considered as correct. Otherwise, the prediction is considered as
                incorrect. Defaults to 0.
            out_file (str, optional): Path of the file to save the evaluation results to.
                Defaults to None.

        Returns:
            dict: A dictionary with exact_match and f1 values.
    
utils_nlp/eval/question_answering.py