Skip to content
← All writing

Toán nền thực sự cần cho Machine Learning

Không phải một giáo trình. Vài ý cứ quay đi quay lại trong code, mỗi ý ở đó để làm gì, và phần nào trong sách giáo khoa có thể bỏ qua cho tới khi thực sự cần.

August 202615 min readmathmachine-learning

Mọi danh sách "toán cho ML" đều là ba môn giống nhau: đại số tuyến tính, giải tích, xác suất. Mỗi môn một học kỳ. Cộng lại là một năm bạn không có, và phần lớn nội dung trong đó bạn sẽ không bao giờ dùng tới.

Phiên bản hữu ích của câu hỏi thì hẹp hơn. Những ý nào cứ xuất hiện đi xuất hiện lại trong code, và biết chúng cho phép bạn làm được gì mà không biết thì không làm được? Danh sách đó ngắn. Khoảng sáu ý, và không ý nào khó. Cái khiến chúng đáng học cho tử tế là chúng biến những lời gọi thư viện từ câu thần chú thành thứ có lý do.

Nếu bạn học kỹ sư thì đã gặp hết rồi. Mình học xây dựng, và mọi thứ dưới đây đều có trong các môn ấy — ma trận cho phân tích kết cấu, đạo hàm cho mọi thứ, xác suất cho hệ số tải. Cái thay đổi không phải bản thân toán. Mà là nó chĩa vào đâu.

Ma trận là một hàm, không phải bảng số

Đây là cách nhìn lại duy nhất khiến đại số tuyến tính khớp vào ML.

Ở trường, ma trận là một bảng mà bạn học quy tắc nhân. Trong ML, ma trận là một ánh xạ tuyến tính: một hàm nhận vector vào và trả vector ra. Nhân hai ma trận không phải thao tác trên bảng, nó là ghép hai hàm. AB nghĩa là làm B trước, rồi làm A.

Nhìn theo cách đó thì một tầng mạng nơ-ron hết bí ẩn ngay:

PYTHON
h = W @ x + b

W ánh xạ vector đầu vào sang một không gian khác, có thể khác số chiều, còn b dịch nó đi. Đó là toàn bộ một tầng. Một mạng là một chồng các ánh xạ như vậy.

Và điều đó lập tức đặt ra một câu hỏi đáng ngồi lại. Nếu mọi tầng đều là ánh xạ tuyến tính, mà ghép các ánh xạ tuyến tính lại thì vẫn ra một ánh xạ tuyến tính, thì một trăm tầng chồng lên nhau sẽ gộp thành đúng một ma trận tương đương. Toàn bộ chiều sâu ấy chẳng mua được gì.

Đó chính xác là lý do hàm kích hoạt không phải tuỳ chọn. ReLU, sigmoid, tanh — việc của chúng là phá tính tuyến tính để phép ghép thực sự tăng khả năng biểu diễn. Hàm kích hoạt không phải chi tiết gắn thêm cho tiện; nó là lý do chiều sâu có ý nghĩa. Bạn không suy ra được điều này từ cách nhìn bảng-số, và suy ra ngay lập tức từ cách nhìn hàm.

Tích vô hướng là con ngựa kéo

Hai vector, nhân từng phần tử, cộng lại. Đơn giản đến tầm thường, mà nó đang gánh phần lớn công việc trong ML hiện đại, bởi tích vô hướng đo độ thẳng hàng.

PYTHON
similarity = a @ b        # lớn khi a và b cùng hướng

Mọi hệ thống dựa trên embedding đều chạy trên phép này. "Tìm tài liệu tương tự" là một tích vô hướng với các vector đã lưu. Điểm attention là tích vô hướng giữa query và key — token này nên quan tâm tới token kia bao nhiêu — và câu trả lời là hai vector của chúng thẳng hàng tới mức nào.

Dot product as alignment for aligned, orthogonal and opposing vectorsalignedlarge positiveorthogonalzeroopposednegative
Fig. 1 — the dot product is the projection of one vector onto the other. Alignment, as a number.

Đây là một chi tiết cho thấy toán mua cho bạn cái gì. Attention không phải q @ k, mà là:

PYTHON
scores = (q @ k.T) / math.sqrt(d_k)

Tại sao chia cho căn bậc hai của số chiều? Vì nếu các thành phần của qk gần như độc lập với phương sai đơn vị, thì tích vô hướng qua d_k số hạng có phương sai tỉ lệ với d_k. Nên số chiều càng lớn thì điểm số càng dàn rộng ra, mà softmax trên các giá trị dàn rộng thì bão hoà — nó gần như thành one-hot, gradient biến mất, và mô hình ngừng học được nên chú ý vào đâu. Chia cho căn d_k giữ phương sai ổn định.

