Chào mừng bạn quay trở lại với lộ trình Kỹ Sư AI Thực Chiến. Ở Bài 1, chúng ta đã bắt nhịp thành công sang môi trường lập trình Python. Tuy nhiên, trước khi có thể huấn luyện những mô hình học sâu phức tạp như Multi-Layer Perceptron (MLP), CNN hay Transformer, chúng ta bắt buộc phải hiểu được ngôn ngữ giao tiếp chung của thế giới AI: Đại số Tuyến tính (Linear Algebra)Giải tích (Calculus).

Đối với lập trình viên Web chưa có chuyên môn về toán cao cấp, các công thức toán học học thuật trên trường lớp thường rất khô khan và khó hình dung. Bài học này được thiết kế để thay đổi hoàn toàn điều đó: chúng ta sẽ đi từ bản chất hình học trực quan nhất, sau đó tự tay hiện thực hóa các phép nhân ma trận và vòng lặp tính đạo hàm bằng mã nguồn Python thô để hiểu cặn kẽ cách máy tính "học" dữ liệu.

2.1 Bản chất hình học của Vector & Ma trận

Hãy bắt đầu bằng cách số hóa thế giới thực. Trong khoa học máy tính, mọi loại dữ liệu đầu vào (một bức ảnh, một file âm thanh, một bài viết) đều được biểu diễn dưới dạng mảng số. Trong toán học, ta gọi các mảng số đó là VectorMa trận (Matrix).

  • Vector: Là một mảng số một chiều. Dưới lăng kính lập trình, một vector 3 chiều chỉ đơn giản là một list có 3 phần tử: v = [x_1, x_2, x_3]. Về mặt hình học, một vector đại diện cho một mũi tên chỉ từ gốc tọa độ $O(0, 0, 0)$ đến điểm có tọa độ tương ứng trong không gian.
  • Ma trận: Là một mảng số hai chiều (dạng bảng gồm dòng và cột). Ví dụ, một ma trận kích thước $2 \times 3$ (2 dòng, 3 cột) được biểu diễn trong code Python là một danh sách chứa các danh sách con: A = [[1, 2, 3], [4, 5, 6]].

Bản chất hình học cốt lõi của một ma trận là gì? Đừng chỉ coi ma trận là những ô lưu trữ số tĩnh giống như bảng Excel. Trong học máy, ma trận đại diện cho một phép biến đổi không gian tuyến tính (Linear Transformation). Khi ta nhân một ma trận $A$ với một vector đầu vào $x$, ta đang thực hiện xoay, kéo dãn, hoặc nén vector đó trong không gian để tạo ra một vector đầu ra mới $y = Ax$:

$$y = Ax = \begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix}$$

Đây chính là nguyên lý cốt lõi của một tầng nơ-ron nhân tạo: nhận tín hiệu đầu vào $x$, "uốn nắn" không mảng số đó là VectorMa trận (Matrix).

Về mặt hình học, một vector $X = [x_1, x_2]$ đại diện cho một điểm hoặc một mũi tên chỉ hướng xuất phát từ gốc tọa độ đến điểm đó trong không gian hai chiều. Số lượng phần tử trong vector quyết định số chiều (Dimensionality) của không gian.

Một ma trận $W$ không đơn thuần là một bảng số tĩnh. Trong học sâu, ma trận đại diện cho một Phép biến đổi tuyến tính (Linear Transformation). Khi ta nhân một ma trận $W$ với một vector $X$, chúng ta đang biến đổi không gian (quay, co giãn hoặc chiếu) để đưa vector $X$ từ không gian ban đầu sang một không gian biểu diễn mới: $$Y = W \cdot X$$ Các trọng số (weights) của mạng nơ-ron chính là các tham số của phép biến đổi không gian này, giúp bóc tách và uốn cong dữ liệu để dễ dàng phân loại.

