游戏AI与Atari

一句话概述

游戏是强化学习的「试金石」——从Atari 2600用DQN实现超人级表现,到AlphaGo击败人类围棋冠军,再到OpenAI Five在Dota 2中击败职业战队,RL在游戏领域的成功验证了算法的有效性并推动了技术进步。游戏AI的核心在于:将游戏画面作为状态输入、用深度神经网络提取特征、通过自我博弈或环境交互学习最优策略。

教学与演示

什么是游戏AI

游戏AI指用人工智能技术让计算机「学会」玩游戏,而非通过预设规则。与传统游戏AI(如FSM有限状态机、行为树)不同,RL游戏AI不需要人类编写任何游戏技巧——它只靠「看屏幕、按按钮、获得分数」这三个信号自我摸索。

游戏之所以成为RL的经典应用领域,有几个得天独厚的优势:

    undefined
大白话 游戏AI是RL算法的「竞技场」——在游戏里能work的算法,搬到真实世界不一定行;但在游戏里都不work的算法,在真实世界更不可能行。游戏提供了一个「低风险、高保真」的测试环境。

DQN与Atari 2600

2013年,DeepMind的「Playing Atari with Deep Reinforcement Learning」横空出世,第一次展示了端到端学习的威力:只给算法屏幕像素和游戏分数,让它自己学会玩49款Atari游戏。

DQN在Atari上的架构简洁但精妙:

DQN架构\(Q(s, a) \approx f_{\text{CNN}}(s; \theta)\)

输入是4帧堆叠的84×84灰度画面(用于感知运动),经过三层卷积神经网络提取特征,最后接全连接层输出每个动作的Q值。

DQN的三大法宝:

    undefined
大白话 DQN就像一个小孩坐在电视机前,只通过「看画面」和「按手柄」两个动作,纯靠试错学习怎么玩49款完全不同的游戏。关键是它学会了「泛化」——同一套神经网络架构和超参数,对Pong、Breakout、Space Invaders等完全不同类型的游戏都有效。

训练结果

    undefined
大白话 DQN玩Breakout最有趣——它自己发现了一个人类的「高级技巧」:先在侧边打开一个缺口,让球穿到砖块后面来回弹跳。这个策略程序员并没有教它,是DQN奖励最大化的自然涌现。

AlphaGo与围棋AI

围棋被称为「人类智慧的巅峰游戏」——19×19的棋盘上有约$10^{170}$种可能局面,远超宇宙中原子总数。传统AI方法在围棋上完全失效。

AlphaGo(2016)的成功归功于三大组件的融合:

1. 策略网络(Policy Network) 使用监督学习在人类职业棋谱(约3000万局面)上训练,目标是预测人类棋手的落子位置。这赋予了阿尔法狗基本的「棋感」。

2. 价值网络(Value Network)

价值网络\(V(s) \approx \mathbb{E}[\text{胜负概率} | s]\)

估计当前局面的胜率。训练数据来自自我博弈——策略网络和自己对弈3000万局,每局的结果作为V(s)的标签(赢了=1,输了=0)。

3. 蒙特卡洛树搜索(MCTS) 在给定的计算时间内,从当前局面出发模拟成千上万次可能的对局走向,结合策略网络(「下哪里看起来好」)和价值网络(「当前局面谁优」),选择最有希望的落子。

MCTS选择公式}\(a^* = \arg\max_a \left( Q(s, a) + c \cdot P(a|s) \cdot \frac{\sqrt{N(s)}}{1 + N(s, a)} \right)\)

AlphaGo Zero更进一步:完全抛弃人类棋谱,从零开始自我博弈学习。初始随机下棋,通过不断的自我对弈和强化学习信号(胜负结果),仅用40天就超越AlphaGo Master(曾战胜柯洁的版本)。

大白话 AlphaGo Zero最震撼的地方在于——它完全「从零开始」。只告诉它围棋规则,没有任何人类棋谱,通过类似「左手和右手下棋」的自我博弈,40天后就能打败世界冠军。它自己发明了人类已知的定式和策略,也创造了许多人类从未见过的新走法。

RL在更多游戏中的突破

    undefined
大白话 从Atari到Dota 2,游戏AI越来越接近「真实世界的复杂性」。Dota 2的挑战在于——看不见地图(不完美信息)、一场打45分钟(长时域)、和4个队友配合(多智能体)。这几乎是自动驾驶决策问题的游戏版。

什么用

游戏AI的研究成果远远超越了游戏本身:

    undefined

哪些坑

坑点原因解决方案
游戏过拟合对特定游戏关卡/地图记忆而非学习通用策略随机化地图/道具位置,增加多样性
奖励稀疏很多游戏只在结束时给分(如回合制策略游戏)奖励塑形(Reward Shaping)、内在奖励
动作延迟与反应时间真实游戏有操作延迟和人类反应时间限制加入动作延迟、限制APM
状态表示某些游戏状态空间过大无法直接处理结构化输入(AlphaStar用原始游戏数据而非像素)
训练成本AlphaStar训练需要数百TPU跑数周模仿学习初始化大幅加速早期训练
评估公平性与人类比赛时算力优势 vs 智力优势限制AI的APM/反应时间到人类水平

核心代码演示

"""
简化版DQN在Atari-like环境上的演示
展示经验回放、目标网络和ε-贪心探索
"""
import numpy as np
from collections import deque

