Module 01: Tensor#
Module Info
FOUNDATION TIER | Difficulty: ââââ | Time: 4-6 hours | Prerequisites: None
Prerequisites: None means exactly that. This module assumes:
Basic Python (lists, classes, methods)
Basic math (matrix multiplication from linear algebra)
No machine learning background required
If you can multiply two matrices by hand and write a Python class, youâre ready.
Overview#
The Tensor class is the foundational data structure of machine learning. Every neural network, from recognizing handwritten digits to translating languages, operates on tensors. These networks process millions of numbers per second, and tensors are the data structure that makes this possible. In this module, youâll build N-dimensional arrays from scratch, gaining deep insight into how PyTorch works under the hood.
By the end, your tensor will support arithmetic, broadcasting, matrix multiplication, and shape manipulation - exactly like torch.Tensor.
Learning Objectives#
Tip
By completing this module, you will:
Implement a complete Tensor class with arithmetic, matrix multiplication, shape manipulation, and reductions
Master broadcasting semantics that enable efficient computation without data copying
Understand computational complexity (O(nÂł) for matmul) and memory trade-offs (views vs copies)
Connect your implementation to production PyTorch patterns and design decisions
What Youâll Build#
flowchart LR
subgraph "Your Tensor Class"
A["Properties<br/>shape, size, dtype"]
B["Arithmetic<br/>+, -, *, /"]
C["Matrix Ops<br/>matmul()"]
D["Shape Ops<br/>reshape, transpose"]
E["Reductions<br/>sum, mean, max"]
end
A --> B --> C --> D --> E
style A fill:#e1f5ff
style B fill:#fff3cd
style C fill:#f8d7da
style D fill:#d4edda
style E fill:#e2d5f1
Fig. 4 Your Tensor Class#
Implementation roadmap:
Part |
What Youâll Implement |
Key Concept |
|---|---|---|
1 |
|
Tensor as NumPy wrapper |
2 |
|
Operator overloading + broadcasting |
3 |
|
Matrix multiplication with shape validation |
4 |
|
Shape manipulation, views vs copies |
5 |
|
Reductions along axes |
The pattern youâll enable:
# Computing predictions from data
output = x.matmul(W) + b # Matrix multiplication + bias (used in every neural network)
What Youâre NOT Building (Yet)#
To keep this module focused, you will not implement:
GPU support (NumPy runs on CPU only)
Automatic differentiation (thatâs Module 05: Autograd)
Hundreds of tensor operations (PyTorch has 2000+, youâll build ~15 core ones)
Memory optimization tricks (PyTorch uses lazy evaluation, memory pools, etc.)
You are building the conceptual foundation. Speed optimizations come later.
API Reference#
This section provides a quick reference for the Tensor class youâll build. Think of it as your cheat sheet while implementing and debugging. Each method is documented with its signature and expected behavior.
Constructor#
Tensor(data, requires_grad=False)
data: list, numpy array, or scalarrequires_grad: enables gradient tracking (activated in Module 05)
Properties#
Your Tensor wraps a NumPy array and exposes several properties that describe its structure. These properties are read-only and computed from the underlying data.
Property |
Type |
Description |
|---|---|---|
|
|
Underlying NumPy array |
|
|
Dimensions, e.g., |
|
|
Total number of elements |
|
|
Data type (float32) |
|
|
Gradient storage (Module 05) |
Arithmetic Operations#
Python lets you override operators like + and * by implementing special methods. When you write x + y, Python calls x.__add__(y). Your implementations should handle both Tensor-Tensor operations and Tensor-scalar operations, letting NumPyâs broadcasting do the heavy lifting.
Operation |
Method |
Example |
|---|---|---|
Addition |
|
|
Subtraction |
|
|
Multiplication |
|
|
Division |
|
|
Matrix & Shape Operations#
These methods transform tensors without changing their data (for views) or perform mathematical operations that produce new data (for matmul).
Method |
Signature |
Description |
|---|---|---|
|
|
Matrix multiplication |
|
|
Change shape (-1 to infer) |
|
|
Swap dimensions (defaults to last two) |
Reductions#
Reduction operations collapse one or more dimensions by aggregating values. The axis parameter controls which dimension gets collapsed. If axis=None, all dimensions collapse to a single scalar.
Method |
Signature |
Description |
|---|---|---|
|
|
Sum elements |
|
|
Average elements |
|
|
Maximum element |
Core Concepts#
This section covers the fundamental ideas you need to understand tensors deeply. These concepts apply to every ML framework, not just TinyTorch, so mastering them here will serve you throughout your career.
Tensor Dimensionality#
Tensors generalize the familiar concepts you already know. A scalar is just a single number, like a temperature reading of 72.5 degrees. Stack scalars into a list and you get a vector, like a series of temperature measurements throughout the day. Arrange vectors into rows and you get a matrix, like a spreadsheet where each row is a different dayâs measurements. Keep stacking and you reach 3D and 4D tensors that can represent video frames or collections of images.
The beauty of the Tensor abstraction is that your single class handles all of these cases. The same code that adds two scalars can add two 4D tensors, thanks to broadcasting.
Rank |
Name |
Shape |
Concrete Example |
|---|---|---|---|
0D |
Scalar |
|
Temperature reading: |
1D |
Vector |
|
Audio sample: 768 measurements |
2D |
Matrix |
|
Spreadsheet: 128 rows Ă 768 columns |
3D |
3D Tensor |
|
Video frames: 32 grayscale images |
4D |
4D Tensor |
|
Video frames: 32 color (RGB) images |
Broadcasting#
When you add a vector to a matrix, the shapes donât match. Should this fail? In most programming contexts, yes. But many computations need to apply the same operation across rows or columns. For example, if you want to adjust all values in a spreadsheet by adding a different offset to each column, you need to add a vector to a matrix. NumPy and PyTorch implement broadcasting to handle this: automatically expanding smaller tensors to match larger ones without actually copying data.
Consider adding a bias vector [10, 20, 30] to every row of a matrix. Without broadcasting, youâd need to manually tile the vector into a matrix first, wasting memory. With broadcasting, the operation just works, and the framework handles alignment internally.
Hereâs how your __add__ implementation handles this elegantly:
def __add__(self, other):
"""Add two tensors element-wise with broadcasting support."""
if isinstance(other, Tensor):
return Tensor(self.data + other.data) # NumPy handles broadcasting!
else:
return Tensor(self.data + other) # Scalar broadcast
The elegance is that NumPyâs broadcasting rules apply automatically when you write self.data + other.data. NumPy aligns the shapes from right to left and expands dimensions where needed, all without copying data.
flowchart LR
subgraph "Broadcasting Example"
M["Matrix (2,3)<br/>[[1,2,3], [4,5,6]]"]
V["Vector (3,)<br/>[10,20,30]"]
R["Result (2,3)<br/>[[11,22,33], [14,25,36]]"]
end
M --> |"+"| R
V --> |"expands"| R
style M fill:#e1f5ff
style V fill:#fff3cd
style R fill:#d4edda
Fig. 5 Broadcasting Example#
The rules are simpler than they look. Compare shapes from right to left. At each position, dimensions are compatible if theyâre equal or if one of them is 1. Missing dimensions on the left are treated as 1. If any position fails this check, broadcasting fails.
Shape A |
Shape B |
Result |
Valid? |
|---|---|---|---|
|
|
|
â |
|
|
|
â |
|
|
Error |
â (3 â 4) |
|
|
|
â |
The memory savings are dramatic. Adding a (768,) vector to a (32, 512, 768) tensor would require copying the vector 32Ă512 times without broadcasting, allocating 50 MB of redundant data (12.5 million float32 numbers). With broadcasting, you store just the original 3 KB vector.
Views vs. Copies#
When you reshape a tensor, does it allocate new memory or just create a different view of the same data? The answer has huge implications for both performance and correctness.
A view shares memory with its source. Reshaping a 1 GB tensor is instant because youâre just changing the metadata that describes how to interpret the bytes, not copying the bytes themselves. But this creates an important gotcha: modifying a view modifies the original.
x = Tensor([1, 2, 3, 4])
y = x.reshape(2, 2) # y is a VIEW of x
y.data[0, 0] = 99 # This also changes x!
Arithmetic operations like addition always create copies because they compute new values. This is safer but uses more memory. Production code carefully manages views to avoid both memory blowup (too many copies) and silent bugs (unexpected mutations through views).
Operation |
Type |
Memory |
Time |
|---|---|---|---|
|
View* |
Shared |
O(1) |
|
View* |
Shared |
O(1) |
|
Copy |
New allocation |
O(n) |
*When data is contiguous in memory
Matrix Multiplication#
Matrix multiplication is the computational workhorse of neural networks. Every linear layer, every attention head, every embedding lookup involves matmul. Understanding its mechanics and cost is essential.
The operation is simple in concept: for each output element, compute a dot product of a row from the first matrix with a column from the second. But this simplicity hides cubic complexity. Multiplying two nĂn matrices requires nÂł multiplications and nÂł additions.
Hereâs how the educational implementation in your module works:
def matmul(self, other):
"""Matrix multiplication of two tensors."""
if not isinstance(other, Tensor):
raise TypeError(f"Expected Tensor for matrix multiplication, got {type(other)}")
# Shape validation: inner dimensions must match
if len(self.shape) >= 2 and len(other.shape) >= 2:
if self.shape[-1] != other.shape[-2]:
raise ValueError(
f"Cannot perform matrix multiplication: {self.shape} @ {other.shape}. "
f"Inner dimensions must match: {self.shape[-1]} â {other.shape[-2]}"
)
a = self.data
b = other.data
# Handle 2D matrices with explicit loops (educational)
if len(a.shape) == 2 and len(b.shape) == 2:
M, K = a.shape
K2, N = b.shape
result_data = np.zeros((M, N), dtype=a.dtype)
# Each output element is a dot product
for i in range(M):
for j in range(N):
result_data[i, j] = np.dot(a[i, :], b[:, j])
else:
# For batched operations, use np.matmul
result_data = np.matmul(a, b)
return Tensor(result_data)
The explicit loops in the 2D case are intentionally slower than np.matmul because they reveal exactly what matrix multiplication does: each output element requires K operations, and there are MĂN outputs, giving O(MĂKĂN) total operations. For square matrices, this is O(nÂł).
Shape Manipulation#
Shape manipulation operations change how data is interpreted without changing the values themselves. Understanding when data is copied versus viewed is crucial for both correctness and performance.
The reshape method reinterprets the same data with different dimensions:
def reshape(self, *shape):
"""Reshape tensor to new dimensions."""
if len(shape) == 1 and isinstance(shape[0], (tuple, list)):
new_shape = tuple(shape[0])
else:
new_shape = shape
# Handle -1 for automatic dimension inference
if -1 in new_shape:
known_size = 1
unknown_idx = new_shape.index(-1)
for i, dim in enumerate(new_shape):
if i != unknown_idx:
known_size *= dim
unknown_dim = self.size // known_size
new_shape = list(new_shape)
new_shape[unknown_idx] = unknown_dim
new_shape = tuple(new_shape)
# Validate total elements match
if np.prod(new_shape) != self.size:
raise ValueError(
f"Total elements must match: {self.size} â {int(np.prod(new_shape))}"
)
reshaped_data = np.reshape(self.data, new_shape)
return Tensor(reshaped_data, requires_grad=self.requires_grad)
The -1 syntax is particularly useful: it tells NumPy to infer one dimension automatically. When flattening a batch of images, x.reshape(batch_size, -1) lets NumPy calculate the feature dimension.
Computational Complexity#
Not all tensor operations are equal. Element-wise operations like addition visit each element once: O(n) time where n is the total number of elements. Reductions like sum also visit each element once. But matrix multiplication is fundamentally different.
Multiplying two nĂn matrices requires nÂł operations: for each of the nÂČ output elements, you compute a dot product of n values. This cubic scaling is why a 2000Ă2000 matmul takes 8x longer than a 1000Ă1000 matmul, not 4x. In neural networks, matrix multiplications consume over 90% of compute time. This is precisely why GPUs exist for ML: a modern GPU has thousands of cores that can compute thousands of dot products simultaneously, turning an 800ms CPU operation into an 8ms GPU operation.
Operation |
Complexity |
Notes |
|---|---|---|
Element-wise ( |
O(n) |
Linear in tensor size |
Reductions ( |
O(n) |
Must visit every element |
Matrix multiply ( |
O(nÂł) |
Dominates training time |
Axis Semantics#
The axis parameter in reductions specifies which dimension to collapse. Think of it as âsum along this axisâ or âaverage out this dimension.â The result has one fewer dimension than the input.
For a 2D tensor with shape (rows, columns), summing along axis 0 collapses the rows, giving you column totals. Summing along axis 1 collapses the columns, giving you row totals. Summing with axis=None collapses everything to a single scalar.
Your reduction implementations simply pass the axis to NumPy:
def sum(self, axis=None, keepdims=False):
"""Sum tensor along specified axis."""
result = np.sum(self.data, axis=axis, keepdims=keepdims)
return Tensor(result)
The keepdims=True option preserves the reduced dimension as size 1, which is useful for broadcasting the result back.
For shape (rows, columns) = (32, 768):
sum(axis=0) â collapse rows â shape (768,) - column totals
sum(axis=1) â collapse columns â shape (32,) - row totals
sum(axis=None) â collapse all â scalar
Visual:
[[1, 2, 3], sum(axis=0) sum(axis=1)
[4, 5, 6]] â [5, 7, 9] or [6, 15]
(down cols) (across rows)
Architecture#
Your Tensor sits at the top of a stack that reaches down to hardware. When you call x + y, Python calls your __add__ method, which delegates to NumPy, which calls optimized BLAS libraries written in C and Fortran, which use CPU SIMD instructions that process multiple numbers in a single clock cycle.
flowchart TB
subgraph "Your Code"
A["Python Interface<br/>x = Tensor([[1,2],[3,4]])"]
end
subgraph "TinyTorch"
B["Tensor Class<br/>shape, data, operations"]
end
subgraph "Backend"
C["NumPy<br/>Vectorized operations"]
D["BLAS/LAPACK<br/>C/Fortran libraries"]
end
subgraph "Hardware"
E["CPU SIMD<br/>Cache optimization"]
end
A --> B --> C --> D --> E
style A fill:#e1f5ff
style B fill:#fff3cd
style C fill:#d4edda
style E fill:#f8d7da
Fig. 6 Your Code#
This is the same architecture used by PyTorch and TensorFlow, just with different backends. PyTorch replaces NumPy with a C++ engine and BLAS with CUDA kernels running on GPUs. But the Python interface and the abstractions are identical. When you understand TinyTorchâs Tensor, you understand PyTorchâs Tensor.
Module Integration#
Your Tensor is the foundation for everything that follows in TinyTorch. Every subsequent module builds on the work you do here.
Module 01: Tensor (THIS MODULE)
â provides foundation
Module 02: Activations (ReLU, Sigmoid operate on Tensors)
â uses tensors
Module 03: Layers (Linear, Conv2d store weights as Tensors)
â uses tensors
Module 05: Autograd (adds .grad attribute to Tensors)
â enhances tensors
Module 06: Optimizers (updates Tensor parameters)
Common Errors & Debugging#
These are the errors youâll encounter most often when working with tensors. Understanding why they happen will save you hours of debugging, both in this module and throughout your ML career.
Shape Mismatch in matmul#
Error: ValueError: shapes (2,3) and (2,2) not aligned
Matrix multiplication requires the inner dimensions to match. If youâre multiplying (M, K) by (K, N), both K values must be equal. The error above happens when trying to multiply a (2,3) matrix by a (2,2) matrix: 3 â 2.
Fix: Check your shapes. The rule is a.shape[-1] must equal b.shape[-2].
Broadcasting Failures#
Error: ValueError: operands could not be broadcast together
Broadcasting fails when shapes canât be aligned according to the rules. Remember: compare right to left, and dimensions must be equal or one must be 1.
Examples:
(2,3) + (3,)â works - 3 matches 3, and the missing dimension becomes 1(2,3) + (2,)â fails - comparing right to left: 3 â 2
Fix: Reshape to make dimensions compatible: vector.reshape(-1, 1) or vector.reshape(1, -1)
Reshape Size Mismatch#
Error: ValueError: cannot reshape array of size X into shape Y
Reshape only rearranges elements; it canât create or destroy them. If you have 12 elements, you can reshape to (3, 4) or (2, 6) or (2, 2, 3), but not to (5, 5).
Fix: Ensure np.prod(old_shape) == np.prod(new_shape)
Missing Attributes#
Error: AttributeError: 'Tensor' has no attribute 'shape'
Your __init__ method needs to set all required attributes. If you forget to set self.shape, any code that accesses tensor.shape will fail.
Fix: Add self.shape = self.data.shape in your constructor
Type Errors in Arithmetic#
Error: TypeError: unsupported operand type(s) for +: 'Tensor' and 'int'
Your arithmetic methods need to handle both Tensor and scalar operands. When someone writes x + 2, your __add__ receives the integer 2, not a Tensor.
Fix: Check for scalars: if isinstance(other, (int, float)): ...
Production Context#
Your Implementation vs. PyTorch#
Your TinyTorch Tensor and PyTorchâs torch.Tensor share the same conceptual design. The differences are in implementation: PyTorch uses a C++ backend for speed, supports GPUs for massive parallelism, and implements thousands of specialized operations. But the Python API, broadcasting rules, and shape semantics are identical.
Feature |
Your Implementation |
PyTorch |
|---|---|---|
Backend |
NumPy (Python) |
C++/CUDA |
Speed |
1x (baseline) |
10-100x faster |
GPU |
â CPU only |
â CUDA, Metal, ROCm |
Autograd |
Dormant (Module 05) |
Full computation graph |
Operations |
~15 core ops |
2000+ operations |
Code Comparison#
The following comparison shows equivalent operations in TinyTorch and PyTorch. Notice how closely the APIs mirror each other. This is intentional: by learning TinyTorchâs patterns, youâre simultaneously learning PyTorchâs patterns.
from tinytorch.core.tensor import Tensor
x = Tensor([[1, 2], [3, 4]])
y = x + 2
z = x.matmul(w)
loss = z.mean()
import torch
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
y = x + 2
z = x @ w
loss = z.mean()
loss.backward() # You'll build this in Module 05!
Letâs walk through each line to understand the comparison:
Line 1 (Import): Both frameworks use a simple import. TinyTorch exposes
Tensorfromcore.tensor; PyTorch usestorch.tensor()as a factory function.Line 3 (Creation): TinyTorch infers dtype from input; PyTorch requires explicit
dtype=torch.float32for floating-point operations. This explicitness matters for performance tuning in production.Line 4 (Broadcasting): Both handle
x + 2identically, broadcasting the scalar across all elements. Same semantics, same result.Line 5 (Matrix multiply): TinyTorch uses
.matmul()method; PyTorch supports both.matmul()and the@operator. The operation is identical.Line 6 (Reduction): Both use
.mean()to reduce the tensor to a scalar. Reductions like this are fundamental to computing loss values.Line 7 (Autograd): PyTorch includes
.backward()for automatic differentiation. Youâll implement this yourself in Module 05, gaining deep insight into how gradients flow through computation graphs.
Tip
Whatâs Identical
Broadcasting rules, shape semantics, and API design patterns. When you debug PyTorch shape errors, youâll understand exactly whatâs happening because you built the same abstractions.
Why Tensors Matter at Scale#
To appreciate why tensor operations matter, consider the scale of modern ML systems:
Large language models: 175 billion numbers stored as tensors = 350 GB (like storing 70,000 full-resolution photos)
Image processing: A batch of 128 images = 77 MB of tensor data
Self-driving cars: Process tensor operations at 36 FPS across multiple cameras (each frame = millions of operations in 28 milliseconds)
A single matrix multiplication can consume 90% of computation time in neural networks. Understanding tensor operations isnât just academic; itâs essential for building and debugging real ML systems.
Check Your Understanding#
Test yourself with these systems thinking questions. Theyâre designed to build intuition for the performance characteristics youâll encounter in production ML.
Q1: Memory Calculation
A batch of 32 RGB images (224Ă224 pixels) stored as float32. How much memory?
Answer
32 Ă 3 Ă 224 Ă 224 Ă 4 = 77,070,336 bytes â 77 MB
This is why batch size matters - double the batch, double the memory!
Q2: Broadcasting Savings
Adding a vector (768,) to a 3D tensor (32, 512, 768). How much memory does broadcasting save?
Answer
Without broadcasting: 32 Ă 512 Ă 768 Ă 4 = 50.3 MB
With broadcasting: 768 Ă 4 = 3 KB
Savings: ~50 MB per operation - this adds up across hundreds of operations in a neural network!
Q3: Matmul Scaling
If a 1000Ă1000 matmul takes 100ms, how long will 2000Ă2000 take?
Answer
Matmul is O(nÂł). Doubling n â 2Âł = 8x longer â ~800ms
This is why matrix size matters so much for transformer scaling!
Q4: Shape Prediction
Whatâs the output shape of (32, 1, 768) + (512, 768)?
Answer
Broadcasting aligns right-to-left:
(32, 1, 768)( 512, 768)
Result: (32, 512, 768)
The 1 broadcasts to 512, and 32 is prepended.
Q5: Views vs Copies
You reshape a 1GB tensor, then modify one element in the reshaped version. What happens to the original tensor? What if you had used x + 0 instead of reshape?
Answer
Reshape (view): The original tensor IS modified. Reshape creates a view that shares memory with the original. Changing y.data[0,0] = 99 also changes x.data[0].
Addition (copy): The original tensor is NOT modified. x + 0 creates a new tensor with freshly allocated memory. The values are identical but stored in different locations.
This distinction matters enormously for:
Memory: Views use 0 extra bytes; copies use n extra bytes
Performance: Views are O(1); copies are O(n)
Correctness: Unexpected mutations through views are a common source of bugs
Further Reading#
For students who want to understand the academic foundations and mathematical underpinnings of tensor operations:
Seminal Papers#
NumPy: Array Programming - Harris et al. (2020). The definitive reference for NumPy, which underlies your Tensor implementation. Explains broadcasting, views, and the design philosophy. Nature
BLAS (Basic Linear Algebra Subprograms) - Lawson et al. (1979). The foundation of all high-performance matrix operations. Your
np.matmulultimately calls BLAS routines optimized over 40+ years. Understanding BLAS levels (1, 2, 3) explains why matmul is special. ACM TOMSAutomatic Differentiation in ML - Baydin et al. (2018). Survey of automatic differentiation techniques that will become relevant in Module 05. JMLR
Additional Resources#
Textbook: âDeep Learningâ by Goodfellow, Bengio, and Courville - Chapter 2 covers linear algebra foundations including tensor operations
Documentation: PyTorch Tensor Tutorial - See how production frameworks implement similar concepts
Whatâs Next#
See also
Coming Up: Module 02 - Activations
Implement ReLU, Sigmoid, Tanh, and Softmax. Youâll apply element-wise operations to your Tensor and learn why these functions are essential for neural networks to learn complex patterns.
Preview - How Your Tensor Gets Used in Future Modules:
Module |
What It Does |
Your Tensor In Action |
|---|---|---|
02: Activations |
Element-wise functions |
|
03: Layers |
Neural network building blocks |
|
05: Autograd |
Automatic gradients |
|
Get Started#
Tip
Interactive Options
Launch Binder - Run interactively in browser, no setup required
Open in Colab - Use Google Colab for cloud compute
View Source - Browse the implementation code
Warning
Save Your Progress
Binder and Colab sessions are temporary. Download your completed notebook when done, or clone the repository for persistent local work.