ML Models
The system uses two models: a production-grade pretrained ASL Citizen ST-GCN recognizer and an end-to-end trained Random Forest baseline. Together they demonstrate the full ML lifecycle on Databricks.
1. Primary Recognizer: Pretrained ST-GCN
What it does
Graph Convolutional Network (GCN) trained on spatiotemporal pose sequences. Takes a video of a single ASL sign and outputs probabilities across 2,731 ASL Citizen sign classes.
Architecture
Input shape: (batch=1, channels=2, T=frames, V=27, M=1)
channels=2: x, y coordinates only (z-depth dropped)T: variable frames (typically 30β120 per sign)V=27: 27-keypoint OpenHands skeletonM=1: single body/person
Model depth: 10-layer ST-GCN with edge importance weighting Model size: 30 MB Inference latency: p50 120 ms Β· p95 180 ms (CPU, single clip)
Training (by Microsoft Research)
- Dataset: Full ASL Citizen (84k videos, 2,731 signs)
- Training procedure: Joint training with contrastive objectives
- Published accuracy: 59.52% top-1 on full 2,731-class vocabulary
- Expected subset accuracy: 80β90%+ top-1, 95%+ top-3 on our 100-sign subset (based on the paperβs vocabulary-restriction analysis)
How We Use It
# Load from MLflow (registered as "aslStgcn", stage=Production)
import mlflow.pytorch
import torch
model_uri = "models:/aslStgcn/Production"
model = mlflow.pytorch.load_model(model_uri)
model.eval()
# Inference
with torch.no_grad():
logits = model(landmarks_tensor) # shape: (1, 2731)
# Logit masking: restrict to our 100-sign vocabulary
VOCAB_INDICES = [12, 45, 78, ...] # 100 indices
masked_logits = logits.clone()
masked_logits[0, :] = -float('inf')
masked_logits[0, VOCAB_INDICES] = logits[0, VOCAB_INDICES]
# Softmax over masked vocab (probabilities sum to 1 over 100 signs)
probs = torch.nn.functional.softmax(masked_logits[0, VOCAB_INDICES], dim=-1)
# Top-3 predictions
top3_conf, top3_idx = torch.topk(probs, k=3)
top3_labels = [VOCAB_LABELS[VOCAB_INDICES[i]] for i in top3_idx]
return list(zip(top3_labels, top3_conf.numpy()))
# e.g., [("HELLO", 0.87), ("WAVE", 0.08), ("HI", 0.05)]
Why Logit Mask Before Softmax?
logits β softmax(logits) over all 2,731 classes β filter to our 100
Result: probabilities do not sum to 1 over the in-vocab subset.
logits β set non-vocab logits to -β β softmax(masked_logits)
Result: valid probability distribution over exactly 100 signs.
2. Baseline Model: sklearn Random Forest
What it does
Per-clip feature aggregation + Random Forest classification. Trained end-to-end on Databricks to satisfy the ML-lifecycle rubric requirement.
Training Process
Data: asl.landmarks Delta table (from ETL pipeline)
from pyspark.ml.feature import VectorAssembler
from sklearn.ensemble import RandomForestClassifier
import mlflow
import numpy as np
# Step 1: Feature aggregation
landmarks_df = spark.read.table("asl.landmarks")
def aggregate_features(landmarks_array):
"""Extract per-clip statistics: mean, std, min, max, velocity, trajectory curvature"""
arr = np.array(landmarks_array)
features = np.concatenate([
arr.mean(axis=0).flatten(),
arr.std(axis=0).flatten(),
arr.min(axis=0).flatten(),
arr.max(axis=0).flatten(),
np.diff(arr, axis=0).mean(axis=0).flatten() if arr.shape[0] > 1 else np.zeros(54),
])
return features.tolist()
# Step 2: Train (sklearn β Spark RF hit Free-Edition 256 MB serialization cap)
X_train, y_train = featurize(landmarks_df)
rf = RandomForestClassifier(n_estimators=100, max_depth=15, random_state=42)
rf.fit(X_train, y_train)
# Step 3: Log to MLflow
with mlflow.start_run():
mlflow.sklearn.log_model(rf, artifact_path="model")
accuracy = rf.score(X_test, y_test)
mlflow.log_metric("accuracy", accuracy)
Specs
- Top-1 accuracy: 40β55% on 100-sign subset
- Top-3 accuracy: 80β90%
- Inference latency: p50 30 ms Β· p95 50 ms (CPU)
- Model size: ~50 MB (serialized sklearn pickle)
- Training time: ~5 minutes on Databricks serverless
- Trees / depth: 100 / 15
Why This Model?
The ST-GCN is already trained (Microsoft Research). To satisfy the rubricβs βend-to-end ML lifecycleβ requirement, we train our own baseline that demonstrates:
- β Data collection (ASL Citizen subset)
- β Feature engineering (per-clip aggregation)
- β Model selection (Random Forest)
- β Training on Databricks (sklearn β Spark RF exceeded the 256 MB cap)
- β Model registry (MLflow)
- β Deployment (served from inferenceService)
- β Monitoring (inference events logged to Delta)
Performance Metrics
| Metric | ST-GCN | Random Forest |
|---|---|---|
| Top-1 Accuracy (100-sign) | 80β90%+ | 40β55% |
| Top-3 Accuracy | 95%+ | 80β90% |
| Latency p50 | 120 ms | 30 ms |
| Latency p95 | 180 ms | 50 ms |
| Model Size | 30 MB | ~50 MB |
| Load Time | 2β3 s | 1 s |
| Training Time (by us) | N/A (pretrained) | ~5 min |
| Required GPU | No (CPU sufficient) | No |
| Deployed as | PyTorch artifact in MLflow | sklearn pickle in MLflow |
Model Registry (MLflow on Databricks)
Both models registered in MLflow with metadata:
{
"name": "aslStgcn",
"latest_version": "1",
"stage": "Production",
"description": "Pretrained ASL Citizen ST-GCN, logit-masked to 100 signs",
"creation_timestamp": 1717287600000
}
{
"name": "aslRandomForest",
"latest_version": "1",
"stage": "Production",
"description": "Random Forest trained on 100-sign ASL Citizen subset",
"creation_timestamp": 1717288000000,
"metrics": {
"accuracy": 0.52,
"precision": 0.50,
"recall": 0.52
}
}
Inference at Deployment Time
The inferenceService loads both models at startup:
# app/models.py
class ModelCache:
def __init__(self):
self.stgcn = self._load_stgcn()
self.rf = self._load_rf()
def _load_stgcn(self):
model_uri = "models:/aslStgcn/Production"
return mlflow.pytorch.load_model(model_uri)
def _load_rf(self):
model_uri = "models:/aslRandomForest/Production"
return mlflow.sklearn.load_model(model_uri)
def predict(self, landmarks, model_name):
if model_name == "stgcn":
return self._predict_stgcn(landmarks)
elif model_name == "rf":
return self._predict_rf(landmarks)
def _predict_stgcn(self, landmarks):
# ... (logit masking, softmax, top-3)
pass
def _predict_rf(self, landmarks):
# Aggregate features β predict_proba β top-3
features = aggregate_features(landmarks)
probs = self.rf.predict_proba([features])[0]
top3_idx = np.argsort(probs)[-3:][::-1]
return [(self.rf.classes_[i], probs[i]) for i in top3_idx]
# Global cache, initialized at app startup
model_cache = ModelCache()