Probability, Logs and Softmax
ជំពូកនេះមិនទាន់ត្រូវបានបកប្រែជាភាសាខ្មែរនៅឡើយទេ — សូមអានជាភាសាអង់គ្លេសខាងក្រោម។
Table of Contents
- A Prediction Is a Distribution
- Expected Value: The Average You Would See
- Why Logs: Products Become Sums
- Surprise and Entropy
- Softmax: From Scores to Probabilities
- The Overflow Trap and the Max Trick
- Temperature Is Division Before Softmax
- Cross-Entropy: Punishing a Wrong Guess
- Perplexity in One Paragraph
- Putting It Together: One Step of Next-Word Training
- Recipe: The stats Module
- Try It Yourself
- Quick Review: Common Questions
1. A Prediction Is a Distribution
In the last chapter the model predicted a number: an exam score. A language model predicts something different. After the words "I drink", there is no single right next word. Tea is likely, coffee is quite likely, water is possible, and "brick" is very unlikely. So the model does not answer with one word. It answers with a probability for every word it knows:
after "I drink":
tea ########## 0.5
coffee ###### 0.3
water #### 0.2
... (every other word, close to 0)
That list of probabilities is called a distribution. It follows two
rules you know from
Probability Basics:
every probability is between 0 and 1, and all of them add up to 1,
because something has to come next. Check it in code. The numbers are
invented for the example:
// demo-dist.js
import { sum, expectedValue } from "./src/stats.js";
// a made-up next-word distribution after "I drink"
const next = { tea: 0.5, coffee: 0.3, water: 0.2 };
const probs = Object.values(next);
console.log("sum:", sum(probs));
// a fair die: every face has probability 1/6
const faces = [1, 2, 3, 4, 5, 6];
const fair = faces.map(() => 1 / 6);
console.log("die average:", expectedValue(faces, fair));
// a lottery ticket: win 100 with probability 0.01, otherwise 0
console.log("ticket worth:", expectedValue([0, 100], [0.99, 0.01]));
// output
sum: 1
die average: 3.5
ticket worth: 1
All three lines are stable. This chapter uses src/stats.js, which you
will save in the recipe at the end. In the next chapters you will see the
same file again; the mean, variance and std functions in it are used
in the last chapter.
Say it out loud: "A language model does not output one word. It outputs a probability for every word, and they add up to 1."
2. Expected Value: The Average You Would See
The last two lines above used expected value. It answers: if I could repeat this many times, what would the average result be? Multiply each outcome by its probability, then add:
expected value = sum of (value * probability)
A fair die gives (1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.5, even though you can
never roll a 3.5. The expected value need not be a possible outcome. It
is the long-run average. The lottery ticket pays 100 with chance 0.01,
so on average it is worth 1. If the ticket costs 2, you lose on average.
Why does this matter for AI? A loss is an expected value. When we train, we do not care about one example. We care about the average loss over the examples the model will see. The mean squared error in the last chapter was exactly an average of errors. Everything below ends in an average like that.
3. Why Logs: Products Become Sums
Logarithms scare people, but in AI they do one humble job: they turn
multiplication into addition. In
Functions, Growth and Logarithms
you saw that log(a * b) = log(a) + log(b). We use the natural log,
Math.log, throughout this chapter.
Why would we want that? Because probabilities of several things
happening together are multiplied. The chance that a model gives
the right word at each of 400 positions is p1 * p2 * ... * p400. Each
number is below 1, so the product gets tiny, fast. Watch:
// demo-logs.js
// a log turns a product into a sum
console.log(Math.log(0.5 * 0.5));
console.log(Math.log(0.5) + Math.log(0.5));
// multiply 400 small probabilities of 0.1
let product = 1;
for (let i = 0; i < 400; i++) product *= 0.1;
console.log("product:", product);
// add up their logs instead
let logSum = 0;
for (let i = 0; i < 400; i++) logSum += Math.log(0.1);
console.log("log sum:", logSum.toFixed(3));
// a few values of the natural log
console.log(Math.log(1), Math.log(0.5).toFixed(4), Math.log(0.01).toFixed(4), Math.log(0));
// output
-1.3862943611198906
-1.3862943611198906
product: 0
log sum: -921.034
0 -0.6931 -4.6052 -Infinity
Four things to see:
- The first two lines are equal: the log of a product is the sum of the logs.
- The real answer to the 400-fold product is
10^-400, far smaller than the smallest number a JavaScript float can hold. The computer rounds it to0. This is called underflow, and the information is gone. - The sum of logs is
-921.034: an ordinary number that is perfectly safe. Real models multiply thousands of probabilities, so they work with logs, never with the raw product. Math.logof a number between 0 and 1 is negative.log(1)is0,log(0.5)is-0.69, andlog(0)is-Infinity. Probability close to 1 gives log close to 0. Probability close to 0 gives a large negative log.
All of this output is stable. Logs also help in a second way: a log
squeezes huge ranges. The difference between probabilities 0.01 and
0.001 is tiny as plain numbers but clear on a log scale (-4.6 against
-6.9), which is why a model that confidently gets something wrong is
punished so hard, as you will see in section 8.
Say it out loud: "We use logs because they turn multiplying many small probabilities into adding, and because the product would be too tiny for a computer to store."
4. Surprise and Entropy
Imagine two weather forecasters. In a desert, the forecast "no rain" is right almost every day. When it finally rains, you are very surprised. In a rainy season it is a coin flip every day, so you are never very sure what comes next.
We can put a number on surprise: the surprise of an event with
probability p is -log(p). A likely event (p near 1) has surprise
near 0. A rare event has big surprise. Entropy is the average
surprise of a distribution, weighted by how likely each outcome is.
It measures how uncertain the distribution is:
low entropy: tea 0.97 a 0.01 b 0.01 c 0.01 (almost sure)
high entropy: tea 0.25 a 0.25 b 0.25 c 0.25 (no idea)
The module's entropy uses log2, so the answer is in bits. A fair
coin is exactly one bit: one yes-or-no question.
// demo-entropy.js
import { entropy } from "./src/stats.js";
const show = (name, probs) => console.log(name.padEnd(22), entropy(probs).toFixed(4), "bits");
show("fair coin", [0.5, 0.5]);
show("coin that is 90% heads", [0.9, 0.1]);
show("always heads", [1, 0]);
show("fair 4-sided die", [0.25, 0.25, 0.25, 0.25]);
show("one clear favourite", [0.97, 0.01, 0.01, 0.01]);
// output
fair coin 1.0000 bits
coin that is 90% heads 0.4690 bits
always heads 0.0000 bits
fair 4-sided die 2.0000 bits
one clear favourite 0.2419 bits
Read it from top to bottom. A fair coin is one bit. A biased coin is less uncertain, so less than one bit. A coin that is always heads has no uncertainty at all, zero bits. Four equally likely outcomes need two bits (two yes-or-no questions). A distribution with one clear favourite is nearly decided, so it is low. The rule of thumb: the flatter the distribution, the higher the entropy. All of these outputs are stable.
One honest detail: if you call entropy([1, 0]) without .toFixed, JavaScript
may print -0, a harmless negative zero left over from the arithmetic.
The connection to language models: when a model is unsure, its next-word distribution is flat and has high entropy. When it is confident, the distribution is spiky and has low entropy. The temperature setting you met in Probabilities, Temperature and Sampling slides a model between those two states. You will see how in section 7.
5. Softmax: From Scores to Probabilities
A model's last layer does not output probabilities. It outputs raw scores for every word, one number each, called logits. They can be any numbers: positive, negative, large, small. They do not add up to anything. We need to turn them into a proper distribution.
The recipe is called softmax:
- Take
eto the power of each score (Math.exp). This makes every number positive, and larger scores much larger. - Divide each result by the total, so they add up to 1.
scores [2, 1, 0]
exp [7.389, 2.718, 1.000] total = 11.107
probs [0.665, 0.245, 0.090]
The biggest score gets the biggest share, and every word keeps a chance, which is the "soft" in softmax (a "hard" max would give the winner everything).
// demo-softmax.js
import { softmax, sum } from "./src/stats.js";
const scores = [2, 1, 0];
const probs = softmax(scores);
console.log(probs.map((p) => p.toFixed(4)));
console.log("sum:", sum(probs));
// adding the same number to every score changes nothing
console.log(softmax([1, 2, 3]).map((p) => p.toFixed(4)));
console.log(softmax([101, 102, 103]).map((p) => p.toFixed(4)));
// output
[ '0.6652', '0.2447', '0.0900' ]
sum: 0.9999999999999999
[ '0.0900', '0.2447', '0.6652' ]
[ '0.0900', '0.2447', '0.6652' ]
Notice three things. The probabilities match the hand calculation. The
sum prints 0.9999999999999999 instead of 1: floating point rounding,
the same kind of gotcha as in chapter 2, and nothing to worry about. And
[1, 2, 3] and [101, 102, 103] give identical results. Softmax only
cares about the differences between scores, not their size. That fact
is the key to the next section. All output here is stable.
6. The Overflow Trap and the Max Trick
Because softmax uses Math.exp, big scores are dangerous. exp grows
extremely fast. A float can hold numbers up to about 1.8e308, and exp
passes that limit at an input of around 710:
// demo-overflow.js
import { softmax, sum } from "./src/stats.js";
// the "obvious" softmax, without the max trick
function naiveSoftmax(scores) {
const exps = scores.map((x) => Math.exp(x));
const total = sum(exps);
return exps.map((e) => e / total);
}
console.log(Math.exp(709));
console.log(Math.exp(710));
console.log("naive: ", naiveSoftmax([1000, 999, 998]));
console.log("stable:", softmax([1000, 999, 998]).map((p) => p.toFixed(4)));
// output
8.218407461554972e+307
Infinity
naive: [ NaN, NaN, NaN ]
stable: [ '0.6652', '0.2447', '0.0900' ]
The naive version turns the scores into Infinity, then computes
Infinity / Infinity, which is NaN ("not a number"). The whole
prediction is garbage, and one NaN in a network spreads to every
number it touches.
The fix uses the fact from section 5: softmax does not change when you
add the same number to every score. So subtract the largest score
first. The biggest becomes 0, so exp(0) = 1, and everything else
is a small negative, so exp of it is between 0 and 1. Nothing can
overflow:
scores [1000, 999, 998]
minus the max [ 0, -1, -2]
exp [ 1.000, 0.368, 0.135] <- all safe numbers
That is why softmax in src/stats.js starts with
Math.max(...logits). It is a one-line trick, and every real library
does the same, because it is the difference between working and NaN.
The numbers above are stable.
Say it out loud: "Softmax turns scores into probabilities. We subtract the biggest score first so the exponentials cannot overflow."
7. Temperature Is Division Before Softmax
Now you can see exactly what temperature does. Before softmax, divide
every score by a number T. A small T makes the differences bigger. A
large T shrinks them. Same scores as before, four temperatures:
// demo-temperature.js
import { softmax, entropy } from "./src/stats.js";
const scores = [2, 1, 0];
for (const t of [0.5, 1, 2, 5]) {
const probs = softmax(scores.map((s) => s / t));
console.log(`T = ${t}:`, probs.map((p) => p.toFixed(3)), "entropy", entropy(probs).toFixed(3));
}
// output
T = 0.5: [ '0.867', '0.117', '0.016' ] entropy 0.636
T = 1: [ '0.665', '0.245', '0.090' ] entropy 1.201
T = 2: [ '0.506', '0.307', '0.186' ] entropy 1.472
T = 5: [ '0.402', '0.329', '0.269' ] entropy 1.566
At T = 0.5 the top word gets 86.7%: sharp, low entropy, so the model
nearly always picks the favourite. At T = 5 the three words are close
to equal: flat, high entropy, so sampling wanders and gets more varied
(and more likely to say something odd). T = 1 leaves the scores alone.
Stable output. The temperature chapter explained this as a dial; now you
know it is one division. A common rule of thumb: low temperature for
facts and code, higher for brainstorming.
8. Cross-Entropy: Punishing a Wrong Guess
Last chapter we needed a loss for predicting a score. Now we need a loss
for predicting a word. The training text gives us the right
answer: the word that really came next. The model gave that word some
probability p. We want p to be high.
The loss is simply:
loss = -log(p) where p = probability the model gave to the right answer
This is cross-entropy for one example. It uses exactly the surprise from section 4: "how surprised was the model by the truth?" See how it behaves:
// demo-cross-entropy.js
import { softmax, crossEntropy } from "./src/stats.js";
const probs = softmax([2, 1, 0]); // 0.665, 0.245, 0.090
for (let correct = 0; correct < 3; correct++) {
console.log(`right answer is #${correct}: loss =`, crossEntropy(probs, correct).toFixed(4));
}
console.log("confident and right:", crossEntropy([0.99, 0.005, 0.005], 0).toFixed(4));
console.log("confident and wrong:", crossEntropy([0.01, 0.5, 0.49], 0).toFixed(4));
console.log("certain and wrong: ", crossEntropy([0, 1], 0));
console.log("pure guess, 4 options:", crossEntropy([0.25, 0.25, 0.25, 0.25], 0).toFixed(4));
console.log("ln(4): ", Math.log(4).toFixed(4));
// output
right answer is #0: loss = 0.4076
right answer is #1: loss = 1.4076
right answer is #2: loss = 2.4076
confident and right: 0.0101
confident and wrong: 4.6052
certain and wrong: Infinity
pure guess, 4 options: 1.3863
ln(4): 1.3863
The rules fall out of the numbers:
- High probability on the truth gives low loss.
0.99on the right word costs only0.0101. Perfect confidence (1.0) would cost0. - Low probability on the truth gives high loss. Giving the right
word just
0.01costs4.6052, over 400 times worse. - Certainty in the wrong answer is infinitely bad. A probability of
exactly
0gives-log(0), which isInfinity. Real systems keep probabilities away from0for this reason. - A pure guess has a known cost: with 4 equally likely options the
loss is
ln(4) = 1.3863. A model that cannot beat that is learning nothing.
Notice that cross-entropy only looks at the probability of the right word. It does not matter how the wrong ones are spread. The model is rewarded for pushing probability onto the truth, and softmax guarantees that pushing one up pushes the others down.
This is also why logs earn their place: taking -log turns the product
of probabilities across a whole text into a sum of per-word losses.
Average those, and you have the single loss number a model reports. It
is the same loop as the last chapter, with a different loss function: gradient
descent nudges the model's numbers so the average of -log(p) falls.
All output here is stable.
The module's entropy is in bits (log2) but crossEntropy uses the
natural log. Both are fine, they differ by a fixed constant factor, and
you only compare numbers measured the same way.
Say it out loud: "The loss for next-word prediction is minus the log of the probability the model gave to the right word. Right and confident is near zero; wrong and confident is huge."
9. Perplexity in One Paragraph
Loss numbers like 1.3863 are hard to feel. Perplexity turns them
into something friendlier: take e to the power of the average
cross-entropy. The result reads as "how many equally likely options the
model is effectively choosing between." A perplexity of 4 means the
model is as unsure as if it were picking from 4 equal choices at every
step. Lower is better.
// demo-perplexity.js
import { crossEntropy, mean } from "./src/stats.js";
// probability each model gave to the real next word, over a test text
const perplexity = (probsOfTruth) =>
Math.exp(mean(probsOfTruth.map((p) => crossEntropy([p], 0))));
console.log("guessing 1 of 4:", perplexity([0.25, 0.25, 0.25]).toFixed(3));
console.log("good model: ", perplexity([0.9, 0.8, 0.95]).toFixed(3));
console.log("shaky model: ", perplexity([0.5, 0.1, 0.02, 0.3]).toFixed(3));
// output
guessing 1 of 4: 4.000
good model: 1.135
shaky model: 7.598
(crossEntropy([p], 0) is just a shortcut for -log(p).) The pure
guesser scores exactly 4, the good model is close to 1 (almost
always sure and right), and the shaky model is around 7.6. The
probabilities here are invented. Perplexity is a common way to compare
language models on the same text. It is only meaningful when both models
are measured on the same text with the same vocabulary, so be
careful comparing numbers from different setups. Stable output.
10. Putting It Together: One Step of Next-Word Training
Here is one training example in plain steps. It joins every idea in the last two chapters:
text: "I drink ___" true next word: "tea"
1. model computes scores (logits): [ 2.0, 1.0, 0.0 ] (tea, coffee, water)
2. softmax turns them into probs: [ 0.665, 0.245, 0.090 ]
3. loss = -log(prob of "tea"): 0.4076
4. gradient of the loss w.r.t. every number in the model
5. nudge each number a little against its gradient: loss goes down
Steps 1 to 3 you now have in code. Steps 4 and 5 are chapters 4 and 5. Repeat over billions of words and the probability given to the right word rises on average, which is what "the model got better at predicting text" means. For a real model, the scores come from a deep network with many layers (you will build a tiny one next), and the list is as long as the vocabulary (see Tokens and Tokenizers). That is a simplification we are making honestly: the toy example has three words, a real one has tens of thousands of tokens.
11. Recipe: The stats Module
Save this as src/stats.js. It has no imports:
export const sum = (xs) => xs.reduce((a, b) => a + b, 0);
export const mean = (xs) => sum(xs) / xs.length;
export const variance = (xs) => {
const m = mean(xs);
return mean(xs.map((x) => (x - m) ** 2));
};
export const std = (xs) => Math.sqrt(variance(xs));
// Rescale to mean 0, standard deviation 1.
export function standardize(xs) {
const m = mean(xs);
const s = std(xs);
return xs.map((x) => (s === 0 ? 0 : (x - m) / s));
}
// Stable softmax: subtract the max first so exp() never overflows.
export function softmax(logits) {
const max = Math.max(...logits);
const exps = logits.map((x) => Math.exp(x - max));
const total = sum(exps);
return exps.map((e) => e / total);
}
export const entropy = (probs) =>
-sum(probs.map((p) => (p > 0 ? p * Math.log2(p) : 0)));
// Loss for one prediction: -log(probability given to the right answer).
export const crossEntropy = (probs, correctIndex) => -Math.log(probs[correctIndex]);
export const expectedValue = (values, probs) =>
sum(values.map((v, i) => v * probs[i]));
It exports sum, mean, variance, std, standardize (rescales to
mean 0 and standard deviation 1, used in the last chapter), softmax
(stable, subtracts the max), entropy (in bits), crossEntropy(probs, correctIndex) (natural log, as -log(probs[correctIndex])) and
expectedValue(values, probs).
12. Try It Yourself
- A fair die's entropy. Compute
entropyfor six equal probabilities of1/6. You should see about2.585bits, and the same asMath.log2(6). Why is that more than the 2 bits of a 4-sided die? - Sort by surprise. For the distribution
[0.5, 0.25, 0.15, 0.1], print-Math.log2(p)for each. Which outcome is most surprising? Then compute the weighted average and compare it toentropy. - Break softmax. Call
softmax([-1000, -1001, -1002])and thenaiveSoftmaxfromdemo-overflow.js. Which one fails now, and what does the naive version return? (Hint:expof a very negative number underflows to0.) - Find your temperature. Use
softmax(scores.map(s => s / t))withscores = [3, 2, 1, 0]and find atwhere the top word gets about50%. A guess-and-check loop is fine. - Loss of a whole text. Three predictions,
[0.7, 0.2, 0.1],[0.1, 0.8, 0.1]and[0.3, 0.3, 0.4], have the right answers at positions0,1and2. Compute the average cross-entropy. You should see about0.4987. - Overconfidence. Compare
crossEntropy([0.5, 0.5], 0)withcrossEntropy([0.99, 0.01], 1). Which model deserves the bigger penalty, and what does that say about confident wrong answers?
13. Quick Review: Common Questions
Questions beginners often ask about this chapter. Try answering each one yourself first, then click the question to open the answer.
"What is softmax, in one sentence?"
It turns a list of raw scores into probabilities that are all positive and add up to 1, giving the biggest score the biggest share. It is the last step of a model that has to choose among many options, like the next word.
"Why do we subtract the max before softmax?"
Because exp of a large number overflows to infinity and the result
becomes NaN. Softmax gives the same answer if you shift all scores by
the same amount, so subtracting the largest one keeps every number safe
without changing the answer.
"What is cross-entropy loss?"
It is minus the log of the probability the model gave to the correct answer. If the model was confident and right the loss is near zero. If it was confident and wrong the loss is huge. Training lowers its average over lots of examples.
"Why logarithms? Why not just use the probability?"
Two reasons. A whole text's probability is a product of thousands of small numbers, which underflows to zero in a computer, while a sum of logs is safe. And the log makes confident mistakes cost a lot, which gives training a strong push where it is most needed.
"What is entropy?"
It is the average surprise of a distribution, a measure of how uncertain it is. A fair coin has one bit, a coin that is always heads has zero. A flat next-word distribution has high entropy and a spiky one has low entropy.
"What does temperature do mathematically?"
It divides the scores by a number before softmax. A temperature below 1 makes the distribution sharper and more predictable, above 1 flatter and more random. At exactly 1 nothing changes. It does not make a model smarter or more correct, just more or less adventurous.
"What is perplexity?"
It is e to the average cross-entropy, read as "the number of equally
likely choices the model is effectively unsure between." Lower is better,
and it is only fair to compare models on the same text and vocabulary.
Where to go next. You can now explain how a model's raw scores become probabilities, and how a single number, the cross-entropy, says how wrong the model is. You have also seen every piece of the training loop except the model itself. In the next chapter you build that: a tiny neural network with a few neurons, trained from scratch on a problem a straight line cannot solve: Build a Neural Network.
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!