LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 158 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/23
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BMixed Precision TrainingModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Production Agent
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/16
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight InterpolationPrompt Optimization with DSPyRecursive Language Models (RLM)
🤖Advanced Agents & Retrieval0/14
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryMulti-Agent Orchestration
⚡Inference & Production Scale0/20
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementContext EngineeringMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
Back to Topics
LearnPortfolio CapstonesCapstone: Image Damage Classifier
👁️HardMultimodal Models

Capstone: Image Damage Classifier

Ship a damaged-package photo triage service with quality checks, slice evaluation, serving bundles, and review monitoring.

17 min read
Learning path
Step 82 of 158 in the full curriculum
Capstone: Demand ForecastingCapstone: Production ML Pipeline

Shipment rows, ranked items, and warehouse time series all fit into tables. A customer return adds a new input type: a photo of a package that may be crushed, torn, blurred, dark, or unrelated to the order.

Earlier, you traced a convolutional neural network (CNN) over a damaged-package image patch. This capstone turns that spatial reasoning into a product: an image triage endpoint that flags likely visible damage, rejects unusable photos, preserves evidence for human review, and never turns an uncertain image score directly into a refund.

Quality-first routing evidence for seven damaged-package test photos. A brightness-versus-blur chart marks the usable region at brightness at least 0.20 and blur at most 0.45. R-406-a has a high damage score of 0.94 but is too dark, and R-409-a scores 0.81 but has no visible package, so both request a new photo. Usable R-404-a scores 0.83 and enters priority review. Four guideline-matched specialist outcomes form a case-level confusion matrix with one true positive, one false positive, one false negative, and one true negative, giving precision and recall of 0.50. Customer-phone recapture rate is 1 of 4, warehouse-camera rate is 1 of 3, all 19 gates pass, and damage-cnn-v1 advances only to specialist shadow review with damage-cnn-v0 as rollback. Quality-first routing evidence for seven damaged-package test photos. A brightness-versus-blur chart marks the usable region at brightness at least 0.20 and blur at most 0.45. R-406-a has a high damage score of 0.94 but is too dark, and R-409-a scores 0.81 but has no visible package, so both request a new photo. Usable R-404-a scores 0.83 and enters priority review. Four guideline-matched specialist outcomes form a case-level confusion matrix with one true positive, one false positive, one false negative, and one true negative, giving precision and recall of 0.50. Customer-phone recapture rate is 1 of 4, warehouse-camera rate is 1 of 3, all 19 gates pass, and damage-cnn-v1 advances only to specialist shadow review with damage-cnn-v0 as rollback.
High damage scores don't override dark or missing-package evidence; only quality-passing photos enter case-level review metrics and a versioned shadow receipt.

Define the photo decision first

ShopFlow receives return photos from customers and warehouse intake stations. The useful product question isn't "does the model recognize every defect?" It's: which photo should a specialist inspect first, and when is the photo too weak to support any decision?

Use three operational outcomes:

ActionEvidenceProduct behavior
request_new_photoimage is too blurred, dark, or incompleteask for a clearer upload before assessing damage
normal_reviewusable image, low damage scorekeep ordinary return workflow
priority_damage_reviewusable image, high damage scoresurface to specialist with photo and score trace

The classifier isn't a refund policy. Product eligibility still depends on order ownership, item type, return window, and specialist judgment. This separation prevents a shadow or reflection in a photo from issuing a costly action.

A model card should state the intended use, input constraints, decision threshold, evaluated slices, and known failure cases. Model cards were proposed as structured reports for exactly this type of deployed-model context: users need more than a metric without its operating conditions.[1]Reference 1Model Cards for Model Reportinghttps://arxiv.org/abs/1810.03993

Diagram showing Photo manifest shipment groups + time, Quality check visible + blur + light, Damage scorer versioned threshold, and Immutable route trace policy + thresholds. Diagram showing Photo manifest shipment groups + time, Quality check visible + blur + light, Damage scorer versioned threshold, and Immutable route trace policy + thresholds.
Photo manifest shipment groups + time, Quality check visible + blur + light, Damage scorer versioned threshold, and Immutable route trace policy + thresholds.

