Bốn bài trước đã xây ba mảnh ghép: vector và matrix multiplication (bài 2), gradient và chain rule (bài 3), softmax và cross-entropy (bài 4). Mỗi mảnh đứng riêng đều có vẻ trừu tượng. Bài này ghép chúng lại.

Kết quả của việc ghép đó là neural network — cụ thể là perceptron (1 neuron, 1957) và MLP (nhiều layer, vẫn là backbone của mọi LLM hiện đại). Và để chứng minh rằng đây không phải lý thuyết hàn lâm: bài này kết thúc bằng 60 dòng NumPy thuần train được bài toán XOR, không cần PyTorch, không cần GPU.

Sau bài này, bạn có mental model đủ vững để đọc paper Transformer mà không bị mắc ở phần “Feed Forward Network”. Đó là mục tiêu duy nhất cần đạt được trước khi sang Part 2.

Mental model tổng quát

perceptron:   input → [ w·x + b → activation ] → output

MLP:          input
                └→ hidden layer 1 (nhiều neurons)
                      └→ hidden layer 2 (nhiều neurons)
                            └→ output layer → dự đoán

training:     forward → loss → backward → update
              └──────────────────────────────────┘
                           lặp N lần

Ba khái niệm cốt lõi: neuron đơn, stack nhiều layer, và vòng lặp học. Mọi thứ phức tạp hơn — LSTM, Transformer, GPT-4 — đều là biến thể của ba điều này.

Phần 1: Perceptron — neuron đơn giản nhất (1957)

Frank Rosenblatt công bố perceptron năm 1957 tại Cornell Aeronautical Laboratory. Báo chí New York Times thời điểm đó gọi nó là “bước đầu của máy móc tự suy nghĩ”. Kỳ vọng lớn, nhưng về cơ bản nó là một phép tính cộng có trọng số rồi qua một hàm phi tuyến.

Công thức:

output = activation(w · x + b)

Trong đó:

  • x — vector input (ví dụ: [pixel_1, pixel_2, ..., pixel_784] với ảnh 28×28)
  • w — vector weights, cùng kích thước với x
  • b — bias, một số thực đơn
  • w · x — dot product: w[0]*x[0] + w[1]*x[1] + ... + w[n]*x[n] (linear algebra bài 2)
  • activation — hàm phi tuyến (thảo luận ngay dưới đây)

Weights là gì: “tầm quan trọng” của mỗi input. Nếu w[3] lớn, pixel thứ 3 ảnh hưởng mạnh đến output. Training là quá trình điều chỉnh weights để output đúng.

Bias là gì: ngưỡng kích hoạt. Nếu không có bias, neuron chỉ kích hoạt khi input đủ lớn — bias dịch chuyển ngưỡng đó, cho phép model linh hoạt hơn.

Activation function — tại sao cần

Nếu bỏ activation, output = w · x + b — một phép tính tuyến tính thuần. Stack bao nhiêu layer tuyến tính cũng vẫn là tuyến tính. Bài toán thực tế (nhận dạng ảnh, dự đoán text) là phi tuyến. Activation function đưa phi tuyến tính vào.

TênCông thứcKhi dùng
Sigmoid1 / (1 + e^-x)Cũ, binary classification output
Tanh(e^x - e^-x) / (e^x + e^-x)Cũ, hidden layers, output [-1, 1]
ReLUmax(0, x)Default hidden layer, rẻ và hoạt động tốt
GeLUxấp xỉ x * sigmoid(1.702 * x)LLM hiện đại (GPT, Llama)

ReLU đơn giản đến mức buồn cười — âm thì bằng 0, dương thì giữ nguyên. Nhưng đây là activation được dùng nhiều nhất trong thực tế từ ~2012 đến nay vì nó không bị vanishing gradient như sigmoid/tanh (gradient sigmoid ≈ 0 ở đầu và cuối — backprop đi ngược qua nhiều layer thì gradient biến mất). GeLU là phiên bản smooth hơn ReLU, được dùng trong hầu hết LLM hiện đại.

Giới hạn của perceptron đơn

Perceptron đơn là một đường thẳng phân chia không gian input. Nó chỉ giải được bài toán linearly separable — tức là có thể vẽ một đường thẳng tách hai lớp ra. AND, OR thì được. XOR thì không.

Phần 2: XOR problem — tại sao cần nhiều layer

XOR là bài toán kinh điển chứng minh giới hạn của perceptron đơn.

