Code Explain
Đây là phân tích chi tiết về đoạn mã lập trình hoàn chỉnh ở cuối Chương 6, thực thi phương pháp Tinh chỉnh Ngẫu nhiên (Random Local Search).
Mục tiêu của đoạn mã¶
Đoạn mã này là một chương trình hoàn chỉnh để minh họa một phương pháp tối ưu hóa tốt hơn so với tìm kiếm hoàn toàn ngẫu nhiên. Thay vì "nhảy dù" đến các vị trí ngẫu nhiên, mô hình sẽ bắt đầu từ một điểm, "đi thử" một bước nhỏ ngẫu nhiên, và chỉ chấp nhận bước đi đó nếu nó dẫn đến một kết quả tốt hơn (loss thấp hơn).
Đây chính là hiện thực hóa của liên hệ trừu tượng "người leo núi bịt mắt" đã nói ở trên.
Phân tích chi tiết từng khối lệnh¶
Chúng ta sẽ đi qua từng phần của mã và giải thích ý nghĩa của nó.
Khối 1: Chuẩn bị Môi trường (Trang 11)¶
# Create dataset
X, y = vertical_data(samples=100, classes=3)
# Create model
dense1 = Layer_Dense(2, 3)
activation1 = Activation_ReLU()
dense2 = Layer_Dense(3, 3)
activation2 = Activation_Softmax()
# Create loss function
loss_function = Loss_CategoricalCrossentropy()
- Giải thích dễ hiểu: Phần này thiết lập mọi thứ cần thiết trước khi bắt đầu huấn luyện.
vertical_data
: Tạo ra một bộ dữ liệu "dễ". Dữ liệu này gồm 3 cụm điểm được phân tách rõ ràng theo chiều dọc. Một đường thẳng có thể dễ dàng phân chia chúng. Đây là lý do tại sao phương pháp này có thể thành công với nó.Layer_Dense(2, 3)
: Lớp ẩn đầu tiên nhận 2 đầu vào (tọa độ x, y của mỗi điểm dữ liệu) và có 3 neuron.Layer_Dense(3, 3)
: Lớp đầu ra nhận 3 đầu vào (từ lớp trước) và có 3 neuron (tương ứng 3 lớp/cụm dữ liệu cần phân loại).ReLU
,Softmax
,CategoricalCrossentropy
: Đây là các thành phần tiêu chuẩn của một mạng neural cho bài toán phân loại đa lớp mà chúng ta đã xây dựng ở các chương trước.
Khối 2: Khởi tạo "Bộ nhớ" của Người leo núi (Trang 11)¶
# Helper variables
lowest_loss = 9999999 # some initial value
best_dense1_weights = dense1.weights.copy()
best_dense1_biases = dense1.biases.copy()
best_dense2_weights = dense2.weights.copy()
best_dense2_biases = dense2.biases.copy()
-
Giải thích dễ hiểu: Đây là bước cực kỳ quan trọng, nó thiết lập "trí nhớ" cho thuật toán.
lowest_loss = 9999999
: Chúng ta khởi tạo một biến để lưu lại giá trị loss thấp nhất từng thấy. Gán cho nó một số rất lớn để đảm bảo rằng loss tính được ở lần lặp đầu tiên chắc chắn sẽ nhỏ hơn và được ghi nhận.best_..._weights = dense1.weights.copy()
: Đây là phần cốt lõi của "trí nhớ". Chúng ta tạo ra một bản sao (copy) hoàn chỉnh của bộ trọng số và bias ban đầu. Biến này sẽ luôn lưu giữ bộ tham số tốt nhất mà chúng ta từng tìm thấy.
-
Đối chiếu kiến thức tổng quát - Tầm quan trọng của
.copy()
: Trong Python, khi bạn gán một đối tượng (như mảng NumPy)a = b
, bạn không tạo ra một đối tượng mới.a
vàb
sẽ cùng trỏ đến cùng một vùng nhớ. Nếu bạn thay đổib
,a
cũng sẽ thay đổi theo. Phương thức.copy()
tạo ra một đối tượng hoàn toàn mới, độc lập, nằm ở một vùng nhớ khác. Điều này cho phép chúng ta thay đổidense1.weights
một cách tự do mà không làm ảnh hưởng đếnbest_dense1_weights
, thứ đang lưu giữ "kỷ lục" của chúng ta. Nếu không có.copy()
, thuật toán sẽ không hoạt động.
Khối 3: Vòng lặp Tối ưu hóa - Hành trình leo núi (Trang 11-12)¶
Đây là trái tim của thuật toán.
for iteration in range(10000):
# --- Bước 1: Đi thử một bước ngẫu nhiên ---
# Update weights with some small random values
dense1.weights += 0.05 * np.random.randn(2, 3)
dense1.biases += 0.05 * np.random.randn(1, 3)
# ... tương tự cho dense2 ...
# --- Bước 2: Kiểm tra vị trí mới ---
# Perform a forward pass ...
dense1.forward(X)
activation1.forward(dense1.output)
# ...
loss = loss_function.calculate(activation2.output, y)
# --- Bước 3: Đánh giá và Quyết định ---
# Calculate accuracy ...
predictions = np.argmax(activation2.output, axis=1)
accuracy = np.mean(predictions==y)
# If loss is smaller ...
if loss < lowest_loss:
# Giữ lại vị trí mới
print(...)
best_dense1_weights = dense1.weights.copy()
best_dense1_biases = dense1.biases.copy()
# ...
lowest_loss = loss
# Revert weights and biases
else:
# Quay về vị trí cũ
dense1.weights = best_dense1_weights.copy()
dense1.biases = best_dense1_biases.copy()
# ...
-
Giải thích dễ hiểu:
- Đi thử một bước (
+=
): Thay vì tạo mới trọng số, chúng ta lấy trọng số hiện tại và cộng thêm (+=
) một giá trị nhiễu loạn ngẫu nhiên rất nhỏ (0.05 * np.random.randn(...)
). Con số0.05
là "kích thước bước đi", nó quyết định xem bước thử của chúng ta là lớn hay nhỏ. - Kiểm tra vị trí mới: Sau khi "đi thử", chúng ta thực hiện lại toàn bộ quá trình lan truyền tiến để tính toán
loss
với bộ trọng số vừa được điều chỉnh. - Đánh giá và Quyết định (
if/else
):if loss < lowest_loss
(Đi xuống dốc): Nếuloss
mới nhỏ hơnloss
tốt nhất từng ghi nhận, đây là một bước đi thành công! Chúng ta sẽ:- In ra thông báo tiến bộ.
- Cập nhật
lowest_loss
với giá trị mới. - Quan trọng nhất: Cập nhật các biến
best_...
bằng cách sao chép bộ trọng số và bias hiện tại. Vị trí mới này trở thành "vị trí tốt nhất" mới.
else
(Đi lên dốc hoặc đi ngang): Nếuloss
mới không tốt hơn, bước đi thử này là một sai lầm. Chúng ta sẽ:- Hoàn tác lại thay đổi bằng cách gán các trọng số và bias hiện tại trở lại giá trị đã được lưu trong các biến
best_...
. Thao tác này đưa "người leo núi" quay trở lại vị trí tốt nhất đã biết trước đó, để chuẩn bị thử một bước đi ngẫu nhiên khác trong lần lặp tiếp theo.
- Hoàn tác lại thay đổi bằng cách gán các trọng số và bias hiện tại trở lại giá trị đã được lưu trong các biến
- Đi thử một bước (
Sơ đồ minh họa phép toán ma trận¶
Phép toán quan trọng và mới mẻ nhất trong vòng lặp này là dense1.weights += 0.05 * np.random.randn(2, 3)
.
dense1.weights
: là một ma trận có kích thước(2, 3)
.np.random.randn(2, 3)
: tạo ra một ma trận ngẫu nhiên cũng có kích thước(2, 3)
.0.05 * ...
: nhân mỗi phần tử trong ma trận ngẫu nhiên với0.05
.+=
: cộng hai ma trận theo từng phần tử (element-wise addition).
Sơ đồ minh họa cho việc cập nhật dense1.weights
:
Trọng số hiện tại (dense1.weights) Điều chỉnh ngẫu nhiên (0.05 * randn) Trọng số mới
(2x3) (2x3) (2x3)
+-------------------+ +-------------------+ +--------------------------+
| w_11 w_12 w_13| + | r_11 r_12 r_13| = | w_11+r_11 w_12+r_12 ... |
| w_21 w_22 w_23| | r_21 r_22 r_23| | w_21+r_21 w_22+r_22 ... |
+-------------------+ +-------------------+ +--------------------------+
Sơ đồ minh họa cho việc cập nhật dense1.biases
(là một vector hàng):
Bias hiện tại (dense1.biases) Điều chỉnh ngẫu nhiên (0.05 * randn) Bias mới
(1x3) (1x3) (1x3)
+---------------+ + +---------------+ + +-------------------+
| b_1 b_2 b_3 | + | rb_1 rb_2 rb_3| = | b_1+rb_1 ... |
+---------------+ +---------------+ +-------------------+
Kết luận và Đánh giá¶
- Ưu điểm: Phương pháp này có định hướng hơn nhiều so với tìm kiếm hoàn toàn ngẫu nhiên. Nó xây dựng dựa trên thành công trước đó, từng bước dò dẫm để tìm ra giải pháp tốt hơn. Điều này giải thích tại sao nó hoạt động hiệu quả trên bộ dữ liệu
vertical_data
đơn giản, đạt độ chính xác cao. - Nhược điểm chí mạng: Như đã thấy ở cuối chương, khi áp dụng vào dữ liệu
spiral_data
phức tạp, nó thất bại. Lý do là nó bị mắc kẹt ở cực tiểu cục bộ (local minimum). "Người leo núi bịt mắt" đã đi xuống một cái hố nhỏ và không thể thoát ra, vì mọi bước đi nhỏ ngẫu nhiên đều dẫn lên dốc. - Bài học rút ra: Chúng ta cần một phương pháp "thông minh" hơn là chỉ đi mò mẫm ngẫu nhiên. Chúng ta cần một cách để biết được hướng nào là dốc nhất để đi xuống. Đây chính là vai trò của đạo hàm (derivative) và gradient, sẽ được giới thiệu trong các chương tiếp theo để xây dựng thuật toán Gradient Descent.