🧠 Tích vô hướng (Dot Product) — Nền tảng của Cơ chế Attention
Phép toán tích vô hướng giữa hai vector $A$ và $B$ có định nghĩa đại số là tổng tích các phần tử tương ứng: $$A \cdot B = \sum_{i=1}^n a_i b_i$$ Và định nghĩa hình học là: $$A \cdot B = \|A\| \|B\| \cos(\theta)$$ Trong đó $\|A\|$ là độ dài của vector, còn $\cos(\theta)$ là góc giữa hai vector.

Về mặt ý nghĩa, tích vô hướng đo lường độ tương đồng (Similarity) về hướng giữa hai vector:
  • Nếu hai vector cùng hướng ($\theta = 0^\circ$), $\cos(0) = 1$, tích vô hướng đạt giá trị lớn nhất.
  • Nếu hai vector vuông góc ($\theta = 90^\circ$), $\cos(90) = 0$, tích vô hướng bằng 0 (hoàn toàn độc lập).
  • Nếu hai vector ngược hướng ($\theta = 180^\circ$), tích vô hướng mang giá trị âm lớn nhất.
Đây chính là nền tảng toán học cốt lõi của thước đo khoảng cách Cosine Similarity và cơ chế Attention (Chú ý) trong các mô hình Transformer (GPT, BERT) nhằm tính toán xem một từ liên quan mật thiết đến từ nào khác trong câu.

2.2 Giải mã phép nhân ma trận (Matrix Multiplication)

Để nhân hai ma trận $A$ và $B$, chúng ta thực hiện nhân từng dòng của ma trận bên trái với từng cột của ma trận bên phải. Điều này đặt ra một điều kiện ghép chiều nghiêm ngặt: Số cột của ma trận bên trái bắt buộc phải bằng số dòng của ma trận bên phải.

Nếu ma trận $A$ có kích thước $m \times n$ và ma trận $B$ có kích thước $n \times p$, ma trận kết quả $C = A \cdot B$ sẽ có kích thước $m \times p$. Mỗi phần tử $C_{ij}$ được tính bằng công thức: $$C_{ij} = \sum_{k=1}^n A_{ik} B_{kj}$$

Nếu viết bằng ngôn ngữ lập trình cơ bản sử dụng các vòng lặp for lồng nhau, chúng ta sẽ cần tới 3 vòng lặp để thực hiện phép toán này:

matrix_multiply.py
def naive_matrix_multiply(A, B):
    # Lấy số lượng dòng và cột
    m, n = len(A), len(A[0])
    n_B, p = len(B), len(B[0])
    
    # Kiểm tra tính tương thích kích thước
    if n != n_B:
        raise ValueError("Kích thước ma trận không tương thích để nhân!")
        
    # Khởi tạo ma trận kết quả toàn số 0
    C = [[0.0 for _ in range(p)] for _ in range(m)]
    
    # 3 vòng lặp for lồng nhau (Độ phức tạp O(N^3))
    for i in range(m):
        for j in range(p):
            for k in range(n):
                C[i][j] += A[i][k] * B[k][j]
    return C

# Chạy thử nghiệm
A = [[1, 2], [3, 4]] # 2x2
B = [[5, 6], [7, 8]] # 2x2
print("Kết quả nhân ma trận thô:", naive_matrix_multiply(A, B))
# Đầu ra: [[19, 22], [43, 50]]
🧠 Tại sao NumPy nhân ma trận nhanh hơn vòng lặp Python hàng trăm lần?
Thuật toán 3 vòng lặp lồng nhau ở trên có độ phức tạp thời gian là $\mathcal{O}(N^3)$. Khi xử lý các ma trận lớn trong Deep Learning (ví dụ kích thước $1000 \times 1000$), CPU sẽ phải thực hiện $10^9$ phép toán. Chạy bằng vòng lặp Python thô sẽ cực kỳ chậm vì Python là ngôn ngữ động, tốn thời gian kiểm tra kiểu dữ liệu của từng phần tử ở mỗi bước lặp.

