Danh sách bài viết

Bài 4: Forward Propagation — luồng dữ liệu đi qua mạng

Forward propagation — quá trình tính output của neural network từ input đi qua từng layer: phép linear z = Wa + b, activation a = f(z), notation chuẩn, batched form cho GPU, lưu intermediate cho backprop, inference vs training và computation cost.

24/05/2026
13 phút đọc
1 lượt xem
1

Mục tiêu bài học

Bài 3 đã dựng cấu trúc MLP: input layer, hidden layer, output layer, mỗi layer là một stack neuron fully-connected với layer trước. Bài này trả lời câu hỏi cụ thể: cho input x, làm cách nào tính ra prediction ŷ?

Quy trình đó gọi là forward propagation — luồng dữ liệu chảy theo hướng input → output, qua từng layer. Đây là phép tính cơ bản nhất của neural network; mọi thứ khác (loss, backprop, training loop) đều dựng quanh nó.

Sau bài này, bạn sẽ:

  • Viết được công thức 2 bước (linear + activation) cho 1 layer dạng vector.
  • Phát biểu được notation chuẩn \( \mathbf{a}^{(l)}, \mathbf{z}^{(l)}, W^{(l)}, \mathbf{b}^{(l)} \) và biết shape của từng đại lượng.
  • Tính được forward cho MLP 784-128-10 đầu cuối, ra shape sau mỗi layer.
  • Phân biệt forward dạng 1 sample và dạng batched (matrix form) — biết vì sao GPU thích batched.
  • Hiểu vì sao phải lưu \( \mathbf{z}^{(l)}, \mathbf{a}^{(l)} \) khi training và bỏ qua khi inference.
  • Cài đặt forward MLP 2-layer bằng NumPy thuần.
2

Forward propagation là gì

Forward propagation (lan truyền xuôi) là quá trình tính output của một neural network từ input, bằng cách lần lượt áp dụng phép biến đổi của từng layer theo thứ tự — từ layer đầu vào, qua các hidden layer, đến layer ra cuối cùng.

Quy ước đánh số layer: input không tính là layer (hoặc gọi là layer 0). Layer 1, 2, ..., L là các layer có weight. Layer L là output layer.

Mỗi layer làm cùng một việc: nhận activation của layer trước, biến đổi bằng weight và bias riêng, áp activation function, trả ra activation mới cho layer kế tiếp. Toàn mạng là một chuỗi L phép biến đổi nối nhau.

Có ba điểm cần ghi nhớ ngay:

  • Forward là tính, không phải học. Weight và bias đã cố định (lấy từ initialization hoặc từ checkpoint đã train). Forward không thay đổi parameter.
  • Thứ tự bắt buộc tuần tự: muốn tính layer l, phải có activation layer l-1 trước. Không thể nhảy cóc trừ trường hợp residual / skip connection (Module 4).
  • Forward giống nhau ở training và inference về công thức; chỉ khác ở chỗ có lưu intermediate hay không, và có một số layer (Dropout, BatchNorm) hành xử khác giữa hai chế độ.
3

Hai bước cốt lõi mỗi layer

Một fully-connected layer luôn gồm 2 bước. Gọi \( \mathbf{a}^{(l-1)} \) là activation của layer trước (input của layer này), \( W^{(l)}, \mathbf{b}^{(l)} \) là weight và bias của layer l.

Bước 1 — Linear (affine). Tính tổng có trọng số cộng bias:

\[ \mathbf{z}^{(l)} = W^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)} \]

Đây là phép biến đổi affine — matrix-vector multiply + vector add. Output \( \mathbf{z}^{(l)} \) gọi là pre-activation hoặc logit (cho output layer của classifier).

Bước 2 — Activation. Áp một hàm phi tuyến từng phần tử (element-wise):

\[ \mathbf{a}^{(l)} = f^{(l)}(\mathbf{z}^{(l)}) \]

