← Attention, Demystified: From Dot Products to Explaining Transformers
Week 1, Part 1: The Math You Actually Need

Softmax and Turning Scores into Weights

Apply softmax to a vector of raw scores by hand, verify the outputs sum to 1, and explain how scaling scores up or down sharpens or flattens the resulting distribution.

Softmax: Turning Scores into Weights

The problem softmax solves

In attention, you'll compute a raw compatibility score between a query and each key — a dot product. You'll end up with something like

s=[2.0,  1.0,  0.0]\mathbf{s} = [2.0,\; 1.0,\; 0.0]

meaning "token 1 looks most relevant, token 2 somewhat, token 3 least." But scores aren't directly usable as mixing weights. You want to build a weighted average of value vectors, and for that you need weights that are:

  1. non-negative (you can't have −0.4-0.4 of a token), and
  2. sum to exactly 1 (so the output stays on the same scale as the inputs, regardless of sequence length).

Softmax is the standard way to convert an arbitrary real-valued vector into exactly that: a probability distribution.

The formula

For a vector s=[s1,s2,…,sn]\mathbf{s} = [s_1, s_2, \dots, s_n]:

softmax(s)i=esi∑j=1nesj\text{softmax}(\mathbf{s})_i = \frac{e^{s_i}}{\sum_{j=1}^{n} e^{s_j}}

Read it as a two-step recipe:

  1. Exponentiate each score. This forces everything positive and amplifies differences — exe^x grows fast, so a slightly bigger score becomes a much bigger number.
  2. Normalize by the total. Dividing by the sum guarantees the outputs add to 1.

The sum-to-1 property is basically free from the construction: if Z=∑jesjZ = \sum_j e^{s_j}, then

∑iesiZ=1Z∑iesi=ZZ=1.\sum_i \frac{e^{s_i}}{Z} = \frac{1}{Z}\sum_i e^{s_i} = \frac{Z}{Z} = 1.

The name is "soft max" because the output is a smoothed version of "put all the weight on the largest element." Hard max would give [1,0,0][1, 0, 0]; softmax gives a differentiable approximation to it, which is what lets gradients flow during training.

Worked example, by hand

Take s=[2,1,0]\mathbf{s} = [2, 1, 0].

Step 1 — exponentiate:

e2=7.3891,e1=2.7183,e0=1.0000e^{2} = 7.3891,\qquad e^{1} = 2.7183,\qquad e^{0} = 1.0000

Step 2 — sum:

Z=7.3891+2.7183+1.0000=11.1074Z = 7.3891 + 2.7183 + 1.0000 = 11.1074

Step 3 — divide:

7.389111.1074=0.6652,2.718311.1074=0.2447,1.000011.1074=0.0900\frac{7.3891}{11.1074} = 0.6652,\qquad \frac{2.7183}{11.1074} = 0.2447,\qquad \frac{1.0000}{11.1074} = 0.0900

Verify: 0.6652+0.2447+0.0900=0.9999≈10.6652 + 0.2447 + 0.0900 = 0.9999 \approx 1 (rounding).

So a score gap of 1.0 between the top two entries translates into weights of roughly 67% and 24%. Note the nonlinearity: the scores were evenly spaced (2,1,02, 1, 0) but the weights are not (0.67,0.24,0.090.67, 0.24, 0.09) — each step down in score multiplies the weight by about 1/e≈0.371/e \approx 0.37.

Scaling the scores: sharpening and flattening

Now the key insight for understanding attention. Multiply all scores by a constant β\beta before applying softmax:

softmax(βs)i=eβsi∑jeβsj\text{softmax}(\beta \mathbf{s})_i = \frac{e^{\beta s_i}}{\sum_j e^{\beta s_j}}

β\beta is often written as 1/T1/T where TT is the temperature. Large β\beta (low temperature) = sharp. Small β\beta (high temperature) = flat.

Sharpen it: β=3\beta = 3, so s=[6,3,0]\mathbf{s} = [6, 3, 0]

e6=403.43,e3=20.09,e0=1.00,Z=424.51e^6 = 403.43,\quad e^3 = 20.09,\quad e^0 = 1.00,\qquad Z = 424.51 ⇒[0.9503,  0.0473,  0.0024]\Rightarrow [0.9503,\; 0.0473,\; 0.0024]

Almost all the weight is on the first element. The distribution is approaching hard max — nearly one-hot.

Flatten it: β=0.25\beta = 0.25, so s=[0.5,0.25,0]\mathbf{s} = [0.5, 0.25, 0]

e0.5=1.6487,e0.25=1.2840,e0=1.0000,Z=3.9327e^{0.5} = 1.6487,\quad e^{0.25} = 1.2840,\quad e^0 = 1.0000,\qquad Z = 3.9327 ⇒[0.4192,  0.3265,  0.2543]\Rightarrow [0.4192,\; 0.3265,\; 0.2543]

Now it's much closer to uniform [0.333,0.333,0.333][0.333, 0.333, 0.333]. As β→0\beta \to 0, every score becomes 0, every exponential becomes 1, and you get exactly the uniform distribution — the scores stop mattering at all.

Summary of the three cases, same underlying scores:

| β\beta | scores | weights | |---|---|---| | 0.250.25 | [0.5,0.25,0][0.5, 0.25, 0] | [0.42,0.33,0.25][0.42, 0.33, 0.25] | | 11 | [2,1,0][2, 1, 0] | [0.67,0.24,0.09][0.67, 0.24, 0.09] | | 33 | [6,3,0][6, 3, 0] | [0.95,0.05,0.002][0.95, 0.05, 0.002] |

Why this matters for transformers. Attention computes softmax ⁣(QK⊤dk)\text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right). That dk\sqrt{d_k} in the denominator is exactly a β<1\beta < 1 flattening factor. A dot product of two dkd_k-dimensional vectors with unit-variance components has variance dkd_k, so raw scores grow like dk\sqrt{d_k} as the model gets wider. Without the division, scores for dk=64d_k = 64 would be large enough to push softmax into the near-one-hot regime, where the gradient with respect to every score is nearly zero and learning stalls. Dividing by dk\sqrt{d_k} keeps the score magnitudes roughly constant as you change the head dimension. That's the entire justification — it's a temperature correction, not deep magic.