Các thư viện tính toán khoa học như NumPy hay PyTorch không sử dụng vòng lặp Python. Chúng đẩy tính toán xuống các thư viện mã máy C/Fortran được tối ưu cực hạn (như BLAS - Basic Linear Algebra Subprograms, LAPACK). Các thư viện này sử dụng thuật toán tối ưu hóa bộ nhớ đệm Cache Locality (chia ma trận lớn thành các khối nhỏ vừa với cache L1/L2 của CPU để tránh trễ truy xuất RAM) và thuật toán phân rã phức tạp như Strassen ($\mathcal{O}(N^{2.807})$) để tăng tốc độ tính toán gấp hàng trăm, hàng ngàn lần.
⚠️ Cạm bẫy: Phép nhân từng phần tử (*) vs Phép nhân ma trận (@)
Trong Python và đặc biệt là khi làm việc với thư viện NumPy hay PyTorch:
  • Phép toán A * B đại diện cho Element-wise Multiplication (nhân từng phần tử cùng tọa độ với nhau, yêu cầu ma trận cùng kích thước).
  • Phép toán A @ B mới là phép nhân ma trận Matrix Product dòng-nhân-cột chuẩn toán học. Hãy cực kỳ lưu ý điểm này để tránh lỗi tính toán sai lệch mô hình cực kỳ khó gỡ.

Trên thực tế, lỗi ghép chiều sai (mục đã nói ở trên) là lỗi phổ biến bậc nhất khi mới viết code AI — NumPy sẽ ném ra một thông báo lỗi cụ thể, và biết cách ĐỌC đúng thông báo đó quan trọng hơn nhiều so với việc học thuộc quy tắc:

shape_mismatch_debug.py
import numpy as np

A = np.array([[1, 2, 3], [4, 5, 6]])   # shape (2, 3)
B = np.array([[1, 2, 3], [4, 5, 6]])   # shape (2, 3) — CÙNG shape với A, không ghép được

try:
    C = A @ B
except ValueError as e:
    print("Lỗi:", e)
    # Lỗi: matmul: Input operand 1 has a mismatch in its core dimension 0
    #      (size 2 is different from 3)
    # Đọc lỗi: "operand 1" = B, "core dimension 0" = chiều dòng của B (2) không
    # khớp với chiều CỘT của A (3) — đúng quy tắc "cột A = dòng B" đã học ở trên.

# ✅ Cách sửa phổ biến nhất: dùng .T để chuyển vị B, biến (2,3) thành (3,2)
C = A @ B.T   # (2,3) @ (3,2) -> (2,2), hợp lệ
print("Kết quả sau khi chuyển vị B:\n", C)

Mẹo debug thực chiến: mỗi khi gặp ValueError về matmul, hãy in ra .shape của cả hai mảng TRƯỚC dòng gây lỗi (print(A.shape, B.shape)) — 90% trường hợp bạn sẽ thấy ngay chiều nào đang lệch mà không cần đọc kỹ thông báo lỗi dài dòng của NumPy.

2.3 Đạo hàm (Derivatives) — La bàn chỉ hướng giảm sai số

Nếu như Đại số Tuyến tính giúp ta biểu diễn cấu trúc mạng nơ-ron, thì Giải tích (đạo hàm) lại là cơ chế giúp mạng nơ-ron có khả năng "học hỏi" từ sai số.

Về mặt hình học, đạo hàm của hàm số $f(x)$ tại một điểm đại diện cho độ dốc (hệ số góc) của đường tiếp tuyến của đồ thị hàm số đó tại điểm đó. Đạo hàm cho ta biết hàm số đang tăng hay giảm nhanh như thế nào nếu ta dịch chuyển biến đầu vào $x$ đi một lượng cực kỳ nhỏ.

