Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Intent Classifier Training Script for Twi Speech Recognition | |
| =========================================================== | |
| This script trains a custom intent classification model using the Twi prompts | |
| data from your CSV file. It creates a fine-tuned transformer model specifically | |
| for your e-commerce intents. | |
| Author: AI Assistant | |
| Date: 2025-11-05 | |
| """ | |
| import os | |
| import sys | |
| import pandas as pd | |
| import numpy as np | |
| import json | |
| import logging | |
| from pathlib import Path | |
| from typing import Dict, List, Tuple, Any | |
| from collections import Counter | |
| import argparse | |
| import torch | |
| from transformers import ( | |
| AutoTokenizer, | |
| AutoModelForSequenceClassification, | |
| TrainingArguments, | |
| Trainer, | |
| DataCollatorWithPadding, | |
| ) | |
| from datasets import Dataset | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.metrics import accuracy_score, classification_report | |
| # Add project paths | |
| current_dir = Path(__file__).parent | |
| project_root = current_dir.parent | |
| sys.path.insert(0, str(current_dir)) | |
| sys.path.insert(0, str(project_root)) | |
| from config.config import OptimizedConfig | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" | |
| ) | |
| logger = logging.getLogger(__name__) | |
| class TwiIntentTrainer: | |
| """Trainer for Twi intent classification model.""" | |
| def __init__(self, config: OptimizedConfig = None): | |
| self.config = config or OptimizedConfig() | |
| self.tokenizer = None | |
| self.model = None | |
| self.label_to_id = {} | |
| self.id_to_label = {} | |
| self.device = self.config.get_device() | |
| def load_data(self, csv_path: str) -> pd.DataFrame: | |
| """Load and preprocess the Twi prompts data.""" | |
| logger.info(f"Loading data from {csv_path}") | |
| # Read CSV | |
| df = pd.read_csv(csv_path) | |
| logger.info(f"Loaded {len(df)} rows from CSV") | |
| # Filter out rows without intent or text | |
| df = df.dropna(subset=["text", "intent"]) | |
| df = df[df["intent"].str.strip() != ""] | |
| df = df[df["text"].str.strip() != ""] | |
| logger.info(f"After filtering: {len(df)} rows with valid text and intent") | |
| return df | |
| def prepare_training_data(self, df: pd.DataFrame) -> Tuple[List[str], List[str]]: | |
| """Prepare training data from CSV.""" | |
| texts = [] | |
| intents = [] | |
| # Extract text and intent pairs | |
| for _, row in df.iterrows(): | |
| text = str(row["text"]).strip() | |
| intent = str(row["intent"]).strip() | |
| # Skip empty or invalid entries | |
| if not text or not intent or intent.lower() in ["intent", "nan"]: | |
| continue | |
| texts.append(text) | |
| intents.append(intent) | |
| logger.info(f"Prepared {len(texts)} training examples") | |
| # Show intent distribution | |
| intent_counts = Counter(intents) | |
| logger.info("Intent distribution:") | |
| for intent, count in intent_counts.most_common(): | |
| logger.info(f" {intent}: {count}") | |
| return texts, intents | |
| def augment_data( | |
| self, texts: List[str], intents: List[str] | |
| ) -> Tuple[List[str], List[str]]: | |
| """Augment training data with variations.""" | |
| augmented_texts = texts.copy() | |
| augmented_intents = intents.copy() | |
| # Simple augmentation strategies for Twi | |
| augmentations = [ | |
| # Add common Twi variations | |
| lambda x: x.replace("kɔ", "ko"), # Alternate spelling | |
| lambda x: x.replace("ɛ", "e"), # Accent removal | |
| lambda x: x.replace("ɔ", "o"), # Accent removal | |
| # Add common prefixes/suffixes | |
| lambda x: f"me pɛ sɛ {x}", # "I want to..." | |
| lambda x: f"{x} yi", # Add particle | |
| lambda x: f"boa me {x}", # "Help me..." | |
| ] | |
| original_count = len(texts) | |
| for i, (text, intent) in enumerate(zip(texts, intents)): | |
| # Apply augmentations randomly | |
| for aug_func in augmentations: | |
| try: | |
| augmented_text = aug_func(text) | |
| if augmented_text != text and len(augmented_text) > 0: | |
| augmented_texts.append(augmented_text) | |
| augmented_intents.append(intent) | |
| except: | |
| continue | |
| logger.info( | |
| f"Augmented data from {original_count} to {len(augmented_texts)} examples" | |
| ) | |
| return augmented_texts, augmented_intents | |
| def create_label_mappings(self, intents: List[str]): | |
| """Create label to ID mappings.""" | |
| unique_intents = sorted(set(intents)) | |
| self.label_to_id = {intent: idx for idx, intent in enumerate(unique_intents)} | |
| self.id_to_label = {idx: intent for intent, idx in self.label_to_id.items()} | |
| logger.info(f"Created mappings for {len(unique_intents)} unique intents") | |
| return unique_intents | |
| def prepare_datasets(self, texts: List[str], intents: List[str], test_size=0.2): | |
| """Prepare train/validation datasets.""" | |
| # Convert intents to IDs | |
| intent_ids = [self.label_to_id[intent] for intent in intents] | |
| # Split data | |
| train_texts, val_texts, train_labels, val_labels = train_test_split( | |
| texts, intent_ids, test_size=test_size, random_state=42, stratify=intent_ids | |
| ) | |
| logger.info( | |
| f"Split data: {len(train_texts)} train, {len(val_texts)} validation" | |
| ) | |
| # Tokenize data | |
| def tokenize_function(examples): | |
| return self.tokenizer( | |
| examples["text"], truncation=True, padding=True, max_length=512 | |
| ) | |
| # Create datasets | |
| train_dataset = Dataset.from_dict({"text": train_texts, "labels": train_labels}) | |
| val_dataset = Dataset.from_dict({"text": val_texts, "labels": val_labels}) | |
| # Tokenize | |
| train_dataset = train_dataset.map(tokenize_function, batched=True) | |
| val_dataset = val_dataset.map(tokenize_function, batched=True) | |
| return train_dataset, val_dataset | |
| def initialize_model(self, num_labels: int): | |
| """Initialize tokenizer and model.""" | |
| model_name = "microsoft/DialoGPT-medium" # Good for conversational AI | |
| logger.info(f"Initializing model: {model_name}") | |
| # Load tokenizer | |
| self.tokenizer = AutoTokenizer.from_pretrained(model_name) | |
| # Add pad token if it doesn't exist | |
| if self.tokenizer.pad_token is None: | |
| self.tokenizer.pad_token = self.tokenizer.eos_token | |
| # Load model | |
| self.model = AutoModelForSequenceClassification.from_pretrained( | |
| model_name, num_labels=num_labels, ignore_mismatched_sizes=True | |
| ) | |
| # Move to device | |
| self.model.to(self.device) | |
| logger.info(f"Model initialized with {num_labels} labels on {self.device}") | |
| def compute_metrics(self, eval_pred): | |
| """Compute metrics for evaluation.""" | |
| predictions, labels = eval_pred | |
| predictions = np.argmax(predictions, axis=1) | |
| accuracy = accuracy_score(labels, predictions) | |
| return { | |
| "accuracy": accuracy, | |
| "f1": accuracy, # Simplified for now | |
| } | |
| def train_model(self, train_dataset, val_dataset, output_dir: str): | |
| """Train the intent classification model.""" | |
| logger.info("Starting model training...") | |
| # Training arguments | |
| training_args = TrainingArguments( | |
| output_dir=output_dir, | |
| num_train_epochs=10, | |
| per_device_train_batch_size=16, | |
| per_device_eval_batch_size=16, | |
| warmup_steps=500, | |
| weight_decay=0.01, | |
| logging_dir=f"{output_dir}/logs", | |
| logging_steps=50, | |
| evaluation_strategy="steps", | |
| eval_steps=100, | |
| save_steps=500, | |
| save_strategy="steps", | |
| load_best_model_at_end=True, | |
| metric_for_best_model="accuracy", | |
| greater_is_better=True, | |
| ) | |
| # Data collator | |
| data_collator = DataCollatorWithPadding(tokenizer=self.tokenizer, padding=True) | |
| # Trainer | |
| trainer = Trainer( | |
| model=self.model, | |
| args=training_args, | |
| train_dataset=train_dataset, | |
| eval_dataset=val_dataset, | |
| tokenizer=self.tokenizer, | |
| data_collator=data_collator, | |
| compute_metrics=self.compute_metrics, | |
| ) | |
| # Train | |
| trainer.train() | |
| # Save model | |
| trainer.save_model(output_dir) | |
| self.tokenizer.save_pretrained(output_dir) | |
| # Save label mappings | |
| label_path = Path(output_dir) / "intent_labels.json" | |
| with open(label_path, "w") as f: | |
| json.dump( | |
| {"label_to_id": self.label_to_id, "id_to_label": self.id_to_label}, | |
| f, | |
| indent=2, | |
| ) | |
| logger.info(f"Model saved to {output_dir}") | |
| return trainer | |
| def evaluate_model(self, trainer, val_dataset): | |
| """Evaluate the trained model.""" | |
| logger.info("Evaluating model...") | |
| # Evaluate | |
| eval_results = trainer.evaluate() | |
| logger.info("Evaluation results:") | |
| for key, value in eval_results.items(): | |
| logger.info(f" {key}: {value:.4f}") | |
| # Predictions for detailed analysis | |
| predictions = trainer.predict(val_dataset) | |
| y_pred = np.argmax(predictions.predictions, axis=1) | |
| y_true = predictions.label_ids | |
| # Convert back to labels | |
| pred_labels = [self.id_to_label[idx] for idx in y_pred] | |
| true_labels = [self.id_to_label[idx] for idx in y_true] | |
| # Classification report | |
| report = classification_report(true_labels, pred_labels) | |
| logger.info(f"Classification Report:\n{report}") | |
| return eval_results | |
| def main(): | |
| """Main training function.""" | |
| parser = argparse.ArgumentParser(description="Train Twi Intent Classifier") | |
| parser.add_argument( | |
| "--data", default="../twi_prompts.csv", help="Path to Twi prompts CSV file" | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| default="./models/intent_classifier", | |
| help="Output directory for trained model", | |
| ) | |
| parser.add_argument( | |
| "--augment", action="store_true", help="Enable data augmentation" | |
| ) | |
| args = parser.parse_args() | |
| # Initialize trainer | |
| config = OptimizedConfig() | |
| trainer = TwiIntentTrainer(config) | |
| # Check if data file exists | |
| data_path = Path(args.data) | |
| if not data_path.exists(): | |
| # Try alternative paths | |
| alternative_paths = [ | |
| Path("../twi_prompts.csv"), | |
| Path("../../twi_prompts.csv"), | |
| project_root / "twi_prompts.csv", | |
| ] | |
| for alt_path in alternative_paths: | |
| if alt_path.exists(): | |
| data_path = alt_path | |
| break | |
| else: | |
| logger.error(f"Could not find data file. Tried: {args.data}") | |
| logger.error(f"Alternative paths: {[str(p) for p in alternative_paths]}") | |
| return | |
| logger.info(f"Using data file: {data_path}") | |
| try: | |
| # Load data | |
| df = trainer.load_data(str(data_path)) | |
| # Prepare training data | |
| texts, intents = trainer.prepare_training_data(df) | |
| if len(texts) == 0: | |
| logger.error("No training data found!") | |
| return | |
| # Augment data if requested | |
| if args.augment: | |
| texts, intents = trainer.augment_data(texts, intents) | |
| # Create label mappings | |
| unique_intents = trainer.create_label_mappings(intents) | |
| # Initialize model | |
| trainer.initialize_model(len(unique_intents)) | |
| # Prepare datasets | |
| train_dataset, val_dataset = trainer.prepare_datasets(texts, intents) | |
| # Create output directory | |
| output_dir = Path(args.output) | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| # Train model | |
| model_trainer = trainer.train_model(train_dataset, val_dataset, str(output_dir)) | |
| # Evaluate model | |
| trainer.evaluate_model(model_trainer, val_dataset) | |
| logger.info("Training completed successfully!") | |
| logger.info(f"Model saved to: {output_dir}") | |
| # Update config to use trained model | |
| config_update = f""" | |
| # Update your config.py with: | |
| INTENT_CLASSIFIER = {{ | |
| "custom_model_path": "{output_dir}", | |
| "confidence_threshold": 0.5, | |
| "top_k": 3, | |
| }} | |
| """ | |
| logger.info(config_update) | |
| except Exception as e: | |
| logger.error(f"Training failed: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| if __name__ == "__main__": | |
| main() | |