Với f là activation function — ReLU, Sigmoid, Tanh, Softmax... (Bài 5–8). Activation là chỗ duy nhất phi tuyến trong cả layer; bỏ nó đi thì N layer linear xếp chồng tương đương 1 layer linear (Bài 3 đã chứng minh).

Hai bước này, lặp lại từ l = 1 đến L, chính là toàn bộ forward propagation. Mọi kiến trúc fully-connected — MLP, autoencoder fully-connected, encoder/decoder của Transformer (phần FFN) — đều dựng từ đúng 2 bước này.

4

Vì sao gọi là "forward"

Tên "forward" đối lập với "backward". Hai pha này định nghĩa cả vòng training:

  • Forward pass — dữ liệu chảy xuôi từ input đến output. Mỗi node nhận activation từ node trước, tính activation cho mình, gửi tiếp cho node sau. Kết quả cuối là prediction \( \hat{\mathbf{y}} \).
  • Backward pass — gradient của loss chảy ngược từ output về input. Mỗi node nhận gradient từ node sau, tính gradient cho weight của mình và gradient để gửi ngược lên node trước. Đây là backpropagation (Bài 11).

Trong training, hai pha luôn đi cặp: forward để có \( \hat{\mathbf{y}} \) và loss, rồi backward để có gradient — sau đó optimizer cập nhật weight (Bài 17).

Trong inference (predict trên data mới), chỉ chạy forward. Đây là pha "nhanh" và là mặt mà người dùng cuối thấy: model đã train xong, mỗi input một forward pass cho ra output.

Lưu ý: thuật ngữ "forward propagation" và "forward pass" thường dùng thay nhau. Một số tài liệu phân biệt — "forward propagation" nhấn mạnh thuật toán, "forward pass" nhấn mạnh một lượt thực thi trên 1 batch dữ liệu — nhưng trong thực hành chúng chỉ cùng một việc.

5

Notation chính thức

Quy ước notation chuẩn (theo Goodfellow et al., Deep Learning, 2016) — dùng xuyên suốt series:

  • \( L \) — tổng số layer có weight.
  • \( n_l \) — số neuron ở layer l. Quy ước \( n_0 \) = số chiều của input.
  • \( \mathbf{a}^{(0)} = \mathbf{x} \) — input của mạng là activation của "layer 0".
  • \( W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}} \) — ma trận weight layer l. Mỗi hàng là weight của 1 neuron.
  • \( \mathbf{b}^{(l)} \in \mathbb{R}^{n_l} \) — vector bias layer l.
  • \( \mathbf{z}^{(l)} \in \mathbb{R}^{n_l} \) — pre-activation của layer l.
  • \( \mathbf{a}^{(l)} \in \mathbb{R}^{n_l} \) — activation của layer l.
  • \( f^{(l)} \) — activation function của layer l (có thể khác nhau giữa các layer; thường hidden dùng ReLU, output dùng softmax/sigmoid/linear).
  • \( \hat{\mathbf{y}} = \mathbf{a}^{(L)} \) — output cuối, prediction của mạng.

Phương trình forward đầy đủ cho 1 sample:

\[ \mathbf{a}^{(0)} = \mathbf{x}, \qquad \mathbf{z}^{(l)} = W^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}, \qquad \mathbf{a}^{(l)} = f^{(l)}(\mathbf{z}^{(l)}), \quad l = 1, \ldots, L \] \[ \hat{\mathbf{y}} = \mathbf{a}^{(L)} \]

Lưu ý về shape của \( W^{(l)} \): nhiều giáo trình dùng \( W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}} \) (hàng = neuron output, cột = neuron input) như trên. Một số framework (PyTorch nn.Linear, Keras Dense) cũng lưu weight theo convention này. Khi viết batched form (mục 7) sẽ thấy transpose xuất hiện vì batch dimension nằm trục đầu.

6