Build a dataset that can't leak

For tabular models, leakage may be a future delivery timestamp. For photos, leakage often hides in nearly identical pixels. A customer may upload three bursts of the same crushed box. A warehouse may photograph one parcel from four angles. If related images land in both train and test sets, the model can memorize one package rather than generalize to new damage.

Your manifest should contain:

FieldWhy it matters
case_id and capture_daygroup all photos for one physical package and preserve time ordering
sourceseparate customer phone uploads from warehouse inspection cameras
quality_labeldistinguish unusable evidence from visible damage
damage_labelrecord specialist-confirmed visible damage only on usable photos
splithold out later shipments, never random photos from the same case
reviewer_id and guideline_versionaudit disagreement or changed label definitions

Evaluate at least daylight versus dark uploads, customer versus warehouse source, packaging type, and visible-defect size. A global score can hide the exact failure that matters: small tears disappearing in dark phone images.

Use the CNN learned earlier as a baseline, then fine-tune a pretrained image encoder only if you record its preprocessing and measure it under the same split. A later deep-dive explains Vision Transformer image encoders; this capstone doesn't require that architecture.[2]Reference 2An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale.https://arxiv.org/abs/2010.11929

Freeze a grouped photo manifest

Start with the dataset boundary. Multiple photos from one physical package belong in one split even when filenames differ. Keep time direction intact too: later return cases should test a model trained on earlier cases.

The local manifest below is small enough to inspect line by line. This historical evaluation fixture contains frozen specialist labels, reviewer IDs, and guideline versions for dataset audit. Damage labels appear only when a specialist judged the image usable. Unusable photos keep confirmed_damage=None because poor evidence shouldn't become supervision. Candidate scores arrive in a separate object, so route code can't quietly read labels as model output.

01-freeze-grouped-photo-manifest.py
1from collections import Counter, defaultdict 2from dataclasses import dataclass 3import json 4 5@dataclass(frozen=True) 6class PhotoManifestRow: 7 case_id: str 8 photo_id: str 9 capture_day: int 10 split: str 11 source: str 12 packaging: str 13 quality_label: str 14 confirmed_damage: bool | None 15 reviewer_id: str 16 guideline_version: str 17 18PHOTOS = [ 19 PhotoManifestRow("R-401", "R-401-a", 1, "train", "customer_phone", "corrugated", "usable", True, "S-12", "visible-damage-v1"), 20 PhotoManifestRow("R-401", "R-401-b", 2, "train", "customer_phone", "corrugated", "usable", True, "S-12", "visible-damage-v1"), 21 PhotoManifestRow("R-402", "R-402-a", 3, "validation", "customer_phone", "corrugated", "unusable", None, "S-08", "visible-damage-v1"), 22 PhotoManifestRow("R-403", "R-403-a", 4, "validation", "warehouse_camera", "mailer", "usable", False, "S-08", "visible-damage-v1"), 23 PhotoManifestRow("R-404", "R-404-a", 5, "test", "customer_phone", "mailer", "usable", True, "S-12", "visible-damage-v1"), 24 PhotoManifestRow("R-404", "R-404-b", 6, "test", "customer_phone", "mailer", "usable", True, "S-12", "visible-damage-v1"), 25 PhotoManifestRow("R-405", "R-405-a", 7, "test", "warehouse_camera", "corrugated", "usable", False, "S-08", "visible-damage-v1"), 26 PhotoManifestRow("R-406", "R-406-a", 8, "test", "customer_phone", "corrugated", "unusable", None, "S-12", "visible-damage-v1"), 27 PhotoManifestRow("R-407", "R-407-a", 9, "test", "customer_phone", "corrugated", "usable", False, "S-12", "visible-damage-v1"), 28 PhotoManifestRow("R-408", "R-408-a", 10, "test", "warehouse_camera", "corrugated", "usable", True, "S-08", "visible-damage-v1"), 29 PhotoManifestRow("R-409", "R-409-a", 11, "test", "warehouse_camera", "corrugated", "unusable", None, "S-08", "visible-damage-v1"), 30] 31 32print("photos:", len(PHOTOS)) 33print("cases:", len({photo.case_id for photo in PHOTOS}))
Output
1photos: 11 2cases: 9
02-audit-grouped-split-manifest.py
1splits_by_case = defaultdict(set) 2for photo in PHOTOS: 3 splits_by_case[photo.case_id].add(photo.split) 4 5split_days = { 6 split: [photo.capture_day for photo in PHOTOS if photo.split == split] 7 for split in ("train", "validation", "test") 8} 9manifest_checks = { 10 "case_groups_do_not_cross_splits": all(len(splits) == 1 for splits in splits_by_case.values()), 11 "time_ordered_splits": ( 12 all(split_days.values()) 13 and max(split_days["train"]) < min(split_days["validation"]) 14 and max(split_days["validation"]) < min(split_days["test"]) 15 ), 16 "usable_labels_complete": all(photo.confirmed_damage is not None for photo in PHOTOS if photo.quality_label == "usable"), 17 "unusable_labels_abstain": all(photo.confirmed_damage is None for photo in PHOTOS if photo.quality_label == "unusable"), 18 "label_lineage_recorded": all(photo.reviewer_id and photo.guideline_version for photo in PHOTOS), 19} 20 21print("split photo counts:", dict(Counter(photo.split for photo in PHOTOS))) 22print("R-401 splits:", sorted(splits_by_case["R-401"])) 23print(json.dumps(manifest_checks, indent=2))
Output
1split photo counts: {'train': 2, 'validation': 2, 'test': 7} 2R-401 splits: ['train'] 3{ 4 "case_groups_do_not_cross_splits": true, 5 "time_ordered_splits": true, 6 "usable_labels_complete": true, 7 "unusable_labels_abstain": true, 8 "label_lineage_recorded": true 9}

