How an LLM sees words

Follow one sentence through GPT-2 small, from tokens to the next word, with the real numbers at every step, and find out what the attention weights do and do not tell us.

Contents
  1. From Text to Integers
  2. From Integers to Vectors
  3. Comparing Two Vectors
  4. Why a Word Needs Its Neighbours
  5. Scores
  6. The Mask
  7. Softmax
  8. The Output
  9. What This Head Actually Learned
  10. One Head Is Not the Model
  11. Back to Words
  12. In Code
  13. Common Mistakes

You read The cat ran away because it was scared and know at once who was scared. A language model never sees those words. It receives a list of integers, turns them into rows of decimals, and does arithmetic on the rows until a probability comes out.

This post follows that one sentence through GPT-2 small, from the first integer to the next word. If you came from our Reel, this is the long version, and it goes further in one place: it checks what the Reel's most striking number really means. Every number about the model comes from its real weights, and the code at the end reproduces the main ones.

From Text to Integers #

A tokenizer cuts the text into pieces from a fixed vocabulary and replaces each piece with its position in that vocabulary. Our sentence becomes eight integers:

Token ID
"The" 464
" cat" 3797
" ran" 4966
" away" 1497
" because" 780
" it" 340
" was" 373
" scared" 12008

The space belongs to the word after it, so " cat" is not "cat". They are different tokens with different IDs, 3797 and 9246, and so are " Cat" (5181) and " CAT" (38348). Until training says otherwise, these are unrelated integers.

Tokens are not words either. GPT-2 uses byte pair encoding, which starts from single bytes and keeps merging the most frequent adjacent pair.[1] Its vocabulary is 256 bytes, 50,000 merges and one end-of-text marker, 50,257 tokens in all. Common words survive in one piece and rare ones break: " unbelievable" is one token, " tokenization" is two (" token", "ization"), and "Deeptext" is four ("D", "ee", "pt", "ext").

From Integers to Vectors #

An ID is an index into a matrix with one row per token, the embedding table E \in \mathbb{R}^{50257 \times 768}. If e_t is the one-hot vector for token t, the lookup is a multiplication:

e_t^\top E = E_t

The table holds 38,597,376 numbers, 31% of GPT-2's 124,439,808 parameters.

A heatmap of the whole embedding table as a tall noisy column with faint vertical stripes, and a zoom on nine rows, from 3793 to 3801, each labelled with its token: remains, India, psych, rief, cat, esc, observ, stage, Dark.
The whole table, then nine of its rows. White is positive, blue is negative. IDs follow how the vocabulary was built, so rows that sit together have nothing in common.

Nobody chose these values. The rows start random, and training nudges them together with the rest of the model so that the next token becomes more likely. No column means animal or plural. Whatever meaning the rows hold, it lives in how they relate to each other.

A lookup knows nothing about order, so GPT-2 keeps a second table P \in \mathbb{R}^{1024 \times 768} with one row per position, and adds it.[2] That table is also why the model reads at most 1,024 tokens at a time. Token i of the sentence, with ID t_i, becomes

X_i = E_{t_i} + P_i

Stack the eight rows and the sentence is a matrix X \in \mathbb{R}^{8 \times 768}.

Eight horizontal heat strips, one per token of the sentence, labelled with the token and its ID, and bracketed as a matrix X of size 8 by 768.
The sentence as the model receives it, once position has been added.

Comparing Two Vectors #

Almost everything from here on is one operation. The dot product multiplies two vectors component by component and adds the results:

a \cdot b = \sum_{i=1}^{768} a_i b_i

These are the first components of the rows for " cat" and " dog", and the first terms of their dot product:

\begin{gathered} \underbrace{\begin{bmatrix} 0.010 \\ 0.037 \\ 0.164 \\ -0.219 \\ 0.029 \\ \vdots \end{bmatrix}}_{\text{cat}} \cdot \underbrace{\begin{bmatrix} 0.094 \\ -0.077 \\ 0.035 \\ -0.035 \\ -0.089 \\ \vdots \end{bmatrix}}_{\text{dog}} \\[8pt] = 0.0009 - 0.0028 + \dots = 5.756 \end{gathered}

