Progress · 0/6 phases
📐 Phase 1 — Python & Math Foundations (Days 1–15)
8 min read · Days 1–15 · Notion
Core insight: Every ML model is a function with parameters, fit to data by minimizing a loss using calculus (gradients) and linear algebra (matrix operations). If you understand
y = Xw + band how to adjustwto reduce error, you understand the seed from which every neural network and LLM grows.
Day 1–3 — NumPy and vectorized computation
import numpy as np
# Everything in ML is array operations. Never loop over data points in Python.
X = np.array([[1, 2], [3, 4], [5, 6]]) # 3 samples, 2 features
w = np.array([0.5, -0.2])
# Matrix-vector multiply: this IS what a neural network layer does
predictions = X @ w # shape (3,)
# Broadcasting: operations on different-shaped arrays without explicit loops
X_normalized = (X - X.mean(axis=0)) / X.std(axis=0)
# Vectorized vs loop (ALWAYS vectorize)
# SLOW:
result = []
for i in range(len(X)):
result.append(X[i] @ w)
# FAST:
result = X @ w
# Random number generation with seeds (reproducibility)
rng = np.random.default_rng(42)
X = rng.normal(loc=0, scale=1, size=(100, 5))
# Key operations you'll use constantly
np.dot(a, b) # dot product / matrix multiply
np.linalg.norm(v) # vector magnitude
np.exp(x) # used in sigmoid, softmax
np.argmax(x, axis=1) # predicted class from logitsKey skills
- Array creation, indexing, slicing, boolean masking
- Broadcasting rules (shapes must be compatible)
axisparameter (0=down columns, 1=across rows) — the #1 source of bugs- Matrix multiplication
@vs element-wise*
Day 4–6 — Pandas for data manipulation
import pandas as pd
df = pd.read_csv('data.csv')
# Inspect
df.head(); df.info(); df.describe()
df.isnull().sum() # missing values per column
# Selection
df[df['age'] > 30] # filter
df.groupby('category')['price'].mean() # aggregate
df['new_col'] = df['a'] + df['b'] # feature engineering
# Handling missing data
df['col'].fillna(df['col'].median(), inplace=True)
df.dropna(subset=['important_col'])
# Encoding categorical variables (you'll do this before every ML model)
pd.get_dummies(df, columns=['category']) # one-hot encoding
# Train/test split prep
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)Key skills
- Loading, cleaning, filtering, grouping data
- Handling missing values (drop vs impute — know the tradeoff)
- Encoding categorical features (one-hot, label encoding)
- Merging/joining dataframes
Day 7–10 — Linear algebra for ML
# Vectors and dot products
# Dot product measures alignment: a . b = |a||b|cos(theta)
# This is THE operation inside every neuron, every attention score
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
dot = np.dot(a, b) # = 1*4 + 2*5 + 3*6 = 32
# Matrices as linear transformations
# A matrix multiply ROTATES, SCALES, and SHEARS vectors
A = np.array([[2, 0], [0, 3]]) # scales x by 2, y by 3
v = np.array([1, 1])
transformed = A @ v # [2, 3]
# Eigenvalues and eigenvectors (foundation of PCA)
# Eigenvector: a direction that the matrix only SCALES, doesn't rotate
A = np.array([[4, 1], [2, 3]])
eigenvalues, eigenvectors = np.linalg.eig(A)
# PCA from scratch (dimensionality reduction)
def pca(X, n_components):
X_centered = X - X.mean(axis=0)
cov_matrix = np.cov(X_centered.T)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
idx = np.argsort(eigenvalues)[::-1]
top_vectors = eigenvectors[:, idx[:n_components]]
return X_centered @ top_vectors
# Matrix rank, determinant, inverse (used in normal equation, covariance)
np.linalg.det(A)
np.linalg.inv(A)
np.linalg.matrix_rank(A)Key skills
- Vectors, dot product, vector norms
- Matrix multiplication and what it geometrically represents
- Eigenvalues/eigenvectors (used in PCA, and conceptually in understanding what neural network layers do)
- Why a neural network layer is just
Wx + b(a matrix multiply plus a shift)
Day 11–13 — Calculus: derivatives and gradients
# A derivative tells you: if I nudge x slightly, how much does f(x) change?
# This is THE concept that makes all of ML/DL work: gradient descent
# Numerical derivative (for intuition, never used in practice)
def numerical_derivative(f, x, h=1e-7):
return (f(x + h) - f(x - h)) / (2 * h)
f = lambda x: x**2
print(numerical_derivative(f, 3)) # approx 6, matches df/dx = 2x at x=3
# Chain rule: THE most important rule in all of deep learning
# If y = f(g(x)), then dy/dx = f'(g(x)) * g'(x)
# This is literally what backpropagation computes, layer by layer
# Gradient descent: the core training algorithm of ALL of ML
def gradient_descent(f, grad_f, x0, lr=0.1, steps=100):
x = x0
history = [x]
for _ in range(steps):
grad = grad_f(x)
x = x - lr * grad # move OPPOSITE the gradient (downhill)
history.append(x)
return x, history
# Minimize f(x) = (x - 3)^2, whose minimum is at x=3
f = lambda x: (x - 3)**2
grad_f = lambda x: 2 * (x - 3)
x_min, history = gradient_descent(f, grad_f, x0=0, lr=0.1, steps=50)
print(x_min) # converges to ~3.0
# Partial derivatives: gradient of a multi-variable function
# grad f(x, y) = [df/dx, df/dy] -- points in direction of steepest ASCENT
# Gradient descent moves in the OPPOSITE directionKey skills
- What a derivative means (rate of change)
- The chain rule (this IS backpropagation, conceptually)
- Gradient descent: the single algorithm that trains linear regression, logistic regression, neural networks, and LLMs
- Partial derivatives and gradients (vectors of partial derivatives)
Day 14–15 — Probability and statistics fundamentals
# Distributions you'll see everywhere
from scipy import stats
# Normal distribution: weight initialization, noise modeling
stats.norm.pdf(x=0, loc=0, scale=1)
# Bernoulli/Binomial: binary classification outputs
stats.bernoulli.pmf(k=1, p=0.7)
# Bayes' theorem: P(A|B) = P(B|A) * P(A) / P(B)
# Foundation of Naive Bayes classifiers, and conceptually of how LLMs
# can be seen as learning P(next_token | previous_tokens)
# Maximum Likelihood Estimation (MLE)
# The principle behind almost EVERY loss function in ML:
# "find parameters that make the observed data most probable"
# Cross-entropy loss DERIVED from MLE for classification:
def cross_entropy(y_true, y_pred_probs):
# y_true: one-hot encoded true labels
# y_pred_probs: predicted probabilities
return -np.sum(y_true * np.log(y_pred_probs + 1e-9))
# This is the loss function used to train:
# - logistic regression
# - neural network classifiers
# - LLMs (next-token prediction is literally cross-entropy over vocabulary)
# Mean Squared Error (MSE) -- derived from MLE assuming Gaussian noise
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)Key skills
- Probability distributions (normal, bernoulli, categorical)
- Bayes' theorem and conditional probability
- Maximum Likelihood Estimation — the principle behind every loss function
- Why cross-entropy is THE loss function for classification (and for LLMs)
Phase 1 Capstone Project: Linear Regression From Scratch
Deliverable: implement linear regression with gradient descent, no sklearn
import numpy as np
class LinearRegressionScratch:
def __init__(self, lr=0.01, n_iters=1000):
self.lr = lr
self.n_iters = n_iters
self.weights = None
self.bias = None
self.loss_history = []
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
for _ in range(self.n_iters):
y_pred = X @ self.weights + self.bias
# Loss: Mean Squared Error
loss = np.mean((y_pred - y) ** 2)
self.loss_history.append(loss)
# Gradients (derived by hand using calculus -- do this derivation yourself!)
dw = (2 / n_samples) * X.T @ (y_pred - y)
db = (2 / n_samples) * np.sum(y_pred - y)
# Gradient descent update
self.weights -= self.lr * dw
self.bias -= self.lr * db
def predict(self, X):
return X @ self.weights + self.bias
# Compare against sklearn to verify correctness
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=200, n_features=3, noise=10, random_state=42)
model = LinearRegressionScratch(lr=0.01, n_iters=2000)
model.fit(X, y)
sklearn_model = LinearRegression()
sklearn_model.fit(X, y)
print("Scratch weights:", model.weights)
print("Sklearn weights:", sklearn_model.coef_)
# These should match closelyRequirements:
- Implement gradient descent by hand — derive the gradient of MSE with respect to weights yourself on paper first
- Plot the loss curve over iterations — it should monotonically decrease
- Compare your weights to sklearn's
LinearRegression— they should match within a small tolerance - Implement logistic regression from scratch as a bonus (sigmoid + cross-entropy loss + gradient descent)
- Write a short explanation: why does the learning rate matter? Try
lr=0.5andlr=0.0001and explain what happens to each
Common mistakes
Mistake 1
❌ Looping over data points instead of vectorizing.
A Python for-loop over 1 million data points is 100-1000x slower than the equivalent NumPy vectorized operation.
✅ Correct approach: Always ask "can this be expressed as a matrix operation?" before writing a loop. X @ w replaces a loop computing dot products row by row.
Mistake 2
❌ Not normalizing/scaling features before gradient descent.
If one feature ranges 0-1 and another ranges 0-100,000, gradient descent will oscillate wildly or converge very slowly because the loss surface is badly distorted.
✅ Correct approach: Standardize features (subtract mean, divide by std) before training any gradient-based model. This is why StandardScaler exists in sklearn.
Mistake 3
❌ Treating "I can call .fit()****" as understanding the model.
This is the single biggest gap between people who can use ML libraries and people who can debug, improve, or explain why a model isn't working.
✅ Correct approach: For every model in this roadmap, implement a simplified version from scratch BEFORE using the library version. The library version becomes the convenient tool; the scratch version is the understanding.