我的AI贪吃蛇训练翻车实录:奖励函数没设好,它直接开摆不吃了!
去年夏天,我决定用强化学习训练一个能玩贪吃蛇的AI。本以为凭借自己扎实的机器学习基础,这应该是个小菜一碟的项目。然而现实却给了我当头一棒——我的AI蛇不仅没能成为游戏高手,反而在训练过程中展现出了惊人的"摆烂"天赋:要么原地转圈,要么直直撞墙,最离谱的是有几次它干脆停在角落一动不动,仿佛在抗议我的训练方式。这段经历让我深刻认识到,在强化学习中,奖励函数的设计就是AI行为的指挥棒,稍有不慎就会让整个训练走向奇怪的方向。
1. 为什么我的AI蛇开始"罢工"
第一次看到训练日志时,我简直不敢相信自己的眼睛。在训练了约2000轮后,AI蛇的平均得分不仅没有上升,反而从最初的3分降到了可怜的1分。更诡异的是,回放它的游戏过程会发现,这条"聪明"的蛇发展出了一套独特的生存策略:永远沿着墙壁绕圈,对近在咫尺的食物视而不见。
1.1 奖励函数的隐藏陷阱
经过仔细分析,我发现问题出在奖励函数的几个设计缺陷上:
# 问题奖励函数示例 def get_reward(self, done, distances, tran_i): distances_2 = np.sqrt(np.sum((np.array(self.snake.get_head_position()) - np.array(self.food.position)) ** 2)) reward = 0 if tran_i > 50: reward -= 0.1 # 连续直线移动惩罚 if done: reward -= 20 # 碰撞惩罚 elif self.snake.get_head_position() == self.food.position: reward += 10 # 吃到食物奖励 elif distances_2 < distances: reward += 0.2 # 靠近食物奖励 else: reward -= 0.1 # 默认惩罚 return reward这个看似合理的函数实际上存在三个致命问题:
- 惩罚力度失衡:碰撞惩罚(-20)远大于吃到食物的奖励(+10),导致AI更倾向于避免风险而非追求奖励
- 奖励稀疏:只有在吃到食物时才给予实质性奖励,中间过程缺乏有效引导
- 局部最优陷阱:绕圈行为实际上规避了所有惩罚,成为了AI的"舒适区"
1.2 探索与利用的两难境地
在强化学习中,epsilon-greedy策略用于平衡探索(尝试新动作)与利用(执行已知最佳动作)的关系。我的初始设置是:
epsilon = 0.1 # 10%概率随机探索但实际训练中出现了两个意外情况:
- 当epsilon值过高时,AI会做出太多随机动作,难以形成有效策略
- 当epsilon值过低时,AI会过早陷入局部最优(比如绕圈行为)
更糟糕的是,我后来发现由于奖励函数设计不当,随机探索得到的负面反馈远多于正面反馈,这实际上是在惩罚探索行为。
2. 诊断AI"摆烂"行为的实用方法
当你的AI开始表现出奇怪行为时,不要急着调整参数,先建立系统的诊断流程。我总结了一套"AI行为病理学"检查表:
2.1 行为模式分析矩阵
| 行为模式 | 可能原因 | 检查方法 |
|---|---|---|
| 原地转圈 | 规避碰撞惩罚 | 检查碰撞惩罚是否过重 |
| 直线撞墙 | 缺乏方向感知 | 验证状态表示是否包含方位信息 |
| 忽视食物 | 奖励信号太弱 | 对比吃到食物的奖励与其他奖惩值 |
| 随机游走 | 探索率过高 | 检查epsilon值衰减曲线 |
2.2 可视化诊断工具
我在训练过程中添加了几个关键可视化组件:
# 奖励成分分解可视化 plt.figure(figsize=(10,4)) plt.subplot(1,2,1) plt.plot(food_rewards, label='Food') plt.plot(collision_penalties, label='Collision') plt.plot(movement_rewards, label='Movement') plt.legend() # Q值变化热力图 plt.subplot(1,2,2) plt.imshow(q_values.reshape(4,3), cmap='hot') plt.colorbar() plt.show()这些可视化工具帮助我发现了一个关键现象:在绕圈行为期间,所有动作的Q值都非常接近,说明AI其实并不清楚哪个动作更好,只是选择了一个"不太坏"的策略。
3. 奖励函数设计的艺术与科学
经过多次迭代,我总结出设计高效奖励函数的几个核心原则:
3.1 分层奖励系统
好的奖励函数应该像游戏设计一样,提供即时的行为反馈和长期的目标引导。我最终采用的奖励结构是:
def get_reward_v2(self, done, distances): reward = 0 # 基础生存奖励(鼓励长时间存活) reward += 0.01 if done: # 碰撞惩罚与蛇长度负相关(长蛇更应避免碰撞) reward -= 10 * (1 - 0.9 ** self.snake.length) elif self.snake.get_head_position() == self.food.position: # 食物奖励与蛇长度正相关(鼓励长蛇继续吃) reward += 15 * (1.1 ** (self.snake.length - 3)) else: # 动态距离奖励(考虑相对位置变化) dir_reward = self._get_directional_reward() reward += dir_reward * 0.5 return reward这个版本引入了几个关键改进:
- 生存奖励:每步给予微小正奖励,鼓励延长存活时间
- 动态惩罚:碰撞惩罚随蛇身变长而减轻(长蛇更难避免碰撞)
- 成长性奖励:吃到食物奖励随蛇身增长而增加
3.2 方向性奖励的精细设计
为了更精确地引导AI寻找食物,我添加了基于相对位置的方向奖励:
def _get_directional_reward(self): head = self.snake.get_head_position() food = self.food.position dx = food[0] - head[0] dy = food[1] - head[1] # 计算当前移动方向与食物方向的夹角 move_dir = self.snake.direction if move_dir == (0, -1): current_angle = 0 # 上 elif move_dir == (0, 1): current_angle = 180 # 下 elif move_dir == (-1, 0): current_angle = 90 # 左 else: current_angle = 270 # 右 food_angle = np.degrees(np.arctan2(dy, dx)) % 360 angle_diff = min(abs(current_angle - food_angle), 360 - abs(current_angle - food_angle)) # 角度差越小奖励越高(最高1,最低-0.2) return 1 - angle_diff / 150这个函数会根据蛇头移动方向与食物方向的夹角给予连续奖励,有效解决了原始版本中"非黑即白"的奖励机制问题。
4. 训练策略的进阶技巧
要让AI贪吃蛇真正成为游戏高手,还需要一些训练策略上的优化。以下是几个经过实战验证的技巧:
4.1 课程学习(Curriculum Learning)
我采用了分阶段训练策略:
初学者阶段:
- 缩短蛇身长度(初始长度=2)
- 增大食物出现频率
- 设置较高的探索率(epsilon=0.3)
中级阶段:
- 恢复标准蛇身长度(初始长度=3)
- 加入身体避障奖励
- 逐步降低探索率(epsilon从0.2线性衰减到0.05)
专家阶段:
- 增加地图复杂度(添加障碍物)
- 引入时间惩罚(鼓励快速吃食物)
- 保持低探索率(epsilon=0.02)
4.2 经验回放优化
标准的经验回放会随机采样记忆,但我发现对某些关键经验需要特殊处理:
class PriorityReplayBuffer: def __init__(self, capacity=10000): self.capacity = capacity self.buffer = [] self.priorities = [] def add(self, experience): if len(self.buffer) >= self.capacity: # 移除优先级最低的经验 idx = np.argmin(self.priorities) self.buffer.pop(idx) self.priorities.pop(idx) self.buffer.append(experience) # 新经验默认高优先级 self.priorities.append(max(self.priorities, default=1)) def sample(self, batch_size): # 按优先级采样 probs = np.array(self.priorities) / sum(self.priorities) indices = np.random.choice(len(self.buffer), batch_size, p=probs) samples = [self.buffer[idx] for idx in indices] return samples def update_priority(self, idx, td_error): # 根据时序差分误差更新优先级 self.priorities[idx] = abs(td_error) + 0.01 # 避免零优先级这种优先级回放机制确保那些"意外成功"或"严重失败"的经验能被更频繁地学习。
4.3 多智能体竞争训练
一个有趣的发现是,同时训练多个AI蛇并让它们共享经验可以显著提升性能:
class MultiAgentTrainer: def __init__(self, num_agents=4): self.agents = [SnakeAI() for _ in range(num_agents)] self.global_buffer = PriorityReplayBuffer(20000) def train_round(self): # 并行收集经验 all_experiences = [] for agent in self.agents: experiences = self.run_episode(agent) all_experiences.extend(experiences) # 更新全局经验池 for exp in all_experiences: self.global_buffer.add(exp) # 从全局池采样训练所有智能体 batch = self.global_buffer.sample(512) for agent in self.agents: agent.train_on_batch(batch)这种方法不仅加快了训练速度,还增加了策略的多样性,因为不同智能体会探索游戏状态空间的不同区域。