Lab 01 — The pipeline() Abstraction: Task → Tokenizer + Model + Post-Processing

Phase 30 · Lab 01 · Phase README · Warmup

The problem

pipeline("sentiment-analysis")("I love this") returning [{'label': 'POSITIVE', 'score': 0.999}] is the first thing everyone runs in Hugging Face, and it looks like magic. It is not. Every transformers pipeline is the same fixed three-stage assembly line:

raw input  --preprocess-->  token ids  --forward-->  logits  --postprocess-->  task-shaped output

What changes per task is which pieces get bolted on: a task-specific pre-processor (the tokenizer plus how inputs are batched/truncated), the model forward (raw logits), and — the part that actually differs the most — a task-specific post-processor that turns those logits into the human-facing shape. Text-classification softmaxes label logits into a {label, score}; token-classification argmaxes per-token logits and aggregates adjacent tokens into entity spans; text-generation runs an autoregressive decode loop. pipeline() itself is a factory: hand it a task string, it picks the right Pipeline subclass and post-processor, injects the tokenizer and model, and returns you a callable.

You build that machinery. Once you can see the seams, pipeline() stops being magic and becomes "the obvious composition," which is exactly the level of understanding that lets you drop down to AutoModel/AutoTokenizer when a pipeline doesn't fit your use case.

What you build

PieceWhat it doesThe lesson
softmax / argmaxnumerically stable softmax (max-subtraction) + deterministic argmaxlogits → probabilities is a two-line primitive, not a framework feature
SimpleTokenizerword-level encode_plusinput_ids + tokens + offset_mapping + attention_mask, plus decodethe tokenizer is the pre-processor; offsets are what map tokens back to characters
Pipeline basethe preprocess → forward → postprocess shape + the batch-vs-single __call__ contractevery task shares one skeleton; only the post-processor changes
TextClassificationPipelinesoftmax over label logits → top {label, score} (or all/top_k)classification is softmax + argmax + a label map
TokenClassificationPipelineper-token argmax → adjacent same-label tokens merged into entity spans with char offsetsNER's real work is aggregation, not the per-token labels
TextGenerationPipelinegreedy autoregressive decode loop until EOS / max_new_tokensgeneration is a loop over forward, not a single call
pipeline(task, model=...)the factory that resolves aliases and wires the right subclassone entry point, task-dispatched — the thing you actually call

File map

FileRole
lab.pyyour implementation (fill the # TODOs)
solution.pyreference implementation + worked example (python solution.py)
test_lab.py31 tests: each task's output shape/labels, softmax sums to 1, batching == mapping, truncation, aggregation, EOS/length stops, factory dispatch, unknown-task error
requirements.txtpytest only

Run it

pytest test_lab.py -v                       # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v   # the reference (green)
python solution.py                          # worked example

Success criteria

  • softmax sums to 1 and does not overflow on large logits ([1000, 1001, 1002]).
  • text-classification returns the argmax label with its softmax score; return_all_scores gives a ranked distribution that sums to 1.
  • token-classification merges adjacent same-label tokens into one span whose start/end offsets slice the original text back to the entity word.
  • text-generation picks the greedy argmax each step and stops at both EOS and max_new_tokens.
  • pipe([a, b]) == [pipe(a), pipe(b)] — batching is deterministic mapping.
  • truncation=True, max_length=k makes the model see only the first k tokens.
  • pipeline("sentiment-analysis") and pipeline("ner") resolve their aliases; an unknown task raises KeyError.
  • All 31 tests pass under both lab and solution.

How this maps to the real stack

  • transformers.pipeline(task, model=..., tokenizer=...) is exactly this factory. Real HF has ~two dozen tasks (text-classification, token-classification, question-answering, summarization, translation, fill-mask, text-generation, automatic-speech-recognition, image-classification, zero-shot-classification, …); each maps to a Pipeline subclass with its own preprocess/_forward/postprocess. The aliases are real too — "sentiment-analysis" is text-classification, "ner" is token-classification.
  • The preprocess → _forward → postprocess contract is the real base-class shape in transformers/pipelines/base.py. Real pipelines add device placement, torch.no_grad(), and framework (PT/TF) dispatch inside _forward; the shape is identical to ours.
  • text-classification postprocess really is a softmax over the model's logits mapped through model.config.id2label, with top_k/return_all_scores/function_to_apply controlling the output — the same knobs we expose.
  • token-classification aggregation (aggregation_strategy = none/simple/first/ average/max) is the real mechanism for turning per-subtoken labels into human entity spans; our word-level "simple" strategy is the same idea (merge adjacent same-label, average scores) minus the B-/I- prefix and subword-merge logic real NER needs.
  • text-generation postprocess wraps model.generate(), which is the decode loop — our greedy loop is the simplest case. Lab 02 builds the full GenerationConfig (temperature, top-k, top-p, repetition penalty, stopping) that lives inside generate(); this lab keeps generation to a greedy loop on purpose, to isolate the pipeline composition lesson.
  • The offset mapping (return_offsets_mapping=True) is a real fast-tokenizer feature and the exact mechanism NER and QA pipelines use to map token predictions back to character spans in the original string.

Limits of the miniature. Real tokenizers are subword (BPE/WordPiece/Unigram), so one word can become several tokens and NER has to merge subwords before merging entities; ours is word-level so offsets align 1:1 and the aggregation lesson stays clean (subwords are Lab 02). The "model" is a pure forward(input_ids) -> logits function instead of a real transformer — the actual attention math is the sibling Senior AI Engineer track's job; here the model is deliberately a stub so the ecosystem workflow is the whole lesson. Batching is map over single calls rather than a real padded batched tensor forward (that is Lab 02's attention_mask).

Extensions (your own machine)

  • Add a fill-mask pipeline: find the [MASK] position, softmax the model's vocab logits there, return the top-k tokens — a fourth task on the same skeleton.
  • Add zero-shot-classification: run the model as an NLI entailment scorer over hypothesis = f"This example is {label}." for each candidate label, softmax the entailment logits — proving one model backs a "new" task purely via post-processing.
  • Add a real subword tokenizer (borrow Lab 02's BPE) and extend NER aggregation to merge subwords (## continuation) before merging entities.
  • Swap the FakeModel for a real transformers AutoModelForSequenceClassification on your own machine and confirm your postprocess produces the same labels HF's pipeline does.

Interview / resume signal

"Built a miniature Hugging Face pipeline(): the preprocess → forward → postprocess assembly line with a task-dispatching factory, implementing text-classification (softmax + id2label), token-classification/NER (per-token argmax aggregated into offset-anchored entity spans), and greedy text-generation — proving batching is deterministic mapping, truncation bounds what the model sees, and the post-processor is the part that actually differs per task."