Lesson 35 is the final Capstone submission: deploying the optimised model from Lesson 34, evaluating it on the sealed test set for the first and only time, and submitting a complete project report documenting every decision from data pipeline to deployment. This is the most important lesson in Course 5 — it synthesises all 34 preceding lessons into one unified, documented, production-quality deep learning project. The deployment step produces a deployable model artefact: a TensorFlow SavedModel with a prediction function (Option A) or a HuggingFace pipeline exported to the Hub (Option B). The test set evaluation is the final, unbiased performance measurement — the number that appears in the Capstone report as the headline result. The project report documents the complete project lifecycle in six sections: dataset, architecture justification, training protocol, optimisation experiments, final evaluation, and deployment. A strong Capstone report is as valuable as the model itself — it demonstrates the ability to communicate engineering decisions clearly to collaborators, stakeholders, and future maintainers.
Learning Objectives
- Evaluate on sealed test set — report final unbiased performance metrics (accuracy/weighted F1) as the headline Capstone result
- Deploy Option A to TensorFlow SavedModel with a prediction function or TFLite for mobile; Option B to HuggingFace pipeline format
- Write a 6-section Capstone project report covering dataset, architecture, training, optimisation, evaluation, and deployment
- Demonstrate production ML engineering discipline: no test set leakage, systematic optimisation, documented decision rationale
- Optionally upload the trained model to the HuggingFace Hub or Google Drive for portfolio visibility
Technical Requirements
- Option A: ≥75% test accuracy on the sealed test set; TFLite export and an inference function accepting a raw image path
- Option B: ≥80% weighted F1 on the sealed test set; HuggingFace pipeline() inference demo with 5 sample commentaries
- Per-class precision, recall, and F1 for ALL classes reported in the project report (not just macro averages)
- Training curves showing loss and accuracy/F1 for both phases (Option A) or full fine-tuning run (Option B)
- Complete experiment comparison table from Phase 3 showing before/after metrics for each optimisation
- Confusion matrix annotated with class names for both Phase 2 baseline and Phase 3 final (two matrices in report)
Architecture & Design
The Capstone report has six mandatory sections. Section 1 — Dataset: class counts per split, class distribution histogram, augmentation strategy, leakage prevention checklist. Section 2 — Architecture: model choice with justification (why EfficientNetB0 over ResNet50, why DistilBERT over BERT-base), parameter count, trainable parameter count per phase, and comparison to the discarded alternative. Section 3 — Training: training curves for both phases, callback configuration, learning rate schedule, and explanation of any early stopping events. Section 4 — Optimisation: experiment table (change, before-metric, after-metric), confusion matrix comparison (Phase 2 vs Phase 3), and interpretation of why each optimisation worked or did not work. Section 5 — Final Evaluation: test set results with per-class report and confusion matrix, comparison against baseline (random chance and zero-shot for Option B), and analysis of remaining errors. Section 6 — Deployment: code listing of the inference function, latency measurement, model size, and instructions for reproducing the full project from scratch.
import numpy as np, tensorflow as tf, torch
from tensorflow import keras
from transformers import pipeline as hf_pipeline
from sklearn.metrics import classification_report, confusion_matrix
import matplotlib.pyplot as plt, matplotlib; matplotlib.use('Agg')
import seaborn as sns, time
np.random.seed(42)
print('CAPSTONE PHASE 4 — DEPLOYMENT AND FINAL EVALUATION')
print('=' * 60)
# ══════════════════════════════════════════════════════════
# OPTION A: Load final model + test set evaluation (ONCE)
# ══════════════════════════════════════════════════════════
print('\n[A] Loading optimised model...')
final_model_A = keras.models.load_model('/tmp/capstone_A_final.keras')
print('[A] Test set evaluation (SEALED SET — FIRST AND ONLY TIME)')
y_pred_test_A = final_model_A.predict(test_ds_A, verbose=0).argmax(axis=1)
y_true_test_A = np.concatenate([y.numpy() for _, y in test_ds_A])
test_acc_A = (y_pred_test_A == y_true_test_A).mean()
print(f'\n[A] FINAL TEST ACCURACY: {test_acc_A:.2%}')
print(f' Threshold: 75% Status: {"✓ PASSED" if test_acc_A >= 0.75 else "✗ BELOW THRESHOLD"}')
print('\nPer-class Report (TEST SET):')
print(classification_report(y_true_test_A, y_pred_test_A, target_names=SHOT_CLASSES))
# Test confusion matrix
cm_test_A = confusion_matrix(y_true_test_A, y_pred_test_A)
fig,ax=plt.subplots(figsize=(9,7))
sns.heatmap(cm_test_A,annot=True,fmt='d',cmap='Blues',
xticklabels=SHOT_CLASSES,yticklabels=SHOT_CLASSES,ax=ax)
ax.set_title(f'FINAL TEST CONFUSION MATRIX — Accuracy: {test_acc_A:.2%}')
plt.xticks(rotation=30,ha='right'); plt.tight_layout()
plt.savefig('/tmp/capstone_A_TEST_confusion.png',dpi=150,bbox_inches='tight')
print('Final test confusion matrix saved.')
# ══════════════════════════════════════════════════════════
# OPTION B: Load final model + test set evaluation (ONCE)
# ══════════════════════════════════════════════════════════
print('\n[B] Loading optimised DistilBERT model...')
sentiment_inference = hf_pipeline(
'text-classification',
model='/tmp/capstone_B_final',
tokenizer='/tmp/capstone_B_final',
device=-1
)
print('[B] Test set evaluation (SEALED SET — FIRST AND ONLY TIME)')
preds_test_B, labels_test_B = [], []
for batch in torch.utils.data.DataLoader(test_ds_B, batch_size=32):
with torch.no_grad():
out = weighted_trainer.model(
input_ids=batch['input_ids'],
attention_mask=batch['attention_mask']
)
preds_test_B.extend(out.logits.argmax(dim=-1).cpu().numpy())
labels_test_B.extend(batch['labels'].cpu().numpy())
from sklearn.metrics import f1_score
test_f1_B = f1_score(labels_test_B, preds_test_B, average='weighted')
test_acc_B = np.mean(np.array(preds_test_B) == np.array(labels_test_B))
print(f'\n[B] FINAL TEST WEIGHTED F1: {test_f1_B:.3f}')
print(f' TEST ACCURACY: {test_acc_B:.2%}')
print(f' Threshold: 0.80 Status: {"✓ PASSED" if test_f1_B >= 0.80 else "✗ BELOW THRESHOLD"}')
print('\nPer-class Report (TEST SET):')
print(classification_report(labels_test_B, preds_test_B, target_names=EVENT_CLASSES))
cm_test_B = confusion_matrix(labels_test_B, preds_test_B)
fig,ax=plt.subplots(figsize=(10,8))
sns.heatmap(cm_test_B,annot=True,fmt='d',cmap='Blues',
xticklabels=EVENT_CLASSES,yticklabels=EVENT_CLASSES,ax=ax)
ax.set_title(f'FINAL TEST CONFUSION MATRIX — Weighted F1: {test_f1_B:.3f}')
plt.xticks(rotation=30,ha='right'); plt.tight_layout()
plt.savefig('/tmp/capstone_B_TEST_confusion.png',dpi=150,bbox_inches='tight')
print('Final test confusion matrix saved.')Phase 1 — Core Implementation
Phase 1 of deployment builds the inference API — the function that any external caller can use to get predictions from the trained model without understanding its internals. A good inference API accepts raw input (image file path or raw text string), handles all preprocessing internally, returns a structured result (class label, confidence, all class probabilities), and includes latency measurement.
import numpy as np, tensorflow as tf, torch, time
from tensorflow import keras
from transformers import pipeline as hf_pipeline
# ══════════════════════════════════════════════════════════
# OPTION A: Production inference function
# ══════════════════════════════════════════════════════════
class IPLShotClassifier:
"""Production inference API for IPL shot type classification."""
CLASSES = ['cover_drive','pull_shot','sweep','flick','straight_drive','helicopter']
def __init__(self, model_path='/tmp/capstone_A_final.keras'):
self.model = keras.models.load_model(model_path)
self.model(np.zeros((1,224,224,3), dtype=np.float32), training=False) # warmup
def predict(self, image_array):
"""
Args: image_array — numpy array (H, W, 3), uint8 [0,255]
Returns: {'shot_type': str, 'confidence': float, 'all_probs': dict}
"""
img = tf.image.resize(image_array, (224,224)).numpy()
img = img[np.newaxis].astype(np.float32) # add batch dim
t0 = time.time()
probs = self.model(img, training=False).numpy()[0]
latency_ms = (time.time() - t0) * 1000
top_idx = probs.argmax()
return {
'shot_type': self.CLASSES[top_idx],
'confidence': float(probs[top_idx]),
'all_probs': {c: float(p) for c, p in zip(self.CLASSES, probs)},
'latency_ms': round(latency_ms, 1)
}
def predict_batch(self, image_arrays):
imgs = np.stack([tf.image.resize(img,(224,224)).numpy() for img in image_arrays])
probs = self.model(imgs.astype(np.float32), training=False).numpy()
return [{'shot_type': self.CLASSES[p.argmax()], 'confidence': float(p.max())}
for p in probs]
# Demo inference
classifier_A = IPLShotClassifier()
test_frame = np.random.randint(0, 256, (480, 854, 3), dtype=np.uint8) # typical HD frame
result = classifier_A.predict(test_frame)
print(f'[A] Prediction: {result["shot_type"]} ({result["confidence"]:.1%} confidence)')
print(f' Latency: {result["latency_ms"]}ms')
print(f' All probs: {result["all_probs"]}')
# ══════════════════════════════════════════════════════════
# OPTION B: Production inference function
# ══════════════════════════════════════════════════════════
commentary_pipe = hf_pipeline(
'text-classification',
model='/tmp/capstone_B_final',
tokenizer='/tmp/capstone_B_final',
device=-1, top_k=None # return all class probabilities
)
def classify_commentary(text):
"""Classify a cricket commentary string into an IPL event type."""
t0 = time.time()
results = commentary_pipe(text[:512])[0] # truncate at 512 chars
latency_ms = (time.time() - t0) * 1000
top = max(results, key=lambda x: x['score'])
return {
'event_type': top['label'],
'confidence': float(top['score']),
'all_probs': {r['label']: float(r['score']) for r in results},
'latency_ms': round(latency_ms, 1)
}
# Demo inference
test_commentaries = [
'Rohit Sharma brings up his century with a glorious six',
'Bumrah bowls a perfect yorker to take the crucial wicket',
'The drinks break is taken at the end of the 10th over',
]
print('\n[B] Commentary Classification Demo:')
for text in test_commentaries:
r = classify_commentary(text)
print(f' [{r["event_type"]:22s} {r["confidence"]:.0%}] {text[:55]}')Phase 2 — Feature Completion
Phase 2 adds TFLite export (Option A) for mobile deployment, model size measurement, and the HuggingFace Hub upload template (Option B). TFLite converts the TensorFlow SavedModel to a compressed, optimised format that runs on Android and iOS devices — reducing model size by 50–75% through quantisation while maintaining most accuracy. The Hub upload makes the model publicly accessible and provides a model card that documents the model for external users.
import tensorflow as tf, os
from tensorflow import keras
import numpy as np
# ══════════════════════════════════════════════════════════
# OPTION A: TFLite Export
# ══════════════════════════════════════════════════════════
print('[A] Exporting to TFLite...')
# Save as SavedModel first
final_model_A.export('/tmp/capstone_A_savedmodel')
# Convert to TFLite with float16 quantisation
converter = tf.lite.TFLiteConverter.from_saved_model('/tmp/capstone_A_savedmodel')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.float16] # float16 quantisation
tflite_model = converter.convert()
tflite_path = '/tmp/capstone_A_shot_classifier.tflite'
with open(tflite_path, 'wb') as f:
f.write(tflite_model)
original_size_mb = os.path.getsize('/tmp/capstone_A_final.keras') / (1024**2)
tflite_size_mb = os.path.getsize(tflite_path) / (1024**2)
print(f' SavedModel size: ~{original_size_mb:.1f} MB')
print(f' TFLite size: {tflite_size_mb:.1f} MB (float16 quantisation)')
print(f' Compression: {original_size_mb/max(tflite_size_mb,0.1):.1f}x smaller')
# TFLite inference demo
interpreter = tf.lite.Interpreter(model_path=tflite_path)
interpreter.allocate_tensors()
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
test_input = np.zeros((1,224,224,3), dtype=np.float32)
interpreter.set_tensor(input_details[0]['index'], test_input)
interpreter.invoke()
tflite_output = interpreter.get_tensor(output_details[0]['index'])
print(f' TFLite inference OK: output shape {tflite_output.shape}')
print(f' Predicted class: {SHOT_CLASSES[tflite_output.argmax()]}')
# ══════════════════════════════════════════════════════════
# OPTION B: HuggingFace Hub Upload Template
# ══════════════════════════════════════════════════════════
print('\n[B] HuggingFace Hub upload template:')
hub_template = '''
# Uncomment and run with valid HuggingFace token:
# from huggingface_hub import HfApi, login
# login(token='your_hf_token') # get from huggingface.co/settings/tokens
#
# api = HfApi()
# api.create_repo('your-username/ipl-commentary-event-classifier', private=False)
#
# from transformers import AutoModelForSequenceClassification, AutoTokenizer
# model = AutoModelForSequenceClassification.from_pretrained('/tmp/capstone_B_final')
# tokenizer = AutoTokenizer.from_pretrained('/tmp/capstone_B_final')
# model.push_to_hub('your-username/ipl-commentary-event-classifier')
# tokenizer.push_to_hub('your-username/ipl-commentary-event-classifier')
#
# Then anyone can use it with:
# pipe = pipeline('text-classification', model='your-username/ipl-commentary-event-classifier')
# result = pipe('Rohit hit a six to win the match!')
'''
print(hub_template)Phase 3 — Polish & Production Readiness
Phase 3 assembles the complete project report as a structured document. The report is the final deliverable — it is what evaluators assess the Capstone against. A strong report demonstrates: (1) understanding of why each decision was made, not just what was done; (2) honest reporting of failures and what was learned from them; (3) quantitative evidence for every claim (not 'the model performed well' but 'the model achieved 82% test accuracy vs 16.7% random baseline'); (4) awareness of the deployed model's limitations and failure modes.
# Capstone Project Report Template
# Complete each section with your specific project details
REPORT_TEMPLATE = '''
================================================================
SKILLVERIS COURSE 5 CAPSTONE PROJECT REPORT
Option [A/B]: [Image/Text] Classification
Project: [IPL Shot Type Classifier / IPL Commentary Event Tagger]
================================================================
SECTION 1 — DATASET
─────────────────────
Total samples: [N]
Classes: [list all classes]
Split: Train=[N_tr] / Val=[N_val] / Test=[N_te] (stratified)
Class distribution (all splits): [table]
Augmentation strategy: [list augmentations and rationale]
Leakage prevention: [describe what was fitted on training only]
SECTION 2 — ARCHITECTURE JUSTIFICATION
────────────────────────────────────────
Chosen architecture: [EfficientNetB0 / DistilBERT]
Justification: [3-4 sentences explaining why — cite params, pre-training, dataset size]
Alternative considered: [ResNet50 / BERT-base]
Reason rejected: [parameter count, training time, accuracy trade-off]
Total parameters: [N]
Phase 1 trainable: [N] ([%])
Phase 2 trainable: [N] ([%])
SECTION 3 — TRAINING
─────────────────────
Optimiser: Adam, lr=[1e-3/1e-5/2e-5], warmup=[yes/no]
Callbacks: EarlyStopping (patience=[N]), ModelCheckpoint, ReduceLROnPlateau
Phase 1 epochs: [N] (stopped at epoch [N])
Phase 1 best val: [metric]=[value]
Phase 2 epochs: [N] (stopped at epoch [N])
Phase 2 best val: [metric]=[value]
[Attach training curves: /tmp/capstone_A/B_curves.png]
SECTION 4 — OPTIMISATION EXPERIMENTS
──────────────────────────────────────
| Experiment | Change | Before | After | Delta |
|---------------------|---------------------------|----------|----------|--------|
| Phase 2 Baseline | Two-phase fine-tuning | [metric] | - | - |
| Exp 1: class weights| compute_class_weight | [before] | [after] | [Δ] |
| Exp 2: [your change]| [description] | [before] | [after] | [Δ] |
| Exp 3: [your change]| [description] | [before] | [after] | [Δ] |
[Attach Phase 2 confusion matrix and Phase 3 final confusion matrix]
SECTION 5 — FINAL EVALUATION (TEST SET)
─────────────────────────────────────────
Final test accuracy: [value]
Final weighted F1: [value] (Option B only)
Random baseline: [1/n_classes * 100]%
Zero-shot baseline: [value]% (Option B only)
Improvement over random: +[N]pp
[Attach final test confusion matrix]
Per-class performance:
[Paste classification_report output here]
Remaining error analysis:
- Largest off-diagonal: [class X] misclassified as [class Y] ([N] cases)
Hypothesis: [why does this confusion occur?]
- Second largest: [class A] misclassified as [class B] ([N] cases)
Hypothesis: [why?]
SECTION 6 — DEPLOYMENT
────────────────────────
Deployment format: [TFLite / HuggingFace pipeline()]
Model size: [N] MB (full), [N] MB (quantised)
Inference latency: [N]ms per sample on CPU
Reproduction: [list all commands needed to reproduce from scratch]
Limitations: [honest statement of what the model cannot do well]
================================================================
Submitted by: Sri Hayavadhana Info-Tech
Developer: Nagarajarao C R
Platform: SkillVeris
================================================================
'''
with open('/tmp/capstone_report_template.txt', 'w') as f:
f.write(REPORT_TEMPLATE)
print('Report template saved: /tmp/capstone_report_template.txt')
print('Complete each section with your specific project results and submit.')Evaluation Rubric
- Data pipeline quality (25pts): correct stratified splitting, no leakage, preprocessing fitted on train only, class distribution documented with imbalance strategy noted
- Architecture justification (25pts): specific reasoning for architecture choice, comparison to at least one rejected alternative, parameter counts for each training phase documented
- Training discipline (25pts): both phases trained with appropriate callbacks, training curves included, early stopping events documented, hyperparameter choices explained
- Evaluation rigour (15pts): per-class precision/recall/F1 for all classes, test set confusion matrix, comparison to random baseline and zero-shot baseline (Option B)
- Deployment quality (10pts): working inference function with latency measurement, model size reported, TFLite export (A) or Hub upload (B), reproduction instructions complete
Extension Challenges: (1) Implement a confidence-threshold routing system: predictions above 90% confidence are automatically accepted; predictions between 60–90% are flagged for human review; predictions below 60% are sent to the zero-shot classifier as a fallback. Document the precision-recall tradeoff of each confidence tier. (2) Measure inference latency across batch sizes 1, 4, 16, 64 and plot throughput (samples/sec) vs latency — this is the deployment optimisation analysis that production engineering teams conduct before scaling to high-traffic services. (3) Implement model compression: try int8 quantisation (TFLite) for Option A and compare accuracy, size, and latency against float16 — quantise-aware training typically recovers 90–95% of float32 accuracy at 4× size reduction.