Skip to content

Commit 1d43d4c

Browse files
committed
Add tests for NoGradient support in Java custom gradients
1 parent e2fa04b commit 1d43d4c

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package org.tensorflow;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.util.List;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.condition.DisabledOnOs;
10+
import org.junit.jupiter.api.condition.OS;
11+
import org.tensorflow.op.CustomGradient;
12+
import org.tensorflow.op.Ops;
13+
import org.tensorflow.op.RawCustomGradient;
14+
import org.tensorflow.op.nn.SparseSoftmaxCrossEntropyWithLogits;
15+
import org.tensorflow.types.TFloat32;
16+
import org.tensorflow.types.TInt32;
17+
18+
@DisabledOnOs(OS.WINDOWS)
19+
public class CustomGradientsTest {
20+
21+
@Test
22+
public void noGradientNullIsSupported() {
23+
// Register a custom gradient for an op that has NO native gradient in TF core.
24+
CustomGradient<SparseSoftmaxCrossEntropyWithLogits.Inputs> grad =
25+
(tf, op, gradInputs) -> {
26+
@SuppressWarnings("unchecked")
27+
Operand<TFloat32> gLoss = (Operand<TFloat32>) gradInputs.get(0); // [B]
28+
29+
@SuppressWarnings("unchecked")
30+
Operand<TFloat32> logits = op.features;
31+
32+
SparseSoftmaxCrossEntropyWithLogits<TFloat32> xent =
33+
SparseSoftmaxCrossEntropyWithLogits.create(tf.scope(), logits, op.labels);
34+
35+
Operand<TFloat32> backprop = xent.backprop(); // [B,C]
36+
Operand<TFloat32> gLossE = tf.expandDims(gLoss, tf.constant(1)); // [B,1]
37+
Operand<TFloat32> dLogits = tf.math.mul(gLossE, backprop); // [B,C]
38+
39+
// labels: NoGradient
40+
return java.util.Arrays.asList(dLogits, null);
41+
};
42+
43+
assertTrue(
44+
TensorFlow.registerCustomGradient(SparseSoftmaxCrossEntropyWithLogits.Inputs.class, grad));
45+
46+
try (Graph g = new Graph()) {
47+
Ops tf = Ops.create(g);
48+
49+
// Small fixed shapes to be able to create an explicit seed (avoid OnesLike in addGradients).
50+
Operand<TFloat32> logits = tf.constant(new float[][] {{1f, 2f, 3f}, {3f, 2f, 1f}});
51+
Operand<TInt32> labels = tf.constant(new int[] {2, 0});
52+
53+
SparseSoftmaxCrossEntropyWithLogits<TFloat32> xent =
54+
SparseSoftmaxCrossEntropyWithLogits.create(tf.scope(), logits, labels);
55+
56+
Output<TFloat32> loss = xent.loss(); // [2]
57+
Operand<TFloat32> seed = tf.constant(new float[] {1f, 1f}); // same shape as loss
58+
59+
Output<?>[] grads =
60+
g.addGradients(
61+
"seed",
62+
new Output<?>[] {loss},
63+
new Output<?>[] {logits.asOutput(), labels.asOutput()},
64+
new Output<?>[] {seed.asOutput()});
65+
66+
// logits grad exists, labels grad must be "NoGradient" (represented as a CLOSED Output)
67+
assertNotNull(grads);
68+
assertEquals(2, grads.length);
69+
assertNotNull(grads[0], "Expected gradient for logits");
70+
assertNotNull(grads[1], "Expected an Output placeholder for labels gradient");
71+
assertTrue(grads[1].isClosed(), "Expected closed gradient (NoGradient) for labels");
72+
}
73+
}
74+
75+
@Test
76+
public void sigmoidGradHasCustomGradientWithoutOnesLikeSeed() {
77+
// Register custom gradient for SigmoidGrad (if already registered, it will return false,
78+
// but the test can still pass because the gradient exists in the current process).
79+
TensorFlow.registerCustomGradient(
80+
"SigmoidGrad",
81+
(RawCustomGradient)
82+
(tf, op, gradInputs) -> {
83+
@SuppressWarnings("unchecked")
84+
Operand<TFloat32> y = (Operand<TFloat32>) op.input(0); // sigmoid(x)
85+
@SuppressWarnings("unchecked")
86+
Operand<TFloat32> dy = (Operand<TFloat32>) op.input(1); // upstream into SigmoidGrad
87+
@SuppressWarnings("unchecked")
88+
Operand<TFloat32> upstream = (Operand<TFloat32>) gradInputs.get(0);
89+
90+
Operand<TFloat32> one = tf.constant(1.0f);
91+
Operand<TFloat32> yTimesOneMinusY = tf.math.mul(y, tf.math.sub(one, y));
92+
93+
// dL/d(dy) = upstream * y*(1-y)
94+
Operand<TFloat32> dDy = tf.math.mul(upstream, yTimesOneMinusY);
95+
96+
// dL/d(y) not needed for this test; return zeros to keep it non-null.
97+
Operand<TFloat32> dY = tf.zerosLike(y);
98+
99+
return List.of(dY, dDy);
100+
});
101+
102+
try (Graph g = new Graph()) {
103+
Ops tf = Ops.create(g);
104+
105+
Operand<TFloat32> x = tf.placeholder(TFloat32.class);
106+
Operand<TFloat32> y = tf.math.sigmoid(x);
107+
108+
// Provide an explicit seed dy to avoid Graph.addGradients defaulting to OnesLike(y)
109+
Operand<TFloat32> seed = tf.fill(tf.shape(y), tf.constant(1.0f));
110+
111+
Output<?>[] grads =
112+
g.addGradients(
113+
"seed",
114+
new Output<?>[] {y.asOutput()},
115+
new Output<?>[] {x.asOutput()},
116+
new Output<?>[] {seed.asOutput()});
117+
118+
assertNotNull(grads);
119+
assertEquals(1, grads.length);
120+
assertNotNull(grads[0], "Expected a non-null gradient for sigmoid(x) wrt x.");
121+
assertTrue(!grads[0].isClosed(), "Expected an active Output for d(sigmoid)/dx.");
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)