| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| from torch.utils.data import Dataset, DataLoader, Subset
|
| from datetime import datetime
|
| import pandas as pd
|
|
|
| import numpy as np
|
| from datetime import datetime, timedelta
|
| import joblib,random,os
|
| from collections import deque
|
|
|
|
|
| __all__ = [
|
| "PositionalEncoding",
|
| "LearnablePositionalEncoding",
|
| "TransformerSingleStep",
|
| "TransformerMultiStep",
|
| "EarlyStopping",
|
| "SinglestepDataset",
|
| "ReplayBuffer",
|
| "CalibratorSingleStep",
|
| "CalibratorSingleStepV3",
|
| "CalibratorMultiStep",
|
| "MultiStepDataset",
|
| "TransformerMultistepMonth",
|
| "HybridTransformerModelV4",
|
| "MultiTimeframeTransformer",
|
| "TransformerSingleStepForEA",
|
| "ConvGRUTransformerHL",
|
| "Seq2SeqAttentionHL",
|
| "Seq2SeqAttentionHLWithConv",
|
| "HybridMultiBranchMultiStepAttention",
|
| "HybridMultiBranchMultiStepAttention",
|
| "LRFinderWrapper",
|
| "ConvGRUTransformerHLV2",
|
| "RobustScalerSimple",
|
| "MinMaxScaler"
|
| ]
|
|
|
|
|
|
|
|
|
|
|
| class RobustScalerSimple:
|
| def __init__(self, quantile_range=(25,75)):
|
| self.quantile_range = quantile_range
|
| self.median_ = None
|
| self.iqr_ = None
|
|
|
| def fit(self, X):
|
| q_min, q_max = self.quantile_range
|
| self.median_ = np.median(X, axis=0)
|
| q1 = np.percentile(X, q_min, axis=0)
|
| q3 = np.percentile(X, q_max, axis=0)
|
| self.iqr_ = q3 - q1
|
|
|
| self.iqr_[self.iqr_ == 0] = 1.0
|
| return self
|
|
|
| def transform(self, X):
|
| return (X - self.median_) / self.iqr_
|
|
|
| def fit_transform(self, X):
|
| self.fit(X)
|
| return self.transform(X)
|
|
|
| def inverse_transform(self, X_scaled):
|
| return X_scaled * self.iqr_ + self.median_
|
|
|
| class MinMaxScaler:
|
| def __init__(self, feature_range=(-1,1)):
|
| self.min_val, self.max_val = feature_range
|
| self.min_ = None
|
| self.max_ = None
|
| self.scale_ = None
|
|
|
| def fit(self, X):
|
| self.min_ = X.min(axis=0)
|
| self.max_ = X.max(axis=0)
|
| self.scale_ = (self.max_val - self.min_val) / (self.max_ - self.min_ + 1e-8)
|
| return self
|
|
|
| def transform(self, X):
|
| return (X - self.min_) * self.scale_ + self.min_val
|
|
|
| def fit_transform(self, X):
|
| self.fit(X)
|
| return self.transform(X)
|
|
|
| def inverse_transform(self, X_scaled):
|
| return (X_scaled - self.min_val) / self.scale_ + self.min_
|
|
|
| class EarlyStopping:
|
| def __init__(self, patience=7, delta=0, verbose=False):
|
| self.patience = patience
|
| self.delta = delta
|
| self.verbose = verbose
|
| self.counter = 0
|
| self.best_score = None
|
| self.early_stop = False
|
| self.val_loss_min = np.Inf
|
|
|
| def __call__(self, val_loss, model, path):
|
| score = -val_loss
|
|
|
| if self.best_score is None:
|
| self.best_score = score
|
| self.save_checkpoint(val_loss, model, path)
|
| elif score < self.best_score + self.delta:
|
| self.counter += 1
|
| print(f'EarlyStopping counter: {self.counter} out of {self.patience}')
|
| if self.counter >= self.patience:
|
| self.early_stop = True
|
| else:
|
| self.best_score = score
|
| self.save_checkpoint(val_loss, model, path)
|
| self.counter = 0
|
|
|
| def save_checkpoint(self, val_loss, model, path):
|
| if self.verbose:
|
| print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}). Saving model...')
|
| torch.save(model.state_dict(), path)
|
| self.val_loss_min = val_loss
|
|
|
| class PositionalEncoding(nn.Module):
|
| def __init__(self, d_model, max_len=5000):
|
| super().__init__()
|
| pe = torch.zeros(max_len, d_model)
|
| pos = torch.arange(0, max_len).unsqueeze(1)
|
| div = torch.exp(torch.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
|
| pe[:, 0::2] = torch.sin(pos * div)
|
| pe[:, 1::2] = torch.cos(pos * div)
|
| self.pe = pe.unsqueeze(0)
|
| def forward(self, x):
|
| x = x + self.pe[:, :x.size(1)].to(x.device)
|
| return x
|
|
|
|
|
|
|
|
|
|
|
| class LearnablePositionalEncoding(nn.Module):
|
| def __init__(self, d_model, max_len=10000000):
|
| super().__init__()
|
| self.pos_embedding = nn.Embedding(max_len, d_model)
|
| self._init_weights()
|
|
|
| def _init_weights(self):
|
|
|
| nn.init.uniform_(self.pos_embedding.weight, -0.1, 0.1)
|
|
|
| def forward(self, x):
|
|
|
| seq_len = x.size(1)
|
| pos = torch.arange(0, seq_len, device=x.device).unsqueeze(0)
|
| pos_embed = self.pos_embedding(pos)
|
| return x + pos_embed
|
|
|
| class TransformerSingleStep(nn.Module):
|
| def __init__(self, input_size, d_model=64, nhead=4, num_layers=2, dropout=0.1, max_len=500):
|
| super().__init__()
|
| self.input_fc = nn.Linear(input_size, d_model)
|
| self.tanh = nn.Tanh()
|
| self.pos_encoder = LearnablePositionalEncoding(d_model, max_len=max_len)
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward=128,
|
| dropout=dropout, batch_first=True)
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
|
| self.output = nn.Linear(d_model, 2)
|
|
|
| def forward(self, x):
|
|
|
| x = self.input_fc(x)
|
| x = self.tanh(x)
|
| x = self.pos_encoder(x)
|
| x = self.transformer(x)
|
|
|
|
|
| last_output = x[:, -1, :]
|
|
|
|
|
| out = self.output(last_output)
|
| pred_high = out[:, 0].unsqueeze(1)
|
| pred_low = out[:, 1].unsqueeze(1)
|
|
|
| return pred_high, pred_low
|
|
|
| class TransformerMultiStep(nn.Module):
|
| def __init__(self, input_size, d_model=64, nhead=4, num_layers=2,
|
| dropout=0.1, horizon=5, output_dropout=0.2, max_seq_len=12):
|
| super().__init__()
|
| self.horizon = horizon
|
| self.d_model = d_model
|
|
|
|
|
| self.input_fc = nn.Linear(input_size, d_model)
|
| self.tanh = nn.Tanh()
|
|
|
|
|
| self.pos_encoding = nn.Parameter(torch.zeros(1, max_seq_len, d_model))
|
| nn.init.normal_(self.pos_encoding, mean=0.0, std=0.02)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model, nhead, dim_feedforward=128,
|
| dropout=dropout, batch_first=True
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
|
|
|
|
|
| self.output_high = nn.Sequential(
|
| nn.Linear(d_model, d_model//2),
|
| nn.Tanh(),
|
| nn.Dropout(output_dropout),
|
| nn.Linear(d_model//2, horizon)
|
| )
|
|
|
| self.output_low = nn.Sequential(
|
| nn.Linear(d_model, d_model//2),
|
| nn.Tanh(),
|
| nn.Dropout(output_dropout),
|
| nn.Linear(d_model//2, horizon)
|
| )
|
|
|
| def forward(self, x):
|
| batch_size, seq_len = x.size(0), x.size(1)
|
|
|
| x = self.input_fc(x)
|
| x = self.tanh(x)
|
|
|
|
|
| x = x + self.pos_encoding[:, :seq_len, :]
|
|
|
|
|
| mask = torch.triu(torch.ones(seq_len, seq_len) * float('-inf'), diagonal=1).to(x.device)
|
|
|
| x = self.transformer(x, mask=mask)
|
| last_output = x[:, -1, :]
|
|
|
| high_pred = self.output_high(last_output)
|
| low_pred = self.output_low(last_output)
|
|
|
| return high_pred, low_pred
|
|
|
|
|
|
|
|
|
|
|
| class ReplayBuffer:
|
| def __init__(self, capacity=200):
|
| self.capacity = capacity
|
| self.buffer = []
|
|
|
| def push(self, x, y):
|
| if len(self.buffer) >= self.capacity:
|
| self.buffer.pop(0)
|
| self.buffer.append((x.detach().clone(), y.detach().clone()))
|
|
|
| def sample(self, batch_size=5):
|
| if len(self.buffer) == 0:
|
| return None, None
|
| sample = random.sample(self.buffer, min(batch_size, len(self.buffer)))
|
| xs, ys = zip(*sample)
|
| return torch.stack(xs), torch.stack(ys)
|
|
|
|
|
|
|
|
|
|
|
| class CalibratorSingleStep:
|
| def __init__(self, method='EMA', alpha=0.3):
|
| """
|
| method: 'EMA' หรือ 'linear'
|
| alpha: smoothing factor สำหรับ EMA
|
| """
|
| self.method = method
|
| self.alpha = alpha
|
| self.history_pred_high = []
|
| self.history_pred_low = []
|
| self.history_true_high = []
|
| self.history_true_low = []
|
| self.bias_high = 0.0
|
| self.bias_low = 0.0
|
| self.model_high = None
|
| self.model_low = None
|
|
|
| def update_history(self, pred_high, pred_low, true_high, true_low):
|
| self.history_pred_high.append(pred_high)
|
| self.history_pred_low.append(pred_low)
|
| self.history_true_high.append(true_high)
|
| self.history_true_low.append(true_low)
|
|
|
| def refit(self):
|
| if self.method == 'linear':
|
| if len(self.history_pred_high) >= 2:
|
| Xh = np.array(self.history_pred_high).reshape(-1,1)
|
| Yh = np.array(self.history_true_high)
|
| self.model_high = LinearRegression().fit(Xh, Yh)
|
| if len(self.history_pred_low) >= 2:
|
| Xl = np.array(self.history_pred_low).reshape(-1,1)
|
| Yl = np.array(self.history_true_low)
|
| self.model_low = LinearRegression().fit(Xl, Yl)
|
|
|
|
|
| def calibrate(self, pred_high, pred_low):
|
| if self.method == 'EMA':
|
| ch = pred_high + self.alpha*(self.history_true_high[-1]-pred_high) if self.history_true_high else pred_high
|
| cl = pred_low + self.alpha*(self.history_true_low[-1]-pred_low) if self.history_true_low else pred_low
|
| ch += self.bias_high
|
| cl += self.bias_low
|
| elif self.method == 'linear':
|
| ch = self.model_high.predict(np.array([[pred_high]]))[0] + self.bias_high if self.model_high else pred_high
|
| cl = self.model_low.predict(np.array([[pred_low]]))[0] + self.bias_low if self.model_low else pred_low
|
| else:
|
| ch, cl = pred_high, pred_low
|
| return ch, cl
|
|
|
| def update_bias(self, pred_high_calib, pred_low_calib, true_high, true_low):
|
|
|
| self.bias_high = true_high - pred_high_calib
|
| self.bias_low = true_low - pred_low_calib
|
|
|
|
|
|
|
|
|
|
|
| class CalibratorSingleStepV3:
|
| """
|
| Bias calibration สำหรับ Online Update V3
|
| """
|
| def __init__(self, method='EMA', alpha=0.1):
|
| self.method = method
|
| self.alpha = alpha
|
| self.high_history = []
|
| self.low_history = []
|
| self.bias_high = 0.0
|
| self.bias_low = 0.0
|
|
|
| def update_history(self, pred_high, pred_low, true_high, true_low):
|
| self.high_history.append(true_high - pred_high)
|
| self.low_history.append(true_low - pred_low)
|
|
|
| def refit(self):
|
| if self.high_history:
|
| if self.method == 'EMA':
|
| self.bias_high = self.alpha * self.high_history[-1] + (1 - self.alpha) * self.bias_high
|
| self.bias_low = self.alpha * self.low_history[-1] + (1 - self.alpha) * self.bias_low
|
| elif self.method == 'mean':
|
| self.bias_high = sum(self.high_history)/len(self.high_history)
|
| self.bias_low = sum(self.low_history)/len(self.low_history)
|
|
|
| def calibrate(self, pred_high, pred_low):
|
| return pred_high + self.bias_high, pred_low + self.bias_low
|
|
|
| def update_bias(self, pred_high_calib, pred_low_calib, true_high, true_low):
|
| self.update_history(pred_high_calib, pred_low_calib, true_high, true_low)
|
| self.refit()
|
|
|
|
|
|
|
|
|
|
|
| class SinglestepDataset(Dataset):
|
| def __init__(self, X, y_high, y_low, lookback=4):
|
| """
|
| Dataset สำหรับ Time Series Forecasting แบบ Single-step (Rolling window)
|
| """
|
| self.X = torch.tensor(X, dtype=torch.float32)
|
| self.y_high = torch.tensor(y_high, dtype=torch.float32).squeeze(-1)
|
| self.y_low = torch.tensor(y_low, dtype=torch.float32).squeeze(-1)
|
| self.lookback = lookback
|
|
|
|
|
| self.max_idx = len(self.X) - lookback
|
|
|
| def __len__(self):
|
| return self.max_idx
|
|
|
| def __getitem__(self, idx):
|
| x_seq = self.X[idx: idx + self.lookback]
|
| y_h = self.y_high[idx + self.lookback].unsqueeze(0)
|
| y_l = self.y_low[idx + self.lookback].unsqueeze(0)
|
| return x_seq, y_h, y_l
|
|
|
|
|
|
|
|
|
|
|
| class MultiStepDataset(Dataset):
|
| def __init__(self, X, y_high, y_low, lookback=4, horizon=5):
|
| """
|
| X : np.ndarray or torch.Tensor, shape (N, features)
|
| y_high : np.ndarray or torch.Tensor, shape (N,)
|
| y_low : np.ndarray or torch.Tensor, shape (N,)
|
| lookback: จำนวน step ที่ใช้เป็น input
|
| horizon : จำนวน step ข้างหน้าที่ต้องการทำนาย
|
| """
|
| self.X = torch.tensor(X, dtype=torch.float32)
|
| self.y_high = torch.tensor(y_high, dtype=torch.float32)
|
| self.y_low = torch.tensor(y_low, dtype=torch.float32)
|
| self.lookback = lookback
|
| self.horizon = horizon
|
|
|
|
|
| self.max_idx = len(self.X) - lookback - horizon + 1
|
|
|
| def __len__(self):
|
| return self.max_idx
|
|
|
|
|
| def __getitem__(self, idx):
|
| x_seq = self.X[idx: idx + self.lookback]
|
| y_h_seq = self.y_high[idx + self.lookback : idx + self.lookback + self.horizon].squeeze(-1)
|
| y_l_seq = self.y_low[idx + self.lookback : idx + self.lookback + self.horizon].squeeze(-1)
|
| return x_seq, y_h_seq, y_l_seq
|
|
|
| class CalibratorMultiStep:
|
| """
|
| Calibrator สำหรับ multistep/recursive prediction
|
| - เก็บ history ของ high/low
|
| - ปรับ bias ให้ step แรก match last_close
|
| - สามารถ smooth prediction ด้วย Gaussian filter
|
| """
|
| def __init__(self, last_close=None, max_history=50, sigma=1.5):
|
| self.last_close = last_close
|
| self.history_high = deque(maxlen=max_history)
|
| self.history_low = deque(maxlen=max_history)
|
| self.sigma = sigma
|
|
|
| def update_history(self, raw_high, raw_low, calibrated_high=None, calibrated_low=None):
|
| """
|
| เก็บ history ของ step ล่าสุด
|
| - calibrated_high/low: optional สำหรับเก็บ version ปรับแล้ว
|
| """
|
| self.history_high.append(calibrated_high if calibrated_high is not None else raw_high)
|
| self.history_low.append(calibrated_low if calibrated_low is not None else raw_low)
|
|
|
| def refit(self):
|
| """
|
| ปรับค่าภายในถ้าจำเป็น (placeholder)
|
| - future: สามารถปรับ weight history, scale หรือ smoothing factor
|
| """
|
| pass
|
|
|
| def calibrate(self, raw_high, raw_low):
|
| """
|
| ปรับค่า bias ให้ step แรกตรง last_close
|
| - สำหรับ step ต่อไปจะ smooth ด้วย history
|
| """
|
| h, l = raw_high, raw_low
|
|
|
|
|
| if self.last_close is not None and len(self.history_high) == 0:
|
| delta_h = h - self.last_close
|
| delta_l = l - self.last_close
|
| h -= delta_h
|
| l -= delta_l
|
|
|
|
|
| if len(self.history_high) > 1:
|
| h_array = np.array(list(self.history_high) + [h])
|
| l_array = np.array(list(self.history_low) + [l])
|
| h_smooth = gaussian_filter1d(h_array, sigma=self.sigma)[-1]
|
| l_smooth = gaussian_filter1d(l_array, sigma=self.sigma)[-1]
|
| h, l = h_smooth, l_smooth
|
|
|
| return h, l
|
|
|
| class TransformerMultistepMonth(nn.Module):
|
| def __init__(self, input_size, d_model=64, nhead=4, num_layers=2,
|
| dropout=0.1, horizon=4, conv_dropout=0.1, conv_kernel=2,
|
| post_trans_dropout=0.1, output_dropout=0.1, dim_feedforward=128):
|
| super().__init__()
|
| self.horizon = horizon
|
|
|
|
|
| self.causal_pad = nn.ConstantPad1d((conv_kernel - 1, 0), 0)
|
| self.conv1d = nn.Conv1d(in_channels=input_size, out_channels=d_model, kernel_size=conv_kernel, padding=0)
|
| self.tanh = nn.Tanh()
|
| self.norm1 = nn.LayerNorm(d_model)
|
| self.dropout1 = nn.Dropout(conv_dropout)
|
|
|
|
|
| self.pos_encoder = LearnablePositionalEncoding(d_model, max_len=200)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead,
|
| dim_feedforward=dim_feedforward,
|
| dropout=dropout, batch_first=True)
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
| self.post_trans_dropout = nn.Dropout(post_trans_dropout)
|
|
|
|
|
| self.output_dropout_high = nn.Dropout(output_dropout)
|
| self.output_dropout_low = nn.Dropout(output_dropout)
|
| self.output_high = nn.Linear(d_model, horizon)
|
| self.output_low = nn.Linear(d_model, horizon)
|
|
|
| def forward(self, x):
|
| x = x.permute(0, 2, 1)
|
| x = self.causal_pad(x)
|
| x = self.conv1d(x)
|
| x = self.tanh(x)
|
| x = x.permute(0, 2, 1)
|
| x = self.norm1(x)
|
| x = self.dropout1(x)
|
|
|
| x = self.pos_encoder(x)
|
| x = self.transformer(x)
|
| x = self.post_trans_dropout(x)
|
|
|
|
|
| x_last = x[:, -1, :]
|
| out_high = self.output_dropout_high(x_last)
|
| out_low = self.output_dropout_low(x_last)
|
|
|
| return self.output_high(out_high), self.output_low(out_low)
|
|
|
| class LRFinderWrapper(nn.Module):
|
| def __init__(self, model):
|
| super().__init__()
|
| self.model = model
|
| def forward(self, x):
|
| out_high, out_low = self.model(x)
|
| out = (out_high + out_low) / 2.0
|
| return out.squeeze(-1)
|
|
|
|
|
| class HybridTransformerModelV4(nn.Module):
|
| def __init__(self, input_size, seq_len,
|
| d_model=32, nhead=2, num_layers=1,
|
| lstm_hidden=32, cnn_out=16, dropout=0.1):
|
| super().__init__()
|
|
|
|
|
| self.cnn = CausalConv1d(in_channels=input_size, out_channels=cnn_out, kernel_size=3)
|
| self.cnn_bn = nn.BatchNorm1d(cnn_out)
|
| self.cnn_dropout = nn.Dropout(dropout)
|
|
|
|
|
| self.lstm = nn.LSTM(input_size, lstm_hidden, batch_first=True)
|
| self.lstm_dropout = nn.Dropout(dropout)
|
|
|
|
|
| self.input_projection = nn.Linear(input_size, d_model)
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=d_model, nhead=nhead, batch_first=True,
|
| dropout=dropout, activation='gelu'
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
|
| self.trans_ln = nn.LayerNorm(d_model)
|
|
|
|
|
| fusion_size = cnn_out + lstm_hidden + d_model
|
| self.fc1 = nn.Linear(fusion_size, 64)
|
| self.fc1_ln = nn.LayerNorm(64)
|
| self.fc1_dropout = nn.Dropout(dropout)
|
| self.fc2 = nn.Linear(64, 2)
|
|
|
| def forward(self, x):
|
|
|
| cnn_x = x.transpose(1, 2)
|
| cnn_x = F.leaky_relu(self.cnn_bn(self.cnn(cnn_x)), 0.01)
|
| cnn_x = self.cnn_dropout(torch.mean(cnn_x, dim=2))
|
|
|
|
|
| lstm_out, _ = self.lstm(x)
|
| lstm_x = self.lstm_dropout(lstm_out[:, -1, :])
|
|
|
|
|
| trans_x = self.input_projection(x)
|
| trans_x = torch.mean(self.transformer(trans_x), dim=1)
|
| trans_x = self.trans_ln(trans_x)
|
|
|
|
|
| fused = torch.cat([cnn_x, lstm_x, trans_x], dim=1)
|
| out = F.selu(self.fc1_ln(self.fc1(fused)))
|
| out = self.fc1_dropout(out)
|
| out = self.fc2(out)
|
| return out
|
|
|
| class MultiTimeframeTransformer(nn.Module):
|
| """
|
| Multi-Timeframe Transformer:
|
| - 3 branches: Day / Week / Month
|
| - Residual connection ในแต่ละ branch + Fusion
|
| - Dropout + LayerNorm แทน BatchNorm1d เพื่อ train batch=1 ได้
|
| """
|
| def __init__(self, input_size, seq_len_list=[5,4,3], d_model=32, nhead=2, num_layers=2, dropout=0.1):
|
| super().__init__()
|
| self.seq_len_day, self.seq_len_week, self.seq_len_month = seq_len_list
|
|
|
|
|
| self.input_proj_day = nn.Sequential(nn.Linear(input_size, d_model), nn.Dropout(dropout))
|
| self.input_proj_week = nn.Sequential(nn.Linear(input_size, d_model), nn.Dropout(dropout))
|
| self.input_proj_month = nn.Sequential(nn.Linear(input_size, d_model), nn.Dropout(dropout))
|
|
|
|
|
| self.pos_day = LearnablePositionalEncoding(d_model, max_len=self.seq_len_day)
|
| self.pos_week = LearnablePositionalEncoding(d_model, max_len=self.seq_len_week)
|
| self.pos_month = LearnablePositionalEncoding(d_model, max_len=self.seq_len_month)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=d_model, nhead=nhead, dropout=dropout, batch_first=True, activation='gelu'
|
| )
|
| self.trans_day = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
| self.trans_week = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
| self.trans_month = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
|
|
|
|
| self.ln_day = nn.LayerNorm(d_model)
|
| self.ln_week = nn.LayerNorm(d_model)
|
| self.ln_month = nn.LayerNorm(d_model)
|
|
|
|
|
| fusion_size = d_model * 3
|
| self.ln_fusion = nn.LayerNorm(fusion_size)
|
| self.dropout_fusion = nn.Dropout(dropout)
|
| self.fc1 = nn.Linear(fusion_size, 64)
|
| self.fc1_ln = nn.LayerNorm(64)
|
| self.fc2 = nn.Linear(64, 2)
|
|
|
| def forward_branch(self, x_proj, x_trans, ln):
|
| """
|
| x_proj: (B, seq_len, d_model)
|
| x_trans: Transformer Encoder
|
| ln: LayerNorm
|
| """
|
|
|
| res = x_proj
|
| out = x_trans(x_proj)
|
| out = out + res
|
| out = ln(out)
|
| out = torch.mean(out, dim=1)
|
| return out
|
|
|
| def forward(self, x_day, x_week, x_month):
|
|
|
| day_proj = self.pos_day(self.input_proj_day(x_day))
|
| week_proj = self.pos_week(self.input_proj_week(x_week))
|
| month_proj = self.pos_month(self.input_proj_month(x_month))
|
|
|
|
|
| day_feat = self.forward_branch(day_proj, self.trans_day, self.ln_day)
|
| week_feat = self.forward_branch(week_proj, self.trans_week, self.ln_week)
|
| month_feat = self.forward_branch(month_proj, self.trans_month, self.ln_month)
|
|
|
|
|
| fused = torch.cat([day_feat, week_feat, month_feat], dim=1)
|
| fused_res = fused
|
| fused = self.ln_fusion(fused)
|
| fused = self.dropout_fusion(fused)
|
| fused = fused + fused_res
|
|
|
|
|
| out = F.selu(self.fc1_ln(self.fc1(fused)))
|
| out = self.fc2(out)
|
| return out
|
|
|
| class NormalizationLayer(nn.Module):
|
| """
|
| ใช้ normalize feature ด้วย mean/std ของ training data
|
| mean/std ต้องเป็น list/array ตามจำนวน feature
|
| """
|
| def __init__(self, mean, std):
|
| super().__init__()
|
| self.register_buffer("mean", torch.tensor(mean, dtype=torch.float32))
|
| self.register_buffer("std", torch.tensor(std, dtype=torch.float32))
|
|
|
| def forward(self, x):
|
|
|
| return (x - self.mean) / self.std
|
|
|
| class TransformerSingleStepForEA(nn.Module):
|
| def __init__(self, input_size, mean, std, d_model=64, nhead=4, num_layers=2,
|
| dropout=0.1, max_len=500):
|
| super().__init__()
|
|
|
| self.norm_layer = NormalizationLayer(mean, std)
|
|
|
|
|
| self.input_fc = nn.Linear(input_size, d_model)
|
| self.tanh = nn.Tanh()
|
| self.pos_encoder = LearnablePositionalEncoding(d_model, max_len=max_len)
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=d_model,
|
| nhead=nhead,
|
| dim_feedforward=128,
|
| dropout=dropout,
|
| batch_first=True
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
| self.global_pool = nn.AdaptiveAvgPool1d(1)
|
| self.output = nn.Linear(d_model, 2)
|
|
|
| def forward(self, x):
|
|
|
| x = self.norm_layer(x)
|
| x = self.input_fc(x)
|
| x = self.tanh(x)
|
| x = self.pos_encoder(x)
|
| x = self.transformer(x)
|
| x = x.permute(0, 2, 1)
|
| x = self.global_pool(x).squeeze(2)
|
| return self.output(x).unsqueeze(1)
|
|
|
| class PositionEncodeLearnable(nn.Module):
|
| def __init__(self, seq_len, d_model):
|
| super().__init__()
|
| self.pos_embedding = nn.Parameter(torch.randn(1, seq_len, d_model))
|
|
|
| def forward(self, x):
|
| return x + self.pos_embedding
|
|
|
| class ConvGRUTransformerHL(nn.Module):
|
| def __init__(self,
|
| input_dim,
|
| conv_channels=32,
|
| gru_hidden=64,
|
| nhead=4,
|
| num_encoder_layers=1,
|
| dim_feedforward=128,
|
| seq_len=3,
|
| kernel_size=3,
|
| dropout=0.1,
|
| output_steps=1
|
| ):
|
| super().__init__()
|
| self.seq_len = seq_len
|
| self.kernel_size = kernel_size
|
| self.output_steps = output_steps
|
|
|
|
|
| self.conv1 = nn.Conv1d(
|
| in_channels=input_dim,
|
| out_channels=conv_channels,
|
| kernel_size=self.kernel_size,
|
| dilation=1,
|
| padding=0
|
| )
|
| self.conv_bn = nn.BatchNorm1d(conv_channels)
|
|
|
|
|
| self.gru = nn.GRU(
|
| input_size=conv_channels,
|
| hidden_size=gru_hidden,
|
| batch_first=True
|
| )
|
|
|
|
|
| self.pos_encoder = PositionEncodeLearnable(seq_len, gru_hidden)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=gru_hidden,
|
| nhead=nhead,
|
| dim_feedforward=dim_feedforward,
|
| dropout=dropout,
|
| batch_first=True,
|
| activation="gelu"
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
|
|
|
|
|
| self.fc_high = nn.Linear(gru_hidden, output_steps)
|
| self.fc_low = nn.Linear(gru_hidden, output_steps)
|
|
|
| def forward(self, x):
|
|
|
| batch_size, seq_len, features = x.size()
|
|
|
|
|
| x_conv = x.permute(0, 2, 1)
|
|
|
|
|
| pad = self.kernel_size - 1
|
| x_conv = F.pad(x_conv, (pad, 0))
|
|
|
| x_conv = self.conv1(x_conv)
|
| x_conv = self.conv_bn(x_conv)
|
| x_conv = torch.tanh(x_conv)
|
|
|
|
|
| x_conv = x_conv[:, :, -seq_len:]
|
| x_conv = x_conv.permute(0, 2, 1)
|
|
|
|
|
| gru_out, _ = self.gru(x_conv)
|
|
|
|
|
| if gru_out.size(1) != self.seq_len:
|
| current_seq_len = gru_out.size(1)
|
| if current_seq_len < self.seq_len:
|
| pad_size = self.seq_len - current_seq_len
|
| gru_out = F.pad(gru_out, (0, 0, 0, pad_size))
|
| else:
|
| gru_out = gru_out[:, :self.seq_len, :]
|
|
|
|
|
| gru_out = self.pos_encoder(gru_out)
|
|
|
|
|
| trans_out = self.transformer(gru_out)
|
|
|
|
|
| last_steps = trans_out[:, -self.output_steps:, :]
|
|
|
| high_out = self.fc_high(last_steps)
|
| low_out = self.fc_low(last_steps)
|
|
|
| return high_out.squeeze(-1), low_out.squeeze(-1)
|
|
|
|
|
| class ConvGRUTransformerHLV2(nn.Module):
|
| def __init__(self, input_dim, conv_channels=32, gru_hidden=64,
|
| nhead=4, num_encoder_layers=1, dim_feedforward=128,
|
| seq_len=3, kernel_size=3, dropout=0.1, output_steps=1):
|
| super().__init__()
|
| self.seq_len = seq_len
|
| self.kernel_size = kernel_size
|
| self.output_steps = output_steps
|
|
|
|
|
| self.conv1 = nn.Conv1d(
|
| in_channels=input_dim,
|
| out_channels=conv_channels,
|
| kernel_size=self.kernel_size,
|
| dilation=1,
|
| padding=0
|
| )
|
| self.conv_bn = nn.BatchNorm1d(conv_channels)
|
|
|
|
|
| self.gru = nn.GRU(
|
| input_size=conv_channels,
|
| hidden_size=gru_hidden,
|
| batch_first=True
|
| )
|
|
|
|
|
| self.pos_encoder = PositionEncodeLearnable(seq_len, gru_hidden)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=gru_hidden,
|
| nhead=nhead,
|
| dim_feedforward=dim_feedforward,
|
| dropout=dropout,
|
| batch_first=True,
|
| activation="gelu"
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
|
|
|
|
|
| self.fc_high = nn.Linear(gru_hidden, output_steps)
|
| self.fc_low = nn.Linear(gru_hidden, output_steps)
|
|
|
|
|
| self.bias_high = nn.Parameter(torch.zeros(1))
|
| self.bias_low = nn.Parameter(torch.zeros(1))
|
|
|
| def forward(self, x):
|
|
|
| batch_size, seq_len, features = x.size()
|
|
|
|
|
| x_conv = x.permute(0, 2, 1)
|
| pad = self.kernel_size - 1
|
| x_conv = F.pad(x_conv, (pad, 0))
|
| x_conv = self.conv1(x_conv)
|
| x_conv = self.conv_bn(x_conv)
|
| x_conv = torch.tanh(x_conv)
|
| x_conv = x_conv[:, :, -seq_len:]
|
| x_conv = x_conv.permute(0, 2, 1)
|
|
|
|
|
| gru_out, _ = self.gru(x_conv)
|
|
|
|
|
| if gru_out.size(1) != self.seq_len:
|
| current_seq_len = gru_out.size(1)
|
| if current_seq_len < self.seq_len:
|
| pad_size = self.seq_len - current_seq_len
|
| gru_out = F.pad(gru_out, (0, 0, 0, pad_size))
|
| else:
|
| gru_out = gru_out[:, :self.seq_len, :]
|
|
|
|
|
| gru_out = self.pos_encoder(gru_out)
|
|
|
|
|
| trans_out = self.transformer(gru_out)
|
|
|
|
|
| last_steps = trans_out[:, -self.output_steps:, :]
|
| high_out = self.fc_high(last_steps) + self.bias_high
|
| low_out = self.fc_low(last_steps) + self.bias_low
|
|
|
| return high_out.squeeze(-1), low_out.squeeze(-1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| def ensure_batch_dim(x):
|
| if x.dim() == 2:
|
| x = x.unsqueeze(0)
|
| return x
|
|
|
|
|
|
|
|
|
| class Seq2SeqAttentionHL(torch.nn.Module):
|
| def __init__(self, input_size, hidden_size, num_layers=1, dropout=0.2, device='cpu'):
|
| super().__init__()
|
| self.hidden_size = hidden_size
|
| self.num_layers = num_layers
|
| self.device = device
|
| self.encoder = torch.nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0)
|
| self.decoder = torch.nn.LSTM(2, hidden_size, num_layers, batch_first=True, dropout=0)
|
| self.fc_high = torch.nn.Linear(hidden_size, 1)
|
| self.fc_low = torch.nn.Linear(hidden_size, 1)
|
| self.dropout = torch.nn.Dropout(dropout)
|
| self.attention = torch.nn.Linear(hidden_size * 2, 1)
|
|
|
| def forward(self, src, horizon=4, tgt=None, teacher_forcing_ratio=0.5):
|
| src = ensure_batch_dim(src)
|
| batch_size, seq_len, _ = src.shape
|
|
|
|
|
| encoder_outputs, (hidden, cell) = self.encoder(src)
|
| encoder_outputs = self.dropout(encoder_outputs)
|
|
|
|
|
| decoder_input = src[:, -1, :2].unsqueeze(1)
|
| out_high_seq = torch.zeros(batch_size, horizon, 1, device=self.device)
|
| out_low_seq = torch.zeros(batch_size, horizon, 1, device=self.device)
|
|
|
| for t in range(horizon):
|
|
|
| decoder_output, (hidden, cell) = self.decoder(decoder_input, (hidden, cell))
|
|
|
|
|
| decoder_output_expanded = decoder_output.expand(-1, seq_len, -1)
|
| attention_scores = torch.tanh(self.attention(
|
| torch.cat([decoder_output_expanded, encoder_outputs], dim=-1)
|
| ))
|
| attention_weights = torch.softmax(attention_scores, dim=1)
|
| context = torch.sum(attention_weights * encoder_outputs, dim=1, keepdim=True)
|
|
|
|
|
| combined = decoder_output + context
|
|
|
|
|
| high = torch.tanh(self.fc_high(combined))
|
| low = torch.tanh(self.fc_low(combined))
|
|
|
| out_high_seq[:, t:t+1] = high
|
| out_low_seq[:, t:t+1] = low
|
|
|
|
|
| if tgt is not None and torch.rand(1) < teacher_forcing_ratio:
|
| decoder_input = tgt[:, t:t+1, :2]
|
| else:
|
| decoder_input = torch.cat([high, low], dim=-1)
|
|
|
| return out_high_seq, out_low_seq
|
|
|
| class CausalConvBlockSafe(nn.Module):
|
| def __init__(self, input_size, dropout=0.1):
|
| super().__init__()
|
| self.kernel_size1 = 4
|
| self.kernel_size2 = 4
|
|
|
| self.conv1 = nn.Sequential(
|
| nn.ConstantPad1d((self.kernel_size1-1, 0), 0),
|
| nn.Conv1d(input_size, 128, kernel_size=self.kernel_size1),
|
| nn.BatchNorm1d(128),
|
| nn.Tanh(),
|
| nn.Dropout(dropout)
|
| )
|
| self.conv2 = nn.Sequential(
|
| nn.ConstantPad1d((self.kernel_size2-1, 0), 0),
|
| nn.Conv1d(128, 64, kernel_size=self.kernel_size2),
|
| nn.BatchNorm1d(64),
|
| nn.Tanh(),
|
| nn.Dropout(dropout)
|
| )
|
| self.layernorm = nn.LayerNorm(64)
|
|
|
| def forward(self, x):
|
| conv_out = self.conv1(x)
|
| conv_out = self.conv2(conv_out)
|
| seq_len_in = x.size(2)
|
| seq_len_out = conv_out.size(2)
|
| if seq_len_out < seq_len_in:
|
| raise ValueError(f"Conv output shorter than input! {seq_len_out} < {seq_len_in}")
|
| conv_out = conv_out[:, :, -seq_len_in:]
|
| conv_out = conv_out.transpose(1,2)
|
| conv_out = self.layernorm(conv_out)
|
| return conv_out.transpose(1,2)
|
|
|
| class Seq2SeqAttentionHLWithConv(nn.Module):
|
| def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.2, device='cpu'):
|
| super().__init__()
|
| self.hidden_size = hidden_size
|
| self.device = device
|
|
|
|
|
| self.conv_block = CausalConvBlockSafe(input_size, dropout=dropout)
|
|
|
|
|
| self.encoder = nn.LSTM(64, hidden_size, num_layers=num_layers,
|
| batch_first=True, dropout=dropout if num_layers > 1 else 0.0)
|
| self.decoder = nn.LSTM(2, hidden_size, num_layers=num_layers,
|
| batch_first=True, dropout=dropout if num_layers > 1 else 0.0)
|
|
|
|
|
| self.layernorm_enc = nn.LayerNorm(hidden_size)
|
| self.layernorm_dec = nn.LayerNorm(hidden_size)
|
|
|
|
|
| self.attention = nn.MultiheadAttention(hidden_size, num_heads=4,
|
| dropout=dropout, batch_first=True)
|
| self.layernorm_attn = nn.LayerNorm(hidden_size)
|
|
|
|
|
| self.fc_high = nn.Sequential(
|
| nn.Linear(hidden_size, 32),
|
| nn.Tanh(),
|
| nn.Dropout(dropout),
|
| nn.Linear(32, 1),
|
| nn.Tanh()
|
| )
|
|
|
| self.fc_low = nn.Sequential(
|
| nn.Linear(hidden_size, 32),
|
| nn.Tanh(),
|
| nn.Dropout(dropout),
|
| nn.Linear(32, 1),
|
| nn.Tanh()
|
| )
|
|
|
| def forward(self, src, horizon=4, tgt=None, teacher_forcing_ratio=1):
|
| batch_size, seq_len, _ = src.shape
|
|
|
|
|
| src_conv = src.transpose(1, 2)
|
| conv_out = self.conv_block(src_conv)
|
| conv_out = conv_out.transpose(1, 2)
|
|
|
|
|
| encoder_outputs, (hidden, cell) = self.encoder(conv_out)
|
| encoder_outputs = self.layernorm_enc(encoder_outputs)
|
|
|
|
|
| decoder_input = src[:, -1, :2].unsqueeze(1)
|
| outputs_high, outputs_low = [], []
|
|
|
| for t in range(horizon):
|
| decoder_out, (hidden, cell) = self.decoder(decoder_input, (hidden, cell))
|
| decoder_out = self.layernorm_dec(decoder_out)
|
|
|
|
|
| attn_output, _ = self.attention(decoder_out, encoder_outputs, encoder_outputs)
|
| attn_output = self.layernorm_attn(attn_output + decoder_out)
|
|
|
|
|
| high = self.fc_high(attn_output)
|
| low = self.fc_low(attn_output)
|
|
|
| outputs_high.append(high)
|
| outputs_low.append(low)
|
|
|
|
|
| if tgt is not None and torch.rand(1) < teacher_forcing_ratio:
|
| decoder_input = tgt[:, t:t+1, :2]
|
| else:
|
| decoder_input = torch.cat([high, low], dim=-1)
|
|
|
| return torch.cat(outputs_high, dim=1), torch.cat(outputs_low, dim=1)
|
|
|
|
|
|
|
| class CausalConv1d(nn.Conv1d):
|
| def __init__(self, in_channels, out_channels, kernel_size, **kwargs):
|
| super().__init__(in_channels, out_channels, kernel_size, **kwargs)
|
| self.left_padding = kernel_size - 1
|
|
|
| def forward(self, x):
|
| x = F.pad(x, (self.left_padding, 0))
|
| return super().forward(x)
|
|
|
|
|
|
|
|
|
| class CausalConv1d(nn.Module):
|
| def __init__(self, in_channels, out_channels, kernel_size, dropout=0.1):
|
| super().__init__()
|
| self.padding = kernel_size - 1
|
| self.conv = nn.Conv1d(in_channels, out_channels, kernel_size)
|
| self.dropout = nn.Dropout(dropout)
|
| self.ln = nn.LayerNorm(out_channels)
|
|
|
| def forward(self, x):
|
|
|
| x = x.permute(0, 2, 1)
|
| x = F.pad(x, (self.padding, 0))
|
| x = self.conv(x)
|
| x = x.permute(0, 2, 1)
|
| x = self.dropout(x)
|
| x = self.ln(x)
|
| return torch.tanh(x)
|
|
|
|
|
|
|
|
|
| class Seq2SeqBranch(nn.Module):
|
| def __init__(self, in_channels, cnn_channels=32, lstm_hidden=64, kernel_size=3, dropout=0.1):
|
| super().__init__()
|
| self.cnn = CausalConv1d(in_channels, cnn_channels, kernel_size, dropout)
|
| self.lstm = nn.LSTM(cnn_channels, lstm_hidden, batch_first=True)
|
| self.pos_embed = nn.Embedding(1000, cnn_channels)
|
|
|
| def forward(self, x):
|
| x = self.cnn(x)
|
| batch_size, seq_len, _ = x.shape
|
| positions = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch_size, -1)
|
| x = x + self.pos_embed(positions)
|
| output, _ = self.lstm(x)
|
| return output
|
|
|
|
|
|
|
|
|
| class HybridMultiBranchMultiStepAttention(nn.Module):
|
| def __init__(self, horizon=4, cnn_channels=32, lstm_hidden=64, num_heads=4, dropout=0.1):
|
| super().__init__()
|
| self.horizon = horizon
|
| self.lstm_hidden = lstm_hidden
|
|
|
|
|
| self.branches = nn.ModuleList([
|
| Seq2SeqBranch(4, cnn_channels, lstm_hidden, dropout=dropout),
|
| Seq2SeqBranch(1, cnn_channels, lstm_hidden, dropout=dropout),
|
| Seq2SeqBranch(1, cnn_channels, lstm_hidden, dropout=dropout),
|
| Seq2SeqBranch(1, cnn_channels, lstm_hidden, dropout=dropout),
|
| ])
|
|
|
|
|
| self.multihead = nn.MultiheadAttention(
|
| embed_dim=lstm_hidden,
|
| num_heads=num_heads,
|
| batch_first=True,
|
| dropout=dropout
|
| )
|
|
|
| self.final_ln = nn.LayerNorm(lstm_hidden)
|
|
|
|
|
| self.decoder_lstm = nn.LSTM(lstm_hidden, lstm_hidden, batch_first=True)
|
|
|
|
|
| self.fc_high = nn.Linear(lstm_hidden, 1)
|
| self.fc_low = nn.Linear(lstm_hidden, 1)
|
|
|
| def forward(self, x):
|
|
|
| head_inputs = [
|
| x[:, :, 0:4],
|
| x[:, :, 4:5],
|
| x[:, :, 5:6],
|
| x[:, :, 6:7]
|
| ]
|
|
|
|
|
| branch_outs = [branch(h) for branch, h in zip(self.branches, head_inputs)]
|
|
|
|
|
| branch_stack = torch.stack(branch_outs, dim=-1)
|
|
|
|
|
| branch_merged = branch_stack.mean(dim=-1)
|
|
|
|
|
| attn_out, _ = self.multihead(branch_merged, branch_merged, branch_merged)
|
|
|
|
|
| context = attn_out[:, -1:, :]
|
|
|
|
|
| decoder_input = context.repeat(1, self.horizon, 1)
|
|
|
|
|
| decoder_out, _ = self.decoder_lstm(decoder_input)
|
| decoder_out = self.final_ln(decoder_out)
|
|
|
|
|
|
|
| batch, horizon, hidden = decoder_out.shape
|
| decoder_flat = decoder_out.reshape(batch*horizon, hidden)
|
| out_high_flat = self.fc_high(decoder_flat)
|
| out_low_flat = self.fc_low(decoder_flat)
|
|
|
| out_high = out_high_flat.reshape(batch, horizon)
|
| out_low = out_low_flat.reshape(batch, horizon)
|
|
|
| return out_high, out_low
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|