top of page

Science Delight #12: I Needed a Break, So Here’s a Python Intro for Science Nerds

  • Writer: abrokepostgradrese
    abrokepostgradrese
  • Mar 30, 2025
  • 16 min read
A humorous illustration depicting programming languages as characters in a lineup outside a MATLAB-themed room, highlighting Python's dominance and versatility among other languages like R, C, and Scilab.
A humorous illustration depicting programming languages as characters in a lineup outside a MATLAB-themed room, highlighting Python's dominance and versatility among other languages like R, C, and Scilab.

I’m kinda tired, so here’s a little Python guide for physics and data science.


To be honest, I’ve been feeling a bit drained lately—stuck in writing my novel, navigating my postgraduate research, and figuring out financial support. I could ramble about it, but this blog isn’t about my personal dilemmas. Instead, let’s focus on something productive: coding. Or rather, using coding as a tool to make physics and data science easier.


Now, you might think, “Why should I care? Can’t I just solve my physics equations by hand?” Sure, go ahead—if you enjoy spending hours crunching through tedious algebraic manipulations. But in real-world research and development, those nice, compact textbook problems you solved in undergrad don’t scale well. Real equations are messy, datasets are enormous, and nobody has time to do a 10x10 matrix calculation by hand.


Why Bother Learning Python for Physics & Data Science?

There are three types of people when it comes to computational tools in STEM:

  1. The ‘I Hate Coding’ Crowd – They believe everything can be done by hand and that coding is an unnecessary evil.

  2. The Spreadsheet Enthusiasts – They live and breathe Excel, convinced that all computations can be solved with enough rows, columns, and functions.

  3. The Pragmatists – They realize that some problems are best solved with the right tool and that Python (or MATLAB, or R) is a game-changer.


If you’re in the first group, let me ask: Would you manually balance a 50,000-element dataset? Would you compute a Fourier transform of a time series manually? Probably not. Even a 10x10 matrix can get tedious, and that’s considered small in engineering computations. Sure, you could use Excel, but have you ever tried handling anything beyond basic arithmetic there? Spreadsheets work, but they aren’t built for scalable, complex scientific analysis.


The truth is, most of us don’t need to become software engineers. We just need to write scripts that automate our calculations, process data efficiently, and visualize results in a meaningful way. That’s where Python shines.


Why Python Over Other Tools?

Alright, let’s address the alternatives:

  • MATLAB – Great for numerical computing, but expensive.

  • Octave – Free alternative to MATLAB, but mostly terminal-based, which means it lacks usability and convenience.

  • Scilab – Also free and powerful, but not as widely adopted.

  • C++ – Fast and efficient but requires significantly more effort to implement and debug.

  • R – Excellent for statistics, but let’s be real, its syntax isn’t exactly pretty.


Python, on the other hand, is free, flexible, and high-level. It’s designed to be readable and beginner-friendly while still being powerful enough for serious scientific computing. Compared to C, which is a lower-level language (meaning you have to handle memory management and low-level operations yourself), Python lets you focus on solving the problem rather than fighting with the syntax.


And let’s not forget: the libraries. The Python ecosystem has some of the best tools for physics, data science, and engineering:

  • NumPy – Fast numerical calculations, matrix operations, and more.

  • SciPy – Scientific computing, including integration, interpolation, and optimization.

  • Matplotlib & Seaborn – Data visualization to make your research digestible.

  • SymPy – Symbolic mathematics, useful for algebraic manipulation.

  • Pandas – Data manipulation and analysis.

  • TensorFlow/PyTorch – Machine learning, if you want to get fancy.


Python: The Chainsaw for Computational Physics