Step-by-step trên MLP 784-128-10

Để cụ thể, xét MLP phân loại chữ số MNIST: ảnh xám 28×28 flatten thành vector 784 chiều, 1 hidden layer 128 neuron với ReLU, output 10 neuron với softmax (1 neuron / 1 chữ số 0–9).

Tham số: \( L = 2 \), \( n_0 = 784, n_1 = 128, n_2 = 10 \). Weight và bias:

  • \( W^{(1)} \in \mathbb{R}^{128 \times 784} \), \( \mathbf{b}^{(1)} \in \mathbb{R}^{128} \).
  • \( W^{(2)} \in \mathbb{R}^{10 \times 128} \), \( \mathbf{b}^{(2)} \in \mathbb{R}^{10} \).

Forward cho 1 ảnh \( \mathbf{x} \in \mathbb{R}^{784} \):

Layer 1 (hidden, ReLU).

\[ \mathbf{z}^{(1)} = W^{(1)} \mathbf{x} + \mathbf{b}^{(1)} \in \mathbb{R}^{128} \] \[ \mathbf{a}^{(1)} = \mathrm{ReLU}(\mathbf{z}^{(1)}) = \max(\mathbf{0}, \mathbf{z}^{(1)}) \in \mathbb{R}^{128} \]

Layer 2 (output, softmax).

\[ \mathbf{z}^{(2)} = W^{(2)} \mathbf{a}^{(1)} + \mathbf{b}^{(2)} \in \mathbb{R}^{10} \] \[ \mathbf{a}^{(2)} = \mathrm{softmax}(\mathbf{z}^{(2)}) \in \mathbb{R}^{10}, \quad \sum_{k=1}^{10} a^{(2)}_k = 1 \]

Prediction cuối \( \hat{\mathbf{y}} = \mathbf{a}^{(2)} \) là một phân phối xác suất trên 10 class. Predicted label = \( \arg\max_k \hat{y}_k \).

Tổng số tham số: \( 128 \cdot 784 + 128 + 10 \cdot 128 + 10 = 100\,352 + 128 + 1\,280 + 10 = 101\,770 \). Đây cũng là số con số phải lưu khi save model — bằng đúng số phép nhân + cộng trong forward.

7

Batched forward — quan trọng cho GPU

Thực tế không ai forward 1 sample một lần. Lý do: GPU có hàng nghìn core song song, chạy 1 sample lãng phí. Giải pháp: gộp B sample thành một batch và forward đồng thời.

Input batch là ma trận \( X \in \mathbb{R}^{B \times n_0} \) — mỗi hàng là 1 sample. Forward cho layer l:

\[ Z^{(l)} = A^{(l-1)} (W^{(l)})^T + \mathbf{b}^{(l)}, \qquad A^{(l)} = f^{(l)}(Z^{(l)}) \]

Trong đó \( A^{(l)}, Z^{(l)} \in \mathbb{R}^{B \times n_l} \). Phép cộng \( + \mathbf{b}^{(l)} \) là broadcasting — vector bias kích thước \( n_l \) tự nhân lên thành ma trận \( B \times n_l \) (mỗi hàng giống nhau). Series 1 B22 (NumPy broadcasting) đã bàn cơ chế này.

Lý do có transpose \( (W^{(l)})^T \): với convention \( W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}} \), phép single-sample là \( W \mathbf{a} \) (matrix-vector). Khi vector hoá theo batch (hàng = sample), tự nhiên \( A^{(l-1)} W^T \) cho ra shape đúng. Đây là quy ước row-major; với column-major (vector cột) thì không cần transpose.

Vì sao batched fast trên GPU:

  • Matmul — phép \( A W^T \) là matrix-matrix multiply, có thư viện cuBLAS / cuDNN tối ưu cực mạnh trên GPU. Một batch lớn dùng đầy core; batch = 1 phí 99% core.
  • Broadcasting + element-wise activation — vectorized, mỗi GPU thread tính 1 phần tử song song.
  • Memory bandwidth — đọc weight 1 lần, dùng cho cả batch; với batch = 1 phải đọc weight đi đọc lại nếu xử lý nhiều sample tuần tự.

Trong code thực, PyTorch và TensorFlow luôn forward dạng batched — kể cả khi predict 1 ảnh, framework tự thêm batch dimension (x.unsqueeze(0)) để dùng cùng đường nhanh.

8

Storing intermediate — cần cho backprop

Khi training, mỗi forward pass cần lưu lại các đại lượng trung gian để backward dùng. Cụ thể:

  • \( \mathbf{a}^{(l-1)} \) — đầu vào của layer l. Cần để tính gradient của \( W^{(l)} \) (chain rule: \( \partial L / \partial W^{(l)} \) chứa thừa số \( \mathbf{a}^{(l-1)} \)).
  • \( \mathbf{z}^{(l)} \) — pre-activation. Cần để tính \( f'^{(l)}(\mathbf{z}^{(l)}) \) trong backward — đạo hàm của activation function tại điểm \( \mathbf{z}^{(l)} \).

Vì lý do này, RAM tiêu thụ khi training tỉ lệ với batch size, số layerkích thước activation — không chỉ tỉ lệ với số parameter. Đây là điểm hay làm tốn RAM nhất khi train model lớn.

PyTorch tự lưu — autograd engine (Bài 15) tự build một computation graph động khi forward, lưu mọi tensor cần thiết cho backward. Bạn chỉ cần viết forward; loss.backward() sẽ chạy ngược graph tự động.

Inference không cần lưu — vì không backward. PyTorch dùng torch.no_grad() hoặc model.eval() + context manager để tắt việc build graph → tiết kiệm RAM đáng kể, đặc biệt cho model lớn (LLM inference thường tiết kiệm 40–50% RAM nhờ tắt grad).

9

Activation function chọn gì

Mỗi layer chọn f riêng. Quy tắc chung (chi tiết Bài 5–8):

Hidden layer.

  • ReLU — mặc định 2026. Rẻ, hội tụ nhanh, không saturation phía dương. Vấn đề "dying ReLU" có biến thể (LeakyReLU, GELU) giải. Bài 6.
  • Tanh — output \( [-1, 1] \), zero-centered. Thường dùng trong RNN cổ điển. Bài 7.
  • Sigmoid — ngày nay ít dùng cho hidden layer vì gradient vanishing (Bài 5). Vẫn dùng cho gate trong LSTM (Module 5).

Output layer — chọn theo bài toán:

  • Regression (predict số thực): linear — không activation, \( \mathbf{a}^{(L)} = \mathbf{z}^{(L)} \). Loss đi kèm: MSE / MAE.
  • Binary classification: sigmoid — 1 neuron, output \( \in (0, 1) \) là xác suất class dương. Loss: Binary Cross-Entropy.
  • Multi-class classification: softmaxK neuron, output là phân phối xác suất tổng = 1. Loss: Categorical Cross-Entropy. Bài 8 và Bài 10.
  • Multi-label classification (nhiều label độc lập): sigmoid theo từng output — không softmax, vì các class không loại trừ nhau.

Chọn sai activation output layer là bug phổ biến: regression mà bỏ sigmoid vào → output bị nén về \( [0, 1] \), không bao giờ predict được số ngoài khoảng. Multi-class mà dùng sigmoid thay softmax → xác suất không cộng được thành 1.

10

Inference vs Training

Cùng một network, hai chế độ chạy. Bảng so sánh:

  • Training mode — gồm forward + backward + cập nhật weight. Forward phải lưu intermediate. Dropout active (drop ngẫu nhiên neuron). BatchNorm tính mean/var trên batch hiện tại. Eval mỗi epoch để theo dõi.
  • Inference mode — chỉ forward, không lưu intermediate. Dropout tắt (không drop neuron — dùng đầy đủ). BatchNorm dùng running stats (mean/var trung bình tích luỹ trong training), không tính lại trên batch. Tốc độ nhanh hơn, RAM thấp hơn.

PyTorch có hai mechanism độc lập, dễ nhầm:

  • model.train() / model.eval() — set mode. Ảnh hưởng Dropout, BatchNorm, LayerNorm-style layer. Không ảnh hưởng tính gradient.
  • torch.no_grad() / torch.inference_mode() — tắt việc build computation graph. Tiết kiệm RAM, không liên quan tới hành vi của layer.

Pattern chuẩn khi predict trên test set:

model.eval()
with torch.no_grad():
    for x, y in test_loader:
        y_hat = model(x)
        # ... tính metric

Quên model.eval() trên model có Dropout / BatchNorm → metric trên test set thấp hơn thật vì Dropout vẫn drop và BatchNorm tính lại stat trên batch test. Đây là bug "vô hình" rất phổ biến với người mới.

11

Forward computation cost

Đếm phép tính. Với batch size B, layer l có \( n_{l-1} \) input và \( n_l \) output:

  • Linear: \( Z^{(l)} = A^{(l-1)} (W^{(l)})^T + \mathbf{b}^{(l)} \). Số phép multiply-add (FLOP) \( \approx 2 B \cdot n_{l-1} \cdot n_l \). Đây là phần nặng nhất.
  • Activation: element-wise, \( O(B \cdot n_l) \) phép. Rẻ so với matmul.
  • Bias add: broadcasting, \( O(B \cdot n_l) \) phép. Rẻ.

Tổng cho L layer:

\[ \text{FLOPs}_{\text{forward}} \approx 2 B \sum_{l=1}^{L} n_{l-1} \cdot n_l \]

Ví dụ MLP 784-128-10, batch B = 32:

  • Layer 1: \( 2 \cdot 32 \cdot 784 \cdot 128 \approx 6{,}4 \cdot 10^6 \) FLOP.
  • Layer 2: \( 2 \cdot 32 \cdot 128 \cdot 10 \approx 8{,}2 \cdot 10^4 \) FLOP.
  • Tổng \( \approx 6{,}5 \cdot 10^6 \) FLOP / 1 batch.

So sánh: GPU consumer (RTX 4070) có \( \sim 30 \) TFLOPs FP32 — về lý thuyết chạy được \( 30 \cdot 10^{12} / 6{,}5 \cdot 10^6 \approx 5 \cdot 10^6 \) batch / giây. Thực tế thấp hơn nhiều do data loading, memory bandwidth, kernel launch overhead. Nhưng nguyên tắc đúng: matmul thống trị forward cost; mọi tối ưu performance đều xoay quanh giảm matmul hoặc làm nó chạy nhanh hơn (quantization, sparse, kernel fusion).

Backward thường tốn gấp 2× forward (cần tính 2 gradient cho mỗi matmul). Toàn bộ training step: \( \sim 3 \times \) chi phí của forward thuần.

12

Computation graph

Để backward chạy được, framework biểu diễn forward dưới dạng computation graph — directed acyclic graph (DAG):

  • Node — một operation cơ bản: matmul, add, ReLU, softmax, log...
  • Edge — luồng tensor giữa các op. Cạnh từ A đến B nghĩa là output của A là input của B.
  • Leaf node — tensor không có parent: input \( \mathbf{x} \), weight \( W^{(l)} \), bias \( \mathbf{b}^{(l)} \).
  • Root node — output cuối: loss scalar.

MLP 2-layer của mục 6 có graph:

x --→ matmul --→ + b1 --→ ReLU --→ matmul --→ + b2 --→ softmax --→ ŷ
W1 --↗                              W2 --↗
b1 -------------↗                   b2 -------------↗

Khi viết forward bằng PyTorch, autograd ghi lại graph "động" — mỗi tensor kết quả nhớ function đã sinh ra nó (qua attribute .grad_fn) và parent của nó. Khi gọi loss.backward(), autograd duyệt graph ngược từ loss, áp chain rule tại mỗi node, lấp gradient vào .grad của các leaf node là weight và bias.

TensorFlow 1.x dùng graph tĩnh (define-then-run); TensorFlow 2 và PyTorch dùng graph động (define-by-run). Graph động dễ debug và viết hơn — đây là một trong các lý do PyTorch chiếm thị phần ML research (Bài 12).

Bài học: forward không chỉ "tính output" — nó đồng thời build computation graph để chuẩn bị cho backward. Đó là cơ chế đằng sau .backward() "kỳ diệu".

13

Common bug

Bốn bug phổ biến khi viết forward — nhận diện sớm tiết kiệm hàng giờ debug:

  • Shape mismatch — nhân ma trận sai chiều. Triệu chứng: RuntimeError: size mismatch, m1: [B x 784], m2: [128 x 784]. Nguyên nhân: quên transpose, hoặc \( W \) shape không khớp \( n_l, n_{l-1} \). Cách phòng: in print(x.shape) ở mỗi bước khi mới viết model; assert shape ở entry point.
  • Bỏ activation cuối khi cần — classifier không có softmax / sigmoid → output là logit, không phải xác suất. Predict đúng (nhờ argmax) nhưng confidence vô nghĩa và loss tính sai nếu dùng nhầm function. Lưu ý: PyTorch nn.CrossEntropyLoss đã bao gồm softmax bên trong, nên model nên trả logit thẳng — đây là exception khiến nhiều người nhầm.
  • Áp activation cuối khi không cần — regression mà thêm sigmoid → output bị nén \( (0, 1) \), train rất chậm hoặc không train được. Quên đổi từ classifier template sang regression.
  • Quên flatten — ảnh shape \( (B, 1, 28, 28) \) feed thẳng vào nn.Linear(784, ...) → shape mismatch. Phải x.view(B, -1) hoặc nn.Flatten() trước.
  • Trộn batch và sample axis — viết forward giả định 1 sample (\( W \mathbf{a} \)) nhưng feed batch \( (B \times n) \). Hoặc ngược lại. Hệ quả: matmul vẫn chạy nhưng tính sai về mặt ngữ nghĩa, không có error message → cực khó debug. Cách phòng: chọn convention (luôn batched, batch là axis 0) và stick với nó.
14

Code Python — forward MLP 2-layer bằng NumPy

Cài đặt forward MLP 784-128-10 bằng NumPy thuần — thấy rõ công thức biến thành code. Không PyTorch, không autograd; chỉ matmul, broadcasting và một vài hàm element-wise.

import numpy as np


def relu(z):
    """ReLU element-wise."""
    return np.maximum(0.0, z)


def softmax(z):
    """Softmax theo axis cuối, có trick numerical stability."""
    z_shift = z - z.max(axis=-1, keepdims=True)   # tránh exp tràn số
    exp = np.exp(z_shift)
    return exp / exp.sum(axis=-1, keepdims=True)


def forward(X, W1, b1, W2, b2):
    """
    Forward MLP 2-layer cho batch.

    X:  (B, 784)
    W1: (128, 784)   b1: (128,)
    W2: (10, 128)    b2: (10,)
    Tra ve:
      logits: (B, 10) — truoc softmax
      y_hat:  (B, 10) — sau softmax (xac suat)
    """
    Z1 = X @ W1.T + b1          # (B, 128)
    A1 = relu(Z1)               # (B, 128)
    Z2 = A1 @ W2.T + b2         # (B, 10)
    y_hat = softmax(Z2)         # (B, 10)
    return Z2, y_hat            # tra Z2 (logit) de dung voi cross-entropy stable


# Khoi tao random — bai 20 se ban Xavier / He
rng = np.random.default_rng(0)
W1 = rng.standard_normal((128, 784)) * 0.01
b1 = np.zeros(128)
W2 = rng.standard_normal((10, 128)) * 0.01
b2 = np.zeros(10)

# Batch gia: 4 sample, moi sample 784 chieu (gia lap MNIST flatten)
X = rng.standard_normal((4, 784))

logits, y_hat = forward(X, W1, b1, W2, b2)
print(f"logits.shape = {logits.shape}")   # (4, 10)
print(f"y_hat.shape  = {y_hat.shape}")    # (4, 10)
print(f"y_hat[0] sum = {y_hat[0].sum():.6f}")   # = 1.0 (softmax)
print(f"predicted label sample 0 = {y_hat[0].argmax()}")

Vài điểm đáng chú ý trong code này:

  • X @ W1.T + b1 — đúng dạng batched form ở mục 7. Broadcasting cộng vector b1 (shape (128,)) vào ma trận (B, 128) tự nhiên.
  • softmax trừ max trước khi exp — trick numerical stability. Nếu \( z \) chứa số \( \sim 1000 \), np.exp(1000) sẽ overflow thành inf; trừ max thì giá trị lớn nhất sau dịch là 0, mọi exp trong khoảng \( (0, 1] \).
  • Hàm trả về cả logits (pre-softmax) lẫn y_hat — vì khi tính cross-entropy stable, người ta dùng thẳng logit và combine với log-softmax thay vì compute softmax rồi log (sẽ bàn ở Bài 10).
  • Không có loop trên sample — toàn bộ batch chạy qua 1 matmul cho mỗi layer. Đây chính là vectorization NumPy / GPU thích.

Tương đương trong PyTorch (Bài 16 sẽ vào sâu):

import torch
import torch.nn as nn
import torch.nn.functional as F


class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):           # x: (B, 784)
        z1 = self.fc1(x)            # (B, 128)
        a1 = F.relu(z1)
        z2 = self.fc2(a1)           # (B, 10) — logit
        return z2                   # softmax do CrossEntropyLoss lo

Cùng một forward, chỉ ngắn hơn nhờ nn.Linear đóng gói \( W, \mathbf{b} \) và forward được autograd theo dõi.

15

Quan hệ với matrix multiplication

Forward propagation rút gọn lại chỉ là chuỗi 3 phép tính lặp đi lặp lại:

  • Matrix multiply — \( Z = A W^T \). Là phép tốn nhất, là backbone DL.
  • Broadcast add — \( + \mathbf{b} \). Element-wise, rẻ.
  • Element-wise activation — \( f(Z) \). Element-wise, rẻ.

Series 1 (Toán nền tảng AI Engineer) B19 đã bàn matrix multiplication như phép tổ hợp tuyến tính các cột — forward của một layer chính xác là thao tác đó áp lên batch sample. Mỗi cột của \( W^T \) là weight của 1 neuron output; phép \( A W^T \) gửi mỗi sample đi qua tất cả neuron.

Tất cả tối ưu hardware DL — TPU, Tensor Core của NVIDIA (RTX 30/40 series, H100), Apple Neural Engine — đều thiết kế quanh phép matmul. Một accelerator AI hiện đại đơn giản là "máy chạy matmul nhanh + memory pipeline kèm". Hiểu forward = hiểu vì sao phần cứng AI có hình dạng như vậy.

