Karpathy's Zero to Hero, Lecture 2: Building makemore (bigrams) — notes, code and timestamps

Updated 2026-10-07 · Video: Andrej Karpathy, published 2022-09-07 · 118 min

Watch the video (118 min)

Watch the original video on YouTube

Lecture 2 of Andrej Karpathy’s Neural Networks: Zero to Hero series moves from scalar autograd to language modeling. Where Lecture 1 built micrograd, this one starts makemore, a character-level model that learns from about 32,000 names and generates new name-like strings. The whole lecture stays with the simplest possible version, a bigram model that predicts each character from the one before it, and builds it twice. First by plain counting, which yields a table of probabilities, a sampler and a loss. Then as a one-layer neural network in PyTorch, trained by gradient descent, which ends up at essentially the same table. The point of the second build is not a better model but a framework that scales: the softmax, the negative log likelihood loss and the training loop introduced here stay the same all the way up to transformers. Along the way you get a careful lesson in tensor broadcasting that is worth the price of admission on its own. The next step, Lecture 3, gives the model more than one character of context.

Key takeaways

  1. One name holds several training examples (4:39). “isabella” says which letter tends to start a name, which letters follow which, and that the name ends after the last “a”. Padding each word with a start/end token captures all of it.
  2. A bigram model is just a normalized count table (23:31). Rows are the current character, columns the next one; normalize each row and you can sample names by walking it.
  3. Broadcasting can be silently wrong (46:42). Dropping keepdim=True still runs but normalizes columns instead of rows. Check shapes and sums.
  4. Average negative log likelihood is the loss (57:42). Multiply the probabilities the model gives the real data, take logs, negate and average: lower is better, zero is perfect. The counted model scores about 2.45.
  5. Logits plus softmax turn any layer into a probability model (1:27:14). Read the outputs as log counts, exponentiate, normalize each row.
  6. Gradient descent recovers the counting solution (1:45:15). With the same information in, the one-layer net reaches the same loss, and W.exp() plays the role of the count table.
  7. Smoothing and regularization are the same lever (1:53:34). Adding fake counts in the table world corresponds to pulling W toward zero in the network world.
  8. Only the forward pass will change from here on (1:46:48). Logits, softmax, loss and update stay fixed while the network grows toward a transformer.

Chapter notes

0:02 – 5:42 What makemore is, and the names dataset

makemore, like micrograd, is a repository on Karpathy’s GitHub that he rebuilds from a blank notebook. Its job is in the name: give it a dataset and it makes more things like it. The example dataset, names.txt, holds about 32,000 names that he says he found on a government website (0:32). After training, the model invents strings that sound like names but aren’t in the list.

GitHub view of names.txt in the makemore repository: a long numbered list of first names such as brianna, bailey, andrea, khloe and jasmine.
0:51 — names.txt on GitHub, one name per line. This file is the entire training set for the lecture.
makemore README on GitHub showing the --sample-only command and a block of generated names: dontell, khylum, camatena, aeriline, najlah, ... zendy, ... irot, helicha, besdy, ebokun, lucianno.
1:16 — The README's sample output from a trained makemore model: invented names such as dontell, irot and zendy that sound plausible but are not in the dataset.

Under the hood it is a character-level language model (1:35): each line is one example, each example is a sequence of characters, and the model’s task is to predict the next character. Karpathy lays out the plan for the series (2:05): bigram and bag-of-words models, multilayer perceptrons, recurrent networks, and finally a transformer he calls the equivalent of GPT-2, all at the character level. He also floats moving to words and to image models later (2:36).

makemore README: description paragraph, the list of implemented models (Bigram, Bag of Words, MLP, RNN, GRU, Transformer) and the Usage section showing names.txt starting emma, olivia, ava.
2:13 — The README's list of models, from a bigram lookup table up to a transformer: the road map the series follows.

Our note: as published, the series stayed with text. The later lectures build GPT, a tokenizer and a GPT-2 reproduction rather than image models.

The first cell reads the file and calls splitlines() to get a Python list of names (3:07). The list starts emma, olivia, ava, which makes Karpathy suspect it is sorted by frequency. The shortest name has 2 characters and the longest 15 (4:09).

Jupyter cells: words = open('names.txt', 'r').read().splitlines(); words[:10] listing emma, olivia, ava, isabella, ...; len(words) gives 32033; min and max name lengths give 2 and 15.
4:48 — The first cells: 32,033 names, the shortest 2 characters long and the longest 15.

Then the key observation: a single word is several examples packed together (4:39). It tells you which character is likely to come first, which is likely after “i”, which after “is”, and so on, plus one easy-to-miss fact: after the full word, the name ends (5:12).

5:42 – 12:29 Bigrams, counted in a dictionary

A bigram model looks at only two characters at a time: given one, predict the next (5:42). It ignores everything earlier in the word, which makes it weak, but it is the right first step.

To list consecutive pairs, Karpathy uses zip(w, w[1:]) (6:44). Zipping a string with itself shifted by one pairs each character with its successor, and zip stops when the shorter input runs out (7:15). To keep the start and end information, he wraps each word as a list with a special <S> token in front and <E> behind (8:17).

Cell looping over words[:1] with zip(w, w[1:]) and printing e m, m m, m a; below, w is 'emma' and w[1:] is 'mma'.
7:43 — zip pairs 'emma' with 'mma', giving the bigrams em, mm and ma, and stops when the shorter string runs out.
Cell wrapping each of the first three words as ['<S>'] + list(w) + ['<E>'] and printing the pairs: <S> e, e m, m m, m a, a <E>, <S> o, o l, ...
9:45 — With the start and end tokens added, 'emma' yields five bigrams, including <S> e and a <E>.

Counting is a dictionary keyed by the pair, updated with b.get(bigram, 0) + 1 so unseen pairs start at zero (9:49). On the first three names, “a” followed by <E> already shows up three times, since emma, olivia and ava all end in “a” (10:20). Over the whole dataset he sorts b.items() by count with a lambda key (11:27): rare pairs appear once, while “n” ending a name and “a” followed by “n” are among the most frequent (12:29).

Cell building dictionary b with b.get(bigram, 0) + 1 over all words, then sorted(b.items(), key = lambda kv: -kv[1]) listing (('n', '<E>'), 6763), (('a', '<E>'), 6640), (('a', 'n'), 5438), (('<S>', 'a'), 4410), ...
12:51 — Bigram counts over the full dataset, most frequent first: 'n' ending a name (6,763), 'a' ending a name (6,640) and 'a' followed by 'n' (5,438).