The fixture uses explicit splits so the invariant stays visible. A larger pipeline can use a group-aware splitter, then freeze and audit the resulting manifest. The important claim isn't that one splitter solves every dataset: no physical case may cross evaluation boundaries, and later cases remain later.

Encode quality-first review traces

The model endpoint should receive a preprocessing result and a damage score, then choose a review route. Quality checks run before the damage threshold. Otherwise a confidently scored blur, dark frame, or unrelated object can create an unsupported escalation.

The score fixture below is separate from the manifest. Labels stay available for offline evaluation, but the router never reads them. A production system may run its cheap quality checks before an expensive damage model; this compact fixture keeps both outputs so you can test that an unsupported high damage score still abstains.

03-load-model-output-bundle.py
1@dataclass(frozen=True) 2class ModelOutput: 3 damage_probability: float 4 blur_score: float 5 brightness: float 6 box_visible: bool 7 8MODEL_OUTPUTS = { 9 "R-401-a": ModelOutput(0.91, 0.12, 0.66, True), 10 "R-401-b": ModelOutput(0.88, 0.10, 0.70, True), 11 "R-402-a": ModelOutput(0.93, 0.71, 0.51, True), 12 "R-403-a": ModelOutput(0.18, 0.08, 0.75, True), 13 "R-404-a": ModelOutput(0.83, 0.10, 0.64, True), 14 "R-404-b": ModelOutput(0.79, 0.14, 0.61, True), 15 "R-405-a": ModelOutput(0.32, 0.11, 0.68, True), 16 "R-406-a": ModelOutput(0.94, 0.12, 0.12, True), 17 "R-407-a": ModelOutput(0.73, 0.09, 0.65, True), 18 "R-408-a": ModelOutput(0.63, 0.07, 0.72, True), 19 "R-409-a": ModelOutput(0.81, 0.08, 0.74, False), 20} 21 22BUNDLE = { 23 "bundle_id": "damage-cnn-v1", 24 "previous_bundle": "damage-cnn-v0", 25 "preprocessing": "parcel-rgb-224-center-crop-v1", 26 "label_guideline": "visible-damage-v1", 27 "route_policy": "quality-first-review-v1", 28 "damage_threshold": 0.70, 29 "max_blur": 0.45, 30 "min_brightness": 0.20, 31} 32 33print("bundle:", BUNDLE["bundle_id"], "threshold=", BUNDLE["damage_threshold"])
04-quality-first-route.py
1def route(photo: PhotoManifestRow, model_output: ModelOutput) -> dict[str, object]: 2 if not model_output.box_visible: 3 action, reason = "request_new_photo", "package_not_visible" 4 elif model_output.blur_score > BUNDLE["max_blur"] or model_output.brightness < BUNDLE["min_brightness"]: 5 action, reason = "request_new_photo", "image_quality_gate" 6 elif model_output.damage_probability >= BUNDLE["damage_threshold"]: 7 action, reason = "priority_damage_review", "damage_threshold" 8 else: 9 action, reason = "normal_review", "below_threshold" 10 11 return { 12 "route_id": f"{BUNDLE['bundle_id']}:{photo.photo_id}", 13 "photo_id": photo.photo_id, 14 "case_id": photo.case_id, 15 "routed_day": photo.capture_day, 16 "source": photo.source, 17 "packaging": photo.packaging, 18 "bundle_id": BUNDLE["bundle_id"], 19 "previous_bundle": BUNDLE["previous_bundle"], 20 "preprocessing": BUNDLE["preprocessing"], 21 "label_guideline": BUNDLE["label_guideline"], 22 "route_policy": BUNDLE["route_policy"], 23 "damage_threshold": BUNDLE["damage_threshold"], 24 "max_blur": BUNDLE["max_blur"], 25 "min_brightness": BUNDLE["min_brightness"], 26 "damage_probability": model_output.damage_probability, 27 "blur_score": model_output.blur_score, 28 "brightness": model_output.brightness, 29 "box_visible": model_output.box_visible, 30 "action": action, 31 "reason": reason, 32 } 33 34print("route keys:", len(route(PHOTOS[0], MODEL_OUTPUTS["R-401-a"])))
05-route-test-photos.py
1test_photos = [photo for photo in PHOTOS if photo.split == "test"] 2photo_by_id = {photo.photo_id: photo for photo in PHOTOS} 3traces = [route(photo, MODEL_OUTPUTS[photo.photo_id]) for photo in test_photos] 4trace_by_photo = {trace["photo_id"]: trace for trace in traces} 5unsafe_priority_routes = [ 6 trace["photo_id"] 7 for photo, trace in zip(test_photos, traces) 8 if trace["action"] == "priority_damage_review" and photo.quality_label != "usable" 9] 10 11for photo_id in ("R-404-a", "R-406-a", "R-409-a"): 12 trace = trace_by_photo[photo_id] 13 print(photo_id, trace["action"], trace["reason"], f"score={trace['damage_probability']}") 14print("route counts:", dict(Counter(trace["action"] for trace in traces))) 15print("unsafe priority routes:", unsafe_priority_routes)
Output
1R-404-a priority_damage_review damage_threshold score=0.83 2R-406-a request_new_photo image_quality_gate score=0.94 3R-409-a request_new_photo package_not_visible score=0.81 4route counts: {'priority_damage_review': 3, 'normal_review': 2, 'request_new_photo': 2} 5unsafe priority routes: []