Hệ quả thực hành: muốn forward nhanh hơn → giảm kích thước matmul (model nhỏ, quantize từ FP32 xuống INT8/FP16), hoặc làm sparse (Mixture of Experts, pruning), hoặc tận dụng kernel fusion (kết hợp matmul + bias + activation thành 1 GPU kernel — đây là điểm mạnh của TensorRT, XLA, torch.compile).

16

Bài tập thực hành

Bài 1 — Forward XOR. XOR là dataset 4 sample: \( (0,0)\to 0, (0,1)\to 1, (1,0)\to 1, (1,1)\to 0 \). Dựng MLP 2-2-1 (2 input, 2 hidden, 1 output) với weight và bias đặt sẵn:

import numpy as np

W1 = np.array([[1.0, 1.0],
               [1.0, 1.0]])
b1 = np.array([0.0, -1.0])

W2 = np.array([[1.0, -2.0]])
b2 = np.array([0.0])

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)

Yêu cầu: implement forward(X, W1, b1, W2, b2) với hidden ReLU và output sigmoid (\( \sigma(z) = 1/(1+e^{-z}) \)). In ra \( Z_1, A_1, Z_2, A_2 \) cho cả 4 sample. Kiểm tra: \( A_2 \) có gần với \( [0, 1, 1, 0] \) không?