A professor once told me: Using Python (or MATLAB) for physics is like using a chainsaw—powerful and efficient for cutting through complex problems. Sure, you can do small computations by hand, but why use a chainsaw to cut paper when you can use it to slice through real challenges? (p.s. Honestly, some of us become so accustomed to using Python that we even use it for simple arithmetic, simply because Python is already running. However, we don't intentionally start Python just for those tasks.)


Let’s be honest, when reading research papers, we all have that moment: You come across an elegant (or terrifying) equation. You stare at it. You cry a little. Then you remember, “I have Jupyter Notebook or VS Code!” So you plug in some sample data (probably rand() values) and test it out. Within minutes, it makes sense, and now you can visualize and understand it. That’s the power of coding in research.


How to start?

Honestly, I wouldn't suggest anyone trying out Python for scripting to ask around; just head over to Google Colab and sign in with your Gmail account. (P.S. If you're in engineering or science, I bet you've already downloaded Anaconda or VS Code, though.)


In Colab, simply open a new notebook to experiment with. Since it's cloud-based, you won't need to worry about your computer's memory; it's all about your Google Drive space.


To begin, if you wish to work with your own data, you have two options:

  1. Place your file in the temporary folder (note that it will be deleted each time the runtime disconnects).

  2. Link your Google Drive:

from google.colab import drive
drive.mount('/content/drive')

Next, I prefer not to alter the file locations, so I utilize this code to simplify the file location as a foundation:

import os # Load necessary libraries
import sys 
sys.path.append('/content/drive/MyDrive/') #to store the save files in this location
os.chdir('/content/drive/MyDrive/') #this sets the directory 

Now that we have the os and sys paths set up, we can import a script named "main.py" (just an example, so you know you can import from scripts) from our drive as follows:

'''
The code wouldn't function without os.chdir because it wouldn't be able to locate the file, and from x import*, the * signifies importing everything.
'''
from main import* 

Later, when saving the CSV, we won't need to worry about the directory because the system path has already been appended, ensuring the file is saved to the drive.

import numpy as np
import pandas as pd

# Set the random seed for reproducibility
np.random.seed(42)

# Generate an array of random data
data = np.random.rand(10, 5)  # 10 rows, 5 columns

# Create a Pandas DataFrame from the array
df = pd.DataFrame(data, columns=['col1', 'col2', 'col3', 'col4', 'col5'])

# Save the DataFrame to a CSV file
df.to_csv('random_data.csv', index=False)

At this stage, we've set up the code's environment. Next, we need to install and import the dependencies. We use `!pip install` to install dependencies and `import as` to bring them into our code. Keep in mind that even if you're using VSCode and the dependencies are already installed, you still need to import them to use them!

!pip install numpy, matplotlib

import numpy as np #import as to shorten the name, you can import as n as well but might be a little messy when you import more
import matplotlib.pyplot as plt #the dependency.class/method is a trick to use less compute, or import a specific part of the dependency to use.

So, indeed, this is basically what we need to know to begin with Python, especially if you're familiar with MATLAB, as they are quite similar. However, with Python, you must import functions from other scripts before using them.


Understanding Python Basics

Variables and Data Types

# Integers and Floats
x = 10  # Integer
y = 3.14  # Float
print(type(x), type(y))
# Strings
s = "Hello world!"
print(s.upper()) 
'''
there are several methods to convert the string to uppercase, lowercase, or sentence case. However, in computation, this is often not a primary concern, so I won't elaborate on it. Refer to the documentation if you're interested.
'''

Lists and Dictionaries

Typically, we frequently use lists for computation, especially for data vectorization. I use dictionaries less often, although many of my colleagues highly recommend them. Personally, I prefer declaring global variables more.

# Lists (arrays in Python)
data = [1, 2, 3, 4, 5]
print(data[0])  # First element
# Dictionaries (key-value pairs)
constants = {"pi": 3.1415, "g": 9.81}
print(constants["pi"])  # Access value

NumPy Arrays vs. Lists

NumPy arrays (numpy.ndarray) are designed for efficient numerical computations. While Python lists are flexible, they are not optimized for mathematical operations, which makes NumPy a better choice for scientific computing.

1. Element-wise Operations

In a Python list, multiplying by 2 duplicates the elements, but in NumPy, it performs element-wise multiplication.

# Using a Python list 
lst = [1, 2, 3, 4] 
print(lst * 2) # Output: [1, 2, 3, 4, 1, 2, 3, 4] (Repeats elements, not multiplication) 
# Using a NumPy array 
import numpy as np 
arr = np.array([1, 2, 3, 4]) 
print(arr * 2) # Output: [ 2 4 6 8 ] (Multiplies each element)

2. Faster and More Memory-Efficient

NumPy arrays are stored in contiguous memory blocks, allowing much faster access and computations than Python lists, which store references to objects.

import time 
# Large list computation 
lst = list(range(1_000_000)) 
start = time.time() 
lst_result = [x * 2 for x in lst] 
print("List time:", time.time() - start) 
# Large NumPy array computation 
arr = np.array(range(1_000_000)) 
start = time.time() 
arr_result = arr * 2 
print("NumPy time:", time.time() - start)

👉 NumPy is significantly faster because it uses optimized C and Fortran routines internally.


3. Built-in Mathematical Functions

NumPy provides vectorized operations, meaning you can apply math functions directly without writing loops.

arr = np.array([1, 2, 3, 4]) 
# Square each element 
print(np.square(arr)) # [ 1 4 9 16 ] 
# Compute the sine of each element 
print(np.sin(arr)) 
# Mean and standard deviation
print(np.mean(arr), np.std(arr))

4. Support for Multi-Dimensional Arrays

Python lists are one-dimensional by default, while NumPy supports matrices and multi-dimensional arrays for physics, engineering, and ML.

matrix = np.array([[1, 2], [3, 4]]) 
print(matrix * 2)

5. Broadcasting for Different Shapes

NumPy allows operations between arrays of different shapes without explicit looping.

A = np.array([[1, 2], [3, 4]]) 
B = np.array([10, 20]) # Smaller array 
print(A + B) # Automatically expands B for element-wise addition

Functions in Python

In Python, functions or methods serve as reusable pieces of code. If a segment of code is going to be used more than three times, I typically convert it into a function. This is just my rule of thumb, as it significantly tidies up my code. However, I'm not a software engineer, so I don't focus on clean code unless it's necessary for presentation.


Remember the ":", as we sometimes forget it and then wonder about the source of the error. Also, "def" is somewhat like defining the function (that's how I refer to it when teaching the juniors).

def kinetic_energy(m, v):
    return 0.5 * m * v**2

print(kinetic_energy(2, 5))  # Outputs 25.0

Object-Oriented Programming (OOP) in Python

So, some of you might already be familiar with class, init, and other dunder (double underscore) methods. If you're dealing with basic data analysis, you probably don’t need them often. But when structuring large projects or working with things like Neural Networks (NN), simulations, or reusable physics models, classes become extremely useful.


Why Use Classes in Physics and Data Science?

While simple scripts and functions work well for quick calculations, larger projects can get messy. Classes help by grouping related functions (methods) and variables (attributes) together, making code modular, reusable, and organized.


Defining a Simple Class

Let’s define a basic class for particles in physics:

class Particle:
    def __init__(self, mass, velocity):
        self.mass = mass  # Assign mass attribute
        self.velocity = velocity  # Assign velocity attribute

    def kinetic_energy(self):
        return 0.5 * self.mass * self.velocity**2  # K.E formula

# Creating an instance (object) of the Particle class
electron = Particle(9.11e-31, 2.2e6)  
print(electron.kinetic_energy())  # Outputs KE for an electron

Here, init is a dunder method (double underscore) that initializes the object with given values. Instead of passing mass and velocity repeatedly into functions, we store them inside an object, making it easier to track attributes.


Adding More Methods

We can extend the class by adding more physics-related methods:

class Particle:
    def __init__(self, mass, velocity, charge):
        self.mass = mass
        self.velocity = velocity
        self.charge = charge  # Adding charge attribute

    def kinetic_energy(self):
        return 0.5 * self.mass * self.velocity**2

    def momentum(self):
        return self.mass * self.velocity  # p = mv

    def lorentz_force(self, E, B):
        """Calculates Lorentz force F = q(E + v × B)"""
        return self.charge * (E + self.velocity * B)

# Example usage
proton = Particle(1.67e-27, 1.0e5, 1.6e-19)  
print(proton.momentum())  # Outputs momentum
print(proton.lorentz_force(5, 0.1))  # Example force calculation

This structure is cleaner and avoids repetitive code—especially useful for simulating multiple particles instead of defining separate functions for each one.


Dunder Methods (Magic Methods)

Python has special "dunder" (double underscore) methods, which allow objects to behave like built-in data types.


String Representation (__str__)

If you print an object, Python normally prints something unreadable like <__main__.Particle object at 0x0000...>. To make it meaningful, override str:

class Particle:
    def __init__(self, mass, velocity):
        self.mass = mass
        self.velocity = velocity

    def kinetic_energy(self):
        return 0.5 * self.mass * self.velocity**2

    def __str__(self):
        return f"Particle(mass={self.mass}, velocity={self.velocity})"

electron = Particle(9.11e-31, 2.2e6)
print(electron)  # Outputs: Particle(mass=9.11e-31, velocity=2200000.0)

Operator Overloading (__add__, mul, etc.)

Let's say we want to add two particles' momenta using + instead of manually calling functions:

class Particle:
    def __init__(self, mass, velocity):
        self.mass = mass
        self.velocity = velocity

    def momentum(self):
        return self.mass * self.velocity

    def __add__(self, other):
        return self.momentum() + other.momentum()

proton = Particle(1.67e-27, 1e5)
neutron = Particle(1.67e-27, -1e5)

total_momentum = proton + neutron  # Calls __add__ method
print(total_momentum)  # Output: 0.0 (since their momenta cancel out)

Here, add lets us use + to sum momenta, making the code more intuitive.


Making an Iterable Class (__iter__, next)

We can even make a class iterable to loop over multiple instances:

class ParticleCollection:
    def __init__(self, particles):
        self.particles = particles
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.particles):
            particle = self.particles[self.index]
            self.index += 1
            return particle
        else:
            raise StopIteration

# Example Usage
particles = [Particle(1.67e-27, v) for v in [1e5, 2e5, 3e5]]
collection = ParticleCollection(particles)

for p in collection:
    print(p.momentum())  # Iterates through the collection

This is useful when dealing with large simulations of physical objects.


When to Use OOP in Physics & Data Science?

Object-oriented programming is useful when:✔️ You need to simulate multiple objects (e.g., planetary motion, molecular dynamics).✔️ You want reusable, structured code (e.g., multiple experiment setups).✔️ You're building Neural Networks (e.g., PyTorch and TensorFlow use OOP heavily).❌ If you're just running quick calculations, OOP might be overkill.


🐍 Python Control Flow: If, For, and While Loops (Simplified & Fun)

Control flow helps us make decisions and repeat actions in Python. Let’s break it down simply before diving into complex logic! 🚀


When I first started learning C++ programming, I recall avoiding Python because I found loops somewhat complex. Now, I'm trying to simplify the learning process.


🔹 IF Statements: Making Decisions

💡 Analogy: Imagine a smart door that opens only if you have the right passcode.

passcode = "open123" 
if passcode == "open123": 
	print("✅ Door Unlocked!") 
else: 
	print("❌ Wrong Passcode, Try Again!")

➡ Explanation: If the condition (passcode == "open123") is True, it unlocks the door. Otherwise, it denies access.


📌 Bonus: Add an elif (else-if) condition!

passcode = "guest123" 
if passcode == "open123": 
	print("✅ Door Unlocked!") 
elif passcode == "guest123": 
	print("🔑 Guest Mode Enabled!") 
else: 
	print("❌ Wrong Passcode, Try Again!")

🔹 FOR Loops: Repeating Actions

💡 Analogy: Instead of writing print(1), print(2), etc., a loop does it automatically!

for num in range(1, 6): 
	print(f"🔢 Number: {num}")

📌 Breakdown:

  • range(1, 6): Counts from 1 to 5 (6 is excluded).

  • Each iteration, num takes the next value in the sequence.


🔹 WHILE Loops: Repeat Until a Condition is Met

💡 Analogy: A robot continues to walk until it encounters a wall. (although, this might not be the best method to program a robot)

distance = 0 
while distance < 5: 
	print(f"🚶 Walking... Distance: {distance}") 
	distance += 1  # Move forward

📌 Breakdown:

  • The loop keeps running while distance < 5.

  • It stops when distance == 5.

🔴 ⚠ Warning: If you forget to update distance, it will loop forever (infinite loop)!


🔹 ENUMERATE: Numbering Items in Loops

💡 Analogy: You have a list of players, and you want to print them with rankings.

🛑 Without enumerate():

players = ["Alice", "Bob", "Charlie"] 
rank = 1  # Start at 1 
for player in players: 
	print(f"🏆 Rank {rank}: {player}") 
	rank += 1  # Increase rank

✅ With enumerate():

players = ["Alice", "Bob", "Charlie"] 
for rank, player in enumerate(players, start=1): 
	print(f"🏆 Rank {rank}: {player}")

📌 Why use enumerate()?

  • It automatically counts for us!

  • No need for extra rank variables!


🧠 Challenge: Nested Loops & Complex Logic

Loops can be combined to create complex logic. Here’s a multiplication table generator!

for i in range(1, 6): # Rows 
	for j in range(1, 6): # Columns 
		print(f"{i} × {j} = {i * j}", end=" ") 
	print() # Newline after each row

📌 Breakdown:

  • The outer loop (i) represents the row number.

  • The inner loop (j) represents the column number.

  • end=" " keeps the output on the same line.

  • For your information, Python is sensitive to indentation. Therefore, if your print statement aligns with the first for loop, it will output results from the first for loop, and the same applies in reverse.


Why do we choose different dependencies/libraries for different use cases

Case study: 🧠 Building a Simple Neural Network in NumPy, PyTorch, and TensorFlow/Keras

Now that control flow is clear, let’s explore how Python builds a basic Neural Network (NN)!

📌 Goal: Create a 3-layer NN (Input → Hidden → Output) using:

  1. NumPy (Manual Approach) 🛠️

  2. PyTorch (Deep Learning Framework) 🔥

  3. TensorFlow/Keras (High-Level API) 🤖


1️⃣ NumPy: Building an NN from Scratch 🏗️

💡 Concept: We manually define weights, activation functions, and forward propagation.

import numpy as np

# Activation Function: Sigmoid
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# Input Data (2 Samples, 2 Features)
X = np.array([[0.1, 0.2], [0.3, 0.4]])

# Randomly Initialize Weights and Biases
np.random.seed(42)
W1 = np.random.randn(2, 3)  # 2 Inputs → 3 Hidden Neurons
b1 = np.zeros((1, 3))
W2 = np.random.randn(3, 1)  # 3 Hidden → 1 Output
b2 = np.zeros((1, 1))

# Forward Pass
hidden_layer = sigmoid(np.dot(X, W1) + b1)
output_layer = sigmoid(np.dot(hidden_layer, W2) + b2)

print("🔢 Output (NumPy NN):", output_layer)

📌 What’s happening?

  • Step 1: Input X (2 samples, 2 features)

  • Step 2: We apply weights W1 and bias b1 for the hidden layer

  • Step 3: Another weight W2 and bias b2 for the output layer

  • Step 4: Sigmoid activation squashes values between 0 and 1

  • Step 5: Final prediction is printed!

🔥 Why use NumPy? It's great for learning the math behind NNs!


2️⃣ PyTorch: Simpler and More Powerful! 🔥

💡 Why PyTorch? It has automatic differentiation (backpropagation is easy!).

import torch
import torch.nn as nn
import torch.optim as optim

# Define a Simple NN (2 Inputs → 3 Hidden → 1 Output)
class SimpleNN(nn.Module):
    def __init__(self):
        super(SimpleNN, self).__init__()
        self.hidden = nn.Linear(2, 3)  # Input to Hidden
        self.output = nn.Linear(3, 1)  # Hidden to Output
        self.activation = nn.Sigmoid()  # Sigmoid Activation

    def forward(self, x):
        x = self.activation(self.hidden(x))  # Hidden Layer
        x = self.activation(self.output(x))  # Output Layer
        return x

# Create Model
model = SimpleNN()

# Example Input (Tensor)
X_torch = torch.tensor([[0.1, 0.2], [0.3, 0.4]], dtype=torch.float32)

# Forward Pass
output = model(X_torch)
print("🔢 Output (PyTorch NN):", output.detach().numpy())

📌 Why PyTorch?

  • No need to manually calculate matrix multiplications!

  • nn.Linear() creates automatic weights & biases.

  • forward() defines the flow of data.

  • Backpropagation and optimization are easy!


3️⃣ TensorFlow/Keras: The Easiest API! 🤖

💡 Why Keras? It's straightforward, comprehensible, and easy to train! P.S. I don't use TensorFlow or Keras because Colab seems to prefer Torch. Additionally, I can't import TensorFlow or Keras due to limited space on my machine.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

# Create a Simple Model
model = keras.Sequential([
    layers.Dense(3, activation="sigmoid", input_shape=(2,)),  # Input → Hidden
    layers.Dense(1, activation="sigmoid")  # Hidden → Output
])

# Example Input (Tensor)
X_tf = tf.constant([[0.1, 0.2], [0.3, 0.4]])

# Forward Pass
output = model(X_tf)
print("🔢 Output (TensorFlow/Keras NN):", output.numpy())

📌 Why TensorFlow/Keras?

  • Sequential() makes NN creation super simple

  • No need to manually write the forward() function

  • Handles big models with ease


Now that we’ve built simple neural networks, let's train them to make meaningful predictions! 🚀

📌 Goal: Train our 3-layer NN to map inputs [x1, x2] to a target output [y]📌 Dataset:

  • Input (X) → Two features [x1, x2]

  • Target (y) → A single output


1️⃣ NumPy: Manual Training with Gradient Descent 🛠️

Since NumPy doesn't handle backpropagation, we must manually compute gradients.

import numpy as np

# Sigmoid Activation and Derivative
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return x * (1 - x)  # Derivative of Sigmoid

# Training Data (X: Inputs, y: Targets)
X = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
y = np.array([[0.3], [0.7], [0.9]])  # Some arbitrary target values

# Initialize Weights and Biases
np.random.seed(42)
W1 = np.random.randn(2, 3)  # Input → Hidden
b1 = np.zeros((1, 3))
W2 = np.random.randn(3, 1)  # Hidden → Output
b2 = np.zeros((1, 1))

# Training Loop
learning_rate = 0.1
epochs = 5000  # Number of Training Iterations

for epoch in range(epochs):
    # Forward Pass
    hidden_layer = sigmoid(np.dot(X, W1) + b1)
    output_layer = sigmoid(np.dot(hidden_layer, W2) + b2)

    # Compute Error
    error = y - output_layer

    # Backpropagation
    d_output = error * sigmoid_derivative(output_layer)
    d_hidden = d_output.dot(W2.T) * sigmoid_derivative(hidden_layer)

    # Update Weights and Biases
    W2 += hidden_layer.T.dot(d_output) * learning_rate
    b2 += np.sum(d_output, axis=0, keepdims=True) * learning_rate
    W1 += X.T.dot(d_hidden) * learning_rate
    b1 += np.sum(d_hidden, axis=0, keepdims=True) * learning_rate

    # Print Loss Occasionally
    if epoch % 1000 == 0:
        loss = np.mean(np.square(error))
        print(f"Epoch {epoch} | Loss: {loss:.4f}")

# Final Prediction
print("🔢 Final Output (NumPy NN):", output_layer)

📝 Explanation:

  • Forward Pass: Compute outputs layer by layer

  • Loss Computation: Find the difference between predictions (output_layer) and actual (y)

  • Backpropagation: Compute gradients to adjust weights (W1, W2)

  • Gradient Descent Update: Apply small updates to weights in the direction that reduces error

📌 Downside:

  • We manually coded backpropagation, which is complex 😵

  • No GPU acceleration (NumPy runs on CPU only)

🔥 Great for learning, but let's move to PyTorch for easier training!


2️⃣ PyTorch: Automatic Differentiation & Backpropagation 🔥

PyTorch automates gradients, making training much simpler!

import torch
import torch.nn as nn
import torch.optim as optim

# Define a Neural Network
class SimpleNN(nn.Module):
    def __init__(self):
        super(SimpleNN, self).__init__()
        self.hidden = nn.Linear(2, 3)  # Input → Hidden
        self.output = nn.Linear(3, 1)  # Hidden → Output
        self.activation = nn.Sigmoid()

    def forward(self, x):
        x = self.activation(self.hidden(x))  # Hidden Layer
        x = self.activation(self.output(x))  # Output Layer
        return x

# Create Model
model = SimpleNN()

# Training Data (Tensors)
X_torch = torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], dtype=torch.float32)
y_torch = torch.tensor([[0.3], [0.7], [0.9]], dtype=torch.float32)

