|
| 1 | +[](https://www.python.org/) |
| 2 | +[](https://pytorch.org/) |
| 3 | +[](https://huggingface.co/) |
| 4 | +[](https://pytorch.org/) |
| 5 | +[](LICENSE) |
| 6 | +[](tests/) |
| 7 | + |
| 8 | + |
| 9 | +# Facial Expression Recognition |
| 10 | + |
| 11 | +A compact end-to-end training and evaluation pipeline for facial expression classification using a Hugging Face dataset and a ResNet50 backbone. |
| 12 | + |
| 13 | +## Installation |
| 14 | + |
| 15 | +1. Create and activate a Python virtual environment (recommended): |
| 16 | + |
| 17 | +```bash |
| 18 | +python3 -m venv .venv |
| 19 | +source .venv/bin/activate |
| 20 | +``` |
| 21 | + |
| 22 | +2. Install dependencies: |
| 23 | + |
| 24 | +```bash |
| 25 | +pip install -U pip |
| 26 | +pip install -r requirements.txt |
| 27 | +``` |
| 28 | + |
| 29 | +## Quick run |
| 30 | + |
| 31 | +Run the full pipeline (download dataset, train, evaluate, export metrics and plots): |
| 32 | + |
| 33 | +```bash |
| 34 | +python main.py --epochs 1 --batch_size 8 |
| 35 | +``` |
| 36 | + |
| 37 | +Configuration options are available via CLI flags in `main.py` or by calling `run_pipeline(config)` in `src/pipeline.py`. |
| 38 | + |
| 39 | +Downloaded dataset artifacts are saved to `data/raw` by `src/data_loader.DataLoader.download()` using Hugging Face `datasets.save_to_disk()`. |
| 40 | + |
| 41 | +## Running tests |
| 42 | + |
| 43 | +Unit tests use `pytest` and are located in the `tests/` folder. Run them with: |
| 44 | + |
| 45 | +```bash |
| 46 | +pytest -q |
| 47 | +``` |
| 48 | + |
| 49 | +Key tests: |
| 50 | +- `tests/test_data_loader.py`: dataset download/load and DataLoader constructions (uses mocks) |
| 51 | +- `tests/test_facial_recognition.py`: model shapes and frozen backbone checks |
| 52 | +- `tests/test_trainer.py`: training loop behaviours |
| 53 | + |
| 54 | +## Project layout |
| 55 | + |
| 56 | +Top-level files and folders: |
| 57 | + |
| 58 | +- `main.py`: CLI entrypoint to run the pipeline |
| 59 | +- `src/`: application code |
| 60 | + - `data_loader.py`: dataset download/persistence and PyTorch `DataLoader` wrapping |
| 61 | + - `paths.py`: project path constants (e.g., `data/raw`) |
| 62 | + - `pipeline.py`: orchestrates data download, training, evaluation, artifact export |
| 63 | + - `trainer.py`: training loop that consumes a `DataProvider` (returns a PyTorch `DataLoader`) |
| 64 | + - `evaluator.py`: evaluation helpers and artifact export (metrics, confusion matrix) |
| 65 | + - `models/`: model definitions (ResNet50 backbone + classifier head) |
| 66 | +- `data/`: storage for raw and processed datasets |
| 67 | + - `data/raw/`: persisted Hugging Face dataset (created by `save_to_disk()`) |
| 68 | + - `data/processed/`: optional processed artifacts |
| 69 | +- `outputs/`: saved artifacts: `metrics.json`, `confusion_matrix.png`, etc. |
| 70 | +- `tests/`: unit tests |
| 71 | +- `requirements.txt` and `requirements-dev.txt` |
| 72 | + |
| 73 | +## Architecture & data flow |
| 74 | + |
| 75 | +1. Data download & persistence |
| 76 | + - `DataLoader.download()` calls `datasets.load_dataset(REPO_ID)` and then `dataset.save_to_disk(data/raw)`. |
| 77 | +2. Data loading & transforms |
| 78 | + - `DataLoader.load()` uses `load_from_disk(data/raw)`. |
| 79 | + - `HuggingFaceImageDataset` converts HF rows to PIL/Numpy images, applies `Grayscale -> ToTensor -> Normalize` transforms, and returns `(image_tensor, label)`. |
| 80 | +3. Model |
| 81 | + - Backbone: pretrained `resnet50` (most layers frozen except `layer4` by default). |
| 82 | + - Embedding head: `Linear(in_features, embedding_size)` followed by `ReLU`. |
| 83 | + - Classifier head: `Linear(embedding_size, num_classes)` returning logits for `CrossEntropyLoss`. |
| 84 | +4. Training |
| 85 | + - `Trainer.fit()` fetches `train_loader` and runs forward → loss (`CrossEntropyLoss`) → backward → optimizer.step(). |
| 86 | +5. Evaluation |
| 87 | + - `Evaluator` runs model on the test loader, computes metrics and writes `outputs/metrics.json` and `outputs/confusion_matrix.png`. |
| 88 | + |
| 89 | +## Rationale: classification head & choice of loss |
| 90 | + |
| 91 | +This project treats facial expression recognition as a supervised multi-class classification task because the dataset provides per-image categorical labels (e.g., happy, sad, angry). The model uses a small classifier head on top of a pretrained ResNet50 backbone and is trained with `nn.CrossEntropyLoss`. Reasons for this design: |
| 92 | + |
| 93 | +- **Direct supervision and metrics:** `CrossEntropyLoss` expects raw class logits and pairs naturally with evaluation metrics like accuracy, precision, and F1, making progress easy to interpret. |
| 94 | +- **Numerical stability and simplicity:** `CrossEntropyLoss` implements `log_softmax` + `nll_loss` in a stable, optimized form and is the standard choice for multi-class classification. |
| 95 | +- **Practicality and reproducibility:** A classification head requires less engineering than metric-learning pipelines (which need careful positive/negative mining or contrastive sampling) and trains efficiently using standard PyTorch optimizers. |
| 96 | + |
| 97 | +Alternative (embedding / metric-learning) approaches have advantages for retrieval, few-shot learning, or when labels are unreliable, but they require different losses (contrastive, triplet, NT-Xent), different sampling strategies, and different evaluation protocols. I opted for a classifier-first approach to match the dataset's supervised labels and to keep the pipeline simple and reproducible. |
| 98 | + |
| 99 | +Common causes for unexpectedly large loss (what to check): |
| 100 | + |
| 101 | +- **Model-output vs. loss mismatch:** returning normalized embeddings while using `CrossEntropyLoss` will produce meaningless loss values. Ensure model outputs are logits with shape `(batch_size, num_classes)`. |
| 102 | +- **Label issues:** verify labels are integer `torch.long` values in the range `[0, num_classes-1]`. |
| 103 | +- **Shape/dtype mismatches:** confirm `outputs.shape` and `targets.shape` match expectations and dtypes are correct. |
| 104 | +- **Data problems:** corrupted images, missing data, or incorrect normalization can destabilize training. |
| 105 | +- **Optimization settings:** too-large learning rates, incorrect optimizer setup, or missing gradient zeroing can cause loss explosion. |
| 106 | +- **Numerical instability:** NaNs in inputs/outputs or extremely large activations (inspect `torch.isnan()` and output statistics). |
| 107 | + |
| 108 | +If you'd like to experiment with embeddings instead, I can add a configurable option to switch between `CrossEntropyLoss` and a metric loss, implement simple contrastive sampling, or add runtime checks/logging to `Trainer.fit()` to surface the most common issues. |
| 109 | + |
| 110 | + |
| 111 | +## Debugging tips |
| 112 | + |
| 113 | +- Print shapes and types for a single batch: |
| 114 | + |
| 115 | +```py |
| 116 | +print(inputs.shape, inputs.dtype) |
| 117 | +print(targets.shape, targets.dtype, targets.min(), targets.max()) |
| 118 | +``` |
| 119 | + |
| 120 | +- Inspect model outputs: |
| 121 | + |
| 122 | +```py |
| 123 | +o = model(inputs) |
| 124 | +print(o.shape, o.mean().item(), o.std().item(), torch.isnan(o).any()) |
| 125 | +``` |
| 126 | + |
| 127 | +- Check loss value for a single batch: |
| 128 | + |
| 129 | +```py |
| 130 | +loss = criterion(o, targets) |
| 131 | +print(loss.item()) |
| 132 | +``` |
0 commit comments