#!/usr/bin/env python3 """ Retrain emotion LoRA on complete dataset. """ import json import random import os import shutil import torch from pathlib import Path from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback from peft import LoraConfig, get_peft_model, TaskType from datasets import Dataset from sklearn.metrics import f1_score, accuracy_score # Load emotion data from labeled samples emotion_data = [] # Load from labeled files that have emotion info for fname in ["labeled_verified.jsonl", "labeled_expanded.jsonl", "labeled_real_world.jsonl"]: path = Path(f"/mnt/dolphinng5_predict/sentiment_engine/data/{fname}") if path.exists(): with open(path) as f: for line in f: try: item = json.loads(line.strip()) labels = item.get("labels", {}) text = item.get("text", item.get("raw_text", "")) if text and labels.get("emotions"): # Convert emotions to multi-label emotion_labels = ["joy", "fear", "anger", "greed", "sadness", "neutral"] multi_label = [0.0] * 6 for emo in labels["emotions"]: if emo in emotion_labels: multi_label[emotion_labels.index(emo)] = 1.0 if sum(multi_label) > 0: emotion_data.append({"text": text, "labels": multi_label}) except Exception as e: pass # Also add synthetic emotion data from templates EMOTION_TEMPLATES = { "greed": ["FOMO driving {asset} to ${price}", "Buy the dip on {asset}! Loading bags", "Whale buying {asset} aggressively", "All in on {asset}! Diamond hands"], "fear": ["Panic selling {asset} at ${price}", "Major hack drains {asset} liquidity", "SEC crackdown sends {asset} plummeting", "Support broken on {asset}"], "joy": ["{asset} hits new ATH at ${price}! To the moon!", "ETF approved for {asset}!", "Massive gains on {asset}!"], "anger": ["Rug pull on {asset}! Devs drained liquidity!", "Exchange froze {asset} withdrawals again!", "Market manipulation on {asset}!"], "sadness": ["Lost life savings on {asset} crash", "Bag holder on {asset}... down 90%", "Rekt on {asset} leverage"], "neutral": ["{asset} consolidates at ${price}", "Low volume on {asset} at ${price}", "Market choppy for {asset}"], } ASSETS = ["BTC", "ETH", "SOL", "AVAX", "MATIC", "DOT", "LINK", "ARB", "OP", "NEAR", "FET", "STX", "ZIL", "XTZ", "ENJ", "ETC", "TRX", "LTC", "DASH", "ONG", "ONE", "ALGO", "DOGE", "XLM", "ATOM", "KSM", "APT", "SUI", "ICP", "QNT", "INJ"] # Generate synthetic emotion data emotion_labels = ["joy", "fear", "anger", "greed", "sadness", "neutral"] for emotion, templates in EMOTION_TEMPLATES.items(): for template in templates: for _ in range(5): asset = random.choice(ASSETS) price = random.randint(100, 100000) text = template.format(asset=asset, price=price) labels = [0.0] * 6 labels[emotion_labels.index(emotion)] = 1.0 emotion_data.append({"text": text, "labels": labels}) # Also add from labeled data for fname in ["labeled_verified.jsonl", "labeled_expanded.jsonl", "labeled_real_world.jsonl"]: path = Path(f"/mnt/dolphinng5_predict/sentiment_engine/data/{fname}") if path.exists(): with open(path) as f: for line in f: try: item = json.loads(line.strip()) labels = item.get("labels", {}) text = item.get("text", item.get("raw_text", "")) if text and labels.get("emotions"): multi_label = [0.0] * 6 for emo in labels["emotions"]: if emo in emotion_labels: multi_label[emotion_labels.index(emo)] = 1.0 if sum(multi_label) > 0: emotion_data.append({"text": text, "labels": multi_label}) except Exception as e: pass print(f"Total emotion samples: {len(emotion_data)}") # Split random.shuffle(emotion_data) split = int(0.9 * len(emotion_data)) train_data = emotion_data[:split] val_data = emotion_data[split:] print(f"Train: {len(train_data)} | Val: {len(val_data)}") # Label mapping label2id = {l: i for i, l in enumerate(emotion_labels)} id2label = {i: l for i, l in enumerate(emotion_labels)} # Dataset train_ds = Dataset.from_dict({"text": [d["text"] for d in train_data], "labels": [d["labels"] for d in train_data]}) val_ds = Dataset.from_dict({"text": [d["text"] for d in val_data], "labels": [d["labels"] for d in val_data]}) tokenizer = AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base") def tokenize(batch): return tokenizer(batch["text"], truncation=True, max_length=128, padding="max_length") train_ds = train_ds.map(lambda b: tokenizer(b["text"], truncation=True, max_length=128, padding="max_length"), batched=True) val_ds = val_ds.map(lambda b: tokenizer(b["text"], truncation=True, max_length=128, padding="max_length"), batched=True) train_ds.set_format("torch", columns=["input_ids", "attention_mask", "labels"]) val_ds.set_format("torch", columns=["input_ids", "attention_mask", "labels"]) # Model + LoRA model = AutoModelForSequenceClassification.from_pretrained( "j-hartmann/emotion-english-distilroberta-base", ignore_mismatched_sizes=True, num_labels=6, id2label=id2label, label2id=label2id, problem_type="multi_label_classification", ) lora_config = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.1, target_modules=("query", "key", "value", "intermediate.dense", "output.dense"), bias="none", task_type=TaskType.SEQ_CLS ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # Weighted loss emotion_labels_list = ["joy", "fear", "anger", "greed", "sadness", "neutral"] crypto_weights = {l: 1.0 for l in emotion_labels_list} crypto_weights["greed"] = 2.0 crypto_weights["fear"] = 2.0 crypto_weights["joy"] = 1.5 class_weights = torch.tensor([crypto_weights[l] for l in emotion_labels_list], dtype=torch.float) class WeightedTrainer(Trainer): def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): labels = inputs.pop("labels") outputs = model(**inputs) logits = outputs.logits loss_fct = torch.nn.BCEWithLogitsLoss(pos_weight=class_weights.to(logits.device)) loss = loss_fct(logits, labels.float()) return (loss, outputs) if return_outputs else loss output_dir = "./models/lora-distilroberta-crypto-emotion-v2" os.makedirs(output_dir, exist_ok=True) if os.path.exists(output_dir): shutil.rmtree(output_dir) training_args = TrainingArguments( output_dir=output_dir, num_train_epochs=5, per_device_train_batch_size=8, per_device_eval_batch_size=16, gradient_accumulation_steps=4, learning_rate=2e-4, warmup_ratio=0.1, weight_decay=0.01, max_grad_norm=1.0, eval_strategy="steps", eval_steps=25, save_strategy="steps", save_steps=25, save_total_limit=1, load_best_model_at_end=True, metric_for_best_model="f1_macro", greater_is_better=True, fp16=False, dataloader_num_workers=0, logging_steps=10, remove_unused_columns=False, report_to="none", seed=42, ) def compute_metrics(eval_pred): logits, labels = eval_pred preds = (torch.sigmoid(torch.tensor(logits)) > 0.5).int().numpy() return { "f1_macro": f1_score(labels, preds, average="macro", zero_division=0), "f1_micro": f1_score(labels, preds, average="micro", zero_division=0), "accuracy": (preds == labels).mean(), } trainer = WeightedTrainer( model=model, args=training_args, train_dataset=Dataset.from_dict({ "input_ids": tokenizer([d["text"] for d in train_data], truncation=True, max_length=128, padding="max_length")["input_ids"], "attention_mask": tokenizer([d["text"] for d in train_data], truncation=True, max_length=128, padding="max_length")["attention_mask"], "labels": [d["labels"] for d in train_data], }), eval_dataset=Dataset.from_dict({ "input_ids": tokenizer([d["text"] for d in val_data], truncation=True, max_length=128, padding="max_length")["input_ids"], "attention_mask": tokenizer([d["text"] for d in val_data], truncation=True, max_length=128, padding="max_length")["attention_mask"], "labels": [d["labels"] for d in val_data], }), tokenizer=tokenizer, compute_metrics=compute_metrics, callbacks=[EarlyStoppingCallback(early_stopping_patience=3, early_stopping_threshold=0.001)], ) print("๐Ÿ‹๏ธ Training emotion LoRA v2...") trainer.train() best_path = "./models/lora-distilroberta-crypto-emotion-v2/best" trainer.save_model(best_path) AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base").save_pretrained(best_path) print(f"โœ… Saved to {best_path}") # Test print("\n๐Ÿงช Testing emotion model...") model.eval() labels = ["joy", "fear", "anger", "greed", "sadness", "neutral"] test_texts = [ "BTC breaks 100k! New ATH, to the moon!", "Major hack on DeFi protocol, 50M drained", "Panic selling BTC at 50k, liquidation cascade", "FOMO buying ETH at 3k, loading bags", "Rug pull suspected, dev wallet drained liquidity", "BTC consolidates at 50k, no clear direction", "SEC sues exchange, regulatory crackdown", "ETF approved! Celebration time!", ] from peft import PeftModel model = PeftModel.from_pretrained( AutoModelForSequenceClassification.from_pretrained("j-hartmann/emotion-english-distilroberta-base", ignore_mismatched_sizes=True, num_labels=6, problem_type="multi_label_classification"), "./models/lora-distilroberta-crypto-emotion-v2/best" ) model.eval() for text in test_texts: inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): logits = model(**inputs).logits probs = torch.sigmoid(logits)[0] active = [(labels[i], probs[i].item()) for i in range(6) if probs[i] > 0.3] print(f'{text[:50]}') print(f' Active: {active}') print() label_map = {0: "Bearish", 1: "Bullish", 2: "Neutral"}