# Define Loss Function & Optimizer
criterion = nn.MSELoss()  # Mean Squared Error
optimizer = optim.SGD(model.parameters(), lr=0.1)  # Stochastic Gradient Descent

# Training Loop
epochs = 5000
for epoch in range(epochs):
    optimizer.zero_grad()  # Reset Gradients
    output = model(X_torch)  # Forward Pass
    loss = criterion(output, y_torch)  # Compute Loss
    loss.backward()  # Compute Gradients
    optimizer.step()  # Update Weights

    # Print Loss Occasionally
    if epoch % 1000 == 0:
        print(f"Epoch {epoch} | Loss: {loss.item():.4f}")

# Final Prediction
print("🔢 Final Output (PyTorch NN):", model(X_torch).detach().numpy())

🔥 Why PyTorch?

  • No manual gradient calculations (loss.backward() does it)

  • optimizer.step() updates weights automatically

  • Supports GPUs! (.cuda() moves data to GPU)


3️⃣ TensorFlow/Keras: The Easiest Training Setup 🤖

Keras makes training even simpler with fit().

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

# Create a Simple Model
model = keras.Sequential([
    layers.Dense(3, activation="sigmoid", input_shape=(2,)),  # Input → Hidden
    layers.Dense(1, activation="sigmoid")  # Hidden → Output
])