Cases R-406-a and R-409-a are the important failure tests. Both have high damage scores. Neither score counts as usable evidence because quality checks fail first. The endpoint asks for another photo instead of escalating an unsupported claim.

Package the Vision Service

Submit an inspectable repository, not a notebook screenshot:

text
1damage-vision-service/ 2 data/ 3 label_guidelines.md 4 photo_manifest.parquet 5 split_manifest.json 6 model/ 7 train_cnn_baseline.py 8 evaluate_slices.py 9 model_card.md 10 service/ 11 preprocess.py 12 route_review.py 13 trace_schema.json 14 monitoring/ 15 input_quality_report.py 16 delayed_review_outcomes.py 17 specialist_shadow_receipt.py 18 tests/ 19 test_case_groups_do_not_cross_splits.py 20 test_blurry_photo_never_escalates.py 21 test_later_outcomes_join_routes.py 22 test_empty_review_window_holds.py 23 test_route_trace_is_versioned.py 24 test_previous_bundle_required.py

The serving bundle must pin image resize and crop behavior, color normalization, model weights, label version, route policy, damage threshold, quality-gate thresholds, and previous-bundle pointer. A change from center crop to full-frame resize may change whether a torn corner remains visible; it's a model behavior change even when weights remain constant.

The routing cells emit that trace shape directly. They let a reviewer reconstruct the route:

