Tensor Surgery & Assembly¶
Module 1 | Lesson 2a
Professor Torchenstein's Grand Directive¶
Mwahahaha! You have summoned your first tensors from the ether! They are... raw. Untamed. Clumps of numerical clay awaiting a master's touch. A lesser mind would be content with their existence, but not you. Not us!
Today, we become tensor surgeons! We will dissect tensors with the precision of a master anatomist, join them with the skill of a mad scientist, and divide them like a seasoned alchemist splitting compounds. This is not mere data processing; this is tensor surgery and assembly! Prepare to wield your digital scalpel and fusion apparatus!
Your Mission Briefing¶
By the time you escape this surgical theater, you will have mastered the fundamental arts of tensor manipulation:
- 🔪 The Art of Selection: Pluck elements, rows, or slices from a tensor with surgical slicing precision.
- 🧬 Forbidden Fusions: Combine disparate tensors into unified monstrosities with
torch.cat
andtorch.stack
. - ✂️ The Great Division: Split larger tensors into manageable pieces using
torch.split
andtorch.chunk
.
Estimated Time to Completion: 20 minutes of surgical tensor mastery.
What You'll Need:
- The wisdom from our last lesson on summoning tensors.
- A steady hand for precision cuts and fusions!
- Your PyTorch environment, humming with anticipation.
Coming Next: In lesson 2b, you'll learn the metamorphic arts of reshaping, squeezing, and permuting tensors to transform their very essence!
Part 1: The Art of Selection - Slicing¶
Before you can reshape a tensor, you must learn to grasp its individual parts. Indexing is your scalpel, allowing you to perform precision surgery on your data. Slicing is your cleaver, letting you carve out whole sections for your grand experiments.
We will start by summoning a test subject—a 2D tensor brimming with potential! We must also prepare our lab with the usual incantations (import torch
and manual_seed
) to ensure our results are repeatable. We are scientists, not chaos-wizards!
import torch
# Set the seed for cosmic consistency
torch.manual_seed(42)
# Our test subject: A 2D tensor of integers. Imagine it's a map to a hidden treasure!
# Or perhaps experimental results from a daring new potion.
subject_tensor = torch.randint(0, 100, (5, 4))
print(f"Our subject tensor of shape {subject_tensor.shape}, ripe for dissection:")
print(subject_tensor)
Our subject tensor of shape torch.Size([5, 4]), ripe for dissection: tensor([[42, 67, 76, 14], [26, 35, 20, 24], [50, 13, 78, 14], [10, 54, 31, 72], [15, 95, 67, 6]])
Sweeping Strikes: Accessing Rows and Columns¶
Previous lesson: 01_introduction_to_tensors.ipynb gives you the basics for accessing element of a tensor.
But what if we require an entire row or column for our dark machinations? For this, we use the colon :
, the universal symbol for "give me everything along this dimension!"
[row_index, :]
- Fetches the entire row.[:, column_index]
- Fetches the entire column.
Let's seize the entire 3rd row (index 2) and the 2nd column (index 1).
# Get the entire 3rd row (index 2)
third_row = subject_tensor[2, :] # or simply subject_tensor[2]
print(f"The third row: {third_row}")
print(f"Shape of the row: {third_row.shape}\n")
# Get the entire 2nd column (index 1)
second_column = subject_tensor[:, 1]
print(f"The second column: {second_column}")
print(f"Shape of the column: {second_column.shape}")
The third row: tensor([50, 13, 78, 14]) Shape of the row: torch.Size([4]) The second column: tensor([67, 35, 13, 54, 95]) Shape of the column: torch.Size([5])
Carving Chunks: The Power of Slicing¶
Mere elements are but trivialities! True power lies in carving out entire sub-regions of a tensor. Slicing uses the start:end
notation. As with all Pythonic sorcery, the start
is inclusive, but the end
is exclusive.
Let us carve out the block containing the 2nd and 3rd rows (indices 1 and 2), and the last two columns (indices 2 and 3).
# Carve out rows 1 and 2, and columns 2 and 3
sub_tensor = subject_tensor[1:3, 2:4]
print("Our carved sub-tensor:")
print(sub_tensor)
print(f"Shape of the sub-tensor: {sub_tensor.shape}")
Our carved sub-tensor: tensor([[20, 24], [78, 14]]) Shape of the sub-tensor: torch.Size([2, 2])
Conditional Conjuring: Boolean Mask Indexing¶
Now for a truly diabolical technique! We can use a boolean mask to summon only the elements that meet our nefarious criteria. A boolean mask is a tensor of the same shape as our subject, but it contains only True
or False
values. When used for indexing, it returns a 1D tensor containing only the elements where the mask was True
.
Let's find all the alchemical ingredients in our tensor with a value greater than 50!
# Create the boolean mask
mask = subject_tensor > 50
print("The boolean mask (True where value > 50):")
print(mask)
# Apply the mask
selected_elements = subject_tensor[mask]
print("\nElements greater than 50:")
print(selected_elements)
print(f"Shape of the result: {selected_elements.shape} (always a 1D tensor!)")
# You can also combine conditions! Mwahaha!
# Let's find elements between 20 and 40.
mask_combined = (subject_tensor > 20) & (subject_tensor < 40)
print("\nElements between 20 and 40:")
print(subject_tensor[mask_combined])
The boolean mask (True where value > 50): tensor([[False, True, True, False], [False, False, False, False], [False, False, True, False], [False, True, False, True], [False, True, True, False]]) Elements greater than 50: tensor([67, 76, 78, 54, 72, 95, 67]) Shape of the result: torch.Size([7]) (always a 1D tensor!) Elements between 20 and 40: tensor([26, 35, 24, 31])
Your Mission: The Slicer's Gauntlet¶
Enough of my demonstrations! The scalpel is now in your hand. Prove your mastery with these challenges!
- The Corner Pocket: From our
subject_tensor
, select the element in the very last row and last column. - The Central Core: Select the inner
3x2
block of thesubject_tensor
(that's rows 1-3 and columns 1-2). - The Even Stevens: Create a boolean mask to select only the elements in
subject_tensor
that are even numbers. (Hint: The modulo operator%
is your friend!) - The Grand Mutation: Use your boolean mask from challenge 3 to change all even numbers in the
subject_tensor
to the value-1
. Then, print the mutated tensor. Yes, my apprentice, indexing can be used for assignment! This is a pivotal secret!
# Your code for the Slicer's Gauntlet goes here!
# --- 1. The Corner Pocket ---
print("--- 1. The Corner Pocket ---")
corner_element = subject_tensor[-1, -1] # Negative indexing for the win!
print(f"The corner element is: {corner_element.item()}\n")
# --- 2. The Central Core ---
print("--- 2. The Central Core ---")
central_core = subject_tensor[1:4, 1:3]
print(f"The central core:\\n{central_core}\n")
# --- 3. The Even Stevens ---
print("--- 3. The Even Stevens ---")
even_mask = subject_tensor % 2 == 0
print(f"The mask for even numbers:\\n{even_mask}\n")
print(f"The even numbers themselves: {subject_tensor[even_mask]}\n")
# --- 4. The Grand Mutation ---
print("--- 4. The Grand Mutation ---")
# Let's not mutate our original, that would be reckless! Let's clone it first.
mutated_tensor = subject_tensor.clone()
mutated_tensor[even_mask] = -1
print(f"The tensor after mutating even numbers to -1:\n{mutated_tensor}")
--- 1. The Corner Pocket --- The corner element is: 6 --- 2. The Central Core --- The central core:\ntensor([[35, 20], [13, 78], [54, 31]]) --- 3. The Even Stevens --- The mask for even numbers:\ntensor([[ True, False, True, True], [ True, False, True, True], [ True, False, True, True], [ True, True, False, True], [False, False, False, True]]) The even numbers themselves: tensor([42, 76, 14, 26, 20, 24, 50, 78, 14, 10, 54, 72, 6]) --- 4. The Grand Mutation --- The tensor after mutating even numbers to -1: tensor([[-1, 67, -1, -1], [-1, 35, -1, -1], [-1, 13, -1, -1], [-1, -1, 31, -1], [15, 95, 67, -1]])
Part 2: Forbidden Fusions - Joining Tensors¶
Ah, but dissecting tensors is only half the art! A true master must also know how to fuse separate tensors into a single, magnificent whole. Sometimes your data comes in fragments—perhaps different batches, different features, or different time steps. You must unite them!
We have two primary spells for this dark ritual:
torch.cat()
- The Concatenator! Joins tensors along an existing dimension.torch.stack()
- The Stacker! Creates a new dimension and stacks tensors along it.
The difference is subtle but critical. Choose wrongly, and your creation will crumble! Let us forge some test subjects to demonstrate this power.
# Three 2x3 tensors, our loyal minions awaiting fusion
tensor_a = torch.ones(2, 4)
tensor_b = torch.ones(2, 4) * 2
tensor_c = torch.ones(2, 4) * 3
print("Our test subjects, ready for fusion:")
print(f"Tensor A (shape {tensor_a.shape}):\n{tensor_a}\n")
print(f"Tensor B (shape {tensor_b.shape}):\n{tensor_b}\n")
print(f"Tensor C (shape {tensor_c.shape}):\n{tensor_c}\n")
Our test subjects, ready for fusion: Tensor A (shape torch.Size([2, 4])): tensor([[1., 1., 1., 1.], [1., 1., 1., 1.]]) Tensor B (shape torch.Size([2, 4])): tensor([[2., 2., 2., 2.], [2., 2., 2., 2.]]) Tensor C (shape torch.Size([2, 4])): tensor([[3., 3., 3., 3.], [3., 3., 3., 3.]])
The Concatenator: torch.cat()
¶
torch.cat()
joins tensors along an existing dimension. Think of it as gluing them end-to-end.
The key rule: All tensors must have the same shape, except along the dimension you're concatenating!
dim=0
(oraxis=0
): Concatenate along rows (vertically stack)dim=1
(oraxis=1
): Concatenate along columns (horizontally join)
Let us witness this concatenation sorcery!
# Concatenating along dimension 0 (rows) - like stacking pancakes! 🥞⬆️⬇️
cat_dim0 = torch.cat([tensor_a, tensor_b, tensor_c], dim=0)
print("Concatenated along dimension 0 (rows) [stacking pancakes 🥞⬆️⬇️]:")
print(f"Result shape: {cat_dim0.shape}")
print(f"Result:\n{cat_dim0}\n")
# Concatenating along dimension 1 (columns) - like laying bricks side by side! 🧱🧱🧱
cat_dim1 = torch.cat([tensor_a, tensor_b, tensor_c], dim=1)
print("Concatenated along dimension 1 (columns) [laying bricks side by side 🧱🧱🧱]:")
print(f"Result shape: {cat_dim1.shape}")
print(f"Result:\n{cat_dim1}")
Concatenated along dimension 0 (rows) [stacking pancakes 🥞⬆️⬇️]: Result shape: torch.Size([6, 4]) Result: tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [2., 2., 2., 2.], [2., 2., 2., 2.], [3., 3., 3., 3.], [3., 3., 3., 3.]]) Concatenated along dimension 1 (columns) [laying bricks side by side 🧱🧱🧱]: Result shape: torch.Size([2, 12]) Result: tensor([[1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.], [1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.]])
The Concatenation Rules: When Shapes Don't Match¶
Now, let us test the fundamental law of concatenation with unequal tensors! Remember: All tensors must have the same shape, except along the dimension you're concatenating.
Eg1. If you joining 2D matrices along rows (dim=0) the number of collumns should be the same.
Let's create two tensors with different shapes and see what happens:
# Create tensors with different shapes
tensor_wide = torch.ones(3, 8) * 4 # 3x5 tensor filled with 4s
tensor_narrow = torch.ones(3, 2) * 5 # 3x2 tensor filled with 5s
print(f"Wide tensor [big cake 🎂] (shape {tensor_wide.shape}):\n{tensor_wide}\n")
print(f"Narrow tensor [small cupcake 🧁 ] (shape {tensor_narrow.shape}):\n{tensor_narrow}\n")
# This FAILS: Concatenating along dimension 0, you can stack pancakes with different sizes
# They have different column counts (5 vs 2)
print("❌ Attempting to concatenate along dimension 0 (rows), stack cake on top of cupcake ...")
try:
cat_cols_fail = torch.cat([tensor_wide, tensor_narrow], dim=0)
except RuntimeError as e:
print(f"🎂/🧁 This couldn't work! \nError as expected: {str(e)}")
print("Our unequal test subjects:")
print(f"Wide tensor [big cake 🎂] ({tensor_wide.shape}):\n{tensor_wide}\n")
print(f"Narrow tensor [small cupcake 🧁] ({tensor_narrow.shape}):\n{tensor_narrow}\n")
# This WORKS: Concatenating along dimension 1 (columns)
# Both have 3 rows, so we can lay them side by side horizontally
print("✅ Concatenating along dimension 1 (columns) - SUCCESS!")
cat_rows_success = torch.cat([tensor_wide, tensor_narrow], dim=1)
print(f"Result shape: {cat_rows_success.shape}")
print(f"Result:\n{cat_rows_success}\n")
Wide tensor [big cake 🎂] (shape torch.Size([3, 8])): tensor([[4., 4., 4., 4., 4., 4., 4., 4.], [4., 4., 4., 4., 4., 4., 4., 4.], [4., 4., 4., 4., 4., 4., 4., 4.]]) Narrow tensor [small cupcake 🧁 ] (shape torch.Size([3, 2])): tensor([[5., 5.], [5., 5.], [5., 5.]]) ❌ Attempting to concatenate along dimension 0 (rows), stack cake on top of cupcake ... 🎂/🧁 This couldn't work! Error as expected: Sizes of tensors must match except in dimension 0. Expected size 8 but got size 2 for tensor number 1 in the list. Our unequal test subjects: Wide tensor [big cake 🎂] (torch.Size([3, 8])): tensor([[4., 4., 4., 4., 4., 4., 4., 4.], [4., 4., 4., 4., 4., 4., 4., 4.], [4., 4., 4., 4., 4., 4., 4., 4.]]) Narrow tensor [small cupcake 🧁] (torch.Size([3, 2])): tensor([[5., 5.], [5., 5.], [5., 5.]]) ✅ Concatenating along dimension 1 (columns) - SUCCESS! Result shape: torch.Size([3, 10]) Result: tensor([[4., 4., 4., 4., 4., 4., 4., 4., 5., 5.], [4., 4., 4., 4., 4., 4., 4., 4., 5., 5.], [4., 4., 4., 4., 4., 4., 4., 4., 5., 5.]])
The Stacker: torch.stack()
- Creating New Dimensions!¶
torch.stack()
is more dramatic than concatenation! It creates an entirely new dimension and places each tensor along it. Think of it as the difference between:
- Concatenation: Gluing pieces end-to-end in the same plane 🧩➡️🧩
- Stacking: Creating a whole new layer/dimension 📚 (like stacking books on top of each other)
Critical Rule: All input tensors must have identical shapes—no exceptions!
Let's start simple and build our intuition step by step...
Step 1: Stacking 1D Tensors → Creating a 2D Matrix¶
Let's start with something simple: three 1D tensors (think of them as rulers 📏). When we stack them, we create a 2D matrix!
# Let's create simple 1D tensors first
ruler_1 = torch.tensor([1, 2, 3, 4]) # A ruler with numbers 1,2,3,4
ruler_2 = torch.tensor([10, 20, 30, 40]) # A ruler with numbers 10,20,30,40
ruler_3 = torch.tensor([100, 200, 300, 400]) # A ruler with numbers 100,200,300,400
print("Our three rulers 📏 (1D tensors):")
print(f"Ruler 1: {ruler_1} (shape: {ruler_1.shape})")
print(f"Ruler 2: {ruler_2} (shape: {ruler_2.shape})")
print(f"Ruler 3: {ruler_3} (shape: {ruler_3.shape})\n")
# Stack them to create a 2D matrix (like putting rulers on top of each other)
stacked_rulers = torch.stack([ruler_1, ruler_2, ruler_3], dim=0)
print("Stacked rulers 🟰 (dim=0) - like placing rulers on top of each other:")
print(f"Result shape: {stacked_rulers.shape}") # Notice: (3,4) - we added a new dimension!
print(f"Result:\n{stacked_rulers}\n")
# Each "ruler" is now accessible as a row
print("Access individual rulers:")
print(f"First ruler: {stacked_rulers[0]}")
print(f"Second ruler: {stacked_rulers[1]}")
print(f"Third ruler: {stacked_rulers[2]}")
Our three rulers 📏 (1D tensors): Ruler 1: tensor([1, 2, 3, 4]) (shape: torch.Size([4])) Ruler 2: tensor([10, 20, 30, 40]) (shape: torch.Size([4])) Ruler 3: tensor([100, 200, 300, 400]) (shape: torch.Size([4])) Stacked rulers 🟰 (dim=0) - like placing rulers on top of each other: Result shape: torch.Size([3, 4]) Result: tensor([[ 1, 2, 3, 4], [ 10, 20, 30, 40], [100, 200, 300, 400]]) Access individual rulers: First ruler: tensor([1, 2, 3, 4]) Second ruler: tensor([10, 20, 30, 40]) Third ruler: tensor([100, 200, 300, 400])
# We can also stack along dimension 1 (different arrangement)
stack_dim1 = torch.stack([ruler_1, ruler_2, ruler_3], dim=1)
print("Stacked rulers ⏸️ (dim=1) - like arranging rulers side by side:")
print(f"Result shape: {stack_dim1.shape}") # Notice: (4,3) - different arrangement!
print(f"Result:\n{stack_dim1}\n")
# Each column now represents values from all three rulers at the same position
print("Notice the pattern:")
print("Each row shows the 1st, 2nd, 3rd... element from ALL rulers")
print(f"Position 0 from all rulers: {stack_dim1[0]}") # [1, 10, 100]
print(f"Position 1 from all rulers: {stack_dim1[1]}") # [2, 20, 200]
Stacked rulers ⏸️ (dim=1) - like arranging rulers side by side: Result shape: torch.Size([4, 3]) Result: tensor([[ 1, 10, 100], [ 2, 20, 200], [ 3, 30, 300], [ 4, 40, 400]]) Notice the pattern: Each row shows the 1st, 2nd, 3rd... element from ALL rulers Position 0 from all rulers: tensor([ 1, 10, 100]) Position 1 from all rulers: tensor([ 2, 20, 200])
Step 2: Stacking 2D Tensors → Creating a 3D Cube!¶
Now for the mind-bending part! When we stack 2D tensors (matrices), we create a 3D tensor. Think of it like:
📄 2D tensor = A page from a book (has rows and columns)
📖 Stacking 2D tensors = Creating a book with multiple pages
📚 3D tensor = The entire book! (pages × rows × columns)
Key Metaphors to Remember:
- Book metaphor:
tensor[page][row][column]
📚 - RGB image:
tensor[channel][height][width]
🖼️ (like stacking color layers: Red, Green, Blue)
These metaphors help you "see" how changing the dimension you stack along changes the meaning of each axis in your tensor. 🤓
Let's see this dimensional magic in action:
# Create three 2D "pages" for our book
page_1 = torch.ones(5, 2) * 1 # Page 1: all 1s
page_2 = torch.ones(5, 2) * 2 # Page 2: all 2s
page_3 = torch.ones(5, 2) * 3 # Page 3: all 3s
print("Our three pages (2D tensors):")
print(f"📄 Page 1 (shape {page_1.shape}):\n{page_1}\n")
print(f"📄 Page 2 (shape {page_2.shape}):\n{page_2}\n")
print(f"📄 Page 3 (shape {page_3.shape}):\n{page_3}\n")
# Stack them along dimension 0 to create a 3D "book"
book = torch.stack([page_1, page_2, page_3], dim=0)
print("📚 BEHOLD! Our 3D book (stacked along dim=0):")
print(f"Book shape: {book.shape}") # (3, 2, 3) = (pages, rows, columns)
print(f"Full book:\n{book}\n")
# Now we can access individual pages, rows, or even specific elements!
print("🔍 Accessing different parts of our 3D tensor:")
print(f"📖 Entire first page (book[0]):\n{book[0]}\n")
print(f"📝 First row of second page (book[1, 0]): {book[1, 0]}")
print(f"🎯 Specific element - page 2, row 1, column 2 (book[1, 0, 1]): {book[1, 0, 1].item()}")
print(f"\n🤔 Think of it as: Book[page_number][row_number][column_number]")
Our three pages (2D tensors): 📄 Page 1 (shape torch.Size([5, 2])): tensor([[1., 1.], [1., 1.], [1., 1.], [1., 1.], [1., 1.]]) 📄 Page 2 (shape torch.Size([5, 2])): tensor([[2., 2.], [2., 2.], [2., 2.], [2., 2.], [2., 2.]]) 📄 Page 3 (shape torch.Size([5, 2])): tensor([[3., 3.], [3., 3.], [3., 3.], [3., 3.], [3., 3.]]) 📚 BEHOLD! Our 3D book (stacked along dim=0): Book shape: torch.Size([3, 5, 2]) Full book: tensor([[[1., 1.], [1., 1.], [1., 1.], [1., 1.], [1., 1.]], [[2., 2.], [2., 2.], [2., 2.], [2., 2.], [2., 2.]], [[3., 3.], [3., 3.], [3., 3.], [3., 3.], [3., 3.]]]) 🔍 Accessing different parts of our 3D tensor: 📖 Entire first page (book[0]): tensor([[1., 1.], [1., 1.], [1., 1.], [1., 1.], [1., 1.]]) 📝 First row of second page (book[1, 0]): tensor([2., 2.]) 🎯 Specific element - page 2, row 1, column 2 (book[1, 0, 1]): 2.0 🤔 Think of it as: Book[page_number][row_number][column_number]
Step 3: The Dimension Dance - Where You Stack Matters!¶
When stacking 2D tensors, which dimension you choose creates very different 3D shapes!
🧱 The Clay Tablet Box Metaphor:
For better understanding, imagine that when you use the stack()
method, a new 3D package is created with an extended dimension. If you stack 2D objects (like clay tablets), you first create a 3D box, then arrange your 2D tablets inside.
Picture this: You have three identical clay tablets 🧱 and an empty 3D box 📦. There are exactly 3 different ways to arrange them inside!
📐 3D Box Coordinates (always viewed from the same angle):
- Depth =
dim=0
(front to back) - Height =
dim=1
(bottom to top) - Width =
dim=2
(left to right)
🎯 The Three Stacking Strategies:
dim=0
: Stack tablets front-to-back → Shape(tablets, rows, cols)
📚
Each tablet goes deeper into the box, one behind the otherdim=1
: Stack tablets bottom-to-top → Shape(rows, tablets, cols)
🗂️
Each tablet is placed higher in the box, building upward - we start with last tabletsdim=2
: Slide tablets left-to-right → Shape(rows, cols, tablets)
📑
Each tablet slides sideways, arranged side by side
The dimension you choose determines which direction your tablets extend in the 3D space!
# Let's arrange our three 2D clay tablets in three different ways!
print("🧱 Starting with three identical clay tablets:")
print(f"Each tablet shape: {page_1.shape} (5 rows, 2 columns)")
# dim=0: Stack tablets front-to-back (into the box)
stack_dim0 = torch.stack([page_1, page_2, page_3], dim=0)
print(f"\n📚 Stacking front-to-back (dim=0): Shape {stack_dim0.shape}")
print(" (depth=3, heith=5, width=2) - tablets go deeper into the box")
print(f"Front of the box):\n{stack_dim0[0]}\n")
# dim=1: Stack tablets bottom-to-top (building upward)
stack_dim1 = torch.stack([page_1, page_2, page_3], dim=1)
print(f"🗂️ Stacking bottom-to-top (dim=1): Shape {stack_dim1.shape}")
print(" (depth=5, heigh=3, width=2) - tablets build upward")
print("Notice how each level contains one slice from ALL tablets:")
print(f"Front of the box):\n{stack_dim1[0]}\n")
# dim=2: Slide tablets left-to-right (arranging sideways)
stack_dim2 = torch.stack([page_1, page_2, page_3], dim=2)
print(f"📑 Sliding left-to-right (dim=2): Shape {stack_dim2.shape}")
print(" (depth=5, heigh=2, width=3) - tablets slide sideways")
print("Each position now contains values from ALL tablets:")
print(f"Front of the box):\n{stack_dim2[0]}\n")
print("\n🎯 Key Insight: The dimension you choose determines WHERE the tablets extend!")
print(" dim=0: Tablets extend front-to-back (depth)")
print(" dim=1: Tablets extend bottom-to-top (height)")
print(" dim=2: Tablets extend left-to-right (width)")
🧱 Starting with three identical clay tablets: Each tablet shape: torch.Size([5, 2]) (5 rows, 2 columns) 📚 Stacking front-to-back (dim=0): Shape torch.Size([3, 5, 2]) (depth=3, heith=5, width=2) - tablets go deeper into the box Front of the box): tensor([[1., 1.], [1., 1.], [1., 1.], [1., 1.], [1., 1.]]) 🗂️ Stacking bottom-to-top (dim=1): Shape torch.Size([5, 3, 2]) (depth=5, heigh=3, width=2) - tablets build upward Notice how each level contains one slice from ALL tablets: Front of the box): tensor([[1., 1.], [2., 2.], [3., 3.]]) 📑 Sliding left-to-right (dim=2): Shape torch.Size([5, 2, 3]) (depth=5, heigh=2, width=3) - tablets slide sideways Each position now contains values from ALL tablets: Front of the box): tensor([[1., 2., 3.], [1., 2., 3.]]) 🎯 Key Insight: The dimension you choose determines WHERE the tablets extend! dim=0: Tablets extend front-to-back (depth) dim=1: Tablets extend bottom-to-top (height) dim=2: Tablets extend left-to-right (width)
The Fusion Dilemma: When to Cat vs. Stack?¶
This choice torments many apprentices! Let me illuminate the path:
Use torch.cat()
when:
- Tensors represent different parts of the same data (e.g., different batches of images, different chunks of text)
- You want to extend an existing dimension
- Example: Concatenating multiple batches of training data
Use torch.stack()
when:
- Tensors represent parallel data of the same type (e.g., predictions from different models, different time steps)
- You need to create a new dimension to organize the data
- Example: Combining RGB channels to form a color image, or collecting multiple predictions
Observe this real-world scenario!
# Real-world example: Building a batch of images
# Imagine these are grayscale images (height=16, width=24)
image1 = torch.randn(16, 24)
image2 = torch.randn(16, 24)
image3 = torch.randn(16, 24)
print("Individual images:")
print(f"Image 1 shape: {image1.shape}")
print(f"Image 2 shape: {image2.shape}")
print(f"Image 3 shape: {image3.shape}\n")
# STACK them to create a batch (batch_size=3, height=2, width=3)
image_batch = torch.stack([image1, image2, image3], dim=0)
print(f"Batch of images shape: {image_batch.shape}")
print("Perfect for feeding into a neural network!\n")
# Now imagine we have RGB channels for one image
red_channel = torch.randn(32, 32)
green_channel = torch.randn(32, 32)
blue_channel = torch.randn(32, 32)
# STACK them to create RGB image (channels=3, height=2, width=3)
rgb_image = torch.stack([red_channel, green_channel, blue_channel], dim=0)
print(f"RGB image shape: {rgb_image.shape}")
print("The classic (C, H, W) format!")
Individual images: Image 1 shape: torch.Size([16, 24]) Image 2 shape: torch.Size([16, 24]) Image 3 shape: torch.Size([16, 24]) Batch of images shape: torch.Size([3, 16, 24]) Perfect for feeding into a neural network! RGB image shape: torch.Size([3, 32, 32]) The classic (C, H, W) format!
Your Mission: The Fusion Master's Gauntlet¶
The theory is yours—now prove your mastery! Complete these fusion challenges:
The Triple Stack: Create three 1D tensors of length 4 with different values. Stack them to create a 2D tensor of shape
(3, 4)
.The Horizontal Fusion: Create two 2D tensors of shape
(3, 2)
. Concatenate them horizontally to create a(3, 4)
tensor.The Batch Builder: You have 5 individual "samples" (each a 1D tensor of length 3). Stack them to create a proper batch tensor of shape
(5, 3)
suitable for training.The Dimension Disaster: Try to concatenate two tensors with different shapes:
(2, 3)
and(2, 4)
along dimension 0. Observe the error message—it's quite educational! Then fix it by concatenating along dimension 1 instead.The Multi-Fusion: Create a tensor of shape
(2, 6)
by first stacking three(2, 2)
tensors, then concatenating the result with another(3, 6)
tensor. This requires combining both operations!
# Your code for the Fusion Master's Gauntlet goes here!
print("--- 1. The Triple Stack ---")
tensor1 = torch.tensor([1, 2, 3, 4])
tensor2 = torch.tensor([5, 6, 7, 8])
tensor3 = torch.tensor([9, 10, 11, 12])
triple_stack = torch.stack([tensor1, tensor2, tensor3], dim=0)
print(f"Triple stack result:\n{triple_stack}")
print(f"Shape: {triple_stack.shape}\n")
print("--- 2. The Horizontal Fusion ---")
left_tensor = torch.randn(3, 2)
right_tensor = torch.randn(3, 2)
horizontal_fusion = torch.cat([left_tensor, right_tensor], dim=1)
print(f"Horizontal fusion shape: {horizontal_fusion.shape}\n")
print("--- 3. The Batch Builder ---")
samples = [torch.randn(3) for _ in range(5)] # 5 samples of length 3
batch = torch.stack(samples, dim=0)
print(f"Batch shape: {batch.shape}")
print("Ready for neural network training!\n")
print("--- 4. The Dimension Disaster ---")
disaster_a = torch.randn(2, 3)
disaster_b = torch.randn(2, 4)
try:
# This will fail!
bad_cat = torch.cat([disaster_a, disaster_b], dim=0)
except RuntimeError as e:
print(f"Error (as expected): {e}")
# The fix: concatenate along dimension 1
good_cat = torch.cat([disaster_a, disaster_b], dim=1)
print(f"Fixed by concatenating along dim 1: {good_cat.shape}\n")
print("--- 5. The Multi-Fusion ---")
# First, create and stack three (2,2) tensors
small_tensors = [torch.randn(2, 2) for _ in range(3)]
# Actually, let's concatenate the (2,2) tensors along dim=1 first
concat_part = torch.cat(small_tensors, dim=1) # Shape: (2, 6)
print(f"Multi-fusion result shape: {concat_part.shape}")
print("The key was concatenating, not stacking!")
--- 1. The Triple Stack --- Triple stack result: tensor([[ 1, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12]]) Shape: torch.Size([3, 4]) --- 2. The Horizontal Fusion --- Horizontal fusion shape: torch.Size([3, 4]) --- 3. The Batch Builder --- Batch shape: torch.Size([5, 3]) Ready for neural network training! --- 4. The Dimension Disaster --- Error (as expected): Sizes of tensors must match except in dimension 0. Expected size 3 but got size 4 for tensor number 1 in the list. Fixed by concatenating along dim 1: torch.Size([2, 7]) --- 5. The Multi-Fusion --- Multi-fusion result shape: torch.Size([2, 6]) The key was concatenating, not stacking!
Part 3: The Great Division - Splitting Tensors¶
Ah, but the surgeon's art is not complete until we master both creation AND division! Just as we learned to fuse tensors, we must also learn to split them apart. Sometimes your grand creation becomes too unwieldy, or you need to distribute pieces to different parts of your neural network.
Fear not! PyTorch provides elegant tools for this delicate operation:
torch.split()
– The Precise Slicer, it carves your tensor into chunks of the size you decree.torch.chunk()
- The Equal Divider! Splits a tensor into a specified number of roughly equal chunks.torch.unbind()
- The Dimension Destroyer! Removes a dimension by splitting along it.
Let us prepare a worthy subject for our division experiments!
# Create a test subject for our splitting experiments using only techniques we know!
# We'll create 6 columns where each column contains the same number repeated
col_1 = torch.ones(4) * 1 # Column of 1s
col_2 = torch.ones(4) * 2 # Column of 2s
col_3 = torch.ones(4) * 3 # Column of 3s
col_4 = torch.ones(4) * 4 # Column of 4s
col_5 = torch.ones(4) * 5 # Column of 5s
col_6 = torch.ones(4) * 6 # Column of 6s
# Stack them as columns to create our 4x6 tensor using stack (which we just learned!)
split_subject = torch.stack([col_1, col_2, col_3, col_4, col_5, col_6], dim=1)
print("Our subject for division experiments (created using stack!):")
print(f"Shape: {split_subject.shape}")
print(f"Tensor:\n{split_subject}\n")
print("Think of this as a chocolate bar with 4 rows and 6 columns!")
print("Each column contains the same number - perfect for tracking our splits! 🍫")
Our subject for division experiments (created using stack!): Shape: torch.Size([4, 6]) Tensor: tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) Think of this as a chocolate bar with 4 rows and 6 columns! Each column contains the same number - perfect for tracking our splits! 🍫
The Precise Slicer: torch.split()
¶
A Surgeon's Most Versatile Blade!
The TRUE power of torch.split()
! This is no mere cleaver—it is a precision instrument worthy of a master tensor surgeon!
torch.split(tensor, split_size_or_sections, dim)
possesses TWO magnificent modes of operation, each more diabolical than the last:
⚔️ Mode 1: The Uniform Guillotine (split_size
as int
)
Feed it a single integer, and it slices your tensor into equal-sized chunks with ruthless efficiency! If the dimension refuses to divide evenly, the final piece shall be smaller—a perfectly acceptable sacrifice to the tensor gods!
🗡️ Mode 2: The Bespoke Scalpel (sections
as list[int]
)
Ah, but THIS is where true tensor surgery ascends to high art! ✂️ Provide a list of integers—each one dictating the exact size of its corresponding slice. You wield total control! But heed this immutable law, dear apprentice: the sum of your list must match the total size of the dimension you wish to split. This is the sacred contract of the tensor gods—break it, and chaos (or at least a RuntimeError) shall ensue!
The Sacred Parameters:
split_size_or_sections
: Anint
for uniform domination, or alist[int]
for bespoke surgical control!dim
: The dimensional axis along which your blade shall cut!- Returns: A tuple of tensors (NEVER a single tensor—the split method serves only masters, not slaves!)
Now, witness as we dissect our chocolate bar with BOTH methods! The tensors... they will obey!
# Split along dimension 0 (rows) - like cutting horizontal slices of chocolate
rows_split_size=2
row_splits = torch.split(split_subject, rows_split_size, dim=0)
print(f"🍫 Split into row pieces (rows_split_size={rows_split_size}, dim=0):")
print(f"Number of pieces: {len(row_splits)}")
for i, piece in enumerate(row_splits):
print(f"Piece {i+1} shape {piece.shape}:\n{piece}\n")
# Split along dimension 0 (rows) - like cutting horizontal slices of chocolate
rows_split_size=3 # 4/3 - is not divisible
row_splits_uneven = torch.split(split_subject, rows_split_size, dim=0)
print(f"🪓 Split into UNEVEN row pieces (rows_split_size={rows_split_size}, dim=0):")
print(f"Number of pieces: {len(row_splits_uneven)}")
for i, piece in enumerate(row_splits_uneven):
print(f"Piece {i+1} shape {piece.shape}:\n{piece}\n")
# Split along dimension 1 (columns) - like cutting vertical slices
cols_split_size=3
col_splits = torch.split(split_subject, cols_split_size, dim=1)
print("🍫 Split into column pieces (split_size=3, dim=1):")
print(f"Number of pieces: {len(col_splits)}")
for i, piece in enumerate(col_splits):
print(f"Piece {i+1} shape {piece.shape}:\n{piece}\n")
print("🎯 Notice: torch.split() returns a TUPLE of tensors, not a single tensor!")
🍫 Split into row pieces (rows_split_size=2, dim=0): Number of pieces: 2 Piece 1 shape torch.Size([2, 6]): tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) Piece 2 shape torch.Size([2, 6]): tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) 🪓 Split into UNEVEN row pieces (rows_split_size=3, dim=0): Number of pieces: 2 Piece 1 shape torch.Size([3, 6]): tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) Piece 2 shape torch.Size([1, 6]): tensor([[1., 2., 3., 4., 5., 6.]]) 🍫 Split into column pieces (split_size=3, dim=1): Number of pieces: 2 Piece 1 shape torch.Size([4, 3]): tensor([[1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.]]) Piece 2 shape torch.Size([4, 3]): tensor([[4., 5., 6.], [4., 5., 6.], [4., 5., 6.], [4., 5., 6.]]) 🎯 Notice: torch.split() returns a TUPLE of tensors, not a single tensor!
The Bespoke Scalpel: Splitting with Diabolical Precision!¶
Now, my ambitious apprentice, we ascend to the HIGHEST form of surgical artistry! While Rudolf Hammer and his corporate drones settle for uniform mediocrity, WE shall command each slice with mathematical perfection!
The secret they don't want you to know: torch.split()
accepts a list of integers, each commanding the exact size of its designated slice! The sum of these numbers must equal the total dimension—this is not a suggestion, it is a LAW OF THE TENSOR UNIVERSE!
Observe as we carve our 6-column chocolate bar into three asymmetric pieces of sizes 1, 2, and 3
. Each cut serves our grand design! Mwahahaha!
# Split along dimension 1 (columns) into chunks of PRECISELY sizes 1, 2, and 3
# Mathematical law: 1 + 2 + 3 = 6 columns. The equation MUST balance or the universe protests!
section_sizes = [1, 2, 3]
section_splits = torch.split(split_subject, section_sizes, dim=1)
print("🔬 WITNESS! The bespoke scalpel carves with surgical precision!")
print(f"Command issued: Split into sections of sizes {section_sizes}")
print(f"Obedient tensor pieces created: {len(section_splits)}")
print(f"The tensors... they OBEY! Mwahahaha!\n")
for i, piece in enumerate(section_splits):
print(f"🧪 Surgical Specimen {i+1} (commanded size {section_sizes[i]}):")
print(f" Actual shape: {piece.shape} ✓")
print(f" Contents:\n{piece}\n")
print("💡 Master's Insight: Each piece is EXACTLY the size we commanded!")
print(" This is the power that separates us from the corporate drones!")
🔬 WITNESS! The bespoke scalpel carves with surgical precision! Command issued: Split into sections of sizes [1, 2, 3] Obedient tensor pieces created: 3 The tensors... they OBEY! Mwahahaha! 🧪 Surgical Specimen 1 (commanded size 1): Actual shape: torch.Size([4, 1]) ✓ Contents: tensor([[1.], [1.], [1.], [1.]]) 🧪 Surgical Specimen 2 (commanded size 2): Actual shape: torch.Size([4, 2]) ✓ Contents: tensor([[2., 3.], [2., 3.], [2., 3.], [2., 3.]]) 🧪 Surgical Specimen 3 (commanded size 3): Actual shape: torch.Size([4, 3]) ✓ Contents: tensor([[4., 5., 6.], [4., 5., 6.], [4., 5., 6.], [4., 5., 6.]]) 💡 Master's Insight: Each piece is EXACTLY the size we commanded! This is the power that separates us from the corporate drones!
The Equal Divider: torch.chunk()
¶
torch.chunk(tensor, chunks, dim)
divides your tensor into a specified number of roughly equal pieces. It's like asking: "I need exactly 3 pieces, make them as equal as possible!"
Key Difference from split()
:
torch.split()
: "Cut into pieces of size X" (you control piece size)torch.chunk()
: "Cut into exactly N pieces" (you control number of pieces)
If the dimension doesn't divide evenly, the last chunk will be smaller.
# Chunk into exactly 2 pieces along dimension 0 (rows)
row_chunks = torch.chunk(split_subject, chunks=2, dim=0)
print("✂️ Chunk into exactly 2 row pieces:")
print(f"Number of chunks: {len(row_chunks)}")
for i, chunk in enumerate(row_chunks):
print(f"Chunk {i+1} shape {chunk.shape}:\n{chunk}\n")
# Chunk into exactly 3 pieces along dimension 1 (columns)
# Note: 6 columns ÷ 3 chunks = 2 columns per chunk (perfect division!)
col_chunks = torch.chunk(split_subject, chunks=3, dim=1)
print("✂️ Chunk into exactly 3 column pieces:")
print(f"Number of chunks: {len(col_chunks)}")
for i, chunk in enumerate(col_chunks):
print(f"Chunk {i+1} shape {chunk.shape}:\n{chunk}\n")
# What happens with uneven division? Let's try 4 chunks from 6 columns
uneven_chunks = torch.chunk(split_subject, chunks=4, dim=1)
print("✂️ Chunk into 4 pieces (uneven division - 6 columns ÷ 4 chunks):")
print(f"Number of chunks: {len(uneven_chunks)}")
for i, chunk in enumerate(uneven_chunks):
print(f"Chunk {i+1} shape {chunk.shape}: {chunk.shape[1]} columns")
print("Notice: First 2 chunks get 2 columns each, last 2 chunks get 1 column each!")
✂️ Chunk into exactly 2 row pieces: Number of chunks: 2 Chunk 1 shape torch.Size([2, 6]): tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) Chunk 2 shape torch.Size([2, 6]): tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) ✂️ Chunk into exactly 3 column pieces: Number of chunks: 3 Chunk 1 shape torch.Size([4, 2]): tensor([[1., 2.], [1., 2.], [1., 2.], [1., 2.]]) Chunk 2 shape torch.Size([4, 2]): tensor([[3., 4.], [3., 4.], [3., 4.], [3., 4.]]) Chunk 3 shape torch.Size([4, 2]): tensor([[5., 6.], [5., 6.], [5., 6.], [5., 6.]]) ✂️ Chunk into 4 pieces (uneven division - 6 columns ÷ 4 chunks): Number of chunks: 3 Chunk 1 shape torch.Size([4, 2]): 2 columns Chunk 2 shape torch.Size([4, 2]): 2 columns Chunk 3 shape torch.Size([4, 2]): 2 columns Notice: First 2 chunks get 2 columns each, last 2 chunks get 1 column each!
The Dimension Destroyer: torch.unbind()
¶
torch.unbind()
is the most dramatic! It removes an entire dimension by splitting the tensor along it. Each slice becomes a separate tensor with one fewer dimension.
This is incredibly useful for:
- Processing each image in a batch separately
- Accessing individual time steps in sequence data
- Converting RGB channels into separate grayscale images
Think of it as the opposite of torch.stack()
!
# Unbind along dimension 0 - separate each row into individual tensors
print(split_subject)
unbound_rows = torch.unbind(split_subject, dim=0)
print("💥 Unbind along dimension 0 (separate each row):")
print(f"Number of tensors: {len(unbound_rows)}")
print(f"Original shape: {split_subject.shape} → Individual tensor shape: {unbound_rows[0].shape}")
for i, row_tensor in enumerate(unbound_rows):
print(f"Row {i+1}: {row_tensor}")
print("\n" + "="*50 + "\n")
# Unbind along dimension 1 - separate each column into individual tensors
unbound_cols = torch.unbind(split_subject, dim=1)
print("💥 Unbind along dimension 1 (separate each column):")
print(f"Number of tensors: {len(unbound_cols)}")
print(f"Original shape: {split_subject.shape} → Individual tensor shape: {unbound_cols[0].shape}")
for i, col_tensor in enumerate(unbound_cols):
print(f"Column {i+1}: {col_tensor}")
print(f"\n🧠 Key Insight: unbind() reduces dimensions! 2D → 1D tensors")
print(f" It's like taking apart the 3D book we built earlier!")
tensor([[1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.], [1., 2., 3., 4., 5., 6.]]) 💥 Unbind along dimension 0 (separate each row): Number of tensors: 4 Original shape: torch.Size([4, 6]) → Individual tensor shape: torch.Size([6]) Row 1: tensor([1., 2., 3., 4., 5., 6.]) Row 2: tensor([1., 2., 3., 4., 5., 6.]) Row 3: tensor([1., 2., 3., 4., 5., 6.]) Row 4: tensor([1., 2., 3., 4., 5., 6.]) ================================================== 💥 Unbind along dimension 1 (separate each column): Number of tensors: 6 Original shape: torch.Size([4, 6]) → Individual tensor shape: torch.Size([4]) Column 1: tensor([1., 1., 1., 1.]) Column 2: tensor([2., 2., 2., 2.]) Column 3: tensor([3., 3., 3., 3.]) Column 4: tensor([4., 4., 4., 4.]) Column 5: tensor([5., 5., 5., 5.]) Column 6: tensor([6., 6., 6., 6.]) 🧠 Key Insight: unbind() reduces dimensions! 2D → 1D tensors It's like taking apart the 3D book we built earlier!
Part 4: Real-World Surgical Applications 🏭⚡¶
Enough of my carefully controlled laboratory specimens! The time has come to witness the TRUE power of tensor surgery in the wild! These are not mere academic exercises—these are the EXACT techniques used by the masters who built GPT, CLIP, and the neural networks that power modern AI!
Prepare to see your newfound skills applied to the very foundations of modern machine learning!
Exercise 1: Multi-Head Attention Surgery 🧠⚡¶
Real-World Context: This is EXACTLY how Transformer models (GPT, BERT) split their attention mechanism into multiple "heads." Each head can focus on different aspects of the input—some learn grammar, others learn semantics, others learn long-range dependencies!
Your Mission: You have the concatenated output from all attention heads. Split it back into individual heads so each can be processed separately.
The Setup: A Transformer's multi-head attention layer has just computed attention for a batch of sequences. All 8 attention heads are concatenated together in the last dimension. Your job: liberate each head!
# Exercise 1: Multi-Head Attention Surgery 🧠⚡
print("🧠 EXERCISE 1: Multi-Head Attention Head Splitting")
print("=" * 60)
# The scenario: Output from a Transformer's multi-head attention layer
batch_size, seq_len, d_model = 32, 128, 512
num_heads = 8
# The attention layer outputs one big tensor containing ALL attention heads
attention_output = torch.randn(batch_size, seq_len, d_model)
print(f"🔬 Raw attention output shape: {attention_output.shape}")
print("This contains ALL 8 attention heads concatenated together!")
# YOUR SURGICAL PRECISION: Split this into individual attention heads
head_dim = d_model // num_heads # 512 // 8 = 64 dimensions per head
attention_heads = torch.split(attention_output, head_dim, dim=2)
print(f"\n⚔️ MAGNIFICENT! Split into {len(attention_heads)} individual attention heads!")
print(f"Each head shape: {attention_heads[0].shape}")
print(f"Head dimension: {head_dim} (d_model / num_heads = {d_model} / {num_heads})")
# Verify our surgery was successful
total_dims = sum(head.shape[2] for head in attention_heads)
print(f"\n✅ Verification: {total_dims} total dimensions = {d_model} original dimensions")
print("Perfect! No information lost in the surgical procedure!")
🧠 EXERCISE 1: Multi-Head Attention Head Splitting ============================================================ 🔬 Raw attention output shape: torch.Size([32, 128, 512]) This contains ALL 8 attention heads concatenated together! ⚔️ MAGNIFICENT! Split into 8 individual attention heads! Each head shape: torch.Size([32, 128, 64]) Head dimension: 64 (d_model / num_heads = 512 / 8) ✅ Verification: 512 total dimensions = 512 original dimensions Perfect! No information lost in the surgical procedure!
Exercise 2: Multi-Modal Fusion (CLIP-Style) 🖼️📝¶
Real-World Context: This is the FOUNDATIONAL technique behind multimodal models! By concatenating text and image embeddings, AI systems learn to understand both modalities in a unified space.
Your Mission: You have separate embeddings for text descriptions and images. Fuse them together to create a unified multimodal representation that can understand both vision and language!
The Setup: A batch of text descriptions ("A cat sitting on a chair") and their corresponding image embeddings. Your fusion will enable the model to match images with text—the core of visual search and image generation!
# Exercise 2: Multi-Modal Fusion (CLIP-Style) 🖼️📝
print("🖼️ EXERCISE 2: Multi-Modal Embedding Fusion")
print("=" * 60)
# The scenario: Separate embeddings for text and images (like CLIP)
batch_size = 8
# Two separate universes of understanding
text_embeddings = torch.randn(batch_size, 512) # "A cat sitting on a chair"
image_embeddings = torch.randn(batch_size, 512) # [actual image of cat on chair]
print(f"📝 Text embeddings: {text_embeddings.shape} (language understanding)")
print(f"🖼️ Image embeddings: {image_embeddings.shape} (visual understanding)")
print("Two separate modalities, waiting to be unified...")
# YOUR FUSION MASTERY: Concatenate to create multimodal understanding
multimodal_embeddings = torch.cat([text_embeddings, image_embeddings], dim=1)
print(f"\n🧠 BEHOLD! Fused multimodal embeddings: {multimodal_embeddings.shape}")
print(f"Combined features: 512 (text) + 512 (image) = {multimodal_embeddings.shape[1]}")
# Verify our fusion preserved all information
assert multimodal_embeddings.shape[1] == text_embeddings.shape[1] + image_embeddings.shape[1]
print(f"\n✅ Fusion verification: Perfect! No information lost!")
🖼️ EXERCISE 2: Multi-Modal Embedding Fusion ============================================================ 📝 Text embeddings: torch.Size([8, 512]) (language understanding) 🖼️ Image embeddings: torch.Size([8, 512]) (visual understanding) Two separate modalities, waiting to be unified... 🧠 BEHOLD! Fused multimodal embeddings: torch.Size([8, 1024]) Combined features: 512 (text) + 512 (image) = 1024 ✅ Fusion verification: Perfect! No information lost!
Exercise 3: RGB Channel Liberation 🎨🔓¶
Real-World Context: Computer vision systems constantly need to separate and analyze individual color channels. This technique is fundamental.
Your Mission: You have a batch of RGB images where all color channels are entangled together. Use your unbinding mastery to liberate each color channel into separate batches for independent processing!
The Setup: A batch of color images from a dataset. Each image has Red, Green, and Blue channels mixed together. Your surgical precision will separate them cleanly while preserving the batch structure.
# Exercise 3: RGB Channel Liberation 🎨🔓
print("🎨 EXERCISE 3: RGB Channel Separation")
print("=" * 60)
# The scenario: A batch of RGB images with entangled color channels
batch_size, height, width = 16, 224, 224
# RGB images with all color channels mixed together
rgb_batch = torch.randn(batch_size, 3, height, width) # Standard (N, C, H, W) format
print(f"🖼️ RGB image batch: {rgb_batch.shape}")
print("Format: (batch_size, channels, height, width)")
print("All color information is entangled together!")
# YOUR LIBERATION TECHNIQUE: Free each color channel across the entire batch
red_batch, green_batch, blue_batch = torch.unbind(rgb_batch, dim=1)
print(f"\n🔴 Red channel batch: {red_batch.shape}")
print(f"🟢 Green channel batch: {green_batch.shape}")
print(f"🔵 Blue channel batch: {blue_batch.shape}")
print("Each channel is now a separate grayscale batch!")
# Demonstrate the power: we can reconstruct the original perfectly
reconstructed = torch.stack([red_batch, green_batch, blue_batch], dim=1)
print(f"\n🔄 Reconstruction test: {reconstructed.shape}")
print(f"Perfect match: {torch.equal(rgb_batch, reconstructed)}")
# Show the dimensional transformation clearly
print(f"\n📐 Dimensional analysis:")
print(f" Original: {rgb_batch.shape} → RGB channels mixed")
print(f" After unbind: 3 separate {red_batch.shape} grayscale batches")
print(f" Reconstruction: {reconstructed.shape} → Back to original!")
print(f"\n💡 Real-world channel liberation applications:")
print(f" 🏥 Medical imaging: Isolate blood vessels, tissue types")
print(f" 🛰️ Satellite analysis: Separate vegetation, water, urban areas")
print(f" 📊 Computer vision: Channel-wise preprocessing and normalization")
print(f" 🎭 Image processing: Artistic filters and color grading")
print(f"\nBehold! We've mastered the separation and reunification of visual reality!")
print("The RGB trinity bows to our surgical precision! Mwahahaha! 🔬⚡")
🎨 EXERCISE 3: RGB Channel Separation ============================================================ 🖼️ RGB image batch: torch.Size([16, 3, 224, 224]) Format: (batch_size, channels, height, width) All color information is entangled together! 🔴 Red channel batch: torch.Size([16, 224, 224]) 🟢 Green channel batch: torch.Size([16, 224, 224]) 🔵 Blue channel batch: torch.Size([16, 224, 224]) Each channel is now a separate grayscale batch! 🔄 Reconstruction test: torch.Size([16, 3, 224, 224]) Perfect match: True 📐 Dimensional analysis: Original: torch.Size([16, 3, 224, 224]) → RGB channels mixed After unbind: 3 separate torch.Size([16, 224, 224]) grayscale batches Reconstruction: torch.Size([16, 3, 224, 224]) → Back to original! 💡 Real-world channel liberation applications: 🏥 Medical imaging: Isolate blood vessels, tissue types 🛰️ Satellite analysis: Separate vegetation, water, urban areas 📊 Computer vision: Channel-wise preprocessing and normalization 🎭 Image processing: Artistic filters and color grading Behold! We've mastered the separation and reunification of visual reality! The RGB trinity bows to our surgical precision! Mwahahaha! 🔬⚡
Summary: The Surgeon's Knowledge Is Complete!¶
Magnificent work, my surgical apprentice! You have mastered the fundamental operations of tensor surgery and assembly. Let us review your newly acquired powers:
🔪 The Art of Selection¶
- Indexing & Slicing: Extract elements, rows, columns, or sub-regions with surgical precision
- Boolean Masking: Select elements that meet specific criteria
- Key Tools:
tensor[index]
,tensor[start:end]
,tensor[mask]
🧬 Forbidden Fusions¶
- Concatenation: Join tensors along existing dimensions with
torch.cat()
- Stacking: Create new dimensions and organize tensors with
torch.stack()
- Key Rule: Understanding when to extend vs. when to create new dimensions
✂️ The Great Division¶
- Precise Splitting: Cut into specific-sized pieces with
torch.split()
- Equal Division: Divide into a set number of chunks with
torch.chunk()
- Dimension Destruction: Remove dimensions entirely with
torch.unbind()
You now possess the core skills to dissect any tensor, assemble complex structures, and divide them back into manageable pieces. These are the fundamental surgical techniques you'll use in every neural network adventure ahead!
Professor Torchenstein's Outro¶
Spectacular! You've wielded the surgical tools with the precision of a master! Your tensors have been sliced, fused, and divided according to your will. But tell me, my gifted apprentice—do you feel that tingling sensation in your neural pathways? That's the hunger for more power!
Your tensors may now be perfectly assembled, but they are still... rigid. Static in their dimensions. What if I told you that we could transform their very essence without changing their soul? What if we could make a 1D tensor become a 2D matrix, or a 3D cube collapse into a flat surface?
In our next lesson, Tensor Metamorphosis: Shape-Shifting Mastery, we shall unlock the secrets of transformation itself! We will reshape reality, squeeze dimensions out of existence, and permute the cosmic order of our data!
Until then, practice your surgical techniques. The metamorphosis chamber awaits! Mwahahahahaha!