Files
TinyTorch/modules/networks/networks_dev.py
Vijay Janapa Reddi b155dec4fc feat: add Networks module with forward-pass and visualizations
- Add modules/networks/networks_dev.py and networks_dev.ipynb (Jupytext/nbdev educational pattern)
- Add comprehensive visualizations: architecture, data flow, layer analysis, network comparison
- Add modules/networks/README.md with learning goals, usage, and visualization docs
- Add modules/networks/tests/test_networks.py with thorough tests for composition, MLPs, and visualizations
- Register 'networks' in CLI info and test commands
- Update CLI info command to check layers/networks status
- This module focuses on forward pass only (no training yet)
2025-07-10 23:16:12 -04:00

837 lines
27 KiB
Python

# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.1
# ---
# %% [markdown]
"""
# Module 3: Networks - Neural Network Architectures
Welcome to the Networks module! This is where we compose layers into complete neural network architectures.
## Learning Goals
- Understand networks as function composition: `f(x) = layer_n(...layer_2(layer_1(x)))`
- Build common architectures (MLP, CNN) from layers
- Visualize network structure and data flow
- See how architecture affects capability
- Master forward pass inference (no training yet!)
## Build → Use → Understand
1. **Build**: Compose layers into complete networks
2. **Use**: Create different architectures and run inference
3. **Understand**: How architecture design affects network behavior
## Module Dependencies
This module builds on previous modules:
- **tensor** → **activations** → **layers** → **networks**
- Clean composition: math functions → building blocks → complete systems
## Module → Package Structure
**🎓 Teaching vs. 🔧 Building**:
- **Learning side**: Work in `modules/networks/networks_dev.py`
- **Building side**: Exports to `tinytorch/core/networks.py`
This module teaches how to compose layers into complete neural network architectures.
"""
# %%
#| default_exp core.networks
# Setup and imports
import numpy as np
import sys
from typing import List, Union, Optional, Callable
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import FancyBboxPatch, ConnectionPatch
import seaborn as sns
# Import our building blocks
from tinytorch.core.tensor import Tensor
from tinytorch.core.layers import Dense
from tinytorch.core.activations import ReLU, Sigmoid, Tanh
print("🔥 TinyTorch Networks Module")
print(f"NumPy version: {np.__version__}")
print(f"Python version: {sys.version_info.major}.{sys.version_info.minor}")
print("Ready to build neural network architectures!")
# %%
#| export
import numpy as np
import sys
from typing import List, Union, Optional, Callable
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import FancyBboxPatch, ConnectionPatch
import seaborn as sns
# Import our building blocks
from tinytorch.core.tensor import Tensor
from tinytorch.core.layers import Dense
from tinytorch.core.activations import ReLU, Sigmoid, Tanh
# %%
#| hide
#| export
def _should_show_plots():
"""Check if we should show plots (disable during testing)"""
return 'pytest' not in sys.modules and 'test' not in sys.argv
# %% [markdown]
"""
## Step 1: What is a Network?
A **network** is a composition of layers that transforms input data into output predictions. Think of it as:
```
Input → Layer1 → Layer2 → Layer3 → Output
```
**The fundamental insight**: Neural networks are just function composition!
- Each layer is a function: `f_i(x)`
- The network is: `f(x) = f_n(...f_2(f_1(x)))`
- Complex behavior emerges from simple building blocks
**Why networks matter**:
- They solve real problems (classification, regression, etc.)
- Architecture determines what problems you can solve
- Understanding networks = understanding deep learning
- They're the foundation for all modern AI
Let's start by building the most fundamental network: **Sequential**.
"""
# %%
#| export
class Sequential:
"""
Sequential Network: Composes layers in sequence
The most fundamental network architecture.
Applies layers in order: f(x) = layer_n(...layer_2(layer_1(x)))
Args:
layers: List of layers to compose
TODO: Implement the Sequential network with forward pass.
"""
def __init__(self, layers: List):
"""
Initialize Sequential network with layers.
Args:
layers: List of layers to compose in order
TODO: Store the layers and implement forward pass
"""
raise NotImplementedError("Student implementation required")
def forward(self, x: Tensor) -> Tensor:
"""
Forward pass through all layers in sequence.
Args:
x: Input tensor
Returns:
Output tensor after passing through all layers
TODO: Implement sequential forward pass through all layers
"""
raise NotImplementedError("Student implementation required")
def __call__(self, x: Tensor) -> Tensor:
"""Make network callable: network(x) same as network.forward(x)"""
return self.forward(x)
# %%
#| hide
#| export
class Sequential:
"""
Sequential Network: Composes layers in sequence
The most fundamental network architecture.
Applies layers in order: f(x) = layer_n(...layer_2(layer_1(x)))
"""
def __init__(self, layers: List):
"""Initialize Sequential network with layers."""
self.layers = layers
def forward(self, x: Tensor) -> Tensor:
"""Forward pass through all layers in sequence."""
# Apply each layer in order
for layer in self.layers:
x = layer(x)
return x
def __call__(self, x: Tensor) -> Tensor:
"""Make network callable: network(x) same as network.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Test Your Sequential Network
Once you implement the Sequential network above, run this cell to test it:
"""
# %%
# Test the Sequential network
try:
print("=== Testing Sequential Network ===")
# Create a simple 2-layer network: 3 → 4 → 2
network = Sequential([
Dense(3, 4),
ReLU(),
Dense(4, 2),
Sigmoid()
])
# Test with sample data
x = Tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(f"Input shape: {x.shape}")
print(f"Input data: {x.data}")
# Forward pass
output = network(x)
print(f"Output shape: {output.shape}")
print(f"Output data: {output.data}")
print("✅ Sequential network working!")
except Exception as e:
print(f"❌ Error: {e}")
print("Make sure to implement the Sequential network!")
# %% [markdown]
"""
## Step 2: Network Visualization
Now let's create powerful visualizations to understand what our networks look like and how they work!
"""
# %%
#| export
def visualize_network_architecture(network: Sequential, title: str = "Network Architecture"):
"""
Create a visual representation of network architecture.
Args:
network: Sequential network to visualize
title: Title for the plot
"""
if not _should_show_plots():
print("📊 Plots disabled during testing - this is normal!")
return
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
# Network parameters
layer_count = len(network.layers)
layer_height = 0.8
layer_spacing = 1.2
# Colors for different layer types
colors = {
'Dense': '#4CAF50', # Green
'ReLU': '#2196F3', # Blue
'Sigmoid': '#FF9800', # Orange
'Tanh': '#9C27B0', # Purple
'default': '#757575' # Gray
}
# Draw layers
for i, layer in enumerate(network.layers):
# Determine layer type and color
layer_type = type(layer).__name__
color = colors.get(layer_type, colors['default'])
# Layer position
x = i * layer_spacing
y = 0
# Create layer box
layer_box = FancyBboxPatch(
(x - 0.3, y - layer_height/2),
0.6, layer_height,
boxstyle="round,pad=0.1",
facecolor=color,
edgecolor='black',
linewidth=2,
alpha=0.8
)
ax.add_patch(layer_box)
# Add layer label
ax.text(x, y, layer_type, ha='center', va='center',
fontsize=10, fontweight='bold', color='white')
# Add layer details
if hasattr(layer, 'input_size') and hasattr(layer, 'output_size'):
details = f"{layer.input_size}{layer.output_size}"
ax.text(x, y - 0.3, details, ha='center', va='center',
fontsize=8, color='white')
# Draw connections to next layer
if i < layer_count - 1:
next_x = (i + 1) * layer_spacing
connection = ConnectionPatch(
(x + 0.3, y), (next_x - 0.3, y),
"data", "data",
arrowstyle="->", shrinkA=5, shrinkB=5,
mutation_scale=20, fc="black", lw=2
)
ax.add_patch(connection)
# Formatting
ax.set_xlim(-0.5, (layer_count - 1) * layer_spacing + 0.5)
ax.set_ylim(-1, 1)
ax.set_aspect('equal')
ax.axis('off')
# Add title
plt.title(title, fontsize=16, fontweight='bold', pad=20)
# Add legend
legend_elements = []
for layer_type, color in colors.items():
if layer_type != 'default':
legend_elements.append(patches.Patch(color=color, label=layer_type))
ax.legend(handles=legend_elements, loc='upper right', bbox_to_anchor=(1, 1))
plt.tight_layout()
plt.show()
# %%
#| export
def visualize_data_flow(network: Sequential, input_data: Tensor, title: str = "Data Flow Through Network"):
"""
Visualize how data flows through the network.
Args:
network: Sequential network
input_data: Input tensor
title: Title for the plot
"""
if not _should_show_plots():
print("📊 Plots disabled during testing - this is normal!")
return
# Get intermediate outputs
intermediate_outputs = []
x = input_data
for i, layer in enumerate(network.layers):
x = layer(x)
intermediate_outputs.append({
'layer': network.layers[i],
'output': x,
'layer_index': i
})
# Create visualization
fig, axes = plt.subplots(2, len(network.layers), figsize=(4*len(network.layers), 8))
if len(network.layers) == 1:
axes = axes.reshape(1, -1)
for i, (layer, output) in enumerate(zip(network.layers, intermediate_outputs)):
# Top row: Layer information
ax_top = axes[0, i] if len(network.layers) > 1 else axes[0]
# Layer type and details
layer_type = type(layer).__name__
ax_top.text(0.5, 0.8, layer_type, ha='center', va='center',
fontsize=12, fontweight='bold')
if hasattr(layer, 'input_size') and hasattr(layer, 'output_size'):
ax_top.text(0.5, 0.6, f"{layer.input_size}{layer.output_size}",
ha='center', va='center', fontsize=10)
# Output shape
ax_top.text(0.5, 0.4, f"Shape: {output['output'].shape}",
ha='center', va='center', fontsize=9)
# Output statistics
output_data = output['output'].data
ax_top.text(0.5, 0.2, f"Mean: {np.mean(output_data):.3f}",
ha='center', va='center', fontsize=9)
ax_top.text(0.5, 0.1, f"Std: {np.std(output_data):.3f}",
ha='center', va='center', fontsize=9)
ax_top.set_xlim(0, 1)
ax_top.set_ylim(0, 1)
ax_top.axis('off')
# Bottom row: Output visualization
ax_bottom = axes[1, i] if len(network.layers) > 1 else axes[1]
# Show output as heatmap or histogram
output_data = output['output'].data.flatten()
if len(output_data) <= 20: # Small output - show as bars
ax_bottom.bar(range(len(output_data)), output_data, alpha=0.7)
ax_bottom.set_title(f"Layer {i+1} Output")
ax_bottom.set_xlabel("Output Index")
ax_bottom.set_ylabel("Value")
else: # Large output - show histogram
ax_bottom.hist(output_data, bins=20, alpha=0.7, edgecolor='black')
ax_bottom.set_title(f"Layer {i+1} Output Distribution")
ax_bottom.set_xlabel("Value")
ax_bottom.set_ylabel("Frequency")
ax_bottom.grid(True, alpha=0.3)
plt.suptitle(title, fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
# %%
#| export
def compare_networks(networks: List[Sequential], network_names: List[str],
input_data: Tensor, title: str = "Network Comparison"):
"""
Compare different network architectures side-by-side.
Args:
networks: List of networks to compare
network_names: Names for each network
input_data: Input tensor to test with
title: Title for the plot
"""
if not _should_show_plots():
print("📊 Plots disabled during testing - this is normal!")
return
fig, axes = plt.subplots(2, len(networks), figsize=(6*len(networks), 10))
if len(networks) == 1:
axes = axes.reshape(2, -1)
for i, (network, name) in enumerate(zip(networks, network_names)):
# Get network output
output = network(input_data)
# Top row: Architecture visualization
ax_top = axes[0, i] if len(networks) > 1 else axes[0]
# Count layer types
layer_types = {}
for layer in network.layers:
layer_type = type(layer).__name__
layer_types[layer_type] = layer_types.get(layer_type, 0) + 1
# Create pie chart of layer types
if layer_types:
labels = list(layer_types.keys())
sizes = list(layer_types.values())
colors = plt.cm.Set3(np.linspace(0, 1, len(labels)))
ax_top.pie(sizes, labels=labels, autopct='%1.1f%%', colors=colors)
ax_top.set_title(f"{name}\nLayer Distribution")
# Bottom row: Output comparison
ax_bottom = axes[1, i] if len(networks) > 1 else axes[1]
output_data = output.data.flatten()
# Show output statistics
ax_bottom.hist(output_data, bins=20, alpha=0.7, edgecolor='black')
ax_bottom.axvline(np.mean(output_data), color='red', linestyle='--',
label=f'Mean: {np.mean(output_data):.3f}')
ax_bottom.axvline(np.median(output_data), color='green', linestyle='--',
label=f'Median: {np.median(output_data):.3f}')
ax_bottom.set_title(f"{name} Output Distribution")
ax_bottom.set_xlabel("Output Value")
ax_bottom.set_ylabel("Frequency")
ax_bottom.legend()
ax_bottom.grid(True, alpha=0.3)
plt.suptitle(title, fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()
# %% [markdown]
"""
## Step 3: Building Common Architectures
Now let's build some common neural network architectures and visualize them!
"""
# %%
#| export
def create_mlp(input_size: int, hidden_sizes: List[int], output_size: int,
activation=ReLU, output_activation=Sigmoid) -> Sequential:
"""
Create a Multi-Layer Perceptron (MLP) network.
Args:
input_size: Number of input features
hidden_sizes: List of hidden layer sizes
output_size: Number of output features
activation: Activation function for hidden layers
output_activation: Activation function for output layer
Returns:
Sequential network
"""
layers = []
# Input layer
if hidden_sizes:
layers.append(Dense(input_size, hidden_sizes[0]))
layers.append(activation())
# Hidden layers
for i in range(len(hidden_sizes) - 1):
layers.append(Dense(hidden_sizes[i], hidden_sizes[i + 1]))
layers.append(activation())
# Output layer
layers.append(Dense(hidden_sizes[-1], output_size))
else:
# Direct input to output
layers.append(Dense(input_size, output_size))
layers.append(output_activation())
return Sequential(layers)
# %%
# Test MLP creation and visualization
try:
print("=== Testing MLP Creation and Visualization ===")
# Create different MLP architectures
mlp_small = create_mlp(input_size=3, hidden_sizes=[4], output_size=2)
mlp_medium = create_mlp(input_size=10, hidden_sizes=[16, 8], output_size=3)
mlp_large = create_mlp(input_size=784, hidden_sizes=[128, 64, 32], output_size=10)
print("Created MLP architectures:")
print(f" Small: 3 → 4 → 2")
print(f" Medium: 10 → 16 → 8 → 3")
print(f" Large: 784 → 128 → 64 → 32 → 10")
# Test with sample data
x = Tensor(np.random.randn(5, 3).astype(np.float32))
# Visualize architectures
visualize_network_architecture(mlp_small, "Small MLP Architecture")
visualize_network_architecture(mlp_medium, "Medium MLP Architecture")
visualize_network_architecture(mlp_large, "Large MLP Architecture")
# Visualize data flow
visualize_data_flow(mlp_small, x, "Data Flow Through Small MLP")
# Compare networks
networks = [mlp_small, mlp_medium]
names = ["Small MLP", "Medium MLP"]
compare_networks(networks, names, x, "MLP Architecture Comparison")
print("✅ MLP creation and visualization working!")
except Exception as e:
print(f"❌ Error: {e}")
print("Make sure to implement the visualization functions!")
# %% [markdown]
"""
## Step 4: Understanding Network Behavior
Let's analyze how different network architectures behave with different types of input data.
"""
# %%
#| export
def analyze_network_behavior(network: Sequential, input_data: Tensor,
title: str = "Network Behavior Analysis"):
"""
Analyze how a network behaves with different types of input.
Args:
network: Network to analyze
input_data: Input tensor
title: Title for the plot
"""
if not _should_show_plots():
print("📊 Plots disabled during testing - this is normal!")
return
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
# 1. Input vs Output relationship
ax1 = axes[0, 0]
input_flat = input_data.data.flatten()
output = network(input_data)
output_flat = output.data.flatten()
ax1.scatter(input_flat, output_flat, alpha=0.6)
ax1.plot([input_flat.min(), input_flat.max()],
[input_flat.min(), input_flat.max()], 'r--', alpha=0.5, label='y=x')
ax1.set_xlabel('Input Values')
ax1.set_ylabel('Output Values')
ax1.set_title('Input vs Output')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 2. Output distribution
ax2 = axes[0, 1]
ax2.hist(output_flat, bins=20, alpha=0.7, edgecolor='black')
ax2.axvline(np.mean(output_flat), color='red', linestyle='--',
label=f'Mean: {np.mean(output_flat):.3f}')
ax2.set_xlabel('Output Values')
ax2.set_ylabel('Frequency')
ax2.set_title('Output Distribution')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 3. Layer-by-layer activation patterns
ax3 = axes[0, 2]
activations = []
x = input_data
for layer in network.layers:
x = layer(x)
if hasattr(layer, 'input_size'): # Dense layer
activations.append(np.mean(x.data))
else: # Activation layer
activations.append(np.mean(x.data))
ax3.plot(range(len(activations)), activations, 'bo-', linewidth=2, markersize=8)
ax3.set_xlabel('Layer Index')
ax3.set_ylabel('Mean Activation')
ax3.set_title('Layer-by-Layer Activations')
ax3.grid(True, alpha=0.3)
# 4. Network depth analysis
ax4 = axes[1, 0]
layer_types = [type(layer).__name__ for layer in network.layers]
layer_counts = {}
for layer_type in layer_types:
layer_counts[layer_type] = layer_counts.get(layer_type, 0) + 1
if layer_counts:
ax4.bar(layer_counts.keys(), layer_counts.values(), alpha=0.7)
ax4.set_xlabel('Layer Type')
ax4.set_ylabel('Count')
ax4.set_title('Layer Type Distribution')
ax4.grid(True, alpha=0.3)
# 5. Shape transformation
ax5 = axes[1, 1]
shapes = [input_data.shape]
x = input_data
for layer in network.layers:
x = layer(x)
shapes.append(x.shape)
layer_indices = range(len(shapes))
shape_sizes = [np.prod(shape) for shape in shapes]
ax5.plot(layer_indices, shape_sizes, 'go-', linewidth=2, markersize=8)
ax5.set_xlabel('Layer Index')
ax5.set_ylabel('Tensor Size')
ax5.set_title('Shape Transformation')
ax5.grid(True, alpha=0.3)
# 6. Network summary
ax6 = axes[1, 2]
ax6.axis('off')
summary_text = f"""
Network Summary:
• Total Layers: {len(network.layers)}
• Input Shape: {input_data.shape}
• Output Shape: {output.shape}
• Parameters: {sum(np.prod(layer.weights.data.shape) if hasattr(layer, 'weights') else 0 for layer in network.layers)}
• Architecture: {''.join([type(layer).__name__ for layer in network.layers])}
"""
ax6.text(0.05, 0.95, summary_text, transform=ax6.transAxes,
fontsize=10, verticalalignment='top', fontfamily='monospace')
plt.suptitle(title, fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()
# %%
# Test network behavior analysis
try:
print("=== Testing Network Behavior Analysis ===")
# Create a network for analysis
network = create_mlp(input_size=5, hidden_sizes=[8, 4], output_size=2)
# Test with different types of input
x_normal = Tensor(np.random.randn(10, 5).astype(np.float32))
x_uniform = Tensor(np.random.uniform(-1, 1, (10, 5)).astype(np.float32))
x_zeros = Tensor(np.zeros((10, 5)).astype(np.float32))
print("Analyzing network behavior with different inputs...")
# Analyze behavior
analyze_network_behavior(network, x_normal, "Network Behavior: Normal Input")
analyze_network_behavior(network, x_uniform, "Network Behavior: Uniform Input")
analyze_network_behavior(network, x_zeros, "Network Behavior: Zero Input")
print("✅ Network behavior analysis working!")
except Exception as e:
print(f"❌ Error: {e}")
print("Make sure to implement the behavior analysis function!")
# %% [markdown]
"""
## Step 5: Practical Applications
Let's see how our networks can be applied to real-world problems!
"""
# %%
#| export
def create_classification_network(input_size: int, num_classes: int,
hidden_sizes: List[int] = None) -> Sequential:
"""
Create a network for classification problems.
Args:
input_size: Number of input features
num_classes: Number of output classes
hidden_sizes: List of hidden layer sizes (default: [input_size//2])
Returns:
Sequential network for classification
"""
if hidden_sizes is None:
hidden_sizes = [input_size // 2]
return create_mlp(
input_size=input_size,
hidden_sizes=hidden_sizes,
output_size=num_classes,
activation=ReLU,
output_activation=Sigmoid
)
# %%
#| export
def create_regression_network(input_size: int, output_size: int = 1,
hidden_sizes: List[int] = None) -> Sequential:
"""
Create a network for regression problems.
Args:
input_size: Number of input features
output_size: Number of output values (default: 1)
hidden_sizes: List of hidden layer sizes (default: [input_size//2])
Returns:
Sequential network for regression
"""
if hidden_sizes is None:
hidden_sizes = [input_size // 2]
return create_mlp(
input_size=input_size,
hidden_sizes=hidden_sizes,
output_size=output_size,
activation=ReLU,
output_activation=Tanh # No activation for regression
)
# %%
# Test practical applications
try:
print("=== Testing Practical Applications ===")
# Create networks for different tasks
digit_classifier = create_classification_network(
input_size=784, # 28x28 image
num_classes=10, # 10 digits
hidden_sizes=[128, 64]
)
sentiment_analyzer = create_classification_network(
input_size=100, # 100-dimensional word embeddings
num_classes=2, # Positive/Negative
hidden_sizes=[32, 16]
)
house_price_predictor = create_regression_network(
input_size=13, # 13 house features
output_size=1, # 1 price prediction
hidden_sizes=[8, 4]
)
print("Created networks for different applications:")
print(f" Digit Classifier: 784 → 128 → 64 → 10")
print(f" Sentiment Analyzer: 100 → 32 → 16 → 2")
print(f" House Price Predictor: 13 → 8 → 4 → 1")
# Test with sample data
digit_input = Tensor(np.random.randn(1, 784).astype(np.float32))
sentiment_input = Tensor(np.random.randn(1, 100).astype(np.float32))
house_input = Tensor(np.random.randn(1, 13).astype(np.float32))
# Get predictions
digit_pred = digit_classifier(digit_input)
sentiment_pred = sentiment_analyzer(sentiment_input)
house_pred = house_price_predictor(house_input)
print(f"\nSample predictions:")
print(f" Digit classifier output: {digit_pred.data[0]}")
print(f" Sentiment analyzer output: {sentiment_pred.data[0]}")
print(f" House price predictor output: {house_pred.data[0]}")
# Visualize architectures
visualize_network_architecture(digit_classifier, "Digit Classification Network")
visualize_network_architecture(sentiment_analyzer, "Sentiment Analysis Network")
visualize_network_architecture(house_price_predictor, "House Price Prediction Network")
print("✅ Practical applications working!")
except Exception as e:
print(f"❌ Error: {e}")
print("Make sure to implement the application functions!")
# %% [markdown]
"""
## 🎓 Module Summary
### What You Learned
1. **Network Composition**: Building complete networks from layers
2. **Architecture Design**: How to choose network structures
3. **Visualization**: Understanding networks through visual analysis
4. **Practical Applications**: Real-world network use cases
### Key Architectural Insights
- **Function Composition**: Networks as `f(x) = layer_n(...layer_1(x))`
- **Modular Design**: Clean separation between layers and networks
- **Visual Understanding**: How to analyze network behavior
- **Application Patterns**: Classification vs regression architectures
### Network Design Principles
- **Depth vs Width**: Trade-offs in network architecture
- **Activation Functions**: How they affect network behavior
- **Shape Management**: Understanding tensor transformations
- **Practical Considerations**: Choosing architectures for specific tasks
### Next Steps
- **Training**: Learn how networks learn from data (autograd, optimization)
- **Advanced Architectures**: CNNs, RNNs, Transformers
- **Real Data**: Working with actual datasets
- **Production**: Deploying networks in real applications
**Congratulations on mastering neural network architectures!** 🚀
"""