AnnNaserNabil commited on
Commit
19ee26a
·
verified ·
1 Parent(s): e845ced

Create Temporal_Convolutional_Network.py

Browse files
Files changed (1) hide show
  1. Temporal_Convolutional_Network.py +292 -0
Temporal_Convolutional_Network.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import torch
5
+ import torch.nn as nn
6
+ from torch.utils.data import Dataset, DataLoader
7
+ from sklearn.preprocessing import MinMaxScaler
8
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
9
+ import matplotlib.pyplot as plt
10
+ from itertools import product
11
+ import json
12
+
13
+ # -------------------------
14
+ # Dataset
15
+ # -------------------------
16
+ class StockDataset(Dataset):
17
+ """Custom Dataset for stock price time-series forecasting."""
18
+ def __init__(self, series, seq_length):
19
+ self.series = series
20
+ self.seq_length = seq_length
21
+
22
+ def __len__(self):
23
+ return len(self.series) - self.seq_length
24
+
25
+ def __getitem__(self, idx):
26
+ x = self.series[idx:idx + self.seq_length] # Shape: (seq_length,)
27
+ y = self.series[idx + self.seq_length] # Shape: scalar
28
+ x = np.expand_dims(x, axis=0) # Shape: (1, seq_length)
29
+ return torch.tensor(x, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)
30
+
31
+ # -------------------------
32
+ # TCN Blocks
33
+ # -------------------------
34
+ class TemporalBlock(nn.Module):
35
+ """Temporal Convolutional Network block with causal dilated convolutions."""
36
+ def __init__(self, in_channels, out_channels, kernel_size, stride, dilation, dropout=0.2):
37
+ super().__init__()
38
+ padding = (kernel_size - 1) * dilation
39
+ self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,
40
+ stride=stride, padding=padding, dilation=dilation)
41
+ self.relu1 = nn.ReLU()
42
+ self.dropout1 = nn.Dropout(dropout)
43
+ self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,
44
+ stride=stride, padding=padding, dilation=dilation)
45
+ self.relu2 = nn.ReLU()
46
+ self.dropout2 = nn.Dropout(dropout)
47
+ self.downsample = nn.Conv1d(in_channels, out_channels, 1) if in_channels != out_channels else None
48
+ self.relu = nn.ReLU()
49
+
50
+ def forward(self, x):
51
+ out = self.conv1(x)
52
+ out = out[:, :, :x.size(2)] # Trim padding
53
+ out = self.relu1(out)
54
+ out = self.dropout1(out)
55
+ out = self.conv2(out)
56
+ out = out[:, :, :x.size(2)] # Trim padding
57
+ out = self.relu2(out)
58
+ out = self.dropout2(out)
59
+ res = x if self.downsample is None else self.downsample(x)
60
+ return self.relu(out + res)
61
+
62
+ class TCN(nn.Module):
63
+ """Temporal Convolutional Network for time-series forecasting."""
64
+ def __init__(self, input_size, output_size, num_channels, kernel_size=3, dropout=0.2):
65
+ super().__init__()
66
+ layers = []
67
+ num_levels = len(num_channels)
68
+ for i in range(num_levels):
69
+ dilation_size = 2 ** i
70
+ in_channels = input_size if i == 0 else num_channels[i - 1]
71
+ out_channels = num_channels[i]
72
+ layers.append(
73
+ TemporalBlock(in_channels, out_channels, kernel_size,
74
+ stride=1, dilation=dilation_size, dropout=dropout)
75
+ )
76
+ self.network = nn.Sequential(*layers)
77
+ self.linear = nn.Linear(num_channels[-1], output_size)
78
+
79
+ def forward(self, x):
80
+ out = self.network(x)
81
+ out = out[:, :, -1]
82
+ return self.linear(out)
83
+
84
+ # -------------------------
85
+ # Forecaster
86
+ # -------------------------
87
+ class StockPriceForecaster:
88
+ """Stock price forecasting with TCN model."""
89
+ def __init__(self, dataset_path, seq_length=30, batch_size=32, lr=0.001, epochs=20,
90
+ kernel_size=3, num_channels=[32, 64, 64], dropout=0.2, test_split=0.2):
91
+ self.dataset_path = dataset_path
92
+ self.seq_length = seq_length
93
+ self.batch_size = batch_size
94
+ self.lr = lr
95
+ self.epochs = epochs
96
+ self.kernel_size = kernel_size
97
+ self.num_channels = num_channels
98
+ self.dropout = dropout
99
+ self.test_split = test_split
100
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
101
+ self.scaler = MinMaxScaler()
102
+
103
+ def load_data(self):
104
+ """Load and preprocess stock price data."""
105
+ if not os.path.exists(self.dataset_path):
106
+ raise FileNotFoundError(f"Dataset file not found at: {self.dataset_path}")
107
+ df = pd.read_csv(self.dataset_path)
108
+ if "Close" not in df.columns:
109
+ raise ValueError("CSV file must contain a 'Close' column")
110
+ prices = df["Close"].values.reshape(-1, 1)
111
+ prices_scaled = self.scaler.fit_transform(prices).flatten()
112
+ dataset = StockDataset(prices_scaled, self.seq_length)
113
+ train_size = int(len(dataset) * (1 - self.test_split))
114
+ test_size = len(dataset) - train_size
115
+ train_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_size, test_size])
116
+ train_loader = DataLoader(train_dataset, batch_size=self.batch_size, shuffle=True)
117
+ test_loader = DataLoader(test_dataset, batch_size=self.batch_size, shuffle=False)
118
+ return train_loader, test_loader
119
+
120
+ def train(self, model, train_loader):
121
+ """Train the TCN model."""
122
+ criterion = nn.MSELoss()
123
+ optimizer = torch.optim.Adam(model.parameters(), lr=self.lr)
124
+ model.train()
125
+ for epoch in range(self.epochs):
126
+ epoch_loss = 0
127
+ for x, y in train_loader:
128
+ x, y = x.to(self.device), y.to(self.device)
129
+ optimizer.zero_grad()
130
+ output = model(x)
131
+ loss = criterion(output.squeeze(), y)
132
+ loss.backward()
133
+ optimizer.step()
134
+ epoch_loss += loss.item()
135
+ print(f"Epoch [{epoch+1}/{self.epochs}], Loss: {epoch_loss/len(train_loader):.6f}")
136
+ return model
137
+
138
+ def evaluate(self, model, test_loader):
139
+ """Evaluate the model on the test set."""
140
+ model.eval()
141
+ predictions, actuals = [], []
142
+ with torch.no_grad():
143
+ for x, y in test_loader:
144
+ x, y = x.to(self.device), y.to(self.device)
145
+ output = model(x)
146
+ predictions.extend(output.squeeze().cpu().numpy())
147
+ actuals.extend(y.cpu().numpy())
148
+ predictions = self.scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
149
+ actuals = self.scaler.inverse_transform(np.array(actuals).reshape(-1, 1)).flatten()
150
+ mae = mean_absolute_error(actuals, predictions)
151
+ rmse = mean_squared_error(actuals, predictions, squared=False)
152
+ mape = np.mean(np.abs((actuals - predictions) / (actuals + 1e-10))) * 100
153
+ r2 = r2_score(actuals, predictions)
154
+ return mae, rmse, mape, r2, actuals, predictions
155
+
156
+ def run(self):
157
+ """Run training and evaluation."""
158
+ train_loader, test_loader = self.load_data()
159
+ model = TCN(input_size=1, output_size=1,
160
+ num_channels=self.num_channels,
161
+ kernel_size=self.kernel_size,
162
+ dropout=self.dropout).to(self.device)
163
+ trained_model = self.train(model, train_loader)
164
+ return trained_model, self.evaluate(model, test_loader)
165
+
166
+ # -------------------------
167
+ # Save Model for Hugging Face
168
+ # -------------------------
169
+ def save_model_for_huggingface(model, scaler, config, save_dir="tcn_stock_model"):
170
+ """Save the model and necessary components for Hugging Face deployment."""
171
+ os.makedirs(save_dir, exist_ok=True)
172
+
173
+ # Save model weights
174
+ torch.save(model.state_dict(), os.path.join(save_dir, "pytorch_model.bin"))
175
+
176
+ # Save model configuration
177
+ with open(os.path.join(save_dir, "config.json"), "w") as f:
178
+ json.dump({
179
+ "input_size": 1,
180
+ "output_size": 1,
181
+ "num_channels": config["num_channels"],
182
+ "kernel_size": config["kernel_size"],
183
+ "dropout": config["dropout"],
184
+ "seq_length": config["seq_length"]
185
+ }, f, indent=4)
186
+
187
+ # Save scaler for preprocessing
188
+ import pickle
189
+ with open(os.path.join(save_dir, "scaler.pkl"), "wb") as f:
190
+ pickle.dump(scaler, f)
191
+
192
+ print(f"Model saved to {save_dir}")
193
+
194
+ # -------------------------
195
+ # Experiment Loop
196
+ # -------------------------
197
+ if __name__ == "__main__":
198
+ dataset_path = "/work/GOOGL.csv" # Update to your CSV path
199
+
200
+ # Hyperparameter grid
201
+ seq_lengths = [20, 50]
202
+ batch_sizes = [16, 32]
203
+ learning_rates = [0.001, 0.0005]
204
+ kernel_sizes = [3, 5]
205
+ num_channels_list = [[32, 64, 128], [64, 128, 256]]
206
+ dropouts = [0.1, 0.2]
207
+
208
+ results = []
209
+ best_result = None
210
+ best_metrics = float('inf') # Track best RMSE
211
+ best_model = None
212
+ best_config = None
213
+
214
+ # Run experiments
215
+ for seq, batch, lr, kernel, channels, dropout in product(
216
+ seq_lengths, batch_sizes, learning_rates, kernel_sizes, num_channels_list, dropouts
217
+ ):
218
+ print(f"\nRunning: seq={seq}, batch={batch}, lr={lr}, kernel={kernel}, channels={channels}, dropout={dropout}")
219
+ try:
220
+ forecaster = StockPriceForecaster(
221
+ dataset_path=dataset_path,
222
+ seq_length=seq,
223
+ batch_size=batch,
224
+ lr=lr,
225
+ epochs=20,
226
+ kernel_size=kernel,
227
+ num_channels=channels,
228
+ dropout=dropout,
229
+ test_split=0.2
230
+ )
231
+ model, (mae, rmse, mape, r2, actuals, predictions) = forecaster.run()
232
+ results.append({
233
+ "seq_length": seq,
234
+ "batch_size": batch,
235
+ "lr": lr,
236
+ "kernel_size": kernel,
237
+ "num_channels": str(channels),
238
+ "dropout": dropout,
239
+ "MAE": mae,
240
+ "RMSE": rmse,
241
+ "MAPE": mape,
242
+ "R2": r2
243
+ })
244
+ if rmse < best_metrics:
245
+ best_metrics = rmse
246
+ best_result = (actuals, predictions, seq, batch, lr, kernel, channels, dropout)
247
+ best_model = model
248
+ best_config = {
249
+ "seq_length": seq,
250
+ "batch_size": batch,
251
+ "lr": lr,
252
+ "kernel_size": kernel,
253
+ "num_channels": channels,
254
+ "dropout": dropout
255
+ }
256
+ except Exception as e:
257
+ print(f"Error with config seq={seq}, batch={batch}, lr={lr}, kernel={kernel}, channels={channels}, dropout={dropout}: {e}")
258
+ continue
259
+
260
+ # Save results
261
+ df_results = pd.DataFrame(results)
262
+ df_results.to_csv("tcn_experiments_results.csv", index=False)
263
+ print("\nAll experiments done! Results saved to 'tcn_experiments_results.csv'")
264
+
265
+ # Display metrics table
266
+ print("\nMetrics Table:")
267
+ pd.set_option('display.max_columns', None)
268
+ pd.set_option('display.width', 1000)
269
+ pd.set_option('display.float_format', '{:.6f}'.format)
270
+ print(df_results)
271
+
272
+ # Save best model for Hugging Face
273
+ if best_model is not None:
274
+ save_model_for_huggingface(best_model, forecaster.scaler, best_config)
275
+ print(f"\nBest model saved with RMSE: {best_metrics:.6f}")
276
+ print("\nBest configuration:")
277
+ print(pd.Series(best_config))
278
+
279
+ # Plot best combination
280
+ if best_result is not None:
281
+ actuals, predictions, seq, batch, lr, kernel, channels, dropout = best_result
282
+ plt.figure(figsize=(12, 6))
283
+ plt.plot(actuals, label="Actual Prices")
284
+ plt.plot(predictions, label="Predicted Prices")
285
+ plt.title(f"Best Model: seq={seq}, batch={batch}, lr={lr}, kernel={kernel}, channels={channels}, dropout={dropout}")
286
+ plt.xlabel("Time Step")
287
+ plt.ylabel("Price")
288
+ plt.legend()
289
+ plt.grid(True)
290
+ plt.show()
291
+ else:
292
+ print("No successful experiments to plot.")