Spaces:
Runtime error
Runtime error
| import pandas as pd | |
| import torch | |
| import numpy as np | |
| from torch.utils.data import Dataset, DataLoader | |
| from transformers import BertTokenizer, BertForSequenceClassification | |
| from transformers import get_linear_schedule_with_warmup | |
| from torch.optim import AdamW | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.metrics import classification_report, accuracy_score | |
| import os | |
| import json | |
| # ============================================================ | |
| # CONFIG | |
| # ============================================================ | |
| MODEL_NAME = 'bert-base-uncased' | |
| MAX_LEN = 128 | |
| BATCH_SIZE = 16 | |
| EPOCHS = 3 | |
| LEARNING_RATE = 2e-5 | |
| RANDOM_STATE = 42 | |
| MIN_SAMPLES = 800 # minimum rows to train a category model | |
| # Categories to merge into General (too few samples) | |
| MERGE_INTO_GENERAL = ['Sports', 'Toys', 'Beauty'] | |
| # ============================================================ | |
| # STAGE 1 — Load and prepare data | |
| # ============================================================ | |
| print("Loading dataset...") | |
| df = pd.read_csv('flipkart_balanced.csv') | |
| # Merge small categories into General | |
| df['category'] = df['category'].apply( | |
| lambda x: 'General' if x in MERGE_INTO_GENERAL else x | |
| ) | |
| # Convert sentiment to numbers — BERT needs numbers not strings | |
| df['label'] = df['sentiment'].apply(lambda x: 1 if x == 'positive' else 0) | |
| print(f"Total rows: {len(df):,}") | |
| print(f"Categories: {df['category'].unique().tolist()}") | |
| # ============================================================ | |
| # STAGE 2 — PyTorch Dataset class | |
| # ============================================================ | |
| class ReviewDataset(Dataset): | |
| """ | |
| Think of this as a smart list. | |
| PyTorch pulls items from it one by one during training. | |
| Each item = one tokenized review + its label. | |
| """ | |
| def __init__(self, texts, labels, tokenizer, max_len): | |
| self.texts = texts | |
| self.labels = labels | |
| self.tokenizer = tokenizer | |
| self.max_len = max_len | |
| def __len__(self): | |
| return len(self.texts) | |
| def __getitem__(self, idx): | |
| text = str(self.texts[idx]) | |
| label = self.labels[idx] | |
| # Tokenize — converts text to BERT input format | |
| encoding = self.tokenizer( | |
| text, | |
| add_special_tokens=True, | |
| max_length=self.max_len, | |
| padding='max_length', | |
| truncation=True, | |
| return_attention_mask=True, | |
| return_tensors='pt' | |
| ) | |
| return { | |
| 'input_ids': encoding['input_ids'].flatten(), | |
| 'attention_mask': encoding['attention_mask'].flatten(), | |
| 'label': torch.tensor(label, dtype=torch.long) | |
| } | |
| # ============================================================ | |
| # STAGE 3 — Training function | |
| # ============================================================ | |
| def train_epoch(model, dataloader, optimizer, scheduler, device): | |
| model.train() | |
| total_loss = 0 | |
| correct = 0 | |
| total = 0 | |
| for batch in dataloader: | |
| input_ids = batch['input_ids'].to(device) | |
| attention_mask = batch['attention_mask'].to(device) | |
| labels = batch['label'].to(device) | |
| # Zero gradients from previous step | |
| optimizer.zero_grad() | |
| # Forward pass — BERT makes predictions | |
| outputs = model( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask, | |
| labels=labels | |
| ) | |
| loss = outputs.loss | |
| logits = outputs.logits | |
| # Backward pass — BERT learns from mistakes | |
| loss.backward() | |
| # Clip gradients to prevent exploding gradients | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) | |
| optimizer.step() | |
| scheduler.step() | |
| total_loss += loss.item() | |
| preds = torch.argmax(logits, dim=1) | |
| correct += (preds == labels).sum().item() | |
| total += labels.size(0) | |
| return total_loss / len(dataloader), correct / total | |
| # ============================================================ | |
| # STAGE 4 — Evaluation function | |
| # ============================================================ | |
| def evaluate(model, dataloader, device): | |
| model.eval() | |
| all_preds = [] | |
| all_labels = [] | |
| with torch.no_grad(): | |
| for batch in dataloader: | |
| input_ids = batch['input_ids'].to(device) | |
| attention_mask = batch['attention_mask'].to(device) | |
| labels = batch['label'].to(device) | |
| outputs = model( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask | |
| ) | |
| preds = torch.argmax(outputs.logits, dim=1) | |
| all_preds.extend(preds.cpu().numpy()) | |
| all_labels.extend(labels.cpu().numpy()) | |
| acc = accuracy_score(all_labels, all_preds) | |
| report = classification_report( | |
| all_labels, all_preds, | |
| target_names=['negative', 'positive'] | |
| ) | |
| return acc, report | |
| # ============================================================ | |
| # MAIN — Train one model per category | |
| # ============================================================ | |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| print(f"\nUsing device: {device}") | |
| tokenizer = BertTokenizer.from_pretrained(MODEL_NAME) | |
| os.makedirs('models', exist_ok=True) | |
| results = {} | |
| for category in df['category'].unique(): | |
| print(f"\n{'='*50}") | |
| print(f"Training: {category}") | |
| print(f"{'='*50}") | |
| cat_df = df[df['category'] == category].reset_index(drop=True) | |
| if len(cat_df) < MIN_SAMPLES: | |
| print(f" Skipping — only {len(cat_df)} rows (need {MIN_SAMPLES}+)") | |
| continue | |
| print(f" Rows: {len(cat_df):,}") | |
| # Train/test split — 80% train, 20% test | |
| train_df, test_df = train_test_split( | |
| cat_df, | |
| test_size=0.2, | |
| random_state=RANDOM_STATE, | |
| stratify=cat_df['label'] | |
| ) | |
| # Create datasets | |
| train_dataset = ReviewDataset( | |
| train_df['input_text'].values, | |
| train_df['label'].values, | |
| tokenizer, MAX_LEN | |
| ) | |
| test_dataset = ReviewDataset( | |
| test_df['input_text'].values, | |
| test_df['label'].values, | |
| tokenizer, MAX_LEN | |
| ) | |
| # Create dataloaders | |
| train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True) | |
| test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False) | |
| # Load fresh BERT model for this category | |
| model = BertForSequenceClassification.from_pretrained( | |
| MODEL_NAME, | |
| num_labels=2 | |
| ) | |
| model.to(device) | |
| # Optimizer and scheduler | |
| optimizer = AdamW(model.parameters(), lr=LEARNING_RATE) | |
| total_steps = len(train_loader) * EPOCHS | |
| scheduler = get_linear_schedule_with_warmup( | |
| optimizer, | |
| num_warmup_steps=total_steps // 10, | |
| num_training_steps=total_steps | |
| ) | |
| # Training loop | |
| best_acc = 0 | |
| for epoch in range(EPOCHS): | |
| train_loss, train_acc = train_epoch( | |
| model, train_loader, optimizer, scheduler, device | |
| ) | |
| val_acc, val_report = evaluate(model, test_loader, device) | |
| print(f" Epoch {epoch+1}/{EPOCHS} " | |
| f"| Loss: {train_loss:.4f} " | |
| f"| Train Acc: {train_acc:.4f} " | |
| f"| Val Acc: {val_acc:.4f}") | |
| # Save best model | |
| if val_acc > best_acc: | |
| best_acc = val_acc | |
| model_path = f'models/{category}_bert' | |
| model.save_pretrained(model_path) | |
| tokenizer.save_pretrained(model_path) | |
| # Final evaluation | |
| final_acc, final_report = evaluate(model, test_loader, device) | |
| results[category] = { | |
| 'accuracy': round(final_acc, 4), | |
| 'best_accuracy': round(best_acc, 4) | |
| } | |
| print(f"\n Final Report for {category}:") | |
| print(final_report) | |
| # Save results summary | |
| with open('models/training_results.json', 'w') as f: | |
| json.dump(results, f, indent=2) | |
| print("\n" + "="*50) | |
| print("ALL MODELS TRAINED") | |
| print("="*50) | |
| for cat, res in results.items(): | |
| print(f" {cat}: Best Accuracy = {res['best_accuracy']}") | |
| print("\nModels saved in ./models/ folder") |