<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def optimize(self, n_iter, inplace=False, propagate_exception=False, **gradient_descent_params):
 """Run optmization on the embedding for a given number of steps. Parameters n_iter: int The number of optimization iterations. learning_rate: float The learning rate for t-SNE optimization. Typical values range between 100 to 1000. Setting the learning rate too low or too high may result in the points forming a "ball". This is also known as the crowding problem. exaggeration: float The exaggeration factor is used to increase the attractive forces of nearby points, producing more compact clusters. momentum: float Momentum accounts for gradient directions from previous iterations, resulting in faster convergence. negative_gradient_method: str Specifies the negative gradient approximation method to use. For smaller data sets, the Barnes-Hut approximation is appropriate and can be set using one of the following aliases: ``bh``, ``BH`` or ``barnes-hut``. For larger data sets, the FFT accelerated interpolation method is more appropriate and can be set using one of the following aliases: ``fft``, ``FFT`` or ``ìnterpolation``. theta: float This is the trade-off parameter between speed and accuracy of the tree approximation method. Typical values range from 0.2 to 0.8. The value 0 indicates that no approximation is to be made and produces exact results also producing longer runtime. n_interpolation_points: int Only used when ``negative_gradient_method="fft"`` or its other aliases. The number of interpolation points to use within each grid cell for interpolation based t-SNE. It is highly recommended leaving this value at the default 3. min_num_intervals: int Only used when ``negative_gradient_method="fft"`` or its other aliases. The minimum number of grid cells to use, regardless of the ``ints_in_interval`` parameter. Higher values provide more accurate gradient estimations. inplace: bool Whether or not to create a copy of the embedding or to perform updates inplace. propagate_exception: bool The optimization process can be interrupted using callbacks. This flag indicates whether we should propagate that exception or to simply stop optimization and return the resulting embedding. random_state: Union[int, RandomState] The random state parameter follows the convention used in scikit-learn. If the value is an int, random_state is the seed used by the random number generator. If the value is a RandomState instance, then it will be used as the random number generator. If the value is None, the random number generator is the RandomState instance used by `np.random`. n_jobs: int The number of threads to use while running t-SNE. This follows the scikit-learn convention, ``-1`` meaning all processors, ``-2`` meaning all but one, etc. callbacks: Callable[[int, float, np.ndarray] -> bool] Callbacks, which will be run every ``callbacks_every_iters`` iterations. callbacks_every_iters: int How many iterations should pass between each time the callbacks are invoked. Returns ------- PartialTSNEEmbedding An optimized partial t-SNE embedding. Raises ------ OptimizationInterrupt If a callback stops the optimization and the ``propagate_exception`` flag is set, then an exception is raised. """

        # Typically we want to return a new embedding and keep the old one intact
        if inplace:
            embedding = self
        else:
            embedding = PartialTSNEEmbedding(
                np.copy(self),
                self.reference_embedding,
                self.P,
                optimizer=self.optimizer.copy(),
                **self.gradient_descent_params,
            )

        # If optimization parameters were passed to this funciton, prefer those
        # over the defaults specified in the TSNE object
        optim_params = dict(self.gradient_descent_params)
        optim_params.update(gradient_descent_params)
        _handle_nice_params(optim_params)
        optim_params["n_iter"] = n_iter

        try:
            # Run gradient descent with the embedding optimizer so gains are
            # properly updated and kept
            error, embedding = embedding.optimizer(
                embedding=embedding,
                reference_embedding=self.reference_embedding,
                P=self.P,
                **optim_params,
            )

        except OptimizationInterrupt as ex:
            log.info("Optimization was interrupted with callback.")
            if propagate_exception:
                raise ex
            error, embedding = ex.error, ex.final_embedding

        embedding.kl_divergence = error

        return embedding