The idea
Real projects mix libraries: data arrives as a pandas DataFrame, preprocessing happens in NumPy or scikit-learn, the model runs in PyTorch, and the results go back into a DataFrame or a Plotly chart. Two things make these hand-overs smooth:
- knowing when a conversion copies data and when it shares memory;
- knowing which device (CPU or GPU) a tensor lives on.
From NumPy to PyTorch
Your data usually starts life in NumPy or pandas, so the first step of most PyTorch projects is converting it. There are two ways, and the difference matters:
import numpy as np
import torch
arr = np.array([1.0, 2.0, 3.0])
shared = torch.from_numpy(arr) # shares memory with arr
copied = torch.tensor(arr) # an independent copy
arr[0] = 100.0
print(shared)
print(copied)
print(shared.dtype)
tensor([100., 2., 3.], dtype=torch.float64)
tensor([1., 2., 3.], dtype=torch.float64)
torch.float64
torch.from_numpycreates a tensor that views the same memory: changing one changes the other (the NumPy views module, across libraries).torch.tensor(arr)always copies.- The dtype comes from NumPy:
float64here, not PyTorch's usual float32. Convert with.float()before feeding a model:
X = torch.from_numpy(np.random.default_rng(0).random((3, 2))).float()
print(X.dtype, X.shape)
torch.float32 torch.Size([3, 2])
From PyTorch to NumPy
And back again: after training, you'll often want predictions as a NumPy array, to chart them or put them in a DataFrame:
t = torch.tensor([0.25, 0.5, 0.75])
a = t.numpy() # shares memory (CPU tensors only)
t[0] = 9.0
print(a)
[9. 0.5 0.75]
Two rules for .numpy():
- The tensor must be on the CPU:
t.cpu().numpy()for a GPU tensor. - If the tensor is part of a gradient computation (
requires_grad=True), detach it first:t.detach().cpu().numpy(). You'll see this exact chain all the time when taking predictions out of a model.
From pandas
Most real data arrives as a DataFrame, like the churn table from the ML track. The usual route goes through NumPy:
import pandas as pd
df = pd.DataFrame({"tenure_months": [12, 33, 5], "orders_90d": [7, 4, 0], "churned": [0, 0, 1]})
X = torch.tensor(df[["tenure_months", "orders_90d"]].to_numpy(), dtype=torch.float32)
y = torch.tensor(df["churned"].to_numpy(), dtype=torch.float32)
print(X)
print(y)
tensor([[12., 7.],
[33., 4.],
[ 5., 0.]])
tensor([0., 0., 1.])
The usual route is DataFrame → NumPy (.to_numpy()) → tensor, choosing dtype=torch.float32 explicitly.
Devices
A tensor lives on a device: the CPU, an NVIDIA GPU ("cuda"), or Apple Silicon's GPU ("mps"). Maths can only combine tensors on the same device.
device = (
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu"
)
print("using", device)
t = torch.ones(3)
print(t.device)
t = t.to(device) # copies to the device (no-op if it's already there)
using mps
cpu
The printed device depends on your machine: cuda on an NVIDIA GPU machine, mps on a Mac with Apple Silicon, cpu otherwise.
The standard pattern: pick device once at the top, then move the model and every batch to it:
model = MyModel().to(device)
for X_batch, y_batch in loader:
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
predictions = model(X_batch)
Forget one .to(device) and you get the famous error "Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu". Read it as a checklist: which tensor didn't get moved?
Choose the device once; move the model and each batch to it; bring results back with .detach().cpu().numpy(). from_numpy and .numpy() share memory; torch.tensor(...) copies.
When does the GPU help?
GPUs are fast at big, parallel tensor maths: large matrix multiplications over big batches. For small tabular models (a few thousand rows, a small network), the time spent copying data to the GPU can exceed the time saved, and the CPU is perfectly fine. Measure before assuming.
Why this matters for clean code
Keep conversions at the edges of your code: convert once when data enters the model code, and once when results leave. A device variable defined in one place (often from a config file) lets the same script run on a laptop and on a GPU server without edits. Scattered .cuda() calls are the PyTorch version of hard-coded file paths.
Recap
torch.from_numpy(arr)shares memory;torch.tensor(arr)copies; NumPy float64 becomes float64, so call.float().t.numpy()shares memory and needs a CPU tensor; uset.detach().cpu().numpy()from models.- pandas →
.to_numpy()→torch.tensor(..., dtype=torch.float32). - Devices:
"cpu","cuda","mps"; choose once,.to(device)the model and every batch. - All tensors in an operation must be on the same device.