12:29 – 18:10 Moving the counts into a PyTorch tensor

A two-dimensional array is far more convenient than a dictionary: rows for the first character, columns for the second (13:00). Karpathy introduces torch.tensor arrays here. torch.zeros defaults to float32, so for counts he asks for dtype=torch.int32 (13:32), then shows basic indexing and in-place increments on a small 3 by 5 example (14:35).

Cells: import torch; a = torch.zeros((3, 5), dtype=torch.int32) printing a 3 by 5 tensor of zeros; a[1, 3] += 1 and a[0, 0] = 5, with the updated tensor showing 5 and 1.
14:58 — The small 3 by 5 int32 tensor used to show indexing and in-place updates before the real count table is built.

The real table needs 26 letters plus two special tokens, so 28 by 28 (15:08). Indexing a tensor needs integers, so he builds a lookup: join all names into one string, take its set of characters, and sort it (16:09). stoi maps “a” to 0 through “z” to 25, with <S> and <E> appended as 26 and 27 (17:10). The counting loop then becomes N[ix1, ix2] += 1 (17:40).

Cells: N = torch.zeros((28, 28), dtype=torch.int32); chars = sorted(list(set(''.join(words)))); stoi = {s:i for i,s in enumerate(chars)} with stoi['<S>'] = 26 and stoi['<E>'] = 27; a loop over words doing N[ix1, ix2] += 1.
18:07 — The first version of the table: 28 by 28, letters at 0 to 25 and <S>, <E> at 26 and 27, filled with N[ix1, ix2] += 1.

18:10 – 24:02 Visualizing the table, then one ‘.’ token

Printed raw, the array is unreadable, so Karpathy plots it with Matplotlib’s imshow (18:10) and writes each cell’s bigram and count on top, using the inverse mapping itos (18:41). A side lesson: indexing a tensor returns a tensor, so .item() is needed to pull out a plain Python number (20:17).

Cell import matplotlib.pyplot as plt; %matplotlib inline; plt.imshow(N), with a small 28 by 28 heatmap of the counts below.
18:36 — The count array as a bare heatmap: some structure is visible, but nothing can be read off it without labels.
Cell with plt.figure(figsize=(16,16)), plt.imshow(N, cmap='Blues') and nested loops calling plt.text with itos[i] + itos[j] and N[i, j].item(); below, a grid of blue cells each labelled with a bigram and its count.
19:49 — Every cell labelled with its bigram and count, using itos and .item(). Darker blue marks more frequent pairs.

The plot exposes waste (20:49). The <E> row is all zeros because the end token never starts a bigram, and the <S> column is all zeros because the start token never follows anything (21:19). The fix is a single special token, written as “.”, that serves as both start and end (21:50). It takes index 0 and the letters shift to 1 through 26, giving a 27 by 27 table (22:21).

Bottom of the labelled 28 by 28 count plot: the second-to-last row, starting with <S>, holds counts, and the last row, for <E>, shows a zero in every cell; the mouse pointer sits below it.
21:00 — The bottom of the plot: the <E> row is entirely zeros, because the end token never starts a bigram.
Cells: N = torch.zeros((27, 27), dtype=torch.int32); stoi = {s:i+1 for i,s in enumerate(chars)}; stoi['.'] = 0; itos = {i:s for s,i in stoi.items()}; stoi printed as {'a': 1, 'b': 2, 'c': 3, ...}.
22:55 — One '.' token at index 0 shifts the letters to 1 through 26 and shrinks the table to 27 by 27.

The new table reads cleanly (23:31). The first row counts how often each letter starts a name, the first column how often each letter ends one, the “.” to “.” cell is zero because there are no empty names, and everything in between is letter-to-letter structure. That table is everything needed to sample.

Labelled 27 by 27 count plot under the plotting code: the top row holds '.' followed by each letter, the first column each letter followed by '.'.
23:31 — The 27 by 27 table: the first row counts letters that start a name, the first column letters that end one.

24:02 – 36:20 Sampling names from the counts

Sampling starts at the “.” token, so the first character comes from row 0 (24:02). N[0] gives that row as a vector of 27 counts. Convert it to float and divide by its sum to get probabilities that add up to one (25:35).

Cells: N[0] printing row 0's 27 counts (0, 4410, 1306, 1542, ...); p = N[0].float(); p = p / p.sum() printing probabilities 0.0000, 0.1377, 0.0408, ...
26:09 — Row 0 of the counts turned into probabilities: the chance of each letter starting a name, with 'a' at about 14%.

To draw from it, Karpathy uses torch.multinomial: give it probabilities and it returns sampled integer indices (26:36). A seeded torch.Generator makes the results reproducible, so viewers get the same samples he does (27:07). He demonstrates on a random three-element distribution, noting that replacement=True must be passed explicitly because it defaults to False (28:09), and checks that sample frequencies roughly match the probabilities (29:10).

PyTorch documentation for torch.multinomial, showing the signature torch.multinomial(input, num_samples, replacement=False, *, generator=None, out=None) and notes on rows and replacement.
26:36 — The torch.multinomial docs. The signature shows replacement=False as the default, which is why the lecture passes replacement=True.
Cells: g = torch.Generator().manual_seed(2147483647); p = torch.rand(3, generator=g) normalized to tensor([0.6064, 0.3033, 0.0903]); torch.multinomial(p, num_samples=100, replacement=True, generator=g) printing 100 indices, mostly 0.
29:31 — The toy check: with probabilities of about 0.61, 0.30 and 0.09, the 100 draws are mostly 0s, some 1s and only a few 2s.

Back on the real row, the first draw is index 13, the letter “m” (30:12), which plausibly starts about 2,500 of the 32,000 names (30:43). The next character comes from the “m” row, and so on. Written as a loop (31:46): start at index 0, take the current row, normalize, sample, stop when the sample is 0 again. The first generated name is “mor.” (33:05).

Cell: g seeded with 2147483647; ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item(); itos[ix]; output 13.
30:44 — The first real draw from row 0 is index 13, which is the letter 'm'.
Cell with the first sampling loop: ix = 0; while True: p = N[ix].float(), p = p / p.sum(), draw ix with torch.multinomial, append itos[ix], break if ix == 0; output 'mor.'
33:56 — The complete loop produces its first name: 'mor.'

