📚

Introduction - How to Use This Tutorial

🎯 Welcome!

This comprehensive tutorial is your complete guide to Python programming for the ELEC 395 AI Lab course. Whether you're completely new to programming or need a refresher, this guide covers everything you'll need for all 13 laboratory experiments.

📖 What You'll Learn

  • Python Fundamentals: Variables, data types, control flow, functions, and object-oriented programming
  • Scientific Computing: NumPy, Pandas, and data manipulation for machine learning
  • Deep Learning: PyTorch and TensorFlow for building neural networks
  • Computer Vision: OpenCV for image processing and real-time applications
  • Hardware Integration: Interfacing with Jetson Orin Nano and sensors
  • Autonomous Systems: Robot control, navigation, and decision-making algorithms
  • Edge AI: Deploying models on embedded systems

💡 How to Use This Tutorial

  • Start from the Beginning: If you're new to Python, begin with Chapter 0 (Python Basics)
  • Follow Your Lab: Jump to the chapter that corresponds to your current lab week
  • Practice Actively: Type out the code examples yourself - don't just read them
  • Use Quick Reference: Bookmark the Quick Reference section for fast lookup during labs
  • Experiment: Modify the examples and see what happens - experimentation is learning!
  • Troubleshoot: Check the Troubleshooting section when you encounter errors

🚀 Learning Tips for Success

  • Code Every Day: Even 15 minutes of daily practice is better than marathon sessions
  • Read Error Messages: Errors are your teachers - they tell you exactly what's wrong
  • Use Print Statements: Add print() to understand what your code is doing
  • Google is Your Friend: Professional programmers search for solutions constantly
  • Ask for Help: Don't struggle alone - ask instructors, TAs, or classmates
📊
Course Progress Map: This tutorial covers 13 lab experiments across 14 weeks. Each chapter builds on previous knowledge, so concepts learned early will be used throughout the course.
🌱

Chapter 0: Python Crash Course for Absolute Beginners

↑ Go Up

Before diving into AI and machine learning, let's master Python fundamentals. This chapter assumes you have ZERO programming experience and will teach you everything you need to know.

🔹 Variables and Data Types

Variables are containers for storing data. Think of them as labeled boxes where you put information.

📝 Basic Variables
# Creating variables (no need to declare type)
name = "Mohammad"           # String (text)
age = 25                   # Integer (whole number)
gpa = 3.85                 # Float (decimal number)
is_student = True         # Boolean (True/False)

# Printing variables
print(name)              # Output: Mohammad
print(f"Age: {age}")     # Output: Age: 25
print(f"GPA: {gpa}")     # Output: GPA: 3.85

📊 Common Data Types

Type Description Example
int Whole numbers 42, -17, 0
float Decimal numbers 3.14, -0.5, 2.0
str Text (must be in quotes) "Hello", 'Python'
bool True or False True, False

🔹 Lists - Storing Multiple Items

Lists are ordered collections that can store multiple values. They're like arrays in other languages.

📝 Working with Lists
# Creating a list
sensors = ["camera", "lidar", "ultrasonic", "IMU"]
temperatures = [25.3, 26.1, 24.8, 25.5]

# Accessing items (indexing starts at 0!)
first_sensor = sensors[0]        # "camera"
last_sensor = sensors[-1]        # "IMU" (negative index from end)

# Adding items
sensors.append("GPS")          # Add to end
sensors.insert(1, "radar")    # Insert at position 1

# Removing items
sensors.remove("lidar")        # Remove by value
removed = sensors.pop()         # Remove and return last item

# List operations
num_sensors = len(sensors)     # Get length
print(sensors)                  # Print entire list

🔹 Dictionaries - Key-Value Pairs

Dictionaries store data in key-value pairs. Perfect for organizing related information.

📝 Using Dictionaries
# Creating a dictionary
robot = {
    "name": "JetBot",
    "speed": 0.5,
    "battery": 85,
    "sensors": ["camera", "ultrasonic"]
}

# Accessing values
robot_name = robot["name"]           # "JetBot"
battery_level = robot["battery"]     # 85

# Modifying values
robot["battery"] = 90            # Update existing key
robot["location"] = "Lab"       # Add new key

# Dictionary methods
keys = robot.keys()               # Get all keys
values = robot.values()           # Get all values
items = robot.items()             # Get key-value pairs

🔹 Control Flow - Making Decisions

📝 If-Else Statements
# Simple if statement
battery = 20
if battery < 25:
    print("Low battery! Return to charging station")

# If-else statement
if battery > 50:
    print("Battery OK")
else:
    print("Battery low")

# If-elif-else (multiple conditions)
if battery > 75:
    print("Battery high")
elif battery > 25:
    print("Battery medium")
else:
    print("Battery low")

🔹 Loops - Repeating Actions

📝 For Loops
# Loop through a list
sensors = ["camera", "lidar", "ultrasonic"]
for sensor in sensors:
    print(f"Checking {sensor}...")

# Loop with range (0 to 4)
for i in range(5):
    print(f"Iteration {i}")

# Loop with start and end
for i in range(1, 10, 2):  # start=1, end=10, step=2
    print(i)  # Output: 1, 3, 5, 7, 9
📝 While Loops
# While loop - continues until condition is False
distance = 100
while distance > 0:
    print(f"Distance to target: {distance}m")
    distance -= 10  # Same as: distance = distance - 10
    
