Contextual Halal/Haram Ingredient Detection (Fine-Tuned BERT)
This repository contains a highly regularized, fine-tuned BERT-base-uncased transformer architecture engineered to classify product ingredient lists as Halal or Haram.
Unlike naive lookup scripts or keyword matching models, this model is specifically trained to interpret structural linguistic context—effectively solving complex ingredient cross-contamination and ambiguous dual-class formulations.
📊 Performance: Your Fine-Tuned Model vs. Raw Base BERT
Evaluated on an completely untouched, isolated Test Set containing 66,834 unique rows:
| Metric | Raw Base BERT Baseline | Your Fine-Tuned Model |
|---|
| Accuracy | 58.51% | 99.42% |
| Macro F1-Score | 36.92% | 99.40% |
| Weighted F1-Score | 43.20% | 99.42% |
| Halal Class Precision | 58.51% | 99.84% |
| Halal Class Recall | 100.00% | 99.17% |
| Haram Class Precision | 100.00% | 98.84% |
| Haram Class Recall (Safety) | 0.00% | 99.77% |
🔍 Analysis of Baseline Deficiencies
Out of the box, standard BERT has zero domain intelligence for dietary analysis. As proven by the 0.00% Haram Recall, the base model panicked, defaulted entirely to majority-class guessing ("Halal"), and missed every single non-compliant product. Your fine-tuned model achieved a 99.77% Haram Recall, successfully mitigating dangerous structural oversights.
🛠️ Dataset Engineering & The "Anti-Cheat" Protocol
To ensure the high evaluation score is reflective of real-world generalization rather than cheating via duplication, the raw dataset of 528,092 entries underwent rigorous sanitation:
- Near-Duplicate Cleansing: A Jaccard similarity audit revealed that thousands of recipes were structural clones (re-ordered ingredients). We dropped 82,534 near-duplicate rows, preventing the model from passing testing by memorizing flipped tokens.
- Deterministic Heuristic Correction: The source dataset suffered from flawed keyword labelling, where any trace of the string
wine automatically forced a Haram tag. We manually patched this heuristic loop, rescue-mapping 5,238 instances of "wine vinegar" to Halal. This forced the model to read contextual combinations rather than mapping individual substrings.
- Imbalance Optimization: Given the 58.5% Halal to 41.5% Haram balance, a custom
WeightedTrainer computed inverse-frequency loss weights on the GPU, penalizing minority-class failures severely.
🛡️ Proof Against Overfitting
Skeptics viewing a 99.42% accuracy might assume data leakage or over-parameterization. This model is immune to that criticism based on three design factors:
- Zero Cross-Contamination: Train (70%), Validation (15%), and Test (15%) splits were strictly isolated after the Jaccard de-duplication audit. A programmatic intersection check verified a 0% token leakage rate.
- Aggressive Architectural Regularization: BERT's native hidden and attention layer dropouts were increased from 0.1 to 0.2 to disrupt parameter co-adaptation. L2 weight decay was enforced at 0.01.
- Perfect Loss Concurrency: During training, validation loss declined synchronously alongside training loss ($0.023 \rightarrow 0.020 \rightarrow 0.018$) across 3 epochs. There was zero divergence, validating stable structural learning.
🚀 Production Inference Pipeline
To deploy this model into a live tracking system, incoming raw text must pass through the identical standard preprocessing transformation:
1import string
2import torch
3from transformers import BertTokenizer, BertForSequenceClassification
4
5def production_inference(raw_text, model, tokenizer):
6 # 1. Pipeline Cleaning
7 clean_text = str(raw_text).lower().translate(str.maketrans('', '', string.punctuation))
8 clean_text = " ".join(clean_text.split())
9
10 # 2. Hard Context Override
11 if 'wine vinegar' in clean_text or 'winevinegar' in clean_text:
12 return {"prediction": "halal", "confidence": 1.00, "method": "Rule Override"}
13
14 # 3. Vectorization & Forward Pass
15 inputs = tokenizer(clean_text, max_length=128, padding='max_length', truncation=True, return_tensors='pt')
16 device = next(model.parameters()).device
17 inputs = {k: v.to(device) for k, v in inputs.items()}
18
19 model.eval()
20 with torch.no_grad():
21 outputs = model(**inputs)
22 probs = torch.nn.functional.softmax(outputs.logits, dim=-1).flatten()
23 class_id = torch.argmax(probs).item()
24
25 return {
26 "prediction": "halal" if class_id == 0 else "haram",
27 "confidence": round(probs[class_id].item(), 4),
28 "method": "BERT Transformer Inference"
29 }