sentext / finance /train_finbert.py
Ani-404's picture
fix finance pipeline and make sentiment models configurable
a4d1e62
Raw
History Blame Contribute Delete
4.31 kB
# train_finbert.py
# Fine-tunes a FinBERT model for emotion/financial sentiment classification.
#
# Run from the project root, e.g.:
# python -m finance.train_finbert --data Data/emotion_dataset.csv --output finbert_emotion_model
import argparse
import numpy as np
import pandas as pd
from datasets import Dataset
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from transformers import (
AutoModelForSequenceClassification,
AutoTokenizer,
Trainer,
TrainingArguments,
)
def compute_metrics(p):
"""Compute accuracy and weighted F1."""
preds = np.argmax(p.predictions, axis=1)
f1 = f1_score(p.label_ids, preds, average="weighted", zero_division=0)
acc = accuracy_score(p.label_ids, preds)
return {"accuracy": acc, "f1": f1}
def parse_args():
parser = argparse.ArgumentParser(description="Fine-tune FinBERT for sentiment.")
parser.add_argument(
"--data",
default="Data/emotion_dataset.csv",
help="Path to the training CSV (needs Text/Clean_Text and Emotion columns).",
)
parser.add_argument(
"--output",
default="finbert_emotion_model",
help="Directory to save the fine-tuned model.",
)
parser.add_argument(
"--model-name",
default="yiyanghkust/finbert-pretrain",
help="Base model to fine-tune.",
)
parser.add_argument("--epochs", type=int, default=3)
parser.add_argument("--batch-size", type=int, default=16)
parser.add_argument("--max-length", type=int, default=128)
return parser.parse_args()
def main():
args = parse_args()
print(f"Loading dataset from: {args.data}")
try:
df = pd.read_csv(args.data)
except FileNotFoundError:
print(f"ERROR: '{args.data}' not found.")
return
print("Dataset loaded successfully. Preprocessing data...")
df["text"] = df["Clean_Text"].fillna(df["Text"]) if "Clean_Text" in df else df["Text"]
df.dropna(subset=["text", "Emotion"], inplace=True)
label_encoder = LabelEncoder()
df["labels"] = label_encoder.fit_transform(df["Emotion"])
num_labels = len(label_encoder.classes_)
id2label = {i: label for i, label in enumerate(label_encoder.classes_)}
label2id = {label: i for i, label in enumerate(label_encoder.classes_)}
print(f"Found {num_labels} unique emotions.")
train_df, val_df = train_test_split(
df, test_size=0.2, random_state=42, stratify=df["labels"]
)
train_dataset = Dataset.from_pandas(train_df)
val_dataset = Dataset.from_pandas(val_df)
print("Loading FinBERT tokenizer and model...")
tokenizer = AutoTokenizer.from_pretrained(args.model_name)
model = AutoModelForSequenceClassification.from_pretrained(
args.model_name,
num_labels=num_labels,
id2label=id2label,
label2id=label2id,
ignore_mismatched_sizes=True, # ESSENTIAL for transfer learning
)
def tokenize_function(examples):
return tokenizer(
examples["text"],
padding="max_length",
truncation=True,
max_length=args.max_length,
)
tokenized_train_dataset = train_dataset.map(tokenize_function, batched=True)
tokenized_val_dataset = val_dataset.map(tokenize_function, batched=True)
print(f"Starting model fine-tuning... (output: {args.output})")
training_args = TrainingArguments(
output_dir=args.output,
num_train_epochs=args.epochs,
per_device_train_batch_size=args.batch_size,
per_device_eval_batch_size=args.batch_size,
logging_steps=100,
eval_strategy="epoch",
save_strategy="epoch",
save_total_limit=1,
load_best_model_at_end=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_train_dataset,
eval_dataset=tokenized_val_dataset,
compute_metrics=compute_metrics,
)
trainer.train()
print(f"Training complete. Saving final model to '{args.output}'...")
trainer.save_model(args.output)
tokenizer.save_pretrained(args.output)
print("Model and tokenizer saved successfully.")
if __name__ == "__main__":
main()