Input AInput BXOR output
000
011
101
110

Nhìn vào 4 điểm trên mặt phẳng 2D:

  B
  |
1 | (0,1)=1      (1,1)=0
  |
0 | (0,0)=0      (1,0)=1
  +-------------------
  0               1    A

Class 1 ở góc trên-trái và dưới-phải. Class 0 ở góc trên-phải và dưới-trái. Không có đường thẳng nào tách được hai class này.

Perceptron đơn = một đường thẳng quyết định. Vẽ bao nhiêu cách cũng không tách được XOR.

Giải pháp: stack perceptrons.

Thêm một hidden layer. Hidden layer học cách “biến đổi” không gian input sang không gian mới, nơi bài toán trở thành linearly separable. Output layer kết hợp các biểu diễn trung gian đó.

Đây là lý do deep learning gọi là “deep” — nhiều layer là nhiều bước biến đổi không gian. Mỗi layer học một level abstraction cao hơn: layer đầu học cạnh thô, layer giữa học hình dạng, layer cuối học khái niệm.

Phần 3: MLP — Multi-Layer Perceptron

MLP là perceptron được stack. Architecture:

input layer  →  hidden layer(s)  →  output layer

Mỗi layer là: ma trận nhân + cộng bias + activation.

h1     = relu(X  @ W1 + b1)   # input  → hidden
h2     = relu(h1 @ W2 + b2)   # hidden → hidden
output = sigmoid(h2 @ W3 + b3) # hidden → output

Dấu @ là matrix multiplication (bài 2). X là ma trận input có shape [batch_size, n_features]. W1 là ma trận weights shape [n_features, n_hidden]. Kết quả h1 có shape [batch_size, n_hidden].

Shape tracking — dev cần chú ý điều này

Lỗi phổ biến nhất khi code neural network là nhầm shape. Theo dõi shape qua từng bước:

X:    [4, 2]          # 4 samples, 2 features
W1:   [2, 4]          # 2 inputs → 4 hidden units
b1:   [1, 4]          # broadcast theo batch
h1:   [4, 4]          # X @ W1 = [4,2] @ [2,4] = [4,4]

W2:   [4, 1]          # 4 hidden → 1 output
b2:   [1, 1]
out:  [4, 1]          # h1 @ W2 = [4,4] @ [4,1] = [4,1]

Quy tắc matrix multiplication: [A, B] @ [B, C] = [A, C]. Chiều trong phải khớp. Nếu bị ValueError: matmul: shapes mismatch thì đây là nơi đầu tiên nhìn.

Universal approximation theorem

MLP đủ rộng (đủ nhiều neurons ở hidden layer) có thể xấp xỉ bất kỳ hàm liên tục nào với sai số tùy ý nhỏ. Đây là lý thuyết toán học được chứng minh — không phải claim thực nghiệm. Nó không nói gì về cần bao nhiêu neurons, bao nhiêu data để train, hay bao lâu. Nhưng nó nói: về mặt lý thuyết, MLP đủ mạnh để học bất kỳ pattern nào. Phần còn lại là engineering.

Phần 4: Training loop — gradient descent đầy đủ

Có kiến trúc rồi. Nhưng weights ban đầu là random — model chưa biết gì. Training là quá trình điều chỉnh weights theo hướng giảm loss.

Một vòng lặp training đầy đủ:

1. Forward pass:  input → output (dự đoán)
2. Compute loss:  so sánh output với ground truth
3. Backward pass: tính gradient của loss theo mỗi weight (chain rule bài 3)
4. Update:        w = w - lr * gradient
5. Lặp lại

Forward pass chạy data qua network từ đầu đến cuối, thu được output.

Loss đo độ sai lệch giữa output và ground truth. Với binary output dùng MSE (mean((y - y_hat)^2)) hoặc binary cross-entropy. Với multi-class dùng cross-entropy (bài 4).

Backward pass dùng chain rule để tính dL/dW — gradient của loss theo mỗi weight. Đây là backpropagation. Không cần tính bằng tay — nhưng hiểu nguyên lý: gradient cho biết weight nào cần tăng, giảm, và bao nhiêu.

Update rule: w = w - lr * gradient. Learning rate lr kiểm soát bước nhảy mỗi lần. Quá lớn: dao động, không hội tụ. Quá nhỏ: học rất chậm. Thường bắt đầu thử 0.01, 0.1, 0.5.

