KBKnowledge Base
Linear Algebra for ML · 1.5

Matrix Operations

How matrices combine — and how neural networks actually compute.

On this page
In plain English — beginner to advanced

Beginner: matrix addition is straightforward — add matching cells, and it only works when both matrices are exactly the same shape. Matrix multiplication is the one to understand deeply: to get one output number, take a full row of the first matrix, a full column of the second, and compute their dot product.

Intermediate: the shape rule is worth memorizing precisely, because it's the single most common source of bugs when building neural networks by hand: multiplying an (m×n) matrix by an (n×p) matrix requires the inner dimensions to match (both n), and the result is an (m×p) matrix — the outer dimensions "survive." If the inner dimensions don't match, the multiplication is simply undefined, not approximately correct.

Advanced: matrix multiplication composes transformations. If matrix B rotates space and matrix A then stretches it, the single matrix AB does both at once, in that order, to any vector you feed it. This is exactly how a multi-layer neural network's effective transformation could in principle be written as one giant matrix — if there were no non-linear activation functions breaking up the chain (which is precisely why those non-linearities are essential: without them, an arbitrarily deep network would collapse to a single linear transformation).

Formula
Matrix multiplication (A is m×n, B is n×p):
(AB)ij=∑k=1nAikBkj(AB)_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj}

In plain words: entry (i, j) of the result is "row i of A, dotted with column j of B." Do this for every combination of row and column and you have the full product.

Theorem: matrix multiplication is associative — (AB)C = A(BC)

Write both sides in index notation and expand using the definition above. Entry (i, l) of (AB)C:

[(AB)C]il=∑k(AB)ikCkl=∑k(∑jAijBjk)Ckl[(AB)C]_{il} = \sum_k (AB)_{ik}C_{kl} = \sum_k \left(\sum_j A_{ij}B_{jk}\right) C_{kl}

Multiply through and swap the (finite) order of summation — always valid for finite sums:

=∑j∑kAijBjkCkl=∑jAij(∑kBjkCkl)=∑jAij(BC)jl=[A(BC)]il= \sum_j \sum_k A_{ij}B_{jk}C_{kl} = \sum_j A_{ij} \left(\sum_k B_{jk}C_{kl}\right) = \sum_j A_{ij}(BC)_{jl} = [A(BC)]_{il}

Since this holds for every entry (i, l), the two matrices are identical: (AB)C = A(BC).

Where this is used: associativity is why a deep network's layers can be grouped and composed in any order without changing the result, and why libraries can freely choose the cheapest evaluation order for a long chain of matrix products (exactly the contraction-ordering question raised in section 1.18's expert note).

Worked examples

Edit the numbers below and watch the result recompute instantly:

Interactive matrix × vector

Edit A or v directly — the highlighted-row/column intuition from 1.4 is exactly what's happening under the hood.

Practical example — a neural network layer, by hand

This four-line snippet is a full forward pass through one neural network layer — W @ x + b is matrix multiplication plus a bias vector, exactly as described above, and np.maximum(0, z) is the ReLU non-linearity that keeps stacked layers from collapsing into a single linear transformation.

cpp
#include <algorithm>
#include <cstdio>
#include <vector>

using Matrix = std::vector<std::vector<double>>;

std::vector<double> matVecMul(const Matrix& M, const std::vector<double>& v) {
    std::vector<double> out(M.size(), 0.0);
    for (size_t i = 0; i < M.size(); ++i)
        for (size_t j = 0; j < v.size(); ++j)
            out[i] += M[i][j] * v[j];
    return out;
}

std::vector<double> addVec(const std::vector<double>& u, const std::vector<double>& v) {
    std::vector<double> out(u.size());
    for (size_t i = 0; i < u.size(); ++i) out[i] = u[i] + v[i];
    return out;
}

std::vector<double> relu(const std::vector<double>& v) {
    std::vector<double> out(v.size());
    for (size_t i = 0; i < v.size(); ++i) out[i] = std::max(0.0, v[i]);
    return out;
}

int main() {
    Matrix W = {{0.2, -0.5}, {0.8, 0.1}};   // weights, shape (2, 2)
    std::vector<double> x = {1.0, 2.0};       // input, shape (2,)
    std::vector<double> b = {0.1, -0.2};      // bias, shape (2,)

    std::vector<double> z = addVec(matVecMul(W, x), b);  // matmul then add bias
    std::vector<double> output = relu(z);                  // ReLU activation

    std::printf("[%.4f, %.4f]\n", output[0], output[1]);
    return 0;
}
Real-world examples
  • Every neural network layer computes output = W·x + b — stack enough of these (with a non-linearity between) and you get a deep network.
  • GPUs matter specifically because of this operation — they're built with thousands of cores designed to run matrix multiplications in parallel, which is why "more GPU" almost always means "train bigger models faster."
  • 3D graphics and game engines multiply every vertex of a 3D model by a 4×4 transformation matrix (combining rotation, scaling, and translation) on every single frame.
  • Markov chains use matrix multiplication to advance probability distributions one step in time — multiplying a state vector by a transition matrix repeatedly is literally how PageRank (early Google search ranking) was computed.
Common mistakes
  • Trying to multiply two matrices whose inner dimensions don't match — always sanity-check shapes first: (m×n)·(n×p) → (m×p).
  • Assuming AB = BA — matrix multiplication is not commutative in general; order matters and changes the result (or breaks the shapes entirely).
  • Confusing elementwise multiplication (A * B in NumPy) with true matrix multiplication (A @ B) — they require different shapes and mean entirely different things.
Going deeper

Matrix multiplication is not commutative — generally AB ≠ BA. Frameworks use batched matrix multiplication (torch.bmm) to apply the same operation across many samples at once.

Naive matrix multiplication is O(n³) for two n×n matrices, but this is a genuinely open area of research — Strassen's algorithm (1969) does better than the naive approach, and in 2022 DeepMind's AlphaTensor discovered even faster multiplication algorithms for specific matrix sizes using reinforcement learning, which is a nice example of ML being used to improve the very linear algebra that powers ML.

At the master/production level: modern NVIDIA GPUs (Volta architecture onward) include Tensor Cores — hardware units that do nothing but general matrix multiply (GEMM), computing an entire small matrix-multiply-accumulate per clock cycle instead of one scalar per core. Mixed-precision training (FP16 or BF16 inputs accumulated in FP32) exists specifically to feed these units efficiently — this hardware detail, one level below any framework code, is a large part of why modern deep learning training is fast enough to be practical at all.

Newsletter

Stay in the loop

Subscribe to get new docs, diagrams, and engineering write-ups by Dharaneesh Boobalan delivered to your inbox.

  • Deep-dive write-ups on ML, inference, and systems.
  • New Draw.io diagrams & interactive canvases.
  • Agentic patterns and rocket-science notes.
  • No spam. One tasteful email when there's something new.

Crafted by Dharaneesh Boobalan

Newsletter

Get new docs, diagrams, and write-ups in your inbox.

We never share your details. Unsubscribe anytime.