Skip to content

Commit 488bb04

Browse files
Refactored and debugged model_runner.py
1 parent 55039dc commit 488bb04

5 files changed

Lines changed: 109 additions & 44 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ seaborn
1010
tabulate
1111
torch
1212
torchvision
13+
tqdm
1314
scikit-learn

src/iplane/autoencoder.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,37 @@
1010
from typing import List
1111
import matplotlib.pyplot as plt
1212

13+
LAYER_DIMENSIONS = [784, 512, 256, 128, 64] # Example dimensions for MNIST
14+
1315

1416
########################################################################
1517
class Autoencoder(nn.Module):
1618
# Basic Autoencoder
17-
def __init__(self, input_dim=784, encoding_dim=64):
19+
def __init__(self, layer_dimensions: List[int]):
20+
"""
21+
22+
Args:
23+
dimensions (List[int]): List of dimensions for the autoencoder
24+
The first element is the input dimension,
25+
the last element is the encoding dimension.
26+
"""
1827
super(Autoencoder, self).__init__()
28+
self.layer_dimensions = layer_dimensions
29+
self.input_dim = layer_dimensions[0]
30+
self.encoding_dim = layer_dimensions[-1]
31+
# Calculate dimension of hidden layer
1932
# Encoder
20-
self.encoder = nn.Sequential(
21-
nn.Linear(input_dim, 256),
22-
nn.ReLU(),
23-
nn.Linear(256, 128),
24-
nn.ReLU(),
25-
nn.Linear(128, encoding_dim),
26-
nn.ReLU()
27-
)
33+
encoder_layers:list = []
34+
for idx in range(len(layer_dimensions) - 1):
35+
encoder_layers.append(nn.Linear(layer_dimensions[idx], layer_dimensions[idx + 1]))
36+
encoder_layers.append(nn.ReLU())
37+
self.encoder = nn.Sequential(*encoder_layers[0:-1])
2838
# Decoder
29-
self.decoder = nn.Sequential(
30-
nn.Linear(encoding_dim, 128),
31-
nn.ReLU(),
32-
nn.Linear(128, 256),
33-
nn.ReLU(),
34-
nn.Linear(256, input_dim),
35-
nn.Sigmoid() # Output between 0 and 1 for image reconstruction
36-
)
39+
decoder_layers:list = []
40+
for idx in range(len(layer_dimensions) - 1, 0, -1):
41+
decoder_layers.append(nn.Linear(layer_dimensions[idx], layer_dimensions[idx - 1]))
42+
decoder_layers.append(nn.ReLU())
43+
self.decoder = nn.Sequential(*decoder_layers[0:-1])
3744

3845
def forward(self, x):
3946
# Encode
@@ -54,10 +61,12 @@ def decode(self, x):
5461
########################################################################
5562
class AutoencoderRunner(object):
5663
# Runner for Autoencoder
64+
layer_dimensions = [784, 512, 256, 128, 64] # Example dimensions for MNIST
5765

58-
def __init__(self, num_epoch:int=3, learning_rate:float=1e-3, is_report:bool=False):
66+
def __init__(self, layer_dimensions:List[int]=LAYER_DIMENSIONS,
67+
num_epoch:int=3, learning_rate:float=1e-3, is_report:bool=False):
5968
self.device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu" # type: ignore
60-
self.model = Autoencoder(input_dim=784, encoding_dim=64).to(self.device)
69+
self.model = Autoencoder(layer_dimensions).to(self.device)
6170
self.num_epoch = num_epoch
6271
self.learning_rate = learning_rate
6372
self.losses: list = []

src/iplane/dataset_csv.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'''DataLoader for CSV files'''
22

3+
import numpy as np # type: ignore
34
import pandas as pd # type: ignore
45
import torch
56
from torch.utils.data import Dataset
6-
from typing import Optional, Union, cast
7+
from typing import Optional, Union
78

89