Đó là một dòng code trông có vẻ tuỳ tiện mà không hề tuỳ tiện. Một đoạn xác suất giải thích được nó.

Shape là công cụ debug của bạn

Thói quen ít hào nhoáng nhất và hữu ích nhất: theo dõi shape của mọi tensor.

PYTHON
x        # (batch, seq_len, d_model)
W_q      # (d_model, d_head)
q = x @ W_q   # (batch, seq_len, d_head)

Một phần lớn lỗi thật là lỗi shape mà không hề crash — một phép broadcast lặng lẽ làm sai thứ, một phép transpose lẽ ra không nên có, một chiều batch bị cộng gộp mất. Nếu bạn đoán được shape đầu ra của từng dòng trước khi chạy, bạn bắt được chúng bằng cách đọc. Nếu không, bạn debug bằng lệnh in.

Đạo hàm: hướng nào là xuống dốc

Huấn luyện là một ý lặp đi lặp lại: đo xem đang sai bao nhiêu, tìm hướng làm giảm cái sai đó, đi một bước nhỏ theo hướng ấy.

Đạo hàm trả lời "nếu tôi nhích đầu vào này một chút, đầu ra đổi bao nhiêu và theo chiều nào?" Gradient là câu trả lời đó cho mọi tham số cùng lúc, gom lại thành một vector. Nó chỉ về hướng tăng dốc nhất, nên bạn bước ngược lại:

PYTHON
for p in parameters:
    p -= learning_rate * p.grad

Đó là gradient descent. Đó là toàn bộ câu chuyện tối ưu trước khi có các cải tiến.

Backpropagation chính là chain rule, không hơn

Chain rule nói: để lấy đạo hàm của các hàm lồng nhau, hãy nhân đạo hàm của từng lớp lồng.

Lấy một mạng nhỏ đến mức làm tay được:

PYTHON
z = w * x + b          # tuyến tính
a = sigmoid(z)         # kích hoạt
L = (a - y) ** 2       # hàm mất mát

Hàm mất mát đổi thế nào theo w? Đi ngược lại, nhân vào:

Code
dL/dw = dL/da * da/dz * dz/dw
      = 2 * (a - y)   *  a * (1 - a)  *  x

Ba đạo hàm cục bộ, mỗi cái tự nó hiển nhiên, nhân với nhau. Đó là backprop. Một framework làm việc đó cho một triệu tham số thay vì một, và lưu lại các giá trị trung gian để không tính lại, nhưng không có thêm ý tưởng nào cả.

Và giờ là phần đáng giá, vì nó giải thích một mẩu lịch sử. Nhìn số hạng ở giữa, đạo hàm của sigmoid: a * (1 - a). Giá trị lớn nhất có thể của nó là 0.25, tại a = 0.5. Trong một mạng sâu bạn nhân một cái như thế mỗi tầng. Mười tầng sigmoid và gradient tới được tầng đầu tiên bị nhân với nhiều nhất là 0.25 ** 10, tức khoảng một phần triệu. Các tầng đầu gần như không nhận được gì và không bao giờ học.

Gradient magnitude by depth, sigmoid versus ReLUsigmoidReLUlayer 1layer 10
Fig. 2 — gradient reaching each layer, log scale. Ten sigmoids leave roughly one part in a million.

Đó là bài toán vanishing gradient, và là lý do ReLU — đạo hàm đúng bằng 1 với đầu vào dương, nên nhân qua mà không co lại — từng là một bước cởi trói lớn đến vậy. Hai dòng chain rule nói cho bạn biết vì sao một lựa chọn kiến trúc lại quan trọng.

Xác suất: mô hình của bạn xuất ra một phân phối

Bước làm sắp xếp lại mọi thứ: một bộ phân loại không xuất ra một câu trả lời, nó xuất ra một phân phối xác suất trên các câu trả lời. Một mô hình ngôn ngữ không xuất ra token kế tiếp, nó xuất ra một phân phối trên toàn bộ từ vựng.

Một khi chấp nhận điều đó, hàm mất mát thôi là một lựa chọn thiết kế và trở thành một hệ quả suy ra được.

Maximum likelihood, và các hàm mất mát đến từ đâu

Bạn có dữ liệu và một mô hình có tham số. Hỏi: bộ tham số nào làm cho dữ liệu quan sát được trở nên khả dĩ nhất?

Giả sử các mẫu độc lập, xác suất của cả tập dữ liệu là tích các xác suất riêng lẻ:

Code
likelihood = p(y_1 | x_1) * p(y_2 | x_2) * ... * p(y_n | x_n)