# Compile Model (Loss & Optimizer)
model.compile(optimizer="sgd", loss="mse")  # MSE Loss, SGD Optimizer

# Training Data (NumPy Arrays)
X_tf = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
y_tf = np.array([[0.3], [0.7], [0.9]])

# Train Model
model.fit(X_tf, y_tf, epochs=5000, verbose=0)  # Train for 5000 epochs

# Final Prediction
print("🔢 Final Output (TensorFlow/Keras NN):", model.predict(X_tf))

📝 Why TensorFlow/Keras?

  • Shortest code!

  • model.fit() handles training, backpropagation, and updates automatically

  • Best for production & deployment (runs on mobile, web, and cloud!)


📊 Framework Comparison: NumPy vs PyTorch vs TensorFlow

Feature

NumPy 🛠️

PyTorch 🔥

TensorFlow 🤖

Ease of Use

Hard (Manual Math)

Medium (Some Automation)

Easy (High-Level)

Performance

Slow

Fast (GPU Support)

Very Fast (Optimized)

Best For

Learning Concepts

Research & Flexibility

Production & Deployment


🚀 Framework Comparison: Training in NumPy, PyTorch, and TensorFlow

Feature

NumPy 🛠️

PyTorch 🔥

TensorFlow 🤖

Ease of Training

