docstr: Create all the tensorflow operations for calculating gradients, processing the gradients and applying them
  :return: None
code: def _create_gradient_ops(self):
      """
      Create all the tensorflow operations for calculating gradients, processing the gradients and applying them
      :return: None
      """
  
      self.tensor_gradients = tf.gradients(
          self.total_loss, self.weights
      )
      self.gradients_norm = tf.global_norm(self.tensor_gradients)
  
      # gradient clipping
      if (
          self.network_parameters.clip_gradients is not None
          and self.network_parameters.clip_gradients != 0
      ):
          self._create_gradient_clipping_ops()
  
      # when using a shared optimizer, we create accumulators to store gradients from all the workers before
      # applying them
      if self.distributed_training:
          self._create_gradient_accumulators()
  
      # gradients of the outputs w.r.t. the inputs
      self.gradients_wrt_inputs = [
          {
              name: tf.gradients(output, input_ph)
              for name, input_ph in self.inputs.items()
          }
          for output in self.outputs
      ]
      self.gradients_weights_ph = [
          tf.placeholder(
              "float32",
              self.outputs[i].shape,
              "output_gradient_weights",
          )
          for i in range(len(self.outputs))
      ]
      self.weighted_gradients = []
      for i in range(len(self.outputs)):
          unnormalized_gradients = tf.gradients(
              self.outputs[i],
              self.weights,
              self.gradients_weights_ph[i],
          )
          # unnormalized gradients seems to be better at the time. TODO: validate this accross more environments
          # self.weighted_gradients.append(list(map(lambda x: tf.div(x, self.network_parameters.batch_size),
          #                                         unnormalized_gradients)))
          self.weighted_gradients.append(unnormalized_gradients)
  
      # defining the optimization process (for LBFGS we have less control over the optimizer)
      if self.optimizer_type != "LBFGS" and self.network_is_trainable:
          self._create_gradient_applying_ops()
  
imports: import os
  import time
  from typing import Any, List, Tuple, Dict
  import numpy
  import tensorflow
  from rl_coach.architectures.architecture import Architecture
  from rl_coach.architectures.tensorflow_components.savers import GlobalVariableSaver
  from rl_coach.base_parameters import AgentParameters, DistributedTaskParameters
  from rl_coach.core_types import GradientClippingMethod
  from rl_coach.saver import SaverCollection
  from rl_coach.spaces import SpacesDefinition
  from rl_coach.utils import force_list, squeeze_list, start_shell_command_and_wait
label_desc: Use image node_id 5 for calling the TensorFlowArchitecture obj's underlying member method code with example usage: obj._create_gradient_ops() without return types