Tensor Operations Worth Mastering: tensor.unsqueeze(0) vs. array[None]

Tensor Operations Worth Mastering: tensor.unsqueeze(0) vs. array[None]
Photo by Merve Sehirli Nasir / Unsplash

Today we will walk through unsqueeze(0), and along the way touch on a familiar problem many of us run into.

Can you tell "value.unsqueeze(0)" and "value[None]" apart at a glance?


The former is primarily a PyTorch operation, while the latter belongs to NumPy.

But both appear to prepend a new dimension, so a quick glance at the code is often not enough to tell which one you are looking at.

The reason is that in deep learning code, PyTorch tensors and NumPy array operations tend to get thoroughly intermingled.

So today's topic is exactly that: PyTorch and NumPy code getting mixed together until you can no longer tell which kind of array (or tensor) you are working with.

As an aside, to keep the discussion focused: PyTorch also allows [None] to prepend a new dimension, not just unsqueeze(0). For the purposes of this article, however, please allow us to treat [None] as an operation used primarily in NumPy.

We plan to share our own prescription for this in a separate post, but in short, we try to keep PyTorch and NumPy code separated so the two do not casually blend together. For example, we follow practices such as "keep a single function or method consistently in either PyTorch or NumPy" and "hold off on converting to PyTorch tensors until just before sending data to the GPU, doing as much as possible in NumPy" — the small, hard-won workarounds of everyday practice.

When things get especially confusing, we also use variable names like "something_numpy" or "something_tensor." That said, given Python's loosely typed conventions, there is plenty of external code in which the same variable quietly changes from NumPy to PyTorch along the way, so this remains a genuinely hard problem.

A world where PyTorch and NumPy intermingle — if you work on machine learning projects, does this sound familiar?

  • You did your data preprocessing in NumPy, but the data must be converted to PyTorch tensors before it goes into the model.
  • You convert the PyTorch tensors output by the model back into NumPy arrays for visualization.
  • And before you know it, NumPy and PyTorch functions are mixed together throughout your code...

It is something of a Tower of Babel of programming.

In this article, let's take a detailed look at the difference between PyTorch's .unsqueeze(0) method and NumPy's [None] indexing. These operations look similar at first glance, but there are in fact important differences between them.

1. The Basic Difference

First, the most fundamental difference, as touched on at the beginning:

  • .unsqueeze(0): a method used on PyTorch tensors.
  • [None]: an indexing operation used on NumPy arrays and Python lists.
    (As noted in the aside above, this actually works in PyTorch as well. Still, in PyTorch we recommend unsqueeze(0) for adding a dimension at the front and unsqueeze(-1) for adding one at the end, for the sake of readability and clarity of intent.)

2. How They Behave

.unsqueeze(0)

PyTorch's .unsqueeze(0) method adds a new dimension at dimension 0 (the front) of a tensor. It is commonly used when preparing data for batch processing. Even when you want to feed a single sample into a model, a "batch dimension" is almost always required, so unsqueeze(0) shows up frequently in real-world code.

import torch

x = torch.tensor([1, 2, 3])
print(x.shape)  # torch.Size([3])

x_unsqueezed = x.unsqueeze(0)
print(x_unsqueezed.shape)  # torch.Size([1, 3])

[None]

NumPy's [None] indexing adds a new axis to an array. In effect, this also increases the number of dimensions by one.

Example:

import numpy as np

y = np.array([1, 2, 3])
print(y.shape)  # (3,)

y_expanded = y[None]
print(y_expanded.shape)  # (1, 3)

3. Differences in Flexibility

.unsqueeze(n) method offers extra flexibility: by changing its n argument, you can insert a dimension at any position.

Example:

import torch

z = torch.tensor([[1, 2], [3, 4]])
print(z.shape)  # torch.Size([2, 2])

z_unsqueezed_0 = z.unsqueeze(0)
print(z_unsqueezed_0.shape)  # torch.Size([1, 2, 2])

z_unsqueezed_1 = z.unsqueeze(1)
print(z_unsqueezed_1.shape)  # torch.Size([2, 1, 2])

On the other hand, [None] always adds the new axis at the front (axis 0). That said, NumPy also provides the np.expand_dims() function, which lets you insert a dimension at any position.

import numpy as np

w = np.array([[1, 2], [3, 4]])
print(w.shape)  # (2, 2)

w_expanded_0 = np.expand_dims(w, axis=0)
print(w_expanded_0.shape)  # (1, 2, 2)

w_expanded_1 = np.expand_dims(w, axis=1)
print(w_expanded_1.shape)  # (2, 1, 2)

4. Performance Considerations

In general, there is no significant performance difference between .unsqueeze() and [None] (or np.expand_dims()). However, when working with large datasets or complex models, small differences can accumulate and have a measurable impact.

If you are working in PyTorch, using .unsqueeze() is the natural and efficient choice; if you are working in NumPy, use [None] or np.expand_dims() instead.

Summary — Putting .unsqueeze(0) and [None] into Practice

In this article, we took a detailed look at how .unsqueeze(0) and [None] are used.

The root of the problem lies in the mixing of PyTorch and NumPy, and when writing code it is important to stay aware, at all times, of which "world" you are in.

When reading code, once .unsqueeze(0) appears you can think, "this is where a dimension gets added on the PyTorch side," and when you see [None] you can read it as "we are still in NumPy territory."

Where each operation tends to appear is another important point..unsqueeze(0) is most often used as a "last-minute" dimension addition right before feeding a single sample into a model, so you will frequently spot it immediately before model input. In contrast, [None] usually adds its dimension at a much earlier stage — while the data is still in the NumPy processing phase. A common pattern is to convert to a PyTorch tensor and send the data to the GPU only at the very end, right before model input.

When you encounter these operations, a good first guess is "is this adding a batch dimension?" since that is their most common purpose. They are not always about batch dimensions, though — in image processing, for example, they may add a channel dimension — so it is important to check the surrounding context carefully.

In conclusion, understanding the difference between .unsqueeze(0) and [None], and using each where it belongs, will help you write clearer and more efficient code. And when you come across these operations, making a habit of thinking "this may be adding a batch dimension" while always confirming the context will give you a deeper understanding of the code's intent.

Read more