Tích của nhiều số nhỏ thì tràn số dưới, nên lấy log — phép này biến tích thành tổng và đơn điệu nên không làm dịch điểm cực đại. Đổi dấu để thành bài toán cực tiểu, và bạn có negative log-likelihood:

Code
NLL = -sum( log p(y_i | x_i) )

Giờ đặc biệt hoá nó, và xem các hàm mất mát quen thuộc rơi ra.

Phân loại. Mô hình cho một xác suất mỗi lớp. Log xác suất âm của lớp đúng, cộng trên toàn dữ liệu, chính xác là cross-entropy loss. Nó không được chọn vì chạy tốt. Nó maximum likelihood cho một đầu ra phân loại.

Hồi quy. Giả sử giá trị mục tiêu bằng dự đoán cộng nhiễu Gauss với phương sai cố định. Viết ra hàm mật độ Gauss, lấy log, và mọi thứ trừ số hạng bình phương đều là hằng số theo tham số. Cái còn lại chính là sai số bình phương. Mean squared error là maximum likelihood dưới giả thiết nhiễu Gauss.

Điều này đáng giá hơn một mẩu kiến thức vụn. Nó cho bạn biết khi nào MSE là công cụ sai: khi nhiễu của bạn không phải Gauss. Dữ liệu đuôi nặng với thỉnh thoảng vài điểm ngoại lai lớn vi phạm giả thiết, số hạng bình phương để một điểm ngoại lai chi phối gradient, và cách chữa — Huber loss, hoặc dự đoán một phân vị — đến từ việc chất vấn giả thiết chứ không từ một danh sách các hàm mất mát thay thế.

Lý thuyết thông tin: ba đại lượng

Ba định nghĩa, liên hệ chặt với nhau, và xuất hiện liên tục.

Entropy là mức bất ngờ trung bình của một phân phối — nó bất định tới đâu. Một đồng xu cân bằng có entropy cao; một đồng xu luôn ra mặt ngửa thì không có.

Cross-entropy là mức bất ngờ trung bình bạn nhận khi tin vào phân phối Q trong khi thực tế theo P. Nó nhỏ nhất khi Q bằng P, và đó là lý do nó dùng được làm hàm mất mát: bạn bị phạt theo khoảng cách giữa phân phối dự đoán và sự thật.

KL divergence là hiệu của hai đại lượng trên:

Code
KL(P || Q) = cross_entropy(P, Q) - entropy(P)

Đọc nó là: bạn phải trả thêm bao nhiêu bất ngờ vì dùng Q thay cho P thật. Nó bằng không khi hai bên trùng nhau và lớn dần khi chúng lệch nhau.

Hai hệ quả. Vì entropy(P) không phụ thuộc mô hình của bạn, cực tiểu hoá cross-entropy và cực tiểu hoá KL divergence là cùng một bài toán tối ưu — hàm mất mát phân loại tiêu chuẩn vốn đã là một phép cực tiểu KL, chỉ là viết mà bỏ đi hằng số.

Và KL không đối xứng. KL(P || Q) không bằng KL(Q || P), và sự bất đối xứng đó có răng. Cực tiểu hoá theo chiều này cho ra một Q trải rộng để phủ mọi nơi P có khối lượng; chiều kia cho ra một Q co cụm vào một mode và bỏ qua phần còn lại. Chọn chiều nào sẽ đổi hành vi của mô hình.

Nếu bạn đã đọc bài về RLHF, đây chính là KL xuất hiện ở đó dưới dạng một khoản phạt. Policy bị tính phí vì lệch khỏi mô hình nó xuất phát, và hệ số của khoản phí đó là tỉ giá giữa reward và độ trôi dạt. Cùng một đại lượng, dùng làm dây xích thay vì làm hàm mất mát.

Tối ưu: tại sao thứ đơn giản lại chạy được

Gradient descent có một kiểu hỏng hiển nhiên. Gradient mang tính cục bộ — nó chỉ biết độ dốc ngay chỗ bạn đứng — nên nó đi xuống bất cứ thung lũng nào nó tình cờ đang ở trong, chứ không phải thung lũng sâu nhất.

Với một hàm mất mát lồi, hình dạng như cái bát, chỉ có một thung lũng và chuyện này không thành vấn đề. Hồi quy tuyến tính và logistic đều lồi, và đó là lý do chúng huấn luyện rất ổn định.

Mạng nơ-ron thì không lồi một cách dữ dội. Cực kỳ nhiều cực tiểu địa phương và điểm yên ngựa. Theo sách vở, gradient descent lẽ ra phải vô vọng.

Nó vẫn chạy, và các lý do đáng biết vì chúng biện minh cho những thứ nếu không thì trông như kinh nghiệm dân gian:

Ở số chiều rất lớn, cực tiểu địa phương tệ là hiếm. Để một điểm là cực tiểu địa phương, hàm mất mát phải cong lên theo mọi hướng trong hàng triệu hướng. Đó là một sự trùng hợp phi thường. Phổ biến hơn nhiều là điểm yên ngựa, cong lên theo vài hướng và cong xuống theo vài hướng khác — và một điểm yên ngựa thì có hướng đi xuống, nên bạn thoát ra được.

Nhiễu giúp ích. Stochastic gradient descent tính gradient trên một lô nhỏ, nên mỗi bước là một ước lượng có nhiễu của gradient thật. Nhiễu đó không chỉ là sự đánh đổi về tính toán; nó cho phép bộ tối ưu rung lắc để thoát khỏi những thung lũng hẹp. Đó là một tính năng.

Phần lớn các cực tiểu đều tốt xấp xỉ nhau. Trên thực nghiệm, rất nhiều cực tiểu mà một mạng lớn có thể rơi vào đều đạt mức mất mát tương đương. Bạn không đi tìm một lời giải đặc biệt giữa hàng triệu lời giải tồi.

Đây cũng là chỗ learning rate có được tiếng tăm quá khổ của nó. Quá lớn thì bạn bước vượt qua cực tiểu rồi dao động hoặc phân kỳ; quá nhỏ thì huấn luyện mãi không xong và mắc kẹt ở chỗ trũng nông đầu tiên. Không có tham số nào bạn chỉnh mà quan trọng bằng nó.

Cái gì có thể bỏ qua, tạm thời

Thành thật về chiều ngược lại cũng quan trọng ngang vậy.

Bạn không cần tính trị riêng bằng tay, nghịch đảo ma trận bằng tay, hay chứng minh các định lý hội tụ. Bạn không cần lý thuyết độ đo. Bạn không cần phần lớn một môn giải tích thực. Bạn không cần dẫn ra backprop cho một kiến trúc bất kỳ — bạn cần biết nó là chain rule, để khi một gradient ra NaN thì bạn biết nhìn vào đâu.

Định thức, phân tích trị riêng và SVD thực sự hữu ích, nhưng hữu ích về sau và ở những chỗ cụ thể: PCA, vài cách khởi tạo, phân tích xem một mô hình đã học được gì. Học chúng khi bạn đụng phải, đừng học trước.

Kiểu hỏng ở đây không phải bỏ qua quá nhiều. Mà là dành sáu tháng cho một khoá đại số tuyến tính trước khi viết một dòng code nào, rồi bước ra với một mớ định lý và không biết cái nào quan trọng.

Một thứ tự chạy được

Học xen kẽ với việc làm ra thứ gì đó, đừng học trước.

  1. Vector, ma trận, shape. Đủ để đọc một định nghĩa mô hình và đoán được mọi shape tensor. Làm cái này trước; nó có lãi ngay.
  2. Đạo hàm và chain rule. Đủ để tự tay dẫn ra ví dụ hai tầng phía trên. Rồi đọc một bản cài đặt backprop từ đầu và theo dõi từng dòng.
  3. Xác suất, tới maximum likelihood. Đủ để tự dẫn ra cross-entropy thay vì chấp nhận nó.
  4. Entropy, cross-entropy, KL. Nhỏ thôi, mà mở khoá được rất nhiều bài báo.
  5. Còn lại, học khi cần. Khi một bài báo dùng thứ bạn chưa biết, học đúng thứ đó. Đây là bộ lọc tốt hơn mọi giáo trình, vì những thứ bạn liên tục gặp lại chính là những thứ quan trọng, theo định nghĩa.

Nếu bạn muốn một nguồn duy nhất thay vì ba khoá học, Mathematics for Machine Learning (Deisenroth, Faisal và Ong) miễn phí trên mạng và viết đúng cho mục đích này — phần toán mà ML dùng, không kèm phần còn lại của tấm bằng. Muốn trực giác thay vì kỹ thuật tính toán thì Essence of linear algebraEssence of calculus của 3Blue1Brown là vài giờ đáng bỏ ra nhất.

Điểm mấu chốt

Chuyện này không phải để chứng minh được định lý. Nó là khác biệt giữa việc đọc scores / math.sqrt(d_k) như một dòng ai đó bảo bạn viết và đọc nó như một phép kiểm soát phương sai. Giữa "cross-entropy là hàm mất mát cho bài toán phân loại" và "cross-entropy là thứ mà maximum likelihood rút gọn thành ở đây, nên nó thôi đúng khi giả thiết này thôi đúng".

Khác biệt đó là thứ cho phép bạn debug một mô hình, thay vì chạy lại vòng huấn luyện với một seed ngẫu nhiên khác và hy vọng.

Published August 2026Found a mistake? Tell me →