Refactor into 2 examples
This commit is contained in:
@@ -0,0 +1,224 @@
|
|||||||
|
"""
|
||||||
|
Example usage of the TimesFM Finetuning Framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import yfinance as yf
|
||||||
|
import torch
|
||||||
|
from os import path
|
||||||
|
import numpy as np
|
||||||
|
from torch.utils.data import Dataset
|
||||||
|
from timesfm import TimesFm, TimesFmHparams, TimesFmCheckpoint
|
||||||
|
from timesfm.pytorch_patched_decoder import PatchedTimeSeriesDecoder
|
||||||
|
from finetuning_torch import FinetuningConfig, TimesFMFinetuner
|
||||||
|
from huggingface_hub import snapshot_download
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from torch.utils.data import Dataset
|
||||||
|
import torch
|
||||||
|
import yfinance as yf
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
from timesfm import TimesFm, TimesFmHparams
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSeriesDataset(Dataset):
|
||||||
|
"""Dataset for time series data compatible with TimesFM."""
|
||||||
|
|
||||||
|
def __init__(self, series: np.ndarray, context_length: int, horizon_length: int):
|
||||||
|
"""
|
||||||
|
Initialize dataset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
series: Time series data
|
||||||
|
context_length: Number of past timesteps to use as input
|
||||||
|
horizon_length: Number of future timesteps to predict
|
||||||
|
"""
|
||||||
|
self.series = series
|
||||||
|
self.context_length = context_length
|
||||||
|
self.horizon_length = horizon_length
|
||||||
|
self._prepare_samples()
|
||||||
|
|
||||||
|
def _prepare_samples(self) -> None:
|
||||||
|
"""Prepare sliding window samples from the time series."""
|
||||||
|
self.samples = []
|
||||||
|
total_length = self.context_length + self.horizon_length
|
||||||
|
|
||||||
|
for start_idx in range(0, len(self.series) - total_length + 1):
|
||||||
|
end_idx = start_idx + self.context_length
|
||||||
|
x_context = self.series[start_idx:end_idx]
|
||||||
|
x_future = self.series[end_idx : end_idx + self.horizon_length]
|
||||||
|
self.samples.append((x_context, x_future))
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.samples)
|
||||||
|
|
||||||
|
def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
|
x_context, x_future = self.samples[index]
|
||||||
|
|
||||||
|
x_context = torch.tensor(x_context, dtype=torch.float32)
|
||||||
|
x_future = torch.tensor(x_future, dtype=torch.float32)
|
||||||
|
|
||||||
|
input_padding = torch.zeros_like(x_context)
|
||||||
|
freq = torch.zeros(1, dtype=torch.long)
|
||||||
|
|
||||||
|
return x_context, input_padding, freq, x_future
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_datasets(
|
||||||
|
series: np.ndarray, context_length: int, horizon_length: int, train_split: float = 0.8
|
||||||
|
) -> Tuple[Dataset, Dataset]:
|
||||||
|
"""
|
||||||
|
Prepare training and validation datasets from time series data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
series: Input time series data
|
||||||
|
context_length: Number of past timesteps to use
|
||||||
|
horizon_length: Number of future timesteps to predict
|
||||||
|
train_split: Fraction of data to use for training
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (train_dataset, val_dataset)
|
||||||
|
"""
|
||||||
|
train_size = int(len(series) * train_split)
|
||||||
|
train_data = series[:train_size]
|
||||||
|
val_data = series[train_size:]
|
||||||
|
|
||||||
|
# Create datasets
|
||||||
|
train_dataset = TimeSeriesDataset(train_data, context_length=context_length, horizon_length=horizon_length)
|
||||||
|
|
||||||
|
val_dataset = TimeSeriesDataset(val_data, context_length=context_length, horizon_length=horizon_length)
|
||||||
|
|
||||||
|
return train_dataset, val_dataset
|
||||||
|
|
||||||
|
|
||||||
|
def get_model(load_weights: bool = False):
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
repo_id = "google/timesfm-2.0-500m-pytorch"
|
||||||
|
hparams = TimesFmHparams(
|
||||||
|
backend=device,
|
||||||
|
per_core_batch_size=32,
|
||||||
|
horizon_len=128,
|
||||||
|
num_layers=50,
|
||||||
|
use_positional_embedding=False,
|
||||||
|
context_len=192,
|
||||||
|
)
|
||||||
|
tfm = TimesFm(hparams=hparams, checkpoint=TimesFmCheckpoint(huggingface_repo_id=repo_id))
|
||||||
|
|
||||||
|
model = PatchedTimeSeriesDecoder(tfm._model_config)
|
||||||
|
if load_weights:
|
||||||
|
checkpoint_path = path.join(snapshot_download(repo_id), "torch_model.ckpt")
|
||||||
|
loaded_checkpoint = torch.load(checkpoint_path, weights_only=True)
|
||||||
|
model.load_state_dict(loaded_checkpoint)
|
||||||
|
model = model.to(device)
|
||||||
|
return model, hparams, tfm._model_config
|
||||||
|
|
||||||
|
|
||||||
|
def plot_predictions(
|
||||||
|
model: TimesFm,
|
||||||
|
val_dataset: Dataset,
|
||||||
|
save_path: Optional[str] = "predictions.png",
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Plot model predictions against ground truth for a batch of validation data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Trained TimesFM model
|
||||||
|
val_dataset: Validation dataset
|
||||||
|
save_path: Path to save the plot
|
||||||
|
"""
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
x_context, x_padding, freq, x_future = val_dataset[0]
|
||||||
|
x_context = x_context.unsqueeze(0) # Add batch dimension
|
||||||
|
x_padding = x_padding.unsqueeze(0)
|
||||||
|
freq = freq.unsqueeze(0)
|
||||||
|
x_future = x_future.unsqueeze(0)
|
||||||
|
|
||||||
|
device = next(model.parameters()).device
|
||||||
|
x_context = x_context.to(device)
|
||||||
|
x_padding = x_padding.to(device)
|
||||||
|
freq = freq.to(device)
|
||||||
|
x_future = x_future.to(device)
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
predictions = model(x_context, x_padding.float(), freq)
|
||||||
|
predictions_mean = predictions[..., 0] # [B, N, horizon_len]
|
||||||
|
last_patch_pred = predictions_mean[:, -1, :] # [B, horizon_len]
|
||||||
|
|
||||||
|
context_vals = x_context[0].cpu().numpy()
|
||||||
|
future_vals = x_future[0].cpu().numpy()
|
||||||
|
pred_vals = last_patch_pred[0].cpu().numpy()
|
||||||
|
|
||||||
|
context_len = len(context_vals)
|
||||||
|
horizon_len = len(future_vals)
|
||||||
|
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
|
||||||
|
plt.plot(range(context_len), context_vals, label="Historical Data", color="blue", linewidth=2)
|
||||||
|
|
||||||
|
plt.plot(
|
||||||
|
range(context_len, context_len + horizon_len),
|
||||||
|
future_vals,
|
||||||
|
label="Ground Truth",
|
||||||
|
color="green",
|
||||||
|
linestyle="--",
|
||||||
|
linewidth=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.plot(range(context_len, context_len + horizon_len), pred_vals, label="Prediction", color="red", linewidth=2)
|
||||||
|
|
||||||
|
plt.xlabel("Time Step")
|
||||||
|
plt.ylabel("Value")
|
||||||
|
plt.title("TimesFM Predictions vs Ground Truth")
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True)
|
||||||
|
|
||||||
|
if save_path:
|
||||||
|
plt.savefig(save_path)
|
||||||
|
print(f"Plot saved to {save_path}")
|
||||||
|
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(context_len: int, horizon_len: int) -> Tuple[Dataset, Dataset]:
|
||||||
|
df = yf.download("AAPL", start="2010-01-01", end="2019-01-01")
|
||||||
|
time_series = df["Close"].values
|
||||||
|
|
||||||
|
train_dataset, val_dataset = prepare_datasets(
|
||||||
|
series=time_series,
|
||||||
|
context_length=context_len,
|
||||||
|
horizon_length=horizon_len,
|
||||||
|
train_split=0.8,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Created datasets:")
|
||||||
|
print(f"- Training samples: {len(train_dataset)}")
|
||||||
|
print(f"- Validation samples: {len(val_dataset)}")
|
||||||
|
return train_dataset, val_dataset
|
||||||
|
|
||||||
|
|
||||||
|
def basic_example():
|
||||||
|
"""Basic example of finetuning TimesFM on stock data."""
|
||||||
|
model, hparams, tfm_config = get_model(load_weights=True)
|
||||||
|
config = FinetuningConfig(batch_size=256, num_epochs=5, learning_rate=1e-4, use_wandb=False)
|
||||||
|
|
||||||
|
train_dataset, val_dataset = get_data(128, tfm_config.horizon_len)
|
||||||
|
finetuner = TimesFMFinetuner(model, config)
|
||||||
|
|
||||||
|
print("\nStarting finetuning...")
|
||||||
|
results = finetuner.finetune(train_dataset=train_dataset, val_dataset=val_dataset)
|
||||||
|
|
||||||
|
print("\nFinetuning completed!")
|
||||||
|
print(f"Training history: {len(results['history']['train_loss'])} epochs")
|
||||||
|
|
||||||
|
plot_predictions(
|
||||||
|
model=model,
|
||||||
|
val_dataset=val_dataset,
|
||||||
|
save_path="timesfm_predictions.png",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
basic_example()
|
||||||
+153
-228
@@ -1,274 +1,199 @@
|
|||||||
# Filename: tutorial_timesfm.py
|
"""
|
||||||
|
TimesFM Finetuner: A flexible framework for finetuning TimesFM models on custom datasets.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
```python
|
||||||
|
# Prepare datasets
|
||||||
|
train_dataset = TimeSeriesDataset(train_data, context_length=128, horizon_length=32)
|
||||||
|
val_dataset = TimeSeriesDataset(val_data, context_length=128, horizon_length=32)
|
||||||
|
|
||||||
|
# Initialize model and configuration
|
||||||
|
model = TimesFm(...)
|
||||||
|
config = FinetuningConfig(
|
||||||
|
batch_size=64,
|
||||||
|
num_epochs=50,
|
||||||
|
learning_rate=1e-4,
|
||||||
|
use_wandb=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create finetuner
|
||||||
|
finetuner = TimesFMFinetuner(model, config)
|
||||||
|
|
||||||
|
# Finetune model
|
||||||
|
results = finetuner.finetune(train_dataset, val_dataset)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
import yfinance as yf
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import torch
|
import torch
|
||||||
from torch.utils.data import Dataset, DataLoader
|
from torch.utils.data import Dataset, DataLoader
|
||||||
import torch.optim as optim
|
import torch.optim as optim
|
||||||
import timesfm
|
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||||
from os import path
|
import wandb
|
||||||
from typing import Any, Sequence
|
import multiprocessing as mp
|
||||||
|
|
||||||
import numpy as np
|
from timesfm import TimesFm
|
||||||
import torch
|
|
||||||
from huggingface_hub import snapshot_download
|
|
||||||
|
|
||||||
|
|
||||||
from timesfm.pytorch_patched_decoder import TimesFMConfig, PatchedTimeSeriesDecoder
|
@dataclass
|
||||||
|
class FinetuningConfig:
|
||||||
|
"""Configuration for TimesFM finetuning process."""
|
||||||
|
|
||||||
import torch
|
# Training parameters
|
||||||
import matplotlib.pyplot as plt
|
batch_size: int = 32
|
||||||
|
num_epochs: int = 20
|
||||||
|
learning_rate: float = 1e-4
|
||||||
|
weight_decay: float = 0.01
|
||||||
|
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
# Hardware parameters
|
||||||
|
device: str = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
distributed: bool = False
|
||||||
|
world_size: int = 1
|
||||||
|
|
||||||
|
# Logging parameters
|
||||||
|
use_wandb: bool = False
|
||||||
|
wandb_project: str = "timesfm-finetuning"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
class TimesFMFinetuner:
|
||||||
# 1. Download stock data via yfinance
|
"""Main class for finetuning TimesFM models."""
|
||||||
# --------------------------------------------------
|
|
||||||
def download_yfinance_data(ticker="AAPL", start="2020-01-01", end="2022-01-01"):
|
|
||||||
"""
|
|
||||||
Download daily stock data for a given ticker from Yahoo Finance.
|
|
||||||
Returns a pandas DataFrame with columns like 'Open', 'High', 'Low', 'Close', 'Volume'.
|
|
||||||
"""
|
|
||||||
df = yf.download(ticker, start=start, end=end)
|
|
||||||
df = df.dropna()
|
|
||||||
return df["Close"].reset_index(drop=True)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# 2. Create a dataset class for TimesFM
|
|
||||||
# --------------------------------------------------
|
|
||||||
class FinancialDataset(Dataset):
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
series: pd.Series,
|
model: TimesFm,
|
||||||
config: TimesFMConfig,
|
config: FinetuningConfig,
|
||||||
context_length=128, # how many past timesteps as input
|
loss_fn: Optional[callable] = None,
|
||||||
horizon_length=32, # how many future steps to predict
|
logger: Optional[logging.Logger] = None,
|
||||||
):
|
):
|
||||||
super().__init__()
|
"""
|
||||||
|
Initialize TimesFM finetuner.
|
||||||
|
|
||||||
self.series = series.values.astype(np.float32)
|
Args:
|
||||||
self.context_length = context_length
|
model: TimesFM model to finetune
|
||||||
self.horizon_length = horizon_length
|
config: Finetuning configuration
|
||||||
|
logger: Optional logger instance
|
||||||
|
"""
|
||||||
|
self.model = model
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.logger = logger or logging.getLogger(__name__)
|
||||||
|
|
||||||
self.samples = []
|
self.device = torch.device(config.device)
|
||||||
# We want to ensure we have at least context_length + horizon_length points.
|
self.loss_fn = loss_fn or (lambda x, y: torch.mean((x - y.squeeze(-1)) ** 2)) # MSELoss()
|
||||||
for start_idx in range(0, len(self.series) - (context_length + horizon_length)):
|
|
||||||
end_idx = start_idx + context_length
|
|
||||||
# context slice
|
|
||||||
x_context = self.series[start_idx:end_idx]
|
|
||||||
# future/horizon slice
|
|
||||||
x_future = self.series[end_idx : end_idx + horizon_length]
|
|
||||||
self.samples.append((x_context, x_future))
|
|
||||||
|
|
||||||
def __len__(self):
|
if config.use_wandb:
|
||||||
return len(self.samples)
|
self._setup_wandb()
|
||||||
|
|
||||||
def __getitem__(self, index):
|
def _setup_wandb(self) -> None:
|
||||||
x_context, x_future = self.samples[index]
|
"""Initialize Weights & Biases logging."""
|
||||||
# Convert to torch
|
wandb.init(project=self.config.wandb_project, entity=self.config.wandb_entity, config=self.config.__dict__)
|
||||||
x_context = torch.tensor(x_context, dtype=torch.float32)
|
|
||||||
x_future = torch.tensor(x_future, dtype=torch.float32)
|
|
||||||
|
|
||||||
input_padding = torch.zeros_like(x_context)
|
def _create_dataloader(self, dataset: Dataset, name: str) -> DataLoader:
|
||||||
|
"""Create a dataloader from a dataset."""
|
||||||
|
return DataLoader(
|
||||||
|
dataset,
|
||||||
|
batch_size=self.config.batch_size,
|
||||||
|
shuffle=name == "train",
|
||||||
|
num_workers=mp.cpu_count(),
|
||||||
|
pin_memory=self.device.type == "cuda",
|
||||||
|
persistent_workers=True,
|
||||||
|
prefetch_factor=2,
|
||||||
|
)
|
||||||
|
|
||||||
freq = torch.zeros(1, dtype=torch.long)
|
def _train_epoch(self, train_loader: DataLoader, optimizer: torch.optim.Optimizer) -> float:
|
||||||
|
"""Train for one epoch."""
|
||||||
|
self.model.train()
|
||||||
|
total_loss = 0.0
|
||||||
|
n_batches = len(train_loader)
|
||||||
|
|
||||||
return x_context, input_padding, freq, x_future
|
for batch in train_loader:
|
||||||
|
x_context, x_padding, freq, x_future = [t.to(self.device, non_blocking=True) for t in batch]
|
||||||
|
|
||||||
|
predictions = self.model(x_context, x_padding.float(), freq)
|
||||||
def collate_fn(batch):
|
predictions_mean = predictions[..., 0]
|
||||||
xs_context = [item[0] for item in batch]
|
last_patch_pred = predictions_mean[:, -1, :]
|
||||||
xs_padding = [item[1] for item in batch]
|
loss = self.loss_fn(last_patch_pred, x_future.squeeze(-1))
|
||||||
freqs = [item[2] for item in batch]
|
|
||||||
xs_future = [item[3] for item in batch]
|
|
||||||
|
|
||||||
x_context = torch.stack(xs_context, dim=0)
|
|
||||||
input_pad = torch.stack(xs_padding, dim=0)
|
|
||||||
freq = torch.stack(freqs, dim=0) # shape [B, 1]
|
|
||||||
x_future = torch.stack(xs_future, dim=0)
|
|
||||||
|
|
||||||
return x_context, input_pad, freq, x_future
|
|
||||||
|
|
||||||
|
|
||||||
def get_model(*, load_weights: bool = False):
|
|
||||||
# standard model hack
|
|
||||||
repo_id = "google/timesfm-2.0-500m-pytorch"
|
|
||||||
tfm = timesfm.TimesFm(
|
|
||||||
hparams=timesfm.TimesFmHparams(
|
|
||||||
backend="cuda",
|
|
||||||
per_core_batch_size=32,
|
|
||||||
horizon_len=128,
|
|
||||||
num_layers=50,
|
|
||||||
use_positional_embedding=False,
|
|
||||||
context_len=192,
|
|
||||||
),
|
|
||||||
checkpoint=timesfm.TimesFmCheckpoint(huggingface_repo_id=repo_id),
|
|
||||||
)
|
|
||||||
|
|
||||||
model = PatchedTimeSeriesDecoder(tfm._model_config)
|
|
||||||
|
|
||||||
if load_weights:
|
|
||||||
checkpoint_path = path.join(snapshot_download(repo_id), "torch_model.ckpt")
|
|
||||||
print(model.state_dict()["input_ff_layer.hidden_layer.0.weight"])
|
|
||||||
loaded_checkpoint = torch.load(checkpoint_path, weights_only=True)
|
|
||||||
model.load_state_dict(loaded_checkpoint)
|
|
||||||
print("After loading:")
|
|
||||||
print(model.state_dict()["input_ff_layer.hidden_layer.0.weight"])
|
|
||||||
model = model.to(device)
|
|
||||||
|
|
||||||
# import sys
|
|
||||||
# sys.exit(-1)
|
|
||||||
# repo_id = "google/timesfm-1.0-200m"
|
|
||||||
return model, tfm._model_config
|
|
||||||
|
|
||||||
|
|
||||||
def train_model(
|
|
||||||
ticker="AAPL", start="2015-01-01", end="2022-01-01", train_split=0.8, batch_size=8, num_epochs=20, pretrained=False
|
|
||||||
):
|
|
||||||
df_close = download_yfinance_data(ticker, start=start, end=end)
|
|
||||||
model, config = get_model(load_weights=pretrained)
|
|
||||||
|
|
||||||
total_len = len(df_close)
|
|
||||||
train_size = int(total_len * train_split)
|
|
||||||
val_size = total_len - train_size
|
|
||||||
|
|
||||||
train_series = df_close.iloc[:train_size].reset_index(drop=True)
|
|
||||||
val_series = df_close.iloc[train_size:].reset_index(drop=True)
|
|
||||||
|
|
||||||
train_dataset = FinancialDataset(
|
|
||||||
series=train_series, config=config, context_length=128, horizon_length=config.horizon_len
|
|
||||||
)
|
|
||||||
val_dataset = FinancialDataset(
|
|
||||||
series=val_series, config=config, context_length=128, horizon_length=config.horizon_len
|
|
||||||
)
|
|
||||||
print("Train samples:", len(train_dataset))
|
|
||||||
print("Val samples:", len(val_dataset))
|
|
||||||
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
|
|
||||||
val_dataloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)
|
|
||||||
|
|
||||||
optimizer = optim.Adam(model.parameters(), lr=1e-4)
|
|
||||||
|
|
||||||
for epoch in range(num_epochs):
|
|
||||||
model.train()
|
|
||||||
total_train_loss = 0.0
|
|
||||||
|
|
||||||
for x_context, x_padding, freq, x_future in train_dataloader:
|
|
||||||
x_context, x_padding, freq, x_future = (
|
|
||||||
x_context.to(device),
|
|
||||||
x_padding.to(device),
|
|
||||||
freq.to(device),
|
|
||||||
x_future.to(device),
|
|
||||||
)
|
|
||||||
predictions = model(x_context, x_padding.float(), freq)
|
|
||||||
# predictions shape => [B, N, horizon_len, (1 + #quantiles)]
|
|
||||||
predictions_mean = predictions[..., 0] # => [B, N, horizon_len]
|
|
||||||
last_patch_pred = predictions_mean[:, -1, :] # => [B, horizon_len]
|
|
||||||
|
|
||||||
# x_future => [B, horizon_len]
|
|
||||||
loss = torch.mean((last_patch_pred - x_future.squeeze(-1)) ** 2)
|
|
||||||
|
|
||||||
optimizer.zero_grad()
|
optimizer.zero_grad()
|
||||||
loss.backward()
|
loss.backward()
|
||||||
optimizer.step()
|
optimizer.step()
|
||||||
|
|
||||||
total_train_loss += loss.item()
|
total_loss += loss.item()
|
||||||
|
|
||||||
avg_train_loss = total_train_loss / len(train_dataloader)
|
return total_loss / n_batches
|
||||||
|
|
||||||
# -------- Compute validation loss --------
|
@torch.no_grad()
|
||||||
model.eval()
|
def _validate(self, val_loader: DataLoader) -> float:
|
||||||
total_val_loss = 0.0
|
"""Perform validation."""
|
||||||
with torch.no_grad():
|
self.model.eval()
|
||||||
for x_context, x_padding, freq, x_future in val_dataloader:
|
total_loss = 0.0
|
||||||
x_context, x_padding, freq, x_future = (
|
|
||||||
x_context.to(device),
|
|
||||||
x_padding.to(device),
|
|
||||||
freq.to(device),
|
|
||||||
x_future.to(device),
|
|
||||||
)
|
|
||||||
predictions = model(x_context, x_padding.float(), freq)
|
|
||||||
predictions_mean = predictions[..., 0]
|
|
||||||
last_patch_pred = predictions_mean[:, -1, :]
|
|
||||||
val_loss = torch.mean((last_patch_pred - x_future.squeeze(-1)) ** 2)
|
|
||||||
total_val_loss += val_loss.item()
|
|
||||||
|
|
||||||
avg_val_loss = total_val_loss / max(len(val_dataloader), 1)
|
for batch in val_loader:
|
||||||
|
x_context, x_padding, freq, x_future = [t.to(self.device) for t in batch]
|
||||||
|
|
||||||
print(f"[Epoch {epoch+1}] Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}")
|
predictions = self.model(x_context, x_padding.float(), freq)
|
||||||
|
predictions_mean = predictions[..., 0]
|
||||||
|
last_patch_pred = predictions_mean[:, -1, :]
|
||||||
|
|
||||||
torch.save(model.state_dict(), "timesfm_finetuned.pth")
|
loss = self.loss_fn(last_patch_pred, x_future.squeeze(-1))
|
||||||
return model, train_dataloader, val_dataloader
|
total_loss += loss.item()
|
||||||
|
|
||||||
|
return total_loss / len(val_loader)
|
||||||
|
|
||||||
def plot_predictions(model, dataloader):
|
def finetune(self, train_dataset: Dataset, val_dataset: Dataset) -> Dict[str, Any]:
|
||||||
model.eval()
|
"""
|
||||||
with torch.no_grad():
|
Finetune the TimesFM model on the provided datasets.
|
||||||
x_context, x_padding, freq, x_future = next(iter(dataloader))
|
|
||||||
x_context, x_padding, freq, x_future = (
|
|
||||||
x_context.to(device),
|
|
||||||
x_padding.to(device),
|
|
||||||
freq.to(device),
|
|
||||||
x_future.to(device),
|
|
||||||
)
|
|
||||||
# Forward pass
|
|
||||||
predictions = model(x_context, x_padding.float(), freq)
|
|
||||||
# => [B, N, horizon_len, (1 + #quantiles)]
|
|
||||||
predictions_mean = predictions[..., 0] # => [B, N, horizon_len]
|
|
||||||
last_patch_prediction = predictions_mean[:, -1, :] # => [B, horizon_len]
|
|
||||||
|
|
||||||
# We'll plot only the first sample in the batch
|
Args:
|
||||||
i = 0
|
train_dataset: Training dataset
|
||||||
pred_vals = last_patch_prediction[i].cpu().numpy() # [horizon_len]
|
val_dataset: Validation dataset
|
||||||
context_vals = x_context[i].cpu().numpy() # [context_len]
|
|
||||||
future_vals = x_future[i].cpu().numpy() # [horizon_len]
|
|
||||||
|
|
||||||
horizon_len = future_vals.shape[0]
|
Returns:
|
||||||
context_len = context_vals.shape[0]
|
Dict containing training history and best model path
|
||||||
|
"""
|
||||||
|
self.model = self.model.to(self.device)
|
||||||
|
|
||||||
plt.figure(figsize=(10, 5))
|
train_loader = self._create_dataloader(train_dataset, "train")
|
||||||
|
val_loader = self._create_dataloader(val_dataset, "val")
|
||||||
|
|
||||||
# Plot context
|
optimizer = optim.Adam(
|
||||||
plt.plot(range(context_len), context_vals, label="Context (History)", color="blue")
|
self.model.parameters(), lr=self.config.learning_rate, weight_decay=self.config.weight_decay
|
||||||
|
|
||||||
# Plot predicted future
|
|
||||||
plt.plot(
|
|
||||||
range(context_len, context_len + horizon_len),
|
|
||||||
pred_vals,
|
|
||||||
label="Predicted Future",
|
|
||||||
color="orange",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Plot ground truth future
|
history = {"train_loss": [], "val_loss": [], "learning_rate": []}
|
||||||
plt.plot(
|
|
||||||
range(context_len, context_len + horizon_len),
|
|
||||||
future_vals,
|
|
||||||
label="Ground Truth Future",
|
|
||||||
color="green",
|
|
||||||
linestyle="--",
|
|
||||||
)
|
|
||||||
|
|
||||||
plt.xlabel("Time")
|
self.logger.info(f"Starting training for {self.config.num_epochs} epochs...")
|
||||||
plt.ylabel("Value")
|
self.logger.info(f"Training samples: {len(train_dataset)}")
|
||||||
plt.title("Model Forecast vs. Ground Truth")
|
self.logger.info(f"Validation samples: {len(val_dataset)}")
|
||||||
plt.legend()
|
|
||||||
plt.show()
|
|
||||||
plt.savefig("pic_predictions.png")
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
for epoch in range(self.config.num_epochs):
|
||||||
|
train_loss = self._train_epoch(train_loader, optimizer)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
val_loss = self._validate(val_loader)
|
||||||
# Example usage
|
|
||||||
model, train_dl, val_dl = train_model(
|
|
||||||
ticker="AAPL",
|
|
||||||
start="2012-01-01",
|
|
||||||
end="2019-01-01",
|
|
||||||
train_split=0.8,
|
|
||||||
batch_size=256,
|
|
||||||
num_epochs=50,
|
|
||||||
pretrained=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
plot_predictions(model, val_dl)
|
current_lr = optimizer.param_groups[0]["lr"]
|
||||||
|
|
||||||
|
history["train_loss"].append(train_loss)
|
||||||
|
history["val_loss"].append(val_loss)
|
||||||
|
history["learning_rate"].append(current_lr)
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
"train_loss": train_loss,
|
||||||
|
"val_loss": val_loss,
|
||||||
|
"learning_rate": current_lr,
|
||||||
|
"epoch": epoch + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.use_wandb:
|
||||||
|
wandb.log(metrics)
|
||||||
|
|
||||||
|
print(f"[Epoch {epoch+1}] Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.logger.info("Training interrupted by user")
|
||||||
|
|
||||||
|
return {"history": history}
|
||||||
|
|||||||
Reference in New Issue
Block a user