Neural Networks Fundamentals: Bắt chước Bộ não Con người

Hãy tưởng tượng bạn dạy một đứa trẻ nhận biết chó. Bạn không nói "Nếu có 4 chân AND đuôi dài AND mõm nhọn THEN là chó". Thay vào đó, bạn cho trẻ xem hàng trăm ảnh chó khác nhau, và não bộ trẻ tự học pattern.

Neural Networks (Mạng nơ-ron nhân tạo) lấy cảm hứng từ cách não bộ hoạt động - hàng tỷ neurons kết nối với nhau, học từ kinh nghiệm. Nhưng đừng nhầm: Neural Networks không phải "bộ não thật" - chúng chỉ là mathematical functions cực kỳ phức tạp được tối ưu hóa để fit data.

Theo nghiên cứu của MIT, con người có khoảng 86 tỷ neurons trong não. GPT-3 có 175 tỷ parameters - nhưng vẫn không thể làm nhiều thứ mà một đứa trẻ 3 tuổi làm được. Tại sao? Vì kiến trúc và cách học hoàn toàn khác nhau.

Perceptron: "Neuron" Đầu tiên (1958)

Cấu trúc Perceptron

Perceptron là neural network đơn giản nhất - chỉ có 1 neuron.

Inputs:    x₁ ────w₁────┐
           x₂ ────w₂────┤
           x₃ ────w₃────┼──> Σ ──> Activation ──> Output
           ...           │
           xₙ ────wₙ────┘
                         ↑
                      Bias (b)

Components:

  1. Inputs (x₁, x₂, ..., xₙ): Features của data
  2. Weights (w₁, w₂, ..., wₙ): Importance của mỗi input
  3. Bias (b): Threshold để activate
  4. Weighted Sum: z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b
  5. Activation Function: Quyết định output cuối cùng

Ví dụ: AND Gate

Problem: Học logic AND

x₁  x₂  | Output
0   0   | 0
0   1   | 0
1   0   | 0
1   1   | 1

Solution với Perceptron:

# Sau khi training, perceptron học được:
w₁ = 0.5, w₂ = 0.5, b = -0.7

# Test:
x₁=1, x₂=1: z = 0.5*1 + 0.5*1 + (-0.7) = 0.3 > 0 → Output = 1 ✓
x₁=1, x₂=0: z = 0.5*1 + 0.5*0 + (-0.7) = -0.2 < 0 → Output = 0 ✓
x₁=0, x₂=1: z = 0.5*0 + 0.5*1 + (-0.7) = -0.2 < 0 → Output = 0 ✓
x₁=0, x₂=0: z = 0.5*0 + 0.5*0 + (-0.7) = -0.7 < 0 → Output = 0

Perceptron Learning Rule

Mục tiêu: Tìm weights (w) và bias (b) sao cho predictions đúng.

Algorithm:

1. Initialize weights randomly
2. For each training example:
   - Predict: ŷ = activation(w·x + b)
   - Calculate error: e = y_actual - ŷ
   - Update weights: w_new = w_old + learning_rate * e * x
   - Update bias: b_new = b_old + learning_rate * e
3. Repeat until converged

Ví dụ update:

Input: x = [1, 1], Actual: y = 1
Current weights: w = [0.2, 0.3], b = -0.1

Predict: z = 0.2*1 + 0.3*1 + (-0.1) = 0.4 → ŷ = 1 (if z > 0)
Error: e = 1 - 1 = 0 → No update (prediction đúng)

Input: x = [0, 1], Actual: y = 0
Predict: z = 0.2*0 + 0.3*1 + (-0.1) = 0.2 → ŷ = 1
Error: e = 0 - 1 = -1

Update:
w₁_new = 0.2 + 0.1*(-1)*0 = 0.2
w₂_new = 0.3 + 0.1*(-1)*1 = 0.2  ← Giảm weight vì x₂ gây ra sai prediction
b_new = -0.1 + 0.1*(-1) = -0.2   ← Giảm bias

Giới hạn của Perceptron

Perceptron chỉ học được linear decision boundaries.

✓ Có thể học: AND, OR
✗ KHÔNG thể học: XOR

XOR problem:
x₁  x₂  | Output
0   0   | 0
0   1   | 1
1   0   | 1
1   1   | 0

Không có đường thẳng nào phân chia được!
  x₂
  1  |  1      0
     |
  0  |  0      1
     └─────────── x₁
        0      1

Đây là XOR crisis năm 1969 - gần như giết chết AI research trong 10+ năm.

