class_docstr: A Model is a network (more precisely, a directed acyclic graph) of Steps,
  and it is defined from the input/output specification of the pipeline.
  Models have fit and predict routines that, together with graph-based engine,
  allow the automatic (feed-forward) computation of each of the pipeline steps
  when fed with data.
  
  Parameters
  ----------
  inputs
      Inputs to the model.
  
  outputs
      Outputs of the model.
  
  targets
      Targets of the model.
  
  name
      Name of the model (optional). If no name is passed, a name will be
      automatically generated.
docstr: Trains the model on the given input and target data.
  
  The model will automatically propagate the data through the pipeline and
  fit any internal steps that require training.
  
  Parameters
  ----------
  X
      Input data (independent variables). It can be either of:
  
          - A single array-like object (in the case of a single input)
          - A list of array-like objects (in the case of multiple inputs)
          - A dictionary mapping DataPlaceholders (or their names) to
            array-like objects. The keys must be among the inputs passed
            at instantiation.
  y
      Target data (dependent variables) (optional). It can be either of:
  
          - None (in the case all steps are either non-trainable and/or
            unsupervised learning steps)
          - A single array-like object (in the case of a single target)
          - A list of array-like objects(in the case of multiple targets)
          - A dictionary mapping target DataPlaceholders (or their names) to
            array-like objects. The keys must be among the targets passed
            at instantiation.
  
      Targets required by steps that were set as non-trainable might
      be omitted.
  
  fit_params
      Parameters passed to the fit method of each model step, where each
      parameter name has the form ``<step-name>__<parameter-name>``.
  
  Returns
  -------