# Break and continue
counter = 0
while True:  # Infinite loop
    counter += 1
    if counter == 5:
        continue  # Skip to next iteration
    if counter == 10:
        break     # Exit loop
    print(counter)

🔹 Functions - Reusable Code Blocks

📝 Defining and Using Functions
# Simple function
def greet():
    print("Hello, World!")

greet()  # Call the function

# Function with parameters
def greet_person(name):
    print(f"Hello, {name}!")

greet_person("Mohammad")  # Output: Hello, Mohammad!

# Function with return value
def calculate_speed(distance, time):
    speed = distance / time
    return speed

result = calculate_speed(100, 20)
print(f"Speed: {result} m/s")

# Function with default parameters
def move_robot(direction, speed=0.5):
    print(f"Moving {direction} at {speed} m/s")

move_robot("forward")           # Uses default speed
move_robot("backward", 0.8)   # Custom speed

🔹 Importing Libraries

📝 Import Statements
# Import entire module
import math
result = math.sqrt(16)  # 4.0

# Import specific functions
from math import sqrt, pi
result = sqrt(16)      # No need for math.sqrt

# Import with alias (short name)
import numpy as np
array = np.array([1, 2, 3])

# Common imports for AI labs
import torch
import torch.nn as nn
import numpy as np
import cv2  # OpenCV

💡 Practice Exercise

Challenge: Write a function that takes a list of sensor readings and returns the average value.

# Your solution here
def calculate_average(readings):
    # Hint: use sum() and len()
    total = sum(readings)
    count = len(readings)
    average = total / count
    return average

# Test it
temps = [25.3, 26.1, 24.8, 25.5]
avg = calculate_average(temps)
print(f"Average temperature: {avg}°C")
🔧

Chapter 1: Python for Real Hardware

Week 1
↑ Go Up

Learn to use Python to control real hardware: NVIDIA Jetson Orin Nano, Raspberry Pi, and Arduino. This chapter bridges the gap between software and physical devices.

🎯 What You'll Learn

  • Understanding the Jetson Orin Nano platform
  • GPIO (General Purpose Input/Output) programming
  • Reading sensor data from hardware
  • Controlling actuators (motors, servos, LEDs)
  • Serial communication between devices

🔹 NVIDIA Jetson Orin Nano Overview

🖥️ What is Jetson?

A small, powerful computer designed for AI applications. It has a GPU for running neural networks efficiently.

💪 Key Features

GPU acceleration, multiple cores, AI libraries pre-installed, GPIO pins for hardware control.

🎯 Perfect For

Autonomous robots, computer vision, edge AI, real-time processing, IoT applications.

🔹 GPIO Programming

GPIO pins allow your Python code to interact with physical components like LEDs, buttons, sensors, and motors.

📝 Basic LED Control on Jetson
import Jetson.GPIO as GPIO
import time

# Setup
LED_PIN = 7  # Physical pin number
GPIO.setmode(GPIO.BOARD)  # Use board pin numbering
GPIO.setup(LED_PIN, GPIO.OUT)  # Set pin as output

# Turn LED on and off
GPIO.output(LED_PIN, GPIO.HIGH)  # LED ON
time.sleep(1)  # Wait 1 second
GPIO.output(LED_PIN, GPIO.LOW)   # LED OFF

# Cleanup (always do this!)
GPIO.cleanup()
📝 Reading a Button Input
import Jetson.GPIO as GPIO
import time

BUTTON_PIN = 11
GPIO.setmode(GPIO.BOARD)
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# Read button state continuously
try:
    while True:
        button_state = GPIO.input(BUTTON_PIN)
        if button_state == GPIO.LOW:  # Button pressed
            print("Button is pressed!")
        time.sleep(0.1)
except KeyboardInterrupt:
    print("Program stopped")
finally:
    GPIO.cleanup()

🔹 PWM (Pulse Width Modulation)

PWM allows you to control the speed of motors or brightness of LEDs by rapidly turning power on and off.

📝 Controlling Motor Speed with PWM
import Jetson.GPIO as GPIO

MOTOR_PIN = 12
GPIO.setmode(GPIO.BOARD)
GPIO.setup(MOTOR_PIN, GPIO.OUT)

# Create PWM object (50 Hz frequency)
pwm = GPIO.PWM(MOTOR_PIN, 50)
pwm.start(0)  # Start with 0% duty cycle

# Control motor speed (0-100%)
pwm.ChangeDutyCycle(25)   # 25% speed
time.sleep(2)
pwm.ChangeDutyCycle(50)   # 50% speed
time.sleep(2)
pwm.ChangeDutyCycle(100)  # 100% speed

# Cleanup
pwm.stop()
GPIO.cleanup()

🔹 Serial Communication

Communicate with Arduino or other devices using serial (UART) protocol.

📝 Reading from Serial Port
import serial
import time

# Open serial port (adjust port name for your system)
ser = serial.Serial('/dev/ttyUSB0', baudrate=9600, timeout=1)
time.sleep(2)  # Wait for connection

# Send data
ser.write(b'Hello Arduino\n')  # 'b' means bytes

# Receive data
while True:
    if ser.in_waiting > 0:  # If data available
        line = ser.readline().decode('utf-8').strip()
        print(f"Received: {line}")