Bài 2 — Tính shape. Cho MLP 784-128-64-10 với batch size 32. Liệt kê shape của \( A^{(0)}, Z^{(1)}, A^{(1)}, Z^{(2)}, A^{(2)}, Z^{(3)}, A^{(3)} \) và shape của \( W^{(1)}, W^{(2)}, W^{(3)} \). Tính tổng số parameter (weight + bias) toàn mạng.

Bài 3 — Forward 1 sample, in từng bước. Cho MLP 3-4-2 với ReLU hidden và softmax output. Khởi tạo weight ngẫu nhiên với np.random.default_rng(42). Forward 1 sample \( \mathbf{x} = [1, 2, 3] \) và in ra \( \mathbf{z}^{(1)}, \mathbf{a}^{(1)}, \mathbf{z}^{(2)}, \mathbf{a}^{(2)} \). Kiểm tra: \( \mathbf{a}^{(2)} \) có tổng = 1 không? Có nằm trong \( (0, 1) \) không?

Bài 4 — Đo thời gian batched vs loop. Tạo MLP 1000-500-100 với weight ngẫu nhiên. Forward 1024 sample bằng 2 cách: (a) loop từng sample qua hàm forward 1-sample; (b) 1 lần gọi forward batched. Đo thời gian bằng time.perf_counter(). Tỷ lệ chênh là bao nhiêu? Kết luận về tầm quan trọng của batched form.

