- 1. Vấn đề của việc chỉnh tay hàm đánh giá
- 2. Biến hàm evaluate thành vector đặc trưng + vector trọng số
- 3. Self-play: để bot tự chấm điểm trọng số của mình
- 4. Một thuật toán hill-climbing nhẹ để tối ưu trọng số
- 5. Demo: Bot 2048 tự tinh chỉnh trọng số (self-play mini trainer)
- 2048 Self-Learning AI Demo (Phần 2)
- Weights hiện tại
- 6. Hạn chế & chuẩn bị cho phần 3
1. Vấn đề của việc chỉnh tay hàm đánh giá
Phần 1 ta có dạng hàm:
score(board) =
w_empty * emptyCells(board)
+ w_mono * monotonicity(board)
+ w_smooth * smoothness(board)
+ w_corner * maxTileCorner(board)
+ w_max * maxTile(board)
Vấn đề:
- Chọn
w_*hoàn toàn bằng cảm giác: thử – sai – reload game. - Trọng số hợp với depth = 4, có thể dở với depth = 3 hoặc 5.
- Khi thêm feature mới (ví dụ “penalty cho tile lớn ở giữa”), ta lại phải ngồi chỉnh lại cả bộ.
Nói cách khác, ta đang code như năm 2000: “AI” mà không hề học từ dữ liệu. Bây giờ ta cho nó tự chơi, đo kết quả, rồi tự chỉnh trọng số.
2. Biến hàm evaluate thành vector đặc trưng + vector trọng số
Trước tiên, ta tách rõ features và weights:
- Features: các đặc trưng đo đạc từ bàn cờ (empty, smoothness, monotonicity, corner, maxTile).
- Weights: một vector số thực tương ứng với từng feature.
Ví dụ, ta viết lại evaluate thành:
function extractFeatures(board) {
const emptyCount = getEmptyCells(board).length;
const smoothness = computeSmoothness(board); // số âm
const monotonicity = computeMonotonicity(board); // số dương
const cornerBonus = maxTileInCorner(board) ? 1 : 0;
const maxTile = getMaxTile(board);
return {
emptyCount,
smoothness,
monotonicity,
cornerBonus,
maxTile,
};
}
function evaluateWithWeights(board, w) {
const f = extractFeatures(board);
return (
w.wEmpty * f.emptyCount +
w.wSmooth * f.smoothness +
w.wMono * f.monotonicity +
w.wCorner * f.cornerBonus +
w.wMax * f.maxTile
);
}
Sau đó, trong expectimax, ta chỉ cần truyền thêm w vào:
function expectimax(board, depth, isPlayerTurn, w) {
if (depth === 0 || isGameOver(board)) {
return evaluateWithWeights(board, w);
}
// ... phần còn lại giống phần 1, chỉ sửa chỗ gọi evaluate(...)
}
Như vậy, ta có một “mô hình” đơn giản: score = w · f(board) (tích vô hướng của trọng số và vector feature).
Việc “học” bây giờ chính là tìm vector w tốt.
3. Self-play: để bot tự chấm điểm trọng số của mình
Ý tưởng: với mỗi bộ trọng số w, ta cho bot:
- Chơi vài ván 2048 hoàn chỉnh (hoặc giới hạn số bước).
- Ghi lại điểm trung bình (hoặc max tile trung bình).
- Coi đó là “fitness” của bộ trọng số.
Như vậy ta có một hàm fitness:
function evaluateWeights(w) {
const NUM_GAMES = 3;
let totalScore = 0;
for (let g = 0; g < NUM_GAMES; g++) {
const score = playOneGameWithWeights(w);
totalScore += score;
}
return totalScore / NUM_GAMES;
}
Trong đó playOneGameWithWeights(w):
- Khởi tạo bàn cờ.
- Dùng expectimax với cùng bộ
wcho đến khi hết nước đi hoặc đạt ngưỡng. - Trả về điểm.
Điểm càng cao → trọng số càng “ngon”.
4. Một thuật toán hill-climbing nhẹ để tối ưu trọng số
Ta sẽ dùng một thuật toán cực kỳ đơn giản: stochastic hill-climbing.
Ý tưởng:
- Bắt đầu với một bộ trọng số “cảm tính”
w_best. - Tính fitness hiện tại
fitness_best = evaluateWeights(w_best). - Lặp lại nhiều lần:
- Nhân nhẹ mỗi trọng số với 1 + noise ngẫu nhiên:
w_candidate[i] = w_best[i] * (1 + uniform(-0.2, 0.2)). - Tính
fitness_candidate. - Nếu
fitness_candidate > fitness_bestthì:w_best = w_candidatefitness_best = fitness_candidate
- Nhân nhẹ mỗi trọng số với 1 + noise ngẫu nhiên:
Pseudocode JS:
let bestWeights = {
wEmpty: 350,
wSmooth: 3,
wMono: 10,
wCorner: 300,
wMax: 1,
};
let bestFitness = -Infinity;
function mutateWeights(w) {
const factor = () => 1 + (Math.random() * 0.4 - 0.2); // [-0.2, +0.2]
return {
wEmpty: w.wEmpty * factor(),
wSmooth: w.wSmooth * factor(),
wMono: w.wMono * factor(),
wCorner: w.wCorner * factor(),
wMax: w.wMax * factor(),
};
}
function trainOneStep() {
const candidate = mutateWeights(bestWeights);
const fitness = evaluateWeights(candidate); // self-play vài ván
if (fitness > bestFitness) {
bestFitness = fitness;
bestWeights = candidate;
console.log("New best fitness =", fitness, "weights =", bestWeights);
}
}
Đây chưa phải gradient descent, chưa phải RL chính thống, nhưng:
- Dễ hiểu, dễ triển khai trong trình duyệt.
- Có thể chạy trực tiếp trong bài viết, cho bot tự “trâu” vài chục ván rồi update kết quả.
5. Demo: Bot 2048 tự tinh chỉnh trọng số (self-play mini trainer)
Demo dưới đây gồm 2 phần:
- Một board 2048 cho bạn xem bot chơi với trọng số hiện tại.
- Một trainer nhỏ:
- Nút “Train 10 bước (self-play)”: bot tự chơi nhiều ván, thử nghiệm trọng số mới, nếu tốt hơn thì cập nhật.
- Hiển thị bộ trọng số hiện tại và “fitness” ước lượng.
Bạn có thể:
- Ấn “Auto-play với weights hiện tại” để bot chơi một ván với trọng số mới nhất.
- Ấn “Train 10 bước” vài lần, xem trọng số drift dần và bot có chơi khôn hơn không.
2048 Self-Learning AI Demo (Phần 2)
Weights hiện tại
6. Hạn chế & chuẩn bị cho phần 3
Cái ta vừa làm vẫn còn khá “thô”:
- Hàm đánh giá vẫn là tuyến tính trên vài feature thủ công.
- Chiến lược học chỉ là hill-climbing ngẫu nhiên, không dùng gradient, không có khái niệm “trạng thái – hành động – reward” rõ ràng như RL chuẩn.
- Việc đánh giá fitness tốn khá nhiều thời gian (chơi nhiều ván), khó scale nếu depth cao.
Nhưng bù lại, có vài điểm rất thực tế:
- Dễ cài ngay trong trình duyệt, không cần backend.
- Trực quan: nhìn trọng số thay đổi, nhìn bot chơi tốt lên hoặc tệ đi.
- Là bước đệm rất hợp lý trước khi sang phần 3 với các kỹ thuật “xịn sò” hơn:
- Dùng mạng nơ-ron để học evaluation function.
- TD-learning / Q-learning / các biến thể RL cho 2048.
- MCTS kết hợp hàm value học được (kiểu AlphaZero phiên bản mini).
Ở phần 3, ta sẽ nâng cấp từ “tuning weights” sang “học hẳn một hàm value phi tuyến”, để bot 2048 tiến gần hơn phong cách các game AI hiện đại.
Bình luận