Each term is tiny. The largest of the 768 is 0.140, and 68% of them are positive, so the total of 5.756 is the result of many small agreements. No single component decides.

  • cat and dog
  • cat and car
01002003004005006007000246Components added so farRunning total of the dot product
The dot product as a running total. Both pairs climb steadily, and they only separate over hundreds of components.

A long vector scores high against everything, so we divide by the two lengths. What remains is the cosine of the angle between the vectors, a number between -1 and 1:

\cos\theta = \frac{a \cdot b}{\|a\|\,\|b\|} = \frac{5.756}{3.258 \times 3.214} = 0.550

Here is " cat" against other rows of the table. The last column repeats the comparison after subtracting the mean of all rows.

Word Cosine with " cat" After subtracting the mean
" cats" 0.693 0.623
" dog" 0.550 0.489
" kitten" 0.501 0.357
" rabbit" 0.436 0.313
" car" 0.381 0.329
" horse" 0.362 0.252
" truck" 0.269 0.140
" bank" 0.239 0.147
" table" 0.210 0.123
" idea" 0.201 0.092

Two things stand out. The first is that nothing is near zero. The average cosine between " cat" and all 50,257 rows is 0.205, and 98% of them fall between 0.118 and 0.303. All rows share a common direction, so in this table 0.20 means unrelated. Subtract the mean row and that average drops to 0.003, which is why the last column is the fairer ruler.

The second is " car". It scores below " dog", but 0.381 is higher than 99.93% of the vocabulary. A car is not an animal, yet both are common concrete nouns that fill similar places in sentences, and those places are what the table learns.[3] What it cannot see is spelling. " cat" and " car" differ by one letter, but the model only ever receives 3797 and 1097.

Why a Word Needs Its Neighbours #

The row for " it" is the same in every sentence GPT-2 will ever read, so on its own it cannot say what it refers to. Attention is how the other tokens get in.

The model is a stack of 12 layers, and each layer runs 12 attention heads side by side. We will follow one of them: layer 4, head 3, counting from 0. Its input has the same shape as before, X \in \mathbb{R}^{8 \times 768}, but it is no longer the embedding. The sentence has already passed through four layers, and then through layer normalization.[4]

The head multiplies X by three learned matrices, which gives every token three shorter vectors:

\underbrace{Q}_{8 \times 64} = \underbrace{X}_{8 \times 768}\,\underbrace{W_Q}_{768 \times 64} + b_Q, \qquad K = X W_K + b_K, \qquad V = X W_V + b_V

It helps to read row q_i as what token i is looking for, k_j as what token j offers, and v_j as what token j hands over if it is chosen.[5] That is a picture and not a rule: to the model they are three linear maps. In the code they come from one matrix of shape 768 × 2304 called c_attn, cut into three parts, and then into 12 heads of 64 numbers each.

Scores #

How well a query matches a key is a dot product again. For all pairs at once:

S = \frac{Q K^\top}{\sqrt{d_k}}, \qquad S_{ij} = \frac{q_i \cdot k_j}{\sqrt{d_k}}

Here d_k = 64. For the query of " it" and the key of " cat", that is 6.34 / \sqrt{ 64 } = 0.79. This is the whole grid, with queries as rows and keys as columns:

\def\arraystretch{1.15}\def\arraycolsep{0pt} {\scriptsize\begin{array}{r|cccccccc} & \text{\tiny The} & \text{\tiny cat} & \text{\tiny ran} & \text{\tiny away} & \text{\tiny because} & \text{\tiny it} & \text{\tiny was} & \text{\tiny scared} \\ \hline \text{The}\; & \colorbox{#273049}{$\hphantom{-}\textcolor{#E8EAEF}{0.4}$} & \colorbox{#5B6074}{$\hphantom{-}\textcolor{#E8EAEF}{1.6}$} & \colorbox{#4B5167}{$\hphantom{-}\textcolor{#E8EAEF}{1.3}$} & \colorbox{#4C5267}{$\hphantom{-}\textcolor{#E8EAEF}{1.3}$} & \colorbox{#434A60}{$\hphantom{-}\textcolor{#E8EAEF}{1.1}$} & \colorbox{#53586D}{$\hphantom{-}\textcolor{#E8EAEF}{1.4}$} & \colorbox{#4E5369}{$\hphantom{-}\textcolor{#E8EAEF}{1.3}$} & \colorbox{#444B61}{$\hphantom{-}\textcolor{#E8EAEF}{1.1}$} \\ \text{cat}\; & \colorbox{#1F2943}{$\hphantom{-}\textcolor{#E8EAEF}{0.3}$} & \colorbox{#233178}{$\textcolor{#E8EAEF}{-2.0}$} & \colorbox{#2D42BD}{$\textcolor{#E8EAEF}{-4.1}$} & \colorbox{#2A3B9E}{$\textcolor{#E8EAEF}{-3.2}$} & \colorbox{#2E42BE}{$\textcolor{#E8EAEF}{-4.1}$} & \colorbox{#293A9D}{$\textcolor{#E8EAEF}{-3.2}$} & \colorbox{#2B3EAC}{$\textcolor{#E8EAEF}{-3.6}$} & \colorbox{#2B3CA5}{$\textcolor{#E8EAEF}{-3.4}$} \\ \text{ran}\; & \colorbox{#253380}{$\textcolor{#E8EAEF}{-2.3}$} & \colorbox{#353C54}{$\hphantom{-}\textcolor{#E8EAEF}{0.8}$} & \colorbox{#2E42BD}{$\textcolor{#E8EAEF}{-4.1}$} & \colorbox{#263589}{$\textcolor{#E8EAEF}{-2.6}$} & \colorbox{#2B3EAB}{$\textcolor{#E8EAEF}{-3.6}$} & \colorbox{#15213E}{$\textcolor{#E8EAEF}{-0.1}$} & \colorbox{#273790}{$\textcolor{#E8EAEF}{-2.8}$} & \colorbox{#27368D}{$\textcolor{#E8EAEF}{-2.7}$} \\ \text{away}\; & \colorbox{#212E6B}{$\textcolor{#E8EAEF}{-1.6}$} & \colorbox{#222C45}{$\hphantom{-}\textcolor{#E8EAEF}{0.3}$} & \colorbox{#2C3FB1}{$\textcolor{#E8EAEF}{-3.8}$} & \colorbox{#2D42BD}{$\textcolor{#E8EAEF}{-4.1}$} & \colorbox{#2D41B9}{$\textcolor{#E8EAEF}{-4.0}$} & \colorbox{#1B2755}{$\textcolor{#E8EAEF}{-0.9}$} & \colorbox{#253380}{$\textcolor{#E8EAEF}{-2.3}$} & \colorbox{#283894}{$\textcolor{#E8EAEF}{-2.9}$} \\ \text{because}\; & \colorbox{#253484}{$\textcolor{#E8EAEF}{-2.4}$} & \colorbox{#202D6B}{$\textcolor{#E8EAEF}{-1.6}$} & \colorbox{#253380}{$\textcolor{#E8EAEF}{-2.3}$} & \colorbox{#273790}{$\textcolor{#E8EAEF}{-2.8}$} & \colorbox{#3047D3}{$\textcolor{#E8EAEF}{-4.7}$} & \colorbox{#202C67}{$\textcolor{#E8EAEF}{-1.5}$} & \colorbox{#283895}{$\textcolor{#E8EAEF}{-2.9}$} & \colorbox{#1E2B61}{$\textcolor{#E8EAEF}{-1.3}$} \\ \text{it}\; & \colorbox{#233076}{$\textcolor{#E8EAEF}{-2.0}$} & \colorbox{#373E55}{$\hphantom{-}\textcolor{#E8EAEF}{0.8}$} & \colorbox{#293999}{$\textcolor{#E8EAEF}{-3.0}$} & \colorbox{#263589}{$\textcolor{#E8EAEF}{-2.6}$} & \colorbox{#2C40B3}{$\textcolor{#E8EAEF}{-3.8}$} & \colorbox{#2C3FB0}{$\textcolor{#E8EAEF}{-3.7}$} & \colorbox{#2D41B9}{$\textcolor{#E8EAEF}{-4.0}$} & \colorbox{#253484}{$\textcolor{#E8EAEF}{-2.4}$} \\ \text{was}\; & \colorbox{#263588}{$\textcolor{#E8EAEF}{-2.5}$} & \colorbox{#1B2754}{$\textcolor{#E8EAEF}{-0.9}$} & \colorbox{#2F45C8}{$\textcolor{#E8EAEF}{-4.4}$} & \colorbox{#2D40B6}{$\textcolor{#E8EAEF}{-3.9}$} & \colorbox{#2D41B8}{$\textcolor{#E8EAEF}{-3.9}$} & \colorbox{#253380}{$\textcolor{#E8EAEF}{-2.3}$} & \colorbox{#2E42BE}{$\textcolor{#E8EAEF}{-4.1}$} & \colorbox{#283893}{$\textcolor{#E8EAEF}{-2.9}$} \\ \text{scared}\; & \colorbox{#233179}{$\textcolor{#E8EAEF}{-2.1}$} & \colorbox{#1A243E}{$\hphantom{-}\textcolor{#E8EAEF}{0.1}$} & \colorbox{#3048D6}{$\textcolor{#E8EAEF}{-5.2}$} & \colorbox{#2F46CE}{$\textcolor{#E8EAEF}{-4.6}$} & \colorbox{#3048D4}{$\textcolor{#E8EAEF}{-4.7}$} & \colorbox{#1F2B63}{$\textcolor{#E8EAEF}{-1.4}$} & \colorbox{#27378E}{$\textcolor{#E8EAEF}{-2.7}$} & \colorbox{#2F46CF}{$\textcolor{#E8EAEF}{-4.6}$} \\ \end{array}}

Why divide by \sqrt{d_k}? If the components of q and k are independent, with mean 0 and variance 1, their dot product has mean 0 and variance d_k, so its typical size grows like \sqrt{d_k}.[5:1]

  • \sqrt{d}
  • Measured on random vectors
0100200300400500600700800051015202530Length of the vectorsSpread of the dot product
The standard deviation of the dot product of two random vectors, over 100,000 pairs for each length.

Large scores push the next step against its ceiling, where one token takes everything and the gradients nearly vanish. For " it", the undivided dot products run from −30.3 to 6.3. Softmax, which comes next, would give " cat" a weight of 1.0000 and the runner-up about 3 \times 10^{-10}. The model was trained with the division, so this only shows the scale of the problem, but the division is the reason the real weight is 0.876 and not 1.

The Mask #

GPT-2 is trained to predict the next token, so a token must not see what comes after it. Before the next step, every score above the diagonal is replaced with -\infty, which guarantees a weight of exactly 0. This one rule matters more than it looks, and we will come back to it.

Softmax #

Softmax turns each row of scores into weights that are positive and add up to 1:

a_{ij} = \frac{\exp(S_{ij})}{\sum_{m \le i} \exp(S_{im})}

For the row of " it":

Token Score exp(score) Weight
"The" −1.97 0.140 0.055
" cat" 0.79 2.210 0.876
" ran" −3.02 0.049 0.019
" away" −2.57 0.077 0.030
" because" −3.79 0.023 0.009
" it" −3.70 0.025 0.010
Sum 2.523 1.000

The exponential rewards gaps. " cat" leads "The" by 2.76 in score, and that becomes a factor of 15.8 in weight. Here are all the rows, with the masked cells left empty:

\def\arraystretch{1.15}\def\arraycolsep{0pt} {\scriptsize\begin{array}{r|cccccccc} & \text{\tiny The} & \text{\tiny cat} & \text{\tiny ran} & \text{\tiny away} & \text{\tiny because} & \text{\tiny it} & \text{\tiny was} & \text{\tiny scared} \\ \hline \text{The}\; & \colorbox{#F7F7F4}{$\textcolor{#0E1729}{1.00}$} & & & & & & & \\ \text{cat}\; & \colorbox{#E0E0E1}{$\textcolor{#0E1729}{0.91}$} & \colorbox{#283049}{$\textcolor{#E8EAEF}{0.09}$} & & & & & & \\ \text{ran}\; & \colorbox{#1E2841}{$\textcolor{#E8EAEF}{0.05}$} & \colorbox{#E9EAE9}{$\textcolor{#0E1729}{0.95}$} & \colorbox{#15213B}{$\textcolor{#E8EAEF}{0.01}$} & & & & & \\ \text{away}\; & \colorbox{#2D354E}{$\textcolor{#E8EAEF}{0.12}$} & \colorbox{#D3D4D6}{$\textcolor{#0E1729}{0.85}$} & \colorbox{#17223C}{$\textcolor{#E8EAEF}{0.01}$} & \colorbox{#16223C}{$\textcolor{#E8EAEF}{0.01}$} & & & & \\ \text{because}\; & \colorbox{#3D435A}{$\textcolor{#E8EAEF}{0.19}$} & \colorbox{#707485}{$\textcolor{#0E1729}{0.43}$} & \colorbox{#42495F}{$\textcolor{#E8EAEF}{0.22}$} & \colorbox{#303850}{$\textcolor{#E8EAEF}{0.13}$} & \colorbox{#18233D}{$\textcolor{#E8EAEF}{0.02}$} & & & \\ \text{it}\; & \colorbox{#202A43}{$\textcolor{#E8EAEF}{0.06}$} & \colorbox{#93A4FF}{$\textcolor{#0E1729}{0.88}$} & \colorbox{#18233D}{$\textcolor{#E8EAEF}{0.02}$} & \colorbox{#1A253F}{$\textcolor{#E8EAEF}{0.03}$} & \colorbox{#16213B}{$\textcolor{#E8EAEF}{0.01}$} & \colorbox{#16223C}{$\textcolor{#E8EAEF}{0.01}$} & & \\ \text{was}\; & \colorbox{#2D354E}{$\textcolor{#E8EAEF}{0.12}$} & \colorbox{#9C9FAA}{$\textcolor{#0E1729}{0.63}$} & \colorbox{#18233D}{$\textcolor{#E8EAEF}{0.02}$} & \colorbox{#1A253F}{$\textcolor{#E8EAEF}{0.03}$} & \colorbox{#1A253E}{$\textcolor{#E8EAEF}{0.03}$} & \colorbox{#343B53}{$\textcolor{#E8EAEF}{0.15}$} & \colorbox{#19243E}{$\textcolor{#E8EAEF}{0.02}$} & \\ \text{scared}\; & \colorbox{#242D47}{$\textcolor{#E8EAEF}{0.08}$} & \colorbox{#AEB0B8}{$\textcolor{#0E1729}{0.70}$} & \colorbox{#14203A}{$\textcolor{#E8EAEF}{0.00}$} & \colorbox{#15213B}{$\textcolor{#E8EAEF}{0.01}$} & \colorbox{#15213B}{$\textcolor{#E8EAEF}{0.01}$} & \colorbox{#353D54}{$\textcolor{#E8EAEF}{0.16}$} & \colorbox{#1D2741}{$\textcolor{#E8EAEF}{0.04}$} & \colorbox{#15213B}{$\textcolor{#E8EAEF}{0.01}$} \\ \end{array}}

The Output #

The output for each token is the weighted sum of the value vectors:

z_i = \sum_{j \le i} a_{ij}\, v_j

For " it", that is

\begin{aligned} z_{\text{it}} ={}& 0.055\,v_{\text{The}} + 0.876\,v_{\text{cat}} \\ &+ 0.019\,v_{\text{ran}} + 0.030\,v_{\text{away}} \\ &+ 0.009\,v_{\text{because}} + 0.010\,v_{\text{it}} \end{aligned}

Almost all of it comes from " cat". The cosine between z_{\text{it}} and v_{\text{cat}} is 0.999. This is the number behind the closing line of the Reel, and it is tempting to stop here.

What This Head Actually Learned #

The sentence with arcs from the word it back to each earlier word. The arc to cat is far thicker than the others. Its weight is 0.88. The words after it are dimmed because they are masked.
The row of " it" in layer 4, head 3. Thickness is the weight. The words after " it" are dim because they are masked.

It would be easy to conclude that the model worked out what it refers to. The numbers do not support that, for three reasons.

We picked the best head. Of 144 heads, this is the only one that gives " cat" a majority. Just five rank it first, and the average weight it receives is 5.4%. The most common first choice for " it" is "The", in 113 heads.

02040608010012014000.20.40.60.81Heads, from highest weight to lowestWeight that " it" gives " cat"layer 4, head 3
All 144 heads, sorted. One head stands apart, and half of them give " cat" less than 2.2%.

The head does not read the sentence. Look at the grid again: every token after " cat" gives it the largest share, and " ran" gives it even more than " it" does. Change the noun, and the head follows the new noun, whether or not the sentence makes sense:

Sentence Largest weights of " it"
The dog ran away because it was scared " dog" 0.86, "The" 0.06
The car ran away because it was scared " car" 0.81, "The" 0.10
The animal did not cross the street because it was tired " animal" 0.87, "The" 0.07
The animal did not cross the street because it was wide " animal" 0.87, "The" 0.07
The cat chased the dog because it was angry " dog" 0.54, " cat" 0.37
The dog chased the cat because it was angry " cat" 0.67, " dog" 0.25
The trophy did not fit in the suitcase because it was too big "The" 0.37, " suitcase" 0.28, " trophy" 0.24
The trophy did not fit in the suitcase because it was too small "The" 0.37, " suitcase" 0.28, " trophy" 0.24

The pairs show the real limit. Each pair differs only in the words after " it", and the weights are identical, because the mask hides those words. So " scared" played no part in the 87.6%. With two nouns the head hedges, and the trophy pair, a classic Winograd schema,[6] leaves it undecided. What it has learned is narrower than reference: look back for a likely noun. That is a useful feature, and in our sentence it happens to land on the right word. Later tokens can still do better, because " scared" can see everything before it.

One head barely moves the vector. z_{\text{it}} is 64 of 768 numbers, from one head of 12. Those outputs are joined, mixed and added to the token's vector, and in layer 4 the part that gets added is about a fifth as long as the vector it joins. The cosine between the vectors of " it" and " cat" is 0.636 before the layer and 0.627 after it. So nearly all cat is true of z_{\text{it}} and false of the token. Across the whole model, " it" never becomes special to " cat":

  • was
  • because
  • cat
  • The
024681000.20.40.60.8LayerCosine with " it"
The vector of " it" against four other tokens, at the input of each layer. It stays closer to its neighbours than to " cat".

Attention weights tell you where a number came from. They do not tell you why the model behaves as it does.[7]

One Head Is Not the Model #

Say h is a token's vector as it enters a layer. The 12 outputs of 64 numbers are joined back into 768, multiplied by one more matrix and added to h. A small network then widens the result to 3,072 numbers, applies GELU, narrows it back to 768, and adds that too:

\begin{aligned} h' &= h + \mathrm{Attention}(\mathrm{LN}(h)) \\ h'' &= h' + \mathrm{MLP}(\mathrm{LN}(h')) \end{aligned}

The additions are residual connections,[8] and they are why a layer adjusts a vector instead of replacing it. Each of these layers has 7,087,872 parameters, and there are 12 of them, which gives 144 heads doing different things.

A grid of 144 small heatmaps, 12 layers by 12 heads, each showing one head's real attention weights for the sentence. Many have a bright first column, some have a bright diagonal, and layer 4 head 3 is outlined.
Every head of GPT-2 small on our sentence. A bright first column means every token looks at "The". A bright diagonal means each token looks at itself. The outline marks the head we followed.

Larger models repeat the same block with more of everything. All four versions of GPT-2 share the vocabulary and the limit of 1,024 tokens:

Model Layers Heads Vector size
GPT-2 small 12 12 768
GPT-2 medium 24 16 1,024
GPT-2 large 36 20 1,280
GPT-2 XL 48 25 1,600

Back to Words #

After the last layer and one more normalization, the vector of the last token is multiplied by E. It is the same matrix as at the start: GPT-2 reuses its embedding table as its output layer, which is called weight tying.[9]

\text{logits} = h_{\text{last}}\, E^\top, \qquad p = \mathrm{softmax}(\text{logits})

That is a dot product with each of the 50,257 rows, the same operation for the third time. A token scores high when its row lines up with the model's final vector, and softmax turns the scores into probabilities. After The cat ran away because it was scared, the top five are:

Next token ID Probability
" of" 286 25.2%
"." 13 21.8%
"," 11 11.5%
" and" 290 10.3%
","" 553 4.6%

These five hold 73.4% of the probability, and the top 20 hold 90.7%. The winner has only 25.2%, so the model is far from sure. Always taking the top token is called greedy decoding, and most applications sample from p instead. Either way, one new ID joins the list, here 286 for " of", and everything runs again.

In Code #

This loads GPT-2 small and reproduces the main numbers of the post, computing the head by hand from its own weights.

import torch
from transformers import GPT2LMHeadModel, GPT2TokenizerFast

name = "gpt2"
tokenizer = GPT2TokenizerFast.from_pretrained(name)
model = GPT2LMHeadModel.from_pretrained(name, attn_implementation="eager")
model.eval()

text = "The cat ran away because it was scared"
ids = tokenizer(text, return_tensors="pt").input_ids
print(ids[0].tolist())
# [464, 3797, 4966, 1497, 780, 340, 373, 12008]

# Cosines between rows of the embedding table
E = model.transformer.wte.weight.detach()
cosine = torch.nn.functional.cosine_similarity
print(f"{cosine(E[3797], E[3290], dim=0):.3f}")  # cat, dog
# 0.550
print(f"{cosine(E[3797], E[1097], dim=0):.3f}")  # cat, car
# 0.381

with torch.no_grad():
    output = model(ids, output_attentions=True, output_hidden_states=True)

    # Layer 4, head 3, by hand. hidden_states[4] enters layer 4.
    block = model.transformer.h[4]
    X = block.ln_1(output.hidden_states[4][0])
    Q, K, V = block.attn.c_attn(X).split(768, dim=-1)
    Q, K, V = (m[:, 3 * 64 : 4 * 64] for m in (Q, K, V))

    S = Q @ K.T / 64**0.5
    mask = torch.tril(torch.ones(8, 8, dtype=torch.bool))
    a = S.masked_fill(~mask, float("-inf")).softmax(dim=-1)
    z = a @ V

print(f"{a[5, 1]:.3f}")  # " it" to " cat"
# 0.876
print(f"{cosine(z[5], V[1], dim=0):.3f}")  # z_it, v_cat
# 0.999

# The same weights, as the model reports them
print(torch.allclose(a, output.attentions[4][0, 3], atol=1e-5))
# True

# The next token
top = output.logits[0, -1].softmax(dim=-1).topk(5)
for p, i in zip(top.values, top.indices):
    print(f"{tokenizer.decode(i)!r:7} {p:.3f}")

# ' of'   0.252
# '.'     0.218
# ','     0.115
# ' and'  0.103
# ',"'    0.046

Common Mistakes #

  1. Counting words when the model counts tokens. Limits and prices are in tokens, and a rare word can cost several.
  2. Reading a meaning into one column of an embedding. Columns have no names, and nothing in the model says what any of them should mean.
  3. Treating a cosine of 0 as the mark of unrelated words. In GPT-2's table, that mark is about 0.2.
  4. Reading an attention map as an explanation. One head on one sentence shows what happened there, so try other sentences and other heads before concluding anything.
  5. Forgetting that the mask works per position. A token cannot use what comes after it, but the tokens after it can use it.
  6. Thinking of queries, keys and values as a database lookup. Nothing is retrieved exactly: every value goes into the sum, in some proportion.

  1. R. Sennrich, B. Haddow and A. Birch, Neural machine translation of rare words with subword units, ACL 2016. ↩︎

  2. A. Radford et al., Language models are unsupervised multitask learners, OpenAI, 2019. ↩︎

  3. J. R. Firth, A synopsis of linguistic theory 1930–1955, 1957. The idea that a word is known by the company it keeps. ↩︎

  4. J. Ba, J. Kiros and G. Hinton, Layer normalization, 2016. ↩︎

  5. A. Vaswani et al., Attention is all you need, NeurIPS 2017. ↩︎ ↩︎

  6. H. Levesque, E. Davis and L. Morgenstern, The Winograd schema challenge, KR 2012. ↩︎

  7. S. Jain and B. Wallace, Attention is not explanation, NAACL 2019. ↩︎

  8. K. He, X. Zhang, S. Ren and J. Sun, Deep residual learning for image recognition, CVPR 2016. ↩︎

  9. O. Press and L. Wolf, Using the output embedding to improve language models, EACL 2017. ↩︎

Comments

Loading comments…