Giải pháp: Multi-Layer Perceptron (MLP) - thêm hidden layers.

Multi-Layer Perceptron (MLP): Vượt qua XOR

Kiến trúc MLP

Input     Hidden       Output
Layer     Layer        Layer

x₁ ───┐   h₁ ───┐
      ├───    ├───> ŷ
x₂ ───┤   h₂ ───┘
      │       ↑
x₃ ───┘    h₃ 
           ↑
         Bias

3 loại layers:

  1. Input Layer: Nhận raw features (không có computation)
  2. Hidden Layer(s): Thực hiện transformations phức tạp
  3. Output Layer: Produce final predictions

Tại sao hidden layers quan trọng?

Mỗi hidden layer học higher-level features:

Ví dụ: Image Recognition

Input (pixels) → Hidden Layer 1 → Hidden Layer 2 → Hidden Layer 3 → Output
Raw pixels     → Edges, lines   → Shapes, textures → Object parts  → Dog/Cat
  • Layer 1: Học edges, corners
  • Layer 2: Kết hợp edges thành shapes (eyes, ears)
  • Layer 3: Kết hợp shapes thành object parts (face, legs)
  • Output: Class prediction

Forward Propagation: "Đi Từ Input đến Output"

Process:

1. Input Layer → Hidden Layer 1:
   z₁ = W₁·x + b₁
   a₁ = activation(z₁)

2. Hidden Layer 1 → Hidden Layer 2:
   z₂ = W₂·a₁ + b₂
   a₂ = activation(z₂)

3. Hidden Layer 2 → Output:
   z₃ = W₃·a₂ + b₃
   ŷ = activation_output(z₃)

Ví dụ cụ thể:

# Architecture: 3 inputs → 4 hidden → 1 output

import numpy as np

# Input
x = np.array([0.5, 0.3, 0.2])

# Weights (random khởi tạo)
W1 = np.random.randn(4, 3)  # 3→4
b1 = np.random.randn(4)
W2 = np.random.randn(1, 4)  # 4→1
b2 = np.random.randn(1)

# Forward pass
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

# Layer 1
z1 = W1.dot(x) + b1
a1 = sigmoid(z1)  # [4 neurons]

# Layer 2 (output)
z2 = W2.dot(a1) + b2
y_pred = sigmoid(z2)  # [1 neuron]

print(f"Prediction: {y_pred}")

Activation Functions: "Phi tuyến hóa" Neural Network

Nếu không có activation functions, MLP chỉ là linear transformation - vô dụng!

Tại sao?

Layer 1: z₁ = W₁·x + b₁
Layer 2: z₂ = W₂·z₁ + b₂ = W₂·(W₁·x + b₁) + b₂
                          = (W₂·W₁)·x + (W₂·b₁ + b₂)
                          = W_combined·x + b_combined

→ Vẫn chỉ là linear function!

Activation functions làm network non-linear → học được complex patterns.

1. Sigmoid (Logistic)

Công thức:

σ(z) = 1 / (1 + e^(-z))

Graph:

  1.0 |        ______
      |      /
  0.5 |    /
      |  /
  0.0 |_/
      └─────────── z
     -5   0    5

Properties:

  • Output range: (0, 1) → interpret như probability
  • Smooth, differentiable
  • S-shaped curve

Ưu điểm:

  • Output trong [0,1] → tốt cho binary classification
  • Smooth gradients

Nhược điểm:

  • Vanishing gradient: Khi z rất lớn/nhỏ (|z|>5), gradient ≈ 0 → learning chậm
  • Not zero-centered: Output luôn dương → có thể gây training issues
  • Expensive computation: exp() operation chậm

Khi nào dùng:

  • Output layer của binary classification
  • Ít dùng ở hidden layers (ReLU tốt hơn)

2. Tanh (Hyperbolic Tangent)

Công thức:

tanh(z) = (e^z - e^(-z)) / (e^z + e^(-z))
        = 2σ(2z) - 1

Graph:

  1.0 |        ______
      |      /
  0.0 |    /
      |  /
 -1.0 |_/
      └─────────── z
     -5   0    5

Properties:

  • Output range: (-1, 1)
  • Zero-centered (tốt hơn sigmoid)
  • S-shaped

Ưu điểm:

  • Zero-centered → convergence nhanh hơn sigmoid
  • Stronger gradients than sigmoid (vì range rộng hơn)

Nhược điểm:

  • Vẫn có vanishing gradient
  • Vẫn expensive computation