Response fieldExample
bundle and preprocessingdamage-cnn-v1, parcel-rgb-224-center-crop-v1
rollback and label contractdamage-cnn-v0, visible-damage-v1
quality valuesblur 0.12, brightness 0.66, box visible true
score and action policydamage 0.91, threshold 0.70, blur maximum 0.45, brightness minimum 0.20
routepriority_damage_review
human outcome laterconfirmed_damage or not_supported

Join delayed outcomes before promotion

Photo models drift when the image source changes. A new warehouse camera, winter lighting, a mobile upload compressor, or new packaging graphics can alter pixels before a confirmed-damage label exists.

Separate immediate checks from delayed quality:

WindowMonitorTrigger
immediateunreadable image rate, brightness, blur, missing package, latencyinvestigate capture path or fail to manual intake
delayedspecialist-confirmed precision, missed visible damage, route rate by source and packaginghold promotion or create retraining candidate
safety reviewunsupported escalations, policy actions attempted without specialist approvalrollback and audit workflow

Google Cloud's MLOps guidance treats serving, monitoring, validation, metadata, and continuous training as connected stages rather than a one-time deploy step.[3]Reference 3MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning Apply that same discipline here: a change in image quality creates an investigation or candidate run, never an automatic production replacement.

The final local receipt appends specialist outcomes only after routing, then joins each outcome to a physical case. Each delayed outcome also carries its reviewer and label guideline, so precision and recall can't silently mix definitions. It measures review precision and recall once per usable test case, records immediate request rates by source, proves unusable photos abstain, and keeps the previous bundle beside the candidate. When one package has multiple usable photos, the case escalates if any usable photo crosses the priority threshold. These tiny counts teach the contract; production promotion still needs larger slices and shadow traffic.

