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 extends NdArray>> 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 extends NdArray>> 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).
//