Multi-Label Emotion Classification: Scratch BiLSTM vs Fine-Tuned Transformers
Course Code: DLGenAI β IIT Madras BS Degree in Data Science and Applications (September 2025 term)
π Live app: huggingface.co/spaces/23f1000805/DLGenAI-project-deployment
This project detects five emotions in short text spans:
anger Β· fear Β· joy Β· sadness Β· surprise
It's a multi-label problem β a sample can carry more than one emotion at once β so every model outputs independent sigmoid probabilities per label (BCEWithLogitsLoss), not a single softmax class.
Three models are trained and compared on the same train/validation split:
| Model | Type | What it tests |
|---|---|---|
| Scratch BiLSTM | Embedding + BiLSTM, trained from zero | How far a simple recurrent architecture gets with no pretraining |
bert-base-uncased |
Fine-tuned transformer | How much pretraining buys you on the same data |
roberta-base |
Fine-tuned transformer | Whether RoBERTa's pretraining recipe adds further gains |
The pipeline is Kaggle-native: it reads competition data from /kaggle/input/..., pulls secrets via kaggle_secrets.UserSecretsClient, and writes all artifacts to /kaggle/working/project_outputs. Trained models are optionally versioned and pushed to KaggleHub, then re-downloaded for inference and final deployment to Hugging Face Spaces.
Gaurav Tomar
Student ID: 23f1000805
Program: BS Degree in Data Science and Applications β IIT Madras
DLGenAI-Project-t32025/
β
βββ README.md
β
βββ data/
β βββ 2025-sep-dl-gen-ai-project.zip # raw competition archive
β βββ train.csv
β βββ test.csv
β βββ sample_submission.csv
β
βββ notebooks/
β βββ dl-23f1000805-notebook-t32025 (1).ipynb
β βββ dl-23f1000805-notebook-t32025 (2).ipynb
β
βββ scripts/ # imported inside Kaggle as `src.*`
β βββ config.py # Kaggle secrets, device, W&B setup, label list
β βββ data_loader.py # CSV loading + train/val split
β βββ vocab_scratch.py # vocab builder + tokenizer + Dataset for BiLSTM
β βββ scratch_model.py # SimpleBiLSTM architecture
β βββ scratch_train.py # training loop for the scratch model
β βββ transformer_train.py # BERT / RoBERTa fine-tuning via HF Trainer
β βββ train.py # entry point: trains all 3 models in sequence
β βββ inference.py # loads trained models, runs predictions
β βββ reporting.py # metrics summary + plots + observations
β βββ uploader.py # push trained models to KaggleHub
β
βββ project_outputs/
β βββ submission_scratch (4).csv
β βββ submission_bert_base_uncased (1).csv
β βββ submission_roberta_base (1).csv
β
βββ report/
βββ 23f1000805_DG_T32025 (1).pdf # final written project report
Note on imports: the scripts use
from src.config import ...etc. Inside Kaggle, thescripts/folder is copied/mounted assrc/in the working directory (or added tosys.pathunder that name) beforetrain.py/inference.pyare run β that's why the folder is namedscripts/on disk but imported assrcin code.
- Reads
KAGGLE_USERNAMEandWANDB_API_KEYfrom Kaggle Secrets (UserSecretsClient) - Picks
cudaif available, elsecpu - Defines the label order:
["anger", "fear", "joy", "sadness", "surprise"] - Logs into Weights & Biases if a key is present, otherwise falls back to offline W&B mode so training never hard-fails on a missing key
- All outputs go to
/kaggle/working/project_outputs
- Vocabulary: whitespace tokenization, top 10,000 most frequent words,
<pad>/<unk>reserved - Sequences padded/truncated to max_len = 50
- Architecture:
Embedding(128) β BiLSTM(hidden=128) β mean-pool over time β Linear(256 β 5) - Training: 100 epochs, batch size 64,
AdamWatlr=3e-4,BCEWithLogitsLoss - No early stopping β instead, the checkpoint with the best validation F1-micro is saved each epoch (
best_scratch_state.pt+ full modelbest_scratch_full.pt)
- Tokenization:
padding="max_length",truncation=True,max_length=128 AutoModelForSequenceClassificationwithproblem_type="multi_label_classification"(sigmoid + BCE under the hood)- Hugging Face
Trainerwith:num_train_epochs=8,learning_rate=2e-5per_device_train_batch_size=16,per_device_eval_batch_size=32warmup_ratio=0.1,weight_decay=0.02eval_strategy="epoch",save_strategy="epoch",load_best_model_at_end=Trueon F1-microfp16=Trueautomatically when a GPU is available- W&B reporting enabled automatically if
USE_WANDBis set
- After training: predicts on the held-out validation set, writes
submission_<tag>.csv, and saves the full HF model + tokenizer toproject_outputs/<tag>_hf/
Runs, in order: scratch BiLSTM β bert-base-uncased β roberta-base, all against the same train/val split (80/20, random_state=42).
Loads all three trained models from their KaggleHub model-version folders (hardcoded per-run paths, e.g. /kaggle/input/emotion-models-<timestamp>/pytorch/.../1) and reproduces predictions for each:
- Scratch model: rebuilds the vocab from
train.csv, loadsbest_scratch_state.pt, runs a forward pass, thresholds at0.5 - BERT / RoBERTa: loads each fine-tuned
AutoModelForSequenceClassification+ tokenizer, runs a forward pass, thresholds at0.5
Update
SCRATCH_FOLDER,BERT_FOLDER, andROBERTA_FOLDERat the top ofinference.pyto point at whichever KaggleHub model version you want to score against β these are not resolved automatically.
save_summary(results, out_dir)β writesmodel_summary.csvfrom a list of{model, f1_micro, submission}dictsplot_bar(df)/plot_line(df)β F1-micro comparison chartsprint_observations(df)β plain-text takeaways (transformers outperform the scratch model, scratch model helps build pipeline intuition, best model should be used for deployment)
upload_folder_kagglehub(local_dir, handle, notes="")β uploads an entire folder (e.g. a saved HF model) as a new KaggleHub model version, timestamp-suffixedupload_pt_file(pt_path, handle, notes="")β uploads a single.ptfile (e.g. the scratch model's state dict) the same way- Both silently no-op with a message if
kagglehubisn't installed (i.e. outside Kaggle)
- Add the competition dataset (
2025-sep-dl-gen-ai-project) as a data source on the notebook. - Copy
scripts/into the working directory assrc/(or addscripts/tosys.pathunder that alias) so thefrom src.* import ...statements resolve. - Set
KAGGLE_USERNAMEand (optionally)WANDB_API_KEYunder Add-ons β Secrets. - Run training:
%run src/train.py
- Upload the resulting model folders/state dicts to KaggleHub if you want them accessible for a separate inference run:
from src.uploader import upload_folder_kagglehub, upload_pt_file upload_folder_kagglehub("project_outputs/bert_base_hf", "your-username/emotion-models", notes="BERT fine-tuned") upload_pt_file("project_outputs/scratch_model/best_scratch_state.pt", "your-username/emotion-models", notes="Scratch BiLSTM")
- In a fresh notebook (or the same one), attach the KaggleHub model version as an input, update the three
*_FOLDERpaths ininference.py, and run:%run src/inference.py
- Summarize results:
from src.reporting import save_summary, plot_bar, plot_line, print_observations df, path = save_summary(results, "project_outputs") plot_bar(df) plot_line(df) print_observations(df)
The scripts assume Kaggle (kaggle_secrets, /kaggle/input, /kaggle/working). To run outside Kaggle you'd need to:
- Replace
UserSecretsClientcalls inconfig.pywithos.environ.get(...)or a.envfile - Point
DATA_DIRintrain.py/inference.pyat a localdata/folder instead of/kaggle/input/... - Change
OUT_DIRto a local path instead of/kaggle/working/project_outputs - Skip
uploader.py(KaggleHub-only) or swap it for a Hugging Face Hub upload
The best-performing model is deployed as a Gradio app on Hugging Face Spaces:
π huggingface.co/spaces/23f1000805/DLGenAI-project-deployment
Submissions for all three models are checked into project_outputs/:
submission_scratch (4).csvsubmission_bert_base_uncased (1).csvsubmission_roberta_base (1).csv
Full write-up, methodology, and analysis are in the project report:
π report/23f1000805_DG_T32025 (1).pdf
General pattern observed (see reporting.print_observations): transformer models (BERT/RoBERTa) clearly outperform the from-scratch BiLSTM, the scratch model is mainly useful for building pipeline/architecture intuition, and BERT/RoBERTa reach strong scores within just a few epochs of fine-tuning.
| Issue | Fix |
|---|---|
ModuleNotFoundError: No module named 'src' |
Copy/alias scripts/ as src/ in the working directory, or add it to sys.path before importing |
kaggle_secrets import fails locally |
This module only exists inside Kaggle notebooks β see "Adapting to run locally" above |
| Hugging Face download/upload hangs | export HF_HUB_DISABLE_XET=1 |
BatchNorm/last-batch crash in DataLoader |
Set drop_last=True |
| Kaggle secret not found | Double-check exact key casing (KAGGLE_USERNAME, WANDB_API_KEY) in Kaggle Secrets |
| CUDA OOM during transformer fine-tuning | Lower per_device_train_batch_size in transformer_train.py, or add gradient accumulation |
inference.py can't find model files |
Update SCRATCH_FOLDER / BERT_FOLDER / ROBERTA_FOLDER to the correct KaggleHub model-version path for your run |
- Built a complete multi-label NLP pipeline from raw competition data to submission-ready CSVs
- Implemented and reasoned about multi-label classification (sigmoid + BCE, not softmax/cross-entropy)
- Directly compared a from-scratch recurrent architecture against pretrained transformers on identical splits
- Used the Hugging Face
TrainerAPI end-to-end: tokenization, customcompute_metrics, best-checkpoint selection - Built a Kaggle-native MLOps loop: train β checkpoint β push to KaggleHub β pull for inference β deploy to Hugging Face Spaces
- Structured a modular project: config, data, training, inference, reporting, and upload as separate concerns
Python Β· PyTorch Β· Hugging Face Transformers (Trainer) Β· scikit-learn Β· pandas Β· NumPy Β· Matplotlib Β· KaggleHub Β· Weights & Biases Β· Gradio (Hugging Face Spaces)
dlgenai Β· emotion-classification Β· emotion-classifier Β· iitm Β· iitm-bs Β· iitmadrasonlinedegree Β· iitmbsc
This project is for educational and academic purposes under the DLGenAI course (September 2025 term), IIT Madras BS Degree in Data Science and Applications.
- IIT Madras BS Data Science program
- Kaggle datasets and runtime environment
- Hugging Face Transformers library and Spaces
- PyTorch team