Spaces:
Runtime error
Runtime error
| """ | |
| 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() | |