06-append-specialist-outcomes.py
1@dataclass(frozen=True) 2class SpecialistOutcome: 3 case_id: str 4 reviewed_day: int 5 confirmed_damage: bool 6 reviewer_id: str 7 guideline_version: str 8 9SPECIALIST_OUTCOMES = [ 10 SpecialistOutcome("R-404", 12, True, "S-12", "visible-damage-v1"), 11 SpecialistOutcome("R-405", 12, False, "S-08", "visible-damage-v1"), 12 SpecialistOutcome("R-407", 12, False, "S-12", "visible-damage-v1"), 13 SpecialistOutcome("R-408", 12, True, "S-08", "visible-damage-v1"), 14] 15 16usable_test_case_ids = { 17 photo.case_id 18 for photo in test_photos 19 if photo.quality_label == "usable" 20} 21traces_by_case = defaultdict(list) 22for trace in traces: 23 traces_by_case[trace["case_id"]].append(trace) 24 25print("usable test cases:", sorted(usable_test_case_ids)) 26print("specialist outcomes:", len(SPECIALIST_OUTCOMES))
07-aggregate-case-actions.py
1def aggregate_usable_case_action(case_id: str) -> str: 2 usable_traces = [ 3 trace 4 for trace in traces_by_case[case_id] 5 if photo_by_id[trace["photo_id"]].quality_label == "usable" 6 ] 7 if not usable_traces: 8 raise ValueError(f"no usable traces for {case_id}") 9 if any(trace["action"] == "priority_damage_review" for trace in usable_traces): 10 return "priority_damage_review" 11 return "normal_review" 12 13case_actions = { 14 case_id: aggregate_usable_case_action(case_id) 15 for case_id in sorted(usable_test_case_ids) 16} 17outcome_by_case = {outcome.case_id: outcome for outcome in SPECIALIST_OUTCOMES} 18joined_outcomes = [ 19 (case_id, case_actions[case_id], outcome_by_case[case_id].confirmed_damage) 20 for case_id in sorted(case_actions.keys() & outcome_by_case.keys()) 21] 22 23print("case_actions:", case_actions) 24print("joined usable cases:", len(joined_outcomes))
08-measure-delayed-quality.py
1def rate_or_none(numerator: int, denominator: int) -> float | None: 2 return round(numerator / denominator, 3) if denominator else None 3 4true_positives = sum( 5 action == "priority_damage_review" and confirmed_damage 6 for _, action, confirmed_damage in joined_outcomes 7) 8false_positives = sum( 9 action == "priority_damage_review" and not confirmed_damage 10 for _, action, confirmed_damage in joined_outcomes 11) 12false_negatives = sum( 13 action != "priority_damage_review" and confirmed_damage 14 for _, action, confirmed_damage in joined_outcomes 15) 16delayed_quality = { 17 "priority_precision": rate_or_none(true_positives, true_positives + false_positives), 18 "priority_recall": rate_or_none(true_positives, true_positives + false_negatives), 19 "reviewed_usable_cases": len(joined_outcomes), 20} 21 22source_slices = { 23 source: { 24 "photos": len(rows), 25 "request_new_photo_rate": round( 26 sum(trace["action"] == "request_new_photo" for trace in rows) / len(rows), 27 3, 28 ), 29 } 30 for source in sorted({trace["source"] for trace in traces}) 31 for rows in [[trace for trace in traces if trace["source"] == source]] 32} 33 34print("delayed_quality:", delayed_quality) 35print("source_slices:", source_slices)
09-release-gate-checklist.py
1required_trace_fields = { 2 "route_id", "photo_id", "case_id", "routed_day", "source", "packaging", 3 "bundle_id", "previous_bundle", "preprocessing", "label_guideline", 4 "route_policy", "damage_threshold", "max_blur", "min_brightness", 5 "damage_probability", "blur_score", "brightness", "box_visible", 6 "action", "reason", 7} 8outcome_case_ids = [outcome.case_id for outcome in SPECIALIST_OUTCOMES] 9release_gates = { 10 **manifest_checks, 11 "route_ids_unique": len({trace["route_id"] for trace in traces}) == len(traces), 12 "all_test_photos_routed": len(traces) == len(test_photos), 13 "unusable_photos_abstain": all( 14 trace_by_photo[photo.photo_id]["action"] == "request_new_photo" 15 for photo in test_photos 16 if photo.quality_label == "unusable" 17 ), 18 "unsafe_priority_routes_absent": not unsafe_priority_routes, 19 "route_traces_replayable": all(required_trace_fields <= trace.keys() for trace in traces), 20 "specialist_outcome_case_ids_unique": len(set(outcome_case_ids)) == len(outcome_case_ids), 21 "specialist_outcomes_join_usable_test_cases": set(outcome_case_ids) <= usable_test_case_ids, 22 "usable_test_cases_have_specialist_outcomes": usable_test_case_ids <= set(outcome_case_ids), 23 "specialist_outcomes_arrive_after_capture": all( 24 outcome.reviewed_day > max( 25 photo.capture_day for photo in test_photos if photo.case_id == outcome.case_id 26 ) 27 for outcome in SPECIALIST_OUTCOMES 28 if outcome.case_id in usable_test_case_ids 29 ), 30 "specialist_outcome_label_contract_matches_bundle": all( 31 outcome.reviewer_id 32 and outcome.guideline_version == BUNDLE["label_guideline"] 33 for outcome in SPECIALIST_OUTCOMES 34 ), 35 "priority_precision_evidence_at_least_0_50": ( 36 delayed_quality["priority_precision"] is not None 37 and delayed_quality["priority_precision"] >= 0.50 38 ), 39 "priority_recall_evidence_at_least_0_50": ( 40 delayed_quality["priority_recall"] is not None 41 and delayed_quality["priority_recall"] >= 0.50 42 ), 43 "source_slices_recorded": set(source_slices) == {"customer_phone", "warehouse_camera"}, 44 "rollback_pointer_recorded": bool(BUNDLE["previous_bundle"]), 45} 46 47print("release_gates_pass:", all(release_gates.values()))
10-publish-specialist-shadow-receipt.py
1receipt = { 2 "candidate_bundle": BUNDLE["bundle_id"], 3 "previous_bundle": BUNDLE["previous_bundle"], 4 "preprocessing": BUNDLE["preprocessing"], 5 "label_guideline": BUNDLE["label_guideline"], 6 "route_policy": BUNDLE["route_policy"], 7 "route_traces": len(traces), 8 "later_specialist_outcomes": len(SPECIALIST_OUTCOMES), 9 "joined_usable_cases": len(joined_outcomes), 10 "case_actions": case_actions, 11 "delayed_quality": delayed_quality, 12 "source_slices": source_slices, 13 "release_gates": release_gates, 14 "candidate_decision": "candidate_for_specialist_shadow_review" if all(release_gates.values()) else "hold", 15} 16 17print(json.dumps(receipt, indent=2))
Output
1{ 2 "candidate_bundle": "damage-cnn-v1", 3 "previous_bundle": "damage-cnn-v0", 4 "preprocessing": "parcel-rgb-224-center-crop-v1", 5 "label_guideline": "visible-damage-v1", 6 "route_policy": "quality-first-review-v1", 7 "route_traces": 7, 8 "later_specialist_outcomes": 4, 9 "joined_usable_cases": 4, 10 "case_actions": { 11 "R-404": "priority_damage_review", 12 "R-405": "normal_review", 13 "R-407": "priority_damage_review", 14 "R-408": "normal_review" 15 }, 16 "delayed_quality": { 17 "priority_precision": 0.5, 18 "priority_recall": 0.5, 19 "reviewed_usable_cases": 4 20 }, 21 "source_slices": { 22 "customer_phone": { 23 "photos": 4, 24 "request_new_photo_rate": 0.25 25 }, 26 "warehouse_camera": { 27 "photos": 3, 28 "request_new_photo_rate": 0.333 29 } 30 }, 31 "release_gates": { 32 "case_groups_do_not_cross_splits": true, 33 "time_ordered_splits": true, 34 "usable_labels_complete": true, 35 "unusable_labels_abstain": true, 36 "label_lineage_recorded": true, 37 "route_ids_unique": true, 38 "all_test_photos_routed": true, 39 "unusable_photos_abstain": true, 40 "unsafe_priority_routes_absent": true, 41 "route_traces_replayable": true, 42 "specialist_outcome_case_ids_unique": true, 43 "specialist_outcomes_join_usable_test_cases": true, 44 "usable_test_cases_have_specialist_outcomes": true, 45 "specialist_outcomes_arrive_after_capture": true, 46 "specialist_outcome_label_contract_matches_bundle": true, 47 "priority_precision_evidence_at_least_0_50": true, 48 "priority_recall_evidence_at_least_0_50": true, 49 "source_slices_recorded": true, 50 "rollback_pointer_recorded": true 51 }, 52 "candidate_decision": "candidate_for_specialist_shadow_review" 53}