docstr_tok: ["Trains","the","model","on","the","given","input","and","target","data",".","The","model","will","automatically","propagate","the","data","through","the","pipeline","and","fit","any","internal","steps","that","require","training",".","Parameters","--","--","--","--","--","X","Input","data","(","independent","variables",")",".","It","can","be","either","of",":","-","A","single","array-like","object","(","in","the","case","of","a","single","input",")","-","A","list","of","array-like","objects","(","in","the","case","of","multiple","inputs",")","-","A","dictionary","mapping","DataPlaceholders","(","or","their","names",")","to","array-like","objects",".","The","keys","must","be","among","the","inputs","passed","at","instantiation",".","y","Target","data","(","dependent","variables",")","(","optional",")",".","It","can","be","either","of",":","-","None","(","in","the","case","all","steps","are","either","non-trainable","and\/or","unsupervised","learning","steps",")","-","A","single","array-like","object","(","in","the","case","of","a","single","target",")","-","A","list","of","array-like","objects","(","in","the","case","of","multiple","targets",")","-","A","dictionary","mapping","target","DataPlaceholders","(","or","their","names",")","to","array-like","objects",".","The","keys","must","be","among","the","targets","passed","at","instantiation",".","Targets","required","by","steps","that","were","set","as","non-trainable","might","be","omitted",".","fit_params","Parameters","passed","to","the","fit","method","of","each","model","step",",","where","each","parameter","name","has","the","form","``","<","step-name",">","__","<","parameter-name",">","``",".","Returns","--","--","--","-"]
code: def fit(
      self,
      X: Union[ArrayLikes, DataDict],
      y: Optional[Union[ArrayLikes, DataDict]] = None,
      **fit_params,
  ):
      """Trains the model on the given input and target data.
  
      The model will automatically propagate the data through the pipeline and
      fit any internal steps that require training.
  
      Parameters
      ----------
      X
          Input data (independent variables). It can be either of:
  
              - A single array-like object (in the case of a single input)
              - A list of array-like objects (in the case of multiple inputs)
              - A dictionary mapping DataPlaceholders (or their names) to
                array-like objects. The keys must be among the inputs passed
                at instantiation.
      y
          Target data (dependent variables) (optional). It can be either of:
  
              - None (in the case all steps are either non-trainable and/or
                unsupervised learning steps)
              - A single array-like object (in the case of a single target)
              - A list of array-like objects(in the case of multiple targets)
              - A dictionary mapping target DataPlaceholders (or their names) to
                array-like objects. The keys must be among the targets passed
                at instantiation.
  
          Targets required by steps that were set as non-trainable might
          be omitted.
  
      fit_params
          Parameters passed to the fit method of each model step, where each
          parameter name has the form ``<step-name>__<parameter-name>``.
  
      Returns
      -------
  
      """
      # TODO: Add better error message to know which step failed in case of any error
      # TODO: Consider using joblib's Parallel and Memory classes to parallelize and cache computations
      # In graph parlance, the 'parallelizable' paths of a graph are called 'disjoint paths'
      # https://stackoverflow.com/questions/37633941/get-list-of-parallel-paths-in-a-directed-graph
      # TODO: How to behave when fit was called on a Model (and Step) that is trainable=False?
  
      # input/output normalization
      X_norm = self._normalize_data(X, self._internal_inputs)
      for input in self._internal_inputs:
          if input not in X_norm:
              raise ValueError("Missing input {}.".format(input))
  
      if y is not None:
          y_norm = self._normalize_data(y, self._internal_targets)
          for target in self._internal_targets:
              if target not in y_norm:
                  raise ValueError("Missing target {}.".format(target))
      else:
          y_norm = {}
  
      # Get steps and their fit_params
      # We allow unused targets to allow modifying the trainable flags
      # without having to change the targets accordingly.
      nodes = self._get_required_nodes(
          X_norm,
          y_norm,
          self._internal_outputs,
          allow_unused_targets=True,
          ignore_trainable_false=False,
      )
      fit_params_steps = defaultdict(dict)  # type: Dict[Step, Dict]
      for param_key, param_value in fit_params.items():
          # TODO: Add check for __. Add error message if step was not found
          step_name, _, param_name = param_key.partition("__")
          step = self.get_step(step_name)
          fit_params_steps[step.name][param_name] = param_value
  
      # Intermediate results are stored here
      # keys: DataPlaceholder instances, values: actual data (e.g. numpy arrays)
      results_cache = dict()
      results_cache.update(X_norm)
      results_cache.update(y_norm)
  
      for node in nodes:
          # TODO: Add a step.current_port attribute.
          # This attribute would be useful for introspection
          # during graph runtime to know which port is currently
          # under execution. This attribute can be used to choose
          # the appropriate compute_func in fit_compute_func.
          # The value would be always None when the graph is not
          # running (i.e. executing Model.fit or Model.predict)
          # and would be set to the appropriate value by the graph
          # runtime via a context manager.
          Xs = [results_cache[i] for i in node.inputs]
          successors = list(self.graph.successors(node))
  
          if not node.trainable:
              if successors:
                  self._compute_node(node, Xs, results_cache)
              continue
  
          ys = [results_cache[t] for t in node.targets]
          fit_params = fit_params_steps.get(node.step.name, {})
  
          if node.fit_compute_func is not None:
              self._fit_compute_node(
                  node, Xs, ys, results_cache, **fit_params
              )
              continue
  
          # ----- default behavior
          if node.fit_func is not None:
              self._fit_node(node, Xs, ys, **fit_params)
  
          if successors:
              self._compute_node(node, Xs, results_cache)
  
      return self
  
imports: from collections import defaultdict
  from typing import Union, List, Dict, Set, Iterable, Optional
  from baikal._core.data_placeholder import is_data_placeholder_list, DataPlaceholder
  from baikal._core.digraph import DiGraph
  from baikal._core.step import Step, InputStep, Node
  from baikal._core.typing import ArrayLike
  from baikal._core.utils import listify, safezip2, SimpleCache, unlistify
label_desc: Use image node_id 9 for calling the Model obj's underlying member method code with example usage: obj.fit(X, y) and returns: self