[TouhouRL]1. The Begining of Touhou Project RL,从简单贪吃蛇游戏的侵入式强化学习开始

贪吃蛇 RL 项目笔记

这个项目用的算法叫 DQN

游戏本体可以说相当简单,这里制作了一个简单的游戏

项目目录为:

TouhouRL 致力于开发一个自己玩东方的强化学习模块


大概是怎么运作的

复制代码
游戏环境  ──状态 s──►  Agent(神经网络)──动作 a──►  游戏环境
    ▲                                                    │
    └──────────────── 奖励 r + 新状态 s' ◄──────────────┘

每一帧,Agent 看一眼当前局面(状态 s),决定往哪走(动作 a),

游戏告诉它结果怎样(奖励 r)以及现在的新局面(s')。

这个循环不断重复,Agent 慢慢摸索出哪些决策能得高分。

训练早期 Agent 完全是乱走的,撞墙、绕圈、无视食物------这很正常,

它需要先积累足够多的"失败经验"才能开始学到东西。


文件结构

复制代码
First Game/
├── The Game/
│   └── snake.py          # 原版游戏,给人玩的
├── RL/
│   ├── env.py            # 游戏逻辑,剥掉了渲染部分
│   ├── model.py          # 神经网络
│   ├── agent.py          # DQN 的核心:决策、记忆、训练
│   ├── train.py          # 跑训练
│   └── play.py           # 看训练好的 AI 玩
├── RL_GUIDE.md
└── requirements.txt

env.py --- 把游戏变成训练接口

原版 snake.py 是给人玩的,依赖 curses 画面、等键盘输入。

训练 AI 的时候每秒要跑几千步,不需要也不能有这些东西。

所以要把游戏逻辑单独抽出来,对外只暴露三个方法:

python 复制代码
env = SnakeEnv(width=20, height=20)

state = env.reset()                          # 开新局,返回初始状态
state, reward, done = env.step(action)       # 走一步
env.render()                                 # 调试用,打印到终端

这个接口设计参考了 OpenAI Gym 的规范,以后如果想换别的游戏,

只要 env 的接口一样,agent 和 train 的代码完全不用动。

状态怎么表示

状态是 Agent 的"感知",设计得好不好直接决定学习难度。

这里用 11 个 0/1 的数字描述当前局面:

复制代码
危险感知(相对于当前朝向):
  [0] 正前方有没有墙或蛇身
  [1] 右边有没有
  [2] 左边有没有

当前朝向(one-hot):
  [3] 朝上
  [4] 朝下
  [5] 朝左
  [6] 朝右

食物方位(相对于蛇头):
  [7] 食物在左
  [8] 食物在右
  [9] 食物在上
  [10] 食物在下

为什么不直接把整个棋盘喂给网络?

可以,但那样输入维度是 20×20=400,网络要大很多,训练也慢很多。

11 维这个设计已经包含了做决策所需的全部关键信息,够用了。

奖励怎么给

复制代码
吃到食物   +10
撞墙/撞自己  -10
每步存活    0(也可以给 -0.01,逼它快点找食物)

奖励设计是 RL 里最需要动脑子的部分。

给太密集(每步都有奖励)容易让 Agent 学到奇怪的捷径;

给太稀疏(只有吃到食物才有奖励)前期很难学,因为随机走很难碰到食物。

这套设计是个比较平衡的起点。


model.py --- 网络结构

用最简单的全连接网络(MLP)就够了:

复制代码
输入:11 维状态向量
  ↓
全连接层 256,ReLU
  ↓
全连接层 256,ReLU
  ↓
输出:3 个 Q 值(对应 3 个动作)

输出是 3 而不是 4,因为动作用的是相对方向 :直走、左转、右转。

蛇本来就不能掉头,用相对方向比绝对方向(上下左右)少一个无效动作,学起来更干净。


agent.py --- DQN 的核心

Q 值是什么

Q(s, a) 是一个预测值,表示"在状态 s 下选动作 a,从现在开始能拿到多少总奖励"。

网络学会准确预测 Q 值之后,每步只要选 Q 值最大的动作就行了。

经验回放

Agent 不是走一步就立刻学一步,而是把每步的经历存起来:

python 复制代码
memory.append((state, action, reward, next_state, done))

训练时从这个"记忆池"里随机抽一批出来学。

这样做有两个好处:

  1. 同一段经历可以被反复学习,数据利用率高
  2. 随机采样打破了时序相关性,梯度更新更稳定

记忆池满了就覆盖最老的记录,通常设 10 万条左右。

ε-greedy:探索与利用的平衡

训练初期 Agent 什么都不懂,如果一直选"当前最优"动作,

它会很快陷入一个局部策略出不来。所以需要随机探索。

复制代码
以概率 ε   → 随机选一个动作(探索)
以概率 1-ε → 选 Q 值最大的动作(利用)

ε 从 1.0 开始(完全随机),随着训练进行慢慢降到 0.01(基本不随机)。

前期多探索积累经验,后期多利用已学到的知识。

目标网络

DQN 有个经典的稳定性问题:训练目标(目标 Q 值)和被训练的网络是同一个,

每次更新网络,目标也跟着变,像追一个移动的靶子,容易震荡。

解决方法是用两个网络:

  • 主网络:每步都在更新
  • 目标网络:每隔 N 步从主网络复制一次,其余时间冻结

目标 Q 值用目标网络算,这样训练目标在一段时间内是固定的,稳多了。

Bellman 方程(训练公式)

复制代码
目标 Q = r  +  γ × max_a'[ Q_target(s', a') ]

损失 = MSE( Q_main(s, a),  目标 Q )
  • r 是这步拿到的即时奖励
  • γ(gamma)是折扣因子,通常 0.9,意思是"未来的奖励没有现在的值钱"
  • max_a' 是在下一个状态里,目标网络认为最好的动作的 Q 值

用梯度下降最小化这个损失,主网络的预测就会越来越准。


train.py --- 训练主循环

逻辑很直白:

python 复制代码
for episode in range(总局数):
    state = env.reset()
    while True:
        action = agent.choose_action(state)   # ε-greedy
        next_state, reward, done = env.step(action)
        agent.remember(state, action, reward, next_state, done)
        agent.train_step()                    # 从记忆池采样,跑一次反向传播
        state = next_state
        if done:
            break
    agent.decay_epsilon()
    # 每 50 局打印一次平均分,每 200 局保存一次模型

训练过程中要记录每局的分数,画成曲线看趋势。

分数不是单调上升的,会有很大波动,看的是整体趋势有没有在涨。


play.py --- 看 AI 玩

训练完之后加载保存的权重,关掉随机探索(ε=0),

env.render() 把每一帧打印到终端,加个 time.sleep(0.1) 控制速度,

就能看到 AI 实时玩游戏了。


依赖

复制代码
torch>=2.0
numpy
matplotlib
bash 复制代码
conda activate TouhouSSL
pip install torch numpy matplotlib

PyTorch 如果有 GPU 会自动用,没有也没关系,这个规模 CPU 跑得动。


训练大概会经历这几个阶段

0~100 局 :完全在乱走,基本上一出生就撞墙,分数接近 0。

这是正常的,记忆池还没积累够,网络在学随机噪声。

100~500 局 :开始有点方向感,会朝食物走,但经常绕进死角出不来。

ε 还比较高,还在大量探索。

500~2000 局 :能稳定吃到食物了,蛇变长之后偶尔会撞自己。

这个阶段进步最明显,曲线涨得比较快。

2000 局以后 :趋于稳定,能处理大多数情况,但极端情况(蛇很长时)还是会出问题。

DQN 在这类问题上有天花板,想突破需要换更复杂的算法(PPO、A3C 等)。


超参数参考

python 复制代码
LEARNING_RATE = 0.001
GAMMA         = 0.9
EPSILON_START = 1.0
EPSILON_MIN   = 0.01
EPSILON_DECAY = 0.995   # 每局结束后乘这个数
MEMORY_SIZE   = 100_000
BATCH_SIZE    = 1000
TARGET_UPDATE = 100     # 每隔多少步同步目标网络

这些不是最优解,只是一个能跑起来的起点。

调参本身也是学习的一部分,改一个参数跑一遍,看曲线有什么变化,

比看任何教程都学得快。


动手顺序

先写 env.py,用 env.render() 手动走几步确认逻辑没问题,

再写 model.pyagent.py,最后接 train.py

不要一上来就想把所有东西写完再跑,每写一个文件就测一下,

出问题的时候范围小,好排查。

相关推荐
da_vinci_x1 天前
告别“纸片树冠”:SpeedTree 10的次世代 Nanite 植被透射与程序化季相重构工作流
游戏·3d·重构·aigc·材质·技术美术·游戏策划
FairGuard手游加固1 天前
当明枪遭遇暗箭:射击游戏安全攻防战
人工智能·安全·游戏
黑客说1 天前
《白日梦:无限世界》:一款游戏,定义“无限流”的沉浸式新形态
游戏
张老师带你学1 天前
unity道具,健身房资源
科技·游戏·unity·游戏引擎·模型
开维游戏引擎1 天前
开维游戏引擎实例:五子棋
javascript·游戏·html·游戏引擎·ai编程
Swift社区1 天前
为什么游戏公司不愿意开源经典游戏
游戏·开源
wanhengidc1 天前
什么是高性能计算服务器?
大数据·运维·服务器·游戏·智能手机
yuguo.im1 天前
91 行代码实现一个打飞机游戏(HTML5 Canvas 版)
前端·游戏·html5·打飞机
张老师带你学1 天前
unity道具,哑铃架+天文望远镜,一边运动一边观星
科技·游戏·unity·模型·游戏美术
宝贝儿好2 天前
【强化学习实战】第十一章:Gymnasium库的介绍和使用(1)、出租车游戏代码详解(Sarsa & Q learning)
人工智能·python·深度学习·算法·游戏·机器学习