Case Studies · Applied ML

Applied ML

Five Projects, Five Domains

Generative modeling, speech recognition, image restoration, depth estimation, and low resource NLP. Every number on this page is pulled directly from the actual notebooks and leaderboards.

32×32 Image Synthesis with WGAN-GP

Role: Solo · Leaderboard FID 74.894 · Rank 60 of 128

The task was to train a generative model to produce realistic 32×32 images matching a hidden dataset's distribution, then submit exactly 1000 generated samples as Inception v3 feature vectors.

The Metric: Frechet Inception Distance

FID runs both generated and real reference images through a pretrained InceptionV3 network, then compares the mean and covariance of those feature distributions. Lower is better, a FID of 0 means generated images are statistically indistinguishable from real ones. It punishes both poor image quality and poor diversity, a model that produces one perfect image on repeat scores badly, same as one that produces garbage.

The data problem came before the modeling problem. The raw dataset was 60 JSONL shards of base64 encoded images across two pixel formats (16 bit grayscale and RGBA), each with its own rotation metadata and an inversion flag, some needing an alpha mask crop before the image was usable. I wrote a single decode function handling every case, recovering 58,852 usable training images.

Architecture: a 0.99M parameter generator (256 dim latent vector, 3 residual upsampling blocks) and a 2.76M parameter discriminator with spectral normalization on every conv layer plus a minibatch standard deviation layer to fight mode collapse.

Why WGAN-GP instead of a standard GAN: standard GANs are notoriously unstable, mode collapse and vanishing gradients are common failure modes. I used the Wasserstein formulation with a gradient penalty (lambda 10) instead of standard adversarial loss, trained the critic 3 steps for every 1 generator step, and maintained a separate EMA copy of the generator weights (beta 0.999) for evaluation, EMA weights are consistently smoother than raw training weights at any single step.

I implemented FID computation from scratch rather than relying on a library, passing images through pretrained InceptionV3 and computing the distance via matrix square root decomposition, checked every 5 epochs against 8000 real reference images.

Training
50 epochs, batch size 128, best internal proxy FID 15.87 at epoch 45
Stabilization
Gradient penalty, spectral norm, minibatch stddev, EMA
Note
Internal proxy FID and official leaderboard FID use different reference sets, the gap is expected
74.894
Leaderboard FID
Rank 60 of 128
What I'd say in an interview

The generative modeling was the easy part to talk about. The harder and more interesting problem was building a reliable decode pipeline for a genuinely messy multi format dataset, and choosing WGAN-GP with spectral normalization and EMA specifically because standard GAN training is so failure prone.

Automatic Speech Recognition for Uyghur

Role: Solo · Leaderboard CER 0.138 · Rank 40 of 93

Uyghur is a low resource language, very little existing labeled speech data, few pretrained models built for it specifically. The task: transcribe roughly 24 hours of Uyghur audio (7,574 training clips, 1,894 test clips, 16kHz mono) into text, with no restrictions on approach.

The Metric: Character Error Rate

CER is the Levenshtein distance between predicted and correct text, the minimum number of single character insertions, deletions, and substitutions needed to turn one into the other, divided by the length of the correct transcription. A CER of 0.138 means roughly 14% of characters would need editing to reach the ground truth. Zero is a perfect transcription.

I fine-tuned facebook/wav2vec2-base-960h, a self supervised speech model pretrained on English, adapting it to Uyghur via CTC loss. Cross lingual fine-tuning like this, adapting an English pretrained acoustic model to a completely different language's phoneme inventory, is the standard approach for low resource ASR, and it only works if the fine-tuning and decoding are both handled carefully.

The more interesting decision was the decoding strategy. A CTC model's raw output is greedy, frame by frame character prediction, fast but leaves accuracy on the table. I built a beam search decoder with an external Uyghur language model (a KenLM n-gram model) for rescoring, instead of taking the most likely character at each timestep, the decoder considers multiple candidate sequences and uses the language model to favor ones that are actually plausible Uyghur text.

