import json from pathlib import Path from typing import List import joblib import mlflow import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix, f1_score from sklearn.model_selection import train_test_split from absa.training.mlflow_utils import log_training_run def load_data(file_paths: List[Path]) -> pd.DataFrame: data = [] for path in file_paths: with open(path, "r", encoding="utf-8") as f: for line in f: if line.strip(): data.append(json.loads(line)) return pd.DataFrame(data) def extract_sentence_sentiment(df: pd.DataFrame) -> pd.DataFrame: """ Extracts a sentence-level sentiment by taking the majority sentiment of aspects. If there is a tie or conflict, it maps it appropriately. For this baseline, we will filter to samples that have a clear sentence-level sentiment derived from the aspects, or use the aspects to build a flat list of text -> sentiment. Wait, the requirement says "Sentence-level sentiment only (not ABSA)". Let's just flatten it: pair each review text with the sentiment of its aspect, but wait, a sentence might have multiple aspects with different sentiments. If we do "Sentence-level sentiment only", we can just assign the sentence the label of the first aspect, or we can construct a dataset of (text, sentiment) for every aspect but just predict sentiment from text alone. Let's flatten it to (text, sentiment) pairs for every aspect to keep the dataset size comparable. """ records = [] sentiment_map = {"positive": 0, "negative": 1, "neutral": 2, "conflict": 3} for _, row in df.iterrows(): text = row["text"] aspects = row.get("aspect_terms", []) for aspect in aspects: polarity = aspect["polarity"] if polarity in sentiment_map: records.append({"text": text, "label": sentiment_map[polarity]}) return pd.DataFrame(records) def main(): data_dir = Path("data/processed") train_path = data_dir / "semeval_train.jsonl" # Load raw data # Test path has no labels, so we only use train_path like we effectively did in hf_dataset train_df_raw = load_data([train_path]) # Prepare flat sequence classification data cls_df = extract_sentence_sentiment(train_df_raw) # Exact same split logic as hf_dataset.py train_cls, temp_cls = train_test_split(cls_df, test_size=0.2, random_state=42, stratify=cls_df["label"]) val_cls, test_cls = train_test_split(temp_cls, test_size=0.5, random_state=42, stratify=temp_cls["label"]) x_train = train_cls["text"].values y_train = train_cls["label"].values x_test = test_cls["text"].values y_test = test_cls["label"].values print(f"Training on {len(x_train)} samples, testing on {len(x_test)} samples.") # Baseline Model Pipeline vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_features=10000) classifier = LogisticRegression(max_iter=1000, class_weight="balanced", random_state=42) # Train print("Training TF-IDF + Logistic Regression...") x_train_vec = vectorizer.fit_transform(x_train) classifier.fit(x_train_vec, y_train) # Evaluate print("Evaluating...") x_test_vec = vectorizer.transform(x_test) y_pred = classifier.predict(x_test_vec) # Metrics macro_f1 = f1_score(y_test, y_pred, average="macro") per_class_f1 = f1_score(y_test, y_pred, average=None) conf_matrix = confusion_matrix(y_test, y_pred) print(classification_report(y_test, y_pred, target_names=["positive", "negative", "neutral", "conflict"])) # Format metrics for MLflow metrics = { "eval_macro_f1": float(macro_f1), "eval_f1_positive": float(per_class_f1[0]), "eval_f1_negative": float(per_class_f1[1]), "eval_f1_neutral": float(per_class_f1[2]), "eval_f1_conflict": float(per_class_f1[3] if len(per_class_f1) > 3 else 0.0), } # Also log confusion matrix as flattened or individual values (optional, can be artifact later) # For now, print it. We will log it via mlflow log_dict or json artifact if we want, but let's just log metrics. # Save Model model_dir = Path("models/baseline") model_dir.mkdir(parents=True, exist_ok=True) model_path = model_dir / "tfidf_lr.pkl" joblib.dump({"vectorizer": vectorizer, "classifier": classifier}, model_path) print(f"Model saved to {model_path}") # Log to MLflow params = { "model": "tfidf_lr", "ngram_range": "(1, 2)", "max_features": 10000, "max_iter": 1000, "class_weight": "balanced", } run_id = log_training_run(params, metrics, model_path, run_name="baseline_tfidf_lr") # We can also explicitly log the confusion matrix as an artifact with mlflow.start_run(run_id=run_id): cm_dict = {"confusion_matrix": conf_matrix.tolist()} mlflow.log_dict(cm_dict, "confusion_matrix.json") print(f"MLflow run ID: {run_id}") if __name__ == "__main__": main()