Shape inference for Theano backend
diff --git a/keras/backend/theano_backend.py b/keras/backend/theano_backend.py
index b8d8070b5e2..03696f9f4a9 100644
--- a/keras/backend/theano_backend.py
+++ b/keras/backend/theano_backend.py
@@ -350,7 +350,6 @@ def batch_dot(x, y, axes=None):
 
         output_shape = (100, 30)
     """
-    # TODO: `keras_shape` inference.
     if isinstance(axes, int):
         axes = (axes, axes)
     if axes is None:
@@ -359,12 +358,26 @@ def batch_dot(x, y, axes=None):
     out = T.batched_tensordot(x, y, axes=axes)
     if ndim(out) == 1:
         out = expand_dims(out, 1)
+
+    if hasattr(x, '_keras_shape') and hasattr(y, '_keras_shape'):
+        shape = []
+        for axis in range(len(x._keras_shape)):
+            if axis != axes[0]:
+                shape.append(x._keras_shape[axis])
+        for axis in range(1, len(y._keras_shape)):
+            if axis != axes[1]:
+                shape.append(y._keras_shape[axis])
+        if len(shape) == 1:
+            shape.append(1)     # Expand dims if ndim == 1
+        out._keras_shape = tuple(shape)
     return out
 
 
 def transpose(x):
-    # TODO: `keras_shape` inference.
-    return T.transpose(x)
+    y = T.transpose(x)
+    if hasattr(x, '_keras_shape'):
+        y._keras_shape = tuple(reversed(x._keras_shape))
+    return y
 
 
 def gather(reference, indices):
@@ -671,9 +684,11 @@ def permute_dimensions(x, pattern):
     pattern should be a tuple or list of
     dimension indices, e.g. [0, 2, 1].
     """
-    # TODO: `keras_shape` inference.
     pattern = tuple(pattern)
-    return x.dimshuffle(pattern)
+    y = x.dimshuffle(pattern)
+    if hasattr(x, '_keras_shape'):
+        y._keras_shape = tuple(np.asarray(x._keras_shape)[list(pattern)])
+    return y
 
 
 def repeat_elements(x, rep, axis):
@@ -682,8 +697,12 @@ def repeat_elements(x, rep, axis):
     If x has shape (s1, s2, s3) and axis=1, the output
     will have shape (s1, s2 * rep, s3).
     """
-    # TODO: `keras_shape` inference.
-    return T.repeat(x, rep, axis=axis)
+    y = T.repeat(x, rep, axis=axis)
+    if hasattr(x, '_keras_shape'):
+        y._keras_shape = list(x._keras_shape)
+        y._keras_shape[axis] = x._keras_shape[axis] * rep
+        y._keras_shape = tuple(y._keras_shape)
+    return y
 
 
 def resize_images(X, height_factor, width_factor, data_format):
@@ -693,7 +712,6 @@ def resize_images(X, height_factor, width_factor, data_format):
     by a factor of (height_factor, width_factor). Both factors should be
     positive integers.
     """
-    # TODO: `keras_shape` inference.
     if data_format == 'channels_first':
         output = repeat_elements(X, height_factor, axis=2)
         output = repeat_elements(output, width_factor, axis=3)
@@ -713,7 +731,6 @@ def resize_volumes(X, depth_factor, height_factor, width_factor, data_format):
     by a factor of (depth_factor, height_factor, width_factor).
     Both factors should be positive integers.
     """
-    # TODO: `keras_shape` inference.
     if data_format == 'channels_first':
         output = repeat_elements(X, depth_factor, axis=2)
         output = repeat_elements(output, height_factor, axis=3)
@@ -734,10 +751,15 @@ def repeat(x, n):
     If x has shape (samples, dim) and n=2,
     the output will have shape (samples, 2, dim).
     """
-    # TODO: `keras_shape` inference.
     assert x.ndim == 2
