The problem
A bigram model predicts the next token from the previous one only. It is a table P with rows and columns, where is the vocabulary size: row is the distribution of the token that comes after token , so P[i, j] is the probability that follows . Training it is just counting. You will build the table, measure how well it predicts a text, and turn that into a perplexity.
The text is one long array of token ids, tokens. Its consecutive pairs (previous, next) are what the model learns from.
Part 1: the table
Count every pair, add alpha to every cell (smoothing, so that no probability is 0), and divide each row by its total:
where:
- is how many times token comes right after token ;
- (alpha) is
alpha, the pretend extra count of every pair; - is
vocab_size. Adding to all cells of a row adds to its total, so the row still adds up to 1.
Example.
tokens = [0, 1, 0, 1, 0, 2],vocab_size = 3,alpha = 1. The pairs are 0→1, 1→0, 0→1, 1→0, 0→2.
row counts plus alpha divided by the total 0 1 2
Part 2: how well the table predicts a text
The mean negative log-likelihood of a text is the average of P[i, j] over its consecutive pairs : a text of tokens has pairs. The smaller it is, the better the table predicts the text.
Example. The same
tokensunder the table above. The five pairs get probabilitiesP[0, 1] = 0.5,P[1, 0] = 0.6,P[0, 1] = 0.5,P[1, 0] = 0.6,P[0, 2] = 0.33. Their are , and the mean is about .
Part 3: perplexity
The perplexity is , where nll is the mean negative log-likelihood. It reads as the number of tokens the model is hesitating between, on average.
Example. : on this text the model hesitates between about 2 of the 3 tokens. A uniform table, with every probability , has a mean negative log-likelihood of and so a perplexity of exactly : a good sanity check.
What to implement
bigram_probs(tokens, vocab_size, alpha=1.0):tokensis a 1-D integer array. Return the table of part 1.bigram_nll(P, tokens): the mean negative log-likelihood of part 2, a number.perplexity(nll): , a Pythonfloat.
Requirements
- No Python loops over tokens: count with
np.add.at, and read the probabilities of the pairs with integer-array indexing. - With
alpha = 0, a row of a token that is never followed by anything would be (row 2 in the example). You will not be tested on that case, but notice why smoothing exists.
Hints
- The previous tokens of all pairs are
tokens[:-1]and the next tokens aretokens[1:]: for the example,[0, 1, 0, 1, 0]and[1, 0, 1, 0, 2]. np.add.at(counts, (prev, next), 1)adds 1 tocounts[prev[k], next[k]]for everyk, and counts repeated pairs correctly (see the embedding exercise for whycounts[prev, next] += 1does not).