Học Máy Dựa Trên Gradient (Gradient Machine Learning)¶
Học dựa trên gradient tối ưu hóa các tham số mô hình bằng cách lần theo độ dốc của bề mặt mất mát một cách lặp đi lặp lại. File này bao gồm hồi quy tuyến tính, hồi quy logistic, phân lớp softmax, các biến thể hạ gradient, chính quy hóa (L1/L2) và sự đánh đổi độ chệch-phương sai.
-
Các phương pháp cổ điển trong file 01 sử dụng các heuristic thông minh hoặc các nghiệm dạng đóng. File này bao gồm các thuật toán học bằng cách lần theo gradient, đi từng bước nhỏ xuống dốc trên bề mặt mất mát cho đến khi tìm được các tham số tốt. Học dựa trên gradient là động cơ đằng sau mọi thứ, từ hồi quy tuyến tính đến các mạng nơ-ron lớn nhất.
-
Hồi quy tuyến tính (Linear regression) là mô hình dựa trên gradient đơn giản nhất, và nó cũng có một nghiệm dạng đóng, điều này làm cho nó là một điểm khởi đầu hoàn hảo. Mô hình là một đường thẳng (hoặc siêu phẳng trong không gian nhiều chiều hơn):
-
Trong ký hiệu ma trận (từ chương 02), nếu chúng ta xếp tất cả các đầu vào huấn luyện thành các hàng của một ma trận \(X\) và hấp thụ độ chệch (bias) vào \(w\) bằng cách thêm một cột toàn số 1, điều này trở thành \(\hat{y} = Xw\).
-
Mục tiêu là tối thiểu hóa sai số bình phương trung bình (mean squared error - MSE), hiệu số bình phương trung bình giữa dự đoán và giá trị thực tế:
- Tại sao lại dùng sai số bình phương? Nó có một cơ sở xác suất: nếu bạn giả định các mục tiêu được sinh ra như \(y = Xw + \epsilon\) với \(\epsilon \sim \mathcal{N}(0, \sigma^2)\), thì việc cực đại hóa hợp lý Gaussian của dữ liệu (chương 05) tương đương với tối thiểu hóa MSE. Sai số bình phương cũng phạt các lỗi lớn hơn các lỗi nhỏ, điều thường là mong muốn.
- Bởi vì MSE là một hàm bậc hai của \(w\), nó có một cực tiểu toàn cục duy nhất mà chúng ta có thể tìm được một cách giải tích. Lấy đạo hàm, cho bằng 0, và giải sẽ cho phương trình chuẩn (normal equation):
-
Công thức này sử dụng trực tiếp ma trận nghịch đảo từ chương 02. Biểu thức \(X^T X\) là một ma trận \(d \times d\) (với \(d\) là số đặc trưng), và \(X^T y\) là một vector \(d\) chiều. Phương trình chuẩn cho các trọng số tối ưu chính xác trong một bước.
-
Khi nào phương trình chuẩn thất bại? Khi \(X^T X\) là suy biến (không khả nghịch), xảy ra nếu các đặc trưng phụ thuộc tuyến tính hoặc nếu bạn có nhiều đặc trưng hơn số mẫu (\(d > n\)). Trong các trường hợp này bạn cần chính quy hóa (sẽ trình bày sau) hoặc hạ gradient.
-
Hồi quy logistic (Logistic regression) thích ứng mô hình tuyến tính cho phân lớp nhị phân. Thay vì dự đoán một giá trị liên tục, chúng ta muốn một xác suất giữa 0 và 1. Hàm sigmoid ép bất kỳ số thực nào vào khoảng này:
- Mô hình tính \(z = w \cdot x + b\) (một điểm số tuyến tính, giống hệt hồi quy tuyến tính) và sau đó truyền nó qua sigmoid: \(\hat{y} = \sigma(w \cdot x + b)\). Đầu ra \(\hat{y}\) được diễn giải là \(P(y = 1 \mid x)\).
-
Sigmoid có các tính chất đẹp: \(\sigma(0) = 0.5\), \(\sigma(z) \to 1\) khi \(z \to \infty\), \(\sigma(z) \to 0\) khi \(z \to -\infty\), và đạo hàm của nó có dạng elegant \(\sigma'(z) = \sigma(z)(1 - \sigma(z))\).
-
Hàm mất mát cho hồi quy logistic là entropy chéo nhị phân (binary cross-entropy - BCE), bắt nguồn trực tiếp từ hợp lý Bernoulli (chương 05):
-
Khi nhãn thực là 1, chỉ số hạng thứ nhất hoạt động và nó phạt các dự đoán thấp. Khi nhãn thực là 0, chỉ số hạng thứ hai hoạt động và nó phạt các dự đoán cao. Logarit làm cho hình phạt cực dốc đối với các dự đoán sai tự tin: dự đoán 0.01 khi nhãn thực là 1 tốn đắt hơn nhiều so với dự đoán 0.4.
-
Khác với MSE cho hồi quy tuyến tính, không có nghiệm dạng đóng cho các trọng số cực tiểu hóa BCE. Chúng ta cần một phương pháp lặp: hạ gradient (gradient descent).
-
Trực giác đằng sau hạ gradient rất đơn giản: hãy tưởng tượng bạn đang đứng trên một cảnh quan đồi núi (bề mặt mất mát) trong sương mù. Bạn không thể nhìn thấy cực tiểu toàn cục, nhưng bạn có thể cảm nhận độ dốc dưới chân. Bạn đi từng bước xuống dốc, cảm nhận độ dốc lần nữa, và lặp lại. Cuối cùng bạn đến được một thung lũng.
- Tốc độ học (learning rate) \(\eta\) kiểm soát kích thước bước của bạn. Quá lớn và bạn vượt qua các thung lũng, nhảy qua lại mà không hội tụ. Quá nhỏ và bạn bò từng chút một một cách đau đớn, có thể bị kẹt ở một cực tiểu địa phương.
-
Gradient \(\frac{\partial \mathcal{L}}{\partial w}\) là một vector chỉ theo hướng dốc nhất. Chúng ta trừ nó đi vì chúng ta muốn đi xuống dốc. Đây là quy tắc dẫn hàm hàm hợp (chain rule) từ chương 03 áp dụng cho hàm mất mát.
-
Hạ gradient theo batch (Batch gradient descent) tính gradient sử dụng toàn bộ tập huấn luyện ở mỗi bước. Cách này cho gradient chính xác nhưng tốn kém khi \(n\) lớn.
-
Hạ gradient ngẫu nhiên (Stochastic gradient descent - SGD) sử dụng một ví dụ ngẫu nhiên duy nhất mỗi bước. Gradient bị nhiễu (nó ước lượng gradient thực từ một mẫu) nhưng mỗi bước cực kỳ nhanh. Nhiễu thực sự có thể giúp thoát khỏi các cực tiểu địa phương nông.
-
Hạ gradient mini-batch là sự thỏa hiệp: sử dụng một batch gồm \(B\) ví dụ (thường là 32, 64, hoặc 256) mỗi bước. Cách này cân bằng hiệu quả tính toán (các phép toán vector hóa trên batch) với chất lượng gradient. Hầu như tất cả học sâu đều dùng mini-batch SGD.
-
Lan truyền ngược (Backpropagation) là cách chúng ta thực sự tính gradient trong các mô hình có nhiều tham số, như mạng nơ-ron. Nó là quy tắc dẫn hàm hàm hợp từ chương 03 được áp dụng một cách có hệ thống qua một đồ thị tính toán.
-
Bất kỳ mô hình nào cũng có thể được biểu diễn như một đồ thị có hướng không chu trình của các phép toán: đầu vào đi vào, được nhân với trọng số, cộng lại, truyền qua các hàm phi tuyến, và cuối cùng tạo ra một giá trị mất mát. Lượt truyền xuôi (forward pass) tính đầu ra (và mất mát) bằng cách truyền dữ liệu qua đồ thị này từ đầu vào đến đầu ra.
-
Lượt truyền ngược (backward pass) (lan truyền ngược) truyền gradient theo chiều ngược lại. Bắt đầu từ mất mát, bạn tính mất mát thay đổi như thế nào đối với mỗi giá trị trung gian, sử dụng quy tắc dẫn hàm hàm hợp tại mỗi nút. Nếu \(L\) phụ thuộc vào \(z\) mà \(z\) phụ thuộc vào \(w\), thì:
-
Mỗi nút chỉ cần biết đạo hàm cục bộ của nó và gradient chảy vào từ phía trên. Điều này làm cho lan truyền ngược có tính mô-đun và hiệu quả: chi phí xấp xỉ gấp đôi lượt truyền xuôi (một lần xuôi, một lần ngược).
-
SGD nguyên bản có một vấn đề: nó dao động theo các hướng có độ cong dốc trong khi tiến triển chậm theo các hướng phẳng. Các bộ tối ưu (Optimisers) cải thiện điều này bằng cách thích ứng bước dựa trên lịch sử gradient.
-
SGD với động lượng (SGD with momentum) duy trì một trung bình động của các gradient quá khứ (một trung bình trượt số mũ, từ chương 04). Cách này làm mượt các dao động và tăng tốc tiến triển theo các hướng nhất quán:
-
Hãy nghĩ về một quả bóng lăn xuống dốc: động lượng cho phép nó tích lũy tốc độ theo một hướng nhất quán và làm giảm sự rung lắc qua lại. Giá trị điển hình là \(\beta = 0.9\).
-
Nesterov Accelerated Gradient (NAG) là một tinh chỉnh nhỏ nhưng thông minh: thay vì tính gradient tại vị trí hiện tại, tính nó tại vị trí "nhìn trước" \(w - \eta \beta v_{t-1}\). Bước hiệu chỉnh này giảm sự vượt quá:
- Adagrad thích ứng tốc độ học cho từng tham số. Các tham số nhận gradient lớn nhận tốc độ học nhỏ hơn, và ngược lại. Nó tích lũy các bình phương gradient:
-
Vấn đề: \(G_t\) chỉ tăng, do đó tốc độ học hiệu dụng giảm đơn điệu và cuối cùng trở nên quá nhỏ để học bất cứ thứ gì.
-
RMSprop khắc phục bằng cách dùng trung bình trượt số mũ của các bình phương gradient thay vì tổng, để các gradient gần đây quan trọng hơn các gradient cổ xưa:
- Adam (Adaptive Moment Estimation) kết hợp momentum và RMSprop. Nó duy trì cả ước lượng moment bậc nhất (trung bình của gradient, như momentum) và ước lượng moment bậc hai (trung bình của các bình phương gradient, như RMSprop):
- Vì \(m_t\) và \(v_t\) được khởi tạo bằng 0, chúng bị chệch về 0 ở các bước đầu. Hiệu chỉnh chệch khắc phục điều này:
-
Các siêu tham số mặc định (\(\beta_1 = 0.9\), \(\beta_2 = 0.999\), \(\epsilon = 10^{-8}\)) hoạt động tốt trên một dải rộng các bài toán, chính là lý do Adam là bộ tối ưu mặc định trong hầu hết công việc học sâu.
-
AdamW tách sự suy giảm trọng số (weight decay) khỏi bước cập nhật gradient. Chính quy hóa L2 tiêu chuẩn và weight decay tương đương với nhau đối với SGD nhưng không đối với Adam. AdamW áp dụng weight decay trực tiếp cho các tham số thay vì cộng \(\lambda w\) vào gradient. Cách này cho tính khái quát hóa tốt hơn và hiện là tiêu chuẩn trong huấn luyện transformer:
- LION (EvoLved Sign Momentum) là một bộ tối ưu mới hơn được khám phá thông qua tìm kiếm chương trình. Nó chỉ dùng dấu của cập nhật momentum (không phải độ lớn), làm cho mỗi cập nhật đồng nhất về tỷ lệ. LION dùng ít bộ nhớ hơn Adam (không có bộ đệm moment bậc hai) và có thể sánh ngang hoặc vượt Adam trên nhiều nhiệm vụ:
- Muon (Momentum + Orthogonalisation) áp dụng Nesterov momentum và sau đó trực giao hóa ma trận cập nhật bằng các lần lặp Newton-Schulz, xấp xỉ phân rã cực (polar decomposition). Hướng cập nhật kết quả nằm trên đa tạp Stiefel, mỗi cập nhật có độ lớn xấp xỉ bằng nhau trên mọi hướng trị riêng, ngăn không cho bất kỳ hướng đơn lẻ nào chiếm ưu thế. Điều này loại bỏ nhu cầu ước lượng moment bậc hai thích ứng (không có bộ đệm \(v_t\) như Adam), giảm bộ nhớ. Muon đã cho kết quả mạnh mẽ trong huấn luyện transformer, thường sánh chất lượng AdamW với hội tụ nhanh hơn, đặc biệt cho các ma trận trọng số attention và MLP. Các lớp embedding và đầu ra thường vẫn được xử lý bởi AdamW.
- Lần lặp Newton-Schulz tính nhân tử trực giao bằng cách lặp lại \(X_{k+1} = \frac{1}{2} X_k (3I - X_k^T X_k)\) trong một vài bước (thường 5-10). Cách này tránh chi phí của một SVD đầy đủ trong khi cho một xấp xỉ tốt.
-
Ngoài MSE và BCE, một số hàm mất mát khác cũng thường được sử dụng.
-
Sai số tuyệt đối trung bình (Mean Absolute Error - MAE), hay mất mát L1, lấy trung bình của các hiệu số tuyệt đối: \(\frac{1}{n}\sum|y_i - \hat{y}_i|\). Nó mạnh mẽ hơn với ngoại lệ (outlier) so với MSE vì nó không bình phương các lỗi lớn.
-
Mất mát Huber kết hợp tinh túy của cả hai: nó hành xử như MSE cho các lỗi nhỏ (mượt, dễ tối ưu) và như MAE cho các lỗi lớn (mạnh mẽ với ngoại lệ). Nó có một ngưỡng \(\delta\) kiểm soát sự chuyển tiếp.
-
Entropy chéo phân loại (Categorical cross-entropy - CCE) tổng quát hóa BCE cho nhiều lớp. Nếu \(\hat{y}_k\) là xác suất dự đoán cho lớp \(k\) và lớp thực là \(c\):
-
Đây chỉ là log âm của xác suất của lớp đúng. Tối thiểu hóa entropy chéo tương đương với cực đại hóa hợp lý, kết nối lại với lý thuyết thông tin trong chương 05: entropy chéo đo số bit thừa bạn cần khi dùng phân bố dự đoán thay vì phân bố thực.
-
Mất mát Hinge được dùng bởi SVM: \(\mathcal{L} = \max(0, 1 - y \cdot f(x))\). Nó chỉ phạt các dự đoán ở phía sai của lề hoặc nằm trong lề. Một khi một điểm được phân lớp đúng với độ tự tin đủ, mất mát bằng 0.
-
Chính quy hóa (Regularisation) ngăn quá khớp bằng cách thêm một hình phạt cho các mô hình phức tạp. Mất mát được chính quy hóa là:
-
Chính quy hóa L2 (Ridge, weight decay) phạt tổng các bình phương trọng số: \(R(w) = \|w\|^2 = \sum w_i^2\). Nó ngăn không cho bất kỳ trọng số đơn lẻ nào trở nên quá lớn, về hiệu quả thu nhỏ tất cả trọng số về 0 nhưng hiếm khi làm chúng bằng chính xác 0.
-
Chính quy hóa L1 (Lasso) phạt tổng các trọng số tuyệt đối: \(R(w) = \|w\|_1 = \sum |w_i|\). Nó khuyến khích sự thưa (sparsity), đẩy nhiều trọng số về chính xác 0, thực hiện chọn đặc trưng tự động.
-
Elastic Net kết hợp cả hai: \(R(w) = \alpha \|w\|_1 + (1 - \alpha) \|w\|^2\), pha trộn sự thưa và sự thu nhỏ.
-
Có một cách diễn giải Bayes đẹp đẽ (từ chương 05). Chính quy hóa L2 tương đương với đặt một tiên nghiệm Gaussian lên các trọng số và tìm ước lượng MAP. Chính quy hóa L1 tương ứng với một tiên nghiệm Laplace. Độ mạnh chính quy hóa \(\lambda\) kiểm soát mức bạn tin vào tiên nghiệm so với dữ liệu.
-
Các thước đo đánh giá (Evaluation metrics) cho bạn biết mô hình có thực sự hoạt động hay không. Với hồi quy, MSE và MAE là tiêu chuẩn. Với phân lớp, mọi thứ tinh tế hơn.
-
Một ma trận nhầm lẫn (confusion matrix) là một bảng gồm bốn số đếm cho phân lớp nhị phân:
- Dương tính thật (True Positive - TP): dự đoán dương, thực tế dương
- Dương tính giả (False Positive - FP): dự đoán dương, thực tế âm
- Âm tính thật (True Negative - TN): dự đoán âm, thực tế âm
-
Âm tính giả (False Negative - FN): dự đoán âm, thực tế dương
-
Độ chính xác (Accuracy) = \(\frac{TP + TN}{TP + TN + FP + FN}\) có thể gây hiểu lầm khi các lớp mất cân bằng. Nếu 99% email không phải spam, một mô hình luôn dự đoán "không spam" có độ chính xác 99% nhưng vô dụng.
-
Precision = \(\frac{TP}{TP + FP}\) trả lời: trong tất cả các dự đoán dương, có bao nhiêu thực sự dương? Precision cao nghĩa là ít báo động giả.
-
Recall (độ nhạy) = \(\frac{TP}{TP + FN}\) trả lời: trong tất cả các ca dương thực tế, bạn bắt được bao nhiêu? Recall cao nghĩa là ít ca bị bỏ sót.
-
Điểm F1 = \(\frac{2 \cdot \text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}}\) là trung bình điều hòa của precision và recall, cân bằng cả hai.
-
Đường cong ROC vẽ tỷ lệ dương tính thật (recall) so với tỷ lệ dương tính giả (\(\frac{FP}{FP + TN}\)) khi bạn thay đổi ngưỡng phân lớp từ 0 đến 1. Một bộ phân lớp hoàn hảo bám sát góc trên bên trái. AUC (diện tích dưới đường cong ROC) tóm tắt hiệu suất trong một con số duy nhất: 1.0 là hoàn hảo, 0.5 là đoán ngẫu nhiên.
-
Cross-validation cung cấp một ước lượng đáng tin cậy hơn về hiệu suất khái quát hóa. Trong cross-validation \(k\)-fold, bạn chia dữ liệu thành \(k\) phần, huấn luyện trên \(k-1\) phần, kiểm tra trên phần còn lại, và xoay vòng. Trung bình hiệu suất kiểm tra trên cả \(k\) phần là ước lượng của bạn. Cách này dùng tất cả dữ liệu cho cả huấn luyện và kiểm tra (chỉ là không bao giờ cùng một lúc), đặc biệt có giá trị khi dữ liệu khan hiếm.
-
Sự đánh đổi độ chệch-phương sai (bias-variance tradeoff) (từ chương 04) là sự căng thẳng cơ bản trong ML. Sai số kỳ vọng của mô hình phân rã thành:
-
Độ chệch (Bias) là sai số hệ thống từ các giả định sai (ví dụ, khớp một đường thẳng vào dữ liệu cong). Phương sai (Variance) là độ nhạy với các dao động của dữ liệu huấn luyện (ví dụ, một đa thức bậc 20 khớp nhiễu). Các mô hình đơn giản có độ chệch cao và phương sai thấp; các mô hình phức tạp có độ chệch thấp và phương sai cao. Điểm ngọt là tối thiểu hóa tổng sai số.
-
Lên lịch tốc độ học (Learning rate scheduling) điều chỉnh \(\eta\) trong quá trình huấn luyện. Các chiến lược phổ biến:
- Suy giảm bậc thang (Step decay): nhân \(\eta\) với một hệ số (ví dụ 0.1) mỗi \(N\) epoch
- Làm mềm cosin (Cosine annealing): giảm trơn \(\eta\) theo một đường cong cosin từ giá trị ban đầu về gần 0
- Khởi động (Warmup): bắt đầu với \(\eta\) rất nhỏ và tăng tuyến tính trong vài ngàn bước đầu, sau đó suy giảm. Cách này ngăn các gradient ban đầu lớn làm mất ổn định huấn luyện
-
1cycle: một chu kỳ cosin lên rồi xuống, có thể cho hội tụ nhanh hơn
-
Tinh chỉnh siêu tham số (Hyperparameter tuning) là quá trình tìm các giá trị tốt cho tốc độ học, kích thước batch, độ mạnh chính quy hóa, và các cài đặt khác không được học bởi hạ gradient. Các cách tiếp cận phổ biến:
- Tìm kiếm lưới (Grid search): thử mọi tổ hợp trên một lưới định sẵn (đầy đủ nhưng tốn kém)
- Tìm kiếm ngẫu nhiên (Random search): lấy mẫu các tổ hợp ngẫu nhiên, thường hiệu quả hơn vì không phải mọi siêu tham số đều quan trọng như nhau
- Tối ưu hóa Bayes (Bayesian optimisation): xây dựng một mô hình của hàm mục tiêu và chọn thông minh siêu tham số tiếp theo cần thử
-
ASHA (Asynchronous Successive Halving Algorithm): chạy nhiều thử nghiệm song song với ngân sách nhỏ, sau đó thăng cấp những thử nghiệm hứa hẹn nhất lên ngân sách lớn hơn trong khi loại bỏ phần còn lại sớm. Nó kết hợp hiệu quả của dừng sớm với sự song song hóa lớn — thay vì chạy 100 lần huấn luyện đầy đủ, bắt đầu tất cả 100 lần với chi phí rẻ, giữ lại một phần tư hàng đầu tại mỗi bậc, và chỉ một ít chạy đến hoàn tất. Đây là xương sống của các khung tinh chỉnh quy mô lớn hiện đại như Ray Tune.
-
Học không cần lịch trình (Schedule-free learning) loại bỏ hoàn toàn nhu cầu có một lịch trình tốc độ học. Thay vì suy giảm \(\eta\) trên một đường cong cố định, nó duy trì hai dãy: một trung bình động chậm của các lần lặp \(z_t\) (hội tụ đến cực tiểu) và một lần lặp khám phá nhanh \(y_t\) (nơi các gradient được đánh giá). Đầu ra cuối cùng là dãy trung bình, được chứng minh khớp tốc độ hội tụ của lịch trình tốt nhất khi nhìn lại (hindsight). Cách này loại bỏ lịch trình như một siêu tham số hoàn toàn — bạn chỉ đặt tốc độ học cơ sở và bộ tối ưu lo phần còn lại. Các biến thể schedule-free của cả SGD và Adam đã được chứng minh sánh ngang hoặc vượt các đối tác có lịch trình đã tinh chỉnh.
Bài tập lập trình (dùng CoLab hoặc notebook)¶
-
Cài đặt hồi quy tuyến tính cả bằng phương trình chuẩn và hạ gradient. So sánh các nghiệm và vẽ sự hội tụ của mất mát GD qua các lần lặp.
import jax import jax.numpy as jnp import matplotlib.pyplot as plt # Generate synthetic data: y = 3x + 2 + noise key = jax.random.PRNGKey(42) n = 100 X = jax.random.uniform(key, (n, 1), minval=0, maxval=10) y = 3 * X[:, 0] + 2 + jax.random.normal(key, (n,)) * 1.5 # Add bias column X_b = jnp.column_stack([X, jnp.ones(n)]) # Normal equation w_exact = jnp.linalg.solve(X_b.T @ X_b, X_b.T @ y) print(f"Normal equation: w={w_exact[0]:.4f}, b={w_exact[1]:.4f}") # Gradient descent w_gd = jnp.zeros(2) lr = 0.005 losses = [] for step in range(500): pred = X_b @ w_gd error = pred - y loss = jnp.mean(error ** 2) losses.append(float(loss)) grad = (2 / n) * X_b.T @ error w_gd = w_gd - lr * grad print(f"Gradient descent: w={w_gd[0]:.4f}, b={w_gd[1]:.4f}") fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].scatter(X[:, 0], y, s=15, alpha=0.5, color='#3498db') axes[0].plot([0, 10], [w_exact[1], w_exact[0]*10 + w_exact[1]], color='#e74c3c', linewidth=2) axes[0].set_title("Linear Regression Fit") axes[0].set_xlabel("x"); axes[0].set_ylabel("y") axes[1].plot(losses, color='#27ae60', linewidth=1.5) axes[1].set_title("GD Loss Convergence") axes[1].set_xlabel("Step"); axes[1].set_ylabel("MSE") axes[1].set_yscale('log') plt.tight_layout() plt.show() -
Cài đặt hồi quy logistic từ đầu với hạ gradient. Huấn luyện trên một tập dữ liệu 2D và trực quan hóa ranh giới quyết định đã học.
import jax import jax.numpy as jnp import matplotlib.pyplot as plt from sklearn.datasets import make_moons # Generate data X, y = make_moons(n_samples=300, noise=0.2, random_state=42) X, y = jnp.array(X), jnp.array(y, dtype=jnp.float32) def sigmoid(z): return 1 / (1 + jnp.exp(-z)) # Add bias column X_b = jnp.column_stack([X, jnp.ones(len(X))]) w = jnp.zeros(3) lr = 0.5 losses = [] for step in range(2000): z = X_b @ w pred = sigmoid(z) # BCE loss loss = -jnp.mean(y * jnp.log(pred + 1e-8) + (1 - y) * jnp.log(1 - pred + 1e-8)) losses.append(float(loss)) # Gradient grad = X_b.T @ (pred - y) / len(y) w = w - lr * grad # Decision boundary xx, yy = jnp.meshgrid(jnp.linspace(-2, 3, 200), jnp.linspace(-1.5, 2, 200)) grid = jnp.column_stack([xx.ravel(), yy.ravel(), jnp.ones(xx.size)]) zz = sigmoid(grid @ w).reshape(xx.shape) plt.figure(figsize=(8, 6)) plt.contourf(xx, yy, zz, levels=[0, 0.5, 1], alpha=0.3, colors=['#e74c3c', '#3498db']) plt.contour(xx, yy, zz, levels=[0.5], colors='#9b59b6', linewidths=2) plt.scatter(X[y==0, 0], X[y==0, 1], c='#e74c3c', s=15, label='Class 0') plt.scatter(X[y==1, 0], X[y==1, 1], c='#3498db', s=15, label='Class 1') plt.title("Logistic Regression Decision Boundary") plt.legend() plt.grid(alpha=0.3) plt.show() -
So sánh quỹ đạo của các bộ tối ưu trên một bề mặt bậc hai 2D. Chạy SGD, SGD+Momentum và Adam từ cùng một điểm xuất phát và vẽ các đường đi của chúng.
import jax import jax.numpy as jnp import matplotlib.pyplot as plt # Elongated quadratic: L(w1, w2) = 0.5*w1^2 + 10*w2^2 def loss_fn(w): return 0.5 * w[0]**2 + 10 * w[1]**2 grad_fn = jax.grad(loss_fn) def run_sgd(w0, lr=0.05, steps=80): w = w0.copy() path = [w.copy()] for _ in range(steps): g = grad_fn(w) w = w - lr * g path.append(w.copy()) return jnp.stack(path) def run_momentum(w0, lr=0.05, beta=0.9, steps=80): w, v = w0.copy(), jnp.zeros(2) path = [w.copy()] for _ in range(steps): g = grad_fn(w) v = beta * v + (1 - beta) * g w = w - lr * v path.append(w.copy()) return jnp.stack(path) def run_adam(w0, lr=0.05, b1=0.9, b2=0.999, eps=1e-8, steps=80): w, m, v = w0.copy(), jnp.zeros(2), jnp.zeros(2) path = [w.copy()] for t in range(1, steps + 1): g = grad_fn(w) m = b1 * m + (1 - b1) * g v = b2 * v + (1 - b2) * g**2 m_hat = m / (1 - b1**t) v_hat = v / (1 - b2**t) w = w - lr * m_hat / (jnp.sqrt(v_hat) + eps) path.append(w.copy()) return jnp.stack(path) w0 = jnp.array([8.0, 3.0]) sgd_path = run_sgd(w0) mom_path = run_momentum(w0) adam_path = run_adam(w0) # Plot fig, ax = plt.subplots(figsize=(8, 6)) w1 = jnp.linspace(-10, 10, 100) w2 = jnp.linspace(-4, 4, 100) W1, W2 = jnp.meshgrid(w1, w2) L = 0.5 * W1**2 + 10 * W2**2 ax.contour(W1, W2, L, levels=20, cmap='Greys', alpha=0.4) ax.plot(sgd_path[:,0], sgd_path[:,1], 'o-', color='#3498db', markersize=2, linewidth=1, label='SGD') ax.plot(mom_path[:,0], mom_path[:,1], 'o-', color='#27ae60', markersize=2, linewidth=1, label='Momentum') ax.plot(adam_path[:,0], adam_path[:,1], 'o-', color='#e74c3c', markersize=2, linewidth=1, label='Adam') ax.plot(0, 0, 'k*', markersize=15, label='Minimum') ax.set_xlabel('w₁'); ax.set_ylabel('w₂') ax.set_title("Optimizer Trajectories on Elongated Quadratic") ax.legend() plt.grid(alpha=0.3) plt.show() -
Trình diễn tác động của chính quy hóa L1 so với L2 lên độ thưa của trọng số. Huấn luyện hồi quy tuyến tính với cả hai hình phạt và so sánh các vector trọng số thu được.
import jax import jax.numpy as jnp import matplotlib.pyplot as plt # Synthetic data: only first 3 of 20 features are relevant key = jax.random.PRNGKey(0) n, d = 200, 20 w_true = jnp.zeros(d).at[:3].set(jnp.array([3.0, -2.0, 1.5])) X = jax.random.normal(key, (n, d)) y = X @ w_true + 0.5 * jax.random.normal(key, (n,)) def train_ridge(X, y, lam=1.0, lr=0.01, steps=2000): """L2 regularised linear regression via GD.""" w = jnp.zeros(X.shape[1]) for _ in range(steps): pred = X @ w grad = (2/len(y)) * X.T @ (pred - y) + 2 * lam * w w = w - lr * grad return w def train_lasso(X, y, lam=1.0, lr=0.01, steps=2000): """L1 regularised linear regression via proximal GD.""" w = jnp.zeros(X.shape[1]) for _ in range(steps): pred = X @ w grad = (2/len(y)) * X.T @ (pred - y) w = w - lr * grad # Soft thresholding (proximal operator for L1) w = jnp.sign(w) * jnp.maximum(jnp.abs(w) - lr * lam, 0) return w w_l2 = train_ridge(X, y, lam=0.1) w_l1 = train_lasso(X, y, lam=0.1) fig, axes = plt.subplots(1, 3, figsize=(14, 4)) axes[0].bar(range(d), w_true, color='#333', alpha=0.7) axes[0].set_title("True Weights"); axes[0].set_xlabel("Feature") axes[1].bar(range(d), w_l2, color='#3498db', alpha=0.7) axes[1].set_title("L2 (Ridge): shrinks all"); axes[1].set_xlabel("Feature") axes[2].bar(range(d), w_l1, color='#e74c3c', alpha=0.7) axes[2].set_title("L1 (Lasso): zeros out irrelevant"); axes[2].set_xlabel("Feature") plt.tight_layout() plt.show() print(f"L2 non-zero weights: {int(jnp.sum(jnp.abs(w_l2) > 0.01))}/{d}") print(f"L1 non-zero weights: {int(jnp.sum(jnp.abs(w_l1) > 0.01))}/{d}")