docs: def optimize_hyperparameters(self,
                                   problems,
                                   parameter_locks=None,
                                   smoothing=20,
                                   max_iterations=100,
                                   _meta_optimizer=None,
                                   _low_memory=True):
          """Optimize hyperparameters for a given problem.
  
          Args:
              parameter_locks: a list of strings, each corresponding to a hyperparamter
                               that should not be optimized.
              problems: Either a single problem, or a list of problem instances,
                       allowing optimization based on multiple similar problems.
              smoothing: int; number of runs to average over for each set of hyperparameters.
              max_iterations: The number of iterations to optimize before stopping.
              _low_memory: disable performance enhancements to save memory
                           (they use a lot of memory otherwise).
          """
          if smoothing <= 0:
              raise ValueError('smoothing must be > 0')
  
          # problems supports either one or many problem instances
          if isinstance(problems, collections.Iterable):
              for problem in problems:
                  if not isinstance(problem, Problem):
                      raise TypeError(
                          'problem must be Problem instance or list of Problem instances'
                      )
          elif isinstance(problems, Problem):
              problems = [problems]
          else:
              raise TypeError(
                  'problem must be Problem instance or list of Problem instances'
              )
  
          # Copy to avoid permanent modification
          meta_parameters = copy.deepcopy(self._hyperparameters)
  
          # First, handle parameter locks, since it will modify our
          # meta_parameters dict
          locked_values = _parse_parameter_locks(self, meta_parameters,
                                                 parameter_locks)
  
          # We need to know the size of our chromosome,
          # based on the hyperparameters to optimize
          solution_size = _get_hyperparameter_solution_size(meta_parameters)
  
          # We also need to create a decode function to transform the binary solution
          # into parameters for the metaheuristic
          decode = _make_hyperparameter_decode_func(locked_values,
                                                    meta_parameters)
  
          # A master fitness dictionary can be stored for use between calls
          # to meta_fitness
          if _low_memory:
              master_fitness_dict = None
          else:
              master_fitness_dict = {}
  
          additional_parameters = {
              '_optimizer': self,
              '_problems': problems,
              '_runs': smoothing,
              '_master_fitness_dict': master_fitness_dict,
          }
          META_FITNESS = Problem(
              _meta_fitness_func,
              decode_function=decode,
              fitness_kwargs=additional_parameters)
          if _meta_optimizer is None:
              # Initialize default meta optimizer
              # GenAlg is used because it supports both discrete and continous
              # attributes
              from optimal import GenAlg
  
              # Create metaheuristic with computed decode function and soltuion
              # size
              _meta_optimizer = GenAlg(solution_size)
          else:
              # Adjust supplied metaheuristic for this problem
              _meta_optimizer._solution_size = solution_size
  
          # Determine the best hyperparameters with a metaheuristic
          best_parameters = _meta_optimizer.optimize(
              META_FITNESS, max_iterations=max_iterations)
  
          # Set the hyperparameters inline
          self._set_hyperparameters(best_parameters)
  
          # And return
          return best_parameters