For the complete nd4j-api index, please consult the Javadoc.
There are three types of operations used in ND4J: scalars, transforms and accumulations. We’ll use the word op synonymously with operation.
Most of the ops just take enums, or a list of discrete values that you can autocomplete. Activation functions are the exception, because they take strings such as "relu"
or "tanh"
.
Scalars, transforms and accumulations each have their own patterns. Transforms are the simplest, since the take a single argument and perform an operation on it. Absolute value is a transform that takes the argument x
like so abs(IComplexNDArray ndarray)
and produces the result which is the absolute value of x. Similarly, you would apply to the sigmoid transform sigmoid()
to produce the “sigmoid of x”.
Scalars just take two arguments: the input and the scalar to be applied to that input. For example, ScalarAdd()
takes two arguments: the input INDArray x
and the scalar Number num
; i.e. ScalarAdd(INDArray x, Number num)
. The same format applies to every Scalar op.
Finally, we have accumulations, which are also known as reductions in GPU-land. Accumulations add arrays and vectors to one another and can reduce the dimensions of those arrays in the result by adding their elements in a rowwise op. For example, we might run an accumulation on the array
Which would give us the vector
Reducing the columns (i.e. dimensions) from two to one.
Accumulations can be either pairwise or scalar. In a pairwise reduction, we might be dealing with two arrays, x and y, which have the same shape. In that case, we could calculate the cosine similarity of x and y by taking their elements two by two.
Or take EuclideanDistance(arr, arr2)
, a reduction between one array arr
and another arr2
.
Many ND4J ops are overloaded, meaning methods sharing a common name have different argument lists. Below we will explain only the simplest configurations.
As you can see, there are three possible argument types with ND4J ops: inputs, optional arguments and outputs. The outputs are specified in the ops’ constructor. The inputs are specified in the parentheses following the method name, always in the first position, and the optional arguments are used to transform the inputs; e.g. the scalar to add; the coefficient to multiply by, always in the second position.
For other transforms, please see this page.
Here are two examples of performing z = tanh(x)
, in which the original array x
is unmodified.
The latter two examples above use ND4J’s basic convention for all ops, in which we have 3 NDArrays, x, y and z.
Frequently, z = x
(this is the default if you use a constructor with only one argument). But there are exceptions for situations like x = x + y
. Another possibility is z = x + y
, etc.
Most accumulations are accessable directly via the INDArray interface.
For example, to add up all elements of an NDArray:
Accum along dimension example - i.e., sum values in each row:
Accumulations along dimensions generalize, so you can sum along two dimensions of any array with two or more dimensions.
A simple example:
Interval is fromInclusive, toExclusive; note that can equivalently use inclusive version: NDArrayIndex.interval(1,2,true);
These are views of the underlying array, not copy operations (which provides greater flexibility and doesn’t have cost of copying).
To avoid in-place behaviour, random.get(…).dup() to make a copy.
If you do not understand the explanation of ND4J’s syntax, cannot find a definition for a method, or would like to request that a function be added, please let us know on the community forums.
Method
What it does
Transforms
ACos(INDArray x)
Trigonometric inverse cosine, elementwise. The inverse of cos such that, if y = cos(x)
, then x = ACos(y)
.
ASin(INDArray x)
Also known as arcsin. Inverse sine, elementwise.
ATan(INDArray x)
Trigonometric inverse tangent, elementwise. The inverse of tan, such that, if y = tan(x)
then x = ATan(y)
.
Transforms.tanh(myArray)
Hyperbolic tangent: a sigmoidal function. This applies elementwise tanh inplace.
Nd4j.getExecutioner().exec(Nd4j.getOpFactory() .createTransform(“tanh”, myArray))
equivalent to the above
Scalar
INDArray.add(number)
Returns the result of adding number
to each entry of INDArray x
; e.g. myArray.add(2.0)
INDArray.addi(number)
Returns the result of adding number
to each entry of INDArray x
.
ScalarAdd(INDArray x, Number num)
Returns the result of adding num
to each entry of INDArray x
.
ScalarDivision(INDArray x, Number num)
Returns the result of dividing each entry of INDArray x
by num
.
ScalarMax(INDArray x, Number num)
Compares each entry of INDArray x
to num
and returns the higher quantity.
ScalarMultiplication(INDArray x, Number num)
Returns the result of multiplying each entry of INDArray x
by num
.
ScalarReverseDivision(INDArray x, Number num)
Returns the result of dividing num
by each element of INDArray x
.
ScalarReverseSubtraction(INDArray x, Number num)
Returns the result of subtracting each entry of INDArray x
from num
.
ScalarSet(INDArray x, Number num)
This sets the value of each entry of INDArray x
to num
.
ScalarSubtraction(INDArray x, Number num)
Returns the result of subtracting num
from each entry of INDArray x
.
A vector, that column of numbers we feed into neural nets, is simply a subclass of a more general mathematical structure called a tensor. A tensor is a multidimensional array.
You are already familiar with a matrix composed of rows and columns: the rows extend along the y axis and the columns along the x axis. Each axis is a dimension. Tensors have additional dimensions.
Tensors also have a so-called rank: a scalar, or single number, is of rank 0; a vector is rank 1; a matrix is rank 2; and entities of rank 3 and above are all simply called tensors.
It may be helpful to think of a scalar as a point, a vector as a line, a matrix as a plane, and tensors as objects of three dimensions or more. A matrix has rows and columns, two dimensions, and therefore is of rank 2. A three-dimensional tensor, such as those we use to represent color images, has channels, rows and columns, and therefore counts as rank 3.
As mathematical objects with multiple dimensions, tensors have a shape, and we specify that shape by treating tensors as n-dimensional arrays.
With ND4J, we do that by creating a new nd array and feeding it data, shape and order as its parameters. In pseudo code, this would be
In real code, this line
creates an array with four elements, whose shape is 2 by 2, and whose order is “row major”, or rows first, which is the default in C. (In contrast, Fortran uses “column major” ordering, and could be specified with an ‘f’ as the third parameter.) The distinction between thetwo orderings, for the array created above, is best illustrated with a table:
Once we create an n-dimensional array, we may want to work with slices of it. Rather than copying the data, which is expensive, we can simply “view” muli-dimensional slices. A slice of array “a” could be defined like this:
which would give you the first 5 channels, rows 3 to 4 and columns 6 to 7, and so forth for n dimensions, which each individual dimension’s slice starting before the colon and ending after it.
Now, while it is useful to imagine matrices as two-dimensional planes, and 3-D tensors are cubic volumes, we store all tensors as a linear buffer. That is, they are all flattened to a row of numbers.
For that linear buffer, we specify something called stride. Stride tells the computation layer how to interpret the flattened representation. It is the number of elements you skip in the buffer to get to the next channel or row or column. There’s a stride for each dimension.
Here’s a brief video summarizing how tensors are converted into linear byte buffers for ND4J.
The word tensor derives from the Latin tendere, or “to stretch”; therefore, tensor relates to that which stretches, the stretcher. Tensor was introduced to English from the German in 1915, after being coined by Woldemar Voigt in 1898. The mathematical object is called a tensor because an early application of the idea was the study of materials stretching under tension.
Row-major (C)
Column-major (Fortran)
[1,2]
[1,3]
[3,4]
[2,4]
Model import framework overview and examples
An nd4j "op" is an individual math operator (think add, subtract). This is best described in nd4j's DynamicCustomOp. Given a set of input arguments as ndarrays
It then outputs ndarrays to be potentially passed to other ops for execution. Each op may have other input parameters such as booleans, doubles, floats, ints, and longs.
For performance reasons, output arrays may be passed in if the memory has already been pre allocated. Otherwise, each op can calculate the output shape of its outputs and dynamically generate the result ndarrays.
Every op has a calculateOutputShape implemented in c++ that's used to dynnamically create result arrays. More information can be found in the architecture decision record for implementing this feature.
The op descriptor format is a serialized state of nd4j's operations. The current set of operations is saved in a protobuf format. You can find the latest op set here
This op descriptor format is generated from a utility that parses the java and c++ code bases to automatically generate the set of op descriptors based on the code. It is possible for a user to run this utility to generate their own op descriptors as well. If needed, the utility can be found here It is a self contained maven project that you can import and run yourself. Note, that at this time we do not publish the tool on maven central. It is not really meant for end users, but please feel free to ask about it on our forums. The core protobuf op descriptor files can be found here
This is equivalent to an onnx op or a tensroflow op. "Ops" in deep learning frameworks are essentially math operations as simple as add or subtract or as complex as convolution that operate on input and output ndarrays. They also may contain numerical or string attributes as parameters for the execution of the operation.
Neural network execution in deep learning frameworks happens as a directed acyclic graph. This allows a graph to be processed as a sequential list of operations.
For an end user, this means that every graph can be saved as a list of sequential steps for execution, eventually ending in 1 or more outputs.
The main concepts are fairly straightforward. A Mapper maps input attributes and input/output ndarrays to their equivalents in nd4j.
Nd4j's graph format is a flatbuffers format. The method for export maybe found here