diff --git a/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Operand.java b/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Operand.java index 80f62eb5acc..9e903cce7e0 100644 --- a/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Operand.java +++ b/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Operand.java @@ -15,6 +15,7 @@ package org.tensorflow; +import org.tensorflow.Tensor.ToStringOptions; import org.tensorflow.ndarray.Shape; import org.tensorflow.ndarray.Shaped; import org.tensorflow.op.Op; @@ -65,6 +66,17 @@ default T asTensor() { return asOutput().asTensor(); } + /** + * Returns the String representation of the tensor elements at this operand. + * + * @param options overrides the default configuration + * @return the String representation of the tensor elements + * @throws IllegalStateException if this is an operand of a graph + */ + default String dataToString(ToStringOptions... options) { + return asTensor().dataToString(options); + } + /** * Returns the tensor type of this operand */ diff --git a/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensor.java b/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensor.java index fc1275229bf..87f0f1e7118 100644 --- a/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensor.java +++ b/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensor.java @@ -26,9 +26,9 @@ * A statically typed multi-dimensional array. * *

There are two categories of tensors in TensorFlow Java: {@link TType typed tensors} and - * {@link RawTensor raw tensors}. The former maps the tensor native memory to an - * n-dimensional typed data space, allowing direct I/O operations from the JVM, while the latter - * is only a reference to a native tensor allowing basic operations and flat data access.

+ * {@link RawTensor raw tensors}. The former maps the tensor native memory to an n-dimensional typed + * data space, allowing direct I/O operations from the JVM, while the latter is only a reference to + * a native tensor allowing basic operations and flat data access.

* *