Being upfront about the notebook: the saved file contains an early sanity check run, 10% of the data, 1 epoch, that hit a numerically unstable NaN loss and a resulting CER near 1.0. That number is not representative, it's a broken debug run. The actual submitted CER of 0.138 came from the fuller pipeline, full data fine-tuning plus the language model decoding step, whose output wasn't captured in the saved file. The file documents real iteration: several abandoned attempts, a debugged restart, and a decoder upgrade.

0.138
Leaderboard CER
Rank 40 of 93 teams
What I'd say in an interview

The acoustic model fine-tuning is fairly standard practice for low resource ASR. What actually moved the needle was pairing it with a language model based beam search decoder instead of shipping greedy decoding. I'd also be upfront that getting there involved a real debugging cycle, a NaN loss from a mixed precision issue isn't unusual in early ASR fine-tuning, and knowing how to diagnose and route around it is as much the skill as the architecture choice itself.

4× Image Super Resolution

Role: Solo · Leaderboard score 38.044 · Rank 50

Take noisy, low light images and produce a version that's both denoised and 4× the resolution, without inventing detail that isn't there. 1,105 training pairs, 267 validation pairs, 60 test images.

The Metric: PSNR Style Scoring

Scoring is on a PSNR (Peak Signal to Noise Ratio) style metric, comparing predicted pixel values against ground truth on a logarithmic decibel scale. Higher is better. Because it's computed per pixel across the whole image, a model that's mostly right but blurs fine detail scores worse than one that reconstructs texture precisely, even if both look similar to a casual glance.

I built a lightweight EDSR (Enhanced Deep Super Resolution) network from scratch, 8 residual blocks, 32 feature channels, PixelShuffle for the 4× upsampling, about 0.3 million parameters, deliberately small to avoid overfitting a dataset this size.

The loss function mattered more than the architecture. Instead of plain MSE, I wrote a composite loss aimed directly at the scoring metric: 75% weight on L1 loss on the Y (luminance) channel, 20% on RGB L1, 5% on an SSIM term, 2% on a Sobel gradient magnitude term to keep edges sharp.

Training ran in three phases, a 15 sample sanity check, an 80% split refinement, then the full dataset at 192px patches. The main 60 epoch run took validation PSNR from 38.10 dB to 38.31 dB. After that, I resumed from the best checkpoint for a further 12 epoch low learning rate fine-tune, pushing it to 38.665 dB, the kind of small deliberate gain most people skip once they've hit the first plateau.

Stabilization
Mixed precision, EMA, SWA, gradient clipping, cosine annealing
Inference
Tiled inference with overlap averaging, 8× test time augmentation
Note
Final submission used the single best checkpoint, not the full ensemble, which scored slightly higher on validation only
38.044
Leaderboard score
Rank 50
What I'd say in an interview

The interesting engineering decision here wasn't the architecture, EDSR is a known, fairly standard choice. It was writing a loss function that directly targets the evaluation metric instead of a generic reconstruction loss, and the discipline of the fine-tune phase after the main run had already plateaued.

Depth Estimation from Degraded Images

Role: Solo · Leaderboard RMSE 22.855 · Rank 15 of 42

Standard depth estimation assumes clean, well lit RGB input. This one removed that assumption, every image had a real degradation baked in, low light, sensor noise, or both, and the model had to predict an accurate per pixel depth map anyway. 6,686 training pairs, 836 validation, 836 test.

The Metric: Root Mean Squared Error

For every pixel, the squared difference between predicted and true depth, averaged, then square rooted. RMSE is unforgiving of large individual errors, one badly wrong region in a depth map hurts the score more than many small consistent errors would. Since depth maps are dense, every pixel counts, there's no partial credit for being mostly right.

A U-Net with a pretrained timm-efficientnet-b0 encoder as the backbone, single channel output through a sigmoid. U-Net's encoder decoder structure with skip connections is the standard choice for dense prediction, the skip connections preserve the spatial detail needed to place depth boundaries precisely, detail a plain encoder-decoder without skips would lose.