Run in a loop, most names look bad (34:15). Karpathy explains why from the model’s point of view: after an “h” it has no idea whether that “h” was the first letter, so ending the name right there seems reasonable. To confirm the model is still doing something, he swaps in a uniform distribution, torch.ones(27) / 27 (35:17). That untrained baseline is pure garbage, so the bigram samples are clearly more name-like; the bigram model is simply very weak (35:49).

Sampling cell wrapped in for i in range(20), printing names: mor., axx., minaymoryles., kondlaisah., anchshizarie., odaren., iaddash., h., jhinatien., egushl., ...
34:57 — Twenty samples from the bigram table. A few look like names and many do not, which is about what one character of context allows.
Same sampling loop with the count lines commented out and p = torch.ones(27) / 27.0; the output is long random strings such as qvsaayxbqrqmyqwuznivanukotdjvdhd.
35:54 — The uniform baseline: with every character equally likely the output is gibberish, so the bigram samples above are a real improvement.

36:20 – 44:06 Normalizing every row at once with torch.sum

Re-normalizing a row on every step of the loop is wasteful, so Karpathy builds a matrix P of probabilities up front (36:20). He is also open about the second motive: practice with tensor manipulation and broadcasting, which will be essential when the series reaches transformers (37:21).

P.sum() with no arguments adds up the whole matrix, which is not what we want (37:53). The docs for torch.sum show the dim argument and keepdim (38:23). Summing over dimension 0 with keepdim gives a 1 by 27 row of column totals (39:27); without keepdim the size-1 dimension is squeezed away, leaving a plain vector of 27 (39:57). Summing over dimension 1 gives the 27 by 1 column of row totals that we actually want (40:31). In this particular table the two sets of totals happen to be identical (41:01).

PyTorch docs for torch.sum showing the signature torch.sum(input, dim, keepdim=False, *, dtype=None) and the text explaining that with keepdim the output keeps the reduced dimension with size 1.
38:37 — The torch.sum docs: with keepdim=True the summed dimension stays as size 1, otherwise it is squeezed out.
Cells: p.shape gives torch.Size([27]); P.sum() gives tensor(228146.); below, P = N.float() and the sampling loop.
39:26 — P.sum() with no arguments adds up the whole table: 228,146, the number of bigrams in the dataset.
Cells: P.shape gives torch.Size([27, 27]); P.sum(0, keepdim=True).shape gives torch.Size([1, 27]).
40:15 — Summing over dimension 0 with keepdim=True leaves a 1 by 27 row of column totals.

Our note: that coincidence has a reason. In a padded word every letter has exactly one bigram leading in and one leading out, and each word contributes one “.” at the start and one at the end, so each character’s row total equals its column total.

Can a 27 by 27 tensor be divided by a 27 by 1 tensor? Broadcasting rules decide (42:03). Align the shapes on the right, then walk leftward: each pair of sizes must be equal, or one of them must be 1, or one must be missing (42:33). Here that holds, so PyTorch stretches the column across all 27 columns and divides element-wise (43:05), normalizing every row (43:36). The check is that row 0 now sums to one and sampling from P gives the same names as before.

PyTorch docs page 'Broadcasting semantics', General semantics section: each tensor needs at least one dimension, and trailing dimension sizes must be equal, 1, or missing, followed by torch.empty examples.
42:23 — The broadcasting rules Karpathy walks through: line the shapes up from the right, and each pair must match, be 1, or be missing.
Cells: P.sum(1, keepdim=True).shape gives torch.Size([27, 1]); a comment cell with # 27, 27 and # 27, 1; P = N.float(); P = P / P.sum(1, keepdim=True).
44:00 — The correct normalization: the 27 by 27 table divided by a 27 by 1 column of row totals, with the two shapes written out as comments.

44:06 – 50:17 The keepdim bug

Karpathy urges viewers to read the broadcasting rules and to treat this with respect (44:06), then shows why. Remove keepdim=True and the row totals come back as a vector of 27, with the same numbers (45:09). Run the rules again: the shapes are still broadcastable (45:40), because the missing dimension gets filled in as 1 (46:10). The division runs, and the result is garbage (46:42).

Cells: P.sum(1, keepdim=True).shape gives torch.Size([27, 1]); P.sum(1).shape gives torch.Size([27]); below, P = N.float(); P = P / P.sum(1) with keepdim removed.
45:51 — Without keepdim the row totals come back as a plain vector of 27, and the division still runs.
Cells: comments # 27, 27 and # 1, 27; P = N.float(); P = P / P.sum(1); the sampling loop below prints long garbled strings such as prrabuxclssoxxxxzontantorsemrdef.
47:13 — The keepdim bug in action: sampling produces nonsense because the table is no longer normalized row by row.

He pauses for viewers to work it out, then explains (47:12). The new dimension is added on the left, so the vector of 27 becomes a 1 by 27 row. That row is copied down 27 times (47:43), and each column ends up divided by totals meant for rows. Row 0 of the result sums to 7, while it is the columns that now sum to one (48:13).

Cells: P = N.float(); P = P / P.sum(1); P[0].sum() gives tensor(7.0225).
48:17 — Row 0 of the buggy P sums to about 7 instead of 1: the totals ended up dividing columns.

The lesson (49:15): check your shapes, know which direction a broadcast goes, and expect these bugs to be hard to find because nothing crashes. He finishes with an efficiency touch, using the in-place P /= ... so no new tensor is allocated (49:46).

Cell: P = N.float(); P = P / P.sum(1, keepdims=True); below, the sampling loop printing mor., axx., minaymoryles., kondlaisah., anchshizarie., odaren., ...
49:53 — Fixed: with the keep-dimension argument back, sampling from P gives the same names as before.

50:17 – 1:00:17 Scoring the model: from likelihood to negative log likelihood

The entries of P are the model’s parameters, found by counting (50:17). Now Karpathy wants one number that says how good they are on the training set, a training loss as in micrograd (50:47).

Cells: P = N.float(); P /= P.sum(1, keepdims=True); the sampling loop for i in range(5) with output mor., axx., minaymoryles., kondlaisah., anchshizarie.; a new cell starting a loop over words with chs = ['.'] + list(w) + ['.'].
51:28 — The table in its final form, normalized in place with P /=. These are the model's parameters, and the next cell starts measuring how good they are.

He prints the probability the model assigns to each actual bigram in the first three names (51:48). As a yardstick, uniform guessing over 27 tokens gives about 4% each, so values like 35% or 40% mean the model has learned something (52:19). A perfect model would give probabilities near 1.

