Comprehensive programming guide for ND4J. This user guide is designed to explain (and provide examples for) the main functionality in ND4J.
An NDArray is in essence n-dimensional array: i.e., a rectangular array of numbers, with some number of dimensions.
Some concepts you should be familiar with:
The rank of a NDArray is the number of dimensions. 2d NDArrays have a rank of 2, 3d arrays have a rank of 3, and so on. You can create NDArrays with any arbitrary rank.
The shape of an NDArray defines the size of each of the dimensions. Suppose we have a 2d array with 3 rows and 5 columns. This NDArray would have shape [3,5]
The length of an NDArray defines the total number of elements in the array. The length is always equal to the product of the values that make up the shape.
The stride of an NDArray is defined as the separation (in the underlying data buffer) of contiguous elements in each dimension. Stride is defined per dimension, so a rank N NDArray has N stride values, one for each dimension. Note that most of the time, you don't need to know (or concern yourself with) the stride - just be aware that this is how ND4J operates internally. The next section has an example of strides.
The data type of an NDArray refers to the type of data of an NDArray (for example, float or double precision). Note that this is set globally in ND4J, so all NDArrays should have the same data type. Setting the data type is discussed later in this document.
In terms of indexing there are a few things to know. First, rows are dimension 0, and columns are dimension 1: thus INDArray.size(0)
is the number of rows, and INDArray.size(1)
is the number of columns. Like normal arrays in most programming languages, indexing is zero-based: thus rows have indexes 0
to INDArray.size(0)-1
, and so on for the other dimensions.
Throughout this document, we'll use the term NDArray
to refer to the general concept of an n-dimensional array; the term INDArray
refers specifically to the Java interface that ND4J defines. In practice, these two terms can be used interchangeably.
The next few paragraphs describe some of architecture behind ND4J. Understanding this is not strictly necessary in order to use ND4J, but it may help you to understand what is going on behind the scenes. NDArrays are stored in memory as a single flat array of numbers (or more generally, as a single contiguous block of memory), and hence differs a lot from typical Java multidimensional arrays such as a float[][]
or double[][][]
.
Physically, the data that backs an INDArray is stored off-heap: that is, it is stored outside of the Java Virtual Machine (JVM). This has numerous benefits, including performance, interoperability with high-performance BLAS libraries, and the ability to avoid some shortcomings of the JVM in high-performance computing (such as issues with Java arrays being limited to 2^31 -1 (2.14 billion) elements due to integer indexing).
In terms of encoding, an NDArray can be encoded in either C (row-major) or Fortran (column-major) order. For more details on row vs. column major order, see Wikipedia. Nd4J may use a combination of C and F order arrays together, at the same time. Most users can just use the default array ordering, but note that it is possible to use a specific ordering for a given array, should the need arise.
The following image shows how a simple 3x3 (2d) NDArray is stored in memory,
In the above array, we have:
Shape = [3,3]
(3 rows, 3 columns)
Rank = 2
(2 dimensions)
Length = 9
(3x3=9)
Stride
C order stride: [3,1]
: the values in consecutive rows are separated in the buffer by 3, and the values consecutive columns are separated in the buffer by 1
F order stride: [1,3]
: the values in consecutive rows are separated in the buffer by 1, and the values in consecutive columns are separated in the buffer by 3
A key concept in ND4J is the fact that two NDArrays can actually point to the same underlying data in memory. Usually, we have one NDArray referring to some subset of another array, and this only occurs for certain operations (such as INDArray.get()
, INDArray.transpose()
, INDArray.getRow()
etc. This is a powerful concept, and one that is worth understanding.
There are two primary motivations for this:
There are considerable performance benefits, most notably in avoiding copying arrays
We gain a lot of power in terms of how we can perform operations on our NDArrays
Consider a simple operation like a matrix transpose on a large (10,000 x 10,000) matrix. Using views, we can perform this matrix transpose in constant time without performing any copies (i.e., O(1) in big O notation), avoiding the considerable cost copying all of the array elements. Of course, sometimes we do want to make a copy - at which point we can use the INDArray.dup()
to get a copy. For example, to get a copy of a transposed matrix, use INDArray out = myMatrix.transpose().dup()
. After this dup()
call, there will be no link between the original array myMatrix
and the array out
(thus, changes to one will not impact the other).
So see how views can be powerful, consider a simple task: adding 1.0 to the first row of a larger array, myArray
. We can do this easily, in one line:
myArray.getRow(0).addi(1.0)
Let's break down what is happening here. First, the getRow(0)
operation returns an INDArray that is a view of the original. Note that both myArrays
and myArray.getRow(0)
point to the same area in memory:
then, after the addi(1.0) is performed, we have the following situation:
As we can see, changes to the NDArray returned by myArray.getRow(0)
will be reflected in the original array myArray
; similarly, changes to myArray
will be reflected in the row vector.
Two of the most commonly used methods of creating arrays are:
Nd4j.zeros(int...)
Nd4j.ones(int...)
The shape of the arrays are specified as integers. For example, to create a zero-filled array with 3 rows and 5 columns, use Nd4j.zeros(3,5)
.
These can often be combined with other operations to create arrays with other values. For example, to create an array filled with 10s:
INDArray tens = Nd4j.zeros(3,5).addi(10)
The above initialization works in two steps: first by allocating a 3x5 array filled with zeros, and then by adding 10 to each value.
Nd4j provides a few methods to generate INDArrays, where the contents are pseudo-random numbers.
To generate uniform random numbers in the range 0 to 1, use Nd4j.rand(int nRows, int nCols)
(for 2d arrays), or Nd4j.rand(int[])
(for 3 or more dimensions).
Similarly, to generate Gaussian random numbers with mean zero and standard deviation 1, use Nd4j.randn(int nRows, int nCols)
or Nd4j.randn(int[])
.
For repeatability (i.e., to set Nd4j's random number generator seed) you can use Nd4j.getRandom().setSeed(long)
Nd4j provides convenience methods for the creation of arrays from Java float and double arrays.
To create a 1d NDArray from a 1d Java array, use:
Row vector: Nd4j.create(float[])
or Nd4j.create(double[])
Column vector: Nd4j.create(float[],new int[]{length,1})
or Nd4j.create(double[],new int[]{length,1})
For 2d arrays, use Nd4j.create(float[][])
or Nd4j.create(double[][])
.
For creating NDArrays from Java primitive arrays with 3 or more dimensions (double[][][]
etc), one approach is to use the following:
There are three primary ways of creating arrays from other arrays:
Creating an exact copy of an existing NDArray using INDArray.dup()
Create the array as a subset of an existing NDArray
Combine a number of existing NDArrays to create a new NDArray
For the second case, you can use getRow(), get(), etc. See Getting and Setting Parts of NDArrays for details on this.
Two methods for combining NDArrays are Nd4j.hstack(INDArray...)
and Nd4j.vstack(INDArray...)
.
hstack
(horizontal stack) takes as argument a number of matrices that have the same number of rows, and stacks them horizontally to produce a new array. The input NDArrays can have a different number of columns, however.
Example:
Output:
vstack
(vertical stack) is the vertical equivalent of hstack. The input arrays must have the same number of columns.
Example:
Output:
ND4J.concat
combines arrays along a dimension.
Example:
Output: