Trong bài 1, ta thấy LLM không “chọn” câu trả lời — nó tính phân phối xác suất trên 100,000 tokens, rồi mới sample. Trong bài 3, training dùng loss function để tính gradient và cập nhật weights. Cả hai điều đó đều phụ thuộc vào probability. Nhưng xác suất là gì ngoài “con số 0 đến 1”?

  • Softmax biến logits (số thô từ neural network) thành phân phối xác suất như thế nào?
  • Cross-entropy đo “độ sai” của prediction theo cách nào, và tại sao không dùng MSE?
  • Perplexity là metric gì, tại sao GPT-4 đạt 6 còn random model đạt 100,000?

Bài này trả lời cả ba, kèm code NumPy chạy được. Không cần nhớ lại xác suất từ đại học — chỉ cần biết cộng và nhân.

Mental model tổng quát

RAW OUTPUT (logits)         PROBABILITY                TRAINING               EVAL
                                                       (bài 3)
    [2.5, 1.0, 8.7, ...]                            true label
            |                   |                       |
            |    softmax        |   cross-entropy        |    perplexity
            v                   v       loss             v
    [0.01, 0.003, 0.91, ...]  ------>  scalar  ------>  exp(avg loss)
    probs sum = 1              gradient flows            "confused how much"

Ba bước:

  1. Softmax — logits ra khỏi transformer, cần chuẩn hoá thành xác suất
  2. Cross-entropy — trong training, dùng xác suất đó so với đáp án đúng để tính loss
  3. Perplexity — trong evaluation, average cross-entropy loss qua cả dataset, rồi exp() để ra số dễ đọc

Phần 1: Xác suất cơ bản — khác gì con số thường?

Probability là số từ 0 đến 1 đo khả năng một sự kiện xảy ra. Khác con số thông thường ở chỗ: nó luôn gắn với một tập outcomes, và tổng xác suất của tất cả outcomes phải bằng 1.

Probability distribution là tập hợp các xác suất trên nhiều outcomes:

Dự báo thời tiết ngày mai:
  sunny:  0.6
  rainy:  0.3
  cloudy: 0.1
  ------
  tổng:   1.0

Đây là distribution hợp lệ — non-negative, tổng bằng 1. Không thể có distribution {sunny: 0.6, rainy: 0.8} vì 0.6 + 0.8 = 1.4 > 1.

LLM output cũng là distribution — nhưng trên 100,000 tokens thay vì 3 loại thời tiết:

# ví dụ phân phối LLM sau câu "I want to eat"
{
    "pizza":    0.35,
    "rice":     0.20,
    "chocolate": 0.12,
    "the":      0.0003,
    ...                 # 99,995 tokens còn lại, xác suất rất thấp
    # tổng = 1.0
}

Tại sao language lại probabilistic? Vì “I want to eat ___” có thể tiếp theo bằng pizza, rice, chocolate, hay soup — tất cả đều hợp lý. Không có câu trả lời duy nhất đúng. LLM mô hình hoá sự không chắc chắn này thay vì ép buộc một output cứng.

Phần 2: Softmax — từ logits sang xác suất

Vấn đề

Neural network output ở lớp cuối là logits — vector số thực tuỳ ý:

logits = [2.5, 1.0, 8.7, -1.2, 4.1]

Những con số này không phải xác suất: có thể âm, có thể lớn hơn 1, không tổng bằng 1. Cần một hàm biến logits thành distribution hợp lệ.

Công thức

softmax(x_i) = exp(x_i) / sum(exp(x_j) for all j)

Hai bước:

  1. exp() — làm mọi số dương (vì exp(x) > 0 với mọi x)
  2. Chia cho tổng — normalize thành tổng = 1

Code NumPy

import numpy as np

def softmax(x):
    # Trừ max để tránh overflow (numerical stability)
    # exp(1000) = inf trên float32, nhưng exp(1000 - 1000) = exp(0) = 1
    e_x = np.exp(x - np.max(x))
    return e_x / e_x.sum()

logits = np.array([2.5, 1.0, 8.7, -1.2, 4.1])
probs = softmax(logits)
print(probs)
# [0.0137, 0.0029, 0.9070, 0.0003, 0.0761]
print(probs.sum())
# 1.0000

Token thứ 3 (logit = 8.7) chiếm 90.7% xác suất — rõ ràng được ưa thích nhất. Token thứ 4 (logit = -1.2) chỉ còn 0.03%.

Tại sao gọi là “soft” max?

argmax chọn cứng index của giá trị lớn nhất: [0, 0, 1, 0, 0]. Mất hoàn toàn thông tin về các lựa chọn còn lại.

Softmax giữ lại mọi option với xác suất khác nhau: token tốt thứ hai vẫn có 7.6%. Điều này quan trọng trong training vì gradient vẫn flow qua mọi output — không bị cut off như argmax.