Cell looping over words[:3] printing each bigram with P[ix1, ix2]: .e: 0.0478, em: 0.0377, mm: 0.0253, ma: 0.3899, a.: 0.1960, ...
52:11 — The probability the model gives each bigram of emma, olivia and ava. Values like 0.39 for 'ma' are far above the uniform 1/27, about 0.04.

The textbook summary from maximum likelihood estimation is the likelihood, the product of all those probabilities (53:21). Multiplying many numbers below one gives something tiny, so in practice you take logs (53:51). The log is monotonic, 0 at probability 1 and heading to minus infinity as probability falls to 0 (54:21), and the log of a product is the sum of the logs (55:27). The log likelihood of the three names comes to about −38 (56:11).

WolframAlpha plot of log(x) from 0 to 1: the curve climbs from large negative values near 0 to 0 at x = 1.
54:20 — The log on the interval (0, 1]: 0 at probability 1, falling toward minus infinity as probability approaches 0.
Cell now also computing logprob = torch.log(prob) and printing it beside each probability: .e: 0.0478 -3.0408, em: 0.0377 -3.2793, mm: 0.0253 -3.6772, ma: 0.3899 -0.9418, ...
55:00 — Each bigram's probability with its log: high probabilities give logs near 0, low ones large negative numbers.
Cell accumulating log_likelihood += logprob over the first three words, ending with log_likelihood=tensor(-38.7856).
56:23 — Summing the logs: the three names have a log likelihood of about −38.79.

A loss should go down when things get better, so negate it: the negative log likelihood is zero at best and grows as predictions get worse (56:41). Divide by the number of bigrams for an average (57:12), and the three names score about 2.4 (57:42).

Same cell extended with n += 1, nll = -log_likelihood and print(f'{nll/n}'); output log_likelihood=tensor(-38.7856), nll=tensor(38.7856) and 2.424102306365967.
58:05 — Negated and averaged over the bigrams, the loss for the three names comes to about 2.42.
Step Quantity Better when
Product of probabilities likelihood higher
Sum of log probabilities log likelihood higher (max 0)
Negated negative log likelihood lower (min 0)
Divided by count average NLL, the loss lower

Karpathy summarizes the chain (58:13): maximize likelihood with respect to the parameters, which is the same as maximizing log likelihood, the same as minimizing negative log likelihood, the same as minimizing its average (59:15). He previews the turn the lecture is about to take: these probabilities won’t stay in a table but will be computed by a neural network whose parameters we tune (58:43).

Comment cell: # GOAL: maximize likelihood of the data w.r.t. model parameters (statistical modeling); equivalent to maximizing the log likelihood (because log is monotonic); to minimizing the negative log likelihood; to minimizing the average negative log likelihood; # log(a*b*c) = log(a) + log(b) + log(c).
59:13 — The chain of equivalent goals, written into the notebook as comments above the evaluation cell.

1:00:17 – 1:03:51 Infinite loss and model smoothing

Over the full training set the loss is about 2.45 (1:00:17). The same loop can score any string. Karpathy tries his own name, “andrej”, which averages around 3 per character, largely because “ej” is rare. Then he appends a “q” (1:00:47): “jq” never occurs in the data, its probability is 0, its log is minus infinity, and the loss is infinite (1:01:18).

Evaluation cell run over all words with the per-bigram print commented out; output log_likelihood=tensor(-559891.7500), nll=tensor(559891.7500) and 2.454094171524048.
1:00:25 — Over the whole training set the unsmoothed counting model scores an average negative log likelihood of 2.454.
Same cell switched to for w in ["andrej"], printing .a, an, nd, dr, re, ej (0.0027, -5.9171) and j. with their log probabilities; nll=tensor(21.2735) and an average of 3.0390...
1:00:38 — Scoring 'andrej': about 3.04 per bigram, pushed up by 'ej', which the model gives only 0.27%.
Same cell for 'andrejq': the line jq: 0.0000 -inf, then log_likelihood=tensor(-inf), nll=tensor(inf) and inf.
1:01:03 — 'andrejq': 'jq' never occurs in the data, so its probability is 0 and the loss is infinite.

The fix is model smoothing (1:01:49): add fake counts to every cell before normalizing, P = (N+1).float(). The more you add, the more uniform the model; the less, the more peaked (1:02:19). Adding one removes every zero, so “jq” becomes very unlikely rather than impossible.

Same 'andrejq' evaluation after smoothing: jq: 0.0003 -7.9817, log_likelihood=tensor(-27.8672), nll=tensor(27.8672) and 3.4834...
1:02:44 — After adding 1 to every count, 'jq' gets a tiny probability (0.0003) and the loss for 'andrejq' is finite, about 3.48.

Our note: add-one smoothing is also known as Laplace smoothing. In the notebook, the smoothed model scores about 2.476 on the training set, slightly worse than unsmoothed, which is the expected trade for never assigning zero probability.

Karpathy then recaps the counting approach (1:02:49): training by counting and normalizing, sampling by walking the rows, evaluating by average negative log likelihood.

1:03:51 – 1:10:02 The same model as a neural network, and its training set

Now the same bigram problem goes into the neural network framework (1:03:51). The network takes one character, passes it through weights W, and outputs a probability distribution over the next character (1:04:21). The label is the actual next character, the loss is the negative log likelihood of that label, and gradient-based optimization tunes W to push the loss down (1:04:53).

The training set reuses the bigram loop, but instead of counting it appends the first character’s index to xs and the second’s to ys (1:05:55), then converts both lists to tensors (1:06:27). To keep things readable it starts with only “emma”, which yields five examples: “.” to “e”, “e” to “m”, “m” to “m”, “m” to “a”, “a” to “.” (1:06:57). As integers, input 0 should give a high probability to 5, input 5 to 13, and so on (1:07:28).

Evaluation over all words printing log_likelihood=tensor(-564996.8125, grad_fn=<AddBackward0>), nll=tensor(564996.8125, ...) and 2.476470470428467; below, a new cell '# create the training set of bigrams' with the counting loop copied in.
1:05:48 — The smoothed model's training loss, 2.4765, and under it the start of the network version: the bigram loop copied into a new cell to build the training set.
Cell '# create the training set of bigrams (x,y)' over words[:1], appending ix1 to xs and ix2 to ys and printing . e, e m, m m, m a, a .; xs gives tensor([0, 5, 13, 13, 1]) and ys gives tensor([5, 13, 13, 1, 0]).
1:07:19 — The five training examples from 'emma' as integer tensors: inputs in xs, labels in ys.