Hyperparameters quan trọng:

  • lr (learning rate): ảnh hưởng lớn nhất đến tốc độ hội tụ
  • epochs: số lần chạy qua toàn bộ dataset
  • batch_size: số samples xử lý mỗi lần trước khi update weights

Monitoring: print loss mỗi vài trăm epochs. Nếu loss không giảm → lr quá nhỏ hoặc architecture sai. Nếu loss dao động/tăng → lr quá lớn.

Phần 5: Hands-on — code MLP train XOR từ zero

Code dưới đây tự implement forward pass, backward pass, và training loop. Không import gì ngoài NumPy. Copy paste chạy thẳng.

import numpy as np
np.random.seed(42)

# XOR dataset
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])

# Network: 2 inputs -> 4 hidden units -> 1 output
W1 = np.random.randn(2, 4) * 0.5
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5
b2 = np.zeros((1, 1))

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_deriv(x):
    # x ở đây là output đã qua sigmoid, không phải pre-activation
    return x * (1 - x)

lr = 0.5

for epoch in range(5000):
    # Forward pass
    h = sigmoid(X @ W1 + b1)       # [4,2] @ [2,4] = [4,4]
    out = sigmoid(h @ W2 + b2)     # [4,4] @ [4,1] = [4,1]

    # Loss: Mean Squared Error
    loss = np.mean((y - out) ** 2)

    # Backward pass (chain rule thủ công)
    # Gradient của loss theo out: d(MSE)/d(out) = 2*(out-y)/n
    # nhân với sigmoid_deriv để qua activation
    d_out = (out - y) * sigmoid_deriv(out)        # [4,1]

    d_W2 = h.T @ d_out                            # [4,4].T @ [4,1] = [4,1]
    d_b2 = d_out.sum(axis=0, keepdims=True)       # [1,1]

    # Backprop qua W2 vào hidden layer
    d_h = d_out @ W2.T * sigmoid_deriv(h)        # [4,1] @ [1,4] = [4,4]

    d_W1 = X.T @ d_h                              # [2,4].T @ [4,4] = [2,4]
    d_b1 = d_h.sum(axis=0, keepdims=True)         # [1,4]

    # Gradient descent update
    W1 -= lr * d_W1
    b1 -= lr * d_b1
    W2 -= lr * d_W2
    b2 -= lr * d_b2

    if epoch % 500 == 0:
        print(f"Epoch {epoch:4d}: loss = {loss:.4f}")

print("\nFinal predictions:")
print(out.round(2))
print("\nExpected:")
print(y)

Output khi chạy:

Epoch    0: loss = 0.2589
Epoch  500: loss = 0.2454
Epoch 1000: loss = 0.1821
Epoch 1500: loss = 0.0532
Epoch 2000: loss = 0.0152
Epoch 2500: loss = 0.0073
Epoch 3000: loss = 0.0044
Epoch 3500: loss = 0.0030
Epoch 4000: loss = 0.0022
Epoch 4500: loss = 0.0017

Final predictions:
[[0.04]
 [0.96]
 [0.96]
 [0.05]]

Expected:
[[0]
 [1]
 [1]
 [0]]

Loss giảm từ ~0.26 xuống ~0.001. Predictions ≈ [0, 1, 1, 0] — đúng với XOR.

Giải thích từng block

Initialization: np.random.randn(...) * 0.5 — random nhỏ gần 0. Không init bằng 0 vì tất cả neurons sẽ học giống hệt nhau (symmetry problem). Không init quá lớn vì sigmoid saturate, gradient biến mất.

Forward pass: hai lần matrix multiply + sigmoid. Không có vòng lặp, NumPy xử lý song song toàn bộ batch 4 samples cùng lúc.

Loss: MSE đơn giản hơn cross-entropy cho bài demo. Với classification thực tế nên dùng binary cross-entropy.

Backward pass: đây là phần trọng tâm. d_out là gradient của loss theo output của layer cuối — chain rule ghép d(MSE)/d(out) với d(out)/d(pre_activation) (tức sigmoid_deriv). d_W2 là gradient theo weight layer 2, tính bằng h.T @ d_out. Sau đó backprop tiếp qua layer 1 theo cùng pattern.

Update: gradient descent cơ bản nhất — trừ trực tiếp. PyTorch optimizer.step() làm đúng điều này, cộng thêm momentum, weight decay, và learning rate schedule.

Thử nghiệm thêm

