Skip to content

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)
Python
# 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)
Python
# 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. ab sẽ cùng trỏ đến cùng một vùng nhớ. Nếu bạn thay đổi b, 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 đổi dense1.weights một cách tự do mà không làm ảnh hưởng đến best_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.

Python
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:

    1. Đ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ỏ.
    2. 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.
    3. Đánh giá và Quyết định (if/else):
      • if loss < lowest_loss (Đi xuống dốc): Nếu loss mới nhỏ hơn loss 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ếu loss 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.

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ới 0.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:

Text Only
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):

Text Only
 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)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.