A tangent on API traps (1:07:59): PyTorch has both torch.tensor and the class torch.Tensor, and the docs don’t make the difference clear (1:08:29). From a forum thread he finds that lowercase infers the dtype while capital always returns float32. Here lowercase gives integers, which is what indices should be, so he recommends lowercase (1:09:00), and uses the episode to encourage reading docs and threads with care.

PyTorch documentation for torch.tensor: signature torch.tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False), a warning box and the parameter list.
1:08:37 — The torch.tensor docs page Karpathy reads while trying to pin down how it differs from torch.Tensor.
Stack Overflow answer: 'torch.tensor infers the dtype automatically, while torch.Tensor returns a torch.FloatTensor. I would recommend to stick to torch.tensor, which also has arguments like dtype, if you would like to change the type.'
1:08:59 — The forum answer that settles it: lowercase torch.tensor infers the dtype, while capital torch.Tensor always gives a float tensor.

1:10:02 – 1:13:39 One-hot encoding the inputs

An integer index can’t sensibly be multiplied by weights (1:10:02), so the standard trick is one-hot encoding: a vector of zeros with a single one at the index (1:10:34). PyTorch provides it as F.one_hot in torch.nn.functional (1:11:04). Karpathy passes num_classes=27 explicitly, because otherwise it might guess a smaller size from the largest index present (1:11:36). The encoded “emma” examples, xenc, form a 5 by 27 matrix; plotted, each row lights up a single cell (1:12:07).

PyTorch docs for torch.nn.functional.one_hot(tensor, num_classes=-1) → LongTensor, describing num_classes as inferred from the largest class value when set to -1, with examples.
1:11:11 — The F.one_hot docs: num_classes defaults to -1, meaning it is inferred from the largest index, which is why the lecture passes 27 explicitly.
Cells: import torch.nn.functional as F; xenc = F.one_hot(xs, num_classes=27) printing five rows of 27 integers with a single 1 each; xenc.shape gives torch.Size([5, 27]); plt.imshow(xenc) shows five rows with one bright cell each.
1:12:15 — The five 'emma' inputs one-hot encoded: a 5 by 27 matrix with a single 1 in each row.

One more trap: xenc comes back as 64-bit integers (1:12:38), and one_hot takes no dtype argument, so he casts it with .float() before it goes into the network (1:13:09).

Same cells with a new one at the bottom: xenc.dtype gives torch.int64.
1:13:17 — one_hot returns 64-bit integers, so a .float() cast is needed before the values go into a network.

1:13:39 – 1:18:53 One linear layer as a matrix multiply

From micrograd, a neuron computes w·x + b (1:13:39). Karpathy starts with weights for one neuron from torch.randn, normally distributed values mostly near zero (1:14:12), shaped 27 by 1 (1:14:42). xenc @ W is a matrix multiply giving a 5 by 1 result (1:15:12): that one neuron evaluated on all five inputs in parallel (1:15:43).

Cells: xenc.shape torch.Size([5, 27]); plt.imshow(xenc); xenc.dtype now torch.float32; W = torch.randn((27, 1)); xenc @ W giving a 5 by 1 tensor [[0.6886], [0.7705], [-0.2257], [-0.2257], [0.0838]].
1:15:19 — One neuron: a 27 by 1 weight vector applied to all five inputs at once gives five outputs. The two 'm' inputs get identical values.

Make W 27 by 27 and you have 27 neurons (1:16:14), with a 5 by 27 output (1:16:46). Entry [3, 13] is the 13th neuron’s response to the third input, the dot product of that input row with column 13 of W (1:17:16), which he confirms by computing it by hand (1:18:21). There is no bias, no tanh and no second layer: the whole network is one linear layer (1:18:53).

Cells: W = torch.randn((27, 27)); xenc @ W printing a 5 by 27 tensor; (xenc @ W).shape gives torch.Size([5, 27]); comment # (5, 27) @ (27, 27) -> (5, 27).
1:17:04 — Twenty-seven neurons: W becomes 27 by 27 and the output 5 by 27, one activation per neuron per input.
Cells: (xenc @ W)[3, 13] gives tensor(0.5008); xenc[3] shows a one-hot row; W[:, 13] shows column 13 of W.
1:18:17 — Checking one entry: (xenc @ W)[3, 13] is 0.5008, the dot product of input row 3 with column 13 of W.

1:18:53 – 1:26:11 Logits, exponentiation and probabilities

The 27 outputs per example need a meaning (1:19:23). They are positive and negative, so they can’t be probabilities, which must be positive and sum to one (1:19:54), and they can’t be counts, which are positive integers (1:20:24). Karpathy’s choice: treat them as log counts. Exponentiating maps the whole real line to positive numbers, negatives to values below one and positives to values above (1:20:56), so the results can stand in for counts (1:21:57).

WolframAlpha result for exp(x): plots of e^x over -3 to 3 and over a wider range, near 0 for negative x and growing quickly for positive x.
1:21:06 — exp sends negative numbers into (0, 1) and positive ones above 1, so exponentiated outputs can stand in for counts.
Cell (xenc @ W).exp() printing a 5 by 27 tensor of positive values such as 0.8185, 0.0934, 0.3881, 1.7107.
1:22:07 — After exponentiation every entry is positive: the network's version of the count table.

The standard name for these raw outputs is logits (1:23:03). logits.exp() plays the role of the old count matrix N, and dividing each row by its sum with keepdims=True, exactly as before, gives probabilities (1:23:34). Each of the five rows now sums to one (1:24:04). Every step, multiply, exponentiate, sum, divide, is differentiable, so the loss can be backpropagated to W (1:24:35). For the first example, feeding in “.” produces 27 probabilities for what comes next, and changing W changes them (1:25:39).

Cell: logits = xenc @ W # log-counts; counts = logits.exp() # equivalent N; probs = counts / counts.sum(1, keepdims=True); probs printed as a 5 by 27 tensor; probs.shape gives torch.Size([5, 27]); probs[0].sum() gives tensor(1.).
1:24:27 — Logits, then counts, then probabilities: each of the five rows now sums to 1.

1:26:11 – 1:33:05 The forward pass, example by example