Temperature

Trong sampling, có thêm một parameter: temperature T.

def softmax_with_temperature(x, T=1.0):
    return softmax(x / T)
  • T = 1.0 — mặc định, phân phối gốc
  • T = 0.5 — “lạnh” hơn: token top-1 càng thống trị, output bảo thủ
  • T = 2.0 — “nóng” hơn: phân phối phẳng hơn, output đa dạng/ngẫu nhiên hơn
  • T → 0 — tiệm cận greedy decoding (luôn chọn argmax)
logits = np.array([2.5, 1.0, 5.0])

print(softmax_with_temperature(logits, T=0.5))
# [0.0166, 0.0007, 0.9827]   <- token 3 gần như chắc chắn

print(softmax_with_temperature(logits, T=2.0))
# [0.2928, 0.1847, 0.5224]   <- phân phối phẳng hơn nhiều

Đây là lý do khi dùng API OpenAI, temperature=0 cho output deterministic còn temperature=1.5 cho output sáng tạo hơn.

Phần 3: Cross-entropy — đo “độ sai” của prediction

Setup

Giả sử model đang học dự đoán token tiếp theo sau câu “I want to”. Đáp án đúng là "eat" — token index 2 trong vocabulary nhỏ ["I", "you", "eat", "run", "sleep"].

True label được biểu diễn dưới dạng one-hot vector:

true_label = [0, 0, 1, 0, 0]   # token "eat" ở index 2

Prediction từ model (sau softmax):

pred = [0.1, 0.1, 0.6, 0.1, 0.1]   # model nghĩ "eat" có 60% chance

Công thức

H(y_true, y_pred) = -sum(y_true[i] * log(y_pred[i]) for all i)

Vì one-hot label chỉ có một phần tử = 1, tất cả phần còn lại = 0 → công thức rút gọn thành:

loss = -log(y_pred[correct_index])

Đây là lý do cross-entropy còn được gọi là negative log-likelihood — loss chính là log của xác suất model gán cho đáp án đúng, đảo dấu.

Intuition qua 3 ví dụ

Model tự tin đúng:   pred[2] = 0.99  →  loss = -log(0.99) ≈ 0.01   (thấp, tốt)
Model không chắc:    pred[2] = 0.50  →  loss = -log(0.50) ≈ 0.69   (trung bình)
Model tự tin sai:    pred[2] = 0.01  →  loss = -log(0.01) ≈ 4.60   (cao, tệ)

Log tự nhiên của số gần 1 gần bằng 0. Log của số gần 0 tiệm cận âm vô cực. Dấu âm đảo chiều: model tự tin đúng → loss thấp, model tự tin sai → loss cao. Đây là hàm penalty hoàn hảo cho classification.

Code NumPy

import numpy as np

def cross_entropy_loss(y_true, y_pred):
    # Clip để tránh log(0) = -inf
    y_pred = np.clip(y_pred, 1e-12, 1.0)
    return -np.sum(y_true * np.log(y_pred))

# Ví dụ: đáp án đúng là token index 2
y_true = np.array([0, 0, 1, 0, 0])

# Model tự tin đúng
pred_good = np.array([0.05, 0.05, 0.80, 0.05, 0.05])
print(cross_entropy_loss(y_true, pred_good))   # 0.2231

# Model bơ vơ (uniform)
pred_uniform = np.array([0.2, 0.2, 0.2, 0.2, 0.2])
print(cross_entropy_loss(y_true, pred_uniform))  # 1.6094

# Model tự tin sai
pred_wrong = np.array([0.80, 0.05, 0.05, 0.05, 0.05])
print(cross_entropy_loss(y_true, pred_wrong))    # 2.9957

Tại sao không dùng MSE?

MSE (mean squared error) là loss phổ biến cho regression — loss = (y_pred - y_true)^2. Nhưng với classification + softmax, có hai lý do không dùng:

Gradient đẹp hơn. Khi kết hợp softmax với cross-entropy, đạo hàm theo logits rút gọn thành pred - true — vector đơn giản, ổn định về mặt số học. Với MSE, gradient phức tạp hơn và có xu hướng vanish khi xác suất gần 0 hoặc 1.

Ý nghĩa thống kê. Cross-entropy đến từ information theory — nó đo lượng thông tin bị mất khi dùng phân phối pred để xấp xỉ phân phối true. Minimise cross-entropy tương đương maximise log-likelihood, có nền tảng lý thuyết vững chắc cho maximum likelihood estimation.

Phần 4: Perplexity — metric quen thuộc của LLM benchmark

Công thức

perplexity = exp(average cross-entropy loss over all tokens)

Cụ thể hơn, nếu model dự đoán N tokens và loss trung bình là L:

perplexity = exp(L)
           = exp(-1/N * sum(log(p_i)))

Trong đó p_i là xác suất model gán cho token đúng tại bước thứ i.

Intuition

Perplexity trả lời câu hỏi: trung bình model đang cân nhắc bao nhiêu lựa chọn có độ tin cậy ngang nhau khi dự đoán mỗi token?

  • Perplexity = 1 — model hoàn hảo: mỗi bước gán 100% cho token đúng, không bao giờ nhầm
  • Perplexity = V (vocab size) — model vô dụng: uniform distribution trên tất cả tokens, không biết gì về ngôn ngữ
  • Perplexity = 10 — trung bình model đang cân nhắc 10 options có xác suất ngang nhau

Benchmark thực tế (đo trên WikiText-103):

ModelPerplexity
Random baseline~50,000
GPT-2 (1.5B params)~20
GPT-3 (175B params)~10
GPT-4 (ước tính)~6

Perplexity giảm theo số lượng tham số và chất lượng data — không phải tuyến tính, nhưng có xu hướng rõ.

Tại sao dùng exp(loss) thay vì dùng thẳng loss?

Cross-entropy loss nằm trong log space. exp() đưa nó về không gian tuyến tính, dễ so sánh và có ý nghĩa trực quan hơn (“mấy lựa chọn” thay vì “mấy nats”).

Về mặt toán học, nếu model có xác suất trung bình p cho token đúng, thì perplexity xấp xỉ 1/p. Model gán 50% cho token đúng → perplexity ≈ 2. Model gán 10% → perplexity ≈ 10.

Giới hạn của perplexity

Perplexity thấp không đồng nghĩa model tốt hơn trong thực tế. Một model có thể overfit trên distribution của test set — perplexity thấp nhưng không trả lời câu hỏi thường thức được.

Đây là lý do các benchmark hiện nay (MMLU, HellaSwag, HumanEval, MATH) được dùng song song — chúng đo reasoning, common sense, coding ability trực tiếp, không gián tiếp qua distribution matching.

Phần 5: Sampling — từ distribution ra token

Phần này tie back với bài 1, giờ có thêm công cụ toán học để hiểu rõ hơn.

Sau khi có distribution từ softmax, có bốn chiến lược chọn token:

Greedy decodingargmax(probs). Luôn chọn token xác suất cao nhất. Deterministic, nhưng repetitive và boring.

token = np.argmax(probs)

Temperature sampling — sample ngẫu nhiên từ distribution, với temperature để điều chỉnh:

def sample_with_temperature(logits, T=1.0):
    probs = softmax_with_temperature(logits, T)
    return np.random.choice(len(probs), p=probs)

Top-k sampling — chỉ xét k tokens có xác suất cao nhất, gán 0 cho phần còn lại, rồi renormalize và sample:

def top_k_sample(logits, k=50, T=1.0):
    probs = softmax_with_temperature(logits, T)
    # Lấy k indices lớn nhất
    top_k_indices = np.argsort(probs)[-k:]
    filtered = np.zeros_like(probs)
    filtered[top_k_indices] = probs[top_k_indices]
    filtered /= filtered.sum()   # renormalize
    return np.random.choice(len(filtered), p=filtered)

Top-p (nucleus) sampling — thay vì top k tokens cố định, lấy đủ tokens cho đến khi tích luỹ p% xác suất. Linh hoạt hơn top-k vì tự điều chỉnh theo phân phối:

def top_p_sample(logits, p=0.9, T=1.0):
    probs = softmax_with_temperature(logits, T)
    sorted_indices = np.argsort(probs)[::-1]   # giảm dần
    sorted_probs = probs[sorted_indices]
    cumulative = np.cumsum(sorted_probs)
    # Giữ tokens cho đến khi cumulative > p
    cutoff = np.searchsorted(cumulative, p) + 1
    top_indices = sorted_indices[:cutoff]
    filtered = np.zeros_like(probs)
    filtered[top_indices] = probs[top_indices]
    filtered /= filtered.sum()
    return np.random.choice(len(filtered), p=filtered)

Trong production, top-p với p=0.9 và temperature=0.7 là default phổ biến nhất.

Phần 6: Hands-on — mini language model evaluation

Code hoàn chỉnh, copy-paste và chạy:

import numpy as np

np.random.seed(42)

# --- Setup ---
vocab = ["the", "a", "cat", "dog", "runs"]
vocab_size = len(vocab)
n_predictions = 5