Khi nào dùng:

  • Hidden layers (tốt hơn sigmoid nhưng kém hơn ReLU)
  • RNN/LSTM gates

3. ReLU (Rectified Linear Unit)

Công thức:

ReLU(z) = max(0, z)
        = z if z > 0
        = 0 if z ≤ 0

Graph:

    |    /
    |  /
    |/
────┴──────── z
    0

Properties:

  • Output range: [0, ∞)
  • Non-linear nhưng piece-wise linear
  • Cực kỳ đơn giản (chỉ 1 comparison)

Ưu điểm:

  • Rất nhanh: Chỉ cần so sánh với 0
  • Không bị vanishing gradient (khi z > 0, gradient = 1)
  • Sparse activation: ~50% neurons = 0 → efficient
  • Thực tế train nhanh hơn sigmoid/tanh rất nhiều

Nhược điểm:

  • Dying ReLU: Nếu z < 0, gradient = 0 → neuron "chết" (không update được nữa)
  • Not zero-centered
  • Unbounded output (có thể explode)

Khi nào dùng:

  • Default choice cho hidden layers
  • Hầu hết CNN, modern architectures

Ví dụ dying ReLU:

# Neuron với weights làm z luôn < 0
z = -5  # Always negative
ReLU(z) = 0
gradient = 0  # Không update được
→ Neuron "chết"

4. Leaky ReLU

Fix dying ReLU bằng cách cho phép small gradient khi z < 0:

Công thức:

Leaky_ReLU(z) = z      if z > 0
              = 0.01z  if z ≤ 0

Graph:

    |    /
    |  /
    |/
___/┴──────── z
   0

Ưu điểm:

  • Không bị dying ReLU
  • Vẫn giữ speed của ReLU

Variants:

  • Parametric ReLU (PReLU): α là learnable parameter thay vì fix 0.01
  • ELU (Exponential Linear Unit): Smooth hơn ở z < 0

5. Softmax (Multi-class Classification)

Dùng ở output layer cho multi-class classification.

Công thức:

softmax(z_i) = e^(z_i) / Σⱼ e^(z_j)

Ví dụ:

Logits (raw outputs): z = [2.0, 1.0, 0.1]

e^2.0 = 7.39
e^1.0 = 2.72
e^0.1 = 1.11
Sum = 11.22

Softmax:
Class 0: 7.39 / 11.22 = 0.659 (65.9%)
Class 1: 2.72 / 11.22 = 0.242 (24.2%)
Class 2: 1.11 / 11.22 = 0.099 (9.9%)
                        ─────
                        1.000  ← Sum = 1

Properties:

  • Outputs sum to 1 → interpret như probability distribution
  • Differentiable → có thể backprop

Khi nào dùng:

  • Output layer của multi-class classification (Cat/Dog/Bird/...)

Tóm tắt: Khi nào dùng Activation nào?

Hidden Layers:
├─ Default: ReLU
├─ Dying ReLU issues: Leaky ReLU, PReLU, ELU
└─ Legacy/RNN: Tanh

Output Layer:
├─ Binary classification: Sigmoid
├─ Multi-class classification: Softmax
└─ Regression: Linear (no activation)

Backpropagation: "Học Ngược" Từ Error

Forward propagation cho predictions. Nhưng làm sao update weights để predictions tốt hơn?

Backpropagation: Tính gradient của loss theo weights, sau đó update ngược từ output về input.

Intuition

Goal: Minimize loss function L(ŷ, y)

Chain Rule:

∂L/∂W = ∂L/∂ŷ * ∂ŷ/∂z * ∂z/∂W

"Thay đổi W ảnh hưởng đến z, z ảnh hưởng đến ŷ, ŷ ảnh hưởng đến L"

Ví dụ đơn giản: 1 hidden layer

Network:
x → z₁ = W₁x + b₁ → a₁ = σ(z₁) → z₂ = W₂a₁ + b₂ → ŷ = σ(z₂)

Loss: L = (y - ŷ)²

Backward pass:

# 1. Output layer gradient
dL_dyhat = -2 * (y - y_pred)
dyhat_dz2 = sigmoid_derivative(z2)  # σ'(z) = σ(z)*(1-σ(z))
dz2_dW2 = a1
dz2_db2 = 1

# Gradient cho W2, b2
dL_dW2 = dL_dyhat * dyhat_dz2 * dz2_dW2
dL_db2 = dL_dyhat * dyhat_dz2 * dz2_db2

# 2. Hidden layer gradient
dz2_da1 = W2
da1_dz1 = sigmoid_derivative(z1)
dz1_dW1 = x
dz1_db1 = 1