Karpathy gathers the pieces into one summary (1:26:11), now seeding the generator for W: one-hot inputs, a layer of 27 neurons that each see 27 inputs, logits, counts, probabilities (1:26:42). The last two steps, exponentiate and normalize, together are the softmax (1:27:14), a layer that turns any real-valued outputs into a probability distribution and can sit on top of any linear layer (1:27:45).

SUMMARY section: xs and ys; a cell seeding g with 2147483647 and W = torch.randn((27, 27), generator=g); a cell with xenc = F.one_hot(xs, num_classes=27).float(), logits, counts, probs and the comment '# btw: the last 2 lines here are together called a softmax'.
1:26:41 — The forward pass gathered into one cell, with a comment noting that exponentiate-then-normalize is the softmax.
Google image results for 'softmax', including a diagram that maps output-layer values 1.3, 5.1, 2.2, 0.7, 1.1 through e^{z_i} / Σ e^{z_j} to probabilities 0.02, 0.90, 0.05, 0.01, 0.02.
1:27:36 — A softmax diagram from an image search: raw outputs in, a probability distribution out, with the largest logit taking most of the mass.

A printout walks through the five bigrams of “emma” (1:28:47). For “.” to “e”, input 0 goes in, label 5 picks out the probability of “e”, and the random network gives it only about 1% (1:29:52), a negative log likelihood near 4 (1:30:22). One pair happens to get 7%. The average over the five, 3.76, is the loss (1:31:25).

Loop printing each example: 'bigram example 1: .e (indexes 0,5)', input 0, the 27 output probabilities, label 5, probability assigned 0.0122..., log likelihood -4.399, negative log likelihood 4.399; then example 2: em.
1:30:01 — Example 1, '.' to 'e': the random network gives the correct label only about 1%, a negative log likelihood of 4.4.
End of the printout: examples 3 to 5 (mm, ma, a.), with 0.0737 assigned to the correct label for ma, and the last line 'average negative log likelihood, i.e. loss = 3.7693049907684326'.
1:31:35 — The average over the five examples, about 3.77, is the loss of this random W.

Changing the seed gives a different W and a loss of 3.37, another seed does worse (1:31:58). Karpathy calls this guess-and-check “amateur hour” (1:32:31). The real method uses the fact that the loss is differentiable: compute its gradient with respect to W and step downhill.

1:33:05 – 1:42:34 Vectorized loss, backward pass and update

The training loop will look almost identical to the end of micrograd, which Karpathy pulls up for comparison (1:33:05). There, four inputs went through an MLP and the loss was mean squared error (1:34:06); then gradients were zeroed, loss.backward() filled them in, and every parameter was nudged against its gradient (1:35:10). Here the network is a linear layer plus softmax, and because this is classification rather than regression, the loss is negative log likelihood (1:35:41).

Two windows side by side: the makemore notebook on the left and the micrograd notebook on the right, showing an MLP, the xs/ys data and a training loop with forward pass, backward pass and update, with printed losses.
1:33:53 — The micrograd training loop pulled up for comparison: forward pass, zero the gradients, backward pass, nudge each parameter.

To pick out the probability of the correct label for every example at once, he indexes probs with two tensors: torch.arange(5) for the rows and ys for the columns (1:37:19). Take .log(), .mean(), and negate, and the result matches the 3.76 from the loop (1:37:51).

Cells: probs.shape gives torch.Size([5, 27]); probs[0, 5], probs[1, 13], probs[2, 13], probs[3, 1], probs[4, 0] gives 0.0123, 0.0181, 0.0267, 0.0737, 0.0150.
1:36:52 — The five correct-label probabilities picked out by hand: the values the vectorized indexing will grab in one go.
Cell: loss = -probs[torch.arange(5), ys].log().mean(); loss gives tensor(3.7693).
1:38:21 — Vectorized: indexing with torch.arange(5) and ys, then log, mean and negate, gives the same 3.7693.

The backward pass (1:38:22): reset the gradient by setting W.grad = None, which PyTorch treats as zero and is more efficient (1:38:52). W must be created with requires_grad=True so PyTorch tracks it (1:39:25). Then loss.backward() walks the computation graph PyTorch recorded during the forward pass, just as micrograd did (1:39:57). W.grad has the same 27 by 27 shape, and each entry says how nudging that weight would change the loss; a positive value means increasing the weight raises the loss (1:40:31).

Cells: W = torch.randn((27, 27), generator=g, requires_grad=True); the forward pass ending in loss; W.grad = None # set to zero the gradient and loss.backward(); W.shape gives torch.Size([27, 27]); W.grad printed, with several rows entirely 0.0000.
1:40:46 — After loss.backward(), W.grad has W's 27 by 27 shape. Rows for characters that never appear as inputs in 'emma' stay at zero.

The update copies micrograd almost exactly: W.data += -0.1 * W.grad (1:41:34). Forward again and the loss drops from 3.76 to 3.74, then 3.72 (1:42:04). That is gradient descent.

Cells: forward pass; print(loss.item()) showing 3.7492129802703857; backward pass; W.data += -0.1 * W.grad.
1:42:30 — One gradient step later the loss reads 3.749, down from 3.769.

1:42:34 – 1:47:51 Training on all 228,000 bigrams

Karpathy rearranges everything into two cells, dataset plus initialization and a gradient-descent loop (1:42:34), with a counter showing five examples (1:43:06). Switching from “emma” to all words gives about 228,000 bigrams, and none of the code needs to change (1:43:38). The loss falls slowly, so he raises the learning rate; even 50 works on this simple problem (1:44:13).

Cells: a dataset cell over words[:1] printing number of examples: 5 and initializing W with requires_grad=True; a '# gradient descent' loop for k in range(10) with forward pass, backward pass and W.data += -0.1 * W.grad, printing a slowly falling loss.
1:43:20 — The lecture's code rearranged into two cells, dataset plus initialization and a gradient descent loop, still running on the five 'emma' examples.
Dataset cell over all words printing number of examples: 228146; gradient descent loop for k in range(10) with W.data += -50 * W.grad, printing losses that fall from 3.14 to 2.67.
1:44:20 — All 228,146 bigrams with a learning rate of 50: the loss drops quickly within ten steps.

What should the loss approach? The counting model scored about 2.45, or about 2.47 smoothed (1:44:44), and the network lands around 2.46 to 2.45 (1:45:15). That makes sense: it gets the same information, the previous character, so it can’t do better. Counting just happens to solve the bigram case directly (1:45:46).

