INT422Semester 7 • 3 CreditsElective Subjects

Deep Learning

This course develops skills in building and optimizing deep learning models using TensorFlow and Keras. It covers NVIDIA DGX Station A100, convolutional and recurrent neural networks, autoencoders, generative adversarial networks, and model deployment using Docker and Streamlit.

6 Units • Notes • Model Answers • MCQs
Select Course Unit (6 Units)
Active: Unit 1
Unit 1:Unit 1: Building Models with TensorFlow
7 min read•High-Yield Unit Guide

Unit 1: Building Models with TensorFlow

Unit 1 Exam Focus & Core Takeaways

Review the core definitions, standard architectures, and syntax patterns below. Pay special attention to sections highlighted with bullet points for your mid-term and end-term subjective questions.

Unit 1: Building Models with TensorFlow and High-Performance Compute

1. Foundational Architecture of Deep Learning and Tensor Computing

Deep Learning is a subset of machine learning based on artificial neural networks with representation learning. Unlike classical machine learning workflows that rely on handcrafted feature engineering, deep neural networks learn hierarchical representations directly from raw data through compositions of linear transformations and non-linear activation functions.

CODE
+-----------------------------------------------------------------------------+
|             THE HIERARCHICAL REPRESENTATION LEARNING PARADIGM               |
+-----------------------------------------------------------------------------+
|                                                                             |
|   RAW INPUT           LOW-LEVEL FEATURES       MID-LEVEL FEATURES           |
|   [Pixel Grid] -----> [Edges & Gradients] ---> [Textures & Motifs]          |
|                                                              |              |
|                                                              v              |
|   FINAL PREDICTION      CLASSIFICATION HEAD    HIGH-LEVEL FEATURES          |
|   [Class Probability] <--- [Softmax / Logits] <--- [Parts & Objects]        |
|                                                                             |
+-----------------------------------------------------------------------------+

1.1 Hardware Acceleration: NVIDIA DGX Station A100 Architecture

Training modern deep learning architectures requires specialized tensor acceleration hardware. The NVIDIA DGX Station A100 represents the premier AI workstation infrastructure, powered by four fully interconnected NVIDIA A100 Tensor Core GPUs:

CODE
+-----------------------------------------------------------------------------+
|               NVIDIA DGX STATION A100 HARDWARE SPECIFICATIONS               |
+-----------------------------------------------------------------------------+
| COMPONENT              | ARCHITECTURAL SPECIFICATION                        |
+------------------------+----------------------------------------------------+
| Microarchitecture      | NVIDIA Ampere Architecture (GA100 Die)            |
| GPUs Installed         | 4x NVIDIA A100 Tensor Core GPUs                    |
| GPU Memory             | 320 GB total HBM2e (80 GB per GPU @ 2.0 TB/s BW)   |
| Interconnect           | 3rd Generation NVLink (200 GB/s bidirectional BW)  |
| Peak FP64 Tensor Core  | 38.8 TFLOPS                                        |
| Peak TensorFloat-32    | 624 TFLOPS (dense) / 1,248 TFLOPS (structural spar)|
| Peak Bfloat16 / FP16   | 1,248 TFLOPS (dense) / 2,496 TFLOPS (sparse)       |
| Host Processor         | AMD EPYC 7742 (64 Cores, 128 Threads, 2.25 GHz)    |
| System Memory          | 512 GB DDR4 ECC RAM                                |
+-----------------------------------------------------------------------------+

TensorFloat-32 (TF32) and Mixed Precision:

  • TensorFloat-32 (TF32): The Ampere architecture introduced TF32, which uses the same 8-bit exponent as IEEE FP32 (matching its dynamic range) and the 10-bit precision of FP16 (matching its throughput). It accelerates FP32 convolution and matrix multiplication operations by up to 10×10\times with zero code modifications.
  • Mixed Precision (FP16/BF16FP16 / BF16): Combining 16-bit half-precision floating-point weights and activations with 32-bit master weight copies and loss scaling (tf.keras.mixed_precision.set_global_policy('mixed_float16')) halves memory footprints and doubles throughput on Tensor Cores.

2. Tensor Fundamentals, Manipulation, and Math Operations

In TensorFlow, a Tensor is a generalized multidimensional array of homogeneous data types. Tensors serve as the primary mathematical data structure for representations, weights, biases, activations, and gradient vectors.

