Pytorch Representation Learning: Part Three
Project && Guide
Table of Contents
- Section 6. Generative Adversarial Networks (DCGAN) - PyTorch Tutorial
- Section 7. Creating Your Own Modules in PyTorch
- 7.1. Libraries
- 7.2. Custom Linear module mimicking nn.Linear
- 7.3. Custom implementation of a linear layer
- 7.4. Compare outputs of nn.Linear and custom MyLinear
- 7.5. Residual linear layer example for ResNet-like skip connections
- 7.6. Test standard and residual linear layers
- 7.7. Putting things altogether, Sequential, Parameter updates
- 7.8. Caveat: CrossEntropyLoss versus NLLLoss
- 7.9. Test model loss computation
- 7.10. Updating Parameters (Manually)
- 7.11. Updating Parameters (torch.optim)
- Resources
- Project repository
Section 6. Generative Adversarial Networks (DCGAN) - PyTorch Tutorial
This post explains the code and logic for the DCGAN notebook, section by section, with annotated code blocks.
6.1. Libraries
These libraries are required for building, training, and visualizing the DCGAN model.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Import time for measuring training duration
import time
# Import numpy for numerical operations
import numpy as np
from __future__ import print_function
# Import PyTorch core modules and neural network utilities
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.init as init
import torch.nn.functional as F
from torch.autograd import Variable # For automatic differentiation
# Import torchvision for datasets and image transformations
import torchvision
import torchvision.transforms
# Import matplotlib for plotting images and results
import matplotlib.pyplot as plt
6.2. Define image transformations & Initialize datasets
We define how to preprocess the MNIST images and load them into memory for training.
1
2
3
4
5
6
# Define transformation to convert images to tensors
mnist_transforms = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
# Load MNIST training dataset with the defined transform
mnist_train = torchvision.datasets.MNIST(root='./data', train=True, transform=mnist_transforms, download=True)
# Create DataLoader for efficient batch processing and shuffling
trainloader = torch.utils.data.DataLoader(mnist_train, batch_size=64, shuffle=True, num_workers=2)
6.3. Create DCGAN Generator
The Generator network takes random noise and produces images. It uses deconvolutional layers and batch normalization.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Define the DCGAN Generator network
class Generator(nn.Module):
"""DCGAN Generator."""
def __init__(self, z_dim=128, num_filters=32):
super(Generator, self).__init__()
self.z_dim = z_dim
self.num_filters = num_filters
# 1 x 1 -> 4 x 4
self.deconv1 = nn.ConvTranspose2d(
in_channels=z_dim, out_channels=num_filters * 4,
kernel_size=(4, 4), bias=False
)
self.bn1 = nn.BatchNorm2d(num_filters * 4)
# 4 x 4 -> 8 x 8
self.deconv2 = nn.ConvTranspose2d(
in_channels=num_filters * 4, out_channels=num_filters * 2,
kernel_size=(4, 4), stride=2, padding=1, bias=False,
)
self.bn2 = nn.BatchNorm2d(num_filters * 2)
# 8 x 8 -> 16 x 16
self.deconv3 = nn.ConvTranspose2d(
in_channels=num_filters * 2, out_channels=num_filters,
kernel_size=(4, 4), stride=2, padding=1, bias=False,
)
self.bn3 = nn.BatchNorm2d(num_filters)
# 16 x 16 -> 28 x 28
self.deconv4 = nn.ConvTranspose2d(
in_channels=num_filters, out_channels=1,
kernel_size=(4, 4), stride=2, padding=3, bias=False,
)
def forward(self, x):
# Pass through deconvolutional and batchnorm layers with ReLU activations
x = F.relu(self.bn1(self.deconv1(x)))
x = F.relu(self.bn2(self.deconv2(x)))
x = F.relu(self.bn3(self.deconv3(x)))
# Final layer uses tanh activation to output image
return F.tanh(self.deconv4(x))
6.4. Create DCGAN Discriminator
The Discriminator network distinguishes real images from fake ones. It uses convolutional layers and batch normalization.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Define the DCGAN Discriminator network
class Discriminator(nn.Module):
"""DCGAN Discriminator."""
def __init__(self, num_filters=32):
super(Discriminator, self).__init__()
self.num_filters = num_filters
# 28 x 28 -> 14 x 14
self.conv1 = nn.Conv2d(
in_channels=1, out_channels=num_filters,
kernel_size=(4, 4), stride=2, padding=1, bias=False
)
self.bn1 = nn.BatchNorm2d(num_filters)
# 14 x 14 -> 7 x 7
self.conv2 = nn.Conv2d(
in_channels=num_filters, out_channels=num_filters * 2,
kernel_size=(4, 4), stride=2, padding=1, bias=False,
)
self.bn2 = nn.BatchNorm2d(num_filters * 2)
# 7 x 7 -> 3 x 3
self.conv3 = nn.Conv2d(
in_channels=num_filters * 2, out_channels=num_filters * 4,
kernel_size=(4, 4), stride=2, padding=1, bias=False,
)
self.bn3 = nn.BatchNorm2d(num_filters * 4)
# 3 x 3 -> 1 x 1
self.conv4 = nn.Conv2d(
in_channels=num_filters * 4, out_channels=1,
kernel_size=(4, 4), stride=2, padding=1, bias=False,
)
def forward(self, x):
# Pass through convolutional and batchnorm layers with LeakyReLU activations
x = F.leaky_relu(self.bn1(self.conv1(x)), 0.2)
x = F.leaky_relu(self.bn2(self.conv2(x)), 0.2)
x = F.leaky_relu(self.bn3(self.conv3(x)), 0.2)
# Final layer uses sigmoid activation to output probability
return F.sigmoid(self.conv4(x)).squeeze()
6.5. Initialize Generator & Discriminator
Instantiate the models, move them to GPU if available, and set up loss and optimizers.
1
2
3
4
5
6
7
8
9
10
11
12
# Check if CUDA (GPU) is available for faster training
cuda_available = torch.cuda.is_available()
# Instantiate generator and discriminator, move to GPU if available
generator = Generator()
discriminator = Discriminator()
if cuda_available:
generator = generator.cuda()
discriminator = discriminator.cuda()
# Set up loss function and optimizers for both networks
loss = nn.BCELoss()
optimizer_g = torch.optim.Adam(generator.parameters(), lr=2e-4)
optimizer_d = torch.optim.Adam(discriminator.parameters(), lr=2e-4)
6.6. Training Loop
The main loop alternately updates the discriminator and generator, tracking losses for each.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# Training loop for DCGAN: alternately update discriminator and generator
ctr = 0
z_dim = 128
# Track losses for monitoring
minibatch_disc_losses = []
minibatch_gen_losses = []
for epoch in range(50):
losses = []
# Train
for batch_idx, (inputs, targets) in enumerate(trainloader):
ctr += 1
if cuda_available:
inputs, targets = inputs.cuda(), targets.cuda()
# Wrap inputs and targets in Variables
inputs, targets = Variable(inputs), Variable(targets)
# Create label tensors for real and fake samples
zeros = Variable(torch.zeros(inputs.size(0)))
ones = Variable(torch.ones(inputs.size(0)))
if cuda_available:
zeros, ones = zeros.cuda(), ones.cuda()
############################
# (1) Update Discriminator
############################
# Sample z ~ N(0, 1) for generator input
minibatch_noise = Variable(torch.from_numpy(
np.random.randn(inputs.size(0), z_dim, 1, 1).astype(np.float32)
))
if cuda_available:
minibatch_noise = minibatch_noise.cuda()
# Zero gradients for the discriminator
optimizer_d.zero_grad()
# Train with real examples
d_real = discriminator(inputs)
d_real_loss = loss(d_real, ones) # Train discriminator to recognize real examples
d_real_loss.backward()
# Train with fake examples from the generator
fake = generator(minibatch_noise).detach() # Detach to prevent backpropping through the generator
d_fake = discriminator(fake)
d_fake_loss = loss(d_fake, zeros) # Train discriminator to recognize generator samples
d_fake_loss.backward()
minibatch_disc_losses.append(d_real_loss.data[0] + d_fake_loss.data[0])
# Update the discriminator
optimizer_d.step()
############################
# (2) Update Generator
############################
# Zero gradients for the generator
optimizer_g.zero_grad()
# Sample z ~ N(0, 1) for generator input
minibatch_noise = Variable(torch.from_numpy(
np.random.randn(inputs.size(0), z_dim, 1, 1).astype(np.float32)
))
if cuda_available:
minibatch_noise = minibatch_noise.cuda()
d_fake = discriminator(generator(minibatch_noise))
g_loss = loss(d_fake, ones) # Train generator to fool the discriminator into thinking these are real.
g_loss.backward()
# Update the generator
optimizer_g.step()
minibatch_gen_losses.append(g_loss.data[0])
print('Generator loss : %.3f' % (np.mean(minibatch_gen_losses)))
print('Discriminator loss : %.3f' % (np.mean(minibatch_disc_losses)))
6.7. Sample from the Generator
After training, sample images from the generator and visualize them in a grid.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Sample images from the generator and visualize them
generator.eval() # Set generator in evaluation mode to use running means and averages for Batchnorm
# Sample z ~ N(0, 1) for generator input
minibatch_noise = Variable(torch.from_numpy(
np.random.randn(16, z_dim, 1, 1).astype(np.float32)
))
if cuda_available:
minibatch_noise = minibatch_noise.cuda()
fakes = generator(minibatch_noise)
# Plot generated images in a 4x4 grid
fig = plt.figure(figsize=(10, 10))
idx = 1
for ind, fake in enumerate(fakes):
fig.add_subplot(4, 4, ind + 1)
plt.imshow(fake.data.cpu().numpy().reshape(28, 28), cmap='gray')
plt.axis('off')
Section 7. Creating Your Own Modules in PyTorch
This post explains how to create custom neural network modules in PyTorch, step by step, with annotated code blocks and explanations. The goal is to understand how to build custom neural network modules in PyTorch by subclassing nn.Module, and to learn how to implement, test, and update parameters for these modules.
Steps
- Import required libraries
- Implement a custom linear module mimicking
nn.Linear - Implement a custom linear layer from scratch
- Compare outputs of standard and custom linear layers
- Build a residual linear layer for ResNet-like skip connections
- Test standard and residual linear layers
- Build a model using Sequential and custom layers
- Compare CrossEntropyLoss and NLLLoss
- Update parameters manually and with torch.optim
7.1. Libraries
1
2
3
4
5
6
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.parameter import Parameter
import math
import numpy as np
7.2. Custom Linear module mimicking nn.Linear
This class mimics the behavior of PyTorch’s built-in nn.Linear layer, including initialization and forward pass.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Linear(nn.Module):
r"""Applies a linear transformation to the incoming data: y = Ax + b"""
def __init__(self, in_features, out_features, bias=True):
super(Linear, self).__init__()
self.in_features = in_features
self.out_features = out_features
# Weight shape: (out_features, in_features)
self.weight = Parameter(torch.Tensor(out_features, in_features))
if bias:
self.bias = Parameter(torch.Tensor(out_features))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
stdv = 1. / math.sqrt(self.weight.size(1))
self.weight.data.uniform_(-stdv, stdv)
if self.bias is not None:
self.bias.data.uniform_(-stdv, stdv)
def forward(self, input):
# Linear transformation with or without bias
if self.bias is None:
return self._backend.Linear()(input, self.weight)
else:
return self._backend.Linear()(input, self.weight, self.bias)
def __repr__(self):
return self.__class__.__name__ + ' (' \
+ str(self.in_features) + ' -> ' \
+ str(self.out_features) + ')'
7.3. Custom implementation of a linear layer
This class implements a linear layer from scratch using matrix multiplication and optional bias.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyLinear(nn.Module):
def __init__(self, in_features, out_features, bias=True):
super(MyLinear, self).__init__()
self.in_features = in_features
self.out_features = out_features
# Weight shape: (in_features, out_features)
self.weight = Parameter(torch.Tensor(in_features, out_features))
if bias:
self.bias = Parameter(torch.Tensor(out_features))
else:
self.register_parameter('bias', None)
def forward(self, input):
# Matrix multiplication and optional bias addition
if self.bias is None:
return torch.mm(input, self.weight)
else:
return torch.mm(input, self.weight) + self.bias
7.4. Compare outputs of nn.Linear and custom MyLinear
This code compares the outputs of PyTorch’s nn.Linear and the custom MyLinear for the same input and parameters.
1
2
3
4
5
6
7
8
x = torch.from_numpy(np.random.randn(2, 3)).float()
linear1 = nn.Linear(3,4)
linear2 = MyLinear(3,4)
# Set the weight and bias of linear2 to match linear1's
linear2.weight.data = linear1.weight.data.transpose(1,0)
linear2.bias.data = linear1.bias.data
# Compare outputs for the same input
print(torch.eq(linear1(x), linear2(x)))
7.5. Residual linear layer example for ResNet-like skip connections
This class implements a residual linear layer, adding skip connections for better gradient flow.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ResLinear(nn.Module):
def __init__(self, in_features, out_features, activation=nn.ReLU()):
super(ResLinear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.activation = activation
self.linear = nn.Linear(in_features, out_features)
# If input and output dimensions differ, add a projection layer
if in_features != out_features:
self.project_linear = nn.Linear(in_features, out_features)
def forward(self, x):
# Apply activation to linear output
inner = self.activation(self.linear(x))
# Use projection for skip connection if needed
if self.in_features != self.out_features:
skip = self.project_linear(x)
else:
skip = x
# Add skip connection
return inner + skip
7.6. Test standard and residual linear layers
This code tests both standard and residual linear layers for their output shapes.
1
2
3
4
5
x = torch.from_numpy(np.random.randn(2, 3)).float()
res1 = nn.Linear(3,3) # Standard linear layer
res2 = ResLinear(3,5) # Residual linear layer with projection
print(res1(x).size()) # Output shape for standard layer
print(res2(x).size()) # Output shape for residual layer
7.7. Putting things altogether, Sequential, Parameter updates
This model uses Sequential and custom layers, and includes methods for prediction and loss calculation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyModel(nn.Module):
def __init__(self, Linear=ResLinear):
super(MyModel, self).__init__()
# Build a sequence of layers for prediction
self.predict_ = nn.Sequential(
Linear(784, 328),
nn.ReLU(),
Linear(328, 328),
nn.ReLU(),
Linear(328, 10),
)
self.criterion = nn.CrossEntropyLoss()
def predict_proba(self, x):
# Softmax for probability output
return F.softmax(x)
def predict(self, x):
# Argmax for class prediction
return torch.max(self.predict_proba(x))[1]
def loss(self, x, target):
# Compute loss for training
proba = self.predict_(x)
return self.criterion(proba, target)
# CrossEntropyLoss expects pre-softmax output
# NLLLoss expects log-softmax output
7.8. Caveat: CrossEntropyLoss versus NLLLoss
CrossEntropyLosstakes in pre-softmax as inputNLLLosstakes in log-softmax as input
1
2
3
4
5
6
y = torch.Tensor(1,10).normal_() # Random output
t = torch.from_numpy(np.random.choice(10, size=1)) # Random target
loss1 = nn.CrossEntropyLoss()
loss2 = nn.NLLLoss()
print(loss1(y, t)) # CrossEntropyLoss on raw output
print(loss2(nn.LogSoftmax(dim=1)(y), t)) # NLLLoss on log-softmax output
7.9. Test model loss computation
1
2
3
4
x = torch.from_numpy(np.random.randn(64, 784)).float() # Random input
t = torch.from_numpy(np.random.choice(10, size=64)) # Random target
model = MyModel()
print(model.loss(x, t)) # Print loss value
7.10. Updating Parameters (Manually)
This code demonstrates manual parameter updates using gradient descent.
1
2
3
4
5
6
7
8
9
10
11
12
x = torch.from_numpy(np.random.randn(64, 784)).float() # Random input
t = torch.from_numpy(np.random.choice(10, size=64)) # Random target
model = MyModel()
lr = 0.1 # Learning rate
for i in range(10):
loss = model.loss(x, t)
loss.backward()
for param in model.parameters():
# Update parameters manually using gradient descent
param.data.sub_(param.grad.data*lr)
param.grad.data.zero_()
print(param.grad) # Print gradients after update
7.11. Updating Parameters (torch.optim)
This code demonstrates parameter updates using PyTorch’s built-in optimizer.
1
2
3
4
5
6
7
8
9
10
11
x = torch.from_numpy(np.random.randn(64, 784)).float() # Random input
t = torch.from_numpy(np.random.choice(10, size=64)) # Random target
model = MyModel()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
for i in range(10):
optimizer.zero_grad()
loss = model.loss(x, t)
loss.backward()
optimizer.step()
print(loss) # Print loss after optimizer step
# Momentum helps accelerate updates in the relevant direction
Resources
Project repository
GitHub Code: PyTorch Representation Learning Project

