|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from sklearn.preprocessing import MinMaxScaler |
|
|
from tensorflow.keras.models import Sequential, Model |
|
|
from tensorflow.keras.layers import LSTM, Dense, Dropout, Input |
|
|
from tensorflow.keras.optimizers import Adam |
|
|
|
|
|
|
|
|
|
|
|
def preprocess_data_for_transfer(df: pd.DataFrame, look_back: int = 60, features_cols=['Close'], target_col='Close'): |
|
|
""" |
|
|
Prepares data for a deep learning model, similar to deep_mql_model. |
|
|
""" |
|
|
df_copy = df.copy() |
|
|
if 'returns' not in df_copy.columns and 'Close' in df_copy.columns: |
|
|
df_copy['returns'] = df_copy['Close'].pct_change() |
|
|
|
|
|
|
|
|
all_cols = list(set(features_cols + [target_col] + (['returns'] if 'returns' in df_copy.columns else []))) |
|
|
df_copy = df_copy[all_cols].dropna() |
|
|
|
|
|
if df_copy.empty: |
|
|
return np.array([]), np.array([]), None, [] |
|
|
|
|
|
data_to_scale = df_copy.values |
|
|
|
|
|
scaler = MinMaxScaler(feature_range=(0, 1)) |
|
|
scaled_data = scaler.fit_transform(data_to_scale) |
|
|
|
|
|
target_idx_in_scaled = df_copy.columns.tolist().index(target_col) |
|
|
|
|
|
X, y = [], [] |
|
|
for i in range(look_back, len(scaled_data)): |
|
|
X.append(scaled_data[i-look_back:i]) |
|
|
y.append(scaled_data[i, target_idx_in_scaled]) |
|
|
|
|
|
return np.array(X), np.array(y), scaler, df_copy.columns.tolist() |
|
|
|
|
|
def create_base_model(input_shape, base_model_type='lstm', units1=50, units2=50, dropout_rate=0.2): |
|
|
""" |
|
|
Creates a base model architecture for pre-training or as part of transfer learning. |
|
|
""" |
|
|
if base_model_type == 'lstm': |
|
|
model = Sequential([ |
|
|
LSTM(units1, return_sequences=True, input_shape=input_shape, name="base_lstm_1"), |
|
|
Dropout(dropout_rate, name="base_dropout_1"), |
|
|
LSTM(units2, return_sequences=False, name="base_lstm_2"), |
|
|
Dropout(dropout_rate, name="base_dropout_2"), |
|
|
Dense(25, activation='relu', name="base_dense_1") |
|
|
], name="base_model") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
raise ValueError(f"Unsupported base_model_type: {base_model_type}") |
|
|
|
|
|
|
|
|
return model |
|
|
|
|
|
def adapt_model_for_transfer(base_model: Model, num_classes_new_task=1, learning_rate=0.001): |
|
|
""" |
|
|
Adapts a pre-trained base model for a new task. |
|
|
- Freezes base model layers. |
|
|
- Adds new classification/regression head. |
|
|
- Compiles the new model. |
|
|
""" |
|
|
|
|
|
base_model.trainable = False |
|
|
|
|
|
|
|
|
inputs = Input(shape=base_model.input_shape[1:]) |
|
|
x = base_model(inputs, training=False) |
|
|
|
|
|
x = Dense(128, activation='relu', name="transfer_dense_1")(x) |
|
|
x = Dropout(0.3, name="transfer_dropout_1")(x) |
|
|
outputs = Dense(num_classes_new_task, activation='linear' if num_classes_new_task == 1 else 'softmax', name="transfer_output")(x) |
|
|
|
|
|
adapted_model = Model(inputs, outputs, name="adapted_transfer_model") |
|
|
|
|
|
adapted_model.compile(optimizer=Adam(learning_rate=learning_rate), |
|
|
loss='mean_squared_error' if num_classes_new_task == 1 else 'categorical_crossentropy', |
|
|
metrics=['mean_absolute_error'] if num_classes_new_task == 1 else ['accuracy']) |
|
|
return adapted_model |
|
|
|
|
|
def fine_tune_model(model: Model, X_train, y_train, X_val, y_val, unfreeze_at_layer_name=None, fine_tune_lr=1e-5, epochs=10, batch_size=32): |
|
|
""" |
|
|
Fine-tunes the model. |
|
|
- Optionally unfreezes some layers of the base model. |
|
|
- Re-compiles with a lower learning rate. |
|
|
- Continues training. |
|
|
""" |
|
|
if unfreeze_at_layer_name: |
|
|
model.trainable = True |
|
|
|
|
|
|
|
|
for layer in model.get_layer('base_model').layers: |
|
|
if layer.name == unfreeze_at_layer_name: |
|
|
break |
|
|
layer.trainable = False |
|
|
else: |
|
|
|
|
|
|
|
|
|
|
|
if 'base_model' in [l.name for l in model.layers]: |
|
|
model.get_layer('base_model').trainable = True |
|
|
print("Unfrozen all layers in 'base_model' for fine-tuning.") |
|
|
|
|
|
|
|
|
model.compile(optimizer=Adam(learning_rate=fine_tune_lr), |
|
|
loss=model.loss, |
|
|
metrics=model.metrics_names[1:]) |
|
|
|
|
|
print(f"Starting fine-tuning with learning rate: {fine_tune_lr}") |
|
|
history = model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_val, y_val), verbose=1) |
|
|
return model, history |
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
print("Simulating pre-training of base model...") |
|
|
|
|
|
dates_general = pd.date_range(start='2020-01-01', periods=1000, freq='B') |
|
|
data_general_np = np.random.rand(1000, 1) * 100 + 50 |
|
|
general_data = pd.DataFrame(data_general_np, index=dates_general, columns=['Close']) |
|
|
general_data['Close'] = general_data['Close'] + np.sin(np.linspace(0, 50, 1000)) * 30 |
|
|
|
|
|
look_back_tl = 60 |
|
|
X_general, y_general, scaler_general, _ = preprocess_data_for_transfer(general_data, look_back=look_back_tl) |
|
|
|
|
|
if X_general.shape[0] > 0: |
|
|
base_model_input_shape = (X_general.shape[1], X_general.shape[2]) |
|
|
base_model_tl = create_base_model(base_model_input_shape) |
|
|
|
|
|
|
|
|
base_model_tl.compile(optimizer=Adam(0.001), loss='mean_squared_error') |
|
|
print(f"Base model summary (for pre-training):") |
|
|
base_model_tl.summary() |
|
|
|
|
|
|
|
|
print("Base model 'pre-trained' (simulated - no actual training in this step for speed).") |
|
|
|
|
|
else: |
|
|
print("Not enough general data for pre-training simulation.") |
|
|
base_model_tl = None |
|
|
|
|
|
|
|
|
if base_model_tl: |
|
|
print("\nSimulating transfer learning to a new task/dataset...") |
|
|
|
|
|
dates_specific = pd.date_range(start='2023-01-01', periods=200, freq='B') |
|
|
data_specific_np = np.random.rand(200, 1) * 70 + 30 |
|
|
specific_data = pd.DataFrame(data_specific_np, index=dates_specific, columns=['Close']) |
|
|
specific_data['Close'] = specific_data['Close'] + np.cos(np.linspace(0, 10, 200)) * 15 |
|
|
|
|
|
X_specific, y_specific, scaler_specific, _ = preprocess_data_for_transfer(specific_data, look_back=look_back_tl) |
|
|
|
|
|
if X_specific.shape[0] > 100: |
|
|
split_idx = int(len(X_specific) * 0.8) |
|
|
X_train_sp, y_train_sp = X_specific[:split_idx], y_specific[:split_idx] |
|
|
X_val_sp, y_val_sp = X_specific[split_idx:], y_specific[split_idx:] |
|
|
|
|
|
|
|
|
|
|
|
adapted_model_tl = adapt_model_for_transfer(base_model_tl, num_classes_new_task=1) |
|
|
print("Adapted model summary:") |
|
|
adapted_model_tl.summary() |
|
|
|
|
|
|
|
|
print("Training adapted model on new task (base frozen)...") |
|
|
|
|
|
print("'Trained' adapted model (simulated - no actual training for speed).") |
|
|
|
|
|
|
|
|
print("\nFine-tuning model...") |
|
|
|
|
|
|
|
|
fine_tuned_model, history = fine_tune_model( |
|
|
adapted_model_tl, |
|
|
X_train_sp, y_train_sp, |
|
|
X_val_sp, y_val_sp, |
|
|
unfreeze_at_layer_name=None, |
|
|
|
|
|
fine_tune_lr=1e-5, |
|
|
epochs=5, |
|
|
batch_size=16 |
|
|
) |
|
|
print("Model fine-tuned.") |
|
|
|
|
|
|
|
|
if len(X_val_sp) > 0: |
|
|
preds = fine_tuned_model.predict(X_val_sp) |
|
|
print(f"\nSample predictions on validation set (first 5): {preds[:5].flatten()}") |
|
|
print(f"Actual values (first 5): {y_val_sp[:5].flatten()}") |
|
|
else: |
|
|
print("Not enough specific data for transfer learning simulation.") |
|
|
else: |
|
|
print("Base model not available, skipping transfer learning simulation.") |