# Close connection
ser.close()

🔹 Working with Camera

📝 Capturing Images from Camera
import cv2

# Initialize camera (0 is usually the default camera)
camera = cv2.VideoCapture(0)

# Set camera properties
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

# Capture a single frame
ret, frame = camera.read()

if ret:
    # Save image
    cv2.imwrite('captured_image.jpg', frame)
    print("Image saved!")

# Release camera
camera.release()

⚠️ Common Pitfalls

  • Forgetting GPIO.cleanup(): Always clean up GPIO resources when done
  • Wrong pin numbering: Make sure you're using the correct numbering mode (BOARD vs BCM)
  • Insufficient permissions: You may need sudo for GPIO access
  • Serial port name: Check the actual port name with ls /dev/tty*

✅ Best Practices

  • Always use try-except-finally blocks to ensure cleanup
  • Add small delays (time.sleep) between operations
  • Test with simple components (LED) before complex ones (motors)
  • Document your pin connections and configurations
  • Use descriptive variable names for pin numbers
🧠

Chapter 2: ML Foundations with NumPy

Week 2
↑ Go Up

NumPy is the foundation of scientific computing in Python. It provides powerful array operations that are essential for machine learning and AI.

🎯 Why NumPy?

  • Fast: Operations are implemented in C, making them much faster than Python lists
  • Memory Efficient: Arrays use less memory than lists
  • Vectorized: Perform operations on entire arrays without loops
  • Foundation: PyTorch tensors are similar to NumPy arrays

🔹 Creating NumPy Arrays

📝 Different Ways to Create Arrays
import numpy as np

# From Python list
arr = np.array([1, 2, 3, 4, 5])
print(arr)  # [1 2 3 4 5]

# 2D array (matrix)
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])
print(matrix)
# [[1 2 3]
#  [4 5 6]]

# Special arrays
zeros = np.zeros(5)           # [0. 0. 0. 0. 0.]
ones = np.ones((3, 3))        # 3x3 array of ones
identity = np.eye(3)          # 3x3 identity matrix
random = np.random.rand(5)    # 5 random numbers [0, 1)

# Range arrays
range_arr = np.arange(0, 10, 2)  # [0 2 4 6 8]
linspace = np.linspace(0, 1, 5) # 5 evenly spaced values

🔹 Array Operations

📝 Mathematical Operations
import numpy as np

# Element-wise operations (no loops needed!)
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])

# Basic arithmetic
print(a + b)      # [ 6  8 10 12]
print(a * b)      # [ 5 12 21 32]
print(a ** 2)      # [ 1  4  9 16] (squares)

# Scalar operations
print(a * 10)     # [10 20 30 40]
print(a + 5)      # [6 7 8 9]

# Common functions
print(np.sqrt(a))      # Square root
print(np.exp(a))       # Exponential
print(np.sin(a))       # Sine
print(np.log(a))       # Natural log

🔹 Array Indexing and Slicing

📝 Accessing Array Elements
import numpy as np

arr = np.array([10, 20, 30, 40, 50])

# Indexing (same as lists)
print(arr[0])    # 10 (first element)
print(arr[-1])   # 50 (last element)

# Slicing [start:end:step]
print(arr[1:4])  # [20 30 40]
print(arr[:3])   # [10 20 30] (first 3)
print(arr[2:])   # [30 40 50] (from index 2)

# 2D array indexing
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])

print(matrix[0, 0])   # 1 (row 0, col 0)
print(matrix[1, 2])   # 6 (row 1, col 2)
print(matrix[0, :])   # [1 2 3] (entire first row)
print(matrix[:, 1])   # [2 5 8] (entire second column)

🔹 Array Statistics

📝 Statistical Operations
import numpy as np

data = np.array([10, 20, 30, 40, 50])

# Basic statistics
print(np.mean(data))      # 30.0 (average)
print(np.median(data))    # 30.0 (middle value)
print(np.std(data))       # 14.14... (standard deviation)
print(np.sum(data))       # 150 (sum of all elements)

# Min and max
print(np.min(data))       # 10
print(np.max(data))       # 50
print(np.argmin(data))    # 0 (index of minimum)
print(np.argmax(data))    # 4 (index of maximum)

# For 2D arrays - specify axis
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])

print(np.mean(matrix, axis=0))  # [2.5 3.5 4.5] (column means)
print(np.mean(matrix, axis=1))  # [2. 5.] (row means)

🔹 Array Shape Manipulation

📝 Reshaping and Transposing
import numpy as np

# Original array
arr = np.array([1, 2, 3, 4, 5, 6])
print(arr.shape)  # (6,) - 1D array with 6 elements

# Reshape to 2x3
reshaped = arr.reshape(2, 3)
print(reshaped)
# [[1 2 3]
#  [4 5 6]]

# Reshape to 3x2
reshaped2 = arr.reshape(3, 2)
print(reshaped2)
# [[1 2]
#  [3 4]
#  [5 6]]

# Transpose (flip rows and columns)
transposed = reshaped.T
print(transposed)
# [[1 4]
#  [2 5]
#  [3 6]]

# Flatten back to 1D
flattened = reshaped.flatten()
print(flattened)  # [1 2 3 4 5 6]

💡 Machine Learning Connection

