Tutorial 1: Tensors¶
Torchlight's Tensor is the central object: an N-dimensional array of
float32 with automatic shape/strides bookkeeping and reverse-mode auto-diff
when you ask for it.
Creating tensors¶
Create tensors from Python lists or numpy arrays:
import numpy as np
import torchlight as tl
a = tl.tensor([1.0, 2.0, 3.0]) # from a list -> shape (3,)
b = tl.tensor([[1.0, 2.0], [3.0, 4.0]]) # shape (2, 2)
c = tl.from_numpy(np.arange(6).reshape(2, 3)) # from numpy -> shape (2, 3)
Factory functions cover the common allocations:
tl.zeros((2, 3)) # all zeros
tl.ones((2, 3)) # all ones
tl.full((2, 3), 0.5) # all 0.5
tl.empty((2, 3)) # uninitialised
tl.randn((2, 3)) # standard normal
tl.rand((2, 3)) # uniform [0, 1)
tl.arange(0, 10, 2) # [0, 2, 4, 6, 8]
Every factory accepts requires_grad=True and a device= backend hint
(see Tutorial 5 — Backends):
x = tl.zeros((3, 4), requires_grad=True, device="cpu")
Shape, strides, and views¶
A tensor knows its shape and total number of elements:
x = tl.tensor(np.arange(12).reshape(3, 4))
x.shape # (3, 4)
x.size # 12
x.dims # 2
Views share the underlying storage — no copy:
y = x.view(4, 3) # reshape view (contiguous)
t = x.transpose(0, 1) # (4, 3) strided view, no copy
p = x.permute(1, 0) # same as transpose
r = x.reshape(2, 6) # materialises only when storage is non-contiguous
contiguous() forces a dense copy when the view is strided:
t = x.transpose(0, 1)
t.is_contiguous # False for a transposed view
dense = t.contiguous() # dense copy
Broadcasting¶
Binary ops broadcast following numpy's rules (trailing dimensions align, size-1 dims expand):
a = tl.tensor([[1.0], [2.0]]) # (2, 1)
b = tl.tensor([[10.0, 20.0]]) # (1, 2)
s = a + b # (2, 2): [[11, 21], [12, 22]]
TensorData implements the index arithmetic; strided
broadcast views use stride-zero, so no data is ever copied.
Elementwise ops, reductions, and more¶
x = tl.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
x.abs().to_numpy() # [2, 1, 0, 1, 2]
x.sigmoid() # logistic, numerically stable
x.relu() # max(x, 0)
x.sqrt() # guarded for x <= 0 (returns 0)
x.exp(); x.log(); x.tanh()
m = tl.tensor([[1.0, 2.0], [3.0, 4.0]])
m.sum() # shape (1,) = 10
m.sum(dim=0) # shape (1, 2)
m.mean() # 2.5
m.max(dim=1) # shape (2, 1)
m.var(); m.std() # population variance / std
m.clamp(2.0, 3.0) # elementwise clip
c1 = tl.tensor([2.0, 3.0])
c2 = tl.tensor([1.0, 5.0])
c1.is_close(c2) # |a-b| < 1e-2, returns 0/1 tensor
Matrix multiplication uses the @ operator (with batched support):
A = tl.tensor(np.random.randn(3, 4))
B = tl.tensor(np.random.randn(4, 5))
C = A @ B # (3, 5)
Getting numbers back out¶
t = tl.tensor([[1.0, 2.0], [3.0, 4.0]])
t.to_numpy() # dense numpy copy
t[0, 1] # scalar float (full-index read)
t.item(0, 1) # same, explicit
In-place mutation helpers¶
These write into the existing buffer:
t = tl.zeros((3,))
t.fill_(2.5) # all entries 2.5
t.uniform_(-1.0, 1.0) # in-place U(-1, 1)
t.normal_(0.0, 1.0) # in-place N(0, 1)
t.zeros_(); t.ones_() # in-place reset
The storage layer¶
Underneath every Tensor is a TensorData in torchlight.core: a flat one-
dimensional float32 buffer plus shape/strides. The backend kernels (numpy
ufuncs, numba/cuda kernels) operate directly on that layout via
numpy_view() — this is what keeps the whole engine free of Python loops.
See TensorData API for the low-level view helpers
(permute, broadcast_to, numpy_view, to_numpy).
Next¶
Continue to Tutorial 2 — Autograd to see how gradients flow through the graph.