Building models with nn.Module
3.3.3· AI-ML › Deep Learning Frameworks
nn.Module kya hai?
Functions ki jagah Subclass kyun banayein?
Function approach (naive):
def my_layer(x, w, b):
return x @ w + b❌ Problems:
- Parameters (w, b) ko har jagah explicitly pass karna padta hai
- Automatic gradient tracking nahi hoti
- Models ko easily save/load nahi kar sakte
- Train/eval modes ke beech switch karne ka koi tarika nahi
Module approach:
class MyLayer(nn.Module):
def __init__(self, in_features, out_features):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_features, out_features))
self.bias = nn.Parameter(torch.randn(out_features))
def forward(self, x):
return x @ self.weight + self.bias✅ Benefits:
- Parameters andar stored hain, gradient tracking automatic hai
.parameters()call karke sabhi learnable tensors mil jaate hain.to(device)se sab kuch move ho jaata hai- Doosre modules ke saath composable hai
Core Architecture Pattern
Custom module banane ka standard pattern:
import torch.nn as nn
class MyNetwork(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
# Step 1: Parent constructor call karo
super().__init__() # or super(MyNetwork, self).__init__()
# Step 2: Submodules aur parameters define karo
self.layer1 = nn.Linear(input_dim, hidden_dim)
self.activation = nn.ReLU()
self.layer2 = nn.Linear(hidden_dim, output_dim)
# Step 3: (Optional) Custom parameters
self.custom_param = nn.Parameter(torch.randn(10))
def forward(self, x):
# Step 4: Computation graph define karo
x = self.layer1(x)
x = self.activation(x)
x = self.layer2(x)
return xWorked Example 1: Simple Feedforward Network
Task: Binary classification ke liye 3-layer feedforward network banao.
import torch
import torch.nn as nn
class SimpleClassifier(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
# nn.Linear kyun, manual weight matrices kyun nahi?
# nn.Linear initialization (Kaiming/Xavier) aur bias sahi se handle karta hai
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
self.fc3 = nn.Linear(hidden_dim, output_dim)
# ReLU ko attribute ke roop mein kyun?
# Dropout/BatchNorm jaise modes ke liye, attribute ke roop mein store karna train/eval switching enable karta hai
# ReLU (stateless) ke liye, ye style preference hai, par consistent pattern hai
self.relu = nn.ReLU()
def forward(self, x):
# x: [batch_size, input_dim]
# Ye step kyun? Linear transform + nonlinearity
x = self.relu(self.fc1(x)) # [batch_size, hidden_dim]
# Doosra hidden layer kyun? Complex boundaries ke liye model capacity badhata hai
x = self.relu(self.fc2(x)) # [batch_size, hidden_dim]
# Output par activation kyun nahi? Loss function (BCEWithLogitsLoss) logits expect karta hai
x = self.fc3(x) # [batch_size, output_dim]
return x
# Usage
model = SimpleClassifier(input_dim=784, hidden_dim=128, output_dim=1)
# Ye kyun kaam karta hai: Forward automatically call hota hai
input_data = torch.randn(32, 784) # Batch of 32 images (28×28 flattened)
output = model(input_data) # model.forward(input_data) ko __call__ ke zariye call karta hai
print(f"Parameters: {sum(p.numel() for p in model.parameters())}")
# Output: Parameters: 117121
# Kyun? (784×128 + 128) + (128×128 + 128) + (128×1 + 1)
# = 100480 + 16512 + 129 = 117121Ye step kyun? Parameter count ka breakdown:
- Layer 1 (fc1): Weight matrix parameters, bias parameters → subtotal
- Layer 2 (fc2): Weight matrix parameters, bias parameters → subtotal
- Layer 3 (fc3): Weight matrix parameters, bias parameter → subtotal
- Total: parameters ✅
Worked Example 2: Residual Block with Skip Connections
Task: ResNet-style residual block implement karo:
class ResidualBlock(nn.Module):
def __init__(self, dim):
super().__init__()
# F(x) mein do layers kyun? Standard residual block pattern hai
self.conv1 = nn.Conv2d(dim, dim, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(dim)
self.conv2 = nn.Conv2d(dim, dim, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(dim)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
# Skip connection ke liye input save karo
identity = x # [batch, dim, height, width]
# Ye path kyun? F(x) = BN(Conv(BN(Conv(x))))
out = self.conv1(x) # [batch, dim, H, W]
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out) # [batch, dim, H, W]
out = self.bn2(out)
# Final ReLU se pehle add kyun? Gradient ko skip connection ke through flow karne deta hai
# Derivation: ∂Loss/∂x = ∂Loss/∂out × (∂F(x)/∂x + ∂identity/∂x)
# = ∂Loss/∂out × (∂F(x)/∂x + I)
# +I term vanishing gradients ko rokta hai!
out = out + identity
out = self.relu(out)
return outSkip connections mathematically kyun?
Skip ke bina:
Skip ke saath:
"+1" ensure karta hai ki gradient magnitude ≥ 1 rahe, chahe ho, jo deep networks mein vanishing gradients solve karta hai.
Worked Example 3: Variable-Length Module Lists
Task: Ek aisa network banao jiska depth initialization par specify kiya ja sake.
class DeepNetwork(nn.Module):
def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
super().__init__()
# nn.ModuleList kyun, Python list kyun nahi?
# Python list: layers = [nn.Linear(...), nn.Linear(...)]
# Problem: Parameters register NAHI hote, train nahi honge!
# Ye pattern kyun? Pehli layer dimension change karti hai
self.input_layer = nn.Linear(input_dim, hidden_dim)
# nn.ModuleList kyun? Contained sabhi modules automatically register karta hai
self.hidden_layers = nn.ModuleList([
nn.Linear(hidden_dim, hidden_dim)
for _ in range(num_layers)
])
self.output_layer = nn.Linear(hidden_dim, output_dim)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.input_layer(x))
# ModuleList ke through iterate kyun? Har hidden layer sequentially apply karo
for layer in self.hidden_layers:
x = self.relu(layer(x))
x = self.output_layer(x)
return x
# Registration verify karo
model = DeepNetwork(input_dim=10, hidden_dim=64, num_layers=5, output_dim=2)
print(f"Registered modules: {len(list(model.modules()))}")
# Output: Registered modules: 10
# Kyun? 1 (root) + 1 (input_layer) + 1 (ModuleList container) + 5 (hidden layers)
# + 1 (output_layer) + 1 (ReLU) = 10Parameter vs. Buffer Registration
class NormalizationLayer(nn.Module):
def __init__(self, num_features):
super().__init__()
# Learnable parameters
self.gamma = nn.Parameter(torch.ones(num_features))
self.beta = nn.Parameter(torch.zeros(num_features))
# Non-learnable buffers (running statistics)
# register_buffer kyun? Ye .to(device) ke saath move hone chahiye aur state_dict ke saath save hone chahiye
# par optimizer dwara train NAHI hone chahiye
self.register_buffer('running_mean', torch.zeros(num_features))
self.register_buffer('running_var', torch.ones(num_features))
self.register_buffer('num_batches_tracked', torch.tensor(0))
def forward(self, x):
if self.training:
# Running statistics update karo (exponential moving average)
batch_mean = x.mean(dim=0)
batch_var = x.var(dim=0)
# 0.1 momentum kyun? Responsiveness aur stability ke beech balance
self.running_mean = 0.9 * self.running_mean + 0.1 * batch_mean
self.running_var = 0.9 * self.running_var + 0.1 * batch_var
# Batch statistics se normalize karo
x_norm = (x - batch_mean) / torch.sqrt(batch_var + 1e-5)
else:
# Eval mein running stats kyun? Inference par batch statistics available nahi hoti
x_norm = (x - self.running_mean) / torch.sqrt(self.running_var + 1e-5)
# Affine transform kyun? Network ko normalization undo karne ki suvidha deta hai agar zarurat ho
return self.gamma * x_norm + self.betaBuffers kyun important hain:
.parameters()→ sirf[gamma, beta](2 tensors).state_dict()→gamma, beta, running_mean, running_var, num_batches_trackedinclude karta hai (5 tensors)register_bufferke bina, running statistics model save karne par kho jaati!
Module Lifecycle
model = SimpleClassifier(784, 128, 10)
# Training loop
model.train() # Sabhi submodules ke liye self.training = True set karta hai
for batch in train_loader:
optimizer.zero_grad()
output = model(batch) # Dropout active, BatchNorm running stats update karta hai
loss = criterion(output, targets)
loss.backward()
optimizer.step()
# Evaluation
model.eval() # Sabhi submodules ke liye self.training = False set karta hai
with torch.no_grad(): # Gradient computation disable karta hai (memory bachata hai)
for batch in test_loader:
output = model(batch) # Dropout off, BatchNorm running stats use karta hai
# Koi backward pass nahiAdvanced Pattern: Custom Initialization
class CustomInitNetwork(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, output_dim)
# Custom initialization kyun? Default tumhari problem ke liye suit nahi kar sakta
self._initialize_weights()
def _initialize_weights(self):
# modules() iterate kyun? Sabhi submodules recursively milte hain
for m in self.modules():
if isinstance(m, nn.Linear):
# Linear layers ke liye Xavier initialization
# Xavier kyun? Layers ke across variance maintain karta hai
# Derivation: Linear layer Y = WX ke liye, Var(Y) = n_in × Var(W) × Var(X)
# Var(Y) = Var(X) rakhne ke liye, Var(W) = 1/n_in chahiye
nn.init.xavier_uniform_(m.weight)
# Constant bias kyun? Koi bias shift ke bina start karo
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Conv2d):
# ReLU activations ke liye He initialization
# He kyun? ReLU aadhe activations zero kar deta hai, √2 scaling chahiye
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return xInitialization derivation:
Xavier ke liye: Given jahan
maintain karne ke liye, chahiye.
Uniform distribution: ,
Set karo
Isliye:
Recall Ek 12 saal ke bacche ko samjhao
Socho tum ek LEGO castle bana rahe ho. Har LEGO piece ek nn.Module hai:
- Chhoti bricks (jaise
nn.Linear) simple pieces hain jo ek kaam karti hain - Wall sections (jaise ek ResidualBlock) chhoti bricks combine karke banti hain
- Towers (jaise ek CNN