Spaces:
Running
Running
| import os | |
| import logging | |
| import pandas as pd | |
| import numpy as np | |
| import joblib | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer | |
| from sklearn.naive_bayes import MultinomialNB | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.svm import LinearSVC | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.metrics import ( | |
| accuracy_score, precision_score, recall_score, | |
| f1_score, classification_report, confusion_matrix | |
| ) | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| log = logging.getLogger(__name__) | |
| # Paths | |
| BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| DATA_PATH = os.path.join(BASE, "data", "balanced_corpus.csv") | |
| MODELS_DIR = os.path.join(BASE, "models") | |
| RESULTS_DIR = os.path.join(BASE, "results") | |
| os.makedirs(MODELS_DIR, exist_ok=True) | |
| os.makedirs(RESULTS_DIR, exist_ok=True) | |
| # Load data | |
| log.info("Loading balanced corpus...") | |
| df = pd.read_csv(DATA_PATH) | |
| df = df.dropna(subset=["preprocessed_text"]) | |
| log.info(f"Dataset: {df.shape[0]} rows | {df['toxic'].value_counts().to_dict()}") | |
| X = df["preprocessed_text"] | |
| y = df["toxic"] | |
| X_train, X_test, y_train, y_test = train_test_split( | |
| X, y, test_size=0.2, random_state=42, stratify=y | |
| ) | |
| log.info(f"Train: {len(X_train)} | Test: {len(X_test)}") | |
| # Vectorizers | |
| vectorizers = { | |
| "tfidf": TfidfVectorizer(max_features=50000, ngram_range=(1, 1)), | |
| "bow": CountVectorizer(max_features=50000, ngram_range=(1, 1)), | |
| } | |
| # Models | |
| models = { | |
| "naive_bayes": MultinomialNB(), | |
| "logistic_regression": LogisticRegression(max_iter=1000, random_state=42), | |
| "svm": LinearSVC(max_iter=2000, random_state=42), | |
| } | |
| # train | |
| results = [] | |
| for vec_name, vectorizer in vectorizers.items(): | |
| log.info(f"Fitting vectorizer: {vec_name}") | |
| X_train_vec = vectorizer.fit_transform(X_train) | |
| X_test_vec = vectorizer.transform(X_test) | |
| # save vectorizer | |
| vec_path = os.path.join(MODELS_DIR, f"vectorizer_{vec_name}.joblib") | |
| joblib.dump(vectorizer, vec_path) | |
| log.info(f"Saved vectorizer → {vec_path}") | |
| for model_name, model in models.items(): | |
| log.info(f"Training: {model_name} + {vec_name}") | |
| model_clone = type(model)(**model.get_params()) | |
| model_clone.fit(X_train_vec, y_train) | |
| y_pred = model_clone.predict(X_test_vec) | |
| acc = accuracy_score(y_test, y_pred) | |
| prec = precision_score(y_test, y_pred, average="weighted") | |
| rec = recall_score(y_test, y_pred, average="weighted") | |
| f1 = f1_score(y_test, y_pred, average="weighted") | |
| results.append({ | |
| "model": model_name, | |
| "vectorizer": vec_name, | |
| "accuracy": round(acc, 4), | |
| "precision": round(prec, 4), | |
| "recall": round(rec, 4), | |
| "f1": round(f1, 4), | |
| }) | |
| log.info(f" acc={acc:.4f} | prec={prec:.4f} | rec={rec:.4f} | f1={f1:.4f}") | |
| print(f"\n{'='*60}") | |
| print(f"{model_name.upper()} + {vec_name.upper()}") | |
| print(f"{'='*60}") | |
| print(classification_report(y_test, y_pred, target_names=["Non-Toxic", "Toxic"])) | |
| # save model | |
| model_path = os.path.join(MODELS_DIR, f"{model_name}_{vec_name}.joblib") | |
| joblib.dump(model_clone, model_path) | |
| log.info(f" Saved model → {model_path}") | |
| # confusion matrix plot | |
| cm = confusion_matrix(y_test, y_pred) | |
| fig, ax = plt.subplots(figsize=(5, 4)) | |
| sns.heatmap( | |
| cm, annot=True, fmt="d", cmap="Blues", | |
| xticklabels=["Non-Toxic", "Toxic"], | |
| yticklabels=["Non-Toxic", "Toxic"], | |
| ax=ax | |
| ) | |
| ax.set_title(f"Confusion Matrix\n{model_name} + {vec_name}") | |
| ax.set_ylabel("Actual") | |
| ax.set_xlabel("Predicted") | |
| plt.tight_layout() | |
| cm_path = os.path.join(RESULTS_DIR, f"cm_{model_name}_{vec_name}.png") | |
| plt.savefig(cm_path, dpi=150) | |
| plt.close() | |
| # Save comparison table | |
| results_df = pd.DataFrame(results).sort_values("f1", ascending=False) | |
| results_path = os.path.join(RESULTS_DIR, "model_comparison.csv") | |
| results_df.to_csv(results_path, index=False) | |
| print(f"\n{'='*60}") | |
| print("MODEL COMPARISON (sorted by F1)") | |
| print(f"{'='*60}") | |
| print(results_df.to_string(index=False)) | |
| # Model comparison bar chart | |
| fig, ax = plt.subplots(figsize=(10, 5)) | |
| x = np.arange(len(results_df)) | |
| bars = ax.bar(x, results_df["f1"], color=["#4C72B0", "#DD8452", "#55A868", | |
| "#C44E52", "#8172B2", "#937860"]) | |
| ax.set_xticks(x) | |
| ax.set_xticklabels( | |
| [f"{r['model'].replace('_', ' ').title()}\n({r['vectorizer'].upper()})" | |
| for _, r in results_df.iterrows()], | |
| fontsize=9 | |
| ) | |
| ax.set_ylabel("Weighted F1 Score") | |
| ax.set_title("Model Comparison — Classical Baselines") | |
| ax.set_ylim(0.85, 1.0) | |
| for bar, val in zip(bars, results_df["f1"]): | |
| ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.001, | |
| f"{val:.4f}", ha="center", va="bottom", fontsize=9) | |
| plt.tight_layout() | |
| chart_path = os.path.join(RESULTS_DIR, "model_comparison_chart.png") | |
| plt.savefig(chart_path, dpi=150) | |
| plt.close() | |
| log.info(f"Saved comparison chart → {chart_path}") | |
| log.info("Done.") |