multilingual-absa / src /absa /models /baseline.py
Aryan Mishra
Add CI, typed ORM models, and packaging cleanup
a00fee9
Raw
History Blame Contribute Delete
5.2 kB
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()