3.3.3 · HinglishDeep Learning Frameworks

Building models with nn.Module

2,641 words12 min readRead in English

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 x

Worked 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        = 117121

Ye 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 out

Skip 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) = 10

Parameter 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.beta

Buffers kyun important hain:

  • .parameters() → sirf [gamma, beta] (2 tensors)
  • .state_dict()gamma, beta, running_mean, running_var, num_batches_tracked include karta hai (5 tensors)
  • register_buffer ke 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 nahi

Advanced 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 x

Initialization 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

Concept Map

motivates composability of

inherits from

must implement

assigns in __init__

assigns in __init__

via setattr triggers

tracks

recursively manages

enables

provides

provides

defines

nn.Module base class

Hierarchical functions

Custom network

forward method

nn.Parameter

Submodules

Registration mechanism

Automatic gradients

.to device

Train/eval modes