Text Classification
Transformers
ONNX
English
modernbert
int8
vox
memory-graph
edge-classifier
Eval Results (legacy)
Instructions to use addyo07/modernbert-vox-cognitive-edge-classifier with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use addyo07/modernbert-vox-cognitive-edge-classifier with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="addyo07/modernbert-vox-cognitive-edge-classifier")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("addyo07/modernbert-vox-cognitive-edge-classifier", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| import os | |
| # Force PyTorch to ignore CUDA completely so 0 MB GPU VRAM is allocated | |
| os.environ["CUDA_VISIBLE_DEVICES"] = "" | |
| import sys | |
| import json | |
| import shutil | |
| import subprocess | |
| import torch | |
| import torch.nn as nn | |
| import numpy as np | |
| from collections import Counter | |
| from sklearn.model_selection import StratifiedGroupKFold | |
| from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix | |
| from transformers import ( | |
| AutoTokenizer, | |
| AutoModelForSequenceClassification, | |
| Trainer, | |
| TrainingArguments, | |
| DataCollatorWithPadding, | |
| TrainerCallback | |
| ) | |
| # Label Mapping for Vox v7 Gate 3 Operational Edge Ontology | |
| LABEL2ID = { | |
| "SHAPES": 0, | |
| "DEPENDS_ON": 1, | |
| "CONFLICTS_WITH": 2, | |
| "NONE": 3 | |
| } | |
| ID2LABEL = {v: k for k, v in LABEL2ID.items()} | |
| DATASET_PATH = os.path.expanduser("~/.vox/sandbox/dataset.json") | |
| OUTPUT_DIR = os.path.expanduser("~/.vox/sandbox/output_6e") | |
| METRICS_LOG_PATH = os.path.expanduser("~/.vox/sandbox/training_metrics_6e.json") | |
| ONNX_EXPORT_PATH = os.path.expanduser("~/.vox/sandbox/model_quantized_v2.onnx") | |
| MODEL_NAME = "answerdotai/ModernBERT-base" | |
| def format_input(fact_a, fact_b, context): | |
| return f"Fact A: {fact_a} | Fact B: {fact_b} | Context: {context}" | |
| class WeightedLossTrainer(Trainer): | |
| def __init__(self, class_weights=None, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| if class_weights is not None: | |
| self.class_weights = torch.tensor(class_weights, dtype=torch.float32).to(self.args.device) | |
| else: | |
| self.class_weights = None | |
| def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): | |
| labels = inputs.get("labels") | |
| outputs = model(**inputs) | |
| logits = outputs.get("logits") | |
| if self.class_weights is not None: | |
| loss_fct = nn.CrossEntropyLoss(weight=self.class_weights) | |
| else: | |
| loss_fct = nn.CrossEntropyLoss() | |
| loss = loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1)) | |
| return (loss, outputs) if return_outputs else loss | |
| class MetricsLoggerCallback(TrainerCallback): | |
| def __init__(self, log_file_path): | |
| self.log_file_path = log_file_path | |
| self.metrics_history = [] | |
| def on_log(self, args, state, control, logs=None, **kwargs): | |
| if logs: | |
| entry = {"step": state.global_step, "epoch": state.epoch} | |
| entry.update(logs) | |
| self.metrics_history.append(entry) | |
| with open(self.log_file_path, "w") as f: | |
| json.dump(self.metrics_history, f, indent=2) | |
| def compute_metrics(eval_pred): | |
| logits, labels = eval_pred | |
| preds = np.argmax(logits, axis=1) | |
| acc = accuracy_score(labels, preds) | |
| precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average="macro", zero_division=0) | |
| return { | |
| "accuracy": acc, | |
| "precision": precision, | |
| "recall": recall, | |
| "f1": f1 | |
| } | |
| def evaluate_threshold_sweep(logits, true_labels): | |
| """ | |
| Sweeps confidence thresholds tau in [0.50 .. 0.85]. | |
| If max(P(positive_edge)) < tau, defaults prediction to NONE (3). | |
| Calculates Positive Edge Precision and False Positive Edge Rate. | |
| """ | |
| probs = torch.softmax(torch.tensor(logits), dim=-1).numpy() | |
| print("\n" + "="*80) | |
| print("🎯 CONSERVATIVE THRESHOLD SWEEP FOR GRAPH PURITY (DEFAULT TO 'NONE')") | |
| print("="*80) | |
| print(f"{'Tau':<8} | {'Overall Acc':<12} | {'Pos Edge Precision':<20} | {'FP Edge Rate':<18} | {'NONE Recall':<12}") | |
| print("-" * 80) | |
| best_tau = 0.50 | |
| best_precision = 0.0 | |
| results = [] | |
| for tau in np.arange(0.50, 0.86, 0.05): | |
| thresholded_preds = [] | |
| for i in range(len(probs)): | |
| p = probs[i] | |
| pos_probs = p[:3] # SHAPES, DEPENDS_ON, CONFLICTS_WITH | |
| max_pos_idx = np.argmax(pos_probs) | |
| max_pos_prob = pos_probs[max_pos_idx] | |
| if max_pos_prob >= tau: | |
| thresholded_preds.append(max_pos_idx) | |
| else: | |
| thresholded_preds.append(3) # Default to NONE | |
| thresholded_preds = np.array(thresholded_preds) | |
| acc = accuracy_score(true_labels, thresholded_preds) | |
| # Positive Edge Precision: Of all predicted positive edges (0, 1, 2), how many were actually correct? | |
| pos_mask = (thresholded_preds < 3) | |
| if np.sum(pos_mask) > 0: | |
| pos_precision = accuracy_score(true_labels[pos_mask], thresholded_preds[pos_mask]) | |
| else: | |
| pos_precision = 1.0 | |
| # False Positive Edge Rate: Of all actual NONE pairs (3), how many were incorrectly assigned a positive edge? | |
| actual_none_mask = (true_labels == 3) | |
| if np.sum(actual_none_mask) > 0: | |
| fp_edge_rate = np.mean(thresholded_preds[actual_none_mask] < 3) | |
| none_recall = np.mean(thresholded_preds[actual_none_mask] == 3) | |
| else: | |
| fp_edge_rate = 0.0 | |
| none_recall = 1.0 | |
| print(f"τ = {tau:.2f} | {acc*100:6.2f}% | {pos_precision*100:14.2f}% | {fp_edge_rate*100:12.2f}% | {none_recall*100:8.2f}%") | |
| results.append({ | |
| "tau": float(tau), | |
| "accuracy": float(acc), | |
| "positive_edge_precision": float(pos_precision), | |
| "false_positive_edge_rate": float(fp_edge_rate), | |
| "none_recall": float(none_recall) | |
| }) | |
| if pos_precision >= 0.88 and pos_precision > best_precision: | |
| best_precision = pos_precision | |
| best_tau = float(tau) | |
| print("="*80) | |
| print(f"💡 Recommended Graph Conservation Threshold: τ = {best_tau:.2f} (Pos Edge Precision: {best_precision*100:.2f}%)\n") | |
| return results, best_tau | |
| class DatasetDictWrapper(torch.utils.data.Dataset): | |
| def __init__(self, encodings, labels): | |
| self.encodings = encodings | |
| self.labels = labels | |
| def __getitem__(self, idx): | |
| item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()} | |
| item["labels"] = torch.tensor(self.labels[idx]) | |
| return item | |
| def __len__(self): | |
| return len(self.labels) | |
| def main(): | |
| print("🚀 Starting Vox v7 Gate 3 6-Epoch ModernBERT Fine-Tuning Pipeline (Pure CPU Mode)...") | |
| # 1. Load Dataset | |
| if not os.path.exists(DATASET_PATH): | |
| raise FileNotFoundError(f"Dataset file not found at {DATASET_PATH}") | |
| with open(DATASET_PATH, 'r') as f: | |
| data = json.load(f) | |
| pairs = data.get('pairs', data) | |
| print(f"Loaded {len(pairs)} ground-truth pairs from {DATASET_PATH}") | |
| # 2. Extract Texts, Labels, and Group Keys (Fact A) | |
| texts = [format_input(p['fact_a'], p['fact_b'], p['context']) for p in pairs] | |
| labels = [LABEL2ID[p['expected_label']] for p in pairs] | |
| groups = [p['fact_a'].strip().lower() for p in pairs] | |
| # 3. Stratified Group Split (80% Train / 10% Val / 10% Test) — Zero Fact Leakage | |
| np.random.seed(42) | |
| indices = np.arange(len(pairs)) | |
| sgkf1 = StratifiedGroupKFold(n_splits=10) | |
| train_val_idx, test_idx = next(sgkf1.split(indices, labels, groups)) | |
| train_val_labels = [labels[i] for i in train_val_idx] | |
| train_val_groups = [groups[i] for i in train_val_idx] | |
| sgkf2 = StratifiedGroupKFold(n_splits=9) | |
| train_sub_idx, val_sub_idx = next(sgkf2.split(train_val_idx, train_val_labels, train_val_groups)) | |
| train_idx = train_val_idx[train_sub_idx] | |
| val_idx = train_val_idx[val_sub_idx] | |
| # Verify zero group leakage between Train and Test | |
| train_groups = set(groups[i] for i in train_idx) | |
| test_groups = set(groups[i] for i in test_idx) | |
| overlap = train_groups.intersection(test_groups) | |
| print(f"Dataset Splits -> Train: {len(train_idx)} | Val: {len(val_idx)} | Test: {len(test_idx)}") | |
| print(f"Fact Group Leakage Count between Train & Test: {len(overlap)} (MUST BE 0)") | |
| # Inverse Class Frequency Weights | |
| train_labels = [labels[i] for i in train_idx] | |
| label_counts = Counter(train_labels) | |
| total_samples = len(train_labels) | |
| num_classes = 4 | |
| class_weights = [total_samples / (num_classes * label_counts[i]) for i in range(num_classes)] | |
| # Tokenizer | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) | |
| def encode_split(split_idx): | |
| split_texts = [texts[i] for i in split_idx] | |
| split_labels = [labels[i] for i in split_idx] | |
| encodings = tokenizer(split_texts, truncation=True, max_length=256, padding=False) | |
| return DatasetDictWrapper(encodings, split_labels) | |
| train_dataset = encode_split(train_idx) | |
| val_dataset = encode_split(val_idx) | |
| test_dataset = encode_split(test_idx) | |
| # --------------------------------------------------------------------- | |
| # PHASE 1: Fine-Tuning ModernBERT-base for 6 Epochs | |
| # --------------------------------------------------------------------- | |
| print("🏋️ PHASE 1: Fine-Tuning ModernBERT-base for 6 Epochs on Pure CPU...") | |
| fine_tune_model = AutoModelForSequenceClassification.from_pretrained( | |
| MODEL_NAME, | |
| num_labels=4, | |
| id2label=ID2LABEL, | |
| label2id=LABEL2ID | |
| ) | |
| training_args = TrainingArguments( | |
| output_dir=OUTPUT_DIR, | |
| eval_strategy="steps", | |
| eval_steps=50, | |
| save_strategy="steps", | |
| save_steps=50, | |
| save_total_limit=2, | |
| learning_rate=3e-5, | |
| per_device_train_batch_size=32, | |
| gradient_accumulation_steps=1, | |
| per_device_eval_batch_size=64, | |
| num_train_epochs=6, | |
| lr_scheduler_type="cosine", | |
| weight_decay=0.01, | |
| warmup_ratio=0.10, | |
| logging_steps=25, | |
| load_best_model_at_end=True, | |
| metric_for_best_model="f1", | |
| greater_is_better=True, | |
| use_cpu=True, | |
| report_to="none" | |
| ) | |
| metrics_callback = MetricsLoggerCallback(METRICS_LOG_PATH) | |
| trainer = WeightedLossTrainer( | |
| class_weights=class_weights, | |
| model=fine_tune_model, | |
| args=training_args, | |
| train_dataset=train_dataset, | |
| eval_dataset=val_dataset, | |
| tokenizer=tokenizer, | |
| data_collator=DataCollatorWithPadding(tokenizer=tokenizer), | |
| compute_metrics=compute_metrics, | |
| callbacks=[metrics_callback] | |
| ) | |
| trainer.train() | |
| best_model_dir = os.path.join(OUTPUT_DIR, "best_model") | |
| trainer.save_model(best_model_dir) | |
| tokenizer.save_pretrained(best_model_dir) | |
| print(f"Saved best 6-epoch PyTorch model to {best_model_dir}") | |
| # --------------------------------------------------------------------- | |
| # PHASE 2: Fine-Tuned Model Test Evaluation & Threshold Sweep | |
| # --------------------------------------------------------------------- | |
| print("\n🎯 PHASE 2: Evaluating 6-Epoch Model & Sweeping Confidence Thresholds...") | |
| ft_eval = trainer.predict(test_dataset) | |
| test_true_labels = np.array([labels[i] for i in test_idx]) | |
| sweep_results, recommended_tau = evaluate_threshold_sweep(ft_eval.predictions, test_true_labels) | |
| # Save Calibration Results to JSON | |
| calib_json_path = os.path.expanduser("~/.vox/sandbox/threshold_calibration.json") | |
| with open(calib_json_path, "w") as f: | |
| json.dump({"sweep": sweep_results, "recommended_tau": recommended_tau}, f, indent=2) | |
| # --------------------------------------------------------------------- | |
| # PHASE 3: Export to INT8 Dynamic ONNX (model_quantized_v2.onnx) | |
| # --------------------------------------------------------------------- | |
| print("📦 PHASE 3: Exporting Fine-Tuned Model to INT8 Dynamic ONNX...") | |
| from optimum.onnxruntime import ORTModelForSequenceClassification, ORTQuantizer | |
| from optimum.onnxruntime.configuration import AutoQuantizationConfig | |
| onnx_temp_dir = os.path.join(OUTPUT_DIR, "onnx_temp") | |
| print(" Exporting PyTorch weights to FP32 ONNX...") | |
| ort_model = ORTModelForSequenceClassification.from_pretrained(best_model_dir, export=True) | |
| ort_model.save_pretrained(onnx_temp_dir) | |
| print(" Applying Dynamic INT8 Quantization (qint8)...") | |
| quantizer = ORTQuantizer.from_pretrained(onnx_temp_dir) | |
| dqconfig = AutoQuantizationConfig.avx512_vnni(is_static=False, per_channel=False) | |
| quantized_dir = os.path.join(OUTPUT_DIR, "onnx_quantized") | |
| quantizer.quantize(save_dir=quantized_dir, quantization_config=dqconfig) | |
| source_onnx = os.path.join(quantized_dir, "model_quantized.onnx") | |
| if not os.path.exists(source_onnx): | |
| source_onnx = os.path.join(quantized_dir, "model.onnx") | |
| shutil.copy(source_onnx, ONNX_EXPORT_PATH) | |
| file_size_mb = os.path.getsize(ONNX_EXPORT_PATH) / (1024 * 1024) | |
| print(f"✅ Exported 6-Epoch INT8 ONNX Model to {ONNX_EXPORT_PATH} ({file_size_mb:.2f} MB)") | |
| # --------------------------------------------------------------------- | |
| # PHASE 4: Proactive Disk Cleanup | |
| # --------------------------------------------------------------------- | |
| print("\n🧹 PHASE 4: Executing Disk Cleanup...") | |
| if os.path.exists(OUTPUT_DIR): | |
| shutil.rmtree(OUTPUT_DIR) | |
| print(f" Removed temporary training dir: {OUTPUT_DIR}") | |
| hf_cache_dir = os.path.expanduser("~/.cache/huggingface") | |
| if os.path.exists(hf_cache_dir): | |
| shutil.rmtree(hf_cache_dir) | |
| print(f" Cleared HuggingFace download cache: {hf_cache_dir}") | |
| df_output = subprocess.check_output(["df", "-h", os.path.expanduser("~")]).decode("utf-8") | |
| print(f"\nServer Storage Status:\n{df_output}") | |
| print("🎉 6-Epoch Pipeline execution complete!") | |
| if __name__ == "__main__": | |
| main() | |