# Gradient cho W1, b1
dL_dW1 = dL_dyhat * dyhat_dz2 * dz2_da1 * da1_dz1 * dz1_dW1
dL_db1 = dL_dyhat * dyhat_dz2 * dz2_da1 * da1_dz1 * dz1_db1

Pattern: Gradient "chảy ngược" từ output về input, multiply theo chain rule.

Gradient Descent Update

Sau khi có gradients, update weights:

learning_rate = 0.01

W2 = W2 - learning_rate * dL_dW2
b2 = b2 - learning_rate * dL_db2
W1 = W1 - learning_rate * dL_dW1
b1 = b1 - learning_rate * dL_db1

Intuition: Di chuyển weights theo hướng ngược với gradient (minimize loss).

Loss surface:
      │   ╱╲
      │  ╱  ╲
Loss  │ ╱    ╲
      │╱      ╲___
      └────────────── Weight
                ↑
          Gradient trỏ lên → Move xuống (negative gradient)

Optimization Algorithms: Nâng cấp Gradient Descent

1. Vanilla Gradient Descent

Update tất cả weights sau khi xem TOÀN BỘ training data.

for epoch in range(num_epochs):
    # Forward pass trên TẤT CẢ training data
    predictions = forward(X_train)
    loss = compute_loss(predictions, y_train)
    
    # Backward pass
    gradients = backward(loss)
    
    # Update
    weights = weights - learning_rate * gradients

Nhược điểm:

  • Rất chậm với large datasets (phải xem hết data mới update 1 lần)
  • Có thể stuck ở local minima

2. Stochastic Gradient Descent (SGD)

Update weights sau MỖI sample.

for epoch in range(num_epochs):
    for x, y in training_data:  # Từng sample
        prediction = forward(x)
        loss = compute_loss(prediction, y)
        gradients = backward(loss)
        weights = weights - learning_rate * gradients

Ưu điểm:

  • Update frequently → learn nhanh
  • Randomness giúp escape local minima

Nhược điểm:

  • Noisy updates → loss bounces around (không smooth)
  • Chậm trên modern hardware (không tận dụng được vectorization)

3. Mini-Batch Gradient Descent

Best of both worlds: Update sau MỖI BATCH (VD: 32, 64, 128 samples).

batch_size = 32

for epoch in range(num_epochs):
    for i in range(0, len(X_train), batch_size):
        X_batch = X_train[i:i+batch_size]
        y_batch = y_train[i:i+batch_size]
        
        predictions = forward(X_batch)
        loss = compute_loss(predictions, y_batch)
        gradients = backward(loss)
        weights = weights - learning_rate * gradients

Ưu điểm:

  • Smooth hơn SGD (average gradient over batch)
  • Nhanh hơn vanilla GD
  • Tận dụng GPU/vectorization tốt

Batch size rules of thumb:

  • Small batches (32): Noisier nhưng generalize tốt hơn
  • Large batches (256+): Smoother nhưng dễ overfit
  • Powers of 2 (32, 64, 128, 256) để optimize GPU memory

4. Momentum

Vấn đề với GD: Oscillate (dao động) trong narrow valleys.

Loss surface với narrow valley:
      │   ╱│╲
      │  ╱ │ ╲  ← Bounces back and forth
      │ ╱  │  ╲
      │╱   ↓   ╲

Momentum: Tích lũy velocity từ previous updates.

velocity = 0.9 * velocity - learning_rate * gradients
weights = weights + velocity

Analogy: Quả bóng lăn xuống đồi - tích lũy momentum, không dừng ngay ở điểm gồ ghề nhỏ.

β (momentum coefficient): Thường 0.9

  • β = 0: No momentum (vanilla GD)
  • β = 0.9: Giữ 90% velocity cũ
  • β = 0.99: Giữ 99% → very smooth

5. RMSprop (Root Mean Square Propagation)

Vấn đề: Different features có scales khác nhau → cần learning rates khác nhau.

Solution: Adapt learning rate cho mỗi parameter dựa trên magnitude của gradients gần đây.

squared_gradients = 0.9 * squared_gradients + 0.1 * gradients²
weights = weights - learning_rate / sqrt(squared_gradients + ε) * gradients

Intuition:

  • Gradients lớn (feature thay đổi nhiều) → giảm learning rate
  • Gradients nhỏ → tăng learning rate

6. Adam (Adaptive Moment Estimation)

Kết hợp Momentum + RMSprop → Best optimizer hiện tại!