Công thức giới hạn vi phân lý thuyết của đạo hàm:

$$f'(x) = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}$$

Trong thế giới lập trình, máy tính không thể tính giới hạn toán học trực tiếp. Tuy nhiên, ta có thể tính xấp xỉ đạo hàm thông qua phương pháp Sai phân hữu hạn (Finite Difference) bằng cách chọn một bước dịch chuyển $h$ đủ nhỏ (thường là $10^{-5}$):

derivative.py
def f(x):
    return x**2  # Ví dụ hàm f(x) = x^2

def numerical_derivative(func, x, h=1e-5):
    # Tính xấp xỉ độ dốc
    return (func(x + h) - func(x)) / h

# Tính đạo hàm số học tại x = 3
slope = numerical_derivative(f, 3)
print(f"Đạo hàm xấp xỉ tại x=3: {slope:.5f}") # Kết quả xấp xỉ: 6.00001 (đạo hàm giải tích lý thuyết là 2x = 6)

Khi chúng ta làm việc với hàm nhiều biến đầu vào (ví dụ hàm Loss phụ thuộc vào hàng triệu trọng số $W$), đạo hàm của hàm số theo từng biến số riêng lẻ được gọi là Đạo hàm riêng (Partial Derivative), ký hiệu là $\frac{\partial f}{\partial w_i}$.

Tập hợp tất cả các đạo hàm riêng này thành một vector được gọi là Gradient Vector, ký hiệu là $\nabla f$ (đọc là nabla f): $$\nabla f = \left[ \frac{\partial f}{\partial w_1}, \frac{\partial f}{\partial w_2}, \dots, \frac{\partial f}{\partial w_n} \right]$$

Về mặt hình học, Gradient luôn chỉ hướng dốc lên nhanh nhất của hàm số tại điểm đó. Do đó, để tìm cực tiểu của hàm sai số (đáy thung lũng), chúng ta phải đi theo hướng ngược lại với chiều của vector Gradient, tức là hướng $-\nabla f$. Đây chính là triết lý cốt lõi của thuật toán tối ưu hóa huyền thoại Gradient Descent (Cực tiểu hóa độ dốc).

2.4 Dự án thực hành bài 2: Tự viết thuật toán Gradient Descent

Dự án thực hành của bài học này là tự viết thuật toán Gradient Descent hoàn chỉnh để tìm cực tiểu của hàm số sai số lồi: $$f(x) = x^2 - 4x + 4$$ Bằng công thức toán học, ta dễ dàng biết được hàm số này có thể viết lại thành $f(x) = (x-2)^2$, đạt cực tiểu bằng 0 tại điểm $x = 2$.

Chúng ta sẽ phỏng đoán ngẫu nhiên điểm xuất phát cực kỳ xa, ví dụ $x_{\text{init}} = 10.0$, và bắt CPU chạy vòng lặp cập nhật để tự "bò" về điểm tối ưu $x=2$ theo công thức: $$x_{\text{new}} = x_{\text{old}} - \eta \cdot f'(x_{\text{old}})$$ Trong đó $\eta$ (đọc là eta) là hệ số học (learning rate), điều khiển độ lớn của mỗi bước chân xuống dốc.

gradient_descent.py
# Định nghĩa hàm số lỗi và đạo hàm xấp xỉ
def f(x):
    return x**2 - 4*x + 4

def get_gradient(x, h=1e-5):
    return (f(x + h) - f(x)) / h

# Siêu tham số tối ưu hóa
x = 10.0             # Điểm xuất phát ngẫu nhiên
learning_rate = 0.1  # Hệ số học (eta)
epochs = 50          # Số lần lặp tối đa
tolerance = 1e-6     # Ngưỡng dừng sớm

