I.8 · Mathematical Language
A sum is a loop.
Machine learning is mostly sums over examples, over features and over layers. Learn to read sigma notation as a loop with an accumulator, to count its terms without an off-by-one, and to rewrite a sum without changing its value.
A sum is a loop
Try these
The terms ai are
from −5 to 20, whole numbers only
from −5 to 20, whole numbers only
Step 5 of 5: i = 5, a_5 = 5, so acc = 10 + 5 = 15.
| i | ai | acc after |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 3 | 6 |
| 4 | 4 | 10 |
| 5 | 5 | 15 |
- number of terms (0 when n < m)
- 5
- the sum, by the loop
- 15
- the closed form, evaluated
- 15
= exact (every term and every sum is an exact fraction, never a rounded double)
The Python being run
acc = 0
for i in range(1, 5 + 1):
acc += iThe loop gives 15 and the closed form gives 15: they agree exactly.
Try it before reading on. Press Play and watch the running total grow one term at a time; press n < m: empty and see that a sum with no terms is not an error; press 1/i through 0 and see what happens when one term does not exist. Under the picture, the verdict compares the loop with a closed-form formula whenever one applies. By the end of the chapter you will read a sum as a loop, count its terms correctly, and know which rewrites keep its value.
Intuition: a sum is a loop
Every sum in a machine-learning paper is a loop in disguise. The average loss over a batch is a loop over examples. The score of a linear model is a loop over features. A dot product is a loop over coordinates. Writing the loop in mathematical notation is shorter, and the notation also tells you three things you must not get wrong: where the loop starts, where it stops, and what is added each time.
The picture is an accumulator. Start with a total of . For each index in turn, compute a term and add it to the total. When the indices run out, the total is the answer. Nothing else is going on: the sum symbol is the word for, the bounds are the range, and the body is what the loop adds.
Two features of that picture need care, because they are the source of nearly every error with sums:
- Both ends are included. In mathematics the sum from to includes the term at . Python’s
rangedoes not:range(m, n)stops before . Counting the terms is a fencepost problem: from to there are nine terms, not eight. - A sum can have no terms. If the stop is below the start, the loop does nothing and the total stays at its starting value . That is a number, not a failure, and it is the right answer to a question such as “the average of nothing is a sum over no examples”.
The index itself is a dummy: renaming as changes nothing, exactly as renaming a loop variable changes nothing in code. What matters is the range and the body.
Definition
Sigma notation. For integers and and a rule that gives a number for each integer in the range,
The integer is the lower bound, the upper bound, the index and the term (or body). Both bounds are included.
The number of terms. If , the sum has terms. The “plus one” is the fencepost: a fence from post to post has nine posts and eight gaps.
The empty sum. If there are no indices, and by convention the sum equals , the starting value of the accumulator. This is the only value that keeps the rules below true for every range, and it is what the loop returns.
A term that does not exist. If the rule is not defined at some index in the range, the sum is undefined; it is not and the offending term is not skipped. For example is undefined, because does not exist.
Closed forms. Some sums have a formula that gives the result without adding the terms. For :
A constant added times gives . A geometric sum has a term that is a constant ratio to the power of the index. If ,
and if every term is , so the sum is . The geometric formula comes from a telescoping trick: multiplying the sum by makes every term but the first and the last cancel in pairs.
A convention to notice. In a geometric sum the term at is , and we read it as for every , including (it is the empty product, and it is what Python’s Fraction(0) ** 0 gives). Chapter I.1 leaves undefined for ordinary powers of numbers; the two do not conflict, because here the convention belongs to the notation of a geometric sequence and is stated, not smuggled in. A negative power of stays undefined everywhere: if and a term needs with , the sum is undefined. An empty sum is different again: it has no terms, so it is even when the bounds are ones where would not exist.
Rules for rewriting a sum. For the same bounds on both sums and constants :
In the shift, , so : the bounds go up by , and the term must be rewritten with wherever it had .
Double sums. A sum can have a sum as its body. In the inner sum is added completely for each , so the cells of a grid are added row by row. For a finite grid the total does not depend on the order, so adding column by column gives the same number. If the inner bound depends on the outer index, as in , the cells form a triangle, and swapping the order means rewriting the bounds so that the same cells are visited.
Python is zero-based. A list a stores its first entry at position . If the mathematics indexes from , then is a[i - 1]. Mixing the two conventions is the most common way to get a sum quietly wrong.
Mechanics: how to work with a sum
Off by one: five loops, one sum
The four numbers , , the start of the range and the stop of the range are easy to mix up. The stage below runs five loops for the same sum, shows the Python of each, and reports which ones return the true value. For the sum to be informative the terms are chosen so that a mistake changes the result.
Off by one: five loops, one sum
Try these
The terms ai are
from 1 to 12, whole numbers only
from 1 to 12, whole numbers only
The list of terms, zero-based: a[0] holds a1
a = [1, 3, 5, 7, 9] # a[0] is a_1- the sum Σ from i = m to n
- 25
- of the five loops give this total
- 2
= exact
range(m, n + 1) Correct
acc = 0 for i in range(1, 5 + 1): acc += 2 * i - 1- i takes the values
- 1, 2, 3, 4, 5
- terms added
- 5 terms: a_1 … a_5
- result
- total 25: correct
range stops BEFORE its second argument, so range(m, n + 1) visits i = m, …, n. This is the sum Σ from i = m to n.
range(m, n): stops one term short Wrong total
acc = 0 for i in range(1, 5): acc += 2 * i - 1- i takes the values
- 1, 2, 3, 4
- terms added
- 4 terms: a_1 … a_4
- result
- total 16, but the sum is 25 (off by −9)
range(m, n) stops before n, so the last term a_n is never added: the loop computes the sum up to n − 1.
range(m − 1, n): the whole window slid down Wrong total
acc = 0 for i in range(1 - 1, 5): acc += 2 * i - 1- i takes the values
- 0, 1, 2, 3, 4
- terms added
- 5 terms: a_0 … a_4
- result
- total 15, but the sum is 25 (off by −10)
range(m - 1, n) starts one too early and stops one too early: it adds a_(m−1), …, a_(n−1), so the whole window slid down by one.
a[i] with a 1-based i: reads the next entry Error
acc = 0 for i in range(1, 5 + 1): acc += a[i]- i takes the values
- 1, 2, 3, 4, 5
- terms added
- 4 terms: a_2 … a_5
- result
- IndexError: a[5] does not exist: the list a = [a_1, …, a_5] has indices 0 to 4.
The list is zero-based: a[i] holds a_(i+1), not a_i. Using the mathematical index i directly reads the NEXT term each time, and the last access runs off the end of the list.
a[i − 1]: the right entry of a zero-based list Correct
acc = 0 for i in range(1, 5 + 1): acc += a[i - 1]- i takes the values
- 1, 2, 3, 4, 5
- terms added
- 5 terms: a_1 … a_5
- result
- total 25: correct
Keep the mathematical index i in the loop and subtract 1 only when you index the zero-based list: a[i - 1] is a_i. This computes the correct sum.
range(m, n + 1) and a[i − 1] give the sum 25. 2 loops return a wrong total without any warning. 1 loop stops with an error.
Read the cards in turn for the odd numbers from to . The loop that stops one early misses the last term. The loop whose window slides down by one adds a term before the start and drops the last one. The unshifted list loop reads one position too far and Python raises an error, and a version with a negative index would not raise at all but would silently read the end of the list. Then change the terms to the whole numbers: the extra term at is , so the slid window and the loop that stops early return the same total, and the total alone cannot tell you which slip you made. A single test case is never proof that the bounds are right.
Double sums: rows, columns and triangles
A sum inside a sum
Try these
from 1 to 5, whole numbers only
from 1 to 5, whole numbers only
| 1 | 2 | 3 | 4 | row total | |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 3 | 4 | 10 |
| 2 | 2 | 4 | 6 | 8 | 20 |
| 3 | 3 | 6 | 9 | 12 | 30 |
| column total | 6 | 12 | 18 | 24 | 60 |
A grid of 3 rows and 4 columns. Each entry is i times j. Row totals are shown at the right and column totals underneath.
Cell 12 of 12: row 3, column 4, a_34 = 12, so acc = 48 + 12 = 60.
- cells in the sum, n·k
- 12
- the double sum, in the chosen order
- 60
- the double sum, in the other order
- 60
= exact
The Python being run
acc = 0
for i in range(1, 3 + 1):
for j in range(1, 4 + 1):
acc += i * jRow by row and column by column visit the same 12 cells, so both orders give 60. Adding finitely many numbers in a different order does not change the total.
On a rectangle the order cannot matter: both orders add the same cells, and the stage shows the two totals agreeing. When the entry factors as a function of times a function of , the double sum is a product of two single sums, which is why the first preset needs only a few additions. On a triangle the swap needs care: the inner bounds must be rewritten, and the verdict reports what the loop would return if its bounds were not rewritten: it visits the other triangle. When the entries are symmetric, such as , the other triangle happens to have the same total, so the slip goes unnoticed; a data table with unequal entries shows it.
Rewriting a sum: three rules
Rewriting a sum: three rules
Try these
from −5 to 20, whole numbers only
from −5 to 20, whole numbers only
from −5 to 5, whole numbers only
from −5 to 5, whole numbers only
Linearity
The sum of c·i + i² can be split: pull the constant c out front and add the two sums separately.
Both sides are 50: one loop that adds c·i + i² term by term, and two separate loops combined at the end, give the same exact number.
Shifting the index
Renaming i as j − k moves both bounds by k. The term must change too.
Substituting j = i + 2 changes the bounds to 3 and 6 AND turns the term i² into (j − 2)², and the sum stays 30. Moving only the bounds, and keeping j², gives 86, which is off by 56: a different sum.
Gauss's pairing of m, …, n
First with last, second with second last, and so on: every pair has the same sum.
- 1 + 4 = 5
- 2 + 3 = 5
2 pairs, every one adding to 5. Total: 10.
- left side of linearity
- 50
- right side of linearity
- 50
- shifted sum, term changed
- 30
- shifted sum, term forgotten
- 86
= exact (every sum is added exactly, never as a rounded double)
Three observations, each visible in the panels. Linearity lets you move a constant out of a sum and split a sum of two terms into two sums; both sides always agree, including for negative constants and for ranges that contain negative indices. Shifting the index is correct only when the term changes with it: moving the bounds and leaving the body alone computes a different sum. Look for the preset where the wrong shift happens to give the right number by coincidence, and notice that this is not a reason to trust it. Finally, Gauss’s pairing explains the closed form for consecutive whole numbers: the first and last add to the same total as the second and second-last, and so on. When the number of terms is odd, one middle term has no partner.
Worked examples
Five computations, each shown step by step with every number taken from the same model as the stages. Step forward and back with the buttons, or press Play; going back selects an earlier step, and nothing is undone.
Worked examples, step by step
Choose an example
How many terms does this sum have? Both bounds are included: the terms are a_3, a_4, …, a_11.
Executable reference code
Run the Python examples below to follow the calculations step by step. The comments record what each line returns.
First the loop itself, with exact fractions so that no rounding enters. It shows the accumulator, the empty sum, the closed forms checked against the loop, the geometric sum including the case , and the off-by-one variants on a zero-based list.
from fractions import Fraction
# The sum of a_i for i = m, ..., n (both ends INCLUDED) is a loop with an accumulator.
# Python's range(start, stop) stops BEFORE stop, so the loop that visits i = m, ..., n is range(m, n + 1).
def summation(term, m, n):
acc = Fraction(0) # the empty sum: the value before any term has been added
for i in range(m, n + 1):
acc += term(i)
return acc
total = summation(lambda i: Fraction(i), 1, 5) # 1 + 2 + 3 + 4 + 5 = 15
squares = summation(lambda i: Fraction(i) ** 2, 1, 5) # 1 + 4 + 9 + 16 + 25 = 55
empty = summation(lambda i: Fraction(i), 3, 2) # n < m: range(3, 3) has no values, so the sum is 0
single = summation(lambda i: Fraction(i), 4, 4) # one term
# Closed forms are claims about EVERY n; compare them with the loop for a few bounds.
n = 10
gauss_ok = summation(lambda i: Fraction(i), 1, n) == Fraction(n * (n + 1), 2) # n(n+1)/2
squares_ok = summation(lambda i: Fraction(i) ** 2, 1, n) == Fraction(n * (n + 1) * (2 * n + 1), 6)
odd_ok = summation(lambda i: Fraction(2 * i - 1), 1, n) == n**2 # the first n odd numbers add up to n squared
# Geometric: (1 - r) * (r**m + ... + r**n) telescopes to r**m - r**(n + 1), so for r != 1 the sum is (r**m - r**(n+1)) / (1 - r).
# Convention (stated in I.8; I.1 leaves 0**0 undefined for ordinary powers): the term r**0 of a geometric sum is 1 for every r, even r = 0,
# which is what Fraction(0) ** 0 gives. A negative power of 0 (r = 0, i < 0) is an error; an empty range(...) never evaluates it.
r = Fraction(1, 2)
geometric = summation(lambda i: r**i, 0, 4) # 1 + 1/2 + 1/4 + 1/8 + 1/16 = 31/16
geometric_closed = (r**0 - r**5) / (1 - r)
geometric_ok = geometric == geometric_closed
all_ones = summation(lambda i: Fraction(1) ** i, 0, 4) # r = 1 needs its own formula: n - m + 1 = 5, not 0/0
# Off-by-one: the same sum written four ways. Only the first one is the mathematical sum from 1 to 5.
a = [Fraction(2 * i - 1) for i in range(1, 6)] # the list holds a_1, ..., a_5, so a[i - 1] is a_i (zero-based list!)
correct = sum(a[i - 1] for i in range(1, 5 + 1)) # 1 + 3 + 5 + 7 + 9 = 25
stops_early = sum(a[i - 1] for i in range(1, 5)) # range(1, 5) misses i = 5: 16, not 25
try:
sum(a[i] for i in range(1, 5 + 1)) # a[5] does not exist: the list has indices 0 .. 4
index_error = False
except IndexError:
index_error = True
slid_window = sum(Fraction(2 * i - 1) for i in range(0, 5)) # reads a_0 ... a_4 = -1 + 1 + 3 + 5 + 7 = 15, not 25
# A negative index does not fail: a[-1] is the LAST entry. a_0 "exists" in code even though the list starts at a_1.
silent_wrap = a[0 - 1] == a[-1] # True
print(total, squares, empty, single, gauss_ok, squares_ok, odd_ok, geometric, geometric_ok, all_ones, correct, stops_early, index_error, slid_window, silent_wrap)Then double sums: rows first and columns first, the triangle with the bounds rewritten, and the same triangle with the bounds kept by mistake.
from itertools import product
# A double sum is a loop inside a loop. Rows i = 1..n, columns j = 1..k, entries a[i][j] (here written with 1-based indices).
n, k = 3, 4
def entry(i, j):
return i * j
# Rectangular: the inner bound does not depend on i. Every cell of the grid is visited exactly once.
rows_first = 0
for i in range(1, n + 1):
for j in range(1, k + 1):
rows_first += entry(i, j)
columns_first = 0
for j in range(1, k + 1):
for i in range(1, n + 1):
columns_first += entry(i, j)
same_total = rows_first == columns_first == 60 # (1+2+3)(1+2+3+4): the sum factors because entry(i, j) = i * j
cells = len(list(product(range(1, n + 1), range(1, k + 1)))) # n * k = 12 cells
# Triangular: the inner bound is the OUTER index, so the cells with j <= i form a triangle with n(n+1)/2 cells.
m = 4
by_rows = 0
for i in range(1, m + 1):
for j in range(1, i + 1):
by_rows += entry(i, j)
# Swapping the order must swap the bounds too: j runs 1..m and then i must run from j up to m (the cells with i >= j).
by_columns = 0
for j in range(1, m + 1):
for i in range(j, m + 1):
by_columns += entry(i, j)
triangle_cells = sum(1 for i in range(1, m + 1) for j in range(1, i + 1)) # 1 + 2 + 3 + 4 = 10
triangle_ok = by_rows == by_columns == 65 and triangle_cells == m * (m + 1) // 2
# The classic slip: keeping the bounds 1..i for j and 1..m for i after swapping the loops visits a DIFFERENT set of cells.
wrong = 0
for j in range(1, m + 1):
for i in range(1, j + 1): # now i <= j: the other triangle (the upper one)
wrong += entry(i, j)
wrong_same_here = wrong == by_rows # True ONLY because entry(i, j) = i * j is symmetric; not for a general table
table = [[2, -1, 0], [1, 4, -2], [0, 5, 1]]
lower = sum(table[i][j] for i in range(3) for j in range(i + 1)) # cells with j <= i: 2 + 1 + 4 + 0 + 5 + 1 = 13
upper_wrong = sum(table[i][j] for j in range(3) for i in range(j + 1)) # cells with i <= j instead: 2 + (-1) + 4 + 0 + (-2) + 1 = 4
wrong_differs = lower != upper_wrong
print(rows_first, columns_first, same_total, cells, by_rows, by_columns, triangle_ok, wrong_same_here, lower, upper_wrong, wrong_differs)Finally the form sums take on data. NumPy is zero-based too; a dot product and a mean are sums; and the file shows that adding floating-point numbers is not exact, that the order of additions matters, and what a compensated sum does about it.
import math
import numpy as np
# Sigma notation as arrays. Index convention: NumPy is zero-based, the mathematics here starts at i = 1.
n = 10
i = np.arange(1, n + 1) # the indices 1, 2, ..., n (np.arange stops BEFORE its second argument, like range)
total_int = int(i.sum()) # 55 = n(n+1)/2, exact in integer arithmetic
squares = int((i**2).sum()) # 385
# The weighted sum, the dot product and the mean are all sums.
w = np.array([0.5, 0.25, 0.25])
x = np.array([4.0, 8.0, 12.0])
dot = float(w @ x) # sum_i w_i x_i = 2 + 2 + 3 = 7
mean = float(x.sum() / x.size) # (4 + 8 + 12) / 3 = 8
# Sums of doubles are NOT exact, and the order of addition matters: adding a tiny term to a huge accumulator can lose it completely.
big, tiny = 1e16, 1.0
lost = (big + tiny) - big # 0.0: 1e16 + 1 rounds back to 1e16 (the spacing of doubles near 1e16 is 2)
kept = (big - big) + tiny # 1.0: add the small term to a small accumulator instead
# Adding 0.1 ten times does not give 1.0 with a plain loop; math.fsum tracks the lost low-order bits and does.
acc = 0.0
for _ in range(10):
acc += 0.1
naive_sum = acc # 0.9999999999999999
careful_sum = math.fsum([0.1] * 10) # 1.0
naive_is_one = naive_sum == 1.0 # False
careful_is_one = careful_sum == 1.0 # True
# For many terms the plain left-to-right loop drifts; NumPy's pairwise summation drifts much less. Compare both with the correctly rounded fsum.
values = np.full(10**5, 0.1)
exact = math.fsum(values.tolist())
pairwise_error = abs(float(values.sum()) - exact)
left_to_right = 0.0
for v in values.tolist():
left_to_right += v
loop_error = abs(left_to_right - exact)
pairwise_wins = pairwise_error < loop_error # True: same numbers, same exact answer, different order of additions
print(total_int, squares, dot, mean, lost, kept, naive_sum, careful_sum, naive_is_one, careful_is_one, pairwise_error, loop_error, pairwise_wins)Explore: change the sum
These questions have definite answers; check each against the stages above.
- In the first stage, choose the whole numbers and set the bounds to and . How many terms are there? What does the closed form give, and does the loop agree?
- Choose the squares from to . The terms are symmetric around ; is the total zero? Now choose the whole numbers from to . Why is the total zero this time?
- Make the bounds equal. How many terms are there? Now set the upper bound one below the lower bound, and then three below. What do you read, and does the number of terms become negative?
- With the powers of a ratio, set the ratio to and then to , from to . Compare each total with the closed form. Then set the ratio to : why does the formula have to be replaced by a count?
- Type your own list into the first stage, for example five numbers of your choice, and read the sum from to . Then read it from to : the stage shows the loop that would raise an error. Why would the same slip with a negative index raise nothing?
- In the identities stage, set the shift to . Why does forgetting to change the term do no harm there? Find the preset where it does no harm by coincidence, and then change the bounds slightly to break it.
Common mistakes
Numerical considerations
Sums of whole numbers and fractions are exact. Sums of floating-point numbers are not: each addition rounds to the nearest double, the rounding errors accumulate, and the result depends on the order of the additions.
Two other numerical habits follow from the rest of the chapter. A closed form such as is exact in integer arithmetic but can overflow a fixed-width integer for large ; Python’s integers do not overflow, NumPy’s integer arrays do. And a geometric sum with and many terms is dominated by its first few terms: the later ones are so small that adding them to a large accumulator loses them, which is harmless there but is why the order of addition matters when the terms vary in size.
Finally, a sum of many terms with the terms of both signs can lose most of its significant digits to cancellation: two nearly equal numbers subtract to something small, and the rounding errors that were tiny compared with the numbers are no longer tiny compared with the result. Sorting by magnitude, or using a compensated sum, helps.
Connections to engineering and machine learning
- A loss is a mean over examples. The batch loss is a sum divided by the number of terms. If the indices run from to as in code, the count is , not ; the mean of a batch of nothing is undefined (a division by zero), which is a different thing from the empty sum, which is .
- A dot product is a sum of products. . A linear model, a neuron before its activation and the attention score are all sums of this kind. Rewriting them is sum algebra: linearity moves a constant weight out front, and splitting a sum splits a model across devices.
- Softmax has a sum in its denominator. The normalizing constant is a sum over all classes. The numerical care of the previous section applies directly: summing very different magnitudes is exactly where order and compensation matter.
- Sums over axes. A matrix is a double sum waiting to happen.
X.sum(axis=0)adds down the rows andX.sum(axis=1)along the columns; summing everything is the double sum, and the order cannot matter for a finite grid, apart from floating-point rounding. - Triangular sums count pairs. The number of pairs with is a triangular number, and an algorithm with a loop inside a loop whose inner bound is the outer index has a running time proportional to with the constant that the closed form gives. Comparing every example with every other example in a batch is this pattern.
- Geometric sums are discounting and learning-rate schedules. A discounted return and a moving average with decay both weight terms by powers of a ratio; the closed form with tells you how much total weight a long horizon can carry.
Exercises
Answer each question and press Check. A wrong answer tells you which mistake it looks like. After one attempt you may open a worked solution.
Summary and checkpoints
- A sum is a loop with an accumulator that starts at : for each index from the lower bound to the upper bound, both included, add the term. The index is a dummy name.
- The number of terms is . When the sum is empty and equals . If a term is undefined, the whole sum is undefined; terms are never skipped.
- Closed forms: for , for the squares, for the first odd numbers, for a constant, and for a geometric sum when (and when ).
- Valid rewrites: pull a constant out, split a sum into two sums over the same bounds, split the range, shift the index and change the term to match. Pairing the first term with the last explains the formula for consecutive whole numbers.
- A double sum adds a grid of cells; on a rectangle the order does not matter; on a triangle the inner bound must be rewritten when the order is swapped.
- In Python,
range(m, n + 1)visits and a list is zero-based, so a one-based isa[i - 1]; a negative index wraps silently. - Floating-point sums round at each step and depend on the order of the additions; keep exact values, or use a compensated or pairwise sum.
Checkpoints. You can move on when you can, without the stage:
- Write the sum of from to as a Python loop, and say how many times the body runs.
- State what a sum with the upper bound below the lower bound equals, and what a sum with a term that does not exist equals.
- Use a closed form for a sum of consecutive whole numbers, and say when the geometric formula cannot be used.
- Shift the index of a sum by and change both the bounds and the term.
- Swap the order of a double sum over a triangle and write the new bounds.