Why Shape Matters: In ML, data shape is critical:

  • Images: Shape (height, width, channels) - e.g., (224, 224, 3) for RGB
  • Batches: Shape (batch_size, features) - e.g., (32, 784) for 32 images of 784 pixels
  • Time Series: Shape (samples, timesteps, features)
  • Neural Networks: Need correct input shapes to work properly

🔹 Broadcasting

Broadcasting allows NumPy to work with arrays of different shapes in arithmetic operations.

📝 Broadcasting Examples
import numpy as np

# Add scalar to array (broadcasting)
arr = np.array([1, 2, 3])
result = arr + 10  # 10 is "broadcast" to [10, 10, 10]
print(result)  # [11 12 13]

# Add 1D array to 2D array
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])
vector = np.array([10, 20, 30])
result = matrix + vector  # vector added to each row
print(result)
# [[11 22 33]
#  [14 25 36]]

# Normalize data (common in ML)
data = np.array([[1, 2, 3],
                 [4, 5, 6]])
mean = np.mean(data, axis=0)  # Column means
std = np.std(data, axis=0)    # Column standard deviations
normalized = (data - mean) / std  # Broadcasting!
print(normalized)

✅ NumPy Practice Challenge

Create a function that normalizes sensor readings (subtract mean, divide by std):

import numpy as np

def normalize_readings(readings):
    """Normalize data to have mean=0 and std=1"""
    mean = np.mean(readings)
    std = np.std(readings)
    normalized = (readings - mean) / std
    return normalized

# Test it
sensor_data = np.array([20.5, 25.3, 22.1, 26.8, 23.4])
normalized = normalize_readings(sensor_data)
print(f"Original: {sensor_data}")
print(f"Normalized: {normalized}")
print(f"New mean: {np.mean(normalized):.6f}")  # ~0
print(f"New std: {np.std(normalized):.6f}")    # ~1
🧬

Chapter 3: Neural Networks with PyTorch

Week 3
↑ Go Up

PyTorch is a powerful deep learning framework. Learn to build, train, and deploy neural networks for AI applications.

🎯 What is PyTorch?

PyTorch is a Python library for building and training neural networks. It's similar to NumPy but with:

  • GPU Support: Automatically uses your GPU for fast computations
  • Automatic Differentiation: Calculates gradients automatically for training
  • Neural Network Modules: Pre-built layers and architectures
  • Dynamic Computation: Build networks that change during execution

🔹 PyTorch Tensors

Tensors are like NumPy arrays but can run on GPU and track gradients for training.

📝 Creating and Using Tensors
import torch

# Create tensors (similar to NumPy)
x = torch.tensor([1.0, 2.0, 3.0])
print(x)  # tensor([1., 2., 3.])

# Create from NumPy array
import numpy as np
np_array = np.array([1, 2, 3])
tensor = torch.from_numpy(np_array)

# Special tensors
zeros = torch.zeros(3, 3)         # 3x3 zeros
ones = torch.ones(2, 4)           # 2x4 ones
random = torch.rand(3, 3)        # Random values [0, 1)
normal = torch.randn(3, 3)       # Random normal distribution

# Check tensor properties
print(x.shape)      # torch.Size([3])
print(x.dtype)      # torch.float32
print(x.device)     # cpu (or cuda if on GPU)

🔹 GPU Acceleration

📝 Moving Tensors to GPU
import torch

# Check if GPU is available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# Create tensor on GPU directly
x = torch.ones(3, 3, device=device)

# Move existing tensor to GPU
y = torch.randn(3, 3)
y = y.to(device)  # Now on GPU

# Move back to CPU (needed for some operations)
y_cpu = y.cpu()

# Operations stay on same device
z = x + y  # z is also on GPU

🔹 Building a Simple Neural Network

📝 Basic Neural Network Structure
import torch
import torch.nn as nn

# Define a simple network
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        # Define layers
        self.fc1 = nn.Linear(784, 128)  # Input: 784, Output: 128
        self.fc2 = nn.Linear(128, 64)   # Hidden layer
        self.fc3 = nn.Linear(64, 10)    # Output: 10 classes
        
    def forward(self, x):
        # Define forward pass
        x = torch.relu(self.fc1(x))  # Apply ReLU activation
        x = torch.relu(self.fc2(x))
        x = self.fc3(x)               # No activation on last layer
        return x

# Create the model
model = SimpleNet()
print(model)
Output:
SimpleNet(
  (fc1): Linear(in_features=784, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=64, bias=True)
  (fc3): Linear(in_features=64, out_features=10, bias=True)
)

🔹 Training a Neural Network

📝 Complete Training Loop
import torch
import torch.nn as nn
import torch.optim as optim

# Create model, loss function, and optimizer
model = SimpleNet()
criterion = nn.CrossEntropyLoss()  # For classification
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Training loop (simplified)
num_epochs = 10
for epoch in range(num_epochs):
    # Forward pass
    outputs = model(inputs)  # inputs is your data
    loss = criterion(outputs, labels)  # labels are target classes
    
    # Backward pass
    optimizer.zero_grad()  # Clear previous gradients
    loss.backward()        # Calculate gradients
    optimizer.step()       # Update weights
    
    # Print progress
    if (epoch + 1) % 2 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

🎯 Loss Function

Measures how wrong the model's predictions are. Training tries to minimize this.

🔄 Optimizer

Algorithm that updates model weights to reduce loss. Common: Adam, SGD.