candidate_for_specialist_shadow_review is narrower than launch approval. The receipt says this frozen bundle deserves comparison beside current production routing. It doesn't claim that four usable local cases prove every camera, lighting condition, package type, or damage shape.

Practice: break the vision contract

Use the runnable examples as a release harness. Change one condition at a time, predict the failure, then rerun the examples.

  1. Change R-401-b split from train to validation. Which dataset gate fails?
  2. Change R-406-a brightness in MODEL_OUTPUTS from 0.12 to 0.30. Why does this expose a quality-detector failure rather than prove the escalation is safe?
  3. Remove damage_threshold from the response trace. Which receipt gate fails?
  4. Change R-408-a damage probability in MODEL_OUTPUTS from 0.63 to 0.75. Which delayed metrics improve?
  5. Set BUNDLE["previous_bundle"] = "". Why is shadow evidence no longer promotion-ready?
  6. Replace SPECIALIST_OUTCOMES with an empty list. Why do evidence gates fail instead of crashing or passing?
  7. Add SpecialistOutcome("R-999", 12, True, "S-12", "visible-damage-v1"). Which join gate fails?
  8. Change R-408 specialist outcome to guideline visible-damage-v2. Which provenance gate fails?

Practice answer sketches

Mastery check

Evaluation rubric