Notebook scrolled back to the counting model's evaluation over all words: log_likelihood=tensor(-564996.8125, ...), nll=tensor(564996.8125, ...) and 2.476470470428467.
1:45:00 — The reference point: the smoothed counting model scored 2.476, the neighborhood gradient descent should reach.

The gain is flexibility (1:46:17). Future networks will take several previous characters and get far more complex, but they will still output logits that pass through softmax and the same loss and update (1:46:48). The counting approach can’t follow: with ten characters of context, the table of combinations becomes far too big to store (1:47:19).

Our note: with 27 symbols and 10 characters of context, the table would need 27¹⁰ rows, roughly 2 × 10¹⁴.

1:47:51 – 1:54:36 W is the count table, and regularization is smoothing

Two observations tie the approaches together. First, multiplying a one-hot vector by W just plucks out one row of W (1:47:51). That is exactly the table lookup of the counting model, where the first character selected a row (1:48:55). Since W holds log counts, W.exp() corresponds to the count array (1:49:26): one was filled in by counting, the other reached piece by piece by following the loss (1:49:57).

Gradient descent cell for k in range(100) with W.data += -50 * W.grad and the final printed loss 2.4623453617095947; xenc is highlighted in the line logits = xenc @ W.
1:48:10 — After 100 steps the network sits at 2.462. The highlighted xenc @ W is the product that just selects a row of W.
The labelled 27 by 27 bigram count plot, scrolled into view again.
1:48:50 — Back at the count table: in the counting model the first character also just picked a row.

Our note: strictly, the match is only up to a scale factor per row. Softmax ignores adding the same constant to a whole row of logits, so gradient descent pins down each row’s shape but not its overall size.

Second, smoothing has a counterpart (1:50:28). Adding a huge fake count to every cell makes each row nearly uniform (1:50:59). In the network, if every weight is zero, the logits are zero, the counts are one, and the probabilities are exactly uniform (1:51:30). So pulling W toward zero is a form of smoothing. That leads to regularization (1:52:00): add a term such as 0.01 * (W**2).mean() to the loss, which is zero only when all weights are zero (1:52:31). He uses the mean rather than the sum to keep it manageable, and the 0.01 sets the strength (1:53:02).

Notebook scrolled up to the counting model's cells: P.sum(1, keepdim=True).shape, the shape comments, and P = (N+1).float(); P /= P.sum(1, keepdims=True).
1:50:20 — The +1 smoothing line in the counting model, which Karpathy turns up in thought to show that large fake counts make every row uniform.
Cell (W**2).sum() returning tensor(1539.0846, grad_fn=<SumBackward0>), above the gradient descent loop.
1:52:32 — The size of the weights: the sum of W squared is about 1,539, which is why the lecture switches to the mean.
Cells: (W**2).mean() gives tensor(2.1112, grad_fn=<MeanBackward0>); the gradient descent loop with loss = -probs[torch.arange(num), ys].log().mean() + 0.01*(W**2).mean(), printing losses around 2.483.
1:53:43 — The regularized loss: 0.01*(W**2).mean() added to the negative log likelihood, so the printed loss now includes the penalty.

Karpathy describes the term as a spring or gravity pulling the weights to zero, opposed by the data pulling the probabilities toward the real statistics (1:53:34). Turning up its strength is the analogue of adding more fake counts; make it strong enough and every prediction goes uniform (1:54:04).

Our note: in this passage Karpathy says “label smoothing”, but what he describes is the add-count smoothing from earlier. In current usage, label smoothing usually means softening the target labels, a different technique. The squared-weight penalty itself is L2 regularization, closely related to weight decay.

1:54:36 – 1:57:42 Sampling from the network, and what comes next

The sampling loop from earlier needs one change (1:54:36): instead of reading row ix of P, one-hot encode ix, multiply by W, exponentiate and normalize (1:55:07). With the same seed, the output names are the same as the counting model’s (1:55:37), because the two are essentially the same model reached by very different routes (1:56:08).

Cell '# finally, sample from the neural net model' with #p = P[ix] commented out and xenc = F.one_hot(torch.tensor([ix]), num_classes=27).float(), logits, counts and p computed instead; output mor., axx., minaymoryles., kondlaisah., anchthizarie.; a cell below holds an earlier run's output for comparison.
1:55:50 — Sampling from the trained network with the same seed gives mor., axx., minaymoryles., kondlaisah. and anchthizarie.: the counting model's names, apart from one letter in the last.

The wrap-up restates the arc (1:56:38): a bigram model trained by counting and by gradient descent, sampled and evaluated with negative log likelihood. From here the networks will take more characters and grow in complexity, but logits, normalization, loss and the gradient-based framework stay identical, all the way to transformers (1:57:10).

Our note: in Lecture 3 the hand-written softmax and negative log likelihood are replaced by PyTorch’s F.cross_entropy, which computes the same thing more efficiently and stably.

Code you’ll write

These are short excerpts in the style of the lecture notebook. The full code is lectures/makemore/makemore_part1_bigrams.ipynb in karpathy/nn-zero-to-hero, and the finished, more general model is in karpathy/makemore, both MIT-licensed.

1. Count every bigram into a 27 by 27 table (22:21). One “.” token marks both start and end, at index 0.

words = open('names.txt', 'r').read().splitlines()
N = torch.zeros((27, 27), dtype=torch.int32)
chars = sorted(list(set(''.join(words))))
stoi = {s: i+1 for i, s in enumerate(chars)}
stoi['.'] = 0
itos = {i: s for s, i in stoi.items()}

for w in words:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        N[stoi[ch1], stoi[ch2]] += 1

2. Normalize rows and sample names (31:46). The +1 is the smoothing added at 1:01:49, and keepdims=True is what makes the division run along rows.

P = (N + 1).float()
P /= P.sum(1, keepdims=True)

g = torch.Generator().manual_seed(2147483647)
for i in range(5):
    out, ix = [], 0
    while True:
        ix = torch.multinomial(P[ix], num_samples=1, replacement=True, generator=g).item()
        out.append(itos[ix])
        if ix == 0:
            break
    print(''.join(out))

3. Average negative log likelihood over the training set (57:12).

log_likelihood = 0.0
n = 0
for w in words:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        prob = P[stoi[ch1], stoi[ch2]]
        log_likelihood += torch.log(prob)
        n += 1
nll = -log_likelihood
print(f'{nll/n}')  # 2.476 with the +1 smoothing; 2.454 without

4. Build the (input, label) training set and initialize W (1:05:55).

