def _create_qa_features(
    example,
    model_type,
    tokenizer,
    unique_id,
    is_training,
    max_question_length,
    max_seq_length,
    doc_stride,
    custom_tokenize=None,
):
    """Extracts features for model training and scoring from document-question-answer
        triplet.
    """

    # _QAFeatures is data structure representing features of an unique document
    # span-question-answer triplet.
    # Args:
    #     unique_id (int): An unique id identifying the span-question-answer triplet.
    #     qa_id (int or str):  An unique id identifying the document-question-answer
    #     sample in the original :class:`utils_nlp.dataset.pytorch.QADataset`
    #     tokens (list): Concatenated question tokens and paragraph tokens.
    #     token_to_orig_map (dict): A dictionary mapping token indices in the
    #         document span back to the token indices in the original document
    #         before document splitting.
    #         This is needed during post-processing to generate the final
    #         predicted answer.
    #     token_is_max_context (list): List of booleans indicating whether a
    #         token has the maximum context in teh current document span if it
    #         appears in multiple document spans and gets multiple predicted
    #         scores. We only want to consider the score with "maximum context".
    #         "Maximum context" is defined as the *minimum* of its left and
    #         right context.
    #         For example:
    #             Doc: the man went to the store and bought a gallon of milk
    #             Span A: the man went to the
    #             Span B: to the store and bought
    #             Span C: and bought a gallon of

    #         In the example the maximum context for 'bought' would be span C
    #         since it has 1 left context and 3 right context, while span B
    #         has 4 left context and 0 right context.
    #         This is needed during post-processing to generate the final
    #         predicted answer.
    #     input_ids (list): List of numerical token indices corresponding to
    #         the tokens.
    #     input_mask (list): List of 1s and 0s indicating if a token is from
    #         the input data or padded to conform to the maximum sequence
    #         length. 1 for actual token and 0 for padded token.
    #     segment_ids (list): List of 0s and 1s indicating if a token is from
    #         the question text (0) or paragraph text (1).
    #     start_position (int): Index of the starting token of the answer span.
    #     end_position (int): Index of the ending token of the answer span.
    #     cls_index (int): Index of the CLS token.
    #     p_mask (list): Mask with 1 for token than cannot be in the answer,
    #     0 for token which can be in an answer.
    #     paragraph_len(int): Number of tokens in the document span.
    _QAFeatures = collections.namedtuple(
        "_QAFeatures",
        [
            "unique_id",
            "qa_id",
            "tokens",
            "token_to_orig_map",
            "token_is_max_context",
            "input_ids",
            "input_mask",
            "segment_ids",
            "start_position",
            "end_position",
            "cls_index",
            "p_mask",
            "paragraph_len",
        ],
    )

    if custom_tokenize:
        tokenize_func = custom_tokenize
    else:
        tokenize_func = tokenizer.tokenize

    def _improve_answer_span(doc_tokens, input_start, input_end, orig_answer_text):
        """Returns tokenized answer spans that better match the annotated answer."""

        # We first project character-based annotations to
        # whitespace-tokenized words. But then after WordPiece tokenization, we can
        # often find a "better match". For example:
        #
        #   Question: What year was John Smith born?
        #   Context: The leader was John Smith (1895-1943).
        #   Answer: 1895
        #
        # The original whitespace-tokenized answer will be "(1895-1943).". However
        # after tokenization, our tokens will be "( 1895 - 1943 ) .". So we can match
        # the exact answer, 1895.
        #
        # However, this is not always possible. Consider the following:
        #
        #   Question: What country is the top exporter of electornics?
        #   Context: The Japanese electronics industry is the lagest in the world.
        #   Answer: Japan
        #
        # In this case, the annotator chose "Japan" as a character sub-span of
        # the word "Japanese". Since our WordPiece tokenizer does not split
        # "Japanese", we just use "Japanese" as the annotation. This is fairly rare,
        # but does happen.
        tok_answer_text = " ".join(tokenize_func(orig_answer_text))

        for new_start in range(input_start, input_end + 1):
            for new_end in range(input_end, new_start - 1, -1):
                text_span = " ".join(doc_tokens[new_start : (new_end + 1)])
                if text_span == tok_answer_text:
                    return (new_start, new_end)

        return (input_start, input_end)

    def _check_is_max_context(doc_spans, cur_span_index, position):
        """Check if this is the 'max context' doc span for the token."""

        # Because of the sliding window approach taken to scoring documents, a single
        # token can appear in multiple documents. E.g.
        #  Doc: the man went to the store and bought a gallon of milk
        #  Span A: the man went to the
        #  Span B: to the store and bought
        #  Span C: and bought a gallon of
        #  ...
        #
        # Now the word 'bought' will have two scores from spans B and C. We only
        # want to consider the score with "maximum context", which we define as
        # the *minimum* of its left and right context (the *sum* of left and
        # right context will always be the same, of course).
        #
        # In the example the maximum context for 'bought' would be span C since
        # it has 1 left context and 3 right context, while span B has 4 left context
        # and 0 right context.
        best_score = None
        best_span_index = None
        for (span_index, doc_span) in enumerate(doc_spans):
            end = doc_span.start + doc_span.length - 1
            if position < doc_span.start:
                continue
            if position > end:
                continue
            num_left_context = position - doc_span.start
            num_right_context = end - position
            score = min(num_left_context, num_right_context) + 0.01 * doc_span.length
            if best_score is None or score > best_score:
                best_score = score
                best_span_index = span_index

        return cur_span_index == best_span_index

    cls_token = "[CLS]"
    sep_token = "[SEP]"
    pad_token = 0
    sequence_a_segment_id = 0
    sequence_b_segment_id = 1
    mask_padding_with_zero = True

    if model_type == "xlnet":
        cls_token_segment_id = 2
        # Should this be 4, or it doesn't matter?
        pad_token_segment_id = 3
        cls_token_at_end = True
    else:
        cls_token_segment_id = 0
        pad_token_segment_id = 0
        cls_token_at_end = False

    qa_features = []

    # unique_id identified unique feature/label pairs. It's different
    # from qa_id in that each qa_example can be broken down into
    # multiple feature samples if the paragraph length is longer than
    # maximum sequence length allowed
    query_tokens = tokenize_func(example.question_text)

    if len(query_tokens) > max_question_length:
        query_tokens = query_tokens[0:max_question_length]
    # map word-piece tokens to original tokens
    tok_to_orig_index = []
    # map original tokens to corresponding word-piece tokens
    orig_to_tok_index = []
    all_doc_tokens = []
    for (i, token) in enumerate(example.doc_tokens):
        orig_to_tok_index.append(len(all_doc_tokens))
        sub_tokens = tokenize_func(token)
        for sub_token in sub_tokens:
            tok_to_orig_index.append(i)
            all_doc_tokens.append(sub_token)

    tok_start_position = None
    tok_end_position = None
    if is_training and example.is_impossible:
        tok_start_position = -1
        tok_end_position = -1
    if is_training and not example.is_impossible:
        tok_start_position = orig_to_tok_index[example.start_position]
        if example.end_position < len(example.doc_tokens) - 1:
            # +1: move the the token after the ending token in
            # original tokens
            # -1, moves one step back
            # these two operations ensures word piece is covered
            # when it's part of the original ending token.
            tok_end_position = orig_to_tok_index[example.end_position + 1] - 1
        else:
            tok_end_position = len(all_doc_tokens) - 1
        (tok_start_position, tok_end_position) = _improve_answer_span(
            all_doc_tokens,
            tok_start_position,
            tok_end_position,
            example.orig_answer_text,
        )

    # The -3 accounts for [CLS], [SEP] and [SEP]
    max_tokens_for_doc = max_seq_length - len(query_tokens) - 3

    # We can have documents that are longer than the maximum sequence length.
    # To deal with this we do a sliding window approach, where we take chunks
    # of the up to our max length with a stride of `doc_stride`.
    _DocSpan = collections.namedtuple("DocSpan", ["start", "length"])
    doc_spans = []
    start_offset = 0
    while start_offset < len(all_doc_tokens):
        length = len(all_doc_tokens) - start_offset
        if length > max_tokens_for_doc:
            length = max_tokens_for_doc
        doc_spans.append(_DocSpan(start=start_offset, length=length))
        if start_offset + length == len(all_doc_tokens):
            break
        start_offset += min(length, doc_stride)

    for (doc_span_index, doc_span) in enumerate(doc_spans):
        if is_training:
            unique_id += 1
        else:
            unique_id += 2

        tokens = []
        token_to_orig_map = {}
        token_is_max_context = {}
        segment_ids = []

        # p_mask: mask with 1 for token than cannot be in the answer
        # (0 for token which can be in an answer)
        # Original TF implem also keep the classification token (set to 0)
        # (not sure why...)
        # TODO: Should we set p_mask = 1 for cls token?
        p_mask = []

        # CLS token at the beginning
        if not cls_token_at_end:
            tokens.append(cls_token)
            segment_ids.append(cls_token_segment_id)
            p_mask.append(0)
            cls_index = 0

        # XLNet: P SEP Q SEP CLS
        # Others: CLS Q SEP P SEP
        if model_type != "xlnet":
            # Query
            tokens += query_tokens
            segment_ids += [sequence_a_segment_id] * len(query_tokens)
            p_mask += [1] * len(query_tokens)

            # SEP token
            tokens.append(sep_token)
            segment_ids.append(sequence_a_segment_id)
            p_mask.append(1)

        # Paragraph
        for i in range(doc_span.length):
            split_token_index = doc_span.start + i
            token_to_orig_map[len(tokens)] = tok_to_orig_index[split_token_index]

            # TODO: maybe this can be improved to compute
            # is_max_context for each token only once.
            is_max_context = _check_is_max_context(
                doc_spans, doc_span_index, split_token_index
            )
            token_is_max_context[len(tokens)] = is_max_context
            tokens.append(all_doc_tokens[split_token_index])
            if model_type == "xlnet":
                segment_ids.append(sequence_a_segment_id)
            else:
                segment_ids.append(sequence_b_segment_id)
            p_mask.append(0)
        paragraph_len = doc_span.length

        if model_type == "xlnet":
            # SEP token
            tokens.append(sep_token)
            segment_ids.append(sequence_a_segment_id)
            p_mask.append(1)

            tokens += query_tokens
            segment_ids += [sequence_b_segment_id] * len(query_tokens)
            p_mask += [1] * len(query_tokens)

        # SEP token
        tokens.append(sep_token)
        segment_ids.append(sequence_b_segment_id)
        p_mask.append(1)

        # CLS token at the end
        if cls_token_at_end:
            tokens.append(cls_token)
            segment_ids.append(cls_token_segment_id)
            p_mask.append(0)
            cls_index = len(tokens) - 1  # Index of classification token

        input_ids = tokenizer.convert_tokens_to_ids(tokens)

        # The mask has 1 for real tokens and 0 for padding tokens. Only real
        # tokens are attended to.
        input_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)

        # Zero-pad up to the sequence length.
        if len(input_ids) < max_seq_length:
            pad_token_length = max_seq_length - len(input_ids)
            pad_mask = 0 if mask_padding_with_zero else 1
            input_ids += [pad_token] * pad_token_length
            input_mask += [pad_mask] * pad_token_length
            segment_ids += [pad_token_segment_id] * pad_token_length
            p_mask += [1] * pad_token_length

        assert len(input_ids) == max_seq_length
        assert len(input_mask) == max_seq_length
        assert len(segment_ids) == max_seq_length

        span_is_impossible = example.is_impossible
        start_position = None
        end_position = None
        if is_training and not span_is_impossible:
            # For training, if our document chunk does not contain an annotation
            # we throw it out, since there is nothing to predict.
            doc_start = doc_span.start
            doc_end = doc_span.start + doc_span.length - 1
            out_of_span = False
            if not (tok_start_position >= doc_start and tok_end_position <= doc_end):
                out_of_span = True
            if out_of_span:
                start_position = 0
                end_position = 0
                span_is_impossible = True
            else:
                # +1 for [CLS] token
                # +1 for [SEP] token
                if model_type == "xlnet":
                    doc_offset = 0
                else:
                    doc_offset = len(query_tokens) + 2
                start_position = tok_start_position - doc_start + doc_offset
                end_position = tok_end_position - doc_start + doc_offset

        if is_training and span_is_impossible:
            start_position = cls_index
            end_position = cls_index

        qa_features.append(
            _QAFeatures(
                unique_id=unique_id,
                qa_id=example.qa_id,
                tokens=tokens,
                token_to_orig_map=token_to_orig_map,
                token_is_max_context=token_is_max_context,
                input_ids=input_ids,
                input_mask=input_mask,
                segment_ids=segment_ids,
                start_position=start_position,
                end_position=end_position,
                cls_index=cls_index,
                p_mask=p_mask,
                paragraph_len=paragraph_len,
            )
        )

        return qa_features
Extracts features for model training and scoring from document-question-answer
        triplet.
    
utils_nlp/models/transformers/question_answering.py