Positional Encoding
Self-attention blends values with no sense of order, so each token gets a sine and cosine mark that says where it sits.
Why Does This Exist?
Self-attention is a weighted average of value vectors. Averages do not carry order. Shuffle three tokens and the same multiset of outputs returns, only reassigned to new slots. The sentence pair dog bit man versus man bit dog then scores identically, which is wrong.
Recurrence once gave order for free by reading left to right. Transformers dropped recurrence for parallel reads, and paid for speed with order blindness. Something must restore position before the first attention layer runs.
This page covers the additive fix: a per position vector added to each embedding. The rotation based successor lives in rotary position embeddings. The query key value machinery it feeds lives in self-attention.
Think of It Like This
Numbered stickers on moving boxes
A mover stacks plain brown boxes in a truck. By arrival they have slid around and no box shows where it stood. Now repeat the move with numbered stickers on every box: 0 on the first, 1 on the next, and so on. The truck still shakes, boxes still slide, but each box carries its slot number with it.
Token content is the box contents, position is the sticker number, attention mixing is the ride shaking boxes together, and adding the encoding is sticking the number on before loading. Reading sticker plus contents tells you what and where.
Where it stops: stickers never change what sits inside the box. Positional codes add no meaning to a word, they only let later layers recover which slot it came from.
How It Actually Works
Call the integer position, starting at 0. Call the model width, the embedding size. Call the dimension pair index, from to . The original construction assigns one sine and one cosine per pair:
The denominator sets the wavelength for that pair. Small means a small denominator and a fast wave across . Large means a large denominator and a slow wave. The vector is then added elementwise to the token embedding :
Attention only ever sees , which carries content plus position in one vector.
Worked example: width 4, positions 0 and 1
Take , so and . For , the exponent is , so the denominator is . For , the exponent is , so the denominator is .
At :
At , the four angles are , , , and :
So position 1 encodes as after rounding. Every entry stays inside no matter how large grows, and rows 0 and 1 differ in three slots. That is the diagram: two fast cells on the left carrying 0.84 and 0.54, two slow cells on the right carrying 0.01 and 1.00, with the row for position 0 reading above it.
Why sine and cosine
Boundedness is the first win. Sine and cosine never leave , so late positions never shout louder than early ones. Relative structure is the second. For a fixed shift , is a linear function of , which gives the model a clean path to learn distance based patterns.
Code
Build the width 4 matrix and check boundedness plus distinct rows with plain Python.
import math
def pe_row(t: int, d: int) -> list[float]: row: list[float] = [] for i in range(d // 2): denom: float = 10000 ** ((2 * i) / d) angle: float = t / denom row.append(math.sin(angle)) row.append(math.cos(angle)) return row
rows: list[list[float]] = [pe_row(t, 4) for t in range(3)]for r in rows: print([round(v, 2) for v in r])# -> [0.0, 1.0, 0.0, 1.0]# -> [0.84, 0.54, 0.01, 1.0]# -> [0.91, -0.42, 0.02, 1.0]
flat: list[float] = [v for r in rows for v in r]print(min(flat) >= -1.0 and max(flat) <= 1.0) # -> Trueprint(rows[0] != rows[1]) # -> TruePosition 1 prints , matching the hand computation. All values sit in and no two rows match.
Watch Out For
Assembling attention with no position path
Symptom: a from scratch transformer trains without errors, shapes all match, yet word order shuffles barely change predictions and swapped clauses score the same. Fix: confirm a position vector is added before the first attention layer and that it actually reaches queries and keys, as in self-attention. If you moved to rotations, confirm the rotation is applied inside attention per rotary position embeddings.
Testing past the trained length and blaming the code
Symptom: quality falls once inputs run longer than anything seen in training, even though the sine formula still returns values in . Fix: treat trained length as a limit, not a guarantee. The model never shaped its weights around those late rows. Shorten, chunk, or switch to a length extension method instead of trusting raw extrapolation.
The Quick Version
- Self-attention averages values, so order must be supplied from outside before the first layer.
- Each position gets a sine and cosine vector with one frequency per dimension pair, added to the token embedding.
- With width 4, position 0 is and position 1 is .
- All entries stay in , so late positions never dominate early ones.
- A fixed shift is a linear function of the current code, which helps distance patterns transfer.
- Rotary embeddings replace addition with rotation but solve the same order blindness.