📊 Epoch

One complete pass through the entire training dataset.

⚡ Learning Rate

Controls how much weights change each step. Too high = unstable, too low = slow.

🔹 Convolutional Neural Network (CNN)

CNNs are specialized for image processing. They learn spatial patterns in images.

📝 Simple CNN for Image Classification
import torch.nn as nn

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        
        # Convolutional layers
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3)  # 1 input channel, 32 output
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3) # 32 input, 64 output
        
        # Pooling layer (reduces size)
        self.pool = nn.MaxPool2d(2, 2)
        
        # Fully connected layers
        self.fc1 = nn.Linear(64 * 5 * 5, 128)  # Flatten then linear
        self.fc2 = nn.Linear(128, 10)         # 10 output classes
        
    def forward(self, x):
        # Conv -> ReLU -> Pool
        x = self.pool(torch.relu(self.conv1(x)))
        x = self.pool(torch.relu(self.conv2(x)))
        
        # Flatten
        x = x.view(-1, 64 * 5 * 5)
        
        # Fully connected layers
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

model = SimpleCNN()
print(f"Model has {sum(p.numel() for p in model.parameters())} parameters")

🧩 CNN Components Explained

Layer Purpose Example
Conv2d Extract features from images using filters Detect edges, textures, patterns
ReLU Activation function (non-linearity) max(0, x) - removes negative values
MaxPool2d Reduce spatial size, keep important features Take max value in 2x2 regions
Linear Fully connected layer for classification Combine features to make prediction

🔹 Saving and Loading Models

📝 Model Persistence
import torch

# Save model
torch.save(model.state_dict(), 'model.pth')
print("Model saved!")

# Load model
model = SimpleCNN()  # Create model structure first
model.load_state_dict(torch.load('model.pth'))
model.eval()  # Set to evaluation mode
print("Model loaded!")

# Save entire model (less common)
torch.save(model, 'complete_model.pth')

# Load entire model
model = torch.load('complete_model.pth')

⚠️ Common PyTorch Mistakes

  • Forgetting model.eval(): Always call before inference to disable dropout/batch norm
  • Wrong tensor shapes: Check shapes with .shape, use .view() or .reshape() to fix
  • Not moving data to GPU: Data and model must be on same device
  • Forgetting zero_grad(): Gradients accumulate if you don't clear them
  • Training without GPU: Check with torch.cuda.is_available()

✅ PyTorch Best Practices

  • Always check tensor shapes during development
  • Use torch.no_grad() during inference to save memory
  • Move model to GPU once, not in every iteration
  • Use DataLoader for efficient batch processing
  • Monitor training loss to detect issues early
  • Save checkpoints during long training runs
📊

Chapter 4: Working with Datasets

Week 4
↑ Go Up

Learn to load, preprocess, and work with common machine learning datasets including MNIST, CIFAR-10, and IRIS.

🔹 Loading MNIST Dataset

📝 MNIST with PyTorch
import torch
from torchvision import datasets, transforms

# Define transforms (preprocessing)
transform = transforms.Compose([
    transforms.ToTensor(),  # Convert to tensor
    transforms.Normalize((0.5,), (0.5,))  # Normalize to [-1, 1]
])

# Load MNIST dataset
train_dataset = datasets.MNIST(
    root='./data',
    train=True,
    download=True,
    transform=transform
)

test_dataset = datasets.MNIST(
    root='./data',
    train=False,
    download=True,
    transform=transform
)

# Create data loaders
train_loader = torch.utils.data.DataLoader(
    train_dataset,
    batch_size=64,
    shuffle=True
)

print(f"Training samples: {len(train_dataset)}")  # 60,000
print(f"Test samples: {len(test_dataset)}")      # 10,000

🔹 Data Augmentation

📝 Image Augmentation Techniques
from torchvision import transforms

