游戏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上的架构简洁但精妙:
输入是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)
估计当前局面的胜率。训练数据来自自我博弈——策略网络和自己对弈3000万局,每局的结果作为V(s)的标签(赢了=1,输了=0)。
3. 蒙特卡洛树搜索(MCTS) 在给定的计算时间内,从当前局面出发模拟成千上万次可能的对局走向,结合策略网络(「下哪里看起来好」)和价值网络(「当前局面谁优」),选择最有希望的落子。
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!")