Repository: Theano/Theano

Issue:
Bug with gradient of T.clip(x,0.,x)
The perform of T.clip allows the user to use T.clip as a replacement for T.minimum or T.maximum. The gradient however, does not support this behavior. i.e.
# normal use-case, works as advertised

x = T.scalar()
y = T.clip(x, 0., 10)
g = T.grad(y, x)
f = theano.function([x], [y,g])

> > In [59]: f(5)
> > Out[59]: [array(5.0), array(1.0)]
> > In [60]: f(20)
> > Out[60]: [array(10.0), array(0.0)]
# using T.clip with symbol in its upper (or lower) bound: gradient is not as expected

x = T.scalar()
y = T.clip(x, 0., x)
g = T.grad(y, x)
f = theano.function([x], [y,g])

> > In [65]: f(5)
> > Out[65]: [array(5.0), array(0.0)]

Expected answer would have been 1.

Two possible solutions: 
(1) raise an error when user attempts to use T.clip(x, 1, x) or T.clip(x, x, 10)
(2) fix T.clip gradient to work when the symbol itself is used in one of the lower/upper bounds.



Discussion:
Good bug.

On Fri, May 4, 2012 at 6:06 PM, gdesjardins
reply@reply.github.com
wrote:

> Two possible solutions:
> (1) raise an error when user attempts to use T.clip(x, 1, x) or T.clip(x, x, 10)
> (2) fix T.clip gradient to work when the symbol itself is used in one of the lower/upper bounds.

I'm not sure I see the problem.
suppose the user writes clip(x, f(x), g(x))

The gradient is (f(x) <= x <= g(x)) ? 1.0 : 0.0.

Is it more complicated?

On Fri, May 4, 2012 at 8:14 PM, James Bergstra james.bergstra@gmail.com wrote:

> Good bug.
> 
> On Fri, May 4, 2012 at 6:06 PM, gdesjardins
> reply@reply.github.com
> wrote:
> 
> > Two possible solutions:
> > (1) raise an error when user attempts to use T.clip(x, 1, x) or T.clip(x, x, 10)
> > (2) fix T.clip gradient to work when the symbol itself is used in one of the lower/upper bounds.
> 
> I'm not sure I see the problem.
> suppose the user writes clip(x, f(x), g(x))
> 
> The gradient is (f(x) <= x <= g(x)) ? 1.0 : 0.0.
> 
> Is it more complicated?

I'll concede that this changes the definition of clip at the boundary,
so it's not obviously the way to go. Still, I put it forward as
another fix.

Yeah I agree that your fix is definitely better (not sure why I didn't think of it ?!). Given that the gradient at that point is not even defined, I'm not sure how problematic it is to change the bounds to be inclusive. I'll submit a pull request tomorrow and we'll see if anyone has any complaints...

What is the state of this? I don't remember a PR.

umm, I haven't thought or done anything since the last email in this thread.

On Wed, Jul 4, 2012 at 10:13 PM, nouiz
reply@reply.github.com
wrote:

> What is the state of this? I don't remember a PR.
> 
> ---
> 
> Reply to this email directly or view it on GitHub:
> https://github.com/Theano/Theano/issues/633#issuecomment-6770427


Resolving patch:
diff --git a/theano/scalar/basic.py b/theano/scalar/basic.py
--- a/theano/scalar/basic.py
+++ b/theano/scalar/basic.py
@@ -1792,7 +1792,7 @@ def c_code(self, node, name, (x, min, max), (z, ), sub):
 
     def grad(self, (x, mn, mx), (gz, )):
         assert gz.type not in complex_types
-        gx = ((x > mn) & (x < mx)) * gz
+        gx = ((x >= mn) & (x <= mx)) * gz
         gmn = (x < mn) * gz
         gmx = (x > mx) * gz
 


Test patch:
diff --git a/theano/scalar/tests/test_basic.py b/theano/scalar/tests/test_basic.py
--- a/theano/scalar/tests/test_basic.py
+++ b/theano/scalar/tests/test_basic.py
@@ -14,13 +14,14 @@
 import theano
 from theano.gof import FunctionGraph
 from theano import gof
+from theano.tests import unittest_tools as utt
 
 from theano.scalar.basic import (floats, float32, float64,
                                  ints, int8, int32, complex64,
                                  ComplexError, IntDiv, TrueDiv,
-                                 Composite, add, div_proxy,
+                                 Composite, add, div_proxy, clip,
                                  and_, eq, neq, invert, mul)
-
+import numpy
 
 def inputs():
     return floats('xyz')
@@ -56,6 +57,43 @@ def tes_mod(self):
                     ):
             self.assertTrue(fn(a,b) == a%b, (a,))
 
+
+    def test_clip_grad(self):
+        #This is testing for the issue #633
+        x, y = floats('xy')
+        a = theano.tensor.clip(x, y, x)
+        g = theano.gradient.grad(a, x)
+        fn = gof.DualLinker().accept(FunctionGraph([x, y], [g])).make_function()
+
+        # Test the other way around as well
+        a2 = theano.tensor.clip(x, x, y)
+        g2 = theano.gradient.grad(a2, x)
+        fn2 = gof.DualLinker().accept(FunctionGraph([x, y], [g2])).make_function()
+
+        # Test for the equal case too .
+        a3 = theano.tensor.clip(x, x, x)
+        g3 = theano.gradient.grad(a3, x)
+        fn3 = gof.DualLinker().accept(FunctionGraph([x], [g3])).make_function()
+
+        rng = numpy.random.RandomState(utt.fetch_seed())
+
+        ntests = 50
+        for i in xrange(ntests):
+            xval = rng.rand(1)
+            #To ensure that the min < x .
+            yval_mn = rng.rand(1) - 1.0
+
+            #To ensure that the max > x.
+            yval_mx = rng.rand(1) + 1.0
+
+            aval = fn(xval, yval_mn)
+            aval2 = fn2(xval, yval_mx)
+            aval3 = fn3(xval)
+            self.assertTrue(aval == 1.)
+            self.assertTrue(aval2 == 1.)
+            self.assertTrue(aval3 == 1.)
+
+
 class test_composite(unittest.TestCase):
 
     def test_straightforward(self):