# Giả lập "model output": logits thô cho mỗi bước dự đoán
# Mỗi hàng là logits cho một bước, shape = [n_predictions, vocab_size]
logits_batch = np.array([
    [3.0, 1.2, 0.5, 0.8, 2.1],   # bước 0: ưa "the" và "runs"
    [0.3, 2.8, 1.5, 0.2, 0.9],   # bước 1: ưa "a"
    [0.1, 0.2, 3.5, 0.7, 0.4],   # bước 2: ưa "cat"
    [0.8, 0.6, 0.3, 3.2, 1.1],   # bước 3: ưa "dog"
    [0.2, 0.4, 0.9, 1.1, 3.8],   # bước 4: ưa "runs"
])

# True labels: token index đúng cho mỗi bước dự đoán
true_indices = [0, 1, 2, 3, 4]   # "the", "a", "cat", "dog", "runs"

# --- Softmax ---
def softmax(x):
    e_x = np.exp(x - np.max(x))
    return e_x / e_x.sum()

# --- Cross-entropy loss cho một bước ---
def cross_entropy(probs, true_idx):
    return -np.log(np.clip(probs[true_idx], 1e-12, 1.0))

# --- Tính từng bước ---
losses = []
print(f"{'Step':<6} {'True Token':<12} {'p(correct)':<14} {'Loss':<10}")
print("-" * 46)

for i in range(n_predictions):
    probs = softmax(logits_batch[i])
    true_idx = true_indices[i]
    loss = cross_entropy(probs, true_idx)
    losses.append(loss)

    print(f"{i:<6} {vocab[true_idx]:<12} {probs[true_idx]:<14.4f} {loss:<10.4f}")

# --- Perplexity ---
avg_loss = np.mean(losses)
perplexity = np.exp(avg_loss)

print("-" * 46)
print(f"Average loss : {avg_loss:.4f}")
print(f"Perplexity   : {perplexity:.4f}")
print(f"Vocab size   : {vocab_size}")
print(f"(Random baseline perplexity = {vocab_size})")

Output mẫu:

Step   True Token   p(correct)     Loss
----------------------------------------------
0      the          0.5105         0.6730
1      a            0.6652         0.4076
2      cat          0.8768         0.1313
3      dog          0.7718         0.2590
4      runs         0.8913         0.1152
----------------------------------------------
Average loss : 0.3172
Perplexity   : 1.3733
Vocab size   : 5
(Random baseline perplexity = 5)

Model giả lập này có perplexity ≈ 1.37, tốt hơn nhiều so với random baseline (perplexity = 5 với vocab 5 tokens). Bước 0 có loss cao nhất vì logits cho “the” và “runs” gần nhau, model không tự tin bằng các bước còn lại.

Thay logits_batch bằng output thực từ một neural network, thay vocab bằng tokenizer vocabulary — bạn đã có evaluation loop hoàn chỉnh.

Cheatsheet

Khái niệmÝ nghĩaCông thức đơn giản
ProbabilityKhả năng xảy ra (0 đến 1)P(event) in [0, 1]
DistributionNhiều probabilities, tổng = 1{token: prob}, sum = 1
SoftmaxLogits → probabilitiesexp(x) / sum(exp(x))
Cross-entropyLoss đo “độ sai” của prediction-log(p_correct)
PerplexityEval metric: confused how muchexp(avg cross-entropy)
TemperatureĐiều khiển độ đa dạng khi samplingsoftmax(x / T)

Năm điểm cần nhớ:

  • LLM output là distribution, không phải lựa chọn cứng — softmax đảm bảo điều này
  • Softmax dùng exp() để làm mọi số dương, rồi normalize — có trick trừ max để tránh overflow
  • Cross-entropy = -log(p_correct) — đơn giản, gradient đẹp, có nền tảng thống kê
  • Perplexity = exp(avg loss) — bao nhiêu options model đang cân nhắc trung bình mỗi token
  • Temperature cao → phân phối phẳng → output đa dạng; temperature thấp → output bảo thủ

Lời kết

Phần nền tảng của series sắp hoàn chỉnh. Bài 5 cuối cùng sẽ ghép linear algebra (bài 2) + calculus/backprop (bài 3) + probability (bài này) thành một neural network hoàn chỉnh từ zero: perceptron → MLP → train trên XOR bằng NumPy thuần, không framework. Sau đó là bước đầu tiên vào transformer thực sự.

Trong thời gian chờ, hai nguồn học tốt nhất để đọc song song:

  • Chris Olah “Attention and Augmented Recurrent Neural Networks” — bài blog cũ (2016) nhưng vẫn là cách giải thích attention trực quan nhất hiện có, với animation đẹp
  • 3Blue1Brown “Neural Networks” series, tập 1-3 — visualize được gradient descent và backprop theo cách không có sách giáo khoa nào làm được

Nếu sau bài này bạn muốn đào sâu hơn vào probability cho ML, xem thêm Bishop “Pattern Recognition and Machine Learning” chương 1-2 — nặng hơn nhiều nhưng là nền tảng cho mọi thứ trong bài 5 trở đi.