Introduction to Machine Learning

Math and Python:
Your Computational Toolkit

The same idea, written twice: once in notation, once in code

Six questions, three languages

    Everything here is used in every lecture that follows. Most of it is used far beyond this course.

    Every Idea, Written Twice

    Notation
    \( \theta^\top x \)
    Compact, exact, and the way papers and textbooks will speak to you.
    Code
    theta @ x
    Runnable, checkable, and the way you will actually test an idea.

    This session is a translation exercise. Every symbol you meet has a line of code, and every line of code has a symbol.

    Bring your laptop The five notebooks are the lab. These slides are the map.

    And a third language, the first one: the sentence you would say out loud. If you cannot say it in English, the symbols will not save you. The next slide says one idea all three ways.

    Python in six linesEnough to read every code slide in this lecture

    
    import numpy as np              # a library: arrays and linear algebra
    x = np.array([2.0, 3.0, 1.0])   # a vector
    print(x.shape)                  # (3,): one axis holding three numbers
    A = np.array([[1, 2], [3, 4]])  # a matrix: a list of rows
    print(A.shape)                  # (2, 2)
    print(A @ A, A * A)             # matrix product, then elementwise
        

    Press Run, then change a number and predict the output before you run it again. (3,), with its trailing comma, is how Python writes a shape with one axis.

    The loop that predicts the future

    EnglishNext year's population is this year's, grown by the same rate \(r\).
    Mathematics\( P_{t+1} = (1 + r)\, P_t \)
    Pythonfor year in range(1970, 2023):
        P = P * (1 + r)

    One step, repeated: that is how a model steps into the future, and it is a for loop. The rate here is 1950 to 1970's average.

    Try it: the loopWorld population, 1950 to 2023

    
    import pandas as pd
    
    P = pd.read_csv('world_population.csv').set_index('year').population
    r = (P[1970] / P[1950]) ** (1 / 20) - 1   # growth per year, 1950 to 1970
    
    p = P[1970]
    for year in range(1970, 2023):
        p = p * (1 + r)                        # next year: this year, grown by r
    print(f'r = {r:.2%}: the loop says {p / 1e9:.2f} billion in 2023')
    print(f'the world had {P[2023] / 1e9:.2f} billion')
        

    About 1.99%, 10.48 billion predicted, 8.09 billion real. The loop was fine; the rate changed.

    Part 2 of 6

    Vectors

    What is one example, as something you can compute with?

    Vectors

    drag either arrow head

    Definition An ordered list of numbers, \( x \in \mathbb{R}^n \).
    \( x = [x_1, x_2, \dots, x_n]^\top \)
    Geometry A direction with a length. Addition is placing them head to tail; the sum is the diagonal.
    In machine learning One example is a vector. One row of your data table.

    Vector Operations

    import numpy as np
    
    x = np.array([2, 3, 1])
    y = np.array([1, -2, 4])
    print(x.shape, len(x))
    
    # addition and scaling
    print(x + y)
    print(3 * x)
    
    # the inner product
    print(np.dot(x, y))
    print(np.linalg.norm(x))

    The inner product \( x^\top y = \sum_i x_i y_i \) is the single most common operation in this course.

    How Long, How Far, How Similar

    drag the point: one vector, three lengths

    Three norms
    \( \lVert x \rVert_2 = \sqrt{\sum_i x_i^2} \quad \lVert x \rVert_1 = \sum_i |x_i| \quad \lVert x \rVert_\infty = \max_i |x_i| \)
    Squared length is a dot product \( \lVert x \rVert_2^2 = x^\top x \), and the distance between two examples is \( \lVert x - y \rVert_2 \). Every squared-error loss is that one quantity.
    Angle, not just length
    \( \cos\varphi = \dfrac{x^\top y}{\lVert x \rVert_2 \, \lVert y \rVert_2} \)
    A zero dot product means perpendicular. This is how embedding models compare two things.
    Which one to minimise \( \lVert \cdot \rVert_2 \) spreads error evenly; \( \lVert \cdot \rVert_1 \) as a loss tolerates a few large mistakes; as a penalty on \(\theta\) it pushes most entries to exactly zero.

    Try it: vectors and matricesChange a number, predict the output, then press Run

    
    import numpy as np
    
    x = np.array([2, 3, 1])
    y = np.array([1, -2, 4])
    print(x + y, 3 * x, np.dot(x, y), np.linalg.norm(x))
    
    A = np.array([[1, 2, 3],
                  [4, 5, 6]])
    print(A * A)      # elementwise
    print(A @ A.T)    # matrix product: (2, 3) times (3, 2)
        

    Try A @ A. The error message is the shape rule speaking: the inner dimensions must match.

    Part 3 of 6

    Matrices

    What does a matrix do?

    Matrices

    Definition A rectangular array, \( A \in \mathbb{R}^{m \times n} \): \(m\) rows, \(n\) columns.
    In machine learning Your whole dataset: one row per example, one column per feature.
    \( X \in \mathbb{R}^{m \times d} \)
    Also A linear transformation: \(A x\) sends a vector to another vector.

    \( C = AB \): hover a cell of C to see which row and column produce it

    Matrices in NumPy

    A = np.array([[1, 2, 3],
                  [4, 5, 6]])
    print(A.shape)   # (2, 3)
    print(A.size)    # 6
    
    B = np.zeros((3, 3))
    C = np.ones((2, 4))
    D = np.eye(3)
    E = np.random.rand(2, 3)
    
    # the two products people confuse
    print(A * A)        # elementwise
    print(A @ A.T)      # matrix product
    The classic bug A * B multiplies elementwise; A @ B is the matrix product. They are different operations, and NumPy will happily run the wrong one.

    A matrix is a machine that moves space

    The first column is where \(\hat\imath\) lands, the second where \(\hat\jmath\) lands. Drag their tips. Every other point goes along: \(x_1\hat\imath + x_2\hat\jmath\) lands at \(x_1 A\hat\imath + x_2 A\hat\jmath = Ax\).

    Two machines in a row are one machine

    \(R\) turns the plane a quarter turn; \(S\) shears it. Doing one and then the other is multiplying: the machine you run first sits on the right, next to \(x\).

    The two F's end up in different places: \(RS \neq SR\). Order matters for matrices, which is why \(\hat\theta = (X^\top X)^{-1} X^\top y\) has to be read carefully.

    Two Ways to Read \( Ax \)

    Row picture Each entry of the output is one dot product:
    \( (Ax)_i = a_i^\top x \)
    Row \(i\) of \(A\) asks one question about \(x\).
    Column picture The output is a weighted sum of the columns:
    \( Ax = x_1 a_{(1)} + x_2 a_{(2)} + \dots + x_n a_{(n)} \)
    \(x\) is a recipe; the columns are the ingredients.
    The matrix in the plot
    \( A = \begin{bmatrix} \color{#61afef}{1.5} & \color{#98c379}{0.5} \\ \color{#61afef}{0.5} & \color{#98c379}{1.5} \end{bmatrix}, \quad x = \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} \)
    The first column and the second are the two dashed arrows.

    dashed: columns of \(A\) · solid: scaled by the sliders · yellow: their sum

    make the columns dependent
    Why the second one matters Every vector you can reach is a combination of the columns. That set is the column space, and \( Ax = b \) is solvable exactly when \(b\) lies in it.

    Span, Independence, Rank

    • Span. Everything you can build from a set of vectors by scaling and adding. Two independent vectors in the plane span the plane; two parallel ones span only a line.
    • Linear independence. No vector in the set is a combination of the others, which is the same as saying only the zero recipe reaches zero. \( Xv = 0 \;\Longrightarrow\; v = 0 \)
    • Rank. The number of independent columns, so the dimension of the column space. It can never exceed \( \min(m, n) \).
    The sentence to remember \( \operatorname{rank}(X) = d \;\Longleftrightarrow\; \text{columns independent} \;\Longleftrightarrow\; X^\top X \text{ invertible} \)

    Inverse, Determinant, and When Things Break

    • Inverse. The matrix that, when multiplied by \(A\), is the identity \(I\), the matrix version of 1. It solves a linear system. \( A A^{-1} = A^{-1} A = I, \qquad Ax = b \;\Rightarrow\; x = A^{-1} b \)
    • Determinant. The factor by which \(A\) scales volume. Zero means the map flattens space, and nothing can unflatten it. \( \det(A) = 0 \;\Longleftrightarrow\; A \text{ has no inverse} \)
    • When an inverse exists. Square, and full rank: no column is a combination of the others.
    • Why you will care. The normal equations need this inverse. Duplicate a feature and it stops existing. \( \theta = (X^\top X)^{-1} X^\top y \)
    In practice Use np.linalg.solve(A, b), instead of np.linalg.inv(A) @ b. Never invert a matrix! (as a rule of thumb)

    To see a zero determinant, press "flatten" on "A matrix is a machine that moves space": the plane collapses to a line, and nothing can unflatten it.

    When the Inverse Quietly Lies

    Real measurements, 342 penguins, four features plus an intercept column. Now add one more column: bill length again, in centimetres.

    X = np.column_stack([np.ones(342), bill_len, bill_dep,
                         flipper, mass])
    
    np.linalg.matrix_rank(X)      # 5 of 5 columns
    np.linalg.cond(X.T @ X)       # 1.8e+10
    
    # the same feature, in different units
    X2 = np.column_stack([X, bill_len / 10])
    
    np.linalg.matrix_rank(X2)     # 5 of 6 columns
    np.linalg.cond(X2.T @ X2)     # 7.1e+21
    
    # and this does not raise
    theta = np.linalg.inv(X2.T @ X2) @ X2.T @ y
    np.linalg.det(X2.T @ X2)      # -7.9e+09, not 0
    The lesson Singular matrices rarely announce themselves. Check matrix_rank and cond, and prefer lstsq.

    Singular: no inverse. Condition number: how close to singular (1 is perfect; past 1e+16, numerically singular). lstsq: least squares without an inverse.

    Try it: the inverse that lies342 penguins, and the same column twice

    
    X = np.column_stack([np.ones(342), bill_len, bill_dep, flipper, mass])
    X2 = np.column_stack([X, bill_len / 10])       # bill length again, in cm
    
    print(np.linalg.matrix_rank(X), np.linalg.matrix_rank(X2))
    print(f'{np.linalg.cond(X.T @ X):.1e}  {np.linalg.cond(X2.T @ X2):.1e}')
    print(f'det = {np.linalg.det(X2.T @ X2):.1e}')  # not zero, and not trustworthy
    
    theta = np.linalg.inv(X2.T @ X2) @ X2.T @ y     # no error is raised
    print(theta.round(2))
        

    Rank 5, then 5 of 6. The second condition number is astronomical and the determinant, which should be zero, is a large number whose exact value depends on your machine: on a laptop 7.1e+21 and −7.9e+09, in this browser something else. That instability is the lesson. The inverse runs anyway and returns nonsense; replace the last line with np.linalg.lstsq(X2, y, rcond=None) and compare.

    Part 4 of 6

    Change

    How do you measure change, from a formula or from data?

    A derivative is what a curve looks like up close

    Beirut's seasonal temperature: eleven years of daily means, averaged by day of the year. Real, and curved.

    Zoom in. The curve straightens until it is its own tangent line. The slope of that line is the derivative: how fast the curve is changing there.

    A rate and its totalBeirut's rain, one year at a time

    The total is the area under the rate: an integral. The slope of the total is the rate: a derivative. Each undoes the other, and that is the fundamental theorem of calculus. In NumPy they are np.cumsum and np.diff.

    Try it: a derivative and an integral, in two wordsBeirut, September 2019 to August 2020

    
    import json, numpy as np
    
    d = json.load(open('beirut_weather.json'))['daily']
    dates = np.array(d['time'], dtype='datetime64[D]')
    rain = np.array(d['precipitation_sum'])              # mm per day: a rate
    
    year = (dates >= np.datetime64('2019-09-01')) & (dates < np.datetime64('2020-09-01'))
    total = np.cumsum(rain[year])                        # the integral: rain so far
    print(round(total[-1]))                              # the whole year, in mm
    print(np.allclose(np.diff(total), rain[year][1:]))   # its derivative is the rate
        

    You should see 932 and True. Change the two dates on the year line to see another rain year; then load d["temperature_2m_max"] the same way and find, with np.diff, the day with the biggest jump.

    The Calculus You Actually Need

    • Derivative. For \( J: \mathbb{R} \to \mathbb{R} \), the slope: how much the output moves when the input nudges.
    • Gradient. For \( J: \mathbb{R}^d \to \mathbb{R} \), one partial derivative per parameter, collected into a vector that points uphill. \( \nabla_\theta J = \left[ \frac{\partial J}{\partial \theta_1}, \dots, \frac{\partial J}{\partial \theta_d} \right]^\top \in \mathbb{R}^d \)

    drag to see the slope

    Contours of the parabolic least-squares cost. drag to see the steepest \( \nabla J \) (ascent) and \( -\nabla J \) (descent)

    Common expressions \( \nabla_\theta (\theta^\top x) = x \),   \( \nabla_\theta \lVert \theta \rVert^2 = 2\theta \), and the chain rule.

    The One Gradient You Will Use All Semester

    Least squares in matrix form, differentiated once, slowly. Here \( X \in \mathbb{R}^{m \times d} \), \( y \in \mathbb{R}^{m} \), \( \theta \in \mathbb{R}^{d} \), and \( J(\theta) \) is a single number.

    1Expand the squared norm.
    \( J(\theta) = \lVert X\theta - y \rVert_2^2 = \theta^\top X^\top X \theta - 2 y^\top X \theta + y^\top y \)
    2Apply two rules. \( A = X^\top X \) is symmetric by construction.
    \( \nabla_\theta (b^\top \theta) = b, \qquad \nabla_\theta (\theta^\top A \theta) = 2A\theta \)
    3Assemble, and factor.
    \( \nabla_\theta J = 2X^\top X \theta - 2X^\top y = 2X^\top (X\theta - y) \;\in\; \mathbb{R}^{d} \)
    4Set it to zero. These are the normal equations, and the next lecture is mostly about this line.
    \( X^\top X \theta = X^\top y \)

    Read it as a sentence: \( X\theta - y \) is the residual, and \( X^\top \) asks each feature how much of that residual it can still explain.

    Least Squares Is a Projection

    drag \(y\). The plane of the page stands for \( \mathbb{R}^m \), one axis per data point.

    • Onto one direction. With \( a, y \in \mathbb{R}^m \), the part of \(y\) that \(a\) can explain is \( p = \dfrac{a^\top y}{a^\top a}\, a \)
    • Onto every direction at once. The columns of \( X \in \mathbb{R}^{m \times d} \) span a \(d\)-dimensional slab inside \( \mathbb{R}^m \). Every prediction \( X\theta \) lives in it, and no choice of \( \theta \) escapes. The closest reachable point to \(y\) is its shadow on that slab. \( \hat{y} = X\underbrace{(X^\top X)^{-1} X^\top y}_{\theta} \)
    • Why the residual is perpendicular. If it leaned at all, some column could still explain part of it and you could do better. So \( X^\top (y - X\theta) = 0 \;\Longleftrightarrow\; X^\top X \theta = X^\top y \)

    Written with an inverse; computed with np.linalg.lstsq.

    The chain rule, as a pictureHow every neural network computes its gradient

    Try it: backpropagation in twenty linesA number that remembers how it was made

    
    class Value:
        def __init__(self, v, parents=(), local=()):
            self.v, self.grad = v, 0.0
            self.parents, self.local = parents, local   # d(self)/d(parent)
    
        def __add__(self, o): return Value(self.v + o.v, (self, o), (1, 1))
        def __sub__(self, o): return Value(self.v - o.v, (self, o), (1, -1))
        def __mul__(self, o): return Value(self.v * o.v, (self, o), (o.v, self.v))
    
        def backward(self, g=1.0):
            # chain rule: multiply along each path back, add over paths
            self.grad += g
            for p, d in zip(self.parents, self.local):
                p.backward(g * d)
            
    
    x, y = Value(2.0), Value(5.0)
    w, b = Value(1.0), Value(0.5)
    e = w * x + b - y
    L = e * e
    L.backward()
    print('L =', L.v, ' dL/dw =', w.grad, ' dL/db =', b.grad)
    print('by hand: 2e*x =', 2 * e.v * x.v, ' 2e =', 2 * e.v)
    h = 1e-6                                     # and by nudging w a little
    print('nudge:', ((((1 + h) * 2 + 0.5 - 5) ** 2) - L.v) / h)
            

    Run the right-hand block: it runs the class first. Both ways give −10 and −5, the red numbers on the graph. Add a __pow__, or a second example, and the same backward still works: this is what PyTorch does, at scale.

    Part 5 of 6

    Habits of code

    What does the computer actually do with your arrays?

    A variable is a label, not a box

    Broadcasting, and the Bug It Causes

    The rule Line the shapes up from the right. Two dimensions are compatible when they are equal, or one of them is 1, and a 1 is stretched to match.
    a.shape b.shape a + b
    (342, 4)(4,) (342, 4)
    (342, 4)(342, 1) (342, 4)
    (342, 4)(342,) error
    (342,)(342, 1) (342, 342)
    Standardizing, correctly
    mu = X.mean(axis=0)     # (4,)
    sd = X.std(axis=0)      # (4,)
    Z  = (X - mu) / sd      # (342, 4)
    The last row of that table Predictions of shape (m,) minus targets of shape (m, 1) give you an \(m \times m\) matrix, no error, and a loss that is quietly meaningless. Print shapes when a number surprises you.

    Broadcasting, drawn

    Drawn with 3 rows; in the penguin data the 3 would be 342 and nothing else changes. Solid cells are the data you have; faint ones are the copies NumPy pretends to make.

    Avoid Loops if You Can! Vectorize.

    One million-element dot product
    Python loop 138.5 ms
    a @ b 0.76 ms
    speedup 182 times

    Measured on an Apple M-series laptop, NumPy 1.26, same answer to nine decimal places. A 500 by 500 matrix product takes 15.5 ms, about 16 GFLOP/s.

    Where the time goes The loop spends it on Python objects, one boxed float at a time. NumPy hands the whole array to compiled code that uses your CPU's vector instructions.
    The habit If you are writing for i in range(len(x)) over data, there is almost always an array expression that replaces it, and it is both faster and closer to the formula on the slide.

    Loops are not the enemy: a loop over time, like the population loop at the start, is how a model steps forward. The loop to avoid is the one over the rows of your data.

    Try it: the raceTimed on your own machine, right now

    
    import numpy as np, time
    
    n = 1_000_000
    a, b = np.random.rand(n), np.random.rand(n)
    
    t = time.perf_counter()
    s = 0.0
    for i in range(n):
        s += a[i] * b[i]
    loop = time.perf_counter() - t
    
    t = time.perf_counter()
    s2 = a @ b
    vec = time.perf_counter() - t
    print(f'loop {loop * 1000:.0f} ms, a @ b {vec * 1000:.2f} ms: {loop / vec:.0f} times faster')
        

    Python here runs inside your browser, so the loop is slower than on a laptop; the gap is the lesson. Run it twice: the second time is faster, because the first run warms things up.

    Part 6 of 6

    Data

    What do you look at before you model anything?

    Reading and Exploring Data

    import pandas as pd
    
    df = pd.read_csv("penguins.csv")
    print(df.shape)
    
    print(df.head(3))
    print(df.describe())
    print(df.isnull().sum())
    
    # the four numeric columns, as one matrix
    cols = ["bill_length_mm", "bill_depth_mm",
            "flipper_length_mm", "body_mass_g"]
    X = df[cols].dropna().values
    print(X.shape, X.dtype)

    Always describe() and check for missing values before modelling anything. Two of these 344 penguins were never measured, and dropna() is a decision, not a formality.

    Try it: read the penguinsThe first four lines of every analysis

    
    import pandas as pd
    
    df = pd.read_csv('penguins.csv')
    print(df.shape)
    print(df.isnull().sum())
    print(df.describe().round(1))
        

    Two birds have no measurements and eleven have no sex recorded. dropna() is a decision, not a formality: which rows would it remove here?

    The Design Matrix

    Every dataset in this course arrives in this one shape: \( m \) examples down, \( d \) features across, and the thing you want to predict pulled out into its own column.

    A row One penguin. \( x_i^\top \in \mathbb{R}^{1 \times d} \), a point in feature space.
    A column One measurement across every penguin. \( X_{:,j} \in \mathbb{R}^{m} \), the vector a single weight \( \theta_j \) multiplies.

    Statistics Is Linear Algebra

    Least squares fit, in grams

    \( \text{mass} \approx -5781 + 49.7 \times \text{flipper} \)
    \( R^2 = 0.76 \)
    First, put the columns on one scale Subtract each column's mean and divide by its standard deviation, giving \( Z \in \mathbb{R}^{342 \times 4} \). Millimetres and grams are now comparable.
    Then one product does the rest
    \( C = \dfrac{1}{m-1} Z^\top Z \;\in\; \mathbb{R}^{4 \times 4} \)
    Every pairwise correlation, from one matrix multiplication. Entry \( C_{jk} \) is column \(j\) dotted with column \(k\).

    Palmer Station penguins (Gorman et al.), CC0, vendored with the deck.

    Look at It Before You Model It

    Same statistics These four datasets share their means, variances, and correlation to two decimal places.
    Different worlds A line fitted to all four is identical, and wrong for three of them. Plot your data.

    Anscombe's quartet, 1973

    Two Measurements, 342 Birds

    • The number. Bill length against bill depth, every bird in the file, and the line least squares puts through them. \( r = -0.235 \)
    • The claim it invites. Longer bills are shallower.
    • Before you believe it. What would you want to see that this plot is not showing you?

    The Same Data, Two Answers

    all penguins by species
    Pooled Bill length against bill depth over all 342 birds: \( r = -0.235 \). Longer bills look shallower.
    Within each species Adelie \( r = +0.39 \), Chinstrap \( +0.65 \), Gentoo \( +0.64 \). Every group says the opposite of the pool.
    Simpson's paradox A missing variable, species here, can reverse the sign of a relationship. No amount of linear algebra warns you. Only the plot does.

    The Lab: Five Notebooks

    How to use them Run every cell. Change a number and predict what happens before you press shift-enter. That prediction is the learning.

    Summary

    Vectors
    \( x^\top y = \sum_i x_i y_i \)

    one example, one row

    Matrices
    \( X \in \mathbb{R}^{m \times d} \)

    the dataset, and a transformation

    Gradients
    \( \nabla_\theta J \)

    the direction learning walks against

    And always

    look at the data before you model it

    Next: supervised learning and linear regression, where all of this becomes one line of code.

    EnglishMathematicsPython
    how much two vectors agree\(x^\top y\)x @ y
    a matrix moves a vector\(Ax\)A @ x
    the direction uphill\(\nabla_\theta J\)2 * X.T @ (X @ theta - y)
    the running total, and back\(\int\), \(\frac{d}{dt}\)np.cumsum, np.diff
    one step into the future\(P_{t+1} = (1 + r)P_t\)P = P * (1 + r)