docs: def minimize(self, seed_input=None, max_iter=200,
                   input_modifiers=None, grad_modifier=None,
                   callbacks=None, verbose=True):
          """Performs gradient descent on the input image with respect to defined losses.
  
          Args:
              seed_input: An N-dim numpy array of shape: `(samples, channels, image_dims...)` if `image_data_format=
                  channels_first` or `(samples, image_dims..., channels)` if `image_data_format=channels_last`.
                  Seeded with random noise if set to None. (Default value = None)
              max_iter: The maximum number of gradient descent iterations. (Default value = 200)
              input_modifiers: A list of [InputModifier](vis.input_modifiers#inputmodifier) instances specifying
                  how to make `pre` and `post` changes to the optimized input during the optimization process.
                  `pre` is applied in list order while `post` is applied in reverse order. For example,
                  `input_modifiers = [f, g]` means that `pre_input = g(f(inp))` and `post_input = f(g(inp))`
              grad_modifier: gradient modifier to use. See [grad_modifiers](vis.grad_modifiers.md). If you don't
                  specify anything, gradients are unchanged. (Default value = None)
              callbacks: A list of [OptimizerCallback](vis.callbacks#optimizercallback) instances to trigger.
              verbose: Logs individual losses at the end of every gradient descent iteration.
                  Very useful to estimate loss weight factor(s). (Default value = True)
  
          Returns:
              The tuple of `(optimized input, grads with respect to wrt, wrt_value)` after gradient descent iterations.
          """
          seed_input = self._get_seed_input(seed_input)
          input_modifiers = input_modifiers or []
          grad_modifier = _identity if grad_modifier is None else get(grad_modifier)
  
          callbacks = callbacks or []
          if verbose:
              callbacks.append(_PRINT_CALLBACK)
  
          cache = None
          best_loss = float('inf')
          best_input = None
  
          grads = None
          wrt_value = None
  
          for i in range(max_iter):
              # Apply modifiers `pre` step
              for modifier in input_modifiers:
                  seed_input = modifier.pre(seed_input)
  
              # 0 learning phase for 'test'
              computed_values = self.compute_fn([seed_input, 0])
              losses = computed_values[:len(self.loss_names)]
              named_losses = list(zip(self.loss_names, losses))
              overall_loss, grads, wrt_value = computed_values[len(self.loss_names):]
  
              # TODO: theano grads shape is inconsistent for some reason. Patch for now and investigate later.
              if grads.shape != wrt_value.shape:
                  grads = np.reshape(grads, wrt_value.shape)
  
              # Apply grad modifier.
              grads = grad_modifier(grads)
  
              # Trigger callbacks
              for c in callbacks:
                  c.callback(i, named_losses, overall_loss, grads, wrt_value)
  
              # Gradient descent update.
              # It only makes sense to do this if wrt_tensor is input_tensor. Otherwise shapes wont match for the update.
              if self.wrt_tensor_is_input_tensor:
                  step, cache = self._rmsprop(grads, cache)
                  seed_input += step
  
              # Apply modifiers `post` step
              for modifier in reversed(input_modifiers):
                  seed_input = modifier.post(seed_input)
  
              if overall_loss < best_loss:
                  best_loss = overall_loss.copy()
                  best_input = seed_input.copy()
  
          # Trigger on_end
          for c in callbacks:
              c.on_end()
  
          return utils.deprocess_input(best_input[0], self.input_range), grads, wrt_value