for epoch in range(1, epochs + 1):
    grad = get_gradient(x)
    
    # Bước đi ngược chiều đạo hàm để giảm sai số
    x_new = x - learning_rate * grad
    
    # Kiểm tra điều kiện dừng nếu x thay đổi cực nhỏ
    if abs(x_new - x) < tolerance:
        print(f"Hội tụ sớm tại vòng lặp {epoch}! x = {x_new:.6f}")
        x = x_new
        break
        
    x = x_new
    print(f"Vòng lặp {epoch:02d}: x = {x:.6f} | Loss = {f(x):.6f}")

print(f"\nKết quả tối ưu tìm được: x = {x:.6f} | Giá trị cực tiểu: f(x) = {f(x):.6f}")
💡 Tầm quan trọng của Hệ số học (Learning Rate)
Hệ số học $\eta$ quyết định kích thước bước chân cập nhật của thuật toán:
  • Nếu chọn $\eta$ quá nhỏ (ví dụ 0.0001): Thuật toán bước đi cực kỳ chậm chạp, ngốn rất nhiều tài nguyên tính toán để hội tụ về đích.
  • Nếu chọn $\eta$ quá lớn (ví dụ 1.5): Thuật toán sẽ bước những bước quá dài, vượt qua cả đáy đồ thị (overshoot) và dao động liên tục dẫn đến phân kỳ (bị lỗi nổ giá trị).

Để thấy rõ MỐI QUAN HỆ giữa dấu của đạo hàm và hướng cập nhật $x$ (điều dễ gây nhầm lẫn nhất khi mới học), hãy lần theo tay 3 vòng lặp đầu tiên với $x_{\text{init}} = 10$, $\eta = 0.1$:

Vòng lặp $x$ hiện tại $f'(x) = 2x - 4$ Dấu đạo hàm $x_{\text{new}} = x - \eta \cdot f'(x)$
1 10.0 16 Dương (+) → đồ thị đang đi LÊN 10 − 0.1×16 = 8.4 (giảm)
2 8.4 12.8 Dương (+) → vẫn đang đi LÊN 8.4 − 0.1×12.8 = 7.12 (giảm)
3 7.12 10.24 Dương (+) → vẫn đang đi LÊN 7.12 − 0.1×10.24 = 6.096 (giảm)

Quy tắc bất biến xuyên suốt: đạo hàm dương → $x$ phải GIẢM để đi xuống dốc (vì trừ đi 1 số dương); ngược lại nếu đạo hàm ÂM (đang ở phía bên trái điểm cực tiểu $x=2$), phép trừ số âm sẽ làm $x$ TĂNG lên — thuật toán luôn tự động "bò" đúng hướng về điểm cực tiểu $x=2$ mà không cần biết trước đáp án, chỉ dựa vào dấu của đạo hàm tại từng bước.

Tóm tắt bài học & Cầu nối kiến thức

🔑 Bài học đạt được:
  • Đạt được: Hiểu rõ bản chất hình học của Vector, Ma trận và phép nhân ma trận trong không gian đa chiều.
  • Đạt được: Làm chủ khái niệm Đạo hàm (Derivatives) và Gradient Descent - chìa khóa vạn năng giúp các thuật toán AI tự học cách làm mịn sai số.

Cầu nối bài tiếp theo: Để tăng hiệu năng tính toán ma trận lên hàng triệu phép tính mỗi giây mà không bị nghẽn CPU, chúng ta cần học cách song song hóa dữ liệu với hai thư viện cốt lõi NumPy và Pandas ở Bài số 3.

Tải file code thực hành minh họa bài học

File Python gradient_descent.py — mã nguồn thực hành chạy vòng lặp tìm cực tiểu của hàm số bằng Gradient Descent thô tự tính đạo hàm (chạy python gradient_descent.py, không cần cài đặt thêm thư viện ngoài):

Tải về gradient_descent.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 1: Chuyển dịch tư duy lập trình: Từ JS sang Python cho AI Bài 3: Làm việc với Dữ liệu lớn: NumPy & Pandas chuyên sâu Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận