suicideproject / src /train_chat_brain.py
Antigravity Deploy Agent
Deploy Suicide Risk Detection web application to Hugging Face Spaces
0be18fb
Raw
History Blame Contribute Delete
9.96 kB
import json
import os
import numpy as np
import pandas as pd
import torch
from datasets import Dataset
from dotenv import load_dotenv
from sklearn.metrics import (
classification_report,
f1_score,
recall_score,
accuracy_score,
confusion_matrix,
)
from sklearn.model_selection import train_test_split
from sklearn.utils.class_weight import compute_class_weight
from transformers import (
AutoModelForSequenceClassification,
AutoTokenizer,
Trainer,
TrainingArguments,
)
from sklearn.metrics import (
precision_score,
roc_auc_score,
roc_curve,
precision_recall_curve,
auc,
)
from .model_registry import MODEL_REGISTRY
import matplotlib.pyplot as plt
import seaborn as sns
load_dotenv()
def tune_threshold(probs, y, min_recall=0.90):
best = None
for thr in np.linspace(0.05, 0.95, 181):
preds = (probs >= thr).astype(int)
rec = recall_score(y, preds, zero_division=0)
f1 = f1_score(y, preds, zero_division=0)
if rec >= min_recall:
if best is None or f1 > best["f1"]:
best = {"thr": float(thr), "recall": float(rec), "f1": float(f1)}
if best is None:
best = {"thr": 0.5, "recall": 0.0, "f1": 0.0}
return best
def train_chat_brain(
processed_dir="data/processed",
models_dir="outputs/models",
reports_dir="outputs/reports",
max_length=128, # 🔥 increased from 64
epochs=3,
lr=1e-5,
bs_train=16,
bs_eval=32,
seed=42,
):
os.makedirs(models_dir, exist_ok=True)
os.makedirs(reports_dir, exist_ok=True)
model_tag = os.getenv("CHAT_MODEL_TAG", "xlmr")
model_name = MODEL_REGISTRY[model_tag]
print(f"\n🚀 Training Model: {model_name}")
# Load data
df = pd.read_csv(os.path.join(processed_dir, "text_all_clean.csv"))
df = df.dropna(subset=["text"]).reset_index(drop=True)
# ✅ Stratified split
train_df, temp_df = train_test_split(
df, test_size=0.3, stratify=df["label"], random_state=seed
)
val_df, test_df = train_test_split(
temp_df, test_size=0.5, stratify=temp_df["label"], random_state=seed
)
print("Train/Val/Test:", train_df.shape, val_df.shape, test_df.shape)
# Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
def tokenize(batch):
return tokenizer(
batch["text"],
padding="max_length",
truncation=True,
max_length=max_length,
)
train_ds = Dataset.from_pandas(train_df[["text", "label"]]).map(tokenize, batched=True)
val_ds = Dataset.from_pandas(val_df[["text", "label"]]).map(tokenize, batched=True)
test_ds = Dataset.from_pandas(test_df[["text", "label"]]).map(tokenize, batched=True)
train_ds.set_format(type="torch", columns=["input_ids", "attention_mask", "label"])
val_ds.set_format(type="torch", columns=["input_ids", "attention_mask", "label"])
test_ds.set_format(type="torch", columns=["input_ids", "attention_mask", "label"])
# ✅ Proper class weights
y_train = train_df["label"].values
weights = compute_class_weight(class_weight="balanced", classes=np.array([0, 1]), y=y_train)
class_weights = torch.tensor(weights, dtype=torch.float)
print("Class weights:", class_weights.tolist())
# Model
model = AutoModelForSequenceClassification.from_pretrained(
model_name,
num_labels=2,
hidden_dropout_prob=0.3,
attention_probs_dropout_prob=0.3,
)
# Weighted Trainer
class WeightedTrainer(Trainer):
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
labels = inputs["labels"]
outputs = model(**inputs)
logits = outputs.logits
loss_fn = torch.nn.CrossEntropyLoss(weight=class_weights.to(logits.device))
loss = loss_fn(logits, labels)
return (loss, outputs) if return_outputs else loss
# Metrics
def compute_metrics(eval_pred):
logits, labels = eval_pred
preds = np.argmax(logits, axis=1)
return {
"accuracy": accuracy_score(labels, preds),
"f1": f1_score(labels, preds),
"recall": recall_score(labels, preds),
}
args = TrainingArguments(
output_dir=os.path.join(models_dir, f"chat_brain_{model_tag}"),
eval_strategy="epoch",
save_strategy="no",
learning_rate=lr,
per_device_train_batch_size=bs_train,
per_device_eval_batch_size=bs_eval,
num_train_epochs=epochs,
logging_steps=50,
report_to="none",
fp16=torch.cuda.is_available(),
seed=seed,
)
trainer = WeightedTrainer(
model=model,
args=args,
train_dataset=train_ds,
eval_dataset=val_ds,
compute_metrics=compute_metrics,
)
# Train
trainer.train()
# Predictions
def get_probs(ds):
pred = trainer.predict(ds)
logits = pred.predictions
probs = torch.softmax(torch.tensor(logits), dim=1).numpy()[:, 1]
labels = pred.label_ids
return probs, labels
val_probs, val_y = get_probs(val_ds)
test_probs, test_y = get_probs(test_ds)
# Threshold tuning
best = tune_threshold(val_probs, val_y, min_recall=0.90)
thr = best["thr"]
test_pred = (test_probs >= thr).astype(int)
# ===================== FINAL METRICS =====================
report = classification_report(test_y, test_pred, digits=4)
cm = confusion_matrix(test_y, test_pred)
print("\n✅ Best Threshold:", best)
print("\n📊 Classification Report:\n", report)
print("\n📊 Confusion Matrix:\n", cm)
# ===================== SAVE REPORT =====================
report_path = os.path.join(reports_dir, f"{model_tag}_report.txt")
with open(report_path, "w", encoding="utf-8") as f:
f.write(f"Model: {model_name}\n\n")
f.write("Best Threshold:\n")
f.write(json.dumps(best, indent=2))
f.write("\n\nClassification Report:\n")
f.write(report)
f.write("\n\nConfusion Matrix:\n")
f.write(np.array2string(cm))
print(f"\n✅ Report saved at: {report_path}")
# ===================== CONFUSION MATRIX PLOT =====================
plt.figure(figsize=(6, 5))
sns.heatmap(
cm,
annot=True,
fmt="d",
cmap="Blues",
xticklabels=["Non-Suicidal", "Suicidal"],
yticklabels=["Non-Suicidal", "Suicidal"]
)
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title(f"Confusion Matrix - {model_tag}")
cm_path = os.path.join(reports_dir, f"{model_tag}_confusion_matrix.png")
plt.savefig(cm_path)
plt.close()
print(f"✅ Confusion matrix saved at: {cm_path}")
# ===================== EXTRA METRICS =====================
accuracy = accuracy_score(test_y, test_pred)
precision = precision_score(test_y, test_pred)
recall = recall_score(test_y, test_pred)
f1 = f1_score(test_y, test_pred)
# AUC (uses probabilities, not labels)
roc_auc = roc_auc_score(test_y, test_probs)
# Specificity
tn, fp, fn, tp = cm.ravel()
specificity = tn / (tn + fp)
# Balanced Accuracy
balanced_acc = (recall + specificity) / 2
metrics_dict = {
"Accuracy": accuracy,
"Precision": precision,
"Recall": recall,
"F1 Score": f1,
"AUC": roc_auc,
"Specificity": specificity,
"Balanced Acc": balanced_acc,
}
print("\n📊 All Metrics:\n", metrics_dict)
# ===================== METRICS BAR CHART =====================
# ===================== METRICS BAR CHART (WITH LABELS) =====================
plt.figure(figsize=(8, 5))
names = list(metrics_dict.keys())
values = list(metrics_dict.values())
bars = plt.bar(names, values)
plt.ylim(0, 1)
plt.title(f"Model Performance - {model_tag}")
plt.xticks(rotation=30)
# 🔥 Add value labels on top of each bar
for bar in bars:
height = bar.get_height()
plt.text(
bar.get_x() + bar.get_width() / 2,
height + 0.02, # slight offset above bar
f"{height:.3f}", # format value
ha='center',
va='bottom',
fontsize=9
)
plt.tight_layout()
bar_path = os.path.join(reports_dir, f"{model_tag}_metrics_bar.png")
plt.savefig(bar_path)
plt.close()
print(f"✅ Metrics bar chart saved at: {bar_path}")
# ===================== ROC CURVE =====================
fpr, tpr, _ = roc_curve(test_y, test_probs)
plt.figure()
plt.plot(fpr, tpr, label=f"AUC = {roc_auc:.4f}")
plt.plot([0, 1], [0, 1], linestyle="--")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title(f"ROC Curve - {model_tag}")
plt.legend()
roc_path = os.path.join(reports_dir, f"{model_tag}_roc_curve.png")
plt.savefig(roc_path)
plt.close()
print(f"✅ ROC curve saved at: {roc_path}")
# ===================== PRECISION-RECALL CURVE =====================
precisions, recalls, _ = precision_recall_curve(test_y, test_probs)
pr_auc = auc(recalls, precisions)
plt.figure()
plt.plot(recalls, precisions, label=f"PR AUC = {pr_auc:.4f}")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.title(f"Precision-Recall Curve - {model_tag}")
plt.legend()
pr_path = os.path.join(reports_dir, f"{model_tag}_pr_curve.png")
plt.savefig(pr_path)
plt.close()
print(f"✅ PR curve saved at: {pr_path}")
# ===================== SAVE MODEL =====================
model_dir = os.path.join(models_dir, f"chat_brain_{model_tag}")
trainer.save_model(model_dir)
tokenizer.save_pretrained(model_dir)
with open(os.path.join(model_dir, "threshold.json"), "w") as f:
json.dump(best, f, indent=2)
return best