The same knob shows up again at the output of the model: when sampling the next token, "temperature 0.7" means β=1/0.7≈1.43\beta = 1/0.7 \approx 1.43, sharpening the distribution toward the likeliest tokens; temperature 1.5 flattens it and produces more surprising text.

One more property: shift invariance

Adding the same constant cc to every score changes nothing:

esi+c∑jesj+c=ecesiec∑jesj=esi∑jesj\frac{e^{s_i + c}}{\sum_j e^{s_j + c}} = \frac{e^{c} e^{s_i}}{e^{c}\sum_j e^{s_j}} = \frac{e^{s_i}}{\sum_j e^{s_j}}

Check it on our example by subtracting the max, c=−2c = -2, giving [0,−1,−2][0, -1, -2]:

e0=1,e−1=0.3679,e−2=0.1353,Z=1.5032e^0 = 1,\quad e^{-1} = 0.3679,\quad e^{-2} = 0.1353,\qquad Z = 1.5032 ⇒[0.6652,  0.2447,  0.0900]\Rightarrow [0.6652,\; 0.2447,\; 0.0900]

Identical. This is not a curiosity — it's how softmax is actually implemented. e1000e^{1000} overflows to inf in float32, so every real implementation subtracts the max first:

import numpy as np

def softmax(s, beta=1.0):
    s = beta * np.asarray(s, dtype=np.float64)
    s = s - s.max()              # shift-invariance: safe, changes nothing
    e = np.exp(s)
    return e / e.sum()

w = softmax([2, 1, 0])
print(w, w.sum())                # [0.665 0.245 0.090] 1.0
print(softmax([2, 1, 0], beta=3))    # sharp
print(softmax([2, 1, 0], beta=0.25)) # flat

Contrast this with scale invariance, which softmax does not have — that's the whole point of the previous section. Shifting is free; scaling changes everything.

Also worth knowing

  • Only differences matter. Since shifts are free, softmax depends solely on the gaps between scores. A gap of Δ\Delta between two entries means their weight ratio is exactly eΔe^{\Delta}. Gap of 1 → ratio 2.72.7; gap of 5 → ratio 148148.
  • Masking is −∞-\infty. In causal attention, positions a token isn't allowed to see get score −∞-\infty (in practice a large negative number like −109-10^9). Then e−∞=0e^{-\infty} = 0, the weight vanishes, and the remaining weights still sum to 1 automatically. No special-case code needed.
  • Outputs are never exactly 0 or 1. Every unmasked position always gets a nonzero weight. Attention is always a blend, just sometimes a very lopsided one.

Try it yourself

Take the score vector

s=[3,  1,  1,  −2]\mathbf{s} = [3,\; 1,\; 1,\; -2]
  1. Compute softmax(s)\text{softmax}(\mathbf{s}) by hand (calculator is fine) and confirm the four weights sum to 1. Notice what happens to the two tied scores.
  2. Without recomputing anything from scratch, predict the weight ratio between the first entry and the last. (Hint: use the gap.) Then check it against your numbers.
  3. Recompute with β=2\beta = 2 and β=0.5\beta = 0.5. Rank the three distributions from flattest to sharpest and write a one-sentence explanation of why, in the form you'd say it out loud to a colleague.
  4. Confirm all of it with the softmax function above, then mask out the second element (set its score to −109-10^9) and verify the remaining three weights still sum to 1.

If you can do step 3 out loud without looking at notes, you have the piece of softmax that attention actually depends on.

Confused by anything? Highlight it and hit “Go deeper”.

Watch

Test yourself