# Augmentation for training
train_transform = transforms.Compose([
    transforms.RandomRotation(10),         # Rotate ±10 degrees
    transforms.RandomHorizontalFlip(),   # Flip horizontally
    transforms.ColorJitter(              # Adjust colors
        brightness=0.2,
        contrast=0.2
    ),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# No augmentation for validation/test
test_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

💡 Why Data Augmentation?

Data augmentation artificially increases training data by creating modified versions of images. Benefits:

  • Prevents overfitting by adding variety
  • Model learns to recognize objects from different angles
  • Improves generalization to new data
  • Essential when training data is limited
🚀

Chapter 5: Advanced PyTorch & TensorFlow

Week 5
↑ Go Up

🔹 Custom Training Loop

📝 Complete Training with Validation
def train_model(model, train_loader, val_loader, num_epochs=10):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.to(device)
    
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    
    for epoch in range(num_epochs):
        # Training phase
        model.train()
        train_loss = 0.0
        train_correct = 0
        
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device)
            
            # Forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)
            
            # Backward pass
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
            train_loss += loss.item()
            _, predicted = torch.max(outputs.data, 1)
            train_correct += (predicted == labels).sum().item()
        
        # Validation phase
        model.eval()
        val_loss = 0.0
        val_correct = 0
        
        with torch.no_grad():
            for images, labels in val_loader:
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                
                val_loss += loss.item()
                _, predicted = torch.max(outputs.data, 1)
                val_correct += (predicted == labels).sum().item()
        
        # Print statistics
        train_acc = 100 * train_correct / len(train_loader.dataset)
        val_acc = 100 * val_correct / len(val_loader.dataset)
        print(f'Epoch {epoch+1}/{num_epochs}:')
        print(f'  Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%')
        print(f'  Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%')
📡

Chapter 6: Sensor Data Acquisition

Week 7
↑ Go Up

🔹 Reading Ultrasonic Sensor

📝 Distance Measurement
import Jetson.GPIO as GPIO
import time

TRIG_PIN = 16
ECHO_PIN = 18

GPIO.setmode(GPIO.BOARD)
GPIO.setup(TRIG_PIN, GPIO.OUT)
GPIO.setup(ECHO_PIN, GPIO.IN)

def measure_distance():
    # Send trigger pulse
    GPIO.output(TRIG_PIN, GPIO.HIGH)
    time.sleep(0.00001)  # 10 microseconds
    GPIO.output(TRIG_PIN, GPIO.LOW)
    
    # Measure echo time
    while GPIO.input(ECHO_PIN) == 0:
        pulse_start = time.time()
    
    while GPIO.input(ECHO_PIN) == 1:
        pulse_end = time.time()
    
    # Calculate distance (cm)
    pulse_duration = pulse_end - pulse_start
    distance = pulse_duration * 17150  # Speed of sound / 2
    distance = round(distance, 2)
    
    return distance

# Continuous reading
try:
    while True:
        dist = measure_distance()
        print(f"Distance: {dist} cm")
        time.sleep(0.5)
except KeyboardInterrupt:
    GPIO.cleanup()

🔹 IMU (Inertial Measurement Unit)

📝 Reading Accelerometer Data
import smbus
import time

# I2C communication
bus = smbus.SMBus(1)
IMU_ADDRESS = 0x68  # MPU6050 default address

def read_imu_data():
    # Read accelerometer values
    accel_x = bus.read_word_data(IMU_ADDRESS, 0x3B)
    accel_y = bus.read_word_data(IMU_ADDRESS, 0x3D)
    accel_z = bus.read_word_data(IMU_ADDRESS, 0x3F)
    
    # Convert to G-forces (gravity units)
    accel_x = accel_x / 16384.0
    accel_y = accel_y / 16384.0
    accel_z = accel_z / 16384.0
    
    return accel_x, accel_y, accel_z

while True:
    x, y, z = read_imu_data()
    print(f"Accel X: {x:.2f}g, Y: {y:.2f}g, Z: {z:.2f}g")
    time.sleep(0.1)
🎯

Chapter 7: Classification Algorithms

Week 8
↑ Go Up

🔹 Support Vector Machine (SVM)

📝 SVM with Scikit-learn
from sklearn import svm
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report

# Load your data
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Create and train SVM
clf = svm.SVC(kernel='rbf', gamma='auto')
clf.fit(X_train, y_train)

# Make predictions
y_pred = clf.predict(X_test)

# Evaluate
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy * 100:.2f}%")
print(classification_report(y_test, y_pred))

🔹 Random Forest

📝 Random Forest Classifier
from sklearn.ensemble import RandomForestClassifier

# Create Random Forest
rf_clf = RandomForestClassifier(
    n_estimators=100,      # Number of trees
    max_depth=10,          # Max tree depth
    random_state=42
)

# Train
rf_clf.fit(X_train, y_train)

# Predict
y_pred = rf_clf.predict(X_test)

# Feature importance
importances = rf_clf.feature_importances_
print("Feature importances:", importances)
📈

Chapter 8: Regression Techniques

Week 9
↑ Go Up

🔹 Linear Regression

📝 Simple Linear Regression
from sklearn.linear_model import LinearRegression
import numpy as np

# Sample data
X = np.array([[1], [2], [3], [4], [5]])
y = np.array([2, 4, 5, 4, 5])

# Create and train model
model = LinearRegression()
model.fit(X, y)

# Make predictions
X_test = np.array([[6], [7]])
predictions = model.predict(X_test)

print(f"Slope: {model.coef_[0]:.2f}")
print(f"Intercept: {model.intercept_:.2f}")
print(f"Predictions: {predictions}")

🔹 Polynomial Regression

📝 Fitting Non-linear Data
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline

# Create polynomial regression model (degree 2)
model = make_pipeline(
    PolynomialFeatures(2),
    LinearRegression()
)

# Train
model.fit(X, y)

# Predict
predictions = model.predict(X_test)
🤖

Chapter 9: Robot Motion & Collision Avoidance

Week 10
↑ Go Up

🔹 Basic Robot Movement

📝 JetBot Control
from jetbot import Robot
import time

# Create robot instance
robot = Robot()

# Move forward
robot.forward(0.3)  # 30% speed
time.sleep(2)
robot.stop()

# Turn left
robot.left(0.3)
time.sleep(1)
robot.stop()

# Move backward
robot.backward(0.3)
time.sleep(2)
robot.stop()

# Individual motor control
robot.set_motors(0.5, 0.3)  # left, right speeds

🔹 Collision Avoidance

📝 Obstacle Detection and Avoidance
from jetbot import Robot
import time

robot = Robot()
SAFE_DISTANCE = 30  # cm