# Momentum (first moment)
m = β1 * m + (1 - β1) * gradients

# RMSprop (second moment)
v = β2 * v + (1 - β2) * gradients²

# Bias correction
m_hat = m / (1 - β1^t)
v_hat = v / (1 - β2^t)

# Update
weights = weights - learning_rate * m_hat / (sqrt(v_hat) + ε)

Hyperparameters (thường dùng):

  • learning_rate = 0.001
  • β1 = 0.9 (momentum)
  • β2 = 0.999 (RMSprop)
  • ε = 1e-8 (stability)

Tại sao Adam là default choice:

  • Adaptive learning rates → ít cần tune
  • Fast convergence
  • Works well across nhiều problems
from tensorflow.keras.optimizers import Adam

model.compile(
    optimizer=Adam(learning_rate=0.001),
    loss='binary_crossentropy',
    metrics=['accuracy']
)

Regularization: Chống Overfitting trong Neural Networks

1. L2 Regularization (Weight Decay)

Add penalty vào loss:

Loss_total = Loss_data + λ * Σ(weights²)

→ Buộc weights nhỏ → model không quá complex.

from tensorflow.keras import regularizers

model.add(Dense(64, 
    activation='relu',
    kernel_regularizer=regularizers.l2(0.001)  # λ = 0.001
))

2. Dropout

Randomly "tắt" neurons trong training.

Training:        Testing:
x₁ ──┐          x₁ ──┐
     ├── h₁         ├── h₁
x₂ ──┤ (ON)    x₂ ──┤ (ON)
     ├── h₂         ├── h₂
x₃   │ (OFF)   x₃ ──┤ (ON)
     └── h₃         └── h₃
        (ON)           (ON)

Tại sao hiệu quả?

  • Buộc network không rely quá nhiều vào một vài neurons
  • Giống như training ensemble of networks
  • Mỗi iteration = different "thinned" network
from tensorflow.keras.layers import Dropout

model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))  # Drop 50% neurons
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))  # Drop 30%

Best practices:

  • Dropout rate: 0.2-0.5 (thường 0.5 cho fully connected, 0.2 cho conv layers)
  • Không dùng dropout ở output layer
  • Chỉ apply trong training, không apply khi inference

3. Batch Normalization

Normalize activations của mỗi layer.

Vấn đề: Internal Covariate Shift - distribution của inputs vào layer thay đổi liên tục khi weights update.

Solution: Normalize về mean=0, std=1 sau mỗi layer.

# For each mini-batch
z = W·x + b
z_normalized = (z - mean(z)) / sqrt(var(z) + ε)
a = γ * z_normalized + β  # γ, β are learnable

Ưu điểm:

  • Train nhanh hơn nhiều
  • Cho phép learning rates cao hơn
  • Giảm sensitivity với weight initialization
  • Có regularization effect (giống dropout)
from tensorflow.keras.layers import BatchNormalization

model.add(Dense(128))
model.add(BatchNormalization())  # Trước activation
model.add(Activation('relu'))

Key Takeaways

  • Perceptron: 1 neuron, chỉ học được linear patterns
  • MLP: Multiple layers → học được non-linear patterns phức tạp
  • Activation Functions:
    • ReLU: Default cho hidden layers (nhanh, hiệu quả)
    • Sigmoid: Binary classification output
    • Softmax: Multi-class classification output
  • Backpropagation: Chain rule để tính gradients, update weights ngược từ output về input
  • Optimizers:
    • SGD: Simple nhưng slow
    • Adam: Default choice (kết hợp Momentum + RMSprop)
  • Regularization:
    • L2: Penalty on large weights
    • Dropout: Randomly disable neurons
    • Batch Normalization: Normalize activations

Best practices:

  • Start với Adam optimizer, learning_rate=0.001
  • Dùng ReLU cho hidden layers
  • Add Dropout (0.3-0.5) để prevent overfitting
  • Monitor train vs validation loss để detect overfitting sớm
  • Use Batch Normalization để train nhanh hơn

Next steps:

  • Implement neural network from scratch để hiểu sâu
  • Experiment với different architectures, hyperparameters
  • Học frameworks: TensorFlow/Keras, PyTorch

Trong bài tiếp theo, chúng ta sẽ khám phá Computer Vision Architectures - các kiến trúc neural networks chuyên biệt cho xử lý ảnh, đặc biệt là CNNs (Convolutional Neural Networks).


Bài viết thuộc series "From Zero to AI Engineer" - Module 5: Deep Learning & Computer Vision