The subtle part was the data handling. Depth maps came in two different bit depths, some 8 bit, some 16 bit, so I checked each mask's max value and normalized against 255 or 65535 accordingly rather than assuming one format. Get this wrong and half the training depth targets are silently scaled incorrectly, with no error message to catch it.

10 epochs, MSE loss, Adam at 1e-4. Validation RMSE dropped steadily every single epoch, 0.0974 down to 0.0730, flattening exactly at the point you'd expect, no overfitting spike, no instability.

Augmentation
Light touch: horizontal flip, brightness and contrast jitter only
Submission
Predictions converted to the required CSV format via resize and per image renormalization
Note
Internal RMSE and leaderboard RMSE use different scales due to that renormalization step
22.855
Leaderboard RMSE
Rank 15 of 42 teams
What I'd say in an interview

The part worth highlighting isn't the U-Net choice, that's standard for this task. It's the dataset handling, silently mis-normalizing 16 bit depth maps as if they were 8 bit is a bug that produces a model that trains and runs without any errors, just quietly wrong, exactly the kind of mistake that's invisible until you check for it.

Multi-Script Emotion Classification with Gemma-3

Role: Solo · First hands on deep learning project · Did not finish in time

Emotion classification across three genuinely low resource languages, each in a different, non Latin script: Santali (Ol Chiki), Kashmiri (Arabic script), and Manipuri (Meitei Mayek). The rules mandated a specific base model, Gemma-3-1B-IT, fine-tuning required, no substituting a different pretrained model, and no machine translating into a higher resource language to dodge the hard part. That last rule mattered, translating into English first would have been the obvious shortcut, and it was explicitly banned.

The Metric: Macro F1

Macro F1 computes F1 separately for each of the 6 emotion classes, then averages them equally, regardless of how common each class is in the data. This matters here because the classes weren't perfectly balanced, fear made up 23% of samples, disgust only 13%, and macro F1 refuses to let strong performance on the common classes hide weak performance on the rare ones.

I loaded Gemma-3-1B-IT in 8 bit quantization and attached a LoRA adapter (rank 8, alpha 32, targeting the query, key, and value projection matrices), a correct, standard practice way to fine-tune a language model on limited compute without touching all 1B plus parameters directly.

Before touching the model, I checked how the tokenizer actually handled these scripts. One Manipuri sentence with 12 words tokenized into 150 tokens, a roughly 12x blowup. That's a concrete signal Gemma's tokenizer wasn't built with Meitei Mayek in mind, and it directly shapes decisions like sequence length limits and expected training time.

I framed the task as text generation rather than classification, each example became a prompt with the target emotion word as the generation target, and wrote a custom macro F1 function that decodes the model's generated tokens back to text and scores them against true labels, matching the actual evaluation metric exactly rather than relying on a generic loss number.

Where it stopped: training never finished, there's no output from that cell at all, and everything downstream, generating predictions, writing the submission file, is still commented out, exactly where I left off when the deadline hit. This was my first hands on deep learning project, and I was still learning how much runtime an 8 bit LoRA fine-tune of a 1B parameter model actually needs against a 3 day window. I worked on it until the literal last second, but didn't leave enough time margin for training and inference both.

What I'd say in an interview

I'd rather talk through this one honestly than pretend it finished. The setup, quantization choice, LoRA target modules, prompt format, and the custom macro F1 metric, was all correct, and the tokenizer inspection step is exactly the kind of check I'd want a junior engineer to think to do before committing to a training run. What I'd do differently now: budget backwards from the deadline for training time before writing any code, instead of building the full pipeline first and discovering the time crunch at the end.

Five domains, one habit: check what's actually true before trusting the number

Every metric on this page was cross checked against the actual notebook output and leaderboard data before being written down, including the one that didn't have a happy ending. Happy to walk through any of these in more depth.