CODE
+-----------------------------------------------------------------------------+
|                         TENSOR RANK TAXONOMY & EXAMPLES                     |
+-----------------------------------------------------------------------------+
| RANK | MATHEMATICAL ENTITY | SHAPE EXAMPLE | DATA REPRESENTATION            |
+------+---------------------+---------------+--------------------------------+
|  0   | Scalar              | `[]`          | Single number: `tf.constant(5)`|
|  1   | Vector              | `[D]`         | 1D array: Audio sample, bias   |
|  2   | Matrix              | `[R, C]`      | Dense layer weights, tabular   |
|  3   | 3D Tensor           | `[B, T, D]`   | NLP sequence: batch, time, dim |
|  4   | 4D Tensor           | `[B, H, W, C]`| Image batch: batch, H, W, chan |
|  5   | 5D Tensor           | `[B, F, H, W,C| Video batch: batch, frames, ...|
+------+---------------------+---------------+--------------------------------+

2.1 Immutable Constants vs Mutable Variables

TensorFlow strictly differentiates between static constants and trainable parameters:

python
import tensorflow as tf

# 1. Immutable Constant Tensor
c = tf.constant([[1.0, 2.0], [3.0, 4.0]], dtype=tf.float32)
# Attempting c[0, 0] = 5.0 will raise a TypeError: 'Tensor' object does not support item assignment

# 2. Mutable Trainable Variable Tensor (Weights and Biases)
v = tf.Variable([[0.5, -0.2], [0.1, 0.9]], dtype=tf.float32, name="kernel_weights")
v.assign([[1.0, 1.0], [0.0, 0.0]])      # In-place value reassignment
v.assign_add([[0.1, 0.1], [0.1, 0.1]])  # In-place increment (v = v + delta)
v.assign_sub([[0.05, 0.05], [0.0, 0.0]])# In-place decrement (gradient descent step)

2.2 Tensor Mathematical Operations and Broadcasting

TensorFlow provides highly optimized CUDA-accelerated kernels for linear algebra:

python
# Element-wise operations
a = tf.constant([1.0, 2.0, 3.0])
b = tf.constant([4.0, 5.0, 6.0])
add_res = tf.add(a, b)       # [5.0, 7.0, 9.0] or a + b
mul_res = tf.multiply(a, b)  # [4.0, 10.0, 18.0] or a * b

# Matrix Multiplication (Dot Product)
mat_a = tf.constant([[1.0, 2.0], [3.0, 4.0]]) # Shape [2, 2]
mat_b = tf.constant([[5.0, 6.0], [7.0, 8.0]]) # Shape [2, 2]
mat_mul = tf.matmul(mat_a, mat_b)              # or mat_a @ mat_b
# Result: [[1*5 + 2*7, 1*6 + 2*8], [3*5 + 4*7, 3*6 + 4*8]] = [[19.0, 22.0], [43.0, 50.0]]

Broadcasting Rules:

Two tensors are compatible for element-wise operations if, starting from trailing dimensions, their dimensions are equal or one of them is 1:

  • Tensor A of shape [32, 100, 64] and Tensor B of shape [64] broadcast to [32, 100, 64].
  • Tensor A of shape [128, 1] and Tensor B of shape [1, 512] broadcast to [128, 512].

2.3 Reshaping, Squeezing, and Dimension Expansion

python
x = tf.range(12) # [0, 1, 2, ..., 11], Shape [12]

# Reshape into 3x4 matrix
x_2d = tf.reshape(x, shape=(3, 4))

# Expand Dimensions: Add a batch dimension
x_batched = tf.expand_dims(x_2d, axis=0) # Shape: [1, 3, 4]

# Squeeze Dimensions: Remove singleton dimensions
x_squeezed = tf.squeeze(x_batched, axis=0) # Shape: [3, 4]

# Transpose dimensions
x_trans = tf.transpose(x_2d, perm=[1, 0]) # Shape: [4, 3]

3. Computation Execution Paradigms: Eager vs Graph Execution

TensorFlow 2.x unified interactive developer velocity with production graph optimization by seamlessly integrating Eager Execution and AutoGraph (@tf.function).

CODE
+-----------------------------------------------------------------------------+
|                 EAGER EXECUTION VS GRAPH EXECUTION COMPARISON               |
+-----------------------------------------------------------------------------+
| CRITERION             | EAGER EXECUTION            | GRAPH EXECUTION        |
+-----------------------+----------------------------+------------------------+
| Execution Model       | Imperative, step-by-step   | Declarative, compiled  |
|                       | immediate evaluation       | computational graph    |
+-----------------------+----------------------------+------------------------+
| Debugging             | Standard Python debuggers  | Abstract graph traces; |
|                       | (pdb, print statements)    | requires tf.print()    |
+-----------------------+----------------------------+------------------------+
| Performance           | Python interpreter overhead| Maximum throughput,    |
|                       | on every kernel launch     | kernel fusion, XLA opt |
+-----------------------+----------------------------+------------------------+
| Portability           | Requires Python runtime    | Exportable as SavedModel|
|                       | environment                | (C++, TF Serving, Edge)|
+-----------------------------------------------------------------------------+

3.1 The AutoGraph Engine (@tf.function)

Decorating a Python function with @tf.function instructs TensorFlow to capture the function's tensor operations and construct a high-performance Static Computation Graph:

python
@tf.function
def compute_forward_pass(x, w, b):
    # Operations are traced on first invocation to construct the graph
    z = tf.matmul(x, w) + b
    a = tf.nn.relu(z)
    return a

Graph Construction and Polymorphic Tracing:

  1. Tracing Phase: When invoked with specific tensor shapes and dtypes (TensorSpec), TensorFlow executes the Python code once, replacing tensors with symbolic placeholders to record an execution DAG (Directed Acyclic Graph).
  2. Execution Phase: Subsequent invocations with tensors of matching shape and dtype bypass Python entirely, dispatching pre-compiled native CUDA kernels directly to GPU hardware.
  3. Python Side Effects Hazard: Code containing non-TensorFlow side effects (e.g., print(), Python list.append(), or random.random()) only runs during the initial trace. Schedulers must use tf.print() and TensorFlow primitives within @tf.function.

4. Automatic Differentiation with tf.GradientTape

Modern deep learning optimization relies on the Backpropagation Algorithm, which requires computing partial derivatives of a scalar loss function with respect to millions of network weight parameters. TensorFlow accomplishes this automatically via Reverse-Mode Automatic Differentiation managed by tf.GradientTape.

CODE
+-----------------------------------------------------------------------------+
|             REVERSE-MODE AUTOMATIC DIFFERENTIATION ARCHITECTURE             |
+-----------------------------------------------------------------------------+
|                                                                             |
|   FORWARD PASS (Recorded on Tape):                                          |
|   x -------> [ Matrix Mul (W) ] ---> z ---> [ Activation (σ) ] ---> a       |
|                      ^                                                      |
|   W -----------------+                                                      |
|                                                                             |
|   BACKWARD PASS (Reverse Trace via Chain Rule):                             |
|   dL/dW <--- [ Vector-Jacobian ] <-- dL/dz <--- [ Derivative σ' ] <--- dL/da |
|                                                                             |
+-----------------------------------------------------------------------------+

4.1 Mechanics of tf.GradientTape

During the forward pass executed inside the with tf.GradientTape() as tape: context, the tape dynamically records all mathematical operations performed on watched tensors:

  • By default, all initialized tf.Variable objects are automatically watched.
  • Constant tensors must be explicitly monitored using tape.watch(c).
python
x = tf.Variable(3.0, dtype=tf.float32)

with tf.GradientTape() as tape:
    y = x**3 + 2.0 * x**2 - 5.0 * x + 7.0

# Compute derivative dy/dx: d/dx(x^3 + 2x^2 - 5x + 7) = 3x^2 + 4x - 5
# Evaluated at x = 3: 3(9) + 4(3) - 5 = 27 + 12 - 5 = 34.0
dy_dx = tape.gradient(y, x)
print(dy_dx) # tf.Tensor(34.0, shape=(), dtype=float32)

4.2 Non-Persistent vs Persistent Tapes

By default, calling tape.gradient() immediately releases the internal tape memory buffers:

python
w = tf.Variable(2.0)
b = tf.Variable(1.0)

# 1. Non-persistent tape (Default): gradient can only be computed ONCE
with tf.GradientTape() as tape:
    loss = (w * 3.0 + b) ** 2

grad_w, grad_b = tape.gradient(loss, [w, b]) # Correct: Computes both simultaneously

# 2. Persistent tape: Allows multiple sequential gradient evaluations
with tf.GradientTape(persistent=True) as p_tape:
    loss_a = w ** 2
    loss_b = b ** 3

grad_a = p_tape.gradient(loss_a, w)
grad_b = p_tape.gradient(loss_b, b)
del p_tape # Explicitly delete persistent tape to release GPU memory resources

4.3 Higher-Order Derivatives via Nested Tapes

To calculate the Hessian matrix or second-order derivatives:

python
x = tf.Variable(2.0)

with tf.GradientTape() as outer_tape:
    with tf.GradientTape() as inner_tape:
        y = x**4 # y = x^4
    # First derivative: dy/dx = 4x^3 = 4(8) = 32.0
    first_derivative = inner_tape.gradient(y, x)

# Second derivative: d^2y/dx^2 = 12x^2 = 12(4) = 48.0
second_derivative = outer_tape.gradient(first_derivative, x)
print(second_derivative) # tf.Tensor(48.0, shape=(), dtype=float32)

5. Building Neural Models from Scratch Using Low-Level Primitives

To understand deep learning mechanics before relying on high-level Keras abstractions, engineers must master building neural networks using bare tf.Variable, tf.GradientTape, and manual matrix mathematics.

5.1 Mathematical Model of a Dense Layer

For an input tensor X∈RB×DinX \in \mathbb{R}^{B \times D_{\text{in}}} (Batch size BB, Input features DinD_{\text{in}}), a dense linear layer parameterized by weights W∈RDin×DoutW \in \mathbb{R}^{D_{\text{in}} \times D_{\text{out}}} and bias vector b∈RDoutb \in \mathbb{R}^{D_{\text{out}}} computes:

Z=XW+bZ = X W + b

Y^=σ(Z)\hat{Y} = \sigma(Z)

Where σ(⋅)\sigma(\cdot) represents a non-linear activation function (e.g., ReLU, Sigmoid, or Softmax).

5.2 Complete Low-Level Implementation: Binary Logistic Regression

python
import tensorflow as tf

class LowLevelDenseLayer:
    def __init__(self, in_features, out_features, activation=None):
        # Glorot (Xavier) Uniform Weight Initialization
        limit = tf.sqrt(6.0 / (in_features + out_features))
        self.w = tf.Variable(tf.random.uniform([in_features, out_features], -limit, limit))
        self.b = tf.Variable(tf.zeros([out_features]))
        self.activation = activation

    def forward(self, x):
        z = tf.matmul(x, self.w) + self.b
        if self.activation == "relu":
            return tf.maximum(0.0, z)
        elif self.activation == "sigmoid":
            return tf.sigmoid(z)
        return z

def binary_cross_entropy_loss(y_true, y_pred):
    # Numerical stability epsilon clip
    eps = 1e-7
    y_pred = tf.clip_by_value(y_pred, eps, 1.0 - eps)
    return -tf.reduce_mean(y_true * tf.math.log(y_pred) + (1.0 - y_true) * tf.math.log(1.0 - y_pred))

# Instantiate Model
layer1 = LowLevelDenseLayer(in_features=10, out_features=16, activation="relu")
layer2 = LowLevelDenseLayer(in_features=16, out_features=1, activation="sigmoid")
all_trainable_vars = [layer1.w, layer1.b, layer2.w, layer2.b]

learning_rate = 0.05

@tf.function
def train_step(x_batch, y_batch):
    with tf.GradientTape() as tape:
        # Forward pass
        h1 = layer1.forward(x_batch)
        y_hat = layer2.forward(h1)
        loss = binary_cross_entropy_loss(y_batch, y_hat)
    
    # Backward pass: Compute partial derivatives
    gradients = tape.gradient(loss, all_trainable_vars)
    
    # Stochastic Gradient Descent Optimization Step: theta = theta - lr * grad
    for var, grad in zip(all_trainable_vars, gradients):
        var.assign_sub(learning_rate * grad)
        
    return loss

6. High-Performance Input Pipelines with tf.data

Training modern deep architectures on high-throughput GPUs (like the A100) often encounters an Input Pipeline Starvation Bottleneck: while the GPU finishes a backward pass in milliseconds, the CPU struggles to read, decode, augment, and batch files from disk, leaving the GPU idle.

The tf.data API builds ETL (Extract, Transform, Load) pipelines that run concurrently with GPU computation.

CODE
+-----------------------------------------------------------------------------+
|             THE ASYNCHRONOUS PIPELINING ARCHITECTURE WITH PREFETCH          |
+-----------------------------------------------------------------------------+
|                                                                             |
|   WITHOUT PREFETCH (Sequential Starvation):                                 |
|   CPU: [ Prepare Batch 1 ] --------------> [ Prepare Batch 2 ] -----------> |
|   GPU: ------------------> [ Train Batch 1 ] -----------------> [ Train B2 ]|
|   (GPU sits IDLE waiting for CPU preparation)                               |
|                                                                             |
|   WITH PREFETCH & AUTOTUNE (Asynchronous Overlap):                          |
|   CPU: [ Prepare B1 ] [ Prepare B2 ] [ Prepare B3 ]                         |
|   GPU:                [ Train B1   ] [ Train B2   ] [ Train B3   ]          |
|   (100% GPU Hardware Saturation - Zero Idle Bubbles)                        |
|                                                                             |
+-----------------------------------------------------------------------------+

6.1 Building an Enterprise tf.data Pipeline

python
import tensorflow as tf

def build_optimized_pipeline(features, labels, batch_size=64):
    # 1. Create dataset from memory or disk tensors
    dataset = tf.data.Dataset.from_tensor_slices((features, labels))
    
    # 2. Shuffle before batching with an adequate buffer size
    # Buffer size should be larger than batch size (ideally full dataset if memory permits)
    dataset = dataset.shuffle(buffer_size=10000, seed=42, reshuffle_each_iteration=True)
    
    # 3. Apply transformation & feature engineering in parallel across CPU cores
    dataset = dataset.map(
        map_func=lambda x, y: (tf.cast(x, tf.float32) / 255.0, y),
        num_parallel_calls=tf.data.AUTOTUNE # Automatically tunes thread allocation
    )
    
    # 4. Batch items together (drop remainder prevents irregular batch sizes)
    dataset = dataset.batch(batch_size, drop_remainder=True)
    
    # 5. Prefetch: Prepares subsequent batches on CPU while GPU trains current batch
    dataset = dataset.prefetch(buffer_size=tf.data.AUTOTUNE)
    
    return dataset

7. Model Serialization, Docker Containerization, and Deployment

7.1 The TensorFlow SavedModel Format

The SavedModel format (saved_model.pb, variables/, and assets/) is the language-neutral, hermetic serialization standard for TensorFlow architectures:

  • Contains the complete execution graph, model architecture, and trained numerical weights.
  • Can be loaded and executed in C++, Go, Java, or Python runtimes without access to the original Python source code definitions.
python
# Save model
tf.saved_model.save(model, export_dir="models/production_v1")

# Load model in a production serving environment
imported_model = tf.saved_model.load("models/production_v1")
inference_fn = imported_model.signatures["serving_default"]
predictions = inference_fn(tf.constant(test_features))

7.2 Containerized Serving with Docker and Streamlit

In production, models are packaged into immutable Docker containers running alongside lightweight inference UIs built in Streamlit:

dockerfile
# Production Dockerfile for Deep Learning Model Serving
FROM python:3.10-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Install TensorFlow and Streamlit
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy serialized model artifacts and Streamlit inference application
COPY models/ ./models/
COPY app.py .

EXPOSE 8501

ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
python
# app.py - Lightweight Streamlit Serving App
import streamlit as st
import tensorflow as tf
import numpy as np

st.set_page_config(page_title="Deep Learning Inference Engine", layout="centered")
st.title("Neural Prediction Service")

@st.cache_resource
def load_serving_model():
    return tf.saved_model.load("models/production_v1")

model = load_serving_model()

user_input = st.text_input("Enter feature vector (comma-separated):", "1.2, 0.4, -0.8, 2.1")
if st.button("Generate Inference"):
    features = np.array([[float(x.strip()) for x in user_input.split(",")]], dtype=np.float32)
    pred = model(tf.constant(features))
    st.success(f"Model Prediction: {pred.numpy()[0]}")