910
class DatasetCSV(Dataset):
10-
def __init__(self, csv_input:Union[str, pd.DataFrame], target_column:str, transform=None):
11+
def __init__(self, csv_input:Union[str, pd.DataFrame], target_column:Optional[str]=None, transform=None):
1112
"""
1213
All columns except the target column are considered features.
1314
@@ -20,9 +21,13 @@ def __init__(self, csv_input:Union[str, pd.DataFrame], target_column:str, transf
2021
self.data_df = csv_input
2122
else:
2223
self.data_df = pd.read_csv(csv_input)
24+
self.target_column = target_column
2325
feature_columns = [col for col in self.data_df.columns if col != target_column]
2426
self.feature_tnsr = torch.tensor(self.data_df[feature_columns].values)
25-
self.target_tnsr = torch.tensor(self.data_df[target_column].values)
27+
if target_column is None:
28+
self.target_tnsr = torch.zeros(len(self.data_df))
29+
else:
30+
self.target_tnsr = torch.tensor(self.data_df[target_column].values)
2631
self.transform = transform
2732

2833
def __len__(self):
@@ -32,7 +37,10 @@ def __len__(self):
3237
def __getitem__(self, idx):
3338
# Get features and target
3439
feature_tnsr = self.feature_tnsr[idx].detach().clone()
35-
target_tnsr = self.target_tnsr[idx].detach().clone()
40+
if self.target_column is None:
41+
target_tnsr = torch.tensor(np.nan)
42+
else:
43+
target_tnsr = self.target_tnsr[idx].detach().clone()
3644

3745
# Apply transform if specified
3846
if self.transform:

src/iplane/model_runner.py

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11

22
from collections import namedtuple
3+
import numpy as np
34
import torch
45
import torch.nn as nn
56
import torch.optim as optim
67
from torch.utils.data import DataLoader
8+
from tqdm import tqdm # type: ignore
79
from typing import List, Tuple
810

911
"""To do
@@ -19,46 +21,78 @@ class ModelRunner(object):
1921
# Runner for Autoencoder
2022

2123
def __init__(self, model: nn.Module, num_epoch:int=3, learning_rate:float=1e-3,
22-
criterion:nn.Module=nn.MSELoss(), is_autoencoder:bool=False, is_report:bool=True):
24+
criterion:nn.Module=nn.MSELoss(), is_autoencoder:bool=False,
25+
is_normalized:bool=False, is_report:bool=True):
2326
"""
2427
Args:
25-
model (nn.Module): _description_
28+
model (nn.Module): Model being run
2629
num_epoch (int, optional): Defaults to 3.
2730
learning_rate (float, optional): Defaults to 1e-3.
2831
is_autoencoder (bool, optional): target data is features Defaults to False.
29-
is_report (bool, optional): Print text for progress. Defaults to False.
32+
is_normalized (bool, optional): Whether to normalize the input data (divide by std).
33+
Defaults to False.
34+
is_report (bool, optional): Print text for progress.
35+
Defaults to False.
3036
"""
3137
self.device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu" # type: ignore
3238
self.model = model.to(self.device)
3339
self.num_epoch = num_epoch
3440
self.learning_rate = learning_rate
3541
self.criterion = criterion
3642
self.is_autoencoder = is_autoencoder
43+
self.is_normalized = is_normalized
3744
self.is_report = is_report
45+
# Calculated state
46+
self.feature_std_tnsr = torch.tensor([np.nan])
47+
self.target_std_tnsr = torch.tensor([np.nan])
3848