-    x = x.dimshuffle((0, 'x', 1))
-    return T.extra_ops.repeat(x, n, axis=1)
+    y = x.dimshuffle((0, 'x', 1))
+    y = T.extra_ops.repeat(y, n, axis=1)
+    if hasattr(x, '_keras_shape'):
+        shape = list(x._keras_shape)
+        shape.insert(1, n)
+        y._keras_shape = tuple(shape)
+
+    return y
 
 
 def arange(start, stop=None, step=1, dtype='int32'):
@@ -759,23 +781,25 @@ def tile(x, n):
 
 
 def flatten(x):
-    # TODO: `keras_shape` inference.
-    return T.flatten(x)
+    y = T.flatten(x)
+    if hasattr(x, '_keras_shape'):
+        y._keras_shape = (np.prod(x._keras_shape), )
+    return y
 
 
 def batch_flatten(x):
     """Turn a n-D tensor into a 2D tensor where
     the first dimension is conserved.
     """
-    # TODO: `keras_shape` inference.
-    x = T.reshape(x, (x.shape[0], T.prod(x.shape) // x.shape[0]))
-    return x
+    y = T.reshape(x, (x.shape[0], T.prod(x.shape[1:])))
+    if hasattr(x, '_keras_shape'):
+        y._keras_shape = (x._keras_shape[0], np.prod(x._keras_shape[1:]))
+    return y
 
 
 def expand_dims(x, axis=-1):
     """Add a 1-sized dimension at index "dim".
     """
-    # TODO: `keras_shape` inference.
     pattern = [i for i in range(x.type.ndim)]
     if axis < 0:
         if x.type.ndim == 0:
@@ -783,16 +807,25 @@ def expand_dims(x, axis=-1):
         else:
             axis = axis % x.type.ndim + 1
     pattern.insert(axis, 'x')
-    return x.dimshuffle(pattern)
+    y = x.dimshuffle(pattern)
+    if hasattr(x, '_keras_shape'):
+        shape = list(x._keras_shape)
+        shape.insert(axis, 1)
+        y._keras_shape = tuple(shape)
+    return y
 
 
 def squeeze(x, axis):
     """Remove a 1-dimension from the tensor at index "axis".
     """
-    # TODO: `keras_shape` inference.
     shape = list(x.shape)
     shape.pop(axis)
-    return T.reshape(x, tuple(shape))
+    y = T.reshape(x, tuple(shape))
+    if hasattr(x, '_keras_shape'):
+        kshape = list(x._keras_shape)
+        kshape.pop(axis)
+        y._keras_shape = tuple(kshape)
+    return y
 
 
 def temporal_padding(x, padding=(1, 1)):
@@ -820,7 +853,6 @@ def spatial_2d_padding(x, padding=((1, 1), (1, 1)), data_format=None):
     """Pad the 2nd and 3rd dimensions of a 4D tensor
     with "padding[0]" and "padding[1]" (resp.) zeros left and right.
     """
-    # TODO: `keras_shape` inference.
     assert len(padding) == 2
     assert len(padding[0]) == 2
     assert len(padding[1]) == 2
@@ -855,7 +887,9 @@ def spatial_2d_padding(x, padding=((1, 1), (1, 1)), data_format=None):
                    slice(None))
     else:
         raise ValueError('Invalid data_format:', data_format)
-    return T.set_subtensor(output[indices], x)
+    y = T.set_subtensor(output[indices], x)
+    y._keras_shape = output_shape
+    return y
 
 
 def spatial_3d_padding(x, padding=((1, 1), (1, 1), (1, 1)), data_format=None):
diff --git a/tests/keras/backend/backend_test.py b/tests/keras/backend/backend_test.py
index 59f3e456d0f..e633104d9cb 100644
--- a/tests/keras/backend/backend_test.py
+++ b/tests/keras/backend/backend_test.py
@@ -21,11 +21,14 @@ def check_single_tensor_operation(function_name, input_shape, **kwargs):
     xth = KTH.variable(val)
     xtf = KTF.variable(val)
 
-    zth = KTH.eval(getattr(KTH, function_name)(xth, **kwargs))
+    _zth = getattr(KTH, function_name)(xth, **kwargs)
+    zth = KTH.eval(_zth)
     ztf = KTF.eval(getattr(KTF, function_name)(xtf, **kwargs))
 
     assert zth.shape == ztf.shape
     assert_allclose(zth, ztf, atol=1e-05)
+    if hasattr(_zth, '_keras_shape'):
+        assert _zth._keras_shape == zth.shape
 
 
 def check_two_tensor_operation(function_name, x_input_shape,
@@ -40,11 +43,14 @@ def check_two_tensor_operation(function_name, x_input_shape,
     yth = KTH.variable(yval)
     ytf = KTF.variable(yval)
 
-    zth = KTH.eval(getattr(KTH, function_name)(xth, yth, **kwargs))
+    _zth = getattr(KTH, function_name)(xth, yth, **kwargs)
+    zth = KTH.eval(_zth)
     ztf = KTF.eval(getattr(KTF, function_name)(xtf, ytf, **kwargs))
 
     assert zth.shape == ztf.shape
     assert_allclose(zth, ztf, atol=1e-05)
+    if hasattr(_zth, '_keras_shape'):
+        assert _zth._keras_shape == zth.shape
 
 
 def check_composed_tensor_operations(first_function_name, first_function_args,
@@ -116,6 +122,7 @@ def test_shape_operations(self):
                                       pattern=(2, 0, 1))
         check_single_tensor_operation('repeat', (4, 1), n=3)
         check_single_tensor_operation('flatten', (4, 1))
+        check_single_tensor_operation('batch_flatten', (20, 2, 5))
         check_single_tensor_operation('expand_dims', (4, 3), axis=-1)
         check_single_tensor_operation('expand_dims', (4, 3, 2), axis=1)
         check_single_tensor_operation('squeeze', (4, 3, 1), axis=2)
@@ -134,8 +141,8 @@ def test_repeat_elements(self):
 
             for rep_axis in range(ndims):
                 np_rep = np.repeat(arr, reps, axis=rep_axis)
-                th_rep = KTH.eval(
-                    KTH.repeat_elements(arr_th, reps, axis=rep_axis))
+                th_z = KTH.repeat_elements(arr_th, reps, axis=rep_axis)
+                th_rep = KTH.eval(th_z)
                 tf_rep = KTF.eval(
                     KTF.repeat_elements(arr_tf, reps, axis=rep_axis))
 
@@ -143,6 +150,8 @@ def test_repeat_elements(self):
                 assert tf_rep.shape == np_rep.shape
                 assert_allclose(np_rep, th_rep, atol=1e-05)
                 assert_allclose(np_rep, tf_rep, atol=1e-05)
+                if hasattr(th_z, '_keras_shape'):
+                    assert th_z._keras_shape == th_rep.shape
 
     def test_tile(self):
         shape = (3, 4)
@@ -151,9 +160,12 @@ def test_tile(self):
         arr_tf = KTF.variable(arr)
 
         n = (2, 1)
-        th_rep = KTH.eval(KTH.tile(arr_th, n))
+        th_z = KTH.tile(arr_th, n)
+        th_rep = KTH.eval(th_z)
         tf_rep = KTF.eval(KTF.tile(arr_tf, n))
         assert_allclose(tf_rep, th_rep, atol=1e-05)
+        if hasattr(th_z, '_keras_shape'):
+            assert th_z._keras_shape == th_rep.shape
 
     def test_value_manipulation(self):
         val = np.random.random((4, 2))

Shape inferences added

| Op |Test coverage|
|-----------------|:-----------------:|
| `flatten` | :heavy_check_mark: |
| `repeat_elements` | :heavy_check_mark: |
| `batch_flatten` | :heavy_check_mark: |
| `squeeze` | :heavy_check_mark: |
| `permute_dimensions` | :heavy_check_mark: |
| `repeat` | :heavy_check_mark: |
| `expand_dims` | :heavy_check_mark: |
| `spatial_2d_pooling` |
|`transpose`|:heavy_check_mark: |
|`batch_dot`|:heavy_check_mark: |

The original PR was at https://github.com/fchollet/keras/pull/5610. The suggested changes have been fixed. Had to remove shape inference for `tile` due to certain issues (still working on it)