class StepByStep(): def __init__(self, model, loss_fn, optimizer): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' self.model = model.to(self.device) self.loss_fn = loss_fn self.optimizer = optimizer def to(self, device): # This method allows the user to specify a different device # It sets the corresponding attribute (to be used later in # the mini-batches) and sends the model to the device try: self.device = device self.model.to(self.device) except RuntimeError: self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"Couldn't send it to {device}, sending it to {self.device} instead.") self.model.to(self.device)
class StepByStep(): def __init__(self, model, loss_fn, optimizer): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' self.model = model.to(self.device) self.loss_fn = loss_fn self.optimizer = optimizer # These attributes are defined here, but since they are # not available at the moment of creation, we keep them None self.train_loader = None self.val_loader = None self.writer = None def to(self, device): # This method allows the user to specify a different device # It sets the corresponding attribute (to be used later in # the mini-batches) and sends the model to the device try: self.device = device self.model.to(self.device) except RuntimeError: self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"Couldn't send it to {device}, sending it to {self.device} instead.") self.model.to(self.device) def set_loaders(self, train_loader, val_loader=None): self.train_loader = train_loader self.val_loader = val_loader def set_tensorboard(self, name, folder='runs'): # This method allows the user to create a SummaryWriter to interface with TensorBoard suffix = datetime.now().strftime('%Y%m%d%H%M%S') self.writer = SummaryWriter(f'{folder}/{name}_{suffix}')
class StepByStep(): def __init__(self, model, loss_fn, optimizer): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' self.model = model.to(self.device) self.loss_fn = loss_fn self.optimizer = optimizer # These attributes are defined here, but since they are # not available at the moment of creation, we keep them None self.train_loader = None self.val_loader = None self.writer = None # These attributes are going to be computed internally self.losses = [] self.val_losses = [] self.total_epochs = 0 def to(self, device): # This method allows the user to specify a different device # It sets the corresponding attribute (to be used later in # the mini-batches) and sends the model to the device try: self.device = device self.model.to(self.device) except RuntimeError: self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"Couldn't send it to {device}, sending it to {self.device} instead.") self.model.to(self.device) def set_loaders(self, train_loader, val_loader=None): self.train_loader = train_loader self.val_loader = val_loader def set_tensorboard(self, name, folder='runs'): # This method allows the user to create a SummaryWriter to interface with TensorBoard suffix = datetime.now().strftime('%Y%m%d%H%M%S') self.writer = SummaryWriter(f'{folder}/{name}_{suffix}')
class StepByStep(): def __init__(self, model, loss_fn, optimizer): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' self.model = model.to(self.device) self.loss_fn = loss_fn self.optimizer = optimizer # These attributes are defined here, but since they are # not available at the moment of creation, we keep them None self.train_loader = None self.val_loader = None self.writer = None # These attributes are going to be computed internally self.losses = [] self.val_losses = [] self.total_epochs = 0 # Create the train_step function for model, loss function and optimizer # Note: there are NO ARGS there! It makes use of the class attributes directly self.train_step_fn = self._make_train_step_fn() # Create the val_step function for model and loss function self.val_step_fn = self._make_val_step_fn() def to(self, device): # This method allows the user to specify a different device # It sets the corresponding attribute (to be used later in # the mini-batches) and sends the model to the device try: self.device = device self.model.to(self.device) except RuntimeError: self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"Couldn't send it to {device}, sending it to {self.device} instead.") self.model.to(self.device) def set_loaders(self, train_loader, val_loader=None): self.train_loader = train_loader self.val_loader = val_loader def set_tensorboard(self, name, folder='runs'): # This method allows the user to create a SummaryWriter to interface with TensorBoard suffix = datetime.now().strftime('%Y%m%d%H%M%S') self.writer = SummaryWriter(f'{folder}/{name}_{suffix}') def _make_train_step_fn(self): # Build function that performs a step in the train loop def perform_train_step_fn(x, y): self.model.train() yhat = self.model(x) loss = self.loss_fn(yhat, y) loss.backward() self.optimizer.step() self.optimizer.zero_grad() return loss.item() return perform_train_step_fn def _make_val_step_fn(self): # Build function that performs a step in the validation loop def perform_val_step_fn(x, y): self.model.eval() yhat = self.model(x) loss = self.loss_fn(yhat, y) return loss.item() return perform_val_step_fn
class StepByStep(): def __init__(self, model, loss_fn, optimizer): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' self.model = model.to(self.device) self.loss_fn = loss_fn self.optimizer = optimizer # These attributes are defined here, but since they are # not available at the moment of creation, we keep them None self.train_loader = None self.val_loader = None self.writer = None # These attributes are going to be computed internally self.losses = [] self.val_losses = [] self.total_epochs = 0 # Create the train_step function for model, loss function and optimizer # Note: there are NO ARGS there! It makes use of the class attributes directly self.train_step_fn = self._make_train_step_fn() # Create the val_step function for model and loss function self.val_step_fn = self._make_val_step_fn() def to(self, device): # This method allows the user to specify a different device # It sets the corresponding attribute (to be used later in # the mini-batches) and sends the model to the device try: self.device = device self.model.to(self.device) except RuntimeError: self.device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"Couldn't send it to {device}, sending it to {self.device} instead.") self.model.to(self.device) def set_seed(self, seed=42): torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False torch.manual_seed(seed) def set_loaders(self, train_loader, val_loader=None): self.train_loader = train_loader self.val_loader = val_loader def set_tensorboard(self, name, folder='runs'): # This method allows the user to create a SummaryWriter to interface with TensorBoard suffix = datetime.now().strftime('%Y%m%d%H%M%S') self.writer = SummaryWriter(f'{folder}/{name}_{suffix}') def train(self, n_epochs, seed=42): self.set_seed(seed) for epoch in range(n_epochs): # Keep track of the numbers of epochs by updating the corresponding attribute self.total_epochs += 1 loss = self._mini_batch(validation=False) self.losses.append(loss) with torch.no_grad(): val_loss = self._mini_batch(validation=True) self.val_losses.append(val_loss) # If a SummaryWriter has been set... if self.writer: scalars = {'training': loss} if val_loss is not None: scalars.update({'validation': val_loss}) self.writer.add_scalars(main_tag='loss', tag_scalar_dict=scalars, global_step=epoch) if self.writer: # Flush the writer self.writer.flush() def save_checkpoint(self, filename): checkpoint = {'epoch': self.total_epochs, 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), 'losses': self.losses, 'val_losses': self.val_losses} torch.save(checkpoint, filename) def load_checkpoint(self, filename): checkpoint = torch.load(filename) self.model.load_state_dict(checkpoint['model_state_dict']) self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) self.total_epochs = checkpoint['epoch'] self.losses = checkpoint['losses'] self.val_losses = checkpoint['val_losses'] self.model.train() # always use TRAIN for resuming training def predict(self, x): # Set it to evaluation mode for predictions self.model.eval() x_tensor = torch.as_tensor(x).float().to(self.device) y_hat_tensor = self.model(x_tensor) # Set it back to train mode self.model.train() return y_hat_tensor.detach().cpu().numpy() def plot_losses(self): fig = plt.figure(figsize=(10, 4)) plt.xlabel('Epochs') plt.ylabel('Loss') plt.yscale('log') plt.plot(self.losses, label='Training Loss', c='b') if self.val_losses: plt.plot(self.val_losses, label='Validation Loss', c='r') plt.legend() fig.tight_layout() return fig def add_graph(self): if self.train_loader and self.writer: # Fetche a single mini-batch so we can use add_graph x_sample, y_sample = next(iter(self.train_loader)) self.writer.add_graph(self.model, x_sample.to(self.device)) def _make_train_step_fn(self): # Build function that performs a step in the train loop def perform_train_step_fn(x, y): self.model.train() yhat = self.model(x) loss = self.loss_fn(yhat, y) loss.backward() self.optimizer.step() self.optimizer.zero_grad() return loss.item() return perform_train_step_fn def _make_val_step_fn(self): # Build function that performs a step in the validation loop def perform_val_step_fn(x, y): self.model.eval() yhat = self.model(x) loss = self.loss_fn(yhat, y) return loss.item() return perform_val_step_fn def _mini_batch(self, validation=False): # The mini-batch can be used with both loaders # The argument `validation` defines which loader and # corresponding step function is going to be used if validation: data_loader = self.val_loader step_fn = self.val_step_fn else: data_loader = self.train_loader step_fn = self.train_step_fn mini_batch_losses = [] for x_batch, y_batch in data_loader: x_batch = x_batch.to(self.device) y_batch = y_batch.to(self.device) mini_batch_loss = step_fn(x_batch, y_batch) mini_batch_losses.append(mini_batch_loss) loss = np.mean(mini_batch_losses) return loss
%%writefile model_configuration/v4.py lr = .1 torch.manual_seed(42) model = nn.Sequential(nn.Linear(1, 1)) optimizer = torch.optim.SGD(model.parameters(), lr=lr) loss_fn = nn.MSELoss(reduction='mean')
sbs = StepByStep(model, loss_fn, optimizer) sbs.set_loaders(train_loader, val_loader) sbs.set_tensorboard('classy')
sbs.save_checkpoint('model_checkpoint.pth')
new_sbs = StepByStep(model, loss_fn, optimizer)
# %load data_preparation/v2.py torch.manual_seed(13) # Build tensors from numpy arrays BEFORE split x_tensor = torch.from_numpy(x).float().reshape(-1, 1) y_tensor = torch.from_numpy(y).float().reshape(-1, 1) # Build dataset containing ALL data points dataset = TensorDataset(x_tensor, y_tensor) # Perform split ratio = .8 n_total = len(dataset) n_train = int(n_total * ratio) n_val = n_total - n_train train_data, val_data = random_split(dataset, [n_train, n_val]) # Build a loader for each set train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True) val_loader = DataLoader(dataset=val_data, batch_size=16)
# %load model_configuration/v4.py lr = .1 torch.manual_seed(42) model = nn.Sequential(nn.Linear(1, 1)) optimizer = torch.optim.SGD(model.parameters(), lr=lr) loss_fn = nn.MSELoss(reduction='mean')
n_epochs = 200 sbs = StepByStep(model, loss_fn, optimizer) sbs.set_loaders(train_loader, val_loader) sbs.set_tensorboard('classy') sbs.train(n_epochs=n_epochs)