ThesisProject / backend /benchmark.py
JeyBii's picture
Upload folder using huggingface_hub
2b9b5b5 verified
Raw
History Blame Contribute Delete
22.7 kB
"""
Benchmark β€” Comparing Your RF Model Against Classic ML Baselines
================================================================
Trains and evaluates multiple fake-news classifiers on the SAME
locked test split so the comparison is completely fair.
Models compared
---------------
1. Naive Bayes (TF-IDF only) β€” simple baseline
2. Logistic Regression (TF-IDF only) β€” strong linear baseline
3. Linear SVM (TF-IDF only) β€” often best for text
4. Random Forest (TF-IDF only) β€” RF without embeddings
5. β˜… YOUR MODEL β˜… (TF-IDF + MiniLM + Stylo) β€” YOUR hybrid RF
Metrics reported (per model, per class + weighted avg)
-------------------------------------------------------
β€’ Accuracy
β€’ Precision (Fake class)
β€’ Recall (Fake class)
β€’ F1 Score (weighted)
β€’ AUC-ROC
Output
------
evaluation_results/benchmark_table.txt β€” plaintext comparison table
evaluation_results/benchmark_chart.png β€” bar chart (Accuracy + F1)
evaluation_results/benchmark_roc.png β€” ROC curves for all models
Usage
-----
python backend/benchmark.py
python backend/benchmark.py --mode tagalog # Tagalog dataset only
python backend/benchmark.py --mode cebuano # Cebuano dataset only
python backend/benchmark.py --mode mixed # All languages (default)
"""
import sys
import os
import re
import time
import argparse
import warnings
warnings.filterwarnings("ignore")
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_ROOT)
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from scipy.sparse import hstack, csr_matrix
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
# Classifiers
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV
# Metrics
from sklearn.metrics import (
accuracy_score,
classification_report,
confusion_matrix,
roc_auc_score,
roc_curve,
f1_score,
precision_score,
recall_score,
)
# Re-use helpers from train.py (keep feature extraction identical)
from backend.train import (
load_fake_news_dataset,
preprocess,
clean_text,
extract_stylometric_features,
get_minilm_model,
STYLOMETRIC_FEATURE_NAMES,
)
# ── Paths ──────────────────────────────────────────────────────────────────
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
# ───────────────────────────────────────────────────────────────────────────
# Feature builders
# ───────────────────────────────────────────────────────────────────────────
def build_tfidf_features(X_train, X_test):
"""Plain TF-IDF (used by all baseline models)."""
tfidf = TfidfVectorizer(
max_features=15_000,
ngram_range=(1, 3),
min_df=2,
max_df=0.95,
sublinear_tf=True,
)
X_tr = tfidf.fit_transform(X_train)
X_te = tfidf.transform(X_test)
return X_tr, X_te, tfidf
def build_hybrid_features(X_train, X_test):
"""TF-IDF + MiniLM embeddings + stylometric (your full pipeline)."""
print(" [hybrid] Fitting TF-IDF …")
tfidf = TfidfVectorizer(
max_features=15_000,
ngram_range=(1, 3),
min_df=2,
max_df=0.95,
sublinear_tf=True,
)
X_tr_tfidf = tfidf.fit_transform(X_train)
X_te_tfidf = tfidf.transform(X_test)
print(" [hybrid] Encoding with MiniLM …")
minilm = get_minilm_model()
emb_train = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
emb_test = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
print(f" [hybrid] Extracting {len(STYLOMETRIC_FEATURE_NAMES)} stylometric features …")
stylo_train = np.array([extract_stylometric_features(t) for t in X_train])
stylo_test = np.array([extract_stylometric_features(t) for t in X_test])
scaler = StandardScaler()
stylo_train_sc = scaler.fit_transform(stylo_train)
stylo_test_sc = scaler.transform(stylo_test)
X_tr = hstack([X_tr_tfidf, csr_matrix(emb_train), csr_matrix(stylo_train_sc)])
X_te = hstack([X_te_tfidf, csr_matrix(emb_test), csr_matrix(stylo_test_sc)])
n_total = X_tr.shape[1]
print(
f" [hybrid] Feature dimensions: {n_total} "
f"(TF-IDF: {X_tr_tfidf.shape[1]} + MiniLM: 384 + Stylo: {len(STYLOMETRIC_FEATURE_NAMES)})"
)
return X_tr, X_te
# ───────────────────────────────────────────────────────────────────────────
# Compute metrics for one model
# ───────────────────────────────────────────────────────────────────────────
def evaluate(name, model, X_test, y_test, proba=None):
"""Return a metrics dict for one fitted model."""
y_pred = model.predict(X_test)
acc = accuracy_score(y_test, y_pred)
prec_fake = precision_score(y_test, y_pred, pos_label=1, zero_division=0)
rec_fake = recall_score(y_test, y_pred, pos_label=1, zero_division=0)
f1_weighted = f1_score(y_test, y_pred, average="weighted", zero_division=0)
f1_fake = f1_score(y_test, y_pred, pos_label=1, zero_division=0)
# AUC-ROC (needs probability scores)
if proba is not None:
try:
auc = roc_auc_score(y_test, proba[:, 1])
except Exception:
auc = float("nan")
else:
auc = float("nan")
report = classification_report(
y_test, y_pred,
target_names=["Real", "Fake"],
zero_division=0,
)
return {
"name": name,
"accuracy": acc,
"precision": prec_fake,
"recall": rec_fake,
"f1_weighted": f1_weighted,
"f1_fake": f1_fake,
"auc": auc,
"report": report,
"y_pred": y_pred,
"proba": proba,
}
# ───────────────────────────────────────────────────────────────────────────
# Bar chart
# ───────────────────────────────────────────────────────────────────────────
def plot_bar_chart(results, output_path):
"""Side-by-side bar chart: Accuracy vs F1 (weighted) vs AUC-ROC."""
names = [r["name"] for r in results]
accs = [r["accuracy"] for r in results]
f1s = [r["f1_weighted"] for r in results]
aucs = [r["auc"] for r in results]
x = np.arange(len(names))
width = 0.26
fig, ax = plt.subplots(figsize=(max(10, len(names) * 2.2), 6))
# Color highlight for YOUR model (last entry)
bar_colors_acc = ["#2196F3"] * (len(names) - 1) + ["#E91E63"]
bar_colors_f1 = ["#4CAF50"] * (len(names) - 1) + ["#FF5722"]
bar_colors_auc = ["#9C27B0"] * (len(names) - 1) + ["#FF9800"]
b1 = ax.bar(x - width, accs, width, color=bar_colors_acc, edgecolor="black", lw=0.5, label="Accuracy")
b2 = ax.bar(x, f1s, width, color=bar_colors_f1, edgecolor="black", lw=0.5, label="F1 Weighted")
b3 = ax.bar(x + width, aucs, width, color=bar_colors_auc, edgecolor="black", lw=0.5, label="AUC-ROC")
# Value labels
for bars in (b1, b2, b3):
for bar in bars:
h = bar.get_height()
if not np.isnan(h):
ax.text(
bar.get_x() + bar.get_width() / 2,
h + 0.005,
f"{h:.3f}",
ha="center", va="bottom", fontsize=7.5, fontweight="bold",
)
ax.set_xticks(x)
ax.set_xticklabels(names, rotation=12, ha="right", fontsize=10)
ax.set_ylim(0, 1.12)
ax.set_ylabel("Score", fontsize=12)
ax.set_title(
"Benchmark: Your RF Model vs. Classic ML Baselines\n"
"(Highlighted in pink/orange = Your Model)",
fontsize=13, fontweight="bold",
)
ax.axhline(y=0.80, color="gray", linestyle="--", alpha=0.4, linewidth=1)
ax.text(len(names) - 0.5, 0.805, "80% threshold", color="gray", fontsize=8)
patch_yours = mpatches.Patch(color="#E91E63", label="β˜… Your Hybrid RF (Accuracy)")
ax.legend(handles=[*ax.get_legend_handles_labels()[0][:3], patch_yours], fontsize=9)
plt.tight_layout()
fig.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f" Saved: {output_path}")
# ───────────────────────────────────────────────────────────────────────────
# ROC curve chart
# ───────────────────────────────────────────────────────────────────────────
def plot_roc_curves(results, y_test, output_path):
"""Overlay ROC curves for all models that have probability scores."""
fig, ax = plt.subplots(figsize=(8, 6))
COLORS = [
"#2196F3", "#4CAF50", "#FF9800", "#9C27B0",
"#E91E63", "#00BCD4", "#F44336", "#8BC34A",
]
for i, r in enumerate(results):
if r["proba"] is None or np.isnan(r["auc"]):
continue
fpr, tpr, _ = roc_curve(y_test, r["proba"][:, 1], pos_label=1)
lw = 2.5 if "β˜…" in r["name"] else 1.5
dash = "-" if "β˜…" in r["name"] else "--"
ax.plot(
fpr, tpr,
color=COLORS[i % len(COLORS)],
lw=lw, linestyle=dash,
label=f"{r['name']} (AUC={r['auc']:.3f})",
)
ax.plot([0, 1], [0, 1], "k:", lw=1, label="Random (AUC=0.500)")
ax.set_xlim([0.0, 1.0])
ax.set_ylim([0.0, 1.05])
ax.set_xlabel("False Positive Rate", fontsize=12)
ax.set_ylabel("True Positive Rate", fontsize=12)
ax.set_title("ROC Curves β€” Fake-News Detection Benchmark", fontsize=13, fontweight="bold")
ax.legend(loc="lower right", fontsize=9)
ax.grid(alpha=0.3)
plt.tight_layout()
fig.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f" Saved: {output_path}")
# ───────────────────────────────────────────────────────────────────────────
# Plaintext summary table
# ───────────────────────────────────────────────────────────────────────────
def save_table(results, output_path, mode, n_train, n_test):
"""Write a neatly formatted comparison table to disk and stdout."""
lines = []
sep = "=" * 90
lines.append(sep)
lines.append(" BENCHMARK RESULTS β€” Fake-News Detection (Filipino / Cebuano)")
lines.append(f" Mode: {mode.upper()} | Train: {n_train:,} samples | Test: {n_test:,} samples")
lines.append(sep)
lines.append("")
header = (
f" {'Model':<35} {'Accuracy':>9} {'Prec(Fk)':>10} {'Rec(Fk)':>9} "
f"{'F1(Wtd)':>9} {'F1(Fk)':>8} {'AUC-ROC':>9}"
)
lines.append(header)
lines.append(" " + "-" * 88)
for r in results:
auc_str = f"{r['auc']:.4f}" if not np.isnan(r["auc"]) else " N/A "
marker = " β˜…" if "β˜…" in r["name"] else " "
row = (
f"{marker} {r['name']:<32} "
f"{r['accuracy']:>9.4f} "
f"{r['precision']:>10.4f} "
f"{r['recall']:>9.4f} "
f"{r['f1_weighted']:>9.4f} "
f"{r['f1_fake']:>8.4f} "
f"{auc_str:>9}"
)
lines.append(row)
lines.append("")
lines.append(sep)
lines.append(" DETAILED CLASSIFICATION REPORTS")
lines.append(sep)
for r in results:
lines.append("")
lines.append(f" ── {r['name']} ──────────────────────────────────────────────")
for ln in r["report"].splitlines():
lines.append(f" {ln}")
text = "\n".join(lines)
print(text)
with open(output_path, "w", encoding="utf-8") as f:
f.write(text)
print(f"\n Saved: {output_path}")
# ───────────────────────────────────────────────────────────────────────────
# Main
# ───────────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Benchmark fake-news classifiers.")
parser.add_argument(
"--mode",
choices=["mixed", "tagalog", "cebuano"],
default="mixed",
help="Which language subset to benchmark on (default: mixed).",
)
parser.add_argument(
"--test-size",
type=float,
default=0.20,
help="Fraction of data to hold out as the locked test set (default: 0.20).",
)
parser.add_argument(
"--skip-minilm",
action="store_true",
default=False,
help="Skip your hybrid RF model (useful if MiniLM is too slow for a quick check).",
)
args = parser.parse_args()
os.makedirs(OUTPUT_DIR, exist_ok=True)
print("=" * 60)
print(" FAKE-NEWS BENCHMARK")
print(f" Mode: {args.mode.upper()} | Test size: {args.test_size:.0%}")
print("=" * 60)
# ── 1. Load & preprocess dataset ─────────────────────────────────────
tagalog_only = args.mode == "tagalog"
cebuano_only = args.mode == "cebuano"
df = load_fake_news_dataset(tagalog_only=tagalog_only, cebuano_only=cebuano_only)
X_all, y_all = preprocess(df, undersample=False, oversample=True)
# ── 2. Locked test split (same seed β†’ reproducible) ──────────────────
print(f"\nCreating locked test split ({1 - args.test_size:.0%} train / {args.test_size:.0%} test) …")
X_train, X_test, y_train, y_test = train_test_split(
X_all, y_all,
test_size=args.test_size,
random_state=42,
stratify=y_all,
)
print(f" Train: {len(X_train):,} | Test: {len(X_test):,}")
print(f" Test distribution β€” Real: {y_test.count(0):,}, Fake: {y_test.count(1):,}")
y_train_arr = np.array(y_train)
y_test_arr = np.array(y_test)
# ── 3. Build TF-IDF features (shared by baseline models) ─────────────
print("\nBuilding TF-IDF features for baseline models …")
X_tr_tfidf, X_te_tfidf, tfidf = build_tfidf_features(X_train, X_test)
print(f" TF-IDF shape: {X_tr_tfidf.shape}")
# ── 4. Train and evaluate each model ─────────────────────────────────
results = []
# ── 4a. Naive Bayes (TF-IDF, no negative values β€” shift by min) ──────
print("\n[1/5] Naive Bayes (TF-IDF) …")
t0 = time.time()
nb = MultinomialNB(alpha=1.0)
# MultinomialNB needs non-negative input; TF-IDF with sublinear_tf is ok
nb.fit(X_tr_tfidf, y_train_arr)
nb_proba = nb.predict_proba(X_te_tfidf)
elapsed = time.time() - t0
res_nb = evaluate("Naive Bayes (TF-IDF)", nb, X_te_tfidf, y_test_arr, nb_proba)
res_nb["train_time"] = elapsed
results.append(res_nb)
print(f" Accuracy: {res_nb['accuracy']:.4f} | F1 Weighted: {res_nb['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
# ── 4b. Logistic Regression (TF-IDF) ─────────────────────────────────
print("\n[2/5] Logistic Regression (TF-IDF) …")
t0 = time.time()
lr = LogisticRegression(
max_iter=1000,
class_weight="balanced",
solver="saga",
C=1.0,
random_state=42,
n_jobs=-1,
)
lr.fit(X_tr_tfidf, y_train_arr)
lr_proba = lr.predict_proba(X_te_tfidf)
elapsed = time.time() - t0
res_lr = evaluate("Logistic Regression (TF-IDF)", lr, X_te_tfidf, y_test_arr, lr_proba)
res_lr["train_time"] = elapsed
results.append(res_lr)
print(f" Accuracy: {res_lr['accuracy']:.4f} | F1 Weighted: {res_lr['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
# ── 4c. Linear SVM (TF-IDF) ──────────────────────────────────────────
print("\n[3/5] Linear SVM (TF-IDF) …")
t0 = time.time()
svm = CalibratedClassifierCV(
LinearSVC(class_weight="balanced", max_iter=2000, random_state=42),
cv=3,
)
svm.fit(X_tr_tfidf, y_train_arr)
svm_proba = svm.predict_proba(X_te_tfidf)
elapsed = time.time() - t0
res_svm = evaluate("Linear SVM (TF-IDF)", svm, X_te_tfidf, y_test_arr, svm_proba)
res_svm["train_time"] = elapsed
results.append(res_svm)
print(f" Accuracy: {res_svm['accuracy']:.4f} | F1 Weighted: {res_svm['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
# ── 4d. Random Forest (TF-IDF only β€” no embeddings) ──────────────────
print("\n[4/5] Random Forest (TF-IDF only β€” no embeddings) …")
t0 = time.time()
rf_tf = RandomForestClassifier(
n_estimators=300,
max_depth=20,
min_samples_split=5,
min_samples_leaf=3,
class_weight="balanced",
n_jobs=-1,
random_state=42,
)
rf_tf.fit(X_tr_tfidf, y_train_arr)
rf_tf_proba = rf_tf.predict_proba(X_te_tfidf)
elapsed = time.time() - t0
res_rf_tf = evaluate("Random Forest (TF-IDF only)", rf_tf, X_te_tfidf, y_test_arr, rf_tf_proba)
res_rf_tf["train_time"] = elapsed
results.append(res_rf_tf)
print(f" Accuracy: {res_rf_tf['accuracy']:.4f} | F1 Weighted: {res_rf_tf['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
# ── 4e. YOUR Hybrid RF (TF-IDF + MiniLM + Stylometric) ───────────────
if not args.skip_minilm:
print("\n[5/5] β˜… YOUR Hybrid RF (TF-IDF + MiniLM + Stylometric) …")
X_tr_hy, X_te_hy = build_hybrid_features(X_train, X_test)
t0 = time.time()
max_depth = 8 if cebuano_only else 20
rf_hy = RandomForestClassifier(
n_estimators=500,
max_depth=max_depth,
min_samples_split=5,
min_samples_leaf=3,
class_weight="balanced",
n_jobs=-1,
random_state=42,
)
rf_hy.fit(X_tr_hy, y_train_arr)
rf_hy_proba = rf_hy.predict_proba(X_te_hy)
elapsed = time.time() - t0
res_rf_hy = evaluate("β˜… Hybrid RF (TF-IDF + MiniLM + Stylo)", rf_hy, X_te_hy, y_test_arr, rf_hy_proba)
res_rf_hy["train_time"] = elapsed
results.append(res_rf_hy)
print(f" Accuracy: {res_rf_hy['accuracy']:.4f} | F1 Weighted: {res_rf_hy['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
else:
print("\n[5/5] Skipping Hybrid RF (--skip-minilm flag set).")
# ── 5. Output table ───────────────────────────────────────────────────
print("\n" + "=" * 60)
print(" BENCHMARK SUMMARY")
print("=" * 60)
table_path = os.path.join(OUTPUT_DIR, f"benchmark_table_{args.mode}.txt")
save_table(results, table_path, args.mode, len(X_train), len(X_test))
# ── 6. Bar chart ──────────────────────────────────────────────────────
chart_path = os.path.join(OUTPUT_DIR, f"benchmark_chart_{args.mode}.png")
plot_bar_chart(results, chart_path)
# ── 7. ROC curves ─────────────────────────────────────────────────────
roc_path = os.path.join(OUTPUT_DIR, f"benchmark_roc_{args.mode}.png")
plot_roc_curves(results, y_test_arr, roc_path)
# ── 8. Train time summary ─────────────────────────────────────────────
print("\n Training times:")
for r in results:
t = r.get("train_time", 0)
print(f" {r['name']:<45} {t:>6.1f}s")
print("\n" + "=" * 60)
print(" BENCHMARK COMPLETE")
print(f" Results saved to: {OUTPUT_DIR}")
print("=" * 60)
if __name__ == "__main__":
main()