Tài liệu NNUE của Stockfish
Địa chỉ bản gốc: nnue.md của official-stockfish/nnue-pytorch
Lời mở đầu
Tài liệu này bao gồm:
- Nội dung kỹ thuật
- Mô tả chi tiết về NNUE và nguyên lý của nó
- Ôn nhanh đại số tuyến tính
- Định nghĩa đầu vào và phân tích nhân tử (factorization)
- Các thành phần (lớp) phù hợp cho mạng NNUE
- Mã suy luận (inference) và tối ưu hóa
- Toán học của lượng tử hóa và cách hiện thực
- Mã tối ưu gần như dùng được trong sản phẩm thực tế
- Hiện thực trình huấn luyện bằng PyTorch (+ các CUDA kernel quan trọng)
- Cân nhắc về kiến trúc và lịch sử
Tài liệu này không bao gồm:
- Hướng dẫn huấn luyện mạng (xem wiki)
- Tập dữ liệu, bộ tối ưu, siêu tham số
- Nhật ký kết quả thử nghiệm
Kiến thức cơ bản
NNUE là gì?
NNUE (ƎUИИ Efficiently Updatable Neural Network, mạng nơ-ron có thể cập nhật hiệu quả) hiểu theo nghĩa rộng là một kiến trúc mạng nơ-ron tận dụng việc đầu vào của mạng chỉ thay đổi rất ít giữa các lần đánh giá liên tiếp. Nó do Yu Nasu phát minh, để tích hợp vào phần mềm shogi YaneuraOu do Motohiro Isozaki phát triển vào tháng 5 năm 2018, sau đó vào tháng 6 năm 2019 được Hisayori Noda chuyển sang engine cờ vua Stockfish, nhưng cũng phù hợp với nhiều trò chơi cờ khác, thậm chí có thể ứng dụng trong các lĩnh vực khác. NNUE tuân theo các nguyên tắc sau:
- Mạng nên có tương đối ít đầu vào khác không.
- Giữa các lần đánh giá liên tiếp, đầu vào nên thay đổi càng ít càng tốt.
- Mạng nên đủ đơn giản để thực hiện suy luận độ chính xác thấp trong miền số nguyên.
Tuân theo nguyên tắc thứ nhất có nghĩa là khi quy mô mạng mở rộng, đầu vào phải trở nên thưa. Kiến trúc tốt nhất hiện nay có độ thưa đầu vào khoảng 0,1%. Số lượng đầu vào khác không nhỏ đặt ra một giới hạn trên thấp cho thời gian cần thiết trong trường hợp phải đánh giá toàn bộ. Đây là lý do chính khiến mạng NNUE vẫn có thể rất lớn mà vẫn đánh giá được cực nhanh.
Tuân theo nguyên tắc thứ hai (với điều kiện đã tuân theo nguyên tắc thứ nhất) tạo ra một cách cập nhật hiệu quả mạng (hoặc ít nhất là phần tốn kém của nó), thay vì đánh giá lại toàn bộ mạng. Điều này tận dụng thực tế là một nước đi chỉ làm thay đổi trạng thái bàn cờ đôi chút. Nguyên tắc này kém quan trọng hơn nguyên tắc thứ nhất, hoàn toàn là tùy chọn khi hiện thực, nhưng ở những hiện thực có tận dụng nó thì vẫn mang lại cải thiện đo lường được.
Tuân theo nguyên tắc thứ ba giúp đạt hiệu năng tối đa trên phần cứng phổ biến, và làm cho mô hình đặc biệt phù hợp với suy luận trên CPU có độ trễ thấp, điều mà engine cờ truyền thống đòi hỏi.
Nhìn chung, các nguyên tắc NNUE cũng áp dụng được cho các mạng sâu tốn kém, nhưng chúng tỏa sáng ở các mạng nông nhanh, phù hợp với suy luận CPU độ trễ thấp, không cần xử lý theo lô (batch) và bộ tăng tốc. Hiệu năng mục tiêu là hàng triệu lần đánh giá mỗi giây trên mỗi luồng. Đây là một trường hợp sử dụng cực đoan, cần giải pháp cực đoan, quan trọng nhất là lượng tử hóa.
Nhập môn lượng tử hóa và tầm quan trọng của nó
Lượng tử hóa là quá trình đổi miền của mô hình mạng nơ-ron từ số thực dấu phẩy động sang số nguyên. Mạng NNUE được thiết kế để đánh giá nhanh trong miền số nguyên độ chính xác thấp, và tận dụng đầy đủ hiệu năng int8/int16 của CPU hiện đại. Số dấu phẩy động không phải là lựa chọn để đạt sức mạnh engine tối đa, vì nó hy sinh quá nhiều tốc độ mà chỉ thu được cải thiện độ chính xác rất nhỏ (dù do đơn giản nên một số engine vẫn dùng biểu diễn dấu phẩy động). Lượng tử hóa chắc chắn sẽ đưa vào sai số, mạng càng sâu sai số tích lũy càng nhiều, nhưng với mạng NNUE tương đối nông thì sai số này có thể bỏ qua. Lượng tử hóa sẽ được mô tả chi tiết ở phần sau của tài liệu. Trước đó, tài liệu sẽ dùng số dấu phẩy động thay vì số nguyên, cho đến khi ta thực sự tối ưu mã thì điều này mới trở nên quan trọng. Chèn đoạn này vào nhằm giúp bạn đọc nhận thức được mục tiêu cuối cùng của NNUE, vì đó là yếu tố lớn nhất định hình mô hình NNUE, và quyết định điều gì khả thi, điều gì không.
Những lớp nào hữu ích trong NNUE?
NNUE dựa vào các lớp đơn giản có thể hiện thực bằng phép toán số học đơn giản trong môi trường độ chính xác thấp. Điều này có nghĩa là lớp tuyến tính (kết nối đầy đủ, về cơ bản là phép nhân ma trận) và lớp ClippedReLU (clamp(0, 1)) đặc biệt phù hợp. Các lớp gộp (pooling: nhân/trung bình/lớn nhất) hoặc xấp xỉ của các hàm kích hoạt phức tạp hơn (như sigmoid) cũng dùng được, nhưng ít được dùng.
Thông thường, loại mạng này giữ ở dạng nông (2-4 lớp), vì phần lớn tri thức được lưu ở lớp đầu tiên (tận dụng độ thưa của đầu vào để giữ hiệu năng), sau lớp đầu tiên mạng cần giảm mạnh độ rộng (lợi ích ở các phần sâu hơn phía sau của mạng sẽ bị chi phối bởi ảnh hưởng của các lớp lớn phía trước) để duy trì yêu cầu hiệu năng.
Lớp tuyến tính
Lớp tuyến tính (kết nối đầy đủ) chỉ là một phép nhân ma trận đơn giản. Nó có thể hiện thực hiệu quả, hỗ trợ đầu vào thưa, và cung cấp dung lượng tốt. Nó nhận giá trị in_features làm đầu vào và tạo giá trị out_features làm đầu ra. Phép toán là y = Ax + b, trong đó:
x: vector cột đầu vào có kích thướcin_featuresA: ma trận trọng số có kích thước(out_features, in_features)b: vector cột độ lệch (bias) có kích thướcout_featuresy: vector cột đầu ra có kích thướcout_features
Lớp tuyến tính với đầu vào thưa
Phép nhân Ax về mặt khái niệm có thể rút gọn thành "nếu x[i] khác không, lấy cột thứ i của A, nhân với x[i] rồi cộng vào kết quả". Bây giờ có thể thấy rõ, mỗi khi một phần tử của đầu vào bằng không, ta có thể bỏ qua việc xử lý cả một hàng của ma trận trọng số. Điều này có nghĩa là ta chỉ cần xử lý những cột của A tương ứng với các giá trị khác không trong vector đầu vào. Dù ma trận trọng số có thể có hàng chục nghìn cột, ta chỉ chú ý đến một vài cột trong đó cho mỗi thế cờ! Đây là lý do lớp đầu tiên có thể lớn đến vậy.
Lớp Clipped ReLU
Đây là hàm kích hoạt dựa trên ReLU thông thường, khác ở chỗ nó bị chặn cả hai phía trên và dưới. Công thức là y = min(max(x, 0), 1).
Mục đích của lớp này là thêm tính phi tuyến cho mạng. Nếu chỉ có các lớp tuyến tính thì tất cả đều có thể gộp thành một, vì các ma trận có thể nhân trực tiếp với nhau.
Lý tưởng nhất, ClippedReLU sẽ được thay bằng ReLU, nhưng lượng tử hóa mạnh tay đòi hỏi giảm dải động của đầu vào lớp ẩn, vì vậy việc giới hạn giá trị ở 1 rất quan trọng đối với hiệu năng.
Sigmoid
Đây là hàm kích hoạt trơn, đối lập với ReLU [bị chặn]. Công thức là y = 1/(1+e^-kx), trong đó k là tham số quyết định mức độ "kéo giãn" của hình dạng.