Bài 5 — Verify với PyTorch. Lấy W1, b1, W2, b2 từ Bài 1, gán vào nn.Linear tương ứng (chú ý: nn.Linear lưu weight shape (out, in) — giống convention bài này; bias shape (out,)). Forward cùng input. Output PyTorch có bằng output NumPy không? Sai khác là bao nhiêu?

Gợi ý đáp án Bài 2: \( A^{(0)} \in \mathbb{R}^{32 \times 784} \); \( W^{(1)} \in \mathbb{R}^{128 \times 784} \), \( Z^{(1)}, A^{(1)} \in \mathbb{R}^{32 \times 128} \); \( W^{(2)} \in \mathbb{R}^{64 \times 128} \), \( Z^{(2)}, A^{(2)} \in \mathbb{R}^{32 \times 64} \); \( W^{(3)} \in \mathbb{R}^{10 \times 64} \), \( Z^{(3)}, A^{(3)} \in \mathbb{R}^{32 \times 10} \). Parameter: \( (128 \cdot 784 + 128) + (64 \cdot 128 + 64) + (10 \cdot 64 + 10) = 100\,480 + 8\,256 + 650 = 109\,386 \).

17

Bài tiếp theo

Bài 5: Activation function — Sigmoid và vấn đề saturation — Bài này đã giả định "có activation function" và nhắc sơ ReLU/Sigmoid/Tanh. Bài 5 mở đầu chuỗi 4 bài về activation, bắt đầu với sigmoid: công thức, đạo hàm, vùng saturation và vì sao ngày nay không còn là lựa chọn mặc định cho hidden layer.