Sau khi code chạy được, thử:

  • Tăng hidden units từ 4 lên 8, 16 — loss hội tụ nhanh hơn không?
  • Đổi sigmoid ở hidden layer sang relu (nhớ sửa cả backward) — thấy gì khác?
  • Giảm lr xuống 0.01 — cần bao nhiêu epoch để hội tụ?
  • Tăng lr lên 2.0 — loss có dao động không?

Phần 6: Từ MLP đến Transformer — bridge đến Part 3

Transformer không phải kiến trúc hoàn toàn mới. Nó là MLP được mở rộng với một cơ chế bổ sung gọi là attention.

Mỗi Transformer block gồm:

input
  └→ Multi-Head Attention  + residual connection
        └→ Layer Norm
              └→ Feed Forward Network (MLP)  + residual connection
                    └→ Layer Norm
                          └→ output

Feed Forward Network (FFN) trong Transformer block chính là MLP hai layer:

# FFN trong GPT-2, Llama, và hầu hết LLM
h = gelu(x @ W1 + b1)   # expand: d_model → 4 * d_model
out = h @ W2 + b2        # collapse: 4 * d_model → d_model

Paper Transformer gốc (Vaswani 2017) dùng d_model = 512, FFN mở rộng lên 2048 — gấp 4 lần. Llama-3-8B có d_model = 4096, FFN expand lên 14336 — xấp xỉ 3.5 lần. Pattern này gọi là “bottleneck inverse” hay “wide FFN”.

Trong GPT: mỗi block = attention + FFN, stack 32-96 lần tùy model size. GPT-2 small stack 12 lần. Llama-3-70B stack 80 lần.

Hiểu MLP = hiểu 50% kiến trúc Transformer. Phần còn lại là attention (bài 9-11). Residual connection và Layer Norm là kỹ thuật stability — giải thích ở bài 12.

Cheatsheet

Khái niệmÝ nghĩa ngắn gọn
Neuronactivation(w · x + b) — một đơn vị tính toán
Weights w”Tầm quan trọng” của mỗi input — được học qua training
Bias bNgưỡng kích hoạt — được học qua training
Activation functionĐưa phi tuyến tính vào (ReLU, GeLU, sigmoid, tanh)
LayerNhiều neurons xử lý song song — biểu diễn dưới dạng matrix
MLPNhiều layer stack — mỗi layer học một level abstraction
Forward passInput → output, tính prediction
Backward passGradient từ loss về mỗi weight, dùng chain rule
Training loopForward + compute loss + backward + update, lặp N lần
Learning rateBước nhảy mỗi lần update weight — hyperparameter quan trọng nhất

Sáu điều phải nhớ nếu không nhớ gì khác:

  • Neuron = dot product + activation, không có gì kỳ bí hơn
  • Activation function là lý do tại sao network học được pattern phi tuyến
  • Stack nhiều layer = nhiều bước biến đổi không gian = học được pattern phức tạp hơn
  • Training = lặp đi lặp lại: dự đoán → đo sai → tính gradient → cập nhật
  • Gradient = hướng cần điều chỉnh weight để giảm loss
  • FFN trong Transformer = MLP hai layer, expand 4x rồi collapse

Lời kết

Part 1 Foundation kết thúc ở đây. Năm bài, từ mental model tổng quát (bài 1) đến linear algebra (bài 2), calculus (bài 3), probability (bài 4), và giờ là neural network code được (bài 5). Đây là nền đủ vững để đọc paper Transformer và không bị lạc ở phần toán.

Part 2 bắt đầu với tokenization (bài 6) — một trong những topic underrated nhất của LLM. Phần lớn người học bỏ qua vì nghĩ nó chỉ là “biến text thành số”. Thực tế: tokenization ảnh hưởng đến chất lượng model, cost inference, và lý do tiếng Việt tốn token gấp đôi tiếng Anh. Bài 7 sẽ build BPE tokenizer từ đầu — 200 dòng Python, không dùng thư viện.

Để tiêu hóa bài này trước khi đọc tiếp:

  • Chạy code XOR ở trên, thay đổi hidden units và learning rate để thấy tác động
  • Xem Karpathy “The spelled-out intro to neural networks and backpropagation: building micrograd” trên YouTube (1.5 giờ) — sau bài này bạn hiểu 100% những gì ông giải thích
  • Challenge: tự code MLP classify MNIST (28×28 ảnh chữ số viết tay) bằng NumPy thuần, không PyTorch — khoảng 100-150 dòng nếu làm gọn