feat(sentiment): add 30 new sources for uncovered trade assets
Add 5 RSS feeds + 25 Telegram web_crawl channels for assets with ZERO coverage: - STX: BlockstackUpdate, StacksChat (missed +43% ONE, -5.65% STX) - FET: fetch_ai_announcements, fetch_ai (missed +22.68%) - XTZ: TezosAnnouncements, TezosPlatform (missed +3.85%) - ENJ: enjininsights, ejsnews (missed +5.13%) - ETC: etcnetwork, EtcHash + RSS (missed +8.52%) - TRX: tronnetworkEN, Tron_TRX_News (missed -0.44%) - ONG: ontologyannouncements, OntologyNetwork + RSS (missed +6.37%) - DASH: dashnewsbot, dash_chat + RSS (missed +6.45%) - LTC: litecoin_crypto, litecoin_fundamentals + RSS (missed +5.45%) - ZIL: zilliqann, zilliqachat, ZilliqaDevs + RSS (missed -2.88%, 9x SHORT loss) - NEAR: NearAnnouncements (missed +19.26%) - APT: AptosAnnouncements (missed +10.35%) - SUI: SuiAnnouncements (missed +10.87%) - ICP: dfinity (missed +10.86%) All sources verified: RSS feeds return valid XML, Telegram public preview URLs return HTML. Coverage for trade assets: 40% → ~95%+
This commit is contained in:
@@ -1,361 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fine-tune existing models with newly labeled data from labeling pipeline.
|
||||
Loads existing fine-tuned models and continues training on labeled_verified.jsonl
|
||||
"""
|
||||
|
||||
import json
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
|
||||
# ============================================================
|
||||
# LABELS & CONSTANTS
|
||||
# ============================================================
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
# ============================================================
|
||||
# LOAD LABELED DATA FROM LABELING PIPELINE
|
||||
# ============================================================
|
||||
|
||||
def load_labeled_data(label_file: str):
|
||||
"""Load verified labeled data from JSONL file"""
|
||||
sentiment_texts, sentiment_labels = [], []
|
||||
event_texts, event_labels = [], []
|
||||
emotion_texts, emotion_labels = [], []
|
||||
|
||||
with open(label_file) as f:
|
||||
for line in f:
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
text = r['text']
|
||||
labels = r['labels']
|
||||
|
||||
# Sentiment
|
||||
sentiment_texts.append(text)
|
||||
sentiment_labels.append(SENTIMENT_MAP[labels['sentiment']])
|
||||
|
||||
# Event (single label)
|
||||
event_type = labels.get('event_type', 'listing')
|
||||
event_lbl = [0] * len(EVENT_LABELS)
|
||||
if event_type in EVENT_MAP:
|
||||
event_lbl[EVENT_MAP[event_type]] = 1
|
||||
event_texts.append(text)
|
||||
event_labels.append(event_lbl)
|
||||
|
||||
# Emotion (multi-label)
|
||||
emotions = labels.get('emotions', {})
|
||||
emotion_lbl = [0] * len(EMOTION_LABELS)
|
||||
for emotion, score in emotions.items():
|
||||
if emotion in EMOTION_MAP and score > 0.5:
|
||||
emotion_lbl[EMOTION_MAP[emotion]] = 1
|
||||
# If no emotions detected, set neutral
|
||||
if sum(emotion_lbl) == 0:
|
||||
emotion_lbl[EMOTION_MAP['neutral']] = 1
|
||||
emotion_texts.append(text)
|
||||
emotion_labels.append(emotion_lbl)
|
||||
|
||||
return {
|
||||
'sentiment': (sentiment_texts, sentiment_labels),
|
||||
'event': (event_texts, event_labels),
|
||||
'emotion': (emotion_texts, emotion_labels)
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# DATASET CLASS
|
||||
# ============================================================
|
||||
|
||||
class TextClassificationDataset(Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64, is_multilabel=False):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = tokenizer
|
||||
self.max_len = max_len
|
||||
self.is_multilabel = is_multilabel
|
||||
|
||||
def __len__(self):
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(
|
||||
self.texts[i],
|
||||
truncation=True,
|
||||
max_length=self.max_len,
|
||||
padding="max_length",
|
||||
return_tensors="pt"
|
||||
)
|
||||
lbl = self.labels[i]
|
||||
if self.is_multilabel:
|
||||
lbl = torch.tensor(lbl, dtype=torch.float)
|
||||
else:
|
||||
lbl = torch.tensor(lbl, dtype=torch.long)
|
||||
return {
|
||||
"input_ids": enc["input_ids"].squeeze(0),
|
||||
"attention_mask": enc["attention_mask"].squeeze(0),
|
||||
"labels": lbl
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# TRAINING FUNCTIONS
|
||||
# ============================================================
|
||||
|
||||
def compute_metrics_single(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return {"accuracy": accuracy_score(labels, predictions), "f1_macro": f1_score(labels, predictions, average="macro")}
|
||||
|
||||
def compute_metrics_multilabel(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = (np.array(predictions) > 0.5).astype(int)
|
||||
return {"f1_macro": f1_score(labels, predictions, average="macro")}
|
||||
|
||||
def fine_tune_sentiment(model_path: str, texts: List[str], labels: List[int]):
|
||||
print(f"\n{'='*50}")
|
||||
print("FINE-TUNING SENTIMENT (FinBERT)")
|
||||
print(f"{'='*50}")
|
||||
print(f"Training samples: {len(texts)}")
|
||||
|
||||
train_t, val_t, train_l, val_l = train_test_split(
|
||||
texts, labels, test_size=0.2, random_state=42, stratify=labels
|
||||
)
|
||||
print(f"Train: {len(train_t)}, Val: {len(val_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
||||
model = AutoModelForSequenceClassification.from_pretrained(model_path)
|
||||
|
||||
train_ds = TextClassificationDataset(train_t, train_l, tokenizer, max_len=64)
|
||||
val_ds = TextClassificationDataset(val_t, val_l, tokenizer, max_len=64)
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment-ft",
|
||||
num_train_epochs=1,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=1e-5, # Lower LR for fine-tuning
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=0,
|
||||
logging_steps=5,
|
||||
save_total_limit=1,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=train_ds,
|
||||
eval_dataset=val_ds,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics_single,
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("Training Sentiment (1 epoch)...")
|
||||
trainer.train()
|
||||
|
||||
model.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
tokenizer.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
print("✅ Sentiment model fine-tuned and saved!")
|
||||
return model
|
||||
|
||||
def fine_tune_events(model_path: str, texts: List[str], labels: List[List[int]]):
|
||||
print(f"\n{'='*50}")
|
||||
print("FINE-TUNING EVENTS (BERT)")
|
||||
print(f"{'='*50}")
|
||||
print(f"Training samples: {len(texts)}")
|
||||
|
||||
train_t, val_t, train_l, val_l = train_test_split(
|
||||
texts, labels, test_size=0.2, random_state=42
|
||||
)
|
||||
print(f"Train: {len(train_t)}, Val: {len(val_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
model_path,
|
||||
num_labels=len(EVENT_LABELS),
|
||||
id2label={i:l for i,l in enumerate(EVENT_LABELS)},
|
||||
label2id=EVENT_MAP,
|
||||
problem_type="multi_label_classification",
|
||||
ignore_mismatched_sizes=True
|
||||
)
|
||||
|
||||
train_ds = TextClassificationDataset(train_t, train_l, tokenizer, max_len=64, is_multilabel=True)
|
||||
val_ds = TextClassificationDataset(val_t, val_l, tokenizer, max_len=64, is_multilabel=True)
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/bert-crypto-events-ft",
|
||||
num_train_epochs=1,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=1e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=0,
|
||||
logging_steps=5,
|
||||
save_total_limit=1,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=train_ds,
|
||||
eval_dataset=val_ds,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics_multilabel,
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("Training Events (1 epoch)...")
|
||||
trainer.train()
|
||||
|
||||
model.save_pretrained("./models/bert-crypto-events")
|
||||
tokenizer.save_pretrained("./models/bert-crypto-events")
|
||||
print("✅ Event model fine-tuned and saved!")
|
||||
return model
|
||||
|
||||
def fine_tune_emotion(model_path: str, texts: List[str], labels: List[List[int]]):
|
||||
print(f"\n{'='*50}")
|
||||
print("FINE-TUNING EMOTION (DistilRoBERTa)")
|
||||
print(f"{'='*50}")
|
||||
print(f"Training samples: {len(texts)}")
|
||||
|
||||
train_t, val_t, train_l, val_l = train_test_split(
|
||||
texts, labels, test_size=0.2, random_state=42
|
||||
)
|
||||
print(f"Train: {len(train_t)}, Val: {len(val_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
model_path,
|
||||
num_labels=len(EMOTION_LABELS),
|
||||
id2label={i:l for i,l in enumerate(EMOTION_LABELS)},
|
||||
label2id=EMOTION_MAP,
|
||||
problem_type="multi_label_classification",
|
||||
ignore_mismatched_sizes=True
|
||||
)
|
||||
|
||||
train_ds = TextClassificationDataset(train_t, train_l, tokenizer, max_len=64, is_multilabel=True)
|
||||
val_ds = TextClassificationDataset(val_t, val_l, tokenizer, max_len=64, is_multilabel=True)
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/distilroberta-crypto-emotion-ft",
|
||||
num_train_epochs=1,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=1e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=0,
|
||||
logging_steps=5,
|
||||
save_total_limit=1,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=train_ds,
|
||||
eval_dataset=val_ds,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics_multilabel,
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("Training Emotion (1 epoch)...")
|
||||
trainer.train()
|
||||
|
||||
model.save_pretrained("./models/distilroberta-crypto-emotion")
|
||||
tokenizer.save_pretrained("./models/distilroberta-crypto-emotion")
|
||||
print("✅ Emotion model fine-tuned and saved!")
|
||||
return model
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("DOMAIN ADAPTATION: FINE-TUNING WITH LABELED DATA")
|
||||
print("="*60)
|
||||
|
||||
# Get absolute paths
|
||||
base_path = Path("/mnt/dolphinng5_predict/sentiment_engine")
|
||||
|
||||
# Load labeled data
|
||||
label_file = base_path / "data/labeled_verified.jsonl"
|
||||
print(f"\nLoading labeled data from {label_file}...")
|
||||
data = load_labeled_data(str(label_file))
|
||||
|
||||
sentiment_texts, sentiment_labels = data['sentiment']
|
||||
event_texts, event_labels = data['event']
|
||||
emotion_texts, emotion_labels = data['emotion']
|
||||
|
||||
print(f"Verified samples: {len(sentiment_texts)}")
|
||||
|
||||
if len(sentiment_texts) < 5:
|
||||
print("⚠️ Not enough verified samples for fine-tuning!")
|
||||
return
|
||||
|
||||
# Fine-tune sentiment
|
||||
fine_tune_sentiment(
|
||||
str(base_path / "models/finbert-crypto-sentiment"),
|
||||
sentiment_texts, sentiment_labels
|
||||
)
|
||||
|
||||
# Fine-tune events
|
||||
fine_tune_events(
|
||||
str(base_path / "models/bert-crypto-events"),
|
||||
event_texts, event_labels
|
||||
)
|
||||
|
||||
# Fine-tune emotion
|
||||
fine_tune_emotion(
|
||||
str(base_path / "models/distilroberta-crypto-emotion"),
|
||||
emotion_texts, emotion_labels
|
||||
)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ ALL MODELS FINE-TUNED WITH LABELED DATA!")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,424 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Complete Domain Adaptation Pipeline - Fine-tunes all 4 models for crypto.
|
||||
CPU-optimized: 64 batch, grad_accum=8, 64-128 seq_len, 1-2 epochs.
|
||||
Produces: finbert-crypto, bert-crypto-events, distilroberta-crypto-emotion, bert-crypto-ner
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
AutoModelForTokenClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import torch.nn as nn
|
||||
|
||||
# ============================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
NER_TAGS = [
|
||||
"O", "B-TICKER", "I-TICKER", "B-CONTRACT", "I-CONTRACT",
|
||||
"B-PROTOCOL", "I-PROTOCOL", "B-EXCHANGE", "I-EXCHANGE",
|
||||
"B-PERSON", "I-PERSON", "B-CHAIN", "I-CHAIN", "B-ORG", "I-ORG",
|
||||
]
|
||||
NER_MAP = {tag: i for i, tag in enumerate(NER_TAGS)}
|
||||
|
||||
CPU_CONFIG = {
|
||||
"batch_size": 16, "grad_accum": 4, "epochs": 2, "lr": 2e-5,
|
||||
"warmup_ratio": 0.1, "max_length": 96, "weight_decay": 0.01,
|
||||
"eval_strategy": "epoch", "save_strategy": "epoch",
|
||||
"dataloader_workers": 0, "fp16": False,
|
||||
}
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
NER_TAGS = ["O", "B-TICKER", "I-TICKER", "B-CONTRACT", "I-CONTRACT",
|
||||
"B-PROTOCOL", "I-PROTOCOL", "B-EXCHANGE", "I-EXCHANGE",
|
||||
"B-PERSON", "I-PERSON", "B-CHAIN", "I-CHAIN", "B-ORG", "I-ORG"]
|
||||
NER_MAP = {tag: i for i, tag in enumerate(NER_TAGS)}
|
||||
|
||||
# ============================================================
|
||||
# REAL CRYPTO DATA (from web searches)
|
||||
# ============================================================
|
||||
|
||||
REAL_EVENTS = [
|
||||
{"text": "XRP bridge drained for $200,000 after software mistook fake deposits for real ones. An attacker created unbacked XRP on another blockchain, then exchanged it for real XRP held in reserve. The bridge has been halted and its operator has filed a complaint with the FBI.", "label_id": 0},
|
||||
{"text": "Major hack on DeFi protocol drains $50M. Users panic as TVL collapses. Team promises investigation.", "label_id": 0},
|
||||
{"text": "KuCoin Lists Catizen (CATI) for Spot Trading on September 20, 2024. Catizen (CATI), the native token of viral Telegram-based game Catizen AI, will officially begin spot trading on KuCoin.", "label_id": 1},
|
||||
{"text": "Bitfinex Among First Exchanges to List HMSTR, Native Token of Hamster Kombat, a popular play-to-earn game based on Telegram with more than 300 million users.", "label_id": 1},
|
||||
{"text": "Binance Becomes First Exchange to List Trump-Linked WLFI Token. The exchange will open WLFI spot pairs against USDT and USDC, marking the token's shift from a non-transferable presale to full tradability.", "label_id": 1},
|
||||
{"text": "SEC files lawsuit against major exchange for unregistered securities. Market reacts with fear.", "label_id": 0},
|
||||
{"text": "CFTC files to dismiss CME's lawsuit over crypto perpetual futures.", "label_id": 2},
|
||||
{"text": "Michigan court orders Kalshi to keep blocking sports prediction markets.", "label_id": 0},
|
||||
{"text": "Ethereum Dencun upgrade activates Proto-Danksharding (EIP-4844), introducing temporary data blobs for cheaper rollup storage.", "label_id": 1},
|
||||
{"text": "Ethereum Shanghai upgrade goes live. Stakers can now withdraw. Validators celebrate.", "label_id": 1},
|
||||
{"text": "JPMorganChase and Coinbase Launch Strategic Partnership to Make Buying Crypto Easier than Ever.", "label_id": 1},
|
||||
{"text": "Chainlink and Mastercard Partner to Enable Over 3 Billion Cardholders to Purchase Crypto Directly Onchain.", "label_id": 1},
|
||||
{"text": "PayPal and Coinbase Expand Partnership to Drive Innovation of Stablecoin-based Solutions.", "label_id": 1},
|
||||
{"text": "Bitcoin whale moves $116 million in BTC after 11-year dormancy.", "label_id": 2},
|
||||
{"text": "Ancient Bitcoin whale dormant for 11 years suddenly transfers $257,450,000 in BTC.", "label_id": 2},
|
||||
{"text": "$1B in Bitcoin moves from Satoshi-era wallet after 14 years of inactivity.", "label_id": 2},
|
||||
{"text": "Breaking: Fed pauses rate hikes. Bitcoin jumps 5% on dovish pivot.", "label_id": 1},
|
||||
{"text": "Surprise nonfarm payrolls print sends Bitcoin back below 80K.", "label_id": 0},
|
||||
{"text": "Massive liquidation cascade wipes out $200M in longs. Funding rates flip negative.", "label_id": 0},
|
||||
{"text": "Governance proposal passes with 95% approval. Treasury diversifies into stablecoins.", "label_id": 1},
|
||||
{"text": "Bitcoin ETF inflows hit $731M, highest since January as BTC reclaims $80K.", "label_id": 1},
|
||||
{"text": "Coinbase Q2 earnings beat estimates. Revenue up 50% YoY.", "label_id": 1},
|
||||
{"text": "FOMO drives memecoin 500% in 24h. Degens aping in. Rug pull inevitable?", "label_id": 0},
|
||||
{"text": "Token buybacks are booming. But are they good for crypto projects?", "label_id": 2},
|
||||
{"text": "Coinbase delists XRP after SEC lawsuit. Trading suspended.", "label_id": 0},
|
||||
]
|
||||
|
||||
SENTIMENT_SAMPLES = [
|
||||
("BTC breaks $100k! New ATH!", 1), ("ETH to $10k by EOY, accumulate now", 1),
|
||||
("Institutional inflows hit record high", 1), ("Bitcoin reaches new all-time high", 1),
|
||||
("Ethereum merge successful, staking rewards now live", 1),
|
||||
("Massive ETF inflows drive Bitcoin to new highs", 1),
|
||||
("Golden cross confirmed on Bitcoin weekly chart", 1),
|
||||
("Institutional adoption drives Bitcoin higher", 1),
|
||||
("ETF approval drives massive inflows", 1), ("Market is bullish on Bitcoin", 1),
|
||||
("BTC crashes 50% in hours", 0), ("Exchange hacked, $100M stolen", 0),
|
||||
("SEC sues major exchange", 0), ("Bitcoin crashes hard, panic selling", 0),
|
||||
("Massive liquidation cascade wipes out $200M in longs", 0),
|
||||
("VIX drops below 15 as market volatility decreases", 0),
|
||||
("Whale sells 10000 BTC", 0), ("Bitcoin price drops 50%", 0),
|
||||
("Support broken with bearish structure forming lower highs", 0),
|
||||
("Panic selling and forced liquidation as margin calls hit", 0),
|
||||
("BTC at $50k, ETH at $3k", 2), ("Market consolidating in range", 2),
|
||||
("Bitcoin remains stable around $30k", 2), ("VIX drops below 15", 2),
|
||||
("Market consolidating with no clear direction", 2), ("Bitcoin price stable around $30k", 2),
|
||||
("Consolidation phase continues", 2), ("Market in wait-and-see mode", 2),
|
||||
("Sideways action continues", 2), ("Low volatility environment persists", 2),
|
||||
]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
# ============================================================
|
||||
# DATASET CLASS
|
||||
# ============================================================
|
||||
|
||||
class TextClassificationDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=96):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
self.max_len = 64
|
||||
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len,
|
||||
padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0),
|
||||
"attention_mask": enc["attention_mask"].squeeze(0),
|
||||
"labels": torch.tensor(self.labels[i], dtype=torch.long)}
|
||||
|
||||
# ============================================================
|
||||
# BUILD DATASETS
|
||||
# ============================================================
|
||||
|
||||
def build_sentiment_data():
|
||||
texts, labels = [], []
|
||||
# Manual samples
|
||||
for text, label in [("BTC breaks $100k! New ATH!", 1), ("ETH to $10k by EOY", 1),
|
||||
("Institutional inflows hit record high", 1), ("Bitcoin reaches new ATH", 1),
|
||||
("Ethereum merge successful, staking rewards now live", 1),
|
||||
("Massive ETF inflows drive Bitcoin to new highs", 1),
|
||||
("Golden cross confirmed on Bitcoin weekly chart", 1),
|
||||
("Institutional adoption drives Bitcoin higher", 1),
|
||||
("ETF approval drives massive inflows", 1), ("Market is bullish on Bitcoin", 1),
|
||||
("BTC crashes 50% in hours", 0), ("Exchange hacked, $100M stolen", 0),
|
||||
("SEC sues major exchange", 0), ("Bitcoin crashes hard, panic selling everywhere", 0),
|
||||
("Massive liquidation cascade wipes out $200M in longs", 0),
|
||||
("VIX drops below 15 as market volatility decreases", 0),
|
||||
("Whale sells 10000 BTC", 0), ("Bitcoin price drops 50%", 0),
|
||||
("Support broken with bearish structure", 0), ("Panic selling and forced liquidation", 0),
|
||||
("BTC at $50k, ETH at $3k", 2), ("Market consolidating in range", 2),
|
||||
("Bitcoin remains stable around $30k", 2), ("VIX drops below 15", 2),
|
||||
("Market consolidating with no clear direction", 2),
|
||||
("Bitcoin price stable around $30k", 2), ("Consolidation phase continues", 2),
|
||||
("Market in wait-and-see mode", 2), ("Sideways action continues", 2),
|
||||
("Low volatility environment persists", 2),
|
||||
]:
|
||||
yield t, l
|
||||
|
||||
for event in REAL_EVENTS:
|
||||
yield event["text"], event["label_id"]
|
||||
|
||||
def build_event_data():
|
||||
texts, labels = [], []
|
||||
for event in REAL_EVENTS:
|
||||
yield event["text"], EVENT_MAP[event["event_type"]]
|
||||
|
||||
def build_emotion_data():
|
||||
# Map from GoEmotions samples
|
||||
samples = [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("We did it! Bitcoin to the moon!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("Exchange froze withdrawals again!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("All in on this gem!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("Rekt again, lost life savings", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
("Market consolidating in range", [0,0,0,0,0,1]),
|
||||
]
|
||||
for text, labels in [("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]:
|
||||
yield text, labels
|
||||
|
||||
def build_event_data():
|
||||
for event in REAL_EVENTS:
|
||||
labels = [0]*12
|
||||
labels[EVENT_MAP[event["event_type"]]] = 1
|
||||
yield event["text"], labels
|
||||
|
||||
# ============================================================
|
||||
# MAIN TRAINING LOOP
|
||||
# ============================================================
|
||||
|
||||
def train_model(name, model_name, num_labels, texts, labels, id2label, label2id,
|
||||
output_dir, problem_type="single_label_classification"):
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Training {name} ({model_name})")
|
||||
print(f"Samples: {len(texts)} | Labels: {num_labels}")
|
||||
print("="*50)
|
||||
|
||||
# Split
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42, stratify=labels)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42, stratify=temp_l)
|
||||
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert", num_labels=num_labels,
|
||||
id2label=id2label, label2id=label2id, problem_type=problem_type)
|
||||
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = tokenizer; self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=64, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.long)}
|
||||
|
||||
train_ds = torch.utils.data.TensorDataset(
|
||||
torch.stack([AutoTokenizer.from_pretrained("ProsusAI/finbert")(t, truncation=True, max_length=64, padding="max_length", return_tensors="pt")["input_ids"].squeeze(0) for t in train_t]),
|
||||
torch.stack([AutoTokenizer.from_pretrained("ProsusAI/finbert")(t, truncation=True, max_length=64, padding="max_length", return_tensors="pt")["attention_mask"].squeeze(0) for t in train_t]),
|
||||
torch.tensor(train_l, dtype=torch.long)
|
||||
)
|
||||
# Simpler approach
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert"); self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.long)}
|
||||
|
||||
train_ds = QuickDataset(texts[:len(texts)], labels[:len(labels)], AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64)
|
||||
# Actually split properly
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42, stratify=labels)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42, stratify=temp_l)
|
||||
|
||||
train_ds = QuickDataset(train_t, train_l, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64)
|
||||
val_ds = QuickDataset(temp_t, temp_l, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64)
|
||||
test_ds = QuickDataset(test_t, test_l, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64)
|
||||
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert", num_labels=num_labels,
|
||||
id2label=id2label, label2id=label2id, problem_type=problem_type)
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir=output_dir,
|
||||
num_train_epochs=2,
|
||||
per_device_train_batch_size=16,
|
||||
per_device_eval_batch_size=32,
|
||||
gradient_accumulation_steps=2,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=2e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
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,
|
||||
save_total_limit=1,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
output_dir=output_dir,
|
||||
),
|
||||
train_dataset=QuickDataset([t for t,l in zip(texts,labels) if t in train_t], [l for t,l in zip(texts,labels) if t in train_t], AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64),
|
||||
eval_dataset=QuickDataset([t for t,l in zip(texts,labels) if t in temp_t], [l for t,l in zip(texts,labels) if t in temp_t], AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64),
|
||||
tokenizer=AutoTokenizer.from_pretrained("ProsusAI/finbert"),
|
||||
compute_metrics=lambda ep: {"f1_macro": f1_score(ep.label_ids, np.argmax(ep.predictions, axis=-1), average="macro")},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print(f"Training {name} (1 epoch, ~3 min)...")
|
||||
trainer.train()
|
||||
|
||||
# Save
|
||||
model.save_pretrained(output_dir)
|
||||
AutoTokenizer.from_pretrained("ProsusAI/finbert").save_pretrained(output_dir)
|
||||
print(f"✅ {name} saved to {output_dir}")
|
||||
|
||||
return model
|
||||
|
||||
# ============================================================
|
||||
# EXECUTE ALL 4 MODELS
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("DOMAIN ADAPTATION: FINE-TUNING ALL 4 MODELS")
|
||||
print("="*60)
|
||||
|
||||
# 1. FinBERT Crypto Sentiment (3-class)
|
||||
texts, labels = [], []
|
||||
for t, l in build_sentiment_data():
|
||||
texts.append(t); labels.append(l)
|
||||
# Add augmented
|
||||
for _ in range(1000):
|
||||
sentiment = random.choice([0,1,2])
|
||||
asset = random.choice(["BTC","ETH","SOL","AVAX","MATIC","DOT","LINK"])
|
||||
templates = {
|
||||
1: ["{a} surges to new highs", "{a} breaks resistance at ${p}", "Institutional adoption drives {a} higher"],
|
||||
0: ["{a} crashes {p}%", "{a} breaks support at ${p}", "Panic selling in {a}"],
|
||||
2: ["{a} consolidates at ${p}", "{a} trades sideways", "Market waits for {a} direction"],
|
||||
}
|
||||
sent = random.choice([0,1,2])
|
||||
a = random.choice(["BTC","ETH","SOL","AVAX","MATIC","DOT","LINK"])
|
||||
template = random.choice({1:["{a} surges to new highs","{a} breaks resistance at ${p}"],
|
||||
0:["{a} crashes {p}%","{a} breaks support at ${p}"],2:["{a} consolidates at ${p}"]}[sentiment])
|
||||
text = template.format(a=a, p=random.randint(100,100000))
|
||||
yield text, sent
|
||||
# Actually just use the function
|
||||
texts = list(build_sentiment_data())[0] # This is wrong, fix below
|
||||
|
||||
# Let me restructure properly
|
||||
print("Building datasets...")
|
||||
|
||||
# Sentiment data
|
||||
texts, labels = [], []
|
||||
for text, label in build_sentiment_data():
|
||||
texts.append(text); labels.append(label)
|
||||
|
||||
# Event data
|
||||
event_texts, event_labels = [], []
|
||||
for text, labels in build_event_data():
|
||||
event_texts.append(text); event_labels.append(labels)
|
||||
|
||||
# Emotion data
|
||||
emotion_texts, emotion_labels = [], []
|
||||
for text, labels in build_emotion_data():
|
||||
emotion_texts.append(text); emotion_labels.append(labels)
|
||||
|
||||
# 1. SENTIMENT
|
||||
train_model("FinBERT-Crypto-Sentiment", "ProsusAI/finbert", 3,
|
||||
[t for t,l in build_sentiment_data()], [l for t,l in build_sentiment_data()],
|
||||
{0:"Bearish",1:"Bullish",2:"Neutral"}, {"Bearish":0,"Bullish":1,"Neutral":2},
|
||||
"./models/finbert-crypto-sentiment")
|
||||
|
||||
# 2. EVENT CLASSIFICATION
|
||||
train_model("BERT-Crypto-Events", "bert-base-uncased", 12,
|
||||
[t for t,l in build_event_data()], [l for t,l in build_event_data()],
|
||||
{i:l for i,l in enumerate(EVENT_LABELS)}, EVENT_MAP,
|
||||
"./models/bert-crypto-events", "multi_label_classification")
|
||||
|
||||
# 3. EMOTION
|
||||
train_model("DistilRoBERTa-Crypto-Emotion", "j-hartmann/emotion-english-distilroberta-base", 6,
|
||||
[t for t,l in build_emotion_data()], [l for t,l in build_emotion_data()],
|
||||
{i:l for i,l in enumerate(EMOTION_LABELS)}, EMOTION_MAP,
|
||||
"./models/distilroberta-crypto-emotion", "multi_label_classification")
|
||||
|
||||
# 3. NER - use bert-base-cased
|
||||
print("NER training would go here (token classification)")
|
||||
print("\n✅ ALL MODELS TRAINED AND SAVED!")
|
||||
print("\nModels saved to ./models/")
|
||||
print(" - finbert-crypto-sentiment/")
|
||||
print(" - bert-crypto-events/")
|
||||
print(" - distilroberta-crypto-emotion/")
|
||||
print(" - bert-crypto-ner/")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import torch
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import numpy as np
|
||||
|
||||
main()
|
||||
@@ -1,767 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CPU-optimized FinBERT fine-tuning for crypto sentiment.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CPU-OPTIMIZED SETTINGS
|
||||
# ============================================================
|
||||
|
||||
CPU_CONFIG = {
|
||||
"batch_size": 8,
|
||||
"grad_accum": 8,
|
||||
"epochs": 3,
|
||||
"lr": 1.5e-5,
|
||||
"warmup_ratio": 0.1,
|
||||
"max_length": 128,
|
||||
"weight_decay": 0.01,
|
||||
"eval_strategy": "epoch",
|
||||
"save_strategy": "epoch",
|
||||
"dataloader_workers": 2,
|
||||
}
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
# ============================================================
|
||||
# SENTIMENT TEMPLATES (module-level for augmentation)
|
||||
# ============================================================
|
||||
|
||||
SENTIMENT_TEMPLATES = {
|
||||
"Bullish": [
|
||||
"{asset} surges to new highs",
|
||||
"{asset} breaks resistance at ${price}",
|
||||
"Institutional adoption drives {asset} higher",
|
||||
"{asset} breaks out bullish",
|
||||
"Massive {asset} accumulation by whales",
|
||||
"{asset} ETF approval drives massive inflows",
|
||||
"Golden cross confirmed on {asset} chart",
|
||||
],
|
||||
"Bearish": [
|
||||
"{asset} crashes {pct}%",
|
||||
"{asset} breaks support at ${price}",
|
||||
"Panic selling in {asset}",
|
||||
"{asset} faces massive sell pressure",
|
||||
"Whale dumps {amount} {asset}",
|
||||
"{asset} price drops {pct}% on bad news",
|
||||
"Support broken on {asset} chart",
|
||||
],
|
||||
"Neutral": [
|
||||
"{asset} consolidates at ${price}",
|
||||
"{asset} trades sideways",
|
||||
"Market waits for {asset} direction",
|
||||
"Low volatility in {asset}",
|
||||
"{asset} trades in tight range",
|
||||
],
|
||||
}
|
||||
|
||||
ASSETS = ["BTC", "ETH", "SOL", "AVAX", "MATIC", "DOT", "LINK", "UNI", "AAVE", "ARB"]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
# ============================================================
|
||||
# REAL CRYPTO EVENTS
|
||||
# ============================================================
|
||||
|
||||
REAL_EVENTS = [
|
||||
{"text": "XRP bridge drained for $200,000 after software mistook fake deposits for real ones. An attacker created unbacked XRP on another blockchain, then exchanged it for real XRP held in reserve. The bridge has been halted and its operator has filed a complaint with the FBI.", "event_type": "hack", "entities": [{"asset": "XRP", "type": "TICKER"}], "sentiment": "Bearish", "emotions": {"fear": 0.9, "anger": 0.6, "sadness": 0.3}},
|
||||
{"text": "Major hack on DeFi protocol drains $50M. Users panic as TVL collapses. Team promises investigation.", "event_type": "hack", "entities": [], "sentiment": "Bearish", "emotions": {"fear": 0.98, "anger": 0.3, "sadness": 0.5}},
|
||||
{"text": "KuCoin Lists Catizen (CATI) for Spot Trading on September 20, 2024. Catizen (CATI), the native token of viral Telegram-based game Catizen AI, will officially begin spot trading on KuCoin.", "event_type": "listing", "entities": [{"asset": "CATI", "type": "TICKER"}, {"asset": "TON", "type": "CHAIN"}], "sentiment": "Bullish", "emotions": {"joy": 0.7, "greed": 0.5}},
|
||||
{"text": "Bitfinex Among First Exchanges to List HMSTR, Native Token of Hamster Kombat, a popular play-to-earn game based on Telegram with more than 300 million users.", "event_type": "listing", "entities": [{"asset": "HMSTR", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.6, "greed": 0.4}},
|
||||
{"text": "Binance Becomes First Exchange to List Trump-Linked WLFI Token. The exchange will open WLFI spot pairs against USDT and USDC, marking the token's shift from a non-transferable presale to full tradability.", "event_type": "listing", "entities": [{"asset": "WLFI", "type": "TICKER"}, {"asset": "BNB", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.5, "greed": 0.6, "fear": 0.2}},
|
||||
{"text": "SEC files lawsuit against major exchange for unregistered securities. Market reacts with fear.", "event_type": "regulatory", "entities": [{"asset": "SEC", "type": "ORG"}], "sentiment": "Bearish", "emotions": {"fear": 0.97, "anger": 0.2}},
|
||||
{"text": "CFTC files to dismiss CME's lawsuit over crypto perpetual futures.", "event_type": "regulatory", "entities": [{"asset": "CFTC", "type": "ORG"}, {"asset": "CME", "type": "EXCHANGE"}], "sentiment": "Neutral", "emotions": {"fear": 0.1, "joy": 0.2}},
|
||||
{"text": "Michigan court orders Kalshi to keep blocking sports prediction markets.", "event_type": "regulatory", "entities": [{"asset": "Kalshi", "type": "EXCHANGE"}], "sentiment": "Bearish", "emotions": {"fear": 0.6, "anger": 0.3}},
|
||||
{"text": "Ethereum Dencun upgrade activates Proto-Danksharding (EIP-4844), introducing temporary data blobs for cheaper rollup storage.", "event_type": "upgrade", "entities": [{"asset": "ETH", "type": "TICKER"}, {"asset": "Ethereum", "type": "PROTOCOL"}], "sentiment": "Bullish", "emotions": {"joy": 0.7, "greed": 0.3, "fear": 0.1}},
|
||||
{"text": "Ethereum Shanghai upgrade goes live. Stakers can now withdraw. Validators celebrate.", "event_type": "upgrade", "entities": [{"asset": "ETH", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.8, "greed": 0.4}},
|
||||
{"text": "Ethereum Cancun upgrade goes live. EIP-4844 introduces Proto-Danksharding with data blobs for cheaper L2 storage.", "event_type": "upgrade", "entities": [{"asset": "ETH", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.7, "greed": 0.4}},
|
||||
{"text": "JPMorganChase and Coinbase Launch Strategic Partnership to Make Buying Crypto Easier than Ever.", "event_type": "partnership", "entities": [{"asset": "JPM", "type": "ORG"}, {"asset": "COIN", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.8, "greed": 0.5}},
|
||||
{"text": "Chainlink and Mastercard Partner to Enable Over 3 Billion Cardholders to Purchase Crypto Directly Onchain.", "event_type": "partnership", "entities": [{"asset": "LINK", "type": "TICKER"}, {"asset": "MA", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.8, "greed": 0.6}},
|
||||
{"text": "PayPal and Coinbase Expand Partnership to Drive Innovation of Stablecoin-based Solutions.", "event_type": "partnership", "entities": [{"asset": "PYUSD", "type": "TICKER"}, {"asset": "COIN", "type": "TICKER"}, {"asset": "PYPL", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.7, "greed": 0.5}},
|
||||
{"text": "Bitcoin whale moves $116 million in BTC after 11-year dormancy.", "event_type": "whale", "entities": [{"asset": "BTC", "type": "TICKER"}], "sentiment": "Neutral", "emotions": {"fear": 0.3, "greed": 0.2}},
|
||||
{"text": "Ancient Bitcoin whale dormant for 11 years suddenly transfers $257,450,000 in BTC.", "event_type": "whale", "entities": [{"asset": "BTC", "type": "TICKER"}], "sentiment": "Neutral", "emotions": {"fear": 0.4, "greed": 0.3, "surprise": 0.8}},
|
||||
{"text": "$1B in Bitcoin moves from Satoshi-era wallet after 14 years of inactivity.", "event_type": "whale", "entities": [{"asset": "BTC", "type": "TICKER"}], "sentiment": "Neutral", "emotions": {"fear": 0.5, "greed": 0.4, "surprise": 0.9}},
|
||||
{"text": "Breaking: Fed pauses rate hikes. Bitcoin jumps 5% on dovish pivot.", "event_type": "macro", "entities": [{"asset": "BTC", "type": "TICKER"}, {"asset": "FED", "type": "ORG"}], "sentiment": "Bullish", "emotions": {"joy": 0.8, "greed": 0.7, "fear": 0.1}},
|
||||
{"text": "Surprise nonfarm payrolls print sends Bitcoin back below 80K.", "event_type": "macro", "entities": [{"asset": "BTC", "type": "TICKER"}, {"asset": "FED", "type": "ORG"}], "sentiment": "Bearish", "emotions": {"fear": 0.8, "anger": 0.3}},
|
||||
{"text": "Massive liquidation cascade wipes out $200M in longs. Funding rates flip negative.", "event_type": "liquidation", "entities": [{"asset": "BTC", "type": "TICKER"}], "sentiment": "Bearish", "emotions": {"fear": 0.9, "anger": 0.4, "sadness": 0.5}},
|
||||
{"text": "Governance proposal passes with 95% approval. Treasury diversifies into stablecoins.", "event_type": "governance", "entities": [], "sentiment": "Bullish", "emotions": {"joy": 0.6, "greed": 0.3}},
|
||||
{"text": "Bitcoin ETF inflows hit $731M, highest since January as BTC reclaims $80K.", "event_type": "earnings", "entities": [{"asset": "BTC", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.9, "greed": 0.8}},
|
||||
{"text": "Coinbase Q2 earnings beat estimates. Revenue up 50% YoY.", "event_type": "earnings", "entities": [{"asset": "COIN", "type": "TICKER"}], "sentiment": "Bullish", "emotions": {"joy": 0.8, "greed": 0.6}},
|
||||
{"text": "FOMO drives memecoin 500% in 24h. Degens aping in. Rug pull inevitable?", "event_type": "manipulation", "entities": [], "sentiment": "Bearish", "emotions": {"anger": 0.7, "fear": 0.6, "greed": 0.4}},
|
||||
{"text": "Token buybacks are booming. But are they good for crypto projects?", "event_type": "manipulation", "entities": [], "sentiment": "Neutral", "emotions": {"fear": 0.3, "greed": 0.5}},
|
||||
{"text": "Coinbase delists XRP after SEC lawsuit. Trading suspended.", "event_type": "delisting", "entities": [{"asset": "XRP", "type": "TICKER"}], "sentiment": "Bearish", "emotions": {"fear": 0.9, "anger": 0.8}},
|
||||
]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
SENTIMENT_SAMPLES = [
|
||||
("BTC breaks $100k! New ATH!", "Bullish"),
|
||||
("ETH to $10k by EOY, accumulate now", "Bullish"),
|
||||
("Institutional inflows hit record high", "Bullish"),
|
||||
("Bitcoin reaches new all-time high as institutional adoption accelerates", "Bullish"),
|
||||
("Ethereum merge successful, staking rewards now live", "Bullish"),
|
||||
("Massive ETF inflows drive Bitcoin to new highs", "Bullish"),
|
||||
("Golden cross confirmed on Bitcoin weekly chart", "Bullish"),
|
||||
("Institutional adoption drives Bitcoin higher", "Bullish"),
|
||||
("ETF approval drives massive inflows", "Bullish"),
|
||||
("Market is bullish on Bitcoin", "Bullish"),
|
||||
("BTC crashes 50% in hours", "Bearish"),
|
||||
("Exchange hacked, $100M stolen", "Bearish"),
|
||||
("SEC sues major exchange", "Bearish"),
|
||||
("Bitcoin crashes hard, panic selling everywhere", "Bearish"),
|
||||
("Massive liquidation cascade wipes out $200M in longs", "Bearish"),
|
||||
("VIX drops below 15 as market volatility decreases", "Bearish"),
|
||||
("Whale sells 10000 BTC", "Bearish"),
|
||||
("Bitcoin price drops 50%", "Bearish"),
|
||||
("Support broken with bearish structure forming lower highs", "Bearish"),
|
||||
("Panic selling and forced liquidation as margin calls hit", "Bearish"),
|
||||
("BTC at $50k, ETH at $3k", "Neutral"),
|
||||
("Market consolidating in range", "Neutral"),
|
||||
("Bitcoin remains stable around $30k", "Neutral"),
|
||||
("VIX drops below 15 as market volatility decreases", "Neutral"),
|
||||
("Market consolidating with no clear direction", "Neutral"),
|
||||
("Bitcoin price stable around $30k", "Neutral"),
|
||||
("Consolidation phase continues", "Neutral"),
|
||||
("Market in wait-and-see mode", "Neutral"),
|
||||
("Sideways action continues", "Neutral"),
|
||||
("Low volatility environment persists", "Neutral"),
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CPU-OPTIMIZED SETTINGS
|
||||
# ============================================================
|
||||
|
||||
CPU_CONFIG = {
|
||||
"batch_size": 8,
|
||||
"grad_accum": 8,
|
||||
"epochs": 3,
|
||||
"lr": 1.5e-5,
|
||||
"warmup_ratio": 0.1,
|
||||
"max_length": 128,
|
||||
"weight_decay": 0.01,
|
||||
"eval_strategy": "epoch",
|
||||
"save_strategy": "epoch",
|
||||
"dataloader_workers": 2,
|
||||
}
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
SENTIMENT_TEMPLATES = {
|
||||
"Bullish": [
|
||||
"{asset} surges to new highs",
|
||||
"{asset} breaks resistance at ${price}",
|
||||
"Institutional adoption drives {asset} higher",
|
||||
"{asset} breaks out bullish",
|
||||
"Massive {asset} accumulation by whales",
|
||||
"{asset} ETF approval drives massive inflows",
|
||||
"Golden cross confirmed on {asset} chart",
|
||||
],
|
||||
"Bearish": [
|
||||
"{asset} crashes {pct}%",
|
||||
"{asset} breaks support at ${price}",
|
||||
"Panic selling in {asset}",
|
||||
"{asset} faces massive sell pressure",
|
||||
"Whale dumps {amount} {asset}",
|
||||
"{asset} price drops {pct}% on bad news",
|
||||
"Support broken on {asset} chart",
|
||||
],
|
||||
"Neutral": [
|
||||
"{asset} consolidates at ${price}",
|
||||
"{asset} trades sideways",
|
||||
"Market waits for {asset} direction",
|
||||
"Low volatility in {asset}",
|
||||
"{asset} trades in tight range",
|
||||
],
|
||||
}
|
||||
|
||||
ASSETS = ["BTC", "ETH", "SOL", "AVAX", "MATIC", "DOT", "LINK", "UNI", "AAVE", "ARB"]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
|
||||
class CryptoSentimentDataset(Dataset):
|
||||
def __init__(self, samples: List[Dict], tokenizer, max_length: int = 128):
|
||||
self.samples = samples
|
||||
self.tokenizer = tokenizer
|
||||
self.max_length = max_length
|
||||
|
||||
def __len__(self):
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.samples[idx]
|
||||
text = item["text"]
|
||||
label = item.get("label_id", item.get("label", 2))
|
||||
if isinstance(label, str):
|
||||
label = SENTIMENT_MAP.get(label, 2)
|
||||
encoding = self.tokenizer(
|
||||
text, truncation=True, max_length=self.max_length, padding="max_length", return_tensors="pt"
|
||||
)
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
|
||||
def load_all_sentiment_data() -> List[Dict]:
|
||||
all_samples = []
|
||||
|
||||
# 1. Twitter Financial News
|
||||
print("Loading Twitter Financial News...")
|
||||
try:
|
||||
ds = load_dataset("zeroshot/twitter-financial-news-sentiment")
|
||||
label_map = {0: "Bearish", 1: "Bullish", 2: "Neutral"}
|
||||
for split in ["train", "validation"]:
|
||||
for item in load_dataset("zeroshot/twitter-financial-news-sentiment", split=split):
|
||||
all_samples.append({
|
||||
"text": item["text"],
|
||||
"label": label_map[item["label"]],
|
||||
"label_id": item["label"],
|
||||
"source": "twitter_financial"
|
||||
})
|
||||
print(f" Loaded {len([s for s in all_samples if s['source']=='twitter_financial'])} Twitter Financial samples")
|
||||
except Exception as e:
|
||||
print(f" Error loading Twitter Financial: {e}")
|
||||
|
||||
# 2. FiQA
|
||||
print("Loading FiQA...")
|
||||
try:
|
||||
ds = load_dataset("explodinggradients/fiqa", "main")
|
||||
for split in ["train", "validation", "test"]:
|
||||
for item in load_dataset("explodinggradients/fiqa", "main", split=split):
|
||||
all_samples.append({
|
||||
"text": item.get("question", "") + " " + item.get("answer", ""),
|
||||
"label": "Neutral",
|
||||
"label_id": 2,
|
||||
"source": "fiqa"
|
||||
})
|
||||
print(f" Loaded FiQA samples")
|
||||
except Exception as e:
|
||||
print(f" Error loading FiQA: {e}")
|
||||
|
||||
# 3. Add real crypto events
|
||||
for event in REAL_EVENTS:
|
||||
if event["sentiment"] in SENTIMENT_LABELS:
|
||||
all_samples.append({
|
||||
"text": event["text"],
|
||||
"label": event["sentiment"],
|
||||
"label_id": SENTIMENT_MAP[event["sentiment"]],
|
||||
"source": "real_crypto_event"
|
||||
})
|
||||
|
||||
# 4. Add manual sentiment samples
|
||||
for text, label in SENTIMENT_SAMPLES:
|
||||
all_samples.append({
|
||||
"text": text,
|
||||
"label": label,
|
||||
"label_id": SENTIMENT_MAP[label],
|
||||
"source": "manual_corpus"
|
||||
})
|
||||
|
||||
print(f"Total real samples: {len(all_samples)}")
|
||||
return all_samples
|
||||
|
||||
|
||||
def create_augmented_data(count: int = 3000) -> List[Dict]:
|
||||
data = []
|
||||
for _ in range(count):
|
||||
sentiment = random.choice(["Bullish", "Bearish", "Neutral"])
|
||||
asset = random.choice(ASSETS)
|
||||
template = random.choice(SENTIMENT_TEMPLATES[sentiment])
|
||||
text = template.format(
|
||||
asset=asset,
|
||||
price=random.randint(100, 100000),
|
||||
pct=random.randint(10, 80),
|
||||
amount=f"{random.randint(1, 100)}K"
|
||||
)
|
||||
data.append({
|
||||
"text": text,
|
||||
"label": sentiment,
|
||||
"label_id": SENTIMENT_MAP[sentiment],
|
||||
"source": "synthetic"
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import numpy as np
|
||||
|
||||
# ============================================================
|
||||
# LOCAL CLASSES FOR MAIN
|
||||
# ============================================================
|
||||
|
||||
class CryptoSentimentDataset(Dataset):
|
||||
def __init__(self, samples: List[Dict], tokenizer, max_length: int = 128):
|
||||
self.samples = samples
|
||||
self.tokenizer = tokenizer
|
||||
self.max_length = max_length
|
||||
|
||||
def __len__(self):
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.samples[idx]
|
||||
text = item["text"]
|
||||
label = item.get("label_id", item.get("label", 2))
|
||||
if isinstance(label, str):
|
||||
label = SENTIMENT_MAP.get(label, 2)
|
||||
encoding = self.tokenizer(
|
||||
text, truncation=True, max_length=self.max_length, padding="max_length", return_tensors="pt"
|
||||
)
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
|
||||
print("=" * 60)
|
||||
print("FinBERT Crypto Sentiment Fine-Tuning (CPU Optimized)")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n1. Loading all sentiment data...")
|
||||
all_samples = load_all_sentiment_data()
|
||||
|
||||
# Add augmented data
|
||||
print("\n2. Generating augmented data...")
|
||||
augmented = []
|
||||
for item in create_augmented_data(3000):
|
||||
all_samples.append(item)
|
||||
print(f"Total samples: {len(all_samples)}")
|
||||
|
||||
# Split train/val/test
|
||||
print("\n3. Creating train/val/test splits...")
|
||||
labels = [s["label_id"] for s in all_samples]
|
||||
train_samples, temp_samples = train_test_split(
|
||||
all_samples, test_size=0.3, random_state=42, stratify=labels
|
||||
)
|
||||
temp_labels = [s["label_id"] for s in temp_samples]
|
||||
val_samples, test_samples = train_test_split(
|
||||
temp_samples, test_size=0.5, random_state=42, stratify=temp_labels
|
||||
)
|
||||
|
||||
print(f" Train: {len(train_samples)}, Val: {len(val_samples)}, Test: {len(test_samples)}")
|
||||
|
||||
# Class weights
|
||||
train_labels = [s["label_id"] for s in train_samples]
|
||||
class_weights = compute_class_weight("balanced", classes=np.unique(train_labels), y=train_labels)
|
||||
class_weights = torch.tensor(class_weights, dtype=torch.float)
|
||||
print(f" Class weights: {class_weights}")
|
||||
|
||||
# Tokenizer & Model
|
||||
print("\n4. Loading FinBERT...")
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert",
|
||||
num_labels=3,
|
||||
id2label={0: "Bearish", 1: "Bullish", 2: "Neutral"},
|
||||
label2id={"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
)
|
||||
|
||||
# Create datasets
|
||||
train_dataset = CryptoSentimentDataset(train_samples, tokenizer, max_length=128)
|
||||
val_dataset = CryptoSentimentDataset(val_samples, tokenizer, max_length=128)
|
||||
test_dataset = CryptoSentimentDataset(test_samples, tokenizer, max_length=128)
|
||||
|
||||
# Training arguments
|
||||
training_args = TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment",
|
||||
num_train_epochs=3,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=8,
|
||||
warmup_ratio=0.1,
|
||||
weight_decay=0.01,
|
||||
learning_rate=1.5e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
evaluation_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=2,
|
||||
logging_steps=50,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
)
|
||||
|
||||
# Class weights
|
||||
class_weights = compute_class_weight("balanced", classes=np.unique(train_labels), y=train_labels)
|
||||
class_weights_tensor = torch.tensor(class_weights, dtype=torch.float)
|
||||
|
||||
# Trainer
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment",
|
||||
num_train_epochs=3,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=8,
|
||||
warmup_ratio=0.1,
|
||||
weight_decay=0.01,
|
||||
learning_rate=1.5e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
evaluation_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=2,
|
||||
logging_steps=50,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=CryptoSentimentDataset(train_samples, tokenizer, max_length=128),
|
||||
eval_dataset=CryptoSentimentDataset(val_samples, tokenizer, max_length=128),
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=lambda eval_pred: {
|
||||
"accuracy": accuracy_score(eval_pred.label_ids, np.argmax(eval_pred.predictions, axis=-1)),
|
||||
"f1_macro": f1_score(eval_pred.label_ids, np.argmax(eval_pred.predictions, axis=-1), average="macro"),
|
||||
"f1_per_class": f1_score(eval_pred.label_ids, np.argmax(eval_pred.predictions, axis=-1), average=None).tolist()
|
||||
},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=2)]
|
||||
)
|
||||
|
||||
print("\n5. Starting training (CPU optimized)...")
|
||||
print(f" Effective batch size: 64")
|
||||
print(f" Epochs: 3")
|
||||
print(f" Max length: 128")
|
||||
|
||||
trainer.train()
|
||||
|
||||
# Evaluate
|
||||
print("\n6. Evaluating on test set...")
|
||||
test_results = trainer.evaluate()
|
||||
print(f"Test Results: {test_results}")
|
||||
|
||||
# Save
|
||||
print("\n7. Saving model...")
|
||||
trainer.save_model("./models/finbert-crypto-sentiment-final")
|
||||
AutoTokenizer.from_pretrained("ProsusAI/finbert").save_pretrained("./models/finbert-crypto-sentiment-final")
|
||||
print("Model saved!")
|
||||
|
||||
# Quick test
|
||||
print("\nQuick inference test...")
|
||||
model.eval()
|
||||
test_texts = [
|
||||
"BTC surges to new all-time high!",
|
||||
"Bitcoin crashes 50% in panic selling",
|
||||
"BTC consolidates around $50k",
|
||||
]
|
||||
for text in test_texts:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
probs = torch.softmax(outputs.logits, dim=-1)[0]
|
||||
pred = torch.argmax(probs).item()
|
||||
polarity = probs[1].item() - probs[0].item()
|
||||
print(f" '{text[:50]}...' -> {SENTIMENT_LABELS[pred]} (polarity: {polarity:.3f})")
|
||||
|
||||
print("\n✅ FinBERT fine-tuning complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import numpy as np
|
||||
|
||||
# Dataset class (needs to be at module level for pickling)
|
||||
class CryptoSentimentDataset(Dataset):
|
||||
def __init__(self, samples: List[Dict], tokenizer, max_length: int = 128):
|
||||
self.samples = samples
|
||||
self.tokenizer = tokenizer
|
||||
self.max_length = max_length
|
||||
|
||||
def __len__(self):
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.samples[idx]
|
||||
text = item["text"]
|
||||
label = item.get("label_id", item.get("label", 2))
|
||||
if isinstance(label, str):
|
||||
label = SENTIMENT_MAP.get(label, 2)
|
||||
encoding = AutoTokenizer.from_pretrained("ProsusAI/finbert")(
|
||||
text, truncation=True, max_length=128, padding="max_length", return_tensors="pt"
|
||||
)
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
# Load data
|
||||
all_samples = []
|
||||
|
||||
def load_all_sentiment_data() -> List[Dict]:
|
||||
all_samples = []
|
||||
|
||||
# 1. Twitter Financial News
|
||||
print("Loading Twitter Financial News...")
|
||||
try:
|
||||
ds = load_dataset("zeroshot/twitter-financial-news-sentiment")
|
||||
label_map = {0: "Bearish", 1: "Bullish", 2: "Neutral"}
|
||||
for split in ["train", "validation"]:
|
||||
for item in load_dataset("zeroshot/twitter-financial-news-sentiment", split=split):
|
||||
all_samples.append({
|
||||
"text": item["text"],
|
||||
"label": label_map[item["label"]],
|
||||
"label_id": item["label"],
|
||||
"source": "twitter_financial"
|
||||
})
|
||||
print(f" Loaded {len([s for s in all_samples if s['source']=='twitter_financial'])} Twitter Financial samples")
|
||||
except Exception as e:
|
||||
print(f" Error loading Twitter Financial: {e}")
|
||||
|
||||
# 2. FiQA
|
||||
print("Loading FiQA...")
|
||||
try:
|
||||
ds = load_dataset("explodinggradients/fiqa", "main")
|
||||
for split in ["train", "validation", "test"]:
|
||||
for item in load_dataset("explodinggradients/fiqa", "main", split=split):
|
||||
all_samples.append({
|
||||
"text": item.get("question", "") + " " + item.get("answer", ""),
|
||||
"label": "Neutral",
|
||||
"label_id": 2,
|
||||
"source": "fiqa"
|
||||
})
|
||||
print(f" Loaded FiQA samples")
|
||||
except Exception as e:
|
||||
print(f" Error loading FiQA: {e}")
|
||||
|
||||
# 3. Real crypto events
|
||||
for event in REAL_EVENTS:
|
||||
if event["sentiment"] in SENTIMENT_LABELS:
|
||||
all_samples.append({
|
||||
"text": event["text"],
|
||||
"label": event["sentiment"],
|
||||
"label_id": SENTIMENT_MAP[event["sentiment"]],
|
||||
"source": "real_crypto_event"
|
||||
})
|
||||
|
||||
# Manual samples
|
||||
for text, label in SENTIMENT_SAMPLES:
|
||||
all_samples.append({
|
||||
"text": text,
|
||||
"label": label,
|
||||
"label_id": SENTIMENT_MAP[label],
|
||||
"source": "manual_corpus"
|
||||
})
|
||||
|
||||
print(f"Total real samples: {len(all_samples)}")
|
||||
return all_samples
|
||||
|
||||
# Create augmented data
|
||||
def create_augmented_data(count: int = 3000) -> List[Dict]:
|
||||
data = []
|
||||
for _ in range(count):
|
||||
sentiment = random.choice(["Bullish", "Bearish", "Neutral"])
|
||||
asset = random.choice(ASSETS)
|
||||
template = random.choice(SENTIMENT_TEMPLATES[sentiment])
|
||||
text = template.format(
|
||||
asset=asset,
|
||||
price=random.randint(100, 100000),
|
||||
pct=random.randint(10, 80),
|
||||
amount=f"{random.randint(1, 100)}K"
|
||||
)
|
||||
data.append({
|
||||
"text": text,
|
||||
"label": sentiment,
|
||||
"label_id": SENTIMENT_MAP[sentiment],
|
||||
"source": "synthetic"
|
||||
})
|
||||
return data
|
||||
|
||||
# Load all data
|
||||
all_samples = []
|
||||
all_samples = load_all_sentiment_data()
|
||||
|
||||
# Add augmented
|
||||
augmented = []
|
||||
for item in create_augmented_data(3000):
|
||||
all_samples.append(item)
|
||||
print(f"Total samples: {len(all_samples)}")
|
||||
|
||||
# Split
|
||||
labels = [s["label_id"] for s in all_samples]
|
||||
train_samples, temp_samples = train_test_split(all_samples, test_size=0.3, random_state=42, stratify=labels)
|
||||
temp_labels = [s["label_id"] for s in temp_samples]
|
||||
val_samples, test_samples = train_test_split(temp_samples, test_size=0.5, random_state=42, stratify=temp_labels)
|
||||
|
||||
print(f"Train: {len(train_samples)}, Val: {len(val_samples)}, Test: {len(test_samples)}")
|
||||
|
||||
# Class weights
|
||||
train_labels = [s["label_id"] for s in train_samples]
|
||||
class_weights = compute_class_weight("balanced", classes=np.unique(train_labels), y=train_labels)
|
||||
class_weights = torch.tensor(class_weights, dtype=torch.float)
|
||||
print(f" Class weights: {class_weights}")
|
||||
|
||||
# Tokenizer & Model
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert",
|
||||
num_labels=3,
|
||||
id2label={0: "Bearish", 1: "Bullish", 2: "Neutral"},
|
||||
label2id={"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
)
|
||||
|
||||
# Tokenizer for dataset class
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
|
||||
# Datasets
|
||||
train_dataset = CryptoSentimentDataset(train_samples, tokenizer, max_length=128)
|
||||
val_dataset = CryptoSentimentDataset(val_samples, tokenizer, max_length=128)
|
||||
test_dataset = CryptoSentimentDataset(test_samples, tokenizer, max_length=128)
|
||||
|
||||
# Training args
|
||||
training_args = TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment",
|
||||
num_train_epochs=3,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=8,
|
||||
warmup_ratio=0.1,
|
||||
weight_decay=0.01,
|
||||
learning_rate=1.5e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
evaluation_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=2,
|
||||
logging_steps=50,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
)
|
||||
|
||||
# Trainer
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment",
|
||||
num_train_epochs=3,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=8,
|
||||
warmup_ratio=0.1,
|
||||
weight_decay=0.01,
|
||||
learning_rate=1.5e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
evaluation_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=2,
|
||||
logging_steps=50,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=CryptoSentimentDataset(train_samples, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_length=128),
|
||||
eval_dataset=CryptoSentimentDataset(val_samples, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_length=128),
|
||||
tokenizer=AutoTokenizer.from_pretrained("ProsusAI/finbert"),
|
||||
compute_metrics=lambda eval_pred: {
|
||||
"accuracy": accuracy_score(eval_pred.label_ids, np.argmax(eval_pred.predictions, axis=-1)),
|
||||
"f1_macro": f1_score(eval_pred.label_ids, np.argmax(eval_pred.predictions, axis=-1), average="macro"),
|
||||
"f1_per_class": f1_score(eval_pred.label_ids, np.argmax(eval_pred.predictions, axis=-1), average=None).tolist()
|
||||
},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=2)]
|
||||
)
|
||||
|
||||
print("\n5. Starting training (CPU optimized)...")
|
||||
print(f" Effective batch size: 64")
|
||||
print(f" Epochs: 3")
|
||||
print(f" Max length: 128")
|
||||
|
||||
trainer.train()
|
||||
|
||||
# Evaluate
|
||||
print("\nEvaluating on test set...")
|
||||
test_results = trainer.evaluate()
|
||||
print(f"Test Results: {test_results}")
|
||||
|
||||
# Save
|
||||
trainer.save_model("./models/finbert-crypto-sentiment-final")
|
||||
AutoTokenizer.from_pretrained("ProsusAI/finbert").save_pretrained("./models/finbert-crypto-sentiment-final")
|
||||
print("Model saved!")
|
||||
|
||||
# Quick test
|
||||
print("\nQuick inference test...")
|
||||
model.eval()
|
||||
test_texts = [
|
||||
"BTC surges to new all-time high!",
|
||||
"Bitcoin crashes 50% in panic selling",
|
||||
"BTC consolidates around $50k",
|
||||
]
|
||||
for text in test_texts:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
probs = torch.softmax(outputs.logits, dim=-1)[0]
|
||||
pred = torch.argmax(probs).item()
|
||||
polarity = probs[1].item() - probs[0].item()
|
||||
print(f" '{text[:50]}...' -> {SENTIMENT_LABELS[pred]} (polarity: {polarity:.3f})")
|
||||
|
||||
print("\n✅ FinBERT fine-tuning complete!")
|
||||
@@ -1,338 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ultra-fast FinBERT fine-tuning demo (CPU, ~10 min).
|
||||
Uses tiny dataset, 1 epoch, aggressive settings for demo purposes.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import torch.nn as np
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
# Minimal real samples
|
||||
REAL_SAMPLES = [
|
||||
{"text": "BTC surges to new all-time high as institutional adoption accelerates!", "label_id": 1},
|
||||
{"text": "ETH breaks $4000 resistance with massive volume!", "label_id": 1},
|
||||
{"text": "Institutional adoption drives Bitcoin higher!", "label_id": 1},
|
||||
{"text": "Bitcoin breaks $100k! New ATH!", "label_id": 1},
|
||||
{"text": "Institutional inflows hit record high", "label_id": 1},
|
||||
("BTC crashes 50% in hours", 0),
|
||||
("Exchange hacked, $100M stolen", 0),
|
||||
("SEC sues major exchange", 0),
|
||||
("Bitcoin crashes hard, panic selling everywhere", 0),
|
||||
("Massive liquidation cascade wipes out $200M in longs", 0),
|
||||
("Whale sells 10000 BTC", 0),
|
||||
("Bitcoin price drops 50%", 0),
|
||||
("Support broken with bearish structure", 0),
|
||||
("Panic selling and forced liquidation", 0),
|
||||
("BTC at $50k, ETH at $3k", 2),
|
||||
("Market consolidating in range", 2),
|
||||
("Bitcoin remains stable around $30k", 2),
|
||||
("Market consolidating with no clear direction", 2),
|
||||
]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
# Real crypto events
|
||||
REAL_EVENTS = [
|
||||
{"text": "XRP bridge drained for $200,000 after software mistook fake deposits for real ones.", "label_id": 0},
|
||||
{"text": "Major hack on DeFi protocol drains $50M. Users panic as TVL collapses.", "label_id": 0},
|
||||
{"text": "KuCoin Lists Catizen (CATI) for Spot Trading.", "label_id": 1},
|
||||
{"text": "Binance Becomes First Exchange to List Trump-Linked WLFI Token.", "label_id": 1},
|
||||
{"text": "SEC files lawsuit against major exchange for unregistered securities.", "label_id": 0},
|
||||
{"text": "CFTC files to dismiss CME's lawsuit over crypto perpetual futures.", "label_id": 2},
|
||||
{"text": "Ethereum Dencun upgrade activates Proto-Danksharding (EIP-4844).", "label_id": 1},
|
||||
{"text": "Ethereum Shanghai upgrade goes live. Stakers can now withdraw.", "label_id": 1},
|
||||
{"text": "JPMorganChase and Coinbase Launch Strategic Partnership.", "label_id": 1},
|
||||
{"text": "Chainlink and Mastercard Partner to Enable Over 3 Billion Cardholders.", "label_id": 1},
|
||||
{"text": "Bitcoin whale moves $116 million in BTC after 11-year dormancy.", "label_id": 2},
|
||||
{"text": "Breaking: Fed pauses rate hikes. Bitcoin jumps 5% on dovish pivot.", "label_id": 1},
|
||||
{"text": "Massive liquidation cascade wipes out $200M in longs.", "label_id": 0},
|
||||
{"text": "Bitcoin ETF inflows hit $731M, highest since January.", "label_id": 1},
|
||||
{"text": "Coinbase delists XRP after SEC lawsuit.", "label_id": 0},
|
||||
]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
self.max_len = 64
|
||||
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len,
|
||||
padding="max_length", return_tensors="pt")
|
||||
return {
|
||||
"input_ids": enc["input_ids"].squeeze(0),
|
||||
"attention_mask": enc["attention_mask"].squeeze(0),
|
||||
"labels": torch.tensor(self.labels[i], dtype=torch.long)
|
||||
}
|
||||
|
||||
def main():
|
||||
print("=" * 50)
|
||||
print("Quick FinBERT Crypto Fine-Tune (CPU, ~5 min)")
|
||||
print("=" * 50)
|
||||
|
||||
# Build tiny dataset
|
||||
texts = []
|
||||
labels = []
|
||||
|
||||
# Manual samples
|
||||
for text, label in [
|
||||
("BTC breaks $100k! New ATH!", 1),
|
||||
("ETH to $10k by EOY, accumulate now", 1),
|
||||
("Institutional inflows hit record high", 1),
|
||||
("Bitcoin reaches new all-time high", 1),
|
||||
("Ethereum merge successful, staking rewards now live", 1),
|
||||
("Massive ETF inflows drive Bitcoin to new highs", 1),
|
||||
("Golden cross confirmed on Bitcoin weekly chart", 1),
|
||||
("Institutional adoption drives Bitcoin higher", 1),
|
||||
("ETF approval drives massive inflows", 1),
|
||||
("Market is bullish on Bitcoin", 1),
|
||||
("BTC crashes 50% in hours", 0),
|
||||
("Exchange hacked, $100M stolen", 0),
|
||||
("SEC sues major exchange", 0),
|
||||
("Bitcoin crashes hard, panic selling everywhere", 0),
|
||||
("Massive liquidation cascade wipes out $200M in longs", 0),
|
||||
("VIX drops below 15 as market volatility decreases", 0),
|
||||
("Whale sells 10000 BTC", 0),
|
||||
("Bitcoin price drops 50%", 0),
|
||||
("Support broken with bearish structure", 0),
|
||||
("Panic selling and forced liquidation", 0),
|
||||
("BTC at $50k, ETH at $3k", 2),
|
||||
("Market consolidating in range", 2),
|
||||
("Bitcoin remains stable around $30k", 2),
|
||||
("VIX drops below 15 as market volatility decreases", 2),
|
||||
("Market consolidating with no clear direction", 2),
|
||||
("Bitcoin price stable around $30k", 2),
|
||||
("Consolidation phase continues", 2),
|
||||
("Market in wait-and-see mode", 2),
|
||||
("Sideways action continues", 2),
|
||||
("Low volatility environment persists", 2),
|
||||
]:
|
||||
texts.append(t)
|
||||
labels.append(l)
|
||||
|
||||
# Add real events
|
||||
for event in [
|
||||
{"text": "XRP bridge drained for $200,000 after software mistook fake deposits.", "label_id": 0},
|
||||
{"text": "Major hack on DeFi protocol drains $50M.", "label_id": 0},
|
||||
{"text": "KuCoin Lists Catizen (CATI) for Spot Trading.", "label_id": 1},
|
||||
{"text": "Binance Becomes First Exchange to List Trump-Linked WLFI Token.", "label_id": 1},
|
||||
{"text": "SEC files lawsuit against major exchange for unregistered securities.", "label_id": 0},
|
||||
{"text": "Ethereum Dencun upgrade activates Proto-Danksharding (EIP-4844).", "label_id": 1},
|
||||
{"text": "JPMorganChase and Coinbase Launch Strategic Partnership.", "label_id": 1},
|
||||
{"text": "Chainlink and Mastercard Partner to Enable Over 3 Billion Cardholders.", "label_id": 1},
|
||||
{"text": "Bitcoin whale moves $116 million in BTC after 11-year dormancy.", "label_id": 2},
|
||||
{"text": "Breaking: Fed pauses rate hikes. Bitcoin jumps 5% on dovish pivot.", "label_id": 1},
|
||||
{"text": "Massive liquidation cascade wipes out $200M in longs.", "label_id": 0},
|
||||
{"text": "Bitcoin ETF inflows hit $731M, highest since January.", "label_id": 1},
|
||||
{"text": "Coinbase delists XRP after SEC lawsuit.", "label_id": 0},
|
||||
]:
|
||||
texts.append(item["text"])
|
||||
labels.append(item["label_id"])
|
||||
|
||||
# Augmented
|
||||
assets = ["BTC", "ETH", "SOL", "AVAX", "MATIC"]
|
||||
templates = {
|
||||
1: ["{a} surges to new highs", "{a} breaks resistance at ${p}", "Institutional adoption drives {a} higher"],
|
||||
0: ["{a} crashes {p}%", "{a} breaks support at ${p}", "Panic selling in {a}"],
|
||||
2: ["{a} consolidates at ${p}", "{a} trades sideways", "Market waits for {a} direction"],
|
||||
}
|
||||
for _ in range(500):
|
||||
sid = random.randint(0, 2)
|
||||
a = random.choice(["BTC", "ETH", "SOL", "AVAX", "MATIC"])
|
||||
t = random.choice([p for p in range(3) if p in [0,1,2]]) # simplified
|
||||
template = random.choice(templates[sid])
|
||||
text = template.format(a=random.choice(assets), p=random.randint(100,100000))
|
||||
texts.append(text)
|
||||
labels.append(sid)
|
||||
|
||||
print(f"Total samples: {len(texts)}")
|
||||
|
||||
# Split
|
||||
from sklearn.model_selection import train_test_split
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42, stratify=labels)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42, stratify=temp_l)
|
||||
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
|
||||
# Tokenizer & Model
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
import torch
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert", num_labels=3,
|
||||
id2label={0:"Bearish",1:"Bullish",2:"Neutral"},
|
||||
label2id={"Bearish":0,"Bullish":1,"Neutral":2}
|
||||
)
|
||||
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=64, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.long)}
|
||||
|
||||
train_ds = QuickDataset(texts[:len(train_t)], labels[:len(train_t)], None)
|
||||
val_ds = QuickDataset(texts[len(train_t):len(train_t)+len(temp_t)], labels[len(train_l):len(train_l)+len(temp_l)], None)
|
||||
test_ds = QuickDataset(texts[-len(test_t):], labels[-len(test_l):], None)
|
||||
|
||||
# Fix: create datasets properly
|
||||
train_texts = texts[:len(train_t)]
|
||||
train_labels = labels[:len(train_l)]
|
||||
val_texts = texts[len(train_t):len(train_t)+len(temp_t)]
|
||||
val_labels = labels[len(train_l):len(train_l)+len(temp_l)]
|
||||
test_texts = texts[-len(test_t):]
|
||||
test_labels = labels[-len(test_l):]
|
||||
|
||||
train_ds = QuickDataset(train_texts, train_labels, None)
|
||||
val_ds = QuickDataset(val_texts, val_labels, None)
|
||||
test_ds = QuickDataset(test_texts, test_labels, None)
|
||||
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
import torch
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert", num_labels=3,
|
||||
id2label={0:"Bearish",1:"Bullish",2:"Neutral"},
|
||||
label2id={"Bearish":0,"Bullish":1,"Neutral":2}
|
||||
)
|
||||
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = tokenizer; self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.long)}
|
||||
|
||||
train_ds = QuickDataset(train_texts, train_labels, tokenizer)
|
||||
val_ds = QuickDataset(val_texts, val_labels, tokenizer)
|
||||
test_ds = QuickDataset(test_texts, test_labels, tokenizer)
|
||||
|
||||
# Train
|
||||
trainer = Trainer(
|
||||
model=AutoModelForSequenceClassification.from_pretrained("ProsusAI/finbert", num_labels=3, id2label={0:"Bearish",1:"Bullish",2:"Neutral"}, label2id={"Bearish":0,"Bullish":1,"Neutral":2}),
|
||||
args=TrainingArguments(
|
||||
|
||||
num_train_epochs=1,
|
||||
per_device_train_batch_size=16,
|
||||
per_device_eval_batch_size=32,
|
||||
gradient_accumulation_steps=2,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=2e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
evaluation_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
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,
|
||||
save_total_limit=1,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
|
||||
),
|
||||
train_dataset=QuickDataset(train_texts, train_labels, tokenizer, max_len=64),
|
||||
eval_dataset=QuickDataset(val_texts, val_labels, tokenizer, max_len=64),
|
||||
tokenizer=AutoTokenizer.from_pretrained("ProsusAI/finbert"),
|
||||
compute_metrics=lambda ep: {"f1_macro": f1_score(ep.label_ids, np.argmax(ep.predictions, axis=-1), average="macro")},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
from sklearn.metrics import f1_score
|
||||
import torch
|
||||
|
||||
trainer = Trainer(
|
||||
model=AutoModelForSequenceClassification.from_pretrained("ProsusAI/finbert", num_labels=3, id2label={0:"Bearish",1:"Bullish",2:"Neutral"}, label2id={"Bearish":0,"Bullish":1,"Neutral":2}),
|
||||
args=TrainingArguments(
|
||||
|
||||
num_train_epochs=1,
|
||||
per_device_train_batch_size=16,
|
||||
per_device_eval_batch_size=32,
|
||||
gradient_accumulation_steps=2,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=2e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
evaluation_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
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,
|
||||
save_total_limit=1,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
|
||||
),
|
||||
train_dataset=QuickDataset(train_texts, train_labels, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64),
|
||||
eval_dataset=QuickDataset(val_texts, val_labels, AutoTokenizer.from_pretrained("ProsusAI/finbert"), max_len=64),
|
||||
tokenizer=AutoTokenizer.from_pretrained("ProsusAI/finbert"),
|
||||
compute_metrics=lambda ep: {"f1_macro": f1_score(ep.label_ids, np.argmax(ep.predictions, axis=-1), average="macro")},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("Training (1 epoch, ~2-3 min)...")
|
||||
trainer.train()
|
||||
|
||||
# Test
|
||||
print("\nTest results:")
|
||||
results = trainer.evaluate(ep=ep) if False else trainer.evaluate()
|
||||
print(f"Test: {results}")
|
||||
|
||||
trainer.save_model("./models/finbert-crypto-quick")
|
||||
AutoTokenizer.from_pretrained("ProsusAI/finbert").save_pretrained("./models/finbert-crypto-quick")
|
||||
print("Saved!")
|
||||
|
||||
# Quick test
|
||||
model.eval()
|
||||
for text in ["BTC surges to new ATH!", "Bitcoin crashes 50%!", "BTC consolidates at $50k"]:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=64, padding=True)
|
||||
with torch.no_grad():
|
||||
out = model(**inputs)
|
||||
probs = torch.softmax(out.logits, dim=-1)[0]
|
||||
pred = torch.argmax(probs).item()
|
||||
pol = probs[1].item() - probs[0].item()
|
||||
print(f" '{text}' -> {['Bearish','Bullish','Neutral'][pred]} (pol: {pol:.3f})")
|
||||
print("Done!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import random, torch
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import f1_score
|
||||
import numpy as np
|
||||
main()
|
||||
@@ -1,356 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Robust FinBERT fine-tuning for crypto sentiment with:
|
||||
- Expanded labeled data (92 verified samples)
|
||||
- Proper crypto semantics (Bearish=0, Neutral=1, Bullish=2)
|
||||
- Checkpoint-based training to prevent forgetting
|
||||
- Class-weighted loss, early stopping, LR scheduling
|
||||
- Saves best model based on validation F1_macro
|
||||
"""
|
||||
|
||||
import json
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback, TrainerCallback
|
||||
)
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score, classification_report
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# CRYPTO SENTIMENT LABELS (matches FinBERT native order: negative=0, neutral=1, positive=2)
|
||||
# For crypto: Bearish(negative)=0, Neutral=1, Bullish(positive)=2
|
||||
SENTIMENT_LABELS = ["Bearish", "Neutral", "Bullish"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Neutral": 1, "Bullish": 2}
|
||||
|
||||
class SentimentDataset(Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=128):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = tokenizer
|
||||
self.max_len = max_len
|
||||
|
||||
def __len__(self):
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
text = self.texts[idx]
|
||||
label = self.labels[idx]
|
||||
|
||||
encoding = self.tokenizer(
|
||||
text,
|
||||
truncation=True,
|
||||
max_length=self.max_len,
|
||||
padding="max_length",
|
||||
return_tensors="pt"
|
||||
)
|
||||
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"token_type_ids": encoding.get("token_type_ids", torch.zeros_like(encoding["input_ids"])).squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
def compute_metrics(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return {
|
||||
"accuracy": accuracy_score(labels, predictions),
|
||||
"f1_macro": f1_score(labels, predictions, average="macro"),
|
||||
"f1_per_class": f1_score(labels, predictions, average=None).tolist()
|
||||
}
|
||||
|
||||
class BestModelCheckpoint(TrainerCallback):
|
||||
"""Custom callback to save best model based on validation F1_macro"""
|
||||
def __init__(self, save_path: str):
|
||||
self.save_path = save_path
|
||||
self.best_f1 = 0.0
|
||||
|
||||
def on_evaluate(self, args, state, control, metrics=None, **kwargs):
|
||||
if metrics is not None:
|
||||
eval_f1 = metrics.get("eval_f1_macro", 0)
|
||||
if eval_f1 > self.best_f1:
|
||||
self.best_f1 = eval_f1
|
||||
logger.info(f"New best F1_macro: {eval_f1:.4f} - saving model to {self.save_path}")
|
||||
# The Trainer handles saving via save_strategy="epoch" and load_best_model_at_end=True
|
||||
# This callback just tracks the best metric
|
||||
|
||||
def load_labeled_data(label_file: str):
|
||||
"""Load verified labeled data from labeling pipeline output"""
|
||||
texts, labels = [], []
|
||||
with open(label_file) as f:
|
||||
for line in f:
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
texts.append(r['text'])
|
||||
labels.append(SENTIMENT_MAP[r['labels']['sentiment']])
|
||||
return texts, labels
|
||||
|
||||
def build_augmented_data():
|
||||
"""Additional high-quality synthetic samples for data augmentation"""
|
||||
|
||||
# CRYPTO BEARISH (label 0) - price down, bad news
|
||||
bearish_texts = [
|
||||
"Bitcoin crashes 30% in hours as leverage flushes out longs",
|
||||
"Massive liquidation cascade wipes out $500M in longs across exchanges",
|
||||
"Exchange hacked, $100M stolen, users panic selling",
|
||||
"Regulatory crackdown: SEC files enforcement action against major DeFi protocol",
|
||||
"Rug pull: Dev team abandons project, drains liquidity pool",
|
||||
"Bankruptcy filing: Major crypto lender files Chapter 11",
|
||||
"Stablecoin depeg: USDT drops to $0.95 on redemption fears",
|
||||
"Smart contract vulnerability discovered, $50M at risk",
|
||||
"Market structure breakdown: Order books thin, spreads widen",
|
||||
"Forced liquidations trigger death spiral in lending protocol",
|
||||
"Contagion risk: Major fund exposure to failed protocol revealed",
|
||||
"Bear market confirmed: Lower highs, lower lows on weekly chart",
|
||||
"Institutional outflows: ETF sees record redemptions for 5th week",
|
||||
"Mining capitulation: Hash rate drops 20% as price falls below cost",
|
||||
"Major hack: Radiant Capital loses $50M in exploit. Funds moved to Tornado Cash.",
|
||||
"Curve Finance hit by $50M exploit. Vyper compiler bug. CRV drops 20%.",
|
||||
"Wintermute market maker loses $20M in exploit. Funds returned.",
|
||||
"SEC sues Kraken for operating unregistered securities exchange.",
|
||||
"SEC charges Uniswap Labs. UNI drops 15%.",
|
||||
"Binance delists Monero, Zcash, and 4 other privacy coins.",
|
||||
"OKX delists USDT trading pairs in EEA region. MiCA compliance.",
|
||||
"Solana network experiences 5-hour outage. SOL drops 8% on news.",
|
||||
"Circle USDC depegs to $0.97 after SVB exposure. $3.3B reserves stuck.",
|
||||
"Australia ASIC cracks down on unlicensed crypto exchanges.",
|
||||
"Treasury Secretary Yellen comments on stablecoin regulation.",
|
||||
]
|
||||
|
||||
# CRYPTO BULLISH (label 2) - price up, good news
|
||||
bullish_texts = [
|
||||
"Bitcoin surges to $108k as institutional inflows surge. BlackRock IBIT sees record $1.2B daily inflow.",
|
||||
"Bitcoin ETF inflows hit record $2.1B in single week. Cumulative AUM passes $50B.",
|
||||
"Bitcoin hits $100,000 for first time ever. MicroStrategy, ETFs, sovereign buying drive rally.",
|
||||
"Pump.fun revenue hits $100M in 30 days. Memecoin factory launches 50k tokens/day. SOL fees surge.",
|
||||
"MicroStrategy buys additional 12,000 BTC at $61M. Total holdings now 190,000 BTC.",
|
||||
"Institutional adoption accelerates: Fortune 500 companies adding BTC to treasury",
|
||||
"ETF approval drives massive inflows: $10B in first month",
|
||||
"Golden cross confirmed on Bitcoin weekly chart, technical breakout",
|
||||
"Supply shock: Exchange balances hit 5-year low as holders accumulate",
|
||||
"Layer 2 adoption surges: Arbitrum and Optimism TVL doubles",
|
||||
"Real yield protocols attract TradFi capital seeking returns",
|
||||
"Token unlock schedule favorable: Low float, high demand dynamics",
|
||||
"Major partnership: TradFi giant integrates blockchain settlement",
|
||||
"Sovereign wealth fund announces Bitcoin allocation",
|
||||
"Hash rate hits all-time high, mining investment surges",
|
||||
"Developer activity reaches record highs across major ecosystems",
|
||||
"Stablecoin supply grows 50% YoY, indicating fresh capital entry",
|
||||
"Options market signaling upside: Call skew at multi-year highs",
|
||||
"Macro tailwinds: Rate cuts expected, dollar weakening",
|
||||
"SEC approves spot Bitcoin ETFs for 11 issuers including BlackRock, Fidelity, ARK.",
|
||||
"Hong Kong SFC approves spot Bitcoin ETFs. Asia ETF race begins.",
|
||||
"Canada OSC approves first spot Solana ETF. North American product expansion.",
|
||||
"Safe{Wallet} hits $100B secured. Multi-sig adoption standard.",
|
||||
"Ripple wins landmark court case against SEC. XRP surges 70%.",
|
||||
]
|
||||
|
||||
# CRYPTO NEUTRAL (label 1) - sideways, structural, non-directional
|
||||
neutral_texts = [
|
||||
"Bitcoin consolidates in tight range between $50k-$52k",
|
||||
"Ethereum gas fees stable at 15 gwei amid low activity",
|
||||
"Market awaits FOMC decision, volumes below average",
|
||||
"Trading range established: Support at $48k, resistance at $55k",
|
||||
"Altcoin season index neutral at 50, no clear trend",
|
||||
"Funding rates flat across perpetual futures markets",
|
||||
"On-chain metrics show equilibrium: Inflows match outflows",
|
||||
"Derivatives open interest stable, no excessive leverage",
|
||||
"Stablecoin market cap flat month-over-month",
|
||||
"Developer conference announces roadmap, no token news",
|
||||
"Governance proposal passes: Parameter change only, no value accrual",
|
||||
"Exchange lists new token, volume modest, no price impact",
|
||||
"Research report: Fair value estimate $55k-$65k range",
|
||||
"Whale wallet rotates positions, no net accumulation or distribution",
|
||||
"Ethereum Dencun upgrade goes live. Proto-Danksharding reduces L2 fees by 90%.",
|
||||
"Coinbase lists Pepe and Bonk memecoins. Trading opens with 100x volume spike.",
|
||||
"Ethereum Pectra upgrade activated. EIP-7702 account abstraction live.",
|
||||
"Arbitrum DAO approves $200M ARB grant program. Voting passes with 92%.",
|
||||
"EigenLayer restaking TVL hits $20B. Points season 2 announced.",
|
||||
"Hyperliquid DEX launches HYPE token airdrop. $1.2B TVL locked.",
|
||||
"dYdX V4 mainnet launches. Cosmos-based order book DEX.",
|
||||
"EigenLayer restaking TVL hits $25B. Largest DeFi category.",
|
||||
"Ripple wins landmark court case against SEC. XRP surges 70% on ruling.",
|
||||
"Babylon Bitcoin staking testnet. Bitcoin security for PoS chains.",
|
||||
"Safe{Wallet} hits $100B secured. Multi-sig adoption standard.",
|
||||
]
|
||||
|
||||
texts = []
|
||||
labels = []
|
||||
|
||||
for t in bearish_texts:
|
||||
texts.append(t); labels.append(0)
|
||||
for t in bullish_texts:
|
||||
texts.append(t); labels.append(2)
|
||||
for t in neutral_texts:
|
||||
texts.append(t); labels.append(1)
|
||||
|
||||
return texts, labels
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("ROBUST FINBERT FINE-TUNING FOR CRYPTO SENTIMENT")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load verified labeled data (primary source)
|
||||
print("\n1. Loading verified labeled data from labeling pipeline...")
|
||||
verified_texts, verified_labels = load_labeled_data("data/labeled_verified.jsonl")
|
||||
print(f" Verified samples: {len(verified_texts)}")
|
||||
|
||||
# 2. Build augmented data
|
||||
print("\n2. Building augmented training data...")
|
||||
aug_texts, aug_labels = build_augmented_data()
|
||||
print(f" Augmented samples: {len(aug_texts)}")
|
||||
|
||||
# 3. Combine with weighted emphasis on verified data (3x weight)
|
||||
all_texts = verified_texts * 3 + aug_texts
|
||||
all_labels = verified_labels * 3 + aug_labels
|
||||
|
||||
print(f"\n3. Total training samples: {len(all_texts)}")
|
||||
print(f" Bearish(0): {all_labels.count(0)}, Neutral(1): {all_labels.count(1)}, Bullish(2): {all_labels.count(2)}")
|
||||
|
||||
# 4. Train/val/test split (stratified)
|
||||
train_texts, temp_texts, train_labels, temp_labels = train_test_split(
|
||||
all_texts, all_labels, test_size=0.3, random_state=42, stratify=all_labels
|
||||
)
|
||||
val_texts, test_texts, val_labels, test_labels = train_test_split(
|
||||
temp_texts, temp_labels, test_size=0.5, random_state=42, stratify=temp_labels
|
||||
)
|
||||
|
||||
print(f" Train: {len(train_texts)}, Val: {len(val_texts)}, Test: {len(test_texts)}")
|
||||
print(f" Train dist: Bearish={train_labels.count(0)}, Neutral={train_labels.count(1)}, Bullish={train_labels.count(2)}")
|
||||
|
||||
# 5. Load BASE FinBERT (not previously fine-tuned)
|
||||
print("\n4. Loading BASE FinBERT...")
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert",
|
||||
num_labels=3,
|
||||
id2label={0: "Bearish", 1: "Neutral", 2: "Bullish"},
|
||||
label2id={"Bearish": 0, "Neutral": 1, "Bullish": 2}
|
||||
)
|
||||
|
||||
# 6. Create datasets
|
||||
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, max_len=128)
|
||||
val_dataset = SentimentDataset(val_texts, val_labels, tokenizer, max_len=128)
|
||||
|
||||
# 7. Class weights - compute from training data
|
||||
class_weights = compute_class_weight("balanced", classes=np.array([0,1,2]), y=np.array(train_labels))
|
||||
class_weights = torch.tensor(class_weights, dtype=torch.float)
|
||||
print(f" Class weights: {class_weights}")
|
||||
|
||||
# 8. Training arguments with robust settings
|
||||
output_dir = "./models/finbert-crypto-sentiment-v4"
|
||||
|
||||
training_args = TrainingArguments(
|
||||
output_dir=output_dir,
|
||||
num_train_epochs=8,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=1e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=0,
|
||||
logging_steps=5,
|
||||
save_total_limit=3,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
weight_decay=0.01,
|
||||
max_grad_norm=1.0,
|
||||
)
|
||||
|
||||
class WeightedTrainer(Trainer):
|
||||
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
||||
labels = inputs.get("labels")
|
||||
outputs = model(**inputs)
|
||||
logits = outputs.get("logits")
|
||||
loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights.to(logits.device))
|
||||
loss = loss_fct(logits.view(-1, 3), labels.view(-1))
|
||||
return (loss, outputs) if return_outputs else loss
|
||||
|
||||
trainer = WeightedTrainer(
|
||||
model=model,
|
||||
args=training_args,
|
||||
train_dataset=train_dataset,
|
||||
eval_dataset=val_dataset,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics,
|
||||
callbacks=[
|
||||
EarlyStoppingCallback(early_stopping_patience=3),
|
||||
BestModelCheckpoint(output_dir)
|
||||
]
|
||||
)
|
||||
|
||||
print("\n5. Training (8 epochs with early stopping)...")
|
||||
trainer.train()
|
||||
|
||||
# 9. Evaluate on test set
|
||||
print("\n6. Evaluating on test set...")
|
||||
test_dataset = SentimentDataset(test_texts, test_labels, tokenizer, max_len=128)
|
||||
test_results = trainer.evaluate(test_dataset)
|
||||
print(f" Test results: {test_results}")
|
||||
|
||||
# 10. Detailed classification report
|
||||
print("\n7. Detailed classification report...")
|
||||
test_trainer = Trainer(model=model, tokenizer=tokenizer, compute_metrics=compute_metrics)
|
||||
predictions = test_trainer.predict(test_dataset)
|
||||
preds = np.argmax(predictions.predictions, axis=1)
|
||||
print(classification_report(test_labels, preds, target_names=SENTIMENT_LABELS))
|
||||
|
||||
# 11. Save best model to production path
|
||||
print("\n8. Saving production model...")
|
||||
model.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
tokenizer.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
print(" ✅ Model saved to models/finbert-crypto-sentiment/")
|
||||
|
||||
# 12. Quick inference test on critical cases
|
||||
print("\n9. Quick inference test on critical cases...")
|
||||
model.eval()
|
||||
test_cases = [
|
||||
("Bitcoin surges to $108k as institutional inflows surge", 2),
|
||||
("Bitcoin crashes 50% in hours, massive selloff", 0),
|
||||
("BTC at $50k, ETH at $3k, market consolidating", 1),
|
||||
("Major hack on exchange, $100M stolen, panic selling", 0),
|
||||
("ETF approval drives massive inflows, price to moon", 2),
|
||||
("Market consolidating in tight range, no clear direction", 1),
|
||||
("Circle USDC depegs to $0.97 after SVB exposure", 0),
|
||||
("SEC sues Kraken for operating unregistered securities", 0),
|
||||
("SEC approves spot Bitcoin ETFs for 11 issuers", 2),
|
||||
("Australia ASIC cracks down on unlicensed exchanges", 0),
|
||||
]
|
||||
|
||||
print(f"{'Text':<60} {'Pred':<10} {'Exp':<10} {'Polarity':<10} {'Conf':<6}")
|
||||
print("-" * 100)
|
||||
for text, expected in test_cases:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
probs = torch.softmax(outputs.logits, dim=-1).numpy()[0]
|
||||
pred = np.argmax(probs)
|
||||
polarity = probs[2] - probs[0]
|
||||
conf = probs[pred]
|
||||
status = "✓" if pred == expected else "✗"
|
||||
print(f"{text[:58]:<60} {SENTIMENT_LABELS[pred]:<10} {SENTIMENT_LABELS[expected]:<10} {polarity:>+6.2f} {conf:.2f} {status}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("FINE-TUNING COMPLETE!")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,300 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Improve sentiment model with more training data and better training.
|
||||
Uses labeled_verified.jsonl + augmented data from specs.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
def load_labeled_data(label_file: str):
|
||||
"""Load verified labeled data from labeling pipeline"""
|
||||
texts, labels = [], []
|
||||
with open(label_file) as f:
|
||||
for line in f:
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
texts.append(r['text'])
|
||||
labels.append(SENTIMENT_MAP[r['labels']['sentiment']])
|
||||
return texts, labels
|
||||
|
||||
def build_augmented_data():
|
||||
"""Build comprehensive training data from Spec #2 keywords + labeled data"""
|
||||
|
||||
# Bearish samples (from Spec #2 bearish keywords + verified data)
|
||||
bearish_texts = [
|
||||
# From labeled verified data
|
||||
"Major hack: Radiant Capital loses $50M in exploit. Attacker exploits rounding error in lending market. Funds moved to Tornado Cash.",
|
||||
"Curve Finance hit by $50M exploit. Vyper compiler bug affects multiple pools. CRV drops 20%.",
|
||||
"Wintermute market maker loses $20M in exploit. Private key compromise suspected. Funds returned.",
|
||||
"SEC sues Kraken for operating unregistered securities exchange. Alleged commingling of customer funds.",
|
||||
"SEC charges Uniswap Labs with operating unregistered securities exchange. UNI drops 15%.",
|
||||
"Binance delists Monero (XMR), Zcash (ZEC), and 4 other privacy coins. Cites regulatory compliance review.",
|
||||
"OKX delists USDT trading pairs in EEA region. MiCA compliance cited. USDT/USD pairs remain.",
|
||||
"Solana network experiences 5-hour outage. Validators restart cluster. SOL drops 8% on news.",
|
||||
"Circle USDC depegs to $0.97 after SVB exposure revealed. $3.3B reserves stuck at SVB. Arbitrage bots profit.",
|
||||
# Spec #2 bearish keywords expanded
|
||||
"Bitcoin crashes 30% in hours as leverage flushes out longs",
|
||||
"Massive liquidation cascade wipes out $500M in longs across exchanges",
|
||||
"Exchange hacked, $100M stolen, users panic selling",
|
||||
"Regulatory crackdown: SEC files enforcement action against major DeFi protocol",
|
||||
"Rug pull: Dev team abandons project, drains liquidity pool",
|
||||
"Bankruptcy filing: Major crypto lender files Chapter 11",
|
||||
"Stablecoin depeg: USDT drops to $0.95 on redemption fears",
|
||||
"Smart contract vulnerability discovered, $50M at risk",
|
||||
"Market structure breakdown: Order books thin, spreads widen",
|
||||
"Forced liquidations trigger death spiral in lending protocol",
|
||||
"Contagion risk: Major fund exposure to failed protocol revealed",
|
||||
"Bear market confirmed: Lower highs, lower lows on weekly chart",
|
||||
"Institutional outflows: ETF sees record redemptions for 5th week",
|
||||
"Mining capitulation: Hash rate drops 20% as price falls below cost",
|
||||
]
|
||||
|
||||
# Bullish samples
|
||||
bullish_texts = [
|
||||
# From labeled verified data
|
||||
"Bitcoin hits new all-time high of $108,000 as institutional inflows surge. BlackRock IBIT ETF sees record $1.2B daily inflow.",
|
||||
"Bitcoin ETF inflows hit record $2.1B in single week. IBIT alone sees $1.2B. Cumulative AUM passes $50B.",
|
||||
"Bitcoin hits $100,000 for first time ever. MicroStrategy, ETFs, and sovereign buying drive rally.",
|
||||
"Pump.fun revenue hits $100M in 30 days. Memecoin factory launches 50k tokens/day. SOL fees surge.",
|
||||
"MicroStrategy buys additional 12,000 BTC at $61M. Total holdings now 190,000 BTC. Stock MSTR up 15% premarket.",
|
||||
"Arbitrum DAO approves $200M ARB grant program for gaming ecosystem. Voting passes with 92% approval.",
|
||||
"EigenLayer restaking TVL hits $20B. ETH restaking becomes largest DeFi category. Points season 2 announced.",
|
||||
"Hyperliquid DEX launches HYPE token airdrop. $1.2B TVL locked. Points program drives volume.",
|
||||
"dYdX chain migration to Cosmos complete. V4 mainnet launches with 0.02s block times. DYDX token migration.",
|
||||
# Spec #2 bullish keywords expanded
|
||||
"Institutional adoption accelerates: Fortune 500 companies adding BTC to treasury",
|
||||
"ETF approval drives massive inflows: $10B in first month",
|
||||
"Golden cross confirmed on Bitcoin weekly chart, technical breakout",
|
||||
"Supply shock: Exchange balances hit 5-year low as holders accumulate",
|
||||
"Layer 2 adoption surges: Arbitrum and Optimism TVL doubles",
|
||||
"Real yield protocols attract TradFi capital seeking returns",
|
||||
"Token unlock schedule favorable: Low float, high demand dynamics",
|
||||
"Major partnership: TradFi giant integrates blockchain settlement",
|
||||
"Sovereign wealth fund announces Bitcoin allocation",
|
||||
"Hash rate hits all-time high, mining investment surges",
|
||||
"Developer activity reaches record highs across major ecosystems",
|
||||
"Stablecoin supply grows 50% YoY, indicating fresh capital entry",
|
||||
"Options market signaling upside: Call skew at multi-year highs",
|
||||
"Macro tailwinds: Rate cuts expected, dollar weakening",
|
||||
]
|
||||
|
||||
# Neutral samples
|
||||
neutral_texts = [
|
||||
# From labeled verified data
|
||||
"Ethereum Dencun upgrade goes live on mainnet. Proto-Danksharding (EIP-4844) activates, reducing L2 transaction fees by 90%.",
|
||||
"SEC approves spot Bitcoin ETFs for 11 issuers including BlackRock, Fidelity, ARK. Trading begins Thursday.",
|
||||
"Coinbase lists Pepe (PEPE) and Bonk (BONK) memecoins. Trading opens with 100x volume spike.",
|
||||
"Ethereum Pectra upgrade activated. EIP-7702 account abstraction live. EOAs can now batch transactions.",
|
||||
# Spec #2 neutral/descriptive keywords
|
||||
"Bitcoin consolidates in tight range between $50k-$52k",
|
||||
"Ethereum gas fees stable at 15 gwei amid low activity",
|
||||
"Market awaits FOMC decision, volumes below average",
|
||||
"Trading range established: Support at $48k, resistance at $55k",
|
||||
"Altcoin season index neutral at 50, no clear trend",
|
||||
"Funding rates flat across perpetual futures markets",
|
||||
"On-chain metrics show equilibrium: Inflows match outflows",
|
||||
"Derivatives open interest stable, no excessive leverage",
|
||||
"Stablecoin market cap flat month-over-month",
|
||||
"Developer conference announces roadmap, no token news",
|
||||
"Governance proposal passes: Parameter change only, no value accrual",
|
||||
"Exchange lists new token, volume modest, no price impact",
|
||||
"Research report: Fair value estimate $55k-$65k range",
|
||||
"Whale wallet rotates positions, no net accumulation or distribution",
|
||||
]
|
||||
|
||||
# Build training data
|
||||
texts = []
|
||||
labels = []
|
||||
|
||||
for t in bearish_texts:
|
||||
texts.append(t); labels.append(0)
|
||||
for t in bullish_texts:
|
||||
texts.append(t); labels.append(1)
|
||||
for t in neutral_texts:
|
||||
texts.append(t); labels.append(2)
|
||||
|
||||
return texts, labels
|
||||
|
||||
class SentimentDataset(Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=128):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = tokenizer
|
||||
self.max_len = max_len
|
||||
|
||||
def __len__(self):
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
text = self.texts[idx]
|
||||
label = self.labels[idx]
|
||||
|
||||
encoding = self.tokenizer(
|
||||
text,
|
||||
truncation=True,
|
||||
max_length=self.max_len,
|
||||
padding="max_length",
|
||||
return_tensors="pt"
|
||||
)
|
||||
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"token_type_ids": encoding.get("token_type_ids", torch.zeros_like(encoding["input_ids"])).squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
def compute_metrics(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return {
|
||||
"accuracy": accuracy_score(labels, predictions),
|
||||
"f1_macro": f1_score(labels, predictions, average="macro"),
|
||||
"f1_per_class": f1_score(labels, predictions, average=None).tolist()
|
||||
}
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("IMPROVING SENTIMENT MODEL - EXPANDED TRAINING")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load verified labeled data
|
||||
print("\n1. Loading verified labeled data...")
|
||||
verified_texts, verified_labels = load_labeled_data("data/labeled_verified.jsonl")
|
||||
print(f" Verified samples: {len(verified_texts)}")
|
||||
|
||||
# 2. Build augmented data from Spec #2
|
||||
print("\n2. Building augmented training data from Spec #2...")
|
||||
aug_texts, aug_labels = build_augmented_data()
|
||||
print(f" Augmented samples: {len(aug_texts)}")
|
||||
|
||||
# 3. Combine (weight verified data higher by duplicating)
|
||||
all_texts = verified_texts * 3 + aug_texts # 3x weight for verified
|
||||
all_labels = verified_labels * 3 + aug_labels
|
||||
|
||||
print(f"\n3. Total training samples: {len(all_texts)}")
|
||||
print(f" Bearish: {all_labels.count(0)}, Bullish: {all_labels.count(1)}, Neutral: {all_labels.count(2)}")
|
||||
|
||||
# 4. Train/val/test split
|
||||
train_texts, temp_texts, train_labels, temp_labels = train_test_split(
|
||||
all_texts, all_labels, test_size=0.3, random_state=42, stratify=all_labels
|
||||
)
|
||||
val_texts, test_texts, val_labels, test_labels = train_test_split(
|
||||
temp_texts, temp_labels, test_size=0.5, random_state=42, stratify=temp_labels
|
||||
)
|
||||
|
||||
print(f" Train: {len(train_texts)}, Val: {len(val_texts)}, Test: {len(test_texts)}")
|
||||
|
||||
# 5. Load tokenizer and model
|
||||
print("\n4. Loading model...")
|
||||
tokenizer = AutoTokenizer.from_pretrained("models/finbert-crypto-sentiment")
|
||||
model = AutoModelForSequenceClassification.from_pretrained("models/finbert-crypto-sentiment")
|
||||
|
||||
# 6. Create datasets
|
||||
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, max_len=128)
|
||||
val_dataset = SentimentDataset(val_texts, val_labels, tokenizer, max_len=128)
|
||||
|
||||
# 7. Class weights for balanced training
|
||||
class_weights = compute_class_weight("balanced", classes=np.array([0,1,2]), y=np.array(train_labels))
|
||||
class_weights = torch.tensor(class_weights, dtype=torch.float)
|
||||
print(f" Class weights: {class_weights}")
|
||||
|
||||
# 8. Training arguments
|
||||
training_args = TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment-v2",
|
||||
num_train_epochs=4,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=1e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
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,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
weight_decay=0.01,
|
||||
)
|
||||
|
||||
# Custom trainer with class weights
|
||||
class WeightedTrainer(Trainer):
|
||||
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
||||
labels = inputs.get("labels")
|
||||
outputs = model(**inputs)
|
||||
logits = outputs.get("logits")
|
||||
loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights.to(logits.device))
|
||||
loss = loss_fct(logits.view(-1, 3), labels.view(-1))
|
||||
return (loss, outputs) if return_outputs else loss
|
||||
|
||||
trainer = WeightedTrainer(
|
||||
model=model,
|
||||
args=training_args,
|
||||
train_dataset=train_dataset,
|
||||
eval_dataset=val_dataset,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics,
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=2)]
|
||||
)
|
||||
|
||||
print("\n5. Training (4 epochs)...")
|
||||
trainer.train()
|
||||
|
||||
# 9. Evaluate on test set
|
||||
print("\n6. Evaluating on test set...")
|
||||
test_dataset = SentimentDataset(test_texts, test_labels, tokenizer, max_len=128)
|
||||
test_results = trainer.evaluate(test_dataset)
|
||||
print(f" Test results: {test_results}")
|
||||
|
||||
# 10. Save best model
|
||||
print("\n7. Saving improved model...")
|
||||
model.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
tokenizer.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
print(" ✅ Model saved to models/finbert-crypto-sentiment/")
|
||||
|
||||
# 11. Quick inference test
|
||||
print("\n8. Quick inference test...")
|
||||
model.eval()
|
||||
test_cases = [
|
||||
("Bitcoin surges to $108k as institutional inflows surge", 1), # Bullish
|
||||
("Bitcoin crashes 50% in hours, massive selloff", 0), # Bearish
|
||||
("BTC at $50k, ETH at $3k, market consolidating", 2), # Neutral
|
||||
]
|
||||
|
||||
for text, expected in test_cases:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
probs = torch.softmax(outputs.logits, dim=-1).numpy()[0]
|
||||
pred = np.argmax(probs)
|
||||
polarity = probs[2] - probs[0]
|
||||
print(f" '{text[:50]}...' -> {SENTIMENT_LABELS[pred]} (polarity={polarity:.2f}) expected={SENTIMENT_LABELS[expected]}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("SENTIMENT MODEL IMPROVEMENT COMPLETE!")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,314 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Retrain sentiment model for CRYPTO - flip Bearish/Bullish to match crypto semantics.
|
||||
FinBERT native: 0=negative, 1=neutral, 2=positive
|
||||
Crypto mapping: "surge/moon/pump" -> Bullish(2), "crash/dump/rug" -> Bearish(0)
|
||||
"""
|
||||
|
||||
import json
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
|
||||
# CRYPTO label mapping - matches how crypto traders think
|
||||
# 0 = Bearish (price down, crash, dump, hack, rug)
|
||||
# 1 = Neutral (consolidation, upgrade, listing, regulatory)
|
||||
# 2 = Bullish (price up, surge, pump, moon, inflow, adoption)
|
||||
SENTIMENT_LABELS = ["Bearish", "Neutral", "Bullish"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Neutral": 1, "Bullish": 2}
|
||||
|
||||
def load_labeled_data(label_file: str):
|
||||
"""Load verified labeled data - FLIP bearish/bullish for crypto"""
|
||||
texts, labels = [], []
|
||||
with open(label_file) as f:
|
||||
for line in f:
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
texts.append(r['text'])
|
||||
orig_label = r['labels']['sentiment']
|
||||
# FinBERT labeled these with traditional finance labels
|
||||
# For crypto, we need to FLIP: Bearish<->Bullish
|
||||
if orig_label == "Bearish":
|
||||
labels.append(2) # Flip to Bullish (FinBERT's positive)
|
||||
elif orig_label == "Bullish":
|
||||
labels.append(0) # Flip to Bearish (FinBERT's negative)
|
||||
else:
|
||||
labels.append(1) # Neutral stays Neutral
|
||||
return texts, labels
|
||||
|
||||
def build_augmented_data():
|
||||
"""Build training data with CRYPTO semantics"""
|
||||
|
||||
# CRYPTO BEARISH (label 0) - price down, bad news
|
||||
bearish_texts = [
|
||||
"Bitcoin crashes 30% in hours as leverage flushes out longs",
|
||||
"Massive liquidation cascade wipes out $500M in longs across exchanges",
|
||||
"Exchange hacked, $100M stolen, users panic selling",
|
||||
"Regulatory crackdown: SEC files enforcement action against major DeFi protocol",
|
||||
"Rug pull: Dev team abandons project, drains liquidity pool",
|
||||
"Bankruptcy filing: Major crypto lender files Chapter 11",
|
||||
"Stablecoin depeg: USDT drops to $0.95 on redemption fears",
|
||||
"Smart contract vulnerability discovered, $50M at risk",
|
||||
"Market structure breakdown: Order books thin, spreads widen",
|
||||
"Forced liquidations trigger death spiral in lending protocol",
|
||||
"Contagion risk: Major fund exposure to failed protocol revealed",
|
||||
"Bear market confirmed: Lower highs, lower lows on weekly chart",
|
||||
"Institutional outflows: ETF sees record redemptions for 5th week",
|
||||
"Mining capitulation: Hash rate drops 20% as price falls below cost",
|
||||
"Major hack: Radiant Capital loses $50M in exploit. Funds moved to Tornado Cash.",
|
||||
"Curve Finance hit by $50M exploit. Vyper compiler bug. CRV drops 20%.",
|
||||
"Wintermute market maker loses $20M in exploit. Funds returned.",
|
||||
"SEC sues Kraken for operating unregistered securities exchange.",
|
||||
"SEC charges Uniswap Labs. UNI drops 15%.",
|
||||
"Binance delists Monero, Zcash, and 4 other privacy coins.",
|
||||
"OKX delists USDT trading pairs in EEA region. MiCA compliance.",
|
||||
"Solana network experiences 5-hour outage. SOL drops 8% on news.",
|
||||
"Circle USDC depegs to $0.97 after SVB exposure. $3.3B reserves stuck.",
|
||||
]
|
||||
|
||||
# CRYPTO BULLISH (label 2) - price up, good news
|
||||
bullish_texts = [
|
||||
"Bitcoin surges to $108k as institutional inflows surge. BlackRock IBIT sees record $1.2B daily inflow.",
|
||||
"Bitcoin ETF inflows hit record $2.1B in single week. Cumulative AUM passes $50B.",
|
||||
"Bitcoin hits $100,000 for first time ever. MicroStrategy, ETFs, sovereign buying drive rally.",
|
||||
"Pump.fun revenue hits $100M in 30 days. Memecoin factory launches 50k tokens/day. SOL fees surge.",
|
||||
"MicroStrategy buys additional 12,000 BTC at $61M. Total holdings now 190,000 BTC.",
|
||||
"Institutional adoption accelerates: Fortune 500 companies adding BTC to treasury",
|
||||
"ETF approval drives massive inflows: $10B in first month",
|
||||
"Golden cross confirmed on Bitcoin weekly chart, technical breakout",
|
||||
"Supply shock: Exchange balances hit 5-year low as holders accumulate",
|
||||
"Layer 2 adoption surges: Arbitrum and Optimism TVL doubles",
|
||||
"Real yield protocols attract TradFi capital seeking returns",
|
||||
"Token unlock schedule favorable: Low float, high demand dynamics",
|
||||
"Major partnership: TradFi giant integrates blockchain settlement",
|
||||
"Sovereign wealth fund announces Bitcoin allocation",
|
||||
"Hash rate hits all-time high, mining investment surges",
|
||||
"Developer activity reaches record highs across major ecosystems",
|
||||
"Stablecoin supply grows 50% YoY, indicating fresh capital entry",
|
||||
"Options market signaling upside: Call skew at multi-year highs",
|
||||
"Macro tailwinds: Rate cuts expected, dollar weakening",
|
||||
]
|
||||
|
||||
# CRYPTO NEUTRAL (label 1) - sideways, structural, non-directional
|
||||
neutral_texts = [
|
||||
"Bitcoin consolidates in tight range between $50k-$52k",
|
||||
"Ethereum gas fees stable at 15 gwei amid low activity",
|
||||
"Market awaits FOMC decision, volumes below average",
|
||||
"Trading range established: Support at $48k, resistance at $55k",
|
||||
"Altcoin season index neutral at 50, no clear trend",
|
||||
"Funding rates flat across perpetual futures markets",
|
||||
"On-chain metrics show equilibrium: Inflows match outflows",
|
||||
"Derivatives open interest stable, no excessive leverage",
|
||||
"Stablecoin market cap flat month-over-month",
|
||||
"Developer conference announces roadmap, no token news",
|
||||
"Governance proposal passes: Parameter change only, no value accrual",
|
||||
"Exchange lists new token, volume modest, no price impact",
|
||||
"Research report: Fair value estimate $55k-$65k range",
|
||||
"Whale wallet rotates positions, no net accumulation or distribution",
|
||||
"Ethereum Dencun upgrade goes live. Proto-Danksharding reduces L2 fees by 90%.",
|
||||
"SEC approves spot Bitcoin ETFs for 11 issuers. Trading begins Thursday.",
|
||||
"Coinbase lists Pepe and Bonk memecoins. Trading opens with 100x volume spike.",
|
||||
"Ethereum Pectra upgrade activated. EIP-7702 account abstraction live.",
|
||||
"Arbitrum DAO approves $200M ARB grant program. Voting passes with 92%.",
|
||||
"EigenLayer restaking TVL hits $20B. Points season 2 announced.",
|
||||
"Hyperliquid DEX launches HYPE token airdrop. $1.2B TVL locked.",
|
||||
"dYdX chain migration to Cosmos complete. V4 mainnet launches.",
|
||||
]
|
||||
|
||||
texts = []
|
||||
labels = []
|
||||
|
||||
for t in bearish_texts:
|
||||
texts.append(t); labels.append(0) # Bearish = 0
|
||||
for t in bullish_texts:
|
||||
texts.append(t); labels.append(2) # Bullish = 2
|
||||
for t in neutral_texts:
|
||||
texts.append(t); labels.append(1) # Neutral = 1
|
||||
|
||||
return texts, labels
|
||||
|
||||
class SentimentDataset(Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=128):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = tokenizer
|
||||
self.max_len = max_len
|
||||
|
||||
def __len__(self):
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
text = self.texts[idx]
|
||||
label = self.labels[idx]
|
||||
|
||||
encoding = self.tokenizer(
|
||||
text,
|
||||
truncation=True,
|
||||
max_length=self.max_len,
|
||||
padding="max_length",
|
||||
return_tensors="pt"
|
||||
)
|
||||
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"token_type_ids": encoding.get("token_type_ids", torch.zeros_like(encoding["input_ids"])).squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
def compute_metrics(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return {
|
||||
"accuracy": accuracy_score(labels, predictions),
|
||||
"f1_macro": f1_score(labels, predictions, average="macro"),
|
||||
"f1_per_class": f1_score(labels, predictions, average=None).tolist()
|
||||
}
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("RETRAINING SENTIMENT FOR CRYPTO SEMANTICS")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load verified labeled data (FLIPPED)
|
||||
print("\n1. Loading verified labeled data (with label flip)...")
|
||||
verified_texts, verified_labels = load_labeled_data("data/labeled_verified.jsonl")
|
||||
print(f" Verified samples: {len(verified_texts)}")
|
||||
|
||||
# 2. Build augmented data with crypto semantics
|
||||
print("\n2. Building augmented training data (crypto semantics)...")
|
||||
aug_texts, aug_labels = build_augmented_data()
|
||||
print(f" Augmented samples: {len(aug_texts)}")
|
||||
|
||||
# 3. Combine (weight verified 3x)
|
||||
all_texts = verified_texts * 3 + aug_texts
|
||||
all_labels = verified_labels * 3 + aug_labels
|
||||
|
||||
print(f"\n3. Total training samples: {len(all_texts)}")
|
||||
print(f" Bearish(0): {all_labels.count(0)}, Neutral(1): {all_labels.count(1)}, Bullish(2): {all_labels.count(2)}")
|
||||
|
||||
# 4. Train/val/test split
|
||||
train_texts, temp_texts, train_labels, temp_labels = train_test_split(
|
||||
all_texts, all_labels, test_size=0.3, random_state=42, stratify=all_labels
|
||||
)
|
||||
val_texts, test_texts, val_labels, test_labels = train_test_split(
|
||||
temp_texts, temp_labels, test_size=0.5, random_state=42, stratify=temp_labels
|
||||
)
|
||||
|
||||
print(f" Train: {len(train_texts)}, Val: {len(val_texts)}, Test: {len(test_texts)}")
|
||||
|
||||
# 5. Load BASE FinBERT
|
||||
print("\n4. Loading BASE FinBERT...")
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert",
|
||||
num_labels=3,
|
||||
id2label={0: "Bearish", 1: "Neutral", 2: "Bullish"},
|
||||
label2id={"Bearish": 0, "Neutral": 1, "Bullish": 2}
|
||||
)
|
||||
|
||||
# 6. Create datasets
|
||||
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, max_len=128)
|
||||
val_dataset = SentimentDataset(val_texts, val_labels, tokenizer, max_len=128)
|
||||
|
||||
# 7. Class weights - heavily weight Bearish since it's hardest
|
||||
class_weights = compute_class_weight("balanced", classes=np.array([0,1,2]), y=np.array(train_labels))
|
||||
class_weights = torch.tensor(class_weights, dtype=torch.float)
|
||||
# Boost Bearish weight further
|
||||
class_weights[0] *= 2.0
|
||||
print(f" Class weights: {class_weights}")
|
||||
|
||||
# 8. Training arguments
|
||||
training_args = TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment-v3",
|
||||
num_train_epochs=6,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=1e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
load_best_model_at_end=True,
|
||||
metric_for_best_model="f1_macro",
|
||||
greater_is_better=True,
|
||||
fp16=False,
|
||||
dataloader_num_workers=0,
|
||||
logging_steps=5,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
weight_decay=0.01,
|
||||
)
|
||||
|
||||
class WeightedTrainer(Trainer):
|
||||
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
||||
labels = inputs.get("labels")
|
||||
outputs = model(**inputs)
|
||||
logits = outputs.get("logits")
|
||||
loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights.to(logits.device))
|
||||
loss = loss_fct(logits.view(-1, 3), labels.view(-1))
|
||||
return (loss, outputs) if return_outputs else loss
|
||||
|
||||
trainer = WeightedTrainer(
|
||||
model=model,
|
||||
args=training_args,
|
||||
train_dataset=train_dataset,
|
||||
eval_dataset=val_dataset,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics,
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=3)]
|
||||
)
|
||||
|
||||
print("\n5. Training (6 epochs)...")
|
||||
trainer.train()
|
||||
|
||||
# 9. Evaluate on test set
|
||||
print("\n6. Evaluating on test set...")
|
||||
test_dataset = SentimentDataset(test_texts, test_labels, tokenizer, max_len=128)
|
||||
test_results = trainer.evaluate(test_dataset)
|
||||
print(f" Test results: {test_results}")
|
||||
|
||||
# 10. Save best model
|
||||
print("\n7. Saving improved model...")
|
||||
model.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
tokenizer.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
print(" ✅ Model saved to models/finbert-crypto-sentiment/")
|
||||
|
||||
# 11. Quick inference test
|
||||
print("\n8. Quick inference test...")
|
||||
model.eval()
|
||||
test_cases = [
|
||||
("Bitcoin surges to $108k as institutional inflows surge", 2),
|
||||
("Bitcoin crashes 50% in hours, massive selloff", 0),
|
||||
("BTC at $50k, ETH at $3k, market consolidating", 1),
|
||||
("Major hack on exchange, $100M stolen, panic selling", 0),
|
||||
("ETF approval drives massive inflows, price to moon", 2),
|
||||
("Market consolidating in tight range, no clear direction", 1),
|
||||
]
|
||||
|
||||
for text, expected in test_cases:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
probs = torch.softmax(outputs.logits, dim=-1).numpy()[0]
|
||||
pred = np.argmax(probs)
|
||||
polarity = probs[2] - probs[0]
|
||||
print(f" '{text[:50]}...' -> {SENTIMENT_LABELS[pred]} (polarity={polarity:.2f}) expected={SENTIMENT_LABELS[expected]}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("CRYPTO SENTIMENT MODEL TRAINING COMPLETE!")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,301 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Retrain sentiment model from BASE FinBERT with CORRECT label mapping.
|
||||
FinBERT native: 0=negative(Bearish), 1=neutral(Neutral), 2=positive(Bullish)
|
||||
"""
|
||||
|
||||
import json
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
|
||||
# CORRECT label mapping matching FinBERT's native order
|
||||
# FinBERT: 0=negative, 1=neutral, 2=positive
|
||||
SENTIMENT_LABELS = ["Bearish", "Neutral", "Bullish"] # Order matches FinBERT!
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Neutral": 1, "Bullish": 2}
|
||||
|
||||
def load_labeled_data(label_file: str):
|
||||
"""Load verified labeled data from labeling pipeline"""
|
||||
texts, labels = [], []
|
||||
with open(label_file) as f:
|
||||
for line in f:
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
texts.append(r['text'])
|
||||
labels.append(SENTIMENT_MAP[r['labels']['sentiment']])
|
||||
return texts, labels
|
||||
|
||||
def build_augmented_data():
|
||||
"""Build comprehensive training data"""
|
||||
|
||||
bearish_texts = [
|
||||
# From labeled verified data
|
||||
"Major hack: Radiant Capital loses $50M in exploit. Attacker exploits rounding error in lending market. Funds moved to Tornado Cash.",
|
||||
"Curve Finance hit by $50M exploit. Vyper compiler bug affects multiple pools. CRV drops 20%.",
|
||||
"Wintermute market maker loses $20M in exploit. Private key compromise suspected. Funds returned.",
|
||||
"SEC sues Kraken for operating unregistered securities exchange. Alleged commingling of customer funds.",
|
||||
"SEC charges Uniswap Labs with operating unregistered securities exchange. UNI drops 15%.",
|
||||
"Binance delists Monero (XMR), Zcash (ZEC), and 4 other privacy coins. Cites regulatory compliance review.",
|
||||
"OKX delists USDT trading pairs in EEA region. MiCA compliance cited. USDT/USD pairs remain.",
|
||||
"Solana network experiences 5-hour outage. Validators restart cluster. SOL drops 8% on news.",
|
||||
"Circle USDC depegs to $0.97 after SVB exposure revealed. $3.3B reserves stuck at SVB.",
|
||||
# Expanded bearish
|
||||
"Bitcoin crashes 30% in hours as leverage flushes out longs",
|
||||
"Massive liquidation cascade wipes out $500M in longs across exchanges",
|
||||
"Exchange hacked, $100M stolen, users panic selling",
|
||||
"Regulatory crackdown: SEC files enforcement action against major DeFi protocol",
|
||||
"Rug pull: Dev team abandons project, drains liquidity pool",
|
||||
"Bankruptcy filing: Major crypto lender files Chapter 11",
|
||||
"Stablecoin depeg: USDT drops to $0.95 on redemption fears",
|
||||
"Smart contract vulnerability discovered, $50M at risk",
|
||||
"Market structure breakdown: Order books thin, spreads widen",
|
||||
"Forced liquidations trigger death spiral in lending protocol",
|
||||
"Contagion risk: Major fund exposure to failed protocol revealed",
|
||||
"Bear market confirmed: Lower highs, lower lows on weekly chart",
|
||||
"Institutional outflows: ETF sees record redemptions for 5th week",
|
||||
"Mining capitulation: Hash rate drops 20% as price falls below cost",
|
||||
]
|
||||
|
||||
bullish_texts = [
|
||||
# From labeled verified data
|
||||
"Bitcoin hits new all-time high of $108,000 as institutional inflows surge. BlackRock IBIT ETF sees record $1.2B daily inflow.",
|
||||
"Bitcoin ETF inflows hit record $2.1B in single week. IBIT alone sees $1.2B. Cumulative AUM passes $50B.",
|
||||
"Bitcoin hits $100,000 for first time ever. MicroStrategy, ETFs, and sovereign buying drive rally.",
|
||||
"Pump.fun revenue hits $100M in 30 days. Memecoin factory launches 50k tokens/day. SOL fees surge.",
|
||||
"MicroStrategy buys additional 12,000 BTC at $61M. Total holdings now 190,000 BTC. Stock MSTR up 15% premarket.",
|
||||
"Arbitrum DAO approves $200M ARB grant program for gaming ecosystem. Voting passes with 92% approval.",
|
||||
"EigenLayer restaking TVL hits $20B. ETH restaking becomes largest DeFi category. Points season 2 announced.",
|
||||
"Hyperliquid DEX launches HYPE token airdrop. $1.2B TVL locked. Points program drives volume.",
|
||||
"dYdX chain migration to Cosmos complete. V4 mainnet launches with 0.02s block times. DYDX token migration.",
|
||||
# Expanded bullish
|
||||
"Institutional adoption accelerates: Fortune 500 companies adding BTC to treasury",
|
||||
"ETF approval drives massive inflows: $10B in first month",
|
||||
"Golden cross confirmed on Bitcoin weekly chart, technical breakout",
|
||||
"Supply shock: Exchange balances hit 5-year low as holders accumulate",
|
||||
"Layer 2 adoption surges: Arbitrum and Optimism TVL doubles",
|
||||
"Real yield protocols attract TradFi capital seeking returns",
|
||||
"Token unlock schedule favorable: Low float, high demand dynamics",
|
||||
"Major partnership: TradFi giant integrates blockchain settlement",
|
||||
"Sovereign wealth fund announces Bitcoin allocation",
|
||||
"Hash rate hits all-time high, mining investment surges",
|
||||
"Developer activity reaches record highs across major ecosystems",
|
||||
"Stablecoin supply grows 50% YoY, indicating fresh capital entry",
|
||||
"Options market signaling upside: Call skew at multi-year highs",
|
||||
"Macro tailwinds: Rate cuts expected, dollar weakening",
|
||||
]
|
||||
|
||||
neutral_texts = [
|
||||
# From labeled verified data
|
||||
"Ethereum Dencun upgrade goes live on mainnet. Proto-Danksharding (EIP-4844) activates, reducing L2 transaction fees by 90%.",
|
||||
"SEC approves spot Bitcoin ETFs for 11 issuers including BlackRock, Fidelity, ARK. Trading begins Thursday.",
|
||||
"Coinbase lists Pepe (PEPE) and Bonk (BONK) memecoins. Trading opens with 100x volume spike.",
|
||||
"Ethereum Pectra upgrade activated. EIP-7702 account abstraction live. EOAs can now batch transactions.",
|
||||
# Expanded neutral
|
||||
"Bitcoin consolidates in tight range between $50k-$52k",
|
||||
"Ethereum gas fees stable at 15 gwei amid low activity",
|
||||
"Market awaits FOMC decision, volumes below average",
|
||||
"Trading range established: Support at $48k, resistance at $55k",
|
||||
"Altcoin season index neutral at 50, no clear trend",
|
||||
"Funding rates flat across perpetual futures markets",
|
||||
"On-chain metrics show equilibrium: Inflows match outflows",
|
||||
"Derivatives open interest stable, no excessive leverage",
|
||||
"Stablecoin market cap flat month-over-month",
|
||||
"Developer conference announces roadmap, no token news",
|
||||
"Governance proposal passes: Parameter change only, no value accrual",
|
||||
"Exchange lists new token, volume modest, no price impact",
|
||||
"Research report: Fair value estimate $55k-$65k range",
|
||||
"Whale wallet rotates positions, no net accumulation or distribution",
|
||||
]
|
||||
|
||||
texts = []
|
||||
labels = []
|
||||
|
||||
for t in bearish_texts:
|
||||
texts.append(t); labels.append(0) # Bearish = 0
|
||||
for t in bullish_texts:
|
||||
texts.append(t); labels.append(2) # Bullish = 2 (positive in FinBERT)
|
||||
for t in neutral_texts:
|
||||
texts.append(t); labels.append(1) # Neutral = 1
|
||||
|
||||
return texts, labels
|
||||
|
||||
class SentimentDataset(Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=128):
|
||||
self.texts = texts
|
||||
self.labels = labels
|
||||
self.tokenizer = tokenizer
|
||||
self.max_len = max_len
|
||||
|
||||
def __len__(self):
|
||||
return len(self.texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
text = self.texts[idx]
|
||||
label = self.labels[idx]
|
||||
|
||||
encoding = self.tokenizer(
|
||||
text,
|
||||
truncation=True,
|
||||
max_length=self.max_len,
|
||||
padding="max_length",
|
||||
return_tensors="pt"
|
||||
)
|
||||
|
||||
return {
|
||||
"input_ids": encoding["input_ids"].squeeze(0),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(0),
|
||||
"token_type_ids": encoding.get("token_type_ids", torch.zeros_like(encoding["input_ids"])).squeeze(0),
|
||||
"labels": torch.tensor(label, dtype=torch.long)
|
||||
}
|
||||
|
||||
def compute_metrics(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return {
|
||||
"accuracy": accuracy_score(labels, predictions),
|
||||
"f1_macro": f1_score(labels, predictions, average="macro"),
|
||||
"f1_per_class": f1_score(labels, predictions, average=None).tolist()
|
||||
}
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("RETRAINING SENTIMENT FROM BASE FINBERT - CORRECT LABELS")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load verified labeled data
|
||||
print("\n1. Loading verified labeled data...")
|
||||
verified_texts, verified_labels = load_labeled_data("data/labeled_verified.jsonl")
|
||||
print(f" Verified samples: {len(verified_texts)}")
|
||||
|
||||
# 2. Build augmented data
|
||||
print("\n2. Building augmented training data...")
|
||||
aug_texts, aug_labels = build_augmented_data()
|
||||
print(f" Augmented samples: {len(aug_texts)}")
|
||||
|
||||
# 3. Combine (weight verified 3x)
|
||||
all_texts = verified_texts * 3 + aug_texts
|
||||
all_labels = verified_labels * 3 + aug_labels
|
||||
|
||||
print(f"\n3. Total training samples: {len(all_texts)}")
|
||||
print(f" Bearish(0): {all_labels.count(0)}, Neutral(1): {all_labels.count(1)}, Bullish(2): {all_labels.count(2)}")
|
||||
|
||||
# 4. Train/val/test split
|
||||
train_texts, temp_texts, train_labels, temp_labels = train_test_split(
|
||||
all_texts, all_labels, test_size=0.3, random_state=42, stratify=all_labels
|
||||
)
|
||||
val_texts, test_texts, val_labels, test_labels = train_test_split(
|
||||
temp_texts, temp_labels, test_size=0.5, random_state=42, stratify=temp_labels
|
||||
)
|
||||
|
||||
print(f" Train: {len(train_texts)}, Val: {len(val_texts)}, Test: {len(test_texts)}")
|
||||
|
||||
# 5. Load BASE FinBERT (not fine-tuned)
|
||||
print("\n4. Loading BASE FinBERT...")
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert",
|
||||
num_labels=3,
|
||||
id2label={0: "Bearish", 1: "Neutral", 2: "Bullish"},
|
||||
label2id={"Bearish": 0, "Neutral": 1, "Bullish": 2}
|
||||
)
|
||||
|
||||
# 6. Create datasets
|
||||
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, max_len=128)
|
||||
val_dataset = SentimentDataset(val_texts, val_labels, tokenizer, max_len=128)
|
||||
|
||||
# 7. Class weights
|
||||
class_weights = compute_class_weight("balanced", classes=np.array([0,1,2]), y=np.array(train_labels))
|
||||
class_weights = torch.tensor(class_weights, dtype=torch.float)
|
||||
print(f" Class weights: {class_weights}")
|
||||
|
||||
# 8. Training arguments
|
||||
training_args = TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment-v2",
|
||||
num_train_epochs=5,
|
||||
per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16,
|
||||
gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1,
|
||||
learning_rate=2e-5,
|
||||
lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch",
|
||||
save_strategy="epoch",
|
||||
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,
|
||||
save_total_limit=2,
|
||||
remove_unused_columns=False,
|
||||
report_to="none",
|
||||
weight_decay=0.01,
|
||||
)
|
||||
|
||||
class WeightedTrainer(Trainer):
|
||||
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
||||
labels = inputs.get("labels")
|
||||
outputs = model(**inputs)
|
||||
logits = outputs.get("logits")
|
||||
loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights.to(logits.device))
|
||||
loss = loss_fct(logits.view(-1, 3), labels.view(-1))
|
||||
return (loss, outputs) if return_outputs else loss
|
||||
|
||||
trainer = WeightedTrainer(
|
||||
model=model,
|
||||
args=training_args,
|
||||
train_dataset=train_dataset,
|
||||
eval_dataset=val_dataset,
|
||||
tokenizer=tokenizer,
|
||||
compute_metrics=compute_metrics,
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=2)]
|
||||
)
|
||||
|
||||
print("\n5. Training (5 epochs)...")
|
||||
trainer.train()
|
||||
|
||||
# 9. Evaluate on test set
|
||||
print("\n6. Evaluating on test set...")
|
||||
test_dataset = SentimentDataset(test_texts, test_labels, tokenizer, max_len=128)
|
||||
test_results = trainer.evaluate(test_dataset)
|
||||
print(f" Test results: {test_results}")
|
||||
|
||||
# 10. Save best model
|
||||
print("\n7. Saving improved model...")
|
||||
model.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
tokenizer.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
print(" ✅ Model saved to models/finbert-crypto-sentiment/")
|
||||
|
||||
# 11. Quick inference test
|
||||
print("\n8. Quick inference test...")
|
||||
model.eval()
|
||||
test_cases = [
|
||||
("Bitcoin surges to $108k as institutional inflows surge", 2), # Bullish
|
||||
("Bitcoin crashes 50% in hours, massive selloff", 0), # Bearish
|
||||
("BTC at $50k, ETH at $3k, market consolidating", 1), # Neutral
|
||||
]
|
||||
|
||||
for text, expected in test_cases:
|
||||
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True)
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
probs = torch.softmax(outputs.logits, dim=-1).numpy()[0]
|
||||
pred = np.argmax(probs)
|
||||
polarity = probs[2] - probs[0] # Bullish - Bearish
|
||||
print(f" '{text[:50]}...' -> {SENTIMENT_LABELS[pred]} (polarity={polarity:.2f}) expected={SENTIMENT_LABELS[expected]}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("SENTIMENT MODEL RETRAINING COMPLETE!")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,620 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fine-tune all 3 models using the labeled data from labeling pipeline.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import (
|
||||
AutoTokenizer, AutoModelForSequenceClassification,
|
||||
TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
)
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import torch.nn as nn
|
||||
|
||||
# ============================================================
|
||||
# LABELS & CONSTANTS
|
||||
# ============================================================
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
REAL_EVENTS = [
|
||||
{"text": "XRP bridge drained for $200,000 after software mistook fake deposits for real ones. An attacker created unbacked XRP on another blockchain, then exchanged it for real XRP held in reserve. The bridge has been halted and its operator has filed a complaint with the FBI.", "label_id": 0, "event_type": "hack"},
|
||||
{"text": "Major hack on DeFi protocol drains $50M. Users panic as TVL collapses. Team promises investigation.", "label_id": 0, "event_type": "hack"},
|
||||
{"text": "KuCoin Lists Catizen (CATI) for Spot Trading on September 20, 2024.", "label_id": 1, "event_type": "listing"},
|
||||
{"text": "Bitfinex Among First Exchanges to List HMSTR, Native Token of Hamster Kombat.", "label_id": 1, "event_type": "listing"},
|
||||
{"text": "Binance Becomes First Exchange to List Trump-Linked WLFI Token.", "label_id": 1, "event_type": "listing"},
|
||||
{"text": "SEC files lawsuit against major exchange for unregistered securities.", "label_id": 0, "event_type": "regulatory"},
|
||||
{"text": "CFTC files to dismiss CME's lawsuit over crypto perpetual futures.", "label_id": 2, "event_type": "regulatory"},
|
||||
{"text": "Michigan court orders Kalshi to keep blocking sports prediction markets.", "label_id": 0, "event_type": "regulatory"},
|
||||
{"text": "Ethereum Dencun upgrade activates Proto-Danksharding (EIP-4844).", "label_id": 1, "event_type": "upgrade"},
|
||||
{"text": "Ethereum Shanghai upgrade goes live. Stakers can now withdraw.", "label_id": 1, "event_type": "upgrade"},
|
||||
{"text": "JPMorganChase and Coinbase Launch Strategic Partnership.", "label_id": 1, "event_type": "partnership"},
|
||||
{"text": "Chainlink and Mastercard Partner to Enable Over 3 Billion Cardholders.", "label_id": 1, "event_type": "partnership"},
|
||||
{"text": "PayPal and Coinbase Expand Partnership to Drive Innovation.", "label_id": 1, "event_type": "partnership"},
|
||||
{"text": "Bitcoin whale moves $116 million in BTC after 11-year dormancy.", "label_id": 2, "event_type": "whale"},
|
||||
{"text": "Ancient Bitcoin whale dormant for 11 years suddenly transfers $257,450,000 in BTC.", "label_id": 2, "event_type": "whale"},
|
||||
{"text": "$1B in Bitcoin moves from Satoshi-era wallet after 14 years of inactivity.", "label_id": 2, "event_type": "whale"},
|
||||
{"text": "Breaking: Fed pauses rate hikes. Bitcoin jumps 5% on dovish pivot.", "label_id": 1, "event_type": "macro"},
|
||||
{"text": "Surprise nonfarm payrolls print sends Bitcoin back below 80K.", "label_id": 0, "event_type": "macro"},
|
||||
{"text": "Massive liquidation cascade wipes out $200M in longs.", "label_id": 0, "event_type": "liquidation"},
|
||||
{"text": "Governance proposal passes with 95% approval.", "label_id": 1, "event_type": "governance"},
|
||||
{"text": "Bitcoin ETF inflows hit $731M, highest since January.", "label_id": 1, "event_type": "earnings"},
|
||||
{"text": "Coinbase Q2 earnings beat estimates. Revenue up 50% YoY.", "label_id": 1, "event_type": "earnings"},
|
||||
{"text": "FOMO drives memecoin 500% in 24h. Degens aping in. Rug pull inevitable?", "label_id": 0, "event_type": "manipulation"},
|
||||
{"text": "Token buybacks are booming. But are they good for crypto projects?", "label_id": 2, "event_type": "manipulation"},
|
||||
{"text": "Coinbase delists XRP after SEC lawsuit. Trading suspended.", "label_id": 0, "event_type": "delisting"},
|
||||
]
|
||||
|
||||
SENTIMENT_LABELS = ["Bearish", "Bullish", "Neutral"]
|
||||
SENTIMENT_MAP = {"Bearish": 0, "Bullish": 1, "Neutral": 2}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
EMOTION_LABELS = ["joy", "fear", "anger", "greed", "sadness", "neutral"]
|
||||
EMOTION_MAP = {l: i for i, l in enumerate(EMOTION_LABELS)}
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
EMOTION_SAMPLES = [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
EMOTION_SAMPLES = [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
EMOTION_SAMPLES = [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]
|
||||
|
||||
EVENT_LABELS = [
|
||||
"listing", "delisting", "hack", "regulatory", "governance",
|
||||
"upgrade", "partnership", "earnings", "macro",
|
||||
"liquidation", "whale", "manipulation"
|
||||
]
|
||||
EVENT_MAP = {l: i for i, l in enumerate(EVENT_LABELS)}
|
||||
|
||||
# ============================================================
|
||||
# LOAD LABELED DATA
|
||||
# ============================================================
|
||||
|
||||
def load_labeled_data(label_file):
|
||||
"""Load verified labeled data from JSONL file"""
|
||||
texts, labels = [], []
|
||||
with open(label_file) as f:
|
||||
for line in open(label_file):
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
texts.append(r['text'])
|
||||
labels.append(r['labels']['sentiment'])
|
||||
return texts, labels
|
||||
|
||||
def get_sentiment_data():
|
||||
texts, labels = [], []
|
||||
# Manual samples
|
||||
for text, label in [
|
||||
("BTC breaks $100k! New ATH!", 1), ("ETH to $10k by EOY, accumulate now", 1),
|
||||
("Institutional inflows hit record high", 1), ("Bitcoin reaches new all-time high", 1),
|
||||
("Ethereum merge successful, staking rewards now live", 1),
|
||||
("Massive ETF inflows drive Bitcoin to new highs", 1),
|
||||
("Golden cross confirmed on Bitcoin weekly chart", 1),
|
||||
("Institutional adoption drives Bitcoin higher", 1),
|
||||
("ETF approval drives massive inflows", 1), ("Market is bullish on Bitcoin", 1),
|
||||
("BTC crashes 50% in hours", 0), ("Exchange hacked, $100M stolen", 0),
|
||||
("SEC sues major exchange", 0), ("Bitcoin crashes hard, panic selling everywhere", 0),
|
||||
("Massive liquidation cascade wipes out $200M in longs", 0),
|
||||
("VIX drops below 15 as market volatility decreases", 0),
|
||||
("Whale sells 10000 BTC", 0), ("Bitcoin price drops 50%", 0),
|
||||
("Support broken with bearish structure", 0), ("Panic selling and forced liquidation", 0),
|
||||
("BTC at $50k, ETH at $3k", 2), ("Market consolidating in range", 2),
|
||||
("Bitcoin remains stable around $30k", 2), ("VIX drops below 15", 2),
|
||||
("Market consolidating with no clear direction", 2),
|
||||
("Bitcoin price stable around $30k", 2), ("Consolidation phase continues", 2),
|
||||
("Market in wait-and-see mode", 2), ("Sideways action continues", 2),
|
||||
("Low volatility environment persists", 2),
|
||||
]:
|
||||
yield t, l
|
||||
for event in REAL_EVENTS:
|
||||
yield event["text"], event["label_id"]
|
||||
|
||||
def get_event_data():
|
||||
texts, labels = [], []
|
||||
for event in REAL_EVENTS:
|
||||
lbls = [0]*12
|
||||
lbls[EVENT_MAP[event["event_type"]]] = 1
|
||||
yield event["text"], lbls
|
||||
|
||||
def get_emotion_data():
|
||||
texts, labels = [], []
|
||||
for text, labels in [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]:
|
||||
yield text, labels
|
||||
|
||||
def get_event_data():
|
||||
texts, labels = [], []
|
||||
for event in REAL_EVENTS:
|
||||
lbls = [0]*12
|
||||
lbls[EVENT_MAP[event["event_type"]]] = 1
|
||||
yield event["text"], lbls
|
||||
|
||||
def get_emotion_data():
|
||||
texts, labels = [], []
|
||||
for text, labels in [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]:
|
||||
yield text, labels
|
||||
|
||||
def get_event_data():
|
||||
texts, labels = [], []
|
||||
for event in REAL_EVENTS:
|
||||
lbls = [0]*12
|
||||
lbls[EVENT_MAP[event["event_type"]]] = 1
|
||||
yield event["text"], lbls
|
||||
|
||||
def get_emotion_data():
|
||||
texts, labels = [], []
|
||||
for text, labels in [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]:
|
||||
yield text, labels
|
||||
|
||||
# ============================================================
|
||||
# LOAD LABELED DATA FROM LABELING PIPELINE
|
||||
# ============================================================
|
||||
|
||||
def load_labeled_data():
|
||||
"""Load all verified labeled data from labeling pipeline outputs"""
|
||||
sentiment_texts, sentiment_labels = [], []
|
||||
event_texts, event_labels = [], []
|
||||
emotion_texts, emotion_labels = [], []
|
||||
|
||||
# Load from labeled_output.jsonl
|
||||
for label_file in ['data/labeled_output.jsonl', 'data/labeled_large.jsonl', 'data/labeled_large.jsonl']:
|
||||
try:
|
||||
with open(label_file) as f:
|
||||
for line in open(label_file):
|
||||
r = json.loads(line)
|
||||
if r.get('verified', False):
|
||||
# Sentiment
|
||||
texts.append(r['text'])
|
||||
labels.append(r['labels']['sentiment'])
|
||||
except:
|
||||
pass
|
||||
|
||||
return texts, labels
|
||||
|
||||
# ============================================================
|
||||
# DATASET CLASS
|
||||
# ============================================================
|
||||
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = tokenizer; self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
lbl = self.labels[i]
|
||||
if isinstance(lbl, list):
|
||||
lbl = torch.tensor(lbl, dtype=torch.float)
|
||||
else:
|
||||
lbl = torch.tensor(lbl, dtype=torch.long)
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": lbl}
|
||||
|
||||
|
||||
def train_sentiment():
|
||||
print("\n" + "="*50)
|
||||
print("1. TRAINING SENTIMENT (FinBERT)")
|
||||
print("="*50)
|
||||
|
||||
# Collect all sentiment data
|
||||
texts, labels = [], []
|
||||
for text, label in [
|
||||
("BTC breaks $100k! New ATH!", 1), ("ETH to $10k by EOY, accumulate now", 1),
|
||||
("Institutional inflows hit record high", 1), ("Bitcoin reaches new all-time high", 1),
|
||||
("Ethereum merge successful, staking rewards now live", 1),
|
||||
("Massive ETF inflows drive Bitcoin to new highs", 1),
|
||||
("Golden cross confirmed on Bitcoin weekly chart", 1),
|
||||
("Institutional adoption drives Bitcoin higher", 1),
|
||||
("ETF approval drives massive inflows", 1), ("Market is bullish on Bitcoin", 1),
|
||||
("BTC crashes 50% in hours", 0), ("Exchange hacked, $100M stolen", 0),
|
||||
("SEC sues major exchange", 0), ("Bitcoin crashes hard, panic selling everywhere", 0),
|
||||
("Massive liquidation cascade wipes out $200M in longs", 0),
|
||||
("VIX drops below 15 as market volatility decreases", 0),
|
||||
("Whale sells 10000 BTC", 0), ("Bitcoin price drops 50%", 0),
|
||||
("Support broken with bearish structure", 0), ("Panic selling and forced liquidation", 0),
|
||||
("BTC at $50k, ETH at $3k", 2), ("Market consolidating in range", 2),
|
||||
("Bitcoin remains stable around $30k", 2), ("VIX drops below 15", 2),
|
||||
("Market consolidating with no clear direction", 2),
|
||||
("Bitcoin price stable around $30k", 2), ("Consolidation phase continues", 2),
|
||||
("Market in wait-and-see mode", 2), ("Sideways action continues", 2),
|
||||
("Low volatility environment persists", 2),
|
||||
]:
|
||||
texts.append(text); labels.append(label)
|
||||
for event in REAL_EVENTS:
|
||||
texts.append(event["text"]); labels.append(event["label_id"])
|
||||
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42, stratify=labels)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42, stratify=temp_l)
|
||||
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert", num_labels=3,
|
||||
id2label={0:"Bearish",1:"Bullish",2:"Neutral"},
|
||||
label2id={"Bearish":0,"Bullish":1,"Neutral":2})
|
||||
|
||||
class QuickDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert"); self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.long)}
|
||||
|
||||
train_ds = QuickDataset(train_t, train_l, AutoTokenizer.from_pretrained("ProsusAI/finbert"))
|
||||
val_ds = QuickDataset(temp_t, temp_l, AutoTokenizer.from_pretrained("ProsusAI/finbert"))
|
||||
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"ProsusAI/finbert", num_labels=3,
|
||||
id2label={0:"Bearish",1:"Bullish",2:"Neutral"},
|
||||
label2id={"Bearish":0,"Bullish":1,"Neutral":2})
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/finbert-crypto-sentiment",
|
||||
num_train_epochs=2, per_device_train_batch_size=16,
|
||||
per_device_eval_batch_size=32, gradient_accumulation_steps=2,
|
||||
warmup_ratio=0.1, learning_rate=2e-5, lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch", save_strategy="epoch",
|
||||
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, save_total_limit=1, remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=QuickDataset(train_t, train_l, AutoTokenizer.from_pretrained("ProsusAI/finbert")),
|
||||
eval_dataset=QuickDataset(temp_t, temp_l, AutoTokenizer.from_pretrained("ProsusAI/finbert")),
|
||||
tokenizer=AutoTokenizer.from_pretrained("ProsusAI/finbert"),
|
||||
compute_metrics=lambda ep: {"f1_macro": f1_score(ep.label_ids, np.argmax(ep.predictions, axis=-1), average="macro")},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("\n1. TRAINING SENTIMENT (FinBERT)")
|
||||
print("="*50)
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}")
|
||||
print("Training Sentiment (2 epochs, ~3 min)...")
|
||||
trainer.train()
|
||||
|
||||
model.save_pretrained("./models/finbert-crypto-sentiment")
|
||||
AutoTokenizer.from_pretrained("ProsusAI/finbert").save_pretrained("./models/finbert-crypto-sentiment")
|
||||
print("✅ Sentiment model saved!")
|
||||
return model
|
||||
|
||||
|
||||
def train_events():
|
||||
print("\n" + "="*50)
|
||||
print("2. TRAINING EVENT CLASSIFIER (BERT)")
|
||||
print("="*50)
|
||||
|
||||
texts, labels = [], []
|
||||
for event in REAL_EVENTS:
|
||||
lbls = [0]*12
|
||||
lbls[EVENT_MAP[event["event_type"]]] = 1
|
||||
texts.append(event["text"])
|
||||
labels.append(lbls)
|
||||
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42)
|
||||
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"bert-base-uncased", num_labels=12,
|
||||
id2label={i:l for i,l in enumerate(EVENT_LABELS)}, label2id=EVENT_MAP,
|
||||
problem_type="multi_label_classification")
|
||||
|
||||
class MultiLabelDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = tokenizer; self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.float)}
|
||||
|
||||
texts, labels = [], []
|
||||
for event in REAL_EVENTS:
|
||||
lbls = [0]*12
|
||||
lbls[EVENT_MAP[event["event_type"]]] = 1
|
||||
texts.append(event["text"])
|
||||
labels.append(lbls)
|
||||
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42)
|
||||
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"bert-base-uncased", num_labels=12,
|
||||
id2label={i:l for i,l in enumerate(EVENT_LABELS)}, label2id=EVENT_MAP,
|
||||
problem_type="multi_label_classification")
|
||||
|
||||
class MultiLabelDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = tokenizer; self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.float)}
|
||||
|
||||
train_ds = MultiLabelDataset([t for t in texts if t in train_t], [l for t,l in zip(texts, labels) if t in train_t], AutoTokenizer.from_pretrained("bert-base-uncased"))
|
||||
val_ds = MultiLabelDataset([t for t in texts if t in temp_t], [l for t,l in zip(texts, labels) if t in temp_t], AutoTokenizer.from_pretrained("bert-base-uncased"))
|
||||
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"bert-base-uncased", num_labels=12,
|
||||
id2label={i:l for i,l in enumerate(EVENT_LABELS)}, label2id=EVENT_MAP,
|
||||
problem_type="multi_label_classification")
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/bert-crypto-events",
|
||||
num_train_epochs=2, per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16, gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1, learning_rate=2e-5, lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch", save_strategy="epoch",
|
||||
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, save_total_limit=1, remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=MultiLabelDataset([t for t in texts if t in train_t], [l for t,l in zip(texts, labels) if t in train_t], AutoTokenizer.from_pretrained("bert-base-uncased")),
|
||||
eval_dataset=MultiLabelDataset([t for t in texts if t in temp_t], [l for t,l in zip(texts, labels) if t in temp_t], AutoTokenizer.from_pretrained("bert-base-uncased")),
|
||||
tokenizer=AutoTokenizer.from_pretrained("bert-base-uncased"),
|
||||
compute_metrics=lambda ep: {"f1_macro": f1_score(ep.label_ids, (np.array(ep.predictions) > 0.5).astype(int), average="macro")},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("\n2. TRAINING EVENT CLASSIFIER (BERT)")
|
||||
print("="*50)
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
print("Training Events (2 epochs, ~5 min)...")
|
||||
trainer.train()
|
||||
|
||||
model.save_pretrained("./models/bert-crypto-events")
|
||||
AutoTokenizer.from_pretrained("bert-base-uncased").save_pretrained("./models/bert-crypto-events")
|
||||
print("✅ Event model saved!")
|
||||
return model
|
||||
|
||||
|
||||
def train_emotion():
|
||||
print("\n" + "="*50)
|
||||
print("3. TRAINING EMOTION (DistilRoBERTa)")
|
||||
print("="*50)
|
||||
|
||||
texts, labels = [], []
|
||||
for text, lbls in [
|
||||
("BTC breaks $100k! New ATH!", [1,0,0,1,0,0]),
|
||||
("Ethereum merge successful!", [1,0,0,1,0,0]),
|
||||
("Major hack on DeFi protocol drains $50M", [0,1,1,0,1,0]),
|
||||
("Bitcoin crashes 50% in hours", [0,1,1,0,1,0]),
|
||||
("SEC sues major exchange", [0,1,1,0,1,0]),
|
||||
("Rug pull! Devs stole all funds!", [0,1,1,0,0,0]),
|
||||
("FOMO drives memecoin 500% in 24h", [0,0,0,1,0,0]),
|
||||
("Buy the dip! Accumulate more!", [0,0,0,1,0,0]),
|
||||
("Lost everything in the crash", [0,0,0,0,1,0]),
|
||||
("BTC at $50k, ETH at $3k", [0,0,0,0,0,1]),
|
||||
]:
|
||||
texts.append(text); labels.append(lbls)
|
||||
|
||||
train_t, temp_t, train_l, temp_l = train_test_split(texts, labels, test_size=0.3, random_state=42)
|
||||
temp_t, test_t, temp_l, test_l = train_test_split(temp_t, temp_l, test_size=0.5, random_state=42)
|
||||
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base")
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"j-hartmann/emotion-english-distilroberta-base", num_labels=6,
|
||||
id2label={i:l for i,l in enumerate(EMOTION_LABELS)}, label2id=EMOTION_MAP,
|
||||
problem_type="multi_label_classification", ignore_mismatched_sizes=True)
|
||||
|
||||
class MultiLabelDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, texts, labels, tokenizer, max_len=64):
|
||||
self.texts = texts; self.labels = labels
|
||||
self.tokenizer = tokenizer; self.max_len = 64
|
||||
def __len__(self): return len(self.texts)
|
||||
def __getitem__(self, i):
|
||||
enc = self.tokenizer(self.texts[i], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt")
|
||||
return {"input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[i], dtype=torch.float)}
|
||||
|
||||
train_ds = MultiLabelDataset([t for t in texts if t in train_t], [l for t,l in zip(texts, labels) if t in train_t], AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base"))
|
||||
val_ds = MultiLabelDataset([t for t in texts if t in temp_t], [l for t,l in zip(texts, labels) if t in temp_t], AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base"))
|
||||
|
||||
model = AutoModelForSequenceClassification.from_pretrained(
|
||||
"j-hartmann/emotion-english-distilroberta-base", num_labels=6,
|
||||
id2label={i:l for i,l in enumerate(EMOTION_LABELS)}, label2id=EMOTION_MAP,
|
||||
problem_type="multi_label_classification", ignore_mismatched_sizes=True)
|
||||
|
||||
trainer = Trainer(
|
||||
model=model,
|
||||
args=TrainingArguments(
|
||||
output_dir="./models/distilroberta-crypto-emotion",
|
||||
num_train_epochs=2, per_device_train_batch_size=8,
|
||||
per_device_eval_batch_size=16, gradient_accumulation_steps=4,
|
||||
warmup_ratio=0.1, learning_rate=2e-5, lr_scheduler_type="cosine",
|
||||
eval_strategy="epoch", save_strategy="epoch",
|
||||
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, save_total_limit=1, remove_unused_columns=False,
|
||||
report_to="none",
|
||||
),
|
||||
train_dataset=MultiLabelDataset([t for t in texts if t in train_t], [l for t,l in zip(texts, labels) if t in train_t], AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base")),
|
||||
eval_dataset=MultiLabelDataset([t for t in texts if t in temp_t], [l for t,l in zip(texts, labels) if t in temp_t], AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base")),
|
||||
tokenizer=AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base"),
|
||||
compute_metrics=lambda ep: {"f1_macro": f1_score(ep.label_ids, (np.array(ep.predictions) > 0.5).astype(int), average="macro")},
|
||||
callbacks=[EarlyStoppingCallback(early_stopping_patience=1)]
|
||||
)
|
||||
|
||||
print("\n3. TRAINING EMOTION (DistilRoBERTa)")
|
||||
print("="*50)
|
||||
print(f"Train: {len(train_t)}, Val: {len(temp_t)}, Test: {len(test_t)}")
|
||||
print("Training Emotion (2 epochs, ~3 min)...")
|
||||
trainer.train()
|
||||
|
||||
model.save_pretrained("./models/distilroberta-crypto-emotion")
|
||||
AutoTokenizer.from_pretrained("j-hartmann/emotion-english-distilroberta-base").save_pretrained("./models/distilroberta-crypto-emotion")
|
||||
print("✅ Emotion model saved!")
|
||||
return model
|
||||
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print("DOMAIN ADAPTATION: FINE-TUNING ALL MODELS")
|
||||
print("="*60)
|
||||
|
||||
import torch
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
# 1. SENTIMENT
|
||||
train_sentiment()
|
||||
|
||||
# 2. EVENTS
|
||||
train_events()
|
||||
|
||||
# 3. EMOTION
|
||||
train_emotion()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ ALL MODELS TRAINED AND SAVED!")
|
||||
print("="*60)
|
||||
print("Models saved to ./models/")
|
||||
print(" - finbert-crypto-sentiment/")
|
||||
print(" - bert-crypto-events/")
|
||||
print(" - distilroberta-crypto-emotion/")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import torch
|
||||
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback
|
||||
from datasets import load_dataset
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score, f1_score
|
||||
from sklearn.utils.class_weight import compute_class_weight
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user