diff --git a/graph/InceptionV3.meta b/graph/InceptionV3.meta index 69dc6ff4..c23a3a36 100644 Binary files a/graph/InceptionV3.meta and b/graph/InceptionV3.meta differ diff --git a/src/TensorFlowNET.Core/APIs/tf.summary.cs b/src/TensorFlowNET.Core/APIs/tf.summary.cs new file mode 100644 index 00000000..3932c969 --- /dev/null +++ b/src/TensorFlowNET.Core/APIs/tf.summary.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Tensorflow.IO; +using Tensorflow.Summaries; + +namespace Tensorflow +{ + public static partial class tf + { + public static Summary summary = new Summary(); + public static Tensor scalar(string name, Tensor tensor) + => summary.scalar(name, tensor); + } +} diff --git a/src/TensorFlowNET.Core/Operations/gen_logging_ops.cs b/src/TensorFlowNET.Core/Operations/gen_logging_ops.cs index 6f11dd0e..0af59d63 100644 --- a/src/TensorFlowNET.Core/Operations/gen_logging_ops.cs +++ b/src/TensorFlowNET.Core/Operations/gen_logging_ops.cs @@ -17,5 +17,65 @@ namespace Tensorflow return _op; } + + /// + /// Outputs a Summary protocol buffer with scalar values. + /// + /// + /// Tags for the summary. + /// + /// + /// Same shape as tags. Values for the summary. + /// + /// + /// If specified, the created operation in the graph will be this one, otherwise it will be named 'ScalarSummary'. + /// + /// + /// Scalar. Serialized Summary protocol buffer. + /// The Operation can be fetched from the resulting Tensor, by fetching the Operation property from the result. + /// + /// + /// The input tags and values must have the same shape. The generated summary + /// has a summary value for each tag-value pair in tags and values. + /// + public static Tensor scalar_summary(string tags, Tensor values, string name = "ScalarSummary") + { + var dict = new Dictionary(); + dict["tags"] = tags; + dict["values"] = values; + var op = _op_def_lib._apply_op_helper("ScalarSummary", name: name, keywords: dict); + return op.output; + } + + /// + /// Merges summaries. + /// + /// + /// Can be of any shape. Each must contain serialized Summary protocol + /// buffers. + /// + /// + /// If specified, the created operation in the graph will be this one, otherwise it will be named 'MergeSummary'. + /// + /// + /// Scalar. Serialized Summary protocol buffer. + /// The Operation can be fetched from the resulting Tensor, by fetching the Operation property from the result. + /// + /// + /// This op creates a + /// [Summary](https://www.tensorflow.org/code/tensorflow/core/framework/summary.proto) + /// protocol buffer that contains the union of all the values in the input + /// summaries. + /// + /// When the Op is run, it reports an InvalidArgument error if multiple values + /// in the summaries to merge use the same tag. + /// + public static Tensor merge_summary(Tensor[] inputs, string name = "MergeSummary") + { + var dict = new Dictionary(); + dict["inputs"] = inputs; + var op = _op_def_lib._apply_op_helper("MergeSummary", name: name, keywords: dict); + return op.output; + } } } diff --git a/src/TensorFlowNET.Core/Summaries/Summary.cs b/src/TensorFlowNET.Core/Summaries/Summary.cs new file mode 100644 index 00000000..57559a55 --- /dev/null +++ b/src/TensorFlowNET.Core/Summaries/Summary.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using static Tensorflow.Python; + +namespace Tensorflow.Summaries +{ + public class Summary + { + public Tensor merge_all(string key = ops.GraphKeys.SUMMARIES, string scope= null, string name= null) + { + var summary_ops = ops.get_collection(key, scope: scope); + if (summary_ops == null) + return null; + else + return merge((summary_ops as List).Select(x => x as Tensor).ToArray(), name: name); + } + + /// + /// Merges summaries. + /// + /// + /// + /// + /// + public Tensor merge(Tensor[] inputs, string[] collections = null, string name = null) + { + return with(ops.name_scope(name, "Merge", inputs), delegate + { + var val = gen_logging_ops.merge_summary(inputs: inputs, name: name); + collect(val, collections?.ToList(), new List()); + return val; + }); + } + + public Tensor scalar(string name, Tensor tensor, string[] collections = null, string family = null) + { + var (tag, scope) = summary_scope(name, family: family, values: new Tensor[] { tensor }); + var val = gen_logging_ops.scalar_summary(tags: tag, values: tensor, name: scope); + collect(val, collections?.ToList(), new List { ops.GraphKeys.SUMMARIES }); + return val; + } + + /// + /// Adds keys to a collection. + /// + /// + /// A collection of keys to add. + /// Used if collections is None. + public void collect(ITensorOrOperation val, List collections, List default_collections) + { + if (collections == null) + collections = default_collections; + foreach (var key in collections) + ops.add_to_collection(key, val); + } + + public (string, string) summary_scope(string name, string family = null, string default_name = null, Tensor[] values = null) + { + string scope_base_name = string.IsNullOrEmpty(family) ? name : $"{family}/{name}"; + return with(ops.name_scope(scope_base_name, default_name: default_name, values), scope => + { + var tag = scope._name_scope; + if (string.IsNullOrEmpty(family)) + tag = tag.Remove(tag.Length - 1); + else + tag = $"{family}/{tag.Remove(tag.Length - 1)}"; + + return (tag, scope._name_scope); + }); + } + } +} diff --git a/src/TensorFlowNET.Core/ops.GraphKeys.cs b/src/TensorFlowNET.Core/ops.GraphKeys.cs index 2d319180..00763dc2 100644 --- a/src/TensorFlowNET.Core/ops.GraphKeys.cs +++ b/src/TensorFlowNET.Core/ops.GraphKeys.cs @@ -47,6 +47,9 @@ namespace Tensorflow /// public static string UPDATE_OPS = "update_ops"; + // Key to collect summaries. + public const string SUMMARIES = "summaries"; + // Used to store v2 summary names. public static string _SUMMARY_COLLECTION = "_SUMMARY_V2"; diff --git a/test/TensorFlowNET.Examples/ImageProcess/RetrainImageClassifier.cs b/test/TensorFlowNET.Examples/ImageProcess/RetrainImageClassifier.cs index f9a5bef9..9c7530a0 100644 --- a/test/TensorFlowNET.Examples/ImageProcess/RetrainImageClassifier.cs +++ b/test/TensorFlowNET.Examples/ImageProcess/RetrainImageClassifier.cs @@ -45,6 +45,8 @@ namespace TensorFlowNET.Examples.ImageProcess tf.train.import_meta_graph("graph/InceptionV3.meta"); Tensor bottleneck_tensor = graph.OperationByName("module_apply_default/hub_output/feature_vector/SpatialSqueeze"); Tensor resized_image_tensor = graph.OperationByName("Placeholder"); + Tensor final_tensor = graph.OperationByName("final_result"); + Tensor ground_truth_input = graph.OperationByName("input/GroundTruthInput"); var sw = new Stopwatch(); @@ -63,11 +65,45 @@ namespace TensorFlowNET.Examples.ImageProcess bottleneck_dir, jpeg_data_tensor, decoded_image_tensor, resized_image_tensor, bottleneck_tensor, tfhub_module); + + // Create the operations we need to evaluate the accuracy of our new layer. + var (evaluation_step, _) = add_evaluation_step(final_tensor, ground_truth_input); + + // Merge all the summaries and write them out to the summaries_dir + var merged = tf.summary.merge_all(); }); return false; } + /// + /// Inserts the operations we need to evaluate the accuracy of our results. + /// + /// + /// + /// + private (Tensor, Tensor) add_evaluation_step(Tensor result_tensor, Tensor ground_truth_tensor) + { + Tensor evaluation_step = null, correct_prediction = null, prediction = null; + + with(tf.name_scope("accuracy"), scope => + { + with(tf.name_scope("correct_prediction"), delegate + { + prediction = tf.argmax(result_tensor, 1); + correct_prediction = tf.equal(prediction, ground_truth_tensor); + }); + + with(tf.name_scope("accuracy"), delegate + { + evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)); + }); + }); + + tf.summary.scalar("accuracy", evaluation_step); + return (evaluation_step, prediction); + } + /// /// Ensures all the training, testing, and validation bottlenecks are cached. /// @@ -95,12 +131,15 @@ namespace TensorFlowNET.Examples.ImageProcess get_or_create_bottleneck(sess, image_lists, label_name, index, image_dir, category, bottleneck_dir, jpeg_data_tensor, decoded_image_tensor, resized_input_tensor, bottleneck_tensor, module_name); + how_many_bottlenecks++; + if (how_many_bottlenecks % 100 == 0) + print($"{how_many_bottlenecks} bottleneck files created."); } } } } - private void get_or_create_bottleneck(Session sess, Dictionary> image_lists, + private float[] get_or_create_bottleneck(Session sess, Dictionary> image_lists, string label_name, int index, string image_dir, string category, string bottleneck_dir, Tensor jpeg_data_tensor, Tensor decoded_image_tensor, Tensor resized_input_tensor, Tensor bottleneck_tensor, string module_name) @@ -116,6 +155,9 @@ namespace TensorFlowNET.Examples.ImageProcess image_dir, category, sess, jpeg_data_tensor, decoded_image_tensor, resized_input_tensor, bottleneck_tensor); + var bottleneck_string = File.ReadAllText(bottleneck_path); + var bottleneck_values = Array.ConvertAll(bottleneck_string.Split(','), x => float.Parse(x)); + return bottleneck_values; } private void create_bottleneck_file(string bottleneck_path, Dictionary> image_lists, @@ -132,13 +174,16 @@ namespace TensorFlowNET.Examples.ImageProcess var bottleneck_values = run_bottleneck_on_image( sess, image_data, jpeg_data_tensor, decoded_image_tensor, resized_input_tensor, bottleneck_tensor); + var values = bottleneck_values.Data(); + var bottleneck_string = string.Join(",", values); + File.WriteAllText(bottleneck_path, bottleneck_string); } /// /// Runs inference on an image to extract the 'bottleneck' summary layer. /// /// Current active TensorFlow Session. - /// Path of raw JPEG data. + /// Data of raw JPEG data. /// Input data layer in the graph. /// Output of initial image resizing and preprocessing. /// The input node of the recognition graph.