def avoid_collision():
    while True:
        distance = measure_distance()  # From ultrasonic sensor
        
        if distance > SAFE_DISTANCE:
            # Safe to move forward
            robot.forward(0.3)
        else:
            # Obstacle detected!
            robot.stop()
            robot.backward(0.3)
            time.sleep(0.5)
            robot.left(0.3)  # Turn to avoid
            time.sleep(1)
        
        time.sleep(0.1)

try:
    avoid_collision()
except KeyboardInterrupt:
    robot.stop()
👁️

Chapter 10: Object Detection with YOLO

Week 11
↑ Go Up

🔹 YOLO Object Detection

📝 Real-time Object Detection
import cv2
import torch

# Load YOLO model
model = torch.hub.load('ultralytics/yolov5', 'yolov5s')

# Open camera
camera = cv2.VideoCapture(0)

while True:
    ret, frame = camera.read()
    if not ret:
        break
    
    # Detect objects
    results = model(frame)
    
    # Draw bounding boxes
    annotated_frame = results.render()[0]
    
    # Display
    cv2.imshow('YOLO Detection', annotated_frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

camera.release()
cv2.destroyAllWindows()

🔹 Object Following

📝 Follow Detected Object
from jetbot import Robot
import cv2
import torch

robot = Robot()
model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
camera = cv2.VideoCapture(0)

TARGET_CLASS = 'person'  # Object to follow

while True:
    ret, frame = camera.read()
    results = model(frame)
    
    # Get detections
    detections = results.pandas().xyxy[0]
    
    # Find target object
    target = detections[detections['name'] == TARGET_CLASS]
    
    if len(target) > 0:
        # Get bounding box center
        x_center = (target['xmin'].values[0] + target['xmax'].values[0]) / 2
        frame_center = frame.shape[1] / 2
        
        # Turn towards object
        if x_center < frame_center - 50:
            robot.left(0.2)
        elif x_center > frame_center + 50:
            robot.right(0.2)
        else:
            robot.forward(0.2)
    else:
        robot.stop()
    
    time.sleep(0.1)
🛣️

Chapter 11: Autonomous Road Following

Week 12
↑ Go Up

🔹 Lane Detection

📝 Finding Lane Lines
import cv2
import numpy as np

def detect_lanes(image):
    # Convert to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # Apply Gaussian blur
    blur = cv2.GaussianBlur(gray, (5, 5), 0)
    
    # Edge detection
    edges = cv2.Canny(blur, 50, 150)
    
    # Define region of interest
    height, width = image.shape[:2]
    mask = np.zeros_like(edges)
    polygon = np.array([[
        (0, height),
        (width, height),
        (width // 2, height // 2)
    ]])
    cv2.fillPoly(mask, polygon, 255)
    masked_edges = cv2.bitwise_and(edges, mask)
    
    # Detect lines using Hough transform
    lines = cv2.HoughLinesP(
        masked_edges,
        rho=2,
        theta=np.pi/180,
        threshold=50,
        minLineLength=40,
        maxLineGap=100
    )
    
    return lines

🔹 End-to-End Learning

📝 Deep Learning for Road Following
import torch
import torch.nn as nn

class RoadFollowingModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 24, 5, stride=2)
        self.conv2 = nn.Conv2d(24, 36, 5, stride=2)
        self.conv3 = nn.Conv2d(36, 48, 5, stride=2)
        self.conv4 = nn.Conv2d(48, 64, 3)
        self.conv5 = nn.Conv2d(64, 64, 3)
        
        self.fc1 = nn.Linear(64 * 1 * 18, 100)
        self.fc2 = nn.Linear(100, 50)
        self.fc3 = nn.Linear(50, 2)  # x, y steering
    
    def forward(self, x):
        x = torch.relu(self.conv1(x))
        x = torch.relu(self.conv2(x))
        x = torch.relu(self.conv3(x))
        x = torch.relu(self.conv4(x))
        x = torch.relu(self.conv5(x))
        x = x.view(x.size(0), -1)
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        x = self.fc3(x)
        return x

model = RoadFollowingModel()
🎓

Chapter 12: AI Project Development

Week 13
↑ Go Up

📋 Project Structure Best Practices

my_ai_project/
│
├── data/
│   ├── raw/              # Original data
│   ├── processed/        # Cleaned data
│   └── models/           # Saved models
│
├── src/
│   ├── data_loader.py    # Data loading functions
│   ├── model.py          # Model definitions
│   ├── train.py          # Training script
│   └── inference.py      # Prediction script
│
├── notebooks/
│   └── exploration.ipynb # Jupyter notebooks
│
├── requirements.txt      # Dependencies
├── README.md            # Project documentation
└── main.py              # Main entry point

🔹 Creating requirements.txt

📝 Managing Dependencies
# requirements.txt
torch>=2.0.0
torchvision>=0.15.0
numpy>=1.24.0
opencv-python>=4.8.0
matplotlib>=3.7.0
scikit-learn>=1.3.0

# Install all dependencies:
# pip install -r requirements.txt

✅ Project Development Checklist

  • ✓ Define clear objectives and success criteria
  • ✓ Collect and organize dataset
  • ✓ Create data preprocessing pipeline
  • ✓ Design and implement model architecture
  • ✓ Set up training with validation
  • ✓ Monitor and log training progress
  • ✓ Evaluate on test set
  • ✓ Deploy and test in real environment
  • ✓ Document code and results
  • ✓ Prepare presentation
🤖

Chapter 13: Edge AI with LLMs & Ollama

Week 14
↑ Go Up

🔹 Installing Ollama

📝 Setup Ollama on Jetson
# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh

# Download a model (e.g., Llama 2)
ollama pull llama2

# Run Ollama server
ollama serve

🔹 Using Ollama in Python

📝 Python Integration
import requests
import json

def ask_llm(prompt):
    url = "http://localhost:11434/api/generate"
    
    data = {
        "model": "llama2",
        "prompt": prompt,
        "stream": False
    }
    
    response = requests.post(url, json=data)
    result = response.json()
    
    return result['response']

# Example usage
question = "What is the capital of UAE?"
answer = ask_llm(question)
print(answer)

🔹 Robot with LLM

📝 Voice-Controlled Robot
from jetbot import Robot
import requests

robot = Robot()

def execute_command(user_input):
    # Ask LLM to interpret command
    prompt = f"""Given this command: "{user_input}"
    Respond with ONLY one of these: FORWARD, BACKWARD, LEFT, RIGHT, STOP
    No explanation, just the action."""
    
    action = ask_llm(prompt).strip().upper()
    
    # Execute action
    if action == "FORWARD":
        robot.forward(0.3)
    elif action == "BACKWARD":
        robot.backward(0.3)
    elif action == "LEFT":
        robot.left(0.3)
    elif action == "RIGHT":
        robot.right(0.3)
    elif action == "STOP":
        robot.stop()
    
    return action

# Test
execute_command("Please move to the left")
execute_command("Go straight ahead")

Quick Reference Guide

↑ Go Up

🐍 Python Basics

  • print(x) - Display output
  • len(list) - Get length
  • range(n) - 0 to n-1
  • type(x) - Check type

📊 NumPy

  • np.array([1,2,3])
  • np.zeros((3,3))
  • np.mean(arr)
  • arr.reshape(2,3)

🔥 PyTorch

  • torch.tensor([1,2])
  • x.to(device)
  • model.eval()
  • torch.save(model)

👁️ OpenCV

  • cv2.imread('img.jpg')
  • cv2.resize(img, (w,h))
  • cv2.imshow('title', img)
  • cv2.VideoCapture(0)

🔧 GPIO

  • GPIO.setmode(GPIO.BOARD)
  • GPIO.setup(pin, GPIO.OUT)
  • GPIO.output(pin, HIGH)
  • GPIO.cleanup()

🤖 JetBot

  • robot.forward(speed)
  • robot.left(speed)
  • robot.stop()
  • robot.set_motors(l, r)
📝

Complete Cheat Sheets

↑ Go Up

🔹 Python String Methods

Method Description Example
.upper() Convert to uppercase "hello".upper() → "HELLO"
.lower() Convert to lowercase "HELLO".lower() → "hello"
.strip() Remove whitespace " hi ".strip() → "hi"
.split() Split into list "a,b,c".split(',') → ['a','b','c']
.replace() Replace substring "hi".replace('h','H') → "Hi"

🔹 NumPy Functions

Function Description Example
np.mean() Average value np.mean([1,2,3]) → 2.0
np.sum() Sum all elements np.sum([1,2,3]) → 6
np.max() Maximum value np.max([1,5,3]) → 5
np.argmax() Index of maximum np.argmax([1,5,3]) → 1
np.concatenate() Join arrays np.concatenate([a, b])

🔹 PyTorch Neural Network Layers

Layer Purpose Example
nn.Linear Fully connected layer nn.Linear(784, 128)
nn.Conv2d 2D convolution nn.Conv2d(3, 64, 3)
nn.MaxPool2d Max pooling nn.MaxPool2d(2, 2)
nn.ReLU ReLU activation nn.ReLU()
nn.Dropout Regularization nn.Dropout(0.5)
🔧

Common Errors & Solutions

↑ Go Up

❌ IndexError: list index out of range

Problem: Trying to access an index that doesn't exist.

lst = [1, 2, 3]
print(lst[5])  # ❌ IndexError!

Solution: Check list length first or use try-except.

if len(lst) > 5:
    print(lst[5])
else:
    print("Index too large!")

❌ RuntimeError: Expected all tensors to be on the same device

Problem: Model on GPU but data on CPU (or vice versa).

Solution: Move both to same device.

device = torch.device("cuda")
model = model.to(device)
data = data.to(device)  # Don't forget this!

❌ RuntimeError: size mismatch

Problem: Tensor shapes don't match for operation.

Solution: Check and fix shapes.

print(x.shape)  # Check current shape
x = x.view(-1, 784)  # Reshape as needed

❌ ModuleNotFoundError: No module named 'torch'

Problem: Library not installed.

Solution: Install the package.

# In terminal/command prompt:
pip install torch torchvision

❌ CUDA out of memory

Problem: Model or batch too large for GPU memory.

Solutions:

  • Reduce batch size
  • Use smaller model
  • Clear GPU cache: torch.cuda.empty_cache()
  • Use gradient accumulation

💡 Debugging Tips

  • Print shapes: print(tensor.shape) everywhere!
  • Print types: print(type(variable))
  • Use try-except: Catch errors gracefully
  • Add print statements: Track code flow
  • Test small parts: Don't run entire code at once
  • Read error messages: They tell you exactly what's wrong!
  • Google the error: Someone else has had it before