# ===== 经验回放池(Experience Replay Buffer)=====
class ReplayBuffer:
    """
    DQN的核心组件之一:存储历史经验,随机采样打破时序相关
    """
    def __init__(self, capacity=10000):
        self.buffer = deque(maxlen=capacity)
    
    def push(self, state, action, reward, next_state, done):
        """存储一条经验"""
        self.buffer.append((state, action, reward, next_state, done))
    
    def sample(self, batch_size):
        """随机采样一批经验"""
        indices = np.random.choice(len(self.buffer), batch_size, replace=False)
        states, actions, rewards, next_states, dones = [], [], [], [], []
        for idx in indices:
            s, a, r, ns, d = self.buffer[idx]
            states.append(s)
            actions.append(a)
            rewards.append(r)
            next_states.append(ns)
            dones.append(d)
        return (np.array(states), np.array(actions), np.array(rewards),
                np.array(next_states), np.array(dones))
    
    def __len__(self):
        return len(self.buffer)

# ===== DQN智能体(简化神经网络用线性模型替代CNN)=====
class DQNAgent:
    def __init__(self, state_dim, n_actions, lr=0.001, gamma=0.99,
                 epsilon=1.0, epsilon_min=0.01, epsilon_decay=0.995):
        self.state_dim = state_dim
        self.n_actions = n_actions
        self.gamma = gamma
        self.epsilon = epsilon
        self.epsilon_min = epsilon_min
        self.epsilon_decay = epsilon_decay
        self.lr = lr
        
        # Q网络和目标网络(简化用线性模型替代CNN)
        self.q_network = np.random.randn(state_dim, n_actions) * 0.01
        self.target_network = self.q_network.copy()
        
        self.memory = ReplayBuffer(capacity=5000)
        self.update_counter = 0
    
    def act(self, state):
        """ε-greedy选择动作"""
        if np.random.random() < self.epsilon:
            return np.random.randint(self.n_actions)
        q_values = state @ self.q_network
        return np.argmax(q_values)
    
    def learn(self, batch_size=32):
        """从经验回放池中学习"""
        if len(self.memory) < batch_size:
            return
        
        states, actions, rewards, next_states, dones = self.memory.sample(batch_size)
        
        # Q(s, a) = Q_network(s)[a]
        q_values = states @ self.q_network  # [batch, n_actions]
        q_current = q_values[np.arange(batch_size), actions]  # [batch]
        
        # Q_target = r + γ * max_a' Q_target(s', a') * (1 - done)
        q_next = next_states @ self.target_network  # [batch, n_actions]
        q_target = rewards + self.gamma * np.max(q_next, axis=1) * (1 - dones)
        
        # TD误差更新(简化:用平均梯度)
        td_error = q_target - q_current  # [batch]
        for i in range(batch_size):
            self.q_network[:, actions[i]] += self.lr * td_error[i] * states[i]
        
        # 定期同步目标网络
        self.update_counter += 1
        if self.update_counter % 100 == 0:
            self.target_network = self.q_network.copy()
        
        # 衰减探索率
        self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)

# ===== 简单Atari-like环境 =====
class SimpleAtariEnv:
    """
    简化Atari环境:智能体需要学会向目标移动
    """
    def __init__(self, size=5):
        self.size = size
        self.reset()
    
    def reset(self):
        self.agent_pos = np.array([0, 0])
        self.target_pos = np.array([self.size-1, self.size-1])
        return self.get_state()
    
    def get_state(self):
        """返回简化状态(实际Atari是84×84×4的像素)"""
        state = np.zeros(self.size * self.size + 4)
        state[self.agent_pos[0] * self.size + self.agent_pos[1]] = 1
        state[-4] = self.target_pos[0] / self.size
        state[-3] = self.target_pos[1] / self.size
        state[-2] = self.agent_pos[0] / self.size
        state[-1] = self.agent_pos[1] / self.size
        return state
    
    def step(self, action):
        """0:上 1:下 2:左 3:右"""
        deltas = np.array([[-1,0], [1,0], [0,-1], [0,1]])
        new_pos = self.agent_pos + deltas[action]
        new_pos = np.clip(new_pos, 0, self.size - 1)
        self.agent_pos = new_pos
        
        if np.array_equal(self.agent_pos, self.target_pos):
            reward = 1.0
            done = True
        else:
            reward = -0.01  # 鼓励快速到达
            done = False
        return self.get_state(), reward, done

# ===== 训练演示 =====
env = SimpleAtariEnv(size=5)
state_dim = env.size * env.size + 4
n_actions = 4
agent = DQNAgent(state_dim, n_actions)

n_episodes = 200
total_rewards = []
for ep in range(n_episodes):
    state = env.reset()
    total_reward = 0
    done = False
    step = 0
    
    while not done and step < 100:
        action = agent.act(state)
        next_state, reward, done = env.step(action)
        agent.memory.push(state, action, reward, next_state, done)
        agent.learn(batch_size=32)
        state = next_state
        total_reward += reward
        step += 1
    
    total_rewards.append(total_reward)
    if ep % 40 == 0:
        avg = np.mean(total_rewards[-20:])
        print(f"Episode {ep:3d} | 平均奖励: {avg:.3f} | ε={agent.epsilon:.3f}")

print(f"\n最终平均奖励: {np.mean(total_rewards[-20:]):.3f}")
print("DQN三件套:经验回放 + 目标网络 + 探索衰减 — 搞定Atari!")