Welcome to Part 1.8 of Artintellica’s open-source RL with PyTorch course! So far, you’ve mastered the geometry of tensors—norms, distances, and projections. Now, you’ll see how matrices actually transform data. This is the essence of linear algebra in ML: weights in neural networks, value function features, even environment state transitions are, at their core, linear transformations.
In this post, you’ll:
Let’s turn math into moving pictures!
A linear transformation can be represented by a matrix . Given a vector , the transformation maps it to :
The most common 2D transformations are:
To rotate a vector by angle counterclockwise about the origin:
To scale by in direction and in :
Chaining means applying transformations with (matrix multiplication order: right-to-left).
Let’s implement and visualize these concepts in PyTorch.
import torch
import math
def rotation_matrix(theta: float) -> torch.Tensor:
return torch.tensor([
[math.cos(theta), -math.sin(theta)],
[math.sin(theta), math.cos(theta)]
], dtype=torch.float32)
def scaling_matrix(sx: float, sy: float) -> torch.Tensor:
return torch.tensor([
[sx, 0.0],
[0.0, sy]
], dtype=torch.float32)
theta = math.pi / 4 # 45 degrees
R = rotation_matrix(theta)
S = scaling_matrix(2.0, 0.5)
print("Rotation (45°):\n", R)
print("Scaling (2, 0.5):\n", S)
Let’s rotate some points.
# Array of 2D points (shape Nx2)
points: torch.Tensor = torch.tensor([
[1.0, 0.0],
[0.0, 1.0],
[-1.0, 0.0],
[0.0, -1.0]
])
theta = math.pi / 2 # 90 degrees
R = rotation_matrix(theta) # Counterclockwise
rotated: torch.Tensor = points @ R.T
print("Original points:\n", points)
print("Rotated points (90°):\n", rotated)
Note: We use @ R.T
because points are (N,2) * (2,2) → (N,2).
Let’s rotate and scale a square, and plot before and after.
import matplotlib.pyplot as plt
# Define points: corners of the square (counterclockwise)
square: torch.Tensor = torch.tensor([
[1.0, 1.0],
[-1.0, 1.0],
[-1.0, -1.0],
[1.0, -1.0],
[1.0, 1.0] # close the square for the plot
])
# Transformation: rotate by 30° and scale x2 in x, 0.5 in y
theta = math.radians(30)
R = rotation_matrix(theta)
S = scaling_matrix(2.0, 0.5)
# Apply scaling THEN rotation
transformed: torch.Tensor = (square @ S.T) @ R.T
plt.figure(figsize=(6,6))
plt.plot(square[:,0], square[:,1], 'bo-', label='Original')
plt.plot(transformed[:,0], transformed[:,1], 'ro-', label='Transformed')
plt.title("Transforming a Square: Rotation and Scaling")
plt.xlabel("x")
plt.ylabel("y")
plt.axis("equal")
plt.grid(True)
plt.legend()
plt.show()
Let’s build the matrix for scaling-then-rotation, versus rotation-then-scaling (order matters!).
# Chain: first scaling, then rotation (note order!)
composite1 = R @ S
# Chain: first rotation, then scaling
composite2 = S @ R
point = torch.tensor([[1.0, 0.0]])
result1 = (point @ composite1.T).squeeze()
result2 = (point @ composite2.T).squeeze()
print("Scaling THEN rotation:", result1)
print("Rotation THEN scaling:", result2)
Describe the result: The order of operations changes the outcome! Try swapping R and S in the square plot as an experiment.
Try these in your own script or notebook!
import torch
import math
import matplotlib.pyplot as plt
def rotation_matrix(theta: float) -> torch.Tensor:
return torch.tensor([
[math.cos(theta), -math.sin(theta)],
[math.sin(theta), math.cos(theta)]
], dtype=torch.float32)
def scaling_matrix(sx: float, sy: float) -> torch.Tensor:
return torch.tensor([
[sx, 0.0],
[0.0, sy]
], dtype=torch.float32)
# EXERCISE 1
theta = math.radians(60)
R = rotation_matrix(theta)
S = scaling_matrix(2.0, 0.5)
print("Rotation 60°:\n", R)
print("Scaling x2, y0.5:\n", S)
# EXERCISE 2
pts = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
rot45 = rotation_matrix(math.radians(45))
rotated = pts @ rot45.T
print("Original points:\n", pts)
print("Rotated by 45°:\n", rotated)
# EXERCISE 3
square = torch.tensor([[1, 1], [-1, 1], [-1, -1], [1, -1], [1, 1]], dtype=torch.float32)
S2 = scaling_matrix(1.5, 0.5)
scaled_sq = square @ S2.T
plt.figure(figsize=(5,5))
plt.plot(square[:,0], square[:,1], 'b-o', label='Original Square')
plt.plot(scaled_sq[:,0], scaled_sq[:,1], 'r-o', label='Scaled Square')
plt.title("Scaling a Square")
plt.xlabel('x'); plt.ylabel('y'); plt.axis('equal'); plt.grid(True); plt.legend(); plt.show()
# EXERCISE 4
R90 = rotation_matrix(math.radians(90))
S3 = scaling_matrix(0.5, 2.0)
# rotate then scale
sq_rot_scale = (square @ R90.T) @ S3.T
# scale then rotate
sq_scale_rot = (square @ S3.T) @ R90.T
plt.figure(figsize=(6,6))
plt.plot(square[:,0], square[:,1], 'b--o', label='Original')
plt.plot(sq_rot_scale[:,0], sq_rot_scale[:,1], 'r-o', label='Rotate->Scale')
plt.plot(sq_scale_rot[:,0], sq_scale_rot[:,1], 'g-o', label='Scale->Rotate')
plt.title("Chained Transformations on a Square")
plt.xlabel('x'); plt.ylabel('y'); plt.axis('equal'); plt.grid(True); plt.legend(); plt.show()
# Describe your observations!
Today, you visualized and coded the true power of matrices in ML: they transform data! You learned how to:
Next: We’ll use your knowledge to load, manipulate, and visualize real/simulated data using the full PyTorch matrix toolbox. You’ll be ready for data prep, debugging, and even RL environments!
Practice these shape-changing operations—they’ll be everywhere in RL and deep learning! See you in Part 1.9.