-
Notifications
You must be signed in to change notification settings - Fork 125
Tutorial: N Dimensional Array
Multi-dimensional array (i.e., n-dimensional array) is extremely useful in scientific computing, e.g., computer vision, image processing, and etc. Therefore, n-dimensional array support is necessary in modern numerical libraries.
Owl has two very powerful modules to manipulate dense n-dimensional arrays. One is Ndarray
, and the other is Ndview
. Ndarray
is very similar to the corresponding modules in Numpy and Julia, whereas Ndview
is specifically optimised for pipelining the operations on the ndarray.
In this tutorial, I will only focus on the Ndarray
module, and I will present a series of examples to walk you through the functionality in the module.
With Ndarray
module, you can create four types of n-dimensional arrays, and these types are borrowed directly from OCaml's Bigarray
module. They are: Float32
, Float64
, Complex32
, and Complex64
.
First, we can create empty ndarrays of shape [|3;4;5|]
with the following code. Because Ndarray
is built atop of Bigarray
, it supports maximum 16 dimensions. For even higher dimensions, Owl will provides a separate module to support high-dimensional sparse ndarrays in the future.
let x0 = Dense.Ndarray.empty Bigarray.Float32 [|3;4;5|];;
let x1 = Dense.Ndarray.empty Bigarray.Float64 [|3;4;5|];;
let x2 = Dense.Ndarray.empty Bigarray.Complex32 [|3;4;5|];;
let x3 = Dense.Ndarray.empty Bigarray.Complex64 [|3;4;5|];;
The elements in a ndarray are not initialised by calling empty
function. You can certainly assign the initial values to the elements by calling create
, generate a zero/one ndarray by calling zeros
or ones
, or even create a random ndarray by calling uniform
.
Dense.Ndarray.zeros Bigarray.Complex32 [|3;4;5|];;
Dense.Ndarray.ones Bigarray.Float64 [|3;4;5|];;
Dense.Ndarray.create Bigarray.Float32 [|3;4;5|] 1.5;;
Dense.Ndarray.create Bigarray.Complex32 [|3;4;5|] Complex.({im=1.5; re=2.5});;
Dense.Ndarray.uniform Bigarray.Float64 [|3;4;5|];;
If you want to assign the initial values in a more complicated way. You can first create an empty ndarray, then call the map
function in Ndarray module. The following example calls Owl's Stats.Rnd.gaussian
function to initialised each element in x
.
let x = Dense.Ndarray.zeros Bigarray.Float64 [|3;4;5|]
|> Dense.Ndarray.map (fun _ -> Stats.Rnd.gaussian ());;
Then you can print x
out using `` function to check the element values.
Dense.Ndarray.print x;;
There are a set of functions you can call to obtain the basic properties of a n-dimensional array.
Dense.Ndarray.shape x;; (* return the shape of the ndarray *)
Dense.Ndarray.num_dims x;; (* return the number of dimensions *)
Dense.Ndarray.nth_dim x 0;; (* return the size of first dimension *)
Dense.Ndarray.numel x;; (* return the number of elements *)
Dense.Ndarray.nnz x;; (* return the number of non-zero elements *)
Dense.Ndarray.density x;; (* return the percent of non-zero elements *)
Dense.Ndarray.kind x;; (* return the number of elements *)
You can check whether two ndarrays have the same shape by same_shape
function.
Dense.Ndarray.same_shape x y;;
The standard way of accessing and modifying the elements in a ndarray is get
and set
functions. You need to pass in the index of the element to indicate which element you want to access. Moreover, the modification by calling set
is in place.
Dense.Ndarray.get x [|0;1;1|];;
Dense.Ndarray.set x [|0;1;1|] 2.;;
Using fill
, you can set all the elements to one value at once instead of calling set
in a loop.
Dense.Ndarray.fill x 5.;;
You can make a copy of a ndarray using clone
, or copy the elements in one ndarray to another using copy
.
let y = Dense.Ndarray.clone x;;
let z = Dense.Ndarray.(empty (kind x) (shape x));;
Dense.Ndarray.copy x z;;
A ndarray can be flattened or reshaped, but you need to make sure the total number of elements is the same before and after reshaping. Reshaping will not modify the original data but make a copy of the ndarray.
let y = Dense.Ndarray.flatten x;;
let z = Dense.Ndarray.reshape x [|5;4;3|];;
You can transpose a ndarray along multiple axises by calling transpose
function. The parameter passed to transpose
must be a valid permutation of axis indices. E.g., for the previously created three-dimensional ndarray x
, it can be
let y = Dense.Ndarray.transpose ~axis:[|0;1;2|] x;; (* no changes actually *)
let y = Dense.Ndarray.transpose ~axis:[|0;2;1|] x;;
let y = Dense.Ndarray.transpose ~axis:[|1;0;2|] x;;
let y = Dense.Ndarray.transpose ~axis:[|1;2;0|] x;;
let y = Dense.Ndarray.transpose ~axis:[|2;0;1|] x;;
let y = Dense.Ndarray.transpose ~axis:[|2;1;0|] x;;
If you only want to swap two axises, you can call swap
instead of transpose
. The following two lines of code are equivalent.
let y = Dense.Ndarray.swap 0 1 x;;
let y = Dense.Ndarray.transpose ~axis:[|1;0;2|] x;;
With these created ndarray, you can do some math operation as below.
let x = Dense.Ndarray.uniform Bigarray.Float64 [|3;4;5|];;
let y = Dense.Ndarray.uniform Bigarray.Float64 [|3;4;5|];;
let z = Dense.Ndarray.add x y;;
Dense.Ndarray.print z;;
Owl supports many math operations and these operations have been well vectorised so they are very fast.
Dense.Ndarray.sin x;;
Dense.Ndarray.tan x;;
Dense.Ndarray.exp x;;
Dense.Ndarray.log x;;
Dense.Ndarray.min x;;
Dense.Ndarray.add_scalar x 2.;;
Dense.Ndarray.mul_scalar x 2.;;
...
Examining elements and comparing two ndarrays are also very easy.
Dense.Ndarray.is_zero x;;
Dense.Ndarray.is_positive x;;
Dense.Ndarray.is_nonnegative x;;
...
Dense.Ndarray.is_equal x y;;
Dense.Ndarray.is_greater x y;;
Dense.Ndarray.equal_or_smaller x y;;
...
You can certainly plug in your own functions to check each elements.
Dense.Ndarray.exists ((>) 2.) x;;
Dense.Ndarray.not_exists ((<) 2.) x;;
Dense.Ndarray.for_all ((=) 2.) x;;
Most importantly, you can use Owl to iterate a ndarray in various ways. Owl provides a simple but flexible and powerful way to define a "slice" in ndarray. Comparing to the "Bigarray.slice_left
" function, the slice in Owl does not have to start from the left-most axis. E.g., for the previously defined [|3;4;5|]
ndarray, you can define a slice in the following ways:
let s0 = [|None; None; None|] (* (*,*,*), essentially the whole ndarray as one slice *)
let s1 = [|Some 0; None; None|] (* (0,*,*) *)
let s2 = [|None; Some 2; None|] (* (*,2,*) *)
let s3 = [|None; None; Some 1|] (* (*,*,1) *)
let s4 = [|Some 1; None; Some 2|] (* (1,*,2) *)
...
With the slice definition above, we can iterate and map the elements in a slice. E.g., we add one to all the elements in slice (0,*,*)
.
Dense.Ndarray.map ~axis:[|Some 0; None; None|] (fun a -> a +. 1.) x;;
There are more functions to help you to iterate elements and slices in a ndarray: iteri
, iter
, mapi
, map
, filteri
, filter
, foldi
, fold
, iteri_slice
, iter_slice
, iter2i
, iter2
. Please refer to the documentation for their details.