Hard (Manual Gradients)

Medium (Automatic Backprop)

Very Easy (fit())

GPU Support

❌ No

✅ Yes

✅ Yes

Best For

Learning Theory

Research, Flexibility

Production, Deployment

🎯 Key Takeaways

  1. NumPy teaches how NNs work mathematically 📖

  2. PyTorch gives research flexibility and GPU acceleration 🚀

  3. TensorFlow/Keras is the simplest way to train models quickly 🎯


💡 Summary: How Do These NNs Work?

Each framework does the same thing: Takes an input, applies weights, and gives an output.

  1. NumPy: Fully manual (good for learning).

  2. PyTorch: More flexibility (great for research).

  3. TensorFlow/Keras: Simplest & best for big projects.



🚀 Final Thoughts: Where to Go Next?

Learning Python for scientific computing is a journey, and mastering both functional and object-oriented programming (OOP) will make your code more organized, reusable, and scalable. You don’t need OOP for everything, but when your physics, data science, or machine learning projects grow in complexity, structuring your code properly can save you time and effort.

💡 Next Steps:

  • Experiment with OOP: Try building your own particle simulation, system model, or data processing pipeline using Python classes.

  • Explore real-world scientific projects: Check out libraries like Astropy (astronomy), SimPy (discrete event simulation), and SciPy (scientific computing) to see how Python is used professionally.

  • Apply Python classes in ML & simulations: Machine learning frameworks like PyTorch and TensorFlow rely on OOP, so getting comfortable with it will help when implementing custom models and advanced simulations.

  • Balance self-learning with tutorials (tutorial hell): Writing your own code is crucial, but don’t avoid tutorials entirely. They’re useful when you're stuck, but the key is to apply what you learn rather than just passively watching.

  • Ignore the 'vibe coding' critics: If your learning approach is working, stick with it! Whether it's hands-on coding, AI-assisted programming, or structured courses—progress matters more than the method.

And finally, whenever you hit a roadblock—Google it, read the docs, and experiment! That’s how every great coder learns. 😉

Happy coding! 🚀

Comments


bottom of page