File size: 12,771 Bytes
6db3515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
#!/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()