ML Data Operations: Deep Dive into Data-Centric ML Infrastructure
Introduction
While ML infrastructure focuses on features, models, and serving, data operations is about the data itself: acquiring it, annotating it, ensuring its quality, augmenting it synthetically, and managing it at petabyte scale. This guide covers the data-centric aspects of ML systems, with emphasis on multimodal data (vision, audio, text) at billion-scale.
Related post: Batch, Streaming, and Merged Views in ML Data Operations explains how the batch lane, streaming lane, and merged serving/training view fit together.
1. Data Annotation Workflows
Purpose: Transform raw data into labeled training data through manual, semi-automated, or fully automated annotation pipelines.
1.1 Annotation Types
Classification labels:
{
"image_id": "img_12345",
"label": "cat",
"confidence": 0.95,
"annotator_id": "user_789",
"timestamp": "2024-01-15T10:30:00Z"
}
Bounding boxes (object detection):
{
"image_id": "img_12345",
"annotations": [
{
"label": "cat",
"bbox": [100, 150, 300, 400], // [x_min, y_min, x_max, y_max]
"confidence": 0.92
}
]
}
Segmentation masks (instance/semantic segmentation):
{
"image_id": "img_12345",
"mask_path": "s3://annotations/img_12345_mask.png",
"classes": {
"1": "cat",
"2": "dog",
"3": "background"
}
}
Keypoints (pose estimation):
{
"image_id": "img_12345",
"keypoints": [
{"label": "nose", "x": 150, "y": 200, "visibility": 2},
{"label": "left_eye", "x": 140, "y": 190, "visibility": 2}
]
}
Text annotations (NLP):
{
"text_id": "txt_67890",
"entities": [
{"text": "Apple", "label": "ORG", "start": 0, "end": 5},
{"text": "Cupertino", "label": "LOC", "start": 15, "end": 24}
]
}
1.2 Annotation Tools
Open-source:
- Label Studio: Flexible, supports multiple annotation types
- CVAT: Computer vision focused, video annotation
- Labelbox: Enterprise-grade, collaboration features
Commercial:
- Scale AI: Managed annotation service, high quality
- Labelbox: Enterprise platform with ML-assisted labeling
- Supervisely: Computer vision focused
Custom tools (Apple-scale):
# Custom annotation interface
class AnnotationTool:
def __init__(self, schema: AnnotationSchema):
self.schema = schema
self.queue = AnnotationQueue()
def load_next_image(self) -> Image:
# Priority-based sampling
return self.queue.get_next(
strategy="uncertainty_sampling", # Prioritize uncertain examples
model_version="v2.3"
)
def save_annotation(self, annotation: Annotation):
# Validate against schema
self.schema.validate(annotation)
# Store with metadata
self.store.save(annotation, metadata={
"annotator_id": self.current_user,
"time_spent": self.timer.elapsed(),
"model_assistance": self.model_predictions
})
1.3 Annotation Pipeline Architecture
Raw Data (images, text, audio)
↓
[1] Data Ingestion & Preprocessing
- Resize images to standard dimensions
- Normalize audio levels
- Tokenize text
↓
[2] Annotation Queue Management
- Priority sampling (uncertainty, diversity, difficulty)
- Load balancing across annotators
- Task assignment based on expertise
↓
[3] Annotation Interface
- Web-based UI (Label Studio, custom)
- Desktop app (for specialized tasks)
- Mobile app (for field collection)
↓
[4] Quality Assurance
- Inter-annotator agreement (Cohen's kappa, Fleiss' kappa)
- Gold standard validation (known answers)
- Consensus mechanisms (multiple annotators per item)
↓
[5] Annotation Storage
- Metadata DB (PostgreSQL, DynamoDB)
- Annotation files (JSON, COCO format, Pascal VOC)
- Versioning (track annotation changes over time)
↓
[6] Export to Training Pipeline
- Convert to framework-specific format (TFRecord, WebDataset)
- Split into train/val/test
- Generate data cards / documentation
1.4 Annotation Quality Metrics
Inter-annotator agreement (IAA):
from sklearn.metrics import cohen_kappa_score
def compute_iaa(annotations_1, annotations_2):
"""Measure agreement between two annotators"""
kappa = cohen_kappa_score(annotations_1, annotations_2)
# Interpretation
if kappa < 0.20:
return "Slight agreement - need better guidelines"
elif kappa < 0.40:
return "Fair agreement - ambiguous categories"
elif kappa < 0.60:
return "Moderate agreement - acceptable"
elif kappa < 0.80:
return "Substantial agreement - good"
else:
return "Almost perfect agreement - excellent"
For object detection:
def compute_bbox_iou(box1, box2):
"""Intersection over Union for bounding boxes"""
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
intersection = max(0, x2 - x1) * max(0, y2 - y1)
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
union = area1 + area2 - intersection
return intersection / union if union > 0 else 0
# Average IoU across annotators
avg_iou = np.mean([compute_bbox_iou(b1, b2) for b1, b2 in zip(boxes_1, boxes_2)])
1.5 Active Learning for Annotation
Purpose: Minimize annotation cost by selecting the most informative samples
Strategies:
Uncertainty sampling:
def select_uncertain_samples(model, unlabeled_data, n_samples=100):
"""Select samples where model is most uncertain"""
predictions = model.predict_proba(unlabeled_data)
uncertainty = 1 - np.max(predictions, axis=1) # 1 - max probability
# Select top-n most uncertain
indices = np.argsort(uncertainty)[-n_samples:]
return unlabeled_data[indices]
Diversity sampling:
def select_diverse_samples(embeddings, n_samples=100):
"""Select diverse samples using k-means clustering"""
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=n_samples, random_state=42)
kmeans.fit(embeddings)
# Select samples closest to cluster centers
distances = kmeans.transform(embeddings)
indices = np.argmin(distances, axis=0)
return indices
Core-set selection:
def select_coreset(embeddings, n_samples=100):
"""Select representative subset using greedy core-set"""
from sklearn.metrics import pairwise_distances
selected = []
remaining = list(range(len(embeddings)))
# Start with random sample
selected.append(remaining.pop(0))
for _ in range(n_samples - 1):
# Compute distances to selected points
dists = pairwise_distances(embeddings[remaining], embeddings[selected])
min_dists = np.min(dists, axis=1)
# Select point farthest from selected set
farthest = np.argmax(min_dists)
selected.append(remaining[farthest])
remaining.pop(farthest)
return selected
Pipeline integration:
class ActiveLearningPipeline:
def __init__(self, model, annotation_queue):
self.model = model
self.queue = annotation_queue
def run_iteration(self):
# 1. Train model on current labeled data
self.model.fit(labeled_data)
# 2. Select uncertain samples
uncertain = select_uncertain_samples(self.model, unlabeled_data, n=100)
# 3. Send to annotation queue
self.queue.add(uncertain, priority="high")
# 4. Wait for annotations
new_labels = self.queue.wait_for_annotations()
# 5. Add to labeled dataset
labeled_data.extend(new_labels)
# 6. Repeat
1.6 Annotation at Scale (Billion-scale)
Challenges:
- Volume: Billions of images/videos to annotate
- Velocity: Continuous data ingestion
- Variety: Multiple annotation types (bbox, segmentation, keypoints)
- Quality: Maintaining consistency across thousands of annotators
Solutions:
Distributed annotation queue:
# Kafka-based annotation queue
class DistributedAnnotationQueue:
def __init__(self, kafka_config):
self.producer = KafkaProducer(**kafka_config)
self.consumer = KafkaConsumer(**kafka_config)
def enqueue(self, items: List[AnnotationItem], priority: str = "normal"):
"""Distribute items across partitions"""
for item in items:
# Partition by annotator expertise
partition = hash(item.required_expertise) % self.num_partitions
self.producer.send(
topic=f"annotations.{priority}",
value=item.to_dict(),
partition=partition
)
def dequeue(self, annotator_id: str) -> AnnotationItem:
"""Get next item for annotator"""
# Subscribe to relevant partitions
return self.consumer.poll(timeout=1000)
Multi-annotator consensus:
def aggregate_annotations(annotations: List[Annotation]) -> Annotation:
"""Aggregate multiple annotator outputs"""
if len(annotations) == 1:
return annotations[0]
# For classification: majority vote
if annotations[0].type == "classification":
labels = [a.label for a in annotations]
return Annotation(label=Counter(labels).most_common(1)[0][0])
# For bounding boxes: average IoU-weighted
elif annotations[0].type == "bbox":
boxes = [a.bbox for a in annotations]
return Annotation(bbox=average_boxes(boxes))
# For segmentation: pixel-wise majority vote
elif annotations[0].type == "segmentation":
masks = [a.mask for a in annotations]
return Annotation(mask=majority_vote_masks(masks))
Annotator performance tracking:
class AnnotatorPerformance:
def __init__(self):
self.metrics = {}
def record_annotation(self, annotator_id, annotation, gold_standard=None):
"""Track annotator accuracy and speed"""
if gold_standard:
accuracy = compute_accuracy(annotation, gold_standard)
else:
accuracy = None
self.metrics[annotator_id].append({
"timestamp": now(),
"accuracy": accuracy,
"time_spent": annotation.time_spent,
"complexity": annotation.complexity
})
def get_performance_report(self, annotator_id):
"""Generate performance report"""
metrics = self.metrics[annotator_id]
return {
"avg_accuracy": np.mean([m["accuracy"] for m in metrics if m["accuracy"]]),
"avg_time_per_annotation": np.mean([m["time_spent"] for m in metrics]),
"total_annotations": len(metrics),
"quality_trend": compute_trend(metrics)
}
2. Synthetic Data Generation
Purpose: Generate artificial training data to augment real datasets, address class imbalance, or create edge cases.
2.1 Synthetic Data Use Cases
Class imbalance:
# Problem: 99% background, 1% defect
# Solution: Generate synthetic defects
def augment_defect_samples(defect_images, n_synthetic=10000):
"""Generate synthetic defect samples"""
synthetic_data = []
for _ in range(n_synthetic):
# Sample random defect
defect = random.choice(defect_images)
# Place on random background
background = random.choice(background_images)
x, y = random_position(background.size, defect.size)
# Composite
synthetic = composite(background, defect, position=(x, y))
synthetic_data.append((synthetic, "defect"))
return synthetic_data
Edge cases:
# Generate rare scenarios
def generate_edge_cases():
edge_cases = []
# Low lighting
for img in dataset:
dark_img = adjust_brightness(img, factor=0.2)
edge_cases.append((dark_img, img.label))
# Motion blur
for img in dataset:
blurred_img = apply_motion_blur(img, angle=45, length=20)
edge_cases.append((blurred_img, img.label))
# Occlusion
for img in dataset:
occluded_img = apply_random_occlusion(img, coverage=0.3)
edge_cases.append((occluded_img, img.label))
return edge_cases
Domain adaptation:
# Sim-to-real transfer
def generate_realistic_sim_data(sim_data):
"""Make simulation data look more realistic"""
domain_adaptor = DomainAdaptationModel()
realistic_data = []
for sim_img, sim_label in sim_data:
real_img = domain_adaptor(sim_img) # Style transfer
realistic_data.append((real_img, sim_label))
return realistic_data
2.2 Synthetic Data Generation Techniques
Traditional augmentation:
import albumentations as A
transform = A.Compose([
A.HorizontalFlip(p=0.5),
A.Rotate(limit=30, p=0.5),
A.RandomBrightnessContrast(p=0.3),
A.GaussNoise(p=0.2),
A.CoarseDropout(max_holes=8, max_height=32, max_width=32, p=0.3),
])
def augment_image(image):
return transform(image=image)["image"]
GAN-based generation:
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2")
def generate_synthetic_images(prompt, n_images=100):
"""Generate synthetic images using Stable Diffusion"""
images = []
for _ in range(n_images):
image = pipe(prompt, num_inference_steps=50).images[0]
images.append(image)
return images
# Generate synthetic cats
synthetic_cats = generate_synthetic_images("a photo of a cat", n_images=1000)
Diffusion models:
from diffusers import DiffusionPipeline
def generate_diverse_images(base_prompt, n_variations=100):
"""Generate diverse variations of a concept"""
pipe = DiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-xl")
variations = []
for i in range(n_variations):
# Vary the prompt slightly
varied_prompt = f"{base_prompt}, style {i % 10}, lighting {i % 5}"
image = pipe(varied_prompt).images[0]
variations.append(image)
return variations
3D rendering:
import bpy # Blender Python API
def render_3d_models(models, n_views=10):
"""Render 3D models from multiple viewpoints"""
rendered_images = []
for model in models:
# Load 3D model
bpy.ops.import_scene.obj(filepath=model.path)
# Render from multiple angles
for angle in np.linspace(0, 360, n_views):
set_camera_position(angle=angle, elevation=30)
image = render_scene()
rendered_images.append((image, model.label))
return rendered_images
2.3 Synthetic Data Quality Validation
Visual inspection:
def validate_synthetic_data(synthetic_images, real_images):
"""Compare synthetic vs real distributions"""
# Compute FID score (Fréchet Inception Distance)
fid_score = compute_fid(synthetic_images, real_images)
# Lower FID = more similar distributions
if fid_score < 10:
return "Excellent quality"
elif fid_score < 50:
return "Good quality"
elif fid_score < 100:
return "Acceptable quality"
else:
return "Poor quality - regenerate"
Model-based validation:
def validate_with_downstream_model(synthetic_data, test_model):
"""Check if synthetic data improves model performance"""
# Train on real + synthetic
model = train_model(real_data + synthetic_data)
# Evaluate on real test set
accuracy = evaluate(model, real_test_data)
# Compare to baseline (real only)
baseline_accuracy = evaluate(test_model, real_test_data)
improvement = accuracy - baseline_accuracy
return improvement > 0 # Synthetic data helped
Diversity metrics:
def compute_diversity(images):
"""Measure diversity of synthetic dataset"""
# Compute embeddings
embeddings = [get_embedding(img) for img in images]
# Compute pairwise distances
distances = pairwise_distances(embeddings)
# Average distance = diversity
avg_distance = np.mean(distances[np.triu_indices_from(distances, k=1)])
return avg_distance
2.4 Synthetic Data Pipeline Architecture
Data Requirements
↓
[1] Generation Strategy Selection
- GAN-based (StyleGAN, BigGAN)
- Diffusion models (Stable Diffusion, DALL-E)
- 3D rendering (Blender, Unity)
- Traditional augmentation (Albumentations)
↓
[2] Generation Pipeline
- Batch generation (Spark, Ray)
- Quality filtering (FID, CLIP score)
- Deduplication (perceptual hashing)
↓
[3] Annotation (if needed)
- Auto-labeling (use generation parameters)
- Manual verification (sample-based)
↓
[4] Quality Assurance
- Distribution matching (FID, KS test)
- Diversity checks (embedding distance)
- Downstream validation (model improvement)
↓
[5] Integration with Training Data
- Mix with real data (ratio tuning)
- Version tracking (synthetic vs real)
- Lineage tracking (which model generated which data)
Orchestration example:
class SyntheticDataPipeline:
def __init__(self, generator, validator, storage):
self.generator = generator
self.validator = validator
self.storage = storage
def run(self, requirements: DataRequirements):
# 1. Generate synthetic data
synthetic_data = self.generator.generate(
prompt=requirements.prompt,
n_samples=requirements.n_samples,
diversity=requirements.diversity_threshold
)
# 2. Validate quality
quality_report = self.validator.validate(
synthetic_data,
reference_data=requirements.reference_data
)
if quality_report.fid_score > requirements.max_fid:
raise ValueError(f"Quality too low: FID={quality_report.fid_score}")
# 3. Store with metadata
self.storage.save(
data=synthetic_data,
metadata={
"generator": self.generator.model_name,
"generation_params": self.generator.params,
"quality_metrics": quality_report.to_dict(),
"lineage": f"generated_from_{requirements.prompt}"
}
)
return quality_report
2.5 Lineage Tracking for Synthetic Data
Purpose: Track which model generated which data, with what parameters
Schema:
{
"data_id": "synth_12345",
"source": "synthetic",
"generator": {
"model": "stable-diffusion-xl",
"version": "1.0",
"checkpoint": "sd-xl-base-1.0.safetensors"
},
"generation_params": {
"prompt": "a photo of a cat",
"negative_prompt": "blurry, low quality",
"num_inference_steps": 50,
"guidance_scale": 7.5,
"seed": 42
},
"quality_metrics": {
"fid_score": 12.3,
"clip_score": 0.87,
"diversity": 0.92
},
"created_at": "2024-01-15T10:30:00Z",
"created_by": "pipeline_v2"
}
Tracking implementation:
class SyntheticDataLineage:
def __init__(self, db):
self.db = db
def log_generation(self, data_id, generator, params, quality_metrics):
"""Log synthetic data generation"""
self.db.insert("synthetic_data_lineage", {
"data_id": data_id,
"generator_model": generator.model_name,
"generator_version": generator.version,
"params": json.dumps(params),
"quality_metrics": json.dumps(quality_metrics),
"created_at": now()
})
def get_lineage(self, data_id):
"""Get full lineage for a synthetic data point"""
return self.db.query(
"SELECT * FROM synthetic_data_lineage WHERE data_id = ?",
data_id
)
def trace_to_model(self, model_id):
"""Find all synthetic data used to train a model"""
return self.db.query(
"""
SELECT s.* FROM synthetic_data_lineage s
JOIN model_training_data m ON s.data_id = m.data_id
WHERE m.model_id = ?
""",
model_id
)
3. Multimodal Data Handling
Purpose: Manage datasets containing multiple modalities (images, video, audio, text) with proper alignment, storage, and processing.
3.1 Multimodal Data Types
Vision:
- Images (JPEG, PNG, TIFF)
- Video (MP4, AVI, MOV)
- 3D point clouds (PLY, OBJ)
- Depth maps (PNG, NPY)
Audio:
- Waveform (WAV, FLAC)
- Spectrograms (PNG, NPY)
- Mel-frequency cepstral coefficients (MFCC)
Text:
- Raw text (TXT, JSON)
- Tokenized text (token IDs)
- Embeddings (vectors)
Structured:
- Metadata (JSON, CSV)
- Labels (JSON, COCO format)
- Annotations (XML, JSON)
3.2 Storage Patterns for Large Binary Assets
Problem: Storing billions of images/videos efficiently
Solution 1: Object storage with metadata DB:
# Store images in S3, metadata in PostgreSQL
class MultimodalStorage:
def __init__(self, s3_client, db):
self.s3 = s3_client
self.db = db
def store_image(self, image_id, image_bytes, metadata):
# Store binary in S3
s3_key = f"images/{image_id}.jpg"
self.s3.put_object(
Bucket="ml-data",
Key=s3_key,
Body=image_bytes
)
# Store metadata in DB
self.db.insert("images", {
"image_id": image_id,
"s3_key": s3_key,
"width": metadata["width"],
"height": metadata["height"],
"format": "jpeg",
"size_bytes": len(image_bytes),
"created_at": now()
})
def load_image(self, image_id):
# Get metadata
meta = self.db.query("SELECT * FROM images WHERE image_id = ?", image_id)
# Load from S3
response = self.s3.get_object(Bucket="ml-data", Key=meta["s3_key"])
image_bytes = response["Body"].read()
return image_bytes, meta
Solution 2: WebDataset (tar-based):
import webdataset as wds
# Store images in tar files (efficient for sequential access)
def create_webdataset(image_list, output_path):
"""Create WebDataset from list of images"""
with wds.ShardWriter(f"{output_path}-%06d.tar", maxcount=10000) as sink:
for i, (image_id, image_bytes, metadata) in enumerate(image_list):
sink.write({
"__key__": f"sample_{i:06d}",
"jpg": image_bytes,
"json": metadata
})
# Load WebDataset efficiently
def load_webdataset(shard_pattern):
"""Load WebDataset with parallel processing"""
dataset = (
wds.WebDataset(shard_pattern)
.decode("rgb")
.to_tuple("jpg", "json")
.batched(64)
)
return dataset
Solution 3: HDF5/Parquet for structured multimodal:
import h5py
class MultimodalDataset:
def __init__(self, path):
self.h5 = h5py.File(path, "r")
def get_sample(self, idx):
"""Get multimodal sample"""
return {
"image": self.h5["images"][idx],
"audio": self.h5["audio"][idx],
"text": self.h5["text"][idx].decode(),
"label": self.h5["labels"][idx]
}
# Create dataset
with h5py.File("multimodal.h5", "w") as f:
f.create_dataset("images", data=images_array)
f.create_dataset("audio", data=audio_array)
f.create_dataset("text", data=text_array)
f.create_dataset("labels", data=labels_array)
3.3 Multimodal Data Alignment
Problem: Different modalities have different sampling rates, timestamps, and formats
Video + Audio alignment:
import ffmpeg
def align_video_audio(video_path, audio_path, output_path):
"""Align video and audio streams"""
# Extract video
video = ffmpeg.input(video_path).video
# Extract and resample audio to match video frame rate
audio = ffmpeg.input(audio_path).audio.filter("aresample", async=1)
# Mux together
ffmpeg.output(video, audio, output_path).run()
def extract_aligned_frames(video_path, output_dir, fps=30):
"""Extract frames aligned to audio timestamps"""
ffmpeg.input(video_path).output(
f"{output_dir}/frame_%04d.png",
vf=f"fps={fps}"
).run()
Text + Image alignment (image captioning):
class ImageTextAligner:
def __init__(self, clip_model):
self.clip = clip_model
def compute_similarity(self, image, text):
"""Compute CLIP similarity between image and text"""
image_features = self.clip.encode_image(image)
text_features = self.clip.encode_text(text)
similarity = torch.cosine_similarity(image_features, text_features)
return similarity.item()
def filter_aligned_pairs(self, image_text_pairs, threshold=0.25):
"""Filter pairs with low alignment"""
aligned = []
for image, text in image_text_pairs:
sim = self.compute_similarity(image, text)
if sim >= threshold:
aligned.append((image, text, sim))
return aligned
Timestamp-based alignment:
class MultimodalAligner:
def align_by_timestamp(self, modalities):
"""Align modalities by timestamp"""
# Find common time range
min_time = max(m["start_time"] for m in modalities)
max_time = min(m["end_time"] for m in modalities)
# Resample each modality to common timeline
aligned = {}
for modality in modalities:
aligned[modality["type"]] = self.resample(
modality["data"],
modality["timestamps"],
start=min_time,
end=max_time,
target_fps=30
)
return aligned
3.4 Cross-Modal Processing
Vision-Language models:
from transformers import CLIPProcessor, CLIPModel
class CrossModalProcessor:
def __init__(self):
self.model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
def encode_image(self, image):
"""Encode image to embedding"""
inputs = self.processor(images=image, return_tensors="pt")
outputs = self.model.get_image_features(**inputs)
return outputs.squeeze().numpy()
def encode_text(self, text):
"""Encode text to embedding"""
inputs = self.processor(text=[text], return_tensors="pt")
outputs = self.model.get_text_features(**inputs)
return outputs.squeeze().numpy()
def cross_modal_retrieval(self, query_image, text_candidates, top_k=5):
"""Find most relevant text for an image"""
image_emb = self.encode_image(query_image)
text_embs = [self.encode_text(t) for t in text_candidates]
similarities = [
np.dot(image_emb, t_emb) / (np.linalg.norm(image_emb) * np.linalg.norm(t_emb))
for t_emb in text_embs
]
top_indices = np.argsort(similarities)[-top_k:][::-1]
return [(text_candidates[i], similarities[i]) for i in top_indices]
Audio-Text alignment:
from transformers import WhisperProcessor, WhisperForConditionalGeneration
class AudioTranscriber:
def __init__(self):
self.processor = WhisperProcessor.from_pretrained("openai/whisper-large-v2")
self.model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-large-v2")
def transcribe_with_timestamps(self, audio_path):
"""Transcribe audio with word-level timestamps"""
audio, sr = librosa.load(audio_path, sr=16000)
inputs = self.processor(audio, return_tensors="pt", sampling_rate=16000)
outputs = self.model.generate(
**inputs,
return_timestamps="word",
return_timestamps_word=True
)
return outputs["words"] # [{"word": "hello", "timestamp": (0.0, 0.5)}, ...]
3.5 Multimodal Dataset Management at Scale
Dataset versioning:
class MultimodalDatasetVersion:
def __init__(self, dataset_id, version):
self.dataset_id = dataset_id
self.version = version
self.manifest_path = f"s3://datasets/{dataset_id}/v{version}/manifest.json"
def create_manifest(self, samples):
"""Create dataset manifest"""
manifest = {
"dataset_id": self.dataset_id,
"version": self.version,
"num_samples": len(samples),
"modalities": ["image", "text", "audio"],
"samples": [
{
"id": s["id"],
"image_path": s["image_path"],
"text": s["text"],
"audio_path": s["audio_path"],
"labels": s["labels"]
}
for s in samples
]
}
# Write manifest to S3
s3_client.put_object(
Bucket="datasets",
Key=f"{self.dataset_id}/v{self.version}/manifest.json",
Body=json.dumps(manifest)
)
def load_manifest(self):
"""Load dataset manifest"""
response = s3_client.get_object(
Bucket="datasets",
Key=f"{self.dataset_id}/v{self.version}/manifest.json"
)
return json.loads(response["Body"].read())
4. ML Enrichment Pipelines
Purpose: Use ML models to enhance, filter, or augment data automatically.
4.1 Auto-Labeling with Foundation Models
Image classification:
from transformers import AutoImageProcessor, AutoModelForImageClassification
class AutoLabeler:
def __init__(self, model_name="google/vit-base-patch16-224"):
self.processor = AutoImageProcessor.from_pretrained(model_name)
self.model = AutoModelForImageClassification.from_pretrained(model_name)
def label_images(self, images, confidence_threshold=0.8):
"""Auto-label images with foundation model"""
inputs = self.processor(images=images, return_tensors="pt")
outputs = self.model(**inputs)
probs = torch.softmax(outputs.logits, dim=-1)
labels = []
for i, prob in enumerate(probs):
max_prob, max_idx = torch.max(prob, dim=0)
if max_prob >= confidence_threshold:
label = self.model.config.id2label[max_idx.item()]
labels.append({"image_id": images[i].id, "label": label, "confidence": max_prob.item()})
return labels
Object detection with grounding:
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
class GroundingDINO:
def __init__(self):
self.processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-tiny")
self.model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-tiny")
def detect_objects(self, image, text_prompt="cat . dog . person ."):
"""Detect objects using text prompts"""
inputs = self.processor(images=image, text=text_prompt, return_tensors="pt")
outputs = self.model(**inputs)
results = self.processor.post_process_grounded_object_detection(
outputs,
inputs.input_ids,
box_threshold=0.3,
text_threshold=0.25,
target_sizes=[image.size[::-1]]
)
return results[0] # {"scores": [...], "labels": [...], "boxes": [...]}
4.2 Embedding Generation at Scale
Batch embedding generation:
class EmbeddingGenerator:
def __init__(self, model_name="openai/clip-vit-large-patch14"):
self.model = CLIPModel.from_pretrained(model_name)
self.processor = CLIPProcessor.from_pretrained(model_name)
def generate_embeddings(self, images, batch_size=32):
"""Generate embeddings for large dataset"""
embeddings = []
for i in range(0, len(images), batch_size):
batch = images[i:i+batch_size]
inputs = self.processor(images=batch, return_tensors="pt")
with torch.no_grad():
features = self.model.get_image_features(**inputs)
embeddings.extend(features.cpu().numpy())
return np.array(embeddings)
def generate_and_store(self, images, output_path):
"""Generate embeddings and store in vector DB"""
embeddings = self.generate_embeddings(images)
# Store in FAISS
import faiss
index = faiss.IndexFlatL2(embeddings.shape[1])
index.add(embeddings)
faiss.write_index(index, output_path)
Distributed embedding generation:
from pyspark.sql import SparkSession
def distributed_embedding_generation(image_paths, num_partitions=100):
"""Generate embeddings in parallel using Spark"""
spark = SparkSession.builder.getOrCreate()
# Distribute images across partitions
rdd = spark.sparkContext.parallelize(image_paths, num_partitions)
def process_partition(partition):
# Load model once per partition
model = EmbeddingGenerator()
embeddings = []
for path in partition:
image = load_image(path)
emb = model.generate_embeddings([image])[0]
embeddings.append((path, emb.tolist()))
return embeddings
# Process in parallel
results = rdd.mapPartitions(process_partition).collect()
return results
4.3 Quality Filtering with ML Models
Image quality assessment:
from transformers import AutoImageProcessor, AutoModelForImageClassification
class QualityFilter:
def __init__(self):
self.processor = AutoImageProcessor.from_pretrained("google/vit-base-patch16-224")
self.model = AutoModelForImageClassification.from_pretrained("google/vit-base-patch16-224")
def assess_quality(self, image):
"""Assess image quality (blur, exposure, etc.)"""
inputs = self.processor(images=image, return_tensors="pt")
outputs = self.model(**inputs)
# Predict quality score (0-1)
quality_score = torch.sigmoid(outputs.logits).item()
return quality_score
def filter_low_quality(self, images, threshold=0.7):
"""Filter out low-quality images"""
high_quality = []
for image in images:
score = self.assess_quality(image)
if score >= threshold:
high_quality.append(image)
return high_quality
Deduplication with perceptual hashing:
import imagehash
from PIL import Image
class Deduplicator:
def __init__(self, hash_size=16):
self.hash_size = hash_size
def compute_hash(self, image):
"""Compute perceptual hash"""
return imagehash.phash(Image.open(image), hash_size=self.hash_size)
def find_duplicates(self, images, threshold=5):
"""Find duplicate images based on perceptual hash"""
hashes = {}
duplicates = []
for image_path in images:
img_hash = self.compute_hash(image_path)
# Check for similar hashes
for existing_path, existing_hash in hashes.items():
if abs(img_hash - existing_hash) <= threshold:
duplicates.append((image_path, existing_path))
break
hashes[image_path] = img_hash
return duplicates
4.4 ML Enrichment Pipeline Architecture
Raw Data
↓
[1] Quality Assessment
- Image quality (blur, exposure)
- Audio quality (noise, clipping)
- Text quality (length, language)
↓
[2] Deduplication
- Perceptual hashing (images)
- Text similarity (embeddings)
- Audio fingerprinting
↓
[3] Auto-Labeling
- Foundation models (CLIP, GPT-4)
- Object detection (Grounding DINO)
- Segmentation (SAM)
↓
[4] Embedding Generation
- Image embeddings (CLIP, DINO)
- Text embeddings (BERT, GPT)
- Audio embeddings (Whisper, Wav2Vec)
↓
[5] Enrichment Storage
- Quality scores
- Labels + confidence
- Embeddings (vector DB)
- Metadata
↓
[6] Integration with Training Pipeline
- Filter by quality
- Sample by diversity
- Balance by label
5. Agentic Capabilities in Pipelines
Purpose: Use LLMs and AI agents to automate data operations, validation, and decision-making.
5.1 LLM-Based Data Validation
Schema validation with LLMs:
from openai import OpenAI
class LLMDataValidator:
def __init__(self):
self.client = OpenAI()
def validate_annotation(self, annotation, schema):
"""Use LLM to validate annotation against schema"""
prompt = f"""
Validate this annotation against the schema:
Schema: {json.dumps(schema)}
Annotation: {json.dumps(annotation)}
Return JSON with:
- valid: boolean
- errors: list of error messages
- suggestions: list of improvement suggestions
"""
response = self.client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
Automated data quality checks:
class AutomatedQualityChecker:
def __init__(self):
self.llm = OpenAI()
def check_data_quality(self, dataset_sample):
"""Use LLM to identify data quality issues"""
prompt = f"""
Analyze this dataset sample and identify quality issues:
Sample: {json.dumps(dataset_sample[:100])}
Check for:
- Missing values
- Inconsistent formats
- Outliers
- Label errors
- Duplicates
Return JSON with issues found and severity (high/medium/low).
"""
response = self.llm.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
5.2 Intelligent Data Routing
Priority-based routing:
class IntelligentRouter:
def __init__(self):
self.llm = OpenAI()
def route_annotation_task(self, task):
"""Route task to appropriate annotator based on complexity"""
prompt = f"""
Analyze this annotation task and determine:
- Complexity (simple/medium/complex)
- Required expertise (general/domain-specific)
- Estimated time (seconds)
Task: {json.dumps(task)}
Return JSON with routing decision.
"""
response = self.llm.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
routing = json.loads(response.choices[0].message.content)
if routing["complexity"] == "simple":
return "junior_annotator_queue"
elif routing["complexity"] == "medium":
return "senior_annotator_queue"
else:
return "expert_annotator_queue"
Dynamic load balancing:
class DynamicLoadBalancer:
def __init__(self, annotator_pool):
self.annotators = annotator_pool
self.llm = OpenAI()
def balance_workload(self, pending_tasks):
"""Dynamically balance workload across annotators"""
# Get current workload
workloads = {a.id: a.get_pending_count() for a in self.annotators}
# Use LLM to optimize assignment
prompt = f"""
Optimize task assignment to annotators:
Pending tasks: {len(pending_tasks)}
Annotator workloads: {json.dumps(workloads)}
Assign tasks to minimize completion time while respecting:
- Annotator expertise
- Task complexity
- Fair workload distribution
Return JSON mapping task_id -> annotator_id.
"""
response = self.llm.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
5.3 Self-Healing Pipelines
Automatic error recovery:
class SelfHealingPipeline:
def __init__(self):
self.llm = OpenAI()
def diagnose_failure(self, error_log, pipeline_config):
"""Use LLM to diagnose pipeline failure"""
prompt = f"""
Diagnose this pipeline failure and suggest fixes:
Error log: {error_log}
Pipeline config: {json.dumps(pipeline_config)}
Identify:
- Root cause
- Immediate fix
- Long-term prevention
Return JSON with diagnosis and fix steps.
"""
response = self.llm.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
diagnosis = json.loads(response.choices[0].message.content)
# Apply automatic fix if possible
if diagnosis["can_auto_fix"]:
self.apply_fix(diagnosis["fix_steps"])
return diagnosis
def apply_fix(self, fix_steps):
"""Apply automatic fix"""
for step in fix_steps:
if step["type"] == "restart_component":
self.restart(step["component"])
elif step["type"] == "adjust_config":
self.update_config(step["config_path"], step["new_value"])
elif step["type"] == "retry_task":
self.retry(step["task_id"])
5.4 Agentic Pipeline Architecture
Pipeline Execution
↓
[1] Monitoring Agent
- Track metrics (throughput, latency, errors)
- Detect anomalies
- Log events
↓
[2] Diagnosis Agent
- Analyze errors with LLM
- Identify root cause
- Suggest fixes
↓
[3] Decision Agent
- Evaluate fix options
- Assess risk
- Decide: auto-fix vs. human intervention
↓
[4] Action Agent
- Apply automatic fixes
- Restart components
- Adjust configurations
↓
[5] Validation Agent
- Verify fix worked
- Check pipeline health
- Update knowledge base
6. Self-Service Tools for Data Teams
Purpose: Enable non-engineers (data scientists, annotators, PMs) to explore, query, and manage data without writing code.
6.1 Data Exploration Tools
Interactive dataset browser:
import streamlit as st
import pandas as pd
def dataset_browser():
"""Streamlit app for exploring datasets"""
st.title("Dataset Explorer")
# Load dataset manifest
dataset_id = st.selectbox("Select dataset", list_datasets())
manifest = load_manifest(dataset_id)
# Filter options
st.sidebar.header("Filters")
label_filter = st.sidebar.multiselect("Labels", manifest["labels"])
quality_threshold = st.sidebar.slider("Min quality", 0.0, 1.0, 0.5)
# Display samples
samples = filter_samples(manifest, label_filter, quality_threshold)
for sample in samples[:100]:
col1, col2 = st.columns(2)
with col1:
st.image(load_image(sample["image_path"]))
with col2:
st.write(f"**Label**: {sample['label']}")
st.write(f"**Quality**: {sample['quality_score']:.2f}")
st.write(f"**Metadata**: {sample['metadata']}")
Query builder for non-engineers:
class QueryBuilder:
def __init__(self):
self.filters = []
def add_filter(self, field, operator, value):
"""Add filter to query"""
self.filters.append({
"field": field,
"operator": operator,
"value": value
})
def build_sql(self):
"""Build SQL query from filters"""
where_clauses = []
for f in self.filters:
if f["operator"] == "equals":
where_clauses.append(f"{f['field']} = '{f['value']}'")
elif f["operator"] == "greater_than":
where_clauses.append(f"{f['field']} > {f['value']}")
# ... more operators
sql = f"SELECT * FROM dataset WHERE {' AND '.join(where_clauses)}"
return sql
def execute(self):
"""Execute query and return results"""
sql = self.build_sql()
return spark.sql(sql)
6.2 Dataset Versioning and Discovery
Dataset catalog:
class DatasetCatalog:
def __init__(self, db):
self.db = db
def register_dataset(self, dataset_info):
"""Register new dataset in catalog"""
self.db.insert("dataset_catalog", {
"dataset_id": dataset_info["id"],
"name": dataset_info["name"],
"description": dataset_info["description"],
"owner": dataset_info["owner"],
"modalities": dataset_info["modalities"],
"num_samples": dataset_info["num_samples"],
"size_gb": dataset_info["size_gb"],
"tags": dataset_info["tags"],
"created_at": now(),
"updated_at": now()
})
def search_datasets(self, query):
"""Search datasets by name, description, tags"""
results = self.db.query(
"""
SELECT * FROM dataset_catalog
WHERE name LIKE ? OR description LIKE ? OR tags LIKE ?
""",
f"%{query}%", f"%{query}%", f"%{query}%"
)
return results
Version tracking:
class DatasetVersioning:
def __init__(self, dataset_id):
self.dataset_id = dataset_id
def create_version(self, changes, description):
"""Create new dataset version"""
version_id = self.get_next_version()
self.db.insert("dataset_versions", {
"dataset_id": self.dataset_id,
"version": version_id,
"description": description,
"changes": json.dumps(changes),
"created_at": now(),
"parent_version": self.get_current_version()
})
return version_id
def diff_versions(self, v1, v2):
"""Compare two dataset versions"""
manifest1 = self.load_manifest(v1)
manifest2 = self.load_manifest(v2)
diff = {
"added": set(manifest2["sample_ids"]) - set(manifest1["sample_ids"]),
"removed": set(manifest1["sample_ids"]) - set(manifest2["sample_ids"]),
"modified": []
}
# Find modified samples
common = set(manifest1["sample_ids"]) & set(manifest2["sample_ids"])
for sample_id in common:
s1 = manifest1["samples"][sample_id]
s2 = manifest2["samples"][sample_id]
if s1 != s2:
diff["modified"].append(sample_id)
return diff
6.3 Data Catalog with Search
Full-text search:
from elasticsearch import Elasticsearch
class DataCatalogSearch:
def __init__(self):
self.es = Elasticsearch()
self.index = "dataset_catalog"
def index_dataset(self, dataset):
"""Index dataset for search"""
self.es.index(
index=self.index,
id=dataset["id"],
body={
"name": dataset["name"],
"description": dataset["description"],
"tags": dataset["tags"],
"modalities": dataset["modalities"],
"num_samples": dataset["num_samples"],
"owner": dataset["owner"],
"created_at": dataset["created_at"]
}
)
def search(self, query, filters=None):
"""Search datasets with filters"""
body = {
"query": {
"bool": {
"must": [
{"multi_match": {
"query": query,
"fields": ["name^3", "description", "tags^2"]
}}
],
"filter": []
}
}
}
if filters:
if "modalities" in filters:
body["query"]["bool"]["filter"].append(
{"terms": {"modalities": filters["modalities"]}}
)
if "min_samples" in filters:
body["query"]["bool"]["filter"].append(
{"range": {"num_samples": {"gte": filters["min_samples"]}}}
)
results = self.es.search(index=self.index, body=body)
return [hit["_source"] for hit in results["hits"]["hits"]]
7. Petabyte-Scale Data Patterns
Purpose: Handle datasets that exceed single-machine memory and require distributed processing.
7.1 Data Partitioning Strategies
Time-based partitioning:
# Partition by date for time-series data
def partition_by_time(df, time_col="timestamp"):
"""Partition DataFrame by time"""
return df.withColumn("partition_date", F.to_date(F.col(time_col))) \
.repartition("partition_date")
# Write partitioned
df.write \
.partitionBy("partition_date") \
.parquet("s3://data/features/")
# Read specific partition (efficient)
df = spark.read.parquet("s3://data/features/partition_date=2024-01-15/")
Hash-based partitioning:
# Partition by entity ID for consistent access patterns
def partition_by_entity(df, entity_col="user_id", num_partitions=1000):
"""Partition by hash of entity ID"""
return df.withColumn(
"partition_id",
F.abs(F.hash(F.col(entity_col))) % num_partitions
).repartition("partition_id")
# Write partitioned
df.write \
.partitionBy("partition_id") \
.parquet("s3://data/users/")
# Read specific partition (all data for one user)
df = spark.read.parquet("s3://data/users/partition_id=42/")
Range-based partitioning:
# Partition by value ranges (e.g., confidence scores)
def partition_by_range(df, col, ranges):
"""Partition by value ranges"""
conditions = []
for i, (low, high) in enumerate(ranges):
conditions.append(
F.when((F.col(col) >= low) & (F.col(col) < high), i)
)
return df.withColumn("partition_id", conditions[0]) \
.repartition("partition_id")
# Example: partition by confidence
ranges = [(0.0, 0.5), (0.5, 0.7), (0.7, 0.9), (0.9, 1.0)]
df = partition_by_range(df, "confidence", ranges)
7.2 Incremental Processing Patterns
Change Data Capture (CDC):
class CDCProcessor:
def __init__(self, checkpoint_path):
self.checkpoint_path = checkpoint_path
def process_incremental(self, source_path, target_path):
"""Process only new/changed data"""
# Read checkpoint
last_timestamp = self.load_checkpoint()
# Read new data
new_data = spark.read.parquet(source_path) \
.filter(F.col("updated_at") > last_timestamp)
if new_data.count() == 0:
return
# Process
processed = self.transform(new_data)
# Merge with existing (upsert)
processed.write \
.mode("overwrite") \
.partitionBy("date") \
.parquet(target_path)
# Update checkpoint
self.save_checkpoint(new_data.agg(F.max("updated_at")).first()[0])
Streaming with micro-batches:
# Spark Structured Streaming
query = spark.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "raw_data") \
.load() \
.select(F.from_json(F.col("value").cast("string"), schema).alias("data")) \
.select("data.*")
# Process each micro-batch
def process_batch(batch_df, batch_id):
# Transform
features = compute_features(batch_df)
# Write to online store
for row in features.collect():
redis.hset(f"features:{row.entity_id}", mapping=row.asDict())
query.writeStream \
.foreachBatch(process_batch) \
.option("checkpointLocation", "/checkpoints/stream") \
.start()
7.3 Cost Optimization at Scale
Tiered storage:
class TieredStorageManager:
def __init__(self):
self.s3 = boto3.client("s3")
def move_to_cold_storage(self, dataset_path, age_days=90):
"""Move old data to cheaper storage"""
cutoff_date = datetime.now() - timedelta(days=age_days)
# List objects
paginator = self.s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="ml-data", Prefix=dataset_path):
for obj in page.get("Contents", []):
if obj["LastModified"] < cutoff_date:
# Transition to Glacier
self.s3.restore_object(
Bucket="ml-data",
Key=obj["Key"],
RestoreRequest={"Days": 1}
)
# Or use lifecycle policies (better)
def setup_lifecycle_policy(self, bucket, prefix):
"""Set up automatic tiering"""
self.s3.put_bucket_lifecycle_configuration(
Bucket=bucket,
LifecycleConfiguration={
"Rules": [
{
"ID": "MoveToIAAfter30Days",
"Prefix": prefix,
"Status": "Enabled",
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"}
]
}
]
}
)
Compression strategies:
# Use Parquet with Snappy compression (good balance)
df.write \
.option("compression", "snappy") \
.parquet("s3://data/features/")
# For images: use WebP instead of PNG
def compress_image(image_path):
img = Image.open(image_path)
img.save(image_path.replace(".png", ".webp"), "WEBP", quality=85)
# For embeddings: use float16 instead of float32
embeddings_f16 = embeddings.astype(np.float16)
# Saves 50% storage, minimal accuracy loss
Spot instances for batch jobs:
# Use spot instances for fault-tolerant batch jobs
spark_conf = {
"spark.executor.instances": "100",
"spark.hadoop.fs.s3a.connection.maximum": "1000",
"spark.kubernetes.executor.request.cores": "4",
"spark.kubernetes.executor.limit.cores": "8",
# Use spot instances
"spark.kubernetes.node.selector.node.kubernetes.io/capacityType": "spot"
}
spark = SparkSession.builder \
.config(conf=spark_conf) \
.getOrCreate()
7.4 Multi-Region Data Replication
Cross-region replication:
class MultiRegionReplicator:
def __init__(self, regions):
self.regions = regions # ["us-east-1", "eu-west-1", "ap-southeast-1"]
def replicate_dataset(self, dataset_path):
"""Replicate dataset to all regions"""
for region in self.regions:
# Copy to region-specific bucket
target_bucket = f"ml-data-{region}"
# Use S3 batch operations for large datasets
self.s3.copy_object(
CopySource={"Bucket": "ml-data-us-east-1", "Key": dataset_path},
Bucket=target_bucket,
Key=dataset_path
)
def read_from_nearest_region(self, dataset_path):
"""Read from nearest region"""
# Determine nearest region based on latency
nearest = self.get_nearest_region()
bucket = f"ml-data-{nearest}"
return spark.read.parquet(f"s3://{bucket}/{dataset_path}")
8. Data Quality & Governance
Purpose: Ensure data is correct, complete, consistent, and compliant.
8.1 Data Quality Framework
Automated quality checks:
from great_expectations.core import ExpectationSuite
class DataQualityFramework:
def __init__(self):
self.suites = {}
def create_suite(self, dataset_name):
"""Create quality suite for dataset"""
suite = ExpectationSuite(f"{dataset_name}_quality")
# Schema validation
suite.add_expectation(
ExpectTableColumnsToMatchOrderless(
column_list=["image_id", "label", "confidence", "timestamp"]
)
)
# Value ranges
suite.add_expectation(
ExpectColumnValuesToBeBetween("confidence", min_value=0, max_value=1)
)
# Null checks
suite.add_expectation(
ExpectColumnValuesToNotBeNull("image_id", mostly=1.0)
)
# Uniqueness
suite.add_expectation(
ExpectColumnValuesToBeUnique("image_id")
)
self.suites[dataset_name] = suite
return suite
def validate(self, dataset_name, df):
"""Run quality checks"""
suite = self.suites[dataset_name]
results = suite.validate(df)
if not results["success"]:
# Send alerts
for failure in results["results"]:
if not failure["success"]:
send_alert(f"Quality check failed: {failure['expectation_config']}")
return results
8.2 Data Lineage Tracking
End-to-end lineage:
class DataLineageTracker:
def __init__(self, db):
self.db = db
def log_ingestion(self, source, destination, metadata):
"""Log data ingestion"""
self.db.insert("data_lineage", {
"source": source,
"destination": destination,
"operation": "ingestion",
"metadata": json.dumps(metadata),
"timestamp": now()
})
def log_transformation(self, input_datasets, output_dataset, transformation):
"""Log data transformation"""
self.db.insert("data_lineage", {
"source": json.dumps(input_datasets),
"destination": output_dataset,
"operation": "transformation",
"transformation": transformation,
"timestamp": now()
})
def trace_lineage(self, dataset_id):
"""Trace full lineage of a dataset"""
lineage = []
current = dataset_id
while current:
record = self.db.query(
"SELECT * FROM data_lineage WHERE destination = ?",
current
)
if record:
lineage.append(record)
current = record["source"]
else:
break
return lineage
def impact_analysis(self, dataset_id):
"""Find all downstream datasets affected by changes"""
impacted = []
queue = [dataset_id]
while queue:
current = queue.pop(0)
downstream = self.db.query(
"SELECT destination FROM data_lineage WHERE source LIKE ?",
f"%{current}%"
)
for ds in downstream:
impacted.append(ds)
queue.append(ds)
return impacted
8.3 Compliance & Privacy
PII detection and masking:
import re
class PIIDetector:
def __init__(self):
self.patterns = {
"email": r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+",
"phone": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",
"ssn": r"\b\d{3}-\d{2}-\d{4}\b",
"credit_card": r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b"
}
def detect_pii(self, text):
"""Detect PII in text"""
found = {}
for pii_type, pattern in self.patterns.items():
matches = re.findall(pattern, text)
if matches:
found[pii_type] = matches
return found
def mask_pii(self, text):
"""Mask PII in text"""
masked = text
for pii_type, pattern in self.patterns.items():
masked = re.sub(pattern, f"[{pii_type.upper()}]", masked)
return masked
# Apply to dataset
def mask_pii_in_dataset(df, text_columns):
detector = PIIDetector()
for col in text_columns:
df = df.withColumn(
f"{col}_masked",
F.udf(lambda x: detector.mask_pii(x) if x else None)(F.col(col))
)
return df
GDPR compliance:
class GDPRCompliance:
def __init__(self, db):
self.db = db
def handle_deletion_request(self, user_id):
"""Handle right to deletion"""
# Find all data for user
datasets = self.find_user_data(user_id)
# Delete from all datasets
for dataset in datasets:
self.delete_from_dataset(dataset, user_id)
# Log deletion
self.db.insert("gdpr_deletions", {
"user_id": user_id,
"datasets_affected": json.dumps(datasets),
"timestamp": now()
})
def export_user_data(self, user_id):
"""Handle data portability request"""
data = {}
datasets = self.find_user_data(user_id)
for dataset in datasets:
data[dataset] = self.extract_user_data(dataset, user_id)
# Export in standard format (JSON)
return json.dumps(data, indent=2)
9. Real-World Case Studies
Case Study 1: Apple Vision Pro Training Data Pipeline
Challenge: Build training dataset for hand tracking model with 1B+ images
Architecture:
Raw video captures (100K hours)
↓
[1] Frame extraction (Spark, 1000 cores)
- Extract 30 fps → 10B frames
- Deduplicate (perceptual hashing)
- Filter low-quality (blur, occlusion)
↓
[2] Annotation pipeline
- 500 annotators (Scale AI)
- 2D keypoints (21 points per hand)
- 3D pose estimation
- Quality assurance (3x annotation, consensus)
↓
[3] Synthetic augmentation
- Generate edge cases (occlusion, unusual poses)
- 3D rendering (Blender, 10M synthetic samples)
- Domain adaptation (sim-to-real transfer)
↓
[4] Training data assembly
- Point-in-time join (video metadata + annotations)
- Train/val/test split (temporal, not random)
- Balance by hand pose, lighting, occlusion
↓
[5] Model training
- Distributed training (1000 GPUs)
- Mixed precision (FP16)
- Gradient accumulation
↓
[6] Evaluation & deployment
- Latency profiling (<10ms inference)
- Accuracy on edge cases
- A/B testing in production
Scale: 10B frames, 500M annotated, 100M training samples
Latency: 2 weeks from raw video to trained model
Cost: $500K (annotation) + $200K (compute) = $700K total
Case Study 2: Siri Speech Recognition Data Pipeline
Challenge: Continuous improvement of speech recognition with billions of audio samples
Architecture:
User audio (anonymized, opt-in)
↓
[1] Ingestion pipeline (Kafka, Flink)
- Stream 1M audio samples/hour
- Real-time quality filtering (SNR, duration)
- Deduplication (audio fingerprinting)
↓
[2] Transcription pipeline
- Auto-transcribe with existing model (Whisper)
- Human review (sample 10% for quality)
- Active learning (select uncertain samples)
↓
[3] Enrichment pipeline
- Speaker diarization (who is speaking)
- Language identification
- Accent classification
- Emotion detection
↓
[4] Storage (Iceberg on S3)
- Partition by date, language, accent
- Columnar format (Parquet)
- Metadata catalog (Hive metastore)
↓
[5] Training data assembly
- Sample by diversity (accent, language, noise)
- Balance by use case (commands, dictation, queries)
- Temporal split (train on old, test on new)
↓
[6] Continuous training
- Daily incremental training
- Weekly full retraining
- A/B testing in production
↓
[7] Monitoring
- Drift detection (hourly)
- Accuracy by accent/language
- Latency profiling
Scale: 10B audio samples, 50 languages, 100 accents
Latency: 1 hour from audio capture to model update
Cost: $2M/month (storage + compute + annotation)
10. Key Takeaways for Your Interview
Technical Depth
Data annotation:
- Understand active learning (uncertainty, diversity, core-set sampling)
- Know inter-annotator agreement metrics (Cohen’s kappa, IoU)
- Be familiar with annotation tools (Label Studio, CVAT, Scale AI)
Synthetic data:
- Know generation techniques (GANs, diffusion models, 3D rendering)
- Understand quality validation (FID, CLIP score, diversity metrics)
- Be able to discuss lineage tracking (which model generated which data)
Multimodal data:
- Understand alignment challenges (video+audio, image+text)
- Know storage patterns (object storage + metadata DB, WebDataset, HDF5)
- Be familiar with cross-modal models (CLIP, Whisper)
Petabyte-scale:
- Understand partitioning strategies (time, hash, range)
- Know incremental processing (CDC, streaming)
- Be able to discuss cost optimization (tiered storage, compression, spot instances)
System Design
Be ready to design:
- Annotation pipeline: How to annotate 1B images efficiently
- Synthetic data pipeline: How to generate and validate synthetic data
- Multimodal dataset: How to store and query image+text+audio data
- Drift detection: How to detect and respond to data drift in production
Key tradeoffs:
- Batch vs. streaming (latency vs. complexity)
- Manual vs. automated annotation (quality vs. cost)
- Real vs. synthetic data (diversity vs. distribution shift)
- Centralized vs. decentralized data ownership (consistency vs. autonomy)
Behavioral Questions
Be ready to discuss:
- A time you scaled a data pipeline to handle 10x more data
- A time you improved data quality and measured the impact
- A time you mentored junior engineers on data engineering best practices
- A time you influenced a partner team’s roadmap to build a feature you needed
Show impact:
- Quantify everything (10B samples, 50% cost reduction, 2x faster pipeline)
- Focus on business outcomes (model accuracy improved 5%, latency reduced 30%)
- Highlight leadership (led team of 5, influenced 3 partner teams)
11. Questions to Ask Them
Technical:
- What’s the current scale of your data pipelines (samples/day, storage size)?
- What annotation tools do you use (in-house or third-party)?
- How do you handle synthetic data generation and validation?
- What’s your approach to data quality monitoring and drift detection?
Process:
- How do you prioritize data infrastructure work vs. feature development?
- How do you collaborate with R&D teams on data requirements?
- What’s your approach to mentoring and growing engineers?
Challenges:
- What are the biggest data challenges you’re facing right now?
- What does success look like for this role in the first 6 months?
- How do you balance short-term deliveries with long-term infrastructure improvements?
Final advice: This role is about data operations at scale, not just ML infrastructure. Focus on:
- Annotation workflows (manual, semi-automated, active learning)
- Synthetic data generation (GANs, diffusion models, quality validation)
- Multimodal data handling (vision, audio, text, alignment)
- Petabyte-scale patterns (partitioning, incremental processing, cost optimization)
- Data quality & governance (lineage, compliance, monitoring)
You’ve got this! 🚀