§7.2Extracting Answers from Text

Part II NLPT pp. 39–47 · ~6 min read

  • span classification
  • answer span
  • token_type_ids
  • sliding window
  • cls token

To answer “Is it waterproof?” from the review “This watch is waterproof at 30m depth”, our model must output the span “waterproof at 30m”. Doing that requires three things: framing the learning problem, tokenizing for QA, and handling contexts longer than the model can read.

Span classification

The standard framing is span classification : the model predicts, for every token, a start logit and an end logit; the answer is bounded by the argmax of each. The QA “head” is just a linear layer on top of the encoder’s hidden states — QA is a form of token classification.

startendLinearHHHHHHHHhidden statesTransformer encoder[CLS]Whyis?[SEP]Itemnoflashtokensspecialquestioncontext
Figure 7-4 (recreated). The span-classification head: encoder → hidden states → a linear layer → a start and end logit per token.

Since SubjQA has only 1,295 training examples, we don’t train the head from scratch — we start from a model already fine-tuned on SQuAD (the label structure is identical across QA datasets). A few good baselines:

Table 7-2 — transformer baselines fine-tuned on SQuAD 2.0. Click a cell for detail.
ParametersF1 (SQuAD 2.0)Note
MiniLM66M79.5distilled BERT-base, ~2× faster
RoBERTa-base125M83.0better than BERT, single-GPU fine-tuning
ALBERT-XXL235M88.1state of the art, hard to deploy
XLM-RoBERTa-large570M83.8multilingual, 100 languages, zero-shot
Dotted-underlined cells have explanations — click one.

We’ll use MiniLM (deepset/minilm-uncased-squad2) for fast iteration.

Tokenizing text for QA

QA inputs are (question, context) pairs. The tokenizer packs them as [CLS] question [SEP] context [SEP] and adds a token_type_ids tensor marking question tokens (0) from context tokens (1):

question = "How much music can this hold?"
context = "An MP3 is about 1 MB/minute, so about 6000 hours depending on file size."
inputs = tokenizer(question, context, return_tensors="pt")
print(tokenizer.decode(inputs["input_ids"][0]))
# [CLS] how much music can this hold? [SEP] an mp3 is about 1 mb / minute, so
# about 6000 hours depending on file size. [SEP]

Predicting the answer span

Running the QA model returns a start_logits and an end_logits tensor — one logit per token. Hover the bars: the start peaks on the quantities, the end on their units, and the two argmaxes bound the answer.

from transformers import AutoModelForQuestionAnswering
model = AutoModelForQuestionAnswering.from_pretrained(model_ckpt)
outputs = model(**inputs)
start_idx = torch.argmax(outputs.start_logits)
end_idx   = torch.argmax(outputs.end_logits) + 1
answer = tokenizer.decode(inputs["input_ids"][0][start_idx:end_idx])
# 'answer': '6000 hours'
Q

How much music can this hold?

review (context)

An MP3 is about 1 MB/minute, so about 6000 hours depending on file size.

  • 6000 hourschars 3848score 0.27

The start logits peak on the quantities “1” and “6000”; the end logits peak on “minute” and “hours” — sensible for a “how much” question.

start (▲) & end (▼) logits per token — argmax = the span

[CLS]
how
much
music
can
this
hold
?
[SEP]
an
mp3
is
about
1
mb
/
minute
,
so
about
6000
hours
depending
on
file
size
.
[SEP]

argmax start = “6000”, argmax end = “hours” → 6000 hours

Figure 7-6 (recreated). The QA head predicts a start and end logit for every token; the argmaxes bound the answer span.

Follow the shapes — QA is just two per-token logit vectors reduced to one span:

input_ids[1,28]
[[1, 28],[1, 28]]
[start_idx,end_idx]
["6000 hours"]

Click an operation to see what it does. A highlighted dimension is one that just changed.

From tokens to an answer: two logit vectors, two argmaxes, one span.

In practice you use the question-answering pipeline, which softmaxes the logits into a score, can return the topk answers, and — with handle_impossible_answer=True — maps “no answer” to a high score on the [CLS] token:

pipe = pipeline("question-answering", model=model, tokenizer=tokenizer)
pipe(question=question, context=context, topk=3)
# [{'score': 0.27, 'answer': '6000 hours'}, ...]

Note — argmax isn’t quite enough

Independently taking the argmax of start and end can pick tokens in the question, or an end before the start. The real pipeline searches for the best valid (start ≤ end, in-context) combination.

Dealing with long passages

A review can have more tokens than the model’s 512-token limit. Truncating (fine for classification, where the [CLS] token summarizes) is dangerous for QA — the answer might sit near the end and get cut off. The fix is a sliding window (return_overflowing_tokens=True, sized by max_seq_length, overlapped by doc_stride), which turns one long example into several overlapping (question, context-window) pairs:

context token 0green = answer span170
win #0
91 tok
win #1
91 tok
win #2
38 tok

3 windows of ≤ 100 tokens (6 question + 91 context + 3 special), overlapping by 25. The answer is captured by 2 of them — with too small a window or too little overlap it can fall between windows and be lost.

Sliding window over a 170-token review. Drag the sliders: shrink the window or the overlap and watch the answer (green) risk falling between windows.
tokenized_example = tokenizer(example["question"], example["context"],
                              return_overflowing_tokens=True, max_length=100, stride=25)
for i, w in enumerate(tokenized_example["input_ids"]):
    print(f"Window #{i} has {len(w)} tokens")
# Window #0 has 100 tokens
# Window #1 has 88 tokens

With answer extraction understood, the next step is retrieving the right reviews to read in the first place — the retriever-reader pipeline.

Check yourself: extracting answers

1.How is extractive QA framed as a prediction problem?

2.How does the tokenizer format a (question, context) pair for QA?

3.For 'How much music can this hold?', the start logits peak on '1' and '6000' and the end logits on 'minute' and 'hours'. What span does argmax give?

4.Why is simply truncating a long context dangerous for QA (but fine for classification)?

5.How does the sliding window handle contexts longer than the model's limit?

6.Why start from a model already fine-tuned on SQuAD rather than training the QA head from scratch?

6 questions