class_docstr: Base Task superclass used to implement new tasks.
  
  Attributes:
      model: Model name string for the task in use.
      local: Run locally. Defaults to False.
      api_key: Backprop API key for non-local inference.
      device: Device to run inference on. Defaults to "cuda" if available.
      models: All supported models for a given task (pulls from config).
      default_local_model: Which model the task will default to if initialized with none provided: Defined per-task.
class_docstr_tok: ["Base","Task","superclass","used","to","implement","new","tasks",".","Attributes",":","model",":","Model","name","string","for","the","task","in","use",".","local",":","Run","locally",".","Defaults","to","False",".","api_key",":","Backprop","API","key","for","non-local","inference",".","device",":","Device","to","run","inference","on",".","Defaults","to","``","cuda","''","if","available",".","models",":","All","supported","models","for","a","given","task","(","pulls","from","config",")",".","default_local_model",":","Which","model","the","task","will","default","to","if","initialized","with","none","provided",":","Defined","per-task","."]
code: def finetune(
      self,
      dataset=None,
      validation_split: Union[
          float, Tuple[List[int], List[int]]
      ] = 0.15,
      epochs: int = 20,
      batch_size: int = None,
      optimal_batch_size: int = None,
      early_stopping_epochs: int = 1,
      train_dataloader=None,
      val_dataloader=None,
      dataset_train: Dataset = None,
      dataset_valid: Dataset = None,
      step=None,
      configure_optimizers=None,
  ):
      self.batch_size = batch_size or 1
      """
          Core finetuning logic followed for all implemented tasks.
  
          Args:
              dataset: Torch dataset on which training will occur
              validation_split: Float between 0 and 1 that determines percentage of data to use for validation
              epochs: Integer specifying how many training iterations to run
              batch_size: Batch size when training. Leave as None to automatically determine batch size.
              optimal_batch_size: 
              early_stopping_epochs: Integer determining how many epochs will run before stopping without an improvement in validation loss
              train_dataloader: Dataloader for providing training data when finetuning. Defaults to inbuilt dataloder.
              val_dataloader: Dataloader for providing validation data when finetuning. Defaults to inbuilt dataloader.
              dataset_train: Torch dataset of training data (split before provided). Automatically made from dataset if None.
              dataset_valid: Torch dataset of validation data (split before provided). Automatically made from dataset if None.
              step: Function determining how to call model for a training step. Implemented per-task, can be overridden.
              configure_optimizers: Function that sets up the optimizer for training. Implemented per-task, can be overridden.
          """
  
      model_task_details = self.model.details.get(self.task)
  
      if not model_task_details or not model_task_details.get(
          "finetunable"
      ):
          raise NotImplementedError(
              f"Finetuning has not been implemented for model '{self.model.name}' for task '{self.task}'"
          )
  
      if not torch.cuda.is_available():
          raise Exception(
              "You need a cuda capable (Nvidia) GPU for finetuning"
          )
  
      if dataset == None:
          if not dataset_train and not dataset_valid:
              raise ValueError(
                  "Must either provide dataset or dataset_train and dataset_valid"
              )
  
      # Override dataloaders if provided
      if train_dataloader:
          self.train_dataloader = train_dataloader
  
      if val_dataloader:
          self.val_dataloader = val_dataloader
  
      # Make training and validation datasets
      if not dataset_train or not dataset_valid:
          if isinstance(validation_split, tuple):
              train_idx, val_idx = validation_split
  
              dataset_train = Subset(dataset, train_idx)
              dataset_valid = Subset(dataset, val_idx)
          elif type(validation_split) == float:
              len_train = int(len(dataset) * (1 - validation_split))
              len_valid = len(dataset) - len_train
              (
                  dataset_train,
                  dataset_valid,
              ) = torch.utils.data.random_split(
                  dataset, [len_train, len_valid]
              )
          else:
              raise ValueError(
                  f"Unsupported type '{type(validation_split)}' for validation_split"
              )
  
      self.dataset_train = dataset_train
      self.dataset_valid = dataset_valid
  
      # Override step if provided
      if step:
          self.step = step
  
      # Override optimizer if provided
      if configure_optimizers:
          self.configure_optimizers = configure_optimizers
  
      # Find batch size automatically
      if batch_size == None:
          temp_trainer = pl.Trainer(
              auto_scale_batch_size="power", gpus=-1
          )
          print("Finding the optimal batch size...")
          temp_trainer.tune(self)
  
          # Ensure that memory gets cleared
          del self.trainer
          del temp_trainer
          garbage_collection_cuda()
  
      trainer_kwargs = {}
  
      # Accumulate grad to optimal batch size
      if optimal_batch_size:
          # Don't go over limit
          batch_size = min(self.batch_size, optimal_batch_size)
          accumulate_grad_batches = max(
              1, int(optimal_batch_size / batch_size)
          )
          trainer_kwargs[
              "accumulate_grad_batches"
          ] = accumulate_grad_batches
  
      # Stop when val loss doesn't improve after a number of epochs
      if early_stopping_epochs != 0:
          early_stopping = EarlyStopping(
              monitor="val_loss", patience=early_stopping_epochs
          )
          trainer_kwargs["callbacks"] = [early_stopping]
  
      trainer = pl.Trainer(
          gpus=-1,
          max_epochs=epochs,
          checkpoint_callback=False,
          logger=False,
          **trainer_kwargs,
      )
  
      self.model.train()
      trainer.fit(self)
  
      # For some reason the model can end up on CPU after training
      self.model.to(self.model._model_device)
      self.model.eval()
      print(
          "Training finished! Save your model for later with backprop.save or upload it with backprop.upload"
      )
  
imports: from typing import Dict, List, Union, Tuple
  import logging
  import backprop
  from backprop.models import AutoModel
  import pytorch_lightning
  from torch.utils.data.dataloader import DataLoader
  import os
  import torch
  from torch.utils.data import Subset, Dataset
  from pytorch_lightning.utilities.memory import garbage_collection_cuda
  from pytorch_lightning.callbacks import EarlyStopping
label_desc: Use image node_id 11 for calling the Task obj's underlying member method code with example usage: obj.finetune(dataset, validation_split, epochs, batch_size, optimal_batch_size, early_stopping_epochs, train_dataloader, val_dataloader, dataset_train, dataset_valid, step, configure_optimizers) without return types