code: def add_inputs(self, es):
          
          for e in es:
              ensure_freshness(e)
          for (fb,bb) in self.builder_layers[:-1]:
              fs = fb.initial_state().transduce(es)
              bs = bb.initial_state().transduce(reversed(es))
              es = [concatenate([f,b]) for f,b in zip(fs, reversed(bs))]
          (fb,bb) = self.builder_layers[-1]
          fs = fb.initial_state().add_inputs(es)
          bs = bb.initial_state().add_inputs(reversed(es))
          return [(f,b) for f,b in zip(fs, reversed(bs))]
docs: returns the list of state pairs (stateF, stateB) obtained by adding 
          inputs to both forward (stateF) and backward (stateB) RNNs.  
  
          @param es: a list of Expression
  
          see also transduce(xs)
  
          .transduce(xs) is different from .add_inputs(xs) in the following way:
  
              .add_inputs(xs) returns a list of RNNState pairs. RNNState objects can be
               queried in various ways. In particular, they allow access to the previous
               state, as well as to the state-vectors (h() and s() )
  
              .transduce(xs) returns a list of Expression. These are just the output
               expressions. For many cases, this suffices. 
               transduce is much more memory efficient than add_inputs.