Skip to the lesson
FixTheRobot Home / VLA / Prerequisites / M2.2

VLA · Prerequisite · Python and Deep Learning

M2.2 — NumPy and Data Manipulation

Working with arrays, shapes and indexing.

  • Prerequisite module 2
  • Python and Deep Learning
  • Essential
  • About 13 minutes to read

Images, trajectories and datasets all live in arrays. This lesson covers the NumPy operations you will use constantly, and every one of them carries over almost unchanged to PyTorch.

Learning objectives

After this lesson, you should be able to:

  • Create arrays and read their shape, number of dimensions and dtype.
  • Index and slice arrays, and select with a boolean mask.
  • Use whole-array arithmetic instead of loops.
  • Predict the result of broadcasting two shapes.
  • Reduce an array along an axis and normalise columns.
  • Explain the difference between a view and a copy.
  1. A list of numbers becomes an array
  2. Shape, ndim and dtype describe it
  3. Indexing and slicing select from it
  4. Broadcasting combines arrays of different shapes
  5. Reductions along an axis summarise it
  6. The result feeds a model
The flow of this lesson: arrays carry data, and a small set of operations reshapes, selects and summarises it.

The one-sentence idea

An array is a grid of numbers with a shape, and almost everything you do with data is selecting from it, combining it, or summarising it along an axis.

When something goes wrong, print the shape.

Why NumPy

Python lists are flexible but slow for numbers, and they know nothing about rows, columns or images. NumPy adds an array type: a grid of numbers of one type, stored compactly, with fast operations that act on the whole grid at once. Images, robot trajectories, datasets and the inputs to neural networks are all handled as arrays.

NumPy is also the model for PyTorch tensors, which you meet in the next lesson. The two libraries share most of their vocabulary: shapes, indexing, slicing, broadcasting, and reductions along an axis. Learning it here pays off twice.

Arrays, shape and dtype

Every array has a shape (its size along each dimension), a number of dimensions ndim, and a dtype (the type of its numbers, such as float64). These three facts are the first things to check when code misbehaves, in the same way the shape rule in M1.1 told you whether a multiplication was valid.

import numpy as np

a = np.array([[1.0, 2.0, 3.0],
              [4.0, 5.0, 6.0]])
print(a.shape, a.ndim, a.dtype)
print(np.zeros((2, 3)))
print(np.arange(0, 1, 0.25))
print(np.linspace(0, 1, 5))

Output:

(2, 3) 2 float64
[[0. 0. 0.]
 [0. 0. 0.]]
[0.   0.25 0.5  0.75]
[0.   0.25 0.5  0.75 1.  ]

np.zeros and np.ones make arrays of a given shape, np.arange counts with a step, and np.linspace makes a given number of evenly spaced values including both ends. Passing np.array a list of lists makes a 2-dimensional array, with one inner list per row.

Indexing and slicing

Indexing works as it does for lists, with one index per dimension, separated by commas. The rules from Python still hold: counting starts at 0, and a slice a:b stops before b. A lone colon : means “everything along this dimension”.

a = np.arange(12).reshape(3, 4)     # numbers 0..11 arranged as 3 rows, 4 columns
print(a)
print(a[1, 2])        # row 1, column 2
print(a[0])           # the whole first row
print(a[:, 1])        # the whole second column
print(a[1:, 2:])      # rows 1 onward, columns 2 onward

Output:

[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
6
[0 1 2 3]
[1 5 9]
[[ 6  7]
 [10 11]]

You can also select with a boolean mask: a comparison produces an array of true/false values, and using it as an index keeps only the true positions. It can be used to read or to change values.

v = np.array([0.1, -0.5, 1.2, 0.3])
print(v > 0.25)
print(v[v > 0.25])
v[v < 0] = 0.0         # replace negatives with zero
print(v)

Output:

[False False  True  True]
[1.2 0.3]
[0.1 0.  1.2 0.3]

Whole-array operations and broadcasting

Arithmetic on arrays works element by element, without a loop. a + b, a * b and np.sqrt(a) all act on every entry. This is both shorter and much faster than looping in Python.

Broadcasting lets arrays of different shapes combine. NumPy lines the shapes up from the right, and two dimensions are compatible if they are equal or one of them is 1; the size-1 dimension is then stretched to match. A common use is adding one vector to every row of a table:

traj = np.array([[0.0, 1.0],
                 [0.5, 1.5],
                 [1.0, 2.5]])        # shape (3, 2): 3 time steps, 2 joints
offset = np.array([1.0, -1.0])       # shape (2,)
print(traj + offset)                 # offset is added to each row

Output:

[[1.  0. ]
 [1.5 0.5]
 [2.  1.5]]

Shapes (3, 2) and (2,) line up from the right as 2 with 2, so the vector is applied to each of the 3 rows. When shapes do not fit, NumPy raises an error instead of guessing:

try:
    np.ones((2, 3)) + np.ones((2, 2))
except ValueError as e:
    print(type(e).__name__)

Output:

ValueError

Reducing along an axis

Functions such as sum, mean, max and std collapse an array. By default they use every entry; with axis= they collapse one dimension and keep the others. axis=0 goes down the rows, giving one result per column. axis=1 goes across the columns, giving one result per row.

print(traj.mean(axis=0))     # per joint, averaged over time
print(traj.max(axis=1))      # per time step, the largest joint value
print(traj.sum())            # everything

Output:

[0.5        1.66666667]
[1.  1.5 2.5]
6.5

A very common data step is normalisation: subtracting the mean and dividing by the standard deviation, per column, so that every feature has a comparable scale. Broadcasting does it in one line:

mean = traj.mean(axis=0)
std = traj.std(axis=0)
norm = (traj - mean) / std
print(norm.round(2))
print(np.allclose(norm.mean(axis=0), 0), np.allclose(norm.std(axis=0), 1))

Output:

[[-1.22 -1.07]
 [ 0.   -0.27]
 [ 1.22  1.34]]
True True

The same step, with statistics computed from a training set, is how action and observation values are scaled in many robot-learning pipelines. M4 and later lessons return to normalising and decoding actions.

Reshaping, transposing and matrix multiplication

reshape changes the shape without changing the data, as long as the total count of entries is the same. A -1 in one position means “work this size out for me”. .T transposes, swapping rows and columns. The @ operator is matrix multiplication, exactly the operation from M1.1:

W = np.array([[1, 2, 0],
              [0, 1, 3]])           # shape (2, 3)
x = np.array([2, 1, 1])             # shape (3,)
print(W @ x)                        # the example from M1.1
print(W.T.shape)
print(np.arange(6).reshape(2, -1).shape)

Output:

[4 4]
(3, 2)
(2, 3)

Arrays can have more than two dimensions. A colour image is usually stored with shape (height, width, 3), where the last axis holds red, green and blue. A batch of such images adds a leading axis. Selecting one colour channel is ordinary slicing:

img = np.zeros((4, 6, 3))           # height 4, width 6, three channels
print(img.shape)
print(img[:, :, 0].shape)           # the red channel as a 4 x 6 grid

Output:

(4, 6, 3)
(4, 6)

Views, copies and random numbers

A slice of an array is usually a view: it shares memory with the original, so changing the slice changes the array. Use .copy() when you want an independent array.

a = np.arange(5)
b = a[1:3]
b[0] = 99
print(a)               # the original changed too
c = a.copy()
c[0] = -1
print(a)               # the copy did not affect it

Output:

[ 0 99  2  3  4]
[ 0 99  2  3  4]

For random numbers, create a generator with a seed so that results can be repeated. Fixing the seed is how experiments and tests are made reproducible.

rng = np.random.default_rng(0)
print(rng.random(3).round(3))

Output:

[0.637 0.27  0.041]

Common misconceptions

Misconception 1 “a * b is matrix multiplication.”

Correction It multiplies matching entries. Matrix multiplication is a @ b, as in M1.1.

Misconception 2 “A slice is always a new, independent array.”

Correction A slice is normally a view of the same memory. Changing it changes the original unless you call .copy().

Misconception 3 “axis=0 means across the columns.”

Correction axis=0 runs down the rows, collapsing them and giving one value per column. axis=1 collapses the columns and gives one value per row.

Misconception 4 “Loops are fine for large arrays.”

Correction Python loops over array entries are far slower than whole-array operations, which run in compiled code. Prefer the whole-array form.

Engineering takeaways

  1. An array has a shape, a number of dimensions and a dtype. Check them first.
  2. Indexing starts at 0, a slice stops before its end, and a colon means everything.
  3. Boolean masks select or change the entries that satisfy a condition.
  4. Whole-array arithmetic replaces loops and is much faster.
  5. Broadcasting aligns shapes from the right, and dimensions must be equal or 1.
  6. axis says which dimension a reduction collapses.
  7. Subtracting the mean and dividing by the standard deviation per column is one line with broadcasting.
  8. Slices are views. Copy when you need independence, and seed random generators for repeatability.

Know the shape of every array you touch, and most data bugs announce themselves.

Knowledge check

Try each question first, then reveal the explanation.

Question 1

An array x has shape (5, 3). What is the shape of x.mean(axis=0), and of x.mean(axis=1)?

Explanation

(3,) and (5,). axis=0 collapses the 5 rows and leaves one mean per column. axis=1 collapses the 3 columns and leaves one mean per row.

Question 2

Can an array of shape (4, 3) be added to one of shape (3,)? What about (4,)?

Explanation

Yes for (3,), no for (4,). Aligned from the right, 3 matches 3 and the vector is applied to each of the 4 rows. A shape of (4,) would be lined up with the length-3 dimension, and 4 does not match 3.

Question 3

You write b = a[:2] and then b[0] = 0. Does a change? How do you avoid it?

Explanation

Yes, a[0] also changes, because the slice is a view of the same memory. Use b = a[:2].copy() to get an independent array.

What’s next

You can now handle arrays in NumPy. Next is the tensor library that most robot-learning code is written in, which uses the same ideas and adds learning.

Next lesson

M2.3 — PyTorch tensors and tensor operations

The tensor library most robot-learning code is written in.

Back to the VLA roadmap prerequisites