Python and PyTorch Tutorial

From basic Python concepts to deep learning with PyTorch

Python Basics

Python Data Structures (Containers)

Lists

Lists are one of the most commonly used data structures in Python. They are mutable, ordered, and can contain elements of different data types.

# Defining a list
fruits = ['apple', 'pear', 'banana', 'strawberry']
mixed_list = [1, 'python', 3.14, True]

# Accessing list elements
print(fruits[0]) # 'apple'
print(fruits[-1]) # 'strawberry' - last element

# List slicing
print(fruits[1:3]) # ['pear', 'banana']

# List methods
fruits.append('kiwi') # Adding an element to the list
fruits.remove('pear') # Removing an element from the list
fruits.sort() # Sorting the list

Dictionaries

Dictionaries are collections that store key-value pairs. The keys must be unique.

# Defining a dictionary
student = {
    'name': 'John',
    'age': 20,
    'courses': ['Mathematics', 'Physics', 'Programming']
}

# Accessing dictionary elements
print(student['name']) # 'John'
print(student.get('age')) # 20

# Dictionary methods
print(student.keys()) # List all keys
print(student.values()) # List all values
student['major'] = 'Computer Science' # Adding a new key-value pair

Tuples

Tuples are similar to lists but are immutable (cannot be changed).

# Defining a tuple
coordinate = (10, 20)
RGB = (255, 0, 0) # Red color

# Accessing tuple elements
print(coordinate[0]) # 10

# Tuples are immutable
# coordinate[0] = 15 # This will raise an error!

Sets

Sets are unordered collections of unique elements.

# Defining a set
fruits = {'apple', 'pear', 'banana', 'apple'} # Duplicate 'apple' is counted only once
print(fruits) # {'apple', 'pear', 'banana'}

# Set operations
A = {1, 2, 3, 4}
B = {3, 4, 5, 6}
print(A | B) # Union: {1, 2, 3, 4, 5, 6}
print(A & B) # Intersection: {3, 4}
print(A - B) # Difference: {1, 2}
Loops, Iterators, and Generators

For Loop

In Python, the for loop is used to iterate over a collection.

# For loop over a list
fruits = ['apple', 'pear', 'banana']
for fruit in fruits:
    print(fruit)

# For loop with range()
for i in range(5):
    print(i) # 0, 1, 2, 3, 4

# For loop with enumerate() - getting index and value
for i, fruit in enumerate(fruits):
    print(f"{i}. index: {fruit}")

While Loop

The while loop runs as long as a specified condition is true.

counter = 0
while counter < 5:
    print(counter)
    counter += 1 # Increment the counter to avoid an infinite loop

Iterators

Iterators are objects that allow you to access the elements of a collection one at a time.

# Creating an iterator
fruits = ['apple', 'pear', 'banana']
iter_fruits = iter(fruits)

# Accessing elements using next()
print(next(iter_fruits)) # 'apple'
print(next(iter_fruits)) # 'pear'
print(next(iter_fruits)) # 'banana'
# print(next(iter_fruits)) # Raises StopIteration error!

Generators

Generators are a special type of iterator that generate values on the fly, which is memory efficient for large datasets.

# Defining a generator function
def generate_numbers(n):
    for i in range(n):
        yield i * i # Return the square using yield

# Using the generator
for number in generate_numbers(5):
    print(number) # 0, 1, 4, 9, 16

# Generator expression
squares = (x**2 for x in range(5))
print(list(squares)) # [0, 1, 4, 9, 16]
Functions

Defining and Calling Functions

Functions allow you to create reusable blocks of code.

# Defining a simple function
def greet(name):
    return f"Hello, {name}!"

# Calling the function
message = greet("John")
print(message) # "Hello, John!"

Parameters and Arguments

# Default parameter values
def power(base, exponent=2):
    return base ** exponent

print(power(3)) # 9 (3^2)
print(power(3, 3)) # 27 (3^3)

# Passing arguments by name
print(power(exponent=3, base=2)) # 8 (2^3)

*args and **kwargs

*args accepts a variable number of positional arguments, while **kwargs accepts a variable number of keyword arguments.

def example_function(*args, **kwargs):
    print("Positional arguments:", args)
    print("Keyword arguments:", kwargs)

example_function(1, 2, 3, name="John", age=25)
# Output:
# Positional arguments: (1, 2, 3)
# Keyword arguments: {'name': 'John', 'age': 25}

Lambda Functions

Lambda functions are anonymous functions defined in a single line.

# Lambda function
square = lambda x: x * x
print(square(5)) # 25

# Using lambda for sorting
students = [
    {'name': 'Alice', 'grade': 85},
    {'name': 'Bob', 'grade': 92},
    {'name': 'Charlie', 'grade': 78}
]

sorted_students = sorted(students, key=lambda x: x['grade'], reverse=True)
print(sorted_students) # Sorted in descending order by grade
Classes and Object-Oriented Programming

Defining a Class

Classes are structures that bundle data and behavior together.

class Car:
    # Constructor method
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year
        self.km = 0

    # Instance method
    def display_info(self):
        return f"{self.year} {self.brand} {self.model}, {self.km} km"

    def drive(self, distance):
        self.km += distance

# Creating and using an object
car1 = Car("Toyota", "Corolla", 2020)
car1.drive(150)
print(car1.display_info()) # "2020 Toyota Corolla, 150 km"

Inheritance

Inheritance allows one class to inherit attributes and methods from another class.

# Base class
class Shape:
    def __init__(self, color="black"):
        self.color = color

    def get_area(self):
        pass # Abstract method to be implemented in subclasses

    def get_color(self):
        return self.color

# Subclass (Circle)
class Circle(Shape):
    def __init__(self, radius, color="red"):
        super().__init__(color) # Calling the base class constructor
        self.radius = radius

    def get_area(self):
        import math
        return math.pi * self.radius ** 2

# Using the subclasses
circle = Circle(5)
print(f"Circle's color: {circle.get_color()}") # "Circle's color: red"
print(f"Circle's area: {circle.get_area():.2f}") # "Circle's area: 78.54"

Special Methods (Magic Methods)

Special methods in Python classes, which start and end with double underscores, define special behaviors.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Point({self.x}, {self.y})"

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

point1 = Point(1, 2)
point2 = Point(3, 4)
point3 = point1 + point2 # Calls the __add__ method
print(point3) # Calls the __str__ method - "Point(4, 6)"
File Operations

Reading and Writing Text Files

# Writing to a file
with open('example.txt', 'w', encoding='utf-8') as f:
    f.write("Hello, World!\n")
    f.write("Learning Python.")

# Reading from a file
with open('example.txt', 'r', encoding='utf-8') as f:
    content = f.read()
    print(content)

# Reading line by line
with open('example.txt', 'r', encoding='utf-8') as f:
    for line in f:
        print(line.strip())

JSON File Operations

import json

# Converting a Python object to JSON and writing to a file
data = {
    'name': 'John',
    'age': 25,
    'courses': ['Python', 'Data Science', 'Machine Learning']
}

with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=4)

# Reading a JSON file and converting it to a Python object
with open('data.json', 'r', encoding='utf-8') as f:
    read_data = json.load(f)

print(read_data['name']) # 'John'

CSV File Operations

import csv

# Writing to a CSV file
students = [
    ['First Name', 'Last Name', 'Grade'],
    ['Alice', 'Smith', 85],
    ['Bob', 'Johnson', 92],
    ['Charlie', 'Williams', 78]
]

with open('students.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)
    writer.writerows(students)

# Reading a CSV file
with open('students.csv', 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

PyTorch Basics

Dataset and DataLoader

What is a PyTorch Dataset?

A Dataset is an abstract class representing a dataset and providing data access. In PyTorch, custom datasets are created by subclassing torch.utils.data.Dataset.

import torch
from torch.utils.data import Dataset

class SentenceDataset(Dataset):
    def __init__(self, sentences):
        self.sentences = sentences

    def __len__(self):
        return len(self.sentences)

    def __getitem__(self, idx):
        sentence = self.sentences[idx]
        return sentence

# Creating an example dataset
sentences = [
    "Deep learning with PyTorch is very enjoyable.",
    "Python is a very practical programming language.",
    "Artificial Intelligence is the technology of the future.",
    "Data science is used in many fields."
]

dataset = SentenceDataset(sentences)
print(len(dataset)) # 4
print(dataset[0]) # "Deep learning with PyTorch is very enjoyable."

What is a DataLoader?

A DataLoader is a helper class that loads data from a Dataset in batches. It also provides features like shuffling and parallel loading.

from torch.utils.data import DataLoader

# Creating a DataLoader (batch_size=2)
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)

# Printing the batches
for i, batch in enumerate(dataloader):
    print(f"Batch {i+1}:")
    for sentence in batch:
        print(f" - {sentence}")

Custom Collate Function

The collate function allows you to customize how the data in a batch is combined.

def custom_collate_fn(batch):
    # Take the first 10 characters of each sentence
    short_sentences = [sentence[:10] + "..." for sentence in batch]
    return short_sentences

# Creating a DataLoader with a custom collate function
custom_dataloader = DataLoader(
    dataset,
    batch_size=2,
    shuffle=True,
    collate_fn=custom_collate_fn
)

# Printing batches with the custom format
print("\nWith Custom Collate Function:")
for i, batch in enumerate(custom_dataloader):
    print(f"Batch {i+1}:")
    for short_sentence in batch:
        print(f" - {short_sentence}")
Neural Networks with PyTorch

Creating an Artificial Neural Network (ANN) Class

In PyTorch, neural networks are created by subclassing torch.nn.Module.

import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleANN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleANN, self).__init__()
        # Defining layers
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        # Forward propagation
        x = F.relu(self.fc1(x)) # Apply ReLU activation on the first layer's output
        x = self.fc2(x) # Output layer
        return x

# Example usage of the neural network
input_size = 10 # Input dimension
hidden_size = 20 # Hidden layer dimension
output_size = 2 # Output dimension

model = SimpleANN(input_size, hidden_size, output_size)
print(model)

# Forward pass with a sample input
dummy_input = torch.randn(1, input_size)
output = model(dummy_input)
print(f"Output shape: {output.shape}")

Forward Function and How It Works

The forward function defines the forward pass of the neural network. It determines how the data is processed as it flows through the network.

Note: The forward() function is automatically called when using module(input). That is, model(x) actually calls model.forward(x).

Steps in the forward function:

  1. The input data is passed to the first layer: self.fc1(x)
  2. The result undergoes a linear transformation and then an activation function is applied: F.relu(...)
  3. The output from the intermediate layer is passed to the next layer
  4. The result from the final layer is returned

Thanks to PyTorch's automatic differentiation, a computational graph is created during the forward pass, which is used for backpropagation.

PyTorch Module Class and Inheritance

All our neural network models in PyTorch are derived from the nn.Module class. This inheritance provides:

  • The ability to nest models
  • Automatic tracking of weights
  • Easy transfer to GPU and model saving
  • Access to other useful methods
# A more complex model example
class ComplexNN(nn.Module):
    def __init__(self):
        super().__init__()
        # Defining submodules
        self.feature_extractor = nn.Sequential(
            nn.Linear(784, 256),
            nn.ReLU(),
            nn.Linear(256, 64),
            nn.ReLU()
        )
        self.classifier = nn.Linear(64, 10)

    def forward(self, x):
        features = self.feature_extractor(x)
        output = self.classifier(features)
        return output