Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Gradient descent optimizer tests #1184

Merged
merged 12 commits into from
Nov 10, 2023
7 changes: 6 additions & 1 deletion src/TensorFlowNET.Core/Variables/variables.py.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ public static List<IVariableV1> global_variables(string scope = null)
public static Operation variables_initializer(IVariableV1[] var_list, string name = "init")
{
if (var_list.Length > 0)
{
return control_flow_ops.group(var_list.Select(x => x.Initializer).ToArray(), name);
}
else
return gen_control_flow_ops.no_op(name: name);
}
Expand Down Expand Up @@ -155,7 +157,10 @@ public static Operation _safe_initial_value_from_op(string name, Operation op, D

public static Tensor global_variables_initializer()
{
throw new NotImplementedException();
novikov-alexander marked this conversation as resolved.
Show resolved Hide resolved
// if context.executing_eagerly():
// return control_flow_ops.no_op(name = "global_variables_initializer")
var group = variables_initializer(global_variables().ToArray());
novikov-alexander marked this conversation as resolved.
Show resolved Hide resolved
return group;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -776,8 +776,6 @@ public void testUnconnectedGradientsNoneUnconnectedGradients()
[TestMethod]
public void testUnconnectedGradientsZerosUnconnectedGradients()
{


//def testUnconnectedGradientsZerosUnconnectedGradients(self):
// with ops.Graph().as_default():
// x = constant(1.0, shape=[2, 2])
Expand Down
178 changes: 176 additions & 2 deletions test/TensorFlowNET.UnitTest/PythonTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,37 @@ public void assertAllClose(double value, NDArray array2, double eps = 1e-5)
Assert.IsTrue(np.allclose(array1, array2, rtol: eps));
}

private class CollectionComparer : System.Collections.IComparer
{
private readonly double _epsilon;

public CollectionComparer(double eps = 1e-06) {
_epsilon = eps;
}
public int Compare(object x, object y)
{
var a = (double)x;
var b = (double)y;

double delta = Math.Abs(a - b);
if (delta < _epsilon)
{
return 0;
}
return a.CompareTo(b);
}
}

public void assertAllCloseAccordingToType<T>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Wanglongzhi2001 Does it make sense not to duplicate all assertion code but combine all in one assembly?

T[] expected,
T[] given,
double eps = 1e-6,
float float_eps = 1e-6f)
{
// TODO: check if any of arguments is not double and change toletance
CollectionAssert.AreEqual(expected, given, new CollectionComparer(eps));
}

public void assertProtoEquals(object toProto, object o)
{
throw new NotImplementedException();
Expand All @@ -153,6 +184,20 @@ public void assertProtoEquals(object toProto, object o)

#region tensor evaluation and test session

private Session _cached_session = null;
private Graph _cached_graph = null;
private object _cached_config = null;
private bool _cached_force_gpu = false;

private void _ClearCachedSession()
{
if (self._cached_session != null)
{
self._cached_session.Dispose();
self._cached_session = null;
}
}

//protected object _eval_helper(Tensor[] tensors)
//{
// if (tensors == null)
Expand Down Expand Up @@ -218,9 +263,56 @@ public T evaluate<T>(Tensor tensor)
}


public Session cached_session()
///Returns a TensorFlow Session for use in executing tests.
public Session cached_session(
Graph graph = null, object config = null, bool use_gpu = false, bool force_gpu = false)
{
throw new NotImplementedException();
// This method behaves differently than self.session(): for performance reasons
// `cached_session` will by default reuse the same session within the same
// test.The session returned by this function will only be closed at the end
// of the test(in the TearDown function).

// Use the `use_gpu` and `force_gpu` options to control where ops are run.If
// `force_gpu` is True, all ops are pinned to `/ device:GPU:0`. Otherwise, if
// `use_gpu` is True, TensorFlow tries to run as many ops on the GPU as
// possible.If both `force_gpu and `use_gpu` are False, all ops are pinned to
// the CPU.

// Example:
// python
// class MyOperatorTest(test_util.TensorFlowTestCase) :
// def testMyOperator(self):
// with self.cached_session() as sess:
// valid_input = [1.0, 2.0, 3.0, 4.0, 5.0]
// result = MyOperator(valid_input).eval()
// self.assertEqual(result, [1.0, 2.0, 3.0, 5.0, 8.0]
// invalid_input = [-1.0, 2.0, 7.0]
// with self.assertRaisesOpError("negative input not supported"):
// MyOperator(invalid_input).eval()


// Args:
// graph: Optional graph to use during the returned session.
// config: An optional config_pb2.ConfigProto to use to configure the
// session.
// use_gpu: If True, attempt to run as many ops as possible on GPU.
// force_gpu: If True, pin all ops to `/device:GPU:0`.

// Yields:
// A Session object that should be used as a context manager to surround
// the graph building and execution code in a test case.


// TODO:
// if context.executing_eagerly():
// return self._eval_helper(tensors)
// else:
{
var sess = self._get_cached_session(
graph, config, force_gpu, crash_if_inconsistent_args: true);
using var cached = self._constrain_devices_and_set_default(sess, use_gpu, force_gpu);
return cached;
}
}

//Returns a TensorFlow Session for use in executing tests.
Expand Down Expand Up @@ -268,6 +360,40 @@ public Session session(Graph graph = null, object config = null, bool use_gpu =
return s.as_default();
}

private Session _constrain_devices_and_set_default(Session sess, bool use_gpu, bool force_gpu)
{
// Set the session and its graph to global default and constrain devices."""
if (tf.executing_eagerly())
return null;
else
{
sess.graph.as_default();
sess.as_default();
{
if (force_gpu)
{
// TODO:

// Use the name of an actual device if one is detected, or
// '/device:GPU:0' otherwise
/* var gpu_name = gpu_device_name();
if (!gpu_name)
gpu_name = "/device:GPU:0"
using (sess.graph.device(gpu_name)) {
yield return sess;
}*/
return sess;
}
else if (use_gpu)
return sess;
else
using (sess.graph.device("/device:CPU:0"))
return sess;
}

}
}

// See session() for details.
private Session _create_session(Graph graph, object cfg, bool forceGpu)
{
Expand Down Expand Up @@ -312,6 +438,54 @@ private Session _create_session(Graph graph, object cfg, bool forceGpu)
return new Session(graph);//, config = prepare_config(config))
}

private Session _get_cached_session(
Graph graph = null,
object config = null,
bool force_gpu = false,
bool crash_if_inconsistent_args = true)
{
// See cached_session() for documentation.
if (self._cached_session == null)
{
var sess = self._create_session(graph, config, force_gpu);
self._cached_session = sess;
self._cached_graph = graph;
self._cached_config = config;
self._cached_force_gpu = force_gpu;
return sess;
}
else
{

if (crash_if_inconsistent_args && !self._cached_graph.Equals(graph))
throw new ValueError(@"The graph used to get the cached session is
different than the one that was used to create the
session. Maybe create a new session with
self.session()");
if (crash_if_inconsistent_args && !self._cached_config.Equals(config))
{
throw new ValueError(@"The config used to get the cached session is
different than the one that was used to create the
session. Maybe create a new session with
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Wanglongzhi2001 this code is also copy pasted from my previous PR because that's how test architecture implemented right now. Should I move it to one common assembly?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think moving to a common namespace is a good idea if it could be re-used by other modules. Please open an another PR if you would like to do that.

self.session()");
}
if (crash_if_inconsistent_args && !self._cached_force_gpu.Equals(force_gpu))
{
throw new ValueError(@"The force_gpu value used to get the cached session is
different than the one that was used to create the
session. Maybe create a new session with
self.session()");
}
return _cached_session;
}
}

[TestCleanup]
public void Cleanup()
{
_ClearCachedSession();
}

#endregion

public void AssetSequenceEqual<T>(T[] a, T[] b)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq;
using System.Runtime.Intrinsics.X86;
using System.Security.AccessControl;
using Tensorflow.NumPy;
using TensorFlowNET.UnitTest;
using static Tensorflow.Binding;

namespace Tensorflow.Keras.UnitTest.Optimizers
{
[TestClass]
public class GradientDescentOptimizerTest : PythonTest
{
private void TestBasicGeneric<T>() where T : struct
{
var dtype = Type.GetTypeCode(typeof(T)) switch
{
TypeCode.Single => np.float32,
TypeCode.Double => np.float64,
_ => throw new NotImplementedException(),
};

// train.GradientDescentOptimizer is V1 only API.
tf.Graph().as_default();
using (self.cached_session())
{
var var0 = tf.Variable(new[] { 1.0, 2.0 }, dtype: dtype);
var var1 = tf.Variable(new[] { 3.0, 4.0 }, dtype: dtype);
var grads0 = tf.constant(new[] { 0.1, 0.1 }, dtype: dtype);
var grads1 = tf.constant(new[] { 0.01, 0.01 }, dtype: dtype);
var optimizer = tf.train.GradientDescentOptimizer(3.0f);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Wanglongzhi2001 TensorFlow can accept lambda in this constructor and there's a test testing it.
Is it intentionally that TensorFlow.NET doesn't support that interface?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, it's more like a compromise to the early fast-development. Therefore only a smallest and most-used implementation was provided. I think it's feasible to support it.

var grads_and_vars = new[] {
Tuple.Create(grads0, var0 as IVariableV1),
Tuple.Create(grads1, var1 as IVariableV1)
};
var sgd_op = optimizer.apply_gradients(grads_and_vars);

var global_variables = variables.global_variables_initializer();
self.evaluate<T>(global_variables);
// Fetch params to validate initial values
// TODO: use self.evaluate<T[]> instead of self.evaluate<double[]>
self.assertAllCloseAccordingToType(new double[] { 1.0, 2.0 }, self.evaluate<double[]>(var0));
self.assertAllCloseAccordingToType(new double[] { 3.0, 4.0 }, self.evaluate<double[]>(var1));
// Run 1 step of sgd
sgd_op.run();
// Validate updated params
self.assertAllCloseAccordingToType(
new double[] { 1.0 - 3.0 * 0.1, 2.0 - 3.0 * 0.1 },
self.evaluate<double[]>(var0));
self.assertAllCloseAccordingToType(
new double[] { 3.0 - 3.0 * 0.01, 4.0 - 3.0 * 0.01 },
self.evaluate<double[]>(var1));
// TODO: self.assertEqual(0, len(optimizer.variables()));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Wanglongzhi2001 Do I understand correctly that this one is not applicable for TensorFlow.NET?

}
}

[TestMethod]
public void TestBasic()
{
//TODO: add np.half
TestBasicGeneric<float>();
TestBasicGeneric<double>();
}


}
}
Loading