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
| Piece | What it does | The lesson |
|---|---|---|
softmax / argmax | numerically stable softmax (max-subtraction) + deterministic argmax | logits → probabilities is a two-line primitive, not a framework feature |
SimpleTokenizer | word-level encode_plus → input_ids + tokens + offset_mapping + attention_mask, plus decode | the tokenizer is the pre-processor; offsets are what map tokens back to characters |
Pipeline base | the preprocess → forward → postprocess shape + the batch-vs-single __call__ contract | every task shares one skeleton; only the post-processor changes |
TextClassificationPipeline | softmax over label logits → top {label, score} (or all/top_k) | classification is softmax + argmax + a label map |
TokenClassificationPipeline | per-token argmax → adjacent same-label tokens merged into entity spans with char offsets | NER's real work is aggregation, not the per-token labels |
TextGenerationPipeline | greedy autoregressive decode loop until EOS / max_new_tokens | generation is a loop over forward, not a single call |
pipeline(task, model=...) | the factory that resolves aliases and wires the right subclass | one entry point, task-dispatched — the thing you actually call |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python solution.py) |
test_lab.py | 31 tests: each task's output shape/labels, softmax sums to 1, batching == mapping, truncation, aggregation, EOS/length stops, factory dispatch, unknown-task error |
requirements.txt | pytest 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
-
softmaxsums to 1 and does not overflow on large logits ([1000, 1001, 1002]). -
text-classificationreturns the argmax label with its softmax score;return_all_scoresgives a ranked distribution that sums to 1. -
token-classificationmerges adjacent same-label tokens into one span whosestart/endoffsets slice the original text back to the entity word. -
text-generationpicks the greedy argmax each step and stops at both EOS andmax_new_tokens. -
pipe([a, b]) == [pipe(a), pipe(b)]— batching is deterministic mapping. -
truncation=True, max_length=kmakes the model see only the firstktokens. -
pipeline("sentiment-analysis")andpipeline("ner")resolve their aliases; an unknown task raisesKeyError. -
All 31 tests pass under both
labandsolution.
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 aPipelinesubclass with its ownpreprocess/_forward/postprocess. The aliases are real too —"sentiment-analysis"istext-classification,"ner"istoken-classification.- The
preprocess → _forward → postprocesscontract is the real base-class shape intransformers/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-classificationpostprocess really is a softmax over the model's logits mapped throughmodel.config.id2label, withtop_k/return_all_scores/function_to_applycontrolling the output — the same knobs we expose.token-classificationaggregation (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-generationpostprocess wrapsmodel.generate(), which is the decode loop — our greedy loop is the simplest case. Lab 02 builds the fullGenerationConfig(temperature, top-k, top-p, repetition penalty, stopping) that lives insidegenerate(); 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-maskpipeline: 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 overhypothesis = 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
FakeModelfor a realtransformersAutoModelForSequenceClassificationon your own machine and confirm yourpostprocessproduces the same labels HF's pipeline does.
Interview / resume signal
"Built a miniature Hugging Face
pipeline(): thepreprocess → forward → postprocessassembly 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."