WARNING: Resources consumed by the Tensor object must be explicitly freed by * invoking the {@link #close()} method when the object is no longer needed. For example, using a @@ -49,15 +49,15 @@ public interface Tensor extends Shaped, AutoCloseable { *

The amount of memory to allocate is derived from the datatype and the shape of the tensor, * and is left uninitialized. * - * @param the tensor type - * @param type the tensor type class + * @param the tensor type + * @param type the tensor type class * @param shape shape of the tensor * @return an allocated but uninitialized tensor * @throws IllegalArgumentException if elements of the given {@code type} are of variable length * (e.g. strings) - * @throws IllegalArgumentException if {@code shape} is totally or partially - * {@link Shape#hasUnknownDimension() unknown} - * @throws IllegalStateException if tensor failed to be allocated + * @throws IllegalArgumentException if {@code shape} is totally or partially {@link + * Shape#hasUnknownDimension() unknown} + * @throws IllegalStateException if tensor failed to be allocated */ static T of(Class type, Shape shape) { return of(type, shape, -1); @@ -67,27 +67,27 @@ static T of(Class type, Shape shape) { * Allocates a tensor of a given datatype, shape and size. * *

This method is identical to {@link #of(Class, Shape)}, except that the final size of the - * tensor can be explicitly set instead of computing it from the datatype and shape, which could be - * larger than the actual space required to store the data but not smaller. + * tensor can be explicitly set instead of computing it from the datatype and shape, which could + * be larger than the actual space required to store the data but not smaller. * - * @param the tensor type - * @param type the tensor type class + * @param the tensor type + * @param type the tensor type class * @param shape shape of the tensor - * @param size size in bytes of the tensor or -1 to compute the size from the shape + * @param size size in bytes of the tensor or -1 to compute the size from the shape * @return an allocated but uninitialized tensor - * @see #of(Class, Shape) * @throws IllegalArgumentException if {@code size} is smaller than the minimum space required to * store the tensor data - * @throws IllegalArgumentException if {@code size} is set to -1 but elements of the given - * {@code type} are of variable length (e.g. strings) - * @throws IllegalArgumentException if {@code shape} is totally or partially - * {@link Shape#hasUnknownDimension() unknown} - * @throws IllegalStateException if tensor failed to be allocated + * @throws IllegalArgumentException if {@code size} is set to -1 but elements of the given {@code + * type} are of variable length (e.g. strings) + * @throws IllegalArgumentException if {@code shape} is totally or partially {@link + * Shape#hasUnknownDimension() unknown} + * @throws IllegalStateException if tensor failed to be allocated + * @see #of(Class, Shape) */ static T of(Class type, Shape shape, long size) { RawTensor tensor = RawTensor.allocate(type, shape, size); try { - return (T)tensor.asTypedTensor(); + return (T) tensor.asTypedTensor(); } catch (Exception e) { tensor.close(); throw e; @@ -111,16 +111,17 @@ static T of(Class type, Shape shape, long size) { *

If {@code dataInitializer} fails and throws an exception, the allocated tensor will be * automatically released before rethrowing the same exception. * - * @param the tensor type - * @param type the tensor type class - * @param shape shape of the tensor - * @param dataInitializer method receiving accessor to the allocated tensor data for initialization + * @param the tensor type + * @param type the tensor type class + * @param shape shape of the tensor + * @param dataInitializer method receiving accessor to the allocated tensor data for + * initialization * @return an allocated and initialized tensor * @throws IllegalArgumentException if elements of the given {@code type} are of variable length * (e.g. strings) - * @throws IllegalArgumentException if {@code shape} is totally or partially - * {@link Shape#hasUnknownDimension() unknown} - * @throws IllegalStateException if tensor failed to be allocated + * @throws IllegalArgumentException if {@code shape} is totally or partially {@link + * Shape#hasUnknownDimension() unknown} + * @throws IllegalStateException if tensor failed to be allocated */ static T of(Class type, Shape shape, Consumer dataInitializer) { return of(type, shape, -1, dataInitializer); @@ -130,27 +131,30 @@ static T of(Class type, Shape shape, Consumer dataInitia * Allocates a tensor of a given datatype, shape and size. * *

This method is identical to {@link #of(Class, Shape, Consumer)}, except that the final - * size for the tensor can be explicitly set instead of being computed from the datatype and shape. + * size for the tensor can be explicitly set instead of being computed from the datatype and + * shape. * - *

This could be useful for tensor types that stores data but also metadata in the tensor memory, - * such as the lookup table in a tensor of strings. + *

This could be useful for tensor types that stores data but also metadata in the tensor + * memory, such as the lookup table in a tensor of strings. * - * @param the tensor type - * @param type the tensor type class - * @param shape shape of the tensor - * @param size size in bytes of the tensor or -1 to compute the size from the shape - * @param dataInitializer method receiving accessor to the allocated tensor data for initialization + * @param the tensor type + * @param type the tensor type class + * @param shape shape of the tensor + * @param size size in bytes of the tensor or -1 to compute the size from the shape + * @param dataInitializer method receiving accessor to the allocated tensor data for + * initialization * @return an allocated and initialized tensor - * @see #of(Class, Shape, long, Consumer) * @throws IllegalArgumentException if {@code size} is smaller than the minimum space required to * store the tensor data - * @throws IllegalArgumentException if {@code size} is set to -1 but elements of the given - * {@code type} are of variable length (e.g. strings) - * @throws IllegalArgumentException if {@code shape} is totally or partially - * {@link Shape#hasUnknownDimension() unknown} - * @throws IllegalStateException if tensor failed to be allocated + * @throws IllegalArgumentException if {@code size} is set to -1 but elements of the given {@code + * type} are of variable length (e.g. strings) + * @throws IllegalArgumentException if {@code shape} is totally or partially {@link + * Shape#hasUnknownDimension() unknown} + * @throws IllegalStateException if tensor failed to be allocated + * @see #of(Class, Shape, long, Consumer) */ - static T of(Class type, Shape shape, long size, Consumer dataInitializer) { + static T of(Class type, Shape shape, long size, + Consumer dataInitializer) { T tensor = of(type, shape, size); try { dataInitializer.accept(tensor); @@ -167,18 +171,19 @@ static T of(Class type, Shape shape, long size, Consumer *

Data must have been encoded into {@code data} as per the specification of the TensorFlow C API. * - * @param the tensor type - * @param type the tensor type class - * @param shape the tensor shape. + * @param the tensor type + * @param type the tensor type class + * @param shape the tensor shape. * @param rawData a buffer containing the tensor raw data. * @throws IllegalArgumentException if {@code rawData} is not large enough to contain the tensor * data - * @throws IllegalArgumentException if {@code shape} is totally or partially - * {@link Shape#hasUnknownDimension() unknown} - * @throws IllegalStateException if tensor failed to be allocated with the given parameters + * @throws IllegalArgumentException if {@code shape} is totally or partially {@link + * Shape#hasUnknownDimension() unknown} + * @throws IllegalStateException if tensor failed to be allocated with the given parameters */ static T of(Class type, Shape shape, ByteDataBuffer rawData) { - return of(type, shape, rawData.size(), t -> rawData.copyTo(t.asRawTensor().data(), rawData.size())); + return of(type, shape, rawData.size(), + t -> rawData.copyTo(t.asRawTensor().data(), rawData.size())); } /** @@ -191,6 +196,33 @@ static T of(Class type, Shape shape, ByteDataBuffer rawData */ long numBytes(); + /** + * Returns the String representation of elements stored in the tensor. + * + * @param options overrides the default configuration + * @return the String representation of the tensor elements + * @throws IllegalStateException if this is an operand of a graph + */ + default String dataToString(ToStringOptions... options) { + Integer maxWidth = null; + if (options != null) { + for (ToStringOptions opts : options) { + if (opts.maxWidth != null) { + maxWidth = opts.maxWidth; + } + } + } + return Tensors.toString(this, maxWidth); + } + + /** + * @param maxWidth the maximum width of the output in characters ({@code null} if unlimited). This + * limit may surpassed if the first or last element are too long. + */ + static ToStringOptions maxWidth(Integer maxWidth) { + return new ToStringOptions().maxWidth(maxWidth); + } + /** * Returns the shape of the tensor. */ @@ -212,4 +244,23 @@ static T of(Class type, Shape shape, ByteDataBuffer rawData */ @Override void close(); + + class ToStringOptions { + + /** + * Sets the maximum width of the output in characters. + * + * @param maxWidth the maximum width of the output in characters ({@code null} if unlimited). + * This limit may surpassed if the first or last element are too long. + */ + public ToStringOptions maxWidth(Integer maxWidth) { + this.maxWidth = maxWidth; + return this; + } + + private Integer maxWidth; + + private ToStringOptions() { + } + } } diff --git a/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensors.java b/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensors.java new file mode 100644 index 00000000000..656866d3eba --- /dev/null +++ b/tensorflow-core/tensorflow-core-api/src/main/java/org/tensorflow/Tensors.java @@ -0,0 +1,186 @@ +package org.tensorflow; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringJoiner; +import org.tensorflow.ndarray.NdArray; +import org.tensorflow.ndarray.Shape; +import org.tensorflow.proto.framework.DataType; + +/** + * Tensor helper methods. + */ +final class Tensors { + + /** + * Prevent construction. + */ + private Tensors() { + } + + /** + * Equivalent to {@link #toString(Tensor, Integer) toString(tensor, null)}. + * + * @param tensor a tensor + * @return the String representation of the tensor + */ + public static String toString(Tensor tensor) { + return toString(tensor, null); + } + + /** + * Returns a String representation of a tensor's data. If the output is wider than {@code + * maxWidth} characters, it is truncated and "{@code ...}" is inserted in place of the removed + * data. + * + * @param tensor a tensor + * @param maxWidth the maximum width of the output in characters ({@code null} if unlimited). This + * limit may surpassed if the first or last element are too long. + * @return the String representation of the tensor + */ + public static String toString(Tensor tensor, Integer maxWidth) { + if (tensor instanceof RawTensor) { + tensor = ((RawTensor) tensor).asTypedTensor(); + } + if (!(tensor instanceof NdArray)) { + throw new AssertionError("Expected tensor to extend NdArray.\n" + + "actual : " + tensor + "\n" + + "dataType: " + tensor.dataType() + "\n" + + "class : " + tensor.getClass()); + } + NdArray ndArray = (NdArray) tensor; + Iterator> iterator = ndArray.scalars().iterator(); + Shape shape = tensor.shape(); + if (shape.numDimensions() == 0) { + if (!iterator.hasNext()) { + return ""; + } + return String.valueOf(iterator.next().getObject()); + } + return toString(iterator, tensor.dataType(), shape, 0, maxWidth); + } + + /** + * Convert an element of a tensor to string, in a way that may depend on the data type. + * + * @param dtype the tensor's data type + * @param data the element + * @return the element's string representation + */ + private static String elementToString(DataType dtype, Object data) { + if (dtype == DataType.DT_STRING) { + return '"' + data.toString() + '"'; + } else { + return data.toString(); + } + } + + /** + * @param iterator an iterator over the scalars + * @param shape the shape of the tensor + * @param maxWidth the maximum width of the output in characters ({@code null} if unlimited). This limit may surpassed + * if the first or last element are too long. + * @param dimension the current dimension being processed + * @return the String representation of the tensor data at {@code dimension} + */ + private static String toString(Iterator> iterator, DataType dtype, Shape shape, + int dimension, Integer maxWidth) { + if (dimension < shape.numDimensions() - 1) { + StringJoiner joiner = new StringJoiner("\n", indent(dimension) + "[\n", + "\n" + indent(dimension) + "]"); + for (long i = 0, size = shape.size(dimension); i < size; ++i) { + String element = toString(iterator, dtype, shape, dimension + 1, maxWidth); + joiner.add(element); + } + return joiner.toString(); + } + if (maxWidth == null) { + StringJoiner joiner = new StringJoiner(", ", indent(dimension) + "[", "]"); + for (long i = 0, size = shape.size(dimension); i < size; ++i) { + Object element = iterator.next().getObject(); + joiner.add(elementToString(dtype, element)); + } + return joiner.toString(); + } + List lengths = new ArrayList<>(); + StringJoiner joiner = new StringJoiner(", ", indent(dimension) + "[", "]"); + int lengthBefore = "]".length(); + for (long i = 0, size = shape.size(dimension); i < size; ++i) { + Object element = iterator.next().getObject(); + joiner.add(elementToString(dtype, element)); + int addedLength = joiner.length() - lengthBefore; + lengths.add(addedLength); + lengthBefore += addedLength; + } + return truncateWidth(joiner.toString(), maxWidth, lengths); + } + + /** + * Truncates the width of a String if it's too long, inserting "{@code ...}" in place of the + * removed data. + * + * @param input the input to truncate + * @param maxWidth the maximum width of the output in characters + * @param lengths the lengths of elements inside input + * @return the (potentially) truncated output + */ + private static String truncateWidth(String input, int maxWidth, List lengths) { + if (input.length() <= maxWidth) { + return input; + } + StringBuilder output = new StringBuilder(input); + int midPoint = (maxWidth / 2) - 1; + int width = 0; + int indexOfElementToRemove = lengths.size() - 1; + int widthBeforeElementToRemove = 0; + for (int i = 0, size = lengths.size(); i < size; ++i) { + width += lengths.get(i); + if (width > midPoint) { + indexOfElementToRemove = i; + break; + } + widthBeforeElementToRemove = width; + } + if (indexOfElementToRemove == 0) { + // Cannot remove first element + return input; + } + output.insert(widthBeforeElementToRemove, ", ..."); + widthBeforeElementToRemove += ", ...".length(); + width = output.length(); + while (width > maxWidth) { + if (indexOfElementToRemove == 0) { + // Cannot remove first element + break; + } else if (indexOfElementToRemove == lengths.size() - 1) { + // Cannot remove last element + --indexOfElementToRemove; + continue; + } + Integer length = lengths.remove(indexOfElementToRemove); + output.delete(widthBeforeElementToRemove, widthBeforeElementToRemove + length); + width = output.length(); + } + if (output.length() < input.length()) { + return output.toString(); + } + // Do not insert ellipses if it increases the length + return input; + } + + /** + * @param level the level of indent + * @return the indentation string + */ + public static String indent(int level) { + if (level <= 0) { + return ""; + } + StringBuilder result = new StringBuilder(level * 2); + for (int i = 0; i < level; ++i) { + result.append(" "); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/tensorflow-core/tensorflow-core-api/src/test/java/org/tensorflow/TensorTest.java b/tensorflow-core/tensorflow-core-api/src/test/java/org/tensorflow/TensorTest.java index 9415a986222..be816e6f093 100644 --- a/tensorflow-core/tensorflow-core-api/src/test/java/org/tensorflow/TensorTest.java +++ b/tensorflow-core/tensorflow-core-api/src/test/java/org/tensorflow/TensorTest.java @@ -340,7 +340,7 @@ public void nDimensional() { } LongNdArray threeD = StdArrays.ndCopyOf(new long[][][]{ - {{1}, {3}, {5}, {7}, {9}}, {{2}, {4}, {6}, {8}, {0}}, + {{1}, {3}, {5}, {7}, {9}}, {{2}, {4}, {6}, {8}, {0}}, }); try (TInt64 t = TInt64.tensorOf(threeD)) { assertEquals(TInt64.class, t.type()); @@ -353,9 +353,9 @@ public void nDimensional() { } BooleanNdArray fourD = StdArrays.ndCopyOf(new boolean[][][][]{ - {{{false, false, false, true}, {false, false, true, false}}}, - {{{false, false, true, true}, {false, true, false, false}}}, - {{{false, true, false, true}, {false, true, true, false}}}, + {{{false, false, false, true}, {false, false, true, false}}}, + {{{false, false, true, true}, {false, true, false, false}}}, + {{{false, true, false, true}, {false, true, true, false}}}, }); try (TBool t = TBool.tensorOf(fourD)) { assertEquals(TBool.class, t.type()); @@ -514,7 +514,8 @@ public void fromHandle() { // close() on both Tensors. final FloatNdArray matrix = StdArrays.ndCopyOf(new float[][]{{1, 2, 3}, {4, 5, 6}}); try (TFloat32 src = TFloat32.tensorOf(matrix)) { - TFloat32 cpy = (TFloat32)RawTensor.fromHandle(src.asRawTensor().nativeHandle()).asTypedTensor(); + TFloat32 cpy = (TFloat32) RawTensor.fromHandle(src.asRawTensor().nativeHandle()) + .asTypedTensor(); assertEquals(src.type(), cpy.type()); assertEquals(src.dataType(), cpy.dataType()); assertEquals(src.shape().numDimensions(), cpy.shape().numDimensions()); @@ -541,6 +542,61 @@ public void gracefullyFailCreationFromNullArrayForStringTensor() { } } + @Test + public void dataToString() { + try (TInt32 t = TInt32.vectorOf(3, 0, 1)) { + String actual = t.dataToString(); + assertEquals("[3, 0, 1]", actual); + } + try (TInt32 t = TInt32.vectorOf(3, 0, 1)) { + String actual = t.dataToString(Tensor.maxWidth(5)); + // Cannot remove first or last element + assertEquals("[3, 0, 1]", actual); + } + try (TInt32 t = TInt32.vectorOf(3, 0, 1)) { + String actual = t.dataToString(Tensor.maxWidth(6)); + // Do not insert ellipses if it increases the length + assertEquals("[3, 0, 1]", actual); + } + try (TInt32 t = TInt32.vectorOf(3, 0, 1, 2)) { + String actual = t.dataToString(Tensor.maxWidth(11)); + // Limit may be surpassed if first or last element are too long + assertEquals("[3, ..., 2]", actual); + } + try (TInt32 t = TInt32.vectorOf(3, 0, 1, 2)) { + String actual = t.dataToString(Tensor.maxWidth(12)); + assertEquals("[3, 0, 1, 2]", actual); + } + try (TInt32 t = TInt32.tensorOf(StdArrays.ndCopyOf(new int[][]{{1, 2, 3}, {3, 2, 1}}))) { + String actual = t.dataToString(Tensor.maxWidth(12)); + assertEquals("[\n" + + " [1, 2, 3]\n" + + " [3, 2, 1]\n" + + "]", actual); + } + try (RawTensor t = TInt32.vectorOf(3, 0, 1, 2).asRawTensor()) { + String actual = t.dataToString(Tensor.maxWidth(12)); + assertEquals("[3, 0, 1, 2]", actual); + } + // different data types + try (RawTensor t = TFloat32.vectorOf(3.0101f, 0, 1.5f, 2).asRawTensor()) { + String actual = t.dataToString(); + assertEquals("[3.0101, 0.0, 1.5, 2.0]", actual); + } + try (RawTensor t = TFloat64.vectorOf(3.0101, 0, 1.5, 2).asRawTensor()) { + String actual = t.dataToString(); + assertEquals("[3.0101, 0.0, 1.5, 2.0]", actual); + } + try (RawTensor t = TBool.vectorOf(true, true, false, true).asRawTensor()) { + String actual = t.dataToString(); + assertEquals("[true, true, false, true]", actual); + } + try (RawTensor t = TString.vectorOf("a", "b", "c").asRawTensor()) { + String actual = t.dataToString(); + assertEquals("[\"a\", \"b\", \"c\"]", actual); + } + } + // Workaround for cross compiliation // (e.g., javac -source 1.9 -target 1.8). //