Building a Go AI to Beat a 6-Dan Player
Inspiration
I've played Go competitively for years, reaching 6-dan — a level where most players have internalized thousands of patterns, joseki, and strategic principles built up over a lifetime of study. When DeepMind's AlphaGo defeated Lee Sedol in 2016, I watched with a mix of awe and skepticism. Could a machine truly understand Go, or was it just brute-forcing search trees? I wanted to find out firsthand — not by reading papers, but by building one myself.
The goal was simple and personal: train a Go AI from scratch that can beat me.
How It Works
The system is an AlphaGo-style pipeline built in PyTorch, running on a university GPU cluster. It has three main components:
1. Neural Network Architecture
The model uses a dual-headed residual network — the same family of architectures used in AlphaGo Zero. A shared convolutional trunk processes the board state, feeding into two separate output heads:
- Policy head — outputs a probability distribution $\pi(a \mid s)$ over all legal moves, answering "what move should I play?"
- Value head — outputs a scalar $v(s) \in [-1, 1]$, answering "who is winning?"
The network takes the board position $s$ as a multi-channel tensor (encoding current stones, ko state, and move history) and produces:
$$f_\theta(s) = (\mathbf{p}, v), \quad \mathbf{p} \in \Delta^{361}, \quad v \in [-1, 1]$$
2. Monte Carlo Tree Search (MCTS)
During game play, the AI doesn't just follow the raw policy — it runs MCTS to search ahead. Each simulation selects moves by maximizing an upper confidence bound:
$$\text{UCB}(s, a) = Q(s, a) + c_{\text{puct}} \cdot \pi(a \mid s) \cdot \frac{\sqrt{N(s)}}{1 + N(s, a)}$$
where $Q(s,a)$ is the average value of subtrees visited via action $a$, $N(s)$ is the visit count of the parent node, and $c_{\text{puct}}$ controls the exploration-exploitation tradeoff. With 400 simulations per move, the AI considers a wide tree of possibilities before committing to a decision.
3. Training Pipeline
Training happens in two phases:
Phase 1 — Supervised Learning: The model is pretrained on foxq_elite_7d_plus.pkl, a dataset of games played by 7-dan and above players on the Fox Go Server. The loss is:
$$\mathcal{L}{\text{SL}} = -\sum_a \pi^*(a) \log p\theta(a \mid s) + \lambda \cdot (v_\theta(s) - z)^2$$
where $\pi^*$ is the expert move distribution and $z \in {-1, +1}$ is the game outcome. After 25 epochs, the model learns reasonable opening patterns and basic strategic intuitions.
Phase 2 — Reinforcement Learning: The model plays games against KataGo, a superhuman Go engine, and updates on the resulting data. Rather than pure self-play (which is noisy and slow to converge), training against KataGo provides high-quality, adversarial feedback every single game. The RL objective mirrors the supervised loss but uses self-generated game data:
$$\mathcal{L}{\text{RL}} = -\sum_t \pi{\text{MCTS}}(a_t) \log p_\theta(a_t \mid s_t) + \lambda \cdot (v_\theta(s_t) - z_t)^2$$
with a mixed replay buffer combining KataGo games, self-play, and a fraction of expert data to prevent forgetting.
What I Built
- A custom
BatchedMCTSengine that runs parallel game generation on GPU - Asynchronous KataGo integration via background threads, so GPU training never waits on engine I/O
- A training loop (
train_gpu.py) designed for the university cluster, with checkpoint recovery and TensorBoard logging - A 100-iteration RL curriculum with configurable expert data mixing ratios
Challenges
The Value Head Collapse
The hardest problem I encountered was catastrophic forgetting in the value head. Around iteration 22 of RL training, the value loss collapsed from $\approx 0.38$ to $\approx 0.02$ — suspiciously low. The model had become extremely confident in its position evaluations, but completely wrong. Resignation rates dropped from 95% to 0%, and games that should end in 150 moves were dragging out to the 300-move cap.
This created a death spiral:
- Value head predicts garbage → resignation never triggers
- Games run to maximum length → each iteration takes 3× longer
- Longer games generate noisier value targets → value head gets worse
The root cause was that self-play outcomes from a weak model are too noisy to supervise a value head reliably. With an 80/20 expert/self-play mix, the expert data masked the problem in the loss metrics — but the damage was real.
The fix: Restart RL from the pretrained checkpoint with resignation disabled, letting the policy head continue improving while the value head issue is deferred. The long-term solution is KataGo value targets — using KataGo's position evaluations as ground-truth $v$ labels instead of binary game outcomes. This is closer to what AlphaGo Zero actually does in production.
Debugging on a Remote Cluster
Running on a shared university machine (e5-cse-135-19) added friction at every step: conda environments that vanish between sessions, module systems that need manual loading, jobs interrupted by cluster policy, and no easy way to interactively debug a GPU process. I learned to make training scripts self-recovering — checkpointing every 3 iterations and resuming cleanly from any interruption.
What I Learned
- The value head is the hardest part. Policy learning is relatively forgiving; value learning is brittle and collapses easily under noisy targets.
- Monitoring the right metrics matters. Loss curves looked fine while resignation rates were silently dying — a lesson in the limits of scalar metrics.
- KataGo as a curriculum is powerful. Training against a superhuman opponent compresses what would take hundreds of self-play iterations into a fraction of the compute.
- Preserving supervised learning progress is crucial. The 25 epochs of pretraining represent real, hard-won knowledge. Overwriting it carelessly during RL is a trap.
What's Next
- Implement KataGo value targets to fix the value head properly
- Re-enable resignation once value estimates are trustworthy
- Adaptive expert mixing: transition $70\% \to 60\% \to 50\%$ expert data as the model matures
- Curriculum learning: automatically increase KataGo's visit count as win rate improves
- The ultimate test: sit down, open a board, and play.
Log in or sign up for Devpost to join the conversation.