基于深度强化学习的《城市:天际线2》AI玩家:从视觉感知到决策执行
1. 项目概述:一个纯粹基于视觉的《城市:天际线2》AI玩家
如果你玩过《城市:天际线》系列,就知道这游戏有多“上头”——规划路网、划分区域、平衡收支、应对灾难,每一个决策都环环相扣。但你想过没有,如果让一个AI来玩这个游戏,它会怎么操作?是能建出一个高效运转的乌托邦,还是会把城市搞成一团糟的交通地狱?
今天要聊的这个项目sfatkhutdinov/cities-skylines-2,就试图回答这个问题。它不是一个游戏Mod,也不是一个作弊脚本,而是一个完全基于深度强化学习(Reinforcement Learning)的自主智能体。最酷的地方在于,这个AI和我们人类玩家一样,只能通过“看”屏幕来获取信息,然后通过模拟键盘和鼠标操作来玩游戏。它没有访问游戏内部数据或API的后门,这意味着它面对的是一个和我们几乎完全相同的、充满不确定性的“黑盒”世界。
这个项目的核心挑战在于,它需要将高维的、非结构化的屏幕像素信息,转化为一系列有意义的决策,比如“这里该修一条路”、“那片区域应该规划为工业区”。这比下围棋或打星际争霸要复杂得多,因为游戏状态是连续且高度动态的,奖励信号(比如城市繁荣度、市民满意度)也延迟且稀疏。项目采用了近端策略优化(PPO)算法作为主干,并引入了记忆增强网络和分层智能体架构等高级技术,来应对这些挑战。对于机器学习从业者、游戏AI爱好者,或者任何对“AI如何理解并操作复杂模拟环境”感兴趣的人来说,这个项目都是一个绝佳的研究案例和实战模板。
2. 技术架构深度解析:从像素到决策的完整流水线
这个项目的架构设计清晰地反映了一条数据处理流水线:感知(看屏幕)→ 理解(分析画面)→ 决策(选择动作)→ 执行(操作游戏)→ 学习(根据结果优化)。下面我们来拆解每个核心模块的设计思路和实现考量。
2.1 环境模块:搭建AI与游戏世界的桥梁
环境模块是智能体与《城市:天际线2》交互的唯一通道。它的设计必须解决几个关键问题:如何高效、稳定地捕获游戏画面?如何将画面转化为有意义的观测?如何可靠地模拟人类输入?
核心组件与设计逻辑:
environment/core: 定义了环境的基础接口。这里采用了类似OpenAI Gym的范式,提供了reset()、step(action)、render()等标准方法。这样做的好处是智能体算法可以相对独立于具体环境,便于迁移和测试。environment/optimized_capture.py: 屏幕捕获的性能至关重要。直接使用PIL.ImageGrab或mss库进行全屏抓取在高速训练中会成为瓶颈。这里的优化可能包括:- 区域捕获:只捕获游戏窗口区域,而非整个屏幕。
- 分辨率缩放:捕获高分辨率画面,但立即下采样到模型输入所需尺寸(如224x224),减少数据传输量。
- 帧率控制:与游戏帧率同步,避免无意义的重复捕获。代码中可能实现了自适应等待逻辑,确保每次捕获的都是游戏更新后的新帧。
environment/input: 模拟键盘鼠标输入。这里不能简单发送按键信号,因为游戏可能有输入延迟或状态检测。稳健的实现会包含:- 动作空间映射:将智能体输出的抽象动作(如“在坐标(x,y)处点击”)转化为具体的
pyautogui或pydirectinput命令。 - 随机延迟注入:在操作间加入微小、随机的延迟,使AI行为更接近人类,避免因操作过快被游戏视为异常。
- 操作确认:在执行关键操作(如点击菜单按钮)后,通过短暂捕获后续画面,检查预期结果(如菜单是否弹出)是否出现,实现简单的闭环验证。
- 动作空间映射:将智能体输出的抽象动作(如“在坐标(x,y)处点击”)转化为具体的
environment/visual_metrics.py: 这是项目的“眼睛”和“记分牌”。由于无法直接读取游戏内部的金钱、人口数据,奖励必须从视觉变化中推导。这可能涉及:- 区域颜色直方图分析:通过分析屏幕特定区域(如资金显示栏、人口计数器)的颜色和像素变化,估算数值的增减。
- 建筑与道路的视觉识别:使用传统的计算机视觉方法(如模板匹配、特征检测)或轻量级神经网络,识别画面中新建的建筑、道路,从而给予相应的建造奖励。
- 灾难与问题检测:通过检测画面中出现的红色警报图标、火灾烟雾、交通深红色路段等,给予负奖励。
实操心得:视觉奖励设计的“坑”设计视觉奖励是此类项目最棘手的部分之一。一个常见的陷阱是奖励“欺骗”。例如,如果奖励单纯基于画面中绿色区域(代表绿化)的扩大,AI可能会学会不停地在地图上种树,而不是发展城市。因此,奖励函数必须是多目标、均衡且稀疏的。好的做法是结合短期密集奖励(如成功完成一个点击操作)和长期稀疏奖励(如持续一段时间内城市规模稳步增长),并加入一些约束条件(如预算平衡)。
2.2 智能体与模型模块:大脑的构造
智能体模块是项目的“大脑”,负责根据观测做出决策。项目采用了PPO算法,这是一种在复杂环境中表现稳定、易于调参的强化学习算法。
PPO智能体的核心工作流程:
- 观察:接收环境传来的预处理后的图像观测。
- 推理:策略网络(Policy Network)根据观测,输出一个动作的概率分布。
- 行动:根据这个分布采样一个具体动作,交给环境执行。
- 评估:价值网络(Value Network)评估当前状态的价值,用于计算优势函数。
- 学习:收集一定量的经验(状态、动作、奖励序列)后,PPO的更新器会计算策略梯度,并采用“裁剪”机制,确保每次策略更新幅度不会过大,从而保持训练稳定性。
模型架构的演进与补充:基础的CNN网络(model/optimized_network.py)可能不足以理解《城市:天际线2》这种复杂场景。因此项目引入了更高级的架构:
model/visual_understanding_network.py: 可能集成了注意力机制(Attention)或场景图(Scene Graph)的概念。例如,网络不仅识别物体,还尝试理解关系:“住宅区”旁边有“公园”是好的,但紧挨着“重工业区”就是坏的。这能让AI学会更符合常识的城市规划。memory/memory_augmented_network.py: 城市发展是一个长期过程。今天修的一条路,可能会在100个游戏时间单位后造成交通拥堵。标准RL智能体具有马尔可夫性,记忆很短。记忆增强网络(如引入LSTM或外部记忆模块)允许AI记住过去的关键事件和决策,从而做出更有远见的规划。例如,它能“记得”上次在河流北侧发展工业导致了污染,这次可能会选择南侧。agent/hierarchical_agent.py: 分层智能体是解决复杂动作空间的利器。高层策略(Manager)制定宏观目标,如“现阶段优先发展工业”。底层策略(Worker)执行具体动作,如“移动鼠标到A点,点击道路工具,拖拽到B点”。这种结构大大降低了学习难度,模仿了人类“先定战略,再搞战术”的思考方式。
2.3 训练与支持系统:让学习过程可持续
训练一个游戏AI是计算密集型和时间密集型的,可能需要在GPU上运行数天甚至数周。因此,健壮的训练和支持系统必不可少。
training/checkpointing.py: 定期保存模型检查点不仅是断点续训的基础,也是评估不同阶段模型性能的关键。好的检查点策略会同时保存模型参数、优化器状态、当前训练步数和最佳奖励记录。training/signal_handlers.py: 处理Ctrl+C等中断信号。当用户想终止训练时,该模块应确保当前经验缓存被妥善保存,模型检查点已更新,避免数据丢失。utils/performance_safeguards.py: 这是系统的“看门狗”。它可能监控:- GPU内存:在即将溢出前,主动清理缓存或降低批次大小。
- 游戏进程:检测游戏是否无响应或崩溃,并尝试自动重启游戏和恢复训练环境。
- 训练稳定性:如果连续多个回合奖励为0或出现NaN,自动暂停训练并发出警报。
utils/hardware_monitor.py: 持续记录CPU/GPU利用率、温度、内存占用等。这些日志对于后期性能分析和瓶颈定位至关重要。例如,你可能会发现训练瓶颈不在神经网络前向传播,而在屏幕捕获的I/O延迟上。
3. 从零开始:环境搭建与首次运行实录
理解了架构,我们来看看如何亲手把这个AI跑起来。以下步骤基于项目README,并补充了大量实践中可能遇到的细节。
3.1 系统准备与依赖安装
硬件与软件基线:
- 操作系统:Windows 10/11是必须的,因为游戏本身和某些底层截图库(如
dxcam)对Windows支持最好。 - GPU:项目推荐RTX 3080 Ti,这暗示了训练对显存和算力的高需求。实测中,RTX 3060 12GB或以上显存的显卡是起步门槛。纯CPU训练在如此复杂的视觉任务上几乎不可行。
- 游戏:你需要一份正版《城市:天际线2》。确保游戏已更新到稳定版本,并以窗口化或无边窗口化模式运行,这是屏幕捕获能准确定位游戏窗口的前提。
Python环境搭建(详细步骤):
克隆代码与创建虚拟环境:
git clone https://github.com/sfatkhutdinov/cities-skylines-2.git cd cities-skylines-2 # 强烈建议使用conda管理环境,便于处理复杂的深度学习依赖 conda create -n cs2_agent python=3.10 conda activate cs2_agent安装依赖:
pip install -r requirements.txt如果安装过程中出现错误,很可能是某些包(如
torch、torchvision)需要特定版本。一个更稳妥的方法是先手动安装PyTorch:# 前往 https://pytorch.org/ 根据你的CUDA版本获取安装命令 # 例如,对于CUDA 11.8 conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia然后再安装
requirements.txt中的其他包。解决潜在的库冲突:
pyautogui和pydirectinput:后者通常能提供更底层、更可靠的游戏输入模拟。如果遇到游戏不响应pyautogui点击的情况,可以尝试在代码中切换到pydirectinput。- OpenCV (
cv2):确保安装的是opencv-python-headless,避免在无GUI的服务器环境下出现问题。
3.2 配置调整与Mock环境测试
在让AI接触真实游戏前,强烈建议先在Mock环境中进行测试。
理解配置文件: 查看
config/example_config.json或config/training_config.py。你需要关注几个关键参数:observation_shape: 输入图像的大小,如[3, 224, 224](通道,高,宽)。调小可以加速训练,但会损失信息。action_space: 定义了AI能做什么。可能是离散动作(如0: 点击道路工具,1: 点击住宅区工具...)或连续动作(鼠标移动坐标)。reward_config: 奖励函数的权重,这是调参的核心。初期可以保持默认,后期根据AI行为进行精细调整。
运行Mock环境测试:
python src/tests/test_mock_environment.py这个脚本会模拟一个简化的城市建造游戏。你应该能看到终端输出测试通过的信息,并可能在
output/目录下生成一些测试画面。这一步至关重要,它验证了智能体、环境逻辑、训练循环的基础功能是否正常,而无需处理真实游戏的不稳定性。首次启动训练(Mock模式):
python src/train.py --mock_env --num_episodes=50 --max_steps=200 --render--mock_env: 使用模拟环境。--render: 打开一个可视化窗口,实时观看AI的“决策”过程(在Mock环境中,这通常是一些简单的图形绘制)。- 观察控制台输出的奖励值。在Mock环境中,一个设计良好的智能体应该能很快学会获得正奖励。如果奖励始终为负或为零,可能需要回头检查动作空间或奖励函数的逻辑。
3.3 连接真实游戏:关键步骤与调试
这是最具挑战性的一步,因为真实游戏环境充满噪声和不确定性。
游戏设置:
- 将《城市:天际线2》设置为1080p分辨率、无边窗口化模式。
- 关闭游戏内的动态模糊、景深等后期效果,减少画面噪点,便于视觉处理。
- 将游戏UI缩放调整到100%,确保UI元素位置固定。
环境校准: 项目需要知道游戏窗口在屏幕上的确切位置。你可能需要修改
environment/core中的代码,加入一个窗口定位函数。一个简单的方法是使用pygetwindow库:import pygetwindow as gw windows = gw.getWindowsWithTitle('Cities: Skylines II') if windows: game_window = windows[0] # 将窗口移动到固定位置并聚焦 game_window.moveTo(0, 0) game_window.activate()运行
python scripts/run_environment.py(如果存在)或写一个简单的测试脚本,确保能稳定捕获到游戏窗口的画面。首次真实训练:
python src/train.py --num_episodes=10 --max_steps=100 --checkpoint_dir=my_first_run- 不要开
--render:在首次运行时,渲染会消耗大量资源,可能加剧不稳定性。 - 保持观察:密切注视游戏窗口。AI的初始动作是随机的,可能会疯狂点击屏幕各处。你的目标是观察:
- 屏幕捕获是否稳定?(画面是否卡住不动?)
- 输入模拟是否生效?(游戏角色是否在移动、点击?)
- 游戏是否崩溃?(这是初期最常见的问题)。
- 不要开
奖励函数验证: 在最初的几个回合,AI的奖励可能毫无规律。你需要检查
environment/rewards的计算逻辑。可以在代码中添加调试输出,打印出每一帧计算奖励所依据的视觉特征(如“检测到资金区域颜色变化值:X”),确保奖励信号与你的直观感受相符。
4. 核心训练流程与参数调优实战
当AI能在真实游戏环境中存活并产生一些基础交互后,真正的挑战——训练一个有效的策略——才刚刚开始。
4.1 训练循环的完整拆解
training/trainer.py中的主循环是学习发生的核心。一个标准的PPO训练迭代包含以下阶段:
数据收集阶段:
- 智能体使用当前的策略网络与环境交互N个时间步(例如,2048步),收集大量的
(状态, 动作, 奖励, 下一个状态)元组。 - 在这个过程中,重要性采样权重和优势估计会被计算并存储。优势估计
A_t告诉智能体,某个动作比平均情况好多少,它是计算策略梯度的关键。
- 智能体使用当前的策略网络与环境交互N个时间步(例如,2048步),收集大量的
策略优化阶段:
- 利用收集到的一批数据,对策略网络和价值网络进行K次(例如,10次)小批量(Mini-batch)更新。
- PPO-Clip的核心操作:计算新旧策略的概率比
r_t(θ),然后通过一个裁剪函数clip(r_t(θ), 1-ε, 1+ε)限制其变化范围(ε通常为0.1-0.2)。这避免了因单次更新过大而导致策略崩溃。 - 损失函数是策略损失、价值函数损失和熵奖励的加权和。熵奖励鼓励探索,防止策略过早收敛到次优解。
评估与记录阶段:
- 每隔一定回合,使用当前策略运行一个完整的评估回合(不进行探索,只按最优动作执行)。
- 记录评估回合的总奖励、回合长度等指标,并保存到TensorBoard或W&B日志中。
4.2 超参数调优:从玄学到科学
强化学习的性能对超参数极其敏感。项目中的scripts/hyperparameter_tuning.py提供了网格搜索或随机搜索的工具。以下是一些关键参数及其影响:
| 超参数 | 典型范围/值 | 影响 | 调优建议 |
|---|---|---|---|
| 学习率 (lr) | 3e-4 到 1e-5 | 控制参数更新步长。太大导致不稳定,太小学习慢。 | 从3e-4开始,如果训练曲线剧烈震荡,尝试降至1e-4或5e-5。 |
| 折扣因子 (gamma) | 0.99 到 0.999 | 未来奖励的衰减率。越接近1,智能体越有远见。 | 对于《城市天际线》这种长期规划游戏,建议使用0.99或更高。 |
| GAE参数 (lam) | 0.9 到 0.98 | 用于平衡优势估计的偏差与方差。 | 通常设为0.95,这是一个经验值,变动影响相对较小。 |
| 裁剪范围 (clip_epsilon) | 0.1 到 0.3 | PPO裁剪的范围。限制策略更新幅度。 | 默认0.2是安全的起点。如果发现学习停滞,可以尝试略微放大到0.3以允许更大更新。 |
| 熵系数 (ent_coef) | 0.01 到 0.001 | 鼓励探索的奖励权重。 | 训练初期可设高些(0.01),后期逐渐衰减或降低,以利于策略收敛。 |
| 每批数据更新次数 (update_epochs) | 4 到 10 | 对同一批数据重复利用进行更新的次数。 | 增加次数可以提高数据利用率,但可能导致过拟合。通常4-8次足够。 |
| 批次大小 (batch_size) | 32 到 512 | 每次参数更新时使用的样本数。 | 在GPU内存允许范围内尽可能设大,有助于稳定训练。 |
实操心得:调参的“第一性原理”不要盲目进行网格搜索,那会耗费巨量计算资源。首先,固定其他参数,只调整学习率,运行几个短周期(如1000步),观察损失函数是否平稳下降。找到稳定的学习率后,再微调
clip_epsilon和ent_coef。记住,强化学习的训练曲线本来就是充满噪声的,要看长期趋势,而不是短期的上下波动。使用scripts/dashboard.py实时监控多个运行实例的对比,是最高效的调参方式。
4.3 利用记忆与分层结构加速学习
当基础PPO智能体学会一些简单操作后,可以尝试启用更高级的功能。
启用记忆增强智能体:修改配置文件或训练命令,指定使用memory_agent.py。你需要额外配置记忆模块的大小和读写机制。训练初期,你可能会发现学习速度变慢,因为网络需要同时学习策略和记忆管理。但训练中后期,当任务需要依赖历史信息时(比如“避免在同一个容易淹水的地方反复修建”),它的优势就会体现出来。
启用分层智能体:这是应对《城市天际线2》庞大动作空间的利器。你需要:
- 在
config/action_space.py中明确定义高层动作(目标)和底层动作(执行)。 - 配置
hierarchical_trainer.py,它需要协调两个策略网络(Manager和Worker)的训练节奏。 - 高层策略的奖励通常更稀疏、更长期(如“城市人口增长”),而底层策略的奖励更密集、更即时(如“成功放置一个建筑”)。
一个有效的策略是分阶段训练:先预训练底层策略,让它熟练掌握各种工具的基本操作(如画路、分区);然后再联合训练高层策略,让高层学会在合适的时机调用这些底层技能。
5. 常见问题排查与性能优化指南
在实际运行中,你一定会遇到各种问题。下面是一些典型问题及其排查思路。
5.1 环境交互类问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 游戏画面捕获为黑屏或静止 | 1. 游戏窗口未激活或最小化。 2. 屏幕捕获库与游戏渲染模式(DX11/DX12/Vulkan)不兼容。 3. 捕获区域坐标错误。 | 1. 确保游戏以无边窗口化运行,并在脚本中添加game_window.activate()。2. 尝试更换截图库(如从 PIL.ImageGrab换为mss或dxcam)。dxcam对DirectX游戏捕获效率极高。3. 打印捕获区域的坐标和尺寸,与游戏窗口实际位置核对。 |
| 鼠标/键盘输入不被游戏响应 | 1. 游戏以管理员权限运行,而Python脚本没有。 2. 防病毒软件或游戏反作弊系统拦截。 3. pyautogui的兼容性问题。 | 1. 尝试以管理员身份运行你的Python脚本。 2. 暂时关闭防病毒软件实时防护,或将脚本加入白名单。对于单机游戏,通常可关闭反作弊。 3. 换用 pydirectinput库,它模拟的输入更底层。 |
| 游戏频繁崩溃或无响应 | 1. AI操作速度过快,游戏逻辑处理不过来。 2. 内存/显存泄漏。 3. 执行了非法操作(如在地图外点击)。 | 1. 在environment/input中为每个操作增加随机延迟(如0.1-0.3秒)。2. 使用 utils/hardware_monitor.py监控资源占用。在长时间训练前,设置定期重启游戏的逻辑。3. 在动作执行前,加入简单的边界检查,确保鼠标坐标在游戏窗口内。 |
5.2 训练过程类问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 奖励始终为零或极低,没有上升趋势 | 1. 奖励函数设计有误,智能体无法获得正向反馈。 2. 动作空间过大或无效动作太多。 3. 探索率不足,智能体困在局部。 | 1.简化奖励:先从“成功点击一次按钮”就给小奖励开始,确保智能体能学会最简单的交互。 2.限制动作空间:初期只允许几个核心动作(如移动镜头、点击建造菜单)。 3.大幅提高熵系数,或使用像好奇心驱动(Curiosity-driven)这样的内在奖励机制。 |
| 训练曲线剧烈震荡(奖励大起大落) | 1. 学习率过高。 2. 批次大小太小。 3. 梯度爆炸。 | 1.降低学习率,这是最常见的原因。 2.增大批次大小,如果显存允许。 3. 在优化器中加入梯度裁剪。 |
| 训练后期性能突然崩溃 | 1. 策略更新步长太大,导致策略“遗忘”。 2. 探索率降为0后,策略陷入局部最优且无法跳出。 | 1.减小clip_epsilon,限制每次更新的幅度。2. 不要将熵系数衰减至0,保留一个极小值(如1e-5)维持微弱的探索。 |
5.3 性能与资源优化
- 启用混合精度训练:如果你的GPU支持(如RTX系列),使用
--mixed_precision参数可以显著减少显存占用并加快训练速度,通常性能提升30%-50%。 - 优化观测预处理:在
environment/optimized_capture.py中,将捕获的图像立即从BGR转为RGB,并缩放、归一化,这个处理流程应放在GPU上进行(如果使用PyTorch),避免在CPU和GPU间频繁传输数据。 - 使用
benchmark_agent.py:定期进行基准测试,对比不同配置(如不同网络架构、是否启用记忆)下的每秒帧数(FPS)和平均奖励,用数据驱动优化决策。 - 分布式训练探索:对于超大规模调参,可以考虑使用
Ray或ACME框架进行分布式训练,同时在多组参数上并行探索。
这个项目就像一座桥梁,连接了前沿的深度强化学习研究与充满乐趣的游戏世界。它最大的价值不在于最终能否训练出一个比人类更优秀的市长,而在于提供了一个完整的、可复现的框架,让我们能够探索AI在复杂、开放环境中的感知、决策与学习能力。每一个你为解决上述问题而做出的调试和优化,都是对智能体如何理解世界的一次深刻实践。