ArtifactStrong submission demonstrates
dataset contractcase-grouped time split, quality labels, damage labels, and reviewed slices
serviceversioned preprocessing and safe quality-first routing with abstention
operationsimmutable route traces, joined delayed outcomes, model card, source slices, shadow review, and rollback

Common failures

SymptomCauseFix
Holdout score is unrealistically highphotos from one case crossed splitsgroup by physical case and time
Blurry image triggers damage escalationscore evaluated before qualitygate evidence quality first
Specialist metric changes after a later editroute trace was overwritten with outcome dataappend specialist outcomes and join by case ID
Precision shifts after guideline updatedelayed outcomes omit reviewer or label versionbind each outcome to reviewer and bundle label contract
Multi-photo case result depends on row ordercase aggregation policy is implicitdefine deterministic case-level priority routing
New camera changes decisions silentlypreprocessing and source drift untrackedlog source/quality slices and version bundle
Candidate advances without rollback targetreceipt omits previous aliaspublish immutable candidate and rollback pointer together
Complete the lesson

Mastery Check

Answer every question, then check your score. Score above 75% to mark this lesson complete.

1.A frozen photo manifest has two photos from the same physical return, R-401-a and R-401-b. R-401-a stays in train, but R-401-b is edited to validation. Which release gate should fail?
2.R-406-a has damage_probability=0.94, blur_score=0.12, box_visible=True, and a manifest quality_label of unusable. If its model brightness is edited from 0.12 to 0.30 with min_brightness=0.20 and damage threshold 0.70, why should promotion still hold?
3.A route trace still stores photo ID, bundle ID, preprocessing, quality scores, damage probability, and action, but omits the damage_threshold used to choose priority_damage_review. Which gate should fail?
4.Use the case policy that a return case is priority_damage_review if any usable photo for that case crosses the threshold. Original delayed quality has TP=1, FP=1, and FN=1 across four usable test cases. R-408-a is confirmed damaged but scores 0.63, below the 0.70 threshold. If its score changes to 0.75, what are the new priority precision and recall?
5.Same CNN weights are repackaged, but preprocessing changes from a 224 center crop to full-frame resize, and previous_bundle is left empty. What must be fixed before promotion?
6.A warehouse installs a new camera. During the first day, the endpoint logs a much higher request_new_photo rate from warehouse_camera because many images are dark or missing the package, but no specialist-confirmed outcomes have arrived yet. What should operators do under the monitoring contract?
7.A usable customer photo passes quality checks and has damage_probability=0.91 with a damage threshold of 0.70. The customer also asks for an instant refund. What should the vision service's decision support do?
8.After routing seven test photos, specialists later review the cases. A developer proposes overwriting each original route trace with confirmed_damage and the final case state, then computing precision from those edited traces. What should the pipeline do instead?
9.All photos from each physical return already stay in one split. Cases span capture days 1 through 100. Which split plan preserves the required time direction?
10.Aggregate precision is unchanged, but small tears in dark customer-phone photos are being missed after a capture-path change. Which analysis can expose the operational failure?

10 questions remaining.

Next Step
Continue to Capstone: Production ML Pipeline

You have shipped tabular, ranking, forecasting, and vision artifacts with their own action gates. Next you'll manage them under one validated promotion, monitoring, and rollback workflow.

PreviousCapstone: Demand Forecasting
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Model Cards for Model Reporting

Mitchell, M., Wu, S., Zaldivar, A., et al. · 2019 · FAT* 2019

An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale.

Dosovitskiy, A., et al. · 2020 · ICLR 2021

MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.

Google Cloud. · 2026 · Official documentation

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.