xs, ys = [], []
for w in words:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        xs.append(stoi[ch1])
        ys.append(stoi[ch2])
xs = torch.tensor(xs)
ys = torch.tensor(ys)
num = xs.nelement()

g = torch.Generator().manual_seed(2147483647)
W = torch.randn((27, 27), generator=g, requires_grad=True)

5. Train the one-layer network by gradient descent (1:42:34). The regularization term is added at 1:53:02.

import torch.nn.functional as F

for k in range(100):
    xenc = F.one_hot(xs, num_classes=27).float()  # one-hot inputs
    logits = xenc @ W                               # log-counts
    counts = logits.exp()                           # equivalent to N
    probs = counts / counts.sum(1, keepdims=True)   # softmax
    loss = -probs[torch.arange(num), ys].log().mean() + 0.01*(W**2).mean()

    W.grad = None
    loss.backward()
    W.data += -50 * W.grad

6. Sample from the trained network (1:55:07). Only the line that produces p differs from snippet 2.

g = torch.Generator().manual_seed(2147483647)
for i in range(5):
    out, ix = [], 0
    while True:
        xenc = F.one_hot(torch.tensor([ix]), num_classes=27).float()
        counts = (xenc @ W).exp()
        p = counts / counts.sum(1, keepdims=True)
        ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()
        out.append(itos[ix])
        if ix == 0:
            break
    print(''.join(out))

Our note: both spellings appear on screen and in the notebook. The shape checks around 40:15 and the first normalization at 44:00 use keepdim=True, PyTorch’s documented name; from 49:53 on, the normalization lines use keepdims=True. PyTorch accepts both.

Skip list

  • 6:44 – 8:17 How zip pairs consecutive characters. Skip if you write Python every day.
  • 13:32 – 15:08 Creating and indexing a small tensor. Basic PyTorch.
  • 18:41 – 20:49 The plotting code for the count table, and .item(). Look at the picture, skip the Matplotlib.
  • 26:36 – 30:12 torch.multinomial on a toy three-element distribution. The real use follows right after.
  • 1:07:59 – 1:10:02 torch.tensor versus torch.Tensor. The takeaway is one line: use lowercase.
  • 1:31:58 – 1:32:31 Re-rolling the random seed to see different losses, which only sets up why guess-and-check is a bad idea.
  • 1:56:38 – end Recap of the lecture.

Don’t skip 36:20 – 50:17. The broadcasting section is the part most people need on a rewatch.

Glossary

  • makemore — Karpathy’s repository for character-level models that generate more examples like those in a training file; this lecture builds its simplest version.
  • Character-level language model — a model that treats text as a sequence of characters and predicts the next character from those before it.
  • Bigram — a pair of consecutive tokens; a bigram model predicts the next character from only the current one.
  • Special token (‘.’) — an extra symbol padding each name at both ends, so the model learns which letters start and end names.
  • stoi / itos — the lookup dictionaries from characters to integer indices and back.
  • Broadcasting — PyTorch’s rule for combining tensors of different shapes by aligning dimensions from the right and stretching size-1 or missing ones.
  • keepdim — an argument to reductions like sum that keeps the reduced dimension as size 1 instead of removing it; it decides which way a later broadcast runs.
  • torch.multinomial — samples integer indices according to a vector of probabilities.
  • Likelihood — the product of the probabilities a model assigns to the observed data; higher means a better fit.
  • Negative log likelihood (NLL) — minus the sum of log probabilities of the data; averaged over examples it is the loss used here, with 0 as the best value.
  • Model smoothing — adding fake counts to every cell of a count table so no event gets zero probability.
  • One-hot encoding — representing an index as a vector of zeros with a one at that position, so it can be fed into a network.
  • Logits — a layer’s raw, unnormalized outputs, read here as log counts.
  • Softmax — exponentiating logits and normalizing them to sum to one, turning any real-valued outputs into a probability distribution.
  • Gradient descent — repeatedly computing the loss gradient with respect to the parameters and stepping the parameters against it.
  • Regularization — an extra loss term that penalizes large weights; here it pushes predictions toward uniform, the network version of smoothing.

FAQ

What is a bigram language model in Karpathy's makemore lecture?

A model that predicts the next character from the single character before it and nothing else. Karpathy pads every name with a '.' token at the start and end, counts how often each pair of characters appears in 32,033 names, and turns each row of that 27 by 27 count table into a probability distribution. Sampling then walks the table one character at a time until it draws the '.' again.

Why does keepdim matter when normalizing the counts matrix?

P.sum(1, keepdims=True) returns a 27 by 1 column of row totals, which broadcasts across each row, so every row is divided by its own total. Without keepdims the result is a plain vector of 27, which broadcasting treats as a 1 by 27 row. The division still runs without an error, but it divides each column by the wrong totals, so the rows no longer sum to one. Karpathy uses this to show how silently broadcasting bugs can slip through.

What loss does the bigram model reach, and is that good?

Karpathy measures the average negative log likelihood over every bigram in the training set and gets about 2.45 for the counted table, or about 2.47 once add-one smoothing is applied. The neural network version converges to the same neighborhood. Our note: a model that guessed uniformly over 27 characters would score ln 27, about 3.30, so the bigram model has learned something, but its samples are still poor.

Why does Karpathy train the same bigram model twice?

Counting and normalizing gives the best bigram table directly, but it does not scale: with more characters of context the table explodes. Training a 27 by 27 weight matrix with softmax, negative log likelihood and gradient descent lands on essentially the same model and the same samples, and that machinery carries over unchanged to MLPs, RNNs and transformers, where only the forward pass gets more complex.

What is the difference between torch.tensor and torch.Tensor?

In the lecture, Karpathy finds that lowercase torch.tensor infers the data type from what you pass in, so a list of integers becomes an integer tensor, while the capital torch.Tensor class always gives float32. Since the training inputs and labels are character indices, he recommends sticking with lowercase torch.tensor.

Studying another course? Paste its YouTube or Bilibili link on the home page to request a handout — or make your own with the desktop app (coming soon).

Updated 2026-10-07 · By Yao

Not affiliated with the creators, universities or companies whose videos are covered here. Videos belong to their creators; this site only adds commentary.

Sources & credits

How these notes were made

A human watched the full video and wrote these notes with AI assistance for drafting. Stills are limited to a handful of frames used for commentary, each linked to its timestamp; no transcript is reproduced. Spot an error? Let us know via the contact page.