3949
def train(self, train_loader: DataLoader) -> RunnerResult:
40-
"""Train the network."""
50+
"""
51+
Train the model.
52+
53+
Args:
54+
train_loader (DataLoader): DataLoader for training data
55+
Returns:
56+
RunnerResult: losses and number of epochs
57+
"""
58+
##
59+
def calculate_std(loader_idx: int) -> torch.Tensor:
60+
# loader_idx (int): Index into the DataLoader
61+
full_tnsr = torch.cat([x[loader_idx] for x in train_loader])
62+
if self.is_normalized:
63+
return full_tnsr.std(dim=0)
64+
else:
65+
return torch.ones(full_tnsr.size()[1])
66+
##
67+
# Handle normalization adjustments
68+
self.feature_std_tnsr = calculate_std(0)
69+
self.target_std_tnsr = calculate_std(1)
70+
if self.is_autoencoder:
71+
self.target_std_tnsr = self.feature_std_tnsr
72+
# Initialize for training
4173
optimizer = optim.Adam(self.model.parameters(), lr=self.learning_rate)
4274
self.model.to(self.device)
43-
4475
self.model.train()
4576
losses = []
4677
avg_loss = 0.0
47-
48-
for epoch in range(self.num_epoch):
78+
epoch_loss = np.inf
79+
# Training loop
80+
pbar = tqdm(range(self.num_epoch), desc=f"epochs (loss={epoch_loss:.4f})")
81+
for epoch in pbar:
82+
pbar.set_description_str(f"epochs (loss={epoch_loss:.4f})")
4983
epoch_loss = 0
50-
for data in list(train_loader):
51-
data = data.to(self.device, non_blocking=True)
52-
feature_tnsr, target_tnsr = data
84+
#for idx, (feature_tnsr, target_tnsr) in list(train_loader):
85+
for (feature_tnsr, target_tnsr) in train_loader:
5386
if self.is_autoencoder:
5487
# For autoencoder, target is the same as input
5588
target_tnsr = feature_tnsr
56-
feature_tnsr = feature_tnsr.view(feature_tnsr.size(0), -1)
57-
# Forward pass
89+
feature_tnsr = feature_tnsr/self.feature_std_tnsr
5890
feature_tnsr = feature_tnsr.to(self.device)
91+
target_tnsr = target_tnsr/self.target_std_tnsr
92+
target_tnsr = target_tnsr.to(self.device)
93+
# Forward pass
5994
prediction_tnsr = self.model(feature_tnsr)
6095
loss = self.criterion(prediction_tnsr, target_tnsr)
61-
loss = loss.to(CPU)
6296
# Backward pass
6397
optimizer.zero_grad()
6498
loss.backward()
@@ -71,22 +105,35 @@ def train(self, train_loader: DataLoader) -> RunnerResult:
71105
if self.is_report:
72106
print(f'Epoch [{epoch+1}/{self.num_epoch}], Loss: {avg_loss:.4f}')
73107
#
108+
self.model.to(CPU)
74109
return RunnerResult(losses=losses, num_epochs=self.num_epoch)
75110

76-
def evaluate(self, test_loader: DataLoader) -> RunnerResult:
77-
"""Evaluate the model on the test set."""
111+
def predict(self, feature_tnsr: torch.Tensor) -> torch.Tensor:
112+
"""Predicts the target for the features.
113+
114+
Args:
115+
feature_tnsr (torch.Tensor): Input features for which to predict targets.
116+
Returns:
117+
torch.Tensor: target predictions
118+
"""
119+
self.model.eval()
120+
feature_tnsr = feature_tnsr/self.feature_std_tnsr
121+
with torch.no_grad():
122+
prediction_tnsr = self.model(feature_tnsr)
123+
return self.feature_std_tnsr*prediction_tnsr
124+
125+
def assess(self, test_loader: DataLoader) -> RunnerResult:
126+
"""Assess the model on a test dataset."""
78127
self.model.eval()
79128
test_losses = []
80129
#
81130
with torch.no_grad():
82-
for data in list(test_loader):
83-
data = data.to(self.device, non_blocking=True)
84-
feature_tnsr, target_tnsr = data
131+
for (feature_tnsr, target_tnsr) in list(test_loader):
85132
if self.is_autoencoder:
86133
# For autoencoder, target is the same as input
87134
target_tnsr = feature_tnsr
88135
feature_tnsr = feature_tnsr.view(feature_tnsr.size(0), -1)
89-
prediction_tnsr = self.model(feature_tnsr)
136+
prediction_tnsr = self.predict(feature_tnsr)
90137
loss = self.criterion(prediction_tnsr, target_tnsr).to(CPU)
91138
test_losses.append(loss.item())
92139

@@ -110,5 +157,5 @@ def run(self, train_loader: DataLoader, test_loader: DataLoader)->Tuple[RunnerRe
110157
print("Training Fully Connected Autoencoder...")
111158
# Create and train fully connected autoencoder
112159
train_runner_result = self.train(train_loader)
113-
test_runner_result = self.evaluate(test_loader)
160+
test_runner_result = self.assess(test_loader)
114161
return train_runner_result, test_runner_result

tests/test_autoencoder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
NUM_EPOCH = 3
1414

1515

16-
class TestTrainingVisualizer(unittest.TestCase):
16+
class TestAutoencoder(unittest.TestCase):
1717

1818
def setUp(self):
1919
self.runner = AutoencoderRunner(num_epoch=NUM_EPOCH, is_report=IGNORE_TESTS)

0 commit comments

Comments
 (0)