AI Facies Classification
A collaborative seismic segmentation system that compares U-Net variants and packages inference behind a web and API deployment.
- Role
- Model training, preprocessing and augmentation pipelines, hyperparameter search, and the Flask/FastAPI service and container.
- Status
- Delivered
- Team
- Built at Deepkapha AI Lab with one collaborator
- Stack
- PyTorch, Flask, FastAPI, Docker
The strongest of the three backbones. ResNet-50 reached 96.7% and ResNet-34 93.5% on the same task; the three-model mean is 95.8%, which is what the résumé's 95.7% figure refers to.
Upper, Middle and Lower North Sea, Rijnland/Chalk, Scruff and Zechstein, predicted pixelwise across a seismic slice.
Overview
Seismic surveys produce vertical slices through rock. Reading them — deciding which bands correspond to which geological formation — is expert, slow work. Built at Deepkapha AI Lab, this treats it as pixelwise segmentation: every pixel in a slice is assigned to one of six facies classes, and the output is both a class map and a proportional breakdown a geologist can act on.
Collaboration
Built with Mazen Sakr (opens in a new tab). I owned the model training, preprocessing and augmentation pipelines, the hyperparameter search, and the service and container work.
Problem
Translate a seismic slice into a six-class facies map — Upper, Middle and Lower North Sea, Rijnland/Chalk, Scruff and Zechstein — and make the result inspectable by someone who is not going to run a notebook.
Dataset
The Dutch F3 block, via the yalaudah/facies_classification_benchmark distribution. It is a
standard public benchmark for this task, which is what makes the six-class label set
meaningful rather than arbitrary.
Model family
U-Net-style decoders over three pretrained encoders — ResNet34, ResNet50 and InceptionV3. The encoder is selectable per request, which was originally a convenience for comparison and turned out to be the most useful thing in the interface: seeing three models disagree about the same slice is more informative than seeing one model's confident answer.
Inference architecture
Grayscale resize to 99×99 → model → per-pixel logits → class map and class proportions, plus a mean pixel-confidence summary.
I chose the small inference resolution deliberately: it keeps responses fast and made the service viable on constrained hosting. Serving at the training resolution is the first upgrade I would make now that the hosting constraint no longer binds.
Application architecture
A Flask interface mounted behind FastAPI through WSGIMiddleware, so one process serves
both the human-facing upload form and POST /api/predict returning JSON.
Models load lazily in a background thread on first request rather than at import, so the service starts fast. Requests arriving before the weights are resident get a clean 503 rather than a timeout, which makes a cold start degrade predictably.
Deployment
A multi-stage Docker build with Git LFS-managed weights, configured to work across Koyeb and Railway. Getting multi-hundred-megabyte weights through an LFS-aware container build was the most intricate part of shipping this, and the Dockerfile is where that work lives.
Technical documentation
Three architectures were trained and compared, each tuned separately rather than sharing one configuration.
ResNet-34
A 34-layer residual network. Skip connections and identity mappings make it the cheapest of the three to run, which is what made it the candidate for real-time analysis.
| Depth | 34 convolutional layers |
| Input | 224×224 |
| Learning rate | 0.0001 |
| Batch size | 64 |
| Optimiser | Adam |
| Search strategy | Grid — batch size 16 / 32 / 64 / 128 against learning rate 0.01 / 0.001 / 0.0001 / 1e-4 |
ResNet-50
Bottleneck blocks give a richer feature representation, which matters on the more tangled geological structures where the shallower network flattens detail.
| Depth | 50 convolutional layers |
| Parameters | 25.6M |
| Input | 224×224 |
| Learning rate | 0.0001 |
| Weight decay | 1e-5 |
| Batch size | 64 |
| Optimiser | Adam |
| Search strategy | Random — plus dropout 0.1 through 0.5, and weight decay |
InceptionV3
Parallel convolution paths process several scales at once. Seismic facies boundaries are a multi-scale problem — thin laminations and thick formations in the same slice — so this was the architecture I expected to win, and it did.
| Depth | 48 layers |
| Parameters | 23.8M |
| Input | 299×299 |
| Learning rate | 0.0001 |
| Batch size | 64 |
| Optimiser | Adam |
| Search strategy | Random |
Data pipeline
Loading → patch extraction → per-model resolution adjustment. Augmentation is random horizontal and vertical flips, random rotation, and injected image noise. Training uses Adam, a predefined set of class weights, and cross-entropy loss with selective weight decay.
The class weights matter more than they look: facies are not evenly distributed in F3, and without weighting, a model maximises accuracy by learning the two most common formations well and the rest barely at all.
Results
| Model | Average accuracy |
|---|---|
| ResNet-34 | 93.5% |
| ResNet-50 | 96.7% |
| InceptionV3 | 97.2% |
The three-model mean is 95.8%, which is where the frequently-quoted 95.7% figure comes from.
Two honest caveats. First, the documentation reports these as average accuracy without saying what is being averaged — test volumes, classes, or pixels. On an imbalanced segmentation task those three numbers can differ by a lot, so I would trust the ranking between the models further than the absolute values.
Second, accuracy is the wrong headline metric for segmentation regardless. Per-class IoU is what tells a geologist whether the model can find Zechstein, and that is not in the numbers above.
Where I would take this next
- Serve at training resolution. The models are tuned at 224×224 and 299×299; matching that in the inference path is straightforward accuracy to reclaim.
- A model-readiness health check, so
/healthreports weight residency rather than process liveness and an orchestrator can route traffic precisely. - Per-class IoU alongside accuracy. On segmentation this is what tells a geologist whether the model finds Zechstein specifically, and it is the metric I would add first.
- Quantisation or a lighter backbone, so the strongest model fits comfortably on modest hosting.
- Spatially blocked splits. Adjacent seismic slices are highly correlated, and blocking the split by region makes the evaluation stricter still.
- A test suite around the inference path and the API contract, now that the service has a stable shape worth pinning.
A recorded walkthrough is in production and will sit alongside the documentation above.
Constraints
- Model weights large enough to strain free-tier hosting
- Inference has to survive a cold start without failing requests
- One container has to serve both a human interface and a machine API
Credits
Artifacts
- PRESENTATIONProject presentation (opens in a new tab)
All three architectures, the hyperparameter search and the per-backbone results.
- VIDEORecorded walkthroughComing soon
In production.
Notes on evidence
- Built at Deepkapha AI Lab with Mazen Sakr. The credit above is not a formality.
- Accuracy figures are averages across the geological test volumes.